Repository: Provenance-Emu/Provenance (language: cpp)

Issue:
Multidisc appears not to work on Sega Saturn
Multidisc appears not to work on Sega Saturn.

### Expected: 

Adding a multidisc game from a properly formatted ZIP should create a multidisc game entry.

### Actual: 

Provenance prompts to resolve conflicts (specify system) for each disc, then creates an icon associated with only one disc (whichever conflict you resolved first). All other discs and the m3u are orphaned in the Sega Saturn directory and not associated with the entry. They will remain if you delete the game entry with only the single disc associated with the icon being removed, and cause a crash if you attempt to re-add the game.

 ### Reproduce:  

Create a multidisc file named as:
- Sakura Wars (Japan) (Disc 1).bin
- Sakura Wars (Japan) (Disc 1).cue
- Sakura Wars (Japan) (Disc 2).bin
- Sakura Wars (Japan) (Disc 2).cue
- Sakura Wars (Japan).m3u

These are contained in Sakura Wars (Japan).zip. I have also tested with Panzer Dragoon Saga and Grandia with the same results.

#### NOTES  

I don't think it's user error. Single disc Sega Saturn games load, and I have no problem creating multidisc entries in PlayStation using the same method.




#











































































#### APP VERSION
- [x] 2.0.0
- [ ] 1.5 *beta*
- [ ] 1.4

#### APP SOURCE
- [x] Provenance-Emu GitHub  *&nbsp;(Official)*
- [ ] Other  *&nbsp;(Unofficial)*

