diff options
Diffstat (limited to '')
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | src/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/audio_core/CMakeLists.txt | 16 | ||||
-rw-r--r-- | src/audio_core/audio_core.cpp | 53 | ||||
-rw-r--r-- | src/audio_core/audio_core.h | 26 | ||||
-rw-r--r-- | src/audio_core/hle/dsp.cpp | 42 | ||||
-rw-r--r-- | src/audio_core/hle/dsp.h | 502 | ||||
-rw-r--r-- | src/audio_core/hle/pipe.cpp | 55 | ||||
-rw-r--r-- | src/audio_core/hle/pipe.h | 38 | ||||
-rw-r--r-- | src/audio_core/sink.h | 34 | ||||
-rw-r--r-- | src/citra/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/citra_qt/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/common/bit_field.h | 2 | ||||
-rw-r--r-- | src/common/logging/backend.cpp | 2 | ||||
-rw-r--r-- | src/common/logging/log.h | 2 | ||||
-rw-r--r-- | src/core/hle/kernel/memory.cpp | 5 | ||||
-rw-r--r-- | src/core/hle/service/dsp_dsp.cpp | 135 | ||||
-rw-r--r-- | src/core/hle/service/dsp_dsp.h | 12 | ||||
-rw-r--r-- | src/core/hw/gpu.cpp | 6 | ||||
-rw-r--r-- | src/core/system.cpp | 7 | ||||
-rw-r--r-- | src/video_core/command_processor.cpp | 24 |
21 files changed, 892 insertions, 76 deletions
@@ -9,6 +9,8 @@ Citra is an experimental open-source Nintendo 3DS emulator/debugger written in C Citra is licensed under the GPLv2 (or any later version). Refer to the license.txt file included. Please read the [FAQ](https://github.com/citra-emu/citra/wiki/FAQ) before getting started with the project. +Check out our [website](https://citra-emu.org/)! + For development discussion, please join us @ #citra on freenode. ### Development diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cb09f3cd1..2bb411492 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,6 +4,7 @@ include_directories(.) add_subdirectory(common) add_subdirectory(core) add_subdirectory(video_core) +add_subdirectory(audio_core) if (ENABLE_GLFW) add_subdirectory(citra) endif() diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt new file mode 100644 index 000000000..b0d1c7eb6 --- /dev/null +++ b/src/audio_core/CMakeLists.txt @@ -0,0 +1,16 @@ +set(SRCS + audio_core.cpp + hle/dsp.cpp + hle/pipe.cpp + ) + +set(HEADERS + audio_core.h + hle/dsp.h + hle/pipe.h + sink.h + ) + +create_directory_groups(${SRCS} ${HEADERS}) + +add_library(audio_core STATIC ${SRCS} ${HEADERS})
\ No newline at end of file diff --git a/src/audio_core/audio_core.cpp b/src/audio_core/audio_core.cpp new file mode 100644 index 000000000..894f46990 --- /dev/null +++ b/src/audio_core/audio_core.cpp @@ -0,0 +1,53 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "audio_core/audio_core.h" +#include "audio_core/hle/dsp.h" + +#include "core/core_timing.h" +#include "core/hle/kernel/vm_manager.h" +#include "core/hle/service/dsp_dsp.h" + +namespace AudioCore { + +// Audio Ticks occur about every 5 miliseconds. +static int tick_event; ///< CoreTiming event +static constexpr u64 audio_frame_ticks = 1310252ull; ///< Units: ARM11 cycles + +static void AudioTickCallback(u64 /*userdata*/, int cycles_late) { + if (DSP::HLE::Tick()) { + // HACK: We're not signaling the interrups when they should be, but just firing them all off together. + // It should be only (interrupt_id = 2, channel_id = 2) that's signalled here. + // TODO(merry): Understand when the other interrupts are fired. + DSP_DSP::SignalAllInterrupts(); + } + + // Reschedule recurrent event + CoreTiming::ScheduleEvent(audio_frame_ticks - cycles_late, tick_event); +} + +/// Initialise Audio +void Init() { + DSP::HLE::Init(); + + tick_event = CoreTiming::RegisterEvent("AudioCore::tick_event", AudioTickCallback); + CoreTiming::ScheduleEvent(audio_frame_ticks, tick_event); +} + +/// Add DSP address spaces to Process's address space. +void AddAddressSpace(Kernel::VMManager& address_space) { + auto r0_vma = address_space.MapBackingMemory(DSP::HLE::region0_base, reinterpret_cast<u8*>(&DSP::HLE::g_region0), sizeof(DSP::HLE::SharedMemory), Kernel::MemoryState::IO).MoveFrom(); + address_space.Reprotect(r0_vma, Kernel::VMAPermission::ReadWrite); + + auto r1_vma = address_space.MapBackingMemory(DSP::HLE::region1_base, reinterpret_cast<u8*>(&DSP::HLE::g_region1), sizeof(DSP::HLE::SharedMemory), Kernel::MemoryState::IO).MoveFrom(); + address_space.Reprotect(r1_vma, Kernel::VMAPermission::ReadWrite); +} + +/// Shutdown Audio +void Shutdown() { + CoreTiming::UnscheduleEvent(tick_event, 0); + DSP::HLE::Shutdown(); +} + +} //namespace diff --git a/src/audio_core/audio_core.h b/src/audio_core/audio_core.h new file mode 100644 index 000000000..64c330914 --- /dev/null +++ b/src/audio_core/audio_core.h @@ -0,0 +1,26 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +namespace Kernel { +class VMManager; +} + +namespace AudioCore { + +constexpr int num_sources = 24; +constexpr int samples_per_frame = 160; ///< Samples per audio frame at native sample rate +constexpr int native_sample_rate = 32728; ///< 32kHz + +/// Initialise Audio Core +void Init(); + +/// Add DSP address spaces to a Process. +void AddAddressSpace(Kernel::VMManager& vm_manager); + +/// Shutdown Audio Core +void Shutdown(); + +} // namespace diff --git a/src/audio_core/hle/dsp.cpp b/src/audio_core/hle/dsp.cpp new file mode 100644 index 000000000..c89356edc --- /dev/null +++ b/src/audio_core/hle/dsp.cpp @@ -0,0 +1,42 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "audio_core/hle/dsp.h" +#include "audio_core/hle/pipe.h" + +namespace DSP { +namespace HLE { + +SharedMemory g_region0; +SharedMemory g_region1; + +void Init() { + DSP::HLE::ResetPipes(); +} + +void Shutdown() { +} + +bool Tick() { + return true; +} + +SharedMemory& CurrentRegion() { + // The region with the higher frame counter is chosen unless there is wraparound. + + if (g_region0.frame_counter == 0xFFFFu && g_region1.frame_counter != 0xFFFEu) { + // Wraparound has occured. + return g_region1; + } + + if (g_region1.frame_counter == 0xFFFFu && g_region0.frame_counter != 0xFFFEu) { + // Wraparound has occured. + return g_region0; + } + + return (g_region0.frame_counter > g_region1.frame_counter) ? g_region0 : g_region1; +} + +} // namespace HLE +} // namespace DSP diff --git a/src/audio_core/hle/dsp.h b/src/audio_core/hle/dsp.h new file mode 100644 index 000000000..14c4000c6 --- /dev/null +++ b/src/audio_core/hle/dsp.h @@ -0,0 +1,502 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <cstddef> +#include <type_traits> + +#include "audio_core/audio_core.h" + +#include "common/bit_field.h" +#include "common/common_funcs.h" +#include "common/common_types.h" +#include "common/swap.h" + +namespace DSP { +namespace HLE { + +// The application-accessible region of DSP memory consists of two parts. +// Both are marked as IO and have Read/Write permissions. +// +// First Region: 0x1FF50000 (Size: 0x8000) +// Second Region: 0x1FF70000 (Size: 0x8000) +// +// The DSP reads from each region alternately based on the frame counter for each region much like a +// double-buffer. The frame counter is located as the very last u16 of each region and is incremented +// each audio tick. + +struct SharedMemory; + +constexpr VAddr region0_base = 0x1FF50000; +extern SharedMemory g_region0; + +constexpr VAddr region1_base = 0x1FF70000; +extern SharedMemory g_region1; + +/** + * The DSP is native 16-bit. The DSP also appears to be big-endian. When reading 32-bit numbers from + * its memory regions, the higher and lower 16-bit halves are swapped compared to the little-endian + * layout of the ARM11. Hence from the ARM11's point of view the memory space appears to be + * middle-endian. + * + * Unusually this does not appear to be an issue for floating point numbers. The DSP makes the more + * sensible choice of keeping that little-endian. There are also some exceptions such as the + * IntermediateMixSamples structure, which is little-endian. + * + * This struct implements the conversion to and from this middle-endianness. + */ +struct u32_dsp { + u32_dsp() = default; + operator u32() const { + return Convert(storage); + } + void operator=(u32 new_value) { + storage = Convert(new_value); + } +private: + static constexpr u32 Convert(u32 value) { + return (value << 16) | (value >> 16); + } + u32_le storage; +}; +#if (__GNUC__ >= 5) || defined(__clang__) || defined(_MSC_VER) +static_assert(std::is_trivially_copyable<u32_dsp>::value, "u32_dsp isn't trivially copyable"); +#endif + +// There are 15 structures in each memory region. A table of them in the order they appear in memory +// is presented below +// +// Pipe 2 # First Region DSP Address Purpose Control +// 5 0x8400 DSP Status DSP +// 9 0x8410 DSP Debug Info DSP +// 6 0x8540 Final Mix Samples DSP +// 2 0x8680 Source Status [24] DSP +// 8 0x8710 Compressor Table Application +// 4 0x9430 DSP Configuration Application +// 7 0x9492 Intermediate Mix Samples DSP + App +// 1 0x9E92 Source Configuration [24] Application +// 3 0xA792 Source ADPCM Coefficients [24] Application +// 10 0xA912 Surround Sound Related +// 11 0xAA12 Surround Sound Related +// 12 0xAAD2 Surround Sound Related +// 13 0xAC52 Surround Sound Related +// 14 0xAC5C Surround Sound Related +// 0 0xBFFF Frame Counter Application +// +// Note that the above addresses do vary slightly between audio firmwares observed; the addresses are +// not fixed in stone. The addresses above are only an examplar; they're what this implementation +// does and provides to applications. +// +// Application requests the DSP service to convert DSP addresses into ARM11 virtual addresses using the +// ConvertProcessAddressFromDspDram service call. Applications seem to derive the addresses for the +// second region via: +// second_region_dsp_addr = first_region_dsp_addr | 0x10000 +// +// Applications maintain most of its own audio state, the memory region is used mainly for +// communication and not storage of state. +// +// In the documentation below, filter and effect transfer functions are specified in the z domain. +// (If you are more familiar with the Laplace transform, z = exp(sT). The z domain is the digital +// frequency domain, just like how the s domain is the analog frequency domain.) + +#define INSERT_PADDING_DSPWORDS(num_words) INSERT_PADDING_BYTES(2 * (num_words)) + +// GCC versions < 5.0 do not implement std::is_trivially_copyable. +// Excluding MSVC because it has weird behaviour for std::is_trivially_copyable. +#if (__GNUC__ >= 5) || defined(__clang__) + #define ASSERT_DSP_STRUCT(name, size) \ + static_assert(std::is_standard_layout<name>::value, "DSP structure " #name " doesn't use standard layout"); \ + static_assert(std::is_trivially_copyable<name>::value, "DSP structure " #name " isn't trivially copyable"); \ + static_assert(sizeof(name) == (size), "Unexpected struct size for DSP structure " #name) +#else + #define ASSERT_DSP_STRUCT(name, size) \ + static_assert(std::is_standard_layout<name>::value, "DSP structure " #name " doesn't use standard layout"); \ + static_assert(sizeof(name) == (size), "Unexpected struct size for DSP structure " #name) +#endif + +struct SourceConfiguration { + struct Configuration { + /// These dirty flags are set by the application when it updates the fields in this struct. + /// The DSP clears these each audio frame. + union { + u32_le dirty_raw; + + BitField<2, 1, u32_le> adpcm_coefficients_dirty; + BitField<3, 1, u32_le> partial_embedded_buffer_dirty; ///< Tends to be set when a looped buffer is queued. + + BitField<16, 1, u32_le> enable_dirty; + BitField<17, 1, u32_le> interpolation_dirty; + BitField<18, 1, u32_le> rate_multiplier_dirty; + BitField<19, 1, u32_le> buffer_queue_dirty; + BitField<20, 1, u32_le> loop_related_dirty; + BitField<21, 1, u32_le> play_position_dirty; ///< Tends to also be set when embedded buffer is updated. + BitField<22, 1, u32_le> filters_enabled_dirty; + BitField<23, 1, u32_le> simple_filter_dirty; + BitField<24, 1, u32_le> biquad_filter_dirty; + BitField<25, 1, u32_le> gain_0_dirty; + BitField<26, 1, u32_le> gain_1_dirty; + BitField<27, 1, u32_le> gain_2_dirty; + BitField<28, 1, u32_le> sync_dirty; + BitField<29, 1, u32_le> reset_flag; + + BitField<31, 1, u32_le> embedded_buffer_dirty; + }; + + // Gain control + + /** + * Gain is between 0.0-1.0. This determines how much will this source appear on + * each of the 12 channels that feed into the intermediate mixers. + * Each of the three intermediate mixers is fed two left and two right channels. + */ + float_le gain[3][4]; + + // Interpolation + + /// Multiplier for sample rate. Resampling occurs with the selected interpolation method. + float_le rate_multiplier; + + enum class InterpolationMode : u8 { + None = 0, + Linear = 1, + Polyphase = 2 + }; + + InterpolationMode interpolation_mode; + INSERT_PADDING_BYTES(1); ///< Interpolation related + + // Filters + + /** + * This is the simplest normalized first-order digital recursive filter. + * The transfer function of this filter is: + * H(z) = b0 / (1 + a1 z^-1) + * Values are signed fixed point with 15 fractional bits. + */ + struct SimpleFilter { + s16_le b0; + s16_le a1; + }; + + /** + * This is a normalised biquad filter (second-order). + * The transfer function of this filter is: + * H(z) = (b0 + b1 z^-1 + b2 z^-2) / (1 - a1 z^-1 - a2 z^-2) + * Nintendo chose to negate the feedbackward coefficients. This differs from standard notation + * as in: https://ccrma.stanford.edu/~jos/filters/Direct_Form_I.html + * Values are signed fixed point with 14 fractional bits. + */ + struct BiquadFilter { + s16_le b0; + s16_le b1; + s16_le b2; + s16_le a1; + s16_le a2; + }; + + union { + u16_le filters_enabled; + BitField<0, 1, u16_le> simple_filter_enabled; + BitField<1, 1, u16_le> biquad_filter_enabled; + }; + + SimpleFilter simple_filter; + BiquadFilter biquad_filter; + + // Buffer Queue + + /// A buffer of audio data from the application, along with metadata about it. + struct Buffer { + /// Physical memory address of the start of the buffer + u32_dsp physical_address; + + /// This is length in terms of samples. + /// Note that in different buffer formats a sample takes up different number of bytes. + u32_dsp length; + + /// ADPCM Predictor (4 bits) and Scale (4 bits) + union { + u16_le adpcm_ps; + BitField<0, 4, u16_le> adpcm_scale; + BitField<4, 4, u16_le> adpcm_predictor; + }; + + /// ADPCM Historical Samples (y[n-1] and y[n-2]) + u16_le adpcm_yn[2]; + + /// This is non-zero when the ADPCM values above are to be updated. + u8 adpcm_dirty; + + /// Is a looping buffer. + u8 is_looping; + + /// This value is shown in SourceStatus::previous_buffer_id when this buffer has finished. + /// This allows the emulated application to tell what buffer is currently playing + u16_le buffer_id; + + INSERT_PADDING_DSPWORDS(1); + }; + + u16_le buffers_dirty; ///< Bitmap indicating which buffers are dirty (bit i -> buffers[i]) + Buffer buffers[4]; ///< Queued Buffers + + // Playback controls + + u32_dsp loop_related; + u8 enable; + INSERT_PADDING_BYTES(1); + u16_le sync; ///< Application-side sync (See also: SourceStatus::sync) + u32_dsp play_position; ///< Position. (Units: number of samples) + INSERT_PADDING_DSPWORDS(2); + + // Embedded Buffer + // This buffer is often the first buffer to be used when initiating audio playback, + // after which the buffer queue is used. + + u32_dsp physical_address; + + /// This is length in terms of samples. + /// Note a sample takes up different number of bytes in different buffer formats. + u32_dsp length; + + enum class MonoOrStereo : u16_le { + Mono = 1, + Stereo = 2 + }; + + enum class Format : u16_le { + PCM8 = 0, + PCM16 = 1, + ADPCM = 2 + }; + + union { + u16_le flags1_raw; + BitField<0, 2, MonoOrStereo> mono_or_stereo; + BitField<2, 2, Format> format; + BitField<5, 1, u16_le> fade_in; + }; + + /// ADPCM Predictor (4 bit) and Scale (4 bit) + union { + u16_le adpcm_ps; + BitField<0, 4, u16_le> adpcm_scale; + BitField<4, 4, u16_le> adpcm_predictor; + }; + + /// ADPCM Historical Samples (y[n-1] and y[n-2]) + u16_le adpcm_yn[2]; + + union { + u16_le flags2_raw; + BitField<0, 1, u16_le> adpcm_dirty; ///< Has the ADPCM info above been changed? + BitField<1, 1, u16_le> is_looping; ///< Is this a looping buffer? + }; + + /// Buffer id of embedded buffer (used as a buffer id in SourceStatus to reference this buffer). + u16_le buffer_id; + }; + + Configuration config[AudioCore::num_sources]; +}; +ASSERT_DSP_STRUCT(SourceConfiguration::Configuration, 192); +ASSERT_DSP_STRUCT(SourceConfiguration::Configuration::Buffer, 20); + +struct SourceStatus { + struct Status { + u8 is_enabled; ///< Is this channel enabled? (Doesn't have to be playing anything.) + u8 previous_buffer_id_dirty; ///< Non-zero when previous_buffer_id changes + u16_le sync; ///< Is set by the DSP to the value of SourceConfiguration::sync + u32_dsp buffer_position; ///< Number of samples into the current buffer + u16_le previous_buffer_id; ///< Updated when a buffer finishes playing + INSERT_PADDING_DSPWORDS(1); + }; + + Status status[AudioCore::num_sources]; +}; +ASSERT_DSP_STRUCT(SourceStatus::Status, 12); + +struct DspConfiguration { + /// These dirty flags are set by the application when it updates the fields in this struct. + /// The DSP clears these each audio frame. + union { + u32_le dirty_raw; + + BitField<8, 1, u32_le> mixer1_enabled_dirty; + BitField<9, 1, u32_le> mixer2_enabled_dirty; + BitField<10, 1, u32_le> delay_effect_0_dirty; + BitField<11, 1, u32_le> delay_effect_1_dirty; + BitField<12, 1, u32_le> reverb_effect_0_dirty; + BitField<13, 1, u32_le> reverb_effect_1_dirty; + + BitField<16, 1, u32_le> volume_0_dirty; + + BitField<24, 1, u32_le> volume_1_dirty; + BitField<25, 1, u32_le> volume_2_dirty; + BitField<26, 1, u32_le> output_format_dirty; + BitField<27, 1, u32_le> limiter_enabled_dirty; + BitField<28, 1, u32_le> headphones_connected_dirty; + }; + + /// The DSP has three intermediate audio mixers. This controls the volume level (0.0-1.0) for each at the final mixer + float_le volume[3]; + + INSERT_PADDING_DSPWORDS(3); + + enum class OutputFormat : u16_le { + Mono = 0, + Stereo = 1, + Surround = 2 + }; + + OutputFormat output_format; + + u16_le limiter_enabled; ///< Not sure of the exact gain equation for the limiter. + u16_le headphones_connected; ///< Application updates the DSP on headphone status. + INSERT_PADDING_DSPWORDS(4); ///< TODO: Surround sound related + INSERT_PADDING_DSPWORDS(2); ///< TODO: Intermediate mixer 1/2 related + u16_le mixer1_enabled; + u16_le mixer2_enabled; + + /** + * This is delay with feedback. + * Transfer function: + * H(z) = a z^-N / (1 - b z^-1 + a g z^-N) + * where + * N = frame_count * samples_per_frame + * g, a and b are fixed point with 7 fractional bits + */ + struct DelayEffect { + /// These dirty flags are set by the application when it updates the fields in this struct. + /// The DSP clears these each audio frame. + union { + u16_le dirty_raw; + BitField<0, 1, u16_le> enable_dirty; + BitField<1, 1, u16_le> work_buffer_address_dirty; + BitField<2, 1, u16_le> other_dirty; ///< Set when anything else has been changed + }; + + u16_le enable; + INSERT_PADDING_DSPWORDS(1); + u16_le outputs; + u32_dsp work_buffer_address; ///< The application allocates a block of memory for the DSP to use as a work buffer. + u16_le frame_count; ///< Frames to delay by + + // Coefficients + s16_le g; ///< Fixed point with 7 fractional bits + s16_le a; ///< Fixed point with 7 fractional bits + s16_le b; ///< Fixed point with 7 fractional bits + }; + + DelayEffect delay_effect[2]; + + struct ReverbEffect { + INSERT_PADDING_DSPWORDS(26); ///< TODO + }; + + ReverbEffect reverb_effect[2]; + + INSERT_PADDING_DSPWORDS(4); +}; +ASSERT_DSP_STRUCT(DspConfiguration, 196); +ASSERT_DSP_STRUCT(DspConfiguration::DelayEffect, 20); +ASSERT_DSP_STRUCT(DspConfiguration::ReverbEffect, 52); + +struct AdpcmCoefficients { + /// Coefficients are signed fixed point with 11 fractional bits. + /// Each source has 16 coefficients associated with it. + s16_le coeff[AudioCore::num_sources][16]; +}; +ASSERT_DSP_STRUCT(AdpcmCoefficients, 768); + +struct DspStatus { + u16_le unknown; + u16_le dropped_frames; + INSERT_PADDING_DSPWORDS(0xE); +}; +ASSERT_DSP_STRUCT(DspStatus, 32); + +/// Final mixed output in PCM16 stereo format, what you hear out of the speakers. +/// When the application writes to this region it has no effect. +struct FinalMixSamples { + s16_le pcm16[2 * AudioCore::samples_per_frame]; +}; +ASSERT_DSP_STRUCT(FinalMixSamples, 640); + +/// DSP writes output of intermediate mixers 1 and 2 here. +/// Writes to this region by the application edits the output of the intermediate mixers. +/// This seems to be intended to allow the application to do custom effects on the ARM11. +/// Values that exceed s16 range will be clipped by the DSP after further processing. +struct IntermediateMixSamples { + struct Samples { + s32_le pcm32[4][AudioCore::samples_per_frame]; ///< Little-endian as opposed to DSP middle-endian. + }; + + Samples mix1; + Samples mix2; +}; +ASSERT_DSP_STRUCT(IntermediateMixSamples, 5120); + +/// Compressor table +struct Compressor { + INSERT_PADDING_DSPWORDS(0xD20); ///< TODO +}; + +/// There is no easy way to implement this in a HLE implementation. +struct DspDebug { + INSERT_PADDING_DSPWORDS(0x130); +}; +ASSERT_DSP_STRUCT(DspDebug, 0x260); + +struct SharedMemory { + /// Padding + INSERT_PADDING_DSPWORDS(0x400); + + DspStatus dsp_status; + + DspDebug dsp_debug; + + FinalMixSamples final_samples; + + SourceStatus source_statuses; + + Compressor compressor; + + DspConfiguration dsp_configuration; + + IntermediateMixSamples intermediate_mix_samples; + + SourceConfiguration source_configurations; + + AdpcmCoefficients adpcm_coefficients; + + /// Unknown 10-14 (Surround sound related) + INSERT_PADDING_DSPWORDS(0x16ED); + + u16_le frame_counter; +}; +ASSERT_DSP_STRUCT(SharedMemory, 0x8000); + +#undef INSERT_PADDING_DSPWORDS +#undef ASSERT_DSP_STRUCT + +/// Initialize DSP hardware +void Init(); + +/// Shutdown DSP hardware +void Shutdown(); + +/** + * Perform processing and updates state of current shared memory buffer. + * This function is called every audio tick before triggering the audio interrupt. + * @return Whether an audio interrupt should be triggered this frame. + */ +bool Tick(); + +/// Returns a mutable reference to the current region. Current region is selected based on the frame counter. +SharedMemory& CurrentRegion(); + +} // namespace HLE +} // namespace DSP diff --git a/src/audio_core/hle/pipe.cpp b/src/audio_core/hle/pipe.cpp new file mode 100644 index 000000000..6542c760c --- /dev/null +++ b/src/audio_core/hle/pipe.cpp @@ -0,0 +1,55 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <array> +#include <vector> + +#include "audio_core/hle/pipe.h" + +#include "common/common_types.h" +#include "common/logging/log.h" + +namespace DSP { +namespace HLE { + +static size_t pipe2position = 0; + +void ResetPipes() { + pipe2position = 0; +} + +std::vector<u8> PipeRead(u32 pipe_number, u32 length) { + if (pipe_number != 2) { + LOG_WARNING(Audio_DSP, "pipe_number = %u (!= 2), unimplemented", pipe_number); + return {}; // We currently don't handle anything other than the audio pipe. + } + + // Canned DSP responses that games expect. These were taken from HW by 3dmoo team. + // TODO: Our implementation will actually use a slightly different response than this one. + // TODO: Use offsetof on DSP structures instead for a proper response. + static const std::array<u8, 32> canned_response {{ + 0x0F, 0x00, 0xFF, 0xBF, 0x8E, 0x9E, 0x80, 0x86, 0x8E, 0xA7, 0x30, 0x94, 0x00, 0x84, 0x40, 0x85, + 0x8E, 0x94, 0x10, 0x87, 0x10, 0x84, 0x0E, 0xA9, 0x0E, 0xAA, 0xCE, 0xAA, 0x4E, 0xAC, 0x58, 0xAC + }}; + + // TODO: Move this into dsp::DSP service since it happens on the service side. + // Hardware observation: No data is returned if requested length reads beyond the end of the data in-pipe. + if (pipe2position + length > canned_response.size()) { + return {}; + } + + std::vector<u8> ret; + for (size_t i = 0; i < length; i++, pipe2position++) { + ret.emplace_back(canned_response[pipe2position]); + } + + return ret; +} + +void PipeWrite(u32 pipe_number, const std::vector<u8>& buffer) { + // TODO: proper pipe behaviour +} + +} // namespace HLE +} // namespace DSP diff --git a/src/audio_core/hle/pipe.h b/src/audio_core/hle/pipe.h new file mode 100644 index 000000000..ff6536950 --- /dev/null +++ b/src/audio_core/hle/pipe.h @@ -0,0 +1,38 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <vector> + +#include "common/common_types.h" + +namespace DSP { +namespace HLE { + +/// Reset the pipes by setting pipe positions back to the beginning. +void ResetPipes(); + +/** + * Read a DSP pipe. + * Pipe IDs: + * pipe_number = 0: Debug + * pipe_number = 1: P-DMA + * pipe_number = 2: Audio + * pipe_number = 3: Binary + * @param pipe_number The Pipe ID + * @param length How much data to request. + * @return The data read from the pipe. The size of this vector can be less than the length requested. + */ +std::vector<u8> PipeRead(u32 pipe_number, u32 length); + +/** + * Write to a DSP pipe. + * @param pipe_number The Pipe ID + * @param buffer The data to write to the pipe. + */ +void PipeWrite(u32 pipe_number, const std::vector<u8>& buffer); + +} // namespace HLE +} // namespace DSP diff --git a/src/audio_core/sink.h b/src/audio_core/sink.h new file mode 100644 index 000000000..cad21a85e --- /dev/null +++ b/src/audio_core/sink.h @@ -0,0 +1,34 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <vector> + +#include "common/common_types.h" + +namespace AudioCore { + +/** + * This class is an interface for an audio sink. An audio sink accepts samples in stereo signed PCM16 format to be output. + * Sinks *do not* handle resampling and expect the correct sample rate. They are dumb outputs. + */ +class Sink { +public: + virtual ~Sink() = default; + + /// The native rate of this sink. The sink expects to be fed samples that respect this. (Units: samples/sec) + virtual unsigned GetNativeSampleRate() const = 0; + + /** + * Feed stereo samples to sink. + * @param samples Samples in interleaved stereo PCM16 format. Size of vector must be multiple of two. + */ + virtual void EnqueueSamples(const std::vector<s16>& samples) = 0; + + /// Samples enqueued that have not been played yet. + virtual std::size_t SamplesInQueue() const = 0; +}; + +} // namespace diff --git a/src/citra/CMakeLists.txt b/src/citra/CMakeLists.txt index e7f8a17f9..b9abb818e 100644 --- a/src/citra/CMakeLists.txt +++ b/src/citra/CMakeLists.txt @@ -17,7 +17,7 @@ include_directories(${GLFW_INCLUDE_DIRS}) link_directories(${GLFW_LIBRARY_DIRS}) add_executable(citra ${SRCS} ${HEADERS}) -target_link_libraries(citra core video_core common) +target_link_libraries(citra core video_core audio_core common) target_link_libraries(citra ${GLFW_LIBRARIES} ${OPENGL_gl_LIBRARY} inih glad) if (MSVC) target_link_libraries(citra getopt) diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index bbf6ae001..b3d1205a4 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -79,7 +79,7 @@ if (APPLE) else() add_executable(citra-qt ${SRCS} ${HEADERS} ${UI_HDRS}) endif() -target_link_libraries(citra-qt core video_core common qhexedit) +target_link_libraries(citra-qt core video_core audio_core common qhexedit) target_link_libraries(citra-qt ${OPENGL_gl_LIBRARY} ${CITRA_QT_LIBS}) target_link_libraries(citra-qt ${PLATFORM_LIBRARIES}) diff --git a/src/common/bit_field.h b/src/common/bit_field.h index 600e0c70c..371eb17a1 100644 --- a/src/common/bit_field.h +++ b/src/common/bit_field.h @@ -185,6 +185,6 @@ private: }; #pragma pack() -#if (__GNUC__ >= 5) || defined __clang__ || defined _MSC_VER +#if (__GNUC__ >= 5) || defined(__clang__) || defined(_MSC_VER) static_assert(std::is_trivially_copyable<BitField<0, 1, u32>>::value, "BitField must be trivially copyable"); #endif diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index d186ba8f8..58819012d 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -58,6 +58,8 @@ namespace Log { CLS(Render) \ SUB(Render, Software) \ SUB(Render, OpenGL) \ + CLS(Audio) \ + SUB(Audio, DSP) \ CLS(Loader) // GetClassName is a macro defined by Windows.h, grrr... diff --git a/src/common/logging/log.h b/src/common/logging/log.h index 2d9323a7b..ec7bb00b8 100644 --- a/src/common/logging/log.h +++ b/src/common/logging/log.h @@ -73,6 +73,8 @@ enum class Class : ClassType { Render, ///< Emulator video output and hardware acceleration Render_Software, ///< Software renderer backend Render_OpenGL, ///< OpenGL backend + Audio, ///< Emulator audio output + Audio_DSP, ///< The HLE implementation of the DSP Loader, ///< ROM loader Count ///< Total number of logging classes diff --git a/src/core/hle/kernel/memory.cpp b/src/core/hle/kernel/memory.cpp index 0cfb43fc7..862643448 100644 --- a/src/core/hle/kernel/memory.cpp +++ b/src/core/hle/kernel/memory.cpp @@ -7,6 +7,8 @@ #include <utility> #include <vector> +#include "audio_core/audio_core.h" + #include "common/common_types.h" #include "common/logging/log.h" @@ -107,7 +109,6 @@ struct MemoryArea { static MemoryArea memory_areas[] = { {SHARED_MEMORY_VADDR, SHARED_MEMORY_SIZE, "Shared Memory"}, // Shared memory {VRAM_VADDR, VRAM_SIZE, "VRAM"}, // Video memory (VRAM) - {DSP_RAM_VADDR, DSP_RAM_SIZE, "DSP RAM"}, // DSP memory {TLS_AREA_VADDR, TLS_AREA_SIZE, "TLS Area"}, // TLS memory }; @@ -133,6 +134,8 @@ void InitLegacyAddressSpace(Kernel::VMManager& address_space) { auto shared_page_vma = address_space.MapBackingMemory(SHARED_PAGE_VADDR, (u8*)&SharedPage::shared_page, SHARED_PAGE_SIZE, MemoryState::Shared).MoveFrom(); address_space.Reprotect(shared_page_vma, VMAPermission::Read); + + AudioCore::AddAddressSpace(address_space); } } // namespace diff --git a/src/core/hle/service/dsp_dsp.cpp b/src/core/hle/service/dsp_dsp.cpp index f9f931f6d..15d3274ec 100644 --- a/src/core/hle/service/dsp_dsp.cpp +++ b/src/core/hle/service/dsp_dsp.cpp @@ -2,6 +2,8 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include "audio_core/hle/pipe.h" + #include "common/logging/log.h" #include "core/hle/kernel/event.h" @@ -14,17 +16,30 @@ namespace DSP_DSP { static u32 read_pipe_count; static Kernel::SharedPtr<Kernel::Event> semaphore_event; -static Kernel::SharedPtr<Kernel::Event> interrupt_event; -void SignalInterrupt() { - // TODO(bunnei): This is just a stub, it does not do anything other than signal to the emulated - // application that a DSP interrupt occurred, without specifying which one. Since we do not - // emulate the DSP yet (and how it works is largely unknown), this is a work around to get games - // that check the DSP interrupt signal event to run. We should figure out the different types of - // DSP interrupts, and trigger them at the appropriate times. +struct PairHash { + template <typename T, typename U> + std::size_t operator()(const std::pair<T, U> &x) const { + // TODO(yuriks): Replace with better hash combining function. + return std::hash<T>()(x.first) ^ std::hash<U>()(x.second); + } +}; + +/// Map of (audio interrupt number, channel number) to Kernel::Events. See: RegisterInterruptEvents +static std::unordered_map<std::pair<u32, u32>, Kernel::SharedPtr<Kernel::Event>, PairHash> interrupt_events; + +// DSP Interrupts: +// Interrupt #2 occurs every frame tick. Userland programs normally have a thread that's waiting +// for an interrupt event. Immediately after this interrupt event, userland normally updates the +// state in the next region and increments the relevant frame counter by two. +void SignalAllInterrupts() { + // HACK: The other interrupts have currently unknown purpose, we trigger them each tick in any case. + for (auto& interrupt_event : interrupt_events) + interrupt_event.second->Signal(); +} - if (interrupt_event != 0) - interrupt_event->Signal(); +void SignalInterrupt(u32 interrupt, u32 channel) { + interrupt_events[std::make_pair(interrupt, channel)]->Signal(); } /** @@ -43,7 +58,7 @@ static void ConvertProcessAddressFromDspDram(Service::Interface* self) { cmd_buff[1] = 0; // No error cmd_buff[2] = (addr << 1) + (Memory::DSP_RAM_VADDR + 0x40000); - LOG_WARNING(Service_DSP, "(STUBBED) called with address 0x%08X", addr); + LOG_TRACE(Service_DSP, "addr=0x%08X", addr); } /** @@ -121,8 +136,8 @@ static void FlushDataCache(Service::Interface* self) { /** * DSP_DSP::RegisterInterruptEvents service function * Inputs: - * 1 : Parameter 0 (purpose unknown) - * 2 : Parameter 1 (purpose unknown) + * 1 : Interrupt Number + * 2 : Channel Number * 4 : Interrupt event handle * Outputs: * 1 : Result of function, 0 on success, otherwise error code @@ -130,22 +145,24 @@ static void FlushDataCache(Service::Interface* self) { static void RegisterInterruptEvents(Service::Interface* self) { u32* cmd_buff = Kernel::GetCommandBuffer(); - u32 param0 = cmd_buff[1]; - u32 param1 = cmd_buff[2]; + u32 interrupt = cmd_buff[1]; + u32 channel = cmd_buff[2]; u32 event_handle = cmd_buff[4]; - auto evt = Kernel::g_handle_table.Get<Kernel::Event>(cmd_buff[4]); - if (evt != nullptr) { - interrupt_event = evt; - cmd_buff[1] = 0; // No error + if (event_handle) { + auto evt = Kernel::g_handle_table.Get<Kernel::Event>(cmd_buff[4]); + if (evt) { + interrupt_events[std::make_pair(interrupt, channel)] = evt; + cmd_buff[1] = RESULT_SUCCESS.raw; + LOG_WARNING(Service_DSP, "Registered interrupt=%u, channel=%u, event_handle=0x%08X", interrupt, channel, event_handle); + } else { + cmd_buff[1] = -1; + LOG_ERROR(Service_DSP, "Invalid event handle! interrupt=%u, channel=%u, event_handle=0x%08X", interrupt, channel, event_handle); + } } else { - LOG_ERROR(Service_DSP, "called with invalid handle=%08X", cmd_buff[4]); - - // TODO(yuriks): An error should be returned from SendSyncRequest, not in the cmdbuf - cmd_buff[1] = -1; + interrupt_events.erase(std::make_pair(interrupt, channel)); + LOG_WARNING(Service_DSP, "Unregistered interrupt=%u, channel=%u, event_handle=0x%08X", interrupt, channel, event_handle); } - - LOG_WARNING(Service_DSP, "(STUBBED) called param0=%u, param1=%u, event_handle=0x%08X", param0, param1, event_handle); } /** @@ -158,8 +175,6 @@ static void RegisterInterruptEvents(Service::Interface* self) { static void SetSemaphore(Service::Interface* self) { u32* cmd_buff = Kernel::GetCommandBuffer(); - SignalInterrupt(); - cmd_buff[1] = 0; // No error LOG_WARNING(Service_DSP, "(STUBBED) called"); @@ -168,9 +183,9 @@ static void SetSemaphore(Service::Interface* self) { /** * DSP_DSP::WriteProcessPipe service function * Inputs: - * 1 : Number + * 1 : Channel * 2 : Size - * 3 : (size <<14) | 0x402 + * 3 : (size << 14) | 0x402 * 4 : Buffer * Outputs: * 0 : Return header @@ -179,21 +194,42 @@ static void SetSemaphore(Service::Interface* self) { static void WriteProcessPipe(Service::Interface* self) { u32* cmd_buff = Kernel::GetCommandBuffer(); - u32 number = cmd_buff[1]; + u32 channel = cmd_buff[1]; u32 size = cmd_buff[2]; - u32 new_size = cmd_buff[3]; u32 buffer = cmd_buff[4]; + if (IPC::StaticBufferDesc(size, 1) != cmd_buff[3]) { + LOG_ERROR(Service_DSP, "IPC static buffer descriptor failed validation (0x%X). channel=%u, size=0x%X, buffer=0x%08X", cmd_buff[3], channel, size, buffer); + cmd_buff[1] = -1; // TODO + return; + } + + if (!Memory::GetPointer(buffer)) { + LOG_ERROR(Service_DSP, "Invalid Buffer: channel=%u, size=0x%X, buffer=0x%08X", channel, size, buffer); + cmd_buff[1] = -1; // TODO + return; + } + + std::vector<u8> message(size); + + for (size_t i = 0; i < size; i++) { + message[i] = Memory::Read8(buffer + i); + } + + DSP::HLE::PipeWrite(channel, message); + cmd_buff[1] = RESULT_SUCCESS.raw; // No error - LOG_WARNING(Service_DSP, "(STUBBED) called number=%u, size=0x%X, new_size=0x%X, buffer=0x%08X", - number, size, new_size, buffer); + LOG_TRACE(Service_DSP, "channel=%u, size=0x%X, buffer=0x%08X", channel, size, buffer); } /** * DSP_DSP::ReadPipeIfPossible service function + * A pipe is a means of communication between the ARM11 and DSP that occurs on + * hardware by writing to/reading from the DSP registers at 0x10203000. + * Pipes are used for initialisation. See also DSP::HLE::PipeRead. * Inputs: - * 1 : Unknown + * 1 : Pipe Number * 2 : Unknown * 3 : Size in bytes of read (observed only lower half word used) * 0x41 : Virtual address to read from DSP pipe to in memory @@ -204,35 +240,25 @@ static void WriteProcessPipe(Service::Interface* self) { static void ReadPipeIfPossible(Service::Interface* self) { u32* cmd_buff = Kernel::GetCommandBuffer(); - u32 unk1 = cmd_buff[1]; + u32 pipe = cmd_buff[1]; u32 unk2 = cmd_buff[2]; u32 size = cmd_buff[3] & 0xFFFF;// Lower 16 bits are size VAddr addr = cmd_buff[0x41]; - // Canned DSP responses that games expect. These were taken from HW by 3dmoo team. - // TODO: Remove this hack :) - static const std::array<u16, 16> canned_read_pipe = {{ - 0x000F, 0xBFFF, 0x9E8E, 0x8680, 0xA78E, 0x9430, 0x8400, 0x8540, - 0x948E, 0x8710, 0x8410, 0xA90E, 0xAA0E, 0xAACE, 0xAC4E, 0xAC58 - }}; + if (!Memory::GetPointer(addr)) { + LOG_ERROR(Service_DSP, "Invalid addr: pipe=0x%08X, unk2=0x%08X, size=0x%X, buffer=0x%08X", pipe, unk2, size, addr); + cmd_buff[1] = -1; // TODO + return; + } - u32 initial_size = read_pipe_count; + std::vector<u8> response = DSP::HLE::PipeRead(pipe, size); - for (unsigned offset = 0; offset < size; offset += sizeof(u16)) { - if (read_pipe_count < canned_read_pipe.size()) { - Memory::Write16(addr + offset, canned_read_pipe[read_pipe_count]); - read_pipe_count++; - } else { - LOG_ERROR(Service_DSP, "canned read pipe log exceeded!"); - break; - } - } + Memory::WriteBlock(addr, response.data(), response.size()); cmd_buff[1] = 0; // No error - cmd_buff[2] = (read_pipe_count - initial_size) * sizeof(u16); + cmd_buff[2] = (u32)response.size(); - LOG_WARNING(Service_DSP, "(STUBBED) called unk1=0x%08X, unk2=0x%08X, size=0x%X, buffer=0x%08X", - unk1, unk2, size, addr); + LOG_TRACE(Service_DSP, "pipe=0x%08X, unk2=0x%08X, size=0x%X, buffer=0x%08X", pipe, unk2, size, addr); } /** @@ -311,7 +337,6 @@ const Interface::FunctionInfo FunctionTable[] = { Interface::Interface() { semaphore_event = Kernel::Event::Create(RESETTYPE_ONESHOT, "DSP_DSP::semaphore_event"); - interrupt_event = nullptr; read_pipe_count = 0; Register(FunctionTable); @@ -319,7 +344,7 @@ Interface::Interface() { Interface::~Interface() { semaphore_event = nullptr; - interrupt_event = nullptr; + interrupt_events.clear(); } } // namespace diff --git a/src/core/hle/service/dsp_dsp.h b/src/core/hle/service/dsp_dsp.h index b6f611db5..32b89e9bb 100644 --- a/src/core/hle/service/dsp_dsp.h +++ b/src/core/hle/service/dsp_dsp.h @@ -23,7 +23,15 @@ public: } }; -/// Signals that a DSP interrupt has occurred to userland code -void SignalInterrupt(); +/// Signal all audio related interrupts. +void SignalAllInterrupts(); + +/** + * Signal a specific audio related interrupt based on interrupt id and channel id. + * @param interrupt_id The interrupt id + * @param channel_id The channel id + * The significance of various values of interrupt_id and channel_id is not yet known. + */ +void SignalInterrupt(u32 interrupt_id, u32 channel_id); } // namespace diff --git a/src/core/hw/gpu.cpp b/src/core/hw/gpu.cpp index c60310586..5312baa83 100644 --- a/src/core/hw/gpu.cpp +++ b/src/core/hw/gpu.cpp @@ -17,7 +17,6 @@ #include "core/core_timing.h" #include "core/hle/service/gsp_gpu.h" -#include "core/hle/service/dsp_dsp.h" #include "core/hle/service/hid/hid.h" #include "core/hw/hw.h" @@ -414,11 +413,6 @@ static void VBlankCallback(u64 userdata, int cycles_late) { GSP_GPU::SignalInterrupt(GSP_GPU::InterruptId::PDC0); GSP_GPU::SignalInterrupt(GSP_GPU::InterruptId::PDC1); - // TODO(bunnei): Fake a DSP interrupt on each frame. This does not belong here, but - // until we can emulate DSP interrupts, this is probably the only reasonable place to do - // this. Certain games expect this to be periodically signaled. - DSP_DSP::SignalInterrupt(); - // Check for user input updates Service::HID::Update(); diff --git a/src/core/system.cpp b/src/core/system.cpp index 7e9c56538..b62ebf69e 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -2,9 +2,12 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include "audio_core/audio_core.h" + #include "core/core.h" #include "core/core_timing.h" #include "core/system.h" +#include "core/gdbstub/gdbstub.h" #include "core/hw/hw.h" #include "core/hle/hle.h" #include "core/hle/kernel/kernel.h" @@ -12,8 +15,6 @@ #include "video_core/video_core.h" -#include "core/gdbstub/gdbstub.h" - namespace System { void Init(EmuWindow* emu_window) { @@ -24,11 +25,13 @@ void Init(EmuWindow* emu_window) { Kernel::Init(); HLE::Init(); VideoCore::Init(emu_window); + AudioCore::Init(); GDBStub::Init(); } void Shutdown() { GDBStub::Shutdown(); + AudioCore::Shutdown(); VideoCore::Shutdown(); HLE::Shutdown(); Kernel::Shutdown(); diff --git a/src/video_core/command_processor.cpp b/src/video_core/command_processor.cpp index ed20057b5..73fdfbe9c 100644 --- a/src/video_core/command_processor.cpp +++ b/src/video_core/command_processor.cpp @@ -157,15 +157,25 @@ static void WritePicaReg(u32 id, u32 value, u32 mask) { // TODO: What happens if a loader overwrites a previous one's data? for (unsigned component = 0; component < loader_config.component_count; ++component) { - if (component >= 12) + if (component >= 12) { LOG_ERROR(HW_GPU, "Overflow in the vertex attribute loader %u trying to load component %u", loader, component); + continue; + } + u32 attribute_index = loader_config.GetComponent(component); - vertex_attribute_sources[attribute_index] = load_address; - vertex_attribute_strides[attribute_index] = static_cast<u32>(loader_config.byte_count); - vertex_attribute_formats[attribute_index] = attribute_config.GetFormat(attribute_index); - vertex_attribute_elements[attribute_index] = attribute_config.GetNumElements(attribute_index); - vertex_attribute_element_size[attribute_index] = attribute_config.GetElementSizeInBytes(attribute_index); - load_address += attribute_config.GetStride(attribute_index); + if (attribute_index < 12) { + vertex_attribute_sources[attribute_index] = load_address; + vertex_attribute_strides[attribute_index] = static_cast<u32>(loader_config.byte_count); + vertex_attribute_formats[attribute_index] = attribute_config.GetFormat(attribute_index); + vertex_attribute_elements[attribute_index] = attribute_config.GetNumElements(attribute_index); + vertex_attribute_element_size[attribute_index] = attribute_config.GetElementSizeInBytes(attribute_index); + load_address += attribute_config.GetStride(attribute_index); + } else if (attribute_index < 16) { + // Attribute ids 12, 13, 14 and 15 signify 4, 8, 12 and 16-byte paddings, respectively + load_address += (attribute_index - 11) * 4; + } else { + UNREACHABLE(); // This is truly unreachable due to the number of bits for each component + } } } |