Browse Source

preferences: support changing renderer backend

osy 2 years ago
parent
commit
201a2206f3

+ 1 - 0
Managers/UTMQemu.h

@@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
 @property (nonatomic, readonly) BOOL hasRemoteProcess;
 @property (nonatomic, readonly) BOOL hasRemoteProcess;
 @property (nonatomic, readonly) NSURL *libraryURL;
 @property (nonatomic, readonly) NSURL *libraryURL;
 @property (nonatomic) NSArray<NSString *> *argv;
 @property (nonatomic) NSArray<NSString *> *argv;
+@property (nonatomic, nullable) NSDictionary<NSString *, NSString *> *environment;
 @property (nonatomic) dispatch_semaphore_t done;
 @property (nonatomic) dispatch_semaphore_t done;
 @property (nonatomic) NSInteger status;
 @property (nonatomic) NSInteger status;
 @property (nonatomic) NSInteger fatal;
 @property (nonatomic) NSInteger fatal;

+ 1 - 0
Managers/UTMQemu.m

@@ -181,6 +181,7 @@
     }
     }
     NSFileHandle *standardOutput = self.logging.standardOutput.fileHandleForWriting;
     NSFileHandle *standardOutput = self.logging.standardOutput.fileHandleForWriting;
     NSFileHandle *standardError = self.logging.standardError.fileHandleForWriting;
     NSFileHandle *standardError = self.logging.standardError.fileHandleForWriting;
