Compare commits

..

16 Commits

Author SHA1 Message Date
nmzik 00a5dd32c0 Implement syncOnAddress ABIs (perf improvement) 2026-08-04 21:24:12 +02:00
nmzik a9ddd44d73 CI - enable LTO, remove OPT:NOREF 2026-08-04 19:30:49 +02:00
nmzik 2f9fcc63c3 fix (agc): implement DCB context state operations
Co-authored-by: DeathWhispers <56944463+Death-Whispers-U@users.noreply.github.com>
2026-08-04 19:14:21 +02:00
nmzik 0bbd0bdd99 fix launcher regression 2026-08-04 03:35:30 +02:00
nmzik 6396dc9a85 feat(input): add host input remapping
Add compact keyboard and mouse mapping with analog-stick defaults and integrate a global remapping editor into the Qt launcher.

Based on the host input remapping work from PR #109.

Co-authored-by: DistantMyth <thetarun3@gmail.com>
2026-08-04 03:24:34 +02:00
nmzik a3a00464bc fix(launcher): focus emulator window on launch 2026-08-04 02:51:11 +02:00
nmzik 1da99a0671 Add warning for Windows-forbidden filename characters 2026-08-04 02:26:50 +02:00
nmzik 9304921132 add fullscreen mode 2026-08-04 02:26:50 +02:00
nmzik c13a1d752b renderer: remove duplicate vertex and index buffer rebind 2026-08-04 02:26:50 +02:00
nmzik 51d0e02543 perf: sync only GPU-dirty readbacks 2026-08-04 02:26:50 +02:00
Ali Almohaya e8752aca7e feat: identify build provenance in window title (#159)
* feat: identify build provenance in window title

* fix: propagate generated version dependency

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-08-04 01:04:10 +02:00
nmzik 7dd50ea127 change default flexible memory size 2026-08-03 18:06:43 +02:00
nmzik d09c81d1c6 Implement ftruncate abi 2026-08-03 08:18:23 +02:00
nmzik 26f0c7b0bf perf: reduce texture registration and lookup overhead 2026-08-03 08:18:05 +02:00
nmzik 2e9e2ae566 loader: preserve guest root stack frame 2026-08-03 04:02:35 +02:00
nmzik 53feb6cb19 graphics: log image descriptor specialization mismatches 2026-08-03 04:02:35 +02:00
67 changed files with 2769 additions and 887 deletions
+35 -6
View File
@@ -6,9 +6,27 @@ on:
branches: [ main, master ]
pull_request:
env:
KYTY_BUILD_ORIGIN: ${{ github.repository != 'KytyPS5/KytyPS5' && 'Fork' || github.event_name == 'push' && 'Official' || 'Source' }}
KYTY_BUILD_REPOSITORY: ${{ github.repository }}
jobs:
metadata:
name: Prepare build metadata
runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.release_tag.outputs.value }}
steps:
- name: Generate release tag
id: release_tag
shell: bash
run: |
echo "value=KytyPS5-$(date -u +'%Y-%m-%d')-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
windows:
name: Build KytyPS5 (Windows)
needs: metadata
runs-on: windows-2022
steps:
@@ -76,6 +94,10 @@ jobs:
cmake -S . -B _Build/windows ^
-G Ninja ^
-DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON ^
-DKYTY_BUILD_ORIGIN=%KYTY_BUILD_ORIGIN% ^
-DKYTY_BUILD_REPOSITORY=%KYTY_BUILD_REPOSITORY% ^
-DKYTY_RELEASE_TAG=${{ needs.metadata.outputs.release_tag }} ^
-DCMAKE_C_COMPILER=clang-cl ^
-DCMAKE_CXX_COMPILER=clang-cl ^
-DCMAKE_PREFIX_PATH="%Qt6_DIR%"
@@ -104,6 +126,7 @@ jobs:
macos:
name: Build KytyPS5 (macOS)
needs: metadata
runs-on: macos-15
steps:
@@ -150,6 +173,10 @@ jobs:
cmake -S . -B _Build/macos \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \
-DKYTY_BUILD_ORIGIN="$KYTY_BUILD_ORIGIN" \
-DKYTY_BUILD_REPOSITORY="$KYTY_BUILD_REPOSITORY" \
-DKYTY_RELEASE_TAG="${{ needs.metadata.outputs.release_tag }}" \
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
@@ -213,6 +240,7 @@ jobs:
linux:
name: Build KytyPS5 (Linux)
needs: metadata
runs-on: ubuntu-24.04
steps:
@@ -273,6 +301,10 @@ jobs:
cmake -S . -B _Build/linux \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \
-DKYTY_BUILD_ORIGIN="$KYTY_BUILD_ORIGIN" \
-DKYTY_BUILD_REPOSITORY="$KYTY_BUILD_REPOSITORY" \
-DKYTY_RELEASE_TAG="${{ needs.metadata.outputs.release_tag }}" \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_PREFIX_PATH="$Qt6_DIR" 2>&1 | tee _Build/configure.log
@@ -342,10 +374,12 @@ jobs:
release:
name: Release KytyPS5
if: github.event_name == 'push' && github.repository == 'KytyPS5/KytyPS5'
needs: [windows, macos, linux]
needs: [metadata, windows, macos, linux]
runs-on: ubuntu-latest
permissions:
contents: write
env:
RELEASE_NAME: ${{ needs.metadata.outputs.release_tag }}
steps:
- name: Download builds
@@ -353,11 +387,6 @@ jobs:
with:
path: artifacts
- name: Set release name
shell: bash
run: |
echo "RELEASE_NAME=KytyPS5-$(date -u +'%Y-%m-%d')-${GITHUB_SHA::7}" >> "$GITHUB_ENV"
- name: Package builds
shell: bash
run: |
+1 -1
+52 -10
View File
@@ -4,6 +4,12 @@ if(CMAKE_VERSION VERSION_GREATER_EQUAL 4.0)
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)
if(NOT KYTY_GENERATOR_IS_MULTI_CONFIG AND NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release RelWithDebInfo MinSizeRel)
endif()
project(Kyty)
set(KYTY_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
@@ -28,6 +34,29 @@ include(CTest)
option(KYTY_ENABLE_CLANG_TIDY "Run clang-tidy checks during builds" OFF)
set(KYTY_BUILD_ORIGIN "Source" CACHE STRING "Build provenance shown in diagnostics")
set_property(CACHE KYTY_BUILD_ORIGIN PROPERTY STRINGS Source Official Fork)
if(NOT KYTY_BUILD_ORIGIN STREQUAL "Source" AND NOT KYTY_BUILD_ORIGIN STREQUAL "Official" AND
NOT KYTY_BUILD_ORIGIN STREQUAL "Fork")
message(FATAL_ERROR "KYTY_BUILD_ORIGIN must be Source, Official, or Fork")
endif()
set(KYTY_RELEASE_TAG "" CACHE STRING "Release tag embedded in official builds")
set(KYTY_BUILD_REPOSITORY "" CACHE STRING "GitHub repository embedded in fork builds")
set(KYTY_OFFICIAL_BUILD OFF)
set(KYTY_FORK_BUILD OFF)
if(KYTY_BUILD_ORIGIN STREQUAL "Official")
if(KYTY_RELEASE_TAG STREQUAL "")
message(FATAL_ERROR "Official builds require KYTY_RELEASE_TAG")
endif()
set(KYTY_OFFICIAL_BUILD ON)
elseif(KYTY_BUILD_ORIGIN STREQUAL "Fork")
if(KYTY_BUILD_REPOSITORY STREQUAL "")
message(FATAL_ERROR "Fork builds require KYTY_BUILD_REPOSITORY")
endif()
set(KYTY_FORK_BUILD ON)
endif()
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
include(TestBigEndian)
@@ -117,16 +146,20 @@ configure_file(
find_package(Git)
add_custom_target( KytyGitVersion
COMMAND ${CMAKE_COMMAND}
-D INPUT_FILE=${KYTY_SOURCE_DIR}/kytyGitVersion.h.in
-D OUTPUT_FILE=${PROJECT_BINARY_DIR}/kytyGitVersion.h
-D GIT_EXECUTABLE=${GIT_EXECUTABLE}
-D GIT_WORKING_DIRECTORY=${CMAKE_CURRENT_SOURCE_DIR}
-P ${KYTY_SOURCE_DIR}/generate_version.cmake
add_custom_target(KytyGitVersion
COMMAND ${CMAKE_COMMAND}
-D INPUT_FILE=${KYTY_SOURCE_DIR}/kytyGitVersion.h.in
-D OUTPUT_FILE=${PROJECT_BINARY_DIR}/kytyGitVersion.h
-D GIT_EXECUTABLE=${GIT_EXECUTABLE}
-D GIT_WORKING_DIRECTORY=${CMAKE_CURRENT_SOURCE_DIR}
-P ${KYTY_SOURCE_DIR}/generate_version.cmake
BYPRODUCTS ${PROJECT_BINARY_DIR}/kytyGitVersion.h
COMMENT "Generate kytyGitVersion.h"
)
add_library(kyty_git_version INTERFACE)
add_dependencies(kyty_git_version KytyGitVersion)
if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0.0)
list(APPEND KYTY_IWYU
kyty_emulator
@@ -259,7 +292,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 SDL2-static xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9)
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL2-static xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version)
# Linux system libraries required by the static FFmpeg archive.
if(LINUX)
@@ -417,6 +450,13 @@ add_executable(event_queue_lifetime_tests EXCLUDE_FROM_ALL
target_link_libraries(event_queue_lifetime_tests common fmt::fmt)
target_include_directories(event_queue_lifetime_tests PRIVATE ${inc_headers})
add_executable(sync_on_address_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/SyncOnAddressTests.cpp"
"${KYTY_SOURCE_DIR}/kernel/syncOnAddress.cpp"
)
target_link_libraries(sync_on_address_tests common)
target_include_directories(sync_on_address_tests PRIVATE ${inc_headers})
add_executable(image_page_table_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/ImagePageTableTests.cpp"
)
@@ -470,6 +510,7 @@ if(BUILD_TESTING)
add_test(NAME resource_tracking COMMAND $<TARGET_FILE:resource_tracking_tests>)
add_test(NAME resource_mutex COMMAND $<TARGET_FILE:resource_mutex_tests>)
add_test(NAME event_queue_lifetime COMMAND $<TARGET_FILE:event_queue_lifetime_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 shader_recompiler_compute COMMAND $<TARGET_FILE:shader_recompiler_compute_tests>)
add_test(NAME virtual_memory_allocation
@@ -480,6 +521,8 @@ if(BUILD_TESTING)
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --stream-buffer-only)
add_test(NAME gpu_command_lane
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --gpu-command-lane-only)
add_test(NAME pm4_context_state
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)
@@ -516,6 +559,7 @@ if(BUILD_TESTING)
resource_tracking_tests
resource_mutex_tests
event_queue_lifetime_tests
sync_on_address_tests
shader_recompiler_compute_tests
virtual_memory_allocation_tests
)
@@ -563,8 +607,6 @@ elseif(WIN32 OR LINUX)
set_target_properties(kyty_emulator PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS} -Wl,-Map=${KYTY_EMULATOR_MAP_LINK_PATH}")
endif()
add_dependencies(kyty_emulator KytyGitVersion)
if(APPLE)
# The emulator writes x86-64 trampolines/PLT stubs into memory and executes them, and
# runs guest code under Rosetta. That requires the JIT / unsigned-executable-memory
+4
View File
@@ -1,4 +1,8 @@
#define KYTY_VERSION "@KYTY_VERSION@"
#define KYTY_BUILD_REPOSITORY "@KYTY_BUILD_REPOSITORY@"
#define KYTY_RELEASE_TAG "@KYTY_RELEASE_TAG@"
#cmakedefine KYTY_OFFICIAL_BUILD
#cmakedefine KYTY_FORK_BUILD
#define KYTY_ENDIAN @KYTY_ENDIAN@
#define KYTY_PLATFORM @KYTY_PLATFORM@
#define KYTY_COMPILER KYTY_COMPILER_@KYTY_COMPILER@
+8
View File
@@ -33,6 +33,10 @@ uint32_t GetScreenHeight() {
return g_config->screen_height;
}
bool FullscreenEnabled() {
return g_config->fullscreen_enabled;
}
uint32_t GetVblankFrequency() {
return std::clamp(g_config->vblank_frequency, 30u, 360u);
}
@@ -97,4 +101,8 @@ bool ReadbackLinearImagesEnabled() {
return g_config->readback_linear_images;
}
const Keymap& GetKeymap() {
return g_config->keymap;
}
} // namespace Config
+9
View File
@@ -5,6 +5,8 @@
#include "common/subsystems.h"
#include <filesystem>
#include <string>
#include <vector>
namespace Config {
@@ -18,12 +20,15 @@ enum class ProfilerDirection { None, Network };
enum class OutputDirection { Silent, Console, File };
using Keymap = std::vector<std::string>;
constexpr uint32_t DEFAULT_CONSOLE_LANGUAGE = 1;
constexpr uint32_t MAX_CONSOLE_LANGUAGE = 29;
struct ConfigOptions {
uint32_t screen_width = 1280;
uint32_t screen_height = 720;
bool fullscreen_enabled = false;
uint32_t vblank_frequency = 60;
uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@@ -40,12 +45,14 @@ struct ConfigOptions {
bool spirv_debug_printf_enabled = false;
bool renderdoc_enabled = false;
bool readback_linear_images = false;
Keymap keymap;
};
void Load(const ConfigOptions& cfg);
uint32_t GetScreenWidth();
uint32_t GetScreenHeight();
bool FullscreenEnabled();
uint32_t GetVblankFrequency();
uint32_t GetConsoleLanguage();
bool VulkanValidationEnabled();
@@ -70,6 +77,8 @@ bool SpirvDebugPrintfEnabled();
bool RenderDocEnabled();
bool ReadbackLinearImagesEnabled();
const Keymap& GetKeymap();
} // namespace Config
#endif /* KYTY_COMMON_EMULATOR_CONFIG_H_ */
+24 -1
View File
@@ -1,7 +1,8 @@
set(KYTY_GIT_VERSION "unknown")
set(KYTY_GIT_HASH "unknown")
if(GIT_EXECUTABLE)
execute_process(
COMMAND "${GIT_EXECUTABLE}" describe --tags --always
COMMAND "${GIT_EXECUTABLE}" describe --tags --always --dirty
WORKING_DIRECTORY "${GIT_WORKING_DIRECTORY}"
OUTPUT_VARIABLE KYTY_GIT_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
@@ -11,5 +12,27 @@ if(GIT_EXECUTABLE)
if(NOT GIT_RESULT EQUAL 0)
set(KYTY_GIT_VERSION "unknown")
endif()
execute_process(
COMMAND "${GIT_EXECUTABLE}" rev-parse --short=7 HEAD
WORKING_DIRECTORY "${GIT_WORKING_DIRECTORY}"
OUTPUT_VARIABLE KYTY_GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE GIT_HASH_RESULT
ERROR_QUIET
)
if(NOT GIT_HASH_RESULT EQUAL 0)
set(KYTY_GIT_HASH "unknown")
else()
execute_process(
COMMAND "${GIT_EXECUTABLE}" diff-index --quiet HEAD --
WORKING_DIRECTORY "${GIT_WORKING_DIRECTORY}"
RESULT_VARIABLE GIT_DIRTY_RESULT
ERROR_QUIET
)
if(NOT GIT_DIRTY_RESULT EQUAL 0)
string(APPEND KYTY_GIT_HASH "-dirty")
endif()
endif()
endif()
configure_file("${INPUT_FILE}" "${OUTPUT_FILE}")
@@ -15,6 +15,13 @@ bool TestWaitRegMemValue(uint64_t value, uint64_t ref, uint64_t mask, uint32_t f
enum class Pm4ProcessResult { Complete, Blocked };
enum class ContextStateOperation : uint32_t {
Clear = 0,
Push = 1,
Pop = 2,
PushClear = 3,
};
class Pm4Execution {
public:
[[nodiscard]] bool MadeProgress() const noexcept { return m_made_progress; }
@@ -49,24 +56,12 @@ public:
KYTY_CLASS_NO_COPY(CommandProcessor);
void Reset();
void ApplyContextStateOperation(ContextStateOperation operation);
void BufferInit();
void BufferFlush();
void BufferFlushAndWait();
void BufferWait();
void BeginReadbackTransaction() {
if (m_readback_active) {
EXIT("nested command-processor readback transaction\n");
}
m_readback_active = true;
}
void EndReadbackTransaction() {
if (!m_readback_active) {
EXIT("command-processor readback transaction is not active\n");
}
m_readback_active = false;
}
void BufferInit();
void BufferFlush();
void BufferFlushAndWait();
void BufferWait();
HW::Context& GetCtx() { return m_ctx; }
HW::UserConfig& GetUcfg() { return m_ucfg; }
HW::Shader& GetShCtx() { return m_sh_ctx; }
@@ -162,6 +157,8 @@ private:
RenderContext& m_renderer;
HW::Context m_ctx;
HW::Context m_saved_ctx;
bool m_context_state_pushed = false;
HW::UserConfig m_ucfg;
HW::Shader m_sh_ctx;
HW::UserSgprType m_user_data_marker = HW::UserSgprType::Unknown;
@@ -173,10 +170,9 @@ private:
// Persistent draw state: indirect draws update it for subsequent draws.
uint32_t m_num_instances = 1;
uint32_t m_de_count = 0;
uint32_t m_ce_count = 0;
bool m_ce_complete = false;
bool m_readback_active = false;
uint32_t m_de_count = 0;
uint32_t m_ce_count = 0;
bool m_ce_complete = false;
uint32_t m_const_ram[0x3000] = {0};
@@ -254,6 +254,7 @@ constexpr auto MakeCustomOpcodeDispatchTable() {
g_cp_op_custom_func[Pm4::R_WAIT_MEM_64] = CpOpWaitRegMem64;
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;
return g_cp_op_custom_func;
}
@@ -121,6 +121,7 @@ uint32_t HwUcSetGeIndexOffset(CommandProcessor&, uint32_t, uint32_t, const uint3
uint32_t HwUcSetGdsOaRegisters(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
uint32_t CpOpNop(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpContextState(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpSetBase(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpClearState(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpIndexBase(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
@@ -2107,6 +2107,19 @@ KYTY_CP_OP_PARSER(CpOpClearState) {
return 1;
}
KYTY_CP_OP_PARSER(CpOpContextState) {
KYTY_PROFILER_FUNCTION();
const auto packet_size_dw = KYTY_PM4_LEN(cmd_id);
EXIT_NOT_IMPLEMENTED(KYTY_PM4_R(cmd_id) != Pm4::R_CONTEXT_STATE);
EXIT_NOT_IMPLEMENTED(packet_size_dw != 3u && packet_size_dw != 5u);
EXIT_NOT_IMPLEMENTED(buffer[0] > static_cast<uint32_t>(ContextStateOperation::PushClear));
cp.ApplyContextStateOperation(static_cast<ContextStateOperation>(buffer[0]));
return packet_size_dw - 1u;
}
KYTY_CP_OP_PARSER(CpOpDumpConstRam) {
KYTY_PROFILER_FUNCTION();
+59 -26
View File
@@ -36,6 +36,7 @@ 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 GpuState* g_gpu_state = nullptr;
class GpuMutexLock final {
public:
@@ -98,11 +99,10 @@ public:
void SubmitFlipPreparation(uint64_t request_id);
void Done();
void Shutdown();
[[nodiscard]] bool IsStopping();
void SendCommand(Common::UniqueFunction<void>&& command);
void SendCommandSync(Common::UniqueFunction<void>&& command);
void SendCommandSyncWithProcessor(Common::UniqueFunction<void, CommandProcessor&>&& command);
int GetFrameNum();
[[nodiscard]] bool IsStopping();
void SendCommand(Common::UniqueFunction<void>&& command);
void SendCommandSync(Common::UniqueFunction<void>&& command);
int GetFrameNum();
[[nodiscard]] static bool IsGpuThread() noexcept { return g_gpu_thread; }
private:
@@ -127,6 +127,7 @@ private:
void WaitLocked();
void Enqueue(Submission submission);
void WaitForIdle();
void ProcessCommands();
bool Process(Submission& submission);
static void ThreadRun(void* data);
CommandProcessor& GetProcessor(uint32_t queue_id);
@@ -139,6 +140,7 @@ private:
Common::CondVar m_idle;
std::array<std::deque<Submission>, QueueCount> m_queues;
std::deque<Common::UniqueFunction<void>> m_commands;
std::atomic_uint32_t m_pending_commands {0};
uint32_t m_next_queue = 0;
uint32_t m_submission_count = 0;
bool m_processing = false;
@@ -153,6 +155,8 @@ private:
uint64_t m_submit_id = 0;
std::atomic_int m_done_num = 0;
std::jthread m_thread;
friend class CommandProcessor;
};
static bool GraphicsRunDebugDumpEnabled() {
@@ -195,9 +199,25 @@ void GpuState::SendCommand(Common::UniqueFunction<void>&& command) {
Common::LockGuard lock(m_queue_mutex);
EXIT_IF(!m_accepting);
m_commands.push_back(std::move(command));
m_pending_commands.fetch_add(1, std::memory_order_release);
m_work_available.Signal();
}
void GpuState::ProcessCommands() {
EXIT_IF(!IsGpuThread());
while (m_pending_commands.load(std::memory_order_acquire) != 0) {
Common::UniqueFunction<void> command;
{
Common::LockGuard lock(m_queue_mutex);
EXIT_IF(m_commands.empty());
command = std::move(m_commands.front());
m_commands.pop_front();
EXIT_IF(m_pending_commands.fetch_sub(1, std::memory_order_acq_rel) == 0);
}
command();
}
}
void GpuState::SendCommandSync(Common::UniqueFunction<void>&& command) {
EXIT_IF(!command);
if (IsGpuThread()) {
@@ -212,17 +232,6 @@ void GpuState::SendCommandSync(Common::UniqueFunction<void>&& command) {
done.acquire();
}
void GpuState::SendCommandSyncWithProcessor(
Common::UniqueFunction<void, CommandProcessor&>&& command) {
EXIT_IF(!command);
SendCommandSync([this, operation = std::move(command)]() mutable {
EXIT_IF(g_current_processor != nullptr);
g_current_processor = m_gfx_cp.get();
operation(*m_gfx_cp);
g_current_processor = nullptr;
});
}
void GpuState::Submit(uint32_t* cmd_draw_buffer, uint32_t num_draw_dw, uint32_t* cmd_const_buffer,
uint32_t num_const_dw, bool trigger_agc_interrupt_on_done) {
GpuMutexLock lock(m_submission_mutex);
@@ -303,6 +312,8 @@ void CommandProcessor::Reset() {
m_sh_ctx.Reset();
m_ucfg.Reset();
m_ctx.Reset();
m_saved_ctx.Reset();
m_context_state_pushed = false;
m_index_type_and_size = 0;
m_index_buffer_size = 0;
m_user_data_marker = HW::UserSgprType::Unknown;
@@ -312,6 +323,30 @@ void CommandProcessor::Reset() {
std::memset(m_const_ram, 0, sizeof(m_const_ram));
}
void CommandProcessor::ApplyContextStateOperation(ContextStateOperation operation) {
switch (operation) {
case ContextStateOperation::Clear: m_ctx.Reset(); break;
case ContextStateOperation::Push:
EXIT_IF(m_context_state_pushed);
m_saved_ctx = m_ctx;
m_context_state_pushed = true;
break;
case ContextStateOperation::Pop:
EXIT_IF(!m_context_state_pushed);
m_ctx = m_saved_ctx;
m_saved_ctx = {};
m_context_state_pushed = false;
break;
case ContextStateOperation::PushClear:
EXIT_IF(m_context_state_pushed);
m_saved_ctx = m_ctx;
m_context_state_pushed = true;
m_ctx.Reset();
break;
default: EXIT("unknown context state operation: %u\n", static_cast<uint32_t>(operation));
}
}
void CommandProcessor::BufferInit() {
GetScheduler().Begin(m_ctx, m_ucfg, m_sh_ctx);
}
@@ -509,6 +544,7 @@ void GpuState::ThreadRun(void* data) {
EXIT_IF(gpu == nullptr);
KYTY_PROFILER_THREAD("Thread_Gpu");
g_gpu_thread = true;
g_gpu_state = gpu;
for (;;) {
Submission submission;
@@ -529,6 +565,7 @@ void GpuState::ThreadRun(void* data) {
} else if (!gpu->m_commands.empty()) {
command = std::move(gpu->m_commands.front());
gpu->m_commands.pop_front();
EXIT_IF(gpu->m_pending_commands.fetch_sub(1, std::memory_order_acq_rel) == 0);
gpu->m_processing = true;
} else {
int selected_queue = -1;
@@ -560,6 +597,7 @@ void GpuState::ThreadRun(void* data) {
}
if (should_stop) {
gpu->m_gfx_cp->BufferWait();
g_gpu_state = nullptr;
g_gpu_thread = false;
return;
}
@@ -741,6 +779,9 @@ void CommandProcessor::SuspendPm4() {
void CommandProcessor::ProcessPm4(Pm4Execution& execution, size_t stop_depth) {
while (execution.m_buffer_stack.size() > stop_depth) {
if (g_gpu_state != nullptr) {
g_gpu_state->ProcessCommands();
}
const auto buffer_index = execution.m_buffer_stack.size() - 1;
auto& cursor = execution.m_buffer_stack[buffer_index];
if (cursor.deferred_advance_dw != 0) {
@@ -1650,10 +1691,6 @@ void Gpu::SendCommandSync(Common::UniqueFunction<void>&& command) {
m_state->SendCommandSync(std::move(command));
}
void Gpu::SendCommandSyncWithProcessor(Common::UniqueFunction<void, CommandProcessor&>&& command) {
m_state->SendCommandSyncWithProcessor(std::move(command));
}
void Gpu::Submit(uint32_t* draw_commands, uint32_t draw_size_dw, uint32_t* constant_commands,
uint32_t constant_size_dw, bool trigger_agc_interrupt_on_done) {
EXIT_IF(draw_commands == nullptr || draw_size_dw == 0);
@@ -1679,12 +1716,8 @@ int Gpu::GetFrameNum() const {
return m_state->GetFrameNum();
}
bool Gpu::IsCommandProcessorThread() noexcept {
return g_current_processor != nullptr;
}
CommandProcessor* Gpu::CurrentCommandProcessor() noexcept {
return g_current_processor;
bool Gpu::IsGpuThread() noexcept {
return GpuState::IsGpuThread();
}
} // namespace Libs::Graphics
+1 -4
View File
@@ -9,7 +9,6 @@
namespace Libs::Graphics {
class CommandProcessor;
class GpuState;
class RenderContext;
@@ -23,7 +22,6 @@ public:
[[nodiscard]] bool IsStopping();
void SendCommand(Common::UniqueFunction<void>&& command);
void SendCommandSync(Common::UniqueFunction<void>&& command);
void SendCommandSyncWithProcessor(Common::UniqueFunction<void, CommandProcessor&>&& command);
void Submit(uint32_t* draw_commands, uint32_t draw_size_dw, uint32_t* constant_commands,
uint32_t constant_size_dw, bool trigger_agc_interrupt_on_done = false);
@@ -33,8 +31,7 @@ public:
void Done();
[[nodiscard]] int GetFrameNum() const;
[[nodiscard]] static bool IsCommandProcessorThread() noexcept;
[[nodiscard]] static CommandProcessor* CurrentCommandProcessor() noexcept;
[[nodiscard]] static bool IsGpuThread() noexcept;
private:
std::unique_ptr<GpuState> m_state;
+1
View File
@@ -30,6 +30,7 @@ constexpr auto MakeRegisterNames() {
names[R_FLIP] = "R_FLIP";
names[R_RELEASE_MEM] = "R_RELEASE_MEM";
names[R_DMA_DATA] = "R_DMA_DATA";
names[R_CONTEXT_STATE] = "R_CONTEXT_STATE";
return names;
}
+1
View File
@@ -87,6 +87,7 @@ constexpr uint32_t R_WAIT_MEM_64 = 0x16;
constexpr uint32_t R_FLIP = 0x17;
constexpr uint32_t R_RELEASE_MEM = 0x18;
constexpr uint32_t R_DMA_DATA = 0x19;
constexpr uint32_t R_CONTEXT_STATE = 0x1A;
constexpr uint32_t R_NUM = 0x3F + 1;
+17 -33
View File
@@ -29,45 +29,29 @@ 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);
// 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) {
void InvalidateRegion(uint64_t vaddr, uint64_t size, Flush&& on_flush) noexcept {
static_assert(std::is_invocable_v<Flush&>);
CheckNotInUploadCallback();
ValidateRange(vaddr, size);
const auto update_cpu_state = [this, vaddr, size] {
std::lock_guard access(m_access_mutex);
std::vector<RegionManager*> managers;
Iterate<false>(vaddr, size, [&](RegionManager* manager, uint64_t, uint64_t) {
managers.push_back(manager);
});
std::vector<std::unique_lock<TrackingSpinLock>> locks;
locks.reserve(managers.size());
for (auto* manager: managers) {
locks.emplace_back(manager->lock);
Iterate<false>(vaddr, size, [&](RegionManager* manager, uint64_t offset, uint64_t bytes) {
const bool should_flush = [&] {
// Perform both the GPU modification check and CPU state change with the lock in
// case the GPU thread is racing to mark the page modified. If a flush is needed,
// on_flush performs the CPU state change.
std::scoped_lock lock(manager->lock);
if (manager->IsModified<DirtySource::Gpu>(offset, bytes)) {
return true;
}
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
return false;
}();
if (should_flush) {
on_flush();
}
const bool gpu_modified = Iterate<false>(
vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
return manager->IsModified<DirtySource::Gpu>(offset, bytes);
});
if (gpu_modified) {
return true;
}
Iterate<false>(vaddr, size,
[](RegionManager* manager, uint64_t offset, uint64_t bytes) {
manager->ChangeState<DirtySource::Cpu, true>(
manager->GetCpuAddr() + offset, bytes);
});
return false;
};
if (!update_cpu_state()) {
return;
}
std::forward<Flush>(on_flush)();
if (update_cpu_state()) {
EXIT("memory invalidation retained GPU-owned pages\n");
}
});
}
#if KYTY_BUILD == KYTY_BUILD_DEBUG
void ValidateGpuDirtyPages(const RangeSet& dirty, uint64_t vaddr, uint64_t size,
+37 -5
View File
@@ -3,15 +3,18 @@
#include "common/assert.h"
#include "common/logging/log.h"
#include "common/profiler.h"
#include "graphics/guest_gpu/graphicsRun.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/cache/resourceMutex.h"
#include "graphics/host_gpu/renderer/cache/textureCache.h"
#include "graphics/host_gpu/renderer/commandScheduler.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "kernel/memory.h"
#include <algorithm>
#include <array>
#include <cinttypes>
#include <cstring>
#include <utility>
#include <vector>
@@ -288,10 +291,29 @@ void BufferCache::InvalidateMemory(uint64_t vaddr, uint64_t size) {
return;
}
m_memory_tracker.InvalidateRegion(vaddr, size,
[this, vaddr, size] { ReadMemory(vaddr, size); });
[this, vaddr, size] { ReadMemory(vaddr, size, true); });
}
void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size) {
void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size, bool is_write) {
if (Gpu::IsGpuThread()) {
ReadMemoryOnGpu(vaddr, size, is_write);
return;
}
if (CommandScheduler::InDeferredOperation()) {
EXIT("unsupported buffer readback from an asynchronous GPU completion, "
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
vaddr, size);
}
if (m_resource_mutex.IsOwnedByCurrentThread()) {
EXIT("unsupported buffer readback from a pre-owned resource transaction, "
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
vaddr, size);
}
m_scheduler.Context().GetGpu().SendCommandSync(
[this, vaddr, size, is_write] { ReadMemoryOnGpu(vaddr, size, is_write); });
}
void BufferCache::ReadMemoryOnGpu(uint64_t vaddr, uint64_t size, bool is_write) {
std::vector<DownloadCopy> copies;
{
FaultSafeCacheLock lock(this, m_mutex);
@@ -322,9 +344,16 @@ void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size) {
}
}
});
}
if (copies.empty()) {
return;
if (copies.empty()) {
if (!is_write) {
return;
}
// A preceding read fault can consume the last GPU-owned copy after this write
// invalidation has already chosen to flush. Complete the CPU ownership handoff even
// though this callback no longer has bytes to download.
m_memory_tracker.MarkRegionAsCpuModified(vaddr, size);
return;
}
}
auto downloads = RecordDownloads(copies);
m_scheduler.FinishCurrent();
@@ -336,6 +365,9 @@ void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size) {
}
// The enumeration above covered whole dirty pages and every exact interval on them.
m_memory_tracker.UnmarkRegionAsGpuModified(vaddr, size);
if (is_write) {
m_memory_tracker.MarkRegionAsCpuModified(vaddr, size);
}
}
}
+2 -1
View File
@@ -46,7 +46,7 @@ public:
KYTY_CLASS_NO_COPY(BufferCache);
void InvalidateMemory(uint64_t vaddr, uint64_t size);
void ReadMemory(uint64_t vaddr, uint64_t size);
void ReadMemory(uint64_t vaddr, uint64_t size, bool is_write = false);
void UnmapMemory(uint64_t vaddr, uint64_t size);
[[nodiscard]] BufferBinding ObtainBuffer(CommandBuffer& command, uint64_t vaddr, uint64_t size,
bool is_written = false, bool is_read = true,
@@ -97,6 +97,7 @@ private:
void PublishDownloads(std::span<const DownloadRange> downloads);
void QueueGarbageDownload(std::span<const DownloadCopy> copies, RetiredBuffer retire);
void WriteHostMemory(uint64_t vaddr, std::span<const uint8_t> data);
void ReadMemoryOnGpu(uint64_t vaddr, uint64_t size, bool is_write);
GraphicContext& m_graphics;
CommandScheduler& m_scheduler;
+8 -57
View File
@@ -1,7 +1,6 @@
#include "graphics/host_gpu/renderer/cache/gpuResourceManager.h"
#include "common/assert.h"
#include "graphics/guest_gpu/command_processor/commandProcessor.h"
#include "graphics/guest_gpu/graphicsRun.h"
#include "graphics/host_gpu/renderer/commandScheduler.h"
namespace Libs::Graphics {
@@ -18,69 +17,21 @@ bool GpuResourceManager::HandleFault(PageFaultAccess access, uint64_t fault_vadd
if (!IsMapped(fault_vaddr, fault_size)) {
return false;
}
if (CommandScheduler::InDeferredOperation()) {
EXIT("unsupported guest-memory fault from an asynchronous GPU completion, "
"addr=0x%016" PRIx64 " access=%u\n",
fault_vaddr, static_cast<uint32_t>(access));
if (access == PageFaultAccess::Write) {
m_buffer_cache.InvalidateMemory(fault_vaddr, fault_size);
m_texture_cache.InvalidateMemory(fault_vaddr, fault_size);
} else {
m_buffer_cache.ReadMemory(fault_vaddr, fault_size);
}
bool handled = false;
const auto resolve = [this, access, fault_vaddr, &handled](CommandProcessor& cp) {
cp.BeginReadbackTransaction();
{
ResourceMutex::FaultScope fault(m_resource_mutex);
if (access == PageFaultAccess::Write) {
m_buffer_cache.InvalidateMemory(fault_vaddr, fault_size);
m_texture_cache.InvalidateMemory(fault_vaddr, fault_size);
} else {
m_buffer_cache.ReadMemory(fault_vaddr, fault_size);
}
handled = true;
}
cp.EndReadbackTransaction();
};
if (auto* cp = Gpu::CurrentCommandProcessor(); cp != nullptr) {
resolve(*cp);
return handled;
}
if (m_resource_mutex.IsOwnedByCurrentThread()) {
EXIT("unsupported page fault from a pre-owned resource transaction, addr=0x%016" PRIx64
" access=%u\n",
fault_vaddr, static_cast<uint32_t>(access));
}
EXIT_IF(m_gpu == nullptr);
m_gpu->SendCommandSyncWithProcessor(resolve);
return handled;
return true;
}
bool GpuResourceManager::InvalidateMemory(uint64_t vaddr, uint64_t size) {
if (!IsMapped(vaddr, size)) {
return false;
}
if (CommandScheduler::InDeferredOperation()) {
EXIT("unsupported memory invalidation from an asynchronous GPU completion, "
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
vaddr, size);
}
const auto resolve = [this, vaddr, size](CommandProcessor& cp) {
cp.BeginReadbackTransaction();
{
ResourceMutex::FaultScope fault(m_resource_mutex);
m_buffer_cache.InvalidateMemory(vaddr, size);
m_texture_cache.InvalidateMemory(vaddr, size);
}
cp.EndReadbackTransaction();
};
if (auto* cp = Gpu::CurrentCommandProcessor(); cp != nullptr) {
resolve(*cp);
return true;
}
if (m_resource_mutex.IsOwnedByCurrentThread()) {
EXIT("unsupported memory invalidation from a pre-owned resource transaction, "
"addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
vaddr, size);
}
EXIT_IF(m_gpu == nullptr);
m_gpu->SendCommandSyncWithProcessor(resolve);
m_buffer_cache.InvalidateMemory(vaddr, size);
m_texture_cache.InvalidateMemory(vaddr, size);
return true;
}
+154 -242
View File
@@ -7,8 +7,9 @@
#include <array>
#include <cstddef>
#include <cstdint>
#include <list>
#include <cstring>
#include <memory>
#include <type_traits>
#include <utility>
#include <vector>
@@ -103,6 +104,158 @@ private:
size_t m_allocated_buckets = 0;
};
// Inline owner storage for page-table entries. Texture pages normally have only a
// handful of owners, so avoid a heap allocation on the first 16 registrations.
//
// Owners are packed into live uint64_t objects instead of being constructed in
// raw storage. This keeps sparse bucket construction cheap without relying on
// subtle implicit-lifetime or pointer-provenance rules.
template <typename OwnerT, size_t InlineCapacity = 16>
class InlinePageOwnerList final {
public:
using value_type = OwnerT;
using size_type = size_t;
class const_iterator final {
public:
[[nodiscard]] OwnerT operator*() const noexcept { return m_owners->At(m_index); }
const_iterator& operator++() noexcept {
++m_index;
return *this;
}
bool operator==(const const_iterator&) const = default;
private:
friend class InlinePageOwnerList;
const_iterator(const InlinePageOwnerList* owners, size_type index) noexcept
: m_owners(owners), m_index(index) {}
const InlinePageOwnerList* m_owners = nullptr;
size_type m_index = 0;
};
static_assert(InlineCapacity > 0);
static_assert(std::is_default_constructible_v<OwnerT>);
static_assert(std::is_trivially_copyable_v<OwnerT>);
static_assert(std::has_unique_object_representations_v<OwnerT>);
static_assert(sizeof(OwnerT) <= sizeof(uint64_t));
InlinePageOwnerList() noexcept {}
InlinePageOwnerList(const InlinePageOwnerList&) = delete;
InlinePageOwnerList& operator=(const InlinePageOwnerList&) = delete;
InlinePageOwnerList(InlinePageOwnerList&& other) noexcept
: m_overflow(std::move(other.m_overflow)),
m_inline_size(std::exchange(other.m_inline_size, 0)) {
if (m_overflow == nullptr) {
std::copy_n(other.m_inline.begin(), m_inline_size, m_inline.begin());
}
}
InlinePageOwnerList& operator=(InlinePageOwnerList&& other) noexcept {
if (this != &other) {
m_overflow = std::move(other.m_overflow);
m_inline_size = std::exchange(other.m_inline_size, 0);
if (m_overflow == nullptr) {
std::copy_n(other.m_inline.begin(), m_inline_size, m_inline.begin());
}
}
return *this;
}
[[nodiscard]] const_iterator begin() const noexcept { return {this, 0}; }
[[nodiscard]] const_iterator end() const noexcept { return {this, size()}; }
[[nodiscard]] bool empty() const noexcept { return size() == 0; }
[[nodiscard]] size_type size() const noexcept {
return m_overflow == nullptr ? m_inline_size : m_overflow->size();
}
[[nodiscard]] OwnerT front() const noexcept { return At(0); }
[[nodiscard]] OwnerT operator[](size_type index) const noexcept { return At(index); }
void push_back(const OwnerT& owner) {
const uint64_t packed = Pack(owner);
if (m_overflow != nullptr) {
m_overflow->push_back(packed);
return;
}
if (m_inline_size < InlineCapacity) {
m_inline[m_inline_size] = packed;
++m_inline_size;
return;
}
auto overflow = std::make_unique<std::vector<uint64_t>>();
overflow->reserve(InlineCapacity * 2);
overflow->assign(m_inline.begin(), m_inline.end());
overflow->push_back(packed);
m_overflow = std::move(overflow);
}
[[nodiscard]] bool Contains(const OwnerT& owner) const noexcept {
const uint64_t packed = Pack(owner);
if (m_overflow != nullptr) {
return std::find(m_overflow->begin(), m_overflow->end(), packed) != m_overflow->end();
}
return std::find(m_inline.begin(), m_inline.begin() + static_cast<ptrdiff_t>(m_inline_size),
packed) != m_inline.begin() + static_cast<ptrdiff_t>(m_inline_size);
}
[[nodiscard]] bool Erase(const OwnerT& owner) noexcept {
const uint64_t packed = Pack(owner);
if (m_overflow != nullptr) {
const auto found = std::find(m_overflow->begin(), m_overflow->end(), packed);
if (found == m_overflow->end()) {
return false;
}
m_overflow->erase(found);
if (m_overflow->size() == InlineCapacity) {
std::copy(m_overflow->begin(), m_overflow->end(), m_inline.begin());
m_inline_size = InlineCapacity;
m_overflow.reset();
}
return true;
}
const auto end = m_inline.begin() + static_cast<ptrdiff_t>(m_inline_size);
const auto found = std::find(m_inline.begin(), end, packed);
if (found == end) {
return false;
}
std::move(found + 1, end, found);
--m_inline_size;
return true;
}
template <typename Func>
void ForEach(Func&& func) const {
if (m_overflow != nullptr) {
for (const uint64_t owner: *m_overflow) {
func(Unpack(owner));
}
return;
}
for (size_type index = 0; index < m_inline_size; ++index) {
func(Unpack(m_inline[index]));
}
}
private:
[[nodiscard]] OwnerT At(size_type index) const noexcept {
return Unpack(m_overflow == nullptr ? m_inline[index] : (*m_overflow)[index]);
}
[[nodiscard]] static uint64_t Pack(const OwnerT& owner) noexcept {
uint64_t packed = 0;
std::memcpy(&packed, &owner, sizeof(owner));
return packed;
}
[[nodiscard]] static OwnerT Unpack(uint64_t packed) noexcept {
OwnerT owner {};
std::memcpy(&owner, &packed, sizeof(owner));
return owner;
}
std::array<uint64_t, InlineCapacity> m_inline;
std::unique_ptr<std::vector<uint64_t>> m_overflow;
size_type m_inline_size = 0;
};
// Removes only the requested owner. Other owners in the same page entry remain intact.
template <typename Container, typename Value>
[[nodiscard]] bool EraseExact(Container& owners, const Value& owner) {
@@ -114,247 +267,6 @@ template <typename Container, typename Value>
return true;
}
// Multi-range ownership with 1 MiB candidate buckets and precise 4 KiB
// lifetime accounting. OwnerT only needs equality.
template <typename OwnerT>
class MultiRangePageOwnerIndex final {
private:
struct Registration;
using MembershipList = std::vector<const Registration*>;
public:
struct ByteRange final {
uint64_t address = 0;
uint64_t size = 0;
};
using CoarseTable = MultiLevelPageTable<MembershipList, 20, 40, 10>;
using TrackingTable = MultiLevelPageTable<MembershipList, 12, 40, 18>;
[[nodiscard]] bool Register(const OwnerT& owner, const std::vector<ByteRange>& ranges) {
if (FindRegistration(owner) != m_registrations.end()) {
return false;
}
auto normalized = Normalize(ranges);
if (normalized.empty()) {
return false;
}
m_registrations.push_back({owner, std::move(normalized)});
const Registration* registration = &m_registrations.back();
for (const size_t page: CollectPages<20>(registration->ranges)) {
m_coarse_pages[page].push_back(registration);
}
for (const size_t page: CollectPages<12>(registration->ranges)) {
m_tracking_pages[page].push_back(registration);
}
return true;
}
// No state changes if an expected membership is absent. Final releases are
// sorted and coalesced 4 KiB spans whose final owner disappeared.
[[nodiscard]] bool Unregister(const OwnerT& owner, std::vector<ByteRange>& final_releases) {
final_releases.clear();
auto registration = FindRegistration(owner);
if (registration == m_registrations.end()) {
return false;
}
const auto coarse_pages = CollectPages<20>(registration->ranges);
const auto tracking_pages = CollectPages<12>(registration->ranges);
const Registration* registration_ptr = &*registration;
if (!HasAllMemberships(m_coarse_pages, coarse_pages, registration_ptr) ||
!HasAllMemberships(m_tracking_pages, tracking_pages, registration_ptr)) {
return false;
}
std::vector<size_t> final_pages;
for (const size_t page: coarse_pages) {
(void)EraseExact(*m_coarse_pages.Find(page), registration_ptr);
}
for (const size_t page: tracking_pages) {
auto& owners = *m_tracking_pages.Find(page);
if (owners.size() == 1) {
final_pages.push_back(page);
}
(void)EraseExact(owners, registration_ptr);
}
m_registrations.erase(registration);
final_releases = CoalesceTrackingPages(final_pages);
return true;
}
[[nodiscard]] std::vector<OwnerT> Query(uint64_t address, uint64_t size) const {
return Query(address, size, [](const OwnerT&) { return true; });
}
template <typename Predicate>
[[nodiscard]] std::vector<OwnerT> Query(uint64_t address, uint64_t size,
Predicate&& predicate) const {
return QueryImpl(address, size, true, std::forward<Predicate>(predicate));
}
// Fault paths use page candidates: exact byte-disjoint owners sharing a
// touched 4 KiB page are intentionally retained.
[[nodiscard]] std::vector<OwnerT> QueryCandidates(uint64_t address, uint64_t size) const {
return QueryCandidates(address, size, [](const OwnerT&) { return true; });
}
template <typename Predicate>
[[nodiscard]] std::vector<OwnerT> QueryCandidates(uint64_t address, uint64_t size,
Predicate&& predicate) const {
return QueryImpl(address, size, false, std::forward<Predicate>(predicate));
}
[[nodiscard]] size_t CoarseMembershipCount(size_t page) const {
const auto* owners = m_coarse_pages.Find(page);
return owners == nullptr ? 0 : owners->size();
}
[[nodiscard]] size_t TrackingMembershipCount(size_t page) const {
const auto* owners = m_tracking_pages.Find(page);
return owners == nullptr ? 0 : owners->size();
}
private:
template <typename Predicate>
[[nodiscard]] std::vector<OwnerT> QueryImpl(uint64_t address, uint64_t size, bool strict_bytes,
Predicate&& predicate) const {
typename CoarseTable::PageRange coarse_range {};
typename TrackingTable::PageRange tracking_range {};
if (!CoarseTable::TryGetPageRange(address, size, coarse_range) ||
(!strict_bytes && !TrackingTable::TryGetPageRange(address, size, tracking_range))) {
return {};
}
MembershipList candidates;
for (size_t page = coarse_range.first; page < coarse_range.last_exclusive; ++page) {
if (const auto* owners = m_coarse_pages.Find(page); owners != nullptr) {
AppendUnique(candidates, *owners);
}
}
std::vector<OwnerT> result;
for (const Registration* registration: candidates) {
if ((strict_bytes ? Overlaps(registration->ranges, address, size)
: HasTrackingMembership(registration, tracking_range)) &&
predicate(registration->owner)) {
result.push_back(registration->owner);
}
}
return result;
}
struct Registration final {
OwnerT owner;
std::vector<ByteRange> ranges;
};
using RegistrationIterator = typename std::list<Registration>::iterator;
using ConstRegistrationIterator = typename std::list<Registration>::const_iterator;
[[nodiscard]] RegistrationIterator FindRegistration(const OwnerT& owner) {
return std::find_if(m_registrations.begin(), m_registrations.end(),
[&](const Registration& item) { return item.owner == owner; });
}
[[nodiscard]] ConstRegistrationIterator FindRegistration(const OwnerT& owner) const {
return std::find_if(m_registrations.begin(), m_registrations.end(),
[&](const Registration& item) { return item.owner == owner; });
}
[[nodiscard]] static std::vector<ByteRange> Normalize(const std::vector<ByteRange>& ranges) {
std::vector<ByteRange> sorted;
for (const auto& range: ranges) {
typename CoarseTable::PageRange ignored {};
if (!CoarseTable::TryGetPageRange(range.address, range.size, ignored)) {
return {};
}
sorted.push_back(range);
}
std::sort(sorted.begin(), sorted.end(), [](const ByteRange& lhs, const ByteRange& rhs) {
return lhs.address < rhs.address;
});
std::vector<ByteRange> merged;
for (const auto& range: sorted) {
if (merged.empty() || range.address > merged.back().address + merged.back().size) {
merged.push_back(range);
} else {
const uint64_t end = std::max(merged.back().address + merged.back().size,
range.address + range.size);
merged.back().size = end - merged.back().address;
}
}
return merged;
}
template <size_t Bits>
[[nodiscard]] static std::vector<size_t> CollectPages(const std::vector<ByteRange>& ranges) {
std::vector<size_t> pages;
for (const auto& range: ranges) {
const size_t first = static_cast<size_t>(range.address >> Bits);
const size_t last = static_cast<size_t>((range.address + range.size - 1) >> Bits);
for (size_t page = first; page <= last; ++page) {
pages.push_back(page);
}
}
std::sort(pages.begin(), pages.end());
pages.erase(std::unique(pages.begin(), pages.end()), pages.end());
return pages;
}
template <typename Table>
[[nodiscard]] static bool HasAllMemberships(const Table& table,
const std::vector<size_t>& pages,
const Registration* registration) {
for (const size_t page: pages) {
const auto* owners = table.Find(page);
if (owners == nullptr ||
std::find(owners->begin(), owners->end(), registration) == owners->end()) {
return false;
}
}
return true;
}
[[nodiscard]] bool HasTrackingMembership(const Registration* registration,
const typename TrackingTable::PageRange& range) const {
for (size_t page = range.first; page < range.last_exclusive; ++page) {
const auto* owners = m_tracking_pages.Find(page);
if (owners != nullptr &&
std::find(owners->begin(), owners->end(), registration) != owners->end()) {
return true;
}
}
return false;
}
[[nodiscard]] static bool Overlaps(const std::vector<ByteRange>& ranges, uint64_t address,
uint64_t size) {
const uint64_t end = address + size;
return std::any_of(ranges.begin(), ranges.end(), [&](const ByteRange& range) {
return range.address < end && address < range.address + range.size;
});
}
static void AppendUnique(MembershipList& destination, const MembershipList& source) {
for (const Registration* registration: source) {
if (std::find(destination.begin(), destination.end(), registration) ==
destination.end()) {
destination.push_back(registration);
}
}
}
[[nodiscard]] static std::vector<ByteRange>
CoalesceTrackingPages(const std::vector<size_t>& pages) {
std::vector<ByteRange> result;
for (const size_t page: pages) {
const uint64_t address = static_cast<uint64_t>(page) << 12;
if (!result.empty() && result.back().address + result.back().size == address) {
result.back().size += uint64_t {1} << 12;
} else {
result.push_back({address, uint64_t {1} << 12});
}
}
return result;
}
CoarseTable m_coarse_pages;
TrackingTable m_tracking_pages;
std::list<Registration> m_registrations;
};
} // namespace Libs::Graphics
#endif // EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_MULTILEVELPAGETABLE_H_
+2 -48
View File
@@ -4,18 +4,10 @@
namespace Libs::Graphics {
ResourceMutex::FaultScope::FaultScope(ResourceMutex& mutex): m_mutex(mutex) {
m_resource_preowned = m_mutex.BeginFault();
}
ResourceMutex::FaultScope::~FaultScope() {
m_mutex.EndFault(m_resource_preowned);
}
ResourceMutex::~ResourceMutex() {
std::lock_guard state(m_state);
if (m_resource_owner != std::thread::id {} || m_fault_owner != std::thread::id {}) {
EXIT("ResourceMutex destroyed with an active owner or fault transaction\n");
if (m_resource_owner != std::thread::id {}) {
EXIT("ResourceMutex destroyed with an active owner\n");
}
}
@@ -26,9 +18,6 @@ void ResourceMutex::lock() {
if (m_resource_owner == current) {
EXIT("recursive resource transaction\n");
}
if (m_fault_owner == current) {
EXIT("resource transaction re-entered from a page-fault callback\n");
}
}
m_resource.lock();
std::lock_guard state(m_state);
@@ -55,39 +44,4 @@ bool ResourceMutex::IsOwnedByCurrentThread() {
return m_resource_owner == std::this_thread::get_id();
}
bool ResourceMutex::BeginFault() {
const auto current = std::this_thread::get_id();
{
std::lock_guard state(m_state);
if (m_fault_owner == current) {
EXIT("nested resource page-fault transaction\n");
}
if (m_resource_owner == current) {
m_fault_owner = current;
return true;
}
}
lock();
std::lock_guard state(m_state);
if (m_resource_owner != current || m_fault_owner != std::thread::id {}) {
EXIT("resource fault transaction acquired inconsistent ownership\n");
}
m_fault_owner = current;
return false;
}
void ResourceMutex::EndFault(bool resource_preowned) {
const auto current = std::this_thread::get_id();
{
std::lock_guard state(m_state);
if (m_fault_owner != current || m_resource_owner != current) {
EXIT("resource page-fault transaction ended with inconsistent ownership\n");
}
m_fault_owner = {};
}
if (!resource_preowned) {
unlock();
}
}
} // namespace Libs::Graphics
+1 -19
View File
@@ -8,21 +8,9 @@
namespace Libs::Graphics {
// Owner-tracked shared buffer/image transaction. External faults pause GPU submissions first;
// command-processor faults drain pending guest processors before entering this transaction.
// Owner-tracked shared buffer/image transaction.
class ResourceMutex final {
public:
class FaultScope final {
public:
explicit FaultScope(ResourceMutex& mutex);
~FaultScope();
KYTY_CLASS_NO_COPY(FaultScope);
private:
ResourceMutex& m_mutex;
bool m_resource_preowned = false;
};
ResourceMutex() = default;
~ResourceMutex();
KYTY_CLASS_NO_COPY(ResourceMutex);
@@ -32,15 +20,9 @@ public:
[[nodiscard]] bool IsOwnedByCurrentThread();
private:
friend class FaultScope;
[[nodiscard]] bool BeginFault();
void EndFault(bool resource_preowned);
std::mutex m_resource;
std::mutex m_state;
std::thread::id m_resource_owner;
std::thread::id m_fault_owner;
};
} // namespace Libs::Graphics
+60 -14
View File
@@ -195,10 +195,12 @@ void TextureCache::RegisterImage(ImageId id) {
if (image.registered || image.info.data.Empty()) {
EXIT("TextureCache: invalid image registration\n");
}
std::vector<ImageOwnerIndex::ByteRange> ranges;
ranges.push_back({image.info.data.address, image.info.data.size});
if (!m_image_owner_index.Register(id, ranges)) {
EXIT("TextureCache: duplicate or invalid image registration\n");
ImagePageTable::PageRange pages {};
if (!ImagePageTable::TryGetPageRange(image.info.data.address, image.info.data.size, pages)) {
EXIT("TextureCache: image registration is outside the guest address space\n");
}
for (size_t page = pages.first; page < pages.last_exclusive; ++page) {
m_image_page_table[page].push_back(id);
}
image.registered = true;
image.lru_id = m_lru_cache.Insert(id, m_gc_tick);
@@ -211,9 +213,15 @@ void TextureCache::UnregisterImage(ImageId id) {
return;
}
UntrackImage(id);
std::vector<ImageOwnerIndex::ByteRange> releases;
if (!m_image_owner_index.Unregister(id, releases)) {
EXIT("TextureCache: image missing from owner index\n");
ImagePageTable::PageRange pages {};
if (!ImagePageTable::TryGetPageRange(image.info.data.address, image.info.data.size, pages)) {
EXIT("TextureCache: registered image is outside the guest address space\n");
}
for (size_t page = pages.first; page < pages.last_exclusive; ++page) {
auto* owners = m_image_page_table.Find(page);
if (owners == nullptr || !owners->Erase(id)) {
EXIT("TextureCache: image missing from page owner index\n");
}
}
m_lru_cache.Free(image.lru_id);
const auto accounted = image.AccountedSize();
@@ -451,10 +459,48 @@ const Image& TextureCache::GetImage(ImageId id) const {
return ResolveImage(id);
}
std::vector<ImageId> TextureCache::FindImagesInRegion(uint64_t address, uint64_t size,
bool page_overlap) const {
return page_overlap ? m_image_owner_index.QueryCandidates(address, size)
: m_image_owner_index.Query(address, size);
TextureCache::ImageIds TextureCache::FindImagesInRegion(uint64_t address, uint64_t size,
bool page_overlap) const {
ImagePageTable::PageRange pages {};
if (!ImagePageTable::TryGetPageRange(address, size, pages)) {
return {};
}
uint32_t query_epoch = ++m_image_query_epoch;
if (query_epoch == 0) {
for (const auto& slot: m_slots) {
if (slot.image != nullptr) {
slot.image->query_epoch = 0;
}
}
query_epoch = ++m_image_query_epoch;
}
ImageIds result;
for (size_t page = pages.first; page < pages.last_exclusive; ++page) {
const auto* owners = m_image_page_table.Find(page);
if (owners == nullptr) {
continue;
}
owners->ForEach([&](ImageId id) {
if (!id || id.index >= m_slots.size()) {
return;
}
const auto& slot = m_slots[id.index];
if (slot.generation != id.generation || slot.image == nullptr) {
return;
}
auto& image = *slot.image;
if (image.query_epoch == query_epoch) {
return;
}
image.query_epoch = query_epoch;
if (image.Overlaps(address, size, page_overlap)) {
result.push_back(id);
}
});
}
return result;
}
ImageId TextureCache::GetNullImage(const ImageDesc& desc) {
@@ -1152,9 +1198,9 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
ImageId result {};
bool inserted_new = false;
{
std::lock_guard transaction(m_resource_mutex);
CacheLock lock(*this, m_lock);
const std::vector<ImageId> candidates =
std::lock_guard transaction(m_resource_mutex);
CacheLock lock(*this, m_lock);
const auto candidates =
FindImagesInRegion(desc.info.data.address, desc.info.data.size, false);
for (const auto id: candidates) {
+10 -7
View File
@@ -99,7 +99,8 @@ private:
int32_t layer = -1;
};
using ImageOwnerIndex = MultiRangePageOwnerIndex<ImageId>;
using ImageIds = InlinePageOwnerList<ImageId, 16>;
using ImagePageTable = MultiLevelPageTable<ImageIds, 20, 40, 10>;
[[nodiscard]] Image& ResolveImage(ImageId id);
[[nodiscard]] const Image& ResolveImage(ImageId id) const;
@@ -125,8 +126,9 @@ private:
[[nodiscard]] static BindingType UploadBinding(const Image& image);
[[nodiscard]] bool SafeToDownload(const Image& image);
[[nodiscard]] std::vector<ImageId> FindImagesInRegion(uint64_t address, uint64_t size,
bool page_overlap) const;
// Caller holds m_lock; it also serializes the per-image query epoch.
[[nodiscard]] ImageIds FindImagesInRegion(uint64_t address, uint64_t size,
bool page_overlap) const;
[[nodiscard]] OverlapResult ResolveOverlap(const ImageInfo& requested, BindingType binding,
ImageId cached, ImageId merged);
[[nodiscard]] ImageId ResolveDepthOverlap(const ImageInfo& requested, BindingType binding,
@@ -169,7 +171,7 @@ private:
ResourceMutex& m_resource_mutex;
std::vector<Slot> m_slots;
std::vector<uint32_t> m_free_slots;
ImageOwnerIndex m_image_owner_index;
ImagePageTable m_image_page_table;
std::map<vk::Format, ImageId> m_null_images;
Common::LeastRecentlyUsedCache<ImageId, uint64_t> m_lru_cache;
std::set<ImageId> m_download_images;
@@ -177,9 +179,10 @@ private:
uint64_t m_total_used_memory = 0;
uint64_t m_trigger_gc_memory = 0;
uint64_t m_pressure_gc_memory = 1536ull * 1024 * 1024;
uint64_t m_critical_gc_memory = 3ull * 1024 * 1024 * 1024;
uint64_t m_gc_tick = 0;
bool m_readback_linear_images = false;
uint64_t m_critical_gc_memory = 3ull * 1024 * 1024 * 1024;
uint64_t m_gc_tick = 0;
mutable uint32_t m_image_query_epoch = 0;
bool m_readback_linear_images = false;
friend struct TextureCacheTestAccess;
friend class BufferCache;
+12 -11
View File
@@ -154,17 +154,18 @@ public:
}
[[nodiscard]] uint64_t HashGuestEdges() const;
ImageInfo info;
VulkanImage backing;
ImageViewCache views;
ImageUsage usage;
ImageBinding binding;
bool registered = false;
uint64_t track_addr = 0;
uint64_t track_addr_end = 0;
ImageId depth_id {};
uint64_t tick_accessed_last = 0;
size_t lru_id = 0;
ImageInfo info;
VulkanImage backing;
ImageViewCache views;
ImageUsage usage;
ImageBinding binding;
bool registered = false;
mutable uint32_t query_epoch = 0;
uint64_t track_addr = 0;
uint64_t track_addr_end = 0;
ImageId depth_id {};
uint64_t tick_accessed_last = 0;
size_t lru_id = 0;
private:
friend struct ImageTestAccess;
+9 -32
View File
@@ -664,12 +664,10 @@ struct DrawIndexBufferSource {
struct PreparedIndexBuffer {
std::shared_ptr<void> owner;
vk::Buffer buffer = nullptr;
uint64_t address = 0;
uint64_t size = 0;
vk::DeviceSize offset = 0;
vk::IndexType type = vk::IndexType::eUint16;
bool streamed = false;
vk::Buffer buffer = nullptr;
uint64_t size = 0;
vk::DeviceSize offset = 0;
vk::IndexType type = vk::IndexType::eUint16;
};
static uint64_t VertexBufferDescriptorSize(const ShaderVertexInputBuffer& buffer) {
@@ -896,12 +894,6 @@ static std::vector<BufferBinding> PrepareVertexBuffers(uint64_t
return AcquireVertexBuffers(buffer, vs_input_info);
}
static void RebindVertexBuffers(RenderCommandBuffer& buffer,
const ShaderVertexInputInfo& vs_input_info,
std::vector<BufferBinding>& bindings) {
bindings = AcquireVertexBuffers(buffer, vs_input_info);
}
static PreparedIndexBuffer PrepareIndexBuffer(RenderCommandBuffer& buffer,
const DrawIndexBufferSource& source) {
PreparedIndexBuffer prepared;
@@ -909,16 +901,14 @@ static PreparedIndexBuffer PrepareIndexBuffer(RenderCommandBuffer& buffe
return prepared;
}
EXIT_IF(source.size == 0);
prepared.address = source.address;
prepared.size = source.size;
prepared.type = source.type;
prepared.size = source.size;
prepared.type = source.type;
if (source.host_data != nullptr) {
auto binding =
buffer.GetContext().GetBufferCache().UploadTransient(source.host_data, source.size, 16);
prepared.owner = std::move(binding.owner);
prepared.buffer = binding.buffer;
prepared.offset = binding.offset;
prepared.streamed = true;
prepared.owner = std::move(binding.owner);
prepared.buffer = binding.buffer;
prepared.offset = binding.offset;
} else {
auto binding =
buffer.GetContext().GetBufferCache().ObtainBuffer(buffer, source.address, source.size);
@@ -929,17 +919,6 @@ static PreparedIndexBuffer PrepareIndexBuffer(RenderCommandBuffer& buffe
return prepared;
}
static void RebindIndexBuffer(RenderCommandBuffer& buffer, PreparedIndexBuffer& prepared) {
if (prepared.size == 0 || prepared.streamed) {
return;
}
auto binding =
buffer.GetContext().GetBufferCache().ObtainBuffer(buffer, prepared.address, prepared.size);
prepared.owner = std::move(binding.owner);
prepared.buffer = binding.buffer;
prepared.offset = binding.offset;
}
static void CommitVertexBuffers(RenderCommandBuffer& buffer, vk::CommandBuffer vk_buffer,
std::vector<BufferBinding>& bindings) {
for (uint32_t slot = 0; slot < bindings.size(); slot++) {
@@ -1046,8 +1025,6 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, RenderCommandBuffer
state.ps_input_info.stage, state.ps_active);
auto vertex_bindings = PrepareVertexBuffers(submit_id, buffer, draw, state.vs_input_info);
auto index_binding = PrepareIndexBuffer(buffer, index_source);
RebindVertexBuffers(buffer, state.vs_input_info, vertex_bindings);
RebindIndexBuffer(buffer, index_binding);
state.rendering =
AcquireRenderTargets(buffer, state.color_info, state.color_count, state.depth_info);
@@ -0,0 +1,256 @@
#include "graphics/presentation/window/hostInput.h"
#include "SDL_keyboard.h"
#include "SDL_keycode.h"
#include "SDL_mouse.h"
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "libs/controller.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <string>
#include <string_view>
#include <utility>
namespace Libs::Graphics {
namespace {
struct Binding {
SDL_Keycode key = SDLK_UNKNOWN;
uint8_t mouse_button = 0;
uint32_t pad_button = 0;
};
static constexpr std::array PAD_BUTTONS = {
std::pair {std::string_view("L3"), Controller::PAD_BUTTON_L3},
std::pair {std::string_view("R3"), Controller::PAD_BUTTON_R3},
std::pair {std::string_view("Options"), Controller::PAD_BUTTON_OPTIONS},
std::pair {std::string_view("Up"), Controller::PAD_BUTTON_UP},
std::pair {std::string_view("Right"), Controller::PAD_BUTTON_RIGHT},
std::pair {std::string_view("Down"), Controller::PAD_BUTTON_DOWN},
std::pair {std::string_view("Left"), Controller::PAD_BUTTON_LEFT},
std::pair {std::string_view("L2"), Controller::PAD_BUTTON_L2},
std::pair {std::string_view("R2"), Controller::PAD_BUTTON_R2},
std::pair {std::string_view("L1"), Controller::PAD_BUTTON_L1},
std::pair {std::string_view("R1"), Controller::PAD_BUTTON_R1},
std::pair {std::string_view("Triangle"), Controller::PAD_BUTTON_TRIANGLE},
std::pair {std::string_view("Circle"), Controller::PAD_BUTTON_CIRCLE},
std::pair {std::string_view("Cross"), Controller::PAD_BUTTON_CROSS},
std::pair {std::string_view("Square"), Controller::PAD_BUTTON_SQUARE},
std::pair {std::string_view("TouchPad"), Controller::PAD_BUTTON_TOUCH_PAD},
};
uint32_t PadButtonFromName(std::string_view name) {
const auto button = std::find_if(PAD_BUTTONS.begin(), PAD_BUTTONS.end(),
[name](const auto& item) { return item.first == name; });
return button != PAD_BUTTONS.end() ? button->second : 0;
}
SDL_Keycode NormalizeKey(SDL_Keycode key) {
switch (key) {
case SDLK_RSHIFT: return SDLK_LSHIFT;
case SDLK_RCTRL: return SDLK_LCTRL;
case SDLK_RALT: return SDLK_LALT;
case SDLK_RGUI: return SDLK_LGUI;
default: return key;
}
}
uint8_t MouseButtonFromName(std::string_view name) {
static constexpr std::array buttons = {
std::pair {std::string_view("Mouse:Left"), uint8_t {SDL_BUTTON_LEFT}},
std::pair {std::string_view("Mouse:Middle"), uint8_t {SDL_BUTTON_MIDDLE}},
std::pair {std::string_view("Mouse:Right"), uint8_t {SDL_BUTTON_RIGHT}},
std::pair {std::string_view("Mouse:X1"), uint8_t {SDL_BUTTON_X1}},
std::pair {std::string_view("Mouse:X2"), uint8_t {SDL_BUTTON_X2}},
};
const auto button = std::find_if(buttons.begin(), buttons.end(),
[name](const auto& item) { return item.first == name; });
return button != buttons.end() ? button->second : 0;
}
bool Conflicts(const Binding& first, const Binding& second) {
return first.pad_button == second.pad_button ||
(first.key != SDLK_UNKNOWN && first.key == second.key) ||
(first.mouse_button != 0 && first.mouse_button == second.mouse_button);
}
class InputMap {
public:
InputMap(): m_custom(!Config::GetKeymap().empty()) {
for (const auto& value: Config::GetKeymap()) {
const std::string_view entry = value;
const auto split = entry.find('=');
Binding binding;
if (split != std::string_view::npos) {
binding.pad_button = PadButtonFromName(entry.substr(0, split));
const auto host_input = entry.substr(split + 1);
binding.mouse_button = MouseButtonFromName(host_input);
if (binding.mouse_button == 0) {
binding.key = NormalizeKey(SDL_GetKeyFromName(std::string(host_input).c_str()));
}
}
const bool reserved =
binding.key == SDLK_ESCAPE || binding.key == SDLK_SPACE || binding.key == SDLK_F1;
if (binding.pad_button == 0 || reserved ||
(binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) {
EXIT("Invalid input mapping: %s\n", value.c_str());
}
Add(binding);
}
}
[[nodiscard]] bool Custom() const { return m_custom; }
[[nodiscard]] uint32_t FindKey(int key_code) const {
key_code = NormalizeKey(static_cast<SDL_Keycode>(key_code));
const auto binding =
std::find_if(m_bindings.begin(), m_bindings.begin() + m_size,
[key_code](const auto& item) { return item.key == key_code; });
return binding != m_bindings.begin() + m_size ? binding->pad_button : 0;
}
[[nodiscard]] uint32_t FindMouseButton(uint8_t mouse_button) const {
const auto binding = std::find_if(
m_bindings.begin(), m_bindings.begin() + m_size,
[mouse_button](const auto& item) { return item.mouse_button == mouse_button; });
return binding != m_bindings.begin() + m_size ? binding->pad_button : 0;
}
private:
void Add(const Binding& binding) {
for (std::size_t index = 0; index < m_size;) {
if (Conflicts(m_bindings[index], binding)) {
m_bindings[index] = m_bindings[--m_size];
} else {
index++;
}
}
EXIT_IF(m_size >= m_bindings.size());
m_bindings[m_size++] = binding;
}
std::array<Binding, PAD_BUTTONS.size()> m_bindings {};
std::size_t m_size = 0;
bool m_custom;
};
const InputMap& GetInputMap() {
static const InputMap map;
return map;
}
void SetButton(uint32_t button, bool down) {
if (button == Controller::PAD_BUTTON_L2) {
Controller::ControllerAxis(Controller::HOST_INPUT_CONTROLLER_ID,
Controller::Axis::TriggerLeft, down ? 255 : 0);
} else if (button == Controller::PAD_BUTTON_R2) {
Controller::ControllerAxis(Controller::HOST_INPUT_CONTROLLER_ID,
Controller::Axis::TriggerRight, down ? 255 : 0);
} else if (button != 0) {
Controller::ControllerButton(Controller::HOST_INPUT_CONTROLLER_ID, button, down);
}
}
uint32_t DefaultKeyboardButton(int key_code) {
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
case SDLK_UP: return Controller::PAD_BUTTON_UP;
case SDLK_LEFT: return Controller::PAD_BUTTON_LEFT;
case SDLK_DOWN: return Controller::PAD_BUTTON_DOWN;
case SDLK_RIGHT: return Controller::PAD_BUTTON_RIGHT;
case SDLK_j: return Controller::PAD_BUTTON_CROSS;
case SDLK_i: return Controller::PAD_BUTTON_TRIANGLE;
case SDLK_k: return Controller::PAD_BUTTON_SQUARE;
case SDLK_l: return Controller::PAD_BUTTON_CIRCLE;
case SDLK_q: return Controller::PAD_BUTTON_L1;
case SDLK_e: return Controller::PAD_BUTTON_R1;
case SDLK_LSHIFT: return Controller::PAD_BUTTON_L3;
case SDLK_LCTRL: return Controller::PAD_BUTTON_R3;
case SDLK_RETURN:
case SDLK_RETURN2: return Controller::PAD_BUTTON_OPTIONS;
case SDLK_BACKSPACE:
case SDLK_TAB: return Controller::PAD_BUTTON_TOUCH_PAD;
default: return 0;
}
}
struct StickKeys {
bool left = false;
bool right = false;
bool up = false;
bool down = false;
};
void SetStickAxis(Controller::Axis axis, bool negative, bool positive) {
const int value = negative == positive ? 128 : negative ? 0 : 255;
Controller::ControllerAxis(Controller::HOST_INPUT_CONTROLLER_ID, axis, value);
}
void DefaultKeyboardInput(int key_code, bool down) {
static StickKeys left;
static StickKeys right;
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
case SDLK_a:
left.left = down;
SetStickAxis(Controller::Axis::LeftX, left.left, left.right);
return;
case SDLK_d:
left.right = down;
SetStickAxis(Controller::Axis::LeftX, left.left, left.right);
return;
case SDLK_w:
left.up = down;
SetStickAxis(Controller::Axis::LeftY, left.up, left.down);
return;
case SDLK_s:
left.down = down;
SetStickAxis(Controller::Axis::LeftY, left.up, left.down);
return;
case SDLK_f:
right.left = down;
SetStickAxis(Controller::Axis::RightX, right.left, right.right);
return;
case SDLK_h:
right.right = down;
SetStickAxis(Controller::Axis::RightX, right.left, right.right);
return;
case SDLK_t:
right.up = down;
SetStickAxis(Controller::Axis::RightY, right.up, right.down);
return;
case SDLK_g:
right.down = down;
SetStickAxis(Controller::Axis::RightY, right.up, right.down);
return;
default: SetButton(DefaultKeyboardButton(key_code), down); return;
}
}
} // namespace
void HostInputInit() {
GetInputMap();
}
void HostInputKey(int key_code, bool down) {
const auto& map = GetInputMap();
if (map.Custom()) {
SetButton(map.FindKey(key_code), down);
} else {
DefaultKeyboardInput(key_code, down);
}
}
void HostInputMouseButton(uint8_t mouse_button, bool down) {
const auto& map = GetInputMap();
if (map.Custom() && mouse_button != 0) {
SetButton(map.FindMouseButton(mouse_button), down);
}
}
} // namespace Libs::Graphics
@@ -0,0 +1,14 @@
#ifndef KYTY_GRAPHICS_PRESENTATION_WINDOW_HOST_INPUT_H_
#define KYTY_GRAPHICS_PRESENTATION_WINDOW_HOST_INPUT_H_
#include <cstdint>
namespace Libs::Graphics {
void HostInputInit();
void HostInputKey(int key_code, bool down);
void HostInputMouseButton(uint8_t mouse_button, bool down);
} // namespace Libs::Graphics
#endif /* KYTY_GRAPHICS_PRESENTATION_WINDOW_HOST_INPUT_H_ */
@@ -789,12 +789,14 @@ void Presenter::Present(Frame& frame, bool reuse) {
[&window] {
window.UpdateIcon();
SDL_ShowWindow(window.window);
SDL_RaiseWindow(window.window);
},
true);
#else
window.UpdateIcon();
SDL_ShowWindow(window.window);
SDL_RaiseWindow(window.window);
#endif
window.window_hidden = false;
+48 -36
View File
@@ -32,7 +32,9 @@
#include "graphics/host_gpu/vma.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/renderDoc.h"
#include "graphics/presentation/window/hostInput.h"
#include "graphics/presentation/window/windowInternal.h"
#include "kytyGitVersion.h"
#include "libs/controller.h"
#include "loader/systemContent.h"
@@ -58,8 +60,6 @@
namespace Libs::Graphics {
constexpr int KEYBOARD_CONTROLLER_ID = -1000;
struct EventKeyboard {
bool down;
bool up;
@@ -72,26 +72,6 @@ struct EventKeyboard {
double timestamp_seconds;
};
static uint32_t KeyboardKeyToPadButton(int key_code) {
switch (key_code) {
case SDLK_w: return Controller::PAD_BUTTON_UP;
case SDLK_a: return Controller::PAD_BUTTON_LEFT;
case SDLK_s: return Controller::PAD_BUTTON_DOWN;
case SDLK_d: return Controller::PAD_BUTTON_RIGHT;
case SDLK_j: return Controller::PAD_BUTTON_CROSS;
case SDLK_i: return Controller::PAD_BUTTON_TRIANGLE;
case SDLK_k: return Controller::PAD_BUTTON_SQUARE;
case SDLK_l: return Controller::PAD_BUTTON_CIRCLE;
case SDLK_q: return Controller::PAD_BUTTON_L1;
case SDLK_e: return Controller::PAD_BUTTON_R1;
case SDLK_RETURN:
case SDLK_RETURN2: return Controller::PAD_BUTTON_OPTIONS;
case SDLK_BACKSPACE:
case SDLK_TAB: return Controller::PAD_BUTTON_TOUCH_PAD;
default: return 0;
}
}
static uint32_t ControllerButtonToPadButton(int button) {
switch (button) {
case SDL_CONTROLLER_BUTTON_A: return Controller::PAD_BUTTON_CROSS;
@@ -260,16 +240,11 @@ static void GameEventKeyboard(WindowLoopState& game, const EventKeyboard& key) {
}
}
const auto button = KeyboardKeyToPadButton(key.key_code);
if (button != 0 && (key.down || key.up) && !key.repeat) {
static bool keyboard_connected = false;
if (!keyboard_connected) {
Controller::ControllerConnect(KEYBOARD_CONTROLLER_ID);
keyboard_connected = true;
}
Controller::ControllerButton(KEYBOARD_CONTROLLER_ID, button, key.down);
}
#endif
if ((key.down || key.up) && !key.repeat) {
HostInputKey(key.key_code, key.down);
}
}
static void GameEventMouse([[maybe_unused]] const EventMouse& mb) {
@@ -291,6 +266,23 @@ static void GameEventMouse([[maybe_unused]] const EventMouse& mb) {
mb.x, mb.y);
}
#endif
if (mb.down || mb.up) {
uint8_t mouse_button = 0;
if (mb.left) {
mouse_button = SDL_BUTTON_LEFT;
} else if (mb.middle) {
mouse_button = SDL_BUTTON_MIDDLE;
} else if (mb.right) {
mouse_button = SDL_BUTTON_RIGHT;
} else if (mb.x1) {
mouse_button = SDL_BUTTON_X1;
} else if (mb.x2) {
mouse_button = SDL_BUTTON_X2;
}
HostInputMouseButton(mouse_button, mb.down);
}
}
static void GameEventFinger([[maybe_unused]] const EventFinger& f) {
@@ -794,10 +786,14 @@ static void WindowCreate(WindowContext& context) {
if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) {
EXIT("%s\n", SDL_GetError());
}
HostInputInit();
LOGF("WindowCreate(): width = %d, height = %d\n", width, height);
uint32_t window_flags = KYTY_SDL_WINDOW_FLAGS;
if (Config::FullscreenEnabled()) {
window_flags |= static_cast<uint32_t>(SDL_WINDOW_FULLSCREEN_DESKTOP);
}
#if defined(__APPLE__)
// macOS 26 window chrome (CoreUI asset decode, SwiftUI titlebar) has been observed
// throwing NSExceptions under Rosetta during the first CATransaction commit. A
@@ -937,6 +933,22 @@ void WindowContext::UpdateTitle() {
static uint64_t fps_frames = 0;
static double current_fps = 0.0;
#if KYTY_BUILD == KYTY_BUILD_DEBUG
static constexpr auto build_type = "Debug";
#elif KYTY_BUILD == KYTY_BUILD_RELEASE
static constexpr auto build_type = "Release";
#else
static constexpr auto build_type = "Unknown";
#endif
#if defined(KYTY_OFFICIAL_BUILD)
static constexpr auto build_label = "Official build " KYTY_RELEASE_TAG;
#elif defined(KYTY_FORK_BUILD)
static constexpr auto build_label = "Fork build " KYTY_BUILD_REPOSITORY " " KYTY_GIT_HASH;
#else
static constexpr auto build_label = "Source build " KYTY_GIT_HASH;
#endif
const auto now = Common::Timer::QueryPerformanceCounter();
const auto frequency = Common::Timer::QueryPerformanceFrequency();
frame_num++;
@@ -948,11 +960,11 @@ void WindowContext::UpdateTitle() {
fps_frames = 0;
}
auto fps =
fmt::format("{}{}{}{}{}{}[{}] [{}], frame: {}, fps: {:f}", (has_title ? title : ""),
(has_title ? ", " : ""), (has_title_id ? title_id : ""),
(has_title_id ? ", " : ""), (has_app_ver ? app_ver : ""),
(has_app_ver ? " " : ""), device_name, processor_name, frame_num, current_fps);
auto fps = fmt::format("[{} | {}] {}{}{}{}{}{}[{}] [{}], frame: {}, fps: {:f}", build_label,
build_type, (has_title ? title : ""), (has_title ? ", " : ""),
(has_title_id ? title_id : ""), (has_title_id ? ", " : ""),
(has_app_ver ? app_ver : ""), (has_app_ver ? " " : ""), device_name,
processor_name, frame_num, current_fps);
#if defined(__APPLE__)
// AppKit traps on title changes off the main thread; fire-and-forget keeps present pacing.
@@ -200,12 +200,12 @@ bool ValidateResourceSpecialization(const Program& program, const ResourceSnapsh
if (dimension == Decoder::ImageDimension::Unknown || dimension != image.dimension ||
DescriptorIsCube(descriptor) != image.cube) {
if (error != nullptr) {
*error = fmt::format(
"image descriptor {} no longer matches specialized dimension: "
"{:08x},{:08x},{:08x},{:08x},{:08x},{:08x},{:08x},{:08x}",
i, descriptor.dwords[0], descriptor.dwords[1], descriptor.dwords[2],
descriptor.dwords[3], descriptor.dwords[4], descriptor.dwords[5],
descriptor.dwords[6], descriptor.dwords[7]);
*error =
fmt::format("image descriptor {} no longer matches specialized dimension: "
"{:08x},{:08x},{:08x},{:08x},{:08x},{:08x},{:08x},{:08x}",
i, descriptor.dwords[0], descriptor.dwords[1], descriptor.dwords[2],
descriptor.dwords[3], descriptor.dwords[4], descriptor.dwords[5],
descriptor.dwords[6], descriptor.dwords[7]);
}
return false;
}
@@ -224,14 +224,17 @@ bool ValidateResourceSpecialization(const Program& program, const ResourceSnapsh
const bool raw_sint_storage =
storage && format == Prospero::GpuEnumValue(Prospero::BufferFormat::k32SInt) &&
!image.read && !image.atomic;
const bool uint_descriptor =
Prospero::IsUintTextureFormat(format) || raw_sint_storage;
const auto uint_program = image.kind == ResourceKind::ImageUint ||
image.kind == ResourceKind::StorageImageUint;
const bool uint_descriptor = Prospero::IsUintTextureFormat(format) || raw_sint_storage;
const auto uint_program = image.kind == ResourceKind::ImageUint ||
image.kind == ResourceKind::StorageImageUint;
if (uint_descriptor != uint_program && !(image.atomic && uint_program)) {
if (error != nullptr) {
*error =
fmt::format("image descriptor {} no longer matches specialized format", i);
*error = fmt::format(
"image descriptor {} no longer matches specialized format: "
"{:08x},{:08x},{:08x},{:08x},{:08x},{:08x},{:08x},{:08x}",
i, descriptor.dwords[0], descriptor.dwords[1], descriptor.dwords[2],
descriptor.dwords[3], descriptor.dwords[4], descriptor.dwords[5],
descriptor.dwords[6], descriptor.dwords[7]);
}
return false;
}
+52
View File
@@ -64,6 +64,7 @@ struct File {
std::filesystem::path real_name;
std::atomic_bool opened;
std::atomic_bool directory;
std::atomic_bool writable;
std::atomic_bool append;
std::atomic_bool sync_writes;
SpecialFile special;
@@ -129,6 +130,7 @@ int FileDescriptors::CreateDescriptor() {
auto* file = new File {};
file->opened = false;
file->directory = false;
file->writable = false;
file->append = false;
file->sync_writes = false;
file->special = SpecialFile::None;
@@ -274,6 +276,10 @@ static std::filesystem::path ResolvePathIgnoringCase(const std::filesystem::path
return resolved;
}
#else
static bool HasWindowsForbiddenFilenameCharacter(const std::string& relative_path) {
return relative_path.find_first_of("<>:\"|?*") != std::string::npos;
}
#endif
std::filesystem::path MountPoints::GetRealFilename(const std::string& mounted_file_name) {
@@ -293,6 +299,10 @@ std::filesystem::path MountPoints::GetRealFilename(const std::string& mounted_fi
rel_path = Common::RemoveFirst(rel_path, 1);
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (HasWindowsForbiddenFilenameCharacter(rel_path)) {
::printf("FileSystem: Windows-incompatible guest filename: %s\n",
mounted_file_name.c_str());
}
return p.dir / rel_path;
#else
return ResolvePathIgnoringCase(p.dir / rel_path);
@@ -408,6 +418,7 @@ int KYTY_SYSV_ABI KernelOpen(const char* path, int flags, uint16_t mode) {
EXIT_IF(file == nullptr || file->opened || file->directory);
file->name = path;
file->writable = rw_mode != Common::File::Mode::Read;
file->append = append;
file->sync_writes = fsync || sync || dsync;
@@ -953,6 +964,47 @@ int KYTY_SYSV_ABI KernelFstat(int d, FileStat* sb) {
return OK;
}
int KYTY_SYSV_ABI KernelFtruncate(int d, int64_t length) {
PRINT_NAME();
if (d < DESCRIPTOR_MIN) {
return KERNEL_ERROR_EBADF;
}
if (length < 0) {
return KERNEL_ERROR_EINVAL;
}
if (::Libs::Network::Net::IsSocket(d)) {
return KERNEL_ERROR_EINVAL;
}
auto* file = g_files->GetFile(d);
if (file == nullptr || !file->opened) {
return KERNEL_ERROR_EBADF;
}
if (!file->writable) {
return KERNEL_ERROR_EBADF;
}
if (file->directory || file->special != SpecialFile::None) {
return KERNEL_ERROR_EINVAL;
}
Common::LockGuard lock(file->mutex);
if (file->f.IsInvalid() || !file->f.Truncate(static_cast<uint64_t>(length))) {
return KERNEL_ERROR_EIO;
}
LOGF("\tFtruncate (size = %" PRId64 ") file: %s\n", length,
Common::PathToString(file->real_name).c_str());
return OK;
}
int KYTY_SYSV_ABI KernelUnlink(const char* path) {
PRINT_NAME();
+1
View File
@@ -48,6 +48,7 @@ int64_t KYTY_SYSV_ABI KernelPwrite(int d, const void* buf, size_t nbytes, int64_
int64_t KYTY_SYSV_ABI KernelLseek(int d, int64_t offset, int whence);
int KYTY_SYSV_ABI KernelStat(const char* path, FileStat* sb);
int KYTY_SYSV_ABI KernelFstat(int d, FileStat* sb);
int KYTY_SYSV_ABI KernelFtruncate(int d, int64_t length);
int KYTY_SYSV_ABI KernelUnlink(const char* path);
int KYTY_SYSV_ABI KernelRename(const char* from, const char* to);
int KYTY_SYSV_ABI KernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep);
+1 -1
View File
@@ -65,7 +65,7 @@ constexpr uint64_t PAGE_TABLE_POOL_SIZE = 4ull * 1024ull * 1024ull * 1024ull;
constexpr uint64_t PAGE_TABLE_GRANULARITY = 2ull * 1024ull * 1024ull;
constexpr int PAGE_TABLE_POOL_ENTRIES =
static_cast<int>(PAGE_TABLE_POOL_SIZE / PAGE_TABLE_GRANULARITY);
constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 4ull * 1024ull * 1024ull * 1024ull;
constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 1ull * 1024ull * 1024ull * 1024ull;
static uint64_t g_flexible_memory_size = DEFAULT_FLEXIBLE_MEMORY_SIZE;
static bool g_flexible_memory_size_frozen = false;
+5 -3
View File
@@ -876,9 +876,11 @@ bool TestGuestStackOwnerLifecycle(uint64_t* first_address, uint64_t* second_addr
static KYTY_SYSV_ABI void* RunOnGuestStack(void* arg, pthread_entry_func_t func, void* stack_top) {
#if defined(__x86_64__) || defined(_M_X64)
void* ret = nullptr;
const auto guest_rsp = reinterpret_cast<uintptr_t>(stack_top) & ~static_cast<uintptr_t>(0x0f);
const auto guest_rbp = guest_rsp - 4u * sizeof(uint64_t);
void* ret = nullptr;
const auto aligned_stack_top =
reinterpret_cast<uintptr_t>(stack_top) & ~static_cast<uintptr_t>(0x0f);
const auto guest_rsp = aligned_stack_top - 2u * sizeof(uintptr_t);
const auto guest_rbp = guest_rsp;
auto* guest_root_frame = reinterpret_cast<uintptr_t*>(guest_rbp);
guest_root_frame[0] = 0;
+287
View File
@@ -0,0 +1,287 @@
#include "kernel/syncOnAddress.h"
#include "common/threads.h"
#include "libs/errno.h"
#include <algorithm>
#include <chrono>
#include <climits>
#include <cstdint>
#include <list>
#include <memory>
#include <mutex>
#include <unordered_map>
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
#include <cerrno>
#include <linux/futex.h>
#include <sys/syscall.h>
#include <unistd.h>
#endif
namespace Libs::LibKernel::SyncOnAddress {
namespace {
constexpr uint32_t SIGNAL_POLL_MICROS = 10000;
using Clock = std::chrono::steady_clock;
template <typename T>
[[nodiscard]] bool IsValidWaitAddress(const volatile T* address) {
return address != nullptr && (reinterpret_cast<uintptr_t>(address) & (alignof(T) - 1u)) == 0;
}
[[nodiscard]] bool IsValidWakeAddress(const volatile void* address) {
return address != nullptr &&
(reinterpret_cast<uintptr_t>(address) & (alignof(uint32_t) - 1u)) == 0;
}
template <typename T>
[[nodiscard]] T ReadWord(const volatile T* address) {
return __atomic_load_n(address, __ATOMIC_ACQUIRE);
}
struct WaitDeadline {
bool finite = false;
Clock::time_point end {};
};
[[nodiscard]] WaitDeadline MakeDeadline(const uint32_t* timeout_micros) {
if (timeout_micros == nullptr) {
return {};
}
return {true, Clock::now() + std::chrono::microseconds(*timeout_micros)};
}
[[nodiscard]] uint32_t GetWaitSliceMicros(const WaitDeadline& deadline, bool first_wait) {
if (!deadline.finite) {
return SIGNAL_POLL_MICROS;
}
const auto now = Clock::now();
if (now >= deadline.end) {
return first_wait ? 0u : UINT32_MAX;
}
const auto remaining =
std::chrono::duration_cast<std::chrono::microseconds>(deadline.end - now).count();
return static_cast<uint32_t>(std::min<int64_t>(remaining, SIGNAL_POLL_MICROS));
}
void PollSignals(signal_poll_func_t signal_poll) {
if (signal_poll != nullptr) {
signal_poll();
}
}
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
template <typename T>
int WaitLinux(volatile T* address, T expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll) {
const auto deadline = MakeDeadline(timeout_micros);
bool first_wait = true;
for (;;) {
if (ReadWord(address) != expected) {
return OK;
}
const auto slice_micros = GetWaitSliceMicros(deadline, first_wait);
if (slice_micros == UINT32_MAX) {
return ReadWord(address) == expected ? KERNEL_ERROR_ETIMEDOUT : OK;
}
const timespec timeout = {
.tv_sec = static_cast<time_t>(slice_micros / 1000000u),
.tv_nsec = static_cast<long>(slice_micros % 1000000u) * 1000L,
};
long result = 0;
int wait_error = 0;
result = syscall(SYS_futex, const_cast<T*>(address), FUTEX_WAIT_PRIVATE,
static_cast<uint32_t>(expected), &timeout, nullptr, 0);
if (result != 0) {
wait_error = errno;
}
if (result == 0 || wait_error == EAGAIN) {
return OK;
}
if (wait_error != ETIMEDOUT && wait_error != EINTR) {
return KERNEL_ERROR_EINVAL;
}
PollSignals(signal_poll);
if (deadline.finite && Clock::now() >= deadline.end) {
return ReadWord(address) == expected ? KERNEL_ERROR_ETIMEDOUT : OK;
}
first_wait = false;
}
}
int WakeLinux(volatile void* address, int32_t count) {
const auto result = syscall(SYS_futex, const_cast<void*>(address), FUTEX_WAKE_PRIVATE, count,
nullptr, nullptr, 0);
return result < 0 ? KERNEL_ERROR_EINVAL : OK;
}
#endif
struct PortableWaiter {
Common::CondVar condition;
bool wake_requested = false;
};
struct PortableAddressEntry {
Common::Mutex mutex;
std::list<PortableWaiter*> waiters;
};
struct PortableAddressRegistry {
std::mutex mutex;
std::unordered_map<uintptr_t, std::shared_ptr<PortableAddressEntry>> entries;
};
PortableAddressRegistry& GetPortableRegistry() {
static PortableAddressRegistry registry;
return registry;
}
std::shared_ptr<PortableAddressEntry> RegisterPortableWaiter(volatile void* address,
PortableWaiter* waiter) {
auto& registry = GetPortableRegistry();
std::lock_guard registry_lock(registry.mutex);
auto& entry = registry.entries[reinterpret_cast<uintptr_t>(address)];
if (!entry) {
entry = std::make_shared<PortableAddressEntry>();
}
entry->mutex.Lock();
entry->waiters.push_back(waiter);
return entry;
}
void UnregisterPortableWaiter(volatile void* address,
const std::shared_ptr<PortableAddressEntry>& entry,
PortableWaiter* waiter) {
entry->waiters.remove(waiter);
const bool empty = entry->waiters.empty();
entry->mutex.Unlock();
if (!empty) {
return;
}
auto& registry = GetPortableRegistry();
std::lock_guard registry_lock(registry.mutex);
entry->mutex.Lock();
const auto it = registry.entries.find(reinterpret_cast<uintptr_t>(address));
if (it != registry.entries.end() && it->second == entry && entry->waiters.empty()) {
registry.entries.erase(it);
}
entry->mutex.Unlock();
}
template <typename T>
int WaitPortable(volatile T* address, T expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll) {
PortableWaiter waiter;
auto entry = RegisterPortableWaiter(address, &waiter);
const auto deadline = MakeDeadline(timeout_micros);
bool first_wait = true;
int result = OK;
while (ReadWord(address) == expected && !waiter.wake_requested) {
const auto slice_micros = GetWaitSliceMicros(deadline, first_wait);
if (slice_micros == UINT32_MAX) {
result = KERNEL_ERROR_ETIMEDOUT;
break;
}
if (slice_micros == 0) {
result = KERNEL_ERROR_ETIMEDOUT;
break;
}
(void)waiter.condition.WaitFor(&entry->mutex, slice_micros);
entry->mutex.Unlock();
PollSignals(signal_poll);
entry->mutex.Lock();
if (deadline.finite && Clock::now() >= deadline.end && ReadWord(address) == expected &&
!waiter.wake_requested) {
result = KERNEL_ERROR_ETIMEDOUT;
break;
}
first_wait = false;
}
UnregisterPortableWaiter(address, entry, &waiter);
return result;
}
int WakePortable(volatile void* address, int32_t count) {
if (count == 0) {
return OK;
}
auto& registry = GetPortableRegistry();
std::unique_lock registry_lock(registry.mutex);
const auto it = registry.entries.find(reinterpret_cast<uintptr_t>(address));
if (it == registry.entries.end()) {
return OK;
}
auto entry = it->second;
entry->mutex.Lock();
registry_lock.unlock();
int32_t remaining = count;
for (auto* waiter: entry->waiters) {
if (!waiter->wake_requested) {
waiter->wake_requested = true;
waiter->condition.Signal();
if (remaining != INT_MAX && --remaining == 0) {
break;
}
}
}
entry->mutex.Unlock();
return OK;
}
template <typename T>
int WaitImpl(volatile T* address, T expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll) {
if (!IsValidWaitAddress(address)) {
return KERNEL_ERROR_EINVAL;
}
int result = OK;
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
result = WaitLinux(address, expected, timeout_micros, signal_poll);
#else
result = WaitPortable(address, expected, timeout_micros, signal_poll);
#endif
PollSignals(signal_poll);
return result;
}
} // namespace
int Wait32(volatile uint32_t* address, uint32_t expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll) {
return WaitImpl(address, expected, timeout_micros, signal_poll);
}
int Wait64(volatile uint64_t* address, uint64_t expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll) {
return WaitImpl(address, expected, timeout_micros, signal_poll);
}
int Wake(volatile void* address, int32_t count) {
if (!IsValidWakeAddress(address) || count < 0) {
return KERNEL_ERROR_EINVAL;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
return WakeLinux(address, count);
#endif
return WakePortable(address, count);
}
} // namespace Libs::LibKernel::SyncOnAddress
+19
View File
@@ -0,0 +1,19 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_KERNEL_SYNC_ON_ADDRESS_H_
#define EMULATOR_INCLUDE_EMULATOR_KERNEL_SYNC_ON_ADDRESS_H_
#include "common/abi.h"
#include "common/common.h"
namespace Libs::LibKernel::SyncOnAddress {
using signal_poll_func_t = void (*)();
int Wait32(volatile uint32_t* address, uint32_t expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll = nullptr);
int Wait64(volatile uint64_t* address, uint64_t expected, const uint32_t* timeout_micros,
signal_poll_func_t signal_poll = nullptr);
int Wake(volatile void* address, int32_t count);
} // namespace Libs::LibKernel::SyncOnAddress
#endif /* EMULATOR_INCLUDE_EMULATOR_KERNEL_SYNC_ON_ADDRESS_H_ */
+2 -1
View File
@@ -1 +1,2 @@
#define KYTY_GIT_VERSION "@KYTY_GIT_VERSION@"
#define KYTY_GIT_VERSION "@KYTY_GIT_VERSION@"
#define KYTY_GIT_HASH "@KYTY_GIT_HASH@"
+1 -1
View File
@@ -16,6 +16,7 @@ file(GLOB launcher_forms
add_executable(launcher WIN32
${launcher_src}
${launcher_forms}
resources.qrc
)
@@ -37,7 +38,6 @@ elseif(LINUX)
set_target_properties(launcher PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS}")
endif()
add_dependencies(launcher KytyGitVersion)
add_dependencies(launcher kyty_emulator)
if(KYTY_CLANG_CL)
@@ -78,6 +78,16 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="checkBox_fullscreen">
<property name="toolTip">
<string>Run in borderless desktop fullscreen</string>
</property>
<property name="text">
<string>Fullscreen</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -69,6 +69,31 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="input_mapping_button">
<property name="minimumSize">
<size>
<width>36</width>
<height>36</height>
</size>
</property>
<property name="toolTip">
<string>Edit input mapping</string>
</property>
<property name="text">
<string/>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="edit_button">
<property name="minimumSize">
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#e8f1ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M8 4h11l5 5v7M24 9h-5V4M16 27H8a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"/>
<path d="m15 24 1-4 8-8 4 4-8 8-4 1Z"/>
<path d="m22 14 4 4" opacity=".8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 336 B

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#e8f1ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M5 8h3M14 8h13M5 16h13M24 16h3M5 24h6M17 24h10"/>
<circle cx="11" cy="8" r="3"/>
<circle cx="21" cy="16" r="3"/>
<circle cx="14" cy="24" r="3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 344 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#e8f1ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M10 10h12c3.6 0 5.8 2.3 6.6 6.8l.9 5.1c.4 2.4-.8 4.1-2.8 4.1-1.4 0-2.5-.7-3.6-2.3L21.3 21H10.7l-1.8 2.7C7.8 25.3 6.7 26 5.3 26c-2 0-3.2-1.7-2.8-4.1l.9-5.1C4.2 12.3 6.4 10 10 10Z"/>
<path d="M8 16h6M11 13v6"/>
<circle cx="22" cy="15" r="1" fill="#e8f1ff" stroke="none"/>
<circle cx="25" cy="18" r="1" fill="#e8f1ff" stroke="none"/>
<path d="M13 7h6M16 4v6" opacity=".8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 574 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#e8f1ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M6 9h20M12 9V5h8v4M9 9l1 18h12l1-18"/>
<path d="M14 14v8M18 14v8" opacity=".8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 272 B

+9 -1
View File
@@ -85,6 +85,7 @@ public:
QString game_comment;
Resolution screen_resolution = Resolution::R1280X720;
bool fullscreen_enabled = false;
int vblank_frequency = 60;
int console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = true;
@@ -98,11 +99,13 @@ public:
QString printf_output_file = "_kyty.txt";
ProfilerDirection profiler_direction = ProfilerDirection::None;
bool renderdoc_enabled = false;
QStringList host_input_mapping;
QString elf = QStringLiteral("eboot.bin");
void CopyEmulatorSettingsFrom(const Configuration& other) {
screen_resolution = other.screen_resolution;
fullscreen_enabled = other.fullscreen_enabled;
vblank_frequency = other.vblank_frequency;
console_language = other.console_language;
vulkan_validation_enabled = other.vulkan_validation_enabled;
@@ -116,6 +119,7 @@ public:
printf_output_file = other.printf_output_file;
profiler_direction = other.profiler_direction;
renderdoc_enabled = other.renderdoc_enabled;
host_input_mapping = other.host_input_mapping;
}
void CopyFrom(const Configuration& other) {
@@ -138,6 +142,7 @@ public:
KYTY_CFG_SET(game_path);
KYTY_CFG_SET(custom_settings);
KYTY_CFG_SET(screen_resolution);
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
@@ -151,6 +156,7 @@ public:
KYTY_CFG_SET(printf_output_file);
KYTY_CFG_SET(profiler_direction);
KYTY_CFG_SET(renderdoc_enabled);
s->setValue("host_input_mapping", host_input_mapping);
KYTY_CFG_SET(elf);
}
@@ -160,6 +166,7 @@ public:
KYTY_CFG_GET(game_path);
KYTY_CFG_GET(custom_settings);
KYTY_CFG_GET(screen_resolution);
KYTY_CFG_GET(fullscreen_enabled);
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
console_language = s->value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
@@ -176,7 +183,8 @@ public:
KYTY_CFG_GET(printf_output_file);
KYTY_CFG_GET(profiler_direction);
KYTY_CFG_GET(renderdoc_enabled);
elf = s->value("elf", elf).toString();
host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList();
elf = s->value("elf", elf).toString();
}
};
@@ -38,7 +38,10 @@ public:
[[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; }
[[nodiscard]] const QStringList& GetGameDirectories() const { return m_game_dirs; }
[[nodiscard]] bool CanViewSelectedTrophies() const;
[[nodiscard]] const QStringList& GetHostInputMapping() const {
return m_global_info.host_input_mapping;
}
[[nodiscard]] bool CanViewSelectedTrophies() const;
bool EnsureGameDirectory();
void ScanGameDirectory();
@@ -58,6 +61,7 @@ protected slots:
void edit_configuration();
void delete_configuartion();
void edit_global_settings();
void edit_input_mapping();
void run_configuration();
void list_currentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous);
void list_itemDoubleClicked(QTreeWidgetItem* witem, int column);
+31
View File
@@ -0,0 +1,31 @@
#ifndef LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
#define LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
#include <QDialog>
#include <QStringList>
class QPushButton;
class QTreeWidget;
class QTreeWidgetItem;
class QWidget;
class InputMappingDialog final: public QDialog {
public:
explicit InputMappingDialog(const QStringList& mapping, QWidget* parent = nullptr);
[[nodiscard]] QStringList Mapping() const;
private:
void ChangeBinding();
void ClearBinding();
void RestoreDefaults();
void SetBinding(QTreeWidgetItem* item, const QString& binding);
void UpdateButtons();
QTreeWidget* m_bindings = nullptr;
QPushButton* m_change_button = nullptr;
QPushButton* m_clear_button = nullptr;
bool m_customized = false;
};
#endif /* LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ */
+8
View File
@@ -0,0 +1,8 @@
<RCC>
<qresource prefix="/icons">
<file alias="edit-configuration.svg">icons/edit-configuration.svg</file>
<file alias="global-settings.svg">icons/global-settings.svg</file>
<file alias="input-mapping.svg">icons/input-mapping.svg</file>
<file alias="remove-configuration.svg">icons/remove-configuration.svg</file>
</qresource>
</RCC>
@@ -153,6 +153,7 @@ static void ListInit(QComboBox* combo, T value) {
void ConfigurationEditDialog::Init(const Configuration& info) {
ListInit(m_ui->comboBox_screen_resolution, info.screen_resolution);
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->comboBox_console_language->clear();
m_ui->comboBox_console_language->addItems(CONSOLE_LANGUAGE_NAMES);
@@ -279,6 +280,7 @@ void ConfigurationEditDialog::moveEvent(QMoveEvent* event) {
static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) {
info.screen_resolution =
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.console_language = ui.comboBox_console_language->currentIndex();
info.vulkan_validation_enabled = ui.checkBox_vulkan_validation->isChecked();
+17 -3
View File
@@ -6,6 +6,7 @@
#include "configurationEditDialog.h"
#include "configurationItem.h"
#include "gameListTreeWidget.h"
#include "inputMappingDialog.h"
#include "mainDialog.h"
#include "patchesDialog.h"
#include "trophyViewerDialog.h"
@@ -169,6 +170,7 @@ static void ApplyGameListStyle(Ui::ConfigurationListWidget* ui) {
"QToolButton:disabled { background: transparent; }"));
ui->edit_button->setStyleSheet(ui->global_settings_button->styleSheet());
ui->delete_button->setStyleSheet(ui->global_settings_button->styleSheet());
ui->input_mapping_button->setStyleSheet(ui->global_settings_button->styleSheet());
}
static void AddSaveDataDir(QStringList* dirs, QSet<QString>* seen, const QString& root,
@@ -226,10 +228,12 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
m_ui->setupUi(this);
ApplyGameListStyle(m_ui);
m_ui->global_settings_button->setIcon(style()->standardIcon(QStyle::SP_FileDialogDetailedView));
m_ui->global_settings_button->setIcon(QIcon(QStringLiteral(":/icons/global-settings.svg")));
m_ui->global_settings_button->setToolTip(tr("Edit global settings and game folders"));
m_ui->edit_button->setIcon(style()->standardIcon(QStyle::SP_FileIcon));
m_ui->delete_button->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton));
m_ui->input_mapping_button->setIcon(QIcon(QStringLiteral(":/icons/input-mapping.svg")));
m_ui->input_mapping_button->setToolTip(tr("Edit global input mapping"));
m_ui->edit_button->setIcon(QIcon(QStringLiteral(":/icons/edit-configuration.svg")));
m_ui->delete_button->setIcon(QIcon(QStringLiteral(":/icons/remove-configuration.svg")));
m_ui->delete_button->setEnabled(false);
m_ui->edit_button->setEnabled(false);
@@ -249,6 +253,8 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
connect(m_ui->global_settings_button, &QToolButton::clicked, this,
&ConfigurationListWidget::edit_global_settings);
connect(m_ui->input_mapping_button, &QToolButton::clicked, this,
&ConfigurationListWidget::edit_input_mapping);
connect(m_ui->edit_button, &QToolButton::clicked, this,
&ConfigurationListWidget::edit_configuration);
connect(m_ui->delete_button, &QToolButton::clicked, this,
@@ -717,6 +723,14 @@ void ConfigurationListWidget::edit_global_settings() {
}
}
void ConfigurationListWidget::edit_input_mapping() {
InputMappingDialog dialog(m_global_info.host_input_mapping, this);
if (dialog.exec() == QDialog::Accepted) {
m_global_info.host_input_mapping = dialog.Mapping();
WriteSettings();
}
}
void ConfigurationListWidget::ClearCustomSettings(ConfigurationItem* item) {
delete m_custom_infos.take(item->GetInfo().game_path);
}
+311
View File
@@ -0,0 +1,311 @@
#include "inputMappingDialog.h"
#include <QAbstractItemView>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QHash>
#include <QHeaderView>
#include <QKeyEvent>
#include <QKeySequence>
#include <QLabel>
#include <QMouseEvent>
#include <QPushButton>
#include <QStringList>
#include <QTreeWidget>
#include <QVBoxLayout>
namespace {
constexpr int BINDING_COLUMN = 1;
struct PadButton {
const char* id;
const char* label;
const char* default_binding;
};
constexpr PadButton PAD_BUTTONS[] = {
{"Up", "D-pad Up", "Up"},
{"Down", "D-pad Down", "Down"},
{"Left", "D-pad Left", "Left"},
{"Right", "D-pad Right", "Right"},
{"Triangle", "Triangle", "I"},
{"Circle", "Circle", "L"},
{"Cross", "Cross", "J"},
{"Square", "Square", "K"},
{"L1", "L1", "Q"},
{"R1", "R1", "E"},
{"L2", "L2", ""},
{"R2", "R2", ""},
{"L3", "L3", "Left Shift"},
{"R3", "R3", "Left Ctrl"},
{"Options", "Options", "Return"},
{"TouchPad", "Touch pad", "Backspace"},
};
QString KeypadName(int key) {
if (key >= Qt::Key_0 && key <= Qt::Key_9) {
return QStringLiteral("Keypad %1").arg(key - Qt::Key_0);
}
switch (key) {
case Qt::Key_Return:
case Qt::Key_Enter: return QStringLiteral("Keypad Enter");
case Qt::Key_Slash: return QStringLiteral("Keypad /");
case Qt::Key_Asterisk: return QStringLiteral("Keypad *");
case Qt::Key_Minus: return QStringLiteral("Keypad -");
case Qt::Key_Plus: return QStringLiteral("Keypad +");
case Qt::Key_Period: return QStringLiteral("Keypad .");
case Qt::Key_Equal: return QStringLiteral("Keypad =");
case Qt::Key_Comma: return QStringLiteral("Keypad ,");
default: return {};
}
}
QString KeyName(const QKeyEvent& event) {
const int key = event.key();
if (event.modifiers().testFlag(Qt::KeypadModifier)) {
return KeypadName(key);
}
if (key >= Qt::Key_A && key <= Qt::Key_Z) {
return QChar(key);
}
if (key >= Qt::Key_0 && key <= Qt::Key_9) {
return QChar(key);
}
if (key >= Qt::Key_F1 && key <= Qt::Key_F24) {
return QStringLiteral("F%1").arg(key - Qt::Key_F1 + 1);
}
switch (key) {
case Qt::Key_Return:
case Qt::Key_Enter: return QStringLiteral("Return");
case Qt::Key_Backspace: return QStringLiteral("Backspace");
case Qt::Key_Tab: return QStringLiteral("Tab");
case Qt::Key_Shift: return QStringLiteral("Left Shift");
case Qt::Key_Control: return QStringLiteral("Left Ctrl");
case Qt::Key_Alt: return QStringLiteral("Left Alt");
case Qt::Key_Meta: return QStringLiteral("Left GUI");
case Qt::Key_Insert: return QStringLiteral("Insert");
case Qt::Key_Delete: return QStringLiteral("Delete");
case Qt::Key_Home: return QStringLiteral("Home");
case Qt::Key_End: return QStringLiteral("End");
case Qt::Key_PageUp: return QStringLiteral("PageUp");
case Qt::Key_PageDown: return QStringLiteral("PageDown");
case Qt::Key_Left: return QStringLiteral("Left");
case Qt::Key_Right: return QStringLiteral("Right");
case Qt::Key_Up: return QStringLiteral("Up");
case Qt::Key_Down: return QStringLiteral("Down");
case Qt::Key_CapsLock: return QStringLiteral("CapsLock");
case Qt::Key_NumLock: return QStringLiteral("Numlock");
case Qt::Key_ScrollLock: return QStringLiteral("ScrollLock");
case Qt::Key_Pause: return QStringLiteral("Pause");
case Qt::Key_Print: return QStringLiteral("PrintScreen");
default: break;
}
if (event.modifiers() != Qt::NoModifier) {
return {};
}
const auto name = QKeySequence(key).toString(QKeySequence::PortableText);
return name.size() == 1 ? name : QString();
}
class InputCaptureDialog final: public QDialog {
public:
explicit InputCaptureDialog(QWidget* parent): QDialog(parent) {
setWindowTitle(tr("Set Binding"));
setModal(true);
setMinimumWidth(360);
auto* layout = new QVBoxLayout(this);
m_label = new QLabel(
tr("Press a key or mouse button.\nSpace and F1 are reserved; Esc cancels."), this);
m_label->setAlignment(Qt::AlignCenter);
layout->addWidget(m_label);
}
[[nodiscard]] const QString& Binding() const { return m_binding; }
protected:
void keyPressEvent(QKeyEvent* event) override {
if (event->isAutoRepeat()) {
return;
}
if (event->key() == Qt::Key_Escape) {
reject();
return;
}
if (event->key() == Qt::Key_Space || event->key() == Qt::Key_F1) {
m_label->setText(tr("That key is reserved by the emulator."));
return;
}
m_binding = KeyName(*event);
if (!m_binding.isEmpty()) {
accept();
} else {
m_label->setText(tr("That key is not supported."));
}
}
void mousePressEvent(QMouseEvent* event) override {
switch (event->button()) {
case Qt::LeftButton: m_binding = QStringLiteral("Mouse:Left"); break;
case Qt::RightButton: m_binding = QStringLiteral("Mouse:Right"); break;
case Qt::MiddleButton: m_binding = QStringLiteral("Mouse:Middle"); break;
case Qt::BackButton: m_binding = QStringLiteral("Mouse:X1"); break;
case Qt::ForwardButton: m_binding = QStringLiteral("Mouse:X2"); break;
default: return;
}
accept();
}
private:
QLabel* m_label = nullptr;
QString m_binding;
};
QHash<QString, QString> ParseMapping(const QStringList& mapping) {
QHash<QString, QString> result;
for (const auto& entry: mapping) {
const auto separator = entry.indexOf(QLatin1Char('='));
if (separator > 0 && separator + 1 < entry.size()) {
const auto binding = entry.mid(separator + 1);
for (auto item = result.begin(); item != result.end();) {
if (item.value().compare(binding, Qt::CaseInsensitive) == 0) {
item = result.erase(item);
} else {
++item;
}
}
result.insert(entry.left(separator), binding);
}
}
return result;
}
} // namespace
InputMappingDialog::InputMappingDialog(const QStringList& mapping, QWidget* parent)
: QDialog(parent), m_customized(!mapping.isEmpty()) {
setWindowTitle(tr("Input Mapping"));
resize(440, 520);
auto* layout = new QVBoxLayout(this);
layout->addWidget(new QLabel(tr("Map keyboard or mouse input to DualSense buttons.\n"
"Defaults: left stick W/A/S/D, right stick T/F/G/H."),
this));
m_bindings = new QTreeWidget(this);
m_bindings->setColumnCount(2);
m_bindings->setHeaderLabels({tr("DualSense button"), tr("Host input")});
m_bindings->setRootIsDecorated(false);
m_bindings->setSelectionMode(QAbstractItemView::SingleSelection);
m_bindings->header()->setSectionResizeMode(0, QHeaderView::Stretch);
m_bindings->header()->setSectionResizeMode(1, QHeaderView::Stretch);
layout->addWidget(m_bindings);
const auto parsed = ParseMapping(mapping);
for (const auto& button: PAD_BUTTONS) {
auto* item = new QTreeWidgetItem(m_bindings);
item->setText(0, tr(button.label));
item->setData(0, Qt::UserRole, QString::fromLatin1(button.id));
SetBinding(item, m_customized ? parsed.value(QString::fromLatin1(button.id))
: QString::fromLatin1(button.default_binding));
}
m_bindings->setCurrentItem(m_bindings->topLevelItem(0));
auto* controls = new QHBoxLayout;
m_change_button = new QPushButton(tr("Change..."), this);
m_clear_button = new QPushButton(tr("Clear"), this);
auto* defaults = new QPushButton(tr("Defaults"), this);
controls->addWidget(m_change_button);
controls->addWidget(m_clear_button);
controls->addWidget(defaults);
controls->addStretch();
layout->addLayout(controls);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
layout->addWidget(buttons);
connect(m_bindings, &QTreeWidget::itemDoubleClicked, this,
[this](QTreeWidgetItem*, int) { ChangeBinding(); });
connect(m_bindings, &QTreeWidget::itemSelectionChanged, this, [this]() { UpdateButtons(); });
connect(m_change_button, &QPushButton::clicked, this, [this]() { ChangeBinding(); });
connect(m_clear_button, &QPushButton::clicked, this, [this]() { ClearBinding(); });
connect(defaults, &QPushButton::clicked, this, [this]() { RestoreDefaults(); });
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
UpdateButtons();
}
QStringList InputMappingDialog::Mapping() const {
QStringList result;
if (!m_customized) {
return result;
}
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
const auto* item = m_bindings->topLevelItem(index);
const auto binding = item->data(BINDING_COLUMN, Qt::UserRole).toString();
if (!binding.isEmpty()) {
result.append(item->data(0, Qt::UserRole).toString() + QLatin1Char('=') + binding);
}
}
return result;
}
void InputMappingDialog::ChangeBinding() {
auto* item = m_bindings->currentItem();
if (item == nullptr) {
return;
}
InputCaptureDialog dialog(this);
if (dialog.exec() != QDialog::Accepted) {
return;
}
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
auto* other = m_bindings->topLevelItem(index);
if (other != item && other->data(BINDING_COLUMN, Qt::UserRole)
.toString()
.compare(dialog.Binding(), Qt::CaseInsensitive) == 0) {
SetBinding(other, {});
}
}
SetBinding(item, dialog.Binding());
m_customized = true;
}
void InputMappingDialog::ClearBinding() {
SetBinding(m_bindings->currentItem(), {});
m_customized = true;
}
void InputMappingDialog::RestoreDefaults() {
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
SetBinding(m_bindings->topLevelItem(index),
QString::fromLatin1(PAD_BUTTONS[index].default_binding));
}
m_customized = false;
}
void InputMappingDialog::SetBinding(QTreeWidgetItem* item, const QString& binding) {
if (item == nullptr) {
return;
}
item->setData(BINDING_COLUMN, Qt::UserRole, binding);
item->setText(BINDING_COLUMN, binding.isEmpty() ? tr("None") : binding);
UpdateButtons();
}
void InputMappingDialog::UpdateButtons() {
if (m_change_button == nullptr) {
return;
}
const auto* item = m_bindings->currentItem();
m_change_button->setEnabled(item != nullptr);
m_clear_button->setEnabled(item != nullptr &&
!item->data(BINDING_COLUMN, Qt::UserRole).toString().isEmpty());
}
+10 -1
View File
@@ -204,6 +204,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--screen-width" << r.at(0);
args << "--screen-height" << r.at(1);
if (info.fullscreen_enabled) {
args << "--fullscreen";
}
args << "--vblank-frequency" << QString::number(info.vblank_frequency);
args << "--console-language" << QString::number(info.console_language);
args << "--vulkan-validation" << BoolArg(info.vulkan_validation_enabled);
@@ -217,6 +220,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--printf-output-file" << info.printf_output_file;
args << "--profiler-direction" << EnumToText(info.profiler_direction);
args << "--spirv-debug-printf" << "false";
for (const auto& binding: info.host_input_mapping) {
args << "--keymap" << binding;
}
if (info.renderdoc_enabled) {
args << "--rd";
}
@@ -435,7 +441,10 @@ void MainDialogPrivate::Run() {
m_running_item->SetRunning(true);
m_main_dialog->RunInterpreter(&m_process, m_running_item->GetInfo());
Configuration info;
info.CopyFrom(m_running_item->GetInfo());
info.host_input_mapping = m_ui->widget->GetHostInputMapping();
m_main_dialog->RunInterpreter(&m_process, info);
Update();
}
+57 -13
View File
@@ -6,6 +6,7 @@
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "common/virtualMemory.h"
#include "graphics/guest_gpu/command_processor/commandProcessor.h"
#include "graphics/guest_gpu/gpu_defs.h"
#include "graphics/guest_gpu/graphicsRun.h"
#include "graphics/guest_gpu/hardwareContext.h"
@@ -1444,29 +1445,72 @@ int KYTY_SYSV_ABI GraphicsSuspendPoint() {
return OK;
}
uint32_t* KYTY_SYSV_ABI GraphicsUnknownQj7QZpgr9Uw(CommandBuffer* buf, uint32_t mode,
uint32_t value) {
PRINT_NAME();
// ResetContextState?
LOGF("\t mode = 0x%08" PRIx32 "\n"
"\t value = 0x%08" PRIx32 "\n",
mode, value);
[[nodiscard]] static constexpr uint32_t context_state_op_size_dw(uint32_t operation) {
switch (static_cast<ContextStateOperation>(operation)) {
case ContextStateOperation::Clear: return 5;
case ContextStateOperation::Push:
case ContextStateOperation::Pop: return 27;
case ContextStateOperation::PushClear: return 32;
default: return 0;
}
}
if (buf == nullptr) {
uint32_t* KYTY_SYSV_ABI GraphicsDcbContextStateOp(CommandBuffer* buf, uint32_t operation) {
PRINT_NAME();
LOGF("\t operation = 0x%08" PRIx32 "\n", operation);
const auto size_dw = context_state_op_size_dw(operation);
if (buf == nullptr || size_dw == 0) {
return nullptr;
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(1);
uint32_t* first = nullptr;
const auto append = [&](uint32_t segment_size_dw) {
auto* segment = buf->AllocateDW(segment_size_dw);
if (segment == nullptr) {
return false;
}
std::fill_n(segment, segment_size_dw, 0u);
if (first == nullptr) {
first = segment;
segment[0] = KYTY_PM4(segment_size_dw, Pm4::IT_NOP, Pm4::R_CONTEXT_STATE);
segment[1] = operation;
} else {
segment[0] = KYTY_PM4(segment_size_dw, Pm4::IT_NOP, Pm4::R_ZERO);
}
return true;
};
const auto append_save_restore = [&] {
// The native helper reserves 22 dwords, then emits 5-, 8-, and 9-dword packets.
return buf->ReserveDW(22) && append(5) && append(8) && append(9);
};
if (cmd == nullptr) {
return nullptr;
// Preserve native packet sizes, allocation boundaries, Push/Pop ordering, and
// returned first-packet address. The native packets reference process-private state buffers,
// so their net Cx operation is represented by the first HLE packet and type-3 NOPs.
bool complete = false;
switch (static_cast<ContextStateOperation>(operation)) {
case ContextStateOperation::Clear: complete = append(5); break;
case ContextStateOperation::Push:
complete = append_save_restore() && append(3) && append(2);
break;
case ContextStateOperation::Pop:
complete = append(3) && append_save_restore() && append(2);
break;
case ContextStateOperation::PushClear:
complete = append_save_restore() && append(3) && append(2) && append(5);
break;
default: break;
}
cmd[0] = 0x80000000u;
return complete ? first : nullptr;
}
return cmd;
uint64_t KYTY_SYSV_ABI GraphicsDcbContextStateOpGetSize(uint32_t operation) {
PRINT_NAME();
return static_cast<uint64_t>(context_state_op_size_dw(operation)) * sizeof(uint32_t);
}
uint64_t KYTY_SYSV_ABI GraphicsGetIsTrinityMode() {
+2 -2
View File
@@ -67,8 +67,8 @@ int KYTY_SYSV_ABI GraphicsUnknownJumpPatchSetTarget(uint32_t* cmd, const volatil
int KYTY_SYSV_ABI GraphicsJumpPatchSetTarget(uint32_t* cmd, const volatile uint32_t* target,
uint32_t size_in_dwords);
int KYTY_SYSV_ABI GraphicsSuspendPoint();
uint32_t* KYTY_SYSV_ABI GraphicsUnknownQj7QZpgr9Uw(CommandBuffer* buf, uint32_t mode,
uint32_t value);
uint32_t* KYTY_SYSV_ABI GraphicsDcbContextStateOp(CommandBuffer* buf, uint32_t operation);
uint64_t KYTY_SYSV_ABI GraphicsDcbContextStateOpGetSize(uint32_t operation);
uint64_t KYTY_SYSV_ABI GraphicsGetIsTrinityMode();
uint32_t KYTY_SYSV_ABI GraphicsDriverGetDefaultOwner();
+2 -4
View File
@@ -85,8 +85,6 @@ private:
static GameController* g_controller = nullptr;
static constexpr int KEYBOARD_CONTROLLER_ID = -1000;
static uint8_t pad_connected_count_to_u8(int connected_count) {
return static_cast<uint8_t>(connected_count > 255 ? 255 : connected_count);
}
@@ -117,7 +115,7 @@ KYTY_SUBSYSTEM_INIT(Controller) {
EXIT_IF(g_controller != nullptr);
g_controller = new GameController;
g_controller->Connect(KEYBOARD_CONTROLLER_ID);
g_controller->Connect(HOST_INPUT_CONTROLLER_ID);
}
KYTY_SUBSYSTEM_UNEXPECTED_SHUTDOWN(Controller) {}
@@ -155,7 +153,7 @@ void GameController::CheckActive() {
if (!m_connected_ids.empty()) {
new_active_id = m_connected_ids[0];
for (const auto id: m_connected_ids) {
if (id != KEYBOARD_CONTROLLER_ID) {
if (id != HOST_INPUT_CONTROLLER_ID) {
new_active_id = id;
break;
}
+2
View File
@@ -9,6 +9,8 @@ namespace Libs::Controller {
KYTY_SUBSYSTEM_DEFINE(Controller);
constexpr int HOST_INPUT_CONTROLLER_ID = -1000;
constexpr uint32_t PAD_BUTTON_L3 = 0x00000002;
constexpr uint32_t PAD_BUTTON_R3 = 0x00000004;
constexpr uint32_t PAD_BUTTON_OPTIONS = 0x00000008;
+2 -1
View File
@@ -178,7 +178,8 @@ LIB_DEFINE(InitGraphicsDriver_1) {
LIB_FUNC("bxca0BK4FNg", Gen5::GraphicsUnknownJumpPatchSetTarget);
LIB_FUNC("2BS4EtAaF28", Gen5::GraphicsJumpPatchSetTarget);
LIB_FUNC("h9z6+0hEydk", Gen5::GraphicsSuspendPoint);
LIB_FUNC("qj7QZpgr9Uw", Gen5::GraphicsUnknownQj7QZpgr9Uw);
LIB_FUNC("qj7QZpgr9Uw", Gen5::GraphicsDcbContextStateOp);
LIB_FUNC("H6vHS5cidSA", Gen5::GraphicsDcbContextStateOpGetSize);
LIB_FUNC("BfBDZGbti7A", Gen5::GraphicsGetIsTrinityMode);
LIB_FUNC("dbOlWdppb4o", LibGen5::Graphics5UnknownDb);
+40 -19
View File
@@ -13,6 +13,7 @@
#include "kernel/memory.h"
#include "kernel/pthread.h"
#include "kernel/semaphore.h"
#include "kernel/syncOnAddress.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "libs/network.h"
@@ -2021,6 +2022,12 @@ int64_t KYTY_SYSV_ABI fstat(int d, LibKernel::FileSystem::FileStat* sb) {
return POSIX_N_CALL(LibKernel::FileSystem::KernelFstat(d, sb));
}
int KYTY_SYSV_ABI ftruncate(int d, int64_t length) {
PRINT_NAME();
return POSIX_CALL(LibKernel::FileSystem::KernelFtruncate(d, length));
}
int KYTY_SYSV_ABI socket(int family, int type, int protocol) {
PRINT_NAME();
return Network::Net::Socket(family, type, protocol);
@@ -2128,26 +2135,37 @@ uint64_t KYTY_SYSV_ABI cfwBSQyr5Ys(uint64_t a1, uint64_t a2, uint64_t a3, uint64
return 0;
}
uint64_t KYTY_SYSV_ABI KernelSyncOnAddressV1(uint64_t op, uint64_t address, uint64_t value,
uint64_t size, uint64_t timeout, uint64_t flags) {
static std::atomic_uint32_t log_count = 0;
const auto index = log_count.fetch_add(1, std::memory_order_relaxed);
if (index < 16) {
LOGF("\t libkernel_sync_on_address_v1: op=0x%016" PRIx64 ", address=0x%016" PRIx64
", value=0x%016" PRIx64 ", size=0x%016" PRIx64 ", timeout=0x%016" PRIx64
", flags=0x%016" PRIx64 "\n",
op, address, value, size, timeout, flags);
static void LogExperimentalSyncOnAddress(std::atomic_bool& logged, const char* function_name) {
if (!logged.exchange(true, std::memory_order_relaxed)) {
::printf("WARNING: %s is experimental\n", function_name);
LOGF("WARNING: %s is experimental\n", function_name);
}
}
if (op != 0 && address == 0 && value == 0 && size == 0) {
// This unsupported form is used as a yield/wait by some Unity jobs.
// SleepMicro() uses a sub-millisecond busy wait on Windows, which can
// pin every worker thread when the guest polls this path.
Common::Thread::Sleep(timeout == 0 ? 1 : 2);
}
int KYTY_SYSV_ABI KernelSyncOnAddressWait(volatile uint32_t* address, uint32_t expected,
const uint32_t* timeout_micros) {
return LibKernel::SyncOnAddress::Wait32(address, expected, timeout_micros,
LibKernel::KernelDispatchPendingSignalForCurrentThread);
}
return 0;
int KYTY_SYSV_ABI KernelSyncOnAddressWait32(volatile uint32_t* address, uint32_t expected,
const uint32_t* timeout_micros) {
static std::atomic_bool logged {false};
LogExperimentalSyncOnAddress(logged, "sceKernelSyncOnAddressWait32");
return LibKernel::SyncOnAddress::Wait32(address, expected, timeout_micros,
LibKernel::KernelDispatchPendingSignalForCurrentThread);
}
int KYTY_SYSV_ABI KernelSyncOnAddressWait64(volatile uint64_t* address, uint64_t expected,
const uint32_t* timeout_micros) {
static std::atomic_bool logged {false};
LogExperimentalSyncOnAddress(logged, "sceKernelSyncOnAddressWait64");
return LibKernel::SyncOnAddress::Wait64(address, expected, timeout_micros,
LibKernel::KernelDispatchPendingSignalForCurrentThread);
}
int KYTY_SYSV_ABI KernelSyncOnAddressWake(volatile void* address, int32_t count) {
return LibKernel::SyncOnAddress::Wake(address, count);
}
LIB_DEFINE(InitLibKernel_1_Posix) {
@@ -2159,6 +2177,7 @@ LIB_DEFINE(InitLibKernel_1_Posix) {
LIB_FUNC("yS8U2TGCe1A", nanosleep);
LIB_FUNC("E6ao34wPw+U", stat);
LIB_FUNC("JGMio+21L4c", mkdir);
LIB_FUNC("ih4CD9-gghM", Posix::ftruncate);
LIB_FUNC("pDuPEf3m4fI", Posix::sem_init);
LIB_FUNC("cDW233RAwWo", Posix::sem_destroy);
LIB_FUNC("YCV5dGGBcCo", Posix::sem_wait);
@@ -3398,8 +3417,10 @@ LIB_DEFINE(InitLibKernel_1) {
LIB_FUNC("Xjoosiw+XPI", LibKernel::KernelUuidCreate);
LIB_FUNC("DLORcroUqbc", LibKernel::KernelGetOpenPsId);
LIB_FUNC("zE-wXIZjLoM", LibKernel::KernelDebugRaiseExceptionOnReleaseMode);
LIB_FUNC("Hc4CaR6JBL0", Posix::KernelSyncOnAddressV1);
LIB_FUNC("q2y-wDIVWZA", Posix::KernelSyncOnAddressV1);
LIB_FUNC("Hc4CaR6JBL0", Posix::KernelSyncOnAddressWait);
LIB_FUNC("B2n8aDorSH4", Posix::KernelSyncOnAddressWait32);
LIB_FUNC("PZQhiiLXRFs", Posix::KernelSyncOnAddressWait64);
LIB_FUNC("q2y-wDIVWZA", Posix::KernelSyncOnAddressWake);
AddLibkernelUnityFunc(s, "Qhv5ARAoOEc",
reinterpret_cast<uint64_t>(LibKernel::KernelRemoveExceptionHandler),
+16 -5
View File
@@ -360,9 +360,10 @@ static KYTY_SYSV_ABI void RunEntry(uint64_t addr, EntryParams* params, atexit_fu
auto* func = reinterpret_cast<entry_func_t>(addr);
if (stack_top != nullptr) {
const auto guest_rsp =
const auto aligned_stack_top =
reinterpret_cast<uintptr_t>(stack_top) & ~static_cast<uintptr_t>(0x0f);
const auto guest_rbp = guest_rsp - 4u * sizeof(uint64_t);
const auto guest_rsp = aligned_stack_top - 2u * sizeof(uintptr_t);
const auto guest_rbp = guest_rsp;
auto* guest_root_frame = reinterpret_cast<uintptr_t*>(guest_rbp);
guest_root_frame[0] = 0;
@@ -507,6 +508,13 @@ struct MainEntryStackTestState {
static KYTY_SYSV_ABI void TestMainEntryStackCallback(EntryParams* params,
atexit_func_t /*atexit_func*/) {
auto* state = reinterpret_cast<MainEntryStackTestState*>(const_cast<char*>(params->argv[0]));
asm volatile("pushq %%r15\n\t"
"pushq %%r14\n\t"
"popq %%r14\n\t"
"popq %%r15\n\t"
:
:
: "memory");
asm volatile("movq %%rsp, %0" : "=r"(state->rsp) : : "memory");
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
asm volatile("movq %%gs:0x08, %0\n\t"
@@ -529,6 +537,8 @@ bool TestMainEntryUsesGuestStack() {
MainEntryStackTestState state {};
EntryParams params {};
params.argv[0] = reinterpret_cast<const char*>(&state);
std::memset(reinterpret_cast<void*>(stack_base), 0xcd, stack_size);
auto* root_frame = reinterpret_cast<const uintptr_t*>(stack_base + stack_size) - 2;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
uintptr_t original_teb_stack_base = 0;
@@ -558,9 +568,10 @@ bool TestMainEntryUsesGuestStack() {
constexpr bool teb_ok = true;
#endif
const bool rsp_ok = state.rsp >= stack_base && state.rsp < stack_base + stack_size;
const bool freed = Libs::LibKernel::Memory::FreeGuestMemory(stack_base, stack_size);
return state.called && rsp_ok && teb_ok && freed;
const bool rsp_ok = state.rsp >= stack_base && state.rsp < stack_base + stack_size;
const bool root_ok = root_frame[0] == 0 && root_frame[1] == 0;
const bool freed = Libs::LibKernel::Memory::FreeGuestMemory(stack_base, stack_size);
return state.called && rsp_ok && root_ok && teb_ok && freed;
}
bool TestModuleRelocationUsesWritableHostMapping() {
+14
View File
@@ -47,6 +47,7 @@ static void PrintUsage() {
" --game-patch <json> Validated patch plan to apply before entry.\n");
::printf(" --screen-width <num> Window width. Default: 1280.\n");
::printf(" --screen-height <num> Window height. Default: 720.\n");
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n");
::printf(" --vulkan-validation <true|false> Enable Vulkan validation.\n");
@@ -63,6 +64,7 @@ static void PrintUsage() {
::printf(" --spirv-debug-printf <true|false> Enable SPIR-V debug printf.\n");
::printf(
" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
::printf(" --keymap <Pad=Key> DualSense mapping; may be repeated.\n");
::printf(" --rd Enable RenderDoc capture.\n");
}
@@ -131,6 +133,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
continue;
}
if (arg == "--fullscreen") {
options.config.fullscreen_enabled = true;
continue;
}
if (!Common::StartsWith(arg, "--")) {
::printf("game input must be provided with --game\n");
return false;
@@ -241,6 +248,13 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--keymap") {
const auto split = value.find('=');
if (split == std::string::npos || split == 0 || split + 1 == value.size()) {
::printf("invalid keymap: %s\n", value.c_str());
return false;
}
options.config.keymap.push_back(value);
} else {
::printf("unknown option: %s\n", arg.c_str());
return false;
-2
View File
@@ -101,8 +101,6 @@ if(KYTY_CLANG_CL)
string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} /utf-8 /Oy- /wd4244 /wd4305 /wd4800 /wd4345")
add_link_options("$<$<CONFIG:RELEASE>:/OPT:NOREF>")
if(KYTY_WARNINGS_ARE_ERRORS)
#set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} /WX")
+87 -68
View File
@@ -3,13 +3,15 @@
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <utility>
#include <vector>
namespace {
using Owners = std::vector<uint32_t>;
using Table = Libs::Graphics::MultiLevelPageTable<Owners>;
using OwnerIndex = Libs::Graphics::MultiRangePageOwnerIndex<uint32_t>;
using PageOwners = Libs::Graphics::InlinePageOwnerList<uint32_t, 16>;
using OwnerTable = Libs::Graphics::MultiLevelPageTable<PageOwners, 20, 40, 10>;
void Check(bool value, const char* text) {
if (!value) {
@@ -77,76 +79,93 @@ void TestAddressSpaceBoundaries() {
"final page supports allocating and nonallocating access");
}
void TestMultiRangeRegistrationDeduplicatesPages() {
OwnerIndex index;
// Depth and stencil-like planes overlap tracking pages and share one 1 MiB bucket.
Check(index.Register(7, {{0x101000, 0x2800}, {0x102000, 0x3000}}),
"multi-range owner registers");
Check(index.CoarseMembershipCount(1) == 1,
"one owner is inserted once in a shared 1 MiB bucket");
Check(index.TrackingMembershipCount(0x102) == 1,
"overlapping planes insert one 4 KiB membership");
Check(!index.Register(7, {{0x101000, 0x1000}}), "duplicate owner registration hard-fails");
void TestInlineOwnerStorageAndOverflow() {
PageOwners owners;
for (uint32_t owner = 1; owner <= 18; ++owner) {
owners.push_back(owner);
}
Check(owners.size() == 18 && owners.front() == 1,
"inline owner storage grows past its 16-owner capacity");
uint32_t expected = 1;
for (const uint32_t owner: owners) {
Check(owner == expected++, "overflow preserves registration order");
}
Check(owners.Erase(5) && owners.size() == 17 && !owners.Contains(5),
"overflow erase removes only the requested owner");
Check(owners.Erase(18) && owners.size() == 16,
"overflow storage shrinks back to inline capacity");
const std::vector<uint32_t> remaining {1, 2, 3, 4, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17};
size_t remaining_index = 0;
for (const uint32_t owner: owners) {
Check(owner == remaining[remaining_index++],
"overflow-to-inline shrink preserves every owner");
}
Check(!owners.Erase(99), "missing inline owner is reported without mutation");
const auto owners = index.Query(0x100000, 0x10000);
Check(owners.size() == 1 && owners.front() == 7, "multi-page query returns an owner once");
PageOwners moved = std::move(owners);
Check(owners.empty() && moved.size() == remaining.size() && moved.front() == 1,
"moving a full inline owner list leaves the source empty");
PageOwners partial;
partial.push_back(41);
partial.push_back(42);
PageOwners partial_moved = std::move(partial);
Check(partial.empty() && partial_moved.size() == 2 && partial_moved[1] == 42,
"moving a partially populated list copies only live owners");
PageOwners assigned;
assigned.push_back(99);
assigned = std::move(partial_moved);
Check(partial_moved.empty() && assigned.size() == 2 && assigned.front() == 41,
"move assignment replaces an inline list without reading inactive slots");
PageOwners empty;
PageOwners empty_moved = std::move(empty);
Check(empty.empty() && empty_moved.empty(), "moving an empty owner list is safe");
}
void TestSharedPageUnregisterLifecycle() {
OwnerIndex index;
const std::vector<OwnerIndex::ByteRange> ranges {{0x202000, 0x2000}};
Check(index.Register(11, ranges) && index.Register(22, ranges),
"two owners register on identical pages");
Check(index.CoarseMembershipCount(2) == 2 && index.TrackingMembershipCount(0x202) == 2,
"coarse and tracking pages retain both owners");
std::vector<OwnerIndex::ByteRange> releases;
Check(index.Unregister(11, releases), "first owner unregisters");
Check(releases.empty(), "shared tracking pages are not released with one owner remaining");
Check(index.Query(0x202000, 1).size() == 1 && index.Query(0x202000, 1).front() == 22,
"unregistering one owner preserves the other");
Check(!index.Unregister(11, releases), "missing membership hard-fails without mutation");
Check(index.Unregister(22, releases), "final owner unregisters");
Check(releases.size() == 1 && releases.front().address == 0x202000 &&
releases.front().size == 0x2000,
"adjacent final-owner tracking pages return one contiguous release");
void TestOneMiBRegistrationGranularity() {
static_assert(OwnerTable::kPageBits == 20);
OwnerTable table;
OwnerTable::PageRange pages {};
constexpr uint64_t range_size = 64ull * 1024 * 1024;
Check(OwnerTable::TryGetPageRange(0, range_size, pages), "large owner range is valid");
Check(pages.first == 0 && pages.last_exclusive == 64,
"64 MiB registration touches exactly 64 one-MiB entries");
for (size_t page = pages.first; page < pages.last_exclusive; ++page) {
table[page].push_back(7);
}
Check(table.AllocatedBucketCount() == 1,
"large registration uses one sparse second-level bucket");
for (size_t page = pages.first; page < pages.last_exclusive; ++page) {
auto* owners = table.Find(page);
Check(owners != nullptr && owners->size() == 1 && owners->front() == 7,
"each touched one-MiB entry retains the owner once");
Check(owners->Erase(7), "large owner unregisters by coarse page");
}
}
void TestStrictByteFilteringAndPredicate() {
OwnerIndex index;
Check(index.Register(31, {{0x300100, 0x100}}), "first byte-disjoint owner registers");
Check(index.Register(32, {{0x300800, 0x100}}), "second byte-disjoint owner registers");
Check(index.Register(33, {{0x30f000, 0x100}}), "coarse-only owner registers");
Check(index.TrackingMembershipCount(0x300) == 2,
"byte-disjoint owners share one tracking page");
Check(index.CoarseMembershipCount(3) == 3,
"all owners share one coarse candidate bucket");
Check(index.Query(0x300400, 0x40).empty(), "page hit without byte overlap is filtered out");
const auto page_candidates = index.QueryCandidates(0x300400, 0x40);
Check(page_candidates.size() == 2,
"fault candidate query retains touched-page owners and rejects coarse-only owners");
const auto first = index.Query(0x300180, 0x10);
Check(first.size() == 1 && first.front() == 31,
"strict byte overlap selects only the matching owner");
const auto predicate_filtered =
index.Query(0x300000, 0x1000, [](uint32_t owner) { return owner == 32; });
Check(predicate_filtered.size() == 1 && predicate_filtered.front() == 32,
"supplied predicate filters query owners");
void TestSharedCoarsePageLifecycle() {
OwnerTable table;
auto& owners = table[2];
owners.push_back(11);
owners.push_back(22);
Check(owners.size() == 2, "two images share one coarse page");
Check(owners.Erase(11) && owners.size() == 1 && owners.front() == 22,
"unregistering one image preserves its coarse-page neighbor");
Check(!owners.Erase(11), "double unregister is rejected without mutation");
Check(owners.Erase(22) && owners.empty(), "final coarse-page owner unregisters");
}
void TestOwnerIndexAddressSpaceBoundary() {
OwnerIndex index;
constexpr uint64_t last_byte = OwnerIndex::CoarseTable::kAddressSpaceSize - 1;
Check(index.Register(41, {{last_byte, 1}}), "final guest byte registers");
const auto exact = index.Query(last_byte, 1);
Check(exact.size() == 1 && exact.front() == 41,
"strict query finds an exact overlap at the final guest byte");
Check(index.Query(last_byte - 1, 1).empty(),
"strict query preserves half-open overlap boundaries");
const auto page_candidates = index.QueryCandidates(last_byte - 1, 1);
Check(page_candidates.size() == 1 && page_candidates.front() == 41,
"page candidate query retains a byte-disjoint owner on the final tracking page");
void TestOneMiBBoundaries() {
OwnerTable::PageRange range {};
Check(OwnerTable::TryGetPageRange(0x0fffff, 2, range),
"range crossing a one-MiB boundary is valid");
Check(range.first == 0 && range.last_exclusive == 2,
"cross-boundary registration touches both coarse pages");
Check(OwnerTable::TryGetPageRange(OwnerTable::kAddressSpaceSize - 1, 1, range),
"final guest byte maps to a coarse owner page");
Check(range.first == OwnerTable::kPageCount - 1 &&
range.last_exclusive == OwnerTable::kPageCount,
"final guest byte uses the final one-MiB page");
}
} // namespace
@@ -156,10 +175,10 @@ int main() {
TestCrossBucketRange();
TestQueriesDoNotAllocate();
TestAddressSpaceBoundaries();
TestMultiRangeRegistrationDeduplicatesPages();
TestSharedPageUnregisterLifecycle();
TestStrictByteFilteringAndPredicate();
TestOwnerIndexAddressSpaceBoundary();
TestInlineOwnerStorageAndOverflow();
TestOneMiBRegistrationGranularity();
TestSharedCoarsePageLifecycle();
TestOneMiBBoundaries();
std::printf("ImagePageTableTests: all cases passed\n");
return 0;
}
+51
View File
@@ -6,7 +6,9 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <semaphore>
#include <string>
#include <thread>
#include <vector>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
@@ -265,6 +267,7 @@ void TestRangeInvalidation() {
flushes++;
tracker.ForEachDownloadRange<true>(address + 16, size - 32,
[](uint64_t, uint64_t) noexcept {});
tracker.MarkRegionAsCpuModified(address + 16, size - 32);
});
Check(flushes == 1 && !tracker.IsRegionGpuModified(address, size) &&
tracker.IsRegionCpuModified(address, size) && IsWritable(memory) &&
@@ -277,6 +280,53 @@ void TestRangeInvalidation() {
Release(page_manager, memory, size);
}
void TestGpuReacquisitionAfterInvalidation() {
TrackerHarness harness;
auto &tracker = harness.tracker;
auto &page_manager = harness.page_manager;
const auto page_size = page_manager.GetPageSize();
auto *memory = Allocate(page_manager, 1);
const auto address = reinterpret_cast<uint64_t>(memory);
tracker.ForEachUploadRange(
address, page_size, true, [](uint64_t, uint64_t) noexcept {},
[]() noexcept {});
Check(tracker.IsRegionGpuModified(address, page_size) &&
!tracker.IsRegionCpuModified(address, page_size),
"reacquisition setup did not establish GPU ownership");
uint32_t flushes = 0;
uint32_t uploads = 0;
std::binary_semaphore reacquire{0};
std::binary_semaphore reacquired{0};
std::jthread publisher([&] {
reacquire.acquire();
tracker.ForEachUploadRange(
address + 16, 32, true,
[&](uint64_t, uint64_t) noexcept { uploads++; }, []() noexcept {});
reacquired.release();
});
tracker.InvalidateRegion(address + 16, 32, [&] {
flushes++;
tracker.ForEachDownloadRange<true>(address + 16, 32,
[](uint64_t, uint64_t) noexcept {});
tracker.MarkRegionAsCpuModified(address + 16, 32);
reacquire.release();
reacquired.acquire();
});
publisher.join();
Check(flushes == 1 && uploads == 1 &&
tracker.IsRegionGpuModified(address, page_size) &&
!tracker.IsRegionCpuModified(address, page_size) &&
!IsWritable(memory),
"invalidation rejected a new generation of GPU ownership");
tracker.UnmarkRegionAsGpuModified(address, page_size);
tracker.MarkRegionAsCpuModified(address, page_size);
tracker.UntrackMemory(address, page_size);
Release(page_manager, memory, page_size);
}
void TestGpuDirtyBits() {
TrackerHarness harness;
auto &tracker = harness.tracker;
@@ -635,6 +685,7 @@ int main(int argc, char **argv) {
TestQueriesDoNotRequireMappedOwnership();
TestCpuDirtyUpload();
TestRangeInvalidation();
TestGpuReacquisitionAfterInvalidation();
TestGpuDirtyBits();
TestExactDirtyIntervalsSharingTrackerPage();
TestGpuDownloadProtectionMirrors();
+28 -152
View File
@@ -35,171 +35,50 @@ void YieldMany() {
}
}
void TestFaultBlocksPublisher() {
ResourceMutex mutex;
std::atomic_bool publisher_started {false};
std::atomic_bool publisher_entered {false};
std::thread publisher;
void TestOwnership() {
ResourceMutex mutex;
Check(!mutex.IsOwnedByCurrentThread(), "fresh mutex reported an owner");
{
ResourceMutex::FaultScope fault(mutex);
publisher = std::thread([&] {
publisher_started.store(true, std::memory_order_release);
std::lock_guard lock(mutex);
publisher_entered.store(true, std::memory_order_release);
});
while (!publisher_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"publisher entered during active fault transaction");
std::lock_guard lock(mutex);
Check(mutex.IsOwnedByCurrentThread(), "owner tracking was not installed");
}
publisher.join();
Check(publisher_entered.load(std::memory_order_acquire),
"publisher did not resume after fault transaction");
Check(!mutex.IsOwnedByCurrentThread(), "owner tracking survived unlock");
}
void TestFaultDrainsExistingOwner() {
void TestSerializesTransactions() {
ResourceMutex mutex;
std::unique_lock owner(mutex);
std::atomic_bool fault_started {false};
std::atomic_bool fault_entered {false};
std::atomic_bool release_fault {false};
std::atomic_bool publisher_started {false};
std::atomic_bool publisher_entered {false};
std::thread fault([&] {
fault_started.store(true, std::memory_order_release);
ResourceMutex::FaultScope scope(mutex);
fault_entered.store(true, std::memory_order_release);
while (!release_fault.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
});
while (!fault_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!fault_entered.load(std::memory_order_acquire),
"fault transaction did not wait for existing owner");
owner.unlock();
while (!fault_entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
std::thread publisher([&] {
publisher_started.store(true, std::memory_order_release);
std::atomic_bool contender_started {false};
std::atomic_bool contender_entered {false};
std::thread contender([&] {
contender_started.store(true, std::memory_order_release);
std::lock_guard lock(mutex);
publisher_entered.store(true, std::memory_order_release);
contender_entered.store(true, std::memory_order_release);
});
while (!publisher_started.load(std::memory_order_acquire)) {
while (!contender_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"publisher entered before drained fault completed");
release_fault.store(true, std::memory_order_release);
fault.join();
publisher.join();
Check(publisher_entered.load(std::memory_order_acquire),
"publisher did not resume after drained fault");
}
void TestFaultScopesSerialize() {
ResourceMutex mutex;
std::atomic_bool first_entered {false};
std::atomic_bool second_started {false};
std::atomic_bool second_entered {false};
std::atomic_bool release_first {false};
std::atomic_bool release_second {false};
std::atomic_bool publisher_entered {false};
std::thread first([&] {
ResourceMutex::FaultScope scope(mutex);
first_entered.store(true, std::memory_order_release);
while (!release_first.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
});
while (!first_entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
std::thread second([&] {
second_started.store(true, std::memory_order_release);
ResourceMutex::FaultScope scope(mutex);
second_entered.store(true, std::memory_order_release);
while (!release_second.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
});
while (!second_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!second_entered.load(std::memory_order_acquire),
"second fault transaction entered before first completed");
release_first.store(true, std::memory_order_release);
while (!second_entered.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
std::thread publisher([&] {
std::lock_guard lock(mutex);
publisher_entered.store(true, std::memory_order_release);
});
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"publisher entered during second fault transaction");
release_second.store(true, std::memory_order_release);
first.join();
second.join();
publisher.join();
Check(publisher_entered.load(std::memory_order_acquire),
"publisher did not resume after serialized faults");
}
void TestPreownedFaultKeepsResourceTransaction() {
ResourceMutex mutex;
std::atomic_bool publisher_started {false};
std::atomic_bool publisher_entered {false};
std::thread publisher;
std::unique_lock owner(mutex);
{
ResourceMutex::FaultScope fault(mutex);
publisher = std::thread([&] {
publisher_started.store(true, std::memory_order_release);
std::lock_guard lock(mutex);
publisher_entered.store(true, std::memory_order_release);
});
while (!publisher_started.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"publisher entered during preowned fault transaction");
}
YieldMany();
Check(!publisher_entered.load(std::memory_order_acquire),
"preowned fault released its outer resource transaction");
Check(!contender_entered.load(std::memory_order_acquire),
"contender entered an active resource transaction");
owner.unlock();
publisher.join();
Check(publisher_entered.load(std::memory_order_acquire),
"publisher did not resume after outer transaction");
contender.join();
Check(contender_entered.load(std::memory_order_acquire),
"contender did not resume after resource transaction");
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
[[noreturn]] void RunDeathCase(const char* name) {
[[noreturn]] void RunDeathCase() {
ResourceMutex mutex;
if (std::strcmp(name, "recursive-lock") == 0) {
std::lock_guard first(mutex);
std::lock_guard second(mutex);
} else if (std::strcmp(name, "nested-fault") == 0) {
ResourceMutex::FaultScope first(mutex);
ResourceMutex::FaultScope second(mutex);
}
std::lock_guard first(mutex);
std::lock_guard second(mutex);
std::_Exit(0x7f);
}
void CheckDeathCase(const char* name) {
void CheckDeathCase() {
char path[MAX_PATH] {};
Check(GetModuleFileNameA(nullptr, path, MAX_PATH) != 0, "GetModuleFileName failed");
std::string command = std::string("\"") + path + "\" --death " + name;
std::string command = std::string("\"") + path + "\" --death";
std::vector<char> mutable_command(command.begin(), command.end());
mutable_command.push_back('\0');
STARTUPINFOA startup {sizeof(startup)};
@@ -224,20 +103,17 @@ void CheckDeathCase(const char* name) {
int main(int argc, char** argv) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (argc == 3 && std::strcmp(argv[1], "--death") == 0) {
RunDeathCase(argv[2]);
if (argc == 2 && std::strcmp(argv[1], "--death") == 0) {
RunDeathCase();
}
#else
(void)argc;
(void)argv;
#endif
TestFaultBlocksPublisher();
TestFaultDrainsExistingOwner();
TestFaultScopesSerialize();
TestPreownedFaultKeepsResourceTransaction();
TestOwnership();
TestSerializesTransactions();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
CheckDeathCase("recursive-lock");
CheckDeathCase("nested-fault");
CheckDeathCase();
#endif
std::puts("ResourceMutexTests: all cases passed");
return 0;
+499 -23
View File
@@ -42,6 +42,7 @@
#include "graphics/shader/rectListShader.h"
#include "graphics/shader/shader.h"
#include "kernel/memory.h"
#include "libs/agc.h"
#include "spirv-tools/libspirv.hpp"
#if __has_include("graphics/host_gpu/renderer/renderTargetBarriers.h")
@@ -58,6 +59,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
#include <chrono>
#include <cinttypes>
@@ -163,6 +165,10 @@ struct TileManagerTestAccess {
};
struct TextureCacheTestAccess {
static_assert(TextureCache::ImagePageTable::kPageBits == 20);
static_assert(TextureCache::ImagePageTable::kAddressSpaceBits == 40);
static_assert(TextureCache::ImagePageTable::kFirstLevelBits == 10);
static void ConfigureGarbageCollection(TextureCache& cache, std::span<const ImageId> oldest,
uint64_t tick, uint64_t pressure) {
cache.m_trigger_gc_memory = 0;
@@ -197,6 +203,75 @@ struct TextureCacheTestAccess {
return owner != nullptr && owner->registered;
}
static std::vector<ImageId> FindImages(TextureCache& cache, uint64_t address, uint64_t size,
bool page_overlap) {
std::lock_guard lock(cache.m_lock);
const auto found = cache.FindImagesInRegion(address, size, page_overlap);
std::vector<ImageId> result;
result.reserve(found.size());
for (const auto id: found) {
result.push_back(id);
}
return result;
}
static size_t PageOwnerCount(TextureCache& cache, uint64_t address) {
std::lock_guard lock(cache.m_lock);
const auto* owners = cache.m_image_page_table.Find(
static_cast<size_t>(address >> TextureCache::ImagePageTable::kPageBits));
return owners == nullptr ? 0 : owners->size();
}
static size_t OwnedPageCount(TextureCache& cache, uint64_t address, uint64_t size, ImageId id) {
std::lock_guard lock(cache.m_lock);
TextureCache::ImagePageTable::PageRange pages {};
if (!TextureCache::ImagePageTable::TryGetPageRange(address, size, pages)) {
return 0;
}
size_t count = 0;
for (size_t page = pages.first; page < pages.last_exclusive; ++page) {
const auto* owners = cache.m_image_page_table.Find(page);
count += owners != nullptr && owners->Contains(id) ? 1 : 0;
}
return count;
}
static void AddPageOwner(TextureCache& cache, uint64_t address, ImageId id) {
std::lock_guard lock(cache.m_lock);
cache.m_image_page_table[static_cast<size_t>(
address >> TextureCache::ImagePageTable::kPageBits)]
.push_back(id);
}
static bool RemovePageOwner(TextureCache& cache, uint64_t address, ImageId id) {
std::lock_guard lock(cache.m_lock);
auto* owners = cache.m_image_page_table.Find(
static_cast<size_t>(address >> TextureCache::ImagePageTable::kPageBits));
return owners != nullptr && owners->Erase(id);
}
static void SetQueryEpoch(TextureCache& cache, uint32_t epoch) {
std::lock_guard lock(cache.m_lock);
cache.m_image_query_epoch = epoch;
}
static uint32_t QueryEpoch(TextureCache& cache) {
std::lock_guard lock(cache.m_lock);
return cache.m_image_query_epoch;
}
static ImageId InsertImage(TextureCache& cache, const ImageInfo& info) {
std::lock_guard transaction(cache.m_resource_mutex);
std::lock_guard lock(cache.m_lock);
return cache.InsertImage(info);
}
static void DeleteImage(TextureCache& cache, ImageId id) {
std::lock_guard transaction(cache.m_resource_mutex);
std::lock_guard lock(cache.m_lock);
cache.DeleteImage(id);
}
static std::shared_ptr<Image> Owner(const TextureCache& cache, ImageId id) {
return cache.ResolveOwner(id);
}
@@ -1299,12 +1374,14 @@ public:
void CheckGpuMappedRangeLifecycle() {
EnsureRuntimeContext();
CommandScheduler scheduler(Renderer(), m_runtime_context);
auto& context = Renderer();
CommandScheduler scheduler(context, m_runtime_context);
HW::Context registers {};
HW::UserConfig user_config {};
HW::Shader shaders {};
scheduler.Begin(registers, user_config, shaders);
Gpu gpu(Renderer());
context.InitializeGpu(nullptr);
auto& gpu = context.GetGpu();
GpuResourceManager resources(m_runtime_context, scheduler);
resources.SetGpu(&gpu);
@@ -1343,6 +1420,7 @@ public:
resources.SetGpu(nullptr);
scheduler.Finish();
context.ShutdownGpu();
std::printf("[host] %-32s ok\n", "GpuMappedRangeLifecycle");
}
@@ -1494,21 +1572,73 @@ public:
gpu_thread = std::this_thread::get_id();
Require("GpuCommandLane", "FIFO", order == 1,
"synchronous command overtook an older host command");
Require("GpuCommandLane", "generic context",
!Gpu::IsCommandProcessorThread() && Gpu::CurrentCommandProcessor() == nullptr,
"generic host command manufactured a PM4 processor context");
Require("GpuCommandLane", "GPU context", Gpu::IsGpuThread(),
"host command did not run on the GPU thread");
gpu.SendCommandSync([&nested_thread] { nested_thread = std::this_thread::get_id(); });
order = 2;
});
gpu.SendCommandSyncWithProcessor([&](CommandProcessor& processor) {
Require("GpuCommandLane", "processor context",
Gpu::IsCommandProcessorThread() && Gpu::CurrentCommandProcessor() == &processor,
"resource command did not receive its explicit processor context");
});
Require("GpuCommandLane", "dispatch thread",
order == 2 && gpu_thread != caller_thread && nested_thread == gpu_thread,
"host command did not run on the GPU thread or nested sync deadlocked");
alignas(uint32_t) uint32_t packet_marker_a = 0;
alignas(uint32_t) uint32_t packet_marker_b = 0;
const auto write_packet = [](uint32_t* packet, uint32_t* destination, uint32_t value) {
const auto destination_address = reinterpret_cast<uint64_t>(destination);
packet[0] = KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0);
packet[1] = 0;
packet[2] = static_cast<uint32_t>(destination_address);
packet[3] = static_cast<uint32_t>(destination_address >> 32u);
packet[4] = value;
};
std::array<uint32_t, 1024> polling_loop {};
for (size_t i = 0; i < polling_loop.size() - 4; i += 2) {
polling_loop[i] = KYTY_PM4(2, Pm4::IT_NOP, Pm4::R_ZERO);
polling_loop[i + 1] = 0;
}
const auto loop_address = reinterpret_cast<uint64_t>(polling_loop.data());
polling_loop[1020] = KYTY_PM4(4, Pm4::IT_INDIRECT_BUFFER, 0);
polling_loop[1021] = static_cast<uint32_t>(loop_address);
polling_loop[1022] = static_cast<uint32_t>(loop_address >> 32u);
polling_loop[1023] = 0x0f200000u | static_cast<uint32_t>(polling_loop.size());
std::array<uint32_t, 14> polling_commands {};
write_packet(polling_commands.data(), &packet_marker_a, 11);
polling_commands[5] = KYTY_PM4(4, Pm4::IT_INDIRECT_BUFFER, 0);
polling_commands[6] = static_cast<uint32_t>(loop_address);
polling_commands[7] = static_cast<uint32_t>(loop_address >> 32u);
polling_commands[8] = 0x0f200000u | static_cast<uint32_t>(polling_loop.size());
write_packet(polling_commands.data() + 9, &packet_marker_b, 22);
std::binary_semaphore stream_gate_entered {0};
std::binary_semaphore stream_gate_release {0};
gpu.SendCommand([&] {
stream_gate_entered.release();
stream_gate_release.acquire();
});
gpu.Submit(polling_commands.data(), polling_commands.size(), nullptr, 0);
stream_gate_entered.acquire();
stream_gate_release.release();
uint32_t packet_marker_a_at_callback = UINT32_MAX;
uint32_t packet_marker_b_at_callback = UINT32_MAX;
for (uint32_t attempt = 0; attempt < 8 && packet_marker_a_at_callback != 11; attempt++) {
gpu.SendCommandSync([&] {
if (packet_marker_a == 11 && packet_marker_b == 0) {
packet_marker_a_at_callback = packet_marker_a;
packet_marker_b_at_callback = packet_marker_b;
polling_loop[1020] = KYTY_PM4(2, Pm4::IT_NOP, Pm4::R_ZERO);
polling_loop[1021] = 0;
polling_loop[1022] = KYTY_PM4(2, Pm4::IT_NOP, Pm4::R_ZERO);
polling_loop[1023] = 0;
}
});
std::this_thread::yield();
}
gpu.Done();
Require("GpuCommandLane", "packet-boundary command polling",
packet_marker_a_at_callback == 11 && packet_marker_b_at_callback == 0 &&
packet_marker_a == 11 && packet_marker_b == 22,
"host command waited for an entire non-suspended PM4 stream");
uint32_t label = 0;
uint32_t prefix = 0;
uint32_t suffix = 0;
@@ -1687,7 +1817,7 @@ public:
gpu.Submit(dma_commands.data(), static_cast<uint32_t>(dma_commands.size()), nullptr, 0);
gpu.Done();
constexpr uint32_t clean_fill_value = 0xdecafbad;
gpu.SendCommandSyncWithProcessor([&](CommandProcessor&) {
gpu.SendCommandSync([&] {
auto& buffer_cache = resources.GetBufferCache();
Require("GpuCommandLane", "clean cached setup",
buffer_cache.HasPageOverlap(clean_cached_fill, sizeof(source_words)) &&
@@ -1767,14 +1897,116 @@ public:
clean_copy_words[1]},
"host-memory fill/copy did not update the clean cached mirror");
gpu.SendCommandSyncWithProcessor([&](CommandProcessor& processor) {
Require("GpuCommandLane", "processor fault context",
Gpu::CurrentCommandProcessor() == &processor,
"processor resource test lost its command context");
Require("GpuCommandLane", "processor memory invalidation",
gpu.SendCommandSync([&] {
Require("GpuCommandLane", "GPU memory invalidation",
resources.InvalidateMemory(fault_base, sizeof(uint32_t)),
"processor memory invalidation did not find its mapped range");
"GPU memory invalidation did not find its mapped range");
});
auto& buffer_cache = resources.GetBufferCache();
Require("GpuCommandLane", "clean tracked fault setup",
buffer_cache.HasPageOverlap(immediate_dst, sizeof(uint32_t)) &&
!buffer_cache.HasGpuDirtyBytes(immediate_dst, sizeof(uint32_t)),
"clean write-fault test address is not backed by a clean cached Buffer");
constexpr uint64_t dirty_fault_address = fault_base + 0xd000;
constexpr uint32_t dirty_fault_value = 0x5a17c0deu;
constexpr uint32_t dirty_fault_stale = 0x0ddba11u;
std::memcpy(reinterpret_cast<void*>(dirty_fault_address), &dirty_fault_stale,
sizeof(dirty_fault_stale));
gpu.SendCommandSync([&] {
auto dirty = buffer_cache.ObtainBuffer(scheduler.Current(), dirty_fault_address,
sizeof(dirty_fault_value), true, false);
Require("GpuCommandLane", "dirty tracked fault setup", dirty.owner != nullptr,
"dirty write-fault test could not create a cached Buffer");
scheduler.Current().RetainResourceUntilFence(dirty.owner);
buffer_cache.FillBuffer(dirty_fault_address, sizeof(dirty_fault_value),
dirty_fault_value);
});
Require("GpuCommandLane", "dirty tracked fault ownership",
buffer_cache.HasGpuDirtyBytes(dirty_fault_address, sizeof(dirty_fault_value)),
"dirty write-fault test address did not acquire GPU ownership");
std::binary_semaphore block_entered {0};
std::binary_semaphore block_release {0};
std::binary_semaphore clean_fault_complete {0};
bool clean_fault_handled = false;
gpu.SendCommand([&] {
block_entered.release();
block_release.acquire();
});
block_entered.acquire();
std::jthread clean_fault_thread([&] {
clean_fault_handled = resources.HandleFault(PageFaultAccess::Write, immediate_dst);
clean_fault_complete.release();
});
const bool clean_fault_was_direct =
clean_fault_complete.try_acquire_for(std::chrono::seconds(1));
if (!clean_fault_was_direct) {
block_release.release();
clean_fault_complete.acquire();
}
clean_fault_thread.join();
Require("GpuCommandLane", "clean fault direct path",
clean_fault_was_direct && clean_fault_handled,
"a clean CPU write fault waited for the blocked GPU command lane");
std::binary_semaphore dirty_fault_complete {0};
bool dirty_fault_handled = false;
std::jthread dirty_fault_thread([&] {
dirty_fault_handled =
resources.HandleFault(PageFaultAccess::Write, dirty_fault_address);
dirty_fault_complete.release();
});
const bool dirty_fault_bypassed_lane =
dirty_fault_complete.try_acquire_for(std::chrono::milliseconds(200));
block_release.release();
if (!dirty_fault_bypassed_lane) {
dirty_fault_complete.acquire();
}
dirty_fault_thread.join();
uint32_t dirty_fault_backing = 0;
std::memcpy(&dirty_fault_backing, reinterpret_cast<const void*>(dirty_fault_address),
sizeof(dirty_fault_backing));
Require("GpuCommandLane", "dirty fault synchronization",
!dirty_fault_bypassed_lane && dirty_fault_handled &&
dirty_fault_backing == dirty_fault_value &&
!buffer_cache.HasGpuDirtyBytes(dirty_fault_address, sizeof(dirty_fault_value)),
"a GPU-dirty write fault bypassed synchronization or lost the published bytes");
constexpr uint64_t empty_copy_fault_address = fault_base + 0xe000;
constexpr uint32_t empty_copy_fault_value = 0x6b28d1efu;
gpu.SendCommandSync([&] {
auto dirty = buffer_cache.ObtainBuffer(scheduler.Current(), empty_copy_fault_address,
sizeof(empty_copy_fault_value), true, false);
Require("GpuCommandLane", "empty-copy write setup", dirty.owner != nullptr,
"empty-copy write test could not create a cached Buffer");
scheduler.Current().RetainResourceUntilFence(dirty.owner);
buffer_cache.FillBuffer(empty_copy_fault_address, sizeof(empty_copy_fault_value),
empty_copy_fault_value);
buffer_cache.ReadMemory(empty_copy_fault_address, sizeof(empty_copy_fault_value));
const bool gpu_owned = buffer_cache.IsRegionGpuModified(
empty_copy_fault_address, sizeof(empty_copy_fault_value));
const bool cpu_owned = buffer_cache.IsRegionCpuModified(
empty_copy_fault_address, sizeof(empty_copy_fault_value));
Require("GpuCommandLane", "empty-copy read ownership",
!gpu_owned && !cpu_owned,
"readback did not leave clean ownership for the queued write invalidation");
buffer_cache.ReadMemory(empty_copy_fault_address, sizeof(empty_copy_fault_value), true);
});
uint32_t empty_copy_fault_backing = 0;
std::memcpy(&empty_copy_fault_backing, reinterpret_cast<const void*>(empty_copy_fault_address),
sizeof(empty_copy_fault_backing));
const bool empty_copy_cpu_owned = buffer_cache.IsRegionCpuModified(
empty_copy_fault_address, sizeof(empty_copy_fault_value));
const bool empty_copy_gpu_owned = buffer_cache.IsRegionGpuModified(
empty_copy_fault_address, sizeof(empty_copy_fault_value));
const bool empty_copy_dirty =
buffer_cache.HasGpuDirtyBytes(empty_copy_fault_address, sizeof(empty_copy_fault_value));
Require("GpuCommandLane", "empty-copy write ownership",
empty_copy_fault_backing == empty_copy_fault_value &&
empty_copy_cpu_owned && !empty_copy_gpu_owned && !empty_copy_dirty,
"write invalidation without a remaining copy did not establish CPU ownership");
resources.UnmapMemory(fault_base, fault_size);
Require("GpuCommandLane", "processor fault unmap",
Libs::LibKernel::Memory::KernelMunmap(fault_base, fault_size) == 0,
@@ -2000,12 +2232,14 @@ public:
constexpr uint32_t ring_fault_second_value = 0x4e5f6071u;
EnsureRuntimeContext();
CommandScheduler scheduler(Renderer(), m_runtime_context);
auto& context = Renderer();
CommandScheduler scheduler(context, m_runtime_context);
HW::Context registers {};
HW::UserConfig user_config {};
HW::Shader shaders {};
scheduler.Begin(registers, user_config, shaders);
Gpu gpu(Renderer());
context.InitializeGpu(nullptr);
auto& gpu = context.GetGpu();
int64_t direct_offset = -1;
Require(name, "direct allocation",
@@ -2398,7 +2632,7 @@ public:
resources.UnmapMemory(base, allocation_size);
scheduler.Finish();
}
gpu.Shutdown();
context.ShutdownGpu();
Require(name, "unmap direct backing",
Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0,
"dirty-GC direct-memory mapping release failed");
@@ -2415,12 +2649,14 @@ public:
constexpr uint64_t allocation_size = 0x2800000;
constexpr uint64_t allocation_alignment = 0x200000;
EnsureRuntimeContext();
CommandScheduler scheduler(Renderer(), m_runtime_context);
auto& context = Renderer();
CommandScheduler scheduler(context, m_runtime_context);
HW::Context registers {};
HW::UserConfig user_config {};
HW::Shader shaders {};
scheduler.Begin(registers, user_config, shaders);
Gpu gpu(Renderer());
context.InitializeGpu(nullptr);
auto& gpu = context.GetGpu();
int64_t direct_offset = -1;
Require(name, "direct allocation",
Libs::LibKernel::Memory::KernelAllocateDirectMemory(
@@ -2488,6 +2724,110 @@ public:
vk::Format::eR8G8B8A8Srgb,
"registered compatible backing did not reuse one ImageId");
const auto IsOnlyImage = [](const std::vector<ImageId>& ids, ImageId expected) {
return ids.size() == 1 && ids.front() == expected;
};
const auto exact_byte_miss =
TextureCacheTestAccess::FindImages(texture_cache, base + 8, 1, false);
const auto touched_page_hit =
TextureCacheTestAccess::FindImages(texture_cache, base + 8, 1, true);
const auto next_page_miss =
TextureCacheTestAccess::FindImages(texture_cache, base + 0x1000, 1, true);
Require(name, "exact and 4-KiB page filtering",
exact_byte_miss.empty() && IsOnlyImage(touched_page_hit, first) &&
next_page_miss.empty(),
"coarse candidates did not preserve exact-byte and touched-page semantics");
const auto MakeOwnershipInfo = [](uint64_t address, uint64_t size) {
ImageInfo info {};
info.data = {address, size};
info.extent = {1, 1, 1};
info.resources = {1, 1};
info.samples = 1;
return info;
};
const auto spanning_info = MakeOwnershipInfo(base + 0x1ff000, 0x2000);
const auto spanning = TextureCacheTestAccess::InsertImage(texture_cache, spanning_info);
const auto spanning_results = TextureCacheTestAccess::FindImages(
texture_cache, spanning_info.data.address, spanning_info.data.size, false);
Require(name, "one-MiB cross-page deduplication",
spanning && IsOnlyImage(spanning_results, spanning) &&
TextureCacheTestAccess::PageOwnerCount(texture_cache,
spanning_info.data.address) == 1 &&
TextureCacheTestAccess::PageOwnerCount(
texture_cache, spanning_info.data.End() - 1) == 1,
"an image spanning two coarse pages was missing or returned more than once");
TextureCacheTestAccess::DeleteImage(texture_cache, spanning);
Require(name, "cross-page owner cleanup",
TextureCacheTestAccess::PageOwnerCount(texture_cache,
spanning_info.data.address) == 0 &&
TextureCacheTestAccess::PageOwnerCount(
texture_cache, spanning_info.data.End() - 1) == 0 &&
TextureCacheTestAccess::FindImages(texture_cache,
spanning_info.data.address,
spanning_info.data.size, false)
.empty(),
"cross-page unregister left a stale coarse-page membership");
const auto shared_info = MakeOwnershipInfo(base + 0x500100, 0x100);
const auto shared_first =
TextureCacheTestAccess::InsertImage(texture_cache, shared_info);
const auto shared_second =
TextureCacheTestAccess::InsertImage(texture_cache, shared_info);
const auto shared_results = TextureCacheTestAccess::FindImages(
texture_cache, shared_info.data.address, shared_info.data.size, false);
Require(name, "shared coarse-page registration",
shared_results.size() == 2 &&
std::find(shared_results.begin(), shared_results.end(), shared_first) !=
shared_results.end() &&
std::find(shared_results.begin(), shared_results.end(), shared_second) !=
shared_results.end(),
"two registered owners were not retained in one coarse page");
TextureCacheTestAccess::DeleteImage(texture_cache, shared_first);
const auto shared_survivor = TextureCacheTestAccess::FindImages(
texture_cache, shared_info.data.address, shared_info.data.size, false);
Require(name, "shared coarse-page unregister",
IsOnlyImage(shared_survivor, shared_second) &&
TextureCacheTestAccess::PageOwnerCount(texture_cache,
shared_info.data.address) == 1,
"unregistering one owner removed its coarse-page neighbor");
TextureCacheTestAccess::DeleteImage(texture_cache, shared_second);
Require(name, "final shared coarse-page unregister",
TextureCacheTestAccess::PageOwnerCount(texture_cache,
shared_info.data.address) == 0,
"the final shared owner remained registered");
constexpr uint64_t large_owner_size = 64ull * 1024 * 1024;
const auto large_info = MakeOwnershipInfo(base + 0x800000, large_owner_size);
const auto large_owner =
TextureCacheTestAccess::InsertImage(texture_cache, large_info);
Require(name, "production one-MiB registration granularity",
TextureCacheTestAccess::OwnedPageCount(
texture_cache, large_info.data.address, large_info.data.size, large_owner) == 64,
"64 MiB image registration did not create exactly 64 coarse memberships");
TextureCacheTestAccess::DeleteImage(texture_cache, large_owner);
Require(name, "production one-MiB unregister granularity",
TextureCacheTestAccess::OwnedPageCount(
texture_cache, large_info.data.address, large_info.data.size, large_owner) == 0,
"large image unregister left coarse memberships behind");
const ImageId stale {first.index, first.generation + 1};
TextureCacheTestAccess::AddPageOwner(texture_cache, base, stale);
const auto stale_filtered =
TextureCacheTestAccess::FindImages(texture_cache, base, sizeof(initial), false);
Require(name, "stale page owner filtering",
IsOnlyImage(stale_filtered, first) &&
TextureCacheTestAccess::RemovePageOwner(texture_cache, base, stale),
"a stale generation escaped the direct page-owner lookup");
TextureCacheTestAccess::SetQueryEpoch(texture_cache, UINT32_MAX);
const auto wrap_results =
TextureCacheTestAccess::FindImages(texture_cache, base, sizeof(initial), false);
Require(name, "page-owner query epoch wrap",
IsOnlyImage(wrap_results, first) &&
TextureCacheTestAccess::QueryEpoch(texture_cache) == 1,
"query deduplication failed while wrapping its epoch");
auto& image = texture_cache.GetImage(first);
constexpr uint32_t final_sampled_value = 0x88776655u;
Require(name, "sampled write between discovery and acquisition",
@@ -4772,7 +5112,7 @@ public:
resources.UnmapMemory(base, allocation_size);
scheduler.Finish();
}
gpu.Shutdown();
context.ShutdownGpu();
Require(name, "unmap direct backing",
Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0,
"cache direct-memory mapping release failed");
@@ -17314,6 +17654,136 @@ void CheckPm4StencilInfoValueLane(RenderContext& renderer) {
std::printf("[host] %-32s ok\n", "Pm4StencilInfoValueLane");
}
void CheckPm4ContextStateOperations(RenderContext& renderer) {
GraphicsInitJmpTables();
CommandProcessor processor(renderer);
struct AgcCommandBufferLayout {
using Callback = KYTY_SYSV_ABI bool (*)(Gen5::CommandBuffer*, uint32_t, void*);
uint32_t* bottom;
uint32_t* top;
uint32_t* cursor_up;
uint32_t* cursor_down;
Callback callback;
void* user_data;
uint32_t reserved_dw;
};
static_assert(offsetof(AgcCommandBufferLayout, cursor_up) == 0x10);
static_assert(offsetof(AgcCommandBufferLayout, reserved_dw) == 0x30);
HW::RenderControl original_control {};
original_control.depth_clear_enable = true;
original_control.stencil_clear_enable = true;
processor.GetCtx().SetRenderControl(original_control);
processor.GetCtx().SetRenderTargetMask(0x1234abcd);
processor.GetUcfg().SetPrimitiveType(0x35);
g_hw_sh_indirect_func[Pm4::SPI_SHADER_PGM_LO_PS](processor, Pm4::SPI_SHADER_PGM_LO_PS,
0x00123456);
const auto shader_address = processor.GetShCtx().GetPs().ps_regs.data_addr;
const auto invoke = [&](ContextStateOperation operation) {
std::array<uint32_t, 32> packet {};
std::array<uint32_t, 6> segment_sizes {};
AgcCommandBufferLayout dcb {packet.data(), packet.data() + packet.size(), packet.data(),
packet.data() + packet.size(), nullptr, nullptr, 0};
const auto operation_value = static_cast<uint32_t>(operation);
size_t segment_count = 0;
switch (operation) {
case ContextStateOperation::Clear: segment_sizes = {5}; segment_count = 1; break;
case ContextStateOperation::Push:
segment_sizes = {5, 8, 9, 3, 2};
segment_count = 5;
break;
case ContextStateOperation::Pop:
segment_sizes = {3, 5, 8, 9, 2};
segment_count = 5;
break;
case ContextStateOperation::PushClear:
segment_sizes = {5, 8, 9, 3, 2, 5};
segment_count = 6;
break;
}
const auto size_dw =
static_cast<size_t>(Gen5::GraphicsDcbContextStateOpGetSize(operation_value) / 4);
auto* emitted = Gen5::GraphicsDcbContextStateOp(
reinterpret_cast<Gen5::CommandBuffer*>(&dcb), operation_value);
bool packet_matches = true;
size_t segment_offset = 0;
for (size_t i = 0; i < segment_count; i++) {
const auto segment_size = segment_sizes[i];
const auto selector = (i == 0 ? Pm4::R_CONTEXT_STATE : Pm4::R_ZERO);
packet_matches &=
packet[segment_offset] == KYTY_PM4(segment_size, Pm4::IT_NOP, selector);
for (size_t j = 1; j < segment_size; j++) {
packet_matches &= packet[segment_offset + j] ==
(i == 0 && j == 1 ? operation_value : 0u);
}
segment_offset += segment_size;
}
Require("Pm4ContextState", "HLE packet",
emitted == packet.data() && dcb.cursor_up == packet.data() + size_dw &&
segment_offset == size_dw && packet_matches,
"context-state HLE packet does not match its real allocation size");
Pm4Execution execution;
return processor.Process(execution, packet.data(), size_dw);
};
Require("Pm4ContextState", "push-clear",
invoke(ContextStateOperation::PushClear) == Pm4ProcessResult::Complete &&
!processor.GetCtx().GetRenderControl().depth_clear_enable &&
!processor.GetCtx().GetRenderControl().stencil_clear_enable &&
processor.GetCtx().GetRenderTargetMask() == HW::Context {}.GetRenderTargetMask() &&
processor.GetUcfg().GetPrimType() == 0x35 &&
processor.GetShCtx().GetPs().ps_regs.data_addr == shader_address,
"push-clear did not save and clear only Cx state");
processor.GetCtx().SetRenderTargetMask(0x55aa55aa);
Require("Pm4ContextState", "pop",
invoke(ContextStateOperation::Pop) == Pm4ProcessResult::Complete &&
processor.GetCtx().GetRenderControl().depth_clear_enable &&
processor.GetCtx().GetRenderControl().stencil_clear_enable &&
processor.GetCtx().GetRenderTargetMask() == 0x1234abcd,
"pop did not restore the complete saved Cx state");
Require("Pm4ContextState", "push",
invoke(ContextStateOperation::Push) == Pm4ProcessResult::Complete &&
processor.GetCtx().GetRenderTargetMask() == 0x1234abcd,
"push changed live Cx state");
processor.GetCtx().SetRenderTargetMask(0x01020304);
Require("Pm4ContextState", "push-pop restore",
invoke(ContextStateOperation::Pop) == Pm4ProcessResult::Complete &&
processor.GetCtx().GetRenderTargetMask() == 0x1234abcd,
"ordinary push/pop did not restore Cx state");
Require("Pm4ContextState", "clear",
invoke(ContextStateOperation::Clear) == Pm4ProcessResult::Complete &&
!processor.GetCtx().GetRenderControl().depth_clear_enable &&
processor.GetCtx().GetRenderTargetMask() == HW::Context {}.GetRenderTargetMask() &&
processor.GetUcfg().GetPrimType() == 0x35 &&
processor.GetShCtx().GetPs().ps_regs.data_addr == shader_address,
"clear reset state outside the Cx register domain");
processor.GetCtx().SetRenderTargetMask(0xabcdef01);
Require("Pm4ContextState", "push before processor reset",
invoke(ContextStateOperation::Push) == Pm4ProcessResult::Complete,
"push before reset failed");
processor.Reset();
processor.GetCtx().SetRenderTargetMask(0x76543210);
Require("Pm4ContextState", "processor reset discards push",
invoke(ContextStateOperation::Push) == Pm4ProcessResult::Complete &&
invoke(ContextStateOperation::Pop) == Pm4ProcessResult::Complete &&
processor.GetCtx().GetRenderTargetMask() == 0x76543210,
"processor reset retained an invalid saved Cx state");
Require("Pm4ContextState", "HLE packet size",
Gen5::GraphicsDcbContextStateOpGetSize(0) == 20 &&
Gen5::GraphicsDcbContextStateOpGetSize(1) == 108 &&
Gen5::GraphicsDcbContextStateOpGetSize(2) == 108 &&
Gen5::GraphicsDcbContextStateOpGetSize(3) == 128 &&
Gen5::GraphicsDcbContextStateOpGetSize(4) == 0,
"context-state HLE sizes do not match libSceAgc");
std::printf("[host] %-32s ok\n", "Pm4ContextState");
}
void CheckPm4WaitResume(RenderContext& renderer) {
GraphicsInitJmpTables();
CommandProcessor processor(renderer);
@@ -17450,6 +17920,11 @@ int main(int argc, char** argv) {
vulkan.CheckGpuCommandLane();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--context-state-only") == 0) {
VulkanHarness vulkan;
CheckPm4ContextStateOperations(vulkan.RuntimeRenderer());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--image-overlap-only") == 0) {
CheckDepthAttachmentWrites();
CheckDynamicRenderingState();
@@ -17584,6 +18059,7 @@ int main(int argc, char** argv) {
CheckVulkan13FeatureRequirements();
CheckPm4AcquireMemNoOp(vulkan.RuntimeRenderer());
CheckPm4StencilInfoValueLane(vulkan.RuntimeRenderer());
CheckPm4ContextStateOperations(vulkan.RuntimeRenderer());
CheckPm4WaitResume(vulkan.RuntimeRenderer());
CheckPm4CeCompletion(vulkan.RuntimeRenderer());
CheckEmbeddedFetchVertexOffset();
+269
View File
@@ -0,0 +1,269 @@
#include "kernel/syncOnAddress.h"
#include "libs/errno.h"
#include <atomic>
#include <chrono>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <thread>
#include <vector>
namespace {
using Libs::LibKernel::SyncOnAddress::Wait32;
using Libs::LibKernel::SyncOnAddress::Wait64;
using Libs::LibKernel::SyncOnAddress::Wake;
std::atomic<int> g_signal_poll_count{0};
void Check(bool value, const char *text) {
if (!value) {
std::fprintf(stderr, "SyncOnAddressTests: failed: %s\n", text);
std::abort();
}
}
template <typename T> void Store(T *address, T value) {
std::atomic_ref<T>(*address).store(value, std::memory_order_release);
}
void CountSignalPoll() {
g_signal_poll_count.fetch_add(1, std::memory_order_relaxed);
}
void TestInvalidAddress() {
uint32_t timeout = 1;
Check(Wait32(nullptr, 0, &timeout) == Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wait32 rejects a null address");
Check(Wait64(nullptr, 0, &timeout) == Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wait64 rejects a null address");
Check(Wake(nullptr, 1) == Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wake rejects a null address");
alignas(uint64_t) uint8_t bytes[16] = {};
auto *misaligned = reinterpret_cast<uint32_t *>(bytes + 1);
Check(Wait32(misaligned, 0, &timeout) == Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wait32 rejects a misaligned address");
Check(Wait64(reinterpret_cast<uint64_t *>(bytes + 4), 0, &timeout) ==
Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wait64 rejects a misaligned address");
Check(Wake(misaligned, 1) == Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wake rejects a misaligned address");
uint64_t aligned = 0;
Check(Wake(&aligned, -1) == Libs::LibKernel::KERNEL_ERROR_EINVAL,
"wake rejects a negative count");
}
void TestMismatchReturnsImmediately() {
uint32_t word = 7;
uint64_t word64 = UINT64_C(0x100000000);
uint32_t timeout = 500000;
const auto start = std::chrono::steady_clock::now();
Check(Wait32(&word, 6, &timeout, CountSignalPoll) == OK,
"mismatch succeeds without parking");
Check(Wait64(&word64, 0, &timeout, CountSignalPoll) == OK,
"wait64 compares all 64 bits");
Check(std::chrono::steady_clock::now() - start <
std::chrono::milliseconds(100),
"mismatched value is a fast path");
Check(g_signal_poll_count.load(std::memory_order_relaxed) >= 2,
"mismatch remains a guest signal safe-point");
}
void TestTimeout() {
uint32_t word = 0;
uint32_t timeout = 20000;
const auto start = std::chrono::steady_clock::now();
Check(Wait32(&word, 0, &timeout) == Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT,
"matching value times out");
const auto elapsed = std::chrono::steady_clock::now() - start;
Check(elapsed >= std::chrono::milliseconds(10),
"timeout does not return too early");
Check(elapsed < std::chrono::milliseconds(500), "timeout remains bounded");
timeout = 0;
Check(Wait32(&word, 0, &timeout) == Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT,
"zero timeout polls a matching value");
word = 1;
Check(Wait32(&word, 0, &timeout) == OK,
"zero timeout succeeds for a mismatched value");
}
void TestValueChangeAndWake() {
uint64_t word = 0;
uint32_t timeout = 1000000;
std::atomic<bool> ready{false};
int result = Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
std::thread waiter([&] {
ready.store(true, std::memory_order_release);
result = Wait64(&word, 0, &timeout);
});
while (!ready.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
Store(&word, UINT64_C(0x100000000));
Check(Wake(&word, 1) == OK, "wake-one succeeds");
waiter.join();
Check(result == OK, "a value change plus wake releases the waiter");
}
void TestWakeOneThenAll() {
constexpr int WAITER_COUNT = 4;
uint32_t word = 0;
uint32_t timeout = 1000000;
std::atomic<int> ready{0};
std::atomic<int> returned{0};
int results[WAITER_COUNT] = {};
std::vector<std::thread> waiters;
for (int i = 0; i < WAITER_COUNT; i++) {
waiters.emplace_back([&, i] {
ready.fetch_add(1, std::memory_order_release);
results[i] = Wait32(&word, 0, &timeout);
returned.fetch_add(1, std::memory_order_release);
});
}
while (ready.load(std::memory_order_acquire) != WAITER_COUNT) {
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Check(Wake(&word, 1) == OK, "wake-one succeeds with multiple waiters");
const auto one_deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
while (returned.load(std::memory_order_acquire) == 0 &&
std::chrono::steady_clock::now() < one_deadline) {
std::this_thread::yield();
}
Check(returned.load(std::memory_order_acquire) == 1,
"wake-one releases exactly one waiter");
Check(Wake(&word, 2) == OK, "wake-two succeeds");
const auto two_deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
while (returned.load(std::memory_order_acquire) < 3 &&
std::chrono::steady_clock::now() < two_deadline) {
std::this_thread::yield();
}
Check(returned.load(std::memory_order_acquire) == 3,
"wake-two releases exactly two more waiters");
Check(Wake(&word, INT_MAX) == OK, "wake-all succeeds");
for (auto &waiter : waiters) {
waiter.join();
}
Check(returned.load(std::memory_order_acquire) == WAITER_COUNT,
"wake-all releases the remaining waiters");
for (int result : results) {
Check(result == OK, "explicitly woken waiters return success");
}
}
void TestAddressesAreIsolated() {
uint32_t first = 0;
uint32_t second = 0;
uint32_t first_timeout = 1000000;
uint32_t second_timeout = 1000000;
std::atomic<int> ready{0};
std::atomic<bool> first_returned{false};
std::atomic<bool> second_returned{false};
int first_result = 0;
int second_result = 0;
std::thread first_waiter([&] {
ready.fetch_add(1, std::memory_order_release);
first_result = Wait32(&first, 0, &first_timeout);
first_returned.store(true, std::memory_order_release);
});
std::thread second_waiter([&] {
ready.fetch_add(1, std::memory_order_release);
second_result = Wait32(&second, 0, &second_timeout);
second_returned.store(true, std::memory_order_release);
});
while (ready.load(std::memory_order_acquire) != 2) {
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Check(Wake(&first, 1) == OK, "first address wakes");
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Check(first_returned.load(std::memory_order_acquire),
"first address waiter returned");
Check(!second_returned.load(std::memory_order_acquire),
"waking one address does not release another address");
Check(Wake(&second, 1) == OK, "second address wakes");
first_waiter.join();
second_waiter.join();
Check(first_result == OK && second_result == OK,
"isolated waiters return success");
}
void TestCompareRegisterWakeRace() {
for (int i = 0; i < 100; i++) {
uint32_t word = 0;
uint32_t timeout = 500000;
std::atomic<bool> ready{false};
int result = Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
std::thread waiter([&] {
ready.store(true, std::memory_order_release);
result = Wait32(&word, 0, &timeout);
});
while (!ready.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
Store(&word, uint32_t{1});
(void)Wake(&word, 1);
waiter.join();
Check(result == OK, "compare/register/wake race never loses progress");
}
}
void TestWakeZeroIsNoOp() {
constexpr int WAITER_COUNT = 2;
uint32_t word = 0;
uint32_t timeout = 1000000;
std::atomic<int> ready{0};
std::atomic<int> returned{0};
int results[WAITER_COUNT] = {};
std::vector<std::thread> waiters;
for (int i = 0; i < WAITER_COUNT; i++) {
waiters.emplace_back([&, i] {
ready.fetch_add(1, std::memory_order_release);
results[i] = Wait32(&word, 0, &timeout);
returned.fetch_add(1, std::memory_order_release);
});
}
while (ready.load(std::memory_order_acquire) != WAITER_COUNT) {
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Check(Wake(&word, 0) == OK, "zero-count wake succeeds");
std::this_thread::sleep_for(std::chrono::milliseconds(20));
Check(returned.load(std::memory_order_acquire) == 0,
"zero-count wake releases no waiters");
Check(Wake(&word, INT_MAX) == OK, "wake-all succeeds after zero-count wake");
for (auto &waiter : waiters) {
waiter.join();
}
for (int result : results) {
Check(result == OK, "wake-all releases waiters after zero-count no-op");
}
}
} // namespace
int main() {
TestInvalidAddress();
TestMismatchReturnsImmediately();
TestTimeout();
TestValueChangeAndWake();
TestWakeOneThenAll();
TestAddressesAreIsolated();
TestCompareRegisterWakeRace();
TestWakeZeroIsNoOp();
std::printf("SyncOnAddressTests: all passed\n");
return 0;
}