GBA: ROM, BIOS and saves through the frontend's VFS

The GBA core's files open as an mGBA VFile over a libretro-common
filestream (GBAVFile.c). A read-only map of a mapped file lends the
mapping itself, so the ROM and the BIOS are read where they lie; a save
is mapped into a buffer of its own and written back when mGBA syncs it.
A ROM in an archive or a zip is read or inflated straight into the
memory file mGBA maps, with no buffer in between.

The state and save import/export, e-Reader card queueing, ROM info and
forced disconnect entry points nothing calls are gone.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-11-07 06:27:34 +01:00
1 parent 4d9402dd6e
commit 734ceaa90c
5 files changed
+254 -139

No files matched your search

+2 -1
View File
@@ -1025,7 +1025,8 @@ DOLPHIN_SOURCES_C += \
$(CORE_DIR)/Source/Core/Common/CallGate.c \
$(CORE_DIR)/Source/Core/Common/FrameClock.c \
$(CORE_DIR)/Source/Core/Common/ParallelFor.c \
$(CORE_DIR)/Source/Core/Common/Quiesce.c
$(CORE_DIR)/Source/Core/Common/Quiesce.c \
$(CORE_DIR)/Source/Core/Core/HW/GBAVFile.c
DOLPHIN_SOURCES_CXX += \
$(CORE_DIR)/Source/Core/Common/Assembler/AssemblerShared.cpp \
$(CORE_DIR)/Source/Core/Common/Assembler/AssemblerTables.cpp \
+23 -130
View File
@@ -18,12 +18,12 @@
#include "AudioCommon/FieldAudio.h"
#include "Core/HW/GBAVFile.h"
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/Config/Config.h"
#include "Common/Crypto/SHA1.h"
#include "Common/FileUtil.h"
#include "Common/IOFile.h"
#include "Common/Logging/Log.h"
#include "Common/MinizipUtil.h"
#include "Common/ScopeGuard.h"
@@ -35,10 +35,6 @@
#include "Core/Host.h"
#include "Core/System.h"
#ifdef ANDROID
#include "jni/AndroidCommon/AndroidCommon.h"
#endif
namespace HW::GBA
{
namespace
@@ -65,14 +61,20 @@ static VFile* OpenROM_Archive(const char* path)
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);
// read straight into the memory file the core will map
const size_t size = static_cast<size_t>(vf_archive->size(vf_archive));
vf = VFileMemChunk(nullptr, size);
if (vf)
{
void* dst = vf->map(vf, size, MAP_WRITE);
vf_archive->seek(vf_archive, 0, SEEK_SET);
if (!dst || vf_archive->read(vf_archive, dst, size) != static_cast<ssize_t>(size))
{
vf->close(vf);
vf = nullptr;
}
}
vf_archive->close(vf_archive);
vf = VFileMemChunk(buffer.data(), size);
}
archive->close(archive);
return vf;
@@ -96,12 +98,13 @@ static VFile* OpenROM_Zip(const char* path)
if (mz_zip_reader_entry_get_info(zip_reader, &info) != MZ_OK || !info->uncompressed_size)
continue;
std::vector<u8> buffer(info->uncompressed_size);
if (!Common::ReadFileFromZip(zip_reader, &buffer))
// inflated straight into the memory file the core will map
vf = VFileMemChunk(nullptr, static_cast<size_t>(info->uncompressed_size));
if (!vf)
continue;
vf = VFileMemChunk(buffer.data(), info->uncompressed_size);
if (mCoreIsCompatible(vf) != mPLATFORM_NONE)
void* dst = vf->map(vf, static_cast<size_t>(info->uncompressed_size), MAP_WRITE);
if (dst && Common::ReadFileFromZip(zip_reader, static_cast<u8*>(dst), info->uncompressed_size) &&
mCoreIsCompatible(vf) != mPLATFORM_NONE)
{
vf->seek(vf, 0, SEEK_SET);
break;
@@ -115,11 +118,7 @@ static VFile* OpenROM_Zip(const char* path)
static VFile* OpenReadOnlyFile(const char* path)
{
#ifdef ANDROID
if (IsPathAndroidContent(path))
return VFileFromFD(OpenAndroidContent(path, "r"));
#endif
return VFileOpen(path, O_RDONLY);
return gba_vfile_open(path, 0);
}
static VFile* OpenROM(const char* rom_path)
@@ -337,23 +336,6 @@ 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 (!GetCoreInfo().has_ereader)
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 = OpenReadOnlyFile(bios_path);
@@ -375,7 +357,7 @@ bool Core::LoadBIOS(const char* bios_path)
bool Core::LoadSave(const char* save_path)
{
VFile* vf = VFileOpen(save_path, O_CREAT | O_RDWR);
VFile* vf = gba_vfile_open(save_path, 1);
if (!vf)
{
PanicAlertFmtT("Error: GBA{0} failed to open the save in {1}", m_device_number + 1, save_path);
@@ -592,7 +574,7 @@ void Core::HandleEvent(SyncEvent event)
if (event.event_type != SyncEventType::RunCommand)
return;
if (m_link_enabled && !m_force_disconnect)
if (m_link_enabled)
{
m_response_size =
u8(GBASIOJOYSendCommand(&m_sio_driver, m_joybus_command, m_joybus_buffer.data()));
@@ -635,66 +617,6 @@ 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::ImportSave(std::string_view save_path)
{
Flush();
if (!IsStarted())
return;
File::IOFile file(std::string(save_path), "rb");
std::vector<u8> save_file(file.GetSize());
if (!file.ReadBytes(save_file.data(), save_file.size()))
return;
m_core->savedataRestore(m_core, save_file.data(), save_file.size(), true);
m_core->reset(m_core);
}
void Core::ExportSave(std::string_view save_path)
{
Flush();
if (!IsStarted())
return;
File::IOFile file(std::string(save_path), "wb");
void* sram = nullptr;
size_t size = m_core->savedataClone(m_core, &sram);
if (!sram)
return;
file.WriteBytes(sram, size);
free(sram);
}
void Core::DoState(PointerWrap& p)
{
Flush();
@@ -753,35 +675,6 @@ void Core::DoState(PointerWrap& p)
}
}
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;
}
mGameInfo info;
core->getGameInfo(core, &info);
title = info.title;
core->deinit(core);
return true;
}
std::string Core::GetSavePath(std::string_view rom_path, int device_number)
{
// This is done to make the save file compatible with the save from the mGBA core in RA as it
-8
View File
@@ -78,8 +78,6 @@ public:
CoreInfo GetCoreInfo() const;
void SetHost(std::weak_ptr<GBAHostInterface> host);
void SetForceDisconnect(bool force_disconnect);
void EReaderQueueCard(std::string_view card_path);
void SyncJoybus(u64 gc_ticks, u16 keys);
void SendJoybusCommand(u64 gc_ticks, int transfer_time, u8* buffer, u16 keys);
@@ -97,13 +95,8 @@ public:
u32 GetAudioSampleRate() const { return m_core->audioSampleRate(m_core); }
int GetDeviceNumber() const { return m_device_number; }
void ImportState(std::string_view state_path);
void ExportState(std::string_view state_path);
void ImportSave(std::string_view save_path);
void ExportSave(std::string_view save_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:
@@ -154,7 +147,6 @@ private:
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;
+202
View File
@@ -0,0 +1,202 @@
/* Copyright 2026 Dolphin Emulator Project
* SPDX-License-Identifier: GPL-2.0-or-later */
/* See GBAVFile.h. */
#include <stdlib.h>
#include <string.h>
#include <mgba-util/vfs.h>
#include <streams/file_stream.h>
#include "Core/HW/GBAVFile.h"
/* A writable map: its own buffer, until it is unmapped. */
struct gba_vfile_map
{
struct gba_vfile_map *next;
size_t size;
/* the octets follow */
};
struct gba_vfile
{
struct VFile d;
RFILE *file;
const unsigned char *map; /* the whole file, lent, or NULL */
int64_t map_size;
struct gba_vfile_map *maps; /* writable maps outstanding */
};
static bool gba_vfile_close(struct VFile *vf)
{
struct gba_vfile *f = (struct gba_vfile*)vf;
int ok = filestream_close(f->file) == 0;
while (f->maps)
{
struct gba_vfile_map *next = f->maps->next;
free(f->maps);
f->maps = next;
}
free(f);
return ok != 0;
}
static off_t gba_vfile_seek(struct VFile *vf, off_t offset, int whence)
{
struct gba_vfile *f = (struct gba_vfile*)vf;
int mode;
switch (whence)
{
case SEEK_CUR:
mode = RETRO_VFS_SEEK_POSITION_CURRENT;
break;
case SEEK_END:
mode = RETRO_VFS_SEEK_POSITION_END;
break;
default:
mode = RETRO_VFS_SEEK_POSITION_START;
break;
}
if (filestream_seek(f->file, (int64_t)offset, mode) < 0)
return -1;
return (off_t)filestream_tell(f->file);
}
static ssize_t gba_vfile_read(struct VFile *vf, void *buffer, size_t size)
{
struct gba_vfile *f = (struct gba_vfile*)vf;
return (ssize_t)filestream_read(f->file, buffer, (int64_t)size);
}
static ssize_t gba_vfile_write(struct VFile *vf, const void *buffer, size_t size)
{
struct gba_vfile *f = (struct gba_vfile*)vf;
return (ssize_t)filestream_write(f->file, buffer, (int64_t)size);
}
static ssize_t gba_vfile_size(struct VFile *vf)
{
struct gba_vfile *f = (struct gba_vfile*)vf;
return (ssize_t)filestream_get_size(f->file);
}
static void *gba_vfile_map(struct VFile *vf, size_t size, int flags)
{
struct gba_vfile *f = (struct gba_vfile*)vf;
struct gba_vfile_map *m;
unsigned char *data;
int64_t have;
if (!size)
return NULL;
/* Read-only, and all there: the mapping, lent. mGBA writes to a
* read-only map nowhere; a ROM it patches is copied first. */
if (!(flags & MAP_WRITE) && f->map && (int64_t)size <= f->map_size)
return (void*)f->map;
m = (struct gba_vfile_map*)malloc(sizeof(*m) + size);
if (!m)
return NULL;
data = (unsigned char*)(m + 1);
have = filestream_get_size(f->file);
if (have < 0)
have = 0;
if ((uint64_t)have > size)
have = (int64_t)size;
if (have && (filestream_seek(f->file, 0, RETRO_VFS_SEEK_POSITION_START) < 0
|| filestream_read(f->file, data, have) != have))
{
free(m);
return NULL;
}
/* what lies past the end of the file reads as zeroes, as a mapping
* of a file grown to the size would */
memset(data + have, 0, size - (size_t)have);
m->size = size;
m->next = f->maps;
f->maps = m;
return data;
}
static void gba_vfile_unmap(struct VFile *vf, void *memory, size_t size)
{
struct gba_vfile *f = (struct gba_vfile*)vf;
struct gba_vfile_map **link = &f->maps;
(void)size;
while (*link)
{
if ((void*)(*link + 1) == memory)
{
struct gba_vfile_map *m = *link;
*link = m->next;
free(m);
return;
}
link = &(*link)->next;
}
/* the lent mapping: nothing to give back */
}
static void gba_vfile_truncate(struct VFile *vf, size_t size)
{
struct gba_vfile *f = (struct gba_vfile*)vf;
filestream_truncate(f->file, (int64_t)size);
}
/* What was written to a map, written to the file. */
static bool gba_vfile_sync(struct VFile *vf, void *buffer, size_t size)
{
struct gba_vfile *f = (struct gba_vfile*)vf;
if (buffer && size)
{
if (filestream_seek(f->file, 0, RETRO_VFS_SEEK_POSITION_START) < 0
|| filestream_write(f->file, buffer, (int64_t)size) != (int64_t)size)
return false;
}
return filestream_flush(f->file) == 0;
}
struct VFile *gba_vfile_open(const char *path, int writable)
{
struct gba_vfile *f;
RFILE *file;
unsigned mode;
if (!path || !*path)
return NULL;
if (!writable)
mode = RETRO_VFS_FILE_ACCESS_READ;
else if (filestream_exists(path))
mode = RETRO_VFS_FILE_ACCESS_READ_WRITE | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING;
else
mode = RETRO_VFS_FILE_ACCESS_READ_WRITE;
file = filestream_open(path, mode, writable ? RETRO_VFS_FILE_ACCESS_HINT_NONE
: RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS);
if (!file)
return NULL;
f = (struct gba_vfile*)calloc(1, sizeof(*f));
if (!f)
{
filestream_close(file);
return NULL;
}
f->file = file;
f->map = writable ? NULL : filestream_get_mapped_ptr(file, &f->map_size);
f->d.close = gba_vfile_close;
f->d.seek = gba_vfile_seek;
f->d.read = gba_vfile_read;
f->d.readline = VFileReadline;
f->d.write = gba_vfile_write;
f->d.map = gba_vfile_map;
f->d.unmap = gba_vfile_unmap;
f->d.truncate = gba_vfile_truncate;
f->d.size = gba_vfile_size;
f->d.sync = gba_vfile_sync;
return &f->d;
}
+27
View File
@@ -0,0 +1,27 @@
/* Copyright 2026 Dolphin Emulator Project
* SPDX-License-Identifier: GPL-2.0-or-later */
/* An mGBA VFile over a libretro-common filestream, so the GBA core opens
* its ROM, BIOS and saves through the frontend's VFS like everything
* else. A read-only map of a mapped file lends the mapping itself: the
* ROM and the BIOS are read where they lie. A writable map is a buffer
* of its own, read in once and written back on sync. */
#ifndef GBA_VFILE_H
#define GBA_VFILE_H
#ifdef __cplusplus
extern "C" {
#endif
struct VFile;
/* writable: open for reading and writing, made if it is not there yet.
* Returns NULL when the file cannot be opened. */
struct VFile* gba_vfile_open(const char* path, int writable);
#ifdef __cplusplus
}
#endif
#endif