Compare commits

..

4 Commits

Author SHA1 Message Date
nmzik 9bc9c84312 docs: refine weekly updates wording 2026-09-27 18:06:00 +04:00
nmzik d3eaafab61 docs: use Discord server invite link 2026-09-27 18:03:49 +04:00
nmzik e568555e8b docs: add Discord development channel 2026-09-27 18:02:06 +04:00
nmzik 9b599ca92c docs: link weekly updates from README 2026-09-27 17:44:08 +04:00
368 changed files with 18725 additions and 73603 deletions
+15 -21
View File
@@ -113,15 +113,14 @@ jobs:
shell: cmd
run: |
cmake --build _Build/windows --target launcher ^
audio_out2_port_tests pad_haptics_tests controller_settings_tests host_input_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
shader_recompiler_compute_tests ^
audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
resource_tracking_tests scalar_provenance_tests ^
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests --parallel
resource_materialization_tests shader_vertex_metadata_tests --parallel
- name: Test
shell: cmd
run: |
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|controller_settings|host_input|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$"
- name: Install
shell: cmd
@@ -202,17 +201,16 @@ jobs:
shell: bash
run: |
cmake --build _Build/macos \
--target launcher audio_out2_port_tests pad_haptics_tests controller_settings_tests host_input_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
memory_tracker_tests page_manager_tests shader_recompiler_compute_tests \
--target launcher audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
resource_tracking_tests scalar_provenance_tests \
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
resource_materialization_tests shader_vertex_metadata_tests \
--parallel
- name: Test
shell: bash
run: |
ctest --test-dir _Build/macos --output-on-failure \
-R '^(audio_out2_port|pad_haptics|controller_settings|host_input|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
-R '^(audio_out2_port|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$'
- name: Install
shell: bash
@@ -257,7 +255,6 @@ jobs:
"$install_dir/libMoltenVK.dylib" \
"$app_dir/Contents/MacOS/KytyPS5" \
"$app_dir/Contents/MacOS/kyty_emulator" \
"$app_dir/Contents/MacOS/kyty_updater" \
"$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do
file "$binary"
lipo "$binary" -verify_arch x86_64
@@ -267,7 +264,6 @@ jobs:
"$install_dir/libMoltenVK.dylib" \
"$app_dir/Contents/MacOS/KytyPS5" \
"$app_dir/Contents/MacOS/kyty_emulator" \
"$app_dir/Contents/MacOS/kyty_updater" \
"$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do
codesign --verify --strict "$binary"
done
@@ -388,16 +384,16 @@ jobs:
run: |
cmake --build _Build/linux \
--target launcher page_manager_tests memory_tracker_tests \
audio_out2_port_tests pad_haptics_tests controller_settings_tests host_input_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
resource_tracking_tests scalar_provenance_tests \
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
resource_materialization_tests shader_vertex_metadata_tests \
--parallel
- name: Test
shell: bash
run: |
ctest --test-dir _Build/linux --output-on-failure \
-R '^(audio_out2_port|pad_haptics|controller_settings|host_input|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
-R '^(audio_out2_port|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$'
- name: Install
shell: bash
@@ -407,12 +403,11 @@ jobs:
- name: Verify artifacts
shell: bash
run: |
file _Build/linux/install/kyty_launcher
file _Build/linux/install/launcher
file _Build/linux/install/kyty_emulator
test -x _Build/linux/install/kyty_updater
file _Build/linux/install/kyty_emulator | grep -q "ELF 64-bit LSB .*x86-64"
ldd _Build/linux/install/kyty_emulator > /dev/null
readelf -d _Build/linux/install/kyty_launcher | grep -q 'RPATH.*\$ORIGIN/lib'
readelf -d _Build/linux/install/launcher | grep -q 'RPATH.*\$ORIGIN/lib'
while IFS= read -r -d '' binary; do
while read -r dependency; do
test -e "_Build/linux/install/lib/$dependency"
@@ -421,8 +416,8 @@ jobs:
sed -n 's/.*Shared library: \[\(libQt6[^]]*\|libicu[^]]*\)\].*/\1/p'
)
done < <(
find _Build/linux/install/kyty_launcher _Build/linux/install/plugins \
-type f \( -name kyty_launcher -o -name '*.so' \) -print0
find _Build/linux/install/launcher _Build/linux/install/plugins \
-type f \( -name launcher -o -name '*.so' \) -print0
)
_Build/linux/install/kyty_emulator --help > /dev/null
@@ -497,9 +492,8 @@ jobs:
linux_dir="artifacts/KytyPS5-Linux-x86_64"
test -d "$linux_dir"
chmod a+x \
"$linux_dir/kyty_launcher" \
"$linux_dir/kyty_emulator" \
"$linux_dir/kyty_updater"
"$linux_dir/launcher" \
"$linux_dir/kyty_emulator"
tar -C "$linux_dir" -czf \
"$GITHUB_WORKSPACE/$RELEASE_NAME-Linux-x86_64.tar.gz" .
+2 -2
View File
@@ -5,7 +5,7 @@
"name": "Debug launcher UI",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}\\_Build\\vscode-clang\\launcher\\kyty_launcher.exe",
"program": "${workspaceFolder}\\_Build\\vscode-clang\\launcher\\launcher.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}\\_Build\\vscode-clang\\launcher",
@@ -13,7 +13,7 @@
"externalConsole": false,
"linux": {
"type": "cppdbg",
"program": "${workspaceFolder}/_Build/vscode-clang/launcher/kyty_launcher",
"program": "${workspaceFolder}/_Build/vscode-clang/launcher/launcher",
"cwd": "${workspaceFolder}/_Build/vscode-clang/launcher",
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb"
+2 -72
View File
@@ -2,31 +2,10 @@ set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
add_subdirectory(cpuinfo)
# The Windows guest red-zone patcher needs a decoder/encoder and a small
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
include(FetchContent)
# The Bluetooth DualSense transports speaker audio as fixed-size Opus packets.
set(kyty_build_testing_saved "${BUILD_TESTING}")
set(BUILD_TESTING OFF)
set(OPUS_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE)
set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF CACHE BOOL "" FORCE)
set(OPUS_INSTALL_CMAKE_CONFIG_MODULE OFF CACHE BOOL "" FORCE)
if(MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
# Opus assumes MSVC accepts SSE4.1 intrinsics without per-file flags, but
# clang-cl rejects them in its baseline x64 files. Its AVX2 dispatch also
# depends on the SSE4.1 declarations, so retain the x64 SSE2 baseline only.
set(OPUS_X86_MAY_HAVE_SSE4_1 OFF CACHE BOOL "" FORCE)
set(OPUS_X86_MAY_HAVE_AVX2 OFF CACHE BOOL "" FORCE)
endif()
FetchContent_Declare(opus
GIT_REPOSITORY https://github.com/xiph/opus.git
GIT_TAG ddbe48383984d56acd9e1ab6a090c54ca6b735a6
GIT_SHALLOW FALSE)
FetchContent_MakeAvailable(opus)
set(BUILD_TESTING "${kyty_build_testing_saved}")
install(FILES "${opus_SOURCE_DIR}/COPYING" DESTINATION "licenses/opus")
# The guest instruction patcher needs a decoder/encoder and a small
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
set(XBYAK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
xbyak
@@ -48,56 +27,8 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(xbyak zydis)
target_compile_definitions(xbyak INTERFACE XBYAK_NO_EXCEPTION)
# ZArchive provides random-access reads for compressed game dumps. Build only
# the reader/writer library and its zstd dependency; the command-line tool is
# not part of KytyPS5's runtime distribution.
set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_STATIC ON CACHE BOOL "" FORCE)
set(ZSTD_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(ZSTD_LEGACY_SUPPORT OFF CACHE BOOL "" FORCE)
set(ZSTD_MULTITHREAD_SUPPORT OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
zstd
GIT_REPOSITORY https://github.com/facebook/zstd.git
GIT_TAG f8745da6ff1ad1e7bab384bd1f9d742439278e99
GIT_SHALLOW FALSE
SOURCE_SUBDIR build/cmake
)
FetchContent_Declare(
zarchive_source
GIT_REPOSITORY https://github.com/Exzap/ZArchive.git
GIT_TAG d2c717730092c7bf8cbb033b12fd4001b7c4d932
GIT_SHALLOW FALSE
PATCH_COMMAND "${GIT_EXECUTABLE}" apply --ignore-whitespace
"${CMAKE_CURRENT_SOURCE_DIR}/patches/zarchive-reader.patch"
SOURCE_SUBDIR kyty-library-only
)
FetchContent_MakeAvailable(zstd zarchive_source)
add_library(zarchive STATIC
"${zarchive_source_SOURCE_DIR}/src/zarchivereader.cpp"
"${zarchive_source_SOURCE_DIR}/src/zarchivewriter.cpp"
"${zarchive_source_SOURCE_DIR}/src/sha_256.c"
)
add_library(ZArchive::zarchive ALIAS zarchive)
target_include_directories(zarchive SYSTEM PUBLIC "${zarchive_source_SOURCE_DIR}/include")
target_include_directories(zarchive PRIVATE "${zstd_SOURCE_DIR}/lib")
target_link_libraries(zarchive PRIVATE libzstd_static)
target_compile_features(zarchive PRIVATE cxx_std_20)
if(MSVC)
target_compile_options(zarchive PRIVATE /W0)
else()
target_compile_options(zarchive PRIVATE -w)
endif()
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
if(APPLE)
# SDL's built-in iconv fallback does not support CP932.
set(SDL_SYSTEM_ICONV ON CACHE BOOL "Use system iconv for CP932 conversion" FORCE)
endif()
add_subdirectory(SDL3)
add_library(Vulkan-Headers INTERFACE)
@@ -189,7 +120,6 @@ endif()
set(TRACY_STATIC ON CACHE BOOL "" FORCE)
set(TRACY_ENABLE ON CACHE BOOL "" FORCE)
set(TRACY_NO_CRASH_HANDLER ON CACHE BOOL "" FORCE)
set(TRACY_ON_DEMAND ON CACHE BOOL "" FORCE)
set(TRACY_DELAYED_INIT ON CACHE BOOL "" FORCE)
set(TRACY_MANUAL_LIFETIME ON CACHE BOOL "" FORCE)
+1 -1
View File
@@ -14,7 +14,7 @@
#include <cpuinfo/internal-api.h>
#include <cpuinfo/log.h>
/* Polyfill recent CPUFAMILY_ARM_* values for older platform headers */
/* Polyfill recent CPUFAMILY_ARM_* values for older SDKs */
#ifndef CPUFAMILY_ARM_MONSOON_MISTRAL
#define CPUFAMILY_ARM_MONSOON_MISTRAL 0xE81E7EF6
#endif
-100
View File
@@ -1,100 +0,0 @@
--- a/include/zarchive/zarchivecommon.h
+++ b/include/zarchive/zarchivecommon.h
@@ -256,7 +256,7 @@
bool IsWithinValidRange(uint64_t fileSize) const
{
- return (offset + size) <= fileSize;
+ return offset <= fileSize && size <= fileSize - offset;
}
};
--- a/src/zarchivereader.cpp
+++ b/src/zarchivereader.cpp
@@ -14,6 +14,7 @@
static bool _ifstream_readBytes(std::ifstream& file, uint64_t offset, void* buffer, uint32_t size)
{
+ file.clear();
file.seekg(offset, std::ios_base::beg);
file.read((char*)buffer, size);
return file.gcount() == size;
@@ -80,9 +81,27 @@
// verify file tree
if (fileTree[0].IsFile())
return nullptr; // first entry must be root directory
- auto rootName = GetName(nameTable, fileTree[0].GetNameOffset());
- if (!rootName.empty())
+ if (fileTree[0].GetNameOffset() != 0x7FFFFFFF)
return nullptr; // root node must not have a name
+ const uint64_t dataCapacity = (uint64_t)offsetRecords.size() * _ZARCHIVE::ENTRIES_PER_OFFSETRECORD * _ZARCHIVE::COMPRESSED_BLOCK_SIZE;
+ for (size_t i = 0; i < fileTree.size(); ++i)
+ {
+ auto& entry = fileTree[i];
+ if (i != 0)
+ {
+ auto name = GetName(nameTable, entry.GetNameOffset());
+ if (name.empty() || name == "." || name == ".." || name.find_first_of("/\\") != std::string_view::npos || name.find('\0') != std::string_view::npos)
+ return nullptr;
+ }
+ if (entry.IsFile())
+ {
+ if (entry.GetFileOffset() > dataCapacity || entry.GetFileSize() > dataCapacity - entry.GetFileOffset())
+ return nullptr;
+ }
+ else if (entry.directoryRecord.nodeStartIndex > fileTree.size() ||
+ entry.directoryRecord.count > fileTree.size() - entry.directoryRecord.nodeStartIndex)
+ return nullptr;
+ }
// read meta data
// todo
@@ -238,7 +257,7 @@
uint32_t stepSize = std::min<uint32_t>(remainingBytes, _ZARCHIVE::COMPRESSED_BLOCK_SIZE - blockOffset);
CacheBlock* block = GetCachedBlock(blockIdx);
if (!block)
- return 0;
+ return bytesToRead - remainingBytes;
std::memcpy(bufferU8, block->data + blockOffset, stepSize);
rawReadOffset += stepSize;
remainingBytes -= stepSize;
@@ -325,14 +344,18 @@
// determine offset and size of compressed block
auto& record = m_offsetRecords[recordIndex];
uint64_t offset = record.baseOffset;
+ if (offset > m_compressedDataSize)
+ return false;
for (uint32_t i = 0; i < recordSubIndex; i++)
{
- offset += (uint64_t)record.size[i];
- offset++;
+ const uint64_t size = (uint64_t)record.size[i] + 1;
+ if (size > m_compressedDataSize - offset)
+ return false;
+ offset += size;
}
uint32_t compressedSize = (uint32_t)record.size[recordSubIndex] + 1;
// load file data
- if ((offset + compressedSize) > m_compressedDataSize)
+ if (compressedSize > m_compressedDataSize - offset)
return false;
offset += m_compressedDataOffset;
if (compressedSize == _ZARCHIVE::COMPRESSED_BLOCK_SIZE)
@@ -350,7 +373,7 @@
// returns empty view on failure
std::string_view ZArchiveReader::GetName(const std::vector<uint8_t>& nameTable, uint32_t nameOffset)
{
- if (nameOffset == 0x7FFFFFFF || nameOffset > nameTable.size())
+ if (nameOffset == 0x7FFFFFFF || nameOffset >= nameTable.size())
return "";
// parse header
uint16_t nameLength = nameTable[nameOffset] & 0x7F;
@@ -359,7 +382,7 @@
// extended 2-byte length
if (nameOffset + 1 >= nameTable.size())
return "";
- nameLength |= ((uint16_t)nameTable[nameOffset] << 7);
+ nameLength |= ((uint16_t)nameTable[nameOffset + 1] << 7);
nameOffset += 2;
}
else
-1724
View File
File diff suppressed because it is too large Load Diff
+30 -135
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.19...4.3)
cmake_minimum_required(VERSION 3.16...4.3)
if(CMAKE_VERSION VERSION_GREATER_EQUAL 4.0)
set(CMAKE_POLICY_VERSION_MINIMUM 3.10 CACHE STRING "Minimum policy version for bundled dependencies")
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "Minimum policy version for bundled dependencies")
endif()
get_property(KYTY_GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
@@ -40,11 +40,6 @@ if (NOT (WIN32 OR LINUX OR APPLE))
message(FATAL_ERROR "only Windows, Linux, and macOS builds are supported")
endif()
if(WIN32)
# Prevent Windows headers from defining min/max macros in all targets.
add_compile_definitions(NOMINMAX)
endif()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
@@ -249,7 +244,7 @@ file(GLOB kyty_emulator_src CONFIGURE_DEPENDS
"${KYTY_SOURCE_DIR}/loader/*.h"
)
find_program(KYTY_GLSLANG_VALIDATOR NAMES glslangValidator glslang REQUIRED)
find_program(KYTY_GLSLANG_VALIDATOR glslangValidator REQUIRED)
set(gpu_tiler_shader_dir "${KYTY_SOURCE_DIR}/graphics/host_gpu/shaders")
set(gpu_tiler_generated_dir "${PROJECT_BINARY_DIR}/gpu_tiler_shaders")
set(gpu_tiler_shader_names
@@ -264,7 +259,7 @@ set(gpu_tiler_shader_names
depth
promote_d16
demote_d16
color_transform
swap_bgra16
)
file(GLOB gpu_tiler_shader_includes CONFIGURE_DEPENDS "${gpu_tiler_shader_dir}/gpu_tiler_*.inc")
foreach(shader_name IN LISTS gpu_tiler_shader_names)
@@ -303,27 +298,28 @@ list(APPEND kyty_emulator_src "${fault_buffer_shader_header}")
list(APPEND kyty_emulator_src ${gpu_tiler_shader_headers})
set(gpu_blit_generated_dir "${PROJECT_BINARY_DIR}/gpu_blit_shaders")
function(add_gpu_blit_shader shader_name shader_file)
set(shader_source "${gpu_tiler_shader_dir}/${shader_file}")
set(gpu_blit_shader_sources
"${gpu_tiler_shader_dir}/gpu_blit_fs_triangle.vert"
"${gpu_tiler_shader_dir}/gpu_blit_color_to_ms_depth.frag"
"${gpu_tiler_shader_dir}/gpu_video_out_overlay.frag"
)
foreach(shader_source IN LISTS gpu_blit_shader_sources)
get_filename_component(shader_name "${shader_source}" NAME_WE)
set(shader_spv "${gpu_blit_generated_dir}/${shader_name}.spv")
set(shader_header "${gpu_blit_generated_dir}/${shader_name}_spv.h")
string(TOUPPER "${shader_name}_SPV" shader_symbol)
add_custom_command(
OUTPUT "${shader_header}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${gpu_blit_generated_dir}"
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os ${ARGN}
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os
"-I${gpu_tiler_shader_dir}" -o "${shader_spv}" "${shader_source}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${shader_spv} -DOUTPUT=${shader_header}
-DSYMBOL=${shader_symbol} -P "${KYTY_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${shader_source}"
VERBATIM
)
set(gpu_blit_shader_headers ${gpu_blit_shader_headers} "${shader_header}" PARENT_SCOPE)
endfunction()
add_gpu_blit_shader(gpu_blit_fs_triangle gpu_blit_fs_triangle.vert)
add_gpu_blit_shader(gpu_blit_color_to_ms_depth gpu_blit_color_to_ms_depth.frag)
add_gpu_blit_shader(gpu_video_out_overlay gpu_video_out_overlay.frag)
add_gpu_blit_shader(gpu_imgui_hdr gpu_video_out_overlay.frag -DIMGUI_HDR=1)
list(APPEND gpu_blit_shader_headers "${shader_header}")
endforeach()
list(APPEND kyty_emulator_src ${gpu_blit_shader_headers})
list(APPEND kyty_emulator_src
@@ -336,7 +332,7 @@ if (KYTY_CLANG_CL)
set_source_files_properties(${kyty_emulator_src} PROPERTIES COMPILE_FLAGS "-Wno-pragma-pack -Wno-deprecated-declarations -D_TIMESPEC_DEFINED")
endif()
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus)
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak)
# Linux system libraries.
if(LINUX)
@@ -397,13 +393,11 @@ add_kyty_full_emulator_test(shader_cfg_tests "${KYTY_TESTS_DIR}/shaderCfgTests.c
add_executable(scalar_provenance_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/ScalarProvenanceTests.cpp"
"${KYTY_SOURCE_DIR}/graphics/guest_gpu/gpu_format.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/hostMemory.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ReadLaneElimination.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/SrtWalker.cpp"
)
target_link_libraries(scalar_provenance_tests fmt::fmt xbyak::xbyak)
target_link_libraries(scalar_provenance_tests fmt::fmt)
target_include_directories(scalar_provenance_tests PRIVATE ${inc_headers})
add_executable(page_manager_tests EXCLUDE_FROM_ALL
@@ -413,33 +407,6 @@ add_executable(page_manager_tests EXCLUDE_FROM_ALL
target_link_libraries(page_manager_tests common)
target_include_directories(page_manager_tests PRIVATE ${inc_headers})
add_executable(archive_file_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/ArchiveFileTests.cpp"
)
target_link_libraries(archive_file_tests common ZArchive::zarchive)
target_include_directories(archive_file_tests PRIVATE ${inc_headers})
add_executable(trophy_system_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/TrophySystemTests.cpp"
)
target_link_libraries(trophy_system_tests common)
target_include_directories(trophy_system_tests PRIVATE ${inc_headers})
add_executable(avplayer_file_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/AvPlayerFileTests.cpp"
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
)
target_link_libraries(avplayer_file_tests common FFmpeg::ffmpeg ZArchive::zarchive)
target_include_directories(avplayer_file_tests PRIVATE ${inc_headers})
add_executable(vdecsw_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/VdecswTests.cpp"
"${KYTY_SOURCE_DIR}/libs/videoDec2Decoder.cpp"
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
)
target_link_libraries(vdecsw_tests common FFmpeg::ffmpeg)
target_include_directories(vdecsw_tests PRIVATE ${inc_headers})
add_executable(bit_array_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/BitArrayTests.cpp"
)
@@ -479,7 +446,7 @@ add_executable(resource_materialization_tests EXCLUDE_FROM_ALL
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/SrtWalker.cpp"
)
target_link_libraries(resource_materialization_tests fmt::fmt xbyak::xbyak)
target_link_libraries(resource_materialization_tests fmt::fmt)
target_include_directories(resource_materialization_tests PRIVATE ${inc_headers})
add_executable(resource_tracking_tests EXCLUDE_FROM_ALL
@@ -491,8 +458,9 @@ add_executable(resource_tracking_tests EXCLUDE_FROM_ALL
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/BindingLayout.cpp"
)
target_link_libraries(resource_tracking_tests fmt::fmt xbyak::xbyak)
target_link_libraries(resource_tracking_tests fmt::fmt)
target_include_directories(resource_tracking_tests PRIVATE ${inc_headers})
add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
@@ -503,10 +471,6 @@ add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
target_link_libraries(audio_out2_port_tests common fmt::fmt)
target_include_directories(audio_out2_port_tests PRIVATE ${inc_headers})
add_executable(audio_out_timing_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/AudioOutTimingTests.cpp")
target_link_libraries(audio_out_timing_tests common fmt::fmt SDL3::SDL3-static)
target_include_directories(audio_out_timing_tests PRIVATE ${inc_headers})
add_executable(ngs2_sampler_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/Ngs2SamplerTests.cpp"
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
@@ -517,40 +481,15 @@ target_include_directories(ngs2_sampler_tests PRIVATE ${inc_headers})
add_executable(pad_haptics_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/PadHapticsTests.cpp"
)
target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static opus)
target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static)
target_include_directories(pad_haptics_tests PRIVATE ${inc_headers})
add_executable(controller_settings_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/ControllerSettingsTests.cpp"
)
target_link_libraries(controller_settings_tests common fmt::fmt SDL3::SDL3-static)
target_include_directories(controller_settings_tests PRIVATE ${inc_headers})
add_executable(host_input_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/HostInputTests.cpp")
target_link_libraries(host_input_tests common fmt::fmt SDL3::SDL3-static)
target_include_directories(host_input_tests PRIVATE ${inc_headers})
add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/SaveDataMemoryTests.cpp"
)
target_link_libraries(save_data_memory_tests common fmt::fmt)
target_include_directories(save_data_memory_tests PRIVATE ${inc_headers})
add_executable(png_enc_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/PngEncTests.cpp")
target_link_libraries(png_enc_tests common fmt::fmt)
target_include_directories(png_enc_tests PRIVATE ${inc_headers})
add_executable(ces_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/CesTests.cpp")
target_link_libraries(ces_tests common fmt::fmt SDL3::SDL3-static)
target_include_directories(ces_tests PRIVATE ${inc_headers})
add_executable(save_data_dialog_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/SaveDataDialogTests.cpp"
"${KYTY_SOURCE_DIR}/libs/dialog.cpp"
)
target_link_libraries(save_data_dialog_tests common fmt::fmt)
target_include_directories(save_data_dialog_tests PRIVATE ${inc_headers})
add_kyty_full_emulator_test(http_uri_parse_tests
"${KYTY_TESTS_DIR}/HttpUriParseTests.cpp")
@@ -564,7 +503,6 @@ target_include_directories(event_queue_lifetime_tests PRIVATE ${inc_headers})
add_kyty_full_emulator_test(kernel_file_system_tests
"${KYTY_TESTS_DIR}/KernelFileSystemTests.cpp")
target_link_libraries(kernel_file_system_tests ZArchive::zarchive)
add_executable(sync_on_address_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/SyncOnAddressTests.cpp"
@@ -631,10 +569,6 @@ if(BUILD_TESTING)
add_test(NAME image_page_table COMMAND $<TARGET_FILE:image_page_table_tests>)
add_test(NAME memory_tracker COMMAND $<TARGET_FILE:memory_tracker_tests>)
add_test(NAME page_manager COMMAND $<TARGET_FILE:page_manager_tests>)
add_test(NAME archive_file COMMAND $<TARGET_FILE:archive_file_tests>)
add_test(NAME trophy_system COMMAND $<TARGET_FILE:trophy_system_tests>)
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_tests>)
add_test(NAME vdecsw COMMAND $<TARGET_FILE:vdecsw_tests>)
add_test(NAME bit_array COMMAND $<TARGET_FILE:bit_array_tests>)
add_test(NAME lru_cache COMMAND $<TARGET_FILE:lru_cache_tests>)
add_test(NAME shader_vertex_metadata COMMAND $<TARGET_FILE:shader_vertex_metadata_tests>)
@@ -644,15 +578,9 @@ if(BUILD_TESTING)
add_test(NAME kernel_file_system COMMAND $<TARGET_FILE:kernel_file_system_tests>)
add_test(NAME sync_on_address COMMAND $<TARGET_FILE:sync_on_address_tests>)
add_test(NAME audio_out2_port COMMAND $<TARGET_FILE:audio_out2_port_tests>)
add_test(NAME audio_out_timing COMMAND $<TARGET_FILE:audio_out_timing_tests>)
add_test(NAME ngs2_sampler COMMAND $<TARGET_FILE:ngs2_sampler_tests>)
add_test(NAME pad_haptics COMMAND $<TARGET_FILE:pad_haptics_tests>)
add_test(NAME controller_settings COMMAND $<TARGET_FILE:controller_settings_tests>)
add_test(NAME host_input COMMAND $<TARGET_FILE:host_input_tests>)
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
add_test(NAME png_enc COMMAND $<TARGET_FILE:png_enc_tests>)
add_test(NAME ces COMMAND $<TARGET_FILE:ces_tests>)
add_test(NAME save_data_dialog COMMAND $<TARGET_FILE:save_data_dialog_tests>)
add_test(NAME http_uri_parse COMMAND $<TARGET_FILE:http_uri_parse_tests>)
add_test(NAME shader_recompiler_compute COMMAND $<TARGET_FILE:shader_recompiler_compute_tests>)
add_test(NAME shader_recompiler_alignbyte
@@ -675,8 +603,6 @@ if(BUILD_TESTING)
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --context-state-only)
add_test(NAME gpu_tiler
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --gpu-tiler-only)
add_test(NAME image_transfer_layout
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --image-layout-only)
add_test(NAME texture_cache_layered_image
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --layered-image-only)
@@ -692,15 +618,16 @@ if(BUILD_TESTING)
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --compute-meta-clear-only)
add_test(NAME buffer_cache_dirty_gc
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --buffer-cache-gc-only)
add_test(NAME color_render_target_1d
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --rt1d-only)
add_test(NAME texture_cache_image_overlap
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --image-overlap-only)
add_test(NAME texture_cache_htile_clear
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --htile-clear-only)
add_test(NAME buffer_cache_ranges
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --buffer-cache-range-only)
if(WIN32)
# These tests still depend on the Windows multisample-depth path.
add_test(NAME texture_cache_image_overlap
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --image-overlap-only)
add_test(NAME texture_cache_htile_clear
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --htile-clear-only)
add_test(NAME buffer_cache_ranges
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --buffer-cache-range-only)
endif()
add_custom_target(kyty_tests DEPENDS
ime_dialog_tests
@@ -709,10 +636,6 @@ if(BUILD_TESTING)
image_page_table_tests
memory_tracker_tests
page_manager_tests
archive_file_tests
trophy_system_tests
avplayer_file_tests
vdecsw_tests
bit_array_tests
lru_cache_tests
shader_vertex_metadata_tests
@@ -724,15 +647,9 @@ if(BUILD_TESTING)
shader_recompiler_compute_tests
virtual_memory_allocation_tests
audio_out2_port_tests
audio_out_timing_tests
ngs2_sampler_tests
pad_haptics_tests
controller_settings_tests
host_input_tests
save_data_memory_tests
png_enc_tests
ces_tests
save_data_dialog_tests
http_uri_parse_tests
)
endif()
@@ -743,9 +660,8 @@ if(APPLE AND (CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64" OR
add_executable(macos_sse4a_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/macosSse4aTests.cpp"
"${KYTY_SOURCE_DIR}/loader/x64InstructionEmulator.cpp"
"${KYTY_SOURCE_DIR}/loader/guestInstructionPatcher.cpp"
)
target_link_libraries(macos_sse4a_tests common fmt::fmt Zydis::Zydis xbyak::xbyak)
target_link_libraries(macos_sse4a_tests common fmt::fmt Zydis::Zydis)
target_include_directories(macos_sse4a_tests PRIVATE ${inc_headers})
if(BUILD_TESTING)
add_test(NAME macos_sse4a COMMAND $<TARGET_FILE:macos_sse4a_tests>)
@@ -757,23 +673,6 @@ endif()
add_executable(kyty_emulator "${KYTY_SOURCE_DIR}/main.cpp" ${kyty_emulator_src})
configure_macos_guest_address_space(kyty_emulator)
set(KYTY_TROPHY_UNLOCK_SOUND "${CMAKE_CURRENT_SOURCE_DIR}/assets/sounds/trophy-unlock.wav")
add_custom_target(kyty_trophy_sound
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:kyty_emulator>/assets/sounds"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${KYTY_TROPHY_UNLOCK_SOUND}"
"$<TARGET_FILE_DIR:kyty_emulator>/assets/sounds/trophy-unlock.wav"
VERBATIM)
add_dependencies(kyty_emulator kyty_trophy_sound)
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION assets/sounds)
add_custom_target(kyty_touch_cursors
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/assets/cursors"
"$<TARGET_FILE_DIR:kyty_emulator>/assets/cursors"
VERBATIM)
add_dependencies(kyty_emulator kyty_touch_cursors)
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/assets/cursors" DESTINATION assets)
target_link_libraries(kyty_emulator ${kyty_emulator_link_libraries})
if (WIN32)
target_link_libraries(kyty_emulator opengl32)
@@ -836,10 +735,6 @@ endif()
install(TARGETS kyty_emulator DESTINATION .)
if(KYTY_BUILD_LAUNCHER)
add_subdirectory("${KYTY_SOURCE_DIR}/launcher" "${CMAKE_CURRENT_BINARY_DIR}/launcher")
if(LINUX)
target_link_options(launcher PRIVATE $<$<CONFIG:Release>:-Wl,--strip-all>)
endif()
endif()
if(KYTY_CLANG_CL)
install(FILES "${KYTY_THIRD_PARTY_DIR}/winpthread/bin/libwinpthread-1.dll" DESTINATION .)
+39 -96
View File
@@ -1,18 +1,18 @@
# KytyPS5
[![Build KytyPS5 (Windows)](https://img.shields.io/github/check-runs/KytyPS5/KytyPS5/main?nameFilter=Build%20KytyPS5%20%28Windows%29&label=Build%20KytyPS5%20%28Windows%29)](https://github.com/KytyPS5/KytyPS5/actions/workflows/build.yml)
[![Build KytyPS5 (Linux)](https://img.shields.io/github/check-runs/KytyPS5/KytyPS5/main?nameFilter=Build%20KytyPS5%20%28Linux%29&label=Build%20KytyPS5%20%28Linux%29)](https://github.com/KytyPS5/KytyPS5/actions/workflows/build.yml)
[![Build KytyPS5 (macOS)](https://img.shields.io/github/check-runs/KytyPS5/KytyPS5/main?nameFilter=Build%20KytyPS5%20%28macOS%29&label=Build%20KytyPS5%20%28macOS%29)](https://github.com/KytyPS5/KytyPS5/actions/workflows/build.yml)
[![Build KytyPS5 (Windows)](https://img.shields.io/github/actions/workflow/status/KytyPS5/KytyPS5/build.yml?branch=main&event=push&label=Build%20KytyPS5%20%28Windows%29)](https://github.com/KytyPS5/KytyPS5/actions/workflows/build.yml)
[![Build KytyPS5 (Linux)](https://img.shields.io/github/actions/workflow/status/KytyPS5/KytyPS5/build.yml?branch=main&event=push&label=Build%20KytyPS5%20%28Linux%29)](https://github.com/KytyPS5/KytyPS5/actions/workflows/build.yml)
[![Build KytyPS5 (macOS)](https://img.shields.io/github/actions/workflow/status/KytyPS5/KytyPS5/build.yml?branch=main&event=push&label=Build%20KytyPS5%20%28macOS%29)](https://github.com/KytyPS5/KytyPS5/actions/workflows/build.yml)
[![Platform](https://img.shields.io/badge/platform-Windows%20x64%20%7C%20Linux%20x64%20%7C%20macOS%20x86__64-0078D4.svg)](#system-requirements)
[![Status](https://img.shields.io/badge/status-active%20development-orange.svg)](#current-status)
[![License](https://img.shields.io/badge/license-GPL--2.0-blue.svg)](LICENSE)
**[Weekly updates](https://github.com/KytyPS5/KytyPS5/discussions/862)** — game progress, recent fixes, and ongoing development.
**[Weekly updates](https://github.com/KytyPS5/KytyPS5/discussions/862)** — game progress, recent fixes and ongoing development.
**[Development on Discord](https://discord.gg/UNrkMqGaBg)** — KytyPS5 development.
KytyPS5 is a free and open-source PlayStation 5 translation layer written in C++ for Windows
and Linux, with experimental macOS support. It is based on a heavily modified version of
KytyPS5 is a free and open-source PlayStation 5 emulator written in C++ for Windows and Linux,
with experimental macOS support. It is based on a heavily modified version of
[Kyty](https://github.com/InoriRus/Kyty). The project is in active development, and behavior
can change significantly between builds.
@@ -23,13 +23,13 @@ can change significantly between builds.
## Current Status
KytyPS5 can boot 2D and 3D games, including titles built with Unreal Engine
KytyPS5 can boot 2D games and a selection of 3D games, including titles built with Unreal Engine
4/5, Unity, and custom engines. External low-level emulation modules are neither required nor
planned.
Development is currently focused on expanding game compatibility and improving boot reliability.
Windows and Linux are the primary target platforms and receive the most testing.
Windows and Linux are the primary platforms and receive the most testing.
macOS support is experimental. The emulator is built for x86-64 and runs on Apple Silicon under
Rosetta 2, with Vulkan provided by MoltenVK. A small number of titles have been verified in-game
@@ -52,8 +52,8 @@ or graphical glitches, so please include the version you tested when reporting a
<img src="docs/screenshots/ps5-01.png" width="300" alt="Astro Bot running in KytyPS5">
</td>
<td align="center">
<strong>Saros</strong><br>
<img src="docs/screenshots/ps5-03.png" width="300" alt="Saros running in KytyPS5">
<strong>Dreaming Sarah</strong><br>
<img src="docs/screenshots/ps5-03.png" width="300" alt="Dreaming Sarah running in KytyPS5">
</td>
</tr>
<tr>
@@ -72,8 +72,8 @@ or graphical glitches, so please include the version you tested when reporting a
<img src="docs/screenshots/ps5-02.png" width="300" alt="Demon's Souls running in KytyPS5">
</td>
<td align="center">
<strong>UFC 6</strong><br>
<img src="docs/screenshots/ps5-06.jpg" width="300" alt="UFC 6 running in KytyPS5">
<strong>Hellboy</strong><br>
<img src="docs/screenshots/ps5-06.png" width="300" alt="Hellboy running in KytyPS5">
</td>
</tr>
</table>
@@ -85,20 +85,14 @@ or graphical glitches, so please include the version you tested when reporting a
Testing games and submitting detailed bug reports are useful ways to contribute. Search existing
issues first, then use the **Game Emulation Status Report** template and attach the complete log file.
**Pull requests for general fixes, architectural changes, or refactoring that do not address a concrete bug or launch problem in a specific game will be automatically closed.**
**Large, unfocused commits or pull requests that bundle unrelated changes without a clear purpose or game-related benefit will also be automatically closed.**
Keep each PR focused. Name the affected game, describe the exact problem, and provide evidence showing what the change fixes.
Windows and Linux are the primary target platforms, so changes to shared code should not
introduce regressions on either platform. Changes confined to a platform's own code paths
only need to build on that platform. Because KytyPS5 is still evolving quickly, consider
opening an issue before starting a large change.
Code contributions should be focused, build successfully on the platforms they touch, and include
relevant tests where practical. Windows is the primary target, so a change that alters shared code
should not regress it; changes confined to a platform's own code paths only need to build there. Because KytyPS5 is still evolving quickly, consider opening an issue before
starting a large change.
### Formatting
Set up the clang-format hook after cloning.
Set up the clang-format hook after cloning:
Install `pre-commit` using the method appropriate for your platform:
@@ -147,8 +141,7 @@ the Vulkan/SPIR-V validation rules.
- Visual Studio 2022 or Build Tools 2022 with the **Desktop development with C++** workload and
**C++ Clang tools for Windows** component
- Qt 6 for MSVC 2022 64-bit, including Concurrent, Network, and Widgets
- [glslang](https://github.com/KhronosGroup/glslang/releases) (`glslang` or `glslangValidator`) on `PATH`
- Python 3 on `PATH`; the bundled SPIRV-Tools runs it at configure time
- [glslang](https://github.com/KhronosGroup/glslang/releases) (`glslangValidator`) on `PATH`
The Microsoft C++ compiler (`cl.exe`) is not supported; use `clang-cl`.
@@ -177,20 +170,20 @@ The finished application and its runtime dependencies will be placed in
### Building on Linux
Install the toolchain and the libraries the bundled SDL3 needs. Without the audio, Wayland, and
udev development packages, SDL3 quietly configures itself without those backends, and the resulting
build has no working audio or gamepad hotplug support:
Install the toolchain and the libraries the bundled SDL3 needs. Without the audio, Wayland and
udev development packages SDL3 quietly configures itself without those backends, and the resulting
build has no working sound and no gamepad hotplug:
```bash
sudo apt-get install --no-install-recommends \
clang lld ninja-build cmake git glslang-tools python3 pkg-config \
clang lld ninja-build cmake git glslang-tools pkg-config \
libgl1-mesa-dev libx11-dev libxcursor-dev libxext-dev libxfixes-dev \
libxi-dev libxrandr-dev libxss-dev libxtst-dev libxkbcommon-dev \
libasound2-dev libpulse-dev libudev-dev libdbus-1-dev libwayland-dev wayland-protocols
```
Qt 6 (Concurrent, Network, and Widgets) is required for the launcher. Use your distribution's
packages (`qt6-base-dev`) or an official Qt installation.
Qt 6 (Concurrent, Network, Widgets) is required for the launcher — either the distribution packages
(`qt6-base-dev`) or an official Qt installation.
```bash
git submodule update --init --recursive
@@ -204,7 +197,7 @@ cmake --install _Build/linux --prefix _Build/linux/install
```
The install step copies the Qt libraries and plugins next to the binaries, so
`_Build/linux/install` runs without a matching system Qt installation. FFmpeg is linked statically
`_Build/linux/install` runs without a matching system Qt. FFmpeg is linked statically
from the pinned [KytyPS5 FFmpeg core](https://github.com/KytyPS5/ext-ffmpeg-core)
release, including VP9 and WebM support. System FFmpeg packages are not required.
@@ -243,47 +236,9 @@ cmake --build _Build/linux --target launcher --parallel
cmake --install _Build/linux --prefix _Build/linux/install
```
The configure step downloads the FFmpeg prebuilts and the `xbyak`, `zydis`, `zstd`, and ZArchive
sources, so it needs network access; a fully sandboxed `nix build` would require vendoring those
inputs. A Vulkan 1.3 driver must be available at runtime (on NixOS,
`hardware.graphics.enable = true`).
### Building on Fedora and Fedora Atomic (Bazzite, Bluefin, Silverblue)
On regular Fedora, install dependencies on the host. On Fedora Atomic variants such as Bazzite,
Bluefin and Silverblue, use [Distrobox](https://distrobox.it/) and run the install and build steps
inside the container. Keep the checkout in your home directory so it is shared with the container.
Add `--nvidia` to the create command for hosts using the proprietary NVIDIA driver:
```bash
distrobox create --name kyty-build --image registry.fedoraproject.org/fedora:latest
distrobox enter kyty-build
```
Install the toolchain, Qt 6 and SDL3 dependencies:
```bash
sudo dnf install -y \
clang lld ninja-build cmake git glslang python3 pkgconf-pkg-config \
mesa-libGL-devel libX11-devel libXcursor-devel libXext-devel libXfixes-devel \
libXi-devel libXrandr-devel libXScrnSaver-devel libXtst-devel libxkbcommon-devel \
alsa-lib-devel pulseaudio-libs-devel systemd-devel dbus-devel \
wayland-devel wayland-protocols-devel qt6-qtbase-devel
```
From the repository root, follow the launcher configure, build and install steps in
[Building on Linux](#building-on-linux), omitting `-DCMAKE_PREFIX_PATH="$Qt6_DIR"`; CMake finds
Fedora's packaged Qt 6 automatically.
Run `./_Build/linux/install/kyty_launcher` on regular Fedora or inside the container. To launch a
container build from the host, leave the container with `exit`, then run from the repository root:
```bash
distrobox enter kyty-build -- ./_Build/linux/install/kyty_launcher
```
Qt libraries and plugins are bundled. You can also launch directly on a Fedora Atomic host
if its other system libraries are compatible with the container build.
The configure step downloads the FFmpeg prebuilts and the `xbyak`/`zydis` sources, so it needs
network access; a fully sandboxed `nix build` would require vendoring those inputs. A Vulkan 1.3
driver must be available at runtime (on NixOS, `hardware.graphics.enable = true`).
### Building on macOS
@@ -295,8 +250,8 @@ Requirements:
- An Apple Silicon Mac with Rosetta 2 installed (`softwareupdate --install-rosetta`)
- Xcode (or the Command Line Tools)
- Homebrew packages: `brew install cmake ninja glslang python`
- Qt 6 (Concurrent, Network, and Widgets) with x86-64 support. The official Qt installation is
- Homebrew packages: `brew install cmake ninja glslang`
- Qt 6 (Concurrent, Network, Widgets) with x86-64 support. The official Qt installation is
universal and works; Homebrew's Qt is arm64-only and will not link
```bash
@@ -312,14 +267,14 @@ cmake --install _Build/macos --prefix _Build/macos/install
```
The build re-signs `kyty_emulator` with the JIT entitlements it needs to execute translated
guest code; no manual signing step is required. When the launcher is built, the installation
guest code; no manual signing step is required. When the launcher is built, the install
also produces `_Build/macos/install/KytyPS5.app` — double-click to launch the GUI.
A flat `kyty_emulator` is kept for CLI usage.
Vulkan comes from MoltenVK. Download `MoltenVK-macos.tar` from the
[MoltenVK releases](https://github.com/KhronosGroup/MoltenVK/releases), then copy
`MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib` next to the flat `kyty_emulator`
(and, for the bundle, into `KytyPS5.app/Contents/Frameworks/`) and sign it with an ad hoc signature:
(and, for the bundle, into `KytyPS5.app/Contents/Frameworks/`) and ad-hoc sign it:
```bash
codesign --force --sign - _Build/macos/install/libMoltenVK.dylib
@@ -345,7 +300,7 @@ Use `_Build/linux` instead of `_Build/windows` for a Linux build.
A ready-made Visual Studio Code setup is included in [`.vscode`](.vscode). It configures CMake
Tools to build the project with Ninja and `clang-cl` and provides launch profiles for both
`kyty_launcher.exe` and `kyty_emulator.exe`. It is Windows-only: VS Code settings cannot select a
`launcher.exe` and `kyty_emulator.exe`. It is Windows-only: VS Code settings cannot select a
compiler per platform, so on Linux configure from the command line as shown above.
Before using it:
@@ -365,11 +320,11 @@ Update your graphics driver before reporting rendering problems.
To use the graphical launcher:
```powershell
.\_Build\windows\install\kyty_launcher.exe
.\_Build\windows\install\launcher.exe
```
```bash
./_Build/linux/install/kyty_launcher
./_Build/linux/install/launcher
```
```bash
@@ -377,29 +332,17 @@ open _Build/macos/install/KytyPS5.app # or double-click in Finder
```
On first launch, add one or more game folders in the global settings. The launcher searches those
folders recursively for game directories containing `eboot.bin` and ZArchive (`.zar`) game dumps
whose archive root contains `eboot.bin`. Select a detected game and run it from the game list.
ZArchive dumps are mounted read-only and streamed directly; they do not need to be extracted first.
folders recursively for game directories containing `eboot.bin`. Select a detected game and run it
from the game list.
Official release builds include a built-in updater. Leave **Check for updates on startup** enabled,
or click **Check for updates**, then choose **Install Update**. The launcher downloads the package
for your platform from GitHub, checks its SHA-256 checksum, and restarts after installation. Close
any running games first. Settings, saves, patches, and other files outside the release package are
preserved; replacement failures restore the previous files. The installation folder must be writable.
If a package cannot be installed automatically, the updater offers the GitHub release page.
Source builds and builds from forks do not replace themselves with official releases.
The emulator can also be started directly with a legally obtained game directory, ELF file, or
ZArchive dump:
The emulator can also be started directly with a legally obtained game directory or ELF file:
```powershell
.\_Build\windows\install\kyty_emulator.exe --game "D:\Games\ExampleGame"
.\_Build\windows\install\kyty_emulator.exe --game "D:\Games\ExampleGame.zar"
```
```bash
./_Build/linux/install/kyty_emulator --game "/games/ExampleGame"
./_Build/linux/install/kyty_emulator --game "/games/ExampleGame.zar"
```
On macOS, the adjacent flat or app-bundled `libMoltenVK.dylib` is found automatically; no
@@ -444,5 +387,5 @@ licenses included with those components.
- [InoriRus/Kyty](https://github.com/InoriRus/Kyty) — KytyPS5 is based on a heavily modified version
of the original Kyty project.
- [shadps4-emu/shadPS4](https://github.com/shadps4-emu/shadPS4) — reference for understanding PS4
memory behavior, GPU resource aliasing, cache coherency,
memory behavior, GPU resource aliasing and cache coherency,
and the AVPlayer implementation.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "تم كسب الكأس!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trofej získána!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trofæ optjent!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trophäe erhalten!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trophy earned!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trophy earned!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "¡Ganaste un trofeo!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "¡Ganaste un trofeo!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Pokaali ansaittu!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trophée obtenu !"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trophée obtenu !"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trófea megszerezve!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trofi diperoleh!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trofeo ottenuto!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "トロフィーを獲得しました!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "트로피를 획득했습니다!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trofee behaald!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trofé opptjent!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Zdobyto trofeum!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Troféu conquistado!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Troféu conquistado!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trofeu câștigat!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Трофей получен!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Trofé intjänad!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Kupa kazanıldı!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "Đã nhận cúp!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "已获得奖杯!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"trophy.toast.earned": "已獲得獎盃!"
}
Binary file not shown.
-20
View File
@@ -1,20 +0,0 @@
# Notification localization
Trophy notification subtitles use the configured console language. Trophy names still come
from the game's metadata. The launcher trophy dialogs are unchanged.
Edit `assets/localization/<locale>.json`, using flat UTF-8 string values:
```json
{
"trophy.toast.earned": "Trophy earned!"
}
```
CMake converts the catalogs into C++ byte arrays in the build directory. The emulator parses
the embedded catalogs once and falls back to `en-US.json` for missing translations. No language
files or translation service are needed at runtime. Rebuild after editing a catalog.
For local testing, rebuild `kyty_emulator`, choose a game's Console Language in the launcher,
and unlock a trophy with notifications enabled. Installed system fonts provide non-Latin
glyphs; the selected language needs an appropriate font on the host.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

-16
View File
@@ -12,23 +12,9 @@ endif()
add_library(common STATIC ${common_src} ${common_headers})
set(localization_dir "${PROJECT_SOURCE_DIR}/assets/localization")
file(GLOB localization_catalogs CONFIGURE_DEPENDS "${localization_dir}/*.json")
set(localization_data "${CMAKE_CURRENT_BINARY_DIR}/localizationData.h")
add_custom_command(
OUTPUT "${localization_data}"
COMMAND ${CMAKE_COMMAND} "-DINPUTS=${localization_catalogs}" "-DOUTPUT=${localization_data}"
-P "${KYTY_SOURCE_DIR}/embed_localization.cmake"
DEPENDS ${localization_catalogs} "${KYTY_SOURCE_DIR}/embed_localization.cmake"
COMMENT "Embed notification localization catalogs"
VERBATIM
)
target_sources(common PRIVATE "${localization_data}")
target_include_directories(common
PUBLIC ${KYTY_SOURCE_DIR}
PRIVATE
${CMAKE_CURRENT_BINARY_DIR}
${KYTY_THIRD_PARTY_DIR}/cpuinfo/include
)
@@ -40,8 +26,6 @@ target_link_libraries(common
cpuinfo
kyty_git_version
spdlog::spdlog
ZArchive::zarchive
nlohmann_json::nlohmann_json
)
get_property(inc_headers TARGET common PROPERTY INCLUDE_DIRECTORIES)
-112
View File
@@ -1,112 +0,0 @@
#include "common/additionalContent.h"
#include "common/stringUtils.h"
#include <algorithm>
#include <fstream>
#include <map>
#include <nlohmann/json.hpp>
#include <regex>
#include <system_error>
namespace Common {
namespace {
std::string GetString(const nlohmann::json& object, const std::string& key) {
const auto it = object.find(key);
return it != object.end() && it->is_string() ? it->get<std::string>() : std::string {};
}
std::string GetTitle(const nlohmann::json& param) {
const auto localized = param.find("localizedParameters");
if (localized != param.end() && localized->is_object()) {
const auto preferred = localized->find(GetString(*localized, "defaultLanguage"));
if (preferred != localized->end()) {
if (auto title = GetString(*preferred, "titleName"); !title.empty()) {
return title;
}
}
for (const auto& language: *localized) {
if (auto title = GetString(language, "titleName"); !title.empty()) {
return title;
}
}
}
return GetString(param, "titleName");
}
} // namespace
std::vector<AddContentEntry> FindAddContent(const std::filesystem::path& dir,
const std::string& title_id) {
if (dir.empty() || title_id.empty()) {
return {};
}
std::error_code ec;
const auto root = std::filesystem::absolute(dir, ec);
if (ec) {
return {};
}
// Scan directories separately so an unreadable child does not discard its siblings.
std::vector<std::filesystem::path> pending {root};
std::vector<std::filesystem::path> params;
while (!pending.empty()) {
const auto current = std::move(pending.back());
pending.pop_back();
std::filesystem::directory_iterator it(
current, std::filesystem::directory_options::skip_permission_denied, ec);
const std::filesystem::directory_iterator end;
for (; !ec && it != end; it.increment(ec)) {
std::error_code entry_ec;
if (it->is_directory(entry_ec) && !it->is_symlink(entry_ec) && !entry_ec) {
pending.push_back(it->path());
} else if (it->is_regular_file(entry_ec) && !entry_ec &&
EqualNoCase(PathToString(it->path().filename()), "param.json") &&
EqualNoCase(PathToString(it->path().parent_path().filename()), "sce_sys")) {
params.push_back(it->path());
}
}
}
std::sort(params.begin(), params.end());
static const std::regex content_id_pattern(
"^[A-Z]{2}[0-9]{4}-[A-Z]{4}[0-9]{5}_[0-9]{2}-[A-Z0-9]{16}$");
std::map<std::string, AddContentEntry> by_label;
for (const auto& file: params) {
const auto size = std::filesystem::file_size(file, ec);
if (ec || size > 1024 * 1024) {
continue;
}
std::ifstream stream(file, std::ios::binary);
if (!stream) {
continue;
}
const auto param = nlohmann::json::parse(stream, nullptr, false);
// applicationCategoryType is required for applications, not additional content.
if (!param.is_object() || param.contains("applicationCategoryType")) {
continue;
}
const auto content_id = GetString(param, "contentId");
if (!std::regex_match(content_id, content_id_pattern) ||
content_id.compare(7, 9, title_id) != 0) {
continue;
}
const auto mount_dir = file.parent_path().parent_path();
const auto label = content_id.substr(20);
by_label.try_emplace(label,
AddContentEntry {content_id, label, mount_dir, GetTitle(param)});
}
std::vector<AddContentEntry> entries;
entries.reserve(by_label.size());
for (auto& [label, entry]: by_label) {
entries.push_back(std::move(entry));
}
return entries;
}
} // namespace Common
-23
View File
@@ -1,23 +0,0 @@
#ifndef KYTY_COMMON_ADDITIONAL_CONTENT_H_
#define KYTY_COMMON_ADDITIONAL_CONTENT_H_
#include <filesystem>
#include <string>
#include <vector>
namespace Common {
struct AddContentEntry {
std::string content_id;
std::string entitlement_label;
std::filesystem::path mount_dir;
std::string title;
};
// Finds packages for this title, deduplicated by entitlement label.
std::vector<AddContentEntry> FindAddContent(const std::filesystem::path& dir,
const std::string& title_id);
} // namespace Common
#endif /* KYTY_COMMON_ADDITIONAL_CONTENT_H_ */
-397
View File
@@ -1,397 +0,0 @@
#include "common/archive.h"
#include "common/stringUtils.h"
#include <algorithm>
#include <condition_variable>
#include <cstring>
#include <mutex>
#include <optional>
#include <string_view>
#include <thread>
#include <unordered_map>
#include <utility>
#include <zarchive/zarchivereader.h>
// POSIX hosts and MinGW's pthread-backed libstdc++ have cancellable condition waits.
// MSVC/ClangCL and MinGW's native Win32 threading backend do not use pthread waits.
#if !defined(_WIN32) || defined(_GLIBCXX_GCC_GTHR_POSIX_H)
#include <pthread.h>
#define KYTY_ARCHIVE_PTHREAD_CANCELLATION
#endif
namespace Common {
// Backends expose immutable metadata and offset-based reads. Cursor management, path routing,
// reader lifetime and the host-stack worker are shared by every archive format.
class ArchiveReader {
public:
struct Entry {
uint64_t id;
uint64_t size;
bool is_file;
};
virtual ~ArchiveReader() = default;
virtual std::optional<Entry> Find(std::string_view member) = 0;
virtual std::vector<File::DirEntry> List(std::string_view member) = 0;
virtual uint64_t Read(uint64_t id, uint64_t offset, uint32_t size, void* data) = 0;
};
namespace {
class ZArchiveReaderBackend final: public ArchiveReader {
public:
explicit ZArchiveReaderBackend(std::unique_ptr<ZArchiveReader> reader)
: m_reader(std::move(reader)) {}
std::optional<Entry> Find(std::string_view member) override {
const auto node = m_reader->LookUp(member);
if (node == ZARCHIVE_INVALID_NODE) {
return {};
}
return Entry {node, m_reader->GetFileSize(node), m_reader->IsFile(node)};
}
std::vector<File::DirEntry> List(std::string_view member) override {
const auto node = m_reader->LookUp(member);
if (!m_reader->IsDirectory(node)) {
return {};
}
const auto count = m_reader->GetDirEntryCount(node);
if (count > (1u << 20u)) {
return {};
}
std::vector<File::DirEntry> result;
result.reserve(count);
for (uint32_t index = 0; index < count; ++index) {
ZArchiveReader::DirEntry entry {};
if (m_reader->GetDirEntry(node, index, entry)) {
result.push_back({std::string(entry.name), entry.isFile});
}
}
return result;
}
uint64_t Read(uint64_t id, uint64_t offset, uint32_t size, void* data) override {
// ZArchiveReader already serializes access to its stream and decompression cache.
return m_reader->ReadFromFile(static_cast<ZArchiveNodeHandle>(id), offset, size, data);
}
private:
std::unique_ptr<ZArchiveReader> m_reader;
};
std::shared_ptr<ArchiveReader> OpenZArchive(const std::filesystem::path& path) {
auto reader = std::unique_ptr<ZArchiveReader>(ZArchiveReader::OpenFromFile(path));
return reader ? std::make_shared<ZArchiveReaderBackend>(std::move(reader)) : nullptr;
}
struct ArchiveFormat {
std::string_view extension;
std::shared_ptr<ArchiveReader> (*open)(const std::filesystem::path&);
};
constexpr ArchiveFormat Formats[] = {{".zar", OpenZArchive}};
using NativeView = std::basic_string_view<std::filesystem::path::value_type>;
const ArchiveFormat* FindFormat(NativeView path) {
for (const auto& format: Formats) {
if (path.size() < format.extension.size()) {
continue;
}
const auto extension = path.substr(path.size() - format.extension.size());
if (std::equal(
extension.begin(), extension.end(), format.extension.begin(),
[](auto a, char b) { return (a >= 'A' && a <= 'Z' ? a + ('a' - 'A') : a) == b; })) {
return &format;
}
}
return nullptr;
}
size_t FindMarker(NativeView path) {
// Native files use this check on every operation. The usual path has no marker and allocates
// nothing.
for (auto marker = path.find('!'); marker != NativeView::npos;
marker = path.find('!', marker + 1)) {
if ((marker + 1 == path.size() || path[marker + 1] == '/' || path[marker + 1] == '\\') &&
FindFormat(path.substr(0, marker)) != nullptr) {
return marker;
}
}
return NativeView::npos;
}
struct ParsedPath {
std::filesystem::path archive;
std::string member;
};
std::optional<ParsedPath> ParsePath(const std::filesystem::path& path) {
const auto& native = path.native();
const auto marker = FindMarker(native);
if (marker == NativeView::npos) {
return {};
}
ParsedPath parsed {.archive = std::filesystem::path(native.substr(0, marker))};
auto member = PathToGenericString(std::filesystem::path(native.substr(marker + 1)));
std::replace(member.begin(), member.end(), '\\', '/');
std::string_view remaining = member;
while (!remaining.empty()) {
const auto separator = remaining.find('/');
const auto component = remaining.substr(0, separator);
remaining.remove_prefix(separator == std::string_view::npos ? remaining.size() : separator + 1);
if (component.empty() || component == ".") {
continue;
}
if (component == "..") {
if (parsed.member.empty()) {
return {};
}
const auto separator = parsed.member.rfind('/');
parsed.member.resize(separator == std::string::npos ? 0 : separator);
} else {
if (!parsed.member.empty()) {
parsed.member += '/';
}
parsed.member += component;
}
}
return parsed;
}
std::mutex g_readers_mutex;
std::unordered_map<std::filesystem::path, std::weak_ptr<ArchiveReader>> g_readers;
// Guest stacks can be only 64 KiB. Decompress on an ordinary host stack, then copy into guest
// memory on the caller so GPU-tracked writes use its fault handler. A single worker bounds host
// threads and avoids duplicating the archive's own block cache. Requests live on waiting callers.
class ArchiveIoThread {
public:
static ArchiveIoThread& Instance() {
// Guest exit can run static destructors while other guest threads are still reading.
// Keep the worker alive until process termination, as with the guest threads themselves.
static auto* instance = new ArchiveIoThread;
return *instance;
}
uint64_t Read(ArchiveReader& reader, uint64_t id, uint64_t offset, uint32_t size, void* data) {
Request request {reader, id, offset, size, data};
std::unique_lock lock(m_mutex);
if (m_last != nullptr) {
m_last->next = &request;
} else {
m_first = &request;
}
m_last = &request;
m_wake.notify_one();
request.ready.wait(lock, [&] { return request.done; });
return request.result;
}
KYTY_CLASS_NO_COPY(ArchiveIoThread);
private:
struct Request {
ArchiveReader& reader;
uint64_t id;
uint64_t offset;
uint32_t size;
void* data;
Request* next = nullptr;
uint64_t result = 0;
bool done = false;
std::condition_variable ready {};
};
ArchiveIoThread() {
std::thread([this] { Loop(); }).detach();
}
void Loop() {
for (;;) {
Request* request;
{
std::unique_lock lock(m_mutex);
m_wake.wait(lock, [this] { return m_first != nullptr; });
request = m_first;
m_first = request->next;
if (m_first == nullptr) {
m_last = nullptr;
}
}
const auto result =
request->reader.Read(request->id, request->offset, request->size, request->data);
{
std::lock_guard lock(m_mutex);
request->result = result;
request->done = true;
// Notify before releasing the mutex: the waiting caller owns the condition
// variable.
request->ready.notify_one();
}
}
}
std::mutex m_mutex;
std::condition_variable m_wake;
Request* m_first = nullptr;
Request* m_last = nullptr;
};
} // namespace
struct ArchiveFile::Private {
std::shared_ptr<ArchiveReader> reader;
ArchiveReader::Entry entry;
uint64_t position = 0;
};
ArchiveFile::ArchiveFile(std::unique_ptr<Private> p): m_p(std::move(p)) {}
ArchiveFile::~ArchiveFile() = default;
uint64_t ArchiveFile::Size() const {
return m_p->entry.size;
}
uint64_t ArchiveFile::Tell() const {
return m_p->position;
}
bool ArchiveFile::Seek(uint64_t offset) {
m_p->position = offset;
return true;
}
void ArchiveFile::Read(void* data, uint32_t size, uint32_t* bytes_read) {
#if defined(KYTY_ARCHIVE_PTHREAD_CANCELLATION)
// A pending cancellation must not destroy the waiting request or its TLS buffer while the
// worker still uses them. Restore the caller's state only after the copy is complete.
int cancellation_state = PTHREAD_CANCEL_ENABLE;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cancellation_state);
#endif
constexpr uint32_t chunk_size = 1u << 20u;
thread_local std::vector<uint8_t> chunk;
uint32_t read = 0;
if (data != nullptr && size != 0 && m_p->position < Size()) {
const auto requested =
static_cast<uint32_t>(std::min<uint64_t>(size, Size() - m_p->position));
if (chunk.size() < std::min(chunk_size, requested)) {
chunk.resize(std::min(chunk_size, requested));
}
while (read < requested) {
const auto step = std::min(chunk_size, requested - read);
const auto got = ArchiveIoThread::Instance().Read(*m_p->reader, m_p->entry.id,
m_p->position, step, chunk.data());
if (got == 0 || got > step) {
break;
}
std::memcpy(static_cast<uint8_t*>(data) + read, chunk.data(), got);
read += static_cast<uint32_t>(got);
m_p->position += got;
if (got < step) {
break;
}
}
}
if (bytes_read != nullptr) {
*bytes_read = read;
}
#if defined(KYTY_ARCHIVE_PTHREAD_CANCELLATION)
pthread_setcancelstate(cancellation_state, nullptr);
#endif
}
std::filesystem::path MakeArchivePath(const std::filesystem::path& archive,
const std::filesystem::path& member) {
auto root = archive;
root += "!";
return member.empty() ? root : root / member.relative_path();
}
bool IsSupportedArchive(const std::filesystem::path& path) {
return FindFormat(path.native()) != nullptr;
}
bool IsArchivePath(const std::filesystem::path& path) {
return FindMarker(path.native()) != NativeView::npos;
}
std::filesystem::path GetArchiveHostPath(const std::filesystem::path& path) {
const auto marker = FindMarker(path.native());
return marker == NativeView::npos ? std::filesystem::path {}
: std::filesystem::path(path.native().substr(0, marker));
}
std::shared_ptr<ArchiveReader> OpenArchive(const std::filesystem::path& path) {
const auto archive = IsArchivePath(path) ? GetArchiveHostPath(path) : path;
const auto* format = FindFormat(archive.native());
if (format == nullptr) {
return {};
}
std::error_code error;
auto key = std::filesystem::absolute(archive, error);
if (error) {
return {};
}
{
std::lock_guard lock(g_readers_mutex);
if (auto it = g_readers.find(key); it != g_readers.end()) {
if (auto reader = it->second.lock()) {
return reader;
}
}
}
// Do not hold the cache mutex while opening an unrelated archive from disk.
auto reader = format->open(archive);
if (reader == nullptr) {
return {};
}
std::lock_guard lock(g_readers_mutex);
std::erase_if(g_readers, [](const auto& entry) { return entry.second.expired(); });
auto& cached = g_readers[key];
if (auto existing = cached.lock()) {
return existing;
}
cached = reader;
return reader;
}
std::optional<File::Info> GetArchiveInfo(const std::filesystem::path& path) {
const auto parsed = ParsePath(path);
if (parsed) {
if (auto reader = OpenArchive(parsed->archive)) {
if (auto entry = reader->Find(parsed->member)) {
return File::Info {entry->is_file, entry->size};
}
}
}
return {};
}
std::vector<File::DirEntry> GetArchiveDirEntries(const std::filesystem::path& path) {
const auto parsed = ParsePath(path);
if (parsed) {
if (auto reader = OpenArchive(parsed->archive)) {
return reader->List(parsed->member);
}
}
return {};
}
std::unique_ptr<ArchiveFile> OpenArchiveFile(const std::filesystem::path& path) {
auto parsed = ParsePath(path);
if (!parsed) {
return {};
}
auto reader = OpenArchive(parsed->archive);
const auto entry = reader ? reader->Find(parsed->member) : std::nullopt;
if (!entry || !entry->is_file) {
return {};
}
auto p = std::make_unique<ArchiveFile::Private>();
p->reader = std::move(reader);
p->entry = *entry;
return std::unique_ptr<ArchiveFile>(new ArchiveFile(std::move(p)));
}
} // namespace Common
-50
View File
@@ -1,50 +0,0 @@
#ifndef KYTY_COMMON_ARCHIVE_H_
#define KYTY_COMMON_ARCHIVE_H_
#include "common/file.h"
#include <cstdint>
#include <filesystem>
#include <memory>
#include <vector>
namespace Common {
class ArchiveReader;
class ArchiveFile {
public:
~ArchiveFile();
[[nodiscard]] uint64_t Size() const;
[[nodiscard]] uint64_t Tell() const;
bool Seek(uint64_t offset);
void Read(void* data, uint32_t size, uint32_t* bytes_read);
KYTY_CLASS_NO_COPY(ArchiveFile);
private:
struct Private;
explicit ArchiveFile(std::unique_ptr<Private> p);
std::unique_ptr<Private> m_p;
friend std::unique_ptr<ArchiveFile> OpenArchiveFile(const std::filesystem::path& path);
};
// Virtual paths use "archive.ext!/member" and retain native host-path encoding.
[[nodiscard]] std::filesystem::path MakeArchivePath(const std::filesystem::path& archive,
const std::filesystem::path& member = {});
[[nodiscard]] bool IsSupportedArchive(const std::filesystem::path& path);
[[nodiscard]] bool IsArchivePath(const std::filesystem::path& path);
[[nodiscard]] std::optional<File::Info> GetArchiveInfo(const std::filesystem::path& path);
[[nodiscard]] std::vector<File::DirEntry> GetArchiveDirEntries(const std::filesystem::path& path);
[[nodiscard]] std::filesystem::path GetArchiveHostPath(const std::filesystem::path& path);
[[nodiscard]] std::unique_ptr<ArchiveFile> OpenArchiveFile(const std::filesystem::path& path);
// Retain this handle while a mount or metadata scan is active to reuse the index and block cache.
// Accepts either a host archive filename or a virtual path inside the archive.
[[nodiscard]] std::shared_ptr<ArchiveReader> OpenArchive(const std::filesystem::path& path);
} // namespace Common
#endif /* KYTY_COMMON_ARCHIVE_H_ */
-45
View File
@@ -23,7 +23,6 @@ void Load(const ConfigOptions& cfg) {
EXIT_IF(g_config == nullptr);
EXIT_IF(cfg.user_name.empty() || cfg.user_name.size() > MAX_USER_NAME_LENGTH);
EXIT_IF(!IsConfiguredUserIdValid(cfg.user_id));
EXIT_IF(cfg.controller_speaker_volume > 100 || cfg.controller_vibration_intensity > 100);
*g_config = cfg;
}
@@ -48,18 +47,6 @@ const std::string& GetAudioInputDevice() {
return g_config->audio_input_device;
}
const std::optional<ControllerColor>& GetControllerColor() {
return g_config->controller_color;
}
uint32_t GetControllerSpeakerVolume() {
return g_config->controller_speaker_volume;
}
uint32_t GetControllerVibrationIntensity() {
return g_config->controller_vibration_intensity;
}
PresentMode GetPresentMode() {
return g_config->present_mode;
}
@@ -72,14 +59,6 @@ bool FullscreenEnabled() {
return g_config->fullscreen_enabled;
}
bool HdrEnabled() {
return g_config->hdr_enabled;
}
bool HideCursorEnabled() {
return g_config->hide_cursor_enabled;
}
bool VrEnabled() {
return g_config->vr_enabled;
}
@@ -100,10 +79,6 @@ bool VulkanValidationEnabled() {
return g_config->vulkan_validation_enabled;
}
bool IsNetworkConnected() {
return g_config->network_connected;
}
bool ShaderValidationEnabled() {
return g_config->shader_validation_enabled;
}
@@ -160,30 +135,14 @@ bool ReadbackLinearImagesEnabled() {
return g_config->readback_linear_images;
}
bool SyncRawImageBuffersEnabled() {
return g_config->sync_raw_image_buffers;
}
bool TessellationEnabled() {
return g_config->tessellation_enabled;
}
bool TrophyEnabled() {
return g_config->trophy_enabled;
}
uint32_t GetTrophyDurationSeconds() {
return std::clamp(g_config->trophy_duration_seconds, 5u, 10u);
}
bool PlayGoHackEnabled() {
return g_config->playgo_hack_enabled;
}
bool SkipNoticeScreen() {
return g_config->skip_notice_screen;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool RedZoneProtectionEnabled() {
return g_config->red_zone_protection_enabled;
@@ -194,8 +153,4 @@ const Keymap& GetKeymap() {
return g_config->keymap;
}
const std::vector<std::string>& GetGuestArguments() {
return g_config->guest_arguments;
}
} // namespace Config
-27
View File
@@ -4,9 +4,7 @@
#include "common/common.h"
#include <cstddef>
#include <array>
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
@@ -28,7 +26,6 @@ enum class LogDirection { Silent, Console, File };
enum class PresentMode { Fifo, Mailbox, Immediate };
using Keymap = std::vector<std::string>;
using ControllerColor = std::array<uint8_t, 3>;
constexpr uint32_t DEFAULT_CONSOLE_LANGUAGE = 1;
constexpr uint32_t MAX_CONSOLE_LANGUAGE = 29;
@@ -47,18 +44,11 @@ struct ConfigOptions {
std::string user_name = "Kyty";
int32_t user_id = DEFAULT_USER_ID;
std::string audio_input_device;
std::optional<ControllerColor> controller_color;
uint32_t controller_speaker_volume = 50;
uint32_t controller_vibration_intensity = 100;
PresentMode present_mode = PresentMode::Mailbox;
int32_t gpu_index = -1;
bool fullscreen_enabled = false;
bool hdr_enabled = false;
bool hide_cursor_enabled = false;
bool splash_enabled = true;
bool vr_enabled = false;
bool amd_cpu_enabled = false;
bool network_connected = false;
uint32_t vblank_frequency = 60;
uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@@ -76,18 +66,12 @@ struct ConfigOptions {
bool gpu_assisted_validation_enabled = false;
bool renderdoc_enabled = false;
bool readback_linear_images = false;
bool sync_raw_image_buffers = false;
bool tessellation_enabled = false;
bool trophy_enabled = true;
uint32_t trophy_duration_seconds = 6;
bool playgo_hack_enabled = false;
bool skip_notice_screen = false;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool red_zone_protection_enabled = false;
#endif
Keymap keymap;
// Ordinary boots need a non-null argv[0]; --guest-exec supplies the caller's arguments.
std::vector<std::string> guest_arguments {""};
};
void Load(const ConfigOptions& cfg);
@@ -97,17 +81,11 @@ uint32_t GetScreenHeight();
const std::string& GetUserName();
int32_t GetUserId();
const std::string& GetAudioInputDevice();
const std::optional<ControllerColor>& GetControllerColor();
uint32_t GetControllerSpeakerVolume();
uint32_t GetControllerVibrationIntensity();
PresentMode GetPresentMode();
int32_t GetGpuIndex();
bool FullscreenEnabled();
bool HdrEnabled();
bool HideCursorEnabled();
bool VrEnabled();
bool AmdCpuEnabled();
bool IsNetworkConnected();
uint32_t GetVblankFrequency();
uint32_t GetConsoleLanguage();
bool VulkanValidationEnabled();
@@ -133,18 +111,13 @@ bool GpuAssistedValidationEnabled();
bool RenderDocEnabled();
bool ReadbackLinearImagesEnabled();
bool SyncRawImageBuffersEnabled();
bool TessellationEnabled();
bool TrophyEnabled();
uint32_t GetTrophyDurationSeconds();
bool PlayGoHackEnabled();
bool SkipNoticeScreen();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool RedZoneProtectionEnabled();
#endif
const Keymap& GetKeymap();
const std::vector<std::string>& GetGuestArguments();
} // namespace Config
+17 -140
View File
@@ -1,6 +1,5 @@
#include "common/file.h"
#include "common/archive.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/platform/sysFileIO.h"
@@ -62,8 +61,7 @@ SysFileTimeStruct DateTimeToFileTimeUtc(const DateTime& date_time) {
} // namespace
struct File::FilePrivate {
sys_file_t* f;
std::unique_ptr<ArchiveFile> archive;
sys_file_t* f;
};
File::File(): m_p(std::make_unique<FilePrivate>()) {
@@ -87,15 +85,11 @@ File::File(const std::filesystem::path& name, Mode mode): m_p(std::make_unique<F
}
bool File::IsInvalid() const {
return m_p->f == nullptr && m_p->archive == nullptr;
return m_p->f == nullptr;
}
bool File::Create(const std::filesystem::path& name) {
EXIT_IF(!IsInvalid());
if (IsArchivePath(name)) {
return false;
}
EXIT_IF(m_p->f != nullptr);
m_file_name = name;
@@ -110,16 +104,9 @@ bool File::Create(const std::filesystem::path& name) {
}
bool File::Open(const std::filesystem::path& name, Mode mode) {
EXIT_IF(!IsInvalid());
EXIT_IF(m_p->f != nullptr);
m_file_name = name;
if (IsArchivePath(name)) {
if (mode != Mode::Read) {
return false;
}
m_p->archive = OpenArchiveFile(name);
return m_p->archive != nullptr;
}
switch (mode) {
case Mode::Read: m_p->f = SysFileOpenR(name); break;
@@ -137,7 +124,7 @@ bool File::Open(const std::filesystem::path& name, Mode mode) {
}
bool File::OpenInMem(void* buf, uint32_t buf_size) {
EXIT_IF(!IsInvalid());
EXIT_IF(m_p->f != nullptr);
m_p->f = SysFileOpen(static_cast<uint8_t*>(buf), buf_size);
@@ -150,7 +137,7 @@ bool File::OpenInMem(void* buf, uint32_t buf_size) {
}
bool File::CreateInMem() {
EXIT_IF(!IsInvalid());
EXIT_IF(m_p->f != nullptr);
m_p->f = SysFileCreate();
@@ -163,7 +150,6 @@ bool File::CreateInMem() {
}
void File::Close() {
m_p->archive.reset();
if (m_p->f != nullptr) {
SysFileClose(m_p->f);
m_p->f = nullptr;
@@ -171,11 +157,7 @@ void File::Close() {
}
uint64_t File::Size() const {
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
return m_p->archive->Size();
}
EXIT_IF(m_p->f == nullptr);
if (m_p->f == nullptr) {
return 0;
@@ -185,76 +167,41 @@ uint64_t File::Size() const {
}
uint64_t File::Remaining() const {
EXIT_IF(IsInvalid());
EXIT_IF(m_p->f == nullptr);
const auto size = Size();
const auto position = Tell();
return position < size ? size - position : 0;
return Size() - Tell();
}
uint64_t File::Size(const std::filesystem::path& name) {
if (IsArchivePath(name)) {
const auto info = GetArchiveInfo(name);
return info ? info->size : 0;
}
return SysFileSize(name);
}
std::optional<File::Info> File::GetInfo(const std::filesystem::path& name) {
if (IsArchivePath(name)) {
return GetArchiveInfo(name);
}
Info info {};
return SysFileGetInfo(name, &info.is_file, &info.size) ? std::optional(info) : std::nullopt;
}
bool File::Seek(uint64_t offset) {
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
return m_p->archive->Seek(offset);
}
EXIT_IF(m_p->f == nullptr);
return SysFileSeek(*m_p->f, offset);
}
bool File::Truncate(uint64_t size) {
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
return false;
}
EXIT_IF(m_p->f == nullptr);
return SysFileTruncate(*m_p->f, size);
}
bool File::Unlink() {
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
return false;
}
EXIT_IF(m_p->f == nullptr);
return SysFileUnlink(*m_p->f, m_file_name);
}
uint64_t File::Tell() const {
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
return m_p->archive->Tell();
}
EXIT_IF(m_p->f == nullptr);
return SysFileTell(*m_p->f);
}
void File::Read(void* data, uint32_t size, uint32_t* bytes_read) {
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
m_p->archive->Read(data, size, bytes_read);
return;
}
EXIT_IF(m_p->f == nullptr);
if (m_p->f != nullptr) {
SysFileRead(data, size, *m_p->f, bytes_read);
@@ -262,14 +209,7 @@ void File::Read(void* data, uint32_t size, uint32_t* bytes_read) {
}
void File::Write(const void* data, uint32_t size, uint32_t* bytes_written) {
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
if (bytes_written != nullptr) {
*bytes_written = 0;
}
return;
}
EXIT_IF(m_p->f == nullptr);
SysFileWrite(data, size, *m_p->f, bytes_written);
}
@@ -317,41 +257,24 @@ static std::filesystem::path WithoutTrailingSeparator(
bool File::IsDirectoryExisting(const std::filesystem::path& path) {
if (IsArchivePath(path)) {
const auto info = GetArchiveInfo(path);
return info && !info->is_file;
}
return SysFileIsDirectoryExisting(WithoutTrailingSeparator(path));
}
bool File::IsFileExisting(const std::filesystem::path& name) {
if (IsArchivePath(name)) {
const auto info = GetArchiveInfo(name);
return info && info->is_file;
}
return SysFileIsFileExisting(name);
}
bool File::CreateDirectory(const std::filesystem::path& path) {
if (IsArchivePath(path)) {
return false;
}
return SysFileCreateDirectory(
WithoutTrailingSeparator(path));
}
bool File::DeleteDirectory(const std::filesystem::path& path) {
if (IsArchivePath(path)) {
return false;
}
return SysFileDeleteDirectory(
WithoutTrailingSeparator(path));
}
bool File::CreateDirectories(const std::filesystem::path& path) {
if (IsArchivePath(path)) {
return false;
}
const auto normalized_path =
WithoutTrailingSeparator(path);
@@ -374,9 +297,6 @@ bool File::CreateDirectories(const std::filesystem::path& path) {
}
bool File::DeleteDirectories(const std::filesystem::path& path) {
if (IsArchivePath(path)) {
return false;
}
const auto normalized_path =
WithoutTrailingSeparator(path);
@@ -402,18 +322,11 @@ bool File::DeleteDirectories(const std::filesystem::path& path) {
bool File::DeleteFile(
const std::filesystem::path& name) // @suppress("Member declaration not found")
{
if (IsArchivePath(name)) {
return false;
}
return SysFileDeleteFile(name);
}
bool File::Flush() {
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
return true;
}
EXIT_IF(m_p->f == nullptr);
return SysFileFlush(*m_p->f);
}
@@ -435,9 +348,6 @@ std::vector<std::byte> File::ReadWholeBuffer() {
}
DateTime File::GetLastAccessTimeUTC(const std::filesystem::path& name) {
if (IsArchivePath(name)) {
return GetLastAccessTimeUTC(GetArchiveHostPath(name));
}
SysTimeStruct t {};
SysFileToSystemTimeUtc(SysFileGetLastAccessTimeUtc(name), t);
@@ -449,9 +359,6 @@ DateTime File::GetLastAccessTimeUTC(const std::filesystem::path& name) {
}
DateTime File::GetLastWriteTimeUTC(const std::filesystem::path& name) {
if (IsArchivePath(name)) {
return GetLastWriteTimeUTC(GetArchiveHostPath(name));
}
SysTimeStruct t {};
SysFileToSystemTimeUtc(SysFileGetLastWriteTimeUtc(name), t);
@@ -466,10 +373,6 @@ void File::GetLastAccessAndWriteTimeUTC(const std::filesystem::path& name, DateT
DateTime* write) {
EXIT_IF(access == nullptr);
EXIT_IF(write == nullptr);
if (IsArchivePath(name)) {
GetLastAccessAndWriteTimeUTC(GetArchiveHostPath(name), access, write);
return;
}
SysTimeStruct at {};
SysTimeStruct wt {};
@@ -496,12 +399,7 @@ void File::GetLastAccessAndWriteTimeUTC(DateTime* access, DateTime* write) {
EXIT_IF(access == nullptr);
EXIT_IF(write == nullptr);
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
GetLastAccessAndWriteTimeUTC(m_file_name, access, write);
return;
}
EXIT_IF(m_p->f == nullptr);
if (m_p->f == nullptr) {
*access = DateTime();
@@ -531,35 +429,23 @@ void File::GetLastAccessAndWriteTimeUTC(DateTime* access, DateTime* write) {
}
bool File::SetLastAccessTimeUTC(const std::filesystem::path& name, const DateTime& dt) {
if (IsArchivePath(name)) {
return false;
}
auto f = DateTimeToFileTimeUtc(dt);
return SysFileSetLastAccessTimeUtc(name, f);
}
bool File::SetLastWriteTimeUTC(const std::filesystem::path& name, const DateTime& dt) {
if (IsArchivePath(name)) {
return false;
}
auto f = DateTimeToFileTimeUtc(dt);
return SysFileSetLastWriteTimeUtc(name, f);
}
bool File::SetLastAccessAndWriteTimeUTC(const std::filesystem::path& name, const DateTime& access,
const DateTime& write) {
if (IsArchivePath(name)) {
return false;
}
auto af = DateTimeToFileTimeUtc(access);
auto wf = DateTimeToFileTimeUtc(write);
return SysFileSetLastAccessAndWriteTimeUtc(name, af, wf);
}
std::vector<File::DirEntry> File::GetDirEntries(const std::filesystem::path& path) {
if (IsArchivePath(path)) {
return GetArchiveDirEntries(path);
}
std::vector<sys_dir_entry_t> files;
SysFileGetDents(path, files);
@@ -577,18 +463,12 @@ std::vector<File::DirEntry> File::GetDirEntries(const std::filesystem::path& pat
bool File::CopyFile(const std::filesystem::path& src,
const std::filesystem::path& dst) // @suppress("Member declaration not found")
{
if (IsArchivePath(src) || IsArchivePath(dst)) {
return false;
}
return SysFileCopyFile(src, dst);
}
bool File::RenameFile(const std::filesystem::path& src,
const std::filesystem::path& dst) // @suppress("Member declaration not found")
{
if (IsArchivePath(src) || IsArchivePath(dst)) {
return false;
}
if (IsFileExisting(dst)) {
DeleteFile(dst); // @suppress("Invalid arguments")
}
@@ -597,9 +477,6 @@ bool File::RenameFile(const std::filesystem::path& src,
}
void File::RemoveReadonly(const std::filesystem::path& name) {
if (IsArchivePath(name)) {
return;
}
SysFileRemoveReadonly(name);
}
-6
View File
@@ -7,7 +7,6 @@
#include <cstddef>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
#include <vector>
@@ -35,10 +34,6 @@ public:
std::string name;
bool is_file;
};
struct Info {
bool is_file;
uint64_t size;
};
File();
explicit File(const std::filesystem::path& name);
@@ -77,7 +72,6 @@ public:
std::vector<std::byte> ReadWholeBuffer();
static uint64_t Size(const std::filesystem::path& name);
static std::optional<Info> GetInfo(const std::filesystem::path& name);
static bool IsDirectoryExisting(const std::filesystem::path& path);
static bool IsFileExisting(const std::filesystem::path& name);
-53
View File
@@ -1,53 +0,0 @@
#include "common/localization.h"
#include <array>
#include <cstddef>
#include <nlohmann/json.hpp>
#include <string>
#include "localizationData.h"
namespace Common::Localization {
namespace {
constexpr std::array<std::string_view, 30> Locales = {
"ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT",
"ru-RU", "ko-KR", "zh-Hant", "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO",
"pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", "fr-CA", "cs-CZ",
"hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID"};
size_t LanguageIndex(int console_language) {
return console_language >= 0 && console_language < static_cast<int>(Locales.size())
? static_cast<size_t>(console_language)
: 1;
}
} // namespace
std::string_view LocaleName(int console_language) {
return Locales[LanguageIndex(console_language)];
}
std::string_view Get(int console_language, std::string_view key) {
static const auto catalogs = [] {
std::array<nlohmann::json, Locales.size()> result;
for (size_t i = 0; i < Locales.size(); ++i) {
const auto bytes = EmbeddedCatalog(Locales[i]);
result[i] = nlohmann::json::parse(bytes.begin(), bytes.end(), nullptr, false);
}
return result;
}();
for (const auto index: std::array {LanguageIndex(console_language), size_t {1}}) {
const auto& catalog = catalogs[index];
const auto found = catalog.find(key);
if (found != catalog.end() && found->is_string()) {
const auto& text = found->get_ref<const std::string&>();
if (!text.empty()) {
return text;
}
}
}
return key;
}
} // namespace Common::Localization
-13
View File
@@ -1,13 +0,0 @@
#pragma once
#include <string_view>
namespace Common::Localization {
// Console language indices follow the system configuration; invalid indices use English.
[[nodiscard]] std::string_view LocaleName(int console_language);
// Translations have static lifetime. Missing translations use English, then the supplied key.
[[nodiscard]] std::string_view Get(int console_language, std::string_view key);
} // namespace Common::Localization
+2 -3
View File
@@ -5,7 +5,6 @@
#include "common/emulatorConfig.h"
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fmt/format.h>
#include <memory>
@@ -46,8 +45,8 @@ std::shared_ptr<spdlog::logger> MakeFileLogger(std::string name
std::filesystem::create_directories(parent);
}
const bool truncate = std::getenv(Log::RelaunchedVariable) == nullptr;
auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(path.native(), truncate);
auto sink =
std::make_shared<spdlog::sinks::basic_file_sink_mt>(path.native(), true);
return MakeLogger(std::move(name), std::move(sink));
}
-4
View File
@@ -21,10 +21,6 @@ struct Lifecycle {
enum class Direction { Silent, Console, File };
// Set by LoadExec for the program it starts, which continues the log file of the one it replaces
// instead of starting it over.
inline constexpr const char* RelaunchedVariable = "KYTY_RELAUNCHED";
Direction GetDirection();
bool IsSilent();
void Write(std::string_view text);
-1
View File
@@ -45,7 +45,6 @@ bool SysFileTruncate(sys_file_t& f, uint64_t size); // NOLINT(google-runt
bool SysFileUnlink(sys_file_t& f,
const std::filesystem::path& name); // NOLINT(google-runtime-references)
uint64_t SysFileSize(const std::filesystem::path& file_name);
bool SysFileGetInfo(const std::filesystem::path& name, bool* is_file, uint64_t* size);
bool SysFileIsError(sys_file_t& f); // NOLINT(google-runtime-references)
bool SysFileIsDirectoryExisting(const std::filesystem::path& path);
bool SysFileIsFileExisting(const std::filesystem::path& name);
+89 -39
View File
@@ -54,7 +54,7 @@ struct sys_file_t {
#endif
static std::filesystem::path get_internal_name(const std::filesystem::path& name) {
return name.empty() || name.is_absolute() ? name : (std::filesystem::path(".") / name);
return name.is_absolute() ? name : (std::filesystem::path(".") / name);
}
// Pass access-pattern hints to the host.
@@ -152,29 +152,42 @@ void SysFileWrite(const void* data, uint32_t size, sys_file_t& f, uint32_t* byte
}
}
static sys_file_t* OpenHostFile(const std::filesystem::path& file_name, const char* mode,
sys_file_cache_type_t cache_type) {
auto* ret = new sys_file_t;
ret->f = fopen(get_internal_name(file_name).c_str(), mode);
ret->type = ret->f == nullptr ? SYS_FILE_ERROR : SYS_FILE_FILE;
if (ret->f != nullptr) {
// Independent handles must observe writes and truncation immediately.
setvbuf(ret->f, nullptr, _IONBF, 0);
apply_cache_hint(ret->f, cache_type);
}
return ret;
}
sys_file_t* SysFileCreate(const std::filesystem::path& file_name) {
auto* ret = OpenHostFile(file_name, "w+", SYS_FILE_CACHE_AUTO);
auto* ret = new sys_file_t;
auto real_name = get_internal_name(file_name);
auto real_name_str = real_name.string();
ret->f = fopen(real_name_str.c_str(), "w+");
if (ret->f == nullptr) {
printf("can't create file: %s\n", file_name.c_str());
printf("can't create file: %s\n", real_name_str.c_str());
}
ret->type = SYS_FILE_FILE;
return ret;
}
sys_file_t* SysFileOpenR(const std::filesystem::path& file_name, sys_file_cache_type_t cache_type) {
return OpenHostFile(file_name, "r", cache_type);
auto* ret = new sys_file_t;
ret->type = SYS_FILE_FILE;
auto internal_name = get_internal_name(file_name);
auto internal_name_str = internal_name.string();
FILE* f = fopen(internal_name_str.c_str(), "r");
if (f == nullptr) {
ret->type = SYS_FILE_ERROR;
}
apply_cache_hint(f, cache_type);
ret->f = f;
return ret;
}
sys_file_t* SysFileOpen(uint8_t* buf, uint32_t buf_size) {
@@ -202,12 +215,46 @@ sys_file_t* SysFileCreate() {
}
sys_file_t* SysFileOpenW(const std::filesystem::path& file_name, sys_file_cache_type_t cache_type) {
return OpenHostFile(file_name, "r+", cache_type);
auto* ret = new sys_file_t;
auto real_name = get_internal_name(file_name);
auto real_name_str = real_name.string();
FILE* f = fopen(real_name_str.c_str(), "r+");
if (f == nullptr) {
ret->type = SYS_FILE_ERROR;
} else {
ret->type = SYS_FILE_FILE;
}
apply_cache_hint(f, cache_type);
ret->f = f;
return ret;
}
sys_file_t* SysFileOpenRw(const std::filesystem::path& file_name,
sys_file_cache_type_t cache_type) {
return OpenHostFile(file_name, "r+", cache_type);
auto* ret = new sys_file_t;
auto real_name = get_internal_name(file_name);
auto real_name_str = real_name.string();
FILE* f = fopen(real_name_str.c_str(), "r+");
if (f == nullptr) {
ret->type = SYS_FILE_ERROR;
} else {
ret->type = SYS_FILE_FILE;
}
apply_cache_hint(f, cache_type);
ret->f = f;
return ret;
}
void SysFileClose(sys_file_t* f) {
@@ -250,21 +297,10 @@ uint64_t SysFileSize(sys_file_t& f) {
return 0;
}
bool SysFileGetInfo(const std::filesystem::path& name, bool* is_file, uint64_t* size) {
const auto path = get_internal_name(name);
struct stat info {};
if (stat(path.c_str(), &info) != 0) {
return false;
}
*is_file = !S_ISDIR(info.st_mode);
*size = *is_file ? static_cast<uint64_t>(info.st_size) : 0;
return true;
}
uint64_t SysFileSize(const std::filesystem::path& file_name) {
bool is_file;
uint64_t size = 0;
SysFileGetInfo(file_name, &is_file, &size);
sys_file_t* f = SysFileOpenR(file_name);
uint64_t size = SysFileSize(*f);
SysFileClose(f);
return size;
}
@@ -317,15 +353,29 @@ bool SysFileIsError(sys_file_t& f) {
}
bool SysFileIsDirectoryExisting(const std::filesystem::path& path) {
bool is_file;
uint64_t size;
return SysFileGetInfo(path, &is_file, &size) && !is_file;
auto real_name = get_internal_name(path);
auto real_name_str = real_name.string();
struct stat s {};
if (0 != stat(real_name_str.c_str(), &s)) {
return false;
}
return S_ISDIR(s.st_mode); // NOLINT
}
bool SysFileIsFileExisting(const std::filesystem::path& name) {
bool is_file;
uint64_t size;
return SysFileGetInfo(name, &is_file, &size) && is_file;
auto real_name = get_internal_name(name);
auto real_name_str = real_name.string();
struct stat s {};
if (0 != stat(real_name_str.c_str(), &s)) {
return false;
}
return !S_ISDIR(s.st_mode); // NOLINT
}
bool SysFileCreateDirectory(const std::filesystem::path& path) {
+20 -26
View File
@@ -266,27 +266,19 @@ uint64_t SysFileSize(sys_file_t& f) {
return 0;
}
bool SysFileGetInfo(const std::filesystem::path& name, bool* is_file, uint64_t* size) {
const bool directory_path = !name.empty() && !name.has_filename() && name != name.root_path();
WIN32_FILE_ATTRIBUTE_DATA info {};
if (GetFileAttributesExW(directory_path ? name.parent_path().c_str() : name.c_str(),
GetFileExInfoStandard, &info) == 0) {
return false;
}
const bool file = (info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
if (directory_path && file) {
return false;
}
*is_file = file;
*size = *is_file ? ((static_cast<uint64_t>(info.nFileSizeHigh) << 32u) | info.nFileSizeLow) : 0;
return true;
}
uint64_t SysFileSize(const std::filesystem::path& file_name) {
bool is_file;
uint64_t size = 0;
SysFileGetInfo(file_name, &is_file, &size);
return size;
LARGE_INTEGER s;
WIN32_FILE_ATTRIBUTE_DATA a;
auto wide = file_name.wstring();
if (GetFileAttributesExW(wide.c_str(), GetFileExInfoStandard, &a) == 0) {
return 0;
}
s.HighPart = static_cast<LONG>(a.nFileSizeHigh);
s.LowPart = a.nFileSizeLow;
return s.QuadPart;
}
bool SysFileTruncate(sys_file_t& f, uint64_t size) {
@@ -354,15 +346,17 @@ bool SysFileIsError(sys_file_t& f) {
}
bool SysFileIsDirectoryExisting(const std::filesystem::path& path) {
bool is_file;
uint64_t size;
return SysFileGetInfo(path, &is_file, &size) && !is_file;
auto wide = path.wstring();
DWORD a = GetFileAttributesW(wide.c_str());
return a != INVALID_FILE_ATTRIBUTES &&
((a & static_cast<DWORD>(FILE_ATTRIBUTE_DIRECTORY)) != 0u);
}
bool SysFileIsFileExisting(const std::filesystem::path& name) {
bool is_file;
uint64_t size;
return SysFileGetInfo(name, &is_file, &size) && is_file;
auto wide = name.wstring();
DWORD a = GetFileAttributesW(wide.c_str());
return a != INVALID_FILE_ATTRIBUTES &&
((a & static_cast<DWORD>(FILE_ATTRIBUTE_DIRECTORY)) == 0u);
}
bool SysFileCreateDirectory(const std::filesystem::path& path) {
+39
View File
@@ -2,13 +2,52 @@
#include "common/emulatorConfig.h"
#include <algorithm>
#include <common/TracyProtocol.hpp>
#include <common/TracyVersion.hpp>
#include <cstdio>
#include <tracy/Tracy.hpp>
#include <vector>
namespace {
thread_local std::vector<Profiler::ScopedBlock*> g_block_stack;
void RemoveBlock(Profiler::ScopedBlock* block) {
auto block_it = std::find(g_block_stack.rbegin(), g_block_stack.rend(), block);
if (block_it != g_block_stack.rend()) {
g_block_stack.erase(std::next(block_it).base());
}
}
} // namespace
namespace Profiler {
ScopedBlock::ScopedBlock(const tracy::SourceLocationData* source_location) {
if (tracy::ProfilerAvailable()) {
m_zone.emplace(source_location, TRACY_CALLSTACK, true);
g_block_stack.push_back(this);
}
}
ScopedBlock::~ScopedBlock() {
End();
}
void ScopedBlock::End() {
if (m_zone.has_value()) {
m_zone.reset();
RemoveBlock(this);
}
}
void EndBlock() {
if (!g_block_stack.empty()) {
g_block_stack.back()->End();
}
}
void SetThreadName(const char* name) {
if (tracy::ProfilerAvailable() && name != nullptr) {
tracy::SetThreadName(name);
+29 -4
View File
@@ -4,6 +4,7 @@
#include "common/common.h"
#include <cstdint>
#include <optional>
#include <tracy/Tracy.hpp> // IWYU pragma: export
namespace profiler::colors {
@@ -22,6 +23,22 @@ inline constexpr uint32_t DeepOrangeA200 = 0xff6e40;
namespace Profiler {
class ScopedBlock {
public:
explicit ScopedBlock(const tracy::SourceLocationData* source_location);
ScopedBlock(const ScopedBlock&) = delete;
ScopedBlock& operator=(const ScopedBlock&) = delete;
ScopedBlock(ScopedBlock&&) = delete;
ScopedBlock& operator=(ScopedBlock&&) = delete;
~ScopedBlock();
void End();
private:
std::optional<tracy::ScopedZone> m_zone;
};
void EndBlock();
void SetThreadName(const char* name);
void Initialize();
@@ -49,11 +66,19 @@ struct Lifecycle {
kyty_profiler_source_location_, line) {name, TracyFunction, TracyFile, \
static_cast<uint32_t>(line), \
KYTY_PROFILER_COLOR_OR_DEFAULT(0, __VA_ARGS__)}; \
tracy::ScopedZone KYTY_PROFILER_CONCAT(kyty_profiler_block_, line)( \
&KYTY_PROFILER_CONCAT(kyty_profiler_source_location_, line), \
TRACY_CALLSTACK, tracy::ProfilerAvailable())
Profiler::ScopedBlock KYTY_PROFILER_CONCAT(kyty_profiler_block_, line)( \
&KYTY_PROFILER_CONCAT(kyty_profiler_source_location_, line))
#define KYTY_PROFILER_FUNCTION(...) KYTY_PROFILER_BLOCK(nullptr __VA_OPT__(, ) __VA_ARGS__)
#define KYTY_PROFILER_FUNCTION(...) KYTY_PROFILER_FUNCTION_IMPL(__LINE__ __VA_OPT__(, ) __VA_ARGS__)
#define KYTY_PROFILER_FUNCTION_IMPL(line, ...) \
static constexpr tracy::SourceLocationData KYTY_PROFILER_CONCAT( \
kyty_profiler_source_location_, line) {nullptr, TracyFunction, TracyFile, \
static_cast<uint32_t>(line), \
KYTY_PROFILER_COLOR_OR_DEFAULT(0, __VA_ARGS__)}; \
Profiler::ScopedBlock KYTY_PROFILER_CONCAT(kyty_profiler_block_, line)( \
&KYTY_PROFILER_CONCAT(kyty_profiler_source_location_, line))
#define KYTY_PROFILER_END_BLOCK Profiler::EndBlock()
#define KYTY_PROFILER_THREAD(name) Profiler::SetThreadName(name)
+59 -17
View File
@@ -2,12 +2,17 @@
#include "common/assert.h"
#include <algorithm>
#include <atomic>
#include <cerrno>
#include <chrono> // IWYU pragma: keep
#include <condition_variable> // IWYU pragma: keep
#include <mutex>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS && KYTY_COMPILER == KYTY_COMPILER_CLANG
#define KYTY_WIN_CS
#endif
// macOS has no clock_nanosleep.
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS && !defined(__APPLE__)
#define KYTY_POSIX_HIGH_RES_SLEEP
@@ -18,10 +23,11 @@
#include <string>
#include <thread>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifdef KYTY_WIN_CS
#include <windows.h> // IWYU pragma: keep
// IWYU pragma: no_include <winbase.h>
constexpr DWORD KYTY_CS_SPIN_COUNT = 4000;
constexpr DWORD KYTY_CS_SPIN_COUNT = 4000;
constexpr uint64_t KYTY_SLEEP_SPIN_LIMIT_100NS = 500; // 50 us
#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002
@@ -32,6 +38,30 @@ static void SleepHighResolution100ns(uint64_t units_100ns) {
return;
}
// Keep spinning only where a kernel transition is
// likely to cost more than the requested delay; ordinary millisecond sleeps use the
// per-thread high-resolution waitable timer below.
if (units_100ns <= KYTY_SLEEP_SPIN_LIMIT_100NS) {
LARGE_INTEGER frequency {};
LARGE_INTEGER start {};
if (QueryPerformanceFrequency(&frequency) != 0 && QueryPerformanceCounter(&start) != 0 &&
frequency.QuadPart > 0) {
const auto wait_ticks =
static_cast<LONGLONG>((static_cast<long double>(units_100ns) *
static_cast<long double>(frequency.QuadPart)) /
10000000.0L);
const auto deadline = start.QuadPart + std::max<LONGLONG>(wait_ticks, 1);
LARGE_INTEGER now {};
do {
if (QueryPerformanceCounter(&now) == 0) {
break;
}
YieldProcessor();
} while (now.QuadPart < deadline);
return;
}
}
thread_local HANDLE timer = CreateWaitableTimerExW(
nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
if (timer == nullptr) {
@@ -101,13 +131,14 @@ static SleepConditionVariableCS_func_t ResolveSleepConditionVariableCS() {
#endif
#ifdef KYTY_POSIX_HIGH_RES_SLEEP
// An absolute deadline preserves the requested sleep across signal interruptions.
// Spin for very short waits; use an absolute deadline for longer waits.
static void SleepHighResolutionNanos(uint64_t nanos) {
if (nanos == 0) {
return;
}
constexpr uint64_t NANOS_PER_SEC = 1000000000;
constexpr uint64_t SPIN_LIMIT_NS = 50000; // below this a context switch dominates
timespec deadline {};
if (clock_gettime(CLOCK_MONOTONIC, &deadline) != 0) {
@@ -119,6 +150,17 @@ static void SleepHighResolutionNanos(uint64_t nanos) {
deadline.tv_sec += static_cast<time_t>(target_nsec / NANOS_PER_SEC);
deadline.tv_nsec = static_cast<long>(target_nsec % NANOS_PER_SEC);
if (nanos <= SPIN_LIMIT_NS) {
timespec now {};
do {
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
return;
}
} while (now.tv_sec < deadline.tv_sec ||
(now.tv_sec == deadline.tv_sec && now.tv_nsec < deadline.tv_nsec));
return;
}
while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &deadline, nullptr) == EINTR) {
}
}
@@ -129,7 +171,7 @@ namespace Common {
using thread_id_t = std::thread::id;
struct MutexPrivate {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifdef KYTY_WIN_CS
MutexPrivate() { InitializeCriticalSectionAndSpinCount(&m_cs, KYTY_CS_SPIN_COUNT); }
~MutexPrivate() { DeleteCriticalSection(&m_cs); }
KYTY_CLASS_NO_COPY(MutexPrivate);
@@ -140,7 +182,7 @@ struct MutexPrivate {
};
struct CondVarPrivate {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifdef KYTY_WIN_CS
CondVarPrivate() {
static auto func = ResolveInitializeConditionVariable();
EXIT_NOT_IMPLEMENTED(func == nullptr);
@@ -212,7 +254,7 @@ void Thread::Detach() {
}
void Thread::SleepMicro(uint32_t micros) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifdef KYTY_WIN_CS
SleepHighResolution100ns(static_cast<uint64_t>(micros) * 10);
#elif defined(KYTY_POSIX_HIGH_RES_SLEEP)
SleepHighResolutionNanos(static_cast<uint64_t>(micros) * 1000);
@@ -222,7 +264,7 @@ void Thread::SleepMicro(uint32_t micros) {
}
void Thread::SleepNano(uint64_t nanos) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifdef KYTY_WIN_CS
SleepHighResolution100ns((nanos + 99) / 100);
#elif defined(KYTY_POSIX_HIGH_RES_SLEEP)
SleepHighResolutionNanos(nanos);
@@ -258,7 +300,7 @@ Mutex::~Mutex() {
}
void Mutex::Lock() {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifdef KYTY_WIN_CS
EnterCriticalSection(&m_mutex->m_cs);
#else
m_mutex->m_mutex.lock();
@@ -266,7 +308,7 @@ void Mutex::Lock() {
}
void Mutex::Unlock() {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifdef KYTY_WIN_CS
LeaveCriticalSection(&m_mutex->m_cs);
#else
m_mutex->m_mutex.unlock();
@@ -274,7 +316,7 @@ void Mutex::Unlock() {
}
bool Mutex::TryLock() {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifdef KYTY_WIN_CS
return (TryEnterCriticalSection(&m_mutex->m_cs) != 0);
#else
return m_mutex->m_mutex.try_lock();
@@ -288,7 +330,7 @@ CondVar::~CondVar() {
}
void CondVar::Wait(Mutex* mutex) {
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
#ifndef KYTY_WIN_CS
std::unique_lock<std::recursive_mutex> cpp_lock(mutex->m_mutex->m_mutex, std::adopt_lock_t());
#endif
auto poll_callback = [&] {
@@ -296,7 +338,7 @@ void CondVar::Wait(Mutex* mutex) {
if (callback == nullptr) {
return;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#if defined(KYTY_WIN_CS)
LeaveCriticalSection(&mutex->m_mutex->m_cs);
callback();
EnterCriticalSection(&mutex->m_mutex->m_cs);
@@ -306,7 +348,7 @@ void CondVar::Wait(Mutex* mutex) {
cpp_lock.lock();
#endif
};
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifdef KYTY_WIN_CS
static auto func = ResolveSleepConditionVariableCS();
EXIT_NOT_IMPLEMENTED(func == nullptr);
if (g_cond_wait_poll_callback == nullptr) {
@@ -336,10 +378,10 @@ void CondVar::SetWaitPollCallback(wait_poll_func_t callback) {
bool CondVar::WaitFor(Mutex* mutex, uint32_t micros) {
bool ok = false;
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
#ifndef KYTY_WIN_CS
std::unique_lock<std::recursive_mutex> cpp_lock(mutex->m_mutex->m_mutex, std::adopt_lock_t());
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifdef KYTY_WIN_CS
static auto func = ResolveSleepConditionVariableCS();
EXIT_NOT_IMPLEMENTED(func == nullptr);
ok = !(func(&m_cond_var->m_cv, &mutex->m_mutex->m_cs, (micros < 1000 ? 1 : micros / 1000)) ==
@@ -354,7 +396,7 @@ bool CondVar::WaitFor(Mutex* mutex, uint32_t micros) {
}
void CondVar::Signal() {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifdef KYTY_WIN_CS
static auto func = ResolveWakeConditionVariable();
EXIT_NOT_IMPLEMENTED(func == nullptr);
func(&m_cond_var->m_cv);
@@ -364,7 +406,7 @@ void CondVar::Signal() {
}
void CondVar::SignalAll() {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifdef KYTY_WIN_CS
static auto func = ResolveWakeAllConditionVariable();
EXIT_NOT_IMPLEMENTED(func == nullptr);
func(&m_cond_var->m_cv);
-447
View File
@@ -1,447 +0,0 @@
#include "common/trophies.h"
#include "common/file.h"
#include "common/localization.h"
#include <algorithm>
#include <charconv>
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#include <span>
namespace Common::Trophies {
namespace {
using Json = nlohmann::json;
using Files = std::map<std::string_view, std::span<const std::byte>>;
constexpr uint64_t MaxUnlocksSize = uint64_t {1} << 20u;
constexpr uint64_t MaxPackageSize = uint64_t {1} << 30u;
uint64_t ReadBigEndian(const std::byte* data, size_t size) {
uint64_t value = 0;
for (size_t i = 0; i < size; ++i) {
value = (value << 8u) | std::to_integer<uint8_t>(data[i]);
}
return value;
}
int ReadId(const Json& value) {
if (value.is_number_integer()) {
const auto number = value.get<int64_t>();
return number >= 0 && number <= INT32_MAX ? static_cast<int>(number) : -1;
}
if (!value.is_string()) {
return -1;
}
const auto& text = value.get_ref<const std::string&>();
int id = -1;
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), id);
return error == std::errc {} && end == text.data() + text.size() && id >= 0 ? id : -1;
}
std::optional<uint64_t> ReadUnsigned(const Json& value) {
if (value.is_number_unsigned()) {
return value.get<uint64_t>();
}
if (value.is_number_integer()) {
const auto number = value.get<int64_t>();
return number >= 0 ? std::optional<uint64_t>(static_cast<uint64_t>(number)) : std::nullopt;
}
if (!value.is_string()) {
return std::nullopt;
}
const auto& text = value.get_ref<const std::string&>();
uint64_t number = 0;
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), number);
return error == std::errc {} && end == text.data() + text.size()
? std::optional<uint64_t>(number)
: std::nullopt;
}
Json ReadJson(const Files& files, std::string_view name) {
const auto entry = files.find(name);
if (entry == files.end()) {
return {};
}
return Json::parse(entry->second.begin(), entry->second.end(), nullptr, false);
}
bool ReadFiles(std::span<const std::byte> data, Files& files, uint64_t& size) {
if (data.size() < 0x40 || data.size() > MaxPackageSize ||
ReadBigEndian(data.data(), 4) != 0xb228c60a || ReadBigEndian(data.data() + 4, 4) != 1) {
return false;
}
size = ReadBigEndian(data.data() + 8, 8);
const auto count = ReadBigEndian(data.data() + 0x10, 4);
const auto toc = ReadBigEndian(data.data() + 0x14, 4);
if (size < 0x40 || size > data.size() || count > 4096 || toc > size ||
0x20 + count * 0x40 > size - toc) {
return false;
}
for (uint64_t i = 0; i < count; ++i) {
const auto* entry = data.data() + toc + 0x20 + i * 0x40;
const auto* name = reinterpret_cast<const char*>(entry);
const std::string_view filename(name, std::find(name, name + 0x20, '\0') - name);
const auto offset = ReadBigEndian(entry + 0x20, 8);
const auto length = ReadBigEndian(entry + 0x28, 8);
if (offset > size || length > size - offset) {
return false;
}
files.emplace(filename, data.subspan(offset, length));
}
return true;
}
Package ParsePackage(std::span<const std::byte> data, int console_language) {
Files files;
uint64_t size = 0;
if (!ReadFiles(data, files, size)) {
return {};
}
const auto conf = ReadJson(files, "tropconf.json");
if (!conf.is_object() || !conf.contains("trophies") || !conf["trophies"].is_array() ||
conf["trophies"].size() > 1000 || !conf.contains("defaultLanguage") ||
!conf["defaultLanguage"].is_string()) {
return {};
}
const auto locale = Localization::LocaleName(console_language);
auto meta = ReadJson(files, fmt::format("tropmeta_{}.json", locale));
if (meta.is_null()) {
meta = ReadJson(files, fmt::format("tropmeta_{}.json",
conf["defaultLanguage"].get_ref<const std::string&>()));
}
if (!meta.is_object() || !meta.contains("metadata") || !meta["metadata"].is_object()) {
return {};
}
const auto& texts = meta["metadata"];
if (!texts.contains("titleMetadata") || !texts["titleMetadata"].is_object() ||
!texts["titleMetadata"].contains("name") || !texts["titleMetadata"]["name"].is_string() ||
!texts.contains("trophyMetadata") || !texts["trophyMetadata"].is_array()) {
return {};
}
Package package;
package.title = texts["titleMetadata"]["name"].get<std::string>();
package.groups.emplace(-1, package.title);
uint64_t icon_bytes = 0;
for (const auto& definition: conf["trophies"]) {
if (!definition.is_object() || !definition.contains("id") ||
!definition.contains("grade") || !definition["grade"].is_string()) {
return {};
}
Trophy trophy;
trophy.id = ReadId(definition["id"]);
const auto grade = definition["grade"].get<std::string>();
const auto grade_index = std::string_view("PGSB").find(grade);
if (trophy.id < 0 || grade.size() != 1 || grade_index == std::string_view::npos) {
return {};
}
trophy.grade = static_cast<int>(grade_index) + 1;
if (definition.contains("groupId")) {
trophy.group_id = ReadId(definition["groupId"]);
if (trophy.group_id < 0) {
return {};
}
}
if (definition.contains("platinumTrophyId")) {
trophy.platinum_id = ReadId(definition["platinumTrophyId"]);
}
trophy.hidden = definition.contains("hidden") && definition["hidden"] == true;
trophy.has_reward = definition.contains("hasReward") && definition["hasReward"] == true;
if (definition.contains("unlockCondition")) {
const auto& condition = definition["unlockCondition"];
if (condition.is_object()) {
trophy.progressive =
condition.contains("progressive") && condition["progressive"] == true;
if (condition.contains("udsStatId")) {
trophy.uds_stat_id = ReadUnsigned(condition["udsStatId"]);
if (!trophy.uds_stat_id) {
return {};
}
}
if (condition.contains("targetValue")) {
trophy.target = ReadUnsigned(condition["targetValue"]);
}
const auto comparison = condition.find("comparator");
if (comparison != condition.end()) {
if (*comparison == "ge")
trophy.comparison = Comparison::GreaterEqual;
else if (*comparison == "gt")
trophy.comparison = Comparison::Greater;
else if (*comparison == "le")
trophy.comparison = Comparison::LessEqual;
else if (*comparison == "lt")
trophy.comparison = Comparison::Less;
}
}
}
const auto icon = files.find(fmt::format("trop{:04}.png", trophy.id));
if (icon != files.end()) {
if (icon->second.size() > size - icon_bytes) {
return {};
}
trophy.icon_png.assign(icon->second.begin(), icon->second.end());
icon_bytes += icon->second.size();
}
package.groups.try_emplace(trophy.group_id);
if (package.groups.size() > 50 ||
!package.trophies.emplace(trophy.id, std::move(trophy)).second) {
return {};
}
}
if (texts.contains("groupMetadata") && texts["groupMetadata"].is_array()) {
for (const auto& group: texts["groupMetadata"]) {
if (group.is_object() && group.contains("id") && group.contains("name") &&
group["name"].is_string()) {
const auto found = package.groups.find(ReadId(group["id"]));
if (found != package.groups.end()) {
found->second = group["name"].get<std::string>();
}
}
}
}
for (const auto& text: texts["trophyMetadata"]) {
if (!text.is_object() || !text.contains("id")) {
continue;
}
const auto found = package.trophies.find(ReadId(text["id"]));
if (found == package.trophies.end()) {
continue;
}
for (const auto& [key, field]: {std::pair {"name", &found->second.name},
{"detail", &found->second.description},
{"reward", &found->second.reward}}) {
if (text.contains(key) && text[key].is_string()) {
*field = text[key].get<std::string>();
}
}
}
return package;
}
} // namespace
Package LoadPackage(const std::filesystem::path& path, int console_language) {
File file(path, File::Mode::Read);
if (file.IsInvalid() || file.Size() > MaxPackageSize) {
return {};
}
return ParsePackage(file.ReadWholeBuffer(), console_language);
}
std::filesystem::path PackagePath(uint32_t service_label) {
return std::filesystem::path(PackageDirectory) / fmt::format("trophy{:02}.ucp", service_label);
}
std::filesystem::path UnlocksPath(const std::filesystem::path& root, std::string_view title_id,
int user_id, uint32_t service_label) {
if (title_id.empty() || !std::all_of(title_id.begin(), title_id.end(), [](char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
c == '_' || c == '-';
})) {
return {};
}
return root / "_Trophies" / title_id /
fmt::format("trophies_{}_{}.json", user_id, service_label);
}
UnlockData LoadUnlockData(const std::filesystem::path& path) {
File file(path, File::Mode::Read);
if (file.IsInvalid() || file.Size() > MaxUnlocksSize) {
return {};
}
const auto bytes = file.ReadWholeBuffer();
const auto json = Json::parse(bytes.begin(), bytes.end(), nullptr, false);
if (!json.is_object() || !json.contains("unlockedTrophies") ||
!json["unlockedTrophies"].is_array()) {
return {};
}
UnlockData unlocks;
for (const auto& value: json["unlockedTrophies"]) {
const auto id = ReadId(value);
if (id >= 0) {
unlocks.unlocked.insert(id);
}
}
if (json.contains("unlockedAt") && json["unlockedAt"].is_object()) {
for (auto it = json["unlockedAt"].begin(); it != json["unlockedAt"].end(); ++it) {
const auto id = ReadId(Json(it.key()));
if (id >= 0 && unlocks.unlocked.contains(id)) {
const auto tick = ReadUnsigned(it.value());
if (tick && *tick >= UnixEpochTick && *tick <= 315537897599999999ULL) {
unlocks.timestamps.emplace(id, *tick);
}
}
}
}
return unlocks;
}
bool SaveUnlockData(const std::filesystem::path& path, const UnlockData& unlocks) {
if (path.empty() || !File::CreateDirectories(path.parent_path())) {
return false;
}
Json timestamps = Json::object();
for (const auto& [id, tick]: unlocks.timestamps) {
if (unlocks.unlocked.contains(id)) {
timestamps[std::to_string(id)] = tick;
}
}
const auto text =
Json {{"unlockedTrophies", unlocks.unlocked}, {"unlockedAt", timestamps}}.dump();
auto temporary = path;
temporary += ".tmp";
File file;
if (text.size() > MaxUnlocksSize || !file.Create(temporary)) {
return false;
}
uint32_t written = 0;
file.Write(text.data(), static_cast<uint32_t>(text.size()), &written);
const bool complete = written == text.size() && file.Flush();
file.Close();
std::error_code error;
if (complete) {
std::filesystem::rename(temporary, path, error);
if (!error) {
return true;
}
}
std::filesystem::remove(temporary, error);
return false;
}
UdsRules LoadUdsRules(const std::filesystem::path& path) {
File file(path, File::Mode::Read);
if (file.IsInvalid() || file.Size() > MaxPackageSize) {
return {};
}
const auto bytes = file.ReadWholeBuffer();
Files files;
uint64_t size = 0;
if (!ReadFiles(bytes, files, size)) {
return {};
}
const auto definitions = ReadJson(files, "stats_definition.json");
const auto extraction = ReadJson(files, "stats_extraction.json");
if (!definitions.is_object() || !definitions.contains("statDefinitionArray") ||
!definitions["statDefinitionArray"].is_array() || !extraction.is_object() ||
!extraction.contains("statsExtractionRuleArray") ||
!extraction["statsExtractionRuleArray"].is_array()) {
return {};
}
// Only direct unsigned latest-value stats are evaluated here. Aggregated stats
// and filtered/nested extraction require UDS state and event types we do not implement.
std::map<uint64_t, bool> stats;
for (const auto& definition: definitions["statDefinitionArray"]) {
if (!definition.is_object() || !definition.contains("statId") ||
!definition.contains("aggregation") || definition["aggregation"] != "latest" ||
!definition.contains("dataType")) {
continue;
}
const auto id = ReadUnsigned(definition["statId"]);
const auto& type = definition["dataType"];
if (!id || (type != "uint64" && type != "uint32")) {
continue;
}
const uint64_t maximum = type == "uint32" ? UINT32_MAX : UINT64_MAX;
if ((definition.contains("minValue") && ReadUnsigned(definition["minValue"]) != 0) ||
(definition.contains("maxValue") && ReadUnsigned(definition["maxValue"]) != maximum)) {
continue;
}
stats.emplace(*id, type == "uint32");
}
UdsRules events;
for (const auto& rule: extraction["statsExtractionRuleArray"]) {
if (!rule.is_object() || !rule.contains("condition") || !rule.contains("action")) {
continue;
}
const auto& condition = rule["condition"];
const auto& action = rule["action"];
if (!condition.is_object() || condition.size() != 1 || !condition.contains("eventName") ||
!condition["eventName"].is_string() || !action.is_object() ||
!action.contains("input") || !action["input"].is_string() ||
!action.contains("output") || !action["output"].is_object() ||
!action["output"].contains("statId")) {
continue;
}
const auto id = ReadUnsigned(action["output"]["statId"]);
const auto& input = action["input"].get_ref<const std::string&>();
if (!id || !stats.contains(*id) || !input.starts_with("$.") || input.size() == 2 ||
input.find_first_not_of(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_", 2) !=
std::string::npos) {
continue;
}
events[condition["eventName"].get<std::string>()].push_back(
{*id, input.substr(2), stats.at(*id)});
}
return events;
}
std::filesystem::path UdsPackagePath(uint32_t service_label) {
return std::filesystem::path("sce_sys/uds") / fmt::format("uds{:02}.ucp", service_label);
}
std::vector<int> FindUdsTrophies(const Package& package, std::string_view event_name,
const std::map<std::string, UdsInteger>& properties) {
const auto rules = package.event_rules.find(std::string(event_name));
if (rules == package.event_rules.end()) {
return {};
}
std::vector<int> ids;
for (const auto& [id, trophy]: package.trophies) {
if (trophy.grade == 1 || !trophy.uds_stat_id || !trophy.target) {
continue;
}
for (const auto& rule: rules->second) {
const auto value = properties.find(rule.input);
if (rule.stat_id != *trophy.uds_stat_id || value == properties.end()) {
continue;
}
std::optional<uint64_t> number;
if (rule.is_uint32) {
if (const auto* input = std::get_if<uint32_t>(&value->second)) number = *input;
} else if (const auto* input = std::get_if<uint64_t>(&value->second)) {
number = *input;
}
if (!number) continue;
bool matched = false;
switch (trophy.comparison) {
case Comparison::GreaterEqual: matched = *number >= *trophy.target; break;
case Comparison::Greater: matched = *number > *trophy.target; break;
case Comparison::LessEqual: matched = *number <= *trophy.target; break;
case Comparison::Less: matched = *number < *trophy.target; break;
case Comparison::None: break;
}
if (matched) {
ids.push_back(id);
break;
}
}
}
return ids;
}
uint32_t Progress::Percentage() const {
const auto points = total_grade[2] * 6 + total_grade[3] * 2 + total_grade[4];
const auto earned_points = earned_grade[2] * 6 + earned_grade[3] * 2 + earned_grade[4];
return points == 0 ? 0 : earned_points * 100 / points;
}
Progress GetProgress(const Package& package, const UnlockData& unlocks, std::optional<int> group) {
Progress progress;
for (const auto& [id, trophy]: package.trophies) {
if (group && trophy.group_id != *group) {
continue;
}
++progress.total;
++progress.total_grade[trophy.grade];
if (unlocks.unlocked.contains(id)) {
++progress.earned;
++progress.earned_grade[trophy.grade];
}
}
return progress;
}
} // namespace Common::Trophies
-87
View File
@@ -1,87 +0,0 @@
#ifndef KYTY_COMMON_TROPHIES_H_
#define KYTY_COMMON_TROPHIES_H_
#include <array>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace Common::Trophies {
inline constexpr char PackageDirectory[] = "sce_sys/trophy2";
inline constexpr uint64_t UnixEpochTick = 62135596800000000ULL;
enum class Comparison { None, GreaterEqual, Greater, LessEqual, Less };
struct Trophy {
int id = 0;
int group_id = -1;
int platinum_id = -1;
int grade = 0;
std::optional<uint64_t> target;
std::optional<uint64_t> uds_stat_id;
Comparison comparison = Comparison::None;
bool progressive = false;
std::string name;
std::string description;
std::string reward;
std::vector<std::byte> icon_png;
bool hidden = false;
bool has_reward = false;
};
struct UdsRule {
uint64_t stat_id;
std::string input;
bool is_uint32;
};
using UdsRules = std::map<std::string, std::vector<UdsRule>>;
using UdsInteger = std::variant<int32_t, uint32_t, uint64_t>;
struct Package {
std::string title;
std::map<int, std::string> groups;
std::map<int, Trophy> trophies;
UdsRules event_rules;
};
struct UnlockData {
std::set<int> unlocked;
std::map<int, uint64_t> timestamps;
};
struct Progress {
std::array<uint32_t, 5> total_grade {};
std::array<uint32_t, 5> earned_grade {};
uint32_t total = 0;
uint32_t earned = 0;
[[nodiscard]] uint32_t Percentage() const;
};
[[nodiscard]] Progress GetProgress(const Package& package, const UnlockData& unlocks,
std::optional<int> group = std::nullopt);
[[nodiscard]] Package LoadPackage(const std::filesystem::path& path, int console_language);
[[nodiscard]] std::filesystem::path PackagePath(uint32_t service_label);
[[nodiscard]] std::filesystem::path UdsPackagePath(uint32_t service_label);
[[nodiscard]] UdsRules LoadUdsRules(const std::filesystem::path& path);
[[nodiscard]] std::vector<int> FindUdsTrophies(const Package& package, std::string_view event_name,
const std::map<std::string, UdsInteger>& properties);
[[nodiscard]] std::filesystem::path UnlocksPath(const std::filesystem::path& root,
std::string_view title_id, int user_id,
uint32_t service_label);
[[nodiscard]] UnlockData LoadUnlockData(const std::filesystem::path& path);
[[nodiscard]] bool SaveUnlockData(const std::filesystem::path& path, const UnlockData& unlocks);
} // namespace Common::Trophies
#endif // KYTY_COMMON_TROPHIES_H_
-62
View File
@@ -1,62 +0,0 @@
if(NOT DEFINED INPUTS OR NOT DEFINED OUTPUT)
message(FATAL_ERROR "embed_localization.cmake requires INPUTS and OUTPUT")
endif()
set(contents "// Generated from assets/localization; do not edit.\n#pragma once\n#include <span>\n#include <string_view>\n\nnamespace {\n")
set(lookup "std::span<const unsigned char> EmbeddedCatalog(std::string_view locale) {\n")
set(continuation "[89ab][0-9a-f]")
string(CONCAT utf8_character
"[0-7][0-9a-f]|(c[2-9a-f]|d[0-9a-f])${continuation}"
"|e0[ab][0-9a-f]${continuation}|e[1-9abcef]${continuation}${continuation}"
"|ed[89][0-9a-f]${continuation}|f0[9ab][0-9a-f]${continuation}${continuation}"
"|f[123]${continuation}${continuation}${continuation}|f48[0-9a-f]${continuation}${continuation}")
foreach(catalog IN LISTS INPUTS)
file(READ "${catalog}" bytes HEX)
# Validate UTF-8, excluding overlong sequences, surrogates, and values above U+10FFFF.
if(NOT bytes MATCHES "^(${utf8_character})*$")
message(FATAL_ERROR "Localization must be UTF-8: ${catalog}")
endif()
file(READ "${catalog}" json)
string(JSON type ERROR_VARIABLE error TYPE "${json}")
if(error OR NOT type STREQUAL "OBJECT")
message(FATAL_ERROR "Invalid localization object in ${catalog}: ${error}")
endif()
string(JSON count LENGTH "${json}")
if(count GREATER 0)
math(EXPR last "${count} - 1")
foreach(index RANGE ${last})
string(JSON key MEMBER "${json}" ${index})
string(JSON type TYPE "${json}" "${key}")
if(NOT type STREQUAL "STRING")
message(FATAL_ERROR "Localization value '${key}' must be a string: ${catalog}")
endif()
string(JSON text GET "${json}" "${key}")
if(key STREQUAL "" OR text STREQUAL "")
message(FATAL_ERROR "Localization keys and values must not be empty: ${catalog}")
endif()
endforeach()
endif()
get_filename_component(locale "${catalog}" NAME_WE)
if(locale STREQUAL "en-US")
string(JSON earned ERROR_VARIABLE error GET "${json}" "trophy.toast.earned")
if(error OR earned STREQUAL "")
message(FATAL_ERROR "English localization requires trophy.toast.earned: ${catalog}")
endif()
set(english_found TRUE)
endif()
string(MAKE_C_IDENTIFIER "${locale}" symbol)
string(REGEX REPLACE "(..)" "0x\\1," bytes "${bytes}")
string(APPEND contents "constexpr unsigned char catalog_${symbol}[] = {${bytes}};\n")
string(APPEND lookup "\tif (locale == \"${locale}\") return catalog_${symbol};\n")
endforeach()
if(NOT english_found)
message(FATAL_ERROR "Localization requires an en-US.json fallback")
endif()
string(APPEND contents "\n${lookup}\treturn {};\n}\n} // namespace\n")
get_filename_component(output_dir "${OUTPUT}" DIRECTORY)
file(MAKE_DIRECTORY "${output_dir}")
file(WRITE "${OUTPUT}.tmp" "${contents}")
execute_process(COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${OUTPUT}.tmp" "${OUTPUT}"
COMMAND_ERROR_IS_FATAL ANY)
file(REMOVE "${OUTPUT}.tmp")
+1 -15
View File
@@ -11,7 +11,6 @@
#include "common/subsystems.h"
#include "common/systemInfo.h"
#include "common/threads.h"
#include "graphics/presentation/systemOverlay.h"
#include "graphics/presentation/window.h"
#include "kernel/fileSystem.h"
#include "kernel/memory.h"
@@ -22,7 +21,6 @@
#include "libs/controller.h"
#include "libs/libs.h"
#include "libs/network.h"
#include "loader/additionalContent.h"
#include "loader/runtimeLinker.h"
#include "loader/systemContent.h"
#include "loader/timer.h"
@@ -140,9 +138,6 @@ static void Init(const Config::ConfigOptions& cfg, const std::filesystem::path&
Libs::LibKernel::Memory::SetFlexibleMemorySize(flexible_memory_size);
}
}
if (cfg.splash_enabled) {
Libs::Graphics::InitializeSplashScreen(param_json.parent_path());
}
// Initialization order is explicit; destruction is automatic and reversed.
subsystems.Initialize<Loader::Timer::Lifecycle>();
@@ -202,15 +197,7 @@ void Run(const RunOptions& options) {
PrintSystemInfo();
std::string title_id;
Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id);
if (title_id.empty()) {
std::string content_id;
if (Loader::SystemContentParamSfoGetString("CONTENT_ID", &content_id) &&
content_id.size() == 36 && content_id[6] == '-' && content_id[16] == '_') {
title_id = content_id.substr(7, 9);
}
}
if (!title_id.empty()) {
if (Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id) && !title_id.empty()) {
Log::WriteToConsoleAndLog(fmt::format("Title ID: {}\n", title_id));
}
@@ -226,7 +213,6 @@ void Run(const RunOptions& options) {
MountSandboxDirs();
Loader::AddContentLoad(options.addcont_dir, title_id);
auto* rt = Common::Singleton<Loader::RuntimeLinker>::Instance();
Libs::InitAll(rt->Symbols());
-2
View File
@@ -13,11 +13,9 @@ struct RunOptions {
std::filesystem::path app0_dir;
std::filesystem::path elf;
std::filesystem::path game_patch;
std::filesystem::path addcont_dir;
};
void Run(const RunOptions& options);
int LoadExec(const char* path, const char* const argv[]);
} // namespace Emulator
@@ -23,12 +23,6 @@ enum class ContextStateOperation : uint32_t {
PushClear = 3,
};
struct IndirectDrawRegisters {
uint32_t vertex_offset;
uint32_t instance_offset;
uint32_t index_offset;
};
class Pm4Execution {
public:
[[nodiscard]] bool MadeProgress() const noexcept { return m_made_progress; }
@@ -68,6 +62,7 @@ public:
void BufferInit();
void BufferFlush();
void BufferFlushAndWait();
void BufferWait();
HW::Context& GetCtx() { return m_ctx; }
HW::UserConfig& GetUcfg() { return m_ucfg; }
@@ -85,11 +80,10 @@ public:
void DrawIndex(DrawIndexArgs args);
void DrawIndexOffset(uint32_t index_offset, uint32_t index_count);
void DrawIndexAuto(DrawAutoArgs args);
void DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers,
uint32_t draw_initiator, bool indexed);
void DrawIndirect(uint32_t data_offset, uint32_t draw_initiator, bool indexed);
void DrawIndirectMulti(uint32_t data_offset, uint32_t max_count_or_count,
const volatile uint32_t* count_addr, uint32_t stride_in_bytes,
IndirectDrawRegisters registers, uint32_t draw_initiator, bool indexed);
uint32_t draw_initiator, bool indexed);
void WriteAtEndOfPipe32(uint32_t cache_policy, uint32_t event_write_dest,
uint32_t eop_event_type, uint32_t cache_action, uint32_t event_index,
uint32_t event_write_source, void* dst_gpu_addr, uint32_t value,
@@ -140,8 +140,6 @@ constexpr auto MakeShaderDispatchTable() {
g_hw_sh_func[Pm4::COMPUTE_PGM_RSRC3] = HwShSetCsRegisters;
g_hw_sh_func[Pm4::COMPUTE_PACE_ID] = HwShSetCsRegisters;
g_hw_sh_func[Pm4::SPI_GRAPHICS_SHADER_CONTROL_PS] = HwShSetRegisters;
g_hw_sh_func[Pm4::SPI_SHADER_USER_DATA_ADDR_LO_PS] = HwShSetRegisters;
g_hw_sh_func[Pm4::SPI_SHADER_USER_DATA_ADDR_HI_PS] = HwShSetRegisters;
g_hw_sh_func[Pm4::SPI_GRAPHICS_SHADER_CONTROL_GS] = HwShSetRegisters;
g_hw_sh_func[Pm4::SPI_SHADER_USER_DATA_ADDR_LO_GS] = HwShSetRegisters;
g_hw_sh_func[Pm4::SPI_SHADER_USER_DATA_ADDR_HI_GS] = HwShSetRegisters;
@@ -231,6 +229,7 @@ constexpr auto MakeCustomOpcodeDispatchTable() {
g_cp_op_custom_func[Pm4::R_WAIT_FLIP_DONE] = CpOpWaitFlipDone;
g_cp_op_custom_func[Pm4::R_PUSH_MARKER] = CpOpPushMarker;
g_cp_op_custom_func[Pm4::R_POP_MARKER] = CpOpPopMarker;
g_cp_op_custom_func[Pm4::R_ACQUIRE_MEM] = CpOpAcquireMem;
g_cp_op_custom_func[Pm4::R_FLIP] = CpOpFlip;
g_cp_op_custom_func[Pm4::R_RELEASE_MEM] = CpOpReleaseMem;
g_cp_op_custom_func[Pm4::R_CONTEXT_STATE] = CpOpContextState;
@@ -184,6 +184,7 @@ static HW::RenderControl DecodeRenderControl(uint32_t value) {
KYTY_PM4_GET(value, DB_RENDER_CONTROL, COPY_STENCIL_TO_COLOR) != 0;
r.copy_centroid = KYTY_PM4_GET(value, DB_RENDER_CONTROL, COPY_CENTROID) != 0;
r.copy_sample = KYTY_PM4_GET(value, DB_RENDER_CONTROL, COPY_SAMPLE);
EXIT_NOT_IMPLEMENTED(r.copy_depth_to_color || r.copy_stencil_to_color);
return r;
}
@@ -295,6 +296,10 @@ static void HwCtxIgnoreDisabledUserClipPlane(CommandProcessor& cp, uint32_t valu
EXIT_NOT_IMPLEMENTED(value != 0 || cp.GetCtx().GetClipControl().user_clip_planes != 0);
}
static void HwCtxIgnoreDrawPayloadControl([[maybe_unused]] uint32_t value) {}
static void HwCtxIgnoreObjprimIdControl([[maybe_unused]] uint32_t value) {}
static void HwCtxIgnorePrimitiveIdReset([[maybe_unused]] uint32_t value) {}
static void HwCtxIgnoreVtxControl([[maybe_unused]] uint32_t value) {}
@@ -450,13 +455,23 @@ KYTY_HW_CTX_PARSER(HwCtxSetAlphaToMask) {
}
KYTY_HW_CTX_PARSER(HwCtxSetDrawPayloadControl) {
cp.GetCtx().SetDrawPayloadControl(buffer[0]);
return KYTY_PM4_LEN(cmd_id) - 2u;
auto num_values = KYTY_PM4_LEN(cmd_id) - 2u;
for (uint32_t i = 0; i < num_values; i++) {
HwCtxIgnoreDrawPayloadControl(buffer[i]);
}
return num_values;
}
KYTY_HW_CTX_PARSER(HwCtxSetObjprimIdControl) {
cp.GetCtx().SetObjprimIdControl(buffer[0]);
return KYTY_PM4_LEN(cmd_id) - 2u;
auto num_values = KYTY_PM4_LEN(cmd_id) - 2u;
for (uint32_t i = 0; i < num_values; i++) {
HwCtxIgnoreObjprimIdControl(buffer[i]);
}
return num_values;
}
KYTY_HW_CTX_PARSER(HwCtxSetPrimitiveIdReset) {
@@ -1290,8 +1305,8 @@ KYTY_HW_UC_PARSER(HwUcSetGdsOaRegisters) {
KYTY_CP_OP_PARSER(CpOpAcquireMem) {
KYTY_PROFILER_FUNCTION();
EXIT_NOT_IMPLEMENTED(cmd_id != 0xc0055800 && cmd_id != 0xc0065800);
return KYTY_PM4_LEN(cmd_id) - 1;
EXIT_NOT_IMPLEMENTED(cmd_id != 0xC0055800 && cmd_id != 0xc0061050);
return (cmd_id == 0xc0061050 ? 7 : 6);
}
KYTY_CP_OP_PARSER(CpOpDispatchDirect) {
@@ -1627,12 +1642,8 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirect) {
const auto data_offset = buffer[0];
const auto draw_initiator = buffer[3];
const bool indexed = (cmd_id == 0xc0032500);
// The native indexed packet enables the high-half first-index destination in word 3.
const IndirectDrawRegisters registers {
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
indexed && (buffer[2] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
cp.DrawIndirect(data_offset, registers, draw_initiator, indexed);
cp.DrawIndirect(data_offset, draw_initiator, indexed);
return 4;
}
@@ -1650,17 +1661,13 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirectMulti) {
const auto stride_in_bytes = buffer[7];
const auto draw_initiator = buffer[8];
const bool indexed = (cmd_id == 0xc0083800);
// Multi-draw moves the first-index enable to word 4; the destinations stay in 2/3.
const IndirectDrawRegisters registers {
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
indexed && (buffer[3] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
if (count_indirect == 0) {
count_addr = nullptr;
}
cp.DrawIndirectMulti(data_offset, max_count_or_count, count_addr, stride_in_bytes,
registers, draw_initiator, indexed);
draw_initiator, indexed);
return 9;
}
@@ -1920,7 +1927,13 @@ KYTY_CP_OP_PARSER(CpOpIndirectBuffer) {
auto* indirect_buffer =
reinterpret_cast<const uint32_t*>(buffer[0] | (static_cast<uint64_t>(buffer[1]) << 32u));
const uint32_t indirect_num_dw = control & 0xfffffu;
uint32_t indirect_num_dw = control & 0xfffffu;
static std::atomic<uint32_t> indirect_log_count {0};
if (indirect_log_count.fetch_add(1) < 128) {
LOGF("\t indirect buffer: addr=0x%016" PRIx64 ", num_dw=%" PRIu32 ", control=0x%08" PRIx32
"\n",
reinterpret_cast<uint64_t>(indirect_buffer), indirect_num_dw, control);
}
if (indirect_num_dw == 0) {
return 3;
@@ -2128,23 +2141,27 @@ KYTY_CP_OP_PARSER(CpOpIndirectUcRegs) {
KYTY_CP_OP_PARSER(CpOpMarker) {
KYTY_PROFILER_FUNCTION();
// EXIT_NOT_IMPLEMENTED(cmd_id != 0xC0001000);
uint32_t id = buffer[0] & 0xfffu;
uint32_t len_dw = ((cmd_id >> 16u) & 0x3fffu);
switch (buffer[0]) {
case 0x68750004: cp.SetUserDataMarker(HW::UserSgprType::Vsharp); break;
case 0x6875000d: cp.SetUserDataMarker(HW::UserSgprType::Region); break;
case 0x68750777: {
switch (id) {
case 0x0: break;
case 0x4: cp.SetUserDataMarker(HW::UserSgprType::Vsharp); break;
case 0xd: cp.SetUserDataMarker(HW::UserSgprType::Region); break;
case 0x777: {
cp.Flip();
break;
}
case 0x68750778: {
case 0x778: {
auto* addr =
reinterpret_cast<void*>(buffer[1] | (static_cast<uint64_t>(buffer[2]) << 32u));
uint32_t value = buffer[3];
cp.Flip(addr, value);
break;
}
case 0x68750781: {
case 0x781: {
auto* addr =
reinterpret_cast<void*>(buffer[1] | (static_cast<uint64_t>(buffer[2]) << 32u));
uint32_t value = buffer[3];
@@ -2153,8 +2170,7 @@ KYTY_CP_OP_PARSER(CpOpMarker) {
cp.FlipWithInterrupt(eop_event_type, cache_action, addr, value);
break;
}
// NOP payloads are caller-owned data; only complete known marker tags execute.
default: break;
default: EXIT("unknown marker at %05" PRIx32 ": 0x%" PRIx32 "\n", num_dw - dw, id);
}
return len_dw + 1;
@@ -2250,6 +2266,7 @@ KYTY_CP_OP_PARSER(CpOpReleaseMem) {
const bool gl2_writeback = ((gcr_cntl & GcrGl2Writeback) != 0);
auto trigger_interrupt = [&]() {
bool queued = false;
switch (interrupt_selector) {
case 0x00:
case 0x03: break;
@@ -2257,10 +2274,13 @@ KYTY_CP_OP_PARSER(CpOpReleaseMem) {
case 0x02:
case 0x04:
cp.TriggerEopEventAtEndOfPipe(interrupt_context_id);
cp.BufferFlush();
queued = true;
break;
default: EXIT("unknown release_mem interrupt selector\n");
}
if (queued) {
cp.BufferFlush();
}
};
if (data_sel == 0 || interrupt_selector == 4) {
@@ -2298,9 +2318,7 @@ KYTY_CP_OP_PARSER(CpOpReleaseMem) {
cp.WriteAtEndOfPipe32(cache_policy, event_write_dest, eop_event_type, cache_action,
event_index, event_source, dst_gpu_addr, static_cast<uint32_t>(value),
interrupt_selector, interrupt_context_id);
if (interrupt_selector == 0x01 || interrupt_selector == 0x02) {
cp.BufferFlush();
}
cp.BufferFlush();
return 7;
}
@@ -3092,7 +3110,7 @@ void GraphicsInitJmpTablesCxIndirect() {
cp.GetCtx().SetTfParam(value);
};
g_hw_ctx_indirect_func[Pm4::VGT_DRAW_PAYLOAD_CNTL] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
cp.GetCtx().SetDrawPayloadControl(value);
HwCtxIgnoreDrawPayloadControl(value);
};
g_hw_ctx_indirect_func[Pm4::VGT_PRIMITIVEID_RESET] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
HwCtxIgnorePrimitiveIdReset(value);
@@ -3101,7 +3119,7 @@ void GraphicsInitJmpTablesCxIndirect() {
cp.GetCtx().SetShaderZFormat(value);
};
g_hw_ctx_indirect_func[Pm4::PA_CL_OBJPRIM_ID_CNTL] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
cp.GetCtx().SetObjprimIdControl(value);
HwCtxIgnoreObjprimIdControl(value);
};
for (auto cmd_offset = Pm4::PA_SC_CLIPRECT_RULE; cmd_offset < Pm4::PA_SC_CLIPRECT_0_TL + 8u;
@@ -3606,18 +3624,6 @@ void GraphicsInitJmpTablesShIndirect() {
g_hw_sh_indirect_func[Pm4::SPI_GRAPHICS_SHADER_CONTROL_PS] = [](KYTY_HW_SH_INDIRECT_ARGS) {
HwShIgnoreShaderRegister(cmd_offset, value);
};
for (uint32_t offset = Pm4::SPI_SHADER_USER_DATA_ADDR_LO_PS;
offset <= Pm4::SPI_SHADER_USER_DATA_ADDR_HI_PS; offset++) {
g_hw_sh_indirect_func[offset] = [](KYTY_HW_SH_INDIRECT_ARGS) {
cp.GetShCtx().SetPsUserDataAddress(cmd_offset - Pm4::SPI_SHADER_USER_DATA_ADDR_LO_PS,
value);
};
}
for (uint32_t offset = 4; offset <= 5; offset++) {
g_hw_sh_indirect_func[offset] = [](KYTY_HW_SH_INDIRECT_ARGS) {
cp.GetShCtx().SetPsAuxiliaryTableAddress(cmd_offset - 4, value);
};
}
g_hw_sh_indirect_func[Pm4::SPI_SHADER_PACE_ID_GS] = [](KYTY_HW_SH_INDIRECT_ARGS) {
HwShIgnoreShaderRegister(cmd_offset, value);
};
+8 -19
View File
@@ -10,9 +10,9 @@ namespace {
struct FormatInfo {
BufferFormat format;
uint8_t bytes_per_element;
uint8_t block_compressed_bytes_per_block;
uint8_t render_target_bytes_per_element;
uint32_t bytes_per_element;
uint32_t block_compressed_bytes_per_block;
uint32_t render_target_bytes_per_element;
bool sampled_texture;
bool uint_texture;
bool sint_texture = false;
@@ -20,17 +20,14 @@ struct FormatInfo {
constexpr FormatInfo kFormatInfo[] = {
{BufferFormat::k8UNorm, 1, 0, 1, true, false},
{BufferFormat::k8UScaled, 1, 0, 0, true, false},
{BufferFormat::k8SNorm, 1, 0, 1, true, false},
{BufferFormat::k8SNorm, 0, 0, 1, false, false},
{BufferFormat::k8UInt, 1, 0, 1, true, true},
{BufferFormat::k8SInt, 1, 0, 1, true, false, true},
{BufferFormat::k16UNorm, 2, 0, 2, true, false},
{BufferFormat::k16SNorm, 2, 0, 2, true, false},
{BufferFormat::k16UInt, 2, 0, 2, true, true},
{BufferFormat::k16SInt, 2, 0, 2, true, false, true},
{BufferFormat::k16Float, 2, 0, 2, true, false},
{BufferFormat::k8_8UNorm, 2, 0, 2, true, false},
{BufferFormat::k8_8UScaled, 2, 0, 0, true, false},
{BufferFormat::k8_8SNorm, 2, 0, 2, true, false},
{BufferFormat::k8_8UInt, 2, 0, 2, true, true},
{BufferFormat::k8_8SInt, 2, 0, 2, true, false, true},
@@ -42,10 +39,8 @@ constexpr FormatInfo kFormatInfo[] = {
{BufferFormat::k16_16UInt, 4, 0, 4, true, true},
{BufferFormat::k16_16SInt, 4, 0, 4, true, false, true},
{BufferFormat::k16_16Float, 4, 0, 4, true, false},
{BufferFormat::k11_11_10UNorm, 4, 0, 4, true, false},
{BufferFormat::k11_11_10UInt, 4, 0, 4, true, true},
{BufferFormat::k11_11_10Float, 4, 0, 4, true, false},
{BufferFormat::k10_11_11Float, 4, 0, 4, true, false},
{BufferFormat::k10_10_10_2UNorm, 4, 0, 4, true, false},
{BufferFormat::k10_10_10_2UInt, 4, 0, 4, true, true},
{BufferFormat::k8_8_8_8UNorm, 4, 0, 4, true, false},
@@ -66,13 +61,13 @@ constexpr FormatInfo kFormatInfo[] = {
{BufferFormat::k32_32_32_32UInt, 16, 0, 16, true, true},
{BufferFormat::k32_32_32_32SInt, 16, 0, 16, true, false, true},
{BufferFormat::k32_32_32_32Float, 16, 0, 16, true, false},
{BufferFormat::k8Srgb, 1, 0, 1, true, false},
{BufferFormat::k8_8Srgb, 2, 0, 2, true, false},
{BufferFormat::k8Srgb, 1, 0, 0, true, false},
{BufferFormat::k8_8Srgb, 2, 0, 0, true, false},
{BufferFormat::k8_8_8_8Srgb, 4, 0, 4, true, false},
{BufferFormat::k9_9_9_5Float, 4, 0, 0, true, false},
{BufferFormat::k5_6_5UNorm, 2, 0, 2, true, false},
{BufferFormat::k5_5_5_1UNorm, 2, 0, 2, true, false},
{BufferFormat::k1_5_5_5UNorm, 2, 0, 2, false, false},
{BufferFormat::k1_5_5_5UNorm, 0, 0, 2, false, false},
{BufferFormat::k4_4_4_4UNorm, 2, 0, 2, true, false},
{BufferFormat::kFmask8_S2_F1, 1, 0, 1, true, false},
{BufferFormat::kFmask8_S4_F1, 1, 0, 1, true, false},
@@ -236,13 +231,7 @@ TextureNumericClass SampledTextureNumericClass(BufferFormat format) {
}
BufferFormat RemapTextureFormat(BufferFormat format) {
switch (format) {
case BufferFormat::k8UScaled: return BufferFormat::k8UNorm;
case BufferFormat::k8_8UScaled: return BufferFormat::k8_8UNorm;
case BufferFormat::k11_11_10UNorm:
case BufferFormat::k11_11_10UInt: return BufferFormat::k32UInt;
default: return format;
}
return format == BufferFormat::k11_11_10UInt ? BufferFormat::k32UInt : format;
}
} // namespace Libs::Graphics::Prospero
+197 -94
View File
@@ -35,6 +35,7 @@ namespace Libs::Graphics {
static thread_local CommandProcessor* g_current_processor = nullptr;
static thread_local Pm4Execution* g_current_execution = nullptr;
static thread_local bool g_gpu_mutex_owned = false;
static thread_local bool g_gpu_thread = false;
static thread_local GuestGpu* g_gpu_state = nullptr;
@@ -53,6 +54,27 @@ struct DrawIndexedIndirectArgs {
uint32_t start_instance_location;
};
class GpuMutexLock final {
public:
explicit GpuMutexLock(Common::Mutex& mutex): m_mutex(mutex) {
if (g_gpu_mutex_owned) {
EXIT("recursive GPU mutex acquisition\n");
}
g_gpu_mutex_owned = true;
m_mutex.Lock();
}
~GpuMutexLock() {
if (!g_gpu_mutex_owned) {
EXIT("invalid GPU mutex release\n");
}
m_mutex.Unlock();
g_gpu_mutex_owned = false;
}
private:
Common::Mutex& m_mutex;
};
static bool GraphicsRunDebugDumpEnabled() {
return Config::GraphicsDebugDumpEnabled() &&
Config::GetPrintfDirection() != Config::LogDirection::Silent;
@@ -62,7 +84,6 @@ GuestGpu::GuestGpu(RenderContext& renderer): m_renderer(renderer) {
EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
GraphicsInitJmpTables();
m_gfx_cp = std::make_unique<CommandProcessor>(renderer, 0);
m_gfx_cp->Reset();
m_thread = std::jthread(ThreadRun, this);
}
@@ -139,16 +160,20 @@ void GuestGpu::Submit(std::span<const uint32_t> draw_commands,
if (draw_commands.empty()) {
return;
}
Submission submission;
GpuMutexLock lock(m_submission_mutex);
Submission submission;
submission.type = SubmissionType::Graphics;
submission.queue_id = 0;
submission.commands = draw_commands;
submission.constant_commands = constant_commands;
submission.reset_processor = m_graphics_done;
m_graphics_done = false;
Enqueue(std::move(submission));
}
void GuestGpu::SubmitCompute(uint32_t queue, std::span<const uint32_t> commands) {
EXIT_IF(commands.empty());
GpuMutexLock lock(m_submission_mutex);
EXIT_NOT_IMPLEMENTED(queue < ComputeQueueBase || queue >= ComputeQueueBase + ComputeQueueCount);
@@ -161,21 +186,22 @@ void GuestGpu::SubmitCompute(uint32_t queue, std::span<const uint32_t> commands)
}
void GuestGpu::SubmitFlipPreparation(uint64_t request_id) {
Submission submission;
GpuMutexLock lock(m_submission_mutex);
Submission submission;
submission.type = SubmissionType::FlipPreparation;
submission.queue_id = 0;
submission.reset_processor = m_graphics_done;
submission.flip_request_id = request_id;
m_graphics_done = false;
Enqueue(std::move(submission));
}
void GuestGpu::SuspendPoint() {
EXIT_IF(IsGpuThread() || CommandScheduler::InDeferredOperation());
// Do not hold a queue lock while waiting: asynchronous work may be needed to
// finish the preceding graphics frame. The first point returns immediately.
m_suspend_point_ready->acquire();
Submission submission;
submission.type = SubmissionType::SuspendPoint;
Enqueue(std::move(submission));
void GuestGpu::Done() {
GpuMutexLock lock(m_submission_mutex);
if (!IsGpuThread()) {
WaitForIdle();
}
m_graphics_done = true;
m_done_num++;
}
@@ -203,9 +229,6 @@ void CommandProcessor::Reset() {
m_context_state_pushed = false;
m_index_type_and_size = 0;
m_index_buffer_size = 0;
m_index_base_addr = 0;
m_num_instances = 1;
m_predicate_skip = false;
m_user_data_marker = HW::UserSgprType::Unknown;
m_draw_indirect_args_base_addr = 0;
m_dispatch_indirect_args_base_addr = 0;
@@ -245,6 +268,10 @@ void CommandProcessor::BufferFlush() {
GetScheduler().Flush();
}
void CommandProcessor::BufferFlushAndWait() {
GetScheduler().FlushAndWait();
}
void CommandProcessor::BufferWait() {
BufferInit();
GetScheduler().Finish();
@@ -432,7 +459,6 @@ void GuestGpu::Enqueue(Submission submission) {
}
void GuestGpu::WaitForIdle() {
EXIT_IF(IsGpuThread() || CommandScheduler::InDeferredOperation());
Common::LockGuard lock(m_queue_mutex);
while (m_processing || !m_commands.empty() || m_submission_count != 0) {
m_idle.Wait(&m_queue_mutex);
@@ -538,7 +564,11 @@ void GuestGpu::ThreadRun(void* data) {
bool GuestGpu::Process(Submission& submission) {
const bool first_slice = !submission.started;
auto& cp = GetProcessor(submission.queue_id);
auto& cp = GetProcessor(submission.queue_id);
if (first_slice && submission.reset_processor) {
cp.Reset();
}
if (first_slice) {
submission.started = true;
@@ -576,7 +606,12 @@ bool GuestGpu::Process(Submission& submission) {
}
}
if (progressed) {
if (complete) {
m_renderer.RunGarbageCollector();
}
cp.BufferFlush();
} else if (complete) {
m_renderer.RunGarbageCollector();
}
break;
}
@@ -597,21 +632,18 @@ bool GuestGpu::Process(Submission& submission) {
complete = cp.Process(submission.command_execution, submission.commands) ==
Pm4ProcessResult::Complete;
if (submission.command_execution.MadeProgress()) {
if (complete) {
m_renderer.RunGarbageCollector();
}
cp.BufferFlush();
} else if (complete) {
m_renderer.RunGarbageCollector();
}
break;
}
case SubmissionType::FlipPreparation:
cp.PrepareCpuFlip(submission.flip_request_id);
break;
case SubmissionType::SuspendPoint:
// Age caches at the frame boundary, not once per command list.
m_renderer.RunGarbageCollector();
cp.EmitGlobalBarrier();
m_renderer.GetCommandScheduler().DeferPriorityOperation(
[ready = m_suspend_point_ready] { ready->release(); });
cp.BufferFlush();
cp.Reset();
cp.PrepareCpuFlip(submission.flip_request_id);
break;
}
@@ -680,7 +712,7 @@ void CommandProcessor::ProcessPm4(Pm4Execution& execution) {
const auto opcode = (packet_header >> 8u) & 0xffu;
EXIT_NOT_IMPLEMENTED(remaining_dw > total_dw);
if (packet_header == 0x80000000u || Pm4::PacketSizeDw(packet_header) == 1u) {
if (packet_header == 0x80000000u) {
cursor.offset_dw++;
execution.m_made_progress = true;
continue;
@@ -817,7 +849,9 @@ void CommandProcessor::SetPredication(uint32_t condition, uint32_t op, uint32_t
}
} break;
case 0x03:
// The wait selector applies only to Z-pass query readiness.
if (wait_op != 0) {
BufferFlushAndWait();
}
EXIT_NOT_IMPLEMENTED(address == nullptr);
value = *reinterpret_cast<const volatile uint64_t*>(address);
break;
@@ -866,33 +900,65 @@ void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_cou
DrawIndex({.index_count = index_count, .index_addr = index_addr});
}
static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters registers,
uint32_t vertex_offset, uint32_t instance_offset,
uint32_t first_index) {
const auto write = [&](uint32_t location, uint32_t value) {
if (location == Pm4::SH_NOP) {
return;
}
EXIT_NOT_IMPLEMENTED(location >= Pm4::SH_NUM ||
g_hw_sh_indirect_func[location] == nullptr);
g_hw_sh_indirect_func[location](cp, location, value);
};
write(registers.vertex_offset, vertex_offset);
write(registers.instance_offset, instance_offset);
write(registers.index_offset, first_index);
}
void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiator, bool indexed) {
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != 2u);
EXIT_NOT_IMPLEMENTED(m_draw_indirect_args_base_addr == 0);
void CommandProcessor::DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers,
uint32_t draw_initiator, bool indexed) {
const auto args_size = indexed ? sizeof(DrawIndexedIndirectArgs) : sizeof(DrawIndirectArgs);
DrawIndirectMulti(data_offset, 1, nullptr, args_size, registers, draw_initiator, indexed);
const auto* args_addr =
reinterpret_cast<const void*>(m_draw_indirect_args_base_addr + data_offset);
if (!indexed) {
DrawIndirectArgs args {};
std::memcpy(&args, args_addr, sizeof(args));
m_num_instances = args.instance_count;
DrawIndexAuto({.vertex_count = args.vertex_count_per_instance,
.instance_count = args.instance_count,
.first_vertex = args.start_vertex_location,
.first_instance = args.start_instance_location,
.offset_source = DrawOffsetSource::IndirectArgs});
return;
}
DrawIndexedIndirectArgs args {};
std::memcpy(&args, args_addr, sizeof(args));
uint64_t index_size = 0;
switch (m_index_type_and_size) {
case 0: index_size = 2; break;
case 1: index_size = 4; break;
case 2: index_size = 1; break;
default: EXIT("unknown index_type_and_size: %u\n", m_index_type_and_size);
}
auto* index_addr = reinterpret_cast<const void*>(
m_index_base_addr + static_cast<uint64_t>(args.start_index_location) * index_size);
const uint32_t index_count =
(m_index_buffer_size != 0 ? std::min(args.index_count_per_instance, m_index_buffer_size)
: args.index_count_per_instance);
if (GraphicsRunDebugDumpEnabled() && index_count != args.index_count_per_instance) {
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1, std::memory_order_relaxed) < 64) {
LOGF("\t DrawIndexIndirect: clamped index_count from %" PRIu32 " to %" PRIu32
" using INDEX_BUFFER_SIZE\n",
args.index_count_per_instance, index_count);
}
}
m_num_instances = args.instance_count;
DrawIndex({.index_count = index_count,
.index_addr = index_addr,
.instance_count = args.instance_count,
.base_vertex = static_cast<int32_t>(args.base_vertex_location),
.first_instance = args.start_instance_location,
.offset_source = DrawOffsetSource::IndirectArgs});
}
void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_count_or_count,
const volatile uint32_t* count_addr,
uint32_t stride_in_bytes, IndirectDrawRegisters registers,
uint32_t draw_initiator, bool indexed) {
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != (indexed ? 0u : 2u));
uint32_t stride_in_bytes, uint32_t draw_initiator,
bool indexed) {
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != 2u);
EXIT_NOT_IMPLEMENTED(m_draw_indirect_args_base_addr == 0);
uint32_t draw_count = max_count_or_count;
@@ -926,8 +992,6 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
if (!indexed) {
auto* args = reinterpret_cast<const DrawIndirectArgs*>(args_addr);
PatchIndirectDrawOffsets(*this, registers, args->start_vertex_location,
args->start_instance_location, 0);
m_num_instances = args->instance_count;
DrawIndexAuto({.vertex_count = args->vertex_count_per_instance,
.instance_count = args->instance_count,
@@ -938,8 +1002,6 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
}
auto* args = reinterpret_cast<const DrawIndexedIndirectArgs*>(args_addr);
PatchIndirectDrawOffsets(*this, registers, args->base_vertex_location,
args->start_instance_location, args->start_index_location);
auto* index_addr = reinterpret_cast<const void*>(
m_index_base_addr + static_cast<uint64_t>(args->start_index_location) * index_size);
@@ -972,6 +1034,9 @@ void CommandProcessor::DispatchDirect(uint32_t thread_group_x, uint32_t thread_g
m_sh_ctx.SetCsWaveSize(Pm4::ComputeWaveSize(mode));
uint32_t frame_num = 0;
// uint32_t local_x = 1;
// uint32_t local_y = 1;
// uint32_t local_z = 1;
{
frame_num = m_renderer.GetGpu().GetFrameNum();
@@ -993,9 +1058,35 @@ void CommandProcessor::DispatchDirect(uint32_t thread_group_x, uint32_t thread_g
}
}
const auto& cs = m_sh_ctx.GetCs().cs_regs;
// local_x = std::max(cs.num_thread_x, 1u);
// local_y = std::max(cs.num_thread_y, 1u);
// local_z = std::max(cs.num_thread_z, 1u);
m_renderer.GetRenderExecutor().DispatchDirect(m_submit_id, CurrentBuffer(), thread_group_x,
thread_group_y, thread_group_z, mode, IsAsyncComputeQueue());
thread_group_y, thread_group_z, mode);
}
/*constexpr uint32_t DispatchInitiatorUseThreadDimensions = 1u << 5u;
auto group_count = [](uint32_t threads, uint32_t group_size) {
return (threads == 0
? 0u
: (threads + std::max(group_size, 1u) - 1u) / std::max(group_size, 1u));
};
auto groups_x = thread_group_x;
auto groups_y = thread_group_y;
auto groups_z = thread_group_z;
if ((mode & DispatchInitiatorUseThreadDimensions) != 0) {
groups_x = group_count(thread_group_x, local_x);
groups_y = group_count(thread_group_y, local_y);
groups_z = group_count(thread_group_z, local_z);
}
const uint64_t invocations =
static_cast<uint64_t>(groups_x) * groups_y * groups_z * local_x * local_y * local_z;
if (invocations != 0) {
BufferFlushAndWait();
}*/
}
void CommandProcessor::DispatchIndirect(uint64_t args_addr, uint32_t mode) {
@@ -1006,8 +1097,7 @@ void CommandProcessor::DispatchIndirect(uint64_t args_addr, uint32_t mode) {
return;
}
m_sh_ctx.SetCsWaveSize(Pm4::ComputeWaveSize(mode));
m_renderer.GetRenderExecutor().DispatchIndirect(m_submit_id, CurrentBuffer(), args_addr, mode,
IsAsyncComputeQueue());
m_renderer.GetRenderExecutor().DispatchIndirect(m_submit_id, CurrentBuffer(), args_addr, mode);
}
void CommandProcessor::DrawIndexAuto(DrawAutoArgs args) {
@@ -1063,7 +1153,8 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
case 0x03: with_interrupt = false; break;
case 0x01:
if (!IsAsyncComputeQueue()) {
TriggerEopEventAtEndOfPipe(interrupt_context_id);
Sync::TriggerEopEventAtEndOfPipe(command, m_interrupt_event_id,
interrupt_context_id);
return;
}
with_interrupt = true;
@@ -1072,15 +1163,24 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
default: EXIT("unknown interrupt_selector\n");
}
auto write32 = [&] {
auto write32 = [&](bool with_writeback) {
auto* dst = static_cast<uint32_t*>(dst_gpu_addr);
auto data = static_cast<uint32_t>(value);
EXIT_IF(dst == nullptr);
(void)command.Handle();
std::memcpy(dst, &data, sizeof(data));
if (with_interrupt) {
TriggerEopEventAtEndOfPipe(interrupt_context_id);
if (with_writeback) {
Sync::WriteAtEndOfPipeWithInterruptWriteBack32(m_submit_id, command, dst,
data, m_interrupt_event_id,
interrupt_context_id);
} else {
Sync::WriteAtEndOfPipeWithInterrupt32(m_submit_id, command, dst, data,
m_interrupt_event_id, interrupt_context_id);
}
} else if (with_writeback) {
Sync::WriteAtEndOfPipeWithWriteBack32(m_submit_id, command, dst, data);
} else {
Sync::WriteAtEndOfPipe32(m_submit_id, command, dst, data);
}
};
@@ -1092,13 +1192,15 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
SynchronizeGpu();
Sync::ReadGds(*m_renderer.GetBufferCache().GetGdsBuffer(), dst, value & 0xffffu,
value >> 16u);
Sync::WriteAtEndOfPipeGds32(m_submit_id, command, dst, value & 0xffffu,
value >> 16u);
if (with_interrupt) {
m_renderer.TriggerInterrupt(m_interrupt_event_id, interrupt_context_id);
}
return;
}
} else if (eop_event_type == 0x04 && cache_action == 0x00 && event_index == 0x05) {
write32();
write32(false);
return;
}
break;
@@ -1107,8 +1209,8 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
if constexpr (sizeof(T) == sizeof(uint32_t)) {
if (event_write_source == 0x02 && eop_event_type == 0x2f && event_index == 0x06) {
switch (cache_action) {
case 0x00:
case 0x38: write32(); return;
case 0x00: write32(false); return;
case 0x38: write32(true); return;
default: break;
}
}
@@ -1116,14 +1218,25 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
if (event_write_source == 0x04) {
value = Sync::ReadReferenceClock();
}
auto write64 = [&] {
auto write64 = [&](bool with_writeback) {
auto* dst = static_cast<uint64_t*>(dst_gpu_addr);
EXIT_IF(dst == nullptr);
(void)command.Handle();
std::memcpy(dst, &value, sizeof(value));
if (with_interrupt) {
TriggerEopEventAtEndOfPipe(interrupt_context_id);
if (with_writeback) {
Sync::WriteAtEndOfPipeWithInterruptWriteBack64(
m_submit_id, command, dst, value, m_interrupt_event_id,
interrupt_context_id);
} else {
Sync::WriteAtEndOfPipeWithInterrupt64(m_submit_id, command, dst,
value, m_interrupt_event_id,
interrupt_context_id);
}
} else if (with_writeback) {
Sync::WriteAtEndOfPipeWithWriteBack64(m_submit_id, command, dst,
value);
} else {
Sync::WriteAtEndOfPipe64(m_submit_id, command, dst, value);
}
};
@@ -1132,7 +1245,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
switch (eop_event_type) {
case 0x04:
if (event_index == 0x05) {
write64();
write64(false);
return;
}
break;
@@ -1140,7 +1253,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
case 0x28:
case 0x2f:
if (event_index == 0x00) {
write64();
write64(false);
return;
}
break;
@@ -1148,7 +1261,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
case 0x2d:
case 0x30:
if (event_index == 0x00 && !with_interrupt) {
write64();
write64(false);
return;
}
break;
@@ -1163,20 +1276,20 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
if (((eop_event_type == 0x04 || eop_event_type == 0x28) &&
event_index == 0x05) ||
(event_index == 0x00)) {
write64();
write64(true);
return;
}
break;
case 0x2b:
case 0x2d:
if (event_index == 0x00 && !with_interrupt) {
write64();
write64(true);
return;
}
break;
case 0x2f:
if (event_index == 0x06 && !with_interrupt) {
write64();
write64(true);
return;
}
break;
@@ -1185,7 +1298,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
break;
case 0x3b:
if (eop_event_type == 0x04 && event_index == 0x05 && with_interrupt) {
write64();
write64(true);
return;
}
break;
@@ -1238,11 +1351,7 @@ void CommandProcessor::EmitGlobalBarrier() {
}
void CommandProcessor::TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id) {
(void)CurrentBuffer().Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, event_id = m_interrupt_event_id, interrupt_context_id] {
renderer.TriggerInterrupt(event_id, interrupt_context_id);
});
Sync::TriggerEopEventAtEndOfPipe(CurrentBuffer(), m_interrupt_event_id, interrupt_context_id);
}
void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
@@ -1332,9 +1441,8 @@ void CommandProcessor::Flip() {
auto& command = CurrentBuffer();
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
(void)command.Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, request] { renderer.GetVideoOut().CompleteFlip(request); });
Sync::WriteAtEndOfPipeOnlyFlip(m_submit_id, command, m_flip.handle, m_flip.index,
m_flip.flip_mode, m_flip.flip_arg, request);
GetScheduler().Flush();
}
@@ -1348,13 +1456,12 @@ void CommandProcessor::Flip(void* dst_gpu_addr, uint32_t value) {
reinterpret_cast<uint64_t>(dst_gpu_addr), value);
}
EXIT_IF(dst_gpu_addr == nullptr);
std::memcpy(dst_gpu_addr, &value, sizeof(value));
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
(void)command.Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, request] { renderer.GetVideoOut().CompleteFlip(request); });
Sync::WriteAtEndOfPipeWithFlip32(m_submit_id, command, static_cast<uint32_t*>(dst_gpu_addr),
value, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg, request);
GetScheduler().Flush();
}
@@ -1374,16 +1481,12 @@ void CommandProcessor::FlipWithInterrupt(uint32_t eop_event_type, uint32_t cache
if (eop_event_type != 0x00000004 || cache_action != 0x00000038) {
EXIT("unknown event type\n");
}
EXIT_IF(dst_gpu_addr == nullptr);
std::memcpy(dst_gpu_addr, &value, sizeof(value));
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
(void)command.Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, event_id = m_interrupt_event_id, request] {
renderer.GetVideoOut().CompleteFlip(request);
renderer.TriggerInterrupt(event_id, 0);
});
Sync::WriteAtEndOfPipeWithInterruptWriteBackFlip32(
m_submit_id, command, static_cast<uint32_t*>(dst_gpu_addr), value, m_flip.handle,
m_flip.index, m_flip.flip_mode, m_flip.flip_arg, request, m_interrupt_event_id);
GetScheduler().Flush();
}
+10 -13
View File
@@ -13,7 +13,6 @@
#include <deque>
#include <memory>
#include <mutex>
#include <semaphore>
#include <span>
#include <thread>
@@ -33,14 +32,11 @@ public:
void SendCommandSync(Common::UniqueFunction<void>&& command);
// Submitted command memory is borrowed and must remain valid until GPU execution completes.
void Submit(std::span<const uint32_t> draw_commands,
std::span<const uint32_t> constant_commands);
void SubmitCompute(uint32_t queue, std::span<const uint32_t> commands);
void SubmitFlipPreparation(uint64_t request_id);
// Insert an ordered graphics drain; only a previous suspend point can block the caller.
void SuspendPoint();
// Wait for guest command processing, including all compute queues (not native GPU completion).
void WaitForIdle();
void Submit(std::span<const uint32_t> draw_commands,
std::span<const uint32_t> constant_commands);
void SubmitCompute(uint32_t queue, std::span<const uint32_t> commands);
void SubmitFlipPreparation(uint64_t request_id);
void Done();
[[nodiscard]] int GetFrameNum() const;
[[nodiscard]] static bool IsGpuThread() noexcept;
@@ -52,7 +48,7 @@ private:
static constexpr uint32_t ComputeQueueBase = 0x20;
static constexpr uint32_t QueueCount = 1 + ComputeQueueCount;
enum class SubmissionType { Graphics, Compute, FlipPreparation, SuspendPoint };
enum class SubmissionType { Graphics, Compute, FlipPreparation };
struct Submission {
SubmissionType type = SubmissionType::Graphics;
@@ -61,6 +57,7 @@ private:
std::span<const uint32_t> constant_commands;
Pm4Execution command_execution;
Pm4Execution constant_execution;
bool reset_processor = false;
bool started = false;
bool command_complete = false;
bool constant_complete = false;
@@ -69,12 +66,14 @@ private:
};
void Enqueue(Submission submission);
void WaitForIdle();
void ProcessCommands();
bool Process(Submission& submission);
static void ThreadRun(void* data);
CommandProcessor& GetProcessor(uint32_t queue_id);
RenderContext& m_renderer;
Common::Mutex m_submission_mutex;
Common::Mutex m_queue_mutex;
std::mutex m_shutdown_mutex;
Common::CondVar m_work_available;
@@ -85,12 +84,10 @@ private:
uint32_t m_next_queue = 0;
uint32_t m_submission_count = 0;
bool m_processing = false;
bool m_graphics_done = true;
bool m_accepting = true;
bool m_stopping = false;
bool m_shutdown_complete = false;
// Completion callbacks can outlive GuestGpu during renderer shutdown.
std::shared_ptr<std::binary_semaphore> m_suspend_point_ready =
std::make_shared<std::binary_semaphore>(1);
std::unique_ptr<CommandProcessor> m_gfx_cp;
std::array<std::unique_ptr<CommandProcessor>, ComputeQueueCount> m_compute_cp;
-17
View File
@@ -458,9 +458,6 @@ struct PsShaderResource2 {
struct PsStageRegisters {
uint64_t data_addr = 0;
uint64_t user_data_addr = 0;
// Hades II uses PS offsets 4:5 for a byte-addressed descriptor table.
uint64_t auxiliary_table_addr = 0;
PsShaderResource1 rsrc1;
PsShaderResource2 rsrc2;
};
@@ -556,8 +553,6 @@ struct ShaderRegisters {
uint32_t m_vgtGsMaxVertOut = 0;
uint32_t m_vgtGsOutPrimType = 0;
uint32_t m_vgtPrimitiveIdEn = 0;
uint32_t m_vgtDrawPayloadCntl = 0;
uint32_t m_paClObjprimIdCntl = 0;
uint32_t m_vgtReuseOff = 0;
uint32_t m_vgtTessDistribution = 0;
uint32_t m_vgtLsHsConfig = 0;
@@ -681,10 +676,6 @@ public:
void SetRenderTargetMask(uint32_t mask) { m_render_target_mask = mask; }
[[nodiscard]] uint32_t GetRenderTargetMask() const { return m_render_target_mask; }
[[nodiscard]] bool HasObjectPrimitiveIdOutput() const {
return (m_sh_regs.m_vgtDrawPayloadCntl & 1u) != 0 &&
(m_render_target_mask & 0xf0000000u) != 0;
}
void SetShaderStages(uint32_t flags) { m_shader_stages = flags; }
[[nodiscard]] uint32_t GetShaderStages() const { return m_shader_stages; }
@@ -883,8 +874,6 @@ public:
void SetGsMaxVertOut(uint32_t value) { m_sh_regs.m_vgtGsMaxVertOut = value; }
void SetGsOutPrimType(uint32_t value) { m_sh_regs.m_vgtGsOutPrimType = value; }
void SetPrimitiveIdEn(uint32_t value) { m_sh_regs.m_vgtPrimitiveIdEn = value; }
void SetDrawPayloadControl(uint32_t value) { m_sh_regs.m_vgtDrawPayloadCntl = value; }
void SetObjprimIdControl(uint32_t value) { m_sh_regs.m_paClObjprimIdCntl = value; }
void SetReuseOff(uint32_t value) { m_sh_regs.m_vgtReuseOff = value; }
void SetTessDistribution(uint32_t value) { m_sh_regs.m_vgtTessDistribution = value; }
void SetLsHsConfig(uint32_t value) { m_sh_regs.m_vgtLsHsConfig = value; }
@@ -1028,12 +1017,6 @@ public:
void SetGsShaderResource2(const GsShaderResource2& rsrc2) { m_vs.gs_regs.rsrc2 = rsrc2; }
void SetPsShaderBase(uint64_t addr) { m_ps.ps_regs.data_addr = addr; }
void SetPsUserDataAddress(uint32_t word, uint32_t value) {
SetUserDataAddressWord(m_ps.ps_regs.user_data_addr, word, value);
}
void SetPsAuxiliaryTableAddress(uint32_t word, uint32_t value) {
SetUserDataAddressWord(m_ps.ps_regs.auxiliary_table_addr, word, value);
}
void SetPsShaderResource1(const PsShaderResource1& rsrc1) { m_ps.ps_regs.rsrc1 = rsrc1; }
void SetPsShaderResource2(const PsShaderResource2& rsrc2) { m_ps.ps_regs.rsrc2 = rsrc2; }
+4 -3
View File
@@ -23,6 +23,7 @@ constexpr auto MakeRegisterNames() {
names[R_DISPATCH_RESET] = "R_DISPATCH_RESET";
names[R_PUSH_MARKER] = "R_PUSH_MARKER";
names[R_POP_MARKER] = "R_POP_MARKER";
names[R_ACQUIRE_MEM] = "R_ACQUIRE_MEM";
names[R_WRITE_DATA] = "R_WRITE_DATA";
names[R_FLIP] = "R_FLIP";
names[R_RELEASE_MEM] = "R_RELEASE_MEM";
@@ -119,15 +120,15 @@ void DumpPm4PacketStream(Common::File* file, const uint32_t* cmd_buffer, uint32_
uint32_t len = 0;
const auto packet_type = static_cast<PacketType>(cmd_id >> 30u);
EXIT_NOT_IMPLEMENTED(dw < 2 && packet_type != PacketType::Type2 &&
PacketSizeDw(cmd_id) != 1u);
// Type-2 packets are header-only padding; every other packet type requires a body.
EXIT_NOT_IMPLEMENTED(dw < 2 && packet_type != PacketType::Type2);
switch (packet_type) {
case PacketType::Type3: {
const bool sh_gx = (cmd_id & 0x2u) == 0;
const uint8_t op = ((cmd_id >> 8u) & 0xffu);
const auto r = KYTY_PM4_R(cmd_id);
len = PacketSizeDw(cmd_id) - 1u;
len = ((cmd_id >> 16u) & 0x3fffu) + 1;
EXIT_NOT_IMPLEMENTED(len >= dw);
+1 -6
View File
@@ -19,10 +19,6 @@ class File;
namespace Libs::Graphics::Pm4 {
constexpr uint32_t PacketSizeDw(uint32_t header) {
return (header & 0x3fffff00u) == 0x3fff1000u ? 1u : KYTY_PM4_LEN(header);
}
constexpr uint32_t IT_NOP = 0x10;
constexpr uint32_t IT_SET_BASE = 0x11;
constexpr uint32_t IT_CLEAR_STATE = 0x12;
@@ -86,6 +82,7 @@ constexpr uint32_t R_WAIT_FLIP_DONE = 0x06;
constexpr uint32_t R_DISPATCH_RESET = 0x09;
constexpr uint32_t R_PUSH_MARKER = 0x0B;
constexpr uint32_t R_POP_MARKER = 0x0C;
constexpr uint32_t R_ACQUIRE_MEM = 0x14;
constexpr uint32_t R_WRITE_DATA = 0x15;
constexpr uint32_t R_FLIP = 0x17;
constexpr uint32_t R_RELEASE_MEM = 0x18;
@@ -700,8 +697,6 @@ constexpr uint32_t CX_NUM = 0x3FF + 1;
/* Shader registers */
constexpr uint32_t SPI_SHADER_USER_DATA_ADDR_LO_PS = 0x2;
constexpr uint32_t SPI_SHADER_USER_DATA_ADDR_HI_PS = 0x3;
constexpr uint32_t SPI_SHADER_PACE_ID_PS = 0x6;
constexpr uint32_t SPI_GRAPHICS_SHADER_CONTROL_PS = 0x7;
constexpr uint32_t SPI_SHADER_PGM_LO_PS = 0x8;
+2 -2
View File
@@ -299,8 +299,8 @@ bool TileGetTextureBlockLayout(Prospero::BufferFormat format, Prospero::TileMode
if (!TileGetTextureElementLayout(format, element)) {
return false;
}
if (family == TileBlockFamily::Depth64KB &&
(element.texel_width != 1 || element.texel_height != 1)) {
if ((family == TileBlockFamily::Depth64KB || family == TileBlockFamily::RenderTarget64KB) &&
Prospero::RenderTargetBytesPerElement(format) != element.bytes) {
return false;
}
-2
View File
@@ -29,11 +29,9 @@ struct GraphicContext {
bool memory_budget_ext_enabled = false;
bool compute_subgroup_size_control_enabled = false;
bool sample_rate_shading_enabled = false;
bool shader_image_int64_atomics_enabled = false;
// bool fp64_denorm_preserve = false; // Temporarily disabled.
bool attachment_feedback_loop_enabled = false;
bool provoking_vertex_last_enabled = false;
bool maintenance8_enabled = false;
bool supports_block_texel_view = false;
bool mesh_shader_enabled = false;
vk::PhysicalDeviceMeshShaderPropertiesEXT mesh_shader_properties = {};
-15
View File
@@ -66,18 +66,6 @@ RegionManager* MemoryTracker::GetOrCreateRegion(uint64_t index) {
return ptr;
}
void MemoryTracker::QueueBdaSync(uint64_t vaddr, uint64_t size) {
const auto begin = Common::AlignDown(vaddr, TRACKER_PAGE_SIZE);
const auto end = Common::AlignUp(vaddr + size, TRACKER_PAGE_SIZE);
std::lock_guard lock(m_bda_sync_mutex);
m_bda_sync.Add(begin, end - begin);
}
RangeSet MemoryTracker::TakeBdaSync() {
std::lock_guard lock(m_bda_sync_mutex);
return std::exchange(m_bda_sync, RangeSet {});
}
bool MemoryTracker::IsRegionCpuModified(uint64_t vaddr, uint64_t size) {
CheckNotInUploadCallback();
return Iterate<true>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
@@ -100,7 +88,6 @@ void MemoryTracker::MarkRegionAsCpuModified(uint64_t vaddr, uint64_t size) {
std::scoped_lock lock(manager->lock);
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
});
QueueBdaSync(vaddr, size);
}
void MemoryTracker::MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size) {
@@ -141,8 +128,6 @@ void MemoryTracker::UntrackMemory(uint64_t vaddr, uint64_t size) {
Iterate<false>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
});
locks.clear();
QueueBdaSync(vaddr, size);
}
} // namespace Libs::Graphics
+2 -8
View File
@@ -29,10 +29,6 @@ public:
void MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
void UnmarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
void UntrackMemory(uint64_t vaddr, uint64_t size);
// Queue after changing ownership; a concurrent drain must not consume an event
// before the dirty pages it describes become visible.
void QueueBdaSync(uint64_t vaddr, uint64_t size);
[[nodiscard]] RangeSet TakeBdaSync();
// Removes protection from a range and flushes GPU-owned data when required.
template <typename Flush>
void InvalidateRegion(uint64_t vaddr, uint64_t size, Flush&& on_flush) noexcept {
@@ -55,7 +51,6 @@ public:
on_flush();
}
});
QueueBdaSync(vaddr, size);
}
#if KYTY_BUILD == KYTY_BUILD_DEBUG
void ValidateGpuDirtyPages(const RangeSet& dirty, uint64_t vaddr, uint64_t size,
@@ -87,8 +82,9 @@ public:
static_assert(std::is_nothrow_invocable_v<RangeFunc&, uint64_t, uint64_t>);
static_assert(std::is_nothrow_invocable_v<UploadFunc&>);
CheckNotInUploadCallback();
Iterate<true>(vaddr, size, [](RegionManager*, uint64_t, uint64_t) {});
const auto* previous_upload_owner = std::exchange(s_upload_owner, this);
Iterate<true>(vaddr, size, [&](RegionManager* manager, uint64_t offset, uint64_t bytes) {
Iterate<false>(vaddr, size, [&](RegionManager* manager, uint64_t offset, uint64_t bytes) {
manager->lock.lock();
manager->ForEachModifiedRange<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset,
bytes, range_func);
@@ -154,8 +150,6 @@ private:
std::unique_ptr<std::atomic<RegionManager*>[]> m_regions;
std::vector<std::unique_ptr<RegionManager>> m_region_storage;
std::mutex m_region_mutex;
std::mutex m_bda_sync_mutex;
RangeSet m_bda_sync;
PageManager& m_page_manager;
};
+1 -13
View File
@@ -15,12 +15,6 @@
#include <mutex>
#include <vector>
#if defined(_MSC_VER)
#include <intrin.h>
#elif defined(__x86_64__)
#include <xmmintrin.h>
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifndef NOMINMAX
#define NOMINMAX
@@ -68,13 +62,7 @@ class SpinGuard final {
public:
explicit SpinGuard(std::atomic_flag& lock): m_lock(lock) {
while (m_lock.test_and_set(std::memory_order_acquire)) {
#if defined(__x86_64__) || defined(_M_X64)
_mm_pause();
#elif defined(_M_ARM64)
__yield();
#elif defined(__aarch64__)
asm volatile("yield");
#endif
std::atomic_signal_fence(std::memory_order_seq_cst);
}
}
~SpinGuard() { m_lock.clear(std::memory_order_release); }
+3 -8
View File
@@ -3,7 +3,6 @@
#include "common/bitArray.h"
#include "common/common.h"
#include "kernel/memory.h"
#include <compare>
@@ -11,8 +10,7 @@ namespace Libs::Graphics {
constexpr uint64_t TRACKER_PAGE_SIZE = 4ull * 1024ull;
constexpr uint64_t TRACKER_REGION_SIZE = 4ull * 1024ull * 1024ull;
constexpr uint64_t TRACKER_ADDRESS_SIZE = 1ull << 44u;
constexpr uint64_t LOWER_ADDRESS_SIZE = 1ull << 40u;
constexpr uint64_t TRACKER_ADDRESS_SIZE = 1ull << 40u;
constexpr size_t TRACKER_REGION_PAGES = TRACKER_REGION_SIZE / TRACKER_PAGE_SIZE;
struct GuestRange {
@@ -21,11 +19,8 @@ struct GuestRange {
[[nodiscard]] constexpr bool Empty() const noexcept { return address == 0 && size == 0; }
[[nodiscard]] constexpr bool Valid() const noexcept {
if (address == 0 || size == 0) return false;
if (address < LOWER_ADDRESS_SIZE) return size <= LOWER_ADDRESS_SIZE - address;
constexpr auto base = LibKernel::Memory::kExtendedMemoryBase;
constexpr auto length = LibKernel::Memory::kExtendedMemorySize;
return address >= base && address - base < length && size <= length - (address - base);
return address != 0 && size != 0 && address < TRACKER_ADDRESS_SIZE &&
size <= TRACKER_ADDRESS_SIZE - address;
}
[[nodiscard]] constexpr bool ValidOrEmpty() const noexcept { return Empty() || Valid(); }
[[nodiscard]] constexpr uint64_t End() const noexcept { return address + size; }
+13 -22
View File
@@ -9,12 +9,6 @@
#include <mutex>
#include <utility>
#if defined(_MSC_VER)
#include <intrin.h>
#elif defined(__x86_64__)
#include <xmmintrin.h>
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifndef NOMINMAX
#define NOMINMAX
@@ -36,20 +30,21 @@ class TrackingSpinLock final {
public:
void lock() noexcept {
const auto thread = CurrentThread();
if (m_owner.load(std::memory_order_relaxed) == thread) {
EXIT("recursive region tracking lock\n");
}
while (m_lock.test_and_set(std::memory_order_acquire)) {
EXIT_NOT_IMPLEMENTED(m_owner.load(std::memory_order_relaxed) == thread);
#if defined(__x86_64__) || defined(_M_X64)
_mm_pause();
#elif defined(_M_ARM64)
__yield();
#elif defined(__aarch64__)
asm volatile("yield");
#endif
if (m_owner.load(std::memory_order_relaxed) == thread) {
EXIT("recursive region tracking lock while contended\n");
}
std::atomic_signal_fence(std::memory_order_seq_cst);
}
m_owner.store(thread, std::memory_order_relaxed);
}
void unlock() noexcept {
EXIT_NOT_IMPLEMENTED(m_owner.load(std::memory_order_relaxed) != CurrentThread());
if (m_owner.load(std::memory_order_relaxed) != CurrentThread()) {
EXIT("region tracking lock released by non-owner\n");
}
m_owner.store(0, std::memory_order_relaxed);
m_lock.clear(std::memory_order_release);
}
@@ -60,9 +55,7 @@ private:
return GetCurrentThreadId();
#elif defined(__APPLE__)
// mach thread port is a nonzero per-thread id (0 is the "no owner" sentinel).
static thread_local const uint32_t tid =
static_cast<uint32_t>(pthread_mach_thread_np(pthread_self()));
return tid;
return static_cast<uint32_t>(pthread_mach_thread_np(pthread_self()));
#elif defined(__linux__)
static thread_local const uint32_t tid = static_cast<uint32_t>(::syscall(SYS_gettid));
return tid;
@@ -95,7 +88,8 @@ public:
template <DirtySource source>
[[nodiscard]] bool IsModified(uint64_t offset, uint64_t size) const {
const auto [start, end] = GetPageRange(m_cpu_addr + offset, size);
return GetBits<source>().FirstRangeFrom(start).first < end;
const auto& bits = GetBits<source>();
return RegionBits(bits, start, end).Any();
}
template <DirtySource source, bool enable>
@@ -128,9 +122,6 @@ public:
void ForEachModifiedRange(uint64_t vaddr, uint64_t size, Func&& func) {
const auto [start, end] = GetPageRange(vaddr, size);
auto& bits = GetBits<source>();
if (bits.FirstRangeFrom(start).first >= end) {
return;
}
RegionBits mask(bits, start, end);
if constexpr (clear) {
bits.UnsetRange(start, end);
+55 -115
View File
@@ -27,26 +27,6 @@ namespace {
constexpr uint64_t MiB = 1024 * 1024;
constexpr uint64_t GdsBufferSize = 64 * 1024;
std::pair<Buffer*, uint64_t> UploadMemory(GraphicContext& graphics, CommandScheduler& scheduler,
StreamBuffer& staging, uint64_t size, uint64_t alignment,
vk::BufferUsageFlags usage, const auto& write) {
const auto [mapped, offset] = staging.Map(size, alignment);
if (mapped != nullptr) {
write(mapped, offset);
staging.Commit();
return {&staging, offset};
}
// Large transfers use the same one-time host allocation as ordinary buffer uploads.
auto temporary = std::make_unique<Buffer>(graphics, scheduler, MemoryUsage::Upload, 0,
usage, size);
write(temporary->Mapped().data(), 0);
temporary->Flush(0, size);
auto* buffer = temporary.get();
scheduler.DeferOperation([owner = std::move(temporary)]() mutable { owner.reset(); });
return {buffer, 0};
}
} // namespace
void BufferCache::WriteDataBuffer(Buffer& buffer, uint64_t address, const void* source,
@@ -54,36 +34,9 @@ void BufferCache::WriteDataBuffer(Buffer& buffer, uint64_t address, const void*
auto* bytes = static_cast<const uint8_t*>(source);
while (size != 0) {
const auto chunk = std::min(size, m_staging_buffer.Size());
// Copy waits for ring reuse and flushes the fresh host data before submission.
const auto offset = m_staging_buffer.Copy(bytes, chunk, 4);
const auto destination_offset = buffer.Offset(address);
EXIT_IF(destination_offset > buffer.Size() || chunk > buffer.Size() - destination_offset);
m_scheduler.EndRendering();
const auto command = m_scheduler.Current().Handle();
vk::BufferMemoryBarrier2 before {};
before.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands;
before.srcAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite;
before.dstStageMask = vk::PipelineStageFlagBits2::eTransfer;
before.dstAccessMask = vk::AccessFlagBits2::eTransferWrite;
before.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
before.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
before.buffer = buffer.Handle();
before.offset = destination_offset;
before.size = chunk;
vk::DependencyInfo dependency {};
dependency.dependencyFlags = vk::DependencyFlagBits::eByRegion;
dependency.bufferMemoryBarrierCount = 1;
dependency.pBufferMemoryBarriers = &before;
command.pipelineBarrier2(dependency);
const vk::BufferCopy copy {offset, destination_offset, chunk};
command.copyBuffer(m_staging_buffer.Handle(), buffer.Handle(), 1, &copy);
auto after = before;
after.srcStageMask = vk::PipelineStageFlagBits2::eTransfer;
after.srcAccessMask = vk::AccessFlagBits2::eTransferWrite;
after.dstStageMask = vk::PipelineStageFlagBits2::eAllCommands;
after.dstAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite;
dependency.pBufferMemoryBarriers = &after;
command.pipelineBarrier2(dependency);
buffer.CopyFrom(m_scheduler.Current(), m_staging_buffer, offset, buffer.Offset(address),
chunk, vk::AccessFlagBits::eHostWrite);
bytes += chunk;
address += chunk;
size -= chunk;
@@ -102,8 +55,7 @@ template <bool insert>
void BufferCache::ChangeRegister(BufferId id) {
auto& buffer = m_slot_buffers[id];
PageTable::PageRange pages {};
EXIT_IF(!(GuestRange {buffer.CpuAddress(), buffer.Size()}.Valid()) ||
!PageTable::TryGetPageRange(buffer.CpuAddress(), buffer.Size(), pages));
EXIT_IF(!PageTable::TryGetPageRange(buffer.CpuAddress(), buffer.Size(), pages));
for (size_t page = pages.first; page < pages.last_exclusive; ++page) {
if constexpr (insert) {
m_page_table[page] = id;
@@ -112,7 +64,6 @@ void BufferCache::ChangeRegister(BufferId id) {
}
}
const auto size_pages = pages.last_exclusive - pages.first;
const auto table_offset = PageIndex(buffer.CpuAddress()) * sizeof(vk::DeviceAddress);
if constexpr (insert) {
const auto [it, inserted] = m_buffers.emplace(buffer.CpuAddress(), id);
(void)it;
@@ -124,9 +75,8 @@ void BufferCache::ChangeRegister(BufferId id) {
for (uint64_t i = 0; i < size_pages; ++i) {
addresses.push_back(buffer.BufferDeviceAddress() + (i << CACHING_PAGEBITS));
}
WriteDataBuffer(m_bda_pagetable_buffer, table_offset,
WriteDataBuffer(m_bda_pagetable_buffer, pages.first * sizeof(vk::DeviceAddress),
addresses.data(), addresses.size() * sizeof(vk::DeviceAddress));
m_memory_tracker.QueueBdaSync(buffer.CpuAddress(), buffer.Size());
} else {
const auto found = m_buffers.find(buffer.CpuAddress());
EXIT_IF(found == m_buffers.end() || found->second != id);
@@ -134,7 +84,7 @@ void BufferCache::ChangeRegister(BufferId id) {
EXIT_IF(buffer.Size() > m_total_used_memory);
m_total_used_memory -= buffer.Size();
m_lru_cache.Free(buffer.lru_id);
m_bda_pagetable_buffer.Fill(table_offset,
m_bda_pagetable_buffer.Fill(pages.first * sizeof(vk::DeviceAddress),
size_pages * sizeof(vk::DeviceAddress), 0);
buffer.is_deleted = true;
}
@@ -158,7 +108,6 @@ void BufferCache::DeleteBuffer(BufferId id) {
}
}
template <bool async>
bool BufferCache::DownloadBufferMemory(Buffer& buffer, uint64_t vaddr, uint64_t size) {
std::vector<vk::BufferCopy> copies;
uint64_t total_size = 0;
@@ -178,16 +127,11 @@ bool BufferCache::DownloadBufferMemory(Buffer& buffer, uint64_t vaddr, uint64_t
return false;
}
auto [mapped, offset] = m_download_buffer.Map(total_size, 64);
std::unique_ptr<Buffer> temporary;
const auto [mapped, offset] = m_download_buffer.Map(total_size, 64);
if (mapped == nullptr) {
temporary = std::make_unique<Buffer>(m_graphics, m_scheduler, MemoryUsage::Download, 0,
vk::BufferUsageFlagBits::eTransferDst, total_size);
mapped = temporary->Mapped().data();
} else {
m_download_buffer.Commit();
EXIT("BufferCache: download exceeds 64 MiB staging buffer capacity\n");
}
const auto& download = temporary ? *temporary : m_download_buffer;
m_download_buffer.Commit();
for (auto& copy: copies) {
copy.dstOffset += offset;
}
@@ -206,35 +150,27 @@ bool BufferCache::DownloadBufferMemory(Buffer& buffer, uint64_t vaddr, uint64_t
native.pipelineBarrier(vk::PipelineStageFlagBits::eAllCommands,
vk::PipelineStageFlagBits::eTransfer, {}, 0, nullptr, 1, &before, 0,
nullptr);
native.copyBuffer(buffer.Handle(), download.Handle(),
native.copyBuffer(buffer.Handle(), m_download_buffer.Handle(),
static_cast<uint32_t>(copies.size()), copies.data());
auto after = before;
after.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
after.dstAccessMask = vk::AccessFlagBits::eHostRead;
after.buffer = download.Handle();
after.buffer = m_download_buffer.Handle();
after.offset = offset;
after.size = total_size;
native.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
vk::PipelineStageFlagBits::eAllCommands |
vk::PipelineStageFlagBits::eHost,
{}, 0, nullptr, 1, &after, 0, nullptr);
auto publish = [this, mapped, offset, total_size, buffer_address,
copies = std::move(copies), owner = std::move(temporary)] {
(owner ? *owner : m_download_buffer).Invalidate(offset, total_size);
m_scheduler.DeferPriorityOperation([this, mapped, offset, total_size, buffer_address,
copies = std::move(copies)] {
m_download_buffer.Invalidate(offset, total_size);
for (const auto& copy: copies) {
Libs::LibKernel::Memory::WriteBacking(buffer_address + copy.srcOffset,
mapped + (copy.dstOffset - offset), copy.size);
}
};
if constexpr (async) {
m_scheduler.DeferPriorityOperation(std::move(publish));
} else {
const auto tick = m_scheduler.CurrentTick();
m_scheduler.Wait(tick);
m_scheduler.WaitPriorityOperations(tick);
publish();
}
});
return true;
}
@@ -306,8 +242,18 @@ void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size, bool is_write) {
}
auto& buffer = m_slot_buffers[FindBuffer(vaddr, size)];
if (DownloadBufferMemory<false>(buffer, vaddr, size)) {
m_memory_tracker.UnmarkRegionAsGpuModified(vaddr, size);
// Widen nearby CPU reads so they share one GPU drain.
constexpr uint64_t WindowSize = 512 * 1024;
const auto buffer_begin = buffer.CpuAddress();
const auto buffer_end = buffer_begin + buffer.Size();
const auto window_begin = std::max(Common::AlignDown(vaddr, WindowSize), buffer_begin);
const auto window_end = std::min(std::max(window_begin + WindowSize, vaddr + size), buffer_end);
if (DownloadBufferMemory(buffer, window_begin, window_end - window_begin)) {
const auto tick = m_scheduler.CurrentTick();
m_scheduler.Wait(tick);
m_scheduler.WaitPriorityOperations(tick);
m_memory_tracker.UnmarkRegionAsGpuModified(window_begin, window_end - window_begin);
}
if (is_write) {
m_memory_tracker.MarkRegionAsCpuModified(vaddr, size);
@@ -366,14 +312,10 @@ BufferCache::OverlapResult BufferCache::ResolveOverlaps(uint64_t vaddr, uint64_t
// Reserve space in the incoming stream's direction of growth.
// The old buffer extending left of the request predicts growth to the right, and vice versa.
if (expands_left) {
end += std::min(StreamLeapSize, (vaddr < LOWER_ADDRESS_SIZE ? LOWER_ADDRESS_SIZE
: LibKernel::Memory::kExtendedMemoryBase +
LibKernel::Memory::kExtendedMemorySize) - end);
end += std::min(StreamLeapSize, PageTable::kAddressSpaceSize - end);
}
if (expands_right) {
const auto minimum = vaddr < LOWER_ADDRESS_SIZE
? CACHING_PAGESIZE * 2
: LibKernel::Memory::kExtendedMemoryBase;
const auto minimum = CACHING_PAGESIZE * 2;
if (begin > minimum) {
begin -= std::min(StreamLeapSize, begin - minimum);
}
@@ -468,17 +410,28 @@ vk::Buffer BufferCache::UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> c
return nullptr;
}
const auto source = UploadMemory(m_graphics, m_scheduler, m_staging_buffer, total_size, 4,
vk::BufferUsageFlagBits::eTransferSrc, [&](uint8_t* mapped, uint64_t base_offset) {
auto [mapped, base_offset] = m_staging_buffer.Map(total_size, 4);
if (mapped != nullptr) {
for (auto& copy: copies) {
const auto address = buffer.CpuAddress() + copy.dstOffset;
if (!LibKernel::Memory::CopySparseMemory(address, mapped + copy.srcOffset, copy.size)) {
EXIT("BufferCache: failed to read sparse guest buffer memory\n");
}
std::memcpy(mapped + copy.srcOffset, reinterpret_cast<const void*>(address), copy.size);
copy.srcOffset += base_offset;
}
});
return source.first->Handle();
m_staging_buffer.Commit();
return m_staging_buffer.Handle();
}
auto temporary = std::make_unique<Buffer>(m_graphics, m_scheduler, MemoryUsage::Upload, 0,
vk::BufferUsageFlagBits::eTransferSrc, total_size);
for (const auto& copy: copies) {
const auto address = buffer.CpuAddress() + copy.dstOffset;
std::memcpy(temporary->Mapped().data() + copy.srcOffset,
reinterpret_cast<const void*>(address), copy.size);
}
temporary->Flush(0, total_size);
const auto handle = temporary->Handle();
m_scheduler.DeferOperation([owner = std::move(temporary)]() mutable { owner.reset(); });
return handle;
}
std::pair<Buffer*, uint64_t> BufferCache::ObtainBuffer(uint64_t vaddr, uint64_t size,
@@ -495,10 +448,7 @@ std::pair<Buffer*, uint64_t> BufferCache::ObtainBuffer(uint64_t vaddr, uint64_t
const auto alignment = std::max<uint64_t>(
m_graphics.physical_device_properties.limits.minUniformBufferOffsetAlignment, 1);
auto [mapped, offset] = m_stream_buffer.Map(size, alignment, false);
if (mapped != nullptr) {
if (!LibKernel::Memory::CopySparseMemory(vaddr, mapped, size)) {
EXIT("BufferCache: failed to read sparse guest buffer memory\n");
}
if (mapped != nullptr && Libs::LibKernel::Memory::TryReadBacking(vaddr, mapped, size)) {
m_stream_buffer.Commit();
return {&m_stream_buffer, offset};
}
@@ -533,13 +483,13 @@ std::pair<Buffer*, uint64_t> BufferCache::ObtainBufferForImage(uint64_t vaddr, u
return ObtainBuffer(vaddr, size, false, false);
}
return UploadMemory(m_graphics, m_scheduler, m_staging_buffer, size, 16,
vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eStorageBuffer,
[&](uint8_t* mapped, uint64_t) {
if (!Libs::LibKernel::Memory::TryReadSparseBacking(vaddr, mapped, size)) {
EXIT("BufferCache: failed to read mapped guest image backing\n");
}
});
auto [staging, stage_offset] = m_staging_buffer.Map(size, 16);
if (staging == nullptr || (!Libs::LibKernel::Memory::TryReadBacking(vaddr, staging, size) &&
!Libs::LibKernel::Memory::TryReadPrtBacking(vaddr, staging, size))) {
EXIT("BufferCache: failed to read mapped guest image backing\n");
}
m_staging_buffer.Commit();
return {&m_staging_buffer, stage_offset};
}
void BufferCache::FillBuffer(uint64_t vaddr, uint64_t size, uint32_t value, bool is_gds) {
@@ -653,7 +603,7 @@ void BufferCache::RunGarbageCollector() {
return false;
}
if (dirty) {
EXIT_NOT_IMPLEMENTED(!DownloadBufferMemory<true>(buffer, buffer.CpuAddress(), buffer.Size()));
EXIT_IF(!DownloadBufferMemory(buffer, buffer.CpuAddress(), buffer.Size()));
dirty_buffers.push_back(id);
} else {
m_memory_tracker.UntrackMemory(buffer.CpuAddress(), buffer.Size());
@@ -686,16 +636,6 @@ void BufferCache::ProcessFaultBuffer() {
m_fault_manager.ProcessFaultBuffer();
}
void BufferCache::SynchronizeBda(const RangeSet& mapped) {
// Take before walking: writes queued during synchronization remain pending.
const auto pending = m_memory_tracker.TakeBdaSync();
pending.ForEach([&](uint64_t begin, uint64_t end) {
mapped.ForEachInRange(begin, end - begin, [this](uint64_t start, uint64_t finish) {
SynchronizeBuffersInRange(start, finish - start);
});
});
}
void BufferCache::SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size) {
const auto end = vaddr + size;
auto it = m_buffers.upper_bound(vaddr);
+4 -21
View File
@@ -29,22 +29,10 @@ class BufferCache {
public:
static constexpr uint32_t CACHING_PAGEBITS = 14;
static constexpr uint64_t CACHING_PAGESIZE = uint64_t {1} << CACHING_PAGEBITS;
static constexpr uint64_t CACHING_NUMPAGES = (LOWER_ADDRESS_SIZE + LibKernel::Memory::kExtendedMemorySize) >> CACHING_PAGEBITS;
static constexpr uint64_t CACHING_NUMPAGES = uint64_t {1} << (40 - CACHING_PAGEBITS);
static constexpr uint64_t BDA_PAGETABLE_SIZE =
CACHING_NUMPAGES * sizeof(vk::DeviceAddress);
static constexpr uint64_t PageIndex(uint64_t address) {
return (address < LOWER_ADDRESS_SIZE
? address
: address - LibKernel::Memory::kExtendedMemoryBase + LOWER_ADDRESS_SIZE) >>
CACHING_PAGEBITS;
}
static constexpr uint64_t GuestAddress(uint64_t offset) {
return offset < LOWER_ADDRESS_SIZE
? offset
: offset - LOWER_ADDRESS_SIZE + LibKernel::Memory::kExtendedMemoryBase;
}
BufferCache(GraphicContext& graphics, CommandScheduler& scheduler, PageManager& page_manager,
TextureCache& texture_cache);
~BufferCache();
@@ -80,10 +68,7 @@ public:
[[nodiscard]] bool IsRegionCpuModified(uint64_t vaddr, uint64_t size);
[[nodiscard]] bool IsRegionGpuModified(uint64_t vaddr, uint64_t size);
void ProcessFaultBuffer();
void SynchronizeBda(const RangeSet& mapped);
void QueueBdaSync(uint64_t vaddr, uint64_t size) {
m_memory_tracker.QueueBdaSync(vaddr, size);
}
void SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size);
void RunGarbageCollector();
private:
@@ -103,7 +88,7 @@ private:
bool has_stream_leap;
};
using PageTable = MultiLevelPageTable<BufferId, CACHING_PAGEBITS, 44, 20>;
using PageTable = MultiLevelPageTable<BufferId, CACHING_PAGEBITS, 40, 16>;
static_assert(CACHING_PAGESIZE == (uint64_t {1} << PageTable::kPageBits));
void WriteDataBuffer(Buffer& buffer, uint64_t address, const void* source, uint64_t size);
void TouchBuffer(const Buffer& buffer);
@@ -117,12 +102,10 @@ private:
void DeleteBuffer(BufferId id);
[[nodiscard]] bool SynchronizeBuffer(Buffer& buffer, uint64_t vaddr, uint64_t size,
bool is_written, bool is_texel_buffer);
void SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size);
[[nodiscard]] vk::Buffer UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> copies,
uint64_t total_size);
[[nodiscard]] bool SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uint64_t size);
// Synchronous downloads publish before returning; asynchronous callers wait before reuse.
template <bool async>
// Queues backing publication; callers wait before clearing dirty pages or reusing their data.
[[nodiscard]] bool DownloadBufferMemory(Buffer& buffer, uint64_t vaddr, uint64_t size);
GraphicContext& m_graphics;
+2 -3
View File
@@ -134,9 +134,8 @@ void FaultManager::ProcessFaultBuffer() {
const auto* faults = std::bit_cast<const uint64_t*>(mapped);
const auto count = static_cast<uint32_t>(faults[0]);
for (uint32_t index = 1; index <= count; ++index) {
const auto address = BufferCache::GuestAddress(faults[index]);
fault_ranges.Add(address, BufferCache::CACHING_PAGESIZE);
LOGF("Accessed non-GPU cached memory at 0x%016" PRIx64 "\n", address);
fault_ranges.Add(faults[index], BufferCache::CACHING_PAGESIZE);
LOGF("Accessed non-GPU cached memory at 0x%016" PRIx64 "\n", faults[index]);
}
fault_ranges.ForEach([this](uint64_t start, uint64_t end) {
EXIT_IF(end - start > std::numeric_limits<uint32_t>::max());
+11 -14
View File
@@ -14,10 +14,10 @@ SamplerCache::~SamplerCache() {
}
}
vk::Sampler SamplerCache::GetSampler(const ShaderSamplerResource& r, bool integer_border) {
vk::Sampler SamplerCache::GetSampler(const ShaderSamplerResource& r) {
Common::LockGuard lock(m_mutex);
const SamplerKey key {r.fields[0], r.fields[1], r.fields[2], r.fields[3], integer_border};
const SamplerKey key {r.fields[0], r.fields[1], r.fields[2], r.fields[3]};
if (auto iter = m_samplers.find(key); iter != m_samplers.end()) {
return iter->second;
}
@@ -93,23 +93,23 @@ vk::Sampler SamplerCache::GetSampler(const ShaderSamplerResource& r, bool intege
return vk::SamplerAddressMode::eClampToBorder;
};
vk::BorderColor border = integer_border ? vk::BorderColor::eIntTransparentBlack
: vk::BorderColor::eFloatTransparentBlack;
vk::BorderColor border = vk::BorderColor::eIntTransparentBlack;
switch (static_cast<Prospero::SamplerBorderColor>(r.BorderColorType())) {
case Prospero::SamplerBorderColor::kTransBlack: break;
case Prospero::SamplerBorderColor::kTransBlack:
border = vk::BorderColor::eIntTransparentBlack;
break;
case Prospero::SamplerBorderColor::kOpaqueBlack:
border = integer_border ? vk::BorderColor::eIntOpaqueBlack
: vk::BorderColor::eFloatOpaqueBlack;
border = vk::BorderColor::eIntOpaqueBlack;
break;
case Prospero::SamplerBorderColor::kOpaqueWhite:
border = integer_border ? vk::BorderColor::eIntOpaqueWhite
: vk::BorderColor::eFloatOpaqueWhite;
border = vk::BorderColor::eIntOpaqueWhite;
break;
case Prospero::SamplerBorderColor::kFromTable:
LOGF(
"temporary: approximating table border color as transparent black, index = %" PRIu16
"\n",
r.BorderColorPtr());
border = vk::BorderColor::eIntTransparentBlack;
break;
default: EXIT("unknown border color: %d", static_cast<int>(r.BorderColorType()));
}
@@ -123,11 +123,8 @@ vk::Sampler SamplerCache::GetSampler(const ShaderSamplerResource& r, bool intege
sampler_info.addressModeU = to_vk_address_mode(r.ClampX());
sampler_info.addressModeV = to_vk_address_mode(r.ClampY());
sampler_info.addressModeW = to_vk_address_mode(r.ClampZ());
const auto max_lod_bias = m_graphics.GetPhysicalDeviceProperties().limits.maxSamplerLodBias;
sampler_info.mipLodBias = std::clamp(
static_cast<float>(static_cast<int16_t>((r.LodBias() ^ 0x2000u) - 0x2000u)) / 256.0f,
-max_lod_bias, max_lod_bias
);
sampler_info.mipLodBias =
static_cast<float>(static_cast<int16_t>((r.LodBias() ^ 0x2000u) - 0x2000u)) / 256.0f;
sampler_info.anisotropyEnable = (aniso ? VK_TRUE : VK_FALSE);
sampler_info.maxAnisotropy = aniso_ratio;
sampler_info.compareEnable = (r.DepthCompareFunc() != 0 ? VK_TRUE : VK_FALSE);
+2 -2
View File
@@ -24,10 +24,10 @@ public:
~SamplerCache();
KYTY_CLASS_NO_COPY(SamplerCache);
vk::Sampler GetSampler(const ShaderSamplerResource& r, bool integer_border);
vk::Sampler GetSampler(const ShaderSamplerResource& r);
private:
using SamplerKey = std::array<uint32_t, 5>;
using SamplerKey = std::array<uint32_t, 4>;
struct SamplerKeyHash {
std::size_t operator()(const SamplerKey& key) const {
+3 -1
View File
@@ -67,9 +67,11 @@ Buffer::Buffer(GraphicContext& graphics, CommandScheduler& scheduler, MemoryUsag
buffer_info.usage = flags;
const bool with_bda = bool(flags & vk::BufferUsageFlagBits::eShaderDeviceAddress);
const VmaAllocationCreateFlags bda_flag =
with_bda ? VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT : 0;
VmaAllocationCreateInfo allocation_info {};
allocation_info.flags =
VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | AllocationFlags(usage);
VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | bda_flag | AllocationFlags(usage);
allocation_info.usage = AllocationUsage(usage);
allocation_info.preferredFlags = usage == MemoryUsage::DeviceLocal
? VkMemoryPropertyFlags {}
File diff suppressed because it is too large Load Diff
+17 -19
View File
@@ -12,9 +12,9 @@
#include "graphics/host_gpu/renderer/image/image.h"
#include "graphics/host_gpu/renderer/image/tiler.h"
#include <bitset>
#include <map>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <vector>
@@ -26,15 +26,8 @@ class BufferCache;
class CommandBuffer;
class CommandScheduler;
class RenderExecutor;
class TextureCache;
struct TextureCacheTestAccess;
namespace BcAlias {
[[nodiscard]] bool Matches(const ImageInfo& requested, const ImageInfo& cached);
[[nodiscard]] bool Publish(TextureCache& cache, Image& image);
void Resolve(TextureCache& cache, ImageId cached, ImageId merged);
} // namespace BcAlias
class TextureCache {
public:
enum class BindingType : uint8_t { Texture, Storage, RenderTarget, DepthTarget, VideoOut };
@@ -68,7 +61,7 @@ public:
uint32_t packed_clear);
void InvalidateMemory(uint64_t address, uint64_t size);
void InvalidateMemoryFromGPU(uint64_t address, uint64_t size);
[[nodiscard]] bool IsRegionRegistered(uint64_t address, uint64_t size);
[[nodiscard]] bool IsRegionGpuModified(uint64_t address, uint64_t size);
[[nodiscard]] bool IsMeta(uint64_t address);
[[nodiscard]] bool IsMetaCleared(uint64_t address, uint32_t slice);
@@ -80,12 +73,15 @@ public:
void RunGarbageCollector();
private:
enum class TransferDirection { Upload, Download };
struct TextureTransfer;
struct ImageDownload;
struct MetaDataInfo {
enum class Type : uint8_t { CMask, FMask, HTile };
Type type;
// PS5 DB_DEPTH_VIEW encodes 13-bit base and last array-slice indices.
std::bitset<8192> clear_mask;
Type type;
uint32_t clear_mask = UINT32_MAX;
};
struct OverlapResult {
@@ -95,9 +91,9 @@ private:
};
using ImageIds = InlinePageOwnerList<ImageId, 16>;
using ImagePageTable = MultiLevelPageTable<ImageIds, 20, 44, 14>;
using ImagePageTable = MultiLevelPageTable<ImageIds, 20, 40, 10>;
// Callers have validated the nonempty 44-bit range with TryGetPageRange.
// Callers have validated the nonempty 40-bit range with TryGetPageRange.
template <typename Func>
static void ForEachPage(uint64_t address, size_t size, Func&& func) {
using FuncReturn = typename std::invoke_result<Func, uint64_t>::type;
@@ -115,6 +111,7 @@ private:
}
[[nodiscard]] ImageId InsertImage(const ImageInfo& info);
[[nodiscard]] ImageId GetNullImage(const ImageDesc& desc);
void RegisterImage(ImageId id);
void UnregisterImage(ImageId id);
void DeleteImage(ImageId id);
@@ -131,6 +128,7 @@ private:
[[nodiscard]] static bool SameBacking(const ImageInfo& cached, const ImageInfo& requested,
bool exact_format);
[[nodiscard]] static BindingType UploadBinding(const Image& image);
[[nodiscard]] bool SafeToDownload(const Image& image);
// Caller holds m_lock; it also serializes the per-image query epoch.
[[nodiscard]] ImageIds FindImagesInRegion(uint64_t address, uint64_t size,
@@ -144,10 +142,12 @@ private:
void MaterializeColorClear(ImageId id, const ImageDesc& desc,
uint32_t metadata_base_layer);
void InitializeImage(ImageId id);
[[nodiscard]] bool CanDownload(const Image& image) const;
[[nodiscard]] TextureTransfer
BuildTextureTransfer(const Image& image, BindingType binding, TransferDirection direction) const;
[[nodiscard]] ImageDownload BuildDownload(const Image& image) const;
void UploadImage(Image& image, Buffer& source, uint64_t source_offset);
void DownloadImage(Image& image, Buffer& destination, uint64_t destination_offset,
uint64_t destination_size, uint32_t levels);
uint64_t destination_size, ImageDownload transfer);
void DownloadDepth(Image& image, Buffer& destination, uint64_t destination_offset);
void CommitGpuWrite(Image& image);
// Caller holds m_lock. Volume layer ranges select depth slices.
@@ -173,6 +173,7 @@ private:
BufferCache& m_buffer_cache;
Common::SlotVector<Image> m_slot_images;
ImagePageTable m_image_page_table;
std::unordered_map<vk::Format, ImageId> m_null_images;
Common::LeastRecentlyUsedCache<ImageId, uint64_t> m_lru_cache;
std::unordered_set<ImageId> m_download_images;
std::map<uint64_t, MetaDataInfo> m_surface_metas;
@@ -187,9 +188,6 @@ private:
friend struct TextureCacheTestAccess;
friend class BufferCache;
friend class RenderExecutor;
friend bool BcAlias::Matches(const ImageInfo&, const ImageInfo&);
friend bool BcAlias::Publish(TextureCache&, Image&);
friend void BcAlias::Resolve(TextureCache&, ImageId, ImageId);
};
} // namespace Libs::Graphics
@@ -21,6 +21,8 @@
namespace Libs::Graphics {
static std::atomic<uint32_t> g_render_color_log_count = 0;
static bool DccAlphaOnMsb(const HW::ColorInfo& info) {
switch (info.format) {
case Prospero::ChannelLayout::k10_10_10_2:
@@ -139,7 +141,9 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
// Nonlinear clear values are still stored as normalized components.
// Metadata clears are materialized during image discovery; render-pass loads preserve contents.
uint32_t width = 0;
uint32_t height = 0;
uint32_t height = 0;
uint32_t pitch = 0;
uint64_t size = 0;
bool tile = false;
const bool standard4 = rt.attrib3.tile_mode == Prospero::TileMode::kStandard4KB;
const bool standard64 = rt.attrib3.tile_mode == Prospero::TileMode::kStandard64KB;
@@ -162,9 +166,6 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
if (texture_tile && samples != 1) {
EXIT("texture-tiled color render targets do not support multisampling\n");
}
if (volume && !tile) {
EXIT("linear 3D render targets are unsupported\n");
}
width = rt.attrib2.width + 1;
height = rt.attrib2.height + 1;
@@ -199,25 +200,94 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
" layer=%u/%u\n",
rt.attrib3.dimension, rt.attrib3.depth, view.base_layer, view.image_layers);
}
auto& desc = r.desc;
if (samples == 1) {
pitch = TileGetTexturePitch(transfer_format, width, rt.attrib3.tile_mode);
} else {
pitch = TileGetRenderTargetPitch(width, bytes_per_element, rt.attrib.num_fragments);
}
if (pitch == 0) {
EXIT("unsupported render-target pitch: width=%u bytes=%u\n", width, bytes_per_element);
}
TileSizeOffset mip_sizes[16] {};
TilePaddedSize mip_padded[16] {};
TileSurfaceLayout volume_layout {};
uint64_t backing_size = 0;
if (volume) {
const TileSurfaceDescription description {transfer_format,
rt.attrib3.tile_mode,
TileSurfaceDimension::Dim3D,
width,
height,
depth,
levels,
1};
if (!tile || !TileGetTiledTextureLayout(description, volume_layout)) {
EXIT("unsupported 3D render-target layout: %ux%ux%u levels=%u tile=%u\n", width, height,
depth, levels, static_cast<uint32_t>(rt.attrib3.tile_mode));
}
size = volume_layout.block_slice_size;
backing_size = volume_layout.total_size;
} else {
TileSizeAlign layout {};
bool valid_layout = false;
if (samples == 1) {
TileGetTextureSize(transfer_format, width, height, levels, rt.attrib3.tile_mode,
&layout, mip_sizes, mip_padded);
valid_layout = layout.size != 0 && layout.align != 0 &&
(rt.attrib3.tile_mode != Prospero::TileMode::kRenderTarget ||
levels <= std::bit_width(std::max(width, height)));
} else {
valid_layout = TileGetRenderTargetSize(width, height, pitch, bytes_per_element,
layout, rt.attrib.num_fragments);
mip_sizes[0] = {layout.size, 0, 0, 0, 0, 0};
mip_padded[0] = {pitch, height};
}
if (!valid_layout) {
EXIT("unsupported render-target layout: %ux%u pitch=%u bytes=%u levels=%u\n", width,
height, pitch, bytes_per_element, levels);
}
size = layout.size;
}
if (size == 0 || (!volume && size > UINT64_MAX / view.image_layers)) {
EXIT("render-target memory footprint is invalid\n");
}
if (!volume) {
backing_size = size * view.image_layers;
}
if (backing_size == 0) {
EXIT("render-target backing is empty\n");
}
if (!GuestRange {rt.base.addr, backing_size}.Valid()) {
EXIT("render-target backing range is invalid\n");
}
const vk::Extent2D view_extent = {std::max(width >> rt.view.current_mip_level, 1u),
std::max(height >> rt.view.current_mip_level, 1u)};
auto decision_log_id = g_render_color_log_count.fetch_add(1);
if (decision_log_id < 128) {
LOGF("RenderColorTarget: slot=%" PRIu32 " addr=0x%010" PRIx64 " size=0x%016" PRIx64
" extent=%ux%ux%u view_mip=%u view_extent=%ux%u levels=%u pitch=%u"
" fmt=0x%08" PRIx32 " nfmt=0x%08" PRIx32 " order=0x%08" PRIx32 " samples=%u tile=%s\n",
rt_slot, rt.base.addr, backing_size, width, height, depth, rt.view.current_mip_level,
view_extent.width, view_extent.height, levels, pitch,
static_cast<uint32_t>(rt.info.format), static_cast<uint32_t>(rt.info.channel_type),
static_cast<uint32_t>(rt.info.channel_order), samples, tile ? "tiled" : "linear");
}
TextureCache::ImageDesc desc {};
desc.type = TextureCache::BindingType::RenderTarget;
desc.info.data = {rt.base.addr, 0};
desc.info.data = {rt.base.addr, backing_size};
desc.info.pixel_format = target_format.format;
desc.info.guest_format = target_format.guest_format;
desc.info.guest_format = transfer_format;
desc.info.type = image_type;
desc.info.extent = {width, height, depth};
desc.info.resources = {levels, volume ? 1u : view.image_layers};
desc.info.resources = {levels, volume ? 1u : view.image_layers};
desc.info.pitch = pitch;
desc.info.bytes_per_block = bytes_per_element;
desc.info.samples = samples;
desc.info.tile_mode = rt.attrib3.tile_mode;
if (!volume && rt.attrib3.tile_mode == Prospero::TileMode::kRenderTarget &&
levels > std::bit_width(std::max(width, height))) {
EXIT("unsupported render-target mip count\n");
}
desc.info.UpdateSize();
if (!desc.info.data.Valid()) {
EXIT("render-target backing range is invalid\n");
}
const bool has_dcc = rt.info.dcc_compression_enable && rt.dcc_addr.addr != 0;
const bool has_cmask = !rt.info.dcc_compression_enable && rt.info.cmask_fast_clear_enable &&
rt.cmask.addr != 0 && samples == 1 &&
@@ -236,10 +306,33 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
// DCC owns the clear when both planes are enabled; single-sample CMASK stays expanded.
desc.info.metadata.kind = has_dcc ? ImageMetadataKind::Dcc : ImageMetadataKind::Cmask;
desc.info.metadata.range = {has_dcc ? rt.dcc_addr.addr : rt.cmask.addr, metadata_size.size};
desc.info.metadata.clear_word =
rt.clear_word0.word0 | (uint64_t {rt.clear_word1.word1} << 32u);
desc.info.metadata.clear_word = rt.clear_word0.word0;
desc.info.metadata.clear_register_valid = true;
}
for (uint32_t level = 0; level < levels; level++) {
if (volume) {
const auto& mip = volume_layout.mips[level];
desc.info.mip_layout[level] = {
mip.offset,
mip.size,
mip.padded_width,
mip.padded_height,
};
continue;
}
const auto level_offset =
mip_sizes[level].src_size != 0 ? mip_sizes[level].src_offset : mip_sizes[level].offset;
const auto level_size =
static_cast<uint64_t>(mip_sizes[level].src_size != 0 ? mip_sizes[level].src_size
: mip_sizes[level].size) *
view.image_layers;
desc.info.mip_layout[level] = {
level_offset,
level_size,
mip_padded[level].width,
mip_padded[level].height,
};
}
desc.view_info.format = target_format.format;
if (is_1d) {
desc.view_info.type =
@@ -255,6 +348,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
desc.view_info.layer_count = view.layer_count;
desc.view_info.usage = vk::ImageUsageFlagBits::eColorAttachment;
auto& texture_cache = m_context.GetTextureCache();
r.desc = std::move(desc);
r.guest_mip_level = rt.view.current_mip_level;
r.guest_array_layer = view.base_layer;
r.image_id = texture_cache.FindImage(r.desc, exact_format);
@@ -226,37 +226,43 @@ void CommandScheduler::PopPendingOperations() {
}
void CommandScheduler::DeferOperation(Common::UniqueFunction<void>&& operation) {
QueueOperation(std::move(operation), false);
}
void CommandScheduler::DeferPriorityOperation(Common::UniqueFunction<void>&& operation) {
QueueOperation(std::move(operation), true);
}
void CommandScheduler::QueueOperation(Common::UniqueFunction<void>&& operation, bool priority) {
CheckActive();
EXIT_IF(!operation);
std::unique_lock lock(m_operation_mutex);
if (m_operation_state == OperationState::Open) {
auto& queue = priority ? m_priority_operations : m_pending_operations;
queue.push({std::move(operation), CurrentTick()});
lock.unlock();
if (priority) {
m_operation_available.notify_one();
}
m_pending_operations.push({std::move(operation), CurrentTick()});
return;
}
if (g_deferred_callback_scheduler != this) {
m_operation_available.wait(lock,
[this] { return m_operation_state == OperationState::Closed; });
if (g_deferred_callback_scheduler == this) {
lock.unlock();
operation();
return;
}
m_operation_available.wait(lock,
[this] { return m_operation_state == OperationState::Closed; });
lock.unlock();
operation();
}
bool CommandScheduler::HasPendingPriorityOperations() {
std::lock_guard lock(m_operation_mutex);
return !m_priority_operations.empty() || m_priority_active;
void CommandScheduler::DeferPriorityOperation(Common::UniqueFunction<void>&& operation) {
CheckActive();
EXIT_IF(!operation);
std::unique_lock lock(m_operation_mutex);
if (m_operation_state == OperationState::Open) {
m_priority_operations.push({std::move(operation), CurrentTick()});
lock.unlock();
m_operation_available.notify_one();
return;
}
if (g_deferred_callback_scheduler == this) {
lock.unlock();
operation();
return;
}
m_operation_available.wait(lock,
[this] { return m_operation_state == OperationState::Closed; });
lock.unlock();
operation();
}
void CommandScheduler::PriorityOperationsThread(std::stop_token stop) {
@@ -334,7 +340,6 @@ CommandBuffer& CommandScheduler::BeginCommand() {
EXIT_IF(!m_command.IsInvalid());
m_command.m_buffer = m_command_pool.Commit();
m_command.Begin();
m_dynamic_state.Invalidate();
return m_command;
}
@@ -3,13 +3,14 @@
#include "common/common.h"
#include "common/uniqueFunction.h"
#include "graphics/host_gpu/renderer/dynamicState.h"
#include "graphics/host_gpu/renderer/masterSemaphore.h"
#include "graphics/host_gpu/renderer/render.h"
#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
@@ -37,10 +38,8 @@ public:
void PopPendingOperations();
void DrainPriorityOperations();
void WaitPriorityOperations(uint64_t tick);
// Guest-memory completions use the priority queue; normal callbacks maintain GPU resources.
void DeferOperation(Common::UniqueFunction<void>&& operation);
void DeferPriorityOperation(Common::UniqueFunction<void>&& operation);
[[nodiscard]] bool HasPendingPriorityOperations();
[[nodiscard]] static bool InDeferredOperation() noexcept;
[[nodiscard]] bool Active() const noexcept { return m_command.m_registers != nullptr; }
@@ -49,7 +48,6 @@ public:
[[nodiscard]] uint64_t CurrentTick() const noexcept { return m_master.CurrentTick(); }
[[nodiscard]] bool IsFree(uint64_t tick);
[[nodiscard]] MasterSemaphore& GetMasterSemaphore() noexcept { return m_master; }
[[nodiscard]] DynamicState& GetDynamicState() noexcept { return m_dynamic_state; }
[[nodiscard]] RenderContext& Context() const noexcept { return m_context; }
[[nodiscard]] GraphicContext& Graphics() const noexcept { return m_graphics; }
@@ -84,7 +82,6 @@ private:
void BeginNext();
void PriorityOperationsThread(std::stop_token stop);
void QueueOperation(Common::UniqueFunction<void>&& operation, bool priority);
void RunOperation(Common::UniqueFunction<void>&& operation);
MasterSemaphore m_master;
@@ -92,7 +89,6 @@ private:
GraphicContext& m_graphics;
CommandPool m_command_pool;
CommandBuffer m_command;
DynamicState m_dynamic_state;
std::queue<PendingOperation> m_pending_operations;
std::queue<PendingOperation> m_priority_operations;
std::mutex m_operation_mutex;
+16 -26
View File
@@ -394,20 +394,26 @@ static void RcPrint(const char* func, const HW::RenderControl& c) {
LOGF("\t depth_clear_enable = %s\n"
"\t stencil_clear_enable = %s\n"
"\t copy_depth_to_color = %s\n"
"\t copy_stencil_to_color = %s\n"
"\t resummarize_enable = %s\n"
"\t stencil_compress_disable = %s\n"
"\t depth_compress_disable = %s\n"
"\t copy_centroid = %s\n"
"\t copy_sample = %" PRIu8 "\n",
c.depth_clear_enable ? "true" : "false", c.stencil_clear_enable ? "true" : "false",
c.copy_depth_to_color ? "true" : "false", c.copy_stencil_to_color ? "true" : "false",
c.resummarize_enable ? "true" : "false", c.stencil_compress_disable ? "true" : "false",
c.depth_compress_disable ? "true" : "false", c.copy_centroid ? "true" : "false",
c.copy_sample);
}
static void RcCheck(const HW::RenderControl& c) {
// EXIT_NOT_IMPLEMENTED(c.depth_clear_enable != false);
// EXIT_NOT_IMPLEMENTED(c.stencil_clear_enable != false);
// EXIT_NOT_IMPLEMENTED(c.stencil_compress_disable != false);
// EXIT_NOT_IMPLEMENTED(c.depth_compress_disable != false);
EXIT_NOT_IMPLEMENTED(c.copy_centroid != false);
EXIT_NOT_IMPLEMENTED(c.copy_sample != 0);
}
static void McPrint(const char* func, const HW::ModeControl& c) {
LOGF("%s\n", func);
@@ -863,6 +869,9 @@ void hw_check(const CommandBuffer& buffer) {
VpCheck(vp, smc);
log_phase("clip");
ClipCheck(c);
log_phase("rc");
RcCheck(rc);
log_phase("depth");
log_phase("mode");
McCheck(mc);
log_phase("blend");
@@ -874,33 +883,14 @@ void hw_check(const CommandBuffer& buffer) {
log_phase("done");
if (graphics_debug_dump_enabled() && RenderTargetMaskHasBoundMrt(buffer)) {
const auto& sh = hw.GetShaderRegisters();
LOGF("MRT render target mask: 0x%08" PRIx32
" export_modes={%u,%u,%u,%u,%u,%u,%u,%u}\n",
hw.GetRenderTargetMask(), sh.target_output_mode[0], sh.target_output_mode[1],
sh.target_output_mode[2], sh.target_output_mode[3], sh.target_output_mode[4],
sh.target_output_mode[5], sh.target_output_mode[6], sh.target_output_mode[7]);
LOGF("MRT render target mask: 0x%08" PRIx32 "\n", hw.GetRenderTargetMask());
for (uint32_t i = 0; i < 8; i++) {
const auto& mrt = hw.GetRenderTarget(i);
const auto& bc = hw.GetBlendControl(i);
LOGF("\t RT%u addr=0x%010" PRIx64 " mask=0x%x fmt=0x%08" PRIx32
" type=%u order=%u shader_mask=0x%x"
" width=%u height=%u tile=%u blend=%u bypass=%u clamp=%u round_mode=%u"
" samples=%u fragments=%u dcc=%u dcc_addr=0x%010" PRIx64
" cmask_clear=%u cmask_addr=0x%010" PRIx64
" dcc_clear_key=%u clear={%08" PRIx32 ",%08" PRIx32 "}\n",
" width=%u height=%u tile=%u\n",
i, mrt.base.addr, (hw.GetRenderTargetMask() >> (i * 4u)) & 0x0fu,
static_cast<uint32_t>(mrt.info.format), static_cast<uint32_t>(mrt.info.channel_type),
static_cast<uint32_t>(mrt.info.channel_order), (sh.m_cbShaderMask >> (i * 4u)) & 0x0fu,
mrt.attrib2.width + 1, mrt.attrib2.height + 1,
static_cast<uint32_t>(mrt.attrib3.tile_mode), static_cast<uint32_t>(bc.enable),
static_cast<uint32_t>(mrt.info.blend_bypass), static_cast<uint32_t>(mrt.info.blend_clamp),
static_cast<uint32_t>(mrt.info.round_mode), mrt.attrib.num_samples,
mrt.attrib.num_fragments,
static_cast<uint32_t>(mrt.info.dcc_compression_enable), mrt.dcc_addr.addr,
static_cast<uint32_t>(mrt.info.cmask_fast_clear_enable), mrt.cmask.addr,
static_cast<uint32_t>(mrt.dcc.dcc_clear_key_enable), mrt.clear_word0.word0,
mrt.clear_word1.word1);
static_cast<uint32_t>(mrt.info.format), mrt.attrib2.width + 1,
mrt.attrib2.height + 1, static_cast<uint32_t>(mrt.attrib3.tile_mode));
}
}
if (rc.depth_clear_enable && hw.GetDepthClearValue() != 0.0f &&
@@ -160,7 +160,8 @@ static TextureCache::ImageDesc MakeDepthTargetDesc(const CommandBuffer& buffer,
z.depth_view.slice_max);
}
// EXPCLEAR permits an HTile acceleration state; the host attachment is already expanded.
if (z.z_info.max_mip_level != 0 ||
if (z.z_info.partially_resident ||
z.stencil_info.partially_resident || z.z_info.max_mip_level != 0 ||
z.depth_view.current_mip_level != 0 || unsupported_shading_rate_encoding ||
depth_address == 0 || (depth_address & 0xffffu) != 0) {
DepthFatal("unsupported depth register state");
@@ -177,6 +178,9 @@ static TextureCache::ImageDesc MakeDepthTargetDesc(const CommandBuffer& buffer,
if (z.htile_data_base_addr == 0 || (z.htile_data_base_addr & 0x7fffu) != 0) {
DepthFatal("invalid HTile metadata address");
}
if (z.depth_view.slice_max >= 32) {
DepthFatal("HTile clear tracking supports at most 32 slices");
}
}
if (!z.size.valid) {
DepthFatal("missing depth extent");
@@ -236,7 +240,7 @@ static TextureCache::ImageDesc MakeDepthTargetDesc(const CommandBuffer& buffer,
desc.info.bytes_per_block = bytes;
desc.info.samples = samples;
desc.info.tile_mode = Prospero::TileMode::kDepth;
desc.info.UpdateSize();
desc.info.mip_layout[0] = {0, depth_backing_size, pitch, height};
desc.info.metadata.range =
has_htile ? GuestRange {z.htile_data_base_addr, htile_backing_size} : GuestRange {};
desc.info.metadata.kind = has_htile ? ImageMetadataKind::Htile : ImageMetadataKind::None;
@@ -295,20 +299,13 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
}
return;
}
if (rc.copy_stencil_to_color ||
dc.zfunc > static_cast<uint8_t>(vk::CompareOp::eAlways) ||
if (rc.copy_depth_to_color || rc.copy_stencil_to_color || rc.copy_centroid ||
rc.copy_sample != 0 || dc.zfunc > static_cast<uint8_t>(vk::CompareOp::eAlways) ||
(!z.depth_view.depth_write_disable && z.z_write_base_addr != z.z_read_base_addr) ||
(has_stencil && !z.depth_view.stencil_write_disable &&
z.stencil_write_base_addr != z.stencil_read_base_addr)) {
DepthFatal("unsupported depth register state");
}
if (rc.copy_depth_to_color &&
(z.z_info.num_samples != 0 || rc.copy_sample != 0 ||
z.z_info.format != Prospero::DepthFormat::kZ32F ||
dc.z_enable || dc.z_write_enable || dc.depth_bounds_enable || dc.stencil_enable ||
rc.depth_clear_enable || rc.stencil_clear_enable)) {
DepthFatal("unsupported depth-to-color copy state");
}
r.desc = MakeDepthTargetDesc(buffer, z);
r.depth_clear_enable = rc.depth_clear_enable;
r.depth_load_clear_enable = r.depth_clear_enable;
@@ -319,6 +316,8 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
r.depth_compare_op = static_cast<vk::CompareOp>(dc.zfunc);
r.depth_bounds_test_enable = dc.depth_bounds_enable;
r.depth_min_bounds = hw.GetDepthBoundsMin();
r.depth_max_bounds = hw.GetDepthBoundsMax();
r.stencil_clear_enable =
has_stencil && rc.stencil_clear_enable && !z.depth_view.stencil_write_disable;
@@ -29,6 +29,8 @@ struct RenderDepthInfo {
bool depth_write_enable = false;
vk::CompareOp depth_compare_op = vk::CompareOp::eNever;
bool depth_bounds_test_enable = false;
float depth_min_bounds = 0.0f;
float depth_max_bounds = 0.0f;
bool stencil_clear_enable = false;
uint8_t stencil_clear_value = 0;
bool stencil_test_enable = false;

Some files were not shown because too many files have changed in this diff Show More