mirror of
https://github.com/utmapp/UTM.git
synced 2026-10-12 02:24:01 +00:00
MTL_DEBUG_LAYER, MTL_DEBUG_LAYER_ERROR_MODE and ANGLE_METAL_DEBUG_BINDINGS
were set alongside the loggers in -setHasDebugLog:, but they are not
loggers: they turn on Metal API validation, which wraps every Metal object
and validates every call. The environment is inherited by everything QEMU
spawns, so the cost lands both in the virglrenderer render-server workers
and in QEMU's own per-frame scanout blit, which is encoded and committed
with the BQL held and therefore stalls every vCPU behind it.
Measured with the Cyberpunk 2077 built-in benchmark on an aarch64 Windows
guest (Neptune/D3D12 on the D3DMetal backend, 6 vCPUs), as the median frame
time over the steady part of the flythrough: 55.5-56.7 fps with "Debug Log"
enabled against 67.3 fps with it disabled -- a 19% loss purely from the
validation layers. That makes verbose logging unusable for exactly the
reports it is most needed for, since anyone asked to reproduce a
performance problem with logging on measures a different machine.
Move the three variables behind a separate hidden opt-in so debug logging
stays a logging switch:
defaults write com.utmapp.UTM GraphicsValidation -bool YES
It still requires "Debug Log", because the validation output goes to the
log.
Assisted-by: Claude:claude-opus-5
300 lines
12 KiB
Objective-C
300 lines
12 KiB
Objective-C
//
|
|
// Copyright © 2020 osy. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
#import <dlfcn.h>
|
|
#import <TargetConditionals.h>
|
|
#import "UTMLogging.h"
|
|
#import "UTMQemuSystem.h"
|
|
|
|
@interface UTMQemuSystem ()
|
|
|
|
@property (nonatomic) NSString *architecture;
|
|
@property (nonatomic) NSMutableDictionary<NSString *, NSString *> *mutableEnvironment;
|
|
|
|
@end
|
|
|
|
@implementation UTMQemuSystem {
|
|
int (*_qemu_init)(int, const char *[], const char *[]);
|
|
void (*_qemu_main_loop)(void);
|
|
void (*_qemu_cleanup)(void);
|
|
}
|
|
|
|
static int startQemu(UTMProcess *process, int argc, const char *argv[], const char *envp[]) {
|
|
UTMQemuSystem *self = (UTMQemuSystem *)process;
|
|
int ret = self->_qemu_init(argc, argv, envp);
|
|
if (ret != 0) {
|
|
return ret;
|
|
}
|
|
self->_qemu_main_loop();
|
|
self->_qemu_cleanup();
|
|
return 0;
|
|
}
|
|
|
|
- (void)setRendererBackend:(UTMQEMURendererBackend)rendererBackend {
|
|
_rendererBackend = rendererBackend;
|
|
switch (rendererBackend) {
|
|
case kQEMURendererBackendDefault:
|
|
case kQEMURendererBackendAngleMetal:
|
|
self.mutableEnvironment[@"ANGLE_DEFAULT_PLATFORM"] = @"metal";
|
|
break;
|
|
case kQEMURendererBackendAngleGL:
|
|
default:
|
|
[self.mutableEnvironment removeObjectForKey:@"ANGLE_DEFAULT_PLATFORM"];
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void)setVulkanDriver:(UTMQEMUVulkanDriver)vulkanDriver {
|
|
NSURL *vulkanIcds = [[NSBundle.mainBundle URLForResource:@"vulkan" withExtension:nil] URLByAppendingPathComponent:@"icd.d" isDirectory:YES];
|
|
NSURL *driver;
|
|
switch (vulkanDriver) {
|
|
case kQEMUVulkanDriverDefault:
|
|
case kQEMUVulkanDriverMoltenVK:
|
|
driver = [vulkanIcds URLByAppendingPathComponent:@"MoltenVK_icd.json"];
|
|
break;
|
|
case kQEMUVulkanDriverKosmicKrisp:
|
|
driver = [vulkanIcds URLByAppendingPathComponent:@"kosmickrisp_mesa_icd.json"];
|
|
break;
|
|
case kQEMUVulkanDriverDisabled:
|
|
default:
|
|
driver = nil;
|
|
break;
|
|
}
|
|
if (driver) {
|
|
self.mutableEnvironment[@"VK_DRIVER_FILES"] = driver.path;
|
|
self.resources = [self.resources arrayByAddingObject:driver];
|
|
_vulkanDriver = vulkanDriver;
|
|
} else {
|
|
[self.mutableEnvironment removeObjectForKey:@"VK_DRIVER_FILES"];
|
|
_vulkanDriver = kQEMUVulkanDriverDisabled;
|
|
}
|
|
}
|
|
|
|
- (void)setDirectXDriver:(UTMQEMUDirectXDriver)directXDriver {
|
|
NSString *backend;
|
|
NSString *frameworkName;
|
|
NSURL *library;
|
|
if (@available(macOS 13, iOS 16, *)) {
|
|
} else {
|
|
return;
|
|
}
|
|
NSURL *bundleURL = NSBundle.mainBundle.bundleURL;
|
|
#if TARGET_OS_OSX
|
|
NSURL *contentsURL = [bundleURL URLByAppendingPathComponent:@"Contents" isDirectory:YES];
|
|
NSString *versionPath = @"Versions/A/";
|
|
#else
|
|
NSURL *contentsURL = bundleURL;
|
|
NSString *versionPath = @"";
|
|
#endif
|
|
NSURL *frameworksURL = [contentsURL URLByAppendingPathComponent:@"Frameworks" isDirectory:YES];
|
|
NSString *d3dMetal = @"D3DMetal";
|
|
NSURL *d3dMetalURL = [frameworksURL URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.framework/%@%@", d3dMetal, versionPath, d3dMetal] isDirectory:NO];
|
|
if (directXDriver == kQEMUDirectXDriverDefault) {
|
|
if (@available(macOS 14, *)) {
|
|
if ([NSFileManager.defaultManager fileExistsAtPath:d3dMetalURL.path]) {
|
|
directXDriver = kQEMUDirectXDriverD3DMetal;
|
|
}
|
|
}
|
|
}
|
|
switch (directXDriver) {
|
|
case kQEMUDirectXDriverDefault:
|
|
case kQEMUDirectXDriverDXMT:
|
|
backend = @"dxmt";
|
|
frameworkName = @"dxmt-native";
|
|
break;
|
|
case kQEMUDirectXDriverD3DMetal:
|
|
backend = @"d3dmetal";
|
|
frameworkName = @"d3dmetal-native";
|
|
break;
|
|
case kQEMUDirectXDriverDisabled:
|
|
default:
|
|
break;
|
|
}
|
|
if (frameworkName) {
|
|
library = [frameworksURL URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.framework/%@%@", frameworkName, versionPath, frameworkName] isDirectory:NO];
|
|
if (![NSFileManager.defaultManager fileExistsAtPath:library.path]) {
|
|
UTMLog(@"DirectX driver '%@' is not available in this build", backend);
|
|
backend = nil;
|
|
library = nil;
|
|
} else if (directXDriver == kQEMUDirectXDriverD3DMetal) {
|
|
self.mutableEnvironment[@"D3DMETAL_FRAMEWORK_PATH"] = d3dMetalURL.path;
|
|
self.resources = [self.resources arrayByAddingObject:d3dMetalURL];
|
|
}
|
|
}
|
|
if (backend && library) {
|
|
self.mutableEnvironment[@"NPT_D3D11_LIBRARY_PATH"] = library.path;
|
|
self.mutableEnvironment[@"NPT_D3D12_LIBRARY_PATH"] = library.path;
|
|
self.mutableEnvironment[@"NPT_DXGI_LIBRARY_PATH"] = library.path;
|
|
self.mutableEnvironment[@"NPT_BACKEND"] = backend;
|
|
if (directXDriver == kQEMUDirectXDriverD3DMetal) {
|
|
self.mutableEnvironment[@"NPT_CAPSET_D3D12"] = @"1";
|
|
}
|
|
self.resources = [self.resources arrayByAddingObject:library];
|
|
_directXDriver = directXDriver;
|
|
} else {
|
|
[self.mutableEnvironment removeObjectForKey:@"NPT_D3D11_LIBRARY_PATH"];
|
|
[self.mutableEnvironment removeObjectForKey:@"NPT_D3D12_LIBRARY_PATH"];
|
|
[self.mutableEnvironment removeObjectForKey:@"NPT_DXGI_LIBRARY_PATH"];
|
|
[self.mutableEnvironment removeObjectForKey:@"NPT_BACKEND"];
|
|
[self.mutableEnvironment removeObjectForKey:@"NPT_CAPSET_D3D12"];
|
|
[self.mutableEnvironment removeObjectForKey:@"D3DMETAL_FRAMEWORK_PATH"];
|
|
_directXDriver = kQEMUDirectXDriverDisabled;
|
|
}
|
|
}
|
|
|
|
- (void)setShmemDirectoryURL:(NSURL *)shmemDirectoryURL {
|
|
_shmemDirectoryURL = shmemDirectoryURL;
|
|
if (shmemDirectoryURL) {
|
|
self.mutableEnvironment[@"XDG_RUNTIME_DIR"] = shmemDirectoryURL.path;
|
|
} else {
|
|
[self.mutableEnvironment removeObjectForKey:@"XDG_RUNTIME_DIR"];
|
|
}
|
|
}
|
|
|
|
- (void)setAppSandboxGroupId:(NSString *)appSandboxGroupId {
|
|
_appSandboxGroupId = appSandboxGroupId;
|
|
if (appSandboxGroupId) {
|
|
self.mutableEnvironment[@"APP_SANDBOX_GROUP_ID"] = appSandboxGroupId;
|
|
} else {
|
|
[self.mutableEnvironment removeObjectForKey:@"APP_SANDBOX_GROUP_ID"];
|
|
}
|
|
}
|
|
|
|
- (NSPipe *)standardOutput {
|
|
return self.logging.standardOutput;
|
|
}
|
|
|
|
- (void)setStandardOutput:(NSPipe *)standardOutput {
|
|
[self doesNotRecognizeSelector:_cmd];
|
|
}
|
|
|
|
- (NSPipe *)standardError {
|
|
return self.logging.standardError;
|
|
}
|
|
|
|
- (void)setStandardError:(NSPipe *)standardError {
|
|
[self doesNotRecognizeSelector:_cmd];
|
|
}
|
|
|
|
- (void)setLogging:(QEMULogging *)logging {
|
|
_logging = logging;
|
|
[logging writeLine:[NSString stringWithFormat:@"Launching: qemu-system-%@%@\n", self.architecture, self.arguments]];
|
|
[logging writeLine:@"Environment variables:\n"];
|
|
[self.environment enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) {
|
|
[logging writeLine:[NSString stringWithFormat:@" %@=%@\n", key, value]];
|
|
}];
|
|
}
|
|
|
|
- (void)setHasDebugLog:(BOOL)hasDebugLog {
|
|
_hasDebugLog = hasDebugLog;
|
|
/* The graphics API validation layers are not loggers: they wrap every
|
|
* Metal object and validate every call, which costs far more than the
|
|
* drivers they wrap (measured: a 3D benchmark loses ~20% of its frame
|
|
* rate with them on). Verbose logging must stay usable for performance
|
|
* bug reports, so validation is a separate opt-in:
|
|
* defaults write com.utmapp.UTM GraphicsValidation -bool YES
|
|
*/
|
|
BOOL hasGraphicsValidation = hasDebugLog &&
|
|
[NSUserDefaults.standardUserDefaults boolForKey:@"GraphicsValidation"];
|
|
if (hasGraphicsValidation) {
|
|
self.mutableEnvironment[@"MTL_DEBUG_LAYER"] = @"1";
|
|
self.mutableEnvironment[@"MTL_DEBUG_LAYER_ERROR_MODE"] = @"nslog";
|
|
self.mutableEnvironment[@"ANGLE_METAL_DEBUG_BINDINGS"] = @"1";
|
|
} else {
|
|
[self.mutableEnvironment removeObjectForKey:@"MTL_DEBUG_LAYER"];
|
|
[self.mutableEnvironment removeObjectForKey:@"MTL_DEBUG_LAYER_ERROR_MODE"];
|
|
[self.mutableEnvironment removeObjectForKey:@"ANGLE_METAL_DEBUG_BINDINGS"];
|
|
}
|
|
if (hasDebugLog) {
|
|
#if TARGET_OS_OSX // FIXME: verbose logging is broken on iOS
|
|
self.mutableEnvironment[@"G_MESSAGES_DEBUG"] = @"all";
|
|
#endif
|
|
self.mutableEnvironment[@"VK_LOADER_DEBUG"] = @"all";
|
|
self.mutableEnvironment[@"VIRGL_LOG_LEVEL"] = @"debug";
|
|
self.mutableEnvironment[@"MESA_DEBUG"] = @"1";
|
|
self.mutableEnvironment[@"MVK_CONFIG_LOG_LEVEL"] = @"4";
|
|
self.mutableEnvironment[@"MVK_DEBUG"] = @"1";
|
|
self.mutableEnvironment[@"ANGLE_ENABLE_DEBUG_TRACE"] = @"1";
|
|
} else {
|
|
[self.mutableEnvironment removeObjectForKey:@"G_MESSAGES_DEBUG"];
|
|
[self.mutableEnvironment removeObjectForKey:@"VK_LOADER_DEBUG"];
|
|
[self.mutableEnvironment removeObjectForKey:@"VIRGL_LOG_LEVEL"];
|
|
[self.mutableEnvironment removeObjectForKey:@"MESA_DEBUG"];
|
|
[self.mutableEnvironment removeObjectForKey:@"MVK_CONFIG_LOG_LEVEL"];
|
|
[self.mutableEnvironment removeObjectForKey:@"MVK_DEBUG"];
|
|
[self.mutableEnvironment removeObjectForKey:@"ANGLE_ENABLE_DEBUG_TRACE"];
|
|
}
|
|
}
|
|
|
|
- (NSDictionary<NSString *,NSString *> *)environment {
|
|
return self.mutableEnvironment;
|
|
}
|
|
|
|
- (instancetype)initWithArguments:(NSArray<NSString *> *)arguments architecture:(nonnull NSString *)architecture {
|
|
self = [super initWithArguments:arguments];
|
|
if (self) {
|
|
self.entry = startQemu;
|
|
self.architecture = architecture;
|
|
self.mutableEnvironment = [NSMutableDictionary dictionary];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (BOOL)didLoadDylib:(void *)handle {
|
|
_qemu_init = dlsym(handle, "qemu_init");
|
|
_qemu_main_loop = dlsym(handle, "qemu_main_loop");
|
|
_qemu_cleanup = dlsym(handle, "qemu_cleanup");
|
|
return (_qemu_init != NULL) && (_qemu_main_loop != NULL) && (_qemu_cleanup != NULL);
|
|
}
|
|
|
|
- (void)startQemuWithCompletion:(nonnull void (^)(NSError * _Nullable))completion {
|
|
dispatch_group_t group = dispatch_group_create();
|
|
for (NSURL *resourceURL in self.resources) {
|
|
NSData *bookmark = self.remoteBookmarks[resourceURL];
|
|
BOOL securityScoped = YES;
|
|
if (!bookmark) {
|
|
bookmark = [resourceURL bookmarkDataWithOptions:0
|
|
includingResourceValuesForKeys:nil
|
|
relativeToURL:nil
|
|
error:nil];
|
|
securityScoped = NO;
|
|
}
|
|
if (bookmark) {
|
|
dispatch_group_enter(group);
|
|
[self accessDataWithBookmark:bookmark securityScoped:securityScoped completion:^(BOOL success, NSData *bookmark, NSString *path) {
|
|
if (!success) {
|
|
UTMLog(@"Access QEMU bookmark failed for: %@", path);
|
|
}
|
|
dispatch_group_leave(group);
|
|
}];
|
|
}
|
|
}
|
|
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
|
|
NSString *name = [NSString stringWithFormat:@"qemu-%@-softmmu", self.architecture];
|
|
[self startProcess:name completion:completion];
|
|
}
|
|
|
|
- (void)stopQemu {
|
|
[self stopProcess];
|
|
}
|
|
|
|
/// Called by superclass
|
|
- (void)processHasExited:(NSInteger)exitCode message:(nullable NSString *)message {
|
|
[self.launcherDelegate qemuLauncher:self didExitWithExitCode:exitCode message:message];
|
|
}
|
|
|
|
@end
|