#### INSTALLED BY 
- [x] [Side-Loading](https://git.io/vpYzf)
- [ ] [Building](https://git.io/vpYzv) *from Source*

#### PLATFORM
- [x] iPhone
- [ ] iPad
- [ ] Apple TV

#### iOS/tvOS VERSION
- [x] 14.x
- [ ] 13.x
- [ ] 12.x
- [ ] 11.x
- [ ] 10.x

-------------------------------------------
<sup> 🚫 We **DO NOT** support unofficial builds installed from 3rd-party sites. ([Official Install](https://git.io/vxABg)) </sup><br><sup> ❓ **Need help or have a suggestion?** Join our [Official Discord](https://discord.gg/4TK7PU5) </sup><br>



Discussion / hints:
Is there a smaller Multidisc game that is also affected by this to help with the testing? @sobodash ?
> Is there a smaller Multidisc game that is also affected by this to help with the testing? @sobodash ?

Any multidisc game is going to be at least two discs. I took a quick look at the REDUMP collection on Archive.org and it looks like one of the smaller multidisc game is:

- Chou Jikuu Yousai Macross - Ai Oboete Imasu ka (Japan) (Disc 1).zip [187.6M]
- Chou Jikuu Yousai Macross - Ai Oboete Imasu ka (Japan) (Disc 2).zip [352.1M]

This is a REDUMP game though and they tend to have a BIN file for each track on each disc. Not sure if Provenance supports that. Might need to convert to a single BIN/CUE for each disc using something like ISOBuster.

I am under the impression all multidisc games on Saturn are affected by this because I have tried five games now and experienced the same behavior with all five.

I would also note that when adding a single CUE/BIN game it always prompts for what system to sort it into. When adding a multidisc PSX game with an M3U it is automatically detected as PSX with no additional prompts. Not sure whether this is related.
I noted this exact problem with Panzer Dragoon Saga. 
I don’t know if it will help, but adding the ccd, img and sub files from the 4 cds at once will add each individual disc to the library except for disc 1. This happens wether I follow the adequate rom formatting with the m3u file or not. If I erase those 3 discs from the library and reload the app it will add the first disc to the library. 
Would we be able to play multidisc games before this bug can be fixed?

Fix patch:
diff --git a/Cores/Mednafen/MednafenGameCore.h b/Cores/Mednafen/MednafenGameCore.h
index 382154885e..b1017d3b33 100644
--- a/Cores/Mednafen/MednafenGameCore.h
+++ b/Cores/Mednafen/MednafenGameCore.h
@@ -63,6 +63,9 @@ __attribute__((visibility("default")))
 @interface MednafenGameCore()
 @property (nonatomic, assign) MednaSystem systemType;
 @property (nonatomic, assign) NSUInteger maxDiscs;
+
+@property (nonatomic, assign) BOOL video_opengl;
+
 -(void)setMedia:(BOOL)open forDisc:(NSUInteger)disc;
 -(void)changeDisplayMode;
 
diff --git a/Cores/Mednafen/MednafenGameCore.mm b/Cores/Mednafen/MednafenGameCore.mm
index 7c67469647..4c5f765424 100644
--- a/Cores/Mednafen/MednafenGameCore.mm
+++ b/Cores/Mednafen/MednafenGameCore.mm
@@ -399,6 +399,7 @@ - (id)init {
     }
 
     cheatList = [[NSMutableDictionary alloc] init];
+    [self parseOptions];
 
     return self;
 }
@@ -482,6 +483,7 @@ static void emulation_run(BOOL skipFrame) {
 
 - (BOOL)loadFileAtPath:(NSString *)path error:(NSError**)error
 {
+    [self parseOptions];
     [[NSFileManager defaultManager] createDirectoryAtPath:[self batterySavesPath] withIntermediateDirectories:YES attributes:nil error:NULL];
 
     if([[self systemIdentifier] isEqualToString:@"com.provenance.lynx"])
@@ -653,6 +655,7 @@ - (BOOL)loadFileAtPath:(NSString *)path error:(NSError**)error
     frontBufferSurf = new Mednafen::MDFN_Surface(NULL, game->fb_width, game->fb_height, game->fb_width, pix_fmt);
 
     masterClock = game->MasterClock >> 32;
+    BOOL multiDiscGame = NO;
 
     if (self.systemType == MednaSystemPCE)
     {
@@ -672,6 +675,19 @@ - (BOOL)loadFileAtPath:(NSString *)path error:(NSError**)error
         game->SetInput(0, "gamepad", (uint8_t *)inputBuffer[0]);
         game->SetInput(1, "gamepad", (uint8_t *)inputBuffer[1]);
     }
+    else if (self.systemType == MednaSystemSS) {
+        BOOL hasM3u = [path.pathExtension.lowercaseString isEqualToString:@"m3u"];
+        if (hasM3u) {
+            multiDiscGame = YES;
+            
+            // TODO: Make this real
+            // https://gamicus.fandom.com/wiki/List_of_Saturn_video_games_with_multiple_discs
+            self.maxDiscs = 4;
+        }
+        
+        game->SetInput(0, "gamepad", (uint8_t *)inputBuffer[0]);
+        game->SetInput(1, "gamepad", (uint8_t *)inputBuffer[1]);
+    }
     else if (self.systemType == MednaSystemPSX)
     {
         for(unsigned i = 0; i < multiTapPlayerCount; i++) {
@@ -686,7 +702,6 @@ - (BOOL)loadFileAtPath:(NSString *)path error:(NSError**)error
         }
         
         // Multi-Disc check
-        BOOL multiDiscGame = NO;
         NSNumber *discCount = [MednafenGameCore multiDiscPSXGames][self.romSerial];
         if (discCount) {
             self.maxDiscs = [discCount intValue];
@@ -770,45 +785,45 @@ - (BOOL)loadFileAtPath:(NSString *)path error:(NSError**)error
 //                return NO;
 //            }
 //        }
-        
-        if (multiDiscGame && ![path.pathExtension.lowercaseString isEqualToString:@"m3u"]) {
-            NSString *m3uPath = [path.stringByDeletingPathExtension stringByAppendingPathExtension:@"m3u"];
-            NSRange rangeOfDocuments = [m3uPath rangeOfString:@"/Documents/" options:NSCaseInsensitiveSearch];
-            if (rangeOfDocuments.location != NSNotFound) {
-                m3uPath = [m3uPath substringFromIndex:rangeOfDocuments.location + 11];
-            }
+    }
+    else
+    {
+        game->SetInput(0, "gamepad", (uint8_t *)inputBuffer[0]);
+        game->SetInput(1, "gamepad", (uint8_t *)inputBuffer[0]);
+    }
+    
+    if (multiDiscGame && ![path.pathExtension.lowercaseString isEqualToString:@"m3u"]) {
+        NSString *m3uPath = [path.stringByDeletingPathExtension stringByAppendingPathExtension:@"m3u"];
+        NSRange rangeOfDocuments = [m3uPath rangeOfString:@"/Documents/" options:NSCaseInsensitiveSearch];
+        if (rangeOfDocuments.location != NSNotFound) {
+            m3uPath = [m3uPath substringFromIndex:rangeOfDocuments.location + 11];
+        }
 
-            if (error) {
-                NSString *message = [NSString stringWithFormat:@"This game requires multiple discs and must be loaded using a m3u file with all %lu discs.\n\nTo enable disc switching and ensure save files load across discs, it cannot be loaded as a single disc.\n\nPlease install a .m3u file with the filename %@.\nSee https://bitly.com/provm3u", self.maxDiscs, m3uPath];
+        if (error) {
+            NSString *message = [NSString stringWithFormat:@"This game requires multiple discs and must be loaded using a m3u file with all %lu discs.\n\nTo enable disc switching and ensure save files load across discs, it cannot be loaded as a single disc.\n\nPlease install a .m3u file with the filename %@.\nSee https://bitly.com/provm3u", self.maxDiscs, m3uPath];
 
-                NSDictionary *userInfo = @{
-                                           NSLocalizedDescriptionKey: @"Failed to load game.",
-                                           NSLocalizedFailureReasonErrorKey: @"Missing required m3u file.",
-                                           NSLocalizedRecoverySuggestionErrorKey: message
-                                           };
+            NSDictionary *userInfo = @{
+                                       NSLocalizedDescriptionKey: @"Failed to load game.",
+                                       NSLocalizedFailureReasonErrorKey: @"Missing required m3u file.",
+                                       NSLocalizedRecoverySuggestionErrorKey: message
+                                       };
 
-                NSError *newError = [NSError errorWithDomain:PVEmulatorCoreErrorDomain
-                                                        code:PVEmulatorCoreErrorCodeMissingM3U
-                                                    userInfo:userInfo];
+            NSError *newError = [NSError errorWithDomain:PVEmulatorCoreErrorDomain
+                                                    code:PVEmulatorCoreErrorCodeMissingM3U
+                                                userInfo:userInfo];
 
-                *error = newError;
-            }
-            return NO;
-        }
-        
-        if (self.maxDiscs > 1) {
-            // Parse number of discs in m3u
-            NSString *m3uString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
-            NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@".cue|.ccd" options:NSRegularExpressionCaseInsensitive error:nil];
-            NSUInteger numberOfMatches = [regex numberOfMatchesInString:m3uString options:0 range:NSMakeRange(0, [m3uString length])];
-            
-            ILOG(@"Loaded m3u containing %lu cue sheets or ccd",numberOfMatches);
+            *error = newError;
         }
+        return NO;
     }
-    else
-    {
-        game->SetInput(0, "gamepad", (uint8_t *)inputBuffer[0]);
-        game->SetInput(1, "gamepad", (uint8_t *)inputBuffer[0]);
+    
+    if (self.maxDiscs > 1) {
+        // Parse number of discs in m3u
+        NSString *m3uString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
+        NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@".cue|.ccd" options:NSRegularExpressionCaseInsensitive error:nil];
+        NSUInteger numberOfMatches = [regex numberOfMatchesInString:m3uString options:0 range:NSMakeRange(0, [m3uString length])];
+        
+        ILOG(@"Loaded m3u containing %lu cue sheets or ccd",numberOfMatches);
     }
 
 //    BOOL success =
diff --git a/Cores/Mednafen/MednafenGameCore.swift b/Cores/Mednafen/MednafenGameCore.swift
index 2a635f0cbe..d52537ecd9 100644
--- a/Cores/Mednafen/MednafenGameCore.swift
+++ b/Cores/Mednafen/MednafenGameCore.swift
@@ -82,7 +82,7 @@ extension MednafenGameCore: CoreOptional {
         
         let videoGroup:CoreOption = .group(.init(title: "Video",
                                                 description: "Video options for all Mednafen cores."),
-                                          subOptions: [video_blit_timesync, video_fs, video_opengl])
+                                          subOptions: [video_blit_timesync, video_fs, video_openglOption])
         
 
         options.append(videoGroup)
@@ -142,7 +142,7 @@ extension MednafenGameCore: CoreOptional {
                                          defaultValue: false)
     }()
     
-    static var video_opengl: CoreOption = {
+    static var video_openglOption: CoreOption = {
         .bool(.init(
             title: "Use OpenGL",
             description: "Experimental OpenGL mode.",
@@ -197,7 +197,6 @@ extension MednafenGameCore: CoreOptional {
 @objc public extension MednafenGameCore {
     @objc(video_blit_timesync) var video_blit_timesync: Bool { MednafenGameCore.valueForOption(MednafenGameCore.video_blit_timesync).asBool }
     @objc(video_fs) var video_fs: Bool { MednafenGameCore.valueForOption(MednafenGameCore.video_fs).asBool }
-    @objc(video_opengl) var video_opengl: Bool { MednafenGameCore.valueForOption(MednafenGameCore.video_opengl).asBool }
     
     @objc(mednafen_pceFast) var mednafen_pceFast: Bool { MednafenGameCore.valueForOption(MednafenGameCore.pceFastOption).asBool }
     @objc(mednafen_snesFast) var mednafen_snesFast: Bool { MednafenGameCore.valueForOption(MednafenGameCore.snesFastOption).asBool }
@@ -228,4 +227,8 @@ extension MednafenGameCore: CoreOptional {
         let value = valueForOption(String.self, option)
         return value
     }
+    
+    func parseOptions() {
+        self.video_opengl = MednafenGameCore.valueForOption(MednafenGameCore.video_openglOption).asBool;
+    }
 }
diff --git a/Cores/Mupen64Plus/MupenGameCore+Audio.swift b/Cores/Mupen64Plus/MupenGameCore+Audio.swift
new file mode 100644
index 0000000000..031dc38f71
--- /dev/null
+++ b/Cores/Mupen64Plus/MupenGameCore+Audio.swift
@@ -0,0 +1,17 @@
+//
+//  MupenGameCore+Audio.swift
+//  PVMupen64Plus
+//
+//  Created by Joseph Mattiello on 1/24/22.
+//  Copyright © 2022 Provenance Emu. All rights reserved.
+//
+
+import Foundation
+import PVSupport
+
+@objc
+public extension MupenGameCore {
+    override var channelCount: UInt { 2 }
+    override var audioSampleRate: Double { self.mupenSampleRate }
+    override var frameInterval: TimeInterval { isNTSC ? 60 : 50 }
+}
diff --git a/Cores/Mupen64Plus/MupenGameCore+Cheats.h b/Cores/Mupen64Plus/MupenGameCore+Cheats.h
new file mode 100644
index 0000000000..efd7ef8248
--- /dev/null
+++ b/Cores/Mupen64Plus/MupenGameCore+Cheats.h
@@ -0,0 +1,17 @@
+//
+//  MupenGameCore+Controls.h
+//  MupenGameCore
+//
+//  Created by Joseph Mattiello on 1/26/22.
+//  Copyright © 2022 Provenance. All rights reserved.
+//
+
+#import <PVMupen64Plus/MupenGameCore.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MupenGameCore (Cheats)
+- (void)setCheat:(NSString *)code setType:(NSString *)type setEnabled:(BOOL)enabled;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Cores/Mupen64Plus/MupenGameCore+Cheats.m b/Cores/Mupen64Plus/MupenGameCore+Cheats.m
new file mode 100644
index 0000000000..28cf178383
--- /dev/null
+++ b/Cores/Mupen64Plus/MupenGameCore+Cheats.m
@@ -0,0 +1,67 @@
+
+//#import "MupenGameCore.h"
+#import <PVMupen64Plus/PVMupen64Plus-Swift.h>
+
+#import "api/config.h"
+#import "api/m64p_common.h"
+#import "api/m64p_config.h"
+#import "api/m64p_frontend.h"
+#import "api/m64p_vidext.h"
+#import "api/callbacks.h"
+#import "osal/dynamiclib.h"
+#import "Plugins/Core/src/main/version.h"
+#import "Plugins/Core/src/plugin/plugin.h"
+
+@implementation MupenGameCore (Cheats)
+
+- (void)setCheat:(NSString *)code setType:(NSString *)type setEnabled:(BOOL)enabled {
+// Need to fix ambigious main.h inclusion
+//    // Sanitize
+//    code = [code stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+//
+//    // Remove any spaces
+//    code = [code stringByReplacingOccurrencesOfString:@" " withString:@""];
+//
+//    NSString *singleCode;
+//    NSArray *multipleCodes = [code componentsSeparatedByString:@"+"];
+//    m64p_cheat_code *gsCode = (m64p_cheat_code*) calloc([multipleCodes count], sizeof(m64p_cheat_code));
+//    int codeCounter = 0;
+//
+//    for (singleCode in multipleCodes)
+//    {
+//        if ([singleCode length] == 12) // GameShark
+//        {
+//            // GameShark N64 format: XXXXXXXX YYYY
+//            NSString *address = [singleCode substringWithRange:NSMakeRange(0, 8)];
+//            NSString *value = [singleCode substringWithRange:NSMakeRange(8, 4)];
+//
+//            // Convert GS hex to int
+//            unsigned int outAddress, outValue;
+//            NSScanner* scanAddress = [NSScanner scannerWithString:address];
+//            NSScanner* scanValue = [NSScanner scannerWithString:value];
+//            [scanAddress scanHexInt:&outAddress];
+//            [scanValue scanHexInt:&outValue];
+//
+//            gsCode[codeCounter].address = outAddress;
+//            gsCode[codeCounter].value = outValue;
+//            codeCounter++;
+//        }
+//    }
+//
+//    // Update address directly if code needs GS button pressed
+//    if ((gsCode[0].address & 0xFF000000) == 0x88000000 || (gsCode[0].address & 0xFF000000) == 0xA8000000)
+//    {
+//        *(unsigned char *)((g_rdram + ((gsCode[0].address & 0xFFFFFF)^S8))) = (unsigned char)gsCode[0].value; // Update 8-bit address
+//    }
+//    else if ((gsCode[0].address & 0xFF000000) == 0x89000000 || (gsCode[0].address & 0xFF000000) == 0xA9000000)
+//    {
+//        *(unsigned short *)((g_rdram + ((gsCode[0].address & 0xFFFFFF)^S16))) = (unsigned short)gsCode[0].value; // Update 16-bit address
+//    }
+//    // Else add code as normal
+//    else
+//    {
+//        enabled ? CoreAddCheat([code UTF8String], gsCode, codeCounter+1) : CoreCheatEnabled([code UTF8String], 0);
+//    }
+}
+
+@end
diff --git a/Cores/Mupen64Plus/MupenGameCore+Controls.h b/Cores/Mupen64Plus/MupenGameCore+Controls.h
new file mode 100644
index 0000000000..159d04cdc0
--- /dev/null
+++ b/Cores/Mupen64Plus/MupenGameCore+Controls.h
@@ -0,0 +1,34 @@
+//
+//  MupenGameCore+Controls.h
+//  MupenGameCore
+//
+//  Created by Joseph Mattiello on 1/26/22.
+//  Copyright © 2022 Provenance. All rights reserved.
+//
+
+#import <PVMupen64Plus/MupenGameCore.h>
+#import "Plugins/Core/src/plugin/plugin.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+void MupenInitiateControllers (CONTROL_INFO ControlInfo);
+void MupenGetKeys(int Control, BUTTONS *Keys);
+void MupenControllerCommand(int Control, unsigned char *Command);
+
+@interface MupenGameCore (Controls) <PVN64SystemResponderClient>
+
+- (void)initControllBuffers;
+- (void)pollControllers;
+
+#pragma mark - Control
+
+- (void)didPushN64Button:(enum PVN64Button)button forPlayer:(NSInteger)player;
+- (void)didReleaseN64Button:(enum PVN64Button)button forPlayer:(NSInteger)player;
+- (void)didMoveN64JoystickDirection:(enum PVN64Button)button withValue:(CGFloat)value forPlayer:(NSInteger)player;
+- (void)didMoveJoystick:(NSInteger)button withValue:(CGFloat)value forPlayer:(NSInteger)player;
+
+- (void)didPush:(NSInteger)button forPlayer:(NSInteger)player;
+- (void)didRelease:(NSInteger)button forPlayer:(NSInteger)player;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Cores/Mupen64Plus/MupenGameCore+Controls.m b/Cores/Mupen64Plus/MupenGameCore+Controls.m
new file mode 100644
index 0000000000..7dbddc28b4
--- /dev/null
+++ b/Cores/Mupen64Plus/MupenGameCore+Controls.m
@@ -0,0 +1,382 @@
+
+#import "MupenGameCore+Controls.h"
+#import <PVMupen64Plus/PVMupen64Plus-Swift.h>
+
+#import "api/config.h"
+#import "api/m64p_common.h"
+#import "api/m64p_config.h"
+#import "api/m64p_frontend.h"
+#import "api/m64p_vidext.h"
+#import "api/callbacks.h"
+#import "osal/dynamiclib.h"
+#import "Plugins/Core/src/main/version.h"
+#import "Plugins/Core/src/plugin/plugin.h"
+//#import "rom.h"
+//#import "savestates.h"
+//#import "memory.h"
+//#import "mupen64plus-core/src/main/main.h"
+@import Dispatch;
+@import PVSupport;
+#if TARGET_OS_MACCATALYST
+@import OpenGL.GL3;
+@import GLUT;
+#else
+@import OpenGLES.ES3;
+@import GLKit;
+#endif
+
+#import <dlfcn.h>
+
+void MupenGetKeys(int Control, BUTTONS *Keys) {
+    GET_CURRENT_AND_RETURN();
+    
+    if (Control == 0) {
+        [current pollControllers];
+    }
+
+    Keys->U_DPAD = current->padData[Control][PVN64ButtonDPadUp];
+    Keys->D_DPAD = current->padData[Control][PVN64ButtonDPadDown];
+    Keys->L_DPAD = current->padData[Control][PVN64ButtonDPadLeft];
+    Keys->R_DPAD = current->padData[Control][PVN64ButtonDPadRight];
+    
+    Keys->START_BUTTON = current->padData[Control][PVN64ButtonStart];
+    
+    Keys->Z_TRIG = current->padData[Control][PVN64ButtonZ];
+    
+    Keys->B_BUTTON = current->padData[Control][PVN64ButtonB];
+    Keys->A_BUTTON = current->padData[Control][PVN64ButtonA];
+    
+    Keys->L_CBUTTON = current->padData[Control][PVN64ButtonCLeft];
+    Keys->R_CBUTTON = current->padData[Control][PVN64ButtonCRight];
+    Keys->D_CBUTTON = current->padData[Control][PVN64ButtonCDown];
+    Keys->U_CBUTTON = current->padData[Control][PVN64ButtonCUp];
+  
+    Keys->L_TRIG = current->padData[Control][PVN64ButtonL];
+    Keys->R_TRIG = current->padData[Control][PVN64ButtonR];
+
+    Keys->X_AXIS = current->xAxis[Control];
+    Keys->Y_AXIS = current->yAxis[Control];
+}
+
+void MupenInitiateControllers (CONTROL_INFO ControlInfo) {
+    GET_CURRENT_OR_RETURN();
+
+    bool p2Present = current.controller2 != nil || current.dualJoystick;
+
+    ControlInfo.Controls[0].Present = 1;
+    ControlInfo.Controls[0].Plugin = current->controllerMode[0];
+    ControlInfo.Controls[1].Present = p2Present;
+    ControlInfo.Controls[1].Plugin = current->controllerMode[1];
+    ControlInfo.Controls[2].Present = current.controller3 != nil;
+    ControlInfo.Controls[2].Plugin = current->controllerMode[2];
+    ControlInfo.Controls[3].Present = current.controller4 != nil || (current.controller3 != nil && current.dualJoystick);
+    ControlInfo.Controls[3].Plugin = current->controllerMode[3];
+}
+
+/******************************************************************
+ Function: ControllerCommand
+ Purpose:  To process the raw data that has just been sent to a
+ specific controller.
+ input:    - Controller Number (0 to 3) and -1 signalling end of
+ processing the pif ram.
+ - Pointer of data to be processed.
+ output:   none
+ note:     This function is only needed if the DLL is allowing raw
+ data, or the plugin is set to raw
+ the data that is being processed looks like this:
+ initilize controller: 01 03 00 FF FF FF
+ read controller:      01 04 01 FF FF FF FF
+ *******************************************************************/
+void MupenControllerCommand(int Control, unsigned char *Command) {
+        // Some stuff from n-rage plugin
+#define RD_GETSTATUS        0x00        // get status
+#define RD_READKEYS         0x01        // read button values
+#define RD_READPAK          0x02        // read from controllerpack
+#define RD_WRITEPAK         0x03        // write to controllerpack
+#define RD_RESETCONTROLLER  0xff        // reset controller
+#define RD_READEEPROM       0x04        // read eeprom
+#define RD_WRITEEPROM       0x05        // write eeprom
+
+#define PAK_IO_RUMBLE       0xC000      // the address where rumble-commands are sent to
+
+    GET_CURRENT_OR_RETURN();
+
+    unsigned char *Data = &Command[5];
+
+    if (Control == -1)
+    return;
+
+    switch (Command[2])
+    {
+        case RD_GETSTATUS:
+        break;
+        case RD_READKEYS:
+        break;
+        case RD_READPAK: {
+//        if (controller[Control].control->Plugin == PLUGIN_RAW)
+//        {
+            unsigned int dwAddress = (Command[3] << 8) + (Command[4] & 0xE0);
+
+            if(( dwAddress >= 0x8000 ) && ( dwAddress < 0x9000 ) )
+            memset( Data, 0x80, 32 );
+            else
+            memset( Data, 0x00, 32 );
+
+            Data[32] = DataCRC( Data, 32 );
+//        }
+        break;
+        }
+        case RD_WRITEPAK: {
+//        if (controller[Control].control->Plugin == PLUGIN_RAW)
+//        {
+            unsigned int dwAddress = (Command[3] << 8) + (Command[4] & 0xE0);
+//            Data[32] = DataCRC( Data, 32 );
+//
+            if ((dwAddress == PAK_IO_RUMBLE) )//&& (rumble.set_rumble_state))
+            {
+                if (*Data)
+                {
+                    [current rumble];
+//                    AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
+//                    rumble.set_rumble_state(Control, RETRO_RUMBLE_WEAK, 0xFFFF);
+//                    rumble.set_rumble_state(Control, RETRO_RUMBLE_STRONG, 0xFFFF);
+                }
+                else
+                {
+//                    rumble.set_rumble_state(Control, RETRO_RUMBLE_WEAK, 0);
+//                    rumble.set_rumble_state(Control, RETRO_RUMBLE_STRONG, 0);
+                }
+            }
+//        }
+
+        break;
+        }
+        case RD_RESETCONTROLLER:
+        break;
+        case RD_READEEPROM:
+        break;
+        case RD_WRITEEPROM:
+        break;
+    }
+}
+
+
+//NSString *MupenControlNames[] = {
+//    @"N64_DPadU", @"N64_DPadD", @"N64_DPadL", @"N64_DPadR",
+//    @"N64_CU", @"N64_CD", @"N64_CL", @"N64_CR",
+//    @"N64_B", @"N64_A", @"N64_R", @"N64_L", @"N64_Z", @"N64_Start"
+//}; // FIXME: missing: joypad X, joypad Y, mempak switch, rumble switch
+
+#define N64_ANALOG_MAX 80
+
+@implementation MupenGameCore (Controls)
+
+-(void)setMode:(NSInteger)mode forController:(NSInteger)controller {
+    NSAssert(controller < 4, @"Out of index");
+    self->controllerMode[controller] = mode;
+}
+
+- (void)pollController:(GCController* _Nullable)controller forIndex:(NSInteger)playerIndex {
+    if (!controller) {
+        return;
+    }
+    if (LIKELY(controller.extendedGamepad)) {
+        GCExtendedGamepad *gamepad     = [controller extendedGamepad];
+        GCDualSenseGamepad *dualSense = [gamepad isKindOfClass:[GCDualSenseGamepad class]] ? gamepad : nil;
+
+        GCControllerDirectionPad *dpad = [gamepad dpad];
+        
+        BOOL dualModeOverrides = self.dualJoystick && (playerIndex == 0 || playerIndex == 2);
+
+        // Left Joystick → Joystick
+        xAxis[playerIndex] = gamepad.leftThumbstick.xAxis.value * N64_ANALOG_MAX;
+        yAxis[playerIndex] = gamepad.leftThumbstick.yAxis.value * N64_ANALOG_MAX;
+
+        // MFi-D-Pad → D-Pad
+        padData[playerIndex][PVN64ButtonDPadUp] = dpad.up.isPressed;
+        padData[playerIndex][PVN64ButtonDPadDown] = dpad.down.isPressed;
+        padData[playerIndex][PVN64ButtonDPadLeft] = dpad.left.isPressed;
+        padData[playerIndex][PVN64ButtonDPadRight] = dpad.right.isPressed;
+
+        if(dualModeOverrides) {
+            // MFi-R2 → P2.Z
+            padData[playerIndex+1][PVN64ButtonZ] = gamepad.rightTrigger.isPressed;
+            
+            // MFi-L2 → P1.Z
+            padData[playerIndex][PVN64ButtonZ] = gamepad.leftTrigger.isPressed;
+
+            if (dualSense) {
+                padData[playerIndex][PVN64ButtonStart] = dualSense.touchpadButton.touched;
+            }
+        } else {
+            if (dualSense) {
+                // DualShock-TouchPad → Start
+                padData[playerIndex][PVN64ButtonStart] = dualSense.touchpadButton.touched;
+                // DualShock-Either Trigger → Z
+                padData[playerIndex][PVN64ButtonZ] = gamepad.leftTrigger.isPressed || gamepad.rightTrigger.isPressed;
+            } else {
+                // MFi-L2 → Start
+                padData[playerIndex][PVN64ButtonStart] = gamepad.leftTrigger.isPressed;
+
+                // MFi-R2 → Z
+                padData[playerIndex][PVN64ButtonZ] = gamepad.rightTrigger.isPressed;
+            }
+        }
+        
+        // If MFi-L2 is not pressed… MFi-L1 → L
+        if (!gamepad.rightShoulder.isPressed) {
+            padData[playerIndex][PVN64ButtonL] = gamepad.leftShoulder.isPressed;
+        }
+        
+        // If MFi-L1 is not pressed… MFi-R1 → R
+        if (!gamepad.leftShoulder.isPressed) {
+            padData[playerIndex][PVN64ButtonR] = gamepad.rightShoulder.isPressed;
+        }
+        // If not C-Mode… MFi-X,A → A,B MFi-Y,B → C←,C↓
+        if (!(gamepad.leftShoulder.isPressed && gamepad.rightShoulder.isPressed)) {
+            padData[playerIndex][PVN64ButtonA] = gamepad.buttonA.isPressed;
+            padData[playerIndex][PVN64ButtonB] = gamepad.buttonX.isPressed;
+            padData[playerIndex][PVN64ButtonCLeft] = gamepad.buttonY.isPressed;
+            padData[playerIndex][PVN64ButtonCDown] = gamepad.buttonB.isPressed;
+        }
+        
+        //C-Mode: MFi-X,Y,A,B -> C←,C↑,C↓,C→
+        if (gamepad.leftShoulder.isPressed && gamepad.rightShoulder.isPressed) {
+            padData[playerIndex][PVN64ButtonCLeft] = gamepad.buttonX.isPressed;
+            padData[playerIndex][PVN64ButtonCUp] = gamepad.buttonY.isPressed;
+            padData[playerIndex][PVN64ButtonCDown] = gamepad.buttonA.isPressed;
+            padData[playerIndex][PVN64ButtonCRight] = gamepad.buttonB.isPressed;
+        }
+
+        // Right Joystick → C Buttons
+        if(dualModeOverrides) {
+            xAxis[playerIndex+1] = gamepad.rightThumbstick.xAxis.value * N64_ANALOG_MAX;
+            yAxis[playerIndex+1] = gamepad.rightThumbstick.yAxis.value * N64_ANALOG_MAX;
+        } else {
+            float rightJoystickDeadZone = 0.45;
+            if (!(gamepad.leftShoulder.isPressed && gamepad.rightShoulder.isPressed) && !(gamepad.buttonY.isPressed || gamepad.buttonB.isPressed)) {
+                padData[playerIndex][PVN64ButtonCUp] = gamepad.rightThumbstick.up.value > rightJoystickDeadZone;
+                padData[playerIndex][PVN64ButtonCDown] = gamepad.rightThumbstick.down.value > rightJoystickDeadZone;
+                padData[playerIndex][PVN64ButtonCLeft] = gamepad.rightThumbstick.left.value > rightJoystickDeadZone;
+                padData[playerIndex][PVN64ButtonCRight] = gamepad.rightThumbstick.right.value > rightJoystickDeadZone;
+            }
+        }
+    } else if ([controller gamepad]) {
+        GCGamepad *gamepad = [controller gamepad];
+        GCControllerDirectionPad *dpad = [gamepad dpad];
+        
+        if (!gamepad.rightShoulder.isPressed) {
+            // Default
+            xAxis[playerIndex] = (dpad.left.value > 0.5 ? -N64_ANALOG_MAX : 0) + (dpad.right.value > 0.5 ? N64_ANALOG_MAX : 0);
+            yAxis[playerIndex] = (dpad.down.value > 0.5 ? -N64_ANALOG_MAX : 0) + (dpad.up.value > 0.5 ? N64_ANALOG_MAX : 0);
+            
+            padData[playerIndex][PVN64ButtonA] = gamepad.buttonA.isPressed;
+            padData[playerIndex][PVN64ButtonB] = gamepad.buttonX.isPressed;
+            
+            padData[playerIndex][PVN64ButtonCLeft] = gamepad.buttonY.isPressed;
+            padData[playerIndex][PVN64ButtonCDown] = gamepad.buttonB.isPressed;
+        } else {
+            // Alt-Mode
+            padData[playerIndex][PVN64ButtonDPadUp] = dpad.up.isPressed;
+            padData[playerIndex][PVN64ButtonDPadDown] = dpad.down.isPressed;
+            padData[playerIndex][PVN64ButtonDPadLeft] = dpad.left.isPressed;
+            padData[playerIndex][PVN64ButtonDPadRight] = dpad.right.isPressed;
+            
+            padData[playerIndex][PVN64ButtonCLeft] = gamepad.buttonX.isPressed;
+            padData[playerIndex][PVN64ButtonCUp] = gamepad.buttonY.isPressed;
+            padData[playerIndex][PVN64ButtonCDown] = gamepad.buttonA.isPressed;
+            padData[playerIndex][PVN64ButtonCRight] = gamepad.buttonB.isPressed;
+        }
+        
+        padData[playerIndex][PVN64ButtonZ] = gamepad.leftShoulder.isPressed;
+        padData[playerIndex][PVN64ButtonR] = gamepad.rightShoulder.isPressed;
+        
+    }
+#if TARGET_OS_TV
+    else if ([controller microGamepad]) {
+        GCMicroGamepad *gamepad = [controller microGamepad];
+        GCControllerDirectionPad *dpad = [gamepad dpad];
+        
+        xAxis[playerIndex] = (dpad.left.value > 0.5 ? -N64_ANALOG_MAX : 0) + (dpad.right.value > 0.5 ? N64_ANALOG_MAX : 0);
+        yAxis[playerIndex] = (dpad.down.value > 0.5 ? -N64_ANALOG_MAX : 0) + (dpad.up.value > 0.5 ? N64_ANALOG_MAX : 0);
+        
+        padData[playerIndex][PVN64ButtonB] = gamepad.buttonA.isPressed;
+        padData[playerIndex][PVN64ButtonA] = gamepad.buttonX.isPressed;
+    }
+#endif
+}
+
+- (void)pollControllers {
+#define USE_CAPTURE 1
+#define USE_QUEUE 1
+
+#if USE_CAPTURE
+#define controllerForNum(num) [self.controller##num capture]
+#else
+#define controllerForNum(num) self.controller##num
+#endif
+
+#if USE_QUEUE
+        //    const NSOperationQueue *queue = [NSOperationQueue currentQueue];
+            [_inputQueue cancelAllOperations];
+            NSMutableArray<NSBlockOperation*>* ops = [NSMutableArray arrayWithCapacity:4];
+        #define CHECK_CONTROLLER(num) \
+            if(self.controller##num) [ops addObject:[NSBlockOperation blockOperationWithBlock:^{[self pollController:controllerForNum(num) forIndex:(num - 1)];}]]
+
+            CHECK_CONTROLLER(1);
+            CHECK_CONTROLLER(2);
+            CHECK_CONTROLLER(3);
+            CHECK_CONTROLLER(4);
+
+            [_inputQueue addOperations:ops waitUntilFinished:NO];
+#else
+#define CHECK_CONTROLLER(num) if(self.controller##num) [self pollController:controllerForNum(num) forIndex:(num - 1)]
+
+    CHECK_CONTROLLER(1);
+    CHECK_CONTROLLER(2);
+    CHECK_CONTROLLER(3);
+    CHECK_CONTROLLER(4);
+#endif
+#undef CHECK_CONTROLLER
+#undef controllerForNum
+}
+
+- (void)didMoveN64JoystickDirection:(PVN64Button)button withValue:(CGFloat)value forPlayer:(NSUInteger)player {
+    if (self.dualJoystickOption && player == 0) {
+        player = 1;
+    }
+    switch (button) {
+        case PVN64ButtonAnalogUp:
+            yAxis[player] = value * N64_ANALOG_MAX;
+            break;
+        case PVN64ButtonAnalogDown:
+            yAxis[player] = value * -N64_ANALOG_MAX;
+            break;
+        case PVN64ButtonAnalogLeft:
+            xAxis[player] = value * -N64_ANALOG_MAX;
+            break;
+        case PVN64ButtonAnalogRight:
+            xAxis[player] = value * N64_ANALOG_MAX;
+            break;
+        default:
+            break;
+    }
+}
+
+- (void)didPushN64Button:(PVN64Button)button forPlayer:(NSUInteger)player {
+    padData[player][button] = 1;
+}
+
+- (void)didReleaseN64Button:(PVN64Button)button forPlayer:(NSUInteger)player {
+    padData[player][button] = 0;
+}
+
+@end
+
+@implementation MupenGameCore (Rumble)
+
+- (BOOL)supportsRumble {
+    return YES;
+}
+
+@end
diff --git a/Cores/Mupen64Plus/MupenGameCore+Mupen.h b/Cores/Mupen64Plus/MupenGameCore+Mupen.h
new file mode 100644
index 0000000000..4767b57004
--- /dev/null
+++ b/Cores/Mupen64Plus/MupenGameCore+Mupen.h
@@ -0,0 +1,54 @@
+//
+//  MupenGameCore+Mupen.h
+//  MupenGameCore
+//
+//  Created by Joseph Mattiello on 1/26/22.
+//  Copyright © 2022 Provenance. All rights reserved.
+//
+
+#import <PVMupen64Plus/MupenGameCore.h>
+@import Foundation;
+
+#import "api/config.h"
+#import "api/m64p_common.h"
+#import "api/m64p_config.h"
+#import "api/m64p_frontend.h"
+#import "api/m64p_vidext.h"
+#import "api/callbacks.h"
+#import "osal/dynamiclib.h"
+#import "Plugins/Core/src/main/version.h"
+#import "Plugins/Core/src/plugin/plugin.h"
+
+#define WIDTH 640
+#define HEIGHT 480
+#define WIDTHf 640.0f
+#define HEIGHTf 480.0f
+
+
+#if TARGET_OS_TV
+#define RESIZE_TO_FULLSCREEN TRUE
+#else
+#define RESIZE_TO_FULLSCREEN [PVSettingsModel.shared nativeScaleEnabled]
+#endif
+
+#import <dlfcn.h>
+#define N64_ANALOG_MAX 80
+
+NS_ASSUME_NONNULL_BEGIN
+
+void MupenAudioSampleRateChanged(int SystemType);
+void MupenAudioLenChanged();
+void SetIsNTSC();
+int MupenOpenAudio(AUDIO_INFO info);
+void MupenSetAudioSpeed(int percent);
+void ConfigureAll(NSString *romFolder);
+void ConfigureCore(NSString *romFolder);
+void ConfigureVideoGeneral();
+void ConfigureGLideN64(NSString *romFolder);
+void ConfigureRICE();
+
+@interface MupenGameCore (Mupen)
+- (void)OE_addHandlerForType:(m64p_core_param)paramType usingBlock:(BOOL(^)(m64p_core_param paramType, int newValue))block;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Cores/Mupen64Plus/MupenGameCore+Mupen.m b/Cores/Mupen64Plus/MupenGameCore+Mupen.m
new file mode 100644
index 0000000000..d69e9848bd
--- /dev/null
+++ b/Cores/Mupen64Plus/MupenGameCore+Mupen.m
@@ -0,0 +1,355 @@
+#import "MupenGameCore+Mupen.h"
+#import "MupenGameCore+Controls.h"
+#import <PVMupen64Plus/PVMupen64Plus-Swift.h>
+
+@import UIKit.UIWindow;
+
+#import "api/config.h"
+#import "api/m64p_common.h"
+#import "api/m64p_config.h"
+#import "api/m64p_frontend.h"
+#import "api/m64p_vidext.h"
+#import "api/callbacks.h"
+#import "osal/dynamiclib.h"
+#import "Plugins/Core/src/main/version.h"
+#import "Plugins/Core/src/plugin/plugin.h"
+
+AUDIO_INFO AudioInfo;
+
+unsigned char DataCRC( unsigned char *Data, int iLenght )
+{
+    unsigned char Remainder = Data[0];
+
+    int iByte = 1;
+    unsigned char bBit = 0;
+
+    while( iByte <= iLenght ) {
+        int HighBit = ((Remainder & 0x80) != 0);
+        Remainder = Remainder << 1;
+
+        Remainder += ( iByte < iLenght && Data[iByte] & (0x80 >> bBit )) ? 1 : 0;
+
+        Remainder ^= (HighBit) ? 0x85 : 0;
+
+        bBit++;
+        iByte += bBit/8;
+        bBit %= 8;
+    }
+
+    return Remainder;
+}
+
+void MupenAudioSampleRateChanged(int SystemType)
+{
+    GET_CURRENT_AND_RETURN();
+
+    float currentRate = current.mupenSampleRate;
+    
+    switch (SystemType)
+    {
+        default:
+        case SYSTEM_NTSC:
+            current.mupenSampleRate = 48681812 / (*AudioInfo.AI_DACRATE_REG + 1);
+            break;
+        case SYSTEM_PAL:
+            current.mupenSampleRate = 49656530 / (*AudioInfo.AI_DACRATE_REG + 1);
+            break;
+    }
+
+    [[current audioDelegate] audioSampleRateDidChange];
+    ILOG(@"Mupen rate changed %f -> %f\n", currentRate, current.mupenSampleRate);
+}
+
+void MupenAudioLenChanged()
+{
+    GET_CURRENT_AND_RETURN();
+
+    const int LenReg = *AudioInfo.AI_LEN_REG;
+    uint8_t *ptr = (uint8_t*)(AudioInfo.RDRAM + (*AudioInfo.AI_DRAM_ADDR_REG & 0xFFFFFF));
+    
+    // Swap channels
+    for (uint32_t i = 0; i < LenReg; i += 4)
+    {
+        ptr[i] ^= ptr[i + 2];
+        ptr[i + 2] ^= ptr[i];
+        ptr[i] ^= ptr[i + 2];
+        ptr[i + 1] ^= ptr[i + 3];
+        ptr[i + 3] ^= ptr[i + 1];
+        ptr[i + 1] ^= ptr[i + 3];
+    }
+    
+    [[current ringBufferAtIndex:0] write:ptr maxLength:LenReg];
+}
+
+void SetIsNTSC()
+{
+    GET_CURRENT_AND_RETURN();
+
+    extern m64p_rom_header ROM_HEADER;
+    switch (ROM_HEADER.Country_code&0xFF)
+    {
+        case 0x44:
+        case 0x46:
+        case 0x49:
+        case 0x50:
+        case 0x53:
+        case 0x55:
+        case 0x58:
+        case 0x59:
+            current.isNTSC = NO;
+            break;
+        case 0x37:
+        case 0x41:
+        case 0x45:
+        case 0x4a:
+            current.isNTSC = YES;
+            break;
+    }
+}
+
+int MupenOpenAudio(AUDIO_INFO info)
+{
+    AudioInfo = info;
+    
+    SetIsNTSC();
+    
+    return M64ERR_SUCCESS;
+}
+
+void MupenSetAudioSpeed(int percent) {
+    // do we need this?
+}
+
+void ConfigureAll(NSString *romFolder) {
+    ConfigureCore(romFolder);
+    ConfigureVideoGeneral();
+    ConfigureGLideN64(romFolder);
+    ConfigureRICE();
+}
+
+void ConfigureCore(NSString *romFolder) {
+    GET_CURRENT_AND_RETURN();
+
+    // TODO: Proper path
+    NSBundle *coreBundle = [NSBundle mainBundle];
+    const char *dataPath;
+    dataPath = [[coreBundle resourcePath] fileSystemRepresentation];
+
+    /** Core Config **/
+    m64p_handle config;
+    ConfigOpenSection("Core", &config);
+
+    // set SRAM path
+    ConfigSetParameter(config, "SaveSRAMPath", M64TYPE_STRING, current.batterySavesPath.fileSystemRepresentation);
+    // set data path
+    ConfigSetParameter(config, "SharedDataPath", M64TYPE_STRING, romFolder.fileSystemRepresentation);
+
+    // Use Pure Interpreter if 0, Cached Interpreter if 1, or Dynamic Recompiler if 2 or more"
+    int emulator = [MupenGameCore intForOption:@"CPU Mode"];
+    printf("emulator %i", emulator);
+    ConfigSetParameter(config, "R4300Emulator", M64TYPE_INT, &emulator);
+
+    ConfigSaveSection("Core");
+    /** End Core Config **/
+}
+
+void ConfigureVideoGeneral() {
+    /** Begin General Video Config **/
+    m64p_handle general;
+    ConfigOpenSection("Video-General", &general);
+
+    // Use fullscreen mode
+    int useFullscreen = 1;
+    ConfigSetParameter(general, "Fullscreen", M64TYPE_BOOL, &useFullscreen);
+
+    int screenWidth = WIDTH;
+    int screenHeight = HEIGHT;
+    if(RESIZE_TO_FULLSCREEN) {
+        CGSize size = UIApplication.sharedApplication.keyWindow.bounds.size;
+        float widthScale = floor(size.height / WIDTHf);
+        float heightScale = floor(size.height / HEIGHTf);
+        float scale = MAX(MIN(widthScale, heightScale), 1);
+        screenWidth =  scale * WIDTHf;
+        screenHeight = scale * HEIGHTf;
+    }
+
+    // Screen width
+    ConfigSetParameter(general, "ScreenWidth", M64TYPE_INT, &screenWidth);
+
+    // Screen height
+    ConfigSetParameter(general, "ScreenHeight", M64TYPE_INT, &screenHeight);
+
+
+    DLOG(@"Setting size to (%i,%i)", screenWidth, screenHeight);
+
+    ConfigSaveSection("Video-General");
+    /** End General Video Config **/
+}
+
+void ConfigureGLideN64(NSString *romFolder) {
+    /** Begin GLideN64 Config **/
+    m64p_handle gliden64;
+    ConfigOpenSection("Video-GLideN64", &gliden64);
+
+        // 0 = stretch, 1 = 4:3, 2 = 16:9, 3 = adjust
+    int aspectRatio = [MupenGameCore intForOption:@"Aspect Ratio"];
+
+//    if(RESIZE_TO_FULLSCREEN) {
+//        #if TARGET_OS_TV
+//            aspectRatio = 1;
+//        #else
+//            aspectRatio = 3;
+//        #endif
+//    }
+
+    ConfigSetParameter(gliden64, "AspectRatio", M64TYPE_INT, &aspectRatio);
+
+    // Per-pixel lighting
+    int enableHWLighting = MupenGameCore.perPixelLighting ? 1 : 0;
+    ConfigSetParameter(gliden64, "EnableHWLighting", M64TYPE_BOOL, &enableHWLighting);
+
+    // HiRez & texture options
+    //  txHiresEnable, "Use high-resolution texture packs if available."
+    int txHiresEnable = [MupenGameCore boolForOption:@"Enable HiRes Texture packs"];
+    ConfigSetParameter(gliden64, "txHiresEnable", M64TYPE_BOOL, &txHiresEnable);
+
+    // Path to folder with hi-res texture packs.
+    ConfigSetParameter(gliden64, "txPath", M64TYPE_STRING, [romFolder stringByAppendingPathComponent:@"/"].fileSystemRepresentation);
+    // Path to folder where plugin saves texture cache files.
+    ConfigSetParameter(gliden64, "txCachePath", M64TYPE_STRING, [romFolder stringByAppendingPathComponent:@"/cache/"].fileSystemRepresentation);
+    // Path to folder where plugin saves dumped textures.
+    ConfigSetParameter(gliden64, "txDumpPath", M64TYPE_STRING, [romFolder stringByAppendingPathComponent:@"/texture_dump/"].fileSystemRepresentation);
+
+//    if(RESIZE_TO_FULLSCREEN) {
+    // "txFilterMode",
+    // "Texture filter (0=none, 1=Smooth filtering 1, 2=Smooth filtering 2, 3=Smooth filtering 3, 4=Smooth filtering 4, 5=Sharp filtering 1, 6=Sharp filtering 2)"
+    int txFilterMode = [MupenGameCore intForOption:@"Texture Filter Mode"];
+    ConfigSetParameter(gliden64, "txFilterMode", M64TYPE_INT, &txFilterMode);
+
+    // "txEnhancementMode", config.textureFilter.txEnhancementMode,
+    // "Texture Enhancement (0=none, 1=store as is, 2=X2, 3=X2SAI, 4=HQ2X, 5=HQ2XS, 6=LQ2X, 7=LQ2XS, 8=HQ4X, 9=2xBRZ, 10=3xBRZ, 11=4xBRZ, 12=5xBRZ), 13=6xBRZ"
+    int txEnhancementMode = [MupenGameCore intForOption:@"Texture Enhancement Mode"];
+    ConfigSetParameter(gliden64, "txEnhancementMode", M64TYPE_INT, &txEnhancementMode);
+
+    // "txCacheCompression", config.textureFilter.txCacheCompression, "Zip textures cache."
+    int txCacheCompression = [MupenGameCore boolForOption:@"Compress texture cache"];
+    ConfigSetParameter(gliden64, "txCacheCompression", M64TYPE_BOOL, &txCacheCompression);
+
+    // "txSaveCache", config.textureFilter.txSaveCache,
+    // "Save texture cache to hard disk."
+    int txSaveCache = [MupenGameCore boolForOption:@"Save texture cache"];
+    ConfigSetParameter(gliden64, "txSaveCache", M64TYPE_BOOL, &txSaveCache);
+
+    // Warning, anything other than 0 crashes shader compilation
+    // "MultiSampling", config.video.multisampling, "Enable/Disable MultiSampling (0=off, 2,4,8,16=quality)"
+    int MultiSampling = [MupenGameCore intForOption:@"Multi Sampling"];
+    ConfigSetParameter(gliden64, "MultiSampling", M64TYPE_INT, &MultiSampling);
+//    }
+
+
+        //#Gamma correction settings
+//    res = ConfigSetDefaultBool(g_configVideoGliden64, "ForceGammaCorrection", config.gammaCorrection.force, "Force gamma correction.");
+//    assert(res == M64ERR_SUCCESS);
+//    res = ConfigSetDefaultFloat(g_configVideoGliden64, "GammaCorrectionLevel", config.gammaCorrection.level, "Gamma correction level.");
+//    assert(res == M64ERR_SUCCESS);
+
+    /*
+     "txCacheSize", config.textureFilter.txCacheSize/ gc_uMegabyte, "Size of filtered textures cache in megabytes."
+    */
+    int txDump = [MupenGameCore boolForOption:@"Texture Dump"];
+    ConfigSetParameter(gliden64, "txDump", M64TYPE_BOOL, &txDump);
+
+    int txFilterIgnoreBG = [MupenGameCore boolForOption:@"Ignore BG Textures"];
+    ConfigSetParameter(gliden64, "txFilterIgnoreBG", M64TYPE_BOOL, &txFilterIgnoreBG);
+
+
+    int txForce16bpp = [MupenGameCore boolForOption:@"Force 16bpp textures"];
+    ConfigSetParameter(gliden64, "txForce16bpp", M64TYPE_BOOL, &txForce16bpp);
+
+
+    // "txHresAltCRC", config.textureFilter.txHresAltCRC, "Use alternative method of paletted textures CRC calculation."
+    int txHresAltCRC = [MupenGameCore boolForOption:@"HiRes Alt CRC"];
+    ConfigSetParameter(gliden64, "txHresAltCRC", M64TYPE_BOOL, &txHresAltCRC);
+
+
+    // "txHiresFullAlphaChannel", "Allow to use alpha channel of high-res texture fully."
+    int txHiresFullAlphaChannel = [MupenGameCore boolForOption:@"HiRes Full Alpha"];;
+    ConfigSetParameter(gliden64, "txHiresFullAlphaChannel", M64TYPE_BOOL, &txHiresFullAlphaChannel);
+
+    // Draw on-screen display if True, otherwise don't draw OSD
+    int osd = [MupenGameCore boolForOption:@"Debug OSD"];
+    ConfigSetParameter(gliden64, "OnScreenDisplay", M64TYPE_BOOL, &osd);
+    ConfigSetParameter(gliden64, "ShowFPS", M64TYPE_BOOL, &osd);            // Show FPS counter.
+    ConfigSetParameter(gliden64, "ShowVIS", M64TYPE_BOOL, &osd);            // Show VI/S counter.
+    ConfigSetParameter(gliden64, "ShowPercent", M64TYPE_BOOL, &osd);        // Show percent counter.
+    ConfigSetParameter(gliden64, "ShowInternalResolution", M64TYPE_BOOL, &osd);    // Show internal resolution.
+    ConfigSetParameter(gliden64, "ShowRenderingResolution", M64TYPE_BOOL, &osd);    // Show rendering resolution.
+
+    ConfigSaveSection("Video-GLideN64");
+    /** End GLideN64 Config **/
+}
+
+void ConfigureRICE() {
+    /** RICE CONFIG **/
+    m64p_handle rice;
+    ConfigOpenSection("Video-Rice", &rice);
+
+    // Use a faster algorithm to speed up texture loading and CRC computation
+    int fastTextureLoading = [MupenGameCore boolForOption:@"Fast Texture Loading"];
+
+    ConfigSetParameter(rice, "FastTextureLoading", M64TYPE_BOOL, &fastTextureLoading);
+
+    // Enable this option to have better render-to-texture quality
+    int doubleSizeForSmallTextureBuffer = [MupenGameCore boolForOption:@"DoubleSizeForSmallTxtrBuf"];
+    ConfigSetParameter(rice, "DoubleSizeForSmallTxtrBuf", M64TYPE_BOOL, &doubleSizeForSmallTextureBuffer);
+
+    // N64 Texture Memory Full Emulation (may fix some games, may break others)
+    int fullTEMEmulation = [MupenGameCore boolForOption:@"FullTMEMEmulation"];
+    ConfigSetParameter(rice, "FullTMEMEmulation", M64TYPE_BOOL, &fullTEMEmulation);
+
+    // Use fullscreen mode if True, or windowed mode if False
+    int fullscreen = 1;
+    ConfigSetParameter(rice, "Fullscreen", M64TYPE_BOOL, &fullscreen);
+
+    // If this option is enabled, the plugin will skip every other frame
+    // Breaks some games in my testing -jm
+    int skipFrame = [MupenGameCore boolForOption:@"SkipFrame"];
+    ConfigSetParameter(rice, "SkipFrame", M64TYPE_BOOL, &skipFrame);
+
+    // Enable hi-resolution texture file loading
+    int hiResTextures = [MupenGameCore boolForOption:@"LoadHiResTextures"];
+    ConfigSetParameter(rice, "LoadHiResTextures", M64TYPE_BOOL, &hiResTextures);
+
+    // Use Mipmapping? 0=no, 1=nearest, 2=bilinear, 3=trilinear
+    int mipmapping = 0;
+    ConfigSetParameter(rice, "Mipmapping", M64TYPE_INT, &mipmapping);
+
+    // Enable/Disable Anisotropic Filtering for Mipmapping (0=no filtering, 2-16=quality).
+    // This is uneffective if Mipmapping is 0. If the given value is to high to be supported by your graphic card, the value will be the highest value your graphic card can support. Better result with Trilinear filtering
+    int anisotropicFiltering = 16;
+    ConfigSetParameter(rice, "AnisotropicFiltering", M64TYPE_INT, &anisotropicFiltering);
+
+    // Enable, Disable or Force fog generation (0=Disable, 1=Enable n64 choose, 2=Force Fog)
+    int fogMethod = 1;
+    ConfigSetParameter(rice, "FogMethod", M64TYPE_INT, &fogMethod);
+
+    // Color bit depth to use for textures (0=default, 1=32 bits, 2=16 bits)
+    // 16 bit breaks some games like GoldenEye
+    int textureQuality = 1;
+    ConfigSetParameter(rice, "TextureQuality", M64TYPE_INT, &textureQuality);
+
+    // Enable/Disable MultiSampling (0=off, 2,4,8,16=quality)
+    int multiSampling = RESIZE_TO_FULLSCREEN ? 4 : 0;
+    ConfigSetParameter(rice, "MultiSampling", M64TYPE_INT, &multiSampling);
+
+    // Color bit depth for rendering window (0=32 bits, 1=16 bits)
+    int colorQuality = 0;
+    ConfigSetParameter(rice, "ColorQuality", M64TYPE_INT, &colorQuality);
+
+    /** End RICE CONFIG **/
+    ConfigSaveSection("Video-Rice");
+}
+
+@implementation MupenGameCore (Mupen)
+
+@end
diff --git a/Cores/Mupen64Plus/MupenGameCore+Saves.h b/Cores/Mupen64Plus/MupenGameCore+Saves.h
new file mode 100644
index 0000000000..b6a7671311
--- /dev/null
+++ b/Cores/Mupen64Plus/MupenGameCore+Saves.h
@@ -0,0 +1,21 @@
+//
+//  MupenGameCore+Controls.h
+//  MupenGameCore
+//
+//  Created by Joseph Mattiello on 1/26/22.
+//  Copyright © 2022 Provenance. All rights reserved.
+//
+
+#import <PVMupen64Plus/MupenGameCore.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MupenGameCore (Saves)
+- (BOOL)saveStateToFileAtPath:(NSString *)fileName error:(NSError**)error;
+- (void)saveStateToFileAtPath:(NSString *)fileName completionHandler:(void (^)(BOOL, NSError *))block;
+- (BOOL)loadStateFromFileAtPath:(NSString *)fileName error:(NSError**)error;
+- (void)loadStateFromFileAtPath:(NSString *)fileName completionHandler:(void (^)(BOOL, NSError *))block;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Cores/Mupen64Plus/MupenGameCore+Saves.m b/Cores/Mupen64Plus/MupenGameCore+Saves.m
new file mode 100644
index 0000000000..4b94c37e5f
--- /dev/null
+++ b/Cores/Mupen64Plus/MupenGameCore+Saves.m
@@ -0,0 +1,146 @@
+
+//#import "MupenGameCore.h"
+#import <PVMupen64Plus/PVMupen64Plus-Swift.h>
+#import "MupenGameCore+Mupen.h"
+
+#import "api/config.h"
+#import "api/m64p_common.h"
+#import "api/m64p_config.h"
+#import "api/m64p_frontend.h"
+#import "api/m64p_vidext.h"
+#import "api/callbacks.h"
+#import "osal/dynamiclib.h"
+#import "Plugins/Core/src/main/version.h"
+#import "Plugins/Core/src/plugin/plugin.h"
+
+@implementation MupenGameCore (Saves)
+
+- (BOOL)saveStateToFileAtPath:(NSString *)fileName error:(NSError**)error   {
+    NSAssert(NO, @"Shouldn't be here since we overwrite the async call");
+}
+
+- (void)saveStateToFileAtPath:(NSString *)fileName completionHandler:(void (^)(BOOL, NSError *))block
+{
+    __block BOOL wasPaused = [self isEmulationPaused];
+    [self OE_addHandlerForType:M64CORE_STATE_SAVECOMPLETE usingBlock:
+     ^ BOOL (m64p_core_param paramType, int newValue)
+     {
+         [self setPauseEmulation:wasPaused];
+         NSAssert(paramType == M64CORE_STATE_SAVECOMPLETE, @"This block should only be called for save completion!");
+         if(newValue == 0)
+         {
+
+             if (block) {
+                 NSError *error = [NSError errorWithDomain:@"org.openemu.GameCore.ErrorDomain"
+                                                      code:-5
+                                                  userInfo:@{
+                                                             NSLocalizedDescriptionKey : @"Mupen Could not save the current state.",
+                                                             NSFilePathErrorKey : fileName
+                                                             }];
+
+                 dispatch_async(dispatch_get_main_queue(), ^{
+                     block(YES, nil);
+                 });
+
+             }
+             return NO;
+         }
+
+         if (block) {
+             dispatch_async(dispatch_get_main_queue(), ^{
+                 block(YES, nil);
+             });
+         }
+         return NO;
+     }];
+
+    BOOL (^scheduleSaveState)(void) =
+    ^ BOOL {
+        if(CoreDoCommand(M64CMD_STATE_SAVE, 1, (void *)[fileName fileSystemRepresentation]) == M64ERR_SUCCESS)
+        {
+            // Mupen needs to run for a bit for the state saving to take place.
+            [self setPauseEmulation:NO];
+            return YES;
+        }
+
+        return NO;
+    };
+
+    if(scheduleSaveState()) return;
+
+    [self OE_addHandlerForType:M64CORE_EMU_STATE usingBlock:
+     ^ BOOL (m64p_core_param paramType, int newValue)
+     {
+         NSAssert(paramType == M64CORE_EMU_STATE, @"This block should only be called for load completion!");
+         if(newValue != M64EMU_RUNNING && newValue != M64EMU_PAUSED)
+             return YES;
+
+         return !scheduleSaveState();
+     }];
+    
+    [super saveStateToFileAtPath:fileName completionHandler:block];
+}
+
+
+- (BOOL)loadStateFromFileAtPath:(NSString *)fileName error:(NSError**)error   {
+    NSAssert(NO, @"Shouldn't be here since we overwrite the async call");
+}
+
+- (void)loadStateFromFileAtPath:(NSString *)fileName completionHandler:(void (^)(BOOL, NSError *))block
+{
+    __block BOOL wasPaused = [self isEmulationPaused];
+    [self OE_addHandlerForType:M64CORE_STATE_LOADCOMPLETE usingBlock:
+     ^ BOOL (m64p_core_param paramType, int newValue)
+     {
+         NSAssert(paramType == M64CORE_STATE_LOADCOMPLETE, @"This block should only be called for load completion!");
+
+         [self setPauseEmulation:wasPaused];
+         if(newValue == 0)
+         {
+             dispatch_async(dispatch_get_main_queue(), ^{
+                 NSError *error = [NSError errorWithDomain:@"org.openemu.GameCore.ErrorDomain"
+                                                      code:-3
+                                                  userInfo:@{
+                                                             NSLocalizedDescriptionKey : @"Mupen Could not load the save state",
+                                                             NSLocalizedRecoverySuggestionErrorKey : @"The loaded file is probably corrupted.",
+                                                             NSFilePathErrorKey : fileName
+                                                             }];
+                 block(NO, error);
+             });
+             return NO;
+         }
+
+         dispatch_async(dispatch_get_main_queue(), ^{
+             block(YES, nil);
+         });
+
+         return NO;
+     }];
+
+    BOOL (^scheduleLoadState)(void) =
+    ^ BOOL {
+        if(CoreDoCommand(M64CMD_STATE_LOAD, 1, (void *)[fileName fileSystemRepresentation]) == M64ERR_SUCCESS)
+        {
+            // Mupen needs to run for a bit for the state loading to take place.
+            [self setPauseEmulation:NO];
+            return YES;
+        }
+
+        return NO;
+    };
+
+    if(scheduleLoadState()) return;
+
+    [self OE_addHandlerForType:M64CORE_EMU_STATE usingBlock:
+     ^ BOOL (m64p_core_param paramType, int newValue)
+     {
+         NSAssert(paramType == M64CORE_EMU_STATE, @"This block should only be called for load completion!");
+         if(newValue != M64EMU_RUNNING && newValue != M64EMU_PAUSED)
+             return YES;
+
+         return !scheduleLoadState();
+     }];
+}
+
+
+@end
diff --git a/Cores/Mupen64Plus/MupenGameCore+Video.swift b/Cores/Mupen64Plus/MupenGameCore+Video.swift
new file mode 100644
index 0000000000..0ea923f170
--- /dev/null
+++ b/Cores/Mupen64Plus/MupenGameCore+Video.swift
@@ -0,0 +1,25 @@
+//
+//  MupenGameCore+Audio.swift
+//  PVMupen64Plus
+//
+//  Created by Joseph Mattiello on 1/24/22.
+//  Copyright © 2022 Provenance Emu. All rights reserved.
+//
+
+import Foundation
+import PVSupport
+import OpenGLES.ES3
+
+@objc
+public extension MupenGameCore {
+    override var rendersToOpenGL: Bool { true }
+    override var pixelType: GLenum { UInt32(GL_UNSIGNED_BYTE) }
+    override var pixelFormat: GLenum { UInt32(GL_BGRA) }
+    override var internalPixelFormat: GLenum { UInt32(GL_RGBA) }
+    override var videoBuffer: UnsafeRawPointer? { nil }
+    
+    override var bufferSize: CGSize { .init(width: 1024, height: 512) }
+    override var screenRect: CGRect { .init(x: 0, y: 0, width: Int(videoWidth), height: Int(videoHeight)) }
+    override var aspectSize: CGSize { .init(width: Int(videoWidth), height: Int(videoHeight)) }
+
+}
diff --git a/Cores/Mupen64Plus/MupenGameCore.h b/Cores/Mupen64Plus/MupenGameCore.h
index 36f1e5ab6f..455d3cabe4 100644
--- a/Cores/Mupen64Plus/MupenGameCore.h
+++ b/Cores/Mupen64Plus/MupenGameCore.h
@@ -35,25 +35,28 @@ __attribute__((visibility("default")))
 @interface MupenGameCore : PVEmulatorCore <PVN64SystemResponderClient>
 {
 //@private
+    @public
     uint8_t padData[4][PVN64ButtonCount];
     int8_t xAxis[4];
     int8_t yAxis[4];
-//    int videoWidth;
-//    int videoHeight;
-//    int videoBitDepth;
-    int videoDepthBitDepth; // eh
     
-    float sampleRate;
-    
-    BOOL isNTSC;
+    int controllerMode[4];
+    NSOperationQueue *_inputQueue;
 }
 
 @property (nonatomic, assign) int videoWidth;
 @property (nonatomic, assign) int videoHeight;
 @property (nonatomic, assign) int videoBitDepth;
 
+@property (nonatomic, assign) double mupenSampleRate;
+@property (nonatomic, assign) int videoDepthBitDepth;
+@property (nonatomic, assign) BOOL isNTSC;
+@property (nonatomic, assign) BOOL dualJoystick;
+
 - (void) videoInterrupt;
 - (void) swapBuffers;
+
+- (void)setMode:(NSInteger)mode forController:(NSInteger)controller;
 @end
 
 extern __weak MupenGameCore *_current;
diff --git a/Cores/Mupen64Plus/MupenGameCore.m b/Cores/Mupen64Plus/MupenGameCore.m
index f1bc363e9c..72f0fbdc32 100644
--- a/Cores/Mupen64Plus/MupenGameCore.m
+++ b/Cores/Mupen64Plus/MupenGameCore.m
@@ -30,6 +30,10 @@
 //#import "MupenGameCore.h"
 #import <PVMupen64Plus/PVMupen64Plus-Swift.h>
 
+#import "MupenGameCore+Controls.h"
+#import "MupenGameCore+Cheats.h"
+#import "MupenGameCore+Mupen.h"
+
 #import "api/config.h"
 #import "api/m64p_common.h"
 #import "api/m64p_config.h"
@@ -53,28 +57,6 @@
 @import GLKit;
 #endif
 
-#define WIDTH 640
-#define HEIGHT 480
-#define WIDTHf 640.0f
-#define HEIGHTf 480.0f
-
-
-#if TARGET_OS_TV
-#define RESIZE_TO_FULLSCREEN TRUE
-#else
-#define RESIZE_TO_FULLSCREEN [PVSettingsModel.shared nativeScaleEnabled]
-#endif
-
-#import <dlfcn.h>
-
-NSString *MupenControlNames[] = {
-    @"N64_DPadU", @"N64_DPadD", @"N64_DPadL", @"N64_DPadR",
-    @"N64_CU", @"N64_CD", @"N64_CL", @"N64_CR",
-    @"N64_B", @"N64_A", @"N64_R", @"N64_L", @"N64_Z", @"N64_Start"
-}; // FIXME: missing: joypad X, joypad Y, mempak switch, rumble switch
-
-#define N64_ANALOG_MAX 80
-
 #if TARGET_OS_MAC
 @interface MupenGameCore () <PVN64SystemResponderClient>
 #else
@@ -120,7 +102,6 @@ static void MupenStateCallback(void *context, m64p_core_param paramType, int new
 @implementation MupenGameCore
 {
     NSData *romData;
-    NSOperationQueue *_inputQueue;
 
     dispatch_semaphore_t mupenWaitToBeginFrameSemaphore;
     dispatch_semaphore_t coreWaitToEndFrameSemaphore;
@@ -153,12 +134,14 @@ - (instancetype)init
             _videoHeight = HEIGHT;
         }
 
+//        controllerMode = {PLUGIN_MEMPAK, PLUGIN_MEMPAK, PLUGIN_MEMPAK, PLUGIN_MEMPAK};
+        
         _videoBitDepth = 32; // ignored
-        videoDepthBitDepth = 0; // TODO
+        _videoDepthBitDepth = 0; // TODO
         
-        sampleRate = 33600;
+        _mupenSampleRate = 33600;
         
-        isNTSC = YES;
+        _isNTSC = YES;
 
         _callbackQueue = dispatch_queue_create("org.openemu.MupenGameCore.CallbackHandlerQueue", DISPATCH_QUEUE_SERIAL);
         _callbackHandlers = [[NSMutableDictionary alloc] init];
@@ -172,8 +155,7 @@ - (instancetype)init
     return self;
 }
 
-- (void)dealloc
-{
+- (void)dealloc {
     SetStateCallback(NULL, NULL);
     SetDebugCallback(NULL, NULL);
     
@@ -254,606 +236,6 @@ - (void)OE_didReceiveStateChangeForParamType:(m64p_core_param)paramType value:(i
     return dlopen(info.dli_fname, RTLD_LAZY | RTLD_GLOBAL);
 }
 
-static void MupenGetKeys(int Control, BUTTONS *Keys)
-{
-    GET_CURRENT_AND_RETURN();
-    
-    [current pollControllers];
-
-    Keys->R_DPAD = current->padData[Control][PVN64ButtonDPadRight];
-    Keys->L_DPAD = current->padData[Control][PVN64ButtonDPadLeft];
-    Keys->D_DPAD = current->padData[Control][PVN64ButtonDPadDown];
-    Keys->U_DPAD = current->padData[Control][PVN64ButtonDPadUp];
-    Keys->START_BUTTON = current->padData[Control][PVN64ButtonStart];
-    Keys->Z_TRIG = current->padData[Control][PVN64ButtonZ];
-    Keys->B_BUTTON = current->padData[Control][PVN64ButtonB];
-    Keys->A_BUTTON = current->padData[Control][PVN64ButtonA];
-    Keys->R_CBUTTON = current->padData[Control][PVN64ButtonCRight];
-    Keys->L_CBUTTON = current->padData[Control][PVN64ButtonCLeft];
-    Keys->D_CBUTTON = current->padData[Control][PVN64ButtonCDown];
-    Keys->U_CBUTTON = current->padData[Control][PVN64ButtonCUp];
-    Keys->R_TRIG = current->padData[Control][PVN64ButtonR];
-    Keys->L_TRIG = current->padData[Control][PVN64ButtonL];
-    Keys->X_AXIS = current->xAxis[Control];
-    Keys->Y_AXIS = current->yAxis[Control];
-}
-
-static void MupenInitiateControllers (CONTROL_INFO ControlInfo)
-{
-    GET_CURRENT_OR_RETURN();
-
-    ControlInfo.Controls[0].Present = 1;
-    ControlInfo.Controls[0].Plugin = PLUGIN_RAW;
-    ControlInfo.Controls[1].Present = current.controller2 != nil;
-    ControlInfo.Controls[1].Plugin = PLUGIN_MEMPAK;
-    ControlInfo.Controls[2].Present = current.controller3 != nil;
-    ControlInfo.Controls[2].Plugin = PLUGIN_MEMPAK;
-    ControlInfo.Controls[3].Present = current.controller4 != nil;
-    ControlInfo.Controls[3].Plugin = PLUGIN_MEMPAK;
-}
-
-static unsigned char DataCRC( unsigned char *Data, int iLenght )
-{
-    unsigned char Remainder = Data[0];
-
-    int iByte = 1;
-    unsigned char bBit = 0;
-
-    while( iByte <= iLenght )
-    {
-        int HighBit = ((Remainder & 0x80) != 0);
-        Remainder = Remainder << 1;
-
-        Remainder += ( iByte < iLenght && Data[iByte] & (0x80 >> bBit )) ? 1 : 0;
-
-        Remainder ^= (HighBit) ? 0x85 : 0;
-
-        bBit++;
-        iByte += bBit/8;
-        bBit %= 8;
-    }
-
-    return Remainder;
-}
-
-/******************************************************************
- Function: ControllerCommand
- Purpose:  To process the raw data that has just been sent to a
- specific controller.
- input:    - Controller Number (0 to 3) and -1 signalling end of
- processing the pif ram.
- - Pointer of data to be processed.
- output:   none
- note:     This function is only needed if the DLL is allowing raw
- data, or the plugin is set to raw
- the data that is being processed looks like this:
- initilize controller: 01 03 00 FF FF FF
- read controller:      01 04 01 FF FF FF FF
- *******************************************************************/
-static void MupenControllerCommand(int Control, unsigned char *Command)
-{
-        // Some stuff from n-rage plugin
-#define RD_GETSTATUS        0x00        // get status
-#define RD_READKEYS         0x01        // read button values
-#define RD_READPAK          0x02        // read from controllerpack
-#define RD_WRITEPAK         0x03        // write to controllerpack
-#define RD_RESETCONTROLLER  0xff        // reset controller
-#define RD_READEEPROM       0x04        // read eeprom
-#define RD_WRITEEPROM       0x05        // write eeprom
-
-#define PAK_IO_RUMBLE       0xC000      // the address where rumble-commands are sent to
-
-    GET_CURRENT_OR_RETURN();
-
-    unsigned char *Data = &Command[5];
-
-    if (Control == -1)
-    return;
-
-    switch (Command[2])
-    {
-        case RD_GETSTATUS:
-        break;
-        case RD_READKEYS:
-        break;
-        case RD_READPAK: {
-//        if (controller[Control].control->Plugin == PLUGIN_RAW)
-//        {
-            unsigned int dwAddress = (Command[3] << 8) + (Command[4] & 0xE0);
-
-            if(( dwAddress >= 0x8000 ) && ( dwAddress < 0x9000 ) )
-            memset( Data, 0x80, 32 );
-            else
-            memset( Data, 0x00, 32 );
-
-            Data[32] = DataCRC( Data, 32 );
-//        }
-        break;
-        }
-        case RD_WRITEPAK: {
-//        if (controller[Control].control->Plugin == PLUGIN_RAW)
-//        {
-            unsigned int dwAddress = (Command[3] << 8) + (Command[4] & 0xE0);
-//            Data[32] = DataCRC( Data, 32 );
-//
-            if ((dwAddress == PAK_IO_RUMBLE) )//&& (rumble.set_rumble_state))
-            {
-                if (*Data)
-                {
-                    [current rumble];
-//                    AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
-//                    rumble.set_rumble_state(Control, RETRO_RUMBLE_WEAK, 0xFFFF);
-//                    rumble.set_rumble_state(Control, RETRO_RUMBLE_STRONG, 0xFFFF);
-                }
-                else
-                {
-//                    rumble.set_rumble_state(Control, RETRO_RUMBLE_WEAK, 0);
-//                    rumble.set_rumble_state(Control, RETRO_RUMBLE_STRONG, 0);
-                }
-            }
-//        }
-
-        break;
-        }
-        case RD_RESETCONTROLLER:
-        break;
-        case RD_READEEPROM:
-        break;
-        case RD_WRITEEPROM:
-        break;
-    }
-}
-
-- (void)pollController:(GCController* _Nullable)controller forIndex:(NSInteger)playerIndex {
-    if (!controller) {
-        return;
-    }
-    if (LIKELY(controller.extendedGamepad)) {
-        GCExtendedGamepad *gamepad     = [controller extendedGamepad];
-        GCControllerDirectionPad *dpad = [gamepad dpad];
-
-        // Left Joystick → Joystick
-        xAxis[playerIndex] = gamepad.leftThumbstick.xAxis.value * N64_ANALOG_MAX;
-        yAxis[playerIndex] = gamepad.leftThumbstick.yAxis.value * N64_ANALOG_MAX;
-
-        // MFi-D-Pad → D-Pad
-        padData[playerIndex][PVN64ButtonDPadUp] = dpad.up.isPressed;
-        padData[playerIndex][PVN64ButtonDPadDown] = dpad.down.isPressed;
-        padData[playerIndex][PVN64ButtonDPadLeft] = dpad.left.isPressed;
-        padData[playerIndex][PVN64ButtonDPadRight] = dpad.right.isPressed;
-
-        // MFi-R2 → Start
-        padData[playerIndex][PVN64ButtonStart] = gamepad.rightTrigger.isPressed;
-        
-        // MFi-L2 → Z
-        padData[playerIndex][PVN64ButtonZ] = gamepad.leftTrigger.isPressed;
-        
-        // If MFi-L2 is not pressed… MFi-L1 → L
-        if (!gamepad.rightShoulder.isPressed) {
-            padData[playerIndex][PVN64ButtonL] = gamepad.leftShoulder.isPressed;
-        }
-        
-        // If MFi-L1 is not pressed… MFi-R1 → R
-        if (!gamepad.leftShoulder.isPressed) {
-            padData[playerIndex][PVN64ButtonR] = gamepad.rightShoulder.isPressed;
-        }
-        // If not C-Mode… MFi-X,A → A,B MFi-Y,B → C←,C↓
-        if (!(gamepad.leftShoulder.isPressed && gamepad.rightShoulder.isPressed)) {
-            padData[playerIndex][PVN64ButtonA] = gamepad.buttonA.isPressed;
-            padData[playerIndex][PVN64ButtonB] = gamepad.buttonX.isPressed;
-            padData[playerIndex][PVN64ButtonCLeft] = gamepad.buttonY.isPressed;
-            padData[playerIndex][PVN64ButtonCDown] = gamepad.buttonB.isPressed;
-        }
-        
-        //C-Mode: MFi-X,Y,A,B -> C←,C↑,C↓,C→
-        if (gamepad.leftShoulder.isPressed && gamepad.rightShoulder.isPressed) {
-            padData[playerIndex][PVN64ButtonCLeft] = gamepad.buttonX.isPressed;
-            padData[playerIndex][PVN64ButtonCUp] = gamepad.buttonY.isPressed;
-            padData[playerIndex][PVN64ButtonCDown] = gamepad.buttonA.isPressed;
-            padData[playerIndex][PVN64ButtonCRight] = gamepad.buttonB.isPressed;
-        }
-
-        // Right Joystick → C Buttons
-        float rightJoystickDeadZone = 0.45;
-        if (!(gamepad.leftShoulder.isPressed && gamepad.rightShoulder.isPressed) && !(gamepad.buttonY.isPressed || gamepad.buttonB.isPressed)) {
-            padData[playerIndex][PVN64ButtonCUp] = gamepad.rightThumbstick.up.value > rightJoystickDeadZone;
-            padData[playerIndex][PVN64ButtonCDown] = gamepad.rightThumbstick.down.value > rightJoystickDeadZone;
-            padData[playerIndex][PVN64ButtonCLeft] = gamepad.rightThumbstick.left.value > rightJoystickDeadZone;
-            padData[playerIndex][PVN64ButtonCRight] = gamepad.rightThumbstick.right.value > rightJoystickDeadZone;
-        }
-    } else if ([controller gamepad]) {
-        GCGamepad *gamepad = [controller gamepad];
-        GCControllerDirectionPad *dpad = [gamepad dpad];
-        
-        if (!gamepad.rightShoulder.isPressed) {
-            // Default
-            xAxis[playerIndex] = (dpad.left.value > 0.5 ? -N64_ANALOG_MAX : 0) + (dpad.right.value > 0.5 ? N64_ANALOG_MAX : 0);
-            yAxis[playerIndex] = (dpad.down.value > 0.5 ? -N64_ANALOG_MAX : 0) + (dpad.up.value > 0.5 ? N64_ANALOG_MAX : 0);
-            
-            padData[playerIndex][PVN64ButtonA] = gamepad.buttonA.isPressed;
-            padData[playerIndex][PVN64ButtonB] = gamepad.buttonX.isPressed;
-            
-            padData[playerIndex][PVN64ButtonCLeft] = gamepad.buttonY.isPressed;
-            padData[playerIndex][PVN64ButtonCDown] = gamepad.buttonB.isPressed;
-        } else {
-            // Alt-Mode
-            padData[playerIndex][PVN64ButtonDPadUp] = dpad.up.isPressed;
-            padData[playerIndex][PVN64ButtonDPadDown] = dpad.down.isPressed;
-            padData[playerIndex][PVN64ButtonDPadLeft] = dpad.left.isPressed;
-            padData[playerIndex][PVN64ButtonDPadRight] = dpad.right.isPressed;
-            
-            padData[playerIndex][PVN64ButtonCLeft] = gamepad.buttonX.isPressed;
-            padData[playerIndex][PVN64ButtonCUp] = gamepad.buttonY.isPressed;
-            padData[playerIndex][PVN64ButtonCDown] = gamepad.buttonA.isPressed;
-            padData[playerIndex][PVN64ButtonCRight] = gamepad.buttonB.isPressed;
-        }
-        
-        padData[playerIndex][PVN64ButtonZ] = gamepad.leftShoulder.isPressed;
-        padData[playerIndex][PVN64ButtonR] = gamepad.rightShoulder.isPressed;
-        
-    }
-#if TARGET_OS_TV
-    else if ([controller microGamepad]) {
-        GCMicroGamepad *gamepad = [controller microGamepad];
-        GCControllerDirectionPad *dpad = [gamepad dpad];
-        
-        xAxis[playerIndex] = (dpad.left.value > 0.5 ? -N64_ANALOG_MAX : 0) + (dpad.right.value > 0.5 ? N64_ANALOG_MAX : 0);
-        yAxis[playerIndex] = (dpad.down.value > 0.5 ? -N64_ANALOG_MAX : 0) + (dpad.up.value > 0.5 ? N64_ANALOG_MAX : 0);
-        
-        padData[playerIndex][PVN64ButtonB] = gamepad.buttonA.isPressed;
-        padData[playerIndex][PVN64ButtonA] = gamepad.buttonX.isPressed;
-    }
-#endif
-}
-
-- (void)pollControllers {
-#define USE_CAPTURE 1
-#define USE_QUEUE 1
-
-#if USE_CAPTURE
-#define controllerForNum(num) [self.controller##num capture]
-#else
-#define controllerForNum(num) self.controller##num
-#endif
-
-#if USE_QUEUE
-		//    const NSOperationQueue *queue = [NSOperationQueue currentQueue];
-			[_inputQueue cancelAllOperations];
-			NSMutableArray<NSBlockOperation*>* ops = [NSMutableArray arrayWithCapacity:4];
-		#define CHECK_CONTROLLER(num) \
-			if(self.controller##num) [ops addObject:[NSBlockOperation blockOperationWithBlock:^{[self pollController:controllerForNum(num) forIndex:(num - 1)];}]]
-
-			CHECK_CONTROLLER(1);
-			CHECK_CONTROLLER(2);
-			CHECK_CONTROLLER(3);
-			CHECK_CONTROLLER(4);
-
-			[_inputQueue addOperations:ops waitUntilFinished:NO];
-#else
-#define CHECK_CONTROLLER(num) if(self.controller##num) [self pollController:controllerForNum(num) forIndex:(num - 1)]
-
-	CHECK_CONTROLLER(1);
-	CHECK_CONTROLLER(2);
-	CHECK_CONTROLLER(3);
-	CHECK_CONTROLLER(4);
-#endif
-#undef CHECK_CONTROLLER
-#undef controllerForNum
-}
-
-static AUDIO_INFO AudioInfo;
-
-static void MupenAudioSampleRateChanged(int SystemType)
-{
-    GET_CURRENT_AND_RETURN();
-
-    float currentRate = current->sampleRate;
-    
-    switch (SystemType)
-    {
-        default:
-        case SYSTEM_NTSC:
-            current->sampleRate = 48681812 / (*AudioInfo.AI_DACRATE_REG + 1);
-            break;
-        case SYSTEM_PAL:
-            current->sampleRate = 49656530 / (*AudioInfo.AI_DACRATE_REG + 1);
-            break;
-    }
-
-    [[current audioDelegate] audioSampleRateDidChange];
-    ILOG(@"Mupen rate changed %f -> %f\n", currentRate, current->sampleRate);
-}
-
-static void MupenAudioLenChanged()
-{
-    GET_CURRENT_AND_RETURN();
-
-    const int LenReg = *AudioInfo.AI_LEN_REG;
-    uint8_t *ptr = (uint8_t*)(AudioInfo.RDRAM + (*AudioInfo.AI_DRAM_ADDR_REG & 0xFFFFFF));
-    
-    // Swap channels
-    for (uint32_t i = 0; i < LenReg; i += 4)
-    {
-        ptr[i] ^= ptr[i + 2];
-        ptr[i + 2] ^= ptr[i];
-        ptr[i] ^= ptr[i + 2];
-        ptr[i + 1] ^= ptr[i + 3];
-        ptr[i + 3] ^= ptr[i + 1];
-        ptr[i + 1] ^= ptr[i + 3];
-    }
-    
-    [[current ringBufferAtIndex:0] write:ptr maxLength:LenReg];
-}
-
-static void SetIsNTSC()
-{
-    GET_CURRENT_AND_RETURN();
-
-    extern m64p_rom_header ROM_HEADER;
-    switch (ROM_HEADER.Country_code&0xFF)
-    {
-        case 0x44:
-        case 0x46:
-        case 0x49:
-        case 0x50:
-        case 0x53:
-        case 0x55:
-        case 0x58:
-        case 0x59:
-            current->isNTSC = NO;
-            break;
-        case 0x37:
-        case 0x41:
-        case 0x45:
-        case 0x4a:
-            current->isNTSC = YES;
-            break;
-    }
-}
-
-static int MupenOpenAudio(AUDIO_INFO info)
-{
-    AudioInfo = info;
-    
-    SetIsNTSC();
-    
-    return M64ERR_SUCCESS;
-}
-
-static void MupenSetAudioSpeed(int percent)
-{
-    // do we need this?
-}
-
-static void ConfigureAll(NSString *romFolder) {
-	ConfigureCore(romFolder);
-	ConfigureVideoGeneral();
-	ConfigureGLideN64(romFolder);
-	ConfigureRICE();
-}
-
-static void ConfigureCore(NSString *romFolder) {
-	GET_CURRENT_AND_RETURN();
-
-	// TODO: Proper path
-	NSBundle *coreBundle = [NSBundle mainBundle];
-	const char *dataPath;
-	dataPath = [[coreBundle resourcePath] fileSystemRepresentation];
-
-	/** Core Config **/
-	m64p_handle config;
-	ConfigOpenSection("Core", &config);
-
-	// set SRAM path
-	ConfigSetParameter(config, "SaveSRAMPath", M64TYPE_STRING, current.batterySavesPath.fileSystemRepresentation);
-	// set data path
-	ConfigSetParameter(config, "SharedDataPath", M64TYPE_STRING, romFolder.fileSystemRepresentation);
-
-	// Use Pure Interpreter if 0, Cached Interpreter if 1, or Dynamic Recompiler if 2 or more"
-	int emulator = [MupenGameCore intForOption:@"CPU Mode"];
-	printf("emulator %i", emulator);
-	ConfigSetParameter(config, "R4300Emulator", M64TYPE_INT, &emulator);
-
-	ConfigSaveSection("Core");
-	/** End Core Config **/
-}
-
-static void ConfigureVideoGeneral() {
-	/** Begin General Video Config **/
-	m64p_handle general;
-	ConfigOpenSection("Video-General", &general);
-
-	// Use fullscreen mode
-	int useFullscreen = 1;
-	ConfigSetParameter(general, "Fullscreen", M64TYPE_BOOL, &useFullscreen);
-
-    int screenWidth = WIDTH;
-    int screenHeight = HEIGHT;
-    if(RESIZE_TO_FULLSCREEN) {
-        CGSize size = UIApplication.sharedApplication.keyWindow.bounds.size;
-        float widthScale = floor(size.height / WIDTHf);
-        float heightScale = floor(size.height / HEIGHTf);
-        float scale = MAX(MIN(widthScale, heightScale), 1);
-        screenWidth =  scale * WIDTHf;
-        screenHeight = scale * HEIGHTf;
-    }
-
-	// Screen width
-	ConfigSetParameter(general, "ScreenWidth", M64TYPE_INT, &screenWidth);
-
-	// Screen height
-	ConfigSetParameter(general, "ScreenHeight", M64TYPE_INT, &screenHeight);
-
-
-    DLOG(@"Setting size to (%i,%i)", screenWidth, screenHeight);
-
-	ConfigSaveSection("Video-General");
-	/** End General Video Config **/
-}
-
-static void ConfigureGLideN64(NSString *romFolder) {
-	/** Begin GLideN64 Config **/
-	m64p_handle gliden64;
-	ConfigOpenSection("Video-GLideN64", &gliden64);
-
-        // 0 = stretch, 1 = 4:3, 2 = 16:9, 3 = adjust
-	int aspectRatio = [MupenGameCore intForOption:@"Aspect Ratio"];
-
-//    if(RESIZE_TO_FULLSCREEN) {
-//        #if TARGET_OS_TV
-//            aspectRatio = 1;
-//        #else
-//            aspectRatio = 3;
-//        #endif
-//    }
-
-    ConfigSetParameter(gliden64, "AspectRatio", M64TYPE_INT, &aspectRatio);
-
-	// Per-pixel lighting
-    int enableHWLighting = MupenGameCore.perPixelLighting ? 1 : 0;
-	ConfigSetParameter(gliden64, "EnableHWLighting", M64TYPE_BOOL, &enableHWLighting);
-
-	// HiRez & texture options
-	//  txHiresEnable, "Use high-resolution texture packs if available."
-	int txHiresEnable = [MupenGameCore boolForOption:@"Enable HiRes Texture packs"];
-	ConfigSetParameter(gliden64, "txHiresEnable", M64TYPE_BOOL, &txHiresEnable);
-
-    // Path to folder with hi-res texture packs.
-	ConfigSetParameter(gliden64, "txPath", M64TYPE_STRING, [romFolder stringByAppendingPathComponent:@"/"].fileSystemRepresentation);
-    // Path to folder where plugin saves texture cache files.
-	ConfigSetParameter(gliden64, "txCachePath", M64TYPE_STRING, [romFolder stringByAppendingPathComponent:@"/cache/"].fileSystemRepresentation);
-    // Path to folder where plugin saves dumped textures.
-	ConfigSetParameter(gliden64, "txDumpPath", M64TYPE_STRING, [romFolder stringByAppendingPathComponent:@"/texture_dump/"].fileSystemRepresentation);
-
-//    if(RESIZE_TO_FULLSCREEN) {
-	// "txFilterMode",
-	// "Texture filter (0=none, 1=Smooth filtering 1, 2=Smooth filtering 2, 3=Smooth filtering 3, 4=Smooth filtering 4, 5=Sharp filtering 1, 6=Sharp filtering 2)"
-	int txFilterMode = [MupenGameCore intForOption:@"Texture Filter Mode"];
-	ConfigSetParameter(gliden64, "txFilterMode", M64TYPE_INT, &txFilterMode);
-
-	// "txEnhancementMode", config.textureFilter.txEnhancementMode,
-	// "Texture Enhancement (0=none, 1=store as is, 2=X2, 3=X2SAI, 4=HQ2X, 5=HQ2XS, 6=LQ2X, 7=LQ2XS, 8=HQ4X, 9=2xBRZ, 10=3xBRZ, 11=4xBRZ, 12=5xBRZ), 13=6xBRZ"
-	int txEnhancementMode = [MupenGameCore intForOption:@"Texture Enhancement Mode"];
-	ConfigSetParameter(gliden64, "txEnhancementMode", M64TYPE_INT, &txEnhancementMode);
-
-	// "txCacheCompression", config.textureFilter.txCacheCompression, "Zip textures cache."
-	int txCacheCompression = [MupenGameCore boolForOption:@"Compress texture cache"];
-	ConfigSetParameter(gliden64, "txCacheCompression", M64TYPE_BOOL, &txCacheCompression);
-
-	// "txSaveCache", config.textureFilter.txSaveCache,
-	// "Save texture cache to hard disk."
-	int txSaveCache = [MupenGameCore boolForOption:@"Save texture cache"];
-	ConfigSetParameter(gliden64, "txSaveCache", M64TYPE_BOOL, &txSaveCache);
-
-	// Warning, anything other than 0 crashes shader compilation
-	// "MultiSampling", config.video.multisampling, "Enable/Disable MultiSampling (0=off, 2,4,8,16=quality)"
-	int MultiSampling = [MupenGameCore intForOption:@"Multi Sampling"];
-	ConfigSetParameter(gliden64, "MultiSampling", M64TYPE_INT, &MultiSampling);
-//    }
-
-
-        //#Gamma correction settings
-//    res = ConfigSetDefaultBool(g_configVideoGliden64, "ForceGammaCorrection", config.gammaCorrection.force, "Force gamma correction.");
-//    assert(res == M64ERR_SUCCESS);
-//    res = ConfigSetDefaultFloat(g_configVideoGliden64, "GammaCorrectionLevel", config.gammaCorrection.level, "Gamma correction level.");
-//    assert(res == M64ERR_SUCCESS);
-
-    /*
-	 "txCacheSize", config.textureFilter.txCacheSize/ gc_uMegabyte, "Size of filtered textures cache in megabytes."
-	*/
-	int txDump = [MupenGameCore boolForOption:@"Texture Dump"];
-	ConfigSetParameter(gliden64, "txDump", M64TYPE_BOOL, &txDump);
-
-	int txFilterIgnoreBG = [MupenGameCore boolForOption:@"Ignore BG Textures"];
-	ConfigSetParameter(gliden64, "txFilterIgnoreBG", M64TYPE_BOOL, &txFilterIgnoreBG);
-
-
-	int txForce16bpp = [MupenGameCore boolForOption:@"Force 16bpp textures"];
-	ConfigSetParameter(gliden64, "txForce16bpp", M64TYPE_BOOL, &txForce16bpp);
-
-
-	// "txHresAltCRC", config.textureFilter.txHresAltCRC, "Use alternative method of paletted textures CRC calculation."
-	int txHresAltCRC = [MupenGameCore boolForOption:@"HiRes Alt CRC"];
-	ConfigSetParameter(gliden64, "txHresAltCRC", M64TYPE_BOOL, &txHresAltCRC);
-
-
-	// "txHiresFullAlphaChannel", "Allow to use alpha channel of high-res texture fully."
-	int txHiresFullAlphaChannel = [MupenGameCore boolForOption:@"HiRes Full Alpha"];;
-	ConfigSetParameter(gliden64, "txHiresFullAlphaChannel", M64TYPE_BOOL, &txHiresFullAlphaChannel);
-
-	// Draw on-screen display if True, otherwise don't draw OSD
-	int osd = [MupenGameCore boolForOption:@"Debug OSD"];
-	ConfigSetParameter(gliden64, "OnScreenDisplay", M64TYPE_BOOL, &osd);
-	ConfigSetParameter(gliden64, "ShowFPS", M64TYPE_BOOL, &osd);			// Show FPS counter.
-	ConfigSetParameter(gliden64, "ShowVIS", M64TYPE_BOOL, &osd);			// Show VI/S counter.
-	ConfigSetParameter(gliden64, "ShowPercent", M64TYPE_BOOL, &osd);		// Show percent counter.
-	ConfigSetParameter(gliden64, "ShowInternalResolution", M64TYPE_BOOL, &osd);	// Show internal resolution.
-	ConfigSetParameter(gliden64, "ShowRenderingResolution", M64TYPE_BOOL, &osd);	// Show rendering resolution.
-
-	ConfigSaveSection("Video-GLideN64");
-	/** End GLideN64 Config **/
-}
-
-static void ConfigureRICE() {
-	/** RICE CONFIG **/
-	m64p_handle rice;
-	ConfigOpenSection("Video-Rice", &rice);
-
-	// Use a faster algorithm to speed up texture loading and CRC computation
-	int fastTextureLoading = [MupenGameCore boolForOption:@"Fast Texture Loading"];
-;
-	ConfigSetParameter(rice, "FastTextureLoading", M64TYPE_BOOL, &fastTextureLoading);
-
-	// Enable this option to have better render-to-texture quality
-	int doubleSizeForSmallTextureBuffer = [MupenGameCore boolForOption:@"DoubleSizeForSmallTxtrBuf"];
-	ConfigSetParameter(rice, "DoubleSizeForSmallTxtrBuf", M64TYPE_BOOL, &doubleSizeForSmallTextureBuffer);
-
-	// N64 Texture Memory Full Emulation (may fix some games, may break others)
-	int fullTEMEmulation = [MupenGameCore boolForOption:@"FullTMEMEmulation"];
-	ConfigSetParameter(rice, "FullTMEMEmulation", M64TYPE_BOOL, &fullTEMEmulation);
-
-	// Use fullscreen mode if True, or windowed mode if False
-	int fullscreen = 1;
-	ConfigSetParameter(rice, "Fullscreen", M64TYPE_BOOL, &fullscreen);
-
-	// If this option is enabled, the plugin will skip every other frame
-	// Breaks some games in my testing -jm
-	int skipFrame = [MupenGameCore boolForOption:@"SkipFrame"];
-	ConfigSetParameter(rice, "SkipFrame", M64TYPE_BOOL, &skipFrame);
-
-	// Enable hi-resolution texture file loading
-	int hiResTextures = [MupenGameCore boolForOption:@"LoadHiResTextures"];
-	ConfigSetParameter(rice, "LoadHiResTextures", M64TYPE_BOOL, &hiResTextures);
-
-	// Use Mipmapping? 0=no, 1=nearest, 2=bilinear, 3=trilinear
-	int mipmapping = 0;
-	ConfigSetParameter(rice, "Mipmapping", M64TYPE_INT, &mipmapping);
-
-	// Enable/Disable Anisotropic Filtering for Mipmapping (0=no filtering, 2-16=quality).
-	// This is uneffective if Mipmapping is 0. If the given value is to high to be supported by your graphic card, the value will be the highest value your graphic card can support. Better result with Trilinear filtering
-	int anisotropicFiltering = 16;
-	ConfigSetParameter(rice, "AnisotropicFiltering", M64TYPE_INT, &anisotropicFiltering);
-
-	// Enable, Disable or Force fog generation (0=Disable, 1=Enable n64 choose, 2=Force Fog)
-	int fogMethod = 1;
-	ConfigSetParameter(rice, "FogMethod", M64TYPE_INT, &fogMethod);
-
-	// Color bit depth to use for textures (0=default, 1=32 bits, 2=16 bits)
-	// 16 bit breaks some games like GoldenEye
-	int textureQuality = 1;
-	ConfigSetParameter(rice, "TextureQuality", M64TYPE_INT, &textureQuality);
-
-	// Enable/Disable MultiSampling (0=off, 2,4,8,16=quality)
-    int multiSampling = RESIZE_TO_FULLSCREEN ? 4 : 0;
-	ConfigSetParameter(rice, "MultiSampling", M64TYPE_INT, &multiSampling);
-
-	// Color bit depth for rendering window (0=32 bits, 1=16 bits)
-	int colorQuality = 0;
-	ConfigSetParameter(rice, "ColorQuality", M64TYPE_INT, &colorQuality);
-
-	/** End RICE CONFIG **/
-	ConfigSaveSection("Video-Rice");
-}
 
 - (void)copyIniFiles:(NSString*)romFolder {
 	NSBundle *coreBundle = [NSBundle mainBundle];
@@ -943,6 +325,8 @@ - (BOOL)loadFileAtPath:(NSString *)path error:(NSError**)error {
 			}
 		}
 	}
+    
+    [self parseOptions];
 
 	// Create hires folder placement
 	[self createHiResFolder:romFolder];
@@ -1180,17 +564,16 @@ -(EAGLContext*)bestContext {
 }
 #endif
 
-- (void)startEmulation
-{
-    if(!self.isRunning)
-    {
+- (void)startEmulation {
+    [self parseOptions];
+
+    if(!self.isRunning) {
         [super startEmulation];
         [NSThread detachNewThreadSelector:@selector(runMupenEmuThread) toTarget:self withObject:nil];
     }
 }
 
-- (void)runMupenEmuThread
-{
+- (void)runMupenEmuThread {
     @autoreleasepool
     {
         [self.renderDelegate startRenderingOnAlternateThread];
@@ -1233,8 +616,7 @@ - (void)runMupenEmuThread
     }
 }
 
-- (m64p_error)pluginsUnload
-{
+- (m64p_error)pluginsUnload {
     // shutdown and unload frameworks for plugins
     
     typedef m64p_error (*ptr_PluginShutdown)(void);
@@ -1275,35 +657,31 @@ - (dispatch_time_t)frameTime {
     return killTime;
 }
 
-- (void)videoInterrupt
-{
-
-
+- (void)videoInterrupt {
     dispatch_semaphore_signal(coreWaitToEndFrameSemaphore);
     
     dispatch_semaphore_wait(mupenWaitToBeginFrameSemaphore, [self frameTime]);
 }
 
-- (void)swapBuffers
-{
+- (void)swapBuffers {
     [self.renderDelegate didRenderFrameOnAlternateThread];
 }
 
-- (void)executeFrameSkippingFrame:(BOOL)skip
-{
+- (void)executeFrameSkippingFrame:(BOOL)skip {
     dispatch_semaphore_signal(mupenWaitToBeginFrameSemaphore);
     
     dispatch_semaphore_wait(coreWaitToEndFrameSemaphore, [self frameTime]);
 }
 
-- (void)executeFrame
-{
+- (void)executeFrame {
     [self executeFrameSkippingFrame:NO];
 }
 
 - (void)setPauseEmulation:(BOOL)flag
 {
     [super setPauseEmulation:flag];
+    [self parseOptions];
+
     if (flag)
     {
         dispatch_semaphore_signal(mupenWaitToBeginFrameSemaphore);
@@ -1336,148 +714,6 @@ - (void)resetEmulation {
     [self.frontBufferCondition unlock];
 }
 
-- (BOOL)saveStateToFileAtPath:(NSString *)fileName error:(NSError**)error   {
-    NSAssert(NO, @"Shouldn't be here since we overwrite the async call");
-}
-
-- (void)saveStateToFileAtPath:(NSString *)fileName completionHandler:(void (^)(BOOL, NSError *))block
-{
-    __block BOOL wasPaused = [self isEmulationPaused];
-    [self OE_addHandlerForType:M64CORE_STATE_SAVECOMPLETE usingBlock:
-     ^ BOOL (m64p_core_param paramType, int newValue)
-     {
-         [self setPauseEmulation:wasPaused];
-         NSAssert(paramType == M64CORE_STATE_SAVECOMPLETE, @"This block should only be called for save completion!");
-         if(newValue == 0)
-         {
-
-             if (block) {
-                 NSError *error = [NSError errorWithDomain:@"org.openemu.GameCore.ErrorDomain"
-                                                      code:-5
-                                                  userInfo:@{
-                                                             NSLocalizedDescriptionKey : @"Mupen Could not save the current state.",
-                                                             NSFilePathErrorKey : fileName
-                                                             }];
-
-                 dispatch_async(dispatch_get_main_queue(), ^{
-                     block(YES, nil);
-                 });
-
-             }
-             return NO;
-         }
-
-         if (block) {
-             dispatch_async(dispatch_get_main_queue(), ^{
-                 block(YES, nil);
-             });
-         }
-         return NO;
-     }];
-
-    BOOL (^scheduleSaveState)(void) =
-    ^ BOOL {
-        if(CoreDoCommand(M64CMD_STATE_SAVE, 1, (void *)[fileName fileSystemRepresentation]) == M64ERR_SUCCESS)
-        {
-            // Mupen needs to run for a bit for the state saving to take place.
-            [self setPauseEmulation:NO];
-            return YES;
-        }
-
-        return NO;
-    };
-
-    if(scheduleSaveState()) return;
-
-    [self OE_addHandlerForType:M64CORE_EMU_STATE usingBlock:
-     ^ BOOL (m64p_core_param paramType, int newValue)
-     {
-         NSAssert(paramType == M64CORE_EMU_STATE, @"This block should only be called for load completion!");
-         if(newValue != M64EMU_RUNNING && newValue != M64EMU_PAUSED)
-             return YES;
-
-         return !scheduleSaveState();
-     }];
-    
-    [super saveStateToFileAtPath:fileName completionHandler:block];
-}
-
-
-- (BOOL)loadStateFromFileAtPath:(NSString *)fileName error:(NSError**)error   {
-    NSAssert(NO, @"Shouldn't be here since we overwrite the async call");
-}
-
-- (void)loadStateFromFileAtPath:(NSString *)fileName completionHandler:(void (^)(BOOL, NSError *))block
-{
-    __block BOOL wasPaused = [self isEmulationPaused];
-    [self OE_addHandlerForType:M64CORE_STATE_LOADCOMPLETE usingBlock:
-     ^ BOOL (m64p_core_param paramType, int newValue)
-     {
-         NSAssert(paramType == M64CORE_STATE_LOADCOMPLETE, @"This block should only be called for load completion!");
-
-         [self setPauseEmulation:wasPaused];
-         if(newValue == 0)
-         {
-             dispatch_async(dispatch_get_main_queue(), ^{
-                 NSError *error = [NSError errorWithDomain:@"org.openemu.GameCore.ErrorDomain"
-                                                      code:-3
-                                                  userInfo:@{
-                                                             NSLocalizedDescriptionKey : @"Mupen Could not load the save state",
-                                                             NSLocalizedRecoverySuggestionErrorKey : @"The loaded file is probably corrupted.",
-                                                             NSFilePathErrorKey : fileName
-                                                             }];
-                 block(NO, error);
-             });
-             return NO;
-         }
-
-         dispatch_async(dispatch_get_main_queue(), ^{
-             block(YES, nil);
-         });
-
-         return NO;
-     }];
-
-    BOOL (^scheduleLoadState)(void) =
-    ^ BOOL {
-        if(CoreDoCommand(M64CMD_STATE_LOAD, 1, (void *)[fileName fileSystemRepresentation]) == M64ERR_SUCCESS)
-        {
-            // Mupen needs to run for a bit for the state loading to take place.
-            [self setPauseEmulation:NO];
-            return YES;
-        }
-
-        return NO;
-    };
-
-    if(scheduleLoadState()) return;
-
-    [self OE_addHandlerForType:M64CORE_EMU_STATE usingBlock:
-     ^ BOOL (m64p_core_param paramType, int newValue)
-     {
-         NSAssert(paramType == M64CORE_EMU_STATE, @"This block should only be called for load completion!");
-         if(newValue != M64EMU_RUNNING && newValue != M64EMU_PAUSED)
-             return YES;
-
-         return !scheduleLoadState();
-     }];
-}
-
-- (CGSize)bufferSize
-{
-    return CGSizeMake(1024, 512);
-}
-
-- (CGRect)screenRect
-{
-    return CGRectMake(0, 0, _videoWidth, _videoHeight);
-}
-
-- (CGSize)aspectSize
-{
-    return CGSizeMake(_videoWidth, _videoHeight);
-}
-
 - (void) tryToResizeVideoTo:(CGSize)size {
     DLOG(@"Calling set video mode size to (%f,%f)", screenWidth, screenHeight);
 
@@ -1487,142 +723,4 @@ - (void) tryToResizeVideoTo:(CGSize)size {
 	}
 }
 
-- (BOOL)rendersToOpenGL
-{
-    return YES;
-}
-
-//- (BOOL)isDoubleBuffered {
-//    return YES;
-//}
-
-- (const void *)videoBuffer
-{
-    return NULL;
-}
-
-- (GLenum)pixelFormat
-{
-    return GL_BGRA;
-}
-
-- (GLenum)pixelType
-{
-    return GL_UNSIGNED_BYTE;
-}
-
-- (GLenum)internalPixelFormat
-{
-    return GL_RGBA;
-}
-
-#pragma mark Mupen Audio
-
-- (NSTimeInterval)frameInterval
-{
-    // Mupen uses 60 but it's probably wrong
-    return isNTSC ? 60 : 50;
-}
-
-- (NSUInteger)channelCount
-{
-    return 2;
-}
-
-- (double)audioSampleRate
-{
-    return sampleRate;
-}
-
-- (void)didMoveN64JoystickDirection:(PVN64Button)button withValue:(CGFloat)value forPlayer:(NSUInteger)player
-{
-    switch (button)
-    {
-        case PVN64ButtonAnalogUp:
-            yAxis[player] = value * N64_ANALOG_MAX;
-            break;
-        case PVN64ButtonAnalogDown:
-            yAxis[player] = value * -N64_ANALOG_MAX;
-            break;
-        case PVN64ButtonAnalogLeft:
-            xAxis[player] = value * -N64_ANALOG_MAX;
-            break;
-        case PVN64ButtonAnalogRight:
-            xAxis[player] = value * N64_ANALOG_MAX;
-            break;
-        default:
-            break;
-    }
-}
-
-- (void)didPushN64Button:(PVN64Button)button forPlayer:(NSUInteger)player
-{
-    padData[player][button] = 1;
-}
-
-- (void)didReleaseN64Button:(PVN64Button)button forPlayer:(NSUInteger)player
-{
-    padData[player][button] = 0;
-}
-
-- (void)setCheat:(NSString *)code setType:(NSString *)type setEnabled:(BOOL)enabled
-{
-// Need to fix ambigious main.h inclusion
-//    // Sanitize
-//    code = [code stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
-//    
-//    // Remove any spaces
-//    code = [code stringByReplacingOccurrencesOfString:@" " withString:@""];
-//    
-//    NSString *singleCode;
-//    NSArray *multipleCodes = [code componentsSeparatedByString:@"+"];
-//    m64p_cheat_code *gsCode = (m64p_cheat_code*) calloc([multipleCodes count], sizeof(m64p_cheat_code));
-//    int codeCounter = 0;
-//    
-//    for (singleCode in multipleCodes)
-//    {
-//        if ([singleCode length] == 12) // GameShark
-//        {
-//            // GameShark N64 format: XXXXXXXX YYYY
-//            NSString *address = [singleCode substringWithRange:NSMakeRange(0, 8)];
-//            NSString *value = [singleCode substringWithRange:NSMakeRange(8, 4)];
-//            
-//            // Convert GS hex to int
-//            unsigned int outAddress, outValue;
-//            NSScanner* scanAddress = [NSScanner scannerWithString:address];
-//            NSScanner* scanValue = [NSScanner scannerWithString:value];
-//            [scanAddress scanHexInt:&outAddress];
-//            [scanValue scanHexInt:&outValue];
-//            
-//            gsCode[codeCounter].address = outAddress;
-//            gsCode[codeCounter].value = outValue;
-//            codeCounter++;
-//        }
-//    }
-//    
-//    // Update address directly if code needs GS button pressed
-//    if ((gsCode[0].address & 0xFF000000) == 0x88000000 || (gsCode[0].address & 0xFF000000) == 0xA8000000)
-//    {
-//        *(unsigned char *)((g_rdram + ((gsCode[0].address & 0xFFFFFF)^S8))) = (unsigned char)gsCode[0].value; // Update 8-bit address
-//    }
-//    else if ((gsCode[0].address & 0xFF000000) == 0x89000000 || (gsCode[0].address & 0xFF000000) == 0xA9000000)
-//    {
-//        *(unsigned short *)((g_rdram + ((gsCode[0].address & 0xFFFFFF)^S16))) = (unsigned short)gsCode[0].value; // Update 16-bit address
-//    }
-//    // Else add code as normal
-//    else
-//    {
-//        enabled ? CoreAddCheat([code UTF8String], gsCode, codeCounter+1) : CoreCheatEnabled([code UTF8String], 0);
-//    }
-}
-
-
-@end
-
-@implementation MupenGameCore (Rumble)
-
-- (BOOL)supportsRumble {
-    return TRUE;
-}
-
 @end
diff --git a/Cores/Mupen64Plus/MupenOptions.swift b/Cores/Mupen64Plus/MupenOptions.swift
index 0286659bf0..25434abf61 100644
--- a/Cores/Mupen64Plus/MupenOptions.swift
+++ b/Cores/Mupen64Plus/MupenOptions.swift
@@ -13,6 +13,41 @@ extension MupenGameCore: CoreOptional {
 		//    public func valueForOption<T>(_ option: CoreOption<T>) -> T where T : Decodable, T : Encodable {
 		//
 		//    }
+    
+    // MARK: Dual-Joystick
+    // Use Pure Interpreter if 0, Cached Interpreter if 1, or Dynamic Recompiler if 2 or more
+    static var dualJoystickOption: CoreOption = {
+        .bool(.init(
+            title: "Dual joystick on first player",
+            description: "If 1st player controller has dual joysticks, use the right joystick as player 2. For games, such as GoldenEye, that support using 2 N64 controllers for single player input.",
+            requiresRestart: false))
+    }()
+    
+    static func controllerPakOption(forController index: Int) -> CoreOption {
+        /*
+         #define PLUGIN_NONE                 1
+         #define PLUGIN_MEMPAK               2
+         #define PLUGIN_RUMBLE_PAK           3 /* not implemented for non raw data */
+         #define PLUGIN_TRANSFER_PAK         4 /* not implemented for non raw data */
+         #define PLUGIN_RAW                  5 /* the controller plugin is passed in raw data */
+         */
+
+        .enumeration(.init(title: "Controller Pak \(index)",
+                                                description: nil,
+                                                requiresRestart: true),
+                                          values:[
+                                            .init(title: "None", description: "", value: 1),
+                                            .init(title: "Memory Pak", description: "", value: 2),
+//                                            .init(title: "Rumble Pak, description: "", value: 3),
+//                                            .init(title: "Transer Pak", description: "", value: 4),
+                                            .init(title: "Raw Data", description: "Used for Rumble or Transer Pak", value: 5),
+                                          ])
+    }
+    
+    static var controllerPak1: CoreOption = controllerPakOption(forController: 1)
+    static var controllerPak2: CoreOption = controllerPakOption(forController: 2)
+    static var controllerPak3: CoreOption = controllerPakOption(forController: 3)
+    static var controllerPak4: CoreOption = controllerPakOption(forController: 4)
 
 	public static var options: [CoreOption] {
 		var options = [CoreOption]()
@@ -30,6 +65,11 @@ extension MupenGameCore: CoreOptional {
 
 		let pluginsGroup = CoreOption.group(.init(title: "Plugins", description: nil),
 											subOptions: [videoPluginOption, rspOptions])
+        
+        // MARK: -- Controls
+        let controlOptions: [CoreOption] = [dualJoystickOption, controllerPak1, controllerPak2, controllerPak3, controllerPak4]
+        let controlGroup:CoreOption = .group(.init(title: "Controls", description: ""),
+                                             subOptions: controlOptions)
 
 			// MARK: -- Core
 		var coreOptions = [CoreOption]()
@@ -114,7 +154,6 @@ extension MupenGameCore: CoreOptional {
 											.init(title: "4xBRZ", description: "", value: 11),
 											.init(title: "5xBRZ", description: "", value: 12),
 											.init(title: "6xBRZ", description: "", value: 13),
-
 										  ]))
 
 			// MARK: txFilterMode <Enum=0>
@@ -241,7 +280,7 @@ extension MupenGameCore: CoreOptional {
 			//        let videoGroup = CoreOption.group(.init(title: "Video"),
 			//										  subOptions: [glidenGroup, riceGroup])
 		
-		options.append(contentsOf: [coreGroup, pluginsGroup, /*videoGroup,*/ glidenGroup, riceGroup])
+		options.append(contentsOf: [coreGroup, pluginsGroup, controlGroup, /*videoGroup,*/ glidenGroup, riceGroup])
 		return options
 	}
 }
@@ -284,3 +323,20 @@ extension MupenGameCore {
 		return valueForOption(Bool.self, "Hardware Lighting") ?? false
 	}
 }
+
+@objc public extension MupenGameCore {
+  @objc var dualJoystickOption: Bool { MupenGameCore.valueForOption(MupenGameCore.dualJoystickOption).asBool }
+    
+    @objc var controllerPak1Option: Int { MupenGameCore.valueForOption(MupenGameCore.controllerPak1).asInt ?? 1 }
+    @objc var controllerPak2Option: Int { MupenGameCore.valueForOption(MupenGameCore.controllerPak2).asInt ?? 1 }
+    @objc var controllerPak3Option: Int { MupenGameCore.valueForOption(MupenGameCore.controllerPak3).asInt ?? 1 }
+    @objc var controllerPak4Option: Int { MupenGameCore.valueForOption(MupenGameCore.controllerPak4).asInt ?? 1 }
+
+    func parseOptions() {
+        self.dualJoystick = dualJoystickOption
+        self.setMode(controllerPak1Option, forController: 0)
+        self.setMode(controllerPak2Option, forController: 1)
+        self.setMode(controllerPak3Option, forController: 2)
+        self.setMode(controllerPak4Option, forController: 3)
+    }
+}
diff --git a/Cores/Mupen64Plus/PVMupen64Plus.xcodeproj/project.pbxproj b/Cores/Mupen64Plus/PVMupen64Plus.xcodeproj/project.pbxproj
index d47e252501..8aecdc775f 100644
--- a/Cores/Mupen64Plus/PVMupen64Plus.xcodeproj/project.pbxproj
+++ b/Cores/Mupen64Plus/PVMupen64Plus.xcodeproj/project.pbxproj
@@ -103,6 +103,26 @@
 		B3962AEC26FBD89A00FAEFC0 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3962AEB26FBD89A00FAEFC0 /* OpenGL.framework */; platformFilter = maccatalyst; };
 		B3962AED26FBD8A500FAEFC0 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3962AEB26FBD89A00FAEFC0 /* OpenGL.framework */; platformFilter = maccatalyst; };
 		B3962AEE26FBD8F100FAEFC0 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3962AEB26FBD89A00FAEFC0 /* OpenGL.framework */; platformFilter = maccatalyst; };
+		B3963E61279E835300B00117 /* MupenGameCore+Audio.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3963E60279E835300B00117 /* MupenGameCore+Audio.swift */; };
+		B3963E62279E835300B00117 /* MupenGameCore+Audio.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3963E60279E835300B00117 /* MupenGameCore+Audio.swift */; };
+		B3963E64279E867100B00117 /* MupenGameCore+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3963E63279E866B00B00117 /* MupenGameCore+Video.swift */; };
+		B3963E65279E867100B00117 /* MupenGameCore+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3963E63279E866B00B00117 /* MupenGameCore+Video.swift */; };
+		B39C29D127A120790078D028 /* MupenGameCore+Controls.m in Sources */ = {isa = PBXBuildFile; fileRef = B39C29D027A120780078D028 /* MupenGameCore+Controls.m */; };
+		B39C29D227A120790078D028 /* MupenGameCore+Controls.m in Sources */ = {isa = PBXBuildFile; fileRef = B39C29D027A120780078D028 /* MupenGameCore+Controls.m */; };
+		B39C29D427A121160078D028 /* MupenGameCore+Controls.h in Headers */ = {isa = PBXBuildFile; fileRef = B39C29D327A121150078D028 /* MupenGameCore+Controls.h */; };
+		B39C29D527A121160078D028 /* MupenGameCore+Controls.h in Headers */ = {isa = PBXBuildFile; fileRef = B39C29D327A121150078D028 /* MupenGameCore+Controls.h */; };
+		B39C29D827A12D100078D028 /* MupenGameCore+Cheats.h in Headers */ = {isa = PBXBuildFile; fileRef = B39C29D627A12D0F0078D028 /* MupenGameCore+Cheats.h */; };
+		B39C29D927A12D100078D028 /* MupenGameCore+Cheats.h in Headers */ = {isa = PBXBuildFile; fileRef = B39C29D627A12D0F0078D028 /* MupenGameCore+Cheats.h */; };
+		B39C29DA27A12D100078D028 /* MupenGameCore+Cheats.m in Sources */ = {isa = PBXBuildFile; fileRef = B39C29D727A12D0F0078D028 /* MupenGameCore+Cheats.m */; };
+		B39C29DB27A12D100078D028 /* MupenGameCore+Cheats.m in Sources */ = {isa = PBXBuildFile; fileRef = B39C29D727A12D0F0078D028 /* MupenGameCore+Cheats.m */; };
+		B39C29DE27A12DA30078D028 /* MupenGameCore+Saves.h in Headers */ = {isa = PBXBuildFile; fileRef = B39C29DC27A12DA20078D028 /* MupenGameCore+Saves.h */; };
+		B39C29DF27A12DA30078D028 /* MupenGameCore+Saves.h in Headers */ = {isa = PBXBuildFile; fileRef = B39C29DC27A12DA20078D028 /* MupenGameCore+Saves.h */; };
+		B39C29E027A12DA30078D028 /* MupenGameCore+Saves.m in Sources */ = {isa = PBXBuildFile; fileRef = B39C29DD27A12DA30078D028 /* MupenGameCore+Saves.m */; };
+		B39C29E127A12DA30078D028 /* MupenGameCore+Saves.m in Sources */ = {isa = PBXBuildFile; fileRef = B39C29DD27A12DA30078D028 /* MupenGameCore+Saves.m */; };
+		B39C29E427A12E6A0078D028 /* MupenGameCore+Mupen.h in Headers */ = {isa = PBXBuildFile; fileRef = B39C29E227A12E690078D028 /* MupenGameCore+Mupen.h */; };
+		B39C29E527A12E6A0078D028 /* MupenGameCore+Mupen.h in Headers */ = {isa = PBXBuildFile; fileRef = B39C29E227A12E690078D028 /* MupenGameCore+Mupen.h */; };
+		B39C29E627A12E6A0078D028 /* MupenGameCore+Mupen.m in Sources */ = {isa = PBXBuildFile; fileRef = B39C29E327A12E6A0078D028 /* MupenGameCore+Mupen.m */; };
+		B39C29E727A12E6A0078D028 /* MupenGameCore+Mupen.m in Sources */ = {isa = PBXBuildFile; fileRef = B39C29E327A12E6A0078D028 /* MupenGameCore+Mupen.m */; };
 		B39E8CB62053B53100380DCD /* Core.plist in Resources */ = {isa = PBXBuildFile; fileRef = B39E8CB52053B53100380DCD /* Core.plist */; };
 		B3A302F22073F2D4008C1955 /* RiceVideoLinux.ini in Resources */ = {isa = PBXBuildFile; fileRef = B3A302912073F2AA008C1955 /* RiceVideoLinux.ini */; };
 		B3A302F32073F2F2008C1955 /* osal_dynamiclib_unix.c in Sources */ = {isa = PBXBuildFile; fileRef = B3A302C12073F2AA008C1955 /* osal_dynamiclib_unix.c */; };
@@ -1519,6 +1539,16 @@
 		B380101320854EAA00FA03B8 /* TxDbg_ios.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TxDbg_ios.mm; sourceTree = "<group>"; };
 		B3962AEA26FBD16000FAEFC0 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
 		B3962AEB26FBD89A00FAEFC0 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.0.sdk/System/Library/Frameworks/OpenGL.framework; sourceTree = DEVELOPER_DIR; };
+		B3963E60279E835300B00117 /* MupenGameCore+Audio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MupenGameCore+Audio.swift"; sourceTree = "<group>"; };
+		B3963E63279E866B00B00117 /* MupenGameCore+Video.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MupenGameCore+Video.swift"; sourceTree = "<group>"; };
+		B39C29D027A120780078D028 /* MupenGameCore+Controls.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MupenGameCore+Controls.m"; sourceTree = "<group>"; };
+		B39C29D327A121150078D028 /* MupenGameCore+Controls.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MupenGameCore+Controls.h"; sourceTree = "<group>"; };
+		B39C29D627A12D0F0078D028 /* MupenGameCore+Cheats.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MupenGameCore+Cheats.h"; sourceTree = "<group>"; };
+		B39C29D727A12D0F0078D028 /* MupenGameCore+Cheats.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MupenGameCore+Cheats.m"; sourceTree = "<group>"; };
+		B39C29DC27A12DA20078D028 /* MupenGameCore+Saves.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MupenGameCore+Saves.h"; sourceTree = "<group>"; };
+		B39C29DD27A12DA30078D028 /* MupenGameCore+Saves.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MupenGameCore+Saves.m"; sourceTree = "<group>"; };
+		B39C29E227A12E690078D028 /* MupenGameCore+Mupen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MupenGameCore+Mupen.h"; sourceTree = "<group>"; };
+		B39C29E327A12E6A0078D028 /* MupenGameCore+Mupen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MupenGameCore+Mupen.m"; sourceTree = "<group>"; };
 		B39E8CB52053B53100380DCD /* Core.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Core.plist; sourceTree = "<group>"; };
 		B3A302852073F2AA008C1955 /* INSTALL */ = {isa = PBXFileReference; lastKnownFileType = text; path = INSTALL; sourceTree = "<group>"; };
 		B3A302862073F2AA008C1955 /* LICENSES */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSES; sourceTree = "<group>"; };
@@ -2382,7 +2412,17 @@
 			children = (
 				3D208F801182B00300BEAA42 /* MupenGameCore.h */,
 				3D208F811182B00300BEAA42 /* MupenGameCore.m */,
+				B39C29E227A12E690078D028 /* MupenGameCore+Mupen.h */,
+				B39C29E327A12E6A0078D028 /* MupenGameCore+Mupen.m */,
+				B39C29D327A121150078D028 /* MupenGameCore+Controls.h */,
+				B39C29D027A120780078D028 /* MupenGameCore+Controls.m */,
+				B39C29D627A12D0F0078D028 /* MupenGameCore+Cheats.h */,
+				B39C29D727A12D0F0078D028 /* MupenGameCore+Cheats.m */,
+				B39C29DC27A12DA20078D028 /* MupenGameCore+Saves.h */,
+				B39C29DD27A12DA30078D028 /* MupenGameCore+Saves.m */,
 				B35E6C3B207EE3750040709A /* MupenOptions.swift */,
+				B3963E63279E866B00B00117 /* MupenGameCore+Video.swift */,
+				B3963E60279E835300B00117 /* MupenGameCore+Audio.swift */,
 			);
 			name = Classes;
 			sourceTree = "<group>";
@@ -4456,9 +4496,13 @@
 			isa = PBXHeadersBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				B39C29DE27A12DA30078D028 /* MupenGameCore+Saves.h in Headers */,
+				B39C29D827A12D100078D028 /* MupenGameCore+Cheats.h in Headers */,
 				B36EF42F279C5284005C7ED4 /* r4300_core.h in Headers */,
+				B39C29E427A12E6A0078D028 /* MupenGameCore+Mupen.h in Headers */,
 				B3B3B8581DECE9E100602746 /* MupenGameCore.h in Headers */,
 				B3C83E1B279617760020824C /* assem_arm.h in Headers */,
+				B39C29D427A121160078D028 /* MupenGameCore+Controls.h in Headers */,
 				B3B3B8A01DED04CE00602746 /* PVMupen64Plus.h in Headers */,
 				B3C83E1D2796177C0020824C /* assem_x86.h in Headers */,
 			);
@@ -4477,9 +4521,13 @@
 			isa = PBXHeadersBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				B39C29DF27A12DA30078D028 /* MupenGameCore+Saves.h in Headers */,
+				B39C29D927A12D100078D028 /* MupenGameCore+Cheats.h in Headers */,
 				B36EF430279C5286005C7ED4 /* r4300_core.h in Headers */,
+				B39C29E527A12E6A0078D028 /* MupenGameCore+Mupen.h in Headers */,
 				B3B3B9001DED130B00602746 /* MupenGameCore.h in Headers */,
 				B3C83E1C279617780020824C /* assem_arm.h in Headers */,
+				B39C29D527A121160078D028 /* MupenGameCore+Controls.h in Headers */,
 				B3B3B9021DED130B00602746 /* PVMupen64Plus.h in Headers */,
 				B3C83E1F2796177E0020824C /* assem_x86.h in Headers */,
 			);
@@ -5453,6 +5501,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				B3AE1DC7207312BB00AD06D4 /* screenshot.m in Sources */,
+				B3963E61279E835300B00117 /* MupenGameCore+Audio.swift in Sources */,
 				B3AE1DC1207311E900AD06D4 /* vidext.m in Sources */,
 				B3AE1D8B207311E900AD06D4 /* eventloop.m in Sources */,
 				B3AE1DC8207312E000AD06D4 /* main.m in Sources */,
@@ -5472,6 +5521,7 @@
 				B3AE1DA2207311E900AD06D4 /* af_rtc.c in Sources */,
 				B3AE1D93207311E900AD06D4 /* memory.c in Sources */,
 				B3AE1D9A207311E900AD06D4 /* interrupt.c in Sources */,
+				B39C29E627A12E6A0078D028 /* MupenGameCore+Mupen.m in Sources */,
 				B3AE1DB5207311E900AD06D4 /* cic.c in Sources */,
 				B3AE1DBE207311E900AD06D4 /* plugin.c in Sources */,
 				B3AE1D82207311E900AD06D4 /* device.c in Sources */,
@@ -5491,7 +5541,9 @@
 				B3AE1DBC207311E900AD06D4 /* rsp_core.c in Sources */,
 				B3AE1D9F207311E900AD06D4 /* eeprom.c in Sources */,
 				B3AE1DD520732E5200AD06D4 /* unzip.c in Sources */,
+				B39C29D127A120790078D028 /* MupenGameCore+Controls.m in Sources */,
 				B3AE1DB8207311E900AD06D4 /* rumblepak.c in Sources */,
+				B39C29DA27A12D100078D028 /* MupenGameCore+Cheats.m in Sources */,
 				B3AE1DA7207311E900AD06D4 /* input_plugin_compat.c in Sources */,
 				B36EF438279C55A4005C7ED4 /* asm_defines.c in Sources */,
 				B3AE1D84207311E900AD06D4 /* dynamiclib_unix.c in Sources */,
@@ -5499,6 +5551,7 @@
 				B3AE1DD720732E5200AD06D4 /* ioapi.c in Sources */,
 				B3AE1D94207311E900AD06D4 /* files_macos.c in Sources */,
 				B3AE1D83207311E900AD06D4 /* savestates.c in Sources */,
+				B3963E64279E867100B00117 /* MupenGameCore+Video.swift in Sources */,
 				B36EF436279C559A005C7ED4 /* Makefile in Sources */,
 				B3AE1DAB207311E900AD06D4 /* dummy_input.c in Sources */,
 				B3AE1D81207311E900AD06D4 /* bootrom_hle.c in Sources */,
@@ -5517,6 +5570,7 @@
 				B3AE1DB1207311E900AD06D4 /* debugger.c in Sources */,
 				B3AE1DCC207317BE00AD06D4 /* idec.c in Sources */,
 				B3AE1DAA207311E900AD06D4 /* sdl_key_converter.c in Sources */,
+				B39C29E027A12DA30078D028 /* MupenGameCore+Saves.m in Sources */,
 				B3AE1D7D207311E900AD06D4 /* cp1.c in Sources */,
 				B3AE1DC6207311E900AD06D4 /* transferpak.c in Sources */,
 				B3AE1DB7207311E900AD06D4 /* clock_ctime_plus_delta.c in Sources */,
@@ -5627,6 +5681,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				B3AE1E022073378600AD06D4 /* ioapi.c in Sources */,
+				B3963E62279E835300B00117 /* MupenGameCore+Audio.swift in Sources */,
 				B3AE1DED2073378600AD06D4 /* plugin.c in Sources */,
 				B3AE1DDE2073378600AD06D4 /* md5.c in Sources */,
 				B3AE1E1B2073378600AD06D4 /* game_controller.c in Sources */,
@@ -5646,6 +5701,7 @@
 				B3AE1E0A2073378600AD06D4 /* gb_cart.c in Sources */,
 				B3AE1DFE2073378600AD06D4 /* rumblepak.c in Sources */,
 				B3AE1E082073378600AD06D4 /* cheat.c in Sources */,
+				B39C29E727A12E6A0078D028 /* MupenGameCore+Mupen.m in Sources */,
 				B3AE1E072073378600AD06D4 /* ri_controller.c in Sources */,
 				B3AE1E0C2073378600AD06D4 /* cart_rom.c in Sources */,
 				B3AE1E0D2073378600AD06D4 /* tlb.c in Sources */,
@@ -5665,7 +5721,9 @@
 				B3AE1E012073378600AD06D4 /* sram.c in Sources */,
 				B3AE1E1D2073378600AD06D4 /* dummy_video.c in Sources */,
 				B3AE1DFB2073378600AD06D4 /* rsp_core.c in Sources */,
+				B39C29D227A120790078D028 /* MupenGameCore+Controls.m in Sources */,
 				B3AE1DDB2073378600AD06D4 /* main.m in Sources */,
+				B39C29DB27A12D100078D028 /* MupenGameCore+Cheats.m in Sources */,
 				B3AE1DF12073378600AD06D4 /* ai_controller.c in Sources */,
 				B36EF437279C559B005C7ED4 /* Makefile in Sources */,
 				B3AE1E062073378600AD06D4 /* bootrom_hle.c in Sources */,
@@ -5673,6 +5731,7 @@
 				B3AE1E152073378600AD06D4 /* sdl_key_converter.c in Sources */,
 				B3AE1DEE2073378600AD06D4 /* device.c in Sources */,
 				B36EF439279C55A4005C7ED4 /* asm_defines.c in Sources */,
+				B3963E65279E867100B00117 /* MupenGameCore+Video.swift in Sources */,
 				B3AE1E0B2073378600AD06D4 /* mbc3_rtc.c in Sources */,
 				B3AE1DDA2073378600AD06D4 /* eventloop.m in Sources */,
 				B3AE1DE52073378600AD06D4 /* common.c in Sources */,
@@ -5691,6 +5750,7 @@
 				B3AE1E122073378600AD06D4 /* pif.c in Sources */,
 				B3AE1DF92073378600AD06D4 /* dummy_audio.c in Sources */,
 				B3AE1DEA2073378600AD06D4 /* memory.c in Sources */,
+				B39C29E127A12DA30078D028 /* MupenGameCore+Saves.m in Sources */,
 				B3AE1DF62073378600AD06D4 /* rom.c in Sources */,
 				B3AE1E102073378600AD06D4 /* pi_controller.c in Sources */,
 				B3AE1DEB2073378600AD06D4 /* interrupt.c in Sources */,
diff --git a/PVLibrary/PVLibrary/Configuration/PVEmulatorConfiguration.swift b/PVLibrary/PVLibrary/Configuration/PVEmulatorConfiguration.swift
index 38b9103433..7272c17241 100644
--- a/PVLibrary/PVLibrary/Configuration/PVEmulatorConfiguration.swift
+++ b/PVLibrary/PVLibrary/Configuration/PVEmulatorConfiguration.swift
@@ -587,7 +587,12 @@ public extension PVEmulatorConfiguration {
     @objc
     class func m3uFile(forGame game: PVGame) -> URL? {
         let gamePath = path(forGame: game)
-        let gameDirectory = romDirectory(forSystemIdentifier: game.system.identifier)
+        return m3uFile(forURL: gamePath, indentifier: game.system.identifier)
+    }
+    
+    @objc
+    class func m3uFile(forURL gamePath: URL, indentifier: String) -> URL? {
+        let gameDirectory = romDirectory(forSystemIdentifier: indentifier)
         let filenameWithoutExtension = stripDiscNames(fromFilename: gamePath.deletingPathExtension().lastPathComponent)
 
         do {
diff --git a/PVSupport/PVSupport.xcodeproj/project.pbxproj b/PVSupport/PVSupport.xcodeproj/project.pbxproj
index f1260925a4..9e3666c25d 100644
--- a/PVSupport/PVSupport.xcodeproj/project.pbxproj
+++ b/PVSupport/PVSupport.xcodeproj/project.pbxproj
@@ -36,8 +36,6 @@
 		B305EF5B276B4E41003AE510 /* PVEmulatorCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A74C8920522B04001D3D2E /* PVEmulatorCore.swift */; };
 		B305EF5C276B4E41003AE510 /* PViCadeSteelSeriesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB37902187F9A9009D9244 /* PViCadeSteelSeriesController.swift */; };
 		B305EF5D276B4E41003AE510 /* ArchiveSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CDEEBB21D4C3E6000C55F7 /* ArchiveSupport.swift */; };
-		B305EF5E276B4E41003AE510 /* OERingBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = B3FA5D5A1D6B908300060D71 /* OERingBuffer.m */; };
-		B305EF5F276B4E41003AE510 /* OEGameAudio.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACEA69217F748F80031B1C9 /* OEGameAudio.m */; };
 		B305EF60276B4E41003AE510 /* PViCade8BitdoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB378C2187F9A9009D9244 /* PViCade8BitdoController.swift */; };
 		B305EF61276B4E41003AE510 /* PViCadeInputAxis.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB378A2187F9A9009D9244 /* PViCadeInputAxis.swift */; };
 		B305EF62276B4E41003AE510 /* DDLogLevel+CustomStringConvertable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3296E9E26C685450088AC32 /* DDLogLevel+CustomStringConvertable.swift */; };
@@ -139,8 +137,6 @@
 		B34AB5742106DC4100C45F09 /* PVLogEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = B302F89920B73D5F00C5E502 /* PVLogEntry.m */; };
 		B34AB5752106DC4100C45F09 /* PVLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = B302F89E20B73D6400C5E502 /* PVLogging.m */; };
 		B34AB5762106DC4C00C45F09 /* UIDeviceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E6DADD20B7BF8600454DD4 /* UIDeviceExtension.swift */; };
-		B34AB5772106DC5300C45F09 /* OEGameAudio.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACEA69217F748F80031B1C9 /* OEGameAudio.m */; };
-		B34AB5782106DC5300C45F09 /* OERingBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = B3FA5D5A1D6B908300060D71 /* OERingBuffer.m */; };
 		B34AB5792106DC5300C45F09 /* TPCircularBuffer.c in Sources */ = {isa = PBXBuildFile; fileRef = 1ACEA69517F748F80031B1C9 /* TPCircularBuffer.c */; };
 		B34AB57A2106DC6100C45F09 /* CoreOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35E6C36207EE15D0040709A /* CoreOptions.swift */; };
 		B34AB57B2106DC6100C45F09 /* NSObject+PVAbstractAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACEA6A117F74A5A0031B1C9 /* NSObject+PVAbstractAdditions.m */; };
@@ -157,6 +153,12 @@
 		B35E6C38207EE15D0040709A /* CoreOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35E6C36207EE15D0040709A /* CoreOptions.swift */; };
 		B372638426EA015700E95488 /* CocoaLumberjack+swift-log.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3296E8326C67F420088AC32 /* CocoaLumberjack+swift-log.swift */; };
 		B372638526EA015800E95488 /* CocoaLumberjack+swift-log.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3296E8326C67F420088AC32 /* CocoaLumberjack+swift-log.swift */; };
