cocoa tree:

* Support OSX Mojave by ensuring that we always make Cocoa UI
    function calls from the main thread, never from any other QEMU
    thread. This was previously mostly harmless, but on Mojave
    it will cause OSX to terminate the QEMU process.
 -----BEGIN PGP SIGNATURE-----
 
 iQJNBAABCAA3FiEE4aXFk81BneKOgxXPPCUl7RQ2DN4FAlx9VvkZHHBldGVyLm1h
 eWRlbGxAbGluYXJvLm9yZwAKCRA8JSXtFDYM3i84EACeXqjX1vMW00bdy4M9iluc
 ItXX2fRfoFwFcSQR8Zknkf/zlC82chY4msKMNpqabsx6cX3eNDmQEsL8VfYSWLPU
 ejb/8gll8DV4FwUtrhgAb/TRuQzJIFMuOjV+KqZvsNcC+Vhoib1GK0846aFHJWBQ
 c8nBz2D/GZQpQoshpgUOqAoo2utEYV2vVqy2r2d9oK0KlZWHIrakiQ6Zz8P5Pqxg
 +SRB/Uon3h58pm82HPX2KuJyruK/D2SCuZpe6JR5Ykz9h6wO1JVEV7BJJA0Kbt1L
 i9pJKpWfvNoGnP+H705r2FGk4wCNbKZTx9Sj+8LhVMW7iOcBHgKyR9L+b7pHE3+f
 p0+SM0h8QGFMN6GC4w5+LlSD3JDHR+Uc7CERHWot9D2o6tusr/8RG0HVWjEvHAGr
 209yis+kn8Zs5V9t7i/Tic42cc2mr7bzzJ4ZzCFBEl7GZMgQhUluOct2NxfyInNv
 rImz7cHhSbNkcmWjVDxf7bsElHI5dQwKxjU3hCohcvmkVi7AxknDWyVVDiCEX7yc
 AVGPvjykbLHjPhGT1/f4AVQmv1N66j3DdAeV39tqlcjw05QxPYnQ4i0nxlG7gHoU
 cCkIH6BWqdBCu/Divo83rYMZd4SbcrtoiTrSB/emnlK/vB89pLRXzzUyheSRH+ef
 sgFL6sGJvrjVM5i9zRhN/A==
 =eOyH
 -----END PGP SIGNATURE-----

Merge remote-tracking branch 'remotes/pmaydell/tags/pull-cocoa-20190304' into staging

cocoa tree:
 * Support OSX Mojave by ensuring that we always make Cocoa UI
   function calls from the main thread, never from any other QEMU
   thread. This was previously mostly harmless, but on Mojave
   it will cause OSX to terminate the QEMU process.

# gpg: Signature made Mon 04 Mar 2019 16:48:57 GMT
# gpg:                using RSA key E1A5C593CD419DE28E8315CF3C2525ED14360CDE
# gpg:                issuer "peter.maydell@linaro.org"
# gpg: Good signature from "Peter Maydell <peter.maydell@linaro.org>" [ultimate]
# gpg:                 aka "Peter Maydell <pmaydell@gmail.com>" [ultimate]
# gpg:                 aka "Peter Maydell <pmaydell@chiark.greenend.org.uk>" [ultimate]
# Primary key fingerprint: E1A5 C593 CD41 9DE2 8E83  15CF 3C25 25ED 1436 0CDE

* remotes/pmaydell/tags/pull-cocoa-20190304:
  ui/cocoa: Perform UI operations only on the main thread
  ui/cocoa: Subclass NSApplication so we can implement sendEvent
  ui/cocoa: Don't call NSApp sendEvent directly from handleEvent
  ui/cocoa: Move console/device menu creation code up in file
  ui/cocoa: Factor out initial menu creation
  ui/cocoa: Use the pixman image directly in switchSurface
  ui/cocoa: Ensure we have the iothread lock when calling into QEMU

Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
This commit is contained in:
Peter Maydell 2019-03-04 16:50:40 +00:00
commit b6179aaff9

View File

