all repos — mgba @ 68e70b61f19b1e2b34e39be0c4dff35a84f2d313

mGBA Game Boy Advance Emulator

Merge branch 'master' into port/psp2
Jeffrey Pfau jeffrey@endrift.com
Sun, 26 Jul 2015 19:43:11 -0700
commit

68e70b61f19b1e2b34e39be0c4dff35a84f2d313

parent

d957736ed95751328f6913b1954cdeac0bf7e80d

60 files changed, 1459 insertions(+), 316 deletions(-)

jump to
M CHANGESCHANGES

@@ -24,6 +24,11 @@ - Ability to boot directly into the BIOS

- Preliminary support for yanking out the game pak while a game is running - Thumb-drive mode by putting a file called portable.ini in the same folder - Configurable display driver, between software and OpenGL + - Undo-able savestate loading and saving + - Controller profiles now store shortcut settings + - Default controller profiles for several common controllers + - Libretro now supports BIOS and rumble + - Implement BIOS call Stop, for sleep mode Bugfixes: - ARM7: Fix SWI and IRQ timings - GBA Audio: Force audio FIFOs to 32-bit

@@ -58,6 +63,7 @@ - ARM7: ARMHotplugDetach should call deinit

- Qt: Fix window being too tall after exiting fullscreen - Qt: Fix a missing va_end call in the log handler lambda within the GameController constructor - GBA Cheats: Fix Pro Action Replay and GameShark issues when used together + - Qt: Fix analog buttons not getting unmapped Misc: - Qt: Handle saving input settings better - Debugger: Free watchpoints in addition to breakpoints

@@ -100,6 +106,9 @@ - All: Proper handling of Unicode file paths

- GBA Video: Slightly optimize mode 0 mosaic rendering - VFS: Add sync method to force syncing with backing - GBA: Savedata is now synced shortly after data finishes being written + - GBA Input: Allow axes and buttons to be mapped to the same key + - GBA BIOS: Stub out SoundBias + - Qt: Gamepads can now have both buttons and analog axes mapped to the same key 0.2.1: (2015-05-13) Bugfixes:
M CMakeLists.txtCMakeLists.txt

@@ -43,7 +43,11 @@ if(NOT CMAKE_BUILD_TYPE)

set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type (e.g. Release or Debug)" FORCE) endif() -set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib") +if (NOT DEFINED LIBDIR) + set(LIBDIR "lib") +endif() + +set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${LIBDIR}") include(GNUInstallDirs)

@@ -76,51 +80,16 @@ endforeach()

endfunction() # Version information -set(LIB_VERSION_MAJOR 0) -set(LIB_VERSION_MINOR 3) -set(LIB_VERSION_PATCH 0) -set(LIB_VERSION_ABI 0.3) -set(LIB_VERSION_STRING ${LIB_VERSION_MAJOR}.${LIB_VERSION_MINOR}.${LIB_VERSION_PATCH}) - -execute_process(COMMAND git describe --always --abbrev=40 --dirty OUTPUT_VARIABLE GIT_COMMIT ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) -execute_process(COMMAND git describe --always --dirty OUTPUT_VARIABLE GIT_COMMIT_SHORT ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) -execute_process(COMMAND git symbolic-ref --short HEAD OUTPUT_VARIABLE GIT_BRANCH ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) -execute_process(COMMAND git rev-list HEAD --count OUTPUT_VARIABLE GIT_REV ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) -execute_process(COMMAND git describe --tag --exact-match OUTPUT_VARIABLE GIT_TAG ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) - -if(GIT_REV STREQUAL "") - set(GIT_REV -1) -endif() -if(NOT GIT_TAG STREQUAL "") - set(VERSION_STRING ${GIT_TAG}) -elseif(GIT_BRANCH STREQUAL "") - set(VERSION_STRING ${LIB_VERSION_STRING}) -else() - if(GIT_BRANCH STREQUAL "master") - set(VERSION_STRING ${GIT_REV}-${GIT_COMMIT_SHORT}) - else() - set(VERSION_STRING ${GIT_BRANCH}-${GIT_REV}-${GIT_COMMIT_SHORT}) - endif() - - if(NOT LIB_VERSION_ABI STREQUAL GIT_BRANCH) - set(VERSION_STRING ${LIB_VERSION_ABI}-${VERSION_STRING}) - endif() -endif() - add_custom_target(version-info ALL ${CMAKE_COMMAND} -E touch ${CMAKE_SOURCE_DIR}/src/util/version.c.in COMMAND ${CMAKE_COMMAND} - -DGIT_COMMIT=${GIT_COMMIT} - -DGIT_COMMIT_SHORT=${GIT_COMMIT_SHORT} - -DGIT_BRANCH=${GIT_BRANCH} - -DGIT_REV=${GIT_REV} -DBINARY_NAME=${BINARY_NAME} - -DPROJECT_NAME=${PROJECT_NAME} - -DVERSION_STRING=${VERSION_STRING} - -D${BINARY_NAME}_SOURCE_DIR=${CMAKE_SOURCE_DIR} + -DCONFIG_FILE=${CMAKE_SOURCE_DIR}/src/util/version.c.in + -DOUT_FILE=${CMAKE_CURRENT_BINARY_DIR}/version.c -P ${CMAKE_SOURCE_DIR}/version.cmake WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) include(${CMAKE_SOURCE_DIR}/version.cmake) +configure_file(${CMAKE_SOURCE_DIR}/src/util/version.c.in ${CMAKE_CURRENT_BINARY_DIR}/version.c) list(APPEND UTIL_SRC ${CMAKE_BINARY_DIR}/version.c) source_group("Generated sources" FILES ${CMAKE_BINARY_DIR}/version.c)

@@ -404,7 +373,7 @@ add_library(${BINARY_NAME} SHARED ${SRC})

if(BUILD_STATIC) add_library(${BINARY_NAME}-static STATIC ${SRC}) set_target_properties(${BINARY_NAME}-static PROPERTIES COMPILE_DEFINITIONS "${FEATURE_DEFINES}") - install(TARGETS ${BINARY_NAME}-static DESTINATION lib COMPONENT lib${BINARY_NAME}) + install(TARGETS ${BINARY_NAME}-static DESTINATION ${LIBDIR} COMPONENT lib${BINARY_NAME}) add_dependencies(${BINARY_NAME}-static version-info) endif() else()

@@ -414,7 +383,7 @@

add_dependencies(${BINARY_NAME} version-info) target_link_libraries(${BINARY_NAME} ${DEBUGGER_LIB} ${DEPENDENCY_LIB} ${OS_LIB}) -install(TARGETS ${BINARY_NAME} DESTINATION lib COMPONENT lib${BINARY_NAME}) +install(TARGETS ${BINARY_NAME} DESTINATION ${LIBDIR} COMPONENT lib${BINARY_NAME}) if(UNIX AND NOT APPLE) install(FILES ${CMAKE_SOURCE_DIR}/res/mgba-16.png DESTINATION share/icons/hicolor/16x16/apps RENAME mgba.png COMPONENT lib${BINARY_NAME}) install(FILES ${CMAKE_SOURCE_DIR}/res/mgba-24.png DESTINATION share/icons/hicolor/24x24/apps RENAME mgba.png COMPONENT lib${BINARY_NAME})
M PORTING.mdPORTING.md

@@ -12,7 +12,7 @@ ------------------

The ports are vaguely usable, but by no means should be considered stable. -### 3DS +### 3DS (port/3ds) * Add menu * Add audio * Thread support testing

@@ -20,7 +20,7 @@ * Make it faster

* ARMv6 dynarec * Hardware acceleration -### PSP +### PSP (port/psp) * Add menu * Add audio * Thread support

@@ -28,7 +28,14 @@ * Make it faster

* MIPS dynarec * Hardware acceleration -### Wii +### PS Vita (port/psp2) +* Add menu +* Add audio +* Make it faster + * Threaded renderer shim + * Hardware acceleration + +### Wii (port/wii) * Add menu * Add audio * Thread support
M src/arm/decoder-thumb.csrc/arm/decoder-thumb.c

@@ -153,7 +153,7 @@ ARM_OPERAND_IMMEDIATE_3;)

#define DEFINE_IMMEDIATE_WITH_REGISTER_MEM_THUMB(NAME, MNEMONIC, REG, CYCLES) \ DEFINE_THUMB_DECODER(NAME, MNEMONIC, \ - info->op1.reg = (opcode >> 6) & 0x0007; \ + info->op1.reg = (opcode >> 8) & 0x0007; \ info->memory.baseReg = REG; \ info->memory.offset.immediate = (opcode & 0x00FF) << 2; \ info->memory.width = ARM_ACCESS_WORD; \
M src/gba/bios.csrc/gba/bios.c

@@ -191,6 +191,9 @@ break;

case 0x2: GBAHalt(gba); break; + case 0x3: + GBAStop(gba); + break; case 0x05: // VBlankIntrWait // Fall through:

@@ -296,6 +299,10 @@ case REGION_VRAM:

_unFilter(gba, immediate == 0x18 ? 2 : 1, immediate == 0x16 ? 1 : 2); break; } + break; + case 0x19: + // SoundBias is mostly meaningless here + GBALog(gba, GBA_LOG_STUB, "Stub software interrupt: SoundBias (19)"); break; case 0x1F: _MidiKey2Freq(gba);
M src/gba/gba.csrc/gba/gba.c

@@ -79,8 +79,10 @@ gba->romVf = 0;

gba->biosVf = 0; gba->logHandler = 0; - gba->logLevel = GBA_LOG_INFO | GBA_LOG_WARN | GBA_LOG_ERROR | GBA_LOG_FATAL; + gba->logLevel = GBA_LOG_WARN | GBA_LOG_ERROR | GBA_LOG_FATAL; gba->stream = 0; + gba->keyCallback = 0; + gba->stopCallback = 0; gba->biosChecksum = GBAChecksum(gba->memory.bios, SIZE_BIOS);

@@ -552,6 +554,14 @@ gba->cpu->nextEvent = 0;

gba->cpu->halted = 1; } +void GBAStop(struct GBA* gba) { + if (!gba->stopCallback) { + return; + } + gba->cpu->nextEvent = 0; + gba->stopCallback->stop(gba->stopCallback); +} + static void _GBAVLog(struct GBA* gba, enum GBALogLevel level, const char* format, va_list args) { struct GBAThread* threadContext = GBAThreadGetContext(); enum GBALogLevel logLevel = GBA_LOG_ALL;

@@ -775,6 +785,10 @@ }

if (gba->stream) { gba->stream->postVideoFrame(gba->stream, gba->video.renderer); + } + + if (gba->memory.hw.devices & (HW_GB_PLAYER | HW_GB_PLAYER_DETECTION)) { + GBAHardwarePlayerUpdate(gba); } struct GBAThread* thread = GBAThreadGetContext();
M src/gba/gba.hsrc/gba/gba.h

@@ -11,6 +11,7 @@

#include "arm.h" #include "debugger/debugger.h" +#include "gba/interface.h" #include "gba/memory.h" #include "gba/video.h" #include "gba/audio.h"

@@ -35,43 +36,6 @@ IRQ_KEYPAD = 0xC,

