mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-20 19:07:01 +02:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9aa0dc3b3 | |||
| f380d69b9d | |||
| f06d7e6084 | |||
| 00a5dd32c0 | |||
| a9ddd44d73 | |||
| 2f9fcc63c3 | |||
| 0bbd0bdd99 | |||
| 6396dc9a85 | |||
| a3a00464bc | |||
| 1da99a0671 | |||
| 9304921132 | |||
| c13a1d752b | |||
| 51d0e02543 |
@@ -94,6 +94,7 @@ 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 }} ^
|
||||
@@ -172,6 +173,7 @@ 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 }}" \
|
||||
@@ -299,6 +301,7 @@ 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 }}" \
|
||||
|
||||
Vendored
+1
-1
Submodule 3rdparty/tracy updated: 00c079cec9...05cceee0df
@@ -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")
|
||||
@@ -444,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"
|
||||
)
|
||||
@@ -497,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
|
||||
@@ -507,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)
|
||||
|
||||
@@ -543,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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_ */
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
-48
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#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"
|
||||
@@ -59,8 +60,6 @@
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
constexpr int KEYBOARD_CONTROLLER_ID = -1000;
|
||||
|
||||
struct EventKeyboard {
|
||||
bool down;
|
||||
bool up;
|
||||
@@ -73,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;
|
||||
@@ -261,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) {
|
||||
@@ -292,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) {
|
||||
@@ -795,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
|
||||
|
||||
@@ -276,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) {
|
||||
@@ -295,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);
|
||||
@@ -795,7 +803,7 @@ int64_t KYTY_SYSV_ABI KernelLseek(int d, int64_t offset, int whence) {
|
||||
return KERNEL_ERROR_ESPIPE;
|
||||
}
|
||||
|
||||
file->mutex.Lock();
|
||||
Common::LockGuard lock(file->mutex);
|
||||
|
||||
bool is_invalid = file->f.IsInvalid();
|
||||
|
||||
@@ -820,8 +828,6 @@ int64_t KYTY_SYSV_ABI KernelLseek(int d, int64_t offset, int whence) {
|
||||
|
||||
EXIT_IF(pos != offset);
|
||||
|
||||
file->mutex.Unlock();
|
||||
|
||||
if (is_invalid) {
|
||||
LOGF("\tfile is invalid\n");
|
||||
return KERNEL_ERROR_EIO;
|
||||
|
||||
@@ -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
|
||||
@@ -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_ */
|
||||
@@ -16,6 +16,7 @@ file(GLOB launcher_forms
|
||||
add_executable(launcher WIN32
|
||||
${launcher_src}
|
||||
${launcher_forms}
|
||||
resources.qrc
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -85,9 +85,10 @@ 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;
|
||||
bool vulkan_validation_enabled = false;
|
||||
bool shader_validation_enabled = true;
|
||||
ShaderOptimizationType shader_optimization_type = ShaderOptimizationType::Performance;
|
||||
ShaderLogDirection shader_log_direction = ShaderLogDirection::Silent;
|
||||
@@ -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);
|
||||
|
||||
@@ -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_ */
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
+19
-5
@@ -82,6 +82,7 @@ public:
|
||||
Id AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format);
|
||||
bool AudioOutClose(Id handle);
|
||||
bool AudioOutValid(Id handle);
|
||||
bool AudioOutHasDevice(Id handle);
|
||||
bool AudioOutSetVolume(Id handle, uint32_t bitflag, const int* volume);
|
||||
uint32_t AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking = true);
|
||||
bool AudioOutGetStatus(Id handle, int* type, int* channels_num);
|
||||
@@ -152,6 +153,10 @@ void AudioOutClose(int handle) {
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioOutHasDevice(int handle) {
|
||||
return g_audio != nullptr && handle > 0 && g_audio->AudioOutHasDevice(Audio::Id(handle));
|
||||
}
|
||||
|
||||
uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) {
|
||||
if (g_audio == nullptr || params == nullptr || num == 0) {
|
||||
return 0;
|
||||
@@ -451,6 +456,13 @@ bool Audio::AudioOutValid(Id handle) {
|
||||
m_out_ports[handle.GetId()].used);
|
||||
}
|
||||
|
||||
bool Audio::AudioOutHasDevice(Id handle) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
return (handle.GetId() >= 0 && handle.GetId() < OUT_PORTS_MAX &&
|
||||
m_out_ports[handle.GetId()].used && m_out_ports[handle.GetId()].audio_device != 0);
|
||||
}
|
||||
|
||||
bool Audio::AudioOutGetStatus(Id handle, int* type, int* channels_num) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
@@ -516,16 +528,18 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
|
||||
max_wait_time = (wait_time > max_wait_time ? wait_time : max_wait_time);
|
||||
}
|
||||
|
||||
bool all_ports_have_device = true;
|
||||
bool any_port_has_device = false;
|
||||
for (uint32_t i = 0; i < num; i++) {
|
||||
if (m_out_ports[params[i].handle.GetId()].audio_device == 0) {
|
||||
all_ports_have_device = false;
|
||||
if (m_out_ports[params[i].handle.GetId()].audio_device != 0) {
|
||||
any_port_has_device = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Device-backed ports are paced by the SDL queue above.
|
||||
if (blocking && max_wait_time != 0 && !all_ports_have_device) {
|
||||
// One real output device is enough to pace the whole synchronized batch. Applying the fallback
|
||||
// when a vibration port is present would rate-limit the device-backed ports to exactly 1x and
|
||||
// prevent their SDL queues from building an underrun cushion.
|
||||
if (blocking && max_wait_time != 0 && !any_port_has_device) {
|
||||
Common::Thread::SleepMicro(max_wait_time);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ static constexpr int OUT_PORTS_MAX = 32;
|
||||
|
||||
int AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format);
|
||||
void AudioOutClose(int handle);
|
||||
bool AudioOutHasDevice(int handle);
|
||||
uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking = true);
|
||||
|
||||
} // namespace Libs::Audio::AudioInternal
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+21
-2
@@ -336,6 +336,17 @@ static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle p
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) {
|
||||
Common::LockGuard lock(g_audioout2_port_mutex);
|
||||
for (const auto& state: g_audioout2_ports) {
|
||||
if (state.used && state.context == ctx && state.audio_handle > 0 &&
|
||||
state.pcm_data != nullptr && AudioInternal::AudioOutHasDevice(state.audio_handle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) {
|
||||
std::vector<AudioInternal::OutputParam> params;
|
||||
params.reserve(AudioInternal::OUT_PORTS_MAX);
|
||||
@@ -493,15 +504,23 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
|
||||
uint32_t sleep_micros = audioout2_grain_micros(512);
|
||||
|
||||
for (;;) {
|
||||
// Only a synchronous submission carrying PCM to a real device can rely on the SDL queue for
|
||||
// pacing. Async pushes must retain queue-depth backpressure, and a handle without PCM (or a
|
||||
// vibration/failed-open handle) has no downstream operation that can block this call.
|
||||
const bool use_device_clock =
|
||||
blocking != 0 && audioout2_context_has_queueable_device(ctx);
|
||||
|
||||
g_audioout2_context_mutex.Lock();
|
||||
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
|
||||
audioout2_update_context_locked(state);
|
||||
sleep_micros = audioout2_grain_micros(state->num_grains);
|
||||
if (state->queued < state->queue_depth) {
|
||||
if (state->queued < state->queue_depth || use_device_clock) {
|
||||
if (state->queued == 0) {
|
||||
state->last_update = LibKernel::KernelGetProcessTime();
|
||||
}
|
||||
state->queued++;
|
||||
if (state->queued < state->queue_depth) {
|
||||
state->queued++;
|
||||
}
|
||||
g_audioout2_context_mutex.Unlock();
|
||||
audioout2_queue_context_audio(ctx, blocking != 0);
|
||||
return OK;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+33
-19
@@ -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"
|
||||
@@ -2134,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) {
|
||||
@@ -3405,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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace AudioOut2 = Libs::Audio::AudioOut2;
|
||||
std::mutex g_device_mutex;
|
||||
std::condition_variable g_device_cv;
|
||||
std::vector<int> g_live_devices;
|
||||
std::vector<int> g_device_backed_handles;
|
||||
std::vector<bool> g_output_blocking;
|
||||
int g_next_device = 1;
|
||||
int g_open_waiters = 0;
|
||||
bool g_block_opens = false;
|
||||
@@ -61,6 +63,17 @@ struct PortState {
|
||||
uint64_t reserved[6];
|
||||
};
|
||||
|
||||
struct Attribute {
|
||||
uint32_t attribute_id;
|
||||
int32_t reserved;
|
||||
const void* value;
|
||||
size_t value_size;
|
||||
};
|
||||
|
||||
struct Pcm {
|
||||
const void* data;
|
||||
};
|
||||
|
||||
const auto* AsParam(const PortParam* param) {
|
||||
return reinterpret_cast<const AudioOut2::AudioOut2PortParam*>(param);
|
||||
}
|
||||
@@ -73,6 +86,10 @@ auto* AsState(PortState* state) {
|
||||
return reinterpret_cast<AudioOut2::AudioOut2PortState*>(state);
|
||||
}
|
||||
|
||||
const auto* AsAttribute(const Attribute* attribute) {
|
||||
return reinterpret_cast<const AudioOut2::AudioOut2Attribute*>(attribute);
|
||||
}
|
||||
|
||||
PortParam MakeParam(uint32_t data_format = 0x200) {
|
||||
PortParam param {};
|
||||
param.data_format = data_format;
|
||||
@@ -80,9 +97,9 @@ PortParam MakeParam(uint32_t data_format = 0x200) {
|
||||
return param;
|
||||
}
|
||||
|
||||
AudioOut2::AudioOut2ContextHandle CreateContext() {
|
||||
AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4) {
|
||||
ContextParam param {};
|
||||
param.queue_depth = 4;
|
||||
param.queue_depth = queue_depth;
|
||||
param.num_grains = 512;
|
||||
AudioOut2::AudioOut2ContextHandle context = 0;
|
||||
Check(AudioOut2::AudioOut2ContextCreate(AsParam(¶m), nullptr, 0, &context) == OK,
|
||||
@@ -112,6 +129,23 @@ int LiveDeviceCount() {
|
||||
return static_cast<int>(g_live_devices.size());
|
||||
}
|
||||
|
||||
void SetPcm(AudioOut2::AudioOut2PortHandle port, const void* data) {
|
||||
const Pcm pcm {data};
|
||||
const Attribute attribute {0, 0, &pcm, sizeof(pcm)};
|
||||
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) == OK,
|
||||
"setting PCM failed");
|
||||
}
|
||||
|
||||
void ResetOutputCalls() {
|
||||
std::lock_guard lock(g_device_mutex);
|
||||
g_output_blocking.clear();
|
||||
}
|
||||
|
||||
std::vector<bool> OutputCalls() {
|
||||
std::lock_guard lock(g_device_mutex);
|
||||
return g_output_blocking;
|
||||
}
|
||||
|
||||
void TestSlotReuse() {
|
||||
const auto context = CreateContext();
|
||||
const auto param = MakeParam();
|
||||
@@ -202,14 +236,83 @@ void TestContextDestroyCancelsPendingCreate() {
|
||||
Check(LiveDeviceCount() == 0, "cancelled port create leaked a device");
|
||||
}
|
||||
|
||||
void TestSynchronousDevicePushBypassesModelledQueue() {
|
||||
const auto context = CreateContext(1);
|
||||
const auto param = MakeParam();
|
||||
AudioOut2::AudioOut2PortHandle port = 0;
|
||||
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK,
|
||||
"device port create failed");
|
||||
|
||||
uint32_t pcm[512] {};
|
||||
SetPcm(port, pcm);
|
||||
ResetOutputCalls();
|
||||
|
||||
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "first sync push failed");
|
||||
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK,
|
||||
"device-paced sync push was blocked by modelled queue");
|
||||
const auto calls = OutputCalls();
|
||||
Check(calls.size() == 2, "sync pushes did not reach the device backend");
|
||||
Check(calls[0] && calls[1], "sync pushes lost their blocking mode");
|
||||
|
||||
AudioOut2::AudioOut2PortDestroy(port);
|
||||
AudioOut2::AudioOut2ContextDestroy(context);
|
||||
}
|
||||
|
||||
void TestAsynchronousDevicePushKeepsQueueBounded() {
|
||||
const auto context = CreateContext(1);
|
||||
const auto param = MakeParam();
|
||||
AudioOut2::AudioOut2PortHandle port = 0;
|
||||
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK,
|
||||
"device port create failed");
|
||||
|
||||
uint32_t pcm[512] {};
|
||||
SetPcm(port, pcm);
|
||||
ResetOutputCalls();
|
||||
|
||||
Check(AudioOut2::AudioOut2ContextPush(context, 0) == OK, "first async push failed");
|
||||
Check(AudioOut2::AudioOut2ContextPush(context, 0) != OK,
|
||||
"full async queue accepted another buffer");
|
||||
const auto calls = OutputCalls();
|
||||
Check(calls.size() == 1 && !calls[0], "rejected async push reached the device backend");
|
||||
|
||||
uint32_t queued = 0;
|
||||
uint32_t available = 0;
|
||||
Check(AudioOut2::AudioOut2ContextGetQueueLevel(context, &queued, &available) == OK,
|
||||
"queue-level query failed");
|
||||
Check(queued == 1 && available == 0, "async queue level does not match accepted pushes");
|
||||
|
||||
AudioOut2::AudioOut2PortDestroy(port);
|
||||
AudioOut2::AudioOut2ContextDestroy(context);
|
||||
}
|
||||
|
||||
void TestHandleWithoutPcmDoesNotBypassQueue() {
|
||||
const auto context = CreateContext(1);
|
||||
const auto param = MakeParam();
|
||||
AudioOut2::AudioOut2PortHandle port = 0;
|
||||
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK,
|
||||
"device port create failed");
|
||||
ResetOutputCalls();
|
||||
|
||||
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "empty sync push failed");
|
||||
Check(AudioOut2::AudioOut2ContextPush(context, 0) != OK,
|
||||
"handle without PCM bypassed queue backpressure");
|
||||
Check(OutputCalls().empty(), "empty push reached the device backend");
|
||||
|
||||
AudioOut2::AudioOut2PortDestroy(port);
|
||||
AudioOut2::AudioOut2ContextDestroy(context);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace Libs::Audio::AudioInternal {
|
||||
|
||||
int AudioOutOpen(int /*type*/, uint32_t /*samples_num*/, uint32_t /*freq*/, Format /*format*/) {
|
||||
int AudioOutOpen(int type, uint32_t /*samples_num*/, uint32_t /*freq*/, Format /*format*/) {
|
||||
std::unique_lock lock(g_device_mutex);
|
||||
const int handle = g_next_device++;
|
||||
g_live_devices.push_back(handle);
|
||||
if (type != 10) {
|
||||
g_device_backed_handles.push_back(handle);
|
||||
}
|
||||
g_open_waiters++;
|
||||
g_device_cv.notify_all();
|
||||
g_device_cv.wait(lock, []() { return !g_block_opens; });
|
||||
@@ -222,9 +325,22 @@ void AudioOutClose(int handle) {
|
||||
if (it != g_live_devices.end()) {
|
||||
g_live_devices.erase(it);
|
||||
}
|
||||
const auto device_it =
|
||||
std::find(g_device_backed_handles.begin(), g_device_backed_handles.end(), handle);
|
||||
if (device_it != g_device_backed_handles.end()) {
|
||||
g_device_backed_handles.erase(device_it);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool /*blocking*/) {
|
||||
bool AudioOutHasDevice(int handle) {
|
||||
std::lock_guard lock(g_device_mutex);
|
||||
return std::find(g_device_backed_handles.begin(), g_device_backed_handles.end(), handle) !=
|
||||
g_device_backed_handles.end();
|
||||
}
|
||||
|
||||
uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool blocking) {
|
||||
std::lock_guard lock(g_device_mutex);
|
||||
g_output_blocking.push_back(blocking);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -244,6 +360,9 @@ int main() {
|
||||
TestFullTableRecovers();
|
||||
TestConcurrentCreates();
|
||||
TestContextDestroyCancelsPendingCreate();
|
||||
TestSynchronousDevicePushBypassesModelledQueue();
|
||||
TestAsynchronousDevicePushKeepsQueueBounded();
|
||||
TestHandleWithoutPcmDoesNotBypassQueue();
|
||||
std::printf("AudioOut2PortTests: all cases passed\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -1372,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);
|
||||
|
||||
@@ -1416,6 +1420,7 @@ public:
|
||||
|
||||
resources.SetGpu(nullptr);
|
||||
scheduler.Finish();
|
||||
context.ShutdownGpu();
|
||||
std::printf("[host] %-32s ok\n", "GpuMappedRangeLifecycle");
|
||||
}
|
||||
|
||||
@@ -1567,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;
|
||||
@@ -1760,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)) &&
|
||||
@@ -1840,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,
|
||||
@@ -2073,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",
|
||||
@@ -2471,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");
|
||||
@@ -2488,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(
|
||||
@@ -4949,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");
|
||||
@@ -17491,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);
|
||||
@@ -17627,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();
|
||||
@@ -17761,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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user