mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-27 16:02:37 +00:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1be2ead54 | ||
|
|
89792a3bca | ||
|
|
39e23ac92e | ||
|
|
a67a033ff8 | ||
|
|
d1c99bbfb0 | ||
|
|
a9620f98f4 | ||
|
|
28f29b8e6f | ||
|
|
3353c1ff3b | ||
|
|
70a475ec8c | ||
|
|
b3e419ff11 | ||
|
|
4428640b44 | ||
|
|
2b2915e271 | ||
|
|
7101741637 | ||
|
|
b86cf0a820 | ||
|
|
f29bd0fe35 | ||
|
|
64eea6b32d | ||
|
|
706eec3ca9 | ||
|
|
35759e48cf | ||
|
|
6409be2874 | ||
|
|
0d0b215fab | ||
|
|
11001c97f7 | ||
|
|
337f432016 | ||
|
|
581dad7dd3 | ||
|
|
64dcbe0ea1 | ||
|
|
0ecd32299c | ||
|
|
c2f8e64f6d | ||
|
|
44ba657573 | ||
|
|
44798085bd | ||
|
|
b7a1fac898 | ||
|
|
bc193f5272 | ||
|
|
17100ec968 | ||
|
|
8ba2caf58a | ||
|
|
48b52d31b5 | ||
|
|
840b9f5753 | ||
|
|
05057c9441 | ||
|
|
af4edc2865 |
No files matched your search
Vendored
+23
-2
@@ -2,10 +2,31 @@ set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
|
||||
add_subdirectory(cpuinfo)
|
||||
|
||||
# The Windows guest red-zone patcher needs a decoder/encoder and a small
|
||||
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
|
||||
include(FetchContent)
|
||||
|
||||
# The Bluetooth DualSense transports speaker audio as fixed-size Opus packets.
|
||||
set(kyty_build_testing_saved "${BUILD_TESTING}")
|
||||
set(BUILD_TESTING OFF)
|
||||
set(OPUS_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE)
|
||||
set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF CACHE BOOL "" FORCE)
|
||||
set(OPUS_INSTALL_CMAKE_CONFIG_MODULE OFF CACHE BOOL "" FORCE)
|
||||
if(MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
|
||||
# Opus assumes MSVC accepts SSE4.1 intrinsics without per-file flags, but
|
||||
# clang-cl rejects them in its baseline x64 files. Its AVX2 dispatch also
|
||||
# depends on the SSE4.1 declarations, so retain the x64 SSE2 baseline only.
|
||||
set(OPUS_X86_MAY_HAVE_SSE4_1 OFF CACHE BOOL "" FORCE)
|
||||
set(OPUS_X86_MAY_HAVE_AVX2 OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
FetchContent_Declare(opus
|
||||
GIT_REPOSITORY https://github.com/xiph/opus.git
|
||||
GIT_TAG ddbe48383984d56acd9e1ab6a090c54ca6b735a6
|
||||
GIT_SHALLOW FALSE)
|
||||
FetchContent_MakeAvailable(opus)
|
||||
set(BUILD_TESTING "${kyty_build_testing_saved}")
|
||||
install(FILES "${opus_SOURCE_DIR}/COPYING" DESTINATION "licenses/opus")
|
||||
|
||||
# The Windows guest red-zone patcher needs a decoder/encoder and a small
|
||||
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
|
||||
set(XBYAK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(
|
||||
xbyak
|
||||
|
||||
Vendored
+1724
File diff suppressed because it is too large.
Load diff
+14
-2
@@ -332,7 +332,7 @@ if (KYTY_CLANG_CL)
|
||||
set_source_files_properties(${kyty_emulator_src} PROPERTIES COMPILE_FLAGS "-Wno-pragma-pack -Wno-deprecated-declarations -D_TIMESPEC_DEFINED")
|
||||
endif()
|
||||
|
||||
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak)
|
||||
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus)
|
||||
|
||||
# Linux system libraries.
|
||||
if(LINUX)
|
||||
@@ -481,6 +481,10 @@ add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
|
||||
target_link_libraries(audio_out2_port_tests common fmt::fmt)
|
||||
target_include_directories(audio_out2_port_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(audio_out_timing_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/AudioOutTimingTests.cpp")
|
||||
target_link_libraries(audio_out_timing_tests common fmt::fmt SDL3::SDL3-static)
|
||||
target_include_directories(audio_out_timing_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(ngs2_sampler_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/Ngs2SamplerTests.cpp"
|
||||
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
|
||||
@@ -491,7 +495,7 @@ target_include_directories(ngs2_sampler_tests PRIVATE ${inc_headers})
|
||||
add_executable(pad_haptics_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/PadHapticsTests.cpp"
|
||||
)
|
||||
target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static)
|
||||
target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static opus)
|
||||
target_include_directories(pad_haptics_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
|
||||
@@ -500,6 +504,10 @@ add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
|
||||
target_link_libraries(save_data_memory_tests common fmt::fmt)
|
||||
target_include_directories(save_data_memory_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(png_enc_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/PngEncTests.cpp")
|
||||
target_link_libraries(png_enc_tests common fmt::fmt)
|
||||
target_include_directories(png_enc_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(ces_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/CesTests.cpp")
|
||||
target_link_libraries(ces_tests common fmt::fmt SDL3::SDL3-static)
|
||||
target_include_directories(ces_tests PRIVATE ${inc_headers})
|
||||
@@ -602,9 +610,11 @@ if(BUILD_TESTING)
|
||||
add_test(NAME kernel_file_system COMMAND $<TARGET_FILE:kernel_file_system_tests>)
|
||||
add_test(NAME sync_on_address COMMAND $<TARGET_FILE:sync_on_address_tests>)
|
||||
add_test(NAME audio_out2_port COMMAND $<TARGET_FILE:audio_out2_port_tests>)
|
||||
add_test(NAME audio_out_timing COMMAND $<TARGET_FILE:audio_out_timing_tests>)
|
||||
add_test(NAME ngs2_sampler COMMAND $<TARGET_FILE:ngs2_sampler_tests>)
|
||||
add_test(NAME pad_haptics COMMAND $<TARGET_FILE:pad_haptics_tests>)
|
||||
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
|
||||
add_test(NAME png_enc COMMAND $<TARGET_FILE:png_enc_tests>)
|
||||
add_test(NAME ces COMMAND $<TARGET_FILE:ces_tests>)
|
||||
add_test(NAME save_data_dialog COMMAND $<TARGET_FILE:save_data_dialog_tests>)
|
||||
add_test(NAME http_uri_parse COMMAND $<TARGET_FILE:http_uri_parse_tests>)
|
||||
@@ -677,9 +687,11 @@ if(BUILD_TESTING)
|
||||
shader_recompiler_compute_tests
|
||||
virtual_memory_allocation_tests
|
||||
audio_out2_port_tests
|
||||
audio_out_timing_tests
|
||||
ngs2_sampler_tests
|
||||
pad_haptics_tests
|
||||
save_data_memory_tests
|
||||
png_enc_tests
|
||||
ces_tests
|
||||
save_data_dialog_tests
|
||||
http_uri_parse_tests
|
||||
|
||||
@@ -47,6 +47,10 @@ const std::string& GetAudioInputDevice() {
|
||||
return g_config->audio_input_device;
|
||||
}
|
||||
|
||||
const std::optional<ControllerColor>& GetControllerColor() {
|
||||
return g_config->controller_color;
|
||||
}
|
||||
|
||||
PresentMode GetPresentMode() {
|
||||
return g_config->present_mode;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
#include "common/common.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -26,6 +28,7 @@ enum class LogDirection { Silent, Console, File };
|
||||
enum class PresentMode { Fifo, Mailbox, Immediate };
|
||||
|
||||
using Keymap = std::vector<std::string>;
|
||||
using ControllerColor = std::array<uint8_t, 3>;
|
||||
|
||||
constexpr uint32_t DEFAULT_CONSOLE_LANGUAGE = 1;
|
||||
constexpr uint32_t MAX_CONSOLE_LANGUAGE = 29;
|
||||
@@ -44,6 +47,7 @@ struct ConfigOptions {
|
||||
std::string user_name = "Kyty";
|
||||
int32_t user_id = DEFAULT_USER_ID;
|
||||
std::string audio_input_device;
|
||||
std::optional<ControllerColor> controller_color;
|
||||
PresentMode present_mode = PresentMode::Mailbox;
|
||||
int32_t gpu_index = -1;
|
||||
bool fullscreen_enabled = false;
|
||||
@@ -81,6 +85,7 @@ uint32_t GetScreenHeight();
|
||||
const std::string& GetUserName();
|
||||
int32_t GetUserId();
|
||||
const std::string& GetAudioInputDevice();
|
||||
const std::optional<ControllerColor>& GetControllerColor();
|
||||
PresentMode GetPresentMode();
|
||||
int32_t GetGpuIndex();
|
||||
bool FullscreenEnabled();
|
||||
|
||||
@@ -62,7 +62,6 @@ public:
|
||||
|
||||
void BufferInit();
|
||||
void BufferFlush();
|
||||
void BufferFlushAndWait();
|
||||
void BufferWait();
|
||||
HW::Context& GetCtx() { return m_ctx; }
|
||||
HW::UserConfig& GetUcfg() { return m_ucfg; }
|
||||
|
||||
@@ -140,6 +140,8 @@ constexpr auto MakeShaderDispatchTable() {
|
||||
g_hw_sh_func[Pm4::COMPUTE_PGM_RSRC3] = HwShSetCsRegisters;
|
||||
g_hw_sh_func[Pm4::COMPUTE_PACE_ID] = HwShSetCsRegisters;
|
||||
g_hw_sh_func[Pm4::SPI_GRAPHICS_SHADER_CONTROL_PS] = HwShSetRegisters;
|
||||
g_hw_sh_func[Pm4::SPI_SHADER_USER_DATA_ADDR_LO_PS] = HwShSetRegisters;
|
||||
g_hw_sh_func[Pm4::SPI_SHADER_USER_DATA_ADDR_HI_PS] = HwShSetRegisters;
|
||||
g_hw_sh_func[Pm4::SPI_GRAPHICS_SHADER_CONTROL_GS] = HwShSetRegisters;
|
||||
g_hw_sh_func[Pm4::SPI_SHADER_USER_DATA_ADDR_LO_GS] = HwShSetRegisters;
|
||||
g_hw_sh_func[Pm4::SPI_SHADER_USER_DATA_ADDR_HI_GS] = HwShSetRegisters;
|
||||
|
||||
@@ -3622,6 +3622,18 @@ void GraphicsInitJmpTablesShIndirect() {
|
||||
g_hw_sh_indirect_func[Pm4::SPI_GRAPHICS_SHADER_CONTROL_PS] = [](KYTY_HW_SH_INDIRECT_ARGS) {
|
||||
HwShIgnoreShaderRegister(cmd_offset, value);
|
||||
};
|
||||
for (uint32_t offset = Pm4::SPI_SHADER_USER_DATA_ADDR_LO_PS;
|
||||
offset <= Pm4::SPI_SHADER_USER_DATA_ADDR_HI_PS; offset++) {
|
||||
g_hw_sh_indirect_func[offset] = [](KYTY_HW_SH_INDIRECT_ARGS) {
|
||||
cp.GetShCtx().SetPsUserDataAddress(cmd_offset - Pm4::SPI_SHADER_USER_DATA_ADDR_LO_PS,
|
||||
value);
|
||||
};
|
||||
}
|
||||
for (uint32_t offset = 4; offset <= 5; offset++) {
|
||||
g_hw_sh_indirect_func[offset] = [](KYTY_HW_SH_INDIRECT_ARGS) {
|
||||
cp.GetShCtx().SetPsAuxiliaryTableAddress(cmd_offset - 4, value);
|
||||
};
|
||||
}
|
||||
g_hw_sh_indirect_func[Pm4::SPI_SHADER_PACE_ID_GS] = [](KYTY_HW_SH_INDIRECT_ARGS) {
|
||||
HwShIgnoreShaderRegister(cmd_offset, value);
|
||||
};
|
||||
|
||||
@@ -35,7 +35,6 @@ namespace Libs::Graphics {
|
||||
|
||||
static thread_local CommandProcessor* g_current_processor = nullptr;
|
||||
static thread_local Pm4Execution* g_current_execution = nullptr;
|
||||
static thread_local bool g_gpu_mutex_owned = false;
|
||||
static thread_local bool g_gpu_thread = false;
|
||||
static thread_local GuestGpu* g_gpu_state = nullptr;
|
||||
|
||||
@@ -54,27 +53,6 @@ struct DrawIndexedIndirectArgs {
|
||||
uint32_t start_instance_location;
|
||||
};
|
||||
|
||||
class GpuMutexLock final {
|
||||
public:
|
||||
explicit GpuMutexLock(Common::Mutex& mutex): m_mutex(mutex) {
|
||||
if (g_gpu_mutex_owned) {
|
||||
EXIT("recursive GPU mutex acquisition\n");
|
||||
}
|
||||
g_gpu_mutex_owned = true;
|
||||
m_mutex.Lock();
|
||||
}
|
||||
~GpuMutexLock() {
|
||||
if (!g_gpu_mutex_owned) {
|
||||
EXIT("invalid GPU mutex release\n");
|
||||
}
|
||||
m_mutex.Unlock();
|
||||
g_gpu_mutex_owned = false;
|
||||
}
|
||||
|
||||
private:
|
||||
Common::Mutex& m_mutex;
|
||||
};
|
||||
|
||||
static bool GraphicsRunDebugDumpEnabled() {
|
||||
return Config::GraphicsDebugDumpEnabled() &&
|
||||
Config::GetPrintfDirection() != Config::LogDirection::Silent;
|
||||
@@ -84,6 +62,7 @@ GuestGpu::GuestGpu(RenderContext& renderer): m_renderer(renderer) {
|
||||
EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
|
||||
GraphicsInitJmpTables();
|
||||
m_gfx_cp = std::make_unique<CommandProcessor>(renderer, 0);
|
||||
m_gfx_cp->Reset();
|
||||
m_thread = std::jthread(ThreadRun, this);
|
||||
}
|
||||
|
||||
@@ -160,20 +139,16 @@ void GuestGpu::Submit(std::span<const uint32_t> draw_commands,
|
||||
if (draw_commands.empty()) {
|
||||
return;
|
||||
}
|
||||
GpuMutexLock lock(m_submission_mutex);
|
||||
Submission submission;
|
||||
Submission submission;
|
||||
submission.type = SubmissionType::Graphics;
|
||||
submission.queue_id = 0;
|
||||
submission.commands = draw_commands;
|
||||
submission.constant_commands = constant_commands;
|
||||
submission.reset_processor = m_graphics_done;
|
||||
m_graphics_done = false;
|
||||
Enqueue(std::move(submission));
|
||||
}
|
||||
|
||||
void GuestGpu::SubmitCompute(uint32_t queue, std::span<const uint32_t> commands) {
|
||||
EXIT_IF(commands.empty());
|
||||
GpuMutexLock lock(m_submission_mutex);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(queue < ComputeQueueBase || queue >= ComputeQueueBase + ComputeQueueCount);
|
||||
|
||||
@@ -186,22 +161,21 @@ void GuestGpu::SubmitCompute(uint32_t queue, std::span<const uint32_t> commands)
|
||||
}
|
||||
|
||||
void GuestGpu::SubmitFlipPreparation(uint64_t request_id) {
|
||||
GpuMutexLock lock(m_submission_mutex);
|
||||
Submission submission;
|
||||
Submission submission;
|
||||
submission.type = SubmissionType::FlipPreparation;
|
||||
submission.queue_id = 0;
|
||||
submission.reset_processor = m_graphics_done;
|
||||
submission.flip_request_id = request_id;
|
||||
m_graphics_done = false;
|
||||
Enqueue(std::move(submission));
|
||||
}
|
||||
|
||||
void GuestGpu::Done() {
|
||||
GpuMutexLock lock(m_submission_mutex);
|
||||
if (!IsGpuThread()) {
|
||||
WaitForIdle();
|
||||
}
|
||||
m_graphics_done = true;
|
||||
void GuestGpu::SuspendPoint() {
|
||||
EXIT_IF(IsGpuThread() || CommandScheduler::InDeferredOperation());
|
||||
// Do not hold a queue lock while waiting: asynchronous work may be needed to
|
||||
// finish the preceding graphics frame. The first point returns immediately.
|
||||
m_suspend_point_ready->acquire();
|
||||
Submission submission;
|
||||
submission.type = SubmissionType::SuspendPoint;
|
||||
Enqueue(std::move(submission));
|
||||
m_done_num++;
|
||||
}
|
||||
|
||||
@@ -229,6 +203,9 @@ void CommandProcessor::Reset() {
|
||||
m_context_state_pushed = false;
|
||||
m_index_type_and_size = 0;
|
||||
m_index_buffer_size = 0;
|
||||
m_index_base_addr = 0;
|
||||
m_num_instances = 1;
|
||||
m_predicate_skip = false;
|
||||
m_user_data_marker = HW::UserSgprType::Unknown;
|
||||
m_draw_indirect_args_base_addr = 0;
|
||||
m_dispatch_indirect_args_base_addr = 0;
|
||||
@@ -268,10 +245,6 @@ void CommandProcessor::BufferFlush() {
|
||||
GetScheduler().Flush();
|
||||
}
|
||||
|
||||
void CommandProcessor::BufferFlushAndWait() {
|
||||
GetScheduler().FlushAndWait();
|
||||
}
|
||||
|
||||
void CommandProcessor::BufferWait() {
|
||||
BufferInit();
|
||||
GetScheduler().Finish();
|
||||
@@ -459,6 +432,7 @@ void GuestGpu::Enqueue(Submission submission) {
|
||||
}
|
||||
|
||||
void GuestGpu::WaitForIdle() {
|
||||
EXIT_IF(IsGpuThread() || CommandScheduler::InDeferredOperation());
|
||||
Common::LockGuard lock(m_queue_mutex);
|
||||
while (m_processing || !m_commands.empty() || m_submission_count != 0) {
|
||||
m_idle.Wait(&m_queue_mutex);
|
||||
@@ -564,11 +538,7 @@ void GuestGpu::ThreadRun(void* data) {
|
||||
|
||||
bool GuestGpu::Process(Submission& submission) {
|
||||
const bool first_slice = !submission.started;
|
||||
auto& cp = GetProcessor(submission.queue_id);
|
||||
|
||||
if (first_slice && submission.reset_processor) {
|
||||
cp.Reset();
|
||||
}
|
||||
auto& cp = GetProcessor(submission.queue_id);
|
||||
|
||||
if (first_slice) {
|
||||
submission.started = true;
|
||||
@@ -645,6 +615,13 @@ bool GuestGpu::Process(Submission& submission) {
|
||||
m_renderer.RunGarbageCollector();
|
||||
cp.PrepareCpuFlip(submission.flip_request_id);
|
||||
break;
|
||||
case SubmissionType::SuspendPoint:
|
||||
cp.EmitGlobalBarrier();
|
||||
m_renderer.GetCommandScheduler().DeferPriorityOperation(
|
||||
[ready = m_suspend_point_ready] { ready->release(); });
|
||||
cp.BufferFlush();
|
||||
cp.Reset();
|
||||
break;
|
||||
}
|
||||
|
||||
return complete;
|
||||
@@ -849,9 +826,7 @@ void CommandProcessor::SetPredication(uint32_t condition, uint32_t op, uint32_t
|
||||
}
|
||||
} break;
|
||||
case 0x03:
|
||||
if (wait_op != 0) {
|
||||
BufferFlushAndWait();
|
||||
}
|
||||
// The wait selector applies only to Z-pass query readiness.
|
||||
EXIT_NOT_IMPLEMENTED(address == nullptr);
|
||||
value = *reinterpret_cast<const volatile uint64_t*>(address);
|
||||
break;
|
||||
@@ -1034,9 +1009,6 @@ void CommandProcessor::DispatchDirect(uint32_t thread_group_x, uint32_t thread_g
|
||||
m_sh_ctx.SetCsWaveSize(Pm4::ComputeWaveSize(mode));
|
||||
|
||||
uint32_t frame_num = 0;
|
||||
// uint32_t local_x = 1;
|
||||
// uint32_t local_y = 1;
|
||||
// uint32_t local_z = 1;
|
||||
|
||||
{
|
||||
frame_num = m_renderer.GetGpu().GetFrameNum();
|
||||
@@ -1058,35 +1030,9 @@ void CommandProcessor::DispatchDirect(uint32_t thread_group_x, uint32_t thread_g
|
||||
}
|
||||
}
|
||||
|
||||
const auto& cs = m_sh_ctx.GetCs().cs_regs;
|
||||
// local_x = std::max(cs.num_thread_x, 1u);
|
||||
// local_y = std::max(cs.num_thread_y, 1u);
|
||||
// local_z = std::max(cs.num_thread_z, 1u);
|
||||
m_renderer.GetRenderExecutor().DispatchDirect(m_submit_id, CurrentBuffer(), thread_group_x,
|
||||
thread_group_y, thread_group_z, mode);
|
||||
}
|
||||
|
||||
/*constexpr uint32_t DispatchInitiatorUseThreadDimensions = 1u << 5u;
|
||||
auto group_count = [](uint32_t threads, uint32_t group_size) {
|
||||
return (threads == 0
|
||||
? 0u
|
||||
: (threads + std::max(group_size, 1u) - 1u) / std::max(group_size, 1u));
|
||||
};
|
||||
|
||||
auto groups_x = thread_group_x;
|
||||
auto groups_y = thread_group_y;
|
||||
auto groups_z = thread_group_z;
|
||||
if ((mode & DispatchInitiatorUseThreadDimensions) != 0) {
|
||||
groups_x = group_count(thread_group_x, local_x);
|
||||
groups_y = group_count(thread_group_y, local_y);
|
||||
groups_z = group_count(thread_group_z, local_z);
|
||||
}
|
||||
|
||||
const uint64_t invocations =
|
||||
static_cast<uint64_t>(groups_x) * groups_y * groups_z * local_x * local_y * local_z;
|
||||
if (invocations != 0) {
|
||||
BufferFlushAndWait();
|
||||
}*/
|
||||
}
|
||||
|
||||
void CommandProcessor::DispatchIndirect(uint64_t args_addr, uint32_t mode) {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <semaphore>
|
||||
#include <span>
|
||||
#include <thread>
|
||||
|
||||
@@ -32,11 +33,14 @@ public:
|
||||
void SendCommandSync(Common::UniqueFunction<void>&& command);
|
||||
|
||||
// Submitted command memory is borrowed and must remain valid until GPU execution completes.
|
||||
void Submit(std::span<const uint32_t> draw_commands,
|
||||
std::span<const uint32_t> constant_commands);
|
||||
void SubmitCompute(uint32_t queue, std::span<const uint32_t> commands);
|
||||
void SubmitFlipPreparation(uint64_t request_id);
|
||||
void Done();
|
||||
void Submit(std::span<const uint32_t> draw_commands,
|
||||
std::span<const uint32_t> constant_commands);
|
||||
void SubmitCompute(uint32_t queue, std::span<const uint32_t> commands);
|
||||
void SubmitFlipPreparation(uint64_t request_id);
|
||||
// Insert an ordered graphics drain; only a previous suspend point can block the caller.
|
||||
void SuspendPoint();
|
||||
// Wait for guest command processing, including all compute queues (not native GPU completion).
|
||||
void WaitForIdle();
|
||||
[[nodiscard]] int GetFrameNum() const;
|
||||
|
||||
[[nodiscard]] static bool IsGpuThread() noexcept;
|
||||
@@ -48,7 +52,7 @@ private:
|
||||
static constexpr uint32_t ComputeQueueBase = 0x20;
|
||||
static constexpr uint32_t QueueCount = 1 + ComputeQueueCount;
|
||||
|
||||
enum class SubmissionType { Graphics, Compute, FlipPreparation };
|
||||
enum class SubmissionType { Graphics, Compute, FlipPreparation, SuspendPoint };
|
||||
|
||||
struct Submission {
|
||||
SubmissionType type = SubmissionType::Graphics;
|
||||
@@ -57,7 +61,6 @@ private:
|
||||
std::span<const uint32_t> constant_commands;
|
||||
Pm4Execution command_execution;
|
||||
Pm4Execution constant_execution;
|
||||
bool reset_processor = false;
|
||||
bool started = false;
|
||||
bool command_complete = false;
|
||||
bool constant_complete = false;
|
||||
@@ -66,14 +69,12 @@ private:
|
||||
};
|
||||
|
||||
void Enqueue(Submission submission);
|
||||
void WaitForIdle();
|
||||
void ProcessCommands();
|
||||
bool Process(Submission& submission);
|
||||
static void ThreadRun(void* data);
|
||||
CommandProcessor& GetProcessor(uint32_t queue_id);
|
||||
|
||||
RenderContext& m_renderer;
|
||||
Common::Mutex m_submission_mutex;
|
||||
Common::Mutex m_queue_mutex;
|
||||
std::mutex m_shutdown_mutex;
|
||||
Common::CondVar m_work_available;
|
||||
@@ -84,10 +85,12 @@ private:
|
||||
uint32_t m_next_queue = 0;
|
||||
uint32_t m_submission_count = 0;
|
||||
bool m_processing = false;
|
||||
bool m_graphics_done = true;
|
||||
bool m_accepting = true;
|
||||
bool m_stopping = false;
|
||||
bool m_shutdown_complete = false;
|
||||
// Completion callbacks can outlive GuestGpu during renderer shutdown.
|
||||
std::shared_ptr<std::binary_semaphore> m_suspend_point_ready =
|
||||
std::make_shared<std::binary_semaphore>(1);
|
||||
|
||||
std::unique_ptr<CommandProcessor> m_gfx_cp;
|
||||
std::array<std::unique_ptr<CommandProcessor>, ComputeQueueCount> m_compute_cp;
|
||||
|
||||
@@ -458,6 +458,9 @@ struct PsShaderResource2 {
|
||||
|
||||
struct PsStageRegisters {
|
||||
uint64_t data_addr = 0;
|
||||
uint64_t user_data_addr = 0;
|
||||
// Hades II uses PS offsets 4:5 for a byte-addressed descriptor table.
|
||||
uint64_t auxiliary_table_addr = 0;
|
||||
PsShaderResource1 rsrc1;
|
||||
PsShaderResource2 rsrc2;
|
||||
};
|
||||
@@ -1017,6 +1020,12 @@ public:
|
||||
void SetGsShaderResource2(const GsShaderResource2& rsrc2) { m_vs.gs_regs.rsrc2 = rsrc2; }
|
||||
|
||||
void SetPsShaderBase(uint64_t addr) { m_ps.ps_regs.data_addr = addr; }
|
||||
void SetPsUserDataAddress(uint32_t word, uint32_t value) {
|
||||
SetUserDataAddressWord(m_ps.ps_regs.user_data_addr, word, value);
|
||||
}
|
||||
void SetPsAuxiliaryTableAddress(uint32_t word, uint32_t value) {
|
||||
SetUserDataAddressWord(m_ps.ps_regs.auxiliary_table_addr, word, value);
|
||||
}
|
||||
void SetPsShaderResource1(const PsShaderResource1& rsrc1) { m_ps.ps_regs.rsrc1 = rsrc1; }
|
||||
void SetPsShaderResource2(const PsShaderResource2& rsrc2) { m_ps.ps_regs.rsrc2 = rsrc2; }
|
||||
|
||||
|
||||
@@ -697,6 +697,8 @@ constexpr uint32_t CX_NUM = 0x3FF + 1;
|
||||
|
||||
/* Shader registers */
|
||||
|
||||
constexpr uint32_t SPI_SHADER_USER_DATA_ADDR_LO_PS = 0x2;
|
||||
constexpr uint32_t SPI_SHADER_USER_DATA_ADDR_HI_PS = 0x3;
|
||||
constexpr uint32_t SPI_SHADER_PACE_ID_PS = 0x6;
|
||||
constexpr uint32_t SPI_GRAPHICS_SHADER_CONTROL_PS = 0x7;
|
||||
constexpr uint32_t SPI_SHADER_PGM_LO_PS = 0x8;
|
||||
|
||||
@@ -30,21 +30,14 @@ class TrackingSpinLock final {
|
||||
public:
|
||||
void lock() noexcept {
|
||||
const auto thread = CurrentThread();
|
||||
if (m_owner.load(std::memory_order_relaxed) == thread) {
|
||||
EXIT("recursive region tracking lock\n");
|
||||
}
|
||||
while (m_lock.test_and_set(std::memory_order_acquire)) {
|
||||
if (m_owner.load(std::memory_order_relaxed) == thread) {
|
||||
EXIT("recursive region tracking lock while contended\n");
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(m_owner.load(std::memory_order_relaxed) == thread);
|
||||
std::atomic_signal_fence(std::memory_order_seq_cst);
|
||||
}
|
||||
m_owner.store(thread, std::memory_order_relaxed);
|
||||
}
|
||||
void unlock() noexcept {
|
||||
if (m_owner.load(std::memory_order_relaxed) != CurrentThread()) {
|
||||
EXIT("region tracking lock released by non-owner\n");
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(m_owner.load(std::memory_order_relaxed) != CurrentThread());
|
||||
m_owner.store(0, std::memory_order_relaxed);
|
||||
m_lock.clear(std::memory_order_release);
|
||||
}
|
||||
@@ -88,8 +81,7 @@ public:
|
||||
template <DirtySource source>
|
||||
[[nodiscard]] bool IsModified(uint64_t offset, uint64_t size) const {
|
||||
const auto [start, end] = GetPageRange(m_cpu_addr + offset, size);
|
||||
const auto& bits = GetBits<source>();
|
||||
return RegionBits(bits, start, end).Any();
|
||||
return GetBits<source>().FirstRangeFrom(start).first < end;
|
||||
}
|
||||
|
||||
template <DirtySource source, bool enable>
|
||||
|
||||
+14
-8
@@ -110,6 +110,7 @@ void BufferCache::DeleteBuffer(BufferId id) {
|
||||
}
|
||||
}
|
||||
|
||||
template <bool async>
|
||||
bool BufferCache::DownloadBufferMemory(Buffer& buffer, uint64_t vaddr, uint64_t size) {
|
||||
std::vector<vk::BufferCopy> copies;
|
||||
uint64_t total_size = 0;
|
||||
@@ -170,14 +171,22 @@ bool BufferCache::DownloadBufferMemory(Buffer& buffer, uint64_t vaddr, uint64_t
|
||||
vk::PipelineStageFlagBits::eAllCommands |
|
||||
vk::PipelineStageFlagBits::eHost,
|
||||
{}, 0, nullptr, 1, &after, 0, nullptr);
|
||||
m_scheduler.DeferPriorityOperation([this, mapped, offset, total_size, buffer_address,
|
||||
copies = std::move(copies), owner = std::move(temporary)] {
|
||||
auto publish = [this, mapped, offset, total_size, buffer_address,
|
||||
copies = std::move(copies), owner = std::move(temporary)] {
|
||||
(owner ? *owner : m_download_buffer).Invalidate(offset, total_size);
|
||||
for (const auto& copy: copies) {
|
||||
Libs::LibKernel::Memory::WriteBacking(buffer_address + copy.srcOffset,
|
||||
mapped + (copy.dstOffset - offset), copy.size);
|
||||
}
|
||||
});
|
||||
};
|
||||
if constexpr (async) {
|
||||
m_scheduler.DeferPriorityOperation(std::move(publish));
|
||||
} else {
|
||||
const auto tick = m_scheduler.CurrentTick();
|
||||
m_scheduler.Wait(tick);
|
||||
m_scheduler.WaitPriorityOperations(tick);
|
||||
publish();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -256,10 +265,7 @@ void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size, bool is_write) {
|
||||
const auto window_begin = std::max(Common::AlignDown(vaddr, WindowSize), buffer_begin);
|
||||
const auto window_end = std::min(std::max(window_begin + WindowSize, vaddr + size), buffer_end);
|
||||
|
||||
if (DownloadBufferMemory(buffer, window_begin, window_end - window_begin)) {
|
||||
const auto tick = m_scheduler.CurrentTick();
|
||||
m_scheduler.Wait(tick);
|
||||
m_scheduler.WaitPriorityOperations(tick);
|
||||
if (DownloadBufferMemory<false>(buffer, window_begin, window_end - window_begin)) {
|
||||
m_memory_tracker.UnmarkRegionAsGpuModified(window_begin, window_end - window_begin);
|
||||
}
|
||||
if (is_write) {
|
||||
@@ -614,7 +620,7 @@ void BufferCache::RunGarbageCollector() {
|
||||
return false;
|
||||
}
|
||||
if (dirty) {
|
||||
EXIT_IF(!DownloadBufferMemory(buffer, buffer.CpuAddress(), buffer.Size()));
|
||||
EXIT_NOT_IMPLEMENTED(!DownloadBufferMemory<true>(buffer, buffer.CpuAddress(), buffer.Size()));
|
||||
dirty_buffers.push_back(id);
|
||||
} else {
|
||||
m_memory_tracker.UntrackMemory(buffer.CpuAddress(), buffer.Size());
|
||||
|
||||
+2
-1
@@ -117,7 +117,8 @@ private:
|
||||
[[nodiscard]] vk::Buffer UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> copies,
|
||||
uint64_t total_size);
|
||||
[[nodiscard]] bool SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uint64_t size);
|
||||
// Queues backing publication; callers wait before clearing dirty pages or reusing their data.
|
||||
// Synchronous downloads publish before returning; asynchronous callers wait before reuse.
|
||||
template <bool async>
|
||||
[[nodiscard]] bool DownloadBufferMemory(Buffer& buffer, uint64_t vaddr, uint64_t size);
|
||||
|
||||
GraphicContext& m_graphics;
|
||||
|
||||
+4
-4
@@ -583,7 +583,7 @@ bool TextureCache::CopyD16(Image& destination, Image& source) {
|
||||
}
|
||||
|
||||
auto depth_buffer = m_tiler.GetScratchBuffer(depth_size);
|
||||
auto color_buffer = m_tiler.GetScratchBuffer(color_size);
|
||||
auto color_buffer = m_tiler.GetScratchBuffer(color_size, depth_buffer.buffer);
|
||||
const TileManager::D16Layout promote_layout {
|
||||
.width = depth.info.extent.width,
|
||||
.height = depth.info.extent.height,
|
||||
@@ -1071,7 +1071,7 @@ void TextureCache::UploadImage(Image& image, Buffer& source, uint64_t source_off
|
||||
texels_per_slice > UINT64_MAX / transfer_bytes);
|
||||
const uint64_t transfer_slice = texels_per_slice * transfer_bytes;
|
||||
EXIT_NOT_IMPLEMENTED(transfer_slice > UINT64_MAX / layers);
|
||||
auto promoted = m_tiler.GetScratchBuffer(transfer_slice * layers);
|
||||
auto promoted = m_tiler.GetScratchBuffer(transfer_slice * layers, linear.buffer);
|
||||
m_tiler.ConvertD16(
|
||||
linear, promoted, TileManager::D16Direction::Promote,
|
||||
info.pixel_format == vk::Format::eD32SfloatS8Uint,
|
||||
@@ -1533,7 +1533,7 @@ bool TextureCache::ClearImageFromBuffer(CommandBuffer& command, uint64_t address
|
||||
auto& image = m_slot_images[selected];
|
||||
vk::ClearValue clear {};
|
||||
if (aspect == vk::ImageAspectFlagBits::eColor) {
|
||||
if (!DecodePackedColorClear(image.info.pixel_format, packed_clear, clear.color)) {
|
||||
if (!DecodeColorDwordFill(image.info.pixel_format, packed_clear, clear.color)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -1679,7 +1679,7 @@ void TextureCache::DownloadDepth(Image& image, Buffer& destination, uint64_t des
|
||||
auto host_linear = m_tiler.GetScratchBuffer(transfer_size);
|
||||
image.Download(copies, host_linear.buffer, 0, host_linear.size);
|
||||
const bool tiled = info.IsTiled();
|
||||
auto guest_linear = tiled ? m_tiler.GetScratchBuffer(info.data.size)
|
||||
auto guest_linear = tiled ? m_tiler.GetScratchBuffer(info.data.size, host_linear.buffer)
|
||||
: TileManager::Result {destination.Handle(), destination_offset,
|
||||
destination.Size() - destination_offset};
|
||||
m_tiler.ConvertD16(host_linear, guest_linear, TileManager::D16Direction::Demote,
|
||||
|
||||
@@ -428,6 +428,20 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unlike a register clear, a DWORD fill repeats the same word across the entire pixel.
|
||||
// Keep this separate: the first register word alone cannot describe a 64-bit color.
|
||||
[[nodiscard]] inline bool DecodeColorDwordFill(vk::Format format, uint32_t packed,
|
||||
vk::ClearColorValue& clear) {
|
||||
if (format == vk::Format::eR16G16B16A16Sfloat) {
|
||||
if (packed != 0) {
|
||||
return false;
|
||||
}
|
||||
clear = vk::ClearColorValue {};
|
||||
return true;
|
||||
}
|
||||
return DecodePackedColorClear(format, packed, clear);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool DecodePackedStencilClear(uint32_t packed, uint8_t& clear) {
|
||||
const auto value = static_cast<uint8_t>(packed);
|
||||
if (packed != static_cast<uint32_t>(value) * 0x01010101u) {
|
||||
|
||||
@@ -31,20 +31,14 @@ namespace ImageViewOps {
|
||||
ImageViewOps::IsFormatDepthCompatible(view_format);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool IsValidImageSwizzle(uint32_t swizzle) noexcept {
|
||||
[[nodiscard]] inline bool IsValidImageSwizzle(uint32_t swizzle,
|
||||
uint32_t component_count = 4) noexcept {
|
||||
if ((swizzle & ~0xfffu) != 0) {
|
||||
return false;
|
||||
}
|
||||
for (uint32_t channel = 0; channel < 4; channel++) {
|
||||
switch (GetDstSel(swizzle, channel)) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
case 7: break;
|
||||
default: return false;
|
||||
}
|
||||
const auto selector = GetDstSel(swizzle, channel);
|
||||
if (selector > 1 && (selector < 4 || selector >= 4 + component_count)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -81,15 +75,8 @@ SelectSampledColorView(vk::Format image_format, vk::Format view_format, uint32_t
|
||||
[[nodiscard]] inline bool IsSupportedSampledDepthView(vk::Format image_format,
|
||||
vk::Format view_format,
|
||||
uint32_t swizzle) noexcept {
|
||||
if (!IsSupportedSampledDepthFormat(image_format, view_format)) {
|
||||
return false;
|
||||
}
|
||||
switch (swizzle) {
|
||||
case DstSel(4, 4, 4, 4):
|
||||
case DstSel(4, 0, 0, 0):
|
||||
case DstSel(4, 0, 0, 1): return true;
|
||||
default: return false;
|
||||
}
|
||||
return IsSupportedSampledDepthFormat(image_format, view_format) &&
|
||||
IsValidImageSwizzle(swizzle, 1);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool
|
||||
|
||||
@@ -88,30 +88,6 @@ TileManager::~TileManager() {
|
||||
}
|
||||
}
|
||||
|
||||
TileManager::Scratch TileManager::AllocateScratch(uint64_t size) {
|
||||
EXIT_IF(size == 0);
|
||||
vk::BufferCreateInfo create {};
|
||||
create.size = size;
|
||||
create.usage = vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc |
|
||||
vk::BufferUsageFlagBits::eTransferDst;
|
||||
|
||||
VmaAllocationCreateInfo allocate {};
|
||||
allocate.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VmaAllocation memory = nullptr;
|
||||
const auto raw = static_cast<VkBufferCreateInfo>(create);
|
||||
RequireVulkanSuccess(static_cast<vk::Result>(vmaCreateBuffer(
|
||||
m_graphics.allocator, &raw, &allocate, &buffer, &memory, nullptr)),
|
||||
"allocate TileManager scratch buffer");
|
||||
return {buffer, memory, size};
|
||||
}
|
||||
|
||||
void TileManager::DeferDestroy(Scratch scratch) {
|
||||
auto allocator = m_graphics.allocator;
|
||||
m_scheduler.DeferOperation(
|
||||
[allocator, scratch] { vmaDestroyBuffer(allocator, scratch.buffer, scratch.allocation); });
|
||||
}
|
||||
|
||||
void TileManager::Prepare(bool tile, uint64_t tiled_capacity, uint64_t linear_capacity,
|
||||
std::span<const GpuTileInfo> infos, uint64_t source_base,
|
||||
uint64_t target_base, std::vector<Dispatch>& dispatches) {
|
||||
@@ -369,8 +345,7 @@ TileManager::Result TileManager::Detile(vk::Buffer tiled, uint64_t tiled_offset,
|
||||
const uint64_t source_base = tiled_offset & (descriptor_alignment - 1);
|
||||
std::vector<Dispatch> dispatches;
|
||||
Prepare(false, tiled_capacity, linear_capacity, infos, source_base, 0, dispatches);
|
||||
auto scratch = AllocateScratch(Common::AlignUp(linear_capacity, 4));
|
||||
DeferDestroy(scratch);
|
||||
auto scratch = GetScratchBuffer(linear_capacity, tiled);
|
||||
Record(tiled, tiled_offset, tiled_capacity, scratch.buffer, 0, scratch.size, dispatches,
|
||||
true);
|
||||
return {scratch.buffer, 0, linear_capacity};
|
||||
@@ -400,11 +375,9 @@ void TileManager::TileImage(Image& image, std::span<const vk::BufferImageCopy> r
|
||||
std::max<uint64_t>(limits.minStorageBufferOffsetAlignment, 4);
|
||||
const uint64_t target_base = tiled_offset & (descriptor_alignment - 1);
|
||||
std::vector<Dispatch> dispatches;
|
||||
// Reserve all stream parameters before creating a scheduler-lived scratch dependency:
|
||||
// StreamBuffer::Map is allowed to submit the current tick when it wraps.
|
||||
// Reserve stream parameters before recording the image download and conversion.
|
||||
Prepare(true, tiled_capacity, linear_capacity, infos, 0, target_base, dispatches);
|
||||
auto linear = AllocateScratch(Common::AlignUp(linear_capacity, 4));
|
||||
DeferDestroy(linear);
|
||||
auto linear = GetScratchBuffer(linear_capacity, tiled);
|
||||
image.Download(regions, linear.buffer, 0, linear.size);
|
||||
Result source {linear.buffer, 0, linear.size};
|
||||
if (transform == ColorTransform::SwapBgra16) {
|
||||
@@ -414,10 +387,19 @@ void TileManager::TileImage(Image& image, std::span<const vk::BufferImageCopy> r
|
||||
dispatches, false);
|
||||
}
|
||||
|
||||
TileManager::Result TileManager::GetScratchBuffer(uint64_t size) {
|
||||
auto scratch = AllocateScratch(Common::AlignUp(size, 4));
|
||||
DeferDestroy(scratch);
|
||||
return {scratch.buffer, 0, scratch.size};
|
||||
TileManager::Result TileManager::GetScratchBuffer(uint64_t size, vk::Buffer input) {
|
||||
EXIT_IF(size == 0);
|
||||
size = Common::AlignUp(size, 4);
|
||||
auto& buffer = m_scratch[m_scratch[0] && m_scratch[0]->Handle() == input ? 1 : 0];
|
||||
if (!buffer || buffer->Size() < size) {
|
||||
if (buffer) {
|
||||
m_scheduler.DeferOperation([old = std::move(buffer)]() mutable { old.reset(); });
|
||||
}
|
||||
buffer = std::make_unique<Buffer>(m_graphics, m_scheduler, MemoryUsage::DeviceLocal, 0,
|
||||
vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc |
|
||||
vk::BufferUsageFlagBits::eTransferDst, size);
|
||||
}
|
||||
return {buffer->Handle(), 0, size};
|
||||
}
|
||||
|
||||
TileManager::StorageBinding TileManager::BindStorage(Result buffer, uint64_t size) const {
|
||||
@@ -668,9 +650,7 @@ void TileManager::SwapBgra16(Result input, Result output, uint32_t pixels) {
|
||||
|
||||
TileManager::Result TileManager::SwapBgra16(Result input) {
|
||||
EXIT_NOT_IMPLEMENTED(input.size == 0 || input.size % 8u != 0 || input.size / 8u > UINT32_MAX);
|
||||
auto output = AllocateScratch(input.size);
|
||||
DeferDestroy(output);
|
||||
Result result {output.buffer, 0, output.size};
|
||||
auto result = GetScratchBuffer(input.size, input.buffer);
|
||||
SwapBgra16(input, result, static_cast<uint32_t>(input.size / 8u));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
#include "graphics/host_gpu/vulkanCommon.h"
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
class CommandScheduler;
|
||||
class Buffer;
|
||||
class Image;
|
||||
class StreamBuffer;
|
||||
struct GraphicContext;
|
||||
@@ -62,7 +63,7 @@ public:
|
||||
~TileManager();
|
||||
KYTY_CLASS_NO_COPY(TileManager);
|
||||
|
||||
// The returned device-local buffer remains alive through the current scheduler tick.
|
||||
// Consume scratch results before the next acquisition, or pass their buffer as input.
|
||||
[[nodiscard]] Result Detile(vk::Buffer tiled, uint64_t tiled_offset, uint64_t tiled_capacity,
|
||||
uint64_t linear_capacity, std::span<const GpuTileInfo> infos);
|
||||
void Tile(vk::Buffer linear, uint64_t linear_offset, uint64_t linear_capacity, vk::Buffer tiled,
|
||||
@@ -71,7 +72,7 @@ public:
|
||||
uint64_t tiled_offset, uint64_t tiled_capacity, uint64_t linear_capacity,
|
||||
std::span<const GpuTileInfo> infos,
|
||||
ColorTransform transform = ColorTransform::None);
|
||||
[[nodiscard]] Result GetScratchBuffer(uint64_t size);
|
||||
[[nodiscard]] Result GetScratchBuffer(uint64_t size, vk::Buffer input = nullptr);
|
||||
void ConvertD16(Result source, Result target, D16Direction direction, bool d32,
|
||||
const D16Layout& layout);
|
||||
[[nodiscard]] Result SwapBgra16(Result input);
|
||||
@@ -105,23 +106,16 @@ private:
|
||||
uint32_t pipeline_slot = 0;
|
||||
uint64_t params_offset = 0;
|
||||
};
|
||||
struct Scratch {
|
||||
vk::Buffer buffer = nullptr;
|
||||
VmaAllocation allocation = nullptr;
|
||||
uint64_t size = 0;
|
||||
};
|
||||
struct StorageBinding {
|
||||
vk::DescriptorBufferInfo info;
|
||||
uint32_t base = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] Scratch AllocateScratch(uint64_t size);
|
||||
[[nodiscard]] StorageBinding BindStorage(Result buffer, uint64_t size) const;
|
||||
[[nodiscard]] static uint32_t ConversionRows(uint64_t offset, uint64_t row_stride,
|
||||
uint64_t active, uint32_t remaining,
|
||||
uint64_t alignment, uint64_t max_range,
|
||||
uint32_t max_groups) noexcept;
|
||||
void DeferDestroy(Scratch scratch);
|
||||
void Prepare(bool tile, uint64_t tiled_capacity, uint64_t linear_capacity,
|
||||
std::span<const GpuTileInfo> infos, uint64_t source_base, uint64_t target_base,
|
||||
std::vector<Dispatch>& dispatches);
|
||||
@@ -134,6 +128,7 @@ private:
|
||||
GraphicContext& m_graphics;
|
||||
CommandScheduler& m_scheduler;
|
||||
StreamBuffer& m_stream_buffer;
|
||||
std::array<std::unique_ptr<Buffer>, 2> m_scratch;
|
||||
vk::DescriptorSetLayout m_descriptor_layout = nullptr;
|
||||
vk::PipelineLayout m_pipeline_layout = nullptr;
|
||||
std::array<vk::Pipeline, PipelineCount> m_pipelines {};
|
||||
|
||||
@@ -974,6 +974,41 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
|
||||
(pipeline_bind_point == vk::PipelineBindPoint::eCompute &&
|
||||
shader_stage != vk::ShaderStageFlagBits::eCompute));
|
||||
}
|
||||
for (const auto* reader: prepared_bindings) {
|
||||
const auto& reads = reader->runtime->resources->specialization_reads;
|
||||
if (reads.empty()) continue;
|
||||
for (const auto* writer: prepared_bindings) {
|
||||
if (writer->runtime->program->has_address_writes) {
|
||||
EXIT("scalar resource reads cannot be proven disjoint from shader address writes\n");
|
||||
}
|
||||
}
|
||||
for (const auto [address, size]: reads) {
|
||||
for (const auto id: m_bound_images) {
|
||||
const auto* image = m_context.GetTextureCache().m_slot_images.try_get(id);
|
||||
if (image == nullptr ||
|
||||
(!image->binding.shader_write && !image->binding.is_target)) continue;
|
||||
for (const auto written: {image->info.data, image->info.stencil,
|
||||
image->info.metadata.range}) {
|
||||
if (written.size != 0 && ImageRangeOverlaps(address, size,
|
||||
written.address, written.size)) {
|
||||
EXIT("scalar resource reads overlap an image or attachment write\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto* writer: prepared_bindings) {
|
||||
const auto& program = *writer->runtime->program;
|
||||
for (uint32_t i = 0; i < writer->buffer_sources.size(); ++i) {
|
||||
const auto resource = program.bindings.descriptors.front().resources[i];
|
||||
if (!program.info.buffers[resource].written) continue;
|
||||
const auto& written = writer->buffer_sources[i];
|
||||
if (written.size != 0 && ImageRangeOverlaps(address, size,
|
||||
written.address, written.size)) {
|
||||
EXIT("scalar resource reads overlap a shader buffer write\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m_descriptor_buffers.clear();
|
||||
m_descriptor_images.clear();
|
||||
m_descriptor_writes.clear();
|
||||
|
||||
@@ -220,7 +220,6 @@ struct PipelineCache::ProgramCache {
|
||||
ShaderRecompiler::IR::ResourceSnapshot resources;
|
||||
ShaderRecompiler::IR::ResourceSpecialization specialization;
|
||||
std::vector<Permutation> permutations;
|
||||
bool skip_dispatch = false;
|
||||
};
|
||||
|
||||
struct ProgramKeyHash {
|
||||
@@ -288,9 +287,6 @@ struct PipelineCache::ProgramCache {
|
||||
lookup_key.code_size = static_cast<uint32_t>(params.code.size());
|
||||
BuildStageStaticKey(input_info, lookup_key.static_state);
|
||||
auto entry = programs.find(lookup_key);
|
||||
if (entry != programs.end() && entry->second.skip_dispatch) {
|
||||
return {};
|
||||
}
|
||||
const ShaderRecompiler::IR::SrtRuntime runtime {
|
||||
.user_data = user_data,
|
||||
.shader_base = params.Base(),
|
||||
@@ -358,11 +354,6 @@ struct PipelineCache::ProgramCache {
|
||||
}
|
||||
DumpShaderOriginal(stage_name, options.shader_hash, params.code);
|
||||
auto translated = ShaderRecompiler::TranslateProgram(params.code, options);
|
||||
if (translated.skip_dispatch) {
|
||||
entry = programs.try_emplace(lookup_key, ShaderRecompiler::IR::ResourcePlan {}).first;
|
||||
entry->second.skip_dispatch = true;
|
||||
return {};
|
||||
}
|
||||
if (entry == programs.end()) {
|
||||
entry = programs.try_emplace(lookup_key,
|
||||
ShaderRecompiler::IR::ExtractResourcePlan(translated.program)).first;
|
||||
@@ -723,13 +714,6 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
|
||||
}
|
||||
const auto& rt = ctx.GetRenderTarget(colors[i].target_slot);
|
||||
const auto& bc = ctx.GetBlendControl(colors[i].target_slot);
|
||||
static_params.color_srcblend[slot] = bc.color_srcblend;
|
||||
static_params.color_comb_fcn[slot] = bc.color_comb_fcn;
|
||||
static_params.color_destblend[slot] = bc.color_destblend;
|
||||
static_params.alpha_srcblend[slot] = bc.alpha_srcblend;
|
||||
static_params.alpha_comb_fcn[slot] = bc.alpha_comb_fcn;
|
||||
static_params.alpha_destblend[slot] = bc.alpha_destblend;
|
||||
static_params.separate_alpha_blend[slot] = bc.separate_alpha_blend;
|
||||
const bool alpha_remap =
|
||||
slot == 0 && ps_input_info != nullptr && ps_input_info->alpha_blend_source_remap;
|
||||
static_params.blend_enable[slot] = bc.enable && !rt.info.blend_bypass;
|
||||
@@ -748,6 +732,17 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
|
||||
if (alpha_remap) {
|
||||
static_params.blend_alpha_source_remap = true;
|
||||
}
|
||||
if (static_params.blend_enable[slot]) {
|
||||
static_params.color_srcblend[slot] = bc.color_srcblend;
|
||||
static_params.color_comb_fcn[slot] = bc.color_comb_fcn;
|
||||
static_params.color_destblend[slot] = bc.color_destblend;
|
||||
static_params.separate_alpha_blend[slot] = bc.separate_alpha_blend;
|
||||
if (bc.separate_alpha_blend) {
|
||||
static_params.alpha_srcblend[slot] = bc.alpha_srcblend;
|
||||
static_params.alpha_comb_fcn[slot] = bc.alpha_comb_fcn;
|
||||
static_params.alpha_destblend[slot] = bc.alpha_destblend;
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool with_depth =
|
||||
depth.desc.view_info.format != vk::Format::eUndefined && static_cast<bool>(depth.image_id);
|
||||
|
||||
@@ -330,9 +330,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
}
|
||||
}
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(vs_input_info.resources[index].AddTid());
|
||||
EXIT_NOT_IMPLEMENTED(vs_input_info.resources[index].SwizzleEnabled());
|
||||
|
||||
EXIT_IF(registers_num < 1 || registers_num > 4);
|
||||
}
|
||||
|
||||
|
||||
@@ -256,10 +256,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
input_info.dispatch_thread_dimensions = use_thread_dimensions;
|
||||
const auto compute_program =
|
||||
m_context.GetPipelineCache().GetComputeProgram(cs_regs, sh_regs, input_info);
|
||||
if (!compute_program) {
|
||||
// Temporary until RT is implemented.
|
||||
return;
|
||||
}
|
||||
if (use_thread_dimensions) {
|
||||
input_info.dispatch_threads_num[0] = thread_group_x;
|
||||
input_info.dispatch_threads_num[1] = thread_group_y;
|
||||
@@ -268,15 +264,14 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
|
||||
const auto& program = *input_info.stage.program;
|
||||
const auto& resources = *input_info.stage.resources;
|
||||
if (TryConsumeComputeMetaClear(input_info, buffer)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
if (TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode)) {
|
||||
if (resources.specialization_reads.empty() &&
|
||||
(TryConsumeComputeMetaClear(input_info, buffer) ||
|
||||
TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode))) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
|
||||
const bool large_workgroup =
|
||||
(input_info.threads_num[0] * input_info.threads_num[1] * input_info.threads_num[2] >= 512);
|
||||
const bool has_sampler = !program.info.samplers.empty();
|
||||
@@ -417,10 +412,6 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
ShaderComputeInputInfo input_info {};
|
||||
const auto compute_program = m_context.GetPipelineCache().GetComputeProgram(
|
||||
cs_regs, buffer.GetRegisters().GetShaderRegisters(), input_info);
|
||||
if (!compute_program) {
|
||||
// Temporary until RT is implemented.
|
||||
return;
|
||||
}
|
||||
buffer.EndRendering();
|
||||
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
|
||||
auto& bindings = m_compute_bindings;
|
||||
|
||||
@@ -119,6 +119,10 @@ void RenderContext::UnmapMemory(uint64_t vaddr, uint64_t size) {
|
||||
}
|
||||
|
||||
void RenderContext::PrepareBda() {
|
||||
if (!m_bda_logged) {
|
||||
Log::WriteToConsoleAndLog("GPU: using buffer device address (BDA) shader memory access.\n");
|
||||
m_bda_logged = true;
|
||||
}
|
||||
std::shared_lock lock(m_mapped_ranges_mutex);
|
||||
m_mapped_ranges.ForEach([this](uint64_t start, uint64_t end) {
|
||||
m_buffer_cache.SynchronizeBuffersInRange(start, end - start);
|
||||
|
||||
@@ -81,6 +81,7 @@ private:
|
||||
std::unique_ptr<GuestGpu> m_gpu;
|
||||
VideoOut::VideoOutDriver* m_video_out = nullptr;
|
||||
bool m_fault_process_pending = false;
|
||||
bool m_bda_logged = false;
|
||||
|
||||
Common::Mutex m_interrupt_mutex;
|
||||
std::vector<InterruptEqRegistration> m_interrupt_eqs;
|
||||
|
||||
@@ -1032,6 +1032,7 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
uint32_t mesh_groups = 0;
|
||||
if (mesh_active) {
|
||||
const auto& mesh = state.vertex_info[0].mesh;
|
||||
EXIT_NOT_IMPLEMENTED(mesh.fast_launch && (draw.IsIndexed() || primitive_restart_enable));
|
||||
static std::atomic_bool restart_warned = false;
|
||||
if (primitive_restart_enable && !restart_warned.exchange(true, std::memory_order_relaxed)) {
|
||||
std::printf("Warning: primitive restart is not implemented for mesh shaders; "
|
||||
|
||||
@@ -1814,6 +1814,38 @@ KYTY_SYSV_ABI int VideoOutGetOutputStatus(int handle, VideoOutOutputStatus* stat
|
||||
return OK;
|
||||
}
|
||||
|
||||
KYTY_SYSV_ABI int VideoOutAllowOutputResolutionWqhdDetection(int handle) {
|
||||
if (!DriverState().IsOpened(handle)) {
|
||||
return VIDEO_OUT_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
KYTY_SYSV_ABI int VideoOutVrrPegToFixedRate(int handle, uint64_t arg1, uint64_t arg2) {
|
||||
PRINT_NAME();
|
||||
|
||||
static std::atomic_bool logged {false};
|
||||
if (!logged.exchange(true, std::memory_order_relaxed)) {
|
||||
LOGF("\t handle = %d\n"
|
||||
"\t arg1 = 0x%016" PRIx64 "\n"
|
||||
"\t arg2 = 0x%016" PRIx64 "\n",
|
||||
handle, arg1, arg2);
|
||||
}
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
KYTY_SYSV_ABI int VideoOutVrrUnpegFromFixedRate(int handle) {
|
||||
PRINT_NAME();
|
||||
|
||||
static std::atomic_bool logged {false};
|
||||
if (!logged.exchange(true, std::memory_order_relaxed)) {
|
||||
LOGF("\t handle = %d\n", handle);
|
||||
}
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
static int ValidateOutputConfig(int handle, uint64_t mode, const VideoOutOutputOptions* options,
|
||||
void* reserved_ptr, uint64_t reserved) {
|
||||
if (!DriverState().IsOpened(handle)) {
|
||||
|
||||
@@ -90,6 +90,9 @@ KYTY_SYSV_ABI int VideoOutGetEventData(const LibKernel::EventQueue::KernelEvent*
|
||||
KYTY_SYSV_ABI int VideoOutGetEventCount(const LibKernel::EventQueue::KernelEvent* ev);
|
||||
KYTY_SYSV_ABI int VideoOutWaitVblank(int handle);
|
||||
KYTY_SYSV_ABI int VideoOutGetOutputStatus(int handle, VideoOutOutputStatus* status);
|
||||
KYTY_SYSV_ABI int VideoOutAllowOutputResolutionWqhdDetection(int handle);
|
||||
KYTY_SYSV_ABI int VideoOutVrrPegToFixedRate(int handle, uint64_t arg1, uint64_t arg2);
|
||||
KYTY_SYSV_ABI int VideoOutVrrUnpegFromFixedRate(int handle);
|
||||
KYTY_SYSV_ABI int VideoOutInitializeOutputOptions(VideoOutOutputOptions* options);
|
||||
KYTY_SYSV_ABI int VideoOutIsOutputSupported(int handle, uint64_t mode,
|
||||
const VideoOutOutputOptions* options,
|
||||
|
||||
@@ -137,11 +137,13 @@ static uint32_t VulkanFindQueueFamily(vk::PhysicalDevice device, vk::SurfaceKHR
|
||||
return static_cast<uint32_t>(-1);
|
||||
}
|
||||
|
||||
// On failure out_device is null and out_rejections says why each device was skipped.
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
||||
static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surface,
|
||||
const std::vector<const char*>& device_extensions,
|
||||
SurfaceCapabilities& out_capabilities,
|
||||
vk::PhysicalDevice& out_device, uint32_t& out_queue_family) {
|
||||
vk::PhysicalDevice& out_device, uint32_t& out_queue_family,
|
||||
std::string& out_rejections) {
|
||||
EXIT_IF(instance == nullptr);
|
||||
EXIT_IF(surface == nullptr);
|
||||
|
||||
@@ -170,14 +172,22 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
vk::PhysicalDeviceProperties device_properties {};
|
||||
device.getProperties(&device_properties);
|
||||
|
||||
const auto reject = [&](const std::string& reason) {
|
||||
LOGF("%s\n", reason.c_str());
|
||||
out_rejections +=
|
||||
skip_device ? "; " : fmt::format("\n {}: ", device_properties.deviceName.data());
|
||||
out_rejections += reason;
|
||||
skip_device = true;
|
||||
};
|
||||
|
||||
LOGF("Vulkan device: %s\n", device_properties.deviceName.data());
|
||||
if (device_properties.apiVersion < VULKAN_TARGET_API_VERSION) {
|
||||
LOGF("Vulkan %u.%u is required, but device supports only %u.%u.%u\n",
|
||||
VK_VERSION_MAJOR(VULKAN_TARGET_API_VERSION),
|
||||
VK_VERSION_MINOR(VULKAN_TARGET_API_VERSION),
|
||||
VK_VERSION_MAJOR(device_properties.apiVersion),
|
||||
VK_VERSION_MINOR(device_properties.apiVersion),
|
||||
VK_VERSION_PATCH(device_properties.apiVersion));
|
||||
reject(fmt::format("Vulkan {}.{} is required, but device supports only {}.{}.{}",
|
||||
VK_VERSION_MAJOR(VULKAN_TARGET_API_VERSION),
|
||||
VK_VERSION_MINOR(VULKAN_TARGET_API_VERSION),
|
||||
VK_VERSION_MAJOR(device_properties.apiVersion),
|
||||
VK_VERSION_MINOR(device_properties.apiVersion),
|
||||
VK_VERSION_PATCH(device_properties.apiVersion)));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -203,7 +213,7 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
fragment_barycentric.pNext = &depth_clip_control;
|
||||
features12.pNext = &fragment_barycentric;
|
||||
#endif
|
||||
features13.pNext = &features12;
|
||||
features13.pNext = &features12;
|
||||
device_features2.pNext = &features13;
|
||||
|
||||
device.getFeatures2(&device_features2);
|
||||
@@ -212,151 +222,82 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
|
||||
const auto queue_family = VulkanFindQueueFamily(device, surface);
|
||||
if (queue_family == static_cast<uint32_t>(-1)) {
|
||||
LOGF("No universal graphics, compute, and presentation queue\n");
|
||||
skip_device = true;
|
||||
reject("No universal graphics, compute, and presentation queue");
|
||||
}
|
||||
|
||||
const auto check_feature = [&](vk::Bool32 supported, const char* name,
|
||||
vk::Bool32 required = VK_TRUE) {
|
||||
if (required == VK_TRUE && supported != VK_TRUE) {
|
||||
reject(fmt::format("{} is not supported", name));
|
||||
}
|
||||
};
|
||||
|
||||
#if defined(__APPLE__)
|
||||
if (color_write_ext.colorWriteEnable != VK_TRUE) {
|
||||
LOGF("colorWriteEnable is not supported\n");
|
||||
#if !defined(__APPLE__)
|
||||
skip_device = true;
|
||||
}
|
||||
#else
|
||||
check_feature(color_write_ext.colorWriteEnable, "colorWriteEnable");
|
||||
#endif
|
||||
}
|
||||
if (image_view_min_lod.minLod != VK_TRUE) {
|
||||
LOGF("image view minLod is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
check_feature(image_view_min_lod.minLod, "image view minLod");
|
||||
|
||||
if (depth_clip_control.depthClipControl != VK_TRUE) {
|
||||
LOGF("depthClipControl is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
check_feature(depth_clip_control.depthClipControl, "depthClipControl");
|
||||
#if defined(__APPLE__)
|
||||
if (depth_clip_enable.depthClipEnable != VK_TRUE) {
|
||||
LOGF("depthClipEnable is not supported\n");
|
||||
#if !defined(__APPLE__)
|
||||
skip_device = true;
|
||||
}
|
||||
#else
|
||||
check_feature(depth_clip_enable.depthClipEnable, "depthClipEnable");
|
||||
#endif
|
||||
}
|
||||
#if !defined(__APPLE__)
|
||||
if (device_features2.features.depthClamp != VK_TRUE) {
|
||||
LOGF("depthClamp is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (fragment_barycentric.fragmentShaderBarycentric != VK_TRUE) {
|
||||
LOGF("fragmentShaderBarycentric is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
check_feature(device_features2.features.depthClamp, "depthClamp");
|
||||
check_feature(fragment_barycentric.fragmentShaderBarycentric, "fragmentShaderBarycentric");
|
||||
#endif
|
||||
|
||||
if (required_features12.samplerMirrorClampToEdge == VK_TRUE &&
|
||||
features12.samplerMirrorClampToEdge != VK_TRUE) {
|
||||
LOGF("samplerMirrorClampToEdge is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (required_features12.timelineSemaphore == VK_TRUE &&
|
||||
features12.timelineSemaphore != VK_TRUE) {
|
||||
LOGF("timelineSemaphore is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (required_features12.shaderOutputLayer == VK_TRUE &&
|
||||
features12.shaderOutputLayer != VK_TRUE) {
|
||||
LOGF("shaderOutputLayer is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (required_features12.shaderOutputViewportIndex == VK_TRUE &&
|
||||
features12.shaderOutputViewportIndex != VK_TRUE) {
|
||||
LOGF("shaderOutputViewportIndex is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (required_features12.bufferDeviceAddress == VK_TRUE &&
|
||||
features12.bufferDeviceAddress != VK_TRUE) {
|
||||
LOGF("bufferDeviceAddress is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (required_features12.shaderBufferInt64Atomics == VK_TRUE &&
|
||||
features12.shaderBufferInt64Atomics != VK_TRUE) {
|
||||
LOGF("shaderBufferInt64Atomics is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (features13.robustImageAccess != VK_TRUE) {
|
||||
LOGF("robustImageAccess is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (required_features13.dynamicRendering == VK_TRUE &&
|
||||
features13.dynamicRendering != VK_TRUE) {
|
||||
LOGF("dynamicRendering is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (required_features13.synchronization2 == VK_TRUE &&
|
||||
features13.synchronization2 != VK_TRUE) {
|
||||
LOGF("synchronization2 is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (device_features2.features.sampleRateShading != VK_TRUE) {
|
||||
LOGF("sampleRateShading is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (device_features2.features.depthBiasClamp != VK_TRUE) {
|
||||
LOGF("depthBiasClamp is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (device_features2.features.shaderClipDistance != VK_TRUE) {
|
||||
LOGF("shaderClipDistance is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (device_features2.features.shaderCullDistance != VK_TRUE) {
|
||||
LOGF("shaderCullDistance is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (device_features2.features.largePoints != VK_TRUE) {
|
||||
LOGF("largePoints is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (device_features2.features.multiViewport != VK_TRUE) {
|
||||
LOGF("multiViewport is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (device_features2.features.fillModeNonSolid != VK_TRUE) {
|
||||
LOGF("fillModeNonSolid is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (device_features2.features.fragmentStoresAndAtomics != VK_TRUE) {
|
||||
LOGF("fragmentStoresAndAtomics is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
check_feature(features12.samplerMirrorClampToEdge, "samplerMirrorClampToEdge",
|
||||
required_features12.samplerMirrorClampToEdge);
|
||||
check_feature(features12.timelineSemaphore, "timelineSemaphore",
|
||||
required_features12.timelineSemaphore);
|
||||
check_feature(features12.shaderOutputLayer, "shaderOutputLayer",
|
||||
required_features12.shaderOutputLayer);
|
||||
check_feature(features12.shaderOutputViewportIndex, "shaderOutputViewportIndex",
|
||||
required_features12.shaderOutputViewportIndex);
|
||||
check_feature(features12.bufferDeviceAddress, "bufferDeviceAddress",
|
||||
required_features12.bufferDeviceAddress);
|
||||
check_feature(features12.shaderBufferInt64Atomics, "shaderBufferInt64Atomics",
|
||||
required_features12.shaderBufferInt64Atomics);
|
||||
check_feature(features13.robustImageAccess, "robustImageAccess");
|
||||
check_feature(features13.dynamicRendering, "dynamicRendering",
|
||||
required_features13.dynamicRendering);
|
||||
check_feature(features13.synchronization2, "synchronization2",
|
||||
required_features13.synchronization2);
|
||||
check_feature(device_features2.features.sampleRateShading, "sampleRateShading");
|
||||
check_feature(device_features2.features.depthBiasClamp, "depthBiasClamp");
|
||||
check_feature(device_features2.features.shaderClipDistance, "shaderClipDistance");
|
||||
check_feature(device_features2.features.shaderCullDistance, "shaderCullDistance");
|
||||
check_feature(device_features2.features.largePoints, "largePoints");
|
||||
check_feature(device_features2.features.multiViewport, "multiViewport");
|
||||
check_feature(device_features2.features.fillModeNonSolid, "fillModeNonSolid");
|
||||
check_feature(device_features2.features.fragmentStoresAndAtomics,
|
||||
"fragmentStoresAndAtomics");
|
||||
|
||||
if (device_features2.features.samplerAnisotropy != VK_TRUE) {
|
||||
LOGF("samplerAnisotropy is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (device_features2.features.robustBufferAccess != VK_TRUE) {
|
||||
LOGF("robustBufferAccess is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
check_feature(device_features2.features.samplerAnisotropy, "samplerAnisotropy");
|
||||
check_feature(device_features2.features.robustBufferAccess, "robustBufferAccess");
|
||||
#if defined(__APPLE__)
|
||||
if (device_features2.features.depthBounds != VK_TRUE) {
|
||||
LOGF("depthBounds is not supported\n");
|
||||
#if !defined(__APPLE__)
|
||||
skip_device = true;
|
||||
}
|
||||
#else
|
||||
check_feature(device_features2.features.depthBounds, "depthBounds");
|
||||
#endif
|
||||
}
|
||||
if (device_features2.features.shaderStorageImageWriteWithoutFormat != VK_TRUE) {
|
||||
LOGF("shaderStorageImageWriteWithoutFormat is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
check_feature(device_features2.features.shaderStorageImageWriteWithoutFormat,
|
||||
"shaderStorageImageWriteWithoutFormat");
|
||||
|
||||
if (device_features2.features.shaderImageGatherExtended != VK_TRUE) {
|
||||
LOGF("shaderImageGatherExtended is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
check_feature(device_features2.features.shaderImageGatherExtended,
|
||||
"shaderImageGatherExtended");
|
||||
|
||||
if (device_features2.features.independentBlend != VK_TRUE) {
|
||||
LOGF("independentBlend is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (device_features2.features.tessellationShader != VK_TRUE) {
|
||||
LOGF("tessellationShader is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
check_feature(device_features2.features.independentBlend, "independentBlend");
|
||||
check_feature(device_features2.features.tessellationShader, "tessellationShader");
|
||||
|
||||
if (!skip_device) {
|
||||
auto available_extensions = EnumerateVulkan<vk::ExtensionProperties>(
|
||||
@@ -368,8 +309,7 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
|
||||
for (const char* ext: device_extensions) {
|
||||
if (!HasExtension(available_extensions, ext)) {
|
||||
skip_device = true;
|
||||
break;
|
||||
reject(fmt::format("{} is not supported", ext));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,86 +327,46 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
|
||||
if (!(candidate_capabilities.capabilities.supportedUsageFlags &
|
||||
vk::ImageUsageFlagBits::eTransferDst)) {
|
||||
LOGF("Surface cannot be destination of blit\n");
|
||||
skip_device = true;
|
||||
reject("Surface cannot be destination of blit");
|
||||
}
|
||||
}
|
||||
|
||||
if (!skip_device && !CheckFormat(device, vk::Format::eD32Sfloat, true,
|
||||
vk::FormatFeatureFlagBits::eDepthStencilAttachment)) {
|
||||
LOGF("Format vk::Format::eD32Sfloat cannot be used as depth buffer\n");
|
||||
skip_device = true;
|
||||
}
|
||||
|
||||
if (!skip_device && !CheckFormat(device, vk::Format::eD32SfloatS8Uint, true,
|
||||
vk::FormatFeatureFlagBits::eDepthStencilAttachment)) {
|
||||
LOGF("Format vk::Format::eD32SfloatS8Uint cannot be used as depth buffer\n");
|
||||
skip_device = true;
|
||||
}
|
||||
|
||||
if (!skip_device && !CheckFormat(device, vk::Format::eD16Unorm, true,
|
||||
vk::FormatFeatureFlagBits::eDepthStencilAttachment)) {
|
||||
LOGF("Format vk::Format::eD16Unorm cannot be used as depth buffer\n");
|
||||
skip_device = true;
|
||||
}
|
||||
|
||||
if (!skip_device && !CheckFormat(device, vk::Format::eBc3SrgbBlock, true,
|
||||
vk::FormatFeatureFlagBits::eSampledImage |
|
||||
vk::FormatFeatureFlagBits::eTransferDst)) {
|
||||
LOGF("Format vk::Format::eBc3SrgbBlock cannot be used as texture\n");
|
||||
skip_device = true;
|
||||
}
|
||||
|
||||
if (!skip_device && !CheckFormat(device, vk::Format::eR8G8B8A8Srgb, true,
|
||||
vk::FormatFeatureFlagBits::eSampledImage |
|
||||
vk::FormatFeatureFlagBits::eTransferDst)) {
|
||||
LOGF("Format vk::Format::eR8G8B8A8Srgb cannot be used as texture\n");
|
||||
skip_device = true;
|
||||
}
|
||||
|
||||
if (!skip_device && !CheckFormat(device, vk::Format::eR8Unorm, true,
|
||||
vk::FormatFeatureFlagBits::eSampledImage |
|
||||
vk::FormatFeatureFlagBits::eTransferDst)) {
|
||||
LOGF("Format vk::Format::eR8Unorm cannot be used as texture\n");
|
||||
skip_device = true;
|
||||
}
|
||||
|
||||
if (!skip_device && !CheckFormat(device, vk::Format::eR8G8Unorm, true,
|
||||
vk::FormatFeatureFlagBits::eSampledImage |
|
||||
vk::FormatFeatureFlagBits::eTransferDst)) {
|
||||
LOGF("Format vk::Format::eR8G8Unorm cannot be used as texture\n");
|
||||
skip_device = true;
|
||||
}
|
||||
|
||||
if (!skip_device && !CheckFormat(device, vk::Format::eR8G8B8A8Srgb, true,
|
||||
vk::FormatFeatureFlagBits::eStorageImage |
|
||||
vk::FormatFeatureFlagBits::eTransferDst)) {
|
||||
LOGF("Format vk::Format::eR8G8B8A8Srgb cannot be used as texture\n");
|
||||
|
||||
if (!skip_device && !CheckFormat(device, vk::Format::eR8G8B8A8Unorm, true,
|
||||
vk::FormatFeatureFlagBits::eStorageImage |
|
||||
vk::FormatFeatureFlagBits::eTransferDst)) {
|
||||
LOGF("Format vk::Format::eR8G8B8A8Unorm cannot be used as texture\n");
|
||||
skip_device = true;
|
||||
const auto check_format = [&](vk::Format format, vk::FormatFeatureFlags features,
|
||||
const char* usage) {
|
||||
if (!skip_device && !CheckFormat(device, format, true, features)) {
|
||||
reject(fmt::format("Format vk::Format::e{} cannot be used as {}",
|
||||
vk::to_string(format), usage));
|
||||
}
|
||||
};
|
||||
|
||||
const auto depth_features = vk::FormatFeatureFlagBits::eDepthStencilAttachment;
|
||||
const auto texture_features =
|
||||
vk::FormatFeatureFlagBits::eSampledImage | vk::FormatFeatureFlagBits::eTransferDst;
|
||||
const auto storage_features =
|
||||
vk::FormatFeatureFlagBits::eStorageImage | vk::FormatFeatureFlagBits::eTransferDst;
|
||||
|
||||
check_format(vk::Format::eD32Sfloat, depth_features, "depth buffer");
|
||||
check_format(vk::Format::eD32SfloatS8Uint, depth_features, "depth buffer");
|
||||
check_format(vk::Format::eD16Unorm, depth_features, "depth buffer");
|
||||
check_format(vk::Format::eBc3SrgbBlock, texture_features, "texture");
|
||||
check_format(vk::Format::eR8G8B8A8Srgb, texture_features, "texture");
|
||||
check_format(vk::Format::eR8Unorm, texture_features, "texture");
|
||||
check_format(vk::Format::eR8G8Unorm, texture_features, "texture");
|
||||
|
||||
if (!skip_device &&
|
||||
!CheckFormat(device, vk::Format::eR8G8B8A8Srgb, true, storage_features)) {
|
||||
LOGF("Format vk::Format::eR8G8B8A8Srgb cannot be used as storage image\n");
|
||||
check_format(vk::Format::eR8G8B8A8Unorm, storage_features, "storage image");
|
||||
}
|
||||
|
||||
if (!skip_device && !CheckFormat(device, vk::Format::eB8G8R8A8Srgb, true,
|
||||
vk::FormatFeatureFlagBits::eStorageImage |
|
||||
vk::FormatFeatureFlagBits::eTransferDst)) {
|
||||
LOGF("Format vk::Format::eB8G8R8A8Srgb cannot be used as texture\n");
|
||||
|
||||
if (!skip_device && !CheckFormat(device, vk::Format::eB8G8R8A8Unorm, true,
|
||||
vk::FormatFeatureFlagBits::eStorageImage |
|
||||
vk::FormatFeatureFlagBits::eTransferDst)) {
|
||||
LOGF("Format vk::Format::eB8G8R8A8Unorm cannot be used as texture\n");
|
||||
skip_device = true;
|
||||
}
|
||||
if (!skip_device &&
|
||||
!CheckFormat(device, vk::Format::eB8G8R8A8Srgb, true, storage_features)) {
|
||||
LOGF("Format vk::Format::eB8G8R8A8Srgb cannot be used as storage image\n");
|
||||
check_format(vk::Format::eB8G8R8A8Unorm, storage_features, "storage image");
|
||||
}
|
||||
|
||||
if (!skip_device && device_properties.limits.maxSamplerAnisotropy < 16.0f) {
|
||||
LOGF("maxSamplerAnisotropy < 16.0f");
|
||||
skip_device = true;
|
||||
reject("maxSamplerAnisotropy < 16.0f");
|
||||
}
|
||||
|
||||
if (skip_device) {
|
||||
@@ -1024,11 +924,13 @@ void WindowContext::CreateVulkan() {
|
||||
}
|
||||
#endif
|
||||
|
||||
std::string rejected_devices;
|
||||
VulkanFindPhysicalDevice(graphic_ctx.instance, surface, device_extensions, surface_capabilities,
|
||||
graphic_ctx.physical_device, graphic_ctx.queue_family);
|
||||
graphic_ctx.physical_device, graphic_ctx.queue_family,
|
||||
rejected_devices);
|
||||
|
||||
if (graphic_ctx.physical_device == nullptr) {
|
||||
EXIT("Could not find suitable device");
|
||||
EXIT("Could not find suitable device:%s", rejected_devices.c_str());
|
||||
}
|
||||
|
||||
vk::PhysicalDevicePushDescriptorProperties push_descriptor_properties {};
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <fmt/format.h>
|
||||
#include <map>
|
||||
@@ -523,20 +522,6 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
|
||||
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
|
||||
static_cast<uint64_t>(decoded.instructions.size()), phase_ms());
|
||||
|
||||
// Temporary workaround for games that compile ray-tracing shaders before
|
||||
// the player can select a mode without ray tracing.
|
||||
if (options.stage == ShaderType::Compute && decoded.has_bvh) {
|
||||
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
|
||||
if (!warned.test_and_set(std::memory_order_relaxed)) {
|
||||
const auto& bvh = decoded.instructions.back();
|
||||
Log::WriteToConsoleAndLog(fmt::format(
|
||||
"Warning: ray tracing is not implemented; skipping compute dispatches containing "
|
||||
"BVH intersection instructions (shader=0x{:016x}, pc=0x{:08x}, opcode=0x{:02x}).\n",
|
||||
options.shader_hash, bvh.pc, bvh.opcode_id));
|
||||
}
|
||||
return {.skip_dispatch = true};
|
||||
}
|
||||
|
||||
std::string decoded_dump;
|
||||
if (options.dump_ir) {
|
||||
decoded_dump = Decoder::ProgramToString(decoded);
|
||||
@@ -609,7 +594,7 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
|
||||
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
|
||||
static_cast<uint64_t>(ir.blocks.size()), phase_ms());
|
||||
IR::RewriteToSsa(ir.blocks);
|
||||
IR::ConstantPropagationPass(ir.blocks);
|
||||
IR::ConstantPropagationPass(ir.blocks, ir.wave_size);
|
||||
IR::ResolveControlFlowIdentities(ir);
|
||||
IR::RemoveIdentities(ir.blocks);
|
||||
IR::EliminateDeadCode(ir.blocks);
|
||||
@@ -617,7 +602,7 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
|
||||
if (read_lane_stats.rewritten_reads != 0) {
|
||||
LOGF("%s read-lane elimination: reads=%" PRIu32 "\n", GetDumpLabel(options),
|
||||
read_lane_stats.rewritten_reads);
|
||||
IR::ConstantPropagationPass(ir.blocks);
|
||||
IR::ConstantPropagationPass(ir.blocks, ir.wave_size);
|
||||
IR::ResolveControlFlowIdentities(ir);
|
||||
IR::RemoveIdentities(ir.blocks);
|
||||
IR::EliminateDeadCode(ir.blocks);
|
||||
@@ -645,7 +630,6 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
|
||||
CompileResult CompileProgram(TranslateResult translated, const CompileOptions& options,
|
||||
const IR::ResourceSpecialization& specialization,
|
||||
uint32_t push_data_start_dword) {
|
||||
EXIT_IF(translated.skip_dispatch);
|
||||
const auto emit_begin = std::chrono::steady_clock::now();
|
||||
auto& ir = translated.program;
|
||||
IR::ApplyResourceSpecialization(ir, specialization);
|
||||
|
||||
@@ -28,7 +28,6 @@ struct TranslateResult {
|
||||
IR::Program program;
|
||||
std::string decoded_dump;
|
||||
std::string cfg_dump;
|
||||
bool skip_dispatch = false;
|
||||
};
|
||||
|
||||
struct CompileResult {
|
||||
|
||||
@@ -217,7 +217,7 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
|
||||
2, ActiveAddress(*block, it, it->Arg(2), it->Arg(it->NumArgs() - 1u), resolved));
|
||||
}
|
||||
}
|
||||
ConstantPropagationPass(program.blocks);
|
||||
ConstantPropagationPass(program.blocks, program.wave_size);
|
||||
uint32_t reads = 0, writes = 0, factors = 0;
|
||||
for (auto* block: program.blocks) {
|
||||
for (auto it = block->begin(); it != block->end(); ++it) {
|
||||
@@ -317,7 +317,7 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
|
||||
inst.Invalidate();
|
||||
}
|
||||
}
|
||||
ConstantPropagationPass(program.blocks);
|
||||
ConstantPropagationPass(program.blocks, program.wave_size);
|
||||
RemoveIdentities(program.blocks);
|
||||
EliminateDeadCode(program.blocks);
|
||||
LOGF("%s tessellation lowering: reads=%u writes=%u factors=%u\n",
|
||||
|
||||
@@ -122,6 +122,16 @@ void ValidateNativeProgram(const IR::Program& program) {
|
||||
program.memory_info[index].kind == IR::ResourceKind::IndirectBuffer;
|
||||
});
|
||||
};
|
||||
const auto local_flat_handle = [&](const IR::Inst& handle) {
|
||||
return !handle.Uses().empty() &&
|
||||
std::ranges::all_of(handle.Uses(), [&](const IR::Use& use) {
|
||||
if (IR::AddressOpcodeInfoOf(use.user->GetOpcode()).access == IR::AddressAccess::None)
|
||||
return false;
|
||||
const auto index = use.user->Flags<IR::MemoryFlags>().index;
|
||||
return index < program.memory_info.size() &&
|
||||
program.memory_info[index].kind == IR::ResourceKind::FlatLocal;
|
||||
});
|
||||
};
|
||||
for (const auto* block: program.blocks) {
|
||||
for (const auto& inst: *block) {
|
||||
const auto dense = inst.Flags<uint32_t>();
|
||||
@@ -138,7 +148,7 @@ void ValidateNativeProgram(const IR::Program& program) {
|
||||
if (planning_only_handle(inst)) {
|
||||
break;
|
||||
}
|
||||
if (inst.NumArgs() != 2 || !program.info.uses_dma) {
|
||||
if (inst.NumArgs() != 2 || (!program.info.uses_dma && !local_flat_handle(inst))) {
|
||||
Fail(program, "typed address handle has invalid DMA metadata");
|
||||
}
|
||||
break;
|
||||
@@ -188,11 +198,16 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
|
||||
if (memory_index >= program.memory_info.size()) {
|
||||
Fail(program, "address operation has invalid memory metadata");
|
||||
}
|
||||
if (program.memory_info[memory_index].kind == IR::ResourceKind::Scratch) {
|
||||
const auto kind = program.memory_info[memory_index].kind;
|
||||
if (kind == IR::ResourceKind::Scratch || kind == IR::ResourceKind::FlatLocal) {
|
||||
if (program.scratch_dwords == 0) {
|
||||
Fail(program, "scratch operation has no per-thread storage");
|
||||
}
|
||||
requirements.function_scratch = true;
|
||||
if (kind == IR::ResourceKind::FlatLocal && program.stage != ShaderType::Compute &&
|
||||
program.stage != ShaderType::Mesh) {
|
||||
requirements.function_lds = true;
|
||||
}
|
||||
} else if (address_access == IR::AddressAccess::Write) {
|
||||
Fail(program, "writable FLAT/GLOBAL addresses require GPU ownership tracking");
|
||||
}
|
||||
@@ -229,7 +244,8 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
|
||||
if (kind != IR::ResourceKind::Lds && kind != IR::ResourceKind::Gds) {
|
||||
Fail(program, "shared operation has invalid resource kind");
|
||||
}
|
||||
if (inst.GetOpcode() == IR::ValueOpcode::SharedAtomicOr64) {
|
||||
if (shared_access == IR::SharedAccess::Atomic &&
|
||||
IR::SharedComponentCount(inst.GetOpcode()) == 2u) {
|
||||
if (kind != IR::ResourceKind::Lds || program.stage != ShaderType::Compute) {
|
||||
Fail(program, "64-bit shared atomics require compute LDS");
|
||||
}
|
||||
@@ -247,6 +263,7 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
|
||||
}
|
||||
}
|
||||
switch (inst.GetOpcode()) {
|
||||
case IR::ValueOpcode::BvhIntersect: requirements.bvh = true; break;
|
||||
case IR::ValueOpcode::Ballot: requirements.subgroup_ballot = true; break;
|
||||
case IR::ValueOpcode::DppMoveU32:
|
||||
case IR::ValueOpcode::ReadFirstLane:
|
||||
|
||||
@@ -444,6 +444,18 @@ uint32_t EmitUGreaterThan64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return CompareOrdered64(state, arg0, arg1, spv::OpUGreaterThan, spv::OpUGreaterThan);
|
||||
}
|
||||
|
||||
uint32_t EmitSLessThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return CompareOrdered64(state, arg0, arg1, spv::OpSLessThan, spv::OpULessThanEqual);
|
||||
}
|
||||
|
||||
uint32_t EmitULessThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return CompareOrdered64(state, arg0, arg1, spv::OpULessThan, spv::OpULessThanEqual);
|
||||
}
|
||||
|
||||
uint32_t EmitUGreaterThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return CompareOrdered64(state, arg0, arg1, spv::OpUGreaterThan, spv::OpUGreaterThanEqual);
|
||||
}
|
||||
|
||||
uint32_t EmitFPIsNan32(EmitterState& state, uint32_t arg0) {
|
||||
return EmitNative<spv::OpFUnordNotEqual, IR::Type::U1>(state, arg0, arg0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h"
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
|
||||
namespace {
|
||||
|
||||
using Vec3 = std::array<uint32_t, 3>;
|
||||
|
||||
uint32_t Extract(EmitterState& s, uint32_t type, uint32_t vector, uint32_t index) {
|
||||
const auto result = s.builder.AllocateId();
|
||||
s.builder.AddFunction(spv::OpCompositeExtract, type, result, vector, index);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t Vector(EmitterState& s, uint32_t type, std::span<const uint32_t> components) {
|
||||
const auto result = s.builder.AllocateId();
|
||||
s.builder.AddFunction(spv::OpCompositeConstruct, type, result, components);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t NodeWord(EmitterState& s, uint32_t block, uint32_t word) {
|
||||
const auto offset = Binary(s, spv::OpShiftLeftLogical, TypeU32(s), word, ConstantU32(s, 2));
|
||||
const auto address = Binary(s, spv::OpIAdd, TypeScalarU64(s), block,
|
||||
Unary(s, spv::OpUConvert, TypeScalarU64(s), offset));
|
||||
const auto pointer = Unary(s, spv::OpConvertUToPtr, TypePhysicalU32Pointer(s), address);
|
||||
const auto result = s.builder.AllocateId();
|
||||
s.builder.AddFunction(spv::OpLoad, TypeU32(s), result, pointer, spv::MemoryAccessAlignedMask, 4u);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t Triangle(EmitterState& s, uint32_t block, uint32_t kind, uint32_t bary,
|
||||
Vec3 origin, Vec3 direction) {
|
||||
const auto f = TypeF32(s), u = TypeU32(s), b = TypeBool(s);
|
||||
const auto zero = ConstantF32Value(s, 0.0f);
|
||||
const auto op = [&](spv::Op code, uint32_t x, uint32_t y) {
|
||||
const auto value = Binary(s, code, f, x, y);
|
||||
s.builder.AddAnnotation(spv::OpDecorate, value, spv::DecorationNoContraction);
|
||||
return value;
|
||||
};
|
||||
const auto cmp = [&](spv::Op code, uint32_t x, uint32_t y) { return Binary(s, code, b, x, y); };
|
||||
const auto either = [&](uint32_t x, uint32_t y) { return Binary(s, spv::OpLogicalOr, b, x, y); };
|
||||
const auto both = [&](uint32_t x, uint32_t y) { return Binary(s, spv::OpLogicalAnd, b, x, y); };
|
||||
const auto abs = [&](uint32_t x) { return EmitGlsl<GLSLstd450FAbs, IR::Type::F32>(s, x); };
|
||||
const auto is_kind = [&](uint32_t n) { return cmp(spv::OpIEqual, kind, ConstantU32(s, n)); };
|
||||
// The four triangles are (0,1,2), (1,3,2), (2,3,4), (2,4,0).
|
||||
const std::array vertices {
|
||||
EmitGlsl<GLSLstd450UMin, IR::Type::U32>(s, kind, ConstantU32(s, 2)),
|
||||
Select(s, u, is_kind(0), ConstantU32(s, 1),
|
||||
Select(s, u, is_kind(3), ConstantU32(s, 4), ConstantU32(s, 3))),
|
||||
Select(s, u, is_kind(2), ConstantU32(s, 4),
|
||||
Select(s, u, is_kind(3), ConstantU32(s, 0), ConstantU32(s, 2))) };
|
||||
std::array<Vec3, 3> positions;
|
||||
for (uint32_t i = 0; i < 3; ++i) {
|
||||
const auto first = Binary(s, spv::OpIMul, u, vertices[i], ConstantU32(s, 3));
|
||||
for (uint32_t axis = 0; axis < 3; ++axis)
|
||||
positions[i][axis] = Unary(s, spv::OpBitcast, f,
|
||||
NodeWord(s, block, Binary(s, spv::OpIAdd, u, first, ConstantU32(s, axis))));
|
||||
}
|
||||
const auto flag = NodeWord(s, block, ConstantU32(s, 15));
|
||||
const auto y_largest = cmp(spv::OpFOrdLessThan, abs(direction[0]), abs(direction[1]));
|
||||
const auto z_largest = cmp(spv::OpFOrdLessThan,
|
||||
Select(s, f, y_largest, abs(direction[1]), abs(direction[0])), abs(direction[2]));
|
||||
const auto rotate = [&](Vec3 value) {
|
||||
Vec3 result;
|
||||
for (uint32_t i = 0; i < 3; ++i)
|
||||
result[i] = Select(s, f, z_largest, value[i],
|
||||
Select(s, f, y_largest, value[(i + 2) % 3], value[(i + 1) % 3]));
|
||||
return result;
|
||||
};
|
||||
origin = rotate(origin);
|
||||
direction = rotate(direction);
|
||||
std::array<Vec3, 3> projected;
|
||||
for (uint32_t i = 0; i < 3; ++i) {
|
||||
const auto position = rotate(positions[i]);
|
||||
Vec3 relative;
|
||||
for (uint32_t axis = 0; axis < 3; ++axis)
|
||||
relative[axis] = op(spv::OpFSub, position[axis], origin[axis]);
|
||||
projected[i] = {op(spv::OpFSub, op(spv::OpFMul, relative[0], direction[2]),
|
||||
op(spv::OpFMul, direction[0], relative[2])),
|
||||
op(spv::OpFSub, op(spv::OpFMul, relative[1], direction[2]),
|
||||
op(spv::OpFMul, direction[1], relative[2])), relative[2]};
|
||||
}
|
||||
Vec3 edge, weighted;
|
||||
for (uint32_t i = 0; i < 3; ++i) {
|
||||
const auto& first = projected[(i + 1) % 3];
|
||||
const auto& second = projected[(i + 2) % 3];
|
||||
edge[i] = op(spv::OpFSub, op(spv::OpFMul, second[0], first[1]),
|
||||
op(spv::OpFMul, second[1], first[0]));
|
||||
weighted[i] = op(spv::OpFMul, edge[i], direction[2]);
|
||||
}
|
||||
auto numerator = op(spv::OpFAdd, op(spv::OpFAdd,
|
||||
op(spv::OpFMul, edge[0], projected[0][2]), op(spv::OpFMul, edge[1], projected[1][2])),
|
||||
op(spv::OpFMul, edge[2], projected[2][2]));
|
||||
auto denominator = op(spv::OpFAdd, op(spv::OpFAdd, weighted[0], weighted[1]), weighted[2]);
|
||||
uint32_t negative = ConstantBool(s, false), positive = negative;
|
||||
for (auto value: edge) {
|
||||
negative = either(negative, cmp(spv::OpFOrdLessThan, value, zero));
|
||||
positive = either(positive, cmp(spv::OpFOrdGreaterThan, value, zero));
|
||||
}
|
||||
const auto winding = cmp(spv::OpFOrdGreaterThan, denominator, zero);
|
||||
auto missed = either(both(negative, positive), cmp(spv::OpFOrdEqual, denominator, zero));
|
||||
missed = either(missed, Unary(s, spv::OpIsNan, b, numerator));
|
||||
missed = either(missed, cmp(spv::OpFOrdLessThan,
|
||||
Select(s, f, winding, numerator, Unary(s, spv::OpFNegate, f, numerator)), zero));
|
||||
for (uint32_t i = 0; i < 3; ++i) {
|
||||
const auto first = projected[(i + 1) % 3][1], second = projected[(i + 2) % 3][1];
|
||||
const auto first_zero = cmp(spv::OpFOrdEqual, first, zero);
|
||||
const auto horizontal = both(first_zero, cmp(spv::OpFOrdEqual, second, zero));
|
||||
const auto below = either(cmp(spv::OpFOrdLessThan, first, zero),
|
||||
both(first_zero, cmp(spv::OpFOrdGreaterThan, second, zero)));
|
||||
const auto right = Binary(s, spv::OpLogicalNotEqual, b, below, winding);
|
||||
const auto excluded = Select(s, b, horizontal,
|
||||
cmp(spv::OpFOrdGreaterThan, projected[i][1], zero), right);
|
||||
missed = either(missed, both(cmp(spv::OpFOrdEqual, edge[i], zero), excluded));
|
||||
}
|
||||
const auto remap = Binary(s, spv::OpShiftRightLogical, u, flag,
|
||||
Binary(s, spv::OpIMul, u, kind, ConstantU32(s, 8)));
|
||||
const auto coordinate = [&](uint32_t shift) {
|
||||
const auto index = Binary(s, spv::OpBitwiseAnd, u,
|
||||
Binary(s, spv::OpShiftRightLogical, u, remap, ConstantU32(s, shift)), ConstantU32(s, 3));
|
||||
return Unary(s, spv::OpBitcast, u, Select(s, f,
|
||||
cmp(spv::OpIEqual, index, ConstantU32(s, 1)), weighted[1],
|
||||
Select(s, f, cmp(spv::OpIEqual, index, ConstantU32(s, 2)), weighted[2], weighted[0])));
|
||||
};
|
||||
const std::array result {
|
||||
Unary(s, spv::OpBitcast, u, Select(s, f, missed, ConstantF32(s, 0x7f800000), numerator)),
|
||||
Unary(s, spv::OpBitcast, u, Select(s, f, missed, ConstantF32Value(s, 1.0f), denominator)),
|
||||
Select(s, u, bary, coordinate(0), Binary(s, spv::OpIAdd, u, flag, kind)),
|
||||
Select(s, u, bary, coordinate(2), Select(s, u, missed, ConstantU32(s, 0), ConstantU32(s, 1))) };
|
||||
return Vector(s, TypeU32Vector(s, 4), result);
|
||||
}
|
||||
|
||||
std::array<uint32_t, 6> Box(EmitterState& s, uint32_t first, uint32_t second,
|
||||
uint32_t half, uint32_t child) {
|
||||
const auto f = TypeF32(s);
|
||||
const auto half_label = s.builder.AllocateId(), full_label = s.builder.AllocateId();
|
||||
const auto merge = s.builder.AllocateId();
|
||||
s.builder.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone);
|
||||
s.builder.AddFunction(spv::OpBranchConditional, half, half_label, full_label);
|
||||
EmitLabel(s, half_label);
|
||||
std::array<uint32_t, 6> packed, full, result;
|
||||
for (uint32_t pair = 0; pair < 3; ++pair) {
|
||||
const auto word = NodeWord(s, first, ConstantU32(s, 4 + child * 3 + pair));
|
||||
const auto unpacked = EmitGlsl<GLSLstd450UnpackHalf2x16, IR::Type::F32x2>(s, word);
|
||||
packed[pair * 2] = Extract(s, f, unpacked, 0);
|
||||
packed[pair * 2 + 1] = Extract(s, f, unpacked, 1);
|
||||
}
|
||||
s.builder.AddFunction(spv::OpBranch, merge);
|
||||
EmitLabel(s, full_label);
|
||||
for (uint32_t component = 0; component < 6; ++component) {
|
||||
const auto word = 4 + child * 6 + component;
|
||||
full[component] = Unary(s, spv::OpBitcast, f,
|
||||
NodeWord(s, word < 16 ? first : second, ConstantU32(s, word % 16)));
|
||||
}
|
||||
s.builder.AddFunction(spv::OpBranch, merge);
|
||||
EmitLabel(s, merge);
|
||||
for (uint32_t component = 0; component < 6; ++component) {
|
||||
result[component] = s.builder.AllocateId();
|
||||
s.builder.AddFunction(spv::OpPhi, f, result[component], packed[component], half_label,
|
||||
full[component], full_label);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t Boxes(EmitterState& s, uint32_t first, uint32_t second, uint32_t half,
|
||||
uint32_t grow, uint32_t sort, uint32_t extent, const Vec3& origin, const Vec3& inverse) {
|
||||
const auto f = TypeF32(s), u = TypeU32(s), b = TypeBool(s);
|
||||
const auto zero = ConstantF32Value(s, 0.0f);
|
||||
std::array<uint32_t, 4> depths, indices;
|
||||
Vec3 forward;
|
||||
for (uint32_t axis = 0; axis < 3; ++axis)
|
||||
forward[axis] = Binary(s, spv::OpFOrdGreaterThanEqual, b, inverse[axis], zero);
|
||||
for (uint32_t child = 0; child < 4; ++child) {
|
||||
const auto box = Box(s, first, second, half, child);
|
||||
Vec3 near, far;
|
||||
for (uint32_t axis = 0; axis < 3; ++axis) {
|
||||
const auto lo = Binary(s, spv::OpFMul, f,
|
||||
Binary(s, spv::OpFSub, f, box[axis], origin[axis]), inverse[axis]);
|
||||
const auto hi = Binary(s, spv::OpFMul, f,
|
||||
Binary(s, spv::OpFSub, f, box[axis + 3], origin[axis]), inverse[axis]);
|
||||
near[axis] = Select(s, f, forward[axis], lo, hi);
|
||||
far[axis] = Select(s, f, forward[axis], hi, lo);
|
||||
}
|
||||
const auto entry = EmitGlsl<GLSLstd450FMax, IR::Type::F32>(s,
|
||||
EmitGlsl<GLSLstd450FMax, IR::Type::F32>(s, near[0], near[1]), near[2]);
|
||||
const auto exit = EmitGlsl<GLSLstd450FMin, IR::Type::F32>(s,
|
||||
EmitGlsl<GLSLstd450FMin, IR::Type::F32>(s, far[0], far[1]), far[2]);
|
||||
// Valid hits have nonnegative exit distance. Saturate ULP growth at +infinity.
|
||||
const auto bits = Unary(s, spv::OpBitcast, u, Select(s, f,
|
||||
Binary(s, spv::OpFOrdEqual, b, exit, zero), zero, exit));
|
||||
const auto grown_bits = EmitGlsl<GLSLstd450UMin, IR::Type::U32>(s,
|
||||
Binary(s, spv::OpIAdd, u, bits, grow), ConstantU32(s, 0x7f800000));
|
||||
const auto grown = Unary(s, spv::OpBitcast, f, grown_bits);
|
||||
auto hit = Binary(s, spv::OpLogicalAnd, b,
|
||||
Binary(s, spv::OpFOrdGreaterThanEqual, b, exit, zero),
|
||||
Binary(s, spv::OpFOrdLessThan, b, entry, extent));
|
||||
hit = Binary(s, spv::OpLogicalAnd, b, hit,
|
||||
Binary(s, spv::OpFOrdLessThanEqual, b, entry, grown));
|
||||
depths[child] = Unary(s, spv::OpBitcast, u, Select(s, f, hit,
|
||||
Select(s, f, Binary(s, spv::OpFOrdGreaterThan, b, entry, zero), entry, zero),
|
||||
ConstantF32(s, 0x7f800000)));
|
||||
indices[child] = Select(s, u, hit, NodeWord(s, first, ConstantU32(s, child)), ConstantU32(s, ~0u));
|
||||
}
|
||||
// A select network keeps the original child order when sorting is disabled.
|
||||
constexpr std::array<std::pair<uint32_t, uint32_t>, 5> network {{{0,1}, {2,3}, {0,2}, {1,3}, {1,2}}};
|
||||
for (size_t step = 0; step < network.size(); ++step) {
|
||||
const auto [a, c] = network[step];
|
||||
const auto swap = Binary(s, spv::OpLogicalAnd, b, sort,
|
||||
Binary(s, spv::OpUGreaterThan, b, depths[a], depths[c]));
|
||||
const auto depth = depths[a], index = indices[a];
|
||||
const auto later = [&](uint32_t position) {
|
||||
return std::ranges::any_of(std::span(network).subspan(step + 1), [=](auto pair) {
|
||||
return pair.first == position || pair.second == position;
|
||||
});
|
||||
};
|
||||
if (later(a)) depths[a] = Select(s, u, swap, depths[c], depth);
|
||||
if (later(c)) depths[c] = Select(s, u, swap, depth, depths[c]);
|
||||
indices[a] = Select(s, u, swap, indices[c], index);
|
||||
indices[c] = Select(s, u, swap, index, indices[c]);
|
||||
}
|
||||
return Vector(s, TypeU32Vector(s, 4), indices);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void DefineBvhIntersect(EmitterState& s) {
|
||||
if (!s.requirements.bvh) return;
|
||||
const auto u = TypeU32(s), f = TypeF32(s), b = TypeBool(s), wide = TypeScalarU64(s);
|
||||
const auto vec4 = TypeU32Vector(s, 4), vec3 = TypeF32Vector(s, 3);
|
||||
const auto signature = s.builder.Type(spv::OpTypeFunction, vec4, vec4, u, f, vec3, vec3, vec3);
|
||||
s.bvh_intersect_function = s.builder.AllocateId();
|
||||
s.builder.AddName(s.bvh_intersect_function, "bvh_intersect");
|
||||
s.builder.AddFunction(spv::OpFunction, vec4, s.bvh_intersect_function,
|
||||
spv::FunctionControlMaskNone, signature);
|
||||
std::array<uint32_t, 6> args;
|
||||
const std::array types {vec4, u, f, vec3, vec3, vec3};
|
||||
for (uint32_t i = 0; i < args.size(); ++i) {
|
||||
args[i] = s.builder.AllocateId();
|
||||
s.builder.AddFunction(spv::OpFunctionParameter, types[i], args[i]);
|
||||
}
|
||||
EmitLabel(s, s.builder.AllocateId());
|
||||
const auto descriptor = args[0], node = args[1], extent = args[2];
|
||||
Vec3 origin, direction, inverse;
|
||||
for (uint32_t i = 0; i < 3; ++i) {
|
||||
origin[i] = Extract(s, f, args[3], i);
|
||||
direction[i] = Extract(s, f, args[4], i);
|
||||
inverse[i] = Extract(s, f, args[5], i);
|
||||
}
|
||||
std::array<uint32_t, 4> words;
|
||||
for (uint32_t i = 0; i < 4; ++i) words[i] = Extract(s, u, descriptor, i);
|
||||
const auto and_bits = [&](uint32_t value, uint32_t mask) {
|
||||
return Binary(s, spv::OpBitwiseAnd, u, value, ConstantU32(s, mask));
|
||||
};
|
||||
const auto shr = [&](uint32_t value, uint32_t shift) {
|
||||
return Binary(s, spv::OpShiftRightLogical, u, value, ConstantU32(s, shift));
|
||||
};
|
||||
const auto kind = and_bits(node, 7);
|
||||
const auto triangle = Binary(s, spv::OpULessThan, b, kind, ConstantU32(s, 4));
|
||||
const auto full = Binary(s, spv::OpIEqual, b, kind, ConstantU32(s, 5));
|
||||
const auto half = Binary(s, spv::OpIEqual, b, kind, ConstantU32(s, 4));
|
||||
const auto bary = Binary(s, spv::OpINotEqual, b, and_bits(words[3], 0x01000000), ConstantU32(s, 0));
|
||||
const auto sort = Binary(s, spv::OpINotEqual, b, and_bits(words[1], 0x80000000), ConstantU32(s, 0));
|
||||
const auto grow = and_bits(shr(words[1], 23), 0xff);
|
||||
const auto base = Binary(s, spv::OpShiftLeftLogical, wide,
|
||||
DeviceAddressFromWords(s, words[0], and_bits(words[1], 0xff)), ConstantDeviceAddress(s, 8));
|
||||
const auto index = Unary(s, spv::OpUConvert, wide, shr(node, 3));
|
||||
const auto last_index = Binary(s, spv::OpIAdd, wide, index,
|
||||
Select(s, wide, full, ConstantDeviceAddress(s, 1), ConstantDeviceAddress(s, 0)));
|
||||
const auto address = Binary(s, spv::OpIAdd, wide, base,
|
||||
Binary(s, spv::OpShiftLeftLogical, wide, index, ConstantDeviceAddress(s, 6)));
|
||||
const auto last_address = Binary(s, spv::OpIAdd, wide, base,
|
||||
Binary(s, spv::OpShiftLeftLogical, wide, last_index, ConstantDeviceAddress(s, 6)));
|
||||
auto valid = Binary(s, spv::OpIEqual, b, shr(words[3], 28), ConstantU32(s, 8));
|
||||
const auto require = [&](uint32_t condition) {
|
||||
valid = Binary(s, spv::OpLogicalAnd, b, valid, condition);
|
||||
};
|
||||
require(Binary(s, spv::OpULessThanEqual, b, kind, ConstantU32(s, 5)));
|
||||
require(Binary(s, spv::OpULessThanEqual, b, last_index, DeviceAddressFromWords(s, words[2], and_bits(words[3], 0x3ff))));
|
||||
require(Binary(s, spv::OpULessThan, b, last_address, ConstantDeviceAddress(s, uint64_t{1} << 48)));
|
||||
require(Unary(s, spv::OpLogicalNot, b, Unary(s, spv::OpIsNan, b, extent)));
|
||||
for (uint32_t i = 0; i < 3; ++i) {
|
||||
for (const auto value: {origin[i], direction[i], inverse[i]})
|
||||
require(Unary(s, spv::OpLogicalNot, b, Unary(s, spv::OpIsNan, b, value)));
|
||||
require(Unary(s, spv::OpLogicalNot, b, Unary(s, spv::OpIsInf, b, origin[i])));
|
||||
}
|
||||
const std::array triangle_invalid {ConstantU32(s, 0x7f800000), ConstantU32(s, 0x3f800000),
|
||||
ConstantU32(s, ~0u), ConstantU32(s, 0)};
|
||||
const std::array box_invalid {ConstantU32(s, ~0u), ConstantU32(s, ~0u),
|
||||
ConstantU32(s, ~0u), ConstantU32(s, ~0u)};
|
||||
std::array<uint32_t, 4> invalid_words;
|
||||
for (uint32_t i = 0; i < 4; ++i)
|
||||
invalid_words[i] = Select(s, u, triangle, triangle_invalid[i], box_invalid[i]);
|
||||
const auto invalid = Vector(s, vec4, invalid_words);
|
||||
const auto result = EmitValueOrDefaultIfCondition(s, valid, vec4, invalid, [&] {
|
||||
const auto first = GetBdaPointer(s, address);
|
||||
const auto second = EmitValueOrDefaultIfCondition(s, full, wide, ConstantDeviceAddress(s, 0), [&] {
|
||||
return GetBdaPointer(s, Binary(s, spv::OpIAdd, wide, address, ConstantDeviceAddress(s, 64)));
|
||||
});
|
||||
const auto present = Binary(s, spv::OpLogicalAnd, b,
|
||||
Binary(s, spv::OpINotEqual, b, first, ConstantDeviceAddress(s, 0)),
|
||||
Binary(s, spv::OpLogicalOr, b, Unary(s, spv::OpLogicalNot, b, full),
|
||||
Binary(s, spv::OpINotEqual, b, second, ConstantDeviceAddress(s, 0))));
|
||||
return EmitValueOrDefaultIfCondition(s, present, vec4, invalid, [&] {
|
||||
const auto tri_label = s.builder.AllocateId(), box_label = s.builder.AllocateId();
|
||||
const auto merge = s.builder.AllocateId();
|
||||
s.builder.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone);
|
||||
s.builder.AddFunction(spv::OpBranchConditional, triangle, tri_label, box_label);
|
||||
EmitLabel(s, tri_label);
|
||||
const auto tri_value = Triangle(s, first, kind, bary, origin, direction);
|
||||
const auto tri_exit = s.current_label;
|
||||
s.builder.AddFunction(spv::OpBranch, merge);
|
||||
EmitLabel(s, box_label);
|
||||
const auto box_value = Boxes(s, first, second, half, grow, sort, extent, origin, inverse);
|
||||
const auto box_exit = s.current_label;
|
||||
s.builder.AddFunction(spv::OpBranch, merge);
|
||||
EmitLabel(s, merge);
|
||||
const auto value = s.builder.AllocateId();
|
||||
s.builder.AddFunction(spv::OpPhi, vec4, value, tri_value, tri_exit, box_value, box_exit);
|
||||
return value;
|
||||
});
|
||||
});
|
||||
s.builder.AddFunction(spv::OpReturnValue, result);
|
||||
s.builder.AddFunction(spv::OpFunctionEnd);
|
||||
}
|
||||
|
||||
uint32_t EmitBvhIntersect(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
auto& s = ctx.state;
|
||||
const auto* ray = ctx.ImageAddress(inst.Arg(1));
|
||||
if (ray == nullptr || ray->NumArgs() < 11 || s.bvh_intersect_function == 0)
|
||||
ctx.Fail(inst, "BVH intersection requires eleven address components and its shared function");
|
||||
const auto vector = [&](uint32_t first) {
|
||||
Vec3 values;
|
||||
for (uint32_t i = 0; i < 3; ++i)
|
||||
values[i] = Unary(s, spv::OpBitcast, TypeF32(s), ctx.Arg(*ray, first + i));
|
||||
return Vector(s, TypeF32Vector(s, 3), values);
|
||||
};
|
||||
const auto descriptor = ctx.Arg(inst, 0), node = ctx.Arg(*ray, 0);
|
||||
const auto extent = Unary(s, spv::OpBitcast, TypeF32(s), ctx.Arg(*ray, 1));
|
||||
const auto origin = vector(2), direction = vector(5), inverse = vector(8);
|
||||
return EmitValueOrDefaultIfCondition(s, ctx.Arg(inst, 2), TypeU32Vector(s, 4),
|
||||
ConstantU32CompositeZero(s, 4), [&] {
|
||||
const auto result = s.builder.AllocateId();
|
||||
s.builder.AddFunction(spv::OpFunctionCall, TypeU32Vector(s, 4), result,
|
||||
s.bvh_intersect_function, descriptor, node, extent, origin, direction, inverse);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter
|
||||
@@ -539,19 +539,6 @@ uint32_t StoreTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t d
|
||||
return PackImageTexel(ctx, mem, texel);
|
||||
}
|
||||
|
||||
spv::Op ImageAtomicOpcode(IR::ValueOpcode opcode) {
|
||||
switch (opcode) {
|
||||
case IR::ValueOpcode::ImageAtomicSwap32: return spv::OpAtomicExchange;
|
||||
case IR::ValueOpcode::ImageAtomicIAdd32: return spv::OpAtomicIAdd;
|
||||
case IR::ValueOpcode::ImageAtomicUMin32: return spv::OpAtomicUMin;
|
||||
case IR::ValueOpcode::ImageAtomicUMax32: return spv::OpAtomicUMax;
|
||||
case IR::ValueOpcode::ImageAtomicAnd32: return spv::OpAtomicAnd;
|
||||
case IR::ValueOpcode::ImageAtomicOr32: return spv::OpAtomicOr;
|
||||
case IR::ValueOpcode::ImageAtomicXor32: return spv::OpAtomicXor;
|
||||
default: return spv::OpNop;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
@@ -876,10 +863,9 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
ctx.Define(inst, ResultVector(ctx, result, numeric_class, dref, mem));
|
||||
return;
|
||||
}
|
||||
const auto atomic_opcode = ImageAtomicOpcode(op);
|
||||
if (image_info.access == IR::ImageAccess::Atomic) {
|
||||
const auto dimension = image.dimension;
|
||||
ctx.Define(inst, EmitValueOrZeroIfCondition(state, ctx.Arg(inst, 3), [&]() {
|
||||
ctx.Define(inst, EmitValueOrZeroIfCondition(state, ctx.Arg(inst, inst.NumArgs() - 1), [&]() {
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
const auto pointer_type = state.builder.Type(
|
||||
spv::OpTypePointer, spv::StorageClassImage, TypeU32(state));
|
||||
@@ -896,12 +882,8 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
op == IR::ValueOpcode::ImageAtomicFMax32);
|
||||
});
|
||||
}
|
||||
const auto old = state.builder.AllocateId();
|
||||
state.builder.AddFunction(atomic_opcode, TypeU32(state), old, pointer,
|
||||
ConstantU32(state, spv::ScopeDevice),
|
||||
ConstantU32(state, spv::MemorySemanticsMaskNone),
|
||||
ctx.Arg(inst, 2));
|
||||
EmitDeviceAtomicMemoryBarrier(state);
|
||||
const auto old = EmitAtomicOperation(ctx, inst, pointer, spv::ScopeDevice);
|
||||
EmitAtomicMemoryBarrier(state, IR::ResourceKind::Image);
|
||||
return old;
|
||||
}));
|
||||
return;
|
||||
|
||||
@@ -111,6 +111,9 @@ uint32_t EmitIEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitINotEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitULessThan64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitSLessThan64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitSLessThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitULessThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitUGreaterThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitUGreaterThan64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
EMIT_NATIVE(LogicalOr, OpLogicalOr, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(LogicalAnd, OpLogicalAnd, U1, uint32_t, uint32_t)
|
||||
@@ -133,6 +136,9 @@ uint32_t EmitFloatCompare32(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
return EmitNative<opcode, IR::Type::U1>(ctx.state, operand(0), operand(1));
|
||||
}
|
||||
inline constexpr auto EmitFPOrdEqual32 = EmitFloatCompare32<spv::OpFOrdEqual>;
|
||||
EMIT_NATIVE(FPOrdEqual64, OpFOrdEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPOrdLessThanEqual64, OpFOrdLessThanEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPOrdGreaterThanEqual64, OpFOrdGreaterThanEqual, U1, uint32_t, uint32_t)
|
||||
inline constexpr auto EmitFPUnordEqual32 = EmitFloatCompare32<spv::OpFUnordEqual>;
|
||||
inline constexpr auto EmitFPOrdNotEqual32 = EmitFloatCompare32<spv::OpFOrdNotEqual>;
|
||||
inline constexpr auto EmitFPUnordNotEqual32 = EmitFloatCompare32<spv::OpFUnordNotEqual>;
|
||||
@@ -149,6 +155,7 @@ inline constexpr auto EmitFPCmpClass32 = EmitClassMaskF32;
|
||||
EMIT_NATIVE(FPAdd32, OpFAdd, F32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPSub32, OpFSub, F32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPMul32, OpFMul, F32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPAdd64, OpFAdd, F64, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPMul64, OpFMul, F64, uint32_t, uint32_t)
|
||||
inline constexpr auto EmitFPFma64 =
|
||||
EmitGlsl<GLSLstd450Fma, IR::Type::F64, uint32_t, uint32_t, uint32_t>;
|
||||
@@ -222,7 +229,7 @@ void EmitLoadMemory(ValueEmitContext& ctx, const IR::Inst& inst
|
||||
void EmitStoreMemory(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitAtomic32(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitBufferAtomic64(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
void EmitSharedAtomicOr64(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
void EmitSharedAtomic64(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitBufferFloatAtomic(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
void EmitSharedFloatAtomic(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitSharedIncDec(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
@@ -258,6 +265,7 @@ inline constexpr auto EmitBufferAtomicUMin32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicSMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicUMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicAnd32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicAnd64 = EmitBufferAtomic64;
|
||||
inline constexpr auto EmitBufferAtomicOr32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicOr64 = EmitBufferAtomic64;
|
||||
inline constexpr auto EmitBufferAtomicXor32 = EmitAtomic32;
|
||||
@@ -279,6 +287,7 @@ inline constexpr auto EmitSharedAtomicFMin32 = EmitSharedFloatAtomic;
|
||||
inline constexpr auto EmitSharedAtomicFMax32 = EmitSharedFloatAtomic;
|
||||
inline constexpr auto EmitSharedAtomicSwap32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicIAdd32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicIAdd64 = EmitSharedAtomic64;
|
||||
inline constexpr auto EmitSharedAtomicISub32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicInc32 = EmitSharedIncDec;
|
||||
inline constexpr auto EmitSharedAtomicDec32 = EmitSharedIncDec;
|
||||
@@ -288,20 +297,25 @@ inline constexpr auto EmitSharedAtomicSMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicUMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicAnd32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicOr32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicOr64 = EmitSharedAtomic64;
|
||||
inline constexpr auto EmitSharedAtomicXor32 = EmitAtomic32;
|
||||
inline constexpr auto EmitDataAppend = EmitAppendConsume;
|
||||
inline constexpr auto EmitDataConsume = EmitAppendConsume;
|
||||
uint32_t EmitSwizzleU32(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
uint32_t EmitBvhIntersect(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
inline constexpr auto EmitImageQueryDimensions = EmitImage;
|
||||
inline constexpr auto EmitImageQueryLod = EmitImage;
|
||||
inline constexpr auto EmitImageRead = EmitImage;
|
||||
inline constexpr auto EmitImageWrite = EmitImage;
|
||||
inline constexpr auto EmitImageSampleRaw = EmitImage;
|
||||
inline constexpr auto EmitImageGatherRaw = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicCompareSwap32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicSwap32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicIAdd32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicSMin32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicUMin32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicSMax32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicUMax32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicAnd32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicOr32 = EmitImage;
|
||||
|
||||
@@ -58,6 +58,7 @@ constexpr std::array<ImageDimensionInfo, 7> ImageDimensions {{
|
||||
const ImageDimensionInfo& ImageDimensionInfoFor(ImageDimension dimension);
|
||||
|
||||
struct SpirvRequirements {
|
||||
bool bvh = false;
|
||||
bool subgroup_ballot = false;
|
||||
bool subgroup_shuffle = false;
|
||||
bool subgroup_local_invocation_id = false;
|
||||
@@ -83,6 +84,20 @@ struct EmitterState {
|
||||
Builder builder;
|
||||
const IR::Program& program;
|
||||
ShaderStageInputInfo input_info;
|
||||
uint32_t void_type = 0;
|
||||
uint32_t bool_type = 0;
|
||||
uint32_t u32_type = 0;
|
||||
uint32_t native_u64_type = 0;
|
||||
uint32_t i32_type = 0;
|
||||
uint32_t f32_type = 0;
|
||||
uint32_t f64_type = 0;
|
||||
uint32_t u32_pair_type = 0;
|
||||
uint32_t i32_pair_type = 0;
|
||||
uint32_t function_type = 0;
|
||||
std::array<uint32_t, 3> bool_vector_types {};
|
||||
std::array<uint32_t, 3> u32_vector_types {};
|
||||
std::array<uint32_t, 3> i32_vector_types {};
|
||||
std::array<uint32_t, 3> f32_vector_types {};
|
||||
std::array<uint32_t, 6> tess_variables {};
|
||||
uint32_t tess_inner_variable = 0;
|
||||
uint32_t tess_patch_base = 0;
|
||||
@@ -96,6 +111,7 @@ struct EmitterState {
|
||||
uint32_t bda_pagetable_variable = 0;
|
||||
uint32_t fault_buffer_variable = 0;
|
||||
uint32_t bda_pointer_function = 0;
|
||||
uint32_t bvh_intersect_function = 0;
|
||||
uint32_t gds_variable = 0;
|
||||
uint32_t gds_length = 0;
|
||||
uint32_t push_constant_variable = 0;
|
||||
@@ -420,7 +436,12 @@ uint32_t EmitUFloatToF32Bits(EmitterState& state, uint32_t raw, uint32_t bits);
|
||||
uint32_t NormalizeFormatComponent(EmitterState& state, const Format::BufferFormatInfo& info,
|
||||
uint32_t component, uint32_t raw);
|
||||
|
||||
void EmitDeviceAtomicMemoryBarrier(EmitterState& state);
|
||||
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode);
|
||||
|
||||
uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t pointer,
|
||||
uint32_t scope);
|
||||
|
||||
void EmitAtomicMemoryBarrier(EmitterState& state, IR::ResourceKind kind);
|
||||
|
||||
uint32_t EmitFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t source,
|
||||
bool max_value);
|
||||
@@ -487,6 +508,10 @@ uint32_t EmitF16BitsToF32(EmitterState& state, uint32_t bits);
|
||||
void EmitProgram(EmitterState& state);
|
||||
|
||||
void DefineGetBdaPointer(EmitterState& state);
|
||||
void DefineBvhIntersect(EmitterState& state);
|
||||
uint32_t GetBdaPointer(EmitterState& state, uint32_t address);
|
||||
uint32_t ConstantDeviceAddress(EmitterState& state, uint64_t value);
|
||||
uint32_t DeviceAddressFromWords(EmitterState& state, uint32_t low, uint32_t high);
|
||||
|
||||
// These templates accept local lambdas from several emitter translation units.
|
||||
template <typename Fn>
|
||||
@@ -528,14 +553,7 @@ uint32_t EmitValueOrZeroIfCondition(EmitterState& state, uint32_t condition, Fn&
|
||||
|
||||
template <typename Fn>
|
||||
uint32_t AtomicUpdate(EmitterState& state, uint32_t pointer, IR::ResourceKind kind, Fn&& desired) {
|
||||
const auto scope = kind == IR::ResourceKind::Lds ? spv::ScopeWorkgroup : spv::ScopeDevice;
|
||||
const auto memory = [&] {
|
||||
switch (kind) {
|
||||
case IR::ResourceKind::Lds: return spv::MemorySemanticsWorkgroupMemoryMask;
|
||||
case IR::ResourceKind::Image: return spv::MemorySemanticsImageMemoryMask;
|
||||
default: return spv::MemorySemanticsUniformMemoryMask;
|
||||
}
|
||||
}();
|
||||
const auto scope = kind == IR::ResourceKind::Lds ? spv::ScopeWorkgroup : spv::ScopeDevice;
|
||||
const auto preheader = state.builder.AllocateId();
|
||||
const auto header = state.builder.AllocateId();
|
||||
const auto cont = state.builder.AllocateId();
|
||||
@@ -564,8 +582,7 @@ uint32_t AtomicUpdate(EmitterState& state, uint32_t pointer, IR::ResourceKind ki
|
||||
EmitLabel(state, cont);
|
||||
state.builder.AddFunction(spv::OpBranch, header);
|
||||
EmitLabel(state, merge);
|
||||
state.builder.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope),
|
||||
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | memory));
|
||||
EmitAtomicMemoryBarrier(state, kind);
|
||||
return observed;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h"
|
||||
|
||||
#include "graphics/host_gpu/renderer/cache/bufferCache.h"
|
||||
#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -105,31 +106,23 @@ uint32_t AddU64Low(EmitterState& state, uint32_t low, uint32_t high, uint32_t ad
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t ScratchByteAddress(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t low,
|
||||
uint32_t high) {
|
||||
auto& state = ctx.state;
|
||||
auto immediate = static_cast<int32_t>(mem.offset);
|
||||
uint32_t AddAddressOffset(EmitterState& state, const IR::MemoryInfo& mem, uint32_t low,
|
||||
uint32_t& high) {
|
||||
const auto immediate = static_cast<int32_t>(mem.offset);
|
||||
if (immediate == 0) return low;
|
||||
const auto immediate_low = ConstantU32(state, static_cast<uint32_t>(immediate));
|
||||
const auto immediate_high = ConstantU32(state, immediate < 0 ? UINT32_MAX : 0u);
|
||||
low = AddU64Low(state, low, high, immediate_low, immediate_high, high);
|
||||
return AddU64Low(state, low, high, immediate_low, immediate_high, high);
|
||||
}
|
||||
|
||||
uint32_t ScratchByteAddress(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t low,
|
||||
uint32_t high) {
|
||||
auto& state = ctx.state;
|
||||
low = AddAddressOffset(state, mem, low, high);
|
||||
const auto valid = Binary(state, spv::OpIEqual, TypeBool(state), high, ConstantU32(state, 0));
|
||||
return Select(state, TypeU32(state), valid, low, ConstantU32(state, UINT32_MAX));
|
||||
}
|
||||
|
||||
uint32_t ConstantDeviceAddress(EmitterState& state, uint64_t value) {
|
||||
return state.builder.Constant(spv::OpConstant, TypeScalarU64(state),
|
||||
static_cast<uint32_t>(value),
|
||||
static_cast<uint32_t>(value >> 32u));
|
||||
}
|
||||
|
||||
uint32_t DeviceAddressFromWords(EmitterState& state, uint32_t low, uint32_t high) {
|
||||
const auto low64 = Unary(state, spv::OpUConvert, TypeScalarU64(state), low);
|
||||
const auto high64 = Binary(state, spv::OpShiftLeftLogical, TypeScalarU64(state),
|
||||
Unary(state, spv::OpUConvert, TypeScalarU64(state), high),
|
||||
ConstantDeviceAddress(state, 32));
|
||||
return Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low64, high64);
|
||||
}
|
||||
|
||||
uint32_t GuestAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem) {
|
||||
auto& state = ctx.state;
|
||||
auto low = ctx.Arg(inst, 1);
|
||||
@@ -186,17 +179,9 @@ void RecordBdaFault(EmitterState& state, uint32_t page) {
|
||||
Binary(state, spv::OpBitwiseOr, TypeU32(state), value, bit));
|
||||
}
|
||||
|
||||
uint32_t GetBdaPointer(ValueEmitContext& ctx, uint32_t address) {
|
||||
auto& state = ctx.state;
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), result,
|
||||
state.bda_pointer_function, address);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t LoadBdaDword(ValueEmitContext& ctx, uint32_t address) {
|
||||
auto& state = ctx.state;
|
||||
const auto bda = GetBdaPointer(ctx, address);
|
||||
const auto bda = GetBdaPointer(state, address);
|
||||
const auto present =
|
||||
Binary(state, spv::OpINotEqual, TypeBool(state), bda, ConstantDeviceAddress(state, 0));
|
||||
return EmitValueOrZeroIfCondition(state, present, [&]() {
|
||||
@@ -511,6 +496,50 @@ void StoreWordInBounds(ValueEmitContext& ctx, const MemoryResourceAccess& resour
|
||||
resource.memory_access);
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
void ForEachLocalFlatAccess(ValueEmitContext& ctx, const IR::Inst& inst, Fn&& emit) {
|
||||
auto& state = ctx.state;
|
||||
auto mem = ctx.Memory(inst);
|
||||
auto high = ctx.Arg(inst, 2);
|
||||
const auto low = AddAddressOffset(state, mem, ctx.Arg(inst, 1), high);
|
||||
const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), low, ConstantU32(state, 2));
|
||||
const auto aligned = Binary(state, spv::OpIEqual, TypeBool(state), ConstantU32(state, 0),
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state), low, ConstantU32(state, 3)));
|
||||
for (const auto kind: {IR::ResourceKind::Scratch, IR::ResourceKind::Lds}) {
|
||||
mem.kind = kind;
|
||||
const auto resource = PrepareMemoryResourceAccess(state, mem);
|
||||
const auto aperture = kind == IR::ResourceKind::Scratch ? Decoder::PrivateApertureHigh
|
||||
: Decoder::SharedApertureHigh;
|
||||
const auto selected = Binary(state, spv::OpIEqual, TypeBool(state), high, ConstantU32(state, aperture));
|
||||
const auto valid = AndCondition(state, aligned,
|
||||
AndCondition(state, selected, EmitMemoryElementInBounds(state, resource, index)));
|
||||
emit(resource, index, valid);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t LoadLocalFlat(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
return EmitValueOrZeroIfCondition(ctx.state, ctx.Arg(inst, inst.NumArgs() - 1), [&]() {
|
||||
uint32_t result = 0;
|
||||
ForEachLocalFlatAccess(ctx, inst, [&](const auto& resource, uint32_t index, uint32_t valid) {
|
||||
const auto value = EmitValueOrZeroIfCondition(ctx.state, valid, [&]() {
|
||||
return LoadWordInBounds(ctx, resource, index);
|
||||
});
|
||||
result = result == 0 ? value : Binary(ctx.state, spv::OpBitwiseOr, TypeU32(ctx.state), result, value);
|
||||
});
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
void StoreLocalFlat(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
EmitIfCondition(ctx.state, ctx.Arg(inst, inst.NumArgs() - 1), [&]() {
|
||||
ForEachLocalFlatAccess(ctx, inst, [&](const auto& resource, uint32_t index, uint32_t valid) {
|
||||
EmitIfCondition(ctx.state, valid, [&]() {
|
||||
StoreWordInBounds(ctx, resource, index, ctx.Arg(inst, inst.NumArgs() - 2));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void StoreWordPrepared(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
|
||||
const MemoryResourceAccess& resource, uint32_t data) {
|
||||
const auto index = EmitMemoryElementIndex(ctx.state, resource, DwordIndex(ctx, inst, mem));
|
||||
@@ -526,55 +555,6 @@ void StoreWord(ValueEmitContext& ctx, const IR::Inst& inst, IR::MemoryInfo mem)
|
||||
});
|
||||
}
|
||||
|
||||
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
||||
switch (opcode) {
|
||||
case IR::ValueOpcode::BufferAtomicCmpSwap32: return spv::OpAtomicCompareExchange;
|
||||
case IR::ValueOpcode::BufferAtomicSwap32:
|
||||
case IR::ValueOpcode::BufferAtomicSwap64:
|
||||
case IR::ValueOpcode::SharedAtomicSwap32: return spv::OpAtomicExchange;
|
||||
case IR::ValueOpcode::BufferAtomicIAdd32:
|
||||
case IR::ValueOpcode::SharedAtomicIAdd32: return spv::OpAtomicIAdd;
|
||||
case IR::ValueOpcode::BufferAtomicISub32:
|
||||
case IR::ValueOpcode::SharedAtomicISub32: return spv::OpAtomicISub;
|
||||
case IR::ValueOpcode::BufferAtomicSMin32:
|
||||
case IR::ValueOpcode::SharedAtomicSMin32: return spv::OpAtomicSMin;
|
||||
case IR::ValueOpcode::BufferAtomicUMin32:
|
||||
case IR::ValueOpcode::SharedAtomicUMin32: return spv::OpAtomicUMin;
|
||||
case IR::ValueOpcode::BufferAtomicSMax32:
|
||||
case IR::ValueOpcode::SharedAtomicSMax32: return spv::OpAtomicSMax;
|
||||
case IR::ValueOpcode::BufferAtomicUMax32:
|
||||
case IR::ValueOpcode::SharedAtomicUMax32: return spv::OpAtomicUMax;
|
||||
case IR::ValueOpcode::BufferAtomicAnd32:
|
||||
case IR::ValueOpcode::SharedAtomicAnd32: return spv::OpAtomicAnd;
|
||||
case IR::ValueOpcode::BufferAtomicOr32:
|
||||
case IR::ValueOpcode::BufferAtomicOr64:
|
||||
case IR::ValueOpcode::SharedAtomicOr64:
|
||||
case IR::ValueOpcode::SharedAtomicOr32: return spv::OpAtomicOr;
|
||||
case IR::ValueOpcode::BufferAtomicXor32:
|
||||
case IR::ValueOpcode::SharedAtomicXor32: return spv::OpAtomicXor;
|
||||
default: return spv::OpNop;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t pointer,
|
||||
uint32_t scope) {
|
||||
const auto old = ctx.state.builder.AllocateId();
|
||||
if (inst.GetOpcode() == IR::ValueOpcode::BufferAtomicCmpSwap32) {
|
||||
const auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
|
||||
const auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
|
||||
ctx.state.builder.AddFunction(
|
||||
spv::OpAtomicCompareExchange, TypeU32(ctx.state), old, pointer,
|
||||
ConstantU32(ctx.state, scope), ConstantU32(ctx.state, spv::MemorySemanticsMaskNone),
|
||||
ConstantU32(ctx.state, spv::MemorySemanticsMaskNone), desired, comparator);
|
||||
} else {
|
||||
const auto value = ctx.Arg(inst, inst.NumArgs() - 2);
|
||||
ctx.state.builder.AddFunction(SpirvAtomicOpcode(inst.GetOpcode()), TypeU32(ctx.state), old,
|
||||
pointer, ConstantU32(ctx.state, scope),
|
||||
ConstantU32(ctx.state, spv::MemorySemanticsMaskNone), value);
|
||||
}
|
||||
return old;
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
uint32_t EmitAtomicAccess(ValueEmitContext& ctx, const IR::Inst& inst,
|
||||
const IR::MemoryInfo& mem, Fn&& operation) {
|
||||
@@ -961,6 +941,27 @@ void StoreWideShared(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t compo
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t ConstantDeviceAddress(EmitterState& state, uint64_t value) {
|
||||
return state.builder.Constant(spv::OpConstant, TypeScalarU64(state),
|
||||
static_cast<uint32_t>(value),
|
||||
static_cast<uint32_t>(value >> 32u));
|
||||
}
|
||||
|
||||
uint32_t DeviceAddressFromWords(EmitterState& state, uint32_t low, uint32_t high) {
|
||||
const auto low64 = Unary(state, spv::OpUConvert, TypeScalarU64(state), low);
|
||||
const auto high64 = Binary(state, spv::OpShiftLeftLogical, TypeScalarU64(state),
|
||||
Unary(state, spv::OpUConvert, TypeScalarU64(state), high),
|
||||
ConstantDeviceAddress(state, 32));
|
||||
return Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low64, high64);
|
||||
}
|
||||
|
||||
uint32_t GetBdaPointer(EmitterState& state, uint32_t address) {
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), result,
|
||||
state.bda_pointer_function, address);
|
||||
return result;
|
||||
}
|
||||
|
||||
void DefineGetBdaPointer(EmitterState& state) {
|
||||
if (!state.program.info.uses_dma) {
|
||||
return;
|
||||
@@ -1024,14 +1025,7 @@ uint32_t EmitAtomic32(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto scope =
|
||||
mem.kind == IR::ResourceKind::Lds ? spv::ScopeWorkgroup : spv::ScopeDevice;
|
||||
const auto old = EmitAtomicOperation(ctx, inst, pointer, scope);
|
||||
if (mem.kind == IR::ResourceKind::Lds) {
|
||||
const auto semantics =
|
||||
spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsWorkgroupMemoryMask;
|
||||
ctx.state.builder.AddFunction(spv::OpMemoryBarrier, ConstantU32(ctx.state, scope),
|
||||
ConstantU32(ctx.state, semantics));
|
||||
} else {
|
||||
EmitDeviceAtomicMemoryBarrier(ctx.state);
|
||||
}
|
||||
EmitAtomicMemoryBarrier(ctx.state, mem.kind);
|
||||
return old;
|
||||
});
|
||||
}
|
||||
@@ -1059,13 +1053,13 @@ uint32_t EmitBufferAtomic64(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
TypeStorageBufferU64ElementPointer(state)),
|
||||
ConstantU32(state, spv::ScopeDevice),
|
||||
ConstantU32(state, spv::MemorySemanticsMaskNone), value);
|
||||
EmitDeviceAtomicMemoryBarrier(state);
|
||||
EmitAtomicMemoryBarrier(state, mem.kind);
|
||||
return Unary(state, spv::OpBitcast, TypeU64(state), old);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void EmitSharedAtomicOr64(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
void EmitSharedAtomic64(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
auto& state = ctx.state;
|
||||
const auto& mem = ctx.Memory(inst);
|
||||
EnsureLdsStorage(state);
|
||||
@@ -1243,7 +1237,9 @@ void EmitLoadMemory(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto shared_components = IR::SharedComponentCount(op);
|
||||
const auto address_info = IR::AddressOpcodeInfoOf(op);
|
||||
uint32_t value;
|
||||
if (buffer_components > 1u)
|
||||
if (mem.kind == IR::ResourceKind::FlatLocal)
|
||||
value = LoadLocalFlat(ctx, inst);
|
||||
else if (buffer_components > 1u)
|
||||
value = LoadWideBuffer(ctx, inst, buffer_components);
|
||||
else if (shared_components > 1u)
|
||||
value = LoadWideShared(ctx, inst, shared_components);
|
||||
@@ -1270,7 +1266,9 @@ void EmitStoreMemory(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto buffer_components = IR::BufferComponentCount(op);
|
||||
const auto shared_components = IR::SharedComponentCount(op);
|
||||
const auto type = inst.Arg(inst.NumArgs() - 2).GetType();
|
||||
if (buffer_components > 1u)
|
||||
if (mem.kind == IR::ResourceKind::FlatLocal)
|
||||
StoreLocalFlat(ctx, inst);
|
||||
else if (buffer_components > 1u)
|
||||
StoreWideBuffer(ctx, inst, buffer_components);
|
||||
else if (shared_components > 1u)
|
||||
StoreWideShared(ctx, inst, shared_components);
|
||||
|
||||
@@ -306,11 +306,79 @@ uint32_t NormalizeFormatComponent(EmitterState& state, const Format::BufferForma
|
||||
}
|
||||
}
|
||||
|
||||
void EmitDeviceAtomicMemoryBarrier(EmitterState& state) {
|
||||
const auto semantics =
|
||||
spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsUniformMemoryMask;
|
||||
state.builder.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeDevice),
|
||||
ConstantU32(state, semantics));
|
||||
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
||||
switch (opcode) {
|
||||
case IR::ValueOpcode::ImageAtomicCompareSwap32:
|
||||
case IR::ValueOpcode::BufferAtomicCmpSwap32: return spv::OpAtomicCompareExchange;
|
||||
case IR::ValueOpcode::ImageAtomicSwap32:
|
||||
case IR::ValueOpcode::BufferAtomicSwap32:
|
||||
case IR::ValueOpcode::BufferAtomicSwap64:
|
||||
case IR::ValueOpcode::SharedAtomicSwap32: return spv::OpAtomicExchange;
|
||||
case IR::ValueOpcode::ImageAtomicIAdd32:
|
||||
case IR::ValueOpcode::BufferAtomicIAdd32:
|
||||
case IR::ValueOpcode::SharedAtomicIAdd64:
|
||||
case IR::ValueOpcode::SharedAtomicIAdd32: return spv::OpAtomicIAdd;
|
||||
case IR::ValueOpcode::BufferAtomicISub32:
|
||||
case IR::ValueOpcode::SharedAtomicISub32: return spv::OpAtomicISub;
|
||||
case IR::ValueOpcode::ImageAtomicSMin32:
|
||||
case IR::ValueOpcode::BufferAtomicSMin32:
|
||||
case IR::ValueOpcode::SharedAtomicSMin32: return spv::OpAtomicSMin;
|
||||
case IR::ValueOpcode::ImageAtomicUMin32:
|
||||
case IR::ValueOpcode::BufferAtomicUMin32:
|
||||
case IR::ValueOpcode::SharedAtomicUMin32: return spv::OpAtomicUMin;
|
||||
case IR::ValueOpcode::ImageAtomicSMax32:
|
||||
case IR::ValueOpcode::BufferAtomicSMax32:
|
||||
case IR::ValueOpcode::SharedAtomicSMax32: return spv::OpAtomicSMax;
|
||||
case IR::ValueOpcode::ImageAtomicUMax32:
|
||||
case IR::ValueOpcode::BufferAtomicUMax32:
|
||||
case IR::ValueOpcode::SharedAtomicUMax32: return spv::OpAtomicUMax;
|
||||
case IR::ValueOpcode::ImageAtomicAnd32:
|
||||
case IR::ValueOpcode::BufferAtomicAnd32:
|
||||
case IR::ValueOpcode::BufferAtomicAnd64:
|
||||
case IR::ValueOpcode::SharedAtomicAnd32: return spv::OpAtomicAnd;
|
||||
case IR::ValueOpcode::ImageAtomicOr32:
|
||||
case IR::ValueOpcode::BufferAtomicOr32:
|
||||
case IR::ValueOpcode::BufferAtomicOr64:
|
||||
case IR::ValueOpcode::SharedAtomicOr64:
|
||||
case IR::ValueOpcode::SharedAtomicOr32: return spv::OpAtomicOr;
|
||||
case IR::ValueOpcode::ImageAtomicXor32:
|
||||
case IR::ValueOpcode::BufferAtomicXor32:
|
||||
case IR::ValueOpcode::SharedAtomicXor32: return spv::OpAtomicXor;
|
||||
default: return spv::OpNop;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t pointer,
|
||||
uint32_t scope) {
|
||||
const auto opcode = SpirvAtomicOpcode(inst.GetOpcode());
|
||||
const auto old = ctx.state.builder.AllocateId();
|
||||
if (opcode == spv::OpAtomicCompareExchange) {
|
||||
const auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
|
||||
const auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
|
||||
ctx.state.builder.AddFunction(
|
||||
spv::OpAtomicCompareExchange, TypeU32(ctx.state), old, pointer,
|
||||
ConstantU32(ctx.state, scope), ConstantU32(ctx.state, spv::MemorySemanticsMaskNone),
|
||||
ConstantU32(ctx.state, spv::MemorySemanticsMaskNone), desired, comparator);
|
||||
} else {
|
||||
const auto value = ctx.Arg(inst, inst.NumArgs() - 2);
|
||||
ctx.state.builder.AddFunction(opcode, TypeU32(ctx.state), old, pointer,
|
||||
ConstantU32(ctx.state, scope),
|
||||
ConstantU32(ctx.state, spv::MemorySemanticsMaskNone), value);
|
||||
}
|
||||
return old;
|
||||
}
|
||||
|
||||
void EmitAtomicMemoryBarrier(EmitterState& state, IR::ResourceKind kind) {
|
||||
const auto scope = kind == IR::ResourceKind::Lds ? spv::ScopeWorkgroup : spv::ScopeDevice;
|
||||
const auto memory = [&] {
|
||||
switch (kind) {
|
||||
case IR::ResourceKind::Lds: return spv::MemorySemanticsWorkgroupMemoryMask;
|
||||
case IR::ResourceKind::Image: return spv::MemorySemanticsImageMemoryMask;
|
||||
default: return spv::MemorySemanticsUniformMemoryMask;
|
||||
}
|
||||
}();
|
||||
state.builder.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope),
|
||||
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | memory));
|
||||
}
|
||||
|
||||
uint32_t EmitFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t source,
|
||||
|
||||
@@ -6,19 +6,32 @@
|
||||
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
|
||||
|
||||
uint32_t TypeVoid(EmitterState& state) {
|
||||
return state.builder.Type(spv::OpTypeVoid);
|
||||
if (state.void_type == 0) {
|
||||
state.void_type = state.builder.Type(spv::OpTypeVoid);
|
||||
}
|
||||
return state.void_type;
|
||||
}
|
||||
|
||||
uint32_t TypeBool(EmitterState& state) {
|
||||
return state.builder.Type(spv::OpTypeBool);
|
||||
if (state.bool_type == 0) {
|
||||
state.bool_type = state.builder.Type(spv::OpTypeBool);
|
||||
}
|
||||
return state.bool_type;
|
||||
}
|
||||
|
||||
uint32_t TypeBoolVector(EmitterState& state, uint32_t components) {
|
||||
return state.builder.Type(spv::OpTypeVector, TypeBool(state), components);
|
||||
auto& type = state.bool_vector_types[components - 2u];
|
||||
if (type == 0) {
|
||||
type = state.builder.Type(spv::OpTypeVector, TypeBool(state), components);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
uint32_t TypeU32(EmitterState& state) {
|
||||
return state.builder.Type(spv::OpTypeInt, 32, 0);
|
||||
if (state.u32_type == 0) {
|
||||
state.u32_type = state.builder.Type(spv::OpTypeInt, 32, 0);
|
||||
}
|
||||
return state.u32_type;
|
||||
}
|
||||
|
||||
uint32_t TypeU64(EmitterState& state) {
|
||||
@@ -26,33 +39,55 @@ uint32_t TypeU64(EmitterState& state) {
|
||||
}
|
||||
|
||||
uint32_t TypeScalarU64(EmitterState& state) {
|
||||
return state.builder.Type(spv::OpTypeInt, 64, 0);
|
||||
if (state.native_u64_type == 0) {
|
||||
state.native_u64_type = state.builder.Type(spv::OpTypeInt, 64, 0);
|
||||
}
|
||||
return state.native_u64_type;
|
||||
}
|
||||
|
||||
uint32_t TypeU32Pair(EmitterState& state) {
|
||||
const auto element = TypeU32(state);
|
||||
return state.builder.Type(spv::OpTypeStruct, element, element);
|
||||
if (state.u32_pair_type == 0) {
|
||||
const auto element = TypeU32(state);
|
||||
state.u32_pair_type = state.builder.Type(spv::OpTypeStruct, element, element);
|
||||
}
|
||||
return state.u32_pair_type;
|
||||
}
|
||||
|
||||
uint32_t TypeI32(EmitterState& state) {
|
||||
return state.builder.Type(spv::OpTypeInt, 32, 1);
|
||||
if (state.i32_type == 0) {
|
||||
state.i32_type = state.builder.Type(spv::OpTypeInt, 32, 1);
|
||||
}
|
||||
return state.i32_type;
|
||||
}
|
||||
|
||||
uint32_t TypeI32Pair(EmitterState& state) {
|
||||
const auto element = TypeI32(state);
|
||||
return state.builder.Type(spv::OpTypeStruct, element, element);
|
||||
if (state.i32_pair_type == 0) {
|
||||
const auto element = TypeI32(state);
|
||||
state.i32_pair_type = state.builder.Type(spv::OpTypeStruct, element, element);
|
||||
}
|
||||
return state.i32_pair_type;
|
||||
}
|
||||
|
||||
uint32_t TypeF32(EmitterState& state) {
|
||||
return state.builder.Type(spv::OpTypeFloat, 32);
|
||||
if (state.f32_type == 0) {
|
||||
state.f32_type = state.builder.Type(spv::OpTypeFloat, 32);
|
||||
}
|
||||
return state.f32_type;
|
||||
}
|
||||
|
||||
uint32_t TypeF64(EmitterState& state) {
|
||||
return state.builder.Type(spv::OpTypeFloat, 64);
|
||||
if (state.f64_type == 0) {
|
||||
state.f64_type = state.builder.Type(spv::OpTypeFloat, 64);
|
||||
}
|
||||
return state.f64_type;
|
||||
}
|
||||
|
||||
uint32_t TypeU32Vector(EmitterState& state, uint32_t components) {
|
||||
return state.builder.Type(spv::OpTypeVector, TypeU32(state), components);
|
||||
auto& type = state.u32_vector_types[components - 2u];
|
||||
if (type == 0) {
|
||||
type = state.builder.Type(spv::OpTypeVector, TypeU32(state), components);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
uint32_t TypeU32Composite(EmitterState& state, uint32_t components) {
|
||||
@@ -61,11 +96,19 @@ uint32_t TypeU32Composite(EmitterState& state, uint32_t components) {
|
||||
}
|
||||
|
||||
uint32_t TypeI32Vector(EmitterState& state, uint32_t components) {
|
||||
return state.builder.Type(spv::OpTypeVector, TypeI32(state), components);
|
||||
auto& type = state.i32_vector_types[components - 2u];
|
||||
if (type == 0) {
|
||||
type = state.builder.Type(spv::OpTypeVector, TypeI32(state), components);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
uint32_t TypeF32Vector(EmitterState& state, uint32_t components) {
|
||||
return state.builder.Type(spv::OpTypeVector, TypeF32(state), components);
|
||||
auto& type = state.f32_vector_types[components - 2u];
|
||||
if (type == 0) {
|
||||
type = state.builder.Type(spv::OpTypeVector, TypeF32(state), components);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
uint32_t TypePointer(EmitterState& state, spv::StorageClass storage_class, uint32_t pointee) {
|
||||
@@ -73,7 +116,10 @@ uint32_t TypePointer(EmitterState& state, spv::StorageClass storage_class, uint3
|
||||
}
|
||||
|
||||
uint32_t TypeFunction(EmitterState& state) {
|
||||
return state.builder.Type(spv::OpTypeFunction, TypeVoid(state));
|
||||
if (state.function_type == 0) {
|
||||
state.function_type = state.builder.Type(spv::OpTypeFunction, TypeVoid(state));
|
||||
}
|
||||
return state.function_type;
|
||||
}
|
||||
|
||||
uint32_t StorageRuntimeArrayType(EmitterState& state) {
|
||||
|
||||
@@ -243,6 +243,12 @@ void Invoke(Return (*emit)(Context&, Args...), ValueEmitContext& ctx, const IR::
|
||||
}
|
||||
|
||||
void EmitDirectInstruction(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
if (ctx.half != 0 && (inst.GetOpcode() == IR::ValueOpcode::Ballot ||
|
||||
inst.GetOpcode() == IR::ValueOpcode::ReadFirstLane)) {
|
||||
// Both operations already combine both emulated halves into one whole-wave result.
|
||||
ctx.Define(inst, ctx.other_half->Result(inst));
|
||||
return;
|
||||
}
|
||||
switch (inst.GetOpcode()) {
|
||||
#define VALUE_OPCODE(name, ...) \
|
||||
case IR::ValueOpcode::name: return Invoke(Emit##name, ctx, inst);
|
||||
@@ -679,6 +685,7 @@ void EmitProgram(EmitterState& state) {
|
||||
}
|
||||
}
|
||||
DefineGetBdaPointer(state);
|
||||
DefineBvhIntersect(state);
|
||||
for (const auto* block: program.blocks) {
|
||||
if (std::ranges::any_of(*block, [](const IR::Inst& inst) {
|
||||
return inst.GetOpcode() == IR::ValueOpcode::SwizzleU32 ||
|
||||
|
||||
@@ -192,8 +192,11 @@ constexpr MimgGatherInfo MIMG_GATHER_OPCODE_LIST[] = {
|
||||
|
||||
constexpr Detail::OpcodeMap MIMG_ATOMIC_OPCODE_LIST[] = {
|
||||
{0x0fu, Opcode::IMAGE_ATOMIC_SWAP},
|
||||
{0x10u, Opcode::IMAGE_ATOMIC_CMPSWAP},
|
||||
{0x11u, Opcode::IMAGE_ATOMIC_ADD},
|
||||
{0x14u, Opcode::IMAGE_ATOMIC_SMIN},
|
||||
{0x15u, Opcode::IMAGE_ATOMIC_UMIN},
|
||||
{0x16u, Opcode::IMAGE_ATOMIC_SMAX},
|
||||
{0x17u, Opcode::IMAGE_ATOMIC_UMAX},
|
||||
{0x18u, Opcode::IMAGE_ATOMIC_AND},
|
||||
{0x19u, Opcode::IMAGE_ATOMIC_OR},
|
||||
@@ -225,6 +228,7 @@ Opcode DecodeMimgOpcode(uint32_t opcode, const MimgSampleInfo* sample, const Mim
|
||||
case 0x09u: return Opcode::IMAGE_STORE_MIP;
|
||||
case 0x0eu: return Opcode::IMAGE_GET_RESINFO;
|
||||
case 0x60u: return Opcode::IMAGE_GET_LOD;
|
||||
case 0xe6u: return Opcode::IMAGE_BVH_INTERSECT_RAY;
|
||||
default: return Opcode::UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
@@ -253,6 +257,7 @@ uint32_t DecodeMimgAddressComponents(uint32_t opcode, ImageDimension dimension,
|
||||
}
|
||||
|
||||
switch (opcode) {
|
||||
case 0xe6u: return 11u;
|
||||
case 0x0eu: return 1u;
|
||||
case 0x01u:
|
||||
case 0x09u: return ImageCoordComponents(dimension) + 1u;
|
||||
@@ -355,11 +360,20 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
SetUnsupported(inst, Family::MIMG, opcode,
|
||||
"MIMG image gather requires exactly one dmask bit");
|
||||
}
|
||||
if (inst.opcode == Opcode::IMAGE_ATOMIC_CMPSWAP && inst.dmask != 0x3u) {
|
||||
SetUnsupported(inst, Family::MIMG, opcode,
|
||||
"MIMG image compare-and-swap requires 32-bit DMASK 0x3");
|
||||
}
|
||||
const bool supports_d16 = sample != nullptr || gather != nullptr || opcode == 0x00u ||
|
||||
opcode == 0x01u || opcode == 0x08u || opcode == 0x09u;
|
||||
if (d16 && !supports_d16) {
|
||||
SetUnsupported(inst, Family::MIMG, opcode, "MIMG opcode does not support D16 data");
|
||||
}
|
||||
if (opcode == 0xe6u &&
|
||||
(a16 || !r128 || inst.dmask != 0xfu || (nsa_dwords != 0u && nsa_dwords != 3u))) {
|
||||
SetUnsupported(inst, Family::MIMG, opcode,
|
||||
"BVH intersection requires eleven full-float ray DWORDs and R128/dmask:0xf");
|
||||
}
|
||||
|
||||
DecodeVectorGpr(vdata, inst.dst);
|
||||
DecodeVectorGpr(vaddr, inst.src0);
|
||||
|
||||
@@ -61,6 +61,7 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
|
||||
{0x3fu, Opcode::BUFFER_ATOMIC_FMIN, 1, 32},
|
||||
{0x40u, Opcode::BUFFER_ATOMIC_FMAX, 1, 32},
|
||||
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
|
||||
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
|
||||
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
|
||||
};
|
||||
|
||||
@@ -106,7 +107,7 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
|
||||
{0x39u, Opcode::DS_READ_I8, 1, 8, true}, {0x3au, Opcode::DS_READ_U8, 1, 8},
|
||||
{0x3bu, Opcode::DS_READ_I16, 1, 16, true}, {0x3cu, Opcode::DS_READ_U16, 1, 16},
|
||||
{0x3du, Opcode::DS_CONSUME, 1, 32}, {0x3eu, Opcode::DS_APPEND, 1, 32},
|
||||
{0x4au, Opcode::DS_OR_B64, 2, 32},
|
||||
{0x40u, Opcode::DS_ADD_U64, 2, 32}, {0x4au, Opcode::DS_OR_B64, 2, 32},
|
||||
{0x4du, Opcode::DS_WRITE_B64, 2, 32}, {0x4eu, Opcode::DS_WRITE2_B64, 4, 32},
|
||||
{0x4fu, Opcode::DS_WRITE2ST64_B64, 4, 32}, {0x76u, Opcode::DS_READ_B64, 2, 32},
|
||||
{0x77u, Opcode::DS_READ2_B64, 4, 32}, {0x78u, Opcode::DS_READ2ST64_B64, 4, 32},
|
||||
@@ -168,6 +169,7 @@ bool IsDsWriteOpcode(Opcode opcode) {
|
||||
bool IsDsAtomicOpcode(Opcode opcode) {
|
||||
switch (opcode) {
|
||||
case Opcode::DS_ADD_U32:
|
||||
case Opcode::DS_ADD_U64:
|
||||
case Opcode::DS_ADD_RTN_U32:
|
||||
case Opcode::DS_SUB_U32:
|
||||
case Opcode::DS_SUB_RTN_U32:
|
||||
|
||||
@@ -46,6 +46,7 @@ constexpr OpcodeMap SOP1_OPCODE_LIST[] = {
|
||||
{0x09u, Opcode::S_WQM_B32},
|
||||
{0x0au, Opcode::S_WQM_B64},
|
||||
{0x0bu, Opcode::S_BREV_B32},
|
||||
{0x0cu, Opcode::S_BREV_B64},
|
||||
{0x0fu, Opcode::S_BCNT1_I32_B32},
|
||||
{0x10u, Opcode::S_BCNT1_I32_B64},
|
||||
{0x13u, Opcode::S_FF1_I32_B32},
|
||||
@@ -108,6 +109,7 @@ constexpr OpcodeMap SOPP_OPCODE_LIST[] = {
|
||||
{0x12u, Opcode::S_TRAP},
|
||||
{0x16u, Opcode::S_TTRACEDATA},
|
||||
{0x17u, Opcode::S_CBRANCH_CDBGSYS},
|
||||
{0x19u, Opcode::S_CBRANCH_CDBGSYS_OR_USER},
|
||||
{0x20u, Opcode::S_INST_PREFETCH},
|
||||
{0x23u, Opcode::S_WAITCNT_DEPCTR},
|
||||
};
|
||||
|
||||
@@ -184,8 +184,9 @@ std::string FormatExp(const Instruction& inst) {
|
||||
|
||||
bool IsConditionalBranch(Opcode opcode) {
|
||||
switch (opcode) {
|
||||
// DevKit NGG should be disabled.
|
||||
case Opcode::S_CBRANCH_CDBGSYS: return false;
|
||||
// Conditional shader debugging is disabled.
|
||||
case Opcode::S_CBRANCH_CDBGSYS:
|
||||
case Opcode::S_CBRANCH_CDBGSYS_OR_USER: return false;
|
||||
case Opcode::S_CBRANCH_SCC0:
|
||||
case Opcode::S_CBRANCH_SCC1:
|
||||
case Opcode::S_CBRANCH_VCCZ:
|
||||
@@ -253,6 +254,8 @@ void DecodeScalarSource(uint32_t code, uint32_t pc, Operand& operand) {
|
||||
case 125u: operand.kind = OperandKind::Null; return;
|
||||
case 126u: operand.kind = OperandKind::ExecLo; return;
|
||||
case 127u: operand.kind = OperandKind::ExecHi; return;
|
||||
case 235u: operand.kind = OperandKind::SharedBase; return;
|
||||
case 237u: operand.kind = OperandKind::PrivateBase; return;
|
||||
case 239u: operand.kind = OperandKind::PopsExitingWaveId; return;
|
||||
case 248u:
|
||||
operand.kind = OperandKind::FloatInlineConstant;
|
||||
@@ -403,7 +406,6 @@ void DecodeProgram(std::span<const uint32_t> code, Program& program) {
|
||||
program.instructions.clear();
|
||||
program.instructions.reserve(code.size());
|
||||
program.code = code;
|
||||
program.has_bvh = false;
|
||||
|
||||
std::vector<bool> branch_targets;
|
||||
for (uint32_t word_index = 0; word_index < code.size();) {
|
||||
@@ -412,10 +414,6 @@ void DecodeProgram(std::span<const uint32_t> code, Program& program) {
|
||||
|
||||
const auto& inst = program.instructions.back();
|
||||
word_index += inst.word_count;
|
||||
if (inst.family == Family::MIMG && (inst.opcode_id == 0xe6u || inst.opcode_id == 0xe7u)) {
|
||||
program.has_bvh = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsDirectBranch(inst.opcode)) {
|
||||
const auto target_index = inst.branch_target / sizeof(uint32_t);
|
||||
@@ -454,6 +452,8 @@ std::string OperandToString(const Operand& operand) {
|
||||
case OperandKind::Scc: text = "scc"; break;
|
||||
case OperandKind::M0: text = "m0"; break;
|
||||
case OperandKind::PopsExitingWaveId: text = "pops_exiting_wave_id"; break;
|
||||
case OperandKind::SharedBase: text = "shared_base"; break;
|
||||
case OperandKind::PrivateBase: text = "private_base"; break;
|
||||
case OperandKind::Null: text = "null"; break;
|
||||
default: text = "unknown"; break;
|
||||
}
|
||||
@@ -504,6 +504,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
OperandToString(inst.src0).c_str()));
|
||||
case Opcode::S_ABS_I32:
|
||||
case Opcode::S_BREV_B32:
|
||||
case Opcode::S_BREV_B64:
|
||||
case Opcode::S_BCNT1_I32_B32:
|
||||
case Opcode::S_FLBIT_I32_B32:
|
||||
case Opcode::S_FF1_I32_B32:
|
||||
@@ -560,6 +561,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::S_CBRANCH_EXECZ:
|
||||
case Opcode::S_CBRANCH_EXECNZ:
|
||||
case Opcode::S_CBRANCH_CDBGSYS:
|
||||
case Opcode::S_CBRANCH_CDBGSYS_OR_USER:
|
||||
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: {} 0x{:08x}", inst.pc,
|
||||
magic_enum::enum_name(inst.opcode),
|
||||
inst.branch_target));
|
||||
@@ -572,9 +574,12 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::IMAGE_SAMPLE:
|
||||
case Opcode::IMAGE_STORE:
|
||||
case Opcode::IMAGE_STORE_MIP:
|
||||
case Opcode::IMAGE_ATOMIC_CMPSWAP:
|
||||
case Opcode::IMAGE_ATOMIC_SWAP:
|
||||
case Opcode::IMAGE_ATOMIC_ADD:
|
||||
case Opcode::IMAGE_ATOMIC_SMIN:
|
||||
case Opcode::IMAGE_ATOMIC_UMIN:
|
||||
case Opcode::IMAGE_ATOMIC_SMAX:
|
||||
case Opcode::IMAGE_ATOMIC_UMAX:
|
||||
case Opcode::IMAGE_ATOMIC_AND:
|
||||
case Opcode::IMAGE_ATOMIC_OR:
|
||||
@@ -641,6 +646,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::BUFFER_ATOMIC_SMAX:
|
||||
case Opcode::BUFFER_ATOMIC_UMAX:
|
||||
case Opcode::BUFFER_ATOMIC_AND:
|
||||
case Opcode::BUFFER_ATOMIC_AND_X2:
|
||||
case Opcode::BUFFER_ATOMIC_OR:
|
||||
case Opcode::BUFFER_ATOMIC_OR_X2:
|
||||
case Opcode::BUFFER_ATOMIC_XOR:
|
||||
@@ -663,6 +669,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::FLAT_STORE_DWORDX3:
|
||||
case Opcode::FLAT_STORE_DWORDX4:
|
||||
case Opcode::DS_ADD_U32:
|
||||
case Opcode::DS_ADD_U64:
|
||||
case Opcode::DS_ADD_RTN_U32:
|
||||
case Opcode::DS_SUB_U32:
|
||||
case Opcode::DS_SUB_RTN_U32:
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::Decoder {
|
||||
|
||||
// PS5 flat stack addresses use these SH_MEM_BASES aperture tags in VA[63:32].
|
||||
constexpr uint32_t PrivateApertureHigh = 0x70000000u;
|
||||
constexpr uint32_t SharedApertureHigh = 0x80000000u;
|
||||
|
||||
enum class Family {
|
||||
Unknown,
|
||||
SOP1,
|
||||
@@ -43,6 +47,7 @@ enum class Opcode {
|
||||
S_ABS_I32,
|
||||
S_ABSDIFF_I32,
|
||||
S_BREV_B32,
|
||||
S_BREV_B64,
|
||||
S_BCNT1_I32_B32,
|
||||
S_BCNT1_I32_B64,
|
||||
S_FF1_I32_B32,
|
||||
@@ -240,6 +245,7 @@ enum class Opcode {
|
||||
V_MAD_U32_U24,
|
||||
V_MAD_U64_U32,
|
||||
V_FMA_F64,
|
||||
V_ADD_F64,
|
||||
V_MUL_F64,
|
||||
V_FMA_F32,
|
||||
V_FMA_F16,
|
||||
@@ -355,6 +361,10 @@ enum class Opcode {
|
||||
V_CMP_NEQ_F32,
|
||||
V_CMP_NLT_F32,
|
||||
V_CMP_TRU_F32,
|
||||
V_CMP_EQ_F64,
|
||||
V_CMP_LE_F64,
|
||||
V_CMPX_LE_F64,
|
||||
V_CMPX_GE_F64,
|
||||
V_CMPX_LT_F32,
|
||||
V_CMPX_EQ_F32,
|
||||
V_CMPX_LE_F32,
|
||||
@@ -384,12 +394,19 @@ enum class Opcode {
|
||||
V_CMP_GT_I16,
|
||||
V_CMP_NE_I16,
|
||||
V_CMP_GE_I16,
|
||||
V_CMPX_LT_I16,
|
||||
V_CMPX_EQ_I16,
|
||||
V_CMPX_LE_I16,
|
||||
V_CMPX_GT_I16,
|
||||
V_CMPX_NE_I16,
|
||||
V_CMPX_GE_I16,
|
||||
V_CMP_LT_F16,
|
||||
V_CMP_EQ_F16,
|
||||
V_CMP_LE_F16,
|
||||
V_CMP_GT_F16,
|
||||
V_CMP_LG_F16,
|
||||
V_CMP_GE_F16,
|
||||
V_CMP_NGE_F16,
|
||||
V_CMP_NGT_F16,
|
||||
V_CMP_NEQ_F16,
|
||||
V_CMP_NLT_F16,
|
||||
@@ -426,11 +443,16 @@ enum class Opcode {
|
||||
V_CMP_GE_U32,
|
||||
V_CMP_T_U32,
|
||||
V_CMP_EQ_I64,
|
||||
V_CMP_LT_I64,
|
||||
V_CMP_LE_I64,
|
||||
V_CMP_LT_U64,
|
||||
V_CMP_EQ_U64,
|
||||
V_CMP_LE_U64,
|
||||
V_CMP_GT_U64,
|
||||
V_CMP_NE_U64,
|
||||
V_CMP_GE_U64,
|
||||
V_CMPX_NE_I64,
|
||||
V_CMPX_LE_U64,
|
||||
V_CMPX_NE_U64,
|
||||
V_CMPX_LT_U32,
|
||||
V_CMPX_EQ_U32,
|
||||
@@ -490,6 +512,7 @@ enum class Opcode {
|
||||
BUFFER_ATOMIC_SMAX,
|
||||
BUFFER_ATOMIC_UMAX,
|
||||
BUFFER_ATOMIC_AND,
|
||||
BUFFER_ATOMIC_AND_X2,
|
||||
BUFFER_ATOMIC_OR,
|
||||
BUFFER_ATOMIC_OR_X2,
|
||||
BUFFER_ATOMIC_XOR,
|
||||
@@ -510,6 +533,7 @@ enum class Opcode {
|
||||
FLAT_STORE_DWORDX3,
|
||||
FLAT_STORE_DWORDX4,
|
||||
DS_ADD_U32,
|
||||
DS_ADD_U64,
|
||||
DS_ADD_RTN_U32,
|
||||
DS_SUB_U32,
|
||||
DS_SUB_RTN_U32,
|
||||
@@ -567,6 +591,7 @@ enum class Opcode {
|
||||
DS_WRITE_B128,
|
||||
DS_WRITE_ADDTID_B32,
|
||||
DS_READ_ADDTID_B32,
|
||||
IMAGE_BVH_INTERSECT_RAY,
|
||||
IMAGE_GET_RESINFO,
|
||||
IMAGE_GET_LOD,
|
||||
IMAGE_LOAD,
|
||||
@@ -574,8 +599,11 @@ enum class Opcode {
|
||||
IMAGE_STORE,
|
||||
IMAGE_STORE_MIP,
|
||||
IMAGE_ATOMIC_SWAP,
|
||||
IMAGE_ATOMIC_CMPSWAP,
|
||||
IMAGE_ATOMIC_ADD,
|
||||
IMAGE_ATOMIC_SMIN,
|
||||
IMAGE_ATOMIC_UMIN,
|
||||
IMAGE_ATOMIC_SMAX,
|
||||
IMAGE_ATOMIC_UMAX,
|
||||
IMAGE_ATOMIC_AND,
|
||||
IMAGE_ATOMIC_OR,
|
||||
@@ -607,6 +635,7 @@ enum class Opcode {
|
||||
S_CBRANCH_EXECZ,
|
||||
S_CBRANCH_EXECNZ,
|
||||
S_CBRANCH_CDBGSYS,
|
||||
S_CBRANCH_CDBGSYS_OR_USER,
|
||||
S_SENDMSG,
|
||||
S_SETREG_B32,
|
||||
S_SLEEP,
|
||||
@@ -633,6 +662,8 @@ enum class OperandKind {
|
||||
Scc,
|
||||
M0,
|
||||
PopsExitingWaveId,
|
||||
SharedBase,
|
||||
PrivateBase,
|
||||
Null,
|
||||
Vgpr,
|
||||
};
|
||||
@@ -744,7 +775,6 @@ struct Instruction {
|
||||
struct Program {
|
||||
std::span<const uint32_t> code;
|
||||
std::vector<Instruction> instructions;
|
||||
bool has_bvh = false;
|
||||
};
|
||||
|
||||
// Code spans are trusted to contain complete instructions, valid branch targets, and 32-bit PCs.
|
||||
|
||||
@@ -228,6 +228,8 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
|
||||
{0x19u, Opcode::V_CMPX_NGE_F32}, {0x1au, Opcode::V_CMPX_NLG_F32},
|
||||
{0x1bu, Opcode::V_CMPX_NGT_F32}, {0x1cu, Opcode::V_CMPX_NLE_F32},
|
||||
{0x1du, Opcode::V_CMPX_NEQ_F32}, {0x1eu, Opcode::V_CMPX_NLT_F32},
|
||||
{0x22u, Opcode::V_CMP_EQ_F64, false}, {0x23u, Opcode::V_CMP_LE_F64, false},
|
||||
{0x33u, Opcode::V_CMPX_LE_F64, false}, {0x36u, Opcode::V_CMPX_GE_F64, false},
|
||||
{0x80u, Opcode::V_CMP_F_I32}, {0x81u, Opcode::V_CMP_LT_I32},
|
||||
{0x82u, Opcode::V_CMP_EQ_I32}, {0x83u, Opcode::V_CMP_LE_I32},
|
||||
{0x84u, Opcode::V_CMP_GT_I32}, {0x85u, Opcode::V_CMP_NE_I32},
|
||||
@@ -239,6 +241,9 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
|
||||
{0x92u, Opcode::V_CMPX_EQ_I32}, {0x93u, Opcode::V_CMPX_LE_I32},
|
||||
{0x94u, Opcode::V_CMPX_GT_I32}, {0x95u, Opcode::V_CMPX_NE_I32},
|
||||
{0x96u, Opcode::V_CMPX_GE_I32}, {0x98u, Opcode::V_CMPX_CLASS_F32},
|
||||
{0x99u, Opcode::V_CMPX_LT_I16, false}, {0x9au, Opcode::V_CMPX_EQ_I16, false},
|
||||
{0x9bu, Opcode::V_CMPX_LE_I16, false}, {0x9cu, Opcode::V_CMPX_GT_I16, false},
|
||||
{0x9du, Opcode::V_CMPX_NE_I16, false}, {0x9eu, Opcode::V_CMPX_GE_I16, false},
|
||||
{0xa9u, Opcode::V_CMP_LT_U16}, {0xaau, Opcode::V_CMP_EQ_U16},
|
||||
{0xabu, Opcode::V_CMP_LE_U16}, {0xacu, Opcode::V_CMP_GT_U16},
|
||||
{0xadu, Opcode::V_CMP_NE_U16}, {0xaeu, Opcode::V_CMP_GE_U16},
|
||||
@@ -248,16 +253,19 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
|
||||
{0xc3u, Opcode::V_CMP_LE_U32}, {0xc4u, Opcode::V_CMP_GT_U32},
|
||||
{0xc5u, Opcode::V_CMP_NE_U32}, {0xc6u, Opcode::V_CMP_GE_U32},
|
||||
{0xc7u, Opcode::V_CMP_T_U32}, {0xa2u, Opcode::V_CMP_EQ_I64, false},
|
||||
{0xa1u, Opcode::V_CMP_LT_I64, false}, {0xa3u, Opcode::V_CMP_LE_I64, false},
|
||||
{0xb5u, Opcode::V_CMPX_NE_I64, false}, {0xd1u, Opcode::V_CMPX_LT_U32},
|
||||
{0xd2u, Opcode::V_CMPX_EQ_U32}, {0xd3u, Opcode::V_CMPX_LE_U32},
|
||||
{0xd4u, Opcode::V_CMPX_GT_U32}, {0xd5u, Opcode::V_CMPX_NE_U32},
|
||||
{0xd6u, Opcode::V_CMPX_GE_U32}, {0xe1u, Opcode::V_CMP_LT_U64, false},
|
||||
{0xe2u, Opcode::V_CMP_EQ_U64, false}, {0xe4u, Opcode::V_CMP_GT_U64, false},
|
||||
{0xe5u, Opcode::V_CMP_NE_U64, false},
|
||||
{0xe2u, Opcode::V_CMP_EQ_U64, false}, {0xe3u, Opcode::V_CMP_LE_U64, false},
|
||||
{0xe4u, Opcode::V_CMP_GT_U64, false}, {0xe5u, Opcode::V_CMP_NE_U64, false},
|
||||
{0xe6u, Opcode::V_CMP_GE_U64, false}, {0xf3u, Opcode::V_CMPX_LE_U64, false},
|
||||
{0xf5u, Opcode::V_CMPX_NE_U64, false}, {0xc9u, Opcode::V_CMP_LT_F16},
|
||||
{0xcau, Opcode::V_CMP_EQ_F16}, {0xcbu, Opcode::V_CMP_LE_F16},
|
||||
{0xccu, Opcode::V_CMP_GT_F16}, {0xcdu, Opcode::V_CMP_LG_F16},
|
||||
{0xceu, Opcode::V_CMP_GE_F16}, {0xebu, Opcode::V_CMP_NGT_F16},
|
||||
{0xceu, Opcode::V_CMP_GE_F16}, {0xe9u, Opcode::V_CMP_NGE_F16},
|
||||
{0xebu, Opcode::V_CMP_NGT_F16},
|
||||
{0xedu, Opcode::V_CMP_NEQ_F16}, {0xeeu, Opcode::V_CMP_NLT_F16},
|
||||
{0xd9u, Opcode::V_CMPX_LT_F16}, {0xdau, Opcode::V_CMPX_EQ_F16},
|
||||
{0xdbu, Opcode::V_CMPX_LE_F16}, {0xdcu, Opcode::V_CMPX_GT_F16},
|
||||
@@ -279,6 +287,7 @@ constexpr OpcodeMap VOP3_OPCODE_LIST[] = {
|
||||
{0x147u, Opcode::V_CUBEMA_F32},
|
||||
{0x14bu, Opcode::V_FMA_F32},
|
||||
{0x14cu, Opcode::V_FMA_F64},
|
||||
{0x164u, Opcode::V_ADD_F64},
|
||||
{0x165u, Opcode::V_MUL_F64},
|
||||
{0x148u, Opcode::V_BFE_U32},
|
||||
{0x149u, Opcode::V_BFE_I32},
|
||||
@@ -828,6 +837,10 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
|
||||
case Opcode::V_CMP_F_F32:
|
||||
case Opcode::V_CMP_LT_F32:
|
||||
case Opcode::V_CMP_EQ_F32:
|
||||
case Opcode::V_CMP_EQ_F64:
|
||||
case Opcode::V_CMP_LE_F64:
|
||||
case Opcode::V_CMPX_LE_F64:
|
||||
case Opcode::V_CMPX_GE_F64:
|
||||
case Opcode::V_CMP_LE_F32:
|
||||
case Opcode::V_CMP_GT_F32:
|
||||
case Opcode::V_CMP_LG_F32:
|
||||
@@ -860,6 +873,7 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
|
||||
case Opcode::V_CMP_GT_F16:
|
||||
case Opcode::V_CMP_LG_F16:
|
||||
case Opcode::V_CMP_GE_F16:
|
||||
case Opcode::V_CMP_NGE_F16:
|
||||
case Opcode::V_CMP_NGT_F16:
|
||||
case Opcode::V_CMP_NEQ_F16:
|
||||
case Opcode::V_CMP_NLT_F16:
|
||||
@@ -1138,7 +1152,9 @@ VopcSdwaFields DecodeVopcSdwaFields(uint32_t modifier) {
|
||||
}
|
||||
|
||||
bool SupportsVopcSdwa(Opcode opcode) {
|
||||
return opcode != Opcode::UNSUPPORTED;
|
||||
return opcode != Opcode::UNSUPPORTED && opcode != Opcode::V_CMP_EQ_F64 &&
|
||||
opcode != Opcode::V_CMP_LE_F64 && opcode != Opcode::V_CMPX_LE_F64 &&
|
||||
opcode != Opcode::V_CMPX_GE_F64;
|
||||
}
|
||||
|
||||
void DecodeVopcSdwa(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
|
||||
@@ -1203,6 +1219,7 @@ void DecodeVopcDpp(uint32_t pc, std::span<const uint32_t> code, uint32_t word_in
|
||||
|
||||
uint32_t NativeVop3SourceCount(Opcode opcode) {
|
||||
switch (opcode) {
|
||||
case Opcode::V_ADD_F64:
|
||||
case Opcode::V_MUL_F64:
|
||||
case Opcode::V_MUL_LO_U32:
|
||||
case Opcode::V_MUL_HI_U32:
|
||||
@@ -1354,6 +1371,7 @@ bool SupportsNativeVop3SourceModifiers(Opcode opcode) {
|
||||
case Opcode::V_MAX_F32:
|
||||
case Opcode::V_MAC_F32:
|
||||
case Opcode::V_MAD_F32:
|
||||
case Opcode::V_ADD_F64:
|
||||
case Opcode::V_MUL_F64:
|
||||
case Opcode::V_FMA_F64:
|
||||
case Opcode::V_FMA_F32:
|
||||
@@ -1459,6 +1477,8 @@ void ApplyNativeVop3SourceModifiers(Instruction& inst, uint32_t abs, uint32_t ne
|
||||
|
||||
bool IsVopcCompareExec(Opcode opcode) {
|
||||
switch (opcode) {
|
||||
case Opcode::V_CMPX_LE_F64:
|
||||
case Opcode::V_CMPX_GE_F64:
|
||||
case Opcode::V_CMPX_LT_F32:
|
||||
case Opcode::V_CMPX_EQ_F32:
|
||||
case Opcode::V_CMPX_LE_F32:
|
||||
@@ -1486,7 +1506,14 @@ bool IsVopcCompareExec(Opcode opcode) {
|
||||
case Opcode::V_CMPX_NE_U32:
|
||||
case Opcode::V_CMPX_GE_U32:
|
||||
case Opcode::V_CMPX_NE_I64:
|
||||
case Opcode::V_CMPX_LE_U64:
|
||||
case Opcode::V_CMPX_NE_U64:
|
||||
case Opcode::V_CMPX_LT_I16:
|
||||
case Opcode::V_CMPX_EQ_I16:
|
||||
case Opcode::V_CMPX_LE_I16:
|
||||
case Opcode::V_CMPX_GT_I16:
|
||||
case Opcode::V_CMPX_NE_I16:
|
||||
case Opcode::V_CMPX_GE_I16:
|
||||
case Opcode::V_CMPX_LT_U16:
|
||||
case Opcode::V_CMPX_EQ_U16:
|
||||
case Opcode::V_CMPX_GT_U16:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "graphics/shader/recompiler/frontend/translate/Translator.h"
|
||||
|
||||
#include <bit>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::Frontend {
|
||||
|
||||
void Translator::EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar,
|
||||
@@ -26,9 +28,20 @@ void Translator::EmitCompareConstant(const Decoder::Instruction& inst, bool valu
|
||||
|
||||
void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
|
||||
IR::Type type, bool scalar, bool cmpx) {
|
||||
const auto lhs = ReadOperand(inst.src0, type);
|
||||
const auto rhs = ReadOperand(inst.src1, type);
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), scalar, cmpx);
|
||||
const bool signed_64 = opcode == IR::ValueOpcode::SLessThan64 ||
|
||||
opcode == IR::ValueOpcode::SLessThanEqual64 ||
|
||||
inst.opcode == Decoder::Opcode::V_CMP_EQ_I64 ||
|
||||
inst.opcode == Decoder::Opcode::V_CMPX_NE_I64;
|
||||
const auto read = [&](const Decoder::Operand& operand) {
|
||||
// RDNA2 expands signed 64-bit integer literals by sign extension.
|
||||
if (signed_64 && operand.kind == Decoder::OperandKind::LiteralConstant) {
|
||||
return IR::Value(static_cast<uint64_t>(
|
||||
static_cast<int64_t>(std::bit_cast<int32_t>(operand.value))));
|
||||
}
|
||||
return ReadOperand(operand, type);
|
||||
};
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {read(inst.src0), read(inst.src1)})), scalar,
|
||||
cmpx);
|
||||
}
|
||||
|
||||
void Translator::EmitInteger16Compare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
|
||||
@@ -40,11 +53,13 @@ void Translator::EmitInteger16Compare(const Decoder::Instruction& inst, IR::Valu
|
||||
|
||||
void Translator::EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
|
||||
bool half, bool cmpx) {
|
||||
const auto type = IR::ArgTypeOf(opcode, 0);
|
||||
const auto lhs =
|
||||
half ? IR::Value(ReadF16AsF32(inst.src0)) : ReadOperand(inst.src0, IR::Type::F32);
|
||||
half ? IR::Value(ReadF16AsF32(inst.src0)) : ReadOperand(inst.src0, type);
|
||||
const auto rhs =
|
||||
half ? IR::Value(ReadF16AsF32(inst.src1)) : ReadOperand(inst.src1, IR::Type::F32);
|
||||
const IR::FPCompareFlags flags{.flush_input_denorms = !half && flush_f32_inputs};
|
||||
half ? IR::Value(ReadF16AsF32(inst.src1)) : ReadOperand(inst.src1, type);
|
||||
const IR::FPCompareFlags flags{
|
||||
.flush_input_denorms = !half && type == IR::Type::F32 && flush_f32_inputs};
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs}, flags)), false, cmpx);
|
||||
}
|
||||
|
||||
|
||||
@@ -558,9 +558,18 @@ void Translator::IMAGE_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode
|
||||
const auto memory = MemoryInfoFromDecoded(inst);
|
||||
const auto resource = GetImageResource(memory);
|
||||
const auto address = MakeImageAddress(inst, MemorySourceAt(inst, 1));
|
||||
const auto result =
|
||||
ir.Emit(opcode, {resource, address, ReadU32(MemorySourceAt(inst, 0)), ir.GetExec()},
|
||||
AddMemoryInfo(memory, inst.pc));
|
||||
const auto data = MemorySourceAt(inst, 0);
|
||||
const auto flags = AddMemoryInfo(memory, inst.pc);
|
||||
IR::Value result;
|
||||
if (opcode == IR::ValueOpcode::ImageAtomicCompareSwap32) {
|
||||
// VDATA supplies the replacement; VDATA+1 supplies the comparison.
|
||||
result = ir.Emit(opcode,
|
||||
{resource, address, ReadU32(data), ReadU32(OffsetOperand(data, 1u)),
|
||||
ir.GetExec()},
|
||||
flags);
|
||||
} else {
|
||||
result = ir.Emit(opcode, {resource, address, ReadU32(data), ir.GetExec()}, flags);
|
||||
}
|
||||
if (inst.glc) {
|
||||
WriteOperand(inst.dst, result);
|
||||
}
|
||||
@@ -883,6 +892,15 @@ void Translator::DS_BPERMUTE_B32(const Decoder::Instruction& inst) {
|
||||
|
||||
void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
switch (inst.opcode) {
|
||||
case Decoder::Opcode::IMAGE_BVH_INTERSECT_RAY: {
|
||||
const auto result = ir.Emit(IR::ValueOpcode::BvhIntersect,
|
||||
{ConstructU32x4(inst.src1, 4), MakeImageAddress(inst, inst.src0), ir.GetExec()});
|
||||
for (uint32_t component = 0; component < 4; ++component) {
|
||||
WriteOperand(OffsetOperand(inst.dst, component),
|
||||
ir.Emit(IR::ValueOpcode::CompositeExtractU32x4, {result, IR::Value(component)}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case Decoder::Opcode::S_LOAD_DWORD:
|
||||
case Decoder::Opcode::S_LOAD_DWORDX2:
|
||||
case Decoder::Opcode::S_LOAD_DWORDX4:
|
||||
@@ -957,6 +975,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicUMax32);
|
||||
case Decoder::Opcode::BUFFER_ATOMIC_AND:
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicAnd32);
|
||||
case Decoder::Opcode::BUFFER_ATOMIC_AND_X2:
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicAnd64);
|
||||
case Decoder::Opcode::BUFFER_ATOMIC_OR:
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicOr32);
|
||||
case Decoder::Opcode::BUFFER_ATOMIC_OR_X2:
|
||||
@@ -970,6 +990,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
|
||||
case Decoder::Opcode::DS_ADD_U32:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicIAdd32, false);
|
||||
case Decoder::Opcode::DS_ADD_U64:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicIAdd64, false);
|
||||
case Decoder::Opcode::DS_ADD_RTN_U32:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicIAdd32, true);
|
||||
case Decoder::Opcode::DS_SUB_U32:
|
||||
@@ -1017,12 +1039,18 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
case Decoder::Opcode::DS_WRXCHG_RTN_B32:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicSwap32, true);
|
||||
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_CMPSWAP:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicCompareSwap32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_SWAP:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicSwap32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_ADD:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicIAdd32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_SMIN:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicSMin32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_UMIN:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicUMin32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_SMAX:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicSMax32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_UMAX:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicUMax32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_AND:
|
||||
|
||||
@@ -155,6 +155,12 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
|
||||
case O::S_BREV_B32:
|
||||
return SimpleInteger(inst, IR::ValueOpcode::BitReverse32, IR::Type::U32, false, false,
|
||||
false);
|
||||
case O::S_BREV_B64: {
|
||||
const auto value = ReadU32Pair(inst.src0);
|
||||
WriteU32Pair(inst.dst, {IR::U32(ir.Emit(IR::ValueOpcode::BitReverse32, {value[1]})),
|
||||
IR::U32(ir.Emit(IR::ValueOpcode::BitReverse32, {value[0]}))});
|
||||
return;
|
||||
}
|
||||
case O::S_BCNT1_I32_B32:
|
||||
return SimpleInteger(inst, IR::ValueOpcode::BitCount32, IR::Type::U32, false, false,
|
||||
true);
|
||||
@@ -231,6 +237,7 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
|
||||
case O::S_CBRANCH_EXECZ:
|
||||
case O::S_CBRANCH_EXECNZ:
|
||||
case O::S_CBRANCH_CDBGSYS:
|
||||
case O::S_CBRANCH_CDBGSYS_OR_USER:
|
||||
case O::S_ENDPGM: return;
|
||||
default: return FailMissingTranslation(inst);
|
||||
}
|
||||
|
||||
@@ -135,6 +135,8 @@ IR::U32 Translator::ReadRawU32(const Decoder::Operand& operand) {
|
||||
case Decoder::OperandKind::IntegerInlineConstant:
|
||||
case Decoder::OperandKind::FloatInlineConstant: return IR::U32(IR::Value(operand.value));
|
||||
case Decoder::OperandKind::Null:
|
||||
case Decoder::OperandKind::SharedBase:
|
||||
case Decoder::OperandKind::PrivateBase:
|
||||
case Decoder::OperandKind::PopsExitingWaveId: return IR::U32(IR::Value(0u));
|
||||
case Decoder::OperandKind::Sgpr:
|
||||
return ir.GetScalarReg(static_cast<IR::ScalarReg>(operand.reg));
|
||||
@@ -474,6 +476,12 @@ IR::U32 Translator::ReadU32(const Decoder::Operand& operand) {
|
||||
}
|
||||
|
||||
std::array<IR::U32, 2> Translator::ReadU32Pair(const Decoder::Operand& operand) {
|
||||
if (operand.kind == Decoder::OperandKind::PrivateBase ||
|
||||
operand.kind == Decoder::OperandKind::SharedBase) {
|
||||
return {IR::U32(IR::Value(0u)), IR::U32(IR::Value(
|
||||
operand.kind == Decoder::OperandKind::PrivateBase ? Decoder::PrivateApertureHigh
|
||||
: Decoder::SharedApertureHigh))};
|
||||
}
|
||||
if (operand.kind == Decoder::OperandKind::ExecLo) {
|
||||
return {ir.GetExecLo(), ir.GetExecHi()};
|
||||
}
|
||||
@@ -560,6 +568,12 @@ IR::U32 Translator::ReadU16LaneRaw(const Decoder::Operand& operand, bool high_la
|
||||
} else if (operand.sdwa_sel == 4u || operand.sdwa_sel == 5u) {
|
||||
offset = operand.sdwa_sel == 5u ? 16u : 0u;
|
||||
}
|
||||
if (width == 8u && operand.sdwa_sext) {
|
||||
// Extend the selected byte into the 16-bit operand before halfword modifiers.
|
||||
const auto value = IR::U32(
|
||||
ir.Emit(IR::ValueOpcode::BitFieldSExtract, {bits, IR::Value(offset), IR::Value(width)}));
|
||||
return ir.BitwiseAnd(value, IR::U32(IR::Value(0xffffu)));
|
||||
}
|
||||
return IR::U32(
|
||||
ir.Emit(IR::ValueOpcode::BitFieldUExtract, {bits, IR::Value(offset), IR::Value(width)}));
|
||||
}
|
||||
@@ -567,7 +581,7 @@ IR::U32 Translator::ReadU16LaneRaw(const Decoder::Operand& operand, bool high_la
|
||||
IR::U32 Translator::ReadU16LaneAsU32(const Decoder::Operand& operand, bool high_lane,
|
||||
bool sign_extend) {
|
||||
auto value = Read16LaneBits(operand, high_lane);
|
||||
if (sign_extend || operand.sdwa_sext) {
|
||||
if (sign_extend) {
|
||||
value = IR::U32(
|
||||
ir.Emit(IR::ValueOpcode::BitFieldSExtract, {value, IR::Value(0u), IR::Value(16u)}));
|
||||
}
|
||||
@@ -880,13 +894,35 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
|
||||
case Decoder::Opcode::V_CVT_F64_F32:
|
||||
case Decoder::Opcode::V_CVT_F64_U32: include_vector(inst.dst, 2u); break;
|
||||
case Decoder::Opcode::V_FMA_F64: include_vector(inst.src2, 2u); [[fallthrough]];
|
||||
case Decoder::Opcode::V_ADD_F64:
|
||||
case Decoder::Opcode::V_MUL_F64: include_vector(inst.src1, 2u); [[fallthrough]];
|
||||
case Decoder::Opcode::V_RCP_F64: include_vector(inst.dst, 2u); [[fallthrough]];
|
||||
case Decoder::Opcode::V_CVT_F32_F64: include_vector(inst.src0, 2u); break;
|
||||
case Decoder::Opcode::V_CMP_EQ_F64:
|
||||
case Decoder::Opcode::V_CMP_LE_F64:
|
||||
case Decoder::Opcode::V_CMPX_LE_F64:
|
||||
case Decoder::Opcode::V_CMPX_GE_F64:
|
||||
case Decoder::Opcode::V_CMP_EQ_I64:
|
||||
case Decoder::Opcode::V_CMP_LT_I64:
|
||||
case Decoder::Opcode::V_CMP_LE_I64:
|
||||
case Decoder::Opcode::V_CMP_LT_U64:
|
||||
case Decoder::Opcode::V_CMP_EQ_U64:
|
||||
case Decoder::Opcode::V_CMP_LE_U64:
|
||||
case Decoder::Opcode::V_CMP_GT_U64:
|
||||
case Decoder::Opcode::V_CMP_NE_U64:
|
||||
case Decoder::Opcode::V_CMP_GE_U64:
|
||||
case Decoder::Opcode::V_CMPX_NE_I64:
|
||||
case Decoder::Opcode::V_CMPX_LE_U64:
|
||||
case Decoder::Opcode::V_CMPX_NE_U64:
|
||||
include_vector(inst.src0, 2u);
|
||||
include_vector(inst.src1, 2u);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
if (inst.family == Decoder::Family::DS) {
|
||||
switch (inst.opcode) {
|
||||
case Decoder::Opcode::DS_ADD_U64:
|
||||
case Decoder::Opcode::DS_OR_B64:
|
||||
case Decoder::Opcode::DS_WRITE_B64:
|
||||
case Decoder::Opcode::DS_WRITE_B96:
|
||||
case Decoder::Opcode::DS_WRITE_B128: include_vector(inst.src1, inst.data_dwords); break;
|
||||
@@ -1120,12 +1156,13 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
||||
return entry_ir.ISub(lhs, minimum(lhs, rhs));
|
||||
};
|
||||
const auto local = builtin(IR::StageInputKind::LocalInvocationIndex);
|
||||
const auto primitive_chunk = entry_ir.IMul(builtin(IR::StageInputKind::WorkgroupId, 0),
|
||||
u32(mesh.primitives_per_group));
|
||||
const auto group = builtin(IR::StageInputKind::WorkgroupId, 0);
|
||||
const auto primitive_chunk = mesh.fast_launch ? group :
|
||||
entry_ir.IMul(group, u32(mesh.primitives_per_group));
|
||||
const auto step = u32(mesh.InputPrimitiveStep());
|
||||
const auto size = u32(mesh.InputPrimitiveSize());
|
||||
const auto chunk = entry_ir.IMul(primitive_chunk, step);
|
||||
const auto vertices =
|
||||
const auto chunk = mesh.fast_launch ? group : entry_ir.IMul(primitive_chunk, step);
|
||||
const auto vertices = mesh.fast_launch ? u32(mesh.vertices_per_group) :
|
||||
minimum(subtract_saturate(draw(0), chunk), u32(mesh.vertices_per_group));
|
||||
const auto primitives = entry_ir.Select(
|
||||
entry_ir.ULessThan(vertices, size), u32(0),
|
||||
@@ -1146,56 +1183,64 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
||||
entry_ir.BitwiseOr(wave_info, entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(
|
||||
primitive_count, u32(8)),
|
||||
vertex_count)));
|
||||
// GS adjacency addresses local ES records in LDS. Fans retain the draw's
|
||||
// center in every subgroup; strip winding follows the global primitive.
|
||||
const auto vertex = entry_ir.IMul(local, step);
|
||||
auto first = vertex;
|
||||
auto second = u32(0);
|
||||
auto third = u32(0);
|
||||
if (mesh.InputPrimitiveSize() >= 2u) {
|
||||
second = entry_ir.IAdd(vertex, u32(1));
|
||||
if (mesh.fast_launch) {
|
||||
// Fast launch broadcasts the subgroup's base vertex and instance to every lane.
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5), entry_ir.IAdd(draw(1), group));
|
||||
entry_ir.SetVectorReg(
|
||||
static_cast<IR::VectorReg>(6),
|
||||
entry_ir.IAdd(draw(2), builtin(IR::StageInputKind::WorkgroupId, 1)));
|
||||
} else {
|
||||
// GS adjacency addresses local ES records in LDS. Fans retain the draw's
|
||||
// center in every subgroup; strip winding follows the global primitive.
|
||||
const auto vertex = entry_ir.IMul(local, step);
|
||||
auto first = vertex;
|
||||
auto second = u32(0);
|
||||
auto third = u32(0);
|
||||
if (mesh.InputPrimitiveSize() >= 2u) {
|
||||
second = entry_ir.IAdd(vertex, u32(1));
|
||||
}
|
||||
if (mesh.InputPrimitiveSize() == 3u) {
|
||||
third = entry_ir.IAdd(vertex, u32(2));
|
||||
}
|
||||
auto input_vertex = entry_ir.IAdd(chunk, local);
|
||||
if (mesh.input_primitive == static_cast<uint32_t>(Prospero::PrimitiveType::kTriFan)) {
|
||||
first = u32(0);
|
||||
input_vertex = entry_ir.Select(entry_ir.IEqual(local, u32(0)), u32(0), input_vertex);
|
||||
} else if (mesh.input_primitive == static_cast<uint32_t>(Prospero::PrimitiveType::kTriStrip)) {
|
||||
const auto parity = entry_ir.BitwiseAnd(entry_ir.IAdd(primitive_chunk, local), u32(1));
|
||||
first = entry_ir.IAdd(first, parity);
|
||||
second = entry_ir.ISub(second, parity);
|
||||
}
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(0),
|
||||
entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(first, u32(2)),
|
||||
entry_ir.ShiftLeftLogical(second, u32(18))));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(1),
|
||||
entry_ir.ShiftLeftLogical(third, u32(2)));
|
||||
const auto index_bytes = draw(3);
|
||||
const auto indexed = entry_ir.INotEqual(index_bytes, u32(0));
|
||||
const auto index_low = draw(4);
|
||||
const auto byte_offset = entry_ir.IAdd(entry_ir.BitwiseAnd(index_low, u32(3)),
|
||||
entry_ir.IMul(input_vertex, index_bytes));
|
||||
const auto index_resource = entry_ir.Emit(
|
||||
IR::ValueOpcode::GetAddressResource,
|
||||
{entry_ir.BitwiseAnd(index_low, u32(~3u)), draw(5)});
|
||||
const auto memory_index = static_cast<uint32_t>(result.memory_info.size());
|
||||
result.memory_info.push_back({.kind = IR::ResourceKind::Global});
|
||||
const auto packed_index = entry_ir.Emit(
|
||||
IR::ValueOpcode::LoadAddressU32,
|
||||
{index_resource, entry_ir.BitwiseAnd(byte_offset, u32(~3u)), u32(0),
|
||||
entry_ir.LogicalAnd(indexed, entry_ir.ULessThan(local, vertices))},
|
||||
IR::MemoryFlags {.index = memory_index});
|
||||
const auto index = IR::U32(entry_ir.Emit(
|
||||
IR::ValueOpcode::BitFieldUExtract,
|
||||
{packed_index, entry_ir.IMul(entry_ir.BitwiseAnd(byte_offset, u32(3)), u32(8)),
|
||||
entry_ir.IMul(index_bytes, u32(8))}));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
|
||||
entry_ir.IAdd(draw(1), entry_ir.Select(indexed, index, input_vertex)));
|
||||
entry_ir.SetVectorReg(
|
||||
static_cast<IR::VectorReg>(8),
|
||||
entry_ir.IAdd(draw(2), builtin(IR::StageInputKind::WorkgroupId, 1)));
|
||||
}
|
||||
if (mesh.InputPrimitiveSize() == 3u) {
|
||||
third = entry_ir.IAdd(vertex, u32(2));
|
||||
}
|
||||
auto input_vertex = entry_ir.IAdd(chunk, local);
|
||||
if (mesh.input_primitive == static_cast<uint32_t>(Prospero::PrimitiveType::kTriFan)) {
|
||||
first = u32(0);
|
||||
input_vertex = entry_ir.Select(entry_ir.IEqual(local, u32(0)), u32(0), input_vertex);
|
||||
} else if (mesh.input_primitive == static_cast<uint32_t>(Prospero::PrimitiveType::kTriStrip)) {
|
||||
const auto parity = entry_ir.BitwiseAnd(entry_ir.IAdd(primitive_chunk, local), u32(1));
|
||||
first = entry_ir.IAdd(first, parity);
|
||||
second = entry_ir.ISub(second, parity);
|
||||
}
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(0),
|
||||
entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(first, u32(2)),
|
||||
entry_ir.ShiftLeftLogical(second, u32(18))));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(1),
|
||||
entry_ir.ShiftLeftLogical(third, u32(2)));
|
||||
const auto index_bytes = draw(3);
|
||||
const auto indexed = entry_ir.INotEqual(index_bytes, u32(0));
|
||||
const auto index_low = draw(4);
|
||||
const auto byte_offset = entry_ir.IAdd(entry_ir.BitwiseAnd(index_low, u32(3)),
|
||||
entry_ir.IMul(input_vertex, index_bytes));
|
||||
const auto index_resource = entry_ir.Emit(
|
||||
IR::ValueOpcode::GetAddressResource,
|
||||
{entry_ir.BitwiseAnd(index_low, u32(~3u)), draw(5)});
|
||||
const auto memory_index = static_cast<uint32_t>(result.memory_info.size());
|
||||
result.memory_info.push_back({.kind = IR::ResourceKind::Global});
|
||||
const auto packed_index = entry_ir.Emit(
|
||||
IR::ValueOpcode::LoadAddressU32,
|
||||
{index_resource, entry_ir.BitwiseAnd(byte_offset, u32(~3u)), u32(0),
|
||||
entry_ir.LogicalAnd(indexed, entry_ir.ULessThan(local, vertices))},
|
||||
IR::MemoryFlags {.index = memory_index});
|
||||
const auto index = IR::U32(entry_ir.Emit(
|
||||
IR::ValueOpcode::BitFieldUExtract,
|
||||
{packed_index, entry_ir.IMul(entry_ir.BitwiseAnd(byte_offset, u32(3)), u32(8)),
|
||||
entry_ir.IMul(index_bytes, u32(8))}));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
|
||||
entry_ir.IAdd(draw(1), entry_ir.Select(indexed, index, input_vertex)));
|
||||
entry_ir.SetVectorReg(
|
||||
static_cast<IR::VectorReg>(8),
|
||||
entry_ir.IAdd(draw(2), builtin(IR::StageInputKind::WorkgroupId, 1)));
|
||||
} else if (options.stage == ShaderType::Local) {
|
||||
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3), IR::U32(IR::Value(64u)));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(2),
|
||||
|
||||
@@ -114,18 +114,40 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false, true);
|
||||
return;
|
||||
|
||||
case O::V_CMP_LT_I64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false);
|
||||
return;
|
||||
case O::V_CMP_LE_I64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false);
|
||||
return;
|
||||
case O::V_CMP_LE_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LE_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, true);
|
||||
return;
|
||||
case O::V_CMP_GE_U64:
|
||||
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, false);
|
||||
return;
|
||||
|
||||
case O::V_CMP_EQ_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false, false);
|
||||
return;
|
||||
case O::V_CMP_EQ_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_EQ_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true, true);
|
||||
return;
|
||||
case O::V_CMP_NE_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false, false);
|
||||
return;
|
||||
case O::V_CMP_NE_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_NE_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true, true);
|
||||
return;
|
||||
case O::V_CMP_GT_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThan32, false, false);
|
||||
return;
|
||||
@@ -150,21 +172,45 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_CMP_GT_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_GT_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true, true);
|
||||
return;
|
||||
case O::V_CMP_GE_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_GE_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true, true);
|
||||
return;
|
||||
case O::V_CMP_LT_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_LT_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true, true);
|
||||
return;
|
||||
case O::V_CMP_LE_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true, false);
|
||||
return;
|
||||
case O::V_CMPX_LE_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true, true);
|
||||
return;
|
||||
|
||||
case O::V_CMP_F_F32: EmitCompareConstant(inst, false, false, false); return;
|
||||
case O::V_CMP_TRU_F32: EmitCompareConstant(inst, true, false, false); return;
|
||||
case O::V_CMP_EQ_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false, false);
|
||||
return;
|
||||
case O::V_CMP_EQ_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMP_LE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false, false);
|
||||
return;
|
||||
case O::V_CMPX_LE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false, true);
|
||||
return;
|
||||
case O::V_CMPX_GE_F64:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, true);
|
||||
return;
|
||||
case O::V_CMPX_EQ_F32:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false, true);
|
||||
return;
|
||||
@@ -267,6 +313,9 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_CMPX_LE_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true, true);
|
||||
return;
|
||||
case O::V_CMP_NGE_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, true, false);
|
||||
return;
|
||||
case O::V_CMP_NGT_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true, false);
|
||||
return;
|
||||
@@ -418,6 +467,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_SUB_F32: return FloatBinary(inst, IR::ValueOpcode::FPSub32, false);
|
||||
case O::V_SUBREV_F32: return FloatBinary(inst, IR::ValueOpcode::FPSub32, true);
|
||||
case O::V_MUL_F32: return FloatBinary(inst, IR::ValueOpcode::FPMul32, false);
|
||||
case O::V_ADD_F64: return FloatBinary(inst, IR::ValueOpcode::FPAdd64, false);
|
||||
case O::V_MUL_F64: return FloatBinary(inst, IR::ValueOpcode::FPMul64, false);
|
||||
case O::V_MIN_F32: return FloatBinary(inst, IR::ValueOpcode::FPMin32, false);
|
||||
case O::V_MAX_F32: return FloatBinary(inst, IR::ValueOpcode::FPMax32, false);
|
||||
|
||||
@@ -142,6 +142,7 @@ CompiledShaderInfo Program::TakeCompiledInfo() && {
|
||||
.user_data_base = user_data_base,
|
||||
.user_data_count = user_data_count,
|
||||
.scratch_dwords = scratch_dwords,
|
||||
.has_address_writes = has_address_writes,
|
||||
.info = std::move(info),
|
||||
.bindings = std::move(bindings),
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
@@ -34,6 +35,7 @@ struct ResourceSnapshot {
|
||||
std::vector<DescriptorValue> samplers;
|
||||
std::vector<uint32_t> flattened_srt;
|
||||
std::vector<uint32_t> user_data;
|
||||
std::vector<std::pair<uint64_t, uint64_t>> specialization_reads;
|
||||
UniformFill uniform_fill;
|
||||
};
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ enum class ResourceKind {
|
||||
Buffer,
|
||||
IndirectBuffer,
|
||||
Flat,
|
||||
FlatLocal,
|
||||
Global,
|
||||
Scratch,
|
||||
Lds,
|
||||
@@ -40,7 +41,8 @@ enum class ResourceKind {
|
||||
|
||||
[[nodiscard]] constexpr bool IsAddressResourceKind(ResourceKind kind) {
|
||||
return kind == ResourceKind::ScalarAddress || kind == ResourceKind::Flat ||
|
||||
kind == ResourceKind::Global || kind == ResourceKind::Scratch;
|
||||
kind == ResourceKind::FlatLocal || kind == ResourceKind::Global ||
|
||||
kind == ResourceKind::Scratch;
|
||||
}
|
||||
|
||||
struct MemoryInfo {
|
||||
@@ -497,6 +499,7 @@ struct ResourceBlock {
|
||||
Value condition;
|
||||
std::vector<uint32_t> successors;
|
||||
std::vector<uint32_t> sources;
|
||||
std::vector<uint32_t> srt_reads;
|
||||
};
|
||||
|
||||
// Stable shader metadata consumed by the renderer after native IR has been discarded.
|
||||
@@ -508,6 +511,7 @@ struct CompiledShaderInfo {
|
||||
uint32_t user_data_count = 64;
|
||||
uint32_t scratch_dwords = 0;
|
||||
uint32_t param_export_mask = 0;
|
||||
bool has_address_writes = false;
|
||||
ShaderInfo info;
|
||||
BindingLayout bindings;
|
||||
};
|
||||
@@ -562,7 +566,6 @@ struct ResourcePlan {
|
||||
mutable std::vector<uint8_t> visited_blocks;
|
||||
mutable std::vector<uint32_t> pending_blocks;
|
||||
mutable std::vector<uint32_t> material_keys;
|
||||
mutable std::vector<std::pair<uint64_t, uint64_t>> specialization_reads;
|
||||
};
|
||||
|
||||
struct Program: ResourcePlan {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
||||
@@ -135,10 +135,16 @@ bool Value::operator==(const Value& other) const {
|
||||
}
|
||||
|
||||
Inst::Inst(ValueOpcode value_opcode, uint64_t value_flags)
|
||||
: opcode(value_opcode), flags(value_flags) {
|
||||
const auto count = NumArgsOf(opcode);
|
||||
if (count != std::numeric_limits<size_t>::max()) {
|
||||
args.resize(count);
|
||||
: opcode(value_opcode),
|
||||
num_args(value_opcode == ValueOpcode::Phi ? PhiArity
|
||||
: static_cast<uint8_t>(NumArgsOf(value_opcode))),
|
||||
flags(value_flags) {
|
||||
if (num_args == PhiArity) {
|
||||
std::destroy_at(&fixed_args);
|
||||
std::construct_at(&phi_args);
|
||||
} else if (num_args > InlineArity) {
|
||||
std::destroy_at(&fixed_args);
|
||||
std::construct_at(&large_args, num_args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,8 +160,8 @@ Type Inst::GetType() const {
|
||||
if (opcode == ValueOpcode::Phi) {
|
||||
return static_cast<Type>(flags);
|
||||
}
|
||||
if (opcode == ValueOpcode::Identity && !args.empty()) {
|
||||
return args.front().GetType();
|
||||
if (opcode == ValueOpcode::Identity && num_args != 0) {
|
||||
return Arg(0).GetType();
|
||||
}
|
||||
return TypeOf(opcode);
|
||||
}
|
||||
@@ -173,21 +179,24 @@ size_t Inst::UseCount() const {
|
||||
}
|
||||
|
||||
size_t Inst::NumArgs() const {
|
||||
return args.size();
|
||||
return num_args == PhiArity ? phi_args.size() : num_args;
|
||||
}
|
||||
|
||||
size_t Inst::NumPhiBlocks() const {
|
||||
return phi_blocks.size();
|
||||
return num_args == PhiArity ? phi_args.size() : 0;
|
||||
}
|
||||
|
||||
Value Inst::Arg(size_t index) const {
|
||||
EXIT_IF(index >= args.size());
|
||||
return args[index];
|
||||
EXIT_IF(index >= NumArgs());
|
||||
if (num_args <= InlineArity) {
|
||||
return fixed_args[index];
|
||||
}
|
||||
return num_args == PhiArity ? phi_args[index].second : large_args[index];
|
||||
}
|
||||
|
||||
Block* Inst::PhiBlock(size_t index) const {
|
||||
EXIT_IF(opcode != ValueOpcode::Phi || index >= phi_blocks.size());
|
||||
return phi_blocks[index];
|
||||
EXIT_IF(opcode != ValueOpcode::Phi || index >= phi_args.size());
|
||||
return phi_args[index].first;
|
||||
}
|
||||
|
||||
Block* Inst::Parent() const {
|
||||
@@ -203,15 +212,17 @@ void Inst::SetParent(Block* block) {
|
||||
}
|
||||
|
||||
void Inst::SetArg(size_t index, Value value) {
|
||||
if (index >= args.size()) {
|
||||
EXIT_IF(NumArgsOf(opcode) != std::numeric_limits<size_t>::max());
|
||||
args.resize(index + 1);
|
||||
}
|
||||
const auto old = args[index];
|
||||
const auto old = Arg(index);
|
||||
if (auto* old_inst = old.TryInstruction(); old_inst != nullptr) {
|
||||
RemoveUse(old_inst, index);
|
||||
}
|
||||
args[index] = value;
|
||||
if (num_args <= InlineArity) {
|
||||
fixed_args[index] = value;
|
||||
} else if (num_args == PhiArity) {
|
||||
phi_args[index].second = value;
|
||||
} else {
|
||||
large_args[index] = value;
|
||||
}
|
||||
if (auto* new_inst = value.TryInstruction(); new_inst != nullptr) {
|
||||
AddUse(new_inst, index);
|
||||
}
|
||||
@@ -219,9 +230,8 @@ void Inst::SetArg(size_t index, Value value) {
|
||||
|
||||
void Inst::AddPhiOperand(Block* predecessor, Value value) {
|
||||
EXIT_IF(opcode != ValueOpcode::Phi);
|
||||
const auto index = args.size();
|
||||
args.push_back(value);
|
||||
phi_blocks.push_back(predecessor);
|
||||
const auto index = phi_args.size();
|
||||
phi_args.emplace_back(predecessor, value);
|
||||
if (auto* value_inst = value.TryInstruction(); value_inst != nullptr) {
|
||||
AddUse(value_inst, index);
|
||||
}
|
||||
@@ -234,21 +244,12 @@ void Inst::ReplaceUsesWith(Value replacement, bool preserve) {
|
||||
}
|
||||
Invalidate();
|
||||
if (preserve) {
|
||||
ReplaceOpcode(ValueOpcode::Identity);
|
||||
args.resize(1);
|
||||
opcode = ValueOpcode::Identity;
|
||||
num_args = 1;
|
||||
SetArg(0, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
void Inst::ReplaceOpcode(ValueOpcode value_opcode) {
|
||||
opcode = value_opcode;
|
||||
const auto count = NumArgsOf(opcode);
|
||||
if (count != std::numeric_limits<size_t>::max()) {
|
||||
EXIT_IF(!args.empty() && args.size() != count);
|
||||
args.resize(count);
|
||||
}
|
||||
}
|
||||
|
||||
void Inst::Invalidate() {
|
||||
ClearArgs();
|
||||
opcode = ValueOpcode::Void;
|
||||
@@ -269,13 +270,21 @@ void Inst::RemoveUse(Inst* used, size_t operand) {
|
||||
}
|
||||
|
||||
void Inst::ClearArgs() {
|
||||
for (size_t index = 0; index < args.size(); index++) {
|
||||
if (auto* value_inst = args[index].TryInstruction(); value_inst != nullptr) {
|
||||
for (size_t index = 0; index < NumArgs(); index++) {
|
||||
if (auto* value_inst = Arg(index).TryInstruction(); value_inst != nullptr) {
|
||||
RemoveUse(value_inst, index);
|
||||
}
|
||||
}
|
||||
args.clear();
|
||||
phi_blocks.clear();
|
||||
if (num_args == PhiArity) {
|
||||
std::destroy_at(&phi_args);
|
||||
std::construct_at(&fixed_args);
|
||||
} else if (num_args > InlineArity) {
|
||||
std::destroy_at(&large_args);
|
||||
std::construct_at(&fixed_args);
|
||||
} else {
|
||||
fixed_args.fill(Value {});
|
||||
}
|
||||
num_args = 0;
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "graphics/shader/recompiler/ir/Reg.h"
|
||||
#include "graphics/shader/recompiler/ir/opcodes/ValueOpcodes.h"
|
||||
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -133,7 +134,6 @@ public:
|
||||
void SetArg(size_t index, Value value);
|
||||
void AddPhiOperand(Block* predecessor, Value value);
|
||||
void ReplaceUsesWith(Value replacement, bool preserve = true);
|
||||
void ReplaceOpcode(ValueOpcode opcode);
|
||||
void Invalidate();
|
||||
|
||||
template <typename T>
|
||||
@@ -156,13 +156,22 @@ private:
|
||||
void RemoveUse(Inst* used, size_t operand);
|
||||
void ClearArgs();
|
||||
|
||||
static constexpr uint8_t InlineArity = 4;
|
||||
static constexpr uint8_t PhiArity = UINT8_MAX;
|
||||
|
||||
ValueOpcode opcode;
|
||||
uint8_t num_args;
|
||||
mutable uint32_t evaluation_index = UINT32_MAX;
|
||||
uint64_t flags;
|
||||
Block* parent = nullptr;
|
||||
std::vector<Value> args;
|
||||
std::vector<Block*> phi_blocks;
|
||||
union {
|
||||
std::array<Value, InlineArity> fixed_args {};
|
||||
std::vector<Value> large_args;
|
||||
std::vector<std::pair<Block*, Value>> phi_args;
|
||||
};
|
||||
std::vector<Use> uses;
|
||||
mutable uint32_t evaluation_index = UINT32_MAX;
|
||||
};
|
||||
|
||||
static_assert(sizeof(Inst) <= 112, "Inst operand storage unintentionally increased");
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
@@ -123,6 +123,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
|
||||
case ValueOpcode::BufferAtomicSMax32:
|
||||
case ValueOpcode::BufferAtomicUMax32:
|
||||
case ValueOpcode::BufferAtomicAnd32:
|
||||
case ValueOpcode::BufferAtomicAnd64:
|
||||
case ValueOpcode::BufferAtomicOr32:
|
||||
case ValueOpcode::BufferAtomicOr64:
|
||||
case ValueOpcode::BufferAtomicXor32:
|
||||
@@ -135,6 +136,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
|
||||
uint32_t BufferComponentCount(ValueOpcode opcode) {
|
||||
switch (opcode) {
|
||||
case ValueOpcode::BufferAtomicSwap64:
|
||||
case ValueOpcode::BufferAtomicAnd64:
|
||||
case ValueOpcode::BufferAtomicOr64:
|
||||
case ValueOpcode::LoadBufferU32x2:
|
||||
case ValueOpcode::StoreBufferU32x2: return 2u;
|
||||
@@ -164,6 +166,7 @@ SharedAccess SharedAccessOf(ValueOpcode opcode) {
|
||||
case ValueOpcode::SharedAtomicFMax32:
|
||||
case ValueOpcode::SharedAtomicSwap32:
|
||||
case ValueOpcode::SharedAtomicIAdd32:
|
||||
case ValueOpcode::SharedAtomicIAdd64:
|
||||
case ValueOpcode::SharedAtomicISub32:
|
||||
case ValueOpcode::SharedAtomicInc32:
|
||||
case ValueOpcode::SharedAtomicDec32:
|
||||
@@ -183,6 +186,7 @@ SharedAccess SharedAccessOf(ValueOpcode opcode) {
|
||||
|
||||
uint32_t SharedComponentCount(ValueOpcode opcode) {
|
||||
switch (opcode) {
|
||||
case ValueOpcode::SharedAtomicIAdd64:
|
||||
case ValueOpcode::SharedAtomicOr64:
|
||||
case ValueOpcode::LoadSharedU32x2:
|
||||
case ValueOpcode::WriteSharedU32x2: return 2u;
|
||||
@@ -216,9 +220,12 @@ ImageOpcodeInfo ImageOpcodeInfoOf(ValueOpcode opcode) {
|
||||
return {ImageAccess::Read, ImageResourceClass::Sampled, true};
|
||||
case ValueOpcode::ImageWrite:
|
||||
return {ImageAccess::Write, ImageResourceClass::Storage, false};
|
||||
case ValueOpcode::ImageAtomicCompareSwap32:
|
||||
case ValueOpcode::ImageAtomicSwap32:
|
||||
case ValueOpcode::ImageAtomicIAdd32:
|
||||
case ValueOpcode::ImageAtomicSMin32:
|
||||
case ValueOpcode::ImageAtomicUMin32:
|
||||
case ValueOpcode::ImageAtomicSMax32:
|
||||
case ValueOpcode::ImageAtomicUMax32:
|
||||
case ValueOpcode::ImageAtomicAnd32:
|
||||
case ValueOpcode::ImageAtomicOr32:
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
||||
enum class ValueOpcode {
|
||||
enum class ValueOpcode : uint16_t {
|
||||
#define VALUE_OPCODE(name, ...) name,
|
||||
#include "graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc"
|
||||
#undef VALUE_OPCODE
|
||||
|
||||
@@ -137,6 +137,9 @@ VALUE_OPCODE(SMedTri32, U32, U32, U32, U32)
|
||||
VALUE_OPCODE(UMedTri32, U32, U32, U32, U32)
|
||||
VALUE_OPCODE(SLessThan32, U1, U32, U32)
|
||||
VALUE_OPCODE(SLessThan64, U1, U64, U64)
|
||||
VALUE_OPCODE(SLessThanEqual64, U1, U64, U64)
|
||||
VALUE_OPCODE(ULessThanEqual64, U1, U64, U64)
|
||||
VALUE_OPCODE(UGreaterThanEqual64, U1, U64, U64)
|
||||
VALUE_OPCODE(ULessThan32, U1, U32, U32)
|
||||
VALUE_OPCODE(ULessThan64, U1, U64, U64)
|
||||
VALUE_OPCODE(IEqual32, U1, U32, U32)
|
||||
@@ -155,6 +158,9 @@ VALUE_OPCODE(LogicalAnd, U1, U1, U1)
|
||||
VALUE_OPCODE(LogicalXor, U1, U1, U1)
|
||||
VALUE_OPCODE(LogicalNot, U1, U1)
|
||||
VALUE_OPCODE(FPOrdEqual32, U1, F32, F32)
|
||||
VALUE_OPCODE(FPOrdEqual64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPOrdLessThanEqual64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPOrdGreaterThanEqual64, U1, F64, F64)
|
||||
VALUE_OPCODE(FPUnordEqual32, U1, F32, F32)
|
||||
VALUE_OPCODE(FPOrdNotEqual32, U1, F32, F32)
|
||||
VALUE_OPCODE(FPUnordNotEqual32, U1, F32, F32)
|
||||
@@ -173,6 +179,7 @@ VALUE_OPCODE(FPAdd32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPSub32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPFma32, F32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPMul32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPAdd64, F64, F64, F64)
|
||||
VALUE_OPCODE(FPMul64, F64, F64, F64)
|
||||
VALUE_OPCODE(FPFma64, F64, F64, F64, F64)
|
||||
VALUE_OPCODE(FPRecip64, F64, F64)
|
||||
@@ -245,6 +252,7 @@ VALUE_OPCODE(BufferAtomicUMin32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicSMax32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicUMax32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicAnd32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicAnd64, U64, BufferResource, U32, U32, U32, U64, U1)
|
||||
VALUE_OPCODE(BufferAtomicOr32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicOr64, U64, BufferResource, U32, U32, U32, U64, U1)
|
||||
VALUE_OPCODE(BufferAtomicXor32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
@@ -267,6 +275,7 @@ VALUE_OPCODE(SharedAtomicFMin32, Void, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicFMax32, Void, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicSwap32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicIAdd32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicIAdd64, Void, U32, U64, U1)
|
||||
VALUE_OPCODE(SharedAtomicISub32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicInc32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicDec32, U32, U32, U32, U1)
|
||||
@@ -283,14 +292,18 @@ VALUE_OPCODE(DataConsume, U32, U32, U1)
|
||||
VALUE_OPCODE(SwizzleU32, U32, U32, U32, U1)
|
||||
|
||||
VALUE_OPCODE(ImageQueryDimensions, U32x4, ImageResource, ImageAddress)
|
||||
VALUE_OPCODE(BvhIntersect, U32x4, U32x4, ImageAddress, U1)
|
||||
VALUE_OPCODE(ImageQueryLod, U32x4, ImageResource, SamplerResource, ImageAddress)
|
||||
VALUE_OPCODE(ImageRead, U32x4, ImageResource, ImageAddress, U1)
|
||||
VALUE_OPCODE(ImageWrite, Void, ImageResource, ImageAddress, U32x4, U1)
|
||||
VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress)
|
||||
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress)
|
||||
VALUE_OPCODE(ImageAtomicCompareSwap32, U32, ImageResource, ImageAddress, U32, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicSwap32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicIAdd32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicSMin32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicUMin32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicSMax32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicUMax32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicAnd32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicOr32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
@@ -575,6 +576,17 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
||||
case ValueOpcode::UGreaterThan64:
|
||||
FoldU64Compare(inst, [](uint64_t a, uint64_t b) { return a > b; });
|
||||
return;
|
||||
case ValueOpcode::ULessThanEqual64:
|
||||
FoldU64Compare(inst, [](uint64_t a, uint64_t b) { return a <= b; });
|
||||
return;
|
||||
case ValueOpcode::UGreaterThanEqual64:
|
||||
FoldU64Compare(inst, [](uint64_t a, uint64_t b) { return a >= b; });
|
||||
return;
|
||||
case ValueOpcode::SLessThanEqual64:
|
||||
FoldU64Compare(inst, [](uint64_t a, uint64_t b) {
|
||||
return std::bit_cast<int64_t>(a) <= std::bit_cast<int64_t>(b);
|
||||
});
|
||||
return;
|
||||
case ValueOpcode::SLessThan64:
|
||||
FoldU64Compare(inst, [](uint64_t a, uint64_t b) {
|
||||
return std::bit_cast<int64_t>(a) < std::bit_cast<int64_t>(b);
|
||||
@@ -649,12 +661,110 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
||||
}
|
||||
}
|
||||
|
||||
// In wave32, extracting this invocation's bit from a ballot recovers its predicate.
|
||||
// Keep that identity through scalar EXEC operations and their loop-carried mask Phis.
|
||||
class LaneMaskProjection {
|
||||
public:
|
||||
explicit LaneMaskProjection(std::unordered_set<Inst*>& lowered_ancillary)
|
||||
: m_lowered_ancillary(lowered_ancillary) {}
|
||||
|
||||
void Fold(Inst& inst) {
|
||||
if (inst.GetOpcode() != ValueOpcode::INotEqual32 || !Immediate(Arg(inst, 1), 0u)) return;
|
||||
const auto* bit = Arg(inst, 0).TryInstruction();
|
||||
if (bit == nullptr || bit->GetOpcode() != ValueOpcode::BitwiseAnd32 ||
|
||||
!Immediate(Arg(*bit, 1), 1u)) return;
|
||||
const auto* shift = Arg(*bit, 0).TryInstruction();
|
||||
if (shift == nullptr || shift->GetOpcode() != ValueOpcode::ShiftRightLogical32) return;
|
||||
const auto* index = Arg(*shift, 1).TryInstruction();
|
||||
if (index == nullptr || index->GetOpcode() != ValueOpcode::BitwiseAnd32 ||
|
||||
!Immediate(Arg(*index, 1), 31u)) return;
|
||||
const auto* lane = Arg(*index, 0).TryInstruction();
|
||||
if (lane == nullptr || lane->GetOpcode() != ValueOpcode::LaneId) return;
|
||||
m_visited.clear();
|
||||
m_grounded = false;
|
||||
if (CanProject(Arg(*shift, 0)) && m_grounded) Replace(inst, Project(Arg(*shift, 0)));
|
||||
}
|
||||
|
||||
private:
|
||||
static bool Immediate(Value value, uint32_t expected) {
|
||||
return IsImmediate(value, Type::U32) && value.U32() == expected;
|
||||
}
|
||||
|
||||
static Value BallotPredicate(const Inst& inst) {
|
||||
if (inst.GetOpcode() != ValueOpcode::CompositeExtractU32x4 ||
|
||||
!Immediate(Arg(inst, 1), 0u)) return {};
|
||||
const auto* source = Arg(inst, 0).TryInstruction();
|
||||
return source != nullptr && source->GetOpcode() == ValueOpcode::Ballot
|
||||
? Arg(*source, 0) : Value {};
|
||||
}
|
||||
|
||||
bool CanProject(Value value) {
|
||||
if (value.IsImmediate()) {
|
||||
const bool supported = Immediate(value, 0u) || Immediate(value, UINT32_MAX);
|
||||
m_grounded |= supported;
|
||||
return supported;
|
||||
}
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) return false;
|
||||
if (m_values.contains(inst) || !BallotPredicate(*inst).IsEmpty()) {
|
||||
m_grounded = true;
|
||||
return true;
|
||||
}
|
||||
if (!m_visited.insert(inst).second) return true;
|
||||
const auto op = inst->GetOpcode();
|
||||
if (op != ValueOpcode::BitwiseAnd32 && op != ValueOpcode::BitwiseOr32 &&
|
||||
op != ValueOpcode::BitwiseNot32 && op != ValueOpcode::SelectU32 && op != ValueOpcode::Phi)
|
||||
return false;
|
||||
for (size_t arg = op == ValueOpcode::SelectU32 ? 1u : 0u; arg < inst->NumArgs(); ++arg) {
|
||||
if (!CanProject(Arg(*inst, arg))) return false;
|
||||
}
|
||||
return inst->NumArgs() != 0;
|
||||
}
|
||||
|
||||
Value Project(Value value) {
|
||||
if (value.IsImmediate()) return Value(value.U32() != 0u);
|
||||
auto* source = value.TryInstruction();
|
||||
if (const auto found = m_values.find(source); found != m_values.end()) return found->second.Resolve();
|
||||
if (const auto predicate = BallotPredicate(*source); !predicate.IsEmpty()) return predicate;
|
||||
auto* block = source->Parent();
|
||||
const auto where = std::find_if(block->begin(), block->end(),
|
||||
[source](const Inst& candidate) { return &candidate == source; });
|
||||
const auto op = source->GetOpcode() == ValueOpcode::BitwiseAnd32 ? ValueOpcode::LogicalAnd
|
||||
: source->GetOpcode() == ValueOpcode::BitwiseOr32 ? ValueOpcode::LogicalOr
|
||||
: source->GetOpcode() == ValueOpcode::BitwiseNot32 ? ValueOpcode::LogicalNot
|
||||
: source->GetOpcode() == ValueOpcode::SelectU32 ? ValueOpcode::SelectU1
|
||||
: ValueOpcode::Phi;
|
||||
auto result = op == ValueOpcode::Phi ? block->PrependNewInst(where, op)
|
||||
: op == ValueOpcode::LogicalNot
|
||||
? block->PrependNewInst(where, op, {Value(false)})
|
||||
: op == ValueOpcode::SelectU1
|
||||
? block->PrependNewInst(where, op, {Arg(*source, 0), Value(false), Value(false)})
|
||||
: block->PrependNewInst(where, op, {Value(false), Value(false)});
|
||||
m_values.emplace(source, Value(&*result));
|
||||
if (op == ValueOpcode::Phi) result->SetFlags(Type::U1);
|
||||
for (size_t arg = op == ValueOpcode::SelectU1 ? 1u : 0u; arg < source->NumArgs(); ++arg) {
|
||||
const auto projected = Project(Arg(*source, arg));
|
||||
if (op == ValueOpcode::Phi) result->AddPhiOperand(source->PhiBlock(arg), projected);
|
||||
else result->SetArg(arg, projected);
|
||||
}
|
||||
FoldInstruction(*block, result, m_lowered_ancillary);
|
||||
return Value(&*result).Resolve();
|
||||
}
|
||||
|
||||
std::unordered_set<Inst*>& m_lowered_ancillary;
|
||||
std::unordered_set<const Inst*> m_visited;
|
||||
std::unordered_map<const Inst*, Value> m_values;
|
||||
bool m_grounded = false;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void ConstantPropagationPass(const BlockList& blocks) {
|
||||
void ConstantPropagationPass(const BlockList& blocks, uint32_t wave_size) {
|
||||
std::unordered_set<Inst*> lowered_ancillary;
|
||||
LaneMaskProjection mask_projection(lowered_ancillary);
|
||||
for (auto* block: blocks) {
|
||||
for (auto inst = block->begin(); inst != block->end(); ++inst) {
|
||||
if (wave_size == 32u) mask_projection.Fold(*inst);
|
||||
FoldInstruction(*block, inst, lowered_ancillary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
||||
void ConstantPropagationPass(const BlockList& blocks);
|
||||
void ConstantPropagationPass(const BlockList& blocks, uint32_t wave_size = 64);
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
@@ -157,34 +157,15 @@ bool CaptureStrictRead(void* userdata, uint64_t address, std::span<uint32_t> val
|
||||
|
||||
bool CaptureOrdinaryRead(void* userdata, uint64_t address, std::span<uint32_t> values) {
|
||||
auto& capture = *static_cast<ReadCapture*>(userdata);
|
||||
if (!capture.source.read_memory(capture.source.userdata, address, values)) {
|
||||
return false;
|
||||
if (capture.source.read_memory != nullptr) {
|
||||
if (!capture.source.read_memory(capture.source.userdata, address, values)) return false;
|
||||
} else {
|
||||
std::memcpy(values.data(), reinterpret_cast<const void*>(address), values.size_bytes());
|
||||
}
|
||||
capture.ranges.emplace_back(address, values.size_bytes());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WrittenBuffersDisjoint(const ResourcePlan& program, const ResourceSnapshot& snapshot,
|
||||
std::span<const std::pair<uint64_t, uint64_t>> reads) {
|
||||
for (uint32_t i = 0; i < program.info.buffers.size(); ++i) {
|
||||
if (!program.info.buffers[i].written) continue;
|
||||
ShaderBufferResource buffer;
|
||||
if (!DecodeBufferDescriptor(snapshot.buffers[i], buffer)) return false;
|
||||
const auto base = buffer.Base48();
|
||||
const auto size = buffer.GetSize();
|
||||
if (size == 0u) continue;
|
||||
if (size - 1u > AddressMask - base) return false;
|
||||
for (const auto [address, bytes]: reads) {
|
||||
if (bytes == 0u) continue;
|
||||
if (address > AddressMask || bytes - 1u > AddressMask - address ||
|
||||
(address <= base ? base - address < bytes : address - base < size)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const DescriptorSource* Source(const ResourcePlan& program, uint32_t source) {
|
||||
if (source >= program.descriptor_sources.size()) {
|
||||
return nullptr;
|
||||
@@ -638,9 +619,10 @@ bool BuildSamplerPlan(const ShaderInfo& base, const Images& images, SamplerPlan&
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
|
||||
// Any shader write may alias a scalar predicate read, including on a later loop visit.
|
||||
if (program.blocks.size() != program.block_info.size() || HasShaderMemoryWrites(program)) {
|
||||
template <typename Predicate>
|
||||
static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, const Predicate& predicate) {
|
||||
// The renderer checks captured scalar reads against final buffer and image write ranges.
|
||||
if (program.blocks.size() != program.block_info.size() || program.has_address_writes) {
|
||||
return {};
|
||||
}
|
||||
std::unordered_map<uint32_t, uint32_t> indices;
|
||||
@@ -659,9 +641,7 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
|
||||
case CFG::TerminatorKind::Branch: successors.push_back(terminator.true_block); break;
|
||||
case CFG::TerminatorKind::ConditionalBranch:
|
||||
successors = {terminator.true_block, terminator.false_block};
|
||||
if (ValidateRuntimeValue(program, info.condition, RuntimeValueType::Integer)) {
|
||||
block.condition = info.condition;
|
||||
}
|
||||
block.condition = info.condition;
|
||||
break;
|
||||
case CFG::TerminatorKind::IndirectBranch:
|
||||
successors = terminator.indirect_targets;
|
||||
@@ -678,13 +658,17 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
|
||||
}
|
||||
for (const auto& inst: *program.blocks[i]) {
|
||||
const auto op = inst.GetOpcode();
|
||||
if (op == ValueOpcode::ReadConst) {
|
||||
block.srt_reads.push_back(inst.Arg(1).Resolve().U32());
|
||||
continue;
|
||||
}
|
||||
const auto buffer = BufferAccessOf(op);
|
||||
const auto image = ImageOpcodeInfoOf(op);
|
||||
if (buffer == BufferAccess::None && image.access == ImageAccess::None) {
|
||||
continue;
|
||||
}
|
||||
const auto& memory = program.memory_info.at(inst.Flags<MemoryFlags>().index);
|
||||
if (memory.planning_only) {
|
||||
if (memory.planning_only || memory.kind == ResourceKind::IndirectBuffer) {
|
||||
continue;
|
||||
}
|
||||
if (buffer != BufferAccess::None) {
|
||||
@@ -700,6 +684,7 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
|
||||
block.sources.erase(std::unique(block.sources.begin(), block.sources.end()),
|
||||
block.sources.end());
|
||||
}
|
||||
for (auto& block: blocks) block.condition = predicate(block.condition);
|
||||
if (std::ranges::none_of(
|
||||
blocks, [](const ResourceBlock& block) { return !block.condition.IsEmpty(); })) {
|
||||
return {};
|
||||
@@ -914,10 +899,38 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
for (const auto& read: program.srt_reads) {
|
||||
plan.srt_reads.push_back({Clone(read.value), read.flat_offset});
|
||||
}
|
||||
plan.control_flow = ResourceControlFlow(program);
|
||||
for (auto& block: plan.control_flow) {
|
||||
block.condition = Clone(block.condition);
|
||||
}
|
||||
// A proven uniform factor can decide a branch even when its other lanes are unknown.
|
||||
// Keep only that Boolean structure, never the varying shader dependency graph.
|
||||
Value unknown;
|
||||
std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value {
|
||||
value = value.Resolve();
|
||||
if (value.IsEmpty()) return {};
|
||||
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) return Clone(value);
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) return {};
|
||||
const auto op = inst->GetOpcode();
|
||||
if (op == ValueOpcode::ConditionRef || op == ValueOpcode::LogicalNot) {
|
||||
const auto operand = ClonePredicate(inst->Arg(0));
|
||||
if (operand.IsEmpty() || op == ValueOpcode::ConditionRef) return operand;
|
||||
auto& node = plan.value_storage.emplace_back(op);
|
||||
node.SetArg(0, operand);
|
||||
return Value(&node);
|
||||
}
|
||||
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr) return {};
|
||||
auto left = ClonePredicate(inst->Arg(0));
|
||||
auto right = ClonePredicate(inst->Arg(1));
|
||||
if (left.IsEmpty() && right.IsEmpty()) return {};
|
||||
if (left.IsEmpty() || right.IsEmpty()) {
|
||||
if (unknown.IsEmpty()) unknown = Value(&plan.value_storage.emplace_back(ValueOpcode::UndefU1));
|
||||
if (left.IsEmpty()) left = unknown;
|
||||
if (right.IsEmpty()) right = unknown;
|
||||
}
|
||||
auto& node = plan.value_storage.emplace_back(op);
|
||||
node.SetArg(0, left);
|
||||
node.SetArg(1, right);
|
||||
return Value(&node);
|
||||
};
|
||||
plan.control_flow = ResourceControlFlow(program, ClonePredicate);
|
||||
plan.uniform_fill = AnalyzeUniformFill(program);
|
||||
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
|
||||
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
|
||||
@@ -936,12 +949,15 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->table_source),
|
||||
plan.clean_flat_slots);
|
||||
}
|
||||
if (masked_image) {
|
||||
plan.resource_tracking_complete &= !program.has_address_writes &&
|
||||
!std::ranges::any_of(plan.info.images, &ImageResource::written);
|
||||
plan.capture_specialization_reads =
|
||||
std::ranges::any_of(plan.info.buffers, &BufferResource::written);
|
||||
plan.capture_specialization_reads = masked_image || !plan.control_flow.empty();
|
||||
// Writable descriptor addresses must come from clean backing for the alias proof.
|
||||
for (const auto& buffer: plan.info.buffers) {
|
||||
if (buffer.written) MarkCleanFlatSlots(plan, Source(plan, buffer.source), plan.clean_flat_slots);
|
||||
}
|
||||
for (const auto& image: plan.info.images) {
|
||||
if (image.written) MarkCleanFlatSlots(plan, Source(plan, image.source), plan.clean_flat_slots);
|
||||
}
|
||||
if (masked_image) plan.resource_tracking_complete &= !program.has_address_writes;
|
||||
return plan;
|
||||
}
|
||||
|
||||
@@ -952,21 +968,22 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
return false;
|
||||
}
|
||||
const bool capture_reads = program.capture_specialization_reads;
|
||||
auto& reads = program.specialization_reads;
|
||||
auto& reads = snapshot.specialization_reads;
|
||||
reads.clear();
|
||||
ReadCapture capture {runtime, reads};
|
||||
SrtRuntime observed = runtime;
|
||||
if (capture_reads) {
|
||||
reads.clear();
|
||||
observed.userdata = &capture;
|
||||
observed.read_specialization_memory = CaptureStrictRead;
|
||||
if (observed.read_memory != nullptr) observed.read_memory = CaptureOrdinaryRead;
|
||||
observed.read_specialization_memory = runtime.read_specialization_memory != nullptr
|
||||
? CaptureStrictRead : nullptr;
|
||||
observed.read_memory = CaptureOrdinaryRead;
|
||||
}
|
||||
SrtWalker clean(program, CleanRuntime(observed));
|
||||
SrtWalker walker(program, observed, program.clean_flat_slots, &clean);
|
||||
const auto active = clean.FindActiveSources();
|
||||
if (!walker.RefreshFlatBuffer(snapshot.flattened_srt)) {
|
||||
return false;
|
||||
}
|
||||
const auto active = std::span<const uint8_t>(program.active_sources);
|
||||
snapshot.uniform_fill = {};
|
||||
const auto& fill = program.uniform_fill;
|
||||
const auto words = fill.fill.words;
|
||||
@@ -979,12 +996,12 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
snapshot.uniform_fill = fill.fill;
|
||||
snapshot.uniform_fill.value = stored[0];
|
||||
}
|
||||
const auto evaluate = [&](uint32_t source, DescriptorValue& value) {
|
||||
const auto evaluate = [&](uint32_t source, DescriptorValue& value, bool written = false) {
|
||||
if (source >= program.descriptor_sources.size()) {
|
||||
return false;
|
||||
}
|
||||
if (active.empty() || active[source]) {
|
||||
return walker.EvaluateDescriptor(source, value);
|
||||
return (written ? clean : walker).EvaluateDescriptor(source, value);
|
||||
}
|
||||
value = {};
|
||||
value.dword_count = program.descriptor_sources[source].dword_count;
|
||||
@@ -992,19 +1009,11 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
};
|
||||
snapshot.buffers.resize(program.info.buffers.size());
|
||||
for (uint32_t i = 0; i < program.info.buffers.size(); ++i) {
|
||||
if (!evaluate(program.info.buffers[i].source, snapshot.buffers[i])) {
|
||||
if (!evaluate(program.info.buffers[i].source, snapshot.buffers[i],
|
||||
program.info.buffers[i].written)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (capture_reads) {
|
||||
for (uint32_t i = 0; i < program.info.buffers.size(); ++i) {
|
||||
const auto& buffer = program.info.buffers[i];
|
||||
if (!buffer.written || (!active.empty() && !active[buffer.source])) continue;
|
||||
DescriptorValue strict;
|
||||
if (!clean.EvaluateDescriptor(buffer.source, strict) ||
|
||||
strict != snapshot.buffers[i]) return false;
|
||||
}
|
||||
}
|
||||
snapshot.images.resize(program.info.images.size());
|
||||
specialization.images.resize(program.info.images.size());
|
||||
for (uint32_t i = 0; i < program.info.images.size(); ++i) {
|
||||
@@ -1040,7 +1049,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!evaluate(image.source, snapshot.images[i])) {
|
||||
if (!evaluate(image.source, snapshot.images[i], image.written)) {
|
||||
return false;
|
||||
}
|
||||
if (!ValidImageDescriptor(snapshot.images[i], image.r128)) {
|
||||
@@ -1054,7 +1063,6 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (capture_reads && !WrittenBuffersDisjoint(program, snapshot, reads)) return false;
|
||||
snapshot.user_data.assign(runtime.user_data.begin(), runtime.user_data.end());
|
||||
return BuildResourceSpecialization(program, snapshot, specialization);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,34 @@ uint32_t PossibleU32Bits(Value value) {
|
||||
}
|
||||
}
|
||||
|
||||
bool IsNoncanonicalFlatAddress(Value high, Value active) {
|
||||
high = high.Resolve();
|
||||
active = active.Resolve();
|
||||
// A predicated VGPR write supplies its new value on exactly the lanes which
|
||||
// execute this access. Do not discard a selection under a different mask.
|
||||
while (const auto* select = high.TryInstruction()) {
|
||||
if (select->GetOpcode() != ValueOpcode::SelectU32 || select->Arg(0).Resolve() != active) break;
|
||||
high = select->Arg(1).Resolve();
|
||||
}
|
||||
uint32_t lower = 0, upper = UINT32_MAX;
|
||||
if (high.IsImmediate() && high.GetType() == Type::U32) {
|
||||
lower = upper = high.U32();
|
||||
} else if (const auto* inst = high.TryInstruction();
|
||||
inst != nullptr && inst->GetOpcode() == ValueOpcode::UMedTri32) {
|
||||
// The median lies between any two constant inputs, regardless of the third.
|
||||
uint32_t count = 0;
|
||||
for (size_t index = 0; index < inst->NumArgs(); ++index) {
|
||||
const auto value = inst->Arg(index).Resolve();
|
||||
if (!value.IsImmediate() || value.GetType() != Type::U32) continue;
|
||||
if (count++ == 0) lower = upper = value.U32();
|
||||
else { lower = std::min(lower, value.U32()); upper = std::max(upper, value.U32()); }
|
||||
}
|
||||
if (count < 2) return false;
|
||||
}
|
||||
// Leave one high DWORD on each side for the signed instruction offset/carry.
|
||||
return lower > 0x00008000u && upper < 0xffff7fffu;
|
||||
}
|
||||
|
||||
Value CanonicalizeSampleAdjustDword3(Value value) {
|
||||
for (;;) {
|
||||
value = value.Resolve();
|
||||
@@ -1649,6 +1677,10 @@ private:
|
||||
|
||||
void Collect(Inst& inst) {
|
||||
const auto op = inst.GetOpcode();
|
||||
if (op == ValueOpcode::BvhIntersect) {
|
||||
m_info.uses_dma = true;
|
||||
return;
|
||||
}
|
||||
const auto buffer = BufferAccessOf(op);
|
||||
const auto address_info = AddressOpcodeInfoOf(op);
|
||||
const auto image_info = ImageOpcodeInfoOf(op);
|
||||
@@ -1695,6 +1727,15 @@ private:
|
||||
if (!IsAddressResourceKind(memory.kind)) {
|
||||
Fail(flags.pc, "address operation has invalid resource kind");
|
||||
}
|
||||
if (memory.kind == ResourceKind::Flat && memory.address_is_full &&
|
||||
IsNoncanonicalFlatAddress(inst.Arg(2), inst.Arg(inst.NumArgs() - 1))) {
|
||||
ValidateAddressHandle(inst.Arg(0), flags.pc);
|
||||
if (memory.data_bits != 32u || m_program.scratch_dwords == 0) {
|
||||
Fail(flags.pc, "local FLAT access requires DWORD data and per-thread scratch storage");
|
||||
}
|
||||
m_program.memory_info[flags.index].kind = ResourceKind::FlatLocal;
|
||||
return;
|
||||
}
|
||||
if (memory.kind == ResourceKind::Scratch) {
|
||||
handle = inst.Arg(0).Resolve().TryInstruction();
|
||||
if (handle == nullptr || handle->GetOpcode() != ValueOpcode::GetScratchResource ||
|
||||
|
||||
@@ -795,18 +795,26 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case ValueOpcode::LogicalAnd:
|
||||
if (binary()) {
|
||||
result = (a != 0u) && (b != 0u);
|
||||
case ValueOpcode::LogicalAnd: {
|
||||
const bool left = Arg(inst, 0, a);
|
||||
if (left && a == 0u) {
|
||||
result = 0u;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case ValueOpcode::LogicalOr:
|
||||
if (binary()) {
|
||||
result = (a != 0u) || (b != 0u);
|
||||
if (!Arg(inst, 1, b) || (b != 0u && !left)) return false;
|
||||
result = b != 0u;
|
||||
return true;
|
||||
}
|
||||
case ValueOpcode::LogicalOr: {
|
||||
const bool left = Arg(inst, 0, a);
|
||||
if (left && a != 0u) {
|
||||
result = 1u;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (!Arg(inst, 1, b) || (b == 0u && !left)) return false;
|
||||
result = b != 0u;
|
||||
return true;
|
||||
}
|
||||
case ValueOpcode::LogicalXor:
|
||||
if (binary()) {
|
||||
result = (a != 0u) != (b != 0u);
|
||||
@@ -844,16 +852,31 @@ bool SrtWalker::EvaluateDescriptor(uint32_t source, DescriptorValue& result) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::span<const uint8_t> SrtWalker::FindActiveSources() {
|
||||
if (m_program.control_flow.empty()) {
|
||||
return {};
|
||||
}
|
||||
bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
|
||||
if (!m_program.srt_plan_complete) return false;
|
||||
const auto refresh = [&](uint32_t slot) {
|
||||
if (slot >= m_program.srt_reads.size()) return false;
|
||||
const auto& read = m_program.srt_reads[slot];
|
||||
const bool clean = read.flat_offset < m_clean_flat_slots.size() &&
|
||||
m_clean_flat_slots[read.flat_offset] != 0u;
|
||||
if (clean && (m_clean_evaluator == nullptr || m_runtime.read_specialization_memory == nullptr))
|
||||
return false;
|
||||
auto& evaluator = clean ? *m_clean_evaluator : *this;
|
||||
return read.flat_offset < flat.size() && evaluator.Evaluate(read.value, flat[read.flat_offset]);
|
||||
};
|
||||
auto& active = m_program.active_sources;
|
||||
if (m_program.control_flow.empty()) {
|
||||
active.clear();
|
||||
flat.resize(m_program.srt_reads.size());
|
||||
for (uint32_t slot = 0; slot < m_program.srt_reads.size(); ++slot) {
|
||||
if (!refresh(slot)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
flat.assign(m_program.srt_reads.size(), 0u);
|
||||
active.assign(m_program.descriptor_sources.size(), 1u);
|
||||
for (const auto& block: m_program.control_flow) {
|
||||
for (const auto source: block.sources) {
|
||||
active.at(source) = 0u;
|
||||
}
|
||||
for (const auto source: block.sources) active.at(source) = 0u;
|
||||
}
|
||||
auto& visited = m_program.visited_blocks;
|
||||
auto& pending = m_program.pending_blocks;
|
||||
@@ -863,41 +886,22 @@ std::span<const uint8_t> SrtWalker::FindActiveSources() {
|
||||
while (!pending.empty()) {
|
||||
const auto index = pending.back();
|
||||
pending.pop_back();
|
||||
if (visited.at(index)) {
|
||||
continue;
|
||||
}
|
||||
if (visited.at(index)) continue;
|
||||
visited[index] = 1u;
|
||||
const auto& block = m_program.control_flow[index];
|
||||
for (const auto source: block.sources) {
|
||||
active[source] = 1u;
|
||||
for (const auto source: block.sources) active[source] = 1u;
|
||||
for (const auto slot: block.srt_reads) {
|
||||
if (!refresh(slot)) return false;
|
||||
}
|
||||
uint32_t condition = 0;
|
||||
auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this;
|
||||
if (!block.condition.IsEmpty() && m_runtime.read_specialization_memory != nullptr &&
|
||||
Evaluate(block.condition, condition)) {
|
||||
predicate.Evaluate(block.condition, condition)) {
|
||||
pending.push_back(block.successors[condition != 0u ? 0u : 1u]);
|
||||
} else {
|
||||
pending.insert(pending.end(), block.successors.begin(), block.successors.end());
|
||||
}
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
|
||||
if (!m_program.srt_plan_complete) {
|
||||
return false;
|
||||
}
|
||||
flat.resize(m_program.srt_reads.size());
|
||||
for (const auto& read: m_program.srt_reads) {
|
||||
const bool clean = read.flat_offset < m_clean_flat_slots.size() &&
|
||||
m_clean_flat_slots[read.flat_offset] != 0u;
|
||||
if (clean && (m_clean_evaluator == nullptr || m_runtime.read_specialization_memory == nullptr)) {
|
||||
return false;
|
||||
}
|
||||
auto& evaluator = clean ? *m_clean_evaluator : *this;
|
||||
if (read.flat_offset >= flat.size() || !evaluator.Evaluate(read.value, flat[read.flat_offset])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,7 @@ public:
|
||||
|
||||
bool Evaluate(Value value, uint32_t& result);
|
||||
bool EvaluateDescriptor(uint32_t source, DescriptorValue& result);
|
||||
// An empty span means that all sources are active.
|
||||
std::span<const uint8_t> FindActiveSources();
|
||||
// Refreshes reachable scalar reads and active descriptor sources in one walk.
|
||||
bool RefreshFlatBuffer(std::vector<uint32_t>& flat);
|
||||
|
||||
private:
|
||||
|
||||
@@ -165,7 +165,6 @@ Value InitialValue(M0Tag) {
|
||||
|
||||
enum class ReadStep { Start, SetValue, PushPhiArgument };
|
||||
|
||||
template <typename T>
|
||||
struct ReadState {
|
||||
Block* block = nullptr;
|
||||
Value result;
|
||||
@@ -183,8 +182,8 @@ public:
|
||||
|
||||
template <typename T>
|
||||
Value Read(T variable, Block* root) {
|
||||
std::vector<ReadState<T>> stack {{}, {.block = root}};
|
||||
const auto prepare_phi = [&]() {
|
||||
stack.assign({{}, {.block = root}});
|
||||
const auto prepare_phi = [&]() {
|
||||
auto& state = stack.back();
|
||||
const auto predecessors = state.block->ImmPredecessors();
|
||||
if (state.pred == predecessors.size()) {
|
||||
@@ -290,6 +289,7 @@ private:
|
||||
|
||||
std::unordered_map<Block*, std::map<Variable, Inst*>> incomplete_phis;
|
||||
DefTable definitions;
|
||||
std::vector<ReadState> stack;
|
||||
};
|
||||
|
||||
void VisitInstruction(Pass& pass, Block* block, Inst& inst) {
|
||||
|
||||
@@ -41,44 +41,33 @@
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
struct ShaderBinaryInfo {
|
||||
uint8_t signature[7];
|
||||
uint8_t version;
|
||||
uint32_t pssl_or_cg : 1;
|
||||
uint32_t cached : 1;
|
||||
uint32_t type : 4;
|
||||
uint32_t source_type : 2;
|
||||
uint32_t length : 24;
|
||||
uint8_t chunk_usage_base_offset_dw;
|
||||
uint8_t num_input_usage_slots;
|
||||
uint8_t is_srt : 1;
|
||||
uint8_t is_srt_used_info_valid : 1;
|
||||
uint8_t is_extended_usage_info : 1;
|
||||
uint8_t reserved2 : 5;
|
||||
uint8_t reserved3;
|
||||
uint32_t hash0;
|
||||
uint32_t hash1;
|
||||
uint32_t crc32;
|
||||
struct ShaderMapEntry {
|
||||
ShaderMappedData data;
|
||||
uint64_t hash;
|
||||
};
|
||||
|
||||
static std::unique_ptr<std::unordered_map<uint64_t, ShaderMappedData>> g_shader_map;
|
||||
static std::unique_ptr<std::unordered_map<uint64_t, ShaderMapEntry>> g_shader_map;
|
||||
static std::mutex g_shader_map_mutex;
|
||||
|
||||
void ShaderInit() {
|
||||
EXIT_IF(g_shader_map != nullptr);
|
||||
|
||||
g_shader_map = std::make_unique<std::unordered_map<uint64_t, ShaderMappedData>>();
|
||||
g_shader_map = std::make_unique<std::unordered_map<uint64_t, ShaderMapEntry>>();
|
||||
}
|
||||
|
||||
void ShaderMapUserData(uint64_t addr, const ShaderMappedData& data) {
|
||||
EXIT_IF(g_shader_map == nullptr);
|
||||
|
||||
if (addr == 0 || data.code_size_bytes == 0 || data.code_size_bytes % sizeof(uint32_t) != 0) {
|
||||
EXIT("ShaderMapUserData shader=0x%016" PRIx64 " has invalid AGC shader_size=0x%08" PRIx32 "\n",
|
||||
addr, data.code_size_bytes);
|
||||
}
|
||||
const auto hash = XXH3_64bits(reinterpret_cast<const void*>(addr), data.code_size_bytes);
|
||||
std::scoped_lock lock(g_shader_map_mutex);
|
||||
|
||||
(*g_shader_map)[addr] = data;
|
||||
(*g_shader_map)[addr] = {data, hash};
|
||||
}
|
||||
|
||||
static ShaderMappedData ShaderGetMappedData(uint64_t addr, const char* label) {
|
||||
static ShaderMapEntry ShaderGetMappedData(uint64_t addr, const char* label) {
|
||||
EXIT_IF(g_shader_map == nullptr);
|
||||
|
||||
std::scoped_lock lock(g_shader_map_mutex);
|
||||
@@ -90,37 +79,15 @@ static ShaderMappedData ShaderGetMappedData(uint64_t addr, const char* label) {
|
||||
EXIT("%s shader=0x%016" PRIx64 " is missing from ShaderMap\n", label, addr);
|
||||
}
|
||||
|
||||
static const ShaderBinaryInfo* GetBinaryInfo(const uint32_t* code) {
|
||||
EXIT_IF(code == nullptr);
|
||||
|
||||
if (code[0] == 0xBEEB03FF) {
|
||||
return reinterpret_cast<const ShaderBinaryInfo*>(code +
|
||||
static_cast<size_t>(code[1] + 1) * 2);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static uint64_t GetDeclaredShaderHash(uint64_t shader_addr) {
|
||||
const auto* header = GetBinaryInfo(reinterpret_cast<const uint32_t*>(shader_addr));
|
||||
return header != nullptr ? (static_cast<uint64_t>(header->hash1) << 32u) | header->hash0 : 0;
|
||||
}
|
||||
|
||||
static ShaderParams GetShaderParams(uint64_t shader_addr, const char* label, uint64_t declared_hash,
|
||||
static ShaderParams GetShaderParams(uint64_t shader_addr, uint64_t hash,
|
||||
std::span<const uint32_t> user_data,
|
||||
const ShaderMappedData& data, uint32_t user_data_base = 0) {
|
||||
if (data.code_size_bytes == 0 || data.code_size_bytes % sizeof(uint32_t) != 0) {
|
||||
EXIT("%s hash=0x%016" PRIx64 " shader=0x%016" PRIx64
|
||||
" has invalid AGC shader_size=0x%08" PRIx32 "\n",
|
||||
label, declared_hash, shader_addr, data.code_size_bytes);
|
||||
}
|
||||
const auto code_words = data.code_size_bytes / sizeof(uint32_t);
|
||||
const auto code = std::span {reinterpret_cast<const uint32_t*>(shader_addr), code_words};
|
||||
ShaderParams params {
|
||||
.code = code,
|
||||
.user_data_count = static_cast<uint32_t>(user_data.size()) + user_data_base,
|
||||
.hash = declared_hash != 0 ? declared_hash
|
||||
: XXH3_64bits(code.data(), code.size_bytes()),
|
||||
.hash = hash,
|
||||
};
|
||||
EXIT_IF(user_data.size() > HW::UserSgprInfo::SGPRS_MAX ||
|
||||
params.user_data_count > params.user_data.size());
|
||||
@@ -414,6 +381,8 @@ static void ShaderApplyAttribSemantics(ShaderVertexInputInfo& info,
|
||||
r.fields[1] = sharp[1];
|
||||
r.fields[2] = sharp[2];
|
||||
r.fields[3] = sharp[3];
|
||||
EXIT_NOT_IMPLEMENTED(r.AddTid());
|
||||
EXIT_NOT_IMPLEMENTED(r.SwizzleEnabled());
|
||||
if (format != Prospero::VertexAttribFormat::kInvalid) {
|
||||
const auto format_raw = static_cast<uint32_t>(format);
|
||||
const auto buffer_format = format_raw >> 2u;
|
||||
@@ -650,7 +619,8 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
|
||||
key.insert(key.end(), {mesh.wave_size, mesh.host_subgroup_size, mesh.lds_size_dwords,
|
||||
mesh.scratch_size_dwords, mesh.input_primitive,
|
||||
mesh.primitives_per_group, mesh.vertices_per_group,
|
||||
mesh.max_vertices, mesh.max_primitives, mesh.provoking_vertex});
|
||||
mesh.max_vertices, mesh.max_primitives, mesh.provoking_vertex,
|
||||
static_cast<uint32_t>(mesh.fast_launch)});
|
||||
}
|
||||
key.push_back(info.tess.input_control_points);
|
||||
if (info.tess.input_control_points != 0) {
|
||||
@@ -662,12 +632,9 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
|
||||
for (int i = 0; i < info.resources_num; i++) {
|
||||
const auto& resource = info.resources[i];
|
||||
const auto& destination = info.resources_dst[i];
|
||||
key.push_back(destination.register_start);
|
||||
key.push_back(destination.registers_num);
|
||||
key.push_back(destination.fetch_index);
|
||||
key.push_back(static_cast<uint32_t>(destination.attr_id));
|
||||
key.push_back(resource.fields[1] & 0xbfff0000u); // Stride and swizzle enable.
|
||||
key.push_back(resource.fields[3] & 0x3087ffffu); // Channels, format, OOB, and add TID.
|
||||
key.push_back(resource.fields[3] & 0x7ffffu); // Channels and format used by embedded fetch.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,11 +693,10 @@ void BuildStageStaticKey(const ShaderComputeInputInfo& info, std::vector<uint32_
|
||||
ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context& context,
|
||||
const HW::UserConfig& user_config, ShaderVertexInputInfo& info) {
|
||||
const auto& sh = context.GetShaderRegisters();
|
||||
const auto data = ShaderGetMappedData(regs.es_regs.data_addr, "ShaderGetInputInfoVS():");
|
||||
const auto [data, hash] = ShaderGetMappedData(regs.es_regs.data_addr, "ShaderGetInputInfoVS():");
|
||||
const bool merged = (context.GetShaderStages() & 0x20u) != 0;
|
||||
auto params = GetShaderParams(
|
||||
regs.es_regs.data_addr, "ShaderRecompiler VS",
|
||||
GetDeclaredShaderHash(regs.es_regs.data_addr),
|
||||
regs.es_regs.data_addr, hash,
|
||||
std::span<const uint32_t>(regs.gs_user_sgpr.value, regs.gs_regs.rsrc2.user_sgpr), data,
|
||||
merged ? 8u : 0u);
|
||||
if (!merged) {
|
||||
@@ -753,12 +719,14 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
|
||||
mesh.provoking_vertex = context.GetModeControl().provoking_vtx_last ? 2u : 0u;
|
||||
mesh.lds_size_dwords = static_cast<uint32_t>(regs.gs_regs.rsrc2.lds_size) * 128u;
|
||||
mesh.scratch_size_dwords = data.scratch_size_dwords;
|
||||
const auto fast_launch = (context.GetShaderStages() >> 19u) & 3u;
|
||||
EXIT_NOT_IMPLEMENTED(fast_launch > 1u);
|
||||
mesh.fast_launch = fast_launch != 0;
|
||||
if (data.type == Prospero::ShaderBinaryType::kGsFront) {
|
||||
EXIT_IF(regs.gs_regs.data_addr == 0);
|
||||
const auto back = ShaderGetMappedData(regs.gs_regs.data_addr, "ShaderGetInputInfoGS():");
|
||||
const auto [back, back_hash] = ShaderGetMappedData(regs.gs_regs.data_addr, "ShaderGetInputInfoGS():");
|
||||
const auto back_params =
|
||||
GetShaderParams(regs.gs_regs.data_addr, "ShaderRecompiler GS",
|
||||
GetDeclaredShaderHash(regs.gs_regs.data_addr), {}, back);
|
||||
GetShaderParams(regs.gs_regs.data_addr, back_hash, {}, back);
|
||||
params.back_code = back_params.code;
|
||||
params.user_data[0] = static_cast<uint32_t>(regs.gs_regs.user_data_addr);
|
||||
params.user_data[1] = static_cast<uint32_t>(regs.gs_regs.user_data_addr >> 32u);
|
||||
@@ -766,9 +734,20 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
|
||||
params.hash = XXH3_64bits(hashes, sizeof(hashes));
|
||||
mesh.scratch_size_dwords = std::max(mesh.scratch_size_dwords, back.scratch_size_dwords);
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(regs.gs_regs.rsrc1.gs_vgpr_component_count != 3u ||
|
||||
regs.gs_regs.rsrc2.es_vgpr_component_count != 3u);
|
||||
const auto& group = user_config.GetGeControl();
|
||||
if (mesh.fast_launch) {
|
||||
const auto& user_vgpr = user_config.GetGeUserVgprEn();
|
||||
EXIT_NOT_IMPLEMENTED(data.type != Prospero::ShaderBinaryType::kGs ||
|
||||
regs.gs_regs.rsrc1.gs_vgpr_component_count != 0u ||
|
||||
regs.gs_regs.rsrc2.es_vgpr_component_count != 1u ||
|
||||
user_config.GetPrimType() != Prospero::PrimitiveType::kPointList ||
|
||||
group.primitive_group_size != 1u || group.vertex_group_size != 1u ||
|
||||
user_vgpr.vgpr1 || user_vgpr.vgpr2 || user_vgpr.vgpr3 ||
|
||||
mesh.max_vertices != sh.m_vgtGsMaxVertOut);
|
||||
} else {
|
||||
EXIT_NOT_IMPLEMENTED(regs.gs_regs.rsrc1.gs_vgpr_component_count != 3u ||
|
||||
regs.gs_regs.rsrc2.es_vgpr_component_count != 3u);
|
||||
}
|
||||
if ((user_config.GetPrimType() != Prospero::PrimitiveType::kPointList &&
|
||||
user_config.GetPrimType() != Prospero::PrimitiveType::kLineList &&
|
||||
user_config.GetPrimType() != Prospero::PrimitiveType::kTriFan &&
|
||||
@@ -781,7 +760,8 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
|
||||
mesh.input_primitive, sh.m_vgtGsOutPrimType, sh.m_vgtGsMaxVertOut,
|
||||
group.primitive_group_size, group.vertex_group_size, mesh.max_vertices);
|
||||
}
|
||||
mesh.max_primitives = group.primitive_group_size * (sh.m_vgtGsMaxVertOut - 2u);
|
||||
mesh.max_primitives = mesh.fast_launch ? mesh.max_vertices :
|
||||
group.primitive_group_size * (sh.m_vgtGsMaxVertOut - 2u);
|
||||
mesh.primitives_per_group = std::min({static_cast<uint32_t>(group.primitive_group_size),
|
||||
mesh.InputPrimitiveCount(group.vertex_group_size),
|
||||
mesh.max_vertices / sh.m_vgtGsMaxVertOut});
|
||||
@@ -797,9 +777,9 @@ std::array<ShaderParams, 3>
|
||||
PrepareTessellationPrograms(const HW::VertexShaderInfo& regs, const HW::Context& context,
|
||||
std::array<ShaderVertexInputInfo, 3>& input_info) {
|
||||
const auto& sh = context.GetShaderRegisters();
|
||||
const auto local = ShaderGetMappedData(regs.ls_regs.data_addr, "ShaderGetInputInfoLS():");
|
||||
const auto control = ShaderGetMappedData(regs.hs_regs.data_addr, "ShaderGetInputInfoHS():");
|
||||
const auto evaluation = ShaderGetMappedData(regs.es_regs.data_addr, "ShaderGetInputInfoTES():");
|
||||
const auto [local, local_hash] = ShaderGetMappedData(regs.ls_regs.data_addr, "ShaderGetInputInfoLS():");
|
||||
const auto [control, control_hash] = ShaderGetMappedData(regs.hs_regs.data_addr, "ShaderGetInputInfoHS():");
|
||||
const auto [evaluation, evaluation_hash] = ShaderGetMappedData(regs.es_regs.data_addr, "ShaderGetInputInfoTES():");
|
||||
EXIT_NOT_IMPLEMENTED(local.type != Prospero::ShaderBinaryType::kHsFront ||
|
||||
control.type != Prospero::ShaderBinaryType::kHsBack ||
|
||||
evaluation.type != Prospero::ShaderBinaryType::kGs);
|
||||
@@ -810,12 +790,9 @@ PrepareTessellationPrograms(const HW::VertexShaderInfo& regs, const HW::Context&
|
||||
const auto local_users = std::span(regs.hs_user_sgpr.value, regs.hs_regs.rsrc2.user_sgpr);
|
||||
const auto evaluation_users = std::span(regs.gs_user_sgpr.value, regs.gs_regs.rsrc2.user_sgpr);
|
||||
std::array<ShaderParams, 3> params {
|
||||
GetShaderParams(regs.ls_regs.data_addr, "ShaderRecompiler LS",
|
||||
GetDeclaredShaderHash(regs.ls_regs.data_addr), local_users, local),
|
||||
GetShaderParams(regs.hs_regs.data_addr, "ShaderRecompiler HS",
|
||||
GetDeclaredShaderHash(regs.hs_regs.data_addr), local_users, control, 8u),
|
||||
GetShaderParams(regs.es_regs.data_addr, "ShaderRecompiler TES",
|
||||
GetDeclaredShaderHash(regs.es_regs.data_addr), evaluation_users,
|
||||
GetShaderParams(regs.ls_regs.data_addr, local_hash, local_users, local),
|
||||
GetShaderParams(regs.hs_regs.data_addr, control_hash, local_users, control, 8u),
|
||||
GetShaderParams(regs.es_regs.data_addr, evaluation_hash, evaluation_users,
|
||||
evaluation),
|
||||
};
|
||||
// The fused HS back half receives its separate user-data address in s0:s1.
|
||||
@@ -855,19 +832,19 @@ ShaderParams PrepareProgram(
|
||||
const HW::PixelShaderInfo& regs, const HW::ShaderRegisters& sh,
|
||||
std::span<const Prospero::ColorComponentMapping, 8> target_export_mapping,
|
||||
ShaderPixelInputInfo& ps_info) {
|
||||
const auto data = ShaderGetMappedData(regs.ps_regs.data_addr, "ShaderGetInputInfoPS():");
|
||||
const auto [data, hash] = ShaderGetMappedData(regs.ps_regs.data_addr, "ShaderGetInputInfoPS():");
|
||||
ShaderGetStaticInputInfoPS(regs, sh, target_export_mapping, data, ps_info);
|
||||
return GetShaderParams(
|
||||
regs.ps_regs.data_addr, "ShaderRecompiler PS", GetDeclaredShaderHash(regs.ps_regs.data_addr),
|
||||
regs.ps_regs.data_addr, hash,
|
||||
std::span<const uint32_t>(regs.ps_user_sgpr.value, regs.ps_regs.rsrc2.user_sgpr), data);
|
||||
}
|
||||
|
||||
ShaderParams PrepareProgram(const HW::ComputeShaderInfo& regs, const HW::ShaderRegisters& sh,
|
||||
ShaderComputeInputInfo& info) {
|
||||
const auto data = ShaderGetMappedData(regs.cs_regs.data_addr, "ShaderGetInputInfoCS():");
|
||||
const auto [data, hash] = ShaderGetMappedData(regs.cs_regs.data_addr, "ShaderGetInputInfoCS():");
|
||||
ShaderGetStaticInputInfoCS(regs, sh, data, info);
|
||||
return GetShaderParams(
|
||||
regs.cs_regs.data_addr, "ShaderRecompiler CS", GetDeclaredShaderHash(regs.cs_regs.data_addr),
|
||||
regs.cs_regs.data_addr, hash,
|
||||
std::span<const uint32_t>(regs.cs_user_sgpr.value, regs.cs_regs.user_sgpr), data);
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ struct ShaderMeshInputInfo: ShaderWorkgroupInputInfo {
|
||||
uint32_t max_vertices = 0;
|
||||
uint32_t max_primitives = 0;
|
||||
uint32_t provoking_vertex = 0;
|
||||
bool fast_launch = false;
|
||||
|
||||
[[nodiscard]] constexpr uint32_t InputPrimitiveSize() const {
|
||||
switch (static_cast<Prospero::PrimitiveType>(input_primitive)) {
|
||||
|
||||
@@ -72,6 +72,27 @@
|
||||
<property name="toolTip"><string>Language</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_controller_color">
|
||||
<property name="text"><string>DualSense lightbar:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<layout class="QHBoxLayout" name="controllerColorLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_controller_color">
|
||||
<property name="toolTip"><string>Choose and preview a fixed LED color. Bluetooth DualSense may take several seconds to update.</string></property>
|
||||
<property name="text"><string>Custom</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_controller_color_reset">
|
||||
<property name="toolTip"><string>Let the game control the lightbar</string></property>
|
||||
<property name="text"><string>Default</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QChar>
|
||||
#include <QColor>
|
||||
#include <QMetaEnum>
|
||||
#include <QMetaType>
|
||||
#include <QObject>
|
||||
@@ -88,6 +89,7 @@ public:
|
||||
QString user_name = "Kyty";
|
||||
int user_id = Config::DEFAULT_USER_ID;
|
||||
QString audio_input_device;
|
||||
QString controller_color;
|
||||
PresentMode present_mode = PresentMode::Mailbox;
|
||||
int gpu_index = -1;
|
||||
bool fullscreen_enabled = false;
|
||||
@@ -119,6 +121,7 @@ public:
|
||||
user_name = other.user_name;
|
||||
user_id = other.user_id;
|
||||
audio_input_device = other.audio_input_device;
|
||||
controller_color = other.controller_color;
|
||||
present_mode = other.present_mode;
|
||||
gpu_index = other.gpu_index;
|
||||
fullscreen_enabled = other.fullscreen_enabled;
|
||||
@@ -165,6 +168,7 @@ public:
|
||||
KYTY_CFG_SET(user_name);
|
||||
KYTY_CFG_SET(user_id);
|
||||
KYTY_CFG_SET(audio_input_device);
|
||||
KYTY_CFG_SET(controller_color);
|
||||
KYTY_CFG_SET(present_mode);
|
||||
KYTY_CFG_SET(gpu_index);
|
||||
KYTY_CFG_SET(fullscreen_enabled);
|
||||
@@ -204,6 +208,8 @@ public:
|
||||
? saved_user_id
|
||||
: Config::DEFAULT_USER_ID;
|
||||
audio_input_device = s->value("audio_input_device", audio_input_device).toString();
|
||||
const QColor color(s->value("controller_color", controller_color).toString());
|
||||
controller_color = color.isValid() ? color.name(QColor::HexRgb) : QString {};
|
||||
KYTY_CFG_GET(present_mode);
|
||||
gpu_index = s->value("gpu_index", -1).toInt();
|
||||
if (EnumToText(present_mode).isEmpty()) {
|
||||
|
||||
@@ -31,6 +31,9 @@ public:
|
||||
void SetGameDirectories(const QStringList& dirs);
|
||||
[[nodiscard]] QStringList GetGameDirectories() const;
|
||||
|
||||
signals:
|
||||
void PreviewControllerColor(const QString& color);
|
||||
|
||||
private:
|
||||
Ui::ConfigurationEditDialog* m_ui = nullptr;
|
||||
Configuration& m_info;
|
||||
|
||||
@@ -33,6 +33,9 @@ public:
|
||||
ConfigurationItem* GetSelectedItem() { return m_selected_item; }
|
||||
|
||||
[[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; }
|
||||
[[nodiscard]] const QString& GetGlobalControllerColor() const {
|
||||
return m_global_info.controller_color;
|
||||
}
|
||||
[[nodiscard]] std::unique_ptr<Configuration>
|
||||
CreateConfiguration(const ConfigurationItem& item) const;
|
||||
|
||||
@@ -44,6 +47,7 @@ signals:
|
||||
|
||||
void Run();
|
||||
void Select();
|
||||
void PreviewControllerColor(const QString& color);
|
||||
|
||||
protected:
|
||||
void changeEvent(QEvent* event) override;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef CONTROLLER_LIGHTBAR_H
|
||||
#define CONTROLLER_LIGHTBAR_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
class ControllerLightbar final: public QObject {
|
||||
public:
|
||||
explicit ControllerLightbar(QObject* parent = nullptr);
|
||||
~ControllerLightbar() override;
|
||||
|
||||
void SetColor(const QString& color);
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
void Refresh();
|
||||
void ApplyColor(SDL_Gamepad* gamepad) const;
|
||||
|
||||
QTimer m_timer;
|
||||
QHash<SDL_JoystickID, SDL_Gamepad*> m_gamepads;
|
||||
QColor m_color;
|
||||
};
|
||||
|
||||
#endif // CONTROLLER_LIGHTBAR_H
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QCheckBox>
|
||||
#include <QColorDialog>
|
||||
#include <QComboBox>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
@@ -40,6 +41,22 @@ constexpr char SETTINGS_CFG_DIALOG[] = "ConfigurationEditDialog";
|
||||
constexpr char SETTINGS_CFG_LAST_GEOMETRY[] = "geometry";
|
||||
constexpr int GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH = 560;
|
||||
|
||||
static void UpdateControllerColorButton(QPushButton* button, const QString& hex) {
|
||||
const QColor color(hex);
|
||||
if (!color.isValid()) {
|
||||
button->setProperty("controllerColor", QString {});
|
||||
button->setText(QObject::tr("Custom"));
|
||||
button->setStyleSheet({});
|
||||
return;
|
||||
}
|
||||
const auto normalized = color.name(QColor::HexRgb);
|
||||
button->setProperty("controllerColor", normalized);
|
||||
button->setText(normalized.toUpper());
|
||||
button->setStyleSheet(QStringLiteral("background-color: %1; color: %2;")
|
||||
.arg(normalized, color.lightness() < 128 ? QStringLiteral("white")
|
||||
: QStringLiteral("black")));
|
||||
}
|
||||
|
||||
const QStringList CONSOLE_LANGUAGE_NAMES = {
|
||||
"Japanese",
|
||||
"English (United States)",
|
||||
@@ -110,6 +127,21 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
|
||||
connect(m_ui->ok_button, &QPushButton::clicked, this, &ConfigurationEditDialog::save);
|
||||
connect(m_ui->cancel_button, &QPushButton::clicked, this, &QDialog::reject);
|
||||
connect(m_ui->clear_button, &QPushButton::clicked, this, &ConfigurationEditDialog::clear);
|
||||
connect(m_ui->button_controller_color, &QPushButton::clicked, this, [this]() {
|
||||
const auto current = m_ui->button_controller_color->property("controllerColor").toString();
|
||||
const auto initial =
|
||||
current.isEmpty() ? QColor(QStringLiteral("#0070d1")) : QColor(current);
|
||||
const auto color = QColorDialog::getColor(initial, this, tr("DualSense lightbar color"));
|
||||
if (color.isValid()) {
|
||||
const auto hex = color.name(QColor::HexRgb);
|
||||
UpdateControllerColorButton(m_ui->button_controller_color, hex);
|
||||
emit PreviewControllerColor(hex);
|
||||
}
|
||||
});
|
||||
connect(m_ui->button_controller_color_reset, &QPushButton::clicked, this, [this]() {
|
||||
UpdateControllerColorButton(m_ui->button_controller_color, {});
|
||||
emit PreviewControllerColor({});
|
||||
});
|
||||
connect(m_ui->comboBox_shader_log_direction, &QComboBox::currentTextChanged, this,
|
||||
[this](const QString& text) {
|
||||
auto log = TextToEnum<Configuration::LogDirection>(text);
|
||||
@@ -169,6 +201,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
|
||||
m_ui->lineEdit_user_name->setMaxLength(static_cast<int>(Config::MAX_USER_NAME_LENGTH));
|
||||
m_ui->lineEdit_user_name->setText(info.user_name);
|
||||
m_ui->spinBox_user_id->setValue(info.user_id);
|
||||
UpdateControllerColorButton(m_ui->button_controller_color, info.controller_color);
|
||||
auto* microphone = m_ui->comboBox_audio_input_device;
|
||||
microphone->clear();
|
||||
microphone->addItem(tr("None"), QString {});
|
||||
@@ -370,6 +403,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) {
|
||||
info.user_name = ui.lineEdit_user_name->text().trimmed();
|
||||
info.user_id = ui.spinBox_user_id->value();
|
||||
info.audio_input_device = ui.comboBox_audio_input_device->currentData().toString();
|
||||
info.controller_color = ui.button_controller_color->property("controllerColor").toString();
|
||||
info.screen_resolution =
|
||||
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
|
||||
info.present_mode =
|
||||
@@ -427,6 +461,7 @@ void ConfigurationEditDialog::save() {
|
||||
void ConfigurationEditDialog::clear() {
|
||||
Configuration default_info;
|
||||
Init(default_info);
|
||||
emit PreviewControllerColor({});
|
||||
|
||||
if (m_show_game_dirs) {
|
||||
m_game_dirs_list->clear();
|
||||
|
||||
@@ -751,6 +751,8 @@ void ConfigurationListWidget::edit_configuration() {
|
||||
auto info = CreateConfiguration(*item);
|
||||
ConfigurationEditDialog dlg(*info, this);
|
||||
dlg.setWindowTitle(tr("Edit game settings"));
|
||||
connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this,
|
||||
&ConfigurationListWidget::PreviewControllerColor);
|
||||
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
info->custom_settings = true;
|
||||
@@ -761,6 +763,8 @@ void ConfigurationListWidget::edit_configuration() {
|
||||
WriteSettings();
|
||||
item->Update();
|
||||
SelectItem(item);
|
||||
} else {
|
||||
emit Select();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -788,6 +792,8 @@ void ConfigurationListWidget::edit_global_settings() {
|
||||
ConfigurationEditDialog dlg(info, this);
|
||||
dlg.setWindowTitle(tr("Global settings"));
|
||||
dlg.SetGameDirectories(m_game_dirs);
|
||||
connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this,
|
||||
&ConfigurationListWidget::PreviewControllerColor);
|
||||
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
m_global_info.CopyEmulatorSettingsFrom(info);
|
||||
@@ -799,6 +805,7 @@ void ConfigurationListWidget::edit_global_settings() {
|
||||
ScanGameDirectory();
|
||||
}
|
||||
}
|
||||
emit Select();
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::edit_input_mapping() {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "controllerLightbar.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
ControllerLightbar::ControllerLightbar(QObject* parent): QObject(parent) {
|
||||
m_timer.setInterval(100);
|
||||
connect(&m_timer, &QTimer::timeout, this, &ControllerLightbar::Refresh);
|
||||
}
|
||||
|
||||
ControllerLightbar::~ControllerLightbar() {
|
||||
Stop();
|
||||
}
|
||||
|
||||
void ControllerLightbar::SetColor(const QString& color) {
|
||||
const QColor parsed(color);
|
||||
if (!parsed.isValid()) {
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
if (m_color == parsed) {
|
||||
return;
|
||||
}
|
||||
if (!m_timer.isActive()) {
|
||||
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
||||
qWarning() << "Cannot initialize controller color preview:" << SDL_GetError();
|
||||
return;
|
||||
}
|
||||
m_timer.start();
|
||||
}
|
||||
m_color = parsed;
|
||||
for (auto* gamepad: m_gamepads) {
|
||||
ApplyColor(gamepad);
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void ControllerLightbar::Stop() {
|
||||
if (!m_timer.isActive()) {
|
||||
return;
|
||||
}
|
||||
m_timer.stop();
|
||||
for (auto* gamepad: m_gamepads) {
|
||||
SDL_CloseGamepad(gamepad);
|
||||
}
|
||||
m_gamepads.clear();
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
|
||||
m_color = {};
|
||||
}
|
||||
|
||||
void ControllerLightbar::Refresh() {
|
||||
// Bluetooth DualSense may defer an accepted LED command until its connection
|
||||
// animation ends. Polling events also delivers those deferred commands.
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED && !m_gamepads.contains(event.gdevice.which)) {
|
||||
auto* gamepad = SDL_OpenGamepad(event.gdevice.which);
|
||||
if (gamepad == nullptr) {
|
||||
qWarning() << "Cannot open controller color preview:" << SDL_GetError();
|
||||
continue;
|
||||
}
|
||||
if (SDL_GetBooleanProperty(SDL_GetGamepadProperties(gamepad),
|
||||
SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false)) {
|
||||
m_gamepads.insert(event.gdevice.which, gamepad);
|
||||
ApplyColor(gamepad);
|
||||
} else {
|
||||
SDL_CloseGamepad(gamepad);
|
||||
}
|
||||
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
if (auto* gamepad = m_gamepads.take(event.gdevice.which); gamepad != nullptr) {
|
||||
SDL_CloseGamepad(gamepad);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerLightbar::ApplyColor(SDL_Gamepad* gamepad) const {
|
||||
if (!SDL_SetGamepadLED(gamepad, static_cast<Uint8>(m_color.red()),
|
||||
static_cast<Uint8>(m_color.green()),
|
||||
static_cast<Uint8>(m_color.blue()))) {
|
||||
qWarning() << "Cannot set controller LED:" << SDL_GetError();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "configuration.h"
|
||||
#include "configurationItem.h"
|
||||
#include "configurationListWidget.h"
|
||||
#include "controllerLightbar.h"
|
||||
#include "gameContent.h"
|
||||
#include "patchesDialog.h"
|
||||
#include "updateChecker.h"
|
||||
@@ -88,6 +89,7 @@ private:
|
||||
Ui::MainDialog* m_ui = {nullptr};
|
||||
MainDialog* m_main_dialog = nullptr;
|
||||
UpdateChecker* m_update_checker = nullptr;
|
||||
ControllerLightbar m_lightbar;
|
||||
QString m_interpreter;
|
||||
|
||||
QProcess m_process;
|
||||
@@ -103,6 +105,7 @@ MainDialog::MainDialog(QWidget* parent): QDialog(parent), m_p(new MainDialogPriv
|
||||
}
|
||||
|
||||
MainDialogPrivate::~MainDialogPrivate() {
|
||||
m_process.disconnect(this);
|
||||
delete m_ui;
|
||||
}
|
||||
|
||||
@@ -121,6 +124,12 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
|
||||
connect(main_dialog, &MainDialog::Start, this, &MainDialogPrivate::FindInterpreter,
|
||||
Qt::QueuedConnection);
|
||||
connect(m_ui->widget, &ConfigurationListWidget::Select, this, &MainDialogPrivate::Update);
|
||||
connect(m_ui->widget, &ConfigurationListWidget::PreviewControllerColor, this,
|
||||
[this](const QString& color) {
|
||||
if (m_process.state() == QProcess::NotRunning) {
|
||||
m_lightbar.SetColor(color);
|
||||
}
|
||||
});
|
||||
connect(m_ui->widget, &ConfigurationListWidget::Run, this, &MainDialogPrivate::Run);
|
||||
connect(m_ui->check_updates_link, &QLabel::linkActivated, this,
|
||||
[this](const QString&) { m_update_checker->Check(true); });
|
||||
@@ -135,14 +144,14 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
|
||||
m_ui->widget->WriteSettings();
|
||||
});
|
||||
|
||||
connect(&m_process,
|
||||
static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
|
||||
[this](int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) {
|
||||
if (m_running_item != nullptr) {
|
||||
m_running_item->SetRunning(false);
|
||||
}
|
||||
Update();
|
||||
});
|
||||
connect(&m_process, &QProcess::stateChanged, this, [this](QProcess::ProcessState state) {
|
||||
if (state == QProcess::NotRunning) {
|
||||
if (m_running_item != nullptr) {
|
||||
m_running_item->SetRunning(false);
|
||||
}
|
||||
Update();
|
||||
}
|
||||
});
|
||||
|
||||
m_ui->label_settings_file->setText(tr("Settings file: ") + m_ui->widget->GetSettingsFile());
|
||||
|
||||
@@ -219,6 +228,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
if (!info.audio_input_device.isEmpty()) {
|
||||
args << "--mic" << info.audio_input_device;
|
||||
}
|
||||
if (!info.controller_color.isEmpty()) {
|
||||
args << "--controller-color" << info.controller_color;
|
||||
}
|
||||
args << "--present-mode" << EnumToText(info.present_mode);
|
||||
if (info.gpu_index >= 0) {
|
||||
args << "--gpu" << QString::number(info.gpu_index);
|
||||
@@ -493,6 +505,7 @@ void MainDialogPrivate::Run() {
|
||||
}
|
||||
|
||||
m_running_item->SetRunning(true);
|
||||
m_lightbar.Stop();
|
||||
|
||||
auto info = m_ui->widget->CreateConfiguration(*m_running_item);
|
||||
m_main_dialog->RunInterpreter(&m_process, *info);
|
||||
@@ -512,6 +525,12 @@ void MainDialogPrivate::Update() {
|
||||
}
|
||||
|
||||
m_ui->widget->SetRunEnabled(run_enabled);
|
||||
if (m_process.state() != QProcess::NotRunning) {
|
||||
m_lightbar.Stop();
|
||||
return;
|
||||
}
|
||||
m_lightbar.SetColor(item != nullptr ? m_ui->widget->CreateConfiguration(*item)->controller_color
|
||||
: m_ui->widget->GetGlobalControllerColor());
|
||||
}
|
||||
|
||||
#include "mainDialog.moc"
|
||||
+1
-1
@@ -1552,7 +1552,7 @@ int KYTY_SYSV_ABI AgcSuspendPoint() {
|
||||
PRINT_NAME();
|
||||
|
||||
EXIT_IF(g_renderer == nullptr);
|
||||
g_renderer->GetGpu().Done();
|
||||
g_renderer->GetGpu().SuspendPoint();
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
+94
-61
@@ -1,6 +1,5 @@
|
||||
#include "libs/audio.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include "common/assert.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/logging/log.h"
|
||||
@@ -12,7 +11,9 @@
|
||||
#include "libs/errno.h"
|
||||
#include "libs/libs.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <magic_enum.hpp>
|
||||
@@ -37,6 +38,24 @@ constexpr uint64_t AUDIO_OUT_TARGET_LATENCY_US = 40000;
|
||||
constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 0x1;
|
||||
constexpr uint32_t AUDIO_IN_GRAIN_MAX_ASYNC = 384;
|
||||
|
||||
struct OutputClock {
|
||||
uint64_t next_time = 0;
|
||||
uint64_t remainder = 0;
|
||||
|
||||
uint64_t Advance(uint64_t now, uint32_t frames, uint32_t rate) {
|
||||
// Preserve small scheduling errors; discard timing debt after a discontinuity.
|
||||
if (next_time == 0 || (now > next_time && now - next_time > 100000)) {
|
||||
next_time = now;
|
||||
remainder = 0;
|
||||
}
|
||||
const auto deadline = next_time;
|
||||
const auto duration = 1000000ULL * frames + remainder;
|
||||
next_time += duration / rate;
|
||||
remainder = duration % rate;
|
||||
return deadline;
|
||||
}
|
||||
};
|
||||
|
||||
static bool audio_out_port_type_is_valid(int type) {
|
||||
return (type >= AUDIO_OUT_PORT_TYPE_MAIN && type <= AUDIO_OUT_PORT_TYPE_PADSPK) ||
|
||||
type == AUDIO_OUT_PORT_TYPE_VIBRATION || type == AUDIO_OUT_PORT_TYPE_AUDIO3D ||
|
||||
@@ -98,17 +117,17 @@ public:
|
||||
|
||||
private:
|
||||
struct PortOut {
|
||||
bool used = false;
|
||||
int type = 0;
|
||||
uint32_t samples_num = 0;
|
||||
uint32_t freq = 0;
|
||||
Format format = Format::Unknown;
|
||||
uint64_t last_output_time = 0;
|
||||
bool queue_primed = false;
|
||||
int channels_num = 0;
|
||||
int volume[12] = {};
|
||||
bool used = false;
|
||||
int type = 0;
|
||||
uint32_t samples_num = 0;
|
||||
uint32_t freq = 0;
|
||||
Format format = Format::Unknown;
|
||||
OutputClock clock;
|
||||
bool queue_primed = false;
|
||||
int channels_num = 0;
|
||||
int volume[12] = {};
|
||||
|
||||
SDL_AudioStream* stream = nullptr;
|
||||
SDL_AudioStream* stream = nullptr;
|
||||
Controller::DualSenseHaptics::Stream* haptics = nullptr;
|
||||
};
|
||||
|
||||
@@ -376,10 +395,7 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
|
||||
: 2u;
|
||||
min_queued_size = prepared_size * std::clamp(buffers, 2u, 16u);
|
||||
const auto wait_start = LibKernel::KernelGetProcessTime();
|
||||
auto queued = SDL_GetAudioStreamQueued(port->stream);
|
||||
if (queued < static_cast<int>(prepared_size)) {
|
||||
port->queue_primed = false;
|
||||
}
|
||||
auto queued = SDL_GetAudioStreamQueued(port->stream);
|
||||
while (queued > static_cast<int>(min_queued_size)) {
|
||||
if (LibKernel::KernelGetProcessTime() - wait_start > 200000) {
|
||||
SDL_ClearAudioStream(port->stream);
|
||||
@@ -389,19 +405,20 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
|
||||
Common::Thread::SleepMicro(1000);
|
||||
queued = SDL_GetAudioStreamQueued(port->stream);
|
||||
}
|
||||
if (port->queue_primed) {
|
||||
const auto next_time = port->last_output_time + buffer_us;
|
||||
const auto now = LibKernel::KernelGetProcessTime();
|
||||
if (next_time > now) {
|
||||
Common::Thread::SleepMicro(next_time - now);
|
||||
}
|
||||
if (queued < static_cast<int>(prepared_size)) {
|
||||
port->queue_primed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!SDL_PutAudioStreamData(port->stream, prepared_data, static_cast<int>(prepared_size))) {
|
||||
LOGF("AudioOut: SDL_PutAudioStreamData failed: %s\n", SDL_GetError());
|
||||
port->queue_primed = false;
|
||||
return false;
|
||||
}
|
||||
// Unpaced priming must not advance the playback deadline into the future.
|
||||
if (!port->queue_primed) {
|
||||
port->clock = {};
|
||||
}
|
||||
if (blocking && !port->queue_primed &&
|
||||
SDL_GetAudioStreamQueued(port->stream) >= static_cast<int>(min_queued_size)) {
|
||||
port->queue_primed = true;
|
||||
@@ -411,18 +428,21 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
|
||||
}
|
||||
|
||||
Audio::Id Audio::AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format) {
|
||||
if (freq == 0) {
|
||||
return Id::Invalid();
|
||||
}
|
||||
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
for (int id = 0; id < OUT_PORTS_MAX; id++) {
|
||||
if (!m_out_ports[id].used) {
|
||||
auto& port = m_out_ports[id];
|
||||
|
||||
port.used = true;
|
||||
port.type = type;
|
||||
port.samples_num = samples_num;
|
||||
port.freq = freq;
|
||||
port.format = format;
|
||||
port.last_output_time = 0;
|
||||
port.used = true;
|
||||
port.type = type;
|
||||
port.samples_num = samples_num;
|
||||
port.freq = freq;
|
||||
port.format = format;
|
||||
|
||||
switch (format) {
|
||||
case Format::Signed16bitMono:
|
||||
@@ -524,61 +544,68 @@ bool Audio::AudioOutSetVolume(Id handle, uint32_t bitflag, const int* volume) {
|
||||
}
|
||||
|
||||
uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking) {
|
||||
EXIT_NOT_IMPLEMENTED(num == 0);
|
||||
EXIT_NOT_IMPLEMENTED(num == 0 || num > OUT_PORTS_MAX);
|
||||
EXIT_NOT_IMPLEMENTED(!AudioOutValid(params[0].handle));
|
||||
|
||||
const auto& first_port = m_out_ports[params[0].handle.GetId()];
|
||||
|
||||
uint64_t block_time = (1000000 * first_port.samples_num) / first_port.freq;
|
||||
uint64_t current_time = LibKernel::KernelGetProcessTime();
|
||||
|
||||
uint64_t max_wait_time = 0;
|
||||
|
||||
for (uint32_t i = 0; i < num; i++) {
|
||||
uint64_t next_time = m_out_ports[params[i].handle.GetId()].last_output_time + block_time;
|
||||
uint64_t wait_time = (next_time > current_time ? next_time - current_time : 0);
|
||||
max_wait_time = (wait_time > max_wait_time ? wait_time : max_wait_time);
|
||||
}
|
||||
|
||||
bool any_port_has_device = false;
|
||||
for (uint32_t i = 0; i < num; i++) {
|
||||
if (m_out_ports[params[i].handle.GetId()].stream != nullptr) {
|
||||
any_port_has_device = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
const auto& first_port = m_out_ports[params[0].handle.GetId()];
|
||||
std::array<bool, OUT_PORTS_MAX> paced {};
|
||||
bool any_device = false;
|
||||
uint64_t deadline = 0;
|
||||
|
||||
for (uint32_t i = 0; i < num; i++) {
|
||||
auto& port = m_out_ports[params[i].handle.GetId()];
|
||||
|
||||
uint64_t controller_queued_us = 0;
|
||||
bool controller_uses_bluetooth = false;
|
||||
if (port.haptics != nullptr) {
|
||||
// Keep the stream alive against close and volume changes.
|
||||
Common::LockGuard lock(m_mutex);
|
||||
controller_queued_us = Controller::DualSenseHaptics::Queue(
|
||||
port.haptics, Controller::GetActiveControllerId(), params[i].data, port.samples_num,
|
||||
static_cast<uint32_t>(port.channels_num), FormatIsFloat(port.format), port.volume);
|
||||
controller_uses_bluetooth =
|
||||
Controller::DualSenseHaptics::UsesBluetooth(port.haptics);
|
||||
}
|
||||
if (controller_queued_us == 0) {
|
||||
// No DualSense took it (e.g. it was unplugged); a pad speaker port plays on the main
|
||||
// output instead, and a vibration port has none.
|
||||
QueueSdlAudio(&port, params[i].data, blocking);
|
||||
} else if (blocking && port.type == AUDIO_OUT_PORT_TYPE_PADSPK &&
|
||||
controller_queued_us > AUDIO_OUT_TARGET_LATENCY_US) {
|
||||
// Vibration never paces output; the speaker is audible, so pace it like other ports.
|
||||
Common::Thread::SleepMicro(controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US);
|
||||
if (QueueSdlAudio(&port, params[i].data, blocking)) {
|
||||
any_device = true;
|
||||
paced[i] = port.queue_primed;
|
||||
}
|
||||
} else {
|
||||
port.queue_primed = false;
|
||||
// Bluetooth HID has its own sender. Let the sample clock pace a batch
|
||||
// without another audio device, instead of waiting on the HID queue.
|
||||
if (port.type == AUDIO_OUT_PORT_TYPE_PADSPK && !controller_uses_bluetooth) {
|
||||
any_device = true;
|
||||
if (blocking && controller_queued_us > AUDIO_OUT_TARGET_LATENCY_US) {
|
||||
// Limit a stalled USB endpoint to one audio block of waiting.
|
||||
const uint64_t block_us = 1000000ULL * port.samples_num / port.freq;
|
||||
deadline = std::max(deadline, LibKernel::KernelGetProcessTime() +
|
||||
std::min(controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US,
|
||||
block_us));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the whole batch before pacing it. Audible devices own its cadence while
|
||||
// priming; silent/vibration-only batches use the same sample clock as SDL output.
|
||||
const auto now = LibKernel::KernelGetProcessTime();
|
||||
for (uint32_t i = 0; i < num; i++) {
|
||||
m_out_ports[params[i].handle.GetId()].last_output_time = LibKernel::KernelGetProcessTime();
|
||||
auto& port = m_out_ports[params[i].handle.GetId()];
|
||||
if (blocking && (paced[i] || !any_device)) {
|
||||
deadline = std::max(deadline, port.clock.Advance(now, port.samples_num, port.freq));
|
||||
} else {
|
||||
port.clock = {};
|
||||
}
|
||||
if (!blocking) {
|
||||
port.queue_primed = false;
|
||||
}
|
||||
}
|
||||
if (deadline > now) {
|
||||
Common::Thread::SleepMicro(deadline - now);
|
||||
}
|
||||
|
||||
return first_port.samples_num;
|
||||
@@ -826,6 +853,9 @@ int KYTY_SYSV_ABI AudioOutOpen(int user_id, int type, int index, uint32_t len, u
|
||||
if (!audio_out_port_type_is_valid(type)) {
|
||||
return AUDIO_OUT_ERROR_INVALID_PORT_TYPE;
|
||||
}
|
||||
if (freq == 0) {
|
||||
return AUDIO_OUT_ERROR_INVALID_SAMPLE_FREQ;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(index != 0);
|
||||
|
||||
Audio::Format format = Audio::Format::Unknown;
|
||||
@@ -935,6 +965,9 @@ int KYTY_SYSV_ABI AudioOutOutputs(AudioOutOutputParam* param, uint32_t num) {
|
||||
PRINT_NAME();
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(param == nullptr);
|
||||
if (num == 0 || num > Audio::OUT_PORTS_MAX) {
|
||||
return AUDIO_OUT_ERROR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
Audio::OutputParam params[Audio::OUT_PORTS_MAX];
|
||||
|
||||
|
||||
+68
-13
@@ -133,6 +133,9 @@ private:
|
||||
int m_connected_count = 0;
|
||||
bool m_motion_enabled = true;
|
||||
uint64_t m_gyro_time = 0;
|
||||
// Accelerometer gravity direction in the reset frame.
|
||||
std::array<float, 3> m_up_reference {0.0f, 1.0f, 0.0f};
|
||||
bool m_up_reference_valid = false;
|
||||
ControllerState m_state;
|
||||
ControllerState m_states[STATES_MAX];
|
||||
bool m_obtained[STATES_MAX] {};
|
||||
@@ -297,6 +300,9 @@ void GameController::Connect(int id) {
|
||||
if (id != HOST_INPUT_CONTROLLER_ID) {
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id));
|
||||
pad != nullptr) {
|
||||
if (const auto& color = Config::GetControllerColor()) {
|
||||
(void)SDL_SetGamepadLED(pad, (*color)[0], (*color)[1], (*color)[2]);
|
||||
}
|
||||
for (auto sensor: {SDL_SENSOR_ACCEL, SDL_SENSOR_GYRO}) {
|
||||
if (SDL_GamepadHasSensor(pad, sensor) &&
|
||||
!SDL_SetGamepadSensorEnabled(pad, sensor, true)) {
|
||||
@@ -345,6 +351,7 @@ void GameController::CheckActive() {
|
||||
m_connected = new_connected;
|
||||
m_state = {};
|
||||
m_gyro_time = 0;
|
||||
m_up_reference_valid = false;
|
||||
m_states_num = 0;
|
||||
m_first_state = 0;
|
||||
m_next_touch_id = 1;
|
||||
@@ -436,6 +443,29 @@ void GameController::TouchPad(int id, int finger, bool down, float x, float y) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using Vec3 = std::array<float, 3>;
|
||||
using Quat = std::array<float, 4>; // x, y, z, w
|
||||
|
||||
Vec3 QuatRotate(const Quat& q, const Vec3& v) {
|
||||
const Vec3 t {2.0f * (q[1] * v[2] - q[2] * v[1]), 2.0f * (q[2] * v[0] - q[0] * v[2]),
|
||||
2.0f * (q[0] * v[1] - q[1] * v[0])};
|
||||
return {v[0] + q[3] * t[0] + (q[1] * t[2] - q[2] * t[1]),
|
||||
v[1] + q[3] * t[1] + (q[2] * t[0] - q[0] * t[2]),
|
||||
v[2] + q[3] * t[2] + (q[0] * t[1] - q[1] * t[0])};
|
||||
}
|
||||
|
||||
Quat QuatConjugate(const Quat& q) {
|
||||
return {-q[0], -q[1], -q[2], q[3]};
|
||||
}
|
||||
|
||||
float Length(const Vec3& v) {
|
||||
return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void GameController::Motion(int id, Sensor sensor, const float* data, uint64_t time_us) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
if (id != m_active_id || !m_motion_enabled) {
|
||||
@@ -447,20 +477,38 @@ void GameController::Motion(int id, Sensor sensor, const float* data, uint64_t t
|
||||
for (int i = 0; i < 3; i++) {
|
||||
m_state.accel[i] = data[i] / SDL_STANDARD_GRAVITY;
|
||||
}
|
||||
// Capture gravity near 1 G to limit interference from linear acceleration.
|
||||
const float magnitude = Length(m_state.accel);
|
||||
if (!m_up_reference_valid && magnitude > 0.9f && magnitude < 1.1f) {
|
||||
const Vec3 up {m_state.accel[0] / magnitude, m_state.accel[1] / magnitude,
|
||||
m_state.accel[2] / magnitude};
|
||||
m_up_reference = QuatRotate(m_state.orientation, up);
|
||||
m_up_reference_valid = true;
|
||||
}
|
||||
} else {
|
||||
std::copy_n(data, 3, m_state.gyro.begin());
|
||||
// Mahony tilt correction; heading is unobservable and reported gyro values stay raw.
|
||||
auto rate = m_state.gyro;
|
||||
const float magnitude = Length(m_state.accel);
|
||||
if (m_up_reference_valid && magnitude > 0.8f && magnitude < 1.2f) {
|
||||
const Vec3 measured {m_state.accel[0] / magnitude, m_state.accel[1] / magnitude,
|
||||
m_state.accel[2] / magnitude};
|
||||
const Vec3 predicted = QuatRotate(QuatConjugate(m_state.orientation), m_up_reference);
|
||||
rate[0] += measured[1] * predicted[2] - measured[2] * predicted[1];
|
||||
rate[1] += measured[2] * predicted[0] - measured[0] * predicted[2];
|
||||
rate[2] += measured[0] * predicted[1] - measured[1] * predicted[0];
|
||||
}
|
||||
// Do not extrapolate a single sample across lost reports (e.g. loss of window focus).
|
||||
constexpr uint64_t max_gyro_interval_us = 100000;
|
||||
if (m_gyro_time != 0 && time_us > m_gyro_time &&
|
||||
time_us - m_gyro_time <= max_gyro_interval_us) {
|
||||
const float dt = static_cast<float>(time_us - m_gyro_time) * 0.000001f;
|
||||
const float speed =
|
||||
std::sqrt(data[0] * data[0] + data[1] * data[1] + data[2] * data[2]);
|
||||
const float dt = static_cast<float>(time_us - m_gyro_time) * 0.000001f;
|
||||
const float speed = Length(rate);
|
||||
const float half_angle = speed * dt * 0.5f;
|
||||
const float scale = speed > 0.0f ? std::sin(half_angle) / speed : 0.0f;
|
||||
const float x = data[0] * scale;
|
||||
const float y = data[1] * scale;
|
||||
const float z = data[2] * scale;
|
||||
const float x = rate[0] * scale;
|
||||
const float y = rate[1] * scale;
|
||||
const float z = rate[2] * scale;
|
||||
const float w = std::cos(half_angle);
|
||||
const auto q = m_state.orientation;
|
||||
// Accumulate body-local rotation relative to connection / orientation reset.
|
||||
@@ -486,19 +534,21 @@ void GameController::Motion(int id, Sensor sensor, const float* data, uint64_t t
|
||||
void GameController::SetMotionSensorState(bool enable) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
if (m_motion_enabled != enable) {
|
||||
m_motion_enabled = enable;
|
||||
m_gyro_time = 0;
|
||||
m_state.accel = {0.0f, 1.0f, 0.0f};
|
||||
m_state.gyro = {};
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
m_motion_enabled = enable;
|
||||
m_gyro_time = 0;
|
||||
m_state.accel = {0.0f, 1.0f, 0.0f};
|
||||
m_state.gyro = {};
|
||||
m_up_reference_valid = false;
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
AddState();
|
||||
}
|
||||
}
|
||||
|
||||
void GameController::ResetOrientation() {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
m_state.orientation = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
m_gyro_time = 0;
|
||||
m_state.orientation = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
m_gyro_time = 0;
|
||||
m_up_reference_valid = false;
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
AddState();
|
||||
}
|
||||
@@ -581,6 +631,11 @@ int GameController::GetActiveControllerId() {
|
||||
|
||||
void GameController::SetLightBar(uint8_t r, uint8_t g, uint8_t b) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
if (const auto& color = Config::GetControllerColor()) {
|
||||
r = (*color)[0];
|
||||
g = (*color)[1];
|
||||
b = (*color)[2];
|
||||
}
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(m_active_id));
|
||||
pad != nullptr) {
|
||||
(void)SDL_SetGamepadLED(pad, r, g, b);
|
||||
|
||||
@@ -0,0 +1,467 @@
|
||||
#include "libs/dualSenseBluetooth.h"
|
||||
|
||||
#include "common/logging/log.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_hidapi.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <opus.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Libs::Controller::DualSenseBluetooth {
|
||||
|
||||
namespace {
|
||||
// Report 0x39 and its sub-packets are described by awalol/DS5Dongle (MIT):
|
||||
// https://github.com/awalol/DS5Dongle
|
||||
// Each report plays two 32-frame stereo haptic blocks at 3 kHz (21.333 ms).
|
||||
constexpr uint64_t PERIOD_NS = 64000000000ULL / 3000;
|
||||
constexpr int HAPTIC_RATE = 3000;
|
||||
constexpr int HAPTIC_REPORT_FLOATS = 2 * 32 * 2;
|
||||
// Two 480-frame Opus packets occupy the same 21.333 ms slot. Resampling
|
||||
// 48 kHz input to 45 kHz before encoding at 48 kHz compensates for playback.
|
||||
constexpr int SPEAKER_RATE = 45000;
|
||||
constexpr int OPUS_FRAMES = 480;
|
||||
constexpr int OPUS_BYTES = 200;
|
||||
constexpr size_t REPORT_BYTES = 547;
|
||||
constexpr int MAX_QUEUE_MS = 80;
|
||||
constexpr uint64_t COALESCE_NS = 1000000;
|
||||
|
||||
struct Session;
|
||||
std::mutex g_mutex;
|
||||
std::condition_variable g_wake;
|
||||
std::map<int, std::shared_ptr<Session>> g_sessions;
|
||||
SDL_Thread* g_thread = nullptr;
|
||||
bool g_stopping = false;
|
||||
void (*g_tick)() = nullptr;
|
||||
|
||||
template <typename Char>
|
||||
std::string Address(const Char* serial) {
|
||||
std::string address;
|
||||
if (serial == nullptr) {
|
||||
return address;
|
||||
}
|
||||
for (; *serial != 0; ++serial) {
|
||||
if (*serial >= '0' && *serial <= '9') {
|
||||
address.push_back(static_cast<char>(*serial));
|
||||
} else if (*serial >= 'a' && *serial <= 'f') {
|
||||
address.push_back(static_cast<char>(*serial));
|
||||
} else if (*serial >= 'A' && *serial <= 'F') {
|
||||
address.push_back(static_cast<char>(*serial - 'A' + 'a'));
|
||||
} else if (*serial != ':' && *serial != '-') {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return address.size() == 12 ? address : std::string {};
|
||||
}
|
||||
|
||||
SDL_hid_device* OpenDevice(int controller) {
|
||||
SDL_LockJoysticks();
|
||||
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller));
|
||||
const bool wireless = pad != nullptr && SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5 &&
|
||||
SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRELESS;
|
||||
const auto vendor = wireless ? SDL_GetGamepadVendor(pad) : 0;
|
||||
const auto product = wireless ? SDL_GetGamepadProduct(pad) : 0;
|
||||
const auto address = wireless ? Address(SDL_GetGamepadSerial(pad)) : std::string {};
|
||||
SDL_UnlockJoysticks();
|
||||
if (vendor != 0x054c || (product != 0x0ce6 && product != 0x0df2) || address.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* devices = SDL_hid_enumerate(vendor, product);
|
||||
std::string path;
|
||||
bool ambiguous = false;
|
||||
for (auto* device = devices; device != nullptr; device = device->next) {
|
||||
if (device->bus_type == SDL_HID_API_BUS_BLUETOOTH && device->path != nullptr &&
|
||||
Address(device->serial_number) == address &&
|
||||
(device->usage_page == 0 || (device->usage_page == 1 && device->usage == 5))) {
|
||||
if (!path.empty()) {
|
||||
ambiguous = true;
|
||||
break;
|
||||
}
|
||||
path = device->path;
|
||||
}
|
||||
}
|
||||
SDL_hid_free_enumeration(devices);
|
||||
return !ambiguous && !path.empty() ? SDL_hid_open_path(path.c_str()) : nullptr;
|
||||
}
|
||||
|
||||
void FinishReport(std::array<uint8_t, REPORT_BYTES>& report) {
|
||||
const uint8_t prefix = 0xa2;
|
||||
auto crc = SDL_crc32(0, &prefix, 1);
|
||||
crc = SDL_crc32(crc, report.data(), report.size() - 4);
|
||||
for (size_t i = 0; i < 4; i++) {
|
||||
report[report.size() - 4 + i] = static_cast<uint8_t>(crc >> (8 * i));
|
||||
}
|
||||
}
|
||||
|
||||
struct Session {
|
||||
SDL_hid_device* hid = nullptr;
|
||||
OpusEncoder* encoder = nullptr;
|
||||
std::vector<Stream*> streams;
|
||||
std::mutex io_mutex;
|
||||
std::atomic<bool> active {true};
|
||||
std::atomic<bool> failed {false};
|
||||
std::atomic<uint64_t> generation {0};
|
||||
uint64_t next_send = 0;
|
||||
uint64_t pending_deadline = 0;
|
||||
uint8_t sequence = 0;
|
||||
uint8_t counter = 0;
|
||||
|
||||
~Session() {
|
||||
if (encoder != nullptr) {
|
||||
opus_encoder_destroy(encoder);
|
||||
}
|
||||
if (hid != nullptr) {
|
||||
SDL_hid_close(hid);
|
||||
}
|
||||
}
|
||||
|
||||
bool CreateEncoder() {
|
||||
if (encoder != nullptr) {
|
||||
return true;
|
||||
}
|
||||
int error = OPUS_OK;
|
||||
encoder = opus_encoder_create(48000, 2, OPUS_APPLICATION_AUDIO, &error);
|
||||
if (encoder == nullptr || error != OPUS_OK) {
|
||||
return false;
|
||||
}
|
||||
if (opus_encoder_ctl(encoder, OPUS_SET_BITRATE(OPUS_BYTES * 8 * 100)) == OPUS_OK &&
|
||||
opus_encoder_ctl(encoder, OPUS_SET_VBR(0)) == OPUS_OK &&
|
||||
opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(0)) == OPUS_OK) {
|
||||
return true;
|
||||
}
|
||||
opus_encoder_destroy(encoder);
|
||||
encoder = nullptr;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
struct Stream {
|
||||
std::shared_ptr<Session> session;
|
||||
SDL_AudioStream* audio = nullptr;
|
||||
uint32_t freq = 0;
|
||||
bool speaker = false;
|
||||
};
|
||||
|
||||
namespace {
|
||||
template <size_t N>
|
||||
void Mix(SDL_AudioStream* stream, std::array<float, N>& mixed) {
|
||||
std::array<float, N> pcm {};
|
||||
const int bytes = SDL_GetAudioStreamData(stream, pcm.data(), sizeof(pcm));
|
||||
for (int i = 0; i < std::max(0, bytes) / static_cast<int>(sizeof(float)); i++) {
|
||||
mixed[i] += pcm[i];
|
||||
}
|
||||
}
|
||||
|
||||
struct PendingReport {
|
||||
std::shared_ptr<Session> session;
|
||||
uint64_t generation;
|
||||
std::array<uint8_t, REPORT_BYTES> data {};
|
||||
std::array<float, 2 * OPUS_FRAMES * 2> audio {};
|
||||
bool speaker = false;
|
||||
};
|
||||
|
||||
bool PrepareReport(Session& session, PendingReport& pending, uint64_t now) {
|
||||
bool has_data = false;
|
||||
bool ready = false;
|
||||
bool speaker_ready = false;
|
||||
for (const auto* stream: session.streams) {
|
||||
const bool available = SDL_GetAudioStreamAvailable(stream->audio) > 0;
|
||||
// Both payloads cover 64 / 3000 seconds. Measure input duration so SDL's
|
||||
// resampler lookahead does not make a complete block appear partial.
|
||||
const auto frames =
|
||||
(static_cast<uint64_t>(stream->freq) * (HAPTIC_REPORT_FLOATS / 2) + HAPTIC_RATE - 1) /
|
||||
HAPTIC_RATE;
|
||||
const bool full = available && SDL_GetAudioStreamQueued(stream->audio) >=
|
||||
static_cast<int>(frames * 2 * sizeof(float));
|
||||
has_data |= available;
|
||||
ready |= full;
|
||||
speaker_ready |= stream->speaker && full;
|
||||
pending.speaker |= stream->speaker;
|
||||
}
|
||||
if (!has_data) {
|
||||
session.pending_deadline = 0;
|
||||
return false;
|
||||
}
|
||||
if (!ready || (!speaker_ready && (pending.speaker || session.next_send == 0))) {
|
||||
// Accumulate short blocks for at most one report period. A full stream
|
||||
// must not wait on a partial peer; only briefly coalesce haptics with
|
||||
// speaker audio arriving in the same emulated batch.
|
||||
const auto deadline = now + (ready ? COALESCE_NS : PERIOD_NS);
|
||||
session.pending_deadline =
|
||||
session.pending_deadline == 0 ? deadline : std::min(session.pending_deadline, deadline);
|
||||
if (now < session.pending_deadline) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
session.pending_deadline = 0;
|
||||
std::array<float, HAPTIC_REPORT_FLOATS> haptics {};
|
||||
for (auto* stream: session.streams) {
|
||||
if (stream->speaker) {
|
||||
Mix(stream->audio, pending.audio);
|
||||
} else {
|
||||
Mix(stream->audio, haptics);
|
||||
}
|
||||
}
|
||||
auto& report = pending.data;
|
||||
report[0] = 0x39;
|
||||
report[1] = static_cast<uint8_t>(session.sequence << 4);
|
||||
session.sequence = (session.sequence + 1) & 0x0f;
|
||||
report[2] = 0x91;
|
||||
report[3] = 6;
|
||||
report[4] = 0x7e;
|
||||
std::fill_n(report.begin() + 5, 4, 48);
|
||||
session.counter += 2;
|
||||
report[9] = session.counter;
|
||||
report[10] = 0xd2;
|
||||
report[11] = 64;
|
||||
for (size_t i = 0; i < haptics.size(); i++) {
|
||||
const auto value = std::isfinite(haptics[i]) ? haptics[i] : 0.0f;
|
||||
report[12 + i] = static_cast<uint8_t>(
|
||||
static_cast<int>(std::lround(std::clamp(value, -1.0f, 1.0f) * 127)));
|
||||
}
|
||||
if (pending.speaker) {
|
||||
report[140] = 0xd3;
|
||||
report[141] = OPUS_BYTES;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EncodeReport(PendingReport& pending) {
|
||||
if (!pending.session->active || pending.session->failed) {
|
||||
return false;
|
||||
}
|
||||
if (pending.speaker) {
|
||||
for (auto& sample: pending.audio) {
|
||||
sample = std::isfinite(sample) ? std::clamp(sample, -1.0f, 1.0f) : 0.0f;
|
||||
}
|
||||
for (int packet = 0; packet < 2; packet++) {
|
||||
if (opus_encode_float(pending.session->encoder,
|
||||
pending.audio.data() + packet * OPUS_FRAMES * 2, OPUS_FRAMES,
|
||||
pending.data.data() + 142 + packet * OPUS_BYTES,
|
||||
OPUS_BYTES) != OPUS_BYTES) {
|
||||
pending.session->failed = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
FinishReport(pending.data);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<PendingReport> Prepare(uint64_t now) {
|
||||
std::vector<PendingReport> pending;
|
||||
{
|
||||
std::lock_guard lock(g_mutex);
|
||||
for (auto& [id, session]: g_sessions) {
|
||||
if (session->failed || now < session->next_send) {
|
||||
continue;
|
||||
}
|
||||
PendingReport report {session, session->generation.load()};
|
||||
if (PrepareReport(*session, report, now)) {
|
||||
pending.push_back(std::move(report));
|
||||
// Regain timing after a late HID write without sending reports in a
|
||||
// burst. Reset the cadence only on the first report.
|
||||
session->next_send = session->next_send == 0
|
||||
? now + PERIOD_NS
|
||||
: std::max(session->next_send + PERIOD_NS,
|
||||
now + PERIOD_NS / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Opus encoding can take milliseconds; the emulation audio thread must be able
|
||||
// to queue its next block while this work runs.
|
||||
pending.erase(std::remove_if(pending.begin(), pending.end(),
|
||||
[](PendingReport& report) { return !EncodeReport(report); }),
|
||||
pending.end());
|
||||
return pending;
|
||||
}
|
||||
|
||||
void Send(const std::vector<PendingReport>& pending) {
|
||||
for (const auto& report: pending) {
|
||||
auto& session = *report.session;
|
||||
std::lock_guard io_lock(session.io_mutex);
|
||||
if (!session.active || session.failed || report.generation != session.generation) {
|
||||
continue;
|
||||
}
|
||||
if (SDL_hid_write(session.hid, report.data.data(), report.data.size()) !=
|
||||
static_cast<int>(report.data.size())) {
|
||||
session.failed = true;
|
||||
LOGF("DualSenseBluetooth: audio HID write failed: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int SDLCALL Sender(void*) {
|
||||
std::unique_lock lock(g_mutex);
|
||||
while (!g_stopping) {
|
||||
auto tick = g_tick;
|
||||
lock.unlock();
|
||||
auto pending = Prepare(SDL_GetTicksNS());
|
||||
Send(pending);
|
||||
if (tick != nullptr) {
|
||||
tick();
|
||||
}
|
||||
lock.lock();
|
||||
if (g_sessions.empty()) {
|
||||
g_wake.wait(lock, [] { return g_stopping || !g_sessions.empty(); });
|
||||
} else {
|
||||
const auto now = SDL_GetTicksNS();
|
||||
uint64_t wait_ns = 10000000; // Check rumble expiry at most 10 ms late.
|
||||
for (const auto& [id, session]: g_sessions) {
|
||||
if (session->next_send > now) {
|
||||
wait_ns = std::min(wait_ns, session->next_send - now);
|
||||
}
|
||||
if (session->pending_deadline > now) {
|
||||
wait_ns = std::min(wait_ns, session->pending_deadline - now);
|
||||
}
|
||||
}
|
||||
g_wake.wait_for(lock, std::chrono::nanoseconds(wait_ns));
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Stream* Open(uint32_t freq, bool speaker, int controller, void (*tick)()) {
|
||||
if (freq == 0 || freq > 192000) {
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard lock(g_mutex);
|
||||
if (g_stopping) {
|
||||
return nullptr;
|
||||
}
|
||||
auto it = g_sessions.find(controller);
|
||||
std::shared_ptr<Session> session;
|
||||
if (it != g_sessions.end() && !it->second->failed) {
|
||||
session = it->second;
|
||||
} else {
|
||||
session = std::make_shared<Session>();
|
||||
session->hid = OpenDevice(controller);
|
||||
if (session->hid == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
if (speaker && !session->CreateEncoder()) {
|
||||
return nullptr;
|
||||
}
|
||||
const SDL_AudioSpec source {SDL_AUDIO_F32, 2, static_cast<int>(freq)};
|
||||
const SDL_AudioSpec target {SDL_AUDIO_F32, 2, speaker ? SPEAKER_RATE : HAPTIC_RATE};
|
||||
auto* audio = SDL_CreateAudioStream(&source, &target);
|
||||
if (audio == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
if (g_thread == nullptr) {
|
||||
g_thread = SDL_CreateThread(Sender, "DualSense Bluetooth audio", nullptr);
|
||||
if (g_thread == nullptr) {
|
||||
SDL_DestroyAudioStream(audio);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
g_tick = tick;
|
||||
auto* stream = new Stream {session, audio, freq, speaker};
|
||||
session->streams.push_back(stream);
|
||||
session->pending_deadline = 0;
|
||||
g_sessions[controller] = session;
|
||||
g_wake.notify_one();
|
||||
return stream;
|
||||
}
|
||||
|
||||
uint64_t Queue(Stream* stream, const float* stereo, uint32_t frames) {
|
||||
if (stream == nullptr || stereo == nullptr || frames == 0) {
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard lock(g_mutex);
|
||||
if (!stream->session->active || stream->session->failed) {
|
||||
return 0;
|
||||
}
|
||||
const uint32_t max_frames = stream->freq * MAX_QUEUE_MS / 1000;
|
||||
if (frames > max_frames) {
|
||||
stereo += static_cast<size_t>(frames - max_frames) * 2;
|
||||
frames = max_frames;
|
||||
}
|
||||
int queued = std::max(0, SDL_GetAudioStreamQueued(stream->audio));
|
||||
const int bytes = static_cast<int>(frames * 2 * sizeof(float));
|
||||
if (queued + bytes > static_cast<int>(max_frames * 2 * sizeof(float))) {
|
||||
// Discard complete old report slots, preserving the newest queued audio.
|
||||
// Clearing the entire stream here creates an audible gap after a brief
|
||||
// Windows Bluetooth write stall.
|
||||
std::array<float, 2 * OPUS_FRAMES * 2> discarded {};
|
||||
const int discard_bytes = stream->speaker
|
||||
? static_cast<int>(sizeof(discarded))
|
||||
: static_cast<int>(HAPTIC_REPORT_FLOATS * sizeof(float));
|
||||
const int target = static_cast<int>(max_frames * 2 * sizeof(float)) - bytes;
|
||||
while (queued > target) {
|
||||
if (SDL_GetAudioStreamData(stream->audio, discarded.data(), discard_bytes) <= 0) {
|
||||
SDL_ClearAudioStream(stream->audio);
|
||||
break;
|
||||
}
|
||||
queued = std::max(0, SDL_GetAudioStreamQueued(stream->audio));
|
||||
}
|
||||
}
|
||||
queued = std::max(0, SDL_GetAudioStreamQueued(stream->audio));
|
||||
if (!SDL_PutAudioStreamData(stream->audio, stereo, bytes)) {
|
||||
return 0;
|
||||
}
|
||||
g_wake.notify_one();
|
||||
return static_cast<uint64_t>(queued + bytes) * 1000000 / (stream->freq * 2 * sizeof(float));
|
||||
}
|
||||
|
||||
void Close(Stream* stream) {
|
||||
if (stream == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto session = stream->session;
|
||||
{
|
||||
std::lock_guard lock(g_mutex);
|
||||
auto& streams = session->streams;
|
||||
streams.erase(std::remove(streams.begin(), streams.end(), stream), streams.end());
|
||||
session->generation++;
|
||||
session->pending_deadline = 0;
|
||||
if (streams.empty()) {
|
||||
session->active = false;
|
||||
for (auto it = g_sessions.begin(); it != g_sessions.end(); ++it) {
|
||||
if (it->second == session) {
|
||||
g_sessions.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
SDL_DestroyAudioStream(stream->audio);
|
||||
delete stream;
|
||||
}
|
||||
// Drain a write already in flight before the caller changes speaker routing.
|
||||
std::lock_guard io_lock(session->io_mutex);
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
SDL_Thread* thread;
|
||||
{
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_stopping = true;
|
||||
for (auto& [id, session]: g_sessions) {
|
||||
session->active = false;
|
||||
}
|
||||
thread = g_thread;
|
||||
g_wake.notify_one();
|
||||
}
|
||||
if (thread != nullptr) {
|
||||
SDL_WaitThread(thread, nullptr);
|
||||
}
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_sessions.clear();
|
||||
g_thread = nullptr;
|
||||
g_tick = nullptr;
|
||||
g_stopping = false;
|
||||
}
|
||||
|
||||
} // namespace Libs::Controller::DualSenseBluetooth
|
||||
@@ -0,0 +1,14 @@
|
||||
#ifndef KYTY_LIBS_DUALSENSE_BLUETOOTH_H_
|
||||
#define KYTY_LIBS_DUALSENSE_BLUETOOTH_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Libs::Controller::DualSenseBluetooth {
|
||||
struct Stream;
|
||||
Stream* Open(uint32_t frequency, bool speaker, int controller, void (*tick)());
|
||||
uint64_t Queue(Stream* stream, const float* samples, uint32_t frames);
|
||||
void Close(Stream* stream);
|
||||
void Shutdown();
|
||||
} // namespace Libs::Controller::DualSenseBluetooth
|
||||
|
||||
#endif
|
||||
+140
-58
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "common/threads.h"
|
||||
#include "libs/dualSenseBluetooth.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
@@ -13,18 +14,20 @@
|
||||
namespace Libs::Controller::DualSenseHaptics {
|
||||
|
||||
struct Stream {
|
||||
uint32_t freq = 0;
|
||||
bool speaker = false;
|
||||
int controller = -1;
|
||||
SDL_AudioStream* sdl = nullptr;
|
||||
SDL_AudioDeviceID device = 0;
|
||||
uint64_t next_check = 0;
|
||||
std::vector<float> buffer;
|
||||
uint32_t freq = 0;
|
||||
bool speaker = false;
|
||||
int controller = -1;
|
||||
SDL_AudioStream* sdl = nullptr;
|
||||
DualSenseBluetooth::Stream* bluetooth = nullptr;
|
||||
SDL_AudioDeviceID device = 0;
|
||||
uint64_t next_check = 0;
|
||||
std::vector<float> buffer;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int FRAME_BYTES = 4 * sizeof(float);
|
||||
constexpr int FRAME_BYTES = 4 * sizeof(float);
|
||||
constexpr uint32_t MAX_QUEUE_MS = 80;
|
||||
Common::Mutex g_mutex;
|
||||
int g_controller = -1;
|
||||
int g_haptics_streams = 0;
|
||||
@@ -107,11 +110,22 @@ void SDLCALL UpdateRumble(void*, SDL_AudioStream*, int, int) {
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateBluetoothRumble() {
|
||||
UpdateRumble(nullptr, nullptr, 0, 0);
|
||||
}
|
||||
|
||||
void CloseDevice(Stream* stream) {
|
||||
const bool had_device = stream->sdl != nullptr || stream->bluetooth != nullptr;
|
||||
if (stream->bluetooth != nullptr) {
|
||||
DualSenseBluetooth::Close(stream->bluetooth);
|
||||
stream->bluetooth = nullptr;
|
||||
}
|
||||
if (stream->sdl != nullptr) {
|
||||
// Destroy outside g_mutex: SDL waits for any running get callback.
|
||||
SDL_DestroyAudioStream(stream->sdl);
|
||||
stream->sdl = nullptr;
|
||||
}
|
||||
if (had_device) {
|
||||
Common::LockGuard lock(g_mutex);
|
||||
if (!stream->speaker && --g_haptics_streams == 0 && g_haptics_until != 0) {
|
||||
g_haptics_until = 0;
|
||||
@@ -127,26 +141,34 @@ void CloseDevice(Stream* stream) {
|
||||
}
|
||||
}
|
||||
|
||||
bool IsWireless(int controller) {
|
||||
SDL_LockJoysticks();
|
||||
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller));
|
||||
const bool wireless =
|
||||
pad != nullptr && SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRELESS;
|
||||
SDL_UnlockJoysticks();
|
||||
return wireless;
|
||||
}
|
||||
|
||||
bool CanUseDevice(int controller) {
|
||||
// SDL exposes no portable association between a gamepad and its USB audio endpoint.
|
||||
// Require one wired DualSense; guessing with multiple pads could play on the wrong one.
|
||||
SDL_LockJoysticks();
|
||||
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller));
|
||||
bool usable =
|
||||
pad != nullptr && SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRED;
|
||||
int count = 0;
|
||||
auto* pads = SDL_GetGamepads(&count);
|
||||
int dualsense_count = 0;
|
||||
bool found = false;
|
||||
int count = 0;
|
||||
auto* pads = SDL_GetGamepads(&count);
|
||||
int wired_dualsense_count = 0;
|
||||
bool found = false;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (SDL_GetGamepadTypeForID(pads[i]) == SDL_GAMEPAD_TYPE_PS5) {
|
||||
dualsense_count++;
|
||||
auto* candidate = SDL_GetGamepadFromID(pads[i]);
|
||||
if (candidate != nullptr && SDL_GetGamepadTypeForID(pads[i]) == SDL_GAMEPAD_TYPE_PS5 &&
|
||||
SDL_GetGamepadConnectionState(candidate) == SDL_JOYSTICK_CONNECTION_WIRED) {
|
||||
wired_dualsense_count++;
|
||||
found |= pads[i] == static_cast<SDL_JoystickID>(controller);
|
||||
}
|
||||
}
|
||||
SDL_free(pads);
|
||||
SDL_UnlockJoysticks();
|
||||
return usable && found && dualsense_count == 1;
|
||||
return found && wired_dualsense_count == 1;
|
||||
}
|
||||
|
||||
SDL_AudioDeviceID FindDevice() {
|
||||
@@ -169,32 +191,42 @@ SDL_AudioDeviceID FindDevice() {
|
||||
return device;
|
||||
}
|
||||
|
||||
void RefreshDevice(Stream* stream, int controller) {
|
||||
if (stream->controller != controller) {
|
||||
void RefreshDevice(Stream* stream, int controller, bool wireless) {
|
||||
if (stream->controller != controller || (wireless && stream->sdl != nullptr) ||
|
||||
(!wireless && stream->bluetooth != nullptr)) {
|
||||
CloseDevice(stream);
|
||||
stream->controller = controller;
|
||||
stream->next_check = 0;
|
||||
}
|
||||
const auto now = SDL_GetTicks();
|
||||
if (now < stream->next_check) {
|
||||
if (stream->bluetooth != nullptr || now < stream->next_check) {
|
||||
return;
|
||||
}
|
||||
stream->next_check = now + 2000;
|
||||
const auto device = FindDevice();
|
||||
const auto device = wireless ? 0 : FindDevice();
|
||||
if (stream->device == device && stream->sdl != nullptr) {
|
||||
return;
|
||||
}
|
||||
CloseDevice(stream);
|
||||
stream->device = device;
|
||||
if (device == 0) {
|
||||
if (!wireless && device == 0) {
|
||||
return;
|
||||
}
|
||||
const SDL_AudioSpec desired {SDL_AUDIO_F32, 4, static_cast<int>(stream->freq)};
|
||||
auto* sdl = SDL_OpenAudioDeviceStream(device, &desired, UpdateRumble, nullptr);
|
||||
SDL_AudioSpec actual {};
|
||||
bool ready = sdl != nullptr &&
|
||||
SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl), &actual, nullptr) &&
|
||||
actual.channels == 4 && SDL_ResumeAudioStreamDevice(sdl);
|
||||
SDL_AudioStream* sdl = nullptr;
|
||||
DualSenseBluetooth::Stream* bluetooth = nullptr;
|
||||
bool ready;
|
||||
if (wireless) {
|
||||
bluetooth = DualSenseBluetooth::Open(stream->freq, stream->speaker, controller,
|
||||
UpdateBluetoothRumble);
|
||||
ready = bluetooth != nullptr;
|
||||
} else {
|
||||
const SDL_AudioSpec desired {SDL_AUDIO_F32, 4, static_cast<int>(stream->freq)};
|
||||
SDL_AudioSpec actual {};
|
||||
sdl = SDL_OpenAudioDeviceStream(device, &desired, UpdateRumble, nullptr);
|
||||
ready = sdl != nullptr &&
|
||||
SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl), &actual, nullptr) &&
|
||||
actual.channels == 4 && SDL_ResumeAudioStreamDevice(sdl);
|
||||
}
|
||||
if (ready) {
|
||||
Common::LockGuard lock(g_mutex);
|
||||
if (stream->speaker) {
|
||||
@@ -207,14 +239,56 @@ void RefreshDevice(Stream* stream, int controller) {
|
||||
}
|
||||
}
|
||||
if (ready) {
|
||||
stream->sdl = sdl;
|
||||
LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device));
|
||||
stream->sdl = sdl;
|
||||
stream->bluetooth = bluetooth;
|
||||
if (!wireless) {
|
||||
LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device));
|
||||
}
|
||||
} else {
|
||||
DualSenseBluetooth::Close(bluetooth);
|
||||
SDL_DestroyAudioStream(sdl);
|
||||
LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError());
|
||||
if (!wireless) {
|
||||
LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t QueueUsbAudio(Stream* stream, uint32_t frames) {
|
||||
auto queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl));
|
||||
const int bytes = static_cast<int>(frames * FRAME_BYTES);
|
||||
const int max_bytes =
|
||||
static_cast<int>(static_cast<uint64_t>(stream->freq) * FRAME_BYTES * MAX_QUEUE_MS / 1000);
|
||||
if (queued + bytes > max_bytes) {
|
||||
// Keep the newest sound after a brief device stall. Clearing the entire
|
||||
// stream produces a gap that is much longer than one audio block.
|
||||
SDL_AudioSpec target {};
|
||||
std::array<uint8_t, 16384> discarded {};
|
||||
if (SDL_GetAudioStreamFormat(stream->sdl, nullptr, &target)) {
|
||||
const int frame_bytes = SDL_AUDIO_FRAMESIZE(target);
|
||||
if (frame_bytes > 0) {
|
||||
const int chunk_frames =
|
||||
std::min<int>(static_cast<int>(discarded.size()) / frame_bytes,
|
||||
static_cast<int>((static_cast<uint64_t>(frames) * target.freq +
|
||||
stream->freq - 1) /
|
||||
stream->freq));
|
||||
while (queued + bytes > max_bytes && chunk_frames > 0 &&
|
||||
SDL_GetAudioStreamData(stream->sdl, discarded.data(),
|
||||
chunk_frames * frame_bytes) > 0) {
|
||||
queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (queued + bytes > max_bytes) {
|
||||
SDL_ClearAudioStream(stream->sdl);
|
||||
queued = 0;
|
||||
}
|
||||
}
|
||||
if (!SDL_PutAudioStreamData(stream->sdl, stream->buffer.data(), bytes)) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<uint64_t>(queued + bytes) * 1000000 / (stream->freq * FRAME_BYTES);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Stream* Open(uint32_t freq, bool speaker) {
|
||||
@@ -232,18 +306,30 @@ void Close(Stream* stream) {
|
||||
}
|
||||
}
|
||||
|
||||
bool UsesBluetooth(const Stream* stream) {
|
||||
return stream != nullptr && stream->bluetooth != nullptr;
|
||||
}
|
||||
|
||||
uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
|
||||
bool is_float, const int* volume) {
|
||||
if (stream == nullptr || data == nullptr || frames == 0 || channels == 0 || volume == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
if (!CanUseDevice(controller)) {
|
||||
const auto max_frames =
|
||||
static_cast<uint32_t>(static_cast<uint64_t>(stream->freq) * MAX_QUEUE_MS / 1000);
|
||||
const auto first_frame = frames > max_frames ? frames - max_frames : 0;
|
||||
frames -= first_frame;
|
||||
if (frames == 0) {
|
||||
return 0;
|
||||
}
|
||||
const bool wireless = IsWireless(controller);
|
||||
if (!wireless && !CanUseDevice(controller)) {
|
||||
CloseDevice(stream);
|
||||
stream->next_check = 0; // Look for the device as soon as a DualSense is active again.
|
||||
return 0;
|
||||
}
|
||||
RefreshDevice(stream, controller);
|
||||
if (stream->sdl == nullptr) {
|
||||
RefreshDevice(stream, controller, wireless);
|
||||
if (stream->sdl == nullptr && stream->bluetooth == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
if (stream->speaker) {
|
||||
@@ -257,47 +343,42 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
stream->buffer.assign(static_cast<size_t>(frames) * 4, 0.0f);
|
||||
const uint32_t output_channels = wireless ? 2 : 4;
|
||||
const uint32_t channel_offset = wireless || stream->speaker ? 0 : 2;
|
||||
stream->buffer.assign(static_cast<size_t>(frames) * output_channels, 0.0f);
|
||||
bool audible = false;
|
||||
for (uint32_t frame = 0; frame < frames; frame++) {
|
||||
for (uint32_t ch = 0; ch < 2; ch++) {
|
||||
const auto src_ch = channels == 1 ? 0 : ch;
|
||||
const auto index = static_cast<size_t>(frame) * channels + src_ch;
|
||||
const auto index = (static_cast<size_t>(first_frame) + frame) * channels + src_ch;
|
||||
float value = is_float ? static_cast<const float*>(data)[index]
|
||||
: static_cast<const int16_t*>(data)[index] / 32768.0f;
|
||||
value *= volume[src_ch] / 32768.0f;
|
||||
// The speaker plays the right front channel; the actuators play the back ones.
|
||||
stream->buffer[static_cast<size_t>(frame) * 4 + ch + (stream->speaker ? 0 : 2)] = value;
|
||||
audible |= std::fabs(value) > 1.0f / 1024;
|
||||
// USB places the speaker in front and the actuators in back; Bluetooth takes stereo.
|
||||
stream->buffer[static_cast<size_t>(frame) * output_channels + ch + channel_offset] =
|
||||
value;
|
||||
audible |= std::isfinite(value) && std::fabs(value) > 1.0f / 1024;
|
||||
}
|
||||
}
|
||||
auto queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl));
|
||||
if (queued > static_cast<int>(stream->freq * FRAME_BYTES * 80 / 1000)) {
|
||||
SDL_ClearAudioStream(stream->sdl);
|
||||
queued = 0;
|
||||
}
|
||||
const int bytes = static_cast<int>(frames * FRAME_BYTES);
|
||||
const auto queued_us =
|
||||
static_cast<uint64_t>(queued + bytes) * 1000000 / (stream->freq * FRAME_BYTES);
|
||||
wireless ? DualSenseBluetooth::Queue(stream->bluetooth, stream->buffer.data(), frames)
|
||||
: QueueUsbAudio(stream, frames);
|
||||
if (queued_us == 0) {
|
||||
CloseDevice(stream);
|
||||
stream->next_check = SDL_GetTicks() + 2000;
|
||||
return 0;
|
||||
}
|
||||
if (audible && !stream->speaker) {
|
||||
Common::LockGuard lock(g_mutex);
|
||||
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) !=
|
||||
SDL_GAMEPAD_TYPE_PS5) {
|
||||
return 0;
|
||||
}
|
||||
SelectController(controller);
|
||||
const auto playback_now = SDL_GetTicks();
|
||||
const bool starting = g_haptics_until <= playback_now;
|
||||
const auto now = SDL_GetTicks();
|
||||
const bool starting = g_haptics_until <= now;
|
||||
g_haptics_until =
|
||||
std::max(g_haptics_until, playback_now + std::max<uint64_t>(queued_us / 1000, 250));
|
||||
std::max(g_haptics_until, now + std::max<uint64_t>(queued_us / 1000, 250));
|
||||
if (starting) {
|
||||
ApplyRumble();
|
||||
}
|
||||
}
|
||||
if (!SDL_PutAudioStreamData(stream->sdl, stream->buffer.data(), bytes)) {
|
||||
CloseDevice(stream);
|
||||
return 0;
|
||||
}
|
||||
return queued_us;
|
||||
}
|
||||
|
||||
@@ -315,6 +396,7 @@ bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) {
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
DualSenseBluetooth::Shutdown();
|
||||
Common::LockGuard lock(g_mutex);
|
||||
SelectController(-1);
|
||||
SelectSpeaker(-1);
|
||||
|
||||
@@ -11,9 +11,11 @@ struct Stream;
|
||||
// ports on the actuators.
|
||||
Stream* Open(uint32_t freq, bool speaker);
|
||||
void Close(Stream* stream);
|
||||
// True while this stream is routed through the asynchronous Bluetooth HID sender.
|
||||
bool UsesBluetooth(const Stream* stream);
|
||||
// Returns how long the queued audio still plays, in microseconds, or 0 when no DualSense took it
|
||||
// (the active pad has no usable USB audio device or its speaker cannot be routed).
|
||||
// Multiple DualSenses or matching audio endpoints are ambiguous and are not selected.
|
||||
// (the active pad has no usable controller audio device or its speaker cannot be routed).
|
||||
// Ambiguous USB or Bluetooth endpoints are not selected.
|
||||
uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
|
||||
bool is_float, const int* volume);
|
||||
// Returns false for other gamepad types, which retain the normal rumble path.
|
||||
|
||||
@@ -1797,6 +1797,12 @@ int KYTY_SYSV_ABI KernelRtldThreadAtexitDecrement(uint64_t* /*c*/) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t KYTY_SYSV_ABI KernelGetAvailableCpumask() {
|
||||
PRINT_NAME();
|
||||
|
||||
return 0x1fff;
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int KernelGetCurrentCpu() {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -3340,6 +3346,7 @@ LIB_DEFINE(InitLibKernel_1) {
|
||||
LIB_FUNC("vYU8P9Td2Zo", KernelAioInitializeImpl);
|
||||
LIB_FUNC("WhCc1w3EhSI", LibKernel::KernelSetThreadAtexitReport);
|
||||
LIB_FUNC("WkwEd3N7w0Y", LibKernel::KernelInstallExceptionHandler);
|
||||
LIB_FUNC("La9uyZv4Kvw", LibKernel::KernelGetAvailableCpumask);
|
||||
LIB_FUNC("g0VTBxfJyu0", LibKernel::KernelGetCurrentCpu);
|
||||
LIB_FUNC("wzvqT4UqKX8", LibKernel::KernelLoadStartModule);
|
||||
LIB_FUNC("Xjoosiw+XPI", LibKernel::KernelUuidCreate);
|
||||
|
||||
@@ -3834,6 +3834,27 @@ namespace LibSharePlay {
|
||||
|
||||
LIB_VERSION("SharePlay", 1, "SharePlay", 1, 1);
|
||||
|
||||
constexpr int SHARE_PLAY_ERROR_INVALID_ARGS = -2129788927;
|
||||
constexpr int SHARE_PLAY_ERROR_ALREADY_INITIALIZED = -2129788925;
|
||||
constexpr int SHARE_PLAY_ERROR_NOT_INITIALIZED = -2129788924;
|
||||
|
||||
struct SharePlayConnectionInfoA {
|
||||
int32_t status;
|
||||
int32_t mode;
|
||||
char host_online_id[20];
|
||||
char visitor_online_id[20];
|
||||
uint64_t host_account_id;
|
||||
uint64_t visitor_account_id;
|
||||
int32_t host_user_id;
|
||||
int32_t visitor_user_id;
|
||||
};
|
||||
|
||||
static_assert(sizeof(SharePlayConnectionInfoA) == 72);
|
||||
static_assert(offsetof(SharePlayConnectionInfoA, host_account_id) == 48);
|
||||
static_assert(offsetof(SharePlayConnectionInfoA, host_user_id) == 64);
|
||||
|
||||
static bool g_share_play_initialized = false;
|
||||
|
||||
static int KYTY_SYSV_ABI SharePlayInitialize(void* heap, size_t heap_size) {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -3841,18 +3862,41 @@ static int KYTY_SYSV_ABI SharePlayInitialize(void* heap, size_t heap_size) {
|
||||
"\t heap_size = %" PRIu64 "\n",
|
||||
reinterpret_cast<uint64_t>(heap), static_cast<uint64_t>(heap_size));
|
||||
|
||||
if (g_share_play_initialized) {
|
||||
return SHARE_PLAY_ERROR_ALREADY_INITIALIZED;
|
||||
}
|
||||
if (heap != nullptr && heap_size < 6u * 1024u) {
|
||||
return SHARE_PLAY_ERROR_INVALID_ARGS;
|
||||
}
|
||||
g_share_play_initialized = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI SharePlayTerminate() {
|
||||
PRINT_NAME();
|
||||
|
||||
if (!g_share_play_initialized) {
|
||||
return SHARE_PLAY_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
g_share_play_initialized = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI SharePlayGetCurrentConnectionInfoA(SharePlayConnectionInfoA* info) {
|
||||
if (!g_share_play_initialized) {
|
||||
return SHARE_PLAY_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
if (info == nullptr) {
|
||||
return SHARE_PLAY_ERROR_INVALID_ARGS;
|
||||
}
|
||||
*info = {.status = 0, .host_user_id = -1, .visitor_user_id = -1};
|
||||
return 0;
|
||||
}
|
||||
|
||||
LIB_DEFINE(InitPlatform_1_SharePlay) {
|
||||
LIB_FUNC("isruqthpYcw", LibSharePlay::SharePlayInitialize);
|
||||
LIB_FUNC("UaLjloJinow", LibSharePlay::SharePlayTerminate);
|
||||
LIB_FUNC("+MCXJlWdi+s", LibSharePlay::SharePlayGetCurrentConnectionInfoA);
|
||||
}
|
||||
|
||||
} // namespace LibSharePlay
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "libs/libs.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#define STB_IMAGE_WRITE_STATIC
|
||||
#define STBIWDEF static inline
|
||||
#define STBI_WRITE_NO_STDIO
|
||||
#include "stb_image_write.h"
|
||||
|
||||
namespace Libs {
|
||||
|
||||
LIB_VERSION("PngEnc", 1, "PngEnc", 1, 1);
|
||||
|
||||
namespace PngEnc {
|
||||
|
||||
constexpr int32_t PNG_ENC_ERROR_INVALID_ADDR = -2140602111; // 0x80690101
|
||||
constexpr int32_t PNG_ENC_ERROR_INVALID_SIZE = -2140602110; // 0x80690102
|
||||
constexpr int32_t PNG_ENC_ERROR_INVALID_PARAM = -2140602109; // 0x80690103
|
||||
constexpr int32_t PNG_ENC_ERROR_INVALID_HANDLE = -2140602108; // 0x80690104
|
||||
constexpr int32_t PNG_ENC_ERROR_DATA_OVERFLOW = -2140602096; // 0x80690110
|
||||
constexpr int32_t PNG_ENC_ERROR_FATAL = -2140602080; // 0x80690120
|
||||
|
||||
constexpr uint32_t PNG_ENC_MAX_IMAGE_WIDTH = 1000000;
|
||||
constexpr uint32_t PNG_ENC_MAX_IMAGE_HEIGHT = 1000000;
|
||||
constexpr uint32_t PNG_ENC_MAX_FILTER_NUMBER = 4;
|
||||
constexpr uint16_t PNG_ENC_COLOR_SPACE_RGB = 3;
|
||||
constexpr uint16_t PNG_ENC_COLOR_SPACE_RGBA = 19;
|
||||
constexpr uint16_t PNG_ENC_PIXEL_FORMAT_R8G8B8A8 = 0;
|
||||
constexpr uint16_t PNG_ENC_PIXEL_FORMAT_B8G8R8A8 = 1;
|
||||
constexpr uint16_t PNG_ENC_FILTER_TYPE_SUB = 1;
|
||||
constexpr uint16_t PNG_ENC_FILTER_TYPE_UP = 2;
|
||||
constexpr uint16_t PNG_ENC_FILTER_TYPE_AVERAGE = 4;
|
||||
constexpr uint16_t PNG_ENC_FILTER_TYPE_PAETH = 8;
|
||||
|
||||
struct PngEncCreateParam {
|
||||
uint32_t this_size;
|
||||
uint32_t attribute;
|
||||
uint32_t max_image_width;
|
||||
uint32_t max_filter_number;
|
||||
};
|
||||
|
||||
struct PngEncEncodeParam {
|
||||
const uint8_t* image_mem_addr;
|
||||
uint8_t* png_mem_addr;
|
||||
uint32_t image_mem_size;
|
||||
uint32_t png_mem_size;
|
||||
uint32_t image_width;
|
||||
uint32_t image_height;
|
||||
uint32_t image_pitch;
|
||||
uint16_t pixel_format;
|
||||
uint16_t color_space;
|
||||
uint16_t bit_depth;
|
||||
uint16_t clut_number;
|
||||
uint16_t filter_type;
|
||||
uint16_t compression_level;
|
||||
};
|
||||
|
||||
struct PngEncOutputInfo {
|
||||
uint32_t data_size;
|
||||
uint32_t processed_height;
|
||||
};
|
||||
|
||||
static_assert(sizeof(PngEncCreateParam) == 16);
|
||||
static_assert(sizeof(PngEncEncodeParam) == 48);
|
||||
|
||||
struct PngEncContext {
|
||||
uint64_t magic;
|
||||
uint32_t max_image_width;
|
||||
uint32_t max_filter_number;
|
||||
};
|
||||
|
||||
constexpr uint64_t PNG_ENC_CONTEXT_MAGIC = 0x4b595459504e4745ull; // KYTYPNGE
|
||||
|
||||
struct PngEncWriter {
|
||||
uint8_t* dst;
|
||||
uint32_t capacity;
|
||||
uint32_t size;
|
||||
bool overflow;
|
||||
};
|
||||
|
||||
// stb_image_write keeps its compression level and filter choice in globals
|
||||
static std::mutex g_png_enc_mutex;
|
||||
|
||||
static void PngEncWrite(void* context, void* data, int size) {
|
||||
auto* writer = static_cast<PngEncWriter*>(context);
|
||||
|
||||
if (writer->overflow || size < 0 ||
|
||||
static_cast<uint64_t>(writer->size) + static_cast<uint32_t>(size) > writer->capacity) {
|
||||
writer->overflow = true;
|
||||
return;
|
||||
}
|
||||
|
||||
std::memcpy(writer->dst + writer->size, data, static_cast<size_t>(size));
|
||||
writer->size += static_cast<uint32_t>(size);
|
||||
}
|
||||
|
||||
static int PngEncForcedFilter(uint16_t filter_type) {
|
||||
if (filter_type == (PNG_ENC_FILTER_TYPE_SUB | PNG_ENC_FILTER_TYPE_UP |
|
||||
PNG_ENC_FILTER_TYPE_AVERAGE | PNG_ENC_FILTER_TYPE_PAETH)) {
|
||||
return -1;
|
||||
}
|
||||
// stb's adaptive mode cannot restrict itself to the guest's filter mask.
|
||||
if ((filter_type & PNG_ENC_FILTER_TYPE_SUB) != 0) {
|
||||
return 1;
|
||||
}
|
||||
if ((filter_type & PNG_ENC_FILTER_TYPE_UP) != 0) {
|
||||
return 2;
|
||||
}
|
||||
if ((filter_type & PNG_ENC_FILTER_TYPE_AVERAGE) != 0) {
|
||||
return 3;
|
||||
}
|
||||
if ((filter_type & PNG_ENC_FILTER_TYPE_PAETH) != 0) {
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t ValidateCreateParam(const PngEncCreateParam* param) {
|
||||
if (param == nullptr) {
|
||||
return PNG_ENC_ERROR_INVALID_ADDR;
|
||||
}
|
||||
|
||||
LOGF("\t this_size = %" PRIu32 "\n", param->this_size);
|
||||
LOGF("\t attribute = %" PRIu32 "\n", param->attribute);
|
||||
LOGF("\t max_image_width = %" PRIu32 "\n", param->max_image_width);
|
||||
LOGF("\t max_filter_number = %" PRIu32 "\n", param->max_filter_number);
|
||||
|
||||
if (param->this_size != sizeof(PngEncCreateParam) || param->attribute != 0 ||
|
||||
param->max_filter_number > PNG_ENC_MAX_FILTER_NUMBER) {
|
||||
return PNG_ENC_ERROR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
if (param->max_image_width == 0 || param->max_image_width > PNG_ENC_MAX_IMAGE_WIDTH) {
|
||||
return PNG_ENC_ERROR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI PngEncQueryMemorySize(const PngEncCreateParam* param) {
|
||||
PRINT_NAME();
|
||||
|
||||
if (int32_t result = ValidateCreateParam(param); result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return sizeof(PngEncContext);
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI PngEncCreate(const PngEncCreateParam* param, void* memory_address,
|
||||
uint32_t memory_size, void** handle) {
|
||||
PRINT_NAME();
|
||||
|
||||
LOGF("\t memory_address = %p\n", memory_address);
|
||||
LOGF("\t memory_size = %" PRIu32 "\n", memory_size);
|
||||
|
||||
if (int32_t result = ValidateCreateParam(param); result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (memory_address == nullptr || handle == nullptr) {
|
||||
return PNG_ENC_ERROR_INVALID_ADDR;
|
||||
}
|
||||
|
||||
if (memory_size < sizeof(PngEncContext)) {
|
||||
return PNG_ENC_ERROR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
auto* ctx = static_cast<PngEncContext*>(memory_address);
|
||||
ctx->magic = PNG_ENC_CONTEXT_MAGIC;
|
||||
ctx->max_image_width = param->max_image_width;
|
||||
ctx->max_filter_number = param->max_filter_number;
|
||||
*handle = ctx;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI PngEncEncode(void* handle, const PngEncEncodeParam* param,
|
||||
PngEncOutputInfo* output_info) {
|
||||
PRINT_NAME();
|
||||
|
||||
if (handle == nullptr) {
|
||||
return PNG_ENC_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
const auto* ctx = static_cast<const PngEncContext*>(handle);
|
||||
if (ctx->magic != PNG_ENC_CONTEXT_MAGIC) {
|
||||
return PNG_ENC_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
if (param == nullptr) {
|
||||
return PNG_ENC_ERROR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
LOGF("\t image_mem_addr = %p\n", static_cast<const void*>(param->image_mem_addr));
|
||||
LOGF("\t png_mem_addr = %p\n", static_cast<const void*>(param->png_mem_addr));
|
||||
LOGF("\t image_mem_size = %" PRIu32 "\n", param->image_mem_size);
|
||||
LOGF("\t png_mem_size = %" PRIu32 "\n", param->png_mem_size);
|
||||
LOGF("\t image_width = %" PRIu32 "\n", param->image_width);
|
||||
LOGF("\t image_height = %" PRIu32 "\n", param->image_height);
|
||||
LOGF("\t image_pitch = %" PRIu32 "\n", param->image_pitch);
|
||||
LOGF("\t pixel_format = %" PRIu16 "\n", param->pixel_format);
|
||||
LOGF("\t color_space = %" PRIu16 "\n", param->color_space);
|
||||
LOGF("\t bit_depth = %" PRIu16 "\n", param->bit_depth);
|
||||
LOGF("\t filter_type = %" PRIu16 "\n", param->filter_type);
|
||||
LOGF("\t compression_level = %" PRIu16 "\n", param->compression_level);
|
||||
|
||||
if (param->image_mem_addr == nullptr || param->png_mem_addr == nullptr ||
|
||||
(reinterpret_cast<uintptr_t>(param->image_mem_addr) & 3u) != 0) {
|
||||
return PNG_ENC_ERROR_INVALID_ADDR;
|
||||
}
|
||||
|
||||
if (param->pixel_format != PNG_ENC_PIXEL_FORMAT_R8G8B8A8 &&
|
||||
param->pixel_format != PNG_ENC_PIXEL_FORMAT_B8G8R8A8) {
|
||||
return PNG_ENC_ERROR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
if (param->color_space != PNG_ENC_COLOR_SPACE_RGB &&
|
||||
param->color_space != PNG_ENC_COLOR_SPACE_RGBA) {
|
||||
return PNG_ENC_ERROR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
if (param->bit_depth != 8 || param->clut_number != 0 || param->compression_level > 9) {
|
||||
return PNG_ENC_ERROR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
constexpr uint16_t filter_mask = PNG_ENC_FILTER_TYPE_SUB | PNG_ENC_FILTER_TYPE_UP |
|
||||
PNG_ENC_FILTER_TYPE_AVERAGE | PNG_ENC_FILTER_TYPE_PAETH;
|
||||
if ((param->filter_type & ~filter_mask) != 0 ||
|
||||
static_cast<uint32_t>(std::popcount(param->filter_type)) > ctx->max_filter_number) {
|
||||
return PNG_ENC_ERROR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
const uint32_t width = param->image_width;
|
||||
const uint32_t height = param->image_height;
|
||||
if (width == 0 || height == 0 || width > ctx->max_image_width ||
|
||||
width > PNG_ENC_MAX_IMAGE_WIDTH || height > PNG_ENC_MAX_IMAGE_HEIGHT) {
|
||||
return PNG_ENC_ERROR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
const int components = (param->color_space == PNG_ENC_COLOR_SPACE_RGBA ? 4 : 3);
|
||||
const uint32_t row_size = width * static_cast<uint32_t>(components);
|
||||
const uint32_t min_pitch = width * 4u;
|
||||
const uint32_t pitch = param->image_pitch;
|
||||
|
||||
// Leave room for deflate expansion and stb's signed-int buffer-capacity doubling.
|
||||
constexpr uint64_t max_filtered_size = std::numeric_limits<int>::max() / 2u;
|
||||
if (static_cast<uint64_t>(row_size + 1u) * height > max_filtered_size ||
|
||||
param->png_mem_size == 0 || pitch < min_pitch || (pitch & 3u) != 0 ||
|
||||
static_cast<uint64_t>(pitch) * height > param->image_mem_size) {
|
||||
return PNG_ENC_ERROR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
const bool bgr = (param->pixel_format == PNG_ENC_PIXEL_FORMAT_B8G8R8A8);
|
||||
std::unique_ptr<uint8_t[]> pixels(new (std::nothrow)
|
||||
uint8_t[static_cast<size_t>(row_size) * height]);
|
||||
if (pixels == nullptr) {
|
||||
return PNG_ENC_ERROR_FATAL;
|
||||
}
|
||||
|
||||
for (uint32_t y = 0; y < height; y++) {
|
||||
const auto* src = param->image_mem_addr + static_cast<size_t>(y) * pitch;
|
||||
auto* dst = pixels.get() + static_cast<size_t>(y) * row_size;
|
||||
|
||||
for (uint32_t x = 0; x < width; x++) {
|
||||
dst[0] = src[bgr ? 2 : 0];
|
||||
dst[1] = src[1];
|
||||
dst[2] = src[bgr ? 0 : 2];
|
||||
if (components == 4) {
|
||||
dst[3] = src[3];
|
||||
}
|
||||
|
||||
src += 4;
|
||||
dst += components;
|
||||
}
|
||||
}
|
||||
|
||||
PngEncWriter writer {param->png_mem_addr, param->png_mem_size, 0, false};
|
||||
|
||||
int ok = 0;
|
||||
{
|
||||
std::scoped_lock lock(g_png_enc_mutex);
|
||||
|
||||
stbi_write_png_compression_level = param->compression_level;
|
||||
stbi_write_force_png_filter = PngEncForcedFilter(param->filter_type);
|
||||
|
||||
ok = stbi_write_png_to_func(PngEncWrite, &writer, static_cast<int>(width),
|
||||
static_cast<int>(height), components, pixels.get(),
|
||||
static_cast<int>(row_size));
|
||||
}
|
||||
|
||||
if (output_info != nullptr) {
|
||||
output_info->data_size = (writer.overflow ? 0 : writer.size);
|
||||
output_info->processed_height = (ok != 0 && !writer.overflow ? height : 0);
|
||||
}
|
||||
|
||||
if (ok == 0) {
|
||||
LOGF("\t encode failed\n");
|
||||
return PNG_ENC_ERROR_FATAL;
|
||||
}
|
||||
|
||||
if (writer.overflow) {
|
||||
LOGF("\t output buffer too small\n");
|
||||
return PNG_ENC_ERROR_DATA_OVERFLOW;
|
||||
}
|
||||
|
||||
LOGF("\t data_size = %" PRIu32 "\n", writer.size);
|
||||
|
||||
return static_cast<int32_t>(writer.size);
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI PngEncDelete(void* handle) {
|
||||
PRINT_NAME();
|
||||
|
||||
if (handle == nullptr) {
|
||||
return PNG_ENC_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
auto* ctx = static_cast<PngEncContext*>(handle);
|
||||
if (ctx->magic != PNG_ENC_CONTEXT_MAGIC) {
|
||||
return PNG_ENC_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
ctx->magic = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace PngEnc
|
||||
|
||||
LIB_DEFINE(InitPngEnc_1) {
|
||||
PRINT_NAME_ENABLE(true);
|
||||
|
||||
LIB_FUNC("9030RnBDoh4", PngEnc::PngEncQueryMemorySize);
|
||||
LIB_FUNC("7aGTPfrqT9s", PngEnc::PngEncCreate);
|
||||
LIB_FUNC("xgDjJKpcyHo", PngEnc::PngEncEncode);
|
||||
LIB_FUNC("RUrWdwTWZy8", PngEnc::PngEncDelete);
|
||||
}
|
||||
|
||||
} // namespace Libs
|
||||
@@ -39,6 +39,8 @@ LIB_DEFINE(InitVideoOut_1) {
|
||||
LIB_FUNC("Mt4QHHkxkOc", VideoOut::VideoOutGetEventCount);
|
||||
LIB_FUNC("j6RaAUlaLv0", VideoOut::VideoOutWaitVblank);
|
||||
LIB_FUNC("utPrVdxio-8", VideoOut::VideoOutGetOutputStatus);
|
||||
LIB_FUNC("5tRaBjtdTzY", VideoOut::VideoOutVrrPegToFixedRate);
|
||||
LIB_FUNC("T4ucGB8CsnM", VideoOut::VideoOutVrrUnpegFromFixedRate);
|
||||
LIB_FUNC("+I4K03i3EL0", VideoOut::VideoOutInitializeOutputOptions);
|
||||
LIB_FUNC("Nv8c-Kb+DUM", VideoOut::VideoOutIsOutputSupported);
|
||||
LIB_FUNC("w0hLuNarQxY", VideoOut::VideoOutConfigureOutput);
|
||||
@@ -46,6 +48,7 @@ LIB_DEFINE(InitVideoOut_1) {
|
||||
LIB_FUNC("MCJ8SkzsQxY", VideoOut::VideoOutLatencyMeasureSetStartPoint);
|
||||
LIB_FUNC("DYhhWbJSeRg", VideoOut::VideoOutColorSettingsSetGamma);
|
||||
LIB_FUNC("pv9CI5VC+R0", VideoOut::VideoOutAdjustColor);
|
||||
LIB_FUNC("w7Ipp9Xl7hg", VideoOut::VideoOutAllowOutputResolutionWqhdDetection);
|
||||
}
|
||||
|
||||
} // namespace LibGen5
|
||||
|
||||
@@ -91,6 +91,7 @@ LIB_DEFINE(InitNet_1);
|
||||
LIB_DEFINE(InitPad_1);
|
||||
LIB_DEFINE(InitPlayGo_1);
|
||||
LIB_DEFINE(InitPngDec_1);
|
||||
LIB_DEFINE(InitPngEnc_1);
|
||||
LIB_DEFINE(InitPlatform_1);
|
||||
LIB_DEFINE(InitRudp_1);
|
||||
LIB_DEFINE(InitSaveData_1);
|
||||
@@ -129,6 +130,7 @@ void InitAll(Loader::SymbolDatabase* s) {
|
||||
InitPlayGo_1(s);
|
||||
LibPsml::InitPsml_1(s);
|
||||
InitPngDec_1(s);
|
||||
InitPngEnc_1(s);
|
||||
InitPlatform_1(s);
|
||||
InitRudp_1(s);
|
||||
LibRtc::InitRtc_1(s);
|
||||
|
||||
+29
-7
@@ -997,14 +997,31 @@ static int ConvertHostSocketError(int error) {
|
||||
case EAGAIN: posix_error = Posix::POSIX_EWOULDBLOCK; break;
|
||||
case EBADF: posix_error = Posix::POSIX_EBADF; break;
|
||||
case EFAULT: posix_error = Posix::POSIX_EFAULT; break;
|
||||
case EINTR: posix_error = Posix::POSIX_EINTR; break;
|
||||
case EINVAL: posix_error = Posix::POSIX_EINVAL; break;
|
||||
case EISCONN: posix_error = Posix::POSIX_EISCONN; break;
|
||||
case EMFILE: posix_error = Posix::POSIX_EMFILE; break;
|
||||
case EMSGSIZE: posix_error = Posix::POSIX_EMSGSIZE; break;
|
||||
case ENFILE: posix_error = Posix::POSIX_ENFILE; break;
|
||||
case ENOBUFS: posix_error = Posix::POSIX_ENOBUFS; break;
|
||||
case ENOMEM: posix_error = Posix::POSIX_ENOMEM; break;
|
||||
case ENETDOWN: posix_error = Posix::POSIX_ENETDOWN; break;
|
||||
case ENETRESET: posix_error = Posix::POSIX_ENETRESET; break;
|
||||
case ENETUNREACH: posix_error = Posix::POSIX_ENETUNREACH; break;
|
||||
case ENOTCONN: posix_error = Posix::POSIX_ENOTCONN; break;
|
||||
case ENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
|
||||
case EOPNOTSUPP: posix_error = Posix::POSIX_EOPNOTSUPP; break;
|
||||
case EPIPE: posix_error = Posix::POSIX_EPIPE; break;
|
||||
case EPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
|
||||
case ESHUTDOWN: posix_error = Posix::POSIX_ESHUTDOWN; break;
|
||||
case ETIMEDOUT: posix_error = Posix::POSIX_ETIMEDOUT; break;
|
||||
case ECONNABORTED: posix_error = Posix::POSIX_ECONNABORTED; break;
|
||||
case ECONNREFUSED: posix_error = Posix::POSIX_ECONNREFUSED; break;
|
||||
case ECONNRESET: posix_error = Posix::POSIX_ECONNRESET; break;
|
||||
case EDESTADDRREQ: posix_error = Posix::POSIX_EDESTADDRREQ; break;
|
||||
case EHOSTUNREACH: posix_error = Posix::POSIX_EHOSTUNREACH; break;
|
||||
case EINPROGRESS: posix_error = Posix::POSIX_EINPROGRESS; break;
|
||||
case EALREADY: posix_error = Posix::POSIX_EALREADY; break;
|
||||
default: break;
|
||||
}
|
||||
#endif
|
||||
@@ -2098,17 +2115,22 @@ int KYTY_SYSV_ABI Setsockopt(int s, int level, int optname, const void* optval,
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
constexpr int ORBIS_SO_NBIO = 0x1200;
|
||||
if (ConvertSocketOptionLevel(level) == SOL_SOCKET && optname == ORBIS_SO_NBIO &&
|
||||
optlen >= sizeof(int)) {
|
||||
u_long enabled = (*static_cast<const int*>(optval) != 0 ? 1 : 0);
|
||||
if (ioctlsocket(socket, FIONBIO, &enabled) == SOCKET_ERROR) {
|
||||
return SetHostSocketError();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const bool enabled = *static_cast<const int*>(optval) != 0;
|
||||
#if defined(_WIN32)
|
||||
u_long mode = enabled ? 1 : 0;
|
||||
const bool failed = ioctlsocket(socket, FIONBIO, &mode) == SOCKET_ERROR;
|
||||
#else
|
||||
const int flags = ::fcntl(socket, F_GETFL, 0);
|
||||
const bool failed =
|
||||
flags < 0 ||
|
||||
::fcntl(socket, F_SETFL, enabled ? (flags | O_NONBLOCK) : (flags & ~O_NONBLOCK)) != 0;
|
||||
#endif
|
||||
return failed ? SetHostSocketError() : 0;
|
||||
}
|
||||
#if !defined(_WIN32)
|
||||
// Guest TCP options: IPPROTO_TCP=6, TCP_NODELAY=1.
|
||||
if (level != 6 || optname != 1) {
|
||||
return SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
||||
|
||||
@@ -53,6 +53,7 @@ static void PrintUsage() {
|
||||
::printf(" --user-id <num> Local user ID. Default: %d.\n",
|
||||
Config::DEFAULT_USER_ID);
|
||||
::printf(" --mic <name> Capture from this microphone; omit for silence.\n");
|
||||
::printf(" --controller-color <#RRGGBB> Override the controller lightbar color.\n");
|
||||
::printf(
|
||||
" --present-mode <value> Fifo, Mailbox, or Immediate. Default: Mailbox.\n");
|
||||
::printf(
|
||||
@@ -145,6 +146,20 @@ static bool ParseUint32(const std::string& value, uint32_t& out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ParseControllerColor(const std::string& value, Config::ControllerColor& out) {
|
||||
if (value.size() != 7 || value[0] != '#') {
|
||||
return false;
|
||||
}
|
||||
uint32_t rgb = 0;
|
||||
auto [end, error] = std::from_chars(value.data() + 1, value.data() + value.size(), rgb, 16);
|
||||
if (error != std::errc {} || end != value.data() + value.size()) {
|
||||
return false;
|
||||
}
|
||||
out = {static_cast<uint8_t>(rgb >> 16), static_cast<uint8_t>(rgb >> 8),
|
||||
static_cast<uint8_t>(rgb)};
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ParseInt32(const std::string& value, int32_t& out) {
|
||||
int32_t number = 0;
|
||||
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), number);
|
||||
@@ -302,6 +317,13 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
}
|
||||
} else if (arg == "--mic") {
|
||||
options.config.audio_input_device = value;
|
||||
} else if (arg == "--controller-color") {
|
||||
Config::ControllerColor color {};
|
||||
if (!ParseControllerColor(value, color)) {
|
||||
::printf("invalid controller color (expected #RRGGBB): %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
options.config.controller_color = color;
|
||||
} else if (arg == "--present-mode") {
|
||||
if (!ParseEnum(value, options.config.present_mode)) {
|
||||
::printf("invalid present mode: %s\n", value.c_str());
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
// Exercise production output scheduling with a deterministic clock and consuming SDL queues.
|
||||
#include "common/threads.h"
|
||||
#include "libs/dualSenseHaptics.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
void Check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "AudioOutTimingTests: %s\n", message);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
struct Stream {
|
||||
SDL_AudioSpec spec;
|
||||
uint64_t frames = 0; // Millionths of a frame retain exact simulated consumption.
|
||||
uint64_t updated;
|
||||
};
|
||||
|
||||
uint64_t now = 1000000;
|
||||
uint32_t oversleep = 0, processing = 0;
|
||||
std::vector<uint32_t> sleeps;
|
||||
std::vector<std::unique_ptr<Stream>> streams;
|
||||
bool fail_open = false, fail_put = false, stalled = false;
|
||||
bool pad_connected = false, pad_bluetooth = false;
|
||||
uint64_t pad_queue_us = 0;
|
||||
int clears = 0;
|
||||
|
||||
Stream& GetStream(SDL_AudioStream* stream) {
|
||||
return *reinterpret_cast<Stream*>(stream);
|
||||
}
|
||||
|
||||
void Drain(Stream& stream) {
|
||||
if (!stalled) {
|
||||
const auto consumed = (now - stream.updated) * stream.spec.freq;
|
||||
stream.frames -= std::min(stream.frames, consumed);
|
||||
}
|
||||
stream.updated = now;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace Fake {
|
||||
bool InitSubSystem(SDL_InitFlags) {
|
||||
return true;
|
||||
}
|
||||
void QuitSubSystem(SDL_InitFlags) {}
|
||||
bool ResumeAudioStreamDevice(SDL_AudioStream*) {
|
||||
return true;
|
||||
}
|
||||
|
||||
SDL_AudioStream* OpenAudioDeviceStream(SDL_AudioDeviceID, const SDL_AudioSpec* spec,
|
||||
SDL_AudioStreamCallback, void*) {
|
||||
if (fail_open) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
streams.push_back(std::make_unique<Stream>(Stream {*spec, 0, now}));
|
||||
return reinterpret_cast<SDL_AudioStream*>(streams.back().get());
|
||||
}
|
||||
|
||||
void DestroyAudioStream(SDL_AudioStream* stream) {
|
||||
const auto it = std::find_if(streams.begin(), streams.end(), [stream](const auto& value) {
|
||||
return value.get() == &GetStream(stream);
|
||||
});
|
||||
Check(it != streams.end(), "unknown stream destroyed");
|
||||
streams.erase(it);
|
||||
}
|
||||
|
||||
int GetAudioStreamQueued(SDL_AudioStream* stream) {
|
||||
auto& state = GetStream(stream);
|
||||
Drain(state);
|
||||
return static_cast<int>(state.frames / 1000000 * SDL_AUDIO_BYTESIZE(state.spec.format) *
|
||||
state.spec.channels);
|
||||
}
|
||||
|
||||
bool PutAudioStreamData(SDL_AudioStream* stream, const void*, int bytes) {
|
||||
now += processing;
|
||||
if (fail_put) {
|
||||
return false;
|
||||
}
|
||||
auto& state = GetStream(stream);
|
||||
Drain(state);
|
||||
state.frames += static_cast<uint64_t>(bytes) * 1000000 /
|
||||
(SDL_AUDIO_BYTESIZE(state.spec.format) * state.spec.channels);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClearAudioStream(SDL_AudioStream* stream) {
|
||||
auto& state = GetStream(stream);
|
||||
state.frames = 0;
|
||||
state.updated = now;
|
||||
clears++;
|
||||
return true;
|
||||
}
|
||||
} // namespace Fake
|
||||
|
||||
namespace Common {
|
||||
class TimingThread: public Thread {
|
||||
public:
|
||||
static void SleepMicro(uint32_t micros) {
|
||||
sleeps.push_back(micros);
|
||||
now += micros + oversleep;
|
||||
}
|
||||
};
|
||||
} // namespace Common
|
||||
|
||||
#define Thread TimingThread
|
||||
#define SDL_InitSubSystem Fake::InitSubSystem
|
||||
#define SDL_QuitSubSystem Fake::QuitSubSystem
|
||||
#define SDL_OpenAudioDeviceStream Fake::OpenAudioDeviceStream
|
||||
#define SDL_ResumeAudioStreamDevice Fake::ResumeAudioStreamDevice
|
||||
#define SDL_DestroyAudioStream Fake::DestroyAudioStream
|
||||
#define SDL_GetAudioStreamQueued Fake::GetAudioStreamQueued
|
||||
#define SDL_PutAudioStreamData Fake::PutAudioStreamData
|
||||
#define SDL_ClearAudioStream Fake::ClearAudioStream
|
||||
#include "libs/audio.cpp"
|
||||
#undef Thread
|
||||
#undef SDL_InitSubSystem
|
||||
#undef SDL_QuitSubSystem
|
||||
#undef SDL_OpenAudioDeviceStream
|
||||
#undef SDL_ResumeAudioStreamDevice
|
||||
#undef SDL_DestroyAudioStream
|
||||
#undef SDL_GetAudioStreamQueued
|
||||
#undef SDL_PutAudioStreamData
|
||||
#undef SDL_ClearAudioStream
|
||||
|
||||
namespace Libs::Controller {
|
||||
int GetActiveControllerId() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace DualSenseHaptics {
|
||||
Stream* Open(uint32_t, bool) {
|
||||
static int pad_stream;
|
||||
return pad_connected ? reinterpret_cast<Stream*>(&pad_stream) : nullptr;
|
||||
}
|
||||
void Close(Stream*) {}
|
||||
bool UsesBluetooth(const Stream* stream) {
|
||||
return stream != nullptr && pad_bluetooth;
|
||||
}
|
||||
uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*) {
|
||||
return stream != nullptr ? pad_queue_us : 0;
|
||||
}
|
||||
} // namespace DualSenseHaptics
|
||||
} // namespace Libs::Controller
|
||||
|
||||
namespace Libs::LibKernel {
|
||||
uint64_t KYTY_SYSV_ABI KernelGetProcessTime() {
|
||||
return now;
|
||||
}
|
||||
} // namespace Libs::LibKernel
|
||||
|
||||
namespace Loader::Timer {
|
||||
double GetTimeMs() {
|
||||
return static_cast<double>(now) / 1000;
|
||||
}
|
||||
} // namespace Loader::Timer
|
||||
|
||||
namespace {
|
||||
using Audio = Libs::Audio::Audio;
|
||||
|
||||
struct Fixture {
|
||||
Audio audio;
|
||||
std::array<float, 2048> pcm {};
|
||||
|
||||
Fixture() {
|
||||
Check(streams.empty(), "stream leaked between tests");
|
||||
now = 1000000;
|
||||
processing = oversleep = 0;
|
||||
clears = 0;
|
||||
fail_open = fail_put = stalled = false;
|
||||
pad_connected = pad_bluetooth = false;
|
||||
pad_queue_us = 0;
|
||||
sleeps.clear();
|
||||
}
|
||||
|
||||
Audio::Id Open(uint32_t frames = 256, int type = 0) {
|
||||
const auto id = audio.AudioOutOpen(type, frames, 48000, Audio::Format::FloatStereo);
|
||||
Check(id.IsValid(), "port open failed");
|
||||
return id;
|
||||
}
|
||||
|
||||
void Output(Audio::Id port, bool blocking = true) {
|
||||
Audio::OutputParam param {port, pcm.data()};
|
||||
audio.AudioOutOutputs(¶m, 1, blocking);
|
||||
}
|
||||
|
||||
void Prime(Audio::Id port) {
|
||||
const auto start = now;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
Output(port);
|
||||
}
|
||||
Check(now == start, "priming was paced before the 40 ms cushion filled");
|
||||
Output(port);
|
||||
Check(now - start >= 5333 && now - start <= 5334,
|
||||
"priming accumulated a multi-block deadline");
|
||||
Drain(*streams.front());
|
||||
Check(streams.front()->frames >= uint64_t {7 * 256} * 1000000,
|
||||
"first paced output consumed the priming cushion");
|
||||
}
|
||||
};
|
||||
|
||||
void TestPrimingAndSteadyCadence() {
|
||||
Fixture f;
|
||||
const auto port = f.Open();
|
||||
f.Prime(port);
|
||||
processing = 125;
|
||||
oversleep = 175;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
f.Output(port);
|
||||
}
|
||||
const auto start = now;
|
||||
for (int i = 0; i < 3000; i++) {
|
||||
f.Output(port);
|
||||
}
|
||||
Check(now - start >= 15999999 && now - start <= 16000001,
|
||||
"processing, sleep overshoot, or fractional periods accumulated timing drift");
|
||||
}
|
||||
|
||||
void TestUnderrunAndStalledQueueRecovery() {
|
||||
Fixture f;
|
||||
const auto port = f.Open();
|
||||
f.Prime(port);
|
||||
now += 1000000;
|
||||
f.Prime(port);
|
||||
|
||||
// A stopped device must clear its stale queue and restart priming, not catch up old deadlines.
|
||||
stalled = true;
|
||||
for (int i = 0; i < 3 && clears == 0; i++) {
|
||||
f.Output(port);
|
||||
}
|
||||
Check(clears == 1, "stalled playback queue was not cleared");
|
||||
stalled = false;
|
||||
const auto start = now;
|
||||
for (int i = 0; i < 7; i++) {
|
||||
f.Output(port);
|
||||
}
|
||||
Check(now == start, "queue clear did not restart unpaced priming");
|
||||
f.Output(port);
|
||||
Check(now - start >= 5333 && now - start <= 5334,
|
||||
"queue clear retained an old pacing deadline");
|
||||
}
|
||||
|
||||
void TestFallbackClockAndLongGap() {
|
||||
Fixture f;
|
||||
fail_open = true;
|
||||
const auto port = f.Open();
|
||||
f.Output(port);
|
||||
const auto start = now;
|
||||
oversleep = 175;
|
||||
for (int i = 0; i < 3000; i++) {
|
||||
now += 125;
|
||||
f.Output(port);
|
||||
}
|
||||
Check(now - start >= 16000174 && now - start <= 16000176,
|
||||
"device-free output accumulated timing drift");
|
||||
now += 1000000;
|
||||
f.Output(port);
|
||||
const auto resumed = now;
|
||||
f.Output(port);
|
||||
Check(now - resumed >= 5333 && now - resumed <= 5509,
|
||||
"long interruption caused a burst of device-free catch-up submissions");
|
||||
}
|
||||
|
||||
void TestAsyncDoesNotAccumulateDeadlines() {
|
||||
Fixture f;
|
||||
const auto port = f.Open();
|
||||
const auto start = now;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
f.Output(port, false);
|
||||
}
|
||||
Check(now == start, "asynchronous output slept");
|
||||
f.Output(port);
|
||||
Check(now - start <= 5334, "asynchronous submissions accumulated a blocking deadline");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
f.Output(port);
|
||||
}
|
||||
now += 1000000;
|
||||
f.Output(port, false);
|
||||
const auto resumed = now;
|
||||
for (int i = 0; i < 7; i++) {
|
||||
f.Output(port);
|
||||
}
|
||||
Check(now == resumed, "async-to-blocking transition did not reset an emptied queue");
|
||||
}
|
||||
|
||||
void TestSynchronizedBatchAndInactivePorts() {
|
||||
Fixture f;
|
||||
const auto vibration = f.Open(256, 10);
|
||||
const auto main = f.Open();
|
||||
const auto background = f.Open();
|
||||
Audio::OutputParam batch[] {
|
||||
{vibration, f.pcm.data()}, {main, f.pcm.data()}, {background, f.pcm.data()}};
|
||||
const auto start = now;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
f.audio.AudioOutOutputs(batch, 3);
|
||||
}
|
||||
Check(now == start, "vibration fallback slowed a device-backed batch's priming");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
sleeps.clear();
|
||||
f.audio.AudioOutOutputs(batch, 3);
|
||||
Check(sleeps.size() == 1 && sleeps.front() >= 5333 && sleeps.front() <= 5334,
|
||||
"synchronized streams accumulated separate pacing waits");
|
||||
}
|
||||
|
||||
Audio::OutputParam inactive[] {{main, nullptr}, {vibration, f.pcm.data()}};
|
||||
now += 1000000;
|
||||
f.audio.AudioOutOutputs(inactive, 2);
|
||||
const auto resumed = now;
|
||||
f.audio.AudioOutOutputs(inactive, 2);
|
||||
Check(now - resumed >= 5333 && now - resumed <= 5334,
|
||||
"a device without PCM disabled fallback pacing");
|
||||
}
|
||||
|
||||
void TestFallbackUsesEachPortsPeriod() {
|
||||
Fixture f;
|
||||
fail_open = true;
|
||||
const auto short_port = f.Open(128);
|
||||
const auto long_port = f.Open(256);
|
||||
Audio::OutputParam batch[] {{short_port, f.pcm.data()}, {long_port, f.pcm.data()}};
|
||||
f.audio.AudioOutOutputs(batch, 2);
|
||||
const auto start = now;
|
||||
f.audio.AudioOutOutputs(batch, 2);
|
||||
Check(now - start >= 5333 && now - start <= 5334,
|
||||
"fallback batch used the first port's period for every port");
|
||||
}
|
||||
|
||||
void TestFailedQueueUsesFallbackClock() {
|
||||
Fixture f;
|
||||
const auto port = f.Open();
|
||||
fail_put = true;
|
||||
f.Output(port);
|
||||
const auto start = now;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
f.Output(port);
|
||||
}
|
||||
Check(now - start == 16000, "repeated queue failures reset fallback pacing");
|
||||
}
|
||||
|
||||
void TestControllerSpeakerPacing() {
|
||||
Fixture f;
|
||||
fail_open = true; // No PC audio device paces the pad speaker.
|
||||
pad_connected = true;
|
||||
pad_queue_us = 80000;
|
||||
const auto speaker = f.Open(256, 4);
|
||||
|
||||
pad_bluetooth = true;
|
||||
f.Output(speaker);
|
||||
const auto bluetooth_start = now;
|
||||
f.Output(speaker);
|
||||
Check(now - bluetooth_start >= 5333 && now - bluetooth_start <= 5334,
|
||||
"Bluetooth speaker waited on its HID queue instead of the sample clock");
|
||||
|
||||
pad_bluetooth = false;
|
||||
const auto usb_start = now;
|
||||
f.Output(speaker);
|
||||
Check(now - usb_start >= 5333 && now - usb_start <= 5334,
|
||||
"stalled USB speaker waited longer than one audio block");
|
||||
}
|
||||
|
||||
void TestInvalidBatchSize() {
|
||||
Libs::Audio::AudioOut::AudioOutOutputParam param {};
|
||||
for (const auto count: {0u, 33u}) {
|
||||
Check(Libs::Audio::AudioOut::AudioOutOutputs(¶m, count) ==
|
||||
Libs::Audio::AUDIO_OUT_ERROR_INVALID_SIZE,
|
||||
"invalid batch size was not rejected before accessing ports");
|
||||
}
|
||||
}
|
||||
|
||||
void TestZeroOutputFrequency() {
|
||||
Fixture f;
|
||||
Check(Libs::Audio::AudioOut::AudioOutOpen(0, 0, 0, 256, 0, 4) ==
|
||||
Libs::Audio::AUDIO_OUT_ERROR_INVALID_SAMPLE_FREQ,
|
||||
"public output open accepted zero frequency");
|
||||
Check(!f.audio.AudioOutOpen(10, 256, 0, Audio::Format::FloatStereo).IsValid(),
|
||||
"internal output open accepted zero frequency");
|
||||
Check(f.Open().ToInt() == 1, "rejected output port occupied a handle");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestPrimingAndSteadyCadence();
|
||||
TestUnderrunAndStalledQueueRecovery();
|
||||
TestFallbackClockAndLongGap();
|
||||
TestAsyncDoesNotAccumulateDeadlines();
|
||||
TestSynchronizedBatchAndInactivePorts();
|
||||
TestFallbackUsesEachPortsPeriod();
|
||||
TestFailedQueueUsesFallbackClock();
|
||||
TestControllerSpeakerPacing();
|
||||
TestInvalidBatchSize();
|
||||
TestZeroOutputFrequency();
|
||||
Check(streams.empty(), "output stream leaked");
|
||||
std::puts("AudioOutTimingTests: all cases passed");
|
||||
}
|
||||
@@ -238,14 +238,31 @@ struct TrackerHarness {
|
||||
MemoryTracker tracker;
|
||||
};
|
||||
|
||||
uint8_t *AllocateFixedGuestRange(uint64_t size, uintptr_t offset) {
|
||||
// Keep the mapping inside the tracker's guest address space and preserve the
|
||||
// requested region alignment. A fixed address can be occupied by the host
|
||||
// process (notably by the macOS runner's ASLR layout).
|
||||
constexpr uintptr_t first_base = 0x0000000200000000ull;
|
||||
constexpr uintptr_t stride = 0x0000000100000000ull;
|
||||
for (uintptr_t attempt = 0; attempt < 256; attempt++) {
|
||||
auto *wanted = reinterpret_cast<void *>(first_base + offset + attempt * stride);
|
||||
auto *memory = static_cast<uint8_t *>(
|
||||
VirtualAlloc(wanted, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
if (memory == wanted) {
|
||||
return memory;
|
||||
}
|
||||
if (memory != nullptr) {
|
||||
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0,
|
||||
"release unexpected fixed allocation failed");
|
||||
}
|
||||
}
|
||||
Check(false, "no free fixed guest address found");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint8_t *Allocate(PageManager &manager, uint64_t pages) {
|
||||
constexpr uintptr_t base = 0x0000000200010000ull;
|
||||
const auto size = manager.GetPageSize() * pages;
|
||||
auto *memory = static_cast<uint8_t *>(
|
||||
VirtualAlloc(reinterpret_cast<void *>(base), size,
|
||||
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
Check(memory == reinterpret_cast<void *>(base), "fixed VirtualAlloc failed");
|
||||
return memory;
|
||||
return AllocateFixedGuestRange(size, 0x10000);
|
||||
}
|
||||
|
||||
void Release(uint8_t *memory) {
|
||||
@@ -386,16 +403,10 @@ void TestCpuDirtyUpload() {
|
||||
}
|
||||
|
||||
void TestRangeInvalidation() {
|
||||
constexpr uintptr_t base = 0x0000000201000000ull;
|
||||
TrackerHarness harness;
|
||||
auto &tracker = harness.tracker;
|
||||
auto &page_manager = harness.page_manager;
|
||||
constexpr uint64_t size = Libs::Graphics::TRACKER_REGION_SIZE * 2;
|
||||
auto *memory = static_cast<uint8_t *>(
|
||||
VirtualAlloc(reinterpret_cast<void *>(base), size,
|
||||
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
Check(memory == reinterpret_cast<void *>(base),
|
||||
"range invalidation allocation failed");
|
||||
auto *memory = AllocateFixedGuestRange(size, 0x1000000);
|
||||
const auto address = reinterpret_cast<uint64_t>(memory);
|
||||
|
||||
tracker.ForEachUploadRange(
|
||||
@@ -614,16 +625,12 @@ void TestGpuDownloadProtectionMirrors() {
|
||||
}
|
||||
|
||||
void TestCrossRegionUpload() {
|
||||
constexpr uintptr_t base = 0x0000000200010000ull;
|
||||
constexpr uint64_t region_size = 4ull * 1024ull * 1024ull;
|
||||
TrackerHarness harness;
|
||||
auto &tracker = harness.tracker;
|
||||
auto &page_manager = harness.page_manager;
|
||||
const auto page_size = page_manager.GetPageSize();
|
||||
auto *memory = static_cast<uint8_t *>(
|
||||
VirtualAlloc(reinterpret_cast<void *>(base), region_size * 2,
|
||||
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
Check(memory == reinterpret_cast<void *>(base), "fixed VirtualAlloc failed");
|
||||
auto *memory = AllocateFixedGuestRange(region_size * 2, 0x10000);
|
||||
const auto address = reinterpret_cast<uint64_t>(memory);
|
||||
const auto boundary = (address + region_size - 1) & ~(region_size - 1);
|
||||
uint32_t ranges = 0;
|
||||
@@ -871,6 +878,15 @@ void TestFullRegionGpuUnmarkBatching() {
|
||||
[&]() noexcept {
|
||||
(void)tracker.IsRegionCpuModified(address, page_size);
|
||||
});
|
||||
} else if (std::strcmp(name, "recursive-tracking-lock") == 0) {
|
||||
Libs::Graphics::TrackingSpinLock lock;
|
||||
lock.lock();
|
||||
lock.lock();
|
||||
} else if (std::strcmp(name, "non-owner-tracking-unlock") == 0) {
|
||||
Libs::Graphics::TrackingSpinLock lock;
|
||||
lock.lock();
|
||||
std::thread worker([&] { lock.unlock(); });
|
||||
worker.join();
|
||||
}
|
||||
std::_Exit(0x7f);
|
||||
}
|
||||
@@ -928,7 +944,8 @@ void CheckDeathCase(const char *name) {
|
||||
}
|
||||
|
||||
void TestFatalPaths() {
|
||||
for (const char *name : {"gpu-dirty-explicit-cpu", "reentrant-upload"}) {
|
||||
for (const char *name : {"gpu-dirty-explicit-cpu", "reentrant-upload",
|
||||
"recursive-tracking-lock", "non-owner-tracking-unlock"}) {
|
||||
CheckDeathCase(name);
|
||||
}
|
||||
}
|
||||
|
||||
+638
-38
@@ -1,11 +1,17 @@
|
||||
// Exercise the production module with real SDL streams and fake USB endpoints.
|
||||
#include <SDL3/SDL.h>
|
||||
#include <opus.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
#include <semaphore>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -24,23 +30,32 @@ struct Rumble {
|
||||
Uint16 large, small;
|
||||
Uint32 duration;
|
||||
};
|
||||
std::vector<Device> devices;
|
||||
std::vector<SDL_AudioStream*> streams;
|
||||
Rumble rumble {};
|
||||
Uint64 now = 1000;
|
||||
int audio_refs = 0, opens = 0, rumble_calls = 0;
|
||||
bool fail_open = false, fail_resume = false;
|
||||
int actual_channels = 4;
|
||||
SDL_AudioDeviceID opened_device = 0;
|
||||
SDL_AudioSpec opened_spec {};
|
||||
std::array<Rumble, 4> pad_rumble {};
|
||||
std::array<std::vector<uint8_t>, 4> effects;
|
||||
std::vector<SDL_JoystickID> connected_pads {1};
|
||||
std::array<Rumble, 4> cached_rumble {};
|
||||
bool fail_effect = false, wireless = false;
|
||||
SDL_AudioStream* default_stream = nullptr;
|
||||
bool fail_default_resume = false;
|
||||
int active_controller = 1;
|
||||
std::vector<Device> devices;
|
||||
std::vector<SDL_AudioStream*> streams;
|
||||
Rumble rumble {};
|
||||
Uint64 now = 1000;
|
||||
int audio_refs = 0, opens = 0, rumble_calls = 0;
|
||||
bool fail_open = false, fail_resume = false;
|
||||
int actual_channels = 4;
|
||||
SDL_AudioDeviceID opened_device = 0;
|
||||
SDL_AudioSpec opened_spec {};
|
||||
std::array<Rumble, 4> pad_rumble {};
|
||||
std::array<std::vector<uint8_t>, 4> effects;
|
||||
std::vector<SDL_JoystickID> connected_pads {1};
|
||||
std::array<Rumble, 4> cached_rumble {};
|
||||
bool fail_effect = false, wireless = false, wireless_pad3 = false;
|
||||
bool hid_available = false, fail_hid_write = false;
|
||||
bool duplicate_hid = false;
|
||||
int hid_opens = 0;
|
||||
int opened_hid_pad = 0;
|
||||
std::vector<std::array<uint8_t, 547>> hid_reports;
|
||||
std::atomic_bool block_opus_encode {false}, block_hid_write {false};
|
||||
std::atomic_int hid_closes {0};
|
||||
std::binary_semaphore encode_started {0}, allow_encode {0};
|
||||
std::binary_semaphore write_started {0}, allow_write {0};
|
||||
SDL_AudioStream* default_stream = nullptr;
|
||||
bool fail_default_resume = false;
|
||||
int active_controller = 1;
|
||||
} // namespace
|
||||
|
||||
namespace Fake {
|
||||
@@ -116,8 +131,9 @@ void DestroyAudioStream(SDL_AudioStream* stream) {
|
||||
return;
|
||||
}
|
||||
const auto it = std::find(streams.begin(), streams.end(), stream);
|
||||
Check(it != streams.end(), "stream destroyed twice");
|
||||
streams.erase(it);
|
||||
if (it != streams.end()) {
|
||||
streams.erase(it);
|
||||
}
|
||||
if (stream == default_stream) {
|
||||
default_stream = nullptr;
|
||||
}
|
||||
@@ -134,6 +150,73 @@ SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
|
||||
return id == 1 || id == 3 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
: nullptr;
|
||||
}
|
||||
SDL_GamepadType GetGamepadType(SDL_Gamepad* pad) {
|
||||
return GetGamepadTypeForID(static_cast<SDL_JoystickID>(reinterpret_cast<uintptr_t>(pad)));
|
||||
}
|
||||
Uint16 GetGamepadVendor(SDL_Gamepad*) {
|
||||
return 0x054c;
|
||||
}
|
||||
Uint16 GetGamepadProduct(SDL_Gamepad*) {
|
||||
return 0x0ce6;
|
||||
}
|
||||
const char* GetGamepadSerial(SDL_Gamepad* pad) {
|
||||
return reinterpret_cast<uintptr_t>(pad) == 3 ? "aa-bb-cc-dd-ee-03" : "aa-bb-cc-dd-ee-01";
|
||||
}
|
||||
Uint64 GetTicksNS() {
|
||||
return now * 1000000;
|
||||
}
|
||||
SDL_Thread* CreateThread(SDL_ThreadFunction, const char*, void*) {
|
||||
return reinterpret_cast<SDL_Thread*>(1);
|
||||
}
|
||||
void WaitThread(SDL_Thread*, int*) {}
|
||||
SDL_hid_device_info* HidEnumerate(unsigned short, unsigned short) {
|
||||
if (!hid_available) {
|
||||
return nullptr;
|
||||
}
|
||||
static wchar_t serial1[] = L"AA:BB:CC:DD:EE:01";
|
||||
static wchar_t serial3[] = L"AA:BB:CC:DD:EE:03";
|
||||
static char path1[] = "pad-1";
|
||||
static char path3[] = "pad-3";
|
||||
static SDL_hid_device_info pad3 {}, pad1 {};
|
||||
pad3.path = path3;
|
||||
pad3.serial_number = duplicate_hid ? serial1 : serial3;
|
||||
pad3.bus_type = SDL_HID_API_BUS_BLUETOOTH;
|
||||
pad1.path = path1;
|
||||
pad1.serial_number = serial1;
|
||||
pad1.bus_type = SDL_HID_API_BUS_BLUETOOTH;
|
||||
pad1.next = &pad3;
|
||||
return &pad1;
|
||||
}
|
||||
void HidFreeEnumeration(SDL_hid_device_info*) {}
|
||||
SDL_hid_device* HidOpenPath(const char* path) {
|
||||
hid_opens++;
|
||||
opened_hid_pad = path[4] == '1' ? 1 : 3;
|
||||
return reinterpret_cast<SDL_hid_device*>(static_cast<uintptr_t>(opened_hid_pad));
|
||||
}
|
||||
void HidClose(SDL_hid_device*) {
|
||||
hid_closes++;
|
||||
}
|
||||
int HidWrite(SDL_hid_device*, const unsigned char* data, size_t size) {
|
||||
if (block_hid_write.exchange(false)) {
|
||||
write_started.release();
|
||||
allow_write.acquire();
|
||||
}
|
||||
if (fail_hid_write || size != 547) {
|
||||
return -1;
|
||||
}
|
||||
std::array<uint8_t, 547> report {};
|
||||
std::copy_n(data, report.size(), report.begin());
|
||||
hid_reports.push_back(report);
|
||||
return static_cast<int>(size);
|
||||
}
|
||||
opus_int32 OpusEncodeFloat(OpusEncoder* encoder, const float* pcm, int frames,
|
||||
unsigned char* packet, opus_int32 bytes) {
|
||||
if (block_opus_encode.exchange(false)) {
|
||||
encode_started.release();
|
||||
allow_encode.acquire();
|
||||
}
|
||||
return ::opus_encode_float(encoder, pcm, frames, packet, bytes);
|
||||
}
|
||||
SDL_JoystickID* GetGamepads(int* count) {
|
||||
*count = static_cast<int>(connected_pads.size());
|
||||
auto* ids = static_cast<SDL_JoystickID*>(
|
||||
@@ -142,8 +225,10 @@ SDL_JoystickID* GetGamepads(int* count) {
|
||||
ids[connected_pads.size()] = 0;
|
||||
return ids;
|
||||
}
|
||||
SDL_JoystickConnectionState GetGamepadConnectionState(SDL_Gamepad*) {
|
||||
return wireless ? SDL_JOYSTICK_CONNECTION_WIRELESS : SDL_JOYSTICK_CONNECTION_WIRED;
|
||||
SDL_JoystickConnectionState GetGamepadConnectionState(SDL_Gamepad* pad) {
|
||||
return wireless || (wireless_pad3 && reinterpret_cast<uintptr_t>(pad) == 3)
|
||||
? SDL_JOYSTICK_CONNECTION_WIRELESS
|
||||
: SDL_JOYSTICK_CONNECTION_WIRED;
|
||||
}
|
||||
bool RumbleGamepad(SDL_Gamepad* pad, Uint16 large, Uint16 small, Uint32 duration) {
|
||||
const auto id = reinterpret_cast<uintptr_t>(pad);
|
||||
@@ -173,22 +258,37 @@ bool SendGamepadEffect(SDL_Gamepad* pad, const void* data, int size) {
|
||||
}
|
||||
} // namespace Fake
|
||||
|
||||
#define SDL_GetTicks Fake::GetTicks
|
||||
#define SDL_InitSubSystem Fake::InitSubSystem
|
||||
#define SDL_QuitSubSystem Fake::QuitSubSystem
|
||||
#define SDL_GetAudioPlaybackDevices Fake::GetAudioPlaybackDevices
|
||||
#define SDL_GetAudioDeviceName Fake::GetAudioDeviceName
|
||||
#define SDL_GetAudioDeviceFormat Fake::GetAudioDeviceFormat
|
||||
#define SDL_OpenAudioDeviceStream Fake::OpenAudioDeviceStream
|
||||
#define SDL_ResumeAudioStreamDevice Fake::ResumeAudioStreamDevice
|
||||
#define SDL_DestroyAudioStream Fake::DestroyAudioStream
|
||||
#define SDL_GetAudioStreamDevice Fake::GetAudioStreamDevice
|
||||
#define SDL_GetGamepadTypeForID Fake::GetGamepadTypeForID
|
||||
#define SDL_GetTicks Fake::GetTicks
|
||||
#define SDL_InitSubSystem Fake::InitSubSystem
|
||||
#define SDL_QuitSubSystem Fake::QuitSubSystem
|
||||
#define SDL_GetAudioPlaybackDevices Fake::GetAudioPlaybackDevices
|
||||
#define SDL_GetAudioDeviceName Fake::GetAudioDeviceName
|
||||
#define SDL_GetAudioDeviceFormat Fake::GetAudioDeviceFormat
|
||||
#define SDL_OpenAudioDeviceStream Fake::OpenAudioDeviceStream
|
||||
#define SDL_ResumeAudioStreamDevice Fake::ResumeAudioStreamDevice
|
||||
#define SDL_DestroyAudioStream Fake::DestroyAudioStream
|
||||
#define SDL_GetAudioStreamDevice Fake::GetAudioStreamDevice
|
||||
#define SDL_GetGamepadTypeForID Fake::GetGamepadTypeForID
|
||||
#define SDL_GetGamepadType Fake::GetGamepadType
|
||||
#define SDL_GetGamepadVendor Fake::GetGamepadVendor
|
||||
#define SDL_GetGamepadProduct Fake::GetGamepadProduct
|
||||
#define SDL_GetGamepadSerial Fake::GetGamepadSerial
|
||||
#define SDL_GetTicksNS Fake::GetTicksNS
|
||||
#undef SDL_CreateThread
|
||||
#define SDL_CreateThread Fake::CreateThread
|
||||
#define SDL_WaitThread Fake::WaitThread
|
||||
#define SDL_hid_enumerate Fake::HidEnumerate
|
||||
#define SDL_hid_free_enumeration Fake::HidFreeEnumeration
|
||||
#define SDL_hid_open_path Fake::HidOpenPath
|
||||
#define SDL_hid_close Fake::HidClose
|
||||
#define SDL_hid_write Fake::HidWrite
|
||||
#define opus_encode_float Fake::OpusEncodeFloat
|
||||
#define SDL_GetGamepadFromID Fake::GetGamepadFromID
|
||||
#define SDL_RumbleGamepad Fake::RumbleGamepad
|
||||
#define SDL_SendGamepadEffect Fake::SendGamepadEffect
|
||||
#define SDL_GetGamepads Fake::GetGamepads
|
||||
#define SDL_GetGamepadConnectionState Fake::GetGamepadConnectionState
|
||||
#include "libs/dualSenseBluetooth.cpp"
|
||||
#include "libs/dualSenseHaptics.cpp"
|
||||
#include "libs/audio.cpp"
|
||||
#undef SDL_GetTicks
|
||||
@@ -202,6 +302,19 @@ bool SendGamepadEffect(SDL_Gamepad* pad, const void* data, int size) {
|
||||
#undef SDL_DestroyAudioStream
|
||||
#undef SDL_GetAudioStreamDevice
|
||||
#undef SDL_GetGamepadTypeForID
|
||||
#undef SDL_GetGamepadType
|
||||
#undef SDL_GetGamepadVendor
|
||||
#undef SDL_GetGamepadProduct
|
||||
#undef SDL_GetGamepadSerial
|
||||
#undef SDL_GetTicksNS
|
||||
#undef SDL_CreateThread
|
||||
#undef SDL_WaitThread
|
||||
#undef SDL_hid_enumerate
|
||||
#undef SDL_hid_free_enumeration
|
||||
#undef SDL_hid_open_path
|
||||
#undef SDL_hid_close
|
||||
#undef SDL_hid_write
|
||||
#undef opus_encode_float
|
||||
#undef SDL_GetGamepadFromID
|
||||
#undef SDL_RumbleGamepad
|
||||
#undef SDL_SendGamepadEffect
|
||||
@@ -228,6 +341,7 @@ double GetTimeMs() {
|
||||
|
||||
namespace {
|
||||
namespace Haptics = Libs::Controller::DualSenseHaptics;
|
||||
namespace Bluetooth = Libs::Controller::DualSenseBluetooth;
|
||||
using Port = std::unique_ptr<Haptics::Stream, decltype(&Haptics::Close)>;
|
||||
constexpr std::array<int, 2> unity {32768, 32768};
|
||||
constexpr std::array<float, 4> pcm {0.5f, 0.5f, 0.5f, 0.5f};
|
||||
@@ -240,11 +354,17 @@ struct Fixture {
|
||||
cached_rumble = {};
|
||||
effects = {};
|
||||
connected_pads = {1};
|
||||
fail_effect = wireless = false;
|
||||
default_stream = nullptr;
|
||||
fail_effect = wireless = wireless_pad3 = false;
|
||||
hid_available = fail_hid_write = false;
|
||||
duplicate_hid = false;
|
||||
hid_opens = 0;
|
||||
hid_closes = 0;
|
||||
opened_hid_pad = 0;
|
||||
hid_reports.clear();
|
||||
default_stream = nullptr;
|
||||
fail_default_resume = false;
|
||||
active_controller = 1;
|
||||
now = 1000;
|
||||
active_controller = 1;
|
||||
now = 1000;
|
||||
opens = rumble_calls = 0;
|
||||
fail_open = fail_resume = false;
|
||||
actual_channels = 4;
|
||||
@@ -255,8 +375,8 @@ struct Fixture {
|
||||
}
|
||||
};
|
||||
|
||||
Port Open(bool speaker = false) {
|
||||
auto* stream = Haptics::Open(48000, speaker);
|
||||
Port Open(bool speaker = false, uint32_t freq = 48000) {
|
||||
auto* stream = Haptics::Open(freq, speaker);
|
||||
Check(stream != nullptr, "port open failed");
|
||||
return Port(stream, Haptics::Close);
|
||||
}
|
||||
@@ -516,6 +636,446 @@ void TestSpeakerRequiresUnambiguousUsbDevice() {
|
||||
"USB to Bluetooth change retained the USB speaker");
|
||||
}
|
||||
|
||||
void TestBluetoothAudioAndIdentity() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
connected_pads = {1, 3};
|
||||
devices.clear(); // Bluetooth does not enumerate a quad USB audio endpoint.
|
||||
auto vibration = Open();
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> haptic(2048, 0.5f);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Check(Haptics::SetVibration(1, 100, 80) && pad_rumble[1].large == 100 * 0x101,
|
||||
"Bluetooth controller did not receive normal rumble");
|
||||
Check(Haptics::Queue(vibration.get(), 1, haptic.data(), 1024, 2, true, unity.data()) != 0,
|
||||
"Bluetooth haptics did not open");
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0,
|
||||
"Bluetooth speaker did not open");
|
||||
Check(hid_opens == 1 && opened_hid_pad == 1 && effects[1][7] == 0x30,
|
||||
"Bluetooth audio was not matched to the active controller");
|
||||
Check(pad_rumble[1].large == 0, "Bluetooth haptics did not suppress motor rumble");
|
||||
auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
|
||||
Check(pending.size() == 1, "Bluetooth streams were not combined in one report");
|
||||
Libs::Controller::DualSenseBluetooth::Send(pending);
|
||||
Check(hid_reports.size() == 1, "Bluetooth audio report was not sent");
|
||||
const auto& report = hid_reports.back();
|
||||
Check(report[0] == 0x39 && report[2] == 0x91 && report[3] == 6 && report[4] == 0x7e &&
|
||||
report[10] == 0xd2 && report[11] == 64 && report[140] == 0xd3 && report[141] == 200,
|
||||
"Bluetooth audio sub-packets are malformed");
|
||||
Check(report[12] > 0 && report[13] > 0,
|
||||
"haptic waveform was not downsampled into the actuator channels");
|
||||
uint8_t prefix = 0xa2;
|
||||
auto crc = SDL_crc32(0, &prefix, 1);
|
||||
crc = SDL_crc32(crc, report.data(), report.size() - 4);
|
||||
for (size_t i = 0; i < 4; i++) {
|
||||
Check(report[report.size() - 4 + i] == static_cast<uint8_t>(crc >> (8 * i)),
|
||||
"Bluetooth audio CRC is invalid");
|
||||
}
|
||||
int opus_error = OPUS_OK;
|
||||
auto* decoder = opus_decoder_create(48000, 2, &opus_error);
|
||||
Check(decoder != nullptr && opus_error == OPUS_OK, "Opus decoder unavailable in test");
|
||||
std::array<float, 960> decoded {};
|
||||
Check(opus_decode_float(decoder, report.data() + 142, 200, decoded.data(), 480, 0) == 480,
|
||||
"Bluetooth speaker packet could not be decoded");
|
||||
opus_decoder_destroy(decoder);
|
||||
const auto mean = std::accumulate(decoded.begin(), decoded.end(), 0.0f) / decoded.size();
|
||||
Check(mean > 0.05f && mean < 0.35f, "Bluetooth speaker audio was not preserved");
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000).empty(),
|
||||
"Bluetooth sender ignored report pacing");
|
||||
now += 300;
|
||||
Haptics::UpdateBluetoothRumble();
|
||||
Check(pad_rumble[1].large == 100 * 0x101,
|
||||
"Bluetooth haptics did not restore normal rumble afterward");
|
||||
|
||||
// A different active DualSense has a different HID address, even if both are
|
||||
// paired.
|
||||
Check(Haptics::Queue(vibration.get(), 3, haptic.data(), 1024, 2, true, unity.data()) != 0 &&
|
||||
opened_hid_pad == 3,
|
||||
"Bluetooth stream followed the wrong controller");
|
||||
Check(Haptics::Queue(speaker.get(), 3, sound.data(), 1024, 2, true, unity.data()) != 0,
|
||||
"Bluetooth speaker did not follow the selected controller");
|
||||
Check(effects[1][7] == 0, "old Bluetooth controller remained speaker-routed");
|
||||
wireless = false;
|
||||
connected_pads = {3};
|
||||
devices = {{20, "Speakers (DualSense Wireless Controller)", 4}};
|
||||
Check(Haptics::Queue(speaker.get(), 3, sound.data(), 1024, 2, true, unity.data()) != 0 &&
|
||||
opened_device == 20 && effects[3][7] == 0x30,
|
||||
"Bluetooth to USB change did not reopen the USB speaker immediately");
|
||||
speaker.reset();
|
||||
vibration.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothFailureFallsBack() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0,
|
||||
"Bluetooth port did not initially open");
|
||||
fail_hid_write = true;
|
||||
auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
|
||||
Libs::Controller::DualSenseBluetooth::Send(pending);
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) == 0 &&
|
||||
effects[1][7] == 0,
|
||||
"failed HID write did not restore the normal audio fallback");
|
||||
speaker.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothFormatsAndRoutingFailure() {
|
||||
Fixture f;
|
||||
wireless = hid_available = fail_effect = true;
|
||||
auto speaker = Open(true);
|
||||
Check(Haptics::Queue(speaker.get(), 1, pcm.data(), 2, 2, true, unity.data()) == 0 &&
|
||||
!Haptics::UsesBluetooth(speaker.get()) && hid_closes == 1 && effects[1].empty(),
|
||||
"failed Bluetooth speaker routing leaked a session or suppressed fallback");
|
||||
fail_effect = false;
|
||||
now += 2001;
|
||||
std::vector<int16_t> mono(1024, 16384);
|
||||
const int volume = 16384;
|
||||
Queue(speaker, mono.data(), 1024, 1, false, &volume);
|
||||
auto pending = Bluetooth::Prepare(now * 1000000);
|
||||
Check(pending.size() == 1 && hid_opens == 2 && effects[1][7] == 0x30 &&
|
||||
std::abs(pending[0].audio[200] - 0.25f) < 1e-4f &&
|
||||
std::abs(pending[0].audio[201] - 0.25f) < 1e-4f,
|
||||
"Bluetooth mono int16 volume mapping or speaker routing recovery failed");
|
||||
}
|
||||
|
||||
void TestBluetoothEncodingDoesNotBlockAudioQueue() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0,
|
||||
"Bluetooth speaker did not open for concurrent encoding test");
|
||||
block_opus_encode = true;
|
||||
std::thread encoder([&] {
|
||||
auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
|
||||
Libs::Controller::DualSenseBluetooth::Send(pending);
|
||||
});
|
||||
encode_started.acquire();
|
||||
std::binary_semaphore queue_finished {0};
|
||||
std::atomic_bool queue_ok {false};
|
||||
std::thread audio([&] {
|
||||
queue_ok = Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true,
|
||||
unity.data()) != 0;
|
||||
queue_finished.release();
|
||||
});
|
||||
const bool queued_without_encoder =
|
||||
queue_finished.try_acquire_for(std::chrono::seconds(2));
|
||||
allow_encode.release();
|
||||
audio.join();
|
||||
encoder.join();
|
||||
Check(queued_without_encoder && queue_ok,
|
||||
"Bluetooth audio queue waited for Opus encoding");
|
||||
speaker.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothCloseCancelsPreparedReport() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
auto vibration = Open();
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Queue(speaker, sound.data(), 1024);
|
||||
Queue(vibration, sound.data(), 1024);
|
||||
const auto base = now * 1000000;
|
||||
auto pending = Bluetooth::Prepare(base);
|
||||
Check(pending.size() == 1, "Bluetooth report was not prepared before stream close");
|
||||
speaker.reset();
|
||||
Bluetooth::Send(pending);
|
||||
Check(hid_reports.empty(), "closed speaker's prepared report was sent");
|
||||
Queue(vibration, sound.data(), 1024);
|
||||
Bluetooth::Send(Bluetooth::Prepare(base + Bluetooth::PERIOD_NS));
|
||||
Check(hid_reports.size() == 1 && hid_reports.back()[140] == 0,
|
||||
"closing the speaker prevented the remaining haptic stream from sending");
|
||||
}
|
||||
|
||||
void TestBluetoothCloseDuringEncoding() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Queue(speaker, sound.data(), 1024);
|
||||
block_opus_encode = true;
|
||||
std::thread encoder([&] { Bluetooth::Send(Bluetooth::Prepare(now * 1000000)); });
|
||||
encode_started.acquire();
|
||||
std::binary_semaphore close_finished {0};
|
||||
std::thread closer([&] {
|
||||
speaker.reset();
|
||||
close_finished.release();
|
||||
});
|
||||
const bool closed_without_encoder = close_finished.try_acquire_for(std::chrono::seconds(2));
|
||||
const bool hid_alive_during_encode = hid_closes == 0;
|
||||
allow_encode.release();
|
||||
closer.join();
|
||||
encoder.join();
|
||||
Check(closed_without_encoder && hid_alive_during_encode && hid_closes == 1 &&
|
||||
hid_reports.empty(),
|
||||
"closing during encoding blocked, destroyed a live session, or sent stale audio");
|
||||
}
|
||||
|
||||
void TestBluetoothCloseWaitsForWriteWithoutBlockingQueue() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
auto vibration = Open();
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Queue(speaker, sound.data(), 1024);
|
||||
Queue(vibration, sound.data(), 1024);
|
||||
auto pending = Bluetooth::Prepare(now * 1000000);
|
||||
Check(pending.size() == 1, "Bluetooth report was not prepared before blocked write");
|
||||
block_hid_write = true;
|
||||
std::thread sender([&] { Bluetooth::Send(pending); });
|
||||
write_started.acquire();
|
||||
std::binary_semaphore close_started {0}, close_finished {0}, queue_finished {0};
|
||||
std::thread closer([&] {
|
||||
close_started.release();
|
||||
speaker.reset();
|
||||
close_finished.release();
|
||||
});
|
||||
close_started.acquire();
|
||||
const bool closed_before_write = close_finished.try_acquire_for(std::chrono::milliseconds(50));
|
||||
std::atomic_bool queue_ok {false};
|
||||
std::thread audio([&] {
|
||||
queue_ok =
|
||||
Haptics::Queue(vibration.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0;
|
||||
queue_finished.release();
|
||||
});
|
||||
const bool queued_without_write = queue_finished.try_acquire_for(std::chrono::seconds(2));
|
||||
allow_write.release();
|
||||
audio.join();
|
||||
closer.join();
|
||||
sender.join();
|
||||
Check(!closed_before_write && queued_without_write && queue_ok && hid_reports.size() == 1 &&
|
||||
effects[1][7] == 0,
|
||||
"close failed to drain the HID write independently of the audio queue");
|
||||
}
|
||||
|
||||
void TestBluetoothSpeakerWaitsForItsAudioBlock() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto vibration = Open();
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> haptic(2048, 0.5f);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Check(Haptics::Queue(vibration.get(), 1, haptic.data(), 1024, 2, true, unity.data()) != 0,
|
||||
"Bluetooth vibration stream did not open");
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000).empty(),
|
||||
"haptics sent a silent speaker packet before the audio batch arrived");
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 256, 2, true, unity.data()) != 0,
|
||||
"Bluetooth speaker did not accept a short block");
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000).empty(),
|
||||
"short Bluetooth speaker block was padded with silence immediately");
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 768, 2, true, unity.data()) != 0,
|
||||
"Bluetooth speaker did not accept the remainder of its block");
|
||||
auto reports = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
|
||||
Check(reports.size() == 1 && reports[0].audio[100] > 0.2f && reports[0].data[12] > 0,
|
||||
"Bluetooth speaker and haptics were not combined into a complete report");
|
||||
speaker.reset();
|
||||
vibration.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothShortHapticBlock() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto vibration = Open();
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Queue(vibration, sound.data(), 256);
|
||||
const auto base = now * 1000000;
|
||||
Check(Bluetooth::Prepare(base).empty() && Bluetooth::Prepare(base + 1000000).empty(),
|
||||
"short haptic block was padded with silence before its remainder arrived");
|
||||
Queue(vibration, sound.data(), 768);
|
||||
Check(Bluetooth::Prepare(base + 1000000).empty(),
|
||||
"completed haptic block skipped its speaker coalescing window");
|
||||
auto pending = Bluetooth::Prepare(base + 2000000);
|
||||
Check(pending.size() == 1 && pending[0].data[12 + 100] > 0,
|
||||
"complete haptic block did not play after its remainder arrived");
|
||||
}
|
||||
|
||||
void TestBluetoothIncrementalBatchCoalesces() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto vibration = Open();
|
||||
auto speaker = Open(true);
|
||||
std::array<float, 512> sound;
|
||||
sound.fill(0.25f);
|
||||
for (int block = 0; block < 3; ++block) {
|
||||
Queue(vibration, sound.data(), 256);
|
||||
Queue(speaker, sound.data(), 256);
|
||||
Check(Bluetooth::Prepare(now * 1000000).empty(),
|
||||
"incremental Bluetooth batch was padded before it was complete");
|
||||
now += 5;
|
||||
}
|
||||
Queue(vibration, sound.data(), 256);
|
||||
Check(Bluetooth::Prepare(now * 1000000).empty(),
|
||||
"completed haptics sent before the same batch's final speaker block arrived");
|
||||
Queue(speaker, sound.data(), 256);
|
||||
auto pending = Bluetooth::Prepare(now * 1000000);
|
||||
Check(pending.size() == 1 && pending[0].audio[1800] > 0.2f && pending[0].data[12 + 100] > 0,
|
||||
"completed incremental speaker and haptics batch did not play immediately");
|
||||
}
|
||||
|
||||
void TestBluetoothLowRateCompleteBlock() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true, 8000);
|
||||
std::array<float, 171 * 2> sound;
|
||||
sound.fill(0.25f);
|
||||
Queue(speaker, sound.data(), 171);
|
||||
auto pending = Bluetooth::Prepare(now * 1000000);
|
||||
Check(pending.size() == 1 && pending[0].audio[1000] > 0.2f,
|
||||
"resampler lookahead delayed a complete low-rate speaker block");
|
||||
}
|
||||
|
||||
void TestBluetoothCompleteStreamDoesNotWaitForPartialPeer() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto complete = Open(true);
|
||||
auto partial = Open(true);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Queue(complete, sound.data(), 1024);
|
||||
Queue(partial, sound.data(), 256);
|
||||
auto pending = Bluetooth::Prepare(now * 1000000);
|
||||
Check(pending.size() == 1 && pending[0].audio[100] > 0.4f && pending[0].audio[1000] > 0.2f,
|
||||
"complete speaker stream waited for a partial peer instead of mixing available audio");
|
||||
}
|
||||
|
||||
void TestBluetoothShortFinalBlock() {
|
||||
for (bool speaker: {false, true}) {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto port = Open(speaker);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Queue(port, sound.data(), 256);
|
||||
const auto base = now * 1000000;
|
||||
Check(Bluetooth::Prepare(base).empty(),
|
||||
"short final Bluetooth block sent before more audio could arrive");
|
||||
Check(Bluetooth::Prepare(base + Bluetooth::PERIOD_NS).size() == 1,
|
||||
"short final Bluetooth block was never played");
|
||||
}
|
||||
}
|
||||
|
||||
void TestBluetoothOverflowKeepsRecentAudio() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> sound(2048);
|
||||
for (int block = 1; block <= 4; ++block) {
|
||||
std::fill(sound.begin(), sound.end(), block * 0.1f);
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true,
|
||||
unity.data()) != 0,
|
||||
"Bluetooth speaker overflow queue failed");
|
||||
}
|
||||
auto first = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
|
||||
Check(first.size() == 1 && first[0].audio[200] > 0.15f && first[0].audio[200] < 0.25f,
|
||||
"Bluetooth queue overflow lost all buffered audio instead of the oldest block");
|
||||
auto second = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000 +
|
||||
Libs::Controller::DualSenseBluetooth::PERIOD_NS);
|
||||
Check(second.size() == 1 && second[0].audio[200] > 0.25f && second[0].audio[200] < 0.35f,
|
||||
"Bluetooth queue overflow did not preserve the next audio block");
|
||||
speaker.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothHapticOverflowKeepsRecentAudio() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto vibration = Open();
|
||||
std::vector<float> haptic(2048);
|
||||
for (int block = 1; block <= 4; ++block) {
|
||||
std::fill(haptic.begin(), haptic.end(), block * 0.1f);
|
||||
Check(Haptics::Queue(vibration.get(), 1, haptic.data(), 1024, 2, true,
|
||||
unity.data()) != 0,
|
||||
"Bluetooth haptic overflow queue failed");
|
||||
}
|
||||
const auto base = now * 1000000;
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(base).empty(),
|
||||
"haptic-only report skipped its coalescing window");
|
||||
auto first = Libs::Controller::DualSenseBluetooth::Prepare(base + 1000000);
|
||||
Check(first.size() == 1 && first[0].data[12 + 32] > 18 && first[0].data[12 + 32] < 32,
|
||||
"Bluetooth haptic overflow discarded buffered waveform instead of one report");
|
||||
vibration.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothRecoversAfterLateWrite() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true,
|
||||
unity.data()) != 0,
|
||||
"Bluetooth speaker could not buffer three reports");
|
||||
}
|
||||
const auto base = now * 1000000;
|
||||
const auto period = Libs::Controller::DualSenseBluetooth::PERIOD_NS;
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(base).size() == 1,
|
||||
"first Bluetooth report was not prepared");
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(base + period + 10000000).size() == 1,
|
||||
"late Bluetooth report was not prepared");
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(base + 2 * period).size() == 1,
|
||||
"Bluetooth sender did not recover its schedule after a late write");
|
||||
speaker.reset();
|
||||
}
|
||||
|
||||
void TestUsbOverflowKeepsRecentAudio() {
|
||||
Fixture f;
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> sound(2048);
|
||||
for (int block = 1; block <= 4; ++block) {
|
||||
std::fill(sound.begin(), sound.end(), block * 0.1f);
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true,
|
||||
unity.data()) != 0,
|
||||
"USB speaker overflow queue failed");
|
||||
}
|
||||
std::array<float, 4> first {};
|
||||
Check(!streams.empty() &&
|
||||
SDL_GetAudioStreamData(streams.back(), first.data(), sizeof(first)) ==
|
||||
sizeof(first) &&
|
||||
first[0] > 0.15f && first[0] < 0.25f,
|
||||
"USB queue overflow did not preserve the newest buffered audio");
|
||||
// A single oversized input must also preserve only its newest 80 ms.
|
||||
sound.assign(4096 * 2, 0.1f);
|
||||
std::fill(sound.begin() + 256 * 2, sound.end(), 0.5f);
|
||||
const auto queued = Haptics::Queue(speaker.get(), 1, sound.data(), 4096, 2, true, unity.data());
|
||||
Check(queued == 80000 && SDL_GetAudioStreamQueued(streams.back()) == 3840 * 4 * sizeof(float) &&
|
||||
SDL_GetAudioStreamData(streams.back(), first.data(), sizeof(first)) ==
|
||||
sizeof(first) &&
|
||||
first[0] == 0.5f && first[1] == 0.5f,
|
||||
"oversized USB block did not retain only the newest 80 ms");
|
||||
speaker.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothAmbiguousDevice() {
|
||||
Fixture f;
|
||||
wireless = hid_available = duplicate_hid = true;
|
||||
auto speaker = Open(true);
|
||||
Check(Haptics::Queue(speaker.get(), 1, pcm.data(), 2, 2, true, unity.data()) == 0 &&
|
||||
hid_opens == 0 && effects[1].empty(),
|
||||
"ambiguous Bluetooth devices were routed arbitrarily");
|
||||
speaker.reset();
|
||||
}
|
||||
|
||||
void TestUsbAndBluetoothFeatureParity() {
|
||||
Fixture f;
|
||||
connected_pads = {1, 3};
|
||||
wireless_pad3 = hid_available = true;
|
||||
auto wired_speaker = Open(true);
|
||||
auto wireless_speaker = Open(true);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Check(Haptics::Queue(wired_speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0 &&
|
||||
opened_device == 10,
|
||||
"Bluetooth pad disabled the wired pad speaker");
|
||||
Check(Haptics::Queue(wireless_speaker.get(), 3, sound.data(), 1024, 2, true, unity.data()) !=
|
||||
0 &&
|
||||
opened_hid_pad == 3,
|
||||
"wired pad disabled the Bluetooth speaker");
|
||||
Check(Haptics::Queue(wired_speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0 &&
|
||||
effects[1][7] == 0x30 && effects[3][7] == 0,
|
||||
"returning to the wired speaker left Bluetooth routing active");
|
||||
wireless_speaker.reset();
|
||||
wired_speaker.reset();
|
||||
}
|
||||
|
||||
void TestAudioSpeakerFallback() {
|
||||
Fixture f;
|
||||
Libs::Audio::Audio audio;
|
||||
@@ -545,6 +1105,26 @@ void TestAudioSpeakerRouting() {
|
||||
ExpectPcm({0.5f, 0.5f, 0, 0, 0.5f, 0.5f, 0, 0});
|
||||
}
|
||||
|
||||
void TestBluetoothAudioSpeakerRouting() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
devices.clear();
|
||||
Libs::Audio::Audio audio;
|
||||
const auto port = audio.AudioOutOpen(4, 1024, 48000, Libs::Audio::Audio::Format::FloatStereo);
|
||||
Check(port.IsValid() && default_stream != nullptr, "Bluetooth pad speaker port did not open");
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Libs::Audio::Audio::OutputParam output {port, sound.data()};
|
||||
Check(audio.AudioOutOutputs(&output, 1, false) == 1024 && hid_opens == 1 &&
|
||||
SDL_GetAudioStreamQueued(default_stream) == 0,
|
||||
"Bluetooth pad speaker also played on the host output");
|
||||
fail_hid_write = true;
|
||||
auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
|
||||
Libs::Controller::DualSenseBluetooth::Send(pending);
|
||||
Check(audio.AudioOutOutputs(&output, 1, false) == 1024 &&
|
||||
SDL_GetAudioStreamQueued(default_stream) > 0,
|
||||
"Bluetooth write failure did not use the normal speaker output");
|
||||
}
|
||||
|
||||
void TestAudioDefaultResumeFailure() {
|
||||
Fixture f;
|
||||
Libs::Audio::Audio audio;
|
||||
@@ -564,6 +1144,7 @@ int main() {
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
TestAudioSpeakerFallback();
|
||||
TestAudioSpeakerRouting();
|
||||
TestBluetoothAudioSpeakerRouting();
|
||||
TestAudioDefaultResumeFailure();
|
||||
TestFormatsAndVolume();
|
||||
TestDiscoveryAndHotplug();
|
||||
@@ -571,6 +1152,25 @@ int main() {
|
||||
TestSpeakerUnplug();
|
||||
TestSpeakerSwitchAndFailures();
|
||||
TestSpeakerRequiresUnambiguousUsbDevice();
|
||||
TestBluetoothAudioAndIdentity();
|
||||
TestBluetoothFailureFallsBack();
|
||||
TestBluetoothFormatsAndRoutingFailure();
|
||||
TestBluetoothEncodingDoesNotBlockAudioQueue();
|
||||
TestBluetoothCloseCancelsPreparedReport();
|
||||
TestBluetoothCloseDuringEncoding();
|
||||
TestBluetoothCloseWaitsForWriteWithoutBlockingQueue();
|
||||
TestBluetoothSpeakerWaitsForItsAudioBlock();
|
||||
TestBluetoothShortHapticBlock();
|
||||
TestBluetoothIncrementalBatchCoalesces();
|
||||
TestBluetoothLowRateCompleteBlock();
|
||||
TestBluetoothCompleteStreamDoesNotWaitForPartialPeer();
|
||||
TestBluetoothShortFinalBlock();
|
||||
TestBluetoothOverflowKeepsRecentAudio();
|
||||
TestBluetoothHapticOverflowKeepsRecentAudio();
|
||||
TestBluetoothRecoversAfterLateWrite();
|
||||
TestUsbOverflowKeepsRecentAudio();
|
||||
TestBluetoothAmbiguousDevice();
|
||||
TestUsbAndBluetoothFeatureParity();
|
||||
TestFailuresAndBoundedQueue();
|
||||
TestRumbleLeaseAndDuration();
|
||||
TestSwitchStopsOldRumble();
|
||||
|
||||
@@ -192,13 +192,25 @@ uint8_t *Allocate(uint64_t size, uint32_t protection = PAGE_READWRITE,
|
||||
Check(memory == reinterpret_cast<void *>(test_address),
|
||||
"fixed low VirtualAlloc failed");
|
||||
#elif defined(__APPLE__)
|
||||
mach_vm_address_t raw = test_address;
|
||||
Check(mach_vm_allocate(mach_task_self(), &raw, size, VM_FLAGS_FIXED) ==
|
||||
KERN_SUCCESS &&
|
||||
mach_vm_protect(mach_task_self(), raw, size, false,
|
||||
static_cast<vm_prot_t>(ToHostProt(protection))) ==
|
||||
KERN_SUCCESS,
|
||||
"fixed low mach_vm_allocate failed");
|
||||
mach_vm_address_t raw = 0;
|
||||
bool allocated = false;
|
||||
constexpr uintptr_t stride = 0x0000000100000000ull;
|
||||
for (uintptr_t candidate = test_address;
|
||||
Libs::Graphics::GuestRange{candidate, size}.Valid(); candidate += stride) {
|
||||
raw = candidate;
|
||||
if (mach_vm_allocate(mach_task_self(), &raw, size, VM_FLAGS_FIXED) !=
|
||||
KERN_SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
if (mach_vm_protect(mach_task_self(), raw, size, false,
|
||||
static_cast<vm_prot_t>(ToHostProt(protection))) ==
|
||||
KERN_SUCCESS) {
|
||||
allocated = true;
|
||||
break;
|
||||
}
|
||||
mach_vm_deallocate(mach_task_self(), raw, size);
|
||||
}
|
||||
Check(allocated, "no free fixed guest address found");
|
||||
auto *memory = reinterpret_cast<uint8_t *>(raw);
|
||||
AllocationSizes()[memory] = static_cast<size_t>(size);
|
||||
#else
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
#include "libs/libPngEnc.cpp"
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STBI_ONLY_PNG
|
||||
#define STBI_NO_STDIO
|
||||
#include "stb_image.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
namespace Loader {
|
||||
void SymbolDatabase::Add(const SymbolResolve&, uint64_t, const std::string&) {}
|
||||
namespace Timer {
|
||||
double GetTimeMs() {
|
||||
return 0.0;
|
||||
}
|
||||
} // namespace Timer
|
||||
} // namespace Loader
|
||||
|
||||
namespace {
|
||||
using namespace Libs::PngEnc;
|
||||
|
||||
#define CHECK(condition) \
|
||||
do { \
|
||||
if (!(condition)) { \
|
||||
std::fprintf(stderr, "PngEncTests:%d: %s\n", __LINE__, #condition); \
|
||||
std::abort(); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
struct Encoder {
|
||||
PngEncContext context {};
|
||||
void* handle = nullptr;
|
||||
|
||||
explicit Encoder(uint32_t max_filters = 4) {
|
||||
const PngEncCreateParam create {sizeof(PngEncCreateParam), 0, PNG_ENC_MAX_IMAGE_WIDTH,
|
||||
max_filters};
|
||||
CHECK(PngEncQueryMemorySize(&create) == sizeof(context));
|
||||
CHECK(PngEncCreate(&create, &context, sizeof(context), &handle) == 0);
|
||||
}
|
||||
|
||||
~Encoder() {
|
||||
CHECK(PngEncDelete(handle) == 0);
|
||||
CHECK(PngEncDelete(handle) == PNG_ENC_ERROR_INVALID_HANDLE);
|
||||
}
|
||||
};
|
||||
|
||||
PngEncEncodeParam EncodeParam(const uint8_t* source, uint32_t source_size, uint8_t* destination,
|
||||
uint32_t destination_size) {
|
||||
PngEncEncodeParam param {};
|
||||
param.image_mem_addr = source;
|
||||
param.png_mem_addr = destination;
|
||||
param.image_mem_size = source_size;
|
||||
param.png_mem_size = destination_size;
|
||||
param.image_width = 2;
|
||||
param.image_height = 2;
|
||||
param.image_pitch = 8;
|
||||
param.color_space = PNG_ENC_COLOR_SPACE_RGBA;
|
||||
param.bit_depth = 8;
|
||||
param.compression_level = 6;
|
||||
return param;
|
||||
}
|
||||
|
||||
uint32_t ReadBe32(const uint8_t* bytes) {
|
||||
return (uint32_t {bytes[0]} << 24u) | (uint32_t {bytes[1]} << 16u) |
|
||||
(uint32_t {bytes[2]} << 8u) | bytes[3];
|
||||
}
|
||||
|
||||
void TestPixels() {
|
||||
Encoder encoder;
|
||||
constexpr std::array<uint8_t, 16> rgba {255, 0, 13, 0, 4, 210, 6, 255,
|
||||
7, 8, 220, 64, 111, 12, 113, 128};
|
||||
for (const bool bgr: {false, true}) {
|
||||
for (const bool alpha: {false, true}) {
|
||||
for (const bool padded: {false, true}) {
|
||||
const uint32_t pitch = padded ? 12 : 8;
|
||||
std::vector<uint8_t> source(pitch * 2, 0xee);
|
||||
std::vector<uint8_t> expected;
|
||||
for (uint32_t y = 0; y < 2; ++y) {
|
||||
for (uint32_t x = 0; x < 2; ++x) {
|
||||
const auto* pixel = rgba.data() + (y * 2 + x) * 4;
|
||||
auto* input = source.data() + y * pitch + x * 4;
|
||||
input[0] = pixel[bgr ? 2 : 0];
|
||||
input[1] = pixel[1];
|
||||
input[2] = pixel[bgr ? 0 : 2];
|
||||
input[3] = pixel[3];
|
||||
expected.insert(expected.end(), pixel, pixel + (alpha ? 4 : 3));
|
||||
}
|
||||
}
|
||||
std::array<uint8_t, 1024> png {};
|
||||
auto param = EncodeParam(source.data(), source.size(), png.data(), png.size());
|
||||
param.pixel_format =
|
||||
bgr ? PNG_ENC_PIXEL_FORMAT_B8G8R8A8 : PNG_ENC_PIXEL_FORMAT_R8G8B8A8;
|
||||
param.color_space = alpha ? PNG_ENC_COLOR_SPACE_RGBA : PNG_ENC_COLOR_SPACE_RGB;
|
||||
param.image_pitch = pitch;
|
||||
PngEncOutputInfo info {};
|
||||
const auto size = PngEncEncode(encoder.handle, ¶m, &info);
|
||||
CHECK(size > 0 && info.data_size == static_cast<uint32_t>(size));
|
||||
CHECK(info.processed_height == 2);
|
||||
int width = 0, height = 0, components = 0;
|
||||
auto* decoded =
|
||||
stbi_load_from_memory(png.data(), size, &width, &height, &components, 0);
|
||||
CHECK(decoded != nullptr);
|
||||
CHECK(width == 2 && height == 2 && components == (alpha ? 4 : 3));
|
||||
CHECK(std::memcmp(decoded, expected.data(), expected.size()) == 0);
|
||||
stbi_image_free(decoded);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestFilterMasks() {
|
||||
Encoder encoder;
|
||||
alignas(4) std::array<uint8_t, 16> pixels {};
|
||||
std::array<uint8_t, 1024> png {};
|
||||
auto param = EncodeParam(pixels.data(), pixels.size(), png.data(), png.size());
|
||||
for (uint16_t mask = 0; mask <= 15; ++mask) {
|
||||
param.filter_type = mask;
|
||||
const auto size = PngEncEncode(encoder.handle, ¶m, nullptr);
|
||||
CHECK(size > 0);
|
||||
CHECK(std::memcmp(png.data() + 37, "IDAT", 4) == 0);
|
||||
std::array<char, 18> filtered {};
|
||||
CHECK(stbi_zlib_decode_buffer(filtered.data(), filtered.size(),
|
||||
reinterpret_cast<const char*>(png.data() + 41),
|
||||
ReadBe32(png.data() + 33)) == filtered.size());
|
||||
for (size_t row = 0; row < 2; ++row) {
|
||||
const auto filter = static_cast<uint8_t>(filtered[row * 9]);
|
||||
CHECK(filter <= 4);
|
||||
CHECK(filter == 0 ? (mask == 0 || mask == 15) : (mask & (1u << (filter - 1u))) != 0);
|
||||
}
|
||||
}
|
||||
param.filter_type = 16;
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, nullptr) == PNG_ENC_ERROR_INVALID_PARAM);
|
||||
for (uint32_t count = 0; count <= 4; ++count) {
|
||||
Encoder limited(count);
|
||||
for (uint16_t mask = 0; mask <= 15; ++mask) {
|
||||
param.filter_type = mask;
|
||||
const auto result = PngEncEncode(limited.handle, ¶m, nullptr);
|
||||
CHECK(std::popcount(mask) <= count ? result > 0
|
||||
: result == PNG_ENC_ERROR_INVALID_PARAM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestOutputCapacity() {
|
||||
Encoder encoder;
|
||||
alignas(4) std::array<uint8_t, 16> pixels {};
|
||||
std::array<uint8_t, 1024> png {};
|
||||
auto param = EncodeParam(pixels.data(), pixels.size(), png.data(), png.size());
|
||||
const auto size = PngEncEncode(encoder.handle, ¶m, nullptr);
|
||||
CHECK(size > 0);
|
||||
std::vector<uint8_t> guarded(size + 2, 0xa5);
|
||||
param.png_mem_addr = guarded.data() + 1;
|
||||
param.png_mem_size = size;
|
||||
PngEncOutputInfo info {};
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, &info) == size);
|
||||
CHECK(guarded.front() == 0xa5 && guarded.back() == 0xa5);
|
||||
CHECK(std::memcmp(guarded.data() + 1, png.data(), size) == 0);
|
||||
std::fill(guarded.begin(), guarded.end(), 0xa5);
|
||||
param.png_mem_size = size - 1;
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, &info) == PNG_ENC_ERROR_DATA_OVERFLOW);
|
||||
CHECK(info.data_size == 0 && info.processed_height == 0);
|
||||
CHECK(std::all_of(guarded.begin(), guarded.end(), [](uint8_t byte) { return byte == 0xa5; }));
|
||||
}
|
||||
|
||||
void TestValidation() {
|
||||
Encoder encoder;
|
||||
PngEncCreateParam create {sizeof(PngEncCreateParam), 0, 2, 4};
|
||||
CHECK(PngEncQueryMemorySize(nullptr) == PNG_ENC_ERROR_INVALID_ADDR);
|
||||
CHECK(PngEncCreate(&create, nullptr, sizeof(PngEncContext), &encoder.handle) ==
|
||||
PNG_ENC_ERROR_INVALID_ADDR);
|
||||
CHECK(PngEncCreate(&create, &encoder.context, 0, &encoder.handle) ==
|
||||
PNG_ENC_ERROR_INVALID_SIZE);
|
||||
create.max_image_width = 0;
|
||||
CHECK(PngEncQueryMemorySize(&create) == PNG_ENC_ERROR_INVALID_SIZE);
|
||||
create.max_image_width = PNG_ENC_MAX_IMAGE_WIDTH + 1;
|
||||
CHECK(PngEncQueryMemorySize(&create) == PNG_ENC_ERROR_INVALID_SIZE);
|
||||
create.max_image_width = 2;
|
||||
create.this_size = 0;
|
||||
CHECK(PngEncQueryMemorySize(&create) == PNG_ENC_ERROR_INVALID_PARAM);
|
||||
create.this_size = sizeof(create);
|
||||
create.max_filter_number = 5;
|
||||
CHECK(PngEncQueryMemorySize(&create) == PNG_ENC_ERROR_INVALID_PARAM);
|
||||
alignas(4) std::array<uint8_t, 16> pixels {};
|
||||
std::array<uint8_t, 1024> png {};
|
||||
const auto valid = EncodeParam(pixels.data(), pixels.size(), png.data(), png.size());
|
||||
CHECK(PngEncEncode(nullptr, &valid, nullptr) == PNG_ENC_ERROR_INVALID_HANDLE);
|
||||
CHECK(PngEncEncode(encoder.handle, nullptr, nullptr) == PNG_ENC_ERROR_INVALID_PARAM);
|
||||
for (const auto width: {0u, PNG_ENC_MAX_IMAGE_WIDTH + 1, 0x40000000u, 0xffffffffu}) {
|
||||
auto param = valid;
|
||||
param.image_width = width;
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, nullptr) == PNG_ENC_ERROR_INVALID_SIZE);
|
||||
}
|
||||
for (const auto height: {0u, PNG_ENC_MAX_IMAGE_HEIGHT + 1, 0xffffffffu}) {
|
||||
auto param = valid;
|
||||
param.image_width = 1;
|
||||
param.image_height = height;
|
||||
param.image_mem_size = 0xffffffffu;
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, nullptr) == PNG_ENC_ERROR_INVALID_SIZE);
|
||||
}
|
||||
for (const auto height: {262144u, 400000u, 0x7ffffu}) {
|
||||
auto param = valid;
|
||||
param.image_width = 1024;
|
||||
param.image_height = height;
|
||||
param.image_pitch = 4096;
|
||||
param.image_mem_size = 0xffffffffu;
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, nullptr) == PNG_ENC_ERROR_INVALID_SIZE);
|
||||
}
|
||||
for (const auto pitch: {0u, 7u, 9u, 0xfffffffcu}) {
|
||||
auto param = valid;
|
||||
param.image_pitch = pitch;
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, nullptr) == PNG_ENC_ERROR_INVALID_SIZE);
|
||||
}
|
||||
auto param = valid;
|
||||
param.image_pitch = 12;
|
||||
param.image_mem_size = 20;
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, nullptr) == PNG_ENC_ERROR_INVALID_SIZE);
|
||||
param = valid;
|
||||
param.image_mem_size--;
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, nullptr) == PNG_ENC_ERROR_INVALID_SIZE);
|
||||
param = valid;
|
||||
param.image_mem_addr = nullptr;
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, nullptr) == PNG_ENC_ERROR_INVALID_ADDR);
|
||||
param.image_mem_addr = pixels.data() + 1;
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, nullptr) == PNG_ENC_ERROR_INVALID_ADDR);
|
||||
param = valid;
|
||||
param.bit_depth = 16;
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, nullptr) == PNG_ENC_ERROR_INVALID_PARAM);
|
||||
param = valid;
|
||||
param.clut_number = 1;
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, nullptr) == PNG_ENC_ERROR_INVALID_PARAM);
|
||||
param = valid;
|
||||
param.compression_level = 10;
|
||||
CHECK(PngEncEncode(encoder.handle, ¶m, nullptr) == PNG_ENC_ERROR_INVALID_PARAM);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestPixels();
|
||||
TestFilterMasks();
|
||||
TestOutputCapacity();
|
||||
TestValidation();
|
||||
std::puts("PNG encoder tests passed");
|
||||
}
|
||||
@@ -219,6 +219,56 @@ void TestUnbasedFlatCacheHitMaterializes() {
|
||||
"unbased FLAT plan produced unexpected descriptors");
|
||||
}
|
||||
|
||||
void TestWrittenDescriptorUsesStrictReaderOnce() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
Program program;
|
||||
program.stage = Libs::Graphics::ShaderType::Compute;
|
||||
program.srt_plan_complete = true;
|
||||
program.resource_tracking_complete = true;
|
||||
auto &block = AddValueBlock(program);
|
||||
program.memory_info.push_back({.kind = ResourceKind::ScalarAddress});
|
||||
auto &handle = block.AppendNewInst(ValueOpcode::GetAddressResource,
|
||||
{Value(0x1000u), Value(0u)});
|
||||
auto &offset = block.AppendNewInst(ValueOpcode::GetUserData,
|
||||
{Value(static_cast<ScalarReg>(0))});
|
||||
auto &read = block.AppendNewInst(ValueOpcode::LoadAddressU32,
|
||||
{Value(&handle), Value(&offset), Value(0u), Value(false)});
|
||||
read.SetFlags(MemoryFlags{.index = 0});
|
||||
DescriptorSource source;
|
||||
source.dwords = {Value(&read), Value(0u), Value(4u), Value(0u)};
|
||||
source.dword_count = 4;
|
||||
program.descriptor_sources.push_back(source);
|
||||
program.info.buffers.push_back({.source = 0, .written = true});
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
struct Reads { uint32_t ordinary = 0; uint32_t strict = 0; bool clean = false; } reads;
|
||||
const std::array<uint32_t, 1> user_data{4u};
|
||||
const SrtRuntime runtime{
|
||||
.user_data = user_data,
|
||||
.read_memory = +[](void *data, uint64_t, std::span<uint32_t> words) {
|
||||
++static_cast<Reads *>(data)->ordinary;
|
||||
words[0] = 0x8000u;
|
||||
return true;
|
||||
},
|
||||
.userdata = &reads,
|
||||
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
|
||||
auto &reads = *static_cast<Reads *>(data);
|
||||
++reads.strict;
|
||||
if (!reads.clean || address != 0x1004u) return false;
|
||||
words[0] = 0x8000u;
|
||||
return true;
|
||||
}};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
reads.ordinary == 0 && reads.strict == 1,
|
||||
"GPU-dirty dynamic writable descriptor bypassed strict provenance");
|
||||
reads.clean = true;
|
||||
reads.strict = 0;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
reads.ordinary == 0 && reads.strict == 1 && snapshot.buffers[0].dwords[0] == 0x8000u,
|
||||
"writable descriptor was evaluated twice or scalar EXEC suppressed its read");
|
||||
}
|
||||
|
||||
void TestFailedMaterializationRejectsStage() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
auto plan = UserDataBufferPlan();
|
||||
@@ -262,6 +312,7 @@ int main() {
|
||||
TestMappedSrtUsesDirectReaderByDefault();
|
||||
TestIntegerRuntimeValueFollowsSrtReads();
|
||||
TestUnbasedFlatCacheHitMaterializes();
|
||||
TestWrittenDescriptorUsesStrictReaderOnce();
|
||||
TestFailedMaterializationRejectsStage();
|
||||
TestMixedSamplerDuplicatesTheCorrectSnapshot();
|
||||
std::puts("ResourceMaterializationTests: all cases passed");
|
||||
|
||||
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user