Compare commits

...
7 changed files with 465 additions and 145 deletions

No files matched your search

+13 -7
View File
@@ -5,6 +5,7 @@
#include <algorithm>
#include <cstdint>
#include <iterator>
#include <map>
namespace Libs::Graphics {
@@ -17,14 +18,19 @@ public:
if (it != m_ranges.begin() && std::prev(it)->second >= address) {
it = std::prev(it);
}
uint64_t begin = address;
uint64_t last = end;
while (it != m_ranges.end() && it->first <= last) {
begin = std::min(begin, it->first);
last = std::max(last, it->second);
it = m_ranges.erase(it);
if (it == m_ranges.end() || it->first > address) {
it = m_ranges.emplace_hint(it, address, end);
} else {
if (it->second >= end) {
return;
}
it->second = end;
}
auto next = std::next(it);
while (next != m_ranges.end() && next->first <= it->second) {
it->second = std::max(it->second, next->second);
next = m_ranges.erase(next);
}
m_ranges.emplace(begin, last);
}
void Subtract(uint64_t address, uint64_t size) {
+13
View File
@@ -519,7 +519,20 @@ void GameController::SetDevices(int active_id) {
m_trigger_state = {};
}
// Input that changes no button or touch contact updates the newest unread sample, so a burst of
// stick or sensor events between two reads cannot push unread button or touch changes out.
void GameController::AddState() {
if (m_states_num != 0) {
const auto newest = (m_first_state + m_states_num - 1) % STATES_MAX;
auto& last = m_states[newest];
if (!m_obtained[newest] && last.buttons == m_state.buttons &&
last.touch[0].down == m_state.touch[0].down &&
last.touch[1].down == m_state.touch[1].down) {
last = m_state;
return;
}
}
if (m_states_num >= STATES_MAX) {
m_states_num = STATES_MAX - 1;
m_first_state = (m_first_state + 1) % STATES_MAX;
+119 -88
View File
@@ -13,6 +13,7 @@
#include <atomic>
#include <cmath>
#include <cstring>
#include <span>
#include <vector>
namespace Libs::Audio {
@@ -168,17 +169,12 @@ struct AudioOut2MasteringStatesV2 {
static std::atomic_uint64_t g_audioout2_next_context {1};
static std::atomic_uint64_t g_audioout2_next_port {1};
static AudioOut2UserHandle g_audioout2_next_user = 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;
int object_audio_handle = 0;
};
static constexpr uint32_t AUDIOOUT2_CHANNEL_PORTS_MAX = 32;
static constexpr uint32_t AUDIOOUT2_OBJECT_PORTS_MAX = 512;
static constexpr uint32_t AUDIOOUT2_PORT_SLOT_BITS = 10;
static constexpr uint32_t AUDIOOUT2_CONTEXT_SLOT_BITS = 4;
struct AudioOut2PortStateEntry {
bool used = false;
@@ -196,6 +192,19 @@ struct AudioOut2PortStateEntry {
bool attributes_locked = false;
std::vector<uint8_t> pcm_data;
};
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;
int object_audio_handle = 0;
uint32_t max_ports = 0;
// Channel slots precede object slots; each partition has its own creation limit.
std::vector<AudioOut2PortStateEntry> ports;
};
struct AudioOut2SpeakerArrayState {
bool used = false;
uint32_t num_speakers = 0;
@@ -223,13 +232,16 @@ struct AudioOut2Submission {
static Common::Mutex g_audioout2_state_mutex;
static std::array<AudioOut2ContextState, 16> g_audioout2_contexts;
static std::array<AudioOut2Submission, 16> g_audioout2_submissions;
static std::array<AudioOut2PortStateEntry, 256> g_audioout2_ports;
static std::vector<AudioOut2UserHandle> g_audioout2_users;
static std::vector<AudioOut2UserHandle> g_audioout2_users;
static Common::Mutex g_audioout2_speaker_array_mutex;
static std::array<AudioOut2SpeakerArrayState, 32> g_audioout2_speaker_arrays;
static Common::Mutex g_audioout2_latency_mutex;
static std::array<AudioOut2LatencyState, 16> g_audioout2_latencies;
static_assert(AUDIOOUT2_CHANNEL_PORTS_MAX + AUDIOOUT2_OBJECT_PORTS_MAX <=
(1u << AUDIOOUT2_PORT_SLOT_BITS));
static_assert(g_audioout2_contexts.size() == (1u << AUDIOOUT2_CONTEXT_SLOT_BITS));
static constexpr int AUDIO_OUT2_ERROR_NOT_READY = -2144960504; /* 0x80268008 */
static constexpr int AUDIO_OUT2_ERROR_PORT_FULL = -2144960494; /* 0x80268012 */
static constexpr int AUDIO_OUT2_ERROR_INVALID_PARAM = -2144960511; /* 0x80268001 */
@@ -262,6 +274,11 @@ static AudioOut2Submission& audioout2_submission(const AudioOut2ContextState& co
return g_audioout2_submissions[&context - g_audioout2_contexts.data()];
}
// Ticks of a sixth of a microsecond: one 48 kHz sample lasts exactly 125 of them.
static uint64_t audioout2_clock() {
return LibKernel::KernelGetProcessTime() * 6;
}
static uint32_t audioout2_grain_micros(uint32_t grains) {
const auto sample_count = (grains == 0 ? 512u : grains);
return std::max<uint32_t>((sample_count * 1000000u) / 48000u, 1000u);
@@ -331,49 +348,51 @@ static void audioout2_update_context_locked(AudioOut2ContextState* state) {
return;
}
const auto now = LibKernel::KernelGetProcessTime();
const auto now = audioout2_clock();
if (state->last_update == 0 || state->queued == 0) {
state->last_update = now;
return;
}
const auto grain_micros = static_cast<uint64_t>(audioout2_grain_micros(state->num_grains));
if (now <= state->last_update || grain_micros == 0) {
if (now <= state->last_update) {
return;
}
const auto elapsed = now - state->last_update;
const auto drained = std::min<uint64_t>(state->queued, elapsed / grain_micros);
const auto grain = uint64_t {state->num_grains} * 125;
const auto drained = std::min<uint64_t>(state->queued, (now - state->last_update) / grain);
if (drained == 0) {
return;
}
state->queued -= static_cast<uint32_t>(drained);
state->last_update += drained * grain_micros;
state->last_update += drained * grain;
if (state->queued == 0) {
state->last_update = now;
}
}
static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle port) {
for (auto& state: g_audioout2_ports) {
if (state.used && state.handle == port) {
return &state;
}
const auto context_slot =
(port >> AUDIOOUT2_PORT_SLOT_BITS) & ((1u << AUDIOOUT2_CONTEXT_SLOT_BITS) - 1u);
const auto port_slot = port & ((1u << AUDIOOUT2_PORT_SLOT_BITS) - 1u);
auto& context = g_audioout2_contexts[context_slot];
if (context.used && port_slot < context.ports.size()) {
auto& state = context.ports[port_slot];
if (state.used && state.handle == port) return &state;
}
return nullptr;
}
static size_t audioout2_pcm_size(const AudioOut2PortStateEntry& state) {
const auto bytes_per_sample = (state.data_format & 0x7fu) == 1 ? sizeof(int16_t) : sizeof(float);
const auto bytes_per_sample =
(state.data_format & 0x7fu) == 1 ? sizeof(int16_t) : sizeof(float);
return static_cast<size_t>(state.samples_num) *
audioout2_data_format_channels(state.data_format) * bytes_per_sample;
}
static bool audioout2_context_has_queueable_device(const AudioOut2ContextState& context) {
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == context.handle && !state.pcm_data.empty()) {
for (const auto& state: context.ports) {
if (state.used && !state.pcm_data.empty()) {
const int handle = audioout2_port_type_is_object(state.port_type)
? context.object_audio_handle
: state.audio_handle;
@@ -427,18 +446,21 @@ static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, AudioOut2S
out.object_mix.clear();
std::vector<AudioInternal::OutputParam> params;
params.reserve(AudioInternal::OUT_PORTS_MAX);
for (const auto& state: g_audioout2_ports) {
if (!state.used || state.context != ctx || state.pcm_data.empty()) {
// A port's PCM covers one grain: the push consumes it, so a port the game stops feeding
// falls silent.
for (auto& state: context->ports) {
if (!state.used || state.pcm_data.empty()) {
continue;
}
if (audioout2_port_type_is_object(state.port_type)) {
audioout2_mix_object(out.object_mix, context->num_grains, state);
} else if (state.audio_handle > 0 && params.size() < AudioInternal::OUT_PORTS_MAX) {
const auto i = params.size();
out.pcm[i] = state.pcm_data;
out.pcm[i].swap(state.pcm_data);
out.gains[i] = state.gains;
params.push_back({state.audio_handle, out.pcm[i].data(), out.gains[i].data()});
}
state.pcm_data.clear();
}
if (!out.object_mix.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back({context->object_audio_handle, out.object_mix.data()});
@@ -467,22 +489,28 @@ int KYTY_SYSV_ABI AudioOut2ContextResetParam(AudioOut2ContextParam* params) {
EXIT_NOT_IMPLEMENTED(params == nullptr);
std::memset(params, 0, sizeof(AudioOut2ContextParam));
params->max_ports = 256;
params->max_object_ports = 256;
params->guarantee_object_ports = 0;
params->queue_depth = 4;
params->num_grains = 512;
params->flags = 1;
params->max_ports = 8;
params->queue_depth = 1;
params->num_grains = 256;
return OK;
}
static bool audioout2_valid_context_capacity(const AudioOut2ContextParam& params) {
return params.max_ports <= AUDIOOUT2_CHANNEL_PORTS_MAX &&
params.max_object_ports <= AUDIOOUT2_OBJECT_PORTS_MAX &&
params.guarantee_object_ports <= params.max_object_ports;
}
int KYTY_SYSV_ABI AudioOut2ContextQueryMemory(const AudioOut2ContextParam* params,
size_t* memory_size) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(params == nullptr);
EXIT_NOT_IMPLEMENTED(memory_size == nullptr);
if (!audioout2_valid_context_capacity(*params)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
const auto queue_depth = (params->queue_depth == 0 ? 4u : params->queue_depth);
*memory_size = 0x10000u + static_cast<size_t>(queue_depth) * 0x590u;
@@ -498,6 +526,9 @@ int KYTY_SYSV_ABI AudioOut2ContextCreate(const AudioOut2ContextParam* params, vo
EXIT_NOT_IMPLEMENTED(params == nullptr);
EXIT_NOT_IMPLEMENTED(ctx == nullptr);
if (!audioout2_valid_context_capacity(*params)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
const auto num_grains = params->num_grains == 0 ? 512u : params->num_grains;
const int object_audio_handle =
@@ -524,8 +555,10 @@ int KYTY_SYSV_ABI AudioOut2ContextCreate(const AudioOut2ContextParam* params, vo
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->last_update = audioout2_clock();
state->object_audio_handle = object_audio_handle;
state->max_ports = params->max_ports;
state->ports.resize(params->max_ports + params->max_object_ports);
g_audioout2_state_mutex.Unlock();
LOGF("\t buffer = 0x%016" PRIx64 "\n"
@@ -541,8 +574,8 @@ int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
PRINT_NAME();
LOGF("\t ctx = 0x%016" PRIx64 "\n", ctx);
std::array<int, 257> audio_handles {};
size_t audio_handles_num = 0;
std::array<int, AUDIOOUT2_CHANNEL_PORTS_MAX + 1> audio_handles {};
size_t audio_handles_num = 0;
g_audioout2_state_mutex.Lock();
auto* state = audioout2_find_context_locked(ctx);
@@ -550,15 +583,12 @@ int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
if (state->object_audio_handle > 0) {
audio_handles[audio_handles_num++] = state->object_audio_handle;
}
*state = AudioOut2ContextState {};
}
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()) {
for (const auto& port_state: state->ports) {
if (port_state.used && port_state.audio_handle > 0) {
audio_handles[audio_handles_num++] = port_state.audio_handle;
}
port_state = AudioOut2PortStateEntry {};
}
*state = AudioOut2ContextState {};
}
g_audioout2_state_mutex.Unlock();
@@ -586,9 +616,7 @@ int KYTY_SYSV_ABI AudioOut2ContextAdvance(AudioOut2ContextHandle ctx) {
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
audioout2_update_context_locked(state);
}
for (auto& port: g_audioout2_ports) {
if (port.used && port.context == ctx) {
for (auto& port: state->ports) {
port.attributes_locked = false;
}
}
@@ -612,7 +640,7 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
sleep_micros = audioout2_grain_micros(state->num_grains);
if (state->queued < state->queue_depth || use_device_clock) {
if (state->queued == 0) {
state->last_update = LibKernel::KernelGetProcessTime();
state->last_update = audioout2_clock();
}
if (state->queued < state->queue_depth) {
state->queued++;
@@ -663,21 +691,19 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
EXIT_NOT_IMPLEMENTED(params == nullptr);
EXIT_NOT_IMPLEMENTED(port == nullptr);
const auto next_port = g_audioout2_next_port.fetch_add(1, std::memory_order_relaxed);
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);
const bool is_object = audioout2_port_type_is_object(params->port_type);
g_audioout2_state_mutex.Lock();
const auto* context_state = audioout2_find_context_locked(ctx);
auto* context_state = audioout2_find_context_locked(ctx);
if (context_state == nullptr) {
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
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))) {
if (is_object && (params->port_type != 0x100 || 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;
}
@@ -688,42 +714,45 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
AudioOut2PortStateEntry* port_state = nullptr;
for (auto& candidate: g_audioout2_ports) {
if (!candidate.used) {
port_state = &candidate;
break;
}
}
if (port_state != nullptr) {
*port_state = AudioOut2PortStateEntry {};
port_state->used = true;
port_state->handle = next_port;
port_state->context = ctx;
port_state->user = params->user_handle;
port_state->port_type = params->port_type;
port_state->data_format = params->data_format;
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_state_mutex.Unlock();
if (port_state == nullptr) {
auto slots = std::span(context_state->ports);
slots =
is_object ? slots.subspan(context_state->max_ports) : slots.first(context_state->max_ports);
const auto available = std::find_if(slots.begin(), slots.end(),
[](const auto& candidate) { return !candidate.used; });
if (available == slots.end()) {
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_PORT_FULL;
}
const auto serial = g_audioout2_next_port.fetch_add(1, std::memory_order_relaxed);
const auto context_slot = static_cast<uint64_t>(context_state - g_audioout2_contexts.data());
const auto port_slot = static_cast<uint64_t>(&*available - context_state->ports.data());
const auto next_port = (serial << (AUDIOOUT2_PORT_SLOT_BITS + AUDIOOUT2_CONTEXT_SLOT_BITS)) |
(context_slot << AUDIOOUT2_PORT_SLOT_BITS) | port_slot;
auto* port_state = &*available;
*port_state = AudioOut2PortStateEntry {};
port_state->used = true;
port_state->handle = next_port;
port_state->context = ctx;
port_state->user = params->user_handle;
port_state->port_type = params->port_type;
port_state->data_format = params->data_format;
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_state_mutex.Unlock();
int audio_handle = 0;
if (audio_format != AudioInternal::Format::Unknown &&
!audioout2_port_type_is_object(params->port_type)) {
if (audio_format != AudioInternal::Format::Unknown && !is_object) {
audio_handle = AudioInternal::AudioOutOpen(audio_type, samples_num, params->sampling_freq,
audio_format);
}
g_audioout2_state_mutex.Lock();
const bool reserved = port_state->used && port_state->handle == next_port;
// Context destruction may have freed or replaced the slots while the backend opened.
port_state = audioout2_find_port_locked(next_port);
const bool reserved = port_state != nullptr;
if (reserved) {
port_state->audio_handle = audio_handle;
}
@@ -735,7 +764,7 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
*port = next_port;
if (next_port <= 16 || (next_port % 600) == 0) {
if (serial <= 16 || (serial % 600) == 0) {
PRINT_NAME();
LOGF("\t ctx = 0x%016" PRIx64 "\n"
"\t port = 0x%016" PRIx64 "\n"
@@ -901,21 +930,23 @@ 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_state_mutex);
const auto it = std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle);
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;
}
if (std::any_of(g_audioout2_ports.begin(), g_audioout2_ports.end(),
[handle](const auto& port) { return port.used && port.user == handle; })) {
return AUDIO_OUT2_ERROR_BUSY;
for (const auto& context: g_audioout2_contexts) {
if (std::any_of(context.ports.begin(), context.ports.end(),
[handle](const auto& port) { return port.used && port.user == handle; })) {
return AUDIO_OUT2_ERROR_BUSY;
}
}
g_audioout2_users.erase(it);
return OK;
}
int KYTY_SYSV_ABI AudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle,
uint32_t* context_attributes,
uint32_t* port_attributes) {
uint32_t* context_attributes,
uint32_t* port_attributes) {
if (context_attributes == nullptr || port_attributes == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
+31 -14
View File
@@ -615,7 +615,12 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
Common::LockGuard lock(g_mount_mutex);
const std::string title_id = cond->title_id != nullptr ? cond->title_id->data : get_title_id();
const auto root = save_user_directory(title_id, cond->user_id);
std::vector<std::string> dir_list;
struct Found {
uint64_t key = 0;
std::string name;
auto operator<=>(const Found&) const = default;
};
std::vector<Found> dir_list;
if (Common::File::IsDirectoryExisting(root)) {
for (const auto& entry: Common::File::GetDirEntries(root)) {
@@ -625,22 +630,34 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
if (cond->dir_name == nullptr || cond->dir_name->data[0] == '\0' ||
dir_name_match(Common::ToLower(entry.name).c_str(),
Common::ToLower(std::string(cond->dir_name->data)).c_str())) {
dir_list.push_back(entry.name);
dir_list.push_back({.name = entry.name});
}
}
}
}
std::sort(dir_list.begin(), dir_list.end(), [](const std::string& a, const std::string& b) {
return std::strcmp(a.c_str(), b.c_str()) < 0;
});
if (cond->order == SaveDataSortOrder::Descent) {
std::vector<std::string> reversed;
for (size_t i = dir_list.size(); i > 0; i--) {
reversed.push_back(dir_list[i - 1]);
// A save whose metadata cannot be read sorts as 0.
for (auto& found: dir_list) {
const auto directory = root / found.name;
switch (cond->key) {
case SaveDataSortKey::UserParam: {
SaveDataParam param {};
read_save_param(save_param_path(directory), &param);
found.key = param.user_param;
break;
}
case SaveDataSortKey::Blocks:
case SaveDataSortKey::FreeBlocks: read_save_blocks(directory, &found.key); break;
case SaveDataSortKey::Mtime:
found.key = static_cast<uint64_t>(newest_save_time(directory));
break;
default: break;
}
dir_list = reversed;
}
// Directory names break ties; descending order reverses both.
std::sort(dir_list.begin(), dir_list.end());
if (cond->order == SaveDataSortOrder::Descent) {
std::reverse(dir_list.begin(), dir_list.end());
}
auto max_count =
@@ -651,9 +668,9 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
for (size_t i = 0; i < max_count; i++) {
std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s",
dir_list[i].c_str());
dir_list[i].name.c_str());
if (result->params != nullptr) {
const int status = load_save_param(root / dir_list[i], &result->params[i]);
const int status = load_save_param(root / dir_list[i].name, &result->params[i]);
if (status != OK) {
return status;
}
@@ -661,7 +678,7 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
if (result->infos != nullptr) {
auto& info = result->infos[i];
info = {};
const int status = read_save_blocks(root / dir_list[i], &info.blocks);
const int status = read_save_blocks(root / dir_list[i].name, &info.blocks);
if (status != OK) {
return status;
}
+225 -36
View File
@@ -35,6 +35,8 @@ int g_open_waiters = 0;
bool g_block_opens = false;
bool g_block_outputs = false;
std::vector<int> g_blocked_output;
std::atomic_uint64_t g_process_time {0};
std::atomic_bool g_process_time_frozen {false};
void Check(bool value, const char* text) {
if (!value) {
@@ -109,8 +111,10 @@ PortParam MakeParam(uint32_t data_format = 0x200) {
return param;
}
AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4, uint32_t max_object_ports = 0) {
AudioOut2::AudioOut2ContextHandle
CreateContext(uint32_t queue_depth = 4, uint32_t max_object_ports = 0, uint32_t max_ports = 8) {
ContextParam param {};
param.max_ports = max_ports;
param.max_object_ports = max_object_ports;
param.queue_depth = queue_depth;
param.num_grains = 512;
@@ -279,13 +283,139 @@ void TestSlotReuse() {
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestObjectPortCapacityPerContext() {
constexpr int port_full = static_cast<int32_t>(0x80268012u);
constexpr int invalid_port = static_cast<int32_t>(0x80268009u);
std::array<AudioOut2::AudioOut2ContextHandle, 3> contexts {};
std::array<AudioOut2::AudioOut2PortHandle, 3> last_ports {};
auto object = MakeParam(0x100);
object.port_type = 0x100;
for (size_t i = 0; i < contexts.size(); i++) {
contexts[i] = CreateContext(4, 512, 32);
CreatePort(contexts[i], MakeParam());
for (size_t j = 0; j < 512; j++) {
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(contexts[i], AsParam(&object), &port) == OK,
"context could not create all 512 object ports");
last_ports[i] = port;
}
AudioOut2::AudioOut2PortHandle overflow = UINT64_MAX;
Check(AudioOut2::AudioOut2PortCreate(contexts[i], AsParam(&object), &overflow) ==
port_full &&
overflow == UINT64_MAX,
"object capacity did not reject the 513th port without changing its output");
CreatePort(contexts[i], MakeParam());
}
for (size_t i = 0; i < contexts.size(); i++) {
const auto stale = last_ports[i];
AudioOut2::AudioOut2PortDestroy(stale);
last_ports[i] = CreatePort(contexts[i], object);
Check(last_ports[i] != stale &&
AudioOut2::AudioOut2PortSetAttributes(stale, nullptr, 0) == invalid_port,
"reused object slot accepted its old handle");
std::array<float, 512> pcm;
pcm.fill(0.25f * static_cast<float>(i + 1));
SetPcm(last_ports[i], pcm.data());
const auto output = PushPcm(contexts[i], 512 * 2 * sizeof(float));
const float expected = pcm[0] * 0.70710678f;
Check(output.size() == 1, "last object port did not reach its context's mixer");
CheckSamples(output[0], {expected, expected}, "object audio crossed context boundaries");
}
AudioOut2::AudioOut2ContextDestroy(contexts[0]);
Check(AudioOut2::AudioOut2PortSetAttributes(last_ports[0], nullptr, 0) == invalid_port,
"destroyed context retained its object handles");
const auto replacement = CreateContext(4, 512);
for (size_t i = 0; i < 512; i++) {
CreatePort(replacement, object);
}
Check(AudioOut2::AudioOut2PortSetAttributes(last_ports[0], nullptr, 0) == invalid_port,
"reused context accepted an old object handle");
for (size_t i = 1; i < contexts.size(); i++) {
Check(AudioOut2::AudioOut2PortSetAttributes(last_ports[i], nullptr, 0) == OK,
"destroying another context invalidated a live object port");
AudioOut2::AudioOut2ContextDestroy(contexts[i]);
}
AudioOut2::AudioOut2ContextDestroy(replacement);
Check(LiveDeviceCount() == 0, "object capacity test leaked audio devices");
}
void TestContextPortLimits() {
constexpr int port_full = static_cast<int32_t>(0x80268012u);
ContextParam param {};
param.max_ports = 1;
param.max_object_ports = 2;
param.num_grains = 512;
AudioOut2::AudioOut2ContextHandle context = 0;
Check(AudioOut2::AudioOut2ContextCreate(AsParam(&param), nullptr, 0, &context) == OK,
"small context create failed");
auto object = MakeParam(0x100);
object.port_type = 0x100;
const auto channel = CreatePort(context, MakeParam());
const auto first_object = CreatePort(context, object);
CreatePort(context, object);
AudioOut2::AudioOut2PortHandle overflow = 0;
const auto channel_param = MakeParam();
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&channel_param), &overflow) ==
port_full &&
AudioOut2::AudioOut2PortCreate(context, AsParam(&object), &overflow) == port_full,
"declared channel/object capacities were ignored");
AudioOut2::AudioOut2PortDestroy(channel);
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&object), &overflow) == port_full,
"object port borrowed a channel slot");
CreatePort(context, channel_param);
AudioOut2::AudioOut2PortDestroy(first_object);
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&channel_param), &overflow) == port_full,
"channel port borrowed an object slot");
CreatePort(context, object);
AudioOut2::AudioOut2ContextDestroy(context);
param.max_ports = param.max_object_ports = 0;
Check(AudioOut2::AudioOut2ContextCreate(AsParam(&param), nullptr, 0, &context) == OK,
"empty context create failed");
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&channel_param), &overflow) ==
port_full &&
AudioOut2::AudioOut2PortCreate(context, AsParam(&object), &overflow) == port_full,
"zero port capacity was ignored");
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestContextCapacityParameters() {
constexpr int invalid_param = static_cast<int32_t>(0x80268001u);
ContextParam param;
std::memset(&param, 0xff, sizeof(param));
Check(AudioOut2::AudioOut2ContextResetParam(
reinterpret_cast<AudioOut2::AudioOut2ContextParam*>(&param)) == OK &&
param.max_ports == 8 && param.max_object_ports == 0 &&
param.guarantee_object_ports == 0 && param.num_grains == 256 &&
param.queue_depth == 1 && param.flags == 0 &&
std::all_of(std::begin(param.reserved), std::end(param.reserved),
[](uint32_t word) { return word == 0; }),
"context reset did not produce valid default parameters");
const auto defaults = param;
for (size_t i = 0; i < 3; i++) {
param = defaults;
if (i == 0) param.max_ports = 33;
if (i == 1) param.max_object_ports = 513;
if (i == 2) param.guarantee_object_ports = 1;
size_t memory = SIZE_MAX;
AudioOut2::AudioOut2ContextHandle context = UINT64_MAX;
Check(AudioOut2::AudioOut2ContextQueryMemory(AsParam(&param), &memory) == invalid_param &&
memory == SIZE_MAX &&
AudioOut2::AudioOut2ContextCreate(AsParam(&param), nullptr, 0, &context) ==
invalid_param &&
context == UINT64_MAX && LiveDeviceCount() == 0,
"invalid capacities allocated resources or modified outputs");
}
}
void TestFullTableRecovers() {
const auto context = CreateContext();
const auto param = MakeParam();
std::vector<AudioOut2::AudioOut2PortHandle> ports;
ports.reserve(256);
ports.reserve(8);
for (int i = 0; i < 256; i++) {
for (int i = 0; i < 8; i++) {
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK,
"port table filled early");
@@ -322,6 +452,11 @@ void TestConcurrentCreates() {
});
}
WaitForDeviceOpens(thread_count);
AudioOut2::AudioOut2PortHandle overflow = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &overflow) ==
static_cast<int32_t>(0x80268012u) &&
overflow == 0,
"pending creates did not reserve their channel capacity");
ReleaseDeviceOpens();
for (auto& thread: threads) {
thread.join();
@@ -349,17 +484,31 @@ void TestContextDestroyCancelsPendingCreate() {
[&]() { result = AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port); });
WaitForDeviceOpens(1);
AudioOut2::AudioOut2ContextDestroy(context);
const auto replacement = CreateContext();
AudioOut2::AudioOut2PortHandle replacement_port = 0;
int replacement_result = OK;
std::thread replacement_creator([&]() {
replacement_result =
AudioOut2::AudioOut2PortCreate(replacement, AsParam(&param), &replacement_port);
});
WaitForDeviceOpens(2);
ReleaseDeviceOpens();
creator.join();
replacement_creator.join();
Check(result != OK, "destroyed context retained a pending port create");
Check(replacement_result == OK &&
AudioOut2::AudioOut2PortSetAttributes(replacement_port, nullptr, 0) == OK &&
LiveDeviceCount() == 1,
"cancelled create corrupted the replacement context or leaked a device");
AudioOut2::AudioOut2ContextDestroy(replacement);
Check(LiveDeviceCount() == 0, "cancelled port create leaked a device");
}
void TestSynchronousDevicePushBypassesModelledQueue() {
const auto context = CreateContext(1);
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle port = 0;
const auto context = CreateContext(1);
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK,
"device port create failed");
@@ -368,6 +517,7 @@ void TestSynchronousDevicePushBypassesModelledQueue() {
ResetOutputCalls();
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "first sync push failed");
SetPcm(port, pcm);
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK,
"device-paced sync push was blocked by modelled queue");
const auto calls = OutputCalls();
@@ -447,6 +597,44 @@ void TestHandleWithoutPcmDoesNotBypassQueue() {
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestPushConsumesPcm() {
const auto context = CreateContext();
const auto port = CreatePort(context, MakeParam());
uint32_t pcm[512] {};
SetPcm(port, pcm);
ResetOutputCalls();
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "push with PCM failed");
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "push without new PCM failed");
SetPcm(port, pcm);
SetPcm(port, nullptr);
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "push after clearing PCM failed");
Check(OutputCalls().size() == 1, "a push replayed PCM the game did not set again");
AudioOut2::AudioOut2PortDestroy(port);
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestQueueDrainsAtExactGrainRate() {
// Three 512-sample grains play in exactly 32000 us.
const auto context = CreateContext();
g_process_time_frozen = true;
const auto start = g_process_time.load();
for (int push = 0; push < 3; push++) {
Check(AudioOut2::AudioOut2ContextPush(context, 0) == OK, "async push failed");
}
const auto queued_after = [&](uint64_t micros) {
g_process_time = start + micros;
uint32_t queued = 0;
Check(AudioOut2::AudioOut2ContextGetQueueLevel(context, &queued, nullptr) == OK,
"queue-level query failed");
return queued;
};
Check(queued_after(31999) == 1, "the third grain drained before it finished playing");
Check(queued_after(32000) == 0, "three grains did not drain in their playing time");
g_process_time_frozen = false;
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestPcmCopiedBeforeScratchBufferReuse() {
const auto context = CreateContext();
const auto param = MakeParam();
@@ -486,7 +674,6 @@ 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");
@@ -494,10 +681,11 @@ void TestPortGainAndValidation() {
Check(g_output_gains.size() == 1 && g_output_gains[0] == gain,
"incorrect per-channel gain forwarded to backend");
};
SetPcm(port, pcm.data());
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);
SetPcm(port, pcm.data());
check_output(gain, 0.25f);
}
@@ -507,10 +695,11 @@ void TestPortGainAndValidation() {
SetPcm(port, pcm.data());
check_output(gain, 0.75f);
std::fill(pcm.begin(), pcm.end(), -0.25f);
const Pcm replacement {pcm.data()};
const std::vector<float> replacement_pcm(pcm.size(), -0.25f);
const Pcm replacement {replacement_pcm.data()};
for (const float invalid: {-1.0f, std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::quiet_NaN()}) {
SetPcm(port, pcm.data());
const std::array invalid_gain {1.0f, invalid};
const Attribute attributes[] {{0, 0, &replacement, sizeof(replacement)},
{1, 0, &invalid_gain, sizeof(invalid_gain)}};
@@ -523,6 +712,7 @@ void TestPortGainAndValidation() {
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) ==
static_cast<int32_t>(0x80268001u), "malformed gain was accepted");
}
SetPcm(port, pcm.data());
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) ==
@@ -553,12 +743,10 @@ void TestObjectGainAndAmbisonics() {
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");
}
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) {
@@ -569,10 +757,8 @@ void TestObjectGainAndAmbisonics() {
{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");
}
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);
@@ -599,22 +785,21 @@ void TestObjectBusRouting() {
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());
const auto object = CreatePort(context, param);
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());
const auto other_user_object = CreatePort(context, param);
std::vector<float> object_pcm(512, 0.25f);
SetPcm(object, object_pcm.data());
SetPcm(other_user_object, object_pcm.data());
SetPcm(CreatePort(other_context, param), std::vector<float>(512, 1.0f).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);
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(object, object_pcm.data());
SetPcm(other_user_object, object_pcm.data());
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");
@@ -731,8 +916,7 @@ uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking)
namespace Libs::LibKernel {
uint64_t KYTY_SYSV_ABI KernelGetProcessTime() {
static std::atomic_uint64_t now {0};
return now.fetch_add(1000);
return g_process_time_frozen ? g_process_time.load() : g_process_time.fetch_add(1000);
}
} // namespace Libs::LibKernel
@@ -741,6 +925,9 @@ int main() {
Check(AudioOut2::AudioOut2UserCreate(1000, &g_user_handle) == OK, "test user create failed");
TestUserSupportedAttributes();
TestSlotReuse();
TestObjectPortCapacityPerContext();
TestContextPortLimits();
TestContextCapacityParameters();
TestFullTableRecovers();
TestConcurrentCreates();
TestContextDestroyCancelsPendingCreate();
@@ -748,6 +935,8 @@ int main() {
TestFloat12ChannelPortOutputsPcm();
TestAsynchronousDevicePushKeepsQueueBounded();
TestHandleWithoutPcmDoesNotBypassQueue();
TestPushConsumesPcm();
TestQueueDrainsAtExactGrainRate();
TestPcmCopiedBeforeScratchBufferReuse();
TestPortGainAndValidation();
TestObjectGainAndAmbisonics();
+22
View File
@@ -689,6 +689,27 @@ void TestTriggerEffectState() {
check_state(0, 0, "switching controllers retained effect states");
}
void TestUnreadPressSurvivesStickBurst() {
Controller controller;
std::array<PadData, 64> data {};
SetButton(1, PAD_BUTTON_CROSS, true);
for (int value = 0; value < 100; value++) {
SetAxis(1, Axis::LeftX, value);
}
SetButton(1, PAD_BUTTON_CROSS, false);
for (int value = 100; value < 200; value++) {
SetAxis(1, Axis::LeftX, value);
}
Check(PadRead(1, data.data(), 64) == 2 && data[0].buttons == PAD_BUTTON_CROSS &&
data[0].left_stick_x == 99 && data[1].buttons == 0 && data[1].left_stick_x == 199,
"a stick burst pushed an unread press out of the pad history");
SetAxis(1, Axis::LeftX, 50);
SetButton(1, PAD_BUTTON_CROSS, true);
Check(PadRead(1, data.data(), 64) == 2 && data[0].buttons == 0 &&
data[0].left_stick_x == 50 && data[1].buttons == PAD_BUTTON_CROSS,
"a stick move after a read did not add a sample");
}
} // namespace
int main() {
@@ -702,6 +723,7 @@ int main() {
TestMaskedTriggersAndValidation();
TestIndependentOutputsAndPadSwitch();
TestTriggerEffectState();
TestUnreadPressSurvivesStickBurst();
TestMultiplayerRouting();
TestDedicatedKeyboardOutputs();
Config::Shutdown();
+42
View File
@@ -660,6 +660,47 @@ void TestClassicSaveParams() {
CHECK(params[0].mtime == nested_mtime);
}
void TestSearchSortKeys() {
Reset("SORTED");
const std::array names {DirName("alpha"), DirName("bravo"), DirName("charlie")};
constexpr std::array<uint64_t, 3> blocks {96, 48, 48};
constexpr std::array<uint32_t, 3> user_params {2, 3, 1};
constexpr std::array<int, 3> hours_ahead {1, 3, 2};
for (size_t i = 0; i < names.size(); i++) {
struct SaveDataMount3 mount {};
mount.user_id = 1;
mount.dir_name = &names[i];
mount.mount_mode = 4;
mount.blocks = blocks[i];
SaveDataMountResult result {};
CHECK(SaveDataMount3(&mount, &result) == OK);
CHECK(SaveDataSetParam(&result.mount_point, 4, &user_params[i], sizeof(user_params[i])) ==
OK);
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
fs::last_write_time(fs::path("_SaveData/SORTED") / names[i].data / "sce_sys/param.bin",
fs::file_time_type::clock::now() + std::chrono::hours(hours_ahead[i]));
}
const auto search = [](SaveDataSortKey key, SaveDataSortOrder order) {
std::array<SceSaveDataDirName, 3> found {};
SaveDataDirNameSearchCond cond {};
cond.user_id = 1;
cond.key = key;
cond.order = order;
SaveDataDirNameSearchResult result {};
result.dir_names = found.data();
result.dir_names_num = found.size();
CHECK(SaveDataDirNameSearch(&cond, &result) == OK && result.set_num == 3);
return std::string(found[0].data) + " " + found[1].data + " " + found[2].data;
};
CHECK(search(SaveDataSortKey::DirName, SaveDataSortOrder::Descent) == "charlie bravo alpha");
CHECK(search(SaveDataSortKey::UserParam, SaveDataSortOrder::Ascent) == "charlie alpha bravo");
CHECK(search(SaveDataSortKey::Blocks, SaveDataSortOrder::Ascent) == "bravo charlie alpha");
CHECK(search(SaveDataSortKey::FreeBlocks, SaveDataSortOrder::Descent) == "alpha charlie bravo");
CHECK(search(SaveDataSortKey::Mtime, SaveDataSortOrder::Descent) == "bravo charlie alpha");
CHECK(fs::remove("_SaveData/SORTED/alpha/sce_sys/blocks.bin"));
CHECK(search(SaveDataSortKey::Blocks, SaveDataSortOrder::Ascent) == "alpha bravo charlie");
}
void RunChild(const fs::path& executable, const char* mode) {
#ifdef _WIN32
CHECK(_spawnl(_P_WAIT, executable.string().c_str(), executable.string().c_str(), mode,
@@ -706,6 +747,7 @@ int main(int argc, char** argv) {
TestGuestSaveIsolation();
TestSaveAllocations();
TestClassicSaveParams();
TestSearchSortKeys();
CHECK(SaveDataTerminate() == OK);
fs::current_path(previous);
fs::remove_all(temp);