// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "audio_core/common/feature_support.h" #include "audio_core/renderer/behavior/behavior_info.h" #include "audio_core/renderer/behavior/info_updater.h" #include "audio_core/renderer/effect/effect_context.h" #include "audio_core/renderer/effect/effect_reset.h" #include "audio_core/renderer/memory/memory_pool_info.h" #include "audio_core/renderer/mix/mix_context.h" #include "audio_core/renderer/performance/performance_manager.h" #include "audio_core/renderer/sink/circular_buffer_sink_info.h" #include "audio_core/renderer/sink/device_sink_info.h" #include "audio_core/renderer/sink/sink_context.h" #include "audio_core/renderer/splitter/splitter_context.h" #include "audio_core/renderer/voice/voice_context.h" namespace AudioCore::Renderer { InfoUpdater::InfoUpdater(std::span input_, std::span output_, Kernel::KProcess* process_handle_, BehaviorInfo& behaviour_) : input{input_.data() + sizeof(UpdateDataHeader)}, input_origin{input_}, output{output_.data() + sizeof(UpdateDataHeader)}, output_origin{output_}, in_header{reinterpret_cast( input_origin.data())}, out_header{reinterpret_cast(output_origin.data())}, expected_input_size{input_.size()}, expected_output_size{output_.size()}, process_handle{process_handle_}, behaviour{behaviour_} { std::construct_at(out_header, behaviour.GetProcessRevision()); } Result InfoUpdater::UpdateVoiceChannelResources(VoiceContext& voice_context) { const auto voice_count{voice_context.GetCount()}; std::span in_params{ reinterpret_cast(input), voice_count}; for (u32 i = 0; i < voice_count; i++) { auto& resource{voice_context.GetChannelResource(i)}; resource.in_use = in_params[i].in_use; if (in_params[i].in_use) { resource.mix_volumes = in_params[i].mix_volumes; } } const auto consumed_input_size{voice_count * static_cast(sizeof(VoiceChannelResource::InParameter))}; if (consumed_input_size != in_header->voice_resources_size) { LOG_ERROR(Service_Audio, "Consumed an incorrect voice resource size, header size={}, consumed={}", in_header->voice_resources_size, consumed_input_size); return Service::Audio::ResultInvalidUpdateInfo; } input += consumed_input_size; return ResultSuccess; } Result InfoUpdater::UpdateVoices(VoiceContext& voice_context, std::span memory_pools, const u32 memory_pool_count) { const PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, behaviour.IsMemoryForceMappingEnabled()); const auto voice_count{voice_context.GetCount()}; std::span out_params{reinterpret_cast(output), voice_count}; for (u32 i = 0; i < voice_count; i++) { auto& voice_info{voice_context.GetInfo(i)}; voice_info.in_use = false; } u32 new_voice_count{0}; // Two input formats exist: legacy (0x170) and v2 with float biquad (0x188). const bool use_v2 = behaviour.IsVoiceInParameterV2Supported(); const u32 in_stride = use_v2 ? 0x188u : static_cast(sizeof(VoiceInfo::InParameter)); for (u32 i = 0; i < voice_count; i++) { VoiceInfo::InParameter local_in{}; std::array float_biquads{}; if (!use_v2) { const auto* in_param_ptr = reinterpret_cast( input + i * sizeof(VoiceInfo::InParameter)); local_in = *in_param_ptr; } else { struct VoiceInParameterV2 { u32 id; u32 node_id; bool is_new; bool in_use; PlayState play_state; SampleFormat sample_format; u32 sample_rate; u32 priority; u32 sort_order; u32 channel_count; f32 pitch; f32 volume; // Two BiquadFilterParameter2 (0x18 each) -> ignored/converted struct BiquadV2 { bool enable; u8 r1; u8 r2; u8 r3; std::array b; std::array a; } biquads[2]; u32 wave_buffer_count; u32 wave_buffer_index; u32 reserved1; u64 src_data_address; u64 src_data_size; s32 mix_id; u32 splitter_id; std::array wavebuffers; std::array channel_resource_ids; bool clear_voice_drop; u8 flush_wave_buffer_count; u16 reserved2; VoiceInfo::Flags flags; u8 reserved_after_flags; SrcQuality src_quality; u8 crossfade_curve; u32 external_ctx; u32 external_ctx_size; u32 reserved3[2]; }; static_assert(sizeof(VoiceInParameterV2) == 0x188); static_assert(offsetof(VoiceInParameterV2, crossfade_curve) == 0x177); const auto* vin = reinterpret_cast(input + i * in_stride); if (behaviour.GetUserRevisionNum() >= 16 && vin->in_use && vin->crossfade_curve != 0) { LOG_ERROR(Service_Audio, "Unsupported voice biquad crossfade curve {}", vin->crossfade_curve); return Service::Audio::ResultInvalidUpdateInfo; } local_in.id = vin->id; local_in.node_id = vin->node_id; local_in.is_new = vin->is_new; local_in.in_use = vin->in_use; local_in.play_state = vin->play_state; local_in.sample_format = vin->sample_format; local_in.sample_rate = vin->sample_rate; local_in.priority = static_cast(vin->priority); local_in.sort_order = static_cast(vin->sort_order); local_in.channel_count = vin->channel_count; local_in.pitch = vin->pitch; local_in.volume = vin->volume; // For REV15+, we keep float coefficients separate and only convert for compatibility for (size_t filter_idx = 0; filter_idx < MaxBiquadFilters; filter_idx++) { const auto& src = vin->biquads[filter_idx]; auto& dst = local_in.biquads[filter_idx]; dst.enabled = src.enable; // Convert float coefficients to fixed-point Q2.14 for legacy path dst.b[0] = static_cast(std::clamp(src.b[0] * 16384.0f, -32768.0f, 32767.0f)); dst.b[1] = static_cast(std::clamp(src.b[1] * 16384.0f, -32768.0f, 32767.0f)); dst.b[2] = static_cast(std::clamp(src.b[2] * 16384.0f, -32768.0f, 32767.0f)); dst.a[0] = static_cast(std::clamp(src.a[0] * 16384.0f, -32768.0f, 32767.0f)); dst.a[1] = static_cast(std::clamp(src.a[1] * 16384.0f, -32768.0f, 32767.0f)); // Also store the native float version float_biquads[filter_idx].enabled = src.enable; float_biquads[filter_idx].numerator = src.b; float_biquads[filter_idx].denominator = src.a; } local_in.wave_buffer_count = vin->wave_buffer_count; local_in.wave_buffer_index = static_cast(vin->wave_buffer_index); local_in.src_data_address = static_cast(vin->src_data_address); local_in.src_data_size = vin->src_data_size; local_in.mix_id = static_cast(vin->mix_id); local_in.splitter_id = vin->splitter_id; local_in.wave_buffer_internal = vin->wavebuffers; local_in.channel_resource_ids = vin->channel_resource_ids; local_in.clear_voice_drop = vin->clear_voice_drop; local_in.flush_buffer_count = vin->flush_wave_buffer_count; local_in.flags = vin->flags; local_in.src_quality = vin->src_quality; } const auto& in_param = local_in; std::array voice_states{}; if (!in_param.in_use) { continue; } auto& voice_info{voice_context.GetInfo(in_param.id)}; for (u32 channel = 0; channel < in_param.channel_count; channel++) { voice_states[channel] = &voice_context.GetState(in_param.channel_resource_ids[channel]); } if (in_param.is_new) { voice_info.Initialize(); for (u32 channel = 0; channel < in_param.channel_count; channel++) { *voice_states[channel] = {}; } } BehaviorInfo::ErrorInfo update_error{}; voice_info.UpdateParameters(update_error, in_param, pool_mapper, behaviour); // For REV15+, store the native float biquad coefficients if (use_v2) { voice_info.use_float_biquads = true; voice_info.biquads_float = float_biquads; } else { voice_info.use_float_biquads = false; } if (!update_error.error_code.IsSuccess()) { behaviour.AppendError(update_error); } std::array, MaxWaveBuffers> wavebuffer_errors{}; voice_info.UpdateWaveBuffers(wavebuffer_errors, MaxWaveBuffers * 2, in_param, voice_states, pool_mapper, behaviour); for (auto& wavebuffer_error : wavebuffer_errors) { for (auto& error : wavebuffer_error) { if (error.error_code.IsError()) { behaviour.AppendError(error); } } } voice_info.WriteOutStatus(out_params[i], in_param, voice_states); new_voice_count += in_param.channel_count; } auto consumed_input_size{voice_count * in_stride}; auto consumed_output_size{voice_count * static_cast(sizeof(VoiceInfo::OutStatus))}; if (consumed_input_size != in_header->voices_size) { LOG_ERROR(Service_Audio, "Consumed an incorrect voices size, header size={}, consumed={}", in_header->voices_size, consumed_input_size); return Service::Audio::ResultInvalidUpdateInfo; } out_header->voices_size = consumed_output_size; out_header->size += consumed_output_size; input += consumed_input_size; output += consumed_output_size; voice_context.SetActiveCount(new_voice_count); return ResultSuccess; } Result InfoUpdater::UpdateEffects(EffectContext& effect_context, const bool renderer_active, std::span memory_pools, const u32 memory_pool_count) { if (behaviour.IsEffectInfoVersion2Supported()) { return UpdateEffectsVersion2(effect_context, renderer_active, memory_pools, memory_pool_count); } else { return UpdateEffectsVersion1(effect_context, renderer_active, memory_pools, memory_pool_count); } } Result InfoUpdater::UpdateEffectsVersion1(EffectContext& effect_context, const bool renderer_active, std::span memory_pools, const u32 memory_pool_count) { PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, behaviour.IsMemoryForceMappingEnabled()); const auto effect_count{effect_context.GetCount()}; std::span in_params{ reinterpret_cast(input), effect_count}; std::span out_params{ reinterpret_cast(output), effect_count}; for (u32 i = 0; i < effect_count; i++) { auto effect_info{&effect_context.GetInfo(i)}; if (effect_info->GetType() != in_params[i].type) { effect_info->ForceUnmapBuffers(pool_mapper); ResetEffect(effect_info, in_params[i].type); } BehaviorInfo::ErrorInfo error_info{}; effect_info->Update(error_info, in_params[i], pool_mapper); if (error_info.error_code.IsError()) { behaviour.AppendError(error_info); } effect_info->StoreStatus(out_params[i], renderer_active); } auto consumed_input_size{effect_count * static_cast(sizeof(EffectInfoBase::InParameterVersion1))}; auto consumed_output_size{effect_count * static_cast(sizeof(EffectInfoBase::OutStatusVersion1))}; if (consumed_input_size != in_header->effects_size) { LOG_ERROR(Service_Audio, "Consumed an incorrect effects size, header size={}, consumed={}", in_header->effects_size, consumed_input_size); return Service::Audio::ResultInvalidUpdateInfo; } out_header->effects_size = consumed_output_size; out_header->size += consumed_output_size; input += consumed_input_size; output += consumed_output_size; return ResultSuccess; } Result InfoUpdater::UpdateEffectsVersion2(EffectContext& effect_context, const bool renderer_active, std::span memory_pools, const u32 memory_pool_count) { PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, behaviour.IsMemoryForceMappingEnabled()); const auto effect_count{effect_context.GetCount()}; std::span in_params{ reinterpret_cast(input), effect_count}; std::span out_params{ reinterpret_cast(output), effect_count}; for (u32 i = 0; i < effect_count; i++) { auto effect_info{&effect_context.GetInfo(i)}; if (effect_info->GetType() != in_params[i].type) { effect_info->ForceUnmapBuffers(pool_mapper); ResetEffect(effect_info, in_params[i].type); } BehaviorInfo::ErrorInfo error_info{}; effect_info->Update(error_info, in_params[i], pool_mapper); if (error_info.error_code.IsError()) { behaviour.AppendError(error_info); } effect_info->StoreStatus(out_params[i], renderer_active); if (in_params[i].is_new) { effect_info->InitializeResultState(effect_context.GetDspSharedResultState(i)); effect_info->InitializeResultState(effect_context.GetResultState(i)); } effect_info->UpdateResultState(out_params[i].result_state, effect_context.GetResultState(i)); } auto consumed_input_size{effect_count * static_cast(sizeof(EffectInfoBase::InParameterVersion2))}; auto consumed_output_size{effect_count * static_cast(sizeof(EffectInfoBase::OutStatusVersion2))}; if (consumed_input_size != in_header->effects_size) { LOG_ERROR(Service_Audio, "Consumed an incorrect effects size, header size={}, consumed={}", in_header->effects_size, consumed_input_size); return Service::Audio::ResultInvalidUpdateInfo; } out_header->effects_size = consumed_output_size; out_header->size += consumed_output_size; input += consumed_input_size; output += consumed_output_size; return ResultSuccess; } Result InfoUpdater::UpdateMixes(MixContext& mix_context, const u32 mix_buffer_count, EffectContext& effect_context, SplitterContext& splitter_context) { s32 mix_count{0}; u32 consumed_input_size{0}; u32 input_mix_size{0}; if (behaviour.IsMixInParameterDirtyOnlyUpdateSupported()) { auto in_dirty_params{reinterpret_cast(input)}; mix_count = in_dirty_params->count; // Validate against expected header size to ensure structure is correct if (mix_count < 0 || mix_count > 0x100) { LOG_ERROR( Service_Audio, "Invalid mix count from dirty parameter: count={}, magic={:#x}, expected_size={}", mix_count, in_dirty_params->magic, in_header->mix_size); return Service::Audio::ResultInvalidUpdateInfo; } consumed_input_size += static_cast(sizeof(MixInfo::InDirtyParameter)); input += sizeof(MixInfo::InDirtyParameter); } else { mix_count = mix_context.GetCount(); } input_mix_size = static_cast(mix_count * sizeof(MixInfo::InParameter)); consumed_input_size += input_mix_size; if (mix_buffer_count == 0) { return Service::Audio::ResultInvalidUpdateInfo; } std::span in_params{ reinterpret_cast(input), static_cast(mix_count)}; u32 total_buffer_count{0}; for (s32 i = 0; i < mix_count; i++) { const auto& params{in_params[i]}; if (params.in_use) { total_buffer_count += params.buffer_count; if (params.dest_mix_id > static_cast(mix_context.GetCount()) && params.dest_mix_id != UnusedMixId && params.mix_id != FinalMixId) { return Service::Audio::ResultInvalidUpdateInfo; } } } if (total_buffer_count > mix_buffer_count) { return Service::Audio::ResultInvalidUpdateInfo; } bool mix_dirty{false}; for (s32 i = 0; i < mix_count; i++) { const auto& params{in_params[i]}; s32 mix_id{i}; if (behaviour.IsMixInParameterDirtyOnlyUpdateSupported()) { mix_id = params.mix_id; } auto mix_info{mix_context.GetInfo(mix_id)}; if (mix_info->in_use != params.in_use) { mix_info->in_use = params.in_use; if (!params.in_use) { mix_info->ClearEffectProcessingOrder(); } mix_dirty = true; } if (params.in_use) { mix_dirty |= mix_info->Update(mix_context.GetEdgeMatrix(), params, effect_context, splitter_context, behaviour); } } if (mix_dirty) { if (behaviour.IsSplitterSupported() && splitter_context.UsingSplitter()) { if (!mix_context.TSortInfo(splitter_context)) { return Service::Audio::ResultInvalidUpdateInfo; } } else { mix_context.SortInfo(); } } if (consumed_input_size != in_header->mix_size) { LOG_ERROR(Service_Audio, "Consumed an incorrect mixes size, header size={}, consumed={}", in_header->mix_size, consumed_input_size); return Service::Audio::ResultInvalidUpdateInfo; } input += mix_count * sizeof(MixInfo::InParameter); return ResultSuccess; } Result InfoUpdater::UpdateSinks(SinkContext& sink_context, std::span memory_pools, const u32 memory_pool_count) { PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, behaviour.IsMemoryForceMappingEnabled()); std::span in_params{ reinterpret_cast(input), memory_pool_count}; std::span out_params{ reinterpret_cast(output), memory_pool_count}; const auto sink_count{sink_context.GetCount()}; for (u32 i = 0; i < sink_count; i++) { const auto& params{in_params[i]}; auto sink_info{sink_context.GetInfo(i)}; if (sink_info->GetType() != params.type) { sink_info->CleanUp(); switch (params.type) { case SinkInfoBase::Type::Invalid: std::construct_at(reinterpret_cast(sink_info)); break; case SinkInfoBase::Type::DeviceSink: std::construct_at(reinterpret_cast(sink_info)); break; case SinkInfoBase::Type::CircularBufferSink: std::construct_at( reinterpret_cast(sink_info)); break; default: LOG_ERROR(Service_Audio, "Invalid sink type {}", static_cast(params.type)); break; } } BehaviorInfo::ErrorInfo error_info{}; sink_info->Update(error_info, out_params[i], params, pool_mapper); if (error_info.error_code.IsError()) { behaviour.AppendError(error_info); } } const auto consumed_input_size{sink_count * static_cast(sizeof(SinkInfoBase::InParameter))}; const auto consumed_output_size{sink_count * static_cast(sizeof(SinkInfoBase::OutStatus))}; if (consumed_input_size != in_header->sinks_size) { LOG_ERROR(Service_Audio, "Consumed an incorrect sinks size, header size={}, consumed={}", in_header->sinks_size, consumed_input_size); return Service::Audio::ResultInvalidUpdateInfo; } input += consumed_input_size; output += consumed_output_size; out_header->sinks_size = consumed_output_size; out_header->size += consumed_output_size; return ResultSuccess; } Result InfoUpdater::UpdateMemoryPools(std::span memory_pools, const u32 memory_pool_count) { PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count, behaviour.IsMemoryForceMappingEnabled()); std::span in_params{ reinterpret_cast(input), memory_pool_count}; std::span out_params{ reinterpret_cast(output), memory_pool_count}; for (size_t i = 0; i < memory_pool_count; i++) { auto state{pool_mapper.Update(memory_pools[i], in_params[i], out_params[i])}; if (state != MemoryPoolInfo::ResultState::Success && state != MemoryPoolInfo::ResultState::BadParam && state != MemoryPoolInfo::ResultState::MapFailed && state != MemoryPoolInfo::ResultState::InUse) { LOG_WARNING(Service_Audio, "Invalid ResultState from updating memory pools"); return Service::Audio::ResultInvalidUpdateInfo; } } const auto consumed_input_size{memory_pool_count * static_cast(sizeof(MemoryPoolInfo::InParameter))}; const auto consumed_output_size{memory_pool_count * static_cast(sizeof(MemoryPoolInfo::OutStatus))}; if (consumed_input_size != in_header->memory_pool_size) { LOG_ERROR(Service_Audio, "Consumed an incorrect memory pool size, header size={}, consumed={}", in_header->memory_pool_size, consumed_input_size); return Service::Audio::ResultInvalidUpdateInfo; } input += consumed_input_size; output += consumed_output_size; out_header->memory_pool_size = consumed_output_size; out_header->size += consumed_output_size; return ResultSuccess; } Result InfoUpdater::UpdatePerformanceBuffer(std::span performance_output, const u64 performance_output_size, PerformanceManager* performance_manager) { auto in_params{reinterpret_cast(input)}; auto out_params{reinterpret_cast(output)}; if (performance_manager != nullptr) { out_params->history_size = performance_manager->CopyHistories(performance_output.data(), performance_output_size); performance_manager->SetDetailTarget(in_params->target_node_id); } else { out_params->history_size = 0; } const auto consumed_input_size{static_cast(sizeof(PerformanceManager::InParameter))}; const auto consumed_output_size{static_cast(sizeof(PerformanceManager::OutStatus))}; if (consumed_input_size != in_header->performance_buffer_size) { LOG_ERROR(Service_Audio, "Consumed an incorrect performance size, header size={}, consumed={}", in_header->performance_buffer_size, consumed_input_size); return Service::Audio::ResultInvalidUpdateInfo; } input += consumed_input_size; output += consumed_output_size; out_header->performance_buffer_size = consumed_output_size; out_header->size += consumed_output_size; return ResultSuccess; } Result InfoUpdater::UpdateBehaviorInfo(BehaviorInfo& behaviour_) { const auto in_params{reinterpret_cast(input)}; if (!CheckValidRevision(in_params->revision)) { return Service::Audio::ResultInvalidUpdateInfo; } // Compare revision numbers: Switch 2 opens the renderer with 'REV?' but tags its updates // 'REv?' (lowercase v) for the same revision. if (GetRevisionNum(in_params->revision) != GetRevisionNum(behaviour_.GetUserRevision())) { LOG_ERROR(Service_Audio, "BehaviorInfo revision {:08X} does not match the renderer's {:08X}", in_params->revision, behaviour_.GetUserRevision()); return Service::Audio::ResultInvalidUpdateInfo; } behaviour_.ClearError(); behaviour_.UpdateFlags(in_params->flags); if (in_header->behaviour_size != sizeof(BehaviorInfo::InParameter)) { return Service::Audio::ResultInvalidUpdateInfo; } input += sizeof(BehaviorInfo::InParameter); return ResultSuccess; } Result InfoUpdater::UpdateErrorInfo(const BehaviorInfo& behaviour_) { auto out_params{reinterpret_cast(output)}; behaviour_.CopyErrorInfo(out_params->errors, out_params->error_count); const auto consumed_output_size{static_cast(sizeof(BehaviorInfo::OutStatus))}; output += consumed_output_size; out_header->behaviour_size = consumed_output_size; out_header->size += consumed_output_size; return ResultSuccess; } Result InfoUpdater::UpdateSplitterInfo(SplitterContext& splitter_context) { if (!splitter_context.Update(input)) { return Service::Audio::ResultInvalidUpdateInfo; } input += in_header->splitter_size; return ResultSuccess; } Result InfoUpdater::UpdateRendererInfo(const u64 elapsed_frames) { struct RenderInfo { /* 0x00 */ u64 frames_elapsed; /* 0x08 */ char unk08[0x8]; }; static_assert(sizeof(RenderInfo) == 0x10, "RenderInfo has the wrong size!"); auto out_params{reinterpret_cast(output)}; out_params->frames_elapsed = elapsed_frames; const auto consumed_output_size{static_cast(sizeof(RenderInfo))}; output += consumed_output_size; out_header->render_info_size = consumed_output_size; out_header->size += consumed_output_size; return ResultSuccess; } Result InfoUpdater::CheckConsumedSize() { if (CpuAddr(input) - CpuAddr(input_origin.data()) != expected_input_size) { return Service::Audio::ResultInvalidUpdateInfo; } else if (CpuAddr(output) - CpuAddr(output_origin.data()) != expected_output_size) { return Service::Audio::ResultInvalidUpdateInfo; } return ResultSuccess; } } // namespace AudioCore::Renderer