mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-20 19:07:01 +02:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e317465d8a | |||
| 684330c9e4 | |||
| f3dc5ee4a9 | |||
| 2545723065 | |||
| 95c89229ad | |||
| 7804eef9b6 | |||
| 82a2f4ad42 | |||
| e1be2ead54 | |||
| 89792a3bca | |||
| 39e23ac92e | |||
| a67a033ff8 | |||
| d1c99bbfb0 | |||
| a9620f98f4 | |||
| 28f29b8e6f | |||
| 3353c1ff3b | |||
| 70a475ec8c |
@@ -113,7 +113,7 @@ jobs:
|
||||
shell: cmd
|
||||
run: |
|
||||
cmake --build _Build/windows --target launcher ^
|
||||
audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
|
||||
shader_recompiler_compute_tests ^
|
||||
resource_tracking_tests scalar_provenance_tests ^
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests --parallel
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
- name: Test
|
||||
shell: cmd
|
||||
run: |
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
|
||||
|
||||
- name: Install
|
||||
shell: cmd
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --build _Build/macos \
|
||||
--target launcher audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
--target launcher audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
memory_tracker_tests page_manager_tests shader_recompiler_compute_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
|
||||
@@ -212,7 +212,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/macos --output-on-failure \
|
||||
-R '^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
@@ -386,7 +386,7 @@ jobs:
|
||||
run: |
|
||||
cmake --build _Build/linux \
|
||||
--target launcher page_manager_tests memory_tracker_tests \
|
||||
audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
|
||||
--parallel
|
||||
@@ -395,7 +395,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/linux --output-on-failure \
|
||||
-R '^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
|
||||
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
|
||||
|
||||
+16
-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,9 +495,15 @@ target_include_directories(ngs2_sampler_tests PRIVATE ${inc_headers})
|
||||
add_executable(pad_haptics_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/PadHapticsTests.cpp"
|
||||
)
|
||||
target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static)
|
||||
target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static opus)
|
||||
target_include_directories(pad_haptics_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(controller_settings_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/ControllerSettingsTests.cpp"
|
||||
)
|
||||
target_link_libraries(controller_settings_tests common fmt::fmt SDL3::SDL3-static)
|
||||
target_include_directories(controller_settings_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/SaveDataMemoryTests.cpp"
|
||||
)
|
||||
@@ -606,8 +616,10 @@ if(BUILD_TESTING)
|
||||
add_test(NAME kernel_file_system COMMAND $<TARGET_FILE:kernel_file_system_tests>)
|
||||
add_test(NAME sync_on_address COMMAND $<TARGET_FILE:sync_on_address_tests>)
|
||||
add_test(NAME audio_out2_port COMMAND $<TARGET_FILE:audio_out2_port_tests>)
|
||||
add_test(NAME audio_out_timing COMMAND $<TARGET_FILE:audio_out_timing_tests>)
|
||||
add_test(NAME ngs2_sampler COMMAND $<TARGET_FILE:ngs2_sampler_tests>)
|
||||
add_test(NAME pad_haptics COMMAND $<TARGET_FILE:pad_haptics_tests>)
|
||||
add_test(NAME controller_settings COMMAND $<TARGET_FILE:controller_settings_tests>)
|
||||
add_test(NAME 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>)
|
||||
@@ -682,8 +694,10 @@ if(BUILD_TESTING)
|
||||
shader_recompiler_compute_tests
|
||||
virtual_memory_allocation_tests
|
||||
audio_out2_port_tests
|
||||
audio_out_timing_tests
|
||||
ngs2_sampler_tests
|
||||
pad_haptics_tests
|
||||
controller_settings_tests
|
||||
save_data_memory_tests
|
||||
png_enc_tests
|
||||
ces_tests
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -25,6 +25,8 @@ struct ControlInfo {
|
||||
Controller::Axis axis = Controller::Axis::AxisMax;
|
||||
bool positive = false;
|
||||
float touch_x = 0.0f;
|
||||
|
||||
Controller::Setting setting {};
|
||||
};
|
||||
|
||||
static constexpr std::array CONTROL_INFO = {
|
||||
@@ -55,6 +57,10 @@ static constexpr std::array CONTROL_INFO = {
|
||||
ControlInfo {"RightStickRight", 0, Controller::Axis::RightX, true},
|
||||
ControlInfo {"RightStickUp", 0, Controller::Axis::RightY, false},
|
||||
ControlInfo {"RightStickDown", 0, Controller::Axis::RightY, true},
|
||||
ControlInfo {.name = "SpeakerVolume", .setting = Controller::Setting::SpeakerVolume},
|
||||
ControlInfo {.name = "VibrationIntensity", .setting = Controller::Setting::VibrationIntensity},
|
||||
ControlInfo {.name = "TriggerEffectIntensity",
|
||||
.setting = Controller::Setting::TriggerEffectIntensity},
|
||||
};
|
||||
|
||||
constexpr std::size_t INVALID_CONTROL = CONTROL_INFO.size();
|
||||
@@ -242,6 +248,12 @@ void SetStickAxis(Controller::Axis axis, bool negative, bool positive) {
|
||||
Controller::SetAxis(Controller::HOST_INPUT_CONTROLLER_ID, axis, value);
|
||||
}
|
||||
|
||||
void CycleSettingOnPress(Controller::Setting setting, bool down) {
|
||||
if (down) {
|
||||
Controller::CycleSetting(setting);
|
||||
}
|
||||
}
|
||||
|
||||
void SetControl(std::size_t control, bool down) {
|
||||
if (control == INVALID_CONTROL) {
|
||||
return;
|
||||
@@ -256,6 +268,10 @@ void SetControl(std::size_t control, bool down) {
|
||||
SetButton(info.button, down);
|
||||
return;
|
||||
}
|
||||
if (info.axis == Controller::Axis::AxisMax) {
|
||||
CycleSettingOnPress(info.setting, down);
|
||||
return;
|
||||
}
|
||||
|
||||
struct AxisKeys {
|
||||
bool negative = false;
|
||||
@@ -280,6 +296,9 @@ void DefaultKeyboardInput(int key_code, bool down) {
|
||||
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
|
||||
case SDLK_BACKSPACE: SetTouchPad(0.25f, down); return;
|
||||
case SDLK_TAB: SetTouchPad(0.75f, down); return;
|
||||
case SDLK_1: CycleSettingOnPress(Controller::Setting::SpeakerVolume, down); return;
|
||||
case SDLK_2: CycleSettingOnPress(Controller::Setting::VibrationIntensity, down); return;
|
||||
case SDLK_3: CycleSettingOnPress(Controller::Setting::TriggerEffectIntensity, down); return;
|
||||
case SDLK_A:
|
||||
left.left = down;
|
||||
SetStickAxis(Controller::Axis::LeftX, left.left, left.right);
|
||||
|
||||
@@ -617,7 +617,6 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
|
||||
}
|
||||
}
|
||||
IR::TrackResources(ir, decoded, native_cfg);
|
||||
IR::EliminateDeadCode(ir.blocks);
|
||||
TranslateResult result;
|
||||
result.program = std::move(ir);
|
||||
if (options.dump_ir) {
|
||||
|
||||
@@ -476,6 +476,7 @@ struct DescriptorSource {
|
||||
uint32_t table_offset = 0;
|
||||
Value key_count;
|
||||
Value selector_mask;
|
||||
std::vector<uint32_t> sources;
|
||||
|
||||
bool operator==(const IndirectImage& other) const = default;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
@@ -152,17 +152,29 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
friend void EliminateDeadCode(const std::vector<Block*>& blocks);
|
||||
|
||||
void AddUse(Inst* used, size_t operand);
|
||||
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;
|
||||
bool live = false;
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "graphics/shader/recompiler/ir/passes/DeadCodeElimination.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
||||
void RemoveIdentities(const BlockList& blocks) {
|
||||
@@ -18,23 +20,32 @@ void RemoveIdentities(const BlockList& blocks) {
|
||||
}
|
||||
|
||||
void EliminateDeadCode(const BlockList& blocks) {
|
||||
bool changed;
|
||||
do {
|
||||
changed = false;
|
||||
for (auto block = blocks.rbegin(); block != blocks.rend(); block++) {
|
||||
auto& instructions = (*block)->Instructions();
|
||||
auto inst = instructions.end();
|
||||
while (inst != instructions.begin()) {
|
||||
--inst;
|
||||
if (inst->HasUses() || inst->MayHaveSideEffects()) {
|
||||
continue;
|
||||
}
|
||||
inst->Invalidate();
|
||||
inst = instructions.erase(inst);
|
||||
changed = true;
|
||||
std::vector<Inst*> pending;
|
||||
for (auto* block: blocks) {
|
||||
for (auto& inst: *block) {
|
||||
// Retained resource-planning instructions live outside the block list.
|
||||
inst.live = inst.MayHaveSideEffects() || std::ranges::any_of(inst.Uses(),
|
||||
[](const Use& use) { return use.user->Parent() == nullptr; });
|
||||
if (inst.live) pending.push_back(&inst);
|
||||
}
|
||||
}
|
||||
while (!pending.empty()) {
|
||||
const auto* inst = pending.back();
|
||||
pending.pop_back();
|
||||
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
|
||||
auto* input = inst->Arg(arg).TryInstruction();
|
||||
if (input != nullptr && input->Parent() != nullptr && !input->live) {
|
||||
input->live = true;
|
||||
pending.push_back(input);
|
||||
}
|
||||
}
|
||||
} while (changed);
|
||||
}
|
||||
// Unlink every dead operand before freeing nodes in a dead Phi cycle.
|
||||
for (auto* block: blocks)
|
||||
for (auto& inst: *block)
|
||||
if (!inst.live) inst.Invalidate();
|
||||
for (auto* block: blocks)
|
||||
std::erase_if(block->Instructions(), [](const Inst& inst) { return !inst.live; });
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
||||
@@ -227,100 +227,114 @@ bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t dynamic_offset,
|
||||
|
||||
bool MaterializeIndirectImage(const ResourcePlan& program,
|
||||
const DescriptorSource::IndirectImage& indirect,
|
||||
const DescriptorValue& material_value,
|
||||
const DescriptorValue& table_value, uint32_t image_index,
|
||||
uint32_t image_index,
|
||||
const SrtRuntime& runtime, SrtWalker& clean,
|
||||
ResourceSnapshot& snapshot,
|
||||
ResourceSpecialization& specialization) {
|
||||
const auto& sources = indirect.sources;
|
||||
auto& keys = program.material_keys;
|
||||
uint64_t table_base = 0;
|
||||
uint64_t table_size = UINT64_MAX; // Scalar addresses have no buffer descriptor bounds.
|
||||
ShaderBufferResource table;
|
||||
if (table_value.dword_count == 2u) {
|
||||
table_base = (static_cast<uint64_t>(table_value.dwords[1]) << 32u) | table_value.dwords[0];
|
||||
} else if (DecodeBufferDescriptor(table_value, table)) {
|
||||
table_base = table.Base48();
|
||||
table_size = table.GetSize();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
auto& keys = program.material_keys;
|
||||
keys.clear();
|
||||
if (indirect.material_source == UINT32_MAX) {
|
||||
uint32_t key_count = 0;
|
||||
const bool evaluated = clean.Evaluate(indirect.key_count, key_count);
|
||||
if (std::bit_cast<int32_t>(key_count) <= 0) key_count = 0;
|
||||
if (table_value.dword_count != 2u || !evaluated ||
|
||||
key_count > MaxIndirectImageProbes ||
|
||||
uint64_t {indirect.table_offset} + uint64_t {key_count} * 32u > UINT32_MAX + 1ull) {
|
||||
if (sources.empty()) {
|
||||
keys.clear();
|
||||
DescriptorValue material_value;
|
||||
DescriptorValue table_value;
|
||||
if ((indirect.material_source != UINT32_MAX &&
|
||||
!clean.EvaluateDescriptor(indirect.material_source, material_value)) ||
|
||||
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
|
||||
return false;
|
||||
}
|
||||
keys.resize(key_count);
|
||||
std::iota(keys.begin(), keys.end(), 0u);
|
||||
} else if (!indirect.selector_mask.IsEmpty()) {
|
||||
uint32_t mask = 0;
|
||||
uint32_t count = 0;
|
||||
if (material_value.dword_count != 2u || table_value.dword_count != 2u ||
|
||||
!clean.Evaluate(indirect.selector_mask, mask) ||
|
||||
!clean.Evaluate(indirect.key_count, count) || count == 0u || count > 32u) {
|
||||
ShaderBufferResource table;
|
||||
if (table_value.dword_count == 2u) {
|
||||
table_base = (static_cast<uint64_t>(table_value.dwords[1]) << 32u) | table_value.dwords[0];
|
||||
} else if (DecodeBufferDescriptor(table_value, table)) {
|
||||
table_base = table.Base48();
|
||||
table_size = table.GetSize();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (count < 32u) mask &= (1u << count) - 1u;
|
||||
const auto material_base =
|
||||
(static_cast<uint64_t>(material_value.dwords[1]) << 32u) | material_value.dwords[0];
|
||||
keys.reserve(std::popcount(mask));
|
||||
while (mask != 0u) {
|
||||
const auto index = std::countr_zero(mask);
|
||||
const auto offset = static_cast<uint64_t>(indirect.selector_offset) +
|
||||
static_cast<uint64_t>(index) * indirect.selector_stride;
|
||||
if (offset > UINT32_MAX) return false;
|
||||
uint32_t key = 0;
|
||||
if (!ReadScalarTable(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
|
||||
runtime, {&key, 1})) return false;
|
||||
keys.push_back(key);
|
||||
mask &= mask - 1u;
|
||||
}
|
||||
std::ranges::sort(keys);
|
||||
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
||||
} else {
|
||||
ShaderBufferResource material;
|
||||
if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u ||
|
||||
material.Stride() != indirect.selector_stride) {
|
||||
return false;
|
||||
}
|
||||
// The first aligned offset includes the immediate added after shader U32 arithmetic.
|
||||
const auto step = std::max<uint64_t>(4u,
|
||||
std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u));
|
||||
const uint64_t first = indirect.selector_offset;
|
||||
const auto size = material.GetSize();
|
||||
const auto limit = std::min(first + (uint64_t {1} << 32u) - step, size >= 4u ? size - 4u : 0u);
|
||||
const auto probe_count = size >= 4u && first <= limit ? (limit - first) / step + 1u : 0u;
|
||||
if (probe_count > MaxIndirectImageProbes) {
|
||||
return false;
|
||||
}
|
||||
keys.reserve(static_cast<size_t>(probe_count) + 1u);
|
||||
keys.push_back(0u);
|
||||
for (uint64_t probe = 0, offset = first; probe < probe_count; ++probe, offset += step) {
|
||||
uint32_t key = 0;
|
||||
if (!ReadScalarTable(material.Base48(), size, offset, runtime, {&key, 1})) {
|
||||
if (indirect.material_source == UINT32_MAX) {
|
||||
uint32_t key_count = 0;
|
||||
const bool evaluated = clean.Evaluate(indirect.key_count, key_count);
|
||||
if (std::bit_cast<int32_t>(key_count) <= 0) key_count = 0;
|
||||
if (table_value.dword_count != 2u || !evaluated ||
|
||||
key_count > MaxIndirectImageProbes ||
|
||||
uint64_t {indirect.table_offset} + uint64_t {key_count} * 32u > UINT32_MAX + 1ull) {
|
||||
return false;
|
||||
}
|
||||
keys.push_back(key);
|
||||
keys.resize(key_count);
|
||||
std::iota(keys.begin(), keys.end(), 0u);
|
||||
} else if (!indirect.selector_mask.IsEmpty()) {
|
||||
uint32_t mask = 0;
|
||||
uint32_t count = 0;
|
||||
if (material_value.dword_count != 2u || table_value.dword_count != 2u ||
|
||||
!clean.Evaluate(indirect.selector_mask, mask) ||
|
||||
!clean.Evaluate(indirect.key_count, count) || count == 0u || count > 32u) {
|
||||
return false;
|
||||
}
|
||||
if (count < 32u) mask &= (1u << count) - 1u;
|
||||
const auto material_base =
|
||||
(static_cast<uint64_t>(material_value.dwords[1]) << 32u) | material_value.dwords[0];
|
||||
keys.reserve(std::popcount(mask));
|
||||
while (mask != 0u) {
|
||||
const auto index = std::countr_zero(mask);
|
||||
const auto offset = static_cast<uint64_t>(indirect.selector_offset) +
|
||||
static_cast<uint64_t>(index) * indirect.selector_stride;
|
||||
if (offset > UINT32_MAX) return false;
|
||||
uint32_t key = 0;
|
||||
if (!ReadScalarTable(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
|
||||
runtime, {&key, 1})) return false;
|
||||
keys.push_back(key);
|
||||
mask &= mask - 1u;
|
||||
}
|
||||
std::ranges::sort(keys);
|
||||
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
||||
} else {
|
||||
ShaderBufferResource material;
|
||||
if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u ||
|
||||
material.Stride() != indirect.selector_stride) {
|
||||
return false;
|
||||
}
|
||||
// The first aligned offset includes the immediate added after shader U32 arithmetic.
|
||||
const auto step = std::max<uint64_t>(4u,
|
||||
std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u));
|
||||
const uint64_t first = indirect.selector_offset;
|
||||
const auto size = material.GetSize();
|
||||
const auto limit = std::min(first + (uint64_t {1} << 32u) - step, size >= 4u ? size - 4u : 0u);
|
||||
const auto probe_count = size >= 4u && first <= limit ? (limit - first) / step + 1u : 0u;
|
||||
if (probe_count > MaxIndirectImageProbes) {
|
||||
return false;
|
||||
}
|
||||
keys.reserve(static_cast<size_t>(probe_count) + 1u);
|
||||
keys.push_back(0u);
|
||||
for (uint64_t probe = 0, offset = first; probe < probe_count; ++probe, offset += step) {
|
||||
uint32_t key = 0;
|
||||
if (!ReadScalarTable(material.Base48(), size, offset, runtime, {&key, 1})) {
|
||||
return false;
|
||||
}
|
||||
keys.push_back(key);
|
||||
}
|
||||
std::ranges::sort(keys);
|
||||
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
||||
}
|
||||
std::ranges::sort(keys);
|
||||
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
||||
}
|
||||
|
||||
const auto children_begin = snapshot.images.size();
|
||||
const auto mapping_offset = snapshot.flattened_srt.size();
|
||||
const auto root_image = specialization.images[image_index];
|
||||
snapshot.flattened_srt.resize(mapping_offset + 1u + keys.size() * 2u);
|
||||
snapshot.flattened_srt[mapping_offset] = static_cast<uint32_t>(keys.size());
|
||||
for (uint32_t entry = 0; entry < keys.size(); ++entry) {
|
||||
const auto key = keys[entry];
|
||||
const auto key_count = sources.empty() ? keys.size() : sources.size();
|
||||
snapshot.flattened_srt.resize(mapping_offset + 1u + key_count * 2u);
|
||||
snapshot.flattened_srt[mapping_offset] = static_cast<uint32_t>(key_count);
|
||||
for (uint32_t entry = 0; entry < key_count; ++entry) {
|
||||
const auto key = sources.empty() ? keys[entry] : entry;
|
||||
DescriptorValue candidate;
|
||||
candidate.dword_count = 8u;
|
||||
const auto table_offset = (key << 5u) + indirect.table_offset;
|
||||
if (!ReadScalarTable(table_base, table_size, table_offset, runtime, candidate.dwords)) {
|
||||
if (sources.empty()) {
|
||||
const auto table_offset = (key << 5u) + indirect.table_offset;
|
||||
if (!ReadScalarTable(table_base, table_size, table_offset, runtime, candidate.dwords)) {
|
||||
return false;
|
||||
}
|
||||
} else if (!clean.EvaluateDescriptor(sources[entry], candidate)) {
|
||||
return false;
|
||||
}
|
||||
if (NullImageDescriptor(candidate) ||
|
||||
@@ -353,7 +367,7 @@ bool MaterializeIndirectImage(const ResourcePlan& program,
|
||||
auto& root = specialization.images[image_index];
|
||||
root.indirect_root = image_index;
|
||||
root.indirect_mapping_offset = static_cast<uint32_t>(mapping_offset);
|
||||
root.indirect_search_iterations = std::bit_width(keys.size());
|
||||
root.indirect_search_iterations = std::bit_width(key_count);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -409,36 +423,6 @@ bool BuildSamplerPlan(const ShaderInfo& base, const Images& images, SamplerPlan&
|
||||
|
||||
static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSnapshot& snapshot,
|
||||
ResourceSpecialization& specialization) {
|
||||
specialization.buffers.clear();
|
||||
specialization.buffers.reserve(program.info.buffers.size());
|
||||
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
|
||||
auto& descriptor_value = snapshot.buffers[i];
|
||||
ShaderBufferResource descriptor;
|
||||
if (!DecodeBufferDescriptor(descriptor_value, descriptor)) {
|
||||
return SpecializationFail(fmt::format("buffer descriptor {} has invalid width", i));
|
||||
}
|
||||
if (descriptor.Type() != 0) {
|
||||
descriptor_value.dwords.fill(0);
|
||||
descriptor = {};
|
||||
}
|
||||
auto packed_stride = descriptor.PackedStride();
|
||||
const auto stride = packed_stride & 0x3fffu;
|
||||
const bool swizzle = stride != 0u && ((packed_stride >> 14u) & 1u) != 0u;
|
||||
if (stride == 0u) {
|
||||
packed_stride &= ~((1u << 14u) | (3u << 16u));
|
||||
} else if (!swizzle) {
|
||||
packed_stride &= ~(3u << 16u);
|
||||
}
|
||||
specialization.buffers.push_back({
|
||||
.packed_stride = packed_stride,
|
||||
.descriptor_format = program.info.buffers[i].formatted
|
||||
? descriptor.Format()
|
||||
: Prospero::BufferFormat::kInvalid,
|
||||
.descriptor_swizzle =
|
||||
program.info.buffers[i].formatted ? descriptor.DstSelXYZW() : DstSel(4, 5, 6, 7),
|
||||
.zero_stride_oob = descriptor.OutOfBounds() == 0u && stride == 0u,
|
||||
});
|
||||
}
|
||||
for (uint32_t i = 0; i < specialization.images.size(); i++) {
|
||||
const auto& descriptor = snapshot.images[i];
|
||||
auto& image = specialization.images[i];
|
||||
@@ -936,20 +920,27 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
|
||||
}
|
||||
plan.clean_flat_slots.resize(plan.srt_reads.size());
|
||||
bool masked_image = false;
|
||||
bool capture_image_reads = false;
|
||||
for (const auto& image: plan.info.images) {
|
||||
const auto* source = Source(plan, image.source);
|
||||
if (source == nullptr || !source->indirect_image.has_value()) {
|
||||
continue;
|
||||
}
|
||||
plan.requires_specialization_memory = true;
|
||||
masked_image |= !source->indirect_image->selector_mask.IsEmpty();
|
||||
capture_image_reads |= !source->indirect_image->selector_mask.IsEmpty() ||
|
||||
!source->indirect_image->sources.empty();
|
||||
MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->material_source),
|
||||
plan.clean_flat_slots, source->indirect_image->selector_mask);
|
||||
MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->table_source),
|
||||
plan.clean_flat_slots);
|
||||
if (source->indirect_image->sources.empty()) {
|
||||
MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->table_source),
|
||||
plan.clean_flat_slots);
|
||||
} else {
|
||||
for (const auto candidate: source->indirect_image->sources) {
|
||||
MarkCleanFlatSlots(plan, Source(plan, candidate), plan.clean_flat_slots);
|
||||
}
|
||||
}
|
||||
}
|
||||
plan.capture_specialization_reads = masked_image || !plan.control_flow.empty();
|
||||
plan.capture_specialization_reads = capture_image_reads || !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);
|
||||
@@ -957,7 +948,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
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;
|
||||
if (capture_image_reads) plan.resource_tracking_complete &= !program.has_address_writes;
|
||||
return plan;
|
||||
}
|
||||
|
||||
@@ -1008,11 +999,39 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
return true;
|
||||
};
|
||||
snapshot.buffers.resize(program.info.buffers.size());
|
||||
specialization.buffers.clear();
|
||||
specialization.buffers.reserve(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],
|
||||
program.info.buffers[i].written)) {
|
||||
return false;
|
||||
}
|
||||
auto& descriptor_value = snapshot.buffers[i];
|
||||
ShaderBufferResource descriptor;
|
||||
if (!DecodeBufferDescriptor(descriptor_value, descriptor)) {
|
||||
return SpecializationFail(fmt::format("buffer descriptor {} has invalid width", i));
|
||||
}
|
||||
if (descriptor.Type() != 0) {
|
||||
descriptor_value.dwords.fill(0);
|
||||
descriptor = {};
|
||||
}
|
||||
auto packed_stride = descriptor.PackedStride();
|
||||
const auto stride = packed_stride & 0x3fffu;
|
||||
const bool swizzle = stride != 0u && ((packed_stride >> 14u) & 1u) != 0u;
|
||||
if (stride == 0u) {
|
||||
packed_stride &= ~((1u << 14u) | (3u << 16u));
|
||||
} else if (!swizzle) {
|
||||
packed_stride &= ~(3u << 16u);
|
||||
}
|
||||
specialization.buffers.push_back({
|
||||
.packed_stride = packed_stride,
|
||||
.descriptor_format = program.info.buffers[i].formatted
|
||||
? descriptor.Format()
|
||||
: Prospero::BufferFormat::kInvalid,
|
||||
.descriptor_swizzle =
|
||||
program.info.buffers[i].formatted ? descriptor.DstSelXYZW() : DstSel(4, 5, 6, 7),
|
||||
.zero_stride_oob = descriptor.OutOfBounds() == 0u && stride == 0u,
|
||||
});
|
||||
}
|
||||
snapshot.images.resize(program.info.images.size());
|
||||
specialization.images.resize(program.info.images.size());
|
||||
@@ -1039,13 +1058,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
continue;
|
||||
}
|
||||
const auto& indirect = *source->indirect_image;
|
||||
DescriptorValue material;
|
||||
DescriptorValue table;
|
||||
if ((indirect.material_source != UINT32_MAX &&
|
||||
!clean.EvaluateDescriptor(indirect.material_source, material)) ||
|
||||
!clean.EvaluateDescriptor(indirect.table_source, table) ||
|
||||
!MaterializeIndirectImage(program, indirect, material, table, i, observed, clean, snapshot,
|
||||
specialization)) {
|
||||
if (!MaterializeIndirectImage(program, indirect, i, observed, clean, snapshot, specialization)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -1073,19 +1086,20 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
EXIT_IF(program.info.buffers.size() != specialization.buffers.size() ||
|
||||
program.info.images.size() > specialization.images.size());
|
||||
|
||||
auto buffers = program.info.buffers;
|
||||
auto& buffers = program.info.buffers;
|
||||
for (size_t index = 0; index < buffers.size(); index++) {
|
||||
buffers[index].packed_stride = specialization.buffers[index].packed_stride;
|
||||
buffers[index].descriptor_format = specialization.buffers[index].descriptor_format;
|
||||
buffers[index].descriptor_swizzle = specialization.buffers[index].descriptor_swizzle;
|
||||
}
|
||||
auto images = program.info.images;
|
||||
auto& images = program.info.images;
|
||||
const auto original_image_count = images.size();
|
||||
images.reserve(specialization.images.size());
|
||||
for (uint32_t index = 0; index < specialization.images.size(); index++) {
|
||||
const auto& source = specialization.images[index];
|
||||
if (index >= images.size()) {
|
||||
EXIT_IF(source.indirect_root >= program.info.images.size());
|
||||
images.push_back(program.info.images[source.indirect_root]);
|
||||
EXIT_IF(source.indirect_root >= original_image_count);
|
||||
images.push_back(images[source.indirect_root]);
|
||||
}
|
||||
auto& image = images[index];
|
||||
image.numeric_class = source.numeric_class;
|
||||
@@ -1109,13 +1123,14 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
|
||||
SamplerPlan sampler_plan;
|
||||
EXIT_IF(!BuildSamplerPlan(program.info, images, sampler_plan));
|
||||
auto samplers = program.info.samplers;
|
||||
auto sampled_pairs = program.info.sampled_pairs;
|
||||
auto& samplers = program.info.samplers;
|
||||
auto& sampled_pairs = program.info.sampled_pairs;
|
||||
const auto original_sampler_count = samplers.size();
|
||||
samplers.reserve(sampler_plan.sampler_count);
|
||||
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
|
||||
const auto& binding = sampler_plan.bindings[index];
|
||||
if (index >= program.info.samplers.size()) {
|
||||
samplers.push_back(program.info.samplers[binding.source]);
|
||||
if (index >= original_sampler_count) {
|
||||
samplers.push_back(samplers[binding.source]);
|
||||
}
|
||||
samplers[index].force_point_filtering = binding.type == SamplerClass::PointInteger;
|
||||
samplers[index].integer_border = binding.type != SamplerClass::Float;
|
||||
@@ -1127,11 +1142,15 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
samplers[pair.sampler].depth_compare |= images[pair.image].depth_compare;
|
||||
}
|
||||
|
||||
auto memory_info = program.memory_info;
|
||||
auto& memory_info = program.memory_info;
|
||||
const ImageRemap image_remap(specialization);
|
||||
for (auto* block: program.blocks) {
|
||||
for (auto it = block->begin(); it != block->end(); ++it) {
|
||||
auto& inst = *it;
|
||||
if (inst.GetOpcode() == ValueOpcode::GetImageResource) {
|
||||
inst.SetFlags(image_remap[inst.Flags<uint32_t>()]);
|
||||
continue;
|
||||
}
|
||||
if (BufferAccessOf(inst.GetOpcode()) == BufferAccess::Read) {
|
||||
const auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
|
||||
if (memory.kind == ResourceKind::Buffer &&
|
||||
@@ -1202,7 +1221,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
continue;
|
||||
}
|
||||
if (image_opcode.needs_sampler &&
|
||||
memory.sampler < program.info.samplers.size()) {
|
||||
memory.sampler < original_sampler_count) {
|
||||
const auto type = static_cast<uint32_t>(ClassifySampler(image));
|
||||
memory.sampler = sampler_plan.mapping[memory.sampler][type];
|
||||
EXIT_IF(memory.sampler == UINT32_MAX);
|
||||
@@ -1211,13 +1230,6 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
inst.GetOpcode() != ValueOpcode::ImageSampleRaw);
|
||||
}
|
||||
}
|
||||
for (auto* block: program.blocks) {
|
||||
for (auto& inst: *block) {
|
||||
if (inst.GetOpcode() == ValueOpcode::GetImageResource) {
|
||||
inst.SetFlags(image_remap[inst.Flags<uint32_t>()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto& memory: memory_info) {
|
||||
if (memory.kind == ResourceKind::Image && !memory.planning_only) {
|
||||
memory.resource = image_remap[memory.resource];
|
||||
@@ -1240,11 +1252,6 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
}
|
||||
}
|
||||
image_remap.Apply(images);
|
||||
program.info.buffers = std::move(buffers);
|
||||
program.info.images = std::move(images);
|
||||
program.info.samplers = std::move(samplers);
|
||||
program.info.sampled_pairs = std::move(sampled_pairs);
|
||||
program.memory_info = std::move(memory_info);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
||||
@@ -321,8 +321,9 @@ public:
|
||||
for (uint32_t dword = 5u; dword < plan.roots.size(); dword++) {
|
||||
plan.handle->SetArg(dword, plan.key);
|
||||
}
|
||||
for (const auto index: plan.memory) {
|
||||
m_program.memory_info[index].planning_only = true;
|
||||
if (plan.reads[0] != nullptr) {
|
||||
for (const auto index: plan.memory)
|
||||
m_program.memory_info[index].planning_only = true;
|
||||
}
|
||||
}
|
||||
m_program.descriptor_sources = std::move(m_sources);
|
||||
@@ -762,7 +763,7 @@ private:
|
||||
const auto& b = *descriptor.indirect_image;
|
||||
if (a.material_source != b.material_source || a.table_source != b.table_source ||
|
||||
a.selector_stride != b.selector_stride || a.selector_offset != b.selector_offset ||
|
||||
a.table_offset != b.table_offset ||
|
||||
a.table_offset != b.table_offset || a.sources != b.sources ||
|
||||
!EquivalentValue(m_program, a.key_count, b.key_count) ||
|
||||
a.selector_mask.IsEmpty() != b.selector_mask.IsEmpty() ||
|
||||
(!a.selector_mask.IsEmpty() &&
|
||||
@@ -1474,6 +1475,152 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t SelectorMaximum(Value value) const {
|
||||
value = value.Resolve();
|
||||
if (value.IsImmediate() && value.GetType() == Type::U32) return value.U32();
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) return UINT32_MAX;
|
||||
if (inst->GetOpcode() == ValueOpcode::FindILsb32)
|
||||
return NonzeroOnEntry(inst->Arg(0), inst->Parent()) ? 31u : UINT32_MAX;
|
||||
if (inst->GetOpcode() == ValueOpcode::UMin32)
|
||||
return std::min(SelectorMaximum(inst->Arg(0)), SelectorMaximum(inst->Arg(1)));
|
||||
uint32_t shift;
|
||||
if (inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32 &&
|
||||
ImmediateU32(inst->Arg(1), shift) && shift < 32u) {
|
||||
const auto maximum = uint64_t {SelectorMaximum(inst->Arg(0))} << shift;
|
||||
if (maximum <= UINT32_MAX) return static_cast<uint32_t>(maximum);
|
||||
}
|
||||
return UINT32_MAX;
|
||||
}
|
||||
|
||||
bool ImpossibleSwitchEdge(const Block* from, const Block* to) const {
|
||||
const auto from_it = std::ranges::find(m_program.blocks, from);
|
||||
const auto to_it = std::ranges::find(m_program.blocks, to);
|
||||
if (from_it == m_program.blocks.end() || to_it == m_program.blocks.end() ||
|
||||
m_program.block_info.size() != m_program.blocks.size()) return false;
|
||||
const auto& info = m_program.block_info[from_it - m_program.blocks.begin()];
|
||||
const auto& term = info.terminator;
|
||||
if (term.kind != CFG::TerminatorKind::IndirectBranch ||
|
||||
term.indirect_selector_code == UINT32_MAX ||
|
||||
term.indirect_selector_values.empty() ||
|
||||
term.indirect_selector_values.size() != term.indirect_selector_targets.size()) return false;
|
||||
const auto target = m_program.block_info[to_it - m_program.blocks.begin()].id;
|
||||
const auto maximum = SelectorMaximum(info.indirect_target);
|
||||
bool found = false;
|
||||
for (size_t i = 0; i < term.indirect_selector_values.size(); ++i) {
|
||||
if (term.indirect_selector_targets[i] != target) continue;
|
||||
if (term.indirect_selector_values[i] <= maximum) return false;
|
||||
found = true;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
bool TryMakeFiniteImage(Inst& handle, IndirectImagePlan& plan) {
|
||||
if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u)
|
||||
return false;
|
||||
if (std::ranges::any_of(handle.Uses(), [](const Use& use) {
|
||||
const auto op = use.user->GetOpcode();
|
||||
return op != ValueOpcode::ImageSampleRaw && op != ValueOpcode::ImageGatherRaw;
|
||||
})) return false;
|
||||
bool has_phi = false;
|
||||
for (size_t word = 0; word < handle.NumArgs(); ++word) has_phi |= handle.Arg(word).Resolve().IsPhi();
|
||||
if (!has_phi) return false;
|
||||
// Normalize the eight synchronized descriptor words to one GPU ordinal.
|
||||
// Build the complete graph before changing IR so an unsupported leaf is transactional.
|
||||
struct Choice {
|
||||
DescriptorSource descriptor;
|
||||
const Inst* branch = nullptr;
|
||||
std::vector<uint32_t> children;
|
||||
Value key;
|
||||
};
|
||||
std::vector<Choice> choices;
|
||||
const auto visit = [&](const auto& self, DescriptorSource descriptor) -> uint32_t {
|
||||
for (auto& word: descriptor.dwords) word = word.Resolve();
|
||||
for (uint32_t i = 0; i < choices.size(); ++i)
|
||||
if (choices[i].descriptor.dwords == descriptor.dwords) return i;
|
||||
const auto index = static_cast<uint32_t>(choices.size());
|
||||
choices.push_back({descriptor});
|
||||
uint32_t bad = 0;
|
||||
if (ValidateSource(descriptor, bad)) return index;
|
||||
const auto* branch = descriptor.dwords[bad].TryInstruction();
|
||||
if (branch == nullptr || branch->Parent() == nullptr ||
|
||||
branch->GetOpcode() != ValueOpcode::Phi ||
|
||||
branch->NumPhiBlocks() != branch->NumArgs()) return UINT32_MAX;
|
||||
choices[index].branch = branch;
|
||||
const auto count = branch->NumArgs();
|
||||
for (uint32_t arm = 0; arm < count; ++arm) {
|
||||
// A nonzero bit scan cannot choose its zero-input switch sentinel.
|
||||
// Preserve the Phi edge; its ordinal is never observed at runtime.
|
||||
if (ImpossibleSwitchEdge(branch->PhiBlock(arm), branch->Parent())) {
|
||||
choices[index].children.push_back(UINT32_MAX);
|
||||
continue;
|
||||
}
|
||||
auto child = descriptor;
|
||||
for (uint32_t word = 0; word < child.dword_count; ++word) {
|
||||
const auto* selected = descriptor.dwords[word].TryInstruction();
|
||||
if (selected == nullptr || selected->GetOpcode() != branch->GetOpcode() ||
|
||||
selected->Parent() != branch->Parent()) {
|
||||
if (!ValidateRuntimeValue(m_program, descriptor.dwords[word])) return UINT32_MAX;
|
||||
continue;
|
||||
}
|
||||
if (selected->NumArgs() != count || selected->NumPhiBlocks() != count)
|
||||
return UINT32_MAX;
|
||||
uint32_t incoming = 0;
|
||||
while (incoming < count && selected->PhiBlock(incoming) != branch->PhiBlock(arm))
|
||||
++incoming;
|
||||
if (incoming == count) return UINT32_MAX;
|
||||
child.dwords[word] = selected->Arg(incoming);
|
||||
}
|
||||
const auto next = self(self, child);
|
||||
if (next == UINT32_MAX) return UINT32_MAX;
|
||||
choices[index].children.push_back(next);
|
||||
}
|
||||
return index;
|
||||
};
|
||||
DescriptorSource root;
|
||||
root.dword_count = 8u;
|
||||
for (uint32_t word = 0; word < root.dword_count; ++word) root.dwords[word] = handle.Arg(word);
|
||||
if (visit(visit, root) == UINT32_MAX || choices.front().branch == nullptr) return false;
|
||||
DescriptorSource image_source;
|
||||
image_source.dword_count = 8u;
|
||||
image_source.dwords.fill(Value(0u));
|
||||
image_source.indirect_image.emplace(DescriptorSource::IndirectImage{});
|
||||
auto& sources = image_source.indirect_image->sources;
|
||||
for (auto& choice: choices) {
|
||||
if (choice.branch != nullptr) continue;
|
||||
const auto source = InternSource(choice.descriptor);
|
||||
auto found = std::ranges::find(sources, source);
|
||||
choice.key = Value(static_cast<uint32_t>(found - sources.begin()));
|
||||
if (found == sources.end()) sources.push_back(source);
|
||||
}
|
||||
if (sources.empty()) return false;
|
||||
for (auto& choice: choices) {
|
||||
if (choice.branch == nullptr) continue;
|
||||
auto* block = choice.branch->Parent();
|
||||
const auto where = std::ranges::find_if(block->Instructions(), [&](const Inst& inst) {
|
||||
return &inst == choice.branch;
|
||||
});
|
||||
auto& key = *block->PrependNewInst(where, choice.branch->GetOpcode());
|
||||
key.SetFlags(Type::U32);
|
||||
choice.key = Value(&key);
|
||||
}
|
||||
for (const auto& choice: choices) {
|
||||
if (choice.branch == nullptr) continue;
|
||||
auto* key = choice.key.Instruction();
|
||||
for (uint32_t arm = 0; arm < choice.children.size(); ++arm) {
|
||||
const auto child = choice.children[arm];
|
||||
const auto value = child == UINT32_MAX ? Value(0u) : choices[child].key;
|
||||
key->AddPhiOperand(choice.branch->PhiBlock(arm), value);
|
||||
}
|
||||
}
|
||||
plan.handle = &handle;
|
||||
plan.source = InternSource(image_source);
|
||||
plan.key = choices.front().key;
|
||||
plan.roots = image_source.dwords;
|
||||
plan.reads.fill(nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
const IndirectImagePlan* FindIndirectImage(const Inst& handle) const {
|
||||
const auto found =
|
||||
std::find_if(m_indirect_images.begin(), m_indirect_images.end(),
|
||||
@@ -1486,7 +1633,8 @@ private:
|
||||
bool IsIndirectPlanningMemory(uint32_t index) const {
|
||||
return std::any_of(m_indirect_images.begin(), m_indirect_images.end(),
|
||||
[&](const IndirectImagePlan& plan) {
|
||||
return std::ranges::find(plan.memory, index) != plan.memory.end();
|
||||
return plan.reads[0] != nullptr &&
|
||||
std::ranges::find(plan.memory, index) != plan.memory.end();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1502,7 +1650,7 @@ private:
|
||||
continue;
|
||||
}
|
||||
IndirectImagePlan plan;
|
||||
if (TryMakeIndirectImage(*handle, plan)) {
|
||||
if (TryMakeIndirectImage(*handle, plan) || TryMakeFiniteImage(*handle, plan)) {
|
||||
m_indirect_images.push_back(std::move(plan));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,7 +412,7 @@ void CollectShaderInfo(Program& program, ShaderStageInputInfo input_info) {
|
||||
ValidateOptions(program, input_info);
|
||||
ValidateValueReferences(program, input_info);
|
||||
|
||||
auto next = program.info;
|
||||
auto& next = program.info;
|
||||
next.inputs.clear();
|
||||
next.outputs.clear();
|
||||
next.has_bitwise_xor =
|
||||
@@ -433,7 +433,6 @@ void CollectShaderInfo(Program& program, ShaderStageInputInfo input_info) {
|
||||
}
|
||||
CollectBuiltinInputs(program, next);
|
||||
CollectOutputs(program, input_info, next);
|
||||
program.info = std::move(next);
|
||||
program.shader_info_complete = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,9 @@ constexpr PadControl PAD_CONTROLS[] = {
|
||||
{"Options", "Options", "Return"},
|
||||
{"TouchPad", "Touch pad left (SELECT)", "Backspace"},
|
||||
{"TouchPadRight", "Touch pad right (START)", "Tab"},
|
||||
{"SpeakerVolume", "Speaker volume (cycle)", "1"},
|
||||
{"VibrationIntensity", "Vibration intensity (cycle)", "2"},
|
||||
{"TriggerEffectIntensity", "Trigger effect intensity (cycle)", "3"},
|
||||
};
|
||||
|
||||
QString KeypadName(int key) {
|
||||
|
||||
+111
-70
@@ -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;
|
||||
};
|
||||
|
||||
@@ -140,8 +159,8 @@ private:
|
||||
static void OpenSdlDevice(PortIn* port, Format format);
|
||||
static void CloseSdlDevice(PortIn* port);
|
||||
static const void* PrepareOutputBuffer(const PortOut& port, const void* data,
|
||||
std::vector<uint8_t>* buffer);
|
||||
static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking);
|
||||
std::vector<uint8_t>* buffer, float gain);
|
||||
static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain);
|
||||
};
|
||||
|
||||
static Audio* g_audio = nullptr;
|
||||
@@ -283,7 +302,7 @@ void Audio::CloseSdlDevice(PortOut* port) {
|
||||
}
|
||||
|
||||
const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
|
||||
std::vector<uint8_t>* buffer) {
|
||||
std::vector<uint8_t>* buffer, float gain) {
|
||||
EXIT_IF(data == nullptr);
|
||||
EXIT_IF(buffer == nullptr);
|
||||
|
||||
@@ -293,7 +312,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
|
||||
const auto bytes_per_sample = BytesPerSample(port.format);
|
||||
const bool reorder = channels >= 8 && !FormatIsStd(port.format);
|
||||
|
||||
bool volume_changed = false;
|
||||
bool volume_changed = gain != 1.0f;
|
||||
for (uint32_t ch = 0; ch < channels; ch++) {
|
||||
if (port.volume[ch] != 32768) {
|
||||
volume_changed = true;
|
||||
@@ -319,7 +338,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
|
||||
const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch;
|
||||
dst[frame * output_channels + ch] =
|
||||
src[frame * channels + src_ch] *
|
||||
(static_cast<float>(port.volume[src_ch]) / 32768.0f);
|
||||
(static_cast<float>(port.volume[src_ch]) / 32768.0f) * gain;
|
||||
}
|
||||
if (channels == 12) {
|
||||
// Add the four top channels to their front/back channels, turning 12 into 8.
|
||||
@@ -328,7 +347,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
|
||||
for (uint32_t ch = 0; ch < 4; ch++) {
|
||||
dst[frame * output_channels + HEIGHT_DST[ch]] +=
|
||||
src[frame * channels + 8 + ch] *
|
||||
(static_cast<float>(port.volume[8 + ch]) / 32768.0f);
|
||||
(static_cast<float>(port.volume[8 + ch]) / 32768.0f) * gain;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,6 +360,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
|
||||
const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch;
|
||||
int64_t sample =
|
||||
static_cast<int64_t>(src[frame * channels + src_ch]) * port.volume[src_ch] / 32768;
|
||||
sample = static_cast<int64_t>(sample * static_cast<double>(gain));
|
||||
if (sample > std::numeric_limits<int16_t>::max()) {
|
||||
sample = std::numeric_limits<int16_t>::max();
|
||||
} else if (sample < std::numeric_limits<int16_t>::min()) {
|
||||
@@ -354,7 +374,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
|
||||
return buffer->data();
|
||||
}
|
||||
|
||||
bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
|
||||
bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain) {
|
||||
EXIT_IF(port == nullptr);
|
||||
|
||||
if (port->stream == nullptr || data == nullptr) {
|
||||
@@ -362,7 +382,7 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
|
||||
}
|
||||
|
||||
std::vector<uint8_t> prepared_buffer;
|
||||
const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer);
|
||||
const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer, gain);
|
||||
const auto output_channels = OutputChannels(*port);
|
||||
const auto prepared_size =
|
||||
BytesPerSample(port->format) * output_channels * port->samples_num;
|
||||
@@ -376,10 +396,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 +406,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 +429,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 +545,75 @@ 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()];
|
||||
const float gain =
|
||||
port.type == AUDIO_OUT_PORT_TYPE_PADSPK
|
||||
? Controller::GetSettingScale(Controller::Setting::SpeakerVolume)
|
||||
: port.type == AUDIO_OUT_PORT_TYPE_VIBRATION
|
||||
? Controller::GetSettingScale(Controller::Setting::VibrationIntensity)
|
||||
: 1.0f;
|
||||
|
||||
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);
|
||||
static_cast<uint32_t>(port.channels_num), FormatIsFloat(port.format), port.volume,
|
||||
gain);
|
||||
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, gain)) {
|
||||
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 +861,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 +973,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];
|
||||
|
||||
|
||||
+100
-17
@@ -15,6 +15,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
@@ -117,6 +118,8 @@ public:
|
||||
int GetActiveControllerId();
|
||||
void SetLightBar(uint8_t r, uint8_t g, uint8_t b);
|
||||
bool SetTriggerEffect(const PadTriggerEffectParam& param);
|
||||
void CycleSetting(Setting setting);
|
||||
float GetSettingScale(Setting setting) const;
|
||||
void ReadState(ControllerState* state, bool* flag, int* count);
|
||||
int ReadStates(ControllerState* states, int states_num, bool* flag, int* count);
|
||||
|
||||
@@ -125,6 +128,9 @@ private:
|
||||
|
||||
void CheckActive();
|
||||
void AddState();
|
||||
// Apply cached output without changing its game-requested lifetime; caller holds m_mutex.
|
||||
void ApplyVibration();
|
||||
bool SendTriggerEffect(const PadTriggerEffectParam& param);
|
||||
|
||||
Common::Mutex m_mutex;
|
||||
std::vector<int> m_connected_ids;
|
||||
@@ -142,6 +148,12 @@ private:
|
||||
uint32_t m_states_num = 0;
|
||||
uint32_t m_first_state = 0;
|
||||
uint8_t m_next_touch_id = 1;
|
||||
// The game's last requests, re-sent when their intensity setting changes.
|
||||
std::array<uint8_t, 2> m_vibration {};
|
||||
uint64_t m_vibration_until = 0;
|
||||
PadTriggerEffectParam m_trigger_effect {};
|
||||
// Setting changes share the output lock; audio only needs an atomic scale snapshot.
|
||||
std::array<std::atomic<uint32_t>, 3> m_setting_steps {};
|
||||
};
|
||||
|
||||
static GameController* g_controller = nullptr;
|
||||
@@ -183,17 +195,56 @@ static void pad_fill_data(PadData* data, const ControllerState& state, bool conn
|
||||
data->device_unique_data_len = 0;
|
||||
}
|
||||
|
||||
static bool trigger_effect_zones(uint8_t* effect, const uint8_t* strengths, uint8_t type,
|
||||
uint8_t frequency = 0) {
|
||||
// In cycle order, starting at the PS5 default.
|
||||
constexpr std::array SPEAKER_VOLUME = {1.0f, 0.0f, 0.02f, 0.12f, 0.42f};
|
||||
constexpr std::array INTENSITY = {1.0f, 0.0f, 0.33f, 0.66f};
|
||||
|
||||
void CycleSetting(Setting setting) {
|
||||
if (g_controller != nullptr) {
|
||||
g_controller->CycleSetting(setting);
|
||||
}
|
||||
}
|
||||
|
||||
float GetSettingScale(Setting setting) {
|
||||
return g_controller != nullptr ? g_controller->GetSettingScale(setting) : 1.0f;
|
||||
}
|
||||
|
||||
void GameController::CycleSetting(Setting setting) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
auto& step = m_setting_steps[static_cast<size_t>(setting)];
|
||||
const auto count = setting == Setting::SpeakerVolume ? SPEAKER_VOLUME.size() : INTENSITY.size();
|
||||
step.store((step.load(std::memory_order_relaxed) + 1) % count, std::memory_order_relaxed);
|
||||
if (setting == Setting::VibrationIntensity) {
|
||||
ApplyVibration();
|
||||
} else if (setting == Setting::TriggerEffectIntensity) {
|
||||
(void)SendTriggerEffect(m_trigger_effect);
|
||||
}
|
||||
}
|
||||
|
||||
float GameController::GetSettingScale(Setting setting) const {
|
||||
const auto step = m_setting_steps[static_cast<size_t>(setting)].load(std::memory_order_relaxed);
|
||||
return setting == Setting::SpeakerVolume ? SPEAKER_VOLUME[step] : INTENSITY[step];
|
||||
}
|
||||
|
||||
static uint8_t Scale(uint8_t value, float scale) {
|
||||
if (value == 0 || scale == 0.0f) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<uint8_t>(std::max(1L, std::lround(value * scale)));
|
||||
}
|
||||
|
||||
static bool trigger_effect_zones(uint8_t* effect, const uint8_t* strengths, float scale,
|
||||
uint8_t type, uint8_t frequency = 0) {
|
||||
uint16_t active = 0;
|
||||
uint32_t packed = 0;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (strengths[i] > 8) {
|
||||
return false;
|
||||
}
|
||||
if (strengths[i] != 0) {
|
||||
const auto strength = Scale(strengths[i], scale);
|
||||
if (strength != 0) {
|
||||
active |= static_cast<uint16_t>(1u << i);
|
||||
packed |= static_cast<uint32_t>(strengths[i] - 1u) << (3 * i);
|
||||
packed |= static_cast<uint32_t>(strength - 1u) << (3 * i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +259,8 @@ static bool trigger_effect_zones(uint8_t* effect, const uint8_t* strengths, uint
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command, uint8_t* effect) {
|
||||
static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command, uint8_t* effect,
|
||||
float scale) {
|
||||
std::memset(effect, 0, 11);
|
||||
effect[0] = 0x05;
|
||||
|
||||
@@ -220,7 +272,7 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command,
|
||||
return false;
|
||||
}
|
||||
std::fill(strengths + command.data[0], strengths + 10, command.data[1]);
|
||||
return trigger_effect_zones(effect, strengths, 0x21);
|
||||
return trigger_effect_zones(effect, strengths, scale, 0x21);
|
||||
case 2: {
|
||||
const auto start = command.data[0];
|
||||
const auto end = command.data[1];
|
||||
@@ -228,14 +280,15 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command,
|
||||
if (start < 2 || start > 7 || end <= start || end > 8 || strength > 8) {
|
||||
return false;
|
||||
}
|
||||
if (strength == 0) {
|
||||
const auto scaled = Scale(strength, scale);
|
||||
if (scaled == 0) {
|
||||
return true;
|
||||
}
|
||||
const uint16_t zones = static_cast<uint16_t>((1u << start) | (1u << end));
|
||||
effect[0] = 0x25;
|
||||
effect[1] = static_cast<uint8_t>(zones);
|
||||
effect[2] = static_cast<uint8_t>(zones >> 8u);
|
||||
effect[3] = strength - 1u;
|
||||
effect[3] = scaled - 1u;
|
||||
return true;
|
||||
}
|
||||
case 3:
|
||||
@@ -243,8 +296,8 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command,
|
||||
return false;
|
||||
}
|
||||
std::fill(strengths + command.data[0], strengths + 10, command.data[1]);
|
||||
return trigger_effect_zones(effect, strengths, 0x26, command.data[2]);
|
||||
case 4: return trigger_effect_zones(effect, command.data, 0x21);
|
||||
return trigger_effect_zones(effect, strengths, scale, 0x26, command.data[2]);
|
||||
case 4: return trigger_effect_zones(effect, command.data, scale, 0x21);
|
||||
case 5: {
|
||||
const int start = command.data[0];
|
||||
const int end = command.data[1];
|
||||
@@ -262,9 +315,9 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command,
|
||||
: start_strength +
|
||||
(delta + (delta < 0 ? -length / 2 : length / 2)) / length);
|
||||
}
|
||||
return trigger_effect_zones(effect, strengths, 0x21);
|
||||
return trigger_effect_zones(effect, strengths, scale, 0x21);
|
||||
}
|
||||
case 6: return trigger_effect_zones(effect, command.data + 1, 0x26, command.data[0]);
|
||||
case 6: return trigger_effect_zones(effect, command.data + 1, scale, 0x26, command.data[0]);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
@@ -355,6 +408,9 @@ void GameController::CheckActive() {
|
||||
m_states_num = 0;
|
||||
m_first_state = 0;
|
||||
m_next_touch_id = 1;
|
||||
m_vibration = {};
|
||||
m_vibration_until = 0;
|
||||
m_trigger_effect = {};
|
||||
}
|
||||
|
||||
void GameController::AddState() {
|
||||
@@ -600,11 +656,24 @@ void GameController::ReleaseHostPads() {
|
||||
}
|
||||
|
||||
m_connected_ids.clear();
|
||||
CheckActive();
|
||||
}
|
||||
|
||||
void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
if (DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor)) {
|
||||
m_vibration = {large_motor, small_motor};
|
||||
m_vibration_until = SDL_GetTicks() + RUMBLE_DURATION_MS;
|
||||
ApplyVibration();
|
||||
}
|
||||
|
||||
void GameController::ApplyVibration() {
|
||||
const auto now = SDL_GetTicks();
|
||||
const auto duration =
|
||||
m_vibration_until > now ? static_cast<uint32_t>(m_vibration_until - now) : 0;
|
||||
const auto scale = duration != 0 ? GetSettingScale(Setting::VibrationIntensity) : 0.0f;
|
||||
const auto large_motor = Scale(m_vibration[0], scale);
|
||||
const auto small_motor = Scale(m_vibration[1], scale);
|
||||
if (DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor, duration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -619,7 +688,7 @@ void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
|
||||
|
||||
const auto large = static_cast<uint16_t>(large_motor * 0x101U);
|
||||
const auto small = static_cast<uint16_t>(small_motor * 0x101U);
|
||||
if (!SDL_RumbleGamepad(pad, large, small, RUMBLE_DURATION_MS)) {
|
||||
if (!SDL_RumbleGamepad(pad, large, small, duration)) {
|
||||
LOGF("\t rumble failed: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
@@ -643,20 +712,35 @@ void GameController::SetLightBar(uint8_t r, uint8_t g, uint8_t b) {
|
||||
}
|
||||
|
||||
bool GameController::SetTriggerEffect(const PadTriggerEffectParam& param) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
if (!SendTriggerEffect(param)) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if ((param.trigger_mask & (1u << i)) != 0) {
|
||||
m_trigger_effect.trigger_mask |= static_cast<uint8_t>(1u << i);
|
||||
m_trigger_effect.command[i] = param.command[i];
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GameController::SendTriggerEffect(const PadTriggerEffectParam& param) {
|
||||
if ((param.trigger_mask & ~0x03u) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DualSenseEffects effect {};
|
||||
const auto scale = GetSettingScale(Setting::TriggerEffectIntensity);
|
||||
if ((param.trigger_mask & 0x01u) != 0) {
|
||||
effect.enable_bits |= 0x08;
|
||||
if (!trigger_effect_to_dualsense(param.command[0], effect.left_trigger)) {
|
||||
if (!trigger_effect_to_dualsense(param.command[0], effect.left_trigger, scale)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if ((param.trigger_mask & 0x02u) != 0) {
|
||||
effect.enable_bits |= 0x04;
|
||||
if (!trigger_effect_to_dualsense(param.command[1], effect.right_trigger)) {
|
||||
if (!trigger_effect_to_dualsense(param.command[1], effect.right_trigger, scale)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -664,7 +748,6 @@ bool GameController::SetTriggerEffect(const PadTriggerEffectParam& param) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Common::LockGuard lock(m_mutex);
|
||||
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(m_active_id));
|
||||
if (pad != nullptr && SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5) {
|
||||
(void)SDL_SendGamepadEffect(pad, &effect, sizeof(effect));
|
||||
|
||||
@@ -75,6 +75,11 @@ void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us);
|
||||
void ResetInputState();
|
||||
int GetActiveControllerId();
|
||||
|
||||
enum class Setting { SpeakerVolume, VibrationIntensity, TriggerEffectIntensity };
|
||||
|
||||
void CycleSetting(Setting setting);
|
||||
float GetSettingScale(Setting setting);
|
||||
|
||||
int KYTY_SYSV_ABI PadInit();
|
||||
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param);
|
||||
int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index);
|
||||
|
||||
@@ -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
|
||||
+144
-62
@@ -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) {
|
||||
bool is_float, const int* volume, float gain) {
|
||||
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,51 +343,46 @@ 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;
|
||||
value *= volume[src_ch] / 32768.0f * gain;
|
||||
// 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;
|
||||
}
|
||||
|
||||
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) {
|
||||
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor, uint32_t duration_ms) {
|
||||
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) != SDL_GAMEPAD_TYPE_PS5) {
|
||||
return false;
|
||||
}
|
||||
@@ -309,12 +390,13 @@ bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) {
|
||||
SelectController(controller);
|
||||
g_large_motor = large_motor;
|
||||
g_small_motor = small_motor;
|
||||
g_rumble_until = SDL_GetTicks() + 0xffff;
|
||||
g_rumble_until = SDL_GetTicks() + duration_ms;
|
||||
ApplyRumble();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
DualSenseBluetooth::Shutdown();
|
||||
Common::LockGuard lock(g_mutex);
|
||||
SelectController(-1);
|
||||
SelectSpeaker(-1);
|
||||
|
||||
@@ -11,13 +11,16 @@ 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);
|
||||
bool is_float, const int* volume, float gain = 1.0f);
|
||||
// Returns false for other gamepad types, which retain the normal rumble path.
|
||||
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor);
|
||||
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor,
|
||||
uint32_t duration_ms = 0xffff);
|
||||
// Also restores the headphone routing.
|
||||
void Shutdown();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -111,6 +111,10 @@ int KYTY_SYSV_ABI NetResolverDestroy(int rid) {
|
||||
return FinishNetCall(Net::NetResolverDestroy(rid));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetResolverAbort(int rid, int flags) {
|
||||
return FinishNetCall(Net::NetResolverAbort(rid, flags));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout,
|
||||
int retry, int flags) {
|
||||
return FinishNetCall(Net::NetResolverStartNtoa(rid, hostname, addr, timeout, retry, flags));
|
||||
@@ -203,6 +207,7 @@ LIB_DEFINE(InitNet_1_Net) {
|
||||
LIB_FUNC("K7RlrTkI-mw", LibNet::NetPoolDestroy);
|
||||
LIB_FUNC("C4UgDHHPvdw", LibNet::NetResolverCreate);
|
||||
LIB_FUNC("kJlYH5uMAWI", LibNet::NetResolverDestroy);
|
||||
LIB_FUNC("AzqoBha7js4", LibNet::NetResolverAbort);
|
||||
LIB_FUNC("Nd91WaWmG2w", LibNet::NetResolverStartNtoa);
|
||||
LIB_FUNC("8Kcp5d-q1Uo", LibNet::NetInetPton);
|
||||
LIB_FUNC("9vA2aW+CHuA", LibNet::NetInetNtop);
|
||||
|
||||
@@ -1343,6 +1343,12 @@ int KYTY_SYSV_ABI NetResolverDestroy(int rid) {
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetResolverAbort(int rid, int flags) {
|
||||
PRINT_NAME();
|
||||
LOGF("\t rid = %d\n\t flags = 0x%08x\n", rid, flags);
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout,
|
||||
int retry, int flags) {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -41,6 +41,7 @@ int KYTY_SYSV_ABI NetPoolCreate(const char* name, int size, int flags);
|
||||
int KYTY_SYSV_ABI NetPoolDestroy(int memid);
|
||||
int KYTY_SYSV_ABI NetResolverCreate(const char* name, int memid, int flags);
|
||||
int KYTY_SYSV_ABI NetResolverDestroy(int rid);
|
||||
int KYTY_SYSV_ABI NetResolverAbort(int rid, int flags);
|
||||
int KYTY_SYSV_ABI NetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout,
|
||||
int retry, int flags);
|
||||
int KYTY_SYSV_ABI NetInetPton(int af, const char* src, void* dst);
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
// 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;
|
||||
}
|
||||
float GetSettingScale(Setting) {
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
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*, float) {
|
||||
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");
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
// Exercise the production controller with deterministic host output and time.
|
||||
#include <SDL3/SDL.h>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
void Check(bool condition, const char* text) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "ControllerSettingsTests: %s\n", text);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
struct Rumble {
|
||||
Uint16 large, small;
|
||||
Uint32 duration;
|
||||
};
|
||||
Uint64 now = 1000;
|
||||
std::vector<Rumble> rumble;
|
||||
std::vector<Rumble> haptics;
|
||||
std::vector<std::array<Uint8, 32>> effects;
|
||||
bool haptics_handles_rumble = false;
|
||||
} // namespace
|
||||
|
||||
namespace Fake {
|
||||
Uint64 GetTicks() {
|
||||
return now;
|
||||
}
|
||||
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
|
||||
return id == 1 || id == 2 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
: nullptr;
|
||||
}
|
||||
SDL_GamepadType GetGamepadType(SDL_Gamepad*) {
|
||||
return SDL_GAMEPAD_TYPE_PS5;
|
||||
}
|
||||
bool RumbleGamepad(SDL_Gamepad*, Uint16 large, Uint16 small, Uint32 duration) {
|
||||
rumble.push_back({large, small, duration});
|
||||
return true;
|
||||
}
|
||||
bool SendGamepadEffect(SDL_Gamepad*, const void* data, int size) {
|
||||
Check(size == 32, "unexpected DualSense effect size");
|
||||
std::array<Uint8, 32> effect {};
|
||||
std::memcpy(effect.data(), data, effect.size());
|
||||
effects.push_back(effect);
|
||||
return true;
|
||||
}
|
||||
bool SetGamepadLED(SDL_Gamepad*, Uint8, Uint8, Uint8) {
|
||||
return true;
|
||||
}
|
||||
bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) {
|
||||
return false;
|
||||
}
|
||||
bool SetGamepadSensorEnabled(SDL_Gamepad*, SDL_SensorType, bool) {
|
||||
return true;
|
||||
}
|
||||
void CloseGamepad(SDL_Gamepad*) {}
|
||||
void Delay(Uint32) {}
|
||||
} // namespace Fake
|
||||
|
||||
#define SDL_GetTicks Fake::GetTicks
|
||||
#define SDL_GetGamepadFromID Fake::GetGamepadFromID
|
||||
#define SDL_GetGamepadType Fake::GetGamepadType
|
||||
#define SDL_RumbleGamepad Fake::RumbleGamepad
|
||||
#define SDL_SendGamepadEffect Fake::SendGamepadEffect
|
||||
#define SDL_SetGamepadLED Fake::SetGamepadLED
|
||||
#define SDL_GamepadHasSensor Fake::GamepadHasSensor
|
||||
#define SDL_SetGamepadSensorEnabled Fake::SetGamepadSensorEnabled
|
||||
#define SDL_CloseGamepad Fake::CloseGamepad
|
||||
#define SDL_Delay Fake::Delay
|
||||
#include "libs/controller.cpp"
|
||||
#undef SDL_GetTicks
|
||||
#undef SDL_GetGamepadFromID
|
||||
#undef SDL_GetGamepadType
|
||||
#undef SDL_RumbleGamepad
|
||||
#undef SDL_SendGamepadEffect
|
||||
#undef SDL_SetGamepadLED
|
||||
#undef SDL_GamepadHasSensor
|
||||
#undef SDL_SetGamepadSensorEnabled
|
||||
#undef SDL_CloseGamepad
|
||||
#undef SDL_Delay
|
||||
|
||||
namespace Libs::Controller::DualSenseHaptics {
|
||||
bool SetVibration(int, uint8_t large_motor, uint8_t small_motor, uint32_t duration_ms) {
|
||||
haptics.push_back({large_motor, small_motor, duration_ms});
|
||||
return haptics_handles_rumble;
|
||||
}
|
||||
void Shutdown() {}
|
||||
} // namespace Libs::Controller::DualSenseHaptics
|
||||
|
||||
namespace Libs::LibKernel {
|
||||
uint64_t KYTY_SYSV_ABI KernelGetProcessTime() {
|
||||
return now * 1000;
|
||||
}
|
||||
} // namespace Libs::LibKernel
|
||||
|
||||
namespace Loader::Timer {
|
||||
double GetTimeMs() {
|
||||
return static_cast<double>(now);
|
||||
}
|
||||
} // namespace Loader::Timer
|
||||
|
||||
namespace {
|
||||
using namespace Libs::Controller;
|
||||
|
||||
struct Controller {
|
||||
Controller() {
|
||||
now = 1000;
|
||||
haptics_handles_rumble = false;
|
||||
Initialize();
|
||||
Connect(1);
|
||||
Check(GetSettingScale(Setting::SpeakerVolume) == 1.0f &&
|
||||
GetSettingScale(Setting::VibrationIntensity) == 1.0f &&
|
||||
GetSettingScale(Setting::TriggerEffectIntensity) == 1.0f,
|
||||
"controller initialization retained old settings");
|
||||
rumble.clear();
|
||||
haptics.clear();
|
||||
effects.clear();
|
||||
}
|
||||
~Controller() { Shutdown(); }
|
||||
};
|
||||
|
||||
DualSenseEffects LastEffect() {
|
||||
Check(!effects.empty(), "missing trigger output");
|
||||
DualSenseEffects effect {};
|
||||
std::memcpy(&effect, effects.back().data(), sizeof(effect));
|
||||
return effect;
|
||||
}
|
||||
|
||||
int ZoneStrength(const uint8_t* effect, int zone) {
|
||||
const unsigned active = effect[1] | (effect[2] << 8u);
|
||||
const uint32_t packed = effect[3] | (effect[4] << 8u) | (effect[5] << 16u) |
|
||||
(static_cast<uint32_t>(effect[6]) << 24u);
|
||||
return (active & (1u << zone)) != 0 ? 1 + ((packed >> (3 * zone)) & 7u) : 0;
|
||||
}
|
||||
|
||||
void SetRumble(uint8_t large, uint8_t small) {
|
||||
const PadVibrationParam param {large, small};
|
||||
Check(PadSetVibration(1, ¶m) == 0, "vibration request failed");
|
||||
}
|
||||
|
||||
void TestSettingCycles() {
|
||||
Controller controller;
|
||||
for (auto setting:
|
||||
{Setting::SpeakerVolume, Setting::VibrationIntensity, Setting::TriggerEffectIntensity}) {
|
||||
Check(GetSettingScale(setting) == 1.0f, "initial setting is not strong");
|
||||
const std::vector<float> levels = setting == Setting::SpeakerVolume
|
||||
? std::vector<float> {0.0f, 0.02f, 0.12f, 0.42f, 1.0f}
|
||||
: std::vector<float> {0.0f, 0.33f, 0.66f, 1.0f};
|
||||
for (int cycle = 0; cycle < 2; ++cycle) {
|
||||
for (float level: levels) {
|
||||
CycleSetting(setting);
|
||||
Check(GetSettingScale(setting) == level, "setting did not cycle or wrap");
|
||||
}
|
||||
}
|
||||
}
|
||||
CycleSetting(Setting::SpeakerVolume);
|
||||
CycleSetting(Setting::VibrationIntensity);
|
||||
CycleSetting(Setting::TriggerEffectIntensity);
|
||||
}
|
||||
|
||||
void TestVibrationLifetime() {
|
||||
Controller controller;
|
||||
SetRumble(255, 1);
|
||||
Check(rumble.size() == 1 && rumble.back().large == 65535 && rumble.back().small == 257 &&
|
||||
rumble.back().duration == 65535,
|
||||
"full vibration changed");
|
||||
now += 100;
|
||||
CycleSetting(Setting::VibrationIntensity);
|
||||
Check(rumble.back().large == 0 && rumble.back().small == 0, "muting did not stop vibration");
|
||||
now += 100;
|
||||
CycleSetting(Setting::VibrationIntensity);
|
||||
Check(rumble.back().large == 84 * 257 && rumble.back().small == 257 &&
|
||||
rumble.back().duration == 65335,
|
||||
"changing intensity lost a small motor or extended the vibration deadline");
|
||||
Check(effects.empty(), "vibration intensity resent triggers");
|
||||
now = 1000 + 65535;
|
||||
const auto calls = rumble.size();
|
||||
CycleSetting(Setting::VibrationIntensity);
|
||||
CycleSetting(Setting::VibrationIntensity);
|
||||
Check(rumble.size() == calls || (rumble.back().large == 0 && rumble.back().small == 0),
|
||||
"expired vibration was resurrected");
|
||||
SetRumble(0, 0);
|
||||
const auto stopped = rumble.size();
|
||||
CycleSetting(Setting::VibrationIntensity);
|
||||
CycleSetting(Setting::VibrationIntensity);
|
||||
Check(rumble.size() == stopped || (rumble.back().large == 0 && rumble.back().small == 0),
|
||||
"explicitly stopped vibration was resurrected");
|
||||
}
|
||||
|
||||
void TestMaskedTriggersAndValidation() {
|
||||
Controller controller;
|
||||
PadTriggerEffectParam param {};
|
||||
param.trigger_mask = 3;
|
||||
param.command[0].mode = 1;
|
||||
param.command[0].data[0] = 2;
|
||||
param.command[0].data[1] = 8;
|
||||
param.command[1].mode = 2;
|
||||
param.command[1].data[0] = 2;
|
||||
param.command[1].data[1] = 7;
|
||||
param.command[1].data[2] = 6;
|
||||
Check(PadSetTriggerEffect(1, ¶m) == 0, "initial trigger request failed");
|
||||
CycleSetting(Setting::TriggerEffectIntensity);
|
||||
Check(LastEffect().left_trigger[0] == 5 && LastEffect().right_trigger[0] == 5,
|
||||
"muting did not disable both triggers");
|
||||
for (uint32_t mode = 1; mode <= 7; ++mode) {
|
||||
auto invalid = param;
|
||||
invalid.command[1] = {};
|
||||
invalid.command[1].mode = mode;
|
||||
std::memset(invalid.command[1].data, 255, sizeof(invalid.command[1].data));
|
||||
const auto calls = effects.size();
|
||||
Check(PadSetTriggerEffect(1, &invalid) == PAD_ERROR_INVALID_ARG && effects.size() == calls,
|
||||
"muting bypassed trigger validation or sent a partial invalid request");
|
||||
}
|
||||
param.trigger_mask = 4;
|
||||
Check(PadSetTriggerEffect(1, ¶m) == PAD_ERROR_INVALID_ARG, "invalid trigger mask accepted");
|
||||
param.trigger_mask = 1;
|
||||
param.command[0].data[1] = 6;
|
||||
Check(PadSetTriggerEffect(1, ¶m) == 0 && LastEffect().enable_bits == 8,
|
||||
"left-only update touched the right trigger");
|
||||
CycleSetting(Setting::TriggerEffectIntensity);
|
||||
auto effect = LastEffect();
|
||||
Check(effect.enable_bits == 12 && effect.left_trigger[0] == 0x21 &&
|
||||
ZoneStrength(effect.left_trigger, 1) == 0 &&
|
||||
ZoneStrength(effect.left_trigger, 2) == 2 && effect.right_trigger[0] == 0x25 &&
|
||||
effect.right_trigger[1] == 0x84 && effect.right_trigger[3] == 1,
|
||||
"masked updates lost a cached trigger or failed to scale its strength");
|
||||
CycleSetting(Setting::TriggerEffectIntensity);
|
||||
CycleSetting(Setting::TriggerEffectIntensity);
|
||||
effect = LastEffect();
|
||||
Check(ZoneStrength(effect.left_trigger, 2) == 6 && effect.right_trigger[3] == 5,
|
||||
"restoring strong intensity did not recover original trigger strengths");
|
||||
Check(rumble.empty() && haptics.empty(), "trigger intensity resent vibration");
|
||||
}
|
||||
|
||||
void TestIndependentOutputsAndPadSwitch() {
|
||||
Controller controller;
|
||||
haptics_handles_rumble = true;
|
||||
SetRumble(200, 100);
|
||||
now += 100;
|
||||
CycleSetting(Setting::VibrationIntensity);
|
||||
CycleSetting(Setting::VibrationIntensity);
|
||||
Check(rumble.empty() && haptics.back().large == 66 && haptics.back().small == 33 &&
|
||||
haptics.back().duration == 65435,
|
||||
"haptics rumble did not receive scaled motors and remaining duration");
|
||||
PadTriggerEffectParam param {};
|
||||
param.trigger_mask = 1;
|
||||
param.command[0].mode = 1;
|
||||
param.command[0].data[1] = 8;
|
||||
Check(PadSetTriggerEffect(1, ¶m) == 0, "trigger request failed");
|
||||
const auto vibration_calls = haptics.size();
|
||||
const auto trigger_calls = effects.size();
|
||||
CycleSetting(Setting::SpeakerVolume);
|
||||
Check(haptics.size() == vibration_calls && effects.size() == trigger_calls,
|
||||
"speaker volume resent controller effects");
|
||||
Connect(2);
|
||||
Disconnect(1);
|
||||
Check(GetActiveControllerId() == 2, "active controller did not change");
|
||||
haptics.clear();
|
||||
effects.clear();
|
||||
CycleSetting(Setting::VibrationIntensity);
|
||||
CycleSetting(Setting::TriggerEffectIntensity);
|
||||
CycleSetting(Setting::TriggerEffectIntensity);
|
||||
Check(effects.empty(), "previous controller's trigger effects leaked to new pad");
|
||||
for (const auto& request: haptics) {
|
||||
Check(request.large == 0 && request.small == 0,
|
||||
"previous controller's vibration leaked to new pad");
|
||||
}
|
||||
SetRumble(200, 100);
|
||||
Check(PadSetTriggerEffect(1, ¶m) == 0, "replacement pad trigger request failed");
|
||||
EmergencyShutdown();
|
||||
Check(GetActiveControllerId() == -1, "released controller remained active");
|
||||
haptics.clear();
|
||||
effects.clear();
|
||||
CycleSetting(Setting::VibrationIntensity);
|
||||
CycleSetting(Setting::TriggerEffectIntensity);
|
||||
Check(effects.empty(), "released controller received a trigger request");
|
||||
for (const auto& request: haptics) {
|
||||
Check(request.large == 0 && request.small == 0,
|
||||
"released controller retained a cached vibration");
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
Config::Initialize();
|
||||
TestSettingCycles();
|
||||
// Each following test initializes another controller and requires strong defaults.
|
||||
TestVibrationLifetime();
|
||||
TestMaskedTriggersAndValidation();
|
||||
TestIndependentOutputsAndPadSwitch();
|
||||
Config::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+756
-40
@@ -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,33 @@ 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;
|
||||
float speaker_scale = 1.0f, vibration_scale = 1.0f;
|
||||
} // namespace
|
||||
|
||||
namespace Fake {
|
||||
@@ -116,8 +132,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 +151,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 +226,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 +259,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 +303,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
|
||||
@@ -212,6 +326,9 @@ namespace Libs::Controller {
|
||||
int GetActiveControllerId() {
|
||||
return active_controller;
|
||||
}
|
||||
float GetSettingScale(Setting setting) {
|
||||
return setting == Setting::SpeakerVolume ? speaker_scale : vibration_scale;
|
||||
}
|
||||
} // namespace Libs::Controller
|
||||
|
||||
namespace Libs::LibKernel {
|
||||
@@ -228,6 +345,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 +358,18 @@ 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;
|
||||
speaker_scale = vibration_scale = 1.0f;
|
||||
now = 1000;
|
||||
opens = rumble_calls = 0;
|
||||
fail_open = fail_resume = false;
|
||||
actual_channels = 4;
|
||||
@@ -255,14 +380,14 @@ 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);
|
||||
}
|
||||
void Queue(const Port& port, const void* data, uint32_t frames = 2, uint32_t channels = 2,
|
||||
bool is_float = true, const int* volume = unity.data()) {
|
||||
Haptics::Queue(port.get(), 1, data, frames, channels, is_float, volume);
|
||||
bool is_float = true, const int* volume = unity.data(), float gain = 1.0f) {
|
||||
Haptics::Queue(port.get(), 1, data, frames, channels, is_float, volume, gain);
|
||||
}
|
||||
void Pull() {
|
||||
std::array<float, 64> buffer {};
|
||||
@@ -292,6 +417,8 @@ void TestFormatsAndVolume() {
|
||||
const std::array<int16_t, 2> mono {16384, -32768};
|
||||
Queue(port, mono.data(), 2, 1, false, volume.data() + 1);
|
||||
ExpectPcm({0, 0, 0.25f, 0.25f, 0, 0, -0.5f, -0.5f});
|
||||
Queue(port, mono.data(), 2, 1, false, volume.data() + 1, 0.5f);
|
||||
ExpectPcm({0, 0, 0.125f, 0.125f, 0, 0, -0.25f, -0.25f});
|
||||
std::array<float, 24> multichannel {};
|
||||
multichannel.fill(0.9f);
|
||||
multichannel[0] = 0.1f;
|
||||
@@ -441,6 +568,33 @@ void TestRumbleLeaseAndDuration() {
|
||||
Check(rumble_calls == calls, "audio pull touched rumble after shutdown");
|
||||
}
|
||||
|
||||
void TestScaledRumbleDuration() {
|
||||
for (bool bluetooth: {false, true}) {
|
||||
Fixture f;
|
||||
wireless = hid_available = bluetooth;
|
||||
auto port = Open();
|
||||
std::vector<float> sound(2048, 0.5f);
|
||||
Haptics::SetVibration(1, 100, 50, 1000);
|
||||
Queue(port, sound.data(), 1024, 2, true, unity.data(), 0.0f);
|
||||
Check(rumble.large == 100 * 0x101, "muted haptics suppressed motor rumble");
|
||||
Queue(port, sound.data(), 1024, 2, true, unity.data(), 0.5f);
|
||||
Check(rumble.large == 0, "scaled haptics did not suppress motor rumble");
|
||||
now += 100;
|
||||
Haptics::SetVibration(1, 50, 25, 900);
|
||||
now += 200;
|
||||
if (bluetooth) {
|
||||
Haptics::UpdateBluetoothRumble();
|
||||
} else {
|
||||
Pull();
|
||||
}
|
||||
Check(rumble.large == 50 * 0x101 && rumble.small == 25 * 0x101 && rumble.duration == 700,
|
||||
"scaled rumble lost its strength or extended its original expiry");
|
||||
Haptics::SetVibration(1, 50, 25, 0);
|
||||
Check(rumble.large == 0 && rumble.small == 0 && rumble.duration == 0,
|
||||
"expired rumble was restarted");
|
||||
}
|
||||
}
|
||||
|
||||
void TestSwitchStopsOldRumble() {
|
||||
Fixture f;
|
||||
auto port = Open();
|
||||
@@ -516,6 +670,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, 0.5f);
|
||||
auto pending = Bluetooth::Prepare(now * 1000000);
|
||||
Check(pending.size() == 1 && hid_opens == 2 && effects[1][7] == 0x30 &&
|
||||
std::abs(pending[0].audio[200] - 0.125f) < 1e-4f &&
|
||||
std::abs(pending[0].audio[201] - 0.125f) < 1e-4f,
|
||||
"Bluetooth mono int16 volume/gain 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;
|
||||
@@ -543,6 +1137,105 @@ void TestAudioSpeakerRouting() {
|
||||
SDL_GetAudioStreamQueued(default_stream) == 0,
|
||||
"successful DualSense playback also queued audio on the main output");
|
||||
ExpectPcm({0.5f, 0.5f, 0, 0, 0.5f, 0.5f, 0, 0});
|
||||
speaker_scale = 0.5f;
|
||||
audio.AudioOutOutputs(&output, 1, false);
|
||||
ExpectPcm({0.25f, 0.25f, 0, 0, 0.25f, 0.25f, 0, 0});
|
||||
speaker_scale = 0.0f;
|
||||
audio.AudioOutOutputs(&output, 1, false);
|
||||
ExpectPcm({});
|
||||
active_controller = 2;
|
||||
audio.AudioOutOutputs(&output, 1, false);
|
||||
std::array<float, 4> fallback {};
|
||||
Check(SDL_GetAudioStreamData(default_stream, fallback.data(), sizeof(fallback)) ==
|
||||
sizeof(fallback) &&
|
||||
fallback == std::array<float, 4> {},
|
||||
"unplugging the controller unmuted its speaker fallback");
|
||||
}
|
||||
|
||||
void TestAudioFallbackGain() {
|
||||
for (bool is_float: {false, true}) {
|
||||
Fixture f;
|
||||
Libs::Audio::Audio audio;
|
||||
active_controller = 2;
|
||||
speaker_scale = 0.5f;
|
||||
const std::array<int16_t, 4> integer_pcm {16384, 16384, 16384, 16384};
|
||||
const auto format = is_float ? Libs::Audio::Audio::Format::FloatStereo
|
||||
: Libs::Audio::Audio::Format::Signed16bitStereo;
|
||||
const void* data = is_float ? static_cast<const void*>(pcm.data()) : integer_pcm.data();
|
||||
for (int type: {0, 4}) {
|
||||
const auto port = audio.AudioOutOpen(type, 2, 48000, format);
|
||||
const std::array<int, 2> volume {16384, 16384};
|
||||
audio.AudioOutSetVolume(port, 3, volume.data());
|
||||
Libs::Audio::Audio::OutputParam output {port, data};
|
||||
audio.AudioOutOutputs(&output, 1, false);
|
||||
const float expected = type == 4 ? 0.125f : 0.25f;
|
||||
std::array<float, 4> actual {};
|
||||
if (is_float) {
|
||||
Check(SDL_GetAudioStreamData(default_stream, actual.data(), sizeof(actual)) ==
|
||||
sizeof(actual),
|
||||
"float fallback output is missing");
|
||||
} else {
|
||||
std::array<int16_t, 4> integers {};
|
||||
Check(SDL_GetAudioStreamData(default_stream, integers.data(), sizeof(integers)) ==
|
||||
sizeof(integers),
|
||||
"integer fallback output is missing");
|
||||
std::transform(integers.begin(), integers.end(), actual.begin(),
|
||||
[](int16_t value) { return value / 32768.0f; });
|
||||
}
|
||||
Check(std::all_of(actual.begin(), actual.end(),
|
||||
[expected](float value) { return value == expected; }),
|
||||
"speaker setting gain was lost or applied to ordinary audio");
|
||||
audio.AudioOutClose(port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestAudioVibrationGain() {
|
||||
for (bool bluetooth: {false, true}) {
|
||||
Fixture f;
|
||||
wireless = hid_available = bluetooth;
|
||||
Libs::Audio::Audio audio;
|
||||
vibration_scale = 0.5f;
|
||||
speaker_scale = 0.0f;
|
||||
const auto port =
|
||||
audio.AudioOutOpen(10, 1024, 48000, Libs::Audio::Audio::Format::FloatStereo);
|
||||
std::vector<float> sound(2048, 0.5f);
|
||||
Libs::Audio::Audio::OutputParam output {port, sound.data()};
|
||||
audio.AudioOutOutputs(&output, 1, false);
|
||||
Check(default_stream == nullptr, "vibration port opened ordinary audio");
|
||||
if (bluetooth) {
|
||||
Check(Bluetooth::Prepare(now * 1000000).empty(),
|
||||
"Bluetooth vibration skipped its coalescing window");
|
||||
const auto pending = Bluetooth::Prepare((now + 1) * 1000000);
|
||||
Check(pending.size() == 1 && pending[0].data[76] == 32,
|
||||
"Bluetooth vibration did not apply its own setting gain");
|
||||
} else {
|
||||
ExpectPcm({0, 0, 0.25f, 0.25f, 0, 0, 0.25f, 0.25f});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
speaker_scale = 0.5f;
|
||||
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);
|
||||
Check(pending.size() == 1 && std::abs(pending[0].audio[200] - 0.125f) < 1e-4f,
|
||||
"Bluetooth speaker did not apply its setting gain");
|
||||
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() {
|
||||
@@ -564,6 +1257,9 @@ int main() {
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
TestAudioSpeakerFallback();
|
||||
TestAudioSpeakerRouting();
|
||||
TestAudioFallbackGain();
|
||||
TestAudioVibrationGain();
|
||||
TestBluetoothAudioSpeakerRouting();
|
||||
TestAudioDefaultResumeFailure();
|
||||
TestFormatsAndVolume();
|
||||
TestDiscoveryAndHotplug();
|
||||
@@ -571,8 +1267,28 @@ 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();
|
||||
TestScaledRumbleDuration();
|
||||
TestSwitchStopsOldRumble();
|
||||
TestCloseRestoresRumble();
|
||||
std::printf("PadHapticsTests: all cases passed\n");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -278,6 +278,101 @@ void TestFailedMaterializationRejectsStage() {
|
||||
"missing runtime user data did not reject the cached stage");
|
||||
}
|
||||
|
||||
void TestFiniteImageRefreshReusesScalarReads() {
|
||||
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 &srt = block.AppendNewInst(ValueOpcode::GetSrtResource);
|
||||
for (uint32_t index = 0; index < 3; ++index) {
|
||||
auto &read = block.AppendNewInst(ValueOpcode::LoadAddressU32,
|
||||
{Value(&handle), Value(index * 4u), Value(0u), Value(true)});
|
||||
read.SetFlags(MemoryFlags{.index = 0});
|
||||
program.srt_reads.push_back({Value(&read), index});
|
||||
auto &flat = block.AppendNewInst(ValueOpcode::ReadConst,
|
||||
{Value(&srt), Value(index)});
|
||||
DescriptorSource source;
|
||||
source.dword_count = 8;
|
||||
source.dwords.fill(Value(0u));
|
||||
source.dwords[0] = Value(&flat);
|
||||
source.dwords[1] = Value(static_cast<uint32_t>(
|
||||
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u);
|
||||
source.dwords[3] = Value(Libs::Graphics::DstSel(4, 5, 6, 7) |
|
||||
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u));
|
||||
program.descriptor_sources.push_back(source);
|
||||
}
|
||||
DescriptorSource root;
|
||||
root.dword_count = 8;
|
||||
root.dwords.fill(Value(0u));
|
||||
root.indirect_image.emplace(DescriptorSource::IndirectImage{}).sources = {0, 1, 2, 1};
|
||||
program.descriptor_sources.push_back(root);
|
||||
program.info.images.push_back({
|
||||
.source = 3,
|
||||
.resource_class = ImageResourceClass::Sampled,
|
||||
.numeric_class = Libs::Graphics::Prospero::TextureNumericClass::Float,
|
||||
.dimension = Libs::Graphics::ShaderRecompiler::Decoder::ImageDimension::Dim2D});
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
struct Reads {
|
||||
std::array<uint32_t, 3> words{0x100u, 0x200u, 0x200u};
|
||||
std::array<uint32_t, 3> counts{};
|
||||
uint32_t ordinary = 0;
|
||||
} reads;
|
||||
const SrtRuntime runtime{
|
||||
.read_memory = +[](void *data, uint64_t, std::span<uint32_t>) {
|
||||
++static_cast<Reads *>(data)->ordinary;
|
||||
return false;
|
||||
},
|
||||
.userdata = &reads,
|
||||
.read_specialization_memory = +[](void *data, uint64_t address,
|
||||
std::span<uint32_t> words) {
|
||||
if (words.size() != 1 || address < 0x1000u || address >= 0x100cu ||
|
||||
(address & 3u) != 0) return false;
|
||||
auto &reads = *static_cast<Reads *>(data);
|
||||
const auto index = (address - 0x1000u) / 4u;
|
||||
++reads.counts[index];
|
||||
words[0] = reads.words[index];
|
||||
return true;
|
||||
}};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
const auto capacities = [&] {
|
||||
return std::array{snapshot.images.capacity(), snapshot.flattened_srt.capacity(),
|
||||
snapshot.specialization_reads.capacity(), specialization.images.capacity()};
|
||||
};
|
||||
const auto check_mapping = [&](std::array<uint32_t, 4> ordinals) {
|
||||
const auto offset = specialization.images[0].indirect_mapping_offset;
|
||||
Check(snapshot.images.size() == 2 && snapshot.flattened_srt[offset] == 4,
|
||||
"finite image candidates were not deduplicated");
|
||||
for (uint32_t key = 0; key < ordinals.size(); ++key) {
|
||||
Check(snapshot.flattened_srt[offset + 1u + key * 2u] == key &&
|
||||
snapshot.flattened_srt[offset + 2u + key * 2u] == ordinals[key],
|
||||
"finite image selector mapping is stale or incorrect");
|
||||
}
|
||||
};
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"finite image materialization failed");
|
||||
Check(reads.ordinary == 0 && reads.counts == std::array<uint32_t, 3>{1, 1, 1} &&
|
||||
snapshot.specialization_reads.size() == 3,
|
||||
"finite image candidates repeated scalar reads or bypassed clean provenance");
|
||||
check_mapping({0, 1, 1, 1});
|
||||
const auto warm_capacities = capacities();
|
||||
reads.words = {0x300u, 0x300u, 0x400u};
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"finite image refresh failed after descriptor changes");
|
||||
Check(reads.ordinary == 0 && reads.counts == std::array<uint32_t, 3>{2, 2, 2} &&
|
||||
snapshot.specialization_reads.size() == 3 &&
|
||||
snapshot.images[0].dwords[0] == 0x300u && snapshot.images[1].dwords[0] == 0x400u,
|
||||
"finite image refresh retained old scalar values or repeated reads");
|
||||
check_mapping({0, 0, 1, 0});
|
||||
Check(capacities() == warm_capacities,
|
||||
"finite image refresh grew reusable resource storage after warmup");
|
||||
}
|
||||
|
||||
void TestMixedSamplerDuplicatesTheCorrectSnapshot() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
auto plan = MixedSamplerPlan();
|
||||
@@ -314,6 +409,7 @@ int main() {
|
||||
TestUnbasedFlatCacheHitMaterializes();
|
||||
TestWrittenDescriptorUsesStrictReaderOnce();
|
||||
TestFailedMaterializationRejectsStage();
|
||||
TestFiniteImageRefreshReusesScalarReads();
|
||||
TestMixedSamplerDuplicatesTheCorrectSnapshot();
|
||||
std::puts("ResourceMaterializationTests: all cases passed");
|
||||
return 0;
|
||||
|
||||
@@ -2488,6 +2488,132 @@ void TestConditionalSamplerPhi() {
|
||||
}
|
||||
}
|
||||
|
||||
void TestFiniteImagePhiCycle() {
|
||||
Fixture fixture;
|
||||
auto *entry = fixture.block;
|
||||
auto *header = fixture.AddBlock();
|
||||
auto *reload = fixture.AddBlock();
|
||||
auto *merge = fixture.AddBlock();
|
||||
entry->AddBranch(header);
|
||||
header->AddBranch(reload);
|
||||
header->AddBranch(merge);
|
||||
reload->AddBranch(merge);
|
||||
merge->AddBranch(header);
|
||||
std::array<Value, 8> words;
|
||||
for (uint32_t word = 0; word < words.size(); ++word) {
|
||||
auto &loop = header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
|
||||
auto &choice = merge->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
|
||||
loop.AddPhiOperand(entry, fixture.UserData(word));
|
||||
loop.AddPhiOperand(merge, Value(&choice));
|
||||
// Incoming order is independent for each descriptor DWORD.
|
||||
if (word & 1u) {
|
||||
choice.AddPhiOperand(reload, fixture.UserData(word + 8));
|
||||
choice.AddPhiOperand(header, Value(&loop));
|
||||
} else {
|
||||
choice.AddPhiOperand(header, Value(&loop));
|
||||
choice.AddPhiOperand(reload, fixture.UserData(word + 8));
|
||||
}
|
||||
words[word] = Value(&choice);
|
||||
}
|
||||
fixture.block = merge;
|
||||
const auto image = fixture.Image(words);
|
||||
const auto sampler = fixture.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)});
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
|
||||
fixture.AddMemory(memory, 4));
|
||||
fixture.PlanAndTrack();
|
||||
const auto source = fixture.program.info.images[0].source;
|
||||
const auto &finite = fixture.program.descriptor_sources[source].indirect_image;
|
||||
Check(finite && finite->sources.size() == 2,
|
||||
"cyclic image selection did not retain two complete descriptor candidates");
|
||||
const auto *key = image.Instruction()->Arg(0).Instruction();
|
||||
Check(key->GetOpcode() == ValueOpcode::Phi && key->Parent() == merge,
|
||||
"finite image key lost its original merge block");
|
||||
const auto *loop = key->Arg(0).Instruction();
|
||||
Check(loop->GetOpcode() == ValueOpcode::Phi && loop->Parent() == header &&
|
||||
loop->Arg(1).Resolve() == Value(const_cast<Inst *>(key)) &&
|
||||
loop->Arg(0).U32() == 0 && key->Arg(1).U32() == 1,
|
||||
"finite image key did not preserve the loop-carried selection");
|
||||
std::array<uint32_t, 16> data;
|
||||
for (uint32_t i = 0; i < data.size(); ++i) data[i] = 100u + i;
|
||||
SrtWalker walker(fixture.program, SrtRuntime{.user_data = data});
|
||||
for (uint32_t candidate = 0; candidate < 2; ++candidate) {
|
||||
DescriptorValue descriptor;
|
||||
Check(walker.EvaluateDescriptor(finite->sources[candidate], descriptor),
|
||||
"finite image candidate is not host-readable");
|
||||
for (uint32_t word = 0; word < 8; ++word)
|
||||
Check(descriptor.dwords[word] == data[candidate * 8 + word],
|
||||
"finite image selection mixed descriptor DWORDs across predecessors");
|
||||
}
|
||||
}
|
||||
|
||||
void TestFiniteImageBitScanSentinel() {
|
||||
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
|
||||
for (const bool nonzero : {false, true}) {
|
||||
Fixture fixture;
|
||||
auto *entry = fixture.block;
|
||||
auto *dispatch = fixture.AddBlock();
|
||||
auto *load = fixture.AddBlock();
|
||||
auto *merge = fixture.AddBlock();
|
||||
auto *exit = fixture.AddBlock();
|
||||
entry->AddBranch(dispatch);
|
||||
entry->AddBranch(exit);
|
||||
dispatch->AddBranch(load);
|
||||
dispatch->AddBranch(merge);
|
||||
load->AddBranch(merge);
|
||||
const auto mask = fixture.Emit(ValueOpcode::GetBuiltin,
|
||||
{Value(uint32_t(StageInputKind::LocalInvocationIndex)), Value(0u)});
|
||||
const auto condition = fixture.Emit(nonzero ? ValueOpcode::INotEqual32 : ValueOpcode::IEqual32,
|
||||
{mask, Value(0u)});
|
||||
auto &guard = fixture.program.block_info[0];
|
||||
guard.condition = condition;
|
||||
guard.terminator.kind = CFG::TerminatorKind::ConditionalBranch;
|
||||
guard.terminator.true_block = 1;
|
||||
guard.terminator.false_block = 4;
|
||||
fixture.block = dispatch;
|
||||
const auto first = fixture.Emit(ValueOpcode::FindILsb32, {mask});
|
||||
const auto minimum = fixture.Emit(ValueOpcode::UMin32, {first, Value(32u)});
|
||||
const auto selector = fixture.Emit(ValueOpcode::ShiftLeftLogical32, {minimum, Value(2u)});
|
||||
fixture.Emit(ValueOpcode::ReferenceU32, {selector});
|
||||
auto &table = fixture.program.block_info[1];
|
||||
table.indirect_target = selector;
|
||||
table.terminator.kind = CFG::TerminatorKind::IndirectBranch;
|
||||
table.terminator.indirect_selector_code = 0;
|
||||
table.terminator.indirect_selector_values = {0u, 128u};
|
||||
table.terminator.indirect_selector_targets = {2u, 3u};
|
||||
std::array<Value, 8> words;
|
||||
for (uint32_t word = 0; word < words.size(); ++word) {
|
||||
auto &phi = merge->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
|
||||
phi.AddPhiOperand(dispatch, mask);
|
||||
phi.AddPhiOperand(load, Value(100u + word));
|
||||
words[word] = Value(&phi);
|
||||
}
|
||||
fixture.block = merge;
|
||||
const auto image = fixture.Image(words);
|
||||
const auto sampler = fixture.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)});
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
|
||||
fixture.AddMemory(memory, 4));
|
||||
if (!nonzero) {
|
||||
CheckFatal([&] { fixture.PlanAndTrack(); }, "not a valid runtime value",
|
||||
"zero bit scan silently accepted a GPU-valued descriptor sentinel");
|
||||
Check(fixture.program.descriptor_sources.empty() && image.Instruction()->Arg(0) == words[0],
|
||||
"failed finite descriptor analysis partially mutated the resource plan");
|
||||
continue;
|
||||
}
|
||||
fixture.PlanAndTrack();
|
||||
const auto source = fixture.program.info.images[0].source;
|
||||
const auto &finite = fixture.program.descriptor_sources[source].indirect_image;
|
||||
Check(finite && finite->sources.size() == 1 &&
|
||||
image.Instruction()->Arg(0).Instruction()->NumPhiBlocks() == 2,
|
||||
"nonzero bit scan retained its impossible sentinel or removed a Phi edge");
|
||||
}
|
||||
}
|
||||
|
||||
void TestLoopCycleEnteredThroughRuntimeValue() {
|
||||
Fixture fixture;
|
||||
auto *entry = fixture.block;
|
||||
@@ -3292,6 +3418,8 @@ int main() {
|
||||
Run("dynamic SRT", TestDynamicSrtReadRemainsExplicit);
|
||||
Run("phi validation", TestPhiValidation);
|
||||
Run("conditional sampler phi", TestConditionalSamplerPhi);
|
||||
Run("finite image phi cycle", TestFiniteImagePhiCycle);
|
||||
Run("finite image bit scan sentinel", TestFiniteImageBitScanSentinel);
|
||||
Run("runtime-rooted loop", TestLoopCycleEnteredThroughRuntimeValue);
|
||||
Run("invariant loop phi", TestInvariantLoopPhi);
|
||||
Run("DMA address materialization", TestDmaAddressMaterialization);
|
||||
|
||||
@@ -281,6 +281,61 @@ void TestInvariantAndDivergentPhi() {
|
||||
"divergent phi was accepted");
|
||||
}
|
||||
|
||||
void TestOperandMutationLifecycle() {
|
||||
Fixture fixture(6);
|
||||
const auto definition = fixture.Emit(ValueOpcode::IAdd32, {Value(0x55u), Value(0u)});
|
||||
auto *source = definition.TryInstruction();
|
||||
const auto inserted = fixture.Emit(ValueOpcode::BitFieldInsert,
|
||||
{definition, definition, Value(4u), Value(4u)});
|
||||
Check(source->UseCount() == 2, "inline operands lost distinct reverse-use indices");
|
||||
inserted.TryInstruction()->SetArg(1, Value(3u));
|
||||
Check(source->UseCount() == 1, "inline operand mutation did not detach its old use");
|
||||
source->ReplaceUsesWith(Value(0xaau));
|
||||
uint32_t result = 0;
|
||||
Check(source->UseCount() == 0 &&
|
||||
SrtWalker(fixture.program, {}).Evaluate(inserted, result) && result == 0x3au,
|
||||
"inline operand replacement changed the evaluated value");
|
||||
|
||||
for (const auto opcode : {ValueOpcode::GetImageResource, ValueOpcode::MakeImageAddress}) {
|
||||
auto &large = fixture.program.value_storage.emplace_back(opcode);
|
||||
auto &replacement = fixture.program.value_storage.emplace_back(opcode);
|
||||
const auto count = large.NumArgs();
|
||||
for (size_t index = 0; index < count; ++index) {
|
||||
large.SetArg(index, definition);
|
||||
replacement.SetArg(index, Value(static_cast<uint32_t>(index)));
|
||||
}
|
||||
const auto user = fixture.Emit(ValueOpcode::Identity, {Value(&large)}, 0, 1);
|
||||
Check(source->UseCount() == count, "large operands lost reverse-use indices");
|
||||
large.ReplaceUsesWith(Value(&replacement));
|
||||
Check(source->UseCount() == 0 && large.GetOpcode() == ValueOpcode::Identity &&
|
||||
large.NumArgs() == 1 && large.Arg(0) == Value(&replacement) &&
|
||||
user.Resolve() == Value(&replacement) && replacement.UseCount() == 2,
|
||||
"large operand replacement did not preserve users or detach old operands");
|
||||
large.Invalidate();
|
||||
large.Invalidate();
|
||||
Check(large.NumArgs() == 0 && replacement.UseCount() == 1,
|
||||
"repeated invalidation detached a surviving large-value user");
|
||||
}
|
||||
|
||||
auto &phi = fixture.BlockAt(5).AppendNewInst(ValueOpcode::Phi);
|
||||
phi.SetFlags(Type::U32);
|
||||
for (size_t index = 0; index < 5; ++index) {
|
||||
phi.AddPhiOperand(&fixture.BlockAt(index), definition);
|
||||
}
|
||||
phi.SetArg(3, Value(7u));
|
||||
Check(phi.NumArgs() == 5 && phi.NumPhiBlocks() == 5 && source->UseCount() == 4,
|
||||
"Phi mutation lost its incoming values or reverse uses");
|
||||
for (size_t index = 0; index < 5; ++index) {
|
||||
Check(phi.PhiBlock(index) == &fixture.BlockAt(index),
|
||||
"Phi mutation changed an incoming predecessor");
|
||||
}
|
||||
const auto sum = fixture.Emit(ValueOpcode::IAdd32, {Value(&phi), Value(5u)}, 0, 5);
|
||||
phi.ReplaceUsesWith(Value(6u));
|
||||
Check(source->UseCount() == 0 && phi.NumPhiBlocks() == 0 && phi.NumArgs() == 1 &&
|
||||
SrtWalker(fixture.program, {}).Evaluate(sum, result) && result == 11,
|
||||
"Phi replacement retained incoming uses or changed the consumer value");
|
||||
}
|
||||
|
||||
void TestControlDependentStandaloneLoadStaysTyped() {
|
||||
Fixture fixture(3);
|
||||
const auto memory = fixture.AddMemory(ResourceKind::ScalarAddress);
|
||||
@@ -595,6 +650,39 @@ void TestControlFlowValueSurvivesReadLaneFolding() {
|
||||
ValidateProgram(fixture.program, true);
|
||||
}
|
||||
|
||||
void TestDeadPhiCyclesAndPlanningRoots() {
|
||||
Fixture fixture(2);
|
||||
auto &entry = fixture.BlockAt(0);
|
||||
auto &loop = fixture.BlockAt(1);
|
||||
entry.AddBranch(&loop);
|
||||
loop.AddBranch(&loop);
|
||||
const auto source = fixture.Emit(ValueOpcode::GetUserData, {Value(static_cast<ScalarReg>(2))});
|
||||
const auto identity = fixture.Emit(ValueOpcode::Identity, {source});
|
||||
auto &live = loop.AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
|
||||
live.AddPhiOperand(&entry, identity);
|
||||
live.AddPhiOperand(&loop, Value(&live));
|
||||
const auto reference = fixture.Emit(ValueOpcode::ReferenceU32, {Value(&live)}, 0, 1);
|
||||
auto &dead = loop.AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
|
||||
const auto increment = fixture.Emit(ValueOpcode::IAdd32, {Value(&dead), Value(1u)}, 0, 1);
|
||||
dead.AddPhiOperand(&entry, source);
|
||||
dead.AddPhiOperand(&loop, increment);
|
||||
const auto retained = fixture.Emit(ValueOpcode::GetUserData, {Value(static_cast<ScalarReg>(3))});
|
||||
auto &planning = fixture.program.value_storage.emplace_back(ValueOpcode::Identity);
|
||||
planning.SetArg(0, retained);
|
||||
|
||||
EliminateDeadCode(fixture.program.blocks);
|
||||
Check(entry.Instructions().size() == 3 && loop.Instructions().size() == 2,
|
||||
"dead Phi cycle survived or a live/planning dependency was removed");
|
||||
Check(identity.Instruction()->Arg(0) == source && live.Arg(0) == identity &&
|
||||
live.Arg(1) == Value(&live) && planning.Arg(0) == retained,
|
||||
"DCE did not preserve direct Identity operands or live Phi recurrence");
|
||||
// Dropping roots must clear prior marks and remove the newly dead SCC safely.
|
||||
reference.Instruction()->Invalidate();
|
||||
planning.Invalidate();
|
||||
EliminateDeadCode(fixture.program.blocks);
|
||||
Check(entry.empty() && loop.empty(), "DCE reused stale marks after removing its roots");
|
||||
}
|
||||
|
||||
void TestUndefinedRuntimeValueFails() {
|
||||
Fixture fixture;
|
||||
const auto undef = fixture.Emit(ValueOpcode::UndefU32);
|
||||
@@ -634,6 +722,7 @@ int main() {
|
||||
TestShaderBaseAndUserData();
|
||||
TestCarryAndBitFields();
|
||||
TestInvariantAndDivergentPhi();
|
||||
TestOperandMutationLifecycle();
|
||||
TestControlDependentStandaloneLoadStaysTyped();
|
||||
TestRuntime64BitDescriptorOps();
|
||||
TestUniformFirstLaneSamplerLod();
|
||||
@@ -643,6 +732,7 @@ int main() {
|
||||
TestReadLaneElimination();
|
||||
TestOptimizationPipeline();
|
||||
TestControlFlowValueSurvivesReadLaneFolding();
|
||||
TestDeadPhiCyclesAndPlanningRoots();
|
||||
TestUndefinedRuntimeValueFails();
|
||||
std::cout << "TypedValuePlanningTests: all cases passed\n";
|
||||
return 0;
|
||||
|
||||
@@ -4208,7 +4208,30 @@ public:
|
||||
cache.FillBuffer(base + offset, sizeof(window_value), window_value,
|
||||
false);
|
||||
}
|
||||
const auto window_publication_tick = scheduler.CurrentTick();
|
||||
std::binary_semaphore window_publication_entered{0};
|
||||
std::binary_semaphore release_window_publication{0};
|
||||
std::atomic<bool> window_readback_returned{false};
|
||||
scheduler.DeferPriorityOperation([&] {
|
||||
window_publication_entered.release();
|
||||
release_window_publication.acquire();
|
||||
Libs::LibKernel::Memory::WriteBacking(
|
||||
base + window_inside_offset, &window_stale, sizeof(window_stale));
|
||||
});
|
||||
std::jthread release_window_callback([&] {
|
||||
window_publication_entered.acquire();
|
||||
const auto deadline = std::chrono::steady_clock::now() +
|
||||
std::chrono::milliseconds(100);
|
||||
while (!window_readback_returned.load() &&
|
||||
std::chrono::steady_clock::now() < deadline) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
release_window_publication.release();
|
||||
});
|
||||
cache.ReadMemory(base + window_fault_offset, sizeof(window_value));
|
||||
window_readback_returned = true;
|
||||
release_window_callback.join();
|
||||
scheduler.WaitPriorityOperations(window_publication_tick);
|
||||
uint32_t window_inside_backing = 0;
|
||||
uint32_t window_outside_backing = 0;
|
||||
Libs::LibKernel::Memory::TryReadBacking(base + window_inside_offset,
|
||||
@@ -4217,7 +4240,7 @@ public:
|
||||
Libs::LibKernel::Memory::TryReadBacking(base + window_outside_offset,
|
||||
&window_outside_backing,
|
||||
sizeof(window_outside_backing));
|
||||
Require(name, "widened-window boundary",
|
||||
Require(name, "widened-window publication order and boundary",
|
||||
window_inside_backing == window_value &&
|
||||
window_outside_backing == window_stale &&
|
||||
!cache.HasGpuDirtyBytes(base + window_inside_offset,
|
||||
@@ -4228,7 +4251,8 @@ public:
|
||||
sizeof(window_value)) &&
|
||||
cache.IsRegionGpuModified(base + window_outside_offset,
|
||||
sizeof(window_value)),
|
||||
"readback did not honor the clamped half-open 512 KiB window");
|
||||
"readback did not publish after the older overlapping callback "
|
||||
"or honor the clamped half-open 512 KiB window");
|
||||
cache.ReadMemory(base + window_outside_offset, sizeof(window_value));
|
||||
|
||||
Libs::LibKernel::Memory::WriteBacking(base + first_offset, &first_stale,
|
||||
|
||||
Reference in New Issue
Block a user