AudioCore: Skeleton Implementation
This commit: * Adds a new subproject, audio_core. * Defines structures that exist in DSP shared memory. * Hooks up various other parts of the emulator into audio core. This sets the foundation for a later HLE DSP implementation.
This commit is contained in:
parent
0d086616d1
commit
8b00954ec7
|
@ -4,6 +4,7 @@ include_directories(.)
|
||||||
add_subdirectory(common)
|
add_subdirectory(common)
|
||||||
add_subdirectory(core)
|
add_subdirectory(core)
|
||||||
add_subdirectory(video_core)
|
add_subdirectory(video_core)
|
||||||
|
add_subdirectory(audio_core)
|
||||||
if (ENABLE_GLFW)
|
if (ENABLE_GLFW)
|
||||||
add_subdirectory(citra)
|
add_subdirectory(citra)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
@ -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})
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -17,7 +17,7 @@ include_directories(${GLFW_INCLUDE_DIRS})
|
||||||
link_directories(${GLFW_LIBRARY_DIRS})
|
link_directories(${GLFW_LIBRARY_DIRS})
|
||||||
|
|
||||||
add_executable(citra ${SRCS} ${HEADERS})
|
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)
|
target_link_libraries(citra ${GLFW_LIBRARIES} ${OPENGL_gl_LIBRARY} inih glad)
|
||||||
if (MSVC)
|
if (MSVC)
|
||||||
target_link_libraries(citra getopt)
|
target_link_libraries(citra getopt)
|
||||||
|
|
|
@ -79,7 +79,7 @@ if (APPLE)
|
||||||
else()
|
else()
|
||||||
add_executable(citra-qt ${SRCS} ${HEADERS} ${UI_HDRS})
|
add_executable(citra-qt ${SRCS} ${HEADERS} ${UI_HDRS})
|
||||||
endif()
|
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 ${OPENGL_gl_LIBRARY} ${CITRA_QT_LIBS})
|
||||||
target_link_libraries(citra-qt ${PLATFORM_LIBRARIES})
|
target_link_libraries(citra-qt ${PLATFORM_LIBRARIES})
|
||||||
|
|
||||||
|
|
|
@ -185,6 +185,6 @@ private:
|
||||||
};
|
};
|
||||||
#pragma pack()
|
#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");
|
static_assert(std::is_trivially_copyable<BitField<0, 1, u32>>::value, "BitField must be trivially copyable");
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -58,6 +58,8 @@ namespace Log {
|
||||||
CLS(Render) \
|
CLS(Render) \
|
||||||
SUB(Render, Software) \
|
SUB(Render, Software) \
|
||||||
SUB(Render, OpenGL) \
|
SUB(Render, OpenGL) \
|
||||||
|
CLS(Audio) \
|
||||||
|
SUB(Audio, DSP) \
|
||||||
CLS(Loader)
|
CLS(Loader)
|
||||||
|
|
||||||
// GetClassName is a macro defined by Windows.h, grrr...
|
// GetClassName is a macro defined by Windows.h, grrr...
|
||||||
|
|
|
@ -73,6 +73,8 @@ enum class Class : ClassType {
|
||||||
Render, ///< Emulator video output and hardware acceleration
|
Render, ///< Emulator video output and hardware acceleration
|
||||||
Render_Software, ///< Software renderer backend
|
Render_Software, ///< Software renderer backend
|
||||||
Render_OpenGL, ///< OpenGL backend
|
Render_OpenGL, ///< OpenGL backend
|
||||||
|
Audio, ///< Emulator audio output
|
||||||
|
Audio_DSP, ///< The HLE implementation of the DSP
|
||||||
Loader, ///< ROM loader
|
Loader, ///< ROM loader
|
||||||
|
|
||||||
Count ///< Total number of logging classes
|
Count ///< Total number of logging classes
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "audio_core/audio_core.h"
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
|
||||||
|
@ -107,7 +109,6 @@ struct MemoryArea {
|
||||||
static MemoryArea memory_areas[] = {
|
static MemoryArea memory_areas[] = {
|
||||||
{SHARED_MEMORY_VADDR, SHARED_MEMORY_SIZE, "Shared Memory"}, // Shared memory
|
{SHARED_MEMORY_VADDR, SHARED_MEMORY_SIZE, "Shared Memory"}, // Shared memory
|
||||||
{VRAM_VADDR, VRAM_SIZE, "VRAM"}, // Video memory (VRAM)
|
{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
|
{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,
|
auto shared_page_vma = address_space.MapBackingMemory(SHARED_PAGE_VADDR,
|
||||||
(u8*)&SharedPage::shared_page, SHARED_PAGE_SIZE, MemoryState::Shared).MoveFrom();
|
(u8*)&SharedPage::shared_page, SHARED_PAGE_SIZE, MemoryState::Shared).MoveFrom();
|
||||||
address_space.Reprotect(shared_page_vma, VMAPermission::Read);
|
address_space.Reprotect(shared_page_vma, VMAPermission::Read);
|
||||||
|
|
||||||
|
AudioCore::AddAddressSpace(address_space);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "audio_core/hle/pipe.h"
|
||||||
|
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
|
||||||
#include "core/hle/kernel/event.h"
|
#include "core/hle/kernel/event.h"
|
||||||
|
@ -14,17 +16,30 @@ namespace DSP_DSP {
|
||||||
|
|
||||||
static u32 read_pipe_count;
|
static u32 read_pipe_count;
|
||||||
static Kernel::SharedPtr<Kernel::Event> semaphore_event;
|
static Kernel::SharedPtr<Kernel::Event> semaphore_event;
|
||||||
static Kernel::SharedPtr<Kernel::Event> interrupt_event;
|
|
||||||
|
|
||||||
void SignalInterrupt() {
|
struct PairHash {
|
||||||
// TODO(bunnei): This is just a stub, it does not do anything other than signal to the emulated
|
template <typename T, typename U>
|
||||||
// application that a DSP interrupt occurred, without specifying which one. Since we do not
|
std::size_t operator()(const std::pair<T, U> &x) const {
|
||||||
// emulate the DSP yet (and how it works is largely unknown), this is a work around to get games
|
// TODO(yuriks): Replace with better hash combining function.
|
||||||
// that check the DSP interrupt signal event to run. We should figure out the different types of
|
return std::hash<T>()(x.first) ^ std::hash<U>()(x.second);
|
||||||
// DSP interrupts, and trigger them at the appropriate times.
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (interrupt_event != 0)
|
/// Map of (audio interrupt number, channel number) to Kernel::Events. See: RegisterInterruptEvents
|
||||||
interrupt_event->Signal();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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[1] = 0; // No error
|
||||||
cmd_buff[2] = (addr << 1) + (Memory::DSP_RAM_VADDR + 0x40000);
|
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
|
* DSP_DSP::RegisterInterruptEvents service function
|
||||||
* Inputs:
|
* Inputs:
|
||||||
* 1 : Parameter 0 (purpose unknown)
|
* 1 : Interrupt Number
|
||||||
* 2 : Parameter 1 (purpose unknown)
|
* 2 : Channel Number
|
||||||
* 4 : Interrupt event handle
|
* 4 : Interrupt event handle
|
||||||
* Outputs:
|
* Outputs:
|
||||||
* 1 : Result of function, 0 on success, otherwise error code
|
* 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) {
|
static void RegisterInterruptEvents(Service::Interface* self) {
|
||||||
u32* cmd_buff = Kernel::GetCommandBuffer();
|
u32* cmd_buff = Kernel::GetCommandBuffer();
|
||||||
|
|
||||||
u32 param0 = cmd_buff[1];
|
u32 interrupt = cmd_buff[1];
|
||||||
u32 param1 = cmd_buff[2];
|
u32 channel = cmd_buff[2];
|
||||||
u32 event_handle = cmd_buff[4];
|
u32 event_handle = cmd_buff[4];
|
||||||
|
|
||||||
auto evt = Kernel::g_handle_table.Get<Kernel::Event>(cmd_buff[4]);
|
if (event_handle) {
|
||||||
if (evt != nullptr) {
|
auto evt = Kernel::g_handle_table.Get<Kernel::Event>(cmd_buff[4]);
|
||||||
interrupt_event = evt;
|
if (evt) {
|
||||||
cmd_buff[1] = 0; // No error
|
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 {
|
} else {
|
||||||
LOG_ERROR(Service_DSP, "called with invalid handle=%08X", cmd_buff[4]);
|
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);
|
||||||
// TODO(yuriks): An error should be returned from SendSyncRequest, not in the cmdbuf
|
|
||||||
cmd_buff[1] = -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
static void SetSemaphore(Service::Interface* self) {
|
||||||
u32* cmd_buff = Kernel::GetCommandBuffer();
|
u32* cmd_buff = Kernel::GetCommandBuffer();
|
||||||
|
|
||||||
SignalInterrupt();
|
|
||||||
|
|
||||||
cmd_buff[1] = 0; // No error
|
cmd_buff[1] = 0; // No error
|
||||||
|
|
||||||
LOG_WARNING(Service_DSP, "(STUBBED) called");
|
LOG_WARNING(Service_DSP, "(STUBBED) called");
|
||||||
|
@ -168,9 +183,9 @@ static void SetSemaphore(Service::Interface* self) {
|
||||||
/**
|
/**
|
||||||
* DSP_DSP::WriteProcessPipe service function
|
* DSP_DSP::WriteProcessPipe service function
|
||||||
* Inputs:
|
* Inputs:
|
||||||
* 1 : Number
|
* 1 : Channel
|
||||||
* 2 : Size
|
* 2 : Size
|
||||||
* 3 : (size <<14) | 0x402
|
* 3 : (size << 14) | 0x402
|
||||||
* 4 : Buffer
|
* 4 : Buffer
|
||||||
* Outputs:
|
* Outputs:
|
||||||
* 0 : Return header
|
* 0 : Return header
|
||||||
|
@ -179,21 +194,42 @@ static void SetSemaphore(Service::Interface* self) {
|
||||||
static void WriteProcessPipe(Service::Interface* self) {
|
static void WriteProcessPipe(Service::Interface* self) {
|
||||||
u32* cmd_buff = Kernel::GetCommandBuffer();
|
u32* cmd_buff = Kernel::GetCommandBuffer();
|
||||||
|
|
||||||
u32 number = cmd_buff[1];
|
u32 channel = cmd_buff[1];
|
||||||
u32 size = cmd_buff[2];
|
u32 size = cmd_buff[2];
|
||||||
u32 new_size = cmd_buff[3];
|
|
||||||
u32 buffer = cmd_buff[4];
|
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
|
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",
|
LOG_TRACE(Service_DSP, "channel=%u, size=0x%X, buffer=0x%08X", channel, size, buffer);
|
||||||
number, size, new_size, buffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DSP_DSP::ReadPipeIfPossible service function
|
* 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:
|
* Inputs:
|
||||||
* 1 : Unknown
|
* 1 : Pipe Number
|
||||||
* 2 : Unknown
|
* 2 : Unknown
|
||||||
* 3 : Size in bytes of read (observed only lower half word used)
|
* 3 : Size in bytes of read (observed only lower half word used)
|
||||||
* 0x41 : Virtual address to read from DSP pipe to in memory
|
* 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) {
|
static void ReadPipeIfPossible(Service::Interface* self) {
|
||||||
u32* cmd_buff = Kernel::GetCommandBuffer();
|
u32* cmd_buff = Kernel::GetCommandBuffer();
|
||||||
|
|
||||||
u32 unk1 = cmd_buff[1];
|
u32 pipe = cmd_buff[1];
|
||||||
u32 unk2 = cmd_buff[2];
|
u32 unk2 = cmd_buff[2];
|
||||||
u32 size = cmd_buff[3] & 0xFFFF;// Lower 16 bits are size
|
u32 size = cmd_buff[3] & 0xFFFF;// Lower 16 bits are size
|
||||||
VAddr addr = cmd_buff[0x41];
|
VAddr addr = cmd_buff[0x41];
|
||||||
|
|
||||||
// Canned DSP responses that games expect. These were taken from HW by 3dmoo team.
|
if (!Memory::GetPointer(addr)) {
|
||||||
// TODO: Remove this hack :)
|
LOG_ERROR(Service_DSP, "Invalid addr: pipe=0x%08X, unk2=0x%08X, size=0x%X, buffer=0x%08X", pipe, unk2, size, addr);
|
||||||
static const std::array<u16, 16> canned_read_pipe = {{
|
cmd_buff[1] = -1; // TODO
|
||||||
0x000F, 0xBFFF, 0x9E8E, 0x8680, 0xA78E, 0x9430, 0x8400, 0x8540,
|
return;
|
||||||
0x948E, 0x8710, 0x8410, 0xA90E, 0xAA0E, 0xAACE, 0xAC4E, 0xAC58
|
|
||||||
}};
|
|
||||||
|
|
||||||
u32 initial_size = read_pipe_count;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_buff[1] = 0; // No error
|
std::vector<u8> response = DSP::HLE::PipeRead(pipe, size);
|
||||||
cmd_buff[2] = (read_pipe_count - initial_size) * sizeof(u16);
|
|
||||||
|
|
||||||
LOG_WARNING(Service_DSP, "(STUBBED) called unk1=0x%08X, unk2=0x%08X, size=0x%X, buffer=0x%08X",
|
Memory::WriteBlock(addr, response.data(), response.size());
|
||||||
unk1, unk2, size, addr);
|
|
||||||
|
cmd_buff[1] = 0; // No error
|
||||||
|
cmd_buff[2] = (u32)response.size();
|
||||||
|
|
||||||
|
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() {
|
Interface::Interface() {
|
||||||
semaphore_event = Kernel::Event::Create(RESETTYPE_ONESHOT, "DSP_DSP::semaphore_event");
|
semaphore_event = Kernel::Event::Create(RESETTYPE_ONESHOT, "DSP_DSP::semaphore_event");
|
||||||
interrupt_event = nullptr;
|
|
||||||
read_pipe_count = 0;
|
read_pipe_count = 0;
|
||||||
|
|
||||||
Register(FunctionTable);
|
Register(FunctionTable);
|
||||||
|
@ -319,7 +344,7 @@ Interface::Interface() {
|
||||||
|
|
||||||
Interface::~Interface() {
|
Interface::~Interface() {
|
||||||
semaphore_event = nullptr;
|
semaphore_event = nullptr;
|
||||||
interrupt_event = nullptr;
|
interrupt_events.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
|
@ -23,7 +23,15 @@ public:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Signals that a DSP interrupt has occurred to userland code
|
/// Signal all audio related interrupts.
|
||||||
void SignalInterrupt();
|
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
|
} // namespace
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
#include "core/core_timing.h"
|
#include "core/core_timing.h"
|
||||||
|
|
||||||
#include "core/hle/service/gsp_gpu.h"
|
#include "core/hle/service/gsp_gpu.h"
|
||||||
#include "core/hle/service/dsp_dsp.h"
|
|
||||||
#include "core/hle/service/hid/hid.h"
|
#include "core/hle/service/hid/hid.h"
|
||||||
|
|
||||||
#include "core/hw/hw.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::PDC0);
|
||||||
GSP_GPU::SignalInterrupt(GSP_GPU::InterruptId::PDC1);
|
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
|
// Check for user input updates
|
||||||
Service::HID::Update();
|
Service::HID::Update();
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,12 @@
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "audio_core/audio_core.h"
|
||||||
|
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/core_timing.h"
|
#include "core/core_timing.h"
|
||||||
#include "core/system.h"
|
#include "core/system.h"
|
||||||
|
#include "core/gdbstub/gdbstub.h"
|
||||||
#include "core/hw/hw.h"
|
#include "core/hw/hw.h"
|
||||||
#include "core/hle/hle.h"
|
#include "core/hle/hle.h"
|
||||||
#include "core/hle/kernel/kernel.h"
|
#include "core/hle/kernel/kernel.h"
|
||||||
|
@ -12,8 +15,6 @@
|
||||||
|
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
#include "core/gdbstub/gdbstub.h"
|
|
||||||
|
|
||||||
namespace System {
|
namespace System {
|
||||||
|
|
||||||
void Init(EmuWindow* emu_window) {
|
void Init(EmuWindow* emu_window) {
|
||||||
|
@ -24,11 +25,13 @@ void Init(EmuWindow* emu_window) {
|
||||||
Kernel::Init();
|
Kernel::Init();
|
||||||
HLE::Init();
|
HLE::Init();
|
||||||
VideoCore::Init(emu_window);
|
VideoCore::Init(emu_window);
|
||||||
|
AudioCore::Init();
|
||||||
GDBStub::Init();
|
GDBStub::Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Shutdown() {
|
void Shutdown() {
|
||||||
GDBStub::Shutdown();
|
GDBStub::Shutdown();
|
||||||
|
AudioCore::Shutdown();
|
||||||
VideoCore::Shutdown();
|
VideoCore::Shutdown();
|
||||||
HLE::Shutdown();
|
HLE::Shutdown();
|
||||||
Kernel::Shutdown();
|
Kernel::Shutdown();
|
||||||
|
|
Reference in New Issue