mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-20 10:56:58 +02:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aa3b2cd57f | |||
| 4865dd92ca | |||
| 3c62359e05 | |||
| 62643c3424 | |||
| 210d4dcd99 | |||
| 5a8808086a | |||
| 861dd76d7f | |||
| db41d0482f | |||
| de62664908 | |||
| 34f6fd1f5f | |||
| 79a91ecfaf | |||
| 420bb51d6c | |||
| c86c8b4d93 | |||
| e43aadf75f | |||
| 9f25e3c23e | |||
| 7881c69a54 | |||
| 9e666e4dd2 |
@@ -60,7 +60,9 @@ private:
|
||||
return GetCurrentThreadId();
|
||||
#elif defined(__APPLE__)
|
||||
// mach thread port is a nonzero per-thread id (0 is the "no owner" sentinel).
|
||||
return static_cast<uint32_t>(pthread_mach_thread_np(pthread_self()));
|
||||
static thread_local const uint32_t tid =
|
||||
static_cast<uint32_t>(pthread_mach_thread_np(pthread_self()));
|
||||
return tid;
|
||||
#elif defined(__linux__)
|
||||
static thread_local const uint32_t tid = static_cast<uint32_t>(::syscall(SYS_gettid));
|
||||
return tid;
|
||||
|
||||
@@ -1284,10 +1284,10 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
|
||||
: ResolveDrawOffsets(ucfg.GetIndexOffset(), state.vertex_info[0]);
|
||||
|
||||
DrawEmitInfo emit {};
|
||||
// Native mesh prologs consume the offsets written to the packet's user SGPRs.
|
||||
const bool mesh_indirect = indirect && state.vertex_info[0].logical_stage == ShaderType::Mesh;
|
||||
emit.vertex_offset = mesh_indirect ? 0 : vertex_offset + args.base_vertex;
|
||||
emit.first_instance = mesh_indirect ? 0 : instance_offset;
|
||||
// Native fetches consume the patched SGPRs; rewritten fetches need Vulkan offsets.
|
||||
const bool native_indirect = indirect && !state.vertex_info[0].fetch_embedded;
|
||||
emit.vertex_offset = native_indirect ? 0 : vertex_offset + args.base_vertex;
|
||||
emit.first_instance = native_indirect ? 0 : instance_offset;
|
||||
|
||||
ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source,
|
||||
primitive_restart);
|
||||
@@ -1375,10 +1375,10 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
|
||||
indirect ? std::pair<int32_t, uint32_t> {0, args.first_instance}
|
||||
: ResolveDrawOffsets(ucfg.GetIndexOffset(), state.vertex_info[0]);
|
||||
DrawEmitInfo emit {};
|
||||
const bool mesh_indirect = indirect && state.vertex_info[0].logical_stage == ShaderType::Mesh;
|
||||
emit.first_vertex = mesh_indirect ? 0 :
|
||||
const bool native_indirect = indirect && !state.vertex_info[0].fetch_embedded;
|
||||
emit.first_vertex = native_indirect ? 0 :
|
||||
static_cast<uint32_t>(vertex_offset + static_cast<int32_t>(args.first_vertex));
|
||||
emit.first_instance = mesh_indirect ? 0 : instance_offset;
|
||||
emit.first_instance = native_indirect ? 0 : instance_offset;
|
||||
|
||||
DrawIndexBufferSource index_source {};
|
||||
ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source, false);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+30
-15
@@ -70,6 +70,7 @@ struct File {
|
||||
std::atomic_bool writable;
|
||||
std::atomic_bool append;
|
||||
std::atomic_bool sync_writes;
|
||||
std::atomic_bool close_on_exec;
|
||||
SpecialFile special;
|
||||
Common::Mutex mutex;
|
||||
std::vector<uint8_t> dirents;
|
||||
@@ -449,11 +450,12 @@ int KYTY_SYSV_ABI KernelOpen(const char* path, int flags, uint16_t mode) {
|
||||
|
||||
EXIT_IF(file == nullptr || file->opened || file->directory);
|
||||
|
||||
file->name = path;
|
||||
file->readable = rw_mode != Common::File::Mode::Write;
|
||||
file->writable = rw_mode != Common::File::Mode::Read;
|
||||
file->append = append;
|
||||
file->sync_writes = fsync || sync || dsync;
|
||||
file->name = path;
|
||||
file->readable = rw_mode != Common::File::Mode::Write;
|
||||
file->writable = rw_mode != Common::File::Mode::Read;
|
||||
file->append = append;
|
||||
file->sync_writes = fsync || sync || dsync;
|
||||
file->close_on_exec = (static_cast<uint32_t>(flags) & 0x00100000u) != 0;
|
||||
|
||||
if (!directory && IsRandomDevice(file->name)) {
|
||||
file->real_name = file->name;
|
||||
@@ -585,6 +587,27 @@ int KYTY_SYSV_ABI KernelClose(int d) {
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelFcntl(int d, int command, int arg) {
|
||||
PRINT_NAME();
|
||||
|
||||
if (d < DESCRIPTOR_MIN) {
|
||||
return KERNEL_ERROR_EBADF;
|
||||
}
|
||||
|
||||
auto* file = g_files->GetFile(d);
|
||||
if (file == nullptr || !file->opened) {
|
||||
return KERNEL_ERROR_EBADF;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 1: return file->close_on_exec.load(std::memory_order_relaxed); // F_GETFD
|
||||
case 2: // F_SETFD, FD_CLOEXEC
|
||||
file->close_on_exec.store((arg & 1) != 0, std::memory_order_relaxed);
|
||||
return OK;
|
||||
default: return KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
int64_t KYTY_SYSV_ABI KernelRead(int d, void* buf, size_t nbytes) {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -1138,19 +1161,11 @@ int KYTY_SYSV_ABI KernelFstat(int d, FileStat* sb) {
|
||||
auto wt = at;
|
||||
|
||||
if (file->special != SpecialFile::None) {
|
||||
stat.st_mode = 0020644u;
|
||||
stat.st_size = 0;
|
||||
stat.st_blksize = 512;
|
||||
stat.st_blocks = 0;
|
||||
SecToTimespec(&stat.st_atim, at.ToUnix());
|
||||
SecToTimespec(&stat.st_mtim, wt.ToUnix());
|
||||
stat.st_ctim = stat.st_atim;
|
||||
stat.st_birthtim = stat.st_mtim;
|
||||
*sb = stat;
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
if (!file->directory) {
|
||||
} else if (!file->directory) {
|
||||
file->mutex.Lock();
|
||||
|
||||
bool is_invalid = file->f.IsInvalid();
|
||||
|
||||
@@ -59,6 +59,7 @@ std::filesystem::path GetRealFilename(const std::string& mounted_file_name);
|
||||
|
||||
int KYTY_SYSV_ABI KernelOpen(const char* path, int flags, uint16_t mode);
|
||||
int KYTY_SYSV_ABI KernelClose(int d);
|
||||
int KYTY_SYSV_ABI KernelFcntl(int d, int command, int arg);
|
||||
int64_t KYTY_SYSV_ABI KernelRead(int d, void* buf, size_t nbytes);
|
||||
int64_t KYTY_SYSV_ABI KernelPread(int d, void* buf, size_t nbytes, int64_t offset);
|
||||
int64_t KYTY_SYSV_ABI KernelPreadv(int d, const KernelIovec* iov, int iovcnt, int64_t offset);
|
||||
|
||||
@@ -3217,6 +3217,14 @@ uint32_t KYTY_SYSV_ABI AgcAcbJumpGetSize() {
|
||||
return 0x10u;
|
||||
}
|
||||
|
||||
uint32_t* KYTY_SYSV_ABI AgcAcbRewind(CommandBuffer* buf, uint8_t initial_state, uint8_t offload) {
|
||||
auto* cmd = AgcDcbRewind(buf, initial_state);
|
||||
if (cmd != nullptr) {
|
||||
cmd[1] |= (static_cast<uint32_t>(offload & 0x1u) << 24u);
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
uint32_t* KYTY_SYSV_ABI AgcAcbWaitRegMem(CommandBuffer* buf, uint8_t size, uint8_t compare_function,
|
||||
uint8_t cache_policy, const volatile void* address,
|
||||
uint64_t reference, uint64_t mask, uint32_t poll_cycles) {
|
||||
|
||||
@@ -206,6 +206,7 @@ uint32_t KYTY_SYSV_ABI AgcAcbCondExecGetSize();
|
||||
uint32_t* KYTY_SYSV_ABI AgcAcbJump(CommandBuffer* buf, uint8_t cache_policy,
|
||||
const uint32_t* target, uint32_t size_in_dwords);
|
||||
uint32_t KYTY_SYSV_ABI AgcAcbJumpGetSize();
|
||||
uint32_t* KYTY_SYSV_ABI AgcAcbRewind(CommandBuffer* buf, uint8_t initial_state, uint8_t offload);
|
||||
uint32_t* KYTY_SYSV_ABI AgcAcbWaitRegMem(CommandBuffer* buf, uint8_t size, uint8_t compare_function,
|
||||
uint8_t cache_policy, const volatile void* address,
|
||||
uint64_t reference, uint64_t mask, uint32_t poll_cycles);
|
||||
|
||||
+25
-29
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -67,8 +67,13 @@ struct PadTriggerEffectParam {
|
||||
PadTriggerEffectCommand command[2];
|
||||
};
|
||||
|
||||
struct PadTriggerEffectStateInformation {
|
||||
int32_t state[2];
|
||||
};
|
||||
|
||||
static_assert(sizeof(PadTriggerEffectCommand) == 56);
|
||||
static_assert(sizeof(PadTriggerEffectParam) == 120);
|
||||
static_assert(sizeof(PadTriggerEffectStateInformation) == 8);
|
||||
|
||||
struct DualSenseEffects {
|
||||
uint8_t enable_bits;
|
||||
@@ -118,6 +123,7 @@ public:
|
||||
int GetActiveControllerId();
|
||||
void SetLightBar(uint8_t r, uint8_t g, uint8_t b);
|
||||
bool SetTriggerEffect(const PadTriggerEffectParam& param);
|
||||
void GetTriggerEffectState(PadTriggerEffectStateInformation* info);
|
||||
void CycleSetting(Setting setting);
|
||||
float GetSettingScale(Setting setting) const;
|
||||
void ReadState(ControllerState* state, bool* flag, int* count);
|
||||
@@ -131,6 +137,7 @@ private:
|
||||
// Apply cached output without changing its game-requested lifetime; caller holds m_mutex.
|
||||
void ApplyVibration();
|
||||
bool SendTriggerEffect(const PadTriggerEffectParam& param);
|
||||
void UpdateTriggerEffectState();
|
||||
|
||||
Common::Mutex m_mutex;
|
||||
std::vector<int> m_connected_ids;
|
||||
@@ -152,6 +159,7 @@ private:
|
||||
std::array<uint8_t, 2> m_vibration {};
|
||||
uint64_t m_vibration_until = 0;
|
||||
PadTriggerEffectParam m_trigger_effect {};
|
||||
PadTriggerEffectStateInformation m_trigger_state {};
|
||||
// Setting changes share the output lock; audio only needs an atomic scale snapshot.
|
||||
std::array<std::atomic<uint32_t>, 3> m_setting_steps {};
|
||||
};
|
||||
@@ -330,6 +338,51 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command,
|
||||
}
|
||||
}
|
||||
|
||||
enum TriggerState : int32_t {
|
||||
TRIGGER_STATE_OFF = 0,
|
||||
TRIGGER_STATE_FEEDBACK_STANDBY = 1,
|
||||
TRIGGER_STATE_FEEDBACK_ACTIVE = 2,
|
||||
TRIGGER_STATE_WEAPON_STANDBY = 3,
|
||||
TRIGGER_STATE_WEAPON_PULLING = 4,
|
||||
TRIGGER_STATE_WEAPON_FIRED = 5,
|
||||
TRIGGER_STATE_VIBRATION_STANDBY = 6,
|
||||
TRIGGER_STATE_VIBRATION_ACTIVE = 7,
|
||||
};
|
||||
|
||||
static int32_t TriggerEffectState(const PadTriggerEffectCommand& command, int value,
|
||||
int32_t previous) {
|
||||
// Trigger positions use nonuniform travel ranges, with endpoints at 0 and 255.
|
||||
const int pos = value == 0 ? 0 : value == 255 ? 9 : 1 + value / 32;
|
||||
switch (command.mode) {
|
||||
case 1: // feedback: resistance from position data[0], strength data[1]
|
||||
return command.data[1] != 0 && pos >= command.data[0] ? TRIGGER_STATE_FEEDBACK_ACTIVE
|
||||
: TRIGGER_STATE_FEEDBACK_STANDBY;
|
||||
case 2: // weapon: resistance from data[0], released at data[1]
|
||||
if (value < command.data[0] * 32) {
|
||||
return TRIGGER_STATE_WEAPON_STANDBY;
|
||||
}
|
||||
return previous == TRIGGER_STATE_WEAPON_FIRED ||
|
||||
value >= std::min(command.data[1] * 32, 254)
|
||||
? TRIGGER_STATE_WEAPON_FIRED
|
||||
: TRIGGER_STATE_WEAPON_PULLING;
|
||||
case 3: // vibration from position data[0], amplitude data[1], frequency data[2]
|
||||
return command.data[1] != 0 && command.data[2] != 0 && pos >= command.data[0]
|
||||
? TRIGGER_STATE_VIBRATION_ACTIVE
|
||||
: TRIGGER_STATE_VIBRATION_STANDBY;
|
||||
case 4: // multiple-position feedback: strength per position
|
||||
return command.data[pos] != 0 ? TRIGGER_STATE_FEEDBACK_ACTIVE
|
||||
: TRIGGER_STATE_FEEDBACK_STANDBY;
|
||||
case 5: // slope feedback between data[0] and data[1]
|
||||
return pos >= command.data[0] && pos < command.data[1] ? TRIGGER_STATE_FEEDBACK_ACTIVE
|
||||
: TRIGGER_STATE_FEEDBACK_STANDBY;
|
||||
case 6: // multiple-position vibration: frequency data[0], amplitude per position
|
||||
return command.data[0] != 0 && command.data[1 + pos] != 0
|
||||
? TRIGGER_STATE_VIBRATION_ACTIVE
|
||||
: TRIGGER_STATE_VIBRATION_STANDBY;
|
||||
default: return TRIGGER_STATE_OFF;
|
||||
}
|
||||
}
|
||||
|
||||
void Initialize() {
|
||||
EXIT_IF(g_controller != nullptr);
|
||||
|
||||
@@ -419,6 +472,7 @@ void GameController::CheckActive() {
|
||||
m_vibration = {};
|
||||
m_vibration_until = 0;
|
||||
m_trigger_effect = {};
|
||||
m_trigger_state = {};
|
||||
}
|
||||
|
||||
void GameController::AddState() {
|
||||
@@ -466,6 +520,7 @@ void GameController::Axis(int id, Controller::Axis axis, int value) {
|
||||
}
|
||||
if (trigger != 0) {
|
||||
m_state.buttons = value > 0 ? m_state.buttons | trigger : m_state.buttons & ~trigger;
|
||||
UpdateTriggerEffectState();
|
||||
}
|
||||
|
||||
AddState();
|
||||
@@ -622,6 +677,7 @@ void GameController::ResetInputState() {
|
||||
m_state.buttons = 0;
|
||||
std::fill_n(m_state.axes, 4, 128);
|
||||
std::fill_n(m_state.axes + 4, 2, 0);
|
||||
UpdateTriggerEffectState();
|
||||
for (auto& touch: m_state.touch) {
|
||||
touch = {};
|
||||
}
|
||||
@@ -730,9 +786,25 @@ bool GameController::SetTriggerEffect(const PadTriggerEffectParam& param) {
|
||||
m_trigger_effect.command[i] = param.command[i];
|
||||
}
|
||||
}
|
||||
UpdateTriggerEffectState();
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameController::GetTriggerEffectState(PadTriggerEffectStateInformation* info) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
*info = m_trigger_state;
|
||||
}
|
||||
|
||||
void GameController::UpdateTriggerEffectState() {
|
||||
// Track every input event so a weapon remains fired even when the game polls after release
|
||||
// starts.
|
||||
for (int i = 0; i < 2; i++) {
|
||||
m_trigger_state.state[i] = TriggerEffectState(
|
||||
m_trigger_effect.command[i], m_state.axes[static_cast<int>(Axis::TriggerLeft) + i],
|
||||
m_trigger_state.state[i]);
|
||||
}
|
||||
}
|
||||
|
||||
bool GameController::SendTriggerEffect(const PadTriggerEffectParam& param) {
|
||||
if ((param.trigger_mask & ~0x03u) != 0) {
|
||||
return false;
|
||||
@@ -1098,4 +1170,21 @@ int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* p
|
||||
return g_controller->SetTriggerEffect(*param) ? OK : PAD_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInformation* info) {
|
||||
PRINT_NAME();
|
||||
|
||||
LOGF("\t handle = %d\n", handle);
|
||||
|
||||
if (handle != 1) {
|
||||
return PAD_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
if (info == nullptr) {
|
||||
return PAD_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
g_controller->GetTriggerEffectState(info);
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
} // namespace Libs::Controller
|
||||
|
||||
@@ -53,6 +53,7 @@ struct PadData;
|
||||
struct PadVibrationParam;
|
||||
struct PadLightBarParam;
|
||||
struct PadTriggerEffectParam;
|
||||
struct PadTriggerEffectStateInformation;
|
||||
|
||||
inline int controller_get_axis(int min, int max, int value) {
|
||||
int v = (255 * (value - min)) / (max - min);
|
||||
@@ -92,6 +93,7 @@ int KYTY_SYSV_ABI PadReadState(int handle, PadData* data);
|
||||
int KYTY_SYSV_ABI PadRead(int handle, PadData* data, int num);
|
||||
int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param);
|
||||
int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* param);
|
||||
int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInformation* info);
|
||||
int KYTY_SYSV_ABI PadResetLightBar(int handle);
|
||||
int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -74,6 +74,7 @@ LIB_DEFINE(InitAgcDriver_1) {
|
||||
LIB_FUNC("ozKzBP4aki4", Gen5::AgcAcbCondExecGetSize);
|
||||
LIB_FUNC("e1DFTg+Sd8U", Gen5::AgcAcbJump);
|
||||
LIB_FUNC("b-oySn+G2tE", Gen5::AgcAcbJumpGetSize);
|
||||
LIB_FUNC("DwICrVxerkY", Gen5::AgcAcbRewind);
|
||||
LIB_FUNC("htn36gPnBk4", Gen5::AgcAcbWaitRegMem);
|
||||
LIB_FUNC("idlaArvdXEs", Gen5::AgcAcbWaitOnAddressGetSize);
|
||||
LIB_FUNC("-RnpfpxIhec", Gen5::AgcAcbDmaData);
|
||||
@@ -156,6 +157,7 @@ LIB_DEFINE(InitAgcDriver_1) {
|
||||
LIB_FUNC("k-JpyR2dYAM", Gen5::AgcCondExecPatchSetEnd);
|
||||
LIB_FUNC("3ZWa3AoyWZQ", Gen5::AgcCondExecPatchSetCommandAddress);
|
||||
LIB_FUNC("ziVA3whp3p4", Gen5::AgcRewindPatchSetRewindState);
|
||||
LIB_FUNC("eWaWyFegzgQ", Gen5::AgcRewindPatchSetRewindState);
|
||||
LIB_FUNC("YUeqkyT7mEQ", Gen5::AgcDcbSetFlip);
|
||||
}
|
||||
|
||||
|
||||
+60
-43
@@ -1,4 +1,5 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/dateTime.h"
|
||||
#include "common/file.h"
|
||||
#include "common/logging/log.h"
|
||||
@@ -52,6 +53,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 +94,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 +113,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 +194,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 +212,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 +227,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) {
|
||||
@@ -665,10 +650,15 @@ struct CommandBufferState {
|
||||
uint64_t address = 0;
|
||||
uint64_t value = 0;
|
||||
};
|
||||
struct WaitAddressCommand {
|
||||
uint64_t address;
|
||||
struct WaitCommand {
|
||||
uint64_t source;
|
||||
uint64_t value;
|
||||
uint8_t compare;
|
||||
bool is_counter;
|
||||
};
|
||||
struct WriteCounterCommand {
|
||||
uint8_t index;
|
||||
uint32_t value;
|
||||
};
|
||||
struct AmmMapCommand {
|
||||
AmmCommandKind kind = AmmCommandKind::MapAuto;
|
||||
@@ -680,7 +670,7 @@ struct CommandBufferState {
|
||||
uint8_t gpu_mask_id = 0;
|
||||
};
|
||||
using CommandData = std::variant<ReadFileCommand, KernelEventCommand, WriteAddressCommand,
|
||||
AmmMapCommand, WaitAddressCommand>;
|
||||
AmmMapCommand, WaitCommand, WriteCounterCommand>;
|
||||
struct Command {
|
||||
uint64_t record_offset;
|
||||
CommandData data;
|
||||
@@ -705,6 +695,8 @@ static_assert(sizeof(AmmUsageStatsData) == 0x18);
|
||||
static std::mutex g_command_buffer_mutex;
|
||||
static std::unordered_map<uint64_t, CommandBufferState> g_command_buffers;
|
||||
static std::unordered_map<uint64_t, uint64_t> g_command_buffer_aliases;
|
||||
// AMM and APR share 256 32-bit counters; only the first 128 are application-writable.
|
||||
static std::array<std::atomic<uint32_t>, 256> g_counters {};
|
||||
using CommandBufferIterator = std::unordered_map<uint64_t, CommandBufferState>::iterator;
|
||||
|
||||
static void RegisterCommandBufferAliasLocked(uint64_t command_buffer, uint64_t buffer) {
|
||||
@@ -1207,6 +1199,9 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
|
||||
const auto& command = *payload;
|
||||
std::atomic_ref(*reinterpret_cast<uint64_t*>(command.address))
|
||||
.store(command.value, std::memory_order_release);
|
||||
} else if (const auto* command =
|
||||
std::get_if<CommandBufferState::WriteCounterCommand>(&entry.data)) {
|
||||
g_counters[command->index].store(command->value, std::memory_order_release);
|
||||
} else if (const auto* payload = std::get_if<CommandBufferState::AmmMapCommand>(&entry.data)) {
|
||||
const auto& command = *payload;
|
||||
int result = OK;
|
||||
@@ -1235,17 +1230,20 @@ struct Submission {
|
||||
uint32_t id = 0;
|
||||
};
|
||||
|
||||
static bool IsWaitSatisfied(const CommandBufferState::WaitAddressCommand& wait) {
|
||||
const uint64_t value =
|
||||
std::atomic_ref(*reinterpret_cast<uint64_t*>(wait.address)).load(std::memory_order_acquire);
|
||||
static bool IsWaitSatisfied(const CommandBufferState::WaitCommand& wait) {
|
||||
const uint64_t value = wait.is_counter
|
||||
? g_counters[wait.source].load(std::memory_order_acquire)
|
||||
: std::atomic_ref(*reinterpret_cast<uint64_t*>(wait.source))
|
||||
.load(std::memory_order_acquire);
|
||||
const uint64_t sign = uint64_t {1} << (wait.is_counter ? 31 : 63);
|
||||
switch (wait.compare) {
|
||||
case 0: return value == wait.value;
|
||||
case 1: return value > wait.value;
|
||||
case 2: return value < wait.value;
|
||||
case 3: return value != wait.value;
|
||||
case 4: return static_cast<int64_t>(value - wait.value) >= 0;
|
||||
case 5: return static_cast<int64_t>(value) > static_cast<int64_t>(wait.value);
|
||||
default: return static_cast<int64_t>(value) < static_cast<int64_t>(wait.value);
|
||||
case 4: return ((value - wait.value) & sign) == 0;
|
||||
case 5: return (value ^ sign) > (wait.value ^ sign);
|
||||
default: return (value ^ sign) < (wait.value ^ sign);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1279,7 +1277,7 @@ private:
|
||||
pending = true;
|
||||
auto& submission = queue.front();
|
||||
if (submission.cursor < submission.commands.size()) {
|
||||
const auto* wait = std::get_if<CommandBufferState::WaitAddressCommand>(
|
||||
const auto* wait = std::get_if<CommandBufferState::WaitCommand>(
|
||||
&submission.commands[submission.cursor].data);
|
||||
if (wait != nullptr && !IsWaitSatisfied(*wait)) {
|
||||
continue;
|
||||
@@ -1927,18 +1925,27 @@ static int KYTY_SYSV_ABI CommandBufferWaitOnAddress(void* command_b
|
||||
return LibKernel::KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
return AppendCommandRecord(reinterpret_cast<uint64_t>(command_buffer),
|
||||
CommandBufferState::WaitAddressCommand {
|
||||
reinterpret_cast<uint64_t>(address), value, compare},
|
||||
CommandBufferState::WaitCommand {
|
||||
reinterpret_cast<uint64_t>(address), value, compare, false},
|
||||
0x20)
|
||||
? OK
|
||||
: LibKernel::KERNEL_ERROR_EBUSY;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWaitOnCounter(void* command_buffer, uint8_t, uint8_t,
|
||||
uint64_t, uint8_t, uint8_t, uint64_t, uint8_t) {
|
||||
static int KYTY_SYSV_ABI CommandBufferWaitOnCounter(void* command_buffer, uint8_t index,
|
||||
uint8_t access, uint64_t value, uint8_t compare,
|
||||
uint8_t mask_op, uint64_t, uint8_t flush) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendNoOpCommand(command_buffer, 0x20);
|
||||
if (command_buffer == nullptr || access > 7 || compare > 6 || mask_op > 1 || flush > 1) {
|
||||
return LibKernel::KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(access != 1 || mask_op != 0 || flush != 0 || value > UINT32_MAX);
|
||||
return AppendCommandRecord(reinterpret_cast<uint64_t>(command_buffer),
|
||||
CommandBufferState::WaitCommand {index, value, compare, true},
|
||||
0x20)
|
||||
? OK
|
||||
: LibKernel::KERNEL_ERROR_EBUSY;
|
||||
}
|
||||
|
||||
static int AppendWriteAddressCommand(void* command_buffer, volatile uint64_t* address,
|
||||
@@ -1970,11 +1977,21 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressOnCompletion(void*
|
||||
return AppendWriteAddressCommand(command_buffer, address, value);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteCounter(void* command_buffer, uint8_t, uint8_t, uint64_t,
|
||||
uint8_t, uint32_t) {
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteCounter(void* command_buffer, uint8_t index,
|
||||
uint8_t access, uint64_t value,
|
||||
uint8_t operation, uint32_t mode) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendNoOpCommand(command_buffer, 0x20);
|
||||
if (command_buffer == nullptr || index >= 128 || access > 7 || operation > 4 || mode > 1) {
|
||||
return LibKernel::KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(access != 1 || operation != 0 || mode != 0);
|
||||
return AppendCommandRecord(reinterpret_cast<uint64_t>(command_buffer),
|
||||
CommandBufferState::WriteCounterCommand {
|
||||
index, static_cast<uint32_t>(value)},
|
||||
0x20)
|
||||
? OK
|
||||
: LibKernel::KERNEL_ERROR_EBUSY;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteCounterOnCompletion(void* command_buffer, uint8_t,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "common/stringUtils.h"
|
||||
#include "libs/errno.h"
|
||||
#include "libs/libs.h"
|
||||
#include "libs/systemService.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
@@ -96,6 +97,7 @@ int KYTY_SYSV_ABI AppContentInitialize(const AppContentInitParam* init_param,
|
||||
LOGF("\t TITLE_ID missing\n");
|
||||
}
|
||||
|
||||
SystemService::NotifyEntitlementUpdate();
|
||||
return OK;
|
||||
}
|
||||
|
||||
|
||||
+219
-100
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1974,6 +1974,12 @@ int64_t KYTY_SYSV_ABI fstat(int d, LibKernel::FileSystem::FileStat* sb) {
|
||||
return POSIX_N_CALL(LibKernel::FileSystem::KernelFstat(d, sb));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI fcntl(int d, int command, int arg) {
|
||||
PRINT_NAME();
|
||||
|
||||
return POSIX_N_CALL(LibKernel::FileSystem::KernelFcntl(d, command, arg));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI ftruncate(int d, int64_t length) {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -2163,6 +2169,7 @@ LIB_DEFINE(InitLibKernel_1_Posix) {
|
||||
LIB_FUNC("E6ao34wPw+U", stat);
|
||||
LIB_FUNC("JGMio+21L4c", mkdir);
|
||||
LIB_FUNC("ih4CD9-gghM", Posix::ftruncate);
|
||||
LIB_FUNC("8nY19bKoiZk", Posix::fcntl);
|
||||
LIB_FUNC("pDuPEf3m4fI", Posix::sem_init);
|
||||
LIB_FUNC("cDW233RAwWo", Posix::sem_destroy);
|
||||
LIB_FUNC("YCV5dGGBcCo", Posix::sem_wait);
|
||||
|
||||
@@ -1559,6 +1559,7 @@ LIB_DEFINE(InitNet_1_NpManager) {
|
||||
LIB_FUNC("rbknaUjpqWo", NpManager::NpGetAccountIdA);
|
||||
LIB_FUNC("JT+t00a3TxA", NpManager::NpGetAccountCountryA);
|
||||
LIB_FUNC("+4DegjBqV1g", NpManager::NpGetAccountAge);
|
||||
LIB_FUNC("3Tcz5bNCfZQ", NpManager::NpGetAccountLanguage2);
|
||||
LIB_FUNC("GpLQDNKICac", NpManager::NpCreateRequest);
|
||||
LIB_FUNC("eiqMCt9UshI", NpManager::NpCreateAsyncRequest);
|
||||
LIB_FUNC("S7QTn72PrDw", NpManager::NpDeleteRequest);
|
||||
|
||||
+1
-21
@@ -22,26 +22,6 @@ static int KYTY_SYSV_ABI PadSetVibrationMode(int handle, int mode) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct PadTriggerEffectStateInformation {
|
||||
int32_t state[2];
|
||||
};
|
||||
|
||||
static int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle,
|
||||
PadTriggerEffectStateInformation* info) {
|
||||
PRINT_NAME();
|
||||
|
||||
LOGF("\t handle = %d\n", handle);
|
||||
|
||||
if (info == nullptr) {
|
||||
return -2137653243; /* 0x80960005 */
|
||||
}
|
||||
|
||||
info->state[0] = 0;
|
||||
info->state[1] = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI PadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool enabled) {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -214,7 +194,7 @@ LIB_DEFINE(InitPad_1) {
|
||||
LIB_FUNC("yFVnOdGxvZY", Controller::PadSetVibration);
|
||||
LIB_FUNC("W2G-yoyMF5U", PadSetVibrationMode);
|
||||
LIB_FUNC("Yq0zOH7YNOM", PadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse);
|
||||
LIB_FUNC("znaWI0gpuo8", PadGetTriggerEffectState);
|
||||
LIB_FUNC("znaWI0gpuo8", Controller::PadGetTriggerEffectState);
|
||||
LIB_FUNC("DscD1i9HX1w", Controller::PadResetLightBar);
|
||||
LIB_FUNC("RR4novUEENY", Controller::PadSetLightBar);
|
||||
LIB_FUNC("6ncge5+l5Qs", PadClose);
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#include "libs/libs.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Libs {
|
||||
|
||||
LIB_VERSION("Sysmodule", 1, "Sysmodule", 1, 1);
|
||||
@@ -17,6 +20,16 @@ int KYTY_SYSV_ABI KernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleI
|
||||
|
||||
namespace Sysmodule {
|
||||
|
||||
constexpr int SYSMODULE_ERROR_UNLOADED = static_cast<int>(0x805a1001u);
|
||||
|
||||
static std::mutex g_module_mutex;
|
||||
static std::unordered_map<uint16_t, uint32_t> g_module_refs;
|
||||
|
||||
static void LoadModule(uint16_t id) {
|
||||
std::scoped_lock lock(g_module_mutex);
|
||||
++g_module_refs[id];
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int SysmoduleGetModuleInfoForUnwind(uint64_t addr, int flags,
|
||||
LibKernel::ModuleInfoForUnwind* info) {
|
||||
return LibKernel::KernelGetModuleInfoForUnwind(addr, flags, info);
|
||||
@@ -27,6 +40,7 @@ static KYTY_SYSV_ABI int SysmoduleLoadModule(uint16_t id) {
|
||||
|
||||
LOGF("\t id = %d\n", static_cast<int>(id));
|
||||
|
||||
LoadModule(id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -35,6 +49,14 @@ static KYTY_SYSV_ABI int SysmoduleUnloadModule(uint16_t id) {
|
||||
|
||||
LOGF("\t id = %d\n", static_cast<int>(id));
|
||||
|
||||
std::scoped_lock lock(g_module_mutex);
|
||||
const auto module = g_module_refs.find(id);
|
||||
if (module == g_module_refs.end()) {
|
||||
return SYSMODULE_ERROR_UNLOADED;
|
||||
}
|
||||
if (--module->second == 0) {
|
||||
g_module_refs.erase(module);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -49,6 +71,7 @@ static KYTY_SYSV_ABI int SysmoduleLoadModuleInternalWithArg(uint16_t id, int arg
|
||||
EXIT_IF(arg3 != 0);
|
||||
EXIT_IF(ret == nullptr);
|
||||
|
||||
LoadModule(id);
|
||||
*ret = 0;
|
||||
|
||||
return 0;
|
||||
@@ -59,7 +82,8 @@ static KYTY_SYSV_ABI int SysmoduleIsLoaded(uint16_t id) {
|
||||
|
||||
LOGF("\t id = %d\n", static_cast<int>(id));
|
||||
|
||||
return 0;
|
||||
std::scoped_lock lock(g_module_mutex);
|
||||
return g_module_refs.contains(id) ? 0 : SYSMODULE_ERROR_UNLOADED;
|
||||
}
|
||||
|
||||
} // namespace Sysmodule
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
#include "libs/dialog.h"
|
||||
#include "libs/errno.h"
|
||||
#include "libs/libs.h"
|
||||
#include "libs/systemService.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
|
||||
namespace Libs {
|
||||
|
||||
@@ -70,6 +73,14 @@ struct SystemServiceEvent {
|
||||
uint8_t data[8192];
|
||||
};
|
||||
|
||||
static std::mutex g_event_mutex;
|
||||
static std::deque<int32_t> g_events;
|
||||
|
||||
void NotifyEntitlementUpdate() {
|
||||
std::scoped_lock lock(g_event_mutex);
|
||||
g_events.push_back(0x10000003); // SCE_SYSTEM_SERVICE_EVENT_ENTITLEMENT_UPDATE
|
||||
}
|
||||
|
||||
struct SystemServiceDisplaySafeAreaInfo {
|
||||
float ratio;
|
||||
uint8_t reserved[128];
|
||||
@@ -153,7 +164,14 @@ static int KYTY_SYSV_ABI SystemServiceReceiveEvent(SystemServiceEvent* event) {
|
||||
event->event_type = -1;
|
||||
std::memset(event->data, 0, sizeof(event->data));
|
||||
|
||||
return SYSTEM_SERVICE_ERROR_NO_EVENT;
|
||||
std::scoped_lock lock(g_event_mutex);
|
||||
if (g_events.empty()) {
|
||||
return SYSTEM_SERVICE_ERROR_NO_EVENT;
|
||||
}
|
||||
event->event_type = g_events.front();
|
||||
g_events.pop_front();
|
||||
LOGF("\t SystemService event = 0x%08x\n", static_cast<uint32_t>(event->event_type));
|
||||
return OK;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI SystemServiceGetStatus(SystemServiceStatus* status) {
|
||||
@@ -167,6 +185,10 @@ static int KYTY_SYSV_ABI SystemServiceGetStatus(SystemServiceStatus* status) {
|
||||
*status = SystemServiceStatus();
|
||||
status->is_system_ui_overlaid = dialog.active;
|
||||
status->is_in_background_execution = dialog.background;
|
||||
{
|
||||
std::scoped_lock lock(g_event_mutex);
|
||||
status->event_num = static_cast<int32_t>(g_events.size());
|
||||
}
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
+25
-11
@@ -832,10 +832,6 @@ namespace Net {
|
||||
|
||||
LIB_NAME("Net", "Net");
|
||||
|
||||
struct NetEtherAddr {
|
||||
uint8_t data[6] = {0};
|
||||
};
|
||||
|
||||
#if defined(_WIN32)
|
||||
using NativeSocket = SOCKET;
|
||||
static constexpr NativeSocket INVALID_NATIVE_SOCKET = INVALID_SOCKET;
|
||||
@@ -1564,14 +1560,12 @@ const char* KYTY_SYSV_ABI NetInetNtop(int af, const void* src, char* dst, uint32
|
||||
int KYTY_SYSV_ABI NetEtherNtostr(const NetEtherAddr* n, char* str, size_t len) {
|
||||
PRINT_NAME();
|
||||
|
||||
NetEtherAddr zero {};
|
||||
if (n == nullptr || str == nullptr || len < 18) {
|
||||
return NET_ERROR_EINVAL;
|
||||
}
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(len != 18);
|
||||
EXIT_NOT_IMPLEMENTED(n == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(str == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(memcmp(n->data, zero.data, sizeof(zero.data)) != 0);
|
||||
|
||||
strcpy(str, "00:00:00:00:00:00"); // NOLINT
|
||||
std::snprintf(str, len, "%02x:%02x:%02x:%02x:%02x:%02x", n->data[0], n->data[1],
|
||||
n->data[2], n->data[3], n->data[4], n->data[5]);
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -4112,6 +4106,26 @@ int KYTY_SYSV_ABI NpGetAccountAge(int req_id, int user_id, uint8_t* age) {
|
||||
return np_error_signed_out;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NpGetAccountLanguage2(int req_id, int user_id, void* language_code) {
|
||||
PRINT_NAME();
|
||||
|
||||
LOGF("\t req_id = %d\n", req_id);
|
||||
LOGF("\t user_id = %d\n", user_id);
|
||||
LOGF("\t language_code = 0x%016" PRIx64 "\n", reinterpret_cast<uint64_t>(language_code));
|
||||
|
||||
if (req_id <= 0 || language_code == nullptr) {
|
||||
return np_error_invalid_argument;
|
||||
}
|
||||
|
||||
std::lock_guard lock(g_np_request_mutex);
|
||||
|
||||
if (np_get_request_locked(req_id) == nullptr) {
|
||||
return np_error_request_not_found;
|
||||
}
|
||||
|
||||
return np_error_signed_out;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NpCreateRequest() {
|
||||
PRINT_NAME();
|
||||
|
||||
|
||||
+4
-1
@@ -16,7 +16,9 @@ struct Lifecycle {
|
||||
|
||||
namespace Net {
|
||||
|
||||
struct NetEtherAddr;
|
||||
struct NetEtherAddr {
|
||||
uint8_t data[6] = {0};
|
||||
};
|
||||
|
||||
union NetEpollData {
|
||||
void* ptr;
|
||||
@@ -185,6 +187,7 @@ int KYTY_SYSV_ABI NpGetOnlineId(int user_id, NpOnlineId* online_id);
|
||||
int KYTY_SYSV_ABI NpGetAccountIdA(int user_id, uint64_t* account_id);
|
||||
int KYTY_SYSV_ABI NpGetAccountCountryA(int user_id, void* country_code);
|
||||
int KYTY_SYSV_ABI NpGetAccountAge(int req_id, int user_id, uint8_t* age);
|
||||
int KYTY_SYSV_ABI NpGetAccountLanguage2(int req_id, int user_id, void* language_code);
|
||||
int KYTY_SYSV_ABI NpCreateRequest();
|
||||
int KYTY_SYSV_ABI NpCreateAsyncRequest(const NpCreateAsyncRequestParameter* param);
|
||||
int KYTY_SYSV_ABI NpDeleteRequest(int req_id);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef EMULATOR_LIBS_SYSTEMSERVICE_H_
|
||||
#define EMULATOR_LIBS_SYSTEMSERVICE_H_
|
||||
|
||||
namespace Libs::SystemService {
|
||||
|
||||
void NotifyEntitlementUpdate();
|
||||
|
||||
} // namespace Libs::SystemService
|
||||
|
||||
#endif
|
||||
@@ -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(¶m), &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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -343,6 +343,162 @@ void TestIndependentOutputsAndPadSwitch() {
|
||||
"released controller retained a cached vibration");
|
||||
}
|
||||
}
|
||||
|
||||
void TestTriggerEffectState() {
|
||||
Controller controller;
|
||||
PadTriggerEffectStateInformation info {{-1, -1}};
|
||||
const auto check_state = [&](int32_t left, int32_t right, const char* message) {
|
||||
Check(PadGetTriggerEffectState(1, &info) == OK && info.state[0] == left &&
|
||||
info.state[1] == right,
|
||||
message);
|
||||
};
|
||||
const auto axis = [&](int left, int right) {
|
||||
SetAxis(1, Axis::TriggerLeft, left);
|
||||
SetAxis(1, Axis::TriggerRight, right);
|
||||
};
|
||||
PadTriggerEffectParam param {};
|
||||
const auto set_effect = [&] {
|
||||
Check(PadSetTriggerEffect(1, ¶m) == OK, "trigger request failed");
|
||||
};
|
||||
Check(PadGetTriggerEffectState(2, &info) == PAD_ERROR_INVALID_HANDLE && info.state[0] == -1 &&
|
||||
info.state[1] == -1,
|
||||
"invalid trigger handle changed the output");
|
||||
Check(PadGetTriggerEffectState(2, nullptr) == PAD_ERROR_INVALID_HANDLE,
|
||||
"trigger state checked the output before the handle");
|
||||
Check(PadGetTriggerEffectState(1, nullptr) == PAD_ERROR_INVALID_ARG,
|
||||
"trigger state accepted a null output");
|
||||
check_state(0, 0, "unset trigger effects did not report off");
|
||||
|
||||
param.trigger_mask = 3;
|
||||
param.command[0].mode = 1;
|
||||
param.command[0].data[0] = 7;
|
||||
param.command[0].data[1] = 8;
|
||||
param.command[1].mode = 2;
|
||||
param.command[1].data[0] = 2;
|
||||
param.command[1].data[1] = 7;
|
||||
param.command[1].data[2] = 6;
|
||||
set_effect();
|
||||
axis(191, 63);
|
||||
check_state(1, 3, "triggers activated before the documented start positions");
|
||||
axis(192, 64);
|
||||
check_state(2, 4, "triggers did not activate at the documented start positions");
|
||||
axis(255, 223);
|
||||
check_state(2, 4, "weapon fired before its end position");
|
||||
SetAxis(1, Axis::TriggerRight, 224);
|
||||
SetAxis(1, Axis::TriggerRight, 100);
|
||||
check_state(2, 5, "unpolled weapon firing was lost on partial release");
|
||||
check_state(2, 5, "reading trigger state changed the weapon latch");
|
||||
set_effect();
|
||||
check_state(2, 5, "repeating a weapon request rearmed a held trigger");
|
||||
CycleSetting(Setting::TriggerEffectIntensity);
|
||||
check_state(2, 5, "muting trigger effects changed their reported states");
|
||||
|
||||
// A rejected two-trigger request must leave both cached commands and states intact.
|
||||
auto invalid = param;
|
||||
invalid.command[0].mode = 0;
|
||||
invalid.command[1].data[1] = 2;
|
||||
Check(PadSetTriggerEffect(1, &invalid) == PAD_ERROR_INVALID_ARG,
|
||||
"invalid weapon end position was accepted");
|
||||
SetAxis(1, Axis::TriggerLeft, 255);
|
||||
check_state(2, 5, "an invalid request changed cached trigger commands or states");
|
||||
SetAxis(1, Axis::TriggerRight, 63);
|
||||
SetAxis(1, Axis::TriggerRight, 100);
|
||||
check_state(2, 4, "unpolled release did not rearm the weapon");
|
||||
param.trigger_mask = 2;
|
||||
param.command[1].data[1] = 8;
|
||||
set_effect();
|
||||
axis(255, 253);
|
||||
check_state(2, 4, "weapon end position 8 fired before output 254");
|
||||
SetAxis(1, Axis::TriggerRight, 254);
|
||||
check_state(2, 5, "weapon end position 8 did not fire at output 254");
|
||||
|
||||
// Masked updates preserve the other trigger, including its fired state.
|
||||
param.trigger_mask = 1;
|
||||
param.command[0].data[0] = 9;
|
||||
set_effect();
|
||||
SetAxis(1, Axis::TriggerLeft, 254);
|
||||
check_state(1, 5, "feedback position 9 activated before full travel");
|
||||
SetAxis(HOST_INPUT_CONTROLLER_ID, Axis::TriggerLeft, 255);
|
||||
check_state(2, 5, "keyboard trigger travel did not activate feedback");
|
||||
param.command[0].data[0] = 0;
|
||||
set_effect();
|
||||
SetAxis(1, Axis::TriggerLeft, 0);
|
||||
check_state(2, 5, "feedback at position 0 was suppressed by a dead zone");
|
||||
param.command[0].data[1] = 0;
|
||||
set_effect();
|
||||
check_state(1, 5, "zero-strength feedback reported active");
|
||||
|
||||
param.command[0] = {};
|
||||
param.command[0].mode = 4;
|
||||
param.command[0].data[0] = 4;
|
||||
param.command[0].data[2] = 4;
|
||||
param.command[0].data[8] = 4;
|
||||
set_effect();
|
||||
const int feedback_cases[][2] = {{0, 2}, {1, 1}, {31, 1}, {32, 2}, {63, 2},
|
||||
{64, 1}, {223, 1}, {224, 2}, {254, 2}, {255, 1}};
|
||||
for (const auto& test: feedback_cases) {
|
||||
SetAxis(1, Axis::TriggerLeft, test[0]);
|
||||
check_state(test[1], 5, "multi-position feedback used the wrong position");
|
||||
}
|
||||
|
||||
param.command[0] = {};
|
||||
param.command[0].mode = 5;
|
||||
param.command[0].data[0] = 3;
|
||||
param.command[0].data[1] = 7;
|
||||
param.command[0].data[2] = 2;
|
||||
param.command[0].data[3] = 6;
|
||||
set_effect();
|
||||
const int slope_cases[][2] = {{63, 1}, {64, 2}, {191, 2}, {192, 1}, {255, 1}};
|
||||
for (const auto& test: slope_cases) {
|
||||
SetAxis(1, Axis::TriggerLeft, test[0]);
|
||||
check_state(test[1], 5, "slope feedback did not respect its start and end");
|
||||
}
|
||||
param.command[0].data[0] = 0;
|
||||
set_effect();
|
||||
SetAxis(1, Axis::TriggerLeft, 0);
|
||||
check_state(2, 5, "slope feedback at position 0 was suppressed");
|
||||
|
||||
param.command[0] = {};
|
||||
param.command[0].mode = 3;
|
||||
param.command[0].data[0] = 6;
|
||||
param.command[0].data[1] = 4;
|
||||
param.command[0].data[2] = 20;
|
||||
set_effect();
|
||||
SetAxis(1, Axis::TriggerLeft, 159);
|
||||
check_state(6, 5, "vibration activated before position 6");
|
||||
SetAxis(1, Axis::TriggerLeft, 160);
|
||||
check_state(7, 5, "vibration did not activate at position 6");
|
||||
param.command[0].data[0] = 0;
|
||||
set_effect();
|
||||
SetAxis(1, Axis::TriggerLeft, 0);
|
||||
check_state(7, 5, "vibration at position 0 was suppressed");
|
||||
param.command[0].data[2] = 0;
|
||||
set_effect();
|
||||
check_state(6, 5, "zero-frequency vibration reported active");
|
||||
|
||||
param.command[0] = {};
|
||||
param.command[0].mode = 6;
|
||||
param.command[0].data[0] = 20;
|
||||
param.command[0].data[10] = 4;
|
||||
set_effect();
|
||||
SetAxis(1, Axis::TriggerLeft, 254);
|
||||
check_state(6, 5, "multi-position vibration activated before position 9");
|
||||
SetAxis(1, Axis::TriggerLeft, 255);
|
||||
check_state(7, 5, "multi-position vibration did not activate at position 9");
|
||||
param.command[0].data[0] = 0;
|
||||
set_effect();
|
||||
check_state(6, 5, "zero-frequency multi-position vibration reported active");
|
||||
|
||||
ResetInputState();
|
||||
check_state(6, 3, "input reset retained a fired weapon");
|
||||
param.command[0].mode = 0;
|
||||
set_effect();
|
||||
check_state(0, 3, "off mode retained an effect state");
|
||||
Connect(2);
|
||||
Disconnect(1);
|
||||
check_state(0, 0, "switching controllers retained effect states");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
@@ -353,6 +509,7 @@ int main() {
|
||||
TestVibrationLifetime();
|
||||
TestMaskedTriggersAndValidation();
|
||||
TestIndependentOutputsAndPadSwitch();
|
||||
TestTriggerEffectState();
|
||||
Config::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ void InitLibKernel_1_Apr(Loader::SymbolDatabase *symbols);
|
||||
|
||||
namespace Libs {
|
||||
void InitLibKernel_1(Loader::SymbolDatabase *symbols);
|
||||
void InitSysmodule_1(Loader::SymbolDatabase *symbols);
|
||||
void InitSystemService_1(Loader::SymbolDatabase *symbols);
|
||||
void InitAppContent_1(Loader::SymbolDatabase *symbols);
|
||||
}
|
||||
|
||||
namespace Libs::LibAmpr {
|
||||
@@ -121,6 +124,138 @@ void CheckSaveRename(const std::filesystem::path &root,
|
||||
"renamed save contents");
|
||||
}
|
||||
|
||||
void TestRandomDevices() {
|
||||
for (const auto* path : {"/dev/urandom", "/dev/random"}) {
|
||||
const int fd = FileSystem::KernelOpen(path, 0, 0);
|
||||
FileSystem::FileStat stat {};
|
||||
Check(fd >= 3 && FileSystem::KernelFstat(fd, &stat) == OK &&
|
||||
(stat.st_mode & 0170000) == 0020000,
|
||||
"entropy sources are character devices");
|
||||
std::array<uint8_t, 32> entropy {};
|
||||
Check(FileSystem::KernelRead(fd, entropy.data(), entropy.size()) == entropy.size(),
|
||||
"character device supplies requested entropy");
|
||||
Check(FileSystem::KernelClose(fd) == OK, "close entropy source");
|
||||
}
|
||||
}
|
||||
|
||||
void TestFileDescriptorFlags() {
|
||||
Loader::SymbolDatabase symbols;
|
||||
Libs::InitLibKernel_1(&symbols);
|
||||
const auto* symbol = symbols.Find(
|
||||
{"8nY19bKoiZk", "Posix", 1, "libkernel", 1, 1, Loader::SymbolType::Func});
|
||||
Check(symbol != nullptr, "POSIX fcntl export resolves");
|
||||
const auto fcntl = reinterpret_cast<int (KYTY_SYSV_ABI *)(int, int, int)>(symbol->vaddr);
|
||||
Check(fcntl(std::numeric_limits<int>::min(), 1, 0) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF,
|
||||
"invalid descriptor reports EBADF");
|
||||
for (const auto* path : {"/dev/urandom", "/dev/random"}) {
|
||||
const int fd = FileSystem::KernelOpen(path, 0, 0);
|
||||
Check(fd >= 3, "open descriptor for flag checks");
|
||||
Check(fcntl(fd, 1, 0) == 0 && fcntl(fd, 2, 1) == 0 && fcntl(fd, 1, 0) == 1,
|
||||
"entropy descriptor retains close-on-exec flag");
|
||||
Check(fcntl(fd, 2, 0) == 0 && fcntl(fd, 1, 0) == 0,
|
||||
"close-on-exec flag can be cleared");
|
||||
Check(fcntl(fd, -1, 0) == -1 && *Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
|
||||
"unsupported fcntl command reports EINVAL");
|
||||
Check(FileSystem::KernelClose(fd) == OK, "close entropy source");
|
||||
Check(fcntl(fd, 1, 0) == -1 && *Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF,
|
||||
"closed descriptor reports EBADF");
|
||||
const int cloexec_fd = FileSystem::KernelOpen(path, 0x00100000, 0);
|
||||
Check(cloexec_fd >= 3 && fcntl(cloexec_fd, 1, 0) == 1,
|
||||
"O_CLOEXEC sets the descriptor flag on open");
|
||||
Check(FileSystem::KernelClose(cloexec_fd) == OK, "close flagged entropy source");
|
||||
}
|
||||
}
|
||||
|
||||
void TestSysmoduleReferences() {
|
||||
Loader::SymbolDatabase symbols;
|
||||
Libs::InitSysmodule_1(&symbols);
|
||||
using ModuleCall = int (KYTY_SYSV_ABI *)(uint16_t);
|
||||
const auto resolve = [&](const char* nid) {
|
||||
const auto* symbol = symbols.Find(
|
||||
{nid, "Sysmodule", 1, "Sysmodule", 1, 1, Loader::SymbolType::Func});
|
||||
Check(symbol != nullptr, "Sysmodule export resolves");
|
||||
return reinterpret_cast<ModuleCall>(symbol->vaddr);
|
||||
};
|
||||
const auto load = resolve("g8cM39EUZ6o");
|
||||
const auto unload = resolve("eR2bZFAAU0Q");
|
||||
const auto is_loaded = resolve("fMP5NHUOaMk");
|
||||
constexpr int unloaded = static_cast<int>(0x805a1001u);
|
||||
Check(load(0x113) == OK && is_loaded(0xb4) == unloaded,
|
||||
"loading entitlement access does not mark AppContent loaded");
|
||||
Check(unload(0x113) == OK, "release independent module reference");
|
||||
for (const uint16_t id : {0x113, 0xb4}) {
|
||||
Check(is_loaded(id) == unloaded, "unloaded module allows guest initialization");
|
||||
Check(unload(id) == unloaded, "unloading an absent module reports UNLOADED");
|
||||
Check(load(id) == OK && load(id) == OK && is_loaded(id) == OK,
|
||||
"repeated loads retain references");
|
||||
Check(unload(id) == OK && is_loaded(id) == OK,
|
||||
"one unload preserves the remaining reference");
|
||||
Check(unload(id) == OK && is_loaded(id) == unloaded,
|
||||
"last unload restores unloaded status");
|
||||
}
|
||||
const auto* internal_symbol = symbols.Find(
|
||||
{"hHrGoGoNf+s", "Sysmodule", 1, "Sysmodule", 1, 1, Loader::SymbolType::Func});
|
||||
Check(internal_symbol != nullptr, "internal Sysmodule load export resolves");
|
||||
const auto internal_load =
|
||||
reinterpret_cast<int (KYTY_SYSV_ABI *)(uint16_t, int, int, int, int*)>(
|
||||
internal_symbol->vaddr);
|
||||
int result = -1;
|
||||
Check(internal_load(0xb4, 0, 0, 0, &result) == OK && result == OK &&
|
||||
is_loaded(0xb4) == OK && unload(0xb4) == OK && is_loaded(0xb4) == unloaded,
|
||||
"internal and public module operations share load state");
|
||||
}
|
||||
|
||||
void TestSystemServiceEntitlementEvents() {
|
||||
Loader::SymbolDatabase symbols;
|
||||
Libs::InitSystemService_1(&symbols);
|
||||
Libs::InitAppContent_1(&symbols);
|
||||
const auto resolve = [&](const char* nid, const char* library, const char* module) {
|
||||
const auto* symbol = symbols.Find(
|
||||
{nid, library, 1, module, 1, 1, Loader::SymbolType::Func});
|
||||
Check(symbol != nullptr, "entitlement event export resolves");
|
||||
return symbol->vaddr;
|
||||
};
|
||||
const auto initialize = reinterpret_cast<int (KYTY_SYSV_ABI *)(const void*, void*)>(
|
||||
resolve("R9lA82OraNs", "AppContent", "AppContentUtil"));
|
||||
struct Status {
|
||||
int32_t event_num;
|
||||
bool overlay, background, vr;
|
||||
uint8_t reserved[127];
|
||||
};
|
||||
struct Event {
|
||||
int32_t type;
|
||||
uint8_t data[8192];
|
||||
};
|
||||
static_assert(sizeof(Status) == 136 && sizeof(Event) == 8196);
|
||||
const auto get_status = reinterpret_cast<int (KYTY_SYSV_ABI *)(Status*)>(
|
||||
resolve("rPo6tV8D9bM", "SystemService", "SystemService"));
|
||||
const auto receive = reinterpret_cast<int (KYTY_SYSV_ABI *)(Event*)>(
|
||||
resolve("656LMQSrg6U", "SystemService", "SystemService"));
|
||||
Status status {};
|
||||
Event event {};
|
||||
Check(get_status(&status) == OK && status.event_num == 0,
|
||||
"SystemService starts without pending events");
|
||||
std::array<uint8_t, 32> init {};
|
||||
std::array<uint8_t, 40> boot {};
|
||||
Check(initialize(init.data(), boot.data()) == OK,
|
||||
"AppContent initialization generates an entitlement notification");
|
||||
Check(get_status(&status) == OK && status.event_num == 1 &&
|
||||
get_status(&status) == OK && status.event_num == 1,
|
||||
"status queries preserve pending notifications");
|
||||
Check(receive(nullptr) == Libs::SystemService::SYSTEM_SERVICE_ERROR_PARAMETER &&
|
||||
get_status(&status) == OK && status.event_num == 1,
|
||||
"invalid event output does not consume a notification");
|
||||
std::memset(&event, 0xff, sizeof(event));
|
||||
Check(receive(&event) == OK && event.type == 0x10000003 &&
|
||||
std::all_of(std::begin(event.data), std::end(event.data),
|
||||
[](uint8_t byte) { return byte == 0; }) &&
|
||||
get_status(&status) == OK && status.event_num == 0,
|
||||
"receive delivers the entitlement event with cleared payload exactly once");
|
||||
Check(receive(&event) == Libs::SystemService::SYSTEM_SERVICE_ERROR_NO_EVENT,
|
||||
"empty event queue reports NO_EVENT");
|
||||
}
|
||||
|
||||
void TestSaveOpenVisibility() {
|
||||
constexpr char Path[] = "/savedata0/visible-save.dat";
|
||||
constexpr char Payload[] = "saved progress";
|
||||
@@ -669,6 +804,10 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
|
||||
using Offset = uint64_t (KYTY_SYSV_ABI *)(void *);
|
||||
using WaitAddress = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t,
|
||||
uint8_t, uint8_t);
|
||||
using WaitCounter = int (KYTY_SYSV_ABI *)(void *, uint8_t, uint8_t, uint64_t,
|
||||
uint8_t, uint8_t, uint64_t, uint8_t);
|
||||
using WriteCounter = int (KYTY_SYSV_ABI *)(void *, uint8_t, uint8_t, uint64_t,
|
||||
uint8_t, uint32_t);
|
||||
using WriteAddress = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t);
|
||||
using ReadFile = int (KYTY_SYSV_ABI *)(void *, uint64_t, uint64_t, uint32_t,
|
||||
void *, uint64_t, uint64_t);
|
||||
@@ -685,6 +824,8 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
|
||||
const auto set_buffer = reinterpret_cast<SetBuffer>(find("N-FSPA4S3nI"));
|
||||
const auto offset = reinterpret_cast<Offset>(find("GnxKOHEawhk"));
|
||||
const auto wait_address = reinterpret_cast<WaitAddress>(find("DLfoNxTFNVk"));
|
||||
const auto wait_counter = reinterpret_cast<WaitCounter>(find("cQb8Zr8Q0Y0"));
|
||||
const auto write_counter = reinterpret_cast<WriteCounter>(find("jK+yuYCI7MA"));
|
||||
const auto write_address = reinterpret_cast<WriteAddress>(find("sJXyWHjP-F8"));
|
||||
const auto read_file = reinterpret_cast<ReadFile>(find("mQ16-QdKv7k"));
|
||||
const auto write_event = reinterpret_cast<WriteKernelEvent>(find("H896Pt-yB4I"));
|
||||
@@ -758,6 +899,59 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
|
||||
"submission wait observes the completed result");
|
||||
}
|
||||
}
|
||||
constexpr std::array counter_comparisons {
|
||||
Comparison{1, 0, 0, 1}, Comparison{1, 0, 0, 1},
|
||||
Comparison{4, 0x7ffffffe, 0x7fffffff, 0x80000000},
|
||||
Comparison{5, 0xffffffff, 0, 1}};
|
||||
for (const auto &comparison : counter_comparisons) {
|
||||
reset(1);
|
||||
auto *producer = buffers[0].header.data();
|
||||
auto *consumer = buffers[1].header.data();
|
||||
auto *lower = buffers[2].header.data();
|
||||
constexpr uint8_t Counter = 127;
|
||||
constexpr auto Invalid = Libs::LibKernel::KERNEL_ERROR_EINVAL;
|
||||
Check(write_counter(producer, 128, 1, 1, 0, 0) == Invalid &&
|
||||
write_counter(producer, Counter, 8, 1, 0, 0) == Invalid &&
|
||||
write_counter(producer, Counter, 1, 1, 5, 0) == Invalid &&
|
||||
write_counter(producer, Counter, 1, 1, 0, 2) == Invalid &&
|
||||
wait_counter(consumer, Counter, 8, 0, 1, 0, 0, 0) == Invalid &&
|
||||
wait_counter(consumer, Counter, 1, 0, 7, 0, 0, 0) == Invalid &&
|
||||
wait_counter(consumer, Counter, 1, 0, 1, 2, 0, 0) == Invalid &&
|
||||
wait_counter(consumer, Counter, 1, 0, 1, 0, 0, 2) == Invalid &&
|
||||
offset(producer) == 0 && offset(consumer) == 0,
|
||||
"invalid counter operations fail without appending a command");
|
||||
uint64_t retired = 0, lower_done = 0;
|
||||
std::array<char, 3> output {};
|
||||
Result result {1234, 5678};
|
||||
std::array<uint32_t, 4> ids {};
|
||||
if (comparison.blocked != 0) {
|
||||
auto *initializer = buffers[3].header.data();
|
||||
Check(write_counter(initializer, Counter, 1, comparison.blocked, 0, 0) == OK &&
|
||||
submit_amm(buffers[3].data.data(), static_cast<uint32_t>(offset(initializer)),
|
||||
0, &ids[3]) == OK && wait_amm(ids[3]) == OK,
|
||||
"initialize shared counter before dependent submissions");
|
||||
}
|
||||
Check(read_file(producer, reinterpret_cast<uint64_t>(&buffers[0].header[3]),
|
||||
reinterpret_cast<uint64_t>(&buffers[0].header[4]), file_id,
|
||||
output.data(), output.size(), 0) == OK &&
|
||||
write_counter(producer, Counter, 1, comparison.released, 0, 0) == OK &&
|
||||
wait_counter(consumer, Counter, 1, comparison.reference, comparison.compare,
|
||||
0, 0, 0) == OK &&
|
||||
write_counter(consumer, Counter, 1, 0, 0, 0) == OK &&
|
||||
write_address(consumer, &retired, 1) == OK &&
|
||||
write_address(lower, &lower_done, 1) == OK,
|
||||
"build APR completion and dependent AMM counter reset");
|
||||
Check(submit_amm(buffers[1].data.data(), static_cast<uint32_t>(offset(consumer)),
|
||||
0, &ids[1]) == OK &&
|
||||
submit_amm(buffers[2].data.data(), static_cast<uint32_t>(offset(lower)),
|
||||
1, &ids[2]) == OK && wait_amm(ids[2]) == OK &&
|
||||
lower_done == 1 && retired == 0,
|
||||
"counter wait blocks AMM retirement while its lower priority progresses");
|
||||
Check(submit_apr(producer, 3, &result, &ids[0]) == OK && wait_amm(ids[1]) == OK &&
|
||||
wait_apr(ids[0]) == OK && result.result == OK && retired == 1 &&
|
||||
std::memcmp(output.data(), "APR", 3) == 0,
|
||||
"shared counter completion releases AMM only after APR read and resets for reuse");
|
||||
}
|
||||
reset(1);
|
||||
namespace EventQueue = Libs::LibKernel::EventQueue;
|
||||
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
|
||||
@@ -857,6 +1051,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 +1104,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");
|
||||
}
|
||||
|
||||
@@ -1088,6 +1312,38 @@ void CheckSocketReceiveBuffer(int reader, int writer) {
|
||||
}
|
||||
#endif
|
||||
|
||||
void CheckEtherAddressFormatting() {
|
||||
Loader::SymbolDatabase symbols;
|
||||
Libs::LibNet::InitNet_1_Net(&symbols);
|
||||
const auto *format_symbol = symbols.FindByNid("v6M4txecCuo", Loader::SymbolType::Func);
|
||||
const auto *errno_symbol = symbols.FindByNid("HQOwnfMGipQ", Loader::SymbolType::Func);
|
||||
Check(format_symbol && errno_symbol, "Ethernet formatting and errno exports resolve");
|
||||
using Format = int (KYTY_SYSV_ABI *)(const Libs::Network::Net::NetEtherAddr *, char *, size_t);
|
||||
using Errno = int *(KYTY_SYSV_ABI *)();
|
||||
const auto format = reinterpret_cast<Format>(format_symbol->vaddr);
|
||||
auto *net_errno = reinterpret_cast<Errno>(errno_symbol->vaddr)();
|
||||
for (const auto address : {Libs::Network::Net::NetEtherAddr{},
|
||||
Libs::Network::Net::NetEtherAddr{{0x01, 0x23, 0x45, 0xab, 0xcd, 0xef}}}) {
|
||||
const auto *expected = address.data[0] == 0 ? "00:00:00:00:00:00" : "01:23:45:ab:cd:ef";
|
||||
for (const size_t size : {18u, 127u}) {
|
||||
std::array<char, 128> text;
|
||||
text.fill('!');
|
||||
Check(format(&address, text.data(), size) == OK &&
|
||||
std::strcmp(text.data(), expected) == 0 && text[18] == '!',
|
||||
"Ethernet formatting accepts exact and larger buffers");
|
||||
}
|
||||
}
|
||||
const Libs::Network::Net::NetEtherAddr address{};
|
||||
std::array<char, 18> text;
|
||||
text.fill('!');
|
||||
Check(format(&address, text.data(), 17) == Libs::Network::NET_ERROR_EINVAL &&
|
||||
*net_errno == Libs::Posix::POSIX_EINVAL &&
|
||||
std::all_of(text.begin(), text.end(), [](char c) { return c == '!'; }) &&
|
||||
format(nullptr, text.data(), text.size()) == Libs::Network::NET_ERROR_EINVAL &&
|
||||
format(&address, nullptr, text.size()) == Libs::Network::NET_ERROR_EINVAL,
|
||||
"Ethernet formatting rejects invalid arguments without writing output");
|
||||
}
|
||||
|
||||
void CheckSocketWakeup() {
|
||||
namespace Net = Libs::Network::Net;
|
||||
Loader::SymbolDatabase symbols;
|
||||
@@ -1368,6 +1624,10 @@ int main(int, char**) {
|
||||
|
||||
TempDirectory temporary;
|
||||
FileSystem::Initialize();
|
||||
TestSysmoduleReferences();
|
||||
TestSystemServiceEntitlementEvents();
|
||||
TestRandomDevices();
|
||||
TestFileDescriptorFlags();
|
||||
CheckMountRoot(temporary.Path());
|
||||
CheckUnmappedPaths(temporary.Path());
|
||||
CheckArchiveMount(temporary.Path());
|
||||
@@ -1382,6 +1642,7 @@ int main(int, char**) {
|
||||
CheckSaveRename(temporary.Path(), "replacement-save");
|
||||
FileSystem::Shutdown();
|
||||
CheckSocketWakeup();
|
||||
CheckEtherAddressFormatting();
|
||||
TestNpWebApi2Memory();
|
||||
graphics.reset();
|
||||
subsystems.Destroy();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
#include "libs/agc.h"
|
||||
#include "libs/dialog.h"
|
||||
#include "libs/errno.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
#include "spirv-tools/libspirv.hpp"
|
||||
|
||||
#if __has_include("graphics/host_gpu/renderer/renderTargetBarriers.h")
|
||||
@@ -104,6 +105,10 @@
|
||||
#undef max
|
||||
#endif
|
||||
|
||||
namespace Libs {
|
||||
void InitAgcDriver_1(Loader::SymbolDatabase *symbols);
|
||||
}
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
template <typename Cache>
|
||||
@@ -386,6 +391,11 @@ struct TextureCacheTestAccess {
|
||||
};
|
||||
|
||||
struct RenderExecutorTestAccess {
|
||||
static void DrawIndex(RenderExecutor &executor, CommandBuffer &command,
|
||||
const DrawIndexArgs &args) {
|
||||
executor.DrawIndex(0, command, args);
|
||||
}
|
||||
|
||||
static void DrawAuto(RenderExecutor &executor, CommandBuffer &command,
|
||||
const DrawAutoArgs &args) {
|
||||
executor.DrawAuto(0, command, args);
|
||||
@@ -15755,6 +15765,7 @@ public:
|
||||
constexpr uintptr_t depth_address = 0x0000000204400000ull;
|
||||
constexpr uint64_t allocation_size = 0x40000;
|
||||
constexpr uint64_t rect_address = depth_address + 0x8000;
|
||||
constexpr uint64_t index_address = rect_address + 0x1000;
|
||||
EnsureRuntimeContext();
|
||||
RenderContext context(m_runtime_context);
|
||||
auto &scheduler = context.GetCommandScheduler();
|
||||
@@ -15798,6 +15809,8 @@ public:
|
||||
-0.75f, 0.75f, 0, 0, 0, 1, 0, 0};
|
||||
std::memcpy(reinterpret_cast<void *>(rect_address), rect_vertices.data(),
|
||||
sizeof(rect_vertices));
|
||||
constexpr std::array<uint32_t, 3> indices{0, 1, 2};
|
||||
std::memcpy(reinterpret_cast<void *>(index_address), indices.data(), sizeof(indices));
|
||||
}
|
||||
resources.MapMemory(depth_address, allocation_size);
|
||||
if (depth_feedback) {
|
||||
@@ -16652,6 +16665,56 @@ public:
|
||||
}
|
||||
registers.SetTargetOutputMode(0, 4);
|
||||
|
||||
// Indirect arguments also reach s8/s9. Native VS prologs must add them
|
||||
// exactly once, for both indexed and automatically generated indices.
|
||||
static const auto indirect_vertex = [&] {
|
||||
auto code = native_vertex;
|
||||
for (auto &word : code) {
|
||||
if (word == EncodeVopc(0xc2, InlineU32(1), 5))
|
||||
word = EncodeVopc(0xc2, InlineU32(8), 5);
|
||||
else if (word == EncodeVopc(0xc2, InlineU32(2), 5))
|
||||
word = EncodeVopc(0xc2, InlineU32(9), 5);
|
||||
}
|
||||
code.insert(code.begin(), {EncodeVop2(0x25, 5, 8, 5),
|
||||
EncodeVop2(0x25, 8, 9, 8)});
|
||||
const auto position = std::ranges::find(code, EncodeExp0(0x0c, 0xf));
|
||||
code.insert(position, {EncodeVopc(0xc2, InlineU32(5), 8),
|
||||
EncodeVop2(0x01, 3, Vgpr(1), 3),
|
||||
EncodeVop2(0x01, 4, Vgpr(1), 4)});
|
||||
return code;
|
||||
}();
|
||||
const auto indirect_vs = reinterpret_cast<uint64_t>(indirect_vertex.data());
|
||||
ShaderMapUserData(indirect_vs,
|
||||
{.type = Prospero::ShaderBinaryType::kGs, .user_data = &native_user_data,
|
||||
.code_size_bytes = static_cast<uint32_t>(indirect_vertex.size() * sizeof(u32))});
|
||||
shaders.SetEsShaderBase(indirect_vs);
|
||||
shaders.SetPsShaderBase(pixel_address);
|
||||
shaders.SetGsShaderResource2({.user_sgpr = 2});
|
||||
shaders.SetGsUserSgpr(0, 7, HW::UserSgprType::Unknown);
|
||||
shaders.SetGsUserSgpr(1, 5, HW::UserSgprType::Unknown);
|
||||
user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriList);
|
||||
for (const bool indexed : {false, true}) {
|
||||
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), sparse_color.image_id,
|
||||
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, {});
|
||||
if (indexed) {
|
||||
RenderExecutorTestAccess::DrawIndex(executor, scheduler.Current(),
|
||||
{.index_count = 3, .index_addr = reinterpret_cast<const void *>(index_address),
|
||||
.instance_count = 1, .index_type_and_size = 1, .base_vertex = 7, .first_instance = 5,
|
||||
.offset_source = DrawOffsetSource::IndirectArgs});
|
||||
} else {
|
||||
RenderExecutorTestAccess::DrawAuto(executor, scheduler.Current(),
|
||||
{.vertex_count = 3, .instance_count = 1, .first_vertex = 7, .first_instance = 5,
|
||||
.offset_source = DrawOffsetSource::IndirectArgs});
|
||||
}
|
||||
const auto pixels = ReadCachedTexel(name, context, sparse_color.image_id,
|
||||
{}, {extent, extent, 1});
|
||||
for (size_t component = 0; component < pixels.size(); component++) {
|
||||
Require("NativeIndirectDrawOffsets", indexed ? "indexed" : "auto",
|
||||
pixels[component] == (component % 4 == 3 ? 0x3f800000u : 0x3e800000u),
|
||||
"native vertex or instance offset was applied twice");
|
||||
}
|
||||
}
|
||||
|
||||
// A fourth VS invocation reads beyond the descriptor instead of reconstructing the corner.
|
||||
const ShaderBufferResource rect_buffer{{
|
||||
static_cast<u32>(rect_address),
|
||||
@@ -19073,6 +19136,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 +20654,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 +20678,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 +20700,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 +20715,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 +22157,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 +22403,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;
|
||||
|
||||
@@ -23360,6 +23608,48 @@ TestCase Vop3pIntegerNegationCapturedAndSelectedHalves() {
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase Vop3pInt16Ops() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
std::vector<u32> code;
|
||||
AppendVMovLiteral(&code, 1, 0x80007fffu);
|
||||
AppendVMovLiteral(&code, 2, 0x00010001u);
|
||||
AppendVMovLiteral(&code, 3, 0xfffe0003u);
|
||||
AppendVMovLiteral(&code, 4, 0x00130004u);
|
||||
AppendVMovLiteral(&code, 5, 0x8010f00fu);
|
||||
AppendVMovLiteral(&code, 6, 0x00050007u);
|
||||
|
||||
AppendVop3p(&code, 0x00u, 10, Vgpr(1), Vgpr(3), Vgpr(6), 0x7u);
|
||||
AppendVop3p(&code, 0x09u, 11, Vgpr(1), Vgpr(3), Vgpr(6), 0x7u);
|
||||
AppendVop3p(&code, 0x01u, 12, Vgpr(1), Vgpr(3), 0, 0x3u);
|
||||
AppendVop3p(&code, 0x02u, 13, Vgpr(1), Vgpr(2), 0, 0x3u);
|
||||
AppendVop3p(&code, 0x0bu, 14, Vgpr(2), Vgpr(3), 0, 0x3u);
|
||||
AppendVop3p(&code, 0x04u, 15, Vgpr(4), Vgpr(5), 0, 0x3u);
|
||||
AppendVop3p(&code, 0x05u, 16, Vgpr(4), Vgpr(5), 0, 0x3u);
|
||||
AppendVop3p(&code, 0x06u, 17, Vgpr(4), Vgpr(5), 0, 0x3u);
|
||||
AppendVop3p(&code, 0x08u, 18, Vgpr(1), Vgpr(2), 0, 0x3u);
|
||||
AppendVop3p(&code, 0x0cu, 19, Vgpr(1), Vgpr(2), 0, 0x3u);
|
||||
AppendVop3p(&code, 0x0du, 20, Vgpr(1), Vgpr(2), 0, 0x3u);
|
||||
|
||||
for (u32 i = 0; i < 11; i++) {
|
||||
AppendStoreVgpr(&code, 10 + i, i);
|
||||
}
|
||||
AppendEnd(&code);
|
||||
|
||||
TestCase test;
|
||||
test.name = "Vop3pInt16Ops";
|
||||
test.code = std::move(code);
|
||||
test.expected = {0x00058004u, 0x00058004u, 0x00007ffdu, 0x80018000u,
|
||||
0x0003fffeu, 0x008000f0u, 0x10020f00u, 0xf002ff00u,
|
||||
0x80000001u, 0x80007fffu, 0x00010001u};
|
||||
test.opcodes = {O::V_MOV_B32, O::V_PK_MAD_I16, O::V_PK_MAD_U16,
|
||||
O::V_PK_MUL_LO_U16, O::V_PK_ADD_I16, O::V_PK_SUB_U16,
|
||||
O::V_PK_LSHLREV_B16, O::V_PK_LSHRREV_B16, O::V_PK_ASHRREV_I16,
|
||||
O::V_PK_MIN_I16, O::V_PK_MAX_U16, O::V_PK_MIN_U16,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase CvtPkU8F32PacksSelectedByte() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -24657,6 +24947,46 @@ TestCase FloatInlineConstantF16UsesNumericValue() {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase Vop3Int16Ops() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
std::vector<u32> code;
|
||||
AppendVMovLiteral(&code, 1, 0xabcd0001u);
|
||||
AppendVMovLiteral(&code, 2, 0x12340002u);
|
||||
AppendVMovLiteral(&code, 3, 0x55557fffu);
|
||||
AppendVMovLiteral(&code, 4, 0x66668000u);
|
||||
AppendVMovLiteral(&code, 5, 0x77770013u);
|
||||
AppendVMovLiteral(&code, 6, 0x88888010u);
|
||||
AppendVMovLiteral(&code, 10, 0xaaaa0000u);
|
||||
AppendVMovLiteral(&code, 11, 0xbbbb0000u);
|
||||
AppendVMovLiteral(&code, 12, 0xcccc0000u);
|
||||
AppendVMovLiteral(&code, 13, 0xdddd0000u);
|
||||
AppendVMovLiteral(&code, 14, 0xeeee0000u);
|
||||
AppendVMovLiteral(&code, 15, 0x11110000u);
|
||||
|
||||
AppendVop3(&code, 0x304u, 10, Vgpr(1), Vgpr(2));
|
||||
AppendVop3(&code, 0x30du, 11, Vgpr(3), Vgpr(1));
|
||||
AppendVop3(&code, 0x308u, 12, Vgpr(5), Vgpr(6));
|
||||
AppendVop3(&code, 0x309u, 13, Vgpr(3), Vgpr(4));
|
||||
AppendVop3(&code, 0x30bu, 14, Vgpr(3), Vgpr(4));
|
||||
AppendVop3(&code, 0x308u, 15, Vgpr(5), Vgpr(3));
|
||||
|
||||
for (u32 i = 0; i < 6; i++) {
|
||||
AppendStoreVgpr(&code, 10 + i, i);
|
||||
}
|
||||
AppendEnd(&code);
|
||||
|
||||
TestCase test;
|
||||
test.name = "Vop3Int16Ops";
|
||||
test.code = std::move(code);
|
||||
test.expected = {0xaaaaffffu, 0xbbbb8000u, 0xccccf002u,
|
||||
0xdddd8000u, 0xeeee7fffu, 0x11110fffu};
|
||||
test.opcodes = {O::V_MOV_B32, O::V_SUB_NC_U16, O::V_ADD_NC_I16,
|
||||
O::V_ASHRREV_I16, O::V_MAX_U16, O::V_MIN_U16,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase VectorFloatControlContractPreservesInfNan() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -26426,6 +26756,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.
|
||||
@@ -26604,6 +27007,61 @@ TestCase VectorCompareF16Ops() {
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase VectorCompareI16U16Ops() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
std::vector<u32> code;
|
||||
std::vector<u32> expected;
|
||||
AppendVMovU32(&code, 1, 1);
|
||||
AppendVMovLiteral(&code, 2, 0xabcd8000u);
|
||||
AppendVMovLiteral(&code, 3, 0x00007fffu);
|
||||
AppendVMovLiteral(&code, 4, 0x12348000u);
|
||||
AppendVMovLiteral(&code, 5, 0xffff0001u);
|
||||
AppendVMovLiteral(&code, 6, 0x00000001u);
|
||||
|
||||
u32 dst = 10;
|
||||
u32 out = 0;
|
||||
auto append_compare = [&](u32 opcode, u32 src0, u32 src1, bool value) {
|
||||
code.push_back(EncodeVopc(opcode, src0, src1));
|
||||
code.push_back(EncodeVop2(0x01, dst, InlineU32(0), 1));
|
||||
AppendStoreVgpr(&code, dst, out++);
|
||||
expected.push_back(value ? 1u : 0u);
|
||||
dst++;
|
||||
};
|
||||
|
||||
append_compare(0x8a, Vgpr(2), 4, true);
|
||||
append_compare(0x8a, Vgpr(2), 3, false);
|
||||
append_compare(0x8b, Vgpr(2), 3, true);
|
||||
append_compare(0x8b, Vgpr(3), 2, false);
|
||||
append_compare(0x8b, Vgpr(5), 6, true);
|
||||
append_compare(0x8c, Vgpr(3), 2, true);
|
||||
append_compare(0x8c, Vgpr(2), 3, false);
|
||||
append_compare(0x8d, Vgpr(2), 3, true);
|
||||
append_compare(0x8d, Vgpr(5), 6, false);
|
||||
|
||||
append_compare(0xaa, Vgpr(2), 4, true);
|
||||
append_compare(0xaa, Vgpr(2), 3, false);
|
||||
append_compare(0xab, Vgpr(3), 2, true);
|
||||
append_compare(0xab, Vgpr(2), 3, false);
|
||||
append_compare(0xac, Vgpr(2), 3, true);
|
||||
append_compare(0xac, Vgpr(3), 2, false);
|
||||
append_compare(0xad, Vgpr(2), 3, true);
|
||||
append_compare(0xad, Vgpr(5), 6, false);
|
||||
append_compare(0xae, Vgpr(2), 3, true);
|
||||
append_compare(0xae, Vgpr(3), 2, false);
|
||||
append_compare(0xae, Vgpr(5), 6, true);
|
||||
AppendEnd(&code);
|
||||
|
||||
return {"VectorCompareI16U16Ops",
|
||||
code,
|
||||
{},
|
||||
expected,
|
||||
{O::V_MOV_B32, O::V_CMP_EQ_I16, O::V_CMP_LE_I16, O::V_CMP_GT_I16,
|
||||
O::V_CMP_NE_I16, O::V_CMP_EQ_U16, O::V_CMP_LE_U16, O::V_CMP_GT_U16,
|
||||
O::V_CMP_NE_U16, O::V_CMP_GE_U16, O::V_CNDMASK_B32,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase Vop2SdwaCndmaskSourceModifier() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -35779,7 +36237,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 +36279,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 +36288,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);
|
||||
@@ -35854,6 +36320,7 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(Vop2PkFmacF16DppNegatesBothPackedHalves);
|
||||
AddCase(Vop3pOpselHiUsesArchitecturalSourceBits);
|
||||
AddCase(Vop3pIntegerNegationCapturedAndSelectedHalves);
|
||||
AddCase(Vop3pInt16Ops);
|
||||
AddCase(CvtPkU8F32PacksSelectedByte);
|
||||
AddCase(CvtPkrtzF16F32SubnormalRoundsTowardZero);
|
||||
AddCase(CvtPkrtzF16F32SdwaAndOutputModifiers);
|
||||
@@ -35893,6 +36360,7 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(MadMixF16NegHiIsAbsAndNegIsIndependent);
|
||||
AddCase(VectorVop3FmaF16UsesRdna2Opcode34b);
|
||||
AddCase(FloatInlineConstantF16UsesNumericValue);
|
||||
AddCase(Vop3Int16Ops);
|
||||
AddCase(VectorFloatControlContractPreservesInfNan);
|
||||
AddCase(VectorFloatArithmeticOps);
|
||||
AddCase(VectorMinMaxF32NanAndSignedZeroEdges);
|
||||
@@ -35932,9 +36400,12 @@ 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);
|
||||
AddCase(VectorCompareI16U16Ops);
|
||||
AddCase(Wave32VccMasksPreserveOtherHalf);
|
||||
AddCase(Vop2SdwaCndmaskSourceModifier);
|
||||
AddCase(Vop2SdwaCndmaskCapturedByte3Source);
|
||||
@@ -41179,35 +41650,97 @@ void CheckPm4WaitResume(RenderContext &renderer) {
|
||||
}
|
||||
|
||||
void CheckPm4RewindResume(RenderContext &renderer) {
|
||||
constexpr const char *name = "Pm4RewindResume";
|
||||
Loader::SymbolDatabase symbols;
|
||||
Libs::InitAgcDriver_1(&symbols);
|
||||
const auto *emit_symbol = symbols.FindByNid("DwICrVxerkY", Loader::SymbolType::Func);
|
||||
const auto *patch_symbol = symbols.FindByNid("eWaWyFegzgQ", Loader::SymbolType::Func);
|
||||
Require(name, "async exports",
|
||||
emit_symbol != nullptr && patch_symbol != nullptr &&
|
||||
emit_symbol->vaddr == reinterpret_cast<uint64_t>(&Gen5::AgcAcbRewind) &&
|
||||
patch_symbol->vaddr ==
|
||||
reinterpret_cast<uint64_t>(&Gen5::AgcRewindPatchSetRewindState),
|
||||
"async rewind imports did not resolve to the emitter and state patch");
|
||||
const auto async_patch = reinterpret_cast<decltype(&Gen5::AgcRewindPatchSetRewindState)>(
|
||||
patch_symbol->vaddr);
|
||||
|
||||
constexpr uint32_t sentinel = 0x12345678u;
|
||||
std::array<uint32_t, 4> packet{sentinel, sentinel, sentinel, sentinel};
|
||||
CommandBufferLayout acb{packet.data(), packet.data() + packet.size(),
|
||||
packet.data() + 1, packet.data() + 3, nullptr, nullptr, 0};
|
||||
auto *buffer = reinterpret_cast<Gen5::CommandBuffer *>(&acb);
|
||||
for (const uint8_t state : {0, 1}) {
|
||||
for (const uint8_t offload : {0, 1}) {
|
||||
acb.cursor_up = packet.data() + 1;
|
||||
Require(name, "async packet",
|
||||
Gen5::AgcAcbRewind(buffer, state, offload) == packet.data() + 1 &&
|
||||
acb.cursor_up == packet.data() + 3 &&
|
||||
packet[0] == sentinel && packet[3] == sentinel &&
|
||||
packet[1] == 0xc0005900u &&
|
||||
packet[2] == ((uint32_t{state} << 31u) | (uint32_t{offload} << 24u)),
|
||||
"async rewind packet or two-DWORD allocation differs from the native command");
|
||||
}
|
||||
}
|
||||
const auto before = packet;
|
||||
acb.cursor_up = packet.data() + 1;
|
||||
acb.reserved_dw = 1;
|
||||
Require(name, "bounded allocation",
|
||||
Gen5::AgcAcbRewind(buffer, 1, 1) == nullptr &&
|
||||
Gen5::AgcAcbRewind(nullptr, 1, 1) == nullptr &&
|
||||
acb.cursor_up == packet.data() + 1 && packet == before,
|
||||
"failed rewind allocation changed the cursor or command storage");
|
||||
|
||||
// Native patchers inspect only the opcode, preserving the header and low 31 bits.
|
||||
std::array<uint32_t, 2> command{0xa5ff591fu, 0x0155aa55u};
|
||||
Require(name, "patch preserves fields",
|
||||
async_patch(command.data(), 1) == 0 && command[0] == 0xa5ff591fu &&
|
||||
command[1] == 0x8155aa55u && async_patch(command.data(), 0) == 0 &&
|
||||
command[0] == 0xa5ff591fu && command[1] == 0x0155aa55u,
|
||||
"rewind patch changed the offload bit, header, or unrelated payload bits");
|
||||
command[0] = 0xc0001000u;
|
||||
const auto invalid = command;
|
||||
Require(name, "wrong opcode",
|
||||
async_patch(command.data(), 1) == static_cast<int>(0x8a6c000cu) &&
|
||||
command == invalid,
|
||||
"rewind patch accepted or modified a packet with the wrong opcode");
|
||||
|
||||
GraphicsInitJmpTables();
|
||||
CommandProcessor processor(renderer, 0);
|
||||
|
||||
uint32_t suffix = 0;
|
||||
const auto address = reinterpret_cast<uint64_t>(&suffix);
|
||||
std::array<uint32_t, 7> commands{};
|
||||
commands[0] = KYTY_PM4(2, Pm4::IT_REWIND, 0);
|
||||
commands[1] = 0;
|
||||
commands[2] = KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0);
|
||||
commands[3] = 0;
|
||||
commands[4] = static_cast<uint32_t>(address);
|
||||
commands[5] = static_cast<uint32_t>(address >> 32u);
|
||||
commands[6] = 33;
|
||||
|
||||
Pm4Execution execution;
|
||||
Require("Pm4RewindResume", "pending",
|
||||
processor.Process(execution, commands) == Pm4ProcessResult::Blocked &&
|
||||
suffix == 0,
|
||||
"pending rewind did not preserve its command position");
|
||||
Require("Pm4RewindResume", "patch valid",
|
||||
Gen5::AgcRewindPatchSetRewindState(commands.data(), 1) == 0 &&
|
||||
commands[1] == 0x80000000u,
|
||||
"rewind state patch did not set the valid bit");
|
||||
Require("Pm4RewindResume", "resume",
|
||||
processor.Process(execution, commands) ==
|
||||
Pm4ProcessResult::Complete &&
|
||||
suffix == 33,
|
||||
"valid rewind did not resume at the following packet");
|
||||
std::printf("[host] %-32s ok\n", "Pm4RewindResume");
|
||||
for (const bool async : {false, true}) {
|
||||
for (const uint8_t offload : {0, 1}) {
|
||||
if (!async && offload != 0) {
|
||||
continue;
|
||||
}
|
||||
uint32_t suffix = 0;
|
||||
const auto address = reinterpret_cast<uint64_t>(&suffix);
|
||||
std::array<uint32_t, 7> commands{
|
||||
0, 0, KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0), 0,
|
||||
static_cast<uint32_t>(address), static_cast<uint32_t>(address >> 32u), 33};
|
||||
CommandBufferLayout cb{commands.data(), commands.data() + 2,
|
||||
commands.data(), commands.data() + 2, nullptr, nullptr, 0};
|
||||
auto *command_buffer = reinterpret_cast<Gen5::CommandBuffer *>(&cb);
|
||||
const auto *emitted = async ? Gen5::AgcAcbRewind(command_buffer, 0, offload)
|
||||
: Gen5::AgcDcbRewind(command_buffer, 0);
|
||||
const auto patch = async ? async_patch : &Gen5::AgcRewindPatchSetRewindState;
|
||||
Pm4Execution execution;
|
||||
Require(name, "pending",
|
||||
emitted == commands.data() &&
|
||||
processor.Process(execution, commands) == Pm4ProcessResult::Blocked &&
|
||||
suffix == 0,
|
||||
"pending rewind did not preserve its command position");
|
||||
Require(name, "still pending",
|
||||
processor.Process(execution, commands) == Pm4ProcessResult::Blocked &&
|
||||
!execution.MadeProgress() && suffix == 0,
|
||||
"pending rewind advanced before its state was patched");
|
||||
Require(name, "patch and resume",
|
||||
patch(commands.data(), 1) == 0 &&
|
||||
commands[1] == (0x80000000u | (uint32_t{offload} << 24u)) &&
|
||||
processor.Process(execution, commands) == Pm4ProcessResult::Complete &&
|
||||
suffix == 33,
|
||||
"patched rewind lost the offload bit or failed to resume");
|
||||
}
|
||||
}
|
||||
std::printf("[host] %-32s ok\n", name);
|
||||
}
|
||||
|
||||
void CheckPm4CeCompletion(RenderContext &renderer) {
|
||||
@@ -41358,6 +41891,13 @@ int main(int argc, char **argv) {
|
||||
RunCase(&vulkan, VectorCompareF16Ops());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--int16-alu-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, VectorCompareI16U16Ops());
|
||||
RunCase(&vulkan, Vop3Int16Ops());
|
||||
RunCase(&vulkan, Vop3pInt16Ops());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--buffer-format-store-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, BufferStoreFormatXyzwPackedUnormSkinningVectors());
|
||||
@@ -41428,6 +41968,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 +42072,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 +42353,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 +42404,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());
|
||||
|
||||
Reference in New Issue
Block a user