+		B39C29C727A0FADF0078D028 /* OERingBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = B3FA5D5A1D6B908300060D71 /* OERingBuffer.m */; };
+		B39C29C827A0FADF0078D028 /* OERingBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = B3FA5D5A1D6B908300060D71 /* OERingBuffer.m */; };
+		B39C29C927A0FAE00078D028 /* OERingBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = B3FA5D5A1D6B908300060D71 /* OERingBuffer.m */; };
+		B39C29CD27A10EE40078D028 /* OEGameAudio.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACEA69217F748F80031B1C9 /* OEGameAudio.m */; };
+		B39C29CE27A10EE50078D028 /* OEGameAudio.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACEA69217F748F80031B1C9 /* OEGameAudio.m */; };
+		B39C29CF27A10EE50078D028 /* OEGameAudio.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACEA69217F748F80031B1C9 /* OEGameAudio.m */; };
 		B3A4FB5A278FE45D00A65248 /* OEGeometry.h in Headers */ = {isa = PBXBuildFile; fileRef = B3A4FB59278FE45C00A65248 /* OEGeometry.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		B3A4FB5B278FE45D00A65248 /* OEGeometry.h in Headers */ = {isa = PBXBuildFile; fileRef = B3A4FB59278FE45C00A65248 /* OEGeometry.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		B3A4FB5C278FE45D00A65248 /* OEGeometry.h in Headers */ = {isa = PBXBuildFile; fileRef = B3A4FB59278FE45C00A65248 /* OEGeometry.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -195,7 +197,6 @@
 		B3B492CB26B3A96B000B426B /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = B3B492CA26B3A96B000B426B /* Reachability */; };
 		B3C96EA71D62C3BE003F1E93 /* DebugUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A4E718C1A6C699F005CA80F /* DebugUtils.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		B3C96EA81D62C3C3003F1E93 /* OEGameAudio.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACEA69117F748F80031B1C9 /* OEGameAudio.h */; settings = {ATTRIBUTES = (Public, ); }; };
-		B3C96EA91D62C3C3003F1E93 /* OEGameAudio.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACEA69217F748F80031B1C9 /* OEGameAudio.m */; };
 		B3C96EAC1D62C3C3003F1E93 /* TPCircularBuffer.c in Sources */ = {isa = PBXBuildFile; fileRef = 1ACEA69517F748F80031B1C9 /* TPCircularBuffer.c */; };
 		B3C96EAD1D62C3C3003F1E93 /* TPCircularBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACEA69617F748F80031B1C9 /* TPCircularBuffer.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		B3C96EAE1D62C3C7003F1E93 /* NSObject+PVAbstractAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACEA6A017F74A5A0031B1C9 /* NSObject+PVAbstractAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -234,7 +235,6 @@
 		B3F0846D2188CA7A00FF39EE /* CocoaLumberExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F0846B2188CA7A00FF39EE /* CocoaLumberExtensions.swift */; };
 		B3FA5D5B1D6B908300060D71 /* OERingBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = B3FA5D591D6B908300060D71 /* OERingBuffer.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		B3FA5D5C1D6B908300060D71 /* OERingBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = B3FA5D591D6B908300060D71 /* OERingBuffer.h */; settings = {ATTRIBUTES = (Public, ); }; };