IRQ_GAMEPAK = 0xD }; -enum GBALogLevel { - GBA_LOG_FATAL = 0x01, - GBA_LOG_ERROR = 0x02, - GBA_LOG_WARN = 0x04, - GBA_LOG_INFO = 0x08, - GBA_LOG_DEBUG = 0x10, - GBA_LOG_STUB = 0x20, - - GBA_LOG_GAME_ERROR = 0x100, - GBA_LOG_SWI = 0x200, - GBA_LOG_STATUS = 0x400, - GBA_LOG_SIO = 0x800, - - GBA_LOG_ALL = 0xF3F, - -#ifdef NDEBUG - GBA_LOG_DANGER = GBA_LOG_ERROR -#else - GBA_LOG_DANGER = GBA_LOG_FATAL -#endif -}; - -enum GBAKey { - GBA_KEY_A = 0, - GBA_KEY_B = 1, - GBA_KEY_SELECT = 2, - GBA_KEY_START = 3, - GBA_KEY_RIGHT = 4, - GBA_KEY_LEFT = 5, - GBA_KEY_UP = 6, - GBA_KEY_DOWN = 7, - GBA_KEY_R = 8, - GBA_KEY_L = 9, - GBA_KEY_MAX, - GBA_KEY_NONE = -1 -}; - enum GBAComponent { GBA_COMPONENT_DEBUGGER, GBA_COMPONENT_CHEAT_DEVICE,

@@ -91,19 +55,10 @@ SP_BASE_SUPERVISOR = 0x03007FE0

}; struct GBA; -struct GBARotationSource; struct GBAThread; struct Patch; struct VFile; -typedef void (*GBALogHandler)(struct GBAThread*, enum GBALogLevel, const char* format, va_list args); - -struct GBAAVStream { - void (*postVideoFrame)(struct GBAAVStream*, struct GBAVideoRenderer* renderer); - void (*postAudioFrame)(struct GBAAVStream*, int16_t left, int16_t right); - void (*postAudioBuffer)(struct GBAAVStream*, struct GBAAudio*); -}; - struct GBATimer { uint16_t reload; uint16_t oldReload;

@@ -156,6 +111,8 @@

GBALogHandler logHandler; enum GBALogLevel logLevel; struct GBAAVStream* stream; + struct GBAKeyCallback* keyCallback; + struct GBAStopCallback* stopCallback; enum GBAIdleLoopOptimization idleOptimization; uint32_t idleLoop;

@@ -199,6 +156,7 @@ void GBAWriteIME(struct GBA* gba, uint16_t value);

void GBARaiseIRQ(struct GBA* gba, enum GBAIRQ irq); void GBATestIRQ(struct ARMCore* cpu); void GBAHalt(struct GBA* gba); +void GBAStop(struct GBA* gba); void GBAAttachDebugger(struct GBA* gba, struct ARMDebugger* debugger); void GBADetachDebugger(struct GBA* gba);
M src/gba/hardware.csrc/gba/hardware.c

@@ -5,6 +5,7 @@ * 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 "hardware.h" +#include "gba/io.h" #include "gba/serialize.h" #include "util/hash.h"

@@ -25,6 +26,10 @@ static void _rumbleReadPins(struct GBACartridgeHardware* hw);

static void _lightReadPins(struct GBACartridgeHardware* hw); +static uint16_t _gbpRead(struct GBAKeyCallback*); +static uint16_t _gbpSioWriteRegister(struct GBASIODriver* driver, uint32_t address, uint16_t value); +static int32_t _gbpSioProcessEvents(struct GBASIODriver* driver, int32_t cycles); + static const int RTC_BYTES[8] = { 0, // Force reset 0, // Empty

@@ -39,6 +44,16 @@

void GBAHardwareInit(struct GBACartridgeHardware* hw, uint16_t* base) { hw->gpioBase = base; GBAHardwareClear(hw); + + hw->gbpCallback.d.readKeys = _gbpRead; + hw->gbpCallback.p = hw; + hw->gbpDriver.d.init = 0; + hw->gbpDriver.d.deinit = 0; + hw->gbpDriver.d.load = 0; + hw->gbpDriver.d.unload = 0; + hw->gbpDriver.d.writeRegister = _gbpSioWriteRegister; + hw->gbpDriver.d.processEvents = _gbpSioProcessEvents; + hw->gbpDriver.p = hw; } void GBAHardwareClear(struct GBACartridgeHardware* hw) {

@@ -461,6 +476,30 @@ };

static const uint32_t _logoHash = 0xEEDA6963; +static const uint32_t _gbpTxData[] = { + 0x0000494E, 0x0000494E, + 0xB6B1494E, 0xB6B1544E, + 0xABB1544E, 0xABB14E45, + 0xB1BA4E45, 0xB1BA4F44, + 0xB0BB4F44, 0xB0BB8002, + 0x10000010, 0x20000013, + 0x30000003, 0x30000003, + 0x30000003, 0x30000003, + 0x30000003, 0x00000000, +}; + +static const uint32_t _gbpRxData[] = { + 0x00000000, 0x494EB6B1, + 0x494EB6B1, 0x544EB6B1, + 0x544EABB1, 0x4E45ABB1, + 0x4E45B1BA, 0x4F44B1BA, + 0x4F44B0BB, 0x8000B0BB, + 0x10000010, 0x20000013, + 0x40000004, 0x40000004, + 0x40000004, 0x40000004, + 0x40000004, 0x40000004 +}; + bool GBAHardwarePlayerCheckScreen(const struct GBAVideo* video) { if (memcmp(video->palette, _logoPalette, sizeof(_logoPalette)) != 0) { return false;

@@ -469,6 +508,83 @@ uint32_t hash = hash32(&video->renderer->vram[0x4000], 0x4000, 0);

return hash == _logoHash; } +void GBAHardwarePlayerUpdate(struct GBA* gba) { + if (gba->memory.hw.devices & HW_GB_PLAYER) { + if (GBAHardwarePlayerCheckScreen(&gba->video)) { + ++gba->memory.hw.gbpInputsPosted; + gba->memory.hw.gbpInputsPosted %= 3; + gba->keyCallback = &gba->memory.hw.gbpCallback.d; + } else { + // TODO: Save and restore + gba->keyCallback = 0; + } + gba->memory.hw.gbpTxPosition = 0; + return; + } + if (gba->keyCallback || gba->sio.drivers.normal) { + return; + } + if (GBAHardwarePlayerCheckScreen(&gba->video)) { + gba->memory.hw.devices |= HW_GB_PLAYER; + gba->memory.hw.gbpInputsPosted = 0; + gba->memory.hw.gbpNextEvent = INT_MAX; + gba->keyCallback = &gba->memory.hw.gbpCallback.d; + GBASIOSetDriver(&gba->sio, &gba->memory.hw.gbpDriver.d, SIO_NORMAL_32); + } +} + +uint16_t _gbpRead(struct GBAKeyCallback* callback) { + struct GBAGBPKeyCallback* gbpCallback = (struct GBAGBPKeyCallback*) callback; + if (gbpCallback->p->gbpInputsPosted == 2) { + return 0x30F; + } + return 0x3FF; +} + +uint16_t _gbpSioWriteRegister(struct GBASIODriver* driver, uint32_t address, uint16_t value) { + struct GBAGBPSIODriver* gbp = (struct GBAGBPSIODriver*) driver; + if (address == REG_SIOCNT) { + if (value & 0x0080) { + if (gbp->p->gbpTxPosition <= 16 && gbp->p->gbpTxPosition > 0) { + uint32_t rx = gbp->p->p->memory.io[REG_SIODATA32_LO >> 1] | (gbp->p->p->memory.io[REG_SIODATA32_HI >> 1] << 16); + uint32_t expected = _gbpRxData[gbp->p->gbpTxPosition]; + // TODO: Check expected + uint32_t mask = 0; + if (gbp->p->gbpTxPosition == 15) { + mask = 0x22; + if (gbp->p->p->rumble) { + gbp->p->p->rumble->setRumble(gbp->p->p->rumble, (rx & mask) == mask); + } + } + } + gbp->p->gbpNextEvent = 2048; + } + value &= 0x78FB; + } + return value; +} + +int32_t _gbpSioProcessEvents(struct GBASIODriver* driver, int32_t cycles) { + struct GBAGBPSIODriver* gbp = (struct GBAGBPSIODriver*) driver; + gbp->p->gbpNextEvent -= cycles; + if (gbp->p->gbpNextEvent <= 0) { + uint32_t tx = 0; + if (gbp->p->gbpTxPosition <= 16) { + tx = _gbpTxData[gbp->p->gbpTxPosition]; + ++gbp->p->gbpTxPosition; + } + gbp->p->p->memory.io[REG_SIODATA32_LO >> 1] = tx; + gbp->p->p->memory.io[REG_SIODATA32_HI >> 1] = tx >> 16; + if (gbp->d.p->normalControl.irq) { + GBARaiseIRQ(gbp->p->p, IRQ_SIO); + } + gbp->d.p->normalControl.start = 0; + gbp->p->p->memory.io[REG_SIOCNT >> 1] = gbp->d.p->siocnt; + gbp->p->gbpNextEvent = INT_MAX; + } + return gbp->p->gbpNextEvent; +} + // == Serialization void GBAHardwareSerialize(const struct GBACartridgeHardware* hw, struct GBASerializedState* state) {

@@ -485,6 +601,9 @@ state->hw.tiltState = hw->tiltState;

state->hw.lightCounter = hw->lightCounter; state->hw.lightSample = hw->lightSample; state->hw.lightEdge = hw->lightEdge; + state->hw.gbpInputsPosted = hw->gbpInputsPosted; + state->hw.gbpTxPosition = hw->gbpTxPosition; + state->hw.gbpNextEvent = hw->gbpNextEvent; } void GBAHardwareDeserialize(struct GBACartridgeHardware* hw, const struct GBASerializedState* state) {

@@ -500,4 +619,7 @@ hw->tiltState = state->hw.tiltState;

hw->lightCounter = state->hw.lightCounter; hw->lightSample = state->hw.lightSample; hw->lightEdge = state->hw.lightEdge; + hw->gbpInputsPosted = state->hw.gbpInputsPosted; + hw->gbpTxPosition = state->hw.gbpTxPosition; + hw->gbpNextEvent = state->hw.gbpNextEvent; }
M src/gba/hardware.hsrc/gba/hardware.h

@@ -7,33 +7,13 @@ #ifndef GBA_HARDWARE_H

#define GBA_HARDWARE_H #include "util/common.h" +#include "gba/interface.h" #include "macros.h" #include <time.h> #define IS_GPIO_REGISTER(reg) ((reg) == GPIO_REG_DATA || (reg) == GPIO_REG_DIRECTION || (reg) == GPIO_REG_CONTROL) - -struct GBARotationSource { - void (*sample)(struct GBARotationSource*); - - int32_t (*readTiltX)(struct GBARotationSource*); - int32_t (*readTiltY)(struct GBARotationSource*); - - int32_t (*readGyroZ)(struct GBARotationSource*); -}; - -struct GBALuminanceSource { - void (*sample)(struct GBALuminanceSource*); - - uint8_t (*readLuminance)(struct GBALuminanceSource*); -}; - -struct GBARTCSource { - void (*sample)(struct GBARTCSource*); - - time_t (*unixTime)(struct GBARTCSource*); -}; struct GBARTCGenericSource { struct GBARTCSource d;

@@ -53,7 +33,9 @@ HW_RTC = 1,

HW_RUMBLE = 2, HW_LIGHT_SENSOR = 4, HW_GYRO = 8, - HW_TILT = 16 + HW_TILT = 16, + HW_GB_PLAYER = 32, + HW_GB_PLAYER_DETECTION = 64 }; enum GPIORegister {

@@ -102,6 +84,16 @@ struct GBARumble {

void (*setRumble)(struct GBARumble*, int enable); }; +struct GBAGBPKeyCallback { + struct GBAKeyCallback d; + struct GBACartridgeHardware* p; +}; + +struct GBAGBPSIODriver { + struct GBASIODriver d; + struct GBACartridgeHardware* p; +}; + DECL_BITFIELD(GPIOPin, uint16_t); struct GBACartridgeHardware {

@@ -125,6 +117,12 @@

uint16_t tiltX; uint16_t tiltY; int tiltState; + + unsigned gbpInputsPosted; + int gbpTxPosition; + int32_t gbpNextEvent; + struct GBAGBPKeyCallback gbpCallback; + struct GBAGBPSIODriver gbpDriver; }; void GBAHardwareInit(struct GBACartridgeHardware* gpio, uint16_t* gpioBase);

@@ -141,6 +139,7 @@ void GBAHardwareTiltWrite(struct GBACartridgeHardware* gpio, uint32_t address, uint8_t value);

uint8_t GBAHardwareTiltRead(struct GBACartridgeHardware* gpio, uint32_t address); struct GBAVideo; +void GBAHardwarePlayerUpdate(struct GBA* gba); bool GBAHardwarePlayerCheckScreen(const struct GBAVideo* video); void GBARTCGenericSourceInit(struct GBARTCGenericSource* rtc, struct GBA* gba);
M src/gba/input.csrc/gba/input.c

@@ -257,7 +257,10 @@ description->lowDirection = GBA_KEY_NONE;

} } -static void _loadAll(struct GBAInputMap* map, uint32_t type, const char* sectionName, const struct Configuration* config) { +static bool _loadAll(struct GBAInputMap* map, uint32_t type, const char* sectionName, const struct Configuration* config) { + if (!ConfigurationHasSection(config, sectionName)) { + return false; + } _loadKey(map, type, sectionName, config, GBA_KEY_A, "A"); _loadKey(map, type, sectionName, config, GBA_KEY_B, "B"); _loadKey(map, type, sectionName, config, GBA_KEY_L, "L");

@@ -279,6 +282,7 @@ _loadAxis(map, type, sectionName, config, GBA_KEY_UP, "Up");

_loadAxis(map, type, sectionName, config, GBA_KEY_DOWN, "Down"); _loadAxis(map, type, sectionName, config, GBA_KEY_LEFT, "Left"); _loadAxis(map, type, sectionName, config, GBA_KEY_RIGHT, "Right"); + return true; } static void _saveAll(const struct GBAInputMap* map, uint32_t type, const char* sectionName, struct Configuration* config) {

@@ -376,7 +380,6 @@ }

if (impl) { impl->map[input] = GBA_NO_MAPPING; } - TableEnumerate(&impl->axes, _unbindAxis, &input); } int GBAInputQueryBinding(const struct GBAInputMap* map, uint32_t type, enum GBAKey input) {

@@ -430,9 +433,9 @@ }

void GBAInputBindAxis(struct GBAInputMap* map, uint32_t type, int axis, const struct GBAAxis* description) { struct GBAInputMapImpl* impl = _guaranteeMap(map, type); + TableEnumerate(&impl->axes, _unbindAxis, &description->highDirection); + TableEnumerate(&impl->axes, _unbindAxis, &description->lowDirection); struct GBAAxis* dup = malloc(sizeof(struct GBAAxis)); - GBAInputUnbindKey(map, type, description->lowDirection); - GBAInputUnbindKey(map, type, description->highDirection); *dup = *description; TableInsert(&impl->axes, axis, dup); }

@@ -483,11 +486,11 @@ _makeSectionName(sectionName, SECTION_NAME_MAX, type);

_saveAll(map, type, sectionName, config); } -void GBAInputProfileLoad(struct GBAInputMap* map, uint32_t type, const struct Configuration* config, const char* profile) { +bool GBAInputProfileLoad(struct GBAInputMap* map, uint32_t type, const struct Configuration* config, const char* profile) { char sectionName[SECTION_NAME_MAX]; snprintf(sectionName, SECTION_NAME_MAX, "input-profile.%s", profile); sectionName[SECTION_NAME_MAX - 1] = '\0'; - _loadAll(map, type, sectionName, config); + return _loadAll(map, type, sectionName, config); } void GBAInputProfileSave(const struct GBAInputMap* map, uint32_t type, struct Configuration* config, const char* profile) {
M src/gba/input.hsrc/gba/input.h

@@ -46,7 +46,7 @@

void GBAInputMapLoad(struct GBAInputMap*, uint32_t type, const struct Configuration*); void GBAInputMapSave(const struct GBAInputMap*, uint32_t type, struct Configuration*); -void GBAInputProfileLoad(struct GBAInputMap*, uint32_t type, const struct Configuration*, const char* profile); +bool GBAInputProfileLoad(struct GBAInputMap*, uint32_t type, const struct Configuration*, const char* profile); void GBAInputProfileSave(const struct GBAInputMap*, uint32_t type, struct Configuration*, const char* profile); const char* GBAInputGetPreferredDevice(const struct Configuration*, uint32_t type, int playerId);
A src/gba/interface.h

@@ -0,0 +1,105 @@

+/* Copyright (c) 2013-2015 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/. */ +#ifndef INTERFACE_H +#define INTERFACE_H + +#include "util/common.h" + +enum GBALogLevel { + GBA_LOG_FATAL = 0x01, + GBA_LOG_ERROR = 0x02, + GBA_LOG_WARN = 0x04, + GBA_LOG_INFO = 0x08, + GBA_LOG_DEBUG = 0x10, + GBA_LOG_STUB = 0x20, + + GBA_LOG_GAME_ERROR = 0x100, + GBA_LOG_SWI = 0x200, + GBA_LOG_STATUS = 0x400, + GBA_LOG_SIO = 0x800, + + GBA_LOG_ALL = 0xF3F, +}; + +enum GBAKey { + GBA_KEY_A = 0, + GBA_KEY_B = 1, + GBA_KEY_SELECT = 2, + GBA_KEY_START = 3, + GBA_KEY_RIGHT = 4, + GBA_KEY_LEFT = 5, + GBA_KEY_UP = 6, + GBA_KEY_DOWN = 7, + GBA_KEY_R = 8, + GBA_KEY_L = 9, + GBA_KEY_MAX, + GBA_KEY_NONE = -1 +}; + +enum GBASIOMode { + SIO_NORMAL_8 = 0, + SIO_NORMAL_32 = 1, + SIO_MULTI = 2, + SIO_UART = 3, + SIO_GPIO = 8, + SIO_JOYBUS = 12 +}; + +struct GBA; +struct GBAAudio; +struct GBASIO; +struct GBAThread; +struct GBAVideoRenderer; + +typedef void (*GBALogHandler)(struct GBAThread*, enum GBALogLevel, const char* format, va_list args); + +struct GBAAVStream { + void (*postVideoFrame)(struct GBAAVStream*, struct GBAVideoRenderer* renderer); + void (*postAudioFrame)(struct GBAAVStream*, int16_t left, int16_t right); + void (*postAudioBuffer)(struct GBAAVStream*, struct GBAAudio*); +}; + +struct GBAKeyCallback { + uint16_t (*readKeys)(struct GBAKeyCallback*); +}; + +struct GBAStopCallback { + void (*stop)(struct GBAStopCallback*); +}; + +struct GBARotationSource { + void (*sample)(struct GBARotationSource*); + + int32_t (*readTiltX)(struct GBARotationSource*); + int32_t (*readTiltY)(struct GBARotationSource*); + + int32_t (*readGyroZ)(struct GBARotationSource*); +}; + +struct GBALuminanceSource { + void (*sample)(struct GBALuminanceSource*); + + uint8_t (*readLuminance)(struct GBALuminanceSource*); +}; + +struct GBARTCSource { + void (*sample)(struct GBARTCSource*); + + time_t (*unixTime)(struct GBARTCSource*); +}; + +struct GBASIODriver { + struct GBASIO* p; + + bool (*init)(struct GBASIODriver* driver); + void (*deinit)(struct GBASIODriver* driver); + bool (*load)(struct GBASIODriver* driver); + bool (*unload)(struct GBASIODriver* driver); + uint16_t (*writeRegister)(struct GBASIODriver* driver, uint32_t address, uint16_t value); + int32_t (*processEvents)(struct GBASIODriver* driver, int32_t cycles); +}; + +#endif
M src/gba/io.csrc/gba/io.c

@@ -505,7 +505,7 @@ value &= 0x80;

if (!value) { GBAHalt(gba); } else { - GBALog(gba, GBA_LOG_STUB, "Stop unimplemented"); + GBAStop(gba); } return; }

@@ -584,14 +584,18 @@

case REG_KEYINPUT: if (gba->rr && gba->rr->isPlaying(gba->rr)) { return 0x3FF ^ gba->rr->queryInput(gba->rr); - } else if (gba->keySource) { - uint16_t input = *gba->keySource; + } else { + uint16_t input = 0x3FF; + if (gba->keyCallback) { + input = gba->keyCallback->readKeys(gba->keyCallback); + } else if (gba->keySource) { + input = *gba->keySource; + } if (gba->rr && gba->rr->isRecording(gba->rr)) { gba->rr->logInput(gba->rr, input); } return 0x3FF ^ input; } - break; case REG_SIOCNT: return gba->sio.siocnt;
M src/gba/renderers/software-mode0.csrc/gba/renderers/software-mode0.c

@@ -406,7 +406,7 @@ if (UNLIKELY(end == outX)) { \

return; \ } \ if (UNLIKELY(end < outX)) { \ - GBALog(0, GBA_LOG_DANGER, "Out of bounds background draw!"); \ + GBALog(0, GBA_LOG_FATAL, "Out of bounds background draw!"); \ return; \ } \ DRAW_BACKGROUND_MODE_0_TILE_SUFFIX_ ## BPP (BLEND, OBJWIN) \
M src/gba/renderers/video-software.csrc/gba/renderers/video-software.c

@@ -403,12 +403,10 @@ softwareRenderer->windows[activeWindow].endX = win->h.end;

if (win->h.end >= oldWindow.endX) { // Trim off extra windows we've overwritten for (++activeWindow; softwareRenderer->nWindows > activeWindow + 1 && win->h.end >= softwareRenderer->windows[activeWindow].endX; ++activeWindow) { -#ifdef DEBUG - if (activeWindow >= MAX_WINDOW) { - GBALog(0, GBA_LOG_DANGER, "Out of bounds window write will occur"); + if (VIDEO_CHECKS && activeWindow >= MAX_WINDOW) { + GBALog(0, GBA_LOG_FATAL, "Out of bounds window write will occur"); return; } -#endif softwareRenderer->windows[activeWindow] = softwareRenderer->windows[activeWindow + 1]; --softwareRenderer->nWindows; }

@@ -428,7 +426,7 @@ }

} #ifdef DEBUG if (softwareRenderer->nWindows > MAX_WINDOW) { - GBALog(0, GBA_LOG_ABORT, "Out of bounds window write occurred!"); + GBALog(0, GBA_LOG_FATAL, "Out of bounds window write occurred!"); } #endif }

@@ -532,7 +530,7 @@ }

} #ifdef COLOR_16_BIT -#ifdef __ARM_NEON +#if defined(__ARM_NEON) && !defined(__APPLE__) _to16Bit(row, softwareRenderer->row, VIDEO_HORIZONTAL_PIXELS); #else for (x = 0; x < VIDEO_HORIZONTAL_PIXELS; ++x) {
M src/gba/savedata.csrc/gba/savedata.c