@ -129,6 +129,40 @@ bool stretch_video;
NSTextField *pauseLabel;
NSArray * supportedImageFileTypes;
static QemuSemaphore display_init_sem;
static QemuSemaphore app_started_sem;
// Utility functions to run specified code block with iothread lock held
typedef void (^CodeBlock)(void);
typedef bool (^BoolCodeBlock)(void);
static void with_iothread_lock(CodeBlock block)
{
bool locked = qemu_mutex_iothread_locked();
if (!locked) {
qemu_mutex_lock_iothread();
}
block();
if (!locked) {
qemu_mutex_unlock_iothread();
}
}
static bool bool_with_iothread_lock(BoolCodeBlock block)
{
bool locked = qemu_mutex_iothread_locked();
bool val;
if (!locked) {
qemu_mutex_lock_iothread();
}
val = block();
if (!locked) {
qemu_mutex_unlock_iothread();
}
return val;
}
// Mac to QKeyCode conversion
const int mac_to_qkeycode_map[] = {
[kVK_ANSI_A] = Q_KEY_CODE_A,
@ -294,18 +328,20 @@ static void handleAnyDeviceErrors(Error * err)
NSWindow *fullScreenWindow;
float cx,cy,cw,ch,cdx,cdy;
CGDataProviderRef dataProviderRef;
pixman_image_t *pixman_image;
BOOL modifiers_state[256];
BOOL isMouseGrabbed;
BOOL isFullscreen;
BOOL isAbsoluteEnabled;
BOOL isMouseDeassociated;
}
- (void) switchSurface:(DisplaySurface *)surface;
- (void) switchSurface:(pixman_image_t *)image;
- (void) grabMouse;
- (void) ungrabMouse;
- (void) toggleFullScreen:(id)sender;
- (void) handleMonitorInput:(NSEvent *)event;
- (void) handleEvent:(NSEvent *)event;
- (bool) handleEvent:(NSEvent *)event;
- (bool) handleEventLocked:(NSEvent *)event;
- (void) setAbsoluteEnabled:(BOOL)tIsAbsoluteEnabled;
/* The state surrounding mouse grabbing is potentially confusing.
* isAbsoluteEnabled tracks qemu_input_is_absolute() [ie "is the emulated
@ -351,8 +387,10 @@ QemuCocoaView *cocoaView;
{
COCOA_DEBUG("QemuCocoaView: dealloc\n");
if (dataProviderRef)
if (dataProviderRef) {
CGDataProviderRelease(dataProviderRef);
pixman_image_unref(pixman_image);
}
[super dealloc];
}
@ -479,12 +517,13 @@ QemuCocoaView *cocoaView;
}
}
- (void) switchSurface:(DisplaySurface *)surface
- (void) switchSurface:(pixman_image_t *)image
{
COCOA_DEBUG("QemuCocoaView: switchSurface\n");
int w = surface_width(surface);
int h = surface_height(surface);
int w = pixman_image_get_width(image);
int h = pixman_image_get_height(image);
pixman_format_code_t image_format = pixman_image_get_format(image);
/* cdx == 0 means this is our very first surface, in which case we need
* to recalculate the content dimensions even if it happens to be the size
* of the initial empty window.
@ -502,14 +541,17 @@ QemuCocoaView *cocoaView;
}
// update screenBuffer
if (dataProviderRef)
if (dataProviderRef) {
CGDataProviderRelease(dataProviderRef);
pixman_image_unref(pixman_image);
}
//sync host window color space with guests
screen.bitsPerPixel = surface_bits_per_pixel(surface);
screen.bitsPerComponent = surface_bytes_per_pixel(surface) * 2;
screen.bitsPerPixel = PIXMAN_FORMAT_BPP(image_format);
screen.bitsPerComponent = DIV_ROUND_UP(screen.bitsPerPixel, 8) * 2;
dataProviderRef = CGDataProviderCreateWithData(NULL, surface_data(surface), w * 4 * h, NULL);
pixman_image = image;
dataProviderRef = CGDataProviderCreateWithData(NULL, pixman_image_get_data(image), w * 4 * h, NULL);
// update windows
if (isFullscreen) {
@ -647,10 +689,17 @@ QemuCocoaView *cocoaView;
}
}
- (void) handleEvent:(NSEvent *)event
- (bool) handleEvent:(NSEvent *)event
{
COCOA_DEBUG("QemuCocoaView: handleEvent\n");
return bool_with_iothread_lock(^{
return [self handleEventLocked:event];
});
}
- (bool) handleEventLocked:(NSEvent *)event
{
/* Return true if we handled the event, false if it should be given to OSX */
COCOA_DEBUG("QemuCocoaView: handleEvent\n");
int buttons = 0;
int keycode = 0;
bool mouse_event = false;
@ -720,8 +769,7 @@ QemuCocoaView *cocoaView;
if (keycode == Q_KEY_CODE_F) {
switched_to_fullscreen = true;
}
[NSApp sendEvent:event];
return;
return false;
}
// default
@ -736,12 +784,12 @@ QemuCocoaView *cocoaView;
// enable graphic console
case '1' ... '9':
console_select(key - '0' - 1); /* ascii math */
return;
return true;
// release the mouse grab
case 'g':
[self ungrabMouse];
return;
return true;
}
}
}
@ -758,7 +806,7 @@ QemuCocoaView *cocoaView;
// don't pass the guest a spurious key-up if we treated this
// command-key combo as a host UI action
if (!isMouseGrabbed && ([event modifierFlags] & NSEventModifierFlagCommand)) {
return;
return true;
}
if (qemu_console_is_graphic(NULL)) {
@ -852,7 +900,7 @@ QemuCocoaView *cocoaView;
mouse_event = false;
break;
default:
[NSApp sendEvent:event];
return false;
}
if (mouse_event) {
@ -888,10 +936,11 @@ QemuCocoaView *cocoaView;
qemu_input_queue_rel(dcl->con, INPUT_AXIS_Y, (int)[event deltaY]);
}
} else {
[NSApp sendEvent:event];
return false;
}
qemu_input_event_sync();
}
return true;
}
- (void) grabMouse
@ -945,15 +994,18 @@ QemuCocoaView *cocoaView;
*/
- (void) raiseAllKeys
{
int index;
const int max_index = ARRAY_SIZE(modifiers_state);
with_iothread_lock(^{
int index;
for (index = 0; index < max_index; index++) {
if (modifiers_state[index]) {
modifiers_state[index] = 0;
qemu_input_event_send_key_qcode(dcl->con, index, false);
}
}
});
}
@end
@ -970,7 +1022,6 @@ QemuCocoaView *cocoaView;
#endif
{
}
- (void)startEmulationWithArgc:(int)argc argv:(char**)argv;
- (void)doToggleFullScreen:(id)sender;
- (void)toggleFullScreen:(id)sender;
- (void)showQEMUDoc:(id)sender;
@ -1058,8 +1109,8 @@ QemuCocoaView *cocoaView;
- (void)applicationDidFinishLaunching: (NSNotification *) note
{
COCOA_DEBUG("QemuCocoaAppController: applicationDidFinishLaunching\n");
// launch QEMU, with the global args
[self startEmulationWithArgc:gArgc argv:(char **)gArgv];
/* Tell cocoa_display_init to proceed */
qemu_sem_post(&app_started_sem);
}
- (void)applicationWillTerminate:(NSNotification *)aNotification
@ -1102,15 +1153,6 @@ QemuCocoaView *cocoaView;
[cocoaView raiseAllKeys];
}
- (void)startEmulationWithArgc:(int)argc argv:(char**)argv
{
COCOA_DEBUG("QemuCocoaAppController: startEmulationWithArgc\n");
int status;
status = qemu_main(argc, argv, *_NSGetEnviron());
exit(status);
}
/* We abstract the method called by the Enter Fullscreen menu item
* because Mac OS 10.7 and higher disables it. This is because of the
* menu item's old selector's name toggleFullScreen:
@ -1178,7 +1220,9 @@ QemuCocoaView *cocoaView;
/* Pause the guest */
- (void)pauseQEMU:(id)sender
{
with_iothread_lock(^{
qmp_stop(NULL);
});
[sender setEnabled: NO];
[[[sender menu] itemWithTitle: @"Resume"] setEnabled: YES];
[self displayPause];
@ -1187,7 +1231,9 @@ QemuCocoaView *cocoaView;
/* Resume running the guest operating system */
- (void)resumeQEMU:(id) sender
{
with_iothread_lock(^{
qmp_cont(NULL);
});
[sender setEnabled: NO];
[[[sender menu] itemWithTitle: @"Pause"] setEnabled: YES];
[self removePause];
@ -1215,13 +1261,17 @@ QemuCocoaView *cocoaView;
/* Restarts QEMU */
- (void)restartQEMU:(id)sender
{
with_iothread_lock(^{
qmp_system_reset(NULL);
});
}
/* Powers down QEMU */
- (void)powerDownQEMU:(id)sender
{
with_iothread_lock(^{
qmp_system_powerdown(NULL);
});
}
/* Ejects the media.
@ -1237,9 +1287,11 @@ QemuCocoaView *cocoaView;
return;
}
Error *err = NULL;
__block Error *err = NULL;
with_iothread_lock(^{
qmp_eject(true, [drive cStringUsingEncoding: NSASCIIStringEncoding],
false, NULL, false, false, &err);
});
handleAnyDeviceErrors(err);
}
@ -1271,7 +1323,8 @@ QemuCocoaView *cocoaView;
return;
}
Error *err = NULL;
__block Error *err = NULL;
with_iothread_lock(^{
qmp_blockdev_change_medium(true,
[drive cStringUsingEncoding:
NSASCIIStringEncoding],
@ -1281,6 +1334,7 @@ QemuCocoaView *cocoaView;
true, "raw",
false, 0,
&err);
});
handleAnyDeviceErrors(err);
}
}
@ -1419,49 +1473,29 @@ QemuCocoaView *cocoaView;
// get the throttle percentage
throttle_pct = [sender tag];
with_iothread_lock(^{
cpu_throttle_set(throttle_pct);
});
COCOA_DEBUG("cpu throttling at %d%c\n", cpu_throttle_get_percentage(), '%');
}
@end
@interface QemuApplication : NSApplication
@end
int main (int argc, const char * argv[]) {
gArgc = argc;
gArgv = (char **)argv;
int i;
/* In case we don't need to display a window, let's not do that */
for (i = 1; i < argc; i++) {
const char *opt = argv[i];
if (opt[0] == '-') {
/* Treat --foo the same as -foo. */
if (opt[1] == '-') {
opt++;
}
if (!strcmp(opt, "-h") || !strcmp(opt, "-help") ||
!strcmp(opt, "-vnc") ||
!strcmp(opt, "-nographic") ||
!strcmp(opt, "-version") ||
!strcmp(opt, "-curses") ||
!strcmp(opt, "-display") ||
!strcmp(opt, "-qtest")) {
return qemu_main(gArgc, gArgv, *_NSGetEnviron());
}
@implementation QemuApplication
- (void)sendEvent:(NSEvent *)event
{
COCOA_DEBUG("QemuApplication: sendEvent\n");
if (![cocoaView handleEvent:event]) {
[super sendEvent: event];
}
}
@end
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
// Pull this console process up to being a fully-fledged graphical
// app with a menubar and Dock icon
ProcessSerialNumber psn = { 0, kCurrentProcess };
TransformProcessType(&psn, kProcessTransformToForegroundApplication);
[NSApplication sharedApplication];
static void create_initial_menus(void)
{
// Add menus
NSMenu *menu;
NSMenuItem *menuItem;
@ -1545,97 +1579,8 @@ int main (int argc, const char * argv[]) {
menuItem = [[[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""] autorelease];
[menuItem setSubmenu:menu];
[[NSApp mainMenu] addItem:menuItem];
// Create an Application controller
QemuCocoaAppController *appController = [[QemuCocoaAppController alloc] init];
[NSApp setDelegate:appController];
// Start the main event loop
[NSApp run];
[appController release];
[pool release];
return 0;
}
#pragma mark qemu
static void cocoa_update(DisplayChangeListener *dcl,
int x, int y, int w, int h)
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
COCOA_DEBUG("qemu_cocoa: cocoa_update\n");
NSRect rect;
if ([cocoaView cdx] == 1.0) {
rect = NSMakeRect(x, [cocoaView gscreen].height - y - h, w, h);
} else {
rect = NSMakeRect(
x * [cocoaView cdx],
([cocoaView gscreen].height - y - h) * [cocoaView cdy],
w * [cocoaView cdx],
h * [cocoaView cdy]);
}
[cocoaView setNeedsDisplayInRect:rect];
[pool release];
}
static void cocoa_switch(DisplayChangeListener *dcl,
DisplaySurface *surface)
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
COCOA_DEBUG("qemu_cocoa: cocoa_switch\n");
[cocoaView switchSurface:surface];
[pool release];
}
static void cocoa_refresh(DisplayChangeListener *dcl)
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
COCOA_DEBUG("qemu_cocoa: cocoa_refresh\n");
graphic_hw_update(NULL);
if (qemu_input_is_absolute()) {
if (![cocoaView isAbsoluteEnabled]) {
if ([cocoaView isMouseGrabbed]) {
[cocoaView ungrabMouse];
}
}
[cocoaView setAbsoluteEnabled:YES];
}
NSDate *distantPast;
NSEvent *event;
distantPast = [NSDate distantPast];
do {
event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:distantPast
inMode: NSDefaultRunLoopMode dequeue:YES];
if (event != nil) {
[cocoaView handleEvent:event];
}
} while(event != nil);
[pool release];
}
static void cocoa_cleanup(void)
{
COCOA_DEBUG("qemu_cocoa: cocoa_cleanup\n");
g_free(dcl);
}
static const DisplayChangeListenerOps dcl_ops = {
.dpy_name = "cocoa",
.dpy_gfx_update = cocoa_update,
.dpy_gfx_switch = cocoa_switch,
.dpy_refresh = cocoa_refresh,
};
/* Returns a name for a given console */
static NSString * getConsoleName(QemuConsole * console)
{
@ -1728,14 +1673,191 @@ static void addRemovableDevicesMenuItems(void)
qapi_free_BlockInfoList(pointerToFree);
}
/*
* The startup process for the OSX/Cocoa UI is complicated, because
* OSX insists that the UI runs on the initial main thread, and so we
* need to start a second thread which runs the vl.c qemu_main():
*
* Initial thread: 2nd thread:
* in main():
* create qemu-main thread
* wait on display_init semaphore
* call qemu_main()
* ...
* in cocoa_display_init():
* post the display_init semaphore
* wait on app_started semaphore
* create application, menus, etc
* enter OSX run loop
* in applicationDidFinishLaunching:
* post app_started semaphore
* tell main thread to fullscreen if needed
* [...]
* run qemu main-loop
*
* We do this in two stages so that we don't do the creation of the
* GUI application menus and so on for command line options like --help
* where we want to just print text to stdout and exit immediately.
*/
static void *call_qemu_main(void *opaque)
{
int status;
COCOA_DEBUG("Second thread: calling qemu_main()\n");
status = qemu_main(gArgc, gArgv, *_NSGetEnviron());
COCOA_DEBUG("Second thread: qemu_main() returned, exiting\n");
exit(status);
}
int main (int argc, const char * argv[]) {
QemuThread thread;
COCOA_DEBUG("Entered main()\n");
gArgc = argc;
gArgv = (char **)argv;
qemu_sem_init(&display_init_sem, 0);
qemu_sem_init(&app_started_sem, 0);
qemu_thread_create(&thread, "qemu_main", call_qemu_main,
NULL, QEMU_THREAD_DETACHED);
COCOA_DEBUG("Main thread: waiting for display_init_sem\n");
qemu_sem_wait(&display_init_sem);
COCOA_DEBUG("Main thread: initializing app\n");
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
// Pull this console process up to being a fully-fledged graphical
// app with a menubar and Dock icon
ProcessSerialNumber psn = { 0, kCurrentProcess };
TransformProcessType(&psn, kProcessTransformToForegroundApplication);
[QemuApplication sharedApplication];
create_initial_menus();
/*
* Create the menu entries which depend on QEMU state (for consoles
* and removeable devices). These make calls back into QEMU functions,
* which is OK because at this point we know that the second thread
* holds the iothread lock and is synchronously waiting for us to
* finish.
*/
add_console_menu_entries();
addRemovableDevicesMenuItems();
// Create an Application controller
QemuCocoaAppController *appController = [[QemuCocoaAppController alloc] init];
[NSApp setDelegate:appController];
// Start the main event loop
COCOA_DEBUG("Main thread: entering OSX run loop\n");
[NSApp run];
COCOA_DEBUG("Main thread: left OSX run loop, exiting\n");
[appController release];
[pool release];
return 0;
}
#pragma mark qemu
static void cocoa_update(DisplayChangeListener *dcl,
int x, int y, int w, int h)
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
COCOA_DEBUG("qemu_cocoa: cocoa_update\n");
dispatch_async(dispatch_get_main_queue(), ^{
NSRect rect;
if ([cocoaView cdx] == 1.0) {
rect = NSMakeRect(x, [cocoaView gscreen].height - y - h, w, h);
} else {
rect = NSMakeRect(
x * [cocoaView cdx],
([cocoaView gscreen].height - y - h) * [cocoaView cdy],
w * [cocoaView cdx],
h * [cocoaView cdy]);
}
[cocoaView setNeedsDisplayInRect:rect];
});
[pool release];
}
static void cocoa_switch(DisplayChangeListener *dcl,
DisplaySurface *surface)
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
pixman_image_t *image = surface->image;
COCOA_DEBUG("qemu_cocoa: cocoa_switch\n");
// The DisplaySurface will be freed as soon as this callback returns.
// We take a reference to the underlying pixman image here so it does
// not disappear from under our feet; the switchSurface method will
// deref the old image when it is done with it.
pixman_image_ref(image);
dispatch_async(dispatch_get_main_queue(), ^{
[cocoaView switchSurface:image];
});
[pool release];
}
static void cocoa_refresh(DisplayChangeListener *dcl)
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
COCOA_DEBUG("qemu_cocoa: cocoa_refresh\n");
graphic_hw_update(NULL);
if (qemu_input_is_absolute()) {
dispatch_async(dispatch_get_main_queue(), ^{
if (![cocoaView isAbsoluteEnabled]) {
if ([cocoaView isMouseGrabbed]) {
[cocoaView ungrabMouse];
}
}
[cocoaView setAbsoluteEnabled:YES];
});
}
[pool release];
}
static void cocoa_cleanup(void)
{
COCOA_DEBUG("qemu_cocoa: cocoa_cleanup\n");
g_free(dcl);
}
static const DisplayChangeListenerOps dcl_ops = {
.dpy_name = "cocoa",
.dpy_gfx_update = cocoa_update,
.dpy_gfx_switch = cocoa_switch,
.dpy_refresh = cocoa_refresh,
};
static void cocoa_display_init(DisplayState *ds, DisplayOptions *opts)
{
COCOA_DEBUG("qemu_cocoa: cocoa_display_init\n");
/* Tell main thread to go ahead and create the app and enter the run loop */
qemu_sem_post(&display_init_sem);
qemu_sem_wait(&app_started_sem);
COCOA_DEBUG("cocoa_display_init: app start completed\n");
/* if fullscreen mode is to be used */
if (opts->has_full_screen && opts->full_screen) {
dispatch_async(dispatch_get_main_queue(), ^{
[NSApp activateIgnoringOtherApps: YES];
[(QemuCocoaAppController *)[[NSApplication sharedApplication] delegate] toggleFullScreen: nil];
});
}
dcl = g_malloc0(sizeof(DisplayChangeListener));
@ -1746,17 +1868,6 @@ static void cocoa_display_init(DisplayState *ds, DisplayOptions *opts)
// register cleanup function
atexit(cocoa_cleanup);
/* At this point QEMU has created all the consoles, so we can add View
* menu entries for them.
*/
add_console_menu_entries();
/* Give all removable devices a menu item.
* Has to be called after QEMU has started to
* find out what removable devices it has.
*/
addRemovableDevicesMenuItems();
}
static QemuDisplay qemu_display_cocoa = {