Core: Implement GBA Core using libmgba

This commit is contained in:
Bonta 2021-07-04 13:13:21 +02:00
parent 2d744da68c
commit 9a22ff653f
4 changed files with 851 additions and 2 deletions

View File

@ -440,8 +440,8 @@ add_library(core
PowerPC/Interpreter/Interpreter_Tables.cpp
PowerPC/Interpreter/Interpreter.cpp
PowerPC/Interpreter/Interpreter.h
PowerPC/JitCommon/DivUtils.cpp
PowerPC/JitCommon/DivUtils.h
PowerPC/JitCommon/DivUtils.cpp
PowerPC/JitCommon/DivUtils.h
PowerPC/JitCommon/JitAsmCommon.cpp
PowerPC/JitCommon/JitAsmCommon.h
PowerPC/JitCommon/JitBase.cpp
@ -621,6 +621,10 @@ if(ENABLE_VULKAN)
endif()
if(USE_MGBA)
target_sources(core PRIVATE
HW/GBACore.cpp
HW/GBACore.h
)
target_link_libraries(core PUBLIC mGBA::mgba)
target_compile_definitions(core PUBLIC -DHAS_LIBMGBA)
endif()

View File

@ -0,0 +1,714 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/HW/GBACore.h"
#include <mbedtls/sha1.h>
#define PYCPARSE // Remove static functions from the header
#include <mgba/core/interface.h>
#undef PYCPARSE
#include <mgba-util/vfs.h>
#include <mgba/core/blip_buf.h>
#include <mgba/core/log.h>
#include <mgba/core/timing.h>
#include <mgba/internal/gb/gb.h>
#include <mgba/internal/gba/gba.h>
#include "AudioCommon/AudioCommon.h"
#include "Common/ChunkFile.h"
#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
#include "Common/Config/Config.h"
#include "Common/FileUtil.h"
#include "Common/IOFile.h"
#include "Common/MinizipUtil.h"
#include "Common/ScopeGuard.h"
#include "Common/Thread.h"
#include "Core/Config/MainSettings.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/HW/SystemTimers.h"
#include "Core/Host.h"
#include "Core/NetPlayProto.h"
namespace HW::GBA
{
namespace
{
mLogger s_stub_logger = {
[](mLogger*, int category, mLogLevel level, const char* format, va_list args) {}, nullptr};
} // namespace
constexpr auto SAMPLES = 512;
constexpr auto SAMPLE_RATE = 48000;
// libmGBA does not return the correct frequency for some GB models
static u32 GetCoreFrequency(mCore* core)
{
if (core->platform(core) != mPLATFORM_GB)
return static_cast<u32>(core->frequency(core));
switch (static_cast<::GB*>(core->board)->model)
{
case GB_MODEL_CGB:
case GB_MODEL_SCGB:
case GB_MODEL_AGB:
return CGB_SM83_FREQUENCY;
case GB_MODEL_SGB:
return SGB_SM83_FREQUENCY;
default:
return DMG_SM83_FREQUENCY;
}
}
static VFile* OpenROM_Archive(const char* path)
{
VFile* vf{};
VDir* archive = VDirOpenArchive(path);
if (!archive)
return nullptr;
VFile* vf_archive =
VDirFindFirst(archive, [](VFile* vf_) { return mCoreIsCompatible(vf_) != mPLATFORM_NONE; });
if (vf_archive)
{
size_t size = static_cast<size_t>(vf_archive->size(vf_archive));
std::vector<u8> buffer(size);
vf_archive->seek(vf_archive, 0, SEEK_SET);
vf_archive->read(vf_archive, buffer.data(), size);
vf_archive->close(vf_archive);
vf = VFileMemChunk(buffer.data(), size);
}
archive->close(archive);
return vf;
}
static VFile* OpenROM_Zip(const char* path)
{
VFile* vf{};
unzFile zip = unzOpen(path);
if (!zip)
return nullptr;
do
{
unz_file_info info{};
if (unzGetCurrentFileInfo(zip, &info, nullptr, 0, nullptr, 0, nullptr, 0) != UNZ_OK ||
!info.uncompressed_size)
continue;
std::vector<u8> buffer(info.uncompressed_size);
if (!Common::ReadFileFromZip(zip, &buffer))
continue;
vf = VFileMemChunk(buffer.data(), info.uncompressed_size);
if (mCoreIsCompatible(vf) == mPLATFORM_GBA)
{
vf->seek(vf, 0, SEEK_SET);
break;
}
vf->close(vf);
vf = nullptr;
} while (unzGoToNextFile(zip) == UNZ_OK);
unzClose(zip);
return vf;
}
static VFile* OpenROM(const char* rom_path)
{
VFile* vf{};
vf = OpenROM_Archive(rom_path);
if (!vf)
vf = OpenROM_Zip(rom_path);
if (!vf)
vf = VFileOpen(rom_path, O_RDONLY);
if (!vf)
return nullptr;
if (mCoreIsCompatible(vf) == mPLATFORM_NONE)
{
vf->close(vf);
return nullptr;
}
vf->seek(vf, 0, SEEK_SET);
return vf;
}
static std::array<u8, 20> GetROMHash(VFile* rom)
{
size_t size = rom->size(rom);
u8* buffer = static_cast<u8*>(rom->map(rom, size, MAP_READ));
std::array<u8, 20> hash;
mbedtls_sha1_ret(buffer, size, hash.data());
rom->unmap(rom, buffer, size);
return hash;
}
Core::Core(int device_number) : m_device_number(device_number)
{
mLogSetDefaultLogger(&s_stub_logger);
}
Core::~Core()
{
Stop();
}
bool Core::Start(u64 gc_ticks)
{
if (IsStarted())
return false;
Common::ScopeGuard start_guard{[&] { Stop(); }};
VFile* rom{};
Common::ScopeGuard rom_guard{[&] {
if (rom)
rom->close(rom);
}};
m_rom_path = Config::Get(Config::MAIN_GBA_ROM_PATHS[m_device_number]);
if (!m_rom_path.empty())
{
rom = OpenROM(m_rom_path.c_str());
if (!rom)
{
PanicAlertFmtT("Error: GBA{0} failed to open the ROM in {1}", m_device_number + 1,
m_rom_path);
return false;
}
m_rom_hash = GetROMHash(rom);
}
m_core = rom ? mCoreFindVF(rom) : mCoreCreate(mPLATFORM_GBA);
if (!m_core)
{
PanicAlertFmtT("Error: GBA{0} failed to create core", m_device_number + 1);
return false;
}
m_core->init(m_core);
mCoreInitConfig(m_core, "dolphin");
mCoreConfigSetValue(&m_core->config, "idleOptimization", "detect");
mCoreConfigSetIntValue(&m_core->config, "useBios", 0);
mCoreConfigSetIntValue(&m_core->config, "skipBios", 0);
if (m_core->platform(m_core) == mPLATFORM_GBA &&
!LoadBIOS(File::GetUserPath(F_GBABIOS_IDX).c_str()))
{
return false;
}
if (rom)
{
if (!m_core->loadROM(m_core, rom))
{
PanicAlertFmtT("Error: GBA{0} failed to load the ROM in {1}", m_device_number + 1,
m_rom_path);
return false;
}
rom_guard.Dismiss();
std::array<char, 17> game_title{};
m_core->getGameTitle(m_core, game_title.data());
m_game_title = game_title.data();
m_save_path = GetSavePath(m_rom_path, m_device_number);
if (!m_save_path.empty() && !LoadSave(m_save_path.c_str()))
return false;
}
m_last_gc_ticks = gc_ticks;
m_gc_ticks_remainder = 0;
m_keys = 0;
SetSIODriver();
SetVideoBuffer();
SetSampleRates();
AddCallbacks();
SetAVStream();
SetupEvent();
m_core->reset(m_core);
m_started = true;
start_guard.Dismiss();
// Notify the host and handle a dimension change if that happened after reset()
SetVideoBuffer();
if (Config::Get(Config::MAIN_GBA_THREADS))
{
m_idle = true;
m_exit_loop = false;
m_thread = std::make_unique<std::thread>([this] { ThreadLoop(); });
}
return true;
}
void Core::Stop()
{
if (m_thread)
{
Flush();
m_exit_loop = true;
{
std::lock_guard<std::mutex> lock(m_queue_mutex);
m_command_cv.notify_one();
}
m_thread->join();
m_thread.reset();
}
if (m_core)
{
mCoreConfigDeinit(&m_core->config);
m_core->deinit(m_core);
m_core = nullptr;
}
m_started = false;
m_rom_path = {};
m_save_path = {};
m_rom_hash = {};
m_game_title = {};
}
void Core::Reset()
{
Flush();
if (!IsStarted())
return;
m_core->reset(m_core);
}
bool Core::IsStarted() const
{
return m_started;
}
void Core::SetHost(std::weak_ptr<GBAHostInterface> host)
{
m_host = std::move(host);
}
void Core::SetForceDisconnect(bool force_disconnect)
{
m_force_disconnect = force_disconnect;
}
void Core::EReaderQueueCard(std::string_view card_path)
{
Flush();
if (!IsStarted() || m_core->platform(m_core) != mPlatform::mPLATFORM_GBA)
return;
File::IOFile file(std::string(card_path), "rb");
std::vector<u8> core_state(file.GetSize());
file.ReadBytes(core_state.data(), core_state.size());
GBACartEReaderQueueCard(static_cast<::GBA*>(m_core->board), core_state.data(), core_state.size());
}
bool Core::LoadBIOS(const char* bios_path)
{
VFile* vf = VFileOpen(bios_path, O_RDONLY);
if (!vf)
{
PanicAlertFmtT("Error: GBA{0} failed to open the BIOS in {1}", m_device_number + 1, bios_path);
return false;
}
if (!m_core->loadBIOS(m_core, vf, 0))
{
PanicAlertFmtT("Error: GBA{0} failed to load the BIOS in {1}", m_device_number + 1, bios_path);
vf->close(vf);
return false;
}
return true;
}
bool Core::LoadSave(const char* save_path)
{
VFile* vf = VFileOpen(save_path, O_CREAT | O_RDWR);
if (!vf)
{
PanicAlertFmtT("Error: GBA{0} failed to open the save in {1}", m_device_number + 1, save_path);
return false;
}
if (!m_core->loadSave(m_core, vf))
{
PanicAlertFmtT("Error: GBA{0} failed to load the save in {1}", m_device_number + 1, save_path);
vf->close(vf);
return false;
}
return true;
}
void Core::SetSIODriver()
{
if (m_core->platform(m_core) != mPLATFORM_GBA)
return;
GBASIOJOYCreate(&m_sio_driver);
GBASIOSetDriver(&static_cast<::GBA*>(m_core->board)->sio, &m_sio_driver, SIO_JOYBUS);
m_sio_driver.core = this;
m_sio_driver.load = [](GBASIODriver* driver) {
static_cast<SIODriver*>(driver)->core->m_link_enabled = true;
return true;
};
m_sio_driver.unload = [](GBASIODriver* driver) {
static_cast<SIODriver*>(driver)->core->m_link_enabled = false;
return true;
};
}
void Core::SetVideoBuffer()
{
u32 width, height;
m_core->desiredVideoDimensions(m_core, &width, &height);
m_video_buffer.resize(width * height);
m_core->setVideoBuffer(m_core, m_video_buffer.data(), width);
if (auto host = m_host.lock())
host->GameChanged();
}
void Core::SetSampleRates()
{
m_core->setAudioBufferSize(m_core, SAMPLES);
blip_set_rates(m_core->getAudioChannel(m_core, 0), m_core->frequency(m_core), SAMPLE_RATE);
blip_set_rates(m_core->getAudioChannel(m_core, 1), m_core->frequency(m_core), SAMPLE_RATE);
g_sound_stream->GetMixer()->SetGBAInputSampleRates(m_device_number, SAMPLE_RATE);
}
void Core::AddCallbacks()
{
mCoreCallbacks callbacks{};
callbacks.context = this;
callbacks.keysRead = [](void* context) {
auto core = static_cast<Core*>(context);
core->m_core->setKeys(core->m_core, core->m_keys);
};
callbacks.videoFrameEnded = [](void* context) {
auto core = static_cast<Core*>(context);
if (auto host = core->m_host.lock())
host->FrameEnded(core->m_video_buffer);
};
m_core->addCoreCallbacks(m_core, &callbacks);
}
void Core::SetAVStream()
{
m_stream = {};
m_stream.core = this;
m_stream.videoDimensionsChanged = [](mAVStream* stream, unsigned width, unsigned height) {
auto core = static_cast<AVStream*>(stream)->core;
core->SetVideoBuffer();
};
m_stream.postAudioBuffer = [](mAVStream* stream, blip_t* left, blip_t* right) {
auto core = static_cast<AVStream*>(stream)->core;
std::vector<s16> buffer(SAMPLES * 2);
blip_read_samples(left, &buffer[0], SAMPLES, 1);
blip_read_samples(right, &buffer[1], SAMPLES, 1);
g_sound_stream->GetMixer()->PushGBASamples(core->m_device_number, &buffer[0], SAMPLES);
};
m_core->setAVStream(m_core, &m_stream);
}
void Core::SetupEvent()
{
m_event.context = this;
m_event.name = "Dolphin Sync";
m_event.callback = [](mTiming* timing, void* context, u32 cycles_late) {
Core* core = static_cast<Core*>(context);
if (core->m_core->platform(core->m_core) == mPLATFORM_GBA)
static_cast<::GBA*>(core->m_core->board)->earlyExit = true;
else if (core->m_core->platform(core->m_core) == mPLATFORM_GB)
static_cast<::GB*>(core->m_core->board)->earlyExit = true;
core->m_waiting_for_event = false;
};
m_event.priority = 0x80;
}
int Core::GetDeviceNumber() const
{
return m_device_number;
}
void Core::GetVideoDimensions(u32* width, u32* height) const
{
if (!IsStarted())
{
*width = GBA_VIDEO_HORIZONTAL_PIXELS;
*height = GBA_VIDEO_VERTICAL_PIXELS;
return;
}
m_core->desiredVideoDimensions(m_core, width, height);
}
std::string Core::GetGameTitle() const
{
return m_game_title;
}
void Core::SendJoybusCommand(u64 gc_ticks, int transfer_time, u8* buffer, u16 keys)
{
if (!IsStarted())
return;
Command command{};
command.ticks = gc_ticks;
command.transfer_time = transfer_time;
command.sync_only = buffer == nullptr;
if (buffer)
std::copy_n(buffer, command.buffer.size(), command.buffer.begin());
command.keys = keys;
if (m_thread)
{
std::lock_guard<std::mutex> lock(m_queue_mutex);
m_command_queue.push(command);
m_idle = false;
m_command_cv.notify_one();
}
else
{
RunCommand(command);
}
}
std::vector<u8> Core::GetJoybusResponse()
{
if (!IsStarted())
return {};
if (m_thread)
{
std::unique_lock<std::mutex> lock(m_response_mutex);
m_response_cv.wait(lock, [&] { return m_response_ready; });
}
m_response_ready = false;
return m_response;
}
void Core::Flush()
{
if (!IsStarted() || !m_thread)
return;
std::unique_lock<std::mutex> lock(m_queue_mutex);
m_response_cv.wait(lock, [&] { return m_idle; });
}
void Core::ThreadLoop()
{
Common::SetCurrentThreadName(fmt::format("GBA{}", m_device_number + 1).c_str());
std::unique_lock<std::mutex> queue_lock(m_queue_mutex);
while (true)
{
m_command_cv.wait(queue_lock, [&] { return !m_command_queue.empty() || m_exit_loop; });
if (m_exit_loop)
break;
Command command{m_command_queue.front()};
m_command_queue.pop();
queue_lock.unlock();
RunCommand(command);
queue_lock.lock();
if (m_command_queue.empty())
m_idle = true;
m_response_cv.notify_one();
}
}
void Core::RunCommand(Command& command)
{
m_keys = command.keys;
RunUntil(command.ticks);
if (!command.sync_only)
{
m_response.clear();
if (m_link_enabled && !m_force_disconnect)
{
int recvd = GBASIOJOYSendCommand(
&m_sio_driver, static_cast<GBASIOJOYCommand>(command.buffer[0]), &command.buffer[1]);
std::copy(command.buffer.begin() + 1, command.buffer.begin() + 1 + recvd,
std::back_inserter(m_response));
}
if (m_thread && !m_response_ready)
{
std::lock_guard<std::mutex> response_lock(m_response_mutex);
m_response_ready = true;
m_response_cv.notify_one();
}
else
{
m_response_ready = true;
}
}
if (command.transfer_time)
RunFor(command.transfer_time);
}
void Core::RunUntil(u64 gc_ticks)
{
if (static_cast<s64>(gc_ticks - m_last_gc_ticks) <= 0)
return;
const u64 gc_frequency = SystemTimers::GetTicksPerSecond();
const u32 core_frequency = GetCoreFrequency(m_core);
mTimingSchedule(m_core->timing, &m_event,
static_cast<s32>((gc_ticks - m_last_gc_ticks) * core_frequency / gc_frequency));
m_waiting_for_event = true;
s32 begin_time = mTimingCurrentTime(m_core->timing);
while (m_waiting_for_event)
m_core->runLoop(m_core);
s32 end_time = mTimingCurrentTime(m_core->timing);
u64 d = (static_cast<u64>(end_time - begin_time) * gc_frequency) + m_gc_ticks_remainder;
m_last_gc_ticks += d / core_frequency;
m_gc_ticks_remainder = d % core_frequency;
}
void Core::RunFor(u64 gc_ticks)
{
RunUntil(m_last_gc_ticks + gc_ticks);
}
void Core::ImportState(std::string_view state_path)
{
Flush();
if (!IsStarted())
return;
std::vector<u8> core_state(m_core->stateSize(m_core));
File::IOFile file(std::string(state_path), "rb");
if (core_state.size() != file.GetSize())
return;
file.ReadBytes(core_state.data(), core_state.size());
m_core->loadState(m_core, core_state.data());
}
void Core::ExportState(std::string_view state_path)
{
Flush();
if (!IsStarted())
return;
std::vector<u8> core_state(m_core->stateSize(m_core));
m_core->saveState(m_core, core_state.data());
File::IOFile file(std::string(state_path), "wb");
file.WriteBytes(core_state.data(), core_state.size());
}
void Core::DoState(PointerWrap& p)
{
Flush();
if (!IsStarted())
{
::Core::DisplayMessage(fmt::format("GBA{} core not started. Aborting.", m_device_number + 1),
3000);
p.SetMode(PointerWrap::MODE_VERIFY);
return;
}
bool has_rom = !m_rom_path.empty();
p.Do(has_rom);
auto old_hash = m_rom_hash;
p.Do(m_rom_hash);
auto old_title = m_game_title;
p.Do(m_game_title);
if (p.GetMode() == PointerWrap::MODE_READ &&
(has_rom != !m_rom_path.empty() ||
(has_rom && (old_hash != m_rom_hash || old_title != m_game_title))))
{
::Core::DisplayMessage(
fmt::format("Incompatible ROM state in GBA{}. Aborting load state.", m_device_number + 1),
3000);
p.SetMode(PointerWrap::MODE_VERIFY);
return;
}
p.Do(m_video_buffer);
p.Do(m_last_gc_ticks);
p.Do(m_gc_ticks_remainder);
p.Do(m_keys);
p.Do(m_link_enabled);
p.Do(m_response_ready);
p.Do(m_response);
std::vector<u8> core_state;
core_state.resize(m_core->stateSize(m_core));
if (p.GetMode() == PointerWrap::MODE_WRITE || p.GetMode() == PointerWrap::MODE_VERIFY)
{
m_core->saveState(m_core, core_state.data());
}
p.Do(core_state);
if (p.GetMode() == PointerWrap::MODE_READ && m_core->stateSize(m_core) == core_state.size())
{
m_core->loadState(m_core, core_state.data());
if (auto host = m_host.lock())
host->FrameEnded(m_video_buffer);
}
}
bool Core::GetRomInfo(const char* rom_path, std::array<u8, 20>& hash, std::string& title)
{
VFile* rom = OpenROM(rom_path);
if (!rom)
return false;
hash = GetROMHash(rom);
mCore* core = mCoreFindVF(rom);
if (!core)
{
rom->close(rom);
return false;
}
core->init(core);
if (!core->loadROM(core, rom))
{
rom->close(rom);
return false;
}
std::array<char, 17> game_title{};
core->getGameTitle(core, game_title.data());
title = game_title.data();
core->deinit(core);
return true;
}
std::string Core::GetSavePath(std::string_view rom_path, int device_number)
{
std::string save_path =
fmt::format("{}-{}.sav", rom_path.substr(0, rom_path.find_last_of('.')), device_number + 1);
if (!Config::Get(Config::MAIN_GBA_SAVES_IN_ROM_PATH))
{
save_path =
File::GetUserPath(D_GBASAVES_IDX) + save_path.substr(save_path.find_last_of("\\/") + 1);
}
return save_path;
}
} // namespace HW::GBA

View File

@ -0,0 +1,129 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <queue>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#define PYCPARSE // Remove static functions from the header
#include <mgba/core/interface.h>
#undef PYCPARSE
#include <mgba/core/core.h>
#include <mgba/gba/interface.h>
#include "Common/CommonTypes.h"
class GBAHostInterface;
class PointerWrap;
namespace HW::GBA
{
class Core;
struct SIODriver : GBASIODriver
{
Core* core;
};
struct AVStream : mAVStream
{
Core* core;
};
class Core final
{
public:
explicit Core(int device_number);
~Core();
bool Start(u64 gc_ticks);
void Stop();
void Reset();
bool IsStarted() const;
void SetHost(std::weak_ptr<GBAHostInterface> host);
void SetForceDisconnect(bool force_disconnect);
void EReaderQueueCard(std::string_view card_path);
int GetDeviceNumber() const;
void GetVideoDimensions(u32* width, u32* height) const;
std::string GetGameTitle() const;
void SendJoybusCommand(u64 gc_ticks, int transfer_time, u8* buffer, u16 keys);
std::vector<u8> GetJoybusResponse();
void ImportState(std::string_view state_path);
void ExportState(std::string_view state_path);
void DoState(PointerWrap& p);
static bool GetRomInfo(const char* rom_path, std::array<u8, 20>& hash, std::string& title);
static std::string GetSavePath(std::string_view rom_path, int device_number);
private:
void ThreadLoop();
void RunUntil(u64 gc_ticks);
void RunFor(u64 gc_ticks);
void Flush();
struct Command
{
u64 ticks;
int transfer_time;
bool sync_only;
std::array<u8, 6> buffer;
u16 keys;
};
void RunCommand(Command& command);
bool LoadBIOS(const char* bios_path);
bool LoadSave(const char* save_path);
void SetSIODriver();
void SetVideoBuffer();
void SetSampleRates();
void AddCallbacks();
void SetAVStream();
void SetupEvent();
const int m_device_number;
bool m_started = false;
std::string m_rom_path;
std::string m_save_path;
std::array<u8, 20> m_rom_hash{};
std::string m_game_title;
mCore* m_core{};
mTimingEvent m_event{};
bool m_waiting_for_event = false;
SIODriver m_sio_driver{};
AVStream m_stream{};
std::vector<u32> m_video_buffer;
u64 m_last_gc_ticks = 0;
u64 m_gc_ticks_remainder = 0;
u16 m_keys = 0;
bool m_link_enabled = false;
bool m_force_disconnect = false;
std::weak_ptr<GBAHostInterface> m_host;
std::unique_ptr<std::thread> m_thread;
bool m_exit_loop = false;
bool m_idle = false;
std::mutex m_queue_mutex;
std::condition_variable m_command_cv;
std::queue<Command> m_command_queue;
std::mutex m_response_mutex;
std::condition_variable m_response_cv;
bool m_response_ready = false;
std::vector<u8> m_response;
};
} // namespace HW::GBA

View File

@ -264,6 +264,7 @@
<ClInclude Include="Core\HW\EXI\EXI_DeviceMemoryCard.h" />
<ClInclude Include="Core\HW\EXI\EXI_DeviceMic.h" />
<ClInclude Include="Core\HW\EXI\EXI.h" />
<ClInclude Include="Core\HW\GBACore.h" />
<ClInclude Include="Core\HW\GBAPad.h" />
<ClInclude Include="Core\HW\GBAPadEmu.h" />
<ClInclude Include="Core\HW\GCKeyboard.h" />
@ -846,6 +847,7 @@
<ClCompile Include="Core\HW\EXI\EXI_DeviceMemoryCard.cpp" />
<ClCompile Include="Core\HW\EXI\EXI_DeviceMic.cpp" />
<ClCompile Include="Core\HW\EXI\EXI.cpp" />
<ClCompile Include="Core\HW\GBACore.cpp" />
<ClCompile Include="Core\HW\GBAPad.cpp" />
<ClCompile Include="Core\HW\GBAPadEmu.cpp" />
<ClCompile Include="Core\HW\GCKeyboard.cpp" />