Merge branch 'master' (early part) into medusa

This commit is contained in:
Vicki Pfau 2021-06-11 20:02:02 -07:00
commit cc06ae4690
88 changed files with 6880 additions and 3252 deletions

View File

@ -36,6 +36,7 @@ Misc:
0.9.0: (Future)
Features:
- e-Reader card scanning
- New tool for converting between different save game formats
- WebP and APNG recording
- Separate overrides for GBC games that can also run on SGB or regular GB
- Game Boy Player features can be enabled by default for all compatible games
@ -84,6 +85,7 @@ Emulation fixes:
- GBA SIO: Fix copying Normal mode transfer values
- GBA SIO: Fix Normal mode being totally broken (fixes mgba.io/i/1800)
- GBA SIO: Fix deseralizing SIO registers
- GBA SIO: Fix hanging on starting a second multiplayer window (fixes mgba.io/i/854)
- GBA Video: Latch scanline at end of Hblank (fixes mgba.io/i/1319)
- GBA Video: Fix Hblank timing
- GBA Video: Implement green swap (fixes mgba.io/i/1609)
@ -93,6 +95,7 @@ Emulation fixes:
Other fixes:
- 3DS: Fix thread cleanup
- All: Improve export headers (fixes mgba.io/i/1738)
- Cheats: Fix indirect write cheats (fixes mgba.io/i/2026)
- CMake: Fix build with downstream minizip that exports incompatible symbols
- CMake: Link with correct OpenGL library (fixes mgba.io/i/1872)
- Core: Ensure ELF regions can be written before trying
@ -129,8 +132,10 @@ Misc:
- Core: Add shutdown callback
- Core: Rework thread state synchronization
- Core: Improve support for ROM patch cheats, supporting disabling overlapping patches
- Core: Adding to library is now recursive
- GB: Allow pausing event loop while CPU is blocked
- GB: Add support for sleep and shutdown callbacks
- GB: Redo double speed emulation (closes mgba.io/i/1515)
- GB Core: Return the current number of banks for ROM/SRAM, not theoretical max
- GB I/O: Implement preliminary support for PCM12/PCM34 (closes mgba.io/i/1468)
- GBA: Allow pausing event loop while CPU is blocked
@ -156,6 +161,7 @@ Misc:
- Qt: Unify monospace font usage
- Qt: Add button to jump to log settings
- Qt: Use relative paths in portable mode when applicable (fixes mgba.io/i/838)
- Qt: Better initial shortcut editor column sizes
- SDL: Fall back to sw blit if OpenGL init fails
- Util: Reset vector size on deinit
- VFS: Change semantics of VFile.sync on mapped files (fixes mgba.io/i/1730)

View File

