Compare commits

...

17 Commits

Author SHA1 Message Date
nmzik ba55ba5dc5 Allow storage access to PS5 cubemap faces 2026-09-21 00:49:54 +04:00
nmzik c997cdb837 Skip zero-valued indirect CX 0x24a 2026-09-21 00:49:54 +04:00
Kody Schram fe4f9423e3 Significantly Reduce Linux Release Size (#738)
Added symbol stripping for linux build, drops fs from 120 mb to 20mb
2026-09-20 21:26:24 +04:00
nmzik 81af130147 Avoid game list rescans on settings changes/Remove Space pause shortcut 2026-09-20 21:07:36 +04:00
Saadi ada0514084 launcher: add a game list refresh action (#683)
* launcher: add a game list refresh action

Dropping new games into an already-configured game folder was only
picked up after restarting the launcher or re-saving the global
settings dialog. Add a refresh toolbar button, an F5 shortcut, and a
"Refresh game list" context-menu entry, all triggering a rescan of the
game folders.

* launcher: do not rescan the game list while a game is running

Clearing cfgs_list destroys the running ConfigurationItem: the running
state is lost and the replacement items allow editing or deleting a
game whose process is still active. Skip the rescan while any item is
running (CodeRabbit review finding for the refresh action).

* launcher: preserve game state when refreshing the list

Share one refresh action across the toolbar, keyboard shortcut, and context menu. Retain running items during rescans, update their settings for the next launch, and preserve selection, filtering, and sort order.

Commit active inline editors before rebuilding rows to avoid losing comments or accessing destroyed configuration data. Tie editor callbacks to item lifetime and guard the context-menu item reference.

Validation: launcher build and startup check passed; 32 Qt interaction checks passed.

* launcher: remove refresh from the game context menu

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-20 20:44:40 +04:00
Ilyas d7da244475 Add automatic sccache/ccache detection to CMake and remove deprecated Ninja setup action (#734)
* remove outdated ninja setup action

* Remove deprecated ninja setup action

The seanmiddleditch/gha-setup-ninja action has been archived by its
maintainer, who explicitly states that Ninja is now pre-installed on
all GitHub-hosted runners. This removes the action from all three
build jobs (Windows, macOS, Linux) and removes ninja from the package
install lists on macOS and Linux.

* Add automatic ccache/sccache detection to CMakeLists.txt

Detects sccache (preferred on Windows/MSVC) or ccache at configure time
and sets CMAKE_C_COMPILER_LAUNCHER / CMAKE_CXX_COMPILER_LAUNCHER
automatically. Works for local builds without manual flags.

Priority: sccache > ccache (sccache handles MSVC PDBs better).

* Delete github-actions-version-research.md

* Respect user-specified CMAKE_C_COMPILER_LAUNCHER / CMAKE_CXX_COMPILER_LAUNCHER

Only auto-detect sccache/ccache if no launcher was explicitly provided
via -DCMAKE_C_COMPILER_LAUNCHER=... or -DCMAKE_CXX_COMPILER_LAUNCHER=...

* Preserve explicit compiler launcher settings per language

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-20 19:49:59 +04:00
Elija 549495575c shader: add S_BITCMP0_B64 and S_BITCMP1_B64 (#688)
* shader: add S_BITCMP0_B64 and S_BITCMP1_B64

* test: move S_BITCMP_B64 coverage to compute suite

* test: use the existing compute CTest entry for bit comparisons

* test: remove the dedicated S_BITCMP_B64 command-line selector

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-20 19:36:12 +04:00
Maayan080 c3cc8454b9 common: remove unused recursive file enumeration (#681)
common: don't follow reparse points in the Windows recursive file walk

SysFileFindFiles recurses into every entry carrying
FILE_ATTRIBUTE_DIRECTORY, which on Windows includes directory symlinks
and junctions. A junction pointing at one of its own ancestors makes the
walk recurse until the stack runs out.

The Linux implementation already guards against this: it uses lstat
rather than stat specifically so a symlink is never followed into a
cycle. Skip reparse-point entries on Windows for the same reason.

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-20 18:50:57 +04:00
Saadi b8ad1d8c4b common: return real Win32 cache flags from GetCacheAccessType (#673)
GetCacheAccessType returned the sys_file_cache_type_t enum values as
CreateFileW flags: SYS_FILE_CACHE_SEQUENTIAL_SCAN (2) became
FILE_ATTRIBUTE_HIDDEN and SYS_FILE_CACHE_AUTO (0) lost the normal
attributes, and no FILE_FLAG_SEQUENTIAL_SCAN hint was ever applied.
Return FILE_FLAG_SEQUENTIAL_SCAN / FILE_ATTRIBUTE_NORMAL instead,
mirroring the POSIX fadvise handling on Linux.
2026-09-20 18:47:25 +04:00
Elija cfb110e0aa shader: allow partial SDWA destinations for V_NOT_B32 (#684) 2026-09-20 18:21:40 +04:00
nmzik e930657bf6 tesselation support in GUI 2026-09-20 17:16:19 +04:00
nmzik 0ad9149c90 Fix three-vertex rectangle rendering 2026-09-20 17:16:19 +04:00
nmzik 25f2699523 Fix video aspect ratio 2026-09-20 17:16:19 +04:00
nmzik a849f25376 Make tessellation optional 2026-09-20 17:16:19 +04:00
nmzik fa51da4d76 Fix AvPlayer end-of-file handling 2026-09-20 17:16:19 +04:00
nmzik 6b79b7a003 Fix AvPlayer startup state 2026-09-20 17:16:19 +04:00
nmzik 102a10f0fe Fix signed sampler comparisons 2026-09-20 17:16:19 +04:00
40 changed files with 697 additions and 321 deletions
+1 -5
View File
@@ -43,9 +43,6 @@ jobs:
- name: Setup Visual Studio environment
uses: ilammy/msvc-dev-cmd@v1
- name: Setup Ninja
uses: seanmiddleditch/gha-setup-ninja@v5
- name: Locate vcpkg
id: vcpkg
shell: pwsh
@@ -155,7 +152,7 @@ jobs:
- name: Install build dependencies
shell: bash
run: brew install glslang ninja
run: brew install glslang
- name: Install Qt 6.10.3
uses: jurplel/install-qt-action@v4
@@ -318,7 +315,6 @@ jobs:
libxrandr-dev \
libxss-dev \
lld \
ninja-build \
pkg-config \
wayland-protocols
+22
View File
@@ -12,6 +12,22 @@ endif()
project(Kyty)
find_program(SCCACHE_PROGRAM sccache)
find_program(CCACHE_PROGRAM ccache)
foreach(lang IN ITEMS C CXX)
# An explicitly empty launcher disables caching for this language.
if(NOT DEFINED CMAKE_${lang}_COMPILER_LAUNCHER)
if(SCCACHE_PROGRAM)
set(CMAKE_${lang}_COMPILER_LAUNCHER "${SCCACHE_PROGRAM}")
message(STATUS "sccache enabled for ${lang}: ${SCCACHE_PROGRAM}")
elseif(CCACHE_PROGRAM)
set(CMAKE_${lang}_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
message(STATUS "ccache enabled for ${lang}: ${CCACHE_PROGRAM}")
endif()
endif()
endforeach()
set(KYTY_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
set(KYTY_TESTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests")
set(KYTY_THIRD_PARTY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty")
@@ -665,6 +681,12 @@ elseif(WIN32 OR LINUX)
set_target_properties(kyty_emulator PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS} -Wl,-Map=${KYTY_EMULATOR_MAP_LINK_PATH}")
endif()
if(LINUX)
target_link_options(kyty_emulator PRIVATE
$<$<CONFIG:Release>:-Wl,--strip-all>
)
endif()
if(APPLE)
# The emulator writes x86-64 trampolines/PLT stubs into memory and executes them, and
# runs guest code under Rosetta. That requires the JIT / unsigned-executable-memory
+4
View File
@@ -135,6 +135,10 @@ bool ReadbackLinearImagesEnabled() {
return g_config->readback_linear_images;
}
bool TessellationEnabled() {
return g_config->tessellation_enabled;
}
bool PlayGoHackEnabled() {
return g_config->playgo_hack_enabled;
}
+2
View File
@@ -66,6 +66,7 @@ struct ConfigOptions {
bool gpu_assisted_validation_enabled = false;
bool renderdoc_enabled = false;
bool readback_linear_images = false;
bool tessellation_enabled = false;
bool playgo_hack_enabled = false;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool red_zone_protection_enabled = false;
@@ -110,6 +111,7 @@ bool GpuAssistedValidationEnabled();
bool RenderDocEnabled();
bool ReadbackLinearImagesEnabled();
bool TessellationEnabled();
bool PlayGoHackEnabled();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool RedZoneProtectionEnabled();
-39
View File
@@ -445,45 +445,6 @@ bool File::SetLastAccessAndWriteTimeUTC(const std::filesystem::path& name, const
return SysFileSetLastAccessAndWriteTimeUtc(name, af, wf);
}
std::vector<File::FindInfo> File::FindFiles(const std::filesystem::path& path) {
std::vector<sys_file_find_t> files;
SysFileFindFiles(path, files);
const auto normalized_path = path.lexically_normal();
std::vector<File::FindInfo> ret;
ret.reserve(files.size());
for (const auto& f: files) {
File::FindInfo r {};
r.path_with_name = f.path_with_name;
r.rel_path_with_name = f.path_with_name.lexically_normal().lexically_relative(normalized_path);
r.size = f.size;
SysTimeStruct at {};
SysTimeStruct wt {};
SysFileToSystemTimeUtc(f.last_access_time, at);
SysFileToSystemTimeUtc(f.last_write_time, wt);
if (!at.is_invalid && !wt.is_invalid) {
r.last_access_time = DateTime(Date(at.Year, at.Month, at.Day),
Time(at.Hour, at.Minute, at.Second, at.Milliseconds));
r.last_write_time = DateTime(Date(wt.Year, wt.Month, wt.Day),
Time(wt.Hour, wt.Minute, wt.Second, wt.Milliseconds));
}
// printf("%s, %s, %" PRIu64", %s, %s\n", r.path_with_name.c_str(),
// r.rel_path_with_name.c_str(), r.size, r.last_access_time.ToString().c_str(),
// r.last_write_time.ToString().c_str());
ret.push_back(std::move(r));
}
return ret;
}
std::vector<File::DirEntry> File::GetDirEntries(const std::filesystem::path& path) {
std::vector<sys_dir_entry_t> files;
-9
View File
@@ -30,14 +30,6 @@ class File {
public:
enum class Mode { Read, Write, ReadWrite, WriteRead };
struct FindInfo {
std::filesystem::path path_with_name;
std::filesystem::path rel_path_with_name;
DateTime last_access_time;
DateTime last_write_time;
uint64_t size;
};
struct DirEntry {
std::string name;
bool is_file;
@@ -98,7 +90,6 @@ public:
static bool SetLastAccessAndWriteTimeUTC(const std::filesystem::path& name,
const DateTime& access, const DateTime& write);
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 RenameFile(const std::filesystem::path& src, const std::filesystem::path& dst);
-10
View File
@@ -18,14 +18,6 @@ enum sys_file_cache_type_t {
SYS_FILE_CACHE_SEQUENTIAL_SCAN = 2 // NOLINT(readability-identifier-naming)
};
// NOLINTNEXTLINE(readability-identifier-naming)
struct sys_file_find_t {
std::filesystem::path path_with_name;
SysFileTimeStruct last_access_time;
SysFileTimeStruct last_write_time;
uint64_t size;
};
// NOLINTNEXTLINE(readability-identifier-naming)
struct sys_dir_entry_t {
std::string name;
@@ -75,8 +67,6 @@ bool SysFileSetLastWriteTimeUtc(const std::filesystem::path& name, SysFileTimeSt
bool SysFileSetLastAccessAndWriteTimeUtc(const std::filesystem::path& name,
SysFileTimeStruct& access, SysFileTimeStruct& write);
// NOLINTNEXTLINE(google-runtime-references)
void SysFileFindFiles(const std::filesystem::path& path, std::vector<sys_file_find_t>& out);
// 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 SysFileRenameFile(const std::filesystem::path& src, const std::filesystem::path& dst);
-45
View File
@@ -610,51 +610,6 @@ bool SysFileSetLastAccessAndWriteTimeUtc(const std::filesystem::path& name,
// }
}
// Recursively collect regular files.
void SysFileFindFiles(const std::filesystem::path& path, std::vector<sys_file_find_t>& out) {
auto real_path = get_internal_name(path);
DIR* dir = opendir(real_path.string().c_str());
if (dir == nullptr) {
return;
}
for (const dirent* entry = readdir(dir); entry != nullptr; entry = readdir(dir)) {
const std::string file_name(entry->d_name);
if (file_name == "." || file_name == "..") {
continue;
}
auto child = real_path / file_name;
struct stat s {};
// lstat, so a symlink is never followed into a cycle during the recursive walk.
if (0 != lstat(child.string().c_str(), &s)) {
continue;
}
if (S_ISDIR(s.st_mode)) {
SysFileFindFiles(child, out);
} else if (S_ISREG(s.st_mode)) {
sys_file_find_t r {};
r.path_with_name = child;
r.size = static_cast<uint64_t>(s.st_size);
r.last_access_time.is_invalid = false;
r.last_access_time.time = s.st_atime;
r.last_access_time.nanos = KYTY_STAT_ATIME_NS(s);
r.last_write_time.is_invalid = false;
r.last_write_time.time = s.st_mtime;
r.last_write_time.nanos = KYTY_STAT_MTIME_NS(s);
out.push_back(r);
}
}
closedir(dir);
}
// Keep "." and ".." to match FindFirstFileW.
void SysFileGetDents(const std::filesystem::path& path, std::vector<sys_dir_entry_t>& out) {
auto real_path = get_internal_name(path);
+2 -42
View File
@@ -57,10 +57,10 @@ static DWORD GetCacheAccessType(sys_file_cache_type_t t) {
}
if (t == SYS_FILE_CACHE_SEQUENTIAL_SCAN) {
return SYS_FILE_CACHE_SEQUENTIAL_SCAN;
return FILE_FLAG_SEQUENTIAL_SCAN;
}
return SYS_FILE_CACHE_AUTO;
return FILE_ATTRIBUTE_NORMAL;
}
void SysFileRead(void* data, uint32_t size, sys_file_t& f, uint32_t* bytes_read) {
@@ -476,46 +476,6 @@ bool SysFileSetLastAccessAndWriteTimeUtc(const std::filesystem::path& name,
return ok;
}
void SysFileFindFiles(const std::filesystem::path& path, std::vector<sys_file_find_t>& out) {
const auto pattern = path / L"*";
WIN32_FIND_DATAW data {};
HANDLE h = FindFirstFileW(pattern.c_str(), &data);
if (h == INVALID_HANDLE_VALUE) {
return;
}
do {
std::filesystem::path file_name(data.cFileName);
if (file_name == L"." || file_name == L"..") {
continue;
}
const auto full_path = path / file_name;
if ((data.dwFileAttributes & static_cast<DWORD>(FILE_ATTRIBUTE_DIRECTORY)) != 0u) {
SysFileFindFiles(full_path, out);
} else {
sys_file_find_t r {};
r.path_with_name = full_path;
r.size = (static_cast<uint64_t>(data.nFileSizeHigh) << 32u) +
static_cast<uint64_t>(data.nFileSizeLow);
r.last_access_time.is_invalid = false;
r.last_access_time.time = data.ftLastAccessTime;
r.last_write_time.is_invalid = false;
r.last_write_time.time = data.ftLastWriteTime;
out.push_back(std::move(r));
}
} while (FindNextFileW(h, &data) != 0);
FindClose(h);
}
void SysFileGetDents(const std::filesystem::path& path, std::vector<sys_dir_entry_t>& out) {
const auto pattern = path / L"*";
@@ -2017,6 +2017,14 @@ KYTY_CP_OP_PARSER(CpOpIndirectCxRegs) {
auto pfunc = g_hw_ctx_indirect_func[cmd_offset & (Pm4::CX_NUM - 1)];
if (pfunc == nullptr) {
if (raw_cmd_offset == 0x24au && value == 0u) {
static std::atomic_flag logged = ATOMIC_FLAG_INIT;
if (!logged.test_and_set(std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
"\t diagnostic: ignoring indirect CX {0x24a, 0}; hardware effect unresolved\n");
}
continue;
}
EXIT("unknown cx reg at %05" PRIx32 ": 0x%" PRIx32 "\n", num_dw - dw, cmd_offset);
}
+4
View File
@@ -26,6 +26,10 @@ enum class PrimitiveType : uint32_t {
kPolygon = 21,
};
constexpr bool IsRectList(PrimitiveType type) {
return type == PrimitiveType::kRectList || type == PrimitiveType::kRectListLegacy;
}
enum class IndexType : uint32_t {
kIndex16 = 0,
kIndex32 = 1,
@@ -252,10 +252,10 @@ static bool IsSupportedStorageTextureDescriptor(const ShaderRecompiler::IR::Imag
(is_color_2d_array && descriptor.BaseArray5() <= descriptor.Depth());
const bool is_2d =
resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2D && valid_2d_slice;
// Storage cube coordinates address individual faces, including partial cube views.
const bool is_cube = resource.cube && descriptor.Type() == Prospero::ImageType::kCube &&
descriptor.Width5() == descriptor.Height5() &&
descriptor.BaseArray5() <= descriptor.Depth() &&
(descriptor.Depth() - descriptor.BaseArray5() + 1u) % 6u == 0;
descriptor.BaseArray5() <= descriptor.Depth();
const bool is_2d_array =
resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DArray &&
((!resource.cube && is_color_2d_array && descriptor.BaseArray5() <= descriptor.Depth()) ||
@@ -755,8 +755,7 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
static_params.depth_bounds_test_enable = depth.depth_bounds_test_enable;
static_params.depth_min_bounds = depth.depth_min_bounds;
static_params.depth_max_bounds = depth.depth_max_bounds;
const bool rect_list =
command.GetUserConfig().GetPrimType() == Prospero::PrimitiveType::kRectList;
const bool rect_list = Prospero::IsRectList(command.GetUserConfig().GetPrimType());
static_params.cull_back = !rect_list && mc.cull_back;
static_params.cull_front = !rect_list && mc.cull_front;
static_params.face = mc.face;
+12 -24
View File
@@ -2,6 +2,7 @@
#include "common/assert.h"
#include "common/common.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "common/profiler.h"
@@ -789,8 +790,7 @@ static void SetDrawDebugPhase(CommandBuffer& buffer, uint64_t submit_id, const D
draw.instance_count, draw.first_instance);
}
static bool GetDrawTopology(const HW::UserConfig& ucfg, bool auto_draw,
vk::PrimitiveTopology& topology) {
static bool GetDrawTopology(const HW::UserConfig& ucfg, vk::PrimitiveTopology& topology) {
topology = vk::PrimitiveTopology::ePointList;
@@ -813,14 +813,11 @@ static bool GetDrawTopology(const HW::UserConfig& ucfg, bool auto_draw,
topology = vk::PrimitiveTopology::eTriangleStrip;
break;
case Prospero::PrimitiveType::kPatch:
if (!Config::TessellationEnabled()) return false;
[[fallthrough]];
case Prospero::PrimitiveType::kRectList:
topology = vk::PrimitiveTopology::ePatchList;
break;
case Prospero::PrimitiveType::kRectListLegacy:
if (!auto_draw) {
EXIT("unknown primitive type: %u\n", static_cast<uint32_t>(ucfg.GetPrimType()));
}
topology = vk::PrimitiveTopology::eTriangleStrip;
topology = vk::PrimitiveTopology::ePatchList;
break;
case Prospero::PrimitiveType::kQuadListLegacy:
topology = vk::PrimitiveTopology::eTriangleFan;
@@ -999,8 +996,7 @@ static void LogDrawStateIfNeeded(const CommandBuffer& buffer, const DrawCallInfo
return;
}
if (!draw.IsIndexed() &&
buffer.GetUserConfig().GetPrimType() != Prospero::PrimitiveType::kRectListLegacy) {
if (!draw.IsIndexed() && !Prospero::IsRectList(buffer.GetUserConfig().GetPrimType())) {
return;
}
@@ -1013,8 +1009,7 @@ static void LogDrawStateIfNeeded(const CommandBuffer& buffer, const DrawCallInfo
}
static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_buffer,
const ShaderVertexInputInfo& vs_input_info, const DrawCallInfo& draw,
const DrawEmitInfo& emit) {
const DrawCallInfo& draw, const DrawEmitInfo& emit) {
switch (ucfg.GetPrimType()) {
case Prospero::PrimitiveType::kPointList:
case Prospero::PrimitiveType::kLineList:
@@ -1023,6 +1018,7 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_
case Prospero::PrimitiveType::kTriFan:
case Prospero::PrimitiveType::kTriStrip:
case Prospero::PrimitiveType::kRectList:
case Prospero::PrimitiveType::kRectListLegacy:
case Prospero::PrimitiveType::kPatch:
if (draw.IsIndexed()) {
vk_buffer.drawIndexed(draw.index_count, draw.instance_count, 0, emit.vertex_offset,
@@ -1032,14 +1028,6 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_
emit.first_instance);
}
break;
case Prospero::PrimitiveType::kRectListLegacy:
if (draw.IsIndexed()) {
EXIT("unknown primitive type: %u\n", static_cast<uint32_t>(ucfg.GetPrimType()));
}
// Sarah
EXIT_NOT_IMPLEMENTED(!(draw.index_count == 3 && vs_input_info.buffers_num == 0));
vk_buffer.draw(4, draw.instance_count, emit.first_vertex, emit.first_instance);
break;
case Prospero::PrimitiveType::kQuadListLegacy:
EXIT_NOT_IMPLEMENTED((draw.index_count & 0x3u) != 0);
for (uint32_t i = 0; i < draw.index_count; i += 4) {
@@ -1182,7 +1170,7 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
if (mesh_active) {
vk_buffer.drawMeshTasksEXT(mesh_groups, draw.instance_count, 1);
} else {
EmitDrawPrimitives(ucfg, vk_buffer, state.vertex_info[0], draw, emit);
EmitDrawPrimitives(ucfg, vk_buffer, draw, emit);
}
if (!draw.IsIndexed()) {
@@ -1258,7 +1246,7 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
hw_check(buffer);
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
if (!GetDrawTopology(ucfg, false, topology)) {
if (!GetDrawTopology(ucfg, topology)) {
return;
}
@@ -1368,7 +1356,7 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
args.vertex_count, args.instance_count, args.first_instance};
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
if (!GetDrawTopology(ucfg, true, topology)) {
if (!GetDrawTopology(ucfg, topology)) {
ResetBindings();
return;
}
@@ -1378,7 +1366,7 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
return;
}
const bool rect_list = ucfg.GetPrimType() == Prospero::PrimitiveType::kRectList;
const bool rect_list = Prospero::IsRectList(ucfg.GetPrimType());
if (rect_list && state.vertex_info[0].buffers_num == 0 &&
state.vertex_info[0].stage.program->param_export_mask == 0 &&
state.ps_input_info.input_num != 0) {
@@ -141,9 +141,8 @@ public:
}
}
const bool reserved = binding.key == SDLK_ESCAPE || binding.key == SDLK_SPACE ||
binding.key == SDLK_F1 || binding.key == SDLK_F7 ||
binding.key == SDLK_F11;
const bool reserved = binding.key == SDLK_ESCAPE || 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());
+2 -3
View File
@@ -222,7 +222,7 @@ static void ToggleDesktopFullscreen() {
}
}
static void GameEventKeyboard(WindowLoopState& game, const EventKeyboard& key) {
static void GameEventKeyboard(const EventKeyboard& key) {
static SDL_Keycode fullscreen_key = SDLK_UNKNOWN;
#ifdef KYTY_DBG_INPUT
@@ -235,7 +235,6 @@ static void GameEventKeyboard(WindowLoopState& game, const EventKeyboard& key) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS || KYTY_PLATFORM == KYTY_PLATFORM_LINUX
if (key.down) {
switch (key.key_code) {
case SDLK_SPACE: SetPause(game, !game.paused.load(std::memory_order_acquire)); break;
case SDLK_F1:
if (!key.repeat) {
RenderDocRequestCapture();
@@ -539,7 +538,7 @@ void WindowContext::ProcessEvent(double time_s) {
key.mod = event->key.keysym.mod;
key.timestamp_seconds = time_s;
GameEventKeyboard(game, key);
GameEventKeyboard(key);
break;
}
@@ -74,7 +74,8 @@ constexpr OpcodeMap SOPC_OPCODE_LIST[] = {
{0x03u, Opcode::S_CMP_GE_I32}, {0x04u, Opcode::S_CMP_LT_I32}, {0x05u, Opcode::S_CMP_LE_I32},
{0x06u, Opcode::S_CMP_EQ_U32}, {0x07u, Opcode::S_CMP_LG_U32}, {0x08u, Opcode::S_CMP_GT_U32},
{0x09u, Opcode::S_CMP_GE_U32}, {0x0au, Opcode::S_CMP_LT_U32}, {0x0bu, Opcode::S_CMP_LE_U32},
{0x0cu, Opcode::S_BITCMP0_B32}, {0x0du, Opcode::S_BITCMP1_B32}, {0x12u, Opcode::S_CMP_EQ_U64},
{0x0cu, Opcode::S_BITCMP0_B32}, {0x0du, Opcode::S_BITCMP1_B32},
{0x0eu, Opcode::S_BITCMP0_B64}, {0x0fu, Opcode::S_BITCMP1_B64}, {0x12u, Opcode::S_CMP_EQ_U64},
{0x13u, Opcode::S_CMP_LG_U64},
};
@@ -73,6 +73,8 @@ enum class Opcode {
S_SUB_I32,
S_BITCMP0_B32,
S_BITCMP1_B32,
S_BITCMP0_B64,
S_BITCMP1_B64,
S_BITSET0_B32,
S_BITSET1_B32,
S_BITSET0_B64,
@@ -521,7 +521,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_NOT_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(),
SdwaSelFull(), 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, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
@@ -507,6 +507,18 @@ bool Translator::S_BITCMP_B32(const Decoder::Instruction& inst, bool expected) {
return true;
}
bool Translator::S_BITCMP_B64(const Decoder::Instruction& inst, bool expected) {
const auto value = ReadU32Pair(inst.src0);
const auto offset = ir.BitwiseAnd(ReadU32(inst.src1), IR::U32(IR::Value(63u)));
const auto word_bit = ir.BitwiseAnd(offset, IR::U32(IR::Value(31u)));
const auto word =
ir.Select(ir.ULessThan(offset, IR::U32(IR::Value(32u))), value[0], value[1]);
const auto bit =
IR::U32(ir.Emit(IR::ValueOpcode::BitFieldUExtract, {word, word_bit, IR::Value(1u)}));
WriteCompareResult(inst.dst, ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
return true;
}
bool Translator::V_ALIGNBIT_B32(const Decoder::Instruction& inst) {
const auto hi = ReadU32(inst.src0);
const auto lo = ReadU32(inst.src1);
@@ -208,6 +208,8 @@ bool Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_BFE_U64: return S_BFE_U64(inst);
case O::S_BITCMP0_B32: return S_BITCMP_B32(inst, false);
case O::S_BITCMP1_B32: return S_BITCMP_B32(inst, true);
case O::S_BITCMP0_B64: return S_BITCMP_B64(inst, false);
case O::S_BITCMP1_B64: return S_BITCMP_B64(inst, true);
case O::S_PACK_LL_B32_B16: return PackB16(inst, false, false);
case O::S_PACK_LH_B32_B16: return PackB16(inst, false, true);
case O::S_PACK_HH_B32_B16: return PackB16(inst, true, true);
@@ -205,6 +205,7 @@ private:
bool V_BFE_U32(const Decoder::Instruction& inst, bool sign);
bool V_BFI_B32(const Decoder::Instruction& inst);
bool S_BITCMP_B32(const Decoder::Instruction& inst, bool expected);
bool S_BITCMP_B64(const Decoder::Instruction& inst, bool expected);
bool V_ALIGNBIT_B32(const Decoder::Instruction& inst);
bool V_ALIGNBYTE_B32(const Decoder::Instruction& inst);
bool V_LSHL_ADD_U32(const Decoder::Instruction& inst);
@@ -125,6 +125,7 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
case ValueOpcode::ULessThan32:
case ValueOpcode::IEqual32:
case ValueOpcode::UGreaterThan32:
case ValueOpcode::SGreaterThanEqual32:
case ValueOpcode::INotEqual32:
case ValueOpcode::LogicalOr:
case ValueOpcode::LogicalAnd:
@@ -930,6 +931,13 @@ private:
return true;
}
return false;
case ValueOpcode::SGreaterThanEqual32:
if (binary()) {
result = std::bit_cast<int32_t>(static_cast<uint32_t>(a)) >=
std::bit_cast<int32_t>(static_cast<uint32_t>(b));
return true;
}
return false;
case ValueOpcode::LogicalAnd:
if (binary()) {
result = (a != 0u) && (b != 0u);
@@ -167,6 +167,12 @@
<property name="text"><string>Enable readback</string></property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="checkBox_tessellation">
<property name="toolTip"><string>Draw tessellated geometry. Disabled by default; unsupported tessellation may cause crashes.</string></property>
<property name="text"><string>Enable tessellation support (experimental)</string></property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -275,13 +281,13 @@
<item>
<widget class="QCheckBox" name="checkBox_amd_cpu">
<property name="toolTip"><string>Patch CPU instructions that differ between AMD and the host CPU.</string></property>
<property name="text"><string>AMD CPU patch (EXPERIMENTAL)</string></property>
<property name="text"><string>AMD CPU patch (experimental)</string></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_red_zone_protection">
<property name="toolTip"><string>Helps prevent crashes in some games on Windows</string></property>
<property name="text"><string>Windows SysV red zone crash protection (Experimental)</string></property>
<property name="text"><string>Windows SysV red zone crash protection (experimental)</string></property>
</widget>
</item>
</layout>
@@ -44,6 +44,28 @@
<property name="spacing">
<number>4</number>
</property>
<item>
<widget class="QToolButton" name="refresh_button">
<property name="minimumSize">
<size>
<width>36</width>
<height>36</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="global_settings_button">
<property name="minimumSize">
@@ -245,6 +267,14 @@
</layout>
</item>
</layout>
<action name="refresh_action">
<property name="text">
<string>Refresh game list</string>
</property>
<property name="toolTip">
<string>Rescan game folders for new games</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#000" d="M17.65 6.35A7.96 7.96 0 0 0 12 4a8 8 0 1 0 7.73 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>

After

Width:  |  Height:  |  Size: 232 B

+5 -3
View File
@@ -92,6 +92,7 @@ public:
int gpu_index = -1;
bool fullscreen_enabled = false;
bool readback_linear_images = false;
bool tessellation_enabled = false;
int vblank_frequency = 60;
int console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@@ -122,6 +123,7 @@ public:
gpu_index = other.gpu_index;
fullscreen_enabled = other.fullscreen_enabled;
readback_linear_images = other.readback_linear_images;
tessellation_enabled = other.tessellation_enabled;
vblank_frequency = other.vblank_frequency;
console_language = other.console_language;
vulkan_validation_enabled = other.vulkan_validation_enabled;
@@ -142,7 +144,7 @@ public:
host_input_mapping = other.host_input_mapping;
}
void CopyFrom(const Configuration& other) {
void CopyGameInfoFrom(const Configuration& other) {
name = other.name;
title_id = other.title_id;
gameVersion = other.gameVersion;
@@ -152,8 +154,6 @@ public:
custom_settings = other.custom_settings;
game_status = other.game_status;
game_comment = other.game_comment;
CopyEmulatorSettingsFrom(other);
elf = other.elf;
}
void WriteSettings(QSettings* s) const {
@@ -169,6 +169,7 @@ public:
KYTY_CFG_SET(gpu_index);
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
@@ -210,6 +211,7 @@ public:
}
KYTY_CFG_GET(fullscreen_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(tessellation_enabled);
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
console_language = s->value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
@@ -8,6 +8,8 @@
#include <QStringList>
#include <QWidget>
#include <memory>
class ConfigurationItem;
class CompatibilityDatabase;
class QEvent;
@@ -30,10 +32,9 @@ public:
[[nodiscard]] const ConfigurationItem* GetSelectedItem() const { return m_selected_item; }
ConfigurationItem* GetSelectedItem() { return m_selected_item; }
[[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; }
[[nodiscard]] const QStringList& GetHostInputMapping() const {
return m_global_info.host_input_mapping;
}
[[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; }
[[nodiscard]] std::unique_ptr<Configuration>
CreateConfiguration(const ConfigurationItem& item) const;
bool EnsureGameDirectory();
void ScanGameDirectory();
@@ -64,7 +65,6 @@ protected slots:
void filter_configurations(const QString& text);
private:
void ClearCustomSettings(ConfigurationItem* item);
void SelectItem(QTreeWidgetItem* witem);
void ApplyCompatibility();
void UpdateToolbarIcons();
+1
View File
@@ -3,6 +3,7 @@
<file alias="edit-configuration.svg">icons/edit-configuration.svg</file>
<file alias="global-settings.svg">icons/global-settings.svg</file>
<file alias="input-mapping.svg">icons/input-mapping.svg</file>
<file alias="refresh.svg">icons/refresh.svg</file>
<file alias="remove-configuration.svg">icons/remove-configuration.svg</file>
</qresource>
<qresource prefix="/styles">
@@ -230,6 +230,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
: 0);
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->comboBox_console_language->clear();
m_ui->comboBox_console_language->addItems(CONSOLE_LANGUAGE_NAMES);
@@ -375,6 +376,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) {
info.gpu_index = ui.comboBox_gpu->currentIndex() - 1;
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.console_language = ui.comboBox_console_language->currentIndex();
info.vulkan_validation_enabled = ui.checkBox_vulkan_validation->isChecked();
+102 -42
View File
@@ -13,6 +13,7 @@
#include <QAbstractItemModel>
#include <QAbstractItemView>
#include <QAction>
#include <QApplication>
#include <QComboBox>
#include <QCoreApplication>
#include <QCursor>
@@ -26,11 +27,13 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QKeySequence>
#include <QLineEdit>
#include <QMenu>
#include <QMessageBox>
#include <QPainter>
#include <QPalette>
#include <QPointer>
#include <QPushButton>
#include <QRegularExpression>
#include <QSet>
@@ -201,6 +204,10 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
m_ui->setupUi(this);
ConfigureGameList(m_ui);
m_ui->refresh_action->setShortcuts(QKeySequence::Refresh);
m_ui->refresh_button->setDefaultAction(m_ui->refresh_action);
addAction(m_ui->refresh_action);
UpdateToolbarIcons();
m_ui->global_settings_button->setToolTip(tr("Edit global settings and game folders"));
m_ui->input_mapping_button->setToolTip(tr("Edit global input mapping"));
@@ -221,7 +228,10 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
m_ui->cfgs_list->setColumnWidth(GAME_PATH_COLUMN, 320);
m_ui->cfgs_list->setColumnWidth(GAME_STATUS_COLUMN, 150);
m_ui->cfgs_list->setColumnWidth(GAME_COMMENT_COLUMN, 240);
m_ui->cfgs_list->sortItems(GAME_NAME_COLUMN, Qt::AscendingOrder);
connect(m_ui->refresh_action, &QAction::triggered, this,
&ConfigurationListWidget::ScanGameDirectory);
connect(m_ui->global_settings_button, &QToolButton::clicked, this,
&ConfigurationListWidget::edit_global_settings);
connect(m_ui->input_mapping_button, &QToolButton::clicked, this,
@@ -276,6 +286,8 @@ void ConfigurationListWidget::UpdateToolbarIcons() {
button->setIcon(QIcon(pixmap));
};
set_icon(m_ui->refresh_button, QStringLiteral(":/icons/refresh.svg"));
m_ui->refresh_action->setIcon(m_ui->refresh_button->icon());
set_icon(m_ui->global_settings_button, QStringLiteral(":/icons/global-settings.svg"));
set_icon(m_ui->input_mapping_button, QStringLiteral(":/icons/input-mapping.svg"));
set_icon(m_ui->edit_button, QStringLiteral(":/icons/edit-configuration.svg"));
@@ -398,10 +410,17 @@ void ConfigurationListWidget::ApplyCompatibility() {
}
}
static Configuration* CloneConfiguration(const Configuration& source) {
auto* ret = new Configuration;
ret->CopyFrom(source);
return ret;
std::unique_ptr<Configuration>
ConfigurationListWidget::CreateConfiguration(const ConfigurationItem& item) const {
auto info = std::make_unique<Configuration>();
const auto* custom = m_custom_infos.value(item.GetInfo().game_path);
info->CopyGameInfoFrom(item.GetInfo());
info->CopyEmulatorSettingsFrom(custom != nullptr ? *custom : m_global_info);
if (custom != nullptr && !custom->elf.isEmpty()) {
info->elf = custom->elf;
}
info->host_input_mapping = m_global_info.host_input_mapping;
return info;
}
struct GameMetadata {
@@ -516,9 +535,6 @@ static void SetGameFiles(Configuration& info, const QString& game_dir, const QSt
if (info.name.isEmpty()) {
info.name = game.dirName();
}
if (info.elf.isEmpty()) {
info.elf = QStringLiteral("eboot.bin");
}
}
static Configuration* FindCustomInfo(QMap<QString, Configuration*>* custom_infos,
@@ -564,11 +580,32 @@ bool ConfigurationListWidget::HasValidGameDirectory() const {
}
void ConfigurationListWidget::ScanGameDirectory() {
// Commit an active inline editor before destroying its row or replacing its data.
if (auto* focused = QApplication::focusWidget();
focused != nullptr && m_ui->cfgs_list->isAncestorOf(focused)) {
m_ui->cfgs_list->setFocus();
}
const auto selected_path =
m_selected_item != nullptr ? m_selected_item->GetInfo().game_path : QString();
const bool sorting_enabled = m_ui->cfgs_list->isSortingEnabled();
const int sort_column = m_ui->cfgs_list->sortColumn();
const auto sort_order = m_ui->cfgs_list->header()->sortIndicatorOrder();
const QSignalBlocker blocker(m_ui->cfgs_list);
m_ui->cfgs_list->setSortingEnabled(false);
m_selected_item = nullptr;
m_ui->cfgs_list->clear();
m_ui->cfgs_list->SetBackgroundImage({});
m_ui->edit_button->setEnabled(false);
m_ui->delete_button->setEnabled(false);
// MainDialog tracks the running item, so keep its identity even if its
// directory is no longer present. Other rows can be rebuilt from disk.
QMap<QString, ConfigurationItem*> running_items;
for (int index = m_ui->cfgs_list->topLevelItemCount() - 1; index >= 0; index--) {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
if (item->IsRunning()) {
running_items.insert(PathKey(item->GetInfo().game_path), item);
} else {
delete item;
}
}
const QString eboot_name = QStringLiteral("eboot.bin");
QSet<QString> found_games;
@@ -601,16 +638,9 @@ void ConfigurationListWidget::ScanGameDirectory() {
auto metadata = GetGameMetadata(
game_dir.filePath(QStringLiteral("sce_sys/param.json")), game_dir.dirName());
auto info = std::make_unique<Configuration>();
auto* custom = FindCustomInfo(&m_custom_infos, game_path, legacy_game_path);
if (custom != nullptr) {
info->CopyFrom(*custom);
info->custom_settings = true;
} else {
info->CopyEmulatorSettingsFrom(m_global_info);
info->custom_settings = false;
}
auto info = std::make_unique<Configuration>();
info->custom_settings =
FindCustomInfo(&m_custom_infos, game_path, legacy_game_path) != nullptr;
SetGameFiles(*info, game_dir.absolutePath(), game_path, metadata);
const auto* compatibility = m_compatibility->Find(info->title_id);
if (compatibility != nullptr) {
@@ -618,10 +648,20 @@ void ConfigurationListWidget::ScanGameDirectory() {
info->game_comment = compatibility->comment;
}
if (auto* item = running_items.value(game_key); item != nullptr) {
// Refresh metadata without losing the running row's identity.
const QSignalBlocker status_blocker(item->GetStatusCombo());
item->GetInfo().CopyGameInfoFrom(*info);
item->Update();
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
!item->GetInfo().title_id.trimmed().isEmpty());
continue;
}
auto* item = new ConfigurationItem(std::move(info), m_ui->cfgs_list);
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
!item->GetInfo().title_id.trimmed().isEmpty());
connect(item->GetStatusCombo(), &QComboBox::currentIndexChanged, this,
connect(item->GetStatusCombo(), &QComboBox::currentIndexChanged, item,
[this, item](int /*index*/) {
if (!m_compatibility->IsLocal()) {
return;
@@ -638,7 +678,7 @@ void ConfigurationListWidget::ScanGameDirectory() {
m_ui->cfgs_list->setCurrentItem(item);
SelectItem(item);
});
connect(item->GetCommentEdit(), &QLineEdit::editingFinished, this, [this, item]() {
connect(item->GetCommentEdit(), &QLineEdit::editingFinished, item, [this, item]() {
if (!m_compatibility->IsLocal()) {
return;
}
@@ -664,8 +704,22 @@ void ConfigurationListWidget::ScanGameDirectory() {
}
}
m_ui->cfgs_list->sortItems(GAME_NAME_COLUMN, Qt::AscendingOrder);
m_ui->cfgs_list->setSortingEnabled(sorting_enabled);
if (sorting_enabled) {
m_ui->cfgs_list->sortItems(sort_column, sort_order);
}
filter_configurations(m_ui->search_line_edit->text());
ConfigurationItem* selected_item = nullptr;
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); index++) {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
if (!item->isHidden() && item->GetInfo().game_path == selected_path) {
selected_item = item;
break;
}
}
m_ui->cfgs_list->setCurrentItem(selected_item);
SelectItem(selected_item);
}
void ConfigurationListWidget::edit_configuration() {
@@ -674,16 +728,19 @@ void ConfigurationListWidget::edit_configuration() {
return;
}
ConfigurationEditDialog dlg(item->GetInfo(), this);
auto info = CreateConfiguration(*item);
ConfigurationEditDialog dlg(*info, this);
dlg.setWindowTitle(tr("Edit game settings"));
if (dlg.exec() == QDialog::Accepted) {
info->custom_settings = true;
item->GetInfo().custom_settings = true;
auto game_path = item->GetInfo().game_path;
auto game_path = info->game_path;
delete m_custom_infos.take(game_path);
m_custom_infos.insert(game_path, CloneConfiguration(item->GetInfo()));
m_custom_infos.insert(game_path, info.release());
WriteSettings();
ScanGameDirectory();
item->Update();
SelectItem(item);
}
}
@@ -695,9 +752,11 @@ void ConfigurationListWidget::delete_configuartion() {
if (QMessageBox::Yes == QMessageBox::question(this, tr("Clear custom settings"),
tr("Do you want to clear custom settings?"))) {
ClearCustomSettings(item);
delete m_custom_infos.take(item->GetInfo().game_path);
item->GetInfo().custom_settings = false;
WriteSettings();
ScanGameDirectory();
item->Update();
SelectItem(item);
}
}
@@ -712,9 +771,13 @@ void ConfigurationListWidget::edit_global_settings() {
if (dlg.exec() == QDialog::Accepted) {
m_global_info.CopyEmulatorSettingsFrom(info);
m_game_dirs = NormalizeGameDirectories(dlg.GetGameDirectories());
const auto game_dirs = NormalizeGameDirectories(dlg.GetGameDirectories());
const bool game_dirs_changed = game_dirs != m_game_dirs;
m_game_dirs = game_dirs;
WriteSettings();
ScanGameDirectory();
if (game_dirs_changed) {
ScanGameDirectory();
}
}
}
@@ -726,10 +789,6 @@ void ConfigurationListWidget::edit_input_mapping() {
}
}
void ConfigurationListWidget::ClearCustomSettings(ConfigurationItem* item) {
delete m_custom_infos.take(item->GetInfo().game_path);
}
void ConfigurationListWidget::ViewTrophies() {
auto* current = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
auto* item = current != nullptr ? current : m_selected_item;
@@ -871,12 +930,13 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
connect(action_view_trophies, &QAction::triggered, this,
&ConfigurationListWidget::ViewTrophies);
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);
dialog->show();
}
});
connect(action_patches, &QAction::triggered, this,
[this, item = QPointer<ConfigurationItem>(item)]() {
if (item != nullptr) {
auto* dialog = new PatchesDialog(item->GetInfo(), this);
dialog->show();
}
});
action_patches->setVisible(item != nullptr &&
PatchesDialog::IsSupportedTitleId(item->GetInfo().title_id));
QAction* action_remove_save_data =
+4 -4
View File
@@ -90,6 +90,7 @@ QString KeyName(const QKeyEvent& event) {
}
switch (key) {
case Qt::Key_Space: return QStringLiteral("Space");
case Qt::Key_Return:
case Qt::Key_Enter: return QStringLiteral("Return");
case Qt::Key_Backspace: return QStringLiteral("Backspace");
@@ -132,8 +133,7 @@ public:
auto* layout = new QVBoxLayout(this);
m_label = new QLabel(
tr("Press a key or mouse button.\nSpace, F1, F7, and F11 are reserved; Esc cancels."),
this);
tr("Press a key or mouse button.\nF1, F7, and F11 are reserved; Esc cancels."), this);
m_label->setAlignment(Qt::AlignCenter);
layout->addWidget(m_label);
}
@@ -149,8 +149,8 @@ protected:
reject();
return;
}
if (event->key() == Qt::Key_Space || event->key() == Qt::Key_F1 ||
event->key() == Qt::Key_F7 || event->key() == Qt::Key_F11) {
if (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;
}
+5 -4
View File
@@ -226,6 +226,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--fullscreen";
}
args << "--readback-linear-images" << BoolArg(info.readback_linear_images);
if (info.tessellation_enabled) {
args << "--tessellation";
}
args << "--vblank-frequency" << QString::number(info.vblank_frequency);
args << "--console-language" << QString::number(info.console_language);
args << "--vulkan-validation" << BoolArg(info.vulkan_validation_enabled);
@@ -490,10 +493,8 @@ void MainDialogPrivate::Run() {
m_running_item->SetRunning(true);
Configuration info;
info.CopyFrom(m_running_item->GetInfo());
info.host_input_mapping = m_ui->widget->GetHostInputMapping();
m_main_dialog->RunInterpreter(&m_process, info);
auto info = m_ui->widget->CreateConfiguration(*m_running_item);
m_main_dialog->RunInterpreter(&m_process, *info);
Update();
}
+1 -1
View File
@@ -19,7 +19,7 @@
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()) {
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle(tr("Cheats (Experimental) - %1").arg(game.name));
setWindowTitle(tr("Cheats (experimental) - %1").arg(game.name));
resize(640, 480);
auto* layout = new QVBoxLayout(this);
+36 -52
View File
@@ -706,7 +706,7 @@ public:
int Enable(uint32_t id) {
std::scoped_lock lifecycle_lock(lifecycle_mutex);
std::lock_guard state_lock(mutex);
if (!stopped) {
if (state == State::Playing) {
return AVPLAYER_ERROR_OPERATION_FAILED;
}
if (fmt == nullptr || id >= fmt->nb_streams) {
@@ -724,7 +724,7 @@ public:
int Disable(uint32_t id) {
std::scoped_lock lifecycle_lock(lifecycle_mutex);
std::lock_guard state_lock(mutex);
if (!stopped) {
if (state == State::Playing) {
return AVPLAYER_ERROR_OPERATION_FAILED;
}
if (video_id == static_cast<int>(id)) {
@@ -755,9 +755,11 @@ public:
return AVPLAYER_ERROR_OPERATION_FAILED;
}
time = CurrentTimeNoLock();
restart = !stopped;
restart = state == State::Playing;
was_paused = paused;
stopped = true;
if (restart) {
state = State::Ready;
}
}
if (restart) {
StopWorkers();
@@ -778,7 +780,7 @@ public:
int StartImpl(uint64_t ms, bool paused_after_start = false, bool show_seek_frame = false) {
{
std::lock_guard lock(mutex);
stopped = true;
state = State::Ready;
}
StopWorkers();
interrupt_io = false;
@@ -791,14 +793,17 @@ public:
AutoEnable();
}
if (!video_id && !audio_id) {
state = State::Stopped;
return AVPLAYER_ERROR_OPERATION_FAILED;
}
if (!OpenCodecs()) {
ResetNoLock(true);
state = State::Stopped;
return AVPLAYER_ERROR_OPERATION_FAILED;
}
if (!AllocateBuffers()) {
ResetNoLock(true);
state = State::Stopped;
return AVPLAYER_ERROR_NO_MEMORY;
}
SeekNoLock(ms);
@@ -809,12 +814,11 @@ public:
pause_time = clock_start;
seek_video_frame_pending =
paused_after_start && show_seek_frame && video_id.has_value();
stopped = false;
}
if (!StartWorkers()) {
std::lock_guard lock(mutex);
stopped = true;
state = State::Stopped;
ResetNoLock(true);
result = AVPLAYER_ERROR_OPERATION_FAILED;
}
@@ -825,20 +829,22 @@ public:
}
int Stop() {
bool was_playing_video = false;
AvPlayerEventReplacement stop_event;
AvPlayerEventReplacement stop_event {};
{
std::scoped_lock lifecycle_lock(lifecycle_mutex);
{
std::lock_guard lock(mutex);
was_playing_video = !stopped && video_id.has_value();
stopped = true;
if (state == State::Playing) {
was_playing_video = video_id.has_value();
stop_event = event;
}
state = State::Stopped;
}
StopWorkers();
{
std::lock_guard lock(mutex);
ResetNoLock();
}
stop_event = TakeStopEvent();
}
if (was_playing_video) {
::printf("AvPlayer video stopped\n");
@@ -872,7 +878,8 @@ public:
}
bool Active() const {
std::lock_guard lock(mutex);
return !stopped && !pipeline_failed && !DrainedNoLock();
return state == State::Ready ||
(state == State::Playing && !pipeline_failed && !DrainedNoLock());
}
uint64_t CurrentTime() const {
std::lock_guard lock(mutex);
@@ -888,7 +895,7 @@ public:
return StartImpl(ms, was_paused, true);
}
uint64_t CurrentTimeNoLock() const {
if (stopped) {
if (state != State::Playing) {
return 0;
}
using namespace std::chrono;
@@ -925,7 +932,7 @@ public:
return false;
}
std::lock_guard lock(mutex);
if (stopped || !video_id) {
if (state != State::Playing || !video_id) {
return false;
}
const bool deliver_seek_frame = paused && seek_video_frame_pending;
@@ -955,7 +962,6 @@ public:
current_video = std::move(*frame);
*out = current_video->info;
RecordLoopBoundary(*current_video);
completion_changed.notify_all();
if (deliver_seek_frame) {
seek_video_frame_pending = false;
}
@@ -966,7 +972,8 @@ public:
return false;
}
std::lock_guard lock(mutex);
if (paused || stopped || !audio_id || trick_speed != AVPLAYER_TRICK_SPEED_NORMAL) {
if (paused || state != State::Playing || !audio_id ||
trick_speed != AVPLAYER_TRICK_SPEED_NORMAL) {
return false;
}
auto frame = audio_frames.TryPop();
@@ -987,7 +994,6 @@ public:
current_audio->info.details.audio.language_code, 4);
last_audio_ts = out->time_stamp;
RecordLoopBoundary(*current_audio);
completion_changed.notify_all();
return true;
}
std::optional<int32_t> TakeWarning() {
@@ -1000,13 +1006,12 @@ public:
}
private:
enum class State { Ready, Playing, Stopped };
bool DrainedNoLock() const {
return demux_eof && video_done && audio_done && video_frames.Empty() &&
audio_frames.Empty();
}
AvPlayerEventReplacement TakeStopEvent() {
return stop_event_sent.exchange(true) ? AvPlayerEventReplacement {} : event;
}
void RecordLoopBoundary(const ReadyFrame& frame) {
if (frame.timestamp_offset > last_output_loop_offset) {
last_output_loop_offset = frame.timestamp_offset;
@@ -1198,11 +1203,10 @@ private:
StopWorkers();
return false;
}
stop_event_sent = false;
state = State::Playing;
return true;
}
void NotifyWorkers() {
completion_changed.notify_all();
video_packets.Notify();
audio_packets.Notify();
video_buffers.Notify();
@@ -1289,13 +1293,6 @@ private:
demux_eof = true;
video_packets.Notify();
audio_packets.Notify();
std::unique_lock lock(mutex);
completion_changed.wait(lock, [&] { return worker_stop || DrainedNoLock(); });
if (!worker_stop) {
const auto stop_event = TakeStopEvent();
lock.unlock();
emit_event(stop_event, AVPLAYER_EVENT_STATE_STOP);
}
}
void VideoDecoder() {
Decoder(video_packets, video_ctx, video_buffers, video_frames, video_done,
@@ -1343,11 +1340,7 @@ private:
break;
}
}
{
std::lock_guard lock(mutex);
done = true;
}
completion_changed.notify_all();
done = true;
}
bool DecodePacket(AVCodecContext* codec, AVPacket* packet, uint64_t timestamp_offset,
WorkQueue<std::unique_ptr<GuestBuffer>>& buffers,
@@ -1412,15 +1405,8 @@ private:
return use_vdec2 ? static_cast<uint32_t>(s->codecpar->height)
: align_up(static_cast<uint32_t>(s->codecpar->height), 16);
}
float Aspect(AVStream* s) const {
if (s->codecpar->height == 0) {
return 0.0f;
}
double r = static_cast<double>(s->codecpar->width) / s->codecpar->height;
if (s->sample_aspect_ratio.num > 0 && s->sample_aspect_ratio.den > 0) {
r *= av_q2d(s->sample_aspect_ratio);
}
return static_cast<float>(r);
float Aspect(AVRational ratio) const {
return ratio.num > 0 && ratio.den > 0 ? static_cast<float>(av_q2d(ratio)) : 0.0f;
}
uint64_t Duration(AVStream* s) const {
return s->duration != AV_NOPTS_VALUE
@@ -1433,14 +1419,14 @@ private:
std::memset(v, 0, sizeof(*v));
v->width = Width(s);
v->height = Height(s);
v->aspect_ratio = Aspect(s);
v->aspect_ratio = Aspect(s->sample_aspect_ratio);
lang(v->language_code, s->metadata);
}
void FillVideoEx(AVStream* s, AvPlayerVideoEx* v) const {
void FillVideoEx(AVStream* s, AvPlayerVideoEx* v, AVRational aspect) const {
std::memset(v, 0, sizeof(*v));
v->width = Width(s);
v->height = Height(s);
v->aspect_ratio = Aspect(s);
v->aspect_ratio = Aspect(aspect);
v->pitch = VideoPitch(s);
v->luma_bit_depth = 8;
v->chroma_bit_depth = 8;
@@ -1476,7 +1462,7 @@ private:
FillVideo(s, &d->video);
}
if (ex) {
FillVideoEx(s, &ex->video);
FillVideoEx(s, &ex->video, s->sample_aspect_ratio);
}
break;
case AVMEDIA_TYPE_AUDIO:
@@ -1561,7 +1547,7 @@ private:
info->time_stamp = to_ms(
src->best_effort_timestamp != AV_NOPTS_VALUE ? src->best_effort_timestamp : src->pts,
s->time_base);
FillVideoEx(s, &info->details.video);
FillVideoEx(s, &info->details.video, src->sample_aspect_ratio);
info->details.video.crop_left_offset = static_cast<uint32_t>(src->crop_left);
info->details.video.crop_right_offset =
static_cast<uint32_t>(src->crop_right + (pitch - src->width));
@@ -1672,11 +1658,9 @@ private:
std::atomic_bool demux_eof {true};
std::atomic_bool video_done {true};
std::atomic_bool audio_done {true};
std::atomic_bool stop_event_sent {true};
std::condition_variable completion_changed;
std::mutex lifecycle_mutex;
mutable std::mutex mutex;
bool stopped = true;
State state = State::Ready;
bool paused = false;
bool seek_video_frame_pending = false;
std::atomic_bool loop {false};
@@ -2029,7 +2013,7 @@ Bool KYTY_SYSV_ABI AvPlayerGetAudioData(AvPlayerInternal* h, AvPlayerFrameInfo*
}
Bool KYTY_SYSV_ABI AvPlayerIsActive(AvPlayerInternal* h) {
PRINT_NAME();
return h != nullptr && h->source != nullptr && h->source->Active() ? 1 : 0;
return h != nullptr && (h->source == nullptr || h->source->Active());
}
uint64_t KYTY_SYSV_ABI AvPlayerCurrentTime(AvPlayerInternal* h) {
PRINT_NAME();
+6
View File
@@ -64,6 +64,7 @@ static void PrintUsage() {
::printf(" --gpu-assisted-validation <t|f> Bounds-check shader accesses on the GPU.\n"
" Implies --vulkan-validation; very slow.\n");
::printf(" --shader-validation <true|false> Enable shader validation.\n");
::printf(" --tessellation Draw tessellation patches; skipped by default.\n");
::printf(" --shader-optimization-type <value> None, Size, or Performance.\n");
::printf(" --shader-log-direction <value> Silent, Console, or File.\n");
::printf(" --shader-log-folder <path> Shader log output folder.\n");
@@ -180,6 +181,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
continue;
}
if (arg == "--tessellation") {
options.config.tessellation_enabled = true;
continue;
}
if (arg == "--profile") {
options.config.profiler_enabled = true;
continue;
-14
View File
@@ -189,20 +189,6 @@ void CheckUnicodePaths(const std::filesystem::path &root) {
Check(fixture.Create(native_file), "create Unicode filename");
fixture.Close();
for (const auto &directory : {unicode_root, std::filesystem::relative(unicode_root)}) {
// A Windows temporary directory may be on a different drive from the working directory.
if (directory.empty()) {
continue;
}
const auto files = Common::File::FindFiles(directory);
const auto relative_file =
std::filesystem::path(u8"nested/asset-\u00e9-\u65e5\u672c\u8a9e.bin");
Check(std::any_of(files.begin(), files.end(), [&](const auto &file) {
return file.path_with_name.lexically_normal() ==
(directory / relative_file).lexically_normal() &&
file.rel_path_with_name == relative_file;
}), "recursive enumeration preserves native and relative Unicode paths");
}
const auto entries = Common::File::GetDirEntries(nested_root);
Check(std::any_of(entries.begin(), entries.end(), [&](const auto &entry) {
return entry.is_file && entry.name == GuestFilename;
+5 -4
View File
@@ -1246,7 +1246,7 @@ ResourcePlan ConditionalSamplerPlan(bool diamond, bool reverse, bool reverse_phi
flag = fixture.Emit(ValueOpcode::LaneId);
}
fixture.program.block_info[0].condition =
fixture.Emit(ValueOpcode::IEqual32, {flag, Value(0u)});
fixture.Emit(ValueOpcode::SGreaterThanEqual32, {flag, Value(0u)});
if (writable) {
MemoryInfo memory;
memory.kind = ResourceKind::Buffer;
@@ -1324,9 +1324,10 @@ void TestConditionalSamplerPhi() {
.userdata = &memory,
.read_specialization_memory =
ReadLinearTestMemory};
for (uint32_t flag = 0; flag < 2; ++flag) {
memory.words[(0x1000 + 196) / 4] = flag;
const uint32_t first = (flag != 0u) != reverse ? 480 / 4 : 448 / 4;
// Sampler selection compares a signed value against zero.
for (const auto flag : {-1, 0, 1, INT32_MIN, INT32_MAX}) {
memory.words[(0x1000 + 196) / 4] = std::bit_cast<uint32_t>(flag);
const uint32_t first = (flag < 0) != reverse ? 480 / 4 : 448 / 4;
memory.fail_address = 0x1000 + (first == 448 / 4 ? 480u : 448u);
DescriptorValue selected;
Check(EvaluateDescriptorSource(plan, source, runtime, selected),
+338 -4
View File
@@ -9405,6 +9405,114 @@ public:
}
void CheckCubeFaceStorageExpansion() {
constexpr const char *name = "CubeFaceStorageExpansion";
constexpr uintptr_t base = 0x0000000204700000ull;
constexpr uint64_t allocation_size = 0x10000;
constexpr std::array<uint32_t, 6> half_red{0x3c00, 0x4000, 0x4200,
0x4400, 0x4500, 0x4600};
EnsureRuntimeContext();
int64_t direct_offset = -1;
Require(name, "direct allocation",
Libs::LibKernel::Memory::KernelAllocateDirectMemory(
0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(),
allocation_size, allocation_size, 0, &direct_offset) == 0,
"cube-face allocation failed");
void *mapped = reinterpret_cast<void *>(base);
Require(name, "direct mapping",
Libs::LibKernel::Memory::KernelMapDirectMemory(
&mapped, allocation_size, 0x3, 0x10, direct_offset,
allocation_size) == 0 && mapped == reinterpret_cast<void *>(base),
"cube-face mapping failed");
std::memset(mapped, 0, allocation_size);
{
RenderContext context(m_runtime_context);
auto &scheduler = context.GetCommandScheduler();
HW::Context registers{};
HW::UserConfig user_config{};
HW::Shader shaders{};
scheduler.Begin(registers, user_config, shaders);
context.MapMemory(base, allocation_size);
auto &cache = context.GetTextureCache();
auto &executor = context.GetRenderExecutor();
for (uint32_t face = 0; face < half_red.size(); ++face) {
// PPSA25380 clears six single-face cube views at one guest address.
ShaderTextureResource descriptor{{static_cast<uint32_t>(base >> 8u),
0xc4700000u, 0x0001c001u, 0xb0000facu,
face | (face << 16u), 0x00700000u, 0, 0}};
TestCase test;
test.name = name;
test.has_user_data = true;
std::copy_n(descriptor.fields, 8, test.user_data.begin());
test.has_compute_info = true;
test.compute_info.threads_num[0] = test.compute_info.threads_num[1] = 8;
test.compute_info.threads_num[2] = 1;
test.compute_info.thread_ids_num = 2;
test.opcodes = {ShaderOpcode::V_MOV_B32, ShaderOpcode::IMAGE_STORE,
ShaderOpcode::S_ENDPGM};
test.code = {EncodeVop1(0x01, 20, 256), EncodeVop1(0x01, 21, 257)};
AppendVMovU32(&test.code, 22, 0);
AppendVMovLiteral(&test.code, 0,
std::bit_cast<uint32_t>(static_cast<float>(face + 1)));
AppendVMovLiteral(&test.code, 1, std::bit_cast<uint32_t>(0.5f));
AppendVMovLiteral(&test.code, 2, std::bit_cast<uint32_t>(0.25f));
AppendVMovLiteral(&test.code, 3, std::bit_cast<uint32_t>(1.0f));
test.code.push_back(EncodeMimg0(0x08, 0xf, 0, false, 5));
test.code.push_back(EncodeMimg1(0, 20));
AppendEnd(&test.code);
const auto compiled = CompileCase(test, SubgroupSize());
const auto &resource = compiled.program.info.images.at(0);
Require(name, "cube storage specialization", resource.cube &&
resource.written && resource.dimension ==
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray,
"single-face cube store lost its array coordinates");
ShaderRecompiler::IR::DescriptorValue value{};
value.dword_count = 8;
std::copy_n(descriptor.fields, 8, value.dwords.begin());
const auto binding = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
Image storage;
storage.view = cache.FindTexture(binding.image_id, binding.desc);
auto &image = cache.GetImage(binding.image_id);
Require(name, "face view and backing", storage.view != nullptr &&
image.info.pixel_format == vk::Format::eR16G16B16A16Sfloat &&
image.info.bytes_per_block == 8 &&
image.info.resources.layers == face + 1 &&
image.backing.layers == face + 1 &&
binding.desc.view_info.type == vk::ImageViewType::e2DArray &&
binding.desc.view_info.base_layer == face &&
binding.desc.view_info.layer_count == 1,
"cube face did not expand the array or select its own layer");
image.Transit(vk::ImageLayout::eGeneral, vk::AccessFlagBits2::eShaderWrite,
{}, scheduler.Current().Handle());
storage.layout = image.backing.state.layout;
scheduler.Finish();
Dispatch(test, compiled, {}, nullptr, nullptr, &storage);
cache.MarkGpuWritten(binding.image_id);
for (uint32_t prior = 0; prior <= face; ++prior) {
const auto pixels = ReadCachedTexel(name, context, binding.image_id,
{}, {8, 8, 1}, prior);
for (uint32_t pixel = 0; pixel < 64; ++pixel) {
Require(name, "stored and preserved faces",
pixels[pixel * 2] == (0x38000000u | half_red[prior]) &&
pixels[pixel * 2 + 1] == 0x3c003400u,
"face " + std::to_string(prior) + " pixel " +
std::to_string(pixel) + " changed after writing face " +
std::to_string(face));
}
}
}
RenderExecutorTestAccess::ResetBindings(executor);
context.UnmapMemory(base, allocation_size);
scheduler.Finish();
}
Require(name, "release",
Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0 &&
Libs::LibKernel::Memory::KernelReleaseDirectMemory(
direct_offset, allocation_size) == 0,
"cube-face backing release failed");
std::printf("[gpu] %-32s ok\n", name);
}
void CheckRenderExecutorColorStandardTileDiscovery() {
constexpr const char *name = "RenderExecutorColorStandardTile";
constexpr uintptr_t base = 0x0000000203b00000ull;
@@ -12389,15 +12497,14 @@ public:
const auto &image =
compiled.program.info.images.at(binding.resources.front());
bool supported =
image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2D;
image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2D ||
image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
if (resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled) {
supported = supported ||
image.dimension ==
ShaderRecompiler::Decoder::ImageDimension::Dim1D ||
image.dimension ==
ShaderRecompiler::Decoder::ImageDimension::Dim1DArray ||
image.dimension ==
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
ShaderRecompiler::Decoder::ImageDimension::Dim1DArray;
}
Require(test.name, "dispatch", supported,
"unsupported image dimension needs a matching Vulkan test view");
@@ -12831,6 +12938,7 @@ public:
const uint32_t extent = depth_feedback ? 8 : 32;
constexpr uintptr_t depth_address = 0x0000000204400000ull;
constexpr uint64_t allocation_size = 0x40000;
constexpr uint64_t rect_address = depth_address + 0x8000;
EnsureRuntimeContext();
RenderContext context(m_runtime_context);
auto &scheduler = context.GetCommandScheduler();
@@ -12865,6 +12973,15 @@ public:
static_cast<float *>(mapped)[y * 64 + x] = initial_depth(x, y);
}
}
if (!depth_feedback && !packed_vertex_color) {
// Rectangle input contains three position/UV records.
constexpr std::array<float, 24> rect_vertices{
-0.75f, -0.75f, 0, 0, 0, 0, 0, 0,
0.75f, -0.75f, 0, 0, 1, 0, 0, 0,
-0.75f, 0.75f, 0, 0, 0, 1, 0, 0};
std::memcpy(reinterpret_cast<void *>(rect_address), rect_vertices.data(),
sizeof(rect_vertices));
}
resources.MapMemory(depth_address, allocation_size);
if (depth_feedback) {
depth.desc.type = BindingType::DepthTarget;
@@ -13521,6 +13638,82 @@ public:
Require(name, "sparse MRT3 output", sparse_pixels[component] == expected,
"MRT3 was compacted to a different slot or clipped by unused MRT0");
}
// A fourth VS invocation reads beyond the descriptor instead of reconstructing the corner.
const ShaderBufferResource rect_buffer{{
static_cast<u32>(rect_address),
static_cast<u32>(rect_address >> 32u) | (32u << 16u), 3,
DstSel(4, 5, 6, 7) |
(static_cast<u32>(Prospero::BufferFormat::k32Float) << 12u)}};
static const auto rect_vertex_code = [] {
std::vector<u32> code{
EncodeMubuf0(0x0d, 0, true, false), EncodeMubuf1(0, 2, 5),
EncodeMubuf0(0x0d, 16, true, false), EncodeMubuf1(2, 2, 5)};
AppendVMovU32(&code, 4, 0);
AppendVMovLiteral(&code, 6, 0x3f800000u);
code.insert(code.end(), {EncodeExp0(0x0c, 0xf), EncodeExp1(0, 1, 4, 6),
EncodeExp0(0x20, 0xf), EncodeExp1(2, 3, 4, 6)});
AppendEnd(&code);
return code;
}();
static const auto rect_pixel_code = [] {
std::vector<u32> code;
for (u32 component = 0; component < 2; component++) {
code.push_back(EncodeVintrp(0, 20 + component, 0, component, 0));
code.push_back(EncodeVintrp(1, 20 + component, 0, component, 1));
}
AppendVMovU32(&code, 22, 0);
AppendVMovLiteral(&code, 23, 0x3f800000u);
code.insert(code.end(), {EncodeExp0(0x03, 0xf), EncodeExp1(20, 21, 22, 23)});
AppendEnd(&code);
return code;
}();
const auto rect_vs = reinterpret_cast<uint64_t>(rect_vertex_code.data());
const auto rect_ps = reinterpret_cast<uint64_t>(rect_pixel_code.data());
ShaderMapUserData(rect_vs,
{.type = Prospero::ShaderBinaryType::kGs, .user_data = &native_user_data,
.code_size_bytes = static_cast<u32>(rect_vertex_code.size() * sizeof(u32))});
ShaderMapUserData(rect_ps,
{.type = Prospero::ShaderBinaryType::kPs,
.code_size_bytes = static_cast<u32>(rect_pixel_code.size() * sizeof(u32))});
shaders.SetEsShaderBase(rect_vs);
shaders.SetPsShaderBase(rect_ps);
shaders.SetGsShaderResource2({.user_sgpr = 4});
for (u32 i = 0; i < 4; i++) {
shaders.SetGsUserSgpr(i, rect_buffer.fields[i], HW::UserSgprType::Vsharp);
}
registers.SetPsInControl(0x8001);
registers.SetPsInputEna(2);
registers.SetPsInputAddr(2);
registers.SetPsInputSettings(0, 0);
for (const auto primitive : {Prospero::PrimitiveType::kRectList,
Prospero::PrimitiveType::kRectListLegacy}) {
const char *rect_name = primitive == Prospero::PrimitiveType::kRectList
? "RectListThreeRecords" : "LegacyRectListThreeRecords";
user_config.SetPrimitiveType(primitive);
registers.SetModeControl({});
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), sparse_color.image_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, {});
RenderExecutorTestAccess::DrawAuto(
executor, scheduler.Current(), {.vertex_count = 3, .instance_count = 1});
const auto pixels = ReadCachedTexel(name, context, sparse_color.image_id,
{}, {extent, extent, 1});
for (u32 y = 0; y < extent; y++) {
for (u32 x = 0; x < extent; x++) {
const bool inside = x >= 4 && x < 28 && y >= 4 && y < 28;
const auto offset = 4 * (y * extent + x);
const std::array<float, 4> expected{
inside ? (x + 0.5f - 4) / 24 : 0,
inside ? (y + 0.5f - 4) / 24 : 0, 0, inside ? 1.0f : 0};
for (u32 component = 0; component < 4; component++) {
Require(rect_name, "three-vertex rectangle coverage and UV",
std::abs(std::bit_cast<float>(pixels[offset + component]) -
expected[component]) < 0.0001f,
"rectangle reconstruction lost its fourth corner or interpolated UVs");
}
}
}
}
DestroyBuffer(&stencil_readback);
vertex_shader = owned_vertex_shader;
@@ -15011,6 +15204,7 @@ private:
Require("VulkanHarness", "graphics", available_features12.shaderOutputLayer == true,
"vertex layer output is not supported");
Require("VulkanHarness", "graphics", available_features.fillModeNonSolid &&
available_features.tessellationShader &&
available_depth_clip.depthClipEnable && available_clip_control.depthClipControl &&
available_color_write.colorWriteEnable &&
available_feedback_layout.attachmentFeedbackLoopLayout &&
@@ -15076,6 +15270,7 @@ private:
device_features.sampleRateShading = true;
device_features.shaderInt64 = true;
device_features.fillModeNonSolid = true;
device_features.tessellationShader = true;
device_info.pEnabledFeatures = &device_features;
constexpr const char *device_extensions[] = {
VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME,
@@ -16969,6 +17164,117 @@ TestCase ScalarGetpcWritesNextInstructionPc() {
{O::S_GETPC_B64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
TestCase ScalarBitcmpB64DynamicOperands() {
using O = ShaderOpcode;
struct Input {
u32 low;
u32 high;
u32 index;
};
const Input inputs[] = {
{1u, 0u, 0u}, {0x80000000u, 0u, 31u}, {0u, 1u, 32u},
{0u, 0x80000000u, 63u}, {1u, 0u, 64u},
{0x80000000u, 0u, 95u}, {0u, 1u, 96u},
{0u, 0x80000000u, 127u}, {1u, 0u, 128u},
{0u, 0x80000000u, 0xffffffffu}, {0u, 0u, 17u},
{0xffffffffu, 0xffffffffu, 42u},
{0x89abcdefu, 0x76543210u, 4u},
{0x89abcdefu, 0x76543210u, 36u},
};
TestCase test;
test.name = "ScalarBitcmpB64DynamicOperands";
for (const auto &input : inputs) {
test.initial.insert(test.initial.end(), {input.low, input.high, input.index});
}
test.expected = test.initial;
for (u32 i = 0; i < std::size(inputs); ++i) {
// Load the value and index on the GPU so the compares cannot constant-fold.
for (u32 component = 0; component < 3; ++component) {
AppendVMovU32(&test.code, 30, (i * 3 + component) * 4);
AppendBufferLoadDword(&test.code, 0, 30);
test.code.push_back(EncodeVop1(0x02, 20 + component, Vgpr(0)));
}
const uint64_t value = uint64_t{inputs[i].low} | (uint64_t{inputs[i].high} << 32);
const u32 bit = static_cast<u32>((value >> (inputs[i].index & 63u)) & 1u);
for (u32 expected_bit = 0; expected_bit < 2; ++expected_bit) {
const bool scc = bit == expected_bit;
test.code.push_back(EncodeSopc(0x0e + expected_bit, 20, 22));
test.code.push_back(EncodeSop2(0x0a, 24, InlineU32(1), InlineU32(0)));
AppendStoreSgpr(&test.code, 24, static_cast<u32>(test.expected.size()));
test.expected.push_back(scc ? 1u : 0u);
// Exercise SCC-driven control flow as well as S_CSELECT_B32.
test.code.push_back(EncodeSopp(0x05, 2)); // s_cbranch_scc1 to the true arm.
test.code.push_back(EncodeSMovB32(24, InlineU32(3)));
test.code.push_back(EncodeSopp(0x02, 1)); // s_branch past the true arm.
test.code.push_back(EncodeSMovB32(24, InlineU32(7)));
AppendStoreSgpr(&test.code, 24, static_cast<u32>(test.expected.size()));
test.expected.push_back(scc ? 7u : 3u);
}
}
AppendEnd(&test.code);
test.initial.resize(test.expected.size());
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_READFIRSTLANE_B32,
O::S_BITCMP0_B64, O::S_BITCMP1_B64, O::S_CSELECT_B32,
O::S_CBRANCH_SCC1, O::S_BRANCH, O::S_MOV_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpBitFieldUExtract", "OpULessThan", "OpSelect"};
return test;
}
TestCase ScalarBitcmpB64IntegerConstants() {
using O = ShaderOpcode;
struct Source {
u32 encoding;
uint64_t bits;
};
// Negative integer inlines sign-extend; B64 literals zero-extend.
const Source sources[] = {
{128u, 0u}, {129u, 1u}, {192u, 64u},
{193u, 0xffffffffffffffffull}, {208u, 0xfffffffffffffff0ull},
{255u, 0xffffffffu}, {255u, 0x80000000u},
};
const u32 indices[] = {0u, 4u, 6u, 31u, 32u, 63u, 64u, 127u};
TestCase test;
test.name = "ScalarBitcmpB64IntegerConstants";
test.initial.assign(std::begin(indices), std::end(indices));
test.expected = test.initial;
for (u32 i = 0; i < std::size(indices); ++i) {
AppendVMovU32(&test.code, 30, i * 4);
AppendBufferLoadDword(&test.code, 0, 30);
test.code.push_back(EncodeVop1(0x02, 22, Vgpr(0)));
for (const auto &source : sources) {
const u32 bit = static_cast<u32>((source.bits >> (indices[i] & 63u)) & 1u);
for (u32 expected_bit = 0; expected_bit < 2; ++expected_bit) {
test.code.push_back(EncodeSopc(0x0e + expected_bit, source.encoding, 22));
if (source.encoding == 255u) {
test.code.push_back(static_cast<u32>(source.bits));
}
test.code.push_back(EncodeSop2(0x0a, 24, InlineU32(1), InlineU32(0)));
AppendStoreSgpr(&test.code, 24, static_cast<u32>(test.expected.size()));
test.expected.push_back(bit == expected_bit ? 1u : 0u);
}
}
}
// An instruction has one literal word even when both sources select it.
for (u32 expected_bit = 0; expected_bit < 2; ++expected_bit) {
test.code.push_back(EncodeSopc(0x0e + expected_bit, 255u, 255u));
test.code.push_back(0x8000001fu); // Select bit 31 of the same zero-extended literal.
test.code.push_back(EncodeSop2(0x0a, 24, InlineU32(1), InlineU32(0)));
AppendStoreSgpr(&test.code, 24, static_cast<u32>(test.expected.size()));
test.expected.push_back(expected_bit);
}
AppendEnd(&test.code);
test.initial.resize(test.expected.size());
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_READFIRSTLANE_B32,
O::S_BITCMP0_B64, O::S_BITCMP1_B64, O::S_CSELECT_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpBitFieldUExtract"};
return test;
}
TestCase ScalarBitfieldPack() {
using O = ShaderOpcode;
@@ -17977,6 +18283,29 @@ TestCase Vop1SdwaNotCapturedByte0Source() {
return test;
}
TestCase Vop1SdwaNotPreservesHighWordDestination() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendBufferLoadDword(&code, 0, 30);
AppendVMovLiteral(&code, 3, 0xabcd5555u);
code.push_back(0x7e066ef9u);
code.push_back(0x00061400u);
AppendStoreVgpr(&code, 3, 0);
AppendEnd(&code);
TestCase test;
test.name = "Vop1SdwaNotPreservesHighWordDestination";
test.code = std::move(code);
test.initial = {0x12345678u};
test.expected = {0xabcda987u};
test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_MOV_B32, O::V_NOT_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.ir_counts = {{" = BitwiseNot32 ", 1}};
test.required_spirv = {"OpNot"};
return test;
}
TestCase Vop1SdwaMovByteDestinations() {
using O = ShaderOpcode;
@@ -27061,6 +27390,8 @@ std::vector<TestCase> MakeCases() {
cases.push_back(ScalarSubvectorLoops(64));
AddCase(ScalarGetpcWritesNextInstructionPc);
AddCase(ScalarBitfieldPack);
AddCase(ScalarBitcmpB64DynamicOperands);
AddCase(ScalarBitcmpB64IntegerConstants);
AddCase(ScalarBrevB32PreservesScc);
AddCase(ScalarBfeI32CapturedRawSignExtends);
AddCase(BitfieldExtractWidthPastEndEdges);
@@ -27088,6 +27419,7 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorFfbhI32NativeAndVop3OnGpu);
AddCase(Vop1SdwaFfblCapturedHighWordSource);
AddCase(Vop1SdwaNotCapturedByte0Source);
AddCase(Vop1SdwaNotPreservesHighWordDestination);
AddCase(Vop1SdwaMovByteDestinations);
AddCase(Vop2SdwaSubNcExactByte2Destination);
AddCase(Vop2SdwaAddNcCapturedHighWordDestination);
@@ -32204,6 +32536,7 @@ int main(int argc, char **argv) {
if (argc == 2 && std::strcmp(argv[1], "--layered-image-only") == 0) {
VulkanHarness vulkan;
vulkan.CheckUnifiedImageViewCache();
vulkan.CheckCubeFaceStorageExpansion();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--image-view-cache-only") == 0) {
@@ -32394,6 +32727,7 @@ int main(int argc, char **argv) {
#endif
vulkan.CheckUnifiedImageViewCache();
vulkan.CheckPackedTextureComponents();
vulkan.CheckCubeFaceStorageExpansion();
const auto tests = MakeCases();
const auto graphics_tests = MakeGraphicsCases();
CheckOpcodeCoverage(tests, graphics_tests);
+57
View File
@@ -3435,6 +3435,62 @@ void TestNewShaderRecompilerCapturedVop1SdwaByteConvert() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerVop1SdwaNotDestination() {
auto options = MakeCompileOptions(ShaderType::Pixel);
const auto check_destination = [&](uint32_t modifier, uint32_t dst_sel,
uint32_t dst_unused) {
const uint32_t shader[] = {
0x7e066ef9u, modifier, EncodeExp0(0x00, 0x1),
EncodeExp1(3, 0, 0, 0), EncodeSopp(0x01),
};
ShaderRecompiler::Decoder::Instruction decoded;
ShaderRecompiler::Decoder::DecodeInstruction(shader, 0, decoded);
Check(decoded.opcode == ShaderRecompiler::Decoder::Opcode::V_NOT_B32,
"V_NOT_B32 SDWA opcode did not decode");
Check(decoded.word_count == 2u,
"V_NOT_B32 SDWA instruction has the wrong word count");
Check(decoded.dst.kind == ShaderRecompiler::Decoder::OperandKind::Vgpr &&
decoded.dst.reg == 3u && decoded.dst.sdwa_sel == dst_sel &&
decoded.dst.sdwa_dst_unused == dst_unused &&
decoded.dst.explicit_sdwa_dst,
"V_NOT_B32 SDWA destination metadata is incorrect");
Check(decoded.src0.kind == ShaderRecompiler::Decoder::OperandKind::Vgpr &&
decoded.src0.reg == 0u && decoded.src0.sdwa_sel == 6u &&
!decoded.src0.negate && !decoded.src0.absolute,
"V_NOT_B32 SDWA source metadata is incorrect");
auto result = RecompileForTest(shader, options);
CheckSpirvBinaryValidates(result.spirv);
};
check_destination(0x00061400u, 4u, 2u);
check_destination(0x00060500u, 5u, 0u);
check_destination(0x00060000u, 0u, 0u);
const auto check_rejected = [&](uint32_t modifier, const char *message) {
const uint32_t instruction[] = {0x7e066ef9u, modifier};
ShaderRecompiler::Decoder::Instruction decoded;
ShaderRecompiler::Decoder::DecodeInstruction(instruction, 0, decoded);
Check(decoded.word_count == 2u &&
decoded.opcode == ShaderRecompiler::Decoder::Opcode::UNSUPPORTED,
message);
};
check_rejected(0x00060700u, "V_NOT_B32 SDWA accepted destination selector 7");
check_rejected(0x00070600u, "V_NOT_B32 SDWA accepted source selector 7");
check_rejected(0x00061c00u,
"V_NOT_B32 SDWA accepted destination unused value 3");
check_rejected(0x00160400u, "V_NOT_B32 SDWA accepted source negation");
check_rejected(0x00260400u, "V_NOT_B32 SDWA accepted source absolute");
check_rejected(0x00062400u, "V_NOT_B32 SDWA accepted clamp");
check_rejected(0x00064400u, "V_NOT_B32 SDWA accepted output modifier");
check_rejected(0x00000400u,
"V_NOT_B32 SDWA partial destination accepted a byte source");
}
void TestNewShaderRecompilerBootB16PackedAndSdwaOpcodes() {
const uint32_t shader[] = {
0xd7070001u,
@@ -13397,6 +13453,7 @@ int main() {
TestNewShaderRecompilerDsReadWrite2Translation();
TestNewShaderRecompilerDsWideAndAtomicTranslation();
TestNewShaderRecompilerCapturedVop1SdwaByteConvert();
TestNewShaderRecompilerVop1SdwaNotDestination();
TestNewShaderRecompilerScalarMemoryBindingDomains();
// Opcode semantics and optimized SPIR-V are exercised by
// ShaderRecompilerComputeTests; keep the distinct decoder contract checks