mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-21 03:16:54 +02:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b095caefe | |||
| 8dc9b9d532 | |||
| e40438f95e | |||
| a6a56c8a44 | |||
| 4b074cf9e9 | |||
| 7c9e18e73b | |||
| d2df2cad74 | |||
| b2a80a3a96 | |||
| cfb8f7b246 | |||
| d792dfa885 | |||
| 12ff571609 | |||
| d3c182f385 | |||
| bf4dcdce52 | |||
| 476204cd4b | |||
| f9e56ef8a0 | |||
| 61761b65f2 | |||
| ba55ba5dc5 | |||
| c997cdb837 | |||
| fe4f9423e3 | |||
| 81af130147 | |||
| ada0514084 | |||
| d7da244475 | |||
| 549495575c | |||
| c3cc8454b9 | |||
| b8ad1d8c4b | |||
| cfb110e0aa | |||
| e930657bf6 | |||
| 0ad9149c90 | |||
| 25f2699523 | |||
| a849f25376 | |||
| fa51da4d76 | |||
| 6b79b7a003 | |||
| 102a10f0fe |
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
+2
-46
@@ -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;
|
||||
|
||||
@@ -492,13 +453,8 @@ std::vector<File::DirEntry> File::GetDirEntries(const std::filesystem::path& pat
|
||||
std::vector<File::DirEntry> ret;
|
||||
ret.reserve(files.size());
|
||||
|
||||
for (const auto& f: files) {
|
||||
File::DirEntry r {};
|
||||
|
||||
r.name = f.name;
|
||||
r.is_file = f.is_file;
|
||||
|
||||
ret.push_back(std::move(r));
|
||||
for (auto& f: files) {
|
||||
ret.emplace_back(std::move(f.name), f.is_file);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -912,17 +912,6 @@ void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiato
|
||||
if (!indexed) {
|
||||
DrawIndirectArgs args {};
|
||||
std::memcpy(&args, args_addr, sizeof(args));
|
||||
if (args.instance_count != 1u || args.start_vertex_location != 0u ||
|
||||
args.start_instance_location != 0u) {
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1) < 64) {
|
||||
LOGF("\t warning: partial DrawIndirect args: vertex_count=%" PRIu32
|
||||
", instance_count=%" PRIu32 ", start_vertex=%" PRIu32
|
||||
", start_instance=%" PRIu32 "\n",
|
||||
args.vertex_count_per_instance, args.instance_count,
|
||||
args.start_vertex_location, args.start_instance_location);
|
||||
}
|
||||
}
|
||||
m_num_instances = args.instance_count;
|
||||
DrawIndexAuto({.vertex_count = args.vertex_count_per_instance,
|
||||
.instance_count = args.instance_count,
|
||||
@@ -934,16 +923,6 @@ void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiato
|
||||
|
||||
DrawIndexedIndirectArgs args {};
|
||||
std::memcpy(&args, args_addr, sizeof(args));
|
||||
if (args.base_vertex_location != 0u || args.start_instance_location != 0u) {
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1) < 64) {
|
||||
LOGF("\t warning: partial DrawIndexIndirect args: index_count=%" PRIu32
|
||||
", instance_count=%" PRIu32 ", start_index=%" PRIu32 ", base_vertex=%" PRIu32
|
||||
", start_instance=%" PRIu32 "\n",
|
||||
args.index_count_per_instance, args.instance_count, args.start_index_location,
|
||||
args.base_vertex_location, args.start_instance_location);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t index_size = 0;
|
||||
switch (m_index_type_and_size) {
|
||||
@@ -1005,17 +984,6 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
|
||||
|
||||
if (!indexed) {
|
||||
auto* args = reinterpret_cast<const DrawIndirectArgs*>(args_addr);
|
||||
if (args->instance_count != 1u || args->start_vertex_location != 0u ||
|
||||
args->start_instance_location != 0u) {
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1) < 64) {
|
||||
LOGF("\t warning: partial DrawIndirectMulti args[%u]: vertex_count=%" PRIu32
|
||||
", instance_count=%" PRIu32 ", start_vertex=%" PRIu32
|
||||
", start_instance=%" PRIu32 "\n",
|
||||
i, args->vertex_count_per_instance, args->instance_count,
|
||||
args->start_vertex_location, args->start_instance_location);
|
||||
}
|
||||
}
|
||||
m_num_instances = args->instance_count;
|
||||
DrawIndexAuto({.vertex_count = args->vertex_count_per_instance,
|
||||
.instance_count = args->instance_count,
|
||||
@@ -1026,17 +994,6 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
|
||||
}
|
||||
|
||||
auto* args = reinterpret_cast<const DrawIndexedIndirectArgs*>(args_addr);
|
||||
if (args->base_vertex_location != 0u || args->start_instance_location != 0u) {
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1) < 64) {
|
||||
LOGF("\t warning: partial DrawIndexIndirectMulti args[%u]: index_count=%" PRIu32
|
||||
", instance_count=%" PRIu32 ", start_index=%" PRIu32 ", base_vertex=%" PRIu32
|
||||
", start_instance=%" PRIu32 "\n",
|
||||
i, args->index_count_per_instance, args->instance_count,
|
||||
args->start_index_location, args->base_vertex_location,
|
||||
args->start_instance_location);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t index_size = 0;
|
||||
switch (m_index_type_and_size) {
|
||||
|
||||
@@ -209,17 +209,16 @@ static void RtCheck(const HW::RenderTarget& rt) {
|
||||
}
|
||||
if (rt.info.fmask_compression_enable) {
|
||||
EXIT_NOT_IMPLEMENTED(rt.attrib.num_samples == 0 && rt.attrib.num_fragments == 0);
|
||||
// Native MSAA stores expanded samples, independent of FMASK metadata compression.
|
||||
static bool logged = false;
|
||||
if (!logged) {
|
||||
LOGF("RenderTarget: using native Vulkan MSAA without guest FMASK metadata, "
|
||||
LOGF("RenderTarget: using expanded native Vulkan MSAA samples, "
|
||||
"fmask=0x%016" PRIx64 "\n",
|
||||
rt.fmask.addr);
|
||||
logged = true;
|
||||
}
|
||||
}
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(rt.info.fmask_data_compression_disable != false);
|
||||
|
||||
if (rt.info.cmask_fast_clear_enable || rt.info.dcc_compression_enable) {
|
||||
static bool logged = false;
|
||||
if (!logged) {
|
||||
|
||||
@@ -184,17 +184,18 @@ bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bo
|
||||
}
|
||||
const bool full = common && descriptor.fields[4] == field4_expected &&
|
||||
descriptor.fields[5] == field5_expected;
|
||||
if (!full || (descriptor.fields[6] == 0 && descriptor.fields[7] != 0) ||
|
||||
if (!full ||
|
||||
(descriptor.MsaaDepth() && !IsMultisampledTexture(descriptor.Type()))) {
|
||||
return false;
|
||||
}
|
||||
if (descriptor.fields[6] == 0) {
|
||||
const auto metadata_control = descriptor.fields[6] & 0x00ffffffu;
|
||||
if (metadata_control == 0) {
|
||||
return true;
|
||||
}
|
||||
constexpr uint32_t htile_control = 0x00280000u;
|
||||
const uint32_t expected_control = htile_control | (descriptor.MsaaDepth() ? (1u << 10u) : 0u);
|
||||
const auto metadata_addr = descriptor.MetaAddr() << 8u;
|
||||
return (descriptor.fields[6] & 0x00ffffffu) == expected_control && metadata_addr != 0 &&
|
||||
return metadata_control == expected_control && metadata_addr != 0 &&
|
||||
metadata_addr < TRACKER_ADDRESS_SIZE && (metadata_addr & 0x7fffu) == 0 &&
|
||||
descriptor.TileMode() == Prospero::TileMode::kDepth;
|
||||
}
|
||||
@@ -252,10 +253,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()) ||
|
||||
|
||||
@@ -337,6 +337,7 @@ struct PipelineCache::ProgramCache {
|
||||
|
||||
if constexpr (std::is_same_v<InputInfo, ShaderVertexInputInfo>) {
|
||||
options.user_data_base = 8;
|
||||
options.wave_size = input_info.wave_size;
|
||||
if (stage == ShaderType::Mesh || stage == ShaderType::TessellationControl) {
|
||||
options.user_data_base = 0;
|
||||
options.wave_size = stage == ShaderType::Mesh ? input_info.mesh.wave_size : 64u;
|
||||
@@ -755,8 +756,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;
|
||||
|
||||
@@ -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"
|
||||
@@ -460,7 +461,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
state.height = std::numeric_limits<uint32_t>::max();
|
||||
state.num_layers = std::numeric_limits<uint32_t>::max();
|
||||
state.num_color_attachments = 0;
|
||||
uint32_t attachment_samples = 0;
|
||||
for (uint32_t i = 0; i < color_count; i++) {
|
||||
auto& target = colors[i];
|
||||
EXIT_IF(!target.image_id);
|
||||
@@ -481,12 +481,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
target.desc.view_info.base_level, target.desc.view_info.base_layer,
|
||||
target.desc.view_info.layer_count);
|
||||
EXIT_IF(image.backing.samples != target.desc.info.samples || image_view == nullptr);
|
||||
if (attachment_samples == 0) {
|
||||
attachment_samples = target.desc.info.samples;
|
||||
} else if (attachment_samples != target.desc.info.samples) {
|
||||
EXIT("mixed color attachment sample counts are unsupported: %u and %u\n",
|
||||
attachment_samples, target.desc.info.samples);
|
||||
}
|
||||
const auto& view = target.desc.view_info;
|
||||
const auto layout = image.binding.is_bound ? vk::ImageLayout::eGeneral
|
||||
: vk::ImageLayout::eColorAttachmentOptimal;
|
||||
@@ -535,12 +529,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
image.info.data.address, depth.desc.view_info.base_layer,
|
||||
depth.desc.view_info.layer_count);
|
||||
EXIT_IF(image_view == nullptr || image.backing.samples != depth.desc.info.samples);
|
||||
if (attachment_samples == 0) {
|
||||
attachment_samples = depth.desc.info.samples;
|
||||
} else if (attachment_samples != depth.desc.info.samples) {
|
||||
EXIT("mixed color/depth sample counts are unsupported: %u and %u\n", attachment_samples,
|
||||
depth.desc.info.samples);
|
||||
}
|
||||
const bool feedback = depth.depth_write_enable && pixel &&
|
||||
std::ranges::any_of(pixel->images, [&](const TextureBinding& binding) {
|
||||
if (binding.image_id != depth.image_id ||
|
||||
@@ -591,9 +579,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits;
|
||||
state.width = limits.maxFramebufferWidth;
|
||||
state.height = limits.maxFramebufferHeight;
|
||||
} else if (attachment_samples == 0 ||
|
||||
vulkan_sample_count(attachment_samples) == vk::SampleCountFlagBits {}) {
|
||||
EXIT("render state has no valid attachments\n");
|
||||
}
|
||||
if (state.num_layers == std::numeric_limits<uint32_t>::max()) {
|
||||
state.num_layers = 1;
|
||||
@@ -789,8 +774,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 +797,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 +980,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 +993,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 +1002,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 +1012,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) {
|
||||
@@ -1121,10 +1093,6 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
vertex_bindings = AcquireVertexBuffers(buffer, state.vertex_info[0]);
|
||||
index_binding = PrepareIndexBuffer(buffer, index_source);
|
||||
}
|
||||
const auto rendering =
|
||||
AcquireRenderTargets(buffer, state.color_info, state.color_count, state.depth_info,
|
||||
bindings.pixel);
|
||||
|
||||
if (draw.IsIndexed()) {
|
||||
LogDrawPhase(draw.Name(), "CreatePipeline");
|
||||
}
|
||||
@@ -1132,6 +1100,9 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
std::span {state.color_info, state.color_count}, state.depth_info, vertex_stages, buffer,
|
||||
state.ps_active ? &state.ps_input_info : nullptr, topology, primitive_restart_enable,
|
||||
state.programs);
|
||||
const auto rendering =
|
||||
AcquireRenderTargets(buffer, state.color_info, state.color_count, state.depth_info,
|
||||
bindings.pixel);
|
||||
|
||||
// Resource preparation above may synchronously finish and restart the scheduler. From this
|
||||
// point onward, every operation targets the current command buffer and cannot touch guest
|
||||
@@ -1182,7 +1153,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 +1229,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 +1339,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 +1349,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) {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "kernel/pthread.h"
|
||||
#include "libs/errno.h"
|
||||
#include "libs/libs.h"
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -1654,8 +1655,12 @@ KYTY_SYSV_ABI int VideoOutGetOutputStatus(int handle, VideoOutOutputStatus* stat
|
||||
return VIDEO_OUT_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
int32_t attribute3 = 0;
|
||||
Loader::SystemContentParamSfoGetInt("ATTRIBUTE3", &attribute3);
|
||||
ctx->mutex.Lock();
|
||||
status->resolution = (ctx->width >= 3840 || ctx->height >= 2160 ? 2u : 1u);
|
||||
// Primary output reports 4K unless param.json Video-out Info enables resolution detection.
|
||||
status->resolution =
|
||||
((attribute3 & 4) != 0 && ctx->width < 3840 && ctx->height < 2160 ? 1u : 2u);
|
||||
status->dynamicRange = 1;
|
||||
status->refreshRate =
|
||||
(ctx->output_mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ || Config::GetVblankFrequency() >= 119
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -132,12 +132,23 @@ void ValidateNativeProgram(const IR::Program& program) {
|
||||
program.memory_info[index].planning_only;
|
||||
});
|
||||
};
|
||||
const auto indirect_buffer_handle = [&](const IR::Inst& handle) {
|
||||
return program.info.uses_dma && handle.NumArgs() == 4u && !handle.Uses().empty() &&
|
||||
std::ranges::all_of(handle.Uses(), [&](const IR::Use& use) {
|
||||
if (IR::BufferAccessOf(use.user->GetOpcode()) != IR::BufferAccess::Read) {
|
||||
return false;
|
||||
}
|
||||
const auto index = use.user->Flags<IR::MemoryFlags>().index;
|
||||
return index < program.memory_info.size() &&
|
||||
program.memory_info[index].kind == IR::ResourceKind::IndirectBuffer;
|
||||
});
|
||||
};
|
||||
for (const auto* block: program.blocks) {
|
||||
for (const auto& inst: *block) {
|
||||
const auto dense = inst.Flags<uint32_t>();
|
||||
switch (inst.GetOpcode()) {
|
||||
case IR::ValueOpcode::GetBufferResource:
|
||||
if (planning_only_handle(inst)) {
|
||||
if (planning_only_handle(inst) || indirect_buffer_handle(inst)) {
|
||||
break;
|
||||
}
|
||||
if (dense >= program.info.buffers.size()) {
|
||||
@@ -212,6 +223,9 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
|
||||
Fail(program, "buffer operation has invalid memory metadata");
|
||||
}
|
||||
const auto& memory = program.memory_info[memory_index];
|
||||
if (memory.kind == IR::ResourceKind::IndirectBuffer) {
|
||||
requirements.subgroup_local_invocation_id = true;
|
||||
}
|
||||
if (memory.kind == IR::ResourceKind::Buffer) {
|
||||
if (memory.resource >= program.info.buffers.size()) {
|
||||
Fail(program, "buffer operation has invalid resource metadata");
|
||||
|
||||
@@ -559,6 +559,10 @@ void EmitVoid(ValueEmitContext&) {}
|
||||
|
||||
void EmitBarrier(EmitterState& state) {
|
||||
const auto tessellation = state.program.stage == ShaderType::TessellationControl;
|
||||
if (!tessellation && ShaderWorkgroupInput(state.program.stage, state.input_info) == nullptr) {
|
||||
// Independent graphics invocations have no native workgroup left to synchronize.
|
||||
return;
|
||||
}
|
||||
const auto memory_scope = tessellation ? spv::ScopeInvocation : spv::ScopeWorkgroup;
|
||||
const auto semantics = tessellation ? spv::MemorySemanticsMaskNone
|
||||
: spv::MemorySemanticsAcquireReleaseMask |
|
||||
|
||||
@@ -27,61 +27,66 @@ uint32_t EmitDsMaskedLaneRead(EmitterState& state, uint32_t source, uint32_t tar
|
||||
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;
|
||||
const uint32_t packed = StorageBufferPackedStride(state, mem);
|
||||
const uint32_t stride = packed & 0x3fffu;
|
||||
const bool swizzle = stride != 0u && ((packed >> 14u) & 1u) != 0u;
|
||||
if (((packed >> 20u) & 1u) != 0u) {
|
||||
const auto lane = Binary(state, spv::OpBitwiseAnd, TypeU32(state),
|
||||
EmitSubgroupLocalInvocationId(state), ConstantU32(state, 63));
|
||||
index = Binary(state, spv::OpIAdd, TypeU32(state), index, lane);
|
||||
}
|
||||
if (mem.offset != 0u) {
|
||||
offset = Binary(state, spv::OpIAdd, TypeU32(state), offset, ConstantU32(state, mem.offset));
|
||||
}
|
||||
struct BufferAddress {
|
||||
uint32_t offset;
|
||||
uint32_t byte;
|
||||
};
|
||||
|
||||
uint32_t address = 0;
|
||||
if (!swizzle) {
|
||||
if (stride == 0u) {
|
||||
address = offset;
|
||||
} else {
|
||||
const auto indexed = stride == 1u ? index
|
||||
: Binary(state, spv::OpIMul, TypeU32(state), index,
|
||||
ConstantU32(state, stride));
|
||||
address = Binary(state, spv::OpIAdd, TypeU32(state), indexed, offset);
|
||||
}
|
||||
} else {
|
||||
const uint32_t stride_enum = (packed >> 16u) & 3u;
|
||||
const uint32_t index_stride = 8u << stride_enum;
|
||||
const auto index_msb = Binary(state, spv::OpShiftRightLogical, TypeU32(state), index,
|
||||
ConstantU32(state, stride_enum + 3u));
|
||||
const auto index_lsb = Binary(state, spv::OpBitwiseAnd, TypeU32(state), index,
|
||||
ConstantU32(state, index_stride - 1u));
|
||||
const auto offset_msb =
|
||||
BufferAddress CalculateBufferAddress(EmitterState& state, uint32_t index, uint32_t offset,
|
||||
uint32_t soffset, uint32_t immediate, uint32_t stride,
|
||||
uint32_t swizzle, uint32_t index_stride) {
|
||||
if (immediate != 0u) {
|
||||
offset = Binary(state, spv::OpIAdd, TypeU32(state), offset, ConstantU32(state, immediate));
|
||||
}
|
||||
const auto indexed = Binary(state, spv::OpIMul, TypeU32(state), index, stride);
|
||||
auto address = Binary(state, spv::OpIAdd, TypeU32(state), indexed, offset);
|
||||
if (swizzle != 0u) {
|
||||
const auto index_shift =
|
||||
Binary(state, spv::OpIAdd, TypeU32(state), index_stride, ConstantU32(state, 3));
|
||||
const auto indices = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
|
||||
ConstantU32(state, 1), index_shift);
|
||||
const auto index_msb =
|
||||
Binary(state, spv::OpShiftRightLogical, TypeU32(state), index, index_shift);
|
||||
const auto index_lsb =
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state), index,
|
||||
Binary(state, spv::OpISub, TypeU32(state), indices, ConstantU32(state, 1)));
|
||||
const auto offset_msb =
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state), offset, ConstantU32(state, ~3u));
|
||||
const auto offset_lsb =
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state), offset, ConstantU32(state, 3u));
|
||||
const auto indexed_msb = stride == 1u ? index_msb
|
||||
: Binary(state, spv::OpIMul, TypeU32(state),
|
||||
index_msb, ConstantU32(state, stride));
|
||||
const auto msb = Binary(state, spv::OpIMul, TypeU32(state),
|
||||
Binary(state, spv::OpIAdd, TypeU32(state), indexed_msb, offset_msb),
|
||||
ConstantU32(state, index_stride));
|
||||
const auto indexed_msb = Binary(state, spv::OpIMul, TypeU32(state), index_msb, stride);
|
||||
const auto msb =
|
||||
Binary(state, spv::OpIMul, TypeU32(state),
|
||||
Binary(state, spv::OpIAdd, TypeU32(state), indexed_msb, offset_msb), indices);
|
||||
const auto lsb = Binary(state, spv::OpIAdd, TypeU32(state),
|
||||
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), index_lsb,
|
||||
ConstantU32(state, 2u)),
|
||||
offset_lsb);
|
||||
address = Binary(state, spv::OpIAdd, TypeU32(state), msb, lsb);
|
||||
address = Select(state, TypeU32(state), swizzle,
|
||||
Binary(state, spv::OpIAdd, TypeU32(state), msb, lsb), address);
|
||||
}
|
||||
return {offset, Binary(state, spv::OpIAdd, TypeU32(state), address, soffset)};
|
||||
}
|
||||
|
||||
const auto soffset_value = inst.Arg(3).Resolve();
|
||||
if (soffset_value.IsImmediate() && soffset_value.GetType() == IR::Type::U32 &&
|
||||
soffset_value.U32() == 0u) {
|
||||
return address;
|
||||
uint32_t BufferLane(EmitterState& state) {
|
||||
return Binary(state, spv::OpBitwiseAnd, TypeU32(state), EmitSubgroupLocalInvocationId(state),
|
||||
ConstantU32(state, 63));
|
||||
}
|
||||
|
||||
uint32_t BufferByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem) {
|
||||
auto& state = ctx.state;
|
||||
const auto packed = StorageBufferPackedStride(state, mem);
|
||||
const auto stride = packed & 0x3fffu;
|
||||
auto index = ctx.Arg(inst, 1);
|
||||
if ((packed & (1u << 20u)) != 0u) {
|
||||
index = Binary(state, spv::OpIAdd, TypeU32(state), index, BufferLane(state));
|
||||
}
|
||||
return Binary(state, spv::OpIAdd, TypeU32(state), address, soffset);
|
||||
const bool swizzle = stride != 0u && (packed & (1u << 14u)) != 0u;
|
||||
return CalculateBufferAddress(state, index, ctx.Arg(inst, 2), ctx.Arg(inst, 3), mem.offset,
|
||||
ConstantU32(state, stride),
|
||||
swizzle ? ConstantBool(state, true) : 0u,
|
||||
ConstantU32(state, (packed >> 16u) & 3u))
|
||||
.byte;
|
||||
}
|
||||
|
||||
uint32_t AddU64Low(EmitterState& state, uint32_t low, uint32_t high, uint32_t add_low,
|
||||
@@ -196,11 +201,8 @@ uint32_t LoadBdaDword(ValueEmitContext& ctx, uint32_t address) {
|
||||
});
|
||||
}
|
||||
|
||||
uint32_t LoadBda(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
|
||||
uint32_t bits) {
|
||||
auto& state = ctx.state;
|
||||
const auto address = GuestAddress(ctx, inst, mem);
|
||||
const auto active = ctx.Arg(inst, inst.NumArgs() - 1);
|
||||
uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint32_t bits) {
|
||||
auto& state = ctx.state;
|
||||
return EmitValueOrZeroIfCondition(state, active, [&]() {
|
||||
const auto aligned = Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), address,
|
||||
ConstantDeviceAddress(state, ~uint64_t {3}));
|
||||
@@ -236,8 +238,7 @@ uint32_t LoadBda(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryIn
|
||||
|
||||
uint32_t ByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem) {
|
||||
if (mem.kind == IR::ResourceKind::Buffer) {
|
||||
return BufferByteAddress(ctx, inst, mem, ctx.Arg(inst, 1), ctx.Arg(inst, 2),
|
||||
ctx.Arg(inst, 3));
|
||||
return BufferByteAddress(ctx, inst, mem);
|
||||
}
|
||||
if (mem.kind == IR::ResourceKind::Lds || mem.kind == IR::ResourceKind::Gds) {
|
||||
if (mem.offset == 0u) {
|
||||
@@ -734,12 +735,86 @@ void FormattedStore(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memor
|
||||
});
|
||||
}
|
||||
|
||||
uint32_t LoadIndirectBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t components) {
|
||||
auto& state = ctx.state;
|
||||
const auto& handle = *inst.Arg(0).ResolveInstruction();
|
||||
const auto word1 = ctx.Arg(handle, 1);
|
||||
const auto records = ctx.Arg(handle, 2);
|
||||
const auto word3 = ctx.Arg(handle, 3);
|
||||
const auto field = [&](uint32_t word, uint32_t first, uint32_t count) {
|
||||
return EmitBitFieldUExtract(state, word, ConstantU32(state, first),
|
||||
ConstantU32(state, count));
|
||||
};
|
||||
const auto nonzero = [&](uint32_t value) {
|
||||
return Binary(state, spv::OpINotEqual, TypeBool(state), value, ConstantU32(state, 0));
|
||||
};
|
||||
const auto has_dword = [&](uint32_t offset, uint32_t size) {
|
||||
return AndCondition(
|
||||
state,
|
||||
Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), size, ConstantU32(state, 4)),
|
||||
Binary(state, spv::OpULessThanEqual, TypeBool(state), offset,
|
||||
Binary(state, spv::OpISub, TypeU32(state), size, ConstantU32(state, 4))));
|
||||
};
|
||||
const auto stride = field(word1, 16, 14);
|
||||
const auto swizzle = AndCondition(state, nonzero(stride), nonzero(field(word1, 31, 1)));
|
||||
const auto index_stride = field(word3, 21, 2);
|
||||
const auto add_tid = nonzero(field(word3, 23, 1));
|
||||
const auto index =
|
||||
Binary(state, spv::OpIAdd, TypeU32(state), ctx.Arg(inst, 1),
|
||||
Select(state, TypeU32(state), add_tid, BufferLane(state), ConstantU32(state, 0)));
|
||||
const auto soffset = ctx.Arg(inst, 3);
|
||||
const auto base = DeviceAddressFromWords(state, ctx.Arg(handle, 0), field(word1, 0, 16));
|
||||
const auto valid_format = nonzero(field(word3, 12, 7));
|
||||
const auto mode = field(word3, 28, 2);
|
||||
const auto index_in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index, records);
|
||||
const auto scalar_in_bounds =
|
||||
Binary(state, spv::OpULessThanEqual, TypeBool(state), soffset, records);
|
||||
const auto raw_records = Binary(state, spv::OpISub, TypeU32(state), records, soffset);
|
||||
const auto raw_index_in_bounds =
|
||||
Binary(state, spv::OpULessThan, TypeBool(state), index, raw_records);
|
||||
std::array<uint32_t, 4> values {};
|
||||
for (uint32_t component = 0; component < components; component++) {
|
||||
const auto address = CalculateBufferAddress(state, index, ctx.Arg(inst, 2), soffset,
|
||||
ctx.Memory(inst).offset + component * 4u,
|
||||
stride, swizzle, index_stride);
|
||||
// RDNA2 raw DWORD vectors check each component, unlike formatted accesses.
|
||||
const auto structured_bounds =
|
||||
AndCondition(state, index_in_bounds,
|
||||
Binary(state, spv::OpULessThan, TypeBool(state), address.offset, stride));
|
||||
// OOB_SELECT=3 reduces NUM_RECORDS by SOFFSET before the offset/index check.
|
||||
const auto raw_bounds = AndCondition(
|
||||
state, scalar_in_bounds,
|
||||
Select(state, TypeBool(state), swizzle,
|
||||
AndCondition(state, raw_index_in_bounds, has_dword(address.offset, stride)),
|
||||
has_dword(address.offset, raw_records)));
|
||||
auto in_bounds =
|
||||
Select(state, TypeBool(state),
|
||||
Binary(state, spv::OpIEqual, TypeBool(state), mode, ConstantU32(state, 0)),
|
||||
structured_bounds, index_in_bounds);
|
||||
in_bounds = Select(
|
||||
state, TypeBool(state),
|
||||
Binary(state, spv::OpULessThan, TypeBool(state), mode, ConstantU32(state, 2)),
|
||||
in_bounds,
|
||||
Select(state, TypeBool(state),
|
||||
Binary(state, spv::OpIEqual, TypeBool(state), mode, ConstantU32(state, 2)),
|
||||
nonzero(records), raw_bounds));
|
||||
const auto guest =
|
||||
Binary(state, spv::OpIAdd, TypeScalarU64(state), base,
|
||||
Unary(state, spv::OpUConvert, TypeScalarU64(state), address.byte));
|
||||
values[component] = LoadBda(ctx, guest, AndCondition(state, valid_format, in_bounds), 32u);
|
||||
}
|
||||
return ConstructU32Composite(state, components, values);
|
||||
}
|
||||
|
||||
uint32_t LoadWideBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t components) {
|
||||
auto& state = ctx.state;
|
||||
return EmitValueOrDefaultIfCondition(
|
||||
state, ctx.Arg(inst, inst.NumArgs() - 1), TypeU32Composite(state, components),
|
||||
ConstantU32CompositeZero(state, components), [&]() {
|
||||
const auto mem = ctx.Memory(inst);
|
||||
if (mem.kind == IR::ResourceKind::IndirectBuffer) {
|
||||
return LoadIndirectBuffer(ctx, inst, components);
|
||||
}
|
||||
const auto resource = PrepareMemoryResourceAccess(state, mem);
|
||||
const auto info = Format::GetFormatInfo(
|
||||
mem.formatted ? BufferFormat(ctx, mem) : Prospero::BufferFormat::kInvalid);
|
||||
@@ -1088,7 +1163,8 @@ void EmitLoadMemory(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
value = LoadWideShared(ctx, inst, shared_components);
|
||||
else if (address_info.access == IR::AddressAccess::Read &&
|
||||
mem.kind != IR::ResourceKind::Scratch)
|
||||
value = LoadBda(ctx, inst, mem, address_info.data_bits);
|
||||
value = LoadBda(ctx, GuestAddress(ctx, inst, mem), ctx.Arg(inst, inst.NumArgs() - 1),
|
||||
address_info.data_bits);
|
||||
else if (op == IR::ValueOpcode::LoadBufferU32 && mem.formatted)
|
||||
value = FormattedLoad(ctx, inst, mem);
|
||||
else if (inst.GetType() == IR::Type::U8)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h"
|
||||
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h"
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
|
||||
namespace {
|
||||
@@ -127,10 +127,7 @@ void EmitMeshEntryPoint(EmitterState& state) {
|
||||
state.builder.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.builder.AllocateId(),
|
||||
state.mesh_guest_func);
|
||||
// All guest waves finish before the uniform Vulkan allocation and output stores.
|
||||
state.builder.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup),
|
||||
ConstantU32(state, spv::ScopeWorkgroup),
|
||||
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask |
|
||||
spv::MemorySemanticsWorkgroupMemoryMask));
|
||||
EmitBarrier(state);
|
||||
const auto vertices = MeshLoad(state, state.mesh_allocation, spv::StorageClassWorkgroup,
|
||||
TypeU32(state), ConstantU32(state, 0));
|
||||
const auto primitives = MeshLoad(state, state.mesh_allocation, spv::StorageClassWorkgroup,
|
||||
|
||||
@@ -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,13 +521,14 @@ 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(),
|
||||
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), true},
|
||||
{Opcode::V_CVT_F16_U16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
|
||||
SdwaSelWords() | SdwaSelFull(), false},
|
||||
{Opcode::V_CVT_F16_U16, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
|
||||
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), false},
|
||||
{Opcode::V_CVT_U16_F16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
|
||||
SdwaSelWords() | SdwaSelFull(), true},
|
||||
{Opcode::V_CVT_F16_I16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
|
||||
@@ -617,13 +618,14 @@ bool SupportsVop1Clamp(Opcode opcode) {
|
||||
}
|
||||
|
||||
bool ValidateVop1Sdwa(Instruction& inst, uint32_t opcode, uint32_t modifier) {
|
||||
const auto dst_sel = (modifier >> 8u) & 0x7u;
|
||||
const auto dst_u = (modifier >> 11u) & 0x3u;
|
||||
const auto clamp = (modifier >> 13u) & 0x1u;
|
||||
const auto omod = (modifier >> 14u) & 0x3u;
|
||||
const auto src0_sel = (modifier >> 16u) & 0x7u;
|
||||
const auto src0_neg = (modifier >> 20u) & 0x1u;
|
||||
const auto src0_abs = (modifier >> 21u) & 0x1u;
|
||||
const auto dst_sel = (modifier >> 8u) & 0x7u;
|
||||
const auto dst_u = (modifier >> 11u) & 0x3u;
|
||||
const auto clamp = (modifier >> 13u) & 0x1u;
|
||||
const auto omod = (modifier >> 14u) & 0x3u;
|
||||
const auto src0_sel = (modifier >> 16u) & 0x7u;
|
||||
const auto src0_sext = (modifier >> 19u) & 0x1u;
|
||||
const auto src0_neg = (modifier >> 20u) & 0x1u;
|
||||
const auto src0_abs = (modifier >> 21u) & 0x1u;
|
||||
|
||||
if (src0_sel > 6u || dst_sel > 6u) {
|
||||
SetUnsupported(inst, Family::VOP1, opcode, "VOP1 SDWA selector is invalid");
|
||||
@@ -639,7 +641,8 @@ bool ValidateVop1Sdwa(Instruction& inst, uint32_t opcode, uint32_t modifier) {
|
||||
"VOP1 SDWA destination selector is not supported");
|
||||
return false;
|
||||
}
|
||||
if (!IsVop1SdwaSourceSupported(inst.opcode, src0_sel, src0_neg != 0u, src0_abs != 0u)) {
|
||||
if (!IsVop1SdwaSourceSupported(inst.opcode, src0_sel, src0_neg != 0u, src0_abs != 0u) ||
|
||||
(inst.opcode == Opcode::V_CVT_F16_U16 && src0_sel <= 3u && src0_sext != 0u)) {
|
||||
SetUnsupported(inst, Family::VOP1, opcode, "VOP1 SDWA source selector is not supported");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1225,6 +1225,12 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
||||
builtin(IR::StageInputKind::PackedAncillary));
|
||||
}
|
||||
} else if (options.stage == ShaderType::Vertex) {
|
||||
// Vulkan owns primitive assembly; each vertex subgroup is one NGG wave.
|
||||
// Keep its full lane extent: mbcnt(-1) uses lane ordinals, not active counts.
|
||||
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(2),
|
||||
IR::U32(IR::Value(options.wave_size << 12u)));
|
||||
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3),
|
||||
IR::U32(IR::Value((1u << 28u) | options.wave_size)));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
|
||||
builtin(IR::StageInputKind::VertexIndex));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -447,13 +447,20 @@ void ValidateProgram(const Program& program, bool require_ssa) {
|
||||
ValueOpcodeName(inst.GetOpcode())));
|
||||
}
|
||||
const auto& memory = program.memory_info[memory_index];
|
||||
if (memory.kind != ResourceKind::Buffer &&
|
||||
memory.kind != ResourceKind::ScalarBuffer) {
|
||||
const bool vector_buffer = memory.kind == ResourceKind::Buffer ||
|
||||
memory.kind == ResourceKind::IndirectBuffer;
|
||||
if (!vector_buffer && memory.kind != ResourceKind::ScalarBuffer) {
|
||||
return Fail(fmt::format("{} has a non-buffer resource kind",
|
||||
ValueOpcodeName(inst.GetOpcode())));
|
||||
}
|
||||
if (memory.kind == ResourceKind::IndirectBuffer &&
|
||||
(memory.formatted || memory.typed ||
|
||||
(inst.GetOpcode() != ValueOpcode::LoadBufferU32x2 &&
|
||||
inst.GetOpcode() != ValueOpcode::LoadBufferU32x4))) {
|
||||
return Fail("indirect buffer requires a raw DWORD x2/x4 load");
|
||||
}
|
||||
if (buffer_components > 1u &&
|
||||
(memory.kind != ResourceKind::Buffer || memory.data_bits != 32u ||
|
||||
(!vector_buffer || memory.data_bits != 32u ||
|
||||
memory.data_dwords != buffer_components || memory.component_index != 0u)) {
|
||||
return Fail(fmt::format("{} has inconsistent native-wide metadata",
|
||||
ValueOpcodeName(inst.GetOpcode())));
|
||||
|
||||
@@ -27,6 +27,7 @@ enum class ResourceKind {
|
||||
ScalarBuffer,
|
||||
ScalarAddress,
|
||||
Buffer,
|
||||
IndirectBuffer,
|
||||
Flat,
|
||||
Global,
|
||||
Scratch,
|
||||
|
||||
@@ -357,16 +357,23 @@ struct SamplerPlan {
|
||||
};
|
||||
|
||||
struct ImageRemap {
|
||||
explicit ImageRemap(const ResourceSpecialization& specialization) {
|
||||
for (const auto& image: specialization.images) {
|
||||
indices.push_back(image.fmask ? UINT32_MAX : count++);
|
||||
explicit ImageRemap(const ResourceSpecialization& specialization)
|
||||
: source_count(static_cast<uint32_t>(specialization.images.size())) {
|
||||
EXIT_IF(specialization.images.size() > indices.size());
|
||||
for (uint32_t index = 0; index < source_count; index++) {
|
||||
indices[index] = specialization.images[index].fmask ? UINT32_MAX : count++;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t operator[](uint32_t index) const {
|
||||
EXIT_IF(index >= source_count);
|
||||
return indices[index];
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void Apply(std::vector<T>& images) const {
|
||||
EXIT_IF(images.size() != indices.size());
|
||||
for (uint32_t index = 0; index < indices.size(); index++) {
|
||||
EXIT_IF(images.size() != source_count);
|
||||
for (uint32_t index = 0; index < source_count; index++) {
|
||||
if (indices[index] != UINT32_MAX && indices[index] != index) {
|
||||
images[indices[index]] = std::move(images[index]);
|
||||
}
|
||||
@@ -374,8 +381,10 @@ struct ImageRemap {
|
||||
images.resize(count);
|
||||
}
|
||||
|
||||
std::vector<uint32_t> indices;
|
||||
uint32_t count = 0;
|
||||
private:
|
||||
std::array<uint32_t, ShaderInfo::MaxImages> indices;
|
||||
uint32_t source_count;
|
||||
uint32_t count = 0;
|
||||
};
|
||||
|
||||
template <typename Images>
|
||||
@@ -1075,29 +1084,29 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
for (auto* block: program.blocks) {
|
||||
for (auto& inst: *block) {
|
||||
if (inst.GetOpcode() == ValueOpcode::GetImageResource) {
|
||||
inst.SetFlags(image_remap.indices.at(inst.Flags<uint32_t>()));
|
||||
inst.SetFlags(image_remap[inst.Flags<uint32_t>()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto& memory: memory_info) {
|
||||
if (memory.kind == ResourceKind::Image && !memory.planning_only) {
|
||||
memory.resource = image_remap.indices.at(memory.resource);
|
||||
memory.resource = image_remap[memory.resource];
|
||||
}
|
||||
}
|
||||
for (auto& buffer: buffers) {
|
||||
if (buffer.image_alias != BufferResource::NoImageAlias) {
|
||||
buffer.image_alias = image_remap.indices.at(buffer.image_alias);
|
||||
buffer.image_alias = image_remap[buffer.image_alias];
|
||||
}
|
||||
}
|
||||
for (auto& pair: sampled_pairs) {
|
||||
pair.image = image_remap.indices.at(pair.image);
|
||||
pair.image = image_remap[pair.image];
|
||||
}
|
||||
for (auto& image: images) {
|
||||
if (image.indirect_root != ImageResource::NoIndirectImage) {
|
||||
image.indirect_root = image_remap.indices.at(image.indirect_root);
|
||||
image.indirect_root = image_remap[image.indirect_root];
|
||||
}
|
||||
for (auto& resource: image.indirect_resources) {
|
||||
resource = image_remap.indices.at(resource);
|
||||
resource = image_remap[resource];
|
||||
}
|
||||
}
|
||||
image_remap.Apply(images);
|
||||
|
||||
@@ -526,7 +526,7 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
void GetHandle(Value value, ValueOpcode expected, uint32_t width, uint32_t pc, Inst*& handle,
|
||||
bool GetHandle(Value value, ValueOpcode expected, uint32_t width, uint32_t pc, Inst*& handle,
|
||||
uint32_t& source, bool sampler = false, bool sample_adjust = false) {
|
||||
handle = value.Resolve().TryInstruction();
|
||||
if (handle == nullptr || handle->GetOpcode() != expected) {
|
||||
@@ -535,21 +535,17 @@ private:
|
||||
DescriptorSource descriptor;
|
||||
MakeSource(*handle, width, sampler, sample_adjust, descriptor, pc);
|
||||
uint32_t bad_dword = 0;
|
||||
if (expected == ValueOpcode::GetImageResource) {
|
||||
for (; bad_dword < descriptor.dword_count; bad_dword++) {
|
||||
const auto* value = descriptor.dwords[bad_dword].Resolve().TryInstruction();
|
||||
if (value != nullptr && value->GetOpcode() == ValueOpcode::ReadConstBuffer) {
|
||||
Fail(pc, fmt::format("{} dword {} is not a valid runtime value",
|
||||
ValueOpcodeName(expected), bad_dword));
|
||||
}
|
||||
}
|
||||
bad_dword = 0;
|
||||
}
|
||||
if (!ValidateSource(descriptor, bad_dword)) {
|
||||
if (expected == ValueOpcode::GetBufferResource &&
|
||||
std::all_of(descriptor.dwords.begin(), descriptor.dwords.begin() + width,
|
||||
[](Value word) { return word.Resolve().GetType() == Type::U32; })) {
|
||||
return false;
|
||||
}
|
||||
Fail(pc, fmt::format("{} dword {} is not a valid runtime value",
|
||||
ValueOpcodeName(expected), bad_dword));
|
||||
}
|
||||
source = InternSource(descriptor);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ValidateAddressHandle(Value value, uint32_t pc) const {
|
||||
@@ -720,7 +716,18 @@ private:
|
||||
uint32_t resource = 0;
|
||||
|
||||
if (buffer != BufferAccess::None) {
|
||||
GetHandle(inst.Arg(0), ValueOpcode::GetBufferResource, 4, flags.pc, handle, source);
|
||||
if (!GetHandle(inst.Arg(0), ValueOpcode::GetBufferResource, 4, flags.pc, handle,
|
||||
source)) {
|
||||
if (memory.kind != ResourceKind::Buffer || memory.formatted || memory.typed ||
|
||||
(op != ValueOpcode::LoadBufferU32x2 && op != ValueOpcode::LoadBufferU32x4)) {
|
||||
Fail(flags.pc,
|
||||
"buffer descriptor is not a valid runtime value; GPU-selected access "
|
||||
"requires a raw DWORD x2/x4 load");
|
||||
}
|
||||
m_program.memory_info[flags.index].kind = ResourceKind::IndirectBuffer;
|
||||
m_info.uses_dma = true;
|
||||
return;
|
||||
}
|
||||
resource = AddBuffer(source, memory, op, flags.pc);
|
||||
if (resource == UINT32_MAX) {
|
||||
Fail(flags.pc, "buffer resource limit exceeded");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -651,6 +651,7 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
|
||||
key.push_back(static_cast<uint32_t>(info.fetch_attrib_reg));
|
||||
key.push_back(static_cast<uint32_t>(info.fetch_buffer_reg));
|
||||
key.push_back(info.resources_num);
|
||||
key.push_back(info.wave_size);
|
||||
key.push_back(info.scratch_size_dwords);
|
||||
key.push_back(info.pa_cl_vs_out_cntl);
|
||||
key.push_back(static_cast<uint32_t>(info.clip_space.enabled));
|
||||
@@ -764,6 +765,7 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
|
||||
regs.gs_regs.rsrc2.user_sgpr, sh, data, info)) {
|
||||
EXIT("failed to prepare vertex shader program\n");
|
||||
}
|
||||
info.wave_size = (context.GetShaderStages() & 0x00400000u) != 0 ? 32u : 64u;
|
||||
return params;
|
||||
}
|
||||
// NGG user SGPRs start at s8; a separately compiled GS back half also receives
|
||||
|
||||
@@ -134,6 +134,7 @@ struct ShaderVertexInputInfo {
|
||||
int fetch_attrib_reg = 0;
|
||||
int fetch_buffer_reg = 0;
|
||||
int buffers_num = 0;
|
||||
uint32_t wave_size = 64;
|
||||
uint32_t scratch_size_dwords = 0;
|
||||
uint32_t pa_cl_vs_out_cntl = 0;
|
||||
ShaderClipSpaceTransform clip_space;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 |
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -464,6 +464,14 @@ private:
|
||||
const auto* chunk = input + offset;
|
||||
const auto chunk_size = static_cast<size_t>(AjmReadLe32(chunk + 4));
|
||||
const auto payload = offset + 8;
|
||||
// The data chunk describes the whole stream; this job may contain only its prefix.
|
||||
if (AjmFourCcEquals(chunk, 'd', 'a', 't', 'a')) {
|
||||
*data_offset = payload;
|
||||
result->input_consumed = payload;
|
||||
result->format = GetFormat();
|
||||
result->total_decoded_samples = m_total_decoded_samples;
|
||||
return true;
|
||||
}
|
||||
if (payload > input_size || chunk_size > input_size - payload) {
|
||||
result->result = AJM_RESULT_PARTIAL_INPUT;
|
||||
return false;
|
||||
@@ -496,12 +504,6 @@ private:
|
||||
LOGF("AJM ATRAC9 gapless: total=%" PRIu32 ", skip=%" PRIu16 "\n",
|
||||
gapless_decode.total_samples, gapless_decode.skip_samples);
|
||||
}
|
||||
} else if (AjmFourCcEquals(chunk, 'd', 'a', 't', 'a')) {
|
||||
*data_offset = payload;
|
||||
result->input_consumed = payload;
|
||||
result->format = GetFormat();
|
||||
result->total_decoded_samples = m_total_decoded_samples;
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto padded_size = chunk_size + (chunk_size & 1u);
|
||||
|
||||
+56
-58
@@ -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");
|
||||
@@ -846,11 +852,15 @@ public:
|
||||
emit_event(stop_event, AVPLAYER_EVENT_STATE_STOP);
|
||||
return 0;
|
||||
}
|
||||
void Pause() {
|
||||
int Pause() {
|
||||
std::scoped_lock lifecycle_lock(lifecycle_mutex);
|
||||
std::lock_guard lock(mutex);
|
||||
if (state != State::Playing) {
|
||||
return AVPLAYER_ERROR_OPERATION_FAILED;
|
||||
}
|
||||
paused = true;
|
||||
pause_time = std::chrono::steady_clock::now();
|
||||
return 0;
|
||||
}
|
||||
void Resume() {
|
||||
std::scoped_lock lifecycle_lock(lifecycle_mutex);
|
||||
@@ -872,7 +882,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);
|
||||
@@ -883,12 +894,15 @@ public:
|
||||
bool was_paused = false;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (state != State::Playing) {
|
||||
return AVPLAYER_ERROR_OPERATION_FAILED;
|
||||
}
|
||||
was_paused = paused;
|
||||
}
|
||||
return StartImpl(ms, was_paused, true);
|
||||
}
|
||||
uint64_t CurrentTimeNoLock() const {
|
||||
if (stopped) {
|
||||
if (state != State::Playing) {
|
||||
return 0;
|
||||
}
|
||||
using namespace std::chrono;
|
||||
@@ -925,7 +939,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 +969,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 +979,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 +1001,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 +1013,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 +1210,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 +1300,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 +1347,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 +1412,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 +1426,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 +1469,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 +1554,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 +1665,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};
|
||||
@@ -1698,6 +1689,7 @@ struct AvPlayerInternal {
|
||||
AvPlayerPostInitData post_init {};
|
||||
bool auto_start = false;
|
||||
int32_t video_buffers = 2;
|
||||
uint32_t sync_mode = 0;
|
||||
uint32_t start_bandwidth = 0;
|
||||
uint32_t minimum_bandwidth = 0;
|
||||
uint32_t maximum_bandwidth = 0;
|
||||
@@ -1746,6 +1738,7 @@ static int add_source(AvPlayerInternal* h, const std::string& filename, AvPlayer
|
||||
if (auto rc = s->Init(filename, type); rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
s->SetSync(h->sync_mode);
|
||||
h->source = std::move(s);
|
||||
emit_event(h->event, AVPLAYER_EVENT_STATE_READY);
|
||||
if (h->auto_start) {
|
||||
@@ -1931,9 +1924,11 @@ int KYTY_SYSV_ABI AvPlayerPause(AvPlayerInternal* h) {
|
||||
if (h == nullptr || h->source == nullptr) {
|
||||
return AVPLAYER_ERROR_INVALID_PARAMS;
|
||||
}
|
||||
h->source->Pause();
|
||||
emit_event(h->event, AVPLAYER_EVENT_STATE_PAUSE);
|
||||
return 0;
|
||||
auto rc = h->source->Pause();
|
||||
if (rc == 0) {
|
||||
emit_event(h->event, AVPLAYER_EVENT_STATE_PAUSE);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
int KYTY_SYSV_ABI AvPlayerResume(AvPlayerInternal* h) {
|
||||
PRINT_NAME();
|
||||
@@ -1967,13 +1962,16 @@ int KYTY_SYSV_ABI AvPlayerSetTrickSpeed(AvPlayerInternal* h, int32_t trick_speed
|
||||
}
|
||||
int KYTY_SYSV_ABI AvPlayerSetAvSyncMode(AvPlayerInternal* h, uint32_t sync_mode) {
|
||||
PRINT_NAME();
|
||||
if (h == nullptr || h->source == nullptr) {
|
||||
if (h == nullptr) {
|
||||
return AVPLAYER_ERROR_INVALID_PARAMS;
|
||||
}
|
||||
if (sync_mode > 1) {
|
||||
return AVPLAYER_ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
h->source->SetSync(sync_mode);
|
||||
h->sync_mode = sync_mode;
|
||||
if (h->source != nullptr) {
|
||||
h->source->SetSync(sync_mode);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int KYTY_SYSV_ABI AvPlayerSetAvailableBandwidth(AvPlayerInternal* h, uint32_t start_bandwidth,
|
||||
@@ -2029,7 +2027,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();
|
||||
|
||||
+59
-16
@@ -27,6 +27,7 @@ namespace SaveData {
|
||||
|
||||
// TODO(): specify dir at launcher
|
||||
static constexpr char SAVE_DATA_DIR[] = "_SaveData";
|
||||
static constexpr uint64_t SAVE_DATA_BLOCKS_MIN = 48;
|
||||
static constexpr uint64_t SAVE_DATA_BLOCKS_MAX = 16384;
|
||||
|
||||
struct SceSaveDataDirName {
|
||||
@@ -305,9 +306,9 @@ static bool valid_memory_range(const SaveDataMemoryData& data, size_t size) {
|
||||
data.buf_size <= size - static_cast<size_t>(data.offset);
|
||||
}
|
||||
|
||||
// Keep memory.dat as raw guest bytes. Replacing a flushed temporary file protects the
|
||||
// Replacing a flushed temporary file protects the
|
||||
// previous save if writing fails or the emulator exits partway through an update.
|
||||
static int write_memory_file(const std::filesystem::path& path, const void* data, uint32_t size) {
|
||||
static int write_save_file(const std::filesystem::path& path, const void* data, uint32_t size) {
|
||||
const auto temporary = std::filesystem::path(path.string() + ".tmp");
|
||||
Common::File file;
|
||||
if (!file.Create(temporary)) {
|
||||
@@ -329,6 +330,21 @@ static int write_memory_file(const std::filesystem::path& path, const void* data
|
||||
return SAVE_DATA_ERROR_INTERNAL;
|
||||
}
|
||||
|
||||
static int read_save_blocks(const std::filesystem::path& directory, uint64_t* blocks) {
|
||||
Common::File file(directory / "sce_sys" / "blocks.bin", Common::File::Mode::Read);
|
||||
if (file.IsInvalid() || file.Size() != sizeof(*blocks)) {
|
||||
return SAVE_DATA_ERROR_BROKEN;
|
||||
}
|
||||
uint64_t value = 0;
|
||||
uint32_t read = 0;
|
||||
file.Read(&value, sizeof(value), &read);
|
||||
if (read != sizeof(value) || value < SAVE_DATA_BLOCKS_MIN || value > SAVE_DATA_BLOCKS_MAX) {
|
||||
return SAVE_DATA_ERROR_BROKEN;
|
||||
}
|
||||
*blocks = value;
|
||||
return OK;
|
||||
}
|
||||
|
||||
static int save_memory(const std::filesystem::path& directory, const SaveDataMemory& memory) {
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(directory, error);
|
||||
@@ -336,10 +352,10 @@ static int save_memory(const std::filesystem::path& directory, const SaveDataMem
|
||||
return SAVE_DATA_ERROR_INTERNAL;
|
||||
}
|
||||
// Display metadata is a separate host sidecar, leaving memory.dat as raw guest bytes.
|
||||
int result = write_memory_file(directory / "param.bin", &memory.param, sizeof(memory.param));
|
||||
int result = write_save_file(directory / "param.bin", &memory.param, sizeof(memory.param));
|
||||
if (result == OK) {
|
||||
result =
|
||||
write_memory_file(directory / "memory.dat", memory.data.data(), memory.data.size());
|
||||
write_save_file(directory / "memory.dat", memory.data.data(), memory.data.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -561,9 +577,14 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
|
||||
result->params[i] = {};
|
||||
}
|
||||
if (result->infos != nullptr) {
|
||||
result->infos[i] = {};
|
||||
result->infos[i].blocks = SAVE_DATA_BLOCKS_MAX;
|
||||
result->infos[i].free_blocks = SAVE_DATA_BLOCKS_MAX;
|
||||
auto& info = result->infos[i];
|
||||
info = {};
|
||||
const int status = read_save_blocks(root / dir_list[i], &info.blocks);
|
||||
if (status != OK) {
|
||||
return status;
|
||||
}
|
||||
// Host-directory saves report their allocation as free space.
|
||||
info.free_blocks = info.blocks;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,10 +642,22 @@ int KYTY_SYSV_ABI SaveDataMount3(const SaveDataMount3* mount, SaveDataMountResul
|
||||
|
||||
bool created = false;
|
||||
if ((create || create2) && !Common::File::IsDirectoryExisting(mount_dir)) {
|
||||
Common::File::CreateDirectories(mount_dir);
|
||||
if (mount->blocks < SAVE_DATA_BLOCKS_MIN || mount->blocks > SAVE_DATA_BLOCKS_MAX) {
|
||||
return SAVE_DATA_ERROR_PARAMETER;
|
||||
}
|
||||
const auto metadata = std::filesystem::path(mount_dir) / "sce_sys";
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(metadata, error);
|
||||
if (error) {
|
||||
return SAVE_DATA_ERROR_INTERNAL;
|
||||
}
|
||||
const int status = write_save_file(metadata / "blocks.bin", &mount->blocks,
|
||||
sizeof(mount->blocks));
|
||||
if (status != OK) {
|
||||
std::filesystem::remove_all(mount_dir, error);
|
||||
return status;
|
||||
}
|
||||
created = true;
|
||||
|
||||
EXIT_NOT_IMPLEMENTED((!Common::File::IsDirectoryExisting(mount_dir)));
|
||||
}
|
||||
|
||||
return mount_save_data(slot, dir_name, mount_dir, created ? 1u : 0u, mount_result);
|
||||
@@ -1101,13 +1134,23 @@ int KYTY_SYSV_ABI SaveDataGetMountInfo(const SaveDataMountPoint* mount_point,
|
||||
SaveDataMountInfo* info) {
|
||||
PRINT_NAME();
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(mount_point == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(info == nullptr);
|
||||
|
||||
if (mount_point == nullptr || info == nullptr ||
|
||||
std::memchr(mount_point->data, 0, sizeof(mount_point->data)) == nullptr) {
|
||||
return SAVE_DATA_ERROR_PARAMETER;
|
||||
}
|
||||
Common::LockGuard lock(g_mount_mutex);
|
||||
if (g_mount_slots.Find(mount_point->data) == SaveDataMountSlots::FULL) {
|
||||
return SAVE_DATA_ERROR_NOT_MOUNTED;
|
||||
}
|
||||
uint64_t blocks = 0;
|
||||
const int status = read_save_blocks(
|
||||
LibKernel::FileSystem::GetRealFilename(mount_point->data), &blocks);
|
||||
if (status != OK) {
|
||||
return status;
|
||||
}
|
||||
*info = {};
|
||||
|
||||
info->blocks = SAVE_DATA_BLOCKS_MAX;
|
||||
info->free_blocks = SAVE_DATA_BLOCKS_MAX;
|
||||
info->blocks = blocks;
|
||||
info->free_blocks = blocks;
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
@@ -164,6 +164,7 @@ void Psf::OpenJson(const std::filesystem::path& file_name) {
|
||||
add_string_param("contentId", "CONTENT_ID");
|
||||
add_string_param("contentVersion", "APP_VER");
|
||||
add_string_param("appVersion", "APP_VER");
|
||||
add_int_param("attribute3", "ATTRIBUTE3");
|
||||
add_int_param("userDefinedParam1", "USER_DEFINED_PARAM_1");
|
||||
add_int_param("userDefinedParam2", "USER_DEFINED_PARAM_2");
|
||||
add_int_param("userDefinedParam3", "USER_DEFINED_PARAM_3");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -213,8 +213,11 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 0,
|
||||
fixture->AddMemory(shared_buffer, 0x10d8));
|
||||
fixture->Emit(ValueOpcode::ReferenceU32, {load});
|
||||
}
|
||||
const auto invocation = fixture->Emit(
|
||||
ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::GlobalInvocationId)), Value(0u)});
|
||||
const auto selector = fixture->Emit(ValueOpcode::ReadFirstLane,
|
||||
{fixture->UserData(8), Value(true)});
|
||||
{invocation, Value(true)});
|
||||
const auto record =
|
||||
fixture->Emit(ValueOpcode::IMul32, {selector, Value(224u)});
|
||||
const auto member = fixture->Emit(ValueOpcode::IAdd32, {record, Value(4u)});
|
||||
@@ -457,6 +460,83 @@ void TestInvariantIndirectImageMaterialization() {
|
||||
"wrapped scalar immediate entered the invariant image proof");
|
||||
}
|
||||
|
||||
void TestUniformScalarBufferImage() {
|
||||
Fixture fixture(ShaderType::Pixel);
|
||||
std::array<Value, 4> material_words;
|
||||
std::array<Value, 4> heap_words;
|
||||
for (uint32_t dword = 0; dword < 4; dword++) {
|
||||
material_words[dword] = fixture.UserData(dword);
|
||||
heap_words[dword] = fixture.UserData(dword + 4u);
|
||||
}
|
||||
const auto material = fixture.Buffer(material_words);
|
||||
const auto heap = fixture.Buffer(heap_words);
|
||||
const auto selector = fixture.Emit(ValueOpcode::ShiftLeftLogical32,
|
||||
{fixture.UserData(8), Value(2u)});
|
||||
MemoryInfo scalar;
|
||||
scalar.kind = ResourceKind::ScalarBuffer;
|
||||
scalar.offset = 0x40u;
|
||||
const auto key = fixture.Emit(ValueOpcode::ReadConstBuffer,
|
||||
{material, selector}, fixture.AddMemory(scalar, 0x100));
|
||||
const auto record = fixture.Emit(ValueOpcode::IMul32, {key, Value(48u)});
|
||||
std::array<Value, 8> image_words;
|
||||
for (uint32_t dword = 0; dword < image_words.size(); dword++) {
|
||||
scalar.offset = 0x10u + dword * sizeof(uint32_t);
|
||||
image_words[dword] = fixture.Emit(ValueOpcode::ReadConstBuffer,
|
||||
{heap, record}, fixture.AddMemory(scalar, 0x200));
|
||||
}
|
||||
const auto image = fixture.Image(image_words);
|
||||
const auto sampler = fixture.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)});
|
||||
MemoryInfo sample;
|
||||
sample.kind = ResourceKind::Image;
|
||||
sample.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw,
|
||||
{image, sampler, fixture.ImageAddress()}, fixture.AddMemory(sample, 0x228));
|
||||
fixture.PlanAndTrack();
|
||||
const auto plan = ExtractResourcePlan(fixture.program);
|
||||
|
||||
std::array<uint32_t, 9> user_data{0x1000u, 0u, 0x100u, 0u,
|
||||
0x2000u, 0u, 0x100u, 0u, 0u};
|
||||
LinearTestMemory memory;
|
||||
memory.words[0x44u / 4u] = 1u;
|
||||
std::array<uint32_t, 8> descriptor{};
|
||||
descriptor[0] = 0x20u;
|
||||
descriptor[1] = static_cast<uint32_t>(
|
||||
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float)
|
||||
<< 20u;
|
||||
descriptor[2] = 3u | (3u << 14u);
|
||||
descriptor[3] = Libs::Graphics::DstSel(4, 5, 6, 7) |
|
||||
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D)
|
||||
<< 28u);
|
||||
for (uint32_t index = 0; index < 2; index++) {
|
||||
std::copy(descriptor.begin(), descriptor.end(),
|
||||
memory.words.begin() + (0x1010u + index * 48u) / 4u);
|
||||
memory.words[(0x1010u + index * 48u) / 4u] += index;
|
||||
}
|
||||
SrtRuntime runtime{.user_data = user_data,
|
||||
.read_memory = ReadLinearTestMemory,
|
||||
.userdata = &memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
const auto materializes = [&](uint32_t address) {
|
||||
return MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1 && snapshot.images[0].dwords[0] == address;
|
||||
};
|
||||
Check(materializes(0x20u), "nested draw-uniform image descriptor did not materialize");
|
||||
user_data[8] = 1u;
|
||||
Check(materializes(0x21u), "changed draw selector reused the previous image descriptor");
|
||||
memory.words[0x44u / 4u] = 0u;
|
||||
memory.words[0x1010u / 4u] = 0x30u;
|
||||
Check(materializes(0x30u), "changed scalar table memory reused the previous image descriptor");
|
||||
const auto prior_snapshot = snapshot;
|
||||
const auto prior_specialization = specialization;
|
||||
memory.fail_address = 0x1044u;
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
SameResourceSnapshot(snapshot, prior_snapshot) &&
|
||||
specialization == prior_specialization,
|
||||
"unavailable nested image table partially changed the resource snapshot");
|
||||
}
|
||||
|
||||
void TestComputeBufferFill() {
|
||||
struct Options {
|
||||
bool scalar = false;
|
||||
@@ -1246,7 +1326,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 +1404,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),
|
||||
@@ -2012,6 +2093,7 @@ int main() {
|
||||
Run("FMASK load specialization", TestFmaskLoadSpecialization);
|
||||
Run("dynamic storage mips", TestDynamicStorageMipTracking);
|
||||
Run("invariant indirect images", TestInvariantIndirectImageMaterialization);
|
||||
Run("draw-uniform scalar image", TestUniformScalarBufferImage);
|
||||
Run("SRT runtime", TestSrtFlatteningAndRuntimeMemoization);
|
||||
Run("dynamic SRT", TestDynamicSrtReadRemainsExplicit);
|
||||
Run("phi validation", TestPhiValidation);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
namespace {
|
||||
std::string g_test_title = "PPSA21564";
|
||||
std::filesystem::path g_mounted_directory;
|
||||
std::map<std::string, std::filesystem::path> g_test_mounts;
|
||||
} // namespace
|
||||
|
||||
namespace Loader {
|
||||
@@ -33,10 +34,14 @@ Common::Time GetTime() {
|
||||
} // namespace Loader
|
||||
|
||||
namespace Libs::LibKernel::FileSystem {
|
||||
void Mount(const std::filesystem::path& directory, const std::string&) {
|
||||
void Mount(const std::filesystem::path& directory, const std::string& point) {
|
||||
g_mounted_directory = directory;
|
||||
g_test_mounts[point] = directory;
|
||||
}
|
||||
void Umount(const std::string& point) { g_test_mounts.erase(point); }
|
||||
std::filesystem::path GetRealFilename(const std::string& point) {
|
||||
return g_test_mounts.at(point);
|
||||
}
|
||||
void Umount(const std::string&) {}
|
||||
} // namespace Libs::LibKernel::FileSystem
|
||||
|
||||
namespace {
|
||||
@@ -337,6 +342,7 @@ void TestClassicSavePaths() {
|
||||
mount.user_id = 1;
|
||||
mount.dir_name = &name;
|
||||
mount.mount_mode = text == "save.1" ? 1 : 4;
|
||||
mount.blocks = 48;
|
||||
SaveDataMountResult result {};
|
||||
CHECK(SaveDataMount3(&mount, &result) == OK);
|
||||
CHECK(g_mounted_directory == fs::path("_SaveData") / "CLASSIC" / text);
|
||||
@@ -385,6 +391,70 @@ void TestClassicSavePaths() {
|
||||
CHECK(Search(1) == names);
|
||||
}
|
||||
|
||||
void TestSaveAllocations() {
|
||||
Reset("CAPACITY");
|
||||
std::array<SceSaveDataDirName, 2> names {DirName("Options"), DirName("Player")};
|
||||
const std::array<uint64_t, 2> allocations {48, 96};
|
||||
for (const auto blocks : {0u, 47u, 16385u}) {
|
||||
struct SaveDataMount3 invalid {};
|
||||
invalid.user_id = 1;
|
||||
invalid.dir_name = &names[0];
|
||||
invalid.mount_mode = 4;
|
||||
invalid.blocks = blocks;
|
||||
SaveDataMountResult result {};
|
||||
CHECK(SaveDataMount3(&invalid, &result) == SAVE_DATA_ERROR_PARAMETER);
|
||||
CHECK(!fs::exists("_SaveData/CAPACITY/Options"));
|
||||
}
|
||||
for (size_t i = 0; i < names.size(); i++) {
|
||||
struct SaveDataMount3 mount {};
|
||||
mount.user_id = 1;
|
||||
mount.dir_name = &names[i];
|
||||
mount.mount_mode = 4;
|
||||
mount.blocks = allocations[i];
|
||||
SaveDataMountResult result {};
|
||||
CHECK(SaveDataMount3(&mount, &result) == OK);
|
||||
CHECK(result.mount_status == 1);
|
||||
std::ofstream(g_mounted_directory / "USR-DATA") << "saved payload";
|
||||
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
||||
SaveDataMountInfo info {};
|
||||
CHECK(SaveDataGetMountInfo(&result.mount_point, &info) == SAVE_DATA_ERROR_NOT_MOUNTED);
|
||||
}
|
||||
Reset("CAPACITY");
|
||||
std::array<SceSaveDataDirName, 2> found {};
|
||||
std::array<SaveDataSearchInfo, 2> infos {};
|
||||
SaveDataDirNameSearchCond cond {};
|
||||
cond.user_id = 1;
|
||||
SaveDataDirNameSearchResult search {};
|
||||
search.dir_names = found.data();
|
||||
search.dir_names_num = found.size();
|
||||
search.infos = infos.data();
|
||||
CHECK(SaveDataDirNameSearch(&cond, &search) == OK && search.set_num == 2);
|
||||
for (size_t i = 0; i < names.size(); i++) {
|
||||
CHECK(std::string(found[i].data) == names[i].data);
|
||||
CHECK(infos[i].blocks == allocations[i] && infos[i].free_blocks == allocations[i]);
|
||||
struct SaveDataMount3 mount {};
|
||||
mount.user_id = 1;
|
||||
mount.dir_name = &names[i];
|
||||
mount.mount_mode = 34;
|
||||
mount.blocks = 16384;
|
||||
SaveDataMountResult result {};
|
||||
CHECK(SaveDataMount3(&mount, &result) == OK && result.mount_status == 0);
|
||||
std::ofstream(g_mounted_directory / "USR-DATA", std::ios::app) << "more data";
|
||||
SaveDataMountInfo info {};
|
||||
CHECK(SaveDataGetMountInfo(&result.mount_point, &info) == OK);
|
||||
CHECK(info.blocks == allocations[i] && info.free_blocks == allocations[i]);
|
||||
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
|
||||
}
|
||||
CHECK(infos[0].blocks + infos[1].blocks == 144);
|
||||
const fs::path metadata = "_SaveData/CAPACITY/Options/sce_sys/blocks.bin";
|
||||
fs::resize_file(metadata, 7);
|
||||
CHECK(SaveDataDirNameSearch(&cond, &search) == SAVE_DATA_ERROR_BROKEN);
|
||||
CHECK(fs::file_size(metadata) == 7);
|
||||
CHECK(fs::remove(metadata));
|
||||
CHECK(SaveDataDirNameSearch(&cond, &search) == SAVE_DATA_ERROR_BROKEN);
|
||||
CHECK(!fs::exists(metadata));
|
||||
}
|
||||
|
||||
void RunChild(const fs::path& executable, const char* mode) {
|
||||
#ifdef _WIN32
|
||||
CHECK(_spawnl(_P_WAIT, executable.string().c_str(), executable.string().c_str(), mode,
|
||||
@@ -425,6 +495,7 @@ int main(int argc, char** argv) {
|
||||
TestIsolationAndSync();
|
||||
TestFailedWritePreservesSave();
|
||||
TestClassicSavePaths();
|
||||
TestSaveAllocations();
|
||||
CHECK(SaveDataTerminate() == OK);
|
||||
fs::current_path(previous);
|
||||
fs::remove_all(temp);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -19295,6 +19624,32 @@ TestCase VectorMinMaxF16Ops() {
|
||||
O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase VectorCvtF16U16ByteSource() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
constexpr std::array inputs = {0xff123456u, 0x80123456u, 0x00123456u};
|
||||
std::vector<u32> code;
|
||||
AppendVMovLiteral(&code, 16, 0xa5a55a5au);
|
||||
for (u32 i = 0; i < inputs.size(); i++) {
|
||||
AppendVMovU32(&code, 30, i * 4u);
|
||||
AppendBufferLoadDword(&code, 18, 30);
|
||||
// Convert unsigned byte3 to FP16 word0 while preserving word1 of v16.
|
||||
code.insert(code.end(), {0x7e20a0f9u, 0x00031412u});
|
||||
AppendStoreVgpr(&code, 16, i);
|
||||
}
|
||||
AppendEnd(&code);
|
||||
|
||||
TestCase test;
|
||||
test.name = "VectorCvtF16U16ByteSource";
|
||||
test.code = std::move(code);
|
||||
test.initial.assign(inputs.begin(), inputs.end());
|
||||
test.expected = {0xa5a55bf8u, 0xa5a55800u, 0xa5a50000u};
|
||||
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_CVT_F16_U16,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.required_spirv = {"OpBitFieldUExtract", "OpConvertUToF"};
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase VectorCvtU16F16Sdwa() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -22173,6 +22528,75 @@ TestCase BufferLoadDwordx4ZeroesOnlyOutOfBoundsTail() {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase BufferLoadsGpuSelectedDescriptors() {
|
||||
using O = ShaderOpcode;
|
||||
constexpr uint64_t GuestBase = 0x0000000110000000ull;
|
||||
struct DescriptorCase {
|
||||
u32 stride, records, mode, soffset;
|
||||
bool swizzle, bound;
|
||||
std::array<u32, 6> expected;
|
||||
};
|
||||
const DescriptorCase cases[] = {
|
||||
{24, 2, 0, 0, false, true, {7, 8, 9, 10, 11, 12}},
|
||||
{12, 2, 0, 0, false, true, {4, 5, 6, 0, 0, 0}},
|
||||
{12, 2, 1, 0, false, true, {4, 5, 6, 7, 8, 9}},
|
||||
{12, 1, 2, 0, false, true, {4, 5, 6, 7, 8, 9}},
|
||||
{12, 0, 2, 0, false, true, {}},
|
||||
// OOB3 checks offset + soffset, independently of index * stride.
|
||||
{12, 20, 3, 4, false, true, {5, 6, 7, 8, 0, 0}},
|
||||
{12, 2, 0, 0, false, false, {}},
|
||||
// RDNA2 swizzling uses four-byte elements; soffset is added afterward.
|
||||
{12, 2, 0, 4, true, true, {3, 11, 19, 0, 0, 0}},
|
||||
{12, 8, 3, 4, true, true, {3, 11, 19, 0, 0, 0}},
|
||||
{12, 4, 3, 4, true, true, {}},
|
||||
};
|
||||
TestCase test;
|
||||
test.name = "BufferLoadsGpuSelectedDescriptors";
|
||||
test.initial.resize(2048);
|
||||
for (u32 i = 0; i < std::size(cases); ++i) {
|
||||
const auto &input = cases[i];
|
||||
const u32 data_offset = 4096 + i * 128;
|
||||
// 120-byte table entries with a descriptor at byte 8, as in PPSA04677.
|
||||
const std::array<u32, 4> descriptor{
|
||||
static_cast<u32>(GuestBase + data_offset),
|
||||
(input.stride << 16u) | (input.swizzle ? 1u << 31u : 0u) | 1u,
|
||||
input.records, (input.bound ? 0x5204u : 0x204u) | (input.mode << 28u)};
|
||||
std::copy(descriptor.begin(), descriptor.end(), test.initial.begin() + 130 + i * 30);
|
||||
for (u32 word = 0; word < 32; ++word) {
|
||||
test.initial[data_offset / 4 + word] = i * 100 + word + 1;
|
||||
}
|
||||
// Reverse the table order through a GPU load and readfirstlane, not host constants.
|
||||
const u32 selected = static_cast<u32>(std::size(cases)) - 1 - i;
|
||||
test.initial[64 + i] = selected;
|
||||
AppendVMovU32(&test.code, 30, (64 + i) * 4);
|
||||
AppendBufferLoadDword(&test.code, 0, 30);
|
||||
test.code.push_back(EncodeVop1(0x02, 20, Vgpr(0)));
|
||||
test.code.push_back(EncodeSop2(0x26, 20, 20, 255)); // s_mul_i32 s20, s20, 120
|
||||
test.code.push_back(120);
|
||||
test.code.push_back(EncodeSmem0(0x0a, 8, 0));
|
||||
test.code.push_back(EncodeSmem1(520, 20));
|
||||
AppendSMovLiteral(&test.code, 22, cases[selected].soffset);
|
||||
AppendVMovU32(&test.code, 21, 1);
|
||||
test.code.push_back(EncodeMubuf0(0x0e, 0, true, false));
|
||||
test.code.push_back(EncodeMubuf1(0, 2, 21, 22));
|
||||
test.code.push_back(EncodeMubuf0(0x0d, 16, true, false));
|
||||
test.code.push_back(EncodeMubuf1(4, 2, 21, 22));
|
||||
for (u32 component = 0; component < 6; ++component) {
|
||||
AppendStoreVgpr(&test.code, component, i * 6 + component);
|
||||
const u32 expected = cases[selected].expected[component];
|
||||
test.expected.push_back(expected == 0 ? 0 : selected * 100 + expected);
|
||||
}
|
||||
}
|
||||
AppendEnd(&test.code);
|
||||
test.bda_mappings = {{GuestBase, 0}};
|
||||
test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD,
|
||||
O::V_READFIRSTLANE_B32, O::S_MUL_I32, O::S_BUFFER_LOAD_DWORDX4,
|
||||
O::BUFFER_LOAD_DWORDX4, O::BUFFER_LOAD_DWORDX2,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.required_spirv = {"OpConvertUToPtr", "PhysicalStorageBuffer"};
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase BufferStoreDwordx4DropsOnlyOutOfBoundsTail() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -27061,6 +27485,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 +27514,7 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(VectorFfbhI32NativeAndVop3OnGpu);
|
||||
AddCase(Vop1SdwaFfblCapturedHighWordSource);
|
||||
AddCase(Vop1SdwaNotCapturedByte0Source);
|
||||
AddCase(Vop1SdwaNotPreservesHighWordDestination);
|
||||
AddCase(Vop1SdwaMovByteDestinations);
|
||||
AddCase(Vop2SdwaSubNcExactByte2Destination);
|
||||
AddCase(Vop2SdwaAddNcCapturedHighWordDestination);
|
||||
@@ -27126,6 +27553,7 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(CvtPkrtzF16F32SdwaAndOutputModifiers);
|
||||
AddCase(PackedMinMaxF16NanAndSignedZeroEdges);
|
||||
AddCase(VectorMinMaxF16Ops);
|
||||
AddCase(VectorCvtF16U16ByteSource);
|
||||
AddCase(VectorCvtU16F16Sdwa);
|
||||
AddCase(NativeAndSdwa16BitDestinationWrites);
|
||||
AddCase(VectorMinMaxMed3F16Ops);
|
||||
@@ -27212,6 +27640,7 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(BufferLoadDwordx3SnapshotsOverlappingAddress);
|
||||
AddCase(BufferLoadDwordx4SnapshotsOverlappingAddress);
|
||||
AddCase(BufferLoadDwordx4ZeroesOnlyOutOfBoundsTail);
|
||||
AddCase(BufferLoadsGpuSelectedDescriptors);
|
||||
AddCase(BufferStoreDwordx4DropsOnlyOutOfBoundsTail);
|
||||
AddCase(BufferLoadFormatXyzwRejectsPartialRecord);
|
||||
AddCase(BufferStoreFormatXyzwDropsPartialRecord);
|
||||
@@ -28770,6 +29199,17 @@ void CheckDepthTextureEncoding() {
|
||||
IsSupportedDepthTextureEncoding(compressed_descriptor),
|
||||
"valid compressed depth descriptor required a prior depth target");
|
||||
|
||||
const ShaderTextureResource disabled_metadata{{
|
||||
0x20018100u, 0xc1600000u, 0x021bc1dfu, 0x91800204u,
|
||||
0x00000000u, 0x00700000u, 0x00000000u, 0x0020037fu,
|
||||
}};
|
||||
auto retained_metadata_address = compressed_descriptor;
|
||||
retained_metadata_address.fields[6] &= 0xff000000u;
|
||||
Require("DepthTextureEncoding", "disabled metadata with retained address",
|
||||
IsSupportedDepthTextureEncoding(disabled_metadata) &&
|
||||
IsSupportedDepthTextureEncoding(retained_metadata_address),
|
||||
"uncompressed depth rejected inactive metadata address bits");
|
||||
|
||||
const ShaderTextureResource first_use_depth{{
|
||||
0x0225fc00u, 0x01600000u, 0x00000000u, 0x91800924u,
|
||||
0x00000000u, 0x00700000u, 0x80280000u, 0x000225fdu,
|
||||
@@ -31938,6 +32378,16 @@ int main(int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (argc == 2 && std::strcmp(argv[1], "--indirect-buffer-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, BufferLoadsGpuSelectedDescriptors());
|
||||
RunCase(&vulkan, BufferLoadDwordx4SnapshotsOverlappingAddress());
|
||||
RunCase(&vulkan, BufferLoadDwordx4ZeroesOnlyOutOfBoundsTail());
|
||||
RunCase(&vulkan, BufferLoadDwordIdxenUsesDescriptorStride());
|
||||
RunCase(&vulkan, BufferStoreFormatXAddTidUsesLaneIndex());
|
||||
RunCase(&vulkan, FlatVirtualAddressRebasesGuestAllocation());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--wave64-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, Wave32VccMasksPreserveOtherHalf());
|
||||
@@ -32204,6 +32654,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 +32845,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);
|
||||
|
||||
+133
-34
@@ -1679,45 +1679,14 @@ void TestNewShaderRecompilerSMovB32() {
|
||||
CheckSpirvBinaryValidates(result.spirv);
|
||||
}
|
||||
|
||||
void TestNewShaderRecompilerSoppMarkers() {
|
||||
void TestShaderStageBarriers() {
|
||||
const uint32_t shader[] = {
|
||||
EncodeSopp(0x00, 3), // s_nop 3
|
||||
EncodeSopp(0x0c, 0), // s_waitcnt 0
|
||||
EncodeSopp(0x10, 0x0f), // s_sendmsg 15
|
||||
EncodeSopp(0x16, 0x2a), // s_ttracedata 42
|
||||
EncodeSopp(0x20, 1), // s_inst_prefetch 1
|
||||
EncodeSopp(0x0a, 0), // s_barrier
|
||||
EncodeSopp(0x01, 0), // s_endpgm
|
||||
};
|
||||
|
||||
auto options = MakeCompileOptions(ShaderType::Compute);
|
||||
options.dump_ir = true;
|
||||
|
||||
auto result = RecompileForTest(shader, options);
|
||||
Check((result.decoded_dump.find("s_nop 0x00000003") != std::string::npos),
|
||||
"new decoder did not decode SOPP s_nop");
|
||||
Check((result.decoded_dump.find("s_waitcnt 0x00000000") != std::string::npos),
|
||||
"new decoder did not decode SOPP s_waitcnt");
|
||||
Check((result.decoded_dump.find("s_sendmsg 0x0000000f") != std::string::npos),
|
||||
"new decoder did not decode SOPP s_sendmsg");
|
||||
Check((result.decoded_dump.find("s_ttracedata 0x0000002a") != std::string::npos),
|
||||
"new decoder did not decode SOPP s_ttracedata");
|
||||
Check((result.decoded_dump.find("s_inst_prefetch 0x00000001") != std::string::npos),
|
||||
"new decoder did not decode SOPP s_inst_prefetch");
|
||||
Check((result.decoded_dump.find("s_barrier") != std::string::npos),
|
||||
"new decoder did not decode SOPP s_barrier");
|
||||
Check((result.ir_dump.find("ControlNop null, 0x00000003") != std::string::npos),
|
||||
"SOPP s_nop did not lower to an IR marker");
|
||||
Check((result.ir_dump.find("Waitcnt null, 0x00000000") != std::string::npos),
|
||||
"SOPP s_waitcnt did not lower to an IR marker");
|
||||
Check((result.ir_dump.find("Sendmsg null, 0x0000000f") != std::string::npos),
|
||||
"SOPP s_sendmsg did not lower to an IR marker");
|
||||
Check((result.ir_dump.find("TtraceData null, 0x0000002a") != std::string::npos),
|
||||
"SOPP s_ttracedata did not lower to an IR marker");
|
||||
Check((result.ir_dump.find("InstPrefetch null, 0x00000001") != std::string::npos),
|
||||
"SOPP s_inst_prefetch did not lower to an IR marker");
|
||||
Check((result.ir_dump.find("Barrier null") != std::string::npos),
|
||||
"SOPP s_barrier did not lower to an IR marker");
|
||||
const auto result =
|
||||
RecompileForTest(shader, MakeCompileOptions(ShaderType::Compute));
|
||||
Check(SpirvContainsOpcode(result.spirv, 224),
|
||||
"SPIR-V binary does not contain OpControlBarrier");
|
||||
Check(
|
||||
@@ -1725,6 +1694,70 @@ void TestNewShaderRecompilerSoppMarkers() {
|
||||
result.spirv.end(),
|
||||
"SPIR-V barrier does not use workgroup acquire-release memory semantics");
|
||||
CheckSpirvBinaryValidates(result.spirv);
|
||||
|
||||
const auto vertex_result =
|
||||
RecompileForTest(shader, MakeCompileOptions(ShaderType::Vertex));
|
||||
CheckSpirvBinaryValidates(vertex_result.spirv);
|
||||
Check(!SpirvContainsOpcode(vertex_result.spirv, 224),
|
||||
"independent vertex invocations retained a workgroup barrier");
|
||||
}
|
||||
|
||||
void TestNggVertexEntryState() {
|
||||
using namespace ShaderRecompiler;
|
||||
// PPSA03309 shader 5a39eb2021a5d2c1: retain its NGG prologue and replace
|
||||
// the resource-dependent vertex body with a position export.
|
||||
const uint32_t shader[] = {
|
||||
0xbfa00003u, 0x93ebff03u, 0x00040018u, 0xbefe03c1u,
|
||||
0x9380ff02u, 0x00090016u, 0x9381ff02u, 0x0009000cu,
|
||||
0xbf8a0000u, 0xbf076b80u, 0xbf850003u, 0x8f6a8c00u,
|
||||
0x887c6a01u, 0xbf900009u, 0xd7650001u, 0x000100c1u,
|
||||
0xd7460001u, 0x04050a6bu, 0x7da80200u, 0xbf880002u,
|
||||
0xf8000941u, 0x00000000u, 0xbf8cff0fu, 0xbefe03c1u,
|
||||
0x7da80201u, EncodeSopp(0x08, 2),
|
||||
EncodeExp0(0x0c, 0xf), EncodeExp1(5, 5, 5, 5), EncodeSopp(0x01),
|
||||
};
|
||||
HW::VertexShaderInfo regs{};
|
||||
regs.es_regs.data_addr = reinterpret_cast<uint64_t>(shader);
|
||||
ShaderUserData user_data{};
|
||||
ShaderMappedData mapped{};
|
||||
mapped.user_data = &user_data;
|
||||
mapped.code_size_bytes = sizeof(shader);
|
||||
ShaderMapUserData(regs.es_regs.data_addr, mapped);
|
||||
std::vector<uint32_t> previous_key;
|
||||
for (const uint32_t wave_size : {32u, 64u}) {
|
||||
HW::Context context;
|
||||
context.SetShaderStages(wave_size == 32u ? 0x00400000u : 0u);
|
||||
ShaderVertexInputInfo input{};
|
||||
const auto params = PrepareProgram(regs, context, HW::UserConfig{}, input);
|
||||
Check(input.wave_size == wave_size,
|
||||
"vertex preparation lost the native NGG wave size");
|
||||
const auto key = MakeStageStaticKey(input);
|
||||
Check(previous_key != key, "NGG wave sizes shared a shader cache key");
|
||||
previous_key = key;
|
||||
auto options = MakeCompileOptions(ShaderType::Vertex);
|
||||
options.user_data_base = 8;
|
||||
options.user_data = params.user_data;
|
||||
options.input_info.vertex = &input;
|
||||
options.wave_size = input.wave_size;
|
||||
const auto result = RecompileForTest(params.code, options);
|
||||
bool live_vertex_range = false;
|
||||
for (const auto *block : result.program.blocks) {
|
||||
for (const auto &inst : *block) {
|
||||
if (inst.GetOpcode() == IR::ValueOpcode::UGreaterThan32) {
|
||||
const auto count = inst.Arg(0).Resolve();
|
||||
live_vertex_range |= count.IsImmediate() && count.U32() == wave_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
Check(live_vertex_range && std::ranges::any_of(result.program.info.outputs,
|
||||
[](const auto &output) {
|
||||
return output.kind == IR::StageOutputKind::Position;
|
||||
}),
|
||||
"NGG launch counts suppressed the vertex export");
|
||||
Check(result.program.wave_size == wave_size,
|
||||
"NGG wave size was lost during translation");
|
||||
CheckSpirvBinaryValidates(result.spirv);
|
||||
}
|
||||
}
|
||||
|
||||
void TestNewShaderRecompilerSopkWaitcntMarkers() {
|
||||
@@ -3433,6 +3466,69 @@ void TestNewShaderRecompilerCapturedVop1SdwaByteConvert() {
|
||||
Check(SpirvContainsOpcode(result.spirv, 112),
|
||||
"captured SDWA byte conversion did not emit OpConvertUToF");
|
||||
CheckSpirvBinaryValidates(result.spirv);
|
||||
|
||||
const uint32_t byte_sext[] = {0x7e20a0f9u, 0x000b1412u};
|
||||
ShaderRecompiler::Decoder::Instruction decoded;
|
||||
ShaderRecompiler::Decoder::DecodeInstruction(byte_sext, 0, decoded);
|
||||
Check(decoded.word_count == 2u &&
|
||||
decoded.opcode == ShaderRecompiler::Decoder::Opcode::UNSUPPORTED,
|
||||
"V_CVT_F16_U16 accepted unimplemented SDWA byte sign extension");
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -13387,6 +13483,8 @@ int main() {
|
||||
TestNewShaderRecompilerSpirvSizeBaselines();
|
||||
TestDemandDrivenSpirvDeclarations();
|
||||
TestNewShaderRecompilerSMovB32();
|
||||
TestShaderStageBarriers();
|
||||
TestNggVertexEntryState();
|
||||
TestNewShaderRecompilerClipDisabledPosition();
|
||||
TestNewShaderRecompilerAuxPositionExports();
|
||||
TestNewShaderRecompilerNativeWideScalarMemoryIr();
|
||||
@@ -13397,6 +13495,7 @@ int main() {
|
||||
TestNewShaderRecompilerDsReadWrite2Translation();
|
||||
TestNewShaderRecompilerDsWideAndAtomicTranslation();
|
||||
TestNewShaderRecompilerCapturedVop1SdwaByteConvert();
|
||||
TestNewShaderRecompilerVop1SdwaNotDestination();
|
||||
TestNewShaderRecompilerScalarMemoryBindingDomains();
|
||||
// Opcode semantics and optimized SPIR-V are exercised by
|
||||
// ShaderRecompilerComputeTests; keep the distinct decoder contract checks
|
||||
|
||||
Reference in New Issue
Block a user