@ -5,7 +5,8 @@ medusa is an emulator for running Nintendo DS, Game Boy Advance and Game Boy gam
Up-to-date news and downloads can be found at [mgba.io](https://mgba.io/).
[![Build status](https://travis-ci.org/mgba-emu/mgba.svg?branch=medusa)](https://travis-ci.org/mgba-emu/mgba)
[![Build status](https://buildbot.mgba.io/badges/build-win32.svg)](https://buildbot.mgba.io)
[![Translation status](https://hosted.weblate.org/widgets/mgba/-/svg-badge.svg)](https://hosted.weblate.org/engage/mgba)
Features
--------
@ -37,6 +38,7 @@ Features
- Configurable emulation rewinding.
- Support for loading and exporting GameShark and Action Replay snapshots.
- Cores available for RetroArch/Libretro and OpenEmu.
- Community-provided translations for several languages via [Weblate](https://hosted.weblate.org/engage/mgba).
- Many, many smaller things.
#### Game Boy mappers
@ -297,7 +299,7 @@ Missing features on DS are
Copyright
---------
medusa is Copyright © 2013 2020 Jeffrey Pfau. It is distributed under the [Mozilla Public License version 2.0](https://www.mozilla.org/MPL/2.0/). A copy of the license is available in the distributed LICENSE file.
medusa is Copyright © 2013 2021 Jeffrey Pfau. It is distributed under the [Mozilla Public License version 2.0](https://www.mozilla.org/MPL/2.0/). A copy of the license is available in the distributed LICENSE file.
medusa contains the following third-party libraries:

View File

@ -6,6 +6,7 @@ mGBA ist ein Emulator für Game Boy Advance-Spiele. Das Ziel von mGBA ist, schne
Aktuelle Neuigkeiten und Downloads findest Du auf [mgba.io](https://mgba.io).
[![Build-Status](https://travis-ci.org/mgba-emu/mgba.svg?branch=master)](https://travis-ci.org/mgba-emu/mgba)
[![Status der Übersetzungen](https://hosted.weblate.org/widgets/mgba/-/svg-badge.svg)](https://hosted.weblate.org/engage/mgba)
Features
--------
@ -36,6 +37,7 @@ Features
- Einstellbare Rücklauf-Funktion.
- Unterstützung für das Laden und Exportieren von GameShark- und Action Replay-Abbildern.
- Verfügbare Cores für RetroArch/Libretro und OpenEmu.
- Übersetzungen für mehrere Sprachen über [Weblate](https://hosted.weblate.org/engage/mgba).
- Viele, viele kleinere Dinge.
### Game Boy-Mapper
@ -240,7 +242,7 @@ Fußnoten
Copyright
---------
Copyright für mGBA © 2013 2020 Jeffrey Pfau. mGBA wird unter der [Mozilla Public License version 2.0](https://www.mozilla.org/MPL/2.0/) veröffentlicht. Eine Kopie der Lizenz ist in der mitgelieferten Datei LICENSE verfügbar.
Copyright für mGBA © 2013 2021 Jeffrey Pfau. mGBA wird unter der [Mozilla Public License version 2.0](https://www.mozilla.org/MPL/2.0/) veröffentlicht. Eine Kopie der Lizenz ist in der mitgelieferten Datei LICENSE verfügbar.
mGBA beinhaltet die folgenden Bibliotheken von Drittanbietern:

View File

@ -5,7 +5,8 @@ mGBA es un emulador para juegos de Game Boy Advance. Su objetivo es ser más rá
Las noticias actualizadas y las descargas se encuentran en [mgba.io](https://mgba.io/).
[![Estado de compilación](https://travis-ci.org/mgba-emu/mgba.svg?branch=master)](https://travis-ci.org/mgba-emu/mgba)
[![Estado de la compilación](https://travis-ci.org/mgba-emu/mgba.svg?branch=master)](https://travis-ci.org/mgba-emu/mgba)
[![Estado de la traducción](https://hosted.weblate.org/widgets/mgba/-/svg-badge.svg)](https://hosted.weblate.org/engage/mgba)
Características
--------
@ -36,6 +37,7 @@ Características
- Retroceso configurable.
- Soporte para cargar y exportar instantáneas de GameShark y Action Replay.
- Núcleos disponibles para RetroArch/Libretro y OpenEmu.
- Traducciones de la comunidad a través de [Weblate](https://hosted.weblate.org/engage/mgba).
- Otras cosas más pequeñas.
#### Mappers (controladores de memoria) soportados
@ -240,7 +242,7 @@ Notas a pie
Copyright
---------
mGBA es Copyright © 2013 2020 Jeffrey Pfau. Es distribuído bajo la [licencia pública de Mozilla (Mozilla Public License) version 2.0](https://www.mozilla.org/MPL/2.0/). Una copia de la licencia está disponible en el archivo LICENSE.
mGBA es Copyright © 2013 2021 Jeffrey Pfau. Es distribuído bajo la [licencia pública de Mozilla (Mozilla Public License) version 2.0](https://www.mozilla.org/MPL/2.0/). Una copia de la licencia está disponible en el archivo LICENSE.
mGBA contiene las siguientes bibliotecas de terceros:

View File

@ -119,6 +119,33 @@ static inline int reduceFraction(int* num, int* den) {
return n;
}
#define TYPE_GENERICIZE(MACRO) \
MACRO(int, Int) \
MACRO(unsigned, UInt)
#define LOCK_ASPECT_RATIO(T, t) \
static inline void lockAspectRatio ## t(T refW, T refH, T* w, T* h) { \
if (*w * refH > *h * refW) { \
*w = *h * refW / refH; \
} else if (*w * refH < *h * refW) { \
*h = *w * refH / refW; \
} \
}
TYPE_GENERICIZE(LOCK_ASPECT_RATIO)
#undef LOCK_ASPECT_RATIO
#define LOCK_INTEGER_RATIO(T, t) \
static inline void lockIntegerRatio ## t(T ref, T* val) { \
if (*val >= ref) { \
*val -= *val % ref; \
} \
}
TYPE_GENERICIZE(LOCK_INTEGER_RATIO)
#undef LOCK_INTEGER_RATIO
#undef TYPE_GENERICIZE
CXX_GUARD_END
#endif

View File

@ -79,7 +79,7 @@ struct VFile* VFileFIFO(struct CircleBuffer* backing);
struct VDir* VDirOpen(const char* path);
struct VDir* VDirOpenArchive(const char* path);
#if defined(USE_LIBZIP) || defined(USE_ZLIB)
#if defined(USE_LIBZIP) || defined(USE_MINIZIP)
struct VDir* VDirOpenZip(const char* path, int flags);
#endif

View File

@ -191,6 +191,7 @@ void mCoreTakeScreenshot(struct mCore* core);
struct mCore* mCoreFindVF(struct VFile* vf);
enum mPlatform mCoreIsCompatible(struct VFile* vf);
struct mCore* mCoreCreate(enum mPlatform);
bool mCoreSaveStateNamed(struct mCore* core, struct VFile* vf, int flags);
bool mCoreLoadStateNamed(struct mCore* core, struct VFile* vf, int flags);

View File

@ -35,7 +35,7 @@ void mLibraryDestroy(struct mLibrary*);
struct VDir;
struct VFile;
void mLibraryLoadDirectory(struct mLibrary* library, const char* base);
void mLibraryLoadDirectory(struct mLibrary* library, const char* base, bool recursive);
void mLibraryClear(struct mLibrary* library);
size_t mLibraryCount(struct mLibrary* library, const struct mLibraryEntry* constraints);

View File

@ -13,8 +13,8 @@ CXX_GUARD_START
#include <mgba/core/core.h>
#include <mgba/internal/gb/gb.h>
extern const uint32_t GB_SAVESTATE_MAGIC;
extern const uint32_t GB_SAVESTATE_VERSION;
extern MGBA_EXPORT const uint32_t GBSavestateMagic;
extern MGBA_EXPORT const uint32_t GBSavestateVersion;
mLOG_DECLARE_CATEGORY(GB_STATE);

View File

@ -14,8 +14,8 @@ CXX_GUARD_START
#include <mgba/internal/gba/gba.h>
#include <mgba/internal/gb/serialize.h>
extern const uint32_t GBA_SAVESTATE_MAGIC;
extern const uint32_t GBA_SAVESTATE_VERSION;
extern MGBA_EXPORT const uint32_t GBASavestateMagic;
extern MGBA_EXPORT const uint32_t GBASavestateVersion;
mLOG_DECLARE_CATEGORY(GBA_STATE);

View File

@ -133,6 +133,7 @@ struct SM83Core {
uint16_t index;
int tMultiplier;
int32_t cycles;
int32_t nextEvent;
enum SM83ExecutionState executionState;

View File

@ -666,7 +666,7 @@ void mCheatRefresh(struct mCheatDevice* device, struct mCheatSet* cheats) {
break;
case CHEAT_ASSIGN_INDIRECT:
value = operand;
address = _readMem(device->p, address + cheat->addressOffset, 4);
address = _readMem(device->p, address, 4) + cheat->addressOffset;
performAssignment = true;
break;
case CHEAT_AND:

View File

@ -212,14 +212,20 @@ void mCoreConfigDirectory(char* out, size_t outLength) {
}
}
#ifdef _WIN32
wchar_t wpath[MAX_PATH];
wchar_t wprojectName[MAX_PATH];
wchar_t* home;
WCHAR wpath[MAX_PATH];
WCHAR wprojectName[MAX_PATH];
WCHAR* home;
MultiByteToWideChar(CP_UTF8, 0, projectName, -1, wprojectName, MAX_PATH);
SHGetKnownFolderPath(&FOLDERID_RoamingAppData, 0, NULL, &home);
StringCchPrintfW(wpath, MAX_PATH, L"%ws\\%ws", home, wprojectName);
CoTaskMemFree(home);
CreateDirectoryW(wpath, NULL);
if (PATH_SEP[0] != '\\') {
WCHAR* pathSep;
for (pathSep = wpath; pathSep = wcschr(pathSep, L'\\');) {
pathSep[0] = PATH_SEP[0];
}
}
WideCharToMultiByte(CP_UTF8, 0, wpath, -1, out, outLength, 0, 0);
#elif defined(PSP2)
snprintf(out, outLength, "ux0:data/%s", projectName);
@ -256,8 +262,14 @@ void mCoreConfigPortablePath(char* out, size_t outLength) {
HMODULE hModule = GetModuleHandleW(NULL);
GetModuleFileNameW(hModule, wpath, MAX_PATH);
PathRemoveFileSpecW(wpath);
if (PATH_SEP[0] != '\\') {
WCHAR* pathSep;
for (pathSep = wpath; pathSep = wcschr(pathSep, L'\\');) {
pathSep[0] = PATH_SEP[0];
}
}
WideCharToMultiByte(CP_UTF8, 0, wpath, -1, out, outLength, 0, 0);
StringCchCatA(out, outLength, "\\portable.ini");
StringCchCatA(out, outLength, PATH_SEP "portable.ini");
#elif defined(PSP2) || defined(GEKKO) || defined(__SWITCH__) || defined(_3DS)
out[0] = '\0';
#else

View File

@ -80,6 +80,19 @@ enum mPlatform mCoreIsCompatible(struct VFile* vf) {
return mPLATFORM_NONE;
}
struct mCore* mCoreCreate(enum mPlatform platform) {
const struct mCoreFilter* filter;
for (filter = &_filters[0]; filter->filter; ++filter) {
if (filter->platform == platform) {
break;
}
}
if (filter->open) {
return filter->open();
}
return NULL;
}
#if !defined(MINIMAL_CORE) || MINIMAL_CORE < 2
#include <mgba-util/png-io.h>

View File

@ -42,7 +42,7 @@ struct mLibrary {
static void _mLibraryDeleteEntry(struct mLibrary* library, struct mLibraryEntry* entry);
static void _mLibraryInsertEntry(struct mLibrary* library, struct mLibraryEntry* entry);
static void _mLibraryAddEntry(struct mLibrary* library, const char* filename, const char* base, struct VFile* vf);
static bool _mLibraryAddEntry(struct mLibrary* library, const char* filename, const char* base, struct VFile* vf);
static void _bindConstraints(sqlite3_stmt* statement, const struct mLibraryEntry* constraints) {
if (!constraints) {
@ -212,7 +212,7 @@ void mLibraryDestroy(struct mLibrary* library) {
free(library);
}
void mLibraryLoadDirectory(struct mLibrary* library, const char* base) {
void mLibraryLoadDirectory(struct mLibrary* library, const char* base, bool recursive) {
struct VDir* dir = VDirOpenArchive(base);
if (!dir) {
dir = VDirOpen(base);
@ -248,44 +248,55 @@ void mLibraryLoadDirectory(struct mLibrary* library, const char* base) {
dir->rewind(dir);
struct VDirEntry* dirent = dir->listNext(dir);
while (dirent) {
struct VFile* vf = dir->openFile(dir, dirent->name(dirent), O_RDONLY);
if (!vf) {
dirent = dir->listNext(dir);
continue;
const char* name = dirent->name(dirent);
struct VFile* vf = dir->openFile(dir, name, O_RDONLY);
bool wasAdded = false;
if (vf) {
wasAdded = _mLibraryAddEntry(library, name, base, vf);
}
if (!wasAdded && name[0] != '.') {
char newBase[PATH_MAX];
snprintf(newBase, sizeof(newBase), "%s" PATH_SEP "%s", base, name);
if (recursive) {
mLibraryLoadDirectory(library, newBase, recursive);
} else if (dirent->type(dirent) == VFS_FILE) {
mLibraryLoadDirectory(library, newBase, true); // This will add as an archive
}
}
_mLibraryAddEntry(library, dirent->name(dirent), base, vf);
dirent = dir->listNext(dir);
}
dir->close(dir);
sqlite3_exec(library->db, "COMMIT;", NULL, NULL, NULL);
}
void _mLibraryAddEntry(struct mLibrary* library, const char* filename, const char* base, struct VFile* vf) {
struct mCore* core;
bool _mLibraryAddEntry(struct mLibrary* library, const char* filename, const char* base, struct VFile* vf) {
if (!vf) {
return;
return false;
}
core = mCoreFindVF(vf);
if (core) {
struct mLibraryEntry entry;
memset(&entry, 0, sizeof(entry));
core->init(core);
core->loadROM(core, vf);
core->getGameTitle(core, entry.internalTitle);
core->getGameCode(core, entry.internalCode);
core->checksum(core, &entry.crc32, mCHECKSUM_CRC32);
entry.platform = core->platform(core);
entry.title = NULL;
entry.base = base;
entry.filename = filename;
entry.filesize = vf->size(vf);
_mLibraryInsertEntry(library, &entry);
// Note: this destroys the VFile
core->deinit(core);
} else {
struct mCore* core = mCoreFindVF(vf);
if (!core) {
vf->close(vf);
return false;
}
struct mLibraryEntry entry;
memset(&entry, 0, sizeof(entry));
core->init(core);
core->loadROM(core, vf);
core->getGameTitle(core, entry.internalTitle);
core->getGameCode(core, entry.internalCode);
core->checksum(core, &entry.crc32, mCHECKSUM_CRC32);
entry.platform = core->platform(core);
entry.title = NULL;
entry.base = base;
entry.filename = filename;
entry.filesize = vf->size(vf);
_mLibraryInsertEntry(library, &entry);
// Note: this destroys the VFile
core->deinit(core);
return true;
}
static void _mLibraryInsertEntry(struct mLibrary* library, struct mLibraryEntry* entry) {

View File

@ -282,7 +282,11 @@ static void* _loadPNGState(struct mCore* core, struct VFile* vf, struct mStateEx
success = success && PNGReadFooter(png, end);
PNGReadClose(png, info, end);
if (success) {
if (!success) {
free(pixels);
mappedMemoryFree(state, stateSize);
return NULL;
} else if (extdata) {
struct mStateExtdataItem item = {
.size = width * height * 4,
.data = pixels,
@ -291,8 +295,6 @@ static void* _loadPNGState(struct mCore* core, struct VFile* vf, struct mStateEx
mStateExtdataPut(extdata, EXTDATA_SCREENSHOT, &item);
} else {
free(pixels);
mappedMemoryFree(state, stateSize);
return 0;
}
return state;
}

View File

@ -65,7 +65,7 @@ void GBAudioInit(struct GBAudio* audio, size_t samples, uint8_t* nr52, enum GBAu
if (style == GB_AUDIO_GBA) {
audio->timingFactor = 4;
} else {
audio->timingFactor = 1;
audio->timingFactor = 2;
}
audio->frameEvent.context = audio;
@ -339,7 +339,7 @@ void GBAudioWriteNR34(struct GBAudio* audio, uint8_t value) {
if (audio->playingCh3) {
audio->ch3.readable = audio->style != GB_AUDIO_DMG;
// TODO: Where does this cycle delay come from?
mTimingSchedule(audio->timing, &audio->ch3Event, audio->timingFactor * 4 + 2 * (2048 - audio->ch3.rate));
mTimingSchedule(audio->timing, &audio->ch3Event, audio->timingFactor * (4 + 2 * (2048 - audio->ch3.rate)));
}
*audio->nr52 &= ~0x0004;
*audio->nr52 |= audio->playingCh3 << 2;
@ -477,11 +477,8 @@ void GBAudioWriteNR52(struct GBAudio* audio, uint8_t value) {
audio->skipFrame = false;
audio->frame = 7;
if (audio->p) {
unsigned timingFactor = 0x400 >> !audio->p->doubleSpeed;
if (audio->p->timer.internalDiv & timingFactor) {
audio->skipFrame = true;
}
if (audio->p && audio->p->timer.internalDiv & 0x400) {
audio->skipFrame = true;
}
}
}
@ -914,7 +911,7 @@ static void _updateChannel3(struct mTiming* timing, void* user, uint32_t cyclesL
audio->ch3.readable = true;
if (audio->style == GB_AUDIO_DMG) {
mTimingDeschedule(audio->timing, &audio->ch3Fade);
mTimingSchedule(timing, &audio->ch3Fade, 2 - cyclesLate);
mTimingSchedule(timing, &audio->ch3Fade, 4 - cyclesLate);
}
int cycles = 2 * (2048 - ch->rate);
mTimingSchedule(timing, &audio->ch3Event, audio->timingFactor * cycles - cyclesLate);

View File

@ -742,7 +742,7 @@ void GBSetInterrupts(struct SM83Core* cpu, bool enable) {
gb->memory.ime = false;
GBUpdateIRQs(gb);
} else {
mTimingSchedule(&gb->timing, &gb->eiPending, 4);
mTimingSchedule(&gb->timing, &gb->eiPending, 4 * cpu->tMultiplier);
}
}
@ -796,7 +796,7 @@ void GBStop(struct SM83Core* cpu) {
struct GB* gb = (struct GB*) cpu->master;
if (gb->model >= GB_MODEL_CGB && gb->memory.io[GB_REG_KEY1] & 1) {
gb->doubleSpeed ^= 1;
gb->audio.timingFactor = gb->doubleSpeed + 1;
gb->cpu->tMultiplier = 2 - gb->doubleSpeed;
gb->memory.io[GB_REG_KEY1] = 0;
gb->memory.io[GB_REG_KEY1] |= gb->doubleSpeed << 7;
} else {

View File

@ -417,15 +417,15 @@ void GBIOWrite(struct GB* gb, unsigned address, uint8_t value) {
}
return;
case GB_REG_TIMA:
if (value && mTimingUntil(&gb->timing, &gb->timer.irq) > 1) {
if (value && mTimingUntil(&gb->timing, &gb->timer.irq) > 2 - (int) gb->doubleSpeed) {
mTimingDeschedule(&gb->timing, &gb->timer.irq);
}
if (mTimingUntil(&gb->timing, &gb->timer.irq) == -1) {
if (mTimingUntil(&gb->timing, &gb->timer.irq) == (int) gb->doubleSpeed - 2) {
return;
}
break;
case GB_REG_TMA:
if (mTimingUntil(&gb->timing, &gb->timer.irq) == -1) {
if (mTimingUntil(&gb->timing, &gb->timer.irq) == (int) gb->doubleSpeed - 2) {
gb->memory.io[GB_REG_TIMA] = value;
}
break;

View File

@ -531,10 +531,7 @@ void GBMemoryDMA(struct GB* gb, uint16_t base) {
base &= 0xDFFF;
}
mTimingDeschedule(&gb->timing, &gb->memory.dmaEvent);
mTimingSchedule(&gb->timing, &gb->memory.dmaEvent, 8);
if (gb->cpu->cycles + 8 < gb->cpu->nextEvent) {
gb->cpu->nextEvent = gb->cpu->cycles + 8;
}
mTimingSchedule(&gb->timing, &gb->memory.dmaEvent, 8 * (2 - gb->doubleSpeed));
gb->memory.dmaSource = base;
gb->memory.dmaDest = 0;
gb->memory.dmaRemaining = 0xA0;
@ -580,7 +577,7 @@ void _GBMemoryDMAService(struct mTiming* timing, void* context, uint32_t cyclesL
++gb->memory.dmaDest;
gb->memory.dmaRemaining = dmaRemaining - 1;
if (gb->memory.dmaRemaining) {
mTimingSchedule(timing, &gb->memory.dmaEvent, 4 - cyclesLate);
mTimingSchedule(timing, &gb->memory.dmaEvent, 4 * (2 - gb->doubleSpeed) - cyclesLate);
}
}
@ -594,7 +591,7 @@ void _GBMemoryHDMAService(struct mTiming* timing, void* context, uint32_t cycles
--gb->memory.hdmaRemaining;
if (gb->memory.hdmaRemaining) {
mTimingDeschedule(timing, &gb->memory.hdmaEvent);
mTimingSchedule(timing, &gb->memory.hdmaEvent, 2 - cyclesLate);
mTimingSchedule(timing, &gb->memory.hdmaEvent, 4 - cyclesLate);
} else {
gb->cpuBlocked = false;
gb->memory.io[GB_REG_HDMA1] = gb->memory.hdmaSource >> 8;

View File

@ -13,11 +13,11 @@
mLOG_DEFINE_CATEGORY(GB_STATE, "GB Savestate", "gb.serialize");
const uint32_t GB_SAVESTATE_MAGIC = 0x00400000;
const uint32_t GB_SAVESTATE_VERSION = 0x00000002;
MGBA_EXPORT const uint32_t GBSavestateMagic = 0x00400000;
MGBA_EXPORT const uint32_t GBSavestateVersion = 0x00000002;
void GBSerialize(struct GB* gb, struct GBSerializedState* state) {
STORE_32LE(GB_SAVESTATE_MAGIC + GB_SAVESTATE_VERSION, 0, &state->versionMagic);
STORE_32LE(GBSavestateMagic + GBSavestateVersion, 0, &state->versionMagic);
STORE_32LE(gb->romCrc32, 0, &state->romCrc32);
STORE_32LE(gb->timing.masterCycles, 0, &state->masterCycles);
STORE_64LE(gb->timing.globalCycles, 0, &state->globalCycles);
@ -76,20 +76,20 @@ bool GBDeserialize(struct GB* gb, const struct GBSerializedState* state) {
int16_t check16;
uint16_t ucheck16;
LOAD_32LE(ucheck, 0, &state->versionMagic);
if (ucheck > GB_SAVESTATE_MAGIC + GB_SAVESTATE_VERSION) {
mLOG(GB_STATE, WARN, "Invalid or too new savestate: expected %08X, got %08X", GB_SAVESTATE_MAGIC + GB_SAVESTATE_VERSION, ucheck);
if (ucheck > GBSavestateMagic + GBSavestateVersion) {
mLOG(GB_STATE, WARN, "Invalid or too new savestate: expected %08X, got %08X", GBSavestateMagic + GBSavestateVersion, ucheck);
error = true;
} else if (ucheck < GB_SAVESTATE_MAGIC) {
mLOG(GB_STATE, WARN, "Invalid savestate: expected %08X, got %08X", GB_SAVESTATE_MAGIC + GB_SAVESTATE_VERSION, ucheck);
} else if (ucheck < GBSavestateMagic) {
mLOG(GB_STATE, WARN, "Invalid savestate: expected %08X, got %08X", GBSavestateMagic + GBSavestateVersion, ucheck);
error = true;
} else if (ucheck < GB_SAVESTATE_MAGIC + GB_SAVESTATE_VERSION) {
mLOG(GB_STATE, WARN, "Old savestate: expected %08X, got %08X, continuing anyway", GB_SAVESTATE_MAGIC + GB_SAVESTATE_VERSION, ucheck);
} else if (ucheck < GBSavestateMagic + GBSavestateVersion) {
mLOG(GB_STATE, WARN, "Old savestate: expected %08X, got %08X, continuing anyway", GBSavestateMagic + GBSavestateVersion, ucheck);
}
bool canSgb = ucheck >= GB_SAVESTATE_MAGIC + 2;
bool canSgb = ucheck >= GBSavestateMagic + 2;
if (gb->memory.rom && memcmp(state->title, ((struct GBCartridge*) &gb->memory.rom[0x100])->titleLong, sizeof(state->title))) {
LOAD_32LE(ucheck, 0, &state->versionMagic);
if (ucheck > GB_SAVESTATE_MAGIC + 2 || memcmp(state->title, ((struct GBCartridge*) gb->memory.rom)->titleLong, sizeof(state->title))) {
if (ucheck > GBSavestateMagic + 2 || memcmp(state->title, ((struct GBCartridge*) gb->memory.rom)->titleLong, sizeof(state->title))) {
// There was a bug in previous versions where the memory address being compared was wrong
mLOG(GB_STATE, WARN, "Savestate is for a different game");
error = true;
@ -175,8 +175,6 @@ bool GBDeserialize(struct GB* gb, const struct GBSerializedState* state) {
gb->cpu->halted = GBSerializedCpuFlagsGetHalted(flags);
gb->cpuBlocked = GBSerializedCpuFlagsGetBlocked(flags);
gb->audio.timingFactor = gb->doubleSpeed + 1;
LOAD_32LE(gb->cpu->cycles, 0, &state->cpu.cycles);
LOAD_32LE(gb->cpu->nextEvent, 0, &state->cpu.nextEvent);
gb->timing.root = NULL;

View File

@ -77,7 +77,7 @@ void _GBSIOProcessEvents(struct mTiming* timing, void* context, uint32_t cyclesL
sio->pendingSB = 0xFF;
}
} else {
mTimingSchedule(timing, &sio->event, sio->period);
mTimingSchedule(timing, &sio->event, sio->period * (2 - sio->p->doubleSpeed));
}
}
@ -93,7 +93,7 @@ void GBSIOWriteSC(struct GBSIO* sio, uint8_t sc) {
if (GBRegisterSCIsEnable(sc)) {
mTimingDeschedule(&sio->p->timing, &sio->event);
if (GBRegisterSCIsShiftClock(sc)) {
mTimingSchedule(&sio->p->timing, &sio->event, sio->period);
mTimingSchedule(&sio->p->timing, &sio->event, sio->period * (2 - sio->p->doubleSpeed));
sio->remainingBits = 8;
}
}

View File

@ -128,7 +128,7 @@ static int32_t _masterUpdate(struct GBSIOLockstepNode* node) {
case TRANSFER_FINISHING:
// Finish the transfer
// We need to make sure the other GBs catch up so they don't get behind
node->nextEvent += node->d.p->period - 8; // Split the cycles to avoid waiting too long
node->nextEvent += node->d.p->period * (2 - node->d.p->p->doubleSpeed) - 8; // Split the cycles to avoid waiting too long
#ifndef NDEBUG
ATOMIC_ADD(node->p->d.transferId, 1);
#endif
@ -208,7 +208,7 @@ static void _GBSIOLockstepNodeProcessEvents(struct mTiming* timing, void* user,
struct GBSIOLockstepNode* node = user;
mLockstepLock(&node->p->d);
if (node->p->d.attached < 2) {
mTimingSchedule(timing, &node->event, (GBSIOCyclesPerTransfer[0] >> 1) - cyclesLate);
mTimingSchedule(timing, &node->event, (GBSIOCyclesPerTransfer[0] >> 1) * (2 - node->d.p->p->doubleSpeed) - cyclesLate);
mLockstepUnlock(&node->p->d);
return;
}

View File

@ -20,17 +20,18 @@ void _GBTimerIRQ(struct mTiming* timing, void* context, uint32_t cyclesLate) {
}
static void _GBTimerDivIncrement(struct GBTimer* timer, uint32_t cyclesLate) {
while (timer->nextDiv >= GB_DMG_DIV_PERIOD) {
timer->nextDiv -= GB_DMG_DIV_PERIOD;
int tMultiplier = 2 - timer->p->doubleSpeed;
while (timer->nextDiv >= GB_DMG_DIV_PERIOD * tMultiplier) {
timer->nextDiv -= GB_DMG_DIV_PERIOD * tMultiplier;
// Make sure to trigger when the correct bit is a falling edge
if (timer->timaPeriod > 0 && (timer->internalDiv & (timer->timaPeriod - 1)) == timer->timaPeriod - 1) {
++timer->p->memory.io[GB_REG_TIMA];
if (!timer->p->memory.io[GB_REG_TIMA]) {
mTimingSchedule(&timer->p->timing, &timer->irq, 7 - ((timer->p->cpu->executionState - cyclesLate) & 3));
mTimingSchedule(&timer->p->timing, &timer->irq, 7 * tMultiplier - ((timer->p->cpu->executionState * tMultiplier - cyclesLate) & (3 * tMultiplier)));
}
}
unsigned timingFactor = 0x3FF >> !timer->p->doubleSpeed;
unsigned timingFactor = 0x1FF;
if ((timer->internalDiv & timingFactor) == timingFactor) {
GBAudioUpdateFrame(&timer->p->audio, &timer->p->timing);
}
@ -52,7 +53,7 @@ void _GBTimerUpdate(struct mTiming* timing, void* context, uint32_t cyclesLate)
if (timaToGo < divsToGo) {
divsToGo = timaToGo;
}
timer->nextDiv = GB_DMG_DIV_PERIOD * divsToGo;
timer->nextDiv = GB_DMG_DIV_PERIOD * divsToGo * (2 - timer->p->doubleSpeed);
mTimingSchedule(timing, &timer->event, timer->nextDiv - cyclesLate);
}
@ -66,7 +67,7 @@ void GBTimerReset(struct GBTimer* timer) {
timer->irq.callback = _GBTimerIRQ;
timer->event.priority = 0x21;
timer->nextDiv = GB_DMG_DIV_PERIOD; // TODO: GBC differences
timer->nextDiv = GB_DMG_DIV_PERIOD * 2;
timer->timaPeriod = 1024 >> 4;
}
@ -74,27 +75,27 @@ void GBTimerDivReset(struct GBTimer* timer) {
timer->nextDiv -= mTimingUntil(&timer->p->timing, &timer->event);
mTimingDeschedule(&timer->p->timing, &timer->event);
_GBTimerDivIncrement(timer, 0);
if (((timer->internalDiv << 1) | ((timer->nextDiv >> 3) & 1)) & timer->timaPeriod) {
int tMultiplier = 2 - timer->p->doubleSpeed;
if (((timer->internalDiv << 1) | ((timer->nextDiv >> (4 - timer->p->doubleSpeed)) & 1)) & timer->timaPeriod) {
++timer->p->memory.io[GB_REG_TIMA];
if (!timer->p->memory.io[GB_REG_TIMA]) {
mTimingSchedule(&timer->p->timing, &timer->irq, 7 - (timer->p->cpu->executionState & 3));
mTimingSchedule(&timer->p->timing, &timer->irq, (7 - (timer->p->cpu->executionState & 3)) * tMultiplier);
}
}
unsigned timingFactor = 0x400 >> !timer->p->doubleSpeed;
if (timer->internalDiv & timingFactor) {
if (timer->internalDiv & 0x200) {
GBAudioUpdateFrame(&timer->p->audio, &timer->p->timing);
}
timer->p->memory.io[GB_REG_DIV] = 0;
timer->internalDiv = 0;
timer->nextDiv = GB_DMG_DIV_PERIOD;
mTimingSchedule(&timer->p->timing, &timer->event, timer->nextDiv - ((timer->p->cpu->executionState + 1) & 3));
timer->nextDiv = GB_DMG_DIV_PERIOD * (2 - timer->p->doubleSpeed);
mTimingSchedule(&timer->p->timing, &timer->event, timer->nextDiv - ((timer->p->cpu->executionState + 1) & 3) * tMultiplier);
}
uint8_t GBTimerUpdateTAC(struct GBTimer* timer, GBRegisterTAC tac) {
if (GBRegisterTACIsRun(tac)) {
timer->nextDiv -= mTimingUntil(&timer->p->timing, &timer->event);
mTimingDeschedule(&timer->p->timing, &timer->event);
_GBTimerDivIncrement(timer, (timer->p->cpu->executionState + 2) & 3);
_GBTimerDivIncrement(timer, ((timer->p->cpu->executionState + 2) & 3) * (2 - timer->p->doubleSpeed));
switch (GBRegisterTACGetClock(tac)) {
case 0:
@ -111,7 +112,7 @@ uint8_t GBTimerUpdateTAC(struct GBTimer* timer, GBRegisterTAC tac) {
break;
}
timer->nextDiv += GB_DMG_DIV_PERIOD;
timer->nextDiv += GB_DMG_DIV_PERIOD * (2 - timer->p->doubleSpeed);
mTimingSchedule(&timer->p->timing, &timer->event, timer->nextDiv);
} else {
timer->timaPeriod = 0;

View File

@ -253,7 +253,7 @@ void GBVideoSkipBIOS(struct GBVideo* video) {
GBUpdateIRQs(video->p);
video->p->memory.io[GB_REG_STAT] = video->stat;
mTimingDeschedule(&video->p->timing, &video->modeEvent);
mTimingSchedule(&video->p->timing, &video->modeEvent, next);
mTimingSchedule(&video->p->timing, &video->modeEvent, next << 1);
}
void _endMode0(struct mTiming* timing, void* context, uint32_t cyclesLate) {
@ -297,7 +297,7 @@ void _endMode0(struct mTiming* timing, void* context, uint32_t cyclesLate) {
GBUpdateIRQs(video->p);
video->p->memory.io[GB_REG_STAT] = video->stat;
mTimingSchedule(timing, &video->modeEvent, (next << video->p->doubleSpeed) - cyclesLate);
mTimingSchedule(timing, &video->modeEvent, (next << 1) - cyclesLate);
}
void _endMode1(struct mTiming* timing, void* context, uint32_t cyclesLate) {
@ -334,14 +334,14 @@ void _endMode1(struct mTiming* timing, void* context, uint32_t cyclesLate) {
GBUpdateIRQs(video->p);
}
video->p->memory.io[GB_REG_STAT] = video->stat;
mTimingSchedule(timing, &video->modeEvent, (next << video->p->doubleSpeed) - cyclesLate);
mTimingSchedule(timing, &video->modeEvent, (next << 1) - cyclesLate);
}
void _endMode2(struct mTiming* timing, void* context, uint32_t cyclesLate) {
struct GBVideo* video = context;
_cleanOAM(video, video->ly);
video->x = -(video->p->memory.io[GB_REG_SCX] & 7);
video->dotClock = mTimingCurrentTime(timing) - cyclesLate + 5 - (video->x << video->p->doubleSpeed);
video->dotClock = mTimingCurrentTime(timing) - cyclesLate + 10 - (video->x << 1);
int32_t next = GB_VIDEO_MODE_3_LENGTH_BASE + video->objMax * 6 - video->x;
video->mode = 3;
video->modeEvent.callback = _endMode3;
@ -352,7 +352,7 @@ void _endMode2(struct mTiming* timing, void* context, uint32_t cyclesLate) {
GBUpdateIRQs(video->p);
}
video->p->memory.io[GB_REG_STAT] = video->stat;
mTimingSchedule(timing, &video->modeEvent, (next << video->p->doubleSpeed) - cyclesLate);
mTimingSchedule(timing, &video->modeEvent, (next << 1) - cyclesLate);
}
void _endMode3(struct mTiming* timing, void* context, uint32_t cyclesLate) {
@ -375,18 +375,18 @@ void _endMode3(struct mTiming* timing, void* context, uint32_t cyclesLate) {
video->p->memory.io[GB_REG_STAT] = video->stat;
// TODO: Cache SCX & 7 in case it changes
int32_t next = GB_VIDEO_MODE_0_LENGTH_BASE - video->objMax * 6 - (video->p->memory.io[GB_REG_SCX] & 7);
mTimingSchedule(timing, &video->modeEvent, (next << video->p->doubleSpeed) - cyclesLate);
mTimingSchedule(timing, &video->modeEvent, (next << 1) - cyclesLate);
}
void _updateFrameCount(struct mTiming* timing, void* context, uint32_t cyclesLate) {
UNUSED(cyclesLate);
struct GBVideo* video = context;
if (video->p->cpu->executionState != SM83_CORE_FETCH) {
mTimingSchedule(timing, &video->frameEvent, 4 - ((video->p->cpu->executionState + 1) & 3));
mTimingSchedule(timing, &video->frameEvent, (4 - ((video->p->cpu->executionState + 1) & 3)) * (2 - video->p->doubleSpeed));
return;
}
if (!GBRegisterLCDCIsEnable(video->p->memory.io[GB_REG_LCDC])) {
mTimingSchedule(timing, &video->frameEvent, GB_VIDEO_TOTAL_LENGTH);
mTimingSchedule(timing, &video->frameEvent, GB_VIDEO_TOTAL_LENGTH << 1);
}
--video->frameskipCounter;
@ -424,7 +424,7 @@ void GBVideoProcessDots(struct GBVideo* video, uint32_t cyclesLate) {
return;
}
int oldX = video->x;
video->x = (int32_t) (mTimingCurrentTime(&video->p->timing) - cyclesLate - video->dotClock) >> video->p->doubleSpeed;
video->x = ((int32_t) (mTimingCurrentTime(&video->p->timing) - cyclesLate - video->dotClock)) >> 1;
if (video->x > GB_VIDEO_HORIZONTAL_PIXELS) {
video->x = GB_VIDEO_HORIZONTAL_PIXELS;
} else if (video->x < 0) {
@ -444,7 +444,7 @@ void GBVideoWriteLCDC(struct GBVideo* video, GBRegisterLCDC value) {
video->modeEvent.callback = _endMode2;
int32_t next = GB_VIDEO_MODE_2_LENGTH - 5; // TODO: Why is this fudge factor needed? Might be related to T-cycles for load/store differing
mTimingDeschedule(&video->p->timing, &video->modeEvent);
mTimingSchedule(&video->p->timing, &video->modeEvent, next << video->p->doubleSpeed);
mTimingSchedule(&video->p->timing, &video->modeEvent, next << 1);
video->ly = 0;
video->p->memory.io[GB_REG_LY] = 0;
@ -471,7 +471,7 @@ void GBVideoWriteLCDC(struct GBVideo* video, GBRegisterLCDC value) {
mTimingDeschedule(&video->p->timing, &video->modeEvent);
mTimingDeschedule(&video->p->timing, &video->frameEvent);
mTimingSchedule(&video->p->timing, &video->frameEvent, GB_VIDEO_TOTAL_LENGTH);
mTimingSchedule(&video->p->timing, &video->frameEvent, GB_VIDEO_TOTAL_LENGTH << 1);
}
video->p->memory.io[GB_REG_STAT] = video->stat;
}

View File

@ -11,6 +11,8 @@
#include <mgba/internal/gba/serialize.h>
#include <mgba-util/vfs.h>
#define MAPPING_MASK (GBA_MATRIX_MAPPINGS_MAX - 1)
static void _remapMatrix(struct GBA* gba) {
if (gba->memory.matrix.vaddr & 0xFFFFE1FF) {
mLOG(GBA_MEM, ERROR, "Invalid Matrix mapping: %08X", gba->memory.matrix.vaddr);
@ -24,11 +26,11 @@ static void _remapMatrix(struct GBA* gba) {
mLOG(GBA_MEM, ERROR, "Invalid Matrix mapping end: %08X", gba->memory.matrix.vaddr + gba->memory.matrix.size);
return;
}
int start = (gba->memory.matrix.vaddr >> 9) & 0x1F;
int size = (gba->memory.matrix.size >> 9) & 0x1F;
int start = gba->memory.matrix.vaddr >> 9;
int size = (gba->memory.matrix.size >> 9) & MAPPING_MASK;
int i;
for (i = 0; i < size; ++i) {
gba->memory.matrix.mappings[start + i] = gba->memory.matrix.paddr + (i << 9);
gba->memory.matrix.mappings[(start + i) & MAPPING_MASK] = gba->memory.matrix.paddr + (i << 9);
}
gba->romVf->seek(gba->romVf, gba->memory.matrix.paddr, SEEK_SET);

View File

@ -14,8 +14,8 @@
#include <fcntl.h>
const uint32_t GBA_SAVESTATE_MAGIC = 0x01000000;
const uint32_t GBA_SAVESTATE_VERSION = 0x00000004;
MGBA_EXPORT const uint32_t GBASavestateMagic = 0x01000000;
MGBA_EXPORT const uint32_t GBASavestateVersion = 0x00000004;
mLOG_DEFINE_CATEGORY(GBA_STATE, "GBA Savestate", "gba.serialize");
@ -25,7 +25,7 @@ struct GBABundledState {
};
void GBASerialize(struct GBA* gba, struct GBASerializedState* state) {
STORE_32(GBA_SAVESTATE_MAGIC + GBA_SAVESTATE_VERSION, 0, &state->versionMagic);
STORE_32(GBASavestateMagic + GBASavestateVersion, 0, &state->versionMagic);
STORE_32(gba->biosChecksum, 0, &state->biosChecksum);
STORE_32(gba->romCrc32, 0, &state->romCrc32);
STORE_32(gba->timing.masterCycles, 0, &state->masterCycles);
@ -87,14 +87,14 @@ bool GBADeserialize(struct GBA* gba, const struct GBASerializedState* state) {
int32_t check;
uint32_t ucheck;
LOAD_32(ucheck, 0, &state->versionMagic);
if (ucheck > GBA_SAVESTATE_MAGIC + GBA_SAVESTATE_VERSION) {
mLOG(GBA_STATE, WARN, "Invalid or too new savestate: expected %08X, got %08X", GBA_SAVESTATE_MAGIC + GBA_SAVESTATE_VERSION, ucheck);
if (ucheck > GBASavestateMagic + GBASavestateVersion) {
mLOG(GBA_STATE, WARN, "Invalid or too new savestate: expected %08X, got %08X", GBASavestateMagic + GBASavestateVersion, ucheck);
error = true;
} else if (ucheck < GBA_SAVESTATE_MAGIC) {
mLOG(GBA_STATE, WARN, "Invalid savestate: expected %08X, got %08X", GBA_SAVESTATE_MAGIC + GBA_SAVESTATE_VERSION, ucheck);
} else if (ucheck < GBASavestateMagic) {
mLOG(GBA_STATE, WARN, "Invalid savestate: expected %08X, got %08X", GBASavestateMagic + GBASavestateVersion, ucheck);
error = true;
} else if (ucheck < GBA_SAVESTATE_MAGIC + GBA_SAVESTATE_VERSION) {
mLOG(GBA_STATE, WARN, "Old savestate: expected %08X, got %08X, continuing anyway", GBA_SAVESTATE_MAGIC + GBA_SAVESTATE_VERSION, ucheck);
} else if (ucheck < GBASavestateMagic + GBASavestateVersion) {
mLOG(GBA_STATE, WARN, "Old savestate: expected %08X, got %08X, continuing anyway", GBASavestateMagic + GBASavestateVersion, ucheck);
}
LOAD_32(ucheck, 0, &state->biosChecksum);
if (ucheck != gba->biosChecksum) {

View File

@ -149,11 +149,7 @@ bool GBASIOLockstepNodeUnload(struct GBASIODriver* driver) {
// Flush ongoing transfer
if (mTimingIsScheduled(&driver->p->p->timing, &node->event)) {
int oldWhen = node->event.when;
mTimingDeschedule(&driver->p->p->timing, &node->event);
mTimingSchedule(&driver->p->p->timing, &node->event, 0);
node->eventDiff -= oldWhen - node->event.when;
node->eventDiff -= node->event.when - mTimingCurrentTime(&driver->p->p->timing);
mTimingDeschedule(&driver->p->p->timing, &node->event);
}
@ -190,15 +186,11 @@ static uint16_t GBASIOLockstepNodeMultiWriteRegister(struct GBASIODriver* driver
ATOMIC_STORE(node->p->d.transferActive, TRANSFER_STARTING);
ATOMIC_STORE(node->p->d.transferCycles, GBASIOCyclesPerTransfer[GBASIOMultiplayerGetBaud(node->d.p->siocnt)][node->p->d.attached - 1]);
bool scheduled = mTimingIsScheduled(&driver->p->p->timing, &node->event);
int oldWhen = node->event.when;
mTimingDeschedule(&driver->p->p->timing, &node->event);
mTimingSchedule(&driver->p->p->timing, &node->event, 0);
if (scheduled) {
node->eventDiff -= oldWhen - node->event.when;
if (mTimingIsScheduled(&driver->p->p->timing, &node->event)) {
node->eventDiff -= node->event.when - mTimingCurrentTime(&driver->p->p->timing);
mTimingDeschedule(&driver->p->p->timing, &node->event);
}
mTimingSchedule(&driver->p->p->timing, &node->event, 0);
} else {
value &= ~0x0080;
}
@ -443,14 +435,13 @@ static uint32_t _slaveUpdate(struct GBASIOLockstepNode* node) {
static void _GBASIOLockstepNodeProcessEvents(struct mTiming* timing, void* user, uint32_t cyclesLate) {
struct GBASIOLockstepNode* node = user;
mLockstepLock(&node->p->d);
if (node->p->d.attached < 2) {
mLockstepUnlock(&node->p->d);
return;
}
int32_t cycles = 0;
node->nextEvent -= cyclesLate;
node->eventDiff += cyclesLate;
if (node->nextEvent <= 0) {
if (node->p->d.attached < 2) {
cycles = GBASIOCyclesPerTransfer[GBASIOMultiplayerGetBaud(node->d.p->siocnt)][0];
} else if (node->nextEvent <= 0) {
if (!node->id) {
cycles = _masterUpdate(node);
} else {

View File

@ -88,19 +88,11 @@ static void mGLContextResized(struct VideoBackend* v, unsigned w, unsigned h) {
unsigned drawW = w;
unsigned drawH = h;
if (v->lockAspectRatio) {
if (w * v->height > h * v->width) {
drawW = h * v->width / v->height;
} else if (w * v->height < h * v->width) {
drawH = w * v->height / v->width;
}
lockAspectRatioUInt(v->width, v->height, &drawW, &drawH);
}
if (v->lockIntegerScaling) {
if (drawW >= v->width) {
drawW -= drawW % v->width;
}
if (drawH >= v->height) {
drawH -= drawH % v->height;
}
lockIntegerRatioUInt(v->width, &drawW);
lockIntegerRatioUInt(v->height, &drawH);
}
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

View File

@ -221,19 +221,11 @@ static void mGLES2ContextResized(struct VideoBackend* v, unsigned w, unsigned h)
unsigned drawW = w;
unsigned drawH = h;
if (v->lockAspectRatio) {
if (w * v->height > h * v->width) {
drawW = h * v->width / v->height;
} else if (w * v->height < h * v->width) {
drawH = w * v->height / v->width;
}
lockAspectRatioUInt(v->width, v->height, &drawW, &drawH);
}
if (v->lockIntegerScaling) {
if (drawW >= v->width) {
drawW -= drawW % v->width;
}
if (drawH >= v->height) {
drawH -= drawH % v->height;
}
lockIntegerRatioUInt(v->width, &drawW);
lockIntegerRatioUInt(v->height, &drawH);
}
size_t n;
for (n = 0; n < context->nShaders; ++n) {

View File

@ -71,4 +71,10 @@ AboutScreen::AboutScreen(QWidget* parent)
patrons.replace("{patrons}", patronList.join(""));
m_ui.patrons->setText(patrons);
}
{
QString copyright = m_ui.copyright->text();
copyright.replace("{year}", tr("2021"));
m_ui.copyright->setText(copyright);
}
}

View File

@ -83,7 +83,7 @@
</font>
</property>
<property name="text">
<string>© 2013 2020 Jeffrey Pfau, licensed under the Mozilla Public License, version 2.0
<string>© 2013 {year} Jeffrey Pfau, licensed under the Mozilla Public License, version 2.0
Game Boy Advance is a registered trademark of Nintendo Co., Ltd.</string>
</property>
<property name="alignment">

View File

@ -20,7 +20,7 @@ void AssetInfo::addCustomProperty(const QString& id, const QString& visibleName)
QHBoxLayout* newLayout = new QHBoxLayout;
newLayout->addWidget(new QLabel(visibleName));
QLabel* value = new QLabel;
value->setFont(GBAApp::monospaceFont());
value->setFont(GBAApp::app()->monospaceFont());
value->setAlignment(Qt::AlignRight);
newLayout->addWidget(value);
m_customProperties[id] = value;

View File

@ -31,7 +31,7 @@ AssetTile::AssetTile(QWidget* parent)
connect(m_ui.preview, &Swatch::indexPressed, this, &AssetTile::selectColor);
const QFont font = GBAApp::monospaceFont();
const QFont font = GBAApp::app()->monospaceFont();
m_ui.tileId->setFont(font);
m_ui.paletteId->setFont(font);

View File

@ -40,20 +40,18 @@ void AudioDevice::setInput(mCoreThread* input) {
}
qint64 AudioDevice::readData(char* data, qint64 maxSize) {
if (maxSize > 0xFFFFFFFFLL) {
maxSize = 0xFFFFFFFFLL;
}
if (!m_context->core) {
LOG(QT, WARN) << tr("Audio device is missing its core");
return 0;
}
maxSize /= sizeof(GBAStereoSample);
mCoreSyncLockAudio(&m_context->impl->sync);
int available = blip_samples_avail(m_context->core->getAudioChannel(m_context->core, 0));
if (available > maxSize / sizeof(GBAStereoSample)) {
available = maxSize / sizeof(GBAStereoSample);
}
int available = std::min<qint64>({
blip_samples_avail(m_context->core->getAudioChannel(m_context->core, 0)),
maxSize,
std::numeric_limits<int>::max()
});
blip_read_samples(m_context->core->getAudioChannel(m_context->core, 0), &reinterpret_cast<GBAStereoSample*>(data)->left, available, true);
blip_read_samples(m_context->core->getAudioChannel(m_context->core, 1), &reinterpret_cast<GBAStereoSample*>(data)->right, available, true);
mCoreSyncConsumeAudio(&m_context->impl->sync);

View File

@ -52,7 +52,7 @@ void AudioProcessorSDL::pause() {
void AudioProcessorSDL::setBufferSamples(int samples) {
AudioProcessor::setBufferSamples(samples);
if (m_audio.samples != samples) {
if (m_audio.samples != static_cast<size_t>(samples)) {
m_audio.samples = samples;
if (m_audio.core) {
mSDLDeinitAudio(&m_audio);

View File

@ -101,6 +101,7 @@ set(SOURCE_FILES
ReportView.cpp
ROMInfo.cpp
RotatedHeaderView.cpp
SaveConverter.cpp
SavestateButton.cpp
SensorView.cpp
SettingsView.cpp
@ -145,6 +146,7 @@ set(UI_FILES
PrinterView.ui
ReportView.ui
ROMInfo.ui
SaveConverter.ui
SensorView.ui
SettingsView.ui
ShaderSelector.ui

View File

@ -20,7 +20,7 @@ CheatsModel::CheatsModel(mCheatDevice* device, QObject* parent)
: QAbstractItemModel(parent)
, m_device(device)
{
m_font = GBAApp::monospaceFont();
m_font = GBAApp::app()->monospaceFont();
}
QVariant CheatsModel::data(const QModelIndex& index, int role) const {

View File

@ -23,7 +23,7 @@ ConfigOption::ConfigOption(const QString& name, QObject* parent)
void ConfigOption::connect(std::function<void(const QVariant&)> slot, QObject* parent) {
m_slots[parent] = slot;
QObject::connect(parent, &QObject::destroyed, [this, slot, parent]() {
QObject::connect(parent, &QObject::destroyed, this, [this, parent]() {
m_slots.remove(parent);
});
}
@ -37,10 +37,10 @@ Action* ConfigOption::addValue(const QString& text, const QVariant& value, Actio
if (actions) {
action = actions->addAction(text, name, function, menu);
} else {
action = new Action(function, name, text);
action = new Action(function, name, text, this);
}
action->setExclusive();
QObject::connect(action, &QObject::destroyed, [this, action, value]() {
QObject::connect(action, &QObject::destroyed, this, [this, action, value]() {
m_actions.removeAll(std::make_pair(action, value));
});
m_actions.append(std::make_pair(action, value));
@ -59,10 +59,10 @@ Action* ConfigOption::addBoolean(const QString& text, ActionMapper* actions, con
if (actions) {
action = actions->addBooleanAction(text, m_name, function, menu);
} else {
action = new Action(function, m_name, text);
action = new Action(function, m_name, text, this);
}
QObject::connect(action, &QObject::destroyed, [this, action]() {
QObject::connect(action, &QObject::destroyed, this, [this, action]() {
m_actions.removeAll(std::make_pair(action, 1));
});
m_actions.append(std::make_pair(action, 1));
@ -103,7 +103,7 @@ ConfigController::ConfigController(QObject* parent)
QString fileName = configDir();
fileName.append(QDir::separator());
fileName.append("qt.ini");
m_settings = new QSettings(fileName, QSettings::IniFormat, this);
m_settings = std::make_unique<QSettings>(fileName, QSettings::IniFormat);
mCoreConfigInit(&m_config, PORT);
@ -150,7 +150,7 @@ ConfigOption* ConfigController::addOption(const char* key) {
}
ConfigOption* newOption = new ConfigOption(optionName, this);
m_optionSet[optionName] = newOption;
connect(newOption, &ConfigOption::valueChanged, [this, key](const QVariant& value) {
connect(newOption, &ConfigOption::valueChanged, this, [this, key](const QVariant& value) {
setOption(key, value);
});
return newOption;
@ -272,6 +272,9 @@ void ConfigController::setMRU(const QList<QString>& mru) {
break;
}
}
for (; i < MRU_LIST_SIZE; ++i) {
m_settings->remove(QString::number(i));
}
m_settings->endGroup();
}
@ -289,12 +292,11 @@ void ConfigController::makePortable() {
QString fileName(configDir());
fileName.append(QDir::separator());
fileName.append("qt.ini");
QSettings* settings2 = new QSettings(fileName, QSettings::IniFormat, this);
auto settings2 = std::make_unique<QSettings>(fileName, QSettings::IniFormat);
for (const auto& key : m_settings->allKeys()) {
settings2->setValue(key, m_settings->value(key));
}
delete m_settings;
m_settings = settings2;
m_settings = std::move(settings2);
}
bool ConfigController::isPortable() {

View File

@ -7,12 +7,13 @@
#include "Override.h"
#include <QMap>
#include <QHash>
#include <QObject>
#include <QSettings>
#include <QVariant>
#include <functional>
#include <memory>
#include <mgba/core/config.h>
#include <mgba-util/configuration.h>
@ -53,7 +54,7 @@ signals:
void valueChanged(const QVariant& value);
private:
QMap<QObject*, std::function<void(const QVariant&)>> m_slots;
QHash<QObject*, std::function<void(const QVariant&)>> m_slots;
QList<std::pair<Action*, QVariant>> m_actions;
QString m_name;
};
@ -110,8 +111,8 @@ private:
mCoreConfig m_config;
mCoreOptions m_opts{};
QMap<QString, ConfigOption*> m_optionSet;
QSettings* m_settings;
QHash<QString, ConfigOption*> m_optionSet;
std::unique_ptr<QSettings> m_settings;
static QString s_configDir;
};

View File

@ -14,9 +14,6 @@
#ifdef M_CORE_GBA
#include <mgba/gba/core.h>
#endif
#ifdef M_CORE_GB
#include <mgba/gb/core.h>
#endif
#include <mgba/core/core.h>
#include <mgba-util/vfs.h>
@ -31,57 +28,6 @@ void CoreManager::setMultiplayerController(MultiplayerController* multiplayer) {
m_multiplayer = multiplayer;
}
QByteArray CoreManager::getExtdata(const QString& filename, mStateExtdataTag extdataType) {
VFileDevice vf(filename, QIODevice::ReadOnly);
if (!vf.isOpen()) {
return {};
}
mStateExtdata extdata;
mStateExtdataInit(&extdata);
QByteArray bytes;
auto extract = [&bytes, &extdata, &vf, extdataType](mCore* core) -> bool {
if (mCoreExtractExtdata(core, vf, &extdata)) {
mStateExtdataItem extitem;
if (!mStateExtdataGet(&extdata, extdataType, &extitem)) {
return false;
}
if (extitem.size) {
bytes = QByteArray::fromRawData(static_cast<const char*>(extitem.data), extitem.size);
}
return true;
}
return false;
};
bool done = false;
struct mCore* core = nullptr;
#ifdef USE_PNG
done = extract(nullptr);
#endif
#ifdef M_CORE_GBA
if (!done) {
core = GBACoreCreate();
core->init(core);
done = extract(core);
core->deinit(core);
}
#endif
#ifdef M_CORE_GB
if (!done) {
core = GBCoreCreate();
core->init(core);
done = extract(core);
core->deinit(core);
}
#endif
mStateExtdataDeinit(&extdata);
return bytes;
}
CoreController* CoreManager::loadGame(const QString& path) {
QFileInfo info(path);
if (!info.isReadable()) {

View File

@ -9,8 +9,6 @@
#include <QObject>
#include <QString>
#include <mgba/core/serialize.h>
struct mCoreConfig;
struct VFile;
@ -27,8 +25,6 @@ public:
void setMultiplayerController(MultiplayerController*);
void setPreload(bool preload) { m_preload = preload; }
static QByteArray getExtdata(const QString& filename, mStateExtdataTag extdataType);
public slots:
CoreController* loadGame(const QString& path);
CoreController* loadGame(VFile* vf, const QString& path, const QString& base);

View File

@ -6,6 +6,7 @@
#include "DisplayQt.h"
#include "CoreController.h"
#include "utils.h"
#include <QPainter>
@ -98,26 +99,7 @@ void DisplayQt::paintEvent(QPaintEvent*) {
if (isFiltered()) {
painter.setRenderHint(QPainter::SmoothPixmapTransform);
}
// TODO: Refactor this code out (since it's copied in like 3 places)
QSize s = size();
QSize ds = viewportSize();
if (isAspectRatioLocked()) {
if (s.width() * m_height > s.height() * m_width) {
ds.setWidth(s.height() * m_width / m_height);
} else if (s.width() * m_height < s.height() * m_width) {
ds.setHeight(s.width() * m_height / m_width);
}
}
if (isIntegerScalingLocked()) {
if (ds.width() >= m_width) {
ds.setWidth(ds.width() - ds.width() % m_width);
}
if (ds.height() >= m_height) {
ds.setHeight(ds.height() - ds.height() % m_height);
}
}
QPoint origin = QPoint((s.width() - ds.width()) / 2, (s.height() - ds.height()) / 2);
QRect full(origin, ds);
QRect full(clampSize(QSize(m_width, m_height), size(), isAspectRatioLocked(), isIntegerScalingLocked()));
if (hasInterframeBlending()) {
painter.drawImage(full, m_oldBacking, QRect(0, 0, m_width, m_height));

View File

@ -83,7 +83,12 @@ FrameView::FrameView(std::shared_ptr<CoreController> controller, QWidget* parent
FrameView::~FrameView() {
QMutexLocker locker(&m_mutex);
*m_callbackLocker = false;
if (m_nextFrame) {
m_controller->endVideoLog(true);
}
if (m_vl) {
mCoreConfigDeinit(&m_vl->config);
m_vl->deinit(m_vl);
}
}
@ -517,6 +522,9 @@ bool FrameView::eventFilter(QObject*, QEvent* event) {
void FrameView::refreshVl() {
QMutexLocker locker(&m_mutex);
if (m_currentFrame) {
m_currentFrame->close(m_currentFrame);
}
m_currentFrame = m_nextFrame;
m_nextFrame = VFileDevice::openMemory();
if (m_currentFrame) {
@ -536,6 +544,7 @@ void FrameView::newVl() {
return;
}
if (m_vl) {
mCoreConfigDeinit(&m_vl->config);
m_vl->deinit(m_vl);
}
m_vl = mCoreFindVF(m_currentFrame);

View File

@ -36,14 +36,12 @@ static GBAApp* g_app = nullptr;
mLOG_DEFINE_CATEGORY(QT, "Qt", "platform.qt");
QFont GBAApp::s_monospace;
GBAApp::GBAApp(int& argc, char* argv[], ConfigController* config)
: QApplication(argc, argv)
, m_configController(config)
, m_monospace(QFontDatabase::systemFont(QFontDatabase::FixedFont))
{
g_app = this;
s_monospace = QFontDatabase::systemFont(QFontDatabase::FixedFont);
#ifdef BUILD_SDL
SDL_Init(SDL_INIT_NOPARACHUTE);

View File

@ -57,7 +57,7 @@ public:
static QString dataDir();
static QFont monospaceFont() { return s_monospace; }
QFont monospaceFont() { return m_monospace; }
QList<Window*> windows() { return m_windows; }
Window* newWindow();
@ -114,7 +114,7 @@ private:
QThreadPool m_workerThreads;
qint64 m_nextJob = 1;
static QFont s_monospace;
QFont m_monospace;
NoIntroDB* m_db = nullptr;
};

View File

@ -1594,7 +1594,7 @@ IOViewer::IOViewer(std::shared_ptr<CoreController> controller, QWidget* parent)
m_ui.regSelect->addItem("0x" + QString("%1: %2").arg((i << m_width) + m_base, 4, 16, QChar('0')).toUpper().arg(reg), i << m_width);
}
const QFont font = GBAApp::monospaceFont();
const QFont font = GBAApp::app()->monospaceFont();
m_ui.regValue->setFont(font);
connect(m_ui.buttonBox, &QDialogButtonBox::clicked, this, &IOViewer::buttonPressed);

View File

@ -200,7 +200,7 @@ void LoadSaveState::loadState(int slot) {
unsigned width, height;
thread->core->desiredVideoDimensions(thread->core, &width, &height);
mStateExtdataItem item;
if (mStateExtdataGet(&extdata, EXTDATA_SCREENSHOT, &item) && item.size >= width * height * 4) {
if (mStateExtdataGet(&extdata, EXTDATA_SCREENSHOT, &item) && item.size >= static_cast<int32_t>(width * height * 4)) {
stateImage = QImage((uchar*) item.data, width, height, QImage::Format_ARGB32).rgbSwapped();
}

View File

@ -109,10 +109,10 @@ MapView::MapView(std::shared_ptr<CoreController> controller, QWidget* parent)
}
void MapView::selectMap(int map) {
if (map >= mMapCacheSetSize(&m_cacheSet->maps)) {
if (map == m_map || map < 0) {
return;
}
if (map == m_map) {
if (static_cast<unsigned>(map) >= mMapCacheSetSize(&m_cacheSet->maps)) {
return;
}
m_map = map;

View File

@ -9,6 +9,7 @@
#include "CoreController.h"
#include "LogController.h"
#include "VFileDevice.h"
#include "utils.h"
#include <QAction>
#include <QApplication>
@ -27,7 +28,7 @@ using namespace QGBA;
MemoryModel::MemoryModel(QWidget* parent)
: QAbstractScrollArea(parent)
{
m_font = GBAApp::monospaceFont();
m_font = GBAApp::app()->monospaceFont();
#ifdef Q_OS_MAC
m_font.setPointSize(12);
#else
@ -346,7 +347,7 @@ void MemoryModel::paintEvent(QPaintEvent*) {
int height = (viewport()->size().height() - m_cellHeight) / m_cellHeight;
for (int y = 0; y < height; ++y) {
int yp = m_cellHeight * y + m_margins.top();
if ((y + m_top) * 16 >= m_size) {
if ((y + m_top) * 16U >= m_size) {
break;
}
QString data;
@ -628,11 +629,7 @@ void MemoryModel::keyPressEvent(QKeyEvent* event) {
}
void MemoryModel::boundsCheck() {
if (m_top < 0) {
m_top = 0;
} else if (m_top > (m_size >> 4) + 1 - viewport()->size().height() / m_cellHeight) {
m_top = (m_size >> 4) + 1 - viewport()->size().height() / m_cellHeight;
}
m_top = clamp(m_top, 0, static_cast<int32_t>(m_size >> 4) + 1 - viewport()->size().height() / m_cellHeight);
}
bool MemoryModel::isInSelection(uint32_t address) {
@ -676,34 +673,35 @@ void MemoryModel::adjustCursor(int adjust, bool shift) {
}
int cursorPosition = m_top;
if (shift) {
uint32_t absolute;
if (m_selectionAnchor == m_selection.first) {
if (adjust < 0 && m_base - adjust > m_selection.second) {
adjust = m_base - m_selection.second + m_align;
absolute = m_base - m_selection.second + m_align;
} else if (adjust > 0 && m_selection.second + adjust >= m_base + m_size) {
adjust = m_base + m_size - m_selection.second;
absolute = m_base + m_size - m_selection.second;
}
adjust += m_selection.second;
if (adjust <= m_selection.first) {
absolute += m_selection.second;
if (absolute <= m_selection.first) {
m_selection.second = m_selection.first + m_align;
m_selection.first = adjust - m_align;
m_selection.first = absolute - m_align;
cursorPosition = m_selection.first;
} else {
m_selection.second = adjust;
m_selection.second = absolute;
cursorPosition = m_selection.second - m_align;
}
} else {
if (adjust < 0 && m_base - adjust > m_selection.first) {
adjust = m_base - m_selection.first;
absolute = m_base - m_selection.first;
} else if (adjust > 0 && m_selection.first + adjust >= m_base + m_size) {
adjust = m_base + m_size - m_selection.first - m_align;
absolute = m_base + m_size - m_selection.first - m_align;
}
adjust += m_selection.first;
if (adjust >= m_selection.second) {
absolute += m_selection.first;
if (absolute >= m_selection.second) {
m_selection.first = m_selection.second - m_align;
m_selection.second = adjust + m_align;
cursorPosition = adjust;
m_selection.second = absolute + m_align;
cursorPosition = absolute;
} else {
m_selection.first = adjust;
m_selection.first = absolute;
cursorPosition = m_selection.first;
}
}

View File

@ -256,7 +256,7 @@ void MemoryView::updateSelection(uint32_t start, uint32_t end) {
}
void MemoryView::updateStatus() {
int align = m_ui.hexfield->alignment();
unsigned align = m_ui.hexfield->alignment();
mCore* core = m_controller->thread()->core;
QByteArray selection(m_ui.hexfield->serialize());
QString text(m_ui.hexfield->decodeText(selection));

View File

@ -16,7 +16,7 @@ using namespace QGBA;
MessagePainter::MessagePainter(QObject* parent)
: QObject(parent)
{
m_messageFont = GBAApp::monospaceFont();
m_messageFont = GBAApp::app()->monospaceFont();
m_messageFont.setPixelSize(13);
connect(&m_messageTimer, &QTimer::timeout, this, &MessagePainter::clearMessage);
m_messageTimer.setSingleShot(true);

View File

@ -33,7 +33,7 @@ ObjView::ObjView(std::shared_ptr<CoreController> controller, QWidget* parent)
m_ui.setupUi(this);
m_ui.tile->setController(controller);
const QFont font = GBAApp::monospaceFont();
const QFont font = GBAApp::app()->monospaceFont();
m_ui.x->setFont(font);
m_ui.y->setFont(font);
@ -133,8 +133,8 @@ void ObjView::updateTilesGBA(bool force) {
mTileCache* tileCache = mTileCacheSetGetPointer(&m_cacheSet->tiles, newInfo.paletteSet);
int i = 0;
for (int y = 0; y < newInfo.height; ++y) {
for (int x = 0; x < newInfo.width; ++x, ++i, ++tile, ++tileBase) {
for (unsigned y = 0; y < newInfo.height; ++y) {
for (unsigned x = 0; x < newInfo.width; ++x, ++i, ++tile, ++tileBase) {
const color_t* data = mTileCacheGetTileIfDirty(tileCache, &m_tileStatus[16 * tileBase], tile, newInfo.paletteId);
if (data) {
m_ui.tiles->setTile(i, data);
@ -224,7 +224,7 @@ void ObjView::updateTilesGB(bool force) {
int i = 0;
m_ui.tile->setPalette(newInfo.paletteId);
for (int y = 0; y < newInfo.height; ++y, ++i) {
for (unsigned y = 0; y < newInfo.height; ++y, ++i) {
unsigned t = tile + i;
const color_t* data = mTileCacheGetTileIfDirty(tileCache, &m_tileStatus[8 * t], t, newInfo.paletteId);
if (data) {

View File

@ -47,7 +47,7 @@ PaletteView::PaletteView(std::shared_ptr<CoreController> controller, QWidget* pa
m_ui.selected->setDimensions(QSize(1, 1));
updatePalette();
const QFont font = GBAApp::monospaceFont();
const QFont font = GBAApp::app()->monospaceFont();
m_ui.hexcode->setFont(font);
m_ui.value->setFont(font);
@ -69,7 +69,7 @@ void PaletteView::updatePalette() {
return;
}
const uint16_t* palette;
size_t count;
int count;
switch (m_controller->platform()) {
#ifdef M_CORE_GBA
case mPLATFORM_GBA:

View File

@ -74,7 +74,7 @@ RegisterView::RegisterView(std::shared_ptr<CoreController> controller, QWidget*
void RegisterView::addRegisters(const QStringList& names) {
QFormLayout* form = static_cast<QFormLayout*>(layout());
const QFont font = GBAApp::monospaceFont();
const QFont font = GBAApp::app()->monospaceFont();
for (const auto& reg : names) {
QLabel* value = new QLabel;
value->setTextInteractionFlags(Qt::TextSelectableByMouse);

View File

@ -12,7 +12,9 @@
#include <QSysInfo>
#include <QWindow>
#include <mgba/core/serialize.h>
#include <mgba/core/version.h>
#include <mgba-util/png-io.h>
#include <mgba-util/vfs.h>
#include "CoreController.h"
@ -37,8 +39,43 @@
#include <QOpenGLFunctions>
#endif
#ifdef USE_EDITLINE
#include <histedit.h>
#endif
#ifdef USE_FFMPEG
#include <libavcodec/version.h>
#include <libavfilter/version.h>
#include <libavformat/version.h>
#include <libavutil/version.h>
#include <libswscale/version.h>
#ifdef USE_LIBAVRESAMPLE
#include <libavresample/version.h>
#endif
#ifdef USE_LIBSWRESAMPLE
#include <libswresample/version.h>
#endif
#endif
#ifdef USE_LIBZIP
#include <zip.h>
#endif
#ifdef USE_LZMA
#include <7zVersion.h>
#endif
#ifdef BUILD_SDL
#include <SDL_version.h>
#endif
#ifdef USE_SQLITE3
#include "feature/sqlite3/no-intro.h"
#include <sqlite3.h>
#endif
#ifdef USE_ZLIB
#include <zlib.h>
#endif
using namespace QGBA;
@ -84,6 +121,72 @@ void ReportView::generateReport() {
swReport << QString("Build architecture: %1").arg(QSysInfo::buildCpuArchitecture());
swReport << QString("Run architecture: %1").arg(QSysInfo::currentCpuArchitecture());
swReport << QString("Qt version: %1").arg(QLatin1String(qVersion()));
#ifdef USE_FFMPEG
QStringList libavVers;
libavVers << QLatin1String(LIBAVCODEC_IDENT);
libavVers << QLatin1String(LIBAVFILTER_IDENT);
libavVers << QLatin1String(LIBAVFORMAT_IDENT);
#ifdef USE_LIBAVRESAMPLE
libavVers << QLatin1String(LIBAVRESAMPLE_IDENT);
#endif
libavVers << QLatin1String(LIBAVUTIL_IDENT);
#ifdef USE_LIBSWRESAMPLE
libavVers << QLatin1String(LIBSWRESAMPLE_IDENT);
#endif
libavVers << QLatin1String(LIBSWSCALE_IDENT);
#ifdef USE_LIBAV
swReport << QString("Libav versions: %1.%2").arg(libavVers.join(", "));
#else
swReport << QString("FFmpeg versions: %1.%2").arg(libavVers.join(", "));
#endif
#else
swReport << QString("FFmpeg not linked");
#endif
#ifdef USE_EDITLINE
swReport << QString("libedit version: %1.%2").arg(LIBEDIT_MAJOR).arg(LIBEDIT_MINOR);
#else
swReport << QString("libedit not linked");
#endif
#ifdef USE_ELF
swReport << QString("libelf linked");
#else
swReport << QString("libelf not linked");
#endif
#ifdef USE_PNG
swReport << QString("libpng version: %1").arg(QLatin1String(PNG_LIBPNG_VER_STRING));
#else
swReport << QString("libpng not linked");
#endif
#ifdef USE_LIBZIP
swReport << QString("libzip version: %1").arg(QLatin1String(LIBZIP_VERSION));
#else
swReport << QString("libzip not linked");
#endif
#ifdef USE_LZMA
swReport << QString("libLZMA version: %1").arg(QLatin1String(MY_VERSION_NUMBERS));
#else
swReport << QString("libLZMA not linked");
#endif
#ifdef USE_MINIZIP
swReport << QString("minizip linked");
#else
swReport << QString("minizip not linked");
#endif
#ifdef BUILD_SDL
swReport << QString("SDL version: %1.%2.%3").arg(SDL_MAJOR_VERSION).arg(SDL_MINOR_VERSION).arg(SDL_PATCHLEVEL);
#else
swReport << QString("SDL not linked");
#endif
#ifdef USE_SQLITE3
swReport << QString("SQLite3 version: %1").arg(QLatin1String(SQLITE_VERSION));
#else
swReport << QString("SQLite3 not linked");
#endif
#ifdef USE_ZLIB
swReport << QString("zlib version: %1").arg(QLatin1String(ZLIB_VERSION));
#else
swReport << QString("zlib not linked");
#endif
addReport(QString("System info"), swReport.join('\n'));
QStringList hwReport;
@ -209,6 +312,7 @@ void ReportView::generateReport() {
}
void ReportView::save() {
#if defined(USE_LIBZIP) || defined(USE_MINIZIP)
QString filename = GBAApp::app()->getSaveFileName(this, tr("Bug report archive"), tr("ZIP archive (*.zip)"));
if (filename.isNull()) {
return;
@ -228,6 +332,7 @@ void ReportView::save() {
vf.close();
}
zip->close(zip);
#endif
}
void ReportView::setShownReport(const QString& filename) {
@ -243,7 +348,9 @@ void ReportView::rebuildModel() {
}
m_ui.fileList->addItem(item);
}
#if defined(USE_LIBZIP) || defined(USE_MINIZIP)
m_ui.save->setEnabled(true);
#endif
m_ui.fileList->setEnabled(true);
m_ui.fileView->setEnabled(true);
m_ui.openList->setEnabled(true);

View File

@ -0,0 +1,658 @@
/* Copyright (c) 2013-2021 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "SaveConverter.h"
#include <QMessageBox>
#include "GBAApp.h"
#include "LogController.h"
#include "VFileDevice.h"
#include "utils.h"
#ifdef M_CORE_GBA
#include <mgba/gba/core.h>
#include <mgba/internal/gba/serialize.h>
#endif
#ifdef M_CORE_GB
#include <mgba/gb/core.h>
#include <mgba/internal/gb/serialize.h>
#endif
#include <mgba-util/memory.h>
#include <mgba-util/vfs.h>
using namespace QGBA;
SaveConverter::SaveConverter(std::shared_ptr<CoreController> controller, QWidget* parent)
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint)
, m_controller(controller)
{
m_ui.setupUi(this);
connect(m_ui.inputFile, &QLineEdit::textEdited, this, &SaveConverter::refreshInputTypes);
connect(m_ui.inputBrowse, &QAbstractButton::clicked, this, [this]() {
// TODO: Add gameshark saves here too
QStringList formats{"*.sav", "*.sgm", "*.ss0", "*.ss1", "*.ss2", "*.ss3", "*.ss4", "*.ss5", "*.ss6", "*.ss7", "*.ss8", "*.ss9"};
QString filter = tr("Save games and save states (%1)").arg(formats.join(QChar(' ')));
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select save game or save state"), filter);
if (!filename.isEmpty()) {
m_ui.inputFile->setText(filename);
refreshInputTypes();
}
});
connect(m_ui.inputType, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &SaveConverter::refreshOutputTypes);
connect(m_ui.outputFile, &QLineEdit::textEdited, this, &SaveConverter::checkCanConvert);
connect(m_ui.outputBrowse, &QAbstractButton::clicked, this, [this]() {
// TODO: Add gameshark saves here too
QStringList formats{"*.sav", "*.sgm"};
QString filter = tr("Save games (%1)").arg(formats.join(QChar(' ')));
QString filename = GBAApp::app()->getSaveFileName(this, tr("Select save game"), filter);
if (!filename.isEmpty()) {
m_ui.outputFile->setText(filename);
checkCanConvert();
}
});
connect(m_ui.outputType, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &SaveConverter::checkCanConvert);
connect(this, &QDialog::accepted, this, &SaveConverter::convert);
refreshInputTypes();
m_ui.buttonBox->button(QDialogButtonBox::Save)->setDisabled(true);
}
void SaveConverter::convert() {
if (m_validSaves.isEmpty() || m_validOutputs.isEmpty()) {
return;
}
const AnnotatedSave& input = m_validSaves[m_ui.inputType->currentIndex()];
const AnnotatedSave& output = m_validOutputs[m_ui.outputType->currentIndex()];
QByteArray converted = input.convertTo(output);
if (converted.isEmpty()) {
QMessageBox* failure = new QMessageBox(QMessageBox::Warning, tr("Conversion failed"), tr("Failed to convert the save game. This is probably a bug."),
QMessageBox::Ok, this, Qt::Sheet);
failure->setAttribute(Qt::WA_DeleteOnClose);
failure->show();
return;
}
QFile out(m_ui.outputFile->text());
out.open(QIODevice::WriteOnly | QIODevice::Truncate);
out.write(converted);
out.close();
}
void SaveConverter::refreshInputTypes() {
m_validSaves.clear();
m_ui.inputType->clear();
if (m_ui.inputFile->text().isEmpty()) {
m_ui.inputType->addItem(tr("No file selected"));
m_ui.inputType->setEnabled(false);
return;
}
std::shared_ptr<VFileDevice> vf = std::make_shared<VFileDevice>(m_ui.inputFile->text(), QIODevice::ReadOnly);
if (!vf->isOpen()) {
m_ui.inputType->addItem(tr("Could not open file"));
m_ui.inputType->setEnabled(false);
return;
}
detectFromSavestate(*vf);
detectFromSize(vf);
for (const auto& save : m_validSaves) {
m_ui.inputType->addItem(save);
}
if (m_validSaves.count()) {
m_ui.inputType->setEnabled(true);
} else {
m_ui.inputType->addItem(tr("No valid formats found"));
m_ui.inputType->setEnabled(false);
}
}
void SaveConverter::refreshOutputTypes() {
m_ui.outputType->clear();
if (m_validSaves.isEmpty()) {
m_ui.outputType->addItem(tr("Please select a valid input file"));
m_ui.outputType->setEnabled(false);
return;
}
m_validOutputs = m_validSaves[m_ui.inputType->currentIndex()].possibleConversions();
for (const auto& save : m_validOutputs) {
m_ui.outputType->addItem(save);
}
if (m_validOutputs.count()) {
m_ui.outputType->setEnabled(true);
} else {
m_ui.outputType->addItem(tr("No valid conversions found"));
m_ui.outputType->setEnabled(false);
}
checkCanConvert();
}
void SaveConverter::checkCanConvert() {
QAbstractButton* button = m_ui.buttonBox->button(QDialogButtonBox::Save);
if (m_ui.inputFile->text().isEmpty()) {
button->setEnabled(false);
return;
}
if (m_ui.outputFile->text().isEmpty()) {
button->setEnabled(false);
return;
}
if (!m_ui.inputType->isEnabled()) {
button->setEnabled(false);
return;
}
if (!m_ui.outputType->isEnabled()) {
button->setEnabled(false);
return;
}
button->setEnabled(true);
}
void SaveConverter::detectFromSavestate(VFile* vf) {
mPlatform platform = getStatePlatform(vf);
if (platform == mPLATFORM_NONE) {
return;
}
QByteArray extSavedata = getExtdata(vf, platform, EXTDATA_SAVEDATA);
if (!extSavedata.size()) {
return;
}
QByteArray state = getState(vf, platform);
AnnotatedSave save{platform, std::make_shared<VFileDevice>(extSavedata)};
switch (platform) {
#ifdef M_CORE_GBA
case mPLATFORM_GBA:
save.gba.type = static_cast<SavedataType>(state.at(offsetof(GBASerializedState, savedata.type)));
if (save.gba.type == SAVEDATA_EEPROM || save.gba.type == SAVEDATA_EEPROM512) {
save.endianness = Endian::LITTLE;
}
break;
#endif
#ifdef M_CORE_GB
case mPLATFORM_GB:
// GB savestates don't store the MBC type...should probably fix that
save.gb.type = GB_MBC_AUTODETECT;
if (state.size() == 0x100) {
// MBC2 packed save
save.endianness = Endian::LITTLE;
save.gb.type = GB_MBC2;
}
break;
#endif
default:
break;
}
m_validSaves.append(save);
}
void SaveConverter::detectFromSize(std::shared_ptr<VFileDevice> vf) {
#ifdef M_CORE_GBA
switch (vf->size()) {
case SIZE_CART_SRAM:
m_validSaves.append(AnnotatedSave{SAVEDATA_SRAM, vf});
break;
case SIZE_CART_FLASH512:
m_validSaves.append(AnnotatedSave{SAVEDATA_FLASH512, vf});
break;
case SIZE_CART_FLASH1M:
m_validSaves.append(AnnotatedSave{SAVEDATA_FLASH1M, vf});
break;
case SIZE_CART_EEPROM:
m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM, vf, Endian::LITTLE});
m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM, vf, Endian::BIG});
break;
case SIZE_CART_EEPROM512:
m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM512, vf, Endian::LITTLE});
m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM512, vf, Endian::BIG});
break;
}
#endif
#ifdef M_CORE_GB
switch (vf->size()) {
case 0x800:
case 0x82C:
case 0x830:
case 0x2000:
case 0x202C:
case 0x2030:
case 0x8000:
case 0x802C:
case 0x8030:
case 0x10000:
case 0x1002C:
case 0x10030:
case 0x20000:
case 0x2002C:
case 0x20030:
m_validSaves.append(AnnotatedSave{GB_MBC_AUTODETECT, vf});
break;
case 0x100:
m_validSaves.append(AnnotatedSave{GB_MBC2, vf, Endian::LITTLE});
m_validSaves.append(AnnotatedSave{GB_MBC2, vf, Endian::BIG});
break;
case 0x200:
m_validSaves.append(AnnotatedSave{GB_MBC2, vf});
break;
case GB_SIZE_MBC6_FLASH: // Flash only
case GB_SIZE_MBC6_FLASH + 0x8000: // Concatenated SRAM and flash
m_validSaves.append(AnnotatedSave{GB_MBC6, vf});
break;
case 0x20:
m_validSaves.append(AnnotatedSave{GB_TAMA5, vf});
break;
}
#endif
}
mPlatform SaveConverter::getStatePlatform(VFile* vf) {
uint32_t magic;
void* state = nullptr;
struct mCore* core = nullptr;
mPlatform platform = mPLATFORM_NONE;
#ifdef M_CORE_GBA
if (platform == mPLATFORM_NONE) {
core = GBACoreCreate();
core->init(core);
state = mCoreExtractState(core, vf, nullptr);
core->deinit(core);
if (state) {
LOAD_32LE(magic, 0, state);
if (magic - GBASavestateMagic <= GBASavestateVersion) {
platform = mPLATFORM_GBA;
}
mappedMemoryFree(state, core->stateSize(core));
}
}
#endif
#ifdef M_CORE_GB
if (platform == mPLATFORM_NONE) {
core = GBCoreCreate();
core->init(core);
state = mCoreExtractState(core, vf, nullptr);
core->deinit(core);
if (state) {
LOAD_32LE(magic, 0, state);
if (magic - GBSavestateMagic <= GBSavestateVersion) {
platform = mPLATFORM_GB;
}
mappedMemoryFree(state, core->stateSize(core));
}
}
#endif
return platform;
}
QByteArray SaveConverter::getState(VFile* vf, mPlatform platform) {
QByteArray bytes;
struct mCore* core = mCoreCreate(platform);
core->init(core);
void* state = mCoreExtractState(core, vf, nullptr);
if (state) {
size_t size = core->stateSize(core);
bytes = QByteArray::fromRawData(static_cast<const char*>(state), size);
bytes.data(); // Trigger a deep copy before we delete the backing
mappedMemoryFree(state, size);
}
core->deinit(core);
return bytes;
}
QByteArray SaveConverter::getExtdata(VFile* vf, mPlatform platform, mStateExtdataTag extdataType) {
mStateExtdata extdata;
mStateExtdataInit(&extdata);
QByteArray bytes;
struct mCore* core = mCoreCreate(platform);
core->init(core);
if (mCoreExtractExtdata(core, vf, &extdata)) {
mStateExtdataItem extitem;
if (mStateExtdataGet(&extdata, extdataType, &extitem) && extitem.size) {
bytes = QByteArray::fromRawData(static_cast<const char*>(extitem.data), extitem.size);
bytes.data(); // Trigger a deep copy before we delete the backing
}
}
core->deinit(core);
mStateExtdataDeinit(&extdata);
return bytes;
}
SaveConverter::AnnotatedSave::AnnotatedSave()
: savestate(false)
, platform(mPLATFORM_NONE)
, size(0)
, backing()
, endianness(Endian::NONE)
{
}
SaveConverter::AnnotatedSave::AnnotatedSave(mPlatform platform, std::shared_ptr<VFileDevice> vf, Endian endianness)
: savestate(true)
, platform(platform)
, size(vf->size())
, backing(vf)
, endianness(endianness)
{
}
#ifdef M_CORE_GBA
SaveConverter::AnnotatedSave::AnnotatedSave(SavedataType type, std::shared_ptr<VFileDevice> vf, Endian endianness)
: savestate(false)
, platform(mPLATFORM_GBA)
, size(vf->size())
, backing(vf)
, endianness(endianness)
, gba({type})
{
}
#endif
#ifdef M_CORE_GB
SaveConverter::AnnotatedSave::AnnotatedSave(GBMemoryBankControllerType type, std::shared_ptr<VFileDevice> vf, Endian endianness)
: savestate(false)
, platform(mPLATFORM_GB)
, size(vf->size())
, backing(vf)
, endianness(endianness)
, gb({type})
{
}
#endif
SaveConverter::AnnotatedSave SaveConverter::AnnotatedSave::asRaw() const {
AnnotatedSave raw;
raw.platform = platform;
raw.size = size;
raw.endianness = endianness;
switch (platform) {
#ifdef M_CORE_GBA
case mPLATFORM_GBA:
raw.gba = gba;
break;
#endif
#ifdef M_CORE_GB
case mPLATFORM_GB:
raw.gb = gb;
break;
#endif
default:
break;
}
return raw;
}
SaveConverter::AnnotatedSave::operator QString() const {
QString sizeStr(niceSizeFormat(size));
QString typeFormat("%1");
QString endianStr;
QString saveType;
QString format = QCoreApplication::translate("SaveConverter", "%1 %2 save game");
switch (endianness) {
case Endian::LITTLE:
endianStr = QCoreApplication::translate("SaveConverter", "little endian");
break;
case Endian::BIG:
endianStr = QCoreApplication::translate("SaveConverter", "big endian");
break;
default:
break;
}
switch (platform) {
#ifdef M_CORE_GBA
case mPLATFORM_GBA:
switch (gba.type) {
case SAVEDATA_SRAM:
typeFormat = QCoreApplication::translate("SaveConverter", "SRAM");
break;
case SAVEDATA_FLASH512:
case SAVEDATA_FLASH1M:
typeFormat = QCoreApplication::translate("SaveConverter", "%1 flash");
break;
case SAVEDATA_EEPROM:
case SAVEDATA_EEPROM512:
typeFormat = QCoreApplication::translate("SaveConverter", "%1 EEPROM");
break;
default:
break;
}
break;
#endif
#ifdef M_CORE_GB
case mPLATFORM_GB:
switch (gb.type) {
case GB_MBC_AUTODETECT:
if (size & 0xFF) {
typeFormat = QCoreApplication::translate("SaveConverter", "%1 SRAM + RTC");
} else {
typeFormat = QCoreApplication::translate("SaveConverter", "%1 SRAM");
}
break;
case GB_MBC2:
if (size == 0x100) {
typeFormat = QCoreApplication::translate("SaveConverter", "packed MBC2");
} else {
typeFormat = QCoreApplication::translate("SaveConverter", "unpacked MBC2");
}
break;
case GB_MBC6:
if (size == GB_SIZE_MBC6_FLASH) {
typeFormat = QCoreApplication::translate("SaveConverter", "MBC6 flash");
} else if (size > GB_SIZE_MBC6_FLASH) {
typeFormat = QCoreApplication::translate("SaveConverter", "MBC6 combined SRAM + flash");
} else {
typeFormat = QCoreApplication::translate("SaveConverter", "MBC6 SRAM");
}
break;
case GB_TAMA5:
typeFormat = QCoreApplication::translate("SaveConverter", "TAMA5");
break;
default:
break;
}
break;
#endif
default:
break;
}
saveType = typeFormat.arg(sizeStr);
if (!endianStr.isEmpty()) {
saveType = QCoreApplication::translate("SaveConverter", "%1 (%2)").arg(saveType).arg(endianStr);
}
if (savestate) {
format = QCoreApplication::translate("SaveConverter", "%1 save state with embedded %2 save game");
}
return format.arg(nicePlatformFormat(platform)).arg(saveType);
}
bool SaveConverter::AnnotatedSave::operator==(const AnnotatedSave& other) const {
if (other.savestate != savestate || other.platform != platform || other.size != size || other.endianness != endianness) {
return false;
}
switch (platform) {
#ifdef M_CORE_GBA
case mPLATFORM_GBA:
if (other.gba.type != gba.type) {
return false;
}
break;
#endif
#ifdef M_CORE_GB
case mPLATFORM_GB:
if (other.gb.type != gb.type) {
return false;
}
break;
#endif
default:
break;
}
return true;
}
QList<SaveConverter::AnnotatedSave> SaveConverter::AnnotatedSave::possibleConversions() const {
QList<AnnotatedSave> possible;
AnnotatedSave same = asRaw();
same.backing.reset();
same.savestate = false;
if (savestate) {
possible.append(same);
}
AnnotatedSave endianSwapped = same;
switch (endianness) {
case Endian::LITTLE:
endianSwapped.endianness = Endian::BIG;
possible.append(endianSwapped);
break;
case Endian::BIG:
endianSwapped.endianness = Endian::LITTLE;
possible.append(endianSwapped);
break;
default:
break;
}
switch (platform) {
#ifdef M_CORE_GB
case mPLATFORM_GB:
switch (gb.type) {
case GB_MBC2:
if (size == 0x100) {
AnnotatedSave unpacked = same;
unpacked.size = 0x200;
unpacked.endianness = Endian::NONE;
possible.append(unpacked);
} else {
AnnotatedSave packed = same;
packed.size = 0x100;
packed.endianness = Endian::LITTLE;
possible.append(packed);
packed.endianness = Endian::BIG;
possible.append(packed);
}
break;
case GB_MBC6:
if (size > GB_SIZE_MBC6_FLASH) {
AnnotatedSave separated = same;
separated.size = size - GB_SIZE_MBC6_FLASH;
possible.append(separated);
separated.size = GB_SIZE_MBC6_FLASH;
possible.append(separated);
}
break;
default:
break;
}
break;
#endif
default:
break;
}
return possible;
}
QByteArray SaveConverter::AnnotatedSave::convertTo(const SaveConverter::AnnotatedSave& target) const {
QByteArray converted;
QByteArray buffer;
backing->seek(0);
if (target == asRaw()) {
return backing->readAll();
}
if (platform != target.platform) {
LOG(QT, ERROR) << tr("Cannot convert save games between platforms");
return {};
}
switch (platform) {
#ifdef M_CORE_GBA
case mPLATFORM_GBA:
switch (gba.type) {
case SAVEDATA_EEPROM:
case SAVEDATA_EEPROM512:
converted.resize(target.size);
buffer = backing->readAll();
for (int i = 0; i < size; i += 8) {
uint64_t word;
const uint64_t* in = reinterpret_cast<const uint64_t*>(buffer.constData());
uint64_t* out = reinterpret_cast<uint64_t*>(converted.data());
LOAD_64LE(word, i, in);
STORE_64BE(word, i, out);
}
break;
default:
break;
}
break;
#endif
#ifdef M_CORE_GB
case mPLATFORM_GB:
switch (gb.type) {
case GB_MBC2:
converted.reserve(target.size);
buffer = backing->readAll();
if (size == 0x100 && target.size == 0x200) {
if (endianness == Endian::LITTLE) {
for (uint8_t byte : buffer) {
converted.append(0xF0 | (byte & 0xF));
converted.append(0xF0 | (byte >> 4));
}
} else if (endianness == Endian::BIG) {
for (uint8_t byte : buffer) {
converted.append(0xF0 | (byte >> 4));
converted.append(0xF0 | (byte & 0xF));
}
}
} else if (size == 0x200 && target.size == 0x100) {
uint8_t byte;
if (target.endianness == Endian::LITTLE) {
for (int i = 0; i < target.size; ++i) {
byte = buffer[i * 2] & 0xF;
byte |= (buffer[i * 2 + 1] & 0xF) << 4;
converted.append(byte);
}
} else if (target.endianness == Endian::BIG) {
for (int i = 0; i < target.size; ++i) {
byte = (buffer[i * 2] & 0xF) << 4;
byte |= buffer[i * 2 + 1] & 0xF;
converted.append(byte);
}
}
} else if (size == 0x100 && target.size == 0x100) {
for (uint8_t byte : buffer) {
converted.append((byte >> 4) | (byte << 4));
}
}
break;
case GB_MBC6:
if (size == target.size + GB_SIZE_MBC6_FLASH) {
converted = backing->read(target.size);
} else if (target.size == GB_SIZE_MBC6_FLASH) {
backing->seek(size - GB_SIZE_MBC6_FLASH);
converted = backing->read(GB_SIZE_MBC6_FLASH);
}
break;
default:
break;
}
break;
#endif
default:
break;
}
return converted;
}

View File

@ -0,0 +1,102 @@
/* Copyright (c) 2013-2021 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#pragma once
#include <QDialog>
#include "CoreController.h"
#include "utils.h"
#ifdef M_CORE_GBA
#include <mgba/gba/core.h>
#include <mgba/internal/gba/savedata.h>
#endif
#ifdef M_CORE_GB
#include <mgba/gb/core.h>
#include <mgba/gb/interface.h>
#endif
#include <mgba/core/serialize.h>
#include "ui_SaveConverter.h"
struct VFile;
namespace QGBA {
class SaveConverter : public QDialog {
Q_OBJECT
public:
SaveConverter(std::shared_ptr<CoreController> controller, QWidget* parent = nullptr);
static mPlatform getStatePlatform(VFile*);
static QByteArray getState(VFile*, mPlatform);
static QByteArray getExtdata(VFile*, mPlatform, mStateExtdataTag);
public slots:
void convert();
private slots:
void refreshInputTypes();
void refreshOutputTypes();
void checkCanConvert();
private:
#ifdef M_CORE_GBA
struct GBASave {
SavedataType type;
};
#endif
#ifdef M_CORE_GB
struct GBSave {
GBMemoryBankControllerType type;
};
#endif
struct AnnotatedSave {
AnnotatedSave();
AnnotatedSave(mPlatform, std::shared_ptr<VFileDevice>, Endian = Endian::NONE);
#ifdef M_CORE_GBA
AnnotatedSave(SavedataType, std::shared_ptr<VFileDevice>, Endian = Endian::NONE);
#endif
#ifdef M_CORE_GB
AnnotatedSave(GBMemoryBankControllerType, std::shared_ptr<VFileDevice>, Endian = Endian::NONE);
#endif
AnnotatedSave asRaw() const;
operator QString() const;
bool operator==(const AnnotatedSave&) const;
QList<AnnotatedSave> possibleConversions() const;
QByteArray convertTo(const AnnotatedSave&) const;
bool savestate;
mPlatform platform;
ssize_t size;
std::shared_ptr<VFileDevice> backing;
Endian endianness;
union {
#ifdef M_CORE_GBA
GBASave gba;
#endif
#ifdef M_CORE_GB
GBSave gb;
#endif
};
};
void detectFromSavestate(VFile*);
void detectFromSize(std::shared_ptr<VFileDevice>);
void detectFromHeaders(std::shared_ptr<VFileDevice>);
Ui::SaveConverter m_ui;
std::shared_ptr<CoreController> m_controller;
QList<AnnotatedSave> m_validSaves;
QList<AnnotatedSave> m_validOutputs;
};
}

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SaveConverter</class>
<widget class="QDialog" name="SaveConverter">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>546</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Convert/Extract Save Game</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Input file</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLineEdit" name="inputFile"/>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="inputBrowse">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QComboBox" name="inputType">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Output file</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLineEdit" name="outputFile"/>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="outputBrowse">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QComboBox" name="outputType">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>inputFile</tabstop>
<tabstop>inputBrowse</tabstop>
<tabstop>inputType</tabstop>
<tabstop>outputFile</tabstop>
<tabstop>outputBrowse</tabstop>
<tabstop>outputType</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SaveConverter</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SaveConverter</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -8,6 +8,7 @@
#include "CoreController.h"
#include "GamepadAxisEvent.h"
#include "InputController.h"
#include "utils.h"
#include <mgba/core/core.h>
#include <mgba/internal/gba/gba.h>
@ -129,7 +130,7 @@ void SensorView::updateSensors() {
}
void SensorView::setLuminanceValue(int value) {
value = std::max(0, std::min(value, 255));
value = clamp(value, 0, 255);
if (m_input) {
m_input->setLuminanceValue(value);
}

View File

@ -883,7 +883,7 @@
<item row="11" column="0">
<widget class="QLabel" name="label_24">
<property name="text">
<string>Savestate extra data:</string>
<string>Save state extra data:</string>
</property>
</widget>
</item>
@ -900,7 +900,7 @@
<item row="12" column="1">
<widget class="QCheckBox" name="saveStateSave">
<property name="text">
<string>Save data</string>
<string>Save game</string>
</property>
<property name="checked">
<bool>true</bool>
@ -927,7 +927,7 @@
<item row="15" column="0">
<widget class="QLabel" name="label_25">
<property name="text">
<string>Load extra data:</string>
<string>Load state extra data:</string>
</property>
</widget>
</item>
@ -944,7 +944,7 @@
<item row="16" column="1">
<widget class="QCheckBox" name="loadStateSave">
<property name="text">
<string>Save data</string>
<string>Save game</string>
</property>
</widget>
</item>

View File

@ -10,6 +10,7 @@
#include "ShortcutController.h"
#include "ShortcutModel.h"
#include <QFontMetrics>
#include <QKeyEvent>
using namespace QGBA;
@ -134,6 +135,15 @@ void ShortcutView::closeEvent(QCloseEvent*) {
}
}
void ShortcutView::showEvent(QShowEvent*) {
QString longString("Ctrl+Alt+Shift+Tab");
int width = QFontMetrics(QFont()).width(longString);
QHeaderView* header = m_ui.shortcutTable->header();
header->resizeSection(0, header->length() - width * 2);
header->resizeSection(1, width);
header->resizeSection(2, width);
}
bool ShortcutView::event(QEvent* event) {
if (m_input) {
QEvent::Type type = event->type();

View File

@ -32,6 +32,7 @@ public:
protected:
virtual bool event(QEvent*) override;
virtual void closeEvent(QCloseEvent*) override;
virtual void showEvent(QShowEvent*) override;
private slots:
void load(const QModelIndex&);

View File

@ -16,6 +16,9 @@
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeView" name="shortcutTable">
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOn</enum>
</property>
<attribute name="headerDefaultSectionSize">
<number>120</number>
</attribute>

View File

@ -91,6 +91,17 @@ VFileDevice::VFileDevice(const QString& filename, QIODevice::OpenMode mode, QObj
}
}
VFileDevice::VFileDevice(const QByteArray& mem, QObject* parent)
: QIODevice(parent)
, m_vf(VFileMemChunk(mem.constData(), mem.size()))
{
setOpenMode(QIODevice::ReadWrite);
}
VFileDevice::~VFileDevice() {
close();
}
void VFileDevice::close() {
if (!m_vf) {
return;
@ -117,6 +128,13 @@ VFileDevice& VFileDevice::operator=(VFile* vf) {
return *this;
}
VFile* VFileDevice::take() {
VFile* vf = m_vf;
m_vf = nullptr;
QIODevice::close();
return vf;
}
qint64 VFileDevice::readData(char* data, qint64 maxSize) {
return m_vf->read(m_vf, data, maxSize);
}

View File

@ -19,7 +19,9 @@ Q_OBJECT
public:
VFileDevice(VFile* vf = nullptr, QObject* parent = nullptr);
VFileDevice(const QByteArray& mem, QObject* parent = nullptr);
VFileDevice(const QString&, QIODevice::OpenMode, QObject* parent = nullptr);
virtual ~VFileDevice();
virtual void close() override;
virtual bool seek(qint64 pos) override;
@ -29,6 +31,7 @@ public:
VFileDevice& operator=(VFile*);
operator VFile*() { return m_vf; }
VFile* take();
static VFile* wrap(QIODevice*, QIODevice::OpenMode);
static VFile* wrap(QFileDevice*, QIODevice::OpenMode);

View File

@ -9,6 +9,7 @@
#include "GBAApp.h"
#include "LogController.h"
#include "utils.h"
#include <mgba-util/math.h>
@ -522,11 +523,7 @@ void VideoView::setPreset(const Preset& preset) {
QSize VideoView::maintainAspect(const QSize& size) {
QSize ds = size;
if (ds.width() * m_nativeHeight > ds.height() * m_nativeWidth) {
ds.setWidth(ds.height() * m_nativeWidth / m_nativeHeight);
} else if (ds.width() * m_nativeHeight < ds.height() * m_nativeWidth) {
ds.setHeight(ds.width() * m_nativeHeight / m_nativeWidth);
}
lockAspectRatio(QSize(m_nativeWidth, m_nativeHeight), ds);
return ds;
}

View File

@ -49,12 +49,14 @@
#include "PrinterView.h"
#include "ReportView.h"
#include "ROMInfo.h"
#include "SaveConverter.h"
#include "SensorView.h"
#include "ShaderSelector.h"
#include "ShortcutController.h"
#include "TileView.h"
#include "VideoProxy.h"
#include "VideoView.h"
#include "utils.h"
#ifdef USE_DISCORD_RPC
#include "DiscordCoordinator.h"
@ -417,8 +419,8 @@ void Window::replaceROM() {
void Window::selectSave(bool temporary) {
QStringList formats{"*.sav"};
QString filter = tr("Game Boy Advance save files (%1)").arg(formats.join(QChar(' ')));
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select save"), filter);
QString filter = tr("Save games (%1)").arg(formats.join(QChar(' ')));
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select save game"), filter);
if (!filename.isEmpty()) {
m_controller->loadSave(filename, temporary);
}
@ -426,14 +428,14 @@ void Window::selectSave(bool temporary) {
void Window::selectState(bool load) {
QStringList formats{"*.ss0", "*.ss1", "*.ss2", "*.ss3", "*.ss4", "*.ss5", "*.ss6", "*.ss7", "*.ss8", "*.ss9"};
QString filter = tr("mGBA savestate files (%1)").arg(formats.join(QChar(' ')));
QString filter = tr("mGBA save state files (%1)").arg(formats.join(QChar(' ')));
if (load) {
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select savestate"), filter);
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select save state"), filter);
if (!filename.isEmpty()) {
m_controller->loadState(filename);
}
} else {
QString filename = GBAApp::app()->getSaveFileName(this, tr("Select savestate"), filter);
QString filename = GBAApp::app()->getSaveFileName(this, tr("Select save state"), filter);
if (!filename.isEmpty()) {
m_controller->saveState(filename);
}
@ -1183,10 +1185,10 @@ void Window::setupMenu(QMenuBar* menubar) {
m_actions.addAction(tr("Add folder to library..."), "addDirToLibrary", this, &Window::addDirToLibrary, "file");
#endif
addGameAction(tr("Load alternate save..."), "loadAlternateSave", [this]() {
addGameAction(tr("Load alternate save game..."), "loadAlternateSave", [this]() {
this->selectSave(false);
}, "file");
addGameAction(tr("Load temporary save..."), "loadTemporarySave", [this]() {
addGameAction(tr("Load temporary save game..."), "loadTemporarySave", [this]() {
this->selectSave(true);
}, "file");
@ -1294,6 +1296,8 @@ void Window::setupMenu(QMenuBar* menubar) {
#ifdef M_CORE_GBA
m_actions.addSeparator("file");
m_actions.addAction(tr("Convert save game..."), "convertSave", openControllerTView<SaveConverter>(), "file");
Action* importShark = addGameAction(tr("Import GameShark Save..."), "importShark", this, &Window::importSharkport, "file");
m_platformActions.insert(mPLATFORM_GBA, importShark);
@ -1980,9 +1984,7 @@ void Window::attachDisplay() {
void Window::setLogo() {
m_screenWidget->setPixmap(m_logo);
m_screenWidget->setCenteredAspectRatio(m_logo.width(), m_logo.height());
m_screenWidget->setDimensions(m_logo.width(), m_logo.height());
m_screenWidget->setLockIntegerScaling(false);
m_screenWidget->setLockAspectRatio(true);
m_screenWidget->filter(true);
m_screenWidget->unsetCursor();
}
@ -2037,30 +2039,6 @@ void WindowBackground::paintEvent(QPaintEvent* event) {
QPainter painter(this);
painter.setRenderHint(QPainter::SmoothPixmapTransform, m_filter);
painter.fillRect(QRect(QPoint(), size()), Qt::black);
QSize s = size();
QSize ds = s;
if (m_centered) {
if (ds.width() * m_aspectHeight < ds.height() * m_aspectWidth) {
ds.setWidth(ds.height() * m_aspectWidth / m_aspectHeight);
} else if (ds.width() * m_aspectHeight > ds.height() * m_aspectWidth) {
ds.setHeight(ds.width() * m_aspectHeight / m_aspectWidth);
}
} else if (m_lockAspectRatio) {
if (ds.width() * m_aspectHeight > ds.height() * m_aspectWidth) {
ds.setWidth(ds.height() * m_aspectWidth / m_aspectHeight);
} else if (ds.width() * m_aspectHeight < ds.height() * m_aspectWidth) {
ds.setHeight(ds.width() * m_aspectHeight / m_aspectWidth);
}
}
if (m_lockIntegerScaling) {
if (ds.width() >= m_aspectWidth) {
ds.setWidth(ds.width() - ds.width() % m_aspectWidth);
}
if (ds.height() >= m_aspectHeight) {
ds.setHeight(ds.height() - ds.height() % m_aspectHeight);
}
}
QPoint origin = QPoint((s.width() - ds.width()) / 2, (s.height() - ds.height()) / 2);
QRect full(origin, ds);
QRect full(clampSize(QSize(m_aspectWidth, m_aspectHeight), size(), m_lockAspectRatio, m_lockIntegerScaling, m_centered));
painter.drawPixmap(full, logo);
}

View File

@ -13,6 +13,7 @@
#include "InputModel.h"
#include "InputProfile.h"
#include "LogController.h"
#include "utils.h"
#include <QApplication>
#include <QKeyEvent>
@ -1002,7 +1003,7 @@ void InputController::decreaseLuminanceLevel() {
void InputController::setLuminanceLevel(int level) {
int value = 0x16;
level = std::max(0, std::min(10, level));
level = clamp(level, 0, 10);
if (level > 0) {
value += GBA_LUX_LEVELS[level - 1];
}

View File

@ -108,10 +108,10 @@ QPair<QString, QString> LibraryController::selectedPath() {
return e ? qMakePair(e->base(), e->filename()) : qMakePair<QString, QString>("", "");
}
void LibraryController::addDirectory(const QString& dir) {
void LibraryController::addDirectory(const QString& dir, bool recursive) {
// The worker thread temporarily owns the library
std::shared_ptr<mLibrary> library = m_library;
m_libraryJob = GBAApp::app()->submitWorkerJob(std::bind(&LibraryController::loadDirectory, this, dir), this, [this, library]() {
m_libraryJob = GBAApp::app()->submitWorkerJob(std::bind(&LibraryController::loadDirectory, this, dir, recursive), this, [this, library]() {
m_libraryJob = -1;
refresh();
});
@ -181,10 +181,10 @@ void LibraryController::selectLastBootedGame() {
}
}
void LibraryController::loadDirectory(const QString& dir) {
// This class can get delted during this function (sigh) so we need to hold onto this
void LibraryController::loadDirectory(const QString& dir, bool recursive) {
// This class can get deleted during this function (sigh) so we need to hold onto this
std::shared_ptr<mLibrary> library = m_library;
mLibraryLoadDirectory(library.get(), dir.toUtf8().constData());
mLibraryLoadDirectory(library.get(), dir.toUtf8().constData(), recursive);
}
void LibraryController::freeLibrary() {
@ -192,4 +192,4 @@ void LibraryController::freeLibrary() {
mLibraryEntryFree(mLibraryListingGetPointer(&m_listing, i));
}
mLibraryListingClear(&m_listing);
}
}

View File

@ -84,7 +84,7 @@ public:
void selectLastBootedGame();
void addDirectory(const QString& dir);
void addDirectory(const QString& dir, bool recursive = true);
public slots:
void clear();
@ -97,7 +97,7 @@ private slots:
void refresh();
private:
void loadDirectory(const QString&); // Called on separate thread
void loadDirectory(const QString&, bool recursive = true); // Called on separate thread
void freeLibrary();
ConfigController* m_config = nullptr;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -11,16 +11,16 @@ namespace QGBA {
QString niceSizeFormat(size_t filesize) {
double size = filesize;
QString unit = "B";
QString unit = QObject::tr("%1 byte");
if (size >= 1024.0) {
size /= 1024.0;
unit = "kiB";
unit = QObject::tr("%1 kiB");
}
if (size >= 1024.0) {
size /= 1024.0;
unit = "MiB";
unit = QObject::tr("%1 MiB");
}
return QString("%0 %1").arg(size, 0, 'f', 1).arg(unit);
return unit.arg(size, 0, 'f', int(size * 10) % 10 ? 1 : 0);
}
QString nicePlatformFormat(mPlatform platform) {
switch (platform) {

View File

@ -7,11 +7,70 @@
#include <mgba/core/core.h>
#include <QRect>
#include <QSize>
#include <QString>
#include <algorithm>
namespace QGBA {
enum class Endian {
NONE = 0b00,
BIG = 0b01,
LITTLE = 0b10,
UNKNOWN = 0b11
};
QString niceSizeFormat(size_t filesize);
QString nicePlatformFormat(mPlatform platform);
inline void lockAspectRatio(const QSize& ref, QSize& size) {
if (size.width() * ref.height() > size.height() * ref.width()) {
size.setWidth(size.height() * ref.width() / ref.height());
} else if (size.width() * ref.height() < size.height() * ref.width()) {
size.setHeight(size.width() * ref.height() / ref.width());
}
}
inline void zoomAspectRatio(const QSize& ref, QSize& size) {
if (size.width() * ref.height() < size.height() * ref.width()) {
size.setWidth(size.height() * ref.width() / ref.height());
} else if (size.width() * ref.height() > size.height() * ref.width()) {
size.setHeight(size.width() * ref.height() / ref.width());
}
}
inline void lockIntegerScaling(const QSize& ref, QSize& size) {
if (size.width() >= ref.width()) {
size.setWidth(size.width() - size.width() % ref.width());
}
if (size.height() >= ref.height()) {
size.setHeight(size.height() - size.height() % ref.height());
}
}
inline QRect clampSize(const QSize& ref, const QSize& size, bool aspectRatio, bool integerScaling, bool zoom = false) {
QSize ds = size;
if (zoom) {
zoomAspectRatio(ref, ds);
} else if (aspectRatio) {
lockAspectRatio(ref, ds);
}
if (integerScaling) {
QGBA::lockIntegerScaling(ref, ds);
}
QPoint origin = QPoint((size.width() - ds.width()) / 2, (size.height() - ds.height()) / 2);
return QRect(origin, ds);
}
#if __cplusplus >= 201703L
using std::clamp;
#else
template<class T>
constexpr const T& clamp(const T& v, const T& lo, const T& hi) {
return std::max(lo, std::min(hi, v));
}
#endif
}

View File

@ -61,6 +61,7 @@ void SM83Reset(struct SM83Core* cpu) {
cpu->instruction = 0;
cpu->tMultiplier = 2;
cpu->cycles = 0;
cpu->nextEvent = 0;
cpu->executionState = SM83_CORE_FETCH;
@ -102,7 +103,7 @@ static void _SM83InstructionIRQ(struct SM83Core* cpu) {
}
static void _SM83Step(struct SM83Core* cpu) {
++cpu->cycles;
cpu->cycles += cpu->tMultiplier;
enum SM83ExecutionState state = cpu->executionState;
cpu->executionState = SM83_CORE_IDLE_0;
switch (state) {
@ -147,23 +148,31 @@ static void _SM83Step(struct SM83Core* cpu) {
}
}
static inline bool _SM83TickInternal(struct SM83Core* cpu) {
bool running = true;
_SM83Step(cpu);
int t = cpu->tMultiplier;
if (cpu->cycles + t * 2 >= cpu->nextEvent) {
int32_t diff = cpu->nextEvent - cpu->cycles;
cpu->cycles = cpu->nextEvent;
cpu->executionState += diff >> (t - 1); // NB: This assumes tMultiplier is either 1 or 2
cpu->irqh.processEvents(cpu);
cpu->cycles += (SM83_CORE_EXECUTE - cpu->executionState) * t;
running = false;
} else {
cpu->cycles += t * 2;
}
cpu->executionState = SM83_CORE_FETCH;
cpu->instruction(cpu);
cpu->cycles += t;
return running;
}
void SM83Tick(struct SM83Core* cpu) {
while (cpu->cycles >= cpu->nextEvent) {
cpu->irqh.processEvents(cpu);
}
_SM83Step(cpu);
if (cpu->cycles + 2 >= cpu->nextEvent) {
int32_t diff = cpu->nextEvent - cpu->cycles;
cpu->cycles = cpu->nextEvent;
cpu->executionState += diff;
cpu->irqh.processEvents(cpu);
cpu->cycles += SM83_CORE_EXECUTE - cpu->executionState;
} else {
cpu->cycles += 2;
}
cpu->executionState = SM83_CORE_FETCH;
cpu->instruction(cpu);
++cpu->cycles;
_SM83TickInternal(cpu);
}
void SM83Run(struct SM83Core* cpu) {
@ -173,19 +182,6 @@ void SM83Run(struct SM83Core* cpu) {
cpu->irqh.processEvents(cpu);
break;
}
_SM83Step(cpu);
if (cpu->cycles + 2 >= cpu->nextEvent) {
int32_t diff = cpu->nextEvent - cpu->cycles;
cpu->cycles = cpu->nextEvent;
cpu->executionState += diff;
cpu->irqh.processEvents(cpu);
cpu->cycles += SM83_CORE_EXECUTE - cpu->executionState;
running = false;
} else {
cpu->cycles += 2;
}
cpu->executionState = SM83_CORE_FETCH;
cpu->instruction(cpu);
++cpu->cycles;
running = _SM83TickInternal(cpu) && running;
}
}

View File

@ -210,6 +210,7 @@ void PNGWriteClose(png_structp png, png_infop info) {
bool isPNG(struct VFile* source) {
png_byte header[PNG_HEADER_BYTES];
source->seek(source, 0, SEEK_SET);
if (source->read(source, header, PNG_HEADER_BYTES) < PNG_HEADER_BYTES) {
return false;
}