DSP/Audio: First pass at implementing audio. Supports PCM16 only.
This commit is contained in:
parent
2c663fbc3e
commit
aa57048a0e
10 changed files with 606 additions and 39 deletions
|
@ -159,6 +159,12 @@ if (ENABLE_GLFW)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
set(OPENAL_PREFIX "${CMAKE_BINARY_DIR}/externals/openal-soft-1.17.1-bin")
|
||||||
|
set(OPENAL_INCLUDE_DIRS "${OPENAL_PREFIX}/include" CACHE PATH "Path to OpenAL-Soft headers")
|
||||||
|
set(OPENAL_LIBRARY_DIRS "${OPENAL_PREFIX}/libs/Win64" CACHE PATH "Path to OpenAL-Soft libraries")
|
||||||
|
set(OPENAL_LIBRARIES OpenAL32)
|
||||||
|
include_directories(${OPENAL_INCLUDE_DIRS})
|
||||||
|
|
||||||
IF (APPLE)
|
IF (APPLE)
|
||||||
FIND_LIBRARY(COCOA_LIBRARY Cocoa) # Umbrella framework for everything GUI-related
|
FIND_LIBRARY(COCOA_LIBRARY Cocoa) # Umbrella framework for everything GUI-related
|
||||||
FIND_LIBRARY(IOKIT_LIBRARY IOKit) # GLFW dependency
|
FIND_LIBRARY(IOKIT_LIBRARY IOKit) # GLFW dependency
|
||||||
|
|
|
@ -14,11 +14,13 @@ set(HEADERS
|
||||||
create_directory_groups(${SRCS} ${HEADERS})
|
create_directory_groups(${SRCS} ${HEADERS})
|
||||||
|
|
||||||
include_directories(${GLFW_INCLUDE_DIRS})
|
include_directories(${GLFW_INCLUDE_DIRS})
|
||||||
|
include_directories(${OPENAL_INCLUDE_DIRS})
|
||||||
link_directories(${GLFW_LIBRARY_DIRS})
|
link_directories(${GLFW_LIBRARY_DIRS})
|
||||||
|
link_directories(${OPENAL_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 common)
|
||||||
target_link_libraries(citra ${GLFW_LIBRARIES} ${OPENGL_gl_LIBRARY} inih glad)
|
target_link_libraries(citra ${GLFW_LIBRARIES} ${OPENGL_gl_LIBRARY} inih glad ${OPENAL_LIBRARIES})
|
||||||
if (MSVC)
|
if (MSVC)
|
||||||
target_link_libraries(citra getopt)
|
target_link_libraries(citra getopt)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
@ -54,6 +54,7 @@ namespace Log {
|
||||||
SUB(HW, Memory) \
|
SUB(HW, Memory) \
|
||||||
SUB(HW, LCD) \
|
SUB(HW, LCD) \
|
||||||
SUB(HW, GPU) \
|
SUB(HW, GPU) \
|
||||||
|
CLS(Audio) \
|
||||||
CLS(Frontend) \
|
CLS(Frontend) \
|
||||||
CLS(Render) \
|
CLS(Render) \
|
||||||
SUB(Render, Software) \
|
SUB(Render, Software) \
|
||||||
|
|
|
@ -69,6 +69,7 @@ enum class Class : ClassType {
|
||||||
HW_Memory, ///< Memory-map and address translation
|
HW_Memory, ///< Memory-map and address translation
|
||||||
HW_LCD, ///< LCD register emulation
|
HW_LCD, ///< LCD register emulation
|
||||||
HW_GPU, ///< GPU control emulation
|
HW_GPU, ///< GPU control emulation
|
||||||
|
Audio, ///< Emulator audio output
|
||||||
Frontend, ///< Emulator UI
|
Frontend, ///< Emulator UI
|
||||||
Render, ///< Emulator video output and hardware acceleration
|
Render, ///< Emulator video output and hardware acceleration
|
||||||
Render_Software, ///< Software renderer backend
|
Render_Software, ///< Software renderer backend
|
||||||
|
|
|
@ -11,6 +11,7 @@ set(SRCS
|
||||||
arm/skyeye_common/vfp/vfpdouble.cpp
|
arm/skyeye_common/vfp/vfpdouble.cpp
|
||||||
arm/skyeye_common/vfp/vfpinstr.cpp
|
arm/skyeye_common/vfp/vfpinstr.cpp
|
||||||
arm/skyeye_common/vfp/vfpsingle.cpp
|
arm/skyeye_common/vfp/vfpsingle.cpp
|
||||||
|
audio/stream.cpp
|
||||||
core.cpp
|
core.cpp
|
||||||
core_timing.cpp
|
core_timing.cpp
|
||||||
file_sys/archive_backend.cpp
|
file_sys/archive_backend.cpp
|
||||||
|
@ -137,6 +138,7 @@ set(HEADERS
|
||||||
arm/skyeye_common/vfp/asm_vfp.h
|
arm/skyeye_common/vfp/asm_vfp.h
|
||||||
arm/skyeye_common/vfp/vfp.h
|
arm/skyeye_common/vfp/vfp.h
|
||||||
arm/skyeye_common/vfp/vfp_helper.h
|
arm/skyeye_common/vfp/vfp_helper.h
|
||||||
|
audio/stream.h
|
||||||
core.h
|
core.h
|
||||||
core_timing.h
|
core_timing.h
|
||||||
file_sys/archive_backend.h
|
file_sys/archive_backend.h
|
||||||
|
|
186
src/core/audio/stream.cpp
Normal file
186
src/core/audio/stream.cpp
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
|
||||||
|
#include "AL/al.h"
|
||||||
|
#include "AL/alc.h"
|
||||||
|
#include "AL/alext.h"
|
||||||
|
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
|
||||||
|
#include "core/audio/stream.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
|
namespace Audio {
|
||||||
|
static const int BASE_SAMPLE_RATE = 22050;
|
||||||
|
|
||||||
|
struct Buffer {
|
||||||
|
u16 id;
|
||||||
|
ALuint buffer;
|
||||||
|
bool is_looping;
|
||||||
|
|
||||||
|
bool operator < (const Buffer& other) const {
|
||||||
|
if ((other.id - id) > 1000) return true;
|
||||||
|
if ((id - other.id) > 1000) return false;
|
||||||
|
return id > other.id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OutputChannel {
|
||||||
|
ALuint source;
|
||||||
|
int mono_or_stereo;
|
||||||
|
Format format;
|
||||||
|
std::priority_queue<Buffer> queue;
|
||||||
|
std::queue<Buffer> playing;
|
||||||
|
u16 last_bufid;
|
||||||
|
};
|
||||||
|
|
||||||
|
OutputChannel chans[24];
|
||||||
|
|
||||||
|
int InitAL(void)
|
||||||
|
{
|
||||||
|
ALCdevice *device;
|
||||||
|
ALCcontext *ctx;
|
||||||
|
|
||||||
|
/* Open and initialize a device with default settings */
|
||||||
|
device = alcOpenDevice(NULL);
|
||||||
|
if (!device)
|
||||||
|
{
|
||||||
|
LOG_CRITICAL(Audio, "Could not open a device!");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = alcCreateContext(device, NULL);
|
||||||
|
if (ctx == NULL || alcMakeContextCurrent(ctx) == ALC_FALSE)
|
||||||
|
{
|
||||||
|
if (ctx != NULL)
|
||||||
|
alcDestroyContext(ctx);
|
||||||
|
alcCloseDevice(device);
|
||||||
|
LOG_CRITICAL(Audio, "Could not set a context!");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Audio, "Opened \"%s\"", alcGetString(device, ALC_DEVICE_SPECIFIER));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ALuint source, buffer;
|
||||||
|
ALCint dev_rate;
|
||||||
|
|
||||||
|
void Init() {
|
||||||
|
InitAL();
|
||||||
|
|
||||||
|
{
|
||||||
|
ALCdevice *device = alcGetContextsDevice(alcGetCurrentContext());
|
||||||
|
alcGetIntegerv(device, ALC_FREQUENCY, 1, &dev_rate);
|
||||||
|
if (alcGetError(device) != ALC_NO_ERROR) LOG_CRITICAL(Audio, "Failed to get device sample rate");
|
||||||
|
LOG_INFO(Audio, "Device Frequency: %i", dev_rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 24; i++) {
|
||||||
|
alGenSources(1, &chans[i].source);
|
||||||
|
if (alGetError() != AL_NO_ERROR) LOG_CRITICAL(Audio, "Failed to setup sound source");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shutdown() {}
|
||||||
|
|
||||||
|
void UpdateFormat(int chanid, int mono_or_stereo, Format format) {
|
||||||
|
chans[chanid].mono_or_stereo = mono_or_stereo;
|
||||||
|
chans[chanid].format = format;
|
||||||
|
|
||||||
|
LOG_WARNING(Audio, "(STUB)");
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnqueueBuffer(int chanid, u16 buffer_id,
|
||||||
|
void* data, int sample_count,
|
||||||
|
bool has_adpcm, u16 adpcm_ps, s16 adpcm_yn[2],
|
||||||
|
bool is_looping) {
|
||||||
|
|
||||||
|
if (chans[chanid].format != FORMAT_PCM16) {
|
||||||
|
LOG_ERROR(Audio, "Unimplemented format");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: ADPCM processing should happen here
|
||||||
|
|
||||||
|
ALuint b;
|
||||||
|
alGenBuffers(1, &b);
|
||||||
|
alBufferData(b, AL_FORMAT_MONO16, data, sample_count*2, BASE_SAMPLE_RATE);
|
||||||
|
if (alGetError() != AL_NO_ERROR) LOG_CRITICAL(Audio, "Failed to init buffer");
|
||||||
|
|
||||||
|
chans[chanid].queue.emplace( Buffer { buffer_id, b, is_looping });
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tick(int chanid) {
|
||||||
|
auto& c = chans[chanid];
|
||||||
|
|
||||||
|
if (!c.queue.empty()) {
|
||||||
|
while (!c.queue.empty()) {
|
||||||
|
alSourceQueueBuffers(c.source, 1, &c.queue.top().buffer);
|
||||||
|
if (alGetError() != AL_NO_ERROR) LOG_CRITICAL(Audio, "Failed to enqueue buffer");
|
||||||
|
c.playing.emplace(c.queue.top());
|
||||||
|
LOG_INFO(Audio, "Enqueued buffer id %i", c.queue.top().id);
|
||||||
|
c.queue.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
ALint state;
|
||||||
|
alGetSourcei(c.source, AL_SOURCE_STATE, &state);
|
||||||
|
if (state != AL_PLAYING) {
|
||||||
|
alSourcePlay(c.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!c.playing.empty()) {
|
||||||
|
c.last_bufid = c.playing.front().id;
|
||||||
|
}
|
||||||
|
|
||||||
|
ALint processed;
|
||||||
|
alGetSourcei(c.source, AL_BUFFERS_PROCESSED, &processed);
|
||||||
|
while (processed > 0) {
|
||||||
|
ALuint buf;
|
||||||
|
alSourceUnqueueBuffers(c.source, 1, &buf);
|
||||||
|
processed--;
|
||||||
|
|
||||||
|
LOG_INFO(Audio, "Finished buffer id %i", c.playing.front().id);
|
||||||
|
|
||||||
|
while (!c.playing.empty() && c.playing.front().buffer != buf) {
|
||||||
|
c.playing.pop();
|
||||||
|
LOG_ERROR(Audio, "Audio is extremely funky. Should abort. (Desynced queue.)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!c.playing.empty()) {
|
||||||
|
c.last_bufid = c.playing.front().id;
|
||||||
|
c.playing.pop();
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(Audio, "Audio is extremely funky. Should abort. (Empty queue.)");
|
||||||
|
}
|
||||||
|
|
||||||
|
alDeleteBuffers(1, &buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!c.playing.empty()) {
|
||||||
|
c.last_bufid = c.playing.front().id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::tuple<bool, u16, u32> GetStatus(int chanid) {
|
||||||
|
auto& c = chans[chanid];
|
||||||
|
|
||||||
|
bool isplaying = false;
|
||||||
|
u16 bufid = 0;
|
||||||
|
u32 pos = 0;
|
||||||
|
|
||||||
|
ALint state, samples;
|
||||||
|
alGetSourcei(c.source, AL_SOURCE_STATE, &state);
|
||||||
|
alGetSourcei(c.source, AL_SAMPLE_OFFSET, &samples);
|
||||||
|
|
||||||
|
if (state == AL_PLAYING) isplaying = true;
|
||||||
|
|
||||||
|
bufid = c.last_bufid;
|
||||||
|
|
||||||
|
pos = samples;
|
||||||
|
|
||||||
|
return std::make_tuple(isplaying, bufid, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
32
src/core/audio/stream.h
Normal file
32
src/core/audio/stream.h
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "AL/al.h"
|
||||||
|
#include "AL/alc.h"
|
||||||
|
#include "AL/alext.h"
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
#include <tuple>
|
||||||
|
|
||||||
|
namespace Audio {
|
||||||
|
void Init();
|
||||||
|
void Play(void* buf, size_t size);
|
||||||
|
void Shutdown();
|
||||||
|
|
||||||
|
enum Format : u16 {
|
||||||
|
FORMAT_PCM8 = 0,
|
||||||
|
FORMAT_PCM16 = 1,
|
||||||
|
FORMAT_ADPCM = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
void UpdateFormat(int chanid, int mono_or_stereo, Format format);
|
||||||
|
|
||||||
|
void EnqueueBuffer(int chanid, u16 buffer_id,
|
||||||
|
void* data, int sample_count,
|
||||||
|
bool has_adpcm, u16 adpcm_ps, s16 adpcm_yn[2],
|
||||||
|
bool is_looping);
|
||||||
|
|
||||||
|
void Tick(int chanid);
|
||||||
|
|
||||||
|
std::tuple<bool, u16, u32> GetStatus(int chanid);
|
||||||
|
};
|
|
@ -2,30 +2,299 @@
|
||||||
// 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 "common/bit_field.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
|
||||||
|
#include "core/audio/stream.h"
|
||||||
|
#include "core/core_timing.h"
|
||||||
#include "core/hle/hle.h"
|
#include "core/hle/hle.h"
|
||||||
#include "core/hle/kernel/event.h"
|
#include "core/hle/kernel/event.h"
|
||||||
#include "core/hle/service/dsp_dsp.h"
|
#include "core/hle/service/dsp_dsp.h"
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Namespace DSP_DSP
|
// Namespace DSP_DSP
|
||||||
|
|
||||||
namespace DSP_DSP {
|
namespace DSP_DSP {
|
||||||
|
|
||||||
|
struct PairHash {
|
||||||
|
public:
|
||||||
|
template <typename T, typename U>
|
||||||
|
std::size_t operator()(const std::pair<T, U> &x) const {
|
||||||
|
return std::hash<T>()(x.first) ^ std::hash<U>()(x.second);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
static u32 semaphore_mask;
|
||||||
|
|
||||||
void SignalInterrupt() {
|
static std::unordered_map<std::pair<u32, u32>, Kernel::SharedPtr<Kernel::Event>, PairHash> interrupt_events;
|
||||||
// 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.
|
|
||||||
|
|
||||||
if (interrupt_event != 0)
|
static const u64 frame_tick = 1310252ull;
|
||||||
interrupt_event->Signal();
|
static int tick_event;
|
||||||
|
|
||||||
|
// Addresses of various things
|
||||||
|
static const VAddr BASE_ADDR_0 = Memory::DSP_RAM_VADDR + 0x40000;
|
||||||
|
static const VAddr BASE_ADDR_1 = Memory::DSP_RAM_VADDR + 0x60000;
|
||||||
|
static constexpr VAddr DspAddrToVAddr(VAddr base, u32 dsp_addr) {
|
||||||
|
return (VAddr(dsp_addr) << 1) + base;
|
||||||
|
}
|
||||||
|
static const u32 DSPADDR0 = 0xBFFF; // Frame Counter
|
||||||
|
static const u32 DSPADDR1 = 0x9E92; // Channel Context (x24)
|
||||||
|
static const u32 DSPADDR2 = 0x8680; // Channel Status (x24)
|
||||||
|
static const u32 DSPADDR3 = 0xA792; // ADPCM Coefficients (x24)
|
||||||
|
static const u32 DSPADDR4 = 0x9430; // Context
|
||||||
|
static const u32 DSPADDR5 = 0x8400; // Status
|
||||||
|
static const u32 DSPADDR6 = 0x8540; // Loopback Samples
|
||||||
|
static const u32 DSPADDR7 = 0x9494;
|
||||||
|
static const u32 DSPADDR8 = 0x8710;
|
||||||
|
static const u32 DSPADDR9 = 0x8410; // ???
|
||||||
|
static const u32 DSPADDR10 = 0xA912;
|
||||||
|
static const u32 DSPADDR11 = 0xAA12;
|
||||||
|
static const u32 DSPADDR12 = 0xAAD2;
|
||||||
|
static const u32 DSPADDR13 = 0xAC52;
|
||||||
|
static const u32 DSPADDR14 = 0xAC5C;
|
||||||
|
static const u32 DSPADDR_frame_counter = DSPADDR0;
|
||||||
|
|
||||||
|
static const int NUM_CHANNELS = 24;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSP_DSP::DspEndian
|
||||||
|
* Care must be taken when reading/writing 32-bit values. The DSP has a 16-bit wordsize and is big-endian.
|
||||||
|
* The bytes in each word when viewed from the ARM11, however, are in little-endian.
|
||||||
|
* Thus we have what appears to be a middle-endian encoding.
|
||||||
|
*
|
||||||
|
* The below function is its own inverse.
|
||||||
|
*/
|
||||||
|
struct dsp_u32 {
|
||||||
|
static constexpr u32 Convert(u32 value) {
|
||||||
|
return ((value & 0x0000FFFF) << 16) | ((value & 0xFFFF0000) >> 16);
|
||||||
|
}
|
||||||
|
operator u32() {
|
||||||
|
return Convert(value);
|
||||||
|
}
|
||||||
|
void operator=(u32 newvalue) {
|
||||||
|
value = Convert(newvalue);
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
u32 value;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define INSERT_PADDING_DSPWORDS(num_words) u16 CONCAT2(pad, __LINE__)[(num_words)]
|
||||||
|
#define ASSERT_STRUCT(name, size) \
|
||||||
|
static_assert(std::is_standard_layout<name>::value, "Structure doesn't use standard layout"); \
|
||||||
|
static_assert(sizeof(name) == (size), "Unexpected struct size")
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ADPCM seems to be the usual Nintendo format.
|
||||||
|
* ps = predictor / scaler
|
||||||
|
* yn[0,1] = sample history
|
||||||
|
* Coefficients are found at DSPADDR3
|
||||||
|
*/
|
||||||
|
|
||||||
|
struct Buffer {
|
||||||
|
dsp_u32 physical_address;
|
||||||
|
dsp_u32 sample_count;
|
||||||
|
u16 adpcm_ps;
|
||||||
|
s16 adpcm_yn[2];
|
||||||
|
u8 has_adpcm;
|
||||||
|
u8 is_looping;
|
||||||
|
u16 buffer_id;
|
||||||
|
INSERT_PADDING_DSPWORDS(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Userland mainly controls the values in this structure
|
||||||
|
struct ChannelContext {
|
||||||
|
u32 dirty;
|
||||||
|
|
||||||
|
// Effects
|
||||||
|
INSERT_PADDING_DSPWORDS(35);
|
||||||
|
|
||||||
|
// Buffer Queue
|
||||||
|
u16 buffers_dirty; //< Which of those queued buffers is dirty (bit i == buffers[i])
|
||||||
|
Buffer buffers[4]; //< Queued Buffers
|
||||||
|
|
||||||
|
INSERT_PADDING_DSPWORDS(2);
|
||||||
|
u16 is_active; //< Lower 8 bits == 0x01 if true.
|
||||||
|
u16 sync;
|
||||||
|
INSERT_PADDING_DSPWORDS(4);
|
||||||
|
|
||||||
|
// Current Buffer
|
||||||
|
dsp_u32 physical_address;
|
||||||
|
dsp_u32 sample_count;
|
||||||
|
union {
|
||||||
|
BitField<0, 2, u16> mono_or_stereo;
|
||||||
|
BitField<2, 2, Audio::Format> format;
|
||||||
|
};
|
||||||
|
u16 adpcm_ps;
|
||||||
|
s16 adpcm_yn[2];
|
||||||
|
union {
|
||||||
|
BitField<0, 1, u16> has_adpcm;
|
||||||
|
BitField<1, 1, u16> is_looping;
|
||||||
|
};
|
||||||
|
u16 buffer_id;
|
||||||
|
};
|
||||||
|
ASSERT_STRUCT(ChannelContext, 192);
|
||||||
|
|
||||||
|
// The DSP controls the values in this structure
|
||||||
|
struct ChannelStatus {
|
||||||
|
u16 is_playing;
|
||||||
|
u16 sync;
|
||||||
|
dsp_u32 buffer_position;
|
||||||
|
u16 current_buffer_id;
|
||||||
|
u16 previous_buffer_id;
|
||||||
|
};
|
||||||
|
ASSERT_STRUCT(ChannelStatus, 12);
|
||||||
|
|
||||||
|
struct AdpcmCoefficients {
|
||||||
|
u16 coeff[16];
|
||||||
|
};
|
||||||
|
ASSERT_STRUCT(AdpcmCoefficients, 32);
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static inline bool TestAndUnsetBit(T& value, size_t bitno) {
|
||||||
|
T mask = 1 << bitno;
|
||||||
|
bool ret = (value & mask) == mask;
|
||||||
|
value &= ~mask;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AudioTick(u64, int cycles_late) {
|
||||||
|
VAddr current_base;
|
||||||
|
|
||||||
|
{
|
||||||
|
int id0 = (int)Memory::Read16(DspAddrToVAddr(BASE_ADDR_0, DSPADDR_frame_counter));
|
||||||
|
int id1 = (int)Memory::Read16(DspAddrToVAddr(BASE_ADDR_1, DSPADDR_frame_counter));
|
||||||
|
|
||||||
|
// The frame id increments once per audio frame, with wraparound at 65,535.
|
||||||
|
// I am uncertain whether the real DSP actually does something like this,
|
||||||
|
// or merely checks for a certan id for wraparound. TODO: Verify.
|
||||||
|
if (id1 - id0 > 10000 && id0 < 10) {
|
||||||
|
current_base = BASE_ADDR_0;
|
||||||
|
} else if (id0 - id1 > 10000 && id1 < 10) {
|
||||||
|
current_base = BASE_ADDR_1;
|
||||||
|
} else if (id1 > id0) {
|
||||||
|
current_base = BASE_ADDR_1;
|
||||||
|
} else {
|
||||||
|
current_base = BASE_ADDR_0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto channel_contexes = (ChannelContext*) Memory::GetPointer(DspAddrToVAddr(current_base, DSPADDR1));
|
||||||
|
auto channel_status0 = (ChannelStatus*)Memory::GetPointer(DspAddrToVAddr(BASE_ADDR_0, DSPADDR2));
|
||||||
|
auto channel_status1 = (ChannelStatus*)Memory::GetPointer(DspAddrToVAddr(BASE_ADDR_1, DSPADDR2));
|
||||||
|
auto channel_adpcm_coeffs = (AdpcmCoefficients*) Memory::GetPointer(DspAddrToVAddr(current_base, DSPADDR3));
|
||||||
|
|
||||||
|
for (int chanid=0; chanid<NUM_CHANNELS; chanid++) {
|
||||||
|
ChannelContext& ctx = channel_contexes[chanid];
|
||||||
|
ChannelStatus& status0 = channel_status0[chanid];
|
||||||
|
ChannelStatus& status1 = channel_status1[chanid];
|
||||||
|
|
||||||
|
if (ctx.dirty) {
|
||||||
|
if (TestAndUnsetBit(ctx.dirty, 29)) {
|
||||||
|
// First time init
|
||||||
|
LOG_WARNING(Service_DSP, "Unimplemented dirty bit 29");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TestAndUnsetBit(ctx.dirty, 16)) {
|
||||||
|
// Is Active?
|
||||||
|
LOG_WARNING(Service_DSP, "Unimplemented dirty bit 16");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TestAndUnsetBit(ctx.dirty, 2)) {
|
||||||
|
// Update ADPCM coefficients
|
||||||
|
LOG_WARNING(Service_DSP, "Unimplemented dirty bit 2");
|
||||||
|
AdpcmCoefficients& coeff = channel_adpcm_coeffs[chanid];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TestAndUnsetBit(ctx.dirty, 17)) {
|
||||||
|
// Interpolation type
|
||||||
|
LOG_WARNING(Service_DSP, "Unimplemented dirty bit 17");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TestAndUnsetBit(ctx.dirty, 18)) {
|
||||||
|
// Rate
|
||||||
|
LOG_WARNING(Service_DSP, "Unimplemented dirty bit 18");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TestAndUnsetBit(ctx.dirty, 22)) {
|
||||||
|
// IIR
|
||||||
|
LOG_WARNING(Service_DSP, "Unimplemented dirty bit 22");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TestAndUnsetBit(ctx.dirty, 28)) {
|
||||||
|
// Sync count
|
||||||
|
LOG_WARNING(Service_DSP, "(STUB) Update Sync Count");
|
||||||
|
|
||||||
|
status0.sync = ctx.sync;
|
||||||
|
status1.sync = ctx.sync;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TestAndUnsetBit(ctx.dirty, 25) | TestAndUnsetBit(ctx.dirty, 26) | TestAndUnsetBit(ctx.dirty, 27)) {
|
||||||
|
// Mix
|
||||||
|
LOG_WARNING(Service_DSP, "Unimplemented dirty bit 25/26/27");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TestAndUnsetBit(ctx.dirty, 4) | TestAndUnsetBit(ctx.dirty, 21) | TestAndUnsetBit(ctx.dirty, 30)) {
|
||||||
|
// TODO(merry): One of these bits might merely signify an update to the format. Verify this.
|
||||||
|
// Embedded Buffer Changed
|
||||||
|
Audio::UpdateFormat(chanid, ctx.mono_or_stereo, ctx.format);
|
||||||
|
Audio::EnqueueBuffer(chanid, ctx.buffer_id,
|
||||||
|
Memory::GetPhysicalPointer(ctx.physical_address), ctx.sample_count,
|
||||||
|
ctx.has_adpcm, ctx.adpcm_ps, ctx.adpcm_yn,
|
||||||
|
ctx.is_looping);
|
||||||
|
|
||||||
|
status0.is_playing |= 0x100; // TODO: This is supposed to flicker on then turn off.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TestAndUnsetBit(ctx.dirty, 19)) {
|
||||||
|
// Buffer queue
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if (TestAndUnsetBit(ctx.buffers_dirty, i)) {
|
||||||
|
auto& b = ctx.buffers[i];
|
||||||
|
Audio::EnqueueBuffer(chanid, b.buffer_id,
|
||||||
|
Memory::GetPhysicalPointer(b.physical_address), b.sample_count,
|
||||||
|
b.has_adpcm, b.adpcm_ps, b.adpcm_yn,
|
||||||
|
b.is_looping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.buffers_dirty) {
|
||||||
|
LOG_ERROR(Service_DSP, "Unknown channel buffer dirty bits: 0x%04x", ctx.buffers_dirty);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.buffers_dirty = 0;
|
||||||
|
|
||||||
|
status0.is_playing |= 0x100; // TODO: This is supposed to flicker on then turn off.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.dirty) {
|
||||||
|
LOG_ERROR(Service_DSP, "Unknown channel dirty bits: 0x%08x", ctx.dirty);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.dirty = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Audio::Tick(chanid);
|
||||||
|
|
||||||
|
// Update channel status
|
||||||
|
bool playing = false;
|
||||||
|
std::tie(playing, status0.current_buffer_id, status0.buffer_position) = Audio::GetStatus(chanid);
|
||||||
|
if (playing) {
|
||||||
|
status0.is_playing |= 1;
|
||||||
|
} else {
|
||||||
|
status0.is_playing = 0;
|
||||||
|
}
|
||||||
|
status1 = status0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto interrupt_event : interrupt_events)
|
||||||
|
interrupt_event.second->Signal();
|
||||||
|
|
||||||
|
CoreTiming::ScheduleEvent(frame_tick-cycles_late, tick_event, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,9 +311,7 @@ static void ConvertProcessAddressFromDspDram(Service::Interface* self) {
|
||||||
u32 addr = cmd_buff[1];
|
u32 addr = cmd_buff[1];
|
||||||
|
|
||||||
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] = DspAddrToVAddr(BASE_ADDR_0, addr);
|
||||||
|
|
||||||
LOG_WARNING(Service_DSP, "(STUBBED) called with address 0x%08X", addr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,8 +389,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
|
||||||
* 2 : Parameter 1 (purpose unknown)
|
* 2 : 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
|
||||||
|
@ -131,22 +398,28 @@ 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]; // TODO(merry): Confirm the purpose of each interrupt. Presumably there would be one interrupt that would allow for ARM11 modification of the output.
|
||||||
u32 param1 = cmd_buff[2];
|
u32 number = 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) {
|
// Unregister the event for this interrupt and number
|
||||||
interrupt_event = evt;
|
interrupt_events.erase(std::make_pair(interrupt, number));
|
||||||
cmd_buff[1] = 0; // No error
|
cmd_buff[1] = RESULT_SUCCESS.raw;
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR(Service_DSP, "called with invalid handle=%08X", cmd_buff[4]);
|
auto evt = Kernel::g_handle_table.Get<Kernel::Event>(event_handle);
|
||||||
|
if (evt != nullptr) {
|
||||||
|
interrupt_events[std::make_pair(interrupt, number)] = evt;
|
||||||
|
cmd_buff[1] = RESULT_SUCCESS.raw; // No error
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(Service_DSP, "called with invalid handle=%08X", event_handle);
|
||||||
|
|
||||||
// TODO(yuriks): An error should be returned from SendSyncRequest, not in the cmdbuf
|
// TODO(yuriks): An error should be returned from SendSyncRequest, not in the cmdbuf
|
||||||
cmd_buff[1] = -1;
|
cmd_buff[1] = -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_WARNING(Service_DSP, "(STUBBED) called param0=%u, param1=%u, event_handle=0x%08X", param0, param1, event_handle);
|
LOG_WARNING(Service_DSP, "(STUBBED) called interrupt=%u, number=%u, event_handle=0x%08X", interrupt, number, event_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -155,12 +428,12 @@ static void RegisterInterruptEvents(Service::Interface* self) {
|
||||||
* 1 : Unknown (observed only half word used)
|
* 1 : Unknown (observed only half word used)
|
||||||
* Outputs:
|
* Outputs:
|
||||||
* 1 : Result of function, 0 on success, otherwise error code
|
* 1 : Result of function, 0 on success, otherwise error code
|
||||||
|
* Notes:
|
||||||
|
* Games do not seem to rely on the DSP semaphore very much
|
||||||
*/
|
*/
|
||||||
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");
|
||||||
|
@ -205,16 +478,35 @@ 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];
|
||||||
|
|
||||||
|
if (pipe != 2) {
|
||||||
|
LOG_ERROR(Service_DSP, "I'm not sure what to do when pipe=0x%08x\n", pipe);
|
||||||
|
}
|
||||||
|
|
||||||
// Canned DSP responses that games expect. These were taken from HW by 3dmoo team.
|
// Canned DSP responses that games expect. These were taken from HW by 3dmoo team.
|
||||||
// TODO: Remove this hack :)
|
// TODO: Remove this hack :)
|
||||||
|
// FIXME(merry): Incorrect behaviour; the read buffer isn't a single stream, nor does it behave like a stream.
|
||||||
static const std::array<u16, 16> canned_read_pipe = {{
|
static const std::array<u16, 16> canned_read_pipe = {{
|
||||||
0x000F, 0xBFFF, 0x9E8E, 0x8680, 0xA78E, 0x9430, 0x8400, 0x8540,
|
0x000F,
|
||||||
0x948E, 0x8710, 0x8410, 0xA90E, 0xAA0E, 0xAACE, 0xAC4E, 0xAC58
|
DSPADDR0,
|
||||||
|
DSPADDR1,
|
||||||
|
DSPADDR2,
|
||||||
|
DSPADDR3,
|
||||||
|
DSPADDR4,
|
||||||
|
DSPADDR5,
|
||||||
|
DSPADDR6,
|
||||||
|
DSPADDR7,
|
||||||
|
DSPADDR8,
|
||||||
|
DSPADDR9,
|
||||||
|
DSPADDR10,
|
||||||
|
DSPADDR11,
|
||||||
|
DSPADDR12,
|
||||||
|
DSPADDR13,
|
||||||
|
DSPADDR14,
|
||||||
}};
|
}};
|
||||||
|
|
||||||
u32 initial_size = read_pipe_count;
|
u32 initial_size = read_pipe_count;
|
||||||
|
@ -232,8 +524,8 @@ static void ReadPipeIfPossible(Service::Interface* self) {
|
||||||
cmd_buff[1] = 0; // No error
|
cmd_buff[1] = 0; // No error
|
||||||
cmd_buff[2] = (read_pipe_count - initial_size) * sizeof(u16);
|
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",
|
LOG_WARNING(Service_DSP, "(STUBBED) called pipe=0x%08X, unk2=0x%08X, size=0x%X, buffer=0x%08X",
|
||||||
unk1, unk2, size, addr);
|
pipe, unk2, size, addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -248,6 +540,8 @@ static void SetSemaphoreMask(Service::Interface* self) {
|
||||||
|
|
||||||
u32 mask = cmd_buff[1];
|
u32 mask = cmd_buff[1];
|
||||||
|
|
||||||
|
semaphore_mask = mask;
|
||||||
|
|
||||||
cmd_buff[1] = RESULT_SUCCESS.raw; // No error
|
cmd_buff[1] = RESULT_SUCCESS.raw; // No error
|
||||||
|
|
||||||
LOG_WARNING(Service_DSP, "(STUBBED) called mask=0x%08X", mask);
|
LOG_WARNING(Service_DSP, "(STUBBED) called mask=0x%08X", mask);
|
||||||
|
@ -271,6 +565,46 @@ static void GetHeadphoneStatus(Service::Interface* self) {
|
||||||
LOG_DEBUG(Service_DSP, "(STUBBED) called");
|
LOG_DEBUG(Service_DSP, "(STUBBED) called");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSP_DSP::RecvData service function
|
||||||
|
* Inputs:
|
||||||
|
* 1 : Register Number
|
||||||
|
* Outputs:
|
||||||
|
* 1 : Result of function, 0 on success, otherwise error code
|
||||||
|
* 2 : Value in the register
|
||||||
|
*/
|
||||||
|
static void RecvData(Service::Interface* self) {
|
||||||
|
u32* cmd_buff = Kernel::GetCommandBuffer();
|
||||||
|
|
||||||
|
u32 registerNo = cmd_buff[1];
|
||||||
|
|
||||||
|
cmd_buff[1] = RESULT_SUCCESS.raw; // No error
|
||||||
|
cmd_buff[2] = 1;
|
||||||
|
|
||||||
|
LOG_WARNING(Service_DSP, "(STUBBED) called register=%u", registerNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSP_DSP::RecvDataIsReady service function
|
||||||
|
* Inputs:
|
||||||
|
* 1 : Register Number
|
||||||
|
* Outputs:
|
||||||
|
* 1 : Result of function, 0 on success, otherwise error code
|
||||||
|
* 2 : non-zero == ready
|
||||||
|
* Notes:
|
||||||
|
* Seems to be mainly called when going into sleep mode.
|
||||||
|
*/
|
||||||
|
static void RecvDataIsReady(Service::Interface* self) {
|
||||||
|
u32* cmd_buff = Kernel::GetCommandBuffer();
|
||||||
|
|
||||||
|
u32 registerNo = cmd_buff[1];
|
||||||
|
|
||||||
|
cmd_buff[1] = RESULT_SUCCESS.raw; // No error
|
||||||
|
cmd_buff[2] = 1;
|
||||||
|
|
||||||
|
LOG_WARNING(Service_DSP, "(STUBBED) called register=%u", registerNo);
|
||||||
|
}
|
||||||
|
|
||||||
const Interface::FunctionInfo FunctionTable[] = {
|
const Interface::FunctionInfo FunctionTable[] = {
|
||||||
{0x00010040, nullptr, "RecvData"},
|
{0x00010040, nullptr, "RecvData"},
|
||||||
{0x00020040, nullptr, "RecvDataIsReady"},
|
{0x00020040, nullptr, "RecvDataIsReady"},
|
||||||
|
@ -312,15 +646,20 @@ 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;
|
interrupt_events.clear();
|
||||||
read_pipe_count = 0;
|
read_pipe_count = 0;
|
||||||
|
|
||||||
Register(FunctionTable);
|
Register(FunctionTable);
|
||||||
|
|
||||||
|
tick_event = CoreTiming::RegisterEvent("DSP_DSP::tick_event", AudioTick);
|
||||||
|
CoreTiming::ScheduleEvent(frame_tick, tick_event, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Interface::~Interface() {
|
Interface::~Interface() {
|
||||||
semaphore_event = nullptr;
|
semaphore_event = nullptr;
|
||||||
interrupt_event = nullptr;
|
interrupt_events.clear();
|
||||||
|
|
||||||
|
CoreTiming::UnscheduleEvent(tick_event, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
|
@ -414,11 +414,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,6 +2,7 @@
|
||||||
// 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 "core/audio/stream.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"
|
||||||
|
@ -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);
|
||||||
|
Audio::Init();
|
||||||
GDBStub::Init();
|
GDBStub::Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Shutdown() {
|
void Shutdown() {
|
||||||
GDBStub::Shutdown();
|
GDBStub::Shutdown();
|
||||||
|
Audio::Shutdown();
|
||||||
VideoCore::Shutdown();
|
VideoCore::Shutdown();
|
||||||
HLE::Shutdown();
|
HLE::Shutdown();
|
||||||
Kernel::Shutdown();
|
Kernel::Shutdown();
|
||||||
|
|
Loading…
Reference in a new issue