Compare commits

...
17 changed files with 874 additions and 181 deletions

No files matched your search

@@ -53,6 +53,7 @@ constexpr OpcodeMap SOP1_OPCODE_LIST[] = {
{0x14u, Opcode::S_FF1_I32_B64},
{0x15u, Opcode::S_FLBIT_I32_B32},
{0x16u, Opcode::S_FLBIT_I32_B64},
{0x19u, Opcode::S_SEXT_I32_I8},
{0x1au, Opcode::S_SEXT_I32_I16},
{0x1bu, Opcode::S_BITSET0_B32},
{0x1cu, Opcode::S_BITSET0_B64},
@@ -503,6 +503,7 @@ std::string InstructionToString(const Instruction& inst) {
OperandToString(inst.dst).c_str(),
OperandToString(inst.src0).c_str()));
case Opcode::S_ABS_I32:
case Opcode::S_SEXT_I32_I8:
case Opcode::S_SEXT_I32_I16:
case Opcode::S_BREV_B32:
case Opcode::S_BREV_B64:
@@ -45,6 +45,7 @@ enum class Opcode {
S_CMOV_B64,
S_MOVK_I32,
S_ABS_I32,
S_SEXT_I32_I8,
S_SEXT_I32_I16,
S_ABSDIFF_I32,
S_BREV_B32,
@@ -400,6 +401,7 @@ enum class Opcode {
V_CMP_T_I32,
V_CMP_CLASS_F32,
V_CMPX_CLASS_F32,
V_CMP_CLASS_F16,
V_CMPX_CLASS_F16,
V_CMP_LT_I16,
V_CMP_EQ_I16,
@@ -36,7 +36,7 @@ constexpr Vop2OpcodeInfo VOP2_OPCODE_LIST[] = {
{0x05u, Opcode::V_SUBREV_F32},
{0x08u, Opcode::V_MUL_F32, Vop2SdwaProfile::Float32},
{0x09u, Opcode::V_MUL_I32_I24, Vop2SdwaProfile::IntegerPartialDestination},
{0x0bu, Opcode::V_MUL_U32_U24, Vop2SdwaProfile::IntegerFullDestination},
{0x0bu, Opcode::V_MUL_U32_U24, Vop2SdwaProfile::IntegerPartialDestination},
{0x0fu, Opcode::V_MIN_F32},
{0x10u, Opcode::V_MAX_F32},
{0x11u, Opcode::V_MIN_I32},
@@ -245,7 +245,8 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0x88u, Opcode::V_CMP_CLASS_F32}, {0x89u, Opcode::V_CMP_LT_I16},
{0x8au, Opcode::V_CMP_EQ_I16}, {0x8bu, Opcode::V_CMP_LE_I16},
{0x8cu, Opcode::V_CMP_GT_I16}, {0x8du, Opcode::V_CMP_NE_I16},
{0x8eu, Opcode::V_CMP_GE_I16}, {0x91u, Opcode::V_CMPX_LT_I32},
{0x8eu, Opcode::V_CMP_GE_I16}, {0x8fu, Opcode::V_CMP_CLASS_F16, false},
{0x91u, Opcode::V_CMPX_LT_I32},
{0x92u, Opcode::V_CMPX_EQ_I32}, {0x93u, Opcode::V_CMPX_LE_I32},
{0x94u, Opcode::V_CMPX_GT_I32}, {0x95u, Opcode::V_CMPX_NE_I32},
{0x96u, Opcode::V_CMPX_GE_I32}, {0x98u, Opcode::V_CMPX_CLASS_F32},
@@ -578,7 +579,7 @@ struct Vop1SdwaRule {
constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
{Opcode::V_MOV_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), false},
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_CVT_F32_U32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_I32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_UBYTE0, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
@@ -929,6 +930,7 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
case Opcode::V_CMPX_NEQ_F16:
case Opcode::V_CMPX_NLT_F16:
case Opcode::V_CMP_CLASS_F32:
case Opcode::V_CMP_CLASS_F16:
case Opcode::V_CMPX_CLASS_F16:
case Opcode::V_CMPX_CLASS_F32: return true;
default: return false;
@@ -134,10 +134,14 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_ABS_I32:
return SimpleInteger(inst, IR::ValueOpcode::IAbs32, IR::Type::U32, false, false, true);
case O::S_SEXT_I32_I16:
case O::S_SEXT_I32_I8:
case O::S_SEXT_I32_I16: {
// Sign extension leaves SCC unchanged.
const auto bits = inst.opcode == O::S_SEXT_I32_I8 ? 8u : 16u;
WriteOperand(inst.dst, IR::U32(ir.Emit(IR::ValueOpcode::BitFieldSExtract,
{ReadU32(inst.src0), IR::Value(0u), IR::Value(16u)})));
{ReadU32(inst.src0), IR::Value(0u), IR::Value(bits)})));
return;
}
case O::S_MUL_I32:
case O::S_MULK_I32:
return SimpleInteger(inst, IR::ValueOpcode::IMul32, IR::Type::U32, false, false, false);
@@ -374,6 +374,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false, true); return;
case O::V_CMP_CLASS_F32: EmitFloatClassCompare(inst, false); return;
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst, true); return;
case O::V_CMP_CLASS_F16: EmitFloatClassCompare(inst, false, true); return;
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true, true); return;
case O::V_CVT_F32_UBYTE0: V_CVT_F32_UBYTE(inst, 0); return;
+25 -29
View File
@@ -15,7 +15,6 @@
#include <algorithm>
#include <array>
#include <cstring>
#include <limits>
#include <magic_enum.hpp>
#include <vector>
@@ -90,8 +89,9 @@ public:
};
struct OutputParam {
Id handle;
const void* data = nullptr;
Id handle;
const void* data = nullptr;
const float* gains = nullptr;
};
Audio() = default;
@@ -159,8 +159,10 @@ private:
static void OpenSdlDevice(PortIn* port, Format format);
static void CloseSdlDevice(PortIn* port);
static const void* PrepareOutputBuffer(const PortOut& port, const void* data,
std::vector<uint8_t>* buffer, float gain);
static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain);
std::vector<uint8_t>* buffer, float gain,
const float* gains);
static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain,
const float* gains);
};
static Audio* g_audio = nullptr;
@@ -196,7 +198,7 @@ uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking)
for (uint32_t i = 0; i < num; i++) {
if (params[i].handle > 0 && params[i].data != nullptr) {
output_params.push_back(
Audio::OutputParam {Audio::Id(params[i].handle), params[i].data});
Audio::OutputParam {Audio::Id(params[i].handle), params[i].data, params[i].gains});
}
}
@@ -302,7 +304,8 @@ void Audio::CloseSdlDevice(PortOut* port) {
}
const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
std::vector<uint8_t>* buffer, float gain) {
std::vector<uint8_t>* buffer, float gain,
const float* gains) {
EXIT_IF(data == nullptr);
EXIT_IF(buffer == nullptr);
@@ -312,12 +315,12 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
const auto bytes_per_sample = BytesPerSample(port.format);
const bool reorder = channels >= 8 && !FormatIsStd(port.format);
bool volume_changed = gain != 1.0f;
std::array<double, 12> scales {};
bool volume_changed = false;
for (uint32_t ch = 0; ch < channels; ch++) {
if (port.volume[ch] != 32768) {
volume_changed = true;
break;
}
scales[ch] = (static_cast<double>(port.volume[ch]) / 32768.0) * gain *
(gains != nullptr ? gains[ch] : 1.0f);
volume_changed |= scales[ch] != 1.0;
}
if (!volume_changed && !reorder) {
@@ -337,8 +340,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
for (uint32_t ch = 0; ch < output_channels; ch++) {
const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch;
dst[frame * output_channels + ch] =
src[frame * channels + src_ch] *
(static_cast<float>(port.volume[src_ch]) / 32768.0f) * gain;
static_cast<float>(src[frame * channels + src_ch] * scales[src_ch]);
}
if (channels == 12) {
// Add the four top channels to their front/back channels, turning 12 into 8.
@@ -346,8 +348,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
static constexpr uint32_t HEIGHT_DST[4] = {0, 1, 4, 5};
for (uint32_t ch = 0; ch < 4; ch++) {
dst[frame * output_channels + HEIGHT_DST[ch]] +=
src[frame * channels + 8 + ch] *
(static_cast<float>(port.volume[8 + ch]) / 32768.0f) * gain;
static_cast<float>(src[frame * channels + 8 + ch] * scales[8 + ch]);
}
}
}
@@ -358,15 +359,9 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
for (uint32_t frame = 0; frame < frames; frame++) {
for (uint32_t ch = 0; ch < output_channels; ch++) {
const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch;
int64_t sample =
static_cast<int64_t>(src[frame * channels + src_ch]) * port.volume[src_ch] / 32768;
sample = static_cast<int64_t>(sample * static_cast<double>(gain));
if (sample > std::numeric_limits<int16_t>::max()) {
sample = std::numeric_limits<int16_t>::max();
} else if (sample < std::numeric_limits<int16_t>::min()) {
sample = std::numeric_limits<int16_t>::min();
}
dst[frame * output_channels + ch] = static_cast<int16_t>(sample);
const double sample = src[frame * channels + src_ch] * scales[src_ch];
dst[frame * output_channels + ch] = static_cast<int16_t>(
std::clamp(sample, -32768.0, 32767.0));
}
}
}
@@ -374,7 +369,8 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
return buffer->data();
}
bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain) {
bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain,
const float* gains) {
EXIT_IF(port == nullptr);
if (port->stream == nullptr || data == nullptr) {
@@ -382,7 +378,7 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float
}
std::vector<uint8_t> prepared_buffer;
const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer, gain);
const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer, gain, gains);
const auto output_channels = OutputChannels(*port);
const auto prepared_size =
BytesPerSample(port->format) * output_channels * port->samples_num;
@@ -570,14 +566,14 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
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,
gain);
gain, params[i].gains);
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.
if (QueueSdlAudio(&port, params[i].data, blocking, gain)) {
if (QueueSdlAudio(&port, params[i].data, blocking, gain, params[i].gains)) {
any_device = true;
paced[i] = port.queue_primed;
}
+3 -2
View File
@@ -19,8 +19,9 @@ enum class Format {
};
struct OutputParam {
int handle = 0;
const void* data = nullptr;
int handle = 0;
const void* data = nullptr;
const float* gains = nullptr;
};
static constexpr int OUT_PORTS_MAX = 32;
+2 -2
View File
@@ -315,7 +315,7 @@ bool UsesBluetooth(const Stream* stream) {
}
uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume, float gain) {
bool is_float, const int* volume, float gain, const float* gains) {
if (stream == nullptr || data == nullptr || frames == 0 || channels == 0 || volume == nullptr) {
return 0;
}
@@ -357,7 +357,7 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames
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 * gain;
value *= volume[src_ch] / 32768.0f * gain * (gains != nullptr ? gains[src_ch] : 1.0f);
// 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;
+2 -1
View File
@@ -17,7 +17,8 @@ bool UsesBluetooth(const Stream* stream);
// (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, float gain = 1.0f);
bool is_float, const int* volume, float gain = 1.0f,
const float* gains = nullptr);
// Returns false for other gamepad types, which retain the normal rumble path.
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor,
uint32_t duration_ms = 0xffff);
+9 -25
View File
@@ -52,6 +52,7 @@ struct ResolvedPathInfo {
static std::mutex g_mutex;
static uint32_t g_next_submission_id = 1;
static uint32_t g_next_file_id = 1;
static std::unordered_map<uint32_t, bool> g_submissions;
static std::condition_variable g_submission_cv;
static std::unordered_map<uint32_t, std::string> g_files;
@@ -92,15 +93,6 @@ static bool WriteGuest(uint64_t addr, const T& value) {
return WriteGuestBytes(addr, &value, sizeof(T));
}
static uint32_t ComputeFileId(const char* guest_path) {
uint32_t hash = 2166136261u;
for (auto* p = reinterpret_cast<const uint8_t*>(guest_path); p != nullptr && *p != 0; ++p) {
hash ^= *p;
hash *= 16777619u;
}
return hash & static_cast<uint32_t>(std::numeric_limits<int32_t>::max());
}
static int ReadGuestCString(uint64_t addr, char* out, size_t out_size) {
if (addr == 0) {
return LibKernel::KERNEL_ERROR_EFAULT;
@@ -120,18 +112,6 @@ static int ReadGuestCString(uint64_t addr, char* out, size_t out_size) {
return LibKernel::KERNEL_ERROR_ENAMETOOLONG;
}
static void RegisterHostPathLocked(uint32_t file_id, const std::string& host_path,
uint64_t file_size, bool is_dir) {
g_files[file_id] = host_path;
g_file_sizes[file_id] = is_dir ? 0 : file_size;
}
static void RegisterHostPath(uint32_t file_id, const std::string& host_path, uint64_t file_size,
bool is_dir) {
std::scoped_lock lock(g_mutex);
RegisterHostPathLocked(file_id, host_path, file_size, is_dir);
}
static bool TryGetHostPath(uint32_t file_id, std::string* out) {
std::scoped_lock lock(g_mutex);
const auto it = g_files.find(file_id);
@@ -213,7 +193,6 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
if (!found) {
const auto real_path = LibKernel::FileSystem::GetRealFilename(path);
info.file_id = AprShared::ComputeFileId(guest_path);
info.host_path = Common::PathToString(real_path);
if (Common::File::IsDirectoryExisting(real_path)) {
@@ -232,7 +211,14 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
if (!inserted) {
info = it->second;
} else if (info.result == OK) {
RegisterHostPathLocked(info.file_id, info.host_path, info.file_size, info.is_dir);
// Allocate once under the cache lock; distinct paths can share a hash.
if (g_next_file_id == 0xffffffffu) {
info.result = it->second.result = LibKernel::KERNEL_ERROR_ENFILE;
} else {
info.file_id = it->second.file_id = g_next_file_id++;
g_files.emplace(info.file_id, info.host_path);
g_file_sizes.emplace(info.file_id, info.is_dir ? 0 : info.file_size);
}
} else if (info.result == LibKernel::KERNEL_ERROR_ENOENT) {
log_missing = true;
}
@@ -240,8 +226,6 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
if (log_missing) {
LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, info.host_path.c_str());
}
} else if (info.result == OK) {
AprShared::RegisterHostPath(info.file_id, info.host_path, info.file_size, info.is_dir);
}
if (info.result != OK) {
+219 -100
View File
@@ -11,6 +11,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstring>
#include <vector>
@@ -170,12 +171,14 @@ static std::atomic_uint64_t g_audioout2_next_port {1};
static AudioOut2UserHandle g_audioout2_next_user = 1;
struct AudioOut2ContextState {
bool used = false;
AudioOut2ContextHandle handle = 0;
uint32_t queue_depth = 4;
uint32_t queued = 0;
uint32_t num_grains = 512;
uint64_t last_update = 0;
bool used = false;
AudioOut2ContextHandle handle = 0;
uint32_t queue_depth = 4;
uint32_t queued = 0;
uint32_t num_grains = 512;
uint64_t last_update = 0;
int object_audio_handle = 0;
std::vector<float> object_mix;
};
struct AudioOut2PortStateEntry {
@@ -189,9 +192,11 @@ struct AudioOut2PortStateEntry {
uint32_t samples_num = 512;
AudioInternal::Format audio_format = AudioInternal::Format::Unknown;
int audio_handle = 0;
std::array<float, 16> gains {};
uint32_t ambisonics = UINT32_MAX;
bool attributes_locked = false;
std::vector<uint8_t> pcm_data;
};
struct AudioOut2SpeakerArrayState {
bool used = false;
uint32_t num_speakers = 0;
@@ -207,9 +212,8 @@ struct AudioOut2LatencyState {
uint32_t latency_us = 0;
};
static Common::Mutex g_audioout2_context_mutex;
static Common::Mutex g_audioout2_state_mutex;
static std::array<AudioOut2ContextState, 16> g_audioout2_contexts;
static Common::Mutex g_audioout2_port_mutex;
static std::array<AudioOut2PortStateEntry, 256> g_audioout2_ports;
static std::vector<AudioOut2UserHandle> g_audioout2_users;
static Common::Mutex g_audioout2_speaker_array_mutex;
@@ -223,7 +227,11 @@ static constexpr int AUDIO_OUT2_ERROR_INVALID_PARAM = -2144960511;
static constexpr int AUDIO_OUT2_ERROR_BUSY = -2144960505; /* 0x80268007 */
static constexpr int AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM = -2144959999; /* 0x80268201 */
static constexpr int AUDIO_OUT2_ERROR_MASTERING_INVALID_STATES_ID = -2144959996; /* 0x80268204 */
static constexpr int AUDIO_OUT2_ERROR_OUT_OF_RESOURCE = -2144960510; /* 0x80268002 */
static constexpr int AUDIO_OUT2_ERROR_INVALID_PORT = -2144960503; /* 0x80268009 */
static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM = 0;
static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN = 1;
static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS = 8;
static constexpr uint32_t AUDIO_OUT2_MASTERING_OUTPUT_RECORDING = 2;
static constexpr uint32_t AUDIO_OUT2_MASTERING_STATES_ID_DEFAULT = 1;
static constexpr uint32_t AUDIO_OUT2_MASTERING_STATES_ID_V2 = 2;
@@ -350,37 +358,80 @@ static size_t audioout2_pcm_size(const AudioOut2PortStateEntry& state) {
audioout2_data_format_channels(state.data_format) * bytes_per_sample;
}
static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) {
Common::LockGuard lock(g_audioout2_port_mutex);
static bool audioout2_context_has_queueable_device(const AudioOut2ContextState& context) {
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
!state.pcm_data.empty() && AudioInternal::AudioOutHasDevice(state.audio_handle)) {
return true;
if (state.used && state.context == context.handle && !state.pcm_data.empty()) {
const int handle = audioout2_port_type_is_object(state.port_type)
? context.object_audio_handle
: state.audio_handle;
if (AudioInternal::AudioOutHasDevice(handle)) {
return true;
}
}
}
return false;
}
static void audioout2_mix_object(AudioOut2ContextState& context,
const AudioOut2PortStateEntry& object) {
auto& mix = context.object_mix;
if (mix.empty()) {
mix.resize(static_cast<size_t>(context.num_grains) * 2, 0.0f);
}
// Center ordinary objects. First-order stereo ambisonics uses left/right-facing
// cardioids; FuMa W has a different normalization from ACN/SN3D W.
constexpr float half_power = 0.70710678f;
float left = half_power, right = half_power;
switch (object.ambisonics) {
case UINT32_MAX:
case 64: break;
case 0: left = right = 1.0f; break;
case 2:
case 65: right = -half_power; break;
default: return;
}
left *= object.gains[0];
right *= object.gains[0];
for (uint32_t frame = 0; frame < context.num_grains; frame++) {
const float sample =
object.audio_format == AudioInternal::Format::FloatMono
? reinterpret_cast<const float*>(object.pcm_data.data())[frame]
: reinterpret_cast<const int16_t*>(object.pcm_data.data())[frame] / 32768.0f;
mix[frame * 2] += sample * left;
mix[frame * 2 + 1] += sample * right;
}
}
static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) {
// The backend consumes the buffers synchronously, but can block while pacing SDL's queue.
// Keep both PCM storage and port handles alive until it returns.
Common::LockGuard lock(g_audioout2_state_mutex);
auto* context = audioout2_find_context_locked(ctx);
if (context == nullptr) {
return;
}
context->object_mix.clear();
std::vector<AudioInternal::OutputParam> params;
params.reserve(AudioInternal::OUT_PORTS_MAX);
Common::LockGuard lock(g_audioout2_port_mutex);
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
!state.pcm_data.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data.data()});
if (!state.used || state.context != ctx || state.pcm_data.empty()) {
continue;
}
if (audioout2_port_type_is_object(state.port_type)) {
audioout2_mix_object(*context, state);
} else if (state.audio_handle > 0 && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back({state.audio_handle, state.pcm_data.data(), state.gains.data()});
}
}
if (!context->object_mix.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back({context->object_audio_handle, context->object_mix.data()});
}
if (!params.empty()) {
(void)AudioInternal::AudioOutOutputs(params.data(), static_cast<uint32_t>(params.size()),
blocking);
}
}
static void audioout2_close_audio_handle(int audio_handle) {
if (audio_handle > 0) {
AudioInternal::AudioOutClose(audio_handle);
@@ -431,52 +482,58 @@ int KYTY_SYSV_ABI AudioOut2ContextCreate(const AudioOut2ContextParam* params, vo
EXIT_NOT_IMPLEMENTED(params == nullptr);
EXIT_NOT_IMPLEMENTED(ctx == nullptr);
*ctx = g_audioout2_next_context.fetch_add(1, std::memory_order_relaxed);
g_audioout2_context_mutex.Lock();
auto* state = audioout2_find_context_locked(0);
if (state == nullptr) {
for (auto& candidate: g_audioout2_contexts) {
if (!candidate.used) {
state = &candidate;
break;
}
}
const auto num_grains = params->num_grains == 0 ? 512u : params->num_grains;
const int object_audio_handle =
params->max_object_ports == 0
? 0
: AudioInternal::AudioOutOpen(AUDIO_OUT_PORT_TYPE_MAIN, num_grains, 48000,
AudioInternal::Format::FloatStereo);
if (params->max_object_ports != 0 && object_audio_handle == 0) {
return AUDIO_OUT2_ERROR_OUT_OF_RESOURCE;
}
EXIT_NOT_IMPLEMENTED(state == nullptr);
*state = AudioOut2ContextState {};
state->used = true;
state->handle = *ctx;
state->queue_depth = (params->queue_depth == 0 ? 4u : params->queue_depth);
state->queued = 0;
state->num_grains = (params->num_grains == 0 ? 512u : params->num_grains);
state->last_update = LibKernel::KernelGetProcessTime();
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Lock();
auto it = std::find_if(g_audioout2_contexts.begin(), g_audioout2_contexts.end(),
[](const auto& state) { return !state.used; });
if (it == g_audioout2_contexts.end()) {
g_audioout2_state_mutex.Unlock();
audioout2_close_audio_handle(object_audio_handle);
return AUDIO_OUT2_ERROR_OUT_OF_RESOURCE;
}
*ctx = g_audioout2_next_context.fetch_add(1, std::memory_order_relaxed);
auto* state = &*it;
*state = AudioOut2ContextState {};
state->used = true;
state->handle = *ctx;
state->queue_depth = params->queue_depth == 0 ? 4u : params->queue_depth;
state->num_grains = num_grains;
state->last_update = LibKernel::KernelGetProcessTime();
state->object_audio_handle = object_audio_handle;
g_audioout2_state_mutex.Unlock();
LOGF("\t buffer = 0x%016" PRIx64 "\n"
"\t buffer_size = 0x%016" PRIx64 "\n"
"\t ctx = 0x%016" PRIx64 "\n"
"\t queue_depth = %" PRIu32 ", num_grains = %" PRIu32 "\n",
reinterpret_cast<uint64_t>(buffer), static_cast<uint64_t>(buffer_size), *ctx,
state->queue_depth, state->num_grains);
(params->queue_depth == 0 ? 4u : params->queue_depth), num_grains);
return OK;
}
int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
PRINT_NAME();
LOGF("\t ctx = 0x%016" PRIx64 "\n", ctx);
g_audioout2_context_mutex.Lock();
std::array<int, 257> audio_handles {};
size_t audio_handles_num = 0;
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
if (state->object_audio_handle > 0) {
audio_handles[audio_handles_num++] = state->object_audio_handle;
}
*state = AudioOut2ContextState {};
}
g_audioout2_context_mutex.Unlock();
std::array<int, 256> audio_handles {};
size_t audio_handles_num = 0;
g_audioout2_port_mutex.Lock();
for (auto& port_state: g_audioout2_ports) {
if (port_state.used && port_state.context == ctx) {
if (port_state.audio_handle > 0 && audio_handles_num < audio_handles.size()) {
@@ -485,7 +542,7 @@ int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
port_state = AudioOut2PortStateEntry {};
}
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
for (size_t i = 0; i < audio_handles_num; i++) {
audioout2_close_audio_handle(audio_handles[i]);
@@ -504,11 +561,16 @@ int KYTY_SYSV_ABI AudioOut2ContextSetAttributes(AudioOut2ContextHandle ctx,
}
int KYTY_SYSV_ABI AudioOut2ContextAdvance(AudioOut2ContextHandle ctx) {
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
audioout2_update_context_locked(state);
}
g_audioout2_context_mutex.Unlock();
for (auto& port: g_audioout2_ports) {
if (port.used && port.context == ctx) {
port.attributes_locked = false;
}
}
g_audioout2_state_mutex.Unlock();
return OK;
}
@@ -520,11 +582,10 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
// Only a synchronous submission carrying PCM to a real device can rely on the SDL queue for
// pacing. Async pushes must retain queue-depth backpressure, and a handle without PCM (or a
// vibration/failed-open handle) has no downstream operation that can block this call.
const bool use_device_clock =
blocking != 0 && audioout2_context_has_queueable_device(ctx);
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
const bool use_device_clock =
blocking != 0 && audioout2_context_has_queueable_device(*state);
audioout2_update_context_locked(state);
sleep_micros = audioout2_grain_micros(state->num_grains);
if (state->queued < state->queue_depth || use_device_clock) {
@@ -534,12 +595,12 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
if (state->queued < state->queue_depth) {
state->queued++;
}
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
audioout2_queue_context_audio(ctx, blocking != 0);
return OK;
}
}
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
if (blocking == 0) {
return AUDIO_OUT2_ERROR_NOT_READY;
@@ -558,7 +619,7 @@ int KYTY_SYSV_ABI AudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint
*available_queues = 4;
}
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
audioout2_update_context_locked(state);
if (queue_level != nullptr) {
@@ -569,7 +630,7 @@ int KYTY_SYSV_ABI AudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint
(state->queued < state->queue_depth ? state->queue_depth - state->queued : 0);
}
}
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
return OK;
}
@@ -583,19 +644,25 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
const auto audio_format = audioout2_data_format_to_audio_format(params->data_format);
const auto audio_type = audioout2_port_type_to_audio_out_type(params->port_type);
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
const auto* context_state = audioout2_find_context_locked(ctx);
if (context_state == nullptr) {
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
const auto samples_num = context_state->num_grains == 0 ? 512u : context_state->num_grains;
if (audioout2_port_type_is_object(params->port_type) &&
(params->port_type != 0x100 || context_state->object_audio_handle == 0 ||
params->sampling_freq != 48000 ||
(audio_format != AudioInternal::Format::FloatMono &&
audio_format != AudioInternal::Format::Signed16bitMono))) {
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
const auto samples_num = context_state->num_grains;
g_audioout2_port_mutex.Lock();
if (std::find(g_audioout2_users.begin(), g_audioout2_users.end(), params->user_handle) ==
g_audioout2_users.end()) {
g_audioout2_port_mutex.Unlock();
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
AudioOut2PortStateEntry* port_state = nullptr;
@@ -616,9 +683,9 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
port_state->sampling_freq = params->sampling_freq;
port_state->samples_num = samples_num;
port_state->audio_format = audio_format;
port_state->gains.fill(1.0f);
}
g_audioout2_port_mutex.Unlock();
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
if (port_state == nullptr) {
return AUDIO_OUT2_ERROR_PORT_FULL;
@@ -632,12 +699,12 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
audio_format);
}
g_audioout2_port_mutex.Lock();
g_audioout2_state_mutex.Lock();
const bool reserved = port_state->used && port_state->handle == next_port;
if (reserved) {
port_state->audio_handle = audio_handle;
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
if (!reserved) {
audioout2_close_audio_handle(audio_handle);
return AUDIO_OUT2_ERROR_INVALID_PARAM;
@@ -663,12 +730,12 @@ int KYTY_SYSV_ABI AudioOut2PortDestroy(AudioOut2PortHandle port) {
LOGF("\t port = 0x%016" PRIx64 "\n", port);
int audio_handle = 0;
g_audioout2_port_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_port_locked(port); state != nullptr) {
audio_handle = state->audio_handle;
*state = AudioOut2PortStateEntry {};
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
audioout2_close_audio_handle(audio_handle);
@@ -677,36 +744,86 @@ int KYTY_SYSV_ABI AudioOut2PortDestroy(AudioOut2PortHandle port) {
int KYTY_SYSV_ABI AudioOut2PortSetAttributes(AudioOut2PortHandle port,
const AudioOut2Attribute* attributes, uint32_t num) {
EXIT_NOT_IMPLEMENTED(num != 0 && attributes == nullptr);
if (num != 0 && attributes == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_state_mutex);
auto* state = audioout2_find_port_locked(port);
if (state == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PORT;
}
const void* pcm_data = nullptr;
bool has_pcm = false;
const void* pcm_data = nullptr;
auto gains = state->gains;
auto ambisonics = state->ambisonics;
uint32_t seen = 0;
for (uint32_t i = 0; i < num; i++) {
if (attributes[i].attribute_id == AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM &&
attributes[i].value != nullptr && attributes[i].value_size >= sizeof(AudioOut2Pcm)) {
AudioOut2Pcm pcm {};
std::memcpy(&pcm, attributes[i].value, sizeof(AudioOut2Pcm));
pcm_data = pcm.data;
has_pcm = true;
const auto& attribute = attributes[i];
const auto id = attribute.attribute_id;
if (id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM && id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN &&
id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS) {
continue;
}
}
if (has_pcm) {
g_audioout2_port_mutex.Lock();
if (auto* state = audioout2_find_port_locked(port); state != nullptr) {
if (pcm_data != nullptr && state->audio_format != AudioInternal::Format::Unknown) {
const auto* bytes = static_cast<const uint8_t*>(pcm_data);
state->pcm_data.assign(bytes, bytes + audioout2_pcm_size(*state));
} else {
state->pcm_data.clear();
if ((seen & (1u << id)) != 0) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
seen |= 1u << id;
if (id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM && state->attributes_locked) {
continue;
}
if (attribute.value == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
switch (id) {
case AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM: {
if (attribute.value_size < sizeof(AudioOut2Pcm)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
AudioOut2Pcm pcm {};
std::memcpy(&pcm, attribute.value, sizeof(pcm));
pcm_data = pcm.data;
break;
}
case AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN: {
const auto channels = audioout2_data_format_channels(state->data_format);
if (attribute.value_size != channels * sizeof(float)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
std::memcpy(gains.data(), attribute.value, attribute.value_size);
for (uint32_t ch = 0; ch < channels; ch++) {
if (!std::isfinite(gains[ch]) || gains[ch] < 0.0f) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
}
break;
}
case AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS:
if (!audioout2_port_type_is_object(state->port_type) ||
attribute.value_size != sizeof(ambisonics)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
std::memcpy(&ambisonics, attribute.value, sizeof(ambisonics));
if (ambisonics != UINT32_MAX && ambisonics > 15 &&
(ambisonics < 64 || ambisonics > 99)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
break;
}
g_audioout2_port_mutex.Unlock();
}
state->gains = gains;
state->ambisonics = ambisonics;
if ((seen & (1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM)) != 0) {
if (pcm_data != nullptr && state->audio_format != AudioInternal::Format::Unknown) {
const auto* bytes = static_cast<const uint8_t*>(pcm_data);
state->pcm_data.assign(bytes, bytes + audioout2_pcm_size(*state));
} else {
state->pcm_data.clear();
}
state->attributes_locked = audioout2_port_type_is_object(state->port_type);
}
return OK;
}
int KYTY_SYSV_ABI AudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) {
PRINT_NAME();
@@ -719,11 +836,11 @@ int KYTY_SYSV_ABI AudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortS
state->reroute_counter = 0;
state->flags = 0;
g_audioout2_port_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* port_state = audioout2_find_port_locked(port); port_state != nullptr) {
state->num_channels = audioout2_data_format_channels(port_state->data_format);
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
// LOGF("\t port = 0x%016" PRIx64 "\n", port);
// LOGF("\t num_channels = %" PRIu8 "\n", state->num_channels);
@@ -743,7 +860,7 @@ int KYTY_SYSV_ABI AudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* han
if (handle == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_port_mutex);
Common::LockGuard lock(g_audioout2_state_mutex);
*handle = g_audioout2_next_user++;
g_audioout2_users.push_back(*handle);
LOGF("\t user_id = %" PRIu32 ", handle = 0x%016" PRIx64 "\n", user_id,
@@ -754,7 +871,7 @@ int KYTY_SYSV_ABI AudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* han
int KYTY_SYSV_ABI AudioOut2UserDestroy(AudioOut2UserHandle handle) {
PRINT_NAME();
LOGF("\t handle = 0x%016" PRIx64 "\n", static_cast<uint64_t>(handle));
Common::LockGuard lock(g_audioout2_port_mutex);
Common::LockGuard lock(g_audioout2_state_mutex);
const auto it = std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle);
if (it == g_audioout2_users.end()) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
@@ -773,13 +890,15 @@ int KYTY_SYSV_ABI AudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle
if (context_attributes == nullptr || port_attributes == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_port_mutex);
Common::LockGuard lock(g_audioout2_state_mutex);
if (std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle) ==
g_audioout2_users.end()) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
*context_attributes = 0;
*port_attributes = 1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM;
*port_attributes = (1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM) |
(1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN) |
(1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS);
return OK;
}
+187 -3
View File
@@ -3,12 +3,15 @@
#include "libs/errno.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <condition_variable>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <mutex>
#include <thread>
#include <vector>
@@ -24,6 +27,7 @@ std::vector<int> g_live_devices;
std::vector<int> g_device_backed_handles;
std::vector<bool> g_output_blocking;
std::vector<std::vector<uint8_t>> g_output_pcm;
std::vector<std::array<float, 2>> g_output_gains;
size_t g_capture_bytes = 0;
int g_next_device = 1;
int g_open_waiters = 0;
@@ -102,8 +106,9 @@ PortParam MakeParam(uint32_t data_format = 0x200) {
return param;
}
AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4) {
AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4, uint32_t max_object_ports = 0) {
ContextParam param {};
param.max_object_ports = max_object_ports;
param.queue_depth = queue_depth;
param.num_grains = 512;
AudioOut2::AudioOut2ContextHandle context = 0;
@@ -112,6 +117,12 @@ AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4) {
return context;
}
AudioOut2::AudioOut2PortHandle CreatePort(AudioOut2::AudioOut2ContextHandle context, const PortParam& param) {
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK, "port create failed");
return port;
}
void TestUserSupportedAttributes() {
constexpr int invalid_param = static_cast<int32_t>(0x80268001u);
constexpr int busy = static_cast<int32_t>(0x80268007u);
@@ -123,8 +134,8 @@ void TestUserSupportedAttributes() {
uint32_t port_attributes = UINT32_MAX;
Check(AudioOut2::AudioOut2UserGetSupportedAttributes(user, &context_attributes,
&port_attributes) == OK &&
context_attributes == 0 && port_attributes == 1,
"user capabilities do not match implemented PCM support");
context_attributes == 0 && port_attributes == 0x103,
"user capabilities do not match implemented attributes");
context_attributes = port_attributes = UINT32_MAX;
Check(AudioOut2::AudioOut2UserGetSupportedAttributes(user, nullptr, &port_attributes) ==
@@ -191,6 +202,13 @@ void SetPcm(AudioOut2::AudioOut2PortHandle port, const void* data) {
"setting PCM failed");
}
template <typename T>
void SetAttribute(AudioOut2::AudioOut2PortHandle port, uint32_t id, const T& value) {
const Attribute attribute {id, 0, &value, sizeof(value)};
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) == OK,
"setting attribute failed");
}
void ResetOutputCalls() {
std::lock_guard lock(g_device_mutex);
g_output_blocking.clear();
@@ -212,6 +230,23 @@ std::vector<std::vector<uint8_t>> OutputPcm() {
return g_output_pcm;
}
std::vector<std::vector<uint8_t>> PushPcm(AudioOut2::AudioOut2ContextHandle context, size_t bytes) {
CaptureOutputPcm(bytes);
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "PCM push failed");
auto output = OutputPcm();
CaptureOutputPcm(0);
return output;
}
void CheckSamples(const std::vector<uint8_t>& pcm, std::initializer_list<float> frame, const char* message) {
Check(!pcm.empty() && pcm.size() % (frame.size() * sizeof(float)) == 0, "incorrect PCM size");
for (size_t i = 0; i < pcm.size() / sizeof(float); i++) {
float sample;
std::memcpy(&sample, pcm.data() + i * sizeof(float), sizeof(float));
Check(std::abs(sample - *(frame.begin() + i % frame.size())) < 0.000001f, message);
}
}
void TestSlotReuse() {
const auto context = CreateContext();
const auto param = MakeParam();
@@ -428,6 +463,148 @@ void TestPcmCopiedBeforeScratchBufferReuse() {
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestPortGainAndValidation() {
const auto context = CreateContext();
const auto port = CreatePort(context, MakeParam());
std::vector<float> pcm(512 * 2, 0.25f);
SetPcm(port, pcm.data());
auto check_output = [&](const std::array<float, 2>& gain, float expected_pcm) {
CheckSamples(PushPcm(context, pcm.size() * sizeof(float)).at(0), {expected_pcm},
"gain modified stored PCM");
std::lock_guard lock(g_device_mutex);
Check(g_output_gains.size() == 1 && g_output_gains[0] == gain,
"incorrect per-channel gain forwarded to backend");
};
check_output({1.0f, 1.0f}, 0.25f);
for (const std::array<float, 2> gain: {std::array {2.0f, 0.5f}, {0.0f, 0.0f}, {1.0f, 1.0f}}) {
SetAttribute(port, 1, gain);
check_output(gain, 0.25f);
check_output(gain, 0.25f);
}
const std::array gain {0.5f, 0.25f};
SetAttribute(port, 1, gain);
std::fill(pcm.begin(), pcm.end(), 0.75f);
SetPcm(port, pcm.data());
check_output(gain, 0.75f);
std::fill(pcm.begin(), pcm.end(), -0.25f);
const Pcm replacement {pcm.data()};
for (const float invalid: {-1.0f, std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::quiet_NaN()}) {
const std::array invalid_gain {1.0f, invalid};
const Attribute attributes[] {{0, 0, &replacement, sizeof(replacement)},
{1, 0, &invalid_gain, sizeof(invalid_gain)}};
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(attributes), 2) ==
static_cast<int32_t>(0x80268001u), "invalid gain was accepted");
check_output(gain, 0.75f);
}
for (const Attribute attribute: {Attribute {1, 0, nullptr, sizeof(gain)},
Attribute {1, 0, &gain, sizeof(float)}}) {
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) ==
static_cast<int32_t>(0x80268001u), "malformed gain was accepted");
}
const Attribute duplicates[] {{0, 0, &replacement, sizeof(replacement)},
{1, 0, &gain, sizeof(gain)}, {1, 0, &gain, sizeof(gain)}};
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(duplicates), 3) ==
static_cast<int32_t>(0x80268001u), "duplicate gain attributes were accepted");
check_output(gain, 0.75f);
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestObjectGainAndAmbisonics() {
constexpr float half_power = 0.70710678f;
struct Coefficients { uint32_t channel; float left; float right; };
constexpr Coefficients cases[] {{UINT32_MAX, half_power, half_power}, {0, 1.0f, 1.0f},
{1, 0.0f, 0.0f}, {2, half_power, -half_power}, {3, 0.0f, 0.0f},
{64, half_power, half_power}, {65, half_power, -half_power},
{66, 0.0f, 0.0f}, {99, 0.0f, 0.0f}};
for (const auto format: {0x100u, 0x101u}) {
const auto context = CreateContext(1, 1);
auto param = MakeParam(format);
param.port_type = 0x100;
const auto object = CreatePort(context, param);
Check(LiveDeviceCount() == 1, "object-only context did not create one output device");
std::vector<float> floating(512, 0.5f);
std::vector<int16_t> integer(512, 16384);
const void* pcm = format == 0x100 ? static_cast<const void*>(floating.data()) : integer.data();
constexpr auto output_bytes = 512 * 2 * sizeof(float);
SetPcm(object, pcm);
std::fill(floating.begin(), floating.end(), 0.0f);
std::fill(integer.begin(), integer.end(), 0);
SetAttribute(object, 1, 0.0f);
SetAttribute(object, 8, 65u);
for (int push = 0; push < 2; push++) {
const auto output = PushPcm(context, output_bytes);
Check(output.size() == 1, "object-only push did not output exactly once");
CheckSamples(output[0], {0.5f * half_power, 0.5f * half_power},
"object PCM ownership, default gain, or attribute freeze failed");
}
std::fill(floating.begin(), floating.end(), 0.5f);
std::fill(integer.begin(), integer.end(), 16384);
for (const auto& test: cases) {
Check(AudioOut2::AudioOut2ContextAdvance(context) == OK, "context advance failed");
const Pcm input {pcm};
const float gain = 0.5f;
const Attribute attributes[] {{0, 0, &input, sizeof(input)}, {1, 0, &gain, sizeof(gain)},
{8, 0, &test.channel, sizeof(test.channel)}};
Check(AudioOut2::AudioOut2PortSetAttributes(object, AsAttribute(attributes), 3) == OK,
"object attributes could not be set atomically with PCM first");
for (int push = 0; push < 2; push++) {
CheckSamples(PushPcm(context, output_bytes).at(0), {0.25f * test.left, 0.25f * test.right},
"object gain or ambisonics decoded incorrectly or accumulated across pushes");
}
}
AudioOut2::AudioOut2ContextAdvance(context);
SetAttribute(object, 8, UINT32_MAX);
SetAttribute(object, 1, 0.0f);
SetPcm(object, pcm);
CheckSamples(PushPcm(context, output_bytes).at(0), {0.0f}, "zero gain did not mute object");
AudioOut2::AudioOut2ContextAdvance(context);
SetPcm(object, pcm);
CheckSamples(PushPcm(context, output_bytes).at(0), {0.0f}, "object gain did not persist");
AudioOut2::AudioOut2ContextDestroy(context);
Check(LiveDeviceCount() == 0, "object context leaked its output device");
}
}
void TestObjectBusRouting() {
const auto context = CreateContext(1, 2);
const auto other_context = CreateContext(1, 1);
AudioOut2::AudioOut2UserHandle other_user = 0;
Check(AudioOut2::AudioOut2UserCreate(2000, &other_user) == OK, "second user create failed");
const auto first = CreatePort(context, MakeParam());
const auto second = CreatePort(context, MakeParam());
std::vector<float> bed(512 * 2, 0.125f);
SetPcm(first, bed.data());
SetPcm(second, bed.data());
auto param = MakeParam(0x100);
param.port_type = 0x100;
std::vector<float> object_pcm(512, 0.25f);
SetPcm(CreatePort(context, param), object_pcm.data());
param.user_handle = other_user;
SetPcm(CreatePort(context, param), object_pcm.data());
std::fill(object_pcm.begin(), object_pcm.end(), 1.0f);
SetPcm(CreatePort(other_context, param), object_pcm.data());
constexpr auto bytes = 512 * 2 * sizeof(float);
for (int push = 0; push < 2; push++) {
const auto output = PushPcm(context, bytes);
Check(output.size() == 3, "object bus was missing or queued more than once");
CheckSamples(output[0], {0.125f}, "objects modified first bed");
CheckSamples(output[1], {0.125f}, "objects modified second bed");
CheckSamples(output[2], {0.35355339f}, "MAIN objects failed to mix across users or leaked contexts");
}
SetPcm(first, nullptr);
SetPcm(second, nullptr);
CheckSamples(PushPcm(context, bytes).at(0), {0.35355339f}, "object output depended on bed PCM");
AudioOut2::AudioOut2ContextDestroy(context);
Check(LiveDeviceCount() == 1, "context destroy closed another context's object device");
CheckSamples(PushPcm(other_context, bytes).at(0), {0.70710678f}, "other context lost its object PCM");
AudioOut2::AudioOut2ContextDestroy(other_context);
Check(AudioOut2::AudioOut2UserDestroy(other_user) == OK, "second user destroy failed");
Check(LiveDeviceCount() == 0, "object bus device leaked");
}
} // namespace
namespace Libs::Audio::AudioInternal {
@@ -467,10 +644,14 @@ bool AudioOutHasDevice(int handle) {
uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) {
std::lock_guard lock(g_device_mutex);
g_output_blocking.push_back(blocking);
g_output_gains.clear();
if (g_capture_bytes != 0) {
for (uint32_t i = 0; i < num; i++) {
const auto* bytes = static_cast<const uint8_t*>(params[i].data);
g_output_pcm.emplace_back(bytes, bytes + g_capture_bytes);
g_output_gains.push_back(params[i].gains != nullptr
? std::array {params[i].gains[0], params[i].gains[1]}
: std::array {1.0f, 1.0f});
}
}
return 0;
@@ -499,6 +680,9 @@ int main() {
TestAsynchronousDevicePushKeepsQueueBounded();
TestHandleWithoutPcmDoesNotBypassQueue();
TestPcmCopiedBeforeScratchBufferReuse();
TestPortGainAndValidation();
TestObjectGainAndAmbisonics();
TestObjectBusRouting();
Check(AudioOut2::AudioOut2UserDestroy(g_user_handle) == OK, "test user destroy failed");
std::printf("AudioOut2PortTests: all cases passed\n");
return 0;
+74 -3
View File
@@ -7,6 +7,8 @@
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <memory>
#include <vector>
@@ -22,6 +24,7 @@ struct Stream {
SDL_AudioSpec spec;
uint64_t frames = 0; // Millionths of a frame retain exact simulated consumption.
uint64_t updated;
std::vector<uint8_t> pcm;
};
uint64_t now = 1000000;
@@ -31,6 +34,7 @@ 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;
const float* queued_pad_gains = nullptr;
int clears = 0;
Stream& GetStream(SDL_AudioStream* stream) {
@@ -61,7 +65,7 @@ SDL_AudioStream* OpenAudioDeviceStream(SDL_AudioDeviceID, const SDL_AudioSpec* s
return nullptr;
}
streams.push_back(std::make_unique<Stream>(Stream {*spec, 0, now}));
streams.push_back(std::make_unique<Stream>(Stream {*spec, 0, now, {}}));
return reinterpret_cast<SDL_AudioStream*>(streams.back().get());
}
@@ -80,12 +84,14 @@ int GetAudioStreamQueued(SDL_AudioStream* stream) {
state.spec.channels);
}
bool PutAudioStreamData(SDL_AudioStream* stream, const void*, int bytes) {
bool PutAudioStreamData(SDL_AudioStream* stream, const void* data, int bytes) {
now += processing;
if (fail_put) {
return false;
}
auto& state = GetStream(stream);
const auto* pcm = static_cast<const uint8_t*>(data);
state.pcm.assign(pcm, pcm + bytes);
Drain(state);
state.frames += static_cast<uint64_t>(bytes) * 1000000 /
(SDL_AUDIO_BYTESIZE(state.spec.format) * state.spec.channels);
@@ -148,7 +154,8 @@ void Close(Stream*) {}
bool UsesBluetooth(const Stream* stream) {
return stream != nullptr && pad_bluetooth;
}
uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*, float) {
uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*, float, const float* gains) {
queued_pad_gains = gains;
return stream != nullptr ? pad_queue_us : 0;
}
} // namespace DualSenseHaptics
@@ -181,6 +188,7 @@ struct Fixture {
fail_open = fail_put = stalled = false;
pad_connected = pad_bluetooth = false;
pad_queue_us = 0;
queued_pad_gains = nullptr;
sleeps.clear();
}
@@ -386,6 +394,68 @@ void TestZeroOutputFrequency() {
"internal output open accepted zero frequency");
Check(f.Open().ToInt() == 1, "rejected output port occupied a handle");
}
template <typename T>
void CheckOutputSamples(std::initializer_list<T> expected, const char* message) {
const auto& pcm = streams.back()->pcm;
Check(pcm.size() == expected.size() * sizeof(T) &&
std::memcmp(pcm.data(), expected.begin(), pcm.size()) == 0, message);
}
void TestChannelGains() {
Fixture f;
const std::array pcm {0.25f, -0.5f, 0.75f, -1.0f};
const auto stereo = f.audio.AudioOutOpen(0, 2, 48000, Audio::Format::FloatStereo);
std::array gains {2.0f, 0.5f};
Audio::OutputParam output {stereo, pcm.data(), gains.data()};
for (int push = 0; push < 2; push++) {
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples({0.5f, -0.25f, 1.5f, -0.5f}, "float channel gains were not applied independently");
}
const int volume[] {16384, 32768};
f.audio.AudioOutSetVolume(stereo, 3, volume);
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples({0.25f, -0.25f, 0.75f, -0.5f}, "channel gain did not combine with port volume");
gains = {0.0f, 0.0f};
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples({0.0f, -0.0f, 0.0f, -0.0f}, "zero channel gains did not mute");
output.gains = nullptr;
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples({0.125f, -0.5f, 0.375f, -1.0f}, "channel scaling changed original float PCM");
const std::array<int16_t, 2> integer_pcm {20000, -20000};
const auto integer = f.audio.AudioOutOpen(0, 1, 48000, Audio::Format::Signed16bitStereo);
output = {integer, integer_pcm.data(), gains.data()};
for (const auto gain: {2.0f, std::numeric_limits<float>::max()}) {
gains = {gain, gain};
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples<int16_t>({32767, -32768}, "integer gain overflowed instead of saturating");
}
gains = {0.5f, 0.25f};
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples<int16_t>({10000, -5000}, "integer channel gains changed original PCM");
const std::array surround_pcm {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f};
for (const auto format: {Audio::Format::Float8Ch, Audio::Format::Float12Ch}) {
const auto surround = f.audio.AudioOutOpen(0, 1, 48000, format);
output = {surround, surround_pcm.data(), surround_pcm.data()};
f.audio.AudioOutOutputs(&output, 1, false);
if (format == Audio::Format::Float8Ch) {
CheckOutputSamples({1.0f, 4.0f, 9.0f, 16.0f, 49.0f, 64.0f, 25.0f, 36.0f},
"channel reordering used destination channel gains");
} else {
CheckOutputSamples({82.0f, 104.0f, 9.0f, 16.0f, 170.0f, 208.0f, 25.0f, 36.0f},
"height downmix lost per-channel gains");
}
}
pad_connected = true;
const auto speaker = f.Open(2, 4);
gains = {0.75f, 0.25f};
output = {speaker, pcm.data(), gains.data()};
f.audio.AudioOutOutputs(&output, 1, false);
Check(queued_pad_gains != nullptr && queued_pad_gains[0] == gains[0] && queued_pad_gains[1] == gains[1],
"connected controller did not receive channel gains");
}
} // namespace
int main() {
@@ -399,6 +469,7 @@ int main() {
TestControllerSpeakerPacing();
TestInvalidBatchSize();
TestZeroOutputFrequency();
TestChannelGains();
Check(streams.empty(), "output stream leaked");
std::puts("AudioOutTimingTests: all cases passed");
}
+31 -1
View File
@@ -857,6 +857,36 @@ void CheckAprPaths(const std::filesystem::path &root) {
ids[0] == 0xffffffffu && ids[1] == expected_id && sizes[0] == 0 && sizes[1] == 3,
"APR foreach reports a missing path and continues to the valid file");
// These paths share the old 31-bit FNV-1a ID (0x122d1544).
const char *collision_paths[] = {"perf-audit/test_00015ddf.bin",
"perf-audit/test_000389b8.bin"};
Check(std::filesystem::create_directory(root / "perf-audit"), "create APR collision directory");
for (size_t i = 0; i < 2; ++i) {
Check(fixture.Create(root / collision_paths[i]), "create APR collision fixture");
fixture.Write(i == 0 ? "APR" : "OTHER", i == 0 ? 3 : 5);
fixture.Close();
}
uint32_t collision_ids[2] = {};
Check(resolve("/app0/", collision_paths, 2, collision_ids, sizes, &error_index) == OK &&
collision_ids[0] != collision_ids[1] && collision_ids[0] != 0xffffffffu &&
collision_ids[1] != 0xffffffffu && sizes[0] == 3 && sizes[1] == 5,
"APR assigns distinct valid IDs to colliding paths");
const auto *stat_symbol = symbols.FindByNid("ApkYaHb8Sek", Loader::SymbolType::Func);
const auto *size_symbol = symbols.FindByNid("WvEu7yl3Ivg", Loader::SymbolType::Func);
Check(stat_symbol && size_symbol, "APR file metadata exports are registered");
using Stat = int (KYTY_SYSV_ABI *)(uint32_t, FileSystem::FileStat *);
using Size = int (KYTY_SYSV_ABI *)(uint32_t, uint64_t *);
for (size_t i = 0; i < 2; ++i) {
FileSystem::FileStat stat {};
uint64_t size = 0;
Check(resolve("/app0/", &collision_paths[i], 1, ids, nullptr, &error_index) == OK &&
ids[0] == collision_ids[i] &&
reinterpret_cast<Stat>(stat_symbol->vaddr)(ids[0], &stat) == OK &&
reinterpret_cast<Size>(size_symbol->vaddr)(ids[0], &size) == OK &&
stat.st_size == static_cast<int64_t>(sizes[i]) && size == sizes[i],
"APR re-resolution preserves each file's ID, host path and cached size");
}
// PATH_MAX includes NUL; all components remain below NAME_MAX (255).
std::string longest = "/app0/";
for (int i = 0; i < 3; ++i) {
@@ -880,7 +910,7 @@ void CheckAprPaths(const std::filesystem::path &root) {
Check(resolve(unterminated.data(), paths, 1, ids, sizes, &error_index) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_ENAMETOOLONG,
"APR rejects an unterminated prefix");
CheckAmprOrdering(symbols, expected_id);
CheckAmprOrdering(symbols, collision_ids[0]);
FileSystem::Umount("/app0");
}
+5
View File
@@ -414,11 +414,16 @@ void TestFormatsAndVolume() {
Check(opened_device == 10 && opened_spec.channels == 4 && opened_spec.format == SDL_AUDIO_F32,
"did not select quad DualSense PCM");
ExpectPcm({0, 0, 0.5f, -0.125f, 0, 0, 1.0f, 0});
const std::array gains {0.25f, 2.0f};
Haptics::Queue(port.get(), 1, stereo.data(), 2, 2, true, volume.data(), 1.0f, gains.data());
ExpectPcm({0, 0, 0.125f, -0.25f, 0, 0, 0.25f, 0});
const std::array<int16_t, 2> mono {16384, -32768};
Queue(port, mono.data(), 2, 1, false, volume.data() + 1);
ExpectPcm({0, 0, 0.25f, 0.25f, 0, 0, -0.5f, -0.5f});
Queue(port, mono.data(), 2, 1, false, volume.data() + 1, 0.5f);
ExpectPcm({0, 0, 0.125f, 0.125f, 0, 0, -0.25f, -0.25f});
Haptics::Queue(port.get(), 1, mono.data(), 2, 1, false, volume.data() + 1, 1.0f, gains.data());
ExpectPcm({0, 0, 0.0625f, 0.0625f, 0, 0, -0.125f, -0.125f});
std::array<float, 24> multichannel {};
multichannel.fill(0.9f);
multichannel[0] = 0.1f;
+301 -10
View File
@@ -19073,6 +19073,7 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
case Opcode::V_CMPX_NLT_F32:
case Opcode::V_CMP_CLASS_F32:
case Opcode::V_CMPX_CLASS_F32:
case Opcode::V_CMP_CLASS_F16:
case Opcode::V_CMPX_CLASS_F16:
case Opcode::V_CMP_LT_F16:
case Opcode::V_CMP_EQ_F16:
@@ -20590,16 +20591,23 @@ TestCase ScalarBrevB32PreservesScc() {
O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
TestCase ScalarSextI16Captured() {
TestCase ScalarSextMasks(u32 bits, u32 wave_size) {
using O = ShaderOpcode;
TestCase test;
test.name = "ScalarSextI16Captured";
constexpr u32 wave_size = 64, high = 0x80000001u;
test.initial = {0xdead0000u, 0xabcd7fffu, 0x12348000u, 0xabcfffffu};
test.name = bits == 8 ? (wave_size == 32 ? "ScalarSextI8MasksWave32"
: "ScalarSextI8MasksWave64")
: (wave_size == 32 ? "ScalarSextI16MasksWave32"
: "ScalarSextI16MasksWave64");
constexpr u32 high = 0x80000001u;
test.initial = bits == 8
? std::vector<u32>{0xdeadff00u, 0xabcdff7fu, 0x12340080u, 0xabcf00ffu}
: std::vector<u32>{0xdead0000u, 0xabcd7fffu, 0x12348000u, 0xabcfffffu};
const u32 extended_values[] = {0u, bits == 8 ? 0x7fu : 0x7fffu,
bits == 8 ? 0xffffff80u : 0xffff8000u, 0xffffffffu};
test.expected = test.initial;
auto &code = test.code;
for (u32 i = 0; i < 4u; ++i) {
const auto extended = static_cast<u32>(static_cast<int32_t>(static_cast<int16_t>(test.initial[i])));
const auto extended = extended_values[i];
for (const bool scc : {false, true}) {
AppendVMovU32(&code, 30, i * 4u);
AppendBufferLoadDword(&code, 3, 30);
@@ -20607,7 +20615,8 @@ TestCase ScalarSextI16Captured() {
AppendSMovLiteral(&code, 107, high);
code.push_back(EncodeSopc(0x06, InlineU32(scc ? 1u : 0u), InlineU32(1)));
code.push_back(EncodeSop1(0x04, 126, 128)); // Scalar operations execute with empty EXEC.
code.push_back(0xbeea1a0bu); // Captured s_sext_i32_i16 vcc_lo, s11.
// The I16 word is captured s_sext_i32_i16 vcc_lo, s11.
code.push_back(bits == 8 ? 0xbeea190bu : 0xbeea1a0bu);
code.push_back(EncodeSMovB32(20, 106));
code.push_back(EncodeSMovB32(21, 107));
code.push_back(EncodeSMovB32(22, 253));
@@ -20628,10 +20637,12 @@ TestCase ScalarSextI16Captured() {
AppendEnd(&code);
test.initial.resize(test.expected.size());
test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_READFIRSTLANE_B32, O::S_MOV_B32,
O::S_MOV_B64, O::S_CMP_EQ_U32, O::S_SEXT_I32_I16,
O::S_MOV_B64, O::S_CMP_EQ_U32,
bits == 8 ? O::S_SEXT_I32_I8 : O::S_SEXT_I32_I16,
O::V_CNDMASK_B32, O::V_MOV_B32, O::V_ADD_NC_U32,
O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.decoded_counts = {{"S_SEXT_I32_I16", 8u}};
test.decoded_counts = {{bits == 8 ? "S_SEXT_I32_I8 vcc_lo, s11"
: "S_SEXT_I32_I16 vcc_lo, s11", 8u}};
test.required_spirv = {"OpBitFieldSExtract"};
test.compute_info.threads_num[0] = wave_size;
test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1;
@@ -20641,6 +20652,38 @@ TestCase ScalarSextI16Captured() {
return test;
}
TestCase ScalarSextConstantsAndAliases() {
using O = ShaderOpcode;
TestCase test;
test.name = "ScalarSextConstantsAndAliases";
auto &code = test.code;
for (const u32 opcode : {0x19u, 0x1au}) {
const u32 literal = opcode == 0x19u ? 0x12345680u : 0x12348000u;
const u32 extended = opcode == 0x19u ? 0xffffff80u : 0xffff8000u;
for (const bool scc : {false, true}) {
code.push_back(EncodeSopc(0x06, InlineU32(scc ? 1u : 0u), InlineU32(1)));
code.push_back(EncodeSop1(opcode, 10, 255));
code.push_back(literal);
code.push_back(EncodeSop1(opcode, 10, 10)); // In-place source/destination.
code.push_back(EncodeSop1(opcode, 11, InlineU32(0)));
code.push_back(EncodeSop1(opcode, 12, InlineU32(64)));
code.push_back(EncodeSop1(opcode, 13, 193)); // Inline -1.
code.push_back(EncodeSMovB32(14, 253));
const u32 expected[] = {extended, 0, 64, 0xffffffffu, scc ? 1u : 0u};
for (u32 word = 0; word < 5u; ++word) {
AppendStoreSgpr(&code, 10u + word, test.expected.size());
test.expected.push_back(expected[word]);
}
}
}
AppendEnd(&code);
test.initial.resize(test.expected.size());
test.opcodes = {O::S_CMP_EQ_U32, O::S_SEXT_I32_I8, O::S_SEXT_I32_I16,
O::S_MOV_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.decoded_counts = {{"S_SEXT_I32_I8", 10u}, {"S_SEXT_I32_I16", 10u}};
return test;
}
TestCase ScalarBrevB64OperandsAndMasks(u32 wave_size) {
using O = ShaderOpcode;
TestCase test;
@@ -22051,6 +22094,80 @@ TestCase Vop1SdwaMovByteDestinations() {
return test;
}
TestCase Vop1SdwaMovByteSources(u32 wave_size) {
using O = ShaderOpcode;
// Select 0x80 from each byte position, with nonzero neighboring bytes.
constexpr std::array<u32, 4> sources{
0x12345680u, 0x12348056u, 0x12803456u, 0x80341256u};
// RDNA2 table 88 / PS5 SDWA-related options: PAD, SEXT, PRESERVE.
constexpr std::array<u32, 18> partial_expected{
0x00000080u, 0xffffff80u, 0xa1b2c380u,
0x00008000u, 0xffff8000u, 0xa1b280d4u,
0x00800000u, 0xff800000u, 0xa180c3d4u,
0x80000000u, 0x80000000u, 0x80b2c3d4u,
0x00000080u, 0x00000080u, 0xa1b20080u,
0x00800000u, 0x00800000u, 0x0080c3d4u};
constexpr std::array<u32, 6> sext_word_expected{
0x0000ff80u, 0xffffff80u, 0xa1b2ff80u,
0xff800000u, 0xff800000u, 0xff80c3d4u};
TestCase test;
test.name = wave_size == 64 ? "Vop1SdwaMovByteSourcesWave64"
: "Vop1SdwaMovByteSourcesWave32";
test.initial.assign(sources.begin(), sources.end());
test.expected = test.initial;
auto &code = test.code;
const auto store = [&](u32 expected) {
AppendStoreVgpr(&code, 1, static_cast<u32>(test.expected.size()));
test.expected.push_back(expected);
};
for (u32 src_sel = 0; src_sel < sources.size(); ++src_sel) {
AppendVMovU32(&code, 30, src_sel * sizeof(u32));
AppendBufferLoadDword(&code, 2, 30);
for (u32 sext = 0; sext < 2; ++sext) {
for (u32 dst_sel = 0; dst_sel < 6; ++dst_sel) {
for (u32 unused = 0; unused < 3; ++unused) {
AppendVMovLiteral(&code, 1, 0xa1b2c3d4u);
code.push_back(EncodeVop1(0x01, 1, 249));
code.push_back(EncodeVop1Sdwa(2, dst_sel, unused, src_sel, sext));
store(sext && dst_sel >= 4 ? sext_word_expected[(dst_sel - 4u) * 3u + unused]
: partial_expected[dst_sel * 3u + unused]);
}
}
}
}
// Aliasing must read the source byte before replacing the destination byte.
AppendVMovU32(&code, 30, 0);
AppendBufferLoadDword(&code, 1, 30);
code.push_back(EncodeVop1(0x01, 1, 249));
code.push_back(EncodeVop1Sdwa(1, 3, 2, 1));
store(0x56345680u);
// Scalar and inline sources use the same independent selectors.
AppendSMovLiteral(&code, 18, 0x12348056u);
code.push_back(EncodeVop1(0x01, 1, 249));
code.push_back(EncodeVop1Sdwa(18, 5, 0, 1, 1, 0, 0, 1));
store(0xff800000u);
AppendVMovLiteral(&code, 1, 0xa1b2c3d4u);
code.push_back(EncodeVop1(0x01, 1, 249));
code.push_back(EncodeVop1Sdwa(193, 0, 2, 3, 0, 0, 0, 1)); // Inline -1.
store(0xa1b2c3ffu);
for (u32 unused = 0; unused < 3; ++unused) {
AppendVMovLiteral(&code, 1, 0xa1b2c3d4u);
code.push_back(EncodeSMovB32(126, InlineU32(0)));
code.push_back(EncodeVop1(0x01, 1, 249));
code.push_back(EncodeVop1Sdwa(2, 4, unused, 0));
code.push_back(EncodeSMovB32(126, InlineU32(1)));
store(0xa1b2c3d4u);
}
AppendEnd(&code);
test.initial.resize(test.expected.size());
test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpBitFieldUExtract", "OpBitFieldSExtract", "OpBitFieldInsert"};
test.compute_info.wave_size = wave_size;
test.has_compute_info = true;
return test;
}
TestCase Vop2SdwaSubNcExactByte2Destination() {
using O = ShaderOpcode;
@@ -22223,6 +22340,74 @@ TestCase Vop2SdwaMulI24Destinations(u32 wave_size) {
return test;
}
TestCase Vop2SdwaMulU24Destinations(u32 wave_size) {
using O = ShaderOpcode;
// The high source bytes are discarded: 3 * 0xe080 = 0x2a180.
constexpr std::array<u32, 18> partial_expected{
0x00000080u, 0xffffff80u, 0xa1b2c380u,
0x00008000u, 0xffff8000u, 0xa1b280d4u,
0x00800000u, 0xff800000u, 0xa180c3d4u,
0x80000000u, 0x80000000u, 0x80b2c3d4u,
0x0000a180u, 0xffffa180u, 0xa1b2a180u,
0xa1800000u, 0xa1800000u, 0xa180c3d4u};
TestCase test;
test.name = wave_size == 64 ? "Vop2SdwaMulU24DestinationsWave64"
: "Vop2SdwaMulU24DestinationsWave32";
test.initial = {0xab000003u, 0xcd00e080u, 0xffffffffu};
test.expected = test.initial;
auto &code = test.code;
const auto store = [&](u32 reg, u32 expected) {
AppendStoreVgpr(&code, reg, static_cast<u32>(test.expected.size()));
test.expected.push_back(expected);
};
for (u32 i = 0; i < test.initial.size(); ++i) {
AppendVMovU32(&code, 30, i * sizeof(u32));
AppendBufferLoadDword(&code, 12u + i, 30);
}
for (u32 selector = 0; selector < 6; ++selector) {
for (u32 unused = 0; unused < 3; ++unused) {
AppendVMovLiteral(&code, 26, 0xa1b2c3d4u);
code.push_back(EncodeVop2(0x0b, 26, 249, 13));
code.push_back(EncodeVop2Sdwa(12, selector, unused));
store(26, partial_expected[selector * 3u + unused]);
}
}
// Full result still truncates the 48-bit unsigned product to 32 bits.
code.push_back(EncodeVop2(0x0b, 26, 249, 14));
code.push_back(EncodeVop2Sdwa(14));
store(26, 0xfe000001u);
// Either multiplicand may alias the partial destination.
code.push_back(EncodeVop2(0x0b, 12, 249, 13));
code.push_back(EncodeVop2Sdwa(12, 5, 2));
store(12, 0xa1800003u);
code.push_back(EncodeVop2(0x0b, 13, 249, 13));
code.push_back(EncodeVop2Sdwa(InlineU32(3), 2, 2, 6, 4,
0, 0, 0, 0, 0, 0, 1));
store(13, 0xcd80e080u);
// Source-byte sign extension happens before the unsigned 24-bit multiply.
AppendSMovLiteral(&code, 18, 0x12348056u);
code.push_back(EncodeVop2(0x0b, 26, 249, InlineU32(3)));
code.push_back(EncodeVop2Sdwa(18, 5, 0, 1, 6, 1, 0, 0, 0, 0, 0, 1, 1));
store(26, 0xfe800000u);
for (u32 unused = 0; unused < 3; ++unused) {
AppendVMovLiteral(&code, 26, 0xa1b2c3d4u);
code.push_back(EncodeSMovB32(126, InlineU32(0)));
code.push_back(EncodeVop2(0x0b, 26, 249, 13));
code.push_back(EncodeVop2Sdwa(12, 4, unused));
code.push_back(EncodeSMovB32(126, InlineU32(1)));
store(26, 0xa1b2c3d4u);
}
AppendEnd(&code);
test.initial.resize(test.expected.size());
test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD,
O::V_MUL_U32_U24, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.decoded_counts = {{"V_MUL_U32_U24", 25u}};
test.required_spirv = {"OpIMul", "OpBitFieldUExtract", "OpBitFieldSExtract"};
test.compute_info.wave_size = wave_size;
test.has_compute_info = true;
return test;
}
TestCase Vop2SdwaLshrrevCapturedByte1Source() {
using O = ShaderOpcode;
@@ -26426,6 +26611,79 @@ TestCase VectorVopcSdwaCmpxClassF32CapturedExecMask() {
return test;
}
TestCase VectorCmpClassF16(u32 wave_size) {
using O = ShaderOpcode;
constexpr std::array<u32, 10> values{
0x7c01u, 0x7e01u, 0xfc00u, 0xbc00u, 0x8001u,
0x8000u, 0x0000u, 0x0001u, 0x3c00u, 0x7c00u};
constexpr u32 guard = 0x87654321u;
TestCase test;
test.name = wave_size == 64 ? "VectorCmpClassF16Wave64" : "VectorCmpClassF16Wave32";
for (const auto value : values) {
test.initial.insert(test.initial.end(), {0xa55a0000u | value, (value << 16u) | 0x7e01u});
}
test.expected = test.initial;
auto &code = test.code;
const auto compare = [&](std::initializer_list<u32> words, u32 expected,
u32 dst = 106u, u32 exec = 1u) {
AppendSMovLiteral(&code, 106, 0x12345678u);
AppendSMovLiteral(&code, 107, guard);
AppendSMovLiteral(&code, dst + 1u, guard);
code.push_back(EncodeSopc(0x06, InlineU32(expected), InlineU32(0)));
code.insert(code.end(), words);
const u32 sources[] = {dst, dst + 1u, 126, 127, 253};
for (u32 i = 0; i < std::size(sources); ++i) {
code.push_back(EncodeSMovB32(30u + i, sources[i]));
}
code.push_back(EncodeSMovB32(126, InlineU32(1)));
const u32 results[] = {expected, wave_size == 32 ? guard : 0u, exec, 0u, 1u - expected};
for (u32 i = 0; i < std::size(results); ++i) {
AppendStoreSgpr(&code, 30u + i, static_cast<u32>(test.expected.size()));
test.expected.push_back(results[i]);
}
if (dst != 106u) {
AppendStoreSgpr(&code, 106, static_cast<u32>(test.expected.size()));
AppendStoreSgpr(&code, 107, static_cast<u32>(test.expected.size()) + 1u);
test.expected.insert(test.expected.end(), {0x12345678u, guard});
}
};
for (u32 index = 0; index < values.size(); ++index) {
AppendVMovU32(&code, 30, index * 8u);
AppendBufferLoadDword(&code, 0, 30);
AppendVMovU32(&code, 30, index * 8u + 4u);
AppendBufferLoadDword(&code, 2, 30);
for (u32 bit = 0; bit < 12; ++bit) {
AppendVMovU32(&code, 1, bit == 11 ? 0 : 1u << bit);
const u32 expected = bit == index ? 1u : 0u;
compare({EncodeVopc(0x8f, Vgpr(0), 1)}, expected);
compare({EncodeVop3Word0(0x8f, 20), EncodeVop3Word1(Vgpr(0), Vgpr(1))},
expected, 20);
}
// SDWA selects the high half, applies -abs, and writes an explicit SGPR pair.
compare({EncodeVopc(0x8f, 249, InlineU32(56)),
EncodeVopcSdwa(2, 20, 1, 5, 6, 0, 0, 1, 1, 0, 0, 0, 1)},
index >= 3 && index <= 8 ? 1u : 0u, 20);
compare({EncodeVop3Word0(0x8f, 20, 1),
EncodeVop3Word1(Vgpr(0), InlineU32(56), 0, 0, 1)},
index >= 3 && index <= 8 ? 1u : 0u, 20);
}
AppendVMovU32(&code, 1, 1u << 8u);
compare({EncodeVopc(0x8f, 242, 1)}, 1); // Inline 1.0 becomes half 0x3c00.
compare({EncodeVopc(0x8f, 255, 1), 0xdead3c00u}, 1); // Literal low half.
compare({EncodeVop3Word0(0x8f, 20), EncodeVop3Word1(242, 248)}, 1, 20);
// Inline INV_2PI supplies half bits 0x3118, including the positive-normal class.
code.push_back(EncodeSMovB32(126, InlineU32(0)));
compare({EncodeVopc(0x8f, 242, 1)}, 0, 106, 0); // Inactive lanes clear the result.
AppendEnd(&code);
test.initial.resize(test.expected.size());
test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::S_CMP_EQ_U32, O::BUFFER_LOAD_DWORD,
O::V_CMP_CLASS_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.decoded_counts = {{"V_CMP_CLASS_F16", 264u}};
test.compute_info.wave_size = wave_size;
test.has_compute_info = true;
return test;
}
TestCase VectorCmpxClassF16(u32 wave_size) {
using O = ShaderOpcode;
// One raw half value for each architectural class bit, in bit order.
@@ -35779,7 +36037,11 @@ std::vector<TestCase> MakeCases() {
AddCase(ScalarBitcmpB64DynamicOperands);
AddCase(ScalarBitcmpB64IntegerConstants);
AddCase(ScalarBrevB32PreservesScc);
AddCase(ScalarSextI16Captured);
for (const u32 bits : {8u, 16u}) {
cases.push_back(ScalarSextMasks(bits, 32));
cases.push_back(ScalarSextMasks(bits, 64));
}
AddCase(ScalarSextConstantsAndAliases);
cases.push_back(ScalarBrevB64OperandsAndMasks(32));
cases.push_back(ScalarBrevB64OperandsAndMasks(64));
AddCase(ScalarBfeI32CapturedRawSignExtends);
@@ -35817,6 +36079,8 @@ std::vector<TestCase> MakeCases() {
AddCase(Vop1SdwaNotPreservesHighWordDestination);
AddCase(Vop1SdwaNotPartialSourcesAndDestinations);
AddCase(Vop1SdwaMovByteDestinations);
cases.push_back(Vop1SdwaMovByteSources(32));
cases.push_back(Vop1SdwaMovByteSources(64));
AddCase(Vop2SdwaSubNcExactByte2Destination);
AddCase(Vop2SdwaAddNcCapturedHighWordDestination);
AddCase(Vop2SdwaAshrrevCapturedWord0SignExtends);
@@ -35824,6 +36088,8 @@ std::vector<TestCase> MakeCases() {
cases.push_back(Vop2SdwaMaxI32CapturedHighWord(64));
cases.push_back(Vop2SdwaMulI24Destinations(32));
cases.push_back(Vop2SdwaMulI24Destinations(64));
cases.push_back(Vop2SdwaMulU24Destinations(32));
cases.push_back(Vop2SdwaMulU24Destinations(64));
AddCase(Vop2SdwaLshrrevCapturedByte1Source);
AddCase(Vop2SdwaSubNcPreservesByteAndWordDestinations);
AddCase(Vop3CvtPkI16I32Captured);
@@ -35932,6 +36198,8 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorVop3CmpxNeI64CapturedExecMask);
AddCase(VectorCompareClassF32);
AddCase(VectorVopcSdwaCmpxClassF32CapturedExecMask);
cases.push_back(VectorCmpClassF16(32));
cases.push_back(VectorCmpClassF16(64));
cases.push_back(VectorCmpxClassF16(32));
cases.push_back(VectorCmpxClassF16(64));
AddCase(VectorCompareF16Ops);
@@ -41428,6 +41696,14 @@ int main(int argc, char **argv) {
RunCase(&vulkan, VectorVopcSdwaCmpxClassF32CapturedExecMask());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--class-f16-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, VectorCmpClassF16(32));
RunCase(&vulkan, VectorCmpClassF16(64));
RunCase(&vulkan, VectorCmpxClassF16(32));
RunCase(&vulkan, VectorCmpxClassF16(64));
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--cmpx-class-f16-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, VectorCmpxClassF16(32));
@@ -41524,7 +41800,11 @@ int main(int argc, char **argv) {
}
if (argc == 2 && std::strcmp(argv[1], "--scalar-sext-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, ScalarSextI16Captured());
for (const u32 bits : {8u, 16u}) {
RunCase(&vulkan, ScalarSextMasks(bits, 32));
RunCase(&vulkan, ScalarSextMasks(bits, 64));
}
RunCase(&vulkan, ScalarSextConstantsAndAliases());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--flat-d16-only") == 0) {
@@ -41801,6 +42081,8 @@ int main(int argc, char **argv) {
if (argc == 2 && std::strcmp(argv[1], "--sdwa-mov-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, Vop1SdwaMovByteDestinations());
RunCase(&vulkan, Vop1SdwaMovByteSources(32));
RunCase(&vulkan, Vop1SdwaMovByteSources(64));
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--sdwa-ffbh-only") == 0) {
@@ -41850,6 +42132,15 @@ int main(int argc, char **argv) {
RunCase(&vulkan, VectorIntegerOps());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--sdwa-mul-u24-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, Vop2SdwaMulU24Destinations(32));
RunCase(&vulkan, Vop2SdwaMulU24Destinations(64));
RunCase(&vulkan, Vop2SdwaMulI24Destinations(32));
RunCase(&vulkan, Vop2SdwaMulI24Destinations(64));
RunCase(&vulkan, VectorIntegerOps());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--cvt-pk-i16-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, Vop3CvtPkI16I32Captured());