Compare commits

...

9 Commits

Author SHA1 Message Date
Pyxellator e1be2ead54 DualSense haptics and controller speaker sound bluethooth (#969)
* pad: send DualSense speaker and haptic audio over Bluetooth

* ci: fix Opus clang-cl build and macOS memory tracker test

* docs: remove Bluetooth notes from project README

* pad: keep Bluetooth audio work off emulation timing path

* pad: smooth DualSense speaker audio on USB and Bluetooth

* tests: find available macOS guest range for page manager

* pad: keep haptic waveform when Bluetooth queue overflows

* audio: reject zero output sample frequency

* Simplify DualSense audio routing and fix Bluetooth batching

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-02 14:55:00 +04:00
nomolao2-cell 89792a3bca Improve Hades II compatibility (#964)
Fix Hades II startup and color clears

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-02 14:37:49 +04:00
nmzik 39e23ac92e Publish synchronous buffer downloads inline after ordered completion 2026-10-02 13:24:03 +04:00
nmzik a67a033ff8 Remove redundant tracking lock checks from the hot path 2026-10-02 13:24:03 +04:00
nmzik d1c99bbfb0 Consolidate instruction operand bounds checks 2026-10-02 13:24:03 +04:00
nmzik a9620f98f4 Query dirty region bits without constructing temporary snapshots 2026-10-02 13:24:03 +04:00
nmzik 28f29b8e6f Inline common IR operands and share storage with Phi pairs 2026-10-02 13:24:03 +04:00
nmzik 3353c1ff3b shader: pack the runtime evaluation index beside the IR opcode 2026-10-02 13:24:03 +04:00
Mr.Hakkuh 70a475ec8c audio: fix popping and time-stretching in output (#966)
fix audio accumulated timing drift

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-02 13:22:38 +04:00
26 changed files with 2052 additions and 254 deletions
+23 -2
View File
@@ -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
+8 -2
View File
@@ -332,7 +332,7 @@ if (KYTY_CLANG_CL)
set_source_files_properties(${kyty_emulator_src} PROPERTIES COMPILE_FLAGS "-Wno-pragma-pack -Wno-deprecated-declarations -D_TIMESPEC_DEFINED")
endif()
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak)
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus)
# Linux system libraries.
if(LINUX)
@@ -481,6 +481,10 @@ add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
target_link_libraries(audio_out2_port_tests common fmt::fmt)
target_include_directories(audio_out2_port_tests PRIVATE ${inc_headers})
add_executable(audio_out_timing_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/AudioOutTimingTests.cpp")
target_link_libraries(audio_out_timing_tests common fmt::fmt SDL3::SDL3-static)
target_include_directories(audio_out_timing_tests PRIVATE ${inc_headers})
add_executable(ngs2_sampler_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/Ngs2SamplerTests.cpp"
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
@@ -491,7 +495,7 @@ target_include_directories(ngs2_sampler_tests PRIVATE ${inc_headers})
add_executable(pad_haptics_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/PadHapticsTests.cpp"
)
target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static)
target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static opus)
target_include_directories(pad_haptics_tests PRIVATE ${inc_headers})
add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
@@ -606,6 +610,7 @@ if(BUILD_TESTING)
add_test(NAME kernel_file_system COMMAND $<TARGET_FILE:kernel_file_system_tests>)
add_test(NAME sync_on_address COMMAND $<TARGET_FILE:sync_on_address_tests>)
add_test(NAME audio_out2_port COMMAND $<TARGET_FILE:audio_out2_port_tests>)
add_test(NAME audio_out_timing COMMAND $<TARGET_FILE:audio_out_timing_tests>)
add_test(NAME ngs2_sampler COMMAND $<TARGET_FILE:ngs2_sampler_tests>)
add_test(NAME pad_haptics COMMAND $<TARGET_FILE:pad_haptics_tests>)
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
@@ -682,6 +687,7 @@ if(BUILD_TESTING)
shader_recompiler_compute_tests
virtual_memory_allocation_tests
audio_out2_port_tests
audio_out_timing_tests
ngs2_sampler_tests
pad_haptics_tests
save_data_memory_tests
@@ -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);
};
+9
View File
@@ -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; }
+2
View File
@@ -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;
+3 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+46 -37
View File
@@ -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
+13 -4
View File
@@ -4,6 +4,7 @@
#include "graphics/shader/recompiler/ir/Reg.h"
#include "graphics/shader/recompiler/ir/opcodes/ValueOpcodes.h"
#include <array>
#include <bit>
#include <cstdint>
#include <cstring>
@@ -133,7 +134,6 @@ public:
void SetArg(size_t index, Value value);
void AddPhiOperand(Block* predecessor, Value value);
void ReplaceUsesWith(Value replacement, bool preserve = true);
void ReplaceOpcode(ValueOpcode opcode);
void Invalidate();
template <typename T>
@@ -156,13 +156,22 @@ private:
void RemoveUse(Inst* used, size_t operand);
void ClearArgs();
static constexpr uint8_t InlineArity = 4;
static constexpr uint8_t PhiArity = UINT8_MAX;
ValueOpcode opcode;
uint8_t num_args;
mutable uint32_t evaluation_index = UINT32_MAX;
uint64_t flags;
Block* parent = nullptr;
std::vector<Value> args;
std::vector<Block*> phi_blocks;
union {
std::array<Value, InlineArity> fixed_args {};
std::vector<Value> large_args;
std::vector<std::pair<Block*, Value>> phi_args;
};
std::vector<Use> uses;
mutable uint32_t evaluation_index = UINT32_MAX;
};
static_assert(sizeof(Inst) <= 112, "Inst operand storage unintentionally increased");
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -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
+94 -61
View File
@@ -1,6 +1,5 @@
#include "libs/audio.h"
#include <SDL3/SDL.h>
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
@@ -12,7 +11,9 @@
#include "libs/errno.h"
#include "libs/libs.h"
#include <SDL3/SDL.h>
#include <algorithm>
#include <array>
#include <cstring>
#include <limits>
#include <magic_enum.hpp>
@@ -37,6 +38,24 @@ constexpr uint64_t AUDIO_OUT_TARGET_LATENCY_US = 40000;
constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 0x1;
constexpr uint32_t AUDIO_IN_GRAIN_MAX_ASYNC = 384;
struct OutputClock {
uint64_t next_time = 0;
uint64_t remainder = 0;
uint64_t Advance(uint64_t now, uint32_t frames, uint32_t rate) {
// Preserve small scheduling errors; discard timing debt after a discontinuity.
if (next_time == 0 || (now > next_time && now - next_time > 100000)) {
next_time = now;
remainder = 0;
}
const auto deadline = next_time;
const auto duration = 1000000ULL * frames + remainder;
next_time += duration / rate;
remainder = duration % rate;
return deadline;
}
};
static bool audio_out_port_type_is_valid(int type) {
return (type >= AUDIO_OUT_PORT_TYPE_MAIN && type <= AUDIO_OUT_PORT_TYPE_PADSPK) ||
type == AUDIO_OUT_PORT_TYPE_VIBRATION || type == AUDIO_OUT_PORT_TYPE_AUDIO3D ||
@@ -98,17 +117,17 @@ public:
private:
struct PortOut {
bool used = false;
int type = 0;
uint32_t samples_num = 0;
uint32_t freq = 0;
Format format = Format::Unknown;
uint64_t last_output_time = 0;
bool queue_primed = false;
int channels_num = 0;
int volume[12] = {};
bool used = false;
int type = 0;
uint32_t samples_num = 0;
uint32_t freq = 0;
Format format = Format::Unknown;
OutputClock clock;
bool queue_primed = false;
int channels_num = 0;
int volume[12] = {};
SDL_AudioStream* stream = nullptr;
SDL_AudioStream* stream = nullptr;
Controller::DualSenseHaptics::Stream* haptics = nullptr;
};
@@ -376,10 +395,7 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
: 2u;
min_queued_size = prepared_size * std::clamp(buffers, 2u, 16u);
const auto wait_start = LibKernel::KernelGetProcessTime();
auto queued = SDL_GetAudioStreamQueued(port->stream);
if (queued < static_cast<int>(prepared_size)) {
port->queue_primed = false;
}
auto queued = SDL_GetAudioStreamQueued(port->stream);
while (queued > static_cast<int>(min_queued_size)) {
if (LibKernel::KernelGetProcessTime() - wait_start > 200000) {
SDL_ClearAudioStream(port->stream);
@@ -389,19 +405,20 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
Common::Thread::SleepMicro(1000);
queued = SDL_GetAudioStreamQueued(port->stream);
}
if (port->queue_primed) {
const auto next_time = port->last_output_time + buffer_us;
const auto now = LibKernel::KernelGetProcessTime();
if (next_time > now) {
Common::Thread::SleepMicro(next_time - now);
}
if (queued < static_cast<int>(prepared_size)) {
port->queue_primed = false;
}
}
if (!SDL_PutAudioStreamData(port->stream, prepared_data, static_cast<int>(prepared_size))) {
LOGF("AudioOut: SDL_PutAudioStreamData failed: %s\n", SDL_GetError());
port->queue_primed = false;
return false;
}
// Unpaced priming must not advance the playback deadline into the future.
if (!port->queue_primed) {
port->clock = {};
}
if (blocking && !port->queue_primed &&
SDL_GetAudioStreamQueued(port->stream) >= static_cast<int>(min_queued_size)) {
port->queue_primed = true;
@@ -411,18 +428,21 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
}
Audio::Id Audio::AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format) {
if (freq == 0) {
return Id::Invalid();
}
Common::LockGuard lock(m_mutex);
for (int id = 0; id < OUT_PORTS_MAX; id++) {
if (!m_out_ports[id].used) {
auto& port = m_out_ports[id];
port.used = true;
port.type = type;
port.samples_num = samples_num;
port.freq = freq;
port.format = format;
port.last_output_time = 0;
port.used = true;
port.type = type;
port.samples_num = samples_num;
port.freq = freq;
port.format = format;
switch (format) {
case Format::Signed16bitMono:
@@ -524,61 +544,68 @@ bool Audio::AudioOutSetVolume(Id handle, uint32_t bitflag, const int* volume) {
}
uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking) {
EXIT_NOT_IMPLEMENTED(num == 0);
EXIT_NOT_IMPLEMENTED(num == 0 || num > OUT_PORTS_MAX);
EXIT_NOT_IMPLEMENTED(!AudioOutValid(params[0].handle));
const auto& first_port = m_out_ports[params[0].handle.GetId()];
uint64_t block_time = (1000000 * first_port.samples_num) / first_port.freq;
uint64_t current_time = LibKernel::KernelGetProcessTime();
uint64_t max_wait_time = 0;
for (uint32_t i = 0; i < num; i++) {
uint64_t next_time = m_out_ports[params[i].handle.GetId()].last_output_time + block_time;
uint64_t wait_time = (next_time > current_time ? next_time - current_time : 0);
max_wait_time = (wait_time > max_wait_time ? wait_time : max_wait_time);
}
bool any_port_has_device = false;
for (uint32_t i = 0; i < num; i++) {
if (m_out_ports[params[i].handle.GetId()].stream != nullptr) {
any_port_has_device = true;
break;
}
}
// One real output device is enough to pace the whole synchronized batch. Applying the fallback
// when a vibration port is present would rate-limit the device-backed ports to exactly 1x and
// prevent their SDL queues from building an underrun cushion.
if (blocking && max_wait_time != 0 && !any_port_has_device) {
Common::Thread::SleepMicro(max_wait_time);
}
const auto& first_port = m_out_ports[params[0].handle.GetId()];
std::array<bool, OUT_PORTS_MAX> paced {};
bool any_device = false;
uint64_t deadline = 0;
for (uint32_t i = 0; i < num; i++) {
auto& port = m_out_ports[params[i].handle.GetId()];
uint64_t controller_queued_us = 0;
bool controller_uses_bluetooth = false;
if (port.haptics != nullptr) {
// Keep the stream alive against close and volume changes.
Common::LockGuard lock(m_mutex);
controller_queued_us = Controller::DualSenseHaptics::Queue(
port.haptics, Controller::GetActiveControllerId(), params[i].data, port.samples_num,
static_cast<uint32_t>(port.channels_num), FormatIsFloat(port.format), port.volume);
controller_uses_bluetooth =
Controller::DualSenseHaptics::UsesBluetooth(port.haptics);
}
if (controller_queued_us == 0) {
// No DualSense took it (e.g. it was unplugged); a pad speaker port plays on the main
// output instead, and a vibration port has none.
QueueSdlAudio(&port, params[i].data, blocking);
} else if (blocking && port.type == AUDIO_OUT_PORT_TYPE_PADSPK &&
controller_queued_us > AUDIO_OUT_TARGET_LATENCY_US) {
// Vibration never paces output; the speaker is audible, so pace it like other ports.
Common::Thread::SleepMicro(controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US);
if (QueueSdlAudio(&port, params[i].data, blocking)) {
any_device = true;
paced[i] = port.queue_primed;
}
} else {
port.queue_primed = false;
// Bluetooth HID has its own sender. Let the sample clock pace a batch
// without another audio device, instead of waiting on the HID queue.
if (port.type == AUDIO_OUT_PORT_TYPE_PADSPK && !controller_uses_bluetooth) {
any_device = true;
if (blocking && controller_queued_us > AUDIO_OUT_TARGET_LATENCY_US) {
// Limit a stalled USB endpoint to one audio block of waiting.
const uint64_t block_us = 1000000ULL * port.samples_num / port.freq;
deadline = std::max(deadline, LibKernel::KernelGetProcessTime() +
std::min(controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US,
block_us));
}
}
}
}
// Submit the whole batch before pacing it. Audible devices own its cadence while
// priming; silent/vibration-only batches use the same sample clock as SDL output.
const auto now = LibKernel::KernelGetProcessTime();
for (uint32_t i = 0; i < num; i++) {
m_out_ports[params[i].handle.GetId()].last_output_time = LibKernel::KernelGetProcessTime();
auto& port = m_out_ports[params[i].handle.GetId()];
if (blocking && (paced[i] || !any_device)) {
deadline = std::max(deadline, port.clock.Advance(now, port.samples_num, port.freq));
} else {
port.clock = {};
}
if (!blocking) {
port.queue_primed = false;
}
}
if (deadline > now) {
Common::Thread::SleepMicro(deadline - now);
}
return first_port.samples_num;
@@ -826,6 +853,9 @@ int KYTY_SYSV_ABI AudioOutOpen(int user_id, int type, int index, uint32_t len, u
if (!audio_out_port_type_is_valid(type)) {
return AUDIO_OUT_ERROR_INVALID_PORT_TYPE;
}
if (freq == 0) {
return AUDIO_OUT_ERROR_INVALID_SAMPLE_FREQ;
}
EXIT_NOT_IMPLEMENTED(index != 0);
Audio::Format format = Audio::Format::Unknown;
@@ -935,6 +965,9 @@ int KYTY_SYSV_ABI AudioOutOutputs(AudioOutOutputParam* param, uint32_t num) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(param == nullptr);
if (num == 0 || num > Audio::OUT_PORTS_MAX) {
return AUDIO_OUT_ERROR_INVALID_SIZE;
}
Audio::OutputParam params[Audio::OUT_PORTS_MAX];
+467
View File
@@ -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
+14
View File
@@ -0,0 +1,14 @@
#ifndef KYTY_LIBS_DUALSENSE_BLUETOOTH_H_
#define KYTY_LIBS_DUALSENSE_BLUETOOTH_H_
#include <cstdint>
namespace Libs::Controller::DualSenseBluetooth {
struct Stream;
Stream* Open(uint32_t frequency, bool speaker, int controller, void (*tick)());
uint64_t Queue(Stream* stream, const float* samples, uint32_t frames);
void Close(Stream* stream);
void Shutdown();
} // namespace Libs::Controller::DualSenseBluetooth
#endif
+140 -58
View File
@@ -2,6 +2,7 @@
#include "common/logging/log.h"
#include "common/threads.h"
#include "libs/dualSenseBluetooth.h"
#include <SDL3/SDL.h>
#include <algorithm>
@@ -13,18 +14,20 @@
namespace Libs::Controller::DualSenseHaptics {
struct Stream {
uint32_t freq = 0;
bool speaker = false;
int controller = -1;
SDL_AudioStream* sdl = nullptr;
SDL_AudioDeviceID device = 0;
uint64_t next_check = 0;
std::vector<float> buffer;
uint32_t freq = 0;
bool speaker = false;
int controller = -1;
SDL_AudioStream* sdl = nullptr;
DualSenseBluetooth::Stream* bluetooth = nullptr;
SDL_AudioDeviceID device = 0;
uint64_t next_check = 0;
std::vector<float> buffer;
};
namespace {
constexpr int FRAME_BYTES = 4 * sizeof(float);
constexpr int FRAME_BYTES = 4 * sizeof(float);
constexpr uint32_t MAX_QUEUE_MS = 80;
Common::Mutex g_mutex;
int g_controller = -1;
int g_haptics_streams = 0;
@@ -107,11 +110,22 @@ void SDLCALL UpdateRumble(void*, SDL_AudioStream*, int, int) {
}
}
void UpdateBluetoothRumble() {
UpdateRumble(nullptr, nullptr, 0, 0);
}
void CloseDevice(Stream* stream) {
const bool had_device = stream->sdl != nullptr || stream->bluetooth != nullptr;
if (stream->bluetooth != nullptr) {
DualSenseBluetooth::Close(stream->bluetooth);
stream->bluetooth = nullptr;
}
if (stream->sdl != nullptr) {
// Destroy outside g_mutex: SDL waits for any running get callback.
SDL_DestroyAudioStream(stream->sdl);
stream->sdl = nullptr;
}
if (had_device) {
Common::LockGuard lock(g_mutex);
if (!stream->speaker && --g_haptics_streams == 0 && g_haptics_until != 0) {
g_haptics_until = 0;
@@ -127,26 +141,34 @@ void CloseDevice(Stream* stream) {
}
}
bool IsWireless(int controller) {
SDL_LockJoysticks();
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller));
const bool wireless =
pad != nullptr && SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRELESS;
SDL_UnlockJoysticks();
return wireless;
}
bool CanUseDevice(int controller) {
// SDL exposes no portable association between a gamepad and its USB audio endpoint.
// Require one wired DualSense; guessing with multiple pads could play on the wrong one.
SDL_LockJoysticks();
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller));
bool usable =
pad != nullptr && SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRED;
int count = 0;
auto* pads = SDL_GetGamepads(&count);
int dualsense_count = 0;
bool found = false;
int count = 0;
auto* pads = SDL_GetGamepads(&count);
int wired_dualsense_count = 0;
bool found = false;
for (int i = 0; i < count; i++) {
if (SDL_GetGamepadTypeForID(pads[i]) == SDL_GAMEPAD_TYPE_PS5) {
dualsense_count++;
auto* candidate = SDL_GetGamepadFromID(pads[i]);
if (candidate != nullptr && SDL_GetGamepadTypeForID(pads[i]) == SDL_GAMEPAD_TYPE_PS5 &&
SDL_GetGamepadConnectionState(candidate) == SDL_JOYSTICK_CONNECTION_WIRED) {
wired_dualsense_count++;
found |= pads[i] == static_cast<SDL_JoystickID>(controller);
}
}
SDL_free(pads);
SDL_UnlockJoysticks();
return usable && found && dualsense_count == 1;
return found && wired_dualsense_count == 1;
}
SDL_AudioDeviceID FindDevice() {
@@ -169,32 +191,42 @@ SDL_AudioDeviceID FindDevice() {
return device;
}
void RefreshDevice(Stream* stream, int controller) {
if (stream->controller != controller) {
void RefreshDevice(Stream* stream, int controller, bool wireless) {
if (stream->controller != controller || (wireless && stream->sdl != nullptr) ||
(!wireless && stream->bluetooth != nullptr)) {
CloseDevice(stream);
stream->controller = controller;
stream->next_check = 0;
}
const auto now = SDL_GetTicks();
if (now < stream->next_check) {
if (stream->bluetooth != nullptr || now < stream->next_check) {
return;
}
stream->next_check = now + 2000;
const auto device = FindDevice();
const auto device = wireless ? 0 : FindDevice();
if (stream->device == device && stream->sdl != nullptr) {
return;
}
CloseDevice(stream);
stream->device = device;
if (device == 0) {
if (!wireless && device == 0) {
return;
}
const SDL_AudioSpec desired {SDL_AUDIO_F32, 4, static_cast<int>(stream->freq)};
auto* sdl = SDL_OpenAudioDeviceStream(device, &desired, UpdateRumble, nullptr);
SDL_AudioSpec actual {};
bool ready = sdl != nullptr &&
SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl), &actual, nullptr) &&
actual.channels == 4 && SDL_ResumeAudioStreamDevice(sdl);
SDL_AudioStream* sdl = nullptr;
DualSenseBluetooth::Stream* bluetooth = nullptr;
bool ready;
if (wireless) {
bluetooth = DualSenseBluetooth::Open(stream->freq, stream->speaker, controller,
UpdateBluetoothRumble);
ready = bluetooth != nullptr;
} else {
const SDL_AudioSpec desired {SDL_AUDIO_F32, 4, static_cast<int>(stream->freq)};
SDL_AudioSpec actual {};
sdl = SDL_OpenAudioDeviceStream(device, &desired, UpdateRumble, nullptr);
ready = sdl != nullptr &&
SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl), &actual, nullptr) &&
actual.channels == 4 && SDL_ResumeAudioStreamDevice(sdl);
}
if (ready) {
Common::LockGuard lock(g_mutex);
if (stream->speaker) {
@@ -207,14 +239,56 @@ void RefreshDevice(Stream* stream, int controller) {
}
}
if (ready) {
stream->sdl = sdl;
LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device));
stream->sdl = sdl;
stream->bluetooth = bluetooth;
if (!wireless) {
LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device));
}
} else {
DualSenseBluetooth::Close(bluetooth);
SDL_DestroyAudioStream(sdl);
LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError());
if (!wireless) {
LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError());
}
}
}
uint64_t QueueUsbAudio(Stream* stream, uint32_t frames) {
auto queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl));
const int bytes = static_cast<int>(frames * FRAME_BYTES);
const int max_bytes =
static_cast<int>(static_cast<uint64_t>(stream->freq) * FRAME_BYTES * MAX_QUEUE_MS / 1000);
if (queued + bytes > max_bytes) {
// Keep the newest sound after a brief device stall. Clearing the entire
// stream produces a gap that is much longer than one audio block.
SDL_AudioSpec target {};
std::array<uint8_t, 16384> discarded {};
if (SDL_GetAudioStreamFormat(stream->sdl, nullptr, &target)) {
const int frame_bytes = SDL_AUDIO_FRAMESIZE(target);
if (frame_bytes > 0) {
const int chunk_frames =
std::min<int>(static_cast<int>(discarded.size()) / frame_bytes,
static_cast<int>((static_cast<uint64_t>(frames) * target.freq +
stream->freq - 1) /
stream->freq));
while (queued + bytes > max_bytes && chunk_frames > 0 &&
SDL_GetAudioStreamData(stream->sdl, discarded.data(),
chunk_frames * frame_bytes) > 0) {
queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl));
}
}
}
if (queued + bytes > max_bytes) {
SDL_ClearAudioStream(stream->sdl);
queued = 0;
}
}
if (!SDL_PutAudioStreamData(stream->sdl, stream->buffer.data(), bytes)) {
return 0;
}
return static_cast<uint64_t>(queued + bytes) * 1000000 / (stream->freq * FRAME_BYTES);
}
} // namespace
Stream* Open(uint32_t freq, bool speaker) {
@@ -232,18 +306,30 @@ void Close(Stream* stream) {
}
}
bool UsesBluetooth(const Stream* stream) {
return stream != nullptr && stream->bluetooth != nullptr;
}
uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume) {
if (stream == nullptr || data == nullptr || frames == 0 || channels == 0 || volume == nullptr) {
return 0;
}
if (!CanUseDevice(controller)) {
const auto max_frames =
static_cast<uint32_t>(static_cast<uint64_t>(stream->freq) * MAX_QUEUE_MS / 1000);
const auto first_frame = frames > max_frames ? frames - max_frames : 0;
frames -= first_frame;
if (frames == 0) {
return 0;
}
const bool wireless = IsWireless(controller);
if (!wireless && !CanUseDevice(controller)) {
CloseDevice(stream);
stream->next_check = 0; // Look for the device as soon as a DualSense is active again.
return 0;
}
RefreshDevice(stream, controller);
if (stream->sdl == nullptr) {
RefreshDevice(stream, controller, wireless);
if (stream->sdl == nullptr && stream->bluetooth == nullptr) {
return 0;
}
if (stream->speaker) {
@@ -257,47 +343,42 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames
return 0;
}
}
stream->buffer.assign(static_cast<size_t>(frames) * 4, 0.0f);
const uint32_t output_channels = wireless ? 2 : 4;
const uint32_t channel_offset = wireless || stream->speaker ? 0 : 2;
stream->buffer.assign(static_cast<size_t>(frames) * output_channels, 0.0f);
bool audible = false;
for (uint32_t frame = 0; frame < frames; frame++) {
for (uint32_t ch = 0; ch < 2; ch++) {
const auto src_ch = channels == 1 ? 0 : ch;
const auto index = static_cast<size_t>(frame) * channels + src_ch;
const auto index = (static_cast<size_t>(first_frame) + frame) * channels + src_ch;
float value = is_float ? static_cast<const float*>(data)[index]
: static_cast<const int16_t*>(data)[index] / 32768.0f;
value *= volume[src_ch] / 32768.0f;
// The speaker plays the right front channel; the actuators play the back ones.
stream->buffer[static_cast<size_t>(frame) * 4 + ch + (stream->speaker ? 0 : 2)] = value;
audible |= std::fabs(value) > 1.0f / 1024;
// USB places the speaker in front and the actuators in back; Bluetooth takes stereo.
stream->buffer[static_cast<size_t>(frame) * output_channels + ch + channel_offset] =
value;
audible |= std::isfinite(value) && std::fabs(value) > 1.0f / 1024;
}
}
auto queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl));
if (queued > static_cast<int>(stream->freq * FRAME_BYTES * 80 / 1000)) {
SDL_ClearAudioStream(stream->sdl);
queued = 0;
}
const int bytes = static_cast<int>(frames * FRAME_BYTES);
const auto queued_us =
static_cast<uint64_t>(queued + bytes) * 1000000 / (stream->freq * FRAME_BYTES);
wireless ? DualSenseBluetooth::Queue(stream->bluetooth, stream->buffer.data(), frames)
: QueueUsbAudio(stream, frames);
if (queued_us == 0) {
CloseDevice(stream);
stream->next_check = SDL_GetTicks() + 2000;
return 0;
}
if (audible && !stream->speaker) {
Common::LockGuard lock(g_mutex);
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) !=
SDL_GAMEPAD_TYPE_PS5) {
return 0;
}
SelectController(controller);
const auto playback_now = SDL_GetTicks();
const bool starting = g_haptics_until <= playback_now;
const auto now = SDL_GetTicks();
const bool starting = g_haptics_until <= now;
g_haptics_until =
std::max(g_haptics_until, playback_now + std::max<uint64_t>(queued_us / 1000, 250));
std::max(g_haptics_until, now + std::max<uint64_t>(queued_us / 1000, 250));
if (starting) {
ApplyRumble();
}
}
if (!SDL_PutAudioStreamData(stream->sdl, stream->buffer.data(), bytes)) {
CloseDevice(stream);
return 0;
}
return queued_us;
}
@@ -315,6 +396,7 @@ bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) {
}
void Shutdown() {
DualSenseBluetooth::Shutdown();
Common::LockGuard lock(g_mutex);
SelectController(-1);
SelectSpeaker(-1);
+4 -2
View File
@@ -11,9 +11,11 @@ struct Stream;
// ports on the actuators.
Stream* Open(uint32_t freq, bool speaker);
void Close(Stream* stream);
// True while this stream is routed through the asynchronous Bluetooth HID sender.
bool UsesBluetooth(const Stream* stream);
// Returns how long the queued audio still plays, in microseconds, or 0 when no DualSense took it
// (the active pad has no usable USB audio device or its speaker cannot be routed).
// Multiple DualSenses or matching audio endpoints are ambiguous and are not selected.
// (the active pad has no usable controller audio device or its speaker cannot be routed).
// Ambiguous USB or Bluetooth endpoints are not selected.
uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume);
// Returns false for other gamepad types, which retain the normal rumble path.
+7
View File
@@ -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);
+401
View File
@@ -0,0 +1,401 @@
// Exercise production output scheduling with a deterministic clock and consuming SDL queues.
#include "common/threads.h"
#include "libs/dualSenseHaptics.h"
#include <SDL3/SDL.h>
#include <algorithm>
#include <array>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <vector>
namespace {
void Check(bool condition, const char* message) {
if (!condition) {
std::fprintf(stderr, "AudioOutTimingTests: %s\n", message);
std::abort();
}
}
struct Stream {
SDL_AudioSpec spec;
uint64_t frames = 0; // Millionths of a frame retain exact simulated consumption.
uint64_t updated;
};
uint64_t now = 1000000;
uint32_t oversleep = 0, processing = 0;
std::vector<uint32_t> sleeps;
std::vector<std::unique_ptr<Stream>> streams;
bool fail_open = false, fail_put = false, stalled = false;
bool pad_connected = false, pad_bluetooth = false;
uint64_t pad_queue_us = 0;
int clears = 0;
Stream& GetStream(SDL_AudioStream* stream) {
return *reinterpret_cast<Stream*>(stream);
}
void Drain(Stream& stream) {
if (!stalled) {
const auto consumed = (now - stream.updated) * stream.spec.freq;
stream.frames -= std::min(stream.frames, consumed);
}
stream.updated = now;
}
} // namespace
namespace Fake {
bool InitSubSystem(SDL_InitFlags) {
return true;
}
void QuitSubSystem(SDL_InitFlags) {}
bool ResumeAudioStreamDevice(SDL_AudioStream*) {
return true;
}
SDL_AudioStream* OpenAudioDeviceStream(SDL_AudioDeviceID, const SDL_AudioSpec* spec,
SDL_AudioStreamCallback, void*) {
if (fail_open) {
return nullptr;
}
streams.push_back(std::make_unique<Stream>(Stream {*spec, 0, now}));
return reinterpret_cast<SDL_AudioStream*>(streams.back().get());
}
void DestroyAudioStream(SDL_AudioStream* stream) {
const auto it = std::find_if(streams.begin(), streams.end(), [stream](const auto& value) {
return value.get() == &GetStream(stream);
});
Check(it != streams.end(), "unknown stream destroyed");
streams.erase(it);
}
int GetAudioStreamQueued(SDL_AudioStream* stream) {
auto& state = GetStream(stream);
Drain(state);
return static_cast<int>(state.frames / 1000000 * SDL_AUDIO_BYTESIZE(state.spec.format) *
state.spec.channels);
}
bool PutAudioStreamData(SDL_AudioStream* stream, const void*, int bytes) {
now += processing;
if (fail_put) {
return false;
}
auto& state = GetStream(stream);
Drain(state);
state.frames += static_cast<uint64_t>(bytes) * 1000000 /
(SDL_AUDIO_BYTESIZE(state.spec.format) * state.spec.channels);
return true;
}
bool ClearAudioStream(SDL_AudioStream* stream) {
auto& state = GetStream(stream);
state.frames = 0;
state.updated = now;
clears++;
return true;
}
} // namespace Fake
namespace Common {
class TimingThread: public Thread {
public:
static void SleepMicro(uint32_t micros) {
sleeps.push_back(micros);
now += micros + oversleep;
}
};
} // namespace Common
#define Thread TimingThread
#define SDL_InitSubSystem Fake::InitSubSystem
#define SDL_QuitSubSystem Fake::QuitSubSystem
#define SDL_OpenAudioDeviceStream Fake::OpenAudioDeviceStream
#define SDL_ResumeAudioStreamDevice Fake::ResumeAudioStreamDevice
#define SDL_DestroyAudioStream Fake::DestroyAudioStream
#define SDL_GetAudioStreamQueued Fake::GetAudioStreamQueued
#define SDL_PutAudioStreamData Fake::PutAudioStreamData
#define SDL_ClearAudioStream Fake::ClearAudioStream
#include "libs/audio.cpp"
#undef Thread
#undef SDL_InitSubSystem
#undef SDL_QuitSubSystem
#undef SDL_OpenAudioDeviceStream
#undef SDL_ResumeAudioStreamDevice
#undef SDL_DestroyAudioStream
#undef SDL_GetAudioStreamQueued
#undef SDL_PutAudioStreamData
#undef SDL_ClearAudioStream
namespace Libs::Controller {
int GetActiveControllerId() {
return 0;
}
namespace DualSenseHaptics {
Stream* Open(uint32_t, bool) {
static int pad_stream;
return pad_connected ? reinterpret_cast<Stream*>(&pad_stream) : nullptr;
}
void Close(Stream*) {}
bool UsesBluetooth(const Stream* stream) {
return stream != nullptr && pad_bluetooth;
}
uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*) {
return stream != nullptr ? pad_queue_us : 0;
}
} // namespace DualSenseHaptics
} // namespace Libs::Controller
namespace Libs::LibKernel {
uint64_t KYTY_SYSV_ABI KernelGetProcessTime() {
return now;
}
} // namespace Libs::LibKernel
namespace Loader::Timer {
double GetTimeMs() {
return static_cast<double>(now) / 1000;
}
} // namespace Loader::Timer
namespace {
using Audio = Libs::Audio::Audio;
struct Fixture {
Audio audio;
std::array<float, 2048> pcm {};
Fixture() {
Check(streams.empty(), "stream leaked between tests");
now = 1000000;
processing = oversleep = 0;
clears = 0;
fail_open = fail_put = stalled = false;
pad_connected = pad_bluetooth = false;
pad_queue_us = 0;
sleeps.clear();
}
Audio::Id Open(uint32_t frames = 256, int type = 0) {
const auto id = audio.AudioOutOpen(type, frames, 48000, Audio::Format::FloatStereo);
Check(id.IsValid(), "port open failed");
return id;
}
void Output(Audio::Id port, bool blocking = true) {
Audio::OutputParam param {port, pcm.data()};
audio.AudioOutOutputs(&param, 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(&param, 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");
}
+36 -19
View File
@@ -238,14 +238,31 @@ struct TrackerHarness {
MemoryTracker tracker;
};
uint8_t *AllocateFixedGuestRange(uint64_t size, uintptr_t offset) {
// Keep the mapping inside the tracker's guest address space and preserve the
// requested region alignment. A fixed address can be occupied by the host
// process (notably by the macOS runner's ASLR layout).
constexpr uintptr_t first_base = 0x0000000200000000ull;
constexpr uintptr_t stride = 0x0000000100000000ull;
for (uintptr_t attempt = 0; attempt < 256; attempt++) {
auto *wanted = reinterpret_cast<void *>(first_base + offset + attempt * stride);
auto *memory = static_cast<uint8_t *>(
VirtualAlloc(wanted, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
if (memory == wanted) {
return memory;
}
if (memory != nullptr) {
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0,
"release unexpected fixed allocation failed");
}
}
Check(false, "no free fixed guest address found");
return nullptr;
}
uint8_t *Allocate(PageManager &manager, uint64_t pages) {
constexpr uintptr_t base = 0x0000000200010000ull;
const auto size = manager.GetPageSize() * pages;
auto *memory = static_cast<uint8_t *>(
VirtualAlloc(reinterpret_cast<void *>(base), size,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
Check(memory == reinterpret_cast<void *>(base), "fixed VirtualAlloc failed");
return memory;
return AllocateFixedGuestRange(size, 0x10000);
}
void Release(uint8_t *memory) {
@@ -386,16 +403,10 @@ void TestCpuDirtyUpload() {
}
void TestRangeInvalidation() {
constexpr uintptr_t base = 0x0000000201000000ull;
TrackerHarness harness;
auto &tracker = harness.tracker;
auto &page_manager = harness.page_manager;
constexpr uint64_t size = Libs::Graphics::TRACKER_REGION_SIZE * 2;
auto *memory = static_cast<uint8_t *>(
VirtualAlloc(reinterpret_cast<void *>(base), size,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
Check(memory == reinterpret_cast<void *>(base),
"range invalidation allocation failed");
auto *memory = AllocateFixedGuestRange(size, 0x1000000);
const auto address = reinterpret_cast<uint64_t>(memory);
tracker.ForEachUploadRange(
@@ -614,16 +625,12 @@ void TestGpuDownloadProtectionMirrors() {
}
void TestCrossRegionUpload() {
constexpr uintptr_t base = 0x0000000200010000ull;
constexpr uint64_t region_size = 4ull * 1024ull * 1024ull;
TrackerHarness harness;
auto &tracker = harness.tracker;
auto &page_manager = harness.page_manager;
const auto page_size = page_manager.GetPageSize();
auto *memory = static_cast<uint8_t *>(
VirtualAlloc(reinterpret_cast<void *>(base), region_size * 2,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
Check(memory == reinterpret_cast<void *>(base), "fixed VirtualAlloc failed");
auto *memory = AllocateFixedGuestRange(region_size * 2, 0x10000);
const auto address = reinterpret_cast<uint64_t>(memory);
const auto boundary = (address + region_size - 1) & ~(region_size - 1);
uint32_t ranges = 0;
@@ -871,6 +878,15 @@ void TestFullRegionGpuUnmarkBatching() {
[&]() noexcept {
(void)tracker.IsRegionCpuModified(address, page_size);
});
} else if (std::strcmp(name, "recursive-tracking-lock") == 0) {
Libs::Graphics::TrackingSpinLock lock;
lock.lock();
lock.lock();
} else if (std::strcmp(name, "non-owner-tracking-unlock") == 0) {
Libs::Graphics::TrackingSpinLock lock;
lock.lock();
std::thread worker([&] { lock.unlock(); });
worker.join();
}
std::_Exit(0x7f);
}
@@ -928,7 +944,8 @@ void CheckDeathCase(const char *name) {
}
void TestFatalPaths() {
for (const char *name : {"gpu-dirty-explicit-cpu", "reentrant-upload"}) {
for (const char *name : {"gpu-dirty-explicit-cpu", "reentrant-upload",
"recursive-tracking-lock", "non-owner-tracking-unlock"}) {
CheckDeathCase(name);
}
}
+638 -38
View File
@@ -1,11 +1,17 @@
// Exercise the production module with real SDL streams and fake USB endpoints.
#include <SDL3/SDL.h>
#include <opus.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <numeric>
#include <semaphore>
#include <thread>
#include <vector>
namespace {
@@ -24,23 +30,32 @@ struct Rumble {
Uint16 large, small;
Uint32 duration;
};
std::vector<Device> devices;
std::vector<SDL_AudioStream*> streams;
Rumble rumble {};
Uint64 now = 1000;
int audio_refs = 0, opens = 0, rumble_calls = 0;
bool fail_open = false, fail_resume = false;
int actual_channels = 4;
SDL_AudioDeviceID opened_device = 0;
SDL_AudioSpec opened_spec {};
std::array<Rumble, 4> pad_rumble {};
std::array<std::vector<uint8_t>, 4> effects;
std::vector<SDL_JoystickID> connected_pads {1};
std::array<Rumble, 4> cached_rumble {};
bool fail_effect = false, wireless = false;
SDL_AudioStream* default_stream = nullptr;
bool fail_default_resume = false;
int active_controller = 1;
std::vector<Device> devices;
std::vector<SDL_AudioStream*> streams;
Rumble rumble {};
Uint64 now = 1000;
int audio_refs = 0, opens = 0, rumble_calls = 0;
bool fail_open = false, fail_resume = false;
int actual_channels = 4;
SDL_AudioDeviceID opened_device = 0;
SDL_AudioSpec opened_spec {};
std::array<Rumble, 4> pad_rumble {};
std::array<std::vector<uint8_t>, 4> effects;
std::vector<SDL_JoystickID> connected_pads {1};
std::array<Rumble, 4> cached_rumble {};
bool fail_effect = false, wireless = false, wireless_pad3 = false;
bool hid_available = false, fail_hid_write = false;
bool duplicate_hid = false;
int hid_opens = 0;
int opened_hid_pad = 0;
std::vector<std::array<uint8_t, 547>> hid_reports;
std::atomic_bool block_opus_encode {false}, block_hid_write {false};
std::atomic_int hid_closes {0};
std::binary_semaphore encode_started {0}, allow_encode {0};
std::binary_semaphore write_started {0}, allow_write {0};
SDL_AudioStream* default_stream = nullptr;
bool fail_default_resume = false;
int active_controller = 1;
} // namespace
namespace Fake {
@@ -116,8 +131,9 @@ void DestroyAudioStream(SDL_AudioStream* stream) {
return;
}
const auto it = std::find(streams.begin(), streams.end(), stream);
Check(it != streams.end(), "stream destroyed twice");
streams.erase(it);
if (it != streams.end()) {
streams.erase(it);
}
if (stream == default_stream) {
default_stream = nullptr;
}
@@ -134,6 +150,73 @@ SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
return id == 1 || id == 3 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
: nullptr;
}
SDL_GamepadType GetGamepadType(SDL_Gamepad* pad) {
return GetGamepadTypeForID(static_cast<SDL_JoystickID>(reinterpret_cast<uintptr_t>(pad)));
}
Uint16 GetGamepadVendor(SDL_Gamepad*) {
return 0x054c;
}
Uint16 GetGamepadProduct(SDL_Gamepad*) {
return 0x0ce6;
}
const char* GetGamepadSerial(SDL_Gamepad* pad) {
return reinterpret_cast<uintptr_t>(pad) == 3 ? "aa-bb-cc-dd-ee-03" : "aa-bb-cc-dd-ee-01";
}
Uint64 GetTicksNS() {
return now * 1000000;
}
SDL_Thread* CreateThread(SDL_ThreadFunction, const char*, void*) {
return reinterpret_cast<SDL_Thread*>(1);
}
void WaitThread(SDL_Thread*, int*) {}
SDL_hid_device_info* HidEnumerate(unsigned short, unsigned short) {
if (!hid_available) {
return nullptr;
}
static wchar_t serial1[] = L"AA:BB:CC:DD:EE:01";
static wchar_t serial3[] = L"AA:BB:CC:DD:EE:03";
static char path1[] = "pad-1";
static char path3[] = "pad-3";
static SDL_hid_device_info pad3 {}, pad1 {};
pad3.path = path3;
pad3.serial_number = duplicate_hid ? serial1 : serial3;
pad3.bus_type = SDL_HID_API_BUS_BLUETOOTH;
pad1.path = path1;
pad1.serial_number = serial1;
pad1.bus_type = SDL_HID_API_BUS_BLUETOOTH;
pad1.next = &pad3;
return &pad1;
}
void HidFreeEnumeration(SDL_hid_device_info*) {}
SDL_hid_device* HidOpenPath(const char* path) {
hid_opens++;
opened_hid_pad = path[4] == '1' ? 1 : 3;
return reinterpret_cast<SDL_hid_device*>(static_cast<uintptr_t>(opened_hid_pad));
}
void HidClose(SDL_hid_device*) {
hid_closes++;
}
int HidWrite(SDL_hid_device*, const unsigned char* data, size_t size) {
if (block_hid_write.exchange(false)) {
write_started.release();
allow_write.acquire();
}
if (fail_hid_write || size != 547) {
return -1;
}
std::array<uint8_t, 547> report {};
std::copy_n(data, report.size(), report.begin());
hid_reports.push_back(report);
return static_cast<int>(size);
}
opus_int32 OpusEncodeFloat(OpusEncoder* encoder, const float* pcm, int frames,
unsigned char* packet, opus_int32 bytes) {
if (block_opus_encode.exchange(false)) {
encode_started.release();
allow_encode.acquire();
}
return ::opus_encode_float(encoder, pcm, frames, packet, bytes);
}
SDL_JoystickID* GetGamepads(int* count) {
*count = static_cast<int>(connected_pads.size());
auto* ids = static_cast<SDL_JoystickID*>(
@@ -142,8 +225,10 @@ SDL_JoystickID* GetGamepads(int* count) {
ids[connected_pads.size()] = 0;
return ids;
}
SDL_JoystickConnectionState GetGamepadConnectionState(SDL_Gamepad*) {
return wireless ? SDL_JOYSTICK_CONNECTION_WIRELESS : SDL_JOYSTICK_CONNECTION_WIRED;
SDL_JoystickConnectionState GetGamepadConnectionState(SDL_Gamepad* pad) {
return wireless || (wireless_pad3 && reinterpret_cast<uintptr_t>(pad) == 3)
? SDL_JOYSTICK_CONNECTION_WIRELESS
: SDL_JOYSTICK_CONNECTION_WIRED;
}
bool RumbleGamepad(SDL_Gamepad* pad, Uint16 large, Uint16 small, Uint32 duration) {
const auto id = reinterpret_cast<uintptr_t>(pad);
@@ -173,22 +258,37 @@ bool SendGamepadEffect(SDL_Gamepad* pad, const void* data, int size) {
}
} // namespace Fake
#define SDL_GetTicks Fake::GetTicks
#define SDL_InitSubSystem Fake::InitSubSystem
#define SDL_QuitSubSystem Fake::QuitSubSystem
#define SDL_GetAudioPlaybackDevices Fake::GetAudioPlaybackDevices
#define SDL_GetAudioDeviceName Fake::GetAudioDeviceName
#define SDL_GetAudioDeviceFormat Fake::GetAudioDeviceFormat
#define SDL_OpenAudioDeviceStream Fake::OpenAudioDeviceStream
#define SDL_ResumeAudioStreamDevice Fake::ResumeAudioStreamDevice
#define SDL_DestroyAudioStream Fake::DestroyAudioStream
#define SDL_GetAudioStreamDevice Fake::GetAudioStreamDevice
#define SDL_GetGamepadTypeForID Fake::GetGamepadTypeForID
#define SDL_GetTicks Fake::GetTicks
#define SDL_InitSubSystem Fake::InitSubSystem
#define SDL_QuitSubSystem Fake::QuitSubSystem
#define SDL_GetAudioPlaybackDevices Fake::GetAudioPlaybackDevices
#define SDL_GetAudioDeviceName Fake::GetAudioDeviceName
#define SDL_GetAudioDeviceFormat Fake::GetAudioDeviceFormat
#define SDL_OpenAudioDeviceStream Fake::OpenAudioDeviceStream
#define SDL_ResumeAudioStreamDevice Fake::ResumeAudioStreamDevice
#define SDL_DestroyAudioStream Fake::DestroyAudioStream
#define SDL_GetAudioStreamDevice Fake::GetAudioStreamDevice
#define SDL_GetGamepadTypeForID Fake::GetGamepadTypeForID
#define SDL_GetGamepadType Fake::GetGamepadType
#define SDL_GetGamepadVendor Fake::GetGamepadVendor
#define SDL_GetGamepadProduct Fake::GetGamepadProduct
#define SDL_GetGamepadSerial Fake::GetGamepadSerial
#define SDL_GetTicksNS Fake::GetTicksNS
#undef SDL_CreateThread
#define SDL_CreateThread Fake::CreateThread
#define SDL_WaitThread Fake::WaitThread
#define SDL_hid_enumerate Fake::HidEnumerate
#define SDL_hid_free_enumeration Fake::HidFreeEnumeration
#define SDL_hid_open_path Fake::HidOpenPath
#define SDL_hid_close Fake::HidClose
#define SDL_hid_write Fake::HidWrite
#define opus_encode_float Fake::OpusEncodeFloat
#define SDL_GetGamepadFromID Fake::GetGamepadFromID
#define SDL_RumbleGamepad Fake::RumbleGamepad
#define SDL_SendGamepadEffect Fake::SendGamepadEffect
#define SDL_GetGamepads Fake::GetGamepads
#define SDL_GetGamepadConnectionState Fake::GetGamepadConnectionState
#include "libs/dualSenseBluetooth.cpp"
#include "libs/dualSenseHaptics.cpp"
#include "libs/audio.cpp"
#undef SDL_GetTicks
@@ -202,6 +302,19 @@ bool SendGamepadEffect(SDL_Gamepad* pad, const void* data, int size) {
#undef SDL_DestroyAudioStream
#undef SDL_GetAudioStreamDevice
#undef SDL_GetGamepadTypeForID
#undef SDL_GetGamepadType
#undef SDL_GetGamepadVendor
#undef SDL_GetGamepadProduct
#undef SDL_GetGamepadSerial
#undef SDL_GetTicksNS
#undef SDL_CreateThread
#undef SDL_WaitThread
#undef SDL_hid_enumerate
#undef SDL_hid_free_enumeration
#undef SDL_hid_open_path
#undef SDL_hid_close
#undef SDL_hid_write
#undef opus_encode_float
#undef SDL_GetGamepadFromID
#undef SDL_RumbleGamepad
#undef SDL_SendGamepadEffect
@@ -228,6 +341,7 @@ double GetTimeMs() {
namespace {
namespace Haptics = Libs::Controller::DualSenseHaptics;
namespace Bluetooth = Libs::Controller::DualSenseBluetooth;
using Port = std::unique_ptr<Haptics::Stream, decltype(&Haptics::Close)>;
constexpr std::array<int, 2> unity {32768, 32768};
constexpr std::array<float, 4> pcm {0.5f, 0.5f, 0.5f, 0.5f};
@@ -240,11 +354,17 @@ struct Fixture {
cached_rumble = {};
effects = {};
connected_pads = {1};
fail_effect = wireless = false;
default_stream = nullptr;
fail_effect = wireless = wireless_pad3 = false;
hid_available = fail_hid_write = false;
duplicate_hid = false;
hid_opens = 0;
hid_closes = 0;
opened_hid_pad = 0;
hid_reports.clear();
default_stream = nullptr;
fail_default_resume = false;
active_controller = 1;
now = 1000;
active_controller = 1;
now = 1000;
opens = rumble_calls = 0;
fail_open = fail_resume = false;
actual_channels = 4;
@@ -255,8 +375,8 @@ struct Fixture {
}
};
Port Open(bool speaker = false) {
auto* stream = Haptics::Open(48000, speaker);
Port Open(bool speaker = false, uint32_t freq = 48000) {
auto* stream = Haptics::Open(freq, speaker);
Check(stream != nullptr, "port open failed");
return Port(stream, Haptics::Close);
}
@@ -516,6 +636,446 @@ void TestSpeakerRequiresUnambiguousUsbDevice() {
"USB to Bluetooth change retained the USB speaker");
}
void TestBluetoothAudioAndIdentity() {
Fixture f;
wireless = hid_available = true;
connected_pads = {1, 3};
devices.clear(); // Bluetooth does not enumerate a quad USB audio endpoint.
auto vibration = Open();
auto speaker = Open(true);
std::vector<float> haptic(2048, 0.5f);
std::vector<float> sound(2048, 0.25f);
Check(Haptics::SetVibration(1, 100, 80) && pad_rumble[1].large == 100 * 0x101,
"Bluetooth controller did not receive normal rumble");
Check(Haptics::Queue(vibration.get(), 1, haptic.data(), 1024, 2, true, unity.data()) != 0,
"Bluetooth haptics did not open");
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0,
"Bluetooth speaker did not open");
Check(hid_opens == 1 && opened_hid_pad == 1 && effects[1][7] == 0x30,
"Bluetooth audio was not matched to the active controller");
Check(pad_rumble[1].large == 0, "Bluetooth haptics did not suppress motor rumble");
auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
Check(pending.size() == 1, "Bluetooth streams were not combined in one report");
Libs::Controller::DualSenseBluetooth::Send(pending);
Check(hid_reports.size() == 1, "Bluetooth audio report was not sent");
const auto& report = hid_reports.back();
Check(report[0] == 0x39 && report[2] == 0x91 && report[3] == 6 && report[4] == 0x7e &&
report[10] == 0xd2 && report[11] == 64 && report[140] == 0xd3 && report[141] == 200,
"Bluetooth audio sub-packets are malformed");
Check(report[12] > 0 && report[13] > 0,
"haptic waveform was not downsampled into the actuator channels");
uint8_t prefix = 0xa2;
auto crc = SDL_crc32(0, &prefix, 1);
crc = SDL_crc32(crc, report.data(), report.size() - 4);
for (size_t i = 0; i < 4; i++) {
Check(report[report.size() - 4 + i] == static_cast<uint8_t>(crc >> (8 * i)),
"Bluetooth audio CRC is invalid");
}
int opus_error = OPUS_OK;
auto* decoder = opus_decoder_create(48000, 2, &opus_error);
Check(decoder != nullptr && opus_error == OPUS_OK, "Opus decoder unavailable in test");
std::array<float, 960> decoded {};
Check(opus_decode_float(decoder, report.data() + 142, 200, decoded.data(), 480, 0) == 480,
"Bluetooth speaker packet could not be decoded");
opus_decoder_destroy(decoder);
const auto mean = std::accumulate(decoded.begin(), decoded.end(), 0.0f) / decoded.size();
Check(mean > 0.05f && mean < 0.35f, "Bluetooth speaker audio was not preserved");
Check(Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000).empty(),
"Bluetooth sender ignored report pacing");
now += 300;
Haptics::UpdateBluetoothRumble();
Check(pad_rumble[1].large == 100 * 0x101,
"Bluetooth haptics did not restore normal rumble afterward");
// A different active DualSense has a different HID address, even if both are
// paired.
Check(Haptics::Queue(vibration.get(), 3, haptic.data(), 1024, 2, true, unity.data()) != 0 &&
opened_hid_pad == 3,
"Bluetooth stream followed the wrong controller");
Check(Haptics::Queue(speaker.get(), 3, sound.data(), 1024, 2, true, unity.data()) != 0,
"Bluetooth speaker did not follow the selected controller");
Check(effects[1][7] == 0, "old Bluetooth controller remained speaker-routed");
wireless = false;
connected_pads = {3};
devices = {{20, "Speakers (DualSense Wireless Controller)", 4}};
Check(Haptics::Queue(speaker.get(), 3, sound.data(), 1024, 2, true, unity.data()) != 0 &&
opened_device == 20 && effects[3][7] == 0x30,
"Bluetooth to USB change did not reopen the USB speaker immediately");
speaker.reset();
vibration.reset();
}
void TestBluetoothFailureFallsBack() {
Fixture f;
wireless = hid_available = true;
auto speaker = Open(true);
std::vector<float> sound(2048, 0.25f);
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0,
"Bluetooth port did not initially open");
fail_hid_write = true;
auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
Libs::Controller::DualSenseBluetooth::Send(pending);
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) == 0 &&
effects[1][7] == 0,
"failed HID write did not restore the normal audio fallback");
speaker.reset();
}
void TestBluetoothFormatsAndRoutingFailure() {
Fixture f;
wireless = hid_available = fail_effect = true;
auto speaker = Open(true);
Check(Haptics::Queue(speaker.get(), 1, pcm.data(), 2, 2, true, unity.data()) == 0 &&
!Haptics::UsesBluetooth(speaker.get()) && hid_closes == 1 && effects[1].empty(),
"failed Bluetooth speaker routing leaked a session or suppressed fallback");
fail_effect = false;
now += 2001;
std::vector<int16_t> mono(1024, 16384);
const int volume = 16384;
Queue(speaker, mono.data(), 1024, 1, false, &volume);
auto pending = Bluetooth::Prepare(now * 1000000);
Check(pending.size() == 1 && hid_opens == 2 && effects[1][7] == 0x30 &&
std::abs(pending[0].audio[200] - 0.25f) < 1e-4f &&
std::abs(pending[0].audio[201] - 0.25f) < 1e-4f,
"Bluetooth mono int16 volume mapping or speaker routing recovery failed");
}
void TestBluetoothEncodingDoesNotBlockAudioQueue() {
Fixture f;
wireless = hid_available = true;
auto speaker = Open(true);
std::vector<float> sound(2048, 0.25f);
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0,
"Bluetooth speaker did not open for concurrent encoding test");
block_opus_encode = true;
std::thread encoder([&] {
auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
Libs::Controller::DualSenseBluetooth::Send(pending);
});
encode_started.acquire();
std::binary_semaphore queue_finished {0};
std::atomic_bool queue_ok {false};
std::thread audio([&] {
queue_ok = Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true,
unity.data()) != 0;
queue_finished.release();
});
const bool queued_without_encoder =
queue_finished.try_acquire_for(std::chrono::seconds(2));
allow_encode.release();
audio.join();
encoder.join();
Check(queued_without_encoder && queue_ok,
"Bluetooth audio queue waited for Opus encoding");
speaker.reset();
}
void TestBluetoothCloseCancelsPreparedReport() {
Fixture f;
wireless = hid_available = true;
auto speaker = Open(true);
auto vibration = Open();
std::vector<float> sound(2048, 0.25f);
Queue(speaker, sound.data(), 1024);
Queue(vibration, sound.data(), 1024);
const auto base = now * 1000000;
auto pending = Bluetooth::Prepare(base);
Check(pending.size() == 1, "Bluetooth report was not prepared before stream close");
speaker.reset();
Bluetooth::Send(pending);
Check(hid_reports.empty(), "closed speaker's prepared report was sent");
Queue(vibration, sound.data(), 1024);
Bluetooth::Send(Bluetooth::Prepare(base + Bluetooth::PERIOD_NS));
Check(hid_reports.size() == 1 && hid_reports.back()[140] == 0,
"closing the speaker prevented the remaining haptic stream from sending");
}
void TestBluetoothCloseDuringEncoding() {
Fixture f;
wireless = hid_available = true;
auto speaker = Open(true);
std::vector<float> sound(2048, 0.25f);
Queue(speaker, sound.data(), 1024);
block_opus_encode = true;
std::thread encoder([&] { Bluetooth::Send(Bluetooth::Prepare(now * 1000000)); });
encode_started.acquire();
std::binary_semaphore close_finished {0};
std::thread closer([&] {
speaker.reset();
close_finished.release();
});
const bool closed_without_encoder = close_finished.try_acquire_for(std::chrono::seconds(2));
const bool hid_alive_during_encode = hid_closes == 0;
allow_encode.release();
closer.join();
encoder.join();
Check(closed_without_encoder && hid_alive_during_encode && hid_closes == 1 &&
hid_reports.empty(),
"closing during encoding blocked, destroyed a live session, or sent stale audio");
}
void TestBluetoothCloseWaitsForWriteWithoutBlockingQueue() {
Fixture f;
wireless = hid_available = true;
auto speaker = Open(true);
auto vibration = Open();
std::vector<float> sound(2048, 0.25f);
Queue(speaker, sound.data(), 1024);
Queue(vibration, sound.data(), 1024);
auto pending = Bluetooth::Prepare(now * 1000000);
Check(pending.size() == 1, "Bluetooth report was not prepared before blocked write");
block_hid_write = true;
std::thread sender([&] { Bluetooth::Send(pending); });
write_started.acquire();
std::binary_semaphore close_started {0}, close_finished {0}, queue_finished {0};
std::thread closer([&] {
close_started.release();
speaker.reset();
close_finished.release();
});
close_started.acquire();
const bool closed_before_write = close_finished.try_acquire_for(std::chrono::milliseconds(50));
std::atomic_bool queue_ok {false};
std::thread audio([&] {
queue_ok =
Haptics::Queue(vibration.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0;
queue_finished.release();
});
const bool queued_without_write = queue_finished.try_acquire_for(std::chrono::seconds(2));
allow_write.release();
audio.join();
closer.join();
sender.join();
Check(!closed_before_write && queued_without_write && queue_ok && hid_reports.size() == 1 &&
effects[1][7] == 0,
"close failed to drain the HID write independently of the audio queue");
}
void TestBluetoothSpeakerWaitsForItsAudioBlock() {
Fixture f;
wireless = hid_available = true;
auto vibration = Open();
auto speaker = Open(true);
std::vector<float> haptic(2048, 0.5f);
std::vector<float> sound(2048, 0.25f);
Check(Haptics::Queue(vibration.get(), 1, haptic.data(), 1024, 2, true, unity.data()) != 0,
"Bluetooth vibration stream did not open");
Check(Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000).empty(),
"haptics sent a silent speaker packet before the audio batch arrived");
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 256, 2, true, unity.data()) != 0,
"Bluetooth speaker did not accept a short block");
Check(Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000).empty(),
"short Bluetooth speaker block was padded with silence immediately");
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 768, 2, true, unity.data()) != 0,
"Bluetooth speaker did not accept the remainder of its block");
auto reports = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
Check(reports.size() == 1 && reports[0].audio[100] > 0.2f && reports[0].data[12] > 0,
"Bluetooth speaker and haptics were not combined into a complete report");
speaker.reset();
vibration.reset();
}
void TestBluetoothShortHapticBlock() {
Fixture f;
wireless = hid_available = true;
auto vibration = Open();
std::vector<float> sound(2048, 0.25f);
Queue(vibration, sound.data(), 256);
const auto base = now * 1000000;
Check(Bluetooth::Prepare(base).empty() && Bluetooth::Prepare(base + 1000000).empty(),
"short haptic block was padded with silence before its remainder arrived");
Queue(vibration, sound.data(), 768);
Check(Bluetooth::Prepare(base + 1000000).empty(),
"completed haptic block skipped its speaker coalescing window");
auto pending = Bluetooth::Prepare(base + 2000000);
Check(pending.size() == 1 && pending[0].data[12 + 100] > 0,
"complete haptic block did not play after its remainder arrived");
}
void TestBluetoothIncrementalBatchCoalesces() {
Fixture f;
wireless = hid_available = true;
auto vibration = Open();
auto speaker = Open(true);
std::array<float, 512> sound;
sound.fill(0.25f);
for (int block = 0; block < 3; ++block) {
Queue(vibration, sound.data(), 256);
Queue(speaker, sound.data(), 256);
Check(Bluetooth::Prepare(now * 1000000).empty(),
"incremental Bluetooth batch was padded before it was complete");
now += 5;
}
Queue(vibration, sound.data(), 256);
Check(Bluetooth::Prepare(now * 1000000).empty(),
"completed haptics sent before the same batch's final speaker block arrived");
Queue(speaker, sound.data(), 256);
auto pending = Bluetooth::Prepare(now * 1000000);
Check(pending.size() == 1 && pending[0].audio[1800] > 0.2f && pending[0].data[12 + 100] > 0,
"completed incremental speaker and haptics batch did not play immediately");
}
void TestBluetoothLowRateCompleteBlock() {
Fixture f;
wireless = hid_available = true;
auto speaker = Open(true, 8000);
std::array<float, 171 * 2> sound;
sound.fill(0.25f);
Queue(speaker, sound.data(), 171);
auto pending = Bluetooth::Prepare(now * 1000000);
Check(pending.size() == 1 && pending[0].audio[1000] > 0.2f,
"resampler lookahead delayed a complete low-rate speaker block");
}
void TestBluetoothCompleteStreamDoesNotWaitForPartialPeer() {
Fixture f;
wireless = hid_available = true;
auto complete = Open(true);
auto partial = Open(true);
std::vector<float> sound(2048, 0.25f);
Queue(complete, sound.data(), 1024);
Queue(partial, sound.data(), 256);
auto pending = Bluetooth::Prepare(now * 1000000);
Check(pending.size() == 1 && pending[0].audio[100] > 0.4f && pending[0].audio[1000] > 0.2f,
"complete speaker stream waited for a partial peer instead of mixing available audio");
}
void TestBluetoothShortFinalBlock() {
for (bool speaker: {false, true}) {
Fixture f;
wireless = hid_available = true;
auto port = Open(speaker);
std::vector<float> sound(2048, 0.25f);
Queue(port, sound.data(), 256);
const auto base = now * 1000000;
Check(Bluetooth::Prepare(base).empty(),
"short final Bluetooth block sent before more audio could arrive");
Check(Bluetooth::Prepare(base + Bluetooth::PERIOD_NS).size() == 1,
"short final Bluetooth block was never played");
}
}
void TestBluetoothOverflowKeepsRecentAudio() {
Fixture f;
wireless = hid_available = true;
auto speaker = Open(true);
std::vector<float> sound(2048);
for (int block = 1; block <= 4; ++block) {
std::fill(sound.begin(), sound.end(), block * 0.1f);
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true,
unity.data()) != 0,
"Bluetooth speaker overflow queue failed");
}
auto first = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
Check(first.size() == 1 && first[0].audio[200] > 0.15f && first[0].audio[200] < 0.25f,
"Bluetooth queue overflow lost all buffered audio instead of the oldest block");
auto second = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000 +
Libs::Controller::DualSenseBluetooth::PERIOD_NS);
Check(second.size() == 1 && second[0].audio[200] > 0.25f && second[0].audio[200] < 0.35f,
"Bluetooth queue overflow did not preserve the next audio block");
speaker.reset();
}
void TestBluetoothHapticOverflowKeepsRecentAudio() {
Fixture f;
wireless = hid_available = true;
auto vibration = Open();
std::vector<float> haptic(2048);
for (int block = 1; block <= 4; ++block) {
std::fill(haptic.begin(), haptic.end(), block * 0.1f);
Check(Haptics::Queue(vibration.get(), 1, haptic.data(), 1024, 2, true,
unity.data()) != 0,
"Bluetooth haptic overflow queue failed");
}
const auto base = now * 1000000;
Check(Libs::Controller::DualSenseBluetooth::Prepare(base).empty(),
"haptic-only report skipped its coalescing window");
auto first = Libs::Controller::DualSenseBluetooth::Prepare(base + 1000000);
Check(first.size() == 1 && first[0].data[12 + 32] > 18 && first[0].data[12 + 32] < 32,
"Bluetooth haptic overflow discarded buffered waveform instead of one report");
vibration.reset();
}
void TestBluetoothRecoversAfterLateWrite() {
Fixture f;
wireless = hid_available = true;
auto speaker = Open(true);
std::vector<float> sound(2048, 0.25f);
for (int i = 0; i < 3; ++i) {
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true,
unity.data()) != 0,
"Bluetooth speaker could not buffer three reports");
}
const auto base = now * 1000000;
const auto period = Libs::Controller::DualSenseBluetooth::PERIOD_NS;
Check(Libs::Controller::DualSenseBluetooth::Prepare(base).size() == 1,
"first Bluetooth report was not prepared");
Check(Libs::Controller::DualSenseBluetooth::Prepare(base + period + 10000000).size() == 1,
"late Bluetooth report was not prepared");
Check(Libs::Controller::DualSenseBluetooth::Prepare(base + 2 * period).size() == 1,
"Bluetooth sender did not recover its schedule after a late write");
speaker.reset();
}
void TestUsbOverflowKeepsRecentAudio() {
Fixture f;
auto speaker = Open(true);
std::vector<float> sound(2048);
for (int block = 1; block <= 4; ++block) {
std::fill(sound.begin(), sound.end(), block * 0.1f);
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true,
unity.data()) != 0,
"USB speaker overflow queue failed");
}
std::array<float, 4> first {};
Check(!streams.empty() &&
SDL_GetAudioStreamData(streams.back(), first.data(), sizeof(first)) ==
sizeof(first) &&
first[0] > 0.15f && first[0] < 0.25f,
"USB queue overflow did not preserve the newest buffered audio");
// A single oversized input must also preserve only its newest 80 ms.
sound.assign(4096 * 2, 0.1f);
std::fill(sound.begin() + 256 * 2, sound.end(), 0.5f);
const auto queued = Haptics::Queue(speaker.get(), 1, sound.data(), 4096, 2, true, unity.data());
Check(queued == 80000 && SDL_GetAudioStreamQueued(streams.back()) == 3840 * 4 * sizeof(float) &&
SDL_GetAudioStreamData(streams.back(), first.data(), sizeof(first)) ==
sizeof(first) &&
first[0] == 0.5f && first[1] == 0.5f,
"oversized USB block did not retain only the newest 80 ms");
speaker.reset();
}
void TestBluetoothAmbiguousDevice() {
Fixture f;
wireless = hid_available = duplicate_hid = true;
auto speaker = Open(true);
Check(Haptics::Queue(speaker.get(), 1, pcm.data(), 2, 2, true, unity.data()) == 0 &&
hid_opens == 0 && effects[1].empty(),
"ambiguous Bluetooth devices were routed arbitrarily");
speaker.reset();
}
void TestUsbAndBluetoothFeatureParity() {
Fixture f;
connected_pads = {1, 3};
wireless_pad3 = hid_available = true;
auto wired_speaker = Open(true);
auto wireless_speaker = Open(true);
std::vector<float> sound(2048, 0.25f);
Check(Haptics::Queue(wired_speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0 &&
opened_device == 10,
"Bluetooth pad disabled the wired pad speaker");
Check(Haptics::Queue(wireless_speaker.get(), 3, sound.data(), 1024, 2, true, unity.data()) !=
0 &&
opened_hid_pad == 3,
"wired pad disabled the Bluetooth speaker");
Check(Haptics::Queue(wired_speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0 &&
effects[1][7] == 0x30 && effects[3][7] == 0,
"returning to the wired speaker left Bluetooth routing active");
wireless_speaker.reset();
wired_speaker.reset();
}
void TestAudioSpeakerFallback() {
Fixture f;
Libs::Audio::Audio audio;
@@ -545,6 +1105,26 @@ void TestAudioSpeakerRouting() {
ExpectPcm({0.5f, 0.5f, 0, 0, 0.5f, 0.5f, 0, 0});
}
void TestBluetoothAudioSpeakerRouting() {
Fixture f;
wireless = hid_available = true;
devices.clear();
Libs::Audio::Audio audio;
const auto port = audio.AudioOutOpen(4, 1024, 48000, Libs::Audio::Audio::Format::FloatStereo);
Check(port.IsValid() && default_stream != nullptr, "Bluetooth pad speaker port did not open");
std::vector<float> sound(2048, 0.25f);
Libs::Audio::Audio::OutputParam output {port, sound.data()};
Check(audio.AudioOutOutputs(&output, 1, false) == 1024 && hid_opens == 1 &&
SDL_GetAudioStreamQueued(default_stream) == 0,
"Bluetooth pad speaker also played on the host output");
fail_hid_write = true;
auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
Libs::Controller::DualSenseBluetooth::Send(pending);
Check(audio.AudioOutOutputs(&output, 1, false) == 1024 &&
SDL_GetAudioStreamQueued(default_stream) > 0,
"Bluetooth write failure did not use the normal speaker output");
}
void TestAudioDefaultResumeFailure() {
Fixture f;
Libs::Audio::Audio audio;
@@ -564,6 +1144,7 @@ int main() {
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
TestAudioSpeakerFallback();
TestAudioSpeakerRouting();
TestBluetoothAudioSpeakerRouting();
TestAudioDefaultResumeFailure();
TestFormatsAndVolume();
TestDiscoveryAndHotplug();
@@ -571,6 +1152,25 @@ int main() {
TestSpeakerUnplug();
TestSpeakerSwitchAndFailures();
TestSpeakerRequiresUnambiguousUsbDevice();
TestBluetoothAudioAndIdentity();
TestBluetoothFailureFallsBack();
TestBluetoothFormatsAndRoutingFailure();
TestBluetoothEncodingDoesNotBlockAudioQueue();
TestBluetoothCloseCancelsPreparedReport();
TestBluetoothCloseDuringEncoding();
TestBluetoothCloseWaitsForWriteWithoutBlockingQueue();
TestBluetoothSpeakerWaitsForItsAudioBlock();
TestBluetoothShortHapticBlock();
TestBluetoothIncrementalBatchCoalesces();
TestBluetoothLowRateCompleteBlock();
TestBluetoothCompleteStreamDoesNotWaitForPartialPeer();
TestBluetoothShortFinalBlock();
TestBluetoothOverflowKeepsRecentAudio();
TestBluetoothHapticOverflowKeepsRecentAudio();
TestBluetoothRecoversAfterLateWrite();
TestUsbOverflowKeepsRecentAudio();
TestBluetoothAmbiguousDevice();
TestUsbAndBluetoothFeatureParity();
TestFailuresAndBoundedQueue();
TestRumbleLeaseAndDuration();
TestSwitchStopsOldRumble();
+19 -7
View File
@@ -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
+56
View File
@@ -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);
@@ -634,6 +689,7 @@ int main() {
TestShaderBaseAndUserData();
TestCarryAndBitFields();
TestInvariantAndDivergentPhi();
TestOperandMutationLifecycle();
TestControlDependentStandaloneLoadStaysTyped();
TestRuntime64BitDescriptorOps();
TestUniformFirstLaneSamplerLod();
+26 -2
View File
@@ -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,