@@ -407,6 +407,9 @@ return 0;

} void GBASavedataClean(struct GBASavedata* savedata, uint32_t frameCount) { + if (!savedata->vf) { + return; + } if (savedata->dirty & SAVEDATA_DIRT_NEW) { savedata->dirty &= ~SAVEDATA_DIRT_NEW; if (!(savedata->dirty & SAVEDATA_DIRT_SEEN)) {
M src/gba/serialize.hsrc/gba/serialize.h

@@ -136,7 +136,8 @@ * | bit 1: Has rumble value (reserved)

* | bit 2: Has light sensor value * | bit 3: Has gyroscope value * | bit 4: Has tilt values - * | bits 5 - 7: Reserved + * | bit 5: Has Game Boy Player attached + * | bits 6 - 7: Reserved * | 0x002B8 - 0x002B9: Gyroscope sample * | 0x002BA - 0x002BB: Tilt x sample * | 0x002BC - 0x002BD: Tilt y sample

@@ -149,8 +150,11 @@ * | bits 4 - 15: Light counter

* | 0x002C0 - 0x002C0: Light sample * | 0x002C1 - 0x002C3: Flags * | bits 0 - 1: Tilt state machine - * | bits 2 - 31: Reserved - * 0x002C4 - 0x002DF: Reserved (leave zero) + * | bits 2 - 3: GB Player inputs posted + * | bits 4 - 8: GB Player transmit position + * | bits 9 - 23: Reserved + * 0x002C4 - 0x002C7: Game Boy Player next event + * 0x002C8 - 0x002DF: Reserved (leave zero) * 0x002E0 - 0x002EF: Savedata state * | 0x002E0 - 0x002E0: Savedata type * | 0x002E1 - 0x002E1: Savedata command (see savedata.h)

@@ -282,10 +286,13 @@ unsigned : 1;

unsigned lightCounter : 12; unsigned lightSample : 8; unsigned tiltState : 2; - unsigned : 22; + unsigned gbpInputsPosted : 2; + unsigned gbpTxPosition : 5; + unsigned : 15; + uint32_t gbpNextEvent : 32; } hw; - uint32_t reservedHardware[7]; + uint32_t reservedHardware[6]; struct { unsigned type : 8;
M src/gba/sio.hsrc/gba/sio.h

@@ -8,34 +8,14 @@ #define GBA_SIO_H

#include "util/common.h" +#include "gba/interface.h" + #define MAX_GBAS 4 extern const int GBASIOCyclesPerTransfer[4][MAX_GBAS]; -enum GBASIOMode { - SIO_NORMAL_8 = 0, - SIO_NORMAL_32 = 1, - SIO_MULTI = 2, - SIO_UART = 3, - SIO_GPIO = 8, - SIO_JOYBUS = 12 -}; - enum { RCNT_INITIAL = 0x8000 -}; - -struct GBASIO; - -struct GBASIODriver { - struct GBASIO* p; - - bool (*init)(struct GBASIODriver* driver); - void (*deinit)(struct GBASIODriver* driver); - bool (*load)(struct GBASIODriver* driver); - bool (*unload)(struct GBASIODriver* driver); - uint16_t (*writeRegister)(struct GBASIODriver* driver, uint32_t address, uint16_t value); - int32_t (*processEvents)(struct GBASIODriver* driver, int32_t cycles); }; struct GBASIODriverSet {
M src/gba/supervisor/config.csrc/gba/supervisor/config.c

@@ -116,13 +116,21 @@ bool GBAConfigLoad(struct GBAConfig* config) {

char path[PATH_MAX]; GBAConfigDirectory(path, PATH_MAX); strncat(path, PATH_SEP "config.ini", PATH_MAX - strlen(path)); - return ConfigurationRead(&config->configTable, path); + return GBAConfigLoadPath(config, path); } bool GBAConfigSave(const struct GBAConfig* config) { char path[PATH_MAX]; GBAConfigDirectory(path, PATH_MAX); strncat(path, PATH_SEP "config.ini", PATH_MAX - strlen(path)); + return GBAConfigSavePath(config, path); +} + +bool GBAConfigLoadPath(struct GBAConfig* config, const char* path) { + return ConfigurationRead(&config->configTable, path); +} + +bool GBAConfigSavePath(const struct GBAConfig* config, const char* path) { return ConfigurationWrite(&config->configTable, path); }
M src/gba/supervisor/config.hsrc/gba/supervisor/config.h

@@ -51,6 +51,8 @@ void GBAConfigDeinit(struct GBAConfig*);

bool GBAConfigLoad(struct GBAConfig*); bool GBAConfigSave(const struct GBAConfig*); +bool GBAConfigLoadPath(struct GBAConfig*, const char* path); +bool GBAConfigSavePath(const struct GBAConfig*, const char* path); void GBAConfigMakePortable(const struct GBAConfig*); void GBAConfigDirectory(char* out, size_t outLength);
M src/gba/supervisor/overrides.csrc/gba/supervisor/overrides.c

@@ -285,6 +285,12 @@

if (override->hardware & HW_TILT) { GBAHardwareInitTilt(&gba->memory.hw); } + + if (override->hardware & HW_GB_PLAYER_DETECTION) { + gba->memory.hw.devices |= HW_GB_PLAYER_DETECTION; + } else { + gba->memory.hw.devices &= ~HW_GB_PLAYER_DETECTION; + } } if (override->idleLoop != IDLE_LOOP_NONE) {
M src/gba/supervisor/thread.csrc/gba/supervisor/thread.c

@@ -96,6 +96,18 @@ _waitUntilNotState(threadContext, THREAD_PAUSING);

} } +struct GBAThreadStop { + struct GBAStopCallback d; + struct GBAThread* p; +}; + +static void _stopCallback(struct GBAStopCallback* stop) { + struct GBAThreadStop* callback = (struct GBAThreadStop*) stop; + if (callback->p->stopCallback(callback->p)) { + _changeState(callback->p, THREAD_EXITING, false); + } +} + static THREAD_ENTRY _GBAThreadRun(void* context) { #ifdef USE_PTHREADS pthread_once(&_contextOnce, _createTLS);

@@ -129,6 +141,14 @@ threadContext->cpu = &cpu;

gba.logLevel = threadContext->logLevel; gba.logHandler = threadContext->logHandler; gba.stream = threadContext->stream; + + struct GBAThreadStop stop; + if (threadContext->stopCallback) { + stop.d.stop = _stopCallback; + stop.p = threadContext; + gba.stopCallback = &stop.d; + } + gba.idleOptimization = threadContext->idleOptimization; #ifdef USE_PTHREADS pthread_setspecific(_contextKey, threadContext);
M src/gba/supervisor/thread.hsrc/gba/supervisor/thread.h

@@ -21,6 +21,7 @@ struct GBACheatSet;

struct GBAOptions; typedef void (*ThreadCallback)(struct GBAThread* threadContext); +typedef bool (*ThreadStopCallback)(struct GBAThread* threadContext); enum ThreadState { THREAD_INITIALIZED = -1,

@@ -86,6 +87,7 @@ int logLevel;

ThreadCallback startCallback; ThreadCallback cleanCallback; ThreadCallback frameCallback; + ThreadStopCallback stopCallback; void* userData; void (*run)(struct GBAThread*);
M src/platform/libretro/libretro.csrc/platform/libretro/libretro.c

@@ -12,9 +12,11 @@ #include "gba/renderers/video-software.h"

#include "gba/serialize.h" #include "gba/supervisor/overrides.h" #include "gba/video.h" +#include "util/circle-buffer.h" #include "util/vfs.h" #define SAMPLES 1024 +#define RUMBLE_PWM 35 static retro_environment_t environCallback; static retro_video_refresh_t videoCallback;

@@ -22,11 +24,13 @@ static retro_audio_sample_batch_t audioCallback;

static retro_input_poll_t inputPollCallback; static retro_input_state_t inputCallback; static retro_log_printf_t logCallback; +static retro_set_rumble_state_t rumbleCallback; static void GBARetroLog(struct GBAThread* thread, enum GBALogLevel level, const char* format, va_list args); static void _postAudioBuffer(struct GBAAVStream*, struct GBAAudio* audio); static void _postVideoFrame(struct GBAAVStream*, struct GBAVideoRenderer* renderer); +static void _setRumble(struct GBARumble* rumble, int enable); static struct GBA gba; static struct ARMCore cpu;

@@ -35,7 +39,11 @@ static struct VFile* rom;

static void* data; static struct VFile* save; static void* savedata; +static struct VFile* bios; static struct GBAAVStream stream; +static int rumbleLevel; +static struct CircleBuffer rumbleHistory; +static struct GBARumble rumble; unsigned retro_api_version(void) { return RETRO_API_VERSION;

@@ -112,7 +120,15 @@ };

environCallback(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, &inputDescriptors); // TODO: RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME when BIOS booting is supported - // TODO: RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE + + struct retro_rumble_interface rumbleInterface; + if (environCallback(RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE, &rumbleInterface)) { + rumbleCallback = rumbleInterface.set_rumble_state; + CircleBufferInit(&rumbleHistory, RUMBLE_PWM); + rumble.setRumble = _setRumble; + } else { + rumbleCallback = 0; + } struct retro_log_callback log; if (environCallback(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &log)) {

@@ -132,8 +148,21 @@ gba.logLevel = 0; // TODO: Settings

gba.logHandler = GBARetroLog; gba.stream = &stream; gba.idleOptimization = IDLE_LOOP_REMOVE; // TODO: Settings + if (rumbleCallback) { + gba.rumble = &rumble; + } rom = 0; + const char* sysDir = 0; + if (environCallback(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &sysDir)) { + char biosPath[PATH_MAX]; + snprintf(biosPath, sizeof(biosPath), "%s%s%s", sysDir, PATH_SEP, "gba_bios.bin"); + bios = VFileOpen(biosPath, O_RDONLY); + if (bios) { + GBALoadBIOS(&gba, bios); + } + } + GBAVideoSoftwareRendererCreate(&renderer); renderer.outputBuffer = malloc(256 * VIDEO_VERTICAL_PIXELS * BYTES_PER_PIXEL); renderer.outputBufferStride = 256;

@@ -148,6 +177,10 @@ #endif

} void retro_deinit(void) { + if (bios) { + bios->close(bios); + bios = 0; + } GBADestroy(&gba); }

@@ -176,6 +209,10 @@ }

void retro_reset(void) { ARMReset(&cpu); + + if (rumbleCallback) { + CircleBufferClear(&rumbleHistory); + } } bool retro_load_game(const struct retro_game_info* game) {

@@ -213,6 +250,7 @@ save->close(save);

save = 0; free(savedata); savedata = 0; + CircleBufferDeinit(&rumbleHistory); } size_t retro_serialize_size(void) {

@@ -311,10 +349,12 @@ break;

case GBA_LOG_INFO: case GBA_LOG_GAME_ERROR: case GBA_LOG_SWI: + case GBA_LOG_STATUS: retroLevel = RETRO_LOG_INFO; break; case GBA_LOG_DEBUG: case GBA_LOG_STUB: + case GBA_LOG_SIO: retroLevel = RETRO_LOG_DEBUG; break; }

@@ -346,3 +386,18 @@ unsigned stride;

renderer->getPixels(renderer, &stride, &pixels); videoCallback(pixels, VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS, BYTES_PER_PIXEL * stride); } + +static void _setRumble(struct GBARumble* rumble, int enable) { + UNUSED(rumble); + if (!rumbleCallback) { + return; + } + rumbleLevel += enable; + if (CircleBufferSize(&rumbleHistory) == RUMBLE_PWM) { + int8_t oldLevel; + CircleBufferRead8(&rumbleHistory, &oldLevel); + rumbleLevel -= oldLevel; + } + CircleBufferWrite8(&rumbleHistory, enable); + rumbleCallback(0, RETRO_RUMBLE_STRONG, rumbleLevel * 0xFFFF / RUMBLE_PWM); +}
M src/platform/qt/AudioProcessor.hsrc/platform/qt/AudioProcessor.h

