diff options
Diffstat (limited to 'ui')
88 files changed, 36836 insertions, 0 deletions
diff --git a/ui/clipboard.c b/ui/clipboard.c new file mode 100644 index 000000000..d7b008d62 --- /dev/null +++ b/ui/clipboard.c @@ -0,0 +1,133 @@ +#include "qemu/osdep.h" +#include "ui/clipboard.h" + +static NotifierList clipboard_notifiers = + NOTIFIER_LIST_INITIALIZER(clipboard_notifiers); + +static QemuClipboardInfo *cbinfo[QEMU_CLIPBOARD_SELECTION__COUNT]; + +void qemu_clipboard_peer_register(QemuClipboardPeer *peer) +{ + notifier_list_add(&clipboard_notifiers, &peer->update); +} + +void qemu_clipboard_peer_unregister(QemuClipboardPeer *peer) +{ + int i; + + for (i = 0; i < QEMU_CLIPBOARD_SELECTION__COUNT; i++) { + qemu_clipboard_peer_release(peer, i); + } + + notifier_remove(&peer->update); +} + +bool qemu_clipboard_peer_owns(QemuClipboardPeer *peer, + QemuClipboardSelection selection) +{ + QemuClipboardInfo *info = qemu_clipboard_info(selection); + + return info && info->owner == peer; +} + +void qemu_clipboard_peer_release(QemuClipboardPeer *peer, + QemuClipboardSelection selection) +{ + g_autoptr(QemuClipboardInfo) info = NULL; + + if (qemu_clipboard_peer_owns(peer, selection)) { + /* set empty clipboard info */ + info = qemu_clipboard_info_new(NULL, selection); + qemu_clipboard_update(info); + } +} + +void qemu_clipboard_update(QemuClipboardInfo *info) +{ + g_autoptr(QemuClipboardInfo) old = NULL; + assert(info->selection < QEMU_CLIPBOARD_SELECTION__COUNT); + + notifier_list_notify(&clipboard_notifiers, info); + + old = cbinfo[info->selection]; + cbinfo[info->selection] = qemu_clipboard_info_ref(info); +} + +QemuClipboardInfo *qemu_clipboard_info(QemuClipboardSelection selection) +{ + assert(selection < QEMU_CLIPBOARD_SELECTION__COUNT); + + return cbinfo[selection]; +} + +QemuClipboardInfo *qemu_clipboard_info_new(QemuClipboardPeer *owner, + QemuClipboardSelection selection) +{ + QemuClipboardInfo *info = g_new0(QemuClipboardInfo, 1); + + info->owner = owner; + info->selection = selection; + info->refcount = 1; + + return info; +} + +QemuClipboardInfo *qemu_clipboard_info_ref(QemuClipboardInfo *info) +{ + info->refcount++; + return info; +} + +void qemu_clipboard_info_unref(QemuClipboardInfo *info) +{ + uint32_t type; + + if (!info) { + return; + } + + info->refcount--; + if (info->refcount > 0) { + return; + } + + for (type = 0; type < QEMU_CLIPBOARD_TYPE__COUNT; type++) { + g_free(info->types[type].data); + } + g_free(info); +} + +void qemu_clipboard_request(QemuClipboardInfo *info, + QemuClipboardType type) +{ + if (info->types[type].data || + info->types[type].requested || + !info->types[type].available || + !info->owner) + return; + + info->types[type].requested = true; + info->owner->request(info, type); +} + +void qemu_clipboard_set_data(QemuClipboardPeer *peer, + QemuClipboardInfo *info, + QemuClipboardType type, + uint32_t size, + const void *data, + bool update) +{ + if (!info || + info->owner != peer) { + return; + } + + g_free(info->types[type].data); + info->types[type].data = g_memdup(data, size); + info->types[type].size = size; + info->types[type].available = true; + + if (update) { + qemu_clipboard_update(info); + } +} diff --git a/ui/cocoa.m b/ui/cocoa.m new file mode 100644 index 000000000..68a630218 --- /dev/null +++ b/ui/cocoa.m @@ -0,0 +1,2063 @@ +/* + * QEMU Cocoa CG display driver + * + * Copyright (c) 2008 Mike Kronenberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" + +#import <Cocoa/Cocoa.h> +#include <crt_externs.h> + +#include "qemu-common.h" +#include "ui/clipboard.h" +#include "ui/console.h" +#include "ui/input.h" +#include "ui/kbd-state.h" +#include "sysemu/sysemu.h" +#include "sysemu/runstate.h" +#include "sysemu/cpu-throttle.h" +#include "qapi/error.h" +#include "qapi/qapi-commands-block.h" +#include "qapi/qapi-commands-machine.h" +#include "qapi/qapi-commands-misc.h" +#include "sysemu/blockdev.h" +#include "qemu-version.h" +#include "qemu/cutils.h" +#include "qemu/main-loop.h" +#include "qemu/module.h" +#include <Carbon/Carbon.h> +#include "hw/core/cpu.h" + +#ifndef MAC_OS_X_VERSION_10_13 +#define MAC_OS_X_VERSION_10_13 101300 +#endif + +/* 10.14 deprecates NSOnState and NSOffState in favor of + * NSControlStateValueOn/Off, which were introduced in 10.13. + * Define for older versions + */ +#if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_13 +#define NSControlStateValueOn NSOnState +#define NSControlStateValueOff NSOffState +#endif + +//#define DEBUG + +#ifdef DEBUG +#define COCOA_DEBUG(...) { (void) fprintf (stdout, __VA_ARGS__); } +#else +#define COCOA_DEBUG(...) ((void) 0) +#endif + +#define cgrect(nsrect) (*(CGRect *)&(nsrect)) + +typedef struct { + int width; + int height; +} QEMUScreen; + +static void cocoa_update(DisplayChangeListener *dcl, + int x, int y, int w, int h); + +static void cocoa_switch(DisplayChangeListener *dcl, + DisplaySurface *surface); + +static void cocoa_refresh(DisplayChangeListener *dcl); + +static NSWindow *normalWindow, *about_window; +static const DisplayChangeListenerOps dcl_ops = { + .dpy_name = "cocoa", + .dpy_gfx_update = cocoa_update, + .dpy_gfx_switch = cocoa_switch, + .dpy_refresh = cocoa_refresh, +}; +static DisplayChangeListener dcl = { + .ops = &dcl_ops, +}; +static int last_buttons; +static int cursor_hide = 1; + +static int gArgc; +static char **gArgv; +static bool stretch_video; +static NSTextField *pauseLabel; +static NSArray * supportedImageFileTypes; + +static QemuSemaphore display_init_sem; +static QemuSemaphore app_started_sem; +static bool allow_events; + +static NSInteger cbchangecount = -1; +static QemuClipboardInfo *cbinfo; +static QemuEvent cbevent; + +// 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 +static const int mac_to_qkeycode_map[] = { + [kVK_ANSI_A] = Q_KEY_CODE_A, + [kVK_ANSI_B] = Q_KEY_CODE_B, + [kVK_ANSI_C] = Q_KEY_CODE_C, + [kVK_ANSI_D] = Q_KEY_CODE_D, + [kVK_ANSI_E] = Q_KEY_CODE_E, + [kVK_ANSI_F] = Q_KEY_CODE_F, + [kVK_ANSI_G] = Q_KEY_CODE_G, + [kVK_ANSI_H] = Q_KEY_CODE_H, + [kVK_ANSI_I] = Q_KEY_CODE_I, + [kVK_ANSI_J] = Q_KEY_CODE_J, + [kVK_ANSI_K] = Q_KEY_CODE_K, + [kVK_ANSI_L] = Q_KEY_CODE_L, + [kVK_ANSI_M] = Q_KEY_CODE_M, + [kVK_ANSI_N] = Q_KEY_CODE_N, + [kVK_ANSI_O] = Q_KEY_CODE_O, + [kVK_ANSI_P] = Q_KEY_CODE_P, + [kVK_ANSI_Q] = Q_KEY_CODE_Q, + [kVK_ANSI_R] = Q_KEY_CODE_R, + [kVK_ANSI_S] = Q_KEY_CODE_S, + [kVK_ANSI_T] = Q_KEY_CODE_T, + [kVK_ANSI_U] = Q_KEY_CODE_U, + [kVK_ANSI_V] = Q_KEY_CODE_V, + [kVK_ANSI_W] = Q_KEY_CODE_W, + [kVK_ANSI_X] = Q_KEY_CODE_X, + [kVK_ANSI_Y] = Q_KEY_CODE_Y, + [kVK_ANSI_Z] = Q_KEY_CODE_Z, + + [kVK_ANSI_0] = Q_KEY_CODE_0, + [kVK_ANSI_1] = Q_KEY_CODE_1, + [kVK_ANSI_2] = Q_KEY_CODE_2, + [kVK_ANSI_3] = Q_KEY_CODE_3, + [kVK_ANSI_4] = Q_KEY_CODE_4, + [kVK_ANSI_5] = Q_KEY_CODE_5, + [kVK_ANSI_6] = Q_KEY_CODE_6, + [kVK_ANSI_7] = Q_KEY_CODE_7, + [kVK_ANSI_8] = Q_KEY_CODE_8, + [kVK_ANSI_9] = Q_KEY_CODE_9, + + [kVK_ANSI_Grave] = Q_KEY_CODE_GRAVE_ACCENT, + [kVK_ANSI_Minus] = Q_KEY_CODE_MINUS, + [kVK_ANSI_Equal] = Q_KEY_CODE_EQUAL, + [kVK_Delete] = Q_KEY_CODE_BACKSPACE, + [kVK_CapsLock] = Q_KEY_CODE_CAPS_LOCK, + [kVK_Tab] = Q_KEY_CODE_TAB, + [kVK_Return] = Q_KEY_CODE_RET, + [kVK_ANSI_LeftBracket] = Q_KEY_CODE_BRACKET_LEFT, + [kVK_ANSI_RightBracket] = Q_KEY_CODE_BRACKET_RIGHT, + [kVK_ANSI_Backslash] = Q_KEY_CODE_BACKSLASH, + [kVK_ANSI_Semicolon] = Q_KEY_CODE_SEMICOLON, + [kVK_ANSI_Quote] = Q_KEY_CODE_APOSTROPHE, + [kVK_ANSI_Comma] = Q_KEY_CODE_COMMA, + [kVK_ANSI_Period] = Q_KEY_CODE_DOT, + [kVK_ANSI_Slash] = Q_KEY_CODE_SLASH, + [kVK_Space] = Q_KEY_CODE_SPC, + + [kVK_ANSI_Keypad0] = Q_KEY_CODE_KP_0, + [kVK_ANSI_Keypad1] = Q_KEY_CODE_KP_1, + [kVK_ANSI_Keypad2] = Q_KEY_CODE_KP_2, + [kVK_ANSI_Keypad3] = Q_KEY_CODE_KP_3, + [kVK_ANSI_Keypad4] = Q_KEY_CODE_KP_4, + [kVK_ANSI_Keypad5] = Q_KEY_CODE_KP_5, + [kVK_ANSI_Keypad6] = Q_KEY_CODE_KP_6, + [kVK_ANSI_Keypad7] = Q_KEY_CODE_KP_7, + [kVK_ANSI_Keypad8] = Q_KEY_CODE_KP_8, + [kVK_ANSI_Keypad9] = Q_KEY_CODE_KP_9, + [kVK_ANSI_KeypadDecimal] = Q_KEY_CODE_KP_DECIMAL, + [kVK_ANSI_KeypadEnter] = Q_KEY_CODE_KP_ENTER, + [kVK_ANSI_KeypadPlus] = Q_KEY_CODE_KP_ADD, + [kVK_ANSI_KeypadMinus] = Q_KEY_CODE_KP_SUBTRACT, + [kVK_ANSI_KeypadMultiply] = Q_KEY_CODE_KP_MULTIPLY, + [kVK_ANSI_KeypadDivide] = Q_KEY_CODE_KP_DIVIDE, + [kVK_ANSI_KeypadEquals] = Q_KEY_CODE_KP_EQUALS, + [kVK_ANSI_KeypadClear] = Q_KEY_CODE_NUM_LOCK, + + [kVK_UpArrow] = Q_KEY_CODE_UP, + [kVK_DownArrow] = Q_KEY_CODE_DOWN, + [kVK_LeftArrow] = Q_KEY_CODE_LEFT, + [kVK_RightArrow] = Q_KEY_CODE_RIGHT, + + [kVK_Help] = Q_KEY_CODE_INSERT, + [kVK_Home] = Q_KEY_CODE_HOME, + [kVK_PageUp] = Q_KEY_CODE_PGUP, + [kVK_PageDown] = Q_KEY_CODE_PGDN, + [kVK_End] = Q_KEY_CODE_END, + [kVK_ForwardDelete] = Q_KEY_CODE_DELETE, + + [kVK_Escape] = Q_KEY_CODE_ESC, + + /* The Power key can't be used directly because the operating system uses + * it. This key can be emulated by using it in place of another key such as + * F1. Don't forget to disable the real key binding. + */ + /* [kVK_F1] = Q_KEY_CODE_POWER, */ + + [kVK_F1] = Q_KEY_CODE_F1, + [kVK_F2] = Q_KEY_CODE_F2, + [kVK_F3] = Q_KEY_CODE_F3, + [kVK_F4] = Q_KEY_CODE_F4, + [kVK_F5] = Q_KEY_CODE_F5, + [kVK_F6] = Q_KEY_CODE_F6, + [kVK_F7] = Q_KEY_CODE_F7, + [kVK_F8] = Q_KEY_CODE_F8, + [kVK_F9] = Q_KEY_CODE_F9, + [kVK_F10] = Q_KEY_CODE_F10, + [kVK_F11] = Q_KEY_CODE_F11, + [kVK_F12] = Q_KEY_CODE_F12, + [kVK_F13] = Q_KEY_CODE_PRINT, + [kVK_F14] = Q_KEY_CODE_SCROLL_LOCK, + [kVK_F15] = Q_KEY_CODE_PAUSE, + + // JIS keyboards only + [kVK_JIS_Yen] = Q_KEY_CODE_YEN, + [kVK_JIS_Underscore] = Q_KEY_CODE_RO, + [kVK_JIS_KeypadComma] = Q_KEY_CODE_KP_COMMA, + [kVK_JIS_Eisu] = Q_KEY_CODE_MUHENKAN, + [kVK_JIS_Kana] = Q_KEY_CODE_HENKAN, + + /* + * The eject and volume keys can't be used here because they are handled at + * a lower level than what an Application can see. + */ +}; + +static int cocoa_keycode_to_qemu(int keycode) +{ + if (ARRAY_SIZE(mac_to_qkeycode_map) <= keycode) { + error_report("(cocoa) warning unknown keycode 0x%x", keycode); + return 0; + } + return mac_to_qkeycode_map[keycode]; +} + +/* Displays an alert dialog box with the specified message */ +static void QEMU_Alert(NSString *message) +{ + NSAlert *alert; + alert = [NSAlert new]; + [alert setMessageText: message]; + [alert runModal]; +} + +/* Handles any errors that happen with a device transaction */ +static void handleAnyDeviceErrors(Error * err) +{ + if (err) { + QEMU_Alert([NSString stringWithCString: error_get_pretty(err) + encoding: NSASCIIStringEncoding]); + error_free(err); + } +} + +/* + ------------------------------------------------------ + QemuCocoaView + ------------------------------------------------------ +*/ +@interface QemuCocoaView : NSView +{ + QEMUScreen screen; + NSWindow *fullScreenWindow; + float cx,cy,cw,ch,cdx,cdy; + pixman_image_t *pixman_image; + QKbdState *kbd; + BOOL isMouseGrabbed; + BOOL isFullscreen; + BOOL isAbsoluteEnabled; +} +- (void) switchSurface:(pixman_image_t *)image; +- (void) grabMouse; +- (void) ungrabMouse; +- (void) toggleFullScreen:(id)sender; +- (void) handleMonitorInput:(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 + * pointing device an absolute-position one?"], but is only updated on + * next refresh. + * isMouseGrabbed tracks whether GUI events are directed to the guest; + * it controls whether special keys like Cmd get sent to the guest, + * and whether we capture the mouse when in non-absolute mode. + */ +- (BOOL) isMouseGrabbed; +- (BOOL) isAbsoluteEnabled; +- (float) cdx; +- (float) cdy; +- (QEMUScreen) gscreen; +- (void) raiseAllKeys; +@end + +QemuCocoaView *cocoaView; + +@implementation QemuCocoaView +- (id)initWithFrame:(NSRect)frameRect +{ + COCOA_DEBUG("QemuCocoaView: initWithFrame\n"); + + self = [super initWithFrame:frameRect]; + if (self) { + + screen.width = frameRect.size.width; + screen.height = frameRect.size.height; + kbd = qkbd_state_init(dcl.con); + + } + return self; +} + +- (void) dealloc +{ + COCOA_DEBUG("QemuCocoaView: dealloc\n"); + + if (pixman_image) { + pixman_image_unref(pixman_image); + } + + qkbd_state_free(kbd); + [super dealloc]; +} + +- (BOOL) isOpaque +{ + return YES; +} + +- (BOOL) screenContainsPoint:(NSPoint) p +{ + return (p.x > -1 && p.x < screen.width && p.y > -1 && p.y < screen.height); +} + +/* Get location of event and convert to virtual screen coordinate */ +- (CGPoint) screenLocationOfEvent:(NSEvent *)ev +{ + NSWindow *eventWindow = [ev window]; + // XXX: Use CGRect and -convertRectFromScreen: to support macOS 10.10 + CGRect r = CGRectZero; + r.origin = [ev locationInWindow]; + if (!eventWindow) { + if (!isFullscreen) { + return [[self window] convertRectFromScreen:r].origin; + } else { + CGPoint locationInSelfWindow = [[self window] convertRectFromScreen:r].origin; + CGPoint loc = [self convertPoint:locationInSelfWindow fromView:nil]; + if (stretch_video) { + loc.x /= cdx; + loc.y /= cdy; + } + return loc; + } + } else if ([[self window] isEqual:eventWindow]) { + if (!isFullscreen) { + return r.origin; + } else { + CGPoint loc = [self convertPoint:r.origin fromView:nil]; + if (stretch_video) { + loc.x /= cdx; + loc.y /= cdy; + } + return loc; + } + } else { + return [[self window] convertRectFromScreen:[eventWindow convertRectToScreen:r]].origin; + } +} + +- (void) hideCursor +{ + if (!cursor_hide) { + return; + } + [NSCursor hide]; +} + +- (void) unhideCursor +{ + if (!cursor_hide) { + return; + } + [NSCursor unhide]; +} + +- (void) drawRect:(NSRect) rect +{ + COCOA_DEBUG("QemuCocoaView: drawRect\n"); + + // get CoreGraphic context + CGContextRef viewContextRef = [[NSGraphicsContext currentContext] CGContext]; + + CGContextSetInterpolationQuality (viewContextRef, kCGInterpolationNone); + CGContextSetShouldAntialias (viewContextRef, NO); + + // draw screen bitmap directly to Core Graphics context + if (!pixman_image) { + // Draw request before any guest device has set up a framebuffer: + // just draw an opaque black rectangle + CGContextSetRGBFillColor(viewContextRef, 0, 0, 0, 1.0); + CGContextFillRect(viewContextRef, NSRectToCGRect(rect)); + } else { + int w = pixman_image_get_width(pixman_image); + int h = pixman_image_get_height(pixman_image); + int bitsPerPixel = PIXMAN_FORMAT_BPP(pixman_image_get_format(pixman_image)); + int stride = pixman_image_get_stride(pixman_image); + CGDataProviderRef dataProviderRef = CGDataProviderCreateWithData( + NULL, + pixman_image_get_data(pixman_image), + stride * h, + NULL + ); + CGImageRef imageRef = CGImageCreate( + w, //width + h, //height + DIV_ROUND_UP(bitsPerPixel, 8) * 2, //bitsPerComponent + bitsPerPixel, //bitsPerPixel + stride, //bytesPerRow + CGColorSpaceCreateWithName(kCGColorSpaceSRGB), //colorspace + kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst, //bitmapInfo + dataProviderRef, //provider + NULL, //decode + 0, //interpolate + kCGRenderingIntentDefault //intent + ); + // selective drawing code (draws only dirty rectangles) (OS X >= 10.4) + const NSRect *rectList; + NSInteger rectCount; + int i; + CGImageRef clipImageRef; + CGRect clipRect; + + [self getRectsBeingDrawn:&rectList count:&rectCount]; + for (i = 0; i < rectCount; i++) { + clipRect.origin.x = rectList[i].origin.x / cdx; + clipRect.origin.y = (float)h - (rectList[i].origin.y + rectList[i].size.height) / cdy; + clipRect.size.width = rectList[i].size.width / cdx; + clipRect.size.height = rectList[i].size.height / cdy; + clipImageRef = CGImageCreateWithImageInRect( + imageRef, + clipRect + ); + CGContextDrawImage (viewContextRef, cgrect(rectList[i]), clipImageRef); + CGImageRelease (clipImageRef); + } + CGImageRelease (imageRef); + CGDataProviderRelease(dataProviderRef); + } +} + +- (void) setContentDimensions +{ + COCOA_DEBUG("QemuCocoaView: setContentDimensions\n"); + + if (isFullscreen) { + cdx = [[NSScreen mainScreen] frame].size.width / (float)screen.width; + cdy = [[NSScreen mainScreen] frame].size.height / (float)screen.height; + + /* stretches video, but keeps same aspect ratio */ + if (stretch_video == true) { + /* use smallest stretch value - prevents clipping on sides */ + if (MIN(cdx, cdy) == cdx) { + cdy = cdx; + } else { + cdx = cdy; + } + } else { /* No stretching */ + cdx = cdy = 1; + } + cw = screen.width * cdx; + ch = screen.height * cdy; + cx = ([[NSScreen mainScreen] frame].size.width - cw) / 2.0; + cy = ([[NSScreen mainScreen] frame].size.height - ch) / 2.0; + } else { + cx = 0; + cy = 0; + cw = screen.width; + ch = screen.height; + cdx = 1.0; + cdy = 1.0; + } +} + +- (void) updateUIInfo +{ + NSSize frameSize; + QemuUIInfo info; + + if (!qemu_console_is_graphic(dcl.con)) { + return; + } + + if ([self window]) { + NSDictionary *description = [[[self window] screen] deviceDescription]; + CGDirectDisplayID display = [[description objectForKey:@"NSScreenNumber"] unsignedIntValue]; + NSSize screenSize = [[[self window] screen] frame].size; + CGSize screenPhysicalSize = CGDisplayScreenSize(display); + + frameSize = isFullscreen ? screenSize : [self frame].size; + info.width_mm = frameSize.width / screenSize.width * screenPhysicalSize.width; + info.height_mm = frameSize.height / screenSize.height * screenPhysicalSize.height; + } else { + frameSize = [self frame].size; + info.width_mm = 0; + info.height_mm = 0; + } + + info.xoff = 0; + info.yoff = 0; + info.width = frameSize.width; + info.height = frameSize.height; + + dpy_set_ui_info(dcl.con, &info); +} + +- (void)viewDidMoveToWindow +{ + [self updateUIInfo]; +} + +- (void) switchSurface:(pixman_image_t *)image +{ + COCOA_DEBUG("QemuCocoaView: switchSurface\n"); + + int w = pixman_image_get_width(image); + int h = pixman_image_get_height(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. + */ + bool isResize = (w != screen.width || h != screen.height || cdx == 0.0); + + int oldh = screen.height; + if (isResize) { + // Resize before we trigger the redraw, or we'll redraw at the wrong size + COCOA_DEBUG("switchSurface: new size %d x %d\n", w, h); + screen.width = w; + screen.height = h; + [self setContentDimensions]; + [self setFrame:NSMakeRect(cx, cy, cw, ch)]; + } + + // update screenBuffer + if (pixman_image) { + pixman_image_unref(pixman_image); + } + + pixman_image = image; + + // update windows + if (isFullscreen) { + [[fullScreenWindow contentView] setFrame:[[NSScreen mainScreen] frame]]; + [normalWindow setFrame:NSMakeRect([normalWindow frame].origin.x, [normalWindow frame].origin.y - h + oldh, w, h + [normalWindow frame].size.height - oldh) display:NO animate:NO]; + } else { + if (qemu_name) + [normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s", qemu_name]]; + [normalWindow setFrame:NSMakeRect([normalWindow frame].origin.x, [normalWindow frame].origin.y - h + oldh, w, h + [normalWindow frame].size.height - oldh) display:YES animate:NO]; + } + + if (isResize) { + [normalWindow center]; + } +} + +- (void) toggleFullScreen:(id)sender +{ + COCOA_DEBUG("QemuCocoaView: toggleFullScreen\n"); + + if (isFullscreen) { // switch from fullscreen to desktop + isFullscreen = FALSE; + [self ungrabMouse]; + [self setContentDimensions]; + [fullScreenWindow close]; + [normalWindow setContentView: self]; + [normalWindow makeKeyAndOrderFront: self]; + [NSMenu setMenuBarVisible:YES]; + } else { // switch from desktop to fullscreen + isFullscreen = TRUE; + [normalWindow orderOut: nil]; /* Hide the window */ + [self grabMouse]; + [self setContentDimensions]; + [NSMenu setMenuBarVisible:NO]; + fullScreenWindow = [[NSWindow alloc] initWithContentRect:[[NSScreen mainScreen] frame] + styleMask:NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:NO]; + [fullScreenWindow setAcceptsMouseMovedEvents: YES]; + [fullScreenWindow setHasShadow:NO]; + [fullScreenWindow setBackgroundColor: [NSColor blackColor]]; + [self setFrame:NSMakeRect(cx, cy, cw, ch)]; + [[fullScreenWindow contentView] addSubview: self]; + [fullScreenWindow makeKeyAndOrderFront:self]; + } +} + +- (void) toggleKey: (int)keycode { + qkbd_state_key_event(kbd, keycode, !qkbd_state_key_get(kbd, keycode)); +} + +// Does the work of sending input to the monitor +- (void) handleMonitorInput:(NSEvent *)event +{ + int keysym = 0; + int control_key = 0; + + // if the control key is down + if ([event modifierFlags] & NSEventModifierFlagControl) { + control_key = 1; + } + + /* translates Macintosh keycodes to QEMU's keysym */ + + int without_control_translation[] = { + [0 ... 0xff] = 0, // invalid key + + [kVK_UpArrow] = QEMU_KEY_UP, + [kVK_DownArrow] = QEMU_KEY_DOWN, + [kVK_RightArrow] = QEMU_KEY_RIGHT, + [kVK_LeftArrow] = QEMU_KEY_LEFT, + [kVK_Home] = QEMU_KEY_HOME, + [kVK_End] = QEMU_KEY_END, + [kVK_PageUp] = QEMU_KEY_PAGEUP, + [kVK_PageDown] = QEMU_KEY_PAGEDOWN, + [kVK_ForwardDelete] = QEMU_KEY_DELETE, + [kVK_Delete] = QEMU_KEY_BACKSPACE, + }; + + int with_control_translation[] = { + [0 ... 0xff] = 0, // invalid key + + [kVK_UpArrow] = QEMU_KEY_CTRL_UP, + [kVK_DownArrow] = QEMU_KEY_CTRL_DOWN, + [kVK_RightArrow] = QEMU_KEY_CTRL_RIGHT, + [kVK_LeftArrow] = QEMU_KEY_CTRL_LEFT, + [kVK_Home] = QEMU_KEY_CTRL_HOME, + [kVK_End] = QEMU_KEY_CTRL_END, + [kVK_PageUp] = QEMU_KEY_CTRL_PAGEUP, + [kVK_PageDown] = QEMU_KEY_CTRL_PAGEDOWN, + }; + + if (control_key != 0) { /* If the control key is being used */ + if ([event keyCode] < ARRAY_SIZE(with_control_translation)) { + keysym = with_control_translation[[event keyCode]]; + } + } else { + if ([event keyCode] < ARRAY_SIZE(without_control_translation)) { + keysym = without_control_translation[[event keyCode]]; + } + } + + // if not a key that needs translating + if (keysym == 0) { + NSString *ks = [event characters]; + if ([ks length] > 0) { + keysym = [ks characterAtIndex:0]; + } + } + + if (keysym) { + kbd_put_keysym(keysym); + } +} + +- (bool) handleEvent:(NSEvent *)event +{ + if(!allow_events) { + /* + * Just let OSX have all events that arrive before + * applicationDidFinishLaunching. + * This avoids a deadlock on the iothread lock, which cocoa_display_init() + * will not drop until after the app_started_sem is posted. (In theory + * there should not be any such events, but OSX Catalina now emits some.) + */ + return false; + } + 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; + static bool switched_to_fullscreen = false; + // Location of event in virtual screen coordinates + NSPoint p = [self screenLocationOfEvent:event]; + NSUInteger modifiers = [event modifierFlags]; + + /* + * Check -[NSEvent modifierFlags] here. + * + * There is a NSEventType for an event notifying the change of + * -[NSEvent modifierFlags], NSEventTypeFlagsChanged but these operations + * are performed for any events because a modifier state may change while + * the application is inactive (i.e. no events fire) and we don't want to + * wait for another modifier state change to detect such a change. + * + * NSEventModifierFlagCapsLock requires a special treatment. The other flags + * are handled in similar manners. + * + * NSEventModifierFlagCapsLock + * --------------------------- + * + * If CapsLock state is changed, "up" and "down" events will be fired in + * sequence, effectively updates CapsLock state on the guest. + * + * The other flags + * --------------- + * + * If a flag is not set, fire "up" events for all keys which correspond to + * the flag. Note that "down" events are not fired here because the flags + * checked here do not tell what exact keys are down. + * + * If one of the keys corresponding to a flag is down, we rely on + * -[NSEvent keyCode] of an event whose -[NSEvent type] is + * NSEventTypeFlagsChanged to know the exact key which is down, which has + * the following two downsides: + * - It does not work when the application is inactive as described above. + * - It malfactions *after* the modifier state is changed while the + * application is inactive. It is because -[NSEvent keyCode] does not tell + * if the key is up or down, and requires to infer the current state from + * the previous state. It is still possible to fix such a malfanction by + * completely leaving your hands from the keyboard, which hopefully makes + * this implementation usable enough. + */ + if (!!(modifiers & NSEventModifierFlagCapsLock) != + qkbd_state_modifier_get(kbd, QKBD_MOD_CAPSLOCK)) { + qkbd_state_key_event(kbd, Q_KEY_CODE_CAPS_LOCK, true); + qkbd_state_key_event(kbd, Q_KEY_CODE_CAPS_LOCK, false); + } + + if (!(modifiers & NSEventModifierFlagShift)) { + qkbd_state_key_event(kbd, Q_KEY_CODE_SHIFT, false); + qkbd_state_key_event(kbd, Q_KEY_CODE_SHIFT_R, false); + } + if (!(modifiers & NSEventModifierFlagControl)) { + qkbd_state_key_event(kbd, Q_KEY_CODE_CTRL, false); + qkbd_state_key_event(kbd, Q_KEY_CODE_CTRL_R, false); + } + if (!(modifiers & NSEventModifierFlagOption)) { + qkbd_state_key_event(kbd, Q_KEY_CODE_ALT, false); + qkbd_state_key_event(kbd, Q_KEY_CODE_ALT_R, false); + } + if (!(modifiers & NSEventModifierFlagCommand)) { + qkbd_state_key_event(kbd, Q_KEY_CODE_META_L, false); + qkbd_state_key_event(kbd, Q_KEY_CODE_META_R, false); + } + + switch ([event type]) { + case NSEventTypeFlagsChanged: + switch ([event keyCode]) { + case kVK_Shift: + if (!!(modifiers & NSEventModifierFlagShift)) { + [self toggleKey:Q_KEY_CODE_SHIFT]; + } + break; + + case kVK_RightShift: + if (!!(modifiers & NSEventModifierFlagShift)) { + [self toggleKey:Q_KEY_CODE_SHIFT_R]; + } + break; + + case kVK_Control: + if (!!(modifiers & NSEventModifierFlagControl)) { + [self toggleKey:Q_KEY_CODE_CTRL]; + } + break; + + case kVK_RightControl: + if (!!(modifiers & NSEventModifierFlagControl)) { + [self toggleKey:Q_KEY_CODE_CTRL_R]; + } + break; + + case kVK_Option: + if (!!(modifiers & NSEventModifierFlagOption)) { + [self toggleKey:Q_KEY_CODE_ALT]; + } + break; + + case kVK_RightOption: + if (!!(modifiers & NSEventModifierFlagOption)) { + [self toggleKey:Q_KEY_CODE_ALT_R]; + } + break; + + /* Don't pass command key changes to guest unless mouse is grabbed */ + case kVK_Command: + if (isMouseGrabbed && + !!(modifiers & NSEventModifierFlagCommand)) { + [self toggleKey:Q_KEY_CODE_META_L]; + } + break; + + case kVK_RightCommand: + if (isMouseGrabbed && + !!(modifiers & NSEventModifierFlagCommand)) { + [self toggleKey:Q_KEY_CODE_META_R]; + } + break; + } + break; + case NSEventTypeKeyDown: + keycode = cocoa_keycode_to_qemu([event keyCode]); + + // forward command key combos to the host UI unless the mouse is grabbed + if (!isMouseGrabbed && ([event modifierFlags] & NSEventModifierFlagCommand)) { + /* + * Prevent the command key from being stuck down in the guest + * when using Command-F to switch to full screen mode. + */ + if (keycode == Q_KEY_CODE_F) { + switched_to_fullscreen = true; + } + return false; + } + + // default + + // handle control + alt Key Combos (ctrl+alt+[1..9,g] is reserved for QEMU) + if (([event modifierFlags] & NSEventModifierFlagControl) && ([event modifierFlags] & NSEventModifierFlagOption)) { + NSString *keychar = [event charactersIgnoringModifiers]; + if ([keychar length] == 1) { + char key = [keychar characterAtIndex:0]; + switch (key) { + + // enable graphic console + case '1' ... '9': + console_select(key - '0' - 1); /* ascii math */ + return true; + + // release the mouse grab + case 'g': + [self ungrabMouse]; + return true; + } + } + } + + if (qemu_console_is_graphic(NULL)) { + qkbd_state_key_event(kbd, keycode, true); + } else { + [self handleMonitorInput: event]; + } + break; + case NSEventTypeKeyUp: + keycode = cocoa_keycode_to_qemu([event keyCode]); + + // 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 true; + } + + if (qemu_console_is_graphic(NULL)) { + qkbd_state_key_event(kbd, keycode, false); + } + break; + case NSEventTypeMouseMoved: + if (isAbsoluteEnabled) { + // Cursor re-entered into a window might generate events bound to screen coordinates + // and `nil` window property, and in full screen mode, current window might not be + // key window, where event location alone should suffice. + if (![self screenContainsPoint:p] || !([[self window] isKeyWindow] || isFullscreen)) { + if (isMouseGrabbed) { + [self ungrabMouse]; + } + } else { + if (!isMouseGrabbed) { + [self grabMouse]; + } + } + } + mouse_event = true; + break; + case NSEventTypeLeftMouseDown: + buttons |= MOUSE_EVENT_LBUTTON; + mouse_event = true; + break; + case NSEventTypeRightMouseDown: + buttons |= MOUSE_EVENT_RBUTTON; + mouse_event = true; + break; + case NSEventTypeOtherMouseDown: + buttons |= MOUSE_EVENT_MBUTTON; + mouse_event = true; + break; + case NSEventTypeLeftMouseDragged: + buttons |= MOUSE_EVENT_LBUTTON; + mouse_event = true; + break; + case NSEventTypeRightMouseDragged: + buttons |= MOUSE_EVENT_RBUTTON; + mouse_event = true; + break; + case NSEventTypeOtherMouseDragged: + buttons |= MOUSE_EVENT_MBUTTON; + mouse_event = true; + break; + case NSEventTypeLeftMouseUp: + mouse_event = true; + if (!isMouseGrabbed && [self screenContainsPoint:p]) { + /* + * In fullscreen mode, the window of cocoaView may not be the + * key window, therefore the position relative to the virtual + * screen alone will be sufficient. + */ + if(isFullscreen || [[self window] isKeyWindow]) { + [self grabMouse]; + } + } + break; + case NSEventTypeRightMouseUp: + mouse_event = true; + break; + case NSEventTypeOtherMouseUp: + mouse_event = true; + break; + case NSEventTypeScrollWheel: + /* + * Send wheel events to the guest regardless of window focus. + * This is in-line with standard Mac OS X UI behaviour. + */ + + /* + * When deltaY is zero, it means that this scrolling event was + * either horizontal, or so fine that it only appears in + * scrollingDeltaY. So we drop the event. + */ + if ([event deltaY] != 0) { + /* Determine if this is a scroll up or scroll down event */ + buttons = ([event deltaY] > 0) ? + INPUT_BUTTON_WHEEL_UP : INPUT_BUTTON_WHEEL_DOWN; + qemu_input_queue_btn(dcl.con, buttons, true); + qemu_input_event_sync(); + qemu_input_queue_btn(dcl.con, buttons, false); + qemu_input_event_sync(); + } + /* + * Since deltaY also reports scroll wheel events we prevent mouse + * movement code from executing. + */ + mouse_event = false; + break; + default: + return false; + } + + if (mouse_event) { + /* Don't send button events to the guest unless we've got a + * mouse grab or window focus. If we have neither then this event + * is the user clicking on the background window to activate and + * bring us to the front, which will be done by the sendEvent + * call below. We definitely don't want to pass that click through + * to the guest. + */ + if ((isMouseGrabbed || [[self window] isKeyWindow]) && + (last_buttons != buttons)) { + static uint32_t bmap[INPUT_BUTTON__MAX] = { + [INPUT_BUTTON_LEFT] = MOUSE_EVENT_LBUTTON, + [INPUT_BUTTON_MIDDLE] = MOUSE_EVENT_MBUTTON, + [INPUT_BUTTON_RIGHT] = MOUSE_EVENT_RBUTTON + }; + qemu_input_update_buttons(dcl.con, bmap, last_buttons, buttons); + last_buttons = buttons; + } + if (isMouseGrabbed) { + if (isAbsoluteEnabled) { + /* Note that the origin for Cocoa mouse coords is bottom left, not top left. + * The check on screenContainsPoint is to avoid sending out of range values for + * clicks in the titlebar. + */ + if ([self screenContainsPoint:p]) { + qemu_input_queue_abs(dcl.con, INPUT_AXIS_X, p.x, 0, screen.width); + qemu_input_queue_abs(dcl.con, INPUT_AXIS_Y, screen.height - p.y, 0, screen.height); + } + } else { + qemu_input_queue_rel(dcl.con, INPUT_AXIS_X, (int)[event deltaX]); + qemu_input_queue_rel(dcl.con, INPUT_AXIS_Y, (int)[event deltaY]); + } + } else { + return false; + } + qemu_input_event_sync(); + } + return true; +} + +- (void) grabMouse +{ + COCOA_DEBUG("QemuCocoaView: grabMouse\n"); + + if (!isFullscreen) { + if (qemu_name) + [normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s - (Press ctrl + alt + g to release Mouse)", qemu_name]]; + else + [normalWindow setTitle:@"QEMU - (Press ctrl + alt + g to release Mouse)"]; + } + [self hideCursor]; + CGAssociateMouseAndMouseCursorPosition(isAbsoluteEnabled); + isMouseGrabbed = TRUE; // while isMouseGrabbed = TRUE, QemuCocoaApp sends all events to [cocoaView handleEvent:] +} + +- (void) ungrabMouse +{ + COCOA_DEBUG("QemuCocoaView: ungrabMouse\n"); + + if (!isFullscreen) { + if (qemu_name) + [normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s", qemu_name]]; + else + [normalWindow setTitle:@"QEMU"]; + } + [self unhideCursor]; + CGAssociateMouseAndMouseCursorPosition(TRUE); + isMouseGrabbed = FALSE; +} + +- (void) setAbsoluteEnabled:(BOOL)tIsAbsoluteEnabled { + isAbsoluteEnabled = tIsAbsoluteEnabled; + if (isMouseGrabbed) { + CGAssociateMouseAndMouseCursorPosition(isAbsoluteEnabled); + } +} +- (BOOL) isMouseGrabbed {return isMouseGrabbed;} +- (BOOL) isAbsoluteEnabled {return isAbsoluteEnabled;} +- (float) cdx {return cdx;} +- (float) cdy {return cdy;} +- (QEMUScreen) gscreen {return screen;} + +/* + * Makes the target think all down keys are being released. + * This prevents a stuck key problem, since we will not see + * key up events for those keys after we have lost focus. + */ +- (void) raiseAllKeys +{ + with_iothread_lock(^{ + qkbd_state_lift_all_keys(kbd); + }); +} +@end + + + +/* + ------------------------------------------------------ + QemuCocoaAppController + ------------------------------------------------------ +*/ +@interface QemuCocoaAppController : NSObject + <NSWindowDelegate, NSApplicationDelegate> +{ +} +- (void)doToggleFullScreen:(id)sender; +- (void)toggleFullScreen:(id)sender; +- (void)showQEMUDoc:(id)sender; +- (void)zoomToFit:(id) sender; +- (void)displayConsole:(id)sender; +- (void)pauseQEMU:(id)sender; +- (void)resumeQEMU:(id)sender; +- (void)displayPause; +- (void)removePause; +- (void)restartQEMU:(id)sender; +- (void)powerDownQEMU:(id)sender; +- (void)ejectDeviceMedia:(id)sender; +- (void)changeDeviceMedia:(id)sender; +- (BOOL)verifyQuit; +- (void)openDocumentation:(NSString *)filename; +- (IBAction) do_about_menu_item: (id) sender; +- (void)make_about_window; +- (void)adjustSpeed:(id)sender; +@end + +@implementation QemuCocoaAppController +- (id) init +{ + COCOA_DEBUG("QemuCocoaAppController: init\n"); + + self = [super init]; + if (self) { + + // create a view and add it to the window + cocoaView = [[QemuCocoaView alloc] initWithFrame:NSMakeRect(0.0, 0.0, 640.0, 480.0)]; + if(!cocoaView) { + error_report("(cocoa) can't create a view"); + exit(1); + } + + // create a window + normalWindow = [[NSWindow alloc] initWithContentRect:[cocoaView frame] + styleMask:NSWindowStyleMaskTitled|NSWindowStyleMaskMiniaturizable|NSWindowStyleMaskClosable + backing:NSBackingStoreBuffered defer:NO]; + if(!normalWindow) { + error_report("(cocoa) can't create window"); + exit(1); + } + [normalWindow setAcceptsMouseMovedEvents:YES]; + [normalWindow setTitle:@"QEMU"]; + [normalWindow setContentView:cocoaView]; + [normalWindow makeKeyAndOrderFront:self]; + [normalWindow center]; + [normalWindow setDelegate: self]; + stretch_video = false; + + /* Used for displaying pause on the screen */ + pauseLabel = [NSTextField new]; + [pauseLabel setBezeled:YES]; + [pauseLabel setDrawsBackground:YES]; + [pauseLabel setBackgroundColor: [NSColor whiteColor]]; + [pauseLabel setEditable:NO]; + [pauseLabel setSelectable:NO]; + [pauseLabel setStringValue: @"Paused"]; + [pauseLabel setFont: [NSFont fontWithName: @"Helvetica" size: 90]]; + [pauseLabel setTextColor: [NSColor blackColor]]; + [pauseLabel sizeToFit]; + + // set the supported image file types that can be opened + supportedImageFileTypes = [NSArray arrayWithObjects: @"img", @"iso", @"dmg", + @"qcow", @"qcow2", @"cloop", @"vmdk", @"cdr", + @"toast", nil]; + [self make_about_window]; + } + return self; +} + +- (void) dealloc +{ + COCOA_DEBUG("QemuCocoaAppController: dealloc\n"); + + if (cocoaView) + [cocoaView release]; + [super dealloc]; +} + +- (void)applicationDidFinishLaunching: (NSNotification *) note +{ + COCOA_DEBUG("QemuCocoaAppController: applicationDidFinishLaunching\n"); + allow_events = true; + /* Tell cocoa_display_init to proceed */ + qemu_sem_post(&app_started_sem); +} + +- (void)applicationWillTerminate:(NSNotification *)aNotification +{ + COCOA_DEBUG("QemuCocoaAppController: applicationWillTerminate\n"); + + qemu_system_shutdown_request(SHUTDOWN_CAUSE_HOST_UI); + + /* + * Sleep here, because returning will cause OSX to kill us + * immediately; the QEMU main loop will handle the shutdown + * request and terminate the process. + */ + [NSThread sleepForTimeInterval:INFINITY]; +} + +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication +{ + return YES; +} + +- (NSApplicationTerminateReply)applicationShouldTerminate: + (NSApplication *)sender +{ + COCOA_DEBUG("QemuCocoaAppController: applicationShouldTerminate\n"); + return [self verifyQuit]; +} + +- (void)windowDidChangeScreen:(NSNotification *)notification +{ + [cocoaView updateUIInfo]; +} + +- (void)windowDidResize:(NSNotification *)notification +{ + [cocoaView updateUIInfo]; +} + +/* Called when the user clicks on a window's close button */ +- (BOOL)windowShouldClose:(id)sender +{ + COCOA_DEBUG("QemuCocoaAppController: windowShouldClose\n"); + [NSApp terminate: sender]; + /* If the user allows the application to quit then the call to + * NSApp terminate will never return. If we get here then the user + * cancelled the quit, so we should return NO to not permit the + * closing of this window. + */ + return NO; +} + +/* Called when QEMU goes into the background */ +- (void) applicationWillResignActive: (NSNotification *)aNotification +{ + COCOA_DEBUG("QemuCocoaAppController: applicationWillResignActive\n"); + [cocoaView raiseAllKeys]; +} + +/* 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: + */ +- (void) doToggleFullScreen:(id)sender +{ + [self toggleFullScreen:(id)sender]; +} + +- (void)toggleFullScreen:(id)sender +{ + COCOA_DEBUG("QemuCocoaAppController: toggleFullScreen\n"); + + [cocoaView toggleFullScreen:sender]; +} + +/* Tries to find then open the specified filename */ +- (void) openDocumentation: (NSString *) filename +{ + /* Where to look for local files */ + NSString *path_array[] = {@"../share/doc/qemu/", @"../doc/qemu/", @"docs/"}; + NSString *full_file_path; + NSURL *full_file_url; + + /* iterate thru the possible paths until the file is found */ + int index; + for (index = 0; index < ARRAY_SIZE(path_array); index++) { + full_file_path = [[NSBundle mainBundle] executablePath]; + full_file_path = [full_file_path stringByDeletingLastPathComponent]; + full_file_path = [NSString stringWithFormat: @"%@/%@%@", full_file_path, + path_array[index], filename]; + full_file_url = [NSURL fileURLWithPath: full_file_path + isDirectory: false]; + if ([[NSWorkspace sharedWorkspace] openURL: full_file_url] == YES) { + return; + } + } + + /* If none of the paths opened a file */ + NSBeep(); + QEMU_Alert(@"Failed to open file"); +} + +- (void)showQEMUDoc:(id)sender +{ + COCOA_DEBUG("QemuCocoaAppController: showQEMUDoc\n"); + + [self openDocumentation: @"index.html"]; +} + +/* Stretches video to fit host monitor size */ +- (void)zoomToFit:(id) sender +{ + stretch_video = !stretch_video; + if (stretch_video == true) { + [sender setState: NSControlStateValueOn]; + } else { + [sender setState: NSControlStateValueOff]; + } +} + +/* Displays the console on the screen */ +- (void)displayConsole:(id)sender +{ + console_select([sender tag]); +} + +/* Pause the guest */ +- (void)pauseQEMU:(id)sender +{ + with_iothread_lock(^{ + qmp_stop(NULL); + }); + [sender setEnabled: NO]; + [[[sender menu] itemWithTitle: @"Resume"] setEnabled: YES]; + [self displayPause]; +} + +/* 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]; +} + +/* Displays the word pause on the screen */ +- (void)displayPause +{ + /* Coordinates have to be calculated each time because the window can change its size */ + int xCoord, yCoord, width, height; + xCoord = ([normalWindow frame].size.width - [pauseLabel frame].size.width)/2; + yCoord = [normalWindow frame].size.height - [pauseLabel frame].size.height - ([pauseLabel frame].size.height * .5); + width = [pauseLabel frame].size.width; + height = [pauseLabel frame].size.height; + [pauseLabel setFrame: NSMakeRect(xCoord, yCoord, width, height)]; + [cocoaView addSubview: pauseLabel]; +} + +/* Removes the word pause from the screen */ +- (void)removePause +{ + [pauseLabel removeFromSuperview]; +} + +/* 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. + * Uses sender's tag to figure out the device to eject. + */ +- (void)ejectDeviceMedia:(id)sender +{ + NSString * drive; + drive = [sender representedObject]; + if(drive == nil) { + NSBeep(); + QEMU_Alert(@"Failed to find drive to eject!"); + return; + } + + __block Error *err = NULL; + with_iothread_lock(^{ + qmp_eject(true, [drive cStringUsingEncoding: NSASCIIStringEncoding], + false, NULL, false, false, &err); + }); + handleAnyDeviceErrors(err); +} + +/* Displays a dialog box asking the user to select an image file to load. + * Uses sender's represented object value to figure out which drive to use. + */ +- (void)changeDeviceMedia:(id)sender +{ + /* Find the drive name */ + NSString * drive; + drive = [sender representedObject]; + if(drive == nil) { + NSBeep(); + QEMU_Alert(@"Could not find drive!"); + return; + } + + /* Display the file open dialog */ + NSOpenPanel * openPanel; + openPanel = [NSOpenPanel openPanel]; + [openPanel setCanChooseFiles: YES]; + [openPanel setAllowsMultipleSelection: NO]; + [openPanel setAllowedFileTypes: supportedImageFileTypes]; + if([openPanel runModal] == NSModalResponseOK) { + NSString * file = [[[openPanel URLs] objectAtIndex: 0] path]; + if(file == nil) { + NSBeep(); + QEMU_Alert(@"Failed to convert URL to file path!"); + return; + } + + __block Error *err = NULL; + with_iothread_lock(^{ + qmp_blockdev_change_medium(true, + [drive cStringUsingEncoding: + NSASCIIStringEncoding], + false, NULL, + [file cStringUsingEncoding: + NSASCIIStringEncoding], + true, "raw", + false, 0, + &err); + }); + handleAnyDeviceErrors(err); + } +} + +/* Verifies if the user really wants to quit */ +- (BOOL)verifyQuit +{ + NSAlert *alert = [NSAlert new]; + [alert autorelease]; + [alert setMessageText: @"Are you sure you want to quit QEMU?"]; + [alert addButtonWithTitle: @"Cancel"]; + [alert addButtonWithTitle: @"Quit"]; + if([alert runModal] == NSAlertSecondButtonReturn) { + return YES; + } else { + return NO; + } +} + +/* The action method for the About menu item */ +- (IBAction) do_about_menu_item: (id) sender +{ + [about_window makeKeyAndOrderFront: nil]; +} + +/* Create and display the about dialog */ +- (void)make_about_window +{ + /* Make the window */ + int x = 0, y = 0, about_width = 400, about_height = 200; + NSRect window_rect = NSMakeRect(x, y, about_width, about_height); + about_window = [[NSWindow alloc] initWithContentRect:window_rect + styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable + backing:NSBackingStoreBuffered + defer:NO]; + [about_window setTitle: @"About"]; + [about_window setReleasedWhenClosed: NO]; + [about_window center]; + NSView *superView = [about_window contentView]; + + /* Create the dimensions of the picture */ + int picture_width = 80, picture_height = 80; + x = (about_width - picture_width)/2; + y = about_height - picture_height - 10; + NSRect picture_rect = NSMakeRect(x, y, picture_width, picture_height); + + /* Make the picture of QEMU */ + NSImageView *picture_view = [[NSImageView alloc] initWithFrame: + picture_rect]; + char *qemu_image_path_c = get_relocated_path(CONFIG_QEMU_ICONDIR "/hicolor/512x512/apps/qemu.png"); + NSString *qemu_image_path = [NSString stringWithUTF8String:qemu_image_path_c]; + g_free(qemu_image_path_c); + NSImage *qemu_image = [[NSImage alloc] initWithContentsOfFile:qemu_image_path]; + [picture_view setImage: qemu_image]; + [picture_view setImageScaling: NSImageScaleProportionallyUpOrDown]; + [superView addSubview: picture_view]; + + /* Make the name label */ + NSBundle *bundle = [NSBundle mainBundle]; + if (bundle) { + x = 0; + y = y - 25; + int name_width = about_width, name_height = 20; + NSRect name_rect = NSMakeRect(x, y, name_width, name_height); + NSTextField *name_label = [[NSTextField alloc] initWithFrame: name_rect]; + [name_label setEditable: NO]; + [name_label setBezeled: NO]; + [name_label setDrawsBackground: NO]; + [name_label setAlignment: NSTextAlignmentCenter]; + NSString *qemu_name = [[bundle executablePath] lastPathComponent]; + [name_label setStringValue: qemu_name]; + [superView addSubview: name_label]; + } + + /* Set the version label's attributes */ + x = 0; + y = 50; + int version_width = about_width, version_height = 20; + NSRect version_rect = NSMakeRect(x, y, version_width, version_height); + NSTextField *version_label = [[NSTextField alloc] initWithFrame: + version_rect]; + [version_label setEditable: NO]; + [version_label setBezeled: NO]; + [version_label setAlignment: NSTextAlignmentCenter]; + [version_label setDrawsBackground: NO]; + + /* Create the version string*/ + NSString *version_string; + version_string = [[NSString alloc] initWithFormat: + @"QEMU emulator version %s", QEMU_FULL_VERSION]; + [version_label setStringValue: version_string]; + [superView addSubview: version_label]; + + /* Make copyright label */ + x = 0; + y = 35; + int copyright_width = about_width, copyright_height = 20; + NSRect copyright_rect = NSMakeRect(x, y, copyright_width, copyright_height); + NSTextField *copyright_label = [[NSTextField alloc] initWithFrame: + copyright_rect]; + [copyright_label setEditable: NO]; + [copyright_label setBezeled: NO]; + [copyright_label setDrawsBackground: NO]; + [copyright_label setAlignment: NSTextAlignmentCenter]; + [copyright_label setStringValue: [NSString stringWithFormat: @"%s", + QEMU_COPYRIGHT]]; + [superView addSubview: copyright_label]; +} + +/* Used by the Speed menu items */ +- (void)adjustSpeed:(id)sender +{ + int throttle_pct; /* throttle percentage */ + NSMenu *menu; + + menu = [sender menu]; + if (menu != nil) + { + /* Unselect the currently selected item */ + for (NSMenuItem *item in [menu itemArray]) { + if (item.state == NSControlStateValueOn) { + [item setState: NSControlStateValueOff]; + break; + } + } + } + + // check the menu item + [sender setState: NSControlStateValueOn]; + + // 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 + +@implementation QemuApplication +- (void)sendEvent:(NSEvent *)event +{ + COCOA_DEBUG("QemuApplication: sendEvent\n"); + if (![cocoaView handleEvent:event]) { + [super sendEvent: event]; + } +} +@end + +static void create_initial_menus(void) +{ + // Add menus + NSMenu *menu; + NSMenuItem *menuItem; + + [NSApp setMainMenu:[[NSMenu alloc] init]]; + + // Application menu + menu = [[NSMenu alloc] initWithTitle:@""]; + [menu addItemWithTitle:@"About QEMU" action:@selector(do_about_menu_item:) keyEquivalent:@""]; // About QEMU + [menu addItem:[NSMenuItem separatorItem]]; //Separator + [menu addItemWithTitle:@"Hide QEMU" action:@selector(hide:) keyEquivalent:@"h"]; //Hide QEMU + menuItem = (NSMenuItem *)[menu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; // Hide Others + [menuItem setKeyEquivalentModifierMask:(NSEventModifierFlagOption|NSEventModifierFlagCommand)]; + [menu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; // Show All + [menu addItem:[NSMenuItem separatorItem]]; //Separator + [menu addItemWithTitle:@"Quit QEMU" action:@selector(terminate:) keyEquivalent:@"q"]; + menuItem = [[NSMenuItem alloc] initWithTitle:@"Apple" action:nil keyEquivalent:@""]; + [menuItem setSubmenu:menu]; + [[NSApp mainMenu] addItem:menuItem]; + [NSApp performSelector:@selector(setAppleMenu:) withObject:menu]; // Workaround (this method is private since 10.4+) + + // Machine menu + menu = [[NSMenu alloc] initWithTitle: @"Machine"]; + [menu setAutoenablesItems: NO]; + [menu addItem: [[[NSMenuItem alloc] initWithTitle: @"Pause" action: @selector(pauseQEMU:) keyEquivalent: @""] autorelease]]; + menuItem = [[[NSMenuItem alloc] initWithTitle: @"Resume" action: @selector(resumeQEMU:) keyEquivalent: @""] autorelease]; + [menu addItem: menuItem]; + [menuItem setEnabled: NO]; + [menu addItem: [NSMenuItem separatorItem]]; + [menu addItem: [[[NSMenuItem alloc] initWithTitle: @"Reset" action: @selector(restartQEMU:) keyEquivalent: @""] autorelease]]; + [menu addItem: [[[NSMenuItem alloc] initWithTitle: @"Power Down" action: @selector(powerDownQEMU:) keyEquivalent: @""] autorelease]]; + menuItem = [[[NSMenuItem alloc] initWithTitle: @"Machine" action:nil keyEquivalent:@""] autorelease]; + [menuItem setSubmenu:menu]; + [[NSApp mainMenu] addItem:menuItem]; + + // View menu + menu = [[NSMenu alloc] initWithTitle:@"View"]; + [menu addItem: [[[NSMenuItem alloc] initWithTitle:@"Enter Fullscreen" action:@selector(doToggleFullScreen:) keyEquivalent:@"f"] autorelease]]; // Fullscreen + [menu addItem: [[[NSMenuItem alloc] initWithTitle:@"Zoom To Fit" action:@selector(zoomToFit:) keyEquivalent:@""] autorelease]]; + menuItem = [[[NSMenuItem alloc] initWithTitle:@"View" action:nil keyEquivalent:@""] autorelease]; + [menuItem setSubmenu:menu]; + [[NSApp mainMenu] addItem:menuItem]; + + // Speed menu + menu = [[NSMenu alloc] initWithTitle:@"Speed"]; + + // Add the rest of the Speed menu items + int p, percentage, throttle_pct; + for (p = 10; p >= 0; p--) + { + percentage = p * 10 > 1 ? p * 10 : 1; // prevent a 0% menu item + + menuItem = [[[NSMenuItem alloc] + initWithTitle: [NSString stringWithFormat: @"%d%%", percentage] action:@selector(adjustSpeed:) keyEquivalent:@""] autorelease]; + + if (percentage == 100) { + [menuItem setState: NSControlStateValueOn]; + } + + /* Calculate the throttle percentage */ + throttle_pct = -1 * percentage + 100; + + [menuItem setTag: throttle_pct]; + [menu addItem: menuItem]; + } + menuItem = [[[NSMenuItem alloc] initWithTitle:@"Speed" action:nil keyEquivalent:@""] autorelease]; + [menuItem setSubmenu:menu]; + [[NSApp mainMenu] addItem:menuItem]; + + // Window menu + menu = [[NSMenu alloc] initWithTitle:@"Window"]; + [menu addItem: [[[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"] autorelease]]; // Miniaturize + menuItem = [[[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""] autorelease]; + [menuItem setSubmenu:menu]; + [[NSApp mainMenu] addItem:menuItem]; + [NSApp setWindowsMenu:menu]; + + // Help menu + menu = [[NSMenu alloc] initWithTitle:@"Help"]; + [menu addItem: [[[NSMenuItem alloc] initWithTitle:@"QEMU Documentation" action:@selector(showQEMUDoc:) keyEquivalent:@"?"] autorelease]]; // QEMU Help + menuItem = [[[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""] autorelease]; + [menuItem setSubmenu:menu]; + [[NSApp mainMenu] addItem:menuItem]; +} + +/* Returns a name for a given console */ +static NSString * getConsoleName(QemuConsole * console) +{ + return [NSString stringWithFormat: @"%s", qemu_console_get_label(console)]; +} + +/* Add an entry to the View menu for each console */ +static void add_console_menu_entries(void) +{ + NSMenu *menu; + NSMenuItem *menuItem; + int index = 0; + + menu = [[[NSApp mainMenu] itemWithTitle:@"View"] submenu]; + + [menu addItem:[NSMenuItem separatorItem]]; + + while (qemu_console_lookup_by_index(index) != NULL) { + menuItem = [[[NSMenuItem alloc] initWithTitle: getConsoleName(qemu_console_lookup_by_index(index)) + action: @selector(displayConsole:) keyEquivalent: @""] autorelease]; + [menuItem setTag: index]; + [menu addItem: menuItem]; + index++; + } +} + +/* Make menu items for all removable devices. + * Each device is given an 'Eject' and 'Change' menu item. + */ +static void addRemovableDevicesMenuItems(void) +{ + NSMenu *menu; + NSMenuItem *menuItem; + BlockInfoList *currentDevice, *pointerToFree; + NSString *deviceName; + + currentDevice = qmp_query_block(NULL); + pointerToFree = currentDevice; + if(currentDevice == NULL) { + NSBeep(); + QEMU_Alert(@"Failed to query for block devices!"); + return; + } + + menu = [[[NSApp mainMenu] itemWithTitle:@"Machine"] submenu]; + + // Add a separator between related groups of menu items + [menu addItem:[NSMenuItem separatorItem]]; + + // Set the attributes to the "Removable Media" menu item + NSString *titleString = @"Removable Media"; + NSMutableAttributedString *attString=[[NSMutableAttributedString alloc] initWithString:titleString]; + NSColor *newColor = [NSColor blackColor]; + NSFontManager *fontManager = [NSFontManager sharedFontManager]; + NSFont *font = [fontManager fontWithFamily:@"Helvetica" + traits:NSBoldFontMask|NSItalicFontMask + weight:0 + size:14]; + [attString addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, [titleString length])]; + [attString addAttribute:NSForegroundColorAttributeName value:newColor range:NSMakeRange(0, [titleString length])]; + [attString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInt: 1] range:NSMakeRange(0, [titleString length])]; + + // Add the "Removable Media" menu item + menuItem = [NSMenuItem new]; + [menuItem setAttributedTitle: attString]; + [menuItem setEnabled: NO]; + [menu addItem: menuItem]; + + /* Loop through all the block devices in the emulator */ + while (currentDevice) { + deviceName = [[NSString stringWithFormat: @"%s", currentDevice->value->device] retain]; + + if(currentDevice->value->removable) { + menuItem = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: @"Change %s...", currentDevice->value->device] + action: @selector(changeDeviceMedia:) + keyEquivalent: @""]; + [menu addItem: menuItem]; + [menuItem setRepresentedObject: deviceName]; + [menuItem autorelease]; + + menuItem = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: @"Eject %s", currentDevice->value->device] + action: @selector(ejectDeviceMedia:) + keyEquivalent: @""]; + [menu addItem: menuItem]; + [menuItem setRepresentedObject: deviceName]; + [menuItem autorelease]; + } + currentDevice = currentDevice->next; + } + qapi_free_BlockInfoList(pointerToFree); +} + +@interface QemuCocoaPasteboardTypeOwner : NSObject<NSPasteboardTypeOwner> +@end + +@implementation QemuCocoaPasteboardTypeOwner + +- (void)pasteboard:(NSPasteboard *)sender provideDataForType:(NSPasteboardType)type +{ + if (type != NSPasteboardTypeString) { + return; + } + + with_iothread_lock(^{ + QemuClipboardInfo *info = qemu_clipboard_info_ref(cbinfo); + qemu_event_reset(&cbevent); + qemu_clipboard_request(info, QEMU_CLIPBOARD_TYPE_TEXT); + + while (info == cbinfo && + info->types[QEMU_CLIPBOARD_TYPE_TEXT].available && + info->types[QEMU_CLIPBOARD_TYPE_TEXT].data == NULL) { + qemu_mutex_unlock_iothread(); + qemu_event_wait(&cbevent); + qemu_mutex_lock_iothread(); + } + + if (info == cbinfo) { + NSData *data = [[NSData alloc] initWithBytes:info->types[QEMU_CLIPBOARD_TYPE_TEXT].data + length:info->types[QEMU_CLIPBOARD_TYPE_TEXT].size]; + [sender setData:data forType:NSPasteboardTypeString]; + [data release]; + } + + qemu_clipboard_info_unref(info); + }); +} + +@end + +static QemuCocoaPasteboardTypeOwner *cbowner; + +static void cocoa_clipboard_notify(Notifier *notifier, void *data); +static void cocoa_clipboard_request(QemuClipboardInfo *info, + QemuClipboardType type); + +static QemuClipboardPeer cbpeer = { + .name = "cocoa", + .update = { .notify = cocoa_clipboard_notify }, + .request = cocoa_clipboard_request +}; + +static void cocoa_clipboard_notify(Notifier *notifier, void *data) +{ + QemuClipboardInfo *info = data; + + if (info->owner == &cbpeer || info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) { + return; + } + + if (info != cbinfo) { + NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; + qemu_clipboard_info_unref(cbinfo); + cbinfo = qemu_clipboard_info_ref(info); + cbchangecount = [[NSPasteboard generalPasteboard] declareTypes:@[NSPasteboardTypeString] owner:cbowner]; + [pool release]; + } + + qemu_event_set(&cbevent); +} + +static void cocoa_clipboard_request(QemuClipboardInfo *info, + QemuClipboardType type) +{ + NSData *text; + + switch (type) { + case QEMU_CLIPBOARD_TYPE_TEXT: + text = [[NSPasteboard generalPasteboard] dataForType:NSPasteboardTypeString]; + if (text) { + qemu_clipboard_set_data(&cbpeer, info, type, + [text length], [text bytes], true); + [text release]; + } + break; + default: + break; + } +} + +/* + * 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"); + [cbowner release]; + exit(status); +} + +int main (int argc, char **argv) { + QemuThread thread; + + COCOA_DEBUG("Entered main()\n"); + gArgc = argc; + gArgv = 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"); + + [cocoaView updateUIInfo]; + + // 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]; + }); + } + + if (cbchangecount != [[NSPasteboard generalPasteboard] changeCount]) { + qemu_clipboard_info_unref(cbinfo); + cbinfo = qemu_clipboard_info_new(&cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD); + if ([[NSPasteboard generalPasteboard] availableTypeFromArray:@[NSPasteboardTypeString]]) { + cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; + } + qemu_clipboard_update(cbinfo); + cbchangecount = [[NSPasteboard generalPasteboard] changeCount]; + qemu_event_set(&cbevent); + } + + [pool release]; +} + +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]; + }); + } + if (opts->has_show_cursor && opts->show_cursor) { + cursor_hide = 0; + } + + // register vga output callbacks + register_displaychangelistener(&dcl); + + qemu_event_init(&cbevent, false); + cbowner = [[QemuCocoaPasteboardTypeOwner alloc] init]; + qemu_clipboard_peer_register(&cbpeer); +} + +static QemuDisplay qemu_display_cocoa = { + .type = DISPLAY_TYPE_COCOA, + .init = cocoa_display_init, +}; + +static void register_cocoa(void) +{ + qemu_display_register(&qemu_display_cocoa); +} + +type_init(register_cocoa); diff --git a/ui/console-gl.c b/ui/console-gl.c new file mode 100644 index 000000000..7c9894a51 --- /dev/null +++ b/ui/console-gl.c @@ -0,0 +1,158 @@ +/* + * QEMU graphical console -- opengl helper bits + * + * Copyright (c) 2014 Red Hat + * + * Authors: + * Gerd Hoffmann <kraxel@redhat.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "qemu/osdep.h" +#include "ui/console.h" +#include "ui/shader.h" + +/* ---------------------------------------------------------------------- */ + +bool console_gl_check_format(DisplayChangeListener *dcl, + pixman_format_code_t format) +{ + switch (format) { + case PIXMAN_BE_b8g8r8x8: + case PIXMAN_BE_b8g8r8a8: + case PIXMAN_r5g6b5: + return true; + default: + return false; + } +} + +void surface_gl_create_texture(QemuGLShader *gls, + DisplaySurface *surface) +{ + assert(gls); + assert(QEMU_IS_ALIGNED(surface_stride(surface), surface_bytes_per_pixel(surface))); + + switch (surface->format) { + case PIXMAN_BE_b8g8r8x8: + case PIXMAN_BE_b8g8r8a8: + surface->glformat = GL_BGRA_EXT; + surface->gltype = GL_UNSIGNED_BYTE; + break; + case PIXMAN_BE_x8r8g8b8: + case PIXMAN_BE_a8r8g8b8: + surface->glformat = GL_RGBA; + surface->gltype = GL_UNSIGNED_BYTE; + break; + case PIXMAN_r5g6b5: + surface->glformat = GL_RGB; + surface->gltype = GL_UNSIGNED_SHORT_5_6_5; + break; + default: + g_assert_not_reached(); + } + + glGenTextures(1, &surface->texture); + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, surface->texture); + glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, + surface_stride(surface) / surface_bytes_per_pixel(surface)); + if (epoxy_is_desktop_gl()) { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, + surface_width(surface), + surface_height(surface), + 0, surface->glformat, surface->gltype, + surface_data(surface)); + } else { + glTexImage2D(GL_TEXTURE_2D, 0, surface->glformat, + surface_width(surface), + surface_height(surface), + 0, surface->glformat, surface->gltype, + surface_data(surface)); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE); + } + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); +} + +void surface_gl_update_texture(QemuGLShader *gls, + DisplaySurface *surface, + int x, int y, int w, int h) +{ + uint8_t *data = (void *)surface_data(surface); + + assert(gls); + + if (surface->texture) { + glBindTexture(GL_TEXTURE_2D, surface->texture); + glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, + surface_stride(surface) + / surface_bytes_per_pixel(surface)); + glTexSubImage2D(GL_TEXTURE_2D, 0, + x, y, w, h, + surface->glformat, surface->gltype, + data + surface_stride(surface) * y + + surface_bytes_per_pixel(surface) * x); + } +} + +void surface_gl_render_texture(QemuGLShader *gls, + DisplaySurface *surface) +{ + assert(gls); + + glClearColor(0.1f, 0.1f, 0.1f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + + qemu_gl_run_texture_blit(gls, false); +} + +void surface_gl_destroy_texture(QemuGLShader *gls, + DisplaySurface *surface) +{ + if (!surface || !surface->texture) { + return; + } + glDeleteTextures(1, &surface->texture); + surface->texture = 0; +} + +void surface_gl_setup_viewport(QemuGLShader *gls, + DisplaySurface *surface, + int ww, int wh) +{ + int gw, gh, stripe; + float sw, sh; + + assert(gls); + + gw = surface_width(surface); + gh = surface_height(surface); + + sw = (float)ww/gw; + sh = (float)wh/gh; + if (sw < sh) { + stripe = wh - wh*sw/sh; + glViewport(0, stripe / 2, ww, wh - stripe); + } else { + stripe = ww - ww*sh/sw; + glViewport(stripe / 2, 0, ww - stripe, wh); + } +} diff --git a/ui/console.c b/ui/console.c new file mode 100644 index 000000000..29a3e3f0f --- /dev/null +++ b/ui/console.c @@ -0,0 +1,2460 @@ +/* + * QEMU graphical console + * + * Copyright (c) 2004 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "ui/console.h" +#include "hw/qdev-core.h" +#include "qapi/error.h" +#include "qapi/qapi-commands-ui.h" +#include "qemu/fifo8.h" +#include "qemu/main-loop.h" +#include "qemu/module.h" +#include "qemu/option.h" +#include "qemu/timer.h" +#include "chardev/char.h" +#include "trace.h" +#include "exec/memory.h" +#include "io/channel-file.h" +#include "qom/object.h" + +#define DEFAULT_BACKSCROLL 512 +#define CONSOLE_CURSOR_PERIOD 500 + +typedef struct TextAttributes { + uint8_t fgcol:4; + uint8_t bgcol:4; + uint8_t bold:1; + uint8_t uline:1; + uint8_t blink:1; + uint8_t invers:1; + uint8_t unvisible:1; +} TextAttributes; + +typedef struct TextCell { + uint8_t ch; + TextAttributes t_attrib; +} TextCell; + +#define MAX_ESC_PARAMS 3 + +enum TTYState { + TTY_STATE_NORM, + TTY_STATE_ESC, + TTY_STATE_CSI, +}; + +typedef enum { + GRAPHIC_CONSOLE, + TEXT_CONSOLE, + TEXT_CONSOLE_FIXED_SIZE +} console_type_t; + +struct QemuConsole { + Object parent; + + int index; + console_type_t console_type; + DisplayState *ds; + DisplaySurface *surface; + int dcls; + DisplayChangeListener *gl; + bool gl_block; + int window_id; + + /* Graphic console state. */ + Object *device; + uint32_t head; + QemuUIInfo ui_info; + QEMUTimer *ui_timer; + const GraphicHwOps *hw_ops; + void *hw; + + /* Text console state */ + int width; + int height; + int total_height; + int backscroll_height; + int x, y; + int x_saved, y_saved; + int y_displayed; + int y_base; + TextAttributes t_attrib_default; /* default text attributes */ + TextAttributes t_attrib; /* currently active text attributes */ + TextCell *cells; + int text_x[2], text_y[2], cursor_invalidate; + int echo; + + int update_x0; + int update_y0; + int update_x1; + int update_y1; + + enum TTYState state; + int esc_params[MAX_ESC_PARAMS]; + int nb_esc_params; + + Chardev *chr; + /* fifo for key pressed */ + Fifo8 out_fifo; + CoQueue dump_queue; + + QTAILQ_ENTRY(QemuConsole) next; +}; + +struct DisplayState { + QEMUTimer *gui_timer; + uint64_t last_update; + uint64_t update_interval; + bool refreshing; + bool have_gfx; + bool have_text; + + QLIST_HEAD(, DisplayChangeListener) listeners; +}; + +static DisplayState *display_state; +static QemuConsole *active_console; +static QTAILQ_HEAD(, QemuConsole) consoles = + QTAILQ_HEAD_INITIALIZER(consoles); +static bool cursor_visible_phase; +static QEMUTimer *cursor_timer; + +static void text_console_do_init(Chardev *chr, DisplayState *ds); +static void dpy_refresh(DisplayState *s); +static DisplayState *get_alloc_displaystate(void); +static void text_console_update_cursor_timer(void); +static void text_console_update_cursor(void *opaque); + +static void gui_update(void *opaque) +{ + uint64_t interval = GUI_REFRESH_INTERVAL_IDLE; + uint64_t dcl_interval; + DisplayState *ds = opaque; + DisplayChangeListener *dcl; + QemuConsole *con; + + ds->refreshing = true; + dpy_refresh(ds); + ds->refreshing = false; + + QLIST_FOREACH(dcl, &ds->listeners, next) { + dcl_interval = dcl->update_interval ? + dcl->update_interval : GUI_REFRESH_INTERVAL_DEFAULT; + if (interval > dcl_interval) { + interval = dcl_interval; + } + } + if (ds->update_interval != interval) { + ds->update_interval = interval; + QTAILQ_FOREACH(con, &consoles, next) { + if (con->hw_ops->update_interval) { + con->hw_ops->update_interval(con->hw, interval); + } + } + trace_console_refresh(interval); + } + ds->last_update = qemu_clock_get_ms(QEMU_CLOCK_REALTIME); + timer_mod(ds->gui_timer, ds->last_update + interval); +} + +static void gui_setup_refresh(DisplayState *ds) +{ + DisplayChangeListener *dcl; + bool need_timer = false; + bool have_gfx = false; + bool have_text = false; + + QLIST_FOREACH(dcl, &ds->listeners, next) { + if (dcl->ops->dpy_refresh != NULL) { + need_timer = true; + } + if (dcl->ops->dpy_gfx_update != NULL) { + have_gfx = true; + } + if (dcl->ops->dpy_text_update != NULL) { + have_text = true; + } + } + + if (need_timer && ds->gui_timer == NULL) { + ds->gui_timer = timer_new_ms(QEMU_CLOCK_REALTIME, gui_update, ds); + timer_mod(ds->gui_timer, qemu_clock_get_ms(QEMU_CLOCK_REALTIME)); + } + if (!need_timer && ds->gui_timer != NULL) { + timer_free(ds->gui_timer); + ds->gui_timer = NULL; + } + + ds->have_gfx = have_gfx; + ds->have_text = have_text; +} + +void graphic_hw_update_done(QemuConsole *con) +{ + if (con) { + qemu_co_queue_restart_all(&con->dump_queue); + } +} + +void graphic_hw_update(QemuConsole *con) +{ + bool async = false; + con = con ? con : active_console; + if (!con) { + return; + } + if (con->hw_ops->gfx_update) { + con->hw_ops->gfx_update(con->hw); + async = con->hw_ops->gfx_update_async; + } + if (!async) { + graphic_hw_update_done(con); + } +} + +void graphic_hw_gl_block(QemuConsole *con, bool block) +{ + assert(con != NULL); + + con->gl_block = block; + if (con->hw_ops->gl_block) { + con->hw_ops->gl_block(con->hw, block); + } +} + +void graphic_hw_gl_flushed(QemuConsole *con) +{ + assert(con != NULL); + + if (con->hw_ops->gl_flushed) { + con->hw_ops->gl_flushed(con->hw); + } +} + +int qemu_console_get_window_id(QemuConsole *con) +{ + return con->window_id; +} + +void qemu_console_set_window_id(QemuConsole *con, int window_id) +{ + con->window_id = window_id; +} + +void graphic_hw_invalidate(QemuConsole *con) +{ + if (!con) { + con = active_console; + } + if (con && con->hw_ops->invalidate) { + con->hw_ops->invalidate(con->hw); + } +} + +static bool ppm_save(int fd, pixman_image_t *image, Error **errp) +{ + int width = pixman_image_get_width(image); + int height = pixman_image_get_height(image); + g_autoptr(Object) ioc = OBJECT(qio_channel_file_new_fd(fd)); + g_autofree char *header = NULL; + g_autoptr(pixman_image_t) linebuf = NULL; + int y; + + trace_ppm_save(fd, image); + + header = g_strdup_printf("P6\n%d %d\n%d\n", width, height, 255); + if (qio_channel_write_all(QIO_CHANNEL(ioc), + header, strlen(header), errp) < 0) { + return false; + } + + linebuf = qemu_pixman_linebuf_create(PIXMAN_BE_r8g8b8, width); + for (y = 0; y < height; y++) { + qemu_pixman_linebuf_fill(linebuf, image, width, 0, y); + if (qio_channel_write_all(QIO_CHANNEL(ioc), + (char *)pixman_image_get_data(linebuf), + pixman_image_get_stride(linebuf), errp) < 0) { + return false; + } + } + + return true; +} + +static void graphic_hw_update_bh(void *con) +{ + graphic_hw_update(con); +} + +/* Safety: coroutine-only, concurrent-coroutine safe, main thread only */ +void coroutine_fn +qmp_screendump(const char *filename, bool has_device, const char *device, + bool has_head, int64_t head, Error **errp) +{ + g_autoptr(pixman_image_t) image = NULL; + QemuConsole *con; + DisplaySurface *surface; + int fd; + + if (has_device) { + con = qemu_console_lookup_by_device_name(device, has_head ? head : 0, + errp); + if (!con) { + return; + } + } else { + if (has_head) { + error_setg(errp, "'head' must be specified together with 'device'"); + return; + } + con = qemu_console_lookup_by_index(0); + if (!con) { + error_setg(errp, "There is no console to take a screendump from"); + return; + } + } + + if (qemu_co_queue_empty(&con->dump_queue)) { + /* Defer the update, it will restart the pending coroutines */ + aio_bh_schedule_oneshot(qemu_get_aio_context(), + graphic_hw_update_bh, con); + } + qemu_co_queue_wait(&con->dump_queue, NULL); + + /* + * All pending coroutines are woken up, while the BQL is held. No + * further graphic update are possible until it is released. Take + * an image ref before that. + */ + surface = qemu_console_surface(con); + if (!surface) { + error_setg(errp, "no surface"); + return; + } + image = pixman_image_ref(surface->image); + + fd = qemu_open_old(filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666); + if (fd == -1) { + error_setg(errp, "failed to open file '%s': %s", filename, + strerror(errno)); + return; + } + + /* + * The image content could potentially be updated as the coroutine + * yields and releases the BQL. It could produce corrupted dump, but + * it should be otherwise safe. + */ + if (!ppm_save(fd, image, errp)) { + qemu_unlink(filename); + } +} + +void graphic_hw_text_update(QemuConsole *con, console_ch_t *chardata) +{ + if (!con) { + con = active_console; + } + if (con && con->hw_ops->text_update) { + con->hw_ops->text_update(con->hw, chardata); + } +} + +static void vga_fill_rect(QemuConsole *con, + int posx, int posy, int width, int height, + pixman_color_t color) +{ + DisplaySurface *surface = qemu_console_surface(con); + pixman_rectangle16_t rect = { + .x = posx, .y = posy, .width = width, .height = height + }; + + pixman_image_fill_rectangles(PIXMAN_OP_SRC, surface->image, + &color, 1, &rect); +} + +/* copy from (xs, ys) to (xd, yd) a rectangle of size (w, h) */ +static void vga_bitblt(QemuConsole *con, + int xs, int ys, int xd, int yd, int w, int h) +{ + DisplaySurface *surface = qemu_console_surface(con); + + pixman_image_composite(PIXMAN_OP_SRC, + surface->image, NULL, surface->image, + xs, ys, 0, 0, xd, yd, w, h); +} + +/***********************************************************/ +/* basic char display */ + +#define FONT_HEIGHT 16 +#define FONT_WIDTH 8 + +#include "vgafont.h" + +#define QEMU_RGB(r, g, b) \ + { .red = r << 8, .green = g << 8, .blue = b << 8, .alpha = 0xffff } + +static const pixman_color_t color_table_rgb[2][8] = { + { /* dark */ + [QEMU_COLOR_BLACK] = QEMU_RGB(0x00, 0x00, 0x00), /* black */ + [QEMU_COLOR_BLUE] = QEMU_RGB(0x00, 0x00, 0xaa), /* blue */ + [QEMU_COLOR_GREEN] = QEMU_RGB(0x00, 0xaa, 0x00), /* green */ + [QEMU_COLOR_CYAN] = QEMU_RGB(0x00, 0xaa, 0xaa), /* cyan */ + [QEMU_COLOR_RED] = QEMU_RGB(0xaa, 0x00, 0x00), /* red */ + [QEMU_COLOR_MAGENTA] = QEMU_RGB(0xaa, 0x00, 0xaa), /* magenta */ + [QEMU_COLOR_YELLOW] = QEMU_RGB(0xaa, 0xaa, 0x00), /* yellow */ + [QEMU_COLOR_WHITE] = QEMU_RGB(0xaa, 0xaa, 0xaa), /* white */ + }, + { /* bright */ + [QEMU_COLOR_BLACK] = QEMU_RGB(0x00, 0x00, 0x00), /* black */ + [QEMU_COLOR_BLUE] = QEMU_RGB(0x00, 0x00, 0xff), /* blue */ + [QEMU_COLOR_GREEN] = QEMU_RGB(0x00, 0xff, 0x00), /* green */ + [QEMU_COLOR_CYAN] = QEMU_RGB(0x00, 0xff, 0xff), /* cyan */ + [QEMU_COLOR_RED] = QEMU_RGB(0xff, 0x00, 0x00), /* red */ + [QEMU_COLOR_MAGENTA] = QEMU_RGB(0xff, 0x00, 0xff), /* magenta */ + [QEMU_COLOR_YELLOW] = QEMU_RGB(0xff, 0xff, 0x00), /* yellow */ + [QEMU_COLOR_WHITE] = QEMU_RGB(0xff, 0xff, 0xff), /* white */ + } +}; + +static void vga_putcharxy(QemuConsole *s, int x, int y, int ch, + TextAttributes *t_attrib) +{ + static pixman_image_t *glyphs[256]; + DisplaySurface *surface = qemu_console_surface(s); + pixman_color_t fgcol, bgcol; + + if (t_attrib->invers) { + bgcol = color_table_rgb[t_attrib->bold][t_attrib->fgcol]; + fgcol = color_table_rgb[t_attrib->bold][t_attrib->bgcol]; + } else { + fgcol = color_table_rgb[t_attrib->bold][t_attrib->fgcol]; + bgcol = color_table_rgb[t_attrib->bold][t_attrib->bgcol]; + } + + if (!glyphs[ch]) { + glyphs[ch] = qemu_pixman_glyph_from_vgafont(FONT_HEIGHT, vgafont16, ch); + } + qemu_pixman_glyph_render(glyphs[ch], surface->image, + &fgcol, &bgcol, x, y, FONT_WIDTH, FONT_HEIGHT); +} + +static void text_console_resize(QemuConsole *s) +{ + TextCell *cells, *c, *c1; + int w1, x, y, last_width; + + last_width = s->width; + s->width = surface_width(s->surface) / FONT_WIDTH; + s->height = surface_height(s->surface) / FONT_HEIGHT; + + w1 = last_width; + if (s->width < w1) + w1 = s->width; + + cells = g_new(TextCell, s->width * s->total_height + 1); + for(y = 0; y < s->total_height; y++) { + c = &cells[y * s->width]; + if (w1 > 0) { + c1 = &s->cells[y * last_width]; + for(x = 0; x < w1; x++) { + *c++ = *c1++; + } + } + for(x = w1; x < s->width; x++) { + c->ch = ' '; + c->t_attrib = s->t_attrib_default; + c++; + } + } + g_free(s->cells); + s->cells = cells; +} + +static inline void text_update_xy(QemuConsole *s, int x, int y) +{ + s->text_x[0] = MIN(s->text_x[0], x); + s->text_x[1] = MAX(s->text_x[1], x); + s->text_y[0] = MIN(s->text_y[0], y); + s->text_y[1] = MAX(s->text_y[1], y); +} + +static void invalidate_xy(QemuConsole *s, int x, int y) +{ + if (!qemu_console_is_visible(s)) { + return; + } + if (s->update_x0 > x * FONT_WIDTH) + s->update_x0 = x * FONT_WIDTH; + if (s->update_y0 > y * FONT_HEIGHT) + s->update_y0 = y * FONT_HEIGHT; + if (s->update_x1 < (x + 1) * FONT_WIDTH) + s->update_x1 = (x + 1) * FONT_WIDTH; + if (s->update_y1 < (y + 1) * FONT_HEIGHT) + s->update_y1 = (y + 1) * FONT_HEIGHT; +} + +static void update_xy(QemuConsole *s, int x, int y) +{ + TextCell *c; + int y1, y2; + + if (s->ds->have_text) { + text_update_xy(s, x, y); + } + + y1 = (s->y_base + y) % s->total_height; + y2 = y1 - s->y_displayed; + if (y2 < 0) { + y2 += s->total_height; + } + if (y2 < s->height) { + if (x >= s->width) { + x = s->width - 1; + } + c = &s->cells[y1 * s->width + x]; + vga_putcharxy(s, x, y2, c->ch, + &(c->t_attrib)); + invalidate_xy(s, x, y2); + } +} + +static void console_show_cursor(QemuConsole *s, int show) +{ + TextCell *c; + int y, y1; + int x = s->x; + + if (s->ds->have_text) { + s->cursor_invalidate = 1; + } + + if (x >= s->width) { + x = s->width - 1; + } + y1 = (s->y_base + s->y) % s->total_height; + y = y1 - s->y_displayed; + if (y < 0) { + y += s->total_height; + } + if (y < s->height) { + c = &s->cells[y1 * s->width + x]; + if (show && cursor_visible_phase) { + TextAttributes t_attrib = s->t_attrib_default; + t_attrib.invers = !(t_attrib.invers); /* invert fg and bg */ + vga_putcharxy(s, x, y, c->ch, &t_attrib); + } else { + vga_putcharxy(s, x, y, c->ch, &(c->t_attrib)); + } + invalidate_xy(s, x, y); + } +} + +static void console_refresh(QemuConsole *s) +{ + DisplaySurface *surface = qemu_console_surface(s); + TextCell *c; + int x, y, y1; + + if (s->ds->have_text) { + s->text_x[0] = 0; + s->text_y[0] = 0; + s->text_x[1] = s->width - 1; + s->text_y[1] = s->height - 1; + s->cursor_invalidate = 1; + } + + vga_fill_rect(s, 0, 0, surface_width(surface), surface_height(surface), + color_table_rgb[0][QEMU_COLOR_BLACK]); + y1 = s->y_displayed; + for (y = 0; y < s->height; y++) { + c = s->cells + y1 * s->width; + for (x = 0; x < s->width; x++) { + vga_putcharxy(s, x, y, c->ch, + &(c->t_attrib)); + c++; + } + if (++y1 == s->total_height) { + y1 = 0; + } + } + console_show_cursor(s, 1); + dpy_gfx_update(s, 0, 0, + surface_width(surface), surface_height(surface)); +} + +static void console_scroll(QemuConsole *s, int ydelta) +{ + int i, y1; + + if (ydelta > 0) { + for(i = 0; i < ydelta; i++) { + if (s->y_displayed == s->y_base) + break; + if (++s->y_displayed == s->total_height) + s->y_displayed = 0; + } + } else { + ydelta = -ydelta; + i = s->backscroll_height; + if (i > s->total_height - s->height) + i = s->total_height - s->height; + y1 = s->y_base - i; + if (y1 < 0) + y1 += s->total_height; + for(i = 0; i < ydelta; i++) { + if (s->y_displayed == y1) + break; + if (--s->y_displayed < 0) + s->y_displayed = s->total_height - 1; + } + } + console_refresh(s); +} + +static void console_put_lf(QemuConsole *s) +{ + TextCell *c; + int x, y1; + + s->y++; + if (s->y >= s->height) { + s->y = s->height - 1; + + if (s->y_displayed == s->y_base) { + if (++s->y_displayed == s->total_height) + s->y_displayed = 0; + } + if (++s->y_base == s->total_height) + s->y_base = 0; + if (s->backscroll_height < s->total_height) + s->backscroll_height++; + y1 = (s->y_base + s->height - 1) % s->total_height; + c = &s->cells[y1 * s->width]; + for(x = 0; x < s->width; x++) { + c->ch = ' '; + c->t_attrib = s->t_attrib_default; + c++; + } + if (s->y_displayed == s->y_base) { + if (s->ds->have_text) { + s->text_x[0] = 0; + s->text_y[0] = 0; + s->text_x[1] = s->width - 1; + s->text_y[1] = s->height - 1; + } + + vga_bitblt(s, 0, FONT_HEIGHT, 0, 0, + s->width * FONT_WIDTH, + (s->height - 1) * FONT_HEIGHT); + vga_fill_rect(s, 0, (s->height - 1) * FONT_HEIGHT, + s->width * FONT_WIDTH, FONT_HEIGHT, + color_table_rgb[0][s->t_attrib_default.bgcol]); + s->update_x0 = 0; + s->update_y0 = 0; + s->update_x1 = s->width * FONT_WIDTH; + s->update_y1 = s->height * FONT_HEIGHT; + } + } +} + +/* Set console attributes depending on the current escape codes. + * NOTE: I know this code is not very efficient (checking every color for it + * self) but it is more readable and better maintainable. + */ +static void console_handle_escape(QemuConsole *s) +{ + int i; + + for (i=0; i<s->nb_esc_params; i++) { + switch (s->esc_params[i]) { + case 0: /* reset all console attributes to default */ + s->t_attrib = s->t_attrib_default; + break; + case 1: + s->t_attrib.bold = 1; + break; + case 4: + s->t_attrib.uline = 1; + break; + case 5: + s->t_attrib.blink = 1; + break; + case 7: + s->t_attrib.invers = 1; + break; + case 8: + s->t_attrib.unvisible = 1; + break; + case 22: + s->t_attrib.bold = 0; + break; + case 24: + s->t_attrib.uline = 0; + break; + case 25: + s->t_attrib.blink = 0; + break; + case 27: + s->t_attrib.invers = 0; + break; + case 28: + s->t_attrib.unvisible = 0; + break; + /* set foreground color */ + case 30: + s->t_attrib.fgcol = QEMU_COLOR_BLACK; + break; + case 31: + s->t_attrib.fgcol = QEMU_COLOR_RED; + break; + case 32: + s->t_attrib.fgcol = QEMU_COLOR_GREEN; + break; + case 33: + s->t_attrib.fgcol = QEMU_COLOR_YELLOW; + break; + case 34: + s->t_attrib.fgcol = QEMU_COLOR_BLUE; + break; + case 35: + s->t_attrib.fgcol = QEMU_COLOR_MAGENTA; + break; + case 36: + s->t_attrib.fgcol = QEMU_COLOR_CYAN; + break; + case 37: + s->t_attrib.fgcol = QEMU_COLOR_WHITE; + break; + /* set background color */ + case 40: + s->t_attrib.bgcol = QEMU_COLOR_BLACK; + break; + case 41: + s->t_attrib.bgcol = QEMU_COLOR_RED; + break; + case 42: + s->t_attrib.bgcol = QEMU_COLOR_GREEN; + break; + case 43: + s->t_attrib.bgcol = QEMU_COLOR_YELLOW; + break; + case 44: + s->t_attrib.bgcol = QEMU_COLOR_BLUE; + break; + case 45: + s->t_attrib.bgcol = QEMU_COLOR_MAGENTA; + break; + case 46: + s->t_attrib.bgcol = QEMU_COLOR_CYAN; + break; + case 47: + s->t_attrib.bgcol = QEMU_COLOR_WHITE; + break; + } + } +} + +static void console_clear_xy(QemuConsole *s, int x, int y) +{ + int y1 = (s->y_base + y) % s->total_height; + if (x >= s->width) { + x = s->width - 1; + } + TextCell *c = &s->cells[y1 * s->width + x]; + c->ch = ' '; + c->t_attrib = s->t_attrib_default; + update_xy(s, x, y); +} + +static void console_put_one(QemuConsole *s, int ch) +{ + TextCell *c; + int y1; + if (s->x >= s->width) { + /* line wrap */ + s->x = 0; + console_put_lf(s); + } + y1 = (s->y_base + s->y) % s->total_height; + c = &s->cells[y1 * s->width + s->x]; + c->ch = ch; + c->t_attrib = s->t_attrib; + update_xy(s, s->x, s->y); + s->x++; +} + +static void console_respond_str(QemuConsole *s, const char *buf) +{ + while (*buf) { + console_put_one(s, *buf); + buf++; + } +} + +/* set cursor, checking bounds */ +static void set_cursor(QemuConsole *s, int x, int y) +{ + if (x < 0) { + x = 0; + } + if (y < 0) { + y = 0; + } + if (y >= s->height) { + y = s->height - 1; + } + if (x >= s->width) { + x = s->width - 1; + } + + s->x = x; + s->y = y; +} + +static void console_putchar(QemuConsole *s, int ch) +{ + int i; + int x, y; + char response[40]; + + switch(s->state) { + case TTY_STATE_NORM: + switch(ch) { + case '\r': /* carriage return */ + s->x = 0; + break; + case '\n': /* newline */ + console_put_lf(s); + break; + case '\b': /* backspace */ + if (s->x > 0) + s->x--; + break; + case '\t': /* tabspace */ + if (s->x + (8 - (s->x % 8)) > s->width) { + s->x = 0; + console_put_lf(s); + } else { + s->x = s->x + (8 - (s->x % 8)); + } + break; + case '\a': /* alert aka. bell */ + /* TODO: has to be implemented */ + break; + case 14: + /* SI (shift in), character set 0 (ignored) */ + break; + case 15: + /* SO (shift out), character set 1 (ignored) */ + break; + case 27: /* esc (introducing an escape sequence) */ + s->state = TTY_STATE_ESC; + break; + default: + console_put_one(s, ch); + break; + } + break; + case TTY_STATE_ESC: /* check if it is a terminal escape sequence */ + if (ch == '[') { + for(i=0;i<MAX_ESC_PARAMS;i++) + s->esc_params[i] = 0; + s->nb_esc_params = 0; + s->state = TTY_STATE_CSI; + } else { + s->state = TTY_STATE_NORM; + } + break; + case TTY_STATE_CSI: /* handle escape sequence parameters */ + if (ch >= '0' && ch <= '9') { + if (s->nb_esc_params < MAX_ESC_PARAMS) { + int *param = &s->esc_params[s->nb_esc_params]; + int digit = (ch - '0'); + + *param = (*param <= (INT_MAX - digit) / 10) ? + *param * 10 + digit : INT_MAX; + } + } else { + if (s->nb_esc_params < MAX_ESC_PARAMS) + s->nb_esc_params++; + if (ch == ';' || ch == '?') { + break; + } + trace_console_putchar_csi(s->esc_params[0], s->esc_params[1], + ch, s->nb_esc_params); + s->state = TTY_STATE_NORM; + switch(ch) { + case 'A': + /* move cursor up */ + if (s->esc_params[0] == 0) { + s->esc_params[0] = 1; + } + set_cursor(s, s->x, s->y - s->esc_params[0]); + break; + case 'B': + /* move cursor down */ + if (s->esc_params[0] == 0) { + s->esc_params[0] = 1; + } + set_cursor(s, s->x, s->y + s->esc_params[0]); + break; + case 'C': + /* move cursor right */ + if (s->esc_params[0] == 0) { + s->esc_params[0] = 1; + } + set_cursor(s, s->x + s->esc_params[0], s->y); + break; + case 'D': + /* move cursor left */ + if (s->esc_params[0] == 0) { + s->esc_params[0] = 1; + } + set_cursor(s, s->x - s->esc_params[0], s->y); + break; + case 'G': + /* move cursor to column */ + set_cursor(s, s->esc_params[0] - 1, s->y); + break; + case 'f': + case 'H': + /* move cursor to row, column */ + set_cursor(s, s->esc_params[1] - 1, s->esc_params[0] - 1); + break; + case 'J': + switch (s->esc_params[0]) { + case 0: + /* clear to end of screen */ + for (y = s->y; y < s->height; y++) { + for (x = 0; x < s->width; x++) { + if (y == s->y && x < s->x) { + continue; + } + console_clear_xy(s, x, y); + } + } + break; + case 1: + /* clear from beginning of screen */ + for (y = 0; y <= s->y; y++) { + for (x = 0; x < s->width; x++) { + if (y == s->y && x > s->x) { + break; + } + console_clear_xy(s, x, y); + } + } + break; + case 2: + /* clear entire screen */ + for (y = 0; y <= s->height; y++) { + for (x = 0; x < s->width; x++) { + console_clear_xy(s, x, y); + } + } + break; + } + break; + case 'K': + switch (s->esc_params[0]) { + case 0: + /* clear to eol */ + for(x = s->x; x < s->width; x++) { + console_clear_xy(s, x, s->y); + } + break; + case 1: + /* clear from beginning of line */ + for (x = 0; x <= s->x && x < s->width; x++) { + console_clear_xy(s, x, s->y); + } + break; + case 2: + /* clear entire line */ + for(x = 0; x < s->width; x++) { + console_clear_xy(s, x, s->y); + } + break; + } + break; + case 'm': + console_handle_escape(s); + break; + case 'n': + switch (s->esc_params[0]) { + case 5: + /* report console status (always succeed)*/ + console_respond_str(s, "\033[0n"); + break; + case 6: + /* report cursor position */ + sprintf(response, "\033[%d;%dR", + (s->y_base + s->y) % s->total_height + 1, + s->x + 1); + console_respond_str(s, response); + break; + } + break; + case 's': + /* save cursor position */ + s->x_saved = s->x; + s->y_saved = s->y; + break; + case 'u': + /* restore cursor position */ + s->x = s->x_saved; + s->y = s->y_saved; + break; + default: + trace_console_putchar_unhandled(ch); + break; + } + break; + } + } +} + +void console_select(unsigned int index) +{ + DisplayChangeListener *dcl; + QemuConsole *s; + + trace_console_select(index); + s = qemu_console_lookup_by_index(index); + if (s) { + DisplayState *ds = s->ds; + + active_console = s; + if (ds->have_gfx) { + QLIST_FOREACH(dcl, &ds->listeners, next) { + if (dcl->con != NULL) { + continue; + } + if (dcl->ops->dpy_gfx_switch) { + dcl->ops->dpy_gfx_switch(dcl, s->surface); + } + } + if (s->surface) { + dpy_gfx_update(s, 0, 0, surface_width(s->surface), + surface_height(s->surface)); + } + } + if (ds->have_text) { + dpy_text_resize(s, s->width, s->height); + } + text_console_update_cursor(NULL); + } +} + +struct VCChardev { + Chardev parent; + QemuConsole *console; +}; +typedef struct VCChardev VCChardev; + +#define TYPE_CHARDEV_VC "chardev-vc" +DECLARE_INSTANCE_CHECKER(VCChardev, VC_CHARDEV, + TYPE_CHARDEV_VC) + +static int vc_chr_write(Chardev *chr, const uint8_t *buf, int len) +{ + VCChardev *drv = VC_CHARDEV(chr); + QemuConsole *s = drv->console; + int i; + + if (!s->ds) { + return 0; + } + + s->update_x0 = s->width * FONT_WIDTH; + s->update_y0 = s->height * FONT_HEIGHT; + s->update_x1 = 0; + s->update_y1 = 0; + console_show_cursor(s, 0); + for(i = 0; i < len; i++) { + console_putchar(s, buf[i]); + } + console_show_cursor(s, 1); + if (s->ds->have_gfx && s->update_x0 < s->update_x1) { + dpy_gfx_update(s, s->update_x0, s->update_y0, + s->update_x1 - s->update_x0, + s->update_y1 - s->update_y0); + } + return len; +} + +static void kbd_send_chars(QemuConsole *s) +{ + uint32_t len, avail; + + len = qemu_chr_be_can_write(s->chr); + avail = fifo8_num_used(&s->out_fifo); + while (len > 0 && avail > 0) { + const uint8_t *buf; + uint32_t size; + + buf = fifo8_pop_buf(&s->out_fifo, MIN(len, avail), &size); + qemu_chr_be_write(s->chr, (uint8_t *)buf, size); + len = qemu_chr_be_can_write(s->chr); + avail -= size; + } +} + +/* called when an ascii key is pressed */ +void kbd_put_keysym_console(QemuConsole *s, int keysym) +{ + uint8_t buf[16], *q; + int c; + uint32_t num_free; + + if (!s || (s->console_type == GRAPHIC_CONSOLE)) + return; + + switch(keysym) { + case QEMU_KEY_CTRL_UP: + console_scroll(s, -1); + break; + case QEMU_KEY_CTRL_DOWN: + console_scroll(s, 1); + break; + case QEMU_KEY_CTRL_PAGEUP: + console_scroll(s, -10); + break; + case QEMU_KEY_CTRL_PAGEDOWN: + console_scroll(s, 10); + break; + default: + /* convert the QEMU keysym to VT100 key string */ + q = buf; + if (keysym >= 0xe100 && keysym <= 0xe11f) { + *q++ = '\033'; + *q++ = '['; + c = keysym - 0xe100; + if (c >= 10) + *q++ = '0' + (c / 10); + *q++ = '0' + (c % 10); + *q++ = '~'; + } else if (keysym >= 0xe120 && keysym <= 0xe17f) { + *q++ = '\033'; + *q++ = '['; + *q++ = keysym & 0xff; + } else if (s->echo && (keysym == '\r' || keysym == '\n')) { + vc_chr_write(s->chr, (const uint8_t *) "\r", 1); + *q++ = '\n'; + } else { + *q++ = keysym; + } + if (s->echo) { + vc_chr_write(s->chr, buf, q - buf); + } + num_free = fifo8_num_free(&s->out_fifo); + fifo8_push_all(&s->out_fifo, buf, MIN(num_free, q - buf)); + kbd_send_chars(s); + break; + } +} + +static const int qcode_to_keysym[Q_KEY_CODE__MAX] = { + [Q_KEY_CODE_UP] = QEMU_KEY_UP, + [Q_KEY_CODE_DOWN] = QEMU_KEY_DOWN, + [Q_KEY_CODE_RIGHT] = QEMU_KEY_RIGHT, + [Q_KEY_CODE_LEFT] = QEMU_KEY_LEFT, + [Q_KEY_CODE_HOME] = QEMU_KEY_HOME, + [Q_KEY_CODE_END] = QEMU_KEY_END, + [Q_KEY_CODE_PGUP] = QEMU_KEY_PAGEUP, + [Q_KEY_CODE_PGDN] = QEMU_KEY_PAGEDOWN, + [Q_KEY_CODE_DELETE] = QEMU_KEY_DELETE, + [Q_KEY_CODE_BACKSPACE] = QEMU_KEY_BACKSPACE, +}; + +static const int ctrl_qcode_to_keysym[Q_KEY_CODE__MAX] = { + [Q_KEY_CODE_UP] = QEMU_KEY_CTRL_UP, + [Q_KEY_CODE_DOWN] = QEMU_KEY_CTRL_DOWN, + [Q_KEY_CODE_RIGHT] = QEMU_KEY_CTRL_RIGHT, + [Q_KEY_CODE_LEFT] = QEMU_KEY_CTRL_LEFT, + [Q_KEY_CODE_HOME] = QEMU_KEY_CTRL_HOME, + [Q_KEY_CODE_END] = QEMU_KEY_CTRL_END, + [Q_KEY_CODE_PGUP] = QEMU_KEY_CTRL_PAGEUP, + [Q_KEY_CODE_PGDN] = QEMU_KEY_CTRL_PAGEDOWN, +}; + +bool kbd_put_qcode_console(QemuConsole *s, int qcode, bool ctrl) +{ + int keysym; + + keysym = ctrl ? ctrl_qcode_to_keysym[qcode] : qcode_to_keysym[qcode]; + if (keysym == 0) { + return false; + } + kbd_put_keysym_console(s, keysym); + return true; +} + +void kbd_put_string_console(QemuConsole *s, const char *str, int len) +{ + int i; + + for (i = 0; i < len && str[i]; i++) { + kbd_put_keysym_console(s, str[i]); + } +} + +void kbd_put_keysym(int keysym) +{ + kbd_put_keysym_console(active_console, keysym); +} + +static void text_console_invalidate(void *opaque) +{ + QemuConsole *s = (QemuConsole *) opaque; + + if (s->ds->have_text && s->console_type == TEXT_CONSOLE) { + text_console_resize(s); + } + console_refresh(s); +} + +static void text_console_update(void *opaque, console_ch_t *chardata) +{ + QemuConsole *s = (QemuConsole *) opaque; + int i, j, src; + + if (s->text_x[0] <= s->text_x[1]) { + src = (s->y_base + s->text_y[0]) * s->width; + chardata += s->text_y[0] * s->width; + for (i = s->text_y[0]; i <= s->text_y[1]; i ++) + for (j = 0; j < s->width; j++, src++) { + console_write_ch(chardata ++, + ATTR2CHTYPE(s->cells[src].ch, + s->cells[src].t_attrib.fgcol, + s->cells[src].t_attrib.bgcol, + s->cells[src].t_attrib.bold)); + } + dpy_text_update(s, s->text_x[0], s->text_y[0], + s->text_x[1] - s->text_x[0], i - s->text_y[0]); + s->text_x[0] = s->width; + s->text_y[0] = s->height; + s->text_x[1] = 0; + s->text_y[1] = 0; + } + if (s->cursor_invalidate) { + dpy_text_cursor(s, s->x, s->y); + s->cursor_invalidate = 0; + } +} + +static QemuConsole *new_console(DisplayState *ds, console_type_t console_type, + uint32_t head) +{ + Object *obj; + QemuConsole *s; + int i; + + obj = object_new(TYPE_QEMU_CONSOLE); + s = QEMU_CONSOLE(obj); + qemu_co_queue_init(&s->dump_queue); + s->head = head; + object_property_add_link(obj, "device", TYPE_DEVICE, + (Object **)&s->device, + object_property_allow_set_link, + OBJ_PROP_LINK_STRONG); + object_property_add_uint32_ptr(obj, "head", &s->head, + OBJ_PROP_FLAG_READ); + + if (!active_console || ((active_console->console_type != GRAPHIC_CONSOLE) && + (console_type == GRAPHIC_CONSOLE))) { + active_console = s; + } + s->ds = ds; + s->console_type = console_type; + s->window_id = -1; + + if (QTAILQ_EMPTY(&consoles)) { + s->index = 0; + QTAILQ_INSERT_TAIL(&consoles, s, next); + } else if (console_type != GRAPHIC_CONSOLE || phase_check(PHASE_MACHINE_READY)) { + QemuConsole *last = QTAILQ_LAST(&consoles); + s->index = last->index + 1; + QTAILQ_INSERT_TAIL(&consoles, s, next); + } else { + /* + * HACK: Put graphical consoles before text consoles. + * + * Only do that for coldplugged devices. After initial device + * initialization we will not renumber the consoles any more. + */ + QemuConsole *c = QTAILQ_FIRST(&consoles); + + while (QTAILQ_NEXT(c, next) != NULL && + c->console_type == GRAPHIC_CONSOLE) { + c = QTAILQ_NEXT(c, next); + } + if (c->console_type == GRAPHIC_CONSOLE) { + /* have no text consoles */ + s->index = c->index + 1; + QTAILQ_INSERT_AFTER(&consoles, c, s, next); + } else { + s->index = c->index; + QTAILQ_INSERT_BEFORE(c, s, next); + /* renumber text consoles */ + for (i = s->index + 1; c != NULL; c = QTAILQ_NEXT(c, next), i++) { + c->index = i; + } + } + } + return s; +} + +DisplaySurface *qemu_create_displaysurface(int width, int height) +{ + DisplaySurface *surface = g_new0(DisplaySurface, 1); + + trace_displaysurface_create(surface, width, height); + surface->format = PIXMAN_x8r8g8b8; + surface->image = pixman_image_create_bits(surface->format, + width, height, + NULL, width * 4); + assert(surface->image != NULL); + surface->flags = QEMU_ALLOCATED_FLAG; + + return surface; +} + +DisplaySurface *qemu_create_displaysurface_from(int width, int height, + pixman_format_code_t format, + int linesize, uint8_t *data) +{ + DisplaySurface *surface = g_new0(DisplaySurface, 1); + + trace_displaysurface_create_from(surface, width, height, format); + surface->format = format; + surface->image = pixman_image_create_bits(surface->format, + width, height, + (void *)data, linesize); + assert(surface->image != NULL); + + return surface; +} + +DisplaySurface *qemu_create_displaysurface_pixman(pixman_image_t *image) +{ + DisplaySurface *surface = g_new0(DisplaySurface, 1); + + trace_displaysurface_create_pixman(surface); + surface->format = pixman_image_get_format(image); + surface->image = pixman_image_ref(image); + + return surface; +} + +DisplaySurface *qemu_create_placeholder_surface(int w, int h, + const char *msg) +{ + DisplaySurface *surface = qemu_create_displaysurface(w, h); + pixman_color_t bg = color_table_rgb[0][QEMU_COLOR_BLACK]; + pixman_color_t fg = color_table_rgb[0][QEMU_COLOR_WHITE]; + pixman_image_t *glyph; + int len, x, y, i; + + len = strlen(msg); + x = (w / FONT_WIDTH - len) / 2; + y = (h / FONT_HEIGHT - 1) / 2; + for (i = 0; i < len; i++) { + glyph = qemu_pixman_glyph_from_vgafont(FONT_HEIGHT, vgafont16, msg[i]); + qemu_pixman_glyph_render(glyph, surface->image, &fg, &bg, + x+i, y, FONT_WIDTH, FONT_HEIGHT); + qemu_pixman_image_unref(glyph); + } + surface->flags |= QEMU_PLACEHOLDER_FLAG; + return surface; +} + +void qemu_free_displaysurface(DisplaySurface *surface) +{ + if (surface == NULL) { + return; + } + trace_displaysurface_free(surface); + qemu_pixman_image_unref(surface->image); + g_free(surface); +} + +bool console_has_gl(QemuConsole *con) +{ + return con->gl != NULL; +} + +static bool displaychangelistener_has_dmabuf(DisplayChangeListener *dcl) +{ + if (dcl->ops->dpy_has_dmabuf) { + return dcl->ops->dpy_has_dmabuf(dcl); + } + + if (dcl->ops->dpy_gl_scanout_dmabuf) { + return true; + } + + return false; +} + +static bool dpy_compatible_with(QemuConsole *con, + DisplayChangeListener *dcl, Error **errp) +{ + int flags; + + flags = con->hw_ops->get_flags ? con->hw_ops->get_flags(con->hw) : 0; + + if (flags & GRAPHIC_FLAGS_GL && + !console_has_gl(con)) { + error_setg(errp, "The console requires a GL context."); + return false; + + } + + if (flags & GRAPHIC_FLAGS_DMABUF && + !displaychangelistener_has_dmabuf(dcl)) { + error_setg(errp, "The console requires display DMABUF support."); + return false; + } + + return true; +} + +void register_displaychangelistener(DisplayChangeListener *dcl) +{ + static const char nodev[] = + "This VM has no graphic display device."; + static DisplaySurface *dummy; + QemuConsole *con; + + assert(!dcl->ds); + + if (dcl->ops->dpy_gl_ctx_create) { + /* display has opengl support */ + assert(dcl->con); + if (dcl->con->gl) { + fprintf(stderr, "can't register two opengl displays (%s, %s)\n", + dcl->ops->dpy_name, dcl->con->gl->ops->dpy_name); + exit(1); + } + dcl->con->gl = dcl; + } + + if (dcl->con) { + dpy_compatible_with(dcl->con, dcl, &error_fatal); + } + + trace_displaychangelistener_register(dcl, dcl->ops->dpy_name); + dcl->ds = get_alloc_displaystate(); + QLIST_INSERT_HEAD(&dcl->ds->listeners, dcl, next); + gui_setup_refresh(dcl->ds); + if (dcl->con) { + dcl->con->dcls++; + con = dcl->con; + } else { + con = active_console; + } + if (dcl->ops->dpy_gfx_switch) { + if (con) { + dcl->ops->dpy_gfx_switch(dcl, con->surface); + } else { + if (!dummy) { + dummy = qemu_create_placeholder_surface(640, 480, nodev); + } + dcl->ops->dpy_gfx_switch(dcl, dummy); + } + } + text_console_update_cursor(NULL); +} + +void update_displaychangelistener(DisplayChangeListener *dcl, + uint64_t interval) +{ + DisplayState *ds = dcl->ds; + + dcl->update_interval = interval; + if (!ds->refreshing && ds->update_interval > interval) { + timer_mod(ds->gui_timer, ds->last_update + interval); + } +} + +void unregister_displaychangelistener(DisplayChangeListener *dcl) +{ + DisplayState *ds = dcl->ds; + trace_displaychangelistener_unregister(dcl, dcl->ops->dpy_name); + if (dcl->con) { + dcl->con->dcls--; + } + QLIST_REMOVE(dcl, next); + dcl->ds = NULL; + gui_setup_refresh(ds); +} + +static void dpy_set_ui_info_timer(void *opaque) +{ + QemuConsole *con = opaque; + + con->hw_ops->ui_info(con->hw, con->head, &con->ui_info); +} + +bool dpy_ui_info_supported(QemuConsole *con) +{ + if (con == NULL) { + con = active_console; + } + + return con->hw_ops->ui_info != NULL; +} + +const QemuUIInfo *dpy_get_ui_info(const QemuConsole *con) +{ + if (con == NULL) { + con = active_console; + } + + return &con->ui_info; +} + +int dpy_set_ui_info(QemuConsole *con, QemuUIInfo *info) +{ + if (con == NULL) { + con = active_console; + } + + if (!dpy_ui_info_supported(con)) { + return -1; + } + if (memcmp(&con->ui_info, info, sizeof(con->ui_info)) == 0) { + /* nothing changed -- ignore */ + return 0; + } + + /* + * Typically we get a flood of these as the user resizes the window. + * Wait until the dust has settled (one second without updates), then + * go notify the guest. + */ + con->ui_info = *info; + timer_mod(con->ui_timer, qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + 1000); + return 0; +} + +void dpy_gfx_update(QemuConsole *con, int x, int y, int w, int h) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + int width = w; + int height = h; + + if (con->surface) { + width = surface_width(con->surface); + height = surface_height(con->surface); + } + x = MAX(x, 0); + y = MAX(y, 0); + x = MIN(x, width); + y = MIN(y, height); + w = MIN(w, width - x); + h = MIN(h, height - y); + + if (!qemu_console_is_visible(con)) { + return; + } + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_gfx_update) { + dcl->ops->dpy_gfx_update(dcl, x, y, w, h); + } + } +} + +void dpy_gfx_update_full(QemuConsole *con) +{ + if (!con->surface) { + return; + } + dpy_gfx_update(con, 0, 0, + surface_width(con->surface), + surface_height(con->surface)); +} + +void dpy_gfx_replace_surface(QemuConsole *con, + DisplaySurface *surface) +{ + static const char placeholder_msg[] = "Display output is not active."; + DisplayState *s = con->ds; + DisplaySurface *old_surface = con->surface; + DisplayChangeListener *dcl; + int width; + int height; + + if (!surface) { + if (old_surface) { + width = surface_width(old_surface); + height = surface_height(old_surface); + } else { + width = 640; + height = 480; + } + + surface = qemu_create_placeholder_surface(width, height, placeholder_msg); + } + + assert(old_surface != surface); + + con->surface = surface; + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_gfx_switch) { + dcl->ops->dpy_gfx_switch(dcl, surface); + } + } + qemu_free_displaysurface(old_surface); +} + +bool dpy_gfx_check_format(QemuConsole *con, + pixman_format_code_t format) +{ + DisplayChangeListener *dcl; + DisplayState *s = con->ds; + + QLIST_FOREACH(dcl, &s->listeners, next) { + if (dcl->con && dcl->con != con) { + /* dcl bound to another console -> skip */ + continue; + } + if (dcl->ops->dpy_gfx_check_format) { + if (!dcl->ops->dpy_gfx_check_format(dcl, format)) { + return false; + } + } else { + /* default is to allow native 32 bpp only */ + if (format != qemu_default_pixman_format(32, true)) { + return false; + } + } + } + return true; +} + +static void dpy_refresh(DisplayState *s) +{ + DisplayChangeListener *dcl; + + QLIST_FOREACH(dcl, &s->listeners, next) { + if (dcl->ops->dpy_refresh) { + dcl->ops->dpy_refresh(dcl); + } + } +} + +void dpy_text_cursor(QemuConsole *con, int x, int y) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + if (!qemu_console_is_visible(con)) { + return; + } + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_text_cursor) { + dcl->ops->dpy_text_cursor(dcl, x, y); + } + } +} + +void dpy_text_update(QemuConsole *con, int x, int y, int w, int h) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + if (!qemu_console_is_visible(con)) { + return; + } + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_text_update) { + dcl->ops->dpy_text_update(dcl, x, y, w, h); + } + } +} + +void dpy_text_resize(QemuConsole *con, int w, int h) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + if (!qemu_console_is_visible(con)) { + return; + } + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_text_resize) { + dcl->ops->dpy_text_resize(dcl, w, h); + } + } +} + +void dpy_mouse_set(QemuConsole *con, int x, int y, int on) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + if (!qemu_console_is_visible(con)) { + return; + } + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_mouse_set) { + dcl->ops->dpy_mouse_set(dcl, x, y, on); + } + } +} + +void dpy_cursor_define(QemuConsole *con, QEMUCursor *cursor) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + if (!qemu_console_is_visible(con)) { + return; + } + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_cursor_define) { + dcl->ops->dpy_cursor_define(dcl, cursor); + } + } +} + +bool dpy_cursor_define_supported(QemuConsole *con) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + QLIST_FOREACH(dcl, &s->listeners, next) { + if (dcl->ops->dpy_cursor_define) { + return true; + } + } + return false; +} + +QEMUGLContext dpy_gl_ctx_create(QemuConsole *con, + struct QEMUGLParams *qparams) +{ + assert(con->gl); + return con->gl->ops->dpy_gl_ctx_create(con->gl, qparams); +} + +void dpy_gl_ctx_destroy(QemuConsole *con, QEMUGLContext ctx) +{ + assert(con->gl); + con->gl->ops->dpy_gl_ctx_destroy(con->gl, ctx); +} + +int dpy_gl_ctx_make_current(QemuConsole *con, QEMUGLContext ctx) +{ + assert(con->gl); + return con->gl->ops->dpy_gl_ctx_make_current(con->gl, ctx); +} + +void dpy_gl_scanout_disable(QemuConsole *con) +{ + assert(con->gl); + con->gl->ops->dpy_gl_scanout_disable(con->gl); +} + +void dpy_gl_scanout_texture(QemuConsole *con, + uint32_t backing_id, + bool backing_y_0_top, + uint32_t backing_width, + uint32_t backing_height, + uint32_t x, uint32_t y, + uint32_t width, uint32_t height) +{ + assert(con->gl); + con->gl->ops->dpy_gl_scanout_texture(con->gl, backing_id, + backing_y_0_top, + backing_width, backing_height, + x, y, width, height); +} + +void dpy_gl_scanout_dmabuf(QemuConsole *con, + QemuDmaBuf *dmabuf) +{ + assert(con->gl); + con->gl->ops->dpy_gl_scanout_dmabuf(con->gl, dmabuf); +} + +void dpy_gl_cursor_dmabuf(QemuConsole *con, QemuDmaBuf *dmabuf, + bool have_hot, uint32_t hot_x, uint32_t hot_y) +{ + assert(con->gl); + + if (con->gl->ops->dpy_gl_cursor_dmabuf) { + con->gl->ops->dpy_gl_cursor_dmabuf(con->gl, dmabuf, + have_hot, hot_x, hot_y); + } +} + +void dpy_gl_cursor_position(QemuConsole *con, + uint32_t pos_x, uint32_t pos_y) +{ + assert(con->gl); + + if (con->gl->ops->dpy_gl_cursor_position) { + con->gl->ops->dpy_gl_cursor_position(con->gl, pos_x, pos_y); + } +} + +void dpy_gl_release_dmabuf(QemuConsole *con, + QemuDmaBuf *dmabuf) +{ + assert(con->gl); + + if (con->gl->ops->dpy_gl_release_dmabuf) { + con->gl->ops->dpy_gl_release_dmabuf(con->gl, dmabuf); + } +} + +void dpy_gl_update(QemuConsole *con, + uint32_t x, uint32_t y, uint32_t w, uint32_t h) +{ + assert(con->gl); + con->gl->ops->dpy_gl_update(con->gl, x, y, w, h); +} + +/***********************************************************/ +/* register display */ + +/* console.c internal use only */ +static DisplayState *get_alloc_displaystate(void) +{ + if (!display_state) { + display_state = g_new0(DisplayState, 1); + cursor_timer = timer_new_ms(QEMU_CLOCK_REALTIME, + text_console_update_cursor, NULL); + } + return display_state; +} + +/* + * Called by main(), after creating QemuConsoles + * and before initializing ui (sdl/vnc/...). + */ +DisplayState *init_displaystate(void) +{ + gchar *name; + QemuConsole *con; + + get_alloc_displaystate(); + QTAILQ_FOREACH(con, &consoles, next) { + if (con->console_type != GRAPHIC_CONSOLE && + con->ds == NULL) { + text_console_do_init(con->chr, display_state); + } + + /* Hook up into the qom tree here (not in new_console()), once + * all QemuConsoles are created and the order / numbering + * doesn't change any more */ + name = g_strdup_printf("console[%d]", con->index); + object_property_add_child(container_get(object_get_root(), "/backend"), + name, OBJECT(con)); + g_free(name); + } + + return display_state; +} + +void graphic_console_set_hwops(QemuConsole *con, + const GraphicHwOps *hw_ops, + void *opaque) +{ + con->hw_ops = hw_ops; + con->hw = opaque; +} + +QemuConsole *graphic_console_init(DeviceState *dev, uint32_t head, + const GraphicHwOps *hw_ops, + void *opaque) +{ + static const char noinit[] = + "Guest has not initialized the display (yet)."; + int width = 640; + int height = 480; + QemuConsole *s; + DisplayState *ds; + DisplaySurface *surface; + + ds = get_alloc_displaystate(); + s = qemu_console_lookup_unused(); + if (s) { + trace_console_gfx_reuse(s->index); + if (s->surface) { + width = surface_width(s->surface); + height = surface_height(s->surface); + } + } else { + trace_console_gfx_new(); + s = new_console(ds, GRAPHIC_CONSOLE, head); + s->ui_timer = timer_new_ms(QEMU_CLOCK_REALTIME, + dpy_set_ui_info_timer, s); + } + graphic_console_set_hwops(s, hw_ops, opaque); + if (dev) { + object_property_set_link(OBJECT(s), "device", OBJECT(dev), + &error_abort); + } + + surface = qemu_create_placeholder_surface(width, height, noinit); + dpy_gfx_replace_surface(s, surface); + return s; +} + +static const GraphicHwOps unused_ops = { + /* no callbacks */ +}; + +void graphic_console_close(QemuConsole *con) +{ + static const char unplugged[] = + "Guest display has been unplugged"; + DisplaySurface *surface; + int width = 640; + int height = 480; + + if (con->surface) { + width = surface_width(con->surface); + height = surface_height(con->surface); + } + + trace_console_gfx_close(con->index); + object_property_set_link(OBJECT(con), "device", NULL, &error_abort); + graphic_console_set_hwops(con, &unused_ops, NULL); + + if (con->gl) { + dpy_gl_scanout_disable(con); + } + surface = qemu_create_placeholder_surface(width, height, unplugged); + dpy_gfx_replace_surface(con, surface); +} + +QemuConsole *qemu_console_lookup_by_index(unsigned int index) +{ + QemuConsole *con; + + QTAILQ_FOREACH(con, &consoles, next) { + if (con->index == index) { + return con; + } + } + return NULL; +} + +QemuConsole *qemu_console_lookup_by_device(DeviceState *dev, uint32_t head) +{ + QemuConsole *con; + Object *obj; + uint32_t h; + + QTAILQ_FOREACH(con, &consoles, next) { + obj = object_property_get_link(OBJECT(con), + "device", &error_abort); + if (DEVICE(obj) != dev) { + continue; + } + h = object_property_get_uint(OBJECT(con), + "head", &error_abort); + if (h != head) { + continue; + } + return con; + } + return NULL; +} + +QemuConsole *qemu_console_lookup_by_device_name(const char *device_id, + uint32_t head, Error **errp) +{ + DeviceState *dev; + QemuConsole *con; + + dev = qdev_find_recursive(sysbus_get_default(), device_id); + if (dev == NULL) { + error_set(errp, ERROR_CLASS_DEVICE_NOT_FOUND, + "Device '%s' not found", device_id); + return NULL; + } + + con = qemu_console_lookup_by_device(dev, head); + if (con == NULL) { + error_setg(errp, "Device %s (head %d) is not bound to a QemuConsole", + device_id, head); + return NULL; + } + + return con; +} + +QemuConsole *qemu_console_lookup_unused(void) +{ + QemuConsole *con; + Object *obj; + + QTAILQ_FOREACH(con, &consoles, next) { + if (con->hw_ops != &unused_ops) { + continue; + } + obj = object_property_get_link(OBJECT(con), + "device", &error_abort); + if (obj != NULL) { + continue; + } + return con; + } + return NULL; +} + +bool qemu_console_is_visible(QemuConsole *con) +{ + return (con == active_console) || (con->dcls > 0); +} + +bool qemu_console_is_graphic(QemuConsole *con) +{ + if (con == NULL) { + con = active_console; + } + return con && (con->console_type == GRAPHIC_CONSOLE); +} + +bool qemu_console_is_fixedsize(QemuConsole *con) +{ + if (con == NULL) { + con = active_console; + } + return con && (con->console_type != TEXT_CONSOLE); +} + +bool qemu_console_is_gl_blocked(QemuConsole *con) +{ + assert(con != NULL); + return con->gl_block; +} + +char *qemu_console_get_label(QemuConsole *con) +{ + if (con->console_type == GRAPHIC_CONSOLE) { + if (con->device) { + return g_strdup(object_get_typename(con->device)); + } + return g_strdup("VGA"); + } else { + if (con->chr && con->chr->label) { + return g_strdup(con->chr->label); + } + return g_strdup_printf("vc%d", con->index); + } +} + +int qemu_console_get_index(QemuConsole *con) +{ + if (con == NULL) { + con = active_console; + } + return con ? con->index : -1; +} + +uint32_t qemu_console_get_head(QemuConsole *con) +{ + if (con == NULL) { + con = active_console; + } + return con ? con->head : -1; +} + +int qemu_console_get_width(QemuConsole *con, int fallback) +{ + if (con == NULL) { + con = active_console; + } + return con ? surface_width(con->surface) : fallback; +} + +int qemu_console_get_height(QemuConsole *con, int fallback) +{ + if (con == NULL) { + con = active_console; + } + return con ? surface_height(con->surface) : fallback; +} + +static void vc_chr_accept_input(Chardev *chr) +{ + VCChardev *drv = VC_CHARDEV(chr); + QemuConsole *s = drv->console; + + kbd_send_chars(s); +} + +static void vc_chr_set_echo(Chardev *chr, bool echo) +{ + VCChardev *drv = VC_CHARDEV(chr); + QemuConsole *s = drv->console; + + s->echo = echo; +} + +static void text_console_update_cursor_timer(void) +{ + timer_mod(cursor_timer, qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + + CONSOLE_CURSOR_PERIOD / 2); +} + +static void text_console_update_cursor(void *opaque) +{ + QemuConsole *s; + int count = 0; + + cursor_visible_phase = !cursor_visible_phase; + + QTAILQ_FOREACH(s, &consoles, next) { + if (qemu_console_is_graphic(s) || + !qemu_console_is_visible(s)) { + continue; + } + count++; + graphic_hw_invalidate(s); + } + + if (count) { + text_console_update_cursor_timer(); + } +} + +static const GraphicHwOps text_console_ops = { + .invalidate = text_console_invalidate, + .text_update = text_console_update, +}; + +static void text_console_do_init(Chardev *chr, DisplayState *ds) +{ + VCChardev *drv = VC_CHARDEV(chr); + QemuConsole *s = drv->console; + int g_width = 80 * FONT_WIDTH; + int g_height = 24 * FONT_HEIGHT; + + fifo8_create(&s->out_fifo, 16); + s->ds = ds; + + s->y_displayed = 0; + s->y_base = 0; + s->total_height = DEFAULT_BACKSCROLL; + s->x = 0; + s->y = 0; + if (!s->surface) { + if (active_console && active_console->surface) { + g_width = surface_width(active_console->surface); + g_height = surface_height(active_console->surface); + } + s->surface = qemu_create_displaysurface(g_width, g_height); + } + + s->hw_ops = &text_console_ops; + s->hw = s; + + /* Set text attribute defaults */ + s->t_attrib_default.bold = 0; + s->t_attrib_default.uline = 0; + s->t_attrib_default.blink = 0; + s->t_attrib_default.invers = 0; + s->t_attrib_default.unvisible = 0; + s->t_attrib_default.fgcol = QEMU_COLOR_WHITE; + s->t_attrib_default.bgcol = QEMU_COLOR_BLACK; + /* set current text attributes to default */ + s->t_attrib = s->t_attrib_default; + text_console_resize(s); + + if (chr->label) { + char *msg; + + s->t_attrib.bgcol = QEMU_COLOR_BLUE; + msg = g_strdup_printf("%s console\r\n", chr->label); + vc_chr_write(chr, (uint8_t *)msg, strlen(msg)); + g_free(msg); + s->t_attrib = s->t_attrib_default; + } + + qemu_chr_be_event(chr, CHR_EVENT_OPENED); +} + +static void vc_chr_open(Chardev *chr, + ChardevBackend *backend, + bool *be_opened, + Error **errp) +{ + ChardevVC *vc = backend->u.vc.data; + VCChardev *drv = VC_CHARDEV(chr); + QemuConsole *s; + unsigned width = 0; + unsigned height = 0; + + if (vc->has_width) { + width = vc->width; + } else if (vc->has_cols) { + width = vc->cols * FONT_WIDTH; + } + + if (vc->has_height) { + height = vc->height; + } else if (vc->has_rows) { + height = vc->rows * FONT_HEIGHT; + } + + trace_console_txt_new(width, height); + if (width == 0 || height == 0) { + s = new_console(NULL, TEXT_CONSOLE, 0); + } else { + s = new_console(NULL, TEXT_CONSOLE_FIXED_SIZE, 0); + s->surface = qemu_create_displaysurface(width, height); + } + + if (!s) { + error_setg(errp, "cannot create text console"); + return; + } + + s->chr = chr; + drv->console = s; + + if (display_state) { + text_console_do_init(chr, display_state); + } + + /* console/chardev init sometimes completes elsewhere in a 2nd + * stage, so defer OPENED events until they are fully initialized + */ + *be_opened = false; +} + +void qemu_console_resize(QemuConsole *s, int width, int height) +{ + DisplaySurface *surface; + + assert(s->console_type == GRAPHIC_CONSOLE); + + if (s->surface && (s->surface->flags & QEMU_ALLOCATED_FLAG) && + pixman_image_get_width(s->surface->image) == width && + pixman_image_get_height(s->surface->image) == height) { + return; + } + + surface = qemu_create_displaysurface(width, height); + dpy_gfx_replace_surface(s, surface); +} + +DisplaySurface *qemu_console_surface(QemuConsole *console) +{ + return console->surface; +} + +PixelFormat qemu_default_pixelformat(int bpp) +{ + pixman_format_code_t fmt = qemu_default_pixman_format(bpp, true); + PixelFormat pf = qemu_pixelformat_from_pixman(fmt); + return pf; +} + +static QemuDisplay *dpys[DISPLAY_TYPE__MAX]; + +void qemu_display_register(QemuDisplay *ui) +{ + assert(ui->type < DISPLAY_TYPE__MAX); + dpys[ui->type] = ui; +} + +bool qemu_display_find_default(DisplayOptions *opts) +{ + static DisplayType prio[] = { +#if defined(CONFIG_GTK) + DISPLAY_TYPE_GTK, +#endif +#if defined(CONFIG_SDL) + DISPLAY_TYPE_SDL, +#endif +#if defined(CONFIG_COCOA) + DISPLAY_TYPE_COCOA +#endif + }; + int i; + + for (i = 0; i < (int)ARRAY_SIZE(prio); i++) { + if (dpys[prio[i]] == NULL) { + ui_module_load_one(DisplayType_str(prio[i])); + } + if (dpys[prio[i]] == NULL) { + continue; + } + opts->type = prio[i]; + return true; + } + return false; +} + +void qemu_display_early_init(DisplayOptions *opts) +{ + assert(opts->type < DISPLAY_TYPE__MAX); + if (opts->type == DISPLAY_TYPE_NONE) { + return; + } + if (dpys[opts->type] == NULL) { + ui_module_load_one(DisplayType_str(opts->type)); + } + if (dpys[opts->type] == NULL) { + error_report("Display '%s' is not available.", + DisplayType_str(opts->type)); + exit(1); + } + if (dpys[opts->type]->early_init) { + dpys[opts->type]->early_init(opts); + } +} + +void qemu_display_init(DisplayState *ds, DisplayOptions *opts) +{ + assert(opts->type < DISPLAY_TYPE__MAX); + if (opts->type == DISPLAY_TYPE_NONE) { + return; + } + assert(dpys[opts->type] != NULL); + dpys[opts->type]->init(ds, opts); +} + +void qemu_display_help(void) +{ + int idx; + + printf("Available display backend types:\n"); + printf("none\n"); + for (idx = DISPLAY_TYPE_NONE; idx < DISPLAY_TYPE__MAX; idx++) { + if (!dpys[idx]) { + ui_module_load_one(DisplayType_str(idx)); + } + if (dpys[idx]) { + printf("%s\n", DisplayType_str(dpys[idx]->type)); + } + } +} + +void qemu_chr_parse_vc(QemuOpts *opts, ChardevBackend *backend, Error **errp) +{ + int val; + ChardevVC *vc; + + backend->type = CHARDEV_BACKEND_KIND_VC; + vc = backend->u.vc.data = g_new0(ChardevVC, 1); + qemu_chr_parse_common(opts, qapi_ChardevVC_base(vc)); + + val = qemu_opt_get_number(opts, "width", 0); + if (val != 0) { + vc->has_width = true; + vc->width = val; + } + + val = qemu_opt_get_number(opts, "height", 0); + if (val != 0) { + vc->has_height = true; + vc->height = val; + } + + val = qemu_opt_get_number(opts, "cols", 0); + if (val != 0) { + vc->has_cols = true; + vc->cols = val; + } + + val = qemu_opt_get_number(opts, "rows", 0); + if (val != 0) { + vc->has_rows = true; + vc->rows = val; + } +} + +static const TypeInfo qemu_console_info = { + .name = TYPE_QEMU_CONSOLE, + .parent = TYPE_OBJECT, + .instance_size = sizeof(QemuConsole), + .class_size = sizeof(QemuConsoleClass), +}; + +static void char_vc_class_init(ObjectClass *oc, void *data) +{ + ChardevClass *cc = CHARDEV_CLASS(oc); + + cc->parse = qemu_chr_parse_vc; + cc->open = vc_chr_open; + cc->chr_write = vc_chr_write; + cc->chr_accept_input = vc_chr_accept_input; + cc->chr_set_echo = vc_chr_set_echo; +} + +static const TypeInfo char_vc_type_info = { + .name = TYPE_CHARDEV_VC, + .parent = TYPE_CHARDEV, + .instance_size = sizeof(VCChardev), + .class_init = char_vc_class_init, +}; + +void qemu_console_early_init(void) +{ + /* set the default vc driver */ + if (!object_class_by_name(TYPE_CHARDEV_VC)) { + type_register(&char_vc_type_info); + } +} + +static void register_types(void) +{ + type_register_static(&qemu_console_info); +} + +type_init(register_types); diff --git a/ui/curses.c b/ui/curses.c new file mode 100644 index 000000000..861d63244 --- /dev/null +++ b/ui/curses.c @@ -0,0 +1,817 @@ +/* + * QEMU curses/ncurses display driver + * + * Copyright (c) 2005 Andrzej Zaborowski <balrog@zabor.org> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" + +#ifndef _WIN32 +#include <sys/ioctl.h> +#include <termios.h> +#endif +#include <locale.h> +#include <wchar.h> +#include <iconv.h> + +#include "qapi/error.h" +#include "qemu/module.h" +#include "ui/console.h" +#include "ui/input.h" +#include "sysemu/sysemu.h" + +#if defined(__APPLE__) || defined(__OpenBSD__) +#define _XOPEN_SOURCE_EXTENDED 1 +#endif + +/* KEY_EVENT is defined in wincon.h and in curses.h. Avoid redefinition. */ +#undef KEY_EVENT +#include <curses.h> +#undef KEY_EVENT + +#define FONT_HEIGHT 16 +#define FONT_WIDTH 8 + +enum maybe_keycode { + CURSES_KEYCODE, + CURSES_CHAR, + CURSES_CHAR_OR_KEYCODE, +}; + +static DisplayChangeListener *dcl; +static console_ch_t *screen; +static WINDOW *screenpad = NULL; +static int width, height, gwidth, gheight, invalidate; +static int px, py, sminx, sminy, smaxx, smaxy; + +static const char *font_charset = "CP437"; +static cchar_t *vga_to_curses; + +static void curses_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + console_ch_t *line; + cchar_t curses_line[width]; + wchar_t wch[CCHARW_MAX]; + attr_t attrs; + short colors; + int ret; + + line = screen + y * width; + for (h += y; y < h; y ++, line += width) { + for (x = 0; x < width; x++) { + chtype ch = line[x] & A_CHARTEXT; + chtype at = line[x] & A_ATTRIBUTES; + short color_pair = PAIR_NUMBER(line[x]); + + ret = getcchar(&vga_to_curses[ch], wch, &attrs, &colors, NULL); + if (ret == ERR || wch[0] == 0) { + wch[0] = ch; + wch[1] = 0; + } + setcchar(&curses_line[x], wch, at, color_pair, NULL); + } + mvwadd_wchnstr(screenpad, y, 0, curses_line, width); + } + + pnoutrefresh(screenpad, py, px, sminy, sminx, smaxy - 1, smaxx - 1); + refresh(); +} + +static void curses_calc_pad(void) +{ + if (qemu_console_is_fixedsize(NULL)) { + width = gwidth; + height = gheight; + } else { + width = COLS; + height = LINES; + } + + if (screenpad) + delwin(screenpad); + + clear(); + refresh(); + + screenpad = newpad(height, width); + + if (width > COLS) { + px = (width - COLS) / 2; + sminx = 0; + smaxx = COLS; + } else { + px = 0; + sminx = (COLS - width) / 2; + smaxx = sminx + width; + } + + if (height > LINES) { + py = (height - LINES) / 2; + sminy = 0; + smaxy = LINES; + } else { + py = 0; + sminy = (LINES - height) / 2; + smaxy = sminy + height; + } +} + +static void curses_resize(DisplayChangeListener *dcl, + int width, int height) +{ + if (width == gwidth && height == gheight) { + return; + } + + gwidth = width; + gheight = height; + + curses_calc_pad(); +} + +#if !defined(_WIN32) && defined(SIGWINCH) && defined(KEY_RESIZE) +static volatile sig_atomic_t got_sigwinch; +static void curses_winch_check(void) +{ + struct winsize { + unsigned short ws_row; + unsigned short ws_col; + unsigned short ws_xpixel; /* unused */ + unsigned short ws_ypixel; /* unused */ + } ws; + + if (!got_sigwinch) { + return; + } + got_sigwinch = false; + + if (ioctl(1, TIOCGWINSZ, &ws) == -1) { + return; + } + + resize_term(ws.ws_row, ws.ws_col); + invalidate = 1; +} + +static void curses_winch_handler(int signum) +{ + got_sigwinch = true; +} + +static void curses_winch_init(void) +{ + struct sigaction old, winch = { + .sa_handler = curses_winch_handler, + }; + sigaction(SIGWINCH, &winch, &old); +} +#else +static void curses_winch_check(void) {} +static void curses_winch_init(void) {} +#endif + +static void curses_cursor_position(DisplayChangeListener *dcl, + int x, int y) +{ + if (x >= 0) { + x = sminx + x - px; + y = sminy + y - py; + + if (x >= 0 && y >= 0 && x < COLS && y < LINES) { + move(y, x); + curs_set(1); + /* it seems that curs_set(1) must always be called before + * curs_set(2) for the latter to have effect */ + if (!qemu_console_is_graphic(NULL)) { + curs_set(2); + } + return; + } + } + + curs_set(0); +} + +/* generic keyboard conversion */ + +#include "curses_keys.h" + +static kbd_layout_t *kbd_layout = NULL; + +static wint_t console_getch(enum maybe_keycode *maybe_keycode) +{ + wint_t ret; + switch (get_wch(&ret)) { + case KEY_CODE_YES: + *maybe_keycode = CURSES_KEYCODE; + break; + case OK: + *maybe_keycode = CURSES_CHAR; + break; + case ERR: + ret = -1; + break; + default: + abort(); + } + return ret; +} + +static int curses2foo(const int _curses2foo[], const int _curseskey2foo[], + int chr, enum maybe_keycode maybe_keycode) +{ + int ret = -1; + if (maybe_keycode == CURSES_CHAR) { + if (chr < CURSES_CHARS) { + ret = _curses2foo[chr]; + } + } else { + if (chr < CURSES_KEYS) { + ret = _curseskey2foo[chr]; + } + if (ret == -1 && maybe_keycode == CURSES_CHAR_OR_KEYCODE && + chr < CURSES_CHARS) { + ret = _curses2foo[chr]; + } + } + return ret; +} + +#define curses2keycode(chr, maybe_keycode) \ + curses2foo(_curses2keycode, _curseskey2keycode, chr, maybe_keycode) +#define curses2keysym(chr, maybe_keycode) \ + curses2foo(_curses2keysym, _curseskey2keysym, chr, maybe_keycode) +#define curses2qemu(chr, maybe_keycode) \ + curses2foo(_curses2qemu, _curseskey2qemu, chr, maybe_keycode) + +static void curses_refresh(DisplayChangeListener *dcl) +{ + int chr, keysym, keycode, keycode_alt; + enum maybe_keycode maybe_keycode = CURSES_KEYCODE; + + curses_winch_check(); + + if (invalidate) { + clear(); + refresh(); + curses_calc_pad(); + graphic_hw_invalidate(NULL); + invalidate = 0; + } + + graphic_hw_text_update(NULL, screen); + + while (1) { + /* while there are any pending key strokes to process */ + chr = console_getch(&maybe_keycode); + + if (chr == -1) + break; + +#ifdef KEY_RESIZE + /* this shouldn't occur when we use a custom SIGWINCH handler */ + if (maybe_keycode != CURSES_CHAR && chr == KEY_RESIZE) { + clear(); + refresh(); + curses_calc_pad(); + curses_update(dcl, 0, 0, width, height); + continue; + } +#endif + + keycode = curses2keycode(chr, maybe_keycode); + keycode_alt = 0; + + /* alt or esc key */ + if (keycode == 1) { + enum maybe_keycode next_maybe_keycode = CURSES_KEYCODE; + int nextchr = console_getch(&next_maybe_keycode); + + if (nextchr != -1) { + chr = nextchr; + maybe_keycode = next_maybe_keycode; + keycode_alt = ALT; + keycode = curses2keycode(chr, maybe_keycode); + + if (keycode != -1) { + keycode |= ALT; + + /* process keys reserved for qemu */ + if (keycode >= QEMU_KEY_CONSOLE0 && + keycode < QEMU_KEY_CONSOLE0 + 9) { + erase(); + wnoutrefresh(stdscr); + console_select(keycode - QEMU_KEY_CONSOLE0); + + invalidate = 1; + continue; + } + } + } + } + + if (kbd_layout) { + keysym = curses2keysym(chr, maybe_keycode); + + if (keysym == -1) { + if (chr < ' ') { + keysym = chr + '@'; + if (keysym >= 'A' && keysym <= 'Z') + keysym += 'a' - 'A'; + keysym |= KEYSYM_CNTRL; + } else + keysym = chr; + } + + keycode = keysym2scancode(kbd_layout, keysym & KEYSYM_MASK, + NULL, false); + if (keycode == 0) + continue; + + keycode |= (keysym & ~KEYSYM_MASK) >> 16; + keycode |= keycode_alt; + } + + if (keycode == -1) + continue; + + if (qemu_console_is_graphic(NULL)) { + /* since terminals don't know about key press and release + * events, we need to emit both for each key received */ + if (keycode & SHIFT) { + qemu_input_event_send_key_number(NULL, SHIFT_CODE, true); + qemu_input_event_send_key_delay(0); + } + if (keycode & CNTRL) { + qemu_input_event_send_key_number(NULL, CNTRL_CODE, true); + qemu_input_event_send_key_delay(0); + } + if (keycode & ALT) { + qemu_input_event_send_key_number(NULL, ALT_CODE, true); + qemu_input_event_send_key_delay(0); + } + if (keycode & ALTGR) { + qemu_input_event_send_key_number(NULL, GREY | ALT_CODE, true); + qemu_input_event_send_key_delay(0); + } + + qemu_input_event_send_key_number(NULL, keycode & KEY_MASK, true); + qemu_input_event_send_key_delay(0); + qemu_input_event_send_key_number(NULL, keycode & KEY_MASK, false); + qemu_input_event_send_key_delay(0); + + if (keycode & ALTGR) { + qemu_input_event_send_key_number(NULL, GREY | ALT_CODE, false); + qemu_input_event_send_key_delay(0); + } + if (keycode & ALT) { + qemu_input_event_send_key_number(NULL, ALT_CODE, false); + qemu_input_event_send_key_delay(0); + } + if (keycode & CNTRL) { + qemu_input_event_send_key_number(NULL, CNTRL_CODE, false); + qemu_input_event_send_key_delay(0); + } + if (keycode & SHIFT) { + qemu_input_event_send_key_number(NULL, SHIFT_CODE, false); + qemu_input_event_send_key_delay(0); + } + } else { + keysym = curses2qemu(chr, maybe_keycode); + if (keysym == -1) + keysym = chr; + + kbd_put_keysym(keysym); + } + } +} + +static void curses_atexit(void) +{ + endwin(); + g_free(vga_to_curses); + g_free(screen); +} + +/* + * In the following: + * - fch is the font glyph number + * - uch is the unicode value + * - wch is the wchar_t value (may not be unicode, e.g. on BSD/solaris) + * - mbch is the native local-dependent multibyte representation + */ + +/* Setup wchar glyph for one UCS-2 char */ +static void convert_ucs(unsigned char fch, uint16_t uch, iconv_t conv) +{ + char mbch[MB_LEN_MAX]; + wchar_t wch[2]; + char *puch, *pmbch; + size_t such, smbch; + mbstate_t ps; + + puch = (char *) &uch; + pmbch = (char *) mbch; + such = sizeof(uch); + smbch = sizeof(mbch); + + if (iconv(conv, &puch, &such, &pmbch, &smbch) == (size_t) -1) { + fprintf(stderr, "Could not convert 0x%04x " + "from UCS-2 to a multibyte character: %s\n", + uch, strerror(errno)); + return; + } + + memset(&ps, 0, sizeof(ps)); + if (mbrtowc(&wch[0], mbch, sizeof(mbch) - smbch, &ps) == -1) { + fprintf(stderr, "Could not convert 0x%04x " + "from a multibyte character to wchar_t: %s\n", + uch, strerror(errno)); + return; + } + + wch[1] = 0; + setcchar(&vga_to_curses[fch], wch, 0, 0, NULL); +} + +/* Setup wchar glyph for one font character */ +static void convert_font(unsigned char fch, iconv_t conv) +{ + char mbch[MB_LEN_MAX]; + wchar_t wch[2]; + char *pfch, *pmbch; + size_t sfch, smbch; + mbstate_t ps; + + pfch = (char *) &fch; + pmbch = (char *) &mbch; + sfch = sizeof(fch); + smbch = sizeof(mbch); + + if (iconv(conv, &pfch, &sfch, &pmbch, &smbch) == (size_t) -1) { + fprintf(stderr, "Could not convert font glyph 0x%02x " + "from %s to a multibyte character: %s\n", + fch, font_charset, strerror(errno)); + return; + } + + memset(&ps, 0, sizeof(ps)); + if (mbrtowc(&wch[0], mbch, sizeof(mbch) - smbch, &ps) == -1) { + fprintf(stderr, "Could not convert font glyph 0x%02x " + "from a multibyte character to wchar_t: %s\n", + fch, strerror(errno)); + return; + } + + wch[1] = 0; + setcchar(&vga_to_curses[fch], wch, 0, 0, NULL); +} + +/* Convert one wchar to UCS-2 */ +static uint16_t get_ucs(wchar_t wch, iconv_t conv) +{ + char mbch[MB_LEN_MAX]; + uint16_t uch; + char *pmbch, *puch; + size_t smbch, such; + mbstate_t ps; + int ret; + + memset(&ps, 0, sizeof(ps)); + ret = wcrtomb(mbch, wch, &ps); + if (ret == -1) { + fprintf(stderr, "Could not convert 0x%04lx " + "from wchar_t to a multibyte character: %s\n", + (unsigned long)wch, strerror(errno)); + return 0xFFFD; + } + + pmbch = (char *) mbch; + puch = (char *) &uch; + smbch = ret; + such = sizeof(uch); + + if (iconv(conv, &pmbch, &smbch, &puch, &such) == (size_t) -1) { + fprintf(stderr, "Could not convert 0x%04lx " + "from a multibyte character to UCS-2 : %s\n", + (unsigned long)wch, strerror(errno)); + return 0xFFFD; + } + + return uch; +} + +/* + * Setup mapping for vga to curses line graphics. + */ +static void font_setup(void) +{ + iconv_t ucs2_to_nativecharset; + iconv_t nativecharset_to_ucs2; + iconv_t font_conv; + int i; + g_autofree gchar *local_codeset = g_get_codeset(); + + /* + * Control characters are normally non-printable, but VGA does have + * well-known glyphs for them. + */ + static const uint16_t control_characters[0x20] = { + 0x0020, + 0x263a, + 0x263b, + 0x2665, + 0x2666, + 0x2663, + 0x2660, + 0x2022, + 0x25d8, + 0x25cb, + 0x25d9, + 0x2642, + 0x2640, + 0x266a, + 0x266b, + 0x263c, + 0x25ba, + 0x25c4, + 0x2195, + 0x203c, + 0x00b6, + 0x00a7, + 0x25ac, + 0x21a8, + 0x2191, + 0x2193, + 0x2192, + 0x2190, + 0x221f, + 0x2194, + 0x25b2, + 0x25bc + }; + + ucs2_to_nativecharset = iconv_open(local_codeset, "UCS-2"); + if (ucs2_to_nativecharset == (iconv_t) -1) { + fprintf(stderr, "Could not convert font glyphs from UCS-2: '%s'\n", + strerror(errno)); + exit(1); + } + + nativecharset_to_ucs2 = iconv_open("UCS-2", local_codeset); + if (nativecharset_to_ucs2 == (iconv_t) -1) { + iconv_close(ucs2_to_nativecharset); + fprintf(stderr, "Could not convert font glyphs to UCS-2: '%s'\n", + strerror(errno)); + exit(1); + } + + font_conv = iconv_open(local_codeset, font_charset); + if (font_conv == (iconv_t) -1) { + iconv_close(ucs2_to_nativecharset); + iconv_close(nativecharset_to_ucs2); + fprintf(stderr, "Could not convert font glyphs from %s: '%s'\n", + font_charset, strerror(errno)); + exit(1); + } + + /* Control characters */ + for (i = 0; i <= 0x1F; i++) { + convert_ucs(i, control_characters[i], ucs2_to_nativecharset); + } + + for (i = 0x20; i <= 0xFF; i++) { + convert_font(i, font_conv); + } + + /* DEL */ + convert_ucs(0x7F, 0x2302, ucs2_to_nativecharset); + + if (strcmp(local_codeset, "UTF-8")) { + /* Non-Unicode capable, use termcap equivalents for those available */ + for (i = 0; i <= 0xFF; i++) { + wchar_t wch[CCHARW_MAX]; + attr_t attr; + short color; + int ret; + + ret = getcchar(&vga_to_curses[i], wch, &attr, &color, NULL); + if (ret == ERR) + continue; + + switch (get_ucs(wch[0], nativecharset_to_ucs2)) { + case 0x00a3: + vga_to_curses[i] = *WACS_STERLING; + break; + case 0x2591: + vga_to_curses[i] = *WACS_BOARD; + break; + case 0x2592: + vga_to_curses[i] = *WACS_CKBOARD; + break; + case 0x2502: + vga_to_curses[i] = *WACS_VLINE; + break; + case 0x2524: + vga_to_curses[i] = *WACS_RTEE; + break; + case 0x2510: + vga_to_curses[i] = *WACS_URCORNER; + break; + case 0x2514: + vga_to_curses[i] = *WACS_LLCORNER; + break; + case 0x2534: + vga_to_curses[i] = *WACS_BTEE; + break; + case 0x252c: + vga_to_curses[i] = *WACS_TTEE; + break; + case 0x251c: + vga_to_curses[i] = *WACS_LTEE; + break; + case 0x2500: + vga_to_curses[i] = *WACS_HLINE; + break; + case 0x253c: + vga_to_curses[i] = *WACS_PLUS; + break; + case 0x256c: + vga_to_curses[i] = *WACS_LANTERN; + break; + case 0x256a: + vga_to_curses[i] = *WACS_NEQUAL; + break; + case 0x2518: + vga_to_curses[i] = *WACS_LRCORNER; + break; + case 0x250c: + vga_to_curses[i] = *WACS_ULCORNER; + break; + case 0x2588: + vga_to_curses[i] = *WACS_BLOCK; + break; + case 0x03c0: + vga_to_curses[i] = *WACS_PI; + break; + case 0x00b1: + vga_to_curses[i] = *WACS_PLMINUS; + break; + case 0x2265: + vga_to_curses[i] = *WACS_GEQUAL; + break; + case 0x2264: + vga_to_curses[i] = *WACS_LEQUAL; + break; + case 0x00b0: + vga_to_curses[i] = *WACS_DEGREE; + break; + case 0x25a0: + vga_to_curses[i] = *WACS_BULLET; + break; + case 0x2666: + vga_to_curses[i] = *WACS_DIAMOND; + break; + case 0x2192: + vga_to_curses[i] = *WACS_RARROW; + break; + case 0x2190: + vga_to_curses[i] = *WACS_LARROW; + break; + case 0x2191: + vga_to_curses[i] = *WACS_UARROW; + break; + case 0x2193: + vga_to_curses[i] = *WACS_DARROW; + break; + case 0x23ba: + vga_to_curses[i] = *WACS_S1; + break; + case 0x23bb: + vga_to_curses[i] = *WACS_S3; + break; + case 0x23bc: + vga_to_curses[i] = *WACS_S7; + break; + case 0x23bd: + vga_to_curses[i] = *WACS_S9; + break; + } + } + } + iconv_close(ucs2_to_nativecharset); + iconv_close(nativecharset_to_ucs2); + iconv_close(font_conv); +} + +static void curses_setup(void) +{ + int i, colour_default[8] = { + [QEMU_COLOR_BLACK] = COLOR_BLACK, + [QEMU_COLOR_BLUE] = COLOR_BLUE, + [QEMU_COLOR_GREEN] = COLOR_GREEN, + [QEMU_COLOR_CYAN] = COLOR_CYAN, + [QEMU_COLOR_RED] = COLOR_RED, + [QEMU_COLOR_MAGENTA] = COLOR_MAGENTA, + [QEMU_COLOR_YELLOW] = COLOR_YELLOW, + [QEMU_COLOR_WHITE] = COLOR_WHITE, + }; + + /* input as raw as possible, let everything be interpreted + * by the guest system */ + initscr(); noecho(); intrflush(stdscr, FALSE); + nodelay(stdscr, TRUE); nonl(); keypad(stdscr, TRUE); + start_color(); raw(); scrollok(stdscr, FALSE); + set_escdelay(25); + + /* Make color pair to match color format (3bits bg:3bits fg) */ + for (i = 0; i < 64; i++) { + init_pair(i, colour_default[i & 7], colour_default[i >> 3]); + } + /* Set default color for more than 64 for safety. */ + for (i = 64; i < COLOR_PAIRS; i++) { + init_pair(i, COLOR_WHITE, COLOR_BLACK); + } + + font_setup(); +} + +static void curses_keyboard_setup(void) +{ +#if defined(__APPLE__) + /* always use generic keymaps */ + if (!keyboard_layout) + keyboard_layout = "en-us"; +#endif + if(keyboard_layout) { + kbd_layout = init_keyboard_layout(name2keysym, keyboard_layout, + &error_fatal); + } +} + +static const DisplayChangeListenerOps dcl_ops = { + .dpy_name = "curses", + .dpy_text_update = curses_update, + .dpy_text_resize = curses_resize, + .dpy_refresh = curses_refresh, + .dpy_text_cursor = curses_cursor_position, +}; + +static void curses_display_init(DisplayState *ds, DisplayOptions *opts) +{ +#ifndef _WIN32 + if (!isatty(1)) { + fprintf(stderr, "We need a terminal output\n"); + exit(1); + } +#endif + + setlocale(LC_CTYPE, ""); + if (opts->u.curses.charset) { + font_charset = opts->u.curses.charset; + } + screen = g_new0(console_ch_t, 160 * 100); + vga_to_curses = g_new0(cchar_t, 256); + curses_setup(); + curses_keyboard_setup(); + atexit(curses_atexit); + + curses_winch_init(); + + dcl = g_new0(DisplayChangeListener, 1); + dcl->ops = &dcl_ops; + register_displaychangelistener(dcl); + + invalidate = 1; +} + +static QemuDisplay qemu_display_curses = { + .type = DISPLAY_TYPE_CURSES, + .init = curses_display_init, +}; + +static void register_curses(void) +{ + qemu_display_register(&qemu_display_curses); +} + +type_init(register_curses); diff --git a/ui/curses_keys.h b/ui/curses_keys.h new file mode 100644 index 000000000..71e04acdc --- /dev/null +++ b/ui/curses_keys.h @@ -0,0 +1,537 @@ +/* + * Keycode and keysyms conversion tables for curses + * + * Copyright (c) 2005 Andrzej Zaborowski <balrog@zabor.org> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef QEMU_CURSES_KEYS_H +#define QEMU_CURSES_KEYS_H + +#include <curses.h> +#include "keymaps.h" + + +#define KEY_MASK SCANCODE_KEYMASK +#define GREY_CODE 0xe0 +#define GREY SCANCODE_GREY +#define SHIFT_CODE 0x2a +#define SHIFT SCANCODE_SHIFT +#define CNTRL_CODE 0x1d +#define CNTRL SCANCODE_CTRL +#define ALT_CODE 0x38 +#define ALT SCANCODE_ALT +#define ALTGR SCANCODE_ALTGR + +#define KEYSYM_MASK 0x0ffffff +#define KEYSYM_SHIFT (SCANCODE_SHIFT << 16) +#define KEYSYM_CNTRL (SCANCODE_CTRL << 16) +#define KEYSYM_ALT (SCANCODE_ALT << 16) +#define KEYSYM_ALTGR (SCANCODE_ALTGR << 16) + +/* curses won't detect a Control + Alt + 1, so use Alt + 1 */ +#define QEMU_KEY_CONSOLE0 (2 | ALT) /* (curses2keycode['1'] | ALT) */ + +#define CURSES_CHARS 0x100 /* Support latin1 only */ +#define CURSES_KEYS KEY_MAX /* KEY_MAX defined in <curses.h> */ + +static const int _curses2keysym[CURSES_CHARS] = { + [0 ... (CURSES_CHARS - 1)] = -1, + + [0x7f] = KEY_BACKSPACE, + ['\r'] = KEY_ENTER, + ['\n'] = KEY_ENTER, + [27] = 27, +}; + +static const int _curseskey2keysym[CURSES_KEYS] = { + [0 ... (CURSES_KEYS - 1)] = -1, + + [KEY_BTAB] = '\t' | KEYSYM_SHIFT, + [KEY_SPREVIOUS] = KEY_PPAGE | KEYSYM_SHIFT, + [KEY_SNEXT] = KEY_NPAGE | KEYSYM_SHIFT, +}; + +static const int _curses2keycode[CURSES_CHARS] = { + [0 ... (CURSES_CHARS - 1)] = -1, + + [0x01b] = 1, /* Escape */ + ['1'] = 2, + ['2'] = 3, + ['3'] = 4, + ['4'] = 5, + ['5'] = 6, + ['6'] = 7, + ['7'] = 8, + ['8'] = 9, + ['9'] = 10, + ['0'] = 11, + ['-'] = 12, + ['='] = 13, + [0x07f] = 14, /* Backspace */ + + ['\t'] = 15, /* Tab */ + ['q'] = 16, + ['w'] = 17, + ['e'] = 18, + ['r'] = 19, + ['t'] = 20, + ['y'] = 21, + ['u'] = 22, + ['i'] = 23, + ['o'] = 24, + ['p'] = 25, + ['['] = 26, + [']'] = 27, + ['\n'] = 28, /* Return */ + ['\r'] = 28, /* Return */ + + ['a'] = 30, + ['s'] = 31, + ['d'] = 32, + ['f'] = 33, + ['g'] = 34, + ['h'] = 35, + ['j'] = 36, + ['k'] = 37, + ['l'] = 38, + [';'] = 39, + ['\''] = 40, /* Single quote */ + ['`'] = 41, + ['\\'] = 43, /* Backslash */ + + ['z'] = 44, + ['x'] = 45, + ['c'] = 46, + ['v'] = 47, + ['b'] = 48, + ['n'] = 49, + ['m'] = 50, + [','] = 51, + ['.'] = 52, + ['/'] = 53, + + [' '] = 57, + + ['!'] = 2 | SHIFT, + ['@'] = 3 | SHIFT, + ['#'] = 4 | SHIFT, + ['$'] = 5 | SHIFT, + ['%'] = 6 | SHIFT, + ['^'] = 7 | SHIFT, + ['&'] = 8 | SHIFT, + ['*'] = 9 | SHIFT, + ['('] = 10 | SHIFT, + [')'] = 11 | SHIFT, + ['_'] = 12 | SHIFT, + ['+'] = 13 | SHIFT, + + ['Q'] = 16 | SHIFT, + ['W'] = 17 | SHIFT, + ['E'] = 18 | SHIFT, + ['R'] = 19 | SHIFT, + ['T'] = 20 | SHIFT, + ['Y'] = 21 | SHIFT, + ['U'] = 22 | SHIFT, + ['I'] = 23 | SHIFT, + ['O'] = 24 | SHIFT, + ['P'] = 25 | SHIFT, + ['{'] = 26 | SHIFT, + ['}'] = 27 | SHIFT, + + ['A'] = 30 | SHIFT, + ['S'] = 31 | SHIFT, + ['D'] = 32 | SHIFT, + ['F'] = 33 | SHIFT, + ['G'] = 34 | SHIFT, + ['H'] = 35 | SHIFT, + ['J'] = 36 | SHIFT, + ['K'] = 37 | SHIFT, + ['L'] = 38 | SHIFT, + [':'] = 39 | SHIFT, + ['"'] = 40 | SHIFT, + ['~'] = 41 | SHIFT, + ['|'] = 43 | SHIFT, + + ['Z'] = 44 | SHIFT, + ['X'] = 45 | SHIFT, + ['C'] = 46 | SHIFT, + ['V'] = 47 | SHIFT, + ['B'] = 48 | SHIFT, + ['N'] = 49 | SHIFT, + ['M'] = 50 | SHIFT, + ['<'] = 51 | SHIFT, + ['>'] = 52 | SHIFT, + ['?'] = 53 | SHIFT, + + ['Q' - '@'] = 16 | CNTRL, /* Control + q */ + ['W' - '@'] = 17 | CNTRL, /* Control + w */ + ['E' - '@'] = 18 | CNTRL, /* Control + e */ + ['R' - '@'] = 19 | CNTRL, /* Control + r */ + ['T' - '@'] = 20 | CNTRL, /* Control + t */ + ['Y' - '@'] = 21 | CNTRL, /* Control + y */ + ['U' - '@'] = 22 | CNTRL, /* Control + u */ + /* Control + i collides with Tab */ + ['O' - '@'] = 24 | CNTRL, /* Control + o */ + ['P' - '@'] = 25 | CNTRL, /* Control + p */ + + ['A' - '@'] = 30 | CNTRL, /* Control + a */ + ['S' - '@'] = 31 | CNTRL, /* Control + s */ + ['D' - '@'] = 32 | CNTRL, /* Control + d */ + ['F' - '@'] = 33 | CNTRL, /* Control + f */ + ['G' - '@'] = 34 | CNTRL, /* Control + g */ + ['H' - '@'] = 35 | CNTRL, /* Control + h */ + /* Control + j collides with Return */ + ['K' - '@'] = 37 | CNTRL, /* Control + k */ + ['L' - '@'] = 38 | CNTRL, /* Control + l */ + + ['Z' - '@'] = 44 | CNTRL, /* Control + z */ + ['X' - '@'] = 45 | CNTRL, /* Control + x */ + ['C' - '@'] = 46 | CNTRL, /* Control + c */ + ['V' - '@'] = 47 | CNTRL, /* Control + v */ + ['B' - '@'] = 48 | CNTRL, /* Control + b */ + ['N' - '@'] = 49 | CNTRL, /* Control + n */ + /* Control + m collides with the keycode for Enter */ + +}; + +static const int _curseskey2keycode[CURSES_KEYS] = { + [0 ... (CURSES_KEYS - 1)] = -1, + + [KEY_BACKSPACE] = 14, /* Backspace */ + + [KEY_ENTER] = 28, /* Return */ + + [KEY_F(1)] = 59, /* Function Key 1 */ + [KEY_F(2)] = 60, /* Function Key 2 */ + [KEY_F(3)] = 61, /* Function Key 3 */ + [KEY_F(4)] = 62, /* Function Key 4 */ + [KEY_F(5)] = 63, /* Function Key 5 */ + [KEY_F(6)] = 64, /* Function Key 6 */ + [KEY_F(7)] = 65, /* Function Key 7 */ + [KEY_F(8)] = 66, /* Function Key 8 */ + [KEY_F(9)] = 67, /* Function Key 9 */ + [KEY_F(10)] = 68, /* Function Key 10 */ + [KEY_F(11)] = 87, /* Function Key 11 */ + [KEY_F(12)] = 88, /* Function Key 12 */ + + [KEY_HOME] = 71 | GREY, /* Home */ + [KEY_UP] = 72 | GREY, /* Up Arrow */ + [KEY_PPAGE] = 73 | GREY, /* Page Up */ + [KEY_LEFT] = 75 | GREY, /* Left Arrow */ + [KEY_RIGHT] = 77 | GREY, /* Right Arrow */ + [KEY_END] = 79 | GREY, /* End */ + [KEY_DOWN] = 80 | GREY, /* Down Arrow */ + [KEY_NPAGE] = 81 | GREY, /* Page Down */ + [KEY_IC] = 82 | GREY, /* Insert */ + [KEY_DC] = 83 | GREY, /* Delete */ + + [KEY_SPREVIOUS] = 73 | GREY | SHIFT, /* Shift + Page Up */ + [KEY_SNEXT] = 81 | GREY | SHIFT, /* Shift + Page Down */ + + [KEY_BTAB] = 15 | SHIFT, /* Shift + Tab */ + + [KEY_F(13)] = 59 | SHIFT, /* Shift + Function Key 1 */ + [KEY_F(14)] = 60 | SHIFT, /* Shift + Function Key 2 */ + [KEY_F(15)] = 61 | SHIFT, /* Shift + Function Key 3 */ + [KEY_F(16)] = 62 | SHIFT, /* Shift + Function Key 4 */ + [KEY_F(17)] = 63 | SHIFT, /* Shift + Function Key 5 */ + [KEY_F(18)] = 64 | SHIFT, /* Shift + Function Key 6 */ + [KEY_F(19)] = 65 | SHIFT, /* Shift + Function Key 7 */ + [KEY_F(20)] = 66 | SHIFT, /* Shift + Function Key 8 */ + [KEY_F(21)] = 67 | SHIFT, /* Shift + Function Key 9 */ + [KEY_F(22)] = 68 | SHIFT, /* Shift + Function Key 10 */ + [KEY_F(23)] = 69 | SHIFT, /* Shift + Function Key 11 */ + [KEY_F(24)] = 70 | SHIFT, /* Shift + Function Key 12 */ +}; + +static const int _curses2qemu[CURSES_CHARS] = { + [0 ... (CURSES_CHARS - 1)] = -1, + + ['\n'] = '\n', + ['\r'] = '\n', + + [0x07f] = QEMU_KEY_BACKSPACE, +}; + +static const int _curseskey2qemu[CURSES_KEYS] = { + [0 ... (CURSES_KEYS - 1)] = -1, + + [KEY_DOWN] = QEMU_KEY_DOWN, + [KEY_UP] = QEMU_KEY_UP, + [KEY_LEFT] = QEMU_KEY_LEFT, + [KEY_RIGHT] = QEMU_KEY_RIGHT, + [KEY_HOME] = QEMU_KEY_HOME, + [KEY_BACKSPACE] = QEMU_KEY_BACKSPACE, + + [KEY_DC] = QEMU_KEY_DELETE, + [KEY_NPAGE] = QEMU_KEY_PAGEDOWN, + [KEY_PPAGE] = QEMU_KEY_PAGEUP, + [KEY_ENTER] = '\n', + [KEY_END] = QEMU_KEY_END, + +}; + +static const name2keysym_t name2keysym[] = { + /* Plain ASCII */ + { "space", 0x020 }, + { "exclam", 0x021 }, + { "quotedbl", 0x022 }, + { "numbersign", 0x023 }, + { "dollar", 0x024 }, + { "percent", 0x025 }, + { "ampersand", 0x026 }, + { "apostrophe", 0x027 }, + { "parenleft", 0x028 }, + { "parenright", 0x029 }, + { "asterisk", 0x02a }, + { "plus", 0x02b }, + { "comma", 0x02c }, + { "minus", 0x02d }, + { "period", 0x02e }, + { "slash", 0x02f }, + { "0", 0x030 }, + { "1", 0x031 }, + { "2", 0x032 }, + { "3", 0x033 }, + { "4", 0x034 }, + { "5", 0x035 }, + { "6", 0x036 }, + { "7", 0x037 }, + { "8", 0x038 }, + { "9", 0x039 }, + { "colon", 0x03a }, + { "semicolon", 0x03b }, + { "less", 0x03c }, + { "equal", 0x03d }, + { "greater", 0x03e }, + { "question", 0x03f }, + { "at", 0x040 }, + { "A", 0x041 }, + { "B", 0x042 }, + { "C", 0x043 }, + { "D", 0x044 }, + { "E", 0x045 }, + { "F", 0x046 }, + { "G", 0x047 }, + { "H", 0x048 }, + { "I", 0x049 }, + { "J", 0x04a }, + { "K", 0x04b }, + { "L", 0x04c }, + { "M", 0x04d }, + { "N", 0x04e }, + { "O", 0x04f }, + { "P", 0x050 }, + { "Q", 0x051 }, + { "R", 0x052 }, + { "S", 0x053 }, + { "T", 0x054 }, + { "U", 0x055 }, + { "V", 0x056 }, + { "W", 0x057 }, + { "X", 0x058 }, + { "Y", 0x059 }, + { "Z", 0x05a }, + { "bracketleft", 0x05b }, + { "backslash", 0x05c }, + { "bracketright", 0x05d }, + { "asciicircum", 0x05e }, + { "underscore", 0x05f }, + { "grave", 0x060 }, + { "a", 0x061 }, + { "b", 0x062 }, + { "c", 0x063 }, + { "d", 0x064 }, + { "e", 0x065 }, + { "f", 0x066 }, + { "g", 0x067 }, + { "h", 0x068 }, + { "i", 0x069 }, + { "j", 0x06a }, + { "k", 0x06b }, + { "l", 0x06c }, + { "m", 0x06d }, + { "n", 0x06e }, + { "o", 0x06f }, + { "p", 0x070 }, + { "q", 0x071 }, + { "r", 0x072 }, + { "s", 0x073 }, + { "t", 0x074 }, + { "u", 0x075 }, + { "v", 0x076 }, + { "w", 0x077 }, + { "x", 0x078 }, + { "y", 0x079 }, + { "z", 0x07a }, + { "braceleft", 0x07b }, + { "bar", 0x07c }, + { "braceright", 0x07d }, + { "asciitilde", 0x07e }, + + /* Latin-1 extensions */ + { "nobreakspace", 0x0a0 }, + { "exclamdown", 0x0a1 }, + { "cent", 0x0a2 }, + { "sterling", 0x0a3 }, + { "currency", 0x0a4 }, + { "yen", 0x0a5 }, + { "brokenbar", 0x0a6 }, + { "section", 0x0a7 }, + { "diaeresis", 0x0a8 }, + { "copyright", 0x0a9 }, + { "ordfeminine", 0x0aa }, + { "guillemotleft", 0x0ab }, + { "notsign", 0x0ac }, + { "hyphen", 0x0ad }, + { "registered", 0x0ae }, + { "macron", 0x0af }, + { "degree", 0x0b0 }, + { "plusminus", 0x0b1 }, + { "twosuperior", 0x0b2 }, + { "threesuperior", 0x0b3 }, + { "acute", 0x0b4 }, + { "mu", 0x0b5 }, + { "paragraph", 0x0b6 }, + { "periodcentered", 0x0b7 }, + { "cedilla", 0x0b8 }, + { "onesuperior", 0x0b9 }, + { "masculine", 0x0ba }, + { "guillemotright", 0x0bb }, + { "onequarter", 0x0bc }, + { "onehalf", 0x0bd }, + { "threequarters", 0x0be }, + { "questiondown", 0x0bf }, + { "Agrave", 0x0c0 }, + { "Aacute", 0x0c1 }, + { "Acircumflex", 0x0c2 }, + { "Atilde", 0x0c3 }, + { "Adiaeresis", 0x0c4 }, + { "Aring", 0x0c5 }, + { "AE", 0x0c6 }, + { "Ccedilla", 0x0c7 }, + { "Egrave", 0x0c8 }, + { "Eacute", 0x0c9 }, + { "Ecircumflex", 0x0ca }, + { "Ediaeresis", 0x0cb }, + { "Igrave", 0x0cc }, + { "Iacute", 0x0cd }, + { "Icircumflex", 0x0ce }, + { "Idiaeresis", 0x0cf }, + { "ETH", 0x0d0 }, + { "Eth", 0x0d0 }, + { "Ntilde", 0x0d1 }, + { "Ograve", 0x0d2 }, + { "Oacute", 0x0d3 }, + { "Ocircumflex", 0x0d4 }, + { "Otilde", 0x0d5 }, + { "Odiaeresis", 0x0d6 }, + { "multiply", 0x0d7 }, + { "Ooblique", 0x0d8 }, + { "Oslash", 0x0d8 }, + { "Ugrave", 0x0d9 }, + { "Uacute", 0x0da }, + { "Ucircumflex", 0x0db }, + { "Udiaeresis", 0x0dc }, + { "Yacute", 0x0dd }, + { "THORN", 0x0de }, + { "Thorn", 0x0de }, + { "ssharp", 0x0df }, + { "agrave", 0x0e0 }, + { "aacute", 0x0e1 }, + { "acircumflex", 0x0e2 }, + { "atilde", 0x0e3 }, + { "adiaeresis", 0x0e4 }, + { "aring", 0x0e5 }, + { "ae", 0x0e6 }, + { "ccedilla", 0x0e7 }, + { "egrave", 0x0e8 }, + { "eacute", 0x0e9 }, + { "ecircumflex", 0x0ea }, + { "ediaeresis", 0x0eb }, + { "igrave", 0x0ec }, + { "iacute", 0x0ed }, + { "icircumflex", 0x0ee }, + { "idiaeresis", 0x0ef }, + { "eth", 0x0f0 }, + { "ntilde", 0x0f1 }, + { "ograve", 0x0f2 }, + { "oacute", 0x0f3 }, + { "ocircumflex", 0x0f4 }, + { "otilde", 0x0f5 }, + { "odiaeresis", 0x0f6 }, + { "division", 0x0f7 }, + { "oslash", 0x0f8 }, + { "ooblique", 0x0f8 }, + { "ugrave", 0x0f9 }, + { "uacute", 0x0fa }, + { "ucircumflex", 0x0fb }, + { "udiaeresis", 0x0fc }, + { "yacute", 0x0fd }, + { "thorn", 0x0fe }, + { "ydiaeresis", 0x0ff }, + + /* Special keys */ + { "BackSpace", KEY_BACKSPACE }, + { "Tab", '\t' }, + { "Return", KEY_ENTER }, + { "Right", KEY_RIGHT }, + { "Left", KEY_LEFT }, + { "Up", KEY_UP }, + { "Down", KEY_DOWN }, + { "Next", KEY_NPAGE }, + { "Page_Down", KEY_NPAGE }, + { "Prior", KEY_PPAGE }, + { "Page_Up", KEY_PPAGE }, + { "Insert", KEY_IC }, + { "Delete", KEY_DC }, + { "Home", KEY_HOME }, + { "End", KEY_END }, + { "F1", KEY_F(1) }, + { "F2", KEY_F(2) }, + { "F3", KEY_F(3) }, + { "F4", KEY_F(4) }, + { "F5", KEY_F(5) }, + { "F6", KEY_F(6) }, + { "F7", KEY_F(7) }, + { "F8", KEY_F(8) }, + { "F9", KEY_F(9) }, + { "F10", KEY_F(10) }, + { "F11", KEY_F(11) }, + { "F12", KEY_F(12) }, + { "F13", KEY_F(13) }, + { "F14", KEY_F(14) }, + { "F15", KEY_F(15) }, + { "F16", KEY_F(16) }, + { "F17", KEY_F(17) }, + { "F18", KEY_F(18) }, + { "F19", KEY_F(19) }, + { "F20", KEY_F(20) }, + { "F21", KEY_F(21) }, + { "F22", KEY_F(22) }, + { "F23", KEY_F(23) }, + { "F24", KEY_F(24) }, + { "Escape", 27 }, + + { NULL, 0 }, +}; + +#endif diff --git a/ui/cursor.c b/ui/cursor.c new file mode 100644 index 000000000..1d62ddd4d --- /dev/null +++ b/ui/cursor.c @@ -0,0 +1,243 @@ +#include "qemu/osdep.h" +#include "ui/console.h" + +#include "cursor_hidden.xpm" +#include "cursor_left_ptr.xpm" + +/* for creating built-in cursors */ +static QEMUCursor *cursor_parse_xpm(const char *xpm[]) +{ + QEMUCursor *c; + uint32_t ctab[128]; + unsigned int width, height, colors, chars; + unsigned int line = 0, i, r, g, b, x, y, pixel; + char name[16]; + uint8_t idx; + + /* parse header line: width, height, #colors, #chars */ + if (sscanf(xpm[line], "%u %u %u %u", + &width, &height, &colors, &chars) != 4) { + fprintf(stderr, "%s: header parse error: \"%s\"\n", + __func__, xpm[line]); + return NULL; + } + if (chars != 1) { + fprintf(stderr, "%s: chars != 1 not supported\n", __func__); + return NULL; + } + line++; + + /* parse color table */ + for (i = 0; i < colors; i++, line++) { + if (sscanf(xpm[line], "%c c %15s", &idx, name) == 2) { + if (sscanf(name, "#%02x%02x%02x", &r, &g, &b) == 3) { + ctab[idx] = (0xff << 24) | (b << 16) | (g << 8) | r; + continue; + } + if (strcmp(name, "None") == 0) { + ctab[idx] = 0x00000000; + continue; + } + } + fprintf(stderr, "%s: color parse error: \"%s\"\n", + __func__, xpm[line]); + return NULL; + } + + /* parse pixel data */ + c = cursor_alloc(width, height); + for (pixel = 0, y = 0; y < height; y++, line++) { + for (x = 0; x < height; x++, pixel++) { + idx = xpm[line][x]; + c->data[pixel] = ctab[idx]; + } + } + return c; +} + +/* nice for debugging */ +void cursor_print_ascii_art(QEMUCursor *c, const char *prefix) +{ + uint32_t *data = c->data; + int x,y; + + for (y = 0; y < c->height; y++) { + fprintf(stderr, "%s: %2d: |", prefix, y); + for (x = 0; x < c->width; x++, data++) { + if ((*data & 0xff000000) != 0xff000000) { + fprintf(stderr, " "); /* transparent */ + } else if ((*data & 0x00ffffff) == 0x00ffffff) { + fprintf(stderr, "."); /* white */ + } else if ((*data & 0x00ffffff) == 0x00000000) { + fprintf(stderr, "X"); /* black */ + } else { + fprintf(stderr, "o"); /* other */ + } + } + fprintf(stderr, "|\n"); + } +} + +QEMUCursor *cursor_builtin_hidden(void) +{ + return cursor_parse_xpm(cursor_hidden_xpm); +} + +QEMUCursor *cursor_builtin_left_ptr(void) +{ + return cursor_parse_xpm(cursor_left_ptr_xpm); +} + +QEMUCursor *cursor_alloc(int width, int height) +{ + QEMUCursor *c; + int datasize = width * height * sizeof(uint32_t); + + c = g_malloc0(sizeof(QEMUCursor) + datasize); + c->width = width; + c->height = height; + c->refcount = 1; + return c; +} + +void cursor_get(QEMUCursor *c) +{ + c->refcount++; +} + +void cursor_put(QEMUCursor *c) +{ + if (c == NULL) + return; + c->refcount--; + if (c->refcount) + return; + g_free(c); +} + +int cursor_get_mono_bpl(QEMUCursor *c) +{ + return DIV_ROUND_UP(c->width, 8); +} + +void cursor_set_mono(QEMUCursor *c, + uint32_t foreground, uint32_t background, uint8_t *image, + int transparent, uint8_t *mask) +{ + uint32_t *data = c->data; + uint8_t bit; + int x,y,bpl; + bool expand_bitmap_only = image == mask; + bool has_inverted_colors = false; + const uint32_t inverted = 0x80000000; + + /* + * Converts a monochrome bitmap with XOR mask 'image' and AND mask 'mask': + * https://docs.microsoft.com/en-us/windows-hardware/drivers/display/drawing-monochrome-pointers + */ + bpl = cursor_get_mono_bpl(c); + for (y = 0; y < c->height; y++) { + bit = 0x80; + for (x = 0; x < c->width; x++, data++) { + if (transparent && mask[x/8] & bit) { + if (!expand_bitmap_only && image[x / 8] & bit) { + *data = inverted; + has_inverted_colors = true; + } else { + *data = 0x00000000; + } + } else if (!transparent && !(mask[x/8] & bit)) { + *data = 0x00000000; + } else if (image[x/8] & bit) { + *data = 0xff000000 | foreground; + } else { + *data = 0xff000000 | background; + } + bit >>= 1; + if (bit == 0) { + bit = 0x80; + } + } + mask += bpl; + image += bpl; + } + + /* + * If there are any pixels with inverted colors, create an outline (fill + * transparent neighbors with the background color) and use the foreground + * color as "inverted" color. + */ + if (has_inverted_colors) { + data = c->data; + for (y = 0; y < c->height; y++) { + for (x = 0; x < c->width; x++, data++) { + if (*data == 0 /* transparent */ && + ((x > 0 && data[-1] == inverted) || + (x + 1 < c->width && data[1] == inverted) || + (y > 0 && data[-c->width] == inverted) || + (y + 1 < c->height && data[c->width] == inverted))) { + *data = 0xff000000 | background; + } + } + } + data = c->data; + for (x = 0; x < c->width * c->height; x++, data++) { + if (*data == inverted) { + *data = 0xff000000 | foreground; + } + } + } +} + +void cursor_get_mono_image(QEMUCursor *c, int foreground, uint8_t *image) +{ + uint32_t *data = c->data; + uint8_t bit; + int x,y,bpl; + + bpl = cursor_get_mono_bpl(c); + memset(image, 0, bpl * c->height); + for (y = 0; y < c->height; y++) { + bit = 0x80; + for (x = 0; x < c->width; x++, data++) { + if (((*data & 0xff000000) == 0xff000000) && + ((*data & 0x00ffffff) == foreground)) { + image[x/8] |= bit; + } + bit >>= 1; + if (bit == 0) { + bit = 0x80; + } + } + image += bpl; + } +} + +void cursor_get_mono_mask(QEMUCursor *c, int transparent, uint8_t *mask) +{ + uint32_t *data = c->data; + uint8_t bit; + int x,y,bpl; + + bpl = cursor_get_mono_bpl(c); + memset(mask, 0, bpl * c->height); + for (y = 0; y < c->height; y++) { + bit = 0x80; + for (x = 0; x < c->width; x++, data++) { + if ((*data & 0xff000000) != 0xff000000) { + if (transparent != 0) { + mask[x/8] |= bit; + } + } else { + if (transparent == 0) { + mask[x/8] |= bit; + } + } + bit >>= 1; + if (bit == 0) { + bit = 0x80; + } + } + mask += bpl; + } +} diff --git a/ui/cursor_hidden.xpm b/ui/cursor_hidden.xpm new file mode 100644 index 000000000..354e7a939 --- /dev/null +++ b/ui/cursor_hidden.xpm @@ -0,0 +1,37 @@ +/* XPM */ +static const char *cursor_hidden_xpm[] = { + "32 32 1 1", + " c None", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", +}; diff --git a/ui/cursor_left_ptr.xpm b/ui/cursor_left_ptr.xpm new file mode 100644 index 000000000..6c9ada902 --- /dev/null +++ b/ui/cursor_left_ptr.xpm @@ -0,0 +1,39 @@ +/* XPM */ +static const char *cursor_left_ptr_xpm[] = { + "32 32 3 1", + "X c #000000", + ". c #ffffff", + " c None", + "X ", + "XX ", + "X.X ", + "X..X ", + "X...X ", + "X....X ", + "X.....X ", + "X......X ", + "X.......X ", + "X........X ", + "X.....XXXXX ", + "X..X..X ", + "X.X X..X ", + "XX X..X ", + "X X..X ", + " X..X ", + " X..X ", + " X..X ", + " XX ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", +}; diff --git a/ui/egl-context.c b/ui/egl-context.c new file mode 100644 index 000000000..368ffa49d --- /dev/null +++ b/ui/egl-context.c @@ -0,0 +1,37 @@ +#include "qemu/osdep.h" +#include "ui/egl-context.h" + +QEMUGLContext qemu_egl_create_context(DisplayChangeListener *dcl, + QEMUGLParams *params) +{ + EGLContext ctx; + EGLint ctx_att_core[] = { + EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, + EGL_CONTEXT_CLIENT_VERSION, params->major_ver, + EGL_CONTEXT_MINOR_VERSION_KHR, params->minor_ver, + EGL_NONE + }; + EGLint ctx_att_gles[] = { + EGL_CONTEXT_CLIENT_VERSION, params->major_ver, + EGL_CONTEXT_MINOR_VERSION_KHR, params->minor_ver, + EGL_NONE + }; + bool gles = (qemu_egl_mode == DISPLAYGL_MODE_ES); + + ctx = eglCreateContext(qemu_egl_display, qemu_egl_config, + eglGetCurrentContext(), + gles ? ctx_att_gles : ctx_att_core); + return ctx; +} + +void qemu_egl_destroy_context(DisplayChangeListener *dcl, QEMUGLContext ctx) +{ + eglDestroyContext(qemu_egl_display, ctx); +} + +int qemu_egl_make_context_current(DisplayChangeListener *dcl, + QEMUGLContext ctx) +{ + return eglMakeCurrent(qemu_egl_display, + EGL_NO_SURFACE, EGL_NO_SURFACE, ctx); +} diff --git a/ui/egl-headless.c b/ui/egl-headless.c new file mode 100644 index 000000000..a26a2520c --- /dev/null +++ b/ui/egl-headless.c @@ -0,0 +1,217 @@ +#include "qemu/osdep.h" +#include "qemu/module.h" +#include "sysemu/sysemu.h" +#include "ui/console.h" +#include "ui/egl-helpers.h" +#include "ui/egl-context.h" +#include "ui/shader.h" + +typedef struct egl_dpy { + DisplayChangeListener dcl; + DisplaySurface *ds; + QemuGLShader *gls; + egl_fb guest_fb; + egl_fb cursor_fb; + egl_fb blit_fb; + bool y_0_top; + uint32_t pos_x; + uint32_t pos_y; +} egl_dpy; + +/* ------------------------------------------------------------------ */ + +static void egl_refresh(DisplayChangeListener *dcl) +{ + graphic_hw_update(dcl->con); +} + +static void egl_gfx_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ +} + +static void egl_gfx_switch(DisplayChangeListener *dcl, + struct DisplaySurface *new_surface) +{ + egl_dpy *edpy = container_of(dcl, egl_dpy, dcl); + + edpy->ds = new_surface; +} + +static QEMUGLContext egl_create_context(DisplayChangeListener *dcl, + QEMUGLParams *params) +{ + eglMakeCurrent(qemu_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + qemu_egl_rn_ctx); + return qemu_egl_create_context(dcl, params); +} + +static void egl_scanout_disable(DisplayChangeListener *dcl) +{ + egl_dpy *edpy = container_of(dcl, egl_dpy, dcl); + + egl_fb_destroy(&edpy->guest_fb); + egl_fb_destroy(&edpy->blit_fb); +} + +static void egl_scanout_texture(DisplayChangeListener *dcl, + uint32_t backing_id, + bool backing_y_0_top, + uint32_t backing_width, + uint32_t backing_height, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + egl_dpy *edpy = container_of(dcl, egl_dpy, dcl); + + edpy->y_0_top = backing_y_0_top; + + /* source framebuffer */ + egl_fb_setup_for_tex(&edpy->guest_fb, + backing_width, backing_height, backing_id, false); + + /* dest framebuffer */ + if (edpy->blit_fb.width != backing_width || + edpy->blit_fb.height != backing_height) { + egl_fb_destroy(&edpy->blit_fb); + egl_fb_setup_new_tex(&edpy->blit_fb, backing_width, backing_height); + } +} + +static void egl_scanout_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf) +{ + egl_dmabuf_import_texture(dmabuf); + if (!dmabuf->texture) { + return; + } + + egl_scanout_texture(dcl, dmabuf->texture, + false, dmabuf->width, dmabuf->height, + 0, 0, dmabuf->width, dmabuf->height); +} + +static void egl_cursor_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf, bool have_hot, + uint32_t hot_x, uint32_t hot_y) +{ + egl_dpy *edpy = container_of(dcl, egl_dpy, dcl); + + if (dmabuf) { + egl_dmabuf_import_texture(dmabuf); + if (!dmabuf->texture) { + return; + } + egl_fb_setup_for_tex(&edpy->cursor_fb, dmabuf->width, dmabuf->height, + dmabuf->texture, false); + } else { + egl_fb_destroy(&edpy->cursor_fb); + } +} + +static void egl_cursor_position(DisplayChangeListener *dcl, + uint32_t pos_x, uint32_t pos_y) +{ + egl_dpy *edpy = container_of(dcl, egl_dpy, dcl); + + edpy->pos_x = pos_x; + edpy->pos_y = pos_y; +} + +static void egl_release_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf) +{ + egl_dmabuf_release_texture(dmabuf); +} + +static void egl_scanout_flush(DisplayChangeListener *dcl, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + egl_dpy *edpy = container_of(dcl, egl_dpy, dcl); + + if (!edpy->guest_fb.texture || !edpy->ds) { + return; + } + assert(surface_format(edpy->ds) == PIXMAN_x8r8g8b8); + + if (edpy->cursor_fb.texture) { + /* have cursor -> render using textures */ + egl_texture_blit(edpy->gls, &edpy->blit_fb, &edpy->guest_fb, + !edpy->y_0_top); + egl_texture_blend(edpy->gls, &edpy->blit_fb, &edpy->cursor_fb, + !edpy->y_0_top, edpy->pos_x, edpy->pos_y, + 1.0, 1.0); + } else { + /* no cursor -> use simple framebuffer blit */ + egl_fb_blit(&edpy->blit_fb, &edpy->guest_fb, edpy->y_0_top); + } + + egl_fb_read(edpy->ds, &edpy->blit_fb); + dpy_gfx_update(edpy->dcl.con, x, y, w, h); +} + +static const DisplayChangeListenerOps egl_ops = { + .dpy_name = "egl-headless", + .dpy_refresh = egl_refresh, + .dpy_gfx_update = egl_gfx_update, + .dpy_gfx_switch = egl_gfx_switch, + + .dpy_gl_ctx_create = egl_create_context, + .dpy_gl_ctx_destroy = qemu_egl_destroy_context, + .dpy_gl_ctx_make_current = qemu_egl_make_context_current, + + .dpy_gl_scanout_disable = egl_scanout_disable, + .dpy_gl_scanout_texture = egl_scanout_texture, + .dpy_gl_scanout_dmabuf = egl_scanout_dmabuf, + .dpy_gl_cursor_dmabuf = egl_cursor_dmabuf, + .dpy_gl_cursor_position = egl_cursor_position, + .dpy_gl_release_dmabuf = egl_release_dmabuf, + .dpy_gl_update = egl_scanout_flush, +}; + +static void early_egl_headless_init(DisplayOptions *opts) +{ + display_opengl = 1; +} + +static void egl_headless_init(DisplayState *ds, DisplayOptions *opts) +{ + DisplayGLMode mode = opts->has_gl ? opts->gl : DISPLAYGL_MODE_ON; + QemuConsole *con; + egl_dpy *edpy; + int idx; + + if (egl_rendernode_init(opts->u.egl_headless.rendernode, mode) < 0) { + error_report("egl: render node init failed"); + exit(1); + } + + for (idx = 0;; idx++) { + con = qemu_console_lookup_by_index(idx); + if (!con || !qemu_console_is_graphic(con)) { + break; + } + + edpy = g_new0(egl_dpy, 1); + edpy->dcl.con = con; + edpy->dcl.ops = &egl_ops; + edpy->gls = qemu_gl_init_shader(); + register_displaychangelistener(&edpy->dcl); + } +} + +static QemuDisplay qemu_display_egl = { + .type = DISPLAY_TYPE_EGL_HEADLESS, + .early_init = early_egl_headless_init, + .init = egl_headless_init, +}; + +static void register_egl(void) +{ + qemu_display_register(&qemu_display_egl); +} + +type_init(register_egl); + +module_dep("ui-opengl"); diff --git a/ui/egl-helpers.c b/ui/egl-helpers.c new file mode 100644 index 000000000..3a88245b6 --- /dev/null +++ b/ui/egl-helpers.c @@ -0,0 +1,529 @@ +/* + * Copyright (C) 2015-2016 Gerd Hoffmann <kraxel@redhat.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <http://www.gnu.org/licenses/>. + */ +#include "qemu/osdep.h" +#include "qemu/drm.h" +#include "qemu/error-report.h" +#include "ui/console.h" +#include "ui/egl-helpers.h" + +EGLDisplay *qemu_egl_display; +EGLConfig qemu_egl_config; +DisplayGLMode qemu_egl_mode; + +/* ------------------------------------------------------------------ */ + +static void egl_fb_delete_texture(egl_fb *fb) +{ + if (!fb->delete_texture) { + return; + } + + glDeleteTextures(1, &fb->texture); + fb->delete_texture = false; +} + +void egl_fb_destroy(egl_fb *fb) +{ + if (!fb->framebuffer) { + return; + } + + egl_fb_delete_texture(fb); + glDeleteFramebuffers(1, &fb->framebuffer); + + fb->width = 0; + fb->height = 0; + fb->texture = 0; + fb->framebuffer = 0; +} + +void egl_fb_setup_default(egl_fb *fb, int width, int height) +{ + fb->width = width; + fb->height = height; + fb->framebuffer = 0; /* default framebuffer */ +} + +void egl_fb_setup_for_tex(egl_fb *fb, int width, int height, + GLuint texture, bool delete) +{ + egl_fb_delete_texture(fb); + + fb->width = width; + fb->height = height; + fb->texture = texture; + fb->delete_texture = delete; + if (!fb->framebuffer) { + glGenFramebuffers(1, &fb->framebuffer); + } + + glBindFramebuffer(GL_FRAMEBUFFER_EXT, fb->framebuffer); + glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, + GL_TEXTURE_2D, fb->texture, 0); +} + +void egl_fb_setup_new_tex(egl_fb *fb, int width, int height) +{ + GLuint texture; + + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, + 0, GL_BGRA, GL_UNSIGNED_BYTE, 0); + + egl_fb_setup_for_tex(fb, width, height, texture, true); +} + +void egl_fb_blit(egl_fb *dst, egl_fb *src, bool flip) +{ + GLuint x1 = 0; + GLuint y1 = 0; + GLuint x2, y2; + GLuint w = src->width; + GLuint h = src->height; + + glBindFramebuffer(GL_READ_FRAMEBUFFER, src->framebuffer); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dst->framebuffer); + glViewport(0, 0, dst->width, dst->height); + + if (src->dmabuf) { + x1 = src->dmabuf->x; + y1 = src->dmabuf->y; + w = src->dmabuf->scanout_width; + h = src->dmabuf->scanout_height; + } + + w = (x1 + w) > src->width ? src->width - x1 : w; + h = (y1 + h) > src->height ? src->height - y1 : h; + + y2 = flip ? y1 : h + y1; + y1 = flip ? h + y1 : y1; + x2 = x1 + w; + + glBlitFramebuffer(x1, y1, x2, y2, + 0, 0, dst->width, dst->height, + GL_COLOR_BUFFER_BIT, GL_LINEAR); +} + +void egl_fb_read(DisplaySurface *dst, egl_fb *src) +{ + glBindFramebuffer(GL_READ_FRAMEBUFFER, src->framebuffer); + glReadBuffer(GL_COLOR_ATTACHMENT0_EXT); + glReadPixels(0, 0, surface_width(dst), surface_height(dst), + GL_BGRA, GL_UNSIGNED_BYTE, surface_data(dst)); +} + +void egl_texture_blit(QemuGLShader *gls, egl_fb *dst, egl_fb *src, bool flip) +{ + glBindFramebuffer(GL_FRAMEBUFFER_EXT, dst->framebuffer); + glViewport(0, 0, dst->width, dst->height); + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, src->texture); + qemu_gl_run_texture_blit(gls, flip); +} + +void egl_texture_blend(QemuGLShader *gls, egl_fb *dst, egl_fb *src, bool flip, + int x, int y, double scale_x, double scale_y) +{ + glBindFramebuffer(GL_FRAMEBUFFER_EXT, dst->framebuffer); + int w = scale_x * src->width; + int h = scale_y * src->height; + if (flip) { + glViewport(x, y, w, h); + } else { + glViewport(x, dst->height - h - y, w, h); + } + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, src->texture); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + qemu_gl_run_texture_blit(gls, flip); + glDisable(GL_BLEND); +} + +/* ---------------------------------------------------------------------- */ + +#ifdef CONFIG_GBM + +int qemu_egl_rn_fd; +struct gbm_device *qemu_egl_rn_gbm_dev; +EGLContext qemu_egl_rn_ctx; + +int egl_rendernode_init(const char *rendernode, DisplayGLMode mode) +{ + qemu_egl_rn_fd = -1; + int rc; + + qemu_egl_rn_fd = qemu_drm_rendernode_open(rendernode); + if (qemu_egl_rn_fd == -1) { + error_report("egl: no drm render node available"); + goto err; + } + + qemu_egl_rn_gbm_dev = gbm_create_device(qemu_egl_rn_fd); + if (!qemu_egl_rn_gbm_dev) { + error_report("egl: gbm_create_device failed"); + goto err; + } + + rc = qemu_egl_init_dpy_mesa((EGLNativeDisplayType)qemu_egl_rn_gbm_dev, + mode); + if (rc != 0) { + /* qemu_egl_init_dpy_mesa reports error */ + goto err; + } + + if (!epoxy_has_egl_extension(qemu_egl_display, + "EGL_KHR_surfaceless_context")) { + error_report("egl: EGL_KHR_surfaceless_context not supported"); + goto err; + } + if (!epoxy_has_egl_extension(qemu_egl_display, + "EGL_MESA_image_dma_buf_export")) { + error_report("egl: EGL_MESA_image_dma_buf_export not supported"); + goto err; + } + + qemu_egl_rn_ctx = qemu_egl_init_ctx(); + if (!qemu_egl_rn_ctx) { + error_report("egl: egl_init_ctx failed"); + goto err; + } + + return 0; + +err: + if (qemu_egl_rn_gbm_dev) { + gbm_device_destroy(qemu_egl_rn_gbm_dev); + } + if (qemu_egl_rn_fd != -1) { + close(qemu_egl_rn_fd); + } + + return -1; +} + +int egl_get_fd_for_texture(uint32_t tex_id, EGLint *stride, EGLint *fourcc, + EGLuint64KHR *modifier) +{ + EGLImageKHR image; + EGLint num_planes, fd; + + image = eglCreateImageKHR(qemu_egl_display, eglGetCurrentContext(), + EGL_GL_TEXTURE_2D_KHR, + (EGLClientBuffer)(unsigned long)tex_id, + NULL); + if (!image) { + return -1; + } + + eglExportDMABUFImageQueryMESA(qemu_egl_display, image, fourcc, + &num_planes, modifier); + if (num_planes != 1) { + eglDestroyImageKHR(qemu_egl_display, image); + return -1; + } + eglExportDMABUFImageMESA(qemu_egl_display, image, &fd, stride, NULL); + eglDestroyImageKHR(qemu_egl_display, image); + + return fd; +} + +void egl_dmabuf_import_texture(QemuDmaBuf *dmabuf) +{ + EGLImageKHR image = EGL_NO_IMAGE_KHR; + EGLint attrs[64]; + int i = 0; + + if (dmabuf->texture != 0) { + return; + } + + attrs[i++] = EGL_WIDTH; + attrs[i++] = dmabuf->width; + attrs[i++] = EGL_HEIGHT; + attrs[i++] = dmabuf->height; + attrs[i++] = EGL_LINUX_DRM_FOURCC_EXT; + attrs[i++] = dmabuf->fourcc; + + attrs[i++] = EGL_DMA_BUF_PLANE0_FD_EXT; + attrs[i++] = dmabuf->fd; + attrs[i++] = EGL_DMA_BUF_PLANE0_PITCH_EXT; + attrs[i++] = dmabuf->stride; + attrs[i++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT; + attrs[i++] = 0; +#ifdef EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT + if (dmabuf->modifier) { + attrs[i++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; + attrs[i++] = (dmabuf->modifier >> 0) & 0xffffffff; + attrs[i++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; + attrs[i++] = (dmabuf->modifier >> 32) & 0xffffffff; + } +#endif + attrs[i++] = EGL_NONE; + + image = eglCreateImageKHR(qemu_egl_display, + EGL_NO_CONTEXT, + EGL_LINUX_DMA_BUF_EXT, + NULL, attrs); + if (image == EGL_NO_IMAGE_KHR) { + error_report("eglCreateImageKHR failed"); + return; + } + + glGenTextures(1, &dmabuf->texture); + glBindTexture(GL_TEXTURE_2D, dmabuf->texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)image); + eglDestroyImageKHR(qemu_egl_display, image); +} + +void egl_dmabuf_release_texture(QemuDmaBuf *dmabuf) +{ + if (dmabuf->texture == 0) { + return; + } + + glDeleteTextures(1, &dmabuf->texture); + dmabuf->texture = 0; +} + +void egl_dmabuf_create_sync(QemuDmaBuf *dmabuf) +{ + EGLSyncKHR sync; + + if (epoxy_has_egl_extension(qemu_egl_display, + "EGL_KHR_fence_sync") && + epoxy_has_egl_extension(qemu_egl_display, + "EGL_ANDROID_native_fence_sync")) { + sync = eglCreateSyncKHR(qemu_egl_display, + EGL_SYNC_NATIVE_FENCE_ANDROID, NULL); + if (sync != EGL_NO_SYNC_KHR) { + dmabuf->sync = sync; + } + } +} + +void egl_dmabuf_create_fence(QemuDmaBuf *dmabuf) +{ + if (dmabuf->sync) { + dmabuf->fence_fd = eglDupNativeFenceFDANDROID(qemu_egl_display, + dmabuf->sync); + eglDestroySyncKHR(qemu_egl_display, dmabuf->sync); + dmabuf->sync = NULL; + } +} + +#endif /* CONFIG_GBM */ + +/* ---------------------------------------------------------------------- */ + +EGLSurface qemu_egl_init_surface_x11(EGLContext ectx, EGLNativeWindowType win) +{ + EGLSurface esurface; + EGLBoolean b; + + esurface = eglCreateWindowSurface(qemu_egl_display, + qemu_egl_config, + win, NULL); + if (esurface == EGL_NO_SURFACE) { + error_report("egl: eglCreateWindowSurface failed"); + return NULL; + } + + b = eglMakeCurrent(qemu_egl_display, esurface, esurface, ectx); + if (b == EGL_FALSE) { + error_report("egl: eglMakeCurrent failed"); + return NULL; + } + + return esurface; +} + +/* ---------------------------------------------------------------------- */ + +#if defined(CONFIG_X11) || defined(CONFIG_GBM) + +/* + * Taken from glamor_egl.h from the Xorg xserver, which is MIT licensed + * + * Create an EGLDisplay from a native display type. This is a little quirky + * for a few reasons. + * + * 1: GetPlatformDisplayEXT and GetPlatformDisplay are the API you want to + * use, but have different function signatures in the third argument; this + * happens not to matter for us, at the moment, but it means epoxy won't alias + * them together. + * + * 2: epoxy 1.3 and earlier don't understand EGL client extensions, which + * means you can't call "eglGetPlatformDisplayEXT" directly, as the resolver + * will crash. + * + * 3: You can't tell whether you have EGL 1.5 at this point, because + * eglQueryString(EGL_VERSION) is a property of the display, which we don't + * have yet. So you have to query for extensions no matter what. Fortunately + * epoxy_has_egl_extension _does_ let you query for client extensions, so + * we don't have to write our own extension string parsing. + * + * 4. There is no EGL_KHR_platform_base to complement the EXT one, thus one + * needs to know EGL 1.5 is supported in order to use the eglGetPlatformDisplay + * function pointer. + * We can workaround this (circular dependency) by probing for the EGL 1.5 + * platform extensions (EGL_KHR_platform_gbm and friends) yet it doesn't seem + * like mesa will be able to advertise these (even though it can do EGL 1.5). + */ +static EGLDisplay qemu_egl_get_display(EGLNativeDisplayType native, + EGLenum platform) +{ + EGLDisplay dpy = EGL_NO_DISPLAY; + + /* In practise any EGL 1.5 implementation would support the EXT extension */ + if (epoxy_has_egl_extension(NULL, "EGL_EXT_platform_base")) { + PFNEGLGETPLATFORMDISPLAYEXTPROC getPlatformDisplayEXT = + (void *) eglGetProcAddress("eglGetPlatformDisplayEXT"); + if (getPlatformDisplayEXT && platform != 0) { + dpy = getPlatformDisplayEXT(platform, native, NULL); + } + } + + if (dpy == EGL_NO_DISPLAY) { + /* fallback */ + dpy = eglGetDisplay(native); + } + return dpy; +} + +static int qemu_egl_init_dpy(EGLNativeDisplayType dpy, + EGLenum platform, + DisplayGLMode mode) +{ + static const EGLint conf_att_core[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + EGL_RED_SIZE, 5, + EGL_GREEN_SIZE, 5, + EGL_BLUE_SIZE, 5, + EGL_ALPHA_SIZE, 0, + EGL_NONE, + }; + static const EGLint conf_att_gles[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 5, + EGL_GREEN_SIZE, 5, + EGL_BLUE_SIZE, 5, + EGL_ALPHA_SIZE, 0, + EGL_NONE, + }; + EGLint major, minor; + EGLBoolean b; + EGLint n; + bool gles = (mode == DISPLAYGL_MODE_ES); + + qemu_egl_display = qemu_egl_get_display(dpy, platform); + if (qemu_egl_display == EGL_NO_DISPLAY) { + error_report("egl: eglGetDisplay failed"); + return -1; + } + + b = eglInitialize(qemu_egl_display, &major, &minor); + if (b == EGL_FALSE) { + error_report("egl: eglInitialize failed"); + return -1; + } + + b = eglBindAPI(gles ? EGL_OPENGL_ES_API : EGL_OPENGL_API); + if (b == EGL_FALSE) { + error_report("egl: eglBindAPI failed (%s mode)", + gles ? "gles" : "core"); + return -1; + } + + b = eglChooseConfig(qemu_egl_display, + gles ? conf_att_gles : conf_att_core, + &qemu_egl_config, 1, &n); + if (b == EGL_FALSE || n != 1) { + error_report("egl: eglChooseConfig failed (%s mode)", + gles ? "gles" : "core"); + return -1; + } + + qemu_egl_mode = gles ? DISPLAYGL_MODE_ES : DISPLAYGL_MODE_CORE; + return 0; +} + +int qemu_egl_init_dpy_x11(EGLNativeDisplayType dpy, DisplayGLMode mode) +{ +#ifdef EGL_KHR_platform_x11 + return qemu_egl_init_dpy(dpy, EGL_PLATFORM_X11_KHR, mode); +#else + return qemu_egl_init_dpy(dpy, 0, mode); +#endif +} + +int qemu_egl_init_dpy_mesa(EGLNativeDisplayType dpy, DisplayGLMode mode) +{ +#ifdef EGL_MESA_platform_gbm + return qemu_egl_init_dpy(dpy, EGL_PLATFORM_GBM_MESA, mode); +#else + return qemu_egl_init_dpy(dpy, 0, mode); +#endif +} + +#endif + +bool qemu_egl_has_dmabuf(void) +{ + if (qemu_egl_display == EGL_NO_DISPLAY) { + return false; + } + + return epoxy_has_egl_extension(qemu_egl_display, + "EGL_EXT_image_dma_buf_import"); +} + +EGLContext qemu_egl_init_ctx(void) +{ + static const EGLint ctx_att_core[] = { + EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, + EGL_NONE + }; + static const EGLint ctx_att_gles[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + bool gles = (qemu_egl_mode == DISPLAYGL_MODE_ES); + EGLContext ectx; + EGLBoolean b; + + ectx = eglCreateContext(qemu_egl_display, qemu_egl_config, EGL_NO_CONTEXT, + gles ? ctx_att_gles : ctx_att_core); + if (ectx == EGL_NO_CONTEXT) { + error_report("egl: eglCreateContext failed"); + return NULL; + } + + b = eglMakeCurrent(qemu_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, ectx); + if (b == EGL_FALSE) { + error_report("egl: eglMakeCurrent failed"); + return NULL; + } + + return ectx; +} diff --git a/ui/gtk-clipboard.c b/ui/gtk-clipboard.c new file mode 100644 index 000000000..35b7a2c22 --- /dev/null +++ b/ui/gtk-clipboard.c @@ -0,0 +1,192 @@ +/* + * GTK UI -- clipboard support + * + * Copyright (C) 2021 Gerd Hoffmann <kraxel@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "qemu/osdep.h" +#include "qemu-common.h" +#include "qemu/main-loop.h" + +#include "ui/gtk.h" + +static QemuClipboardSelection gd_find_selection(GtkDisplayState *gd, + GtkClipboard *clipboard) +{ + QemuClipboardSelection s; + + for (s = 0; s < QEMU_CLIPBOARD_SELECTION__COUNT; s++) { + if (gd->gtkcb[s] == clipboard) { + return s; + } + } + return QEMU_CLIPBOARD_SELECTION_CLIPBOARD; +} + +static void gd_clipboard_get_data(GtkClipboard *clipboard, + GtkSelectionData *selection_data, + guint selection_info, + gpointer data) +{ + GtkDisplayState *gd = data; + QemuClipboardSelection s = gd_find_selection(gd, clipboard); + QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT; + g_autoptr(QemuClipboardInfo) info = NULL; + + info = qemu_clipboard_info_ref(qemu_clipboard_info(s)); + + qemu_clipboard_request(info, type); + while (info == qemu_clipboard_info(s) && + info->types[type].available && + info->types[type].data == NULL) { + main_loop_wait(false); + } + + if (info == qemu_clipboard_info(s) && gd->cbowner[s]) { + gtk_selection_data_set_text(selection_data, + info->types[type].data, + info->types[type].size); + } else { + /* clipboard owner changed while waiting for the data */ + } +} + +static void gd_clipboard_clear(GtkClipboard *clipboard, + gpointer data) +{ + GtkDisplayState *gd = data; + QemuClipboardSelection s = gd_find_selection(gd, clipboard); + + gd->cbowner[s] = false; +} + +static void gd_clipboard_notify(Notifier *notifier, void *data) +{ + GtkDisplayState *gd = container_of(notifier, GtkDisplayState, cbpeer.update); + QemuClipboardInfo *info = data; + QemuClipboardSelection s = info->selection; + bool self_update = info->owner == &gd->cbpeer; + + if (info != qemu_clipboard_info(s)) { + gd->cbpending[s] = 0; + if (!self_update) { + GtkTargetList *list; + GtkTargetEntry *targets; + gint n_targets; + + list = gtk_target_list_new(NULL, 0); + if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { + gtk_target_list_add_text_targets(list, 0); + } + targets = gtk_target_table_new_from_list(list, &n_targets); + + gtk_clipboard_clear(gd->gtkcb[s]); + gd->cbowner[s] = true; + gtk_clipboard_set_with_data(gd->gtkcb[s], + targets, n_targets, + gd_clipboard_get_data, + gd_clipboard_clear, + gd); + + gtk_target_table_free(targets, n_targets); + gtk_target_list_unref(list); + } + return; + } + + if (self_update) { + return; + } + + /* + * Clipboard got updated, with data probably. No action here, we + * are waiting for updates in gd_clipboard_get_data(). + */ +} + +static void gd_clipboard_request(QemuClipboardInfo *info, + QemuClipboardType type) +{ + GtkDisplayState *gd = container_of(info->owner, GtkDisplayState, cbpeer); + char *text; + + switch (type) { + case QEMU_CLIPBOARD_TYPE_TEXT: + text = gtk_clipboard_wait_for_text(gd->gtkcb[info->selection]); + if (text) { + qemu_clipboard_set_data(&gd->cbpeer, info, type, + strlen(text), text, true); + g_free(text); + } + break; + default: + break; + } +} + +static void gd_owner_change(GtkClipboard *clipboard, + GdkEvent *event, + gpointer data) +{ + GtkDisplayState *gd = data; + QemuClipboardSelection s = gd_find_selection(gd, clipboard); + QemuClipboardInfo *info; + + if (gd->cbowner[s]) { + /* ignore notifications about our own grabs */ + return; + } + + + switch (event->owner_change.reason) { + case GDK_OWNER_CHANGE_NEW_OWNER: + info = qemu_clipboard_info_new(&gd->cbpeer, s); + if (gtk_clipboard_wait_is_text_available(clipboard)) { + info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; + } + + qemu_clipboard_update(info); + qemu_clipboard_info_unref(info); + break; + default: + qemu_clipboard_peer_release(&gd->cbpeer, s); + gd->cbowner[s] = false; + break; + } +} + +void gd_clipboard_init(GtkDisplayState *gd) +{ + gd->cbpeer.name = "gtk"; + gd->cbpeer.update.notify = gd_clipboard_notify; + gd->cbpeer.request = gd_clipboard_request; + qemu_clipboard_peer_register(&gd->cbpeer); + + gd->gtkcb[QEMU_CLIPBOARD_SELECTION_CLIPBOARD] = + gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); + gd->gtkcb[QEMU_CLIPBOARD_SELECTION_PRIMARY] = + gtk_clipboard_get(GDK_SELECTION_PRIMARY); + gd->gtkcb[QEMU_CLIPBOARD_SELECTION_SECONDARY] = + gtk_clipboard_get(GDK_SELECTION_SECONDARY); + + g_signal_connect(gd->gtkcb[QEMU_CLIPBOARD_SELECTION_CLIPBOARD], + "owner-change", G_CALLBACK(gd_owner_change), gd); + g_signal_connect(gd->gtkcb[QEMU_CLIPBOARD_SELECTION_PRIMARY], + "owner-change", G_CALLBACK(gd_owner_change), gd); + g_signal_connect(gd->gtkcb[QEMU_CLIPBOARD_SELECTION_SECONDARY], + "owner-change", G_CALLBACK(gd_owner_change), gd); +} diff --git a/ui/gtk-egl.c b/ui/gtk-egl.c new file mode 100644 index 000000000..45cb67712 --- /dev/null +++ b/ui/gtk-egl.c @@ -0,0 +1,372 @@ +/* + * GTK UI -- egl opengl code. + * + * Note that gtk 3.16+ (released 2015-03-23) has a GtkGLArea widget, + * which is GtkDrawingArea like widget with opengl rendering support. + * + * This code handles opengl support on older gtk versions, using egl + * to get a opengl context for the X11 window. + * + * This work is licensed under the terms of the GNU GPL, version 2 or later. + * See the COPYING file in the top-level directory. + */ + +#include "qemu/osdep.h" +#include "qemu/main-loop.h" + +#include "trace.h" + +#include "ui/console.h" +#include "ui/gtk.h" +#include "ui/egl-helpers.h" +#include "ui/shader.h" + +#include "sysemu/sysemu.h" + +static void gtk_egl_set_scanout_mode(VirtualConsole *vc, bool scanout) +{ + if (vc->gfx.scanout_mode == scanout) { + return; + } + + vc->gfx.scanout_mode = scanout; + if (!vc->gfx.scanout_mode) { + egl_fb_destroy(&vc->gfx.guest_fb); + if (vc->gfx.surface) { + surface_gl_destroy_texture(vc->gfx.gls, vc->gfx.ds); + surface_gl_create_texture(vc->gfx.gls, vc->gfx.ds); + } + } +} + +/** DisplayState Callbacks (opengl version) **/ + +void gd_egl_init(VirtualConsole *vc) +{ + GdkWindow *gdk_window = gtk_widget_get_window(vc->gfx.drawing_area); + if (!gdk_window) { + return; + } + + Window x11_window = gdk_x11_window_get_xid(gdk_window); + if (!x11_window) { + return; + } + + vc->gfx.ectx = qemu_egl_init_ctx(); + vc->gfx.esurface = qemu_egl_init_surface_x11 + (vc->gfx.ectx, (EGLNativeWindowType)x11_window); + + assert(vc->gfx.esurface); +} + +void gd_egl_draw(VirtualConsole *vc) +{ + GdkWindow *window; +#ifdef CONFIG_GBM + QemuDmaBuf *dmabuf = vc->gfx.guest_fb.dmabuf; +#endif + int ww, wh; + + if (!vc->gfx.gls) { + return; + } + + window = gtk_widget_get_window(vc->gfx.drawing_area); + ww = gdk_window_get_width(window); + wh = gdk_window_get_height(window); + + if (vc->gfx.scanout_mode) { +#ifdef CONFIG_GBM + if (dmabuf) { + if (!dmabuf->draw_submitted) { + return; + } else { + dmabuf->draw_submitted = false; + } + } +#endif + gd_egl_scanout_flush(&vc->gfx.dcl, 0, 0, vc->gfx.w, vc->gfx.h); + + vc->gfx.scale_x = (double)ww / vc->gfx.w; + vc->gfx.scale_y = (double)wh / vc->gfx.h; + + glFlush(); +#ifdef CONFIG_GBM + if (dmabuf) { + egl_dmabuf_create_fence(dmabuf); + if (dmabuf->fence_fd > 0) { + qemu_set_fd_handler(dmabuf->fence_fd, gd_hw_gl_flushed, NULL, vc); + return; + } + graphic_hw_gl_block(vc->gfx.dcl.con, false); + } +#endif + } else { + if (!vc->gfx.ds) { + return; + } + eglMakeCurrent(qemu_egl_display, vc->gfx.esurface, + vc->gfx.esurface, vc->gfx.ectx); + + surface_gl_setup_viewport(vc->gfx.gls, vc->gfx.ds, ww, wh); + surface_gl_render_texture(vc->gfx.gls, vc->gfx.ds); + + eglSwapBuffers(qemu_egl_display, vc->gfx.esurface); + + vc->gfx.scale_x = (double)ww / surface_width(vc->gfx.ds); + vc->gfx.scale_y = (double)wh / surface_height(vc->gfx.ds); + + glFlush(); + } + + graphic_hw_gl_flushed(vc->gfx.dcl.con); +} + +void gd_egl_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + if (!vc->gfx.gls || !vc->gfx.ds) { + return; + } + + eglMakeCurrent(qemu_egl_display, vc->gfx.esurface, + vc->gfx.esurface, vc->gfx.ectx); + surface_gl_update_texture(vc->gfx.gls, vc->gfx.ds, x, y, w, h); + vc->gfx.glupdates++; +} + +void gd_egl_refresh(DisplayChangeListener *dcl) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + vc->gfx.dcl.update_interval = gd_monitor_update_interval( + vc->window ? vc->window : vc->gfx.drawing_area); + + if (!vc->gfx.esurface) { + gd_egl_init(vc); + if (!vc->gfx.esurface) { + return; + } + vc->gfx.gls = qemu_gl_init_shader(); + if (vc->gfx.ds) { + surface_gl_destroy_texture(vc->gfx.gls, vc->gfx.ds); + surface_gl_create_texture(vc->gfx.gls, vc->gfx.ds); + } +#ifdef CONFIG_GBM + if (vc->gfx.guest_fb.dmabuf) { + egl_dmabuf_release_texture(vc->gfx.guest_fb.dmabuf); + gd_egl_scanout_dmabuf(dcl, vc->gfx.guest_fb.dmabuf); + } +#endif + } + + graphic_hw_update(dcl->con); + + if (vc->gfx.glupdates) { + vc->gfx.glupdates = 0; + gtk_egl_set_scanout_mode(vc, false); + gd_egl_draw(vc); + } +} + +void gd_egl_switch(DisplayChangeListener *dcl, + DisplaySurface *surface) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + bool resized = true; + + trace_gd_switch(vc->label, surface_width(surface), surface_height(surface)); + + if (vc->gfx.ds && + surface_width(vc->gfx.ds) == surface_width(surface) && + surface_height(vc->gfx.ds) == surface_height(surface)) { + resized = false; + } + eglMakeCurrent(qemu_egl_display, vc->gfx.esurface, + vc->gfx.esurface, vc->gfx.ectx); + + surface_gl_destroy_texture(vc->gfx.gls, vc->gfx.ds); + vc->gfx.ds = surface; + if (vc->gfx.gls) { + surface_gl_create_texture(vc->gfx.gls, vc->gfx.ds); + } + + if (resized) { + gd_update_windowsize(vc); + } +} + +QEMUGLContext gd_egl_create_context(DisplayChangeListener *dcl, + QEMUGLParams *params) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + eglMakeCurrent(qemu_egl_display, vc->gfx.esurface, + vc->gfx.esurface, vc->gfx.ectx); + return qemu_egl_create_context(dcl, params); +} + +void gd_egl_scanout_disable(DisplayChangeListener *dcl) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + vc->gfx.w = 0; + vc->gfx.h = 0; + gtk_egl_set_scanout_mode(vc, false); +} + +void gd_egl_scanout_texture(DisplayChangeListener *dcl, + uint32_t backing_id, bool backing_y_0_top, + uint32_t backing_width, uint32_t backing_height, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + vc->gfx.x = x; + vc->gfx.y = y; + vc->gfx.w = w; + vc->gfx.h = h; + vc->gfx.y0_top = backing_y_0_top; + + eglMakeCurrent(qemu_egl_display, vc->gfx.esurface, + vc->gfx.esurface, vc->gfx.ectx); + + gtk_egl_set_scanout_mode(vc, true); + egl_fb_setup_for_tex(&vc->gfx.guest_fb, backing_width, backing_height, + backing_id, false); +} + +void gd_egl_scanout_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf) +{ +#ifdef CONFIG_GBM + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + eglMakeCurrent(qemu_egl_display, vc->gfx.esurface, + vc->gfx.esurface, vc->gfx.ectx); + + egl_dmabuf_import_texture(dmabuf); + if (!dmabuf->texture) { + return; + } + + gd_egl_scanout_texture(dcl, dmabuf->texture, + false, dmabuf->width, dmabuf->height, + 0, 0, dmabuf->width, dmabuf->height); + + if (dmabuf->allow_fences) { + vc->gfx.guest_fb.dmabuf = dmabuf; + } +#endif +} + +void gd_egl_cursor_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf, bool have_hot, + uint32_t hot_x, uint32_t hot_y) +{ +#ifdef CONFIG_GBM + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + if (dmabuf) { + egl_dmabuf_import_texture(dmabuf); + if (!dmabuf->texture) { + return; + } + egl_fb_setup_for_tex(&vc->gfx.cursor_fb, dmabuf->width, dmabuf->height, + dmabuf->texture, false); + } else { + egl_fb_destroy(&vc->gfx.cursor_fb); + } +#endif +} + +void gd_egl_cursor_position(DisplayChangeListener *dcl, + uint32_t pos_x, uint32_t pos_y) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + vc->gfx.cursor_x = pos_x * vc->gfx.scale_x; + vc->gfx.cursor_y = pos_y * vc->gfx.scale_y; +} + +void gd_egl_scanout_flush(DisplayChangeListener *dcl, + uint32_t x, uint32_t y, uint32_t w, uint32_t h) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + GdkWindow *window; + int ww, wh; + + if (!vc->gfx.scanout_mode) { + return; + } + if (!vc->gfx.guest_fb.framebuffer) { + return; + } + + eglMakeCurrent(qemu_egl_display, vc->gfx.esurface, + vc->gfx.esurface, vc->gfx.ectx); + + window = gtk_widget_get_window(vc->gfx.drawing_area); + ww = gdk_window_get_width(window); + wh = gdk_window_get_height(window); + egl_fb_setup_default(&vc->gfx.win_fb, ww, wh); + if (vc->gfx.cursor_fb.texture) { + egl_texture_blit(vc->gfx.gls, &vc->gfx.win_fb, &vc->gfx.guest_fb, + vc->gfx.y0_top); + egl_texture_blend(vc->gfx.gls, &vc->gfx.win_fb, &vc->gfx.cursor_fb, + vc->gfx.y0_top, + vc->gfx.cursor_x, vc->gfx.cursor_y, + vc->gfx.scale_x, vc->gfx.scale_y); + } else { + egl_fb_blit(&vc->gfx.win_fb, &vc->gfx.guest_fb, !vc->gfx.y0_top); + } + +#ifdef CONFIG_GBM + if (vc->gfx.guest_fb.dmabuf) { + egl_dmabuf_create_sync(vc->gfx.guest_fb.dmabuf); + } +#endif + + eglSwapBuffers(qemu_egl_display, vc->gfx.esurface); +} + +void gd_egl_flush(DisplayChangeListener *dcl, + uint32_t x, uint32_t y, uint32_t w, uint32_t h) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + GtkWidget *area = vc->gfx.drawing_area; + + if (vc->gfx.guest_fb.dmabuf) { + graphic_hw_gl_block(vc->gfx.dcl.con, true); + vc->gfx.guest_fb.dmabuf->draw_submitted = true; + gtk_widget_queue_draw_area(area, x, y, w, h); + return; + } + + gd_egl_scanout_flush(&vc->gfx.dcl, x, y, w, h); +} + +void gtk_egl_init(DisplayGLMode mode) +{ + GdkDisplay *gdk_display = gdk_display_get_default(); + Display *x11_display = gdk_x11_display_get_xdisplay(gdk_display); + + if (qemu_egl_init_dpy_x11(x11_display, mode) < 0) { + return; + } + + display_opengl = 1; +} + +int gd_egl_make_current(DisplayChangeListener *dcl, + QEMUGLContext ctx) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + return eglMakeCurrent(qemu_egl_display, vc->gfx.esurface, + vc->gfx.esurface, ctx); +} diff --git a/ui/gtk-gl-area.c b/ui/gtk-gl-area.c new file mode 100644 index 000000000..01e4e74ee --- /dev/null +++ b/ui/gtk-gl-area.c @@ -0,0 +1,288 @@ +/* + * GTK UI -- glarea opengl code. + * + * Requires 3.16+ (GtkGLArea widget). + * + * This work is licensed under the terms of the GNU GPL, version 2 or later. + * See the COPYING file in the top-level directory. + */ + +#include "qemu/osdep.h" +#include "qemu/main-loop.h" + +#include "trace.h" + +#include "ui/console.h" +#include "ui/gtk.h" +#include "ui/egl-helpers.h" + +#include "sysemu/sysemu.h" + +static void gtk_gl_area_set_scanout_mode(VirtualConsole *vc, bool scanout) +{ + if (vc->gfx.scanout_mode == scanout) { + return; + } + + vc->gfx.scanout_mode = scanout; + if (!vc->gfx.scanout_mode) { + egl_fb_destroy(&vc->gfx.guest_fb); + if (vc->gfx.surface) { + surface_gl_destroy_texture(vc->gfx.gls, vc->gfx.ds); + surface_gl_create_texture(vc->gfx.gls, vc->gfx.ds); + } + } +} + +/** DisplayState Callbacks (opengl version) **/ + +void gd_gl_area_draw(VirtualConsole *vc) +{ +#ifdef CONFIG_GBM + QemuDmaBuf *dmabuf = vc->gfx.guest_fb.dmabuf; +#endif + int ww, wh, ws, y1, y2; + + if (!vc->gfx.gls) { + return; + } + + gtk_gl_area_make_current(GTK_GL_AREA(vc->gfx.drawing_area)); + ws = gdk_window_get_scale_factor(gtk_widget_get_window(vc->gfx.drawing_area)); + ww = gtk_widget_get_allocated_width(vc->gfx.drawing_area) * ws; + wh = gtk_widget_get_allocated_height(vc->gfx.drawing_area) * ws; + + if (vc->gfx.scanout_mode) { + if (!vc->gfx.guest_fb.framebuffer) { + return; + } + +#ifdef CONFIG_GBM + if (dmabuf) { + if (!dmabuf->draw_submitted) { + return; + } else { + dmabuf->draw_submitted = false; + } + } +#endif + + glBindFramebuffer(GL_READ_FRAMEBUFFER, vc->gfx.guest_fb.framebuffer); + /* GtkGLArea sets GL_DRAW_FRAMEBUFFER for us */ + + glViewport(0, 0, ww, wh); + y1 = vc->gfx.y0_top ? 0 : vc->gfx.h; + y2 = vc->gfx.y0_top ? vc->gfx.h : 0; + glBlitFramebuffer(0, y1, vc->gfx.w, y2, + 0, 0, ww, wh, + GL_COLOR_BUFFER_BIT, GL_NEAREST); +#ifdef CONFIG_GBM + if (dmabuf) { + egl_dmabuf_create_sync(dmabuf); + } +#endif + glFlush(); +#ifdef CONFIG_GBM + if (dmabuf) { + egl_dmabuf_create_fence(dmabuf); + if (dmabuf->fence_fd > 0) { + qemu_set_fd_handler(dmabuf->fence_fd, gd_hw_gl_flushed, NULL, vc); + return; + } + graphic_hw_gl_block(vc->gfx.dcl.con, false); + } +#endif + } else { + if (!vc->gfx.ds) { + return; + } + gtk_gl_area_make_current(GTK_GL_AREA(vc->gfx.drawing_area)); + + surface_gl_setup_viewport(vc->gfx.gls, vc->gfx.ds, ww, wh); + surface_gl_render_texture(vc->gfx.gls, vc->gfx.ds); + } + + graphic_hw_gl_flushed(vc->gfx.dcl.con); +} + +void gd_gl_area_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + if (!vc->gfx.gls || !vc->gfx.ds) { + return; + } + + gtk_gl_area_make_current(GTK_GL_AREA(vc->gfx.drawing_area)); + surface_gl_update_texture(vc->gfx.gls, vc->gfx.ds, x, y, w, h); + vc->gfx.glupdates++; +} + +void gd_gl_area_refresh(DisplayChangeListener *dcl) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + vc->gfx.dcl.update_interval = gd_monitor_update_interval( + vc->window ? vc->window : vc->gfx.drawing_area); + + if (!vc->gfx.gls) { + if (!gtk_widget_get_realized(vc->gfx.drawing_area)) { + return; + } + gtk_gl_area_make_current(GTK_GL_AREA(vc->gfx.drawing_area)); + vc->gfx.gls = qemu_gl_init_shader(); + if (vc->gfx.ds) { + surface_gl_create_texture(vc->gfx.gls, vc->gfx.ds); + } + } + + graphic_hw_update(dcl->con); + + if (vc->gfx.glupdates) { + vc->gfx.glupdates = 0; + gtk_gl_area_set_scanout_mode(vc, false); + gtk_gl_area_queue_render(GTK_GL_AREA(vc->gfx.drawing_area)); + } +} + +void gd_gl_area_switch(DisplayChangeListener *dcl, + DisplaySurface *surface) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + bool resized = true; + + trace_gd_switch(vc->label, surface_width(surface), surface_height(surface)); + + if (vc->gfx.ds && + surface_width(vc->gfx.ds) == surface_width(surface) && + surface_height(vc->gfx.ds) == surface_height(surface)) { + resized = false; + } + + if (vc->gfx.gls) { + gtk_gl_area_make_current(GTK_GL_AREA(vc->gfx.drawing_area)); + surface_gl_destroy_texture(vc->gfx.gls, vc->gfx.ds); + surface_gl_create_texture(vc->gfx.gls, surface); + } + vc->gfx.ds = surface; + + if (resized) { + gd_update_windowsize(vc); + } +} + +QEMUGLContext gd_gl_area_create_context(DisplayChangeListener *dcl, + QEMUGLParams *params) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + GdkWindow *window; + GdkGLContext *ctx; + GError *err = NULL; + + gtk_gl_area_make_current(GTK_GL_AREA(vc->gfx.drawing_area)); + window = gtk_widget_get_window(vc->gfx.drawing_area); + ctx = gdk_window_create_gl_context(window, &err); + if (err) { + g_printerr("Create gdk gl context failed: %s\n", err->message); + g_error_free(err); + return NULL; + } + gdk_gl_context_set_required_version(ctx, + params->major_ver, + params->minor_ver); + gdk_gl_context_realize(ctx, &err); + if (err) { + g_printerr("Realize gdk gl context failed: %s\n", err->message); + g_error_free(err); + g_clear_object(&ctx); + return NULL; + } + return ctx; +} + +void gd_gl_area_destroy_context(DisplayChangeListener *dcl, QEMUGLContext ctx) +{ + /* FIXME */ +} + +void gd_gl_area_scanout_texture(DisplayChangeListener *dcl, + uint32_t backing_id, + bool backing_y_0_top, + uint32_t backing_width, + uint32_t backing_height, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + vc->gfx.x = x; + vc->gfx.y = y; + vc->gfx.w = w; + vc->gfx.h = h; + vc->gfx.y0_top = backing_y_0_top; + + gtk_gl_area_make_current(GTK_GL_AREA(vc->gfx.drawing_area)); + + if (backing_id == 0 || vc->gfx.w == 0 || vc->gfx.h == 0) { + gtk_gl_area_set_scanout_mode(vc, false); + return; + } + + gtk_gl_area_set_scanout_mode(vc, true); + egl_fb_setup_for_tex(&vc->gfx.guest_fb, backing_width, backing_height, + backing_id, false); +} + +void gd_gl_area_scanout_disable(DisplayChangeListener *dcl) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + gtk_gl_area_set_scanout_mode(vc, false); +} + +void gd_gl_area_scanout_flush(DisplayChangeListener *dcl, + uint32_t x, uint32_t y, uint32_t w, uint32_t h) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + if (vc->gfx.guest_fb.dmabuf) { + graphic_hw_gl_block(vc->gfx.dcl.con, true); + vc->gfx.guest_fb.dmabuf->draw_submitted = true; + } + gtk_gl_area_queue_render(GTK_GL_AREA(vc->gfx.drawing_area)); +} + +void gd_gl_area_scanout_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf) +{ +#ifdef CONFIG_GBM + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + gtk_gl_area_make_current(GTK_GL_AREA(vc->gfx.drawing_area)); + egl_dmabuf_import_texture(dmabuf); + if (!dmabuf->texture) { + return; + } + + gd_gl_area_scanout_texture(dcl, dmabuf->texture, + false, dmabuf->width, dmabuf->height, + 0, 0, dmabuf->width, dmabuf->height); + + if (dmabuf->allow_fences) { + vc->gfx.guest_fb.dmabuf = dmabuf; + } +#endif +} + +void gtk_gl_area_init(void) +{ + display_opengl = 1; +} + +int gd_gl_area_make_current(DisplayChangeListener *dcl, + QEMUGLContext ctx) +{ + gdk_gl_context_make_current(ctx); + return 0; +} diff --git a/ui/gtk.c b/ui/gtk.c new file mode 100644 index 000000000..428f02f2d --- /dev/null +++ b/ui/gtk.c @@ -0,0 +1,2397 @@ +/* + * GTK UI + * + * Copyright IBM, Corp. 2012 + * + * Authors: + * Anthony Liguori <aliguori@us.ibm.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + * Portions from gtk-vnc (originally licensed under the LGPL v2+): + * + * GTK VNC Widget + * + * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws> + * Copyright (C) 2009-2010 Daniel P. Berrange <dan@berrange.com> + */ + +#define GETTEXT_PACKAGE "qemu" +#define LOCALEDIR "po" + +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "qapi/qapi-commands-control.h" +#include "qapi/qapi-commands-machine.h" +#include "qapi/qapi-commands-misc.h" +#include "qemu/cutils.h" +#include "qemu/main-loop.h" + +#include "ui/console.h" +#include "ui/gtk.h" +#ifdef G_OS_WIN32 +#include <gdk/gdkwin32.h> +#endif +#include "ui/win32-kbd-hook.h" + +#include <glib/gi18n.h> +#include <locale.h> +#if defined(CONFIG_VTE) +#include <vte/vte.h> +#endif +#include <math.h> + +#include "trace.h" +#include "qemu/cutils.h" +#include "ui/input.h" +#include "sysemu/runstate.h" +#include "sysemu/sysemu.h" +#include "keymaps.h" +#include "chardev/char.h" +#include "qom/object.h" + +#define VC_WINDOW_X_MIN 320 +#define VC_WINDOW_Y_MIN 240 +#define VC_TERM_X_MIN 80 +#define VC_TERM_Y_MIN 25 +#define VC_SCALE_MIN 0.25 +#define VC_SCALE_STEP 0.25 + +#ifdef GDK_WINDOWING_X11 +#include "x_keymap.h" + +/* Gtk2 compat */ +#ifndef GDK_IS_X11_DISPLAY +#define GDK_IS_X11_DISPLAY(dpy) (dpy != NULL) +#endif +#endif + + +#ifdef GDK_WINDOWING_WAYLAND +/* Gtk2 compat */ +#ifndef GDK_IS_WAYLAND_DISPLAY +#define GDK_IS_WAYLAND_DISPLAY(dpy) (dpy != NULL) +#endif +#endif + + +#ifdef GDK_WINDOWING_WIN32 +/* Gtk2 compat */ +#ifndef GDK_IS_WIN32_DISPLAY +#define GDK_IS_WIN32_DISPLAY(dpy) (dpy != NULL) +#endif +#endif + + +#ifdef GDK_WINDOWING_BROADWAY +/* Gtk2 compat */ +#ifndef GDK_IS_BROADWAY_DISPLAY +#define GDK_IS_BROADWAY_DISPLAY(dpy) (dpy != NULL) +#endif +#endif + + +#ifdef GDK_WINDOWING_QUARTZ +/* Gtk2 compat */ +#ifndef GDK_IS_QUARTZ_DISPLAY +#define GDK_IS_QUARTZ_DISPLAY(dpy) (dpy != NULL) +#endif +#endif + + +#if !defined(CONFIG_VTE) +# define VTE_CHECK_VERSION(a, b, c) 0 +#endif + +#define HOTKEY_MODIFIERS (GDK_CONTROL_MASK | GDK_MOD1_MASK) + +static const guint16 *keycode_map; +static size_t keycode_maplen; + +struct VCChardev { + Chardev parent; + VirtualConsole *console; + bool echo; +}; +typedef struct VCChardev VCChardev; + +#define TYPE_CHARDEV_VC "chardev-vc" +DECLARE_INSTANCE_CHECKER(VCChardev, VC_CHARDEV, + TYPE_CHARDEV_VC) + +bool gtk_use_gl_area; + +static void gd_grab_pointer(VirtualConsole *vc, const char *reason); +static void gd_ungrab_pointer(GtkDisplayState *s); +static void gd_grab_keyboard(VirtualConsole *vc, const char *reason); +static void gd_ungrab_keyboard(GtkDisplayState *s); + +/** Utility Functions **/ + +static VirtualConsole *gd_vc_find_by_menu(GtkDisplayState *s) +{ + VirtualConsole *vc; + gint i; + + for (i = 0; i < s->nb_vcs; i++) { + vc = &s->vc[i]; + if (gtk_check_menu_item_get_active + (GTK_CHECK_MENU_ITEM(vc->menu_item))) { + return vc; + } + } + return NULL; +} + +static VirtualConsole *gd_vc_find_by_page(GtkDisplayState *s, gint page) +{ + VirtualConsole *vc; + gint i, p; + + for (i = 0; i < s->nb_vcs; i++) { + vc = &s->vc[i]; + p = gtk_notebook_page_num(GTK_NOTEBOOK(s->notebook), vc->tab_item); + if (p == page) { + return vc; + } + } + return NULL; +} + +static VirtualConsole *gd_vc_find_current(GtkDisplayState *s) +{ + gint page; + + page = gtk_notebook_get_current_page(GTK_NOTEBOOK(s->notebook)); + return gd_vc_find_by_page(s, page); +} + +static bool gd_is_grab_active(GtkDisplayState *s) +{ + return gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(s->grab_item)); +} + +static bool gd_grab_on_hover(GtkDisplayState *s) +{ + return gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(s->grab_on_hover_item)); +} + +static void gd_update_cursor(VirtualConsole *vc) +{ + GtkDisplayState *s = vc->s; + GdkWindow *window; + + if (vc->type != GD_VC_GFX || + !qemu_console_is_graphic(vc->gfx.dcl.con)) { + return; + } + + if (!gtk_widget_get_realized(vc->gfx.drawing_area)) { + return; + } + + window = gtk_widget_get_window(GTK_WIDGET(vc->gfx.drawing_area)); + if (s->full_screen || qemu_input_is_absolute() || s->ptr_owner == vc) { + gdk_window_set_cursor(window, s->null_cursor); + } else { + gdk_window_set_cursor(window, NULL); + } +} + +static void gd_update_caption(GtkDisplayState *s) +{ + const char *status = ""; + gchar *prefix; + gchar *title; + const char *grab = ""; + bool is_paused = !runstate_is_running(); + int i; + + if (qemu_name) { + prefix = g_strdup_printf("QEMU (%s)", qemu_name); + } else { + prefix = g_strdup_printf("QEMU"); + } + + if (s->ptr_owner != NULL && + s->ptr_owner->window == NULL) { + grab = _(" - Press Ctrl+Alt+G to release grab"); + } + + if (is_paused) { + status = _(" [Paused]"); + } + s->external_pause_update = true; + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(s->pause_item), + is_paused); + s->external_pause_update = false; + + title = g_strdup_printf("%s%s%s", prefix, status, grab); + gtk_window_set_title(GTK_WINDOW(s->window), title); + g_free(title); + + for (i = 0; i < s->nb_vcs; i++) { + VirtualConsole *vc = &s->vc[i]; + + if (!vc->window) { + continue; + } + title = g_strdup_printf("%s: %s%s%s", prefix, vc->label, + vc == s->kbd_owner ? " +kbd" : "", + vc == s->ptr_owner ? " +ptr" : ""); + gtk_window_set_title(GTK_WINDOW(vc->window), title); + g_free(title); + } + + g_free(prefix); +} + +static void gd_update_geometry_hints(VirtualConsole *vc) +{ + GtkDisplayState *s = vc->s; + GdkWindowHints mask = 0; + GdkGeometry geo = {}; + GtkWidget *geo_widget = NULL; + GtkWindow *geo_window; + + if (vc->type == GD_VC_GFX) { + if (!vc->gfx.ds) { + return; + } + if (s->free_scale) { + geo.min_width = surface_width(vc->gfx.ds) * VC_SCALE_MIN; + geo.min_height = surface_height(vc->gfx.ds) * VC_SCALE_MIN; + mask |= GDK_HINT_MIN_SIZE; + } else { + geo.min_width = surface_width(vc->gfx.ds) * vc->gfx.scale_x; + geo.min_height = surface_height(vc->gfx.ds) * vc->gfx.scale_y; + mask |= GDK_HINT_MIN_SIZE; + } + geo_widget = vc->gfx.drawing_area; + gtk_widget_set_size_request(geo_widget, geo.min_width, geo.min_height); + +#if defined(CONFIG_VTE) + } else if (vc->type == GD_VC_VTE) { + VteTerminal *term = VTE_TERMINAL(vc->vte.terminal); + GtkBorder padding = { 0 }; + +#if VTE_CHECK_VERSION(0, 37, 0) + gtk_style_context_get_padding( + gtk_widget_get_style_context(vc->vte.terminal), + gtk_widget_get_state_flags(vc->vte.terminal), + &padding); +#else + { + GtkBorder *ib = NULL; + gtk_widget_style_get(vc->vte.terminal, "inner-border", &ib, NULL); + if (ib) { + padding = *ib; + gtk_border_free(ib); + } + } +#endif + + geo.width_inc = vte_terminal_get_char_width(term); + geo.height_inc = vte_terminal_get_char_height(term); + mask |= GDK_HINT_RESIZE_INC; + geo.base_width = geo.width_inc; + geo.base_height = geo.height_inc; + mask |= GDK_HINT_BASE_SIZE; + geo.min_width = geo.width_inc * VC_TERM_X_MIN; + geo.min_height = geo.height_inc * VC_TERM_Y_MIN; + mask |= GDK_HINT_MIN_SIZE; + + geo.base_width += padding.left + padding.right; + geo.base_height += padding.top + padding.bottom; + geo.min_width += padding.left + padding.right; + geo.min_height += padding.top + padding.bottom; + geo_widget = vc->vte.terminal; +#endif + } + + geo_window = GTK_WINDOW(vc->window ? vc->window : s->window); + gtk_window_set_geometry_hints(geo_window, geo_widget, &geo, mask); +} + +void gd_update_windowsize(VirtualConsole *vc) +{ + GtkDisplayState *s = vc->s; + + gd_update_geometry_hints(vc); + + if (vc->type == GD_VC_GFX && !s->full_screen && !s->free_scale) { + gtk_window_resize(GTK_WINDOW(vc->window ? vc->window : s->window), + VC_WINDOW_X_MIN, VC_WINDOW_Y_MIN); + } +} + +static void gd_update_full_redraw(VirtualConsole *vc) +{ + GtkWidget *area = vc->gfx.drawing_area; + int ww, wh; + ww = gdk_window_get_width(gtk_widget_get_window(area)); + wh = gdk_window_get_height(gtk_widget_get_window(area)); +#if defined(CONFIG_OPENGL) + if (vc->gfx.gls && gtk_use_gl_area) { + gtk_gl_area_queue_render(GTK_GL_AREA(vc->gfx.drawing_area)); + return; + } +#endif + gtk_widget_queue_draw_area(area, 0, 0, ww, wh); +} + +static void gtk_release_modifiers(GtkDisplayState *s) +{ + VirtualConsole *vc = gd_vc_find_current(s); + + if (vc->type != GD_VC_GFX || + !qemu_console_is_graphic(vc->gfx.dcl.con)) { + return; + } + qkbd_state_lift_all_keys(vc->gfx.kbd); +} + +static void gd_widget_reparent(GtkWidget *from, GtkWidget *to, + GtkWidget *widget) +{ + g_object_ref(G_OBJECT(widget)); + gtk_container_remove(GTK_CONTAINER(from), widget); + gtk_container_add(GTK_CONTAINER(to), widget); + g_object_unref(G_OBJECT(widget)); +} + +static void *gd_win32_get_hwnd(VirtualConsole *vc) +{ +#ifdef G_OS_WIN32 + return gdk_win32_window_get_impl_hwnd( + gtk_widget_get_window(vc->window ? vc->window : vc->s->window)); +#else + return NULL; +#endif +} + +/** DisplayState Callbacks **/ + +static void gd_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + GdkWindow *win; + int x1, x2, y1, y2; + int mx, my; + int fbw, fbh; + int ww, wh; + + trace_gd_update(vc->label, x, y, w, h); + + if (!gtk_widget_get_realized(vc->gfx.drawing_area)) { + return; + } + + if (vc->gfx.convert) { + pixman_image_composite(PIXMAN_OP_SRC, vc->gfx.ds->image, + NULL, vc->gfx.convert, + x, y, 0, 0, x, y, w, h); + } + + x1 = floor(x * vc->gfx.scale_x); + y1 = floor(y * vc->gfx.scale_y); + + x2 = ceil(x * vc->gfx.scale_x + w * vc->gfx.scale_x); + y2 = ceil(y * vc->gfx.scale_y + h * vc->gfx.scale_y); + + fbw = surface_width(vc->gfx.ds) * vc->gfx.scale_x; + fbh = surface_height(vc->gfx.ds) * vc->gfx.scale_y; + + win = gtk_widget_get_window(vc->gfx.drawing_area); + if (!win) { + return; + } + ww = gdk_window_get_width(win); + wh = gdk_window_get_height(win); + + mx = my = 0; + if (ww > fbw) { + mx = (ww - fbw) / 2; + } + if (wh > fbh) { + my = (wh - fbh) / 2; + } + + gtk_widget_queue_draw_area(vc->gfx.drawing_area, + mx + x1, my + y1, (x2 - x1), (y2 - y1)); +} + +static void gd_refresh(DisplayChangeListener *dcl) +{ + graphic_hw_update(dcl->con); +} + +static GdkDevice *gd_get_pointer(GdkDisplay *dpy) +{ + return gdk_seat_get_pointer(gdk_display_get_default_seat(dpy)); +} + +static void gd_mouse_set(DisplayChangeListener *dcl, + int x, int y, int visible) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + GdkDisplay *dpy; + gint x_root, y_root; + + if (qemu_input_is_absolute()) { + return; + } + + dpy = gtk_widget_get_display(vc->gfx.drawing_area); + gdk_window_get_root_coords(gtk_widget_get_window(vc->gfx.drawing_area), + x, y, &x_root, &y_root); + gdk_device_warp(gd_get_pointer(dpy), + gtk_widget_get_screen(vc->gfx.drawing_area), + x_root, y_root); + vc->s->last_x = x; + vc->s->last_y = y; +} + +static void gd_cursor_define(DisplayChangeListener *dcl, + QEMUCursor *c) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + GdkPixbuf *pixbuf; + GdkCursor *cursor; + + if (!gtk_widget_get_realized(vc->gfx.drawing_area)) { + return; + } + + pixbuf = gdk_pixbuf_new_from_data((guchar *)(c->data), + GDK_COLORSPACE_RGB, true, 8, + c->width, c->height, c->width * 4, + NULL, NULL); + cursor = gdk_cursor_new_from_pixbuf + (gtk_widget_get_display(vc->gfx.drawing_area), + pixbuf, c->hot_x, c->hot_y); + gdk_window_set_cursor(gtk_widget_get_window(vc->gfx.drawing_area), cursor); + g_object_unref(pixbuf); + g_object_unref(cursor); +} + +static void gd_switch(DisplayChangeListener *dcl, + DisplaySurface *surface) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + bool resized = true; + + trace_gd_switch(vc->label, surface_width(surface), surface_height(surface)); + + if (vc->gfx.surface) { + cairo_surface_destroy(vc->gfx.surface); + vc->gfx.surface = NULL; + } + if (vc->gfx.convert) { + pixman_image_unref(vc->gfx.convert); + vc->gfx.convert = NULL; + } + + if (vc->gfx.ds && + surface_width(vc->gfx.ds) == surface_width(surface) && + surface_height(vc->gfx.ds) == surface_height(surface)) { + resized = false; + } + vc->gfx.ds = surface; + + if (surface->format == PIXMAN_x8r8g8b8) { + /* + * PIXMAN_x8r8g8b8 == CAIRO_FORMAT_RGB24 + * + * No need to convert, use surface directly. Should be the + * common case as this is qemu_default_pixelformat(32) too. + */ + vc->gfx.surface = cairo_image_surface_create_for_data + (surface_data(surface), + CAIRO_FORMAT_RGB24, + surface_width(surface), + surface_height(surface), + surface_stride(surface)); + } else { + /* Must convert surface, use pixman to do it. */ + vc->gfx.convert = pixman_image_create_bits(PIXMAN_x8r8g8b8, + surface_width(surface), + surface_height(surface), + NULL, 0); + vc->gfx.surface = cairo_image_surface_create_for_data + ((void *)pixman_image_get_data(vc->gfx.convert), + CAIRO_FORMAT_RGB24, + pixman_image_get_width(vc->gfx.convert), + pixman_image_get_height(vc->gfx.convert), + pixman_image_get_stride(vc->gfx.convert)); + pixman_image_composite(PIXMAN_OP_SRC, vc->gfx.ds->image, + NULL, vc->gfx.convert, + 0, 0, 0, 0, 0, 0, + pixman_image_get_width(vc->gfx.convert), + pixman_image_get_height(vc->gfx.convert)); + } + + if (resized) { + gd_update_windowsize(vc); + } else { + gd_update_full_redraw(vc); + } +} + +static const DisplayChangeListenerOps dcl_ops = { + .dpy_name = "gtk", + .dpy_gfx_update = gd_update, + .dpy_gfx_switch = gd_switch, + .dpy_gfx_check_format = qemu_pixman_check_format, + .dpy_refresh = gd_refresh, + .dpy_mouse_set = gd_mouse_set, + .dpy_cursor_define = gd_cursor_define, +}; + + +#if defined(CONFIG_OPENGL) + +static bool gd_has_dmabuf(DisplayChangeListener *dcl) +{ + VirtualConsole *vc = container_of(dcl, VirtualConsole, gfx.dcl); + + if (gtk_use_gl_area && !gtk_widget_get_realized(vc->gfx.drawing_area)) { + /* FIXME: Assume it will work, actual check done after realize */ + /* fixing this would require delaying listener registration */ + return true; + } + + return vc->gfx.has_dmabuf; +} + +static void gd_gl_release_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf) +{ +#ifdef CONFIG_GBM + egl_dmabuf_release_texture(dmabuf); +#endif +} + +void gd_hw_gl_flushed(void *vcon) +{ + VirtualConsole *vc = vcon; + QemuDmaBuf *dmabuf = vc->gfx.guest_fb.dmabuf; + + qemu_set_fd_handler(dmabuf->fence_fd, NULL, NULL, NULL); + close(dmabuf->fence_fd); + dmabuf->fence_fd = -1; + graphic_hw_gl_block(vc->gfx.dcl.con, false); + graphic_hw_gl_flushed(vc->gfx.dcl.con); +} + +/** DisplayState Callbacks (opengl version) **/ + +static const DisplayChangeListenerOps dcl_gl_area_ops = { + .dpy_name = "gtk-egl", + .dpy_gfx_update = gd_gl_area_update, + .dpy_gfx_switch = gd_gl_area_switch, + .dpy_gfx_check_format = console_gl_check_format, + .dpy_refresh = gd_gl_area_refresh, + .dpy_mouse_set = gd_mouse_set, + .dpy_cursor_define = gd_cursor_define, + + .dpy_gl_ctx_create = gd_gl_area_create_context, + .dpy_gl_ctx_destroy = gd_gl_area_destroy_context, + .dpy_gl_ctx_make_current = gd_gl_area_make_current, + .dpy_gl_scanout_texture = gd_gl_area_scanout_texture, + .dpy_gl_scanout_disable = gd_gl_area_scanout_disable, + .dpy_gl_update = gd_gl_area_scanout_flush, + .dpy_gl_scanout_dmabuf = gd_gl_area_scanout_dmabuf, + .dpy_gl_release_dmabuf = gd_gl_release_dmabuf, + .dpy_has_dmabuf = gd_has_dmabuf, +}; + +#ifdef CONFIG_X11 + +static const DisplayChangeListenerOps dcl_egl_ops = { + .dpy_name = "gtk-egl", + .dpy_gfx_update = gd_egl_update, + .dpy_gfx_switch = gd_egl_switch, + .dpy_gfx_check_format = console_gl_check_format, + .dpy_refresh = gd_egl_refresh, + .dpy_mouse_set = gd_mouse_set, + .dpy_cursor_define = gd_cursor_define, + + .dpy_gl_ctx_create = gd_egl_create_context, + .dpy_gl_ctx_destroy = qemu_egl_destroy_context, + .dpy_gl_ctx_make_current = gd_egl_make_current, + .dpy_gl_scanout_disable = gd_egl_scanout_disable, + .dpy_gl_scanout_texture = gd_egl_scanout_texture, + .dpy_gl_scanout_dmabuf = gd_egl_scanout_dmabuf, + .dpy_gl_cursor_dmabuf = gd_egl_cursor_dmabuf, + .dpy_gl_cursor_position = gd_egl_cursor_position, + .dpy_gl_update = gd_egl_flush, + .dpy_gl_release_dmabuf = gd_gl_release_dmabuf, + .dpy_has_dmabuf = gd_has_dmabuf, +}; + +#endif + +#endif /* CONFIG_OPENGL */ + +/** QEMU Events **/ + +static void gd_change_runstate(void *opaque, bool running, RunState state) +{ + GtkDisplayState *s = opaque; + + gd_update_caption(s); +} + +static void gd_mouse_mode_change(Notifier *notify, void *data) +{ + GtkDisplayState *s; + int i; + + s = container_of(notify, GtkDisplayState, mouse_mode_notifier); + /* release the grab at switching to absolute mode */ + if (qemu_input_is_absolute() && gd_is_grab_active(s)) { + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(s->grab_item), + FALSE); + } + for (i = 0; i < s->nb_vcs; i++) { + VirtualConsole *vc = &s->vc[i]; + gd_update_cursor(vc); + } +} + +/** GTK Events **/ + +static gboolean gd_window_close(GtkWidget *widget, GdkEvent *event, + void *opaque) +{ + GtkDisplayState *s = opaque; + bool allow_close = true; + + if (s->opts->has_window_close && !s->opts->window_close) { + allow_close = false; + } + + if (allow_close) { + qmp_quit(NULL); + } + + return TRUE; +} + +static void gd_set_ui_info(VirtualConsole *vc, gint width, gint height) +{ + QemuUIInfo info; + + memset(&info, 0, sizeof(info)); + info.width = width; + info.height = height; + dpy_set_ui_info(vc->gfx.dcl.con, &info); +} + +#if defined(CONFIG_OPENGL) + +static gboolean gd_render_event(GtkGLArea *area, GdkGLContext *context, + void *opaque) +{ + VirtualConsole *vc = opaque; + + if (vc->gfx.gls) { + gd_gl_area_draw(vc); + } + return TRUE; +} + +static void gd_resize_event(GtkGLArea *area, + gint width, gint height, gpointer *opaque) +{ + VirtualConsole *vc = (void *)opaque; + + gd_set_ui_info(vc, width, height); +} + +#endif + +/* + * If available, return the update interval of the monitor in ms, + * else return 0 (the default update interval). + */ +int gd_monitor_update_interval(GtkWidget *widget) +{ +#ifdef GDK_VERSION_3_22 + GdkWindow *win = gtk_widget_get_window(widget); + + if (win) { + GdkDisplay *dpy = gtk_widget_get_display(widget); + GdkMonitor *monitor = gdk_display_get_monitor_at_window(dpy, win); + int refresh_rate = gdk_monitor_get_refresh_rate(monitor); /* [mHz] */ + + if (refresh_rate) { + /* T = 1 / f = 1 [s*Hz] / f = 1000*1000 [ms*mHz] / f */ + return MIN(1000 * 1000 / refresh_rate, + GUI_REFRESH_INTERVAL_DEFAULT); + } + } +#endif + return 0; +} + +static gboolean gd_draw_event(GtkWidget *widget, cairo_t *cr, void *opaque) +{ + VirtualConsole *vc = opaque; + GtkDisplayState *s = vc->s; + int mx, my; + int ww, wh; + int fbw, fbh; + +#if defined(CONFIG_OPENGL) + if (vc->gfx.gls) { + if (gtk_use_gl_area) { + /* invoke render callback please */ + return FALSE; + } else { +#ifdef CONFIG_X11 + gd_egl_draw(vc); + return TRUE; +#else + abort(); +#endif + } + } +#endif + + if (!gtk_widget_get_realized(widget)) { + return FALSE; + } + if (!vc->gfx.ds) { + return FALSE; + } + if (!vc->gfx.surface) { + return FALSE; + } + + vc->gfx.dcl.update_interval = + gd_monitor_update_interval(vc->window ? vc->window : s->window); + + fbw = surface_width(vc->gfx.ds); + fbh = surface_height(vc->gfx.ds); + + ww = gdk_window_get_width(gtk_widget_get_window(widget)); + wh = gdk_window_get_height(gtk_widget_get_window(widget)); + + if (s->full_screen) { + vc->gfx.scale_x = (double)ww / fbw; + vc->gfx.scale_y = (double)wh / fbh; + } else if (s->free_scale) { + double sx, sy; + + sx = (double)ww / fbw; + sy = (double)wh / fbh; + + vc->gfx.scale_x = vc->gfx.scale_y = MIN(sx, sy); + } + + fbw *= vc->gfx.scale_x; + fbh *= vc->gfx.scale_y; + + mx = my = 0; + if (ww > fbw) { + mx = (ww - fbw) / 2; + } + if (wh > fbh) { + my = (wh - fbh) / 2; + } + + cairo_rectangle(cr, 0, 0, ww, wh); + + /* Optionally cut out the inner area where the pixmap + will be drawn. This avoids 'flashing' since we're + not double-buffering. Note we're using the undocumented + behaviour of drawing the rectangle from right to left + to cut out the whole */ + cairo_rectangle(cr, mx + fbw, my, + -1 * fbw, fbh); + cairo_fill(cr); + + cairo_scale(cr, vc->gfx.scale_x, vc->gfx.scale_y); + cairo_set_source_surface(cr, vc->gfx.surface, + mx / vc->gfx.scale_x, my / vc->gfx.scale_y); + cairo_paint(cr); + + return TRUE; +} + +static gboolean gd_motion_event(GtkWidget *widget, GdkEventMotion *motion, + void *opaque) +{ + VirtualConsole *vc = opaque; + GtkDisplayState *s = vc->s; + GdkWindow *window; + int x, y; + int mx, my; + int fbh, fbw; + int ww, wh, ws; + + if (!vc->gfx.ds) { + return TRUE; + } + + fbw = surface_width(vc->gfx.ds) * vc->gfx.scale_x; + fbh = surface_height(vc->gfx.ds) * vc->gfx.scale_y; + + window = gtk_widget_get_window(vc->gfx.drawing_area); + ww = gdk_window_get_width(window); + wh = gdk_window_get_height(window); + ws = gdk_window_get_scale_factor(window); + + mx = my = 0; + if (ww > fbw) { + mx = (ww - fbw) / 2; + } + if (wh > fbh) { + my = (wh - fbh) / 2; + } + + x = (motion->x - mx) / vc->gfx.scale_x * ws; + y = (motion->y - my) / vc->gfx.scale_y * ws; + + if (qemu_input_is_absolute()) { + if (x < 0 || y < 0 || + x >= surface_width(vc->gfx.ds) || + y >= surface_height(vc->gfx.ds)) { + return TRUE; + } + qemu_input_queue_abs(vc->gfx.dcl.con, INPUT_AXIS_X, x, + 0, surface_width(vc->gfx.ds)); + qemu_input_queue_abs(vc->gfx.dcl.con, INPUT_AXIS_Y, y, + 0, surface_height(vc->gfx.ds)); + qemu_input_event_sync(); + } else if (s->last_set && s->ptr_owner == vc) { + qemu_input_queue_rel(vc->gfx.dcl.con, INPUT_AXIS_X, x - s->last_x); + qemu_input_queue_rel(vc->gfx.dcl.con, INPUT_AXIS_Y, y - s->last_y); + qemu_input_event_sync(); + } + s->last_x = x; + s->last_y = y; + s->last_set = TRUE; + + if (!qemu_input_is_absolute() && s->ptr_owner == vc) { + GdkScreen *screen = gtk_widget_get_screen(vc->gfx.drawing_area); + GdkDisplay *dpy = gtk_widget_get_display(widget); + GdkWindow *win = gtk_widget_get_window(widget); + GdkMonitor *monitor = gdk_display_get_monitor_at_window(dpy, win); + GdkRectangle geometry; + + int x = (int)motion->x_root; + int y = (int)motion->y_root; + + gdk_monitor_get_geometry(monitor, &geometry); + + /* In relative mode check to see if client pointer hit + * one of the monitor edges, and if so move it back to the + * center of the monitor. This is important because the pointer + * in the server doesn't correspond 1-for-1, and so + * may still be only half way across the screen. Without + * this warp, the server pointer would thus appear to hit + * an invisible wall */ + if (x <= geometry.x || x - geometry.x >= geometry.width - 1 || + y <= geometry.y || y - geometry.y >= geometry.height - 1) { + GdkDevice *dev = gdk_event_get_device((GdkEvent *)motion); + x = geometry.x + geometry.width / 2; + y = geometry.y + geometry.height / 2; + + gdk_device_warp(dev, screen, x, y); + s->last_set = FALSE; + return FALSE; + } + } + return TRUE; +} + +static gboolean gd_button_event(GtkWidget *widget, GdkEventButton *button, + void *opaque) +{ + VirtualConsole *vc = opaque; + GtkDisplayState *s = vc->s; + InputButton btn; + + /* implicitly grab the input at the first click in the relative mode */ + if (button->button == 1 && button->type == GDK_BUTTON_PRESS && + !qemu_input_is_absolute() && s->ptr_owner != vc) { + if (!vc->window) { + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(s->grab_item), + TRUE); + } else { + gd_grab_pointer(vc, "relative-mode-click"); + } + return TRUE; + } + + if (button->button == 1) { + btn = INPUT_BUTTON_LEFT; + } else if (button->button == 2) { + btn = INPUT_BUTTON_MIDDLE; + } else if (button->button == 3) { + btn = INPUT_BUTTON_RIGHT; + } else if (button->button == 8) { + btn = INPUT_BUTTON_SIDE; + } else if (button->button == 9) { + btn = INPUT_BUTTON_EXTRA; + } else { + return TRUE; + } + + qemu_input_queue_btn(vc->gfx.dcl.con, btn, + button->type == GDK_BUTTON_PRESS); + qemu_input_event_sync(); + return TRUE; +} + +static gboolean gd_scroll_event(GtkWidget *widget, GdkEventScroll *scroll, + void *opaque) +{ + VirtualConsole *vc = opaque; + InputButton btn; + + if (scroll->direction == GDK_SCROLL_UP) { + btn = INPUT_BUTTON_WHEEL_UP; + } else if (scroll->direction == GDK_SCROLL_DOWN) { + btn = INPUT_BUTTON_WHEEL_DOWN; + } else if (scroll->direction == GDK_SCROLL_SMOOTH) { + gdouble delta_x, delta_y; + if (!gdk_event_get_scroll_deltas((GdkEvent *)scroll, + &delta_x, &delta_y)) { + return TRUE; + } + if (delta_y == 0) { + return TRUE; + } else if (delta_y > 0) { + btn = INPUT_BUTTON_WHEEL_DOWN; + } else { + btn = INPUT_BUTTON_WHEEL_UP; + } + } else { + return TRUE; + } + + qemu_input_queue_btn(vc->gfx.dcl.con, btn, true); + qemu_input_event_sync(); + qemu_input_queue_btn(vc->gfx.dcl.con, btn, false); + qemu_input_event_sync(); + return TRUE; +} + + +static const guint16 *gd_get_keymap(size_t *maplen) +{ + GdkDisplay *dpy = gdk_display_get_default(); + +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY(dpy)) { + trace_gd_keymap_windowing("x11"); + return qemu_xkeymap_mapping_table( + gdk_x11_display_get_xdisplay(dpy), maplen); + } +#endif + +#ifdef GDK_WINDOWING_WAYLAND + if (GDK_IS_WAYLAND_DISPLAY(dpy)) { + trace_gd_keymap_windowing("wayland"); + *maplen = qemu_input_map_xorgevdev_to_qcode_len; + return qemu_input_map_xorgevdev_to_qcode; + } +#endif + +#ifdef GDK_WINDOWING_WIN32 + if (GDK_IS_WIN32_DISPLAY(dpy)) { + trace_gd_keymap_windowing("win32"); + *maplen = qemu_input_map_atset1_to_qcode_len; + return qemu_input_map_atset1_to_qcode; + } +#endif + +#ifdef GDK_WINDOWING_QUARTZ + if (GDK_IS_QUARTZ_DISPLAY(dpy)) { + trace_gd_keymap_windowing("quartz"); + *maplen = qemu_input_map_osx_to_qcode_len; + return qemu_input_map_osx_to_qcode; + } +#endif + +#ifdef GDK_WINDOWING_BROADWAY + if (GDK_IS_BROADWAY_DISPLAY(dpy)) { + trace_gd_keymap_windowing("broadway"); + g_warning("experimental: using broadway, x11 virtual keysym\n" + "mapping - with very limited support. See also\n" + "https://bugzilla.gnome.org/show_bug.cgi?id=700105"); + *maplen = qemu_input_map_x11_to_qcode_len; + return qemu_input_map_x11_to_qcode; + } +#endif + + g_warning("Unsupported GDK Windowing platform.\n" + "Disabling extended keycode tables.\n" + "Please report to qemu-devel@nongnu.org\n" + "including the following information:\n" + "\n" + " - Operating system\n" + " - GDK Windowing system build\n"); + return NULL; +} + + +static int gd_map_keycode(int scancode) +{ + if (!keycode_map) { + return 0; + } + if (scancode > keycode_maplen) { + return 0; + } + + return keycode_map[scancode]; +} + +static int gd_get_keycode(GdkEventKey *key) +{ +#ifdef G_OS_WIN32 + int scancode = gdk_event_get_scancode((GdkEvent *)key); + + /* translate Windows native scancodes to atset1 keycodes */ + switch (scancode & (KF_EXTENDED | 0xff)) { + case 0x145: /* NUMLOCK */ + return scancode & 0xff; + } + + return scancode & KF_EXTENDED ? + 0xe000 | (scancode & 0xff) : scancode & 0xff; + +#else + return key->hardware_keycode; +#endif +} + +static gboolean gd_text_key_down(GtkWidget *widget, + GdkEventKey *key, void *opaque) +{ + VirtualConsole *vc = opaque; + QemuConsole *con = vc->gfx.dcl.con; + + if (key->keyval == GDK_KEY_Delete) { + kbd_put_qcode_console(con, Q_KEY_CODE_DELETE, false); + } else if (key->length) { + kbd_put_string_console(con, key->string, key->length); + } else { + int qcode = gd_map_keycode(gd_get_keycode(key)); + kbd_put_qcode_console(con, qcode, false); + } + return TRUE; +} + +static gboolean gd_key_event(GtkWidget *widget, GdkEventKey *key, void *opaque) +{ + VirtualConsole *vc = opaque; + int keycode, qcode; + +#ifdef G_OS_WIN32 + /* on windows, we ought to ignore the reserved key event? */ + if (key->hardware_keycode == 0xff) + return false; + + if (!vc->s->kbd_owner) { + if (key->hardware_keycode == VK_LWIN || + key->hardware_keycode == VK_RWIN) { + return FALSE; + } + } +#endif + + if (key->keyval == GDK_KEY_Pause +#ifdef G_OS_WIN32 + /* for some reason GDK does not fill keyval for VK_PAUSE + * See https://bugzilla.gnome.org/show_bug.cgi?id=769214 + */ + || key->hardware_keycode == VK_PAUSE +#endif + ) { + qkbd_state_key_event(vc->gfx.kbd, Q_KEY_CODE_PAUSE, + key->type == GDK_KEY_PRESS); + return TRUE; + } + + keycode = gd_get_keycode(key); + qcode = gd_map_keycode(keycode); + + trace_gd_key_event(vc->label, keycode, qcode, + (key->type == GDK_KEY_PRESS) ? "down" : "up"); + + qkbd_state_key_event(vc->gfx.kbd, qcode, + key->type == GDK_KEY_PRESS); + + return TRUE; +} + +static gboolean gd_grab_broken_event(GtkWidget *widget, + GdkEventGrabBroken *event, void *opaque) +{ +#ifdef CONFIG_WIN32 + /* + * On Windows the Ctrl-Alt-Del key combination can't be grabbed. This + * key combination leaves all three keys in a stuck condition. We use + * the grab-broken-event to release all keys. + */ + if (event->keyboard) { + VirtualConsole *vc = opaque; + GtkDisplayState *s = vc->s; + + gtk_release_modifiers(s); + } +#endif + return TRUE; +} + +static gboolean gd_event(GtkWidget *widget, GdkEvent *event, void *opaque) +{ + if (event->type == GDK_MOTION_NOTIFY) { + return gd_motion_event(widget, &event->motion, opaque); + } + return FALSE; +} + +/** Window Menu Actions **/ + +static void gd_menu_pause(GtkMenuItem *item, void *opaque) +{ + GtkDisplayState *s = opaque; + + if (s->external_pause_update) { + return; + } + if (runstate_is_running()) { + qmp_stop(NULL); + } else { + qmp_cont(NULL); + } +} + +static void gd_menu_reset(GtkMenuItem *item, void *opaque) +{ + qmp_system_reset(NULL); +} + +static void gd_menu_powerdown(GtkMenuItem *item, void *opaque) +{ + qmp_system_powerdown(NULL); +} + +static void gd_menu_quit(GtkMenuItem *item, void *opaque) +{ + qmp_quit(NULL); +} + +static void gd_menu_switch_vc(GtkMenuItem *item, void *opaque) +{ + GtkDisplayState *s = opaque; + VirtualConsole *vc = gd_vc_find_by_menu(s); + GtkNotebook *nb = GTK_NOTEBOOK(s->notebook); + gint page; + + gtk_release_modifiers(s); + if (vc) { + page = gtk_notebook_page_num(nb, vc->tab_item); + gtk_notebook_set_current_page(nb, page); + gtk_widget_grab_focus(vc->focus); + } +} + +static void gd_accel_switch_vc(void *opaque) +{ + VirtualConsole *vc = opaque; + + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(vc->menu_item), TRUE); +} + +static void gd_menu_show_tabs(GtkMenuItem *item, void *opaque) +{ + GtkDisplayState *s = opaque; + VirtualConsole *vc = gd_vc_find_current(s); + + if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(s->show_tabs_item))) { + gtk_notebook_set_show_tabs(GTK_NOTEBOOK(s->notebook), TRUE); + } else { + gtk_notebook_set_show_tabs(GTK_NOTEBOOK(s->notebook), FALSE); + } + gd_update_windowsize(vc); +} + +static gboolean gd_tab_window_close(GtkWidget *widget, GdkEvent *event, + void *opaque) +{ + VirtualConsole *vc = opaque; + GtkDisplayState *s = vc->s; + + gtk_widget_set_sensitive(vc->menu_item, true); + gd_widget_reparent(vc->window, s->notebook, vc->tab_item); + gtk_notebook_set_tab_label_text(GTK_NOTEBOOK(s->notebook), + vc->tab_item, vc->label); + gtk_widget_destroy(vc->window); + vc->window = NULL; +#if defined(CONFIG_OPENGL) + if (vc->gfx.esurface) { + eglDestroySurface(qemu_egl_display, vc->gfx.esurface); + vc->gfx.esurface = NULL; + } + if (vc->gfx.ectx) { + eglDestroyContext(qemu_egl_display, vc->gfx.ectx); + vc->gfx.ectx = NULL; + } +#endif + return TRUE; +} + +static gboolean gd_win_grab(void *opaque) +{ + VirtualConsole *vc = opaque; + + fprintf(stderr, "%s: %s\n", __func__, vc->label); + if (vc->s->ptr_owner) { + gd_ungrab_pointer(vc->s); + } else { + gd_grab_pointer(vc, "user-request-detached-tab"); + } + return TRUE; +} + +static void gd_menu_untabify(GtkMenuItem *item, void *opaque) +{ + GtkDisplayState *s = opaque; + VirtualConsole *vc = gd_vc_find_current(s); + + if (vc->type == GD_VC_GFX && + qemu_console_is_graphic(vc->gfx.dcl.con)) { + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(s->grab_item), + FALSE); + } + if (!vc->window) { + gtk_widget_set_sensitive(vc->menu_item, false); + vc->window = gtk_window_new(GTK_WINDOW_TOPLEVEL); +#if defined(CONFIG_OPENGL) + if (vc->gfx.esurface) { + eglDestroySurface(qemu_egl_display, vc->gfx.esurface); + vc->gfx.esurface = NULL; + } + if (vc->gfx.esurface) { + eglDestroyContext(qemu_egl_display, vc->gfx.ectx); + vc->gfx.ectx = NULL; + } +#endif + gd_widget_reparent(s->notebook, vc->window, vc->tab_item); + + g_signal_connect(vc->window, "delete-event", + G_CALLBACK(gd_tab_window_close), vc); + gtk_widget_show_all(vc->window); + + if (qemu_console_is_graphic(vc->gfx.dcl.con)) { + GtkAccelGroup *ag = gtk_accel_group_new(); + gtk_window_add_accel_group(GTK_WINDOW(vc->window), ag); + + GClosure *cb = g_cclosure_new_swap(G_CALLBACK(gd_win_grab), + vc, NULL); + gtk_accel_group_connect(ag, GDK_KEY_g, HOTKEY_MODIFIERS, 0, cb); + } + + gd_update_geometry_hints(vc); + gd_update_caption(s); + } +} + +static void gd_menu_show_menubar(GtkMenuItem *item, void *opaque) +{ + GtkDisplayState *s = opaque; + VirtualConsole *vc = gd_vc_find_current(s); + + if (s->full_screen) { + return; + } + + if (gtk_check_menu_item_get_active( + GTK_CHECK_MENU_ITEM(s->show_menubar_item))) { + gtk_widget_show(s->menu_bar); + } else { + gtk_widget_hide(s->menu_bar); + } + gd_update_windowsize(vc); +} + +static void gd_accel_show_menubar(void *opaque) +{ + GtkDisplayState *s = opaque; + gtk_menu_item_activate(GTK_MENU_ITEM(s->show_menubar_item)); +} + +static void gd_menu_full_screen(GtkMenuItem *item, void *opaque) +{ + GtkDisplayState *s = opaque; + VirtualConsole *vc = gd_vc_find_current(s); + + if (!s->full_screen) { + gtk_notebook_set_show_tabs(GTK_NOTEBOOK(s->notebook), FALSE); + gtk_widget_hide(s->menu_bar); + if (vc->type == GD_VC_GFX) { + gtk_widget_set_size_request(vc->gfx.drawing_area, -1, -1); + } + gtk_window_fullscreen(GTK_WINDOW(s->window)); + s->full_screen = TRUE; + } else { + gtk_window_unfullscreen(GTK_WINDOW(s->window)); + gd_menu_show_tabs(GTK_MENU_ITEM(s->show_tabs_item), s); + if (gtk_check_menu_item_get_active( + GTK_CHECK_MENU_ITEM(s->show_menubar_item))) { + gtk_widget_show(s->menu_bar); + } + s->full_screen = FALSE; + if (vc->type == GD_VC_GFX) { + vc->gfx.scale_x = 1.0; + vc->gfx.scale_y = 1.0; + gd_update_windowsize(vc); + } + } + + gd_update_cursor(vc); +} + +static void gd_accel_full_screen(void *opaque) +{ + GtkDisplayState *s = opaque; + gtk_menu_item_activate(GTK_MENU_ITEM(s->full_screen_item)); +} + +static void gd_menu_zoom_in(GtkMenuItem *item, void *opaque) +{ + GtkDisplayState *s = opaque; + VirtualConsole *vc = gd_vc_find_current(s); + + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(s->zoom_fit_item), + FALSE); + + vc->gfx.scale_x += VC_SCALE_STEP; + vc->gfx.scale_y += VC_SCALE_STEP; + + gd_update_windowsize(vc); +} + +static void gd_accel_zoom_in(void *opaque) +{ + GtkDisplayState *s = opaque; + gtk_menu_item_activate(GTK_MENU_ITEM(s->zoom_in_item)); +} + +static void gd_menu_zoom_out(GtkMenuItem *item, void *opaque) +{ + GtkDisplayState *s = opaque; + VirtualConsole *vc = gd_vc_find_current(s); + + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(s->zoom_fit_item), + FALSE); + + vc->gfx.scale_x -= VC_SCALE_STEP; + vc->gfx.scale_y -= VC_SCALE_STEP; + + vc->gfx.scale_x = MAX(vc->gfx.scale_x, VC_SCALE_MIN); + vc->gfx.scale_y = MAX(vc->gfx.scale_y, VC_SCALE_MIN); + + gd_update_windowsize(vc); +} + +static void gd_menu_zoom_fixed(GtkMenuItem *item, void *opaque) +{ + GtkDisplayState *s = opaque; + VirtualConsole *vc = gd_vc_find_current(s); + + vc->gfx.scale_x = 1.0; + vc->gfx.scale_y = 1.0; + + gd_update_windowsize(vc); +} + +static void gd_menu_zoom_fit(GtkMenuItem *item, void *opaque) +{ + GtkDisplayState *s = opaque; + VirtualConsole *vc = gd_vc_find_current(s); + + if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(s->zoom_fit_item))) { + s->free_scale = TRUE; + } else { + s->free_scale = FALSE; + vc->gfx.scale_x = 1.0; + vc->gfx.scale_y = 1.0; + } + + gd_update_windowsize(vc); + gd_update_full_redraw(vc); +} + +static void gd_grab_update(VirtualConsole *vc, bool kbd, bool ptr) +{ + GdkDisplay *display = gtk_widget_get_display(vc->gfx.drawing_area); + GdkSeat *seat = gdk_display_get_default_seat(display); + GdkWindow *window = gtk_widget_get_window(vc->gfx.drawing_area); + GdkSeatCapabilities caps = 0; + GdkCursor *cursor = NULL; + + if (kbd) { + caps |= GDK_SEAT_CAPABILITY_KEYBOARD; + } + if (ptr) { + caps |= GDK_SEAT_CAPABILITY_ALL_POINTING; + cursor = vc->s->null_cursor; + } + + if (caps) { + gdk_seat_grab(seat, window, caps, false, cursor, + NULL, NULL, NULL); + } else { + gdk_seat_ungrab(seat); + } +} + +static void gd_grab_keyboard(VirtualConsole *vc, const char *reason) +{ + if (vc->s->kbd_owner) { + if (vc->s->kbd_owner == vc) { + return; + } else { + gd_ungrab_keyboard(vc->s); + } + } + + win32_kbd_set_grab(true); + gd_grab_update(vc, true, vc->s->ptr_owner == vc); + vc->s->kbd_owner = vc; + gd_update_caption(vc->s); + trace_gd_grab(vc->label, "kbd", reason); +} + +static void gd_ungrab_keyboard(GtkDisplayState *s) +{ + VirtualConsole *vc = s->kbd_owner; + + if (vc == NULL) { + return; + } + s->kbd_owner = NULL; + + win32_kbd_set_grab(false); + gd_grab_update(vc, false, vc->s->ptr_owner == vc); + gd_update_caption(s); + trace_gd_ungrab(vc->label, "kbd"); +} + +static void gd_grab_pointer(VirtualConsole *vc, const char *reason) +{ + GdkDisplay *display = gtk_widget_get_display(vc->gfx.drawing_area); + + if (vc->s->ptr_owner) { + if (vc->s->ptr_owner == vc) { + return; + } else { + gd_ungrab_pointer(vc->s); + } + } + + gd_grab_update(vc, vc->s->kbd_owner == vc, true); + gdk_device_get_position(gd_get_pointer(display), + NULL, &vc->s->grab_x_root, &vc->s->grab_y_root); + vc->s->ptr_owner = vc; + gd_update_caption(vc->s); + trace_gd_grab(vc->label, "ptr", reason); +} + +static void gd_ungrab_pointer(GtkDisplayState *s) +{ + VirtualConsole *vc = s->ptr_owner; + GdkDisplay *display; + + if (vc == NULL) { + return; + } + s->ptr_owner = NULL; + + display = gtk_widget_get_display(vc->gfx.drawing_area); + gd_grab_update(vc, vc->s->kbd_owner == vc, false); + gdk_device_warp(gd_get_pointer(display), + gtk_widget_get_screen(vc->gfx.drawing_area), + vc->s->grab_x_root, vc->s->grab_y_root); + gd_update_caption(s); + trace_gd_ungrab(vc->label, "ptr"); +} + +static void gd_menu_grab_input(GtkMenuItem *item, void *opaque) +{ + GtkDisplayState *s = opaque; + VirtualConsole *vc = gd_vc_find_current(s); + + if (gd_is_grab_active(s)) { + gd_grab_keyboard(vc, "user-request-main-window"); + gd_grab_pointer(vc, "user-request-main-window"); + } else { + gd_ungrab_keyboard(s); + gd_ungrab_pointer(s); + } + + gd_update_cursor(vc); +} + +static void gd_change_page(GtkNotebook *nb, gpointer arg1, guint arg2, + gpointer data) +{ + GtkDisplayState *s = data; + VirtualConsole *vc; + gboolean on_vga; + + if (!gtk_widget_get_realized(s->notebook)) { + return; + } + + vc = gd_vc_find_by_page(s, arg2); + if (!vc) { + return; + } + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(vc->menu_item), + TRUE); + on_vga = (vc->type == GD_VC_GFX && + qemu_console_is_graphic(vc->gfx.dcl.con)); + if (!on_vga) { + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(s->grab_item), + FALSE); + } else if (s->full_screen) { + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(s->grab_item), + TRUE); + } + gtk_widget_set_sensitive(s->grab_item, on_vga); +#ifdef CONFIG_VTE + gtk_widget_set_sensitive(s->copy_item, vc->type == GD_VC_VTE); +#endif + + gd_update_windowsize(vc); + gd_update_cursor(vc); +} + +static gboolean gd_enter_event(GtkWidget *widget, GdkEventCrossing *crossing, + gpointer opaque) +{ + VirtualConsole *vc = opaque; + GtkDisplayState *s = vc->s; + + if (gd_grab_on_hover(s)) { + gd_grab_keyboard(vc, "grab-on-hover"); + } + return TRUE; +} + +static gboolean gd_leave_event(GtkWidget *widget, GdkEventCrossing *crossing, + gpointer opaque) +{ + VirtualConsole *vc = opaque; + GtkDisplayState *s = vc->s; + + if (gd_grab_on_hover(s)) { + gd_ungrab_keyboard(s); + } + return TRUE; +} + +static gboolean gd_focus_in_event(GtkWidget *widget, + GdkEventFocus *event, gpointer opaque) +{ + VirtualConsole *vc = opaque; + + win32_kbd_set_window(gd_win32_get_hwnd(vc)); + return TRUE; +} + +static gboolean gd_focus_out_event(GtkWidget *widget, + GdkEventFocus *event, gpointer opaque) +{ + VirtualConsole *vc = opaque; + GtkDisplayState *s = vc->s; + + win32_kbd_set_window(NULL); + gtk_release_modifiers(s); + return TRUE; +} + +static gboolean gd_configure(GtkWidget *widget, + GdkEventConfigure *cfg, gpointer opaque) +{ + VirtualConsole *vc = opaque; + + gd_set_ui_info(vc, cfg->width, cfg->height); + return FALSE; +} + +/** Virtual Console Callbacks **/ + +static GSList *gd_vc_menu_init(GtkDisplayState *s, VirtualConsole *vc, + int idx, GSList *group, GtkWidget *view_menu) +{ + vc->menu_item = gtk_radio_menu_item_new_with_mnemonic(group, vc->label); + gtk_accel_group_connect(s->accel_group, GDK_KEY_1 + idx, + HOTKEY_MODIFIERS, 0, + g_cclosure_new_swap(G_CALLBACK(gd_accel_switch_vc), vc, NULL)); + gtk_accel_label_set_accel( + GTK_ACCEL_LABEL(gtk_bin_get_child(GTK_BIN(vc->menu_item))), + GDK_KEY_1 + idx, HOTKEY_MODIFIERS); + + g_signal_connect(vc->menu_item, "activate", + G_CALLBACK(gd_menu_switch_vc), s); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), vc->menu_item); + + return gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(vc->menu_item)); +} + +#if defined(CONFIG_VTE) +static void gd_menu_copy(GtkMenuItem *item, void *opaque) +{ + GtkDisplayState *s = opaque; + VirtualConsole *vc = gd_vc_find_current(s); + +#if VTE_CHECK_VERSION(0, 50, 0) + vte_terminal_copy_clipboard_format(VTE_TERMINAL(vc->vte.terminal), + VTE_FORMAT_TEXT); +#else + vte_terminal_copy_clipboard(VTE_TERMINAL(vc->vte.terminal)); +#endif +} + +static void gd_vc_adjustment_changed(GtkAdjustment *adjustment, void *opaque) +{ + VirtualConsole *vc = opaque; + + if (gtk_adjustment_get_upper(adjustment) > + gtk_adjustment_get_page_size(adjustment)) { + gtk_widget_show(vc->vte.scrollbar); + } else { + gtk_widget_hide(vc->vte.scrollbar); + } +} + +static void gd_vc_send_chars(VirtualConsole *vc) +{ + uint32_t len, avail; + + len = qemu_chr_be_can_write(vc->vte.chr); + avail = fifo8_num_used(&vc->vte.out_fifo); + while (len > 0 && avail > 0) { + const uint8_t *buf; + uint32_t size; + + buf = fifo8_pop_buf(&vc->vte.out_fifo, MIN(len, avail), &size); + qemu_chr_be_write(vc->vte.chr, (uint8_t *)buf, size); + len = qemu_chr_be_can_write(vc->vte.chr); + avail -= size; + } +} + +static int gd_vc_chr_write(Chardev *chr, const uint8_t *buf, int len) +{ + VCChardev *vcd = VC_CHARDEV(chr); + VirtualConsole *vc = vcd->console; + + vte_terminal_feed(VTE_TERMINAL(vc->vte.terminal), (const char *)buf, len); + return len; +} + +static void gd_vc_chr_accept_input(Chardev *chr) +{ + VCChardev *vcd = VC_CHARDEV(chr); + VirtualConsole *vc = vcd->console; + + gd_vc_send_chars(vc); +} + +static void gd_vc_chr_set_echo(Chardev *chr, bool echo) +{ + VCChardev *vcd = VC_CHARDEV(chr); + VirtualConsole *vc = vcd->console; + + if (vc) { + vc->vte.echo = echo; + } else { + vcd->echo = echo; + } +} + +static int nb_vcs; +static Chardev *vcs[MAX_VCS]; +static void gd_vc_open(Chardev *chr, + ChardevBackend *backend, + bool *be_opened, + Error **errp) +{ + if (nb_vcs == MAX_VCS) { + error_setg(errp, "Maximum number of consoles reached"); + return; + } + + vcs[nb_vcs++] = chr; + + /* console/chardev init sometimes completes elsewhere in a 2nd + * stage, so defer OPENED events until they are fully initialized + */ + *be_opened = false; +} + +static void char_gd_vc_class_init(ObjectClass *oc, void *data) +{ + ChardevClass *cc = CHARDEV_CLASS(oc); + + cc->parse = qemu_chr_parse_vc; + cc->open = gd_vc_open; + cc->chr_write = gd_vc_chr_write; + cc->chr_accept_input = gd_vc_chr_accept_input; + cc->chr_set_echo = gd_vc_chr_set_echo; +} + +static const TypeInfo char_gd_vc_type_info = { + .name = TYPE_CHARDEV_VC, + .parent = TYPE_CHARDEV, + .instance_size = sizeof(VCChardev), + .class_init = char_gd_vc_class_init, +}; + +static gboolean gd_vc_in(VteTerminal *terminal, gchar *text, guint size, + gpointer user_data) +{ + VirtualConsole *vc = user_data; + uint32_t free; + + if (vc->vte.echo) { + VteTerminal *term = VTE_TERMINAL(vc->vte.terminal); + int i; + for (i = 0; i < size; i++) { + uint8_t c = text[i]; + if (c >= 128 || isprint(c)) { + /* 8-bit characters are considered printable. */ + vte_terminal_feed(term, &text[i], 1); + } else if (c == '\r' || c == '\n') { + vte_terminal_feed(term, "\r\n", 2); + } else { + char ctrl[2] = { '^', 0}; + ctrl[1] = text[i] ^ 64; + vte_terminal_feed(term, ctrl, 2); + } + } + } + + free = fifo8_num_free(&vc->vte.out_fifo); + fifo8_push_all(&vc->vte.out_fifo, (uint8_t *)text, MIN(free, size)); + gd_vc_send_chars(vc); + + return TRUE; +} + +static GSList *gd_vc_vte_init(GtkDisplayState *s, VirtualConsole *vc, + Chardev *chr, int idx, + GSList *group, GtkWidget *view_menu) +{ + char buffer[32]; + GtkWidget *box; + GtkWidget *scrollbar; + GtkAdjustment *vadjustment; + VCChardev *vcd = VC_CHARDEV(chr); + + vc->s = s; + vc->vte.echo = vcd->echo; + vc->vte.chr = chr; + fifo8_create(&vc->vte.out_fifo, 4096); + vcd->console = vc; + + snprintf(buffer, sizeof(buffer), "vc%d", idx); + vc->label = g_strdup_printf("%s", vc->vte.chr->label + ? vc->vte.chr->label : buffer); + group = gd_vc_menu_init(s, vc, idx, group, view_menu); + + vc->vte.terminal = vte_terminal_new(); + g_signal_connect(vc->vte.terminal, "commit", G_CALLBACK(gd_vc_in), vc); + + /* The documentation says that the default is UTF-8, but actually it is + * 7-bit ASCII at least in VTE 0.38. The function is deprecated since + * VTE 0.54 (only UTF-8 is supported now). */ +#if !VTE_CHECK_VERSION(0, 54, 0) +#if VTE_CHECK_VERSION(0, 38, 0) + vte_terminal_set_encoding(VTE_TERMINAL(vc->vte.terminal), "UTF-8", NULL); +#else + vte_terminal_set_encoding(VTE_TERMINAL(vc->vte.terminal), "UTF-8"); +#endif +#endif + + vte_terminal_set_scrollback_lines(VTE_TERMINAL(vc->vte.terminal), -1); + vte_terminal_set_size(VTE_TERMINAL(vc->vte.terminal), + VC_TERM_X_MIN, VC_TERM_Y_MIN); + +#if VTE_CHECK_VERSION(0, 28, 0) + vadjustment = gtk_scrollable_get_vadjustment + (GTK_SCROLLABLE(vc->vte.terminal)); +#else + vadjustment = vte_terminal_get_adjustment(VTE_TERMINAL(vc->vte.terminal)); +#endif + + box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2); + scrollbar = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL, vadjustment); + + gtk_box_pack_end(GTK_BOX(box), scrollbar, FALSE, FALSE, 0); + gtk_box_pack_end(GTK_BOX(box), vc->vte.terminal, TRUE, TRUE, 0); + + vc->vte.box = box; + vc->vte.scrollbar = scrollbar; + + g_signal_connect(vadjustment, "changed", + G_CALLBACK(gd_vc_adjustment_changed), vc); + + vc->type = GD_VC_VTE; + vc->tab_item = box; + vc->focus = vc->vte.terminal; + gtk_notebook_append_page(GTK_NOTEBOOK(s->notebook), vc->tab_item, + gtk_label_new(vc->label)); + + qemu_chr_be_event(vc->vte.chr, CHR_EVENT_OPENED); + + return group; +} + +static void gd_vcs_init(GtkDisplayState *s, GSList *group, + GtkWidget *view_menu) +{ + int i; + + for (i = 0; i < nb_vcs; i++) { + VirtualConsole *vc = &s->vc[s->nb_vcs]; + group = gd_vc_vte_init(s, vc, vcs[i], s->nb_vcs, group, view_menu); + s->nb_vcs++; + } +} +#endif /* CONFIG_VTE */ + +/** Window Creation **/ + +static void gd_connect_vc_gfx_signals(VirtualConsole *vc) +{ + g_signal_connect(vc->gfx.drawing_area, "draw", + G_CALLBACK(gd_draw_event), vc); +#if defined(CONFIG_OPENGL) + if (gtk_use_gl_area) { + /* wire up GtkGlArea events */ + g_signal_connect(vc->gfx.drawing_area, "render", + G_CALLBACK(gd_render_event), vc); + g_signal_connect(vc->gfx.drawing_area, "resize", + G_CALLBACK(gd_resize_event), vc); + } +#endif + if (qemu_console_is_graphic(vc->gfx.dcl.con)) { + g_signal_connect(vc->gfx.drawing_area, "event", + G_CALLBACK(gd_event), vc); + g_signal_connect(vc->gfx.drawing_area, "button-press-event", + G_CALLBACK(gd_button_event), vc); + g_signal_connect(vc->gfx.drawing_area, "button-release-event", + G_CALLBACK(gd_button_event), vc); + g_signal_connect(vc->gfx.drawing_area, "scroll-event", + G_CALLBACK(gd_scroll_event), vc); + g_signal_connect(vc->gfx.drawing_area, "key-press-event", + G_CALLBACK(gd_key_event), vc); + g_signal_connect(vc->gfx.drawing_area, "key-release-event", + G_CALLBACK(gd_key_event), vc); + + g_signal_connect(vc->gfx.drawing_area, "enter-notify-event", + G_CALLBACK(gd_enter_event), vc); + g_signal_connect(vc->gfx.drawing_area, "leave-notify-event", + G_CALLBACK(gd_leave_event), vc); + g_signal_connect(vc->gfx.drawing_area, "focus-in-event", + G_CALLBACK(gd_focus_in_event), vc); + g_signal_connect(vc->gfx.drawing_area, "focus-out-event", + G_CALLBACK(gd_focus_out_event), vc); + g_signal_connect(vc->gfx.drawing_area, "configure-event", + G_CALLBACK(gd_configure), vc); + g_signal_connect(vc->gfx.drawing_area, "grab-broken-event", + G_CALLBACK(gd_grab_broken_event), vc); + } else { + g_signal_connect(vc->gfx.drawing_area, "key-press-event", + G_CALLBACK(gd_text_key_down), vc); + } +} + +static void gd_connect_signals(GtkDisplayState *s) +{ + g_signal_connect(s->show_tabs_item, "activate", + G_CALLBACK(gd_menu_show_tabs), s); + g_signal_connect(s->untabify_item, "activate", + G_CALLBACK(gd_menu_untabify), s); + g_signal_connect(s->show_menubar_item, "activate", + G_CALLBACK(gd_menu_show_menubar), s); + + g_signal_connect(s->window, "delete-event", + G_CALLBACK(gd_window_close), s); + + g_signal_connect(s->pause_item, "activate", + G_CALLBACK(gd_menu_pause), s); + g_signal_connect(s->reset_item, "activate", + G_CALLBACK(gd_menu_reset), s); + g_signal_connect(s->powerdown_item, "activate", + G_CALLBACK(gd_menu_powerdown), s); + g_signal_connect(s->quit_item, "activate", + G_CALLBACK(gd_menu_quit), s); +#if defined(CONFIG_VTE) + g_signal_connect(s->copy_item, "activate", + G_CALLBACK(gd_menu_copy), s); +#endif + g_signal_connect(s->full_screen_item, "activate", + G_CALLBACK(gd_menu_full_screen), s); + g_signal_connect(s->zoom_in_item, "activate", + G_CALLBACK(gd_menu_zoom_in), s); + g_signal_connect(s->zoom_out_item, "activate", + G_CALLBACK(gd_menu_zoom_out), s); + g_signal_connect(s->zoom_fixed_item, "activate", + G_CALLBACK(gd_menu_zoom_fixed), s); + g_signal_connect(s->zoom_fit_item, "activate", + G_CALLBACK(gd_menu_zoom_fit), s); + g_signal_connect(s->grab_item, "activate", + G_CALLBACK(gd_menu_grab_input), s); + g_signal_connect(s->notebook, "switch-page", + G_CALLBACK(gd_change_page), s); +} + +static GtkWidget *gd_create_menu_machine(GtkDisplayState *s) +{ + GtkWidget *machine_menu; + GtkWidget *separator; + + machine_menu = gtk_menu_new(); + gtk_menu_set_accel_group(GTK_MENU(machine_menu), s->accel_group); + + s->pause_item = gtk_check_menu_item_new_with_mnemonic(_("_Pause")); + gtk_menu_shell_append(GTK_MENU_SHELL(machine_menu), s->pause_item); + + separator = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(machine_menu), separator); + + s->reset_item = gtk_menu_item_new_with_mnemonic(_("_Reset")); + gtk_menu_shell_append(GTK_MENU_SHELL(machine_menu), s->reset_item); + + s->powerdown_item = gtk_menu_item_new_with_mnemonic(_("Power _Down")); + gtk_menu_shell_append(GTK_MENU_SHELL(machine_menu), s->powerdown_item); + + separator = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(machine_menu), separator); + + s->quit_item = gtk_menu_item_new_with_mnemonic(_("_Quit")); + gtk_menu_item_set_accel_path(GTK_MENU_ITEM(s->quit_item), + "<QEMU>/Machine/Quit"); + gtk_accel_map_add_entry("<QEMU>/Machine/Quit", + GDK_KEY_q, HOTKEY_MODIFIERS); + gtk_menu_shell_append(GTK_MENU_SHELL(machine_menu), s->quit_item); + + return machine_menu; +} + +#if defined(CONFIG_OPENGL) +static void gl_area_realize(GtkGLArea *area, VirtualConsole *vc) +{ + gtk_gl_area_make_current(area); + qemu_egl_display = eglGetCurrentDisplay(); + vc->gfx.has_dmabuf = qemu_egl_has_dmabuf(); + if (!vc->gfx.has_dmabuf) { + error_report("GtkGLArea console lacks DMABUF support."); + } +} +#endif + +static GSList *gd_vc_gfx_init(GtkDisplayState *s, VirtualConsole *vc, + QemuConsole *con, int idx, + GSList *group, GtkWidget *view_menu) +{ + bool zoom_to_fit = false; + + vc->label = qemu_console_get_label(con); + vc->s = s; + vc->gfx.scale_x = 1.0; + vc->gfx.scale_y = 1.0; + +#if defined(CONFIG_OPENGL) + if (display_opengl) { + if (gtk_use_gl_area) { + vc->gfx.drawing_area = gtk_gl_area_new(); + g_signal_connect(vc->gfx.drawing_area, "realize", + G_CALLBACK(gl_area_realize), vc); + vc->gfx.dcl.ops = &dcl_gl_area_ops; + } else { +#ifdef CONFIG_X11 + vc->gfx.drawing_area = gtk_drawing_area_new(); + /* + * gtk_widget_set_double_buffered() was deprecated in 3.14. + * It is required for opengl rendering on X11 though. A + * proper replacement (native opengl support) is only + * available in 3.16+. Silence the warning if possible. + */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + gtk_widget_set_double_buffered(vc->gfx.drawing_area, FALSE); +#pragma GCC diagnostic pop + vc->gfx.dcl.ops = &dcl_egl_ops; + vc->gfx.has_dmabuf = qemu_egl_has_dmabuf(); +#else + abort(); +#endif + } + } else +#endif + { + vc->gfx.drawing_area = gtk_drawing_area_new(); + vc->gfx.dcl.ops = &dcl_ops; + } + + + gtk_widget_add_events(vc->gfx.drawing_area, + GDK_POINTER_MOTION_MASK | + GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_BUTTON_MOTION_MASK | + GDK_ENTER_NOTIFY_MASK | + GDK_LEAVE_NOTIFY_MASK | + GDK_SCROLL_MASK | + GDK_SMOOTH_SCROLL_MASK | + GDK_KEY_PRESS_MASK); + gtk_widget_set_can_focus(vc->gfx.drawing_area, TRUE); + + vc->type = GD_VC_GFX; + vc->tab_item = vc->gfx.drawing_area; + vc->focus = vc->gfx.drawing_area; + gtk_notebook_append_page(GTK_NOTEBOOK(s->notebook), + vc->tab_item, gtk_label_new(vc->label)); + + vc->gfx.kbd = qkbd_state_init(con); + vc->gfx.dcl.con = con; + + register_displaychangelistener(&vc->gfx.dcl); + + gd_connect_vc_gfx_signals(vc); + group = gd_vc_menu_init(s, vc, idx, group, view_menu); + + if (dpy_ui_info_supported(vc->gfx.dcl.con)) { + zoom_to_fit = true; + } + if (s->opts->u.gtk.has_zoom_to_fit) { + zoom_to_fit = s->opts->u.gtk.zoom_to_fit; + } + if (zoom_to_fit) { + gtk_menu_item_activate(GTK_MENU_ITEM(s->zoom_fit_item)); + s->free_scale = true; + } + + return group; +} + +static GtkWidget *gd_create_menu_view(GtkDisplayState *s) +{ + GSList *group = NULL; + GtkWidget *view_menu; + GtkWidget *separator; + QemuConsole *con; + int vc; + + view_menu = gtk_menu_new(); + gtk_menu_set_accel_group(GTK_MENU(view_menu), s->accel_group); + + s->full_screen_item = gtk_menu_item_new_with_mnemonic(_("_Fullscreen")); + +#if defined(CONFIG_VTE) + s->copy_item = gtk_menu_item_new_with_mnemonic(_("_Copy")); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->copy_item); +#endif + + gtk_accel_group_connect(s->accel_group, GDK_KEY_f, HOTKEY_MODIFIERS, 0, + g_cclosure_new_swap(G_CALLBACK(gd_accel_full_screen), s, NULL)); + gtk_accel_label_set_accel( + GTK_ACCEL_LABEL(gtk_bin_get_child(GTK_BIN(s->full_screen_item))), + GDK_KEY_f, HOTKEY_MODIFIERS); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->full_screen_item); + + separator = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), separator); + + s->zoom_in_item = gtk_menu_item_new_with_mnemonic(_("Zoom _In")); + gtk_menu_item_set_accel_path(GTK_MENU_ITEM(s->zoom_in_item), + "<QEMU>/View/Zoom In"); + gtk_accel_map_add_entry("<QEMU>/View/Zoom In", GDK_KEY_plus, + HOTKEY_MODIFIERS); + gtk_accel_group_connect(s->accel_group, GDK_KEY_equal, HOTKEY_MODIFIERS, 0, + g_cclosure_new_swap(G_CALLBACK(gd_accel_zoom_in), s, NULL)); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->zoom_in_item); + + s->zoom_out_item = gtk_menu_item_new_with_mnemonic(_("Zoom _Out")); + gtk_menu_item_set_accel_path(GTK_MENU_ITEM(s->zoom_out_item), + "<QEMU>/View/Zoom Out"); + gtk_accel_map_add_entry("<QEMU>/View/Zoom Out", GDK_KEY_minus, + HOTKEY_MODIFIERS); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->zoom_out_item); + + s->zoom_fixed_item = gtk_menu_item_new_with_mnemonic(_("Best _Fit")); + gtk_menu_item_set_accel_path(GTK_MENU_ITEM(s->zoom_fixed_item), + "<QEMU>/View/Zoom Fixed"); + gtk_accel_map_add_entry("<QEMU>/View/Zoom Fixed", GDK_KEY_0, + HOTKEY_MODIFIERS); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->zoom_fixed_item); + + s->zoom_fit_item = gtk_check_menu_item_new_with_mnemonic(_("Zoom To _Fit")); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->zoom_fit_item); + + separator = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), separator); + + s->grab_on_hover_item = gtk_check_menu_item_new_with_mnemonic(_("Grab On _Hover")); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->grab_on_hover_item); + + s->grab_item = gtk_check_menu_item_new_with_mnemonic(_("_Grab Input")); + gtk_menu_item_set_accel_path(GTK_MENU_ITEM(s->grab_item), + "<QEMU>/View/Grab Input"); + gtk_accel_map_add_entry("<QEMU>/View/Grab Input", GDK_KEY_g, + HOTKEY_MODIFIERS); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->grab_item); + + separator = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), separator); + + /* gfx */ + for (vc = 0;; vc++) { + con = qemu_console_lookup_by_index(vc); + if (!con) { + break; + } + group = gd_vc_gfx_init(s, &s->vc[vc], con, + vc, group, view_menu); + s->nb_vcs++; + } + +#if defined(CONFIG_VTE) + /* vte */ + gd_vcs_init(s, group, view_menu); +#endif + + separator = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), separator); + + s->show_tabs_item = gtk_check_menu_item_new_with_mnemonic(_("Show _Tabs")); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->show_tabs_item); + + s->untabify_item = gtk_menu_item_new_with_mnemonic(_("Detach Tab")); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->untabify_item); + + s->show_menubar_item = gtk_check_menu_item_new_with_mnemonic( + _("Show Menubar")); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(s->show_menubar_item), + TRUE); + gtk_accel_group_connect(s->accel_group, GDK_KEY_m, HOTKEY_MODIFIERS, 0, + g_cclosure_new_swap(G_CALLBACK(gd_accel_show_menubar), s, NULL)); + gtk_accel_label_set_accel( + GTK_ACCEL_LABEL(gtk_bin_get_child(GTK_BIN(s->show_menubar_item))), + GDK_KEY_m, HOTKEY_MODIFIERS); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->show_menubar_item); + + return view_menu; +} + +static void gd_create_menus(GtkDisplayState *s) +{ + GtkSettings *settings; + + s->accel_group = gtk_accel_group_new(); + s->machine_menu = gd_create_menu_machine(s); + s->view_menu = gd_create_menu_view(s); + + s->machine_menu_item = gtk_menu_item_new_with_mnemonic(_("_Machine")); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(s->machine_menu_item), + s->machine_menu); + gtk_menu_shell_append(GTK_MENU_SHELL(s->menu_bar), s->machine_menu_item); + + s->view_menu_item = gtk_menu_item_new_with_mnemonic(_("_View")); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(s->view_menu_item), s->view_menu); + gtk_menu_shell_append(GTK_MENU_SHELL(s->menu_bar), s->view_menu_item); + + g_object_set_data(G_OBJECT(s->window), "accel_group", s->accel_group); + gtk_window_add_accel_group(GTK_WINDOW(s->window), s->accel_group); + + /* Disable the default "F10" menu shortcut. */ + settings = gtk_widget_get_settings(s->window); + g_object_set(G_OBJECT(settings), "gtk-menu-bar-accel", "", NULL); +} + + +static gboolean gtkinit; + +static void gtk_display_init(DisplayState *ds, DisplayOptions *opts) +{ + VirtualConsole *vc; + + GtkDisplayState *s = g_malloc0(sizeof(*s)); + GdkDisplay *window_display; + GtkIconTheme *theme; + char *dir; + + if (!gtkinit) { + fprintf(stderr, "gtk initialization failed\n"); + exit(1); + } + assert(opts->type == DISPLAY_TYPE_GTK); + s->opts = opts; + + theme = gtk_icon_theme_get_default(); + dir = get_relocated_path(CONFIG_QEMU_ICONDIR); + gtk_icon_theme_prepend_search_path(theme, dir); + g_free(dir); + g_set_prgname("qemu"); + + s->window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + s->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + s->notebook = gtk_notebook_new(); + s->menu_bar = gtk_menu_bar_new(); + + s->free_scale = FALSE; + + /* Mostly LC_MESSAGES only. See early_gtk_display_init() for details. For + * LC_CTYPE, we need to make sure that non-ASCII characters are considered + * printable, but without changing any of the character classes to make + * sure that we don't accidentally break implicit assumptions. */ + setlocale(LC_MESSAGES, ""); + setlocale(LC_CTYPE, "C.UTF-8"); + dir = get_relocated_path(CONFIG_QEMU_LOCALEDIR); + bindtextdomain("qemu", dir); + g_free(dir); + bind_textdomain_codeset("qemu", "UTF-8"); + textdomain("qemu"); + + window_display = gtk_widget_get_display(s->window); + if (s->opts->has_show_cursor && s->opts->show_cursor) { + s->null_cursor = NULL; /* default pointer */ + } else { + s->null_cursor = gdk_cursor_new_for_display(window_display, + GDK_BLANK_CURSOR); + } + + s->mouse_mode_notifier.notify = gd_mouse_mode_change; + qemu_add_mouse_mode_change_notifier(&s->mouse_mode_notifier); + qemu_add_vm_change_state_handler(gd_change_runstate, s); + + gtk_window_set_icon_name(GTK_WINDOW(s->window), "qemu"); + + gd_create_menus(s); + + gd_connect_signals(s); + + gtk_notebook_set_show_tabs(GTK_NOTEBOOK(s->notebook), FALSE); + gtk_notebook_set_show_border(GTK_NOTEBOOK(s->notebook), FALSE); + + gd_update_caption(s); + + gtk_box_pack_start(GTK_BOX(s->vbox), s->menu_bar, FALSE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(s->vbox), s->notebook, TRUE, TRUE, 0); + + gtk_container_add(GTK_CONTAINER(s->window), s->vbox); + + gtk_widget_show_all(s->window); + + vc = gd_vc_find_current(s); + gtk_widget_set_sensitive(s->view_menu, vc != NULL); +#ifdef CONFIG_VTE + gtk_widget_set_sensitive(s->copy_item, + vc && vc->type == GD_VC_VTE); +#endif + + if (opts->has_full_screen && + opts->full_screen) { + gtk_menu_item_activate(GTK_MENU_ITEM(s->full_screen_item)); + } + if (opts->u.gtk.has_grab_on_hover && + opts->u.gtk.grab_on_hover) { + gtk_menu_item_activate(GTK_MENU_ITEM(s->grab_on_hover_item)); + } + gd_clipboard_init(s); +} + +static void early_gtk_display_init(DisplayOptions *opts) +{ + /* The QEMU code relies on the assumption that it's always run in + * the C locale. Therefore it is not prepared to deal with + * operations that produce different results depending on the + * locale, such as printf's formatting of decimal numbers, and + * possibly others. + * + * Since GTK+ calls setlocale() by default -importing the locale + * settings from the environment- we must prevent it from doing so + * using gtk_disable_setlocale(). + * + * QEMU's GTK+ UI, however, _does_ have translations for some of + * the menu items. As a trade-off between a functionally correct + * QEMU and a fully internationalized UI we support importing + * LC_MESSAGES from the environment (see the setlocale() call + * earlier in this file). This allows us to display translated + * messages leaving everything else untouched. + */ + gtk_disable_setlocale(); + gtkinit = gtk_init_check(NULL, NULL); + if (!gtkinit) { + /* don't exit yet, that'll break -help */ + return; + } + + assert(opts->type == DISPLAY_TYPE_GTK); + if (opts->has_gl && opts->gl != DISPLAYGL_MODE_OFF) { +#if defined(CONFIG_OPENGL) +#if defined(GDK_WINDOWING_WAYLAND) + if (GDK_IS_WAYLAND_DISPLAY(gdk_display_get_default())) { + gtk_use_gl_area = true; + gtk_gl_area_init(); + } else +#endif + { +#ifdef CONFIG_X11 + DisplayGLMode mode = opts->has_gl ? opts->gl : DISPLAYGL_MODE_ON; + gtk_egl_init(mode); +#endif + } +#endif + } + + keycode_map = gd_get_keymap(&keycode_maplen); + +#if defined(CONFIG_VTE) + type_register(&char_gd_vc_type_info); +#endif +} + +static QemuDisplay qemu_display_gtk = { + .type = DISPLAY_TYPE_GTK, + .early_init = early_gtk_display_init, + .init = gtk_display_init, +}; + +static void register_gtk(void) +{ + qemu_display_register(&qemu_display_gtk); +} + +type_init(register_gtk); + +#ifdef CONFIG_OPENGL +module_dep("ui-opengl"); +#endif diff --git a/ui/icons/Makefile b/ui/icons/Makefile new file mode 100644 index 000000000..20bd64ccc --- /dev/null +++ b/ui/icons/Makefile @@ -0,0 +1,13 @@ + +# Regenerate bitmaps from the SVG using inkscape CLI export +# and ImageMagick. Don't use ImageMagick for the initial +# SVG conversion, since it merely calls inkscape, but uses +# 96 DPI res resulting in poor quality output. + +regenerate: + for s in 16 24 32 48 64 128 256 512; \ + do \ + inkscape --without-gui --export-png=qemu_$${s}x$${s}.png \ + --export-width=$$s --export-height=$$s qemu.svg ; \ + done + convert qemu_32x32.png qemu_32x32.bmp diff --git a/ui/icons/meson.build b/ui/icons/meson.build new file mode 100644 index 000000000..12c52080e --- /dev/null +++ b/ui/icons/meson.build @@ -0,0 +1,13 @@ +foreach s: [16, 24, 32, 48, 64, 128, 256, 512] + s = '@0@x@0@'.format(s.to_string()) + install_data('qemu_@0@.png'.format(s), + rename: 'qemu.png', + install_dir: qemu_icondir / 'hicolor' / s / 'apps') +endforeach + +install_data('qemu_32x32.bmp', + rename: 'qemu.bmp', + install_dir: qemu_icondir / 'hicolor' / '32x32' / 'apps') + +install_data('qemu.svg', + install_dir: qemu_icondir / 'hicolor' / 'scalable' / 'apps') diff --git a/ui/icons/qemu.svg b/ui/icons/qemu.svg new file mode 100644 index 000000000..24ca23a1e --- /dev/null +++ b/ui/icons/qemu.svg @@ -0,0 +1,976 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="111.71874" + height="111.12498" + id="svg2" + version="1.1" + inkscape:version="0.48.2 r9819" + sodipodi:docname="qemu_logo_no_text.svg"> + <defs + id="defs4"> + <linearGradient + id="linearGradient4686"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop4688" /> + <stop + id="stop3956" + offset="0.75" + style="stop-color:#000000;stop-opacity:0.87843138;" /> + <stop + style="stop-color:#ffffff;stop-opacity:0.43921569;" + offset="0.75" + id="stop3958" /> + <stop + id="stop3960" + offset="0.88" + style="stop-color:#181818;stop-opacity:1;" /> + <stop + style="stop-color:#242424;stop-opacity:1;" + offset="0.88" + id="stop3962" /> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="1" + id="stop4690" /> + </linearGradient> + <linearGradient + id="linearGradient4467"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop4469" /> + <stop + style="stop-color:#000000;stop-opacity:0.8974359;" + offset="1" + id="stop4471" /> + </linearGradient> + <linearGradient + id="linearGradient4431"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop4433" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop4435" /> + </linearGradient> + <linearGradient + id="linearGradient4466"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop4468" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop4470" /> + </linearGradient> + <linearGradient + id="linearGradient4321"> + <stop + style="stop-color:#ff6702;stop-opacity:1;" + offset="0" + id="stop4323" /> + <stop + style="stop-color:#ff9a55;stop-opacity:1;" + offset="1" + id="stop4325" /> + </linearGradient> + <linearGradient + id="linearGradient4283"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop4285" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop4287" /> + </linearGradient> + <linearGradient + id="linearGradient4251"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop4253" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop4255" /> + </linearGradient> + <linearGradient + id="linearGradient4007"> + <stop + style="stop-color:#ff6600;stop-opacity:1;" + offset="0" + id="stop4009" /> + <stop + style="stop-color:#ff9148;stop-opacity:1;" + offset="1" + id="stop4011" /> + </linearGradient> + <linearGradient + id="linearGradient3999"> + <stop + style="stop-color:#fff7f2;stop-opacity:1;" + offset="0" + id="stop4001" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop4003" /> + </linearGradient> + <linearGradient + id="linearGradient3890"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop3892" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop3894" /> + </linearGradient> + <linearGradient + id="linearGradient3880"> + <stop + style="stop-color:#eb7400;stop-opacity:1;" + offset="0" + id="stop3882" /> + <stop + style="stop-color:#f7b06a;stop-opacity:1;" + offset="1" + id="stop3884" /> + </linearGradient> + <linearGradient + id="linearGradient4011"> + <stop + style="stop-color:#042dc8;stop-opacity:1;" + offset="0" + id="stop4013" /> + <stop + style="stop-color:#4260d5;stop-opacity:1;" + offset="1" + id="stop4015" /> + </linearGradient> + <linearGradient + id="linearGradient3879"> + <stop + style="stop-color:#ffffff;stop-opacity:0.90598291;" + offset="0" + id="stop3881" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop3883" /> + </linearGradient> + <linearGradient + id="linearGradient3869"> + <stop + style="stop-color:#c95000;stop-opacity:1;" + offset="0" + id="stop3871" /> + <stop + style="stop-color:#ff9e5e;stop-opacity:1;" + offset="1" + id="stop3873" /> + </linearGradient> + <linearGradient + id="linearGradient3861"> + <stop + style="stop-color:#f06000;stop-opacity:1;" + offset="0" + id="stop3863" /> + <stop + style="stop-color:#ffccaa;stop-opacity:1;" + offset="1" + id="stop3865" /> + </linearGradient> + <linearGradient + id="linearGradient3826"> + <stop + style="stop-color:#ff6600;stop-opacity:1;" + offset="0" + id="stop3828" /> + <stop + style="stop-color:#ff893b;stop-opacity:1;" + offset="1" + id="stop3830" /> + </linearGradient> + <linearGradient + id="linearGradient3879-6"> + <stop + style="stop-color:#ffffff;stop-opacity:0.90598291;" + offset="0" + id="stop3881-4" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop3883-7" /> + </linearGradient> + <linearGradient + id="linearGradient3869-5"> + <stop + style="stop-color:#c95000;stop-opacity:1;" + offset="0" + id="stop3871-9" /> + <stop + style="stop-color:#ff9e5e;stop-opacity:1;" + offset="1" + id="stop3873-4" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3879-4" + id="linearGradient3885-6" + x1="76.025352" + y1="124.8497" + x2="75.874107" + y2="143.03978" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3879-4"> + <stop + style="stop-color:#ffffff;stop-opacity:0.93162394;" + offset="0" + id="stop3881-6" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop3883-74" /> + </linearGradient> + <linearGradient + id="linearGradient3869-2"> + <stop + style="stop-color:#c95000;stop-opacity:1;" + offset="0" + id="stop3871-99" /> + <stop + style="stop-color:#ff9e5e;stop-opacity:1;" + offset="1" + id="stop3873-6" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4011" + id="radialGradient4017" + cx="66.639" + cy="93.096375" + fx="66.639" + fy="93.096375" + r="11.515625" + gradientTransform="matrix(0.23244854,1.600893,-1.0124495,0.14700695,145.40424,-26.300303)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3879-4-7" + id="linearGradient3885-6-2" + x1="76.025352" + y1="124.8497" + x2="75.874107" + y2="143.03978" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3879-4-7"> + <stop + style="stop-color:#ffffff;stop-opacity:0.93162394;" + offset="0" + id="stop3881-6-7" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop3883-74-6" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4011-5" + id="radialGradient4017-7" + cx="66.639" + cy="93.096375" + fx="66.639" + fy="93.096375" + r="11.515625" + gradientTransform="matrix(0.99779178,6.8718773,-4.3459674,0.6310314,452.75975,-225.98471)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient4011-5"> + <stop + style="stop-color:#042dc8;stop-opacity:1;" + offset="0" + id="stop4013-1" /> + <stop + style="stop-color:#4260d5;stop-opacity:1;" + offset="1" + id="stop4015-3" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3879-4-75" + id="linearGradient3885-6-8" + x1="76.025352" + y1="124.8497" + x2="75.874107" + y2="143.03978" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3879-4-75"> + <stop + style="stop-color:#ffffff;stop-opacity:0.93162394;" + offset="0" + id="stop3881-6-1" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop3883-74-4" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4011-0" + id="radialGradient4017-5" + cx="66.639" + cy="93.096375" + fx="66.639" + fy="93.096375" + r="11.515625" + gradientTransform="matrix(0.23244854,1.600893,-1.0124495,0.14700695,146.34996,53.681728)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient4011-0"> + <stop + style="stop-color:#042dc8;stop-opacity:1;" + offset="0" + id="stop4013-4" /> + <stop + style="stop-color:#4260d5;stop-opacity:1;" + offset="1" + id="stop4015-0" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4011-0" + id="linearGradient4117" + x1="107.03001" + y1="189.72537" + x2="107.18476" + y2="173.47537" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3879-4-7-2" + id="linearGradient3885-6-2-8" + x1="76.025352" + y1="124.8497" + x2="75.874107" + y2="143.03978" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3879-4-7-2"> + <stop + style="stop-color:#ffffff;stop-opacity:0.93162394;" + offset="0" + id="stop3881-6-7-9" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop3883-74-6-9" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4011-5-1" + id="radialGradient4017-7-9" + cx="66.639" + cy="93.096375" + fx="66.639" + fy="93.096375" + r="11.515625" + gradientTransform="matrix(0.99779178,6.8718773,-4.3459674,0.6310314,448.94742,-406.99277)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient4011-5-1"> + <stop + style="stop-color:#042dc8;stop-opacity:1;" + offset="0" + id="stop4013-1-9" /> + <stop + style="stop-color:#4260d5;stop-opacity:1;" + offset="1" + id="stop4015-3-8" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3879-4-7-2-7" + id="linearGradient3885-6-2-8-0" + x1="76.025352" + y1="124.8497" + x2="75.874107" + y2="143.03978" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3879-4-7-2-7"> + <stop + style="stop-color:#ffffff;stop-opacity:0.93162394;" + offset="0" + id="stop3881-6-7-9-3" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop3883-74-6-9-6" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4011-5-1-5" + id="radialGradient4017-7-9-5" + cx="66.639" + cy="93.096375" + fx="66.639" + fy="93.096375" + r="11.515625" + gradientTransform="matrix(0.55965334,3.8543806,-2.4376181,0.3539404,454.75182,-145.44353)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient4011-5-1-5"> + <stop + style="stop-color:#042dc8;stop-opacity:1;" + offset="0" + id="stop4013-1-9-6" /> + <stop + style="stop-color:#4260d5;stop-opacity:1;" + offset="1" + id="stop4015-3-8-9" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3879-4-7-2-4" + id="linearGradient3885-6-2-8-4" + x1="76.025352" + y1="124.8497" + x2="75.874107" + y2="143.03978" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3879-4-7-2-4"> + <stop + style="stop-color:#ffffff;stop-opacity:0.93162394;" + offset="0" + id="stop3881-6-7-9-9" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop3883-74-6-9-3" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4011-5-1-7" + id="radialGradient4017-7-9-7" + cx="66.639" + cy="93.096375" + fx="66.639" + fy="93.096375" + r="11.515625" + gradientTransform="matrix(0.26837158,1.8482981,-1.1689154,0.16972569,466.57614,26.180822)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient4011-5-1-7"> + <stop + style="stop-color:#042dc8;stop-opacity:1;" + offset="0" + id="stop4013-1-9-1" /> + <stop + style="stop-color:#4260d5;stop-opacity:1;" + offset="1" + id="stop4015-3-8-5" /> + </linearGradient> + <linearGradient + id="linearGradient3879-4-7-2-0"> + <stop + style="stop-color:#ffffff;stop-opacity:0.93162394;" + offset="0" + id="stop3881-6-7-9-7" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop3883-74-6-9-8" /> + </linearGradient> + <linearGradient + id="linearGradient4011-5-1-55"> + <stop + style="stop-color:#000a30;stop-opacity:1;" + offset="0" + id="stop4013-1-9-8" /> + <stop + style="stop-color:#4260d5;stop-opacity:1;" + offset="1" + id="stop4015-3-8-3" /> + </linearGradient> + <linearGradient + id="linearGradient3890-9"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop3892-0" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop3894-9" /> + </linearGradient> + <linearGradient + id="linearGradient3880-4"> + <stop + style="stop-color:#eb7400;stop-opacity:1;" + offset="0" + id="stop3882-5" /> + <stop + style="stop-color:#f7b06a;stop-opacity:1;" + offset="1" + id="stop3884-1" /> + </linearGradient> + <linearGradient + id="linearGradient3999-7"> + <stop + style="stop-color:#fff7f2;stop-opacity:1;" + offset="0" + id="stop4001-9" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop4003-4" /> + </linearGradient> + <linearGradient + id="linearGradient4007-9"> + <stop + style="stop-color:#ff6600;stop-opacity:1;" + offset="0" + id="stop4009-1" /> + <stop + style="stop-color:#ff9148;stop-opacity:1;" + offset="1" + id="stop4011-9" /> + </linearGradient> + <linearGradient + id="linearGradient4007-9-5"> + <stop + style="stop-color:#ff6600;stop-opacity:1;" + offset="0" + id="stop4009-1-9" /> + <stop + style="stop-color:#ff9148;stop-opacity:1;" + offset="1" + id="stop4011-9-5" /> + </linearGradient> + <linearGradient + id="linearGradient3999-7-1"> + <stop + style="stop-color:#fff7f2;stop-opacity:1;" + offset="0" + id="stop4001-9-1" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop4003-4-4" /> + </linearGradient> + <linearGradient + id="linearGradient4007-9-5-3"> + <stop + style="stop-color:#ff6600;stop-opacity:1;" + offset="0" + id="stop4009-1-9-3" /> + <stop + style="stop-color:#ff9148;stop-opacity:1;" + offset="1" + id="stop4011-9-5-9" /> + </linearGradient> + <linearGradient + id="linearGradient3999-7-1-4"> + <stop + style="stop-color:#fff7f2;stop-opacity:1;" + offset="0" + id="stop4001-9-1-4" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop4003-4-4-4" /> + </linearGradient> + <linearGradient + id="linearGradient3879-4-7-2-3"> + <stop + style="stop-color:#ffffff;stop-opacity:0.93162394;" + offset="0" + id="stop3881-6-7-9-1" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop3883-74-6-9-87" /> + </linearGradient> + <linearGradient + id="linearGradient4011-5-1-1"> + <stop + style="stop-color:#fde8a1;stop-opacity:1;" + offset="0" + id="stop4013-1-9-63" /> + <stop + style="stop-color:#2947b9;stop-opacity:1;" + offset="1" + id="stop4015-3-8-8" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4466" + id="linearGradient4472" + x1="161.7561" + y1="540.72662" + x2="161.7561" + y2="579.80206" + gradientUnits="userSpaceOnUse" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4321" + id="radialGradient4474" + cx="130.8242" + cy="575.27838" + fx="130.8242" + fy="575.27838" + r="49.498173" + gradientTransform="matrix(0.95670828,0.96684666,-0.72623533,0.71862001,423.45109,35.05138)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4466-5" + id="linearGradient4472-9" + x1="161.7561" + y1="540.72662" + x2="161.7561" + y2="579.80206" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient4466-5"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop4468-2" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop4470-3" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4321-0" + id="radialGradient4474-6" + cx="130.8242" + cy="575.27838" + fx="130.8242" + fy="575.27838" + r="49.498173" + gradientTransform="matrix(0.95670828,0.96684666,-0.72623533,0.71862001,442.64399,170.9169)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient4321-0"> + <stop + style="stop-color:#ff6702;stop-opacity:1;" + offset="0" + id="stop4323-3" /> + <stop + style="stop-color:#ff9a55;stop-opacity:1;" + offset="1" + id="stop4325-1" /> + </linearGradient> + <linearGradient + id="linearGradient4466-5-5"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop4468-2-9" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop4470-3-4" /> + </linearGradient> + <linearGradient + id="linearGradient4321-0-0"> + <stop + style="stop-color:#ff6702;stop-opacity:1;" + offset="0" + id="stop4323-3-9" /> + <stop + style="stop-color:#ff9a55;stop-opacity:1;" + offset="1" + id="stop4325-1-1" /> + </linearGradient> + <linearGradient + id="linearGradient4466-5-9"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop4468-2-7" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop4470-3-7" /> + </linearGradient> + <linearGradient + id="linearGradient4321-0-7"> + <stop + style="stop-color:#ff6702;stop-opacity:1;" + offset="0" + id="stop4323-3-3" /> + <stop + style="stop-color:#ff9a55;stop-opacity:1;" + offset="1" + id="stop4325-1-6" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4431" + id="linearGradient4437" + x1="142.81854" + y1="831.52283" + x2="142.81854" + y2="878.90735" + gradientUnits="userSpaceOnUse" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4467" + id="radialGradient4475" + cx="116.51958" + cy="98.282051" + fx="116.51958" + fy="98.282051" + r="55.859375" + gradientTransform="matrix(0.97442557,1.5088911,-0.83559154,0.53961599,79.641615,-130.28522)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4431-3" + id="linearGradient4437-6" + x1="142.81854" + y1="831.52283" + x2="142.81854" + y2="878.90735" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient4431-3"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop4433-0" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop4435-2" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4467-7" + id="radialGradient4475-0" + cx="116.51958" + cy="98.282051" + fx="116.51958" + fy="98.282051" + r="55.859375" + gradientTransform="matrix(0.97442557,1.5088911,-0.83559154,0.53961599,225.10358,63.664066)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient4467-7"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop4469-4" /> + <stop + style="stop-color:#000000;stop-opacity:0.8974359;" + offset="1" + id="stop4471-7" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4467" + id="radialGradient3262" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.97442557,1.5088911,-0.83559154,0.53961599,59.641615,-150.28522)" + cx="116.51958" + cy="98.282051" + fx="116.51958" + fy="98.282051" + r="55.859375" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4431" + id="linearGradient3264" + gradientUnits="userSpaceOnUse" + x1="142.81854" + y1="831.52283" + x2="142.81854" + y2="878.90735" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="5.6" + inkscape:cx="31.144191" + inkscape:cy="38.335716" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + showguides="false" + inkscape:guide-bbox="true" + inkscape:window-width="1920" + inkscape:window-height="1056" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + fit-margin-top="0" + fit-margin-right="0" + fit-margin-bottom="0" + fit-margin-left="0"> + <sodipodi:guide + orientation="0,1" + position="72.563745,37.346999" + id="guide2989" /> + <sodipodi:guide + orientation="0,1" + position="74.584055,7.2949693" + id="guide2991" /> + <sodipodi:guide + orientation="1,0" + position="71.048515,20.426949" + id="guide2993" /> + <sodipodi:guide + orientation="1,0" + position="97.817565,20.174409" + id="guide2995" /> + <sodipodi:guide + orientation="1,0" + position="71.048515,20.426949" + id="guide3017" /> + <inkscape:grid + type="xygrid" + id="grid3019" + empspacing="5" + visible="true" + enabled="true" + snapvisiblegridlinesonly="true" /> + <sodipodi:guide + orientation="1,0" + position="105.6589,-12.377861" + id="guide3021" /> + <sodipodi:guide + orientation="1,0" + position="126.6589,-16.377861" + id="guide3023" /> + <sodipodi:guide + orientation="0,1" + position="110.6589,-3.3778607" + id="guide3025" /> + <sodipodi:guide + orientation="0,1" + position="110.6589,-27.377861" + id="guide3027" /> + <sodipodi:guide + orientation="0,1" + position="19.658895,-35.37786" + id="guide3810" /> + <sodipodi:guide + orientation="0,1" + position="21.658895,-70.377861" + id="guide3814" /> + <sodipodi:guide + orientation="0,1" + position="2.301752,-9.4850007" + id="guide3856" /> + <sodipodi:guide + orientation="0,1" + position="26.601806,-9.4850007" + id="guide3887" /> + <sodipodi:guide + orientation="0,1" + position="44.658283,37.346999" + id="guide4019" /> + <sodipodi:guide + orientation="0,1" + position="126.6589,-27.377861" + id="guide4481" /> + <sodipodi:guide + orientation="0,1" + position="159.08747,-213.94929" + id="guide4483" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-42.341105,-35.859333)"> + <path + inkscape:connector-curvature="0" + style="fill:url(#radialGradient3262);fill-opacity:1;stroke:none" + d="m 98.2161,35.859333 c -30.850815,0 -55.874995,24.87043 -55.874995,55.562497 0,30.69207 25.02418,55.56249 55.874995,55.56249 10.09496,0 19.54625,-2.6525 27.71875,-7.3125 l 2.90625,7.3125 2.40625,0 20,0 0.125,0 -8.8125,-21.78124 c 7.21537,-9.3622 11.5,-21.07236 11.5,-33.78125 0,-30.692067 -24.99293,-55.562497 -55.84375,-55.562497 z" + id="path3834-7-7-2-5-5-0-5-4" /> + <path + sodipodi:type="arc" + style="fill:url(#linearGradient3264);fill-opacity:1;stroke:none" + id="path3661" + sodipodi:cx="142.5" + sodipodi:cy="856.29077" + sodipodi:rx="35.357143" + sodipodi:ry="24.642857" + d="m 177.85714,856.29077 c 0,13.60988 -15.82993,24.64286 -35.35714,24.64286 -19.52721,0 -35.35714,-11.03298 -35.35714,-24.64286 0,-13.60987 15.82993,-24.64286 35.35714,-24.64286 19.52721,0 35.35714,11.03299 35.35714,24.64286 z" + transform="matrix(1.0465082,0,0,1.2920463,-51.641235,-1036.8612)" /> + <path + sodipodi:type="arc" + style="fill:#000000;fill-opacity:1;stroke:none" + id="path4442" + sodipodi:cx="115.66247" + sodipodi:cy="856.39258" + sodipodi:rx="6.5659914" + sodipodi:ry="6.5659914" + d="m 122.22846,856.39258 c 0,3.6263 -2.9397,6.56599 -6.56599,6.56599 -3.6263,0 -6.56599,-2.93969 -6.56599,-6.56599 0,-3.6263 2.93969,-6.56599 6.56599,-6.56599 3.62629,0 6.56599,2.93969 6.56599,6.56599 z" + transform="translate(-12.329975,-797.60351)" /> + <rect + style="fill:#000000;fill-opacity:1;stroke:none" + id="rect4444" + width="37.643608" + height="5.5005069" + x="101.55376" + y="48.297417" + transform="matrix(0.98974903,0.14281759,-0.18972639,0.981837,0,0)" /> + <rect + style="fill:#000000;fill-opacity:1;stroke:none" + id="rect4446" + width="6.5659914" + height="2.9041886" + x="124.92451" + y="69.016899" /> + <path + style="fill:#ff6600;fill-opacity:1" + d="m 83.38797,45.010543 c -0.057,2.18531 -3.865755,0.28296 -4.031245,2.78125 -4.22387,-1.88052 0.32884,2.87188 -0.0937,3.3125 l -0.0312,0 -0.3125,-0.0312 c -0.20386,-0.0728 -0.49977,-0.19904 -0.9375,-0.46875 -2.9499,2.35025 -3.02157,7.23369 -6.0625,9.9375 -1.99467,4.30504 -2.47977,8.98337 -3.9375,13.46875 -0.71796,4.30292 -1.34881,8.597857 -0.28125,12.906247 0.32053,3.50159 -0.68919,8.25865 2.5,10.71875 4.72728,3.88304 8.65575,8.79543 12.624995,13.46875 6.21914,7.65333 11.72948,15.86251 16.59375,24.4375 0.32431,-2.11756 1.10954,4.26459 2.53125,4.6875 -0.49161,-3.19231 -1.13213,-8.26328 -1.4375,-12.1875 -1.5814,-10.2909 -6.65305,-19.64903 -8.5625,-29.84375 -0.0587,-0.43037 -0.12809,-0.87203 -0.1875,-1.3125 l 0,-1.28125 -0.15625,0 c -0.62551,-5.04297 -0.8504,-10.46546 2.8125,-14.40625 3.73968,-3.772097 9.30633,-4.722447 13.8125,-7.343747 1.00194,-0.59119 2.04921,-1.07174 3.125,-1.40625 0.009,-0.003 0.0228,0.003 0.0312,0 3.11701,-0.96341 6.44862,-0.93323 9.6875,-0.40625 0.0479,0.008 0.10841,0.0233 0.15625,0.0312 0.29455,0.0493 0.61389,0.099 0.90625,0.15625 2.37136,0.21133 7.14463,1.13687 8,-0.5 -3.27225,-2.78631 -7.98526,-2.59211 -11.96875,-3.6875 -0.63059,-0.11469 -1.41182,-0.24041 -2.1875,-0.3125 l -3.90625,-0.875 -0.96875,-0.25 0,0.0312 -13.96875,-2.71875 c -0.22212,-0.20226 -0.46434,-0.40933 -0.6875,-0.5625 l 13.625,1.6875 0,-0.0625 c 0.48011,0.10699 0.95576,0.19361 1.4375,0.25 l 0,0.0312 9.625,1.78125 c 1.66103,0.61952 3.4322,1.08374 5.09375,1.1875 2.74263,0.39907 6.22526,4.49092 7.125,4.6875 -0.44096,-4.307 -4.7422,-6.23586 -8.3125,-7.5 -4.1712,-2.02803 -10.4023,-1.95417 -11.0625,-7.5625 -0.1756,-0.39076 -0.34902,-0.78118 -0.5625,-1.15625 l -1.625,-2.15625 0.0625,-0.0312 c -2.21724,-2.61691 -5.34011,-4.52196 -8.65625,-5.25 -3.2914,-1.13611 -6.98773,-2.2671 -10.46875,-2.71875 -1.18132,3.47826 -2.5031,-2.75561 -5.34375,-0.90625 -2.48996,0.29488 -2.14614,0.95256 -4,-0.625 z m 17.90625,10.15625 c 0.90187,-0.0238 1.93277,0.14208 2.96875,0.5 2.76259,0.95447 4.56151,2.96523 4.03125,4.5 -0.53026,1.53477 -3.20616,1.98572 -5.96875,1.03125 -2.76259,-0.95447 -4.5615,-2.93398 -4.03125,-4.46875 0.33141,-0.95923 1.49689,-1.52281 3,-1.5625 z" + id="path3499-9-7" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/ui/icons/qemu_128x128.png b/ui/icons/qemu_128x128.png Binary files differnew file mode 100644 index 000000000..96831807b --- /dev/null +++ b/ui/icons/qemu_128x128.png diff --git a/ui/icons/qemu_16x16.png b/ui/icons/qemu_16x16.png Binary files differnew file mode 100644 index 000000000..ff4f04602 --- /dev/null +++ b/ui/icons/qemu_16x16.png diff --git a/ui/icons/qemu_24x24.png b/ui/icons/qemu_24x24.png Binary files differnew file mode 100644 index 000000000..f039c6e25 --- /dev/null +++ b/ui/icons/qemu_24x24.png diff --git a/ui/icons/qemu_256x256.png b/ui/icons/qemu_256x256.png Binary files differnew file mode 100644 index 000000000..a39c0e307 --- /dev/null +++ b/ui/icons/qemu_256x256.png diff --git a/ui/icons/qemu_32x32.bmp b/ui/icons/qemu_32x32.bmp Binary files differnew file mode 100644 index 000000000..c0daa54ab --- /dev/null +++ b/ui/icons/qemu_32x32.bmp diff --git a/ui/icons/qemu_32x32.png b/ui/icons/qemu_32x32.png Binary files differnew file mode 100644 index 000000000..b746096cf --- /dev/null +++ b/ui/icons/qemu_32x32.png diff --git a/ui/icons/qemu_48x48.png b/ui/icons/qemu_48x48.png Binary files differnew file mode 100644 index 000000000..067281225 --- /dev/null +++ b/ui/icons/qemu_48x48.png diff --git a/ui/icons/qemu_512x512.png b/ui/icons/qemu_512x512.png Binary files differnew file mode 100644 index 000000000..86aaa6395 --- /dev/null +++ b/ui/icons/qemu_512x512.png diff --git a/ui/icons/qemu_64x64.png b/ui/icons/qemu_64x64.png Binary files differnew file mode 100644 index 000000000..e00c8b4c9 --- /dev/null +++ b/ui/icons/qemu_64x64.png diff --git a/ui/input-barrier.c b/ui/input-barrier.c new file mode 100644 index 000000000..2d57ca707 --- /dev/null +++ b/ui/input-barrier.c @@ -0,0 +1,746 @@ +/* + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This work is licensed under the terms of the GNU GPL, version 2 or later. + * See the COPYING file in the top-level directory. + * + * TODO: + * + * - Enable SSL + * - Manage SetOptions/ResetOptions commands + */ + +#include "qemu/osdep.h" +#include "sysemu/sysemu.h" +#include "qemu/main-loop.h" +#include "qemu/sockets.h" +#include "qapi/error.h" +#include "qom/object_interfaces.h" +#include "io/channel-socket.h" +#include "ui/input.h" +#include "qom/object.h" +#include "ui/vnc_keysym.h" /* use name2keysym from VNC as we use X11 values */ +#include "qemu/cutils.h" +#include "qapi/qmp/qerror.h" +#include "input-barrier.h" + +#define TYPE_INPUT_BARRIER "input-barrier" +OBJECT_DECLARE_SIMPLE_TYPE(InputBarrier, + INPUT_BARRIER) + + +#define MAX_HELLO_LENGTH 1024 + +struct InputBarrier { + Object parent; + + QIOChannelSocket *sioc; + guint ioc_tag; + + /* display properties */ + gchar *name; + int16_t x_origin, y_origin; + int16_t width, height; + + /* keyboard/mouse server */ + + SocketAddress saddr; + + char buffer[MAX_HELLO_LENGTH]; +}; + + +static const char *cmd_names[] = { + [barrierCmdCNoop] = "CNOP", + [barrierCmdCClose] = "CBYE", + [barrierCmdCEnter] = "CINN", + [barrierCmdCLeave] = "COUT", + [barrierCmdCClipboard] = "CCLP", + [barrierCmdCScreenSaver] = "CSEC", + [barrierCmdCResetOptions] = "CROP", + [barrierCmdCInfoAck] = "CIAK", + [barrierCmdCKeepAlive] = "CALV", + [barrierCmdDKeyDown] = "DKDN", + [barrierCmdDKeyRepeat] = "DKRP", + [barrierCmdDKeyUp] = "DKUP", + [barrierCmdDMouseDown] = "DMDN", + [barrierCmdDMouseUp] = "DMUP", + [barrierCmdDMouseMove] = "DMMV", + [barrierCmdDMouseRelMove] = "DMRM", + [barrierCmdDMouseWheel] = "DMWM", + [barrierCmdDClipboard] = "DCLP", + [barrierCmdDInfo] = "DINF", + [barrierCmdDSetOptions] = "DSOP", + [barrierCmdDFileTransfer] = "DFTR", + [barrierCmdDDragInfo] = "DDRG", + [barrierCmdQInfo] = "QINF", + [barrierCmdEIncompatible] = "EICV", + [barrierCmdEBusy] = "EBSY", + [barrierCmdEUnknown] = "EUNK", + [barrierCmdEBad] = "EBAD", + [barrierCmdHello] = "Barrier", + [barrierCmdHelloBack] = "Barrier", +}; + +static kbd_layout_t *kbd_layout; + +static int input_barrier_to_qcode(uint16_t keyid, uint16_t keycode) +{ + /* keycode is optional, if it is not provided use keyid */ + if (keycode && keycode <= qemu_input_map_xorgkbd_to_qcode_len) { + return qemu_input_map_xorgkbd_to_qcode[keycode]; + } + + if (keyid >= 0xE000 && keyid <= 0xEFFF) { + keyid += 0x1000; + } + + /* keyid is the X11 key id */ + if (kbd_layout) { + keycode = keysym2scancode(kbd_layout, keyid, NULL, false); + + return qemu_input_key_number_to_qcode(keycode); + } + + return qemu_input_map_x11_to_qcode[keyid]; +} + +static int input_barrier_to_mouse(uint8_t buttonid) +{ + switch (buttonid) { + case barrierButtonLeft: + return INPUT_BUTTON_LEFT; + case barrierButtonMiddle: + return INPUT_BUTTON_MIDDLE; + case barrierButtonRight: + return INPUT_BUTTON_RIGHT; + case barrierButtonExtra0: + return INPUT_BUTTON_SIDE; + } + return buttonid; +} + +#define read_char(x, p, l) \ +do { \ + int size = sizeof(char); \ + if (l < size) { \ + return G_SOURCE_REMOVE; \ + } \ + x = *(char *)p; \ + p += size; \ + l -= size; \ +} while (0) + +#define read_short(x, p, l) \ +do { \ + int size = sizeof(short); \ + if (l < size) { \ + return G_SOURCE_REMOVE; \ + } \ + x = ntohs(*(short *)p); \ + p += size; \ + l -= size; \ +} while (0) + +#define write_short(p, x, l) \ +do { \ + int size = sizeof(short); \ + if (l < size) { \ + return G_SOURCE_REMOVE; \ + } \ + *(short *)p = htons(x); \ + p += size; \ + l -= size; \ +} while (0) + +#define read_int(x, p, l) \ +do { \ + int size = sizeof(int); \ + if (l < size) { \ + return G_SOURCE_REMOVE; \ + } \ + x = ntohl(*(int *)p); \ + p += size; \ + l -= size; \ +} while (0) + +#define write_int(p, x, l) \ +do { \ + int size = sizeof(int); \ + if (l < size) { \ + return G_SOURCE_REMOVE; \ + } \ + *(int *)p = htonl(x); \ + p += size; \ + l -= size; \ +} while (0) + +#define write_cmd(p, c, l) \ +do { \ + int size = strlen(cmd_names[c]); \ + if (l < size) { \ + return G_SOURCE_REMOVE; \ + } \ + memcpy(p, cmd_names[c], size); \ + p += size; \ + l -= size; \ +} while (0) + +#define write_string(p, s, l) \ +do { \ + int size = strlen(s); \ + if (l < size + sizeof(int)) { \ + return G_SOURCE_REMOVE; \ + } \ + *(int *)p = htonl(size); \ + p += sizeof(size); \ + l -= sizeof(size); \ + memcpy(p, s, size); \ + p += size; \ + l -= size; \ +} while (0) + +static gboolean readcmd(InputBarrier *ib, struct barrierMsg *msg) +{ + int ret, len, i; + enum barrierCmd cmd; + char *p; + + ret = qio_channel_read(QIO_CHANNEL(ib->sioc), (char *)&len, sizeof(len), + NULL); + if (ret < 0) { + return G_SOURCE_REMOVE; + } + + len = ntohl(len); + if (len > MAX_HELLO_LENGTH) { + return G_SOURCE_REMOVE; + } + + ret = qio_channel_read(QIO_CHANNEL(ib->sioc), ib->buffer, len, NULL); + if (ret < 0) { + return G_SOURCE_REMOVE; + } + + p = ib->buffer; + if (len >= strlen(cmd_names[barrierCmdHello]) && + memcmp(p, cmd_names[barrierCmdHello], + strlen(cmd_names[barrierCmdHello])) == 0) { + cmd = barrierCmdHello; + p += strlen(cmd_names[barrierCmdHello]); + len -= strlen(cmd_names[barrierCmdHello]); + } else { + for (cmd = 0; cmd < barrierCmdHello; cmd++) { + if (memcmp(ib->buffer, cmd_names[cmd], 4) == 0) { + break; + } + } + + if (cmd == barrierCmdHello) { + return G_SOURCE_REMOVE; + } + p += 4; + len -= 4; + } + + msg->cmd = cmd; + switch (cmd) { + /* connection */ + case barrierCmdHello: + read_short(msg->version.major, p, len); + read_short(msg->version.minor, p, len); + break; + case barrierCmdDSetOptions: + read_int(msg->set.nb, p, len); + msg->set.nb /= 2; + if (msg->set.nb > BARRIER_MAX_OPTIONS) { + msg->set.nb = BARRIER_MAX_OPTIONS; + } + i = 0; + while (len && i < msg->set.nb) { + read_int(msg->set.option[i].id, p, len); + /* it's a string, restore endianness */ + msg->set.option[i].id = htonl(msg->set.option[i].id); + msg->set.option[i].nul = 0; + read_int(msg->set.option[i].value, p, len); + i++; + } + break; + case barrierCmdQInfo: + break; + + /* mouse */ + case barrierCmdDMouseMove: + case barrierCmdDMouseRelMove: + read_short(msg->mousepos.x, p, len); + read_short(msg->mousepos.y, p, len); + break; + case barrierCmdDMouseDown: + case barrierCmdDMouseUp: + read_char(msg->mousebutton.buttonid, p, len); + break; + case barrierCmdDMouseWheel: + read_short(msg->mousepos.y, p, len); + msg->mousepos.x = 0; + if (len) { + msg->mousepos.x = msg->mousepos.y; + read_short(msg->mousepos.y, p, len); + } + break; + + /* keyboard */ + case barrierCmdDKeyDown: + case barrierCmdDKeyUp: + read_short(msg->key.keyid, p, len); + read_short(msg->key.modifier, p, len); + msg->key.button = 0; + if (len) { + read_short(msg->key.button, p, len); + } + break; + case barrierCmdDKeyRepeat: + read_short(msg->repeat.keyid, p, len); + read_short(msg->repeat.modifier, p, len); + read_short(msg->repeat.repeat, p, len); + msg->repeat.button = 0; + if (len) { + read_short(msg->repeat.button, p, len); + } + break; + case barrierCmdCInfoAck: + case barrierCmdCResetOptions: + case barrierCmdCEnter: + case barrierCmdDClipboard: + case barrierCmdCKeepAlive: + case barrierCmdCLeave: + case barrierCmdCClose: + break; + + /* Invalid from the server */ + case barrierCmdHelloBack: + case barrierCmdCNoop: + case barrierCmdDInfo: + break; + + /* Error codes */ + case barrierCmdEIncompatible: + read_short(msg->version.major, p, len); + read_short(msg->version.minor, p, len); + break; + case barrierCmdEBusy: + case barrierCmdEUnknown: + case barrierCmdEBad: + break; + default: + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static gboolean writecmd(InputBarrier *ib, struct barrierMsg *msg) +{ + char *p; + int ret, i; + int avail, len; + + p = ib->buffer; + avail = MAX_HELLO_LENGTH; + + /* reserve space to store the length */ + p += sizeof(int); + avail -= sizeof(int); + + switch (msg->cmd) { + case barrierCmdHello: + if (msg->version.major < BARRIER_VERSION_MAJOR || + (msg->version.major == BARRIER_VERSION_MAJOR && + msg->version.minor < BARRIER_VERSION_MINOR)) { + ib->ioc_tag = 0; + return G_SOURCE_REMOVE; + } + write_cmd(p, barrierCmdHelloBack, avail); + write_short(p, BARRIER_VERSION_MAJOR, avail); + write_short(p, BARRIER_VERSION_MINOR, avail); + write_string(p, ib->name, avail); + break; + case barrierCmdCClose: + ib->ioc_tag = 0; + return G_SOURCE_REMOVE; + case barrierCmdQInfo: + write_cmd(p, barrierCmdDInfo, avail); + write_short(p, ib->x_origin, avail); + write_short(p, ib->y_origin, avail); + write_short(p, ib->width, avail); + write_short(p, ib->height, avail); + write_short(p, 0, avail); /* warpsize (obsolete) */ + write_short(p, 0, avail); /* mouse x */ + write_short(p, 0, avail); /* mouse y */ + break; + case barrierCmdCInfoAck: + break; + case barrierCmdCResetOptions: + /* TODO: reset options */ + break; + case barrierCmdDSetOptions: + /* TODO: set options */ + break; + case barrierCmdCEnter: + break; + case barrierCmdDClipboard: + break; + case barrierCmdCKeepAlive: + write_cmd(p, barrierCmdCKeepAlive, avail); + break; + case barrierCmdCLeave: + break; + + /* mouse */ + case barrierCmdDMouseMove: + qemu_input_queue_abs(NULL, INPUT_AXIS_X, msg->mousepos.x, + ib->x_origin, ib->width); + qemu_input_queue_abs(NULL, INPUT_AXIS_Y, msg->mousepos.y, + ib->y_origin, ib->height); + qemu_input_event_sync(); + break; + case barrierCmdDMouseRelMove: + qemu_input_queue_rel(NULL, INPUT_AXIS_X, msg->mousepos.x); + qemu_input_queue_rel(NULL, INPUT_AXIS_Y, msg->mousepos.y); + qemu_input_event_sync(); + break; + case barrierCmdDMouseDown: + qemu_input_queue_btn(NULL, + input_barrier_to_mouse(msg->mousebutton.buttonid), + true); + qemu_input_event_sync(); + break; + case barrierCmdDMouseUp: + qemu_input_queue_btn(NULL, + input_barrier_to_mouse(msg->mousebutton.buttonid), + false); + qemu_input_event_sync(); + break; + case barrierCmdDMouseWheel: + qemu_input_queue_btn(NULL, (msg->mousepos.y > 0) ? INPUT_BUTTON_WHEEL_UP + : INPUT_BUTTON_WHEEL_DOWN, true); + qemu_input_event_sync(); + qemu_input_queue_btn(NULL, (msg->mousepos.y > 0) ? INPUT_BUTTON_WHEEL_UP + : INPUT_BUTTON_WHEEL_DOWN, false); + qemu_input_event_sync(); + break; + + /* keyboard */ + case barrierCmdDKeyDown: + qemu_input_event_send_key_qcode(NULL, + input_barrier_to_qcode(msg->key.keyid, msg->key.button), + true); + break; + case barrierCmdDKeyRepeat: + for (i = 0; i < msg->repeat.repeat; i++) { + qemu_input_event_send_key_qcode(NULL, + input_barrier_to_qcode(msg->repeat.keyid, msg->repeat.button), + false); + qemu_input_event_send_key_qcode(NULL, + input_barrier_to_qcode(msg->repeat.keyid, msg->repeat.button), + true); + } + break; + case barrierCmdDKeyUp: + qemu_input_event_send_key_qcode(NULL, + input_barrier_to_qcode(msg->key.keyid, msg->key.button), + false); + break; + default: + write_cmd(p, barrierCmdEUnknown, avail); + break; + } + + len = MAX_HELLO_LENGTH - avail - sizeof(int); + if (len) { + p = ib->buffer; + avail = sizeof(len); + write_int(p, len, avail); + ret = qio_channel_write(QIO_CHANNEL(ib->sioc), ib->buffer, + len + sizeof(len), NULL); + if (ret < 0) { + ib->ioc_tag = 0; + return G_SOURCE_REMOVE; + } + } + + return G_SOURCE_CONTINUE; +} + +static gboolean input_barrier_event(QIOChannel *ioc G_GNUC_UNUSED, + GIOCondition condition, void *opaque) +{ + InputBarrier *ib = opaque; + int ret; + struct barrierMsg msg; + + ret = readcmd(ib, &msg); + if (ret == G_SOURCE_REMOVE) { + ib->ioc_tag = 0; + return G_SOURCE_REMOVE; + } + + return writecmd(ib, &msg); +} + +static void input_barrier_complete(UserCreatable *uc, Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(uc); + Error *local_err = NULL; + + if (!ib->name) { + error_setg(errp, QERR_MISSING_PARAMETER, "name"); + return; + } + + /* + * Connect to the primary + * Primary is the server where the keyboard and the mouse + * are connected and forwarded to the secondary (the client) + */ + + ib->sioc = qio_channel_socket_new(); + qio_channel_set_name(QIO_CHANNEL(ib->sioc), "barrier-client"); + + qio_channel_socket_connect_sync(ib->sioc, &ib->saddr, &local_err); + if (local_err) { + error_propagate(errp, local_err); + return; + } + + qio_channel_set_delay(QIO_CHANNEL(ib->sioc), false); + + ib->ioc_tag = qio_channel_add_watch(QIO_CHANNEL(ib->sioc), G_IO_IN, + input_barrier_event, ib, NULL); +} + +static void input_barrier_instance_finalize(Object *obj) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + + if (ib->ioc_tag) { + g_source_remove(ib->ioc_tag); + ib->ioc_tag = 0; + } + + if (ib->sioc) { + qio_channel_close(QIO_CHANNEL(ib->sioc), NULL); + object_unref(OBJECT(ib->sioc)); + } + g_free(ib->name); + g_free(ib->saddr.u.inet.host); + g_free(ib->saddr.u.inet.port); +} + +static char *input_barrier_get_name(Object *obj, Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + + return g_strdup(ib->name); +} + +static void input_barrier_set_name(Object *obj, const char *value, + Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + + if (ib->name) { + error_setg(errp, "name property already set"); + return; + } + ib->name = g_strdup(value); +} + +static char *input_barrier_get_server(Object *obj, Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + + return g_strdup(ib->saddr.u.inet.host); +} + +static void input_barrier_set_server(Object *obj, const char *value, + Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + + g_free(ib->saddr.u.inet.host); + ib->saddr.u.inet.host = g_strdup(value); +} + +static char *input_barrier_get_port(Object *obj, Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + + return g_strdup(ib->saddr.u.inet.port); +} + +static void input_barrier_set_port(Object *obj, const char *value, + Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + + g_free(ib->saddr.u.inet.port); + ib->saddr.u.inet.port = g_strdup(value); +} + +static void input_barrier_set_x_origin(Object *obj, const char *value, + Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + int result, err; + + err = qemu_strtoi(value, NULL, 0, &result); + if (err < 0 || result < 0 || result > SHRT_MAX) { + error_setg(errp, + "x-origin property must be in the range [0..%d]", SHRT_MAX); + return; + } + ib->x_origin = result; +} + +static char *input_barrier_get_x_origin(Object *obj, Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + + return g_strdup_printf("%d", ib->x_origin); +} + +static void input_barrier_set_y_origin(Object *obj, const char *value, + Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + int result, err; + + err = qemu_strtoi(value, NULL, 0, &result); + if (err < 0 || result < 0 || result > SHRT_MAX) { + error_setg(errp, + "y-origin property must be in the range [0..%d]", SHRT_MAX); + return; + } + ib->y_origin = result; +} + +static char *input_barrier_get_y_origin(Object *obj, Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + + return g_strdup_printf("%d", ib->y_origin); +} + +static void input_barrier_set_width(Object *obj, const char *value, + Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + int result, err; + + err = qemu_strtoi(value, NULL, 0, &result); + if (err < 0 || result < 0 || result > SHRT_MAX) { + error_setg(errp, + "width property must be in the range [0..%d]", SHRT_MAX); + return; + } + ib->width = result; +} + +static char *input_barrier_get_width(Object *obj, Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + + return g_strdup_printf("%d", ib->width); +} + +static void input_barrier_set_height(Object *obj, const char *value, + Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + int result, err; + + err = qemu_strtoi(value, NULL, 0, &result); + if (err < 0 || result < 0 || result > SHRT_MAX) { + error_setg(errp, + "height property must be in the range [0..%d]", SHRT_MAX); + return; + } + ib->height = result; +} + +static char *input_barrier_get_height(Object *obj, Error **errp) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + + return g_strdup_printf("%d", ib->height); +} + +static void input_barrier_instance_init(Object *obj) +{ + InputBarrier *ib = INPUT_BARRIER(obj); + + /* always use generic keymaps */ + if (keyboard_layout && !kbd_layout) { + /* We use X11 key id, so use VNC name2keysym */ + kbd_layout = init_keyboard_layout(name2keysym, keyboard_layout, + &error_fatal); + } + + ib->saddr.type = SOCKET_ADDRESS_TYPE_INET; + ib->saddr.u.inet.host = g_strdup("localhost"); + ib->saddr.u.inet.port = g_strdup("24800"); + + ib->x_origin = 0; + ib->y_origin = 0; + ib->width = 1920; + ib->height = 1080; +} + +static void input_barrier_class_init(ObjectClass *oc, void *data) +{ + UserCreatableClass *ucc = USER_CREATABLE_CLASS(oc); + + ucc->complete = input_barrier_complete; + + object_class_property_add_str(oc, "name", + input_barrier_get_name, + input_barrier_set_name); + object_class_property_add_str(oc, "server", + input_barrier_get_server, + input_barrier_set_server); + object_class_property_add_str(oc, "port", + input_barrier_get_port, + input_barrier_set_port); + object_class_property_add_str(oc, "x-origin", + input_barrier_get_x_origin, + input_barrier_set_x_origin); + object_class_property_add_str(oc, "y-origin", + input_barrier_get_y_origin, + input_barrier_set_y_origin); + object_class_property_add_str(oc, "width", + input_barrier_get_width, + input_barrier_set_width); + object_class_property_add_str(oc, "height", + input_barrier_get_height, + input_barrier_set_height); +} + +static const TypeInfo input_barrier_info = { + .name = TYPE_INPUT_BARRIER, + .parent = TYPE_OBJECT, + .class_init = input_barrier_class_init, + .instance_size = sizeof(InputBarrier), + .instance_init = input_barrier_instance_init, + .instance_finalize = input_barrier_instance_finalize, + .interfaces = (InterfaceInfo[]) { + { TYPE_USER_CREATABLE }, + { } + } +}; + +static void register_types(void) +{ + type_register_static(&input_barrier_info); +} + +type_init(register_types); diff --git a/ui/input-barrier.h b/ui/input-barrier.h new file mode 100644 index 000000000..e5b090590 --- /dev/null +++ b/ui/input-barrier.h @@ -0,0 +1,112 @@ +/* + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This work is licensed under the terms of the GNU GPL, version 2 or later. + * See the COPYING file in the top-level directory. + */ + +#ifndef UI_INPUT_BARRIER_H +#define UI_INPUT_BARRIER_H + +/* Barrier protocol */ +#define BARRIER_VERSION_MAJOR 1 +#define BARRIER_VERSION_MINOR 6 + +enum barrierCmd { + barrierCmdCNoop, + barrierCmdCClose, + barrierCmdCEnter, + barrierCmdCLeave, + barrierCmdCClipboard, + barrierCmdCScreenSaver, + barrierCmdCResetOptions, + barrierCmdCInfoAck, + barrierCmdCKeepAlive, + barrierCmdDKeyDown, + barrierCmdDKeyRepeat, + barrierCmdDKeyUp, + barrierCmdDMouseDown, + barrierCmdDMouseUp, + barrierCmdDMouseMove, + barrierCmdDMouseRelMove, + barrierCmdDMouseWheel, + barrierCmdDClipboard, + barrierCmdDInfo, + barrierCmdDSetOptions, + barrierCmdDFileTransfer, + barrierCmdDDragInfo, + barrierCmdQInfo, + barrierCmdEIncompatible, + barrierCmdEBusy, + barrierCmdEUnknown, + barrierCmdEBad, + /* connection sequence */ + barrierCmdHello, + barrierCmdHelloBack, +}; + +enum { + barrierButtonNone, + barrierButtonLeft, + barrierButtonMiddle, + barrierButtonRight, + barrierButtonExtra0 +}; + +struct barrierVersion { + int16_t major; + int16_t minor; +}; + +struct barrierMouseButton { + int8_t buttonid; +}; + +struct barrierEnter { + int16_t x; + int16_t y; + int32_t seqn; + int16_t modifier; +}; + +struct barrierMousePos { + int16_t x; + int16_t y; +}; + +struct barrierKey { + int16_t keyid; + int16_t modifier; + int16_t button; +}; + +struct barrierRepeat { + int16_t keyid; + int16_t modifier; + int16_t repeat; + int16_t button; +}; + +#define BARRIER_MAX_OPTIONS 32 +struct barrierSet { + int nb; + struct { + int id; + char nul; + int value; + } option[BARRIER_MAX_OPTIONS]; +}; + +struct barrierMsg { + enum barrierCmd cmd; + union { + struct barrierVersion version; + struct barrierMouseButton mousebutton; + struct barrierMousePos mousepos; + struct barrierEnter enter; + struct barrierKey key; + struct barrierRepeat repeat; + struct barrierSet set; + }; +}; +#endif diff --git a/ui/input-keymap.c b/ui/input-keymap.c new file mode 100644 index 000000000..1b756a697 --- /dev/null +++ b/ui/input-keymap.c @@ -0,0 +1,89 @@ +#include "qemu/osdep.h" +#include "keymaps.h" +#include "ui/input.h" + +#include "standard-headers/linux/input.h" + +#include "ui/input-keymap-atset1-to-qcode.c.inc" +#include "ui/input-keymap-linux-to-qcode.c.inc" +#include "ui/input-keymap-qcode-to-atset1.c.inc" +#include "ui/input-keymap-qcode-to-atset2.c.inc" +#include "ui/input-keymap-qcode-to-atset3.c.inc" +#include "ui/input-keymap-qcode-to-linux.c.inc" +#include "ui/input-keymap-qcode-to-qnum.c.inc" +#include "ui/input-keymap-qcode-to-sun.c.inc" +#include "ui/input-keymap-qnum-to-qcode.c.inc" +#include "ui/input-keymap-usb-to-qcode.c.inc" +#include "ui/input-keymap-win32-to-qcode.c.inc" +#include "ui/input-keymap-x11-to-qcode.c.inc" +#include "ui/input-keymap-xorgevdev-to-qcode.c.inc" +#include "ui/input-keymap-xorgkbd-to-qcode.c.inc" +#include "ui/input-keymap-xorgxquartz-to-qcode.c.inc" +#include "ui/input-keymap-xorgxwin-to-qcode.c.inc" +#include "ui/input-keymap-osx-to-qcode.c.inc" + +int qemu_input_linux_to_qcode(unsigned int lnx) +{ + if (lnx >= qemu_input_map_linux_to_qcode_len) { + return 0; + } + return qemu_input_map_linux_to_qcode[lnx]; +} + +int qemu_input_key_value_to_number(const KeyValue *value) +{ + if (value->type == KEY_VALUE_KIND_QCODE) { + if (value->u.qcode.data >= qemu_input_map_qcode_to_qnum_len) { + return 0; + } + return qemu_input_map_qcode_to_qnum[value->u.qcode.data]; + } else { + assert(value->type == KEY_VALUE_KIND_NUMBER); + return value->u.number.data; + } +} + +int qemu_input_key_number_to_qcode(unsigned int nr) +{ + if (nr >= qemu_input_map_qnum_to_qcode_len) { + return 0; + } + return qemu_input_map_qnum_to_qcode[nr]; +} + +int qemu_input_key_value_to_qcode(const KeyValue *value) +{ + if (value->type == KEY_VALUE_KIND_QCODE) { + return value->u.qcode.data; + } else { + assert(value->type == KEY_VALUE_KIND_NUMBER); + return qemu_input_key_number_to_qcode(value->u.number.data); + } +} + +int qemu_input_key_value_to_scancode(const KeyValue *value, bool down, + int *codes) +{ + int keycode = qemu_input_key_value_to_number(value); + int count = 0; + + if (value->type == KEY_VALUE_KIND_QCODE && + value->u.qcode.data == Q_KEY_CODE_PAUSE) { + /* specific case */ + int v = down ? 0 : 0x80; + codes[count++] = 0xe1; + codes[count++] = 0x1d | v; + codes[count++] = 0x45 | v; + return count; + } + if (keycode & SCANCODE_GREY) { + codes[count++] = SCANCODE_EMUL0; + keycode &= ~SCANCODE_GREY; + } + if (!down) { + keycode |= SCANCODE_UP; + } + codes[count++] = keycode; + + return count; +} diff --git a/ui/input-legacy.c b/ui/input-legacy.c new file mode 100644 index 000000000..9fc78a639 --- /dev/null +++ b/ui/input-legacy.c @@ -0,0 +1,275 @@ +/* + * QEMU System Emulator + * + * Copyright (c) 2003-2008 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "qapi/qapi-commands-ui.h" +#include "ui/console.h" +#include "keymaps.h" +#include "ui/input.h" + +struct QEMUPutMouseEntry { + QEMUPutMouseEvent *qemu_put_mouse_event; + void *qemu_put_mouse_event_opaque; + int qemu_put_mouse_event_absolute; + + /* new input core */ + QemuInputHandler h; + QemuInputHandlerState *s; + int axis[INPUT_AXIS__MAX]; + int buttons; +}; + +struct QEMUPutKbdEntry { + QEMUPutKBDEvent *put_kbd; + void *opaque; + QemuInputHandlerState *s; +}; + +struct QEMUPutLEDEntry { + QEMUPutLEDEvent *put_led; + void *opaque; + QTAILQ_ENTRY(QEMUPutLEDEntry) next; +}; + +static QTAILQ_HEAD(, QEMUPutLEDEntry) led_handlers = + QTAILQ_HEAD_INITIALIZER(led_handlers); + +int index_from_key(const char *key, size_t key_length) +{ + int i; + + for (i = 0; i < Q_KEY_CODE__MAX; i++) { + if (!strncmp(key, QKeyCode_str(i), key_length) && + !QKeyCode_str(i)[key_length]) { + break; + } + } + + /* Return Q_KEY_CODE__MAX if the key is invalid */ + return i; +} + +static KeyValue *copy_key_value(KeyValue *src) +{ + KeyValue *dst = g_new(KeyValue, 1); + memcpy(dst, src, sizeof(*src)); + if (dst->type == KEY_VALUE_KIND_NUMBER) { + QKeyCode code = qemu_input_key_number_to_qcode(dst->u.number.data); + dst->type = KEY_VALUE_KIND_QCODE; + dst->u.qcode.data = code; + } + return dst; +} + +void qmp_send_key(KeyValueList *keys, bool has_hold_time, int64_t hold_time, + Error **errp) +{ + KeyValueList *p; + KeyValue **up = NULL; + int count = 0; + + if (!has_hold_time) { + hold_time = 0; /* use default */ + } + + for (p = keys; p != NULL; p = p->next) { + qemu_input_event_send_key(NULL, copy_key_value(p->value), true); + qemu_input_event_send_key_delay(hold_time); + up = g_realloc(up, sizeof(*up) * (count+1)); + up[count] = copy_key_value(p->value); + count++; + } + while (count) { + count--; + qemu_input_event_send_key(NULL, up[count], false); + qemu_input_event_send_key_delay(hold_time); + } + g_free(up); +} + +static void legacy_kbd_event(DeviceState *dev, QemuConsole *src, + InputEvent *evt) +{ + QEMUPutKbdEntry *entry = (QEMUPutKbdEntry *)dev; + int scancodes[3], i, count; + InputKeyEvent *key = evt->u.key.data; + + if (!entry || !entry->put_kbd) { + return; + } + count = qemu_input_key_value_to_scancode(key->key, + key->down, + scancodes); + for (i = 0; i < count; i++) { + entry->put_kbd(entry->opaque, scancodes[i]); + } +} + +static QemuInputHandler legacy_kbd_handler = { + .name = "legacy-kbd", + .mask = INPUT_EVENT_MASK_KEY, + .event = legacy_kbd_event, +}; + +QEMUPutKbdEntry *qemu_add_kbd_event_handler(QEMUPutKBDEvent *func, void *opaque) +{ + QEMUPutKbdEntry *entry; + + entry = g_new0(QEMUPutKbdEntry, 1); + entry->put_kbd = func; + entry->opaque = opaque; + entry->s = qemu_input_handler_register((DeviceState *)entry, + &legacy_kbd_handler); + qemu_input_handler_activate(entry->s); + return entry; +} + +static void legacy_mouse_event(DeviceState *dev, QemuConsole *src, + InputEvent *evt) +{ + static const int bmap[INPUT_BUTTON__MAX] = { + [INPUT_BUTTON_LEFT] = MOUSE_EVENT_LBUTTON, + [INPUT_BUTTON_MIDDLE] = MOUSE_EVENT_MBUTTON, + [INPUT_BUTTON_RIGHT] = MOUSE_EVENT_RBUTTON, + }; + QEMUPutMouseEntry *s = (QEMUPutMouseEntry *)dev; + InputBtnEvent *btn; + InputMoveEvent *move; + + switch (evt->type) { + case INPUT_EVENT_KIND_BTN: + btn = evt->u.btn.data; + if (btn->down) { + s->buttons |= bmap[btn->button]; + } else { + s->buttons &= ~bmap[btn->button]; + } + if (btn->down && btn->button == INPUT_BUTTON_WHEEL_UP) { + s->qemu_put_mouse_event(s->qemu_put_mouse_event_opaque, + s->axis[INPUT_AXIS_X], + s->axis[INPUT_AXIS_Y], + -1, + s->buttons); + } + if (btn->down && btn->button == INPUT_BUTTON_WHEEL_DOWN) { + s->qemu_put_mouse_event(s->qemu_put_mouse_event_opaque, + s->axis[INPUT_AXIS_X], + s->axis[INPUT_AXIS_Y], + 1, + s->buttons); + } + break; + case INPUT_EVENT_KIND_ABS: + move = evt->u.abs.data; + s->axis[move->axis] = move->value; + break; + case INPUT_EVENT_KIND_REL: + move = evt->u.rel.data; + s->axis[move->axis] += move->value; + break; + default: + break; + } +} + +static void legacy_mouse_sync(DeviceState *dev) +{ + QEMUPutMouseEntry *s = (QEMUPutMouseEntry *)dev; + + s->qemu_put_mouse_event(s->qemu_put_mouse_event_opaque, + s->axis[INPUT_AXIS_X], + s->axis[INPUT_AXIS_Y], + 0, + s->buttons); + + if (!s->qemu_put_mouse_event_absolute) { + s->axis[INPUT_AXIS_X] = 0; + s->axis[INPUT_AXIS_Y] = 0; + } +} + +QEMUPutMouseEntry *qemu_add_mouse_event_handler(QEMUPutMouseEvent *func, + void *opaque, int absolute, + const char *name) +{ + QEMUPutMouseEntry *s; + + s = g_new0(QEMUPutMouseEntry, 1); + + s->qemu_put_mouse_event = func; + s->qemu_put_mouse_event_opaque = opaque; + s->qemu_put_mouse_event_absolute = absolute; + + s->h.name = name; + s->h.mask = INPUT_EVENT_MASK_BTN | + (absolute ? INPUT_EVENT_MASK_ABS : INPUT_EVENT_MASK_REL); + s->h.event = legacy_mouse_event; + s->h.sync = legacy_mouse_sync; + s->s = qemu_input_handler_register((DeviceState *)s, + &s->h); + + return s; +} + +void qemu_activate_mouse_event_handler(QEMUPutMouseEntry *entry) +{ + qemu_input_handler_activate(entry->s); +} + +void qemu_remove_mouse_event_handler(QEMUPutMouseEntry *entry) +{ + qemu_input_handler_unregister(entry->s); + + g_free(entry); +} + +QEMUPutLEDEntry *qemu_add_led_event_handler(QEMUPutLEDEvent *func, + void *opaque) +{ + QEMUPutLEDEntry *s; + + s = g_new0(QEMUPutLEDEntry, 1); + + s->put_led = func; + s->opaque = opaque; + QTAILQ_INSERT_TAIL(&led_handlers, s, next); + return s; +} + +void qemu_remove_led_event_handler(QEMUPutLEDEntry *entry) +{ + if (entry == NULL) + return; + QTAILQ_REMOVE(&led_handlers, entry, next); + g_free(entry); +} + +void kbd_put_ledstate(int ledstate) +{ + QEMUPutLEDEntry *cursor; + + QTAILQ_FOREACH(cursor, &led_handlers, next) { + cursor->put_led(cursor->opaque, ledstate); + } +} diff --git a/ui/input-linux.c b/ui/input-linux.c new file mode 100644 index 000000000..05c0c9881 --- /dev/null +++ b/ui/input-linux.c @@ -0,0 +1,534 @@ +/* + * This work is licensed under the terms of the GNU GPL, version 2 or + * (at your option) any later version. See the COPYING file in the + * top-level directory. + */ + +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "qemu/config-file.h" +#include "qemu/main-loop.h" +#include "qemu/module.h" +#include "qemu/sockets.h" +#include "ui/input.h" +#include "qom/object_interfaces.h" +#include "sysemu/iothread.h" +#include "block/aio.h" + +#include <sys/ioctl.h> +#include "standard-headers/linux/input.h" +#include "qom/object.h" + +static bool linux_is_button(unsigned int lnx) +{ + if (lnx < 0x100) { + return false; + } + if (lnx >= 0x160 && lnx < 0x2c0) { + return false; + } + return true; +} + +#define TYPE_INPUT_LINUX "input-linux" +OBJECT_DECLARE_SIMPLE_TYPE(InputLinux, + INPUT_LINUX) + + +struct InputLinux { + Object parent; + + char *evdev; + int fd; + bool repeat; + bool grab_request; + bool grab_active; + bool grab_all; + bool keydown[KEY_CNT]; + int keycount; + int wheel; + bool initialized; + + bool has_rel_x; + bool has_abs_x; + int num_keys; + int num_btns; + int abs_x_min; + int abs_x_max; + int abs_y_min; + int abs_y_max; + struct input_event event; + int read_offset; + + enum GrabToggleKeys grab_toggle; + + QTAILQ_ENTRY(InputLinux) next; +}; + + +static QTAILQ_HEAD(, InputLinux) inputs = QTAILQ_HEAD_INITIALIZER(inputs); + +static void input_linux_toggle_grab(InputLinux *il) +{ + intptr_t request = !il->grab_active; + InputLinux *item; + int rc; + + rc = ioctl(il->fd, EVIOCGRAB, request); + if (rc < 0) { + return; + } + il->grab_active = !il->grab_active; + + if (!il->grab_all) { + return; + } + QTAILQ_FOREACH(item, &inputs, next) { + if (item == il || item->grab_all) { + /* avoid endless loops */ + continue; + } + if (item->grab_active != il->grab_active) { + input_linux_toggle_grab(item); + } + } +} + +static bool input_linux_check_toggle(InputLinux *il) +{ + switch (il->grab_toggle) { + case GRAB_TOGGLE_KEYS_CTRL_CTRL: + return il->keydown[KEY_LEFTCTRL] && + il->keydown[KEY_RIGHTCTRL]; + + case GRAB_TOGGLE_KEYS_ALT_ALT: + return il->keydown[KEY_LEFTALT] && + il->keydown[KEY_RIGHTALT]; + + case GRAB_TOGGLE_KEYS_SHIFT_SHIFT: + return il->keydown[KEY_LEFTSHIFT] && + il->keydown[KEY_RIGHTSHIFT]; + + case GRAB_TOGGLE_KEYS_META_META: + return il->keydown[KEY_LEFTMETA] && + il->keydown[KEY_RIGHTMETA]; + + case GRAB_TOGGLE_KEYS_SCROLLLOCK: + return il->keydown[KEY_SCROLLLOCK]; + + case GRAB_TOGGLE_KEYS_CTRL_SCROLLLOCK: + return (il->keydown[KEY_LEFTCTRL] || + il->keydown[KEY_RIGHTCTRL]) && + il->keydown[KEY_SCROLLLOCK]; + + case GRAB_TOGGLE_KEYS__MAX: + /* avoid gcc error */ + break; + } + return false; +} + +static bool input_linux_should_skip(InputLinux *il, + struct input_event *event) +{ + return (il->grab_toggle == GRAB_TOGGLE_KEYS_SCROLLLOCK || + il->grab_toggle == GRAB_TOGGLE_KEYS_CTRL_SCROLLLOCK) && + event->code == KEY_SCROLLLOCK; +} + +static void input_linux_handle_keyboard(InputLinux *il, + struct input_event *event) +{ + if (event->type == EV_KEY) { + if (event->value > 2 || (event->value > 1 && !il->repeat)) { + /* + * ignore autorepeat + unknown key events + * 0 == up, 1 == down, 2 == autorepeat, other == undefined + */ + return; + } + if (event->code >= KEY_CNT) { + /* + * Should not happen. But better safe than sorry, + * and we make Coverity happy too. + */ + return; + } + + /* keep track of key state */ + if (!il->keydown[event->code] && event->value) { + il->keydown[event->code] = true; + il->keycount++; + } + if (il->keydown[event->code] && !event->value) { + il->keydown[event->code] = false; + il->keycount--; + } + + /* send event to guest when grab is active */ + if (il->grab_active && !input_linux_should_skip(il, event)) { + int qcode = qemu_input_linux_to_qcode(event->code); + qemu_input_event_send_key_qcode(NULL, qcode, event->value); + } + + /* hotkey -> record switch request ... */ + if (input_linux_check_toggle(il)) { + il->grab_request = true; + } + + /* + * ... and do the switch when all keys are lifted, so we + * confuse neither guest nor host with keys which seem to + * be stuck due to missing key-up events. + */ + if (il->grab_request && !il->keycount) { + il->grab_request = false; + input_linux_toggle_grab(il); + } + } +} + +static void input_linux_event_mouse_button(int button) +{ + qemu_input_queue_btn(NULL, button, true); + qemu_input_event_sync(); + qemu_input_queue_btn(NULL, button, false); + qemu_input_event_sync(); +} + +static void input_linux_handle_mouse(InputLinux *il, struct input_event *event) +{ + if (!il->grab_active) { + return; + } + + switch (event->type) { + case EV_KEY: + switch (event->code) { + case BTN_LEFT: + qemu_input_queue_btn(NULL, INPUT_BUTTON_LEFT, event->value); + break; + case BTN_RIGHT: + qemu_input_queue_btn(NULL, INPUT_BUTTON_RIGHT, event->value); + break; + case BTN_MIDDLE: + qemu_input_queue_btn(NULL, INPUT_BUTTON_MIDDLE, event->value); + break; + case BTN_GEAR_UP: + qemu_input_queue_btn(NULL, INPUT_BUTTON_WHEEL_UP, event->value); + break; + case BTN_GEAR_DOWN: + qemu_input_queue_btn(NULL, INPUT_BUTTON_WHEEL_DOWN, + event->value); + break; + case BTN_SIDE: + qemu_input_queue_btn(NULL, INPUT_BUTTON_SIDE, event->value); + break; + case BTN_EXTRA: + qemu_input_queue_btn(NULL, INPUT_BUTTON_EXTRA, event->value); + break; + }; + break; + case EV_REL: + switch (event->code) { + case REL_X: + qemu_input_queue_rel(NULL, INPUT_AXIS_X, event->value); + break; + case REL_Y: + qemu_input_queue_rel(NULL, INPUT_AXIS_Y, event->value); + break; + case REL_WHEEL: + il->wheel = event->value; + break; + } + break; + case EV_ABS: + switch (event->code) { + case ABS_X: + qemu_input_queue_abs(NULL, INPUT_AXIS_X, event->value, + il->abs_x_min, il->abs_x_max); + break; + case ABS_Y: + qemu_input_queue_abs(NULL, INPUT_AXIS_Y, event->value, + il->abs_y_min, il->abs_y_max); + break; + } + break; + case EV_SYN: + qemu_input_event_sync(); + if (il->wheel != 0) { + input_linux_event_mouse_button((il->wheel > 0) + ? INPUT_BUTTON_WHEEL_UP + : INPUT_BUTTON_WHEEL_DOWN); + il->wheel = 0; + } + break; + } +} + +static void input_linux_event(void *opaque) +{ + InputLinux *il = opaque; + int rc; + int read_size; + uint8_t *p = (uint8_t *)&il->event; + + for (;;) { + read_size = sizeof(il->event) - il->read_offset; + rc = read(il->fd, &p[il->read_offset], read_size); + if (rc != read_size) { + if (rc < 0 && errno != EAGAIN) { + fprintf(stderr, "%s: read: %s\n", __func__, strerror(errno)); + qemu_set_fd_handler(il->fd, NULL, NULL, NULL); + close(il->fd); + } else if (rc > 0) { + il->read_offset += rc; + } + break; + } + il->read_offset = 0; + + if (il->num_keys) { + input_linux_handle_keyboard(il, &il->event); + } + if ((il->has_rel_x || il->has_abs_x) && il->num_btns) { + input_linux_handle_mouse(il, &il->event); + } + } +} + +static void input_linux_complete(UserCreatable *uc, Error **errp) +{ + InputLinux *il = INPUT_LINUX(uc); + uint8_t evtmap, relmap, absmap; + uint8_t keymap[KEY_CNT / 8], keystate[KEY_CNT / 8]; + unsigned int i; + int rc, ver; + struct input_absinfo absinfo; + + if (!il->evdev) { + error_setg(errp, "no input device specified"); + return; + } + + il->fd = open(il->evdev, O_RDWR); + if (il->fd < 0) { + error_setg_file_open(errp, errno, il->evdev); + return; + } + qemu_set_nonblock(il->fd); + + rc = ioctl(il->fd, EVIOCGVERSION, &ver); + if (rc < 0) { + error_setg(errp, "%s: is not an evdev device", il->evdev); + goto err_close; + } + + rc = ioctl(il->fd, EVIOCGBIT(0, sizeof(evtmap)), &evtmap); + if (rc < 0) { + goto err_read_event_bits; + } + + if (evtmap & (1 << EV_REL)) { + relmap = 0; + rc = ioctl(il->fd, EVIOCGBIT(EV_REL, sizeof(relmap)), &relmap); + if (rc < 0) { + goto err_read_event_bits; + } + if (relmap & (1 << REL_X)) { + il->has_rel_x = true; + } + } + + if (evtmap & (1 << EV_ABS)) { + absmap = 0; + rc = ioctl(il->fd, EVIOCGBIT(EV_ABS, sizeof(absmap)), &absmap); + if (rc < 0) { + goto err_read_event_bits; + } + if (absmap & (1 << ABS_X)) { + il->has_abs_x = true; + rc = ioctl(il->fd, EVIOCGABS(ABS_X), &absinfo); + if (rc < 0) { + error_setg(errp, "%s: failed to get get absolute X value", + il->evdev); + goto err_close; + } + il->abs_x_min = absinfo.minimum; + il->abs_x_max = absinfo.maximum; + rc = ioctl(il->fd, EVIOCGABS(ABS_Y), &absinfo); + if (rc < 0) { + error_setg(errp, "%s: failed to get get absolute Y value", + il->evdev); + goto err_close; + } + il->abs_y_min = absinfo.minimum; + il->abs_y_max = absinfo.maximum; + } + } + + if (evtmap & (1 << EV_KEY)) { + memset(keymap, 0, sizeof(keymap)); + rc = ioctl(il->fd, EVIOCGBIT(EV_KEY, sizeof(keymap)), keymap); + if (rc < 0) { + goto err_read_event_bits; + } + rc = ioctl(il->fd, EVIOCGKEY(sizeof(keystate)), keystate); + if (rc < 0) { + error_setg(errp, "%s: failed to get global key state", il->evdev); + goto err_close; + } + for (i = 0; i < KEY_CNT; i++) { + if (keymap[i / 8] & (1 << (i % 8))) { + if (linux_is_button(i)) { + il->num_btns++; + } else { + il->num_keys++; + } + if (keystate[i / 8] & (1 << (i % 8))) { + il->keydown[i] = true; + il->keycount++; + } + } + } + } + + qemu_set_fd_handler(il->fd, input_linux_event, NULL, il); + if (il->keycount) { + /* delay grab until all keys are released */ + il->grab_request = true; + } else { + input_linux_toggle_grab(il); + } + QTAILQ_INSERT_TAIL(&inputs, il, next); + il->initialized = true; + return; + +err_read_event_bits: + error_setg(errp, "%s: failed to read event bits", il->evdev); + +err_close: + close(il->fd); + return; +} + +static void input_linux_instance_finalize(Object *obj) +{ + InputLinux *il = INPUT_LINUX(obj); + + if (il->initialized) { + QTAILQ_REMOVE(&inputs, il, next); + qemu_set_fd_handler(il->fd, NULL, NULL, NULL); + close(il->fd); + } + g_free(il->evdev); +} + +static char *input_linux_get_evdev(Object *obj, Error **errp) +{ + InputLinux *il = INPUT_LINUX(obj); + + return g_strdup(il->evdev); +} + +static void input_linux_set_evdev(Object *obj, const char *value, + Error **errp) +{ + InputLinux *il = INPUT_LINUX(obj); + + if (il->evdev) { + error_setg(errp, "evdev property already set"); + return; + } + il->evdev = g_strdup(value); +} + +static bool input_linux_get_grab_all(Object *obj, Error **errp) +{ + InputLinux *il = INPUT_LINUX(obj); + + return il->grab_all; +} + +static void input_linux_set_grab_all(Object *obj, bool value, + Error **errp) +{ + InputLinux *il = INPUT_LINUX(obj); + + il->grab_all = value; +} + +static bool input_linux_get_repeat(Object *obj, Error **errp) +{ + InputLinux *il = INPUT_LINUX(obj); + + return il->repeat; +} + +static void input_linux_set_repeat(Object *obj, bool value, + Error **errp) +{ + InputLinux *il = INPUT_LINUX(obj); + + il->repeat = value; +} + +static int input_linux_get_grab_toggle(Object *obj, Error **errp) +{ + InputLinux *il = INPUT_LINUX(obj); + + return il->grab_toggle; +} + +static void input_linux_set_grab_toggle(Object *obj, int value, + Error **errp) +{ + InputLinux *il = INPUT_LINUX(obj); + + il->grab_toggle = value; +} + +static void input_linux_instance_init(Object *obj) +{ +} + +static void input_linux_class_init(ObjectClass *oc, void *data) +{ + UserCreatableClass *ucc = USER_CREATABLE_CLASS(oc); + + ucc->complete = input_linux_complete; + + object_class_property_add_str(oc, "evdev", + input_linux_get_evdev, + input_linux_set_evdev); + object_class_property_add_bool(oc, "grab_all", + input_linux_get_grab_all, + input_linux_set_grab_all); + object_class_property_add_bool(oc, "repeat", + input_linux_get_repeat, + input_linux_set_repeat); + object_class_property_add_enum(oc, "grab-toggle", "GrabToggleKeys", + &GrabToggleKeys_lookup, + input_linux_get_grab_toggle, + input_linux_set_grab_toggle); +} + +static const TypeInfo input_linux_info = { + .name = TYPE_INPUT_LINUX, + .parent = TYPE_OBJECT, + .class_init = input_linux_class_init, + .instance_size = sizeof(InputLinux), + .instance_init = input_linux_instance_init, + .instance_finalize = input_linux_instance_finalize, + .interfaces = (InterfaceInfo[]) { + { TYPE_USER_CREATABLE }, + { } + } +}; + +static void register_types(void) +{ + type_register_static(&input_linux_info); +} + +type_init(register_types); diff --git a/ui/input.c b/ui/input.c new file mode 100644 index 000000000..8ac407dec --- /dev/null +++ b/ui/input.c @@ -0,0 +1,622 @@ +#include "qemu/osdep.h" +#include "sysemu/sysemu.h" +#include "qapi/error.h" +#include "qapi/qapi-commands-ui.h" +#include "qapi/qmp/qdict.h" +#include "qemu/error-report.h" +#include "trace.h" +#include "ui/input.h" +#include "ui/console.h" +#include "sysemu/replay.h" +#include "sysemu/runstate.h" + +struct QemuInputHandlerState { + DeviceState *dev; + QemuInputHandler *handler; + int id; + int events; + QemuConsole *con; + QTAILQ_ENTRY(QemuInputHandlerState) node; +}; + +typedef struct QemuInputEventQueue QemuInputEventQueue; +typedef QTAILQ_HEAD(QemuInputEventQueueHead, QemuInputEventQueue) + QemuInputEventQueueHead; + +struct QemuInputEventQueue { + enum { + QEMU_INPUT_QUEUE_DELAY = 1, + QEMU_INPUT_QUEUE_EVENT, + QEMU_INPUT_QUEUE_SYNC, + } type; + QEMUTimer *timer; + uint32_t delay_ms; + QemuConsole *src; + InputEvent *evt; + QTAILQ_ENTRY(QemuInputEventQueue) node; +}; + +static QTAILQ_HEAD(, QemuInputHandlerState) handlers = + QTAILQ_HEAD_INITIALIZER(handlers); +static NotifierList mouse_mode_notifiers = + NOTIFIER_LIST_INITIALIZER(mouse_mode_notifiers); + +static QemuInputEventQueueHead kbd_queue = QTAILQ_HEAD_INITIALIZER(kbd_queue); +static QEMUTimer *kbd_timer; +static uint32_t kbd_default_delay_ms = 10; +static uint32_t queue_count; +static uint32_t queue_limit = 1024; + +QemuInputHandlerState *qemu_input_handler_register(DeviceState *dev, + QemuInputHandler *handler) +{ + QemuInputHandlerState *s = g_new0(QemuInputHandlerState, 1); + static int id = 1; + + s->dev = dev; + s->handler = handler; + s->id = id++; + QTAILQ_INSERT_TAIL(&handlers, s, node); + + qemu_input_check_mode_change(); + return s; +} + +void qemu_input_handler_activate(QemuInputHandlerState *s) +{ + QTAILQ_REMOVE(&handlers, s, node); + QTAILQ_INSERT_HEAD(&handlers, s, node); + qemu_input_check_mode_change(); +} + +void qemu_input_handler_deactivate(QemuInputHandlerState *s) +{ + QTAILQ_REMOVE(&handlers, s, node); + QTAILQ_INSERT_TAIL(&handlers, s, node); + qemu_input_check_mode_change(); +} + +void qemu_input_handler_unregister(QemuInputHandlerState *s) +{ + QTAILQ_REMOVE(&handlers, s, node); + g_free(s); + qemu_input_check_mode_change(); +} + +void qemu_input_handler_bind(QemuInputHandlerState *s, + const char *device_id, int head, + Error **errp) +{ + QemuConsole *con; + Error *err = NULL; + + con = qemu_console_lookup_by_device_name(device_id, head, &err); + if (err) { + error_propagate(errp, err); + return; + } + + s->con = con; +} + +static QemuInputHandlerState* +qemu_input_find_handler(uint32_t mask, QemuConsole *con) +{ + QemuInputHandlerState *s; + + QTAILQ_FOREACH(s, &handlers, node) { + if (s->con == NULL || s->con != con) { + continue; + } + if (mask & s->handler->mask) { + return s; + } + } + + QTAILQ_FOREACH(s, &handlers, node) { + if (s->con != NULL) { + continue; + } + if (mask & s->handler->mask) { + return s; + } + } + return NULL; +} + +void qmp_input_send_event(bool has_device, const char *device, + bool has_head, int64_t head, + InputEventList *events, Error **errp) +{ + InputEventList *e; + QemuConsole *con; + Error *err = NULL; + + con = NULL; + if (has_device) { + if (!has_head) { + head = 0; + } + con = qemu_console_lookup_by_device_name(device, head, &err); + if (err) { + error_propagate(errp, err); + return; + } + } + + if (!runstate_is_running() && !runstate_check(RUN_STATE_SUSPENDED)) { + error_setg(errp, "VM not running"); + return; + } + + for (e = events; e != NULL; e = e->next) { + InputEvent *event = e->value; + + if (!qemu_input_find_handler(1 << event->type, con)) { + error_setg(errp, "Input handler not found for " + "event type %s", + InputEventKind_str(event->type)); + return; + } + } + + for (e = events; e != NULL; e = e->next) { + InputEvent *evt = e->value; + + if (evt->type == INPUT_EVENT_KIND_KEY && + evt->u.key.data->key->type == KEY_VALUE_KIND_NUMBER) { + KeyValue *key = evt->u.key.data->key; + QKeyCode code = qemu_input_key_number_to_qcode(key->u.number.data); + qemu_input_event_send_key_qcode(con, code, evt->u.key.data->down); + } else { + qemu_input_event_send(con, evt); + } + } + + qemu_input_event_sync(); +} + +static int qemu_input_transform_invert_abs_value(int value) +{ + return (int64_t)INPUT_EVENT_ABS_MAX - value + INPUT_EVENT_ABS_MIN; +} + +static void qemu_input_transform_abs_rotate(InputEvent *evt) +{ + InputMoveEvent *move = evt->u.abs.data; + switch (graphic_rotate) { + case 90: + if (move->axis == INPUT_AXIS_X) { + move->axis = INPUT_AXIS_Y; + } else if (move->axis == INPUT_AXIS_Y) { + move->axis = INPUT_AXIS_X; + move->value = qemu_input_transform_invert_abs_value(move->value); + } + break; + case 180: + move->value = qemu_input_transform_invert_abs_value(move->value); + break; + case 270: + if (move->axis == INPUT_AXIS_X) { + move->axis = INPUT_AXIS_Y; + move->value = qemu_input_transform_invert_abs_value(move->value); + } else if (move->axis == INPUT_AXIS_Y) { + move->axis = INPUT_AXIS_X; + } + break; + } +} + +static void qemu_input_event_trace(QemuConsole *src, InputEvent *evt) +{ + const char *name; + int qcode, idx = -1; + InputKeyEvent *key; + InputBtnEvent *btn; + InputMoveEvent *move; + + if (src) { + idx = qemu_console_get_index(src); + } + switch (evt->type) { + case INPUT_EVENT_KIND_KEY: + key = evt->u.key.data; + switch (key->key->type) { + case KEY_VALUE_KIND_NUMBER: + qcode = qemu_input_key_number_to_qcode(key->key->u.number.data); + name = QKeyCode_str(qcode); + trace_input_event_key_number(idx, key->key->u.number.data, + name, key->down); + break; + case KEY_VALUE_KIND_QCODE: + name = QKeyCode_str(key->key->u.qcode.data); + trace_input_event_key_qcode(idx, name, key->down); + break; + case KEY_VALUE_KIND__MAX: + /* keep gcc happy */ + break; + } + break; + case INPUT_EVENT_KIND_BTN: + btn = evt->u.btn.data; + name = InputButton_str(btn->button); + trace_input_event_btn(idx, name, btn->down); + break; + case INPUT_EVENT_KIND_REL: + move = evt->u.rel.data; + name = InputAxis_str(move->axis); + trace_input_event_rel(idx, name, move->value); + break; + case INPUT_EVENT_KIND_ABS: + move = evt->u.abs.data; + name = InputAxis_str(move->axis); + trace_input_event_abs(idx, name, move->value); + break; + case INPUT_EVENT_KIND__MAX: + /* keep gcc happy */ + break; + } +} + +static void qemu_input_queue_process(void *opaque) +{ + QemuInputEventQueueHead *queue = opaque; + QemuInputEventQueue *item; + + g_assert(!QTAILQ_EMPTY(queue)); + item = QTAILQ_FIRST(queue); + g_assert(item->type == QEMU_INPUT_QUEUE_DELAY); + QTAILQ_REMOVE(queue, item, node); + queue_count--; + g_free(item); + + while (!QTAILQ_EMPTY(queue)) { + item = QTAILQ_FIRST(queue); + switch (item->type) { + case QEMU_INPUT_QUEUE_DELAY: + timer_mod(item->timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + + item->delay_ms); + return; + case QEMU_INPUT_QUEUE_EVENT: + qemu_input_event_send(item->src, item->evt); + qapi_free_InputEvent(item->evt); + break; + case QEMU_INPUT_QUEUE_SYNC: + qemu_input_event_sync(); + break; + } + QTAILQ_REMOVE(queue, item, node); + queue_count--; + g_free(item); + } +} + +static void qemu_input_queue_delay(QemuInputEventQueueHead *queue, + QEMUTimer *timer, uint32_t delay_ms) +{ + QemuInputEventQueue *item = g_new0(QemuInputEventQueue, 1); + bool start_timer = QTAILQ_EMPTY(queue); + + item->type = QEMU_INPUT_QUEUE_DELAY; + item->delay_ms = delay_ms; + item->timer = timer; + QTAILQ_INSERT_TAIL(queue, item, node); + queue_count++; + + if (start_timer) { + timer_mod(item->timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + + item->delay_ms); + } +} + +static void qemu_input_queue_event(QemuInputEventQueueHead *queue, + QemuConsole *src, InputEvent *evt) +{ + QemuInputEventQueue *item = g_new0(QemuInputEventQueue, 1); + + item->type = QEMU_INPUT_QUEUE_EVENT; + item->src = src; + item->evt = evt; + QTAILQ_INSERT_TAIL(queue, item, node); + queue_count++; +} + +static void qemu_input_queue_sync(QemuInputEventQueueHead *queue) +{ + QemuInputEventQueue *item = g_new0(QemuInputEventQueue, 1); + + item->type = QEMU_INPUT_QUEUE_SYNC; + QTAILQ_INSERT_TAIL(queue, item, node); + queue_count++; +} + +void qemu_input_event_send_impl(QemuConsole *src, InputEvent *evt) +{ + QemuInputHandlerState *s; + + qemu_input_event_trace(src, evt); + + /* pre processing */ + if (graphic_rotate && (evt->type == INPUT_EVENT_KIND_ABS)) { + qemu_input_transform_abs_rotate(evt); + } + + /* send event */ + s = qemu_input_find_handler(1 << evt->type, src); + if (!s) { + return; + } + s->handler->event(s->dev, src, evt); + s->events++; +} + +void qemu_input_event_send(QemuConsole *src, InputEvent *evt) +{ + /* Expect all parts of QEMU to send events with QCodes exclusively. + * Key numbers are only supported as end-user input via QMP */ + assert(!(evt->type == INPUT_EVENT_KIND_KEY && + evt->u.key.data->key->type == KEY_VALUE_KIND_NUMBER)); + + + /* + * 'sysrq' was mistakenly added to hack around the fact that + * the ps2 driver was not generating correct scancodes sequences + * when 'alt+print' was pressed. This flaw is now fixed and the + * 'sysrq' key serves no further purpose. We normalize it to + * 'print', so that downstream receivers of the event don't + * neeed to deal with this mistake + */ + if (evt->type == INPUT_EVENT_KIND_KEY && + evt->u.key.data->key->u.qcode.data == Q_KEY_CODE_SYSRQ) { + evt->u.key.data->key->u.qcode.data = Q_KEY_CODE_PRINT; + } + + if (!runstate_is_running() && !runstate_check(RUN_STATE_SUSPENDED)) { + return; + } + + replay_input_event(src, evt); +} + +void qemu_input_event_sync_impl(void) +{ + QemuInputHandlerState *s; + + trace_input_event_sync(); + + QTAILQ_FOREACH(s, &handlers, node) { + if (!s->events) { + continue; + } + if (s->handler->sync) { + s->handler->sync(s->dev); + } + s->events = 0; + } +} + +void qemu_input_event_sync(void) +{ + if (!runstate_is_running() && !runstate_check(RUN_STATE_SUSPENDED)) { + return; + } + + replay_input_sync_event(); +} + +static InputEvent *qemu_input_event_new_key(KeyValue *key, bool down) +{ + InputEvent *evt = g_new0(InputEvent, 1); + evt->u.key.data = g_new0(InputKeyEvent, 1); + evt->type = INPUT_EVENT_KIND_KEY; + evt->u.key.data->key = key; + evt->u.key.data->down = down; + return evt; +} + +void qemu_input_event_send_key(QemuConsole *src, KeyValue *key, bool down) +{ + InputEvent *evt; + evt = qemu_input_event_new_key(key, down); + if (QTAILQ_EMPTY(&kbd_queue)) { + qemu_input_event_send(src, evt); + qemu_input_event_sync(); + qapi_free_InputEvent(evt); + } else if (queue_count < queue_limit) { + qemu_input_queue_event(&kbd_queue, src, evt); + qemu_input_queue_sync(&kbd_queue); + } else { + qapi_free_InputEvent(evt); + } +} + +void qemu_input_event_send_key_number(QemuConsole *src, int num, bool down) +{ + QKeyCode code = qemu_input_key_number_to_qcode(num); + qemu_input_event_send_key_qcode(src, code, down); +} + +void qemu_input_event_send_key_qcode(QemuConsole *src, QKeyCode q, bool down) +{ + KeyValue *key = g_new0(KeyValue, 1); + key->type = KEY_VALUE_KIND_QCODE; + key->u.qcode.data = q; + qemu_input_event_send_key(src, key, down); +} + +void qemu_input_event_send_key_delay(uint32_t delay_ms) +{ + if (!runstate_is_running() && !runstate_check(RUN_STATE_SUSPENDED)) { + return; + } + + if (!kbd_timer) { + kbd_timer = timer_new_full(NULL, QEMU_CLOCK_VIRTUAL, + SCALE_MS, QEMU_TIMER_ATTR_EXTERNAL, + qemu_input_queue_process, &kbd_queue); + } + if (queue_count < queue_limit) { + qemu_input_queue_delay(&kbd_queue, kbd_timer, + delay_ms ? delay_ms : kbd_default_delay_ms); + } +} + +void qemu_input_queue_btn(QemuConsole *src, InputButton btn, bool down) +{ + InputBtnEvent bevt = { + .button = btn, + .down = down, + }; + InputEvent evt = { + .type = INPUT_EVENT_KIND_BTN, + .u.btn.data = &bevt, + }; + + qemu_input_event_send(src, &evt); +} + +void qemu_input_update_buttons(QemuConsole *src, uint32_t *button_map, + uint32_t button_old, uint32_t button_new) +{ + InputButton btn; + uint32_t mask; + + for (btn = 0; btn < INPUT_BUTTON__MAX; btn++) { + mask = button_map[btn]; + if ((button_old & mask) == (button_new & mask)) { + continue; + } + qemu_input_queue_btn(src, btn, button_new & mask); + } +} + +bool qemu_input_is_absolute(void) +{ + QemuInputHandlerState *s; + + s = qemu_input_find_handler(INPUT_EVENT_MASK_REL | INPUT_EVENT_MASK_ABS, + NULL); + return (s != NULL) && (s->handler->mask & INPUT_EVENT_MASK_ABS); +} + +int qemu_input_scale_axis(int value, + int min_in, int max_in, + int min_out, int max_out) +{ + int64_t range_in = (int64_t)max_in - min_in; + int64_t range_out = (int64_t)max_out - min_out; + + if (range_in < 1) { + return min_out + range_out / 2; + } + return ((int64_t)value - min_in) * range_out / range_in + min_out; +} + +void qemu_input_queue_rel(QemuConsole *src, InputAxis axis, int value) +{ + InputMoveEvent move = { + .axis = axis, + .value = value, + }; + InputEvent evt = { + .type = INPUT_EVENT_KIND_REL, + .u.rel.data = &move, + }; + + qemu_input_event_send(src, &evt); +} + +void qemu_input_queue_abs(QemuConsole *src, InputAxis axis, int value, + int min_in, int max_in) +{ + InputMoveEvent move = { + .axis = axis, + .value = qemu_input_scale_axis(value, min_in, max_in, + INPUT_EVENT_ABS_MIN, + INPUT_EVENT_ABS_MAX), + }; + InputEvent evt = { + .type = INPUT_EVENT_KIND_ABS, + .u.abs.data = &move, + }; + + qemu_input_event_send(src, &evt); +} + +void qemu_input_check_mode_change(void) +{ + static int current_is_absolute; + int is_absolute; + + is_absolute = qemu_input_is_absolute(); + + if (is_absolute != current_is_absolute) { + trace_input_mouse_mode(is_absolute); + notifier_list_notify(&mouse_mode_notifiers, NULL); + } + + current_is_absolute = is_absolute; +} + +void qemu_add_mouse_mode_change_notifier(Notifier *notify) +{ + notifier_list_add(&mouse_mode_notifiers, notify); +} + +void qemu_remove_mouse_mode_change_notifier(Notifier *notify) +{ + notifier_remove(notify); +} + +MouseInfoList *qmp_query_mice(Error **errp) +{ + MouseInfoList *mice_list = NULL; + MouseInfo *info; + QemuInputHandlerState *s; + bool current = true; + + QTAILQ_FOREACH(s, &handlers, node) { + if (!(s->handler->mask & + (INPUT_EVENT_MASK_REL | INPUT_EVENT_MASK_ABS))) { + continue; + } + + info = g_new0(MouseInfo, 1); + info->index = s->id; + info->name = g_strdup(s->handler->name); + info->absolute = s->handler->mask & INPUT_EVENT_MASK_ABS; + info->current = current; + + current = false; + QAPI_LIST_PREPEND(mice_list, info); + } + + return mice_list; +} + +void hmp_mouse_set(Monitor *mon, const QDict *qdict) +{ + QemuInputHandlerState *s; + int index = qdict_get_int(qdict, "index"); + int found = 0; + + QTAILQ_FOREACH(s, &handlers, node) { + if (s->id != index) { + continue; + } + if (!(s->handler->mask & (INPUT_EVENT_MASK_REL | + INPUT_EVENT_MASK_ABS))) { + error_report("Input device '%s' is not a mouse", s->handler->name); + return; + } + found = 1; + qemu_input_handler_activate(s); + break; + } + + if (!found) { + error_report("Mouse at index '%d' not found", index); + } + + qemu_input_check_mode_change(); +} diff --git a/ui/kbd-state.c b/ui/kbd-state.c new file mode 100644 index 000000000..62d42a7a2 --- /dev/null +++ b/ui/kbd-state.c @@ -0,0 +1,137 @@ +/* + * This work is licensed under the terms of the GNU GPL, version 2 or + * (at your option) any later version. See the COPYING file in the + * top-level directory. + */ +#include "qemu/osdep.h" +#include "qemu/bitmap.h" +#include "ui/console.h" +#include "ui/input.h" +#include "ui/kbd-state.h" + +struct QKbdState { + QemuConsole *con; + int key_delay_ms; + DECLARE_BITMAP(keys, Q_KEY_CODE__MAX); + DECLARE_BITMAP(mods, QKBD_MOD__MAX); +}; + +static void qkbd_state_modifier_update(QKbdState *kbd, + QKeyCode qcode1, QKeyCode qcode2, + QKbdModifier mod) +{ + if (test_bit(qcode1, kbd->keys) || test_bit(qcode2, kbd->keys)) { + set_bit(mod, kbd->mods); + } else { + clear_bit(mod, kbd->mods); + } +} + +bool qkbd_state_modifier_get(QKbdState *kbd, QKbdModifier mod) +{ + return test_bit(mod, kbd->mods); +} + +bool qkbd_state_key_get(QKbdState *kbd, QKeyCode qcode) +{ + return test_bit(qcode, kbd->keys); +} + +void qkbd_state_key_event(QKbdState *kbd, QKeyCode qcode, bool down) +{ + bool state = test_bit(qcode, kbd->keys); + + if (down == false /* got key-up event */ && + state == false /* key is not pressed */) { + /* + * Filter out suspicious key-up events. + * + * This allows simply sending along all key-up events, and + * this function will filter out everything where the + * corresponding key-down event wasn't sent to the guest, for + * example due to being a host hotkey. + * + * Note that key-down events on already pressed keys are *not* + * suspicious, those are keyboard autorepeat events. + */ + return; + } + + /* update key and modifier state */ + if (down) { + set_bit(qcode, kbd->keys); + } else { + clear_bit(qcode, kbd->keys); + } + switch (qcode) { + case Q_KEY_CODE_SHIFT: + case Q_KEY_CODE_SHIFT_R: + qkbd_state_modifier_update(kbd, Q_KEY_CODE_SHIFT, Q_KEY_CODE_SHIFT_R, + QKBD_MOD_SHIFT); + break; + case Q_KEY_CODE_CTRL: + case Q_KEY_CODE_CTRL_R: + qkbd_state_modifier_update(kbd, Q_KEY_CODE_CTRL, Q_KEY_CODE_CTRL_R, + QKBD_MOD_CTRL); + break; + case Q_KEY_CODE_ALT: + qkbd_state_modifier_update(kbd, Q_KEY_CODE_ALT, Q_KEY_CODE_ALT, + QKBD_MOD_ALT); + break; + case Q_KEY_CODE_ALT_R: + qkbd_state_modifier_update(kbd, Q_KEY_CODE_ALT_R, Q_KEY_CODE_ALT_R, + QKBD_MOD_ALTGR); + break; + case Q_KEY_CODE_CAPS_LOCK: + if (down) { + change_bit(QKBD_MOD_CAPSLOCK, kbd->mods); + } + break; + case Q_KEY_CODE_NUM_LOCK: + if (down) { + change_bit(QKBD_MOD_NUMLOCK, kbd->mods); + } + break; + default: + /* keep gcc happy */ + break; + } + + /* send to guest */ + if (qemu_console_is_graphic(kbd->con)) { + qemu_input_event_send_key_qcode(kbd->con, qcode, down); + if (kbd->key_delay_ms) { + qemu_input_event_send_key_delay(kbd->key_delay_ms); + } + } +} + +void qkbd_state_lift_all_keys(QKbdState *kbd) +{ + int qcode; + + for (qcode = 0; qcode < Q_KEY_CODE__MAX; qcode++) { + if (test_bit(qcode, kbd->keys)) { + qkbd_state_key_event(kbd, qcode, false); + } + } +} + +void qkbd_state_set_delay(QKbdState *kbd, int delay_ms) +{ + kbd->key_delay_ms = delay_ms; +} + +void qkbd_state_free(QKbdState *kbd) +{ + g_free(kbd); +} + +QKbdState *qkbd_state_init(QemuConsole *con) +{ + QKbdState *kbd = g_new0(QKbdState, 1); + + kbd->con = con; + + return kbd; +} diff --git a/ui/keymaps.c b/ui/keymaps.c new file mode 100644 index 000000000..d4a647464 --- /dev/null +++ b/ui/keymaps.c @@ -0,0 +1,273 @@ +/* + * QEMU keysym to keycode conversion using rdesktop keymaps + * + * Copyright (c) 2004 Johannes Schindelin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "qemu-common.h" +#include "qemu/datadir.h" +#include "keymaps.h" +#include "trace.h" +#include "qemu/ctype.h" +#include "qemu/error-report.h" +#include "qapi/error.h" +#include "ui/input.h" + +struct keysym2code { + uint32_t count; + uint16_t keycodes[4]; +}; + +struct kbd_layout_t { + GHashTable *hash; +}; + +static int get_keysym(const name2keysym_t *table, + const char *name) +{ + const name2keysym_t *p; + for(p = table; p->name != NULL; p++) { + if (!strcmp(p->name, name)) { + return p->keysym; + } + } + if (name[0] == 'U' && strlen(name) == 5) { /* try unicode Uxxxx */ + char *end; + int ret = (int)strtoul(name + 1, &end, 16); + if (*end == '\0' && ret > 0) { + return ret; + } + } + return 0; +} + + +static void add_keysym(char *line, int keysym, int keycode, kbd_layout_t *k) +{ + struct keysym2code *keysym2code; + + keysym2code = g_hash_table_lookup(k->hash, GINT_TO_POINTER(keysym)); + if (keysym2code) { + if (keysym2code->count < ARRAY_SIZE(keysym2code->keycodes)) { + keysym2code->keycodes[keysym2code->count++] = keycode; + } else { + warn_report("more than %zd keycodes for keysym %d", + ARRAY_SIZE(keysym2code->keycodes), keysym); + } + return; + } + + keysym2code = g_new0(struct keysym2code, 1); + keysym2code->keycodes[0] = keycode; + keysym2code->count = 1; + g_hash_table_replace(k->hash, GINT_TO_POINTER(keysym), keysym2code); + trace_keymap_add(keysym, keycode, line); +} + +static int parse_keyboard_layout(kbd_layout_t *k, + const name2keysym_t *table, + const char *language, Error **errp) +{ + int ret; + FILE *f; + char * filename; + char line[1024]; + char keyname[64]; + int len; + + filename = qemu_find_file(QEMU_FILE_TYPE_KEYMAP, language); + trace_keymap_parse(filename); + f = filename ? fopen(filename, "r") : NULL; + g_free(filename); + if (!f) { + error_setg(errp, "could not read keymap file: '%s'", language); + return -1; + } + + for(;;) { + if (fgets(line, 1024, f) == NULL) { + break; + } + len = strlen(line); + if (len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; + } + if (line[0] == '#') { + continue; + } + if (!strncmp(line, "map ", 4)) { + continue; + } + if (!strncmp(line, "include ", 8)) { + error_setg(errp, "keymap include files are not supported any more"); + ret = -1; + goto out; + } else { + int offset = 0; + while (line[offset] != 0 && + line[offset] != ' ' && + offset < sizeof(keyname) - 1) { + keyname[offset] = line[offset]; + offset++; + } + keyname[offset] = 0; + if (strlen(keyname)) { + int keysym; + keysym = get_keysym(table, keyname); + if (keysym == 0) { + /* warn_report("unknown keysym %s", line);*/ + } else { + const char *rest = line + offset + 1; + int keycode = strtol(rest, NULL, 0); + + if (strstr(rest, "shift")) { + keycode |= SCANCODE_SHIFT; + } + if (strstr(rest, "altgr")) { + keycode |= SCANCODE_ALTGR; + } + if (strstr(rest, "ctrl")) { + keycode |= SCANCODE_CTRL; + } + + add_keysym(line, keysym, keycode, k); + + if (strstr(rest, "addupper")) { + char *c; + for (c = keyname; *c; c++) { + *c = qemu_toupper(*c); + } + keysym = get_keysym(table, keyname); + if (keysym) { + add_keysym(line, keysym, + keycode | SCANCODE_SHIFT, k); + } + } + } + } + } + } + + ret = 0; +out: + fclose(f); + return ret; +} + + +kbd_layout_t *init_keyboard_layout(const name2keysym_t *table, + const char *language, Error **errp) +{ + kbd_layout_t *k; + + k = g_new0(kbd_layout_t, 1); + k->hash = g_hash_table_new(NULL, NULL); + if (parse_keyboard_layout(k, table, language, errp) < 0) { + g_hash_table_unref(k->hash); + g_free(k); + return NULL; + } + return k; +} + + +int keysym2scancode(kbd_layout_t *k, int keysym, + QKbdState *kbd, bool down) +{ + static const uint32_t mask = + SCANCODE_SHIFT | SCANCODE_ALTGR | SCANCODE_CTRL; + uint32_t mods, i; + struct keysym2code *keysym2code; + +#ifdef XK_ISO_Left_Tab + if (keysym == XK_ISO_Left_Tab) { + keysym = XK_Tab; + } +#endif + + keysym2code = g_hash_table_lookup(k->hash, GINT_TO_POINTER(keysym)); + if (!keysym2code) { + trace_keymap_unmapped(keysym); + warn_report("no scancode found for keysym %d", keysym); + return 0; + } + + if (keysym2code->count == 1) { + return keysym2code->keycodes[0]; + } + + /* We have multiple keysym -> keycode mappings. */ + if (down) { + /* + * On keydown: Check whenever we find one mapping where the + * modifier state of the mapping matches the current user + * interface modifier state. If so, prefer that one. + */ + mods = 0; + if (kbd && qkbd_state_modifier_get(kbd, QKBD_MOD_SHIFT)) { + mods |= SCANCODE_SHIFT; + } + if (kbd && qkbd_state_modifier_get(kbd, QKBD_MOD_ALTGR)) { + mods |= SCANCODE_ALTGR; + } + if (kbd && qkbd_state_modifier_get(kbd, QKBD_MOD_CTRL)) { + mods |= SCANCODE_CTRL; + } + + for (i = 0; i < keysym2code->count; i++) { + if ((keysym2code->keycodes[i] & mask) == mods) { + return keysym2code->keycodes[i]; + } + } + } else { + /* + * On keyup: Try find a key which is actually down. + */ + for (i = 0; i < keysym2code->count; i++) { + QKeyCode qcode = qemu_input_key_number_to_qcode + (keysym2code->keycodes[i]); + if (kbd && qkbd_state_key_get(kbd, qcode)) { + return keysym2code->keycodes[i]; + } + } + } + return keysym2code->keycodes[0]; +} + +int keycode_is_keypad(kbd_layout_t *k, int keycode) +{ + if (keycode >= 0x47 && keycode <= 0x53) { + return true; + } + return false; +} + +int keysym_is_numlock(kbd_layout_t *k, int keysym) +{ + switch (keysym) { + case 0xffb0 ... 0xffb9: /* KP_0 .. KP_9 */ + case 0xffac: /* KP_Separator */ + case 0xffae: /* KP_Decimal */ + return true; + } + return false; +} diff --git a/ui/keymaps.h b/ui/keymaps.h new file mode 100644 index 000000000..647340548 --- /dev/null +++ b/ui/keymaps.h @@ -0,0 +1,62 @@ +/* + * QEMU keysym to keycode conversion using rdesktop keymaps + * + * Copyright (c) 2004 Johannes Schindelin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef QEMU_KEYMAPS_H +#define QEMU_KEYMAPS_H + +#include "ui/kbd-state.h" + +typedef struct { + const char* name; + int keysym; +} name2keysym_t; + +/* scancode without modifiers */ +#define SCANCODE_KEYMASK 0xff +/* scancode without grey or up bit */ +#define SCANCODE_KEYCODEMASK 0x7f + +/* "grey" keys will usually need a 0xe0 prefix */ +#define SCANCODE_GREY 0x80 +#define SCANCODE_EMUL0 0xE0 +#define SCANCODE_EMUL1 0xE1 +/* "up" flag */ +#define SCANCODE_UP 0x80 + +/* Additional modifiers to use if not catched another way. */ +#define SCANCODE_SHIFT 0x100 +#define SCANCODE_CTRL 0x200 +#define SCANCODE_ALT 0x400 +#define SCANCODE_ALTGR 0x800 + +typedef struct kbd_layout_t kbd_layout_t; + +kbd_layout_t *init_keyboard_layout(const name2keysym_t *table, + const char *language, Error **errp); +int keysym2scancode(kbd_layout_t *k, int keysym, + QKbdState *kbd, bool down); +int keycode_is_keypad(kbd_layout_t *k, int keycode); +int keysym_is_numlock(kbd_layout_t *k, int keysym); + +#endif /* QEMU_KEYMAPS_H */ diff --git a/ui/meson.build b/ui/meson.build new file mode 100644 index 000000000..ee8ef2771 --- /dev/null +++ b/ui/meson.build @@ -0,0 +1,151 @@ +softmmu_ss.add(pixman) +specific_ss.add(when: ['CONFIG_SOFTMMU'], if_true: pixman) # for the include path + +softmmu_ss.add(files( + 'clipboard.c', + 'console.c', + 'cursor.c', + 'input-keymap.c', + 'input-legacy.c', + 'input-barrier.c', + 'input.c', + 'kbd-state.c', + 'keymaps.c', + 'qemu-pixman.c', +)) +softmmu_ss.add([spice_headers, files('spice-module.c')]) +softmmu_ss.add(when: spice_protocol, if_true: files('vdagent.c')) + +softmmu_ss.add(when: 'CONFIG_LINUX', if_true: files( + 'input-linux.c', + 'udmabuf.c', +)) +softmmu_ss.add(when: cocoa, if_true: files('cocoa.m')) + +vnc_ss = ss.source_set() +vnc_ss.add(files( + 'vnc.c', + 'vnc-enc-zlib.c', + 'vnc-enc-hextile.c', + 'vnc-enc-tight.c', + 'vnc-palette.c', + 'vnc-enc-zrle.c', + 'vnc-auth-vencrypt.c', + 'vnc-ws.c', + 'vnc-jobs.c', + 'vnc-clipboard.c', +)) +vnc_ss.add(zlib, png, jpeg, gnutls) +vnc_ss.add(when: sasl, if_true: files('vnc-auth-sasl.c')) +softmmu_ss.add_all(when: vnc, if_true: vnc_ss) +softmmu_ss.add(when: vnc, if_false: files('vnc-stubs.c')) +specific_ss.add(when: ['CONFIG_SOFTMMU'], if_true: opengl) + +ui_modules = {} + +if curses.found() + curses_ss = ss.source_set() + curses_ss.add(when: [curses, iconv], if_true: [files('curses.c'), pixman]) + ui_modules += {'curses' : curses_ss} +endif + +if config_host.has_key('CONFIG_OPENGL') + opengl_ss = ss.source_set() + opengl_ss.add(gbm) + opengl_ss.add(when: [opengl, pixman, 'CONFIG_OPENGL'], + if_true: files('shader.c', 'console-gl.c', 'egl-helpers.c', 'egl-context.c')) + ui_modules += {'opengl' : opengl_ss} +endif + +if config_host.has_key('CONFIG_OPENGL') and gbm.found() + egl_headless_ss = ss.source_set() + egl_headless_ss.add(when: [opengl, gbm, pixman, 'CONFIG_OPENGL'], + if_true: files('egl-headless.c')) + ui_modules += {'egl-headless' : egl_headless_ss} +endif + +if gtk.found() + softmmu_ss.add(when: 'CONFIG_WIN32', if_true: files('win32-kbd-hook.c')) + + gtk_ss = ss.source_set() + gtk_ss.add(gtk, vte, pixman, files('gtk.c', 'gtk-clipboard.c')) + gtk_ss.add(when: x11, if_true: files('x_keymap.c')) + gtk_ss.add(when: [opengl, 'CONFIG_OPENGL'], if_true: files('gtk-gl-area.c')) + gtk_ss.add(when: [x11, opengl, 'CONFIG_OPENGL'], if_true: files('gtk-egl.c')) + ui_modules += {'gtk' : gtk_ss} +endif + +if sdl.found() + softmmu_ss.add(when: 'CONFIG_WIN32', if_true: files('win32-kbd-hook.c')) + + sdl_ss = ss.source_set() + sdl_ss.add(sdl, sdl_image, pixman, glib, files( + 'sdl2-2d.c', + 'sdl2-input.c', + 'sdl2.c', + )) + sdl_ss.add(when: [opengl, 'CONFIG_OPENGL'], if_true: files('sdl2-gl.c')) + sdl_ss.add(when: x11, if_true: files('x_keymap.c')) + ui_modules += {'sdl' : sdl_ss} +endif + +if spice.found() + spice_core_ss = ss.source_set() + spice_core_ss.add(spice, pixman, files( + 'spice-core.c', + 'spice-input.c', + 'spice-display.c' + )) + ui_modules += {'spice-core' : spice_core_ss} +endif + +if spice.found() and config_host.has_key('CONFIG_GIO') + spice_ss = ss.source_set() + spice_ss.add(spice, gio, pixman, files('spice-app.c')) + ui_modules += {'spice-app': spice_ss} +endif + +keymaps = [ + ['atset1', 'qcode'], + ['linux', 'qcode'], + ['qcode', 'atset1'], + ['qcode', 'atset2'], + ['qcode', 'atset3'], + ['qcode', 'linux'], + ['qcode', 'qnum'], + ['qcode', 'sun'], + ['qnum', 'qcode'], + ['usb', 'qcode'], + ['win32', 'qcode'], + ['x11', 'qcode'], + ['xorgevdev', 'qcode'], + ['xorgkbd', 'qcode'], + ['xorgxquartz', 'qcode'], + ['xorgxwin', 'qcode'], + ['osx', 'qcode'], +] + +if have_system or xkbcommon.found() + foreach e : keymaps + output = 'input-keymap-@0@-to-@1@.c.inc'.format(e[0], e[1]) + genh += custom_target(output, + output: output, + capture: true, + input: files('keycodemapdb/data/keymaps.csv'), + command: [python, files('keycodemapdb/tools/keymap-gen'), + 'code-map', + '--lang', 'glib2', + '--varname', 'qemu_input_map_@0@_to_@1@'.format(e[0], e[1]), + '@INPUT0@', e[0], e[1]]) + endforeach +endif + +subdir('shader') + +if have_system + subdir('icons') + + install_data('qemu.desktop', install_dir: qemu_desktopdir) +endif + +modules += {'ui': ui_modules} diff --git a/ui/qemu-pixman.c b/ui/qemu-pixman.c new file mode 100644 index 000000000..3ab7e2e95 --- /dev/null +++ b/ui/qemu-pixman.c @@ -0,0 +1,280 @@ +/* + * This work is licensed under the terms of the GNU GPL, version 2 or later. + * See the COPYING file in the top-level directory. + */ + +#include "qemu/osdep.h" +#include "ui/console.h" +#include "standard-headers/drm/drm_fourcc.h" + +PixelFormat qemu_pixelformat_from_pixman(pixman_format_code_t format) +{ + PixelFormat pf; + uint8_t bpp; + + bpp = pf.bits_per_pixel = PIXMAN_FORMAT_BPP(format); + pf.bytes_per_pixel = PIXMAN_FORMAT_BPP(format) / 8; + pf.depth = PIXMAN_FORMAT_DEPTH(format); + + pf.abits = PIXMAN_FORMAT_A(format); + pf.rbits = PIXMAN_FORMAT_R(format); + pf.gbits = PIXMAN_FORMAT_G(format); + pf.bbits = PIXMAN_FORMAT_B(format); + + switch (PIXMAN_FORMAT_TYPE(format)) { + case PIXMAN_TYPE_ARGB: + pf.ashift = pf.bbits + pf.gbits + pf.rbits; + pf.rshift = pf.bbits + pf.gbits; + pf.gshift = pf.bbits; + pf.bshift = 0; + break; + case PIXMAN_TYPE_ABGR: + pf.ashift = pf.rbits + pf.gbits + pf.bbits; + pf.bshift = pf.rbits + pf.gbits; + pf.gshift = pf.rbits; + pf.rshift = 0; + break; + case PIXMAN_TYPE_BGRA: + pf.bshift = bpp - pf.bbits; + pf.gshift = bpp - (pf.bbits + pf.gbits); + pf.rshift = bpp - (pf.bbits + pf.gbits + pf.rbits); + pf.ashift = 0; + break; + case PIXMAN_TYPE_RGBA: + pf.rshift = bpp - pf.rbits; + pf.gshift = bpp - (pf.rbits + pf.gbits); + pf.bshift = bpp - (pf.rbits + pf.gbits + pf.bbits); + pf.ashift = 0; + break; + default: + g_assert_not_reached(); + break; + } + + pf.amax = (1 << pf.abits) - 1; + pf.rmax = (1 << pf.rbits) - 1; + pf.gmax = (1 << pf.gbits) - 1; + pf.bmax = (1 << pf.bbits) - 1; + pf.amask = pf.amax << pf.ashift; + pf.rmask = pf.rmax << pf.rshift; + pf.gmask = pf.gmax << pf.gshift; + pf.bmask = pf.bmax << pf.bshift; + + return pf; +} + +pixman_format_code_t qemu_default_pixman_format(int bpp, bool native_endian) +{ + if (native_endian) { + switch (bpp) { + case 15: + return PIXMAN_x1r5g5b5; + case 16: + return PIXMAN_r5g6b5; + case 24: + return PIXMAN_r8g8b8; + case 32: + return PIXMAN_x8r8g8b8; + } + } else { + switch (bpp) { + case 24: + return PIXMAN_b8g8r8; + case 32: + return PIXMAN_b8g8r8x8; + break; + } + } + return 0; +} + +/* Note: drm is little endian, pixman is native endian */ +static const struct { + uint32_t drm_format; + pixman_format_code_t pixman_format; +} drm_format_pixman_map[] = { + { DRM_FORMAT_RGB888, PIXMAN_LE_r8g8b8 }, + { DRM_FORMAT_ARGB8888, PIXMAN_LE_a8r8g8b8 }, + { DRM_FORMAT_XRGB8888, PIXMAN_LE_x8r8g8b8 } +}; + +pixman_format_code_t qemu_drm_format_to_pixman(uint32_t drm_format) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(drm_format_pixman_map); i++) { + if (drm_format == drm_format_pixman_map[i].drm_format) { + return drm_format_pixman_map[i].pixman_format; + } + } + return 0; +} + +uint32_t qemu_pixman_to_drm_format(pixman_format_code_t pixman_format) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(drm_format_pixman_map); i++) { + if (pixman_format == drm_format_pixman_map[i].pixman_format) { + return drm_format_pixman_map[i].drm_format; + } + } + return 0; +} + +int qemu_pixman_get_type(int rshift, int gshift, int bshift) +{ + int type = PIXMAN_TYPE_OTHER; + + if (rshift > gshift && gshift > bshift) { + if (bshift == 0) { + type = PIXMAN_TYPE_ARGB; + } else { + type = PIXMAN_TYPE_RGBA; + } + } else if (rshift < gshift && gshift < bshift) { + if (rshift == 0) { + type = PIXMAN_TYPE_ABGR; + } else { + type = PIXMAN_TYPE_BGRA; + } + } + return type; +} + +pixman_format_code_t qemu_pixman_get_format(PixelFormat *pf) +{ + pixman_format_code_t format; + int type; + + type = qemu_pixman_get_type(pf->rshift, pf->gshift, pf->bshift); + format = PIXMAN_FORMAT(pf->bits_per_pixel, type, + pf->abits, pf->rbits, pf->gbits, pf->bbits); + if (!pixman_format_supported_source(format)) { + return 0; + } + return format; +} + +/* + * Return true for known-good pixman conversions. + * + * UIs using pixman for format conversion can hook this into + * DisplayChangeListenerOps->dpy_gfx_check_format + */ +bool qemu_pixman_check_format(DisplayChangeListener *dcl, + pixman_format_code_t format) +{ + switch (format) { + /* 32 bpp */ + case PIXMAN_x8r8g8b8: + case PIXMAN_a8r8g8b8: + case PIXMAN_b8g8r8x8: + case PIXMAN_b8g8r8a8: + /* 24 bpp */ + case PIXMAN_r8g8b8: + case PIXMAN_b8g8r8: + /* 16 bpp */ + case PIXMAN_x1r5g5b5: + case PIXMAN_r5g6b5: + return true; + default: + return false; + } +} + +pixman_image_t *qemu_pixman_linebuf_create(pixman_format_code_t format, + int width) +{ + pixman_image_t *image = pixman_image_create_bits(format, width, 1, NULL, 0); + assert(image != NULL); + return image; +} + +/* fill linebuf from framebuffer */ +void qemu_pixman_linebuf_fill(pixman_image_t *linebuf, pixman_image_t *fb, + int width, int x, int y) +{ + pixman_image_composite(PIXMAN_OP_SRC, fb, NULL, linebuf, + x, y, 0, 0, 0, 0, width, 1); +} + +/* copy linebuf to framebuffer */ +void qemu_pixman_linebuf_copy(pixman_image_t *fb, int width, int x, int y, + pixman_image_t *linebuf) +{ + pixman_image_composite(PIXMAN_OP_SRC, linebuf, NULL, fb, + 0, 0, 0, 0, x, y, width, 1); +} + +pixman_image_t *qemu_pixman_mirror_create(pixman_format_code_t format, + pixman_image_t *image) +{ + return pixman_image_create_bits(format, + pixman_image_get_width(image), + pixman_image_get_height(image), + NULL, + pixman_image_get_stride(image)); +} + +void qemu_pixman_image_unref(pixman_image_t *image) +{ + if (image == NULL) { + return; + } + pixman_image_unref(image); +} + +pixman_color_t qemu_pixman_color(PixelFormat *pf, uint32_t color) +{ + pixman_color_t c; + + c.red = ((color & pf->rmask) >> pf->rshift) << (16 - pf->rbits); + c.green = ((color & pf->gmask) >> pf->gshift) << (16 - pf->gbits); + c.blue = ((color & pf->bmask) >> pf->bshift) << (16 - pf->bbits); + c.alpha = ((color & pf->amask) >> pf->ashift) << (16 - pf->abits); + return c; +} + +pixman_image_t *qemu_pixman_glyph_from_vgafont(int height, const uint8_t *font, + unsigned int ch) +{ + pixman_image_t *glyph; + uint8_t *data; + bool bit; + int x, y; + + glyph = pixman_image_create_bits(PIXMAN_a8, 8, height, + NULL, 0); + data = (uint8_t *)pixman_image_get_data(glyph); + + font += height * ch; + for (y = 0; y < height; y++, font++) { + for (x = 0; x < 8; x++, data++) { + bit = (*font) & (1 << (7-x)); + *data = bit ? 0xff : 0x00; + } + } + return glyph; +} + +void qemu_pixman_glyph_render(pixman_image_t *glyph, + pixman_image_t *surface, + pixman_color_t *fgcol, + pixman_color_t *bgcol, + int x, int y, int cw, int ch) +{ + pixman_image_t *ifg = pixman_image_create_solid_fill(fgcol); + pixman_image_t *ibg = pixman_image_create_solid_fill(bgcol); + + pixman_image_composite(PIXMAN_OP_SRC, ibg, NULL, surface, + 0, 0, 0, 0, + cw * x, ch * y, + cw, ch); + pixman_image_composite(PIXMAN_OP_OVER, ifg, glyph, surface, + 0, 0, 0, 0, + cw * x, ch * y, + cw, ch); + pixman_image_unref(ifg); + pixman_image_unref(ibg); +} diff --git a/ui/qemu-x509.h b/ui/qemu-x509.h new file mode 100644 index 000000000..095aec161 --- /dev/null +++ b/ui/qemu-x509.h @@ -0,0 +1,9 @@ +#ifndef QEMU_X509_H +#define QEMU_X509_H + +#define X509_CA_CERT_FILE "ca-cert.pem" +#define X509_CA_CRL_FILE "ca-crl.pem" +#define X509_SERVER_KEY_FILE "server-key.pem" +#define X509_SERVER_CERT_FILE "server-cert.pem" + +#endif /* QEMU_X509_H */ diff --git a/ui/qemu.desktop b/ui/qemu.desktop new file mode 100644 index 000000000..20f09f56b --- /dev/null +++ b/ui/qemu.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Version=1.0 +Name=QEMU +Icon=qemu +Type=Application +Terminal=false +Keywords=Emulators;Virtualization;KVM; +NoDisplay=true diff --git a/ui/sdl2-2d.c b/ui/sdl2-2d.c new file mode 100644 index 000000000..bfebbdeae --- /dev/null +++ b/ui/sdl2-2d.c @@ -0,0 +1,165 @@ +/* + * QEMU SDL display driver + * + * Copyright (c) 2003 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +/* Ported SDL 1.2 code to 2.0 by Dave Airlie. */ + +#include "qemu/osdep.h" +#include "ui/console.h" +#include "ui/input.h" +#include "ui/sdl2.h" + +void sdl2_2d_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + DisplaySurface *surf = scon->surface; + SDL_Rect rect; + size_t surface_data_offset; + assert(!scon->opengl); + + if (!scon->texture) { + return; + } + + surface_data_offset = surface_bytes_per_pixel(surf) * x + + surface_stride(surf) * y; + rect.x = x; + rect.y = y; + rect.w = w; + rect.h = h; + + SDL_UpdateTexture(scon->texture, &rect, + surface_data(surf) + surface_data_offset, + surface_stride(surf)); + SDL_RenderClear(scon->real_renderer); + SDL_RenderCopy(scon->real_renderer, scon->texture, NULL, NULL); + SDL_RenderPresent(scon->real_renderer); +} + +void sdl2_2d_switch(DisplayChangeListener *dcl, + DisplaySurface *new_surface) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + DisplaySurface *old_surface = scon->surface; + int format = 0; + + assert(!scon->opengl); + + scon->surface = new_surface; + + if (scon->texture) { + SDL_DestroyTexture(scon->texture); + scon->texture = NULL; + } + + if (is_placeholder(new_surface) && qemu_console_get_index(dcl->con)) { + sdl2_window_destroy(scon); + return; + } + + if (!scon->real_window) { + sdl2_window_create(scon); + } else if (old_surface && + ((surface_width(old_surface) != surface_width(new_surface)) || + (surface_height(old_surface) != surface_height(new_surface)))) { + sdl2_window_resize(scon); + } + + SDL_RenderSetLogicalSize(scon->real_renderer, + surface_width(new_surface), + surface_height(new_surface)); + + switch (surface_format(scon->surface)) { + case PIXMAN_x1r5g5b5: + format = SDL_PIXELFORMAT_ARGB1555; + break; + case PIXMAN_r5g6b5: + format = SDL_PIXELFORMAT_RGB565; + break; + case PIXMAN_a8r8g8b8: + case PIXMAN_x8r8g8b8: + format = SDL_PIXELFORMAT_ARGB8888; + break; + case PIXMAN_a8b8g8r8: + case PIXMAN_x8b8g8r8: + format = SDL_PIXELFORMAT_ABGR8888; + break; + case PIXMAN_r8g8b8a8: + case PIXMAN_r8g8b8x8: + format = SDL_PIXELFORMAT_RGBA8888; + break; + case PIXMAN_b8g8r8x8: + format = SDL_PIXELFORMAT_BGRX8888; + break; + case PIXMAN_b8g8r8a8: + format = SDL_PIXELFORMAT_BGRA8888; + break; + default: + g_assert_not_reached(); + } + scon->texture = SDL_CreateTexture(scon->real_renderer, format, + SDL_TEXTUREACCESS_STREAMING, + surface_width(new_surface), + surface_height(new_surface)); + sdl2_2d_redraw(scon); +} + +void sdl2_2d_refresh(DisplayChangeListener *dcl) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + + assert(!scon->opengl); + graphic_hw_update(dcl->con); + sdl2_poll_events(scon); +} + +void sdl2_2d_redraw(struct sdl2_console *scon) +{ + assert(!scon->opengl); + + if (!scon->surface) { + return; + } + sdl2_2d_update(&scon->dcl, 0, 0, + surface_width(scon->surface), + surface_height(scon->surface)); +} + +bool sdl2_2d_check_format(DisplayChangeListener *dcl, + pixman_format_code_t format) +{ + /* + * We let SDL convert for us a few more formats than, + * the native ones. Thes are the ones I have tested. + */ + return (format == PIXMAN_x8r8g8b8 || + format == PIXMAN_a8r8g8b8 || + format == PIXMAN_a8b8g8r8 || + format == PIXMAN_x8b8g8r8 || + format == PIXMAN_b8g8r8x8 || + format == PIXMAN_b8g8r8a8 || + format == PIXMAN_r8g8b8x8 || + format == PIXMAN_r8g8b8a8 || + format == PIXMAN_x1r5g5b5 || + format == PIXMAN_r5g6b5); +} diff --git a/ui/sdl2-gl.c b/ui/sdl2-gl.c new file mode 100644 index 000000000..a21d2deed --- /dev/null +++ b/ui/sdl2-gl.c @@ -0,0 +1,245 @@ +/* + * QEMU SDL display driver -- opengl support + * + * Copyright (c) 2014 Red Hat + * + * Authors: + * Gerd Hoffmann <kraxel@redhat.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "ui/console.h" +#include "ui/input.h" +#include "ui/sdl2.h" + +static void sdl2_set_scanout_mode(struct sdl2_console *scon, bool scanout) +{ + if (scon->scanout_mode == scanout) { + return; + } + + scon->scanout_mode = scanout; + if (!scon->scanout_mode) { + egl_fb_destroy(&scon->guest_fb); + if (scon->surface) { + surface_gl_destroy_texture(scon->gls, scon->surface); + surface_gl_create_texture(scon->gls, scon->surface); + } + } +} + +static void sdl2_gl_render_surface(struct sdl2_console *scon) +{ + int ww, wh; + + SDL_GL_MakeCurrent(scon->real_window, scon->winctx); + sdl2_set_scanout_mode(scon, false); + + SDL_GetWindowSize(scon->real_window, &ww, &wh); + surface_gl_setup_viewport(scon->gls, scon->surface, ww, wh); + + surface_gl_render_texture(scon->gls, scon->surface); + SDL_GL_SwapWindow(scon->real_window); + graphic_hw_gl_flushed(scon->dcl.con); +} + +void sdl2_gl_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + + assert(scon->opengl); + + SDL_GL_MakeCurrent(scon->real_window, scon->winctx); + surface_gl_update_texture(scon->gls, scon->surface, x, y, w, h); + scon->updates++; +} + +void sdl2_gl_switch(DisplayChangeListener *dcl, + DisplaySurface *new_surface) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + DisplaySurface *old_surface = scon->surface; + + assert(scon->opengl); + + SDL_GL_MakeCurrent(scon->real_window, scon->winctx); + surface_gl_destroy_texture(scon->gls, scon->surface); + + scon->surface = new_surface; + + if (is_placeholder(new_surface) && qemu_console_get_index(dcl->con)) { + qemu_gl_fini_shader(scon->gls); + scon->gls = NULL; + sdl2_window_destroy(scon); + return; + } + + if (!scon->real_window) { + sdl2_window_create(scon); + scon->gls = qemu_gl_init_shader(); + } else if (old_surface && + ((surface_width(old_surface) != surface_width(new_surface)) || + (surface_height(old_surface) != surface_height(new_surface)))) { + sdl2_window_resize(scon); + } + + surface_gl_create_texture(scon->gls, scon->surface); +} + +void sdl2_gl_refresh(DisplayChangeListener *dcl) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + + assert(scon->opengl); + + graphic_hw_update(dcl->con); + if (scon->updates && scon->real_window) { + scon->updates = 0; + sdl2_gl_render_surface(scon); + } + sdl2_poll_events(scon); +} + +void sdl2_gl_redraw(struct sdl2_console *scon) +{ + assert(scon->opengl); + + if (scon->scanout_mode) { + /* sdl2_gl_scanout_flush actually only care about + * the first argument. */ + return sdl2_gl_scanout_flush(&scon->dcl, 0, 0, 0, 0); + } + if (scon->surface) { + sdl2_gl_render_surface(scon); + } +} + +QEMUGLContext sdl2_gl_create_context(DisplayChangeListener *dcl, + QEMUGLParams *params) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + SDL_GLContext ctx; + + assert(scon->opengl); + + SDL_GL_MakeCurrent(scon->real_window, scon->winctx); + + SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1); + if (scon->opts->gl == DISPLAYGL_MODE_ON || + scon->opts->gl == DISPLAYGL_MODE_CORE) { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, + SDL_GL_CONTEXT_PROFILE_CORE); + } else if (scon->opts->gl == DISPLAYGL_MODE_ES) { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, + SDL_GL_CONTEXT_PROFILE_ES); + } + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, params->major_ver); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, params->minor_ver); + + ctx = SDL_GL_CreateContext(scon->real_window); + + /* If SDL fail to create a GL context and we use the "on" flag, + * then try to fallback to GLES. + */ + if (!ctx && scon->opts->gl == DISPLAYGL_MODE_ON) { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, + SDL_GL_CONTEXT_PROFILE_ES); + ctx = SDL_GL_CreateContext(scon->real_window); + } + return (QEMUGLContext)ctx; +} + +void sdl2_gl_destroy_context(DisplayChangeListener *dcl, QEMUGLContext ctx) +{ + SDL_GLContext sdlctx = (SDL_GLContext)ctx; + + SDL_GL_DeleteContext(sdlctx); +} + +int sdl2_gl_make_context_current(DisplayChangeListener *dcl, + QEMUGLContext ctx) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + SDL_GLContext sdlctx = (SDL_GLContext)ctx; + + assert(scon->opengl); + + return SDL_GL_MakeCurrent(scon->real_window, sdlctx); +} + +void sdl2_gl_scanout_disable(DisplayChangeListener *dcl) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + + assert(scon->opengl); + scon->w = 0; + scon->h = 0; + sdl2_set_scanout_mode(scon, false); +} + +void sdl2_gl_scanout_texture(DisplayChangeListener *dcl, + uint32_t backing_id, + bool backing_y_0_top, + uint32_t backing_width, + uint32_t backing_height, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + + assert(scon->opengl); + scon->x = x; + scon->y = y; + scon->w = w; + scon->h = h; + scon->y0_top = backing_y_0_top; + + SDL_GL_MakeCurrent(scon->real_window, scon->winctx); + + sdl2_set_scanout_mode(scon, true); + egl_fb_setup_for_tex(&scon->guest_fb, backing_width, backing_height, + backing_id, false); +} + +void sdl2_gl_scanout_flush(DisplayChangeListener *dcl, + uint32_t x, uint32_t y, uint32_t w, uint32_t h) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + int ww, wh; + + assert(scon->opengl); + if (!scon->scanout_mode) { + return; + } + if (!scon->guest_fb.framebuffer) { + return; + } + + SDL_GL_MakeCurrent(scon->real_window, scon->winctx); + + SDL_GetWindowSize(scon->real_window, &ww, &wh); + egl_fb_setup_default(&scon->win_fb, ww, wh); + egl_fb_blit(&scon->win_fb, &scon->guest_fb, !scon->y0_top); + + SDL_GL_SwapWindow(scon->real_window); + graphic_hw_gl_flushed(dcl->con); +} diff --git a/ui/sdl2-input.c b/ui/sdl2-input.c new file mode 100644 index 000000000..f06838220 --- /dev/null +++ b/ui/sdl2-input.c @@ -0,0 +1,59 @@ +/* + * QEMU SDL display driver + * + * Copyright (c) 2003 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +/* Ported SDL 1.2 code to 2.0 by Dave Airlie. */ + +#include "qemu/osdep.h" +#include "ui/console.h" +#include "ui/input.h" +#include "ui/sdl2.h" +#include "trace.h" + +void sdl2_process_key(struct sdl2_console *scon, + SDL_KeyboardEvent *ev) +{ + int qcode; + QemuConsole *con = scon->dcl.con; + + if (ev->keysym.scancode >= qemu_input_map_usb_to_qcode_len) { + return; + } + qcode = qemu_input_map_usb_to_qcode[ev->keysym.scancode]; + trace_sdl2_process_key(ev->keysym.scancode, qcode, + ev->type == SDL_KEYDOWN ? "down" : "up"); + qkbd_state_key_event(scon->kbd, qcode, ev->type == SDL_KEYDOWN); + + if (!qemu_console_is_graphic(con)) { + bool ctrl = qkbd_state_modifier_get(scon->kbd, QKBD_MOD_CTRL); + if (ev->type == SDL_KEYDOWN) { + switch (qcode) { + case Q_KEY_CODE_RET: + kbd_put_keysym_console(con, '\n'); + break; + default: + kbd_put_qcode_console(con, qcode, ctrl); + break; + } + } + } +} diff --git a/ui/sdl2.c b/ui/sdl2.c new file mode 100644 index 000000000..17c0ec30e --- /dev/null +++ b/ui/sdl2.c @@ -0,0 +1,927 @@ +/* + * QEMU SDL display driver + * + * Copyright (c) 2003 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +/* Ported SDL 1.2 code to 2.0 by Dave Airlie. */ + +#include "qemu/osdep.h" +#include "qemu/module.h" +#include "qemu/cutils.h" +#include "ui/console.h" +#include "ui/input.h" +#include "ui/sdl2.h" +#include "sysemu/runstate.h" +#include "sysemu/runstate-action.h" +#include "sysemu/sysemu.h" +#include "ui/win32-kbd-hook.h" + +static int sdl2_num_outputs; +static struct sdl2_console *sdl2_console; + +static SDL_Surface *guest_sprite_surface; +static int gui_grab; /* if true, all keyboard/mouse events are grabbed */ + +static int gui_saved_grab; +static int gui_fullscreen; +static int gui_grab_code = KMOD_LALT | KMOD_LCTRL; +static SDL_Cursor *sdl_cursor_normal; +static SDL_Cursor *sdl_cursor_hidden; +static int absolute_enabled; +static int guest_cursor; +static int guest_x, guest_y; +static SDL_Cursor *guest_sprite; +static Notifier mouse_mode_notifier; + +#define SDL2_REFRESH_INTERVAL_BUSY 10 +#define SDL2_MAX_IDLE_COUNT (2 * GUI_REFRESH_INTERVAL_DEFAULT \ + / SDL2_REFRESH_INTERVAL_BUSY + 1) + +static void sdl_update_caption(struct sdl2_console *scon); + +static struct sdl2_console *get_scon_from_window(uint32_t window_id) +{ + int i; + for (i = 0; i < sdl2_num_outputs; i++) { + if (sdl2_console[i].real_window == SDL_GetWindowFromID(window_id)) { + return &sdl2_console[i]; + } + } + return NULL; +} + +void sdl2_window_create(struct sdl2_console *scon) +{ + int flags = 0; + + if (!scon->surface) { + return; + } + assert(!scon->real_window); + + if (gui_fullscreen) { + flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; + } else { + flags |= SDL_WINDOW_RESIZABLE; + } + if (scon->hidden) { + flags |= SDL_WINDOW_HIDDEN; + } +#ifdef CONFIG_OPENGL + if (scon->opengl) { + flags |= SDL_WINDOW_OPENGL; + } +#endif + + scon->real_window = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + surface_width(scon->surface), + surface_height(scon->surface), + flags); + scon->real_renderer = SDL_CreateRenderer(scon->real_window, -1, 0); + if (scon->opengl) { + scon->winctx = SDL_GL_GetCurrentContext(); + } + sdl_update_caption(scon); +} + +void sdl2_window_destroy(struct sdl2_console *scon) +{ + if (!scon->real_window) { + return; + } + + SDL_DestroyRenderer(scon->real_renderer); + scon->real_renderer = NULL; + SDL_DestroyWindow(scon->real_window); + scon->real_window = NULL; +} + +void sdl2_window_resize(struct sdl2_console *scon) +{ + if (!scon->real_window) { + return; + } + + SDL_SetWindowSize(scon->real_window, + surface_width(scon->surface), + surface_height(scon->surface)); +} + +static void sdl2_redraw(struct sdl2_console *scon) +{ + if (scon->opengl) { +#ifdef CONFIG_OPENGL + sdl2_gl_redraw(scon); +#endif + } else { + sdl2_2d_redraw(scon); + } +} + +static void sdl_update_caption(struct sdl2_console *scon) +{ + char win_title[1024]; + char icon_title[1024]; + const char *status = ""; + + if (!runstate_is_running()) { + status = " [Stopped]"; + } else if (gui_grab) { + if (alt_grab) { + status = " - Press Ctrl-Alt-Shift-G to exit grab"; + } else if (ctrl_grab) { + status = " - Press Right-Ctrl-G to exit grab"; + } else { + status = " - Press Ctrl-Alt-G to exit grab"; + } + } + + if (qemu_name) { + snprintf(win_title, sizeof(win_title), "QEMU (%s-%d)%s", qemu_name, + scon->idx, status); + snprintf(icon_title, sizeof(icon_title), "QEMU (%s)", qemu_name); + } else { + snprintf(win_title, sizeof(win_title), "QEMU%s", status); + snprintf(icon_title, sizeof(icon_title), "QEMU"); + } + + if (scon->real_window) { + SDL_SetWindowTitle(scon->real_window, win_title); + } +} + +static void sdl_hide_cursor(struct sdl2_console *scon) +{ + if (scon->opts->has_show_cursor && scon->opts->show_cursor) { + return; + } + + SDL_ShowCursor(SDL_DISABLE); + SDL_SetCursor(sdl_cursor_hidden); + + if (!qemu_input_is_absolute()) { + SDL_SetRelativeMouseMode(SDL_TRUE); + } +} + +static void sdl_show_cursor(struct sdl2_console *scon) +{ + if (scon->opts->has_show_cursor && scon->opts->show_cursor) { + return; + } + + if (!qemu_input_is_absolute()) { + SDL_SetRelativeMouseMode(SDL_FALSE); + } + + if (guest_cursor && + (gui_grab || qemu_input_is_absolute() || absolute_enabled)) { + SDL_SetCursor(guest_sprite); + } else { + SDL_SetCursor(sdl_cursor_normal); + } + + SDL_ShowCursor(SDL_ENABLE); +} + +static void sdl_grab_start(struct sdl2_console *scon) +{ + QemuConsole *con = scon ? scon->dcl.con : NULL; + + if (!con || !qemu_console_is_graphic(con)) { + return; + } + /* + * If the application is not active, do not try to enter grab state. This + * prevents 'SDL_WM_GrabInput(SDL_GRAB_ON)' from blocking all the + * application (SDL bug). + */ + if (!(SDL_GetWindowFlags(scon->real_window) & SDL_WINDOW_INPUT_FOCUS)) { + return; + } + if (guest_cursor) { + SDL_SetCursor(guest_sprite); + if (!qemu_input_is_absolute() && !absolute_enabled) { + SDL_WarpMouseInWindow(scon->real_window, guest_x, guest_y); + } + } else { + sdl_hide_cursor(scon); + } + SDL_SetWindowGrab(scon->real_window, SDL_TRUE); + gui_grab = 1; + win32_kbd_set_grab(true); + sdl_update_caption(scon); +} + +static void sdl_grab_end(struct sdl2_console *scon) +{ + SDL_SetWindowGrab(scon->real_window, SDL_FALSE); + gui_grab = 0; + win32_kbd_set_grab(false); + sdl_show_cursor(scon); + sdl_update_caption(scon); +} + +static void absolute_mouse_grab(struct sdl2_console *scon) +{ + int mouse_x, mouse_y; + int scr_w, scr_h; + SDL_GetMouseState(&mouse_x, &mouse_y); + SDL_GetWindowSize(scon->real_window, &scr_w, &scr_h); + if (mouse_x > 0 && mouse_x < scr_w - 1 && + mouse_y > 0 && mouse_y < scr_h - 1) { + sdl_grab_start(scon); + } +} + +static void sdl_mouse_mode_change(Notifier *notify, void *data) +{ + if (qemu_input_is_absolute()) { + if (!absolute_enabled) { + absolute_enabled = 1; + SDL_SetRelativeMouseMode(SDL_FALSE); + absolute_mouse_grab(&sdl2_console[0]); + } + } else if (absolute_enabled) { + if (!gui_fullscreen) { + sdl_grab_end(&sdl2_console[0]); + } + absolute_enabled = 0; + } +} + +static void sdl_send_mouse_event(struct sdl2_console *scon, int dx, int dy, + int x, int y, int state) +{ + static uint32_t bmap[INPUT_BUTTON__MAX] = { + [INPUT_BUTTON_LEFT] = SDL_BUTTON(SDL_BUTTON_LEFT), + [INPUT_BUTTON_MIDDLE] = SDL_BUTTON(SDL_BUTTON_MIDDLE), + [INPUT_BUTTON_RIGHT] = SDL_BUTTON(SDL_BUTTON_RIGHT), + [INPUT_BUTTON_SIDE] = SDL_BUTTON(SDL_BUTTON_X1), + [INPUT_BUTTON_EXTRA] = SDL_BUTTON(SDL_BUTTON_X2) + }; + static uint32_t prev_state; + + if (prev_state != state) { + qemu_input_update_buttons(scon->dcl.con, bmap, prev_state, state); + prev_state = state; + } + + if (qemu_input_is_absolute()) { + qemu_input_queue_abs(scon->dcl.con, INPUT_AXIS_X, + x, 0, surface_width(scon->surface)); + qemu_input_queue_abs(scon->dcl.con, INPUT_AXIS_Y, + y, 0, surface_height(scon->surface)); + } else { + if (guest_cursor) { + x -= guest_x; + y -= guest_y; + guest_x += x; + guest_y += y; + dx = x; + dy = y; + } + qemu_input_queue_rel(scon->dcl.con, INPUT_AXIS_X, dx); + qemu_input_queue_rel(scon->dcl.con, INPUT_AXIS_Y, dy); + } + qemu_input_event_sync(); +} + +static void toggle_full_screen(struct sdl2_console *scon) +{ + gui_fullscreen = !gui_fullscreen; + if (gui_fullscreen) { + SDL_SetWindowFullscreen(scon->real_window, + SDL_WINDOW_FULLSCREEN_DESKTOP); + gui_saved_grab = gui_grab; + sdl_grab_start(scon); + } else { + if (!gui_saved_grab) { + sdl_grab_end(scon); + } + SDL_SetWindowFullscreen(scon->real_window, 0); + } + sdl2_redraw(scon); +} + +static int get_mod_state(void) +{ + SDL_Keymod mod = SDL_GetModState(); + + if (alt_grab) { + return (mod & (gui_grab_code | KMOD_LSHIFT)) == + (gui_grab_code | KMOD_LSHIFT); + } else if (ctrl_grab) { + return (mod & KMOD_RCTRL) == KMOD_RCTRL; + } else { + return (mod & gui_grab_code) == gui_grab_code; + } +} + +static void *sdl2_win32_get_hwnd(struct sdl2_console *scon) +{ +#ifdef CONFIG_WIN32 + SDL_SysWMinfo info; + + SDL_VERSION(&info.version); + if (SDL_GetWindowWMInfo(scon->real_window, &info)) { + return info.info.win.window; + } +#endif + return NULL; +} + +static void handle_keydown(SDL_Event *ev) +{ + int win; + struct sdl2_console *scon = get_scon_from_window(ev->key.windowID); + int gui_key_modifier_pressed = get_mod_state(); + int gui_keysym = 0; + + if (!scon) { + return; + } + + if (!scon->ignore_hotkeys && gui_key_modifier_pressed && !ev->key.repeat) { + switch (ev->key.keysym.scancode) { + case SDL_SCANCODE_2: + case SDL_SCANCODE_3: + case SDL_SCANCODE_4: + case SDL_SCANCODE_5: + case SDL_SCANCODE_6: + case SDL_SCANCODE_7: + case SDL_SCANCODE_8: + case SDL_SCANCODE_9: + if (gui_grab) { + sdl_grab_end(scon); + } + + win = ev->key.keysym.scancode - SDL_SCANCODE_1; + if (win < sdl2_num_outputs) { + sdl2_console[win].hidden = !sdl2_console[win].hidden; + if (sdl2_console[win].real_window) { + if (sdl2_console[win].hidden) { + SDL_HideWindow(sdl2_console[win].real_window); + } else { + SDL_ShowWindow(sdl2_console[win].real_window); + } + } + gui_keysym = 1; + } + break; + case SDL_SCANCODE_F: + toggle_full_screen(scon); + gui_keysym = 1; + break; + case SDL_SCANCODE_G: + gui_keysym = 1; + if (!gui_grab) { + sdl_grab_start(scon); + } else if (!gui_fullscreen) { + sdl_grab_end(scon); + } + break; + case SDL_SCANCODE_U: + sdl2_window_resize(scon); + if (!scon->opengl) { + /* re-create scon->texture */ + sdl2_2d_switch(&scon->dcl, scon->surface); + } + gui_keysym = 1; + break; +#if 0 + case SDL_SCANCODE_KP_PLUS: + case SDL_SCANCODE_KP_MINUS: + if (!gui_fullscreen) { + int scr_w, scr_h; + int width, height; + SDL_GetWindowSize(scon->real_window, &scr_w, &scr_h); + + width = MAX(scr_w + (ev->key.keysym.scancode == + SDL_SCANCODE_KP_PLUS ? 50 : -50), + 160); + height = (surface_height(scon->surface) * width) / + surface_width(scon->surface); + fprintf(stderr, "%s: scale to %dx%d\n", + __func__, width, height); + sdl_scale(scon, width, height); + sdl2_redraw(scon); + gui_keysym = 1; + } +#endif + default: + break; + } + } + if (!gui_keysym) { + sdl2_process_key(scon, &ev->key); + } +} + +static void handle_keyup(SDL_Event *ev) +{ + struct sdl2_console *scon = get_scon_from_window(ev->key.windowID); + + if (!scon) { + return; + } + + scon->ignore_hotkeys = false; + sdl2_process_key(scon, &ev->key); +} + +static void handle_textinput(SDL_Event *ev) +{ + struct sdl2_console *scon = get_scon_from_window(ev->text.windowID); + QemuConsole *con = scon ? scon->dcl.con : NULL; + + if (!con) { + return; + } + + if (qemu_console_is_graphic(con)) { + return; + } + kbd_put_string_console(con, ev->text.text, strlen(ev->text.text)); +} + +static void handle_mousemotion(SDL_Event *ev) +{ + int max_x, max_y; + struct sdl2_console *scon = get_scon_from_window(ev->motion.windowID); + + if (!scon || !qemu_console_is_graphic(scon->dcl.con)) { + return; + } + + if (qemu_input_is_absolute() || absolute_enabled) { + int scr_w, scr_h; + SDL_GetWindowSize(scon->real_window, &scr_w, &scr_h); + max_x = scr_w - 1; + max_y = scr_h - 1; + if (gui_grab && !gui_fullscreen + && (ev->motion.x == 0 || ev->motion.y == 0 || + ev->motion.x == max_x || ev->motion.y == max_y)) { + sdl_grab_end(scon); + } + if (!gui_grab && + (ev->motion.x > 0 && ev->motion.x < max_x && + ev->motion.y > 0 && ev->motion.y < max_y)) { + sdl_grab_start(scon); + } + } + if (gui_grab || qemu_input_is_absolute() || absolute_enabled) { + sdl_send_mouse_event(scon, ev->motion.xrel, ev->motion.yrel, + ev->motion.x, ev->motion.y, ev->motion.state); + } +} + +static void handle_mousebutton(SDL_Event *ev) +{ + int buttonstate = SDL_GetMouseState(NULL, NULL); + SDL_MouseButtonEvent *bev; + struct sdl2_console *scon = get_scon_from_window(ev->button.windowID); + + if (!scon || !qemu_console_is_graphic(scon->dcl.con)) { + return; + } + + bev = &ev->button; + if (!gui_grab && !qemu_input_is_absolute()) { + if (ev->type == SDL_MOUSEBUTTONUP && bev->button == SDL_BUTTON_LEFT) { + /* start grabbing all events */ + sdl_grab_start(scon); + } + } else { + if (ev->type == SDL_MOUSEBUTTONDOWN) { + buttonstate |= SDL_BUTTON(bev->button); + } else { + buttonstate &= ~SDL_BUTTON(bev->button); + } + sdl_send_mouse_event(scon, 0, 0, bev->x, bev->y, buttonstate); + } +} + +static void handle_mousewheel(SDL_Event *ev) +{ + struct sdl2_console *scon = get_scon_from_window(ev->wheel.windowID); + SDL_MouseWheelEvent *wev = &ev->wheel; + InputButton btn; + + if (!scon || !qemu_console_is_graphic(scon->dcl.con)) { + return; + } + + if (wev->y > 0) { + btn = INPUT_BUTTON_WHEEL_UP; + } else if (wev->y < 0) { + btn = INPUT_BUTTON_WHEEL_DOWN; + } else { + return; + } + + qemu_input_queue_btn(scon->dcl.con, btn, true); + qemu_input_event_sync(); + qemu_input_queue_btn(scon->dcl.con, btn, false); + qemu_input_event_sync(); +} + +static void handle_windowevent(SDL_Event *ev) +{ + struct sdl2_console *scon = get_scon_from_window(ev->window.windowID); + bool allow_close = true; + + if (!scon) { + return; + } + + switch (ev->window.event) { + case SDL_WINDOWEVENT_RESIZED: + { + QemuUIInfo info; + memset(&info, 0, sizeof(info)); + info.width = ev->window.data1; + info.height = ev->window.data2; + dpy_set_ui_info(scon->dcl.con, &info); + } + sdl2_redraw(scon); + break; + case SDL_WINDOWEVENT_EXPOSED: + sdl2_redraw(scon); + break; + case SDL_WINDOWEVENT_FOCUS_GAINED: + win32_kbd_set_grab(gui_grab); + if (qemu_console_is_graphic(scon->dcl.con)) { + win32_kbd_set_window(sdl2_win32_get_hwnd(scon)); + } + /* fall through */ + case SDL_WINDOWEVENT_ENTER: + if (!gui_grab && (qemu_input_is_absolute() || absolute_enabled)) { + absolute_mouse_grab(scon); + } + /* If a new console window opened using a hotkey receives the + * focus, SDL sends another KEYDOWN event to the new window, + * closing the console window immediately after. + * + * Work around this by ignoring further hotkey events until a + * key is released. + */ + scon->ignore_hotkeys = get_mod_state(); + break; + case SDL_WINDOWEVENT_FOCUS_LOST: + if (qemu_console_is_graphic(scon->dcl.con)) { + win32_kbd_set_window(NULL); + } + if (gui_grab && !gui_fullscreen) { + sdl_grab_end(scon); + } + break; + case SDL_WINDOWEVENT_RESTORED: + update_displaychangelistener(&scon->dcl, GUI_REFRESH_INTERVAL_DEFAULT); + break; + case SDL_WINDOWEVENT_MINIMIZED: + update_displaychangelistener(&scon->dcl, 500); + break; + case SDL_WINDOWEVENT_CLOSE: + if (qemu_console_is_graphic(scon->dcl.con)) { + if (scon->opts->has_window_close && !scon->opts->window_close) { + allow_close = false; + } + if (allow_close) { + shutdown_action = SHUTDOWN_ACTION_POWEROFF; + qemu_system_shutdown_request(SHUTDOWN_CAUSE_HOST_UI); + } + } else { + SDL_HideWindow(scon->real_window); + scon->hidden = true; + } + break; + case SDL_WINDOWEVENT_SHOWN: + scon->hidden = false; + break; + case SDL_WINDOWEVENT_HIDDEN: + scon->hidden = true; + break; + } +} + +void sdl2_poll_events(struct sdl2_console *scon) +{ + SDL_Event ev1, *ev = &ev1; + bool allow_close = true; + int idle = 1; + + if (scon->last_vm_running != runstate_is_running()) { + scon->last_vm_running = runstate_is_running(); + sdl_update_caption(scon); + } + + while (SDL_PollEvent(ev)) { + switch (ev->type) { + case SDL_KEYDOWN: + idle = 0; + handle_keydown(ev); + break; + case SDL_KEYUP: + idle = 0; + handle_keyup(ev); + break; + case SDL_TEXTINPUT: + idle = 0; + handle_textinput(ev); + break; + case SDL_QUIT: + if (scon->opts->has_window_close && !scon->opts->window_close) { + allow_close = false; + } + if (allow_close) { + shutdown_action = SHUTDOWN_ACTION_POWEROFF; + qemu_system_shutdown_request(SHUTDOWN_CAUSE_HOST_UI); + } + break; + case SDL_MOUSEMOTION: + idle = 0; + handle_mousemotion(ev); + break; + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + idle = 0; + handle_mousebutton(ev); + break; + case SDL_MOUSEWHEEL: + idle = 0; + handle_mousewheel(ev); + break; + case SDL_WINDOWEVENT: + handle_windowevent(ev); + break; + default: + break; + } + } + + if (idle) { + if (scon->idle_counter < SDL2_MAX_IDLE_COUNT) { + scon->idle_counter++; + if (scon->idle_counter >= SDL2_MAX_IDLE_COUNT) { + scon->dcl.update_interval = GUI_REFRESH_INTERVAL_DEFAULT; + } + } + } else { + scon->idle_counter = 0; + scon->dcl.update_interval = SDL2_REFRESH_INTERVAL_BUSY; + } +} + +static void sdl_mouse_warp(DisplayChangeListener *dcl, + int x, int y, int on) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + + if (!qemu_console_is_graphic(scon->dcl.con)) { + return; + } + + if (on) { + if (!guest_cursor) { + sdl_show_cursor(scon); + } + if (gui_grab || qemu_input_is_absolute() || absolute_enabled) { + SDL_SetCursor(guest_sprite); + if (!qemu_input_is_absolute() && !absolute_enabled) { + SDL_WarpMouseInWindow(scon->real_window, x, y); + } + } + } else if (gui_grab) { + sdl_hide_cursor(scon); + } + guest_cursor = on; + guest_x = x, guest_y = y; +} + +static void sdl_mouse_define(DisplayChangeListener *dcl, + QEMUCursor *c) +{ + + if (guest_sprite) { + SDL_FreeCursor(guest_sprite); + } + + if (guest_sprite_surface) { + SDL_FreeSurface(guest_sprite_surface); + } + + guest_sprite_surface = + SDL_CreateRGBSurfaceFrom(c->data, c->width, c->height, 32, c->width * 4, + 0xff0000, 0x00ff00, 0xff, 0xff000000); + + if (!guest_sprite_surface) { + fprintf(stderr, "Failed to make rgb surface from %p\n", c); + return; + } + guest_sprite = SDL_CreateColorCursor(guest_sprite_surface, + c->hot_x, c->hot_y); + if (!guest_sprite) { + fprintf(stderr, "Failed to make color cursor from %p\n", c); + return; + } + if (guest_cursor && + (gui_grab || qemu_input_is_absolute() || absolute_enabled)) { + SDL_SetCursor(guest_sprite); + } +} + +static void sdl_cleanup(void) +{ + if (guest_sprite) { + SDL_FreeCursor(guest_sprite); + } + SDL_QuitSubSystem(SDL_INIT_VIDEO); +} + +static const DisplayChangeListenerOps dcl_2d_ops = { + .dpy_name = "sdl2-2d", + .dpy_gfx_update = sdl2_2d_update, + .dpy_gfx_switch = sdl2_2d_switch, + .dpy_gfx_check_format = sdl2_2d_check_format, + .dpy_refresh = sdl2_2d_refresh, + .dpy_mouse_set = sdl_mouse_warp, + .dpy_cursor_define = sdl_mouse_define, +}; + +#ifdef CONFIG_OPENGL +static const DisplayChangeListenerOps dcl_gl_ops = { + .dpy_name = "sdl2-gl", + .dpy_gfx_update = sdl2_gl_update, + .dpy_gfx_switch = sdl2_gl_switch, + .dpy_gfx_check_format = console_gl_check_format, + .dpy_refresh = sdl2_gl_refresh, + .dpy_mouse_set = sdl_mouse_warp, + .dpy_cursor_define = sdl_mouse_define, + + .dpy_gl_ctx_create = sdl2_gl_create_context, + .dpy_gl_ctx_destroy = sdl2_gl_destroy_context, + .dpy_gl_ctx_make_current = sdl2_gl_make_context_current, + .dpy_gl_scanout_disable = sdl2_gl_scanout_disable, + .dpy_gl_scanout_texture = sdl2_gl_scanout_texture, + .dpy_gl_update = sdl2_gl_scanout_flush, +}; +#endif + +static void sdl2_display_early_init(DisplayOptions *o) +{ + assert(o->type == DISPLAY_TYPE_SDL); + if (o->has_gl && o->gl) { +#ifdef CONFIG_OPENGL + display_opengl = 1; +#endif + } +} + +static void sdl2_display_init(DisplayState *ds, DisplayOptions *o) +{ + uint8_t data = 0; + int i; + SDL_SysWMinfo info; + SDL_Surface *icon = NULL; + char *dir; + + assert(o->type == DISPLAY_TYPE_SDL); + +#ifdef __linux__ + /* on Linux, SDL may use fbcon|directfb|svgalib when run without + * accessible $DISPLAY to open X11 window. This is often the case + * when qemu is run using sudo. But in this case, and when actually + * run in X11 environment, SDL fights with X11 for the video card, + * making current display unavailable, often until reboot. + * So make x11 the default SDL video driver if this variable is unset. + * This is a bit hackish but saves us from bigger problem. + * Maybe it's a good idea to fix this in SDL instead. + */ + if (!g_setenv("SDL_VIDEODRIVER", "x11", 0)) { + fprintf(stderr, "Could not set SDL_VIDEODRIVER environment variable\n"); + exit(1); + } +#endif + + if (SDL_Init(SDL_INIT_VIDEO)) { + fprintf(stderr, "Could not initialize SDL(%s) - exiting\n", + SDL_GetError()); + exit(1); + } +#ifdef SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR /* only available since SDL 2.0.8 */ + SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0"); +#endif + SDL_SetHint(SDL_HINT_GRAB_KEYBOARD, "1"); + memset(&info, 0, sizeof(info)); + SDL_VERSION(&info.version); + + gui_fullscreen = o->has_full_screen && o->full_screen; + + for (i = 0;; i++) { + QemuConsole *con = qemu_console_lookup_by_index(i); + if (!con) { + break; + } + } + sdl2_num_outputs = i; + if (sdl2_num_outputs == 0) { + return; + } + sdl2_console = g_new0(struct sdl2_console, sdl2_num_outputs); + for (i = 0; i < sdl2_num_outputs; i++) { + QemuConsole *con = qemu_console_lookup_by_index(i); + assert(con != NULL); + if (!qemu_console_is_graphic(con) && + qemu_console_get_index(con) != 0) { + sdl2_console[i].hidden = true; + } + sdl2_console[i].idx = i; + sdl2_console[i].opts = o; +#ifdef CONFIG_OPENGL + sdl2_console[i].opengl = display_opengl; + sdl2_console[i].dcl.ops = display_opengl ? &dcl_gl_ops : &dcl_2d_ops; +#else + sdl2_console[i].opengl = 0; + sdl2_console[i].dcl.ops = &dcl_2d_ops; +#endif + sdl2_console[i].dcl.con = con; + sdl2_console[i].kbd = qkbd_state_init(con); + register_displaychangelistener(&sdl2_console[i].dcl); + +#if defined(SDL_VIDEO_DRIVER_WINDOWS) || defined(SDL_VIDEO_DRIVER_X11) + if (SDL_GetWindowWMInfo(sdl2_console[i].real_window, &info)) { +#if defined(SDL_VIDEO_DRIVER_WINDOWS) + qemu_console_set_window_id(con, (uintptr_t)info.info.win.window); +#elif defined(SDL_VIDEO_DRIVER_X11) + qemu_console_set_window_id(con, info.info.x11.window); +#endif + } +#endif + } + +#ifdef CONFIG_SDL_IMAGE + dir = get_relocated_path(CONFIG_QEMU_ICONDIR "/hicolor/128x128/apps/qemu.png"); + icon = IMG_Load(dir); +#else + /* Load a 32x32x4 image. White pixels are transparent. */ + dir = get_relocated_path(CONFIG_QEMU_ICONDIR "/hicolor/32x32/apps/qemu.bmp"); + icon = SDL_LoadBMP(dir); + if (icon) { + uint32_t colorkey = SDL_MapRGB(icon->format, 255, 255, 255); + SDL_SetColorKey(icon, SDL_TRUE, colorkey); + } +#endif + g_free(dir); + if (icon) { + SDL_SetWindowIcon(sdl2_console[0].real_window, icon); + } + + mouse_mode_notifier.notify = sdl_mouse_mode_change; + qemu_add_mouse_mode_change_notifier(&mouse_mode_notifier); + + sdl_cursor_hidden = SDL_CreateCursor(&data, &data, 8, 1, 0, 0); + sdl_cursor_normal = SDL_GetCursor(); + + if (gui_fullscreen) { + sdl_grab_start(&sdl2_console[0]); + } + + atexit(sdl_cleanup); +} + +static QemuDisplay qemu_display_sdl2 = { + .type = DISPLAY_TYPE_SDL, + .early_init = sdl2_display_early_init, + .init = sdl2_display_init, +}; + +static void register_sdl1(void) +{ + qemu_display_register(&qemu_display_sdl2); +} + +type_init(register_sdl1); + +#ifdef CONFIG_OPENGL +module_dep("ui-opengl"); +#endif diff --git a/ui/shader.c b/ui/shader.c new file mode 100644 index 000000000..e8b8d321b --- /dev/null +++ b/ui/shader.c @@ -0,0 +1,174 @@ +/* + * QEMU opengl shader helper functions + * + * Copyright (c) 2014 Red Hat + * + * Authors: + * Gerd Hoffmann <kraxel@redhat.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "qemu/osdep.h" +#include "ui/shader.h" + +#include "ui/shader/texture-blit-vert.h" +#include "ui/shader/texture-blit-flip-vert.h" +#include "ui/shader/texture-blit-frag.h" + +struct QemuGLShader { + GLint texture_blit_prog; + GLint texture_blit_flip_prog; + GLint texture_blit_vao; +}; + +/* ---------------------------------------------------------------------- */ + +static GLuint qemu_gl_init_texture_blit(GLint texture_blit_prog) +{ + static const GLfloat in_position[] = { + -1, -1, + 1, -1, + -1, 1, + 1, 1, + }; + GLint l_position; + GLuint vao, buffer; + + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + /* this is the VBO that holds the vertex data */ + glGenBuffers(1, &buffer); + glBindBuffer(GL_ARRAY_BUFFER, buffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(in_position), in_position, + GL_STATIC_DRAW); + + l_position = glGetAttribLocation(texture_blit_prog, "in_position"); + glVertexAttribPointer(l_position, 2, GL_FLOAT, GL_FALSE, 0, 0); + glEnableVertexAttribArray(l_position); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + return vao; +} + +void qemu_gl_run_texture_blit(QemuGLShader *gls, bool flip) +{ + glUseProgram(flip + ? gls->texture_blit_flip_prog + : gls->texture_blit_prog); + glBindVertexArray(gls->texture_blit_vao); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); +} + +/* ---------------------------------------------------------------------- */ + +static GLuint qemu_gl_create_compile_shader(GLenum type, const GLchar *src) +{ + GLuint shader; + GLint status, length; + char *errmsg; + + shader = glCreateShader(type); + glShaderSource(shader, 1, &src, 0); + glCompileShader(shader); + + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + if (!status) { + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length); + errmsg = g_malloc(length); + glGetShaderInfoLog(shader, length, &length, errmsg); + fprintf(stderr, "%s: compile %s error\n%s\n", __func__, + (type == GL_VERTEX_SHADER) ? "vertex" : "fragment", + errmsg); + g_free(errmsg); + return 0; + } + return shader; +} + +static GLuint qemu_gl_create_link_program(GLuint vert, GLuint frag) +{ + GLuint program; + GLint status, length; + char *errmsg; + + program = glCreateProgram(); + glAttachShader(program, vert); + glAttachShader(program, frag); + glLinkProgram(program); + + glGetProgramiv(program, GL_LINK_STATUS, &status); + if (!status) { + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length); + errmsg = g_malloc(length); + glGetProgramInfoLog(program, length, &length, errmsg); + fprintf(stderr, "%s: link program: %s\n", __func__, errmsg); + g_free(errmsg); + return 0; + } + return program; +} + +static GLuint qemu_gl_create_compile_link_program(const GLchar *vert_src, + const GLchar *frag_src) +{ + GLuint vert_shader, frag_shader, program; + + vert_shader = qemu_gl_create_compile_shader(GL_VERTEX_SHADER, vert_src); + frag_shader = qemu_gl_create_compile_shader(GL_FRAGMENT_SHADER, frag_src); + if (!vert_shader || !frag_shader) { + return 0; + } + + program = qemu_gl_create_link_program(vert_shader, frag_shader); + glDeleteShader(vert_shader); + glDeleteShader(frag_shader); + + return program; +} + +/* ---------------------------------------------------------------------- */ + +QemuGLShader *qemu_gl_init_shader(void) +{ + QemuGLShader *gls = g_new0(QemuGLShader, 1); + + gls->texture_blit_prog = qemu_gl_create_compile_link_program + (texture_blit_vert_src, texture_blit_frag_src); + gls->texture_blit_flip_prog = qemu_gl_create_compile_link_program + (texture_blit_flip_vert_src, texture_blit_frag_src); + if (!gls->texture_blit_prog || !gls->texture_blit_flip_prog) { + exit(1); + } + + gls->texture_blit_vao = + qemu_gl_init_texture_blit(gls->texture_blit_prog); + + return gls; +} + +void qemu_gl_fini_shader(QemuGLShader *gls) +{ + if (!gls) { + return; + } + g_free(gls); +} diff --git a/ui/shader/meson.build b/ui/shader/meson.build new file mode 100644 index 000000000..592bf596b --- /dev/null +++ b/ui/shader/meson.build @@ -0,0 +1,14 @@ +shaders = [ + ['texture-blit', 'frag'], + ['texture-blit', 'vert'], + ['texture-blit-flip', 'vert'], +] + +foreach e : shaders + output = '@0@-@1@.h'.format(e[0], e[1]) + genh += custom_target(output, + output: output, + capture: true, + input: files('@0@.@1@'.format(e[0], e[1])), + command: [shaderinclude, '@INPUT0@']) +endforeach diff --git a/ui/shader/texture-blit-flip.vert b/ui/shader/texture-blit-flip.vert new file mode 100644 index 000000000..ba081fa5a --- /dev/null +++ b/ui/shader/texture-blit-flip.vert @@ -0,0 +1,10 @@ + +#version 300 es + +in vec2 in_position; +out vec2 ex_tex_coord; + +void main(void) { + gl_Position = vec4(in_position, 0.0, 1.0); + ex_tex_coord = vec2(1.0 + in_position.x, 1.0 + in_position.y) * 0.5; +} diff --git a/ui/shader/texture-blit.frag b/ui/shader/texture-blit.frag new file mode 100644 index 000000000..bfa202c22 --- /dev/null +++ b/ui/shader/texture-blit.frag @@ -0,0 +1,10 @@ + +#version 300 es + +uniform sampler2D image; +in mediump vec2 ex_tex_coord; +out mediump vec4 out_frag_color; + +void main(void) { + out_frag_color = texture(image, ex_tex_coord); +} diff --git a/ui/shader/texture-blit.vert b/ui/shader/texture-blit.vert new file mode 100644 index 000000000..6fe2744d6 --- /dev/null +++ b/ui/shader/texture-blit.vert @@ -0,0 +1,10 @@ + +#version 300 es + +in vec2 in_position; +out vec2 ex_tex_coord; + +void main(void) { + gl_Position = vec4(in_position, 0.0, 1.0); + ex_tex_coord = vec2(1.0 + in_position.x, 1.0 - in_position.y) * 0.5; +} diff --git a/ui/spice-app.c b/ui/spice-app.c new file mode 100644 index 000000000..7e71e18da --- /dev/null +++ b/ui/spice-app.c @@ -0,0 +1,227 @@ +/* + * QEMU external Spice client display driver + * + * Copyright (c) 2018 Marc-André Lureau <marcandre.lureau@redhat.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" + +#include <gio/gio.h> + +#include "ui/console.h" +#include "ui/spice-display.h" +#include "qemu/config-file.h" +#include "qemu/option.h" +#include "qemu/cutils.h" +#include "qemu/module.h" +#include "qapi/error.h" +#include "io/channel-command.h" +#include "chardev/spice.h" +#include "sysemu/sysemu.h" +#include "qom/object.h" + +static const char *tmp_dir; +static char *app_dir; +static char *sock_path; + +struct VCChardev { + SpiceChardev parent; +}; + +struct VCChardevClass { + ChardevClass parent; + void (*parent_open)(Chardev *chr, ChardevBackend *backend, + bool *be_opened, Error **errp); +}; + +#define TYPE_CHARDEV_VC "chardev-vc" +OBJECT_DECLARE_TYPE(VCChardev, VCChardevClass, CHARDEV_VC) + +static ChardevBackend * +chr_spice_backend_new(void) +{ + ChardevBackend *be = g_new0(ChardevBackend, 1); + + be->type = CHARDEV_BACKEND_KIND_SPICEPORT; + be->u.spiceport.data = g_new0(ChardevSpicePort, 1); + + return be; +} + +static void vc_chr_open(Chardev *chr, + ChardevBackend *backend, + bool *be_opened, + Error **errp) +{ + VCChardevClass *vc = CHARDEV_VC_GET_CLASS(chr); + ChardevBackend *be; + const char *fqdn = NULL; + + if (strstart(chr->label, "serial", NULL)) { + fqdn = "org.qemu.console.serial.0"; + } else if (strstart(chr->label, "parallel", NULL)) { + fqdn = "org.qemu.console.parallel.0"; + } else if (strstart(chr->label, "compat_monitor", NULL)) { + fqdn = "org.qemu.monitor.hmp.0"; + } + + be = chr_spice_backend_new(); + be->u.spiceport.data->fqdn = fqdn ? + g_strdup(fqdn) : g_strdup_printf("org.qemu.console.%s", chr->label); + vc->parent_open(chr, be, be_opened, errp); + qapi_free_ChardevBackend(be); +} + +static void vc_chr_set_echo(Chardev *chr, bool echo) +{ + /* TODO: set echo for frontends QMP and qtest */ +} + +static void char_vc_class_init(ObjectClass *oc, void *data) +{ + VCChardevClass *vc = CHARDEV_VC_CLASS(oc); + ChardevClass *cc = CHARDEV_CLASS(oc); + + vc->parent_open = cc->open; + + cc->parse = qemu_chr_parse_vc; + cc->open = vc_chr_open; + cc->chr_set_echo = vc_chr_set_echo; +} + +static const TypeInfo char_vc_type_info = { + .name = TYPE_CHARDEV_VC, + .parent = TYPE_CHARDEV_SPICEPORT, + .instance_size = sizeof(VCChardev), + .class_init = char_vc_class_init, + .class_size = sizeof(VCChardevClass), +}; + +static void spice_app_atexit(void) +{ + if (sock_path) { + unlink(sock_path); + } + if (tmp_dir) { + rmdir(tmp_dir); + } + g_free(sock_path); + g_free(app_dir); +} + +static void spice_app_display_early_init(DisplayOptions *opts) +{ + QemuOpts *qopts; + QemuOptsList *list; + GError *err = NULL; + + if (opts->has_full_screen) { + error_report("spice-app full-screen isn't supported yet."); + exit(1); + } + if (opts->has_window_close) { + error_report("spice-app window-close isn't supported yet."); + exit(1); + } + + atexit(spice_app_atexit); + + if (qemu_name) { + app_dir = g_build_filename(g_get_user_runtime_dir(), + "qemu", qemu_name, NULL); + if (g_mkdir_with_parents(app_dir, S_IRWXU) < -1) { + error_report("Failed to create directory %s: %s", + app_dir, strerror(errno)); + exit(1); + } + } else { + app_dir = g_dir_make_tmp(NULL, &err); + tmp_dir = app_dir; + if (err) { + error_report("Failed to create temporary directory: %s", + err->message); + exit(1); + } + } + list = qemu_find_opts("spice"); + if (list == NULL) { + error_report("spice-app missing spice support"); + exit(1); + } + + type_register(&char_vc_type_info); + + sock_path = g_strjoin("", app_dir, "/", "spice.sock", NULL); + qopts = qemu_opts_create(list, NULL, 0, &error_abort); + qemu_opt_set(qopts, "disable-ticketing", "on", &error_abort); + qemu_opt_set(qopts, "unix", "on", &error_abort); + qemu_opt_set(qopts, "addr", sock_path, &error_abort); + qemu_opt_set(qopts, "image-compression", "off", &error_abort); + qemu_opt_set(qopts, "streaming-video", "off", &error_abort); +#ifdef HAVE_SPICE_GL + qemu_opt_set(qopts, "gl", opts->has_gl ? "on" : "off", &error_abort); + display_opengl = opts->has_gl; +#endif +} + +static void spice_app_display_init(DisplayState *ds, DisplayOptions *opts) +{ + ChardevBackend *be = chr_spice_backend_new(); + QemuOpts *qopts; + GError *err = NULL; + gchar *uri; + + be->u.spiceport.data->fqdn = g_strdup("org.qemu.monitor.qmp.0"); + qemu_chardev_new("org.qemu.monitor.qmp", TYPE_CHARDEV_SPICEPORT, + be, NULL, &error_abort); + qopts = qemu_opts_create(qemu_find_opts("mon"), + NULL, 0, &error_fatal); + qemu_opt_set(qopts, "chardev", "org.qemu.monitor.qmp", &error_abort); + qemu_opt_set(qopts, "mode", "control", &error_abort); + + qapi_free_ChardevBackend(be); + uri = g_strjoin("", "spice+unix://", app_dir, "/", "spice.sock", NULL); + info_report("Launching display with URI: %s", uri); + g_app_info_launch_default_for_uri(uri, NULL, &err); + if (err) { + error_report("Failed to launch %s URI: %s", uri, err->message); + error_report("You need a capable Spice client, " + "such as virt-viewer 8.0"); + exit(1); + } + g_free(uri); +} + +static QemuDisplay qemu_display_spice_app = { + .type = DISPLAY_TYPE_SPICE_APP, + .early_init = spice_app_display_early_init, + .init = spice_app_display_init, +}; + +static void register_spice_app(void) +{ + qemu_display_register(&qemu_display_spice_app); +} + +type_init(register_spice_app); + +module_dep("ui-spice-core"); +module_dep("chardev-spice"); diff --git a/ui/spice-core.c b/ui/spice-core.c new file mode 100644 index 000000000..31974b8d6 --- /dev/null +++ b/ui/spice-core.c @@ -0,0 +1,1039 @@ +/* + * Copyright (C) 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 or + * (at your option) version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#include "qemu/osdep.h" +#include <spice.h> + +#include "sysemu/sysemu.h" +#include "sysemu/runstate.h" +#include "ui/qemu-spice.h" +#include "qemu/error-report.h" +#include "qemu/main-loop.h" +#include "qemu/module.h" +#include "qemu/thread.h" +#include "qemu/timer.h" +#include "qemu/queue.h" +#include "qemu-x509.h" +#include "qemu/sockets.h" +#include "qapi/error.h" +#include "qapi/qapi-commands-ui.h" +#include "qapi/qapi-events-ui.h" +#include "qemu/notify.h" +#include "qemu/option.h" +#include "crypto/secret_common.h" +#include "migration/misc.h" +#include "hw/pci/pci_bus.h" +#include "ui/spice-display.h" + +/* core bits */ + +static SpiceServer *spice_server; +static Notifier migration_state; +static const char *auth = "spice"; +static char *auth_passwd; +static time_t auth_expires = TIME_MAX; +static int spice_migration_completed; +static int spice_display_is_running; +static int spice_have_target_host; + +static QemuThread me; + +struct SpiceTimer { + QEMUTimer *timer; +}; + +static SpiceTimer *timer_add(SpiceTimerFunc func, void *opaque) +{ + SpiceTimer *timer; + + timer = g_malloc0(sizeof(*timer)); + timer->timer = timer_new_ms(QEMU_CLOCK_REALTIME, func, opaque); + return timer; +} + +static void timer_start(SpiceTimer *timer, uint32_t ms) +{ + timer_mod(timer->timer, qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + ms); +} + +static void timer_cancel(SpiceTimer *timer) +{ + timer_del(timer->timer); +} + +static void timer_remove(SpiceTimer *timer) +{ + timer_free(timer->timer); + g_free(timer); +} + +struct SpiceWatch { + int fd; + SpiceWatchFunc func; + void *opaque; +}; + +static void watch_read(void *opaque) +{ + SpiceWatch *watch = opaque; + watch->func(watch->fd, SPICE_WATCH_EVENT_READ, watch->opaque); +} + +static void watch_write(void *opaque) +{ + SpiceWatch *watch = opaque; + watch->func(watch->fd, SPICE_WATCH_EVENT_WRITE, watch->opaque); +} + +static void watch_update_mask(SpiceWatch *watch, int event_mask) +{ + IOHandler *on_read = NULL; + IOHandler *on_write = NULL; + + if (event_mask & SPICE_WATCH_EVENT_READ) { + on_read = watch_read; + } + if (event_mask & SPICE_WATCH_EVENT_WRITE) { + on_write = watch_write; + } + qemu_set_fd_handler(watch->fd, on_read, on_write, watch); +} + +static SpiceWatch *watch_add(int fd, int event_mask, SpiceWatchFunc func, void *opaque) +{ + SpiceWatch *watch; + + watch = g_malloc0(sizeof(*watch)); + watch->fd = fd; + watch->func = func; + watch->opaque = opaque; + + watch_update_mask(watch, event_mask); + return watch; +} + +static void watch_remove(SpiceWatch *watch) +{ + qemu_set_fd_handler(watch->fd, NULL, NULL, NULL); + g_free(watch); +} + +typedef struct ChannelList ChannelList; +struct ChannelList { + SpiceChannelEventInfo *info; + QTAILQ_ENTRY(ChannelList) link; +}; +static QTAILQ_HEAD(, ChannelList) channel_list = QTAILQ_HEAD_INITIALIZER(channel_list); + +static void channel_list_add(SpiceChannelEventInfo *info) +{ + ChannelList *item; + + item = g_malloc0(sizeof(*item)); + item->info = info; + QTAILQ_INSERT_TAIL(&channel_list, item, link); +} + +static void channel_list_del(SpiceChannelEventInfo *info) +{ + ChannelList *item; + + QTAILQ_FOREACH(item, &channel_list, link) { + if (item->info != info) { + continue; + } + QTAILQ_REMOVE(&channel_list, item, link); + g_free(item); + return; + } +} + +static void add_addr_info(SpiceBasicInfo *info, struct sockaddr *addr, int len) +{ + char host[NI_MAXHOST], port[NI_MAXSERV]; + + getnameinfo(addr, len, host, sizeof(host), port, sizeof(port), + NI_NUMERICHOST | NI_NUMERICSERV); + + info->host = g_strdup(host); + info->port = g_strdup(port); + info->family = inet_netfamily(addr->sa_family); +} + +static void add_channel_info(SpiceChannel *sc, SpiceChannelEventInfo *info) +{ + int tls = info->flags & SPICE_CHANNEL_EVENT_FLAG_TLS; + + sc->connection_id = info->connection_id; + sc->channel_type = info->type; + sc->channel_id = info->id; + sc->tls = !!tls; +} + +static void channel_event(int event, SpiceChannelEventInfo *info) +{ + SpiceServerInfo *server = g_malloc0(sizeof(*server)); + SpiceChannel *client = g_malloc0(sizeof(*client)); + + /* + * Spice server might have called us from spice worker thread + * context (happens on display channel disconnects). Spice should + * not do that. It isn't that easy to fix it in spice and even + * when it is fixed we still should cover the already released + * spice versions. So detect that we've been called from another + * thread and grab the iothread lock if so before calling qemu + * functions. + */ + bool need_lock = !qemu_thread_is_self(&me); + if (need_lock) { + qemu_mutex_lock_iothread(); + } + + if (info->flags & SPICE_CHANNEL_EVENT_FLAG_ADDR_EXT) { + add_addr_info(qapi_SpiceChannel_base(client), + (struct sockaddr *)&info->paddr_ext, + info->plen_ext); + add_addr_info(qapi_SpiceServerInfo_base(server), + (struct sockaddr *)&info->laddr_ext, + info->llen_ext); + } else { + error_report("spice: %s, extended address is expected", + __func__); + } + + switch (event) { + case SPICE_CHANNEL_EVENT_CONNECTED: + qapi_event_send_spice_connected(qapi_SpiceServerInfo_base(server), + qapi_SpiceChannel_base(client)); + break; + case SPICE_CHANNEL_EVENT_INITIALIZED: + if (auth) { + server->has_auth = true; + server->auth = g_strdup(auth); + } + add_channel_info(client, info); + channel_list_add(info); + qapi_event_send_spice_initialized(server, client); + break; + case SPICE_CHANNEL_EVENT_DISCONNECTED: + channel_list_del(info); + qapi_event_send_spice_disconnected(qapi_SpiceServerInfo_base(server), + qapi_SpiceChannel_base(client)); + break; + default: + break; + } + + if (need_lock) { + qemu_mutex_unlock_iothread(); + } + + qapi_free_SpiceServerInfo(server); + qapi_free_SpiceChannel(client); +} + +static SpiceCoreInterface core_interface = { + .base.type = SPICE_INTERFACE_CORE, + .base.description = "qemu core services", + .base.major_version = SPICE_INTERFACE_CORE_MAJOR, + .base.minor_version = SPICE_INTERFACE_CORE_MINOR, + + .timer_add = timer_add, + .timer_start = timer_start, + .timer_cancel = timer_cancel, + .timer_remove = timer_remove, + + .watch_add = watch_add, + .watch_update_mask = watch_update_mask, + .watch_remove = watch_remove, + + .channel_event = channel_event, +}; + +static void migrate_connect_complete_cb(SpiceMigrateInstance *sin); +static void migrate_end_complete_cb(SpiceMigrateInstance *sin); + +static const SpiceMigrateInterface migrate_interface = { + .base.type = SPICE_INTERFACE_MIGRATION, + .base.description = "migration", + .base.major_version = SPICE_INTERFACE_MIGRATION_MAJOR, + .base.minor_version = SPICE_INTERFACE_MIGRATION_MINOR, + .migrate_connect_complete = migrate_connect_complete_cb, + .migrate_end_complete = migrate_end_complete_cb, +}; + +static SpiceMigrateInstance spice_migrate; + +static void migrate_connect_complete_cb(SpiceMigrateInstance *sin) +{ + /* nothing, but libspice-server expects this cb being present. */ +} + +static void migrate_end_complete_cb(SpiceMigrateInstance *sin) +{ + qapi_event_send_spice_migrate_completed(); + spice_migration_completed = true; +} + +/* config string parsing */ + +static int name2enum(const char *string, const char *table[], int entries) +{ + int i; + + if (string) { + for (i = 0; i < entries; i++) { + if (!table[i]) { + continue; + } + if (strcmp(string, table[i]) != 0) { + continue; + } + return i; + } + } + return -1; +} + +static int parse_name(const char *string, const char *optname, + const char *table[], int entries) +{ + int value = name2enum(string, table, entries); + + if (value != -1) { + return value; + } + error_report("spice: invalid %s: %s", optname, string); + exit(1); +} + +static const char *stream_video_names[] = { + [ SPICE_STREAM_VIDEO_OFF ] = "off", + [ SPICE_STREAM_VIDEO_ALL ] = "all", + [ SPICE_STREAM_VIDEO_FILTER ] = "filter", +}; +#define parse_stream_video(_name) \ + parse_name(_name, "stream video control", \ + stream_video_names, ARRAY_SIZE(stream_video_names)) + +static const char *compression_names[] = { + [ SPICE_IMAGE_COMPRESS_OFF ] = "off", + [ SPICE_IMAGE_COMPRESS_AUTO_GLZ ] = "auto_glz", + [ SPICE_IMAGE_COMPRESS_AUTO_LZ ] = "auto_lz", + [ SPICE_IMAGE_COMPRESS_QUIC ] = "quic", + [ SPICE_IMAGE_COMPRESS_GLZ ] = "glz", + [ SPICE_IMAGE_COMPRESS_LZ ] = "lz", +}; +#define parse_compression(_name) \ + parse_name(_name, "image compression", \ + compression_names, ARRAY_SIZE(compression_names)) + +static const char *wan_compression_names[] = { + [ SPICE_WAN_COMPRESSION_AUTO ] = "auto", + [ SPICE_WAN_COMPRESSION_NEVER ] = "never", + [ SPICE_WAN_COMPRESSION_ALWAYS ] = "always", +}; +#define parse_wan_compression(_name) \ + parse_name(_name, "wan compression", \ + wan_compression_names, ARRAY_SIZE(wan_compression_names)) + +/* functions for the rest of qemu */ + +static SpiceChannelList *qmp_query_spice_channels(void) +{ + SpiceChannelList *head = NULL, **tail = &head; + ChannelList *item; + + QTAILQ_FOREACH(item, &channel_list, link) { + SpiceChannel *chan; + char host[NI_MAXHOST], port[NI_MAXSERV]; + struct sockaddr *paddr; + socklen_t plen; + + assert(item->info->flags & SPICE_CHANNEL_EVENT_FLAG_ADDR_EXT); + + chan = g_malloc0(sizeof(*chan)); + + paddr = (struct sockaddr *)&item->info->paddr_ext; + plen = item->info->plen_ext; + getnameinfo(paddr, plen, + host, sizeof(host), port, sizeof(port), + NI_NUMERICHOST | NI_NUMERICSERV); + chan->host = g_strdup(host); + chan->port = g_strdup(port); + chan->family = inet_netfamily(paddr->sa_family); + + chan->connection_id = item->info->connection_id; + chan->channel_type = item->info->type; + chan->channel_id = item->info->id; + chan->tls = item->info->flags & SPICE_CHANNEL_EVENT_FLAG_TLS; + + QAPI_LIST_APPEND(tail, chan); + } + + return head; +} + +static QemuOptsList qemu_spice_opts = { + .name = "spice", + .head = QTAILQ_HEAD_INITIALIZER(qemu_spice_opts.head), + .merge_lists = true, + .desc = { + { + .name = "port", + .type = QEMU_OPT_NUMBER, + },{ + .name = "tls-port", + .type = QEMU_OPT_NUMBER, + },{ + .name = "addr", + .type = QEMU_OPT_STRING, + },{ + .name = "ipv4", + .type = QEMU_OPT_BOOL, + },{ + .name = "ipv6", + .type = QEMU_OPT_BOOL, +#ifdef SPICE_ADDR_FLAG_UNIX_ONLY + },{ + .name = "unix", + .type = QEMU_OPT_BOOL, +#endif + },{ + .name = "password", + .type = QEMU_OPT_STRING, + },{ + .name = "password-secret", + .type = QEMU_OPT_STRING, + },{ + .name = "disable-ticketing", + .type = QEMU_OPT_BOOL, + },{ + .name = "disable-copy-paste", + .type = QEMU_OPT_BOOL, + },{ + .name = "disable-agent-file-xfer", + .type = QEMU_OPT_BOOL, + },{ + .name = "sasl", + .type = QEMU_OPT_BOOL, + },{ + .name = "x509-dir", + .type = QEMU_OPT_STRING, + },{ + .name = "x509-key-file", + .type = QEMU_OPT_STRING, + },{ + .name = "x509-key-password", + .type = QEMU_OPT_STRING, + },{ + .name = "x509-cert-file", + .type = QEMU_OPT_STRING, + },{ + .name = "x509-cacert-file", + .type = QEMU_OPT_STRING, + },{ + .name = "x509-dh-key-file", + .type = QEMU_OPT_STRING, + },{ + .name = "tls-ciphers", + .type = QEMU_OPT_STRING, + },{ + .name = "tls-channel", + .type = QEMU_OPT_STRING, + },{ + .name = "plaintext-channel", + .type = QEMU_OPT_STRING, + },{ + .name = "image-compression", + .type = QEMU_OPT_STRING, + },{ + .name = "jpeg-wan-compression", + .type = QEMU_OPT_STRING, + },{ + .name = "zlib-glz-wan-compression", + .type = QEMU_OPT_STRING, + },{ + .name = "streaming-video", + .type = QEMU_OPT_STRING, + },{ + .name = "agent-mouse", + .type = QEMU_OPT_BOOL, + },{ + .name = "playback-compression", + .type = QEMU_OPT_BOOL, + },{ + .name = "seamless-migration", + .type = QEMU_OPT_BOOL, + },{ + .name = "display", + .type = QEMU_OPT_STRING, + },{ + .name = "head", + .type = QEMU_OPT_NUMBER, +#ifdef HAVE_SPICE_GL + },{ + .name = "gl", + .type = QEMU_OPT_BOOL, + },{ + .name = "rendernode", + .type = QEMU_OPT_STRING, +#endif + }, + { /* end of list */ } + }, +}; + +static SpiceInfo *qmp_query_spice_real(Error **errp) +{ + QemuOpts *opts = QTAILQ_FIRST(&qemu_spice_opts.head); + int port, tls_port; + const char *addr; + SpiceInfo *info; + unsigned int major; + unsigned int minor; + unsigned int micro; + + info = g_malloc0(sizeof(*info)); + + if (!spice_server || !opts) { + info->enabled = false; + return info; + } + + info->enabled = true; + info->migrated = spice_migration_completed; + + addr = qemu_opt_get(opts, "addr"); + port = qemu_opt_get_number(opts, "port", 0); + tls_port = qemu_opt_get_number(opts, "tls-port", 0); + + info->has_auth = true; + info->auth = g_strdup(auth); + + info->has_host = true; + info->host = g_strdup(addr ? addr : "*"); + + info->has_compiled_version = true; + major = (SPICE_SERVER_VERSION & 0xff0000) >> 16; + minor = (SPICE_SERVER_VERSION & 0xff00) >> 8; + micro = SPICE_SERVER_VERSION & 0xff; + info->compiled_version = g_strdup_printf("%d.%d.%d", major, minor, micro); + + if (port) { + info->has_port = true; + info->port = port; + } + if (tls_port) { + info->has_tls_port = true; + info->tls_port = tls_port; + } + + info->mouse_mode = spice_server_is_server_mouse(spice_server) ? + SPICE_QUERY_MOUSE_MODE_SERVER : + SPICE_QUERY_MOUSE_MODE_CLIENT; + + /* for compatibility with the original command */ + info->has_channels = true; + info->channels = qmp_query_spice_channels(); + + return info; +} + +static void migration_state_notifier(Notifier *notifier, void *data) +{ + MigrationState *s = data; + + if (!spice_have_target_host) { + return; + } + + if (migration_in_setup(s)) { + spice_server_migrate_start(spice_server); + } else if (migration_has_finished(s) || + migration_in_postcopy_after_devices(s)) { + spice_server_migrate_end(spice_server, true); + spice_have_target_host = false; + } else if (migration_has_failed(s)) { + spice_server_migrate_end(spice_server, false); + spice_have_target_host = false; + } +} + +int qemu_spice_migrate_info(const char *hostname, int port, int tls_port, + const char *subject) +{ + int ret; + + ret = spice_server_migrate_connect(spice_server, hostname, + port, tls_port, subject); + spice_have_target_host = true; + return ret; +} + +static int add_channel(void *opaque, const char *name, const char *value, + Error **errp) +{ + int security = 0; + int rc; + + if (strcmp(name, "tls-channel") == 0) { + int *tls_port = opaque; + if (!*tls_port) { + error_setg(errp, "spice: tried to setup tls-channel" + " without specifying a TLS port"); + return -1; + } + security = SPICE_CHANNEL_SECURITY_SSL; + } + if (strcmp(name, "plaintext-channel") == 0) { + security = SPICE_CHANNEL_SECURITY_NONE; + } + if (security == 0) { + return 0; + } + if (strcmp(value, "default") == 0) { + rc = spice_server_set_channel_security(spice_server, NULL, security); + } else { + rc = spice_server_set_channel_security(spice_server, value, security); + } + if (rc != 0) { + error_setg(errp, "spice: failed to set channel security for %s", + value); + return -1; + } + return 0; +} + +static void vm_change_state_handler(void *opaque, bool running, + RunState state) +{ + if (running) { + qemu_spice_display_start(); + } else if (state != RUN_STATE_PAUSED) { + qemu_spice_display_stop(); + } +} + +void qemu_spice_display_init_done(void) +{ + if (runstate_is_running()) { + qemu_spice_display_start(); + } + qemu_add_vm_change_state_handler(vm_change_state_handler, NULL); +} + +static void qemu_spice_init(void) +{ + QemuOpts *opts = QTAILQ_FIRST(&qemu_spice_opts.head); + char *password = NULL; + const char *passwordSecret; + const char *str, *x509_dir, *addr, + *x509_key_password = NULL, + *x509_dh_file = NULL, + *tls_ciphers = NULL; + char *x509_key_file = NULL, + *x509_cert_file = NULL, + *x509_cacert_file = NULL; + int port, tls_port, addr_flags; + spice_image_compression_t compression; + spice_wan_compression_t wan_compr; + bool seamless_migration; + + qemu_thread_get_self(&me); + + if (!opts) { + return; + } + port = qemu_opt_get_number(opts, "port", 0); + tls_port = qemu_opt_get_number(opts, "tls-port", 0); + if (port < 0 || port > 65535) { + error_report("spice port is out of range"); + exit(1); + } + if (tls_port < 0 || tls_port > 65535) { + error_report("spice tls-port is out of range"); + exit(1); + } + passwordSecret = qemu_opt_get(opts, "password-secret"); + if (passwordSecret) { + if (qemu_opt_get(opts, "password")) { + error_report("'password' option is mutually exclusive with " + "'password-secret'"); + exit(1); + } + password = qcrypto_secret_lookup_as_utf8(passwordSecret, + &error_fatal); + } else { + str = qemu_opt_get(opts, "password"); + if (str) { + warn_report("'password' option is deprecated and insecure, " + "use 'password-secret' instead"); + password = g_strdup(str); + } + } + + if (tls_port) { + x509_dir = qemu_opt_get(opts, "x509-dir"); + if (!x509_dir) { + x509_dir = "."; + } + + str = qemu_opt_get(opts, "x509-key-file"); + if (str) { + x509_key_file = g_strdup(str); + } else { + x509_key_file = g_strdup_printf("%s/%s", x509_dir, + X509_SERVER_KEY_FILE); + } + + str = qemu_opt_get(opts, "x509-cert-file"); + if (str) { + x509_cert_file = g_strdup(str); + } else { + x509_cert_file = g_strdup_printf("%s/%s", x509_dir, + X509_SERVER_CERT_FILE); + } + + str = qemu_opt_get(opts, "x509-cacert-file"); + if (str) { + x509_cacert_file = g_strdup(str); + } else { + x509_cacert_file = g_strdup_printf("%s/%s", x509_dir, + X509_CA_CERT_FILE); + } + + x509_key_password = qemu_opt_get(opts, "x509-key-password"); + x509_dh_file = qemu_opt_get(opts, "x509-dh-key-file"); + tls_ciphers = qemu_opt_get(opts, "tls-ciphers"); + } + + addr = qemu_opt_get(opts, "addr"); + addr_flags = 0; + if (qemu_opt_get_bool(opts, "ipv4", 0)) { + addr_flags |= SPICE_ADDR_FLAG_IPV4_ONLY; + } else if (qemu_opt_get_bool(opts, "ipv6", 0)) { + addr_flags |= SPICE_ADDR_FLAG_IPV6_ONLY; +#ifdef SPICE_ADDR_FLAG_UNIX_ONLY + } else if (qemu_opt_get_bool(opts, "unix", 0)) { + addr_flags |= SPICE_ADDR_FLAG_UNIX_ONLY; +#endif + } + + spice_server = spice_server_new(); + spice_server_set_addr(spice_server, addr ? addr : "", addr_flags); + if (port) { + spice_server_set_port(spice_server, port); + } + if (tls_port) { + spice_server_set_tls(spice_server, tls_port, + x509_cacert_file, + x509_cert_file, + x509_key_file, + x509_key_password, + x509_dh_file, + tls_ciphers); + } + if (password) { + qemu_spice.set_passwd(password, false, false); + } + if (qemu_opt_get_bool(opts, "sasl", 0)) { + if (spice_server_set_sasl(spice_server, 1) == -1) { + error_report("spice: failed to enable sasl"); + exit(1); + } + auth = "sasl"; + } + if (qemu_opt_get_bool(opts, "disable-ticketing", 0)) { + auth = "none"; + spice_server_set_noauth(spice_server); + } + + if (qemu_opt_get_bool(opts, "disable-copy-paste", 0)) { + spice_server_set_agent_copypaste(spice_server, false); + } + + if (qemu_opt_get_bool(opts, "disable-agent-file-xfer", 0)) { + spice_server_set_agent_file_xfer(spice_server, false); + } + + compression = SPICE_IMAGE_COMPRESS_AUTO_GLZ; + str = qemu_opt_get(opts, "image-compression"); + if (str) { + compression = parse_compression(str); + } + spice_server_set_image_compression(spice_server, compression); + + wan_compr = SPICE_WAN_COMPRESSION_AUTO; + str = qemu_opt_get(opts, "jpeg-wan-compression"); + if (str) { + wan_compr = parse_wan_compression(str); + } + spice_server_set_jpeg_compression(spice_server, wan_compr); + + wan_compr = SPICE_WAN_COMPRESSION_AUTO; + str = qemu_opt_get(opts, "zlib-glz-wan-compression"); + if (str) { + wan_compr = parse_wan_compression(str); + } + spice_server_set_zlib_glz_compression(spice_server, wan_compr); + + str = qemu_opt_get(opts, "streaming-video"); + if (str) { + int streaming_video = parse_stream_video(str); + spice_server_set_streaming_video(spice_server, streaming_video); + } else { + spice_server_set_streaming_video(spice_server, SPICE_STREAM_VIDEO_OFF); + } + + spice_server_set_agent_mouse + (spice_server, qemu_opt_get_bool(opts, "agent-mouse", 1)); + spice_server_set_playback_compression + (spice_server, qemu_opt_get_bool(opts, "playback-compression", 1)); + + qemu_opt_foreach(opts, add_channel, &tls_port, &error_fatal); + + spice_server_set_name(spice_server, qemu_name ?: "QEMU " QEMU_VERSION); + spice_server_set_uuid(spice_server, (unsigned char *)&qemu_uuid); + + seamless_migration = qemu_opt_get_bool(opts, "seamless-migration", 0); + spice_server_set_seamless_migration(spice_server, seamless_migration); + spice_server_set_sasl_appname(spice_server, "qemu"); + if (spice_server_init(spice_server, &core_interface) != 0) { + error_report("failed to initialize spice server"); + exit(1); + }; + using_spice = 1; + + migration_state.notify = migration_state_notifier; + add_migration_state_change_notifier(&migration_state); + spice_migrate.base.sif = &migrate_interface.base; + qemu_spice.add_interface(&spice_migrate.base); + + qemu_spice_input_init(); + + qemu_spice_display_stop(); + + g_free(x509_key_file); + g_free(x509_cert_file); + g_free(x509_cacert_file); + g_free(password); + +#ifdef HAVE_SPICE_GL + if (qemu_opt_get_bool(opts, "gl", 0)) { + if ((port != 0) || (tls_port != 0)) { + error_report("SPICE GL support is local-only for now and " + "incompatible with -spice port/tls-port"); + exit(1); + } + if (egl_rendernode_init(qemu_opt_get(opts, "rendernode"), + DISPLAYGL_MODE_ON) != 0) { + error_report("Failed to initialize EGL render node for SPICE GL"); + exit(1); + } + display_opengl = 1; + spice_opengl = 1; + } +#endif +} + +static int qemu_spice_add_interface(SpiceBaseInstance *sin) +{ + if (!spice_server) { + if (QTAILQ_FIRST(&qemu_spice_opts.head) != NULL) { + error_report("Oops: spice configured but not active"); + exit(1); + } + /* + * Create a spice server instance. + * It does *not* listen on the network. + * It handles QXL local rendering only. + * + * With a command line like '-vnc :0 -vga qxl' you'll end up here. + */ + spice_server = spice_server_new(); + spice_server_set_sasl_appname(spice_server, "qemu"); + spice_server_init(spice_server, &core_interface); + qemu_add_vm_change_state_handler(vm_change_state_handler, NULL); + } + + return spice_server_add_interface(spice_server, sin); +} + +static GSList *spice_consoles; + +bool qemu_spice_have_display_interface(QemuConsole *con) +{ + if (g_slist_find(spice_consoles, con)) { + return true; + } + return false; +} + +/* + * Recursively (in reverse order) appends addresses of PCI devices as it moves + * up in the PCI hierarchy. + * + * @returns true on success, false when the buffer wasn't large enough + */ +static bool append_pci_address(char *buf, size_t buf_size, const PCIDevice *pci) +{ + PCIBus *bus = pci_get_bus(pci); + /* + * equivalent to if (!pci_bus_is_root(bus)), but the function is not built + * with PCI_CONFIG=n, avoid using an #ifdef by checking directly + */ + if (bus->parent_dev != NULL) { + append_pci_address(buf, buf_size, bus->parent_dev); + } + + size_t len = strlen(buf); + ssize_t written = snprintf(buf + len, buf_size - len, "/%02x.%x", + PCI_SLOT(pci->devfn), PCI_FUNC(pci->devfn)); + + return written > 0 && written < buf_size - len; +} + +bool qemu_spice_fill_device_address(QemuConsole *con, + char *device_address, + size_t size) +{ + DeviceState *dev = DEVICE(object_property_get_link(OBJECT(con), + "device", + &error_abort)); + PCIDevice *pci = (PCIDevice *) object_dynamic_cast(OBJECT(dev), + TYPE_PCI_DEVICE); + + if (pci == NULL) { + warn_report("Setting device address of a display device to SPICE: " + "Not a PCI device."); + return false; + } + + strncpy(device_address, "pci/0000", size); + if (!append_pci_address(device_address, size, pci)) { + warn_report("Setting device address of a display device to SPICE: " + "Too many PCI devices in the chain."); + return false; + } + + return true; +} + +int qemu_spice_add_display_interface(QXLInstance *qxlin, QemuConsole *con) +{ + if (g_slist_find(spice_consoles, con)) { + return -1; + } + qxlin->id = qemu_console_get_index(con); + spice_consoles = g_slist_append(spice_consoles, con); + return qemu_spice_add_interface(&qxlin->base); +} + +static int qemu_spice_set_ticket(bool fail_if_conn, bool disconnect_if_conn) +{ + time_t lifetime, now = time(NULL); + char *passwd; + + if (now < auth_expires) { + passwd = auth_passwd; + lifetime = (auth_expires - now); + if (lifetime > INT_MAX) { + lifetime = INT_MAX; + } + } else { + passwd = NULL; + lifetime = 1; + } + return spice_server_set_ticket(spice_server, passwd, lifetime, + fail_if_conn, disconnect_if_conn); +} + +static int qemu_spice_set_passwd(const char *passwd, + bool fail_if_conn, bool disconnect_if_conn) +{ + if (strcmp(auth, "spice") != 0) { + return -1; + } + + g_free(auth_passwd); + auth_passwd = g_strdup(passwd); + return qemu_spice_set_ticket(fail_if_conn, disconnect_if_conn); +} + +static int qemu_spice_set_pw_expire(time_t expires) +{ + auth_expires = expires; + return qemu_spice_set_ticket(false, false); +} + +static int qemu_spice_display_add_client(int csock, int skipauth, int tls) +{ + if (tls) { + return spice_server_add_ssl_client(spice_server, csock, skipauth); + } else { + return spice_server_add_client(spice_server, csock, skipauth); + } +} + +void qemu_spice_display_start(void) +{ + if (spice_display_is_running) { + return; + } + + spice_display_is_running = true; + spice_server_vm_start(spice_server); +} + +void qemu_spice_display_stop(void) +{ + if (!spice_display_is_running) { + return; + } + + spice_server_vm_stop(spice_server); + spice_display_is_running = false; +} + +int qemu_spice_display_is_running(SimpleSpiceDisplay *ssd) +{ + return spice_display_is_running; +} + +static struct QemuSpiceOps real_spice_ops = { + .init = qemu_spice_init, + .display_init = qemu_spice_display_init, + .migrate_info = qemu_spice_migrate_info, + .set_passwd = qemu_spice_set_passwd, + .set_pw_expire = qemu_spice_set_pw_expire, + .display_add_client = qemu_spice_display_add_client, + .add_interface = qemu_spice_add_interface, + .qmp_query = qmp_query_spice_real, +}; + +static void spice_register_config(void) +{ + qemu_spice = real_spice_ops; + qemu_add_opts(&qemu_spice_opts); +} +opts_init(spice_register_config); +module_opts("spice"); + +#ifdef HAVE_SPICE_GL +module_dep("ui-opengl"); +#endif diff --git a/ui/spice-display.c b/ui/spice-display.c new file mode 100644 index 000000000..f59c69882 --- /dev/null +++ b/ui/spice-display.c @@ -0,0 +1,1199 @@ +/* + * Copyright (C) 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 or + * (at your option) version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#include "qemu/osdep.h" +#include "ui/qemu-spice.h" +#include "qemu/timer.h" +#include "qemu/lockable.h" +#include "qemu/main-loop.h" +#include "qemu/option.h" +#include "qemu/queue.h" +#include "ui/console.h" +#include "trace.h" + +#include "ui/spice-display.h" + +bool spice_opengl; + +int qemu_spice_rect_is_empty(const QXLRect* r) +{ + return r->top == r->bottom || r->left == r->right; +} + +void qemu_spice_rect_union(QXLRect *dest, const QXLRect *r) +{ + if (qemu_spice_rect_is_empty(r)) { + return; + } + + if (qemu_spice_rect_is_empty(dest)) { + *dest = *r; + return; + } + + dest->top = MIN(dest->top, r->top); + dest->left = MIN(dest->left, r->left); + dest->bottom = MAX(dest->bottom, r->bottom); + dest->right = MAX(dest->right, r->right); +} + +QXLCookie *qxl_cookie_new(int type, uint64_t io) +{ + QXLCookie *cookie; + + cookie = g_malloc0(sizeof(*cookie)); + cookie->type = type; + cookie->io = io; + return cookie; +} + +void qemu_spice_add_memslot(SimpleSpiceDisplay *ssd, QXLDevMemSlot *memslot, + qxl_async_io async) +{ + trace_qemu_spice_add_memslot(ssd->qxl.id, memslot->slot_id, + memslot->virt_start, memslot->virt_end, + async); + + if (async != QXL_SYNC) { + spice_qxl_add_memslot_async(&ssd->qxl, memslot, + (uintptr_t)qxl_cookie_new(QXL_COOKIE_TYPE_IO, + QXL_IO_MEMSLOT_ADD_ASYNC)); + } else { + spice_qxl_add_memslot(&ssd->qxl, memslot); + } +} + +void qemu_spice_del_memslot(SimpleSpiceDisplay *ssd, uint32_t gid, uint32_t sid) +{ + trace_qemu_spice_del_memslot(ssd->qxl.id, gid, sid); + spice_qxl_del_memslot(&ssd->qxl, gid, sid); +} + +void qemu_spice_create_primary_surface(SimpleSpiceDisplay *ssd, uint32_t id, + QXLDevSurfaceCreate *surface, + qxl_async_io async) +{ + trace_qemu_spice_create_primary_surface(ssd->qxl.id, id, surface, async); + if (async != QXL_SYNC) { + spice_qxl_create_primary_surface_async(&ssd->qxl, id, surface, + (uintptr_t)qxl_cookie_new(QXL_COOKIE_TYPE_IO, + QXL_IO_CREATE_PRIMARY_ASYNC)); + } else { + spice_qxl_create_primary_surface(&ssd->qxl, id, surface); + } +} + +void qemu_spice_destroy_primary_surface(SimpleSpiceDisplay *ssd, + uint32_t id, qxl_async_io async) +{ + trace_qemu_spice_destroy_primary_surface(ssd->qxl.id, id, async); + if (async != QXL_SYNC) { + spice_qxl_destroy_primary_surface_async(&ssd->qxl, id, + (uintptr_t)qxl_cookie_new(QXL_COOKIE_TYPE_IO, + QXL_IO_DESTROY_PRIMARY_ASYNC)); + } else { + spice_qxl_destroy_primary_surface(&ssd->qxl, id); + } +} + +void qemu_spice_wakeup(SimpleSpiceDisplay *ssd) +{ + trace_qemu_spice_wakeup(ssd->qxl.id); + spice_qxl_wakeup(&ssd->qxl); +} + +static void qemu_spice_create_one_update(SimpleSpiceDisplay *ssd, + QXLRect *rect) +{ + SimpleSpiceUpdate *update; + QXLDrawable *drawable; + QXLImage *image; + QXLCommand *cmd; + int bw, bh; + struct timespec time_space; + pixman_image_t *dest; + + trace_qemu_spice_create_update( + rect->left, rect->right, + rect->top, rect->bottom); + + update = g_malloc0(sizeof(*update)); + drawable = &update->drawable; + image = &update->image; + cmd = &update->ext.cmd; + + bw = rect->right - rect->left; + bh = rect->bottom - rect->top; + update->bitmap = g_malloc(bw * bh * 4); + + drawable->bbox = *rect; + drawable->clip.type = SPICE_CLIP_TYPE_NONE; + drawable->effect = QXL_EFFECT_OPAQUE; + drawable->release_info.id = (uintptr_t)(&update->ext); + drawable->type = QXL_DRAW_COPY; + drawable->surfaces_dest[0] = -1; + drawable->surfaces_dest[1] = -1; + drawable->surfaces_dest[2] = -1; + clock_gettime(CLOCK_MONOTONIC, &time_space); + /* time in milliseconds from epoch. */ + drawable->mm_time = time_space.tv_sec * 1000 + + time_space.tv_nsec / 1000 / 1000; + + drawable->u.copy.rop_descriptor = SPICE_ROPD_OP_PUT; + drawable->u.copy.src_bitmap = (uintptr_t)image; + drawable->u.copy.src_area.right = bw; + drawable->u.copy.src_area.bottom = bh; + + QXL_SET_IMAGE_ID(image, QXL_IMAGE_GROUP_DEVICE, ssd->unique++); + image->descriptor.type = SPICE_IMAGE_TYPE_BITMAP; + image->bitmap.flags = QXL_BITMAP_DIRECT | QXL_BITMAP_TOP_DOWN; + image->bitmap.stride = bw * 4; + image->descriptor.width = image->bitmap.x = bw; + image->descriptor.height = image->bitmap.y = bh; + image->bitmap.data = (uintptr_t)(update->bitmap); + image->bitmap.palette = 0; + image->bitmap.format = SPICE_BITMAP_FMT_32BIT; + + dest = pixman_image_create_bits(PIXMAN_LE_x8r8g8b8, bw, bh, + (void *)update->bitmap, bw * 4); + pixman_image_composite(PIXMAN_OP_SRC, ssd->surface, NULL, ssd->mirror, + rect->left, rect->top, 0, 0, + rect->left, rect->top, bw, bh); + pixman_image_composite(PIXMAN_OP_SRC, ssd->mirror, NULL, dest, + rect->left, rect->top, 0, 0, + 0, 0, bw, bh); + pixman_image_unref(dest); + + cmd->type = QXL_CMD_DRAW; + cmd->data = (uintptr_t)drawable; + + QTAILQ_INSERT_TAIL(&ssd->updates, update, next); +} + +static void qemu_spice_create_update(SimpleSpiceDisplay *ssd) +{ + static const int blksize = 32; + int blocks = DIV_ROUND_UP(surface_width(ssd->ds), blksize); + int dirty_top[blocks]; + int y, yoff1, yoff2, x, xoff, blk, bw; + int bpp = surface_bytes_per_pixel(ssd->ds); + uint8_t *guest, *mirror; + + if (qemu_spice_rect_is_empty(&ssd->dirty)) { + return; + }; + + for (blk = 0; blk < blocks; blk++) { + dirty_top[blk] = -1; + } + + guest = surface_data(ssd->ds); + mirror = (void *)pixman_image_get_data(ssd->mirror); + for (y = ssd->dirty.top; y < ssd->dirty.bottom; y++) { + yoff1 = y * surface_stride(ssd->ds); + yoff2 = y * pixman_image_get_stride(ssd->mirror); + for (x = ssd->dirty.left; x < ssd->dirty.right; x += blksize) { + xoff = x * bpp; + blk = x / blksize; + bw = MIN(blksize, ssd->dirty.right - x); + if (memcmp(guest + yoff1 + xoff, + mirror + yoff2 + xoff, + bw * bpp) == 0) { + if (dirty_top[blk] != -1) { + QXLRect update = { + .top = dirty_top[blk], + .bottom = y, + .left = x, + .right = x + bw, + }; + qemu_spice_create_one_update(ssd, &update); + dirty_top[blk] = -1; + } + } else { + if (dirty_top[blk] == -1) { + dirty_top[blk] = y; + } + } + } + } + + for (x = ssd->dirty.left; x < ssd->dirty.right; x += blksize) { + blk = x / blksize; + bw = MIN(blksize, ssd->dirty.right - x); + if (dirty_top[blk] != -1) { + QXLRect update = { + .top = dirty_top[blk], + .bottom = ssd->dirty.bottom, + .left = x, + .right = x + bw, + }; + qemu_spice_create_one_update(ssd, &update); + dirty_top[blk] = -1; + } + } + + memset(&ssd->dirty, 0, sizeof(ssd->dirty)); +} + +static SimpleSpiceCursor* +qemu_spice_create_cursor_update(SimpleSpiceDisplay *ssd, + QEMUCursor *c, + int on) +{ + size_t size = c ? c->width * c->height * 4 : 0; + SimpleSpiceCursor *update; + QXLCursorCmd *ccmd; + QXLCursor *cursor; + QXLCommand *cmd; + + update = g_malloc0(sizeof(*update) + size); + ccmd = &update->cmd; + cursor = &update->cursor; + cmd = &update->ext.cmd; + + if (c) { + ccmd->type = QXL_CURSOR_SET; + ccmd->u.set.position.x = ssd->ptr_x + ssd->hot_x; + ccmd->u.set.position.y = ssd->ptr_y + ssd->hot_y; + ccmd->u.set.visible = true; + ccmd->u.set.shape = (uintptr_t)cursor; + cursor->header.unique = ssd->unique++; + cursor->header.type = SPICE_CURSOR_TYPE_ALPHA; + cursor->header.width = c->width; + cursor->header.height = c->height; + cursor->header.hot_spot_x = c->hot_x; + cursor->header.hot_spot_y = c->hot_y; + cursor->data_size = size; + cursor->chunk.data_size = size; + memcpy(cursor->chunk.data, c->data, size); + } else if (!on) { + ccmd->type = QXL_CURSOR_HIDE; + } else { + ccmd->type = QXL_CURSOR_MOVE; + ccmd->u.position.x = ssd->ptr_x + ssd->hot_x; + ccmd->u.position.y = ssd->ptr_y + ssd->hot_y; + } + ccmd->release_info.id = (uintptr_t)(&update->ext); + + cmd->type = QXL_CMD_CURSOR; + cmd->data = (uintptr_t)ccmd; + + return update; +} + +/* + * Called from spice server thread context (via interface_release_resource) + * We do *not* hold the global qemu mutex here, so extra care is needed + * when calling qemu functions. QEMU interfaces used: + * - g_free (underlying glibc free is re-entrant). + */ +void qemu_spice_destroy_update(SimpleSpiceDisplay *sdpy, SimpleSpiceUpdate *update) +{ + g_free(update->bitmap); + g_free(update); +} + +void qemu_spice_create_host_memslot(SimpleSpiceDisplay *ssd) +{ + QXLDevMemSlot memslot; + + memset(&memslot, 0, sizeof(memslot)); + memslot.slot_group_id = MEMSLOT_GROUP_HOST; + memslot.virt_end = ~0; + qemu_spice_add_memslot(ssd, &memslot, QXL_SYNC); +} + +void qemu_spice_create_host_primary(SimpleSpiceDisplay *ssd) +{ + QXLDevSurfaceCreate surface; + uint64_t surface_size; + + memset(&surface, 0, sizeof(surface)); + + surface_size = (uint64_t) surface_width(ssd->ds) * + surface_height(ssd->ds) * 4; + assert(surface_size > 0); + assert(surface_size < INT_MAX); + if (ssd->bufsize < surface_size) { + ssd->bufsize = surface_size; + g_free(ssd->buf); + ssd->buf = g_malloc(ssd->bufsize); + } + + surface.format = SPICE_SURFACE_FMT_32_xRGB; + surface.width = surface_width(ssd->ds); + surface.height = surface_height(ssd->ds); + surface.stride = -surface.width * 4; + surface.mouse_mode = true; + surface.flags = 0; + surface.type = 0; + surface.mem = (uintptr_t)ssd->buf; + surface.group_id = MEMSLOT_GROUP_HOST; + + qemu_spice_create_primary_surface(ssd, 0, &surface, QXL_SYNC); +} + +void qemu_spice_destroy_host_primary(SimpleSpiceDisplay *ssd) +{ + qemu_spice_destroy_primary_surface(ssd, 0, QXL_SYNC); +} + +void qemu_spice_display_init_common(SimpleSpiceDisplay *ssd) +{ + qemu_mutex_init(&ssd->lock); + QTAILQ_INIT(&ssd->updates); + ssd->mouse_x = -1; + ssd->mouse_y = -1; + if (ssd->num_surfaces == 0) { + ssd->num_surfaces = 1024; + } +} + +/* display listener callbacks */ + +void qemu_spice_display_update(SimpleSpiceDisplay *ssd, + int x, int y, int w, int h) +{ + QXLRect update_area; + + trace_qemu_spice_display_update(ssd->qxl.id, x, y, w, h); + update_area.left = x, + update_area.right = x + w; + update_area.top = y; + update_area.bottom = y + h; + + if (qemu_spice_rect_is_empty(&ssd->dirty)) { + ssd->notify++; + } + qemu_spice_rect_union(&ssd->dirty, &update_area); +} + +void qemu_spice_display_switch(SimpleSpiceDisplay *ssd, + DisplaySurface *surface) +{ + SimpleSpiceUpdate *update; + bool need_destroy; + + if (ssd->surface && + surface_width(surface) == pixman_image_get_width(ssd->surface) && + surface_height(surface) == pixman_image_get_height(ssd->surface) && + surface_format(surface) == pixman_image_get_format(ssd->surface)) { + /* no-resize fast path: just swap backing store */ + trace_qemu_spice_display_surface(ssd->qxl.id, + surface_width(surface), + surface_height(surface), + true); + qemu_mutex_lock(&ssd->lock); + ssd->ds = surface; + pixman_image_unref(ssd->surface); + ssd->surface = pixman_image_ref(ssd->ds->image); + qemu_mutex_unlock(&ssd->lock); + qemu_spice_display_update(ssd, 0, 0, + surface_width(surface), + surface_height(surface)); + return; + } + + /* full mode switch */ + trace_qemu_spice_display_surface(ssd->qxl.id, + surface_width(surface), + surface_height(surface), + false); + + memset(&ssd->dirty, 0, sizeof(ssd->dirty)); + if (ssd->surface) { + pixman_image_unref(ssd->surface); + ssd->surface = NULL; + pixman_image_unref(ssd->mirror); + ssd->mirror = NULL; + } + + qemu_mutex_lock(&ssd->lock); + need_destroy = (ssd->ds != NULL); + ssd->ds = surface; + while ((update = QTAILQ_FIRST(&ssd->updates)) != NULL) { + QTAILQ_REMOVE(&ssd->updates, update, next); + qemu_spice_destroy_update(ssd, update); + } + qemu_mutex_unlock(&ssd->lock); + if (need_destroy) { + qemu_spice_destroy_host_primary(ssd); + } + if (ssd->ds) { + ssd->surface = pixman_image_ref(ssd->ds->image); + ssd->mirror = qemu_pixman_mirror_create(ssd->ds->format, + ssd->ds->image); + qemu_spice_create_host_primary(ssd); + } + + memset(&ssd->dirty, 0, sizeof(ssd->dirty)); + ssd->notify++; + + qemu_mutex_lock(&ssd->lock); + if (ssd->cursor) { + g_free(ssd->ptr_define); + ssd->ptr_define = qemu_spice_create_cursor_update(ssd, ssd->cursor, 0); + } + qemu_mutex_unlock(&ssd->lock); +} + +void qemu_spice_cursor_refresh_bh(void *opaque) +{ + SimpleSpiceDisplay *ssd = opaque; + + qemu_mutex_lock(&ssd->lock); + if (ssd->cursor) { + QEMUCursor *c = ssd->cursor; + assert(ssd->dcl.con); + cursor_get(c); + qemu_mutex_unlock(&ssd->lock); + dpy_cursor_define(ssd->dcl.con, c); + qemu_mutex_lock(&ssd->lock); + cursor_put(c); + } + + if (ssd->mouse_x != -1 && ssd->mouse_y != -1) { + int x, y; + assert(ssd->dcl.con); + x = ssd->mouse_x; + y = ssd->mouse_y; + ssd->mouse_x = -1; + ssd->mouse_y = -1; + qemu_mutex_unlock(&ssd->lock); + dpy_mouse_set(ssd->dcl.con, x, y, 1); + } else { + qemu_mutex_unlock(&ssd->lock); + } +} + +void qemu_spice_display_refresh(SimpleSpiceDisplay *ssd) +{ + graphic_hw_update(ssd->dcl.con); + + WITH_QEMU_LOCK_GUARD(&ssd->lock) { + if (QTAILQ_EMPTY(&ssd->updates) && ssd->ds) { + qemu_spice_create_update(ssd); + ssd->notify++; + } + } + + trace_qemu_spice_display_refresh(ssd->qxl.id, ssd->notify); + if (ssd->notify) { + ssd->notify = 0; + qemu_spice_wakeup(ssd); + } +} + +/* spice display interface callbacks */ + +static void interface_attach_worker(QXLInstance *sin, QXLWorker *qxl_worker) +{ + /* nothing to do */ +} + +static void interface_set_compression_level(QXLInstance *sin, int level) +{ + /* nothing to do */ +} + +#if SPICE_NEEDS_SET_MM_TIME +static void interface_set_mm_time(QXLInstance *sin, uint32_t mm_time) +{ + /* nothing to do */ +} +#endif + +static void interface_get_init_info(QXLInstance *sin, QXLDevInitInfo *info) +{ + SimpleSpiceDisplay *ssd = container_of(sin, SimpleSpiceDisplay, qxl); + + info->memslot_gen_bits = MEMSLOT_GENERATION_BITS; + info->memslot_id_bits = MEMSLOT_SLOT_BITS; + info->num_memslots = NUM_MEMSLOTS; + info->num_memslots_groups = NUM_MEMSLOTS_GROUPS; + info->internal_groupslot_id = 0; + info->qxl_ram_size = 16 * 1024 * 1024; + info->n_surfaces = ssd->num_surfaces; +} + +static int interface_get_command(QXLInstance *sin, QXLCommandExt *ext) +{ + SimpleSpiceDisplay *ssd = container_of(sin, SimpleSpiceDisplay, qxl); + SimpleSpiceUpdate *update; + int ret = false; + + qemu_mutex_lock(&ssd->lock); + update = QTAILQ_FIRST(&ssd->updates); + if (update != NULL) { + QTAILQ_REMOVE(&ssd->updates, update, next); + *ext = update->ext; + ret = true; + } + qemu_mutex_unlock(&ssd->lock); + + return ret; +} + +static int interface_req_cmd_notification(QXLInstance *sin) +{ + return 1; +} + +static void interface_release_resource(QXLInstance *sin, + QXLReleaseInfoExt rext) +{ + SimpleSpiceDisplay *ssd = container_of(sin, SimpleSpiceDisplay, qxl); + SimpleSpiceUpdate *update; + SimpleSpiceCursor *cursor; + QXLCommandExt *ext; + + if (!rext.info) { + return; + } + + ext = (void *)(intptr_t)(rext.info->id); + switch (ext->cmd.type) { + case QXL_CMD_DRAW: + update = container_of(ext, SimpleSpiceUpdate, ext); + qemu_spice_destroy_update(ssd, update); + break; + case QXL_CMD_CURSOR: + cursor = container_of(ext, SimpleSpiceCursor, ext); + g_free(cursor); + break; + default: + g_assert_not_reached(); + } +} + +static int interface_get_cursor_command(QXLInstance *sin, QXLCommandExt *ext) +{ + SimpleSpiceDisplay *ssd = container_of(sin, SimpleSpiceDisplay, qxl); + int ret; + + QEMU_LOCK_GUARD(&ssd->lock); + if (ssd->ptr_define) { + *ext = ssd->ptr_define->ext; + ssd->ptr_define = NULL; + ret = true; + } else if (ssd->ptr_move) { + *ext = ssd->ptr_move->ext; + ssd->ptr_move = NULL; + ret = true; + } else { + ret = false; + } + return ret; +} + +static int interface_req_cursor_notification(QXLInstance *sin) +{ + return 1; +} + +static void interface_notify_update(QXLInstance *sin, uint32_t update_id) +{ + fprintf(stderr, "%s: abort()\n", __func__); + abort(); +} + +static int interface_flush_resources(QXLInstance *sin) +{ + fprintf(stderr, "%s: abort()\n", __func__); + abort(); + return 0; +} + +static void interface_update_area_complete(QXLInstance *sin, + uint32_t surface_id, + QXLRect *dirty, uint32_t num_updated_rects) +{ + /* should never be called, used in qxl native mode only */ + fprintf(stderr, "%s: abort()\n", __func__); + abort(); +} + +/* called from spice server thread context only */ +static void interface_async_complete(QXLInstance *sin, uint64_t cookie_token) +{ + QXLCookie *cookie = (QXLCookie *)(uintptr_t)cookie_token; + + switch (cookie->type) { +#ifdef HAVE_SPICE_GL + case QXL_COOKIE_TYPE_GL_DRAW_DONE: + { + SimpleSpiceDisplay *ssd = container_of(sin, SimpleSpiceDisplay, qxl); + qemu_bh_schedule(ssd->gl_unblock_bh); + break; + } + case QXL_COOKIE_TYPE_IO: + if (cookie->io == QXL_IO_MONITORS_CONFIG_ASYNC) { + g_free(cookie->u.data); + } + break; +#endif + default: + /* should never be called, used in qxl native mode only */ + fprintf(stderr, "%s: abort()\n", __func__); + abort(); + } + g_free(cookie); +} + +static void interface_set_client_capabilities(QXLInstance *sin, + uint8_t client_present, + uint8_t caps[58]) +{ + /* nothing to do */ +} + +static int interface_client_monitors_config(QXLInstance *sin, + VDAgentMonitorsConfig *mc) +{ + SimpleSpiceDisplay *ssd = container_of(sin, SimpleSpiceDisplay, qxl); + QemuUIInfo info; + int head; + + if (!dpy_ui_info_supported(ssd->dcl.con)) { + return 0; /* == not supported by guest */ + } + + if (!mc) { + return 1; + } + + info = *dpy_get_ui_info(ssd->dcl.con); + + head = qemu_console_get_index(ssd->dcl.con); + if (mc->num_of_monitors > head) { + info.width = mc->monitors[head].width; + info.height = mc->monitors[head].height; +#if SPICE_SERVER_VERSION >= 0x000e04 /* release 0.14.4 */ + if (mc->flags & VD_AGENT_CONFIG_MONITORS_FLAG_PHYSICAL_SIZE) { + VDAgentMonitorMM *mm = (void *)&mc->monitors[mc->num_of_monitors]; + info.width_mm = mm[head].width; + info.height_mm = mm[head].height; + } +#endif + } + + trace_qemu_spice_ui_info(ssd->qxl.id, info.width, info.height); + dpy_set_ui_info(ssd->dcl.con, &info); + return 1; +} + +static const QXLInterface dpy_interface = { + .base.type = SPICE_INTERFACE_QXL, + .base.description = "qemu simple display", + .base.major_version = SPICE_INTERFACE_QXL_MAJOR, + .base.minor_version = SPICE_INTERFACE_QXL_MINOR, + + .attache_worker = interface_attach_worker, + .set_compression_level = interface_set_compression_level, +#if SPICE_NEEDS_SET_MM_TIME + .set_mm_time = interface_set_mm_time, +#endif + .get_init_info = interface_get_init_info, + + /* the callbacks below are called from spice server thread context */ + .get_command = interface_get_command, + .req_cmd_notification = interface_req_cmd_notification, + .release_resource = interface_release_resource, + .get_cursor_command = interface_get_cursor_command, + .req_cursor_notification = interface_req_cursor_notification, + .notify_update = interface_notify_update, + .flush_resources = interface_flush_resources, + .async_complete = interface_async_complete, + .update_area_complete = interface_update_area_complete, + .set_client_capabilities = interface_set_client_capabilities, + .client_monitors_config = interface_client_monitors_config, +}; + +static void display_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + qemu_spice_display_update(ssd, x, y, w, h); +} + +static void display_switch(DisplayChangeListener *dcl, + DisplaySurface *surface) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + qemu_spice_display_switch(ssd, surface); +} + +static void display_refresh(DisplayChangeListener *dcl) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + qemu_spice_display_refresh(ssd); +} + +static void display_mouse_set(DisplayChangeListener *dcl, + int x, int y, int on) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + + qemu_mutex_lock(&ssd->lock); + ssd->ptr_x = x; + ssd->ptr_y = y; + g_free(ssd->ptr_move); + ssd->ptr_move = qemu_spice_create_cursor_update(ssd, NULL, on); + qemu_mutex_unlock(&ssd->lock); + qemu_spice_wakeup(ssd); +} + +static void display_mouse_define(DisplayChangeListener *dcl, + QEMUCursor *c) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + + qemu_mutex_lock(&ssd->lock); + cursor_get(c); + cursor_put(ssd->cursor); + ssd->cursor = c; + ssd->hot_x = c->hot_x; + ssd->hot_y = c->hot_y; + g_free(ssd->ptr_move); + ssd->ptr_move = NULL; + g_free(ssd->ptr_define); + ssd->ptr_define = qemu_spice_create_cursor_update(ssd, c, 0); + qemu_mutex_unlock(&ssd->lock); + qemu_spice_wakeup(ssd); +} + +static const DisplayChangeListenerOps display_listener_ops = { + .dpy_name = "spice", + .dpy_gfx_update = display_update, + .dpy_gfx_switch = display_switch, + .dpy_gfx_check_format = qemu_pixman_check_format, + .dpy_refresh = display_refresh, + .dpy_mouse_set = display_mouse_set, + .dpy_cursor_define = display_mouse_define, +}; + +#ifdef HAVE_SPICE_GL + +static void qemu_spice_gl_monitor_config(SimpleSpiceDisplay *ssd, + int x, int y, int w, int h) +{ + QXLMonitorsConfig *config; + QXLCookie *cookie; + + config = g_malloc0(sizeof(QXLMonitorsConfig) + sizeof(QXLHead)); + config->count = 1; + config->max_allowed = 1; + config->heads[0].x = x; + config->heads[0].y = y; + config->heads[0].width = w; + config->heads[0].height = h; + cookie = qxl_cookie_new(QXL_COOKIE_TYPE_IO, + QXL_IO_MONITORS_CONFIG_ASYNC); + cookie->u.data = config; + + spice_qxl_monitors_config_async(&ssd->qxl, + (uintptr_t)config, + MEMSLOT_GROUP_HOST, + (uintptr_t)cookie); +} + +static void qemu_spice_gl_block(SimpleSpiceDisplay *ssd, bool block) +{ + uint64_t timeout; + + if (block) { + timeout = qemu_clock_get_ms(QEMU_CLOCK_REALTIME); + timeout += 1000; /* one sec */ + timer_mod(ssd->gl_unblock_timer, timeout); + } else { + timer_del(ssd->gl_unblock_timer); + } + graphic_hw_gl_block(ssd->dcl.con, block); +} + +static void qemu_spice_gl_unblock_bh(void *opaque) +{ + SimpleSpiceDisplay *ssd = opaque; + + qemu_spice_gl_block(ssd, false); + graphic_hw_gl_flushed(ssd->dcl.con); +} + +static void qemu_spice_gl_block_timer(void *opaque) +{ + warn_report("spice: no gl-draw-done within one second"); +} + +static void spice_gl_refresh(DisplayChangeListener *dcl) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + uint64_t cookie; + + if (!ssd->ds || qemu_console_is_gl_blocked(ssd->dcl.con)) { + return; + } + + graphic_hw_update(dcl->con); + if (ssd->gl_updates && ssd->have_surface) { + qemu_spice_gl_block(ssd, true); + glFlush(); + cookie = (uintptr_t)qxl_cookie_new(QXL_COOKIE_TYPE_GL_DRAW_DONE, 0); + spice_qxl_gl_draw_async(&ssd->qxl, 0, 0, + surface_width(ssd->ds), + surface_height(ssd->ds), + cookie); + ssd->gl_updates = 0; + } +} + +static void spice_gl_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + + surface_gl_update_texture(ssd->gls, ssd->ds, x, y, w, h); + ssd->gl_updates++; +} + +static void spice_gl_switch(DisplayChangeListener *dcl, + struct DisplaySurface *new_surface) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + EGLint stride, fourcc; + int fd; + + if (ssd->ds) { + surface_gl_destroy_texture(ssd->gls, ssd->ds); + } + ssd->ds = new_surface; + if (ssd->ds) { + surface_gl_create_texture(ssd->gls, ssd->ds); + fd = egl_get_fd_for_texture(ssd->ds->texture, + &stride, &fourcc, + NULL); + if (fd < 0) { + surface_gl_destroy_texture(ssd->gls, ssd->ds); + return; + } + + trace_qemu_spice_gl_surface(ssd->qxl.id, + surface_width(ssd->ds), + surface_height(ssd->ds), + fourcc); + + /* note: spice server will close the fd */ + spice_qxl_gl_scanout(&ssd->qxl, fd, + surface_width(ssd->ds), + surface_height(ssd->ds), + stride, fourcc, false); + ssd->have_surface = true; + ssd->have_scanout = false; + + qemu_spice_gl_monitor_config(ssd, 0, 0, + surface_width(ssd->ds), + surface_height(ssd->ds)); + } +} + +static QEMUGLContext qemu_spice_gl_create_context(DisplayChangeListener *dcl, + QEMUGLParams *params) +{ + eglMakeCurrent(qemu_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + qemu_egl_rn_ctx); + return qemu_egl_create_context(dcl, params); +} + +static void qemu_spice_gl_scanout_disable(DisplayChangeListener *dcl) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + + trace_qemu_spice_gl_scanout_disable(ssd->qxl.id); + spice_qxl_gl_scanout(&ssd->qxl, -1, 0, 0, 0, 0, false); + qemu_spice_gl_monitor_config(ssd, 0, 0, 0, 0); + ssd->have_surface = false; + ssd->have_scanout = false; +} + +static void qemu_spice_gl_scanout_texture(DisplayChangeListener *dcl, + uint32_t tex_id, + bool y_0_top, + uint32_t backing_width, + uint32_t backing_height, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + EGLint stride = 0, fourcc = 0; + int fd = -1; + + assert(tex_id); + fd = egl_get_fd_for_texture(tex_id, &stride, &fourcc, NULL); + if (fd < 0) { + fprintf(stderr, "%s: failed to get fd for texture\n", __func__); + return; + } + trace_qemu_spice_gl_scanout_texture(ssd->qxl.id, w, h, fourcc); + + /* note: spice server will close the fd */ + spice_qxl_gl_scanout(&ssd->qxl, fd, backing_width, backing_height, + stride, fourcc, y_0_top); + qemu_spice_gl_monitor_config(ssd, x, y, w, h); + ssd->have_surface = false; + ssd->have_scanout = true; +} + +static void qemu_spice_gl_scanout_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + + ssd->guest_dmabuf = dmabuf; + ssd->guest_dmabuf_refresh = true; + + ssd->have_surface = false; + ssd->have_scanout = true; +} + +static void qemu_spice_gl_cursor_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf, bool have_hot, + uint32_t hot_x, uint32_t hot_y) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + + ssd->have_hot = have_hot; + ssd->hot_x = hot_x; + ssd->hot_y = hot_y; + + trace_qemu_spice_gl_cursor(ssd->qxl.id, dmabuf != NULL, have_hot); + if (dmabuf) { + egl_dmabuf_import_texture(dmabuf); + if (!dmabuf->texture) { + return; + } + egl_fb_setup_for_tex(&ssd->cursor_fb, dmabuf->width, dmabuf->height, + dmabuf->texture, false); + } else { + egl_fb_destroy(&ssd->cursor_fb); + } +} + +static void qemu_spice_gl_cursor_position(DisplayChangeListener *dcl, + uint32_t pos_x, uint32_t pos_y) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + + qemu_mutex_lock(&ssd->lock); + ssd->ptr_x = pos_x; + ssd->ptr_y = pos_y; + qemu_mutex_unlock(&ssd->lock); +} + +static void qemu_spice_gl_release_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + + if (ssd->guest_dmabuf == dmabuf) { + ssd->guest_dmabuf = NULL; + ssd->guest_dmabuf_refresh = false; + } + egl_dmabuf_release_texture(dmabuf); +} + +static void qemu_spice_gl_update(DisplayChangeListener *dcl, + uint32_t x, uint32_t y, uint32_t w, uint32_t h) +{ + SimpleSpiceDisplay *ssd = container_of(dcl, SimpleSpiceDisplay, dcl); + EGLint stride = 0, fourcc = 0; + bool render_cursor = false; + bool y_0_top = false; /* FIXME */ + uint64_t cookie; + int fd; + + if (!ssd->have_scanout) { + return; + } + + if (ssd->cursor_fb.texture) { + render_cursor = true; + } + if (ssd->render_cursor != render_cursor) { + ssd->render_cursor = render_cursor; + ssd->guest_dmabuf_refresh = true; + egl_fb_destroy(&ssd->blit_fb); + } + + if (ssd->guest_dmabuf_refresh) { + QemuDmaBuf *dmabuf = ssd->guest_dmabuf; + if (render_cursor) { + egl_dmabuf_import_texture(dmabuf); + if (!dmabuf->texture) { + return; + } + + /* source framebuffer */ + egl_fb_setup_for_tex(&ssd->guest_fb, + dmabuf->width, dmabuf->height, + dmabuf->texture, false); + + /* dest framebuffer */ + if (ssd->blit_fb.width != dmabuf->width || + ssd->blit_fb.height != dmabuf->height) { + trace_qemu_spice_gl_render_dmabuf(ssd->qxl.id, dmabuf->width, + dmabuf->height); + egl_fb_destroy(&ssd->blit_fb); + egl_fb_setup_new_tex(&ssd->blit_fb, + dmabuf->width, dmabuf->height); + fd = egl_get_fd_for_texture(ssd->blit_fb.texture, + &stride, &fourcc, NULL); + spice_qxl_gl_scanout(&ssd->qxl, fd, + dmabuf->width, dmabuf->height, + stride, fourcc, false); + } + } else { + trace_qemu_spice_gl_forward_dmabuf(ssd->qxl.id, + dmabuf->width, dmabuf->height); + /* note: spice server will close the fd, so hand over a dup */ + spice_qxl_gl_scanout(&ssd->qxl, dup(dmabuf->fd), + dmabuf->width, dmabuf->height, + dmabuf->stride, dmabuf->fourcc, + dmabuf->y0_top); + } + qemu_spice_gl_monitor_config(ssd, 0, 0, dmabuf->width, dmabuf->height); + ssd->guest_dmabuf_refresh = false; + } + + if (render_cursor) { + int x, y; + qemu_mutex_lock(&ssd->lock); + x = ssd->ptr_x; + y = ssd->ptr_y; + qemu_mutex_unlock(&ssd->lock); + egl_texture_blit(ssd->gls, &ssd->blit_fb, &ssd->guest_fb, + !y_0_top); + egl_texture_blend(ssd->gls, &ssd->blit_fb, &ssd->cursor_fb, + !y_0_top, x, y, 1.0, 1.0); + glFlush(); + } + + trace_qemu_spice_gl_update(ssd->qxl.id, w, h, x, y); + qemu_spice_gl_block(ssd, true); + glFlush(); + cookie = (uintptr_t)qxl_cookie_new(QXL_COOKIE_TYPE_GL_DRAW_DONE, 0); + spice_qxl_gl_draw_async(&ssd->qxl, x, y, w, h, cookie); +} + +static const DisplayChangeListenerOps display_listener_gl_ops = { + .dpy_name = "spice-egl", + .dpy_gfx_update = spice_gl_update, + .dpy_gfx_switch = spice_gl_switch, + .dpy_gfx_check_format = console_gl_check_format, + .dpy_refresh = spice_gl_refresh, + .dpy_mouse_set = display_mouse_set, + .dpy_cursor_define = display_mouse_define, + + .dpy_gl_ctx_create = qemu_spice_gl_create_context, + .dpy_gl_ctx_destroy = qemu_egl_destroy_context, + .dpy_gl_ctx_make_current = qemu_egl_make_context_current, + + .dpy_gl_scanout_disable = qemu_spice_gl_scanout_disable, + .dpy_gl_scanout_texture = qemu_spice_gl_scanout_texture, + .dpy_gl_scanout_dmabuf = qemu_spice_gl_scanout_dmabuf, + .dpy_gl_cursor_dmabuf = qemu_spice_gl_cursor_dmabuf, + .dpy_gl_cursor_position = qemu_spice_gl_cursor_position, + .dpy_gl_release_dmabuf = qemu_spice_gl_release_dmabuf, + .dpy_gl_update = qemu_spice_gl_update, +}; + +#endif /* HAVE_SPICE_GL */ + +static void qemu_spice_display_init_one(QemuConsole *con) +{ + SimpleSpiceDisplay *ssd = g_new0(SimpleSpiceDisplay, 1); + + qemu_spice_display_init_common(ssd); + + ssd->dcl.ops = &display_listener_ops; +#ifdef HAVE_SPICE_GL + if (spice_opengl) { + ssd->dcl.ops = &display_listener_gl_ops; + ssd->gl_unblock_bh = qemu_bh_new(qemu_spice_gl_unblock_bh, ssd); + ssd->gl_unblock_timer = timer_new_ms(QEMU_CLOCK_REALTIME, + qemu_spice_gl_block_timer, ssd); + ssd->gls = qemu_gl_init_shader(); + ssd->have_surface = false; + ssd->have_scanout = false; + } +#endif + ssd->dcl.con = con; + + ssd->qxl.base.sif = &dpy_interface.base; + qemu_spice_add_display_interface(&ssd->qxl, con); + +#if SPICE_SERVER_VERSION >= 0x000e02 /* release 0.14.2 */ + char device_address[256] = ""; + if (qemu_spice_fill_device_address(con, device_address, 256)) { + spice_qxl_set_device_info(&ssd->qxl, + device_address, + qemu_console_get_head(con), + 1); + } +#endif + + qemu_spice_create_host_memslot(ssd); + + register_displaychangelistener(&ssd->dcl); +} + +void qemu_spice_display_init(void) +{ + QemuOptsList *olist = qemu_find_opts("spice"); + QemuOpts *opts = QTAILQ_FIRST(&olist->head); + QemuConsole *spice_con, *con; + const char *str; + int i; + + str = qemu_opt_get(opts, "display"); + if (str) { + int head = qemu_opt_get_number(opts, "head", 0); + Error *err = NULL; + + spice_con = qemu_console_lookup_by_device_name(str, head, &err); + if (err) { + error_report("Failed to lookup display/head"); + exit(1); + } + } else { + spice_con = NULL; + } + + for (i = 0;; i++) { + con = qemu_console_lookup_by_index(i); + if (!con || !qemu_console_is_graphic(con)) { + break; + } + if (qemu_spice_have_display_interface(con)) { + continue; + } + if (spice_con != NULL && spice_con != con) { + continue; + } + qemu_spice_display_init_one(con); + } + + qemu_spice_display_init_done(); +} diff --git a/ui/spice-input.c b/ui/spice-input.c new file mode 100644 index 000000000..bbd502564 --- /dev/null +++ b/ui/spice-input.c @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 or + * (at your option) version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#include "qemu/osdep.h" + +#include <spice.h> +#include <spice/enums.h> + +#include "ui/qemu-spice.h" +#include "ui/console.h" +#include "keymaps.h" +#include "ui/input.h" + +/* keyboard bits */ + +typedef struct QemuSpiceKbd { + SpiceKbdInstance sin; + int ledstate; + bool emul0; + size_t pauseseq; +} QemuSpiceKbd; + +static void kbd_push_key(SpiceKbdInstance *sin, uint8_t frag); +static uint8_t kbd_get_leds(SpiceKbdInstance *sin); + +static const SpiceKbdInterface kbd_interface = { + .base.type = SPICE_INTERFACE_KEYBOARD, + .base.description = "qemu keyboard", + .base.major_version = SPICE_INTERFACE_KEYBOARD_MAJOR, + .base.minor_version = SPICE_INTERFACE_KEYBOARD_MINOR, + .push_scan_freg = kbd_push_key, + .get_leds = kbd_get_leds, +}; + +static void kbd_push_key(SpiceKbdInstance *sin, uint8_t scancode) +{ + static const uint8_t pauseseq[] = { 0xe1, 0x1d, 0x45, 0xe1, 0x9d, 0xc5 }; + QemuSpiceKbd *kbd = container_of(sin, QemuSpiceKbd, sin); + int keycode; + bool up; + + if (scancode == SCANCODE_EMUL0) { + kbd->emul0 = true; + return; + } + + if (scancode == pauseseq[kbd->pauseseq]) { + kbd->pauseseq++; + if (kbd->pauseseq == G_N_ELEMENTS(pauseseq)) { + qemu_input_event_send_key_qcode(NULL, Q_KEY_CODE_PAUSE, true); + kbd->pauseseq = 0; + } + return; + } else { + kbd->pauseseq = 0; + } + + keycode = scancode & ~SCANCODE_UP; + up = scancode & SCANCODE_UP; + if (kbd->emul0) { + kbd->emul0 = false; + keycode |= SCANCODE_GREY; + } + + qemu_input_event_send_key_number(NULL, keycode, !up); +} + +static uint8_t kbd_get_leds(SpiceKbdInstance *sin) +{ + QemuSpiceKbd *kbd = container_of(sin, QemuSpiceKbd, sin); + return kbd->ledstate; +} + +static void kbd_leds(void *opaque, int ledstate) +{ + QemuSpiceKbd *kbd = opaque; + + kbd->ledstate = 0; + if (ledstate & QEMU_SCROLL_LOCK_LED) { + kbd->ledstate |= SPICE_KEYBOARD_MODIFIER_FLAGS_SCROLL_LOCK; + } + if (ledstate & QEMU_NUM_LOCK_LED) { + kbd->ledstate |= SPICE_KEYBOARD_MODIFIER_FLAGS_NUM_LOCK; + } + if (ledstate & QEMU_CAPS_LOCK_LED) { + kbd->ledstate |= SPICE_KEYBOARD_MODIFIER_FLAGS_CAPS_LOCK; + } + spice_server_kbd_leds(&kbd->sin, kbd->ledstate); +} + +/* mouse bits */ + +typedef struct QemuSpicePointer { + SpiceMouseInstance mouse; + SpiceTabletInstance tablet; + int width, height; + uint32_t last_bmask; + Notifier mouse_mode; + bool absolute; +} QemuSpicePointer; + +static void spice_update_buttons(QemuSpicePointer *pointer, + int wheel, uint32_t button_mask) +{ + static uint32_t bmap[INPUT_BUTTON__MAX] = { + [INPUT_BUTTON_LEFT] = 0x01, + [INPUT_BUTTON_MIDDLE] = 0x04, + [INPUT_BUTTON_RIGHT] = 0x02, + [INPUT_BUTTON_WHEEL_UP] = 0x10, + [INPUT_BUTTON_WHEEL_DOWN] = 0x20, + [INPUT_BUTTON_SIDE] = 0x40, + [INPUT_BUTTON_EXTRA] = 0x80, + }; + + if (wheel < 0) { + button_mask |= 0x10; + } + if (wheel > 0) { + button_mask |= 0x20; + } + + if (pointer->last_bmask == button_mask) { + return; + } + qemu_input_update_buttons(NULL, bmap, pointer->last_bmask, button_mask); + pointer->last_bmask = button_mask; +} + +static void mouse_motion(SpiceMouseInstance *sin, int dx, int dy, int dz, + uint32_t buttons_state) +{ + QemuSpicePointer *pointer = container_of(sin, QemuSpicePointer, mouse); + spice_update_buttons(pointer, dz, buttons_state); + qemu_input_queue_rel(NULL, INPUT_AXIS_X, dx); + qemu_input_queue_rel(NULL, INPUT_AXIS_Y, dy); + qemu_input_event_sync(); +} + +static void mouse_buttons(SpiceMouseInstance *sin, uint32_t buttons_state) +{ + QemuSpicePointer *pointer = container_of(sin, QemuSpicePointer, mouse); + spice_update_buttons(pointer, 0, buttons_state); + qemu_input_event_sync(); +} + +static const SpiceMouseInterface mouse_interface = { + .base.type = SPICE_INTERFACE_MOUSE, + .base.description = "mouse", + .base.major_version = SPICE_INTERFACE_MOUSE_MAJOR, + .base.minor_version = SPICE_INTERFACE_MOUSE_MINOR, + .motion = mouse_motion, + .buttons = mouse_buttons, +}; + +static void tablet_set_logical_size(SpiceTabletInstance* sin, int width, int height) +{ + QemuSpicePointer *pointer = container_of(sin, QemuSpicePointer, tablet); + + if (height < 16) { + height = 16; + } + if (width < 16) { + width = 16; + } + pointer->width = width; + pointer->height = height; +} + +static void tablet_position(SpiceTabletInstance* sin, int x, int y, + uint32_t buttons_state) +{ + QemuSpicePointer *pointer = container_of(sin, QemuSpicePointer, tablet); + + spice_update_buttons(pointer, 0, buttons_state); + qemu_input_queue_abs(NULL, INPUT_AXIS_X, x, 0, pointer->width); + qemu_input_queue_abs(NULL, INPUT_AXIS_Y, y, 0, pointer->height); + qemu_input_event_sync(); +} + + +static void tablet_wheel(SpiceTabletInstance* sin, int wheel, + uint32_t buttons_state) +{ + QemuSpicePointer *pointer = container_of(sin, QemuSpicePointer, tablet); + + spice_update_buttons(pointer, wheel, buttons_state); + qemu_input_event_sync(); +} + +static void tablet_buttons(SpiceTabletInstance *sin, + uint32_t buttons_state) +{ + QemuSpicePointer *pointer = container_of(sin, QemuSpicePointer, tablet); + + spice_update_buttons(pointer, 0, buttons_state); + qemu_input_event_sync(); +} + +static const SpiceTabletInterface tablet_interface = { + .base.type = SPICE_INTERFACE_TABLET, + .base.description = "tablet", + .base.major_version = SPICE_INTERFACE_TABLET_MAJOR, + .base.minor_version = SPICE_INTERFACE_TABLET_MINOR, + .set_logical_size = tablet_set_logical_size, + .position = tablet_position, + .wheel = tablet_wheel, + .buttons = tablet_buttons, +}; + +static void mouse_mode_notifier(Notifier *notifier, void *data) +{ + QemuSpicePointer *pointer = container_of(notifier, QemuSpicePointer, mouse_mode); + bool is_absolute = qemu_input_is_absolute(); + + if (pointer->absolute == is_absolute) { + return; + } + + if (is_absolute) { + qemu_spice.add_interface(&pointer->tablet.base); + } else { + spice_server_remove_interface(&pointer->tablet.base); + } + pointer->absolute = is_absolute; +} + +void qemu_spice_input_init(void) +{ + QemuSpiceKbd *kbd; + QemuSpicePointer *pointer; + + kbd = g_malloc0(sizeof(*kbd)); + kbd->sin.base.sif = &kbd_interface.base; + qemu_spice.add_interface(&kbd->sin.base); + qemu_add_led_event_handler(kbd_leds, kbd); + + pointer = g_malloc0(sizeof(*pointer)); + pointer->mouse.base.sif = &mouse_interface.base; + pointer->tablet.base.sif = &tablet_interface.base; + qemu_spice.add_interface(&pointer->mouse.base); + + pointer->absolute = false; + pointer->mouse_mode.notify = mouse_mode_notifier; + qemu_add_mouse_mode_change_notifier(&pointer->mouse_mode); + mouse_mode_notifier(&pointer->mouse_mode, NULL); +} diff --git a/ui/spice-module.c b/ui/spice-module.c new file mode 100644 index 000000000..322233587 --- /dev/null +++ b/ui/spice-module.c @@ -0,0 +1,85 @@ +/* + * spice module support, also spice stubs. + * + * Copyright (C) 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 or + * (at your option) version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#include "qemu/osdep.h" +#include "qemu/error-report.h" +#include "qapi/error.h" +#include "qapi/qapi-types-ui.h" +#include "qapi/qapi-commands-ui.h" +#include "ui/qemu-spice-module.h" + +int using_spice; + +static void qemu_spice_init_stub(void) +{ +} + +static void qemu_spice_display_init_stub(void) +{ + /* This must never be called if CONFIG_SPICE is disabled */ + error_report("spice support is disabled"); + abort(); +} + +static int qemu_spice_migrate_info_stub(const char *h, int p, int t, + const char *s) +{ + return -1; +} + +static int qemu_spice_set_passwd_stub(const char *passwd, + bool fail_if_connected, + bool disconnect_if_connected) +{ + return -1; +} + +static int qemu_spice_set_pw_expire_stub(time_t expires) +{ + return -1; +} + +static int qemu_spice_display_add_client_stub(int csock, int skipauth, + int tls) +{ + return -1; +} + +struct QemuSpiceOps qemu_spice = { + .init = qemu_spice_init_stub, + .display_init = qemu_spice_display_init_stub, + .migrate_info = qemu_spice_migrate_info_stub, + .set_passwd = qemu_spice_set_passwd_stub, + .set_pw_expire = qemu_spice_set_pw_expire_stub, + .display_add_client = qemu_spice_display_add_client_stub, +}; + +#ifdef CONFIG_SPICE + +SpiceInfo *qmp_query_spice(Error **errp) +{ + if (!qemu_spice.qmp_query) { + SpiceInfo *info = g_new0(SpiceInfo, 1); + info->enabled = false; + return info; + } + return qemu_spice.qmp_query(errp); +} + +#endif diff --git a/ui/trace-events b/ui/trace-events new file mode 100644 index 000000000..b9c0dd0fa --- /dev/null +++ b/ui/trace-events @@ -0,0 +1,137 @@ +# See docs/devel/tracing.rst for syntax documentation. + +# console.c +console_gfx_new(void) "" +console_gfx_reuse(int index) "%d" +console_gfx_close(int index) "%d" +console_putchar_csi(int esc_param0, int esc_param1, int ch, int nb_esc_params) "escape sequence CSI%d;%d%c, %d parameters" +console_putchar_unhandled(int ch) "unhandled escape character '%c'" +console_txt_new(int w, int h) "%dx%d" +console_select(int nr) "%d" +console_refresh(int interval) "interval %d ms" +displaysurface_create(void *display_surface, int w, int h) "surface=%p, %dx%d" +displaysurface_create_from(void *display_surface, int w, int h, uint32_t format) "surface=%p, %dx%d, format 0x%x" +displaysurface_create_pixman(void *display_surface) "surface=%p" +displaysurface_free(void *display_surface) "surface=%p" +displaychangelistener_register(void *dcl, const char *name) "%p [ %s ]" +displaychangelistener_unregister(void *dcl, const char *name) "%p [ %s ]" +ppm_save(int fd, void *image) "fd=%d image=%p" + +# gtk-egl.c +# gtk-gl-area.c +# gtk.c +gd_switch(const char *tab, int width, int height) "tab=%s, width=%d, height=%d" +gd_update(const char *tab, int x, int y, int w, int h) "tab=%s, x=%d, y=%d, w=%d, h=%d" +gd_key_event(const char *tab, int gdk_keycode, int qkeycode, const char *action) "tab=%s, translated GDK keycode %d to QKeyCode %d (%s)" +gd_grab(const char *tab, const char *device, const char *reason) "tab=%s, dev=%s, reason=%s" +gd_ungrab(const char *tab, const char *device) "tab=%s, dev=%s" +gd_keymap_windowing(const char *name) "backend=%s" + +# vnc-auth-sasl.c +# vnc-auth-vencrypt.c +# vnc-ws.c +# vnc.c +vnc_key_guest_leds(bool caps, bool num, bool scroll) "caps %d, num %d, scroll %d" +vnc_key_map_init(const char *layout) "%s" +vnc_key_event_ext(bool down, int sym, int keycode, const char *name) "down %d, sym 0x%x, keycode 0x%x [%s]" +vnc_key_event_map(bool down, int sym, int keycode, const char *name) "down %d, sym 0x%x -> keycode 0x%x [%s]" +vnc_key_sync_numlock(bool on) "%d" +vnc_key_sync_capslock(bool on) "%d" +vnc_msg_server_audio_begin(void *state, void *ioc) "VNC server msg audio begin state=%p ioc=%p" +vnc_msg_server_audio_end(void *state, void *ioc) "VNC server msg audio end state=%p ioc=%p" +vnc_msg_server_audio_data(void *state, void *ioc, const void *buf, size_t len) "VNC server msg audio data state=%p ioc=%p buf=%p len=%zd" +vnc_msg_server_desktop_resize(void *state, void *ioc, int width, int height) "VNC server msg ext resize state=%p ioc=%p size=%dx%d" +vnc_msg_server_ext_desktop_resize(void *state, void *ioc, int width, int height, int reason) "VNC server msg ext resize state=%p ioc=%p size=%dx%d reason=%d" +vnc_msg_client_audio_enable(void *state, void *ioc) "VNC client msg audio enable state=%p ioc=%p" +vnc_msg_client_audio_disable(void *state, void *ioc) "VNC client msg audio disable state=%p ioc=%p" +vnc_msg_client_audio_format(void *state, void *ioc, int fmt, int channels, int freq) "VNC client msg audio format state=%p ioc=%p fmt=%d channels=%d freq=%d" +vnc_msg_client_set_desktop_size(void *state, void *ioc, int width, int height, int screens) "VNC client msg set desktop size state=%p ioc=%p size=%dx%d screens=%d" +vnc_client_eof(void *state, void *ioc) "VNC client EOF state=%p ioc=%p" +vnc_client_io_error(void *state, void *ioc, const char *msg) "VNC client I/O error state=%p ioc=%p errmsg=%s" +vnc_client_connect(void *state, void *ioc) "VNC client connect state=%p ioc=%p" +vnc_client_disconnect_start(void *state, void *ioc) "VNC client disconnect start state=%p ioc=%p" +vnc_client_disconnect_finish(void *state, void *ioc) "VNC client disconnect finish state=%p ioc=%p" +vnc_client_io_wrap(void *state, void *ioc, const char *type) "VNC client I/O wrap state=%p ioc=%p type=%s" +vnc_client_throttle_threshold(void *state, void *ioc, size_t oldoffset, size_t offset, int client_width, int client_height, int bytes_per_pixel, void *audio_cap) "VNC client throttle threshold state=%p ioc=%p oldoffset=%zu newoffset=%zu width=%d height=%d bpp=%d audio=%p" +vnc_client_throttle_incremental(void *state, void *ioc, int job_update, size_t offset) "VNC client throttle incremental state=%p ioc=%p job-update=%d offset=%zu" +vnc_client_throttle_forced(void *state, void *ioc, int job_update, size_t offset) "VNC client throttle forced state=%p ioc=%p job-update=%d offset=%zu" +vnc_client_throttle_audio(void *state, void *ioc, size_t offset) "VNC client throttle audio state=%p ioc=%p offset=%zu" +vnc_client_unthrottle_forced(void *state, void *ioc) "VNC client unthrottle forced offset state=%p ioc=%p" +vnc_client_unthrottle_incremental(void *state, void *ioc, size_t offset) "VNC client unthrottle incremental state=%p ioc=%p offset=%zu" +vnc_client_output_limit(void *state, void *ioc, size_t offset, size_t threshold) "VNC client output limit state=%p ioc=%p offset=%zu threshold=%zu" +vnc_server_dpy_pageflip(void *dpy, int w, int h, int fmt) "VNC server dpy pageflip dpy=%p size=%dx%d fmt=%d" +vnc_server_dpy_recreate(void *dpy, int w, int h, int fmt) "VNC server dpy recreate dpy=%p size=%dx%d fmt=%d" +vnc_job_add_rect(void *state, void *job, int x, int y, int w, int h) "VNC add rect state=%p job=%p offset=%d,%d size=%dx%d" +vnc_job_discard_rect(void *state, void *job, int x, int y, int w, int h) "VNC job discard rect state=%p job=%p offset=%d,%d size=%dx%d" +vnc_job_clamp_rect(void *state, void *job, int x, int y, int w, int h) "VNC job clamp rect state=%p job=%p offset=%d,%d size=%dx%d" +vnc_job_clamped_rect(void *state, void *job, int x, int y, int w, int h) "VNC job clamp rect state=%p job=%p offset=%d,%d size=%dx%d" +vnc_job_nrects(void *state, void *job, int nrects) "VNC job state=%p job=%p nrects=%d" +vnc_auth_init(void *display, int websock, int auth, int subauth) "VNC auth init state=%p websock=%d auth=%d subauth=%d" +vnc_auth_start(void *state, int method) "VNC client auth start state=%p method=%d" +vnc_auth_pass(void *state, int method) "VNC client auth passed state=%p method=%d" +vnc_auth_fail(void *state, int method, const char *message, const char *reason) "VNC client auth failed state=%p method=%d message=%s reason=%s" +vnc_auth_reject(void *state, int expect, int got) "VNC client auth rejected state=%p method expected=%d got=%d" +vnc_auth_vencrypt_version(void *state, int major, int minor) "VNC client auth vencrypt version state=%p major=%d minor=%d" +vnc_auth_vencrypt_subauth(void *state, int auth) "VNC client auth vencrypt subauth state=%p auth=%d" +vnc_auth_sasl_mech_list(void *state, const char *mechs) "VNC client auth SASL state=%p mechlist=%s" +vnc_auth_sasl_mech_choose(void *state, const char *mech) "VNC client auth SASL state=%p mech=%s" +vnc_auth_sasl_start(void *state, const void *clientdata, size_t clientlen, const void *serverdata, size_t severlen, int ret) "VNC client auth SASL start state=%p clientdata=%p clientlen=%zu serverdata=%p serverlen=%zu ret=%d" +vnc_auth_sasl_step(void *state, const void *clientdata, size_t clientlen, const void *serverdata, size_t severlen, int ret) "VNC client auth SASL step state=%p clientdata=%p clientlen=%zu serverdata=%p serverlen=%zu ret=%d" +vnc_auth_sasl_ssf(void *state, int ssf) "VNC client auth SASL SSF state=%p size=%d" +vnc_auth_sasl_username(void *state, const char *name) "VNC client auth SASL user state=%p name=%s" +vnc_auth_sasl_acl(void *state, int allow) "VNC client auth SASL ACL state=%p allow=%d" + + +# input.c +input_event_key_number(int conidx, int number, const char *qcode, bool down) "con %d, key number 0x%x [%s], down %d" +input_event_key_qcode(int conidx, const char *qcode, bool down) "con %d, key qcode %s, down %d" +input_event_btn(int conidx, const char *btn, bool down) "con %d, button %s, down %d" +input_event_rel(int conidx, const char *axis, int value) "con %d, axis %s, value %d" +input_event_abs(int conidx, const char *axis, int value) "con %d, axis %s, value 0x%x" +input_event_sync(void) "" +input_mouse_mode(int absolute) "absolute %d" + +# sdl2-input.c +sdl2_process_key(int sdl_scancode, int qcode, const char *action) "translated SDL scancode %d to QKeyCode %d (%s)" + +# spice-display.c +qemu_spice_add_memslot(int qid, uint32_t slot_id, unsigned long virt_start, unsigned long virt_end, int async) "%d %u: host virt 0x%lx - 0x%lx async=%d" +qemu_spice_del_memslot(int qid, uint32_t gid, uint32_t slot_id) "%d gid=%u sid=%u" +qemu_spice_create_primary_surface(int qid, uint32_t sid, void *surface, int async) "%d sid=%u surface=%p async=%d" +qemu_spice_destroy_primary_surface(int qid, uint32_t sid, int async) "%d sid=%u async=%d" +qemu_spice_wakeup(uint32_t qid) "%d" +qemu_spice_create_update(uint32_t left, uint32_t right, uint32_t top, uint32_t bottom) "lr %d -> %d, tb -> %d -> %d" +qemu_spice_display_update(int qid, uint32_t x, uint32_t y, uint32_t w, uint32_t h) "%d +%d+%d %dx%d" +qemu_spice_display_surface(int qid, uint32_t w, uint32_t h, int fast) "%d %dx%d, fast %d" +qemu_spice_display_refresh(int qid, int notify) "%d notify %d" +qemu_spice_ui_info(int qid, uint32_t width, uint32_t height) "%d %dx%d" + +qemu_spice_gl_surface(int qid, uint32_t w, uint32_t h, uint32_t fourcc) "%d %dx%d, fourcc 0x%x" +qemu_spice_gl_scanout_disable(int qid) "%d" +qemu_spice_gl_scanout_texture(int qid, uint32_t w, uint32_t h, uint32_t fourcc) "%d %dx%d, fourcc 0x%x" +qemu_spice_gl_cursor(int qid, bool enabled, bool hotspot) "%d enabled %d, hotspot %d" +qemu_spice_gl_forward_dmabuf(int qid, uint32_t width, uint32_t height) "%d %dx%d" +qemu_spice_gl_render_dmabuf(int qid, uint32_t width, uint32_t height) "%d %dx%d" +qemu_spice_gl_update(int qid, uint32_t x, uint32_t y, uint32_t w, uint32_t h) "%d +%d+%d %dx%d" + +# keymaps.c +keymap_parse(const char *file) "file %s" +keymap_add(int sym, int code, const char *line) "sym=0x%04x code=0x%04x (line: %s)" +keymap_unmapped(int sym) "sym=0x%04x" + +# x_keymap.c +xkeymap_extension(const char *name) "extension '%s'" +xkeymap_vendor(const char *name) "vendor '%s'" +xkeymap_keycodes(const char *name) "keycodes '%s'" +xkeymap_keymap(const char *name) "keymap '%s'" + +# vdagent.c +vdagent_open(void) "" +vdagent_close(void) "" +vdagent_send(const char *name) "msg %s" +vdagent_send_empty_clipboard(void) "" +vdagent_recv_chunk(uint32_t size) "size %d" +vdagent_recv_msg(const char *name, uint32_t size) "msg %s, size %d" +vdagent_peer_cap(const char *name) "cap %s" +vdagent_cb_grab_selection(const char *name) "selection %s" +vdagent_cb_grab_type(const char *name) "type %s" diff --git a/ui/trace.h b/ui/trace.h new file mode 100644 index 000000000..a89d76962 --- /dev/null +++ b/ui/trace.h @@ -0,0 +1 @@ +#include "trace/trace-ui.h" diff --git a/ui/udmabuf.c b/ui/udmabuf.c new file mode 100644 index 000000000..cebceb261 --- /dev/null +++ b/ui/udmabuf.c @@ -0,0 +1,29 @@ +/* + * udmabuf helper functions. + * + * This work is licensed under the terms of the GNU GPL, version 2 or later. + * See the COPYING file in the top-level directory. + */ +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "ui/console.h" + +#include <fcntl.h> +#include <sys/ioctl.h> + +int udmabuf_fd(void) +{ + static bool first = true; + static int udmabuf; + + if (!first) { + return udmabuf; + } + first = false; + + udmabuf = open("/dev/udmabuf", O_RDWR); + if (udmabuf < 0) { + warn_report("open /dev/udmabuf: %s", strerror(errno)); + } + return udmabuf; +} diff --git a/ui/vdagent.c b/ui/vdagent.c new file mode 100644 index 000000000..19e8fbfc9 --- /dev/null +++ b/ui/vdagent.c @@ -0,0 +1,871 @@ +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "include/qemu-common.h" +#include "chardev/char.h" +#include "qemu/buffer.h" +#include "qemu/option.h" +#include "qemu/units.h" +#include "hw/qdev-core.h" +#include "migration/blocker.h" +#include "ui/clipboard.h" +#include "ui/console.h" +#include "ui/input.h" +#include "trace.h" + +#include "qapi/qapi-types-char.h" +#include "qapi/qapi-types-ui.h" + +#include "spice/vd_agent.h" + +#define VDAGENT_BUFFER_LIMIT (1 * MiB) +#define VDAGENT_MOUSE_DEFAULT true +#define VDAGENT_CLIPBOARD_DEFAULT false + +struct VDAgentChardev { + Chardev parent; + + /* TODO: migration isn't yet supported */ + Error *migration_blocker; + + /* config */ + bool mouse; + bool clipboard; + + /* guest vdagent */ + uint32_t caps; + VDIChunkHeader chunk; + uint32_t chunksize; + uint8_t *msgbuf; + uint32_t msgsize; + uint8_t *xbuf; + uint32_t xoff, xsize; + Buffer outbuf; + + /* mouse */ + DeviceState mouse_dev; + uint32_t mouse_x; + uint32_t mouse_y; + uint32_t mouse_btn; + uint32_t mouse_display; + QemuInputHandlerState *mouse_hs; + + /* clipboard */ + QemuClipboardPeer cbpeer; + uint32_t cbpending[QEMU_CLIPBOARD_SELECTION__COUNT]; +}; +typedef struct VDAgentChardev VDAgentChardev; + +#define TYPE_CHARDEV_QEMU_VDAGENT "chardev-qemu-vdagent" + +DECLARE_INSTANCE_CHECKER(VDAgentChardev, QEMU_VDAGENT_CHARDEV, + TYPE_CHARDEV_QEMU_VDAGENT); + +/* ------------------------------------------------------------------ */ +/* names, for debug logging */ + +static const char *cap_name[] = { + [VD_AGENT_CAP_MOUSE_STATE] = "mouse-state", + [VD_AGENT_CAP_MONITORS_CONFIG] = "monitors-config", + [VD_AGENT_CAP_REPLY] = "reply", + [VD_AGENT_CAP_CLIPBOARD] = "clipboard", + [VD_AGENT_CAP_DISPLAY_CONFIG] = "display-config", + [VD_AGENT_CAP_CLIPBOARD_BY_DEMAND] = "clipboard-by-demand", + [VD_AGENT_CAP_CLIPBOARD_SELECTION] = "clipboard-selection", + [VD_AGENT_CAP_SPARSE_MONITORS_CONFIG] = "sparse-monitors-config", + [VD_AGENT_CAP_GUEST_LINEEND_LF] = "guest-lineend-lf", + [VD_AGENT_CAP_GUEST_LINEEND_CRLF] = "guest-lineend-crlf", + [VD_AGENT_CAP_MAX_CLIPBOARD] = "max-clipboard", + [VD_AGENT_CAP_AUDIO_VOLUME_SYNC] = "audio-volume-sync", + [VD_AGENT_CAP_MONITORS_CONFIG_POSITION] = "monitors-config-position", + [VD_AGENT_CAP_FILE_XFER_DISABLED] = "file-xfer-disabled", + [VD_AGENT_CAP_FILE_XFER_DETAILED_ERRORS] = "file-xfer-detailed-errors", +#if 0 + [VD_AGENT_CAP_GRAPHICS_DEVICE_INFO] = "graphics-device-info", + [VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB] = "clipboard-no-release-on-regrab", + [VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL] = "clipboard-grab-serial", +#endif +}; + +static const char *msg_name[] = { + [VD_AGENT_MOUSE_STATE] = "mouse-state", + [VD_AGENT_MONITORS_CONFIG] = "monitors-config", + [VD_AGENT_REPLY] = "reply", + [VD_AGENT_CLIPBOARD] = "clipboard", + [VD_AGENT_DISPLAY_CONFIG] = "display-config", + [VD_AGENT_ANNOUNCE_CAPABILITIES] = "announce-capabilities", + [VD_AGENT_CLIPBOARD_GRAB] = "clipboard-grab", + [VD_AGENT_CLIPBOARD_REQUEST] = "clipboard-request", + [VD_AGENT_CLIPBOARD_RELEASE] = "clipboard-release", + [VD_AGENT_FILE_XFER_START] = "file-xfer-start", + [VD_AGENT_FILE_XFER_STATUS] = "file-xfer-status", + [VD_AGENT_FILE_XFER_DATA] = "file-xfer-data", + [VD_AGENT_CLIENT_DISCONNECTED] = "client-disconnected", + [VD_AGENT_MAX_CLIPBOARD] = "max-clipboard", + [VD_AGENT_AUDIO_VOLUME_SYNC] = "audio-volume-sync", +#if 0 + [VD_AGENT_GRAPHICS_DEVICE_INFO] = "graphics-device-info", +#endif +}; + +static const char *sel_name[] = { + [VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD] = "clipboard", + [VD_AGENT_CLIPBOARD_SELECTION_PRIMARY] = "primary", + [VD_AGENT_CLIPBOARD_SELECTION_SECONDARY] = "secondary", +}; + +static const char *type_name[] = { + [VD_AGENT_CLIPBOARD_NONE] = "none", + [VD_AGENT_CLIPBOARD_UTF8_TEXT] = "text", + [VD_AGENT_CLIPBOARD_IMAGE_PNG] = "png", + [VD_AGENT_CLIPBOARD_IMAGE_BMP] = "bmp", + [VD_AGENT_CLIPBOARD_IMAGE_TIFF] = "tiff", + [VD_AGENT_CLIPBOARD_IMAGE_JPG] = "jpg", +#if 0 + [VD_AGENT_CLIPBOARD_FILE_LIST] = "files", +#endif +}; + +#define GET_NAME(_m, _v) \ + (((_v) < ARRAY_SIZE(_m) && (_m[_v])) ? (_m[_v]) : "???") + +/* ------------------------------------------------------------------ */ +/* send messages */ + +static void vdagent_send_buf(VDAgentChardev *vd) +{ + uint32_t len; + + while (!buffer_empty(&vd->outbuf)) { + len = qemu_chr_be_can_write(CHARDEV(vd)); + if (len == 0) { + return; + } + if (len > vd->outbuf.offset) { + len = vd->outbuf.offset; + } + qemu_chr_be_write(CHARDEV(vd), vd->outbuf.buffer, len); + buffer_advance(&vd->outbuf, len); + } +} + +static void vdagent_send_msg(VDAgentChardev *vd, VDAgentMessage *msg) +{ + uint8_t *msgbuf = (void *)msg; + uint32_t msgsize = sizeof(VDAgentMessage) + msg->size; + uint32_t msgoff = 0; + VDIChunkHeader chunk; + + trace_vdagent_send(GET_NAME(msg_name, msg->type)); + + msg->protocol = VD_AGENT_PROTOCOL; + + if (vd->outbuf.offset + msgsize > VDAGENT_BUFFER_LIMIT) { + error_report("buffer full, dropping message"); + return; + } + + while (msgoff < msgsize) { + chunk.port = VDP_CLIENT_PORT; + chunk.size = msgsize - msgoff; + if (chunk.size > 1024) { + chunk.size = 1024; + } + buffer_reserve(&vd->outbuf, sizeof(chunk) + chunk.size); + buffer_append(&vd->outbuf, &chunk, sizeof(chunk)); + buffer_append(&vd->outbuf, msgbuf + msgoff, chunk.size); + msgoff += chunk.size; + } + vdagent_send_buf(vd); +} + +static void vdagent_send_caps(VDAgentChardev *vd) +{ + g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) + + sizeof(VDAgentAnnounceCapabilities) + + sizeof(uint32_t)); + VDAgentAnnounceCapabilities *caps = (void *)msg->data; + + msg->type = VD_AGENT_ANNOUNCE_CAPABILITIES; + msg->size = sizeof(VDAgentAnnounceCapabilities) + sizeof(uint32_t); + if (vd->mouse) { + caps->caps[0] |= (1 << VD_AGENT_CAP_MOUSE_STATE); + } + if (vd->clipboard) { + caps->caps[0] |= (1 << VD_AGENT_CAP_CLIPBOARD_BY_DEMAND); + caps->caps[0] |= (1 << VD_AGENT_CAP_CLIPBOARD_SELECTION); + } + + vdagent_send_msg(vd, msg); +} + +/* ------------------------------------------------------------------ */ +/* mouse events */ + +static bool have_mouse(VDAgentChardev *vd) +{ + return vd->mouse && + (vd->caps & (1 << VD_AGENT_CAP_MOUSE_STATE)); +} + +static void vdagent_send_mouse(VDAgentChardev *vd) +{ + g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) + + sizeof(VDAgentMouseState)); + VDAgentMouseState *mouse = (void *)msg->data; + + msg->type = VD_AGENT_MOUSE_STATE; + msg->size = sizeof(VDAgentMouseState); + + mouse->x = vd->mouse_x; + mouse->y = vd->mouse_y; + mouse->buttons = vd->mouse_btn; + mouse->display_id = vd->mouse_display; + + vdagent_send_msg(vd, msg); +} + +static void vdagent_pointer_event(DeviceState *dev, QemuConsole *src, + InputEvent *evt) +{ + static const int bmap[INPUT_BUTTON__MAX] = { + [INPUT_BUTTON_LEFT] = VD_AGENT_LBUTTON_MASK, + [INPUT_BUTTON_RIGHT] = VD_AGENT_RBUTTON_MASK, + [INPUT_BUTTON_MIDDLE] = VD_AGENT_MBUTTON_MASK, + [INPUT_BUTTON_WHEEL_UP] = VD_AGENT_UBUTTON_MASK, + [INPUT_BUTTON_WHEEL_DOWN] = VD_AGENT_DBUTTON_MASK, +#ifdef VD_AGENT_EBUTTON_MASK + [INPUT_BUTTON_SIDE] = VD_AGENT_SBUTTON_MASK, + [INPUT_BUTTON_EXTRA] = VD_AGENT_EBUTTON_MASK, +#endif + }; + + VDAgentChardev *vd = container_of(dev, struct VDAgentChardev, mouse_dev); + InputMoveEvent *move; + InputBtnEvent *btn; + uint32_t xres, yres; + + switch (evt->type) { + case INPUT_EVENT_KIND_ABS: + move = evt->u.abs.data; + xres = qemu_console_get_width(src, 1024); + yres = qemu_console_get_height(src, 768); + if (move->axis == INPUT_AXIS_X) { + vd->mouse_x = qemu_input_scale_axis(move->value, + INPUT_EVENT_ABS_MIN, + INPUT_EVENT_ABS_MAX, + 0, xres); + } else if (move->axis == INPUT_AXIS_Y) { + vd->mouse_y = qemu_input_scale_axis(move->value, + INPUT_EVENT_ABS_MIN, + INPUT_EVENT_ABS_MAX, + 0, yres); + } + vd->mouse_display = qemu_console_get_index(src); + break; + + case INPUT_EVENT_KIND_BTN: + btn = evt->u.btn.data; + if (btn->down) { + vd->mouse_btn |= bmap[btn->button]; + } else { + vd->mouse_btn &= ~bmap[btn->button]; + } + break; + + default: + /* keep gcc happy */ + break; + } +} + +static void vdagent_pointer_sync(DeviceState *dev) +{ + VDAgentChardev *vd = container_of(dev, struct VDAgentChardev, mouse_dev); + + if (vd->caps & (1 << VD_AGENT_CAP_MOUSE_STATE)) { + vdagent_send_mouse(vd); + } +} + +static QemuInputHandler vdagent_mouse_handler = { + .name = "vdagent mouse", + .mask = INPUT_EVENT_MASK_BTN | INPUT_EVENT_MASK_ABS, + .event = vdagent_pointer_event, + .sync = vdagent_pointer_sync, +}; + +/* ------------------------------------------------------------------ */ +/* clipboard */ + +static bool have_clipboard(VDAgentChardev *vd) +{ + return vd->clipboard && + (vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_BY_DEMAND)); +} + +static bool have_selection(VDAgentChardev *vd) +{ + return vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_SELECTION); +} + +static uint32_t type_qemu_to_vdagent(enum QemuClipboardType type) +{ + switch (type) { + case QEMU_CLIPBOARD_TYPE_TEXT: + return VD_AGENT_CLIPBOARD_UTF8_TEXT; + default: + return VD_AGENT_CLIPBOARD_NONE; + } +} + +static void vdagent_send_clipboard_grab(VDAgentChardev *vd, + QemuClipboardInfo *info) +{ + g_autofree VDAgentMessage *msg = + g_malloc0(sizeof(VDAgentMessage) + + sizeof(uint32_t) * (QEMU_CLIPBOARD_TYPE__COUNT + 1)); + uint8_t *s = msg->data; + uint32_t *data = (uint32_t *)msg->data; + uint32_t q, type; + + if (have_selection(vd)) { + *s = info->selection; + data++; + msg->size += sizeof(uint32_t); + } else if (info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) { + return; + } + + for (q = 0; q < QEMU_CLIPBOARD_TYPE__COUNT; q++) { + type = type_qemu_to_vdagent(q); + if (type != VD_AGENT_CLIPBOARD_NONE && info->types[q].available) { + *data = type; + data++; + msg->size += sizeof(uint32_t); + } + } + + msg->type = VD_AGENT_CLIPBOARD_GRAB; + vdagent_send_msg(vd, msg); +} + +static void vdagent_send_clipboard_release(VDAgentChardev *vd, + QemuClipboardInfo *info) +{ + g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) + + sizeof(uint32_t)); + + if (have_selection(vd)) { + uint8_t *s = msg->data; + *s = info->selection; + msg->size += sizeof(uint32_t); + } else if (info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) { + return; + } + + msg->type = VD_AGENT_CLIPBOARD_RELEASE; + vdagent_send_msg(vd, msg); +} + +static void vdagent_send_clipboard_data(VDAgentChardev *vd, + QemuClipboardInfo *info, + QemuClipboardType type) +{ + g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) + + sizeof(uint32_t) * 2 + + info->types[type].size); + + uint8_t *s = msg->data; + uint32_t *data = (uint32_t *)msg->data; + + if (have_selection(vd)) { + *s = info->selection; + data++; + msg->size += sizeof(uint32_t); + } else if (info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) { + return; + } + + *data = type_qemu_to_vdagent(type); + data++; + msg->size += sizeof(uint32_t); + + memcpy(data, info->types[type].data, info->types[type].size); + msg->size += info->types[type].size; + + msg->type = VD_AGENT_CLIPBOARD; + vdagent_send_msg(vd, msg); +} + +static void vdagent_send_empty_clipboard_data(VDAgentChardev *vd, + QemuClipboardSelection selection, + QemuClipboardType type) +{ + g_autoptr(QemuClipboardInfo) info = qemu_clipboard_info_new(&vd->cbpeer, selection); + + trace_vdagent_send_empty_clipboard(); + vdagent_send_clipboard_data(vd, info, type); +} + +static void vdagent_clipboard_notify(Notifier *notifier, void *data) +{ + VDAgentChardev *vd = container_of(notifier, VDAgentChardev, cbpeer.update); + QemuClipboardInfo *info = data; + QemuClipboardSelection s = info->selection; + QemuClipboardType type; + bool self_update = info->owner == &vd->cbpeer; + + if (info != qemu_clipboard_info(s)) { + vd->cbpending[s] = 0; + if (!self_update) { + if (info->owner) { + vdagent_send_clipboard_grab(vd, info); + } else { + vdagent_send_clipboard_release(vd, info); + } + } + return; + } + + if (self_update) { + return; + } + + for (type = 0; type < QEMU_CLIPBOARD_TYPE__COUNT; type++) { + if (vd->cbpending[s] & (1 << type)) { + vd->cbpending[s] &= ~(1 << type); + vdagent_send_clipboard_data(vd, info, type); + } + } +} + +static void vdagent_clipboard_request(QemuClipboardInfo *info, + QemuClipboardType qtype) +{ + VDAgentChardev *vd = container_of(info->owner, VDAgentChardev, cbpeer); + g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) + + sizeof(uint32_t) * 2); + uint32_t type = type_qemu_to_vdagent(qtype); + uint8_t *s = msg->data; + uint32_t *data = (uint32_t *)msg->data; + + if (type == VD_AGENT_CLIPBOARD_NONE) { + return; + } + + if (have_selection(vd)) { + *s = info->selection; + data++; + msg->size += sizeof(uint32_t); + } + + *data = type; + msg->size += sizeof(uint32_t); + + msg->type = VD_AGENT_CLIPBOARD_REQUEST; + vdagent_send_msg(vd, msg); +} + +static void vdagent_clipboard_recv_grab(VDAgentChardev *vd, uint8_t s, uint32_t size, void *data) +{ + g_autoptr(QemuClipboardInfo) info = NULL; + + trace_vdagent_cb_grab_selection(GET_NAME(sel_name, s)); + info = qemu_clipboard_info_new(&vd->cbpeer, s); + if (size > sizeof(uint32_t) * 10) { + /* + * spice has 6 types as of 2021. Limiting to 10 entries + * so we we have some wiggle room. + */ + return; + } + while (size >= sizeof(uint32_t)) { + trace_vdagent_cb_grab_type(GET_NAME(type_name, *(uint32_t *)data)); + switch (*(uint32_t *)data) { + case VD_AGENT_CLIPBOARD_UTF8_TEXT: + info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; + break; + default: + break; + } + data += sizeof(uint32_t); + size -= sizeof(uint32_t); + } + qemu_clipboard_update(info); +} + +static void vdagent_clipboard_recv_request(VDAgentChardev *vd, uint8_t s, uint32_t size, void *data) +{ + QemuClipboardType type; + QemuClipboardInfo *info; + + if (size < sizeof(uint32_t)) { + return; + } + switch (*(uint32_t *)data) { + case VD_AGENT_CLIPBOARD_UTF8_TEXT: + type = QEMU_CLIPBOARD_TYPE_TEXT; + break; + default: + return; + } + + info = qemu_clipboard_info(s); + if (info && info->types[type].available && info->owner != &vd->cbpeer) { + if (info->types[type].data) { + vdagent_send_clipboard_data(vd, info, type); + } else { + vd->cbpending[s] |= (1 << type); + qemu_clipboard_request(info, type); + } + } else { + vdagent_send_empty_clipboard_data(vd, s, type); + } +} + +static void vdagent_clipboard_recv_data(VDAgentChardev *vd, uint8_t s, uint32_t size, void *data) +{ + QemuClipboardType type; + + if (size < sizeof(uint32_t)) { + return; + } + switch (*(uint32_t *)data) { + case VD_AGENT_CLIPBOARD_UTF8_TEXT: + type = QEMU_CLIPBOARD_TYPE_TEXT; + break; + default: + return; + } + data += 4; + size -= 4; + + if (qemu_clipboard_peer_owns(&vd->cbpeer, s)) { + qemu_clipboard_set_data(&vd->cbpeer, qemu_clipboard_info(s), + type, size, data, true); + } +} + +static void vdagent_clipboard_recv_release(VDAgentChardev *vd, uint8_t s) +{ + qemu_clipboard_peer_release(&vd->cbpeer, s); +} + +static void vdagent_chr_recv_clipboard(VDAgentChardev *vd, VDAgentMessage *msg) +{ + uint8_t s = VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD; + uint32_t size = msg->size; + void *data = msg->data; + + if (have_selection(vd)) { + if (size < 4) { + return; + } + s = *(uint8_t *)data; + if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) { + return; + } + data += 4; + size -= 4; + } + + switch (msg->type) { + case VD_AGENT_CLIPBOARD_GRAB: + return vdagent_clipboard_recv_grab(vd, s, size, data); + case VD_AGENT_CLIPBOARD_REQUEST: + return vdagent_clipboard_recv_request(vd, s, size, data); + case VD_AGENT_CLIPBOARD: /* data */ + return vdagent_clipboard_recv_data(vd, s, size, data); + case VD_AGENT_CLIPBOARD_RELEASE: + return vdagent_clipboard_recv_release(vd, s); + default: + g_assert_not_reached(); + } +} + +/* ------------------------------------------------------------------ */ +/* chardev backend */ + +static void vdagent_chr_open(Chardev *chr, + ChardevBackend *backend, + bool *be_opened, + Error **errp) +{ + VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(chr); + ChardevQemuVDAgent *cfg = backend->u.qemu_vdagent.data; + +#if defined(HOST_WORDS_BIGENDIAN) + /* + * TODO: vdagent protocol is defined to be LE, + * so we have to byteswap everything on BE hosts. + */ + error_setg(errp, "vdagent is not supported on bigendian hosts"); + return; +#endif + + if (migrate_add_blocker(vd->migration_blocker, errp) != 0) { + return; + } + + vd->mouse = VDAGENT_MOUSE_DEFAULT; + if (cfg->has_mouse) { + vd->mouse = cfg->mouse; + } + + vd->clipboard = VDAGENT_CLIPBOARD_DEFAULT; + if (cfg->has_clipboard) { + vd->clipboard = cfg->clipboard; + } + + if (vd->mouse) { + vd->mouse_hs = qemu_input_handler_register(&vd->mouse_dev, + &vdagent_mouse_handler); + } + + *be_opened = true; +} + +static void vdagent_chr_recv_caps(VDAgentChardev *vd, VDAgentMessage *msg) +{ + VDAgentAnnounceCapabilities *caps = (void *)msg->data; + int i; + + if (msg->size < (sizeof(VDAgentAnnounceCapabilities) + + sizeof(uint32_t))) { + return; + } + + for (i = 0; i < ARRAY_SIZE(cap_name); i++) { + if (caps->caps[0] & (1 << i)) { + trace_vdagent_peer_cap(GET_NAME(cap_name, i)); + } + } + + vd->caps = caps->caps[0]; + if (caps->request) { + vdagent_send_caps(vd); + } + if (have_mouse(vd) && vd->mouse_hs) { + qemu_input_handler_activate(vd->mouse_hs); + } + if (have_clipboard(vd) && vd->cbpeer.update.notify == NULL) { + vd->cbpeer.name = "vdagent"; + vd->cbpeer.update.notify = vdagent_clipboard_notify; + vd->cbpeer.request = vdagent_clipboard_request; + qemu_clipboard_peer_register(&vd->cbpeer); + } +} + +static void vdagent_chr_recv_msg(VDAgentChardev *vd, VDAgentMessage *msg) +{ + trace_vdagent_recv_msg(GET_NAME(msg_name, msg->type), msg->size); + + switch (msg->type) { + case VD_AGENT_ANNOUNCE_CAPABILITIES: + vdagent_chr_recv_caps(vd, msg); + break; + case VD_AGENT_CLIPBOARD: + case VD_AGENT_CLIPBOARD_GRAB: + case VD_AGENT_CLIPBOARD_REQUEST: + case VD_AGENT_CLIPBOARD_RELEASE: + if (have_clipboard(vd)) { + vdagent_chr_recv_clipboard(vd, msg); + } + break; + default: + break; + } +} + +static void vdagent_reset_xbuf(VDAgentChardev *vd) +{ + g_clear_pointer(&vd->xbuf, g_free); + vd->xoff = 0; + vd->xsize = 0; +} + +static void vdagent_chr_recv_chunk(VDAgentChardev *vd) +{ + VDAgentMessage *msg = (void *)vd->msgbuf; + + if (!vd->xsize) { + if (vd->msgsize < sizeof(*msg)) { + error_report("%s: message too small: %d < %zd", __func__, + vd->msgsize, sizeof(*msg)); + return; + } + if (vd->msgsize == msg->size + sizeof(*msg)) { + vdagent_chr_recv_msg(vd, msg); + return; + } + } + + if (!vd->xsize) { + vd->xsize = msg->size + sizeof(*msg); + vd->xbuf = g_malloc0(vd->xsize); + } + + if (vd->xoff + vd->msgsize > vd->xsize) { + error_report("%s: Oops: %d+%d > %d", __func__, + vd->xoff, vd->msgsize, vd->xsize); + vdagent_reset_xbuf(vd); + return; + } + + memcpy(vd->xbuf + vd->xoff, vd->msgbuf, vd->msgsize); + vd->xoff += vd->msgsize; + if (vd->xoff < vd->xsize) { + return; + } + + msg = (void *)vd->xbuf; + vdagent_chr_recv_msg(vd, msg); + vdagent_reset_xbuf(vd); +} + +static void vdagent_reset_bufs(VDAgentChardev *vd) +{ + memset(&vd->chunk, 0, sizeof(vd->chunk)); + vd->chunksize = 0; + g_free(vd->msgbuf); + vd->msgbuf = NULL; + vd->msgsize = 0; +} + +static int vdagent_chr_write(Chardev *chr, const uint8_t *buf, int len) +{ + VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(chr); + uint32_t copy, ret = len; + + while (len) { + if (vd->chunksize < sizeof(vd->chunk)) { + copy = sizeof(vd->chunk) - vd->chunksize; + if (copy > len) { + copy = len; + } + memcpy((void *)(&vd->chunk) + vd->chunksize, buf, copy); + vd->chunksize += copy; + buf += copy; + len -= copy; + if (vd->chunksize < sizeof(vd->chunk)) { + break; + } + + assert(vd->msgbuf == NULL); + vd->msgbuf = g_malloc0(vd->chunk.size); + } + + copy = vd->chunk.size - vd->msgsize; + if (copy > len) { + copy = len; + } + memcpy(vd->msgbuf + vd->msgsize, buf, copy); + vd->msgsize += copy; + buf += copy; + len -= copy; + + if (vd->msgsize == vd->chunk.size) { + trace_vdagent_recv_chunk(vd->chunk.size); + vdagent_chr_recv_chunk(vd); + vdagent_reset_bufs(vd); + } + } + + return ret; +} + +static void vdagent_chr_accept_input(Chardev *chr) +{ + VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(chr); + + vdagent_send_buf(vd); +} + +static void vdagent_disconnect(VDAgentChardev *vd) +{ + buffer_reset(&vd->outbuf); + vdagent_reset_bufs(vd); + vd->caps = 0; + if (vd->mouse_hs) { + qemu_input_handler_deactivate(vd->mouse_hs); + } + if (vd->cbpeer.update.notify) { + qemu_clipboard_peer_unregister(&vd->cbpeer); + memset(&vd->cbpeer, 0, sizeof(vd->cbpeer)); + } +} + +static void vdagent_chr_set_fe_open(struct Chardev *chr, int fe_open) +{ + VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(chr); + + if (!fe_open) { + trace_vdagent_close(); + vdagent_disconnect(vd); + return; + } + + trace_vdagent_open(); +} + +static void vdagent_chr_parse(QemuOpts *opts, ChardevBackend *backend, + Error **errp) +{ + ChardevQemuVDAgent *cfg; + + backend->type = CHARDEV_BACKEND_KIND_QEMU_VDAGENT; + cfg = backend->u.qemu_vdagent.data = g_new0(ChardevQemuVDAgent, 1); + qemu_chr_parse_common(opts, qapi_ChardevQemuVDAgent_base(cfg)); + cfg->has_mouse = true; + cfg->mouse = qemu_opt_get_bool(opts, "mouse", VDAGENT_MOUSE_DEFAULT); + cfg->has_clipboard = true; + cfg->clipboard = qemu_opt_get_bool(opts, "clipboard", VDAGENT_CLIPBOARD_DEFAULT); +} + +/* ------------------------------------------------------------------ */ + +static void vdagent_chr_class_init(ObjectClass *oc, void *data) +{ + ChardevClass *cc = CHARDEV_CLASS(oc); + + cc->parse = vdagent_chr_parse; + cc->open = vdagent_chr_open; + cc->chr_write = vdagent_chr_write; + cc->chr_set_fe_open = vdagent_chr_set_fe_open; + cc->chr_accept_input = vdagent_chr_accept_input; +} + +static void vdagent_chr_init(Object *obj) +{ + VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(obj); + + buffer_init(&vd->outbuf, "vdagent-outbuf"); + error_setg(&vd->migration_blocker, + "The vdagent chardev doesn't yet support migration"); +} + +static void vdagent_chr_fini(Object *obj) +{ + VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(obj); + + migrate_del_blocker(vd->migration_blocker); + vdagent_disconnect(vd); + buffer_free(&vd->outbuf); + error_free(vd->migration_blocker); +} + +static const TypeInfo vdagent_chr_type_info = { + .name = TYPE_CHARDEV_QEMU_VDAGENT, + .parent = TYPE_CHARDEV, + .instance_size = sizeof(VDAgentChardev), + .instance_init = vdagent_chr_init, + .instance_finalize = vdagent_chr_fini, + .class_init = vdagent_chr_class_init, +}; + +static void register_types(void) +{ + type_register_static(&vdagent_chr_type_info); +} + +type_init(register_types); diff --git a/ui/vgafont.h b/ui/vgafont.h new file mode 100644 index 000000000..3606dd733 --- /dev/null +++ b/ui/vgafont.h @@ -0,0 +1,4611 @@ +static const uint8_t vgafont16[256 * 16] = { + + /* 0 0x00 '^@' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 1 0x01 '^A' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0x81, /* 10000001 */ + 0xa5, /* 10100101 */ + 0x81, /* 10000001 */ + 0x81, /* 10000001 */ + 0xbd, /* 10111101 */ + 0x99, /* 10011001 */ + 0x81, /* 10000001 */ + 0x81, /* 10000001 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 2 0x02 '^B' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0xff, /* 11111111 */ + 0xdb, /* 11011011 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xc3, /* 11000011 */ + 0xe7, /* 11100111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 3 0x03 '^C' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x6c, /* 01101100 */ + 0xfe, /* 11111110 */ + 0xfe, /* 11111110 */ + 0xfe, /* 11111110 */ + 0xfe, /* 11111110 */ + 0x7c, /* 01111100 */ + 0x38, /* 00111000 */ + 0x10, /* 00010000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 4 0x04 '^D' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x10, /* 00010000 */ + 0x38, /* 00111000 */ + 0x7c, /* 01111100 */ + 0xfe, /* 11111110 */ + 0x7c, /* 01111100 */ + 0x38, /* 00111000 */ + 0x10, /* 00010000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 5 0x05 '^E' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x3c, /* 00111100 */ + 0xe7, /* 11100111 */ + 0xe7, /* 11100111 */ + 0xe7, /* 11100111 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 6 0x06 '^F' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x7e, /* 01111110 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0x7e, /* 01111110 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 7 0x07 '^G' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x3c, /* 00111100 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 8 0x08 '^H' */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xe7, /* 11100111 */ + 0xc3, /* 11000011 */ + 0xc3, /* 11000011 */ + 0xe7, /* 11100111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + + /* 9 0x09 '^I' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x3c, /* 00111100 */ + 0x66, /* 01100110 */ + 0x42, /* 01000010 */ + 0x42, /* 01000010 */ + 0x66, /* 01100110 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 10 0x0a '^J' */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xc3, /* 11000011 */ + 0x99, /* 10011001 */ + 0xbd, /* 10111101 */ + 0xbd, /* 10111101 */ + 0x99, /* 10011001 */ + 0xc3, /* 11000011 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + + /* 11 0x0b '^K' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x1e, /* 00011110 */ + 0x0e, /* 00001110 */ + 0x1a, /* 00011010 */ + 0x32, /* 00110010 */ + 0x78, /* 01111000 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x78, /* 01111000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 12 0x0c '^L' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x3c, /* 00111100 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x3c, /* 00111100 */ + 0x18, /* 00011000 */ + 0x7e, /* 01111110 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 13 0x0d '^M' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x3f, /* 00111111 */ + 0x33, /* 00110011 */ + 0x3f, /* 00111111 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x70, /* 01110000 */ + 0xf0, /* 11110000 */ + 0xe0, /* 11100000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 14 0x0e '^N' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7f, /* 01111111 */ + 0x63, /* 01100011 */ + 0x7f, /* 01111111 */ + 0x63, /* 01100011 */ + 0x63, /* 01100011 */ + 0x63, /* 01100011 */ + 0x63, /* 01100011 */ + 0x67, /* 01100111 */ + 0xe7, /* 11100111 */ + 0xe6, /* 11100110 */ + 0xc0, /* 11000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 15 0x0f '^O' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0xdb, /* 11011011 */ + 0x3c, /* 00111100 */ + 0xe7, /* 11100111 */ + 0x3c, /* 00111100 */ + 0xdb, /* 11011011 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 16 0x10 '^P' */ + 0x00, /* 00000000 */ + 0x80, /* 10000000 */ + 0xc0, /* 11000000 */ + 0xe0, /* 11100000 */ + 0xf0, /* 11110000 */ + 0xf8, /* 11111000 */ + 0xfe, /* 11111110 */ + 0xf8, /* 11111000 */ + 0xf0, /* 11110000 */ + 0xe0, /* 11100000 */ + 0xc0, /* 11000000 */ + 0x80, /* 10000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 17 0x11 '^Q' */ + 0x00, /* 00000000 */ + 0x02, /* 00000010 */ + 0x06, /* 00000110 */ + 0x0e, /* 00001110 */ + 0x1e, /* 00011110 */ + 0x3e, /* 00111110 */ + 0xfe, /* 11111110 */ + 0x3e, /* 00111110 */ + 0x1e, /* 00011110 */ + 0x0e, /* 00001110 */ + 0x06, /* 00000110 */ + 0x02, /* 00000010 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 18 0x12 '^R' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x7e, /* 01111110 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x7e, /* 01111110 */ + 0x3c, /* 00111100 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 19 0x13 '^S' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x00, /* 00000000 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 20 0x14 '^T' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7f, /* 01111111 */ + 0xdb, /* 11011011 */ + 0xdb, /* 11011011 */ + 0xdb, /* 11011011 */ + 0x7b, /* 01111011 */ + 0x1b, /* 00011011 */ + 0x1b, /* 00011011 */ + 0x1b, /* 00011011 */ + 0x1b, /* 00011011 */ + 0x1b, /* 00011011 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 21 0x15 '^U' */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0x60, /* 01100000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x6c, /* 01101100 */ + 0x38, /* 00111000 */ + 0x0c, /* 00001100 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 22 0x16 '^V' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0xfe, /* 11111110 */ + 0xfe, /* 11111110 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 23 0x17 '^W' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x7e, /* 01111110 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x7e, /* 01111110 */ + 0x3c, /* 00111100 */ + 0x18, /* 00011000 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 24 0x18 '^X' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x7e, /* 01111110 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 25 0x19 '^Y' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x7e, /* 01111110 */ + 0x3c, /* 00111100 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 26 0x1a '^Z' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x0c, /* 00001100 */ + 0xfe, /* 11111110 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 27 0x1b '^[' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0xfe, /* 11111110 */ + 0x60, /* 01100000 */ + 0x30, /* 00110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 28 0x1c '^\' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 29 0x1d '^]' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x28, /* 00101000 */ + 0x6c, /* 01101100 */ + 0xfe, /* 11111110 */ + 0x6c, /* 01101100 */ + 0x28, /* 00101000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 30 0x1e '^^' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x10, /* 00010000 */ + 0x38, /* 00111000 */ + 0x38, /* 00111000 */ + 0x7c, /* 01111100 */ + 0x7c, /* 01111100 */ + 0xfe, /* 11111110 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 31 0x1f '^_' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0xfe, /* 11111110 */ + 0x7c, /* 01111100 */ + 0x7c, /* 01111100 */ + 0x38, /* 00111000 */ + 0x38, /* 00111000 */ + 0x10, /* 00010000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 32 0x20 ' ' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 33 0x21 '!' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x3c, /* 00111100 */ + 0x3c, /* 00111100 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 34 0x22 '"' */ + 0x00, /* 00000000 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x24, /* 00100100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 35 0x23 '#' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0xfe, /* 11111110 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0xfe, /* 11111110 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 36 0x24 '$' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc2, /* 11000010 */ + 0xc0, /* 11000000 */ + 0x7c, /* 01111100 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x86, /* 10000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 37 0x25 '%' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc2, /* 11000010 */ + 0xc6, /* 11000110 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0xc6, /* 11000110 */ + 0x86, /* 10000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 38 0x26 '&' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x38, /* 00111000 */ + 0x76, /* 01110110 */ + 0xdc, /* 11011100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 39 0x27 ''' */ + 0x00, /* 00000000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 40 0x28 '(' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x0c, /* 00001100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 41 0x29 ')' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 42 0x2a '*' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x66, /* 01100110 */ + 0x3c, /* 00111100 */ + 0xff, /* 11111111 */ + 0x3c, /* 00111100 */ + 0x66, /* 01100110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 43 0x2b '+' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x7e, /* 01111110 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 44 0x2c ',' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 45 0x2d '-' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 46 0x2e '.' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 47 0x2f '/' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x02, /* 00000010 */ + 0x06, /* 00000110 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0xc0, /* 11000000 */ + 0x80, /* 10000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 48 0x30 '0' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xd6, /* 11010110 */ + 0xd6, /* 11010110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x6c, /* 01101100 */ + 0x38, /* 00111000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 49 0x31 '1' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x38, /* 00111000 */ + 0x78, /* 01111000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 50 0x32 '2' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0x06, /* 00000110 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0xc0, /* 11000000 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 51 0x33 '3' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x3c, /* 00111100 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 52 0x34 '4' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x0c, /* 00001100 */ + 0x1c, /* 00011100 */ + 0x3c, /* 00111100 */ + 0x6c, /* 01101100 */ + 0xcc, /* 11001100 */ + 0xfe, /* 11111110 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x1e, /* 00011110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 53 0x35 '5' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xfc, /* 11111100 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 54 0x36 '6' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x60, /* 01100000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xfc, /* 11111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 55 0x37 '7' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0xc6, /* 11000110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 56 0x38 '8' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 57 0x39 '9' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7e, /* 01111110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x0c, /* 00001100 */ + 0x78, /* 01111000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 58 0x3a ':' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 59 0x3b ';' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 60 0x3c '<' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x06, /* 00000110 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x0c, /* 00001100 */ + 0x06, /* 00000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 61 0x3d '=' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 62 0x3e '>' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x60, /* 01100000 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x0c, /* 00001100 */ + 0x06, /* 00000110 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 63 0x3f '?' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 64 0x40 '@' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xde, /* 11011110 */ + 0xde, /* 11011110 */ + 0xde, /* 11011110 */ + 0xdc, /* 11011100 */ + 0xc0, /* 11000000 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 65 0x41 'A' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x10, /* 00010000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 66 0x42 'B' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfc, /* 11111100 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x7c, /* 01111100 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0xfc, /* 11111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 67 0x43 'C' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x3c, /* 00111100 */ + 0x66, /* 01100110 */ + 0xc2, /* 11000010 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc2, /* 11000010 */ + 0x66, /* 01100110 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 68 0x44 'D' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xf8, /* 11111000 */ + 0x6c, /* 01101100 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x6c, /* 01101100 */ + 0xf8, /* 11111000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 69 0x45 'E' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0x66, /* 01100110 */ + 0x62, /* 01100010 */ + 0x68, /* 01101000 */ + 0x78, /* 01111000 */ + 0x68, /* 01101000 */ + 0x60, /* 01100000 */ + 0x62, /* 01100010 */ + 0x66, /* 01100110 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 70 0x46 'F' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0x66, /* 01100110 */ + 0x62, /* 01100010 */ + 0x68, /* 01101000 */ + 0x78, /* 01111000 */ + 0x68, /* 01101000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0xf0, /* 11110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 71 0x47 'G' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x3c, /* 00111100 */ + 0x66, /* 01100110 */ + 0xc2, /* 11000010 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xde, /* 11011110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x66, /* 01100110 */ + 0x3a, /* 00111010 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 72 0x48 'H' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 73 0x49 'I' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x3c, /* 00111100 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 74 0x4a 'J' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x1e, /* 00011110 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x78, /* 01111000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 75 0x4b 'K' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xe6, /* 11100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x6c, /* 01101100 */ + 0x78, /* 01111000 */ + 0x78, /* 01111000 */ + 0x6c, /* 01101100 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0xe6, /* 11100110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 76 0x4c 'L' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xf0, /* 11110000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x62, /* 01100010 */ + 0x66, /* 01100110 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 77 0x4d 'M' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0xee, /* 11101110 */ + 0xfe, /* 11111110 */ + 0xfe, /* 11111110 */ + 0xd6, /* 11010110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 78 0x4e 'N' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0xe6, /* 11100110 */ + 0xf6, /* 11110110 */ + 0xfe, /* 11111110 */ + 0xde, /* 11011110 */ + 0xce, /* 11001110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 79 0x4f 'O' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 80 0x50 'P' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfc, /* 11111100 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x7c, /* 01111100 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0xf0, /* 11110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 81 0x51 'Q' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xd6, /* 11010110 */ + 0xde, /* 11011110 */ + 0x7c, /* 01111100 */ + 0x0c, /* 00001100 */ + 0x0e, /* 00001110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 82 0x52 'R' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfc, /* 11111100 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x7c, /* 01111100 */ + 0x6c, /* 01101100 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0xe6, /* 11100110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 83 0x53 'S' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x60, /* 01100000 */ + 0x38, /* 00111000 */ + 0x0c, /* 00001100 */ + 0x06, /* 00000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 84 0x54 'T' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0x7e, /* 01111110 */ + 0x5a, /* 01011010 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 85 0x55 'U' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 86 0x56 'V' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x6c, /* 01101100 */ + 0x38, /* 00111000 */ + 0x10, /* 00010000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 87 0x57 'W' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xd6, /* 11010110 */ + 0xd6, /* 11010110 */ + 0xd6, /* 11010110 */ + 0xfe, /* 11111110 */ + 0xee, /* 11101110 */ + 0x6c, /* 01101100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 88 0x58 'X' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x6c, /* 01101100 */ + 0x7c, /* 01111100 */ + 0x38, /* 00111000 */ + 0x38, /* 00111000 */ + 0x7c, /* 01111100 */ + 0x6c, /* 01101100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 89 0x59 'Y' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x3c, /* 00111100 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 90 0x5a 'Z' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0xc6, /* 11000110 */ + 0x86, /* 10000110 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0xc2, /* 11000010 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 91 0x5b '[' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x3c, /* 00111100 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 92 0x5c '\' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x80, /* 10000000 */ + 0xc0, /* 11000000 */ + 0xe0, /* 11100000 */ + 0x70, /* 01110000 */ + 0x38, /* 00111000 */ + 0x1c, /* 00011100 */ + 0x0e, /* 00001110 */ + 0x06, /* 00000110 */ + 0x02, /* 00000010 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 93 0x5d ']' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x3c, /* 00111100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 94 0x5e '^' */ + 0x10, /* 00010000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 95 0x5f '_' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xff, /* 11111111 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 96 0x60 '`' */ + 0x00, /* 00000000 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x0c, /* 00001100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 97 0x61 'a' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x78, /* 01111000 */ + 0x0c, /* 00001100 */ + 0x7c, /* 01111100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 98 0x62 'b' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xe0, /* 11100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x78, /* 01111000 */ + 0x6c, /* 01101100 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 99 0x63 'c' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 100 0x64 'd' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x1c, /* 00011100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x3c, /* 00111100 */ + 0x6c, /* 01101100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 101 0x65 'e' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 102 0x66 'f' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x1c, /* 00011100 */ + 0x36, /* 00110110 */ + 0x32, /* 00110010 */ + 0x30, /* 00110000 */ + 0x78, /* 01111000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x78, /* 01111000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 103 0x67 'g' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x76, /* 01110110 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x7c, /* 01111100 */ + 0x0c, /* 00001100 */ + 0xcc, /* 11001100 */ + 0x78, /* 01111000 */ + 0x00, /* 00000000 */ + + /* 104 0x68 'h' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xe0, /* 11100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x6c, /* 01101100 */ + 0x76, /* 01110110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0xe6, /* 11100110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 105 0x69 'i' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 106 0x6a 'j' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x00, /* 00000000 */ + 0x0e, /* 00001110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + + /* 107 0x6b 'k' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xe0, /* 11100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x66, /* 01100110 */ + 0x6c, /* 01101100 */ + 0x78, /* 01111000 */ + 0x78, /* 01111000 */ + 0x6c, /* 01101100 */ + 0x66, /* 01100110 */ + 0xe6, /* 11100110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 108 0x6c 'l' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 109 0x6d 'm' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xec, /* 11101100 */ + 0xfe, /* 11111110 */ + 0xd6, /* 11010110 */ + 0xd6, /* 11010110 */ + 0xd6, /* 11010110 */ + 0xd6, /* 11010110 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 110 0x6e 'n' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xdc, /* 11011100 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 111 0x6f 'o' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 112 0x70 'p' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xdc, /* 11011100 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x7c, /* 01111100 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0xf0, /* 11110000 */ + 0x00, /* 00000000 */ + + /* 113 0x71 'q' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x76, /* 01110110 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x7c, /* 01111100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x1e, /* 00011110 */ + 0x00, /* 00000000 */ + + /* 114 0x72 'r' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xdc, /* 11011100 */ + 0x76, /* 01110110 */ + 0x66, /* 01100110 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0xf0, /* 11110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 115 0x73 's' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0x60, /* 01100000 */ + 0x38, /* 00111000 */ + 0x0c, /* 00001100 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 116 0x74 't' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x10, /* 00010000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0xfc, /* 11111100 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x36, /* 00110110 */ + 0x1c, /* 00011100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 117 0x75 'u' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 118 0x76 'v' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x6c, /* 01101100 */ + 0x38, /* 00111000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 119 0x77 'w' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xd6, /* 11010110 */ + 0xd6, /* 11010110 */ + 0xd6, /* 11010110 */ + 0xfe, /* 11111110 */ + 0x6c, /* 01101100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 120 0x78 'x' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0x6c, /* 01101100 */ + 0x38, /* 00111000 */ + 0x38, /* 00111000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 121 0x79 'y' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7e, /* 01111110 */ + 0x06, /* 00000110 */ + 0x0c, /* 00001100 */ + 0xf8, /* 11111000 */ + 0x00, /* 00000000 */ + + /* 122 0x7a 'z' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0xcc, /* 11001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 123 0x7b '{' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x0e, /* 00001110 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x70, /* 01110000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x0e, /* 00001110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 124 0x7c '|' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 125 0x7d '}' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x70, /* 01110000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x0e, /* 00001110 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x70, /* 01110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 126 0x7e '~' */ + 0x00, /* 00000000 */ + 0x76, /* 01110110 */ + 0xdc, /* 11011100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 127 0x7f '' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x10, /* 00010000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 128 0x80 '' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x3c, /* 00111100 */ + 0x66, /* 01100110 */ + 0xc2, /* 11000010 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc2, /* 11000010 */ + 0x66, /* 01100110 */ + 0x3c, /* 00111100 */ + 0x18, /* 00011000 */ + 0x70, /* 01110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 129 0x81 '' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xcc, /* 11001100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 130 0x82 '' */ + 0x00, /* 00000000 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 131 0x83 '' */ + 0x00, /* 00000000 */ + 0x10, /* 00010000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0x00, /* 00000000 */ + 0x78, /* 01111000 */ + 0x0c, /* 00001100 */ + 0x7c, /* 01111100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 132 0x84 '' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xcc, /* 11001100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x78, /* 01111000 */ + 0x0c, /* 00001100 */ + 0x7c, /* 01111100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 133 0x85 '
' */ + 0x00, /* 00000000 */ + 0x60, /* 01100000 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x78, /* 01111000 */ + 0x0c, /* 00001100 */ + 0x7c, /* 01111100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 134 0x86 '' */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0x38, /* 00111000 */ + 0x00, /* 00000000 */ + 0x78, /* 01111000 */ + 0x0c, /* 00001100 */ + 0x7c, /* 01111100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 135 0x87 '' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x18, /* 00011000 */ + 0x70, /* 01110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 136 0x88 '' */ + 0x00, /* 00000000 */ + 0x10, /* 00010000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 137 0x89 '' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 138 0x8a '' */ + 0x00, /* 00000000 */ + 0x60, /* 01100000 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 139 0x8b '' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x66, /* 01100110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 140 0x8c '' */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x66, /* 01100110 */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 141 0x8d '' */ + 0x00, /* 00000000 */ + 0x60, /* 01100000 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 142 0x8e '' */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x10, /* 00010000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 143 0x8f '' */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0x38, /* 00111000 */ + 0x10, /* 00010000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 144 0x90 '' */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0x66, /* 01100110 */ + 0x62, /* 01100010 */ + 0x68, /* 01101000 */ + 0x78, /* 01111000 */ + 0x68, /* 01101000 */ + 0x62, /* 01100010 */ + 0x66, /* 01100110 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 145 0x91 '' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xec, /* 11101100 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x7e, /* 01111110 */ + 0xd8, /* 11011000 */ + 0xd8, /* 11011000 */ + 0x6e, /* 01101110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 146 0x92 '' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x3e, /* 00111110 */ + 0x6c, /* 01101100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xfe, /* 11111110 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xce, /* 11001110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 147 0x93 '' */ + 0x00, /* 00000000 */ + 0x10, /* 00010000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 148 0x94 '' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 149 0x95 '' */ + 0x00, /* 00000000 */ + 0x60, /* 01100000 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 150 0x96 '' */ + 0x00, /* 00000000 */ + 0x30, /* 00110000 */ + 0x78, /* 01111000 */ + 0xcc, /* 11001100 */ + 0x00, /* 00000000 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 151 0x97 '' */ + 0x00, /* 00000000 */ + 0x60, /* 01100000 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 152 0x98 '' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7e, /* 01111110 */ + 0x06, /* 00000110 */ + 0x0c, /* 00001100 */ + 0x78, /* 01111000 */ + 0x00, /* 00000000 */ + + /* 153 0x99 '' */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 154 0x9a '' */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 155 0x9b '' */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 156 0x9c '' */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0x64, /* 01100100 */ + 0x60, /* 01100000 */ + 0xf0, /* 11110000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0xe6, /* 11100110 */ + 0xfc, /* 11111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 157 0x9d '' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x3c, /* 00111100 */ + 0x18, /* 00011000 */ + 0x7e, /* 01111110 */ + 0x18, /* 00011000 */ + 0x7e, /* 01111110 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 158 0x9e '' */ + 0x00, /* 00000000 */ + 0xf8, /* 11111000 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xf8, /* 11111000 */ + 0xc4, /* 11000100 */ + 0xcc, /* 11001100 */ + 0xde, /* 11011110 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 159 0x9f '' */ + 0x00, /* 00000000 */ + 0x0e, /* 00001110 */ + 0x1b, /* 00011011 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x7e, /* 01111110 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0xd8, /* 11011000 */ + 0x70, /* 01110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 160 0xa0 ' ' */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0x00, /* 00000000 */ + 0x78, /* 01111000 */ + 0x0c, /* 00001100 */ + 0x7c, /* 01111100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 161 0xa1 '¡' */ + 0x00, /* 00000000 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 162 0xa2 '¢' */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 163 0xa3 '£' */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0x00, /* 00000000 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 164 0xa4 '¤' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x76, /* 01110110 */ + 0xdc, /* 11011100 */ + 0x00, /* 00000000 */ + 0xdc, /* 11011100 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 165 0xa5 '¥' */ + 0x76, /* 01110110 */ + 0xdc, /* 11011100 */ + 0x00, /* 00000000 */ + 0xc6, /* 11000110 */ + 0xe6, /* 11100110 */ + 0xf6, /* 11110110 */ + 0xfe, /* 11111110 */ + 0xde, /* 11011110 */ + 0xce, /* 11001110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 166 0xa6 '¦' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x3c, /* 00111100 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x3e, /* 00111110 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 167 0xa7 '§' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x38, /* 00111000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 168 0xa8 '¨' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x00, /* 00000000 */ + 0x30, /* 00110000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0xc0, /* 11000000 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x7c, /* 01111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 169 0xa9 '©' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 170 0xaa 'ª' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 171 0xab '«' */ + 0x00, /* 00000000 */ + 0x60, /* 01100000 */ + 0xe0, /* 11100000 */ + 0x62, /* 01100010 */ + 0x66, /* 01100110 */ + 0x6c, /* 01101100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0xdc, /* 11011100 */ + 0x86, /* 10000110 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x3e, /* 00111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 172 0xac '¬' */ + 0x00, /* 00000000 */ + 0x60, /* 01100000 */ + 0xe0, /* 11100000 */ + 0x62, /* 01100010 */ + 0x66, /* 01100110 */ + 0x6c, /* 01101100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x66, /* 01100110 */ + 0xce, /* 11001110 */ + 0x9a, /* 10011010 */ + 0x3f, /* 00111111 */ + 0x06, /* 00000110 */ + 0x06, /* 00000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 173 0xad '' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x3c, /* 00111100 */ + 0x3c, /* 00111100 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 174 0xae '®' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x36, /* 00110110 */ + 0x6c, /* 01101100 */ + 0xd8, /* 11011000 */ + 0x6c, /* 01101100 */ + 0x36, /* 00110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 175 0xaf '¯' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xd8, /* 11011000 */ + 0x6c, /* 01101100 */ + 0x36, /* 00110110 */ + 0x6c, /* 01101100 */ + 0xd8, /* 11011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 176 0xb0 '°' */ + 0x11, /* 00010001 */ + 0x44, /* 01000100 */ + 0x11, /* 00010001 */ + 0x44, /* 01000100 */ + 0x11, /* 00010001 */ + 0x44, /* 01000100 */ + 0x11, /* 00010001 */ + 0x44, /* 01000100 */ + 0x11, /* 00010001 */ + 0x44, /* 01000100 */ + 0x11, /* 00010001 */ + 0x44, /* 01000100 */ + 0x11, /* 00010001 */ + 0x44, /* 01000100 */ + 0x11, /* 00010001 */ + 0x44, /* 01000100 */ + + /* 177 0xb1 '±' */ + 0x55, /* 01010101 */ + 0xaa, /* 10101010 */ + 0x55, /* 01010101 */ + 0xaa, /* 10101010 */ + 0x55, /* 01010101 */ + 0xaa, /* 10101010 */ + 0x55, /* 01010101 */ + 0xaa, /* 10101010 */ + 0x55, /* 01010101 */ + 0xaa, /* 10101010 */ + 0x55, /* 01010101 */ + 0xaa, /* 10101010 */ + 0x55, /* 01010101 */ + 0xaa, /* 10101010 */ + 0x55, /* 01010101 */ + 0xaa, /* 10101010 */ + + /* 178 0xb2 '²' */ + 0xdd, /* 11011101 */ + 0x77, /* 01110111 */ + 0xdd, /* 11011101 */ + 0x77, /* 01110111 */ + 0xdd, /* 11011101 */ + 0x77, /* 01110111 */ + 0xdd, /* 11011101 */ + 0x77, /* 01110111 */ + 0xdd, /* 11011101 */ + 0x77, /* 01110111 */ + 0xdd, /* 11011101 */ + 0x77, /* 01110111 */ + 0xdd, /* 11011101 */ + 0x77, /* 01110111 */ + 0xdd, /* 11011101 */ + 0x77, /* 01110111 */ + + /* 179 0xb3 '³' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 180 0xb4 '´' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0xf8, /* 11111000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 181 0xb5 'µ' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0xf8, /* 11111000 */ + 0x18, /* 00011000 */ + 0xf8, /* 11111000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 182 0xb6 '¶' */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0xf6, /* 11110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + + /* 183 0xb7 '·' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + + /* 184 0xb8 '¸' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xf8, /* 11111000 */ + 0x18, /* 00011000 */ + 0xf8, /* 11111000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 185 0xb9 '¹' */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0xf6, /* 11110110 */ + 0x06, /* 00000110 */ + 0xf6, /* 11110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + + /* 186 0xba 'º' */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + + /* 187 0xbb '»' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0x06, /* 00000110 */ + 0xf6, /* 11110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + + /* 188 0xbc '¼' */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0xf6, /* 11110110 */ + 0x06, /* 00000110 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 189 0xbd '½' */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 190 0xbe '¾' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0xf8, /* 11111000 */ + 0x18, /* 00011000 */ + 0xf8, /* 11111000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 191 0xbf '¿' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xf8, /* 11111000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 192 0xc0 'À' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x1f, /* 00011111 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 193 0xc1 'Á' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0xff, /* 11111111 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 194 0xc2 'Â' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xff, /* 11111111 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 195 0xc3 'Ã' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x1f, /* 00011111 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 196 0xc4 'Ä' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xff, /* 11111111 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 197 0xc5 'Å' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0xff, /* 11111111 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 198 0xc6 'Æ' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x1f, /* 00011111 */ + 0x18, /* 00011000 */ + 0x1f, /* 00011111 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 199 0xc7 'Ç' */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x37, /* 00110111 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + + /* 200 0xc8 'È' */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x37, /* 00110111 */ + 0x30, /* 00110000 */ + 0x3f, /* 00111111 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 201 0xc9 'É' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x3f, /* 00111111 */ + 0x30, /* 00110000 */ + 0x37, /* 00110111 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + + /* 202 0xca 'Ê' */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0xf7, /* 11110111 */ + 0x00, /* 00000000 */ + 0xff, /* 11111111 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 203 0xcb 'Ë' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xff, /* 11111111 */ + 0x00, /* 00000000 */ + 0xf7, /* 11110111 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + + /* 204 0xcc 'Ì' */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x37, /* 00110111 */ + 0x30, /* 00110000 */ + 0x37, /* 00110111 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + + /* 205 0xcd 'Í' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xff, /* 11111111 */ + 0x00, /* 00000000 */ + 0xff, /* 11111111 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 206 0xce 'Î' */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0xf7, /* 11110111 */ + 0x00, /* 00000000 */ + 0xf7, /* 11110111 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + + /* 207 0xcf 'Ï' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0xff, /* 11111111 */ + 0x00, /* 00000000 */ + 0xff, /* 11111111 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 208 0xd0 'Ð' */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0xff, /* 11111111 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 209 0xd1 'Ñ' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xff, /* 11111111 */ + 0x00, /* 00000000 */ + 0xff, /* 11111111 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 210 0xd2 'Ò' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xff, /* 11111111 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + + /* 211 0xd3 'Ó' */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x3f, /* 00111111 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 212 0xd4 'Ô' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x1f, /* 00011111 */ + 0x18, /* 00011000 */ + 0x1f, /* 00011111 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 213 0xd5 'Õ' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x1f, /* 00011111 */ + 0x18, /* 00011000 */ + 0x1f, /* 00011111 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 214 0xd6 'Ö' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x3f, /* 00111111 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + + /* 215 0xd7 '×' */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0xff, /* 11111111 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + + /* 216 0xd8 'Ø' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0xff, /* 11111111 */ + 0x18, /* 00011000 */ + 0xff, /* 11111111 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 217 0xd9 'Ù' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0xf8, /* 11111000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 218 0xda 'Ú' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x1f, /* 00011111 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 219 0xdb 'Û' */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + + /* 220 0xdc 'Ü' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + + /* 221 0xdd 'Ý' */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + 0xf0, /* 11110000 */ + + /* 222 0xde 'Þ' */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + 0x0f, /* 00001111 */ + + /* 223 0xdf 'ß' */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0xff, /* 11111111 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 224 0xe0 'à' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x76, /* 01110110 */ + 0xdc, /* 11011100 */ + 0xd8, /* 11011000 */ + 0xd8, /* 11011000 */ + 0xd8, /* 11011000 */ + 0xdc, /* 11011100 */ + 0x76, /* 01110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 225 0xe1 'á' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x78, /* 01111000 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xcc, /* 11001100 */ + 0xd8, /* 11011000 */ + 0xcc, /* 11001100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xcc, /* 11001100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 226 0xe2 'â' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0xc0, /* 11000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 227 0xe3 'ã' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 228 0xe4 'ä' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0xc6, /* 11000110 */ + 0x60, /* 01100000 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 229 0xe5 'å' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0xd8, /* 11011000 */ + 0xd8, /* 11011000 */ + 0xd8, /* 11011000 */ + 0xd8, /* 11011000 */ + 0xd8, /* 11011000 */ + 0x70, /* 01110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 230 0xe6 'æ' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x7c, /* 01111100 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0xc0, /* 11000000 */ + 0x00, /* 00000000 */ + + /* 231 0xe7 'ç' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x76, /* 01110110 */ + 0xdc, /* 11011100 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 232 0xe8 'è' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0x18, /* 00011000 */ + 0x3c, /* 00111100 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x3c, /* 00111100 */ + 0x18, /* 00011000 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 233 0xe9 'é' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xfe, /* 11111110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x6c, /* 01101100 */ + 0x38, /* 00111000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 234 0xea 'ê' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0xee, /* 11101110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 235 0xeb 'ë' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x1e, /* 00011110 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x0c, /* 00001100 */ + 0x3e, /* 00111110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x66, /* 01100110 */ + 0x3c, /* 00111100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 236 0xec 'ì' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0xdb, /* 11011011 */ + 0xdb, /* 11011011 */ + 0xdb, /* 11011011 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 237 0xed 'í' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x03, /* 00000011 */ + 0x06, /* 00000110 */ + 0x7e, /* 01111110 */ + 0xdb, /* 11011011 */ + 0xdb, /* 11011011 */ + 0xf3, /* 11110011 */ + 0x7e, /* 01111110 */ + 0x60, /* 01100000 */ + 0xc0, /* 11000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 238 0xee 'î' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x1c, /* 00011100 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x7c, /* 01111100 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x60, /* 01100000 */ + 0x30, /* 00110000 */ + 0x1c, /* 00011100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 239 0xef 'ï' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7c, /* 01111100 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0xc6, /* 11000110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 240 0xf0 'ð' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0xfe, /* 11111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 241 0xf1 'ñ' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x7e, /* 01111110 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 242 0xf2 'ò' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x0c, /* 00001100 */ + 0x06, /* 00000110 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 243 0xf3 'ó' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x30, /* 00110000 */ + 0x60, /* 01100000 */ + 0x30, /* 00110000 */ + 0x18, /* 00011000 */ + 0x0c, /* 00001100 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 244 0xf4 'ô' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x0e, /* 00001110 */ + 0x1b, /* 00011011 */ + 0x1b, /* 00011011 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + + /* 245 0xf5 'õ' */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0xd8, /* 11011000 */ + 0xd8, /* 11011000 */ + 0xd8, /* 11011000 */ + 0x70, /* 01110000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 246 0xf6 'ö' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 247 0xf7 '÷' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x76, /* 01110110 */ + 0xdc, /* 11011100 */ + 0x00, /* 00000000 */ + 0x76, /* 01110110 */ + 0xdc, /* 11011100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 248 0xf8 'ø' */ + 0x00, /* 00000000 */ + 0x38, /* 00111000 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x38, /* 00111000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 249 0xf9 'ù' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 250 0xfa 'ú' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x18, /* 00011000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 251 0xfb 'û' */ + 0x00, /* 00000000 */ + 0x0f, /* 00001111 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0x0c, /* 00001100 */ + 0xec, /* 11101100 */ + 0x6c, /* 01101100 */ + 0x6c, /* 01101100 */ + 0x3c, /* 00111100 */ + 0x1c, /* 00011100 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 252 0xfc 'ü' */ + 0x00, /* 00000000 */ + 0x6c, /* 01101100 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x36, /* 00110110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 253 0xfd 'ý' */ + 0x00, /* 00000000 */ + 0x3c, /* 00111100 */ + 0x66, /* 01100110 */ + 0x0c, /* 00001100 */ + 0x18, /* 00011000 */ + 0x32, /* 00110010 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 254 0xfe 'þ' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x7e, /* 01111110 */ + 0x7e, /* 01111110 */ + 0x7e, /* 01111110 */ + 0x7e, /* 01111110 */ + 0x7e, /* 01111110 */ + 0x7e, /* 01111110 */ + 0x7e, /* 01111110 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + + /* 255 0xff 'ÿ' */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + 0x00, /* 00000000 */ + +}; diff --git a/ui/vnc-auth-sasl.c b/ui/vnc-auth-sasl.c new file mode 100644 index 000000000..47fdae5b2 --- /dev/null +++ b/ui/vnc-auth-sasl.c @@ -0,0 +1,692 @@ +/* + * QEMU VNC display driver: SASL auth protocol + * + * Copyright (C) 2009 Red Hat, Inc + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "authz/base.h" +#include "vnc.h" +#include "trace.h" + +/* + * Apple has deprecated sasl.h functions in OS X 10.11. Therefore, + * files that use SASL API need to disable -Wdeprecated-declarations. + */ +#ifdef CONFIG_DARWIN +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +/* Max amount of data we send/recv for SASL steps to prevent DOS */ +#define SASL_DATA_MAX_LEN (1024 * 1024) + + +bool vnc_sasl_server_init(Error **errp) +{ + int saslErr = sasl_server_init(NULL, "qemu"); + + if (saslErr != SASL_OK) { + error_setg(errp, "Failed to initialize SASL auth: %s", + sasl_errstring(saslErr, NULL, NULL)); + return false; + } + return true; +} + +void vnc_sasl_client_cleanup(VncState *vs) +{ + if (vs->sasl.conn) { + vs->sasl.runSSF = false; + vs->sasl.wantSSF = false; + vs->sasl.waitWriteSSF = 0; + vs->sasl.encodedLength = vs->sasl.encodedOffset = 0; + vs->sasl.encoded = NULL; + g_free(vs->sasl.username); + g_free(vs->sasl.mechlist); + vs->sasl.username = vs->sasl.mechlist = NULL; + sasl_dispose(&vs->sasl.conn); + vs->sasl.conn = NULL; + } +} + + +size_t vnc_client_write_sasl(VncState *vs) +{ + size_t ret; + + VNC_DEBUG("Write SASL: Pending output %p size %zd offset %zd " + "Encoded: %p size %d offset %d\n", + vs->output.buffer, vs->output.capacity, vs->output.offset, + vs->sasl.encoded, vs->sasl.encodedLength, vs->sasl.encodedOffset); + + if (!vs->sasl.encoded) { + int err; + err = sasl_encode(vs->sasl.conn, + (char *)vs->output.buffer, + vs->output.offset, + (const char **)&vs->sasl.encoded, + &vs->sasl.encodedLength); + if (err != SASL_OK) + return vnc_client_io_error(vs, -1, NULL); + + vs->sasl.encodedRawLength = vs->output.offset; + vs->sasl.encodedOffset = 0; + } + + ret = vnc_client_write_buf(vs, + vs->sasl.encoded + vs->sasl.encodedOffset, + vs->sasl.encodedLength - vs->sasl.encodedOffset); + if (!ret) + return 0; + + vs->sasl.encodedOffset += ret; + if (vs->sasl.encodedOffset == vs->sasl.encodedLength) { + bool throttled = vs->force_update_offset != 0; + size_t offset; + if (vs->sasl.encodedRawLength >= vs->force_update_offset) { + vs->force_update_offset = 0; + } else { + vs->force_update_offset -= vs->sasl.encodedRawLength; + } + if (throttled && vs->force_update_offset == 0) { + trace_vnc_client_unthrottle_forced(vs, vs->ioc); + } + offset = vs->output.offset; + buffer_advance(&vs->output, vs->sasl.encodedRawLength); + if (offset >= vs->throttle_output_offset && + vs->output.offset < vs->throttle_output_offset) { + trace_vnc_client_unthrottle_incremental(vs, vs->ioc, + vs->output.offset); + } + vs->sasl.encoded = NULL; + vs->sasl.encodedOffset = vs->sasl.encodedLength = 0; + } + + /* Can't merge this block with one above, because + * someone might have written more unencrypted + * data in vs->output while we were processing + * SASL encoded output + */ + if (vs->output.offset == 0) { + if (vs->ioc_tag) { + g_source_remove(vs->ioc_tag); + } + vs->ioc_tag = qio_channel_add_watch( + vs->ioc, G_IO_IN | G_IO_HUP | G_IO_ERR, + vnc_client_io, vs, NULL); + } + + return ret; +} + + +size_t vnc_client_read_sasl(VncState *vs) +{ + size_t ret; + uint8_t encoded[4096]; + const char *decoded; + unsigned int decodedLen; + int err; + + ret = vnc_client_read_buf(vs, encoded, sizeof(encoded)); + if (!ret) + return 0; + + err = sasl_decode(vs->sasl.conn, + (char *)encoded, ret, + &decoded, &decodedLen); + + if (err != SASL_OK) + return vnc_client_io_error(vs, -1, NULL); + VNC_DEBUG("Read SASL Encoded %p size %ld Decoded %p size %d\n", + encoded, ret, decoded, decodedLen); + buffer_reserve(&vs->input, decodedLen); + buffer_append(&vs->input, decoded, decodedLen); + return decodedLen; +} + + +static int vnc_auth_sasl_check_access(VncState *vs) +{ + const void *val; + int rv; + Error *err = NULL; + bool allow; + + rv = sasl_getprop(vs->sasl.conn, SASL_USERNAME, &val); + if (rv != SASL_OK) { + trace_vnc_auth_fail(vs, vs->auth, "Cannot fetch SASL username", + sasl_errstring(rv, NULL, NULL)); + return -1; + } + if (val == NULL) { + trace_vnc_auth_fail(vs, vs->auth, "No SASL username set", ""); + return -1; + } + + vs->sasl.username = g_strdup((const char*)val); + trace_vnc_auth_sasl_username(vs, vs->sasl.username); + + if (vs->vd->sasl.authzid == NULL) { + trace_vnc_auth_sasl_acl(vs, 1); + return 0; + } + + allow = qauthz_is_allowed_by_id(vs->vd->sasl.authzid, + vs->sasl.username, &err); + if (err) { + trace_vnc_auth_fail(vs, vs->auth, "Error from authz", + error_get_pretty(err)); + error_free(err); + return -1; + } + + trace_vnc_auth_sasl_acl(vs, allow); + return allow ? 0 : -1; +} + +static int vnc_auth_sasl_check_ssf(VncState *vs) +{ + const void *val; + int err, ssf; + + if (!vs->sasl.wantSSF) + return 1; + + err = sasl_getprop(vs->sasl.conn, SASL_SSF, &val); + if (err != SASL_OK) + return 0; + + ssf = *(const int *)val; + + trace_vnc_auth_sasl_ssf(vs, ssf); + + if (ssf < 56) + return 0; /* 56 is good for Kerberos */ + + /* Only setup for read initially, because we're about to send an RPC + * reply which must be in plain text. When the next incoming RPC + * arrives, we'll switch on writes too + * + * cf qemudClientReadSASL in qemud.c + */ + vs->sasl.runSSF = 1; + + /* We have a SSF that's good enough */ + return 1; +} + +/* + * Step Msg + * + * Input from client: + * + * u32 clientin-length + * u8-array clientin-string + * + * Output to client: + * + * u32 serverout-length + * u8-array serverout-strin + * u8 continue + */ + +static int protocol_client_auth_sasl_step_len(VncState *vs, uint8_t *data, size_t len); + +static int protocol_client_auth_sasl_step(VncState *vs, uint8_t *data, size_t len) +{ + uint32_t datalen = len; + const char *serverout; + unsigned int serveroutlen; + int err; + char *clientdata = NULL; + + /* NB, distinction of NULL vs "" is *critical* in SASL */ + if (datalen) { + clientdata = (char*)data; + clientdata[datalen-1] = '\0'; /* Wire includes '\0', but make sure */ + datalen--; /* Don't count NULL byte when passing to _start() */ + } + + err = sasl_server_step(vs->sasl.conn, + clientdata, + datalen, + &serverout, + &serveroutlen); + trace_vnc_auth_sasl_step(vs, data, len, serverout, serveroutlen, err); + if (err != SASL_OK && + err != SASL_CONTINUE) { + trace_vnc_auth_fail(vs, vs->auth, "Cannot step SASL auth", + sasl_errdetail(vs->sasl.conn)); + sasl_dispose(&vs->sasl.conn); + vs->sasl.conn = NULL; + goto authabort; + } + + if (serveroutlen > SASL_DATA_MAX_LEN) { + trace_vnc_auth_fail(vs, vs->auth, "SASL data too long", ""); + sasl_dispose(&vs->sasl.conn); + vs->sasl.conn = NULL; + goto authabort; + } + + if (serveroutlen) { + vnc_write_u32(vs, serveroutlen + 1); + vnc_write(vs, serverout, serveroutlen + 1); + } else { + vnc_write_u32(vs, 0); + } + + /* Whether auth is complete */ + vnc_write_u8(vs, err == SASL_CONTINUE ? 0 : 1); + + if (err == SASL_CONTINUE) { + /* Wait for step length */ + vnc_read_when(vs, protocol_client_auth_sasl_step_len, 4); + } else { + if (!vnc_auth_sasl_check_ssf(vs)) { + trace_vnc_auth_fail(vs, vs->auth, "SASL SSF too weak", ""); + goto authreject; + } + + /* Check the username access control list */ + if (vnc_auth_sasl_check_access(vs) < 0) { + goto authreject; + } + + trace_vnc_auth_pass(vs, vs->auth); + vnc_write_u32(vs, 0); /* Accept auth */ + /* + * Delay writing in SSF encoded mode until pending output + * buffer is written + */ + if (vs->sasl.runSSF) + vs->sasl.waitWriteSSF = vs->output.offset; + start_client_init(vs); + } + + return 0; + + authreject: + vnc_write_u32(vs, 1); /* Reject auth */ + vnc_write_u32(vs, sizeof("Authentication failed")); + vnc_write(vs, "Authentication failed", sizeof("Authentication failed")); + vnc_flush(vs); + vnc_client_error(vs); + return -1; + + authabort: + vnc_client_error(vs); + return -1; +} + +static int protocol_client_auth_sasl_step_len(VncState *vs, uint8_t *data, size_t len) +{ + uint32_t steplen = read_u32(data, 0); + + if (steplen > SASL_DATA_MAX_LEN) { + trace_vnc_auth_fail(vs, vs->auth, "SASL step len too large", ""); + vnc_client_error(vs); + return -1; + } + + if (steplen == 0) + return protocol_client_auth_sasl_step(vs, NULL, 0); + else + vnc_read_when(vs, protocol_client_auth_sasl_step, steplen); + return 0; +} + +/* + * Start Msg + * + * Input from client: + * + * u32 clientin-length + * u8-array clientin-string + * + * Output to client: + * + * u32 serverout-length + * u8-array serverout-strin + * u8 continue + */ + +#define SASL_DATA_MAX_LEN (1024 * 1024) + +static int protocol_client_auth_sasl_start(VncState *vs, uint8_t *data, size_t len) +{ + uint32_t datalen = len; + const char *serverout; + unsigned int serveroutlen; + int err; + char *clientdata = NULL; + + /* NB, distinction of NULL vs "" is *critical* in SASL */ + if (datalen) { + clientdata = (char*)data; + clientdata[datalen-1] = '\0'; /* Should be on wire, but make sure */ + datalen--; /* Don't count NULL byte when passing to _start() */ + } + + err = sasl_server_start(vs->sasl.conn, + vs->sasl.mechlist, + clientdata, + datalen, + &serverout, + &serveroutlen); + trace_vnc_auth_sasl_start(vs, data, len, serverout, serveroutlen, err); + if (err != SASL_OK && + err != SASL_CONTINUE) { + trace_vnc_auth_fail(vs, vs->auth, "Cannot start SASL auth", + sasl_errdetail(vs->sasl.conn)); + sasl_dispose(&vs->sasl.conn); + vs->sasl.conn = NULL; + goto authabort; + } + if (serveroutlen > SASL_DATA_MAX_LEN) { + trace_vnc_auth_fail(vs, vs->auth, "SASL data too long", ""); + sasl_dispose(&vs->sasl.conn); + vs->sasl.conn = NULL; + goto authabort; + } + + if (serveroutlen) { + vnc_write_u32(vs, serveroutlen + 1); + vnc_write(vs, serverout, serveroutlen + 1); + } else { + vnc_write_u32(vs, 0); + } + + /* Whether auth is complete */ + vnc_write_u8(vs, err == SASL_CONTINUE ? 0 : 1); + + if (err == SASL_CONTINUE) { + /* Wait for step length */ + vnc_read_when(vs, protocol_client_auth_sasl_step_len, 4); + } else { + if (!vnc_auth_sasl_check_ssf(vs)) { + trace_vnc_auth_fail(vs, vs->auth, "SASL SSF too weak", ""); + goto authreject; + } + + /* Check the username access control list */ + if (vnc_auth_sasl_check_access(vs) < 0) { + goto authreject; + } + + trace_vnc_auth_pass(vs, vs->auth); + vnc_write_u32(vs, 0); /* Accept auth */ + start_client_init(vs); + } + + return 0; + + authreject: + vnc_write_u32(vs, 1); /* Reject auth */ + vnc_write_u32(vs, sizeof("Authentication failed")); + vnc_write(vs, "Authentication failed", sizeof("Authentication failed")); + vnc_flush(vs); + vnc_client_error(vs); + return -1; + + authabort: + vnc_client_error(vs); + return -1; +} + +static int protocol_client_auth_sasl_start_len(VncState *vs, uint8_t *data, size_t len) +{ + uint32_t startlen = read_u32(data, 0); + + if (startlen > SASL_DATA_MAX_LEN) { + trace_vnc_auth_fail(vs, vs->auth, "SASL start len too large", ""); + vnc_client_error(vs); + return -1; + } + + if (startlen == 0) + return protocol_client_auth_sasl_start(vs, NULL, 0); + + vnc_read_when(vs, protocol_client_auth_sasl_start, startlen); + return 0; +} + +static int protocol_client_auth_sasl_mechname(VncState *vs, uint8_t *data, size_t len) +{ + char *mechname = g_strndup((const char *) data, len); + trace_vnc_auth_sasl_mech_choose(vs, mechname); + + if (strncmp(vs->sasl.mechlist, mechname, len) == 0) { + if (vs->sasl.mechlist[len] != '\0' && + vs->sasl.mechlist[len] != ',') { + goto fail; + } + } else { + char *offset = strstr(vs->sasl.mechlist, mechname); + if (!offset) { + goto fail; + } + if (offset[-1] != ',' || + (offset[len] != '\0'&& + offset[len] != ',')) { + goto fail; + } + } + + g_free(vs->sasl.mechlist); + vs->sasl.mechlist = mechname; + + vnc_read_when(vs, protocol_client_auth_sasl_start_len, 4); + return 0; + + fail: + trace_vnc_auth_fail(vs, vs->auth, "Unsupported mechname", mechname); + vnc_client_error(vs); + g_free(mechname); + return -1; +} + +static int protocol_client_auth_sasl_mechname_len(VncState *vs, uint8_t *data, size_t len) +{ + uint32_t mechlen = read_u32(data, 0); + + if (mechlen > 100) { + trace_vnc_auth_fail(vs, vs->auth, "SASL mechname too long", ""); + vnc_client_error(vs); + return -1; + } + if (mechlen < 1) { + trace_vnc_auth_fail(vs, vs->auth, "SASL mechname too short", ""); + vnc_client_error(vs); + return -1; + } + vnc_read_when(vs, protocol_client_auth_sasl_mechname,mechlen); + return 0; +} + +static char * +vnc_socket_ip_addr_string(QIOChannelSocket *ioc, + bool local, + Error **errp) +{ + SocketAddress *addr; + char *ret; + + if (local) { + addr = qio_channel_socket_get_local_address(ioc, errp); + } else { + addr = qio_channel_socket_get_remote_address(ioc, errp); + } + if (!addr) { + return NULL; + } + + if (addr->type != SOCKET_ADDRESS_TYPE_INET) { + error_setg(errp, "Not an inet socket type"); + qapi_free_SocketAddress(addr); + return NULL; + } + ret = g_strdup_printf("%s;%s", addr->u.inet.host, addr->u.inet.port); + qapi_free_SocketAddress(addr); + return ret; +} + +void start_auth_sasl(VncState *vs) +{ + const char *mechlist = NULL; + sasl_security_properties_t secprops; + int err; + Error *local_err = NULL; + char *localAddr, *remoteAddr; + int mechlistlen; + + /* Get local & remote client addresses in form IPADDR;PORT */ + localAddr = vnc_socket_ip_addr_string(vs->sioc, true, &local_err); + if (!localAddr) { + trace_vnc_auth_fail(vs, vs->auth, "Cannot format local IP", + error_get_pretty(local_err)); + goto authabort; + } + + remoteAddr = vnc_socket_ip_addr_string(vs->sioc, false, &local_err); + if (!remoteAddr) { + trace_vnc_auth_fail(vs, vs->auth, "Cannot format remote IP", + error_get_pretty(local_err)); + g_free(localAddr); + goto authabort; + } + + err = sasl_server_new("vnc", + NULL, /* FQDN - just delegates to gethostname */ + NULL, /* User realm */ + localAddr, + remoteAddr, + NULL, /* Callbacks, not needed */ + SASL_SUCCESS_DATA, + &vs->sasl.conn); + g_free(localAddr); + g_free(remoteAddr); + localAddr = remoteAddr = NULL; + + if (err != SASL_OK) { + trace_vnc_auth_fail(vs, vs->auth, "SASL context setup failed", + sasl_errstring(err, NULL, NULL)); + vs->sasl.conn = NULL; + goto authabort; + } + + /* Inform SASL that we've got an external SSF layer from TLS/x509 */ + if (vs->auth == VNC_AUTH_VENCRYPT && + vs->subauth == VNC_AUTH_VENCRYPT_X509SASL) { + int keysize; + sasl_ssf_t ssf; + + keysize = qcrypto_tls_session_get_key_size(vs->tls, + &local_err); + if (keysize < 0) { + trace_vnc_auth_fail(vs, vs->auth, "cannot TLS get cipher size", + error_get_pretty(local_err)); + sasl_dispose(&vs->sasl.conn); + vs->sasl.conn = NULL; + goto authabort; + } + ssf = keysize * CHAR_BIT; /* tls key size is bytes, sasl wants bits */ + + err = sasl_setprop(vs->sasl.conn, SASL_SSF_EXTERNAL, &ssf); + if (err != SASL_OK) { + trace_vnc_auth_fail(vs, vs->auth, "cannot set SASL external SSF", + sasl_errstring(err, NULL, NULL)); + sasl_dispose(&vs->sasl.conn); + vs->sasl.conn = NULL; + goto authabort; + } + } else { + vs->sasl.wantSSF = 1; + } + + memset (&secprops, 0, sizeof secprops); + /* Inform SASL that we've got an external SSF layer from TLS. + * + * Disable SSF, if using TLS+x509+SASL only. TLS without x509 + * is not sufficiently strong + */ + if (vs->vd->is_unix || + (vs->auth == VNC_AUTH_VENCRYPT && + vs->subauth == VNC_AUTH_VENCRYPT_X509SASL)) { + /* If we've got TLS or UNIX domain sock, we don't care about SSF */ + secprops.min_ssf = 0; + secprops.max_ssf = 0; + secprops.maxbufsize = 8192; + secprops.security_flags = 0; + } else { + /* Plain TCP, better get an SSF layer */ + secprops.min_ssf = 56; /* Good enough to require kerberos */ + secprops.max_ssf = 100000; /* Arbitrary big number */ + secprops.maxbufsize = 8192; + /* Forbid any anonymous or trivially crackable auth */ + secprops.security_flags = + SASL_SEC_NOANONYMOUS | SASL_SEC_NOPLAINTEXT; + } + + err = sasl_setprop(vs->sasl.conn, SASL_SEC_PROPS, &secprops); + if (err != SASL_OK) { + trace_vnc_auth_fail(vs, vs->auth, "cannot set SASL security props", + sasl_errstring(err, NULL, NULL)); + sasl_dispose(&vs->sasl.conn); + vs->sasl.conn = NULL; + goto authabort; + } + + err = sasl_listmech(vs->sasl.conn, + NULL, /* Don't need to set user */ + "", /* Prefix */ + ",", /* Separator */ + "", /* Suffix */ + &mechlist, + NULL, + NULL); + if (err != SASL_OK) { + trace_vnc_auth_fail(vs, vs->auth, "cannot list SASL mechanisms", + sasl_errdetail(vs->sasl.conn)); + sasl_dispose(&vs->sasl.conn); + vs->sasl.conn = NULL; + goto authabort; + } + trace_vnc_auth_sasl_mech_list(vs, mechlist); + + vs->sasl.mechlist = g_strdup(mechlist); + mechlistlen = strlen(mechlist); + vnc_write_u32(vs, mechlistlen); + vnc_write(vs, mechlist, mechlistlen); + vnc_flush(vs); + + vnc_read_when(vs, protocol_client_auth_sasl_mechname_len, 4); + + return; + + authabort: + error_free(local_err); + vnc_client_error(vs); +} + + diff --git a/ui/vnc-auth-sasl.h b/ui/vnc-auth-sasl.h new file mode 100644 index 000000000..367b8672c --- /dev/null +++ b/ui/vnc-auth-sasl.h @@ -0,0 +1,74 @@ +/* + * QEMU VNC display driver: SASL auth protocol + * + * Copyright (C) 2009 Red Hat, Inc + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef QEMU_VNC_AUTH_SASL_H +#define QEMU_VNC_AUTH_SASL_H + +#include <sasl/sasl.h> + +typedef struct VncStateSASL VncStateSASL; +typedef struct VncDisplaySASL VncDisplaySASL; + +#include "authz/base.h" + +struct VncStateSASL { + sasl_conn_t *conn; + /* If we want to negotiate an SSF layer with client */ + bool wantSSF; + /* If we are now running the SSF layer */ + bool runSSF; + /* + * If this is non-zero, then wait for that many bytes + * to be written plain, before switching to SSF encoding + * This allows the VNC auth result to finish being + * written in plain. + */ + unsigned int waitWriteSSF; + + /* + * Buffering encoded data to allow more clear data + * to be stuffed onto the output buffer + */ + const uint8_t *encoded; + unsigned int encodedLength; + unsigned int encodedRawLength; + unsigned int encodedOffset; + char *username; + char *mechlist; +}; + +struct VncDisplaySASL { + QAuthZ *authz; + char *authzid; +}; + +bool vnc_sasl_server_init(Error **errp); +void vnc_sasl_client_cleanup(VncState *vs); + +size_t vnc_client_read_sasl(VncState *vs); +size_t vnc_client_write_sasl(VncState *vs); + +void start_auth_sasl(VncState *vs); + +#endif /* QEMU_VNC_AUTH_SASL_H */ diff --git a/ui/vnc-auth-vencrypt.c b/ui/vnc-auth-vencrypt.c new file mode 100644 index 000000000..d9c212ff3 --- /dev/null +++ b/ui/vnc-auth-vencrypt.c @@ -0,0 +1,166 @@ +/* + * QEMU VNC display driver: VeNCrypt authentication setup + * + * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws> + * Copyright (C) 2006 Fabrice Bellard + * Copyright (C) 2009 Red Hat, Inc + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "vnc.h" +#include "qapi/error.h" +#include "qemu/main-loop.h" +#include "trace.h" + +static void start_auth_vencrypt_subauth(VncState *vs) +{ + switch (vs->subauth) { + case VNC_AUTH_VENCRYPT_TLSNONE: + case VNC_AUTH_VENCRYPT_X509NONE: + vnc_write_u32(vs, 0); /* Accept auth completion */ + start_client_init(vs); + break; + + case VNC_AUTH_VENCRYPT_TLSVNC: + case VNC_AUTH_VENCRYPT_X509VNC: + start_auth_vnc(vs); + break; + +#ifdef CONFIG_VNC_SASL + case VNC_AUTH_VENCRYPT_TLSSASL: + case VNC_AUTH_VENCRYPT_X509SASL: + start_auth_sasl(vs); + break; +#endif /* CONFIG_VNC_SASL */ + + default: /* Should not be possible, but just in case */ + trace_vnc_auth_fail(vs, vs->auth, "Unhandled VeNCrypt subauth", ""); + vnc_write_u8(vs, 1); + if (vs->minor >= 8) { + static const char err[] = "Unsupported authentication type"; + vnc_write_u32(vs, sizeof(err)); + vnc_write(vs, err, sizeof(err)); + } + vnc_client_error(vs); + } +} + +static void vnc_tls_handshake_done(QIOTask *task, + gpointer user_data) +{ + VncState *vs = user_data; + Error *err = NULL; + + if (qio_task_propagate_error(task, &err)) { + trace_vnc_auth_fail(vs, vs->auth, "TLS handshake failed", + error_get_pretty(err)); + vnc_client_error(vs); + error_free(err); + } else { + if (vs->ioc_tag) { + g_source_remove(vs->ioc_tag); + } + vs->ioc_tag = qio_channel_add_watch( + vs->ioc, G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_OUT, + vnc_client_io, vs, NULL); + start_auth_vencrypt_subauth(vs); + } +} + + +static int protocol_client_vencrypt_auth(VncState *vs, uint8_t *data, size_t len) +{ + int auth = read_u32(data, 0); + + trace_vnc_auth_vencrypt_subauth(vs, auth); + if (auth != vs->subauth) { + trace_vnc_auth_fail(vs, vs->auth, "Unsupported sub-auth version", ""); + vnc_write_u8(vs, 0); /* Reject auth */ + vnc_flush(vs); + vnc_client_error(vs); + } else { + Error *err = NULL; + QIOChannelTLS *tls; + vnc_write_u8(vs, 1); /* Accept auth */ + vnc_flush(vs); + + if (vs->ioc_tag) { + g_source_remove(vs->ioc_tag); + vs->ioc_tag = 0; + } + + tls = qio_channel_tls_new_server( + vs->ioc, + vs->vd->tlscreds, + vs->vd->tlsauthzid, + &err); + if (!tls) { + trace_vnc_auth_fail(vs, vs->auth, "TLS setup failed", + error_get_pretty(err)); + error_free(err); + vnc_client_error(vs); + return 0; + } + + qio_channel_set_name(QIO_CHANNEL(tls), "vnc-server-tls"); + object_unref(OBJECT(vs->ioc)); + vs->ioc = QIO_CHANNEL(tls); + trace_vnc_client_io_wrap(vs, vs->ioc, "tls"); + vs->tls = qio_channel_tls_get_session(tls); + + qio_channel_tls_handshake(tls, + vnc_tls_handshake_done, + vs, + NULL, + NULL); + } + return 0; +} + +static int protocol_client_vencrypt_init(VncState *vs, uint8_t *data, size_t len) +{ + trace_vnc_auth_vencrypt_version(vs, (int)data[0], (int)data[1]); + if (data[0] != 0 || + data[1] != 2) { + trace_vnc_auth_fail(vs, vs->auth, "Unsupported version", ""); + vnc_write_u8(vs, 1); /* Reject version */ + vnc_flush(vs); + vnc_client_error(vs); + } else { + vnc_write_u8(vs, 0); /* Accept version */ + vnc_write_u8(vs, 1); /* Number of sub-auths */ + vnc_write_u32(vs, vs->subauth); /* The supported auth */ + vnc_flush(vs); + vnc_read_when(vs, protocol_client_vencrypt_auth, 4); + } + return 0; +} + + +void start_auth_vencrypt(VncState *vs) +{ + /* Send VeNCrypt version 0.2 */ + vnc_write_u8(vs, 0); + vnc_write_u8(vs, 2); + + vnc_read_when(vs, protocol_client_vencrypt_init, 2); +} + diff --git a/ui/vnc-auth-vencrypt.h b/ui/vnc-auth-vencrypt.h new file mode 100644 index 000000000..1e3540666 --- /dev/null +++ b/ui/vnc-auth-vencrypt.h @@ -0,0 +1,32 @@ +/* + * QEMU VNC display driver + * + * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws> + * Copyright (C) 2006 Fabrice Bellard + * Copyright (C) 2009 Red Hat, Inc + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef QEMU_VNC_AUTH_VENCRYPT_H +#define QEMU_VNC_AUTH_VENCRYPT_H + +void start_auth_vencrypt(VncState *vs); + +#endif /* QEMU_VNC_AUTH_VENCRYPT_H */ diff --git a/ui/vnc-clipboard.c b/ui/vnc-clipboard.c new file mode 100644 index 000000000..67284b556 --- /dev/null +++ b/ui/vnc-clipboard.c @@ -0,0 +1,325 @@ +/* + * QEMU VNC display driver -- clipboard support + * + * Copyright (C) 2021 Gerd Hoffmann <kraxel@redhat.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "qemu-common.h" +#include "vnc.h" +#include "vnc-jobs.h" + +static uint8_t *inflate_buffer(uint8_t *in, uint32_t in_len, uint32_t *size) +{ + z_stream stream = { + .next_in = in, + .avail_in = in_len, + .zalloc = Z_NULL, + .zfree = Z_NULL, + }; + uint32_t out_len = 8; + uint8_t *out = g_malloc(out_len); + int ret; + + stream.next_out = out + stream.total_out; + stream.avail_out = out_len - stream.total_out; + + ret = inflateInit(&stream); + if (ret != Z_OK) { + goto err; + } + + while (stream.avail_in) { + ret = inflate(&stream, Z_FINISH); + switch (ret) { + case Z_OK: + case Z_STREAM_END: + break; + case Z_BUF_ERROR: + out_len <<= 1; + if (out_len > (1 << 20)) { + goto err_end; + } + out = g_realloc(out, out_len); + stream.next_out = out + stream.total_out; + stream.avail_out = out_len - stream.total_out; + break; + default: + goto err_end; + } + } + + *size = stream.total_out; + inflateEnd(&stream); + + return out; + +err_end: + inflateEnd(&stream); +err: + g_free(out); + return NULL; +} + +static uint8_t *deflate_buffer(uint8_t *in, uint32_t in_len, uint32_t *size) +{ + z_stream stream = { + .next_in = in, + .avail_in = in_len, + .zalloc = Z_NULL, + .zfree = Z_NULL, + }; + uint32_t out_len = 8; + uint8_t *out = g_malloc(out_len); + int ret; + + stream.next_out = out + stream.total_out; + stream.avail_out = out_len - stream.total_out; + + ret = deflateInit(&stream, Z_DEFAULT_COMPRESSION); + if (ret != Z_OK) { + goto err; + } + + while (ret != Z_STREAM_END) { + ret = deflate(&stream, Z_FINISH); + switch (ret) { + case Z_OK: + case Z_STREAM_END: + break; + case Z_BUF_ERROR: + out_len <<= 1; + if (out_len > (1 << 20)) { + goto err_end; + } + out = g_realloc(out, out_len); + stream.next_out = out + stream.total_out; + stream.avail_out = out_len - stream.total_out; + break; + default: + goto err_end; + } + } + + *size = stream.total_out; + deflateEnd(&stream); + + return out; + +err_end: + deflateEnd(&stream); +err: + g_free(out); + return NULL; +} + +static void vnc_clipboard_send(VncState *vs, uint32_t count, uint32_t *dwords) +{ + int i; + + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_CUT_TEXT); + vnc_write_u8(vs, 0); + vnc_write_u8(vs, 0); + vnc_write_u8(vs, 0); + vnc_write_s32(vs, -(count * sizeof(uint32_t))); /* -(message length) */ + for (i = 0; i < count; i++) { + vnc_write_u32(vs, dwords[i]); + } + vnc_unlock_output(vs); + vnc_flush(vs); +} + +static void vnc_clipboard_provide(VncState *vs, + QemuClipboardInfo *info, + QemuClipboardType type) +{ + uint32_t flags = 0; + g_autofree uint8_t *buf = NULL; + g_autofree void *zbuf = NULL; + uint32_t zsize; + + switch (type) { + case QEMU_CLIPBOARD_TYPE_TEXT: + flags |= VNC_CLIPBOARD_TEXT; + break; + default: + return; + } + flags |= VNC_CLIPBOARD_PROVIDE; + + buf = g_malloc(info->types[type].size + 4); + buf[0] = (info->types[type].size >> 24) & 0xff; + buf[1] = (info->types[type].size >> 16) & 0xff; + buf[2] = (info->types[type].size >> 8) & 0xff; + buf[3] = (info->types[type].size >> 0) & 0xff; + memcpy(buf + 4, info->types[type].data, info->types[type].size); + zbuf = deflate_buffer(buf, info->types[type].size + 4, &zsize); + if (!zbuf) { + return; + } + + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_CUT_TEXT); + vnc_write_u8(vs, 0); + vnc_write_u8(vs, 0); + vnc_write_u8(vs, 0); + vnc_write_s32(vs, -(sizeof(uint32_t) + zsize)); /* -(message length) */ + vnc_write_u32(vs, flags); + vnc_write(vs, zbuf, zsize); + vnc_unlock_output(vs); + vnc_flush(vs); +} + +static void vnc_clipboard_notify(Notifier *notifier, void *data) +{ + VncState *vs = container_of(notifier, VncState, cbpeer.update); + QemuClipboardInfo *info = data; + QemuClipboardType type; + bool self_update = info->owner == &vs->cbpeer; + uint32_t flags = 0; + + if (info != vs->cbinfo) { + qemu_clipboard_info_unref(vs->cbinfo); + vs->cbinfo = qemu_clipboard_info_ref(info); + vs->cbpending = 0; + if (!self_update) { + if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { + flags |= VNC_CLIPBOARD_TEXT; + } + flags |= VNC_CLIPBOARD_NOTIFY; + vnc_clipboard_send(vs, 1, &flags); + } + return; + } + + if (self_update) { + return; + } + + for (type = 0; type < QEMU_CLIPBOARD_TYPE__COUNT; type++) { + if (vs->cbpending & (1 << type)) { + vs->cbpending &= ~(1 << type); + vnc_clipboard_provide(vs, info, type); + } + } +} + +static void vnc_clipboard_request(QemuClipboardInfo *info, + QemuClipboardType type) +{ + VncState *vs = container_of(info->owner, VncState, cbpeer); + uint32_t flags = 0; + + if (type == QEMU_CLIPBOARD_TYPE_TEXT) { + flags |= VNC_CLIPBOARD_TEXT; + } + if (!flags) { + return; + } + flags |= VNC_CLIPBOARD_REQUEST; + + vnc_clipboard_send(vs, 1, &flags); +} + +void vnc_client_cut_text_ext(VncState *vs, int32_t len, uint32_t flags, uint8_t *data) +{ + if (flags & VNC_CLIPBOARD_CAPS) { + /* need store caps somewhere ? */ + return; + } + + if (flags & VNC_CLIPBOARD_NOTIFY) { + QemuClipboardInfo *info = + qemu_clipboard_info_new(&vs->cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD); + if (flags & VNC_CLIPBOARD_TEXT) { + info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; + } + qemu_clipboard_update(info); + qemu_clipboard_info_unref(info); + return; + } + + if (flags & VNC_CLIPBOARD_PROVIDE && + vs->cbinfo && + vs->cbinfo->owner == &vs->cbpeer) { + uint32_t size = 0; + g_autofree uint8_t *buf = inflate_buffer(data, len - 4, &size); + if ((flags & VNC_CLIPBOARD_TEXT) && + buf && size >= 4) { + uint32_t tsize = read_u32(buf, 0); + uint8_t *tbuf = buf + 4; + if (tsize < size) { + qemu_clipboard_set_data(&vs->cbpeer, vs->cbinfo, + QEMU_CLIPBOARD_TYPE_TEXT, + tsize, tbuf, true); + } + } + } + + if (flags & VNC_CLIPBOARD_REQUEST && + vs->cbinfo && + vs->cbinfo->owner != &vs->cbpeer) { + if ((flags & VNC_CLIPBOARD_TEXT) && + vs->cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { + if (vs->cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].data) { + vnc_clipboard_provide(vs, vs->cbinfo, QEMU_CLIPBOARD_TYPE_TEXT); + } else { + vs->cbpending |= (1 << QEMU_CLIPBOARD_TYPE_TEXT); + qemu_clipboard_request(vs->cbinfo, QEMU_CLIPBOARD_TYPE_TEXT); + } + } + } +} + +void vnc_client_cut_text(VncState *vs, size_t len, uint8_t *text) +{ + QemuClipboardInfo *info = + qemu_clipboard_info_new(&vs->cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD); + + qemu_clipboard_set_data(&vs->cbpeer, info, QEMU_CLIPBOARD_TYPE_TEXT, + len, text, true); + qemu_clipboard_info_unref(info); +} + +void vnc_server_cut_text_caps(VncState *vs) +{ + uint32_t caps[2]; + + if (!vnc_has_feature(vs, VNC_FEATURE_CLIPBOARD_EXT)) { + return; + } + + caps[0] = (VNC_CLIPBOARD_PROVIDE | + VNC_CLIPBOARD_NOTIFY | + VNC_CLIPBOARD_REQUEST | + VNC_CLIPBOARD_CAPS | + VNC_CLIPBOARD_TEXT); + caps[1] = 0; + vnc_clipboard_send(vs, 2, caps); + + if (!vs->cbpeer.update.notify) { + vs->cbpeer.name = "vnc"; + vs->cbpeer.update.notify = vnc_clipboard_notify; + vs->cbpeer.request = vnc_clipboard_request; + qemu_clipboard_peer_register(&vs->cbpeer); + } +} diff --git a/ui/vnc-enc-hextile-template.h b/ui/vnc-enc-hextile-template.h new file mode 100644 index 000000000..0c56262af --- /dev/null +++ b/ui/vnc-enc-hextile-template.h @@ -0,0 +1,211 @@ +#define CONCAT_I(a, b) a ## b +#define CONCAT(a, b) CONCAT_I(a, b) +#define pixel_t CONCAT(uint, CONCAT(BPP, _t)) +#ifdef GENERIC +#define NAME CONCAT(generic_, BPP) +#else +#define NAME BPP +#endif + +static void CONCAT(send_hextile_tile_, NAME)(VncState *vs, + int x, int y, int w, int h, + void *last_bg_, + void *last_fg_, + int *has_bg, int *has_fg) +{ + VncDisplay *vd = vs->vd; + uint8_t *row = vnc_server_fb_ptr(vd, x, y); + pixel_t *irow = (pixel_t *)row; + int j, i; + pixel_t *last_bg = (pixel_t *)last_bg_; + pixel_t *last_fg = (pixel_t *)last_fg_; + pixel_t bg = 0; + pixel_t fg = 0; + int n_colors = 0; + int bg_count = 0; + int fg_count = 0; + int flags = 0; + uint8_t data[(vs->client_pf.bytes_per_pixel + 2) * 16 * 16]; + int n_data = 0; + int n_subtiles = 0; + + for (j = 0; j < h; j++) { + for (i = 0; i < w; i++) { + switch (n_colors) { + case 0: + bg = irow[i]; + n_colors = 1; + break; + case 1: + if (irow[i] != bg) { + fg = irow[i]; + n_colors = 2; + } + break; + case 2: + if (irow[i] != bg && irow[i] != fg) { + n_colors = 3; + } else { + if (irow[i] == bg) + bg_count++; + else if (irow[i] == fg) + fg_count++; + } + break; + default: + break; + } + } + if (n_colors > 2) + break; + irow += vnc_server_fb_stride(vd) / sizeof(pixel_t); + } + + if (n_colors > 1 && fg_count > bg_count) { + pixel_t tmp = fg; + fg = bg; + bg = tmp; + } + + if (!*has_bg || *last_bg != bg) { + flags |= 0x02; + *has_bg = 1; + *last_bg = bg; + } + + if (n_colors < 3 && (!*has_fg || *last_fg != fg)) { + flags |= 0x04; + *has_fg = 1; + *last_fg = fg; + } + + switch (n_colors) { + case 1: + n_data = 0; + break; + case 2: + flags |= 0x08; + + irow = (pixel_t *)row; + + for (j = 0; j < h; j++) { + int min_x = -1; + for (i = 0; i < w; i++) { + if (irow[i] == fg) { + if (min_x == -1) + min_x = i; + } else if (min_x != -1) { + hextile_enc_cord(data + n_data, min_x, j, i - min_x, 1); + n_data += 2; + n_subtiles++; + min_x = -1; + } + } + if (min_x != -1) { + hextile_enc_cord(data + n_data, min_x, j, i - min_x, 1); + n_data += 2; + n_subtiles++; + } + irow += vnc_server_fb_stride(vd) / sizeof(pixel_t); + } + break; + case 3: + flags |= 0x18; + + irow = (pixel_t *)row; + + if (!*has_bg || *last_bg != bg) + flags |= 0x02; + + for (j = 0; j < h; j++) { + int has_color = 0; + int min_x = -1; + pixel_t color = 0; /* shut up gcc */ + + for (i = 0; i < w; i++) { + if (!has_color) { + if (irow[i] == bg) + continue; + color = irow[i]; + min_x = i; + has_color = 1; + } else if (irow[i] != color) { + has_color = 0; +#ifdef GENERIC + vnc_convert_pixel(vs, data + n_data, color); + n_data += vs->client_pf.bytes_per_pixel; +#else + memcpy(data + n_data, &color, sizeof(color)); + n_data += sizeof(pixel_t); +#endif + hextile_enc_cord(data + n_data, min_x, j, i - min_x, 1); + n_data += 2; + n_subtiles++; + + min_x = -1; + if (irow[i] != bg) { + color = irow[i]; + min_x = i; + has_color = 1; + } + } + } + if (has_color) { +#ifdef GENERIC + vnc_convert_pixel(vs, data + n_data, color); + n_data += vs->client_pf.bytes_per_pixel; +#else + memcpy(data + n_data, &color, sizeof(color)); + n_data += sizeof(pixel_t); +#endif + hextile_enc_cord(data + n_data, min_x, j, i - min_x, 1); + n_data += 2; + n_subtiles++; + } + irow += vnc_server_fb_stride(vd) / sizeof(pixel_t); + } + + /* A SubrectsColoured subtile invalidates the foreground color */ + *has_fg = 0; + if (n_data > (w * h * sizeof(pixel_t))) { + n_colors = 4; + flags = 0x01; + *has_bg = 0; + + /* we really don't have to invalidate either the bg or fg + but we've lost the old values. oh well. */ + } + break; + default: + break; + } + + if (n_colors > 3) { + flags = 0x01; + *has_fg = 0; + *has_bg = 0; + n_colors = 4; + } + + vnc_write_u8(vs, flags); + if (n_colors < 4) { + if (flags & 0x02) + vs->write_pixels(vs, last_bg, sizeof(pixel_t)); + if (flags & 0x04) + vs->write_pixels(vs, last_fg, sizeof(pixel_t)); + if (n_subtiles) { + vnc_write_u8(vs, n_subtiles); + vnc_write(vs, data, n_data); + } + } else { + for (j = 0; j < h; j++) { + vs->write_pixels(vs, row, w * 4); + row += vnc_server_fb_stride(vd); + } + } +} + +#undef NAME +#undef pixel_t +#undef CONCAT_I +#undef CONCAT diff --git a/ui/vnc-enc-hextile.c b/ui/vnc-enc-hextile.c new file mode 100644 index 000000000..4215bd7da --- /dev/null +++ b/ui/vnc-enc-hextile.c @@ -0,0 +1,84 @@ +/* + * QEMU VNC display driver: hextile encoding + * + * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws> + * Copyright (C) 2006 Fabrice Bellard + * Copyright (C) 2009 Red Hat, Inc + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "vnc.h" + +static void hextile_enc_cord(uint8_t *ptr, int x, int y, int w, int h) +{ + ptr[0] = ((x & 0x0F) << 4) | (y & 0x0F); + ptr[1] = (((w - 1) & 0x0F) << 4) | ((h - 1) & 0x0F); +} + +#define BPP 32 +#include "vnc-enc-hextile-template.h" +#undef BPP + +#define GENERIC +#define BPP 32 +#include "vnc-enc-hextile-template.h" +#undef BPP +#undef GENERIC + +int vnc_hextile_send_framebuffer_update(VncState *vs, int x, + int y, int w, int h) +{ + int i, j; + int has_fg, has_bg; + uint8_t *last_fg, *last_bg; + + last_fg = (uint8_t *) g_malloc(VNC_SERVER_FB_BYTES); + last_bg = (uint8_t *) g_malloc(VNC_SERVER_FB_BYTES); + has_fg = has_bg = 0; + for (j = y; j < (y + h); j += 16) { + for (i = x; i < (x + w); i += 16) { + vs->hextile.send_tile(vs, i, j, + MIN(16, x + w - i), MIN(16, y + h - j), + last_bg, last_fg, &has_bg, &has_fg); + } + } + g_free(last_fg); + g_free(last_bg); + + return 1; +} + +void vnc_hextile_set_pixel_conversion(VncState *vs, int generic) +{ + if (!generic) { + switch (VNC_SERVER_FB_BITS) { + case 32: + vs->hextile.send_tile = send_hextile_tile_32; + break; + } + } else { + switch (VNC_SERVER_FB_BITS) { + case 32: + vs->hextile.send_tile = send_hextile_tile_generic_32; + break; + } + } +} diff --git a/ui/vnc-enc-tight.c b/ui/vnc-enc-tight.c new file mode 100644 index 000000000..cebd35841 --- /dev/null +++ b/ui/vnc-enc-tight.c @@ -0,0 +1,1712 @@ +/* + * QEMU VNC display driver: tight encoding + * + * From libvncserver/libvncserver/tight.c + * Copyright (C) 2000, 2001 Const Kaplinsky. All Rights Reserved. + * Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. + * + * Copyright (C) 2010 Corentin Chary <corentin.chary@gmail.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" + +/* This needs to be before jpeglib.h line because of conflict with + INT32 definitions between jmorecfg.h (included by jpeglib.h) and + Win32 basetsd.h (included by windows.h). */ + +#ifdef CONFIG_VNC_PNG +/* The following define is needed by pngconf.h. Otherwise it won't compile, + because setjmp.h was already included by qemu-common.h. */ +#define PNG_SKIP_SETJMP_CHECK +#include <png.h> +#endif +#ifdef CONFIG_VNC_JPEG +#include <jpeglib.h> +#endif + +#include "qemu/bswap.h" +#include "vnc.h" +#include "vnc-enc-tight.h" +#include "vnc-palette.h" + +/* Compression level stuff. The following array contains various + encoder parameters for each of 10 compression levels (0..9). + Last three parameters correspond to JPEG quality levels (0..9). */ + +static const struct { + int max_rect_size, max_rect_width; + int mono_min_rect_size, gradient_min_rect_size; + int idx_zlib_level, mono_zlib_level, raw_zlib_level, gradient_zlib_level; + int gradient_threshold, gradient_threshold24; + int idx_max_colors_divisor; + int jpeg_quality, jpeg_threshold, jpeg_threshold24; +} tight_conf[] = { + { 512, 32, 6, 65536, 0, 0, 0, 0, 0, 0, 4, 5, 10000, 23000 }, + { 2048, 128, 6, 65536, 1, 1, 1, 0, 0, 0, 8, 10, 8000, 18000 }, + { 6144, 256, 8, 65536, 3, 3, 2, 0, 0, 0, 24, 15, 6500, 15000 }, + { 10240, 1024, 12, 65536, 5, 5, 3, 0, 0, 0, 32, 25, 5000, 12000 }, + { 16384, 2048, 12, 65536, 6, 6, 4, 0, 0, 0, 32, 37, 4000, 10000 }, + { 32768, 2048, 12, 4096, 7, 7, 5, 4, 150, 380, 32, 50, 3000, 8000 }, + { 65536, 2048, 16, 4096, 7, 7, 6, 4, 170, 420, 48, 60, 2000, 5000 }, + { 65536, 2048, 16, 4096, 8, 8, 7, 5, 180, 450, 64, 70, 1000, 2500 }, + { 65536, 2048, 32, 8192, 9, 9, 8, 6, 190, 475, 64, 75, 500, 1200 }, + { 65536, 2048, 32, 8192, 9, 9, 9, 6, 200, 500, 96, 80, 200, 500 } +}; + + +static int tight_send_framebuffer_update(VncState *vs, int x, int y, + int w, int h); + +#ifdef CONFIG_VNC_JPEG +static const struct { + double jpeg_freq_min; /* Don't send JPEG if the freq is bellow */ + double jpeg_freq_threshold; /* Always send JPEG if the freq is above */ + int jpeg_idx; /* Allow indexed JPEG */ + int jpeg_full; /* Allow full color JPEG */ +} tight_jpeg_conf[] = { + { 0, 8, 1, 1 }, + { 0, 8, 1, 1 }, + { 0, 8, 1, 1 }, + { 0, 8, 1, 1 }, + { 0, 10, 1, 1 }, + { 0.1, 10, 1, 1 }, + { 0.2, 10, 1, 1 }, + { 0.3, 12, 0, 0 }, + { 0.4, 14, 0, 0 }, + { 0.5, 16, 0, 0 }, +}; +#endif + +#ifdef CONFIG_VNC_PNG +static const struct { + int png_zlib_level, png_filters; +} tight_png_conf[] = { + { 0, PNG_NO_FILTERS }, + { 1, PNG_NO_FILTERS }, + { 2, PNG_NO_FILTERS }, + { 3, PNG_NO_FILTERS }, + { 4, PNG_NO_FILTERS }, + { 5, PNG_ALL_FILTERS }, + { 6, PNG_ALL_FILTERS }, + { 7, PNG_ALL_FILTERS }, + { 8, PNG_ALL_FILTERS }, + { 9, PNG_ALL_FILTERS }, +}; + +static int send_png_rect(VncState *vs, int x, int y, int w, int h, + VncPalette *palette); + +static bool tight_can_send_png_rect(VncState *vs, int w, int h) +{ + if (vs->tight->type != VNC_ENCODING_TIGHT_PNG) { + return false; + } + + if (surface_bytes_per_pixel(vs->vd->ds) == 1 || + vs->client_pf.bytes_per_pixel == 1) { + return false; + } + + return true; +} +#endif + +/* + * Code to guess if given rectangle is suitable for smooth image + * compression (by applying "gradient" filter or JPEG coder). + */ + +static unsigned int +tight_detect_smooth_image24(VncState *vs, int w, int h) +{ + int off; + int x, y, d, dx; + unsigned int c; + unsigned int stats[256]; + int pixels = 0; + int pix, left[3]; + unsigned int errors; + unsigned char *buf = vs->tight->tight.buffer; + + /* + * If client is big-endian, color samples begin from the second + * byte (offset 1) of a 32-bit pixel value. + */ + off = vs->client_be; + + memset(stats, 0, sizeof (stats)); + + for (y = 0, x = 0; y < h && x < w;) { + for (d = 0; d < h - y && d < w - x - VNC_TIGHT_DETECT_SUBROW_WIDTH; + d++) { + for (c = 0; c < 3; c++) { + left[c] = buf[((y+d)*w+x+d)*4+off+c] & 0xFF; + } + for (dx = 1; dx <= VNC_TIGHT_DETECT_SUBROW_WIDTH; dx++) { + for (c = 0; c < 3; c++) { + pix = buf[((y+d)*w+x+d+dx)*4+off+c] & 0xFF; + stats[abs(pix - left[c])]++; + left[c] = pix; + } + pixels++; + } + } + if (w > h) { + x += h; + y = 0; + } else { + x = 0; + y += w; + } + } + + if (pixels == 0) { + return 0; + } + + /* 95% smooth or more ... */ + if (stats[0] * 33 / pixels >= 95) { + return 0; + } + + errors = 0; + for (c = 1; c < 8; c++) { + errors += stats[c] * (c * c); + if (stats[c] == 0 || stats[c] > stats[c-1] * 2) { + return 0; + } + } + for (; c < 256; c++) { + errors += stats[c] * (c * c); + } + errors /= (pixels * 3 - stats[0]); + + return errors; +} + +#define DEFINE_DETECT_FUNCTION(bpp) \ + \ + static unsigned int \ + tight_detect_smooth_image##bpp(VncState *vs, int w, int h) { \ + bool endian; \ + uint##bpp##_t pix; \ + int max[3], shift[3]; \ + int x, y, d, dx; \ + unsigned int c; \ + unsigned int stats[256]; \ + int pixels = 0; \ + int sample, sum, left[3]; \ + unsigned int errors; \ + unsigned char *buf = vs->tight->tight.buffer; \ + \ + endian = 0; /* FIXME */ \ + \ + \ + max[0] = vs->client_pf.rmax; \ + max[1] = vs->client_pf.gmax; \ + max[2] = vs->client_pf.bmax; \ + shift[0] = vs->client_pf.rshift; \ + shift[1] = vs->client_pf.gshift; \ + shift[2] = vs->client_pf.bshift; \ + \ + memset(stats, 0, sizeof(stats)); \ + \ + y = 0, x = 0; \ + while (y < h && x < w) { \ + for (d = 0; d < h - y && \ + d < w - x - VNC_TIGHT_DETECT_SUBROW_WIDTH; d++) { \ + pix = ((uint##bpp##_t *)buf)[(y+d)*w+x+d]; \ + if (endian) { \ + pix = bswap##bpp(pix); \ + } \ + for (c = 0; c < 3; c++) { \ + left[c] = (int)(pix >> shift[c] & max[c]); \ + } \ + for (dx = 1; dx <= VNC_TIGHT_DETECT_SUBROW_WIDTH; \ + dx++) { \ + pix = ((uint##bpp##_t *)buf)[(y+d)*w+x+d+dx]; \ + if (endian) { \ + pix = bswap##bpp(pix); \ + } \ + sum = 0; \ + for (c = 0; c < 3; c++) { \ + sample = (int)(pix >> shift[c] & max[c]); \ + sum += abs(sample - left[c]); \ + left[c] = sample; \ + } \ + if (sum > 255) { \ + sum = 255; \ + } \ + stats[sum]++; \ + pixels++; \ + } \ + } \ + if (w > h) { \ + x += h; \ + y = 0; \ + } else { \ + x = 0; \ + y += w; \ + } \ + } \ + if (pixels == 0) { \ + return 0; \ + } \ + if ((stats[0] + stats[1]) * 100 / pixels >= 90) { \ + return 0; \ + } \ + \ + errors = 0; \ + for (c = 1; c < 8; c++) { \ + errors += stats[c] * (c * c); \ + if (stats[c] == 0 || stats[c] > stats[c-1] * 2) { \ + return 0; \ + } \ + } \ + for (; c < 256; c++) { \ + errors += stats[c] * (c * c); \ + } \ + errors /= (pixels - stats[0]); \ + \ + return errors; \ + } + +DEFINE_DETECT_FUNCTION(16) +DEFINE_DETECT_FUNCTION(32) + +static int +tight_detect_smooth_image(VncState *vs, int w, int h) +{ + unsigned int errors; + int compression = vs->tight->compression; + int quality = vs->tight->quality; + + if (!vs->vd->lossy) { + return 0; + } + + if (surface_bytes_per_pixel(vs->vd->ds) == 1 || + vs->client_pf.bytes_per_pixel == 1 || + w < VNC_TIGHT_DETECT_MIN_WIDTH || h < VNC_TIGHT_DETECT_MIN_HEIGHT) { + return 0; + } + + if (vs->tight->quality != (uint8_t)-1) { + if (w * h < VNC_TIGHT_JPEG_MIN_RECT_SIZE) { + return 0; + } + } else { + if (w * h < tight_conf[compression].gradient_min_rect_size) { + return 0; + } + } + + if (vs->client_pf.bytes_per_pixel == 4) { + if (vs->tight->pixel24) { + errors = tight_detect_smooth_image24(vs, w, h); + if (vs->tight->quality != (uint8_t)-1) { + return (errors < tight_conf[quality].jpeg_threshold24); + } + return (errors < tight_conf[compression].gradient_threshold24); + } else { + errors = tight_detect_smooth_image32(vs, w, h); + } + } else { + errors = tight_detect_smooth_image16(vs, w, h); + } + if (quality != (uint8_t)-1) { + return (errors < tight_conf[quality].jpeg_threshold); + } + return (errors < tight_conf[compression].gradient_threshold); +} + +/* + * Code to determine how many different colors used in rectangle. + */ +#define DEFINE_FILL_PALETTE_FUNCTION(bpp) \ + \ + static int \ + tight_fill_palette##bpp(VncState *vs, int x, int y, \ + int max, size_t count, \ + uint32_t *bg, uint32_t *fg, \ + VncPalette *palette) { \ + uint##bpp##_t *data; \ + uint##bpp##_t c0, c1, ci; \ + int i, n0, n1; \ + \ + data = (uint##bpp##_t *)vs->tight->tight.buffer; \ + \ + c0 = data[0]; \ + i = 1; \ + while (i < count && data[i] == c0) \ + i++; \ + if (i >= count) { \ + *bg = *fg = c0; \ + return 1; \ + } \ + \ + if (max < 2) { \ + return 0; \ + } \ + \ + n0 = i; \ + c1 = data[i]; \ + n1 = 0; \ + for (i++; i < count; i++) { \ + ci = data[i]; \ + if (ci == c0) { \ + n0++; \ + } else if (ci == c1) { \ + n1++; \ + } else \ + break; \ + } \ + if (i >= count) { \ + if (n0 > n1) { \ + *bg = (uint32_t)c0; \ + *fg = (uint32_t)c1; \ + } else { \ + *bg = (uint32_t)c1; \ + *fg = (uint32_t)c0; \ + } \ + return 2; \ + } \ + \ + if (max == 2) { \ + return 0; \ + } \ + \ + palette_init(palette, max, bpp); \ + palette_put(palette, c0); \ + palette_put(palette, c1); \ + palette_put(palette, ci); \ + \ + for (i++; i < count; i++) { \ + if (data[i] == ci) { \ + continue; \ + } else { \ + ci = data[i]; \ + if (!palette_put(palette, (uint32_t)ci)) { \ + return 0; \ + } \ + } \ + } \ + \ + return palette_size(palette); \ + } + +DEFINE_FILL_PALETTE_FUNCTION(8) +DEFINE_FILL_PALETTE_FUNCTION(16) +DEFINE_FILL_PALETTE_FUNCTION(32) + +static int tight_fill_palette(VncState *vs, int x, int y, + size_t count, uint32_t *bg, uint32_t *fg, + VncPalette *palette) +{ + int max; + + max = count / tight_conf[vs->tight->compression].idx_max_colors_divisor; + if (max < 2 && + count >= tight_conf[vs->tight->compression].mono_min_rect_size) { + max = 2; + } + if (max >= 256) { + max = 256; + } + + switch (vs->client_pf.bytes_per_pixel) { + case 4: + return tight_fill_palette32(vs, x, y, max, count, bg, fg, palette); + case 2: + return tight_fill_palette16(vs, x, y, max, count, bg, fg, palette); + default: + max = 2; + return tight_fill_palette8(vs, x, y, max, count, bg, fg, palette); + } + return 0; +} + +/* + * Converting truecolor samples into palette indices. + */ +#define DEFINE_IDX_ENCODE_FUNCTION(bpp) \ + \ + static void \ + tight_encode_indexed_rect##bpp(uint8_t *buf, int count, \ + VncPalette *palette) { \ + uint##bpp##_t *src; \ + uint##bpp##_t rgb; \ + int i, rep; \ + uint8_t idx; \ + \ + src = (uint##bpp##_t *) buf; \ + \ + for (i = 0; i < count; ) { \ + \ + rgb = *src++; \ + i++; \ + rep = 0; \ + while (i < count && *src == rgb) { \ + rep++, src++, i++; \ + } \ + idx = palette_idx(palette, rgb); \ + /* \ + * Should never happen, but don't break everything \ + * if it does, use the first color instead \ + */ \ + if (idx == (uint8_t)-1) { \ + idx = 0; \ + } \ + while (rep >= 0) { \ + *buf++ = idx; \ + rep--; \ + } \ + } \ + } + +DEFINE_IDX_ENCODE_FUNCTION(16) +DEFINE_IDX_ENCODE_FUNCTION(32) + +#define DEFINE_MONO_ENCODE_FUNCTION(bpp) \ + \ + static void \ + tight_encode_mono_rect##bpp(uint8_t *buf, int w, int h, \ + uint##bpp##_t bg, uint##bpp##_t fg) { \ + uint##bpp##_t *ptr; \ + unsigned int value, mask; \ + int aligned_width; \ + int x, y, bg_bits; \ + \ + ptr = (uint##bpp##_t *) buf; \ + aligned_width = w - w % 8; \ + \ + for (y = 0; y < h; y++) { \ + for (x = 0; x < aligned_width; x += 8) { \ + for (bg_bits = 0; bg_bits < 8; bg_bits++) { \ + if (*ptr++ != bg) { \ + break; \ + } \ + } \ + if (bg_bits == 8) { \ + *buf++ = 0; \ + continue; \ + } \ + mask = 0x80 >> bg_bits; \ + value = mask; \ + for (bg_bits++; bg_bits < 8; bg_bits++) { \ + mask >>= 1; \ + if (*ptr++ != bg) { \ + value |= mask; \ + } \ + } \ + *buf++ = (uint8_t)value; \ + } \ + \ + mask = 0x80; \ + value = 0; \ + if (x >= w) { \ + continue; \ + } \ + \ + for (; x < w; x++) { \ + if (*ptr++ != bg) { \ + value |= mask; \ + } \ + mask >>= 1; \ + } \ + *buf++ = (uint8_t)value; \ + } \ + } + +DEFINE_MONO_ENCODE_FUNCTION(8) +DEFINE_MONO_ENCODE_FUNCTION(16) +DEFINE_MONO_ENCODE_FUNCTION(32) + +/* + * ``Gradient'' filter for 24-bit color samples. + * Should be called only when redMax, greenMax and blueMax are 255. + * Color components assumed to be byte-aligned. + */ + +static void +tight_filter_gradient24(VncState *vs, uint8_t *buf, int w, int h) +{ + uint32_t *buf32; + uint32_t pix32; + int shift[3]; + int *prev; + int here[3], upper[3], left[3], upperleft[3]; + int prediction; + int x, y, c; + + buf32 = (uint32_t *)buf; + memset(vs->tight->gradient.buffer, 0, w * 3 * sizeof(int)); + + if (1 /* FIXME */) { + shift[0] = vs->client_pf.rshift; + shift[1] = vs->client_pf.gshift; + shift[2] = vs->client_pf.bshift; + } else { + shift[0] = 24 - vs->client_pf.rshift; + shift[1] = 24 - vs->client_pf.gshift; + shift[2] = 24 - vs->client_pf.bshift; + } + + for (y = 0; y < h; y++) { + for (c = 0; c < 3; c++) { + upper[c] = 0; + here[c] = 0; + } + prev = (int *)vs->tight->gradient.buffer; + for (x = 0; x < w; x++) { + pix32 = *buf32++; + for (c = 0; c < 3; c++) { + upperleft[c] = upper[c]; + left[c] = here[c]; + upper[c] = *prev; + here[c] = (int)(pix32 >> shift[c] & 0xFF); + *prev++ = here[c]; + + prediction = left[c] + upper[c] - upperleft[c]; + if (prediction < 0) { + prediction = 0; + } else if (prediction > 0xFF) { + prediction = 0xFF; + } + *buf++ = (char)(here[c] - prediction); + } + } + } +} + + +/* + * ``Gradient'' filter for other color depths. + */ + +#define DEFINE_GRADIENT_FILTER_FUNCTION(bpp) \ + \ + static void \ + tight_filter_gradient##bpp(VncState *vs, uint##bpp##_t *buf, \ + int w, int h) { \ + uint##bpp##_t pix, diff; \ + bool endian; \ + int *prev; \ + int max[3], shift[3]; \ + int here[3], upper[3], left[3], upperleft[3]; \ + int prediction; \ + int x, y, c; \ + \ + memset(vs->tight->gradient.buffer, 0, w * 3 * sizeof(int)); \ + \ + endian = 0; /* FIXME */ \ + \ + max[0] = vs->client_pf.rmax; \ + max[1] = vs->client_pf.gmax; \ + max[2] = vs->client_pf.bmax; \ + shift[0] = vs->client_pf.rshift; \ + shift[1] = vs->client_pf.gshift; \ + shift[2] = vs->client_pf.bshift; \ + \ + for (y = 0; y < h; y++) { \ + for (c = 0; c < 3; c++) { \ + upper[c] = 0; \ + here[c] = 0; \ + } \ + prev = (int *)vs->tight->gradient.buffer; \ + for (x = 0; x < w; x++) { \ + pix = *buf; \ + if (endian) { \ + pix = bswap##bpp(pix); \ + } \ + diff = 0; \ + for (c = 0; c < 3; c++) { \ + upperleft[c] = upper[c]; \ + left[c] = here[c]; \ + upper[c] = *prev; \ + here[c] = (int)(pix >> shift[c] & max[c]); \ + *prev++ = here[c]; \ + \ + prediction = left[c] + upper[c] - upperleft[c]; \ + if (prediction < 0) { \ + prediction = 0; \ + } else if (prediction > max[c]) { \ + prediction = max[c]; \ + } \ + diff |= ((here[c] - prediction) & max[c]) \ + << shift[c]; \ + } \ + if (endian) { \ + diff = bswap##bpp(diff); \ + } \ + *buf++ = diff; \ + } \ + } \ + } + +DEFINE_GRADIENT_FILTER_FUNCTION(16) +DEFINE_GRADIENT_FILTER_FUNCTION(32) + +/* + * Check if a rectangle is all of the same color. If needSameColor is + * set to non-zero, then also check that its color equals to the + * *colorPtr value. The result is 1 if the test is successful, and in + * that case new color will be stored in *colorPtr. + */ + +static bool +check_solid_tile32(VncState *vs, int x, int y, int w, int h, + uint32_t *color, bool samecolor) +{ + VncDisplay *vd = vs->vd; + uint32_t *fbptr; + uint32_t c; + int dx, dy; + + fbptr = vnc_server_fb_ptr(vd, x, y); + + c = *fbptr; + if (samecolor && (uint32_t)c != *color) { + return false; + } + + for (dy = 0; dy < h; dy++) { + for (dx = 0; dx < w; dx++) { + if (c != fbptr[dx]) { + return false; + } + } + fbptr = (uint32_t *) + ((uint8_t *)fbptr + vnc_server_fb_stride(vd)); + } + + *color = (uint32_t)c; + return true; +} + +static bool check_solid_tile(VncState *vs, int x, int y, int w, int h, + uint32_t* color, bool samecolor) +{ + QEMU_BUILD_BUG_ON(VNC_SERVER_FB_BYTES != 4); + return check_solid_tile32(vs, x, y, w, h, color, samecolor); +} + +static void find_best_solid_area(VncState *vs, int x, int y, int w, int h, + uint32_t color, int *w_ptr, int *h_ptr) +{ + int dx, dy, dw, dh; + int w_prev; + int w_best = 0, h_best = 0; + + w_prev = w; + + for (dy = y; dy < y + h; dy += VNC_TIGHT_MAX_SPLIT_TILE_SIZE) { + + dh = MIN(VNC_TIGHT_MAX_SPLIT_TILE_SIZE, y + h - dy); + dw = MIN(VNC_TIGHT_MAX_SPLIT_TILE_SIZE, w_prev); + + if (!check_solid_tile(vs, x, dy, dw, dh, &color, true)) { + break; + } + + for (dx = x + dw; dx < x + w_prev;) { + dw = MIN(VNC_TIGHT_MAX_SPLIT_TILE_SIZE, x + w_prev - dx); + + if (!check_solid_tile(vs, dx, dy, dw, dh, &color, true)) { + break; + } + dx += dw; + } + + w_prev = dx - x; + if (w_prev * (dy + dh - y) > w_best * h_best) { + w_best = w_prev; + h_best = dy + dh - y; + } + } + + *w_ptr = w_best; + *h_ptr = h_best; +} + +static void extend_solid_area(VncState *vs, int x, int y, int w, int h, + uint32_t color, int *x_ptr, int *y_ptr, + int *w_ptr, int *h_ptr) +{ + int cx, cy; + + /* Try to extend the area upwards. */ + for ( cy = *y_ptr - 1; + cy >= y && check_solid_tile(vs, *x_ptr, cy, *w_ptr, 1, &color, true); + cy-- ); + *h_ptr += *y_ptr - (cy + 1); + *y_ptr = cy + 1; + + /* ... downwards. */ + for ( cy = *y_ptr + *h_ptr; + cy < y + h && + check_solid_tile(vs, *x_ptr, cy, *w_ptr, 1, &color, true); + cy++ ); + *h_ptr += cy - (*y_ptr + *h_ptr); + + /* ... to the left. */ + for ( cx = *x_ptr - 1; + cx >= x && check_solid_tile(vs, cx, *y_ptr, 1, *h_ptr, &color, true); + cx-- ); + *w_ptr += *x_ptr - (cx + 1); + *x_ptr = cx + 1; + + /* ... to the right. */ + for ( cx = *x_ptr + *w_ptr; + cx < x + w && + check_solid_tile(vs, cx, *y_ptr, 1, *h_ptr, &color, true); + cx++ ); + *w_ptr += cx - (*x_ptr + *w_ptr); +} + +static int tight_init_stream(VncState *vs, int stream_id, + int level, int strategy) +{ + z_streamp zstream = &vs->tight->stream[stream_id]; + + if (zstream->opaque == NULL) { + int err; + + VNC_DEBUG("VNC: TIGHT: initializing zlib stream %d\n", stream_id); + VNC_DEBUG("VNC: TIGHT: opaque = %p | vs = %p\n", zstream->opaque, vs); + zstream->zalloc = vnc_zlib_zalloc; + zstream->zfree = vnc_zlib_zfree; + + err = deflateInit2(zstream, level, Z_DEFLATED, MAX_WBITS, + MAX_MEM_LEVEL, strategy); + + if (err != Z_OK) { + fprintf(stderr, "VNC: error initializing zlib\n"); + return -1; + } + + vs->tight->levels[stream_id] = level; + zstream->opaque = vs; + } + + if (vs->tight->levels[stream_id] != level) { + if (deflateParams(zstream, level, strategy) != Z_OK) { + return -1; + } + vs->tight->levels[stream_id] = level; + } + return 0; +} + +static void tight_send_compact_size(VncState *vs, size_t len) +{ + int lpc = 0; + int bytes = 0; + char buf[3] = {0, 0, 0}; + + buf[bytes++] = len & 0x7F; + if (len > 0x7F) { + buf[bytes-1] |= 0x80; + buf[bytes++] = (len >> 7) & 0x7F; + if (len > 0x3FFF) { + buf[bytes-1] |= 0x80; + buf[bytes++] = (len >> 14) & 0xFF; + } + } + for (lpc = 0; lpc < bytes; lpc++) { + vnc_write_u8(vs, buf[lpc]); + } +} + +static int tight_compress_data(VncState *vs, int stream_id, size_t bytes, + int level, int strategy) +{ + z_streamp zstream = &vs->tight->stream[stream_id]; + int previous_out; + + if (bytes < VNC_TIGHT_MIN_TO_COMPRESS) { + vnc_write(vs, vs->tight->tight.buffer, vs->tight->tight.offset); + return bytes; + } + + if (tight_init_stream(vs, stream_id, level, strategy)) { + return -1; + } + + /* reserve memory in output buffer */ + buffer_reserve(&vs->tight->zlib, bytes + 64); + + /* set pointers */ + zstream->next_in = vs->tight->tight.buffer; + zstream->avail_in = vs->tight->tight.offset; + zstream->next_out = vs->tight->zlib.buffer + vs->tight->zlib.offset; + zstream->avail_out = vs->tight->zlib.capacity - vs->tight->zlib.offset; + previous_out = zstream->avail_out; + zstream->data_type = Z_BINARY; + + /* start encoding */ + if (deflate(zstream, Z_SYNC_FLUSH) != Z_OK) { + fprintf(stderr, "VNC: error during tight compression\n"); + return -1; + } + + vs->tight->zlib.offset = vs->tight->zlib.capacity - zstream->avail_out; + /* ...how much data has actually been produced by deflate() */ + bytes = previous_out - zstream->avail_out; + + tight_send_compact_size(vs, bytes); + vnc_write(vs, vs->tight->zlib.buffer, bytes); + + buffer_reset(&vs->tight->zlib); + + return bytes; +} + +/* + * Subencoding implementations. + */ +static void tight_pack24(VncState *vs, uint8_t *buf, size_t count, size_t *ret) +{ + uint8_t *buf8; + uint32_t pix; + int rshift, gshift, bshift; + + buf8 = buf; + + if (1 /* FIXME */) { + rshift = vs->client_pf.rshift; + gshift = vs->client_pf.gshift; + bshift = vs->client_pf.bshift; + } else { + rshift = 24 - vs->client_pf.rshift; + gshift = 24 - vs->client_pf.gshift; + bshift = 24 - vs->client_pf.bshift; + } + + if (ret) { + *ret = count * 3; + } + + while (count--) { + pix = ldl_he_p(buf8); + *buf++ = (char)(pix >> rshift); + *buf++ = (char)(pix >> gshift); + *buf++ = (char)(pix >> bshift); + buf8 += 4; + } +} + +static int send_full_color_rect(VncState *vs, int x, int y, int w, int h) +{ + int stream = 0; + ssize_t bytes; + +#ifdef CONFIG_VNC_PNG + if (tight_can_send_png_rect(vs, w, h)) { + return send_png_rect(vs, x, y, w, h, NULL); + } +#endif + + vnc_write_u8(vs, stream << 4); /* no flushing, no filter */ + + if (vs->tight->pixel24) { + tight_pack24(vs, vs->tight->tight.buffer, w * h, + &vs->tight->tight.offset); + bytes = 3; + } else { + bytes = vs->client_pf.bytes_per_pixel; + } + + bytes = tight_compress_data(vs, stream, w * h * bytes, + tight_conf[vs->tight->compression].raw_zlib_level, + Z_DEFAULT_STRATEGY); + + return (bytes >= 0); +} + +static int send_solid_rect(VncState *vs) +{ + size_t bytes; + + vnc_write_u8(vs, VNC_TIGHT_FILL << 4); /* no flushing, no filter */ + + if (vs->tight->pixel24) { + tight_pack24(vs, vs->tight->tight.buffer, 1, &vs->tight->tight.offset); + bytes = 3; + } else { + bytes = vs->client_pf.bytes_per_pixel; + } + + vnc_write(vs, vs->tight->tight.buffer, bytes); + return 1; +} + +static int send_mono_rect(VncState *vs, int x, int y, + int w, int h, uint32_t bg, uint32_t fg) +{ + ssize_t bytes; + int stream = 1; + int level = tight_conf[vs->tight->compression].mono_zlib_level; + +#ifdef CONFIG_VNC_PNG + if (tight_can_send_png_rect(vs, w, h)) { + int ret; + int bpp = vs->client_pf.bytes_per_pixel * 8; + VncPalette *palette = palette_new(2, bpp); + + palette_put(palette, bg); + palette_put(palette, fg); + ret = send_png_rect(vs, x, y, w, h, palette); + palette_destroy(palette); + return ret; + } +#endif + + bytes = DIV_ROUND_UP(w, 8) * h; + + vnc_write_u8(vs, (stream | VNC_TIGHT_EXPLICIT_FILTER) << 4); + vnc_write_u8(vs, VNC_TIGHT_FILTER_PALETTE); + vnc_write_u8(vs, 1); + + switch (vs->client_pf.bytes_per_pixel) { + case 4: + { + uint32_t buf[2] = {bg, fg}; + size_t ret = sizeof (buf); + + if (vs->tight->pixel24) { + tight_pack24(vs, (unsigned char*)buf, 2, &ret); + } + vnc_write(vs, buf, ret); + + tight_encode_mono_rect32(vs->tight->tight.buffer, w, h, bg, fg); + break; + } + case 2: + vnc_write(vs, &bg, 2); + vnc_write(vs, &fg, 2); + tight_encode_mono_rect16(vs->tight->tight.buffer, w, h, bg, fg); + break; + default: + vnc_write_u8(vs, bg); + vnc_write_u8(vs, fg); + tight_encode_mono_rect8(vs->tight->tight.buffer, w, h, bg, fg); + break; + } + vs->tight->tight.offset = bytes; + + bytes = tight_compress_data(vs, stream, bytes, level, Z_DEFAULT_STRATEGY); + return (bytes >= 0); +} + +struct palette_cb_priv { + VncState *vs; + uint8_t *header; +#ifdef CONFIG_VNC_PNG + png_colorp png_palette; +#endif +}; + +static void write_palette(int idx, uint32_t color, void *opaque) +{ + struct palette_cb_priv *priv = opaque; + VncState *vs = priv->vs; + uint32_t bytes = vs->client_pf.bytes_per_pixel; + + if (bytes == 4) { + ((uint32_t*)priv->header)[idx] = color; + } else { + ((uint16_t*)priv->header)[idx] = color; + } +} + +static bool send_gradient_rect(VncState *vs, int x, int y, int w, int h) +{ + int stream = 3; + int level = tight_conf[vs->tight->compression].gradient_zlib_level; + ssize_t bytes; + + if (vs->client_pf.bytes_per_pixel == 1) { + return send_full_color_rect(vs, x, y, w, h); + } + + vnc_write_u8(vs, (stream | VNC_TIGHT_EXPLICIT_FILTER) << 4); + vnc_write_u8(vs, VNC_TIGHT_FILTER_GRADIENT); + + buffer_reserve(&vs->tight->gradient, w * 3 * sizeof(int)); + + if (vs->tight->pixel24) { + tight_filter_gradient24(vs, vs->tight->tight.buffer, w, h); + bytes = 3; + } else if (vs->client_pf.bytes_per_pixel == 4) { + tight_filter_gradient32(vs, (uint32_t *)vs->tight->tight.buffer, w, h); + bytes = 4; + } else { + tight_filter_gradient16(vs, (uint16_t *)vs->tight->tight.buffer, w, h); + bytes = 2; + } + + buffer_reset(&vs->tight->gradient); + + bytes = w * h * bytes; + vs->tight->tight.offset = bytes; + + bytes = tight_compress_data(vs, stream, bytes, + level, Z_FILTERED); + return (bytes >= 0); +} + +static int send_palette_rect(VncState *vs, int x, int y, + int w, int h, VncPalette *palette) +{ + int stream = 2; + int level = tight_conf[vs->tight->compression].idx_zlib_level; + int colors; + ssize_t bytes; + +#ifdef CONFIG_VNC_PNG + if (tight_can_send_png_rect(vs, w, h)) { + return send_png_rect(vs, x, y, w, h, palette); + } +#endif + + colors = palette_size(palette); + + vnc_write_u8(vs, (stream | VNC_TIGHT_EXPLICIT_FILTER) << 4); + vnc_write_u8(vs, VNC_TIGHT_FILTER_PALETTE); + vnc_write_u8(vs, colors - 1); + + switch (vs->client_pf.bytes_per_pixel) { + case 4: + { + size_t old_offset, offset; + uint32_t header[palette_size(palette)]; + struct palette_cb_priv priv = { vs, (uint8_t *)header }; + + old_offset = vs->output.offset; + palette_iter(palette, write_palette, &priv); + vnc_write(vs, header, sizeof(header)); + + if (vs->tight->pixel24) { + tight_pack24(vs, vs->output.buffer + old_offset, colors, &offset); + vs->output.offset = old_offset + offset; + } + + tight_encode_indexed_rect32(vs->tight->tight.buffer, w * h, palette); + break; + } + case 2: + { + uint16_t header[palette_size(palette)]; + struct palette_cb_priv priv = { vs, (uint8_t *)header }; + + palette_iter(palette, write_palette, &priv); + vnc_write(vs, header, sizeof(header)); + tight_encode_indexed_rect16(vs->tight->tight.buffer, w * h, palette); + break; + } + default: + return -1; /* No palette for 8bits colors */ + } + bytes = w * h; + vs->tight->tight.offset = bytes; + + bytes = tight_compress_data(vs, stream, bytes, + level, Z_DEFAULT_STRATEGY); + return (bytes >= 0); +} + +/* + * JPEG compression stuff. + */ +#ifdef CONFIG_VNC_JPEG +/* + * Destination manager implementation for JPEG library. + */ + +/* This is called once per encoding */ +static void jpeg_init_destination(j_compress_ptr cinfo) +{ + VncState *vs = cinfo->client_data; + Buffer *buffer = &vs->tight->jpeg; + + cinfo->dest->next_output_byte = (JOCTET *)buffer->buffer + buffer->offset; + cinfo->dest->free_in_buffer = (size_t)(buffer->capacity - buffer->offset); +} + +/* This is called when we ran out of buffer (shouldn't happen!) */ +static boolean jpeg_empty_output_buffer(j_compress_ptr cinfo) +{ + VncState *vs = cinfo->client_data; + Buffer *buffer = &vs->tight->jpeg; + + buffer->offset = buffer->capacity; + buffer_reserve(buffer, 2048); + jpeg_init_destination(cinfo); + return TRUE; +} + +/* This is called when we are done processing data */ +static void jpeg_term_destination(j_compress_ptr cinfo) +{ + VncState *vs = cinfo->client_data; + Buffer *buffer = &vs->tight->jpeg; + + buffer->offset = buffer->capacity - cinfo->dest->free_in_buffer; +} + +static int send_jpeg_rect(VncState *vs, int x, int y, int w, int h, int quality) +{ + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + struct jpeg_destination_mgr manager; + pixman_image_t *linebuf; + JSAMPROW row[1]; + uint8_t *buf; + int dy; + + if (surface_bytes_per_pixel(vs->vd->ds) == 1) { + return send_full_color_rect(vs, x, y, w, h); + } + + buffer_reserve(&vs->tight->jpeg, 2048); + + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + + cinfo.client_data = vs; + cinfo.image_width = w; + cinfo.image_height = h; + cinfo.input_components = 3; + cinfo.in_color_space = JCS_RGB; + + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, true); + + manager.init_destination = jpeg_init_destination; + manager.empty_output_buffer = jpeg_empty_output_buffer; + manager.term_destination = jpeg_term_destination; + cinfo.dest = &manager; + + jpeg_start_compress(&cinfo, true); + + linebuf = qemu_pixman_linebuf_create(PIXMAN_BE_r8g8b8, w); + buf = (uint8_t *)pixman_image_get_data(linebuf); + row[0] = buf; + for (dy = 0; dy < h; dy++) { + qemu_pixman_linebuf_fill(linebuf, vs->vd->server, w, x, y + dy); + jpeg_write_scanlines(&cinfo, row, 1); + } + qemu_pixman_image_unref(linebuf); + + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + + vnc_write_u8(vs, VNC_TIGHT_JPEG << 4); + + tight_send_compact_size(vs, vs->tight->jpeg.offset); + vnc_write(vs, vs->tight->jpeg.buffer, vs->tight->jpeg.offset); + buffer_reset(&vs->tight->jpeg); + + return 1; +} +#endif /* CONFIG_VNC_JPEG */ + +/* + * PNG compression stuff. + */ +#ifdef CONFIG_VNC_PNG +static void write_png_palette(int idx, uint32_t pix, void *opaque) +{ + struct palette_cb_priv *priv = opaque; + VncState *vs = priv->vs; + png_colorp color = &priv->png_palette[idx]; + + if (vs->tight->pixel24) + { + color->red = (pix >> vs->client_pf.rshift) & vs->client_pf.rmax; + color->green = (pix >> vs->client_pf.gshift) & vs->client_pf.gmax; + color->blue = (pix >> vs->client_pf.bshift) & vs->client_pf.bmax; + } + else + { + int red, green, blue; + + red = (pix >> vs->client_pf.rshift) & vs->client_pf.rmax; + green = (pix >> vs->client_pf.gshift) & vs->client_pf.gmax; + blue = (pix >> vs->client_pf.bshift) & vs->client_pf.bmax; + color->red = ((red * 255 + vs->client_pf.rmax / 2) / + vs->client_pf.rmax); + color->green = ((green * 255 + vs->client_pf.gmax / 2) / + vs->client_pf.gmax); + color->blue = ((blue * 255 + vs->client_pf.bmax / 2) / + vs->client_pf.bmax); + } +} + +static void png_write_data(png_structp png_ptr, png_bytep data, + png_size_t length) +{ + VncState *vs = png_get_io_ptr(png_ptr); + + buffer_reserve(&vs->tight->png, vs->tight->png.offset + length); + memcpy(vs->tight->png.buffer + vs->tight->png.offset, data, length); + + vs->tight->png.offset += length; +} + +static void png_flush_data(png_structp png_ptr) +{ +} + +static void *vnc_png_malloc(png_structp png_ptr, png_size_t size) +{ + return g_malloc(size); +} + +static void vnc_png_free(png_structp png_ptr, png_voidp ptr) +{ + g_free(ptr); +} + +static int send_png_rect(VncState *vs, int x, int y, int w, int h, + VncPalette *palette) +{ + png_byte color_type; + png_structp png_ptr; + png_infop info_ptr; + png_colorp png_palette = NULL; + pixman_image_t *linebuf; + int level = tight_png_conf[vs->tight->compression].png_zlib_level; + int filters = tight_png_conf[vs->tight->compression].png_filters; + uint8_t *buf; + int dy; + + png_ptr = png_create_write_struct_2(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL, + NULL, vnc_png_malloc, vnc_png_free); + + if (png_ptr == NULL) + return -1; + + info_ptr = png_create_info_struct(png_ptr); + + if (info_ptr == NULL) { + png_destroy_write_struct(&png_ptr, NULL); + return -1; + } + + png_set_write_fn(png_ptr, (void *) vs, png_write_data, png_flush_data); + png_set_compression_level(png_ptr, level); + png_set_filter(png_ptr, PNG_FILTER_TYPE_DEFAULT, filters); + + if (palette) { + color_type = PNG_COLOR_TYPE_PALETTE; + } else { + color_type = PNG_COLOR_TYPE_RGB; + } + + png_set_IHDR(png_ptr, info_ptr, w, h, + 8, color_type, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + if (color_type == PNG_COLOR_TYPE_PALETTE) { + struct palette_cb_priv priv; + + png_palette = png_malloc(png_ptr, sizeof(*png_palette) * + palette_size(palette)); + + priv.vs = vs; + priv.png_palette = png_palette; + palette_iter(palette, write_png_palette, &priv); + + png_set_PLTE(png_ptr, info_ptr, png_palette, palette_size(palette)); + + if (vs->client_pf.bytes_per_pixel == 4) { + tight_encode_indexed_rect32(vs->tight->tight.buffer, w * h, + palette); + } else { + tight_encode_indexed_rect16(vs->tight->tight.buffer, w * h, + palette); + } + } + + png_write_info(png_ptr, info_ptr); + + buffer_reserve(&vs->tight->png, 2048); + linebuf = qemu_pixman_linebuf_create(PIXMAN_BE_r8g8b8, w); + buf = (uint8_t *)pixman_image_get_data(linebuf); + for (dy = 0; dy < h; dy++) + { + if (color_type == PNG_COLOR_TYPE_PALETTE) { + memcpy(buf, vs->tight->tight.buffer + (dy * w), w); + } else { + qemu_pixman_linebuf_fill(linebuf, vs->vd->server, w, x, y + dy); + } + png_write_row(png_ptr, buf); + } + qemu_pixman_image_unref(linebuf); + + png_write_end(png_ptr, NULL); + + if (color_type == PNG_COLOR_TYPE_PALETTE) { + png_free(png_ptr, png_palette); + } + + png_destroy_write_struct(&png_ptr, &info_ptr); + + vnc_write_u8(vs, VNC_TIGHT_PNG << 4); + + tight_send_compact_size(vs, vs->tight->png.offset); + vnc_write(vs, vs->tight->png.buffer, vs->tight->png.offset); + buffer_reset(&vs->tight->png); + return 1; +} +#endif /* CONFIG_VNC_PNG */ + +static void vnc_tight_start(VncState *vs) +{ + buffer_reset(&vs->tight->tight); + + // make the output buffer be the zlib buffer, so we can compress it later + vs->tight->tmp = vs->output; + vs->output = vs->tight->tight; +} + +static void vnc_tight_stop(VncState *vs) +{ + // switch back to normal output/zlib buffers + vs->tight->tight = vs->output; + vs->output = vs->tight->tmp; +} + +static int send_sub_rect_nojpeg(VncState *vs, int x, int y, int w, int h, + int bg, int fg, int colors, VncPalette *palette) +{ + int ret; + + if (colors == 0) { + if (tight_detect_smooth_image(vs, w, h)) { + ret = send_gradient_rect(vs, x, y, w, h); + } else { + ret = send_full_color_rect(vs, x, y, w, h); + } + } else if (colors == 1) { + ret = send_solid_rect(vs); + } else if (colors == 2) { + ret = send_mono_rect(vs, x, y, w, h, bg, fg); + } else if (colors <= 256) { + ret = send_palette_rect(vs, x, y, w, h, palette); + } else { + ret = 0; + } + return ret; +} + +#ifdef CONFIG_VNC_JPEG +static int send_sub_rect_jpeg(VncState *vs, int x, int y, int w, int h, + int bg, int fg, int colors, + VncPalette *palette, bool force) +{ + int ret; + + if (colors == 0) { + if (force || (tight_jpeg_conf[vs->tight->quality].jpeg_full && + tight_detect_smooth_image(vs, w, h))) { + int quality = tight_conf[vs->tight->quality].jpeg_quality; + + ret = send_jpeg_rect(vs, x, y, w, h, quality); + } else { + ret = send_full_color_rect(vs, x, y, w, h); + } + } else if (colors == 1) { + ret = send_solid_rect(vs); + } else if (colors == 2) { + ret = send_mono_rect(vs, x, y, w, h, bg, fg); + } else if (colors <= 256) { + if (force || (colors > 96 && + tight_jpeg_conf[vs->tight->quality].jpeg_idx && + tight_detect_smooth_image(vs, w, h))) { + int quality = tight_conf[vs->tight->quality].jpeg_quality; + + ret = send_jpeg_rect(vs, x, y, w, h, quality); + } else { + ret = send_palette_rect(vs, x, y, w, h, palette); + } + } else { + ret = 0; + } + return ret; +} +#endif + +static __thread VncPalette *color_count_palette; +static __thread Notifier vnc_tight_cleanup_notifier; + +static void vnc_tight_cleanup(Notifier *n, void *value) +{ + g_free(color_count_palette); + color_count_palette = NULL; +} + +static int send_sub_rect(VncState *vs, int x, int y, int w, int h) +{ + uint32_t bg = 0, fg = 0; + int colors; + int ret = 0; +#ifdef CONFIG_VNC_JPEG + bool force_jpeg = false; + bool allow_jpeg = true; +#endif + + if (!color_count_palette) { + color_count_palette = g_malloc(sizeof(VncPalette)); + vnc_tight_cleanup_notifier.notify = vnc_tight_cleanup; + qemu_thread_atexit_add(&vnc_tight_cleanup_notifier); + } + + vnc_framebuffer_update(vs, x, y, w, h, vs->tight->type); + + vnc_tight_start(vs); + vnc_raw_send_framebuffer_update(vs, x, y, w, h); + vnc_tight_stop(vs); + +#ifdef CONFIG_VNC_JPEG + if (!vs->vd->non_adaptive && vs->tight->quality != (uint8_t)-1) { + double freq = vnc_update_freq(vs, x, y, w, h); + + if (freq < tight_jpeg_conf[vs->tight->quality].jpeg_freq_min) { + allow_jpeg = false; + } + if (freq >= tight_jpeg_conf[vs->tight->quality].jpeg_freq_threshold) { + force_jpeg = true; + vnc_sent_lossy_rect(vs, x, y, w, h); + } + } +#endif + + colors = tight_fill_palette(vs, x, y, w * h, &bg, &fg, color_count_palette); + +#ifdef CONFIG_VNC_JPEG + if (allow_jpeg && vs->tight->quality != (uint8_t)-1) { + ret = send_sub_rect_jpeg(vs, x, y, w, h, bg, fg, colors, + color_count_palette, force_jpeg); + } else { + ret = send_sub_rect_nojpeg(vs, x, y, w, h, bg, fg, colors, + color_count_palette); + } +#else + ret = send_sub_rect_nojpeg(vs, x, y, w, h, bg, fg, colors, + color_count_palette); +#endif + + return ret; +} + +static int send_sub_rect_solid(VncState *vs, int x, int y, int w, int h) +{ + vnc_framebuffer_update(vs, x, y, w, h, vs->tight->type); + + vnc_tight_start(vs); + vnc_raw_send_framebuffer_update(vs, x, y, w, h); + vnc_tight_stop(vs); + + return send_solid_rect(vs); +} + +static int send_rect_simple(VncState *vs, int x, int y, int w, int h, + bool split) +{ + int max_size, max_width; + int max_sub_width, max_sub_height; + int dx, dy; + int rw, rh; + int n = 0; + + max_size = tight_conf[vs->tight->compression].max_rect_size; + max_width = tight_conf[vs->tight->compression].max_rect_width; + + if (split && (w > max_width || w * h > max_size)) { + max_sub_width = (w > max_width) ? max_width : w; + max_sub_height = max_size / max_sub_width; + + for (dy = 0; dy < h; dy += max_sub_height) { + for (dx = 0; dx < w; dx += max_width) { + rw = MIN(max_sub_width, w - dx); + rh = MIN(max_sub_height, h - dy); + n += send_sub_rect(vs, x+dx, y+dy, rw, rh); + } + } + } else { + n += send_sub_rect(vs, x, y, w, h); + } + + return n; +} + +static int find_large_solid_color_rect(VncState *vs, int x, int y, + int w, int h, int max_rows) +{ + int dx, dy, dw, dh; + int n = 0; + + /* Try to find large solid-color areas and send them separately. */ + + for (dy = y; dy < y + h; dy += VNC_TIGHT_MAX_SPLIT_TILE_SIZE) { + + /* If a rectangle becomes too large, send its upper part now. */ + + if (dy - y >= max_rows) { + n += send_rect_simple(vs, x, y, w, max_rows, true); + y += max_rows; + h -= max_rows; + } + + dh = MIN(VNC_TIGHT_MAX_SPLIT_TILE_SIZE, (y + h - dy)); + + for (dx = x; dx < x + w; dx += VNC_TIGHT_MAX_SPLIT_TILE_SIZE) { + uint32_t color_value; + int x_best, y_best, w_best, h_best; + + dw = MIN(VNC_TIGHT_MAX_SPLIT_TILE_SIZE, (x + w - dx)); + + if (!check_solid_tile(vs, dx, dy, dw, dh, &color_value, false)) { + continue ; + } + + /* Get dimensions of solid-color area. */ + + find_best_solid_area(vs, dx, dy, w - (dx - x), h - (dy - y), + color_value, &w_best, &h_best); + + /* Make sure a solid rectangle is large enough + (or the whole rectangle is of the same color). */ + + if (w_best * h_best != w * h && + w_best * h_best < VNC_TIGHT_MIN_SOLID_SUBRECT_SIZE) { + continue; + } + + /* Try to extend solid rectangle to maximum size. */ + + x_best = dx; y_best = dy; + extend_solid_area(vs, x, y, w, h, color_value, + &x_best, &y_best, &w_best, &h_best); + + /* Send rectangles at top and left to solid-color area. */ + + if (y_best != y) { + n += send_rect_simple(vs, x, y, w, y_best-y, true); + } + if (x_best != x) { + n += tight_send_framebuffer_update(vs, x, y_best, + x_best-x, h_best); + } + + /* Send solid-color rectangle. */ + n += send_sub_rect_solid(vs, x_best, y_best, w_best, h_best); + + /* Send remaining rectangles (at right and bottom). */ + + if (x_best + w_best != x + w) { + n += tight_send_framebuffer_update(vs, x_best+w_best, + y_best, + w-(x_best-x)-w_best, + h_best); + } + if (y_best + h_best != y + h) { + n += tight_send_framebuffer_update(vs, x, y_best+h_best, + w, h-(y_best-y)-h_best); + } + + /* Return after all recursive calls are done. */ + return n; + } + } + return n + send_rect_simple(vs, x, y, w, h, true); +} + +static int tight_send_framebuffer_update(VncState *vs, int x, int y, + int w, int h) +{ + int max_rows; + + if (vs->client_pf.bytes_per_pixel == 4 && vs->client_pf.rmax == 0xFF && + vs->client_pf.bmax == 0xFF && vs->client_pf.gmax == 0xFF) { + vs->tight->pixel24 = true; + } else { + vs->tight->pixel24 = false; + } + +#ifdef CONFIG_VNC_JPEG + if (vs->tight->quality != (uint8_t)-1) { + double freq = vnc_update_freq(vs, x, y, w, h); + + if (freq > tight_jpeg_conf[vs->tight->quality].jpeg_freq_threshold) { + return send_rect_simple(vs, x, y, w, h, false); + } + } +#endif + + if (w * h < VNC_TIGHT_MIN_SPLIT_RECT_SIZE) { + return send_rect_simple(vs, x, y, w, h, true); + } + + /* Calculate maximum number of rows in one non-solid rectangle. */ + + max_rows = tight_conf[vs->tight->compression].max_rect_size; + max_rows /= MIN(tight_conf[vs->tight->compression].max_rect_width, w); + + return find_large_solid_color_rect(vs, x, y, w, h, max_rows); +} + +int vnc_tight_send_framebuffer_update(VncState *vs, int x, int y, + int w, int h) +{ + vs->tight->type = VNC_ENCODING_TIGHT; + return tight_send_framebuffer_update(vs, x, y, w, h); +} + +int vnc_tight_png_send_framebuffer_update(VncState *vs, int x, int y, + int w, int h) +{ + vs->tight->type = VNC_ENCODING_TIGHT_PNG; + return tight_send_framebuffer_update(vs, x, y, w, h); +} + +void vnc_tight_clear(VncState *vs) +{ + int i; + for (i = 0; i < ARRAY_SIZE(vs->tight->stream); i++) { + if (vs->tight->stream[i].opaque) { + deflateEnd(&vs->tight->stream[i]); + } + } + + buffer_free(&vs->tight->tight); + buffer_free(&vs->tight->zlib); + buffer_free(&vs->tight->gradient); +#ifdef CONFIG_VNC_JPEG + buffer_free(&vs->tight->jpeg); +#endif +#ifdef CONFIG_VNC_PNG + buffer_free(&vs->tight->png); +#endif +} diff --git a/ui/vnc-enc-tight.h b/ui/vnc-enc-tight.h new file mode 100644 index 000000000..95c3dac02 --- /dev/null +++ b/ui/vnc-enc-tight.h @@ -0,0 +1,183 @@ +/* + * QEMU VNC display driver: tight encoding + * + * From libvncserver/rfb/rfbproto.h + * Copyright (C) 2005 Rohit Kumar, Johannes E. Schindelin + * Copyright (C) 2000-2002 Constantin Kaplinsky. All Rights Reserved. + * Copyright (C) 2000 Tridia Corporation. All Rights Reserved. + * Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef VNC_ENC_TIGHT_H +#define VNC_ENC_TIGHT_H + +/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * Tight Encoding. + * + *-- The first byte of each Tight-encoded rectangle is a "compression control + * byte". Its format is as follows (bit 0 is the least significant one): + * + * bit 0: if 1, then compression stream 0 should be reset; + * bit 1: if 1, then compression stream 1 should be reset; + * bit 2: if 1, then compression stream 2 should be reset; + * bit 3: if 1, then compression stream 3 should be reset; + * bits 7-4: if 1000 (0x08), then the compression type is "fill", + * if 1001 (0x09), then the compression type is "jpeg", + * if 1010 (0x0A), then the compression type is "png", + * if 0xxx, then the compression type is "basic", + * values greater than 1010 are not valid. + * + * If the compression type is "basic", then bits 6..4 of the + * compression control byte (those xxx in 0xxx) specify the following: + * + * bits 5-4: decimal representation is the index of a particular zlib + * stream which should be used for decompressing the data; + * bit 6: if 1, then a "filter id" byte is following this byte. + * + *-- The data that follows after the compression control byte described + * above depends on the compression type ("fill", "jpeg", "png" or "basic"). + * + *-- If the compression type is "fill", then the only pixel value follows, in + * client pixel format (see NOTE 1). This value applies to all pixels of the + * rectangle. + * + *-- If the compression type is "jpeg" or "png", the following data stream + * looks like this: + * + * 1..3 bytes: data size (N) in compact representation; + * N bytes: JPEG or PNG image. + * + * Data size is compactly represented in one, two or three bytes, according + * to the following scheme: + * + * 0xxxxxxx (for values 0..127) + * 1xxxxxxx 0yyyyyyy (for values 128..16383) + * 1xxxxxxx 1yyyyyyy zzzzzzzz (for values 16384..4194303) + * + * Here each character denotes one bit, xxxxxxx are the least significant 7 + * bits of the value (bits 0-6), yyyyyyy are bits 7-13, and zzzzzzzz are the + * most significant 8 bits (bits 14-21). For example, decimal value 10000 + * should be represented as two bytes: binary 10010000 01001110, or + * hexadecimal 90 4E. + * + *-- If the compression type is "basic" and bit 6 of the compression control + * byte was set to 1, then the next (second) byte specifies "filter id" which + * tells the decoder what filter type was used by the encoder to pre-process + * pixel data before the compression. The "filter id" byte can be one of the + * following: + * + * 0: no filter ("copy" filter); + * 1: "palette" filter; + * 2: "gradient" filter. + * + *-- If bit 6 of the compression control byte is set to 0 (no "filter id" + * byte), or if the filter id is 0, then raw pixel values in the client + * format (see NOTE 1) will be compressed. See below details on the + * compression. + * + *-- The "gradient" filter pre-processes pixel data with a simple algorithm + * which converts each color component to a difference between a "predicted" + * intensity and the actual intensity. Such a technique does not affect + * uncompressed data size, but helps to compress photo-like images better. + * Pseudo-code for converting intensities to differences is the following: + * + * P[i,j] := V[i-1,j] + V[i,j-1] - V[i-1,j-1]; + * if (P[i,j] < 0) then P[i,j] := 0; + * if (P[i,j] > MAX) then P[i,j] := MAX; + * D[i,j] := V[i,j] - P[i,j]; + * + * Here V[i,j] is the intensity of a color component for a pixel at + * coordinates (i,j). MAX is the maximum value of intensity for a color + * component. + * + *-- The "palette" filter converts true-color pixel data to indexed colors + * and a palette which can consist of 2..256 colors. If the number of colors + * is 2, then each pixel is encoded in 1 bit, otherwise 8 bits is used to + * encode one pixel. 1-bit encoding is performed such way that the most + * significant bits correspond to the leftmost pixels, and each raw of pixels + * is aligned to the byte boundary. When "palette" filter is used, the + * palette is sent before the pixel data. The palette begins with an unsigned + * byte which value is the number of colors in the palette minus 1 (i.e. 1 + * means 2 colors, 255 means 256 colors in the palette). Then follows the + * palette itself which consist of pixel values in client pixel format (see + * NOTE 1). + * + *-- The pixel data is compressed using the zlib library. But if the data + * size after applying the filter but before the compression is less then 12, + * then the data is sent as is, uncompressed. Four separate zlib streams + * (0..3) can be used and the decoder should read the actual stream id from + * the compression control byte (see NOTE 2). + * + * If the compression is not used, then the pixel data is sent as is, + * otherwise the data stream looks like this: + * + * 1..3 bytes: data size (N) in compact representation; + * N bytes: zlib-compressed data. + * + * Data size is compactly represented in one, two or three bytes, just like + * in the "jpeg" compression method (see above). + * + *-- NOTE 1. If the color depth is 24, and all three color components are + * 8-bit wide, then one pixel in Tight encoding is always represented by + * three bytes, where the first byte is red component, the second byte is + * green component, and the third byte is blue component of the pixel color + * value. This applies to colors in palettes as well. + * + *-- NOTE 2. The decoder must reset compression streams' states before + * decoding the rectangle, if some of bits 0,1,2,3 in the compression control + * byte are set to 1. Note that the decoder must reset zlib streams even if + * the compression type is "fill", "jpeg" or "png". + * + *-- NOTE 3. The "gradient" filter and "jpeg" compression may be used only + * when bits-per-pixel value is either 16 or 32, not 8. + * + *-- NOTE 4. The width of any Tight-encoded rectangle cannot exceed 2048 + * pixels. If a rectangle is wider, it must be split into several rectangles + * and each one should be encoded separately. + * + */ + +#define VNC_TIGHT_EXPLICIT_FILTER 0x04 +#define VNC_TIGHT_FILL 0x08 +#define VNC_TIGHT_JPEG 0x09 +#define VNC_TIGHT_PNG 0x0A +#define VNC_TIGHT_MAX_SUBENCODING 0x0A + +/* Filters to improve compression efficiency */ +#define VNC_TIGHT_FILTER_COPY 0x00 +#define VNC_TIGHT_FILTER_PALETTE 0x01 +#define VNC_TIGHT_FILTER_GRADIENT 0x02 + +/* Note: The following constant should not be changed. */ +#define VNC_TIGHT_MIN_TO_COMPRESS 12 + +/* The parameters below may be adjusted. */ +#define VNC_TIGHT_MIN_SPLIT_RECT_SIZE 4096 +#define VNC_TIGHT_MIN_SOLID_SUBRECT_SIZE 2048 +#define VNC_TIGHT_MAX_SPLIT_TILE_SIZE 16 + +#define VNC_TIGHT_JPEG_MIN_RECT_SIZE 4096 +#define VNC_TIGHT_DETECT_SUBROW_WIDTH 7 +#define VNC_TIGHT_DETECT_MIN_WIDTH 8 +#define VNC_TIGHT_DETECT_MIN_HEIGHT 8 + +#endif /* VNC_ENC_TIGHT_H */ diff --git a/ui/vnc-enc-zlib.c b/ui/vnc-enc-zlib.c new file mode 100644 index 000000000..900ae5b30 --- /dev/null +++ b/ui/vnc-enc-zlib.c @@ -0,0 +1,154 @@ +/* + * QEMU VNC display driver: zlib encoding + * + * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws> + * Copyright (C) 2006 Fabrice Bellard + * Copyright (C) 2009 Red Hat, Inc + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "vnc.h" + +#define ZALLOC_ALIGNMENT 16 + +void *vnc_zlib_zalloc(void *x, unsigned items, unsigned size) +{ + void *p; + + size *= items; + size = (size + ZALLOC_ALIGNMENT - 1) & ~(ZALLOC_ALIGNMENT - 1); + + p = g_malloc0(size); + + return (p); +} + +void vnc_zlib_zfree(void *x, void *addr) +{ + g_free(addr); +} + +static void vnc_zlib_start(VncState *vs) +{ + buffer_reset(&vs->zlib.zlib); + + // make the output buffer be the zlib buffer, so we can compress it later + vs->zlib.tmp = vs->output; + vs->output = vs->zlib.zlib; +} + +static int vnc_zlib_stop(VncState *vs) +{ + z_streamp zstream = &vs->zlib.stream; + int previous_out; + + // switch back to normal output/zlib buffers + vs->zlib.zlib = vs->output; + vs->output = vs->zlib.tmp; + + // compress the zlib buffer + + // initialize the stream + // XXX need one stream per session + if (zstream->opaque != vs) { + int err; + + VNC_DEBUG("VNC: initializing zlib stream\n"); + VNC_DEBUG("VNC: opaque = %p | vs = %p\n", zstream->opaque, vs); + zstream->zalloc = vnc_zlib_zalloc; + zstream->zfree = vnc_zlib_zfree; + + err = deflateInit2(zstream, vs->tight->compression, Z_DEFLATED, + MAX_WBITS, + MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY); + + if (err != Z_OK) { + fprintf(stderr, "VNC: error initializing zlib\n"); + return -1; + } + + vs->zlib.level = vs->tight->compression; + zstream->opaque = vs; + } + + if (vs->tight->compression != vs->zlib.level) { + if (deflateParams(zstream, vs->tight->compression, + Z_DEFAULT_STRATEGY) != Z_OK) { + return -1; + } + vs->zlib.level = vs->tight->compression; + } + + // reserve memory in output buffer + buffer_reserve(&vs->output, vs->zlib.zlib.offset + 64); + + // set pointers + zstream->next_in = vs->zlib.zlib.buffer; + zstream->avail_in = vs->zlib.zlib.offset; + zstream->next_out = vs->output.buffer + vs->output.offset; + zstream->avail_out = vs->output.capacity - vs->output.offset; + previous_out = zstream->avail_out; + zstream->data_type = Z_BINARY; + + // start encoding + if (deflate(zstream, Z_SYNC_FLUSH) != Z_OK) { + fprintf(stderr, "VNC: error during zlib compression\n"); + return -1; + } + + vs->output.offset = vs->output.capacity - zstream->avail_out; + return previous_out - zstream->avail_out; +} + +int vnc_zlib_send_framebuffer_update(VncState *vs, int x, int y, int w, int h) +{ + int old_offset, new_offset, bytes_written; + + vnc_framebuffer_update(vs, x, y, w, h, VNC_ENCODING_ZLIB); + + // remember where we put in the follow-up size + old_offset = vs->output.offset; + vnc_write_s32(vs, 0); + + // compress the stream + vnc_zlib_start(vs); + vnc_raw_send_framebuffer_update(vs, x, y, w, h); + bytes_written = vnc_zlib_stop(vs); + + if (bytes_written == -1) + return 0; + + // hack in the size + new_offset = vs->output.offset; + vs->output.offset = old_offset; + vnc_write_u32(vs, bytes_written); + vs->output.offset = new_offset; + + return 1; +} + +void vnc_zlib_clear(VncState *vs) +{ + if (vs->zlib.stream.opaque) { + deflateEnd(&vs->zlib.stream); + } + buffer_free(&vs->zlib.zlib); +} diff --git a/ui/vnc-enc-zrle.c b/ui/vnc-enc-zrle.c new file mode 100644 index 000000000..bd33b8906 --- /dev/null +++ b/ui/vnc-enc-zrle.c @@ -0,0 +1,366 @@ +/* + * QEMU VNC display driver: Zlib Run-length Encoding (ZRLE) + * + * From libvncserver/libvncserver/zrle.c + * Copyright (C) 2002 RealVNC Ltd. All Rights Reserved. + * Copyright (C) 2003 Sun Microsystems, Inc. + * + * Copyright (C) 2010 Corentin Chary <corentin.chary@gmail.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "vnc.h" +#include "vnc-enc-zrle.h" + +static const int bits_per_packed_pixel[] = { + 0, 1, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 +}; + + +static void vnc_zrle_start(VncState *vs) +{ + buffer_reset(&vs->zrle->zrle); + + /* make the output buffer be the zlib buffer, so we can compress it later */ + vs->zrle->tmp = vs->output; + vs->output = vs->zrle->zrle; +} + +static void vnc_zrle_stop(VncState *vs) +{ + /* switch back to normal output/zlib buffers */ + vs->zrle->zrle = vs->output; + vs->output = vs->zrle->tmp; +} + +static void *zrle_convert_fb(VncState *vs, int x, int y, int w, int h, + int bpp) +{ + Buffer tmp; + + buffer_reset(&vs->zrle->fb); + buffer_reserve(&vs->zrle->fb, w * h * bpp + bpp); + + tmp = vs->output; + vs->output = vs->zrle->fb; + + vnc_raw_send_framebuffer_update(vs, x, y, w, h); + + vs->zrle->fb = vs->output; + vs->output = tmp; + return vs->zrle->fb.buffer; +} + +static int zrle_compress_data(VncState *vs, int level) +{ + z_streamp zstream = &vs->zrle->stream; + + buffer_reset(&vs->zrle->zlib); + + if (zstream->opaque != vs) { + int err; + + zstream->zalloc = vnc_zlib_zalloc; + zstream->zfree = vnc_zlib_zfree; + + err = deflateInit2(zstream, level, Z_DEFLATED, MAX_WBITS, + MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY); + + if (err != Z_OK) { + fprintf(stderr, "VNC: error initializing zlib\n"); + return -1; + } + + zstream->opaque = vs; + } + + /* reserve memory in output buffer */ + buffer_reserve(&vs->zrle->zlib, vs->zrle->zrle.offset + 64); + + /* set pointers */ + zstream->next_in = vs->zrle->zrle.buffer; + zstream->avail_in = vs->zrle->zrle.offset; + zstream->next_out = vs->zrle->zlib.buffer; + zstream->avail_out = vs->zrle->zlib.capacity; + zstream->data_type = Z_BINARY; + + /* start encoding */ + if (deflate(zstream, Z_SYNC_FLUSH) != Z_OK) { + fprintf(stderr, "VNC: error during zrle compression\n"); + return -1; + } + + vs->zrle->zlib.offset = vs->zrle->zlib.capacity - zstream->avail_out; + return vs->zrle->zlib.offset; +} + +/* Try to work out whether to use RLE and/or a palette. We do this by + * estimating the number of bytes which will be generated and picking the + * method which results in the fewest bytes. Of course this may not result + * in the fewest bytes after compression... */ +static void zrle_choose_palette_rle(VncState *vs, int w, int h, + VncPalette *palette, int bpp_out, + int runs, int single_pixels, + int zywrle_level, + bool *use_rle, bool *use_palette) +{ + size_t estimated_bytes; + size_t plain_rle_bytes; + + *use_palette = *use_rle = false; + + estimated_bytes = w * h * (bpp_out / 8); /* start assuming raw */ + + if (bpp_out != 8) { + if (zywrle_level > 0 && !(zywrle_level & 0x80)) + estimated_bytes >>= zywrle_level; + } + + plain_rle_bytes = ((bpp_out / 8) + 1) * (runs + single_pixels); + + if (plain_rle_bytes < estimated_bytes) { + *use_rle = true; + estimated_bytes = plain_rle_bytes; + } + + if (palette_size(palette) < 128) { + int palette_rle_bytes; + + palette_rle_bytes = (bpp_out / 8) * palette_size(palette); + palette_rle_bytes += 2 * runs + single_pixels; + + if (palette_rle_bytes < estimated_bytes) { + *use_rle = true; + *use_palette = true; + estimated_bytes = palette_rle_bytes; + } + + if (palette_size(palette) < 17) { + int packed_bytes; + + packed_bytes = (bpp_out / 8) * palette_size(palette); + packed_bytes += w * h * + bits_per_packed_pixel[palette_size(palette)-1] / 8; + + if (packed_bytes < estimated_bytes) { + *use_rle = false; + *use_palette = true; + } + } + } +} + +static void zrle_write_u32(VncState *vs, uint32_t value) +{ + vnc_write(vs, (uint8_t *)&value, 4); +} + +static void zrle_write_u24a(VncState *vs, uint32_t value) +{ + vnc_write(vs, (uint8_t *)&value, 3); +} + +static void zrle_write_u24b(VncState *vs, uint32_t value) +{ + vnc_write(vs, ((uint8_t *)&value) + 1, 3); +} + +static void zrle_write_u16(VncState *vs, uint16_t value) +{ + vnc_write(vs, (uint8_t *)&value, 2); +} + +static void zrle_write_u8(VncState *vs, uint8_t value) +{ + vnc_write_u8(vs, value); +} + +#define ENDIAN_LITTLE 0 +#define ENDIAN_BIG 1 +#define ENDIAN_NO 2 + +#define ZRLE_BPP 8 +#define ZYWRLE_ENDIAN ENDIAN_NO +#include "vnc-enc-zrle.c.inc" +#undef ZRLE_BPP + +#define ZRLE_BPP 15 +#undef ZYWRLE_ENDIAN +#define ZYWRLE_ENDIAN ENDIAN_LITTLE +#include "vnc-enc-zrle.c.inc" + +#undef ZYWRLE_ENDIAN +#define ZYWRLE_ENDIAN ENDIAN_BIG +#include "vnc-enc-zrle.c.inc" + +#undef ZRLE_BPP +#define ZRLE_BPP 16 +#undef ZYWRLE_ENDIAN +#define ZYWRLE_ENDIAN ENDIAN_LITTLE +#include "vnc-enc-zrle.c.inc" + +#undef ZYWRLE_ENDIAN +#define ZYWRLE_ENDIAN ENDIAN_BIG +#include "vnc-enc-zrle.c.inc" + +#undef ZRLE_BPP +#define ZRLE_BPP 32 +#undef ZYWRLE_ENDIAN +#define ZYWRLE_ENDIAN ENDIAN_LITTLE +#include "vnc-enc-zrle.c.inc" + +#undef ZYWRLE_ENDIAN +#define ZYWRLE_ENDIAN ENDIAN_BIG +#include "vnc-enc-zrle.c.inc" + +#define ZRLE_COMPACT_PIXEL 24a +#undef ZYWRLE_ENDIAN +#define ZYWRLE_ENDIAN ENDIAN_LITTLE +#include "vnc-enc-zrle.c.inc" + +#undef ZYWRLE_ENDIAN +#define ZYWRLE_ENDIAN ENDIAN_BIG +#include "vnc-enc-zrle.c.inc" + +#undef ZRLE_COMPACT_PIXEL +#define ZRLE_COMPACT_PIXEL 24b +#undef ZYWRLE_ENDIAN +#define ZYWRLE_ENDIAN ENDIAN_LITTLE +#include "vnc-enc-zrle.c.inc" + +#undef ZYWRLE_ENDIAN +#define ZYWRLE_ENDIAN ENDIAN_BIG +#include "vnc-enc-zrle.c.inc" +#undef ZRLE_COMPACT_PIXEL +#undef ZRLE_BPP + +static int zrle_send_framebuffer_update(VncState *vs, int x, int y, + int w, int h) +{ + bool be = vs->client_be; + size_t bytes; + int zywrle_level; + + if (vs->zrle->type == VNC_ENCODING_ZYWRLE) { + if (!vs->vd->lossy || vs->tight->quality == (uint8_t)-1 + || vs->tight->quality == 9) { + zywrle_level = 0; + vs->zrle->type = VNC_ENCODING_ZRLE; + } else if (vs->tight->quality < 3) { + zywrle_level = 3; + } else if (vs->tight->quality < 6) { + zywrle_level = 2; + } else { + zywrle_level = 1; + } + } else { + zywrle_level = 0; + } + + vnc_zrle_start(vs); + + switch (vs->client_pf.bytes_per_pixel) { + case 1: + zrle_encode_8ne(vs, x, y, w, h, zywrle_level); + break; + + case 2: + if (vs->client_pf.gmax > 0x1F) { + if (be) { + zrle_encode_16be(vs, x, y, w, h, zywrle_level); + } else { + zrle_encode_16le(vs, x, y, w, h, zywrle_level); + } + } else { + if (be) { + zrle_encode_15be(vs, x, y, w, h, zywrle_level); + } else { + zrle_encode_15le(vs, x, y, w, h, zywrle_level); + } + } + break; + + case 4: + { + bool fits_in_ls3bytes; + bool fits_in_ms3bytes; + + fits_in_ls3bytes = + ((vs->client_pf.rmax << vs->client_pf.rshift) < (1 << 24) && + (vs->client_pf.gmax << vs->client_pf.gshift) < (1 << 24) && + (vs->client_pf.bmax << vs->client_pf.bshift) < (1 << 24)); + + fits_in_ms3bytes = (vs->client_pf.rshift > 7 && + vs->client_pf.gshift > 7 && + vs->client_pf.bshift > 7); + + if ((fits_in_ls3bytes && !be) || (fits_in_ms3bytes && be)) { + if (be) { + zrle_encode_24abe(vs, x, y, w, h, zywrle_level); + } else { + zrle_encode_24ale(vs, x, y, w, h, zywrle_level); + } + } else if ((fits_in_ls3bytes && be) || (fits_in_ms3bytes && !be)) { + if (be) { + zrle_encode_24bbe(vs, x, y, w, h, zywrle_level); + } else { + zrle_encode_24ble(vs, x, y, w, h, zywrle_level); + } + } else { + if (be) { + zrle_encode_32be(vs, x, y, w, h, zywrle_level); + } else { + zrle_encode_32le(vs, x, y, w, h, zywrle_level); + } + } + } + break; + } + + vnc_zrle_stop(vs); + bytes = zrle_compress_data(vs, Z_DEFAULT_COMPRESSION); + vnc_framebuffer_update(vs, x, y, w, h, vs->zrle->type); + vnc_write_u32(vs, bytes); + vnc_write(vs, vs->zrle->zlib.buffer, vs->zrle->zlib.offset); + return 1; +} + +int vnc_zrle_send_framebuffer_update(VncState *vs, int x, int y, int w, int h) +{ + vs->zrle->type = VNC_ENCODING_ZRLE; + return zrle_send_framebuffer_update(vs, x, y, w, h); +} + +int vnc_zywrle_send_framebuffer_update(VncState *vs, int x, int y, int w, int h) +{ + vs->zrle->type = VNC_ENCODING_ZYWRLE; + return zrle_send_framebuffer_update(vs, x, y, w, h); +} + +void vnc_zrle_clear(VncState *vs) +{ + if (vs->zrle->stream.opaque) { + deflateEnd(&vs->zrle->stream); + } + buffer_free(&vs->zrle->zrle); + buffer_free(&vs->zrle->fb); + buffer_free(&vs->zrle->zlib); +} diff --git a/ui/vnc-enc-zrle.c.inc b/ui/vnc-enc-zrle.c.inc new file mode 100644 index 000000000..c107d8aff --- /dev/null +++ b/ui/vnc-enc-zrle.c.inc @@ -0,0 +1,263 @@ +/* + * QEMU VNC display driver: Zlib Run-length Encoding (ZRLE) + * + * From libvncserver/libvncserver/zrleencodetemplate.c + * Copyright (C) 2002 RealVNC Ltd. All Rights Reserved. + * Copyright (C) 2003 Sun Microsystems, Inc. + * + * Copyright (C) 2010 Corentin Chary <corentin.chary@gmail.com> + * + * This work is licensed under the terms of the GNU GPL, version 2 or later. + * See the COPYING file in the top-level directory. + */ + +/* + * Before including this file, you must define a number of CPP macros. + * + * ZRLE_BPP should be 8, 16 or 32 depending on the bits per pixel. + * + * Note that the buf argument to ZRLE_ENCODE needs to be at least one pixel + * bigger than the largest tile of pixel data, since the ZRLE encoding + * algorithm writes to the position one past the end of the pixel data. + */ + + +#include "qemu/osdep.h" + +#undef ZRLE_ENDIAN_SUFFIX + +#if ZYWRLE_ENDIAN == ENDIAN_LITTLE +#define ZRLE_ENDIAN_SUFFIX le +#elif ZYWRLE_ENDIAN == ENDIAN_BIG +#define ZRLE_ENDIAN_SUFFIX be +#else +#define ZRLE_ENDIAN_SUFFIX ne +#endif + +#ifndef ZRLE_CONCAT +#define ZRLE_CONCAT_I(a, b) a##b +#define ZRLE_CONCAT2(a, b) ZRLE_CONCAT_I(a, b) +#define ZRLE_CONCAT3(a, b, c) ZRLE_CONCAT2(a, ZRLE_CONCAT2(b, c)) +#endif + +#ifdef ZRLE_COMPACT_PIXEL +#define ZRLE_ENCODE_SUFFIX ZRLE_CONCAT2(ZRLE_COMPACT_PIXEL,ZRLE_ENDIAN_SUFFIX) +#define ZRLE_WRITE_SUFFIX ZRLE_COMPACT_PIXEL +#define ZRLE_PIXEL ZRLE_CONCAT3(uint,ZRLE_BPP,_t) +#define ZRLE_BPP_OUT 24 +#elif ZRLE_BPP == 15 +#define ZRLE_ENCODE_SUFFIX ZRLE_CONCAT2(ZRLE_BPP,ZRLE_ENDIAN_SUFFIX) +#define ZRLE_WRITE_SUFFIX 16 +#define ZRLE_PIXEL uint16_t +#define ZRLE_BPP_OUT 16 +#else +#define ZRLE_ENCODE_SUFFIX ZRLE_CONCAT2(ZRLE_BPP,ZRLE_ENDIAN_SUFFIX) +#define ZRLE_WRITE_SUFFIX ZRLE_BPP +#define ZRLE_BPP_OUT ZRLE_BPP +#define ZRLE_PIXEL ZRLE_CONCAT3(uint,ZRLE_BPP,_t) +#endif + +#define ZRLE_WRITE_PIXEL ZRLE_CONCAT2(zrle_write_u, ZRLE_WRITE_SUFFIX) +#define ZRLE_ENCODE ZRLE_CONCAT2(zrle_encode_, ZRLE_ENCODE_SUFFIX) +#define ZRLE_ENCODE_TILE ZRLE_CONCAT2(zrle_encode_tile, ZRLE_ENCODE_SUFFIX) +#define ZRLE_WRITE_PALETTE ZRLE_CONCAT2(zrle_write_palette,ZRLE_ENCODE_SUFFIX) + +static void ZRLE_ENCODE_TILE(VncState *vs, ZRLE_PIXEL *data, int w, int h, + int zywrle_level); + +#if ZRLE_BPP != 8 +#include "vnc-enc-zywrle-template.c" +#endif + + +static void ZRLE_ENCODE(VncState *vs, int x, int y, int w, int h, + int zywrle_level) +{ + int ty; + + for (ty = y; ty < y + h; ty += VNC_ZRLE_TILE_HEIGHT) { + + int tx, th; + + th = MIN(VNC_ZRLE_TILE_HEIGHT, y + h - ty); + + for (tx = x; tx < x + w; tx += VNC_ZRLE_TILE_WIDTH) { + int tw; + ZRLE_PIXEL *buf; + + tw = MIN(VNC_ZRLE_TILE_WIDTH, x + w - tx); + + buf = zrle_convert_fb(vs, tx, ty, tw, th, ZRLE_BPP); + ZRLE_ENCODE_TILE(vs, buf, tw, th, zywrle_level); + } + } +} + +static void ZRLE_ENCODE_TILE(VncState *vs, ZRLE_PIXEL *data, int w, int h, + int zywrle_level) +{ + VncPalette *palette = &vs->zrle->palette; + + int runs = 0; + int single_pixels = 0; + + bool use_rle; + bool use_palette; + + int i; + + ZRLE_PIXEL *ptr = data; + ZRLE_PIXEL *end = ptr + h * w; + *end = ~*(end-1); /* one past the end is different so the while loop ends */ + + /* Real limit is 127 but we wan't a way to know if there is more than 127 */ + palette_init(palette, 256, ZRLE_BPP); + + while (ptr < end) { + ZRLE_PIXEL pix = *ptr; + if (*++ptr != pix) { /* FIXME */ + single_pixels++; + } else { + while (*++ptr == pix) ; + runs++; + } + palette_put(palette, pix); + } + + /* Solid tile is a special case */ + + if (palette_size(palette) == 1) { + bool found; + + vnc_write_u8(vs, 1); + ZRLE_WRITE_PIXEL(vs, palette_color(palette, 0, &found)); + return; + } + + zrle_choose_palette_rle(vs, w, h, palette, ZRLE_BPP_OUT, + runs, single_pixels, zywrle_level, + &use_rle, &use_palette); + + if (!use_palette) { + vnc_write_u8(vs, (use_rle ? 128 : 0)); + } else { + uint32_t colors[VNC_PALETTE_MAX_SIZE]; + size_t size = palette_size(palette); + + vnc_write_u8(vs, (use_rle ? 128 : 0) | size); + palette_fill(palette, colors); + + for (i = 0; i < size; i++) { + ZRLE_WRITE_PIXEL(vs, colors[i]); + } + } + + if (use_rle) { + ZRLE_PIXEL *ptr = data; + ZRLE_PIXEL *end = ptr + w * h; + ZRLE_PIXEL *run_start; + ZRLE_PIXEL pix; + + while (ptr < end) { + int len; + int index = 0; + + run_start = ptr; + pix = *ptr++; + + while (*ptr == pix && ptr < end) { + ptr++; + } + + len = ptr - run_start; + + if (use_palette) + index = palette_idx(palette, pix); + + if (len <= 2 && use_palette) { + if (len == 2) { + vnc_write_u8(vs, index); + } + vnc_write_u8(vs, index); + continue; + } + if (use_palette) { + vnc_write_u8(vs, index | 128); + } else { + ZRLE_WRITE_PIXEL(vs, pix); + } + + len -= 1; + + while (len >= 255) { + vnc_write_u8(vs, 255); + len -= 255; + } + + vnc_write_u8(vs, len); + } + } else if (use_palette) { /* no RLE */ + int bppp; + ZRLE_PIXEL *ptr = data; + + /* packed pixels */ + + assert (palette_size(palette) < 17); + + bppp = bits_per_packed_pixel[palette_size(palette)-1]; + + for (i = 0; i < h; i++) { + uint8_t nbits = 0; + uint8_t byte = 0; + + ZRLE_PIXEL *eol = ptr + w; + + while (ptr < eol) { + ZRLE_PIXEL pix = *ptr++; + uint8_t index = palette_idx(palette, pix); + + byte = (byte << bppp) | index; + nbits += bppp; + if (nbits >= 8) { + vnc_write_u8(vs, byte); + nbits = 0; + } + } + if (nbits > 0) { + byte <<= 8 - nbits; + vnc_write_u8(vs, byte); + } + } + } else { + + /* raw */ + +#if ZRLE_BPP != 8 + if (zywrle_level > 0 && !(zywrle_level & 0x80)) { + ZYWRLE_ANALYZE(data, data, w, h, w, zywrle_level, vs->zywrle.buf); + ZRLE_ENCODE_TILE(vs, data, w, h, zywrle_level | 0x80); + } + else +#endif + { +#ifdef ZRLE_COMPACT_PIXEL + ZRLE_PIXEL *ptr; + + for (ptr = data; ptr < data + w * h; ptr++) { + ZRLE_WRITE_PIXEL(vs, *ptr); + } +#else + vnc_write(vs, data, w * h * (ZRLE_BPP / 8)); +#endif + } + } +} + +#undef ZRLE_PIXEL +#undef ZRLE_WRITE_PIXEL +#undef ZRLE_ENCODE +#undef ZRLE_ENCODE_TILE +#undef ZYWRLE_ENCODE_TILE +#undef ZRLE_BPP_OUT +#undef ZRLE_WRITE_SUFFIX +#undef ZRLE_ENCODE_SUFFIX diff --git a/ui/vnc-enc-zrle.h b/ui/vnc-enc-zrle.h new file mode 100644 index 000000000..6535cb2aa --- /dev/null +++ b/ui/vnc-enc-zrle.h @@ -0,0 +1,40 @@ +/* + * QEMU VNC display driver: Zlib Run-length Encoding (ZRLE) + * + * From libvncserver/libvncserver/zrle.c + * Copyright (C) 2002 RealVNC Ltd. All Rights Reserved. + * Copyright (C) 2003 Sun Microsystems, Inc. + * + * Copyright (C) 2010 Corentin Chary <corentin.chary@gmail.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef VNC_ENC_ZRLE_H +#define VNC_ENC_ZRLE_H + +/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * ZRLE - encoding combining Zlib compression, tiling, palettisation and + * run-length encoding. + */ + +#define VNC_ZRLE_TILE_WIDTH 64 +#define VNC_ZRLE_TILE_HEIGHT 64 + +#endif diff --git a/ui/vnc-enc-zywrle-template.c b/ui/vnc-enc-zywrle-template.c new file mode 100644 index 000000000..e9be55966 --- /dev/null +++ b/ui/vnc-enc-zywrle-template.c @@ -0,0 +1,171 @@ + +/******************************************************************** + * * + * THIS FILE IS PART OF THE 'ZYWRLE' VNC CODEC SOURCE CODE. * + * * + * USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS * + * GOVERNED BY A FOLLOWING BSD-STYLE SOURCE LICENSE. * + * PLEASE READ THESE TERMS BEFORE DISTRIBUTING. * + * * + * THE 'ZYWRLE' VNC CODEC SOURCE CODE IS (C) COPYRIGHT 2006 * + * BY Hitachi Systems & Services, Ltd. * + * (Noriaki Yamazaki, Research & Development Center) * + * * + * * + ******************************************************************** +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Hitachi Systems & Services, Ltd. nor +the names of its contributors may be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ********************************************************************/ + +/* Change Log: + V0.02 : 2008/02/04 : Fix mis encode/decode when width != scanline + (Thanks Johannes Schindelin, author of LibVNC + Server/Client) + V0.01 : 2007/02/06 : Initial release +*/ + +/* +[References] + PLHarr: + Senecal, J. G., P. Lindstrom, M. A. Duchaineau, and K. I. Joy, + "An Improved N-Bit to N-Bit Reversible Haar-Like Transform," + Pacific Graphics 2004, October 2004, pp. 371-380. + EZW: + Shapiro, JM: Embedded Image Coding Using Zerotrees of Wavelet Coefficients, + IEEE Trans. Signal. Process., Vol.41, pp.3445-3462 (1993). +*/ + + +/* Template Macro stuffs. */ +#undef ZYWRLE_ANALYZE +#undef ZYWRLE_SYNTHESIZE + +#define ZYWRLE_SUFFIX ZRLE_CONCAT2(ZRLE_BPP,ZRLE_ENDIAN_SUFFIX) + +#define ZYWRLE_ANALYZE ZRLE_CONCAT2(zywrle_analyze_, ZYWRLE_SUFFIX) +#define ZYWRLE_SYNTHESIZE ZRLE_CONCAT2(zywrle_synthesize_,ZYWRLE_SUFFIX) + +#define ZYWRLE_RGBYUV ZRLE_CONCAT2(zywrle_rgbyuv_, ZYWRLE_SUFFIX) +#define ZYWRLE_YUVRGB ZRLE_CONCAT2(zywrle_yuvrgb_, ZYWRLE_SUFFIX) +#define ZYWRLE_YMASK ZRLE_CONCAT2(ZYWRLE_YMASK, ZRLE_BPP) +#define ZYWRLE_UVMASK ZRLE_CONCAT2(ZYWRLE_UVMASK, ZRLE_BPP) +#define ZYWRLE_LOAD_PIXEL ZRLE_CONCAT2(ZYWRLE_LOAD_PIXEL, ZRLE_BPP) +#define ZYWRLE_SAVE_PIXEL ZRLE_CONCAT2(ZYWRLE_SAVE_PIXEL, ZRLE_BPP) + +/* Packing/Unpacking pixel stuffs. + Endian conversion stuffs. */ +#undef S_0 +#undef S_1 +#undef L_0 +#undef L_1 +#undef L_2 + +#if ZYWRLE_ENDIAN == ENDIAN_BIG +# define S_0 1 +# define S_1 0 +# define L_0 3 +# define L_1 2 +# define L_2 1 +#else +# define S_0 0 +# define S_1 1 +# define L_0 0 +# define L_1 1 +# define L_2 2 +#endif + +#define ZYWRLE_QUANTIZE +#include "qemu/osdep.h" +#include "vnc-enc-zywrle.h" + +#ifndef ZRLE_COMPACT_PIXEL +static inline void ZYWRLE_RGBYUV(int *buf, ZRLE_PIXEL *data, + int width, int height, int scanline) +{ + int r, g, b; + int y, u, v; + int *line; + int *end; + + end = buf + height * width; + while (buf < end) { + line = buf + width; + while (buf < line) { + ZYWRLE_LOAD_PIXEL(data, r, g, b); + ZYWRLE_RGBYUV_(r, g, b, y, u, v, ZYWRLE_YMASK, ZYWRLE_UVMASK); + ZYWRLE_SAVE_COEFF(buf, v, y, u); + buf++; + data++; + } + data += scanline - width; + } +} + +static ZRLE_PIXEL *ZYWRLE_ANALYZE(ZRLE_PIXEL *dst, ZRLE_PIXEL *src, + int w, int h, int scanline, int level, + int *buf) { + int l; + int uw = w; + int uh = h; + int *top; + int *end; + int *line; + ZRLE_PIXEL *p; + int r, g, b; + int s; + int *ph; + + zywrle_calc_size(&w, &h, level); + + if (w == 0 || h == 0) { + return NULL; + } + uw -= w; + uh -= h; + + p = dst; + ZYWRLE_LOAD_UNALIGN(src,*(ZRLE_PIXEL*)top = *p;); + ZYWRLE_RGBYUV(buf, src, w, h, scanline); + wavelet(buf, w, h, level); + for (l = 0; l < level; l++) { + ZYWRLE_PACK_COEFF(buf, dst, 3, w, h, scanline, l); + ZYWRLE_PACK_COEFF(buf, dst, 2, w, h, scanline, l); + ZYWRLE_PACK_COEFF(buf, dst, 1, w, h, scanline, l); + if (l == level - 1) { + ZYWRLE_PACK_COEFF(buf, dst, 0, w, h, scanline, l); + } + } + ZYWRLE_SAVE_UNALIGN(dst,*dst = *(ZRLE_PIXEL*)top;); + return dst; +} +#endif /* ZRLE_COMPACT_PIXEL */ + +#undef ZYWRLE_RGBYUV +#undef ZYWRLE_YUVRGB +#undef ZYWRLE_LOAD_PIXEL +#undef ZYWRLE_SAVE_PIXEL diff --git a/ui/vnc-enc-zywrle.h b/ui/vnc-enc-zywrle.h new file mode 100644 index 000000000..9b7f69897 --- /dev/null +++ b/ui/vnc-enc-zywrle.h @@ -0,0 +1,659 @@ +/******************************************************************** + * * + * THIS FILE IS PART OF THE 'ZYWRLE' VNC CODEC SOURCE CODE. * + * * + * USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS * + * GOVERNED BY A FOLLOWING BSD-STYLE SOURCE LICENSE. * + * PLEASE READ THESE TERMS BEFORE DISTRIBUTING. * + * * + * THE 'ZYWRLE' VNC CODEC SOURCE CODE IS (C) COPYRIGHT 2006 * + * BY Hitachi Systems & Services, Ltd. * + * (Noriaki Yamazaki, Research & Development Center) * + * * + * * + ******************************************************************** +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Hitachi Systems & Services, Ltd. nor +the names of its contributors may be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ********************************************************************/ + +#ifndef VNC_ENC_ZYWRLE_H +#define VNC_ENC_ZYWRLE_H + +/* Tables for Coefficients filtering. */ +#ifndef ZYWRLE_QUANTIZE +/* Type A:lower bit omitting of EZW style. */ +static const unsigned int zywrle_param[3][3]={ + {0x0000F000, 0x00000000, 0x00000000}, + {0x0000C000, 0x00F0F0F0, 0x00000000}, + {0x0000C000, 0x00C0C0C0, 0x00F0F0F0}, +/* {0x0000FF00, 0x00000000, 0x00000000}, + {0x0000FF00, 0x00FFFFFF, 0x00000000}, + {0x0000FF00, 0x00FFFFFF, 0x00FFFFFF}, */ +}; +#else +/* Type B:Non liner quantization filter. */ +static const int8_t zywrle_conv[4][256]={ +{ /* bi=5, bo=5 r=0.0:PSNR=24.849 */ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +}, +{ /* bi=5, bo=5 r=2.0:PSNR=74.031 */ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 32, + 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, + 48, 48, 48, 48, 48, 48, 48, 48, + 48, 48, 48, 56, 56, 56, 56, 56, + 56, 56, 56, 56, 64, 64, 64, 64, + 64, 64, 64, 64, 72, 72, 72, 72, + 72, 72, 72, 72, 80, 80, 80, 80, + 80, 80, 88, 88, 88, 88, 88, 88, + 88, 88, 88, 88, 88, 88, 96, 96, + 96, 96, 96, 104, 104, 104, 104, 104, + 104, 104, 104, 104, 104, 112, 112, 112, + 112, 112, 112, 112, 112, 112, 120, 120, + 120, 120, 120, 120, 120, 120, 120, 120, + 0, -120, -120, -120, -120, -120, -120, -120, + -120, -120, -120, -112, -112, -112, -112, -112, + -112, -112, -112, -112, -104, -104, -104, -104, + -104, -104, -104, -104, -104, -104, -96, -96, + -96, -96, -96, -88, -88, -88, -88, -88, + -88, -88, -88, -88, -88, -88, -88, -80, + -80, -80, -80, -80, -80, -72, -72, -72, + -72, -72, -72, -72, -72, -64, -64, -64, + -64, -64, -64, -64, -64, -56, -56, -56, + -56, -56, -56, -56, -56, -56, -48, -48, + -48, -48, -48, -48, -48, -48, -48, -48, + -48, -32, -32, -32, -32, -32, -32, -32, + -32, -32, -32, -32, -32, -32, -32, -32, + -32, -32, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +}, +{ /* bi=5, bo=4 r=2.0:PSNR=64.441 */ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 48, 48, 48, 48, 48, 48, 48, 48, + 48, 48, 48, 48, 48, 48, 48, 48, + 48, 48, 48, 48, 48, 48, 48, 48, + 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, + 80, 80, 80, 80, 80, 80, 80, 80, + 80, 80, 80, 80, 80, 88, 88, 88, + 88, 88, 88, 88, 88, 88, 88, 88, + 104, 104, 104, 104, 104, 104, 104, 104, + 104, 104, 104, 112, 112, 112, 112, 112, + 112, 112, 112, 112, 120, 120, 120, 120, + 120, 120, 120, 120, 120, 120, 120, 120, + 0, -120, -120, -120, -120, -120, -120, -120, + -120, -120, -120, -120, -120, -112, -112, -112, + -112, -112, -112, -112, -112, -112, -104, -104, + -104, -104, -104, -104, -104, -104, -104, -104, + -104, -88, -88, -88, -88, -88, -88, -88, + -88, -88, -88, -88, -80, -80, -80, -80, + -80, -80, -80, -80, -80, -80, -80, -80, + -80, -64, -64, -64, -64, -64, -64, -64, + -64, -64, -64, -64, -64, -64, -64, -64, + -64, -48, -48, -48, -48, -48, -48, -48, + -48, -48, -48, -48, -48, -48, -48, -48, + -48, -48, -48, -48, -48, -48, -48, -48, + -48, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +}, +{ /* bi=5, bo=2 r=2.0:PSNR=43.175 */ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 88, 88, 88, 88, 88, 88, 88, 88, + 88, 88, 88, 88, 88, 88, 88, 88, + 88, 88, 88, 88, 88, 88, 88, 88, + 88, 88, 88, 88, 88, 88, 88, 88, + 88, 88, 88, 88, 88, 88, 88, 88, + 88, 88, 88, 88, 88, 88, 88, 88, + 88, 88, 88, 88, 88, 88, 88, 88, + 88, 88, 88, 88, 88, 88, 88, 88, + 0, -88, -88, -88, -88, -88, -88, -88, + -88, -88, -88, -88, -88, -88, -88, -88, + -88, -88, -88, -88, -88, -88, -88, -88, + -88, -88, -88, -88, -88, -88, -88, -88, + -88, -88, -88, -88, -88, -88, -88, -88, + -88, -88, -88, -88, -88, -88, -88, -88, + -88, -88, -88, -88, -88, -88, -88, -88, + -88, -88, -88, -88, -88, -88, -88, -88, + -88, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +} +}; + +static const int8_t *zywrle_param[3][3][3]={ + {{zywrle_conv[0], zywrle_conv[2], zywrle_conv[0]}, + {zywrle_conv[0], zywrle_conv[0], zywrle_conv[0]}, + {zywrle_conv[0], zywrle_conv[0], zywrle_conv[0]}}, + {{zywrle_conv[0], zywrle_conv[3], zywrle_conv[0]}, + {zywrle_conv[1], zywrle_conv[1], zywrle_conv[1]}, + {zywrle_conv[0], zywrle_conv[0], zywrle_conv[0]}}, + {{zywrle_conv[0], zywrle_conv[3], zywrle_conv[0]}, + {zywrle_conv[2], zywrle_conv[2], zywrle_conv[2]}, + {zywrle_conv[1], zywrle_conv[1], zywrle_conv[1]}}, +}; +#endif + +/* Load/Save pixel stuffs. */ +#define ZYWRLE_YMASK15 0xFFFFFFF8 +#define ZYWRLE_UVMASK15 0xFFFFFFF8 +#define ZYWRLE_LOAD_PIXEL15(src, r, g, b) \ + do { \ + r = (((uint8_t*)src)[S_1]<< 1)& 0xF8; \ + g = (((uint8_t*)src)[S_1]<< 6) | (((uint8_t*)src)[S_0]>> 2); \ + g &= 0xF8; \ + b = (((uint8_t*)src)[S_0]<< 3)& 0xF8; \ + } while (0) + +#define ZYWRLE_SAVE_PIXEL15(dst, r, g, b) \ + do { \ + r &= 0xF8; \ + g &= 0xF8; \ + b &= 0xF8; \ + ((uint8_t*)dst)[S_1] = (uint8_t)((r >> 1)|(g >> 6)); \ + ((uint8_t*)dst)[S_0] = (uint8_t)(((b >> 3)|(g << 2))& 0xFF); \ + } while (0) + +#define ZYWRLE_YMASK16 0xFFFFFFFC +#define ZYWRLE_UVMASK16 0xFFFFFFF8 +#define ZYWRLE_LOAD_PIXEL16(src, r, g, b) \ + do { \ + r = ((uint8_t*)src)[S_1] & 0xF8; \ + g = (((uint8_t*)src)[S_1]<< 5) | (((uint8_t*)src)[S_0] >> 3); \ + g &= 0xFC; \ + b = (((uint8_t*)src)[S_0]<< 3) & 0xF8; \ + } while (0) + +#define ZYWRLE_SAVE_PIXEL16(dst, r, g,b) \ + do { \ + r &= 0xF8; \ + g &= 0xFC; \ + b &= 0xF8; \ + ((uint8_t*)dst)[S_1] = (uint8_t)(r | (g >> 5)); \ + ((uint8_t*)dst)[S_0] = (uint8_t)(((b >> 3)|(g << 3)) & 0xFF); \ + } while (0) + +#define ZYWRLE_YMASK32 0xFFFFFFFF +#define ZYWRLE_UVMASK32 0xFFFFFFFF +#define ZYWRLE_LOAD_PIXEL32(src, r, g, b) \ + do { \ + r = ((uint8_t*)src)[L_2]; \ + g = ((uint8_t*)src)[L_1]; \ + b = ((uint8_t*)src)[L_0]; \ + } while (0) +#define ZYWRLE_SAVE_PIXEL32(dst, r, g, b) \ + do { \ + ((uint8_t*)dst)[L_2] = (uint8_t)r; \ + ((uint8_t*)dst)[L_1] = (uint8_t)g; \ + ((uint8_t*)dst)[L_0] = (uint8_t)b; \ + } while (0) + +static inline void harr(int8_t *px0, int8_t *px1) +{ + /* Piecewise-Linear Harr(PLHarr) */ + int x0 = (int)*px0, x1 = (int)*px1; + int orgx0 = x0, orgx1 = x1; + + if ((x0 ^ x1) & 0x80) { + /* differ sign */ + x1 += x0; + if (((x1 ^ orgx1) & 0x80) == 0) { + /* |x1| > |x0| */ + x0 -= x1; /* H = -B */ + } + } else { + /* same sign */ + x0 -= x1; + if (((x0 ^ orgx0) & 0x80) == 0) { + /* |x0| > |x1| */ + x1 += x0; /* L = A */ + } + } + *px0 = (int8_t)x1; + *px1 = (int8_t)x0; +} + +/* + 1D-Wavelet transform. + + In coefficients array, the famous 'pyramid' decomposition is well used. + + 1D Model: + |L0L0L0L0|L0L0L0L0|H0H0H0H0|H0H0H0H0| : level 0 + |L1L1L1L1|H1H1H1H1|H0H0H0H0|H0H0H0H0| : level 1 + + But this method needs line buffer because H/L is different position from X0/X1. + So, I used 'interleave' decomposition instead of it. + + 1D Model: + |L0H0L0H0|L0H0L0H0|L0H0L0H0|L0H0L0H0| : level 0 + |L1H0H1H0|L1H0H1H0|L1H0H1H0|L1H0H1H0| : level 1 + + In this method, H/L and X0/X1 is always same position. + This leads us to more speed and less memory. + Of cause, the result of both method is quite same + because it's only difference that coefficient position. +*/ +static inline void wavelet_level(int *data, int size, int l, int skip_pixel) +{ + int s, ofs; + int8_t *px0; + int8_t *end; + + px0 = (int8_t*)data; + s = (8 << l) * skip_pixel; + end = px0 + (size >> (l + 1)) * s; + s -= 2; + ofs = (4 << l) * skip_pixel; + + while (px0 < end) { + harr(px0, px0 + ofs); + px0++; + harr(px0, px0 + ofs); + px0++; + harr(px0, px0 + ofs); + px0 += s; + } +} + +#ifndef ZYWRLE_QUANTIZE +/* Type A:lower bit omitting of EZW style. */ +static inline void filter_wavelet_square(int *buf, int width, int height, + int level, int l) +{ + int r, s; + int x, y; + int *h; + const unsigned int *m; + + m = &(zywrle_param[level - 1][l]); + s = 2 << l; + + for (r = 1; r < 4; r++) { + h = buf; + if (r & 0x01) { + h += s >> 1; + } + if (r & 0x02) { + h += (s >> 1) * width; + } + for (y = 0; y < height / s; y++) { + for (x = 0; x < width / s; x++) { + /* + these are same following code. + h[x] = h[x] / (~m[x]+1) * (~m[x]+1); + ( round h[x] with m[x] bit ) + '&' operator isn't 'round' but is 'floor'. + So, we must offset when h[x] is negative. + */ + if (((int8_t*)h)[0] & 0x80) { + ((int8_t*)h)[0] += ~((int8_t*)m)[0]; + } + if (((int8_t*)h)[1] & 0x80) { + ((int8_t*)h)[1] += ~((int8_t*)m)[1]; + } + if (((int8_t*)h)[2] & 0x80) { + ((int8_t*)h)[2] += ~((int8_t*)m)[2]; + } + *h &= *m; + h += s; + } + h += (s-1)*width; + } + } +} +#else +/* + Type B:Non liner quantization filter. + + Coefficients have Gaussian curve and smaller value which is + large part of coefficients isn't more important than larger value. + So, I use filter of Non liner quantize/dequantize table. + In general, Non liner quantize formula is explained as following. + + y=f(x) = sign(x)*round( ((abs(x)/(2^7))^ r )* 2^(bo-1) )*2^(8-bo) + x=f-1(y) = sign(y)*round( ((abs(y)/(2^7))^(1/r))* 2^(bi-1) )*2^(8-bi) + ( r:power coefficient bi:effective MSB in input bo:effective MSB in output ) + + r < 1.0 : Smaller value is more important than larger value. + r > 1.0 : Larger value is more important than smaller value. + r = 1.0 : Liner quantization which is same with EZW style. + + r = 0.75 is famous non liner quantization used in MP3 audio codec. + In contrast to audio data, larger value is important in wavelet coefficients. + So, I select r = 2.0 table( quantize is x^2, dequantize sqrt(x) ). + + As compared with EZW style liner quantization, this filter tended to be + more sharp edge and be more compression rate but be more blocking noise and be + less quality. Especially, the surface of graphic objects has distinguishable + noise in middle quality mode. + + We need only quantized-dequantized(filtered) value rather than quantized value + itself because all values are packed or palette-lized in later ZRLE section. + This lead us not to need to modify client decoder when we change + the filtering procedure in future. + Client only decodes coefficients given by encoder. +*/ +static inline void filter_wavelet_square(int *buf, int width, int height, + int level, int l) +{ + int r, s; + int x, y; + int *h; + const int8_t **m; + + m = zywrle_param[level - 1][l]; + s = 2 << l; + + for (r = 1; r < 4; r++) { + h = buf; + if (r & 0x01) { + h += s >> 1; + } + if (r & 0x02) { + h += (s >> 1) * width; + } + for (y = 0; y < height / s; y++) { + for (x = 0; x < width / s; x++) { + ((int8_t*)h)[0] = m[0][((uint8_t*)h)[0]]; + ((int8_t*)h)[1] = m[1][((uint8_t*)h)[1]]; + ((int8_t*)h)[2] = m[2][((uint8_t*)h)[2]]; + h += s; + } + h += (s - 1) * width; + } + } +} +#endif + +static inline void wavelet(int *buf, int width, int height, int level) +{ + int l, s; + int *top; + int *end; + + for (l = 0; l < level; l++) { + top = buf; + end = buf + height * width; + s = width << l; + while (top < end) { + wavelet_level(top, width, l, 1); + top += s; + } + top = buf; + end = buf + width; + s = 1<<l; + while (top < end) { + wavelet_level(top, height, l, width); + top += s; + } + filter_wavelet_square(buf, width, height, level, l); + } +} + + +/* Load/Save coefficients stuffs. + Coefficients manages as 24 bits little-endian pixel. */ +#define ZYWRLE_LOAD_COEFF(src, r, g, b) \ + do { \ + r = ((int8_t*)src)[2]; \ + g = ((int8_t*)src)[1]; \ + b = ((int8_t*)src)[0]; \ + } while (0) + +#define ZYWRLE_SAVE_COEFF(dst, r, g, b) \ + do { \ + ((int8_t*)dst)[2] = (int8_t)r; \ + ((int8_t*)dst)[1] = (int8_t)g; \ + ((int8_t*)dst)[0] = (int8_t)b; \ + } while (0) + +/* + RGB <=> YUV conversion stuffs. + YUV coversion is explained as following formula in strict meaning: + Y = 0.299R + 0.587G + 0.114B ( 0<=Y<=255) + U = -0.169R - 0.331G + 0.500B (-128<=U<=127) + V = 0.500R - 0.419G - 0.081B (-128<=V<=127) + + I use simple conversion RCT(reversible color transform) which is described + in JPEG-2000 specification. + Y = (R + 2G + B)/4 ( 0<=Y<=255) + U = B-G (-256<=U<=255) + V = R-G (-256<=V<=255) +*/ + +/* RCT is N-bit RGB to N-bit Y and N+1-bit UV. + For make Same N-bit, UV is lossy. + More exact PLHarr, we reduce to odd range(-127<=x<=127). */ +#define ZYWRLE_RGBYUV_(r, g, b, y, u, v, ymask, uvmask) \ + do { \ + y = (r + (g << 1) + b) >> 2; \ + u = b - g; \ + v = r - g; \ + y -= 128; \ + u >>= 1; \ + v >>= 1; \ + y &= ymask; \ + u &= uvmask; \ + v &= uvmask; \ + if (y == -128) { \ + y += (0xFFFFFFFF - ymask + 1); \ + } \ + if (u == -128) { \ + u += (0xFFFFFFFF - uvmask + 1); \ + } \ + if (v == -128) { \ + v += (0xFFFFFFFF - uvmask + 1); \ + } \ + } while (0) + + +/* + coefficient packing/unpacking stuffs. + Wavelet transform makes 4 sub coefficient image from 1 original image. + + model with pyramid decomposition: + +------+------+ + | | | + | L | Hx | + | | | + +------+------+ + | | | + | H | Hxy | + | | | + +------+------+ + + So, we must transfer each sub images individually in strict meaning. + But at least ZRLE meaning, following one decompositon image is same as + avobe individual sub image. I use this format. + (Strictly saying, transfer order is reverse(Hxy->Hy->Hx->L) + for simplified procedure for any wavelet level.) + + +------+------+ + | L | + +------+------+ + | Hx | + +------+------+ + | Hy | + +------+------+ + | Hxy | + +------+------+ +*/ +#define ZYWRLE_INC_PTR(data) \ + do { \ + data++; \ + if( data - p >= (w + uw) ) { \ + data += scanline-(w + uw); \ + p = data; \ + } \ + } while (0) + +#define ZYWRLE_TRANSFER_COEFF(buf, data, t, w, h, scanline, level, TRANS) \ + do { \ + ph = buf; \ + s = 2 << level; \ + if (t & 0x01) { \ + ph += s >> 1; \ + } \ + if (t & 0x02) { \ + ph += (s >> 1) * w; \ + } \ + end = ph + h * w; \ + while (ph < end) { \ + line = ph + w; \ + while (ph < line) { \ + TRANS \ + ZYWRLE_INC_PTR(data); \ + ph += s; \ + } \ + ph += (s - 1) * w; \ + } \ + } while (0) + +#define ZYWRLE_PACK_COEFF(buf, data, t, width, height, scanline, level) \ + ZYWRLE_TRANSFER_COEFF(buf, data, t, width, height, scanline, level, \ + ZYWRLE_LOAD_COEFF(ph, r, g, b); \ + ZYWRLE_SAVE_PIXEL(data, r, g, b);) + +#define ZYWRLE_UNPACK_COEFF(buf, data, t, width, height, scanline, level) \ + ZYWRLE_TRANSFER_COEFF(buf, data, t, width, height, scanline, level, \ + ZYWRLE_LOAD_PIXEL(data, r, g, b); \ + ZYWRLE_SAVE_COEFF(ph, r, g, b);) + +#define ZYWRLE_SAVE_UNALIGN(data, TRANS) \ + do { \ + top = buf + w * h; \ + end = buf + (w + uw) * (h + uh); \ + while (top < end) { \ + TRANS \ + ZYWRLE_INC_PTR(data); \ + top++; \ + } \ + } while (0) + +#define ZYWRLE_LOAD_UNALIGN(data,TRANS) \ + do { \ + top = buf + w * h; \ + if (uw) { \ + p = data + w; \ + end = (int*)(p + h * scanline); \ + while (p < (ZRLE_PIXEL*)end) { \ + line = (int*)(p + uw); \ + while (p < (ZRLE_PIXEL*)line) { \ + TRANS \ + p++; \ + top++; \ + } \ + p += scanline - uw; \ + } \ + } \ + if (uh) { \ + p = data + h * scanline; \ + end = (int*)(p + uh * scanline); \ + while (p < (ZRLE_PIXEL*)end) { \ + line = (int*)(p + w); \ + while (p < (ZRLE_PIXEL*)line) { \ + TRANS \ + p++; \ + top++; \ + } \ + p += scanline - w; \ + } \ + } \ + if (uw && uh) { \ + p= data + w + h * scanline; \ + end = (int*)(p + uh * scanline); \ + while (p < (ZRLE_PIXEL*)end) { \ + line = (int*)(p + uw); \ + while (p < (ZRLE_PIXEL*)line) { \ + TRANS \ + p++; \ + top++; \ + } \ + p += scanline-uw; \ + } \ + } \ + } while (0) + +static inline void zywrle_calc_size(int *w, int *h, int level) +{ + *w &= ~((1 << level) - 1); + *h &= ~((1 << level) - 1); +} + +#endif diff --git a/ui/vnc-jobs.c b/ui/vnc-jobs.c new file mode 100644 index 000000000..4562bf892 --- /dev/null +++ b/ui/vnc-jobs.c @@ -0,0 +1,382 @@ +/* + * QEMU VNC display driver + * + * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws> + * Copyright (C) 2006 Fabrice Bellard + * Copyright (C) 2009 Red Hat, Inc + * Copyright (C) 2010 Corentin Chary <corentin.chary@gmail.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + +#include "qemu/osdep.h" +#include "vnc.h" +#include "vnc-jobs.h" +#include "qemu/sockets.h" +#include "qemu/main-loop.h" +#include "block/aio.h" +#include "trace.h" + +/* + * Locking: + * + * There are three levels of locking: + * - jobs queue lock: for each operation on the queue (push, pop, isEmpty?) + * - VncDisplay global lock: mainly used for framebuffer updates to avoid + * screen corruption if the framebuffer is updated + * while the worker is doing something. + * - VncState::output lock: used to make sure the output buffer is not corrupted + * if two threads try to write on it at the same time + * + * While the VNC worker thread is working, the VncDisplay global lock is held + * to avoid screen corruption (this does not block vnc_refresh() because it + * uses trylock()) but the output lock is not held because the thread works on + * its own output buffer. + * When the encoding job is done, the worker thread will hold the output lock + * and copy its output buffer in vs->output. + */ + +struct VncJobQueue { + QemuCond cond; + QemuMutex mutex; + QemuThread thread; + bool exit; + QTAILQ_HEAD(, VncJob) jobs; +}; + +typedef struct VncJobQueue VncJobQueue; + +/* + * We use a single global queue, but most of the functions are + * already reentrant, so we can easily add more than one encoding thread + */ +static VncJobQueue *queue; + +static void vnc_lock_queue(VncJobQueue *queue) +{ + qemu_mutex_lock(&queue->mutex); +} + +static void vnc_unlock_queue(VncJobQueue *queue) +{ + qemu_mutex_unlock(&queue->mutex); +} + +VncJob *vnc_job_new(VncState *vs) +{ + VncJob *job = g_new0(VncJob, 1); + + assert(vs->magic == VNC_MAGIC); + job->vs = vs; + vnc_lock_queue(queue); + QLIST_INIT(&job->rectangles); + vnc_unlock_queue(queue); + return job; +} + +int vnc_job_add_rect(VncJob *job, int x, int y, int w, int h) +{ + VncRectEntry *entry = g_new0(VncRectEntry, 1); + + trace_vnc_job_add_rect(job->vs, job, x, y, w, h); + + entry->rect.x = x; + entry->rect.y = y; + entry->rect.w = w; + entry->rect.h = h; + + vnc_lock_queue(queue); + QLIST_INSERT_HEAD(&job->rectangles, entry, next); + vnc_unlock_queue(queue); + return 1; +} + +void vnc_job_push(VncJob *job) +{ + vnc_lock_queue(queue); + if (queue->exit || QLIST_EMPTY(&job->rectangles)) { + g_free(job); + } else { + QTAILQ_INSERT_TAIL(&queue->jobs, job, next); + qemu_cond_broadcast(&queue->cond); + } + vnc_unlock_queue(queue); +} + +static bool vnc_has_job_locked(VncState *vs) +{ + VncJob *job; + + QTAILQ_FOREACH(job, &queue->jobs, next) { + if (job->vs == vs || !vs) { + return true; + } + } + return false; +} + +void vnc_jobs_join(VncState *vs) +{ + vnc_lock_queue(queue); + while (vnc_has_job_locked(vs)) { + qemu_cond_wait(&queue->cond, &queue->mutex); + } + vnc_unlock_queue(queue); + vnc_jobs_consume_buffer(vs); +} + +void vnc_jobs_consume_buffer(VncState *vs) +{ + bool flush; + + vnc_lock_output(vs); + if (vs->jobs_buffer.offset) { + if (vs->ioc != NULL && buffer_empty(&vs->output)) { + if (vs->ioc_tag) { + g_source_remove(vs->ioc_tag); + } + if (vs->disconnecting == FALSE) { + vs->ioc_tag = qio_channel_add_watch( + vs->ioc, G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_OUT, + vnc_client_io, vs, NULL); + } + } + buffer_move(&vs->output, &vs->jobs_buffer); + + if (vs->job_update == VNC_STATE_UPDATE_FORCE) { + vs->force_update_offset = vs->output.offset; + } + vs->job_update = VNC_STATE_UPDATE_NONE; + } + flush = vs->ioc != NULL && vs->abort != true; + vnc_unlock_output(vs); + + if (flush) { + vnc_flush(vs); + } +} + +/* + * Copy data for local use + */ +static void vnc_async_encoding_start(VncState *orig, VncState *local) +{ + buffer_init(&local->output, "vnc-worker-output"); + local->sioc = NULL; /* Don't do any network work on this thread */ + local->ioc = NULL; /* Don't do any network work on this thread */ + + local->vnc_encoding = orig->vnc_encoding; + local->features = orig->features; + local->vd = orig->vd; + local->lossy_rect = orig->lossy_rect; + local->write_pixels = orig->write_pixels; + local->client_pf = orig->client_pf; + local->client_be = orig->client_be; + local->tight = orig->tight; + local->zlib = orig->zlib; + local->hextile = orig->hextile; + local->zrle = orig->zrle; + local->client_width = orig->client_width; + local->client_height = orig->client_height; +} + +static void vnc_async_encoding_end(VncState *orig, VncState *local) +{ + buffer_free(&local->output); + orig->tight = local->tight; + orig->zlib = local->zlib; + orig->hextile = local->hextile; + orig->zrle = local->zrle; + orig->lossy_rect = local->lossy_rect; +} + +static bool vnc_worker_clamp_rect(VncState *vs, VncJob *job, VncRect *rect) +{ + trace_vnc_job_clamp_rect(vs, job, rect->x, rect->y, rect->w, rect->h); + + if (rect->x >= vs->client_width) { + goto discard; + } + rect->w = MIN(vs->client_width - rect->x, rect->w); + if (rect->w == 0) { + goto discard; + } + + if (rect->y >= vs->client_height) { + goto discard; + } + rect->h = MIN(vs->client_height - rect->y, rect->h); + if (rect->h == 0) { + goto discard; + } + + trace_vnc_job_clamped_rect(vs, job, rect->x, rect->y, rect->w, rect->h); + return true; + + discard: + trace_vnc_job_discard_rect(vs, job, rect->x, rect->y, rect->w, rect->h); + return false; +} + +static int vnc_worker_thread_loop(VncJobQueue *queue) +{ + VncJob *job; + VncRectEntry *entry, *tmp; + VncState vs = {}; + int n_rectangles; + int saved_offset; + + vnc_lock_queue(queue); + while (QTAILQ_EMPTY(&queue->jobs) && !queue->exit) { + qemu_cond_wait(&queue->cond, &queue->mutex); + } + /* Here job can only be NULL if queue->exit is true */ + job = QTAILQ_FIRST(&queue->jobs); + vnc_unlock_queue(queue); + assert(job->vs->magic == VNC_MAGIC); + + if (queue->exit) { + return -1; + } + + vnc_lock_output(job->vs); + if (job->vs->ioc == NULL || job->vs->abort == true) { + vnc_unlock_output(job->vs); + goto disconnected; + } + if (buffer_empty(&job->vs->output)) { + /* + * Looks like a NOP as it obviously moves no data. But it + * moves the empty buffer, so we don't have to malloc a new + * one for vs.output + */ + buffer_move_empty(&vs.output, &job->vs->output); + } + vnc_unlock_output(job->vs); + + /* Make a local copy of vs and switch output buffers */ + vnc_async_encoding_start(job->vs, &vs); + vs.magic = VNC_MAGIC; + + /* Start sending rectangles */ + n_rectangles = 0; + vnc_write_u8(&vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE); + vnc_write_u8(&vs, 0); + saved_offset = vs.output.offset; + vnc_write_u16(&vs, 0); + + vnc_lock_display(job->vs->vd); + QLIST_FOREACH_SAFE(entry, &job->rectangles, next, tmp) { + int n; + + if (job->vs->ioc == NULL) { + vnc_unlock_display(job->vs->vd); + /* Copy persistent encoding data */ + vnc_async_encoding_end(job->vs, &vs); + goto disconnected; + } + + if (vnc_worker_clamp_rect(&vs, job, &entry->rect)) { + n = vnc_send_framebuffer_update(&vs, entry->rect.x, entry->rect.y, + entry->rect.w, entry->rect.h); + + if (n >= 0) { + n_rectangles += n; + } + } + g_free(entry); + } + trace_vnc_job_nrects(&vs, job, n_rectangles); + vnc_unlock_display(job->vs->vd); + + /* Put n_rectangles at the beginning of the message */ + vs.output.buffer[saved_offset] = (n_rectangles >> 8) & 0xFF; + vs.output.buffer[saved_offset + 1] = n_rectangles & 0xFF; + + vnc_lock_output(job->vs); + if (job->vs->ioc != NULL) { + buffer_move(&job->vs->jobs_buffer, &vs.output); + /* Copy persistent encoding data */ + vnc_async_encoding_end(job->vs, &vs); + + qemu_bh_schedule(job->vs->bh); + } else { + buffer_reset(&vs.output); + /* Copy persistent encoding data */ + vnc_async_encoding_end(job->vs, &vs); + } + vnc_unlock_output(job->vs); + +disconnected: + vnc_lock_queue(queue); + QTAILQ_REMOVE(&queue->jobs, job, next); + vnc_unlock_queue(queue); + qemu_cond_broadcast(&queue->cond); + g_free(job); + vs.magic = 0; + return 0; +} + +static VncJobQueue *vnc_queue_init(void) +{ + VncJobQueue *queue = g_new0(VncJobQueue, 1); + + qemu_cond_init(&queue->cond); + qemu_mutex_init(&queue->mutex); + QTAILQ_INIT(&queue->jobs); + return queue; +} + +static void vnc_queue_clear(VncJobQueue *q) +{ + qemu_cond_destroy(&queue->cond); + qemu_mutex_destroy(&queue->mutex); + g_free(q); + queue = NULL; /* Unset global queue */ +} + +static void *vnc_worker_thread(void *arg) +{ + VncJobQueue *queue = arg; + + qemu_thread_get_self(&queue->thread); + + while (!vnc_worker_thread_loop(queue)) ; + vnc_queue_clear(queue); + return NULL; +} + +static bool vnc_worker_thread_running(void) +{ + return queue; /* Check global queue */ +} + +void vnc_start_worker_thread(void) +{ + VncJobQueue *q; + + if (vnc_worker_thread_running()) + return ; + + q = vnc_queue_init(); + qemu_thread_create(&q->thread, "vnc_worker", vnc_worker_thread, q, + QEMU_THREAD_DETACHED); + queue = q; /* Set global queue */ +} diff --git a/ui/vnc-jobs.h b/ui/vnc-jobs.h new file mode 100644 index 000000000..59f66bcc3 --- /dev/null +++ b/ui/vnc-jobs.h @@ -0,0 +1,68 @@ +/* + * QEMU VNC display driver + * + * From libvncserver/rfb/rfbproto.h + * Copyright (C) 2005 Rohit Kumar, Johannes E. Schindelin + * Copyright (C) 2000-2002 Constantin Kaplinsky. All Rights Reserved. + * Copyright (C) 2000 Tridia Corporation. All Rights Reserved. + * Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef VNC_JOBS_H +#define VNC_JOBS_H + +/* Jobs */ +VncJob *vnc_job_new(VncState *vs); +int vnc_job_add_rect(VncJob *job, int x, int y, int w, int h); +void vnc_job_push(VncJob *job); +void vnc_jobs_join(VncState *vs); + +void vnc_jobs_consume_buffer(VncState *vs); +void vnc_start_worker_thread(void); + +/* Locks */ +static inline int vnc_trylock_display(VncDisplay *vd) +{ + return qemu_mutex_trylock(&vd->mutex); +} + +static inline void vnc_lock_display(VncDisplay *vd) +{ + qemu_mutex_lock(&vd->mutex); +} + +static inline void vnc_unlock_display(VncDisplay *vd) +{ + qemu_mutex_unlock(&vd->mutex); +} + +static inline void vnc_lock_output(VncState *vs) +{ + qemu_mutex_lock(&vs->output_mutex); +} + +static inline void vnc_unlock_output(VncState *vs) +{ + qemu_mutex_unlock(&vs->output_mutex); +} + +#endif /* VNC_JOBS_H */ diff --git a/ui/vnc-palette.c b/ui/vnc-palette.c new file mode 100644 index 000000000..dc7c0ba99 --- /dev/null +++ b/ui/vnc-palette.c @@ -0,0 +1,159 @@ +/* + * QEMU VNC display driver: palette hash table + * + * From libvncserver/libvncserver/tight.c + * Copyright (C) 2000, 2001 Const Kaplinsky. All Rights Reserved. + * Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. + * + * Copyright (C) 2010 Corentin Chary <corentin.chary@gmail.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "vnc-palette.h" + +static VncPaletteEntry *palette_find(const VncPalette *palette, + uint32_t color, unsigned int hash) +{ + VncPaletteEntry *entry; + + QLIST_FOREACH(entry, &palette->table[hash], next) { + if (entry->color == color) { + return entry; + } + } + + return NULL; +} + +static unsigned int palette_hash(uint32_t rgb, int bpp) +{ + if (bpp == 16) { + return ((unsigned int)(((rgb >> 8) + rgb) & 0xFF)); + } else { + return ((unsigned int)(((rgb >> 16) + (rgb >> 8)) & 0xFF)); + } +} + +VncPalette *palette_new(size_t max, int bpp) +{ + VncPalette *palette; + + palette = g_malloc0(sizeof(*palette)); + palette_init(palette, max, bpp); + return palette; +} + +void palette_init(VncPalette *palette, size_t max, int bpp) +{ + memset(palette, 0, sizeof (*palette)); + palette->max = max; + palette->bpp = bpp; +} + +void palette_destroy(VncPalette *palette) +{ + g_free(palette); +} + +int palette_put(VncPalette *palette, uint32_t color) +{ + unsigned int hash; + unsigned int idx = palette->size; + VncPaletteEntry *entry; + + hash = palette_hash(color, palette->bpp) % VNC_PALETTE_HASH_SIZE; + entry = palette_find(palette, color, hash); + + if (!entry && palette->size >= palette->max) { + return 0; + } + if (!entry) { + VncPaletteEntry *entry; + + entry = &palette->pool[palette->size]; + entry->color = color; + entry->idx = idx; + QLIST_INSERT_HEAD(&palette->table[hash], entry, next); + palette->size++; + } + return palette->size; +} + +int palette_idx(const VncPalette *palette, uint32_t color) +{ + VncPaletteEntry *entry; + unsigned int hash; + + hash = palette_hash(color, palette->bpp) % VNC_PALETTE_HASH_SIZE; + entry = palette_find(palette, color, hash); + return (entry == NULL ? -1 : entry->idx); +} + +size_t palette_size(const VncPalette *palette) +{ + return palette->size; +} + +void palette_iter(const VncPalette *palette, + void (*iter)(int idx, uint32_t color, void *opaque), + void *opaque) +{ + int i; + VncPaletteEntry *entry; + + for (i = 0; i < VNC_PALETTE_HASH_SIZE; i++) { + QLIST_FOREACH(entry, &palette->table[i], next) { + iter(entry->idx, entry->color, opaque); + } + } +} + +uint32_t palette_color(const VncPalette *palette, int idx, bool *found) +{ + int i; + VncPaletteEntry *entry; + + for (i = 0; i < VNC_PALETTE_HASH_SIZE; i++) { + QLIST_FOREACH(entry, &palette->table[i], next) { + if (entry->idx == idx) { + *found = true; + return entry->color; + } + } + } + + *found = false; + return -1; +} + +static void palette_fill_cb(int idx, uint32_t color, void *opaque) +{ + uint32_t *colors = opaque; + + colors[idx] = color; +} + +size_t palette_fill(const VncPalette *palette, + uint32_t colors[VNC_PALETTE_MAX_SIZE]) +{ + palette_iter(palette, palette_fill_cb, colors); + return palette_size(palette); +} diff --git a/ui/vnc-palette.h b/ui/vnc-palette.h new file mode 100644 index 000000000..e9f0eaf73 --- /dev/null +++ b/ui/vnc-palette.h @@ -0,0 +1,66 @@ +/* + * QEMU VNC display driver: palette hash table + * + * From libvncserver/libvncserver/tight.c + * Copyright (C) 2000, 2001 Const Kaplinsky. All Rights Reserved. + * Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. + * + * Copyright (C) 2010 Corentin Chary <corentin.chary@gmail.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef VNC_PALETTE_H +#define VNC_PALETTE_H + +#include "qemu/queue.h" + +#define VNC_PALETTE_HASH_SIZE 256 +#define VNC_PALETTE_MAX_SIZE 256 + +typedef struct VncPaletteEntry { + int idx; + uint32_t color; + QLIST_ENTRY(VncPaletteEntry) next; +} VncPaletteEntry; + +typedef struct VncPalette { + VncPaletteEntry pool[VNC_PALETTE_MAX_SIZE]; + size_t size; + size_t max; + int bpp; + QLIST_HEAD(,VncPaletteEntry) table[VNC_PALETTE_HASH_SIZE]; +} VncPalette; + +VncPalette *palette_new(size_t max, int bpp); +void palette_init(VncPalette *palette, size_t max, int bpp); +void palette_destroy(VncPalette *palette); + +int palette_put(VncPalette *palette, uint32_t color); +int palette_idx(const VncPalette *palette, uint32_t color); +size_t palette_size(const VncPalette *palette); + +void palette_iter(const VncPalette *palette, + void (*iter)(int idx, uint32_t color, void *opaque), + void *opaque); +uint32_t palette_color(const VncPalette *palette, int idx, bool *found); +size_t palette_fill(const VncPalette *palette, + uint32_t colors[VNC_PALETTE_MAX_SIZE]); + +#endif /* VNC_PALETTE_H */ diff --git a/ui/vnc-stubs.c b/ui/vnc-stubs.c new file mode 100644 index 000000000..b4eb3ce71 --- /dev/null +++ b/ui/vnc-stubs.c @@ -0,0 +1,24 @@ +#include "qemu/osdep.h" +#include "ui/console.h" +#include "qapi/error.h" + +int vnc_display_password(const char *id, const char *password) +{ + return -ENODEV; +} +int vnc_display_pw_expire(const char *id, time_t expires) +{ + return -ENODEV; +}; +void vnc_parse(const char *str) +{ + if (strcmp(str, "none") == 0) { + return; + } + error_setg(&error_fatal, "VNC support is disabled"); +} +int vnc_init_func(void *opaque, QemuOpts *opts, Error **errp) +{ + error_setg(errp, "VNC support is disabled"); + return -1; +} diff --git a/ui/vnc-ws.c b/ui/vnc-ws.c new file mode 100644 index 000000000..6d79f3e5a --- /dev/null +++ b/ui/vnc-ws.c @@ -0,0 +1,150 @@ +/* + * QEMU VNC display driver: Websockets support + * + * Copyright (C) 2010 Joel Martin + * Copyright (C) 2012 Tim Hardeck + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, see <http://www.gnu.org/licenses/>. + */ + +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "vnc.h" +#include "io/channel-websock.h" +#include "qemu/bswap.h" +#include "trace.h" + +static void vncws_tls_handshake_done(QIOTask *task, + gpointer user_data) +{ + VncState *vs = user_data; + Error *err = NULL; + + if (qio_task_propagate_error(task, &err)) { + VNC_DEBUG("Handshake failed %s\n", error_get_pretty(err)); + vnc_client_error(vs); + error_free(err); + } else { + VNC_DEBUG("TLS handshake complete, starting websocket handshake\n"); + if (vs->ioc_tag) { + g_source_remove(vs->ioc_tag); + } + vs->ioc_tag = qio_channel_add_watch( + QIO_CHANNEL(vs->ioc), G_IO_IN | G_IO_HUP | G_IO_ERR, + vncws_handshake_io, vs, NULL); + } +} + + +gboolean vncws_tls_handshake_io(QIOChannel *ioc G_GNUC_UNUSED, + GIOCondition condition, + void *opaque) +{ + VncState *vs = opaque; + QIOChannelTLS *tls; + Error *err = NULL; + + if (vs->ioc_tag) { + g_source_remove(vs->ioc_tag); + vs->ioc_tag = 0; + } + + if (condition & (G_IO_HUP | G_IO_ERR)) { + vnc_client_error(vs); + return TRUE; + } + + tls = qio_channel_tls_new_server( + vs->ioc, + vs->vd->tlscreds, + vs->vd->tlsauthzid, + &err); + if (!tls) { + VNC_DEBUG("Failed to setup TLS %s\n", error_get_pretty(err)); + error_free(err); + vnc_client_error(vs); + return TRUE; + } + + qio_channel_set_name(QIO_CHANNEL(tls), "vnc-ws-server-tls"); + + object_unref(OBJECT(vs->ioc)); + vs->ioc = QIO_CHANNEL(tls); + trace_vnc_client_io_wrap(vs, vs->ioc, "tls"); + vs->tls = qio_channel_tls_get_session(tls); + + qio_channel_tls_handshake(tls, + vncws_tls_handshake_done, + vs, + NULL, + NULL); + + return TRUE; +} + + +static void vncws_handshake_done(QIOTask *task, + gpointer user_data) +{ + VncState *vs = user_data; + Error *err = NULL; + + if (qio_task_propagate_error(task, &err)) { + VNC_DEBUG("Websock handshake failed %s\n", error_get_pretty(err)); + vnc_client_error(vs); + error_free(err); + } else { + VNC_DEBUG("Websock handshake complete, starting VNC protocol\n"); + vnc_start_protocol(vs); + if (vs->ioc_tag) { + g_source_remove(vs->ioc_tag); + } + vs->ioc_tag = qio_channel_add_watch( + vs->ioc, G_IO_IN | G_IO_HUP | G_IO_ERR, + vnc_client_io, vs, NULL); + } +} + + +gboolean vncws_handshake_io(QIOChannel *ioc G_GNUC_UNUSED, + GIOCondition condition, + void *opaque) +{ + VncState *vs = opaque; + QIOChannelWebsock *wioc; + + if (vs->ioc_tag) { + g_source_remove(vs->ioc_tag); + vs->ioc_tag = 0; + } + + if (condition & (G_IO_HUP | G_IO_ERR)) { + vnc_client_error(vs); + return TRUE; + } + + wioc = qio_channel_websock_new_server(vs->ioc); + qio_channel_set_name(QIO_CHANNEL(wioc), "vnc-ws-server-websock"); + + object_unref(OBJECT(vs->ioc)); + vs->ioc = QIO_CHANNEL(wioc); + trace_vnc_client_io_wrap(vs, vs->ioc, "websock"); + + qio_channel_websock_handshake(wioc, + vncws_handshake_done, + vs, + NULL); + + return TRUE; +} diff --git a/ui/vnc-ws.h b/ui/vnc-ws.h new file mode 100644 index 000000000..396cacfcb --- /dev/null +++ b/ui/vnc-ws.h @@ -0,0 +1,31 @@ +/* + * QEMU VNC display driver: Websockets support + * + * Copyright (C) 2010 Joel Martin + * Copyright (C) 2012 Tim Hardeck + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef QEMU_UI_VNC_WS_H +#define QEMU_UI_VNC_WS_H + +gboolean vncws_tls_handshake_io(QIOChannel *ioc, + GIOCondition condition, + void *opaque); +gboolean vncws_handshake_io(QIOChannel *ioc, + GIOCondition condition, + void *opaque); + +#endif /* QEMU_UI_VNC_WS_H */ diff --git a/ui/vnc.c b/ui/vnc.c new file mode 100644 index 000000000..af02522e8 --- /dev/null +++ b/ui/vnc.c @@ -0,0 +1,4312 @@ +/* + * QEMU VNC display driver + * + * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws> + * Copyright (C) 2006 Fabrice Bellard + * Copyright (C) 2009 Red Hat, Inc + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "qemu-common.h" +#include "vnc.h" +#include "vnc-jobs.h" +#include "trace.h" +#include "hw/qdev-core.h" +#include "sysemu/sysemu.h" +#include "sysemu/runstate.h" +#include "qemu/error-report.h" +#include "qemu/main-loop.h" +#include "qemu/module.h" +#include "qemu/option.h" +#include "qemu/sockets.h" +#include "qemu/timer.h" +#include "authz/list.h" +#include "qemu/config-file.h" +#include "qapi/qapi-emit-events.h" +#include "qapi/qapi-events-ui.h" +#include "qapi/error.h" +#include "qapi/qapi-commands-ui.h" +#include "ui/input.h" +#include "crypto/hash.h" +#include "crypto/tlscreds.h" +#include "crypto/tlscredsanon.h" +#include "crypto/tlscredsx509.h" +#include "crypto/random.h" +#include "crypto/secret_common.h" +#include "qom/object_interfaces.h" +#include "qemu/cutils.h" +#include "qemu/help_option.h" +#include "io/dns-resolver.h" + +#define VNC_REFRESH_INTERVAL_BASE GUI_REFRESH_INTERVAL_DEFAULT +#define VNC_REFRESH_INTERVAL_INC 50 +#define VNC_REFRESH_INTERVAL_MAX GUI_REFRESH_INTERVAL_IDLE +static const struct timeval VNC_REFRESH_STATS = { 0, 500000 }; +static const struct timeval VNC_REFRESH_LOSSY = { 2, 0 }; + +#include "vnc_keysym.h" +#include "crypto/cipher.h" + +static QTAILQ_HEAD(, VncDisplay) vnc_displays = + QTAILQ_HEAD_INITIALIZER(vnc_displays); + +static int vnc_cursor_define(VncState *vs); +static void vnc_update_throttle_offset(VncState *vs); + +static void vnc_set_share_mode(VncState *vs, VncShareMode mode) +{ +#ifdef _VNC_DEBUG + static const char *mn[] = { + [0] = "undefined", + [VNC_SHARE_MODE_CONNECTING] = "connecting", + [VNC_SHARE_MODE_SHARED] = "shared", + [VNC_SHARE_MODE_EXCLUSIVE] = "exclusive", + [VNC_SHARE_MODE_DISCONNECTED] = "disconnected", + }; + fprintf(stderr, "%s/%p: %s -> %s\n", __func__, + vs->ioc, mn[vs->share_mode], mn[mode]); +#endif + + switch (vs->share_mode) { + case VNC_SHARE_MODE_CONNECTING: + vs->vd->num_connecting--; + break; + case VNC_SHARE_MODE_SHARED: + vs->vd->num_shared--; + break; + case VNC_SHARE_MODE_EXCLUSIVE: + vs->vd->num_exclusive--; + break; + default: + break; + } + + vs->share_mode = mode; + + switch (vs->share_mode) { + case VNC_SHARE_MODE_CONNECTING: + vs->vd->num_connecting++; + break; + case VNC_SHARE_MODE_SHARED: + vs->vd->num_shared++; + break; + case VNC_SHARE_MODE_EXCLUSIVE: + vs->vd->num_exclusive++; + break; + default: + break; + } +} + + +static void vnc_init_basic_info(SocketAddress *addr, + VncBasicInfo *info, + Error **errp) +{ + switch (addr->type) { + case SOCKET_ADDRESS_TYPE_INET: + info->host = g_strdup(addr->u.inet.host); + info->service = g_strdup(addr->u.inet.port); + if (addr->u.inet.ipv6) { + info->family = NETWORK_ADDRESS_FAMILY_IPV6; + } else { + info->family = NETWORK_ADDRESS_FAMILY_IPV4; + } + break; + + case SOCKET_ADDRESS_TYPE_UNIX: + info->host = g_strdup(""); + info->service = g_strdup(addr->u.q_unix.path); + info->family = NETWORK_ADDRESS_FAMILY_UNIX; + break; + + case SOCKET_ADDRESS_TYPE_VSOCK: + case SOCKET_ADDRESS_TYPE_FD: + error_setg(errp, "Unsupported socket address type %s", + SocketAddressType_str(addr->type)); + break; + default: + abort(); + } + + return; +} + +static void vnc_init_basic_info_from_server_addr(QIOChannelSocket *ioc, + VncBasicInfo *info, + Error **errp) +{ + SocketAddress *addr = NULL; + + if (!ioc) { + error_setg(errp, "No listener socket available"); + return; + } + + addr = qio_channel_socket_get_local_address(ioc, errp); + if (!addr) { + return; + } + + vnc_init_basic_info(addr, info, errp); + qapi_free_SocketAddress(addr); +} + +static void vnc_init_basic_info_from_remote_addr(QIOChannelSocket *ioc, + VncBasicInfo *info, + Error **errp) +{ + SocketAddress *addr = NULL; + + addr = qio_channel_socket_get_remote_address(ioc, errp); + if (!addr) { + return; + } + + vnc_init_basic_info(addr, info, errp); + qapi_free_SocketAddress(addr); +} + +static const char *vnc_auth_name(VncDisplay *vd) { + switch (vd->auth) { + case VNC_AUTH_INVALID: + return "invalid"; + case VNC_AUTH_NONE: + return "none"; + case VNC_AUTH_VNC: + return "vnc"; + case VNC_AUTH_RA2: + return "ra2"; + case VNC_AUTH_RA2NE: + return "ra2ne"; + case VNC_AUTH_TIGHT: + return "tight"; + case VNC_AUTH_ULTRA: + return "ultra"; + case VNC_AUTH_TLS: + return "tls"; + case VNC_AUTH_VENCRYPT: + switch (vd->subauth) { + case VNC_AUTH_VENCRYPT_PLAIN: + return "vencrypt+plain"; + case VNC_AUTH_VENCRYPT_TLSNONE: + return "vencrypt+tls+none"; + case VNC_AUTH_VENCRYPT_TLSVNC: + return "vencrypt+tls+vnc"; + case VNC_AUTH_VENCRYPT_TLSPLAIN: + return "vencrypt+tls+plain"; + case VNC_AUTH_VENCRYPT_X509NONE: + return "vencrypt+x509+none"; + case VNC_AUTH_VENCRYPT_X509VNC: + return "vencrypt+x509+vnc"; + case VNC_AUTH_VENCRYPT_X509PLAIN: + return "vencrypt+x509+plain"; + case VNC_AUTH_VENCRYPT_TLSSASL: + return "vencrypt+tls+sasl"; + case VNC_AUTH_VENCRYPT_X509SASL: + return "vencrypt+x509+sasl"; + default: + return "vencrypt"; + } + case VNC_AUTH_SASL: + return "sasl"; + } + return "unknown"; +} + +static VncServerInfo *vnc_server_info_get(VncDisplay *vd) +{ + VncServerInfo *info; + Error *err = NULL; + + if (!vd->listener || !vd->listener->nsioc) { + return NULL; + } + + info = g_malloc0(sizeof(*info)); + vnc_init_basic_info_from_server_addr(vd->listener->sioc[0], + qapi_VncServerInfo_base(info), &err); + info->has_auth = true; + info->auth = g_strdup(vnc_auth_name(vd)); + if (err) { + qapi_free_VncServerInfo(info); + info = NULL; + error_free(err); + } + return info; +} + +static void vnc_client_cache_auth(VncState *client) +{ + if (!client->info) { + return; + } + + if (client->tls) { + client->info->x509_dname = + qcrypto_tls_session_get_peer_name(client->tls); + client->info->has_x509_dname = + client->info->x509_dname != NULL; + } +#ifdef CONFIG_VNC_SASL + if (client->sasl.conn && + client->sasl.username) { + client->info->has_sasl_username = true; + client->info->sasl_username = g_strdup(client->sasl.username); + } +#endif +} + +static void vnc_client_cache_addr(VncState *client) +{ + Error *err = NULL; + + client->info = g_malloc0(sizeof(*client->info)); + vnc_init_basic_info_from_remote_addr(client->sioc, + qapi_VncClientInfo_base(client->info), + &err); + client->info->websocket = client->websocket; + if (err) { + qapi_free_VncClientInfo(client->info); + client->info = NULL; + error_free(err); + } +} + +static void vnc_qmp_event(VncState *vs, QAPIEvent event) +{ + VncServerInfo *si; + + if (!vs->info) { + return; + } + + si = vnc_server_info_get(vs->vd); + if (!si) { + return; + } + + switch (event) { + case QAPI_EVENT_VNC_CONNECTED: + qapi_event_send_vnc_connected(si, qapi_VncClientInfo_base(vs->info)); + break; + case QAPI_EVENT_VNC_INITIALIZED: + qapi_event_send_vnc_initialized(si, vs->info); + break; + case QAPI_EVENT_VNC_DISCONNECTED: + qapi_event_send_vnc_disconnected(si, vs->info); + break; + default: + break; + } + + qapi_free_VncServerInfo(si); +} + +static VncClientInfo *qmp_query_vnc_client(const VncState *client) +{ + VncClientInfo *info; + Error *err = NULL; + + info = g_malloc0(sizeof(*info)); + + vnc_init_basic_info_from_remote_addr(client->sioc, + qapi_VncClientInfo_base(info), + &err); + if (err) { + error_free(err); + qapi_free_VncClientInfo(info); + return NULL; + } + + info->websocket = client->websocket; + + if (client->tls) { + info->x509_dname = qcrypto_tls_session_get_peer_name(client->tls); + info->has_x509_dname = info->x509_dname != NULL; + } +#ifdef CONFIG_VNC_SASL + if (client->sasl.conn && client->sasl.username) { + info->has_sasl_username = true; + info->sasl_username = g_strdup(client->sasl.username); + } +#endif + + return info; +} + +static VncDisplay *vnc_display_find(const char *id) +{ + VncDisplay *vd; + + if (id == NULL) { + return QTAILQ_FIRST(&vnc_displays); + } + QTAILQ_FOREACH(vd, &vnc_displays, next) { + if (strcmp(id, vd->id) == 0) { + return vd; + } + } + return NULL; +} + +static VncClientInfoList *qmp_query_client_list(VncDisplay *vd) +{ + VncClientInfoList *prev = NULL; + VncState *client; + + QTAILQ_FOREACH(client, &vd->clients, next) { + QAPI_LIST_PREPEND(prev, qmp_query_vnc_client(client)); + } + return prev; +} + +VncInfo *qmp_query_vnc(Error **errp) +{ + VncInfo *info = g_malloc0(sizeof(*info)); + VncDisplay *vd = vnc_display_find(NULL); + SocketAddress *addr = NULL; + + if (vd == NULL || !vd->listener || !vd->listener->nsioc) { + info->enabled = false; + } else { + info->enabled = true; + + /* for compatibility with the original command */ + info->has_clients = true; + info->clients = qmp_query_client_list(vd); + + addr = qio_channel_socket_get_local_address(vd->listener->sioc[0], + errp); + if (!addr) { + goto out_error; + } + + switch (addr->type) { + case SOCKET_ADDRESS_TYPE_INET: + info->host = g_strdup(addr->u.inet.host); + info->service = g_strdup(addr->u.inet.port); + if (addr->u.inet.ipv6) { + info->family = NETWORK_ADDRESS_FAMILY_IPV6; + } else { + info->family = NETWORK_ADDRESS_FAMILY_IPV4; + } + break; + + case SOCKET_ADDRESS_TYPE_UNIX: + info->host = g_strdup(""); + info->service = g_strdup(addr->u.q_unix.path); + info->family = NETWORK_ADDRESS_FAMILY_UNIX; + break; + + case SOCKET_ADDRESS_TYPE_VSOCK: + case SOCKET_ADDRESS_TYPE_FD: + error_setg(errp, "Unsupported socket address type %s", + SocketAddressType_str(addr->type)); + goto out_error; + default: + abort(); + } + + info->has_host = true; + info->has_service = true; + info->has_family = true; + + info->has_auth = true; + info->auth = g_strdup(vnc_auth_name(vd)); + } + + qapi_free_SocketAddress(addr); + return info; + +out_error: + qapi_free_SocketAddress(addr); + qapi_free_VncInfo(info); + return NULL; +} + + +static void qmp_query_auth(int auth, int subauth, + VncPrimaryAuth *qmp_auth, + VncVencryptSubAuth *qmp_vencrypt, + bool *qmp_has_vencrypt); + +static VncServerInfo2List *qmp_query_server_entry(QIOChannelSocket *ioc, + bool websocket, + int auth, + int subauth, + VncServerInfo2List *prev) +{ + VncServerInfo2 *info; + Error *err = NULL; + SocketAddress *addr; + + addr = qio_channel_socket_get_local_address(ioc, NULL); + if (!addr) { + return prev; + } + + info = g_new0(VncServerInfo2, 1); + vnc_init_basic_info(addr, qapi_VncServerInfo2_base(info), &err); + qapi_free_SocketAddress(addr); + if (err) { + qapi_free_VncServerInfo2(info); + error_free(err); + return prev; + } + info->websocket = websocket; + + qmp_query_auth(auth, subauth, &info->auth, + &info->vencrypt, &info->has_vencrypt); + + QAPI_LIST_PREPEND(prev, info); + return prev; +} + +static void qmp_query_auth(int auth, int subauth, + VncPrimaryAuth *qmp_auth, + VncVencryptSubAuth *qmp_vencrypt, + bool *qmp_has_vencrypt) +{ + switch (auth) { + case VNC_AUTH_VNC: + *qmp_auth = VNC_PRIMARY_AUTH_VNC; + break; + case VNC_AUTH_RA2: + *qmp_auth = VNC_PRIMARY_AUTH_RA2; + break; + case VNC_AUTH_RA2NE: + *qmp_auth = VNC_PRIMARY_AUTH_RA2NE; + break; + case VNC_AUTH_TIGHT: + *qmp_auth = VNC_PRIMARY_AUTH_TIGHT; + break; + case VNC_AUTH_ULTRA: + *qmp_auth = VNC_PRIMARY_AUTH_ULTRA; + break; + case VNC_AUTH_TLS: + *qmp_auth = VNC_PRIMARY_AUTH_TLS; + break; + case VNC_AUTH_VENCRYPT: + *qmp_auth = VNC_PRIMARY_AUTH_VENCRYPT; + *qmp_has_vencrypt = true; + switch (subauth) { + case VNC_AUTH_VENCRYPT_PLAIN: + *qmp_vencrypt = VNC_VENCRYPT_SUB_AUTH_PLAIN; + break; + case VNC_AUTH_VENCRYPT_TLSNONE: + *qmp_vencrypt = VNC_VENCRYPT_SUB_AUTH_TLS_NONE; + break; + case VNC_AUTH_VENCRYPT_TLSVNC: + *qmp_vencrypt = VNC_VENCRYPT_SUB_AUTH_TLS_VNC; + break; + case VNC_AUTH_VENCRYPT_TLSPLAIN: + *qmp_vencrypt = VNC_VENCRYPT_SUB_AUTH_TLS_PLAIN; + break; + case VNC_AUTH_VENCRYPT_X509NONE: + *qmp_vencrypt = VNC_VENCRYPT_SUB_AUTH_X509_NONE; + break; + case VNC_AUTH_VENCRYPT_X509VNC: + *qmp_vencrypt = VNC_VENCRYPT_SUB_AUTH_X509_VNC; + break; + case VNC_AUTH_VENCRYPT_X509PLAIN: + *qmp_vencrypt = VNC_VENCRYPT_SUB_AUTH_X509_PLAIN; + break; + case VNC_AUTH_VENCRYPT_TLSSASL: + *qmp_vencrypt = VNC_VENCRYPT_SUB_AUTH_TLS_SASL; + break; + case VNC_AUTH_VENCRYPT_X509SASL: + *qmp_vencrypt = VNC_VENCRYPT_SUB_AUTH_X509_SASL; + break; + default: + *qmp_has_vencrypt = false; + break; + } + break; + case VNC_AUTH_SASL: + *qmp_auth = VNC_PRIMARY_AUTH_SASL; + break; + case VNC_AUTH_NONE: + default: + *qmp_auth = VNC_PRIMARY_AUTH_NONE; + break; + } +} + +VncInfo2List *qmp_query_vnc_servers(Error **errp) +{ + VncInfo2List *prev = NULL; + VncInfo2 *info; + VncDisplay *vd; + DeviceState *dev; + size_t i; + + QTAILQ_FOREACH(vd, &vnc_displays, next) { + info = g_new0(VncInfo2, 1); + info->id = g_strdup(vd->id); + info->clients = qmp_query_client_list(vd); + qmp_query_auth(vd->auth, vd->subauth, &info->auth, + &info->vencrypt, &info->has_vencrypt); + if (vd->dcl.con) { + dev = DEVICE(object_property_get_link(OBJECT(vd->dcl.con), + "device", &error_abort)); + info->has_display = true; + info->display = g_strdup(dev->id); + } + for (i = 0; vd->listener != NULL && i < vd->listener->nsioc; i++) { + info->server = qmp_query_server_entry( + vd->listener->sioc[i], false, vd->auth, vd->subauth, + info->server); + } + for (i = 0; vd->wslistener != NULL && i < vd->wslistener->nsioc; i++) { + info->server = qmp_query_server_entry( + vd->wslistener->sioc[i], true, vd->ws_auth, + vd->ws_subauth, info->server); + } + + QAPI_LIST_PREPEND(prev, info); + } + return prev; +} + +bool vnc_display_reload_certs(const char *id, Error **errp) +{ + VncDisplay *vd = vnc_display_find(id); + QCryptoTLSCredsClass *creds = NULL; + + if (!vd) { + error_setg(errp, "Can not find vnc display"); + return false; + } + + if (!vd->tlscreds) { + error_setg(errp, "vnc tls is not enabled"); + return false; + } + + creds = QCRYPTO_TLS_CREDS_GET_CLASS(OBJECT(vd->tlscreds)); + if (creds->reload == NULL) { + error_setg(errp, "%s doesn't support to reload TLS credential", + object_get_typename(OBJECT(vd->tlscreds))); + return false; + } + if (!creds->reload(vd->tlscreds, errp)) { + return false; + } + + return true; +} + +/* TODO + 1) Get the queue working for IO. + 2) there is some weirdness when using the -S option (the screen is grey + and not totally invalidated + 3) resolutions > 1024 +*/ + +static int vnc_update_client(VncState *vs, int has_dirty); +static void vnc_disconnect_start(VncState *vs); + +static void vnc_colordepth(VncState *vs); +static void framebuffer_update_request(VncState *vs, int incremental, + int x_position, int y_position, + int w, int h); +static void vnc_refresh(DisplayChangeListener *dcl); +static int vnc_refresh_server_surface(VncDisplay *vd); + +static int vnc_width(VncDisplay *vd) +{ + return MIN(VNC_MAX_WIDTH, ROUND_UP(surface_width(vd->ds), + VNC_DIRTY_PIXELS_PER_BIT)); +} + +static int vnc_true_width(VncDisplay *vd) +{ + return MIN(VNC_MAX_WIDTH, surface_width(vd->ds)); +} + +static int vnc_height(VncDisplay *vd) +{ + return MIN(VNC_MAX_HEIGHT, surface_height(vd->ds)); +} + +static void vnc_set_area_dirty(DECLARE_BITMAP(dirty[VNC_MAX_HEIGHT], + VNC_MAX_WIDTH / VNC_DIRTY_PIXELS_PER_BIT), + VncDisplay *vd, + int x, int y, int w, int h) +{ + int width = vnc_width(vd); + int height = vnc_height(vd); + + /* this is needed this to ensure we updated all affected + * blocks if x % VNC_DIRTY_PIXELS_PER_BIT != 0 */ + w += (x % VNC_DIRTY_PIXELS_PER_BIT); + x -= (x % VNC_DIRTY_PIXELS_PER_BIT); + + x = MIN(x, width); + y = MIN(y, height); + w = MIN(x + w, width) - x; + h = MIN(y + h, height); + + for (; y < h; y++) { + bitmap_set(dirty[y], x / VNC_DIRTY_PIXELS_PER_BIT, + DIV_ROUND_UP(w, VNC_DIRTY_PIXELS_PER_BIT)); + } +} + +static void vnc_dpy_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + VncDisplay *vd = container_of(dcl, VncDisplay, dcl); + struct VncSurface *s = &vd->guest; + + vnc_set_area_dirty(s->dirty, vd, x, y, w, h); +} + +void vnc_framebuffer_update(VncState *vs, int x, int y, int w, int h, + int32_t encoding) +{ + vnc_write_u16(vs, x); + vnc_write_u16(vs, y); + vnc_write_u16(vs, w); + vnc_write_u16(vs, h); + + vnc_write_s32(vs, encoding); +} + +static void vnc_desktop_resize_ext(VncState *vs, int reject_reason) +{ + trace_vnc_msg_server_ext_desktop_resize( + vs, vs->ioc, vs->client_width, vs->client_height, reject_reason); + + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE); + vnc_write_u8(vs, 0); + vnc_write_u16(vs, 1); /* number of rects */ + vnc_framebuffer_update(vs, + reject_reason ? 1 : 0, + reject_reason, + vs->client_width, vs->client_height, + VNC_ENCODING_DESKTOP_RESIZE_EXT); + vnc_write_u8(vs, 1); /* number of screens */ + vnc_write_u8(vs, 0); /* padding */ + vnc_write_u8(vs, 0); /* padding */ + vnc_write_u8(vs, 0); /* padding */ + vnc_write_u32(vs, 0); /* screen id */ + vnc_write_u16(vs, 0); /* screen x-pos */ + vnc_write_u16(vs, 0); /* screen y-pos */ + vnc_write_u16(vs, vs->client_width); + vnc_write_u16(vs, vs->client_height); + vnc_write_u32(vs, 0); /* screen flags */ + vnc_unlock_output(vs); + vnc_flush(vs); +} + +static void vnc_desktop_resize(VncState *vs) +{ + if (vs->ioc == NULL || (!vnc_has_feature(vs, VNC_FEATURE_RESIZE) && + !vnc_has_feature(vs, VNC_FEATURE_RESIZE_EXT))) { + return; + } + if (vs->client_width == vs->vd->true_width && + vs->client_height == pixman_image_get_height(vs->vd->server)) { + return; + } + + assert(vs->vd->true_width < 65536 && + vs->vd->true_width >= 0); + assert(pixman_image_get_height(vs->vd->server) < 65536 && + pixman_image_get_height(vs->vd->server) >= 0); + vs->client_width = vs->vd->true_width; + vs->client_height = pixman_image_get_height(vs->vd->server); + + if (vnc_has_feature(vs, VNC_FEATURE_RESIZE_EXT)) { + vnc_desktop_resize_ext(vs, 0); + return; + } + + trace_vnc_msg_server_desktop_resize( + vs, vs->ioc, vs->client_width, vs->client_height); + + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE); + vnc_write_u8(vs, 0); + vnc_write_u16(vs, 1); /* number of rects */ + vnc_framebuffer_update(vs, 0, 0, vs->client_width, vs->client_height, + VNC_ENCODING_DESKTOPRESIZE); + vnc_unlock_output(vs); + vnc_flush(vs); +} + +static void vnc_abort_display_jobs(VncDisplay *vd) +{ + VncState *vs; + + QTAILQ_FOREACH(vs, &vd->clients, next) { + vnc_lock_output(vs); + vs->abort = true; + vnc_unlock_output(vs); + } + QTAILQ_FOREACH(vs, &vd->clients, next) { + vnc_jobs_join(vs); + } + QTAILQ_FOREACH(vs, &vd->clients, next) { + vnc_lock_output(vs); + if (vs->update == VNC_STATE_UPDATE_NONE && + vs->job_update != VNC_STATE_UPDATE_NONE) { + /* job aborted before completion */ + vs->update = vs->job_update; + vs->job_update = VNC_STATE_UPDATE_NONE; + } + vs->abort = false; + vnc_unlock_output(vs); + } +} + +int vnc_server_fb_stride(VncDisplay *vd) +{ + return pixman_image_get_stride(vd->server); +} + +void *vnc_server_fb_ptr(VncDisplay *vd, int x, int y) +{ + uint8_t *ptr; + + ptr = (uint8_t *)pixman_image_get_data(vd->server); + ptr += y * vnc_server_fb_stride(vd); + ptr += x * VNC_SERVER_FB_BYTES; + return ptr; +} + +static void vnc_update_server_surface(VncDisplay *vd) +{ + int width, height; + + qemu_pixman_image_unref(vd->server); + vd->server = NULL; + + if (QTAILQ_EMPTY(&vd->clients)) { + return; + } + + width = vnc_width(vd); + height = vnc_height(vd); + vd->true_width = vnc_true_width(vd); + vd->server = pixman_image_create_bits(VNC_SERVER_FB_FORMAT, + width, height, + NULL, 0); + + memset(vd->guest.dirty, 0x00, sizeof(vd->guest.dirty)); + vnc_set_area_dirty(vd->guest.dirty, vd, 0, 0, + width, height); +} + +static bool vnc_check_pageflip(DisplaySurface *s1, + DisplaySurface *s2) +{ + return (s1 != NULL && + s2 != NULL && + surface_width(s1) == surface_width(s2) && + surface_height(s1) == surface_height(s2) && + surface_format(s1) == surface_format(s2)); + +} + +static void vnc_dpy_switch(DisplayChangeListener *dcl, + DisplaySurface *surface) +{ + VncDisplay *vd = container_of(dcl, VncDisplay, dcl); + bool pageflip = vnc_check_pageflip(vd->ds, surface); + VncState *vs; + + vnc_abort_display_jobs(vd); + vd->ds = surface; + + /* guest surface */ + qemu_pixman_image_unref(vd->guest.fb); + vd->guest.fb = pixman_image_ref(surface->image); + vd->guest.format = surface->format; + + + if (pageflip) { + trace_vnc_server_dpy_pageflip(vd, + surface_width(surface), + surface_height(surface), + surface_format(surface)); + vnc_set_area_dirty(vd->guest.dirty, vd, 0, 0, + surface_width(surface), + surface_height(surface)); + return; + } + + trace_vnc_server_dpy_recreate(vd, + surface_width(surface), + surface_height(surface), + surface_format(surface)); + /* server surface */ + vnc_update_server_surface(vd); + + QTAILQ_FOREACH(vs, &vd->clients, next) { + vnc_colordepth(vs); + vnc_desktop_resize(vs); + vnc_cursor_define(vs); + memset(vs->dirty, 0x00, sizeof(vs->dirty)); + vnc_set_area_dirty(vs->dirty, vd, 0, 0, + vnc_width(vd), + vnc_height(vd)); + vnc_update_throttle_offset(vs); + } +} + +/* fastest code */ +static void vnc_write_pixels_copy(VncState *vs, + void *pixels, int size) +{ + vnc_write(vs, pixels, size); +} + +/* slowest but generic code. */ +void vnc_convert_pixel(VncState *vs, uint8_t *buf, uint32_t v) +{ + uint8_t r, g, b; + +#if VNC_SERVER_FB_FORMAT == PIXMAN_FORMAT(32, PIXMAN_TYPE_ARGB, 0, 8, 8, 8) + r = (((v & 0x00ff0000) >> 16) << vs->client_pf.rbits) >> 8; + g = (((v & 0x0000ff00) >> 8) << vs->client_pf.gbits) >> 8; + b = (((v & 0x000000ff) >> 0) << vs->client_pf.bbits) >> 8; +#else +# error need some bits here if you change VNC_SERVER_FB_FORMAT +#endif + v = (r << vs->client_pf.rshift) | + (g << vs->client_pf.gshift) | + (b << vs->client_pf.bshift); + switch (vs->client_pf.bytes_per_pixel) { + case 1: + buf[0] = v; + break; + case 2: + if (vs->client_be) { + buf[0] = v >> 8; + buf[1] = v; + } else { + buf[1] = v >> 8; + buf[0] = v; + } + break; + default: + case 4: + if (vs->client_be) { + buf[0] = v >> 24; + buf[1] = v >> 16; + buf[2] = v >> 8; + buf[3] = v; + } else { + buf[3] = v >> 24; + buf[2] = v >> 16; + buf[1] = v >> 8; + buf[0] = v; + } + break; + } +} + +static void vnc_write_pixels_generic(VncState *vs, + void *pixels1, int size) +{ + uint8_t buf[4]; + + if (VNC_SERVER_FB_BYTES == 4) { + uint32_t *pixels = pixels1; + int n, i; + n = size >> 2; + for (i = 0; i < n; i++) { + vnc_convert_pixel(vs, buf, pixels[i]); + vnc_write(vs, buf, vs->client_pf.bytes_per_pixel); + } + } +} + +int vnc_raw_send_framebuffer_update(VncState *vs, int x, int y, int w, int h) +{ + int i; + uint8_t *row; + VncDisplay *vd = vs->vd; + + row = vnc_server_fb_ptr(vd, x, y); + for (i = 0; i < h; i++) { + vs->write_pixels(vs, row, w * VNC_SERVER_FB_BYTES); + row += vnc_server_fb_stride(vd); + } + return 1; +} + +int vnc_send_framebuffer_update(VncState *vs, int x, int y, int w, int h) +{ + int n = 0; + + switch(vs->vnc_encoding) { + case VNC_ENCODING_ZLIB: + n = vnc_zlib_send_framebuffer_update(vs, x, y, w, h); + break; + case VNC_ENCODING_HEXTILE: + vnc_framebuffer_update(vs, x, y, w, h, VNC_ENCODING_HEXTILE); + n = vnc_hextile_send_framebuffer_update(vs, x, y, w, h); + break; + case VNC_ENCODING_TIGHT: + n = vnc_tight_send_framebuffer_update(vs, x, y, w, h); + break; + case VNC_ENCODING_TIGHT_PNG: + n = vnc_tight_png_send_framebuffer_update(vs, x, y, w, h); + break; + case VNC_ENCODING_ZRLE: + n = vnc_zrle_send_framebuffer_update(vs, x, y, w, h); + break; + case VNC_ENCODING_ZYWRLE: + n = vnc_zywrle_send_framebuffer_update(vs, x, y, w, h); + break; + default: + vnc_framebuffer_update(vs, x, y, w, h, VNC_ENCODING_RAW); + n = vnc_raw_send_framebuffer_update(vs, x, y, w, h); + break; + } + return n; +} + +static void vnc_mouse_set(DisplayChangeListener *dcl, + int x, int y, int visible) +{ + /* can we ask the client(s) to move the pointer ??? */ +} + +static int vnc_cursor_define(VncState *vs) +{ + QEMUCursor *c = vs->vd->cursor; + int isize; + + if (!vs->vd->cursor) { + return -1; + } + + if (vnc_has_feature(vs, VNC_FEATURE_ALPHA_CURSOR)) { + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE); + vnc_write_u8(vs, 0); /* padding */ + vnc_write_u16(vs, 1); /* # of rects */ + vnc_framebuffer_update(vs, c->hot_x, c->hot_y, c->width, c->height, + VNC_ENCODING_ALPHA_CURSOR); + vnc_write_s32(vs, VNC_ENCODING_RAW); + vnc_write(vs, c->data, c->width * c->height * 4); + vnc_unlock_output(vs); + return 0; + } + if (vnc_has_feature(vs, VNC_FEATURE_RICH_CURSOR)) { + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE); + vnc_write_u8(vs, 0); /* padding */ + vnc_write_u16(vs, 1); /* # of rects */ + vnc_framebuffer_update(vs, c->hot_x, c->hot_y, c->width, c->height, + VNC_ENCODING_RICH_CURSOR); + isize = c->width * c->height * vs->client_pf.bytes_per_pixel; + vnc_write_pixels_generic(vs, c->data, isize); + vnc_write(vs, vs->vd->cursor_mask, vs->vd->cursor_msize); + vnc_unlock_output(vs); + return 0; + } + return -1; +} + +static void vnc_dpy_cursor_define(DisplayChangeListener *dcl, + QEMUCursor *c) +{ + VncDisplay *vd = container_of(dcl, VncDisplay, dcl); + VncState *vs; + + cursor_put(vd->cursor); + g_free(vd->cursor_mask); + + vd->cursor = c; + cursor_get(vd->cursor); + vd->cursor_msize = cursor_get_mono_bpl(c) * c->height; + vd->cursor_mask = g_malloc0(vd->cursor_msize); + cursor_get_mono_mask(c, 0, vd->cursor_mask); + + QTAILQ_FOREACH(vs, &vd->clients, next) { + vnc_cursor_define(vs); + } +} + +static int find_and_clear_dirty_height(VncState *vs, + int y, int last_x, int x, int height) +{ + int h; + + for (h = 1; h < (height - y); h++) { + if (!test_bit(last_x, vs->dirty[y + h])) { + break; + } + bitmap_clear(vs->dirty[y + h], last_x, x - last_x); + } + + return h; +} + +/* + * Figure out how much pending data we should allow in the output + * buffer before we throttle incremental display updates, and/or + * drop audio samples. + * + * We allow for equiv of 1 full display's worth of FB updates, + * and 1 second of audio samples. If audio backlog was larger + * than that the client would already suffering awful audio + * glitches, so dropping samples is no worse really). + */ +static void vnc_update_throttle_offset(VncState *vs) +{ + size_t offset = + vs->client_width * vs->client_height * vs->client_pf.bytes_per_pixel; + + if (vs->audio_cap) { + int bps; + switch (vs->as.fmt) { + default: + case AUDIO_FORMAT_U8: + case AUDIO_FORMAT_S8: + bps = 1; + break; + case AUDIO_FORMAT_U16: + case AUDIO_FORMAT_S16: + bps = 2; + break; + case AUDIO_FORMAT_U32: + case AUDIO_FORMAT_S32: + bps = 4; + break; + } + offset += vs->as.freq * bps * vs->as.nchannels; + } + + /* Put a floor of 1MB on offset, so that if we have a large pending + * buffer and the display is resized to a small size & back again + * we don't suddenly apply a tiny send limit + */ + offset = MAX(offset, 1024 * 1024); + + if (vs->throttle_output_offset != offset) { + trace_vnc_client_throttle_threshold( + vs, vs->ioc, vs->throttle_output_offset, offset, vs->client_width, + vs->client_height, vs->client_pf.bytes_per_pixel, vs->audio_cap); + } + + vs->throttle_output_offset = offset; +} + +static bool vnc_should_update(VncState *vs) +{ + switch (vs->update) { + case VNC_STATE_UPDATE_NONE: + break; + case VNC_STATE_UPDATE_INCREMENTAL: + /* Only allow incremental updates if the pending send queue + * is less than the permitted threshold, and the job worker + * is completely idle. + */ + if (vs->output.offset < vs->throttle_output_offset && + vs->job_update == VNC_STATE_UPDATE_NONE) { + return true; + } + trace_vnc_client_throttle_incremental( + vs, vs->ioc, vs->job_update, vs->output.offset); + break; + case VNC_STATE_UPDATE_FORCE: + /* Only allow forced updates if the pending send queue + * does not contain a previous forced update, and the + * job worker is completely idle. + * + * Note this means we'll queue a forced update, even if + * the output buffer size is otherwise over the throttle + * output limit. + */ + if (vs->force_update_offset == 0 && + vs->job_update == VNC_STATE_UPDATE_NONE) { + return true; + } + trace_vnc_client_throttle_forced( + vs, vs->ioc, vs->job_update, vs->force_update_offset); + break; + } + return false; +} + +static int vnc_update_client(VncState *vs, int has_dirty) +{ + VncDisplay *vd = vs->vd; + VncJob *job; + int y; + int height, width; + int n = 0; + + if (vs->disconnecting) { + vnc_disconnect_finish(vs); + return 0; + } + + vs->has_dirty += has_dirty; + if (!vnc_should_update(vs)) { + return 0; + } + + if (!vs->has_dirty && vs->update != VNC_STATE_UPDATE_FORCE) { + return 0; + } + + /* + * Send screen updates to the vnc client using the server + * surface and server dirty map. guest surface updates + * happening in parallel don't disturb us, the next pass will + * send them to the client. + */ + job = vnc_job_new(vs); + + height = pixman_image_get_height(vd->server); + width = pixman_image_get_width(vd->server); + + y = 0; + for (;;) { + int x, h; + unsigned long x2; + unsigned long offset = find_next_bit((unsigned long *) &vs->dirty, + height * VNC_DIRTY_BPL(vs), + y * VNC_DIRTY_BPL(vs)); + if (offset == height * VNC_DIRTY_BPL(vs)) { + /* no more dirty bits */ + break; + } + y = offset / VNC_DIRTY_BPL(vs); + x = offset % VNC_DIRTY_BPL(vs); + x2 = find_next_zero_bit((unsigned long *) &vs->dirty[y], + VNC_DIRTY_BPL(vs), x); + bitmap_clear(vs->dirty[y], x, x2 - x); + h = find_and_clear_dirty_height(vs, y, x, x2, height); + x2 = MIN(x2, width / VNC_DIRTY_PIXELS_PER_BIT); + if (x2 > x) { + n += vnc_job_add_rect(job, x * VNC_DIRTY_PIXELS_PER_BIT, y, + (x2 - x) * VNC_DIRTY_PIXELS_PER_BIT, h); + } + if (!x && x2 == width / VNC_DIRTY_PIXELS_PER_BIT) { + y += h; + if (y == height) { + break; + } + } + } + + vs->job_update = vs->update; + vs->update = VNC_STATE_UPDATE_NONE; + vnc_job_push(job); + vs->has_dirty = 0; + return n; +} + +/* audio */ +static void audio_capture_notify(void *opaque, audcnotification_e cmd) +{ + VncState *vs = opaque; + + assert(vs->magic == VNC_MAGIC); + switch (cmd) { + case AUD_CNOTIFY_DISABLE: + trace_vnc_msg_server_audio_end(vs, vs->ioc); + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_QEMU); + vnc_write_u8(vs, VNC_MSG_SERVER_QEMU_AUDIO); + vnc_write_u16(vs, VNC_MSG_SERVER_QEMU_AUDIO_END); + vnc_unlock_output(vs); + vnc_flush(vs); + break; + + case AUD_CNOTIFY_ENABLE: + trace_vnc_msg_server_audio_begin(vs, vs->ioc); + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_QEMU); + vnc_write_u8(vs, VNC_MSG_SERVER_QEMU_AUDIO); + vnc_write_u16(vs, VNC_MSG_SERVER_QEMU_AUDIO_BEGIN); + vnc_unlock_output(vs); + vnc_flush(vs); + break; + } +} + +static void audio_capture_destroy(void *opaque) +{ +} + +static void audio_capture(void *opaque, const void *buf, int size) +{ + VncState *vs = opaque; + + assert(vs->magic == VNC_MAGIC); + trace_vnc_msg_server_audio_data(vs, vs->ioc, buf, size); + vnc_lock_output(vs); + if (vs->output.offset < vs->throttle_output_offset) { + vnc_write_u8(vs, VNC_MSG_SERVER_QEMU); + vnc_write_u8(vs, VNC_MSG_SERVER_QEMU_AUDIO); + vnc_write_u16(vs, VNC_MSG_SERVER_QEMU_AUDIO_DATA); + vnc_write_u32(vs, size); + vnc_write(vs, buf, size); + } else { + trace_vnc_client_throttle_audio(vs, vs->ioc, vs->output.offset); + } + vnc_unlock_output(vs); + vnc_flush(vs); +} + +static void audio_add(VncState *vs) +{ + struct audio_capture_ops ops; + + if (vs->audio_cap) { + error_report("audio already running"); + return; + } + + ops.notify = audio_capture_notify; + ops.destroy = audio_capture_destroy; + ops.capture = audio_capture; + + vs->audio_cap = AUD_add_capture(vs->vd->audio_state, &vs->as, &ops, vs); + if (!vs->audio_cap) { + error_report("Failed to add audio capture"); + } +} + +static void audio_del(VncState *vs) +{ + if (vs->audio_cap) { + AUD_del_capture(vs->audio_cap, vs); + vs->audio_cap = NULL; + } +} + +static void vnc_disconnect_start(VncState *vs) +{ + if (vs->disconnecting) { + return; + } + trace_vnc_client_disconnect_start(vs, vs->ioc); + vnc_set_share_mode(vs, VNC_SHARE_MODE_DISCONNECTED); + if (vs->ioc_tag) { + g_source_remove(vs->ioc_tag); + vs->ioc_tag = 0; + } + qio_channel_close(vs->ioc, NULL); + vs->disconnecting = TRUE; +} + +void vnc_disconnect_finish(VncState *vs) +{ + int i; + + trace_vnc_client_disconnect_finish(vs, vs->ioc); + + vnc_jobs_join(vs); /* Wait encoding jobs */ + + vnc_lock_output(vs); + vnc_qmp_event(vs, QAPI_EVENT_VNC_DISCONNECTED); + + buffer_free(&vs->input); + buffer_free(&vs->output); + + qapi_free_VncClientInfo(vs->info); + + vnc_zlib_clear(vs); + vnc_tight_clear(vs); + vnc_zrle_clear(vs); + +#ifdef CONFIG_VNC_SASL + vnc_sasl_client_cleanup(vs); +#endif /* CONFIG_VNC_SASL */ + audio_del(vs); + qkbd_state_lift_all_keys(vs->vd->kbd); + + if (vs->mouse_mode_notifier.notify != NULL) { + qemu_remove_mouse_mode_change_notifier(&vs->mouse_mode_notifier); + } + QTAILQ_REMOVE(&vs->vd->clients, vs, next); + if (QTAILQ_EMPTY(&vs->vd->clients)) { + /* last client gone */ + vnc_update_server_surface(vs->vd); + } + if (vs->cbpeer.update.notify) { + qemu_clipboard_peer_unregister(&vs->cbpeer); + } + + vnc_unlock_output(vs); + + qemu_mutex_destroy(&vs->output_mutex); + if (vs->bh != NULL) { + qemu_bh_delete(vs->bh); + } + buffer_free(&vs->jobs_buffer); + + for (i = 0; i < VNC_STAT_ROWS; ++i) { + g_free(vs->lossy_rect[i]); + } + g_free(vs->lossy_rect); + + object_unref(OBJECT(vs->ioc)); + vs->ioc = NULL; + object_unref(OBJECT(vs->sioc)); + vs->sioc = NULL; + vs->magic = 0; + g_free(vs->zrle); + g_free(vs->tight); + g_free(vs); +} + +size_t vnc_client_io_error(VncState *vs, ssize_t ret, Error *err) +{ + if (ret <= 0) { + if (ret == 0) { + trace_vnc_client_eof(vs, vs->ioc); + vnc_disconnect_start(vs); + } else if (ret != QIO_CHANNEL_ERR_BLOCK) { + trace_vnc_client_io_error(vs, vs->ioc, + err ? error_get_pretty(err) : "Unknown"); + vnc_disconnect_start(vs); + } + + error_free(err); + return 0; + } + return ret; +} + + +void vnc_client_error(VncState *vs) +{ + VNC_DEBUG("Closing down client sock: protocol error\n"); + vnc_disconnect_start(vs); +} + + +/* + * Called to write a chunk of data to the client socket. The data may + * be the raw data, or may have already been encoded by SASL. + * The data will be written either straight onto the socket, or + * written via the GNUTLS wrappers, if TLS/SSL encryption is enabled + * + * NB, it is theoretically possible to have 2 layers of encryption, + * both SASL, and this TLS layer. It is highly unlikely in practice + * though, since SASL encryption will typically be a no-op if TLS + * is active + * + * Returns the number of bytes written, which may be less than + * the requested 'datalen' if the socket would block. Returns + * 0 on I/O error, and disconnects the client socket. + */ +size_t vnc_client_write_buf(VncState *vs, const uint8_t *data, size_t datalen) +{ + Error *err = NULL; + ssize_t ret; + ret = qio_channel_write(vs->ioc, (const char *)data, datalen, &err); + VNC_DEBUG("Wrote wire %p %zd -> %ld\n", data, datalen, ret); + return vnc_client_io_error(vs, ret, err); +} + + +/* + * Called to write buffered data to the client socket, when not + * using any SASL SSF encryption layers. Will write as much data + * as possible without blocking. If all buffered data is written, + * will switch the FD poll() handler back to read monitoring. + * + * Returns the number of bytes written, which may be less than + * the buffered output data if the socket would block. Returns + * 0 on I/O error, and disconnects the client socket. + */ +static size_t vnc_client_write_plain(VncState *vs) +{ + size_t offset; + size_t ret; + +#ifdef CONFIG_VNC_SASL + VNC_DEBUG("Write Plain: Pending output %p size %zd offset %zd. Wait SSF %d\n", + vs->output.buffer, vs->output.capacity, vs->output.offset, + vs->sasl.waitWriteSSF); + + if (vs->sasl.conn && + vs->sasl.runSSF && + vs->sasl.waitWriteSSF) { + ret = vnc_client_write_buf(vs, vs->output.buffer, vs->sasl.waitWriteSSF); + if (ret) + vs->sasl.waitWriteSSF -= ret; + } else +#endif /* CONFIG_VNC_SASL */ + ret = vnc_client_write_buf(vs, vs->output.buffer, vs->output.offset); + if (!ret) + return 0; + + if (ret >= vs->force_update_offset) { + if (vs->force_update_offset != 0) { + trace_vnc_client_unthrottle_forced(vs, vs->ioc); + } + vs->force_update_offset = 0; + } else { + vs->force_update_offset -= ret; + } + offset = vs->output.offset; + buffer_advance(&vs->output, ret); + if (offset >= vs->throttle_output_offset && + vs->output.offset < vs->throttle_output_offset) { + trace_vnc_client_unthrottle_incremental(vs, vs->ioc, vs->output.offset); + } + + if (vs->output.offset == 0) { + if (vs->ioc_tag) { + g_source_remove(vs->ioc_tag); + } + vs->ioc_tag = qio_channel_add_watch( + vs->ioc, G_IO_IN | G_IO_HUP | G_IO_ERR, + vnc_client_io, vs, NULL); + } + + return ret; +} + + +/* + * First function called whenever there is data to be written to + * the client socket. Will delegate actual work according to whether + * SASL SSF layers are enabled (thus requiring encryption calls) + */ +static void vnc_client_write_locked(VncState *vs) +{ +#ifdef CONFIG_VNC_SASL + if (vs->sasl.conn && + vs->sasl.runSSF && + !vs->sasl.waitWriteSSF) { + vnc_client_write_sasl(vs); + } else +#endif /* CONFIG_VNC_SASL */ + { + vnc_client_write_plain(vs); + } +} + +static void vnc_client_write(VncState *vs) +{ + assert(vs->magic == VNC_MAGIC); + vnc_lock_output(vs); + if (vs->output.offset) { + vnc_client_write_locked(vs); + } else if (vs->ioc != NULL) { + if (vs->ioc_tag) { + g_source_remove(vs->ioc_tag); + } + vs->ioc_tag = qio_channel_add_watch( + vs->ioc, G_IO_IN | G_IO_HUP | G_IO_ERR, + vnc_client_io, vs, NULL); + } + vnc_unlock_output(vs); +} + +void vnc_read_when(VncState *vs, VncReadEvent *func, size_t expecting) +{ + vs->read_handler = func; + vs->read_handler_expect = expecting; +} + + +/* + * Called to read a chunk of data from the client socket. The data may + * be the raw data, or may need to be further decoded by SASL. + * The data will be read either straight from to the socket, or + * read via the GNUTLS wrappers, if TLS/SSL encryption is enabled + * + * NB, it is theoretically possible to have 2 layers of encryption, + * both SASL, and this TLS layer. It is highly unlikely in practice + * though, since SASL encryption will typically be a no-op if TLS + * is active + * + * Returns the number of bytes read, which may be less than + * the requested 'datalen' if the socket would block. Returns + * 0 on I/O error or EOF, and disconnects the client socket. + */ +size_t vnc_client_read_buf(VncState *vs, uint8_t *data, size_t datalen) +{ + ssize_t ret; + Error *err = NULL; + ret = qio_channel_read(vs->ioc, (char *)data, datalen, &err); + VNC_DEBUG("Read wire %p %zd -> %ld\n", data, datalen, ret); + return vnc_client_io_error(vs, ret, err); +} + + +/* + * Called to read data from the client socket to the input buffer, + * when not using any SASL SSF encryption layers. Will read as much + * data as possible without blocking. + * + * Returns the number of bytes read, which may be less than + * the requested 'datalen' if the socket would block. Returns + * 0 on I/O error or EOF, and disconnects the client socket. + */ +static size_t vnc_client_read_plain(VncState *vs) +{ + size_t ret; + VNC_DEBUG("Read plain %p size %zd offset %zd\n", + vs->input.buffer, vs->input.capacity, vs->input.offset); + buffer_reserve(&vs->input, 4096); + ret = vnc_client_read_buf(vs, buffer_end(&vs->input), 4096); + if (!ret) + return 0; + vs->input.offset += ret; + return ret; +} + +static void vnc_jobs_bh(void *opaque) +{ + VncState *vs = opaque; + + assert(vs->magic == VNC_MAGIC); + vnc_jobs_consume_buffer(vs); +} + +/* + * First function called whenever there is more data to be read from + * the client socket. Will delegate actual work according to whether + * SASL SSF layers are enabled (thus requiring decryption calls) + * Returns 0 on success, -1 if client disconnected + */ +static int vnc_client_read(VncState *vs) +{ + size_t ret; + +#ifdef CONFIG_VNC_SASL + if (vs->sasl.conn && vs->sasl.runSSF) + ret = vnc_client_read_sasl(vs); + else +#endif /* CONFIG_VNC_SASL */ + ret = vnc_client_read_plain(vs); + if (!ret) { + if (vs->disconnecting) { + vnc_disconnect_finish(vs); + return -1; + } + return 0; + } + + while (vs->read_handler && vs->input.offset >= vs->read_handler_expect) { + size_t len = vs->read_handler_expect; + int ret; + + ret = vs->read_handler(vs, vs->input.buffer, len); + if (vs->disconnecting) { + vnc_disconnect_finish(vs); + return -1; + } + + if (!ret) { + buffer_advance(&vs->input, len); + } else { + vs->read_handler_expect = ret; + } + } + return 0; +} + +gboolean vnc_client_io(QIOChannel *ioc G_GNUC_UNUSED, + GIOCondition condition, void *opaque) +{ + VncState *vs = opaque; + + assert(vs->magic == VNC_MAGIC); + + if (condition & (G_IO_HUP | G_IO_ERR)) { + vnc_disconnect_start(vs); + return TRUE; + } + + if (condition & G_IO_IN) { + if (vnc_client_read(vs) < 0) { + /* vs is free()ed here */ + return TRUE; + } + } + if (condition & G_IO_OUT) { + vnc_client_write(vs); + } + + if (vs->disconnecting) { + if (vs->ioc_tag != 0) { + g_source_remove(vs->ioc_tag); + } + vs->ioc_tag = 0; + } + return TRUE; +} + + +/* + * Scale factor to apply to vs->throttle_output_offset when checking for + * hard limit. Worst case normal usage could be x2, if we have a complete + * incremental update and complete forced update in the output buffer. + * So x3 should be good enough, but we pick x5 to be conservative and thus + * (hopefully) never trigger incorrectly. + */ +#define VNC_THROTTLE_OUTPUT_LIMIT_SCALE 5 + +void vnc_write(VncState *vs, const void *data, size_t len) +{ + assert(vs->magic == VNC_MAGIC); + if (vs->disconnecting) { + return; + } + /* Protection against malicious client/guest to prevent our output + * buffer growing without bound if client stops reading data. This + * should rarely trigger, because we have earlier throttling code + * which stops issuing framebuffer updates and drops audio data + * if the throttle_output_offset value is exceeded. So we only reach + * this higher level if a huge number of pseudo-encodings get + * triggered while data can't be sent on the socket. + * + * NB throttle_output_offset can be zero during early protocol + * handshake, or from the job thread's VncState clone + */ + if (vs->throttle_output_offset != 0 && + (vs->output.offset / VNC_THROTTLE_OUTPUT_LIMIT_SCALE) > + vs->throttle_output_offset) { + trace_vnc_client_output_limit(vs, vs->ioc, vs->output.offset, + vs->throttle_output_offset); + vnc_disconnect_start(vs); + return; + } + buffer_reserve(&vs->output, len); + + if (vs->ioc != NULL && buffer_empty(&vs->output)) { + if (vs->ioc_tag) { + g_source_remove(vs->ioc_tag); + } + vs->ioc_tag = qio_channel_add_watch( + vs->ioc, G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_OUT, + vnc_client_io, vs, NULL); + } + + buffer_append(&vs->output, data, len); +} + +void vnc_write_s32(VncState *vs, int32_t value) +{ + vnc_write_u32(vs, *(uint32_t *)&value); +} + +void vnc_write_u32(VncState *vs, uint32_t value) +{ + uint8_t buf[4]; + + buf[0] = (value >> 24) & 0xFF; + buf[1] = (value >> 16) & 0xFF; + buf[2] = (value >> 8) & 0xFF; + buf[3] = value & 0xFF; + + vnc_write(vs, buf, 4); +} + +void vnc_write_u16(VncState *vs, uint16_t value) +{ + uint8_t buf[2]; + + buf[0] = (value >> 8) & 0xFF; + buf[1] = value & 0xFF; + + vnc_write(vs, buf, 2); +} + +void vnc_write_u8(VncState *vs, uint8_t value) +{ + vnc_write(vs, (char *)&value, 1); +} + +void vnc_flush(VncState *vs) +{ + vnc_lock_output(vs); + if (vs->ioc != NULL && vs->output.offset) { + vnc_client_write_locked(vs); + } + if (vs->disconnecting) { + if (vs->ioc_tag != 0) { + g_source_remove(vs->ioc_tag); + } + vs->ioc_tag = 0; + } + vnc_unlock_output(vs); +} + +static uint8_t read_u8(uint8_t *data, size_t offset) +{ + return data[offset]; +} + +static uint16_t read_u16(uint8_t *data, size_t offset) +{ + return ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF); +} + +static int32_t read_s32(uint8_t *data, size_t offset) +{ + return (int32_t)((data[offset] << 24) | (data[offset + 1] << 16) | + (data[offset + 2] << 8) | data[offset + 3]); +} + +uint32_t read_u32(uint8_t *data, size_t offset) +{ + return ((data[offset] << 24) | (data[offset + 1] << 16) | + (data[offset + 2] << 8) | data[offset + 3]); +} + +static void check_pointer_type_change(Notifier *notifier, void *data) +{ + VncState *vs = container_of(notifier, VncState, mouse_mode_notifier); + int absolute = qemu_input_is_absolute(); + + if (vnc_has_feature(vs, VNC_FEATURE_POINTER_TYPE_CHANGE) && vs->absolute != absolute) { + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE); + vnc_write_u8(vs, 0); + vnc_write_u16(vs, 1); + vnc_framebuffer_update(vs, absolute, 0, + pixman_image_get_width(vs->vd->server), + pixman_image_get_height(vs->vd->server), + VNC_ENCODING_POINTER_TYPE_CHANGE); + vnc_unlock_output(vs); + vnc_flush(vs); + } + vs->absolute = absolute; +} + +static void pointer_event(VncState *vs, int button_mask, int x, int y) +{ + static uint32_t bmap[INPUT_BUTTON__MAX] = { + [INPUT_BUTTON_LEFT] = 0x01, + [INPUT_BUTTON_MIDDLE] = 0x02, + [INPUT_BUTTON_RIGHT] = 0x04, + [INPUT_BUTTON_WHEEL_UP] = 0x08, + [INPUT_BUTTON_WHEEL_DOWN] = 0x10, + }; + QemuConsole *con = vs->vd->dcl.con; + int width = pixman_image_get_width(vs->vd->server); + int height = pixman_image_get_height(vs->vd->server); + + if (vs->last_bmask != button_mask) { + qemu_input_update_buttons(con, bmap, vs->last_bmask, button_mask); + vs->last_bmask = button_mask; + } + + if (vs->absolute) { + qemu_input_queue_abs(con, INPUT_AXIS_X, x, 0, width); + qemu_input_queue_abs(con, INPUT_AXIS_Y, y, 0, height); + } else if (vnc_has_feature(vs, VNC_FEATURE_POINTER_TYPE_CHANGE)) { + qemu_input_queue_rel(con, INPUT_AXIS_X, x - 0x7FFF); + qemu_input_queue_rel(con, INPUT_AXIS_Y, y - 0x7FFF); + } else { + if (vs->last_x != -1) { + qemu_input_queue_rel(con, INPUT_AXIS_X, x - vs->last_x); + qemu_input_queue_rel(con, INPUT_AXIS_Y, y - vs->last_y); + } + vs->last_x = x; + vs->last_y = y; + } + qemu_input_event_sync(); +} + +static void press_key(VncState *vs, QKeyCode qcode) +{ + qkbd_state_key_event(vs->vd->kbd, qcode, true); + qkbd_state_key_event(vs->vd->kbd, qcode, false); +} + +static void vnc_led_state_change(VncState *vs) +{ + if (!vnc_has_feature(vs, VNC_FEATURE_LED_STATE)) { + return; + } + + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE); + vnc_write_u8(vs, 0); + vnc_write_u16(vs, 1); + vnc_framebuffer_update(vs, 0, 0, 1, 1, VNC_ENCODING_LED_STATE); + vnc_write_u8(vs, vs->vd->ledstate); + vnc_unlock_output(vs); + vnc_flush(vs); +} + +static void kbd_leds(void *opaque, int ledstate) +{ + VncDisplay *vd = opaque; + VncState *client; + + trace_vnc_key_guest_leds((ledstate & QEMU_CAPS_LOCK_LED), + (ledstate & QEMU_NUM_LOCK_LED), + (ledstate & QEMU_SCROLL_LOCK_LED)); + + if (ledstate == vd->ledstate) { + return; + } + + vd->ledstate = ledstate; + + QTAILQ_FOREACH(client, &vd->clients, next) { + vnc_led_state_change(client); + } +} + +static void do_key_event(VncState *vs, int down, int keycode, int sym) +{ + QKeyCode qcode = qemu_input_key_number_to_qcode(keycode); + + /* QEMU console switch */ + switch (qcode) { + case Q_KEY_CODE_1 ... Q_KEY_CODE_9: /* '1' to '9' keys */ + if (vs->vd->dcl.con == NULL && down && + qkbd_state_modifier_get(vs->vd->kbd, QKBD_MOD_CTRL) && + qkbd_state_modifier_get(vs->vd->kbd, QKBD_MOD_ALT)) { + /* Reset the modifiers sent to the current console */ + qkbd_state_lift_all_keys(vs->vd->kbd); + console_select(qcode - Q_KEY_CODE_1); + return; + } + default: + break; + } + + /* Turn off the lock state sync logic if the client support the led + state extension. + */ + if (down && vs->vd->lock_key_sync && + !vnc_has_feature(vs, VNC_FEATURE_LED_STATE) && + keycode_is_keypad(vs->vd->kbd_layout, keycode)) { + /* If the numlock state needs to change then simulate an additional + keypress before sending this one. This will happen if the user + toggles numlock away from the VNC window. + */ + if (keysym_is_numlock(vs->vd->kbd_layout, sym & 0xFFFF)) { + if (!qkbd_state_modifier_get(vs->vd->kbd, QKBD_MOD_NUMLOCK)) { + trace_vnc_key_sync_numlock(true); + press_key(vs, Q_KEY_CODE_NUM_LOCK); + } + } else { + if (qkbd_state_modifier_get(vs->vd->kbd, QKBD_MOD_NUMLOCK)) { + trace_vnc_key_sync_numlock(false); + press_key(vs, Q_KEY_CODE_NUM_LOCK); + } + } + } + + if (down && vs->vd->lock_key_sync && + !vnc_has_feature(vs, VNC_FEATURE_LED_STATE) && + ((sym >= 'A' && sym <= 'Z') || (sym >= 'a' && sym <= 'z'))) { + /* If the capslock state needs to change then simulate an additional + keypress before sending this one. This will happen if the user + toggles capslock away from the VNC window. + */ + int uppercase = !!(sym >= 'A' && sym <= 'Z'); + bool shift = qkbd_state_modifier_get(vs->vd->kbd, QKBD_MOD_SHIFT); + bool capslock = qkbd_state_modifier_get(vs->vd->kbd, QKBD_MOD_CAPSLOCK); + if (capslock) { + if (uppercase == shift) { + trace_vnc_key_sync_capslock(false); + press_key(vs, Q_KEY_CODE_CAPS_LOCK); + } + } else { + if (uppercase != shift) { + trace_vnc_key_sync_capslock(true); + press_key(vs, Q_KEY_CODE_CAPS_LOCK); + } + } + } + + qkbd_state_key_event(vs->vd->kbd, qcode, down); + if (!qemu_console_is_graphic(NULL)) { + bool numlock = qkbd_state_modifier_get(vs->vd->kbd, QKBD_MOD_NUMLOCK); + bool control = qkbd_state_modifier_get(vs->vd->kbd, QKBD_MOD_CTRL); + /* QEMU console emulation */ + if (down) { + switch (keycode) { + case 0x2a: /* Left Shift */ + case 0x36: /* Right Shift */ + case 0x1d: /* Left CTRL */ + case 0x9d: /* Right CTRL */ + case 0x38: /* Left ALT */ + case 0xb8: /* Right ALT */ + break; + case 0xc8: + kbd_put_keysym(QEMU_KEY_UP); + break; + case 0xd0: + kbd_put_keysym(QEMU_KEY_DOWN); + break; + case 0xcb: + kbd_put_keysym(QEMU_KEY_LEFT); + break; + case 0xcd: + kbd_put_keysym(QEMU_KEY_RIGHT); + break; + case 0xd3: + kbd_put_keysym(QEMU_KEY_DELETE); + break; + case 0xc7: + kbd_put_keysym(QEMU_KEY_HOME); + break; + case 0xcf: + kbd_put_keysym(QEMU_KEY_END); + break; + case 0xc9: + kbd_put_keysym(QEMU_KEY_PAGEUP); + break; + case 0xd1: + kbd_put_keysym(QEMU_KEY_PAGEDOWN); + break; + + case 0x47: + kbd_put_keysym(numlock ? '7' : QEMU_KEY_HOME); + break; + case 0x48: + kbd_put_keysym(numlock ? '8' : QEMU_KEY_UP); + break; + case 0x49: + kbd_put_keysym(numlock ? '9' : QEMU_KEY_PAGEUP); + break; + case 0x4b: + kbd_put_keysym(numlock ? '4' : QEMU_KEY_LEFT); + break; + case 0x4c: + kbd_put_keysym('5'); + break; + case 0x4d: + kbd_put_keysym(numlock ? '6' : QEMU_KEY_RIGHT); + break; + case 0x4f: + kbd_put_keysym(numlock ? '1' : QEMU_KEY_END); + break; + case 0x50: + kbd_put_keysym(numlock ? '2' : QEMU_KEY_DOWN); + break; + case 0x51: + kbd_put_keysym(numlock ? '3' : QEMU_KEY_PAGEDOWN); + break; + case 0x52: + kbd_put_keysym('0'); + break; + case 0x53: + kbd_put_keysym(numlock ? '.' : QEMU_KEY_DELETE); + break; + + case 0xb5: + kbd_put_keysym('/'); + break; + case 0x37: + kbd_put_keysym('*'); + break; + case 0x4a: + kbd_put_keysym('-'); + break; + case 0x4e: + kbd_put_keysym('+'); + break; + case 0x9c: + kbd_put_keysym('\n'); + break; + + default: + if (control) { + kbd_put_keysym(sym & 0x1f); + } else { + kbd_put_keysym(sym); + } + break; + } + } + } +} + +static const char *code2name(int keycode) +{ + return QKeyCode_str(qemu_input_key_number_to_qcode(keycode)); +} + +static void key_event(VncState *vs, int down, uint32_t sym) +{ + int keycode; + int lsym = sym; + + if (lsym >= 'A' && lsym <= 'Z' && qemu_console_is_graphic(NULL)) { + lsym = lsym - 'A' + 'a'; + } + + keycode = keysym2scancode(vs->vd->kbd_layout, lsym & 0xFFFF, + vs->vd->kbd, down) & SCANCODE_KEYMASK; + trace_vnc_key_event_map(down, sym, keycode, code2name(keycode)); + do_key_event(vs, down, keycode, sym); +} + +static void ext_key_event(VncState *vs, int down, + uint32_t sym, uint16_t keycode) +{ + /* if the user specifies a keyboard layout, always use it */ + if (keyboard_layout) { + key_event(vs, down, sym); + } else { + trace_vnc_key_event_ext(down, sym, keycode, code2name(keycode)); + do_key_event(vs, down, keycode, sym); + } +} + +static void framebuffer_update_request(VncState *vs, int incremental, + int x, int y, int w, int h) +{ + if (incremental) { + if (vs->update != VNC_STATE_UPDATE_FORCE) { + vs->update = VNC_STATE_UPDATE_INCREMENTAL; + } + } else { + vs->update = VNC_STATE_UPDATE_FORCE; + vnc_set_area_dirty(vs->dirty, vs->vd, x, y, w, h); + if (vnc_has_feature(vs, VNC_FEATURE_RESIZE_EXT)) { + vnc_desktop_resize_ext(vs, 0); + } + } +} + +static void send_ext_key_event_ack(VncState *vs) +{ + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE); + vnc_write_u8(vs, 0); + vnc_write_u16(vs, 1); + vnc_framebuffer_update(vs, 0, 0, + pixman_image_get_width(vs->vd->server), + pixman_image_get_height(vs->vd->server), + VNC_ENCODING_EXT_KEY_EVENT); + vnc_unlock_output(vs); + vnc_flush(vs); +} + +static void send_ext_audio_ack(VncState *vs) +{ + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE); + vnc_write_u8(vs, 0); + vnc_write_u16(vs, 1); + vnc_framebuffer_update(vs, 0, 0, + pixman_image_get_width(vs->vd->server), + pixman_image_get_height(vs->vd->server), + VNC_ENCODING_AUDIO); + vnc_unlock_output(vs); + vnc_flush(vs); +} + +static void send_xvp_message(VncState *vs, int code) +{ + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_XVP); + vnc_write_u8(vs, 0); /* pad */ + vnc_write_u8(vs, 1); /* version */ + vnc_write_u8(vs, code); + vnc_unlock_output(vs); + vnc_flush(vs); +} + +static void set_encodings(VncState *vs, int32_t *encodings, size_t n_encodings) +{ + int i; + unsigned int enc = 0; + + vs->features = 0; + vs->vnc_encoding = 0; + vs->tight->compression = 9; + vs->tight->quality = -1; /* Lossless by default */ + vs->absolute = -1; + + /* + * Start from the end because the encodings are sent in order of preference. + * This way the preferred encoding (first encoding defined in the array) + * will be set at the end of the loop. + */ + for (i = n_encodings - 1; i >= 0; i--) { + enc = encodings[i]; + switch (enc) { + case VNC_ENCODING_RAW: + vs->vnc_encoding = enc; + break; + case VNC_ENCODING_HEXTILE: + vs->features |= VNC_FEATURE_HEXTILE_MASK; + vs->vnc_encoding = enc; + break; + case VNC_ENCODING_TIGHT: + vs->features |= VNC_FEATURE_TIGHT_MASK; + vs->vnc_encoding = enc; + break; +#ifdef CONFIG_VNC_PNG + case VNC_ENCODING_TIGHT_PNG: + vs->features |= VNC_FEATURE_TIGHT_PNG_MASK; + vs->vnc_encoding = enc; + break; +#endif + case VNC_ENCODING_ZLIB: + /* + * VNC_ENCODING_ZRLE compresses better than VNC_ENCODING_ZLIB. + * So prioritize ZRLE, even if the client hints that it prefers + * ZLIB. + */ + if ((vs->features & VNC_FEATURE_ZRLE_MASK) == 0) { + vs->features |= VNC_FEATURE_ZLIB_MASK; + vs->vnc_encoding = enc; + } + break; + case VNC_ENCODING_ZRLE: + vs->features |= VNC_FEATURE_ZRLE_MASK; + vs->vnc_encoding = enc; + break; + case VNC_ENCODING_ZYWRLE: + vs->features |= VNC_FEATURE_ZYWRLE_MASK; + vs->vnc_encoding = enc; + break; + case VNC_ENCODING_DESKTOPRESIZE: + vs->features |= VNC_FEATURE_RESIZE_MASK; + break; + case VNC_ENCODING_DESKTOP_RESIZE_EXT: + vs->features |= VNC_FEATURE_RESIZE_EXT_MASK; + break; + case VNC_ENCODING_POINTER_TYPE_CHANGE: + vs->features |= VNC_FEATURE_POINTER_TYPE_CHANGE_MASK; + break; + case VNC_ENCODING_RICH_CURSOR: + vs->features |= VNC_FEATURE_RICH_CURSOR_MASK; + break; + case VNC_ENCODING_ALPHA_CURSOR: + vs->features |= VNC_FEATURE_ALPHA_CURSOR_MASK; + break; + case VNC_ENCODING_EXT_KEY_EVENT: + send_ext_key_event_ack(vs); + break; + case VNC_ENCODING_AUDIO: + send_ext_audio_ack(vs); + break; + case VNC_ENCODING_WMVi: + vs->features |= VNC_FEATURE_WMVI_MASK; + break; + case VNC_ENCODING_LED_STATE: + vs->features |= VNC_FEATURE_LED_STATE_MASK; + break; + case VNC_ENCODING_XVP: + if (vs->vd->power_control) { + vs->features |= VNC_FEATURE_XVP; + send_xvp_message(vs, VNC_XVP_CODE_INIT); + } + break; + case VNC_ENCODING_CLIPBOARD_EXT: + vs->features |= VNC_FEATURE_CLIPBOARD_EXT_MASK; + vnc_server_cut_text_caps(vs); + break; + case VNC_ENCODING_COMPRESSLEVEL0 ... VNC_ENCODING_COMPRESSLEVEL0 + 9: + vs->tight->compression = (enc & 0x0F); + break; + case VNC_ENCODING_QUALITYLEVEL0 ... VNC_ENCODING_QUALITYLEVEL0 + 9: + if (vs->vd->lossy) { + vs->tight->quality = (enc & 0x0F); + } + break; + default: + VNC_DEBUG("Unknown encoding: %d (0x%.8x): %d\n", i, enc, enc); + break; + } + } + vnc_desktop_resize(vs); + check_pointer_type_change(&vs->mouse_mode_notifier, NULL); + vnc_led_state_change(vs); + vnc_cursor_define(vs); +} + +static void set_pixel_conversion(VncState *vs) +{ + pixman_format_code_t fmt = qemu_pixman_get_format(&vs->client_pf); + + if (fmt == VNC_SERVER_FB_FORMAT) { + vs->write_pixels = vnc_write_pixels_copy; + vnc_hextile_set_pixel_conversion(vs, 0); + } else { + vs->write_pixels = vnc_write_pixels_generic; + vnc_hextile_set_pixel_conversion(vs, 1); + } +} + +static void send_color_map(VncState *vs) +{ + int i; + + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_SET_COLOUR_MAP_ENTRIES); + vnc_write_u8(vs, 0); /* padding */ + vnc_write_u16(vs, 0); /* first color */ + vnc_write_u16(vs, 256); /* # of colors */ + + for (i = 0; i < 256; i++) { + PixelFormat *pf = &vs->client_pf; + + vnc_write_u16(vs, (((i >> pf->rshift) & pf->rmax) << (16 - pf->rbits))); + vnc_write_u16(vs, (((i >> pf->gshift) & pf->gmax) << (16 - pf->gbits))); + vnc_write_u16(vs, (((i >> pf->bshift) & pf->bmax) << (16 - pf->bbits))); + } + vnc_unlock_output(vs); +} + +static void set_pixel_format(VncState *vs, int bits_per_pixel, + int big_endian_flag, int true_color_flag, + int red_max, int green_max, int blue_max, + int red_shift, int green_shift, int blue_shift) +{ + if (!true_color_flag) { + /* Expose a reasonable default 256 color map */ + bits_per_pixel = 8; + red_max = 7; + green_max = 7; + blue_max = 3; + red_shift = 0; + green_shift = 3; + blue_shift = 6; + } + + switch (bits_per_pixel) { + case 8: + case 16: + case 32: + break; + default: + vnc_client_error(vs); + return; + } + + vs->client_pf.rmax = red_max ? red_max : 0xFF; + vs->client_pf.rbits = ctpopl(red_max); + vs->client_pf.rshift = red_shift; + vs->client_pf.rmask = red_max << red_shift; + vs->client_pf.gmax = green_max ? green_max : 0xFF; + vs->client_pf.gbits = ctpopl(green_max); + vs->client_pf.gshift = green_shift; + vs->client_pf.gmask = green_max << green_shift; + vs->client_pf.bmax = blue_max ? blue_max : 0xFF; + vs->client_pf.bbits = ctpopl(blue_max); + vs->client_pf.bshift = blue_shift; + vs->client_pf.bmask = blue_max << blue_shift; + vs->client_pf.bits_per_pixel = bits_per_pixel; + vs->client_pf.bytes_per_pixel = bits_per_pixel / 8; + vs->client_pf.depth = bits_per_pixel == 32 ? 24 : bits_per_pixel; + vs->client_be = big_endian_flag; + + if (!true_color_flag) { + send_color_map(vs); + } + + set_pixel_conversion(vs); + + graphic_hw_invalidate(vs->vd->dcl.con); + graphic_hw_update(vs->vd->dcl.con); +} + +static void pixel_format_message (VncState *vs) { + char pad[3] = { 0, 0, 0 }; + + vs->client_pf = qemu_default_pixelformat(32); + + vnc_write_u8(vs, vs->client_pf.bits_per_pixel); /* bits-per-pixel */ + vnc_write_u8(vs, vs->client_pf.depth); /* depth */ + +#ifdef HOST_WORDS_BIGENDIAN + vnc_write_u8(vs, 1); /* big-endian-flag */ +#else + vnc_write_u8(vs, 0); /* big-endian-flag */ +#endif + vnc_write_u8(vs, 1); /* true-color-flag */ + vnc_write_u16(vs, vs->client_pf.rmax); /* red-max */ + vnc_write_u16(vs, vs->client_pf.gmax); /* green-max */ + vnc_write_u16(vs, vs->client_pf.bmax); /* blue-max */ + vnc_write_u8(vs, vs->client_pf.rshift); /* red-shift */ + vnc_write_u8(vs, vs->client_pf.gshift); /* green-shift */ + vnc_write_u8(vs, vs->client_pf.bshift); /* blue-shift */ + vnc_write(vs, pad, 3); /* padding */ + + vnc_hextile_set_pixel_conversion(vs, 0); + vs->write_pixels = vnc_write_pixels_copy; +} + +static void vnc_colordepth(VncState *vs) +{ + if (vnc_has_feature(vs, VNC_FEATURE_WMVI)) { + /* Sending a WMVi message to notify the client*/ + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE); + vnc_write_u8(vs, 0); + vnc_write_u16(vs, 1); /* number of rects */ + vnc_framebuffer_update(vs, 0, 0, + vs->client_width, + vs->client_height, + VNC_ENCODING_WMVi); + pixel_format_message(vs); + vnc_unlock_output(vs); + vnc_flush(vs); + } else { + set_pixel_conversion(vs); + } +} + +static int protocol_client_msg(VncState *vs, uint8_t *data, size_t len) +{ + int i; + uint16_t limit; + uint32_t freq; + VncDisplay *vd = vs->vd; + + if (data[0] > 3) { + update_displaychangelistener(&vd->dcl, VNC_REFRESH_INTERVAL_BASE); + } + + switch (data[0]) { + case VNC_MSG_CLIENT_SET_PIXEL_FORMAT: + if (len == 1) + return 20; + + set_pixel_format(vs, read_u8(data, 4), + read_u8(data, 6), read_u8(data, 7), + read_u16(data, 8), read_u16(data, 10), + read_u16(data, 12), read_u8(data, 14), + read_u8(data, 15), read_u8(data, 16)); + break; + case VNC_MSG_CLIENT_SET_ENCODINGS: + if (len == 1) + return 4; + + if (len == 4) { + limit = read_u16(data, 2); + if (limit > 0) + return 4 + (limit * 4); + } else + limit = read_u16(data, 2); + + for (i = 0; i < limit; i++) { + int32_t val = read_s32(data, 4 + (i * 4)); + memcpy(data + 4 + (i * 4), &val, sizeof(val)); + } + + set_encodings(vs, (int32_t *)(data + 4), limit); + break; + case VNC_MSG_CLIENT_FRAMEBUFFER_UPDATE_REQUEST: + if (len == 1) + return 10; + + framebuffer_update_request(vs, + read_u8(data, 1), read_u16(data, 2), read_u16(data, 4), + read_u16(data, 6), read_u16(data, 8)); + break; + case VNC_MSG_CLIENT_KEY_EVENT: + if (len == 1) + return 8; + + key_event(vs, read_u8(data, 1), read_u32(data, 4)); + break; + case VNC_MSG_CLIENT_POINTER_EVENT: + if (len == 1) + return 6; + + pointer_event(vs, read_u8(data, 1), read_u16(data, 2), read_u16(data, 4)); + break; + case VNC_MSG_CLIENT_CUT_TEXT: + if (len == 1) { + return 8; + } + if (len == 8) { + uint32_t dlen = abs(read_s32(data, 4)); + if (dlen > (1 << 20)) { + error_report("vnc: client_cut_text msg payload has %u bytes" + " which exceeds our limit of 1MB.", dlen); + vnc_client_error(vs); + break; + } + if (dlen > 0) { + return 8 + dlen; + } + } + + if (read_s32(data, 4) < 0) { + vnc_client_cut_text_ext(vs, abs(read_s32(data, 4)), + read_u32(data, 8), data + 12); + break; + } + vnc_client_cut_text(vs, read_u32(data, 4), data + 8); + break; + case VNC_MSG_CLIENT_XVP: + if (!(vs->features & VNC_FEATURE_XVP)) { + error_report("vnc: xvp client message while disabled"); + vnc_client_error(vs); + break; + } + if (len == 1) { + return 4; + } + if (len == 4) { + uint8_t version = read_u8(data, 2); + uint8_t action = read_u8(data, 3); + + if (version != 1) { + error_report("vnc: xvp client message version %d != 1", + version); + vnc_client_error(vs); + break; + } + + switch (action) { + case VNC_XVP_ACTION_SHUTDOWN: + qemu_system_powerdown_request(); + break; + case VNC_XVP_ACTION_REBOOT: + send_xvp_message(vs, VNC_XVP_CODE_FAIL); + break; + case VNC_XVP_ACTION_RESET: + qemu_system_reset_request(SHUTDOWN_CAUSE_HOST_QMP_SYSTEM_RESET); + break; + default: + send_xvp_message(vs, VNC_XVP_CODE_FAIL); + break; + } + } + break; + case VNC_MSG_CLIENT_QEMU: + if (len == 1) + return 2; + + switch (read_u8(data, 1)) { + case VNC_MSG_CLIENT_QEMU_EXT_KEY_EVENT: + if (len == 2) + return 12; + + ext_key_event(vs, read_u16(data, 2), + read_u32(data, 4), read_u32(data, 8)); + break; + case VNC_MSG_CLIENT_QEMU_AUDIO: + if (len == 2) + return 4; + + switch (read_u16 (data, 2)) { + case VNC_MSG_CLIENT_QEMU_AUDIO_ENABLE: + trace_vnc_msg_client_audio_enable(vs, vs->ioc); + audio_add(vs); + break; + case VNC_MSG_CLIENT_QEMU_AUDIO_DISABLE: + trace_vnc_msg_client_audio_disable(vs, vs->ioc); + audio_del(vs); + break; + case VNC_MSG_CLIENT_QEMU_AUDIO_SET_FORMAT: + if (len == 4) + return 10; + switch (read_u8(data, 4)) { + case 0: vs->as.fmt = AUDIO_FORMAT_U8; break; + case 1: vs->as.fmt = AUDIO_FORMAT_S8; break; + case 2: vs->as.fmt = AUDIO_FORMAT_U16; break; + case 3: vs->as.fmt = AUDIO_FORMAT_S16; break; + case 4: vs->as.fmt = AUDIO_FORMAT_U32; break; + case 5: vs->as.fmt = AUDIO_FORMAT_S32; break; + default: + VNC_DEBUG("Invalid audio format %d\n", read_u8(data, 4)); + vnc_client_error(vs); + break; + } + vs->as.nchannels = read_u8(data, 5); + if (vs->as.nchannels != 1 && vs->as.nchannels != 2) { + VNC_DEBUG("Invalid audio channel count %d\n", + read_u8(data, 5)); + vnc_client_error(vs); + break; + } + freq = read_u32(data, 6); + /* No official limit for protocol, but 48khz is a sensible + * upper bound for trustworthy clients, and this limit + * protects calculations involving 'vs->as.freq' later. + */ + if (freq > 48000) { + VNC_DEBUG("Invalid audio frequency %u > 48000", freq); + vnc_client_error(vs); + break; + } + vs->as.freq = freq; + trace_vnc_msg_client_audio_format( + vs, vs->ioc, vs->as.fmt, vs->as.nchannels, vs->as.freq); + break; + default: + VNC_DEBUG("Invalid audio message %d\n", read_u8(data, 4)); + vnc_client_error(vs); + break; + } + break; + + default: + VNC_DEBUG("Msg: %d\n", read_u16(data, 0)); + vnc_client_error(vs); + break; + } + break; + case VNC_MSG_CLIENT_SET_DESKTOP_SIZE: + { + size_t size; + uint8_t screens; + int w, h; + + if (len < 8) { + return 8; + } + + screens = read_u8(data, 6); + size = 8 + screens * 16; + if (len < size) { + return size; + } + w = read_u16(data, 2); + h = read_u16(data, 4); + + trace_vnc_msg_client_set_desktop_size(vs, vs->ioc, w, h, screens); + if (dpy_ui_info_supported(vs->vd->dcl.con)) { + QemuUIInfo info; + memset(&info, 0, sizeof(info)); + info.width = w; + info.height = h; + dpy_set_ui_info(vs->vd->dcl.con, &info); + vnc_desktop_resize_ext(vs, 4 /* Request forwarded */); + } else { + vnc_desktop_resize_ext(vs, 3 /* Invalid screen layout */); + } + + break; + } + default: + VNC_DEBUG("Msg: %d\n", data[0]); + vnc_client_error(vs); + break; + } + + vnc_update_throttle_offset(vs); + vnc_read_when(vs, protocol_client_msg, 1); + return 0; +} + +static int protocol_client_init(VncState *vs, uint8_t *data, size_t len) +{ + char buf[1024]; + VncShareMode mode; + int size; + + mode = data[0] ? VNC_SHARE_MODE_SHARED : VNC_SHARE_MODE_EXCLUSIVE; + switch (vs->vd->share_policy) { + case VNC_SHARE_POLICY_IGNORE: + /* + * Ignore the shared flag. Nothing to do here. + * + * Doesn't conform to the rfb spec but is traditional qemu + * behavior, thus left here as option for compatibility + * reasons. + */ + break; + case VNC_SHARE_POLICY_ALLOW_EXCLUSIVE: + /* + * Policy: Allow clients ask for exclusive access. + * + * Implementation: When a client asks for exclusive access, + * disconnect all others. Shared connects are allowed as long + * as no exclusive connection exists. + * + * This is how the rfb spec suggests to handle the shared flag. + */ + if (mode == VNC_SHARE_MODE_EXCLUSIVE) { + VncState *client; + QTAILQ_FOREACH(client, &vs->vd->clients, next) { + if (vs == client) { + continue; + } + if (client->share_mode != VNC_SHARE_MODE_EXCLUSIVE && + client->share_mode != VNC_SHARE_MODE_SHARED) { + continue; + } + vnc_disconnect_start(client); + } + } + if (mode == VNC_SHARE_MODE_SHARED) { + if (vs->vd->num_exclusive > 0) { + vnc_disconnect_start(vs); + return 0; + } + } + break; + case VNC_SHARE_POLICY_FORCE_SHARED: + /* + * Policy: Shared connects only. + * Implementation: Disallow clients asking for exclusive access. + * + * Useful for shared desktop sessions where you don't want + * someone forgetting to say -shared when running the vnc + * client disconnect everybody else. + */ + if (mode == VNC_SHARE_MODE_EXCLUSIVE) { + vnc_disconnect_start(vs); + return 0; + } + break; + } + vnc_set_share_mode(vs, mode); + + if (vs->vd->num_shared > vs->vd->connections_limit) { + vnc_disconnect_start(vs); + return 0; + } + + assert(pixman_image_get_width(vs->vd->server) < 65536 && + pixman_image_get_width(vs->vd->server) >= 0); + assert(pixman_image_get_height(vs->vd->server) < 65536 && + pixman_image_get_height(vs->vd->server) >= 0); + vs->client_width = pixman_image_get_width(vs->vd->server); + vs->client_height = pixman_image_get_height(vs->vd->server); + vnc_write_u16(vs, vs->client_width); + vnc_write_u16(vs, vs->client_height); + + pixel_format_message(vs); + + if (qemu_name) { + size = snprintf(buf, sizeof(buf), "QEMU (%s)", qemu_name); + if (size > sizeof(buf)) { + size = sizeof(buf); + } + } else { + size = snprintf(buf, sizeof(buf), "QEMU"); + } + + vnc_write_u32(vs, size); + vnc_write(vs, buf, size); + vnc_flush(vs); + + vnc_client_cache_auth(vs); + vnc_qmp_event(vs, QAPI_EVENT_VNC_INITIALIZED); + + vnc_read_when(vs, protocol_client_msg, 1); + + return 0; +} + +void start_client_init(VncState *vs) +{ + vnc_read_when(vs, protocol_client_init, 1); +} + +static void authentication_failed(VncState *vs) +{ + vnc_write_u32(vs, 1); /* Reject auth */ + if (vs->minor >= 8) { + static const char err[] = "Authentication failed"; + vnc_write_u32(vs, sizeof(err)); + vnc_write(vs, err, sizeof(err)); + } + vnc_flush(vs); + vnc_client_error(vs); +} + +static void +vnc_munge_des_rfb_key(unsigned char *key, size_t nkey) +{ + size_t i; + for (i = 0; i < nkey; i++) { + uint8_t r = key[i]; + r = (r & 0xf0) >> 4 | (r & 0x0f) << 4; + r = (r & 0xcc) >> 2 | (r & 0x33) << 2; + r = (r & 0xaa) >> 1 | (r & 0x55) << 1; + key[i] = r; + } +} + +static int protocol_client_auth_vnc(VncState *vs, uint8_t *data, size_t len) +{ + unsigned char response[VNC_AUTH_CHALLENGE_SIZE]; + size_t i, pwlen; + unsigned char key[8]; + time_t now = time(NULL); + QCryptoCipher *cipher = NULL; + Error *err = NULL; + + if (!vs->vd->password) { + trace_vnc_auth_fail(vs, vs->auth, "password is not set", ""); + goto reject; + } + if (vs->vd->expires < now) { + trace_vnc_auth_fail(vs, vs->auth, "password is expired", ""); + goto reject; + } + + memcpy(response, vs->challenge, VNC_AUTH_CHALLENGE_SIZE); + + /* Calculate the expected challenge response */ + pwlen = strlen(vs->vd->password); + for (i=0; i<sizeof(key); i++) + key[i] = i<pwlen ? vs->vd->password[i] : 0; + vnc_munge_des_rfb_key(key, sizeof(key)); + + cipher = qcrypto_cipher_new( + QCRYPTO_CIPHER_ALG_DES, + QCRYPTO_CIPHER_MODE_ECB, + key, G_N_ELEMENTS(key), + &err); + if (!cipher) { + trace_vnc_auth_fail(vs, vs->auth, "cannot create cipher", + error_get_pretty(err)); + error_free(err); + goto reject; + } + + if (qcrypto_cipher_encrypt(cipher, + vs->challenge, + response, + VNC_AUTH_CHALLENGE_SIZE, + &err) < 0) { + trace_vnc_auth_fail(vs, vs->auth, "cannot encrypt challenge response", + error_get_pretty(err)); + error_free(err); + goto reject; + } + + /* Compare expected vs actual challenge response */ + if (memcmp(response, data, VNC_AUTH_CHALLENGE_SIZE) != 0) { + trace_vnc_auth_fail(vs, vs->auth, "mis-matched challenge response", ""); + goto reject; + } else { + trace_vnc_auth_pass(vs, vs->auth); + vnc_write_u32(vs, 0); /* Accept auth */ + vnc_flush(vs); + + start_client_init(vs); + } + + qcrypto_cipher_free(cipher); + return 0; + +reject: + authentication_failed(vs); + qcrypto_cipher_free(cipher); + return 0; +} + +void start_auth_vnc(VncState *vs) +{ + Error *err = NULL; + + if (qcrypto_random_bytes(vs->challenge, sizeof(vs->challenge), &err)) { + trace_vnc_auth_fail(vs, vs->auth, "cannot get random bytes", + error_get_pretty(err)); + error_free(err); + authentication_failed(vs); + return; + } + + /* Send client a 'random' challenge */ + vnc_write(vs, vs->challenge, sizeof(vs->challenge)); + vnc_flush(vs); + + vnc_read_when(vs, protocol_client_auth_vnc, sizeof(vs->challenge)); +} + + +static int protocol_client_auth(VncState *vs, uint8_t *data, size_t len) +{ + /* We only advertise 1 auth scheme at a time, so client + * must pick the one we sent. Verify this */ + if (data[0] != vs->auth) { /* Reject auth */ + trace_vnc_auth_reject(vs, vs->auth, (int)data[0]); + authentication_failed(vs); + } else { /* Accept requested auth */ + trace_vnc_auth_start(vs, vs->auth); + switch (vs->auth) { + case VNC_AUTH_NONE: + if (vs->minor >= 8) { + vnc_write_u32(vs, 0); /* Accept auth completion */ + vnc_flush(vs); + } + trace_vnc_auth_pass(vs, vs->auth); + start_client_init(vs); + break; + + case VNC_AUTH_VNC: + start_auth_vnc(vs); + break; + + case VNC_AUTH_VENCRYPT: + start_auth_vencrypt(vs); + break; + +#ifdef CONFIG_VNC_SASL + case VNC_AUTH_SASL: + start_auth_sasl(vs); + break; +#endif /* CONFIG_VNC_SASL */ + + default: /* Should not be possible, but just in case */ + trace_vnc_auth_fail(vs, vs->auth, "Unhandled auth method", ""); + authentication_failed(vs); + } + } + return 0; +} + +static int protocol_version(VncState *vs, uint8_t *version, size_t len) +{ + char local[13]; + + memcpy(local, version, 12); + local[12] = 0; + + if (sscanf(local, "RFB %03d.%03d\n", &vs->major, &vs->minor) != 2) { + VNC_DEBUG("Malformed protocol version %s\n", local); + vnc_client_error(vs); + return 0; + } + VNC_DEBUG("Client request protocol version %d.%d\n", vs->major, vs->minor); + if (vs->major != 3 || + (vs->minor != 3 && + vs->minor != 4 && + vs->minor != 5 && + vs->minor != 7 && + vs->minor != 8)) { + VNC_DEBUG("Unsupported client version\n"); + vnc_write_u32(vs, VNC_AUTH_INVALID); + vnc_flush(vs); + vnc_client_error(vs); + return 0; + } + /* Some broken clients report v3.4 or v3.5, which spec requires to be treated + * as equivalent to v3.3 by servers + */ + if (vs->minor == 4 || vs->minor == 5) + vs->minor = 3; + + if (vs->minor == 3) { + trace_vnc_auth_start(vs, vs->auth); + if (vs->auth == VNC_AUTH_NONE) { + vnc_write_u32(vs, vs->auth); + vnc_flush(vs); + trace_vnc_auth_pass(vs, vs->auth); + start_client_init(vs); + } else if (vs->auth == VNC_AUTH_VNC) { + VNC_DEBUG("Tell client VNC auth\n"); + vnc_write_u32(vs, vs->auth); + vnc_flush(vs); + start_auth_vnc(vs); + } else { + trace_vnc_auth_fail(vs, vs->auth, + "Unsupported auth method for v3.3", ""); + vnc_write_u32(vs, VNC_AUTH_INVALID); + vnc_flush(vs); + vnc_client_error(vs); + } + } else { + vnc_write_u8(vs, 1); /* num auth */ + vnc_write_u8(vs, vs->auth); + vnc_read_when(vs, protocol_client_auth, 1); + vnc_flush(vs); + } + + return 0; +} + +static VncRectStat *vnc_stat_rect(VncDisplay *vd, int x, int y) +{ + struct VncSurface *vs = &vd->guest; + + return &vs->stats[y / VNC_STAT_RECT][x / VNC_STAT_RECT]; +} + +void vnc_sent_lossy_rect(VncState *vs, int x, int y, int w, int h) +{ + int i, j; + + w = (x + w) / VNC_STAT_RECT; + h = (y + h) / VNC_STAT_RECT; + x /= VNC_STAT_RECT; + y /= VNC_STAT_RECT; + + for (j = y; j <= h; j++) { + for (i = x; i <= w; i++) { + vs->lossy_rect[j][i] = 1; + } + } +} + +static int vnc_refresh_lossy_rect(VncDisplay *vd, int x, int y) +{ + VncState *vs; + int sty = y / VNC_STAT_RECT; + int stx = x / VNC_STAT_RECT; + int has_dirty = 0; + + y = QEMU_ALIGN_DOWN(y, VNC_STAT_RECT); + x = QEMU_ALIGN_DOWN(x, VNC_STAT_RECT); + + QTAILQ_FOREACH(vs, &vd->clients, next) { + int j; + + /* kernel send buffers are full -> refresh later */ + if (vs->output.offset) { + continue; + } + + if (!vs->lossy_rect[sty][stx]) { + continue; + } + + vs->lossy_rect[sty][stx] = 0; + for (j = 0; j < VNC_STAT_RECT; ++j) { + bitmap_set(vs->dirty[y + j], + x / VNC_DIRTY_PIXELS_PER_BIT, + VNC_STAT_RECT / VNC_DIRTY_PIXELS_PER_BIT); + } + has_dirty++; + } + + return has_dirty; +} + +static int vnc_update_stats(VncDisplay *vd, struct timeval * tv) +{ + int width = MIN(pixman_image_get_width(vd->guest.fb), + pixman_image_get_width(vd->server)); + int height = MIN(pixman_image_get_height(vd->guest.fb), + pixman_image_get_height(vd->server)); + int x, y; + struct timeval res; + int has_dirty = 0; + + for (y = 0; y < height; y += VNC_STAT_RECT) { + for (x = 0; x < width; x += VNC_STAT_RECT) { + VncRectStat *rect = vnc_stat_rect(vd, x, y); + + rect->updated = false; + } + } + + qemu_timersub(tv, &VNC_REFRESH_STATS, &res); + + if (timercmp(&vd->guest.last_freq_check, &res, >)) { + return has_dirty; + } + vd->guest.last_freq_check = *tv; + + for (y = 0; y < height; y += VNC_STAT_RECT) { + for (x = 0; x < width; x += VNC_STAT_RECT) { + VncRectStat *rect= vnc_stat_rect(vd, x, y); + int count = ARRAY_SIZE(rect->times); + struct timeval min, max; + + if (!timerisset(&rect->times[count - 1])) { + continue ; + } + + max = rect->times[(rect->idx + count - 1) % count]; + qemu_timersub(tv, &max, &res); + + if (timercmp(&res, &VNC_REFRESH_LOSSY, >)) { + rect->freq = 0; + has_dirty += vnc_refresh_lossy_rect(vd, x, y); + memset(rect->times, 0, sizeof (rect->times)); + continue ; + } + + min = rect->times[rect->idx]; + max = rect->times[(rect->idx + count - 1) % count]; + qemu_timersub(&max, &min, &res); + + rect->freq = res.tv_sec + res.tv_usec / 1000000.; + rect->freq /= count; + rect->freq = 1. / rect->freq; + } + } + return has_dirty; +} + +double vnc_update_freq(VncState *vs, int x, int y, int w, int h) +{ + int i, j; + double total = 0; + int num = 0; + + x = QEMU_ALIGN_DOWN(x, VNC_STAT_RECT); + y = QEMU_ALIGN_DOWN(y, VNC_STAT_RECT); + + for (j = y; j <= y + h; j += VNC_STAT_RECT) { + for (i = x; i <= x + w; i += VNC_STAT_RECT) { + total += vnc_stat_rect(vs->vd, i, j)->freq; + num++; + } + } + + if (num) { + return total / num; + } else { + return 0; + } +} + +static void vnc_rect_updated(VncDisplay *vd, int x, int y, struct timeval * tv) +{ + VncRectStat *rect; + + rect = vnc_stat_rect(vd, x, y); + if (rect->updated) { + return ; + } + rect->times[rect->idx] = *tv; + rect->idx = (rect->idx + 1) % ARRAY_SIZE(rect->times); + rect->updated = true; +} + +static int vnc_refresh_server_surface(VncDisplay *vd) +{ + int width = MIN(pixman_image_get_width(vd->guest.fb), + pixman_image_get_width(vd->server)); + int height = MIN(pixman_image_get_height(vd->guest.fb), + pixman_image_get_height(vd->server)); + int cmp_bytes, server_stride, line_bytes, guest_ll, guest_stride, y = 0; + uint8_t *guest_row0 = NULL, *server_row0; + VncState *vs; + int has_dirty = 0; + pixman_image_t *tmpbuf = NULL; + + struct timeval tv = { 0, 0 }; + + if (!vd->non_adaptive) { + gettimeofday(&tv, NULL); + has_dirty = vnc_update_stats(vd, &tv); + } + + /* + * Walk through the guest dirty map. + * Check and copy modified bits from guest to server surface. + * Update server dirty map. + */ + server_row0 = (uint8_t *)pixman_image_get_data(vd->server); + server_stride = guest_stride = guest_ll = + pixman_image_get_stride(vd->server); + cmp_bytes = MIN(VNC_DIRTY_PIXELS_PER_BIT * VNC_SERVER_FB_BYTES, + server_stride); + if (vd->guest.format != VNC_SERVER_FB_FORMAT) { + int width = pixman_image_get_width(vd->server); + tmpbuf = qemu_pixman_linebuf_create(VNC_SERVER_FB_FORMAT, width); + } else { + int guest_bpp = + PIXMAN_FORMAT_BPP(pixman_image_get_format(vd->guest.fb)); + guest_row0 = (uint8_t *)pixman_image_get_data(vd->guest.fb); + guest_stride = pixman_image_get_stride(vd->guest.fb); + guest_ll = pixman_image_get_width(vd->guest.fb) + * DIV_ROUND_UP(guest_bpp, 8); + } + line_bytes = MIN(server_stride, guest_ll); + + for (;;) { + int x; + uint8_t *guest_ptr, *server_ptr; + unsigned long offset = find_next_bit((unsigned long *) &vd->guest.dirty, + height * VNC_DIRTY_BPL(&vd->guest), + y * VNC_DIRTY_BPL(&vd->guest)); + if (offset == height * VNC_DIRTY_BPL(&vd->guest)) { + /* no more dirty bits */ + break; + } + y = offset / VNC_DIRTY_BPL(&vd->guest); + x = offset % VNC_DIRTY_BPL(&vd->guest); + + server_ptr = server_row0 + y * server_stride + x * cmp_bytes; + + if (vd->guest.format != VNC_SERVER_FB_FORMAT) { + qemu_pixman_linebuf_fill(tmpbuf, vd->guest.fb, width, 0, y); + guest_ptr = (uint8_t *)pixman_image_get_data(tmpbuf); + } else { + guest_ptr = guest_row0 + y * guest_stride; + } + guest_ptr += x * cmp_bytes; + + for (; x < DIV_ROUND_UP(width, VNC_DIRTY_PIXELS_PER_BIT); + x++, guest_ptr += cmp_bytes, server_ptr += cmp_bytes) { + int _cmp_bytes = cmp_bytes; + if (!test_and_clear_bit(x, vd->guest.dirty[y])) { + continue; + } + if ((x + 1) * cmp_bytes > line_bytes) { + _cmp_bytes = line_bytes - x * cmp_bytes; + } + assert(_cmp_bytes >= 0); + if (memcmp(server_ptr, guest_ptr, _cmp_bytes) == 0) { + continue; + } + memcpy(server_ptr, guest_ptr, _cmp_bytes); + if (!vd->non_adaptive) { + vnc_rect_updated(vd, x * VNC_DIRTY_PIXELS_PER_BIT, + y, &tv); + } + QTAILQ_FOREACH(vs, &vd->clients, next) { + set_bit(x, vs->dirty[y]); + } + has_dirty++; + } + + y++; + } + qemu_pixman_image_unref(tmpbuf); + return has_dirty; +} + +static void vnc_refresh(DisplayChangeListener *dcl) +{ + VncDisplay *vd = container_of(dcl, VncDisplay, dcl); + VncState *vs, *vn; + int has_dirty, rects = 0; + + if (QTAILQ_EMPTY(&vd->clients)) { + update_displaychangelistener(&vd->dcl, VNC_REFRESH_INTERVAL_MAX); + return; + } + + graphic_hw_update(vd->dcl.con); + + if (vnc_trylock_display(vd)) { + update_displaychangelistener(&vd->dcl, VNC_REFRESH_INTERVAL_BASE); + return; + } + + has_dirty = vnc_refresh_server_surface(vd); + vnc_unlock_display(vd); + + QTAILQ_FOREACH_SAFE(vs, &vd->clients, next, vn) { + rects += vnc_update_client(vs, has_dirty); + /* vs might be free()ed here */ + } + + if (has_dirty && rects) { + vd->dcl.update_interval /= 2; + if (vd->dcl.update_interval < VNC_REFRESH_INTERVAL_BASE) { + vd->dcl.update_interval = VNC_REFRESH_INTERVAL_BASE; + } + } else { + vd->dcl.update_interval += VNC_REFRESH_INTERVAL_INC; + if (vd->dcl.update_interval > VNC_REFRESH_INTERVAL_MAX) { + vd->dcl.update_interval = VNC_REFRESH_INTERVAL_MAX; + } + } +} + +static void vnc_connect(VncDisplay *vd, QIOChannelSocket *sioc, + bool skipauth, bool websocket) +{ + VncState *vs = g_new0(VncState, 1); + bool first_client = QTAILQ_EMPTY(&vd->clients); + int i; + + trace_vnc_client_connect(vs, sioc); + vs->zrle = g_new0(VncZrle, 1); + vs->tight = g_new0(VncTight, 1); + vs->magic = VNC_MAGIC; + vs->sioc = sioc; + object_ref(OBJECT(vs->sioc)); + vs->ioc = QIO_CHANNEL(sioc); + object_ref(OBJECT(vs->ioc)); + vs->vd = vd; + + buffer_init(&vs->input, "vnc-input/%p", sioc); + buffer_init(&vs->output, "vnc-output/%p", sioc); + buffer_init(&vs->jobs_buffer, "vnc-jobs_buffer/%p", sioc); + + buffer_init(&vs->tight->tight, "vnc-tight/%p", sioc); + buffer_init(&vs->tight->zlib, "vnc-tight-zlib/%p", sioc); + buffer_init(&vs->tight->gradient, "vnc-tight-gradient/%p", sioc); +#ifdef CONFIG_VNC_JPEG + buffer_init(&vs->tight->jpeg, "vnc-tight-jpeg/%p", sioc); +#endif +#ifdef CONFIG_VNC_PNG + buffer_init(&vs->tight->png, "vnc-tight-png/%p", sioc); +#endif + buffer_init(&vs->zlib.zlib, "vnc-zlib/%p", sioc); + buffer_init(&vs->zrle->zrle, "vnc-zrle/%p", sioc); + buffer_init(&vs->zrle->fb, "vnc-zrle-fb/%p", sioc); + buffer_init(&vs->zrle->zlib, "vnc-zrle-zlib/%p", sioc); + + if (skipauth) { + vs->auth = VNC_AUTH_NONE; + vs->subauth = VNC_AUTH_INVALID; + } else { + if (websocket) { + vs->auth = vd->ws_auth; + vs->subauth = VNC_AUTH_INVALID; + } else { + vs->auth = vd->auth; + vs->subauth = vd->subauth; + } + } + VNC_DEBUG("Client sioc=%p ws=%d auth=%d subauth=%d\n", + sioc, websocket, vs->auth, vs->subauth); + + vs->lossy_rect = g_malloc0(VNC_STAT_ROWS * sizeof (*vs->lossy_rect)); + for (i = 0; i < VNC_STAT_ROWS; ++i) { + vs->lossy_rect[i] = g_new0(uint8_t, VNC_STAT_COLS); + } + + VNC_DEBUG("New client on socket %p\n", vs->sioc); + update_displaychangelistener(&vd->dcl, VNC_REFRESH_INTERVAL_BASE); + qio_channel_set_blocking(vs->ioc, false, NULL); + if (vs->ioc_tag) { + g_source_remove(vs->ioc_tag); + } + if (websocket) { + vs->websocket = 1; + if (vd->tlscreds) { + vs->ioc_tag = qio_channel_add_watch( + vs->ioc, G_IO_IN | G_IO_HUP | G_IO_ERR, + vncws_tls_handshake_io, vs, NULL); + } else { + vs->ioc_tag = qio_channel_add_watch( + vs->ioc, G_IO_IN | G_IO_HUP | G_IO_ERR, + vncws_handshake_io, vs, NULL); + } + } else { + vs->ioc_tag = qio_channel_add_watch( + vs->ioc, G_IO_IN | G_IO_HUP | G_IO_ERR, + vnc_client_io, vs, NULL); + } + + vnc_client_cache_addr(vs); + vnc_qmp_event(vs, QAPI_EVENT_VNC_CONNECTED); + vnc_set_share_mode(vs, VNC_SHARE_MODE_CONNECTING); + + vs->last_x = -1; + vs->last_y = -1; + + vs->as.freq = 44100; + vs->as.nchannels = 2; + vs->as.fmt = AUDIO_FORMAT_S16; + vs->as.endianness = 0; + + qemu_mutex_init(&vs->output_mutex); + vs->bh = qemu_bh_new(vnc_jobs_bh, vs); + + QTAILQ_INSERT_TAIL(&vd->clients, vs, next); + if (first_client) { + vnc_update_server_surface(vd); + } + + graphic_hw_update(vd->dcl.con); + + if (!vs->websocket) { + vnc_start_protocol(vs); + } + + if (vd->num_connecting > vd->connections_limit) { + QTAILQ_FOREACH(vs, &vd->clients, next) { + if (vs->share_mode == VNC_SHARE_MODE_CONNECTING) { + vnc_disconnect_start(vs); + return; + } + } + } +} + +void vnc_start_protocol(VncState *vs) +{ + vnc_write(vs, "RFB 003.008\n", 12); + vnc_flush(vs); + vnc_read_when(vs, protocol_version, 12); + + vs->mouse_mode_notifier.notify = check_pointer_type_change; + qemu_add_mouse_mode_change_notifier(&vs->mouse_mode_notifier); +} + +static void vnc_listen_io(QIONetListener *listener, + QIOChannelSocket *cioc, + void *opaque) +{ + VncDisplay *vd = opaque; + bool isWebsock = listener == vd->wslistener; + + qio_channel_set_name(QIO_CHANNEL(cioc), + isWebsock ? "vnc-ws-server" : "vnc-server"); + qio_channel_set_delay(QIO_CHANNEL(cioc), false); + vnc_connect(vd, cioc, false, isWebsock); +} + +static const DisplayChangeListenerOps dcl_ops = { + .dpy_name = "vnc", + .dpy_refresh = vnc_refresh, + .dpy_gfx_update = vnc_dpy_update, + .dpy_gfx_switch = vnc_dpy_switch, + .dpy_gfx_check_format = qemu_pixman_check_format, + .dpy_mouse_set = vnc_mouse_set, + .dpy_cursor_define = vnc_dpy_cursor_define, +}; + +void vnc_display_init(const char *id, Error **errp) +{ + VncDisplay *vd; + + if (vnc_display_find(id) != NULL) { + return; + } + vd = g_malloc0(sizeof(*vd)); + + vd->id = strdup(id); + QTAILQ_INSERT_TAIL(&vnc_displays, vd, next); + + QTAILQ_INIT(&vd->clients); + vd->expires = TIME_MAX; + + if (keyboard_layout) { + trace_vnc_key_map_init(keyboard_layout); + vd->kbd_layout = init_keyboard_layout(name2keysym, + keyboard_layout, errp); + } else { + vd->kbd_layout = init_keyboard_layout(name2keysym, "en-us", errp); + } + + if (!vd->kbd_layout) { + return; + } + + vd->share_policy = VNC_SHARE_POLICY_ALLOW_EXCLUSIVE; + vd->connections_limit = 32; + + qemu_mutex_init(&vd->mutex); + vnc_start_worker_thread(); + + vd->dcl.ops = &dcl_ops; + register_displaychangelistener(&vd->dcl); + vd->kbd = qkbd_state_init(vd->dcl.con); +} + + +static void vnc_display_close(VncDisplay *vd) +{ + if (!vd) { + return; + } + vd->is_unix = false; + + if (vd->listener) { + qio_net_listener_disconnect(vd->listener); + object_unref(OBJECT(vd->listener)); + } + vd->listener = NULL; + + if (vd->wslistener) { + qio_net_listener_disconnect(vd->wslistener); + object_unref(OBJECT(vd->wslistener)); + } + vd->wslistener = NULL; + + vd->auth = VNC_AUTH_INVALID; + vd->subauth = VNC_AUTH_INVALID; + if (vd->tlscreds) { + object_unref(OBJECT(vd->tlscreds)); + vd->tlscreds = NULL; + } + if (vd->tlsauthz) { + object_unparent(OBJECT(vd->tlsauthz)); + vd->tlsauthz = NULL; + } + g_free(vd->tlsauthzid); + vd->tlsauthzid = NULL; + if (vd->lock_key_sync) { + qemu_remove_led_event_handler(vd->led); + vd->led = NULL; + } +#ifdef CONFIG_VNC_SASL + if (vd->sasl.authz) { + object_unparent(OBJECT(vd->sasl.authz)); + vd->sasl.authz = NULL; + } + g_free(vd->sasl.authzid); + vd->sasl.authzid = NULL; +#endif +} + +int vnc_display_password(const char *id, const char *password) +{ + VncDisplay *vd = vnc_display_find(id); + + if (!vd) { + return -EINVAL; + } + if (vd->auth == VNC_AUTH_NONE) { + error_printf_unless_qmp("If you want use passwords please enable " + "password auth using '-vnc ${dpy},password'.\n"); + return -EINVAL; + } + + g_free(vd->password); + vd->password = g_strdup(password); + + return 0; +} + +int vnc_display_pw_expire(const char *id, time_t expires) +{ + VncDisplay *vd = vnc_display_find(id); + + if (!vd) { + return -EINVAL; + } + + vd->expires = expires; + return 0; +} + +static void vnc_display_print_local_addr(VncDisplay *vd) +{ + SocketAddress *addr; + + if (!vd->listener || !vd->listener->nsioc) { + return; + } + + addr = qio_channel_socket_get_local_address(vd->listener->sioc[0], NULL); + if (!addr) { + return; + } + + if (addr->type != SOCKET_ADDRESS_TYPE_INET) { + qapi_free_SocketAddress(addr); + return; + } + error_printf_unless_qmp("VNC server running on %s:%s\n", + addr->u.inet.host, + addr->u.inet.port); + qapi_free_SocketAddress(addr); +} + +static QemuOptsList qemu_vnc_opts = { + .name = "vnc", + .head = QTAILQ_HEAD_INITIALIZER(qemu_vnc_opts.head), + .implied_opt_name = "vnc", + .desc = { + { + .name = "vnc", + .type = QEMU_OPT_STRING, + },{ + .name = "websocket", + .type = QEMU_OPT_STRING, + },{ + .name = "tls-creds", + .type = QEMU_OPT_STRING, + },{ + .name = "share", + .type = QEMU_OPT_STRING, + },{ + .name = "display", + .type = QEMU_OPT_STRING, + },{ + .name = "head", + .type = QEMU_OPT_NUMBER, + },{ + .name = "connections", + .type = QEMU_OPT_NUMBER, + },{ + .name = "to", + .type = QEMU_OPT_NUMBER, + },{ + .name = "ipv4", + .type = QEMU_OPT_BOOL, + },{ + .name = "ipv6", + .type = QEMU_OPT_BOOL, + },{ + .name = "password", + .type = QEMU_OPT_BOOL, + },{ + .name = "password-secret", + .type = QEMU_OPT_STRING, + },{ + .name = "reverse", + .type = QEMU_OPT_BOOL, + },{ + .name = "lock-key-sync", + .type = QEMU_OPT_BOOL, + },{ + .name = "key-delay-ms", + .type = QEMU_OPT_NUMBER, + },{ + .name = "sasl", + .type = QEMU_OPT_BOOL, + },{ + .name = "tls-authz", + .type = QEMU_OPT_STRING, + },{ + .name = "sasl-authz", + .type = QEMU_OPT_STRING, + },{ + .name = "lossy", + .type = QEMU_OPT_BOOL, + },{ + .name = "non-adaptive", + .type = QEMU_OPT_BOOL, + },{ + .name = "audiodev", + .type = QEMU_OPT_STRING, + },{ + .name = "power-control", + .type = QEMU_OPT_BOOL, + }, + { /* end of list */ } + }, +}; + + +static int +vnc_display_setup_auth(int *auth, + int *subauth, + QCryptoTLSCreds *tlscreds, + bool password, + bool sasl, + bool websocket, + Error **errp) +{ + /* + * We have a choice of 3 authentication options + * + * 1. none + * 2. vnc + * 3. sasl + * + * The channel can be run in 2 modes + * + * 1. clear + * 2. tls + * + * And TLS can use 2 types of credentials + * + * 1. anon + * 2. x509 + * + * We thus have 9 possible logical combinations + * + * 1. clear + none + * 2. clear + vnc + * 3. clear + sasl + * 4. tls + anon + none + * 5. tls + anon + vnc + * 6. tls + anon + sasl + * 7. tls + x509 + none + * 8. tls + x509 + vnc + * 9. tls + x509 + sasl + * + * These need to be mapped into the VNC auth schemes + * in an appropriate manner. In regular VNC, all the + * TLS options get mapped into VNC_AUTH_VENCRYPT + * sub-auth types. + * + * In websockets, the https:// protocol already provides + * TLS support, so there is no need to make use of the + * VeNCrypt extension. Furthermore, websockets browser + * clients could not use VeNCrypt even if they wanted to, + * as they cannot control when the TLS handshake takes + * place. Thus there is no option but to rely on https://, + * meaning combinations 4->6 and 7->9 will be mapped to + * VNC auth schemes in the same way as combos 1->3. + * + * Regardless of fact that we have a different mapping to + * VNC auth mechs for plain VNC vs websockets VNC, the end + * result has the same security characteristics. + */ + if (websocket || !tlscreds) { + if (password) { + VNC_DEBUG("Initializing VNC server with password auth\n"); + *auth = VNC_AUTH_VNC; + } else if (sasl) { + VNC_DEBUG("Initializing VNC server with SASL auth\n"); + *auth = VNC_AUTH_SASL; + } else { + VNC_DEBUG("Initializing VNC server with no auth\n"); + *auth = VNC_AUTH_NONE; + } + *subauth = VNC_AUTH_INVALID; + } else { + bool is_x509 = object_dynamic_cast(OBJECT(tlscreds), + TYPE_QCRYPTO_TLS_CREDS_X509) != NULL; + bool is_anon = object_dynamic_cast(OBJECT(tlscreds), + TYPE_QCRYPTO_TLS_CREDS_ANON) != NULL; + + if (!is_x509 && !is_anon) { + error_setg(errp, + "Unsupported TLS cred type %s", + object_get_typename(OBJECT(tlscreds))); + return -1; + } + *auth = VNC_AUTH_VENCRYPT; + if (password) { + if (is_x509) { + VNC_DEBUG("Initializing VNC server with x509 password auth\n"); + *subauth = VNC_AUTH_VENCRYPT_X509VNC; + } else { + VNC_DEBUG("Initializing VNC server with TLS password auth\n"); + *subauth = VNC_AUTH_VENCRYPT_TLSVNC; + } + + } else if (sasl) { + if (is_x509) { + VNC_DEBUG("Initializing VNC server with x509 SASL auth\n"); + *subauth = VNC_AUTH_VENCRYPT_X509SASL; + } else { + VNC_DEBUG("Initializing VNC server with TLS SASL auth\n"); + *subauth = VNC_AUTH_VENCRYPT_TLSSASL; + } + } else { + if (is_x509) { + VNC_DEBUG("Initializing VNC server with x509 no auth\n"); + *subauth = VNC_AUTH_VENCRYPT_X509NONE; + } else { + VNC_DEBUG("Initializing VNC server with TLS no auth\n"); + *subauth = VNC_AUTH_VENCRYPT_TLSNONE; + } + } + } + return 0; +} + + +static int vnc_display_get_address(const char *addrstr, + bool websocket, + bool reverse, + int displaynum, + int to, + bool has_ipv4, + bool has_ipv6, + bool ipv4, + bool ipv6, + SocketAddress **retaddr, + Error **errp) +{ + int ret = -1; + SocketAddress *addr = NULL; + + addr = g_new0(SocketAddress, 1); + + if (strncmp(addrstr, "unix:", 5) == 0) { + addr->type = SOCKET_ADDRESS_TYPE_UNIX; + addr->u.q_unix.path = g_strdup(addrstr + 5); + + if (websocket) { + error_setg(errp, "UNIX sockets not supported with websock"); + goto cleanup; + } + + if (to) { + error_setg(errp, "Port range not support with UNIX socket"); + goto cleanup; + } + ret = 0; + } else { + const char *port; + size_t hostlen; + unsigned long long baseport = 0; + InetSocketAddress *inet; + + port = strrchr(addrstr, ':'); + if (!port) { + if (websocket) { + hostlen = 0; + port = addrstr; + } else { + error_setg(errp, "no vnc port specified"); + goto cleanup; + } + } else { + hostlen = port - addrstr; + port++; + if (*port == '\0') { + error_setg(errp, "vnc port cannot be empty"); + goto cleanup; + } + } + + addr->type = SOCKET_ADDRESS_TYPE_INET; + inet = &addr->u.inet; + if (addrstr[0] == '[' && addrstr[hostlen - 1] == ']') { + inet->host = g_strndup(addrstr + 1, hostlen - 2); + } else { + inet->host = g_strndup(addrstr, hostlen); + } + /* plain VNC port is just an offset, for websocket + * port is absolute */ + if (websocket) { + if (g_str_equal(addrstr, "") || + g_str_equal(addrstr, "on")) { + if (displaynum == -1) { + error_setg(errp, "explicit websocket port is required"); + goto cleanup; + } + inet->port = g_strdup_printf( + "%d", displaynum + 5700); + if (to) { + inet->has_to = true; + inet->to = to + 5700; + } + } else { + inet->port = g_strdup(port); + } + } else { + int offset = reverse ? 0 : 5900; + if (parse_uint_full(port, &baseport, 10) < 0) { + error_setg(errp, "can't convert to a number: %s", port); + goto cleanup; + } + if (baseport > 65535 || + baseport + offset > 65535) { + error_setg(errp, "port %s out of range", port); + goto cleanup; + } + inet->port = g_strdup_printf( + "%d", (int)baseport + offset); + + if (to) { + inet->has_to = true; + inet->to = to + offset; + } + } + + inet->ipv4 = ipv4; + inet->has_ipv4 = has_ipv4; + inet->ipv6 = ipv6; + inet->has_ipv6 = has_ipv6; + + ret = baseport; + } + + *retaddr = addr; + + cleanup: + if (ret < 0) { + qapi_free_SocketAddress(addr); + } + return ret; +} + +static void vnc_free_addresses(SocketAddress ***retsaddr, + size_t *retnsaddr) +{ + size_t i; + + for (i = 0; i < *retnsaddr; i++) { + qapi_free_SocketAddress((*retsaddr)[i]); + } + g_free(*retsaddr); + + *retsaddr = NULL; + *retnsaddr = 0; +} + +static int vnc_display_get_addresses(QemuOpts *opts, + bool reverse, + SocketAddress ***retsaddr, + size_t *retnsaddr, + SocketAddress ***retwsaddr, + size_t *retnwsaddr, + Error **errp) +{ + SocketAddress *saddr = NULL; + SocketAddress *wsaddr = NULL; + QemuOptsIter addriter; + const char *addr; + int to = qemu_opt_get_number(opts, "to", 0); + bool has_ipv4 = qemu_opt_get(opts, "ipv4"); + bool has_ipv6 = qemu_opt_get(opts, "ipv6"); + bool ipv4 = qemu_opt_get_bool(opts, "ipv4", false); + bool ipv6 = qemu_opt_get_bool(opts, "ipv6", false); + int displaynum = -1; + int ret = -1; + + *retsaddr = NULL; + *retnsaddr = 0; + *retwsaddr = NULL; + *retnwsaddr = 0; + + addr = qemu_opt_get(opts, "vnc"); + if (addr == NULL || g_str_equal(addr, "none")) { + ret = 0; + goto cleanup; + } + if (qemu_opt_get(opts, "websocket") && + !qcrypto_hash_supports(QCRYPTO_HASH_ALG_SHA1)) { + error_setg(errp, + "SHA1 hash support is required for websockets"); + goto cleanup; + } + + qemu_opt_iter_init(&addriter, opts, "vnc"); + while ((addr = qemu_opt_iter_next(&addriter)) != NULL) { + int rv; + rv = vnc_display_get_address(addr, false, reverse, 0, to, + has_ipv4, has_ipv6, + ipv4, ipv6, + &saddr, errp); + if (rv < 0) { + goto cleanup; + } + /* Historical compat - first listen address can be used + * to set the default websocket port + */ + if (displaynum == -1) { + displaynum = rv; + } + *retsaddr = g_renew(SocketAddress *, *retsaddr, *retnsaddr + 1); + (*retsaddr)[(*retnsaddr)++] = saddr; + } + + /* If we had multiple primary displays, we don't do defaults + * for websocket, and require explicit config instead. */ + if (*retnsaddr > 1) { + displaynum = -1; + } + + qemu_opt_iter_init(&addriter, opts, "websocket"); + while ((addr = qemu_opt_iter_next(&addriter)) != NULL) { + if (vnc_display_get_address(addr, true, reverse, displaynum, to, + has_ipv4, has_ipv6, + ipv4, ipv6, + &wsaddr, errp) < 0) { + goto cleanup; + } + + /* Historical compat - if only a single listen address was + * provided, then this is used to set the default listen + * address for websocket too + */ + if (*retnsaddr == 1 && + (*retsaddr)[0]->type == SOCKET_ADDRESS_TYPE_INET && + wsaddr->type == SOCKET_ADDRESS_TYPE_INET && + g_str_equal(wsaddr->u.inet.host, "") && + !g_str_equal((*retsaddr)[0]->u.inet.host, "")) { + g_free(wsaddr->u.inet.host); + wsaddr->u.inet.host = g_strdup((*retsaddr)[0]->u.inet.host); + } + + *retwsaddr = g_renew(SocketAddress *, *retwsaddr, *retnwsaddr + 1); + (*retwsaddr)[(*retnwsaddr)++] = wsaddr; + } + + ret = 0; + cleanup: + if (ret < 0) { + vnc_free_addresses(retsaddr, retnsaddr); + vnc_free_addresses(retwsaddr, retnwsaddr); + } + return ret; +} + +static int vnc_display_connect(VncDisplay *vd, + SocketAddress **saddr, + size_t nsaddr, + SocketAddress **wsaddr, + size_t nwsaddr, + Error **errp) +{ + /* connect to viewer */ + QIOChannelSocket *sioc = NULL; + if (nwsaddr != 0) { + error_setg(errp, "Cannot use websockets in reverse mode"); + return -1; + } + if (nsaddr != 1) { + error_setg(errp, "Expected a single address in reverse mode"); + return -1; + } + /* TODO SOCKET_ADDRESS_TYPE_FD when fd has AF_UNIX */ + vd->is_unix = saddr[0]->type == SOCKET_ADDRESS_TYPE_UNIX; + sioc = qio_channel_socket_new(); + qio_channel_set_name(QIO_CHANNEL(sioc), "vnc-reverse"); + if (qio_channel_socket_connect_sync(sioc, saddr[0], errp) < 0) { + object_unref(OBJECT(sioc)); + return -1; + } + vnc_connect(vd, sioc, false, false); + object_unref(OBJECT(sioc)); + return 0; +} + + +static int vnc_display_listen(VncDisplay *vd, + SocketAddress **saddr, + size_t nsaddr, + SocketAddress **wsaddr, + size_t nwsaddr, + Error **errp) +{ + size_t i; + + if (nsaddr) { + vd->listener = qio_net_listener_new(); + qio_net_listener_set_name(vd->listener, "vnc-listen"); + for (i = 0; i < nsaddr; i++) { + if (qio_net_listener_open_sync(vd->listener, + saddr[i], 1, + errp) < 0) { + return -1; + } + } + + qio_net_listener_set_client_func(vd->listener, + vnc_listen_io, vd, NULL); + } + + if (nwsaddr) { + vd->wslistener = qio_net_listener_new(); + qio_net_listener_set_name(vd->wslistener, "vnc-ws-listen"); + for (i = 0; i < nwsaddr; i++) { + if (qio_net_listener_open_sync(vd->wslistener, + wsaddr[i], 1, + errp) < 0) { + return -1; + } + } + + qio_net_listener_set_client_func(vd->wslistener, + vnc_listen_io, vd, NULL); + } + + return 0; +} + + +void vnc_display_open(const char *id, Error **errp) +{ + VncDisplay *vd = vnc_display_find(id); + QemuOpts *opts = qemu_opts_find(&qemu_vnc_opts, id); + SocketAddress **saddr = NULL, **wsaddr = NULL; + size_t nsaddr, nwsaddr; + const char *share, *device_id; + QemuConsole *con; + bool password = false; + bool reverse = false; + const char *credid; + bool sasl = false; + const char *tlsauthz; + const char *saslauthz; + int lock_key_sync = 1; + int key_delay_ms; + const char *audiodev; + const char *passwordSecret; + + if (!vd) { + error_setg(errp, "VNC display not active"); + return; + } + vnc_display_close(vd); + + if (!opts) { + return; + } + + reverse = qemu_opt_get_bool(opts, "reverse", false); + if (vnc_display_get_addresses(opts, reverse, &saddr, &nsaddr, + &wsaddr, &nwsaddr, errp) < 0) { + goto fail; + } + + + passwordSecret = qemu_opt_get(opts, "password-secret"); + if (passwordSecret) { + if (qemu_opt_get(opts, "password")) { + error_setg(errp, + "'password' flag is redundant with 'password-secret'"); + goto fail; + } + vd->password = qcrypto_secret_lookup_as_utf8(passwordSecret, + errp); + if (!vd->password) { + goto fail; + } + password = true; + } else { + password = qemu_opt_get_bool(opts, "password", false); + } + if (password) { + if (fips_get_state()) { + error_setg(errp, + "VNC password auth disabled due to FIPS mode, " + "consider using the VeNCrypt or SASL authentication " + "methods as an alternative"); + goto fail; + } + if (!qcrypto_cipher_supports( + QCRYPTO_CIPHER_ALG_DES, QCRYPTO_CIPHER_MODE_ECB)) { + error_setg(errp, + "Cipher backend does not support DES algorithm"); + goto fail; + } + } + + lock_key_sync = qemu_opt_get_bool(opts, "lock-key-sync", true); + key_delay_ms = qemu_opt_get_number(opts, "key-delay-ms", 10); + sasl = qemu_opt_get_bool(opts, "sasl", false); +#ifndef CONFIG_VNC_SASL + if (sasl) { + error_setg(errp, "VNC SASL auth requires cyrus-sasl support"); + goto fail; + } +#endif /* CONFIG_VNC_SASL */ + credid = qemu_opt_get(opts, "tls-creds"); + if (credid) { + Object *creds; + creds = object_resolve_path_component( + object_get_objects_root(), credid); + if (!creds) { + error_setg(errp, "No TLS credentials with id '%s'", + credid); + goto fail; + } + vd->tlscreds = (QCryptoTLSCreds *) + object_dynamic_cast(creds, + TYPE_QCRYPTO_TLS_CREDS); + if (!vd->tlscreds) { + error_setg(errp, "Object with id '%s' is not TLS credentials", + credid); + goto fail; + } + object_ref(OBJECT(vd->tlscreds)); + + if (!qcrypto_tls_creds_check_endpoint(vd->tlscreds, + QCRYPTO_TLS_CREDS_ENDPOINT_SERVER, + errp)) { + goto fail; + } + } + tlsauthz = qemu_opt_get(opts, "tls-authz"); + if (tlsauthz && !vd->tlscreds) { + error_setg(errp, "'tls-authz' provided but TLS is not enabled"); + goto fail; + } + + saslauthz = qemu_opt_get(opts, "sasl-authz"); + if (saslauthz && !sasl) { + error_setg(errp, "'sasl-authz' provided but SASL auth is not enabled"); + goto fail; + } + + share = qemu_opt_get(opts, "share"); + if (share) { + if (strcmp(share, "ignore") == 0) { + vd->share_policy = VNC_SHARE_POLICY_IGNORE; + } else if (strcmp(share, "allow-exclusive") == 0) { + vd->share_policy = VNC_SHARE_POLICY_ALLOW_EXCLUSIVE; + } else if (strcmp(share, "force-shared") == 0) { + vd->share_policy = VNC_SHARE_POLICY_FORCE_SHARED; + } else { + error_setg(errp, "unknown vnc share= option"); + goto fail; + } + } else { + vd->share_policy = VNC_SHARE_POLICY_ALLOW_EXCLUSIVE; + } + vd->connections_limit = qemu_opt_get_number(opts, "connections", 32); + +#ifdef CONFIG_VNC_JPEG + vd->lossy = qemu_opt_get_bool(opts, "lossy", false); +#endif + vd->non_adaptive = qemu_opt_get_bool(opts, "non-adaptive", false); + /* adaptive updates are only used with tight encoding and + * if lossy updates are enabled so we can disable all the + * calculations otherwise */ + if (!vd->lossy) { + vd->non_adaptive = true; + } + + vd->power_control = qemu_opt_get_bool(opts, "power-control", false); + + if (tlsauthz) { + vd->tlsauthzid = g_strdup(tlsauthz); + } +#ifdef CONFIG_VNC_SASL + if (sasl) { + if (saslauthz) { + vd->sasl.authzid = g_strdup(saslauthz); + } + } +#endif + + if (vnc_display_setup_auth(&vd->auth, &vd->subauth, + vd->tlscreds, password, + sasl, false, errp) < 0) { + goto fail; + } + trace_vnc_auth_init(vd, 0, vd->auth, vd->subauth); + + if (vnc_display_setup_auth(&vd->ws_auth, &vd->ws_subauth, + vd->tlscreds, password, + sasl, true, errp) < 0) { + goto fail; + } + trace_vnc_auth_init(vd, 1, vd->ws_auth, vd->ws_subauth); + +#ifdef CONFIG_VNC_SASL + if (sasl && !vnc_sasl_server_init(errp)) { + goto fail; + } +#endif + vd->lock_key_sync = lock_key_sync; + if (lock_key_sync) { + vd->led = qemu_add_led_event_handler(kbd_leds, vd); + } + vd->ledstate = 0; + + audiodev = qemu_opt_get(opts, "audiodev"); + if (audiodev) { + vd->audio_state = audio_state_by_name(audiodev); + if (!vd->audio_state) { + error_setg(errp, "Audiodev '%s' not found", audiodev); + goto fail; + } + } + + device_id = qemu_opt_get(opts, "display"); + if (device_id) { + int head = qemu_opt_get_number(opts, "head", 0); + Error *err = NULL; + + con = qemu_console_lookup_by_device_name(device_id, head, &err); + if (err) { + error_propagate(errp, err); + goto fail; + } + } else { + con = NULL; + } + + if (con != vd->dcl.con) { + qkbd_state_free(vd->kbd); + unregister_displaychangelistener(&vd->dcl); + vd->dcl.con = con; + register_displaychangelistener(&vd->dcl); + vd->kbd = qkbd_state_init(vd->dcl.con); + } + qkbd_state_set_delay(vd->kbd, key_delay_ms); + + if (saddr == NULL) { + goto cleanup; + } + + if (reverse) { + if (vnc_display_connect(vd, saddr, nsaddr, wsaddr, nwsaddr, errp) < 0) { + goto fail; + } + } else { + if (vnc_display_listen(vd, saddr, nsaddr, wsaddr, nwsaddr, errp) < 0) { + goto fail; + } + } + + if (qemu_opt_get(opts, "to")) { + vnc_display_print_local_addr(vd); + } + + cleanup: + vnc_free_addresses(&saddr, &nsaddr); + vnc_free_addresses(&wsaddr, &nwsaddr); + return; + +fail: + vnc_display_close(vd); + goto cleanup; +} + +void vnc_display_add_client(const char *id, int csock, bool skipauth) +{ + VncDisplay *vd = vnc_display_find(id); + QIOChannelSocket *sioc; + + if (!vd) { + return; + } + + sioc = qio_channel_socket_new_fd(csock, NULL); + if (sioc) { + qio_channel_set_name(QIO_CHANNEL(sioc), "vnc-server"); + vnc_connect(vd, sioc, skipauth, false); + object_unref(OBJECT(sioc)); + } +} + +static void vnc_auto_assign_id(QemuOptsList *olist, QemuOpts *opts) +{ + int i = 2; + char *id; + + id = g_strdup("default"); + while (qemu_opts_find(olist, id)) { + g_free(id); + id = g_strdup_printf("vnc%d", i++); + } + qemu_opts_set_id(opts, id); +} + +void vnc_parse(const char *str) +{ + QemuOptsList *olist = qemu_find_opts("vnc"); + QemuOpts *opts = qemu_opts_parse_noisily(olist, str, !is_help_option(str)); + const char *id; + + if (!opts) { + exit(1); + } + + id = qemu_opts_id(opts); + if (!id) { + /* auto-assign id if not present */ + vnc_auto_assign_id(olist, opts); + } +} + +int vnc_init_func(void *opaque, QemuOpts *opts, Error **errp) +{ + Error *local_err = NULL; + char *id = (char *)qemu_opts_id(opts); + + assert(id); + vnc_display_init(id, &local_err); + if (local_err) { + error_propagate(errp, local_err); + return -1; + } + vnc_display_open(id, &local_err); + if (local_err != NULL) { + error_propagate(errp, local_err); + return -1; + } + return 0; +} + +static void vnc_register_config(void) +{ + qemu_add_opts(&qemu_vnc_opts); +} +opts_init(vnc_register_config); diff --git a/ui/vnc.h b/ui/vnc.h new file mode 100644 index 000000000..a7149831f --- /dev/null +++ b/ui/vnc.h @@ -0,0 +1,645 @@ +/* + * QEMU VNC display driver + * + * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws> + * Copyright (C) 2006 Fabrice Bellard + * Copyright (C) 2009 Red Hat, Inc + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef QEMU_VNC_H +#define QEMU_VNC_H + +#include "qemu/queue.h" +#include "qemu/thread.h" +#include "ui/clipboard.h" +#include "ui/console.h" +#include "audio/audio.h" +#include "qemu/bitmap.h" +#include "crypto/tlssession.h" +#include "qemu/buffer.h" +#include "io/channel-socket.h" +#include "io/channel-tls.h" +#include "io/net-listener.h" +#include "authz/base.h" +#include <zlib.h> + +#include "keymaps.h" +#include "vnc-palette.h" +#include "vnc-enc-zrle.h" +#include "ui/kbd-state.h" + +// #define _VNC_DEBUG 1 + +#ifdef _VNC_DEBUG +#define VNC_DEBUG(fmt, ...) do { fprintf(stderr, fmt, ## __VA_ARGS__); } while (0) +#else +#define VNC_DEBUG(fmt, ...) do { } while (0) +#endif + +/***************************************************************************** + * + * Core data structures + * + *****************************************************************************/ + +typedef struct VncState VncState; +typedef struct VncJob VncJob; +typedef struct VncRect VncRect; +typedef struct VncRectEntry VncRectEntry; + +typedef int VncReadEvent(VncState *vs, uint8_t *data, size_t len); + +typedef void VncWritePixels(VncState *vs, void *data, int size); + +typedef void VncSendHextileTile(VncState *vs, + int x, int y, int w, int h, + void *last_bg, + void *last_fg, + int *has_bg, int *has_fg); + +/* VNC_DIRTY_PIXELS_PER_BIT is the number of dirty pixels represented + * by one bit in the dirty bitmap, should be a power of 2 */ +#define VNC_DIRTY_PIXELS_PER_BIT 16 + +/* VNC_MAX_WIDTH must be a multiple of VNC_DIRTY_PIXELS_PER_BIT. */ + +#define VNC_MAX_WIDTH ROUND_UP(2560, VNC_DIRTY_PIXELS_PER_BIT) +#define VNC_MAX_HEIGHT 2048 + +/* VNC_DIRTY_BITS is the number of bits in the dirty bitmap. */ +#define VNC_DIRTY_BITS (VNC_MAX_WIDTH / VNC_DIRTY_PIXELS_PER_BIT) + +/* VNC_DIRTY_BPL (BPL = bits per line) might be greater than + * VNC_DIRTY_BITS due to alignment */ +#define VNC_DIRTY_BPL(x) (sizeof((x)->dirty) / VNC_MAX_HEIGHT * BITS_PER_BYTE) + +#define VNC_STAT_RECT 64 +#define VNC_STAT_COLS (VNC_MAX_WIDTH / VNC_STAT_RECT) +#define VNC_STAT_ROWS (VNC_MAX_HEIGHT / VNC_STAT_RECT) + +#define VNC_AUTH_CHALLENGE_SIZE 16 + +typedef struct VncDisplay VncDisplay; + +#include "vnc-auth-vencrypt.h" +#ifdef CONFIG_VNC_SASL +#include "vnc-auth-sasl.h" +#endif +#include "vnc-ws.h" + +struct VncRectStat +{ + /* time of last 10 updates, to find update frequency */ + struct timeval times[10]; + int idx; + + double freq; /* Update frequency (in Hz) */ + bool updated; /* Already updated during this refresh */ +}; + +typedef struct VncRectStat VncRectStat; + +struct VncSurface +{ + struct timeval last_freq_check; + DECLARE_BITMAP(dirty[VNC_MAX_HEIGHT], + VNC_MAX_WIDTH / VNC_DIRTY_PIXELS_PER_BIT); + VncRectStat stats[VNC_STAT_ROWS][VNC_STAT_COLS]; + pixman_image_t *fb; + pixman_format_code_t format; +}; + +typedef enum VncShareMode { + VNC_SHARE_MODE_CONNECTING = 1, + VNC_SHARE_MODE_SHARED, + VNC_SHARE_MODE_EXCLUSIVE, + VNC_SHARE_MODE_DISCONNECTED, +} VncShareMode; + +typedef enum VncSharePolicy { + VNC_SHARE_POLICY_IGNORE = 1, + VNC_SHARE_POLICY_ALLOW_EXCLUSIVE, + VNC_SHARE_POLICY_FORCE_SHARED, +} VncSharePolicy; + +struct VncDisplay +{ + QTAILQ_HEAD(, VncState) clients; + int num_connecting; + int num_shared; + int num_exclusive; + int connections_limit; + VncSharePolicy share_policy; + QIONetListener *listener; + QIONetListener *wslistener; + DisplaySurface *ds; + DisplayChangeListener dcl; + kbd_layout_t *kbd_layout; + int lock_key_sync; + QEMUPutLEDEntry *led; + int ledstate; + QKbdState *kbd; + QemuMutex mutex; + + QEMUCursor *cursor; + int cursor_msize; + uint8_t *cursor_mask; + + struct VncSurface guest; /* guest visible surface (aka ds->surface) */ + pixman_image_t *server; /* vnc server surface */ + int true_width; /* server surface width before rounding up */ + + const char *id; + QTAILQ_ENTRY(VncDisplay) next; + bool is_unix; + char *password; + time_t expires; + int auth; + int subauth; /* Used by VeNCrypt */ + int ws_auth; /* Used by websockets */ + int ws_subauth; /* Used by websockets */ + bool lossy; + bool non_adaptive; + bool power_control; + QCryptoTLSCreds *tlscreds; + QAuthZ *tlsauthz; + char *tlsauthzid; +#ifdef CONFIG_VNC_SASL + VncDisplaySASL sasl; +#endif + + AudioState *audio_state; +}; + +typedef struct VncTight { + int type; + uint8_t quality; + uint8_t compression; + uint8_t pixel24; + Buffer tight; + Buffer tmp; + Buffer zlib; + Buffer gradient; +#ifdef CONFIG_VNC_JPEG + Buffer jpeg; +#endif +#ifdef CONFIG_VNC_PNG + Buffer png; +#endif + int levels[4]; + z_stream stream[4]; +} VncTight; + +typedef struct VncHextile { + VncSendHextileTile *send_tile; +} VncHextile; + +typedef struct VncZlib { + Buffer zlib; + Buffer tmp; + z_stream stream; + int level; +} VncZlib; + +typedef struct VncZrle { + int type; + Buffer fb; + Buffer zrle; + Buffer tmp; + Buffer zlib; + z_stream stream; + VncPalette palette; +} VncZrle; + +typedef struct VncZywrle { + int buf[VNC_ZRLE_TILE_WIDTH * VNC_ZRLE_TILE_HEIGHT]; +} VncZywrle; + +struct VncRect +{ + int x; + int y; + int w; + int h; +}; + +struct VncRectEntry +{ + struct VncRect rect; + QLIST_ENTRY(VncRectEntry) next; +}; + +struct VncJob +{ + VncState *vs; + + QLIST_HEAD(, VncRectEntry) rectangles; + QTAILQ_ENTRY(VncJob) next; +}; + +typedef enum { + VNC_STATE_UPDATE_NONE, + VNC_STATE_UPDATE_INCREMENTAL, + VNC_STATE_UPDATE_FORCE, +} VncStateUpdate; + +#define VNC_MAGIC ((uint64_t)0x05b3f069b3d204bb) + +struct VncState +{ + uint64_t magic; + QIOChannelSocket *sioc; /* The underlying socket */ + QIOChannel *ioc; /* The channel currently used for I/O */ + guint ioc_tag; + gboolean disconnecting; + + DECLARE_BITMAP(dirty[VNC_MAX_HEIGHT], VNC_DIRTY_BITS); + uint8_t **lossy_rect; /* Not an Array to avoid costly memcpy in + * vnc-jobs-async.c */ + + VncDisplay *vd; + VncStateUpdate update; /* Most recent pending request from client */ + VncStateUpdate job_update; /* Currently processed by job thread */ + int has_dirty; + uint32_t features; + int absolute; + int last_x; + int last_y; + uint32_t last_bmask; + size_t client_width; /* limited to u16 by RFB proto */ + size_t client_height; /* limited to u16 by RFB proto */ + VncShareMode share_mode; + + uint32_t vnc_encoding; + + int major; + int minor; + + int auth; + int subauth; /* Used by VeNCrypt */ + char challenge[VNC_AUTH_CHALLENGE_SIZE]; + QCryptoTLSSession *tls; /* Borrowed pointer from channel, don't free */ +#ifdef CONFIG_VNC_SASL + VncStateSASL sasl; +#endif + bool encode_ws; + bool websocket; + +#ifdef CONFIG_VNC + VncClientInfo *info; +#endif + + /* Job thread bottom half has put data for a forced update + * into the output buffer. This offset points to the end of + * the update data in the output buffer. This lets us determine + * when a force update is fully sent to the client, allowing + * us to process further forced updates. */ + size_t force_update_offset; + /* We allow multiple incremental updates or audio capture + * samples to be queued in output buffer, provided the + * buffer size doesn't exceed this threshold. The value + * is calculating dynamically based on framebuffer size + * and audio sample settings in vnc_update_throttle_offset() */ + size_t throttle_output_offset; + Buffer output; + Buffer input; + /* current output mode information */ + VncWritePixels *write_pixels; + PixelFormat client_pf; + pixman_format_code_t client_format; + bool client_be; + + CaptureVoiceOut *audio_cap; + struct audsettings as; + + VncReadEvent *read_handler; + size_t read_handler_expect; + + bool abort; + QemuMutex output_mutex; + QEMUBH *bh; + Buffer jobs_buffer; + + /* Encoding specific, if you add something here, don't forget to + * update vnc_async_encoding_start() + */ + VncTight *tight; + VncZlib zlib; + VncHextile hextile; + VncZrle *zrle; + VncZywrle zywrle; + + Notifier mouse_mode_notifier; + + QemuClipboardPeer cbpeer; + QemuClipboardInfo *cbinfo; + uint32_t cbpending; + + QTAILQ_ENTRY(VncState) next; +}; + + +/***************************************************************************** + * + * Authentication modes + * + *****************************************************************************/ + +enum { + VNC_AUTH_INVALID = 0, + VNC_AUTH_NONE = 1, + VNC_AUTH_VNC = 2, + VNC_AUTH_RA2 = 5, + VNC_AUTH_RA2NE = 6, + VNC_AUTH_TIGHT = 16, + VNC_AUTH_ULTRA = 17, + VNC_AUTH_TLS = 18, /* Supported in GTK-VNC & VINO */ + VNC_AUTH_VENCRYPT = 19, /* Supported in GTK-VNC & VeNCrypt */ + VNC_AUTH_SASL = 20, /* Supported in GTK-VNC & VINO */ +}; + +enum { + VNC_AUTH_VENCRYPT_PLAIN = 256, + VNC_AUTH_VENCRYPT_TLSNONE = 257, + VNC_AUTH_VENCRYPT_TLSVNC = 258, + VNC_AUTH_VENCRYPT_TLSPLAIN = 259, + VNC_AUTH_VENCRYPT_X509NONE = 260, + VNC_AUTH_VENCRYPT_X509VNC = 261, + VNC_AUTH_VENCRYPT_X509PLAIN = 262, + VNC_AUTH_VENCRYPT_X509SASL = 263, + VNC_AUTH_VENCRYPT_TLSSASL = 264, +}; + + +/***************************************************************************** + * + * Encoding types + * + *****************************************************************************/ + +#define VNC_ENCODING_RAW 0x00000000 +#define VNC_ENCODING_COPYRECT 0x00000001 +#define VNC_ENCODING_RRE 0x00000002 +#define VNC_ENCODING_CORRE 0x00000004 +#define VNC_ENCODING_HEXTILE 0x00000005 +#define VNC_ENCODING_ZLIB 0x00000006 +#define VNC_ENCODING_TIGHT 0x00000007 +#define VNC_ENCODING_ZLIBHEX 0x00000008 +#define VNC_ENCODING_TRLE 0x0000000f +#define VNC_ENCODING_ZRLE 0x00000010 +#define VNC_ENCODING_ZYWRLE 0x00000011 +#define VNC_ENCODING_COMPRESSLEVEL0 0xFFFFFF00 /* -256 */ +#define VNC_ENCODING_QUALITYLEVEL0 0xFFFFFFE0 /* -32 */ +#define VNC_ENCODING_XCURSOR 0xFFFFFF10 /* -240 */ +#define VNC_ENCODING_RICH_CURSOR 0xFFFFFF11 /* -239 */ +#define VNC_ENCODING_POINTER_POS 0xFFFFFF18 /* -232 */ +#define VNC_ENCODING_LASTRECT 0xFFFFFF20 /* -224 */ +#define VNC_ENCODING_DESKTOPRESIZE 0xFFFFFF21 /* -223 */ +#define VNC_ENCODING_POINTER_TYPE_CHANGE 0XFFFFFEFF /* -257 */ +#define VNC_ENCODING_EXT_KEY_EVENT 0XFFFFFEFE /* -258 */ +#define VNC_ENCODING_AUDIO 0XFFFFFEFD /* -259 */ +#define VNC_ENCODING_TIGHT_PNG 0xFFFFFEFC /* -260 */ +#define VNC_ENCODING_LED_STATE 0XFFFFFEFB /* -261 */ +#define VNC_ENCODING_DESKTOP_RESIZE_EXT 0XFFFFFECC /* -308 */ +#define VNC_ENCODING_XVP 0XFFFFFECB /* -309 */ +#define VNC_ENCODING_ALPHA_CURSOR 0XFFFFFEC6 /* -314 */ +#define VNC_ENCODING_WMVi 0x574D5669 +#define VNC_ENCODING_CLIPBOARD_EXT 0xc0a1e5ce + +/***************************************************************************** + * + * Other tight constants + * + *****************************************************************************/ + +/* + * Vendors known by TightVNC: standard VNC/RealVNC, TridiaVNC, and TightVNC. + */ + +#define VNC_TIGHT_CCB_RESET_MASK (0x0f) +#define VNC_TIGHT_CCB_TYPE_MASK (0x0f << 4) +#define VNC_TIGHT_CCB_TYPE_FILL (0x08 << 4) +#define VNC_TIGHT_CCB_TYPE_JPEG (0x09 << 4) +#define VNC_TIGHT_CCB_TYPE_PNG (0x0A << 4) +#define VNC_TIGHT_CCB_BASIC_MAX (0x07 << 4) +#define VNC_TIGHT_CCB_BASIC_ZLIB (0x03 << 4) +#define VNC_TIGHT_CCB_BASIC_FILTER (0x04 << 4) + +/***************************************************************************** + * + * Features + * + *****************************************************************************/ + +enum VncFeatures { + VNC_FEATURE_RESIZE, + VNC_FEATURE_RESIZE_EXT, + VNC_FEATURE_HEXTILE, + VNC_FEATURE_POINTER_TYPE_CHANGE, + VNC_FEATURE_WMVI, + VNC_FEATURE_TIGHT, + VNC_FEATURE_ZLIB, + VNC_FEATURE_RICH_CURSOR, + VNC_FEATURE_ALPHA_CURSOR, + VNC_FEATURE_TIGHT_PNG, + VNC_FEATURE_ZRLE, + VNC_FEATURE_ZYWRLE, + VNC_FEATURE_LED_STATE, + VNC_FEATURE_XVP, + VNC_FEATURE_CLIPBOARD_EXT, +}; + +#define VNC_FEATURE_RESIZE_MASK (1 << VNC_FEATURE_RESIZE) +#define VNC_FEATURE_RESIZE_EXT_MASK (1 << VNC_FEATURE_RESIZE_EXT) +#define VNC_FEATURE_HEXTILE_MASK (1 << VNC_FEATURE_HEXTILE) +#define VNC_FEATURE_POINTER_TYPE_CHANGE_MASK (1 << VNC_FEATURE_POINTER_TYPE_CHANGE) +#define VNC_FEATURE_WMVI_MASK (1 << VNC_FEATURE_WMVI) +#define VNC_FEATURE_TIGHT_MASK (1 << VNC_FEATURE_TIGHT) +#define VNC_FEATURE_ZLIB_MASK (1 << VNC_FEATURE_ZLIB) +#define VNC_FEATURE_RICH_CURSOR_MASK (1 << VNC_FEATURE_RICH_CURSOR) +#define VNC_FEATURE_ALPHA_CURSOR_MASK (1 << VNC_FEATURE_ALPHA_CURSOR) +#define VNC_FEATURE_TIGHT_PNG_MASK (1 << VNC_FEATURE_TIGHT_PNG) +#define VNC_FEATURE_ZRLE_MASK (1 << VNC_FEATURE_ZRLE) +#define VNC_FEATURE_ZYWRLE_MASK (1 << VNC_FEATURE_ZYWRLE) +#define VNC_FEATURE_LED_STATE_MASK (1 << VNC_FEATURE_LED_STATE) +#define VNC_FEATURE_XVP_MASK (1 << VNC_FEATURE_XVP) +#define VNC_FEATURE_CLIPBOARD_EXT_MASK (1 << VNC_FEATURE_CLIPBOARD_EXT) + + +/* Client -> Server message IDs */ +#define VNC_MSG_CLIENT_SET_PIXEL_FORMAT 0 +#define VNC_MSG_CLIENT_SET_ENCODINGS 2 +#define VNC_MSG_CLIENT_FRAMEBUFFER_UPDATE_REQUEST 3 +#define VNC_MSG_CLIENT_KEY_EVENT 4 +#define VNC_MSG_CLIENT_POINTER_EVENT 5 +#define VNC_MSG_CLIENT_CUT_TEXT 6 +#define VNC_MSG_CLIENT_VMWARE_0 127 +#define VNC_MSG_CLIENT_CALL_CONTROL 249 +#define VNC_MSG_CLIENT_XVP 250 +#define VNC_MSG_CLIENT_SET_DESKTOP_SIZE 251 +#define VNC_MSG_CLIENT_TIGHT 252 +#define VNC_MSG_CLIENT_GII 253 +#define VNC_MSG_CLIENT_VMWARE_1 254 +#define VNC_MSG_CLIENT_QEMU 255 + +/* Server -> Client message IDs */ +#define VNC_MSG_SERVER_FRAMEBUFFER_UPDATE 0 +#define VNC_MSG_SERVER_SET_COLOUR_MAP_ENTRIES 1 +#define VNC_MSG_SERVER_BELL 2 +#define VNC_MSG_SERVER_CUT_TEXT 3 +#define VNC_MSG_SERVER_VMWARE_0 127 +#define VNC_MSG_SERVER_CALL_CONTROL 249 +#define VNC_MSG_SERVER_XVP 250 +#define VNC_MSG_SERVER_TIGHT 252 +#define VNC_MSG_SERVER_GII 253 +#define VNC_MSG_SERVER_VMWARE_1 254 +#define VNC_MSG_SERVER_QEMU 255 + + + +/* QEMU client -> server message IDs */ +#define VNC_MSG_CLIENT_QEMU_EXT_KEY_EVENT 0 +#define VNC_MSG_CLIENT_QEMU_AUDIO 1 + +/* QEMU server -> client message IDs */ +#define VNC_MSG_SERVER_QEMU_AUDIO 1 + + + +/* QEMU client -> server audio message IDs */ +#define VNC_MSG_CLIENT_QEMU_AUDIO_ENABLE 0 +#define VNC_MSG_CLIENT_QEMU_AUDIO_DISABLE 1 +#define VNC_MSG_CLIENT_QEMU_AUDIO_SET_FORMAT 2 + +/* QEMU server -> client audio message IDs */ +#define VNC_MSG_SERVER_QEMU_AUDIO_END 0 +#define VNC_MSG_SERVER_QEMU_AUDIO_BEGIN 1 +#define VNC_MSG_SERVER_QEMU_AUDIO_DATA 2 + +/* XVP server -> client status code */ +#define VNC_XVP_CODE_FAIL 0 +#define VNC_XVP_CODE_INIT 1 + +/* XVP client -> server action request */ +#define VNC_XVP_ACTION_SHUTDOWN 2 +#define VNC_XVP_ACTION_REBOOT 3 +#define VNC_XVP_ACTION_RESET 4 + +/* extended clipboard flags */ +#define VNC_CLIPBOARD_TEXT (1 << 0) +#define VNC_CLIPBOARD_RTF (1 << 1) +#define VNC_CLIPBOARD_HTML (1 << 2) +#define VNC_CLIPBOARD_DIB (1 << 3) +#define VNC_CLIPBOARD_FILES (1 << 4) +#define VNC_CLIPBOARD_CAPS (1 << 24) +#define VNC_CLIPBOARD_REQUEST (1 << 25) +#define VNC_CLIPBOARD_PEEK (1 << 26) +#define VNC_CLIPBOARD_NOTIFY (1 << 27) +#define VNC_CLIPBOARD_PROVIDE (1 << 28) + +/***************************************************************************** + * + * Internal APIs + * + *****************************************************************************/ + +/* Event loop functions */ +gboolean vnc_client_io(QIOChannel *ioc, + GIOCondition condition, + void *opaque); + +size_t vnc_client_read_buf(VncState *vs, uint8_t *data, size_t datalen); +size_t vnc_client_write_buf(VncState *vs, const uint8_t *data, size_t datalen); + +/* Protocol I/O functions */ +void vnc_write(VncState *vs, const void *data, size_t len); +void vnc_write_u32(VncState *vs, uint32_t value); +void vnc_write_s32(VncState *vs, int32_t value); +void vnc_write_u16(VncState *vs, uint16_t value); +void vnc_write_u8(VncState *vs, uint8_t value); +void vnc_flush(VncState *vs); +void vnc_read_when(VncState *vs, VncReadEvent *func, size_t expecting); +void vnc_disconnect_finish(VncState *vs); +void vnc_start_protocol(VncState *vs); + + +/* Buffer I/O functions */ +uint32_t read_u32(uint8_t *data, size_t offset); + +/* Protocol stage functions */ +void vnc_client_error(VncState *vs); +size_t vnc_client_io_error(VncState *vs, ssize_t ret, Error *err); + +void start_client_init(VncState *vs); +void start_auth_vnc(VncState *vs); + + +/* Misc helpers */ + +static inline uint32_t vnc_has_feature(VncState *vs, int feature) { + return (vs->features & (1 << feature)); +} + +/* Framebuffer */ +void vnc_framebuffer_update(VncState *vs, int x, int y, int w, int h, + int32_t encoding); + +/* server fb is in PIXMAN_x8r8g8b8 */ +#define VNC_SERVER_FB_FORMAT PIXMAN_FORMAT(32, PIXMAN_TYPE_ARGB, 0, 8, 8, 8) +#define VNC_SERVER_FB_BITS (PIXMAN_FORMAT_BPP(VNC_SERVER_FB_FORMAT)) +#define VNC_SERVER_FB_BYTES ((VNC_SERVER_FB_BITS+7)/8) + +void *vnc_server_fb_ptr(VncDisplay *vd, int x, int y); +int vnc_server_fb_stride(VncDisplay *vd); + +void vnc_convert_pixel(VncState *vs, uint8_t *buf, uint32_t v); +double vnc_update_freq(VncState *vs, int x, int y, int w, int h); +void vnc_sent_lossy_rect(VncState *vs, int x, int y, int w, int h); + +/* Encodings */ +int vnc_send_framebuffer_update(VncState *vs, int x, int y, int w, int h); + +int vnc_raw_send_framebuffer_update(VncState *vs, int x, int y, int w, int h); + +int vnc_hextile_send_framebuffer_update(VncState *vs, int x, + int y, int w, int h); +void vnc_hextile_set_pixel_conversion(VncState *vs, int generic); + +void *vnc_zlib_zalloc(void *x, unsigned items, unsigned size); +void vnc_zlib_zfree(void *x, void *addr); +int vnc_zlib_send_framebuffer_update(VncState *vs, int x, int y, int w, int h); +void vnc_zlib_clear(VncState *vs); + +int vnc_tight_send_framebuffer_update(VncState *vs, int x, int y, int w, int h); +int vnc_tight_png_send_framebuffer_update(VncState *vs, int x, int y, + int w, int h); +void vnc_tight_clear(VncState *vs); + +int vnc_zrle_send_framebuffer_update(VncState *vs, int x, int y, int w, int h); +int vnc_zywrle_send_framebuffer_update(VncState *vs, int x, int y, int w, int h); +void vnc_zrle_clear(VncState *vs); + +/* vnc-clipboard.c */ +void vnc_server_cut_text_caps(VncState *vs); +void vnc_client_cut_text(VncState *vs, size_t len, uint8_t *text); +void vnc_client_cut_text_ext(VncState *vs, int32_t len, uint32_t flags, uint8_t *data); + +#endif /* QEMU_VNC_H */ diff --git a/ui/vnc_keysym.h b/ui/vnc_keysym.h new file mode 100644 index 000000000..e8a2ec73c --- /dev/null +++ b/ui/vnc_keysym.h @@ -0,0 +1,722 @@ + +#include "keymaps.h" + +static const name2keysym_t name2keysym[]={ +/* ascii */ + { "space", 0x020}, + { "exclam", 0x021}, + { "quotedbl", 0x022}, + { "numbersign", 0x023}, + { "dollar", 0x024}, + { "percent", 0x025}, + { "ampersand", 0x026}, + { "apostrophe", 0x027}, + { "parenleft", 0x028}, + { "parenright", 0x029}, + { "asterisk", 0x02a}, + { "plus", 0x02b}, + { "comma", 0x02c}, + { "minus", 0x02d}, + { "period", 0x02e}, + { "slash", 0x02f}, + { "0", 0x030}, + { "1", 0x031}, + { "2", 0x032}, + { "3", 0x033}, + { "4", 0x034}, + { "5", 0x035}, + { "6", 0x036}, + { "7", 0x037}, + { "8", 0x038}, + { "9", 0x039}, + { "colon", 0x03a}, + { "semicolon", 0x03b}, + { "less", 0x03c}, + { "equal", 0x03d}, + { "greater", 0x03e}, + { "question", 0x03f}, + { "at", 0x040}, + { "A", 0x041}, + { "B", 0x042}, + { "C", 0x043}, + { "D", 0x044}, + { "E", 0x045}, + { "F", 0x046}, + { "G", 0x047}, + { "H", 0x048}, + { "I", 0x049}, + { "J", 0x04a}, + { "K", 0x04b}, + { "L", 0x04c}, + { "M", 0x04d}, + { "N", 0x04e}, + { "O", 0x04f}, + { "P", 0x050}, + { "Q", 0x051}, + { "R", 0x052}, + { "S", 0x053}, + { "T", 0x054}, + { "U", 0x055}, + { "V", 0x056}, + { "W", 0x057}, + { "X", 0x058}, + { "Y", 0x059}, + { "Z", 0x05a}, + { "bracketleft", 0x05b}, + { "backslash", 0x05c}, + { "bracketright", 0x05d}, + { "asciicircum", 0x05e}, + { "underscore", 0x05f}, + { "grave", 0x060}, + { "a", 0x061}, + { "b", 0x062}, + { "c", 0x063}, + { "d", 0x064}, + { "e", 0x065}, + { "f", 0x066}, + { "g", 0x067}, + { "h", 0x068}, + { "i", 0x069}, + { "j", 0x06a}, + { "k", 0x06b}, + { "l", 0x06c}, + { "m", 0x06d}, + { "n", 0x06e}, + { "o", 0x06f}, + { "p", 0x070}, + { "q", 0x071}, + { "r", 0x072}, + { "s", 0x073}, + { "t", 0x074}, + { "u", 0x075}, + { "v", 0x076}, + { "w", 0x077}, + { "x", 0x078}, + { "y", 0x079}, + { "z", 0x07a}, + { "braceleft", 0x07b}, + { "bar", 0x07c}, + { "braceright", 0x07d}, + { "asciitilde", 0x07e}, + +/* latin 1 extensions */ +{ "nobreakspace", 0x0a0}, +{ "exclamdown", 0x0a1}, +{ "cent", 0x0a2}, +{ "sterling", 0x0a3}, +{ "currency", 0x0a4}, +{ "yen", 0x0a5}, +{ "brokenbar", 0x0a6}, +{ "section", 0x0a7}, +{ "diaeresis", 0x0a8}, +{ "copyright", 0x0a9}, +{ "ordfeminine", 0x0aa}, +{ "guillemotleft", 0x0ab}, +{ "notsign", 0x0ac}, +{ "hyphen", 0x0ad}, +{ "registered", 0x0ae}, +{ "macron", 0x0af}, +{ "degree", 0x0b0}, +{ "plusminus", 0x0b1}, +{ "twosuperior", 0x0b2}, +{ "threesuperior", 0x0b3}, +{ "acute", 0x0b4}, +{ "mu", 0x0b5}, +{ "paragraph", 0x0b6}, +{ "periodcentered", 0x0b7}, +{ "cedilla", 0x0b8}, +{ "onesuperior", 0x0b9}, +{ "masculine", 0x0ba}, +{ "guillemotright", 0x0bb}, +{ "onequarter", 0x0bc}, +{ "onehalf", 0x0bd}, +{ "threequarters", 0x0be}, +{ "questiondown", 0x0bf}, +{ "Agrave", 0x0c0}, +{ "Aacute", 0x0c1}, +{ "Acircumflex", 0x0c2}, +{ "Atilde", 0x0c3}, +{ "Adiaeresis", 0x0c4}, +{ "Aring", 0x0c5}, +{ "AE", 0x0c6}, +{ "Ccedilla", 0x0c7}, +{ "Egrave", 0x0c8}, +{ "Eacute", 0x0c9}, +{ "Ecircumflex", 0x0ca}, +{ "Ediaeresis", 0x0cb}, +{ "Igrave", 0x0cc}, +{ "Iacute", 0x0cd}, +{ "Icircumflex", 0x0ce}, +{ "Idiaeresis", 0x0cf}, +{ "ETH", 0x0d0}, +{ "Eth", 0x0d0}, +{ "Ntilde", 0x0d1}, +{ "Ograve", 0x0d2}, +{ "Oacute", 0x0d3}, +{ "Ocircumflex", 0x0d4}, +{ "Otilde", 0x0d5}, +{ "Odiaeresis", 0x0d6}, +{ "multiply", 0x0d7}, +{ "Ooblique", 0x0d8}, +{ "Oslash", 0x0d8}, +{ "Ugrave", 0x0d9}, +{ "Uacute", 0x0da}, +{ "Ucircumflex", 0x0db}, +{ "Udiaeresis", 0x0dc}, +{ "Yacute", 0x0dd}, +{ "THORN", 0x0de}, +{ "Thorn", 0x0de}, +{ "ssharp", 0x0df}, +{ "agrave", 0x0e0}, +{ "aacute", 0x0e1}, +{ "acircumflex", 0x0e2}, +{ "atilde", 0x0e3}, +{ "adiaeresis", 0x0e4}, +{ "aring", 0x0e5}, +{ "ae", 0x0e6}, +{ "ccedilla", 0x0e7}, +{ "egrave", 0x0e8}, +{ "eacute", 0x0e9}, +{ "ecircumflex", 0x0ea}, +{ "ediaeresis", 0x0eb}, +{ "igrave", 0x0ec}, +{ "iacute", 0x0ed}, +{ "icircumflex", 0x0ee}, +{ "idiaeresis", 0x0ef}, +{ "eth", 0x0f0}, +{ "ntilde", 0x0f1}, +{ "ograve", 0x0f2}, +{ "oacute", 0x0f3}, +{ "ocircumflex", 0x0f4}, +{ "otilde", 0x0f5}, +{ "odiaeresis", 0x0f6}, +{ "division", 0x0f7}, +{ "oslash", 0x0f8}, +{ "ooblique", 0x0f8}, +{ "ugrave", 0x0f9}, +{ "uacute", 0x0fa}, +{ "ucircumflex", 0x0fb}, +{ "udiaeresis", 0x0fc}, +{ "yacute", 0x0fd}, +{ "thorn", 0x0fe}, +{ "ydiaeresis", 0x0ff}, +{"EuroSign", 0x20ac}, /* XK_EuroSign */ + +/* latin 2 - Polish national characters */ +{ "eogonek", 0x1ea}, +{ "Eogonek", 0x1ca}, +{ "aogonek", 0x1b1}, +{ "Aogonek", 0x1a1}, +{ "sacute", 0x1b6}, +{ "Sacute", 0x1a6}, +{ "lstroke", 0x1b3}, +{ "Lstroke", 0x1a3}, +{ "zabovedot", 0x1bf}, +{ "Zabovedot", 0x1af}, +{ "zacute", 0x1bc}, +{ "Zacute", 0x1ac}, +{ "Odoubleacute", 0x1d5}, +{ "Udoubleacute", 0x1db}, +{ "cacute", 0x1e6}, +{ "Cacute", 0x1c6}, +{ "nacute", 0x1f1}, +{ "Nacute", 0x1d1}, +{ "odoubleacute", 0x1f5}, +{ "udoubleacute", 0x1fb}, + +/* Czech national characters */ +{ "ecaron", 0x1ec}, +{ "scaron", 0x1b9}, +{ "ccaron", 0x1e8}, +{ "rcaron", 0x1f8}, +{ "zcaron", 0x1be}, +{ "uring", 0x1f9}, + + /* modifiers */ +{"ISO_Level3_Shift", 0xfe03}, /* XK_ISO_Level3_Shift */ +{"Control_L", 0xffe3}, /* XK_Control_L */ +{"Control_R", 0xffe4}, /* XK_Control_R */ +{"Alt_L", 0xffe9}, /* XK_Alt_L */ +{"Alt_R", 0xffea}, /* XK_Alt_R */ +{"Caps_Lock", 0xffe5}, /* XK_Caps_Lock */ +{"Meta_L", 0xffe7}, /* XK_Meta_L */ +{"Meta_R", 0xffe8}, /* XK_Meta_R */ +{"Shift_L", 0xffe1}, /* XK_Shift_L */ +{"Shift_R", 0xffe2}, /* XK_Shift_R */ +{"Super_L", 0xffeb}, /* XK_Super_L */ +{"Super_R", 0xffec}, /* XK_Super_R */ + + /* special keys */ +{"BackSpace", 0xff08}, /* XK_BackSpace */ +{"Tab", 0xff09}, /* XK_Tab */ +{"Return", 0xff0d}, /* XK_Return */ +{"Right", 0xff53}, /* XK_Right */ +{"Left", 0xff51}, /* XK_Left */ +{"Up", 0xff52}, /* XK_Up */ +{"Down", 0xff54}, /* XK_Down */ +{"Next", 0xff56}, +{"Page_Down", 0xff56}, /* XK_Page_Down */ +{"Prior", 0xff55}, +{"Page_Up", 0xff55}, /* XK_Page_Up */ +{"Insert", 0xff63}, /* XK_Insert */ +{"Delete", 0xffff}, /* XK_Delete */ +{"Home", 0xff50}, /* XK_Home */ +{"End", 0xff57}, /* XK_End */ +{"Scroll_Lock", 0xff14}, /* XK_Scroll_Lock */ +{"KP_Home", 0xff95}, +{"KP_Left", 0xff96}, +{"KP_Up", 0xff97}, +{"KP_Right", 0xff98}, +{"KP_Down", 0xff99}, +{"KP_Prior", 0xff9a}, +{"KP_Page_Up", 0xff9a}, +{"KP_Next", 0xff9b}, +{"KP_Page_Down", 0xff9b}, +{"KP_End", 0xff9c}, +{"KP_Begin", 0xff9d}, +{"KP_Insert", 0xff9e}, +{"KP_Delete", 0xff9f}, +{"F1", 0xffbe}, /* XK_F1 */ +{"F2", 0xffbf}, /* XK_F2 */ +{"F3", 0xffc0}, /* XK_F3 */ +{"F4", 0xffc1}, /* XK_F4 */ +{"F5", 0xffc2}, /* XK_F5 */ +{"F6", 0xffc3}, /* XK_F6 */ +{"F7", 0xffc4}, /* XK_F7 */ +{"F8", 0xffc5}, /* XK_F8 */ +{"F9", 0xffc6}, /* XK_F9 */ +{"F10", 0xffc7}, /* XK_F10 */ +{"F11", 0xffc8}, /* XK_F11 */ +{"F12", 0xffc9}, /* XK_F12 */ +{"F13", 0xffca}, /* XK_F13 */ +{"F14", 0xffcb}, /* XK_F14 */ +{"F15", 0xffcc}, /* XK_F15 */ +{"Sys_Req", 0xff15}, /* XK_Sys_Req */ +{"KP_0", 0xffb0}, /* XK_KP_0 */ +{"KP_1", 0xffb1}, /* XK_KP_1 */ +{"KP_2", 0xffb2}, /* XK_KP_2 */ +{"KP_3", 0xffb3}, /* XK_KP_3 */ +{"KP_4", 0xffb4}, /* XK_KP_4 */ +{"KP_5", 0xffb5}, /* XK_KP_5 */ +{"KP_6", 0xffb6}, /* XK_KP_6 */ +{"KP_7", 0xffb7}, /* XK_KP_7 */ +{"KP_8", 0xffb8}, /* XK_KP_8 */ +{"KP_9", 0xffb9}, /* XK_KP_9 */ +{"KP_Add", 0xffab}, /* XK_KP_Add */ +{"KP_Separator", 0xffac},/* XK_KP_Separator */ +{"KP_Decimal", 0xffae}, /* XK_KP_Decimal */ +{"KP_Divide", 0xffaf}, /* XK_KP_Divide */ +{"KP_Enter", 0xff8d}, /* XK_KP_Enter */ +{"KP_Equal", 0xffbd}, /* XK_KP_Equal */ +{"KP_Multiply", 0xffaa}, /* XK_KP_Multiply */ +{"KP_Subtract", 0xffad}, /* XK_KP_Subtract */ +{"help", 0xff6a}, /* XK_Help */ +{"Menu", 0xff67}, /* XK_Menu */ +{"Print", 0xff61}, /* XK_Print */ +{"Mode_switch", 0xff7e}, /* XK_Mode_switch */ +{"Num_Lock", 0xff7f}, /* XK_Num_Lock */ +{"Pause", 0xff13}, /* XK_Pause */ +{"Escape", 0xff1b}, /* XK_Escape */ + +/* dead keys */ +{"dead_grave", 0xfe50}, /* XK_dead_grave */ +{"dead_acute", 0xfe51}, /* XK_dead_acute */ +{"dead_circumflex", 0xfe52}, /* XK_dead_circumflex */ +{"dead_tilde", 0xfe53}, /* XK_dead_tilde */ +{"dead_macron", 0xfe54}, /* XK_dead_macron */ +{"dead_breve", 0xfe55}, /* XK_dead_breve */ +{"dead_abovedot", 0xfe56}, /* XK_dead_abovedot */ +{"dead_diaeresis", 0xfe57}, /* XK_dead_diaeresis */ +{"dead_abovering", 0xfe58}, /* XK_dead_abovering */ +{"dead_doubleacute", 0xfe59}, /* XK_dead_doubleacute */ +{"dead_caron", 0xfe5a}, /* XK_dead_caron */ +{"dead_cedilla", 0xfe5b}, /* XK_dead_cedilla */ +{"dead_ogonek", 0xfe5c}, /* XK_dead_ogonek */ +{"dead_iota", 0xfe5d}, /* XK_dead_iota */ +{"dead_voiced_sound", 0xfe5e}, /* XK_dead_voiced_sound */ +{"dead_semivoiced_sound", 0xfe5f}, /* XK_dead_semivoiced_sound */ +{"dead_belowdot", 0xfe60}, /* XK_dead_belowdot */ +{"dead_hook", 0xfe61}, /* XK_dead_hook */ +{"dead_horn", 0xfe62}, /* XK_dead_horn */ + + + /* localized keys */ +{"BackApostrophe", 0xff21}, +{"Muhenkan", 0xff22}, +{"Katakana", 0xff27}, +{"Hankaku", 0xff29}, +{"Zenkaku_Hankaku", 0xff2a}, +{"Henkan_Mode_Real", 0xff23}, +{"Henkan_Mode_Ultra", 0xff3e}, +{"backslash_ja", 0xffa5}, +{"Katakana_Real", 0xff25}, +{"Eisu_toggle", 0xff30}, + +{"abovedot", 0x01ff}, /* U+02D9 DOT ABOVE */ +{"amacron", 0x03e0}, /* U+0101 LATIN SMALL LETTER A WITH MACRON */ +{"Amacron", 0x03c0}, /* U+0100 LATIN CAPITAL LETTER A WITH MACRON */ +{"Arabic_ain", 0x05d9}, /* U+0639 ARABIC LETTER AIN */ +{"Arabic_alef", 0x05c7}, /* U+0627 ARABIC LETTER ALEF */ +{"Arabic_alefmaksura", 0x05e9}, /* U+0649 ARABIC LETTER ALEF MAKSURA */ +{"Arabic_beh", 0x05c8}, /* U+0628 ARABIC LETTER BEH */ +{"Arabic_comma", 0x05ac}, /* U+060C ARABIC COMMA */ +{"Arabic_dad", 0x05d6}, /* U+0636 ARABIC LETTER DAD */ +{"Arabic_dal", 0x05cf}, /* U+062F ARABIC LETTER DAL */ +{"Arabic_damma", 0x05ef}, /* U+064F ARABIC DAMMA */ +{"Arabic_dammatan", 0x05ec}, /* U+064C ARABIC DAMMATAN */ +{"Arabic_fatha", 0x05ee}, /* U+064E ARABIC FATHA */ +{"Arabic_fathatan", 0x05eb}, /* U+064B ARABIC FATHATAN */ +{"Arabic_feh", 0x05e1}, /* U+0641 ARABIC LETTER FEH */ +{"Arabic_ghain", 0x05da}, /* U+063A ARABIC LETTER GHAIN */ +{"Arabic_ha", 0x05e7}, /* U+0647 ARABIC LETTER HEH */ +{"Arabic_hah", 0x05cd}, /* U+062D ARABIC LETTER HAH */ +{"Arabic_hamza", 0x05c1}, /* U+0621 ARABIC LETTER HAMZA */ +{"Arabic_hamzaonalef", 0x05c3}, /* U+0623 ARABIC LETTER ALEF WITH HAMZA ABOVE */ +{"Arabic_hamzaonwaw", 0x05c4}, /* U+0624 ARABIC LETTER WAW WITH HAMZA ABOVE */ +{"Arabic_hamzaonyeh", 0x05c6}, /* U+0626 ARABIC LETTER YEH WITH HAMZA ABOVE */ +{"Arabic_hamzaunderalef", 0x05c5}, /* U+0625 ARABIC LETTER ALEF WITH HAMZA BELOW */ +{"Arabic_jeem", 0x05cc}, /* U+062C ARABIC LETTER JEEM */ +{"Arabic_kaf", 0x05e3}, /* U+0643 ARABIC LETTER KAF */ +{"Arabic_kasra", 0x05f0}, /* U+0650 ARABIC KASRA */ +{"Arabic_kasratan", 0x05ed}, /* U+064D ARABIC KASRATAN */ +{"Arabic_khah", 0x05ce}, /* U+062E ARABIC LETTER KHAH */ +{"Arabic_lam", 0x05e4}, /* U+0644 ARABIC LETTER LAM */ +{"Arabic_maddaonalef", 0x05c2}, /* U+0622 ARABIC LETTER ALEF WITH MADDA ABOVE */ +{"Arabic_meem", 0x05e5}, /* U+0645 ARABIC LETTER MEEM */ +{"Arabic_noon", 0x05e6}, /* U+0646 ARABIC LETTER NOON */ +{"Arabic_qaf", 0x05e2}, /* U+0642 ARABIC LETTER QAF */ +{"Arabic_question_mark", 0x05bf}, /* U+061F ARABIC QUESTION MARK */ +{"Arabic_ra", 0x05d1}, /* U+0631 ARABIC LETTER REH */ +{"Arabic_sad", 0x05d5}, /* U+0635 ARABIC LETTER SAD */ +{"Arabic_seen", 0x05d3}, /* U+0633 ARABIC LETTER SEEN */ +{"Arabic_semicolon", 0x05bb}, /* U+061B ARABIC SEMICOLON */ +{"Arabic_shadda", 0x05f1}, /* U+0651 ARABIC SHADDA */ +{"Arabic_sheen", 0x05d4}, /* U+0634 ARABIC LETTER SHEEN */ +{"Arabic_sukun", 0x05f2}, /* U+0652 ARABIC SUKUN */ +{"Arabic_tah", 0x05d7}, /* U+0637 ARABIC LETTER TAH */ +{"Arabic_tatweel", 0x05e0}, /* U+0640 ARABIC TATWEEL */ +{"Arabic_teh", 0x05ca}, /* U+062A ARABIC LETTER TEH */ +{"Arabic_tehmarbuta", 0x05c9}, /* U+0629 ARABIC LETTER TEH MARBUTA */ +{"Arabic_thal", 0x05d0}, /* U+0630 ARABIC LETTER THAL */ +{"Arabic_theh", 0x05cb}, /* U+062B ARABIC LETTER THEH */ +{"Arabic_waw", 0x05e8}, /* U+0648 ARABIC LETTER WAW */ +{"Arabic_yeh", 0x05ea}, /* U+064A ARABIC LETTER YEH */ +{"Arabic_zah", 0x05d8}, /* U+0638 ARABIC LETTER ZAH */ +{"Arabic_zain", 0x05d2}, /* U+0632 ARABIC LETTER ZAIN */ +{"breve", 0x01a2}, /* U+02D8 BREVE */ +{"caron", 0x01b7}, /* U+02C7 CARON */ +{"Ccaron", 0x01c8}, /* U+010C LATIN CAPITAL LETTER C WITH CARON */ +{"numerosign", 0x06b0}, /* U+2116 NUMERO SIGN */ +{"Cyrillic_a", 0x06c1}, /* U+0430 CYRILLIC SMALL LETTER A */ +{"Cyrillic_A", 0x06e1}, /* U+0410 CYRILLIC CAPITAL LETTER A */ +{"Cyrillic_be", 0x06c2}, /* U+0431 CYRILLIC SMALL LETTER BE */ +{"Cyrillic_BE", 0x06e2}, /* U+0411 CYRILLIC CAPITAL LETTER BE */ +{"Cyrillic_che", 0x06de}, /* U+0447 CYRILLIC SMALL LETTER CHE */ +{"Cyrillic_CHE", 0x06fe}, /* U+0427 CYRILLIC CAPITAL LETTER CHE */ +{"Cyrillic_de", 0x06c4}, /* U+0434 CYRILLIC SMALL LETTER DE */ +{"Cyrillic_DE", 0x06e4}, /* U+0414 CYRILLIC CAPITAL LETTER DE */ +{"Cyrillic_dzhe", 0x06af}, /* U+045F CYRILLIC SMALL LETTER DZHE */ +{"Cyrillic_DZHE", 0x06bf}, /* U+040F CYRILLIC CAPITAL LETTER DZHE */ +{"Cyrillic_e", 0x06dc}, /* U+044D CYRILLIC SMALL LETTER E */ +{"Cyrillic_E", 0x06fc}, /* U+042D CYRILLIC CAPITAL LETTER E */ +{"Cyrillic_ef", 0x06c6}, /* U+0444 CYRILLIC SMALL LETTER EF */ +{"Cyrillic_EF", 0x06e6}, /* U+0424 CYRILLIC CAPITAL LETTER EF */ +{"Cyrillic_el", 0x06cc}, /* U+043B CYRILLIC SMALL LETTER EL */ +{"Cyrillic_EL", 0x06ec}, /* U+041B CYRILLIC CAPITAL LETTER EL */ +{"Cyrillic_em", 0x06cd}, /* U+043C CYRILLIC SMALL LETTER EM */ +{"Cyrillic_EM", 0x06ed}, /* U+041C CYRILLIC CAPITAL LETTER EM */ +{"Cyrillic_en", 0x06ce}, /* U+043D CYRILLIC SMALL LETTER EN */ +{"Cyrillic_EN", 0x06ee}, /* U+041D CYRILLIC CAPITAL LETTER EN */ +{"Cyrillic_er", 0x06d2}, /* U+0440 CYRILLIC SMALL LETTER ER */ +{"Cyrillic_ER", 0x06f2}, /* U+0420 CYRILLIC CAPITAL LETTER ER */ +{"Cyrillic_es", 0x06d3}, /* U+0441 CYRILLIC SMALL LETTER ES */ +{"Cyrillic_ES", 0x06f3}, /* U+0421 CYRILLIC CAPITAL LETTER ES */ +{"Cyrillic_ghe", 0x06c7}, /* U+0433 CYRILLIC SMALL LETTER GHE */ +{"Cyrillic_GHE", 0x06e7}, /* U+0413 CYRILLIC CAPITAL LETTER GHE */ +{"Cyrillic_ha", 0x06c8}, /* U+0445 CYRILLIC SMALL LETTER HA */ +{"Cyrillic_HA", 0x06e8}, /* U+0425 CYRILLIC CAPITAL LETTER HA */ +{"Cyrillic_hardsign", 0x06df}, /* U+044A CYRILLIC SMALL LETTER HARD SIGN */ +{"Cyrillic_HARDSIGN", 0x06ff}, /* U+042A CYRILLIC CAPITAL LETTER HARD SIGN */ +{"Cyrillic_i", 0x06c9}, /* U+0438 CYRILLIC SMALL LETTER I */ +{"Cyrillic_I", 0x06e9}, /* U+0418 CYRILLIC CAPITAL LETTER I */ +{"Cyrillic_ie", 0x06c5}, /* U+0435 CYRILLIC SMALL LETTER IE */ +{"Cyrillic_IE", 0x06e5}, /* U+0415 CYRILLIC CAPITAL LETTER IE */ +{"Cyrillic_io", 0x06a3}, /* U+0451 CYRILLIC SMALL LETTER IO */ +{"Cyrillic_IO", 0x06b3}, /* U+0401 CYRILLIC CAPITAL LETTER IO */ +{"Cyrillic_je", 0x06a8}, /* U+0458 CYRILLIC SMALL LETTER JE */ +{"Cyrillic_JE", 0x06b8}, /* U+0408 CYRILLIC CAPITAL LETTER JE */ +{"Cyrillic_ka", 0x06cb}, /* U+043A CYRILLIC SMALL LETTER KA */ +{"Cyrillic_KA", 0x06eb}, /* U+041A CYRILLIC CAPITAL LETTER KA */ +{"Cyrillic_lje", 0x06a9}, /* U+0459 CYRILLIC SMALL LETTER LJE */ +{"Cyrillic_LJE", 0x06b9}, /* U+0409 CYRILLIC CAPITAL LETTER LJE */ +{"Cyrillic_nje", 0x06aa}, /* U+045A CYRILLIC SMALL LETTER NJE */ +{"Cyrillic_NJE", 0x06ba}, /* U+040A CYRILLIC CAPITAL LETTER NJE */ +{"Cyrillic_o", 0x06cf}, /* U+043E CYRILLIC SMALL LETTER O */ +{"Cyrillic_O", 0x06ef}, /* U+041E CYRILLIC CAPITAL LETTER O */ +{"Cyrillic_pe", 0x06d0}, /* U+043F CYRILLIC SMALL LETTER PE */ +{"Cyrillic_PE", 0x06f0}, /* U+041F CYRILLIC CAPITAL LETTER PE */ +{"Cyrillic_sha", 0x06db}, /* U+0448 CYRILLIC SMALL LETTER SHA */ +{"Cyrillic_SHA", 0x06fb}, /* U+0428 CYRILLIC CAPITAL LETTER SHA */ +{"Cyrillic_shcha", 0x06dd}, /* U+0449 CYRILLIC SMALL LETTER SHCHA */ +{"Cyrillic_SHCHA", 0x06fd}, /* U+0429 CYRILLIC CAPITAL LETTER SHCHA */ +{"Cyrillic_shorti", 0x06ca}, /* U+0439 CYRILLIC SMALL LETTER SHORT I */ +{"Cyrillic_SHORTI", 0x06ea}, /* U+0419 CYRILLIC CAPITAL LETTER SHORT I */ +{"Cyrillic_softsign", 0x06d8}, /* U+044C CYRILLIC SMALL LETTER SOFT SIGN */ +{"Cyrillic_SOFTSIGN", 0x06f8}, /* U+042C CYRILLIC CAPITAL LETTER SOFT SIGN */ +{"Cyrillic_te", 0x06d4}, /* U+0442 CYRILLIC SMALL LETTER TE */ +{"Cyrillic_TE", 0x06f4}, /* U+0422 CYRILLIC CAPITAL LETTER TE */ +{"Cyrillic_tse", 0x06c3}, /* U+0446 CYRILLIC SMALL LETTER TSE */ +{"Cyrillic_TSE", 0x06e3}, /* U+0426 CYRILLIC CAPITAL LETTER TSE */ +{"Cyrillic_u", 0x06d5}, /* U+0443 CYRILLIC SMALL LETTER U */ +{"Cyrillic_U", 0x06f5}, /* U+0423 CYRILLIC CAPITAL LETTER U */ +{"Cyrillic_ve", 0x06d7}, /* U+0432 CYRILLIC SMALL LETTER VE */ +{"Cyrillic_VE", 0x06f7}, /* U+0412 CYRILLIC CAPITAL LETTER VE */ +{"Cyrillic_ya", 0x06d1}, /* U+044F CYRILLIC SMALL LETTER YA */ +{"Cyrillic_YA", 0x06f1}, /* U+042F CYRILLIC CAPITAL LETTER YA */ +{"Cyrillic_yeru", 0x06d9}, /* U+044B CYRILLIC SMALL LETTER YERU */ +{"Cyrillic_YERU", 0x06f9}, /* U+042B CYRILLIC CAPITAL LETTER YERU */ +{"Cyrillic_yu", 0x06c0}, /* U+044E CYRILLIC SMALL LETTER YU */ +{"Cyrillic_YU", 0x06e0}, /* U+042E CYRILLIC CAPITAL LETTER YU */ +{"Cyrillic_ze", 0x06da}, /* U+0437 CYRILLIC SMALL LETTER ZE */ +{"Cyrillic_ZE", 0x06fa}, /* U+0417 CYRILLIC CAPITAL LETTER ZE */ +{"Cyrillic_zhe", 0x06d6}, /* U+0436 CYRILLIC SMALL LETTER ZHE */ +{"Cyrillic_ZHE", 0x06f6}, /* U+0416 CYRILLIC CAPITAL LETTER ZHE */ +{"doubleacute", 0x01bd}, /* U+02DD DOUBLE ACUTE ACCENT */ +{"doublelowquotemark", 0x0afe}, /* U+201E DOUBLE LOW-9 QUOTATION MARK */ +{"downarrow", 0x08fe}, /* U+2193 DOWNWARDS ARROW */ +{"dstroke", 0x01f0}, /* U+0111 LATIN SMALL LETTER D WITH STROKE */ +{"Dstroke", 0x01d0}, /* U+0110 LATIN CAPITAL LETTER D WITH STROKE */ +{"eabovedot", 0x03ec}, /* U+0117 LATIN SMALL LETTER E WITH DOT ABOVE */ +{"Eabovedot", 0x03cc}, /* U+0116 LATIN CAPITAL LETTER E WITH DOT ABOVE */ +{"emacron", 0x03ba}, /* U+0113 LATIN SMALL LETTER E WITH MACRON */ +{"Emacron", 0x03aa}, /* U+0112 LATIN CAPITAL LETTER E WITH MACRON */ +{"endash", 0x0aaa}, /* U+2013 EN DASH */ +{"eng", 0x03bf}, /* U+014B LATIN SMALL LETTER ENG */ +{"ENG", 0x03bd}, /* U+014A LATIN CAPITAL LETTER ENG */ +{"Execute", 0xff62}, /* Execute, run, do */ +{"F16", 0xffcd}, +{"F17", 0xffce}, +{"F18", 0xffcf}, +{"F19", 0xffd0}, +{"F20", 0xffd1}, +{"F21", 0xffd2}, +{"F22", 0xffd3}, +{"F23", 0xffd4}, +{"F24", 0xffd5}, +{"F25", 0xffd6}, +{"F26", 0xffd7}, +{"F27", 0xffd8}, +{"F28", 0xffd9}, +{"F29", 0xffda}, +{"F30", 0xffdb}, +{"F31", 0xffdc}, +{"F32", 0xffdd}, +{"F33", 0xffde}, +{"F34", 0xffdf}, +{"F35", 0xffe0}, +{"fiveeighths", 0x0ac5}, /* U+215D VULGAR FRACTION FIVE EIGHTHS */ +{"gbreve", 0x02bb}, /* U+011F LATIN SMALL LETTER G WITH BREVE */ +{"Gbreve", 0x02ab}, /* U+011E LATIN CAPITAL LETTER G WITH BREVE */ +{"gcedilla", 0x03bb}, /* U+0123 LATIN SMALL LETTER G WITH CEDILLA */ +{"Gcedilla", 0x03ab}, /* U+0122 LATIN CAPITAL LETTER G WITH CEDILLA */ +{"Greek_OMEGA", 0x07d9}, /* U+03A9 GREEK CAPITAL LETTER OMEGA */ +{"Henkan_Mode", 0xff23}, /* Start/Stop Conversion */ +{"horizconnector", 0x08a3}, /*(U+2500 BOX DRAWINGS LIGHT HORIZONTAL)*/ +{"hstroke", 0x02b1}, /* U+0127 LATIN SMALL LETTER H WITH STROKE */ +{"Hstroke", 0x02a1}, /* U+0126 LATIN CAPITAL LETTER H WITH STROKE */ +{"Iabovedot", 0x02a9}, /* U+0130 LATIN CAPITAL LETTER I WITH DOT ABOVE */ +{"idotless", 0x02b9}, /* U+0131 LATIN SMALL LETTER DOTLESS I */ +{"imacron", 0x03ef}, /* U+012B LATIN SMALL LETTER I WITH MACRON */ +{"Imacron", 0x03cf}, /* U+012A LATIN CAPITAL LETTER I WITH MACRON */ +{"iogonek", 0x03e7}, /* U+012F LATIN SMALL LETTER I WITH OGONEK */ +{"Iogonek", 0x03c7}, /* U+012E LATIN CAPITAL LETTER I WITH OGONEK */ +{"ISO_First_Group", 0xfe0c}, +{"ISO_Last_Group", 0xfe0e}, +{"ISO_Next_Group", 0xfe08}, +{"kana_a", 0x04a7}, /* U+30A1 KATAKANA LETTER SMALL A */ +{"kana_A", 0x04b1}, /* U+30A2 KATAKANA LETTER A */ +{"kana_CHI", 0x04c1}, /* U+30C1 KATAKANA LETTER TI */ +{"kana_closingbracket", 0x04a3}, /* U+300D RIGHT CORNER BRACKET */ +{"kana_comma", 0x04a4}, /* U+3001 IDEOGRAPHIC COMMA */ +{"kana_conjunctive", 0x04a5}, /* U+30FB KATAKANA MIDDLE DOT */ +{"kana_e", 0x04aa}, /* U+30A7 KATAKANA LETTER SMALL E */ +{"kana_E", 0x04b4}, /* U+30A8 KATAKANA LETTER E */ +{"kana_FU", 0x04cc}, /* U+30D5 KATAKANA LETTER HU */ +{"kana_fullstop", 0x04a1}, /* U+3002 IDEOGRAPHIC FULL STOP */ +{"kana_HA", 0x04ca}, /* U+30CF KATAKANA LETTER HA */ +{"kana_HE", 0x04cd}, /* U+30D8 KATAKANA LETTER HE */ +{"kana_HI", 0x04cb}, /* U+30D2 KATAKANA LETTER HI */ +{"kana_HO", 0x04ce}, /* U+30DB KATAKANA LETTER HO */ +{"kana_i", 0x04a8}, /* U+30A3 KATAKANA LETTER SMALL I */ +{"kana_I", 0x04b2}, /* U+30A4 KATAKANA LETTER I */ +{"kana_KA", 0x04b6}, /* U+30AB KATAKANA LETTER KA */ +{"kana_KE", 0x04b9}, /* U+30B1 KATAKANA LETTER KE */ +{"kana_KI", 0x04b7}, /* U+30AD KATAKANA LETTER KI */ +{"kana_KO", 0x04ba}, /* U+30B3 KATAKANA LETTER KO */ +{"kana_KU", 0x04b8}, /* U+30AF KATAKANA LETTER KU */ +{"kana_MA", 0x04cf}, /* U+30DE KATAKANA LETTER MA */ +{"kana_ME", 0x04d2}, /* U+30E1 KATAKANA LETTER ME */ +{"kana_MI", 0x04d0}, /* U+30DF KATAKANA LETTER MI */ +{"kana_MO", 0x04d3}, /* U+30E2 KATAKANA LETTER MO */ +{"kana_MU", 0x04d1}, /* U+30E0 KATAKANA LETTER MU */ +{"kana_N", 0x04dd}, /* U+30F3 KATAKANA LETTER N */ +{"kana_NA", 0x04c5}, /* U+30CA KATAKANA LETTER NA */ +{"kana_NE", 0x04c8}, /* U+30CD KATAKANA LETTER NE */ +{"kana_NI", 0x04c6}, /* U+30CB KATAKANA LETTER NI */ +{"kana_NO", 0x04c9}, /* U+30CE KATAKANA LETTER NO */ +{"kana_NU", 0x04c7}, /* U+30CC KATAKANA LETTER NU */ +{"kana_o", 0x04ab}, /* U+30A9 KATAKANA LETTER SMALL O */ +{"kana_O", 0x04b5}, /* U+30AA KATAKANA LETTER O */ +{"kana_openingbracket", 0x04a2}, /* U+300C LEFT CORNER BRACKET */ +{"kana_RA", 0x04d7}, /* U+30E9 KATAKANA LETTER RA */ +{"kana_RE", 0x04da}, /* U+30EC KATAKANA LETTER RE */ +{"kana_RI", 0x04d8}, /* U+30EA KATAKANA LETTER RI */ +{"kana_RU", 0x04d9}, /* U+30EB KATAKANA LETTER RU */ +{"kana_SA", 0x04bb}, /* U+30B5 KATAKANA LETTER SA */ +{"kana_SE", 0x04be}, /* U+30BB KATAKANA LETTER SE */ +{"kana_SHI", 0x04bc}, /* U+30B7 KATAKANA LETTER SI */ +{"kana_SO", 0x04bf}, /* U+30BD KATAKANA LETTER SO */ +{"kana_SU", 0x04bd}, /* U+30B9 KATAKANA LETTER SU */ +{"kana_TA", 0x04c0}, /* U+30BF KATAKANA LETTER TA */ +{"kana_TE", 0x04c3}, /* U+30C6 KATAKANA LETTER TE */ +{"kana_TO", 0x04c4}, /* U+30C8 KATAKANA LETTER TO */ +{"kana_tsu", 0x04af}, /* U+30C3 KATAKANA LETTER SMALL TU */ +{"kana_TSU", 0x04c2}, /* U+30C4 KATAKANA LETTER TU */ +{"kana_u", 0x04a9}, /* U+30A5 KATAKANA LETTER SMALL U */ +{"kana_U", 0x04b3}, /* U+30A6 KATAKANA LETTER U */ +{"kana_WA", 0x04dc}, /* U+30EF KATAKANA LETTER WA */ +{"kana_WO", 0x04a6}, /* U+30F2 KATAKANA LETTER WO */ +{"kana_ya", 0x04ac}, /* U+30E3 KATAKANA LETTER SMALL YA */ +{"kana_YA", 0x04d4}, /* U+30E4 KATAKANA LETTER YA */ +{"kana_yo", 0x04ae}, /* U+30E7 KATAKANA LETTER SMALL YO */ +{"kana_YO", 0x04d6}, /* U+30E8 KATAKANA LETTER YO */ +{"kana_yu", 0x04ad}, /* U+30E5 KATAKANA LETTER SMALL YU */ +{"kana_YU", 0x04d5}, /* U+30E6 KATAKANA LETTER YU */ +{"Kanji", 0xff21}, /* Kanji, Kanji convert */ +{"kcedilla", 0x03f3}, /* U+0137 LATIN SMALL LETTER K WITH CEDILLA */ +{"Kcedilla", 0x03d3}, /* U+0136 LATIN CAPITAL LETTER K WITH CEDILLA */ +{"kra", 0x03a2}, /* U+0138 LATIN SMALL LETTER KRA */ +{"lcedilla", 0x03b6}, /* U+013C LATIN SMALL LETTER L WITH CEDILLA */ +{"Lcedilla", 0x03a6}, /* U+013B LATIN CAPITAL LETTER L WITH CEDILLA */ +{"leftarrow", 0x08fb}, /* U+2190 LEFTWARDS ARROW */ +{"leftdoublequotemark", 0x0ad2}, /* U+201C LEFT DOUBLE QUOTATION MARK */ +{"Macedonia_dse", 0x06a5}, /* U+0455 CYRILLIC SMALL LETTER DZE */ +{"Macedonia_DSE", 0x06b5}, /* U+0405 CYRILLIC CAPITAL LETTER DZE */ +{"Macedonia_gje", 0x06a2}, /* U+0453 CYRILLIC SMALL LETTER GJE */ +{"Macedonia_GJE", 0x06b2}, /* U+0403 CYRILLIC CAPITAL LETTER GJE */ +{"Macedonia_kje", 0x06ac}, /* U+045C CYRILLIC SMALL LETTER KJE */ +{"Macedonia_KJE", 0x06bc}, /* U+040C CYRILLIC CAPITAL LETTER KJE */ +{"ncedilla", 0x03f1}, /* U+0146 LATIN SMALL LETTER N WITH CEDILLA */ +{"Ncedilla", 0x03d1}, /* U+0145 LATIN CAPITAL LETTER N WITH CEDILLA */ +{"oe", 0x13bd}, /* U+0153 LATIN SMALL LIGATURE OE */ +{"OE", 0x13bc}, /* U+0152 LATIN CAPITAL LIGATURE OE */ +{"ogonek", 0x01b2}, /* U+02DB OGONEK */ +{"omacron", 0x03f2}, /* U+014D LATIN SMALL LETTER O WITH MACRON */ +{"Omacron", 0x03d2}, /* U+014C LATIN CAPITAL LETTER O WITH MACRON */ +{"oneeighth", 0x0ac3}, /* U+215B VULGAR FRACTION ONE EIGHTH */ +{"rcedilla", 0x03b3}, /* U+0157 LATIN SMALL LETTER R WITH CEDILLA */ +{"Rcedilla", 0x03a3}, /* U+0156 LATIN CAPITAL LETTER R WITH CEDILLA */ +{"rightarrow", 0x08fd}, /* U+2192 RIGHTWARDS ARROW */ +{"rightdoublequotemark", 0x0ad3}, /* U+201D RIGHT DOUBLE QUOTATION MARK */ +{"Scaron", 0x01a9}, /* U+0160 LATIN CAPITAL LETTER S WITH CARON */ +{"scedilla", 0x01ba}, /* U+015F LATIN SMALL LETTER S WITH CEDILLA */ +{"Scedilla", 0x01aa}, /* U+015E LATIN CAPITAL LETTER S WITH CEDILLA */ +{"semivoicedsound", 0x04df}, /* U+309C KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK */ +{"seveneighths", 0x0ac6}, /* U+215E VULGAR FRACTION SEVEN EIGHTHS */ +{"Thai_baht", 0x0ddf}, /* U+0E3F THAI CURRENCY SYMBOL BAHT */ +{"Thai_bobaimai", 0x0dba}, /* U+0E1A THAI CHARACTER BO BAIMAI */ +{"Thai_chochan", 0x0da8}, /* U+0E08 THAI CHARACTER CHO CHAN */ +{"Thai_chochang", 0x0daa}, /* U+0E0A THAI CHARACTER CHO CHANG */ +{"Thai_choching", 0x0da9}, /* U+0E09 THAI CHARACTER CHO CHING */ +{"Thai_chochoe", 0x0dac}, /* U+0E0C THAI CHARACTER CHO CHOE */ +{"Thai_dochada", 0x0dae}, /* U+0E0E THAI CHARACTER DO CHADA */ +{"Thai_dodek", 0x0db4}, /* U+0E14 THAI CHARACTER DO DEK */ +{"Thai_fofa", 0x0dbd}, /* U+0E1D THAI CHARACTER FO FA */ +{"Thai_fofan", 0x0dbf}, /* U+0E1F THAI CHARACTER FO FAN */ +{"Thai_hohip", 0x0dcb}, /* U+0E2B THAI CHARACTER HO HIP */ +{"Thai_honokhuk", 0x0dce}, /* U+0E2E THAI CHARACTER HO NOKHUK */ +{"Thai_khokhai", 0x0da2}, /* U+0E02 THAI CHARACTER KHO KHAI */ +{"Thai_khokhon", 0x0da5}, /* U+0E05 THAI CHARACTER KHO KHON */ +{"Thai_khokhuat", 0x0da3}, /* U+0E03 THAI CHARACTER KHO KHUAT */ +{"Thai_khokhwai", 0x0da4}, /* U+0E04 THAI CHARACTER KHO KHWAI */ +{"Thai_khorakhang", 0x0da6}, /* U+0E06 THAI CHARACTER KHO RAKHANG */ +{"Thai_kokai", 0x0da1}, /* U+0E01 THAI CHARACTER KO KAI */ +{"Thai_lakkhangyao", 0x0de5}, /* U+0E45 THAI CHARACTER LAKKHANGYAO */ +{"Thai_lekchet", 0x0df7}, /* U+0E57 THAI DIGIT SEVEN */ +{"Thai_lekha", 0x0df5}, /* U+0E55 THAI DIGIT FIVE */ +{"Thai_lekhok", 0x0df6}, /* U+0E56 THAI DIGIT SIX */ +{"Thai_lekkao", 0x0df9}, /* U+0E59 THAI DIGIT NINE */ +{"Thai_leknung", 0x0df1}, /* U+0E51 THAI DIGIT ONE */ +{"Thai_lekpaet", 0x0df8}, /* U+0E58 THAI DIGIT EIGHT */ +{"Thai_leksam", 0x0df3}, /* U+0E53 THAI DIGIT THREE */ +{"Thai_leksi", 0x0df4}, /* U+0E54 THAI DIGIT FOUR */ +{"Thai_leksong", 0x0df2}, /* U+0E52 THAI DIGIT TWO */ +{"Thai_leksun", 0x0df0}, /* U+0E50 THAI DIGIT ZERO */ +{"Thai_lochula", 0x0dcc}, /* U+0E2C THAI CHARACTER LO CHULA */ +{"Thai_loling", 0x0dc5}, /* U+0E25 THAI CHARACTER LO LING */ +{"Thai_lu", 0x0dc6}, /* U+0E26 THAI CHARACTER LU */ +{"Thai_maichattawa", 0x0deb}, /* U+0E4B THAI CHARACTER MAI CHATTAWA */ +{"Thai_maiek", 0x0de8}, /* U+0E48 THAI CHARACTER MAI EK */ +{"Thai_maihanakat", 0x0dd1}, /* U+0E31 THAI CHARACTER MAI HAN-AKAT */ +{"Thai_maitaikhu", 0x0de7}, /* U+0E47 THAI CHARACTER MAITAIKHU */ +{"Thai_maitho", 0x0de9}, /* U+0E49 THAI CHARACTER MAI THO */ +{"Thai_maitri", 0x0dea}, /* U+0E4A THAI CHARACTER MAI TRI */ +{"Thai_maiyamok", 0x0de6}, /* U+0E46 THAI CHARACTER MAIYAMOK */ +{"Thai_moma", 0x0dc1}, /* U+0E21 THAI CHARACTER MO MA */ +{"Thai_ngongu", 0x0da7}, /* U+0E07 THAI CHARACTER NGO NGU */ +{"Thai_nikhahit", 0x0ded}, /* U+0E4D THAI CHARACTER NIKHAHIT */ +{"Thai_nonen", 0x0db3}, /* U+0E13 THAI CHARACTER NO NEN */ +{"Thai_nonu", 0x0db9}, /* U+0E19 THAI CHARACTER NO NU */ +{"Thai_oang", 0x0dcd}, /* U+0E2D THAI CHARACTER O ANG */ +{"Thai_paiyannoi", 0x0dcf}, /* U+0E2F THAI CHARACTER PAIYANNOI */ +{"Thai_phinthu", 0x0dda}, /* U+0E3A THAI CHARACTER PHINTHU */ +{"Thai_phophan", 0x0dbe}, /* U+0E1E THAI CHARACTER PHO PHAN */ +{"Thai_phophung", 0x0dbc}, /* U+0E1C THAI CHARACTER PHO PHUNG */ +{"Thai_phosamphao", 0x0dc0}, /* U+0E20 THAI CHARACTER PHO SAMPHAO */ +{"Thai_popla", 0x0dbb}, /* U+0E1B THAI CHARACTER PO PLA */ +{"Thai_rorua", 0x0dc3}, /* U+0E23 THAI CHARACTER RO RUA */ +{"Thai_ru", 0x0dc4}, /* U+0E24 THAI CHARACTER RU */ +{"Thai_saraa", 0x0dd0}, /* U+0E30 THAI CHARACTER SARA A */ +{"Thai_saraaa", 0x0dd2}, /* U+0E32 THAI CHARACTER SARA AA */ +{"Thai_saraae", 0x0de1}, /* U+0E41 THAI CHARACTER SARA AE */ +{"Thai_saraaimaimalai", 0x0de4}, /* U+0E44 THAI CHARACTER SARA AI MAIMALAI */ +{"Thai_saraaimaimuan", 0x0de3}, /* U+0E43 THAI CHARACTER SARA AI MAIMUAN */ +{"Thai_saraam", 0x0dd3}, /* U+0E33 THAI CHARACTER SARA AM */ +{"Thai_sarae", 0x0de0}, /* U+0E40 THAI CHARACTER SARA E */ +{"Thai_sarai", 0x0dd4}, /* U+0E34 THAI CHARACTER SARA I */ +{"Thai_saraii", 0x0dd5}, /* U+0E35 THAI CHARACTER SARA II */ +{"Thai_sarao", 0x0de2}, /* U+0E42 THAI CHARACTER SARA O */ +{"Thai_sarau", 0x0dd8}, /* U+0E38 THAI CHARACTER SARA U */ +{"Thai_saraue", 0x0dd6}, /* U+0E36 THAI CHARACTER SARA UE */ +{"Thai_sarauee", 0x0dd7}, /* U+0E37 THAI CHARACTER SARA UEE */ +{"Thai_sarauu", 0x0dd9}, /* U+0E39 THAI CHARACTER SARA UU */ +{"Thai_sorusi", 0x0dc9}, /* U+0E29 THAI CHARACTER SO RUSI */ +{"Thai_sosala", 0x0dc8}, /* U+0E28 THAI CHARACTER SO SALA */ +{"Thai_soso", 0x0dab}, /* U+0E0B THAI CHARACTER SO SO */ +{"Thai_sosua", 0x0dca}, /* U+0E2A THAI CHARACTER SO SUA */ +{"Thai_thanthakhat", 0x0dec}, /* U+0E4C THAI CHARACTER THANTHAKHAT */ +{"Thai_thonangmontho", 0x0db1}, /* U+0E11 THAI CHARACTER THO NANGMONTHO */ +{"Thai_thophuthao", 0x0db2}, /* U+0E12 THAI CHARACTER THO PHUTHAO */ +{"Thai_thothahan", 0x0db7}, /* U+0E17 THAI CHARACTER THO THAHAN */ +{"Thai_thothan", 0x0db0}, /* U+0E10 THAI CHARACTER THO THAN */ +{"Thai_thothong", 0x0db8}, /* U+0E18 THAI CHARACTER THO THONG */ +{"Thai_thothung", 0x0db6}, /* U+0E16 THAI CHARACTER THO THUNG */ +{"Thai_topatak", 0x0daf}, /* U+0E0F THAI CHARACTER TO PATAK */ +{"Thai_totao", 0x0db5}, /* U+0E15 THAI CHARACTER TO TAO */ +{"Thai_wowaen", 0x0dc7}, /* U+0E27 THAI CHARACTER WO WAEN */ +{"Thai_yoyak", 0x0dc2}, /* U+0E22 THAI CHARACTER YO YAK */ +{"Thai_yoying", 0x0dad}, /* U+0E0D THAI CHARACTER YO YING */ +{"threeeighths", 0x0ac4}, /* U+215C VULGAR FRACTION THREE EIGHTHS */ +{"trademark", 0x0ac9}, /* U+2122 TRADE MARK SIGN */ +{"tslash", 0x03bc}, /* U+0167 LATIN SMALL LETTER T WITH STROKE */ +{"Tslash", 0x03ac}, /* U+0166 LATIN CAPITAL LETTER T WITH STROKE */ +{"umacron", 0x03fe}, /* U+016B LATIN SMALL LETTER U WITH MACRON */ +{"Umacron", 0x03de}, /* U+016A LATIN CAPITAL LETTER U WITH MACRON */ +{"uogonek", 0x03f9}, /* U+0173 LATIN SMALL LETTER U WITH OGONEK */ +{"Uogonek", 0x03d9}, /* U+0172 LATIN CAPITAL LETTER U WITH OGONEK */ +{"uparrow", 0x08fc}, /* U+2191 UPWARDS ARROW */ +{"voicedsound", 0x04de}, /* U+309B KATAKANA-HIRAGANA VOICED SOUND MARK */ +{"Zcaron", 0x01ae}, /* U+017D LATIN CAPITAL LETTER Z WITH CARON */ + +{NULL,0}, +}; diff --git a/ui/win32-kbd-hook.c b/ui/win32-kbd-hook.c new file mode 100644 index 000000000..1ac237db9 --- /dev/null +++ b/ui/win32-kbd-hook.c @@ -0,0 +1,102 @@ +/* + * This work is licensed under the terms of the GNU GPL, version 2 or + * (at your option) any later version. See the COPYING file in the + * top-level directory. + * + * The win32 keyboard hooking code was imported from project spice-gtk. + */ + +#include "qemu/osdep.h" +#include "sysemu/sysemu.h" +#include "ui/win32-kbd-hook.h" + +static Notifier win32_unhook_notifier; +static HHOOK win32_keyboard_hook; +static HWND win32_window; +static DWORD win32_grab; + +static LRESULT CALLBACK keyboard_hook_cb(int code, WPARAM wparam, LPARAM lparam) +{ + if (win32_window && code == HC_ACTION && win32_window == GetFocus()) { + KBDLLHOOKSTRUCT *hooked = (KBDLLHOOKSTRUCT *)lparam; + + if (wparam != WM_KEYUP) { + DWORD dwmsg = (hooked->flags << 24) | + ((hooked->scanCode & 0xff) << 16) | 1; + + switch (hooked->vkCode) { + case VK_CAPITAL: + /* fall through */ + case VK_SCROLL: + /* fall through */ + case VK_NUMLOCK: + /* fall through */ + case VK_LSHIFT: + /* fall through */ + case VK_RSHIFT: + /* fall through */ + case VK_RCONTROL: + /* fall through */ + case VK_LMENU: + /* fall through */ + case VK_RMENU: + break; + + case VK_LCONTROL: + /* + * When pressing AltGr, an extra VK_LCONTROL with a special + * scancode with bit 9 set is sent. Let's ignore the extra + * VK_LCONTROL, as that will make AltGr misbehave. + */ + if (hooked->scanCode & 0x200) { + return 1; + } + break; + + default: + if (win32_grab) { + SendMessage(win32_window, wparam, hooked->vkCode, dwmsg); + return 1; + } + break; + } + + } else { + switch (hooked->vkCode) { + case VK_LCONTROL: + if (hooked->scanCode & 0x200) { + return 1; + } + break; + } + } + } + + return CallNextHookEx(NULL, code, wparam, lparam); +} + +static void keyboard_hook_unhook(Notifier *n, void *data) +{ + UnhookWindowsHookEx(win32_keyboard_hook); + win32_keyboard_hook = NULL; +} + +void win32_kbd_set_window(void *hwnd) +{ + if (hwnd && !win32_keyboard_hook) { + /* note: the installing thread must have a message loop */ + win32_keyboard_hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_hook_cb, + GetModuleHandle(NULL), 0); + if (win32_keyboard_hook) { + win32_unhook_notifier.notify = keyboard_hook_unhook; + qemu_add_exit_notifier(&win32_unhook_notifier); + } + } + + win32_window = hwnd; +} + +void win32_kbd_set_grab(bool grab) +{ + win32_grab = grab; +} diff --git a/ui/x_keymap.c b/ui/x_keymap.c new file mode 100644 index 000000000..2ce7b8996 --- /dev/null +++ b/ui/x_keymap.c @@ -0,0 +1,119 @@ +/* + * QEMU X11 keymaps + * + * Copyright (C) 2009-2010 Daniel P. Berrange <dan@berrange.com> + * Copyright (C) 2017 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 2.1 as + * published by the Free Software Foundation. + */ + +#include "qemu/osdep.h" + +#include "x_keymap.h" +#include "trace.h" +#include "qemu/notify.h" +#include "ui/input.h" + +#include <X11/XKBlib.h> +#include <X11/Xutil.h> + +static gboolean check_for_xwin(Display *dpy) +{ + const char *vendor = ServerVendor(dpy); + + trace_xkeymap_vendor(vendor); + + if (strstr(vendor, "Cygwin/X")) { + return TRUE; + } + + return FALSE; +} + +static gboolean check_for_xquartz(Display *dpy) +{ + int nextensions; + int i; + gboolean match = FALSE; + char **extensions = XListExtensions(dpy, &nextensions); + for (i = 0 ; extensions != NULL && i < nextensions ; i++) { + trace_xkeymap_extension(extensions[i]); + if (strcmp(extensions[i], "Apple-WM") == 0 || + strcmp(extensions[i], "Apple-DRI") == 0) { + match = TRUE; + } + } + if (extensions) { + XFreeExtensionList(extensions); + } + + return match; +} + +const guint16 *qemu_xkeymap_mapping_table(Display *dpy, size_t *maplen) +{ + XkbDescPtr desc; + const gchar *keycodes = NULL; + const guint16 *map; + + /* There is no easy way to determine what X11 server + * and platform & keyboard driver is in use. Thus we + * do best guess heuristics. + * + * This will need more work for people with other + * X servers..... patches welcomed. + */ + + desc = XkbGetMap(dpy, + XkbGBN_AllComponentsMask, + XkbUseCoreKbd); + if (desc) { + if (XkbGetNames(dpy, XkbKeycodesNameMask, desc) == Success) { + keycodes = XGetAtomName (dpy, desc->names->keycodes); + if (!keycodes) { + g_warning("could not lookup keycode name"); + } else { + trace_xkeymap_keycodes(keycodes); + } + } + XkbFreeKeyboard(desc, XkbGBN_AllComponentsMask, True); + } + + if (check_for_xwin(dpy)) { + trace_xkeymap_keymap("xwin"); + *maplen = qemu_input_map_xorgxwin_to_qcode_len; + map = qemu_input_map_xorgxwin_to_qcode; + } else if (check_for_xquartz(dpy)) { + trace_xkeymap_keymap("xquartz"); + *maplen = qemu_input_map_xorgxquartz_to_qcode_len; + map = qemu_input_map_xorgxquartz_to_qcode; + } else if ((keycodes && g_str_has_prefix(keycodes, "evdev")) || + (XKeysymToKeycode(dpy, XK_Page_Up) == 0x70)) { + trace_xkeymap_keymap("evdev"); + *maplen = qemu_input_map_xorgevdev_to_qcode_len; + map = qemu_input_map_xorgevdev_to_qcode; + } else if ((keycodes && g_str_has_prefix(keycodes, "xfree86")) || + (XKeysymToKeycode(dpy, XK_Page_Up) == 0x63)) { + trace_xkeymap_keymap("kbd"); + *maplen = qemu_input_map_xorgkbd_to_qcode_len; + map = qemu_input_map_xorgkbd_to_qcode; + } else { + trace_xkeymap_keymap("NULL"); + g_warning("Unknown X11 keycode mapping '%s'.\n" + "Please report to qemu-devel@nongnu.org\n" + "including the following information:\n" + "\n" + " - Operating system\n" + " - X11 Server\n" + " - xprop -root\n" + " - xdpyinfo\n", + keycodes ? keycodes : "<null>"); + map = NULL; + } + if (keycodes) { + XFree((void *)keycodes); + } + return map; +} diff --git a/ui/x_keymap.h b/ui/x_keymap.h new file mode 100644 index 000000000..0395e335f --- /dev/null +++ b/ui/x_keymap.h @@ -0,0 +1,32 @@ +/* + * QEMU X11 keymaps + * + * Copyright (c) 2017 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef QEMU_X_KEYMAP_H +#define QEMU_X_KEYMAP_H + +#include <X11/Xlib.h> + +const guint16 *qemu_xkeymap_mapping_table(Display *dpy, size_t *maplen); + +#endif |