diff --git a/CMakeLists.txt b/CMakeLists.txt
index 595d56068..17b524f4e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -18,6 +18,7 @@ set(USE_LZMA ON CACHE BOOL "Whether or not to enable 7-Zip support")
set(BUILD_QT ON CACHE BOOL "Build Qt frontend")
set(BUILD_SDL ON CACHE BOOL "Build SDL frontend")
set(BUILD_LIBRETRO OFF CACHE BOOL "Build libretro core")
+set(BUILD_OPENEMU OFF CACHE BOOL "Build OpenEmu core")
set(BUILD_PERF OFF CACHE BOOL "Build performance profiling tool")
set(BUILD_TEST OFF CACHE BOOL "Build testing harness")
set(BUILD_STATIC OFF CACHE BOOL "Build a static library")
@@ -558,6 +559,21 @@ if(BUILD_LIBRETRO)
install(TARGETS ${BINARY_NAME}_libretro LIBRARY DESTINATION ${LIBDIR} COMPONENT ${BINARY_NAME}_libretro NAMELINK_SKIP)
endif()
+if(BUILD_OPENEMU)
+ find_library(FOUNDATION Foundation)
+ find_library(OPENEMUBASE OpenEmuBase)
+ file(GLOB OE_SRC ${CMAKE_SOURCE_DIR}/src/platform/openemu/*.m)
+ add_library(${BINARY_NAME}-openemu MODULE ${CORE_SRC} ${OE_SRC})
+ set_target_properties(${BINARY_NAME}-openemu PROPERTIES
+ MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/src/platform/openemu/Info.plist.in
+ BUNDLE TRUE
+ BUNDLE_EXTENSION oecoreplugin
+ OUTPUT_NAME ${PROJECT_NAME}EmuCore
+ COMPILE_DEFINITIONS "DISABLE_THREADING;${OS_DEFINES};${FUNCTION_DEFINES};MINIMAL_CORE=2")
+ target_link_libraries(${BINARY_NAME}-openemu ${OS_LIB} ${FOUNDATION} ${OPENEMUBASE})
+ install(TARGETS ${BINARY_NAME}-openemu LIBRARY DESTINATION ${LIBDIR} COMPONENT ${BINARY_NAME}.oecoreplugin NAMELINK_SKIP)
+endif()
+
if(BUILD_SDL)
add_definitions(-DBUILD_SDL)
add_subdirectory(${CMAKE_SOURCE_DIR}/src/platform/sdl ${CMAKE_BINARY_DIR}/sdl)
diff --git a/src/platform/openemu/Info.plist.in b/src/platform/openemu/Info.plist.in
new file mode 100644
index 000000000..fffbe7aa4
--- /dev/null
+++ b/src/platform/openemu/Info.plist.in
@@ -0,0 +1,42 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ ${EXECUTABLE_NAME}
+ CFBundleIconFile
+ mGBA
+ CFBundleIdentifier
+ com.endrift.mgba
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundlePackageType
+ BNDL
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.8.0.1232
+ NSPrincipalClass
+ OEGameCoreController
+ OEGameCoreClass
+ mGBAGameCore
+ OEGameCoreOptions
+
+ openemu.system.gba
+
+ OEGameCorePlayerCount
+ 1
+ OEProjectURL
+ https://mgba.io/
+ OESystemIdentifiers
+
+ openemu.system.gba
+
+ SUEnableAutomaticChecks
+ 1
+ SUFeedURL
+ https://raw.github.com/OpenEmu/OpenEmu-Update/master/mgba_appcast.xml
+
+
diff --git a/src/platform/openemu/OEGBASystemResponderClient.h b/src/platform/openemu/OEGBASystemResponderClient.h
new file mode 100644
index 000000000..4fc0179d1
--- /dev/null
+++ b/src/platform/openemu/OEGBASystemResponderClient.h
@@ -0,0 +1,51 @@
+/*
+ Copyright (c) 2011, OpenEmu Team
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the OpenEmu Team nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import
+
+@protocol OESystemResponderClient;
+
+typedef enum _OEGBAButton
+{
+ OEGBAButtonUp,
+ OEGBAButtonDown,
+ OEGBAButtonLeft,
+ OEGBAButtonRight,
+ OEGBAButtonA,
+ OEGBAButtonB,
+ OEGBAButtonL,
+ OEGBAButtonR,
+ OEGBAButtonStart,
+ OEGBAButtonSelect,
+ OEGBAButtonCount
+} OEGBAButton;
+
+@protocol OEGBASystemResponderClient
+
+- (oneway void)didPushGBAButton:(OEGBAButton)button forPlayer:(NSUInteger)player;
+- (oneway void)didReleaseGBAButton:(OEGBAButton)button forPlayer:(NSUInteger)player;
+
+@end
diff --git a/src/platform/openemu/mGBAGameCore.h b/src/platform/openemu/mGBAGameCore.h
new file mode 100644
index 000000000..744fdecfe
--- /dev/null
+++ b/src/platform/openemu/mGBAGameCore.h
@@ -0,0 +1,6 @@
+#import
+#import
+
+OE_EXPORTED_CLASS
+@interface mGBAGameCore : OEGameCore
+@end
diff --git a/src/platform/openemu/mGBAGameCore.m b/src/platform/openemu/mGBAGameCore.m
new file mode 100644
index 000000000..1036d6024
--- /dev/null
+++ b/src/platform/openemu/mGBAGameCore.m
@@ -0,0 +1,251 @@
+/*
+ Copyright (c) 2016, Jeffrey Pfau
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ''AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "mGBAGameCore.h"
+
+#include "util/common.h"
+
+#include "gba/cheats.h"
+#include "gba/renderers/video-software.h"
+#include "gba/serialize.h"
+#include "gba/context/context.h"
+#include "util/circle-buffer.h"
+#include "util/memory.h"
+#include "util/vfs.h"
+
+#import
+#import "OEGBASystemResponderClient.h"
+#import
+
+#define SAMPLES 1024
+
+@interface mGBAGameCore ()
+{
+ struct GBAContext context;
+ struct GBAVideoSoftwareRenderer renderer;
+ struct GBACheatDevice cheats;
+ struct GBACheatSet cheatSet;
+ uint16_t keys;
+}
+@end
+
+@implementation mGBAGameCore
+
+- (id)init
+{
+ if ((self = [super init]))
+ {
+ // TODO: Add a log handler
+ GBAContextInit(&context, 0);
+ struct GBAOptions opts = {
+ .useBios = true,
+ .idleOptimization = IDLE_LOOP_REMOVE
+ };
+ GBAConfigLoadDefaults(&context.config, &opts);
+ GBAVideoSoftwareRendererCreate(&renderer);
+ renderer.outputBuffer = malloc(256 * VIDEO_VERTICAL_PIXELS * BYTES_PER_PIXEL);
+ renderer.outputBufferStride = 256;
+ context.renderer = &renderer.d;
+ GBAAudioResizeBuffer(&context.gba->audio, SAMPLES);
+ GBACheatDeviceCreate(&cheats);
+ GBACheatAttachDevice(context.gba, &cheats);
+ GBACheatSetInit(&cheatSet, "openemu");
+ GBACheatAddSet(&cheats, &cheatSet);
+ keys = 0;
+ }
+
+ return self;
+}
+
+- (void)dealloc
+{
+ GBAContextDeinit(&context);
+ GBACheatRemoveSet(&cheats, &cheatSet);
+ GBACheatDeviceDestroy(&cheats);
+ GBACheatSetDeinit(&cheatSet);
+ free(renderer.outputBuffer);
+
+ [super dealloc];
+}
+
+#pragma mark - Execution
+
+- (BOOL)loadFileAtPath:(NSString *)path error:(NSError **)error
+{
+ UNUSED(error);
+ if (!GBAContextLoadROM(&context, [path UTF8String], true)) {
+ return NO;
+ }
+ GBAContextStart(&context);
+ return YES;
+}
+
+- (void)executeFrame
+{
+ GBAContextFrame(&context, keys);
+
+ int16_t samples[SAMPLES * 2];
+ size_t available = 0;
+#if RESAMPLE_LIBRARY == RESAMPLE_BLIP_BUF
+ available = blip_samples_avail(context.gba->audio.left);
+ blip_read_samples(context.gba->audio.left, samples, available, true);
+ blip_read_samples(context.gba->audio.right, samples + 1, available, true);
+#else
+#error BLIP_BUF is required for now
+#endif
+ [[self ringBufferAtIndex:0] write:samples maxLength:available * 4];
+}
+
+- (void)resetEmulation
+{
+ ARMReset(context.cpu);
+}
+
+- (void)stopEmulation
+{
+ NSLog(@"Stopping");
+ GBAContextStop(&context);
+ [super stopEmulation];
+}
+
+- (void)setupEmulation
+{
+#if RESAMPLE_LIBRARY == RESAMPLE_BLIP_BUF
+ blip_set_rates(context.gba->audio.left, GBA_ARM7TDMI_FREQUENCY, 32768);
+ blip_set_rates(context.gba->audio.right, GBA_ARM7TDMI_FREQUENCY, 32768);
+#endif
+}
+
+#pragma mark - Video
+
+- (OEIntSize)aspectSize
+{
+ return OEIntSizeMake(3, 2);
+}
+
+- (OEIntRect)screenRect
+{
+ return OEIntRectMake(0, 0, VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS);
+}
+
+- (OEIntSize)bufferSize
+{
+ return OEIntSizeMake(256, VIDEO_VERTICAL_PIXELS);
+}
+
+- (const void *)videoBuffer
+{
+ return renderer.outputBuffer;
+}
+
+- (GLenum)pixelFormat
+{
+ return GL_RGBA;
+}
+
+- (GLenum)pixelType
+{
+ return GL_UNSIGNED_INT_8_8_8_8_REV;
+}
+
+- (GLenum)internalPixelFormat
+{
+ return GL_RGB8;
+}
+
+- (NSTimeInterval)frameInterval
+{
+ return GBA_ARM7TDMI_FREQUENCY / (double) VIDEO_TOTAL_LENGTH;
+}
+
+#pragma mark - Audio
+
+- (NSUInteger)channelCount
+{
+ return 2;
+}
+
+- (double)audioSampleRate
+{
+ return 32768;
+}
+
+#pragma mark - Save State
+
+- (NSData *)serializeStateWithError:(NSError **)outError
+{
+ UNUSED(outError);
+ // TODO memory VFile that self-manages memory
+ return nil;
+}
+
+- (BOOL)deserializeState:(NSData *)state withError:(NSError **)outError
+{
+ UNUSED(outError);
+ // TODO VFileFromConstMemory
+ struct VFile* vf = VFileFromMemory((void*) state.bytes, state.length);
+ return GBALoadStateNamed(context.gba, vf, 0);
+}
+
+- (void)saveStateToFileAtPath:(NSString *)fileName completionHandler:(void (^)(BOOL, NSError *))block
+{
+ struct VFile* vf = VFileOpen([fileName UTF8String], O_CREAT | O_TRUNC | O_RDWR);
+ block(GBASaveStateNamed(context.gba, vf, 0), nil);
+}
+
+- (void)loadStateFromFileAtPath:(NSString *)fileName completionHandler:(void (^)(BOOL, NSError *))block
+{
+ struct VFile* vf = VFileOpen([fileName UTF8String], O_RDONLY);
+ block(GBALoadStateNamed(context.gba, vf, 0), nil);
+}
+
+#pragma mark - Input
+
+const int GBAMap[] = {
+ GBA_KEY_UP,
+ GBA_KEY_DOWN,
+ GBA_KEY_LEFT,
+ GBA_KEY_RIGHT,
+ GBA_KEY_A,
+ GBA_KEY_B,
+ GBA_KEY_L,
+ GBA_KEY_R,
+ GBA_KEY_START,
+ GBA_KEY_SELECT
+};
+
+- (oneway void)didPushGBAButton:(OEGBAButton)button forPlayer:(NSUInteger)player
+{
+ UNUSED(player);
+ keys |= 1 << GBAMap[button];
+}
+
+- (oneway void)didReleaseGBAButton:(OEGBAButton)button forPlayer:(NSUInteger)player
+{
+ UNUSED(player);
+ keys &= ~(1 << GBAMap[button]);
+}
+
+@end
+