@@ -39,6 +39,8 @@

virtual void setBufferSamples(int samples) = 0; virtual void inputParametersChanged() = 0; + virtual unsigned sampleRate() const = 0; + protected: GBAThread* input() { return m_context; }
M src/platform/qt/AudioProcessorQt.cppsrc/platform/qt/AudioProcessorQt.cpp

@@ -83,3 +83,10 @@ if (m_device) {

m_device->setFormat(m_audioOutput->format()); } } + +unsigned AudioProcessorQt::sampleRate() const { + if (!m_audioOutput) { + return 0; + } + return m_audioOutput->format().sampleRate(); +}
M src/platform/qt/AudioProcessorQt.hsrc/platform/qt/AudioProcessorQt.h

@@ -28,6 +28,8 @@

virtual void setBufferSamples(int samples); virtual void inputParametersChanged(); + virtual unsigned sampleRate() const override; + private: QAudioOutput* m_audioOutput; AudioDevice* m_device;
M src/platform/qt/AudioProcessorSDL.cppsrc/platform/qt/AudioProcessorSDL.cpp

@@ -54,3 +54,7 @@ }

void AudioProcessorSDL::inputParametersChanged() { } + +unsigned AudioProcessorSDL::sampleRate() const { + return m_audio.obtainedSpec.freq; +}
M src/platform/qt/AudioProcessorSDL.hsrc/platform/qt/AudioProcessorSDL.h

@@ -29,6 +29,8 @@

virtual void setBufferSamples(int samples); virtual void inputParametersChanged(); + virtual unsigned sampleRate() const override; + private: GBASDLAudio m_audio; };
M src/platform/qt/CMakeLists.txtsrc/platform/qt/CMakeLists.txt

@@ -61,6 +61,7 @@ GameController.cpp

GamepadAxisEvent.cpp GamepadButtonEvent.cpp InputController.cpp + InputProfile.cpp KeyEditor.cpp LoadSaveState.cpp LogController.cpp
M src/platform/qt/Display.cppsrc/platform/qt/Display.cpp

@@ -51,6 +51,10 @@ , m_filter(false)

{ setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); setMinimumSize(VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS); + connect(&m_mouseTimer, SIGNAL(timeout()), this, SIGNAL(hideCursor())); + m_mouseTimer.setSingleShot(true); + m_mouseTimer.setInterval(MOUSE_DISAPPEAR_TIMER); + setMouseTracking(true); } void Display::resizeEvent(QResizeEvent*) {

@@ -69,3 +73,9 @@

void Display::showMessage(const QString& message) { m_messagePainter.showMessage(message); } + +void Display::mouseMoveEvent(QMouseEvent*) { + emit showCursor(); + m_mouseTimer.stop(); + m_mouseTimer.start(); +}
M src/platform/qt/Display.hsrc/platform/qt/Display.h

@@ -33,6 +33,10 @@

bool isAspectRatioLocked() const { return m_lockAspectRatio; } bool isFiltered() const { return m_filter; } +signals: + void showCursor(); + void hideCursor(); + public slots: virtual void startDrawing(GBAThread* context) = 0; virtual void stopDrawing() = 0;

@@ -47,15 +51,19 @@ void showMessage(const QString& message);

protected: void resizeEvent(QResizeEvent*); + virtual void mouseMoveEvent(QMouseEvent*) override; MessagePainter* messagePainter() { return &m_messagePainter; } + private: static Driver s_driver; + static const int MOUSE_DISAPPEAR_TIMER = 1000; MessagePainter m_messagePainter; bool m_lockAspectRatio; bool m_filter; + QTimer m_mouseTimer; }; }
M src/platform/qt/DisplayGL.cppsrc/platform/qt/DisplayGL.cpp

@@ -21,6 +21,8 @@ , m_painter(new PainterGL(m_gl))

, m_drawThread(nullptr) , m_context(nullptr) { + m_gl->setMouseTracking(true); + m_gl->setAttribute(Qt::WA_TransparentForMouseEvents); // This doesn't seem to work? } DisplayGL::~DisplayGL() {
M src/platform/qt/DisplayGL.hsrc/platform/qt/DisplayGL.h

@@ -9,6 +9,7 @@

#include "Display.h" #include <QGLWidget> +#include <QMouseEvent> #include <QThread> #include <QTimer>

@@ -27,6 +28,7 @@

protected: void paintEvent(QPaintEvent*) override {} void resizeEvent(QResizeEvent*) override {} + void mouseMoveEvent(QMouseEvent* event) override { event->ignore(); } }; class PainterGL;
M src/platform/qt/GBAKeyEditor.cppsrc/platform/qt/GBAKeyEditor.cpp

@@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "GBAKeyEditor.h" #include <QComboBox> +#include <QHBoxLayout> #include <QPaintEvent> #include <QPainter> #include <QPushButton>

@@ -24,6 +25,7 @@

GBAKeyEditor::GBAKeyEditor(InputController* controller, int type, const QString& profile, QWidget* parent) : QWidget(parent) , m_profileSelect(nullptr) + , m_clear(nullptr) , m_type(type) , m_profile(profile) , m_controller(controller)

@@ -32,6 +34,7 @@ setWindowFlags(windowFlags() & ~Qt::WindowFullscreenButtonHint);

setMinimumSize(300, 300); const GBAInputMap* map = controller->map(); + controller->stealFocus(this); m_keyDU = new KeyEditor(this); m_keyDD = new KeyEditor(this);

@@ -64,31 +67,36 @@ m_profile = m_profileSelect->currentText();

m_controller->loadProfile(m_type, m_profile); refresh(); }); + + m_clear = new QWidget(this); + QHBoxLayout* layout = new QHBoxLayout; + m_clear->setLayout(layout); + layout->setSpacing(6); + + QPushButton* clearButton = new QPushButton(tr("Clear Button")); + layout->addWidget(clearButton); + connect(clearButton, &QAbstractButton::pressed, [this]() { + if (!findFocus()) { + return; + } + bool signalsBlocked = (*m_currentKey)->blockSignals(true); + (*m_currentKey)->clearButton(); + (*m_currentKey)->blockSignals(signalsBlocked); + }); + + QPushButton* clearAxis = new QPushButton(tr("Clear Analog")); + layout->addWidget(clearAxis); + connect(clearAxis, &QAbstractButton::pressed, [this]() { + if (!findFocus()) { + return; + } + bool signalsBlocked = (*m_currentKey)->blockSignals(true); + (*m_currentKey)->clearAxis(); + (*m_currentKey)->blockSignals(signalsBlocked); + }); } #endif - connect(m_keyDU, SIGNAL(valueChanged(int)), this, SLOT(setNext())); - connect(m_keyDD, SIGNAL(valueChanged(int)), this, SLOT(setNext())); - connect(m_keyDL, SIGNAL(valueChanged(int)), this, SLOT(setNext())); - connect(m_keyDR, SIGNAL(valueChanged(int)), this, SLOT(setNext())); - connect(m_keySelect, SIGNAL(valueChanged(int)), this, SLOT(setNext())); - connect(m_keyStart, SIGNAL(valueChanged(int)), this, SLOT(setNext())); - connect(m_keyA, SIGNAL(valueChanged(int)), this, SLOT(setNext())); - connect(m_keyB, SIGNAL(valueChanged(int)), this, SLOT(setNext())); - connect(m_keyL, SIGNAL(valueChanged(int)), this, SLOT(setNext())); - connect(m_keyR, SIGNAL(valueChanged(int)), this, SLOT(setNext())); - - connect(m_keyDU, SIGNAL(axisChanged(int, int)), this, SLOT(setNext())); - connect(m_keyDD, SIGNAL(axisChanged(int, int)), this, SLOT(setNext())); - connect(m_keyDL, SIGNAL(axisChanged(int, int)), this, SLOT(setNext())); - connect(m_keyDR, SIGNAL(axisChanged(int, int)), this, SLOT(setNext())); - connect(m_keySelect, SIGNAL(axisChanged(int, int)), this, SLOT(setNext())); - connect(m_keyStart, SIGNAL(axisChanged(int, int)), this, SLOT(setNext())); - connect(m_keyA, SIGNAL(axisChanged(int, int)), this, SLOT(setNext())); - connect(m_keyB, SIGNAL(axisChanged(int, int)), this, SLOT(setNext())); - connect(m_keyL, SIGNAL(axisChanged(int, int)), this, SLOT(setNext())); - connect(m_keyR, SIGNAL(axisChanged(int, int)), this, SLOT(setNext())); - m_buttons = new QWidget(this); QVBoxLayout* layout = new QVBoxLayout; m_buttons->setLayout(layout);

@@ -115,6 +123,11 @@ m_keyL,

m_keyR }; + for (auto& key : m_keyOrder) { + connect(key, SIGNAL(valueChanged(int)), this, SLOT(setNext())); + connect(key, SIGNAL(axisChanged(int, int)), this, SLOT(setNext())); + } + m_currentKey = m_keyOrder.end(); m_background.load(":/res/keymap.qpic");

@@ -141,7 +154,11 @@ setLocation(m_keyL, 0.1, 0.1);

setLocation(m_keyR, 0.9, 0.1); if (m_profileSelect) { - setLocation(m_profileSelect, 0.5, 0.7); + setLocation(m_profileSelect, 0.5, 0.67); + } + + if (m_clear) { + setLocation(m_clear, 0.5, 0.77); } }

@@ -151,6 +168,19 @@ painter.scale(width() / 480.0, height() / 480.0);