-		B3FA5D5E1D6B908300060D71 /* OERingBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = B3FA5D5A1D6B908300060D71 /* OERingBuffer.m */; };
 		B3FA5D631D6B90BD00060D71 /* PVEmulatorCore.h in Headers */ = {isa = PBXBuildFile; fileRef = B3FA5D5F1D6B90BD00060D71 /* PVEmulatorCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		B3FA5D641D6B90BD00060D71 /* PVEmulatorCore.h in Headers */ = {isa = PBXBuildFile; fileRef = B3FA5D5F1D6B90BD00060D71 /* PVEmulatorCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		B3FA5D661D6B90BD00060D71 /* PVEmulatorCore.m in Sources */ = {isa = PBXBuildFile; fileRef = B3FA5D601D6B90BD00060D71 /* PVEmulatorCore.m */; };
@@ -464,13 +464,13 @@
 		1ACEA69017F748F80031B1C9 /* Audio */ = {
 			isa = PBXGroup;
 			children = (
-				B3447E7E218B7E2C00557ACE /* CARingBuffer */,
+				1ACEA69517F748F80031B1C9 /* TPCircularBuffer.c */,
 				1ACEA69117F748F80031B1C9 /* OEGameAudio.h */,
-				1ACEA69217F748F80031B1C9 /* OEGameAudio.m */,
 				B3FA5D591D6B908300060D71 /* OERingBuffer.h */,
-				B3FA5D5A1D6B908300060D71 /* OERingBuffer.m */,
 				1ACEA69617F748F80031B1C9 /* TPCircularBuffer.h */,
-				1ACEA69517F748F80031B1C9 /* TPCircularBuffer.c */,
+				1ACEA69217F748F80031B1C9 /* OEGameAudio.m */,
+				B3FA5D5A1D6B908300060D71 /* OERingBuffer.m */,
+				B3447E7E218B7E2C00557ACE /* CARingBuffer */,
 			);
 			path = Audio;
 			sourceTree = "<group>";
@@ -549,13 +549,13 @@
 		B3447E7E218B7E2C00557ACE /* CARingBuffer */ = {
 			isa = PBXGroup;
 			children = (
+				B3447E84218B7E4B00557ACE /* CAAudioTimeStamp.cpp */,
+				B3447E81218B7E4B00557ACE /* CARingBuffer.cpp */,
 				B3447E83218B7E4B00557ACE /* CAAtomic.h */,
 				B3447E80218B7E4B00557ACE /* CAAudioTimeStamp.h */,
-				B3447E84218B7E4B00557ACE /* CAAudioTimeStamp.cpp */,
 				B3447E82218B7E4B00557ACE /* CAAutoDisposer.h */,
 				B3447E7F218B7E4B00557ACE /* CABitOperations.h */,
 				B3447E85218B7E4B00557ACE /* CARingBuffer.h */,
-				B3447E81218B7E4B00557ACE /* CARingBuffer.cpp */,
 			);
 			path = CARingBuffer;
 			sourceTree = "<group>";
@@ -984,9 +984,8 @@
 				B305EF5B276B4E41003AE510 /* PVEmulatorCore.swift in Sources */,
 				B305EF5C276B4E41003AE510 /* PViCadeSteelSeriesController.swift in Sources */,
 				B305EF5D276B4E41003AE510 /* ArchiveSupport.swift in Sources */,
+				B39C29CF27A10EE50078D028 /* OEGameAudio.m in Sources */,
 				B33FB30F279BE2710013AAD8 /* CoreOptional.swift in Sources */,
-				B305EF5E276B4E41003AE510 /* OERingBuffer.m in Sources */,
-				B305EF5F276B4E41003AE510 /* OEGameAudio.m in Sources */,
 				B305EF60276B4E41003AE510 /* PViCade8BitdoController.swift in Sources */,
 				B305EF61276B4E41003AE510 /* PViCadeInputAxis.swift in Sources */,
 				B305EF62276B4E41003AE510 /* DDLogLevel+CustomStringConvertable.swift in Sources */,
@@ -1025,6 +1024,7 @@
 				B305EF7B276B4E41003AE510 /* TPCircularBuffer.c in Sources */,
 				B305EF7D276B4E41003AE510 /* PVLogging.m in Sources */,
 				B33FB2FE279BE18F0013AAD8 /* CoreOptionEnumValue.swift in Sources */,
+				B39C29C927A0FAE00078D028 /* OERingBuffer.m in Sources */,
 				B33FB302279BE1E40013AAD8 /* OptionDependency.swift in Sources */,
 				B305EF7E276B4E41003AE510 /* PViCadeMocuteController.swift in Sources */,
 				B305EF7F276B4E41003AE510 /* CocoaLumberExtensions.swift in Sources */,
@@ -1047,10 +1047,10 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				B3FA5D5E1D6B908300060D71 /* OERingBuffer.m in Sources */,
 				B3AB37EE21881B7B009D9244 /* PViCadeSteelSeriesController.swift in Sources */,
 				B3CDEEBD21D4C3E6000C55F7 /* ArchiveSupport.swift in Sources */,
 				B33FB30E279BE2710013AAD8 /* CoreOptional.swift in Sources */,
+				B39C29CD27A10EE40078D028 /* OEGameAudio.m in Sources */,
 				B3AB37E421881A83009D9244 /* PViCadeGamepad.swift in Sources */,
 				B35E6C38207EE15D0040709A /* CoreOptions.swift in Sources */,
 				B3AB37EC21881B7B009D9244 /* PViCade8BitdoController.swift in Sources */,
@@ -1087,10 +1087,10 @@
 				B3D0EE21277FE85F002DC0A5 /* HapticsManager.swift in Sources */,
 				B302F8AD20B73D6500C5E502 /* PVLogging.m in Sources */,
 				B3447E91218B7E4B00557ACE /* CAAudioTimeStamp.cpp in Sources */,
-				B3C96EA91D62C3C3003F1E93 /* OEGameAudio.m in Sources */,
 				B3447F9C218C1CD200557ACE /* PVSettingsModel.swift in Sources */,
 				0592894F1DC194FD0012644D /* RealTimeThread.m in Sources */,
 				B33FB2FD279BE18F0013AAD8 /* CoreOptionEnumValue.swift in Sources */,
+				B39C29C827A0FADF0078D028 /* OERingBuffer.m in Sources */,
 				B33FB301279BE1E40013AAD8 /* OptionDependency.swift in Sources */,
 				B302F8A320B73D6500C5E502 /* PVLogEntry.m in Sources */,
 				B3AB37ED21881B7B009D9244 /* PViCadeMocuteController.swift in Sources */,
@@ -1108,9 +1108,8 @@
 				B34AB57F2106DC6100C45F09 /* PVEmulatorCore.swift in Sources */,
 				B3AB37EB21881B7A009D9244 /* PViCadeSteelSeriesController.swift in Sources */,
 				B3CDEEBC21D4C3E6000C55F7 /* ArchiveSupport.swift in Sources */,
+				B39C29CE27A10EE50078D028 /* OEGameAudio.m in Sources */,
 				B33FB30D279BE2710013AAD8 /* CoreOptional.swift in Sources */,
-				B34AB5782106DC5300C45F09 /* OERingBuffer.m in Sources */,
-				B34AB5772106DC5300C45F09 /* OEGameAudio.m in Sources */,
 				B3AB37E921881B7A009D9244 /* PViCade8BitdoController.swift in Sources */,
 				B3AB37DC21881869009D9244 /* PViCadeInputAxis.swift in Sources */,
 				B3296E9F26C685450088AC32 /* DDLogLevel+CustomStringConvertable.swift in Sources */,
@@ -1149,6 +1148,7 @@
 				B34AB5792106DC5300C45F09 /* TPCircularBuffer.c in Sources */,
 				B34AB5752106DC4100C45F09 /* PVLogging.m in Sources */,
 				B33FB2FC279BE18F0013AAD8 /* CoreOptionEnumValue.swift in Sources */,
+				B39C29C727A0FADF0078D028 /* OERingBuffer.m in Sources */,
 				B33FB300279BE1E40013AAD8 /* OptionDependency.swift in Sources */,
 				B3AB37EA21881B7A009D9244 /* PViCadeMocuteController.swift in Sources */,
 				B3F0846C2188CA7A00FF39EE /* CocoaLumberExtensions.swift in Sources */,
diff --git a/PVSupport/Sources/PVSupport/Audio/OEGameAudio.m b/PVSupport/Sources/PVSupport/Audio/OEGameAudio.m
index 9395d695e1..07fb3b8b38 100644
--- a/PVSupport/Sources/PVSupport/Audio/OEGameAudio.m
+++ b/PVSupport/Sources/PVSupport/Audio/OEGameAudio.m
@@ -123,8 +123,7 @@ - (id)init
 }
 
 // Designated Initializer
-- (id)initWithCore:(PVEmulatorCore *)core
-{
+- (id)initWithCore:(PVEmulatorCore *)core {
     self = [super init];
     if(self != nil)
     {
@@ -136,10 +135,6 @@ - (id)initWithCore:(PVEmulatorCore *)core
             ILOG(@"Successfully set audio session to ambient");
         }
 
-		// You can adjust the latency of RemoteIO (and, in fact, any other audio framework) by setting the kAudioSessionProperty_PreferredHardwareIOBufferDuration property
-//		float aBufferLength = 0.005; // In seconds
-//		AudioSessionSetProperty(kAudioSessionProperty_PreferredHardwareIOBufferDuration, sizeof(aBufferLength), &aBufferLength);
-
 		_outputDeviceID = 0;
 		volume = 1;
 
@@ -228,11 +223,10 @@ - (void)createGraph
     desc.componentManufacturer = kAudioUnitManufacturer_Apple;
     
     NSUInteger bufferCount = [gameCore audioBufferCount];
-    if (_contexts)
-        free(_contexts);
+    if (_contexts) { free(_contexts); }
     _contexts = malloc(sizeof(OEGameAudioContext) * bufferCount);
-    for (int i = 0; i < bufferCount; ++i)
-    {
+    
+    for (int i = 0; i < bufferCount; ++i) {
 		TPCircularBufferClear(&([gameCore ringBufferAtIndex:i]->buffer));
 		_contexts[i] = (OEGameAudioContext){&([gameCore ringBufferAtIndex:i]->buffer), (int)[gameCore channelCountForBuffer:i], (int)([gameCore audioBitDepth] /8)};
         
diff --git a/Provenance.xcodeproj/project.pbxproj b/Provenance.xcodeproj/project.pbxproj
index 62080f238f..5b21e98d73 100644
--- a/Provenance.xcodeproj/project.pbxproj
+++ b/Provenance.xcodeproj/project.pbxproj
@@ -4448,7 +4448,7 @@
 				);
 				MARKETING_VERSION = "$(MARKETING_VERSION)";
 				MTL_ENABLE_DEBUG_INFO = NO;
-				PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.topshelf";
+				PRODUCT_BUNDLE_IDENTIFIER = "$(TOPSHELF_PRODUCT_BUNDLE_IDENTIFIER)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
 				SDKROOT = appletvos;
@@ -4694,7 +4694,7 @@
 				);
 				MARKETING_VERSION = "$(MARKETING_VERSION)";
 				MTL_ENABLE_DEBUG_INFO = YES;
-				PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.topshelf";
+				PRODUCT_BUNDLE_IDENTIFIER = "$(TOPSHELF_PRODUCT_BUNDLE_IDENTIFIER)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
 				SDKROOT = appletvos;
@@ -4734,7 +4734,7 @@
 				);
 				MARKETING_VERSION = "$(MARKETING_VERSION)";
 				MTL_ENABLE_DEBUG_INFO = NO;
-				PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.topshelf";
+				PRODUCT_BUNDLE_IDENTIFIER = "$(TOPSHELF_PRODUCT_BUNDLE_IDENTIFIER)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
 				SDKROOT = appletvos;
diff --git a/Provenance/Game Library/UI/GameLaunchingViewController.swift b/Provenance/Game Library/UI/GameLaunchingViewController.swift
index 6dd48d00e0..6a59eae1f2 100644
--- a/Provenance/Game Library/UI/GameLaunchingViewController.swift
+++ b/Provenance/Game Library/UI/GameLaunchingViewController.swift
@@ -393,9 +393,12 @@ extension GameLaunchingViewController where Self: UIViewController {
             return
         }
 
-        let cores = system.cores.sorted(byKeyPath: "projectName").filter({ return PVSettingsModel.shared.debugOptions.experimentalCores || !$0.disabled })
+        let cores = system.cores
+            .sorted(byKeyPath: "projectName")
+            .filter({ PVSettingsModel.shared.debugOptions.experimentalCores || !$0.disabled })
+            .sorted { $0.supportedSystems.count <= $1.supportedSystems.count }
 
-        let coreChoiceAlert = UIAlertController(title: "Multiple cores found", message: "Select which core to use with this game.", preferredStyle: .actionSheet)
+        let coreChoiceAlert = UIAlertController(title: "Multiple cores found", message: "Select which core to use with this game. If not sure, select the 1st option.", preferredStyle: .actionSheet)
         if traitCollection.userInterfaceIdiom == .pad, let senderView = sender as? UIView ?? self.view {
             coreChoiceAlert.popoverPresentationController?.sourceView = senderView
             coreChoiceAlert.popoverPresentationController?.sourceRect = senderView.bounds
@@ -403,7 +406,8 @@ extension GameLaunchingViewController where Self: UIViewController {
 
         for core in cores {
             let action = UIAlertAction(title: core.projectName, style: .default) { [unowned self] _ in
-                let alwaysUseAlert = UIAlertController(title: nil, message: "Open with \(core.projectName)…", preferredStyle: .actionSheet)
+                let message = "Open with \(core.projectName)…"
+                let alwaysUseAlert = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet)
                 if self.traitCollection.userInterfaceIdiom == .pad, let senderView = sender as? UIView ?? self.view {
                     alwaysUseAlert.popoverPresentationController?.sourceView = senderView
                     alwaysUseAlert.popoverPresentationController?.sourceRect = senderView.bounds
diff --git a/Provenance/Game Library/UI/PVConflictViewController.swift b/Provenance/Game Library/UI/PVConflictViewController.swift
index d0f1b84c3c..7a0210a77a 100644
--- a/Provenance/Game Library/UI/PVConflictViewController.swift
+++ b/Provenance/Game Library/UI/PVConflictViewController.swift
@@ -61,7 +61,7 @@ final class PVConflictViewController: UITableViewController {
         rows.bind(to: tableView.rx.items(cellIdentifier: cellIdentifier, cellType: UITableViewCell.self)) { _, row, cell in
             switch row {
             case .conflict(let conflict):
-                cell.textLabel?.text = conflict.path.deletingPathExtension().lastPathComponent
+                cell.textLabel?.text = conflict.path.lastPathComponent
                 cell.accessoryType = .disclosureIndicator
             case .empty(let title):
                 cell.textLabel?.text = title


Test patch: