Compare commits

...

10 Commits

Author SHA1 Message Date
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
38 changed files with 1097 additions and 287 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'
+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
+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();
+29 -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"
@@ -40,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"
@@ -507,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;
}
@@ -783,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());
}
@@ -812,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.
@@ -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;
@@ -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;
@@ -107,6 +107,7 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
{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},
};
@@ -195,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;
@@ -395,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,
@@ -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:
@@ -179,6 +179,8 @@ 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,
@@ -492,6 +494,7 @@ enum class Opcode {
DS_MIN_F32,
DS_MAX_F32,
DS_SWIZZLE_B32,
DS_BPERMUTE_B32,
DS_CONSUME,
DS_APPEND,
DS_READ_I8,
@@ -137,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);
@@ -188,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);
@@ -482,6 +486,8 @@ bool IsVop1FloatSourceOpcode(Opcode opcode) {
case Opcode::V_CEIL_F16:
case Opcode::V_TRUNC_F16:
case Opcode::V_RNDNE_F16:
case Opcode::V_SIN_F16:
case Opcode::V_COS_F16:
case Opcode::V_SIN_F32:
case Opcode::V_COS_F32: return true;
default: return false;
@@ -548,6 +554,10 @@ constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_RNDNE_F16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_SIN_F16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), true},
{Opcode::V_COS_F16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), true},
{Opcode::V_CVT_U32_F32, SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(), SdwaSelFull(), false},
{Opcode::V_CVT_I32_F32, SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(), SdwaSelFull(), false},
{Opcode::V_CVT_RPI_I32_F32, SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(), SdwaSelFull(),
@@ -832,6 +842,8 @@ bool IsVop1FloatResultOpcode(Opcode opcode) {
case Opcode::V_RSQ_F16:
case Opcode::V_LOG_F16:
case Opcode::V_EXP_F16:
case Opcode::V_SIN_F16:
case Opcode::V_COS_F16:
case Opcode::V_SIN_F32:
case Opcode::V_COS_F32: return true;
default: return false;
@@ -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);
@@ -878,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:
@@ -1036,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:
@@ -109,6 +109,7 @@ private:
bool DS_APPEND_CONSUME(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
bool DS_ADDTID(const Decoder::Instruction& inst, bool write);
bool DS_SWIZZLE_B32(const Decoder::Instruction& inst);
bool DS_BPERMUTE_B32(const Decoder::Instruction& inst);
IR::F32 SelectF32(IR::U1 condition, IR::F32 true_value, IR::F32 false_value);
IR::U32 ConvertF32ToU32Saturated(IR::F32 value, float upper_bound, float safe_upper,
@@ -148,6 +149,7 @@ private:
bool quiet_snan);
bool Float16Unary(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool invalid_negative);
bool Float16Trig(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
bool Float16Binary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool reverse);
bool Float16Ternary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator,
bool mix);
@@ -365,6 +365,8 @@ bool Translator::EmitVector(const Decoder::Instruction& inst, std::string* error
case O::V_CEIL_F16: return Float16Unary(inst, IR::ValueOpcode::FPCeil32, false);
case O::V_TRUNC_F16: return Float16Unary(inst, IR::ValueOpcode::FPTrunc32, false);
case O::V_RNDNE_F16: return Float16Unary(inst, IR::ValueOpcode::FPRoundEven32, false);
case O::V_SIN_F16: return Float16Trig(inst, IR::ValueOpcode::FPSin);
case O::V_COS_F16: return Float16Trig(inst, IR::ValueOpcode::FPCos);
case O::V_MIN3_F16: return Float16Ternary(inst, IR::ValueOpcode::FPMinTri32, false, false);
case O::V_MAX3_F16: return Float16Ternary(inst, IR::ValueOpcode::FPMaxTri32, false, false);
case O::V_MED3_F16: return Float16Ternary(inst, IR::ValueOpcode::FPMedTri32, false, false);
@@ -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.
+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");
+10 -1
View File
@@ -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() {
+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
};
+19
View File
@@ -61,6 +61,7 @@ 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);
@@ -265,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 {};
@@ -371,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();
+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);
+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
+8 -1
View File
@@ -1289,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) {
@@ -1412,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();
@@ -1436,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");
+175
View File
@@ -11896,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:
@@ -12029,6 +12031,7 @@ 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:
@@ -14488,6 +14491,91 @@ TestCase VectorSpecialF16Ops() {
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
TestCase VectorCosF16CapturedSdwaAndEdges() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 4, 0x34001234u); // high=+0.25h, low sentinel
code.push_back(0x7e08c2f9u);
code.push_back(0x00051504u); // captured v_cos_f16 v4.word1, v4.word1
AppendVMovLiteral(&code, 5, 0x00008000u); // -0.0h
code.push_back(EncodeVop1(0x61, 6, Vgpr(5)));
AppendVMovLiteral(&code, 7, 0x00007bffu); // max finite
code.push_back(EncodeVop1(0x61, 8, Vgpr(7)));
AppendVMovLiteral(&code, 9, 0x00007c00u); // +inf
code.push_back(EncodeVop1(0x61, 10, Vgpr(9)));
AppendVMovLiteral(&code, 11, 0x00000001u); // minimum positive denormal
code.push_back(EncodeVop1(0x61, 12, Vgpr(11)));
AppendVMovLiteral(&code, 13, 0x00003800u); // +0.5h
code.push_back(EncodeVop1(0x61, 14, Vgpr(13)));
for (u32 i = 0; i < 6; i++) {
AppendStoreVgpr(&code, 4 + i * 2, i);
}
AppendEnd(&code);
TestCase test{"VectorCosF16CapturedSdwaAndEdges",
code,
{},
{0x00001234u, 0x00003c00u, 0x00003c00u, 0x0000fe00u,
0x00003c00u, 0x0000bc00u},
{O::V_MOV_B32, O::V_COS_F16, O::BUFFER_STORE_DWORD,
O::S_ENDPGM}};
test.decoded_counts = {{"V_COS_F16", 6},
{"V_COS_F16 v4.sdwa(sel=5,sext=0), "
"v4.sdwa(sel=5,sext=0)",
1}};
test.ir_counts = {{" = FPCos ", 6}};
test.required_spirv = {" Fract ", " Cos ", " PackHalf2x16 "};
test.forbidden_spirv = {"OpCapability Float16"};
return test;
}
TestCase VectorSinF16SdwaAndEdges() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 4, 0x34001234u); // high=+0.25h, low sentinel
code.push_back(0x7e08c0f9u);
code.push_back(0x00051504u); // v_sin_f16 v4.word1, v4.word1
AppendVMovLiteral(&code, 5, 0x00008000u); // -0.0h
code.push_back(EncodeVop1(0x60, 6, Vgpr(5)));
AppendVMovLiteral(&code, 7, 0x00007bffu); // max finite
code.push_back(EncodeVop1(0x60, 8, Vgpr(7)));
AppendVMovLiteral(&code, 9, 0x0000fbffu); // minimum finite
code.push_back(EncodeVop1(0x60, 10, Vgpr(9)));
AppendVMovLiteral(&code, 11, 0x00007c00u); // +inf
code.push_back(EncodeVop1(0x60, 12, Vgpr(11)));
AppendVMovLiteral(&code, 13, 0x0000fc00u); // -inf
code.push_back(EncodeVop1(0x60, 14, Vgpr(13)));
AppendVMovLiteral(&code, 15, 0x00003800u); // +0.5h
code.push_back(EncodeVop1(0x60, 16, Vgpr(15)));
AppendVMovLiteral(&code, 17, 0x00003a00u); // +0.75h
code.push_back(EncodeVop1(0x60, 18, Vgpr(17)));
AppendVMovLiteral(&code, 19, 0x00003400u); // +0.25h
AppendVop3(&code, 0x1e0, 20, Vgpr(19), 0);
for (u32 i = 0; i < 9; i++) {
AppendStoreVgpr(&code, 4 + i * 2, i);
}
AppendEnd(&code);
TestCase test{"VectorSinF16SdwaAndEdges",
code,
{},
{0x3c001234u, 0x00008000u, 0x00000000u, 0x00000000u,
0x0000fe00u, 0x0000fe00u, 0x00000000u, 0x0000bc00u,
0x00003c00u},
{O::V_MOV_B32, O::V_SIN_F16, O::BUFFER_STORE_DWORD,
O::S_ENDPGM}};
test.decoded_counts = {{"V_SIN_F16", 9},
{"V_SIN_F16 v4.sdwa(sel=5,sext=0), "
"v4.sdwa(sel=5,sext=0)",
1}};
test.ir_counts = {{" = FPSin ", 9}};
test.required_spirv = {" Fract ", " Sin ", " PackHalf2x16 "};
test.forbidden_spirv = {"OpCapability Float16"};
return test;
}
TestCase VectorWritelaneIgnoresExecMask() {
using O = ShaderOpcode;
@@ -18558,6 +18646,89 @@ TestCase DsSwizzleInvalidSourceLaneZero() {
return test;
}
TestCase DsBpermuteCapturedExecOffsetAndWrap() {
using O = ShaderOpcode;
std::vector<u32> code;
code.push_back(EncodeVop2(0x1a, 30, InlineU32(2), 0));
AppendBufferLoadDword(&code, 17, 30);
AppendVMovLiteral(&code, 1, 0xdeadbeefu);
AppendVMovU32(&code, 3, 100);
code.push_back(EncodeVop2(0x25, 3, Vgpr(0), 3));
code.push_back(EncodeSop1(0x04, 4, 126));
code.push_back(EncodeVop2(0x1b, 18, InlineU32(1), 0));
code.push_back(EncodeVopc(0xc2, InlineU32(1), 18));
code.push_back(EncodeSop1(0x04, 126, 106));
code.push_back(0xdacc0000u);
code.push_back(0x01000311u); // ds_bpermute_b32 v1, v17, v3
code.push_back(EncodeSop1(0x04, 126, 4));
AppendStoreVgprAtLaneDwordOffset(&code, 1, 0, 4);
AppendVMovU32(&code, 17, 124);
code.push_back(EncodeDs0(0xb3, 4));
code.push_back(EncodeDs1(2, 3, 17));
AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, 8);
AppendEnd(&code);
TestCase test;
test.name = "DsBpermuteCapturedExecOffsetAndWrap";
test.code = code;
test.initial = {0, 0, 12, 4, 0, 0, 0, 0, 0, 0, 0, 0};
test.expected = {0, 0, 12, 4,
0xdeadbeefu, 0, 0xdeadbeefu, 101,
100, 100, 100, 100};
test.opcodes = {O::V_LSHLREV_B32, O::BUFFER_LOAD_DWORD,
O::V_MOV_B32, O::V_ADD_NC_U32,
O::V_AND_B32, O::V_CMP_EQ_U32,
O::S_MOV_B64,
O::DS_BPERMUTE_B32, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.required_spirv = {"OpGroupNonUniformShuffle",
"OpGroupNonUniformBallot"};
test.forbidden_spirv = {" Workgroup", "OpControlBarrier",
"OpMemoryBarrier"};
test.decoded_counts = {{"DS_BPERMUTE_B32", 2}};
test.ir_counts = {{"BpermuteU32", 2}};
test.compute_info.threads_num[0] = 4;
test.compute_info.threads_num[1] = 1;
test.compute_info.threads_num[2] = 1;
test.compute_info.wave_size = 32;
test.compute_info.thread_ids_num = 1;
test.has_compute_info = true;
return test;
}
TestCase DsBpermuteWave64UsesIndependentHalves() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovU32(&code, 3, 100);
code.push_back(EncodeVop2(0x25, 3, Vgpr(0), 3));
AppendVMovU32(&code, 17, 128);
code.push_back(0xdacc0000u);
code.push_back(0x01000311u); // ds_bpermute_b32 v1, v17, v3
AppendStoreVgprAtLaneDwordOffset(&code, 1, 0, 0);
AppendEnd(&code);
TestCase test;
test.name = "DsBpermuteWave64UsesIndependentHalves";
test.code = code;
test.initial = std::vector<u32>(64, 0);
test.expected = std::vector<u32>(32, 100);
test.expected.insert(test.expected.end(), 32, 132);
test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32,
O::DS_BPERMUTE_B32, O::V_LSHLREV_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpGroupNonUniformShuffle"};
test.compute_info.threads_num[0] = 64;
test.compute_info.threads_num[1] = 1;
test.compute_info.threads_num[2] = 1;
test.compute_info.wave_size = 64;
test.compute_info.thread_ids_num = 1;
test.has_compute_info = true;
return test;
}
TestCase BufferAtomicVariants() {
using O = ShaderOpcode;
@@ -20604,6 +20775,8 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorCvtU16F16Sdwa);
AddCase(VectorMinMaxMed3F16Ops);
AddCase(VectorSpecialF16Ops);
AddCase(VectorCosF16CapturedSdwaAndEdges);
AddCase(VectorSinF16SdwaAndEdges);
AddCase(VectorWritelaneIgnoresExecMask);
AddCase(VectorReadlaneFromInactiveWrittenLane);
AddCase(VectorLaneWave32RuntimeSelectorWraps);
@@ -20746,6 +20919,8 @@ std::vector<TestCase> MakeCases() {
AddCase(DsMiscVariants);
AddCase(DsFloatMinMaxUsesSeparateCompareOperand);
AddCase(DsSwizzleInvalidSourceLaneZero);
AddCase(DsBpermuteCapturedExecOffsetAndWrap);
AddCase(DsBpermuteWave64UsesIndependentHalves);
AddCase(BufferAtomicVariants);
AddCase(BufferAtomicGlc0DoesNotReturnOldValue);
AddCase(BufferAtomicFMinExactRawGlcModes);
+50
View File
@@ -4031,6 +4031,38 @@ void TestNewShaderDecoderArchitecture() {
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());
@@ -4123,6 +4155,18 @@ 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());
@@ -12083,6 +12127,12 @@ void TestNewShaderRecompilerUnsupportedMemoryDecode() {
CheckNewDecoderUnsupported(mtbuf_unknown,
static_cast<uint32_t>(std::size(mtbuf_unknown)),
"MTBUF", "opcode=0x08");
const uint32_t bpermute_gds[] = {
EncodeDs0(0xb3) | (1u << 17u), EncodeDs1(1, 3, 17), 0xbf810000u};
CheckNewDecoderUnsupported(bpermute_gds,
static_cast<uint32_t>(std::size(bpermute_gds)),
"DS", "available only for LDS");
}
void TestNewShaderRecompilerFlatUserPointerUsesDma() {