painter.drawPicture(0, 0, m_background); } +void GBAKeyEditor::closeEvent(QCloseEvent*) { + m_controller->releaseFocus(this); +} + +bool GBAKeyEditor::event(QEvent* event) { + if (event->type() == QEvent::WindowActivate) { + m_controller->stealFocus(this); + } else if (event->type() == QEvent::WindowDeactivate) { + m_controller->releaseFocus(this); + } + return QWidget::event(event); +} + void GBAKeyEditor::setNext() { findFocus();

@@ -167,6 +197,10 @@ }

} void GBAKeyEditor::save() { +#ifdef BUILD_SDL + m_controller->unbindAllAxes(m_type); +#endif + bindKey(m_keyDU, GBA_KEY_UP); bindKey(m_keyDD, GBA_KEY_DOWN); bindKey(m_keyDL, GBA_KEY_LEFT);

@@ -239,14 +273,11 @@ #endif

void GBAKeyEditor::bindKey(const KeyEditor* keyEditor, GBAKey key) { #ifdef BUILD_SDL - if (keyEditor->direction() != GamepadAxisEvent::NEUTRAL) { - m_controller->bindAxis(m_type, keyEditor->value(), keyEditor->direction(), key); - } else { -#endif - m_controller->bindKey(m_type, keyEditor->value(), key); -#ifdef BUILD_SDL + if (m_type == SDL_BINDING_BUTTON) { + m_controller->bindAxis(m_type, keyEditor->axis(), keyEditor->direction(), key); } #endif + m_controller->bindKey(m_type, keyEditor->value(), key); } bool GBAKeyEditor::findFocus() {
M src/platform/qt/GBAKeyEditor.hsrc/platform/qt/GBAKeyEditor.h

@@ -35,6 +35,8 @@

protected: virtual void resizeEvent(QResizeEvent*) override; virtual void paintEvent(QPaintEvent*) override; + virtual bool event(QEvent*) override; + virtual void closeEvent(QCloseEvent*) override; private slots: void setNext();

@@ -64,6 +66,7 @@

KeyEditor* keyById(GBAKey); QComboBox* m_profileSelect; + QWidget* m_clear; QWidget* m_buttons; KeyEditor* m_keyDU; KeyEditor* m_keyDD;
M src/platform/qt/GameController.cppsrc/platform/qt/GameController.cpp

@@ -7,6 +7,7 @@ #include "GameController.h"

#include "AudioProcessor.h" #include "InputController.h" +#include "LogController.h" #include "MultiplayerController.h" #include "VFileDevice.h"

@@ -51,6 +52,8 @@ , m_wasPaused(false)

, m_inputController(nullptr) , m_multiplayer(nullptr) , m_stateSlot(1) + , m_backupLoadState(nullptr) + , m_backupSaveState(nullptr) { m_renderer = new GBAVideoSoftwareRenderer; GBAVideoSoftwareRendererCreate(m_renderer);

@@ -90,6 +93,13 @@ context->gba->rtcSource = &controller->m_rtc.d;

context->gba->rumble = controller->m_inputController->rumble(); context->gba->rotationSource = controller->m_inputController->rotationSource(); controller->m_fpsTarget = context->fpsTarget; + + if (GBALoadState(context, context->stateDir, 0)) { + VFile* vf = GBAGetState(context->gba, context->stateDir, 0, true); + if (vf) { + vf->truncate(vf, 0); + } + } controller->gameStarted(context); };

@@ -111,8 +121,22 @@ controller->frameAvailable(nullptr);

} }; + m_threadContext.stopCallback = [](GBAThread* context) { + if (!context) { + return false; + } + GameController* controller = static_cast<GameController*>(context->userData); + if (!GBASaveState(context, context->stateDir, 0, true)) { + return false; + } + QMetaObject::invokeMethod(controller, "closeGame"); + return true; + }; + m_threadContext.logHandler = [](GBAThread* context, enum GBALogLevel level, const char* format, va_list args) { - static const char* stubMessage = "Stub software interrupt"; + static const char* stubMessage = "Stub software interrupt: %02X"; + static const char* savestateMessage = "State %i loaded"; + static const char* savestateFailedMessage = "State %i failed to load"; if (!context) { return; }

@@ -123,6 +147,25 @@ va_copy(argc, args);

int immediate = va_arg(argc, int); va_end(argc); controller->unimplementedBiosCall(immediate); + } else if (level == GBA_LOG_STATUS) { + // Slot 0 is reserved for suspend points + if (strncmp(savestateMessage, format, strlen(savestateMessage)) == 0) { + va_list argc; + va_copy(argc, args); + int slot = va_arg(argc, int); + va_end(argc); + if (slot == 0) { + format = "Loaded suspend state"; + } + } else if (strncmp(savestateFailedMessage, format, strlen(savestateFailedMessage)) == 0) { + va_list argc; + va_copy(argc, args); + int slot = va_arg(argc, int); + va_end(argc); + if (slot == 0) { + return; + } + } } if (level == GBA_LOG_FATAL) { QMetaObject::invokeMethod(controller, "crashGame", Q_ARG(const QString&, QString().vsprintf(format, args)));

@@ -162,6 +205,7 @@ closeGame();

GBACheatDeviceDestroy(&m_cheatDevice); delete m_renderer; delete[] m_drawContext; + delete m_backupLoadState; } void GameController::setMultiplayerController(MultiplayerController* controller) {

@@ -475,7 +519,9 @@ if (!m_gameOpen || m_rewindTimer.isActive()) {

return; } m_wasPaused = isPaused(); + bool signalsBlocked = blockSignals(true); setPaused(true); + blockSignals(signalsBlocked); m_rewindTimer.start(); }

