mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-20 19:07:01 +02:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39ee9972b8 | |||
| e500333bcd | |||
| 4498d75b9b | |||
| 1df53b169f | |||
| ff61c1cecd | |||
| 60a01817ce | |||
| 1a87cfedb5 | |||
| 5bd447d2a2 | |||
| a4fc58e908 | |||
| 4e8e335b7b | |||
| 947576363e | |||
| 9da390bc0a | |||
| ef639610e7 | |||
| 6c3b562822 | |||
| f1413569ce | |||
| fcc9dff5b9 | |||
| d9ae3ed64c | |||
| 618926b124 | |||
| dca7ece2ca | |||
| ccecebfeff | |||
| 1752f1382f | |||
| 53a59434d8 | |||
| 18b6c9059b | |||
| 715539804b | |||
| b02eb28fd1 | |||
| 07f1e77d23 | |||
| 6980e0863d | |||
| 42bfeec2e9 | |||
| 795743ce22 |
+47
-22
@@ -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'
|
||||
|
||||
@@ -5,3 +5,4 @@ build/
|
||||
_Build/vscode-clang/
|
||||
_Build/
|
||||
cmake-build-debug/
|
||||
_PipelineCache/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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_ */
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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>
|
||||
@@ -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_ */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user