diff --git a/.gitmodules b/.gitmodules index 88635e645..07d1d4ef7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -98,4 +98,7 @@ [submodule "externals/discord-rpc"] path = externals/discord-rpc url = https://github.com/shadps4-emu/ext-discord-rpc.git - shallow = true \ No newline at end of file + shallow = true +[submodule "externals/LibAtrac9"] + path = externals/LibAtrac9 + url = https://github.com/shadps4-emu/ext-LibAtrac9.git diff --git a/CMakeLists.txt b/CMakeLists.txt index eb085572b..d9479e851 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,12 +179,25 @@ set(AUDIO_CORE src/audio_core/sdl_audio.cpp src/audio_core/sdl_audio.h ) +set(AJM_LIB src/core/libraries/ajm/ajm.cpp + src/core/libraries/ajm/ajm.h + src/core/libraries/ajm/ajm_at9.cpp + src/core/libraries/ajm/ajm_at9.h + src/core/libraries/ajm/ajm_batch.cpp + src/core/libraries/ajm/ajm_batch.h + src/core/libraries/ajm/ajm_context.cpp + src/core/libraries/ajm/ajm_context.h + src/core/libraries/ajm/ajm_error.h + src/core/libraries/ajm/ajm_instance.cpp + src/core/libraries/ajm/ajm_instance.h + src/core/libraries/ajm/ajm_mp3.cpp + src/core/libraries/ajm/ajm_mp3.h +) + set(AUDIO_LIB src/core/libraries/audio/audioin.cpp src/core/libraries/audio/audioin.h src/core/libraries/audio/audioout.cpp src/core/libraries/audio/audioout.h - src/core/libraries/ajm/ajm.cpp - src/core/libraries/ajm/ajm.h src/core/libraries/ngs2/ngs2.cpp src/core/libraries/ngs2/ngs2.h ) @@ -194,8 +207,7 @@ set(GNM_LIB src/core/libraries/gnmdriver/gnmdriver.cpp src/core/libraries/gnmdriver/gnm_error.h ) -set(KERNEL_LIB - src/core/libraries/kernel/event_flag/event_flag.cpp +set(KERNEL_LIB src/core/libraries/kernel/event_flag/event_flag.cpp src/core/libraries/kernel/event_flag/event_flag.h src/core/libraries/kernel/event_flag/event_flag_obj.cpp src/core/libraries/kernel/event_flag/event_flag_obj.h @@ -494,6 +506,7 @@ set(CORE src/core/aerolib/stubs.cpp src/core/libraries/error_codes.h src/core/libraries/libs.h src/core/libraries/libs.cpp + ${AJM_LIB} ${AUDIO_LIB} ${GNM_LIB} ${KERNEL_LIB} @@ -795,7 +808,7 @@ endif() create_target_directory_groups(shadps4) target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui gcn half::half) -target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::SPIRV glslang::glslang SDL3::SDL3 pugixml::pugixml) +target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::SPIRV glslang::glslang SDL3::SDL3 pugixml::pugixml) target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h") target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h") diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 8fefee0f8..17d710878 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -47,6 +47,11 @@ if (NOT TARGET FFmpeg::ffmpeg) add_library(FFmpeg::ffmpeg ALIAS ffmpeg) endif() +# LibAtrac9 +file(GLOB LIBATRAC9_SOURCES LibAtrac9/C/src/*.c) +add_library(LibAtrac9 STATIC ${LIBATRAC9_SOURCES}) +target_include_directories(LibAtrac9 INTERFACE LibAtrac9/C/src) + # Zlib-Ng if (NOT TARGET zlib-ng::zlib) set(ZLIB_ENABLE_TESTS OFF) diff --git a/externals/LibAtrac9 b/externals/LibAtrac9 new file mode 160000 index 000000000..3acdcdc78 --- /dev/null +++ b/externals/LibAtrac9 @@ -0,0 +1 @@ +Subproject commit 3acdcdc78f129c2e6145331ff650fa76dd88d62c diff --git a/src/common/slot_array.h b/src/common/slot_array.h new file mode 100644 index 000000000..3a57899c2 --- /dev/null +++ b/src/common/slot_array.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// #include + +#include +#include +#include +#include + +#include +#include + +namespace Common { + +template ::max(), IndexType MinIndex = 0> +class SlotArray { +public: + SlotArray() { + std::iota(m_free_indices.begin(), m_free_indices.end(), MinIndex); + } + + template + std::optional Create(Types&&... args) { + if (!HasFreeSlots()) { + return std::nullopt; + } + const auto index = m_free_indices[m_curr_cursor]; + m_resources[index - MinIndex] = ResourceType(std::forward(args)...); + m_curr_cursor += 1; + return index; + } + + bool Destroy(IndexType index) { + if (!m_resources[index - MinIndex].has_value()) { + return false; + } + m_curr_cursor -= 1; + m_free_indices[m_curr_cursor] = index; + m_resources[index - MinIndex] = std::nullopt; + return true; + } + + ResourceType* Get(IndexType index) { + auto& resource = m_resources[index - MinIndex]; + if (!resource.has_value()) { + return nullptr; + } + return &resource.value(); + } + + bool HasFreeSlots() { + return m_curr_cursor < m_free_indices.size(); + } + +private: + size_t m_curr_cursor = 0; + std::array m_free_indices; + std::array, MaxIndex - MinIndex> m_resources; +}; + +} // namespace Common diff --git a/src/core/libraries/ajm/ajm.cpp b/src/core/libraries/ajm/ajm.cpp index 441a07f63..2396669b6 100644 --- a/src/core/libraries/ajm/ajm.cpp +++ b/src/core/libraries/ajm/ajm.cpp @@ -1,18 +1,46 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "ajm.h" -#include "ajm_error.h" - #include "common/logging/log.h" +#include "core/libraries/ajm/ajm.h" +#include "core/libraries/ajm/ajm_context.h" +#include "core/libraries/ajm/ajm_error.h" +#include "core/libraries/ajm/ajm_mp3.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" +#include + namespace Libraries::Ajm { -int PS4_SYSV_ABI sceAjmBatchCancel() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +constexpr int ORBIS_AJM_CHANNELMASK_MONO = 0x0004; +constexpr int ORBIS_AJM_CHANNELMASK_STEREO = 0x0003; +constexpr int ORBIS_AJM_CHANNELMASK_QUAD = 0x0033; +constexpr int ORBIS_AJM_CHANNELMASK_5POINT1 = 0x060F; +constexpr int ORBIS_AJM_CHANNELMASK_7POINT1 = 0x063F; + +static std::unique_ptr context{}; + +u32 GetChannelMask(u32 num_channels) { + switch (num_channels) { + case 1: + return ORBIS_AJM_CHANNELMASK_MONO; + case 2: + return ORBIS_AJM_CHANNELMASK_STEREO; + case 4: + return ORBIS_AJM_CHANNELMASK_QUAD; + case 6: + return ORBIS_AJM_CHANNELMASK_5POINT1; + case 8: + return ORBIS_AJM_CHANNELMASK_7POINT1; + default: + UNREACHABLE(); + } +} + +int PS4_SYSV_ABI sceAjmBatchCancel(const u32 context_id, const u32 batch_id) { + LOG_INFO(Lib_Ajm, "called context_id = {} batch_id = {}", context_id, batch_id); + return context->BatchCancel(batch_id); } int PS4_SYSV_ABI sceAjmBatchErrorDump() { @@ -20,34 +48,56 @@ int PS4_SYSV_ABI sceAjmBatchErrorDump() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAjmBatchJobControlBufferRa() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +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) { + return BatchJobControlBufferRa(p_buffer, instance_id, flags, p_sideband_input, + sideband_input_size, p_sideband_output, sideband_output_size, + p_return_address); } -int PS4_SYSV_ABI sceAjmBatchJobInlineBuffer() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +void* PS4_SYSV_ABI sceAjmBatchJobInlineBuffer(void* p_buffer, const void* p_data_input, + size_t data_input_size, + const void** pp_batch_address) { + return BatchJobInlineBuffer(p_buffer, p_data_input, data_input_size, pp_batch_address); } -int PS4_SYSV_ABI sceAjmBatchJobRunBufferRa() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +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) { + return BatchJobRunBufferRa(p_buffer, instance_id, flags, p_data_input, data_input_size, + p_data_output, data_output_size, p_sideband_output, + sideband_output_size, p_return_address); } -int PS4_SYSV_ABI sceAjmBatchJobRunSplitBufferRa() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +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) { + return BatchJobRunSplitBufferRa(p_buffer, instance_id, flags, p_data_input_buffers, + num_data_input_buffers, p_data_output_buffers, + num_data_output_buffers, p_sideband_output, + sideband_output_size, p_return_address); } -int PS4_SYSV_ABI sceAjmBatchStartBuffer() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceAjmBatchStartBuffer(u32 context_id, u8* p_batch, u32 batch_size, + const int priority, AjmBatchError* batch_error, + u32* out_batch_id) { + LOG_TRACE(Lib_Ajm, "called context = {}, batch_size = {:#x}, priority = {}", context_id, + batch_size, priority); + return context->BatchStartBuffer(p_batch, batch_size, priority, batch_error, out_batch_id); } -int PS4_SYSV_ABI sceAjmBatchWait() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceAjmBatchWait(const u32 context_id, const u32 batch_id, const u32 timeout, + AjmBatchError* const batch_error) { + LOG_TRACE(Lib_Ajm, "called context = {}, batch_id = {}, timeout = {}", context_id, batch_id, + timeout); + return context->BatchWait(batch_id, timeout, batch_error); } int PS4_SYSV_ABI sceAjmDecAt9ParseConfigData() { @@ -55,9 +105,9 @@ int PS4_SYSV_ABI sceAjmDecAt9ParseConfigData() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAjmDecMp3ParseFrame() { - 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) { + return AjmMp3Decoder::ParseMp3Header(buf, stream_size, parse_ofl, frame); } int PS4_SYSV_ABI sceAjmFinalize() { @@ -65,8 +115,14 @@ int PS4_SYSV_ABI sceAjmFinalize() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAjmInitialize() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); +int PS4_SYSV_ABI sceAjmInitialize(s64 reserved, u32* p_context_id) { + LOG_INFO(Lib_Ajm, "called reserved = {}", reserved); + ASSERT_MSG(context == nullptr, "Multiple contexts are currently unsupported."); + if (p_context_id == nullptr || reserved != 0) { + return ORBIS_AJM_ERROR_INVALID_PARAMETER; + } + *p_context_id = 1; + context = std::make_unique(); return ORBIS_OK; } @@ -75,14 +131,16 @@ int PS4_SYSV_ABI sceAjmInstanceCodecType() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAjmInstanceCreate() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceAjmInstanceCreate(u32 context_id, AjmCodecType codec_type, + AjmInstanceFlags flags, u32* out_instance) { + LOG_INFO(Lib_Ajm, "called context = {}, codec_type = {}, flags = {:#x}", context_id, + magic_enum::enum_name(codec_type), flags.raw); + return context->InstanceCreate(codec_type, flags, out_instance); } -int PS4_SYSV_ABI sceAjmInstanceDestroy() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceAjmInstanceDestroy(u32 context_id, u32 instance_id) { + LOG_INFO(Lib_Ajm, "called context = {}, instance = {}", context_id, instance_id); + return context->InstanceDestroy(instance_id); } int PS4_SYSV_ABI sceAjmInstanceExtend() { @@ -105,9 +163,12 @@ int PS4_SYSV_ABI sceAjmMemoryUnregister() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAjmModuleRegister() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceAjmModuleRegister(u32 context_id, AjmCodecType codec_type, s64 reserved) { + LOG_INFO(Lib_Ajm, "called context = {}, codec_type = {}", context_id, u32(codec_type)); + if (reserved != 0) { + return ORBIS_AJM_ERROR_INVALID_PARAMETER; + } + return context->ModuleRegister(codec_type); } int PS4_SYSV_ABI sceAjmModuleUnregister() { @@ -145,4 +206,4 @@ void RegisterlibSceAjm(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("AxhcqVv5AYU", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmStrError); }; -} // namespace Libraries::Ajm \ No newline at end of file +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm.h b/src/core/libraries/ajm/ajm.h index 8491e519c..1ac7c7629 100644 --- a/src/core/libraries/ajm/ajm.h +++ b/src/core/libraries/ajm/ajm.h @@ -3,6 +3,8 @@ #pragma once +#include "common/bit_field.h" +#include "common/enum.h" #include "common/types.h" namespace Core::Loader { @@ -11,28 +13,176 @@ class SymbolsResolver; namespace Libraries::Ajm { -int PS4_SYSV_ABI sceAjmBatchCancel(); +constexpr u32 ORBIS_AT9_CONFIG_DATA_SIZE = 4; +constexpr u32 AJM_INSTANCE_STATISTICS = 0x80000; + +enum class AjmCodecType : u32 { + Mp3Dec = 0, + At9Dec = 1, + M4aacDec = 2, + Max = 23, +}; +DECLARE_ENUM_FLAG_OPERATORS(AjmCodecType); + +struct AjmBatchInfo { + void* pBuffer; + u64 offset; + u64 size; +}; + +struct AjmBatchError { + int error_code; + const void* job_addr; + u32 cmd_offset; + const void* job_ra; +}; + +struct AjmBuffer { + u8* p_address; + u64 size; +}; + +enum class AjmJobControlFlags : u64 { + Reset = 1 << 0, + Initialize = 1 << 1, + Resample = 1 << 2, +}; +DECLARE_ENUM_FLAG_OPERATORS(AjmJobControlFlags) + +enum class AjmJobRunFlags : u64 { + GetCodecInfo = 1 << 0, + MultipleFrames = 1 << 1, +}; +DECLARE_ENUM_FLAG_OPERATORS(AjmJobRunFlags) + +enum class AjmJobSidebandFlags : u64 { + GaplessDecode = 1 << 0, + Format = 1 << 1, + Stream = 1 << 2, +}; +DECLARE_ENUM_FLAG_OPERATORS(AjmJobSidebandFlags) + +union AjmJobFlags { + u64 raw; + struct { + u64 version : 3; + u64 codec : 8; + AjmJobRunFlags run_flags : 2; + AjmJobControlFlags control_flags : 3; + u64 reserved : 29; + AjmJobSidebandFlags sideband_flags : 3; + }; +}; + +struct AjmSidebandResult { + s32 result; + s32 internal_result; +}; + +struct AjmSidebandMFrame { + u32 num_frames; + u32 reserved; +}; + +struct AjmSidebandStream { + s32 input_consumed; + s32 output_written; + u64 total_decoded_samples; +}; + +enum class AjmFormatEncoding : u32 { + S16 = 0, + S32 = 1, + Float = 2, +}; + +struct AjmSidebandFormat { + u32 num_channels; + u32 channel_mask; + u32 sampl_freq; + AjmFormatEncoding sample_encoding; + u32 bitrate; + u32 reserved; +}; + +struct AjmSidebandGaplessDecode { + u32 total_samples; + u16 skip_samples; + u16 skipped_samples; +}; + +struct AjmSidebandResampleParameters { + float ratio; + uint32_t flags; +}; + +struct AjmDecAt9InitializeParameters { + u8 config_data[ORBIS_AT9_CONFIG_DATA_SIZE]; + u32 reserved; +}; + +union AjmSidebandInitParameters { + AjmDecAt9InitializeParameters at9; + u8 reserved[8]; +}; + +union AjmInstanceFlags { + u64 raw; + struct { + u64 version : 3; + u64 channels : 4; + u64 format : 3; + u64 gapless_loop : 1; + u64 : 21; + u64 codec : 28; + }; +}; +static_assert(sizeof(AjmInstanceFlags) == 8); + +struct AjmDecMp3ParseFrame; + +u32 GetChannelMask(u32 num_channels); + +int PS4_SYSV_ABI sceAjmBatchCancel(const u32 context_id, const u32 batch_id); int PS4_SYSV_ABI sceAjmBatchErrorDump(); -int PS4_SYSV_ABI sceAjmBatchJobControlBufferRa(); -int PS4_SYSV_ABI sceAjmBatchJobInlineBuffer(); -int PS4_SYSV_ABI sceAjmBatchJobRunBufferRa(); -int PS4_SYSV_ABI sceAjmBatchJobRunSplitBufferRa(); -int PS4_SYSV_ABI sceAjmBatchStartBuffer(); -int PS4_SYSV_ABI sceAjmBatchWait(); +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); +void* PS4_SYSV_ABI sceAjmBatchJobInlineBuffer(void* p_buffer, const void* p_data_input, + size_t data_input_size, + const void** pp_batch_address); +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); +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); +int PS4_SYSV_ABI sceAjmBatchStartBuffer(u32 context, u8* batch, u32 batch_size, const int priority, + AjmBatchError* batch_error, u32* out_batch_id); +int PS4_SYSV_ABI sceAjmBatchWait(const u32 context, const u32 batch_id, const u32 timeout, + AjmBatchError* const batch_error); int PS4_SYSV_ABI sceAjmDecAt9ParseConfigData(); -int PS4_SYSV_ABI sceAjmDecMp3ParseFrame(); +int PS4_SYSV_ABI sceAjmDecMp3ParseFrame(const u8* stream, u32 stream_size, int parse_ofl, + AjmDecMp3ParseFrame* frame); int PS4_SYSV_ABI sceAjmFinalize(); -int PS4_SYSV_ABI sceAjmInitialize(); +int PS4_SYSV_ABI sceAjmInitialize(s64 reserved, u32* out_context); int PS4_SYSV_ABI sceAjmInstanceCodecType(); -int PS4_SYSV_ABI sceAjmInstanceCreate(); -int PS4_SYSV_ABI sceAjmInstanceDestroy(); +int PS4_SYSV_ABI sceAjmInstanceCreate(u32 context, AjmCodecType codec_type, AjmInstanceFlags flags, + u32* instance); +int PS4_SYSV_ABI sceAjmInstanceDestroy(u32 context, u32 instance); int PS4_SYSV_ABI sceAjmInstanceExtend(); int PS4_SYSV_ABI sceAjmInstanceSwitch(); int PS4_SYSV_ABI sceAjmMemoryRegister(); int PS4_SYSV_ABI sceAjmMemoryUnregister(); -int PS4_SYSV_ABI sceAjmModuleRegister(); +int PS4_SYSV_ABI sceAjmModuleRegister(u32 context, AjmCodecType codec_type, s64 reserved); int PS4_SYSV_ABI sceAjmModuleUnregister(); int PS4_SYSV_ABI sceAjmStrError(); void RegisterlibSceAjm(Core::Loader::SymbolsResolver* sym); -} // namespace Libraries::Ajm \ No newline at end of file +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_at9.cpp b/src/core/libraries/ajm/ajm_at9.cpp new file mode 100644 index 000000000..7fff0ff0c --- /dev/null +++ b/src/core/libraries/ajm/ajm_at9.cpp @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/assert.h" +#include "core/libraries/ajm/ajm_at9.h" +#include "error_codes.h" + +extern "C" { +#include +#include +} + +#include + +namespace Libraries::Ajm { + +AjmAt9Decoder::AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags) + : m_format(format), m_flags(flags), m_handle(Atrac9GetHandle()) { + ASSERT_MSG(m_handle, "Atrac9GetHandle failed"); + AjmAt9Decoder::Reset(); +} + +AjmAt9Decoder::~AjmAt9Decoder() { + Atrac9ReleaseHandle(m_handle); +} + +void AjmAt9Decoder::Reset() { + Atrac9ReleaseHandle(m_handle); + m_handle = Atrac9GetHandle(); + Atrac9InitDecoder(m_handle, m_config_data); + Atrac9GetCodecInfo(m_handle, &m_codec_info); + + m_num_frames = 0; + m_superframe_bytes_remain = m_codec_info.superframeSize; +} + +void AjmAt9Decoder::Initialize(const void* buffer, u32 buffer_size) { + ASSERT_MSG(buffer_size == sizeof(AjmDecAt9InitializeParameters), + "Incorrect At9 initialization buffer size {}", buffer_size); + const auto params = reinterpret_cast(buffer); + std::memcpy(m_config_data, params->config_data, ORBIS_AT9_CONFIG_DATA_SIZE); + AjmAt9Decoder::Reset(); + m_pcm_buffer.resize(m_codec_info.frameSamples * m_codec_info.channels * GetPointCodeSize(), 0); +} + +u8 AjmAt9Decoder::GetPointCodeSize() { + switch (m_format) { + case AjmFormatEncoding::S16: + return sizeof(s16); + case AjmFormatEncoding::S32: + return sizeof(s32); + case AjmFormatEncoding::Float: + return sizeof(float); + default: + UNREACHABLE(); + } +} + +void AjmAt9Decoder::GetInfo(void* out_info) { + auto* info = reinterpret_cast(out_info); + info->super_frame_size = m_codec_info.superframeSize; + info->frames_in_super_frame = m_codec_info.framesInSuperframe; + info->frame_samples = m_codec_info.frameSamples; + info->next_frame_size = static_cast(m_handle)->Config.FrameBytes; +} + +std::tuple AjmAt9Decoder::ProcessData(std::span& in_buf, SparseOutputBuffer& output, + AjmSidebandGaplessDecode& gapless, + std::optional max_samples_per_channel) { + int ret = 0; + int bytes_used = 0; + switch (m_format) { + case AjmFormatEncoding::S16: + ret = Atrac9Decode(m_handle, in_buf.data(), reinterpret_cast(m_pcm_buffer.data()), + &bytes_used, True(m_flags & AjmAt9CodecFlags::NonInterleavedOutput)); + break; + case AjmFormatEncoding::S32: + ret = Atrac9DecodeS32(m_handle, in_buf.data(), reinterpret_cast(m_pcm_buffer.data()), + &bytes_used, True(m_flags & AjmAt9CodecFlags::NonInterleavedOutput)); + break; + case AjmFormatEncoding::Float: + ret = + Atrac9DecodeF32(m_handle, in_buf.data(), reinterpret_cast(m_pcm_buffer.data()), + &bytes_used, True(m_flags & AjmAt9CodecFlags::NonInterleavedOutput)); + break; + default: + UNREACHABLE(); + } + ASSERT_MSG(ret == At9Status::ERR_SUCCESS, "Atrac9Decode failed ret = {:#x}", ret); + in_buf = in_buf.subspan(bytes_used); + + m_superframe_bytes_remain -= bytes_used; + + u32 skipped_samples = 0; + if (gapless.skipped_samples < gapless.skip_samples) { + skipped_samples = std::min(u32(m_codec_info.frameSamples), + u32(gapless.skip_samples - gapless.skipped_samples)); + gapless.skipped_samples += skipped_samples; + } + + const auto max_samples = max_samples_per_channel.has_value() + ? max_samples_per_channel.value() * m_codec_info.channels + : std::numeric_limits::max(); + + size_t samples_written = 0; + switch (m_format) { + case AjmFormatEncoding::S16: + samples_written = WriteOutputSamples(output, skipped_samples, max_samples); + break; + case AjmFormatEncoding::S32: + samples_written = WriteOutputSamples(output, skipped_samples, max_samples); + break; + case AjmFormatEncoding::Float: + samples_written = WriteOutputSamples(output, skipped_samples, max_samples); + break; + default: + UNREACHABLE(); + } + + m_num_frames += 1; + if ((m_num_frames % m_codec_info.framesInSuperframe) == 0) { + if (m_superframe_bytes_remain) { + in_buf = in_buf.subspan(m_superframe_bytes_remain); + } + m_superframe_bytes_remain = m_codec_info.superframeSize; + m_num_frames = 0; + } + + return {1, samples_written / m_codec_info.channels}; +} + +AjmSidebandFormat AjmAt9Decoder::GetFormat() { + return AjmSidebandFormat{ + .num_channels = u32(m_codec_info.channels), + .channel_mask = GetChannelMask(u32(m_codec_info.channels)), + .sampl_freq = u32(m_codec_info.samplingRate), + .sample_encoding = m_format, + .bitrate = u32(m_codec_info.samplingRate * GetPointCodeSize() * 8), + .reserved = 0, + }; +} + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_at9.h b/src/core/libraries/ajm/ajm_at9.h new file mode 100644 index 000000000..e5f65db93 --- /dev/null +++ b/src/core/libraries/ajm/ajm_at9.h @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" +#include "core/libraries/ajm/ajm_instance.h" + +#include "libatrac9.h" + +#include + +namespace Libraries::Ajm { + +constexpr s32 ORBIS_AJM_DEC_AT9_MAX_CHANNELS = 8; + +enum AjmAt9CodecFlags : u32 { + ParseRiffHeader = 1 << 0, + NonInterleavedOutput = 1 << 8, +}; +DECLARE_ENUM_FLAG_OPERATORS(AjmAt9CodecFlags) + +struct AjmSidebandDecAt9CodecInfo { + u32 super_frame_size; + u32 frames_in_super_frame; + u32 next_frame_size; + u32 frame_samples; +}; + +struct AjmAt9Decoder final : AjmCodec { + explicit AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags); + ~AjmAt9Decoder() override; + + void Reset() override; + void Initialize(const void* buffer, u32 buffer_size) override; + void GetInfo(void* out_info) override; + AjmSidebandFormat GetFormat() override; + std::tuple ProcessData(std::span& input, SparseOutputBuffer& output, + AjmSidebandGaplessDecode& gapless, + std::optional max_samples) override; + +private: + u8 GetPointCodeSize(); + + template + size_t WriteOutputSamples(SparseOutputBuffer& output, u32 skipped_samples, u32 max_samples) { + std::span pcm_data{reinterpret_cast(m_pcm_buffer.data()), + m_pcm_buffer.size() / sizeof(T)}; + pcm_data = pcm_data.subspan(skipped_samples * m_codec_info.channels); + const auto pcm_size = std::min(u32(pcm_data.size()), max_samples); + return output.Write(pcm_data.subspan(0, pcm_size)); + } + + const AjmFormatEncoding m_format; + const AjmAt9CodecFlags m_flags; + void* m_handle{}; + u8 m_config_data[ORBIS_AT9_CONFIG_DATA_SIZE]{}; + u32 m_superframe_bytes_remain{}; + u32 m_num_frames{}; + Atrac9CodecInfo m_codec_info{}; + std::vector m_pcm_buffer; +}; + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_batch.cpp b/src/core/libraries/ajm/ajm_batch.cpp new file mode 100644 index 000000000..5c76beae8 --- /dev/null +++ b/src/core/libraries/ajm/ajm_batch.cpp @@ -0,0 +1,461 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/alignment.h" +#include "core/libraries/ajm/ajm_batch.h" + +namespace Libraries::Ajm { + +enum Identifier : u8 { + AjmIdentJob = 0, + AjmIdentInputRunBuf = 1, + AjmIdentInputControlBuf = 2, + AjmIdentControlFlags = 3, + AjmIdentRunFlags = 4, + AjmIdentReturnAddressBuf = 6, + AjmIdentInlineBuf = 7, + AjmIdentOutputRunBuf = 17, + AjmIdentOutputControlBuf = 18, +}; + +struct AjmChunkHeader { + u32 ident : 6; + u32 payload : 20; + u32 reserved : 6; +}; +static_assert(sizeof(AjmChunkHeader) == 4); + +struct AjmChunkJob { + AjmChunkHeader header; + u32 size; +}; +static_assert(sizeof(AjmChunkJob) == 8); + +struct AjmChunkFlags { + AjmChunkHeader header; + u32 flags_low; +}; +static_assert(sizeof(AjmChunkFlags) == 8); + +struct AjmChunkBuffer { + AjmChunkHeader header; + u32 size; + void* p_address; +}; +static_assert(sizeof(AjmChunkBuffer) == 16); + +class AjmBatchBuffer { +public: + static constexpr size_t s_dynamic_extent = std::numeric_limits::max(); + + AjmBatchBuffer(u8* begin, u8* end) + : m_p_begin(begin), m_p_current(begin), m_size(end - begin) {} + AjmBatchBuffer(u8* begin, size_t size = s_dynamic_extent) + : m_p_begin(begin), m_p_current(m_p_begin), m_size(size) {} + AjmBatchBuffer(std::span data) + : m_p_begin(data.data()), m_p_current(m_p_begin), m_size(data.size()) {} + + AjmBatchBuffer SubBuffer(size_t size = s_dynamic_extent) { + auto current = m_p_current; + if (size != s_dynamic_extent) { + m_p_current += size; + } + return AjmBatchBuffer(current, size); + } + + template + T& Peek() const { + DEBUG_ASSERT(m_size == s_dynamic_extent || + (m_p_current + sizeof(T)) <= (m_p_begin + m_size)); + return *reinterpret_cast(m_p_current); + } + + template + T& Consume() { + auto* const result = reinterpret_cast(m_p_current); + m_p_current += sizeof(T); + DEBUG_ASSERT(m_size == s_dynamic_extent || m_p_current <= (m_p_begin + m_size)); + return *result; + } + + template + void Skip() { + Advance(sizeof(T)); + } + + void Advance(size_t size) { + m_p_current += size; + DEBUG_ASSERT(m_size == s_dynamic_extent || m_p_current <= (m_p_begin + m_size)); + } + + bool IsEmpty() { + return m_size != s_dynamic_extent && m_p_current >= (m_p_begin + m_size); + } + + size_t BytesConsumed() const { + return m_p_current - m_p_begin; + } + + size_t BytesRemaining() const { + if (m_size == s_dynamic_extent) { + return s_dynamic_extent; + } + return m_size - (m_p_current - m_p_begin); + } + + u8* GetCurrent() const { + return m_p_current; + } + +private: + u8* m_p_begin{}; + u8* m_p_current{}; + size_t m_size{}; +}; + +AjmJob AjmJobFromBatchBuffer(u32 instance_id, AjmBatchBuffer batch_buffer) { + std::optional job_flags = {}; + std::optional input_control_buffer = {}; + std::optional output_control_buffer = {}; + std::optional inline_buffer = {}; + + AjmJob job; + job.instance_id = instance_id; + + // Read parameters of a job + while (!batch_buffer.IsEmpty()) { + auto& header = batch_buffer.Peek(); + switch (header.ident) { + case Identifier::AjmIdentInputRunBuf: { + auto& buffer = batch_buffer.Consume(); + u8* p_begin = reinterpret_cast(buffer.p_address); + job.input.buffer.insert(job.input.buffer.end(), p_begin, p_begin + buffer.size); + 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 = batch_buffer.Consume(); + break; + } + case Identifier::AjmIdentControlFlags: + case Identifier::AjmIdentRunFlags: { + ASSERT_MSG(!job_flags.has_value(), "Only one instance of job flags is allowed per job"); + auto& chunk = batch_buffer.Consume(); + job_flags = AjmJobFlags{ + .raw = (u64(chunk.header.payload) << 32) + chunk.flags_low, + }; + break; + } + case Identifier::AjmIdentReturnAddressBuf: { + // Ignore return address buffers. + batch_buffer.Skip(); + break; + } + case Identifier::AjmIdentInlineBuf: { + ASSERT_MSG(!output_control_buffer.has_value(), + "Only one instance of inline buffer is allowed per job"); + inline_buffer = batch_buffer.Consume(); + break; + } + case Identifier::AjmIdentOutputRunBuf: { + auto& buffer = batch_buffer.Consume(); + u8* p_begin = reinterpret_cast(buffer.p_address); + job.output.buffers.emplace_back(std::span(p_begin, p_begin + buffer.size)); + 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 = batch_buffer.Consume(); + break; + } + default: + UNREACHABLE_MSG("Unknown chunk: {}", header.ident); + } + } + + job.flags = job_flags.value(); + + // Initialize sideband input parameters + if (input_control_buffer.has_value()) { + AjmBatchBuffer input_batch(reinterpret_cast(input_control_buffer->p_address), + input_control_buffer->size); + + const auto sideband_flags = job_flags->sideband_flags; + if (True(sideband_flags & AjmJobSidebandFlags::Format) && !input_batch.IsEmpty()) { + job.input.format = input_batch.Consume(); + } + if (True(sideband_flags & AjmJobSidebandFlags::GaplessDecode) && !input_batch.IsEmpty()) { + job.input.gapless_decode = input_batch.Consume(); + } + + const auto control_flags = job_flags.value().control_flags; + if (True(control_flags & AjmJobControlFlags::Initialize)) { + job.input.init_params = AjmDecAt9InitializeParameters{}; + std::memcpy(&job.input.init_params.value(), input_batch.GetCurrent(), + input_batch.BytesRemaining()); + } + } + + if (inline_buffer.has_value()) { + AjmBatchBuffer inline_batch(reinterpret_cast(inline_buffer->p_address), + inline_buffer->size); + + const auto control_flags = job_flags.value().control_flags; + if (True(control_flags & AjmJobControlFlags::Resample)) { + job.input.resample_parameters = inline_batch.Consume(); + } + } + + // Initialize sideband output parameters + if (output_control_buffer.has_value()) { + AjmBatchBuffer output_batch(reinterpret_cast(output_control_buffer->p_address), + output_control_buffer->size); + + job.output.p_result = &output_batch.Consume(); + *job.output.p_result = AjmSidebandResult{}; + + const auto sideband_flags = job_flags->sideband_flags; + if (True(sideband_flags & AjmJobSidebandFlags::Stream) && !output_batch.IsEmpty()) { + job.output.p_stream = &output_batch.Consume(); + *job.output.p_stream = AjmSidebandStream{}; + } + if (True(sideband_flags & AjmJobSidebandFlags::Format) && !output_batch.IsEmpty()) { + job.output.p_format = &output_batch.Consume(); + *job.output.p_format = AjmSidebandFormat{}; + } + if (True(sideband_flags & AjmJobSidebandFlags::GaplessDecode) && !output_batch.IsEmpty()) { + job.output.p_gapless_decode = &output_batch.Consume(); + *job.output.p_gapless_decode = AjmSidebandGaplessDecode{}; + } + + const auto run_flags = job_flags->run_flags; + if (True(run_flags & AjmJobRunFlags::MultipleFrames) && !output_batch.IsEmpty()) { + job.output.p_mframe = &output_batch.Consume(); + *job.output.p_mframe = AjmSidebandMFrame{}; + } + if (True(run_flags & AjmJobRunFlags::GetCodecInfo) && !output_batch.IsEmpty()) { + job.output.p_codec_info = output_batch.GetCurrent(); + } + } + + return job; +} + +std::shared_ptr AjmBatch::FromBatchBuffer(std::span data) { + auto batch = std::make_shared(); + + AjmBatchBuffer buffer(data); + while (!buffer.IsEmpty()) { + auto& job_chunk = buffer.Consume(); + ASSERT(job_chunk.header.ident == AjmIdentJob); + auto instance_id = job_chunk.header.payload; + batch->jobs.push_back(AjmJobFromBatchBuffer(instance_id, buffer.SubBuffer(job_chunk.size))); + } + + return batch; +} + +void* BatchJobControlBufferRa(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"); + + AjmBatchBuffer buffer(reinterpret_cast(p_buffer)); + + auto& job_chunk = buffer.Consume(); + job_chunk.header.ident = AjmIdentJob; + job_chunk.header.payload = instance_id; + + auto job_buffer = buffer.SubBuffer(); + + if (p_return_address != nullptr) { + auto& chunk_ra = job_buffer.Consume(); + chunk_ra.header.ident = AjmIdentReturnAddressBuf; + chunk_ra.header.payload = 0; + chunk_ra.size = 0; + chunk_ra.p_address = p_return_address; + } + + { + auto& chunk_input = job_buffer.Consume(); + chunk_input.header.ident = AjmIdentInputControlBuf; + chunk_input.header.payload = 0; + chunk_input.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 = job_buffer.Consume(); + chunk_flags.header.ident = AjmIdentControlFlags; + chunk_flags.header.payload = u32(flags >> 32); + chunk_flags.flags_low = u32(flags); + } + + { + auto& chunk_output = job_buffer.Consume(); + chunk_output.header.ident = AjmIdentOutputControlBuf; + chunk_output.header.payload = 0; + chunk_output.size = sideband_output_size; + chunk_output.p_address = p_sideband_output; + } + + job_chunk.size = job_buffer.BytesConsumed(); + return job_buffer.GetCurrent(); +} + +void* BatchJobInlineBuffer(void* p_buffer, const void* p_data_input, size_t data_input_size, + const void** pp_batch_address) { + LOG_TRACE(Lib_Ajm, "called"); + + AjmBatchBuffer buffer(reinterpret_cast(p_buffer)); + + auto& job_chunk = buffer.Consume(); + job_chunk.header.ident = AjmIdentInlineBuf; + job_chunk.header.payload = 0; + job_chunk.size = Common::AlignUp(data_input_size, 8); + *pp_batch_address = buffer.GetCurrent(); + + memcpy(buffer.GetCurrent(), p_data_input, data_input_size); + return buffer.GetCurrent() + job_chunk.size; +} + +void* BatchJobRunBufferRa(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"); + + AjmBatchBuffer buffer(reinterpret_cast(p_buffer)); + + auto& job_chunk = buffer.Consume(); + job_chunk.header.ident = AjmIdentJob; + job_chunk.header.payload = instance_id; + + auto job_buffer = buffer.SubBuffer(); + + if (p_return_address != nullptr) { + auto& chunk_ra = job_buffer.Consume(); + chunk_ra.header.ident = AjmIdentReturnAddressBuf; + chunk_ra.header.payload = 0; + chunk_ra.size = 0; + chunk_ra.p_address = p_return_address; + } + + { + auto& chunk_input = job_buffer.Consume(); + chunk_input.header.ident = AjmIdentInputRunBuf; + chunk_input.header.payload = 0; + chunk_input.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 = job_buffer.Consume(); + chunk_flags.header.ident = AjmIdentRunFlags; + chunk_flags.header.payload = u32(flags >> 32); + chunk_flags.flags_low = u32(flags); + } + + { + auto& chunk_output = job_buffer.Consume(); + chunk_output.header.ident = AjmIdentOutputRunBuf; + chunk_output.header.payload = 0; + chunk_output.size = data_output_size; + chunk_output.p_address = p_data_output; + } + + { + auto& chunk_output = job_buffer.Consume(); + chunk_output.header.ident = AjmIdentOutputControlBuf; + chunk_output.header.payload = 0; + chunk_output.size = sideband_output_size; + chunk_output.p_address = p_sideband_output; + } + + job_chunk.size = job_buffer.BytesConsumed(); + return job_buffer.GetCurrent(); +} + +void* BatchJobRunSplitBufferRa(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"); + + AjmBatchBuffer buffer(reinterpret_cast(p_buffer)); + + auto& job_chunk = buffer.Consume(); + job_chunk.header.ident = AjmIdentJob; + job_chunk.header.payload = instance_id; + + auto job_buffer = buffer.SubBuffer(); + + if (p_return_address != nullptr) { + auto& chunk_ra = job_buffer.Consume(); + chunk_ra.header.ident = AjmIdentReturnAddressBuf; + chunk_ra.header.payload = 0; + chunk_ra.size = 0; + chunk_ra.p_address = p_return_address; + } + + for (s32 i = 0; i < num_data_input_buffers; i++) { + auto& chunk_input = job_buffer.Consume(); + chunk_input.header.ident = AjmIdentInputRunBuf; + chunk_input.header.payload = 0; + chunk_input.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 = job_buffer.Consume(); + chunk_flags.header.ident = AjmIdentRunFlags; + chunk_flags.header.payload = u32(flags >> 32); + chunk_flags.flags_low = u32(flags); + } + + for (s32 i = 0; i < num_data_output_buffers; i++) { + auto& chunk_output = job_buffer.Consume(); + chunk_output.header.ident = AjmIdentOutputRunBuf; + chunk_output.header.payload = 0; + chunk_output.size = p_data_output_buffers[i].size; + chunk_output.p_address = p_data_output_buffers[i].p_address; + } + + { + auto& chunk_output = job_buffer.Consume(); + chunk_output.header.ident = AjmIdentOutputControlBuf; + chunk_output.header.payload = 0; + chunk_output.size = sideband_output_size; + chunk_output.p_address = p_sideband_output; + } + + job_chunk.size = job_buffer.BytesConsumed(); + return job_buffer.GetCurrent(); +} + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_batch.h b/src/core/libraries/ajm/ajm_batch.h new file mode 100644 index 000000000..65110ee73 --- /dev/null +++ b/src/core/libraries/ajm/ajm_batch.h @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/assert.h" +#include "common/types.h" +#include "core/libraries/ajm/ajm.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace Libraries::Ajm { + +struct AjmJob { + struct Input { + std::optional init_params; + std::optional resample_parameters; + std::optional format; + std::optional gapless_decode; + std::vector buffer; + }; + + struct Output { + boost::container::small_vector, 8> buffers; + AjmSidebandResult* p_result = nullptr; + AjmSidebandStream* p_stream = nullptr; + AjmSidebandFormat* p_format = nullptr; + AjmSidebandGaplessDecode* p_gapless_decode = nullptr; + AjmSidebandMFrame* p_mframe = nullptr; + u8* p_codec_info = nullptr; + }; + + u32 instance_id{}; + AjmJobFlags flags{}; + Input input; + Output output; +}; + +struct AjmBatch { + u32 id{}; + std::atomic_bool waiting{}; + std::atomic_bool canceled{}; + std::binary_semaphore finished{0}; + boost::container::small_vector jobs; + + static std::shared_ptr FromBatchBuffer(std::span buffer); +}; + +void* BatchJobControlBufferRa(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); + +void* BatchJobInlineBuffer(void* p_buffer, const void* p_data_input, size_t data_input_size, + const void** pp_batch_address); + +void* BatchJobRunBufferRa(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); + +void* BatchJobRunSplitBufferRa(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); + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_context.cpp b/src/core/libraries/ajm/ajm_context.cpp new file mode 100644 index 000000000..e30e1c478 --- /dev/null +++ b/src/core/libraries/ajm/ajm_context.cpp @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#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_context.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 +#include + +namespace Libraries::Ajm { + +static constexpr u32 ORBIS_AJM_WAIT_INFINITE = -1; + +AjmContext::AjmContext() { + worker_thread = std::jthread([this](std::stop_token stop) { this->WorkerThread(stop); }); +} + +bool AjmContext::IsRegistered(AjmCodecType type) const { + return registered_codecs[std::to_underlying(type)]; +} + +s32 AjmContext::BatchCancel(const u32 batch_id) { + std::shared_ptr batch{}; + { + std::shared_lock guard(batches_mutex); + const auto p_batch = batches.Get(batch_id); + if (p_batch == nullptr) { + return ORBIS_AJM_ERROR_INVALID_BATCH; + } + batch = *p_batch; + } + + batch->canceled = true; + return ORBIS_OK; +} + +s32 AjmContext::ModuleRegister(AjmCodecType type) { + if (std::to_underlying(type) >= NumAjmCodecs) { + return ORBIS_AJM_ERROR_INVALID_PARAMETER; + } + if (IsRegistered(type)) { + return ORBIS_AJM_ERROR_CODEC_ALREADY_REGISTERED; + } + registered_codecs[std::to_underlying(type)] = true; + return ORBIS_OK; +} + +void AjmContext::WorkerThread(std::stop_token stop) { + while (!stop.stop_requested()) { + auto batch = batch_queue.PopWait(stop); + if (batch != nullptr) { + ProcessBatch(batch->id, batch->jobs); + batch->finished.release(); + } + } +} + +void AjmContext::ProcessBatch(u32 id, std::span jobs) { + // Perform operation requested by control flags. + for (auto& job : jobs) { + LOG_TRACE(Lib_Ajm, "Processing job {} for instance {}. flags = {:#x}", id, job.instance_id, + job.flags.raw); + + std::shared_ptr instance; + { + std::shared_lock lock(instances_mutex); + auto* p_instance = instances.Get(job.instance_id); + ASSERT_MSG(p_instance != nullptr, "Attempting to execute job on null instance"); + instance = *p_instance; + } + + instance->ExecuteJob(job); + } +} + +s32 AjmContext::BatchWait(const u32 batch_id, const u32 timeout, AjmBatchError* const batch_error) { + std::shared_ptr batch{}; + { + std::shared_lock guard(batches_mutex); + const auto p_batch = batches.Get(batch_id); + if (p_batch == nullptr) { + return ORBIS_AJM_ERROR_INVALID_BATCH; + } + batch = *p_batch; + } + + bool expected = false; + if (!batch->waiting.compare_exchange_strong(expected, true)) { + return ORBIS_AJM_ERROR_BUSY; + } + + if (timeout == ORBIS_AJM_WAIT_INFINITE) { + batch->finished.acquire(); + } else if (!batch->finished.try_acquire_for(std::chrono::milliseconds(timeout))) { + batch->waiting = false; + return ORBIS_AJM_ERROR_IN_PROGRESS; + } + + { + std::unique_lock guard(batches_mutex); + batches.Destroy(batch_id); + } + + if (batch->canceled) { + return ORBIS_AJM_ERROR_CANCELLED; + } + + return ORBIS_OK; +} + +int AjmContext::BatchStartBuffer(u8* p_batch, u32 batch_size, const int priority, + AjmBatchError* batch_error, u32* out_batch_id) { + if ((batch_size & 7) != 0) { + LOG_ERROR(Lib_Ajm, "ORBIS_AJM_ERROR_MALFORMED_BATCH"); + return ORBIS_AJM_ERROR_MALFORMED_BATCH; + } + + const auto batch_info = AjmBatch::FromBatchBuffer({p_batch, batch_size}); + std::optional batch_id; + { + std::unique_lock guard(batches_mutex); + batch_id = batches.Create(batch_info); + } + if (!batch_id.has_value()) { + return ORBIS_AJM_ERROR_OUT_OF_MEMORY; + } + *out_batch_id = batch_id.value(); + batch_info->id = *out_batch_id; + + batch_queue.EmplaceWait(batch_info); + + return ORBIS_OK; +} + +s32 AjmContext::InstanceCreate(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 (!IsRegistered(codec_type)) { + return ORBIS_AJM_ERROR_CODEC_NOT_REGISTERED; + } + std::optional opt_index; + { + std::unique_lock lock(instances_mutex); + opt_index = instances.Create(std::move(std::make_unique(codec_type, flags))); + } + if (!opt_index.has_value()) { + return ORBIS_AJM_ERROR_OUT_OF_RESOURCES; + } + *out_instance = opt_index.value(); + + LOG_INFO(Lib_Ajm, "instance = {}", *out_instance); + return ORBIS_OK; +} + +s32 AjmContext::InstanceDestroy(u32 instance) { + std::unique_lock lock(instances_mutex); + if (!instances.Destroy(instance)) { + return ORBIS_AJM_ERROR_INVALID_INSTANCE; + } + return ORBIS_OK; +} + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_context.h b/src/core/libraries/ajm/ajm_context.h new file mode 100644 index 000000000..e51ea4fcf --- /dev/null +++ b/src/core/libraries/ajm/ajm_context.h @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/bounded_threadsafe_queue.h" +#include "common/slot_array.h" +#include "common/types.h" +#include "core/libraries/ajm/ajm.h" +#include "core/libraries/ajm/ajm_batch.h" +#include "core/libraries/ajm/ajm_instance.h" + +#include +#include +#include +#include +#include +#include + +namespace Libraries::Ajm { + +class AjmContext { +public: + AjmContext(); + + s32 InstanceCreate(AjmCodecType codec_type, AjmInstanceFlags flags, u32* out_instance_id); + s32 InstanceDestroy(u32 instance_id); + + s32 BatchCancel(const u32 batch_id); + s32 ModuleRegister(AjmCodecType type); + s32 BatchWait(const u32 batch_id, const u32 timeout, AjmBatchError* const p_batch_error); + s32 BatchStartBuffer(u8* p_batch, u32 batch_size, const int priority, + AjmBatchError* p_batch_error, u32* p_batch_id); + + void WorkerThread(std::stop_token stop); + void ProcessBatch(u32 id, std::span jobs); + +private: + static constexpr u32 MaxInstances = 0x2fff; + static constexpr u32 MaxBatches = 0x0400; + static constexpr u32 NumAjmCodecs = std::to_underlying(AjmCodecType::Max); + + [[nodiscard]] bool IsRegistered(AjmCodecType type) const; + + std::array registered_codecs{}; + + std::shared_mutex instances_mutex; + Common::SlotArray, MaxInstances, 1> instances; + + std::shared_mutex batches_mutex; + Common::SlotArray, MaxBatches, 1> batches; + + std::jthread worker_thread{}; + Common::MPSCQueue> batch_queue; +}; + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_instance.cpp b/src/core/libraries/ajm/ajm_instance.cpp new file mode 100644 index 000000000..85a6f54a1 --- /dev/null +++ b/src/core/libraries/ajm/ajm_instance.cpp @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/libraries/ajm/ajm_at9.h" +#include "core/libraries/ajm/ajm_instance.h" +#include "core/libraries/ajm/ajm_mp3.h" + +#include + +namespace Libraries::Ajm { + +constexpr int ORBIS_AJM_RESULT_NOT_INITIALIZED = 0x00000001; +constexpr int ORBIS_AJM_RESULT_INVALID_DATA = 0x00000002; +constexpr int ORBIS_AJM_RESULT_INVALID_PARAMETER = 0x00000004; +constexpr int ORBIS_AJM_RESULT_PARTIAL_INPUT = 0x00000008; +constexpr int ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM = 0x00000010; +constexpr int ORBIS_AJM_RESULT_STREAM_CHANGE = 0x00000020; +constexpr int ORBIS_AJM_RESULT_TOO_MANY_CHANNELS = 0x00000040; +constexpr int ORBIS_AJM_RESULT_UNSUPPORTED_FLAG = 0x00000080; +constexpr int ORBIS_AJM_RESULT_SIDEBAND_TRUNCATED = 0x00000100; +constexpr int ORBIS_AJM_RESULT_PRIORITY_PASSED = 0x00000200; +constexpr int ORBIS_AJM_RESULT_CODEC_ERROR = 0x40000000; +constexpr int ORBIS_AJM_RESULT_FATAL = 0x80000000; + +AjmInstance::AjmInstance(AjmCodecType codec_type, AjmInstanceFlags flags) : m_flags(flags) { + switch (codec_type) { + case AjmCodecType::At9Dec: { + m_codec = std::make_unique(AjmFormatEncoding(flags.format), + AjmAt9CodecFlags(flags.codec)); + break; + } + case AjmCodecType::Mp3Dec: { + m_codec = std::make_unique(AjmFormatEncoding(flags.format)); + break; + } + default: + UNREACHABLE_MSG("Unimplemented codec type {}", magic_enum::enum_name(codec_type)); + } +} + +void AjmInstance::ExecuteJob(AjmJob& job) { + const auto control_flags = job.flags.control_flags; + if (True(control_flags & AjmJobControlFlags::Reset)) { + LOG_TRACE(Lib_Ajm, "Resetting instance {}", job.instance_id); + m_format = {}; + m_gapless = {}; + m_resample_parameters = {}; + m_gapless_samples = 0; + m_total_samples = 0; + m_codec->Reset(); + } + if (job.input.init_params.has_value()) { + LOG_TRACE(Lib_Ajm, "Initializing instance {}", job.instance_id); + auto& params = job.input.init_params.value(); + m_codec->Initialize(¶ms, sizeof(params)); + } + if (job.input.resample_parameters.has_value()) { + UNREACHABLE_MSG("Unimplemented: resample parameters"); + m_resample_parameters = job.input.resample_parameters.value(); + } + if (job.input.format.has_value()) { + UNREACHABLE_MSG("Unimplemented: format parameters"); + m_format = job.input.format.value(); + } + if (job.input.gapless_decode.has_value()) { + auto& params = job.input.gapless_decode.value(); + m_gapless.total_samples = params.total_samples; + m_gapless.skip_samples = params.skip_samples; + } + + if (!job.input.buffer.empty() && !job.output.buffers.empty()) { + u32 frames_decoded = 0; + std::span in_buf(job.input.buffer); + SparseOutputBuffer out_buf(job.output.buffers); + + auto in_size = in_buf.size(); + auto out_size = out_buf.Size(); + while (!in_buf.empty() && !out_buf.IsEmpty() && !IsGaplessEnd()) { + const auto samples_remain = + m_gapless.total_samples != 0 + ? std::optional{m_gapless.total_samples - m_gapless_samples} + : std::optional{}; + const auto [nframes, nsamples] = + m_codec->ProcessData(in_buf, out_buf, m_gapless, samples_remain); + frames_decoded += nframes; + m_total_samples += nsamples; + m_gapless_samples += nsamples; + } + if (job.output.p_mframe) { + job.output.p_mframe->num_frames = frames_decoded; + } + if (job.output.p_stream) { + job.output.p_stream->input_consumed = in_size - in_buf.size(); + job.output.p_stream->output_written = out_size - out_buf.Size(); + job.output.p_stream->total_decoded_samples = m_total_samples; + } + } + + if (m_flags.gapless_loop && m_gapless.total_samples != 0 && + m_gapless_samples >= m_gapless.total_samples) { + m_gapless_samples = 0; + m_gapless.skipped_samples = 0; + m_codec->Reset(); + } + if (job.output.p_format != nullptr) { + *job.output.p_format = m_codec->GetFormat(); + } + if (job.output.p_gapless_decode != nullptr) { + *job.output.p_gapless_decode = m_gapless; + } + if (job.output.p_codec_info != nullptr) { + m_codec->GetInfo(job.output.p_codec_info); + } +} + +bool AjmInstance::IsGaplessEnd() { + return m_gapless.total_samples != 0 && m_gapless_samples >= m_gapless.total_samples; +} + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_instance.h b/src/core/libraries/ajm/ajm_instance.h new file mode 100644 index 000000000..d1d398ae8 --- /dev/null +++ b/src/core/libraries/ajm/ajm_instance.h @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/enum.h" +#include "common/types.h" +#include "core/libraries/ajm/ajm.h" +#include "core/libraries/ajm/ajm_batch.h" + +#include +#include +#include + +namespace Libraries::Ajm { + +class SparseOutputBuffer { +public: + SparseOutputBuffer(std::span> chunks) + : m_chunks(chunks), m_current(m_chunks.begin()) {} + + template + size_t Write(std::span pcm) { + size_t samples_written = 0; + while (!pcm.empty() && !IsEmpty()) { + auto size = std::min(pcm.size() * sizeof(T), m_current->size()); + std::memcpy(m_current->data(), pcm.data(), size); + const auto nsamples = size / sizeof(T); + samples_written += nsamples; + pcm = pcm.subspan(nsamples); + *m_current = m_current->subspan(size); + if (m_current->empty()) { + ++m_current; + } + } + return samples_written; + } + + bool IsEmpty() { + return m_current == m_chunks.end(); + } + + size_t Size() { + size_t result = 0; + for (auto it = m_current; it != m_chunks.end(); ++it) { + result += it->size(); + } + return result; + } + +private: + std::span> m_chunks; + std::span>::iterator m_current; +}; + +class AjmCodec { +public: + virtual ~AjmCodec() = default; + + virtual void Initialize(const void* buffer, u32 buffer_size) = 0; + virtual void Reset() = 0; + virtual void GetInfo(void* out_info) = 0; + virtual AjmSidebandFormat GetFormat() = 0; + virtual std::tuple ProcessData(std::span& input, SparseOutputBuffer& output, + AjmSidebandGaplessDecode& gapless, + std::optional max_samples_per_channel) = 0; +}; + +class AjmInstance { +public: + AjmInstance(AjmCodecType codec_type, AjmInstanceFlags flags); + + void ExecuteJob(AjmJob& job); + +private: + bool IsGaplessEnd(); + + AjmInstanceFlags m_flags{}; + AjmSidebandFormat m_format{}; + AjmSidebandGaplessDecode m_gapless{}; + AjmSidebandResampleParameters m_resample_parameters{}; + + u32 m_gapless_samples{}; + u32 m_total_samples{}; + + std::unique_ptr m_codec; +}; + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_mp3.cpp b/src/core/libraries/ajm/ajm_mp3.cpp new file mode 100644 index 000000000..eb65fe2a8 --- /dev/null +++ b/src/core/libraries/ajm/ajm_mp3.cpp @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/assert.h" +#include "core/libraries/ajm/ajm_error.h" +#include "core/libraries/ajm/ajm_mp3.h" +#include "core/libraries/error_codes.h" + +extern "C" { +#include +#include +#include +} + +namespace Libraries::Ajm { + +// Following tables have been reversed from AJM library +static constexpr std::array, 3> SamplerateTable = {{ + {0x5622, 0x5DC0, 0x3E80}, + {0xAC44, 0xBB80, 0x7D00}, + {0x2B11, 0x2EE0, 0x1F40}, +}}; + +static constexpr std::array, 2> BitrateTable = {{ + {0, 0x20, 0x28, 0x30, 0x38, 0x40, 0x50, 0x60, 0x70, 0x80, 0xA0, 0xC0, 0xE0, 0x100, 0x140}, + {0, 0x8, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0}, +}}; + +static constexpr std::array UnkTable = {0x48, 0x90}; + +SwrContext* swr_context{}; + +static AVSampleFormat AjmToAVSampleFormat(AjmFormatEncoding format) { + switch (format) { + case AjmFormatEncoding::S16: + return AV_SAMPLE_FMT_S16; + case AjmFormatEncoding::S32: + return AV_SAMPLE_FMT_S32; + case AjmFormatEncoding::Float: + return AV_SAMPLE_FMT_FLT; + default: + UNREACHABLE(); + } +} + +AVFrame* AjmMp3Decoder::ConvertAudioFrame(AVFrame* frame) { + AVSampleFormat format = AjmToAVSampleFormat(m_format); + if (frame->format == format) { + return frame; + } + + auto pcm16_frame = av_frame_clone(frame); + pcm16_frame->format = format; + + if (swr_context) { + swr_free(&swr_context); + swr_context = nullptr; + } + AVChannelLayout in_ch_layout = frame->ch_layout; + AVChannelLayout out_ch_layout = pcm16_frame->ch_layout; + swr_alloc_set_opts2(&swr_context, &out_ch_layout, AVSampleFormat(pcm16_frame->format), + frame->sample_rate, &in_ch_layout, AVSampleFormat(frame->format), + frame->sample_rate, 0, nullptr); + swr_init(swr_context); + const auto res = swr_convert_frame(swr_context, pcm16_frame, frame); + if (res < 0) { + LOG_ERROR(Lib_AvPlayer, "Could not convert to S16: {}", av_err2str(res)); + return nullptr; + } + av_frame_free(&frame); + return pcm16_frame; +} + +AjmMp3Decoder::AjmMp3Decoder(AjmFormatEncoding format) + : m_format(format), m_codec(avcodec_find_decoder(AV_CODEC_ID_MP3)), + m_parser(av_parser_init(m_codec->id)) { + AjmMp3Decoder::Reset(); +} + +AjmMp3Decoder::~AjmMp3Decoder() { + avcodec_free_context(&m_codec_context); +} + +void AjmMp3Decoder::Reset() { + if (m_codec_context) { + avcodec_free_context(&m_codec_context); + } + m_codec_context = avcodec_alloc_context3(m_codec); + ASSERT_MSG(m_codec_context, "Could not allocate audio m_codec context"); + int ret = avcodec_open2(m_codec_context, m_codec, nullptr); + ASSERT_MSG(ret >= 0, "Could not open m_codec"); +} + +void AjmMp3Decoder::GetInfo(void* out_info) { + auto* info = reinterpret_cast(out_info); +} + +std::tuple AjmMp3Decoder::ProcessData(std::span& in_buf, SparseOutputBuffer& output, + AjmSidebandGaplessDecode& gapless, + std::optional max_samples_per_channel) { + AVPacket* pkt = av_packet_alloc(); + + int ret = av_parser_parse2(m_parser, m_codec_context, &pkt->data, &pkt->size, in_buf.data(), + in_buf.size(), AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); + ASSERT_MSG(ret >= 0, "Error while parsing {}", ret); + in_buf = in_buf.subspan(ret); + + u32 frames_decoded = 0; + u32 samples_decoded = 0; + + if (pkt->size) { + // Send the packet with the compressed data to the decoder + pkt->pts = m_parser->pts; + pkt->dts = m_parser->dts; + pkt->flags = (m_parser->key_frame == 1) ? AV_PKT_FLAG_KEY : 0; + ret = avcodec_send_packet(m_codec_context, pkt); + ASSERT_MSG(ret >= 0, "Error submitting the packet to the decoder {}", ret); + + // Read all the output frames (in general there may be any number of them + while (ret >= 0) { + AVFrame* frame = av_frame_alloc(); + ret = avcodec_receive_frame(m_codec_context, frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + break; + } else if (ret < 0) { + UNREACHABLE_MSG("Error during decoding"); + } + frame = ConvertAudioFrame(frame); + + frames_decoded += 1; + u32 skipped_samples = 0; + if (gapless.skipped_samples < gapless.skip_samples) { + skipped_samples = std::min(u32(frame->nb_samples), + u32(gapless.skip_samples - gapless.skipped_samples)); + gapless.skipped_samples += skipped_samples; + } + + const auto max_samples = + max_samples_per_channel.has_value() + ? max_samples_per_channel.value() * frame->ch_layout.nb_channels + : std::numeric_limits::max(); + + switch (m_format) { + case AjmFormatEncoding::S16: + samples_decoded += + WriteOutputSamples(frame, output, skipped_samples, max_samples); + break; + case AjmFormatEncoding::S32: + samples_decoded += + WriteOutputSamples(frame, output, skipped_samples, max_samples); + break; + case AjmFormatEncoding::Float: + samples_decoded += + WriteOutputSamples(frame, output, skipped_samples, max_samples); + break; + default: + UNREACHABLE(); + } + + av_frame_free(&frame); + } + } + + av_packet_free(&pkt); + + return {frames_decoded, samples_decoded}; +} + +int AjmMp3Decoder::ParseMp3Header(const u8* buf, u32 stream_size, int parse_ofl, + AjmDecMp3ParseFrame* frame) { + LOG_INFO(Lib_Ajm, "called stream_size = {} parse_ofl = {}", stream_size, 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; + } + + const u32 unk_idx = buf[1] >> 3 & 1; + const s32 version_idx = (buf[1] >> 3 & 3) ^ 2; + const s32 sr_idx = buf[2] >> 2 & 3; + const s32 br_idx = (buf[2] >> 4) & 0xf; + const s32 padding_bit = (buf[2] >> 1) & 0x1; + + frame->sample_rate = SamplerateTable[version_idx][sr_idx]; + frame->bitrate = BitrateTable[version_idx != 1][br_idx] * 1000; + frame->num_channels = (buf[3] < 0xc0) + 1; + frame->frame_size = (UnkTable[unk_idx] * frame->bitrate) / frame->sample_rate + padding_bit; + frame->samples_per_channel = UnkTable[unk_idx] * 8; + frame->encoder_delay = 0; + + return ORBIS_OK; +} + +AjmSidebandFormat AjmMp3Decoder::GetFormat() { + LOG_ERROR(Lib_Ajm, "Unimplemented"); + return AjmSidebandFormat{}; +}; + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_mp3.h b/src/core/libraries/ajm/ajm_mp3.h new file mode 100644 index 000000000..0ae956d61 --- /dev/null +++ b/src/core/libraries/ajm/ajm_mp3.h @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" +#include "core/libraries/ajm/ajm_instance.h" + +extern "C" { +#include +} + +namespace Libraries::Ajm { + +enum class AjmDecMp3OflType : u32 { None = 0, Lame = 1, Vbri = 2, Fgh = 3, VbriAndFgh = 4 }; + +// 11-bit syncword if MPEG 2.5 extensions are enabled +static constexpr u8 SYNCWORDH = 0xff; +static constexpr u8 SYNCWORDL = 0xe0; + +struct AjmDecMp3ParseFrame { + u64 frame_size; + u32 num_channels; + u32 samples_per_channel; + u32 bitrate; + u32 sample_rate; + u32 encoder_delay; + u32 num_frames; + u32 total_samples; + AjmDecMp3OflType ofl_type; +}; + +enum class ChannelMode : u8 { + Stereo = 0, + JointStero = 1, + Dual = 2, + Mono = 3, +}; + +struct AjmSidebandDecMp3CodecInfo { + u32 header; + bool has_crc; + ChannelMode channel_mode; + u8 mode_extension; + u8 copyright; + u8 original; + u8 emphasis; + u16 reserved[3]; +}; + +class AjmMp3Decoder : public AjmCodec { +public: + explicit AjmMp3Decoder(AjmFormatEncoding format); + ~AjmMp3Decoder() override; + + void Reset() override; + void Initialize(const void* buffer, u32 buffer_size) override {} + void GetInfo(void* out_info) override; + AjmSidebandFormat GetFormat() override; + std::tuple ProcessData(std::span& input, SparseOutputBuffer& output, + AjmSidebandGaplessDecode& gapless, + std::optional max_samples_per_channel) override; + + static int ParseMp3Header(const u8* buf, u32 stream_size, int parse_ofl, + AjmDecMp3ParseFrame* frame); + +private: + template + size_t WriteOutputSamples(AVFrame* frame, SparseOutputBuffer& output, u32 skipped_samples, + u32 max_samples) { + const auto size = frame->ch_layout.nb_channels * frame->nb_samples * sizeof(T); + std::span pcm_data(reinterpret_cast(frame->data[0]), size >> 1); + pcm_data = pcm_data.subspan(skipped_samples * frame->ch_layout.nb_channels); + const auto pcm_size = std::min(u32(pcm_data.size()), max_samples); + const auto samples_written = output.Write(pcm_data.subspan(0, pcm_size)); + return samples_written / frame->ch_layout.nb_channels; + } + + AVFrame* ConvertAudioFrame(AVFrame* frame); + + const AjmFormatEncoding m_format; + const AVCodec* m_codec = nullptr; + AVCodecContext* m_codec_context = nullptr; + AVCodecParserContext* m_parser = nullptr; +}; + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/videodec/videodec2_impl.cpp b/src/core/libraries/videodec/videodec2_impl.cpp index 021965e3d..37270fa8c 100644 --- a/src/core/libraries/videodec/videodec2_impl.cpp +++ b/src/core/libraries/videodec/videodec2_impl.cpp @@ -8,6 +8,17 @@ #include "common/logging/log.h" #include "core/libraries/error_codes.h" +// The av_err2str macro in libavutil/error.h does not play nice with C++ +#ifdef av_err2str +#undef av_err2str +#include +av_always_inline std::string av_err2string(int errnum) { + char errbuf[AV_ERROR_MAX_STRING_SIZE]; + return av_make_error_string(errbuf, AV_ERROR_MAX_STRING_SIZE, errnum); +} +#define av_err2str(err) av_err2string(err).c_str() +#endif // av_err2str + namespace Libraries::Vdec2 { std::vector gPictureInfos; @@ -225,4 +236,4 @@ AVFrame* VdecDecoder::ConvertNV12Frame(AVFrame& frame) { return nv12_frame; } -} // namespace Libraries::Vdec2 \ No newline at end of file +} // namespace Libraries::Vdec2 diff --git a/src/emulator.cpp b/src/emulator.cpp index ae3cb17dd..d9d32ec1d 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -274,7 +274,7 @@ void Emulator::Run(const std::filesystem::path& file) { } void Emulator::LoadSystemModules(const std::filesystem::path& file, std::string game_serial) { - constexpr std::array ModulesToLoad{ + constexpr std::array ModulesToLoad{ {{"libSceNgs2.sprx", &Libraries::Ngs2::RegisterlibSceNgs2}, {"libSceFiber.sprx", &Libraries::Fiber::RegisterlibSceFiber}, {"libSceUlt.sprx", nullptr}, @@ -285,8 +285,7 @@ void Emulator::LoadSystemModules(const std::filesystem::path& file, std::string {"libSceRtc.sprx", &Libraries::Rtc::RegisterlibSceRtc}, {"libSceJpegEnc.sprx", nullptr}, {"libSceRazorCpu.sprx", nullptr}, - {"libSceCesCs.sprx", nullptr}, - {"libSceRudp.sprx", nullptr}}}; + {"libSceCesCs.sprx", nullptr}}}; std::vector found_modules; const auto& sys_module_path = Common::FS::GetUserPath(Common::FS::PathType::SysModuleDir); diff --git a/src/qt_gui/cheats_patches.cpp b/src/qt_gui/cheats_patches.cpp index 6e2e4f208..a35136f12 100644 --- a/src/qt_gui/cheats_patches.cpp +++ b/src/qt_gui/cheats_patches.cpp @@ -434,7 +434,9 @@ QCheckBox* CheatsPatches::findCheckBoxByName(const QString& name) { QWidget* widget = item->widget(); QCheckBox* checkBox = qobject_cast(widget); if (checkBox) { - if (checkBox->text().toStdString().find(name.toStdString()) != std::string::npos) { + const auto patchName = checkBox->property("patchName"); + if (patchName.isValid() && patchName.toString().toStdString().find( + name.toStdString()) != std::string::npos) { return checkBox; } } @@ -1176,6 +1178,7 @@ void CheatsPatches::addPatchesToLayout(const QString& filePath) { if (!patchName.isEmpty() && !patchLines.isEmpty()) { QCheckBox* patchCheckBox = new QCheckBox(patchName); + patchCheckBox->setProperty("patchName", patchName); patchCheckBox->setChecked(isEnabled); patchesGroupBoxLayout->addWidget(patchCheckBox); @@ -1349,8 +1352,10 @@ bool CheatsPatches::eventFilter(QObject* obj, QEvent* event) { void CheatsPatches::onPatchCheckBoxHovered(QCheckBox* checkBox, bool hovered) { if (hovered) { - QString text = checkBox->text(); - updateNoteTextEdit(text); + const auto patchName = checkBox->property("patchName"); + if (patchName.isValid()) { + updateNoteTextEdit(patchName.toString()); + } } else { instructionsTextEdit->setText(defaultTextEdit); } diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index e52820102..8e56a6e9d 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -673,6 +673,12 @@ void MainWindow::InstallDragDropPkg(std::filesystem::path file, int pkgNum, int QMessageBox::critical(this, tr("PKG ERROR"), QString::fromStdString(failreason)); return; } + if (!psf.Open(pkg.sfo)) { + QMessageBox::critical(this, tr("PKG ERROR"), + "Could not read SFO. Check log for details"); + return; + } + auto category = psf.GetString("CATEGORY"); InstallDirSelect ids; ids.exec(); auto game_install_dir = ids.getSelectedDirectory(); @@ -689,12 +695,6 @@ void MainWindow::InstallDragDropPkg(std::filesystem::path file, int pkgNum, int QMessageBox msgBox; msgBox.setWindowTitle(tr("PKG Extraction")); - if (!psf.Open(pkg.sfo)) { - QMessageBox::critical(this, tr("PKG ERROR"), - "Could not read SFO. Check log for details"); - return; - } - std::string content_id; if (auto value = psf.GetString("CONTENT_ID"); value.has_value()) { content_id = std::string{*value}; @@ -709,7 +709,6 @@ void MainWindow::InstallDragDropPkg(std::filesystem::path file, int pkgNum, int QString addonDirPath; Common::FS::PathToQString(addonDirPath, addon_extract_path); QDir addon_dir(addonDirPath); - auto category = psf.GetString("CATEGORY"); if (pkgType.contains("PATCH")) { QString pkg_app_version; @@ -802,9 +801,10 @@ void MainWindow::InstallDragDropPkg(std::filesystem::path file, int pkgNum, int } } else { // Do nothing; - if (pkgType.contains("PATCH")) { - QMessageBox::information(this, tr("PKG Extraction"), - tr("PKG is a patch, please install the game first!")); + if (pkgType.contains("PATCH") || category == "ac") { + QMessageBox::information( + this, tr("PKG Extraction"), + tr("PKG is a patch or DLC, please install the game first!")); return; } // what else? diff --git a/src/qt_gui/settings_dialog.cpp b/src/qt_gui/settings_dialog.cpp index 309b40b46..84dc5011e 100644 --- a/src/qt_gui/settings_dialog.cpp +++ b/src/qt_gui/settings_dialog.cpp @@ -42,10 +42,11 @@ QStringList languageNames = {"Arabic", "Thai", "Traditional Chinese", "Turkish", + "Ukrainian", "Vietnamese"}; -const QVector languageIndexes = {21, 23, 14, 6, 18, 1, 12, 22, 2, 4, 25, 24, 29, 5, 0, - 9, 15, 16, 17, 7, 26, 8, 11, 20, 3, 13, 27, 10, 19, 28}; +const QVector languageIndexes = {21, 23, 14, 6, 18, 1, 12, 22, 2, 4, 25, 24, 29, 5, 0, 9, + 15, 16, 17, 7, 26, 8, 11, 20, 3, 13, 27, 10, 19, 30, 28}; SettingsDialog::SettingsDialog(std::span physical_devices, QWidget* parent) : QDialog(parent), ui(new Ui::SettingsDialog) { diff --git a/src/qt_gui/translations/uk_UA.ts b/src/qt_gui/translations/uk_UA.ts new file mode 100644 index 000000000..61c884986 --- /dev/null +++ b/src/qt_gui/translations/uk_UA.ts @@ -0,0 +1,1476 @@ + + + + + + AboutDialog + + + About shadPS4 + Про shadPS4 + + + + shadPS4 + shadPS4 + + + + shadPS4 is an experimental open-source emulator for the PlayStation 4. + shadPS4 - це експериментальний емулятор з відкритим вихідним кодом для PlayStation 4. + + + + This software should not be used to play games you have not legally obtained. + Це програмне забезпечення не повинно використовуватися для запуску ігор, котрі ви отримали не легально. + + + + ElfViewer + + + Open Folder + Відкрити папку + + + + GameInfoClass + + + Loading game list, please wait :3 + Завантажуємо список ігор, будь ласка, зачекайте :3 + + + + Cancel + Відмінити + + + + Loading... + Завантаження... + + + + InstallDirSelect + + + shadPS4 - Choose directory + shadPS4 - Виберіть папку + + + + Select which directory you want to install to. + Виберіть папку, до якої ви хочете встановити. + + + + GameInstallDialog + + + shadPS4 - Choose directory + shadPS4 - Виберіть папку + + + + Directory to install games + Папка для встановлення ігор + + + + Browse + Обрати + + + + Error + Помилка + + + + The value for location to install games is not valid. + Не коректне значення розташування для встановлення ігор. + + + + GuiContextMenus + + + Create Shortcut + Створити Ярлик + + + + Open Game Folder + Відкрити папку з грою + + + + Cheats / Patches + Чити та Патчі + + + + SFO Viewer + Перегляд SFO + + + + Trophy Viewer + Перегляд трофеїв + + + + Copy info + Копіювати інформацію + + + + Copy Name + Копіювати Ім’я + + + + Copy Serial + Копіювати серійний номер + + + + Copy All + Копіювати все + + + + Delete... + Видалення... + + + + Delete Game + Видалити гру + + + + Delete Update + Видалити оновлення + + + + Delete DLC + Видалити DLC + + + + Shortcut creation + Створення ярлика + + + + Shortcut created successfully!\n %1 + Ярлик створений успішно!\n %1 + + + + Error + Помилка + + + + Error creating shortcut!\n %1 + Помилка при створенні ярлика!\n %1 + + + + Install PKG + Встановити PKG + + + + Game + Ігри + + + + requiresEnableSeparateUpdateFolder_MSG + Ця функція потребує увімкнути опцію 'Окрема папка оновлень'. Якщо ви хочете використовувати цю функцію, будь ласка, увімкніть її. + + + + This game has no update to delete! + Ця гра не має оновлень для видалення! + + + + Update + Оновлення + + + + This game has no DLC to delete! + Ця гра не має DLC для видалення! + + + + DLC + DLC + + + + Delete %1 + Видалити %1 + + + + Are you sure you want to delete %1's %2 directory? + Ви впевнені, що хочете видалити папку %1 з папки %2?? + + + + MainWindow + + + Open/Add Elf Folder + Відкрити/Додати папку Elf + + + + Install Packages (PKG) + Встановити пакети (PKG) + + + + Boot Game + Запустити гру + + + + Check for Updates + Перевити наявність оновлень + + + + About shadPS4 + Про shadPS4 + + + + Configure... + Налаштувати... + + + + Install application from a .pkg file + Встановити додаток з файлу .pkg + + + + Recent Games + Нещодавні ігри + + + + Exit + Вихід + + + + Exit shadPS4 + Вийти з shadPS4 + + + + Exit the application. + Вийти з додатку. + + + + Show Game List + Показати список ігор + + + + Game List Refresh + Оновити список ігор + + + + Tiny + Крихітний + + + + Small + Маленький + + + + Medium + Середній + + + + Large + Великий + + + + List View + Список + + + + Grid View + Сітка + + + + Elf Viewer + Elf + + + + Game Install Directory + Каталог встановлення гри + + + + Download Cheats/Patches + Завантажити Чити або Патчі + + + + Dump Game List + Дамп списку ігор + + + + PKG Viewer + Перегляд PKG + + + + Search... + Пошук... + + + + File + Файл + + + + View + Вид + + + + Game List Icons + Розмір значків списку игр + + + + Game List Mode + Вид списку ігор + + + + Settings + Налаштування + + + + Utils + Утиліти + + + + Themes + Теми + + + + Help + Допомога + + + + Dark + Темна + + + + Light + Світла + + + + Green + Зелена + + + + Blue + Синя + + + + Violet + Фіолетова + + + + toolBar + Панель інструментів + + + + PKGViewer + + + Open Folder + Відкрити папку + + + + TrophyViewer + + + Trophy Viewer + Трофеї + + + + SettingsDialog + + + Settings + Налаштування + + + + General + Загальні + + + + System + Система + + + + Console Language + Мова консолі + + + + Emulator Language + Мова емулятора + + + + Emulator + Емулятор + + + + Enable Fullscreen + Увімкнути повноекранний режим + + + + Enable Separate Update Folder + Увімкнути окрему папку оновлень + + + + Show Splash + Показувати заставку + + + + Is PS4 Pro + Режим PS4 Pro + + + + Enable Discord Rich Presence + Увімкнути Discord Rich Presence + + + + Username + Ім'я користувача + + + + Logger + Логування + + + + Log Type + Тип логів + + + + Log Filter + Фільтр логів + + + + Input + Введення + + + + Cursor + Курсор миші + + + + Hide Cursor + Приховати курсор + + + + Hide Cursor Idle Timeout + Тайм-аут приховування курсора при бездіяльності + + + + Controller + Контролер + + + + Back Button Behavior + Поведінка кнопки назад + + + + Graphics + Графіка + + + + Graphics Device + Графічний пристрій + + + + Width + Ширина + + + + Height + Висота + + + + Vblank Divider + Розділювач Vblank + + + + Advanced + Розширені + + + + Enable Shaders Dumping + Увімкнути дамп шейдерів + + + + Enable NULL GPU + Увімкнути NULL GPU + + + + Paths + Шляхи + + + + Game Folders + Ігрові папки + + + + Add... + Додати... + + + + Remove + Видалити + + + + Debug + Налагодження + + + + Enable Debug Dumping + Увімкнути налагоджувальні дампи + + + + Enable Vulkan Validation Layers + Увімкнути шари валідації Vulkan + + + + Enable Vulkan Synchronization Validation + Увімкнути валідацію синхронізації Vulkan + + + + Enable RenderDoc Debugging + Увімкнути налагодження RenderDoc + + + + Update + Оновлення + + + + Check for Updates at Startup + Перевірка оновлень під час запуску + + + + Update Channel + Канал оновлення + + + + Check for Updates + Перевірити оновлення + + + + GUI Settings + Інтерфейс + + + + Play title music + Програвати заголовну музику + + + + Volume + Гучність + + + + MainWindow + + + Game List + Список ігор + + + + * Unsupported Vulkan Version + * Непідтримувана версія Vulkan + + + + Download Cheats For All Installed Games + Завантажити чити для всіх встановлених ігор + + + + Download Patches For All Games + Завантажити патчі для всіх ігор + + + + Download Complete + Завантаження завершено + + + + You have downloaded cheats for all the games you have installed. + Ви завантажили чити для всіх встановлених ігор. + + + + Patches Downloaded Successfully! + Патчі успішно завантажено! + + + + All Patches available for all games have been downloaded. + Завантажено всі доступні патчі для всіх ігор. + + + + Games: + Ігри: + + + + PKG File (*.PKG) + Файл PKG (*.PKG) + + + + ELF files (*.bin *.elf *.oelf) + Файл ELF (*.bin *.elf *.oelf) + + + + Game Boot + Запуск гри + + + + Only one file can be selected! + Можна вибрати лише один файл! + + + + PKG Extraction + Видобуток PKG + + + + Patch detected! + Виявлено патч! + + + + PKG and Game versions match: + Версії PKG та гри збігаються: + + + + Would you like to overwrite? + Бажаєте перезаписати? + + + + PKG Version %1 is older than installed version: + Версія PKG %1 старіша за встановлену версію: + + + + Game is installed: + Гра встановлена: + + + + Would you like to install Patch: + Бажаєте встановити патч: + + + + DLC Installation + Встановлення DLC + + + + Would you like to install DLC: %1? + Ви бажаєте встановити DLC: %1?? + + + + DLC already installed: + DLC вже встановлено: + + + + Game already installed + Гра вже встановлена + + + + PKG is a patch, please install the game first! + PKG - це патч, будь ласка, спочатку встановіть гру! + + + + PKG ERROR + ПОМИЛКА PKG + + + + Extracting PKG %1/%2 + Вилучення PKG %1/%2 + + + + Extraction Finished + Вилучення завершено + + + + Game successfully installed at %1 + Гру успішно встановлено у %1 + + + + File doesn't appear to be a valid PKG file + Файл не є дійсним PKG-файлом + + + + CheatsPatches + + + Cheats / Patches + Чити та Патчі + + + + defaultTextEdit_MSG + Чити та Патчі є експериментальними.\nВикористовуйте з обережністю.\n\nЗавантажуйте чити окремо, вибравши репозиторій і натиснувши кнопку завантаження.\nУ вкладці "Патчі" ви можете завантажити всі патчі відразу, вибрати, які з них ви хочете використовувати, і зберегти свій вибір.\n\nОскільки ми не займаємося розробкою читів/патчів,\nбудь ласка, повідомляйте про проблеми автору чита/патча.\n\nСтворили новий чит? Відвідайте:\nhttps://github.com/shadps4-emu/ps4_cheats + + + + No Image Available + Зображення відсутнє + + + + Serial: + Серійний номер: + + + + Version: + Версія: + + + + Size: + Розмір: + + + + Select Cheat File: + Виберіть файл читу: + + + + Repository: + Репозиторій: + + + + Download Cheats + Завантажити чити + + + + Delete File + Видалити файл + + + + No files selected. + Файли не вибрані. + + + + You can delete the cheats you don't want after downloading them. + Ви можете видалити непотрібні чити після їх завантаження. + + + + Do you want to delete the selected file?\n%1 + Ви хочете видалити вибраний файл?\n%1 + + + + Select Patch File: + Виберіть файл патчу: + + + + Download Patches + Завантажити патчі + + + + Save + Зберегти + + + + Cheats + Чити + + + + Patches + Патчі + + + + Error + Помилка + + + + No patch selected. + Патч не вибрано. + + + + Unable to open files.json for reading. + Не вдалось відкрити files.json для читання. + + + + No patch file found for the current serial. + Файл патча для поточного серійного номера не знайдено. + + + + Unable to open the file for reading. + Не вдалося відкрити файл для читання. + + + + Unable to open the file for writing. + Не вдалось відкрити файл для запису. + + + + Failed to parse XML: + Не вдалося розібрати XML: + + + + Success + Успіх + + + + Options saved successfully. + Параметри успішно збережено. + + + + Invalid Source + Неправильне джерело + + + + The selected source is invalid. + Вибране джерело є недійсним. + + + + File Exists + Файл існує + + + + File already exists. Do you want to replace it? + Файл вже існує. Ви хочете замінити його? + + + + Failed to save file: + Не вдалося зберегти файл: + + + + Failed to download file: + Не вдалося завантажити файл: + + + + Cheats Not Found + Читів не знайдено + + + + CheatsNotFound_MSG + У вибраному репозиторії не знайдено Читів для цієї гри, спробуйте інший репозиторій або іншу версію гри. + + + + Cheats Downloaded Successfully + Чити успішно завантажено + + + + CheatsDownloadedSuccessfully_MSG + Ви успішно завантажили чити для цієї версії гри з обраного репозиторія. Ви можете спробувати завантажити з іншого репозиторія, якщо він буде доступним, ви також зможете скористатися ним, вибравши файл зі списку. + + + + Failed to save: + Не вдалося зберегти: + + + + Failed to download: + Не вдалося завантажити: + + + + Download Complete + Заватнаження завершено + + + + DownloadComplete_MSG + Патчі успішно завантажено! Всі доступні патчі для усіх ігор, завантажено, немає необхідності завантажувати їх окремо для кожної гри, як це відбувається у випадку з читами. Якщо патч не з’являється, можливо, його не існує для конкретного серійного номера та версії гри. Можливо, необхідно оновити гру. + + + + Failed to parse JSON data from HTML. + Не вдалося розібрати JSON-дані з HTML. + + + + Failed to retrieve HTML page. + Не вдалося отримати HTML-сторінку. + + + + The game is in version: %1 + Гра у версії: %1 + + + + The downloaded patch only works on version: %1 + Завантажений патч працює лише на версії: %1 + + + + You may need to update your game. + Можливо, вам потрібно оновити гру. + + + + Incompatibility Notice + Повідомлення про несумісність + + + + Failed to open file: + Не вдалося відкрити файл: + + + + XML ERROR: + ПОМИЛКА XML: + + + + Failed to open files.json for writing + Не вдалося відкрити files.json для запису + + + + Author: + Автор: + + + + Directory does not exist: + Каталогу не існує: + + + + Failed to open files.json for reading. + Не вдалося відкрити files.json для читання. + + + + Name: + Ім'я: + + + + Can't apply cheats before the game is started + Неможливо застосовувати чити до початку гри. + + + + SettingsDialog + + + Save + Зберегти + + + + Apply + Застосувати + + + + Restore Defaults + За замовчуванням + + + + Close + Закрити + + + + Point your mouse at an option to display its description. + Наведіть курсор миші на опцію, щоб відобразити її опис. + + + + consoleLanguageGroupBox + Мова консолі:\nВстановіть мову, яка буде використовуватись у іграх PS4.\nРекомендується встановити мову котра підтримується грою, оскільки вона може відрізнятися в залежності від регіону. + + + + emulatorLanguageGroupBox + Мова емулятора:\nВстановіть мову користувацького інтерфейсу емулятора. + + + + fullscreenCheckBox + Повноекранний режим:\nАвтоматично переводить вікно гри у повноекранний режим.\nВи можете відключити це, натиснувши клавішу F11. + + + + separateUpdatesCheckBox + Окрема папка для оновлень:\nДає змогу встановлювати оновлення гри в окрему папку для зручності. + + + + showSplashCheckBox + Показувати заставку:\nВідображає заставку гри (спеціальне зображення) під час запуску гри. + + + + ps4proCheckBox + Режим PS4 Pro:\nЗмушує емулятор працювати як PS4 Pro, що може ввімкнути спеціальні функції в іграх, які підтримують це. + + + + discordRPCCheckbox + Увімкнути Discord Rich Presence:\nВідображає значок емулятора та відповідну інформацію у вашому профілі Discord. + + + + userName + Ім'я користувача:\nВстановіть ім'я користувача акаунта PS4. Це може відображатися в деяких іграх. + + + + logTypeGroupBox + Тип логів:\nВстановіть, чи синхронізувати виведення вікна логів заради продуктивності. Це може негативно вплинути на емуляцію. + + + + logFilter + Фільтр логів:\nФільтрує логи, щоб показувати тільки певну інформацію.\nПриклади: "Core:Trace" "Lib.Pad:Debug Common.Filesystem:Error" "*:Critical" Рівні: Trace, Debug, Info, Warning, Error, Critical - у цьому порядку, конкретний рівень глушить усі попередні рівні у списку і показує всі наступні рівні. + + + + updaterGroupBox + Оновлення:\nRelease: Офіційні версії, які випускаються щомісяця і можуть бути дуже старими, але вони більш надійні та перевірені.\nNightly: Версії для розробників, які мають усі найновіші функції та виправлення, але можуть містити помилки та є менш стабільними. + + + + GUIgroupBox + Грати заголовну музику:\nВмикає відтворення спеціальної музики під час вибору гри в списку, якщо вона це підтримує. + + + + hideCursorGroupBox + Приховувати курсор:\nВиберіть, коли курсор зникне:\nНіколи: Ви завжди будете бачити мишу.\nПри бездіяльності: Встановіть час, через який курсор зникне в разі бездіяльності.\nЗавжди: Ви ніколи не будете бачити мишу. + + + + idleTimeoutGroupBox + Встановіть час, через який курсор зникне в разі бездіяльності. + + + + backButtonBehaviorGroupBox + Поведінка кнопки «Назад»:\nНалаштовує кнопку «Назад» контролера на емуляцію натискання на зазначену область на сенсорній панелі контролера PS4. + + + + Never + Ніколи + + + + Idle + При бездіяльності + + + + Always + Завжди + + + + Touchpad Left + Тачпад ліворуч + + + + Touchpad Right + Тачпад праворуч + + + + Touchpad Center + Тачпад по центру + + + + None + Ні + + + + graphicsAdapterGroupBox + Графічний пристрій:\nУ системах із кількома GPU виберіть з випадаючого списку GPU, який буде використовувати емулятор,\nабо виберіть "Auto Select", щоб визначити його автоматично. + + + + resolutionLayout + Ширина/Висота:\nВстановіть розмір вікна емулятора під час запуску, який може бути змінений під час гри.\nЦе відрізняється від роздільної здатності в грі. + + + + heightDivider + Розділювач Vblank:\nЧастота кадрів, з якою оновлюється емулятор, множиться на це число. Зміна цього параметра може мати негативні наслідки, такі як збільшення швидкості гри або порушення критичних функцій гри, які цього не очікують! + + + + dumpShadersCheckBox + Увімкнути дамп шейдерів:\nДля технічного налагодження зберігає шейдери ігор у папку під час рендерингу. + + + + nullGpuCheckBox + Увімкнути NULL GPU:\nДля технічного налагодження відключає рендеринг гри так, ніби графічної карти немає. + + + + gameFoldersBox + Ігрові папки:\nСписок папок для перевірки встановлених ігор. + + + + addFolderButton + Додати:\nДодати папку в список. + + + + removeFolderButton + Видалити:\nВидалити папку зі списку. + + + + debugDump + Увімкнути налагоджувальні дампи:\nЗберігає символи імпорту, експорту та інформацію про заголовок файлу поточної виконуваної програми PS4 у папку. + + + + vkValidationCheckBox + Увімкнути шари валідації Vulkan:\nВключає систему, яка перевіряє стан рендерера Vulkan і логує інформацію про його внутрішній стан. Це знизить продуктивність і, ймовірно, змінить поведінку емуляції. + + + + vkSyncValidationCheckBox + Увімкнути валідацію синхронізації Vulkan:\nВключає систему, яка перевіряє таймінг завдань рендерингу Vulkan. Це знизить продуктивність і, ймовірно, змінить поведінку емуляції. + + + + rdocCheckBox + Увімкнути налагодження RenderDoc:\nЯкщо увімкнено, емулятор забезпечить сумісність із Renderdoc, даючи змогу захоплювати й аналізувати поточні кадри під час рендерингу. + + + + GameListFrame + + + Icon + Значок + + + + Name + Назва + + + + Serial + Серійний номер + + + + Region + Регіон + + + + Firmware + Прошивка + + + + Size + Розмір + + + + Version + Версія + + + + Path + Шлях + + + + Play Time + Час у грі + + + + CheckUpdate + + + Auto Updater + Автооновлення + + + + Error + Помилка + + + + Network error: + Мережева помилка: + + + + Failed to parse update information. + Не вдалося розібрати інформацію про оновлення. + + + + No pre-releases found. + Попередніх версій не знайдено. + + + + Invalid release data. + Неприпустимі дані релізу. + + + + No download URL found for the specified asset. + Не знайдено URL для завантаження зазначеного ресурсу. + + + + Your version is already up to date! + Вашу версію вже оновлено! + + + + Update Available + Доступне оновлення + + + + Update Channel + Канал оновлення + + + + Current Version + Поточна версія + + + + Latest Version + Остання версія + + + + Do you want to update? + Ви хочете оновитися? + + + + Show Changelog + Показати журнал змін + + + + Check for Updates at Startup + Перевірка оновлень під час запуску + + + + Update + Оновитись + + + + No + Ні + + + + Hide Changelog + Приховати журнал змін + + + + Changes + Журнал змін + + + + Network error occurred while trying to access the URL + Сталася мережева помилка під час спроби доступу до URL + + + + Download Complete + Завантаження завершено + + + + The update has been downloaded, press OK to install. + Оновлення завантажено, натисніть OK для встановлення. + + + + Failed to save the update file at + Не вдалося зберегти файл оновлення в + + + + Starting Update... + Початок оновлення... + + + + Failed to create the update script file + Не вдалося створити файл скрипта оновлення + + + \ No newline at end of file diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp index fc99b8925..40e5ea8b9 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp @@ -176,6 +176,7 @@ Id EmitImageFetch(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, const ImageOperands operands; operands.AddOffset(ctx, offset); operands.Add(spv::ImageOperandsMask::Lod, lod); + operands.Add(spv::ImageOperandsMask::Sample, ms); const Id texel = texture.is_storage ? ctx.OpImageRead(result_type, image, coords, operands.mask, operands.operands) diff --git a/src/shader_recompiler/info.h b/src/shader_recompiler/info.h index b69863f4f..f9cbacaf2 100644 --- a/src/shader_recompiler/info.h +++ b/src/shader_recompiler/info.h @@ -86,6 +86,13 @@ struct SamplerResource { }; using SamplerResourceList = boost::container::small_vector; +struct FMaskResource { + u32 sharp_idx; + + constexpr AmdGpu::Image GetSharp(const Info& info) const noexcept; +}; +using FMaskResourceList = boost::container::small_vector; + struct PushData { static constexpr u32 BufOffsetIndex = 2; static constexpr u32 UdRegsIndex = 4; @@ -178,6 +185,7 @@ struct Info { TextureBufferResourceList texture_buffers; ImageResourceList images; SamplerResourceList samplers; + FMaskResourceList fmasks; PersistentSrtInfo srt_info; std::vector flattened_ud_buf; @@ -282,6 +290,10 @@ constexpr AmdGpu::Sampler SamplerResource::GetSharp(const Info& info) const noex return inline_sampler ? inline_sampler : info.ReadUdSharp(sharp_idx); } +constexpr AmdGpu::Image FMaskResource::GetSharp(const Info& info) const noexcept { + return info.ReadUdSharp(sharp_idx); +} + } // namespace Shader template <> diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index 6c8809cf0..586785e6a 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -142,7 +142,7 @@ public: explicit Descriptors(Info& info_) : info{info_}, buffer_resources{info_.buffers}, texture_buffer_resources{info_.texture_buffers}, image_resources{info_.images}, - sampler_resources{info_.samplers} {} + sampler_resources{info_.samplers}, fmask_resources(info_.fmasks) {} u32 Add(const BufferResource& desc) { const u32 index{Add(buffer_resources, desc, [&desc](const auto& existing) { @@ -183,6 +183,13 @@ public: return index; } + u32 Add(const FMaskResource& desc) { + u32 index = Add(fmask_resources, desc, [&desc](const auto& existing) { + return desc.sharp_idx == existing.sharp_idx; + }); + return index; + } + private: template static u32 Add(Descriptors& descriptors, const Descriptor& desc, Func&& pred) { @@ -199,6 +206,7 @@ private: TextureBufferResourceList& texture_buffer_resources; ImageResourceList& image_resources; SamplerResourceList& sampler_resources; + FMaskResourceList& fmask_resources; }; } // Anonymous namespace @@ -618,6 +626,40 @@ void PatchImageInstruction(IR::Block& block, IR::Inst& inst, Info& info, Descrip } ASSERT(image.GetType() != AmdGpu::ImageType::Invalid); const bool is_storage = IsImageStorageInstruction(inst); + + // Patch image instruction if image is FMask. + if (image.IsFmask()) { + ASSERT_MSG(!is_storage, "FMask storage instructions are not supported"); + + IR::IREmitter ir{block, IR::Block::InstructionList::s_iterator_to(inst)}; + switch (inst.GetOpcode()) { + case IR::Opcode::ImageFetch: + case IR::Opcode::ImageSampleRaw: { + IR::F32 fmaskx = ir.BitCast(ir.Imm32(0x76543210)); + IR::F32 fmasky = ir.BitCast(ir.Imm32(0xfedcba98)); + inst.ReplaceUsesWith(ir.CompositeConstruct(fmaskx, fmasky)); + return; + } + case IR::Opcode::ImageQueryLod: + inst.ReplaceUsesWith(ir.Imm32(1)); + return; + case IR::Opcode::ImageQueryDimensions: { + IR::Value dims = ir.CompositeConstruct(ir.Imm32(static_cast(image.width)), // x + ir.Imm32(static_cast(image.width)), // y + ir.Imm32(1), ir.Imm32(1)); // depth, mip + inst.ReplaceUsesWith(dims); + + // Track FMask resource to do specialization. + descriptors.Add(FMaskResource{ + .sharp_idx = tsharp, + }); + return; + } + default: + UNREACHABLE_MSG("Can't patch fmask instruction {}", inst.GetOpcode()); + } + } + const auto type = image.IsPartialCubemap() ? AmdGpu::ImageType::Color2DArray : image.GetType(); u32 image_binding = descriptors.Add(ImageResource{ .sharp_idx = tsharp, @@ -652,11 +694,14 @@ void PatchImageInstruction(IR::Block& block, IR::Inst& inst, Info& info, Descrip return {body->Arg(0), body->Arg(1)}; case AmdGpu::ImageType::Color1DArray: // x, slice [[fallthrough]]; - case AmdGpu::ImageType::Color2D: // x, y - return {ir.CompositeConstruct(body->Arg(0), body->Arg(1)), body->Arg(2)}; - case AmdGpu::ImageType::Color2DArray: // x, y, slice + case AmdGpu::ImageType::Color2D: // x, y, [lod] [[fallthrough]]; - case AmdGpu::ImageType::Color2DMsaa: // x, y, frag + case AmdGpu::ImageType::Color2DMsaa: // x, y. (sample is passed on different argument) + return {ir.CompositeConstruct(body->Arg(0), body->Arg(1)), body->Arg(2)}; + case AmdGpu::ImageType::Color2DArray: // x, y, slice, [lod] + [[fallthrough]]; + case AmdGpu::ImageType::Color2DMsaaArray: // x, y, slice. (sample is passed on different + // argument) [[fallthrough]]; case AmdGpu::ImageType::Color3D: // x, y, z return {ir.CompositeConstruct(body->Arg(0), body->Arg(1), body->Arg(2)), body->Arg(3)}; @@ -672,7 +717,12 @@ void PatchImageInstruction(IR::Block& block, IR::Inst& inst, Info& info, Descrip if (inst_info.has_lod) { ASSERT(inst.GetOpcode() == IR::Opcode::ImageFetch); + ASSERT(image.GetType() == AmdGpu::ImageType::Color2D || + image.GetType() == AmdGpu::ImageType::Color2DArray); inst.SetArg(3, arg); + } else if (image.GetType() == AmdGpu::ImageType::Color2DMsaa || + image.GetType() == AmdGpu::ImageType::Color2DMsaaArray) { + inst.SetArg(4, arg); } } diff --git a/src/shader_recompiler/specialization.h b/src/shader_recompiler/specialization.h index c25c611e4..225b164b5 100644 --- a/src/shader_recompiler/specialization.h +++ b/src/shader_recompiler/specialization.h @@ -32,6 +32,13 @@ struct ImageSpecialization { auto operator<=>(const ImageSpecialization&) const = default; }; +struct FMaskSpecialization { + u32 width; + u32 height; + + auto operator<=>(const FMaskSpecialization&) const = default; +}; + /** * Alongside runtime information, this structure also checks bound resources * for compatibility. Can be used as a key for storing shader permutations. @@ -47,6 +54,7 @@ struct StageSpecialization { boost::container::small_vector buffers; boost::container::small_vector tex_buffers; boost::container::small_vector images; + boost::container::small_vector fmasks; Backend::Bindings start{}; explicit StageSpecialization(const Shader::Info& info_, RuntimeInfo runtime_info_, @@ -71,6 +79,11 @@ struct StageSpecialization { : sharp.GetType(); spec.is_integer = AmdGpu::IsInteger(sharp.GetNumberFmt()); }); + ForEachSharp(binding, fmasks, info->fmasks, + [](auto& spec, const auto& desc, AmdGpu::Image sharp) { + spec.width = sharp.width; + spec.height = sharp.height; + }); } void ForEachSharp(u32& binding, auto& spec_list, auto& desc_list, auto&& func) { @@ -115,6 +128,11 @@ struct StageSpecialization { return false; } } + for (u32 i = 0; i < fmasks.size(); i++) { + if (other.bitset[binding++] && fmasks[i] != other.fmasks[i]) { + return false; + } + } return true; } }; diff --git a/src/video_core/amdgpu/resource.h b/src/video_core/amdgpu/resource.h index 83be0b0a4..81fe43f4e 100644 --- a/src/video_core/amdgpu/resource.h +++ b/src/video_core/amdgpu/resource.h @@ -295,6 +295,11 @@ struct Image { return GetTilingMode() != TilingMode::Display_Linear; } + bool IsFmask() const noexcept { + return GetDataFmt() >= DataFormat::FormatFmask8_1 && + GetDataFmt() <= DataFormat::FormatFmask64_8; + } + bool IsPartialCubemap() const { const auto viewed_slice = last_array - base_array + 1; return GetType() == ImageType::Cube && viewed_slice < 6;