@@ -484,7 +530,9 @@ if (!m_rewindTimer.isActive()) {

return; } m_rewindTimer.stop(); + bool signalsBlocked = blockSignals(true); setPaused(m_wasPaused); + blockSignals(signalsBlocked); } void GameController::keyPressed(int key) {

@@ -556,11 +604,16 @@ threadContinue();

} void GameController::loadState(int slot) { - if (slot > 0) { + if (slot > 0 && slot != m_stateSlot) { m_stateSlot = slot; + m_backupSaveState.clear(); } GBARunOnThread(&m_threadContext, [](GBAThread* context) { GameController* controller = static_cast<GameController*>(context->userData); + if (!controller->m_backupLoadState) { + controller->m_backupLoadState = new GBASerializedState; + } + GBASerialize(context->gba, controller->m_backupLoadState); if (GBALoadState(context, context->stateDir, controller->m_stateSlot)) { controller->frameAvailable(controller->m_drawContext); controller->stateLoaded(context);

@@ -574,7 +627,47 @@ m_stateSlot = slot;

} GBARunOnThread(&m_threadContext, [](GBAThread* context) { GameController* controller = static_cast<GameController*>(context->userData); + VFile* vf = GBAGetState(context->gba, context->stateDir, controller->m_stateSlot, false); + if (vf) { + controller->m_backupSaveState.resize(vf->size(vf)); + vf->read(vf, controller->m_backupSaveState.data(), controller->m_backupSaveState.size()); + vf->close(vf); + } GBASaveState(context, context->stateDir, controller->m_stateSlot, true); + }); +} + +void GameController::loadBackupState() { + if (!m_backupLoadState) { + return; + } + + GBARunOnThread(&m_threadContext, [](GBAThread* context) { + GameController* controller = static_cast<GameController*>(context->userData); + if (GBADeserialize(context->gba, controller->m_backupLoadState)) { + GBALog(context->gba, GBA_LOG_STATUS, "Undid state load"); + controller->frameAvailable(controller->m_drawContext); + controller->stateLoaded(context); + } + delete controller->m_backupLoadState; + controller->m_backupLoadState = nullptr; + }); +} + +void GameController::saveBackupState() { + if (m_backupSaveState.isEmpty()) { + return; + } + + GBARunOnThread(&m_threadContext, [](GBAThread* context) { + GameController* controller = static_cast<GameController*>(context->userData); + VFile* vf = GBAGetState(context->gba, context->stateDir, controller->m_stateSlot, true); + if (vf) { + vf->write(vf, controller->m_backupSaveState.constData(), controller->m_backupSaveState.size()); + vf->close(vf); + GBALog(context->gba, GBA_LOG_STATUS, "Undid state save"); + } + controller->m_backupSaveState.clear(); }); }

@@ -746,7 +839,7 @@ float ratio;

if (m_threadContext.gba) { sampleRate = m_threadContext.gba->audio.sampleRate; } - ratio = GBAAudioCalculateRatio(sampleRate, m_threadContext.fpsTarget, 44100); + ratio = GBAAudioCalculateRatio(sampleRate, m_threadContext.fpsTarget, m_audioProcess->sampleRate()); m_threadContext.audioBuffers = ceil(samples / ratio); #else m_threadContext.audioBuffers = samples;
M src/platform/qt/GameController.hsrc/platform/qt/GameController.h

@@ -55,7 +55,7 @@ void threadInterrupt();

void threadContinue(); bool isPaused(); - bool isLoaded() { return m_gameOpen; } + bool isLoaded() { return m_gameOpen && GBAThreadIsActive(&m_threadContext); } bool audioSync() const { return m_audioSync; } bool videoSync() const { return m_videoSync; }

@@ -120,6 +120,8 @@ void setAudioBufferSamples(int samples);

void setFPSTarget(float fps); void loadState(int slot = 0); void saveState(int slot = 0); + void loadBackupState(); + void saveBackupState(); void setVideoSync(bool); void setAudioSync(bool); void setFrameskip(int);

@@ -192,6 +194,8 @@ QTimer m_rewindTimer;

bool m_wasPaused; int m_stateSlot; + GBASerializedState* m_backupLoadState; + QByteArray m_backupSaveState; InputController* m_inputController; MultiplayerController* m_multiplayer;
M src/platform/qt/InputController.cppsrc/platform/qt/InputController.cpp

@@ -8,6 +8,7 @@

#include "ConfigController.h" #include "GamepadAxisEvent.h" #include "GamepadButtonEvent.h" +#include "InputProfile.h" #include <QApplication> #include <QTimer>

@@ -24,7 +25,7 @@ int InputController::s_sdlInited = 0;

GBASDLEvents InputController::s_sdlEvents; #endif -InputController::InputController(int playerId, QObject* parent) +InputController::InputController(int playerId, QWidget* topLevel, QObject* parent) : QObject(parent) , m_playerId(playerId) , m_config(nullptr)

@@ -33,6 +34,8 @@ #ifdef BUILD_SDL

, m_playerAttached(false) #endif , m_allowOpposing(false) + , m_topLevel(topLevel) + , m_focusParent(topLevel) { GBAInputMapInit(&m_inputMap);

@@ -106,8 +109,15 @@ #endif

} void InputController::loadProfile(uint32_t type, const QString& profile) { - GBAInputProfileLoad(&m_inputMap, type, m_config->input(), profile.toUtf8().constData()); + bool loaded = GBAInputProfileLoad(&m_inputMap, type, m_config->input(), profile.toUtf8().constData()); recalibrateAxes(); + if (!loaded) { + const InputProfile* ip = InputProfile::findProfile(profile); + if (ip) { + ip->apply(this); + } + } + emit profileLoaded(profile); } void InputController::saveConfiguration() {

@@ -403,6 +413,10 @@ }

GBAInputBindAxis(&m_inputMap, type, axis, &description); } +void InputController::unbindAllAxes(uint32_t type) { + GBAInputUnbindAllAxes(&m_inputMap, type); +} + void InputController::testGamepad(int type) { auto activeAxes = activeGamepadAxes(type); auto oldAxes = m_activeAxes;

@@ -424,7 +438,7 @@ bool newlyAboveThreshold = activeAxes.contains(axis);

if (newlyAboveThreshold) { GamepadAxisEvent* event = new GamepadAxisEvent(axis.first, axis.second, newlyAboveThreshold, type, this); postPendingEvent(event->gbaKey()); - QApplication::sendEvent(QApplication::focusWidget(), event); + sendGamepadEvent(event); if (!event->isAccepted()) { clearPendingEvent(event->gbaKey()); }

@@ -433,7 +447,7 @@ }

for (auto axis : oldAxes) { GamepadAxisEvent* event = new GamepadAxisEvent(axis.first, axis.second, false, type, this); clearPendingEvent(event->gbaKey()); - QApplication::sendEvent(QApplication::focusWidget(), event); + sendGamepadEvent(event); } if (!QApplication::focusWidget()) {

@@ -446,7 +460,7 @@

for (int button : activeButtons) { GamepadButtonEvent* event = new GamepadButtonEvent(GamepadButtonEvent::Down(), button, type, this); postPendingEvent(event->gbaKey()); - QApplication::sendEvent(QApplication::focusWidget(), event); + sendGamepadEvent(event); if (!event->isAccepted()) { clearPendingEvent(event->gbaKey()); }

@@ -454,8 +468,21 @@ }

for (int button : oldButtons) { GamepadButtonEvent* event = new GamepadButtonEvent(GamepadButtonEvent::Up(), button, type, this); clearPendingEvent(event->gbaKey()); - QApplication::sendEvent(QApplication::focusWidget(), event); + sendGamepadEvent(event); + } +} + +void InputController::sendGamepadEvent(QEvent* event) { + QWidget* focusWidget = nullptr; + if (m_focusParent) { + focusWidget = m_focusParent->focusWidget(); + if (!focusWidget) { + focusWidget = m_focusParent; + } + } else { + focusWidget = QApplication::focusWidget(); } + QApplication::sendEvent(focusWidget, event); } void InputController::postPendingEvent(GBAKey key) {

@@ -483,3 +510,13 @@ void InputController::setScreensaverSuspendable(bool suspendable) {

GBASDLSetScreensaverSuspendable(&s_sdlEvents, suspendable); } #endif + +void InputController::stealFocus(QWidget* focus) { + m_focusParent = focus; +} + +void InputController::releaseFocus(QWidget* focus) { + if (focus == m_focusParent) { + m_focusParent = m_topLevel; + } +}
M src/platform/qt/InputController.hsrc/platform/qt/InputController.h

@@ -32,7 +32,7 @@

public: static const uint32_t KEYBOARD = 0x51545F4B; - InputController(int playerId = 0, QObject* parent = nullptr); + InputController(int playerId = 0, QWidget* topLevel = nullptr, QObject* parent = nullptr); ~InputController(); void setConfiguration(ConfigController* config);

@@ -60,6 +60,7 @@ QSet<QPair<int, GamepadAxisEvent::Direction>> activeGamepadAxes(int type);

void recalibrateAxes(); void bindAxis(uint32_t type, int axis, GamepadAxisEvent::Direction, GBAKey); + void unbindAllAxes(uint32_t type); QStringList connectedGamepads(uint32_t type) const; int gamepad(uint32_t type) const;

@@ -73,10 +74,16 @@ void registerGyroAxisY(int axis);

float gyroSensitivity() const; void setGyroSensitivity(float sensitivity); + + void stealFocus(QWidget* focus); + void releaseFocus(QWidget* focus); GBARumble* rumble(); GBARotationSource* rotationSource(); +signals: + void profileLoaded(const QString& profile); + public slots: void testGamepad(int type);

@@ -91,11 +98,14 @@ private:

void postPendingEvent(GBAKey); void clearPendingEvent(GBAKey); bool hasPendingEvent(GBAKey) const; + void sendGamepadEvent(QEvent*); GBAInputMap m_inputMap; ConfigController* m_config; int m_playerId; bool m_allowOpposing; + QWidget* m_topLevel; + QWidget* m_focusParent; #ifdef BUILD_SDL static int s_sdlInited;
A src/platform/qt/InputProfile.cpp

@@ -0,0 +1,222 @@

+/* Copyright (c) 2013-2015 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 "InputProfile.h" + +#include "InputController.h" + +#include <QRegExp> + +using namespace QGBA; + +const InputProfile InputProfile::s_defaultMaps[] = { + { + "XInput Controller #\\d+", // XInput (Windows) + (int[GBA_KEY_MAX]) { + /*keyA */ 11, + /*keyB */ 10, + /*keySelect */ 5, + /*keyStart */ 4, + /*keyRight */ 3, + /*keyLeft */ 2, + /*keyUp */ 0, + /*keyDown */ 1, + /*keyR */ 9, + /*keyL */ 8 + }, + (ShortcutButton[]) { + {"loadState", 12}, + {"saveState", 13}, + {} + }, + (ShortcutAxis[]) { + {"holdFastForward", GamepadAxisEvent::Direction::POSITIVE, 5}, + {"holdRewind", GamepadAxisEvent::Direction::POSITIVE, 4}, + {} + } + }, + { + "(Microsoft X-Box 360 pad|Xbox Gamepad \\(userspace driver\\))", // Linux + (int[GBA_KEY_MAX]) { + /*keyA */ 1, + /*keyB */ 0, + /*keySelect */ 6, + /*keyStart */ 7, + /*keyRight */ -1, + /*keyLeft */ -1, + /*keyUp */ -1, + /*keyDown */ -1, + /*keyR */ 5, + /*keyL */ 4 + }, + (ShortcutButton[]) { + {"loadState", 2}, + {"saveState", 3}, + {} + }, + (ShortcutAxis[]) { + {"holdFastForward", GamepadAxisEvent::Direction::POSITIVE, 5}, + {"holdRewind", GamepadAxisEvent::Direction::POSITIVE, 2}, + {} + } + }, + { + "Controller", // The Xbox 360 controller drivers on OS X are vague... + (int[GBA_KEY_MAX]) { + /*keyA */ 1, + /*keyB */ 0, + /*keySelect */ 9, + /*keyStart */ 8, + /*keyRight */ 14, + /*keyLeft */ 13, + /*keyUp */ 11, + /*keyDown */ 12, + /*keyR */ 5, + /*keyL */ 4 + }, + (ShortcutButton[]) { + {"loadState", 2}, + {"saveState", 3}, + {} + }, + (ShortcutAxis[]) { + {"holdFastForward", GamepadAxisEvent::Direction::POSITIVE, 5}, + {"holdRewind", GamepadAxisEvent::Direction::POSITIVE, 2}, + {} + } + }, + { + "PLAYSTATION\\(R\\)3 Controller", // DualShock 3 (OS X) + (int[GBA_KEY_MAX]) { + /*keyA */ 13, + /*keyB */ 14, + /*keySelect */ 0, + /*keyStart */ 3, + /*keyRight */ 5, + /*keyLeft */ 7, + /*keyUp */ 4, + /*keyDown */ 6, + /*keyR */ 11, + /*keyL */ 10 + }, + (ShortcutButton[]) { + {"loadState", 15}, + {"saveState", 12}, + {"holdFastForward", 9}, + {"holdRewind", 8}, + {} + } + }, + { + "Wiimote \\(..-..-..-..-..-..\\)", // WJoy (OS X) + (int[GBA_KEY_MAX]) { + /*keyA */ 15, + /*keyB */ 16, + /*keySelect */ 7, + /*keyStart */ 6, + /*keyRight */ 14, + /*keyLeft */ 13, + /*keyUp */ 11, + /*keyDown */ 12, + /*keyR */ 20, + /*keyL */ 19 + }, + (ShortcutButton[]) { + {"loadState", 18}, + {"saveState", 17}, + {"holdFastForward", 22}, + {"holdRewind", 21}, + {} + } + }, +}; + +constexpr InputProfile::InputProfile(const char* name, + int keys[GBA_KEY_MAX], + const ShortcutButton* shortcutButtons, + const ShortcutAxis* shortcutAxes, + AxisValue axes[GBA_KEY_MAX], + const struct Coord& tiltAxis, + const struct Coord& gyroAxis, + float gyroSensitivity) + : m_profileName(name) + , m_keys { + keys[GBA_KEY_A], + keys[GBA_KEY_B], + keys[GBA_KEY_SELECT], + keys[GBA_KEY_START], + keys[GBA_KEY_RIGHT], + keys[GBA_KEY_LEFT], + keys[GBA_KEY_UP], + keys[GBA_KEY_DOWN], + keys[GBA_KEY_R], + keys[GBA_KEY_L] + } + , m_shortcutButtons(shortcutButtons) + , m_shortcutAxes(shortcutAxes) + , m_axes { + axes[GBA_KEY_A], + axes[GBA_KEY_B], + axes[GBA_KEY_SELECT], + axes[GBA_KEY_START], + axes[GBA_KEY_RIGHT], + axes[GBA_KEY_LEFT], + axes[GBA_KEY_UP], + axes[GBA_KEY_DOWN], + axes[GBA_KEY_R], + axes[GBA_KEY_L] + } + , m_tiltAxis(tiltAxis) + , m_gyroAxis(gyroAxis) + , m_gyroSensitivity(gyroSensitivity) +{ +} + +const InputProfile* InputProfile::findProfile(const QString& name) { + for (size_t i = 0; i < sizeof(s_defaultMaps) / sizeof(*s_defaultMaps); ++i) { + QRegExp re(s_defaultMaps[i].m_profileName); + if (re.exactMatch(name)) { + return &s_defaultMaps[i]; + } + } + return nullptr; +} + +void InputProfile::apply(InputController* controller) const { + for (size_t i = 0; i < GBA_KEY_MAX; ++i) { +#ifdef BUILD_SDL + controller->bindKey(SDL_BINDING_BUTTON, m_keys[i], static_cast<GBAKey>(i)); + controller->bindAxis(SDL_BINDING_BUTTON, m_axes[i].axis, m_axes[i].direction, static_cast<GBAKey>(i)); +#endif + } + controller->registerTiltAxisX(m_tiltAxis.x); + controller->registerTiltAxisY(m_tiltAxis.y); + controller->registerGyroAxisX(m_gyroAxis.x); + controller->registerGyroAxisY(m_gyroAxis.y); + controller->setGyroSensitivity(m_gyroSensitivity); +} + +bool InputProfile::lookupShortcutButton(const QString& shortcutName, int* button) const { + for (size_t i = 0; m_shortcutButtons[i].shortcut; ++i) { + const ShortcutButton& shortcut = m_shortcutButtons[i]; + if (QLatin1String(shortcut.shortcut) == shortcutName) { + *button = shortcut.button; + return true; + } + } + return false; +} + +bool InputProfile::lookupShortcutAxis(const QString& shortcutName, int* axis, GamepadAxisEvent::Direction* direction) const { + for (size_t i = 0; m_shortcutAxes[i].shortcut; ++i) { + const ShortcutAxis& shortcut = m_shortcutAxes[i]; + if (QLatin1String(shortcut.shortcut) == shortcutName) { + *axis = shortcut.axis; + *direction = shortcut.direction; + return true; + } + } + return false; +}
A src/platform/qt/InputProfile.h

@@ -0,0 +1,78 @@

+/* Copyright (c) 2013-2015 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/. */ +#ifndef QGBA_INPUT_PROFILE +#define QGBA_INPUT_PROFILE + +#include "GamepadAxisEvent.h" + +extern "C" { +#include "gba/interface.h" +} + +namespace QGBA { + +class InputController; + +class InputProfile { +public: + static const InputProfile* findProfile(const QString& name); + + void apply(InputController*) const; + bool lookupShortcutButton(const QString& shortcut, int* button) const; + bool lookupShortcutAxis(const QString& shortcut, int* axis, GamepadAxisEvent::Direction* direction) const; + +private: + struct Coord { + int x; + int y; + }; + + struct AxisValue { + GamepadAxisEvent::Direction direction; + int axis; + }; + + struct ShortcutButton { + const char* shortcut; + int button; + }; + + struct ShortcutAxis { + const char* shortcut; + GamepadAxisEvent::Direction direction; + int axis; + }; + + constexpr InputProfile(const char* name, + int keys[GBA_KEY_MAX], + const ShortcutButton* shortcutButtons = (ShortcutButton[]) {{}}, + const ShortcutAxis* shortcutAxes = (ShortcutAxis[]) {{}}, + AxisValue axes[GBA_KEY_MAX] = (AxisValue[GBA_KEY_MAX]) { + {}, {}, {}, {}, + { GamepadAxisEvent::Direction::POSITIVE, 0 }, + { GamepadAxisEvent::Direction::NEGATIVE, 0 }, + { GamepadAxisEvent::Direction::NEGATIVE, 1 }, + { GamepadAxisEvent::Direction::POSITIVE, 1 }, + {}, {}}, + const struct Coord& tiltAxis = { 2, 3 }, + const struct Coord& gyroAxis = { 0, 1 }, + float gyroSensitivity = 2e+09f); + + static const InputProfile s_defaultMaps[]; + + const char* m_profileName; + const int m_keys[GBA_KEY_MAX]; + const AxisValue m_axes[GBA_KEY_MAX]; + const ShortcutButton* m_shortcutButtons; + const ShortcutAxis* m_shortcutAxes; + Coord m_tiltAxis; + Coord m_gyroAxis; + float m_gyroSensitivity; +}; + +} + +#endif
M src/platform/qt/KeyEditor.cppsrc/platform/qt/KeyEditor.cpp

@@ -15,22 +15,20 @@

KeyEditor::KeyEditor(QWidget* parent) : QLineEdit(parent) , m_direction(GamepadAxisEvent::NEUTRAL) + , m_key(-1) + , m_axis(-1) , m_button(false) { setAlignment(Qt::AlignCenter); } void KeyEditor::setValue(int key) { + m_key = key; if (m_button) { - if (key < 0) { - clear(); - } else { - setText(QString::number(key)); - } + updateButtonText(); } else { setText(QKeySequence(key).toString(QKeySequence::NativeText)); } - m_key = key; emit valueChanged(key); }

@@ -41,18 +39,30 @@ }

void KeyEditor::setValueButton(int button) { m_button = true; - m_direction = GamepadAxisEvent::NEUTRAL; setValue(button); } void KeyEditor::setValueAxis(int axis, int32_t value) { m_button = true; - m_key = axis; + m_axis = axis; m_direction = value < 0 ? GamepadAxisEvent::NEGATIVE : GamepadAxisEvent::POSITIVE; - setText((value < 0 ? "-" : "+") + QString::number(axis)); + updateButtonText(); emit axisChanged(axis, m_direction); } +void KeyEditor::clearButton() { + m_button = true; + setValue(-1); +} + +void KeyEditor::clearAxis() { + m_button = true; + m_axis = -1; + m_direction = GamepadAxisEvent::NEUTRAL; + updateButtonText(); + emit axisChanged(m_axis, m_direction); +} + QSize KeyEditor::sizeHint() const { QSize hint = QLineEdit::sizeHint(); hint.setWidth(40);

@@ -85,3 +95,18 @@ return true;

} return QWidget::event(event); } + +void KeyEditor::updateButtonText() { + QStringList text; + if (m_key >= 0) { + text.append(QString::number(m_key)); + } + if (m_direction != GamepadAxisEvent::NEUTRAL) { + text.append((m_direction == GamepadAxisEvent::NEGATIVE ? "-" : "+") + QString::number(m_axis)); + } + if (text.isEmpty()) { + setText(tr("---")); + } else { + setText(text.join("/")); + } +}
M src/platform/qt/KeyEditor.hsrc/platform/qt/KeyEditor.h

@@ -20,6 +20,7 @@

int value() const { return m_key; } GamepadAxisEvent::Direction direction() const { return m_direction; } + int axis() const { return m_axis; } virtual QSize sizeHint() const override;

@@ -28,6 +29,8 @@ void setValue(int key);

void setValueKey(int key); void setValueButton(int button); void setValueAxis(int axis, int32_t value); + void clearButton(); + void clearAxis(); signals: void valueChanged(int key);

@@ -38,7 +41,10 @@ virtual void keyPressEvent(QKeyEvent* event) override;

virtual bool event(QEvent* event) override; private: + void updateButtonText(); + int m_key; + int m_axis; bool m_button; GamepadAxisEvent::Direction m_direction; };
M src/platform/qt/OverrideView.cppsrc/platform/qt/OverrideView.cpp

@@ -39,6 +39,7 @@ connect(m_ui.hwGyro, SIGNAL(clicked()), this, SLOT(updateOverrides()));

connect(m_ui.hwLight, SIGNAL(clicked()), this, SLOT(updateOverrides())); connect(m_ui.hwTilt, SIGNAL(clicked()), this, SLOT(updateOverrides())); connect(m_ui.hwRumble, SIGNAL(clicked()), this, SLOT(updateOverrides())); + connect(m_ui.hwGBPlayer, SIGNAL(clicked()), this, SLOT(updateOverrides())); connect(m_ui.save, SIGNAL(clicked()), this, SLOT(saveOverride()));

@@ -80,6 +81,9 @@ if (m_ui.hwRumble->isChecked()) {

m_override.hardware |= HW_RUMBLE; } } + if (m_ui.hwGBPlayer->isChecked()) { + m_override.hardware |= HW_GB_PLAYER_DETECTION; + } bool ok; uint32_t parsedIdleLoop = m_ui.idleLoop->text().toInt(&ok, 16);

@@ -115,6 +119,7 @@ m_ui.hwGyro->setChecked(thread->gba->memory.hw.devices & HW_GYRO);