+    [_connection.remoteObjectProxy setEnvironment:self.environment];
     [[_connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
     [[_connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
         if (error.domain == NSCocoaErrorDomain && error.code == NSXPCConnectionInvalid) {
         if (error.domain == NSCocoaErrorDomain && error.code == NSXPCConnectionInvalid) {
             completion(YES, nil); // inhibit this error since we always see it on quit
             completion(YES, nil); // inhibit this error since we always see it on quit

+ 9 - 0
Managers/UTMQemuSystem.h

@@ -16,11 +16,20 @@
 
 
 #import "UTMQemu.h"
 #import "UTMQemu.h"
 
 
+/// Specify the backend renderer for this VM
+typedef NS_ENUM(NSInteger, UTMQEMURendererBackend) {
+    kQEMURendererBackendDefault = 0,
+    kQEMURendererBackendAngleGL = 1,
+    kQEMURendererBackendAngleMetal = 2,
+    kQEMURendererBackendMax = 3,
+};
+
 NS_ASSUME_NONNULL_BEGIN
 NS_ASSUME_NONNULL_BEGIN
 
 
 @interface UTMQemuSystem : UTMQemu
 @interface UTMQemuSystem : UTMQemu
 
 
 @property (nonatomic, nullable, copy) NSArray<NSURL *> *resources;
 @property (nonatomic, nullable, copy) NSArray<NSURL *> *resources;
+@property (nonatomic) UTMQEMURendererBackend rendererBackend;
 
 
 - (instancetype)init NS_UNAVAILABLE;
 - (instancetype)init NS_UNAVAILABLE;
 - (instancetype)initWithArguments:(NSArray<NSString *> *)arguments NS_UNAVAILABLE;
 - (instancetype)initWithArguments:(NSArray<NSString *> *)arguments NS_UNAVAILABLE;

+ 34 - 1
Managers/UTMQemuSystem.m

@@ -32,19 +32,32 @@
 static void *start_qemu(void *args) {
 static void *start_qemu(void *args) {
     UTMQemuSystem *self = (__bridge_transfer UTMQemuSystem *)args;
     UTMQemuSystem *self = (__bridge_transfer UTMQemuSystem *)args;
     NSArray<NSString *> *qemuArgv = self.argv;
     NSArray<NSString *> *qemuArgv = self.argv;
+    NSMutableArray<NSString *> *environment = [NSMutableArray arrayWithCapacity:self.environment.count];
     
     
     NSCAssert(self->_qemu_init != NULL, @"Started thread with invalid function.");
     NSCAssert(self->_qemu_init != NULL, @"Started thread with invalid function.");
     NSCAssert(self->_qemu_main_loop != NULL, @"Started thread with invalid function.");
     NSCAssert(self->_qemu_main_loop != NULL, @"Started thread with invalid function.");
     NSCAssert(self->_qemu_cleanup != NULL, @"Started thread with invalid function.");
     NSCAssert(self->_qemu_cleanup != NULL, @"Started thread with invalid function.");
     NSCAssert(qemuArgv, @"Started thread with invalid argv.");
     NSCAssert(qemuArgv, @"Started thread with invalid argv.");
     
     
+    /* set up environment variables */
+    [self.environment enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) {
+        NSString *combined = [NSString stringWithFormat:@"%@=%@", key, value];
+        [environment addObject:combined];
+        setenv(key.UTF8String, value.UTF8String, 1);
+    }];
+    NSUInteger envc = environment.count;
+    const char *envp[envc + 1];
+    for (NSUInteger i = 0; i < envc; i++) {
+        envp[i] = environment[i].UTF8String;
+    }
+    envp[envc] = NULL;
+    
     int argc = (int)qemuArgv.count + 1;
     int argc = (int)qemuArgv.count + 1;
     const char *argv[argc];
     const char *argv[argc];
     argv[0] = "qemu-system";
     argv[0] = "qemu-system";
     for (int i = 0; i < qemuArgv.count; i++) {
     for (int i = 0; i < qemuArgv.count; i++) {
         argv[i+1] = [qemuArgv[i] UTF8String];
         argv[i+1] = [qemuArgv[i] UTF8String];
     }
     }
-    const char *envp[] = { NULL };
     self->_qemu_init(argc, argv, envp);
     self->_qemu_init(argc, argv, envp);
     self->_qemu_main_loop();
     self->_qemu_main_loop();
     self->_qemu_cleanup();
     self->_qemu_cleanup();
@@ -53,6 +66,26 @@ static void *start_qemu(void *args) {
     return NULL;
     return NULL;
 }
 }
 
 
+- (void)setRendererBackend:(UTMQEMURendererBackend)rendererBackend {
+    _rendererBackend = rendererBackend;
+    switch (rendererBackend) {
+        case kQEMURendererBackendAngleGL:
+            self.environment = @{@"ANGLE_DEFAULT_PLATFORM": @"gl"};
+            break;
+        case kQEMURendererBackendAngleMetal:
+            self.environment = @{@"ANGLE_DEFAULT_PLATFORM": @"metal"};
+            break;
+        case kQEMURendererBackendDefault:
+#if TARGET_OS_IPHONE
+            // on iOS, use Metal as the default because it is more stable
+            self.environment = @{@"ANGLE_DEFAULT_PLATFORM": @"metal"};
+#endif
+            break;
+        default:
+            break;
+    }
+}
+
 - (instancetype)initWithArguments:(NSArray<NSString *> *)arguments architecture:(nonnull NSString *)architecture {
 - (instancetype)initWithArguments:(NSArray<NSString *> *)arguments architecture:(nonnull NSString *)architecture {
     self = [super initWithArguments:arguments];
     self = [super initWithArguments:arguments];
     if (self) {
     if (self) {

+ 11 - 0
Managers/UTMQemuVirtualMachine.m

@@ -45,6 +45,7 @@ NSString *const kSuspendSnapshotName = @"suspend";
 @property (nonatomic, nullable) dispatch_semaphore_t qemuDidConnectEvent;
 @property (nonatomic, nullable) dispatch_semaphore_t qemuDidConnectEvent;
 @property (nonatomic) BOOL changeCursorRequestInProgress;
 @property (nonatomic) BOOL changeCursorRequestInProgress;
 @property (nonatomic, nullable) NSString *lastErrorLine;
 @property (nonatomic, nullable) NSString *lastErrorLine;
+@property (nonatomic, readonly) UTMQEMURendererBackend rendererBackend;
 
 
 @end
 @end
 
 
@@ -68,6 +69,15 @@ NSString *const kSuspendSnapshotName = @"suspend";
     _isGuestToolsInstallRequested = isGuestToolsInstallRequested;
     _isGuestToolsInstallRequested = isGuestToolsInstallRequested;
 }
 }
 
 
+- (UTMQEMURendererBackend)rendererBackend {
+    NSInteger value = [NSUserDefaults.standardUserDefaults integerForKey:@"QEMURendererBackend"];
+    if (value >= 0 && value < kQEMURendererBackendMax) {
+        return (UTMQEMURendererBackend)value;
+    } else {
+        return kQEMURendererBackendDefault;
+    }
+}
+
 - (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration packageURL:(NSURL *)packageURL {
 - (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration packageURL:(NSURL *)packageURL {
     self = [super initWithConfiguration:configuration packageURL:packageURL];
     self = [super initWithConfiguration:configuration packageURL:packageURL];
     if (self) {
     if (self) {
@@ -219,6 +229,7 @@ NSString *const kSuspendSnapshotName = @"suspend";
     __weak typeof(self) weakSelf = self;
     __weak typeof(self) weakSelf = self;
     __block NSError *qemuStartError = nil;
     __block NSError *qemuStartError = nil;
     dispatch_semaphore_t spiceConnectOrErrorEvent = dispatch_semaphore_create(0);
     dispatch_semaphore_t spiceConnectOrErrorEvent = dispatch_semaphore_create(0);
+    self.system.rendererBackend = self.rendererBackend;
     [self.system startWithCompletion:^(BOOL success, NSString *msg){
     [self.system startWithCompletion:^(BOOL success, NSString *msg){
         typeof(self) _self = weakSelf;
         typeof(self) _self = weakSelf;
         if (!_self) {
         if (!_self) {

+ 28 - 0
Platform/iOS/Settings.bundle/Root.plist

@@ -94,6 +94,34 @@
 			<key>DefaultValue</key>
 			<key>DefaultValue</key>
 			<false/>
 			<false/>
 		</dict>
 		</dict>
+		<dict>
+			<key>Type</key>
+			<string>PSGroupSpecifier</string>
+			<key>Title</key>
+			<string>Graphics</string>
+		</dict>
+		<dict>
+			<key>Type</key>
+			<string>PSMultiValueSpecifier</string>
+			<key>Title</key>
+			<string>Renderer Backend</string>
+			<key>Key</key>
+			<string>QEMURendererBackend</string>
+			<key>DefaultValue</key>
+			<integer>0</integer>
+			<key>Titles</key>
+			<array>
+				<string>Default</string>
+				<string>ANGLE (OpenGL)</string>
+				<string>ANGLE (Metal)</string>
+			</array>
+			<key>Values</key>
+			<array>
+				<integer>0</integer>
+				<integer>1</integer>
+				<integer>2</integer>
+			</array>
+		</dict>
 		<dict>
 		<dict>
 			<key>Type</key>
 			<key>Type</key>
 			<string>PSGroupSpecifier</string>
 			<string>PSGroupSpecifier</string>

+ 7 - 0
Platform/macOS/SettingsView.swift

@@ -28,6 +28,7 @@ struct SettingsView: View {
     @AppStorage("IsCapsLockKey") var isCapsLockKey = false
     @AppStorage("IsCapsLockKey") var isCapsLockKey = false
     @AppStorage("NoSaveScreenshot") var isNoSaveScreenshot = false
     @AppStorage("NoSaveScreenshot") var isNoSaveScreenshot = false
     @AppStorage("InvertScroll") var isInvertScroll = false
     @AppStorage("InvertScroll") var isInvertScroll = false
+    @AppStorage("QEMURendererBackend") var qemuRendererBackend: UTMQEMURendererBackend = .qemuRendererBackendDefault
     
     
     var body: some View {
     var body: some View {
         Form {
         Form {
@@ -56,6 +57,11 @@ struct SettingsView: View {
                 Toggle(isOn: $isNoSaveScreenshot) {
                 Toggle(isOn: $isNoSaveScreenshot) {
                     Text("Do not save VM screenshot to disk")
                     Text("Do not save VM screenshot to disk")
                 }.help("If enabled, any existing screenshot will be deleted the next time the VM is started.")
                 }.help("If enabled, any existing screenshot will be deleted the next time the VM is started.")
+                Picker("QEMU Renderer Backend", selection: $qemuRendererBackend) {
+                    Text("Default").tag(UTMQEMURendererBackend.qemuRendererBackendDefault)
+                    Text("ANGLE (OpenGL)").tag(UTMQEMURendererBackend.qemuRendererBackendAngleGL)
+                    Text("ANGLE (Metal)").tag(UTMQEMURendererBackend.qemuRendererBackendAngleMetal)
+                }.help("By default, the best renderer for this device will be used. You can override this with to always use a specific renderer. This only applies to QEMU VMs with GPU accelerated graphics.")
             }
             }
             Section(header: Text("Input")) {
             Section(header: Text("Input")) {
                 Toggle(isOn: $isCtrlRightClick, label: {
                 Toggle(isOn: $isCtrlRightClick, label: {
@@ -93,6 +99,7 @@ extension UserDefaults {
     @objc dynamic var IsCapsLockKey: Bool { false }
     @objc dynamic var IsCapsLockKey: Bool { false }
     @objc dynamic var NoSaveScreenshot: Bool { false }
     @objc dynamic var NoSaveScreenshot: Bool { false }
     @objc dynamic var InvertScroll: Bool { false }
     @objc dynamic var InvertScroll: Bool { false }
+    @objc dynamic var QEMURendererBackend: Int { 0 }
 }
 }
 
 
 @available(macOS 11, *)
 @available(macOS 11, *)

+ 5 - 1
QEMUHelper/QEMUHelper.m

@@ -26,6 +26,8 @@
 
 
 @implementation QEMUHelper
 @implementation QEMUHelper
 
 
+@synthesize environment;
+
 - (instancetype)init {
 - (instancetype)init {
     if (self = [super init]) {
     if (self = [super init]) {
         self.urls = [NSMutableArray array];
         self.urls = [NSMutableArray array];
@@ -112,7 +114,9 @@
     task.arguments = newArgv;
     task.arguments = newArgv;
     task.standardOutput = standardOutput;
     task.standardOutput = standardOutput;
     task.standardError = standardError;
     task.standardError = standardError;
-    //task.environment = @{@"DYLD_LIBRARY_PATH": libraryPath.path};
+    if (self.environment) {
+        task.environment = self.environment;
+    }
     task.qualityOfService = NSQualityOfServiceUserInitiated;
     task.qualityOfService = NSQualityOfServiceUserInitiated;
     task.terminationHandler = ^(NSTask *task) {
     task.terminationHandler = ^(NSTask *task) {
         BOOL normalExit = task.terminationReason == NSTaskTerminationReasonExit && task.terminationStatus == 0;
         BOOL normalExit = task.terminationReason == NSTaskTerminationReasonExit && task.terminationStatus == 0;

+ 2 - 0
QEMUHelper/QEMUHelperProtocol.h

@@ -21,6 +21,8 @@ NS_ASSUME_NONNULL_BEGIN
 // The protocol that this service will vend as its API. This header file will also need to be visible to the process hosting the service.
 // The protocol that this service will vend as its API. This header file will also need to be visible to the process hosting the service.
 @protocol QEMUHelperProtocol
 @protocol QEMUHelperProtocol
 
 
+@property (nonatomic, nullable) NSDictionary<NSString *, NSString *> *environment;
+
 - (void)accessDataWithBookmark:(NSData *)bookmark securityScoped:(BOOL)securityScoped completion:(void(^)(BOOL, NSData * _Nullable, NSString * _Nullable))completion;
 - (void)accessDataWithBookmark:(NSData *)bookmark securityScoped:(BOOL)securityScoped completion:(void(^)(BOOL, NSData * _Nullable, NSString * _Nullable))completion;
 - (void)stopAccessingPath:(nullable NSString *)path;
 - (void)stopAccessingPath:(nullable NSString *)path;
 - (void)startQemu:(NSString *)binName standardOutput:(NSFileHandle *)standardOutput standardError:(NSFileHandle *)standardError libraryBookmark:(NSData *)libBookmark argv:(NSArray<NSString *> *)argv onExit:(void(^)(BOOL,NSString *))onExit;
 - (void)startQemu:(NSString *)binName standardOutput:(NSFileHandle *)standardOutput standardError:(NSFileHandle *)standardError libraryBookmark:(NSData *)libBookmark argv:(NSArray<NSString *> *)argv onExit:(void(^)(BOOL,NSString *))onExit;

+ 5 - 6
QEMULauncher/Bootstrap.c

@@ -68,8 +68,7 @@ static int loadQemu(const char *dylibPath, qemu_main_t *funcs) {
     return 0;
     return 0;
 }
 }
 
 
-static void __attribute__((noreturn)) runQemu(qemu_main_t *funcs, int argc, const char **argv) {
-    const char *envp[] = { NULL };
+static void __attribute__((noreturn)) runQemu(qemu_main_t *funcs, int argc, const char **argv, const char **envp) {
     if (funcs->qemu_init) {
     if (funcs->qemu_init) {
         funcs->qemu_init(argc, argv, envp);
         funcs->qemu_init(argc, argv, envp);
     }
     }
@@ -85,7 +84,7 @@ static void __attribute__((noreturn)) runQemu(qemu_main_t *funcs, int argc, cons
     exit(0);
     exit(0);
 }
 }
 
 
-pid_t startQemuFork(const char *dylibPath, int argc, const char **argv, int newStdout, int newStderr) {
+pid_t startQemuFork(const char *dylibPath, int argc, const char **argv, const char **envp, int newStdout, int newStderr) {
     qemu_main_t funcs = {};
     qemu_main_t funcs = {};
     int res = loadQemu(dylibPath, &funcs);
     int res = loadQemu(dylibPath, &funcs);
     if (res < 0) {
     if (res < 0) {
@@ -103,16 +102,16 @@ pid_t startQemuFork(const char *dylibPath, int argc, const char **argv, int newS
     // set thread QoS
     // set thread QoS
     pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0);
     pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0);
     // launch qemu
     // launch qemu
-    runQemu(&funcs, argc, argv);
+    runQemu(&funcs, argc, argv, envp);
 }
 }
 
 
-int startQemuProcess(const char *dylibPath, int argc, const char **argv) {
+int startQemuProcess(const char *dylibPath, int argc, const char **argv, const char **envp) {
     qemu_main_t funcs = {};
     qemu_main_t funcs = {};
     int res = loadQemu(dylibPath, &funcs);
     int res = loadQemu(dylibPath, &funcs);
     if (res < 0) {
     if (res < 0) {
         return res;
         return res;
     }
     }
     // launch qemu
     // launch qemu
-    runQemu(&funcs, argc, argv);
+    runQemu(&funcs, argc, argv, envp);
     return 0;
     return 0;
 }
 }

+ 2 - 2
QEMULauncher/Bootstrap.h

@@ -19,7 +19,7 @@
 
 
 #include <unistd.h>
 #include <unistd.h>
 
 
-pid_t startQemuFork(const char *dylibPath, int argc, const char **argv, int newStdout, int newStderr);
-int startQemuProcess(const char *dylibPath, int argc, const char **argv);
+pid_t startQemuFork(const char *dylibPath, int argc, const char **argv, const char **envp, int newStdout, int newStderr);
+int startQemuProcess(const char *dylibPath, int argc, const char **argv, const char **envp);
 
 
 #endif /* Bootstrap_h */
 #endif /* Bootstrap_h */

+ 3 - 1
QEMULauncher/main.c

@@ -17,10 +17,12 @@
 #include <stdio.h>
 #include <stdio.h>
 #include "Bootstrap.h"
 #include "Bootstrap.h"
 
 
+extern const char **environ;
+
 int main(int argc, const char * argv[]) {
 int main(int argc, const char * argv[]) {
     if (argc < 2) {
     if (argc < 2) {
         fprintf(stderr, "usage: QEMULauncher dylibPath qemuArguments...\n");
         fprintf(stderr, "usage: QEMULauncher dylibPath qemuArguments...\n");
         return 1;
         return 1;
     }
     }
-    return startQemuProcess(argv[1], argc - 1, &argv[1]);
+    return startQemuProcess(argv[1], argc - 1, &argv[1], environ);
 }
 }