// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include #include "common/alignment.h" #include "common/assert.h" #include "common/logging/log.h" #include "core/libraries/ajm/ajm.h" #include "core/libraries/ajm/ajm_at9.h" #include "core/libraries/ajm/ajm_error.h" #include "core/libraries/ajm/ajm_instance.h" #include "core/libraries/ajm/ajm_mp3.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" extern "C" { #include #include #include #include #include } namespace Libraries::Ajm { static constexpr u32 AJM_INSTANCE_STATISTICS = 0x80000; static constexpr u32 SCE_AJM_WAIT_INFINITE = -1; static constexpr u32 MaxInstances = 0x2fff; static constexpr u32 MaxBatches = 1000; struct BatchInfo { u16 instance{}; u16 offset_in_qwords{}; // Needed for AjmBatchError? bool waiting{}; bool finished{}; std::mutex mtx; std::condition_variable cv; int result{}; }; struct AjmDevice { u32 max_prio{}; u32 min_prio{}; u32 curr_cursor{}; u32 release_cursor{MaxInstances - 1}; std::array is_registered{}; std::array free_instances{}; std::array, MaxInstances> instances; std::vector> batches{}; std::mutex batches_mutex; [[nodiscard]] bool IsRegistered(AjmCodecType type) const { return is_registered[static_cast(type)]; } void Register(AjmCodecType type) { is_registered[static_cast(type)] = true; } AjmDevice() { std::iota(free_instances.begin(), free_instances.end(), 1); } }; static std::unique_ptr dev{}; int PS4_SYSV_ABI sceAjmBatchCancel() { LOG_ERROR(Lib_Ajm, "(STUBBED) called"); return ORBIS_OK; } int PS4_SYSV_ABI sceAjmBatchErrorDump() { LOG_ERROR(Lib_Ajm, "(STUBBED) called"); return ORBIS_OK; } template ChunkType& AjmGetChunk(CursorType& p_cursor) { auto* const result = reinterpret_cast(p_cursor); p_cursor += sizeof(ChunkType); return *result; } template void AjmSkipChunk(CursorType& p_cursor) { p_cursor += sizeof(ChunkType); } template ChunkType& AjmPeekChunk(CursorType p_cursor) { return *reinterpret_cast(p_cursor); } void* PS4_SYSV_ABI sceAjmBatchJobControlBufferRa(void* p_buffer, u32 instance_id, u64 flags, void* p_sideband_input, size_t sideband_input_size, void* p_sideband_output, size_t sideband_output_size, void* p_return_address) { LOG_TRACE(Lib_Ajm, "called"); u8* p_current = (u8*)p_buffer; auto& header = AjmGetChunk(p_current); header.ident = AjmIdentJob; header.payload = instance_id; const u8* const p_begin = p_current; if (p_return_address != nullptr) { auto& chunk_ra = AjmGetChunk(p_current); chunk_ra.header.ident = AjmIdentReturnAddressBuf; chunk_ra.header.size = 0; chunk_ra.p_address = p_return_address; } { auto& chunk_input = AjmGetChunk(p_current); chunk_input.header.ident = AjmIdentInputControlBuf; chunk_input.header.size = sideband_input_size; chunk_input.p_address = p_sideband_input; } { // 0x0000'0000'C001'8007 (AJM_INSTANCE_STATISTICS): // | sideband | reserved | statistics | command | codec | revision | // | 000 | 0000000000000 | 11000000000000011 | 0000 | 00000000 | 111 | // statistics flags: // STATISTICS_ENGINE | STATISTICS_ENGINE_PER_CODEC | ??STATISTICS_UNK?? | STATISTICS_MEMORY // 0x0000'6000'0000'E7FF: // | sideband | reserved | control | run | codec | revision | // | 011 | 00000000000000000000000000000 | 111 | 00 | 11111111 | 111 | const bool is_statistics = instance_id == AJM_INSTANCE_STATISTICS; flags &= is_statistics ? 0x0000'0000'C001'8007 : 0x0000'6000'0000'E7FF; auto& chunk_flags = AjmGetChunk(p_current); chunk_flags.ident = AjmIdentControlFlags; chunk_flags.payload = u32(flags >> 32); chunk_flags.size = u32(flags); } { auto& chunk_output = AjmGetChunk(p_current); chunk_output.header.ident = AjmIdentOutputControlBuf; chunk_output.header.size = sideband_output_size; chunk_output.p_address = p_sideband_output; } header.size = u32(p_current - p_begin); return p_current; } void* PS4_SYSV_ABI sceAjmBatchJobInlineBuffer(void* p_buffer, const void* p_data_input, size_t data_input_size, const void** pp_batch_address) { LOG_TRACE(Lib_Ajm, "called"); u8* p_current = (u8*)p_buffer; auto& header = AjmGetChunk(p_current); header.ident = AjmIdentInlineBuf; header.size = Common::AlignUp(data_input_size, 8); *pp_batch_address = p_current; memcpy(p_current, p_data_input, data_input_size); return p_current + header.size; } void* PS4_SYSV_ABI sceAjmBatchJobRunBufferRa(void* p_buffer, u32 instance_id, u64 flags, void* p_data_input, size_t data_input_size, void* p_data_output, size_t data_output_size, void* p_sideband_output, size_t sideband_output_size, void* p_return_address) { LOG_TRACE(Lib_Ajm, "called"); u8* p_current = (u8*)p_buffer; auto& header = AjmGetChunk(p_current); header.ident = AjmIdentJob; header.payload = instance_id; const u8* const p_begin = p_current; if (p_return_address != nullptr) { auto& chunk_ra = AjmGetChunk(p_current); chunk_ra.header.ident = AjmIdentReturnAddressBuf; chunk_ra.header.size = 0; chunk_ra.p_address = p_return_address; } { auto& chunk_input = AjmGetChunk(p_current); chunk_input.header.ident = AjmIdentInputRunBuf; chunk_input.header.size = data_input_size; chunk_input.p_address = p_data_input; } { // 0x0000'E000'0000'1FFF: // | sideband | reserved | control | run | codec | revision | // | 111 | 00000000000000000000000000000 | 000 | 11 | 11111111 | 111 | flags &= 0x0000'E000'0000'1FFF; auto& chunk_flags = AjmGetChunk(p_current); chunk_flags.ident = AjmIdentRunFlags; chunk_flags.payload = u32(flags >> 32); chunk_flags.size = u32(flags); } { auto& chunk_output = AjmGetChunk(p_current); chunk_output.header.ident = AjmIdentOutputRunBuf; chunk_output.header.size = data_output_size; chunk_output.p_address = p_data_output; } { auto& chunk_output = AjmGetChunk(p_current); chunk_output.header.ident = AjmIdentOutputControlBuf; chunk_output.header.size = sideband_output_size; chunk_output.p_address = p_sideband_output; } header.size = u32(p_current - p_begin); return p_current; } void* PS4_SYSV_ABI sceAjmBatchJobRunSplitBufferRa( void* p_buffer, u32 instance_id, u64 flags, const AjmBuffer* p_data_input_buffers, size_t num_data_input_buffers, const AjmBuffer* p_data_output_buffers, size_t num_data_output_buffers, void* p_sideband_output, size_t sideband_output_size, void* p_return_address) { LOG_TRACE(Lib_Ajm, "called"); u8* p_current = (u8*)p_buffer; auto& header = AjmGetChunk(p_current); header.ident = AjmIdentJob; header.payload = instance_id; const u8* const p_begin = p_current; if (p_return_address != nullptr) { auto& chunk_ra = AjmGetChunk(p_current); chunk_ra.header.ident = AjmIdentReturnAddressBuf; chunk_ra.header.size = 0; chunk_ra.p_address = p_return_address; } for (s32 i = 0; i < num_data_input_buffers; i++) { auto& chunk_input = AjmGetChunk(p_current); chunk_input.header.ident = AjmIdentInputRunBuf; chunk_input.header.size = p_data_input_buffers[i].size; chunk_input.p_address = p_data_input_buffers[i].p_address; } { // 0x0000'E000'0000'1FFF: // | sideband | reserved | control | run | codec | revision | // | 111 | 00000000000000000000000000000 | 000 | 11 | 11111111 | 111 | flags &= 0x0000'E000'0000'1FFF; auto& chunk_flags = AjmGetChunk(p_current); chunk_flags.ident = AjmIdentRunFlags; chunk_flags.payload = u32(flags >> 32); chunk_flags.size = u32(flags); } for (s32 i = 0; i < num_data_output_buffers; i++) { auto& chunk_output = AjmGetChunk(p_current); chunk_output.header.ident = AjmIdentOutputRunBuf; chunk_output.header.size = p_data_output_buffers[i].size; chunk_output.p_address = p_data_output_buffers[i].p_address; } { auto& chunk_output = AjmGetChunk(p_current); chunk_output.header.ident = AjmIdentOutputControlBuf; chunk_output.header.size = sideband_output_size; chunk_output.p_address = p_sideband_output; } header.size = u32(p_current - p_begin); return p_current; } int PS4_SYSV_ABI sceAjmBatchStartBuffer(u32 context, const u8* batch, u32 batch_size, const int priority, AjmBatchError* batch_error, u32* out_batch_id) { LOG_INFO(Lib_Ajm, "called context = {}, batch_size = {:#x}, priority = {}", context, batch_size, priority); if ((batch_size & 7) != 0) { return ORBIS_AJM_ERROR_MALFORMED_BATCH; } const auto batch_info = std::make_shared(); if (dev->batches.size() >= MaxBatches) { LOG_ERROR(Lib_Ajm, "Too many batches in job!"); return ORBIS_AJM_ERROR_OUT_OF_MEMORY; } *out_batch_id = static_cast(dev->batches.size()); dev->batches.push_back(batch_info); const u8* p_current = batch; const u8* p_batch_end = batch + batch_size; while (p_current < p_batch_end) { auto& header = AjmGetChunk(p_current); ASSERT(header.ident == AjmIdentJob); std::optional job_flags = {}; std::optional input_control_buffer = {}; std::optional output_control_buffer = {}; boost::container::small_vector input_run_buffers; boost::container::small_vector output_run_buffers; // Read parameters of a job auto* const p_job_end = p_current + header.size; while (p_current < p_job_end) { auto& header = AjmPeekChunk(p_current); switch (header.ident) { case Identifier::AjmIdentInputRunBuf: { input_run_buffers.emplace_back(AjmGetChunk(p_current)); break; } case Identifier::AjmIdentInputControlBuf: { ASSERT_MSG(!input_control_buffer.has_value(), "Only one instance of input control buffer is allowed per job"); input_control_buffer = AjmGetChunk(p_current); break; } case Identifier::AjmIdentControlFlags: case Identifier::AjmIdentRunFlags: { ASSERT_MSG(!job_flags.has_value(), "Only one instance of job flags is allowed per job"); auto& flags_chunk = AjmGetChunk(p_current); job_flags = AjmJobFlags{ .raw = (u64(flags_chunk.payload) << 32) + flags_chunk.size, }; break; } case Identifier::AjmIdentReturnAddressBuf: { // Ignore return address buffers. AjmSkipChunk(p_current); break; } case Identifier::AjmIdentOutputRunBuf: { output_run_buffers.emplace_back(AjmGetChunk(p_current)); break; } case Identifier::AjmIdentOutputControlBuf: { ASSERT_MSG(!output_control_buffer.has_value(), "Only one instance of output control buffer is allowed per job"); output_control_buffer = AjmGetChunk(p_current); break; } default: LOG_ERROR(Lib_Ajm, "Unknown chunk: {}", header.ident); p_current += header.size; break; } } const u32 instance = header.payload; AjmInstance* p_instance = dev->instances[instance].get(); // Perform operation requested by control flags. const auto control_flags = job_flags.value().control_flags; if (True(control_flags & AjmJobControlFlags::Reset)) { LOG_TRACE(Lib_Ajm, "Resetting instance {}", instance); p_instance->Reset(); } if (True(control_flags & AjmJobControlFlags::Initialize)) { LOG_TRACE(Lib_Ajm, "Initializing instance {}", instance); ASSERT_MSG(input_control_buffer.has_value(), "Initialize called without control buffer"); const auto& in_buffer = input_control_buffer.value(); p_instance->Initialize(in_buffer.p_address, in_buffer.header.size); } if (True(control_flags & AjmJobControlFlags::Resample)) { LOG_ERROR(Lib_Ajm, "Unimplemented: Set resample params of instance {}", instance); } // Write sideband structures. auto* p_sideband = reinterpret_cast(output_control_buffer.value().p_address); auto* result = reinterpret_cast(p_sideband); result->result = 0; result->internal_result = 0; p_sideband += sizeof(AjmSidebandResult); // Perform operation requested by run flags. ASSERT_MSG(input_run_buffers.size() == output_run_buffers.size(), "Run operation with uneven input/output of buffers."); const auto run_flags = job_flags.value().run_flags; const auto sideband_flags = job_flags.value().sideband_flags; for (size_t i = 0; i < input_run_buffers.size(); ++i) { // Decode as much of the input bitstream as possible. const auto& in_buffer = input_run_buffers[i]; const auto& out_buffer = output_run_buffers[i]; const auto [in_remain, out_remain, num_frames] = p_instance->Decode( reinterpret_cast(in_buffer.p_address), in_buffer.header.size, reinterpret_cast(out_buffer.p_address), out_buffer.header.size); // Check sideband flags for decoding if (True(sideband_flags & AjmJobSidebandFlags::Stream)) { auto* stream = reinterpret_cast(p_sideband); stream->input_consumed = in_buffer.header.size - in_remain; stream->output_written = out_buffer.header.size - out_remain; stream->total_decoded_samples = p_instance->decoded_samples; p_sideband += sizeof(AjmSidebandStream); } if (True(run_flags & AjmJobRunFlags::MultipleFrames)) { auto* mframe = reinterpret_cast(p_sideband); mframe->num_frames = num_frames; p_sideband += sizeof(AjmSidebandMFrame); } } if (True(run_flags & AjmJobRunFlags::GetCodecInfo)) { p_instance->GetCodecInfo(p_sideband); } } batch_info->finished = true; return ORBIS_OK; } int PS4_SYSV_ABI sceAjmBatchWait(const u32 context, const u32 batch_id, const u32 timeout, AjmBatchError* const batch_error) { LOG_INFO(Lib_Ajm, "called context = {}, batch_id = {}, timeout = {}", context, batch_id, timeout); if (batch_id > 0xFF || batch_id >= dev->batches.size()) { return ORBIS_AJM_ERROR_INVALID_BATCH; } const auto& batch = dev->batches[batch_id]; if (batch->waiting) { return ORBIS_AJM_ERROR_BUSY; } batch->waiting = true; { std::unique_lock lk{batch->mtx}; if (!batch->cv.wait_for(lk, std::chrono::milliseconds(timeout), [&] { return batch->finished; })) { return ORBIS_AJM_ERROR_IN_PROGRESS; } } dev->batches.erase(dev->batches.begin() + batch_id); return 0; } int PS4_SYSV_ABI sceAjmDecAt9ParseConfigData() { LOG_ERROR(Lib_Ajm, "(STUBBED) called"); return ORBIS_OK; } int PS4_SYSV_ABI sceAjmDecMp3ParseFrame(const u8* buf, u32 stream_size, int parse_ofl, AjmDecMp3ParseFrame* frame) { LOG_INFO(Lib_Ajm, "called parse_ofl = {}", parse_ofl); if (buf == nullptr || stream_size < 4 || frame == nullptr) { return ORBIS_AJM_ERROR_INVALID_PARAMETER; } if ((buf[0] & SYNCWORDH) != SYNCWORDH || (buf[1] & SYNCWORDL) != SYNCWORDL) { return ORBIS_AJM_ERROR_INVALID_PARAMETER; } return AjmMp3Decoder::ParseMp3Header(buf, stream_size, parse_ofl, frame); } int PS4_SYSV_ABI sceAjmFinalize() { LOG_ERROR(Lib_Ajm, "(STUBBED) called"); return ORBIS_OK; } int PS4_SYSV_ABI sceAjmInitialize(s64 reserved, u32* out_context) { LOG_INFO(Lib_Ajm, "called reserved = {}", reserved); if (out_context == nullptr || reserved != 0) { return ORBIS_AJM_ERROR_INVALID_PARAMETER; } *out_context = 1; dev = std::make_unique(); return ORBIS_OK; } int PS4_SYSV_ABI sceAjmInstanceCodecType() { LOG_ERROR(Lib_Ajm, "(STUBBED) called"); return ORBIS_OK; } int PS4_SYSV_ABI sceAjmInstanceCreate(u32 context, AjmCodecType codec_type, AjmInstanceFlags flags, u32* out_instance) { if (codec_type >= AjmCodecType::Max) { return ORBIS_AJM_ERROR_INVALID_PARAMETER; } if (flags.version == 0) { return ORBIS_AJM_ERROR_WRONG_REVISION_FLAG; } if (!dev->IsRegistered(codec_type)) { return ORBIS_AJM_ERROR_CODEC_NOT_REGISTERED; } if (dev->curr_cursor == dev->release_cursor) { return ORBIS_AJM_ERROR_OUT_OF_RESOURCES; } ASSERT_MSG(flags.format == 0, "Only signed 16-bit PCM output is supported currently!"); const u32 index = dev->free_instances[dev->curr_cursor++]; dev->curr_cursor %= MaxInstances; std::unique_ptr instance; switch (codec_type) { case AjmCodecType::Mp3Dec: instance = std::make_unique(); break; case AjmCodecType::At9Dec: instance = std::make_unique(); break; default: UNREACHABLE_MSG("Codec #{} not implemented", u32(codec_type)); } instance->index = index; instance->codec_type = codec_type; instance->num_channels = flags.channels; dev->instances[index] = std::move(instance); *out_instance = index; LOG_INFO(Lib_Ajm, "called codec_type = {}, flags = {:#x}, instance = {}", magic_enum::enum_name(codec_type), flags.raw, index); return ORBIS_OK; } int PS4_SYSV_ABI sceAjmInstanceDestroy(u32 context, u32 instance) { LOG_INFO(Lib_Ajm, "called context = {}, instance = {}", context, instance); if ((instance & 0x3fff) > MaxInstances) { return ORBIS_AJM_ERROR_INVALID_INSTANCE; } const u32 next_slot = (dev->release_cursor + 1) % MaxInstances; if (next_slot != dev->curr_cursor) { dev->free_instances[dev->release_cursor] = instance; dev->release_cursor = next_slot; } dev->instances[instance].reset(); return ORBIS_OK; } int PS4_SYSV_ABI sceAjmInstanceExtend() { LOG_ERROR(Lib_Ajm, "(STUBBED) called"); return ORBIS_OK; } int PS4_SYSV_ABI sceAjmInstanceSwitch() { LOG_ERROR(Lib_Ajm, "(STUBBED) called"); return ORBIS_OK; } int PS4_SYSV_ABI sceAjmMemoryRegister() { LOG_ERROR(Lib_Ajm, "(STUBBED) called"); return ORBIS_OK; } int PS4_SYSV_ABI sceAjmMemoryUnregister() { LOG_ERROR(Lib_Ajm, "(STUBBED) called"); return ORBIS_OK; } int PS4_SYSV_ABI sceAjmModuleRegister(u32 context, AjmCodecType codec_type, s64 reserved) { LOG_INFO(Lib_Ajm, "called context = {}, codec_type = {}, reserved = {}", context, u32(codec_type), reserved); if (codec_type >= AjmCodecType::Max || reserved != 0) { return ORBIS_AJM_ERROR_INVALID_PARAMETER; } if (dev->IsRegistered(codec_type)) { return ORBIS_AJM_ERROR_CODEC_ALREADY_REGISTERED; } dev->Register(codec_type); return ORBIS_OK; } int PS4_SYSV_ABI sceAjmModuleUnregister() { LOG_ERROR(Lib_Ajm, "(STUBBED) called"); return ORBIS_OK; } int PS4_SYSV_ABI sceAjmStrError() { LOG_ERROR(Lib_Ajm, "(STUBBED) called"); return ORBIS_OK; } void RegisterlibSceAjm(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("NVDXiUesSbA", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmBatchCancel); LIB_FUNCTION("WfAiBW8Wcek", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmBatchErrorDump); LIB_FUNCTION("dmDybN--Fn8", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmBatchJobControlBufferRa); LIB_FUNCTION("stlghnic3Jc", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmBatchJobInlineBuffer); LIB_FUNCTION("ElslOCpOIns", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmBatchJobRunBufferRa); LIB_FUNCTION("7jdAXK+2fMo", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmBatchJobRunSplitBufferRa); LIB_FUNCTION("fFFkk0xfGWs", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmBatchStartBuffer); LIB_FUNCTION("-qLsfDAywIY", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmBatchWait); LIB_FUNCTION("1t3ixYNXyuc", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmDecAt9ParseConfigData); LIB_FUNCTION("eDFeTyi+G3Y", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmDecMp3ParseFrame); LIB_FUNCTION("MHur6qCsUus", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmFinalize); LIB_FUNCTION("dl+4eHSzUu4", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmInitialize); LIB_FUNCTION("diXjQNiMu-s", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmInstanceCodecType); LIB_FUNCTION("AxoDrINp4J8", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmInstanceCreate); LIB_FUNCTION("RbLbuKv8zho", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmInstanceDestroy); LIB_FUNCTION("YDFR0dDVGAg", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmInstanceExtend); LIB_FUNCTION("rgLjmfdXocI", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmInstanceSwitch); LIB_FUNCTION("bkRHEYG6lEM", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmMemoryRegister); LIB_FUNCTION("pIpGiaYkHkM", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmMemoryUnregister); LIB_FUNCTION("Q3dyFuwGn64", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmModuleRegister); LIB_FUNCTION("Wi7DtlLV+KI", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmModuleUnregister); LIB_FUNCTION("AxhcqVv5AYU", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmStrError); }; } // namespace Libraries::Ajm