m_ui.hwLight->setChecked(thread->gba->memory.hw.devices & HW_LIGHT_SENSOR); m_ui.hwTilt->setChecked(thread->gba->memory.hw.devices & HW_TILT); m_ui.hwRumble->setChecked(thread->gba->memory.hw.devices & HW_RUMBLE); + m_ui.hwGBPlayer->setChecked(thread->gba->memory.hw.devices & HW_GB_PLAYER_DETECTION); if (thread->gba->idleLoop != IDLE_LOOP_NONE) { m_ui.idleLoop->setText(QString::number(thread->gba->idleLoop, 16));
M src/platform/qt/OverrideView.uisrc/platform/qt/OverrideView.ui

@@ -6,8 +6,8 @@ <property name="geometry">

<rect> <x>0</x> <y>0</y> - <width>409</width> - <height>228</height> + <width>401</width> + <height>203</height> </rect> </property> <property name="sizePolicy">

@@ -23,13 +23,19 @@ <layout class="QGridLayout" name="gridLayout_3">

<property name="sizeConstraint"> <enum>QLayout::SetFixedSize</enum> </property> - <item row="2" column="1"> + <item row="4" column="1"> <layout class="QHBoxLayout" name="horizontalLayout_2"> <item> <spacer name="horizontalSpacer_2"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> </spacer> </item> <item>

@@ -44,7 +50,135 @@ </widget>

</item> </layout> </item> - <item row="0" column="0" rowspan="3"> + <item row="3" column="1"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="1"> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string/> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QFormLayout" name="formLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Save type</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="savetype"> + <item> + <property name="text"> + <string>Autodetect</string> + </property> + </item> + <item> + <property name="text"> + <string>None</string> + </property> + </item> + <item> + <property name="text"> + <string>SRAM</string> + </property> + </item> + <item> + <property name="text"> + <string>Flash 512kb</string> + </property> + </item> + <item> + <property name="text"> + <string>Flash 1Mb</string> + </property> + </item> + <item> + <property name="text"> + <string>EEPROM</string> + </property> + </item> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Idle loop</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="idleLoop"/> + </item> + <item row="1" column="0" colspan="2"> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="Line" name="line_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QCheckBox" name="hwGBPlayer"> + <property name="text"> + <string>Game Boy Player features</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item row="2" column="0" rowspan="3"> <widget class="QGroupBox" name="groupBox"> <property name="title"> <string/>

@@ -112,83 +246,6 @@ </widget>

</item> </layout> </widget> - </item> - <item row="0" column="1"> - <widget class="QGroupBox" name="groupBox_2"> - <property name="title"> - <string/> - </property> - <layout class="QFormLayout" name="formLayout_5"> - <property name="fieldGrowthPolicy"> - <enum>QFormLayout::AllNonFixedFieldsGrow</enum> - </property> - <item row="0" column="0"> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Save type</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QComboBox" name="savetype"> - <item> - <property name="text"> - <string>Autodetect</string> - </property> - </item> - <item> - <property name="text"> - <string>None</string> - </property> - </item> - <item> - <property name="text"> - <string>SRAM</string> - </property> - </item> - <item> - <property name="text"> - <string>Flash 512kb</string> - </property> - </item> - <item> - <property name="text"> - <string>Flash 1Mb</string> - </property> - </item> - <item> - <property name="text"> - <string>EEPROM</string> - </property> - </item> - </widget> - </item> - <item row="2" column="0"> - <widget class="QLabel" name="label_3"> - <property name="text"> - <string>Idle loop</string> - </property> - </widget> - </item> - <item row="2" column="1"> - <widget class="QLineEdit" name="idleLoop"/> - </item> - <item row="1" column="1"> - <widget class="Line" name="line"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item row="1" column="1"> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - </spacer> </item> </layout> </widget>
M src/platform/qt/SettingsView.cppsrc/platform/qt/SettingsView.cpp

@@ -36,6 +36,19 @@ loadSetting("resampleVideo", m_ui.resampleVideo);

loadSetting("allowOpposingDirections", m_ui.allowOpposingDirections); loadSetting("suspendScreensaver", m_ui.suspendScreensaver); + double fastForwardRatio = loadSetting("fastForwardRatio").toDouble(); + if (fastForwardRatio <= 0) { + m_ui.fastForwardUnbounded->setChecked(true); + m_ui.fastForwardRatio->setEnabled(false); + } else { + m_ui.fastForwardUnbounded->setChecked(false); + m_ui.fastForwardRatio->setEnabled(true); + m_ui.fastForwardRatio->setValue(fastForwardRatio); + } + connect(m_ui.fastForwardUnbounded, &QAbstractButton::toggled, [this](bool checked) { + m_ui.fastForwardRatio->setEnabled(!checked); + }); + QString idleOptimization = loadSetting("idleOptimization"); if (idleOptimization == "ignore") { m_ui.idleOptimization->setCurrentIndex(0);

@@ -102,6 +115,12 @@ saveSetting("rewindBufferCapacity", m_ui.rewindCapacity);

saveSetting("resampleVideo", m_ui.resampleVideo); saveSetting("allowOpposingDirections", m_ui.allowOpposingDirections); saveSetting("suspendScreensaver", m_ui.suspendScreensaver); + + if (m_ui.fastForwardUnbounded->isChecked()) { + saveSetting("fastForwardRatio", "-1"); + } else { + saveSetting("fastForwardRatio", m_ui.fastForwardRatio); + } switch (m_ui.idleOptimization->currentIndex() + IDLE_LOOP_IGNORE) { case IDLE_LOOP_IGNORE:
M src/platform/qt/SettingsView.uisrc/platform/qt/SettingsView.ui

@@ -6,8 +6,8 @@ <property name="geometry">

<rect> <x>0</x> <y>0</y> - <width>698</width> - <height>366</height> + <width>707</width> + <height>420</height> </rect> </property> <property name="sizePolicy">

@@ -380,21 +380,21 @@ </widget>

</item> </layout> </item> - <item row="7" column="0" colspan="2"> + <item row="10" column="0" colspan="2"> <widget class="Line" name="line_2"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> </widget> </item> - <item row="8" column="1"> + <item row="11" column="1"> <widget class="QCheckBox" name="allowOpposingDirections"> <property name="text"> <string>Allow opposing input directions</string> </property> </widget> </item> - <item row="9" column="1"> + <item row="12" column="1"> <widget class="QCheckBox" name="suspendScreensaver"> <property name="text"> <string>Suspend screensaver</string>

@@ -404,14 +404,14 @@ <bool>true</bool>

</property> </widget> </item> - <item row="10" column="0"> + <item row="13" column="0"> <widget class="QLabel" name="label_15"> <property name="text"> <string>Idle loops</string> </property> </widget> </item> - <item row="10" column="1"> + <item row="13" column="1"> <widget class="QComboBox" name="idleOptimization"> <item> <property name="text">

@@ -428,6 +428,52 @@ <property name="text">

<string>Detect and remove</string> </property> </item> + </widget> + </item> + <item row="8" column="1"> + <widget class="QDoubleSpinBox" name="fastForwardRatio"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="suffix"> + <string>×</string> + </property> + <property name="minimum"> + <double>0.010000000000000</double> + </property> + <property name="maximum"> + <double>20.000000000000000</double> + </property> + <property name="singleStep"> + <double>0.500000000000000</double> + </property> + <property name="value"> + <double>5.000000000000000</double> + </property> + </widget> + </item> + <item row="8" column="0"> + <widget class="QLabel" name="label_18"> + <property name="text"> + <string>Fast forward speed</string> + </property> + </widget> + </item> + <item row="9" column="1"> + <widget class="QCheckBox" name="fastForwardUnbounded"> + <property name="text"> + <string>Unbounded</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="7" column="0" colspan="2"> + <widget class="Line" name="line_6"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> </widget> </item> </layout>
M src/platform/qt/ShortcutController.cppsrc/platform/qt/ShortcutController.cpp

@@ -7,6 +7,7 @@ #include "ShortcutController.h"

#include "ConfigController.h" #include "GamepadButtonEvent.h" +#include "InputProfile.h" #include <QAction> #include <QKeyEvent>

@@ -18,6 +19,7 @@ ShortcutController::ShortcutController(QObject* parent)

: QAbstractItemModel(parent) , m_rootMenu(nullptr) , m_config(nullptr) + , m_profile(nullptr) { }

@@ -239,6 +241,9 @@ m_buttons[button] = item;

} if (m_config) { m_config->setQtOption(item->name(), button, BUTTON_SECTION); + if (!m_profileName.isNull()) { + m_config->setQtOption(item->name(), button, BUTTON_PROFILE_SECTION + m_profileName); + } } emit dataChanged(createIndex(index.row(), 0, index.internalPointer()), createIndex(index.row(), 2, index.internalPointer()));

@@ -272,6 +277,9 @@ if (direction == GamepadAxisEvent::NEGATIVE) {

d = '-'; } m_config->setQtOption(item->name(), QString("%1%2").arg(d).arg(axis), AXIS_SECTION); + if (!m_profileName.isNull()) { + m_config->setQtOption(item->name(), QString("%1%2").arg(d).arg(axis), AXIS_PROFILE_SECTION + m_profileName); + } } emit dataChanged(createIndex(index.row(), 0, index.internalPointer()), createIndex(index.row(), 2, index.internalPointer()));

@@ -365,6 +373,9 @@ return false;

} void ShortcutController::loadShortcuts(ShortcutItem* item) { + if (item->name().isNull()) { + return; + } QVariant shortcut = m_config->getQtOption(item->name(), KEY_SECTION); if (!shortcut.isNull()) { QKeySequence keySequence(shortcut.toString());

@@ -377,19 +388,45 @@ m_heldKeys[keySequence] = item;

} item->setShortcut(keySequence); } - QVariant button = m_config->getQtOption(item->name(), BUTTON_SECTION); + loadGamepadShortcuts(item); +} + +void ShortcutController::loadGamepadShortcuts(ShortcutItem* item) { + if (item->name().isNull()) { + return; + } + QVariant button = m_config->getQtOption(item->name(), !m_profileName.isNull() ? BUTTON_PROFILE_SECTION + m_profileName : BUTTON_SECTION); + int oldButton = item->button(); + if (oldButton >= 0) { + m_buttons.take(oldButton); + item->setButton(-1); + } + if (button.isNull() && m_profile) { + int buttonInt; + if (m_profile->lookupShortcutButton(item->name(), &buttonInt)) { + button = buttonInt; + } + } if (!button.isNull()) { - int oldButton = item->button(); item->setButton(button.toInt()); - if (oldButton >= 0) { - m_buttons.take(oldButton); + m_buttons[button.toInt()] = item; + } + + QVariant axis = m_config->getQtOption(item->name(), !m_profileName.isNull() ? AXIS_PROFILE_SECTION + m_profileName : AXIS_SECTION); + int oldAxis = item->axis(); + GamepadAxisEvent::Direction oldDirection = item->direction(); + if (oldAxis >= 0) { + m_axes.take(qMakePair(oldAxis, oldDirection)); + item->setAxis(-1, GamepadAxisEvent::NEUTRAL); + } + if (axis.isNull() && m_profile) { + int axisInt; + GamepadAxisEvent::Direction direction; + if (m_profile->lookupShortcutAxis(item->name(), &axisInt, &direction)) { + axis = QLatin1String(direction == GamepadAxisEvent::Direction::NEGATIVE ? "-" : "+") + QString::number(axisInt); } - m_buttons[button.toInt()] = item; } - QVariant axis = m_config->getQtOption(item->name(), AXIS_SECTION); if (!axis.isNull()) { - int oldAxis = item->axis(); - GamepadAxisEvent::Direction oldDirection = item->direction(); QString axisDesc = axis.toString(); if (axisDesc.size() >= 2) { GamepadAxisEvent::Direction direction = GamepadAxisEvent::NEUTRAL;

@@ -403,9 +440,6 @@ bool ok;

int axis = axisDesc.mid(1).toInt(&ok); if (ok) { item->setAxis(axis, direction); - if (oldAxis >= 0) { - m_axes.take(qMakePair(oldAxis, oldDirection)); - } m_axes[qMakePair(axis, direction)] = item; } }

@@ -430,6 +464,21 @@ }

QString key = QKeySequence(event->key()).toString(); return QKeySequence(modifier + key); +} + +void ShortcutController::loadProfile(const QString& profile) { + m_profileName = profile; + m_profile = InputProfile::findProfile(profile); + onSubitems(&m_rootMenu, [this](ShortcutItem* item) { + loadGamepadShortcuts(item); + }); +} + +void ShortcutController::onSubitems(ShortcutItem* item, std::function<void(ShortcutItem*)> func) { + for (ShortcutItem& subitem : item->items()) { + func(&subitem); + onSubitems(&subitem, func); + } } ShortcutController::ShortcutItem::ShortcutItem(QAction* action, const QString& name, ShortcutItem* parent)
M src/platform/qt/ShortcutController.hsrc/platform/qt/ShortcutController.h

@@ -21,6 +21,7 @@

namespace QGBA { class ConfigController; +class InputProfile; class ShortcutController : public QAbstractItemModel { Q_OBJECT

@@ -29,6 +30,8 @@ private:

constexpr static const char* const KEY_SECTION = "shortcutKey"; constexpr static const char* const BUTTON_SECTION = "shortcutButton"; constexpr static const char* const AXIS_SECTION = "shortcutAxis"; + constexpr static const char* const BUTTON_PROFILE_SECTION = "shortcutProfileButton."; + constexpr static const char* const AXIS_PROFILE_SECTION = "shortcutProfileAxis."; class ShortcutItem { public:

@@ -84,6 +87,7 @@ public:

ShortcutController(QObject* parent = nullptr); void setConfigController(ConfigController* controller); + void setProfile(const QString& profile); virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;

@@ -111,6 +115,9 @@ void clearButton(const QModelIndex& index);

static QKeySequence keyEventToSequence(const QKeyEvent*); +public slots: + void loadProfile(const QString& profile); + protected: bool eventFilter(QObject*, QEvent*) override;

@@ -118,6 +125,8 @@ private:

ShortcutItem* itemAt(const QModelIndex& index); const ShortcutItem* itemAt(const QModelIndex& index) const; void loadShortcuts(ShortcutItem*); + void loadGamepadShortcuts(ShortcutItem*); + void onSubitems(ShortcutItem*, std::function<void(ShortcutItem*)> func); ShortcutItem m_rootMenu; QMap<QMenu*, ShortcutItem*> m_menuMap;

@@ -125,6 +134,8 @@ QMap<int, ShortcutItem*> m_buttons;

QMap<QPair<int, GamepadAxisEvent::Direction>, ShortcutItem*> m_axes; QMap<QKeySequence, ShortcutItem*> m_heldKeys; ConfigController* m_config; + QString m_profileName; + const InputProfile* m_profile; }; }
M src/platform/qt/ShortcutView.cppsrc/platform/qt/ShortcutView.cpp

@@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ShortcutView.h" #include "GamepadButtonEvent.h" +#include "InputController.h" #include "ShortcutController.h" #include <QKeyEvent>

@@ -15,6 +16,7 @@

ShortcutView::ShortcutView(QWidget* parent) : QWidget(parent) , m_controller(nullptr) + , m_input(nullptr) { m_ui.setupUi(this); m_ui.keyEdit->setValueButton(-1);

@@ -30,6 +32,14 @@

void ShortcutView::setController(ShortcutController* controller) { m_controller = controller; m_ui.shortcutTable->setModel(controller); +} + +void ShortcutView::setInputController(InputController* controller) { + if (m_input) { + m_input->releaseFocus(this); + } + m_input = controller; + m_input->stealFocus(this); } bool ShortcutView::eventFilter(QObject*, QEvent* event) {

@@ -111,3 +121,20 @@ }

m_controller->updateAxis(m_ui.shortcutTable->selectionModel()->currentIndex(), axis, static_cast<GamepadAxisEvent::Direction>(direction)); } + +void ShortcutView::closeEvent(QCloseEvent*) { + if (m_input) { + m_input->releaseFocus(this); + } +} + +bool ShortcutView::event(QEvent* event) { + if (m_input) { + if (event->type() == QEvent::WindowActivate) { + m_input->stealFocus(this); + } else if (event->type() == QEvent::WindowDeactivate) { + m_input->releaseFocus(this); + } + } + return QWidget::event(event); +}
M src/platform/qt/ShortcutView.hsrc/platform/qt/ShortcutView.h

@@ -14,6 +14,7 @@ #include "ui_ShortcutView.h"

namespace QGBA { +class InputController; class ShortcutController; class ShortcutView : public QWidget {

@@ -23,9 +24,12 @@ public:

ShortcutView(QWidget* parent = nullptr); void setController(ShortcutController* controller); + void setInputController(InputController* input); protected: virtual bool eventFilter(QObject* obj, QEvent* event) override; + virtual bool event(QEvent*) override; + virtual void closeEvent(QCloseEvent*) override; private slots: void load(const QModelIndex&);

@@ -38,6 +42,7 @@ private:

Ui::ShortcutView m_ui; ShortcutController* m_controller; + InputController* m_input; }; }
M src/platform/qt/Window.cppsrc/platform/qt/Window.cpp

@@ -53,7 +53,7 @@ , m_stateWindow(nullptr)

, m_screenWidget(new WindowBackground()) , m_logo(":/res/mgba-1024.png") , m_config(config) - , m_inputController(playerId) + , m_inputController(playerId, this) #ifdef USE_FFMPEG , m_videoView(nullptr) #endif

@@ -128,6 +128,15 @@ connect(this, SIGNAL(shutdown()), m_logView, SLOT(hide()));

connect(this, SIGNAL(audioBufferSamplesChanged(int)), m_controller, SLOT(setAudioBufferSamples(int))); connect(this, SIGNAL(fpsTargetChanged(float)), m_controller, SLOT(setFPSTarget(float))); connect(&m_fpsTimer, SIGNAL(timeout()), this, SLOT(showFPS())); + connect(m_display, &Display::hideCursor, [this]() { + if (static_cast<QStackedLayout*>(m_screenWidget->layout())->currentWidget() == m_display) { + setCursor(Qt::BlankCursor); + } + }); + connect(m_display, &Display::showCursor, [this]() { + unsetCursor(); + }); + connect(&m_inputController, SIGNAL(profileLoaded(const QString&)), m_shortcutController, SLOT(loadProfile(const QString&))); m_log.setLevels(GBA_LOG_WARN | GBA_LOG_ERROR | GBA_LOG_FATAL | GBA_LOG_STATUS); m_fpsTimer.setInterval(FPS_TIMER_INTERVAL);

@@ -296,6 +305,7 @@ void Window::openSettingsWindow() {

SettingsView* settingsWindow = new SettingsView(m_config); connect(settingsWindow, SIGNAL(biosLoaded(const QString&)), m_controller, SLOT(loadBIOS(const QString&))); connect(settingsWindow, SIGNAL(audioDriverChanged()), m_controller, SLOT(reloadAudioDriver())); + connect(settingsWindow, SIGNAL(displayDriverChanged()), this, SLOT(mustRestart())); openView(settingsWindow); }

@@ -305,6 +315,7 @@ m_inputController.recalibrateAxes();

#endif ShortcutView* shortcutView = new ShortcutView(); shortcutView->setController(m_shortcutController); + shortcutView->setInputController(&m_inputController); openView(shortcutView); }

@@ -561,6 +572,23 @@ fail->setAttribute(Qt::WA_DeleteOnClose);

fail->show(); } +void Window::tryMakePortable() { + QMessageBox* confirm = new QMessageBox(QMessageBox::Question, tr("Really make portable?"), + tr("This will make the emulator load its configuration from the same directory as the executable. Do you want to continue?"), + QMessageBox::Yes | QMessageBox::Cancel, this, Qt::Sheet); + confirm->setAttribute(Qt::WA_DeleteOnClose); + connect(confirm->button(QMessageBox::Yes), SIGNAL(clicked()), m_config, SLOT(makePortable())); + confirm->show(); +} + +void Window::mustRestart() { + QMessageBox* dialog = new QMessageBox(QMessageBox::Warning, tr("Restart needed"), + tr("Some changes will not take effect until the emulator is restarted."), + QMessageBox::Ok, this, Qt::Sheet); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); +} + void Window::recordFrame() { m_frameList.append(QDateTime::currentDateTime()); while (m_frameList.count() > FRAME_LIST_SIZE) {

@@ -642,7 +670,7 @@ m_mruMenu = fileMenu->addMenu(tr("Recent"));

fileMenu->addSeparator(); - addControlledAction(fileMenu, fileMenu->addAction(tr("Make portable"), m_config, SLOT(makePortable())), "makePortable"); + addControlledAction(fileMenu, fileMenu->addAction(tr("Make portable"), this, SLOT(tryMakePortable())), "makePortable"); fileMenu->addSeparator();

@@ -676,6 +704,21 @@

quickLoadMenu->addSeparator(); quickSaveMenu->addSeparator(); + QAction* undoLoadState = new QAction(tr("Undo load state"), quickLoadMenu); + undoLoadState->setShortcut(tr("F11")); + connect(undoLoadState, SIGNAL(triggered()), m_controller, SLOT(loadBackupState())); + m_gameActions.append(undoLoadState); + addControlledAction(quickLoadMenu, undoLoadState, "undoLoadState"); + + QAction* undoSaveState = new QAction(tr("Undo save state"), quickSaveMenu); + undoSaveState->setShortcut(tr("Shift+F11")); + connect(undoSaveState, SIGNAL(triggered()), m_controller, SLOT(saveBackupState())); + m_gameActions.append(undoSaveState); + addControlledAction(quickSaveMenu, undoSaveState, "undoSaveState"); + + quickLoadMenu->addSeparator(); + quickSaveMenu->addSeparator(); + int i; for (i = 1; i < 10; ++i) { quickLoad = new QAction(tr("State &%1").arg(i), quickLoadMenu);

@@ -937,14 +980,12 @@ #endif

#ifdef USE_FFMPEG QAction* recordOutput = new QAction(tr("Record output..."), avMenu); - recordOutput->setShortcut(tr("F11")); connect(recordOutput, SIGNAL(triggered()), this, SLOT(openVideoWindow())); addControlledAction(avMenu, recordOutput, "recordOutput"); #endif #ifdef USE_MAGICK QAction* recordGIF = new QAction(tr("Record GIF..."), avMenu); - recordGIF->setShortcut(tr("Shift+F11")); connect(recordGIF, SIGNAL(triggered()), this, SLOT(openGIFWindow())); addControlledAction(avMenu, recordGIF, "recordGIF"); #endif

@@ -1092,6 +1133,7 @@ }

void Window::attachWidget(QWidget* widget) { m_screenWidget->layout()->addWidget(widget); + unsetCursor(); static_cast<QStackedLayout*>(m_screenWidget->layout())->setCurrentWidget(widget); }
M src/platform/qt/Window.hsrc/platform/qt/Window.h

@@ -115,6 +115,9 @@ void gameCrashed(const QString&);

void gameFailed(); void unimplementedBiosCall(int); + void tryMakePortable(); + void mustRestart(); + void recordFrame(); void showFPS();
M src/util/configuration.csrc/util/configuration.c

@@ -100,6 +100,10 @@ }

HashTableRemove(currentSection, key); } +bool ConfigurationHasSection(const struct Configuration* configuration, const char* section) { + return HashTableLookup(&configuration->sections, section); +} + const char* ConfigurationGetValue(const struct Configuration* configuration, const char* section, const char* key) { const struct Table* currentSection = &configuration->root; if (section) {
M src/util/configuration.hsrc/util/configuration.h

@@ -23,6 +23,7 @@ void ConfigurationSetIntValue(struct Configuration*, const char* section, const char* key, int value);

void ConfigurationSetUIntValue(struct Configuration*, const char* section, const char* key, unsigned value); void ConfigurationSetFloatValue(struct Configuration*, const char* section, const char* key, float value); +bool ConfigurationHasSection(const struct Configuration*, const char* section); const char* ConfigurationGetValue(const struct Configuration*, const char* section, const char* key); void ConfigurationClearValue(struct Configuration*, const char* section, const char* key);
M src/util/vfs/vfs-fd.csrc/util/vfs/vfs-fd.c

@@ -173,5 +173,9 @@ static bool _vfdSync(struct VFile* vf, const void* buffer, size_t size) {

UNUSED(buffer); UNUSED(size); struct VFileFD* vfd = (struct VFileFD*) vf; +#ifndef _WIN32 return fsync(vfd->fd) == 0; +#else + return FlushFileBuffers((HANDLE) _get_osfhandle(vfd->fd)); +#endif }
M src/util/vfs/vfs-lzma.csrc/util/vfs/vfs-lzma.c

@@ -95,6 +95,7 @@

SzArEx_Init(&vd->db); SRes res = SzArEx_Open(&vd->db, &vd->lookStream.s, &vd->allocImp, &vd->allocTempImp); if (res != SZ_OK) { + File_Close(&vd->archiveStream.file); free(vd); return 0; }

@@ -115,6 +116,7 @@

bool _vf7zClose(struct VFile* vf) { struct VFile7z* vf7z = (struct VFile7z*) vf; IAlloc_Free(&vf7z->vd->allocImp, vf7z->outBuffer); + File_Close(&vf7z->vd->archiveStream.file); return true; }
M src/util/vfs/vfs-mem.csrc/util/vfs/vfs-mem.c

@@ -20,6 +20,7 @@ static void* _vfmMap(struct VFile* vf, size_t size, int flags);

static void _vfmUnmap(struct VFile* vf, void* memory, size_t size); static void _vfmTruncate(struct VFile* vf, size_t size); static ssize_t _vfmSize(struct VFile* vf); +static bool _vfmSync(struct VFile* vf, const void* buffer, size_t size); struct VFile* VFileFromMemory(void* mem, size_t size) { if (!mem || !size) {

@@ -43,6 +44,7 @@ vfm->d.map = _vfmMap;

vfm->d.unmap = _vfmUnmap; vfm->d.truncate = _vfmTruncate; vfm->d.size = _vfmSize; + vfm->d.sync = _vfmSync; return &vfm->d; }

@@ -137,3 +139,10 @@ ssize_t _vfmSize(struct VFile* vf) {

struct VFileMem* vfm = (struct VFileMem*) vf; return vfm->size; } + +bool _vfmSync(struct VFile* vf, const void* buffer, size_t size) { + UNUSED(vf); + UNUSED(buffer); + UNUSED(size); + return true; +}
M version.cmakeversion.cmake

@@ -1,5 +1,37 @@

-if(NOT ${BINARY_NAME}_SOURCE_DIR) - set(${BINARY_NAME}_SOURCE_DIR ${CMAKE_SOURCE_DIR}) +if(NOT PROJECT_NAME) + set(PROJECT_NAME "mGBA") endif() +set(LIB_VERSION_MAJOR 0) +set(LIB_VERSION_MINOR 3) +set(LIB_VERSION_PATCH 0) +set(LIB_VERSION_ABI 0.3) +set(LIB_VERSION_STRING ${LIB_VERSION_MAJOR}.${LIB_VERSION_MINOR}.${LIB_VERSION_PATCH}) -configure_file("${${BINARY_NAME}_SOURCE_DIR}/src/util/version.c.in" "${CMAKE_CURRENT_BINARY_DIR}/version.c") +execute_process(COMMAND git describe --always --abbrev=40 --dirty OUTPUT_VARIABLE GIT_COMMIT ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) +execute_process(COMMAND git describe --always --dirty OUTPUT_VARIABLE GIT_COMMIT_SHORT ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) +execute_process(COMMAND git symbolic-ref --short HEAD OUTPUT_VARIABLE GIT_BRANCH ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) +execute_process(COMMAND git rev-list HEAD --count OUTPUT_VARIABLE GIT_REV ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) +execute_process(COMMAND git describe --tag --exact-match OUTPUT_VARIABLE GIT_TAG ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + +if(GIT_REV STREQUAL "") + set(GIT_REV -1) +endif() +if(NOT GIT_TAG STREQUAL "") + set(VERSION_STRING ${GIT_TAG}) +elseif(GIT_BRANCH STREQUAL "") + set(VERSION_STRING ${LIB_VERSION_STRING}) +else() + if(GIT_BRANCH STREQUAL "master") + set(VERSION_STRING ${GIT_REV}-${GIT_COMMIT_SHORT}) + else() + set(VERSION_STRING ${GIT_BRANCH}-${GIT_REV}-${GIT_COMMIT_SHORT}) + endif() + + if(NOT LIB_VERSION_ABI STREQUAL GIT_BRANCH) + set(VERSION_STRING ${LIB_VERSION_ABI}-${VERSION_STRING}) + endif() +endif() + +if(CONFIG_FILE AND OUT_FILE) + configure_file("${CONFIG_FILE}" "${OUT_FILE}") +endif()