diff options
author | Daniel Baumann <mail@daniel-baumann.ch> | 2025-06-06 10:05:23 +0000 |
---|---|---|
committer | Daniel Baumann <mail@daniel-baumann.ch> | 2025-06-06 10:05:23 +0000 |
commit | 755cc582a2473d06f3a2131d506d0311cc70e9f9 (patch) | |
tree | 3efb1ddb8d57bbb4539ac0d229b384871c57820f /ui | |
parent | Initial commit. (diff) | |
download | qemu-upstream.tar.xz qemu-upstream.zip |
Adding upstream version 1:7.2+dfsg.upstream/1%7.2+dfsgupstream
Signed-off-by: Daniel Baumann <mail@daniel-baumann.ch>
Diffstat (limited to 'ui')
119 files changed, 46000 insertions, 0 deletions
diff --git a/ui/clipboard.c b/ui/clipboard.c new file mode 100644 index 00000000..3d14bffa --- /dev/null +++ b/ui/clipboard.c @@ -0,0 +1,173 @@ +#include "qemu/osdep.h" +#include "ui/clipboard.h" +#include "trace.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->notifier); +} + +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->notifier); +} + +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); + } +} + +bool qemu_clipboard_check_serial(QemuClipboardInfo *info, bool client) +{ + bool ok; + + if (!info->has_serial || + !cbinfo[info->selection] || + !cbinfo[info->selection]->has_serial) { + trace_clipboard_check_serial(-1, -1, true); + return true; + } + + if (client) { + ok = info->serial >= cbinfo[info->selection]->serial; + } else { + ok = info->serial > cbinfo[info->selection]->serial; + } + + trace_clipboard_check_serial(cbinfo[info->selection]->serial, info->serial, ok); + return ok; +} + +void qemu_clipboard_update(QemuClipboardInfo *info) +{ + QemuClipboardNotify notify = { + .type = QEMU_CLIPBOARD_UPDATE_INFO, + .info = info, + }; + assert(info->selection < QEMU_CLIPBOARD_SELECTION__COUNT); + + notifier_list_notify(&clipboard_notifiers, ¬ify); + + if (cbinfo[info->selection] != info) { + qemu_clipboard_info_unref(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_reset_serial(void) +{ + QemuClipboardNotify notify = { .type = QEMU_CLIPBOARD_RESET_SERIAL }; + int i; + + for (i = 0; i < QEMU_CLIPBOARD_SELECTION__COUNT; i++) { + QemuClipboardInfo *info = qemu_clipboard_info(i); + if (info) { + info->serial = 0; + } + } + notifier_list_notify(&clipboard_notifiers, ¬ify); +} + +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 00000000..660d3e09 --- /dev/null +++ b/ui/cocoa.m @@ -0,0 +1,2092 @@ +/* + * 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/help-texts.h" +#include "qemu-main.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/runstate-action.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; +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 left_command_key_enabled = 1; +static bool swap_opt_cmd; + +static bool stretch_video; +static NSTextField *pauseLabel; + +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; + CFMachPortRef eventsTap; +} +- (void) switchSurface:(pixman_image_t *)image; +- (void) grabMouse; +- (void) ungrabMouse; +- (void) toggleFullScreen:(id)sender; +- (void) setFullGrab:(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; + +static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEventRef cgEvent, void *userInfo) +{ + QemuCocoaView *cocoaView = userInfo; + NSEvent *event = [NSEvent eventWithCGEvent:cgEvent]; + if ([cocoaView isMouseGrabbed] && [cocoaView handleEvent:event]) { + COCOA_DEBUG("Global events tap: qemu handled the event, capturing!\n"); + return NULL; + } + COCOA_DEBUG("Global events tap: qemu did not handle the event, letting it through...\n"); + + return cgEvent; +} + +@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); + + if (eventsTap) { + CFRelease(eventsTap); + } + + [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) updateUIInfoLocked +{ + /* Must be called with the iothread lock, i.e. via 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); + CVDisplayLinkRef displayLink; + + frameSize = isFullscreen ? screenSize : [self frame].size; + + if (!CVDisplayLinkCreateWithCGDisplay(display, &displayLink)) { + CVTime period = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(displayLink); + CVDisplayLinkRelease(displayLink); + if (!(period.flags & kCVTimeIsIndefinite)) { + update_displaychangelistener(&dcl, + 1000 * period.timeValue / period.timeScale); + info.refresh_rate = (int64_t)1000 * period.timeScale / period.timeValue; + } + } + + 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, TRUE); +} + +- (void) updateUIInfo +{ + if (!allow_events) { + /* + * Don't try to tell QEMU about UI information in the application + * startup phase -- we haven't yet registered dcl with the QEMU UI + * layer. + * When cocoa_display_init() does register the dcl, the UI layer + * will call cocoa_switch(), which will call updateUIInfo, so + * we don't lose any information here. + */ + return; + } + + with_iothread_lock(^{ + [self updateUIInfoLocked]; + }); +} + +- (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) setFullGrab:(id)sender +{ + COCOA_DEBUG("QemuCocoaView: setFullGrab\n"); + + CGEventMask mask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) | CGEventMaskBit(kCGEventFlagsChanged); + eventsTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, + mask, handleTapEvent, self); + if (!eventsTap) { + warn_report("Could not create event tap, system key combos will not be captured.\n"); + return; + } else { + COCOA_DEBUG("Global events tap created! Will capture system key combos.\n"); + } + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + if (!runLoop) { + warn_report("Could not obtain current CF RunLoop, system key combos will not be captured.\n"); + return; + } + + CFRunLoopSourceRef tapEventsSrc = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventsTap, 0); + if (!tapEventsSrc ) { + warn_report("Could not obtain current CF RunLoop, system key combos will not be captured.\n"); + return; + } + + CFRunLoopAddSource(runLoop, tapEventsSrc, kCFRunLoopDefaultMode); + CFRelease(tapEventsSrc); +} + +- (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 */ + + static const 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, + }; + + static const 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 +{ + 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; + // 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)) { + if (swap_opt_cmd) { + qkbd_state_key_event(kbd, Q_KEY_CODE_META_L, false); + qkbd_state_key_event(kbd, Q_KEY_CODE_META_R, false); + } else { + qkbd_state_key_event(kbd, Q_KEY_CODE_ALT, false); + qkbd_state_key_event(kbd, Q_KEY_CODE_ALT_R, false); + } + } + if (!(modifiers & NSEventModifierFlagCommand)) { + if (swap_opt_cmd) { + qkbd_state_key_event(kbd, Q_KEY_CODE_ALT, false); + qkbd_state_key_event(kbd, Q_KEY_CODE_ALT_R, false); + } else { + 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)) { + if (swap_opt_cmd) { + [self toggleKey:Q_KEY_CODE_META_L]; + } else { + [self toggleKey:Q_KEY_CODE_ALT]; + } + } + break; + + case kVK_RightOption: + if (!!(modifiers & NSEventModifierFlagOption)) { + if (swap_opt_cmd) { + [self toggleKey:Q_KEY_CODE_META_R]; + } else { + [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) && + left_command_key_enabled) { + if (swap_opt_cmd) { + [self toggleKey:Q_KEY_CODE_ALT]; + } else { + [self toggleKey:Q_KEY_CODE_META_L]; + } + } + break; + + case kVK_RightCommand: + if (isMouseGrabbed && + !!(modifiers & NSEventModifierFlagCommand)) { + if (swap_opt_cmd) { + [self toggleKey:Q_KEY_CODE_ALT_R]; + } else { + [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)) { + 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. + */ + + /* + * We shouldn't have got a scroll event when deltaY and delta Y + * are zero, hence no harm in dropping the event + */ + if ([event deltaY] != 0 || [event deltaX] != 0) { + /* Determine if this is a scroll up or scroll down event */ + if ([event deltaY] != 0) { + buttons = ([event deltaY] > 0) ? + INPUT_BUTTON_WHEEL_UP : INPUT_BUTTON_WHEEL_DOWN; + } else if ([event deltaX] != 0) { + buttons = ([event deltaX] > 0) ? + INPUT_BUTTON_WHEEL_LEFT : INPUT_BUTTON_WHEEL_RIGHT; + } + + 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 deltaX/deltaY also report 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)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]; + } + 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; +} + +- (void)applicationWillTerminate:(NSNotification *)aNotification +{ + COCOA_DEBUG("QemuCocoaAppController: applicationWillTerminate\n"); + + with_iothread_lock(^{ + shutdown_action = SHUTDOWN_ACTION_POWEROFF; + 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 ungrabMouse]; + [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]; +} + +- (void) setFullGrab:(id)sender +{ + COCOA_DEBUG("QemuCocoaAppController: setFullGrab\n"); + + [cocoaView setFullGrab: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]; + 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", + true, false, + 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 +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + char *icon_path_c = get_relocated_path(CONFIG_QEMU_ICONDIR "/hicolor/512x512/apps/qemu.png"); + NSString *icon_path = [NSString stringWithUTF8String:icon_path_c]; + g_free(icon_path_c); + NSImage *icon = [[NSImage alloc] initWithContentsOfFile:icon_path]; + NSString *version = @"QEMU emulator version " QEMU_FULL_VERSION; + NSString *copyright = @QEMU_COPYRIGHT; + NSDictionary *options; + if (icon) { + options = @{ + NSAboutPanelOptionApplicationIcon : icon, + NSAboutPanelOptionApplicationVersion : version, + @"Copyright" : copyright, + }; + [icon release]; + } else { + options = @{ + NSAboutPanelOptionApplicationVersion : version, + @"Copyright" : copyright, + }; + } + [NSApp orderFrontStandardAboutPanelWithOptions:options]; + [pool release]; +} + +/* 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]]; + [NSApp setServicesMenu:[[NSMenu alloc] initWithTitle:@"Services"]]; + + // Application menu + menu = [[NSMenu alloc] initWithTitle:@""]; + [menu addItemWithTitle:@"About QEMU" action:@selector(do_about_menu_item:) keyEquivalent:@""]; // About QEMU + [menu addItem:[NSMenuItem separatorItem]]; //Separator + menuItem = [menu addItemWithTitle:@"Services" action:nil keyEquivalent:@""]; + [menuItem setSubmenu:[NSApp servicesMenu]]; + [menu addItem:[NSMenuItem separatorItem]]; + [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) +{ + g_autofree char *label = qemu_console_get_label(console); + + return [NSString stringWithUTF8String:label]; +} + +/* 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; + + 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", + .notifier = { .notify = cocoa_clipboard_notify }, + .request = cocoa_clipboard_request +}; + +static void cocoa_clipboard_update_info(QemuClipboardInfo *info) +{ + 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_notify(Notifier *notifier, void *data) +{ + QemuClipboardNotify *notify = data; + + switch (notify->type) { + case QEMU_CLIPBOARD_UPDATE_INFO: + cocoa_clipboard_update_info(notify->info); + return; + case QEMU_CLIPBOARD_RESET_SERIAL: + /* ignore */ + return; + } +} + +static void cocoa_clipboard_request(QemuClipboardInfo *info, + QemuClipboardType type) +{ + NSAutoreleasePool *pool; + NSData *text; + + switch (type) { + case QEMU_CLIPBOARD_TYPE_TEXT: + pool = [[NSAutoreleasePool alloc] init]; + text = [[NSPasteboard generalPasteboard] dataForType:NSPasteboardTypeString]; + if (text) { + qemu_clipboard_set_data(&cbpeer, info, type, + [text length], [text bytes], true); + } + [pool 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 qemu_default_main(): + * in main(): + * in cocoa_display_init(): + * assign cocoa_main to qemu_main + * create application, menus, etc + * in cocoa_main(): + * create qemu-main thread + * enter OSX run loop + */ + +static void *call_qemu_main(void *opaque) +{ + int status; + + COCOA_DEBUG("Second thread: calling qemu_default_main()\n"); + qemu_mutex_lock_iothread(); + status = qemu_default_main(); + qemu_mutex_unlock_iothread(); + COCOA_DEBUG("Second thread: qemu_default_main() returned, exiting\n"); + [cbowner release]; + exit(status); +} + +static int cocoa_main() +{ + QemuThread thread; + + COCOA_DEBUG("Entered %s()\n", __func__); + + qemu_mutex_unlock_iothread(); + qemu_thread_create(&thread, "qemu_main", call_qemu_main, + NULL, QEMU_THREAD_DETACHED); + + // Start the main event loop + COCOA_DEBUG("Main thread: entering OSX run loop\n"); + [NSApp run]; + COCOA_DEBUG("Main thread: left OSX run loop, which should never happen\n"); + + abort(); +} + + + +#pragma mark qemu +static void cocoa_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + 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]; + }); +} + +static void cocoa_switch(DisplayChangeListener *dcl, + DisplaySurface *surface) +{ + pixman_image_t *image = surface->image; + + COCOA_DEBUG("qemu_cocoa: cocoa_switch\n"); + + // The DisplaySurface will be freed as soon as this callback returns. + // We take a reference to the underlying pixman image here so it does + // not disappear from under our feet; the switchSurface method will + // deref the old image when it is done with it. + pixman_image_ref(image); + + dispatch_async(dispatch_get_main_queue(), ^{ + [cocoaView updateUIInfo]; + [cocoaView switchSurface:image]; + }); +} + +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) +{ + NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; + + COCOA_DEBUG("qemu_cocoa: cocoa_display_init\n"); + + qemu_main = cocoa_main; + + // 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 *controller = [[QemuCocoaAppController alloc] init]; + [NSApp setDelegate:controller]; + + /* if fullscreen mode is to be used */ + if (opts->has_full_screen && opts->full_screen) { + [NSApp activateIgnoringOtherApps: YES]; + [controller toggleFullScreen: nil]; + } + if (opts->u.cocoa.has_full_grab && opts->u.cocoa.full_grab) { + [controller setFullGrab: nil]; + } + + if (opts->has_show_cursor && opts->show_cursor) { + cursor_hide = 0; + } + if (opts->u.cocoa.has_swap_opt_cmd) { + swap_opt_cmd = opts->u.cocoa.swap_opt_cmd; + } + + if (opts->u.cocoa.has_left_command_key && !opts->u.cocoa.left_command_key) { + left_command_key_enabled = 0; + } + + // register vga output callbacks + register_displaychangelistener(&dcl); + + qemu_event_init(&cbevent, false); + cbowner = [[QemuCocoaPasteboardTypeOwner alloc] init]; + qemu_clipboard_peer_register(&cbpeer); + + [pool release]; +} + +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 00000000..8e3c9a3c --- /dev/null +++ b/ui/console-gl.c @@ -0,0 +1,162 @@ +/* + * 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))); + + if (surface->texture) { + return; + } + + 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 00000000..3c0d9b06 --- /dev/null +++ b/ui/console.c @@ -0,0 +1,2775 @@ +/* + * 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" +#ifdef CONFIG_PNG +#include <png.h> +#endif + +#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; + DisplayScanout scanout; + int dcls; + DisplayGLCtx *gl; + int gl_block; + QEMUTimer *gl_unblock_timer; + 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 bool displaychangelistener_has_dmabuf(DisplayChangeListener *dcl); +static bool console_compatible_with(QemuConsole *con, + DisplayChangeListener *dcl, Error **errp); + +static void gui_update(void *opaque) +{ + uint64_t interval = GUI_REFRESH_INTERVAL_IDLE; + uint64_t dcl_interval; + DisplayState *ds = opaque; + DisplayChangeListener *dcl; + + 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; + 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_enter_all(&con->dump_queue, NULL); + } +} + +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); + } +} + +static void graphic_hw_gl_unblock_timer(void *opaque) +{ + warn_report("console: no gl-unblock within one second"); +} + +void graphic_hw_gl_block(QemuConsole *con, bool block) +{ + uint64_t timeout; + assert(con != NULL); + + if (block) { + con->gl_block++; + } else { + con->gl_block--; + } + assert(con->gl_block >= 0); + if (!con->hw_ops->gl_block) { + return; + } + if ((block && con->gl_block != 1) || (!block && con->gl_block != 0)) { + return; + } + con->hw_ops->gl_block(con->hw, block); + + if (block) { + timeout = qemu_clock_get_ms(QEMU_CLOCK_REALTIME); + timeout += 1000; /* one sec */ + timer_mod(con->gl_unblock_timer, timeout); + } else { + timer_del(con->gl_unblock_timer); + } +} + +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); + } +} + +#ifdef CONFIG_PNG +/** + * png_save: Take a screenshot as PNG + * + * Saves screendump as a PNG file + * + * Returns true for success or false for error. + * + * @fd: File descriptor for PNG file. + * @image: Image data in pixman format. + * @errp: Pointer to an error. + */ +static bool png_save(int fd, pixman_image_t *image, Error **errp) +{ + int width = pixman_image_get_width(image); + int height = pixman_image_get_height(image); + png_struct *png_ptr; + png_info *info_ptr; + g_autoptr(pixman_image_t) linebuf = + qemu_pixman_linebuf_create(PIXMAN_a8r8g8b8, width); + uint8_t *buf = (uint8_t *)pixman_image_get_data(linebuf); + FILE *f = fdopen(fd, "wb"); + int y; + if (!f) { + error_setg_errno(errp, errno, + "Failed to create file from file descriptor"); + return false; + } + + png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, + NULL, NULL); + if (!png_ptr) { + error_setg(errp, "PNG creation failed. Unable to write struct"); + fclose(f); + return false; + } + + info_ptr = png_create_info_struct(png_ptr); + + if (!info_ptr) { + error_setg(errp, "PNG creation failed. Unable to write info"); + fclose(f); + png_destroy_write_struct(&png_ptr, &info_ptr); + return false; + } + + png_init_io(png_ptr, f); + + png_set_IHDR(png_ptr, info_ptr, width, height, 8, + PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); + + png_write_info(png_ptr, info_ptr); + + for (y = 0; y < height; ++y) { + qemu_pixman_linebuf_fill(linebuf, image, width, 0, y); + png_write_row(png_ptr, buf); + } + + png_write_end(png_ptr, NULL); + + png_destroy_write_struct(&png_ptr, &info_ptr); + + if (fclose(f) != 0) { + error_setg_errno(errp, errno, + "PNG creation failed. Unable to close file"); + return false; + } + + return true; +} + +#else /* no png support */ + +static bool png_save(int fd, pixman_image_t *image, Error **errp) +{ + error_setg(errp, "Enable PNG support with libpng for screendump"); + return false; +} + +#endif /* CONFIG_PNG */ + +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, + bool has_format, ImageFormat format, 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 (has_format && format == IMAGE_FORMAT_PNG) { + /* PNG format specified for screendump */ + if (!png_save(fd, image, errp)) { + qemu_unlink(filename); + } + } else { + /* PPM format specified/default for screendump */ + 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; + + assert(s->scanout.kind == SCANOUT_SURFACE); + + 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; + } + } +} + +static void displaychangelistener_gfx_switch(DisplayChangeListener *dcl, + struct DisplaySurface *new_surface, + bool update) +{ + if (dcl->ops->dpy_gfx_switch) { + dcl->ops->dpy_gfx_switch(dcl, new_surface); + } + + if (update && dcl->ops->dpy_gfx_update) { + dcl->ops->dpy_gfx_update(dcl, 0, 0, + surface_width(new_surface), + surface_height(new_surface)); + } +} + +static void dpy_gfx_create_texture(QemuConsole *con, DisplaySurface *surface) +{ + if (con->gl && con->gl->ops->dpy_gl_ctx_create_texture) { + con->gl->ops->dpy_gl_ctx_create_texture(con->gl, surface); + } +} + +static void dpy_gfx_destroy_texture(QemuConsole *con, DisplaySurface *surface) +{ + if (con->gl && con->gl->ops->dpy_gl_ctx_destroy_texture) { + con->gl->ops->dpy_gl_ctx_destroy_texture(con->gl, surface); + } +} + +static void dpy_gfx_update_texture(QemuConsole *con, DisplaySurface *surface, + int x, int y, int w, int h) +{ + if (con->gl && con->gl->ops->dpy_gl_ctx_update_texture) { + con->gl->ops->dpy_gl_ctx_update_texture(con->gl, surface, x, y, w, h); + } +} + +static void displaychangelistener_display_console(DisplayChangeListener *dcl, + QemuConsole *con, + Error **errp) +{ + static const char nodev[] = + "This VM has no graphic display device."; + static DisplaySurface *dummy; + + if (!con || !console_compatible_with(con, dcl, errp)) { + if (!dummy) { + dummy = qemu_create_placeholder_surface(640, 480, nodev); + } + if (con) { + dpy_gfx_create_texture(con, dummy); + } + displaychangelistener_gfx_switch(dcl, dummy, TRUE); + return; + } + + dpy_gfx_create_texture(con, con->surface); + displaychangelistener_gfx_switch(dcl, con->surface, + con->scanout.kind == SCANOUT_SURFACE); + + if (con->scanout.kind == SCANOUT_DMABUF && + displaychangelistener_has_dmabuf(dcl)) { + dcl->ops->dpy_gl_scanout_dmabuf(dcl, con->scanout.dmabuf); + } else if (con->scanout.kind == SCANOUT_TEXTURE && + dcl->ops->dpy_gl_scanout_texture) { + dcl->ops->dpy_gl_scanout_texture(dcl, + con->scanout.texture.backing_id, + con->scanout.texture.backing_y_0_top, + con->scanout.texture.backing_width, + con->scanout.texture.backing_height, + con->scanout.texture.x, + con->scanout.texture.y, + con->scanout.texture.width, + con->scanout.texture.height); + } +} + +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; + } + displaychangelistener_display_console(dcl, s, NULL); + } + } + 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, 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_TAB] = QEMU_KEY_TAB, + [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 console_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 (console_has_gl(con) && + !con->gl->ops->dpy_gl_ctx_is_compatible_dcl(con->gl, dcl)) { + error_setg(errp, "Display %s is incompatible with the GL context", + dcl->ops->dpy_name); + return false; + } + + 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 qemu_console_set_display_gl_ctx(QemuConsole *con, DisplayGLCtx *gl) +{ + /* display has opengl support */ + assert(con); + if (con->gl) { + error_report("The console already has an OpenGL context."); + exit(1); + } + con->gl = gl; +} + +void register_displaychangelistener(DisplayChangeListener *dcl) +{ + QemuConsole *con; + + assert(!dcl->ds); + + 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; + } + displaychangelistener_display_console(dcl, con, dcl->con ? &error_fatal : NULL); + 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, bool delay) +{ + 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) + (delay ? 1000 : 0)); + return 0; +} + +void dpy_gfx_update(QemuConsole *con, int x, int y, int w, int h) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + int width = qemu_console_get_width(con, x + w); + int height = qemu_console_get_height(con, y + h); + + 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; + } + dpy_gfx_update_texture(con, con->surface, x, y, w, h); + 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) +{ + int w = qemu_console_get_width(con, 0); + int h = qemu_console_get_height(con, 0); + + dpy_gfx_update(con, 0, 0, w, h); +} + +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->scanout.kind = SCANOUT_SURFACE; + con->surface = surface; + dpy_gfx_create_texture(con, surface); + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + displaychangelistener_gfx_switch(dcl, surface, FALSE); + } + dpy_gfx_destroy_texture(con, old_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) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + if (con->scanout.kind != SCANOUT_SURFACE) { + con->scanout.kind = SCANOUT_NONE; + } + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_gl_scanout_disable) { + dcl->ops->dpy_gl_scanout_disable(dcl); + } + } +} + +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) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + con->scanout.kind = SCANOUT_TEXTURE; + con->scanout.texture = (ScanoutTexture) { + backing_id, backing_y_0_top, backing_width, backing_height, + x, y, width, height + }; + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_gl_scanout_texture) { + dcl->ops->dpy_gl_scanout_texture(dcl, backing_id, + backing_y_0_top, + backing_width, backing_height, + x, y, width, height); + } + } +} + +void dpy_gl_scanout_dmabuf(QemuConsole *con, + QemuDmaBuf *dmabuf) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + con->scanout.kind = SCANOUT_DMABUF; + con->scanout.dmabuf = dmabuf; + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_gl_scanout_dmabuf) { + dcl->ops->dpy_gl_scanout_dmabuf(dcl, dmabuf); + } + } +} + +void dpy_gl_cursor_dmabuf(QemuConsole *con, QemuDmaBuf *dmabuf, + bool have_hot, uint32_t hot_x, uint32_t hot_y) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_gl_cursor_dmabuf) { + dcl->ops->dpy_gl_cursor_dmabuf(dcl, dmabuf, + have_hot, hot_x, hot_y); + } + } +} + +void dpy_gl_cursor_position(QemuConsole *con, + uint32_t pos_x, uint32_t pos_y) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_gl_cursor_position) { + dcl->ops->dpy_gl_cursor_position(dcl, pos_x, pos_y); + } + } +} + +void dpy_gl_release_dmabuf(QemuConsole *con, + QemuDmaBuf *dmabuf) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_gl_release_dmabuf) { + dcl->ops->dpy_gl_release_dmabuf(dcl, dmabuf); + } + } +} + +void dpy_gl_update(QemuConsole *con, + uint32_t x, uint32_t y, uint32_t w, uint32_t h) +{ + DisplayState *s = con->ds; + DisplayChangeListener *dcl; + + assert(con->gl); + + graphic_hw_gl_block(con, true); + QLIST_FOREACH(dcl, &s->listeners, next) { + if (con != (dcl->con ? dcl->con : active_console)) { + continue; + } + if (dcl->ops->dpy_gl_update) { + dcl->ops->dpy_gl_update(dcl, x, y, w, h); + } + } + graphic_hw_gl_block(con, false); +} + +/***********************************************************/ +/* 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); + width = qemu_console_get_width(s, 0); + height = qemu_console_get_height(s, 0); + } 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); + s->gl_unblock_timer = timer_new_ms(QEMU_CLOCK_REALTIME, + graphic_hw_gl_unblock_timer, s); + 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 = qemu_console_get_width(con, 640); + int height = qemu_console_get_height(con, 480); + + 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; +} + +bool qemu_console_is_multihead(DeviceState *dev) +{ + QemuConsole *con; + Object *obj; + uint32_t f = 0xffffffff; + 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 (f == 0xffffffff) { + f = h; + } else if (h != f) { + return true; + } + } + return false; +} + +char *qemu_console_get_label(QemuConsole *con) +{ + if (con->console_type == GRAPHIC_CONSOLE) { + if (con->device) { + DeviceState *dev; + bool multihead; + + dev = DEVICE(con->device); + multihead = qemu_console_is_multihead(dev); + if (multihead) { + return g_strdup_printf("%s.%d", dev->id ? + dev->id : + object_get_typename(con->device), + con->head); + } else { + return g_strdup_printf("%s", dev->id ? + dev->id : + 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; + } + if (con == NULL) { + return fallback; + } + switch (con->scanout.kind) { + case SCANOUT_DMABUF: + return con->scanout.dmabuf->width; + case SCANOUT_TEXTURE: + return con->scanout.texture.width; + case SCANOUT_SURFACE: + return surface_width(con->surface); + default: + return fallback; + } +} + +int qemu_console_get_height(QemuConsole *con, int fallback) +{ + if (con == NULL) { + con = active_console; + } + if (con == NULL) { + return fallback; + } + switch (con->scanout.kind) { + case SCANOUT_DMABUF: + return con->scanout.dmabuf->height; + case SCANOUT_TEXTURE: + return con->scanout.texture.height; + case SCANOUT_SURFACE: + return surface_height(con->surface); + default: + return 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->scanout.kind != SCANOUT_SURFACE) { + if (active_console && active_console->scanout.kind == SCANOUT_SURFACE) { + g_width = qemu_console_get_width(active_console, g_width); + g_height = qemu_console_get_height(active_console, g_height); + } + s->surface = qemu_create_displaysurface(g_width, g_height); + s->scanout.kind = SCANOUT_SURFACE; + } + + 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->scanout.kind = SCANOUT_SURFACE; + 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 = qemu_console_surface(s); + + assert(s->console_type == GRAPHIC_CONSOLE); + + if ((s->scanout.kind != SCANOUT_SURFACE || + (surface && surface->flags & QEMU_ALLOCATED_FLAG)) && + qemu_console_get_width(s, -1) == width && + qemu_console_get_height(s, -1) == height) { + return; + } + + surface = qemu_create_displaysurface(width, height); + dpy_gfx_replace_surface(s, surface); +} + +DisplaySurface *qemu_console_surface(QemuConsole *console) +{ + switch (console->scanout.kind) { + case SCANOUT_SURFACE: + return console->surface; + default: + return NULL; + } +} + +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) { + Error *local_err = NULL; + int rv = ui_module_load(DisplayType_str(prio[i]), &local_err); + if (rv < 0) { + error_report_err(local_err); + } + } + 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) { + Error *local_err = NULL; + int rv = ui_module_load(DisplayType_str(opts->type), &local_err); + if (rv < 0) { + error_report_err(local_err); + } + } + 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]) { + Error *local_err = NULL; + int rv = ui_module_load(DisplayType_str(idx), &local_err); + if (rv < 0) { + error_report_err(local_err); + } + } + 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 00000000..de962faa --- /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; + g_autofree cchar_t *curses_line = g_new(cchar_t, 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 00000000..71e04acd --- /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 00000000..835f0802 --- /dev/null +++ b/ui/cursor.c @@ -0,0 +1,249 @@ +#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); + assert(c != NULL); + + 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; + size_t datasize = width * height * sizeof(uint32_t); + + if (width > 512 || height > 512) { + return NULL; + } + + 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 00000000..354e7a93 --- /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 00000000..6c9ada90 --- /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/dbus-chardev.c b/ui/dbus-chardev.c new file mode 100644 index 00000000..940ef937 --- /dev/null +++ b/ui/dbus-chardev.c @@ -0,0 +1,296 @@ +/* + * QEMU DBus display + * + * Copyright (c) 2021 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 "trace.h" +#include "qapi/error.h" +#include "qemu/config-file.h" +#include "qemu/option.h" + +#include <gio/gunixfdlist.h> + +#include "dbus.h" + +static char * +dbus_display_chardev_path(DBusChardev *chr) +{ + return g_strdup_printf(DBUS_DISPLAY1_ROOT "/Chardev_%s", + CHARDEV(chr)->label); +} + +static void +dbus_display_chardev_export(DBusDisplay *dpy, DBusChardev *chr) +{ + g_autoptr(GDBusObjectSkeleton) sk = NULL; + g_autofree char *path = dbus_display_chardev_path(chr); + + if (chr->exported) { + return; + } + + sk = g_dbus_object_skeleton_new(path); + g_dbus_object_skeleton_add_interface( + sk, G_DBUS_INTERFACE_SKELETON(chr->iface)); + g_dbus_object_manager_server_export(dpy->server, sk); + chr->exported = true; +} + +static void +dbus_display_chardev_unexport(DBusDisplay *dpy, DBusChardev *chr) +{ + g_autofree char *path = dbus_display_chardev_path(chr); + + if (!chr->exported) { + return; + } + + g_dbus_object_manager_server_unexport(dpy->server, path); + chr->exported = false; +} + +static int +dbus_display_chardev_foreach(Object *obj, void *data) +{ + DBusDisplay *dpy = DBUS_DISPLAY(data); + + if (!CHARDEV_IS_DBUS(obj)) { + return 0; + } + + dbus_display_chardev_export(dpy, DBUS_CHARDEV(obj)); + + return 0; +} + +static void +dbus_display_on_notify(Notifier *notifier, void *data) +{ + DBusDisplay *dpy = container_of(notifier, DBusDisplay, notifier); + DBusDisplayEvent *event = data; + + switch (event->type) { + case DBUS_DISPLAY_CHARDEV_OPEN: + dbus_display_chardev_export(dpy, event->chardev); + break; + case DBUS_DISPLAY_CHARDEV_CLOSE: + dbus_display_chardev_unexport(dpy, event->chardev); + break; + } +} + +void +dbus_chardev_init(DBusDisplay *dpy) +{ + dpy->notifier.notify = dbus_display_on_notify; + dbus_display_notifier_add(&dpy->notifier); + + object_child_foreach(container_get(object_get_root(), "/chardevs"), + dbus_display_chardev_foreach, dpy); +} + +static gboolean +dbus_chr_register( + DBusChardev *dc, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_stream, + QemuDBusDisplay1Chardev *object) +{ + g_autoptr(GError) err = NULL; + int fd; + + fd = g_unix_fd_list_get(fd_list, g_variant_get_handle(arg_stream), &err); + if (err) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Couldn't get peer FD: %s", err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (qemu_chr_add_client(CHARDEV(dc), fd) < 0) { + g_dbus_method_invocation_return_error(invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Couldn't register FD!"); + close(fd); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + g_object_set(dc->iface, + "owner", g_dbus_method_invocation_get_sender(invocation), + NULL); + + qemu_dbus_display1_chardev_complete_register(object, invocation, NULL); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_chr_send_break( + DBusChardev *dc, + GDBusMethodInvocation *invocation, + QemuDBusDisplay1Chardev *object) +{ + qemu_chr_be_event(CHARDEV(dc), CHR_EVENT_BREAK); + + qemu_dbus_display1_chardev_complete_send_break(object, invocation); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +dbus_chr_open(Chardev *chr, ChardevBackend *backend, + bool *be_opened, Error **errp) +{ + ERRP_GUARD(); + + DBusChardev *dc = DBUS_CHARDEV(chr); + DBusDisplayEvent event = { + .type = DBUS_DISPLAY_CHARDEV_OPEN, + .chardev = dc, + }; + g_autoptr(ChardevBackend) be = NULL; + g_autoptr(QemuOpts) opts = NULL; + + dc->iface = qemu_dbus_display1_chardev_skeleton_new(); + g_object_set(dc->iface, "name", backend->u.dbus.data->name, NULL); + g_object_connect(dc->iface, + "swapped-signal::handle-register", + dbus_chr_register, dc, + "swapped-signal::handle-send-break", + dbus_chr_send_break, dc, + NULL); + + dbus_display_notify(&event); + + be = g_new0(ChardevBackend, 1); + opts = qemu_opts_create(qemu_find_opts("chardev"), NULL, 0, &error_abort); + qemu_opt_set(opts, "server", "on", &error_abort); + qemu_opt_set(opts, "wait", "off", &error_abort); + CHARDEV_CLASS(object_class_by_name(TYPE_CHARDEV_SOCKET))->parse( + opts, be, errp); + if (*errp) { + return; + } + CHARDEV_CLASS(object_class_by_name(TYPE_CHARDEV_SOCKET))->open( + chr, be, be_opened, errp); +} + +static void +dbus_chr_set_fe_open(Chardev *chr, int fe_open) +{ + DBusChardev *dc = DBUS_CHARDEV(chr); + + g_object_set(dc->iface, "feopened", fe_open, NULL); +} + +static void +dbus_chr_set_echo(Chardev *chr, bool echo) +{ + DBusChardev *dc = DBUS_CHARDEV(chr); + + g_object_set(dc->iface, "echo", echo, NULL); +} + +static void +dbus_chr_be_event(Chardev *chr, QEMUChrEvent event) +{ + DBusChardev *dc = DBUS_CHARDEV(chr); + DBusChardevClass *klass = DBUS_CHARDEV_GET_CLASS(chr); + + switch (event) { + case CHR_EVENT_CLOSED: + if (dc->iface) { + /* on finalize, iface is set to NULL */ + g_object_set(dc->iface, "owner", "", NULL); + } + break; + default: + break; + }; + + klass->parent_chr_be_event(chr, event); +} + +static void +dbus_chr_parse(QemuOpts *opts, ChardevBackend *backend, + Error **errp) +{ + const char *name = qemu_opt_get(opts, "name"); + ChardevDBus *dbus; + + if (name == NULL) { + error_setg(errp, "chardev: dbus: no name given"); + return; + } + + backend->type = CHARDEV_BACKEND_KIND_DBUS; + dbus = backend->u.dbus.data = g_new0(ChardevDBus, 1); + qemu_chr_parse_common(opts, qapi_ChardevDBus_base(dbus)); + dbus->name = g_strdup(name); +} + +static void +char_dbus_class_init(ObjectClass *oc, void *data) +{ + DBusChardevClass *klass = DBUS_CHARDEV_CLASS(oc); + ChardevClass *cc = CHARDEV_CLASS(oc); + + cc->parse = dbus_chr_parse; + cc->open = dbus_chr_open; + cc->chr_set_fe_open = dbus_chr_set_fe_open; + cc->chr_set_echo = dbus_chr_set_echo; + klass->parent_chr_be_event = cc->chr_be_event; + cc->chr_be_event = dbus_chr_be_event; +} + +static void +char_dbus_finalize(Object *obj) +{ + DBusChardev *dc = DBUS_CHARDEV(obj); + DBusDisplayEvent event = { + .type = DBUS_DISPLAY_CHARDEV_CLOSE, + .chardev = dc, + }; + + dbus_display_notify(&event); + g_clear_object(&dc->iface); +} + +static const TypeInfo char_dbus_type_info = { + .name = TYPE_CHARDEV_DBUS, + .parent = TYPE_CHARDEV_SOCKET, + .class_size = sizeof(DBusChardevClass), + .instance_size = sizeof(DBusChardev), + .instance_finalize = char_dbus_finalize, + .class_init = char_dbus_class_init, +}; +module_obj(TYPE_CHARDEV_DBUS); + +static void +register_types(void) +{ + type_register_static(&char_dbus_type_info); +} + +type_init(register_types); diff --git a/ui/dbus-clipboard.c b/ui/dbus-clipboard.c new file mode 100644 index 00000000..5843d26c --- /dev/null +++ b/ui/dbus-clipboard.c @@ -0,0 +1,457 @@ +/* + * QEMU DBus display + * + * Copyright (c) 2021 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 "qemu/dbus.h" +#include "qemu/main-loop.h" +#include "qom/object_interfaces.h" +#include "sysemu/sysemu.h" +#include "qapi/error.h" +#include "trace.h" + +#include "dbus.h" + +#define MIME_TEXT_PLAIN_UTF8 "text/plain;charset=utf-8" + +static void +dbus_clipboard_complete_request( + DBusDisplay *dpy, + GDBusMethodInvocation *invocation, + QemuClipboardInfo *info, + QemuClipboardType type) +{ + GVariant *v_data = g_variant_new_from_data( + G_VARIANT_TYPE("ay"), + info->types[type].data, + info->types[type].size, + TRUE, + (GDestroyNotify)qemu_clipboard_info_unref, + qemu_clipboard_info_ref(info)); + + qemu_dbus_display1_clipboard_complete_request( + dpy->clipboard, invocation, + MIME_TEXT_PLAIN_UTF8, v_data); +} + +static void +dbus_clipboard_update_info(DBusDisplay *dpy, QemuClipboardInfo *info) +{ + bool self_update = info->owner == &dpy->clipboard_peer; + const char *mime[QEMU_CLIPBOARD_TYPE__COUNT + 1] = { 0, }; + DBusClipboardRequest *req; + int i = 0; + + if (info->owner == NULL) { + if (dpy->clipboard_proxy) { + qemu_dbus_display1_clipboard_call_release( + dpy->clipboard_proxy, + info->selection, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } + return; + } + + if (self_update || !info->has_serial) { + return; + } + + req = &dpy->clipboard_request[info->selection]; + if (req->invocation && info->types[req->type].data) { + dbus_clipboard_complete_request(dpy, req->invocation, info, req->type); + g_clear_object(&req->invocation); + g_source_remove(req->timeout_id); + req->timeout_id = 0; + return; + } + + if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { + mime[i++] = MIME_TEXT_PLAIN_UTF8; + } + + if (i > 0) { + if (dpy->clipboard_proxy) { + qemu_dbus_display1_clipboard_call_grab( + dpy->clipboard_proxy, + info->selection, + info->serial, + mime, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + } + } +} + +static void +dbus_clipboard_reset_serial(DBusDisplay *dpy) +{ + if (dpy->clipboard_proxy) { + qemu_dbus_display1_clipboard_call_register( + dpy->clipboard_proxy, + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, NULL, NULL); + } +} + +static void +dbus_clipboard_notify(Notifier *notifier, void *data) +{ + DBusDisplay *dpy = + container_of(notifier, DBusDisplay, clipboard_peer.notifier); + QemuClipboardNotify *notify = data; + + switch (notify->type) { + case QEMU_CLIPBOARD_UPDATE_INFO: + dbus_clipboard_update_info(dpy, notify->info); + return; + case QEMU_CLIPBOARD_RESET_SERIAL: + dbus_clipboard_reset_serial(dpy); + return; + } +} + +static void +dbus_clipboard_qemu_request(QemuClipboardInfo *info, + QemuClipboardType type) +{ + DBusDisplay *dpy = container_of(info->owner, DBusDisplay, clipboard_peer); + g_autofree char *mime = NULL; + g_autoptr(GVariant) v_data = NULL; + g_autoptr(GError) err = NULL; + const char *data = NULL; + const char *mimes[] = { MIME_TEXT_PLAIN_UTF8, NULL }; + size_t n; + + if (type != QEMU_CLIPBOARD_TYPE_TEXT) { + /* unsupported atm */ + return; + } + + if (dpy->clipboard_proxy) { + if (!qemu_dbus_display1_clipboard_call_request_sync( + dpy->clipboard_proxy, + info->selection, + mimes, + G_DBUS_CALL_FLAGS_NONE, -1, &mime, &v_data, NULL, &err)) { + error_report("Failed to request clipboard: %s", err->message); + return; + } + + if (g_strcmp0(mime, MIME_TEXT_PLAIN_UTF8)) { + error_report("Unsupported returned MIME: %s", mime); + return; + } + + data = g_variant_get_fixed_array(v_data, &n, 1); + qemu_clipboard_set_data(&dpy->clipboard_peer, info, type, + n, data, true); + } +} + +static void +dbus_clipboard_request_cancelled(DBusClipboardRequest *req) +{ + if (!req->invocation) { + return; + } + + g_dbus_method_invocation_return_error( + req->invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Cancelled clipboard request"); + + g_clear_object(&req->invocation); + g_source_remove(req->timeout_id); + req->timeout_id = 0; +} + +static void +dbus_clipboard_unregister_proxy(DBusDisplay *dpy) +{ + const char *name = NULL; + int i; + + for (i = 0; i < G_N_ELEMENTS(dpy->clipboard_request); ++i) { + dbus_clipboard_request_cancelled(&dpy->clipboard_request[i]); + } + + if (!dpy->clipboard_proxy) { + return; + } + + name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy)); + trace_dbus_clipboard_unregister(name); + g_clear_object(&dpy->clipboard_proxy); +} + +static void +dbus_on_clipboard_proxy_name_owner_changed( + DBusDisplay *dpy, + GObject *object, + GParamSpec *pspec) +{ + dbus_clipboard_unregister_proxy(dpy); +} + +static gboolean +dbus_clipboard_register( + DBusDisplay *dpy, + GDBusMethodInvocation *invocation) +{ + g_autoptr(GError) err = NULL; + const char *name = NULL; + + if (dpy->clipboard_proxy) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Clipboard peer already registered!"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + dpy->clipboard_proxy = + qemu_dbus_display1_clipboard_proxy_new_sync( + g_dbus_method_invocation_get_connection(invocation), + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + g_dbus_method_invocation_get_sender(invocation), + "/org/qemu/Display1/Clipboard", + NULL, + &err); + if (!dpy->clipboard_proxy) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Failed to setup proxy: %s", err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy)); + trace_dbus_clipboard_register(name); + + g_object_connect(dpy->clipboard_proxy, + "swapped-signal::notify::g-name-owner", + dbus_on_clipboard_proxy_name_owner_changed, dpy, + NULL); + qemu_clipboard_reset_serial(); + + qemu_dbus_display1_clipboard_complete_register(dpy->clipboard, invocation); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_clipboard_check_caller(DBusDisplay *dpy, GDBusMethodInvocation *invocation) +{ + if (!dpy->clipboard_proxy || + g_strcmp0(g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy)), + g_dbus_method_invocation_get_sender(invocation))) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Unregistered caller"); + return FALSE; + } + + return TRUE; +} + +static gboolean +dbus_clipboard_unregister( + DBusDisplay *dpy, + GDBusMethodInvocation *invocation) +{ + if (!dbus_clipboard_check_caller(dpy, invocation)) { + return DBUS_METHOD_INVOCATION_HANDLED; + } + + dbus_clipboard_unregister_proxy(dpy); + + qemu_dbus_display1_clipboard_complete_unregister( + dpy->clipboard, invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_clipboard_grab( + DBusDisplay *dpy, + GDBusMethodInvocation *invocation, + gint arg_selection, + guint arg_serial, + const gchar *const *arg_mimes) +{ + QemuClipboardSelection s = arg_selection; + g_autoptr(QemuClipboardInfo) info = NULL; + + if (!dbus_clipboard_check_caller(dpy, invocation)) { + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Invalid clipboard selection: %d", arg_selection); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + info = qemu_clipboard_info_new(&dpy->clipboard_peer, s); + if (g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8)) { + info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; + } + info->serial = arg_serial; + info->has_serial = true; + if (qemu_clipboard_check_serial(info, true)) { + qemu_clipboard_update(info); + } else { + trace_dbus_clipboard_grab_failed(); + } + + qemu_dbus_display1_clipboard_complete_grab(dpy->clipboard, invocation); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_clipboard_release( + DBusDisplay *dpy, + GDBusMethodInvocation *invocation, + gint arg_selection) +{ + if (!dbus_clipboard_check_caller(dpy, invocation)) { + return DBUS_METHOD_INVOCATION_HANDLED; + } + + qemu_clipboard_peer_release(&dpy->clipboard_peer, arg_selection); + + qemu_dbus_display1_clipboard_complete_release(dpy->clipboard, invocation); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_clipboard_request_timeout(gpointer user_data) +{ + dbus_clipboard_request_cancelled(user_data); + return G_SOURCE_REMOVE; +} + +static gboolean +dbus_clipboard_request( + DBusDisplay *dpy, + GDBusMethodInvocation *invocation, + gint arg_selection, + const gchar *const *arg_mimes) +{ + QemuClipboardSelection s = arg_selection; + QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT; + QemuClipboardInfo *info = NULL; + + if (!dbus_clipboard_check_caller(dpy, invocation)) { + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Invalid clipboard selection: %d", arg_selection); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (dpy->clipboard_request[s].invocation) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Pending request"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + info = qemu_clipboard_info(s); + if (!info || !info->owner || info->owner == &dpy->clipboard_peer) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Empty clipboard"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (!g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8) || + !info->types[type].available) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Unhandled MIME types requested"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + if (info->types[type].data) { + dbus_clipboard_complete_request(dpy, invocation, info, type); + } else { + qemu_clipboard_request(info, type); + + dpy->clipboard_request[s].invocation = g_object_ref(invocation); + dpy->clipboard_request[s].type = type; + dpy->clipboard_request[s].timeout_id = + g_timeout_add_seconds(5, dbus_clipboard_request_timeout, + &dpy->clipboard_request[s]); + } + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +void +dbus_clipboard_init(DBusDisplay *dpy) +{ + g_autoptr(GDBusObjectSkeleton) clipboard = NULL; + + assert(!dpy->clipboard); + + clipboard = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT "/Clipboard"); + dpy->clipboard = qemu_dbus_display1_clipboard_skeleton_new(); + g_object_connect(dpy->clipboard, + "swapped-signal::handle-register", + dbus_clipboard_register, dpy, + "swapped-signal::handle-unregister", + dbus_clipboard_unregister, dpy, + "swapped-signal::handle-grab", + dbus_clipboard_grab, dpy, + "swapped-signal::handle-release", + dbus_clipboard_release, dpy, + "swapped-signal::handle-request", + dbus_clipboard_request, dpy, + NULL); + + g_dbus_object_skeleton_add_interface( + G_DBUS_OBJECT_SKELETON(clipboard), + G_DBUS_INTERFACE_SKELETON(dpy->clipboard)); + g_dbus_object_manager_server_export(dpy->server, clipboard); + dpy->clipboard_peer.name = "dbus"; + dpy->clipboard_peer.notifier.notify = dbus_clipboard_notify; + dpy->clipboard_peer.request = dbus_clipboard_qemu_request; + qemu_clipboard_peer_register(&dpy->clipboard_peer); +} diff --git a/ui/dbus-console.c b/ui/dbus-console.c new file mode 100644 index 00000000..898a4ac8 --- /dev/null +++ b/ui/dbus-console.c @@ -0,0 +1,496 @@ +/* + * QEMU DBus display console + * + * Copyright (c) 2021 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 "qapi/error.h" +#include "ui/input.h" +#include "ui/kbd-state.h" +#include "trace.h" + +#include <gio/gunixfdlist.h> + +#include "dbus.h" + +struct _DBusDisplayConsole { + GDBusObjectSkeleton parent_instance; + DisplayChangeListener dcl; + + DBusDisplay *display; + GHashTable *listeners; + QemuDBusDisplay1Console *iface; + + QemuDBusDisplay1Keyboard *iface_kbd; + QKbdState *kbd; + + QemuDBusDisplay1Mouse *iface_mouse; + gboolean last_set; + guint last_x; + guint last_y; + Notifier mouse_mode_notifier; +}; + +G_DEFINE_TYPE(DBusDisplayConsole, + dbus_display_console, + G_TYPE_DBUS_OBJECT_SKELETON) + +static void +dbus_display_console_set_size(DBusDisplayConsole *ddc, + uint32_t width, uint32_t height) +{ + g_object_set(ddc->iface, + "width", width, + "height", height, + NULL); +} + +static void +dbus_gfx_switch(DisplayChangeListener *dcl, + struct DisplaySurface *new_surface) +{ + DBusDisplayConsole *ddc = container_of(dcl, DBusDisplayConsole, dcl); + + dbus_display_console_set_size(ddc, + surface_width(new_surface), + surface_height(new_surface)); +} + +static void +dbus_gfx_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ +} + +static void +dbus_gl_scanout_disable(DisplayChangeListener *dcl) +{ +} + +static void +dbus_gl_scanout_texture(DisplayChangeListener *dcl, + uint32_t tex_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) +{ + DBusDisplayConsole *ddc = container_of(dcl, DBusDisplayConsole, dcl); + + dbus_display_console_set_size(ddc, w, h); +} + +static void +dbus_gl_scanout_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf) +{ + DBusDisplayConsole *ddc = container_of(dcl, DBusDisplayConsole, dcl); + + dbus_display_console_set_size(ddc, + dmabuf->width, + dmabuf->height); +} + +static void +dbus_gl_scanout_update(DisplayChangeListener *dcl, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ +} + +const DisplayChangeListenerOps dbus_console_dcl_ops = { + .dpy_name = "dbus-console", + .dpy_gfx_switch = dbus_gfx_switch, + .dpy_gfx_update = dbus_gfx_update, + .dpy_gl_scanout_disable = dbus_gl_scanout_disable, + .dpy_gl_scanout_texture = dbus_gl_scanout_texture, + .dpy_gl_scanout_dmabuf = dbus_gl_scanout_dmabuf, + .dpy_gl_update = dbus_gl_scanout_update, +}; + +static void +dbus_display_console_init(DBusDisplayConsole *object) +{ + DBusDisplayConsole *ddc = DBUS_DISPLAY_CONSOLE(object); + + ddc->listeners = g_hash_table_new_full(g_str_hash, g_str_equal, + NULL, g_object_unref); + ddc->dcl.ops = &dbus_console_dcl_ops; +} + +static void +dbus_display_console_dispose(GObject *object) +{ + DBusDisplayConsole *ddc = DBUS_DISPLAY_CONSOLE(object); + + unregister_displaychangelistener(&ddc->dcl); + g_clear_object(&ddc->iface_kbd); + g_clear_object(&ddc->iface); + g_clear_pointer(&ddc->listeners, g_hash_table_unref); + g_clear_pointer(&ddc->kbd, qkbd_state_free); + + G_OBJECT_CLASS(dbus_display_console_parent_class)->dispose(object); +} + +static void +dbus_display_console_class_init(DBusDisplayConsoleClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); + + gobject_class->dispose = dbus_display_console_dispose; +} + +static void +listener_vanished_cb(DBusDisplayListener *listener) +{ + DBusDisplayConsole *ddc = dbus_display_listener_get_console(listener); + const char *name = dbus_display_listener_get_bus_name(listener); + + trace_dbus_listener_vanished(name); + + g_hash_table_remove(ddc->listeners, name); + qkbd_state_lift_all_keys(ddc->kbd); +} + +static gboolean +dbus_console_set_ui_info(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + guint16 arg_width_mm, + guint16 arg_height_mm, + gint arg_xoff, + gint arg_yoff, + guint arg_width, + guint arg_height) +{ + QemuUIInfo info = { + .width_mm = arg_width_mm, + .height_mm = arg_height_mm, + .xoff = arg_xoff, + .yoff = arg_yoff, + .width = arg_width, + .height = arg_height, + }; + + if (!dpy_ui_info_supported(ddc->dcl.con)) { + g_dbus_method_invocation_return_error(invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_UNSUPPORTED, + "SetUIInfo is not supported"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + dpy_set_ui_info(ddc->dcl.con, &info, false); + qemu_dbus_display1_console_complete_set_uiinfo(ddc->iface, invocation); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_console_register_listener(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + GVariant *arg_listener) +{ + const char *sender = g_dbus_method_invocation_get_sender(invocation); + GDBusConnection *listener_conn; + g_autoptr(GError) err = NULL; + g_autoptr(GSocket) socket = NULL; + g_autoptr(GSocketConnection) socket_conn = NULL; + g_autofree char *guid = g_dbus_generate_guid(); + DBusDisplayListener *listener; + int fd; + + if (sender && g_hash_table_contains(ddc->listeners, sender)) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_INVALID, + "`%s` is already registered!", + sender); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + fd = g_unix_fd_list_get(fd_list, g_variant_get_handle(arg_listener), &err); + if (err) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Couldn't get peer fd: %s", err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + socket = g_socket_new_from_fd(fd, &err); + if (err) { + g_dbus_method_invocation_return_error( + invocation, + DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_FAILED, + "Couldn't make a socket: %s", err->message); + close(fd); + return DBUS_METHOD_INVOCATION_HANDLED; + } + socket_conn = g_socket_connection_factory_create_connection(socket); + + qemu_dbus_display1_console_complete_register_listener( + ddc->iface, invocation, NULL); + + listener_conn = g_dbus_connection_new_sync( + G_IO_STREAM(socket_conn), + guid, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER, + NULL, NULL, &err); + if (err) { + error_report("Failed to setup peer connection: %s", err->message); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + listener = dbus_display_listener_new(sender, listener_conn, ddc); + if (!listener) { + return DBUS_METHOD_INVOCATION_HANDLED; + } + + g_hash_table_insert(ddc->listeners, + (gpointer)dbus_display_listener_get_bus_name(listener), + listener); + g_object_connect(listener_conn, + "swapped-signal::closed", listener_vanished_cb, listener, + NULL); + + trace_dbus_registered_listener(sender); + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_kbd_press(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + guint arg_keycode) +{ + QKeyCode qcode = qemu_input_key_number_to_qcode(arg_keycode); + + trace_dbus_kbd_press(arg_keycode); + + qkbd_state_key_event(ddc->kbd, qcode, true); + + qemu_dbus_display1_keyboard_complete_press(ddc->iface_kbd, invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_kbd_release(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + guint arg_keycode) +{ + QKeyCode qcode = qemu_input_key_number_to_qcode(arg_keycode); + + trace_dbus_kbd_release(arg_keycode); + + qkbd_state_key_event(ddc->kbd, qcode, false); + + qemu_dbus_display1_keyboard_complete_release(ddc->iface_kbd, invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +dbus_kbd_qemu_leds_updated(void *data, int ledstate) +{ + DBusDisplayConsole *ddc = DBUS_DISPLAY_CONSOLE(data); + + qemu_dbus_display1_keyboard_set_modifiers(ddc->iface_kbd, ledstate); +} + +static gboolean +dbus_mouse_rel_motion(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + int dx, int dy) +{ + trace_dbus_mouse_rel_motion(dx, dy); + + if (qemu_input_is_absolute()) { + g_dbus_method_invocation_return_error( + invocation, DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_INVALID, + "Mouse is not relative"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + qemu_input_queue_rel(ddc->dcl.con, INPUT_AXIS_X, dx); + qemu_input_queue_rel(ddc->dcl.con, INPUT_AXIS_Y, dy); + qemu_input_event_sync(); + + qemu_dbus_display1_mouse_complete_rel_motion(ddc->iface_mouse, + invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_mouse_set_pos(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + guint x, guint y) +{ + int width, height; + + trace_dbus_mouse_set_pos(x, y); + + if (!qemu_input_is_absolute()) { + g_dbus_method_invocation_return_error( + invocation, DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_INVALID, + "Mouse is not absolute"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + + width = qemu_console_get_width(ddc->dcl.con, 0); + height = qemu_console_get_height(ddc->dcl.con, 0); + if (x >= width || y >= height) { + g_dbus_method_invocation_return_error( + invocation, DBUS_DISPLAY_ERROR, + DBUS_DISPLAY_ERROR_INVALID, + "Invalid mouse position"); + return DBUS_METHOD_INVOCATION_HANDLED; + } + qemu_input_queue_abs(ddc->dcl.con, INPUT_AXIS_X, x, 0, width); + qemu_input_queue_abs(ddc->dcl.con, INPUT_AXIS_Y, y, 0, height); + qemu_input_event_sync(); + + qemu_dbus_display1_mouse_complete_set_abs_position(ddc->iface_mouse, + invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_mouse_press(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + guint button) +{ + trace_dbus_mouse_press(button); + + qemu_input_queue_btn(ddc->dcl.con, button, true); + qemu_input_event_sync(); + + qemu_dbus_display1_mouse_complete_press(ddc->iface_mouse, invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +dbus_mouse_release(DBusDisplayConsole *ddc, + GDBusMethodInvocation *invocation, + guint button) +{ + trace_dbus_mouse_release(button); + + qemu_input_queue_btn(ddc->dcl.con, button, false); + qemu_input_event_sync(); + + qemu_dbus_display1_mouse_complete_release(ddc->iface_mouse, invocation); + + return DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +dbus_mouse_mode_change(Notifier *notify, void *data) +{ + DBusDisplayConsole *ddc = + container_of(notify, DBusDisplayConsole, mouse_mode_notifier); + + g_object_set(ddc->iface_mouse, + "is-absolute", qemu_input_is_absolute(), + NULL); +} + +int dbus_display_console_get_index(DBusDisplayConsole *ddc) +{ + return qemu_console_get_index(ddc->dcl.con); +} + +DBusDisplayConsole * +dbus_display_console_new(DBusDisplay *display, QemuConsole *con) +{ + g_autofree char *path = NULL; + g_autofree char *label = NULL; + char device_addr[256] = ""; + DBusDisplayConsole *ddc; + int idx; + + assert(display); + assert(con); + + label = qemu_console_get_label(con); + idx = qemu_console_get_index(con); + path = g_strdup_printf(DBUS_DISPLAY1_ROOT "/Console_%d", idx); + ddc = g_object_new(DBUS_DISPLAY_TYPE_CONSOLE, + "g-object-path", path, + NULL); + ddc->display = display; + ddc->dcl.con = con; + /* handle errors, and skip non graphics? */ + qemu_console_fill_device_address( + con, device_addr, sizeof(device_addr), NULL); + + ddc->iface = qemu_dbus_display1_console_skeleton_new(); + g_object_set(ddc->iface, + "label", label, + "type", qemu_console_is_graphic(con) ? "Graphic" : "Text", + "head", qemu_console_get_head(con), + "width", qemu_console_get_width(con, 0), + "height", qemu_console_get_height(con, 0), + "device-address", device_addr, + NULL); + g_object_connect(ddc->iface, + "swapped-signal::handle-register-listener", + dbus_console_register_listener, ddc, + "swapped-signal::handle-set-uiinfo", + dbus_console_set_ui_info, ddc, + NULL); + g_dbus_object_skeleton_add_interface(G_DBUS_OBJECT_SKELETON(ddc), + G_DBUS_INTERFACE_SKELETON(ddc->iface)); + + ddc->kbd = qkbd_state_init(con); + ddc->iface_kbd = qemu_dbus_display1_keyboard_skeleton_new(); + qemu_add_led_event_handler(dbus_kbd_qemu_leds_updated, ddc); + g_object_connect(ddc->iface_kbd, + "swapped-signal::handle-press", dbus_kbd_press, ddc, + "swapped-signal::handle-release", dbus_kbd_release, ddc, + NULL); + g_dbus_object_skeleton_add_interface(G_DBUS_OBJECT_SKELETON(ddc), + G_DBUS_INTERFACE_SKELETON(ddc->iface_kbd)); + + ddc->iface_mouse = qemu_dbus_display1_mouse_skeleton_new(); + g_object_connect(ddc->iface_mouse, + "swapped-signal::handle-set-abs-position", dbus_mouse_set_pos, ddc, + "swapped-signal::handle-rel-motion", dbus_mouse_rel_motion, ddc, + "swapped-signal::handle-press", dbus_mouse_press, ddc, + "swapped-signal::handle-release", dbus_mouse_release, ddc, + NULL); + g_dbus_object_skeleton_add_interface(G_DBUS_OBJECT_SKELETON(ddc), + G_DBUS_INTERFACE_SKELETON(ddc->iface_mouse)); + + register_displaychangelistener(&ddc->dcl); + ddc->mouse_mode_notifier.notify = dbus_mouse_mode_change; + qemu_add_mouse_mode_change_notifier(&ddc->mouse_mode_notifier); + + return ddc; +} diff --git a/ui/dbus-display1.xml b/ui/dbus-display1.xml new file mode 100644 index 00000000..c3b22933 --- /dev/null +++ b/ui/dbus-display1.xml @@ -0,0 +1,761 @@ +<?xml version="1.0" encoding="utf-8"?> +<node> + <!-- + org.qemu.Display1.VM: + + This interface is implemented on ``/org/qemu/Display1/VM``. + --> + <interface name="org.qemu.Display1.VM"> + <!-- + Name: + + The name of the VM. + --> + <property name="Name" type="s" access="read"/> + + <!-- + UUID: + + The UUID of the VM. + --> + <property name="UUID" type="s" access="read"/> + + <!-- + ConsoleIDs: + + The list of consoles available on ``/org/qemu/Display1/Console_$id``. + --> + <property name="ConsoleIDs" type="au" access="read"/> + </interface> + + <!-- + org.qemu.Display1.Console: + + This interface is implemented on ``/org/qemu/Display1/Console_$id``. You + may discover available consoles through introspection or with the + :dbus:prop:`org.qemu.Display1.VM.ConsoleIDs` property. + + A console is attached to a video device head. It may be "Graphic" or + "Text" (see :dbus:prop:`Type` and other properties). + + Interactions with a console may be done with + :dbus:iface:`org.qemu.Display1.Keyboard` and + :dbus:iface:`org.qemu.Display1.Mouse` interfaces when available. + --> + <interface name="org.qemu.Display1.Console"> + <!-- + RegisterListener: + @listener: a Unix socket FD, for peer-to-peer D-Bus communication. + + Register a console listener, which will receive display updates, until + it is disconnected. + + Multiple listeners may be registered simultaneously. + + The listener is expected to implement the + :dbus:iface:`org.qemu.Display1.Listener` interface. + --> + <method name="RegisterListener"> + <arg type="h" name="listener" direction="in"/> + </method> + + <!-- + SetUIInfo: + @width_mm: the physical display width in millimeters. + @height_mm: the physical display height in millimeters. + @xoff: horizontal offset, in pixels. + @yoff: vertical offset, in pixels. + @width: console width, in pixels. + @height: console height, in pixels. + + Modify the dimensions and display settings. + --> + <method name="SetUIInfo"> + <arg name="width_mm" type="q" direction="in"/> + <arg name="height_mm" type="q" direction="in"/> + <arg name="xoff" type="i" direction="in"/> + <arg name="yoff" type="i" direction="in"/> + <arg name="width" type="u" direction="in"/> + <arg name="height" type="u" direction="in"/> + </method> + + <!-- + Label: + + A user-friendly name for the console (for ex: "VGA"). + --> + <property name="Label" type="s" access="read"/> + + <!-- + Head: + + Graphical device head number. + --> + <property name="Head" type="u" access="read"/> + + <!-- + Type: + + Console type ("Graphic" or "Text"). + --> + <property name="Type" type="s" access="read"/> + + <!-- + Width: + + Console width, in pixels. + --> + <property name="Width" type="u" access="read"/> + + <!-- + Height: + + Console height, in pixels. + --> + <property name="Height" type="u" access="read"/> + + <!-- + DeviceAddress: + + The device address (ex: "pci/0000/02.0"). + --> + <property name="DeviceAddress" type="s" access="read"/> + </interface> + + <!-- + org.qemu.Display1.Keyboard: + + This interface in implemented on ``/org/qemu/Display1/Console_$id`` (see + :dbus:iface:`~org.qemu.Display1.Console`). + --> + <interface name="org.qemu.Display1.Keyboard"> + <!-- + Press: + @keycode: QEMU key number (xtkbd + special re-encoding of high bit) + + Send a key press event. + --> + <method name="Press"> + <arg type="u" name="keycode" direction="in"/> + </method> + + <!-- + Release: + @keycode: QEMU key number (xtkbd + special re-encoding of high bit) + + Send a key release event. + --> + <method name="Release"> + <arg type="u" name="keycode" direction="in"/> + </method> + + <!-- + Modifiers: + + The active keyboard modifiers:: + + Scroll = 1 << 0 + Num = 1 << 1 + Caps = 1 << 2 + --> + <property name="Modifiers" type="u" access="read"/> + </interface> + + <!-- + org.qemu.Display1.Mouse: + + This interface in implemented on ``/org/qemu/Display1/Console_$id`` (see + :dbus:iface:`~org.qemu.Display1.Console` documentation). + + .. _dbus-button-values: + + **Button values**:: + + Left = 0 + Middle = 1 + Right = 2 + Wheel-up = 3 + Wheel-down = 4 + Side = 5 + Extra = 6 + --> + <interface name="org.qemu.Display1.Mouse"> + <!-- + Press: + @button: :ref:`button value<dbus-button-values>`. + + Send a mouse button press event. + --> + <method name="Press"> + <arg type="u" name="button" direction="in"/> + </method> + + <!-- + Release: + @button: :ref:`button value<dbus-button-values>`. + + Send a mouse button release event. + --> + <method name="Release"> + <arg type="u" name="button" direction="in"/> + </method> + + <!-- + SetAbsPosition: + @x: X position, in pixels. + @y: Y position, in pixels. + + Set the mouse pointer position. + + Returns an error if not :dbus:prop:`IsAbsolute`. + --> + <method name="SetAbsPosition"> + <arg type="u" name="x" direction="in"/> + <arg type="u" name="y" direction="in"/> + </method> + + <!-- + RelMotion: + @dx: X-delta, in pixels. + @dy: Y-delta, in pixels. + + Move the mouse pointer position, relative to the current position. + + Returns an error if :dbus:prop:`IsAbsolute`. + --> + <method name="RelMotion"> + <arg type="i" name="dx" direction="in"/> + <arg type="i" name="dy" direction="in"/> + </method> + + <!-- + IsAbsolute: + + Whether the mouse is using absolute movements. + --> + <property name="IsAbsolute" type="b" access="read"/> + </interface> + + <!-- + org.qemu.Display1.Listener: + + This client-side interface must be available on + ``/org/qemu/Display1/Listener`` when registering the peer-to-peer + connection with :dbus:meth:`~org.qemu.Display1.Console.Register`. + --> + <interface name="org.qemu.Display1.Listener"> + <!-- + Scanout: + @width: display width, in pixels. + @height: display height, in pixels. + @stride: data stride, in bytes. + @pixman_format: image format (ex: ``PIXMAN_X8R8G8B8``). + @data: image data. + + Resize and update the display content. + + The data to transfer for the display update may be large. The preferred + scanout method is :dbus:meth:`ScanoutDMABUF`, used whenever possible. + --> + <method name="Scanout"> + <arg type="u" name="width" direction="in"/> + <arg type="u" name="height" direction="in"/> + <arg type="u" name="stride" direction="in"/> + <arg type="u" name="pixman_format" direction="in"/> + <arg type="ay" name="data" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + + <!-- + Update: + @x: X update position, in pixels. + @y: Y update position, in pixels. + @width: update width, in pixels. + @height: update height, in pixels. + @stride: data stride, in bytes. + @pixman_format: image format (ex: ``PIXMAN_X8R8G8B8``). + @data: display image data. + + Update the display content. + + This method is only called after a :dbus:meth:`Scanout` call. + --> + <method name="Update"> + <arg type="i" name="x" direction="in"/> + <arg type="i" name="y" direction="in"/> + <arg type="i" name="width" direction="in"/> + <arg type="i" name="height" direction="in"/> + <arg type="u" name="stride" direction="in"/> + <arg type="u" name="pixman_format" direction="in"/> + <arg type="ay" name="data" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + + <!-- + ScanoutDMABUF: + @dmabuf: the DMABUF file descriptor. + @width: display width, in pixels. + @height: display height, in pixels. + @stride: stride, in bytes. + @fourcc: DMABUF fourcc. + @modifier: DMABUF modifier. + @y0_top: whether Y position 0 is the top or not. + + Resize and update the display content with a DMABUF. + --> + <method name="ScanoutDMABUF"> + <arg type="h" name="dmabuf" direction="in"/> + <arg type="u" name="width" direction="in"/> + <arg type="u" name="height" direction="in"/> + <arg type="u" name="stride" direction="in"/> + <arg type="u" name="fourcc" direction="in"/> + <!-- xywh? --> + <arg type="t" name="modifier" direction="in"/> + <arg type="b" name="y0_top" direction="in"/> + </method> + + <!-- + UpdateDMABUF: + @x: the X update position, in pixels. + @y: the Y update position, in pixels. + @width: the update width, in pixels. + @height: the update height, in pixels. + + Update the display content with the current DMABUF and the given region. + --> + <method name="UpdateDMABUF"> + <arg type="i" name="x" direction="in"/> + <arg type="i" name="y" direction="in"/> + <arg type="i" name="width" direction="in"/> + <arg type="i" name="height" direction="in"/> + </method> + + <!-- + Disable: + + Disable the display (turn it off). + --> + <method name="Disable"> + </method> + + <!-- + MouseSet: + @x: X mouse position, in pixels. + @y: Y mouse position, in pixels. + @on: whether the mouse is visible or not. + + Set the mouse position and visibility. + --> + <method name="MouseSet"> + <arg type="i" name="x" direction="in"/> + <arg type="i" name="y" direction="in"/> + <arg type="i" name="on" direction="in"/> + </method> + + <!-- + CursorDefine: + @width: cursor width, in pixels. + @height: cursor height, in pixels. + @hot_x: hot-spot X position, in pixels. + @hot_y: hot-spot Y position, in pixels. + @data: the cursor data. + + Set the mouse cursor shape and hot-spot. The "data" must be ARGB, 32-bit + per pixel. + --> + <method name="CursorDefine"> + <arg type="i" name="width" direction="in"/> + <arg type="i" name="height" direction="in"/> + <arg type="i" name="hot_x" direction="in"/> + <arg type="i" name="hot_y" direction="in"/> + <arg type="ay" name="data" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + </interface> + + <!-- + org.qemu.Display1.Clipboard: + + This interface must be implemented by both the client and the server on + ``/org/qemu/Display1/Clipboard`` to support clipboard sharing between + the client and the guest. + + Once :dbus:meth:`Register`'ed, method calls may be sent and received in both + directions. Unregistered callers will get error replies. + + .. _dbus-clipboard-selection: + + **Selection values**:: + + Clipboard = 0 + Primary = 1 + Secondary = 2 + + .. _dbus-clipboard-serial: + + **Serial counter** + + To solve potential clipboard races, clipboard grabs have an associated + serial counter. It is set to 0 on registration, and incremented by 1 for + each grab. The peer with the highest serial is the clipboard grab owner. + + When a grab with a lower serial is received, it should be discarded. + + When a grab is attempted with the same serial number as the current grab, + the one coming from the client should have higher priority, and the client + should gain clipboard grab ownership. + --> + <interface name="org.qemu.Display1.Clipboard"> + <!-- + Register: + + Register a clipboard session and reinitialize the serial counter. + + The client must register itself, and is granted an exclusive + access for handling the clipboard. + + The server can reinitialize the session as well (to reset the counter). + --> + <method name="Register"/> + + <!-- + Unregister: + + Unregister the clipboard session. + --> + <method name="Unregister"/> + <!-- + Grab: + @selection: a :ref:`selection value<dbus-clipboard-selection>`. + @serial: the current grab :ref:`serial<dbus-clipboard-serial>`. + @mimes: the list of available content MIME types. + + Grab the clipboard, claiming current clipboard content. + --> + <method name="Grab"> + <arg type="u" name="selection"/> + <arg type="u" name="serial"/> + <arg type="as" name="mimes"/> + </method> + + <!-- + Release: + @selection: a :ref:`selection value<dbus-clipboard-selection>`. + + Release the clipboard (does nothing if not the current owner). + --> + <method name="Release"> + <arg type="u" name="selection"/> + </method> + + <!-- + Request: + @selection: a :ref:`selection value<dbus-clipboard-selection>` + @mimes: requested MIME types (by order of preference). + @reply_mime: the returned data MIME type. + @data: the clipboard data. + + Request the clipboard content. + + Return an error if the clipboard is empty, or the requested MIME types + are unavailable. + --> + <method name="Request"> + <arg type="u" name="selection"/> + <arg type="as" name="mimes"/> + <arg type="s" name="reply_mime" direction="out"/> + <arg type="ay" name="data" direction="out"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + </interface> + + <!-- + org.qemu.Display1.Audio: + + Audio backend may be available on ``/org/qemu/Display1/Audio``. + --> + <interface name="org.qemu.Display1.Audio"> + <!-- + RegisterOutListener: + @listener: a Unix socket FD, for peer-to-peer D-Bus communication. + + Register an audio backend playback handler. + + Multiple listeners may be registered simultaneously. + + The listener is expected to implement the + :dbus:iface:`org.qemu.Display1.AudioOutListener` interface. + --> + <method name="RegisterOutListener"> + <arg type="h" name="listener" direction="in"/> + </method> + + <!-- + RegisterInListener: + @listener: a Unix socket FD, for peer-to-peer D-Bus communication. + + Register an audio backend record handler. + + Multiple listeners may be registered simultaneously. + + The listener is expected to implement the + :dbus:iface:`org.qemu.Display1.AudioInListener` interface. + --> + <method name="RegisterInListener"> + <arg type="h" name="listener" direction="in"/> + </method> + </interface> + + <!-- + org.qemu.Display1.AudioOutListener: + + This client-side interface must be available on + ``/org/qemu/Display1/AudioOutListener`` when registering the peer-to-peer + connection with :dbus:meth:`~org.qemu.Display1.Audio.RegisterOutListener`. + --> + <interface name="org.qemu.Display1.AudioOutListener"> + <!-- + Init: + @id: the stream ID. + @bits: PCM bits per sample. + @is_signed: whether the PCM data is signed. + @is_float: PCM floating point format. + @freq: the PCM frequency in Hz. + @nchannels: the number of channels. + @bytes_per_frame: the bytes per frame. + @bytes_per_second: the bytes per second. + @be: whether using big-endian format. + + Initializes a PCM playback stream. + --> + <method name="Init"> + <arg name="id" type="t" direction="in"/> + <arg name="bits" type="y" direction="in"/> + <arg name="is_signed" type="b" direction="in"/> + <arg name="is_float" type="b" direction="in"/> + <arg name="freq" type="u" direction="in"/> + <arg name="nchannels" type="y" direction="in"/> + <arg name="bytes_per_frame" type="u" direction="in"/> + <arg name="bytes_per_second" type="u" direction="in"/> + <arg name="be" type="b" direction="in"/> + </method> + + <!-- + Fini: + @id: the stream ID. + + Finish & close a playback stream. + --> + <method name="Fini"> + <arg name="id" type="t" direction="in"/> + </method> + + <!-- + SetEnabled: + @id: the stream ID. + + Resume or suspend the playback stream. + --> + <method name="SetEnabled"> + <arg name="id" type="t" direction="in"/> + <arg name="enabled" type="b" direction="in"/> + </method> + + <!-- + SetVolume: + @id: the stream ID. + @mute: whether the stream is muted. + @volume: the volume per-channel. + + Set the stream volume and mute state (volume without unit, 0-255). + --> + <method name="SetVolume"> + <arg name="id" type="t" direction="in"/> + <arg name="mute" type="b" direction="in"/> + <arg name="volume" type="ay" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + + <!-- + Write: + @id: the stream ID. + @data: the PCM data. + + PCM stream to play. + --> + <method name="Write"> + <arg name="id" type="t" direction="in"/> + <arg type="ay" name="data" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + </interface> + + <!-- + org.qemu.Display1.AudioInListener: + + This client-side interface must be available on + ``/org/qemu/Display1/AudioInListener`` when registering the peer-to-peer + connection with :dbus:meth:`~org.qemu.Display1.Audio.RegisterInListener`. + --> + <interface name="org.qemu.Display1.AudioInListener"> + <!-- + Init: + @id: the stream ID. + @bits: PCM bits per sample. + @is_signed: whether the PCM data is signed. + @is_float: PCM floating point format. + @freq: the PCM frequency in Hz. + @nchannels: the number of channels. + @bytes_per_frame: the bytes per frame. + @bytes_per_second: the bytes per second. + @be: whether using big-endian format. + + Initializes a PCM record stream. + --> + <method name="Init"> + <arg name="id" type="t" direction="in"/> + <arg name="bits" type="y" direction="in"/> + <arg name="is_signed" type="b" direction="in"/> + <arg name="is_float" type="b" direction="in"/> + <arg name="freq" type="u" direction="in"/> + <arg name="nchannels" type="y" direction="in"/> + <arg name="bytes_per_frame" type="u" direction="in"/> + <arg name="bytes_per_second" type="u" direction="in"/> + <arg name="be" type="b" direction="in"/> + </method> + + <!-- + Fini: + @id: the stream ID. + + Finish & close a record stream. + --> + <method name="Fini"> + <arg name="id" type="t" direction="in"/> + </method> + + <!-- + SetEnabled: + @id: the stream ID. + + Resume or suspend the record stream. + --> + <method name="SetEnabled"> + <arg name="id" type="t" direction="in"/> + <arg name="enabled" type="b" direction="in"/> + </method> + + <!-- + SetVolume: + @id: the stream ID. + @mute: whether the stream is muted. + @volume: the volume per-channel. + + Set the stream volume and mute state (volume without unit, 0-255). + --> + <method name="SetVolume"> + <arg name="id" type="t" direction="in"/> + <arg name="mute" type="b" direction="in"/> + <arg name="volume" type="ay" direction="in"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + + <!-- + Read: + @id: the stream ID. + @size: the amount to read, in bytes. + @data: the recorded data (which may be less than requested). + + Read "size" bytes from the record stream. + --> + <method name="Read"> + <arg name="id" type="t" direction="in"/> + <arg name="size" type="t" direction="in"/> + <arg type="ay" name="data" direction="out"> + <annotation name="org.gtk.GDBus.C.ForceGVariant" value="true"/> + </arg> + </method> + </interface> + + <!-- + org.qemu.Display1.Chardev: + + Character devices may be available on ``/org/qemu/Display1/Chardev_$id``. + + They may be used for different kind of streams, which are identified via + their FQDN :dbus:prop:`Name`. + + .. _dbus-chardev-fqdn: + + Here are some known reserved kind names (the ``org.qemu`` prefix is + reserved by QEMU): + + org.qemu.console.serial.0 + A serial console stream. + + org.qemu.monitor.hmp.0 + A QEMU HMP human monitor. + + org.qemu.monitor.qmp.0 + A QEMU QMP monitor. + + org.qemu.usbredir + A usbredir stream. + --> + <interface name="org.qemu.Display1.Chardev"> + <!-- + Register: + @stream: a Unix FD to redirect the stream to. + + Register a file-descriptor for the stream handling. + + The current handler, if any, will be replaced. + --> + <method name="Register"> + <arg type="h" name="stream" direction="in"/> + </method> + + <!-- + SendBreak: + + Send a break event to the character device. + --> + <method name="SendBreak"/> + + <!-- + Name: + + The FQDN name to identify the kind of stream. See :ref:`reserved + names<dbus-chardev-fqdn>`. + --> + <property name="Name" type="s" access="read"/> + + <!-- + FEOpened: + + Whether the front-end side is opened. + --> + <property name="FEOpened" type="b" access="read"/> + + <!-- + Echo: + + Whether the input should be echo'ed (for serial streams). + --> + <property name="Echo" type="b" access="read"/> + + <!-- + Owner: + + The D-Bus unique name of the registered handler. + --> + <property name="Owner" type="s" access="read"/> + </interface> +</node> diff --git a/ui/dbus-error.c b/ui/dbus-error.c new file mode 100644 index 00000000..85a9194d --- /dev/null +++ b/ui/dbus-error.c @@ -0,0 +1,48 @@ +/* + * QEMU DBus display errors + * + * Copyright (c) 2021 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 "dbus.h" + +static const GDBusErrorEntry dbus_display_error_entries[] = { + { DBUS_DISPLAY_ERROR_FAILED, "org.qemu.Display1.Error.Failed" }, + { DBUS_DISPLAY_ERROR_INVALID, "org.qemu.Display1.Error.Invalid" }, + { DBUS_DISPLAY_ERROR_UNSUPPORTED, "org.qemu.Display1.Error.Unsupported" }, +}; + +G_STATIC_ASSERT(G_N_ELEMENTS(dbus_display_error_entries) == + DBUS_DISPLAY_N_ERRORS); + +GQuark +dbus_display_error_quark(void) +{ + static gsize quark; + + g_dbus_error_register_error_domain( + "dbus-display-error-quark", + &quark, + dbus_display_error_entries, + G_N_ELEMENTS(dbus_display_error_entries)); + + return (GQuark)quark; +} diff --git a/ui/dbus-listener.c b/ui/dbus-listener.c new file mode 100644 index 00000000..f9fc8eda --- /dev/null +++ b/ui/dbus-listener.c @@ -0,0 +1,478 @@ +/* + * QEMU DBus display console + * + * Copyright (c) 2021 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 "sysemu/sysemu.h" +#include "dbus.h" +#include <gio/gunixfdlist.h> + +#include "ui/shader.h" +#include "ui/egl-helpers.h" +#include "ui/egl-context.h" +#include "trace.h" + +struct _DBusDisplayListener { + GObject parent; + + char *bus_name; + DBusDisplayConsole *console; + GDBusConnection *conn; + + QemuDBusDisplay1Listener *proxy; + + DisplayChangeListener dcl; + DisplaySurface *ds; + int gl_updates; +}; + +G_DEFINE_TYPE(DBusDisplayListener, dbus_display_listener, G_TYPE_OBJECT) + +static void dbus_update_gl_cb(GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GError) err = NULL; + DBusDisplayListener *ddl = user_data; + + if (!qemu_dbus_display1_listener_call_update_dmabuf_finish(ddl->proxy, + res, &err)) { + error_report("Failed to call update: %s", err->message); + } + + graphic_hw_gl_block(ddl->dcl.con, false); + g_object_unref(ddl); +} + +static void dbus_call_update_gl(DBusDisplayListener *ddl, + int x, int y, int w, int h) +{ + graphic_hw_gl_block(ddl->dcl.con, true); + glFlush(); + qemu_dbus_display1_listener_call_update_dmabuf(ddl->proxy, + x, y, w, h, + G_DBUS_CALL_FLAGS_NONE, + DBUS_DEFAULT_TIMEOUT, NULL, + dbus_update_gl_cb, + g_object_ref(ddl)); +} + +static void dbus_scanout_disable(DisplayChangeListener *dcl) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + ddl->ds = NULL; + qemu_dbus_display1_listener_call_disable( + ddl->proxy, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static void dbus_scanout_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + g_autoptr(GError) err = NULL; + g_autoptr(GUnixFDList) fd_list = NULL; + + fd_list = g_unix_fd_list_new(); + if (g_unix_fd_list_append(fd_list, dmabuf->fd, &err) != 0) { + error_report("Failed to setup dmabuf fdlist: %s", err->message); + return; + } + + qemu_dbus_display1_listener_call_scanout_dmabuf( + ddl->proxy, + g_variant_new_handle(0), + dmabuf->width, + dmabuf->height, + dmabuf->stride, + dmabuf->fourcc, + dmabuf->modifier, + dmabuf->y0_top, + G_DBUS_CALL_FLAGS_NONE, + -1, + fd_list, + NULL, NULL, NULL); +} + +static void dbus_scanout_texture(DisplayChangeListener *dcl, + uint32_t tex_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) +{ + QemuDmaBuf dmabuf = { + .width = backing_width, + .height = backing_height, + .y0_top = backing_y_0_top, + }; + + assert(tex_id); + dmabuf.fd = egl_get_fd_for_texture( + tex_id, (EGLint *)&dmabuf.stride, + (EGLint *)&dmabuf.fourcc, + &dmabuf.modifier); + if (dmabuf.fd < 0) { + error_report("%s: failed to get fd for texture", __func__); + return; + } + + dbus_scanout_dmabuf(dcl, &dmabuf); + close(dmabuf.fd); +} + +static void dbus_cursor_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf, bool have_hot, + uint32_t hot_x, uint32_t hot_y) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + DisplaySurface *ds; + GVariant *v_data = NULL; + egl_fb cursor_fb; + + if (!dmabuf) { + qemu_dbus_display1_listener_call_mouse_set( + ddl->proxy, 0, 0, false, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + return; + } + + egl_dmabuf_import_texture(dmabuf); + if (!dmabuf->texture) { + return; + } + egl_fb_setup_for_tex(&cursor_fb, dmabuf->width, dmabuf->height, + dmabuf->texture, false); + ds = qemu_create_displaysurface(dmabuf->width, dmabuf->height); + egl_fb_read(ds, &cursor_fb); + + v_data = g_variant_new_from_data( + G_VARIANT_TYPE("ay"), + surface_data(ds), + surface_width(ds) * surface_height(ds) * 4, + TRUE, + (GDestroyNotify)qemu_free_displaysurface, + ds); + qemu_dbus_display1_listener_call_cursor_define( + ddl->proxy, + surface_width(ds), + surface_height(ds), + hot_x, + hot_y, + v_data, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL, + NULL); +} + +static void dbus_cursor_position(DisplayChangeListener *dcl, + uint32_t pos_x, uint32_t pos_y) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + qemu_dbus_display1_listener_call_mouse_set( + ddl->proxy, pos_x, pos_y, true, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static void dbus_release_dmabuf(DisplayChangeListener *dcl, + QemuDmaBuf *dmabuf) +{ + dbus_scanout_disable(dcl); +} + +static void dbus_scanout_update(DisplayChangeListener *dcl, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + dbus_call_update_gl(ddl, x, y, w, h); +} + +static void dbus_gl_refresh(DisplayChangeListener *dcl) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + graphic_hw_update(dcl->con); + + if (!ddl->ds || qemu_console_is_gl_blocked(ddl->dcl.con)) { + return; + } + + if (ddl->gl_updates) { + dbus_call_update_gl(ddl, 0, 0, + surface_width(ddl->ds), surface_height(ddl->ds)); + ddl->gl_updates = 0; + } +} + +static void dbus_refresh(DisplayChangeListener *dcl) +{ + graphic_hw_update(dcl->con); +} + +static void dbus_gl_gfx_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + ddl->gl_updates++; +} + +static void dbus_gfx_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + pixman_image_t *img; + GVariant *v_data; + size_t stride; + + assert(ddl->ds); + stride = w * DIV_ROUND_UP(PIXMAN_FORMAT_BPP(surface_format(ddl->ds)), 8); + + trace_dbus_update(x, y, w, h); + + if (x == 0 && y == 0 && w == surface_width(ddl->ds) && h == surface_height(ddl->ds)) { + v_data = g_variant_new_from_data( + G_VARIANT_TYPE("ay"), + surface_data(ddl->ds), + surface_stride(ddl->ds) * surface_height(ddl->ds), + TRUE, + (GDestroyNotify)pixman_image_unref, + pixman_image_ref(ddl->ds->image)); + qemu_dbus_display1_listener_call_scanout( + ddl->proxy, + surface_width(ddl->ds), + surface_height(ddl->ds), + surface_stride(ddl->ds), + surface_format(ddl->ds), + v_data, + G_DBUS_CALL_FLAGS_NONE, + DBUS_DEFAULT_TIMEOUT, NULL, NULL, NULL); + return; + } + + /* make a copy, since gvariant only handles linear data */ + img = pixman_image_create_bits(surface_format(ddl->ds), + w, h, NULL, stride); + pixman_image_composite(PIXMAN_OP_SRC, ddl->ds->image, NULL, img, + x, y, 0, 0, 0, 0, w, h); + + v_data = g_variant_new_from_data( + G_VARIANT_TYPE("ay"), + pixman_image_get_data(img), + pixman_image_get_stride(img) * h, + TRUE, + (GDestroyNotify)pixman_image_unref, + img); + qemu_dbus_display1_listener_call_update(ddl->proxy, + x, y, w, h, pixman_image_get_stride(img), pixman_image_get_format(img), + v_data, + G_DBUS_CALL_FLAGS_NONE, + DBUS_DEFAULT_TIMEOUT, NULL, NULL, NULL); +} + +static void dbus_gl_gfx_switch(DisplayChangeListener *dcl, + struct DisplaySurface *new_surface) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + ddl->ds = new_surface; + if (ddl->ds) { + int width = surface_width(ddl->ds); + int height = surface_height(ddl->ds); + + /* TODO: lazy send dmabuf (there are unnecessary sent otherwise) */ + dbus_scanout_texture(&ddl->dcl, ddl->ds->texture, false, + width, height, 0, 0, width, height); + } +} + +static void dbus_gfx_switch(DisplayChangeListener *dcl, + struct DisplaySurface *new_surface) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + ddl->ds = new_surface; + if (!ddl->ds) { + /* why not call disable instead? */ + return; + } +} + +static void dbus_mouse_set(DisplayChangeListener *dcl, + int x, int y, int on) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + + qemu_dbus_display1_listener_call_mouse_set( + ddl->proxy, x, y, on, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static void dbus_cursor_define(DisplayChangeListener *dcl, + QEMUCursor *c) +{ + DBusDisplayListener *ddl = container_of(dcl, DBusDisplayListener, dcl); + GVariant *v_data = NULL; + + cursor_get(c); + v_data = g_variant_new_from_data( + G_VARIANT_TYPE("ay"), + c->data, + c->width * c->height * 4, + TRUE, + (GDestroyNotify)cursor_put, + c); + + qemu_dbus_display1_listener_call_cursor_define( + ddl->proxy, + c->width, + c->height, + c->hot_x, + c->hot_y, + v_data, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL, + NULL); +} + +const DisplayChangeListenerOps dbus_gl_dcl_ops = { + .dpy_name = "dbus-gl", + .dpy_gfx_update = dbus_gl_gfx_update, + .dpy_gfx_switch = dbus_gl_gfx_switch, + .dpy_gfx_check_format = console_gl_check_format, + .dpy_refresh = dbus_gl_refresh, + .dpy_mouse_set = dbus_mouse_set, + .dpy_cursor_define = dbus_cursor_define, + + .dpy_gl_scanout_disable = dbus_scanout_disable, + .dpy_gl_scanout_texture = dbus_scanout_texture, + .dpy_gl_scanout_dmabuf = dbus_scanout_dmabuf, + .dpy_gl_cursor_dmabuf = dbus_cursor_dmabuf, + .dpy_gl_cursor_position = dbus_cursor_position, + .dpy_gl_release_dmabuf = dbus_release_dmabuf, + .dpy_gl_update = dbus_scanout_update, +}; + +const DisplayChangeListenerOps dbus_dcl_ops = { + .dpy_name = "dbus", + .dpy_gfx_update = dbus_gfx_update, + .dpy_gfx_switch = dbus_gfx_switch, + .dpy_refresh = dbus_refresh, + .dpy_mouse_set = dbus_mouse_set, + .dpy_cursor_define = dbus_cursor_define, +}; + +static void +dbus_display_listener_dispose(GObject *object) +{ + DBusDisplayListener *ddl = DBUS_DISPLAY_LISTENER(object); + + unregister_displaychangelistener(&ddl->dcl); + g_clear_object(&ddl->conn); + g_clear_pointer(&ddl->bus_name, g_free); + g_clear_object(&ddl->proxy); + + G_OBJECT_CLASS(dbus_display_listener_parent_class)->dispose(object); +} + +static void +dbus_display_listener_constructed(GObject *object) +{ + DBusDisplayListener *ddl = DBUS_DISPLAY_LISTENER(object); + + if (display_opengl) { + ddl->dcl.ops = &dbus_gl_dcl_ops; + } else { + ddl->dcl.ops = &dbus_dcl_ops; + } + + G_OBJECT_CLASS(dbus_display_listener_parent_class)->constructed(object); +} + +static void +dbus_display_listener_class_init(DBusDisplayListenerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + object_class->dispose = dbus_display_listener_dispose; + object_class->constructed = dbus_display_listener_constructed; +} + +static void +dbus_display_listener_init(DBusDisplayListener *ddl) +{ +} + +const char * +dbus_display_listener_get_bus_name(DBusDisplayListener *ddl) +{ + return ddl->bus_name ?: "p2p"; +} + +DBusDisplayConsole * +dbus_display_listener_get_console(DBusDisplayListener *ddl) +{ + return ddl->console; +} + +DBusDisplayListener * +dbus_display_listener_new(const char *bus_name, + GDBusConnection *conn, + DBusDisplayConsole *console) +{ + DBusDisplayListener *ddl; + QemuConsole *con; + g_autoptr(GError) err = NULL; + + ddl = g_object_new(DBUS_DISPLAY_TYPE_LISTENER, NULL); + ddl->proxy = + qemu_dbus_display1_listener_proxy_new_sync(conn, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + NULL, + "/org/qemu/Display1/Listener", + NULL, + &err); + if (!ddl->proxy) { + error_report("Failed to setup proxy: %s", err->message); + g_object_unref(conn); + g_object_unref(ddl); + return NULL; + } + + ddl->bus_name = g_strdup(bus_name); + ddl->conn = conn; + ddl->console = console; + + con = qemu_console_lookup_by_index(dbus_display_console_get_index(console)); + assert(con); + ddl->dcl.con = con; + register_displaychangelistener(&ddl->dcl); + + return ddl; +} diff --git a/ui/dbus-module.c b/ui/dbus-module.c new file mode 100644 index 00000000..c8771fe4 --- /dev/null +++ b/ui/dbus-module.c @@ -0,0 +1,35 @@ +/* + * D-Bus module support. + * + * Copyright (C) 2021 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 "qapi/error.h" +#include "ui/dbus-module.h" + +int using_dbus_display; + +static bool +qemu_dbus_display_add_client(int csock, Error **errp) +{ + error_setg(errp, "D-Bus display isn't enabled"); + return false; +} + +struct QemuDBusDisplayOps qemu_dbus_display = { + .add_client = qemu_dbus_display_add_client, +}; diff --git a/ui/dbus.c b/ui/dbus.c new file mode 100644 index 00000000..32d88dc9 --- /dev/null +++ b/ui/dbus.c @@ -0,0 +1,518 @@ +/* + * QEMU DBus display + * + * Copyright (c) 2021 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 "qemu/cutils.h" +#include "qemu/dbus.h" +#include "qemu/main-loop.h" +#include "qemu/option.h" +#include "qom/object_interfaces.h" +#include "sysemu/sysemu.h" +#include "ui/dbus-module.h" +#include "ui/egl-helpers.h" +#include "ui/egl-context.h" +#include "audio/audio.h" +#include "audio/audio_int.h" +#include "qapi/error.h" +#include "trace.h" + +#include "dbus.h" + +static DBusDisplay *dbus_display; + +static QEMUGLContext dbus_create_context(DisplayGLCtx *dgc, + QEMUGLParams *params) +{ + eglMakeCurrent(qemu_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + qemu_egl_rn_ctx); + return qemu_egl_create_context(dgc, params); +} + +static bool +dbus_is_compatible_dcl(DisplayGLCtx *dgc, + DisplayChangeListener *dcl) +{ + return dcl->ops == &dbus_gl_dcl_ops || dcl->ops == &dbus_console_dcl_ops; +} + +static void +dbus_create_texture(DisplayGLCtx *ctx, DisplaySurface *surface) +{ + surface_gl_create_texture(ctx->gls, surface); +} + +static void +dbus_destroy_texture(DisplayGLCtx *ctx, DisplaySurface *surface) +{ + surface_gl_destroy_texture(ctx->gls, surface); +} + +static void +dbus_update_texture(DisplayGLCtx *ctx, DisplaySurface *surface, + int x, int y, int w, int h) +{ + surface_gl_update_texture(ctx->gls, surface, x, y, w, h); +} + +static const DisplayGLCtxOps dbus_gl_ops = { + .dpy_gl_ctx_is_compatible_dcl = dbus_is_compatible_dcl, + .dpy_gl_ctx_create = dbus_create_context, + .dpy_gl_ctx_destroy = qemu_egl_destroy_context, + .dpy_gl_ctx_make_current = qemu_egl_make_context_current, + .dpy_gl_ctx_create_texture = dbus_create_texture, + .dpy_gl_ctx_destroy_texture = dbus_destroy_texture, + .dpy_gl_ctx_update_texture = dbus_update_texture, +}; + +static NotifierList dbus_display_notifiers = + NOTIFIER_LIST_INITIALIZER(dbus_display_notifiers); + +void +dbus_display_notifier_add(Notifier *notifier) +{ + notifier_list_add(&dbus_display_notifiers, notifier); +} + +static void +dbus_display_notifier_remove(Notifier *notifier) +{ + notifier_remove(notifier); +} + +void +dbus_display_notify(DBusDisplayEvent *event) +{ + notifier_list_notify(&dbus_display_notifiers, event); +} + +static void +dbus_display_init(Object *o) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + g_autoptr(GDBusObjectSkeleton) vm = NULL; + + dd->glctx.ops = &dbus_gl_ops; + if (display_opengl) { + dd->glctx.gls = qemu_gl_init_shader(); + } + dd->iface = qemu_dbus_display1_vm_skeleton_new(); + dd->consoles = g_ptr_array_new_with_free_func(g_object_unref); + + dd->server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT); + + vm = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT "/VM"); + g_dbus_object_skeleton_add_interface( + vm, G_DBUS_INTERFACE_SKELETON(dd->iface)); + g_dbus_object_manager_server_export(dd->server, vm); + + dbus_clipboard_init(dd); + dbus_chardev_init(dd); +} + +static void +dbus_display_finalize(Object *o) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + if (dd->notifier.notify) { + dbus_display_notifier_remove(&dd->notifier); + } + + qemu_clipboard_peer_unregister(&dd->clipboard_peer); + g_clear_object(&dd->clipboard); + + g_clear_object(&dd->server); + g_clear_pointer(&dd->consoles, g_ptr_array_unref); + if (dd->add_client_cancellable) { + g_cancellable_cancel(dd->add_client_cancellable); + } + g_clear_object(&dd->add_client_cancellable); + g_clear_object(&dd->bus); + g_clear_object(&dd->iface); + g_free(dd->dbus_addr); + g_free(dd->audiodev); + g_clear_pointer(&dd->glctx.gls, qemu_gl_fini_shader); + dbus_display = NULL; +} + +static bool +dbus_display_add_console(DBusDisplay *dd, int idx, Error **errp) +{ + QemuConsole *con; + DBusDisplayConsole *dbus_console; + + con = qemu_console_lookup_by_index(idx); + assert(con); + + if (qemu_console_is_graphic(con) && + dd->gl_mode != DISPLAYGL_MODE_OFF) { + qemu_console_set_display_gl_ctx(con, &dd->glctx); + } + + dbus_console = dbus_display_console_new(dd, con); + g_ptr_array_insert(dd->consoles, idx, dbus_console); + g_dbus_object_manager_server_export(dd->server, + G_DBUS_OBJECT_SKELETON(dbus_console)); + return true; +} + +static void +dbus_display_complete(UserCreatable *uc, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(uc); + g_autoptr(GError) err = NULL; + g_autofree char *uuid = qemu_uuid_unparse_strdup(&qemu_uuid); + g_autoptr(GArray) consoles = NULL; + GVariant *console_ids; + int idx; + + if (!object_resolve_path_type("", TYPE_DBUS_DISPLAY, NULL)) { + error_setg(errp, "There is already an instance of %s", + TYPE_DBUS_DISPLAY); + return; + } + + if (dd->p2p) { + /* wait for dbus_display_add_client() */ + dbus_display = dd; + } else if (dd->dbus_addr && *dd->dbus_addr) { + dd->bus = g_dbus_connection_new_for_address_sync(dd->dbus_addr, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | + G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION, + NULL, NULL, &err); + } else { + dd->bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &err); + } + if (err) { + error_setg(errp, "failed to connect to DBus: %s", err->message); + return; + } + + if (dd->audiodev && *dd->audiodev) { + AudioState *audio_state = audio_state_by_name(dd->audiodev); + if (!audio_state) { + error_setg(errp, "Audiodev '%s' not found", dd->audiodev); + return; + } + if (!g_str_equal(audio_state->drv->name, "dbus")) { + error_setg(errp, "Audiodev '%s' is not compatible with DBus", + dd->audiodev); + return; + } + audio_state->drv->set_dbus_server(audio_state, dd->server); + } + + consoles = g_array_new(FALSE, FALSE, sizeof(guint32)); + for (idx = 0;; idx++) { + if (!qemu_console_lookup_by_index(idx)) { + break; + } + if (!dbus_display_add_console(dd, idx, errp)) { + return; + } + g_array_append_val(consoles, idx); + } + + console_ids = g_variant_new_from_data( + G_VARIANT_TYPE("au"), + consoles->data, consoles->len * sizeof(guint32), TRUE, + (GDestroyNotify)g_array_unref, consoles); + g_steal_pointer(&consoles); + g_object_set(dd->iface, + "name", qemu_name ?: "QEMU " QEMU_VERSION, + "uuid", uuid, + "console-ids", console_ids, + NULL); + + if (dd->bus) { + g_dbus_object_manager_server_set_connection(dd->server, dd->bus); + g_bus_own_name_on_connection(dd->bus, "org.qemu", + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, NULL, NULL, NULL); + } +} + +static void +dbus_display_add_client_ready(GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GError) err = NULL; + g_autoptr(GDBusConnection) conn = NULL; + + g_clear_object(&dbus_display->add_client_cancellable); + + conn = g_dbus_connection_new_finish(res, &err); + if (!conn) { + error_printf("Failed to accept D-Bus client: %s", err->message); + } + + g_dbus_object_manager_server_set_connection(dbus_display->server, conn); + g_dbus_connection_start_message_processing(conn); +} + + +static bool +dbus_display_add_client(int csock, Error **errp) +{ + g_autoptr(GError) err = NULL; + g_autoptr(GSocket) socket = NULL; + g_autoptr(GSocketConnection) conn = NULL; + g_autofree char *guid = g_dbus_generate_guid(); + + if (!dbus_display) { + error_setg(errp, "p2p connections not accepted in bus mode"); + return false; + } + + if (dbus_display->add_client_cancellable) { + g_cancellable_cancel(dbus_display->add_client_cancellable); + } + + socket = g_socket_new_from_fd(csock, &err); + if (!socket) { + error_setg(errp, "Failed to setup D-Bus socket: %s", err->message); + return false; + } + + conn = g_socket_connection_factory_create_connection(socket); + + dbus_display->add_client_cancellable = g_cancellable_new(); + + g_dbus_connection_new(G_IO_STREAM(conn), + guid, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER | + G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING, + NULL, + dbus_display->add_client_cancellable, + dbus_display_add_client_ready, + NULL); + + return true; +} + +static bool +get_dbus_p2p(Object *o, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + return dd->p2p; +} + +static void +set_dbus_p2p(Object *o, bool p2p, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + dd->p2p = p2p; +} + +static char * +get_dbus_addr(Object *o, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + return g_strdup(dd->dbus_addr); +} + +static void +set_dbus_addr(Object *o, const char *str, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + g_free(dd->dbus_addr); + dd->dbus_addr = g_strdup(str); +} + +static char * +get_audiodev(Object *o, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + return g_strdup(dd->audiodev); +} + +static void +set_audiodev(Object *o, const char *str, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + g_free(dd->audiodev); + dd->audiodev = g_strdup(str); +} + + +static int +get_gl_mode(Object *o, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + return dd->gl_mode; +} + +static void +set_gl_mode(Object *o, int val, Error **errp) +{ + DBusDisplay *dd = DBUS_DISPLAY(o); + + dd->gl_mode = val; +} + +static void +dbus_display_class_init(ObjectClass *oc, void *data) +{ + UserCreatableClass *ucc = USER_CREATABLE_CLASS(oc); + + ucc->complete = dbus_display_complete; + object_class_property_add_bool(oc, "p2p", get_dbus_p2p, set_dbus_p2p); + object_class_property_add_str(oc, "addr", get_dbus_addr, set_dbus_addr); + object_class_property_add_str(oc, "audiodev", get_audiodev, set_audiodev); + object_class_property_add_enum(oc, "gl-mode", + "DisplayGLMode", &DisplayGLMode_lookup, + get_gl_mode, set_gl_mode); +} + +#define TYPE_CHARDEV_VC "chardev-vc" + +typedef struct DBusVCClass { + DBusChardevClass parent_class; + + void (*parent_parse)(QemuOpts *opts, ChardevBackend *b, Error **errp); +} DBusVCClass; + +DECLARE_CLASS_CHECKERS(DBusVCClass, DBUS_VC, + TYPE_CHARDEV_VC) + +static void +dbus_vc_parse(QemuOpts *opts, ChardevBackend *backend, + Error **errp) +{ + DBusVCClass *klass = DBUS_VC_CLASS(object_class_by_name(TYPE_CHARDEV_VC)); + const char *name = qemu_opt_get(opts, "name"); + const char *id = qemu_opts_id(opts); + + if (name == NULL) { + if (g_str_has_prefix(id, "compat_monitor")) { + name = "org.qemu.monitor.hmp.0"; + } else if (g_str_has_prefix(id, "serial")) { + name = "org.qemu.console.serial.0"; + } else { + name = ""; + } + if (!qemu_opt_set(opts, "name", name, errp)) { + return; + } + } + + klass->parent_parse(opts, backend, errp); +} + +static void +dbus_vc_class_init(ObjectClass *oc, void *data) +{ + DBusVCClass *klass = DBUS_VC_CLASS(oc); + ChardevClass *cc = CHARDEV_CLASS(oc); + + klass->parent_parse = cc->parse; + cc->parse = dbus_vc_parse; +} + +static const TypeInfo dbus_vc_type_info = { + .name = TYPE_CHARDEV_VC, + .parent = TYPE_CHARDEV_DBUS, + .class_size = sizeof(DBusVCClass), + .class_init = dbus_vc_class_init, +}; + +static void +early_dbus_init(DisplayOptions *opts) +{ + DisplayGLMode mode = opts->has_gl ? opts->gl : DISPLAYGL_MODE_OFF; + + if (mode != DISPLAYGL_MODE_OFF) { + if (egl_rendernode_init(opts->u.dbus.rendernode, mode) < 0) { + error_report("dbus: render node init failed"); + exit(1); + } + + display_opengl = 1; + } + + type_register(&dbus_vc_type_info); +} + +static void +dbus_init(DisplayState *ds, DisplayOptions *opts) +{ + DisplayGLMode mode = opts->has_gl ? opts->gl : DISPLAYGL_MODE_OFF; + + if (opts->u.dbus.addr && opts->u.dbus.p2p) { + error_report("dbus: can't accept both addr=X and p2p=yes options"); + exit(1); + } + + using_dbus_display = 1; + + object_new_with_props(TYPE_DBUS_DISPLAY, + object_get_objects_root(), + "dbus-display", &error_fatal, + "addr", opts->u.dbus.addr ?: "", + "audiodev", opts->u.dbus.audiodev ?: "", + "gl-mode", DisplayGLMode_str(mode), + "p2p", yes_no(opts->u.dbus.p2p), + NULL); +} + +static const TypeInfo dbus_display_info = { + .name = TYPE_DBUS_DISPLAY, + .parent = TYPE_OBJECT, + .instance_size = sizeof(DBusDisplay), + .instance_init = dbus_display_init, + .instance_finalize = dbus_display_finalize, + .class_init = dbus_display_class_init, + .interfaces = (InterfaceInfo[]) { + { TYPE_USER_CREATABLE }, + { } + } +}; + +static QemuDisplay qemu_display_dbus = { + .type = DISPLAY_TYPE_DBUS, + .early_init = early_dbus_init, + .init = dbus_init, +}; + +static void register_dbus(void) +{ + qemu_dbus_display = (struct QemuDBusDisplayOps) { + .add_client = dbus_display_add_client, + }; + type_register_static(&dbus_display_info); + qemu_display_register(&qemu_display_dbus); +} + +type_init(register_dbus); + +#ifdef CONFIG_OPENGL +module_dep("ui-opengl"); +#endif diff --git a/ui/dbus.h b/ui/dbus.h new file mode 100644 index 00000000..9c149e7b --- /dev/null +++ b/ui/dbus.h @@ -0,0 +1,148 @@ +/* + * QEMU DBus display + * + * Copyright (c) 2021 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. + */ + +#ifndef UI_DBUS_H +#define UI_DBUS_H + +#include "chardev/char-socket.h" +#include "qemu/dbus.h" +#include "qom/object.h" +#include "ui/console.h" +#include "ui/clipboard.h" + +#include "ui/dbus-display1.h" + +typedef struct DBusClipboardRequest { + GDBusMethodInvocation *invocation; + QemuClipboardType type; + guint timeout_id; +} DBusClipboardRequest; + +struct DBusDisplay { + Object parent; + + DisplayGLMode gl_mode; + bool p2p; + char *dbus_addr; + char *audiodev; + DisplayGLCtx glctx; + + GDBusConnection *bus; + GDBusObjectManagerServer *server; + QemuDBusDisplay1VM *iface; + GPtrArray *consoles; + GCancellable *add_client_cancellable; + + QemuClipboardPeer clipboard_peer; + QemuDBusDisplay1Clipboard *clipboard; + QemuDBusDisplay1Clipboard *clipboard_proxy; + DBusClipboardRequest clipboard_request[QEMU_CLIPBOARD_SELECTION__COUNT]; + + Notifier notifier; +}; + +#define TYPE_DBUS_DISPLAY "dbus-display" +OBJECT_DECLARE_SIMPLE_TYPE(DBusDisplay, DBUS_DISPLAY) + +void dbus_display_notifier_add(Notifier *notifier); + +#define DBUS_DISPLAY_TYPE_CONSOLE dbus_display_console_get_type() +G_DECLARE_FINAL_TYPE(DBusDisplayConsole, + dbus_display_console, + DBUS_DISPLAY, + CONSOLE, + GDBusObjectSkeleton) + +DBusDisplayConsole * +dbus_display_console_new(DBusDisplay *display, QemuConsole *con); + +int +dbus_display_console_get_index(DBusDisplayConsole *ddc); + + +extern const DisplayChangeListenerOps dbus_console_dcl_ops; + +#define DBUS_DISPLAY_TYPE_LISTENER dbus_display_listener_get_type() +G_DECLARE_FINAL_TYPE(DBusDisplayListener, + dbus_display_listener, + DBUS_DISPLAY, + LISTENER, + GObject) + +DBusDisplayListener * +dbus_display_listener_new(const char *bus_name, + GDBusConnection *conn, + DBusDisplayConsole *console); + +DBusDisplayConsole * +dbus_display_listener_get_console(DBusDisplayListener *ddl); + +const char * +dbus_display_listener_get_bus_name(DBusDisplayListener *ddl); + +extern const DisplayChangeListenerOps dbus_gl_dcl_ops; +extern const DisplayChangeListenerOps dbus_dcl_ops; + +#define TYPE_CHARDEV_DBUS "chardev-dbus" + +typedef struct DBusChardevClass { + SocketChardevClass parent_class; + + void (*parent_chr_be_event)(Chardev *s, QEMUChrEvent event); +} DBusChardevClass; + +DECLARE_CLASS_CHECKERS(DBusChardevClass, DBUS_CHARDEV, + TYPE_CHARDEV_DBUS) + +typedef struct DBusChardev { + SocketChardev parent; + + bool exported; + QemuDBusDisplay1Chardev *iface; +} DBusChardev; + +DECLARE_INSTANCE_CHECKER(DBusChardev, DBUS_CHARDEV, TYPE_CHARDEV_DBUS) + +#define CHARDEV_IS_DBUS(chr) \ + object_dynamic_cast(OBJECT(chr), TYPE_CHARDEV_DBUS) + +typedef enum { + DBUS_DISPLAY_CHARDEV_OPEN, + DBUS_DISPLAY_CHARDEV_CLOSE, +} DBusDisplayEventType; + +typedef struct DBusDisplayEvent { + DBusDisplayEventType type; + union { + DBusChardev *chardev; + }; +} DBusDisplayEvent; + +void dbus_display_notify(DBusDisplayEvent *event); + +void dbus_chardev_init(DBusDisplay *dpy); + +void dbus_clipboard_init(DBusDisplay *dpy); + +#endif /* UI_DBUS_H */ diff --git a/ui/egl-context.c b/ui/egl-context.c new file mode 100644 index 00000000..eb5f520f --- /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(DisplayGLCtx *dgc, + 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(DisplayGLCtx *dgc, QEMUGLContext ctx) +{ + eglDestroyContext(qemu_egl_display, ctx); +} + +int qemu_egl_make_context_current(DisplayGLCtx *dgc, + 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 00000000..7a30fd97 --- /dev/null +++ b/ui/egl-headless.c @@ -0,0 +1,240 @@ +#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(DisplayGLCtx *dgc, + QEMUGLParams *params) +{ + eglMakeCurrent(qemu_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + qemu_egl_rn_ctx); + return qemu_egl_create_context(dgc, 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_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 bool +egl_is_compatible_dcl(DisplayGLCtx *dgc, + DisplayChangeListener *dcl) +{ + if (!dcl->ops->dpy_gl_update) { + /* + * egl-headless is compatible with all 2d listeners, as it blits the GL + * updates on the 2d console surface. + */ + return true; + } + + return dcl->ops == &egl_ops; +} + +static const DisplayGLCtxOps eglctx_ops = { + .dpy_gl_ctx_is_compatible_dcl = egl_is_compatible_dcl, + .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, +}; + +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++) { + DisplayGLCtx *ctx; + + 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(); + ctx = g_new0(DisplayGLCtx, 1); + ctx->ops = &eglctx_ops; + qemu_console_set_display_gl_ctx(con, ctx); + 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 00000000..3a88245b --- /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 00000000..8d8a636f --- /dev/null +++ b/ui/gtk-clipboard.c @@ -0,0 +1,207 @@ +/* + * 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/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_update_info(GtkDisplayState *gd, + QemuClipboardInfo *info) +{ + QemuClipboardSelection s = info->selection; + bool self_update = info->owner == &gd->cbpeer; + + if (info != qemu_clipboard_info(s)) { + gd->cbpending[s] = 0; + if (!self_update) { + g_autoptr(GtkTargetList) list = NULL; + 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]); + if (targets) { + 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); + } + } + 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_notify(Notifier *notifier, void *data) +{ + GtkDisplayState *gd = + container_of(notifier, GtkDisplayState, cbpeer.notifier); + QemuClipboardNotify *notify = data; + + switch (notify->type) { + case QEMU_CLIPBOARD_UPDATE_INFO: + gd_clipboard_update_info(gd, notify->info); + return; + case QEMU_CLIPBOARD_RESET_SERIAL: + /* ignore */ + return; + } +} + +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.notifier.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 00000000..e8443179 --- /dev/null +++ b/ui/gtk-egl.c @@ -0,0 +1,373 @@ +/* + * 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(); + } +} + +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); + + gd_update_monitor_refresh_rate( + vc, 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); + } + + eglMakeCurrent(qemu_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + EGL_NO_CONTEXT); +} + +QEMUGLContext gd_egl_create_context(DisplayGLCtx *dgc, + QEMUGLParams *params) +{ + VirtualConsole *vc = container_of(dgc, VirtualConsole, gfx.dgc); + + eglMakeCurrent(qemu_egl_display, vc->gfx.esurface, + vc->gfx.esurface, vc->gfx.ectx); + return qemu_egl_create_context(dgc, 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 && !vc->gfx.guest_fb.dmabuf->draw_submitted) { + 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(DisplayGLCtx *dgc, + QEMUGLContext ctx) +{ + VirtualConsole *vc = container_of(dgc, VirtualConsole, gfx.dgc); + + 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 00000000..7696df1f --- /dev/null +++ b/ui/gtk-gl-area.c @@ -0,0 +1,320 @@ +/* + * 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); + } +} + +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); + + gd_update_monitor_refresh_rate(vc, 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); + } +} + +static int gd_cmp_gl_context_version(int major, int minor, QEMUGLParams *params) +{ + if (major > params->major_ver) { + return 1; + } + if (major < params->major_ver) { + return -1; + } + if (minor > params->minor_ver) { + return 1; + } + if (minor < params->minor_ver) { + return -1; + } + return 0; +} + +QEMUGLContext gd_gl_area_create_context(DisplayGLCtx *dgc, + QEMUGLParams *params) +{ + VirtualConsole *vc = container_of(dgc, VirtualConsole, gfx.dgc); + GdkWindow *window; + GdkGLContext *ctx; + GError *err = NULL; + int major, minor; + + 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; + } + + gdk_gl_context_make_current(ctx); + gdk_gl_context_get_version(ctx, &major, &minor); + gdk_gl_context_clear_current(); + gtk_gl_area_make_current(GTK_GL_AREA(vc->gfx.drawing_area)); + + if (gd_cmp_gl_context_version(major, minor, params) == -1) { + /* created ctx version < requested version */ + g_clear_object(&ctx); + } + + trace_gd_gl_area_create_context(ctx, params->major_ver, params->minor_ver); + return ctx; +} + +void gd_gl_area_destroy_context(DisplayGLCtx *dgc, QEMUGLContext ctx) +{ + GdkGLContext *current_ctx = gdk_gl_context_get_current(); + + trace_gd_gl_area_destroy_context(ctx, current_ctx); + if (ctx == current_ctx) { + gdk_gl_context_clear_current(); + } + g_clear_object(&ctx); +} + +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 && !vc->gfx.guest_fb.dmabuf->draw_submitted) { + 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(DisplayGLCtx *dgc, + 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 00000000..4817623c --- /dev/null +++ b/ui/gtk.c @@ -0,0 +1,2477 @@ +/* + * 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); +} + +/** 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_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, +}; + +static bool +gd_gl_area_is_compatible_dcl(DisplayGLCtx *dgc, + DisplayChangeListener *dcl) +{ + return dcl->ops == &dcl_gl_area_ops; +} + +static const DisplayGLCtxOps gl_area_ctx_ops = { + .dpy_gl_ctx_is_compatible_dcl = gd_gl_area_is_compatible_dcl, + .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, +}; + +#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_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, +}; + +static bool +gd_egl_is_compatible_dcl(DisplayGLCtx *dgc, + DisplayChangeListener *dcl) +{ + return dcl->ops == &dcl_egl_ops; +} + +static const DisplayGLCtxOps egl_ctx_ops = { + .dpy_gl_ctx_is_compatible_dcl = gd_egl_is_compatible_dcl, + .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, +}; +#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() && s->ptr_owner) { + if (!s->ptr_owner->window) { + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(s->grab_item), + FALSE); + } else { + gd_ungrab_pointer(s); + } + } + 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_refresh_rate(VirtualConsole *vc, int refresh_rate) +{ + QemuUIInfo info; + + info = *dpy_get_ui_info(vc->gfx.dcl.con); + info.refresh_rate = refresh_rate; + dpy_set_ui_info(vc->gfx.dcl.con, &info, true); +} + +static void gd_set_ui_size(VirtualConsole *vc, gint width, gint height) +{ + QemuUIInfo info; + + info = *dpy_get_ui_info(vc->gfx.dcl.con); + info.width = width; + info.height = height; + dpy_set_ui_info(vc->gfx.dcl.con, &info, true); +} + +#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_size(vc, width, height); +} + +#endif + +void gd_update_monitor_refresh_rate(VirtualConsole *vc, GtkWidget *widget) +{ +#ifdef GDK_VERSION_3_22 + GdkWindow *win = gtk_widget_get_window(widget); + int refresh_rate; + + if (win) { + GdkDisplay *dpy = gtk_widget_get_display(widget); + GdkMonitor *monitor = gdk_display_get_monitor_at_window(dpy, win); + refresh_rate = gdk_monitor_get_refresh_rate(monitor); /* [mHz] */ + } else { + refresh_rate = 0; + } + + gd_set_ui_refresh_rate(vc, refresh_rate); + + /* T = 1 / f = 1 [s*Hz] / f = 1000*1000 [ms*mHz] / f */ + vc->gfx.dcl.update_interval = refresh_rate ? + MIN(1000 * 1000 / refresh_rate, GUI_REFRESH_INTERVAL_DEFAULT) : + GUI_REFRESH_INTERVAL_DEFAULT; +#endif +} + +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; + } + + gd_update_monitor_refresh_rate(vc, 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; + } + + if (button->type == GDK_2BUTTON_PRESS || button->type == GDK_3BUTTON_PRESS) { + 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_vertical; + InputButton btn_horizontal; + bool has_vertical = false; + bool has_horizontal = false; + + if (scroll->direction == GDK_SCROLL_UP) { + btn_vertical = INPUT_BUTTON_WHEEL_UP; + has_vertical = true; + } else if (scroll->direction == GDK_SCROLL_DOWN) { + btn_vertical = INPUT_BUTTON_WHEEL_DOWN; + has_vertical = true; + } else if (scroll->direction == GDK_SCROLL_LEFT) { + btn_horizontal = INPUT_BUTTON_WHEEL_LEFT; + has_horizontal = true; + } else if (scroll->direction == GDK_SCROLL_RIGHT) { + btn_horizontal = INPUT_BUTTON_WHEEL_RIGHT; + has_horizontal = true; + } 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) { + btn_vertical = INPUT_BUTTON_WHEEL_DOWN; + has_vertical = true; + } else if (delta_y < 0) { + btn_vertical = INPUT_BUTTON_WHEEL_UP; + has_vertical = true; + } else if (delta_x > 0) { + btn_horizontal = INPUT_BUTTON_WHEEL_RIGHT; + has_horizontal = true; + } else if (delta_x < 0) { + btn_horizontal = INPUT_BUTTON_WHEEL_LEFT; + has_horizontal = true; + } else { + return TRUE; + } + } else { + return TRUE; + } + + if (has_vertical) { + qemu_input_queue_btn(vc->gfx.dcl.con, btn_vertical, true); + qemu_input_event_sync(); + qemu_input_queue_btn(vc->gfx.dcl.con, btn_vertical, false); + qemu_input_event_sync(); + } + + if (has_horizontal) { + qemu_input_queue_btn(vc->gfx.dcl.con, btn_horizontal, true); + qemu_input_event_sync(); + qemu_input_queue_btn(vc->gfx.dcl.con, btn_horizontal, 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_size(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, 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; + vc->gfx.dgc.ops = &gl_area_ctx_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.dgc.ops = &egl_ctx_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; + + if (display_opengl) { + qemu_console_set_display_gl_ctx(con, &vc->gfx.dgc); + } + 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, DisplayOptions *opts) +{ + 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), + !opts->u.gtk.has_show_menubar || + opts->u.gtk.show_menubar); + 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, DisplayOptions *opts) +{ + 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, opts); + + 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, opts); + + 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); + if (opts->u.gtk.has_show_menubar && + !opts->u.gtk.show_menubar) { + gtk_widget_hide(s->menu_bar); + } + + 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)); + } + if (opts->u.gtk.has_show_tabs && + opts->u.gtk.show_tabs) { + gtk_menu_item_activate(GTK_MENU_ITEM(s->show_tabs_item)); + } +#ifdef CONFIG_GTK_CLIPBOARD + gd_clipboard_init(s); +#endif /* CONFIG_GTK_CLIPBOARD */ +} + +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 00000000..20bd64cc --- /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 00000000..12c52080 --- /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 00000000..24ca23a1 --- /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 00000000..96831807 --- /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 00000000..ff4f0460 --- /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 00000000..f039c6e2 --- /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 00000000..a39c0e30 --- /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 00000000..c0daa54a --- /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 00000000..b746096c --- /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 00000000..06728122 --- /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 00000000..86aaa639 --- /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 00000000..e00c8b4c --- /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 00000000..2d57ca70 --- /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 00000000..e5b09059 --- /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 00000000..1b756a69 --- /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 00000000..46ea74e4 --- /dev/null +++ b/ui/input-legacy.c @@ -0,0 +1,290 @@ +/* + * 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 "qemu/log.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); + } + if (btn->down && btn->button == INPUT_BUTTON_WHEEL_RIGHT) { + s->qemu_put_mouse_event(s->qemu_put_mouse_event_opaque, + s->axis[INPUT_AXIS_X], + s->axis[INPUT_AXIS_Y], + -2, + s->buttons); + } + if (btn->down && btn->button == INPUT_BUTTON_WHEEL_LEFT) { + s->qemu_put_mouse_event(s->qemu_put_mouse_event_opaque, + s->axis[INPUT_AXIS_X], + s->axis[INPUT_AXIS_Y], + 2, + 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 00000000..e572a2e9 --- /dev/null +++ b/ui/input-linux.c @@ -0,0 +1,537 @@ +/* + * 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; + } + if (!g_unix_set_fd_nonblocking(il->fd, true, NULL)) { + error_setg_errno(errp, errno, "Failed to set FD nonblocking"); + return; + } + + 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 00000000..e2a90af8 --- /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 + * need 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 00000000..62d42a7a --- /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/keycodemapdb/LICENSE.BSD b/ui/keycodemapdb/LICENSE.BSD new file mode 100644 index 00000000..ec1a29d3 --- /dev/null +++ b/ui/keycodemapdb/LICENSE.BSD @@ -0,0 +1,27 @@ +Copyright (c) Individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. 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. + + 3. Neither the name of PyCA Cryptography 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 COPYRIGHT OWNER 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. diff --git a/ui/keycodemapdb/LICENSE.GPL2 b/ui/keycodemapdb/LICENSE.GPL2 new file mode 100644 index 00000000..d511905c --- /dev/null +++ b/ui/keycodemapdb/LICENSE.GPL2 @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/ui/keycodemapdb/README b/ui/keycodemapdb/README new file mode 100644 index 00000000..8b4a845b --- /dev/null +++ b/ui/keycodemapdb/README @@ -0,0 +1,115 @@ + Key code / scan code / key symbol mapping database + ================================================== + +This module provides a database that maps between different +key code / scan code / key symbol sets: + + - Linux evdev + - OS-X + - AT Set 1 + - AT Set 2 + - AT Set 3 + - XT + - Linux XT KBD driver + - USB HID + - Win32 + - XWin XT + - XKBD XT + - Xorg Evdev + - Xorg KBD + - Xorg OS-X + - XOrg Cygwin + - RFB + +Licensing +--------- + +The contents of this package are dual licensed under the terms of: + + - GNU General Public License (version 2 or later) + - 3-clause BSD License + +The output files generated by keymap-gen may be distributed & used under +the terms of either of the above licenses. + +Data formats +------------ + +The following output formats are possible + + - Code map + + An array mapping between key code sets values + + Indexes in the array are values from the source code set. + Entries in the array are values from the target code set + + + - Code table + + An array listing all values in a key code set + + Indexes in the array are simply a numeric counter + Entries in the array are values from the key code set + + The size of the array matches the total number of entries in + the keycode database. + + + - Name map + + An array mapping between key code sets values and names + + Indexes in the array are values from the source code set + Entries in the array are names from the target code set + + + - Name table + + An array listing all names in a key code set + + Indexes in the array are simply a numeric counter + Entries in the array are values from the key code set + + The size of the array matches the total number of entries in + the keycode database. + + +Output languages +---------------- + +The tool is capable of generating data tables for the following +programming languages / environments + + - Standard C + - GLib2 (standard C, but with GLib2 data types) + - Python + - Perl + - Rust + + +Usage +----- + +Map values from AT Set 1 to USB HID, generating tables for the +C programming language + + $ keymap-gen --lang stdc code-map data/keymaps.csv atset1 usb + +Generate a tables of names for Linux key codes, OS-X key codes, +in python - equivalent array indexes map between the two sets. +A variable name override is used + + $ keymap-gen --varname linux_keycodes --lang stdc \ + code-table data/keymaps.csv linux + $ keymap-gen --varname osx_keycodes --lang stdc \ + code-table data/keymaps.csv os-x + +Generate a mapping from XOrg XWin values to Win32 names + + $ keymap-gen --lang perl name-map data/keymaps.csv xorgxwin win32 + +Generate a table of names for Linux key codes in Perl + + $ keymap-gen --lang perl name-table data/keymaps.csv linux + diff --git a/ui/keycodemapdb/data/README b/ui/keycodemapdb/data/README new file mode 100644 index 00000000..6b565349 --- /dev/null +++ b/ui/keycodemapdb/data/README @@ -0,0 +1,89 @@ +This directory contains the raw data for mapping between different +keyboard codes. Naming if often based on the US keyboard layout, but +does not indicate the symbol actually generated by the key. + +The columns currently in this data set are: + +Linux +----- + +Name and value of the hardware independent keycodes used by the linux +kernel and exposed through the input subsystem. + +References: linux/input.h + +macOS +----- + +Low level key codes as exposed by Mac OS X/macOS. + +References: Carbon/HIToolbox/Events.h + +PC scan code sets +----------------- + +Scan codes for the three orignal PC keyboard generations: + + Set 1: XT + Set 2: AT + Set 3: PS/2 + +The sets include codes for modern keys as well and not just the keys +present on those original keyboards. + +References: linux/drivers/input/keyboard/atkbd.c + +USB HID +------- + +Codes as specified by the HID profile in USB. + +References: linux/drivers/hid/usbhid/usbkbd.c + +Windows Virtual-key codes +------------------------- + +The low level, hardware independent "VKEYs" exposed by Windows. + +References: mingw32/winuser.h + +XWin XT +------- + +X11 keycodes generated by the XWin server. Based on the XT scan code +set. + +References: xorg-server/hw/xwin/{winkeybd.c,winkeynames.h} + +Xfree86 KBD XT +-------------- + +X11 keycodes generated by the Xfree86 keyboard drivers. Based on the XT +scan code set. + +References: xf86-input-keyboard/src/at_scancode.c + +X11 keysyms +----------- + +Corresponding X11 keysym value(s) for a US keyboard layout. + +WARNING: These columns represent symbols, not physical keys, and should + be used with extreme care. + +References: http://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h + +HTML KeyboardEvent.code +----------------------- + +Key codes seen in the KeyboardEvent.code attribute as part of the +UI Events specification. + +References: https://www.w3.org/TR/uievents-code/ + +XKEYBOARD key names +------------------- + +Hardware independent key names as used in the XKEYBOARD extension. + +References: /usr/share/X11/xkb/keycodes/ diff --git a/ui/keycodemapdb/data/keymaps.csv b/ui/keycodemapdb/data/keymaps.csv new file mode 100644 index 00000000..1ffe3b03 --- /dev/null +++ b/ui/keycodemapdb/data/keymaps.csv @@ -0,0 +1,539 @@ +"Linux Name","Linux Keycode","OS-X Name","OS-X Keycode","AT set1 keycode","AT set2 keycode","AT set3 keycode","USB Keycodes","Win32 Name","Win32 Keycode","Xwin XT","Xfree86 KBD XT","X11 keysym name","X11 keysym","HTML code","XKB key name","QEMU QKeyCode","Sun KBD","Apple ADB" +KEY_RESERVED,0,,0xff,,,,,,,,,,,,,unmapped,,0xff +KEY_ESC,1,Escape,0x35,0x01,0x76,0x08,41,VK_ESCAPE,0x1b,1,1,XK_Escape,0xff1b,Escape,ESC,esc,0x1d,0x35 +KEY_1,2,ANSI_1,0x12,0x02,0x16,0x16,30,VK_1,0x31,2,2,XK_1,0x0031,Digit1,AE01,1,0x1e,0x12 +KEY_1,2,ANSI_1,0x12,0x02,0x16,0x16,30,VK_1,0x31,2,2,XK_exclam,0x0021,Digit1,AE01,1,0x1e,0x12 +KEY_2,3,ANSI_2,0x13,0x03,0x1e,0x1e,31,VK_2,0x32,3,3,XK_2,0x0032,Digit2,AE02,2,0x1f,0x13 +KEY_2,3,ANSI_2,0x13,0x03,0x1e,0x1e,31,VK_2,0x32,3,3,XK_at,0x0040,Digit2,AE02,2,0x1f,0x13 +KEY_3,4,ANSI_3,0x14,0x04,0x26,0x26,32,VK_3,0x33,4,4,XK_3,0x0033,Digit3,AE03,3,0x20,0x14 +KEY_3,4,ANSI_3,0x14,0x04,0x26,0x26,32,VK_3,0x33,4,4,XK_numbersign,0x0023,Digit3,AE03,3,0x20,0x14 +KEY_4,5,ANSI_4,0x15,0x05,0x25,0x25,33,VK_4,0x34,5,5,XK_4,0x0034,Digit4,AE04,4,0x21,0x15 +KEY_4,5,ANSI_4,0x15,0x05,0x25,0x25,33,VK_4,0x34,5,5,XK_dollar,0x0024,Digit4,AE04,4,0x21,0x15 +KEY_5,6,ANSI_5,0x17,0x06,0x2e,0x2e,34,VK_5,0x35,6,6,XK_5,0x0035,Digit5,AE05,5,0x22,0x17 +KEY_5,6,ANSI_5,0x17,0x06,0x2e,0x2e,34,VK_5,0x35,6,6,XK_percent,0x0025,Digit5,AE05,5,0x22,0x17 +KEY_6,7,ANSI_6,0x16,0x07,0x36,0x36,35,VK_6,0x36,7,7,XK_6,0x0036,Digit6,AE06,6,0x23,0x16 +KEY_6,7,ANSI_6,0x16,0x07,0x36,0x36,35,VK_6,0x36,7,7,XK_asciicircum,0x005e,Digit6,AE06,6,0x23,0x16 +KEY_7,8,ANSI_7,0x1a,0x08,0x3d,0x3d,36,VK_7,0x37,8,8,XK_7,0x0037,Digit7,AE07,7,0x24,0x1a +KEY_7,8,ANSI_7,0x1a,0x08,0x3d,0x3d,36,VK_7,0x37,8,8,XK_ampersand,0x0026,Digit7,AE07,7,0x24,0x1a +KEY_8,9,ANSI_8,0x1c,0x09,0x3e,0x3e,37,VK_8,0x38,9,9,XK_8,0x0038,Digit8,AE08,8,0x25,0x1c +KEY_8,9,ANSI_8,0x1c,0x09,0x3e,0x3e,37,VK_8,0x38,9,9,XK_asterisk,0x002a,Digit8,AE08,8,0x25,0x1c +KEY_9,10,ANSI_9,0x19,0x0a,0x46,0x46,38,VK_9,0x39,10,10,XK_9,0x0039,Digit9,AE09,9,0x26,0x19 +KEY_9,10,ANSI_9,0x19,0x0a,0x46,0x46,38,VK_9,0x39,10,10,XK_parenleft,0x0028,Digit9,AE09,9,0x26,0x19 +KEY_0,11,ANSI_0,0x1d,0x0b,0x45,0x45,39,VK_0,0x30,11,11,XK_0,0x0030,Digit0,AE10,0,0x27,0x1d +KEY_0,11,ANSI_0,0x1d,0x0b,0x45,0x45,39,VK_0,0x30,11,11,XK_parenright,0x0029,Digit0,AE10,0,0x27,0x1d +KEY_MINUS,12,ANSI_Minus,0x1b,0x0c,0x4e,0x4e,45,VK_OEM_MINUS,0xbd,12,12,XK_minus,0x002d,Minus,AE11,minus,0x28,0x1b +KEY_MINUS,12,ANSI_Minus,0x1b,0x0c,0x4e,0x4e,45,VK_OEM_MINUS,0xbd,12,12,XK_underscore,0x005f,Minus,AE11,minus,0x28,0x1b +KEY_EQUAL,13,ANSI_Equal,0x18,0x0d,0x55,0x55,46,VK_OEM_PLUS,0xbb,13,13,XK_equal,0x003d,Equal,AE12,equal,0x29,0x18 +KEY_EQUAL,13,ANSI_Equal,0x18,0x0d,0x55,0x55,46,VK_OEM_PLUS,0xbb,13,13,XK_plus,0x002b,Equal,AE12,equal,0x29,0x18 +KEY_BACKSPACE,14,Delete,0x33,0x0e,0x66,0x66,42,VK_BACK,0x08,14,14,XK_BackSpace,0xff08,Backspace,BKSP,backspace,0x2b,0x33 +KEY_TAB,15,Tab,0x30,0x0f,0x0d,0x0d,43,VK_TAB,0x09,15,15,XK_Tab,0xff09,Tab,TAB,tab,0x35,0x30 +KEY_Q,16,ANSI_Q,0xc,0x10,0x15,0x15,20,VK_Q,0x51,16,16,XK_Q,0x0051,KeyQ,AD01,q,0x36,0xc +KEY_Q,16,ANSI_Q,0xc,0x10,0x15,0x15,20,VK_Q,0x51,16,16,XK_q,0x0071,KeyQ,AD01,q,0x36,0xc +KEY_W,17,ANSI_W,0xd,0x11,0x1d,0x1d,26,VK_W,0x57,17,17,XK_W,0x0057,KeyW,AD02,w,0x37,0xd +KEY_W,17,ANSI_W,0xd,0x11,0x1d,0x1d,26,VK_W,0x57,17,17,XK_w,0x0077,KeyW,AD02,w,0x37,0xd +KEY_E,18,ANSI_E,0xe,0x12,0x24,0x24,8,VK_E,0x45,18,18,XK_E,0x0045,KeyE,AD03,e,0x38,0xe +KEY_E,18,ANSI_E,0xe,0x12,0x24,0x24,8,VK_E,0x45,18,18,XK_e,0x0065,KeyE,AD03,e,0x38,0xe +KEY_R,19,ANSI_R,0xf,0x13,0x2d,0x2d,21,VK_R,0x52,19,19,XK_R,0x0052,KeyR,AD04,r,0x39,0xf +KEY_R,19,ANSI_R,0xf,0x13,0x2d,0x2d,21,VK_R,0x52,19,19,XK_r,0x0072,KeyR,AD04,r,0x39,0xf +KEY_T,20,ANSI_T,0x11,0x14,0x2c,0x2c,23,VK_T,0x54,20,20,XK_T,0x0054,KeyT,AD05,t,0x3a,0x11 +KEY_T,20,ANSI_T,0x11,0x14,0x2c,0x2c,23,VK_T,0x54,20,20,XK_t,0x0074,KeyT,AD05,t,0x3a,0x11 +KEY_Y,21,ANSI_Y,0x10,0x15,0x35,0x35,28,VK_Y,0x59,21,21,XK_Y,0x0059,KeyY,AD06,y,0x3b,0x10 +KEY_Y,21,ANSI_Y,0x10,0x15,0x35,0x35,28,VK_Y,0x59,21,21,XK_y,0x0079,KeyY,AD06,y,0x3b,0x10 +KEY_U,22,ANSI_U,0x20,0x16,0x3c,0x3c,24,VK_U,0x55,22,22,XK_U,0x0055,KeyU,AD07,u,0x3c,0x20 +KEY_U,22,ANSI_U,0x20,0x16,0x3c,0x3c,24,VK_U,0x55,22,22,XK_u,0x0075,KeyU,AD07,u,0x3c,0x20 +KEY_I,23,ANSI_I,0x22,0x17,0x43,0x43,12,VK_I,0x49,23,23,XK_I,0x0049,KeyI,AD08,i,0x3d,0x22 +KEY_I,23,ANSI_I,0x22,0x17,0x43,0x43,12,VK_I,0x49,23,23,XK_i,0x0069,KeyI,AD08,i,0x3d,0x22 +KEY_O,24,ANSI_O,0x1f,0x18,0x44,0x44,18,VK_O,0x4f,24,24,XK_O,0x004f,KeyO,AD09,o,0x3e,0x1f +KEY_O,24,ANSI_O,0x1f,0x18,0x44,0x44,18,VK_O,0x4f,24,24,XK_o,0x006f,KeyO,AD09,o,0x3e,0x1f +KEY_P,25,ANSI_P,0x23,0x19,0x4d,0x4d,19,VK_P,0x50,25,25,XK_P,0x0050,KeyP,AD10,p,0x3f,0x23 +KEY_P,25,ANSI_P,0x23,0x19,0x4d,0x4d,19,VK_P,0x50,25,25,XK_p,0x0070,KeyP,AD10,p,0x3f,0x23 +KEY_LEFTBRACE,26,ANSI_LeftBracket,0x21,0x1a,0x54,0x54,47,VK_OEM_4,0xdb,26,26,XK_bracketleft,0x005b,BracketLeft,AD11,bracket_left,0x40,0x21 +KEY_LEFTBRACE,26,ANSI_LeftBracket,0x21,0x1a,0x54,0x54,47,VK_OEM_4,0xdb,26,26,XK_braceleft,0x007b,BracketLeft,AD11,bracket_left,0x40,0x21 +KEY_RIGHTBRACE,27,ANSI_RightBracket,0x1e,0x1b,0x5b,0x5b,48,VK_OEM_6,0xdd,27,27,XK_bracketright,0x005d,BracketRight,AD12,bracket_right,0x41,0x1e +KEY_RIGHTBRACE,27,ANSI_RightBracket,0x1e,0x1b,0x5b,0x5b,48,VK_OEM_6,0xdd,27,27,XK_braceright,0x007d,BracketRight,AD12,bracket_right,0x41,0x1e +KEY_ENTER,28,Return,0x24,0x1c,0x5a,0x5a,40,VK_RETURN,0x0d,28,28,XK_Return,0xff0d,Enter,RTRN,ret,0x59,0x24 +KEY_LEFTCTRL,29,Control,0x3b,0x1d,0x14,0x11,224,VK_LCONTROL,0xa2,29,29,XK_Control_L,0xffe3,ControlLeft,LCTL,ctrl,0x4c,0x36 +KEY_LEFTCTRL,29,Control,0x3b,0x1d,0x14,0x11,224,VK_CONTROL,0x11,29,29,XK_Control_L,0xffe3,ControlLeft,LCTL,ctrl,0x4c,0x36 +KEY_A,30,ANSI_A,0x0,0x1e,0x1c,0x1c,4,VK_A,0x41,30,30,XK_A,0x0041,KeyA,AC01,a,0x4d,0x0 +KEY_A,30,ANSI_A,0x0,0x1e,0x1c,0x1c,4,VK_A,0x41,30,30,XK_a,0x0061,KeyA,AC01,a,0x4d,0x0 +KEY_S,31,ANSI_S,0x1,0x1f,0x1b,0x1b,22,VK_S,0x53,31,31,XK_S,0x0053,KeyS,AC02,s,0x4e,0x1 +KEY_S,31,ANSI_S,0x1,0x1f,0x1b,0x1b,22,VK_S,0x53,31,31,XK_s,0x0073,KeyS,AC02,s,0x4e,0x1 +KEY_D,32,ANSI_D,0x2,0x20,0x23,0x23,7,VK_D,0x44,32,32,XK_D,0x0044,KeyD,AC03,d,0x4f,0x2 +KEY_D,32,ANSI_D,0x2,0x20,0x23,0x23,7,VK_D,0x44,32,32,XK_d,0x0064,KeyD,AC03,d,0x4f,0x2 +KEY_F,33,ANSI_F,0x3,0x21,0x2b,0x2b,9,VK_F,0x46,33,33,XK_F,0x0046,KeyF,AC04,f,0x50,0x3 +KEY_F,33,ANSI_F,0x3,0x21,0x2b,0x2b,9,VK_F,0x46,33,33,XK_f,0x0066,KeyF,AC04,f,0x50,0x3 +KEY_G,34,ANSI_G,0x5,0x22,0x34,0x34,10,VK_G,0x47,34,34,XK_G,0x0047,KeyG,AC05,g,0x51,0x5 +KEY_G,34,ANSI_G,0x5,0x22,0x34,0x34,10,VK_G,0x47,34,34,XK_g,0x0067,KeyG,AC05,g,0x51,0x5 +KEY_H,35,ANSI_H,0x4,0x23,0x33,0x33,11,VK_H,0x48,35,35,XK_H,0x0048,KeyH,AC06,h,0x52,0x4 +KEY_H,35,ANSI_H,0x4,0x23,0x33,0x33,11,VK_H,0x48,35,35,XK_h,0x0068,KeyH,AC06,h,0x52,0x4 +KEY_J,36,ANSI_J,0x26,0x24,0x3b,0x3b,13,VK_J,0x4a,36,36,XK_J,0x004a,KeyJ,AC07,j,0x53,0x26 +KEY_J,36,ANSI_J,0x26,0x24,0x3b,0x3b,13,VK_J,0x4a,36,36,XK_j,0x006a,KeyJ,AC07,j,0x53,0x26 +KEY_K,37,ANSI_K,0x28,0x25,0x42,0x42,14,VK_K,0x4b,37,37,XK_K,0x004b,KeyK,AC08,k,0x54,0x28 +KEY_K,37,ANSI_K,0x28,0x25,0x42,0x42,14,VK_K,0x4b,37,37,XK_k,0x006b,KeyK,AC08,k,0x54,0x28 +KEY_L,38,ANSI_L,0x25,0x26,0x4b,0x4b,15,VK_L,0x4c,38,38,XK_L,0x004c,KeyL,AC09,l,0x55,0x25 +KEY_L,38,ANSI_L,0x25,0x26,0x4b,0x4b,15,VK_L,0x4c,38,38,XK_l,0x006c,KeyL,AC09,l,0x55,0x25 +KEY_SEMICOLON,39,ANSI_Semicolon,0x29,0x27,0x4c,0x4c,51,VK_OEM_1,0xba,39,39,XK_semicolon,0x003b,Semicolon,AC10,semicolon,0x56,0x29 +KEY_SEMICOLON,39,ANSI_Semicolon,0x29,0x27,0x4c,0x4c,51,VK_OEM_1,0xba,39,39,XK_colon,0x003a,Semicolon,AC10,semicolon,0x56,0x29 +KEY_APOSTROPHE,40,ANSI_Quote,0x27,0x28,0x52,0x52,52,VK_OEM_7,0xde,40,40,XK_apostrophe,0x0027,Quote,AC11,apostrophe,0x57,0x27 +KEY_APOSTROPHE,40,ANSI_Quote,0x27,0x28,0x52,0x52,52,VK_OEM_7,0xde,40,40,XK_quotedbl,0x0022,Quote,AC11,apostrophe,0x57,0x27 +KEY_GRAVE,41,ANSI_Grave,0x32,0x29,0x0e,0x0e,53,VK_OEM_3,0xc0,41,41,XK_grave,0x0060,Backquote,TLDE,grave_accent,0x2a,0x32 +KEY_GRAVE,41,ANSI_Grave,0x32,0x29,0x0e,0x0e,53,VK_OEM_3,0xc0,41,41,XK_grave,0x0060,Backquote,AB00,grave_accent,0x2a,0x32 +KEY_GRAVE,41,ANSI_Grave,0x32,0x29,0x0e,0x0e,53,VK_OEM_3,0xc0,41,41,XK_asciitilde,0x007e,Backquote,TLDE,grave_accent,0x2a,0x32 +KEY_GRAVE,41,ANSI_Grave,0x32,0x29,0x0e,0x0e,53,VK_OEM_3,0xc0,41,41,XK_asciitilde,0x007e,Backquote,AB00,grave_accent,0x2a,0x32 +KEY_SHIFT,42,Shift,0x38,0x2a,0x12,0x12,225,VK_SHIFT,0x10,42,42,XK_Shift_L,0xffe1,ShiftLeft,LFSH,shift,0x63,0x38 +KEY_LEFTSHIFT,42,Shift,0x38,0x2a,0x12,0x12,225,VK_LSHIFT,0xa0,42,42,XK_Shift_L,0xffe1,ShiftLeft,LFSH,shift,0x63,0x38 +KEY_BACKSLASH,43,ANSI_Backslash,0x2a,0x2b,0x5d,0x5c,49,VK_OEM_5,0xdc,43,43,XK_backslash,0x005c,Backslash,BKSL,backslash,0x58,0x2a +KEY_BACKSLASH,43,ANSI_Backslash,0x2a,0x2b,0x5d,0x5c,49,VK_OEM_5,0xdc,43,43,XK_backslash,0x005c,Backslash,AC12,backslash,0x58,0x2a +KEY_BACKSLASH,43,ANSI_Backslash,0x2a,0x2b,0x5d,0x5c,49,VK_OEM_5,0xdc,43,43,XK_bar,0x007c,Backslash,BKSL,backslash,0x58,0x2a +KEY_BACKSLASH,43,ANSI_Backslash,0x2a,0x2b,0x5d,0x5c,49,VK_OEM_5,0xdc,43,43,XK_bar,0x007c,Backslash,AC12,backslash,0x58,0x2a +KEY_BACKSLASH,43,ANSI_Backslash,0x2a,0x2b,0x5d,0x5c,50,VK_OEM_5,0xdc,43,43,XK_backslash,0x005c,Backslash,BKSL,backslash,0x58,0x2a +KEY_BACKSLASH,43,ANSI_Backslash,0x2a,0x2b,0x5d,0x5c,50,VK_OEM_5,0xdc,43,43,XK_backslash,0x005c,Backslash,AC12,backslash,0x58,0x2a +KEY_BACKSLASH,43,ANSI_Backslash,0x2a,0x2b,0x5d,0x5c,50,VK_OEM_5,0xdc,43,43,XK_bar,0x007c,Backslash,BKSL,backslash,0x58,0x2a +KEY_BACKSLASH,43,ANSI_Backslash,0x2a,0x2b,0x5d,0x5c,50,VK_OEM_5,0xdc,43,43,XK_bar,0x007c,Backslash,AC12,backslash,0x58,0x2a +KEY_Z,44,ANSI_Z,0x6,0x2c,0x1a,0x1a,29,VK_Z,0x5a,44,44,XK_Z,0x005a,KeyZ,AB01,z,0x64,0x6 +KEY_Z,44,ANSI_Z,0x6,0x2c,0x1a,0x1a,29,VK_Z,0x5a,44,44,XK_z,0x007a,KeyZ,AB01,z,0x64,0x6 +KEY_X,45,ANSI_X,0x7,0x2d,0x22,0x22,27,VK_X,0x58,45,45,XK_X,0x0058,KeyX,AB02,x,0x65,0x7 +KEY_X,45,ANSI_X,0x7,0x2d,0x22,0x22,27,VK_X,0x58,45,45,XK_x,0x0078,KeyX,AB02,x,0x65,0x7 +KEY_C,46,ANSI_C,0x8,0x2e,0x21,0x21,6,VK_C,0x43,46,46,XK_C,0x0043,KeyC,AB03,c,0x66,0x8 +KEY_C,46,ANSI_C,0x8,0x2e,0x21,0x21,6,VK_C,0x43,46,46,XK_c,0x0063,KeyC,AB03,c,0x66,0x8 +KEY_V,47,ANSI_V,0x9,0x2f,0x2a,0x2a,25,VK_V,0x56,47,47,XK_V,0x0056,KeyV,AB04,v,0x67,0x9 +KEY_V,47,ANSI_V,0x9,0x2f,0x2a,0x2a,25,VK_V,0x56,47,47,XK_v,0x0076,KeyV,AB04,v,0x67,0x9 +KEY_B,48,ANSI_B,0xb,0x30,0x32,0x32,5,VK_B,0x42,48,48,XK_B,0x0042,KeyB,AB05,b,0x68,0xb +KEY_B,48,ANSI_B,0xb,0x30,0x32,0x32,5,VK_B,0x42,48,48,XK_b,0x0062,KeyB,AB05,b,0x68,0xb +KEY_N,49,ANSI_N,0x2d,0x31,0x31,0x31,17,VK_N,0x4e,49,49,XK_N,0x004e,KeyN,AB06,n,0x69,0x2d +KEY_N,49,ANSI_N,0x2d,0x31,0x31,0x31,17,VK_N,0x4e,49,49,XK_n,0x006e,KeyN,AB06,n,0x69,0x2d +KEY_M,50,ANSI_M,0x2e,0x32,0x3a,0x3a,16,VK_M,0x4d,50,50,XK_M,0x004d,KeyM,AB07,m,0x6a,0x2e +KEY_M,50,ANSI_M,0x2e,0x32,0x3a,0x3a,16,VK_M,0x4d,50,50,XK_m,0x006d,KeyM,AB07,m,0x6a,0x2e +KEY_COMMA,51,ANSI_Comma,0x2b,0x33,0x41,0x41,54,VK_OEM_COMMA,0xbc,51,51,XK_comma,0x002c,Comma,AB08,comma,0x6b,0x2b +KEY_COMMA,51,ANSI_Comma,0x2b,0x33,0x41,0x41,54,VK_OEM_COMMA,0xbc,51,51,XK_less,0x003c,Comma,AB08,comma,0x6b,0x2b +KEY_DOT,52,ANSI_Period,0x2f,0x34,0x49,0x49,55,VK_OEM_PERIOD,0xbe,52,52,XK_period,0x002e,Period,AB09,dot,0x6c,0x2f +KEY_DOT,52,ANSI_Period,0x2f,0x34,0x49,0x49,55,VK_OEM_PERIOD,0xbe,52,52,XK_greater,0x003e,Period,AB09,dot,0x6c,0x2f +KEY_SLASH,53,ANSI_Slash,0x2c,0x35,0x4a,0x4a,56,VK_OEM_2,0xbf,53,53,XK_slash,0x002f,Slash,AB10,slash,0x6d,0x2c +KEY_SLASH,53,ANSI_Slash,0x2c,0x35,0x4a,0x4a,56,VK_OEM_2,0xbf,53,53,XK_question,0x003f,Slash,AB10,slash,0x6d,0x2c +KEY_RIGHTSHIFT,54,RightShift,0x3c,0x36,0x59,0x59,229,VK_RSHIFT,0xa1,54,54,XK_Shift_R,0xffe2,ShiftRight,RTSH,shift_r,0x6e,0x7b +KEY_KPASTERISK,55,ANSI_KeypadMultiply,0x43,0x37,0x7c,0x7e,85,VK_MULTIPLY,0x6a,55,55,XK_multiply,0x00d7,NumpadMultiply,KPMU,asterisk,0x2f,0x43 +KEY_KPASTERISK,55,ANSI_KeypadMultiply,0x43,0x37,0x7c,0x7e,85,VK_MULTIPLY,0x6a,55,55,XK_multiply,0x00d7,NumpadMultiply,KPMU,kp_multiply,0x2f,0x43 +KEY_LEFTALT,56,Option,0x3a,0x38,0x11,0x19,226,VK_LMENU,0xa4,56,56,XK_Alt_L,0xffe9,AltLeft,LALT,alt,0x13,0x3a +KEY_LEFTALT,56,Option,0x3a,0x38,0x11,0x19,226,VK_MENU,0x12,56,56,XK_Alt_L,0xffe9,AltLeft,LALT,alt,0x13,0x3a +KEY_SPACE,57,Space,0x31,0x39,0x29,0x29,44,VK_SPACE,0x20,57,57,XK_space,0x0020,Space,SPCE,spc,0x79,0x31 +KEY_CAPSLOCK,58,CapsLock,0x39,0x3a,0x58,0x14,57,VK_CAPITAL,0x14,58,58,XK_Caps_Lock,0xffe5,CapsLock,CAPS,caps_lock,0x77,0x39 +KEY_F1,59,F1,0x7a,0x3b,0x05,0x07,58,VK_F1,0x70,59,59,XK_F1,0xffbe,F1,FK01,f1,0x05,0x7a +KEY_F2,60,F2,0x78,0x3c,0x06,0x0f,59,VK_F2,0x71,60,60,XK_F2,0xffbf,F2,FK02,f2,0x06,0x78 +KEY_F3,61,F3,0x63,0x3d,0x04,0x17,60,VK_F3,0x72,61,61,XK_F3,0xffc0,F3,FK03,f3,0x08,0x63 +KEY_F4,62,F4,0x76,0x3e,0x0c,0x1f,61,VK_F4,0x73,62,62,XK_F4,0xffc1,F4,FK04,f4,0x0a,0x76 +KEY_F5,63,F5,0x60,0x3f,0x03,0x27,62,VK_F5,0x74,63,63,XK_F5,0xffc2,F5,FK05,f5,0x0c,0x60 +KEY_F6,64,F6,0x61,0x40,0x0b,0x2f,63,VK_F6,0x75,64,64,XK_F6,0xffc3,F6,FK06,f6,0x0e,0x61 +KEY_F7,65,F7,0x62,0x41,0x83,0x37,64,VK_F7,0x76,65,65,XK_F7,0xffc4,F7,FK07,f7,0x10,0x62 +KEY_F8,66,F8,0x64,0x42,0x0a,0x3f,65,VK_F8,0x77,66,66,XK_F8,0xffc5,F8,FK08,f8,0x11,0x64 +KEY_F9,67,F9,0x65,0x43,0x01,0x47,66,VK_F9,0x78,67,67,XK_F9,0xffc6,F9,FK09,f9,0x12,0x65 +KEY_F10,68,F10,0x6d,0x44,0x09,0x4f,67,VK_F10,0x79,68,68,XK_F10,0xffc7,F10,FK10,f10,0x07,0x6d +KEY_NUMLOCK,69,ANSI_KeypadClear,0x47,0x45,0x77,0x76,83,VK_NUMLOCK,0x90,69,69,XK_Num_Lock,0xff7f,NumLock,NMLK,num_lock,0x62,0x47 +KEY_SCROLLLOCK,70,,,0x46,0x7e,0x5f,71,VK_SCROLL,0x91,70,70,XK_Scroll_Lock,0xff14,ScrollLock,SCLK,scroll_lock,0x17,0x6b +KEY_KP7,71,ANSI_Keypad7,0x59,0x47,0x6c,0x6c,95,VK_NUMPAD7,0x67,71,71,XK_KP_7,0xffb7,Numpad7,KP7,kp_7,0x44,0x59 +KEY_KP8,72,ANSI_Keypad8,0x5b,0x48,0x75,0x75,96,VK_NUMPAD8,0x68,72,72,XK_KP_8,0xffb8,Numpad8,KP8,kp_8,0x45,0x5b +KEY_KP9,73,ANSI_Keypad9,0x5c,0x49,0x7d,0x7d,97,VK_NUMPAD9,0x69,73,73,XK_KP_9,0xffb9,Numpad9,KP9,kp_9,0x46,0x5c +KEY_KPMINUS,74,ANSI_KeypadMinus,0x4e,0x4a,0x7b,0x4e,86,VK_SUBTRACT,0x6d,74,74,XK_KP_Subtract,0xffad,NumpadSubtract,KPSU,kp_subtract,0x47,0x4e +KEY_KP4,75,ANSI_Keypad4,0x56,0x4b,0x6b,0x6b,92,VK_NUMPAD4,0x64,75,75,XK_KP_4,0xffb4,Numpad4,KP4,kp_4,0x5b,0x56 +KEY_KP5,76,ANSI_Keypad5,0x57,0x4c,0x73,0x73,93,VK_NUMPAD5,0x65,76,76,XK_KP_5,0xffb5,Numpad5,KP5,kp_5,0x5c,0x57 +KEY_KP6,77,ANSI_Keypad6,0x58,0x4d,0x74,0x74,94,VK_NUMPAD6,0x66,77,77,XK_KP_6,0xffb6,Numpad6,KP6,kp_6,0x5d,0x58 +KEY_KPPLUS,78,ANSI_KeypadPlus,0x45,0x4e,0x79,0x7c,87,VK_ADD,0x6b,78,78,XK_KP_Add,0xffab,NumpadAdd,KPAD,kp_add,0x7d,0x45 +KEY_KP1,79,ANSI_Keypad1,0x53,0x4f,0x69,0x69,89,VK_NUMPAD1,0x61,79,79,XK_KP_1,0xffb1,Numpad1,KP1,kp_1,0x70,0x53 +KEY_KP2,80,ANSI_Keypad2,0x54,0x50,0x72,0x72,90,VK_NUMPAD2,0x62,80,80,XK_KP_2,0xffb2,Numpad2,KP2,kp_2,0x71,0x54 +KEY_KP3,81,ANSI_Keypad3,0x55,0x51,0x7a,0x7a,91,VK_NUMPAD3,0x63,81,81,XK_KP_3,0xffb3,Numpad3,KP3,kp_3,0x72,0x55 +KEY_KP0,82,ANSI_Keypad0,0x52,0x52,0x70,0x70,98,VK_NUMPAD0,0x60,82,82,XK_KP_0,0xffb0,Numpad0,KP0,kp_0,0x5e,0x52 +KEY_KPDOT,83,ANSI_KeypadDecimal,0x41,0x53,0x71,0x71,99,VK_DECIMAL,0x6e,83,83,XK_KP_Decimal,0xffae,NumpadDecimal,KPDL,kp_decimal,0x32,0x41 +KEY_KPDOT,83,ANSI_KeypadDecimal,0x41,0x53,0x71,0x71,99,VK_DECIMAL,0x6e,83,83,XK_KP_Decimal,0xffae,NumpadDecimal,KPDC,kp_decimal,0x32,0x41 +,84,,,0x54,,,,,,,,,,,,,, +KEY_ZENKAKUHANKAKU,85,,,0x76,0x5f,,148,,,,,,,Lang5,HZTG,,, +KEY_102ND,86,,,0x56,0x61,0x13,100,VK_OEM_102,0xe2,86,86,,,IntlBackslash,LSGT,less,0x7c, +KEY_F11,87,F11,0x67,0x57,0x78,0x56,68,VK_F11,0x7a,87,87,XK_F11,0xffc8,F11,FK11,f11,0x09,0x67 +KEY_F12,88,F12,0x6f,0x58,0x07,0x5e,69,VK_F12,0x7b,88,88,XK_F12,0xffc9,F12,FK12,f12,0x0b,0x6f +KEY_RO,89,JIS_Underscore,0x5e,0x73,0x51,,135,,,,,,,IntlRo,AB11,ro,, +KEY_KATAKANA,90,,,0x78,0x63,,146,VK_KANA,0x15,,,,,Katakana,KATA,,, +KEY_KATAKANA,90,,,0x78,0x63,,146,VK_KANA,0x15,,,,,Lang3,KATA,,, +KEY_HIRAGANA,91,,,0x77,0x62,0x87,147,,,,,,,Hiragana,HIRA,hiragana,, +KEY_HIRAGANA,91,,,0x77,0x62,0x87,147,,,,,,,Lang4,HIRA,hiragana,, +KEY_HENKAN,92,,,0x79,0x64,0x86,138,,,,,,,Convert,HENK,henkan,, +KEY_KATAKANAHIRAGANA,93,,,0x70,0x13,0x87,136,,,0xc8,0xc8,,,KanaMode,HKTG,katakanahiragana,, +KEY_MUHENKAN,94,,,0x7b,0x67,0x85,139,,,,,,,NonConvert,NFER,muhenkan,, +KEY_MUHENKAN,94,,,0x7b,0x67,0x85,139,,,,,,,NonConvert,MUHE,muhenkan,, +KEY_KPJPCOMMA,95,JIS_KeypadComma,0x5f,0x5c,0x27,,140,,,,,XK_KP_Separator,0xffac,,KPSP,,, +KEY_KPJPCOMMA,95,JIS_KeypadComma,0x5f,0x5c,0x27,,140,,,,,XK_KP_Separator,0xffac,,JPCM,,, +KEY_KPENTER,96,ANSI_KeypadEnter,0x4c,0xe01c,0xe05a,0x79,88,,,0x64,0x64,XK_KP_Enter,0xff8d,NumpadEnter,KPEN,kp_enter,0x5a,0x4c +KEY_RIGHTCTRL,97,RightControl,0x3e,0xe01d,0xe014,0x58,228,VK_RCONTROL,0xa3,0x65,0x65,XK_Control_R,0xffe4,ControlRight,RCTL,ctrl_r,0x4c,0x7d +KEY_KPSLASH,98,ANSI_KeypadDivide,0x4b,0xe035,0xe04a,0x4a,84,VK_DIVIDE,0x6f,0x68,0x68,XK_KP_Divide,0xffaf,NumpadDivide,KPDV,kp_divide,0x2e,0x4b +KEY_SYSRQ,99,,,0x54,0x7f,0x57,70,VK_SNAPSHOT,0x2c,0x67,0x67,XK_Sys_Req,0xff15,PrintScreen,PRSC,print,0x16,0x69 +KEY_SYSRQ,99,,,0x54,0x7f,0x57,70,VK_SNAPSHOT,0x2c,0x67,0x67,XK_Sys_Req,0xff15,PrintScreen,SYRQ,sysrq,0x16,0x69 +KEY_RIGHTALT,100,RightOption,0x3d,0xe038,0xe011,0x39,230,VK_RMENU,0xa5,0x69,0x69,XK_Alt_R,0xffea,AltRight,ALGR,alt_r,0x0d,0x7c +KEY_RIGHTALT,100,RightOption,0x3d,0xe038,0xe011,0x39,230,VK_RMENU,0xa5,0x69,0x69,XK_Alt_R,0xffea,AltRight,RALT,alt_r,0x0d,0x7c +KEY_LINEFEED,101,,,0x5b,,,,,,,,,,,LNFD,lf,0x6f, +KEY_HOME,102,Home,0x73,0xe047,0xe06c,0x6e,74,VK_HOME,0x24,0x59,0x59,XK_Home,0xff50,Home,HOME,home,0x34,0x73 +KEY_UP,103,UpArrow,0x7e,0xe048,0xe075,0x63,82,VK_UP,0x26,0x5a,0x5a,XK_Up,0xff52,ArrowUp,UP,up,0x14,0x3e +KEY_PAGEUP,104,PageUp,0x74,0xe049,0xe07d,0x6f,75,VK_PRIOR,0x21,0x5b,0x5b,XK_Page_Up,0xff55,PageUp,PGUP,pgup,0x60,0x74 +KEY_LEFT,105,LeftArrow,0x7b,0xe04b,0xe06b,0x61,80,VK_LEFT,0x25,0x5c,0x5c,XK_Left,0xff51,ArrowLeft,LEFT,left,0x18,0x3b +KEY_RIGHT,106,RightArrow,0x7c,0xe04d,0xe074,0x6a,79,VK_RIGHT,0x27,0x5e,0x5e,XK_Right,0xff53,ArrowRight,RGHT,right,0x1c,0x3c +KEY_END,107,End,0x77,0xe04f,0xe069,0x65,77,VK_END,0x23,0x5f,0x5f,XK_End,0xff57,End,END,end,0x4a,0x77 +KEY_DOWN,108,DownArrow,0x7d,0xe050,0xe072,0x60,81,VK_DOWN,0x28,0x60,0x60,XK_Down,0xff54,ArrowDown,DOWN,down,0x1b,0x3d +KEY_PAGEDOWN,109,PageDown,0x79,0xe051,0xe07a,0x6d,78,VK_NEXT,0x22,0x61,0x61,XK_Page_Down,0xff56,PageDown,PGDN,pgdn,0x7b,0x79 +KEY_INSERT,110,,,0xe052,0xe070,0x67,73,VK_INSERT,0x2d,0x62,0x62,XK_Insert,0xff63,Insert,INS,insert,0x2c,0x72 +KEY_DELETE,111,ForwardDelete,0x75,0xe053,0xe071,0x64,76,VK_DELETE,0x2e,0x63,0x63,XK_Delete,0xffff,Delete,DEL,delete,0x42,0x75 +KEY_DELETE,111,ForwardDelete,0x75,0xe053,0xe071,0x64,76,VK_DELETE,0x2e,0x63,0x63,XK_Delete,0xffff,Delete,DELE,,0x42,0x75 +KEY_MACRO,112,,,0xe06f,0xe06f,0x8e,,,,,,,,,I120,,, +KEY_MUTE,113,Mute,0x4a,0xe020,0xe023,0x9c,127,VK_VOLUME_MUTE,0xad,,,,,AudioVolumeMute,MUTE,audiomute,, +KEY_MUTE,113,Mute,0x4a,0xe020,0xe023,0x9c,239,VK_VOLUME_MUTE,0xad,,,,,AudioVolumeMute,MUTE,audiomute,, +KEY_VOLUMEDOWN,114,VolumeDown,0x49,0xe02e,0xe021,0x9d,129,VK_VOLUME_DOWN,0xae,,,,,AudioVolumeDown,VOL-,volumedown,, +KEY_VOLUMEDOWN,114,VolumeDown,0x49,0xe02e,0xe021,0x9d,238,VK_VOLUME_DOWN,0xae,,,,,AudioVolumeDown,VOL-,volumedown,, +KEY_VOLUMEUP,115,VolumeUp,0x48,0xe030,0xe032,0x95,128,VK_VOLUME_UP,0xaf,,,,,AudioVolumeUp,VOL+,volumeup,, +KEY_VOLUMEUP,115,VolumeUp,0x48,0xe030,0xe032,0x95,237,VK_VOLUME_UP,0xaf,,,,,AudioVolumeUp,VOL+,volumeup,, +KEY_POWER,116,,,0xe05e,0xe037,,102,,,,,,,Power,POWR,power,,0x7f7f +KEY_KPEQUAL,117,ANSI_KeypadEquals,0x51,0x59,0x0f,,103,,,0x76,0x76,XK_KP_Equal,0xffbd,NumpadEqual,KPEQ,kp_equals,0x2d,0x51 +KEY_KPPLUSMINUS,118,,,0xe04e,0xe079,,,,,,,,,,I126,,, +KEY_PAUSE,119,,,0xe046,0xe077,0x62,72,VK_PAUSE,0x013,0x66,0x66,XK_Pause,0xff13,Pause,PAUS,pause,0x15,0x71 +KEY_SCALE,120,,,0xe00b,,,,,,,,,,,I128,,, +KEY_KPCOMMA,121,,,0x7e,0x6d,,133,VK_SEPARATOR??,0x6c,,,,,NumpadComma,KPCO,kp_comma,, +KEY_KPCOMMA,121,,,0x7e,0x6d,,133,VK_SEPARATOR??,0x6c,,,,,NumpadComma,I129,,, +KEY_HANGEUL,122,JIS_Kana,0x68,0x72,,,144,VK_HANGEUL,0x15,,0x71,,,Lang1,HNGL,lang1,, +KEY_HANJA,123,JIS_Eisu,0x66,0x71,,,145,VK_HANJA,0x19,,0x72,,,Lang2,HJCV,lang2,, +KEY_YEN,124,JIS_Yen,0x5d,0x7d,0x6a,0x5d,137,,,0x7d,0x7d,,,IntlYen,AE13,yen,, +KEY_LEFTMETA,125,Command,0x37,0xe05b,0xe01f,0x8b,227,VK_LWIN,0x5b,0x6b,0x6b,XK_Meta_L,0xffe7,MetaLeft,LMTA,meta_l,0x78,0x37 +KEY_LEFTMETA,125,Command,0x37,0xe05b,0xe01f,0x8b,227,VK_LWIN,0x5b,0x6b,0x6b,XK_Meta_L,0xffe7,MetaLeft,LWIN,meta_l,0x78,0x37 +KEY_RIGHTMETA,126,RightCommand,0x36,0xe05c,0xe027,0x8c,231,VK_RWIN,0x5c,0x6c,0x6c,XK_Meta_R,0xffe8,MetaRight,RMTA,meta_r,0x7a,0x37 +KEY_RIGHTMETA,126,RightCommand,0x36,0xe05c,0xe027,0x8c,231,VK_RWIN,0x5c,0x6c,0x6c,XK_Meta_R,0xffe8,MetaRight,RWIN,meta_r,0x7a,0x37 +KEY_COMPOSE,127,,0x6e,0xe05d,0xe02f,0x8d,101,VK_APPS,0x5d,0x6d,0x6d,,,ContextMenu,MENU,compose,0x43, +KEY_COMPOSE,127,,0x6e,0xe05d,0xe02f,0x8d,101,VK_APPS,0x5d,0x6d,0x6d,,,ContextMenu,COMP,compose,0x43, +KEY_STOP,128,,,0xe068,0xe028,0x0a,120,VK_BROWSER_STOP,0xa9,,,,,BrowserStop,STOP,stop,0x01, +KEY_STOP,128,,,0xe068,0xe028,0x0a,243,VK_BROWSER_STOP,0xa9,,,,,BrowserStop,STOP,stop,0x01, +KEY_AGAIN,129,,,0xe005,,0x0b,121,,,,,,,Again,AGAI,again,0x03, +KEY_PROPS,130,,,0xe006,,0x0c,,,,,,,,Props,PROP,props,0x19, +KEY_UNDO,131,,,0xe007,,0x10,122,,,,,,,Undo,UNDO,undo,0x1a, +KEY_FRONT,132,,,0xe00c,,,119,,,,,,,,FRNT,front,0x31, +KEY_COPY,133,,,0xe078,,0x18,124,,,,,,,Copy,COPY,copy,0x33, +KEY_OPEN,134,,,0x64,,0x20,116,,,,,,,Open,OPEN,open,0x48, +KEY_PASTE,135,,,0x65,,0x28,125,,,,,,,Paste,PAST,paste,0x49, +KEY_FIND,136,,,0xe041,,0x30,126,,,,,,,Find,FIND,find,0x5f, +KEY_FIND,136,,,0xe041,,0x30,244,,,,,,,Find,FIND,find,0x5f, +KEY_CUT,137,,,0xe03c,,0x38,123,,,,,,,Cut,CUT,cut,0x61, +KEY_HELP,138,Help,0x72,0xe075,,0x09,117,VK_HELP,0x2f,,,XK_Help,0xff6a,Help,HELP,help,0x76, +KEY_MENU,139,,,0xe01e,,0x91,118,,,,,,,,I147,menu,, +KEY_CALC,140,,,0xe021,0xe02b,0xa3,251,,,,,,,LaunchApp2,I148,calculator,, +KEY_SETUP,141,,,0x66,,,,,,,,,,,I149,,, +KEY_SLEEP,142,,,0xe05f,0xe03f,,248,VK_SLEEP,0x5f,,,,,Sleep,I150,sleep,, +KEY_WAKEUP,143,,,0xe063,0xe05e,,,,,,,,,WakeUp,I151,wake,, +KEY_FILE,144,,,0x67,,,,,,,,,,,I152,,, +KEY_SENDFILE,145,,,0x68,,,,,,,,,,,I153,,, +KEY_DELETEFILE,146,,,0x69,,,,,,,,,,,I154,,, +KEY_XFER,147,,,0xe013,,0xa2,,,,,,,,,XFER,,, +KEY_XFER,147,,,0xe013,,0xa2,,,,,,,,,I155,,, +KEY_PROG1,148,,,0xe01f,,0xa0,,,,,,,,,I156,,, +KEY_PROG2,149,,,0xe017,,0xa1,,,,,,,,,I157,,, +KEY_WWW,150,,,0xe002,,,240,,,,,,,,I158,,, +KEY_MSDOS,151,,,0x6a,,,,,,,,,,,I159,,, +KEY_SCREENLOCK,152,,,0xe012,,0x96,249,,,,,,,,I160,,, +KEY_DIRECTION,153,,,0x6b,,,,,,,,,,,I161,,, +KEY_CYCLEWINDOWS,154,,,0xe026,,0x9b,,,,,,,,,I162,,, +KEY_MAIL,155,,,0xe06c,0xe048,,,,,,,,,LaunchMail,I163,mail,, +KEY_BOOKMARKS,156,,,0xe066,0xe018,,,,,,,,,BrowserFavorites,I164,ac_bookmarks,, +KEY_COMPUTER,157,,,0xe06b,0xe040,,,,,,,,,LaunchApp1,I165,computer,, +KEY_BACK,158,,,0xe06a,0xe038,,241,VK_BROWSER_BACK,0xa6,,,,,BrowserBack,I166,ac_back,, +KEY_FORWARD,159,,,0xe069,0xe030,,242,VK_BROWSER_FORWARD,0xa7,,,,,BrowserForward,I167,ac_forward,, +KEY_CLOSECD,160,,,0xe023,,0x9a,,,,,,,,,I168,,, +KEY_EJECTCD,161,,,0x6c,,,236,,,,,,,,I169,,, +KEY_EJECTCLOSECD,162,,,0xe07d,,,,,,,,,,Eject,I170,,, +KEY_NEXTSONG,163,,,0xe019,0xe04d,0x93,235,VK_MEDIA_NEXT_TRACK,0xb0,,,,,MediaTrackNext,I171,audionext,, +KEY_PLAYPAUSE,164,,,0xe022,0xe034,,232,VK_MEDIA_PLAY_PAUSE,0xb3,,,,,MediaPlayPause,I172,audioplay,, +KEY_PREVIOUSSONG,165,,,0xe010,0xe015,0x94,234,VK_MEDIA_PREV_TRACK,0xb1,,,,,MediaTrackPrevious,I173,audioprev,, +KEY_STOPCD,166,,,0xe024,0xe03b,0x98,233,VK_MEDIA_STOP,0xb2,,,,,MediaStop,I174,audiostop,, +KEY_RECORD,167,,,0xe031,,0x9e,,,,,,,,,I175,,, +KEY_REWIND,168,,,0xe018,,0x9f,,,,,,,,,I176,,, +KEY_PHONE,169,,,0x63,,,,,,,,,,,I177,,, +KEY_ISO,170,ISO_Section,0xa,,,,,,,,,,,,I178,,, +KEY_CONFIG,171,,,0xe001,,,,,,,,,,,I179,,, +KEY_HOMEPAGE,172,,,0xe032,0xe03a,0x97,,VK_BROWSER_HOME,0xac,,,,,BrowserHome,I180,ac_home,, +KEY_REFRESH,173,,,0xe067,0xe020,,250,VK_BROWSER_REFRESH,0xa8,,,,,BrowserRefresh,I181,ac_refresh,, +KEY_EXIT,174,,,,,,,,,,,,,,I182,,, +KEY_MOVE,175,,,,,,,,,,,,,,I183,,, +KEY_EDIT,176,,,0xe008,,,247,,,,,,,,I184,,, +KEY_SCROLLUP,177,,,0x75,,,245,,,,,,,,I185,,, +KEY_SCROLLDOWN,178,,,0xe00f,,,246,,,,,,,,I186,,, +KEY_KPLEFTPAREN,179,,,0xe076,,,182,,,,,,,NumpadParenLeft,I187,,, +KEY_KPRIGHTPAREN,180,,,0xe07b,,,183,,,,,,,NumpadParenRight,I188,,, +KEY_NEW,181,,,0xe009,,,,,,,,,,,I189,,, +KEY_REDO,182,,,0xe00a,,,,,,,,,,,I190,,, +KEY_F13,183,F13,0x69,0x5d,0x2f,0x7f,104,VK_F13,0x7c,0x6e,0x6e,,,F13,FK13,,,0x69 +KEY_F14,184,F14,0x6b,0x5e,0x37,0x80,105,VK_F14,0x7d,0x6f,0x6f,,,F14,FK14,,,0x6b +KEY_F15,185,F15,0x71,0x5f,0x3f,0x81,106,VK_F15,0x7e,0x70,0x70,,,F15,FK15,,,0x71 +KEY_F16,186,F16,0x6a,0x55,,0x82,107,VK_F16,0x7f,0x71,0x71,,,F16,FK16,,, +KEY_F17,187,F17,0x40,0xe003,,0x83,108,VK_F17,0x80,0x72,0x72,,,F17,FK17,,, +KEY_F18,188,F18,0x4f,0xe077,,,109,VK_F18,0x81,,,,,F18,FK18,,, +KEY_F19,189,F19,0x50,0xe004,,,110,VK_F19,0x82,,,,,F19,FK19,,, +KEY_F20,190,F20,0x5a,0x5a,,,111,VK_F20,0x83,,,,,F20,FK20,,, +KEY_F21,191,,,0x74,,,112,VK_F21,0x84,,,,,F21,FK21,,, +KEY_F22,192,,,0xe079,,,113,VK_F22,0x85,,,,,F22,FK22,,, +KEY_F23,193,,,0x6d,,,114,VK_F23,0x86,,,,,F23,FK23,,, +KEY_F24,194,,,0x6f,,,115,VK_F24,0x87,,,,,F24,FK24,,, +,195,,,0xe015,,,,,,,,,,,,,, +,196,,,0xe016,,,,,,,,,,,,,, +,197,,,0xe01a,,,,,,,,,,,,,, +,198,,,0xe01b,,,,,,,,,,,,,, +,199,,,0xe027,,,,,,,,,,,,,, +KEY_PLAYCD,200,,,0xe028,,,,,,,,,,,I208,,, +KEY_PAUSECD,201,,,0xe029,,,,,,,,,,,I209,,, +KEY_PROG3,202,,,0xe02b,,,,,,,,,,,I210,,, +KEY_PROG4,203,,,0xe02c,,,,,,,,,,,I211,,, +KEY_DASHBOARD,204,,,0xe02d,,,,,,,,,,,I212,,, +KEY_SUSPEND,205,,,0xe025,,,,,,,,,,Suspend,I213,,, +KEY_CLOSE,206,,,0xe02f,,,,,,,,,,,I214,,, +KEY_PLAY,207,,,0xe033,,,,VK_PLAY,0xfa,,,,,,I215,,, +KEY_FASTFORWARD,208,,,0xe034,,,,,,,,,,,I216,,, +KEY_BASSBOOST,209,,,0xe036,,,,,,,,,,,I217,,, +KEY_PRINT,210,,,0xe039,,,,VK_PRINT,0x2a,,,,,,I218,,, +KEY_HP,211,,,0xe03a,,,,,,,,,,,I219,,, +KEY_CAMERA,212,,,0xe03b,,,,,,,,,,,I220,,, +KEY_SOUND,213,,,0xe03d,,,,,,,,,,,I221,,, +KEY_QUESTION,214,,,0xe03e,,,,,,,,,,,I222,,, +KEY_EMAIL,215,,,0xe03f,,,,VK_LAUNCH_MAIL,0xb4,,,,,,I223,,, +KEY_CHAT,216,,,0xe040,,,,,,,,,,,I224,,, +KEY_SEARCH,217,,,0xe065,0xe010,,,VK_BROWSER_SEARCH,0xaa,,,,,BrowserSearch,I225,,, +KEY_CONNECT,218,,,0xe042,,,,,,,,,,,I226,,, +KEY_FINANCE,219,,,0xe043,,,,,,,,,,,I227,,, +KEY_SPORT,220,,,0xe044,,,,,,,,,,,I228,,, +KEY_SHOP,221,,,0xe045,,,,,,,,,,,I229,,, +KEY_ALTERASE,222,,,0xe014,,,,,,,,,,,I230,,, +KEY_CANCEL,223,,,0xe04a,,,,,,,,,,,I231,,, +KEY_BRIGHTNESSDOWN,224,,,0xe04c,,,,,,,,,,,I232,,, +KEY_BRIGHTNESSUP,225,,,0xe054,,,,,,,,,,,I233,,, +KEY_MEDIA,226,,,0xe06d,0xe050,,,,,,,,,MediaSelect,I234,mediaselect,, +KEY_SWITCHVIDEOMODE,227,,,0xe056,,,,,,,,,,,I235,,, +KEY_KBDILLUMTOGGLE,228,,,0xe057,,,,,,,,,,,I236,,, +KEY_KBDILLUMDOWN,229,,,0xe058,,,,,,,,,,,I237,,, +KEY_KBDILLUMUP,230,,,0xe059,,,,,,,,,,,I238,,, +KEY_SEND,231,,,0xe05a,,,,,,,,,,,I239,,, +KEY_REPLY,232,,,0xe064,,,,,,,,,,,I240,,, +KEY_FORWARDMAIL,233,,,0xe00e,,,,,,,,,,,I241,,, +KEY_SAVE,234,,,0xe055,,,,,,,,,,,I242,,, +KEY_DOCUMENTS,235,,,0xe070,,,,,,,,,,,I243,,, +KEY_BATTERY,236,,,0xe071,,,,,,,,,,,I244,,, +KEY_BLUETOOTH,237,,,0xe072,,,,,,,,,,,I245,,, +KEY_WLAN,238,,,0xe073,,,,,,,,,,,I246,,, +KEY_UWB,239,,,0xe074,,,,,,,,,,,I247,,, +KEY_UNKNOWN,240,,,,,,,,,,,,,,I248,,, +KEY_VIDEO_NEXT,241,,,,,,,,,,,,,,I249,,, +KEY_VIDEO_PREV,242,,,,,,,,,,,,,,I250,,, +KEY_BRIGHTNESS_CYCLE,243,,,,,,,,,,,,,,I251,,, +KEY_BRIGHTNESS_ZERO,244,,,,,,,,,,,,,,I252,,, +KEY_DISPLAY_OFF,245,,,,,,,,,,,,,,I253,,, +KEY_WIMAX,246,,,,,,,,,,,,,,,,, +,247,,,,,,,,,,,,,,,,, +,248,,,,,,,,,,,,,,,,, +,249,,,,,,,,,,,,,,,,, +,250,,,,,,,,,,,,,,,,, +,251,,,,,,,,,,,,,,,,, +,252,,,,,,,,,,,,,,,,, +,253,,,,,,,,,,,,,,,,, +,254,,,,,,,,,,,,,,,,, +,255,,,,0xe012,,,,,,,,,,,,, +BTN_MISC,0x100,,,,,,,,,,,,,,,,, +BTN_0,0x100,,,,,,,VK_LBUTTON,0x01,,,,,,,,, +BTN_1,0x101,,,,,,,VK_RBUTTON,0x02,,,,,,,,, +BTN_2,0x102,,,,,,,VK_MBUTTON,0x04,,,,,,,,, +BTN_3,0x103,,,,,,,VK_XBUTTON1,0x05,,,,,,,,, +BTN_4,0x104,,,,,,,VK_XBUTTON2,0x06,,,,,,,,, +BTN_5,0x105,,,,,,,,,,,,,,,,, +BTN_6,0x106,,,,,,,,,,,,,,,,, +BTN_7,0x107,,,,,,,,,,,,,,,,, +BTN_8,0x108,,,,,,,,,,,,,,,,, +BTN_9,0x109,,,,,,,,,,,,,,,,, +BTN_MOUSE,0x110,,,,,,,,,,,,,,,,, +BTN_LEFT,0x110,,,,,,,,,,,,,,,,, +BTN_RIGHT,0x111,,,,,,,,,,,,,,,,, +BTN_MIDDLE,0x112,,,,,,,,,,,,,,,,, +BTN_SIDE,0x113,,,,,,,,,,,,,,,,, +BTN_EXTRA,0x114,,,,,,,,,,,,,,,,, +BTN_FORWARD,0x115,,,,,,,,,,,,,,,,, +BTN_BACK,0x116,,,,,,,,,,,,,,,,, +BTN_TASK,0x117,,,,,,,,,,,,,,,,, +BTN_JOYSTICK,0x120,,,,,,,,,,,,,,,,, +BTN_TRIGGER,0x120,,,,,,,,,,,,,,,,, +BTN_THUMB,0x121,,,,,,,,,,,,,,,,, +BTN_THUMB2,0x122,,,,,,,,,,,,,,,,, +BTN_TOP,0x123,,,,,,,,,,,,,,,,, +BTN_TOP2,0x124,,,,,,,,,,,,,,,,, +BTN_PINKIE,0x125,,,,,,,,,,,,,,,,, +BTN_BASE,0x126,,,,,,,,,,,,,,,,, +BTN_BASE2,0x127,,,,,,,,,,,,,,,,, +BTN_BASE3,0x128,,,,,,,,,,,,,,,,, +BTN_BASE4,0x129,,,,,,,,,,,,,,,,, +BTN_BASE5,0x12a,,,,,,,,,,,,,,,,, +BTN_BASE6,0x12b,,,,,,,,,,,,,,,,, +BTN_DEAD,0x12f,,,,,,,,,,,,,,,,, +BTN_GAMEPAD,0x130,,,,,,,,,,,,,,,,, +BTN_A,0x130,,,,,,,,,,,,,,,,, +BTN_B,0x131,,,,,,,,,,,,,,,,, +BTN_C,0x132,,,,,,,,,,,,,,,,, +BTN_X,0x133,,,,,,,,,,,,,,,,, +BTN_Y,0x134,,,,,,,,,,,,,,,,, +BTN_Z,0x135,,,,,,,,,,,,,,,,, +BTN_TL,0x136,,,,,,,,,,,,,,,,, +BTN_TR,0x137,,,,,,,,,,,,,,,,, +BTN_TL2,0x138,,,,,,,,,,,,,,,,, +BTN_TR2,0x139,,,,,,,,,,,,,,,,, +BTN_SELECT,0x13a,,,,,,,,,,,,,,,,, +BTN_START,0x13b,,,,,,,,,,,,,,,,, +BTN_MODE,0x13c,,,,,,,,,,,,,,,,, +BTN_THUMBL,0x13d,,,,,,,,,,,,,,,,, +BTN_THUMBR,0x13e,,,,,,,,,,,,,,,,, +BTN_DIGI,0x140,,,,,,,,,,,,,,,,, +BTN_TOOL_PEN,0x140,,,,,,,,,,,,,,,,, +BTN_TOOL_RUBBER,0x141,,,,,,,,,,,,,,,,, +BTN_TOOL_BRUSH,0x142,,,,,,,,,,,,,,,,, +BTN_TOOL_PENCIL,0x143,,,,,,,,,,,,,,,,, +BTN_TOOL_AIRBRUSH,0x144,,,,,,,,,,,,,,,,, +BTN_TOOL_FINGER,0x145,,,,,,,,,,,,,,,,, +BTN_TOOL_MOUSE,0x146,,,,,,,,,,,,,,,,, +BTN_TOOL_LENS,0x147,,,,,,,,,,,,,,,,, +BTN_TOUCH,0x14a,,,,,,,,,,,,,,,,, +BTN_STYLUS,0x14b,,,,,,,,,,,,,,,,, +BTN_STYLUS2,0x14c,,,,,,,,,,,,,,,,, +BTN_TOOL_DOUBLETAP,0x14d,,,,,,,,,,,,,,,,, +BTN_TOOL_TRIPLETAP,0x14e,,,,,,,,,,,,,,,,, +BTN_TOOL_QUADTAP,0x14f,,,,,,,,,,,,,,,,, +BTN_WHEEL,0x150,,,,,,,,,,,,,,,,, +BTN_GEAR_DOWN,0x150,,,,,,,,,,,,,,,,, +BTN_GEAR_UP,0x151,,,,,,,,,,,,,,,,, +KEY_OK,0x160,,,,,,,,,,,,,,,,, +KEY_SELECT,0x161,,,,,,,VK_SELECT,0x29,,,XK_Select,0xff60,Select,SELE,,, +KEY_GOTO,0x162,,,,,,,,,,,,,,,,, +KEY_CLEAR,0x163,,,,,,,,,,,,,NumpadClear,CLR,,, +KEY_POWER2,0x164,,,,,,,,,,,,,,,,, +KEY_OPTION,0x165,,,,,,,,,,,,,,,,, +KEY_INFO,0x166,,,,,,,,,,,,,,,,, +KEY_TIME,0x167,,,,,,,,,,,,,,,,, +KEY_VENDOR,0x168,,,,,,,,,,,,,,,,, +KEY_ARCHIVE,0x169,,,,,,,,,,,,,,,,, +KEY_PROGRAM,0x16a,,,,,,,,,,,,,,,,, +KEY_CHANNEL,0x16b,,,,,,,,,,,,,,,,, +KEY_FAVORITES,0x16c,,,,,,,VK_BROWSER_FAVOURITES,0xab,,,,,,,,, +KEY_EPG,0x16d,,,,,,,,,,,,,,,,, +KEY_PVR,0x16e,,,,,,,,,,,,,,,,, +KEY_MHP,0x16f,,,,,,,,,,,,,,,,, +KEY_LANGUAGE,0x170,,,,,,,,,,,,,,,,, +KEY_TITLE,0x171,,,,,,,,,,,,,,,,, +KEY_SUBTITLE,0x172,,,,,,,,,,,,,,,,, +KEY_ANGLE,0x173,,,,,,,,,,,,,,,,, +KEY_ZOOM,0x174,,,,,,,VK_ZOOM,0xfb,,,,,,,,, +KEY_MODE,0x175,,,,,,,,,,,,,,,,, +KEY_KEYBOARD,0x176,,,,,,,,,,,,,,,,, +KEY_SCREEN,0x177,,,,,,,,,,,,,,,,, +KEY_PC,0x178,,,,,,,,,,,,,,,,, +KEY_TV,0x179,,,,,,,,,,,,,,,,, +KEY_TV2,0x17a,,,,,,,,,,,,,,,,, +KEY_VCR,0x17b,,,,,,,,,,,,,,,,, +KEY_VCR2,0x17c,,,,,,,,,,,,,,,,, +KEY_SAT,0x17d,,,,,,,,,,,,,,,,, +KEY_SAT2,0x17e,,,,,,,,,,,,,,,,, +KEY_CD,0x17f,,,,,,,,,,,,,,,,, +KEY_TAPE,0x180,,,,,,,,,,,,,,,,, +KEY_RADIO,0x181,,,,,,,,,,,,,,,,, +KEY_TUNER,0x182,,,,,,,,,,,,,,,,, +KEY_PLAYER,0x183,,,,,,,,,,,,,,,,, +KEY_TEXT,0x184,,,,,,,,,,,,,,,,, +KEY_DVD,0x185,,,,,,,,,,,,,,,,, +KEY_AUX,0x186,,,,,,,,,,,,,,,,, +KEY_MP3,0x187,,,,,,,,,,,,,,,,, +KEY_AUDIO,0x188,,,,,,,,,,,,,,,,, +KEY_VIDEO,0x189,,,,,,,,,,,,,,,,, +KEY_DIRECTORY,0x18a,,,,,,,,,,,,,,,,, +KEY_LIST,0x18b,,,,,,,,,,,,,,,,, +KEY_MEMO,0x18c,,,,,,,,,,,,,,,,, +KEY_CALENDAR,0x18d,,,,,,,,,,,,,,,,, +KEY_RED,0x18e,,,,,,,,,,,,,,,,, +KEY_GREEN,0x18f,,,,,,,,,,,,,,,,, +KEY_YELLOW,0x190,,,,,,,,,,,,,,,,, +KEY_BLUE,0x191,,,,,,,,,,,,,,,,, +KEY_CHANNELUP,0x192,,,,,,,,,,,,,,,,, +KEY_CHANNELDOWN,0x193,,,,,,,,,,,,,,,,, +KEY_FIRST,0x194,,,,,,,,,,,,,,,,, +KEY_LAST,0x195,,,,,,,,,,,,,,,,, +KEY_AB,0x196,,,,,,,,,,,,,,,,, +KEY_NEXT,0x197,,,,,,,,,,,,,,,,, +KEY_RESTART,0x198,,,,,,,,,,,,,,,,, +KEY_SLOW,0x199,,,,,,,,,,,,,,,,, +KEY_SHUFFLE,0x19a,,,,,,,,,,,,,,,,, +KEY_BREAK,0x19b,,,,,,,,,,,,,,BREA,,, +KEY_BREAK,0x19b,,,,,,,,,,,,,,BRK,,, +KEY_PREVIOUS,0x19c,,,,,,,,,,,,,,,,, +KEY_DIGITS,0x19d,,,,,,,,,,,,,,,,, +KEY_TEEN,0x19e,,,,,,,,,,,,,,,,, +KEY_TWEN,0x19f,,,,,,,,,,,,,,,,, +KEY_VIDEOPHONE,0x1a0,,,,,,,,,,,,,,,,, +KEY_GAMES,0x1a1,,,,,,,,,,,,,,,,, +KEY_ZOOMIN,0x1a2,,,,,,,,,,,,,,,,, +KEY_ZOOMOUT,0x1a3,,,,,,,,,,,,,,,,, +KEY_ZOOMRESET,0x1a4,,,,,,,,,,,,,,,,, +KEY_WORDPROCESSOR,0x1a5,,,,,,,,,,,,,,,,, +KEY_EDITOR,0x1a6,,,,,,,,,,,,,,,,, +KEY_SPREADSHEET,0x1a7,,,,,,,,,,,,,,,,, +KEY_GRAPHICSEDITOR,0x1a8,,,,,,,,,,,,,,,,, +KEY_PRESENTATION,0x1a9,,,,,,,,,,,,,,,,, +KEY_DATABASE,0x1aa,,,,,,,,,,,,,,,,, +KEY_NEWS,0x1ab,,,,,,,,,,,,,,,,, +KEY_VOICEMAIL,0x1ac,,,,,,,,,,,,,,,,, +KEY_ADDRESSBOOK,0x1ad,,,,,,,,,,,,,,,,, +KEY_MESSENGER,0x1ae,,,,,,,,,,,,,,,,, +KEY_DISPLAYTOGGLE,0x1af,,,,,,,,,,,,,,,,, +KEY_SPELLCHECK,0x1b0,,,,,,,,,,,,,,,,, +KEY_LOGOFF,0x1b1,,,,,,,,,,,,,,,,, +KEY_DOLLAR,0x1b2,,,,,,,,,,,,,,,,, +KEY_EURO,0x1b3,,,,,,,,,,,,,,,,, +KEY_FRAMEBACK,0x1b4,,,,,,,,,,,,,,,,, +KEY_FRAMEFORWARD,0x1b5,,,,,,,,,,,,,,,,, +KEY_CONTEXT_MENU,0x1b6,,,,,,,,,,,,,,,,, +KEY_MEDIA_REPEAT,0x1b7,,,,,,,,,,,,,,,,, +KEY_DEL_EOL,0x1c0,,,,,,,,,,,,,,,,, +KEY_DEL_EOS,0x1c1,,,,,,,,,,,,,,,,, +KEY_INS_LINE,0x1c2,,,,,,,,,,,,,,,,, +KEY_DEL_LINE,0x1c3,,,,,,,,,,,,,,,,, +KEY_FN,0x1d0,Function,0x3f,,,,,,,,,,,Fn,,,, +KEY_FN_ESC,0x1d1,,,,,,,,,,,,,,,,, +KEY_FN_F1,0x1d2,,,,,,,,,,,,,,,,, +KEY_FN_F2,0x1d3,,,,,,,,,,,,,,,,, +KEY_FN_F3,0x1d4,,,,,,,,,,,,,,,,, +KEY_FN_F4,0x1d5,,,,,,,,,,,,,,,,, +KEY_FN_F5,0x1d6,,,,,,,,,,,,,,,,, +KEY_FN_F6,0x1d7,,,,,,,,,,,,,,,,, +KEY_FN_F7,0x1d8,,,,,,,,,,,,,,,,, +KEY_FN_F8,0x1d9,,,,,,,,,,,,,,,,, +KEY_FN_F9,0x1da,,,,,,,,,,,,,,,,, +KEY_FN_F10,0x1db,,,,,,,,,,,,,,,,, +KEY_FN_F11,0x1dc,,,,,,,,,,,,,,,,, +KEY_FN_F12,0x1dd,,,,,,,,,,,,,,,,, +KEY_FN_1,0x1de,,,,,,,,,,,,,,,,, +KEY_FN_2,0x1df,,,,,,,,,,,,,,,,, +KEY_FN_D,0x1e0,,,,,,,,,,,,,,,,, +KEY_FN_E,0x1e1,,,,,,,,,,,,,,,,, +KEY_FN_F,0x1e2,,,,,,,,,,,,,,,,, +KEY_FN_S,0x1e3,,,,,,,,,,,,,,,,, +KEY_FN_B,0x1e4,,,,,,,,,,,,,,,,, +KEY_BRL_DOT1,0x1f1,,,,,,,,,,,,,,,,, +KEY_BRL_DOT2,0x1f2,,,,,,,,,,,,,,,,, +KEY_BRL_DOT3,0x1f3,,,,,,,,,,,,,,,,, +KEY_BRL_DOT4,0x1f4,,,,,,,,,,,,,,,,, +KEY_BRL_DOT5,0x1f5,,,,,,,,,,,,,,,,, +KEY_BRL_DOT6,0x1f6,,,,,,,,,,,,,,,,, +KEY_BRL_DOT7,0x1f7,,,,,,,,,,,,,,,,, +KEY_BRL_DOT8,0x1f8,,,,,,,,,,,,,,,,, +KEY_BRL_DOT9,0x1f9,,,,,,,,,,,,,,,,, +KEY_BRL_DOT10,0x1fa,,,,,,,,,,,,,,,,, +KEY_NUMERIC_0,0x200,,,,,,,,,,,,,,,,, +KEY_NUMERIC_1,0x201,,,,,,,,,,,,,,,,, +KEY_NUMERIC_2,0x202,,,,,,,,,,,,,,,,, +KEY_NUMERIC_3,0x203,,,,,,,,,,,,,,,,, +KEY_NUMERIC_4,0x204,,,,,,,,,,,,,,,,, +KEY_NUMERIC_5,0x205,,,,,,,,,,,,,,,,, +KEY_NUMERIC_6,0x206,,,,,,,,,,,,,,,,, +KEY_NUMERIC_7,0x207,,,,,,,,,,,,,,,,, +KEY_NUMERIC_8,0x208,,,,,,,,,,,,,,,,, +KEY_NUMERIC_9,0x209,,,,,,,,,,,,,,,,, +KEY_NUMERIC_STAR,0x20a,,,,,,,,,,,,,NumpadStar,,,, +KEY_NUMERIC_POUND,0x20b,,,,,,,,,,,,,NumpadHash,,,, +KEY_RFKILL,0x20c,,,,,,,,,,,,,,,,, diff --git a/ui/keycodemapdb/meson.build b/ui/keycodemapdb/meson.build new file mode 100644 index 00000000..eb9416b3 --- /dev/null +++ b/ui/keycodemapdb/meson.build @@ -0,0 +1 @@ +project('keycodemapdb') diff --git a/ui/keycodemapdb/tests/.gitignore b/ui/keycodemapdb/tests/.gitignore new file mode 100644 index 00000000..05623054 --- /dev/null +++ b/ui/keycodemapdb/tests/.gitignore @@ -0,0 +1,11 @@ +osx2win32.* +osx2win32_name.* +osx2xkb.* +osx2xkb_name.* +html2win32.* +html2win32_name.* +osx.* +osx_name.* +stdc +stdc++ +node_modules/ diff --git a/ui/keycodemapdb/tests/Makefile b/ui/keycodemapdb/tests/Makefile new file mode 100644 index 00000000..3ac7a21a --- /dev/null +++ b/ui/keycodemapdb/tests/Makefile @@ -0,0 +1,171 @@ +TESTS := stdc stdc++ python2 python3 javascript rust + +check: $(TESTS) + @set -e; for fn in $(TESTS); do \ + ./$$fn; \ + echo $$fn: OK; \ + done + @echo Done. + +GEN := ../tools/keymap-gen +DATA := ../data/keymaps.csv +SOURCES := $(GEN) $(DATA) + +.DELETE_ON_ERROR: + +stdc: stdc.c osx2win32.h osx2win32.c osx2win32_name.h osx2win32_name.c \ + osx2xkb.h osx2xkb.c osx2xkb_name.h osx2xkb_name.c \ + html2win32.h html2win32.c html2win32_name.h html2win32_name.c \ + osx.h osx.c osx_name.h osx_name.c + $(CC) -Wall -o $@ $(filter %.c, $^) +osx2win32.c: $(SOURCES) + $(GEN) code-map --lang stdc $(DATA) osx win32 > $@ +osx2win32.h: $(SOURCES) + $(GEN) code-map --lang stdc-header $(DATA) osx win32 > $@ +osx2win32_name.c: $(SOURCES) + $(GEN) name-map --lang stdc $(DATA) osx win32 > $@ +osx2win32_name.h: $(SOURCES) + $(GEN) name-map --lang stdc-header $(DATA) osx win32 > $@ +osx2xkb.c: $(SOURCES) + $(GEN) code-map --lang stdc $(DATA) osx xkb > $@ +osx2xkb.h: $(SOURCES) + $(GEN) code-map --lang stdc-header $(DATA) osx xkb > $@ +osx2xkb_name.c: $(SOURCES) + $(GEN) name-map --lang stdc $(DATA) osx xkb > $@ +osx2xkb_name.h: $(SOURCES) + $(GEN) name-map --lang stdc-header $(DATA) osx xkb > $@ +html2win32.c: $(SOURCES) + $(GEN) code-map --lang stdc $(DATA) html win32 > $@ +html2win32.h: $(SOURCES) + $(GEN) code-map --lang stdc-header $(DATA) html win32 > $@ +html2win32_name.c: $(SOURCES) + $(GEN) name-map --lang stdc $(DATA) html win32 > $@ +html2win32_name.h: $(SOURCES) + $(GEN) name-map --lang stdc-header $(DATA) html win32 > $@ +osx.c: $(SOURCES) + $(GEN) code-table --lang stdc $(DATA) osx > $@ +osx.h: $(SOURCES) + $(GEN) code-table --lang stdc-header $(DATA) osx > $@ +osx_name.c: $(SOURCES) + $(GEN) name-table --lang stdc $(DATA) osx > $@ +osx_name.h: $(SOURCES) + $(GEN) name-table --lang stdc-header $(DATA) osx > $@ + +stdc++: stdc++.cc osx2win32.hh osx2win32.cc osx2win32_name.hh osx2win32_name.cc \ + osx2xkb.hh osx2xkb.cc osx2xkb_name.hh osx2xkb_name.cc \ + html2win32.hh html2win32.cc html2win32_name.hh html2win32_name.cc \ + osx.hh osx.cc osx_name.hh osx_name.cc + $(CXX) -Wall -std=c++11 -o $@ $(filter %.cc, $^) +osx2win32.cc: $(SOURCES) + $(GEN) code-map --lang stdc++ $(DATA) osx win32 > $@ +osx2win32.hh: $(SOURCES) + $(GEN) code-map --lang stdc++-header $(DATA) osx win32 > $@ +osx2win32_name.cc: $(SOURCES) + $(GEN) name-map --lang stdc++ $(DATA) osx win32 > $@ +osx2win32_name.hh: $(SOURCES) + $(GEN) name-map --lang stdc++-header $(DATA) osx win32 > $@ +osx2xkb.cc: $(SOURCES) + $(GEN) code-map --lang stdc++ $(DATA) osx xkb > $@ +osx2xkb.hh: $(SOURCES) + $(GEN) code-map --lang stdc++-header $(DATA) osx xkb > $@ +osx2xkb_name.cc: $(SOURCES) + $(GEN) name-map --lang stdc++ $(DATA) osx xkb > $@ +osx2xkb_name.hh: $(SOURCES) + $(GEN) name-map --lang stdc++-header $(DATA) osx xkb > $@ +html2win32.cc: $(SOURCES) + $(GEN) code-map --lang stdc++ $(DATA) html win32 > $@ +html2win32.hh: $(SOURCES) + $(GEN) code-map --lang stdc++-header $(DATA) html win32 > $@ +html2win32_name.cc: $(SOURCES) + $(GEN) name-map --lang stdc++ $(DATA) html win32 > $@ +html2win32_name.hh: $(SOURCES) + $(GEN) name-map --lang stdc++-header $(DATA) html win32 > $@ +osx.cc: $(SOURCES) + $(GEN) code-table --lang stdc++ $(DATA) osx > $@ +osx.hh: $(SOURCES) + $(GEN) code-table --lang stdc++-header $(DATA) osx > $@ +osx_name.cc: $(SOURCES) + $(GEN) name-table --lang stdc++ $(DATA) osx > $@ +osx_name.hh: $(SOURCES) + $(GEN) name-table --lang stdc++-header $(DATA) osx > $@ + +python2: osx2win32.py osx2win32_name.py \ + osx2xkb.py osx2xkb_name.py \ + html2win32.py html2win32_name.py \ + osx.py osx_name.py +osx2win32.py: $(SOURCES) + $(GEN) code-map --lang python2 $(DATA) osx win32 > $@ +osx2win32_name.py: $(SOURCES) + $(GEN) name-map --lang python2 $(DATA) osx win32 > $@ +osx2xkb.py: $(SOURCES) + $(GEN) code-map --lang python2 $(DATA) osx xkb > $@ +osx2xkb_name.py: $(SOURCES) + $(GEN) name-map --lang python2 $(DATA) osx xkb > $@ +html2win32.py: $(SOURCES) + $(GEN) code-map --lang python2 $(DATA) html win32 > $@ +html2win32_name.py: $(SOURCES) + $(GEN) name-map --lang python2 $(DATA) html win32 > $@ +osx.py: $(SOURCES) + $(GEN) code-table --lang python2 $(DATA) osx > $@ +osx_name.py: $(SOURCES) + $(GEN) name-table --lang python2 $(DATA) osx > $@ + +javascript: node_modules/babel-core \ + node_modules/babel-plugin-transform-es2015-modules-commonjs \ + osx2win32.js osx2win32_name.js \ + osx2xkb.js osx2xkb_name.js \ + html2win32.js html2win32_name.js \ + osx.js osx_name.js +node_modules/babel-core: + npm install babel-core +node_modules/babel-plugin-transform-es2015-modules-commonjs: + npm install babel-plugin-transform-es2015-modules-commonjs +osx2win32.js: $(SOURCES) + $(GEN) code-map --lang js $(DATA) osx win32 > $@ +osx2win32_name.js: $(SOURCES) + $(GEN) name-map --lang js $(DATA) osx win32 > $@ +osx2xkb.js: $(SOURCES) + $(GEN) code-map --lang js $(DATA) osx xkb > $@ +osx2xkb_name.js: $(SOURCES) + $(GEN) name-map --lang js $(DATA) osx xkb > $@ +html2win32.js: $(SOURCES) + $(GEN) code-map --lang js $(DATA) html win32 > $@ +html2win32_name.js: $(SOURCES) + $(GEN) name-map --lang js $(DATA) html win32 > $@ +osx.js: $(SOURCES) + $(GEN) code-table --lang js $(DATA) osx > $@ +osx_name.js: $(SOURCES) + $(GEN) name-table --lang js $(DATA) osx > $@ + +rust: osx2win32.rs osx2win32_name.rs \ + osx2xkb.rs osx2xkb_name.rs \ + html2win32.rs html2win32_name.rs \ + osx.rs osx_name.rs +osx2win32.rs: $(SOURCES) + $(GEN) code-map --lang rust $(DATA) osx win32 > $@ +osx2win32_name.rs: $(SOURCES) + $(GEN) name-map --lang rust $(DATA) osx win32 > $@ +osx2xkb.rs: $(SOURCES) + $(GEN) code-map --lang rust $(DATA) osx xkb > $@ +osx2xkb_name.rs: $(SOURCES) + $(GEN) name-map --lang rust $(DATA) osx xkb > $@ +html2win32.rs: $(SOURCES) + $(GEN) code-map --lang rust $(DATA) html win32 > $@ +html2win32_name.rs: $(SOURCES) + $(GEN) name-map --lang rust $(DATA) html win32 > $@ +osx.rs: $(SOURCES) + $(GEN) code-table --lang rust $(DATA) osx > $@ +osx_name.rs: $(SOURCES) + $(GEN) name-table --lang rust $(DATA) osx > $@ + +clean: + rm -rf node_modules + rm -f osx2win32.* + rm -f osx2win32_name.* + rm -f osx2xkb.* + rm -f osx2xkb_name.* + rm -f html2win32.* + rm -f html2win32_name.* + rm -f osx.* + rm -f osx_name.* + rm -f stdc stdc++ diff --git a/ui/keycodemapdb/tests/javascript b/ui/keycodemapdb/tests/javascript new file mode 100755 index 00000000..5179db2c --- /dev/null +++ b/ui/keycodemapdb/tests/javascript @@ -0,0 +1,53 @@ +#!/usr/bin/env node +/* + * Keycode Map Generator JavaScript Tests + * + * Copyright 2017 Pierre Ossman for Cendio AB + * + * This file is dual license under the terms of the GPLv2 or later + * and 3-clause BSD licenses. + */ + +"use strict"; + +var assert = require('assert'); +var babel = require('babel-core'); +var fs = require('fs'); + +function include(fn) { + var options = { + plugins: ["transform-es2015-modules-commonjs"] + }; + + var code = babel.transformFileSync(fn, options).code; + fs.writeFileSync("." + fn + "_nodejs.js", code); + var imp = require("./." + fn + "_nodejs.js"); + fs.unlinkSync("./." + fn + "_nodejs.js"); + + return imp +} + +var code_map_osx_to_win32 = include("osx2win32.js").default; +var name_map_osx_to_win32 = include("osx2win32_name.js").default; + +var code_map_osx_to_xkb = include("osx2xkb.js").default; +var name_map_osx_to_xkb = include("osx2xkb_name.js").default; + +var code_map_html_to_win32 = include("html2win32.js").default; +var name_map_html_to_win32 = include("html2win32_name.js").default; + +var code_table_osx = include("osx.js").default; +var name_table_osx = include("osx_name.js").default; + +assert.equal(code_map_osx_to_win32[0x1d], 0x30); +assert.equal(name_map_osx_to_win32[0x1d], "VK_0"); + +assert.equal(code_map_osx_to_xkb[0x1d], "AE10"); +assert.equal(name_map_osx_to_xkb[0x1d], "AE10"); + +assert.equal(code_map_html_to_win32["ControlLeft"], 0x11); +assert.equal(name_map_html_to_win32["ControlLeft"], "VK_CONTROL"); + +assert.equal(code_table_osx[0x1d], 0x3b); +assert.equal(name_table_osx[0x1d], "Control"); + diff --git a/ui/keycodemapdb/tests/python2 b/ui/keycodemapdb/tests/python2 new file mode 100755 index 00000000..28a5b034 --- /dev/null +++ b/ui/keycodemapdb/tests/python2 @@ -0,0 +1,3 @@ +#!/bin/sh + +python ./test.py diff --git a/ui/keycodemapdb/tests/python3 b/ui/keycodemapdb/tests/python3 new file mode 100755 index 00000000..ded1f680 --- /dev/null +++ b/ui/keycodemapdb/tests/python3 @@ -0,0 +1,3 @@ +#!/bin/sh + +python3 ./test.py diff --git a/ui/keycodemapdb/tests/rust b/ui/keycodemapdb/tests/rust new file mode 100755 index 00000000..0e133b12 --- /dev/null +++ b/ui/keycodemapdb/tests/rust @@ -0,0 +1,5 @@ +#!/bin/sh + +cd rust-test +cargo test +cargo clippy diff --git a/ui/keycodemapdb/tests/rust-test/Cargo.toml b/ui/keycodemapdb/tests/rust-test/Cargo.toml new file mode 100644 index 00000000..832ac377 --- /dev/null +++ b/ui/keycodemapdb/tests/rust-test/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rust-test" +version = "0.1.0" +authors = ["Marc-André Lureau <marcandre.lureau@redhat.com>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +phf = { version = "0.8", features = ["macros"] } diff --git a/ui/keycodemapdb/tests/rust-test/src/main.rs b/ui/keycodemapdb/tests/rust-test/src/main.rs new file mode 100644 index 00000000..ba165d03 --- /dev/null +++ b/ui/keycodemapdb/tests/rust-test/src/main.rs @@ -0,0 +1,28 @@ +include!("../../html2win32_name.rs"); +include!("../../html2win32.rs"); +include!("../../osx2win32_name.rs"); +include!("../../osx2win32.rs"); +include!("../../osx2xkb_name.rs"); +include!("../../osx2xkb.rs"); +include!("../../osx_name.rs"); +include!("../../osx.rs"); + +fn main() { + assert_eq!(CODE_MAP_OSX_TO_WIN32[0x1d], 0x30); + assert_eq!(NAME_MAP_OSX_TO_WIN32[0x1d], "VK_0"); + + assert_eq!(CODE_MAP_OSX_TO_XKB[0x1d], "AE10"); + assert_eq!(NAME_MAP_OSX_TO_XKB[0x1d], "AE10"); + + assert_eq!(CODE_MAP_HTML_TO_WIN32["ControlLeft"], 0x11); + assert_eq!(NAME_MAP_HTML_TO_WIN32["ControlLeft"], "VK_CONTROL"); + + assert_eq!(CODE_TABLE_OSX[0x1d], 0x3b); + assert_eq!(NAME_TABLE_OSX[0x1d], "Control"); +} + + +#[test] +fn test() { + main() +} diff --git a/ui/keycodemapdb/tests/stdc++.cc b/ui/keycodemapdb/tests/stdc++.cc new file mode 100644 index 00000000..5e3e8f55 --- /dev/null +++ b/ui/keycodemapdb/tests/stdc++.cc @@ -0,0 +1,40 @@ +/* + * Keycode Map Generator C++ Tests + * + * Copyright 2017 Pierre Ossman for Cendio AB + * + * This file is dual license under the terms of the GPLv2 or later + * and 3-clause BSD licenses. + */ + +#include <assert.h> +#include <string.h> + +#include "osx2win32.hh" +#include "osx2win32_name.hh" + +#include "osx2xkb.hh" +#include "osx2xkb_name.hh" + +#include "html2win32.hh" +#include "html2win32_name.hh" + +#include "osx.hh" +#include "osx_name.hh" + +int main(int argc, char** argv) +{ + assert(code_map_osx_to_win32[0x1d] == 0x30); + assert(strcmp(name_map_osx_to_win32[0x1d], "VK_0") == 0); + + assert(strcmp(code_map_osx_to_xkb[0x1d], "AE10") == 0); + assert(strcmp(name_map_osx_to_xkb[0x1d], "AE10") == 0); + + assert(code_map_html_to_win32.at("ControlLeft") == 0x11); + assert(strcmp(name_map_html_to_win32.at("ControlLeft"), "VK_CONTROL") == 0); + + assert(code_table_osx[0x1d] == 0x3b); + assert(strcmp(name_table_osx[0x1d], "Control") == 0); + + return 0; +} diff --git a/ui/keycodemapdb/tests/stdc.c b/ui/keycodemapdb/tests/stdc.c new file mode 100644 index 00000000..e4946fa5 --- /dev/null +++ b/ui/keycodemapdb/tests/stdc.c @@ -0,0 +1,64 @@ +/* + * Keycode Map Generator C Tests + * + * Copyright 2017 Pierre Ossman for Cendio AB + * + * This file is dual license under the terms of the GPLv2 or later + * and 3-clause BSD licenses. + */ + +#include <assert.h> +#include <string.h> + +#include "osx2win32.h" +#include "osx2win32_name.h" + +#include "osx2xkb.h" +#include "osx2xkb_name.h" + +#include "html2win32.h" +#include "html2win32_name.h" + +#include "osx.h" +#include "osx_name.h" + +#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0])) + +int main(int argc, char** argv) +{ + unsigned i; + + assert(code_map_osx_to_win32_len == ARRAY_SIZE(code_map_osx_to_win32)); + assert(code_map_osx_to_win32[0x1d] == 0x30); + assert(name_map_osx_to_win32_len == ARRAY_SIZE(name_map_osx_to_win32)); + assert(strcmp(name_map_osx_to_win32[0x1d], "VK_0") == 0); + + assert(code_map_osx_to_xkb_len == ARRAY_SIZE(code_map_osx_to_xkb)); + assert(strcmp(code_map_osx_to_xkb[0x1d], "AE10") == 0); + assert(name_map_osx_to_xkb_len == ARRAY_SIZE(name_map_osx_to_xkb)); + assert(strcmp(name_map_osx_to_xkb[0x1d], "AE10") == 0); + + assert(code_map_html_to_win32_len == ARRAY_SIZE(code_map_html_to_win32)); + for (i = 0;i < code_map_html_to_win32_len;i++) { + if (strcmp(code_map_html_to_win32[i].from, "ControlLeft") == 0) { + assert(code_map_html_to_win32[i].to == 0x11); + break; + } + } + assert(i != code_map_html_to_win32_len); + assert(name_map_html_to_win32_len == ARRAY_SIZE(name_map_html_to_win32)); + for (i = 0;i < name_map_html_to_win32_len;i++) { + if (strcmp(name_map_html_to_win32[i].from, "ControlLeft") == 0) { + assert(strcmp(name_map_html_to_win32[i].to, "VK_CONTROL") == 0); + break; + } + } + assert(i != name_map_html_to_win32_len); + + assert(code_table_osx_len == ARRAY_SIZE(code_table_osx)); + assert(code_table_osx[0x1d] == 0x3b); + assert(name_table_osx_len == ARRAY_SIZE(name_table_osx)); + assert(strcmp(name_table_osx[0x1d], "Control") == 0); + + return 0; +} diff --git a/ui/keycodemapdb/tests/test.py b/ui/keycodemapdb/tests/test.py new file mode 100644 index 00000000..f2651455 --- /dev/null +++ b/ui/keycodemapdb/tests/test.py @@ -0,0 +1,30 @@ +# Keycode Map Generator Python Tests +# +# Copyright 2017 Pierre Ossman for Cendio AB +# +# This file is dual license under the terms of the GPLv2 or later +# and 3-clause BSD licenses. + +import osx2win32 +import osx2win32_name + +import osx2xkb +import osx2xkb_name + +import html2win32 +import html2win32_name + +import osx +import osx_name + +assert osx2win32.code_map_osx_to_win32[0x1d] == 0x30 +assert osx2win32_name.name_map_osx_to_win32[0x1d] == "VK_0" + +assert osx2xkb.code_map_osx_to_xkb[0x1d] == "AE10" +assert osx2xkb_name.name_map_osx_to_xkb[0x1d] == "AE10" + +assert html2win32.code_map_html_to_win32["ControlLeft"] == 0x11 +assert html2win32_name.name_map_html_to_win32["ControlLeft"] == "VK_CONTROL" + +assert osx.code_table_osx[0x1d] == 0x3b; +assert osx_name.name_table_osx[0x1d] == "Control"; diff --git a/ui/keycodemapdb/thirdparty/LICENSE-argparse.txt b/ui/keycodemapdb/thirdparty/LICENSE-argparse.txt new file mode 100644 index 00000000..640bc780 --- /dev/null +++ b/ui/keycodemapdb/thirdparty/LICENSE-argparse.txt @@ -0,0 +1,20 @@ +argparse is (c) 2006-2009 Steven J. Bethard <steven.bethard@gmail.com>. + +The argparse module was contributed to Python as of Python 2.7 and thus +was licensed under the Python license. Same license applies to all files in +the argparse package project. + +For details about the Python License, please see doc/Python-License.txt. + +History +------- + +Before (and including) argparse 1.1, the argparse package was licensed under +Apache License v2.0. + +After argparse 1.1, all project files from the argparse project were deleted +due to license compatibility issues between Apache License 2.0 and GNU GPL v2. + +The project repository then had a clean start with some files taken from +Python 2.7.1, so definitely all files are under Python License now. + diff --git a/ui/keycodemapdb/thirdparty/__init__.py b/ui/keycodemapdb/thirdparty/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ui/keycodemapdb/thirdparty/__init__.py diff --git a/ui/keycodemapdb/thirdparty/argparse.py b/ui/keycodemapdb/thirdparty/argparse.py new file mode 100644 index 00000000..70a77cc0 --- /dev/null +++ b/ui/keycodemapdb/thirdparty/argparse.py @@ -0,0 +1,2392 @@ +# Author: Steven J. Bethard <steven.bethard@gmail.com>. +# Maintainer: Thomas Waldmann <tw@waldmann-edv.de> + +"""Command-line parsing library + +This module is an optparse-inspired command-line parsing library that: + + - handles both optional and positional arguments + - produces highly informative usage messages + - supports parsers that dispatch to sub-parsers + +The following is a simple usage example that sums integers from the +command-line and writes the result to a file:: + + parser = argparse.ArgumentParser( + description='sum the integers at the command line') + parser.add_argument( + 'integers', metavar='int', nargs='+', type=int, + help='an integer to be summed') + parser.add_argument( + '--log', default=sys.stdout, type=argparse.FileType('w'), + help='the file where the sum should be written') + args = parser.parse_args() + args.log.write('%s' % sum(args.integers)) + args.log.close() + +The module contains the following public classes: + + - ArgumentParser -- The main entry point for command-line parsing. As the + example above shows, the add_argument() method is used to populate + the parser with actions for optional and positional arguments. Then + the parse_args() method is invoked to convert the args at the + command-line into an object with attributes. + + - ArgumentError -- The exception raised by ArgumentParser objects when + there are errors with the parser's actions. Errors raised while + parsing the command-line are caught by ArgumentParser and emitted + as command-line messages. + + - FileType -- A factory for defining types of files to be created. As the + example above shows, instances of FileType are typically passed as + the type= argument of add_argument() calls. + + - Action -- The base class for parser actions. Typically actions are + selected by passing strings like 'store_true' or 'append_const' to + the action= argument of add_argument(). However, for greater + customization of ArgumentParser actions, subclasses of Action may + be defined and passed as the action= argument. + + - HelpFormatter, RawDescriptionHelpFormatter, RawTextHelpFormatter, + ArgumentDefaultsHelpFormatter -- Formatter classes which + may be passed as the formatter_class= argument to the + ArgumentParser constructor. HelpFormatter is the default, + RawDescriptionHelpFormatter and RawTextHelpFormatter tell the parser + not to change the formatting for help text, and + ArgumentDefaultsHelpFormatter adds information about argument defaults + to the help. + +All other classes in this module are considered implementation details. +(Also note that HelpFormatter and RawDescriptionHelpFormatter are only +considered public as object names -- the API of the formatter objects is +still considered an implementation detail.) +""" + +__version__ = '1.4.0' # we use our own version number independant of the + # one in stdlib and we release this on pypi. + +__external_lib__ = True # to make sure the tests really test THIS lib, + # not the builtin one in Python stdlib + +__all__ = [ + 'ArgumentParser', + 'ArgumentError', + 'ArgumentTypeError', + 'FileType', + 'HelpFormatter', + 'ArgumentDefaultsHelpFormatter', + 'RawDescriptionHelpFormatter', + 'RawTextHelpFormatter', + 'Namespace', + 'Action', + 'ONE_OR_MORE', + 'OPTIONAL', + 'PARSER', + 'REMAINDER', + 'SUPPRESS', + 'ZERO_OR_MORE', +] + + +import copy as _copy +import os as _os +import re as _re +import sys as _sys +import textwrap as _textwrap + +from gettext import gettext as _ + +try: + set +except NameError: + # for python < 2.4 compatibility (sets module is there since 2.3): + from sets import Set as set + +try: + basestring +except NameError: + basestring = str + +try: + sorted +except NameError: + # for python < 2.4 compatibility: + def sorted(iterable, reverse=False): + result = list(iterable) + result.sort() + if reverse: + result.reverse() + return result + + +def _callable(obj): + return hasattr(obj, '__call__') or hasattr(obj, '__bases__') + + +SUPPRESS = '==SUPPRESS==' + +OPTIONAL = '?' +ZERO_OR_MORE = '*' +ONE_OR_MORE = '+' +PARSER = 'A...' +REMAINDER = '...' +_UNRECOGNIZED_ARGS_ATTR = '_unrecognized_args' + +# ============================= +# Utility functions and classes +# ============================= + +class _AttributeHolder(object): + """Abstract base class that provides __repr__. + + The __repr__ method returns a string in the format:: + ClassName(attr=name, attr=name, ...) + The attributes are determined either by a class-level attribute, + '_kwarg_names', or by inspecting the instance __dict__. + """ + + def __repr__(self): + type_name = type(self).__name__ + arg_strings = [] + for arg in self._get_args(): + arg_strings.append(repr(arg)) + for name, value in self._get_kwargs(): + arg_strings.append('%s=%r' % (name, value)) + return '%s(%s)' % (type_name, ', '.join(arg_strings)) + + def _get_kwargs(self): + return sorted(self.__dict__.items()) + + def _get_args(self): + return [] + + +def _ensure_value(namespace, name, value): + if getattr(namespace, name, None) is None: + setattr(namespace, name, value) + return getattr(namespace, name) + + +# =============== +# Formatting Help +# =============== + +class HelpFormatter(object): + """Formatter for generating usage messages and argument help strings. + + Only the name of this class is considered a public API. All the methods + provided by the class are considered an implementation detail. + """ + + def __init__(self, + prog, + indent_increment=2, + max_help_position=24, + width=None): + + # default setting for width + if width is None: + try: + width = int(_os.environ['COLUMNS']) + except (KeyError, ValueError): + width = 80 + width -= 2 + + self._prog = prog + self._indent_increment = indent_increment + self._max_help_position = max_help_position + self._width = width + + self._current_indent = 0 + self._level = 0 + self._action_max_length = 0 + + self._root_section = self._Section(self, None) + self._current_section = self._root_section + + self._whitespace_matcher = _re.compile(r'\s+') + self._long_break_matcher = _re.compile(r'\n\n\n+') + + # =============================== + # Section and indentation methods + # =============================== + def _indent(self): + self._current_indent += self._indent_increment + self._level += 1 + + def _dedent(self): + self._current_indent -= self._indent_increment + assert self._current_indent >= 0, 'Indent decreased below 0.' + self._level -= 1 + + class _Section(object): + + def __init__(self, formatter, parent, heading=None): + self.formatter = formatter + self.parent = parent + self.heading = heading + self.items = [] + + def format_help(self): + # format the indented section + if self.parent is not None: + self.formatter._indent() + join = self.formatter._join_parts + for func, args in self.items: + func(*args) + item_help = join([func(*args) for func, args in self.items]) + if self.parent is not None: + self.formatter._dedent() + + # return nothing if the section was empty + if not item_help: + return '' + + # add the heading if the section was non-empty + if self.heading is not SUPPRESS and self.heading is not None: + current_indent = self.formatter._current_indent + heading = '%*s%s:\n' % (current_indent, '', self.heading) + else: + heading = '' + + # join the section-initial newline, the heading and the help + return join(['\n', heading, item_help, '\n']) + + def _add_item(self, func, args): + self._current_section.items.append((func, args)) + + # ======================== + # Message building methods + # ======================== + def start_section(self, heading): + self._indent() + section = self._Section(self, self._current_section, heading) + self._add_item(section.format_help, []) + self._current_section = section + + def end_section(self): + self._current_section = self._current_section.parent + self._dedent() + + def add_text(self, text): + if text is not SUPPRESS and text is not None: + self._add_item(self._format_text, [text]) + + def add_usage(self, usage, actions, groups, prefix=None): + if usage is not SUPPRESS: + args = usage, actions, groups, prefix + self._add_item(self._format_usage, args) + + def add_argument(self, action): + if action.help is not SUPPRESS: + + # find all invocations + get_invocation = self._format_action_invocation + invocations = [get_invocation(action)] + for subaction in self._iter_indented_subactions(action): + invocations.append(get_invocation(subaction)) + + # update the maximum item length + invocation_length = max([len(s) for s in invocations]) + action_length = invocation_length + self._current_indent + self._action_max_length = max(self._action_max_length, + action_length) + + # add the item to the list + self._add_item(self._format_action, [action]) + + def add_arguments(self, actions): + for action in actions: + self.add_argument(action) + + # ======================= + # Help-formatting methods + # ======================= + def format_help(self): + help = self._root_section.format_help() + if help: + help = self._long_break_matcher.sub('\n\n', help) + help = help.strip('\n') + '\n' + return help + + def _join_parts(self, part_strings): + return ''.join([part + for part in part_strings + if part and part is not SUPPRESS]) + + def _format_usage(self, usage, actions, groups, prefix): + if prefix is None: + prefix = _('usage: ') + + # if usage is specified, use that + if usage is not None: + usage = usage % dict(prog=self._prog) + + # if no optionals or positionals are available, usage is just prog + elif usage is None and not actions: + usage = '%(prog)s' % dict(prog=self._prog) + + # if optionals and positionals are available, calculate usage + elif usage is None: + prog = '%(prog)s' % dict(prog=self._prog) + + # split optionals from positionals + optionals = [] + positionals = [] + for action in actions: + if action.option_strings: + optionals.append(action) + else: + positionals.append(action) + + # build full usage string + format = self._format_actions_usage + action_usage = format(optionals + positionals, groups) + usage = ' '.join([s for s in [prog, action_usage] if s]) + + # wrap the usage parts if it's too long + text_width = self._width - self._current_indent + if len(prefix) + len(usage) > text_width: + + # break usage into wrappable parts + part_regexp = r'\(.*?\)+|\[.*?\]+|\S+' + opt_usage = format(optionals, groups) + pos_usage = format(positionals, groups) + opt_parts = _re.findall(part_regexp, opt_usage) + pos_parts = _re.findall(part_regexp, pos_usage) + assert ' '.join(opt_parts) == opt_usage + assert ' '.join(pos_parts) == pos_usage + + # helper for wrapping lines + def get_lines(parts, indent, prefix=None): + lines = [] + line = [] + if prefix is not None: + line_len = len(prefix) - 1 + else: + line_len = len(indent) - 1 + for part in parts: + if line_len + 1 + len(part) > text_width: + lines.append(indent + ' '.join(line)) + line = [] + line_len = len(indent) - 1 + line.append(part) + line_len += len(part) + 1 + if line: + lines.append(indent + ' '.join(line)) + if prefix is not None: + lines[0] = lines[0][len(indent):] + return lines + + # if prog is short, follow it with optionals or positionals + if len(prefix) + len(prog) <= 0.75 * text_width: + indent = ' ' * (len(prefix) + len(prog) + 1) + if opt_parts: + lines = get_lines([prog] + opt_parts, indent, prefix) + lines.extend(get_lines(pos_parts, indent)) + elif pos_parts: + lines = get_lines([prog] + pos_parts, indent, prefix) + else: + lines = [prog] + + # if prog is long, put it on its own line + else: + indent = ' ' * len(prefix) + parts = opt_parts + pos_parts + lines = get_lines(parts, indent) + if len(lines) > 1: + lines = [] + lines.extend(get_lines(opt_parts, indent)) + lines.extend(get_lines(pos_parts, indent)) + lines = [prog] + lines + + # join lines into usage + usage = '\n'.join(lines) + + # prefix with 'usage:' + return '%s%s\n\n' % (prefix, usage) + + def _format_actions_usage(self, actions, groups): + # find group indices and identify actions in groups + group_actions = set() + inserts = {} + for group in groups: + try: + start = actions.index(group._group_actions[0]) + except ValueError: + continue + else: + end = start + len(group._group_actions) + if actions[start:end] == group._group_actions: + for action in group._group_actions: + group_actions.add(action) + if not group.required: + if start in inserts: + inserts[start] += ' [' + else: + inserts[start] = '[' + inserts[end] = ']' + else: + if start in inserts: + inserts[start] += ' (' + else: + inserts[start] = '(' + inserts[end] = ')' + for i in range(start + 1, end): + inserts[i] = '|' + + # collect all actions format strings + parts = [] + for i, action in enumerate(actions): + + # suppressed arguments are marked with None + # remove | separators for suppressed arguments + if action.help is SUPPRESS: + parts.append(None) + if inserts.get(i) == '|': + inserts.pop(i) + elif inserts.get(i + 1) == '|': + inserts.pop(i + 1) + + # produce all arg strings + elif not action.option_strings: + part = self._format_args(action, action.dest) + + # if it's in a group, strip the outer [] + if action in group_actions: + if part[0] == '[' and part[-1] == ']': + part = part[1:-1] + + # add the action string to the list + parts.append(part) + + # produce the first way to invoke the option in brackets + else: + option_string = action.option_strings[0] + + # if the Optional doesn't take a value, format is: + # -s or --long + if action.nargs == 0: + part = '%s' % option_string + + # if the Optional takes a value, format is: + # -s ARGS or --long ARGS + else: + default = action.dest.upper() + args_string = self._format_args(action, default) + part = '%s %s' % (option_string, args_string) + + # make it look optional if it's not required or in a group + if not action.required and action not in group_actions: + part = '[%s]' % part + + # add the action string to the list + parts.append(part) + + # insert things at the necessary indices + for i in sorted(inserts, reverse=True): + parts[i:i] = [inserts[i]] + + # join all the action items with spaces + text = ' '.join([item for item in parts if item is not None]) + + # clean up separators for mutually exclusive groups + open = r'[\[(]' + close = r'[\])]' + text = _re.sub(r'(%s) ' % open, r'\1', text) + text = _re.sub(r' (%s)' % close, r'\1', text) + text = _re.sub(r'%s *%s' % (open, close), r'', text) + text = _re.sub(r'\(([^|]*)\)', r'\1', text) + text = text.strip() + + # return the text + return text + + def _format_text(self, text): + if '%(prog)' in text: + text = text % dict(prog=self._prog) + text_width = self._width - self._current_indent + indent = ' ' * self._current_indent + return self._fill_text(text, text_width, indent) + '\n\n' + + def _format_action(self, action): + # determine the required width and the entry label + help_position = min(self._action_max_length + 2, + self._max_help_position) + help_width = self._width - help_position + action_width = help_position - self._current_indent - 2 + action_header = self._format_action_invocation(action) + + # ho nelp; start on same line and add a final newline + if not action.help: + tup = self._current_indent, '', action_header + action_header = '%*s%s\n' % tup + + # short action name; start on the same line and pad two spaces + elif len(action_header) <= action_width: + tup = self._current_indent, '', action_width, action_header + action_header = '%*s%-*s ' % tup + indent_first = 0 + + # long action name; start on the next line + else: + tup = self._current_indent, '', action_header + action_header = '%*s%s\n' % tup + indent_first = help_position + + # collect the pieces of the action help + parts = [action_header] + + # if there was help for the action, add lines of help text + if action.help: + help_text = self._expand_help(action) + help_lines = self._split_lines(help_text, help_width) + parts.append('%*s%s\n' % (indent_first, '', help_lines[0])) + for line in help_lines[1:]: + parts.append('%*s%s\n' % (help_position, '', line)) + + # or add a newline if the description doesn't end with one + elif not action_header.endswith('\n'): + parts.append('\n') + + # if there are any sub-actions, add their help as well + for subaction in self._iter_indented_subactions(action): + parts.append(self._format_action(subaction)) + + # return a single string + return self._join_parts(parts) + + def _format_action_invocation(self, action): + if not action.option_strings: + metavar, = self._metavar_formatter(action, action.dest)(1) + return metavar + + else: + parts = [] + + # if the Optional doesn't take a value, format is: + # -s, --long + if action.nargs == 0: + parts.extend(action.option_strings) + + # if the Optional takes a value, format is: + # -s ARGS, --long ARGS + else: + default = action.dest.upper() + args_string = self._format_args(action, default) + for option_string in action.option_strings: + parts.append('%s %s' % (option_string, args_string)) + + return ', '.join(parts) + + def _metavar_formatter(self, action, default_metavar): + if action.metavar is not None: + result = action.metavar + elif action.choices is not None: + choice_strs = [str(choice) for choice in action.choices] + result = '{%s}' % ','.join(choice_strs) + else: + result = default_metavar + + def format(tuple_size): + if isinstance(result, tuple): + return result + else: + return (result, ) * tuple_size + return format + + def _format_args(self, action, default_metavar): + get_metavar = self._metavar_formatter(action, default_metavar) + if action.nargs is None: + result = '%s' % get_metavar(1) + elif action.nargs == OPTIONAL: + result = '[%s]' % get_metavar(1) + elif action.nargs == ZERO_OR_MORE: + result = '[%s [%s ...]]' % get_metavar(2) + elif action.nargs == ONE_OR_MORE: + result = '%s [%s ...]' % get_metavar(2) + elif action.nargs == REMAINDER: + result = '...' + elif action.nargs == PARSER: + result = '%s ...' % get_metavar(1) + else: + formats = ['%s' for _ in range(action.nargs)] + result = ' '.join(formats) % get_metavar(action.nargs) + return result + + def _expand_help(self, action): + params = dict(vars(action), prog=self._prog) + for name in list(params): + if params[name] is SUPPRESS: + del params[name] + for name in list(params): + if hasattr(params[name], '__name__'): + params[name] = params[name].__name__ + if params.get('choices') is not None: + choices_str = ', '.join([str(c) for c in params['choices']]) + params['choices'] = choices_str + return self._get_help_string(action) % params + + def _iter_indented_subactions(self, action): + try: + get_subactions = action._get_subactions + except AttributeError: + pass + else: + self._indent() + for subaction in get_subactions(): + yield subaction + self._dedent() + + def _split_lines(self, text, width): + text = self._whitespace_matcher.sub(' ', text).strip() + return _textwrap.wrap(text, width) + + def _fill_text(self, text, width, indent): + text = self._whitespace_matcher.sub(' ', text).strip() + return _textwrap.fill(text, width, initial_indent=indent, + subsequent_indent=indent) + + def _get_help_string(self, action): + return action.help + + +class RawDescriptionHelpFormatter(HelpFormatter): + """Help message formatter which retains any formatting in descriptions. + + Only the name of this class is considered a public API. All the methods + provided by the class are considered an implementation detail. + """ + + def _fill_text(self, text, width, indent): + return ''.join([indent + line for line in text.splitlines(True)]) + + +class RawTextHelpFormatter(RawDescriptionHelpFormatter): + """Help message formatter which retains formatting of all help text. + + Only the name of this class is considered a public API. All the methods + provided by the class are considered an implementation detail. + """ + + def _split_lines(self, text, width): + return text.splitlines() + + +class ArgumentDefaultsHelpFormatter(HelpFormatter): + """Help message formatter which adds default values to argument help. + + Only the name of this class is considered a public API. All the methods + provided by the class are considered an implementation detail. + """ + + def _get_help_string(self, action): + help = action.help + if '%(default)' not in action.help: + if action.default is not SUPPRESS: + defaulting_nargs = [OPTIONAL, ZERO_OR_MORE] + if action.option_strings or action.nargs in defaulting_nargs: + help += ' (default: %(default)s)' + return help + + +# ===================== +# Options and Arguments +# ===================== + +def _get_action_name(argument): + if argument is None: + return None + elif argument.option_strings: + return '/'.join(argument.option_strings) + elif argument.metavar not in (None, SUPPRESS): + return argument.metavar + elif argument.dest not in (None, SUPPRESS): + return argument.dest + else: + return None + + +class ArgumentError(Exception): + """An error from creating or using an argument (optional or positional). + + The string value of this exception is the message, augmented with + information about the argument that caused it. + """ + + def __init__(self, argument, message): + self.argument_name = _get_action_name(argument) + self.message = message + + def __str__(self): + if self.argument_name is None: + format = '%(message)s' + else: + format = 'argument %(argument_name)s: %(message)s' + return format % dict(message=self.message, + argument_name=self.argument_name) + + +class ArgumentTypeError(Exception): + """An error from trying to convert a command line string to a type.""" + pass + + +# ============== +# Action classes +# ============== + +class Action(_AttributeHolder): + """Information about how to convert command line strings to Python objects. + + Action objects are used by an ArgumentParser to represent the information + needed to parse a single argument from one or more strings from the + command line. The keyword arguments to the Action constructor are also + all attributes of Action instances. + + Keyword Arguments: + + - option_strings -- A list of command-line option strings which + should be associated with this action. + + - dest -- The name of the attribute to hold the created object(s) + + - nargs -- The number of command-line arguments that should be + consumed. By default, one argument will be consumed and a single + value will be produced. Other values include: + - N (an integer) consumes N arguments (and produces a list) + - '?' consumes zero or one arguments + - '*' consumes zero or more arguments (and produces a list) + - '+' consumes one or more arguments (and produces a list) + Note that the difference between the default and nargs=1 is that + with the default, a single value will be produced, while with + nargs=1, a list containing a single value will be produced. + + - const -- The value to be produced if the option is specified and the + option uses an action that takes no values. + + - default -- The value to be produced if the option is not specified. + + - type -- The type which the command-line arguments should be converted + to, should be one of 'string', 'int', 'float', 'complex' or a + callable object that accepts a single string argument. If None, + 'string' is assumed. + + - choices -- A container of values that should be allowed. If not None, + after a command-line argument has been converted to the appropriate + type, an exception will be raised if it is not a member of this + collection. + + - required -- True if the action must always be specified at the + command line. This is only meaningful for optional command-line + arguments. + + - help -- The help string describing the argument. + + - metavar -- The name to be used for the option's argument with the + help string. If None, the 'dest' value will be used as the name. + """ + + def __init__(self, + option_strings, + dest, + nargs=None, + const=None, + default=None, + type=None, + choices=None, + required=False, + help=None, + metavar=None): + self.option_strings = option_strings + self.dest = dest + self.nargs = nargs + self.const = const + self.default = default + self.type = type + self.choices = choices + self.required = required + self.help = help + self.metavar = metavar + + def _get_kwargs(self): + names = [ + 'option_strings', + 'dest', + 'nargs', + 'const', + 'default', + 'type', + 'choices', + 'help', + 'metavar', + ] + return [(name, getattr(self, name)) for name in names] + + def __call__(self, parser, namespace, values, option_string=None): + raise NotImplementedError(_('.__call__() not defined')) + + +class _StoreAction(Action): + + def __init__(self, + option_strings, + dest, + nargs=None, + const=None, + default=None, + type=None, + choices=None, + required=False, + help=None, + metavar=None): + if nargs == 0: + raise ValueError('nargs for store actions must be > 0; if you ' + 'have nothing to store, actions such as store ' + 'true or store const may be more appropriate') + if const is not None and nargs != OPTIONAL: + raise ValueError('nargs must be %r to supply const' % OPTIONAL) + super(_StoreAction, self).__init__( + option_strings=option_strings, + dest=dest, + nargs=nargs, + const=const, + default=default, + type=type, + choices=choices, + required=required, + help=help, + metavar=metavar) + + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, values) + + +class _StoreConstAction(Action): + + def __init__(self, + option_strings, + dest, + const, + default=None, + required=False, + help=None, + metavar=None): + super(_StoreConstAction, self).__init__( + option_strings=option_strings, + dest=dest, + nargs=0, + const=const, + default=default, + required=required, + help=help) + + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, self.const) + + +class _StoreTrueAction(_StoreConstAction): + + def __init__(self, + option_strings, + dest, + default=False, + required=False, + help=None): + super(_StoreTrueAction, self).__init__( + option_strings=option_strings, + dest=dest, + const=True, + default=default, + required=required, + help=help) + + +class _StoreFalseAction(_StoreConstAction): + + def __init__(self, + option_strings, + dest, + default=True, + required=False, + help=None): + super(_StoreFalseAction, self).__init__( + option_strings=option_strings, + dest=dest, + const=False, + default=default, + required=required, + help=help) + + +class _AppendAction(Action): + + def __init__(self, + option_strings, + dest, + nargs=None, + const=None, + default=None, + type=None, + choices=None, + required=False, + help=None, + metavar=None): + if nargs == 0: + raise ValueError('nargs for append actions must be > 0; if arg ' + 'strings are not supplying the value to append, ' + 'the append const action may be more appropriate') + if const is not None and nargs != OPTIONAL: + raise ValueError('nargs must be %r to supply const' % OPTIONAL) + super(_AppendAction, self).__init__( + option_strings=option_strings, + dest=dest, + nargs=nargs, + const=const, + default=default, + type=type, + choices=choices, + required=required, + help=help, + metavar=metavar) + + def __call__(self, parser, namespace, values, option_string=None): + items = _copy.copy(_ensure_value(namespace, self.dest, [])) + items.append(values) + setattr(namespace, self.dest, items) + + +class _AppendConstAction(Action): + + def __init__(self, + option_strings, + dest, + const, + default=None, + required=False, + help=None, + metavar=None): + super(_AppendConstAction, self).__init__( + option_strings=option_strings, + dest=dest, + nargs=0, + const=const, + default=default, + required=required, + help=help, + metavar=metavar) + + def __call__(self, parser, namespace, values, option_string=None): + items = _copy.copy(_ensure_value(namespace, self.dest, [])) + items.append(self.const) + setattr(namespace, self.dest, items) + + +class _CountAction(Action): + + def __init__(self, + option_strings, + dest, + default=None, + required=False, + help=None): + super(_CountAction, self).__init__( + option_strings=option_strings, + dest=dest, + nargs=0, + default=default, + required=required, + help=help) + + def __call__(self, parser, namespace, values, option_string=None): + new_count = _ensure_value(namespace, self.dest, 0) + 1 + setattr(namespace, self.dest, new_count) + + +class _HelpAction(Action): + + def __init__(self, + option_strings, + dest=SUPPRESS, + default=SUPPRESS, + help=None): + super(_HelpAction, self).__init__( + option_strings=option_strings, + dest=dest, + default=default, + nargs=0, + help=help) + + def __call__(self, parser, namespace, values, option_string=None): + parser.print_help() + parser.exit() + + +class _VersionAction(Action): + + def __init__(self, + option_strings, + version=None, + dest=SUPPRESS, + default=SUPPRESS, + help="show program's version number and exit"): + super(_VersionAction, self).__init__( + option_strings=option_strings, + dest=dest, + default=default, + nargs=0, + help=help) + self.version = version + + def __call__(self, parser, namespace, values, option_string=None): + version = self.version + if version is None: + version = parser.version + formatter = parser._get_formatter() + formatter.add_text(version) + parser.exit(message=formatter.format_help()) + + +class _SubParsersAction(Action): + + class _ChoicesPseudoAction(Action): + + def __init__(self, name, aliases, help): + metavar = dest = name + if aliases: + metavar += ' (%s)' % ', '.join(aliases) + sup = super(_SubParsersAction._ChoicesPseudoAction, self) + sup.__init__(option_strings=[], dest=dest, help=help, + metavar=metavar) + + def __init__(self, + option_strings, + prog, + parser_class, + dest=SUPPRESS, + help=None, + metavar=None): + + self._prog_prefix = prog + self._parser_class = parser_class + self._name_parser_map = {} + self._choices_actions = [] + + super(_SubParsersAction, self).__init__( + option_strings=option_strings, + dest=dest, + nargs=PARSER, + choices=self._name_parser_map, + help=help, + metavar=metavar) + + def add_parser(self, name, **kwargs): + # set prog from the existing prefix + if kwargs.get('prog') is None: + kwargs['prog'] = '%s %s' % (self._prog_prefix, name) + + aliases = kwargs.pop('aliases', ()) + + # create a pseudo-action to hold the choice help + if 'help' in kwargs: + help = kwargs.pop('help') + choice_action = self._ChoicesPseudoAction(name, aliases, help) + self._choices_actions.append(choice_action) + + # create the parser and add it to the map + parser = self._parser_class(**kwargs) + self._name_parser_map[name] = parser + + # make parser available under aliases also + for alias in aliases: + self._name_parser_map[alias] = parser + + return parser + + def _get_subactions(self): + return self._choices_actions + + def __call__(self, parser, namespace, values, option_string=None): + parser_name = values[0] + arg_strings = values[1:] + + # set the parser name if requested + if self.dest is not SUPPRESS: + setattr(namespace, self.dest, parser_name) + + # select the parser + try: + parser = self._name_parser_map[parser_name] + except KeyError: + tup = parser_name, ', '.join(self._name_parser_map) + msg = _('unknown parser %r (choices: %s)' % tup) + raise ArgumentError(self, msg) + + # parse all the remaining options into the namespace + # store any unrecognized options on the object, so that the top + # level parser can decide what to do with them + namespace, arg_strings = parser.parse_known_args(arg_strings, namespace) + if arg_strings: + vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, []) + getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings) + + +# ============== +# Type classes +# ============== + +class FileType(object): + """Factory for creating file object types + + Instances of FileType are typically passed as type= arguments to the + ArgumentParser add_argument() method. + + Keyword Arguments: + - mode -- A string indicating how the file is to be opened. Accepts the + same values as the builtin open() function. + - bufsize -- The file's desired buffer size. Accepts the same values as + the builtin open() function. + """ + + def __init__(self, mode='r', bufsize=None): + self._mode = mode + self._bufsize = bufsize + + def __call__(self, string): + # the special argument "-" means sys.std{in,out} + if string == '-': + if 'r' in self._mode: + return _sys.stdin + elif 'w' in self._mode: + return _sys.stdout + else: + msg = _('argument "-" with mode %r' % self._mode) + raise ValueError(msg) + + try: + # all other arguments are used as file names + if self._bufsize: + return open(string, self._mode, self._bufsize) + else: + return open(string, self._mode) + except IOError: + err = _sys.exc_info()[1] + message = _("can't open '%s': %s") + raise ArgumentTypeError(message % (string, err)) + + def __repr__(self): + args = [self._mode, self._bufsize] + args_str = ', '.join([repr(arg) for arg in args if arg is not None]) + return '%s(%s)' % (type(self).__name__, args_str) + +# =========================== +# Optional and Positional Parsing +# =========================== + +class Namespace(_AttributeHolder): + """Simple object for storing attributes. + + Implements equality by attribute names and values, and provides a simple + string representation. + """ + + def __init__(self, **kwargs): + for name in kwargs: + setattr(self, name, kwargs[name]) + + __hash__ = None + + def __eq__(self, other): + return vars(self) == vars(other) + + def __ne__(self, other): + return not (self == other) + + def __contains__(self, key): + return key in self.__dict__ + + +class _ActionsContainer(object): + + def __init__(self, + description, + prefix_chars, + argument_default, + conflict_handler): + super(_ActionsContainer, self).__init__() + + self.description = description + self.argument_default = argument_default + self.prefix_chars = prefix_chars + self.conflict_handler = conflict_handler + + # set up registries + self._registries = {} + + # register actions + self.register('action', None, _StoreAction) + self.register('action', 'store', _StoreAction) + self.register('action', 'store_const', _StoreConstAction) + self.register('action', 'store_true', _StoreTrueAction) + self.register('action', 'store_false', _StoreFalseAction) + self.register('action', 'append', _AppendAction) + self.register('action', 'append_const', _AppendConstAction) + self.register('action', 'count', _CountAction) + self.register('action', 'help', _HelpAction) + self.register('action', 'version', _VersionAction) + self.register('action', 'parsers', _SubParsersAction) + + # raise an exception if the conflict handler is invalid + self._get_handler() + + # action storage + self._actions = [] + self._option_string_actions = {} + + # groups + self._action_groups = [] + self._mutually_exclusive_groups = [] + + # defaults storage + self._defaults = {} + + # determines whether an "option" looks like a negative number + self._negative_number_matcher = _re.compile(r'^-\d+$|^-\d*\.\d+$') + + # whether or not there are any optionals that look like negative + # numbers -- uses a list so it can be shared and edited + self._has_negative_number_optionals = [] + + # ==================== + # Registration methods + # ==================== + def register(self, registry_name, value, object): + registry = self._registries.setdefault(registry_name, {}) + registry[value] = object + + def _registry_get(self, registry_name, value, default=None): + return self._registries[registry_name].get(value, default) + + # ================================== + # Namespace default accessor methods + # ================================== + def set_defaults(self, **kwargs): + self._defaults.update(kwargs) + + # if these defaults match any existing arguments, replace + # the previous default on the object with the new one + for action in self._actions: + if action.dest in kwargs: + action.default = kwargs[action.dest] + + def get_default(self, dest): + for action in self._actions: + if action.dest == dest and action.default is not None: + return action.default + return self._defaults.get(dest, None) + + + # ======================= + # Adding argument actions + # ======================= + def add_argument(self, *args, **kwargs): + """ + add_argument(dest, ..., name=value, ...) + add_argument(option_string, option_string, ..., name=value, ...) + """ + + # if no positional args are supplied or only one is supplied and + # it doesn't look like an option string, parse a positional + # argument + chars = self.prefix_chars + if not args or len(args) == 1 and args[0][0] not in chars: + if args and 'dest' in kwargs: + raise ValueError('dest supplied twice for positional argument') + kwargs = self._get_positional_kwargs(*args, **kwargs) + + # otherwise, we're adding an optional argument + else: + kwargs = self._get_optional_kwargs(*args, **kwargs) + + # if no default was supplied, use the parser-level default + if 'default' not in kwargs: + dest = kwargs['dest'] + if dest in self._defaults: + kwargs['default'] = self._defaults[dest] + elif self.argument_default is not None: + kwargs['default'] = self.argument_default + + # create the action object, and add it to the parser + action_class = self._pop_action_class(kwargs) + if not _callable(action_class): + raise ValueError('unknown action "%s"' % action_class) + action = action_class(**kwargs) + + # raise an error if the action type is not callable + type_func = self._registry_get('type', action.type, action.type) + if not _callable(type_func): + raise ValueError('%r is not callable' % type_func) + + return self._add_action(action) + + def add_argument_group(self, *args, **kwargs): + group = _ArgumentGroup(self, *args, **kwargs) + self._action_groups.append(group) + return group + + def add_mutually_exclusive_group(self, **kwargs): + group = _MutuallyExclusiveGroup(self, **kwargs) + self._mutually_exclusive_groups.append(group) + return group + + def _add_action(self, action): + # resolve any conflicts + self._check_conflict(action) + + # add to actions list + self._actions.append(action) + action.container = self + + # index the action by any option strings it has + for option_string in action.option_strings: + self._option_string_actions[option_string] = action + + # set the flag if any option strings look like negative numbers + for option_string in action.option_strings: + if self._negative_number_matcher.match(option_string): + if not self._has_negative_number_optionals: + self._has_negative_number_optionals.append(True) + + # return the created action + return action + + def _remove_action(self, action): + self._actions.remove(action) + + def _add_container_actions(self, container): + # collect groups by titles + title_group_map = {} + for group in self._action_groups: + if group.title in title_group_map: + msg = _('cannot merge actions - two groups are named %r') + raise ValueError(msg % (group.title)) + title_group_map[group.title] = group + + # map each action to its group + group_map = {} + for group in container._action_groups: + + # if a group with the title exists, use that, otherwise + # create a new group matching the container's group + if group.title not in title_group_map: + title_group_map[group.title] = self.add_argument_group( + title=group.title, + description=group.description, + conflict_handler=group.conflict_handler) + + # map the actions to their new group + for action in group._group_actions: + group_map[action] = title_group_map[group.title] + + # add container's mutually exclusive groups + # NOTE: if add_mutually_exclusive_group ever gains title= and + # description= then this code will need to be expanded as above + for group in container._mutually_exclusive_groups: + mutex_group = self.add_mutually_exclusive_group( + required=group.required) + + # map the actions to their new mutex group + for action in group._group_actions: + group_map[action] = mutex_group + + # add all actions to this container or their group + for action in container._actions: + group_map.get(action, self)._add_action(action) + + def _get_positional_kwargs(self, dest, **kwargs): + # make sure required is not specified + if 'required' in kwargs: + msg = _("'required' is an invalid argument for positionals") + raise TypeError(msg) + + # mark positional arguments as required if at least one is + # always required + if kwargs.get('nargs') not in [OPTIONAL, ZERO_OR_MORE]: + kwargs['required'] = True + if kwargs.get('nargs') == ZERO_OR_MORE and 'default' not in kwargs: + kwargs['required'] = True + + # return the keyword arguments with no option strings + return dict(kwargs, dest=dest, option_strings=[]) + + def _get_optional_kwargs(self, *args, **kwargs): + # determine short and long option strings + option_strings = [] + long_option_strings = [] + for option_string in args: + # error on strings that don't start with an appropriate prefix + if not option_string[0] in self.prefix_chars: + msg = _('invalid option string %r: ' + 'must start with a character %r') + tup = option_string, self.prefix_chars + raise ValueError(msg % tup) + + # strings starting with two prefix characters are long options + option_strings.append(option_string) + if option_string[0] in self.prefix_chars: + if len(option_string) > 1: + if option_string[1] in self.prefix_chars: + long_option_strings.append(option_string) + + # infer destination, '--foo-bar' -> 'foo_bar' and '-x' -> 'x' + dest = kwargs.pop('dest', None) + if dest is None: + if long_option_strings: + dest_option_string = long_option_strings[0] + else: + dest_option_string = option_strings[0] + dest = dest_option_string.lstrip(self.prefix_chars) + if not dest: + msg = _('dest= is required for options like %r') + raise ValueError(msg % option_string) + dest = dest.replace('-', '_') + + # return the updated keyword arguments + return dict(kwargs, dest=dest, option_strings=option_strings) + + def _pop_action_class(self, kwargs, default=None): + action = kwargs.pop('action', default) + return self._registry_get('action', action, action) + + def _get_handler(self): + # determine function from conflict handler string + handler_func_name = '_handle_conflict_%s' % self.conflict_handler + try: + return getattr(self, handler_func_name) + except AttributeError: + msg = _('invalid conflict_resolution value: %r') + raise ValueError(msg % self.conflict_handler) + + def _check_conflict(self, action): + + # find all options that conflict with this option + confl_optionals = [] + for option_string in action.option_strings: + if option_string in self._option_string_actions: + confl_optional = self._option_string_actions[option_string] + confl_optionals.append((option_string, confl_optional)) + + # resolve any conflicts + if confl_optionals: + conflict_handler = self._get_handler() + conflict_handler(action, confl_optionals) + + def _handle_conflict_error(self, action, conflicting_actions): + message = _('conflicting option string(s): %s') + conflict_string = ', '.join([option_string + for option_string, action + in conflicting_actions]) + raise ArgumentError(action, message % conflict_string) + + def _handle_conflict_resolve(self, action, conflicting_actions): + + # remove all conflicting options + for option_string, action in conflicting_actions: + + # remove the conflicting option + action.option_strings.remove(option_string) + self._option_string_actions.pop(option_string, None) + + # if the option now has no option string, remove it from the + # container holding it + if not action.option_strings: + action.container._remove_action(action) + + +class _ArgumentGroup(_ActionsContainer): + + def __init__(self, container, title=None, description=None, **kwargs): + # add any missing keyword arguments by checking the container + update = kwargs.setdefault + update('conflict_handler', container.conflict_handler) + update('prefix_chars', container.prefix_chars) + update('argument_default', container.argument_default) + super_init = super(_ArgumentGroup, self).__init__ + super_init(description=description, **kwargs) + + # group attributes + self.title = title + self._group_actions = [] + + # share most attributes with the container + self._registries = container._registries + self._actions = container._actions + self._option_string_actions = container._option_string_actions + self._defaults = container._defaults + self._has_negative_number_optionals = \ + container._has_negative_number_optionals + + def _add_action(self, action): + action = super(_ArgumentGroup, self)._add_action(action) + self._group_actions.append(action) + return action + + def _remove_action(self, action): + super(_ArgumentGroup, self)._remove_action(action) + self._group_actions.remove(action) + + +class _MutuallyExclusiveGroup(_ArgumentGroup): + + def __init__(self, container, required=False): + super(_MutuallyExclusiveGroup, self).__init__(container) + self.required = required + self._container = container + + def _add_action(self, action): + if action.required: + msg = _('mutually exclusive arguments must be optional') + raise ValueError(msg) + action = self._container._add_action(action) + self._group_actions.append(action) + return action + + def _remove_action(self, action): + self._container._remove_action(action) + self._group_actions.remove(action) + + +class ArgumentParser(_AttributeHolder, _ActionsContainer): + """Object for parsing command line strings into Python objects. + + Keyword Arguments: + - prog -- The name of the program (default: sys.argv[0]) + - usage -- A usage message (default: auto-generated from arguments) + - description -- A description of what the program does + - epilog -- Text following the argument descriptions + - parents -- Parsers whose arguments should be copied into this one + - formatter_class -- HelpFormatter class for printing help messages + - prefix_chars -- Characters that prefix optional arguments + - fromfile_prefix_chars -- Characters that prefix files containing + additional arguments + - argument_default -- The default value for all arguments + - conflict_handler -- String indicating how to handle conflicts + - add_help -- Add a -h/-help option + """ + + def __init__(self, + prog=None, + usage=None, + description=None, + epilog=None, + version=None, + parents=[], + formatter_class=HelpFormatter, + prefix_chars='-', + fromfile_prefix_chars=None, + argument_default=None, + conflict_handler='error', + add_help=True): + + if version is not None: + import warnings + warnings.warn( + """The "version" argument to ArgumentParser is deprecated. """ + """Please use """ + """"add_argument(..., action='version', version="N", ...)" """ + """instead""", DeprecationWarning) + + superinit = super(ArgumentParser, self).__init__ + superinit(description=description, + prefix_chars=prefix_chars, + argument_default=argument_default, + conflict_handler=conflict_handler) + + # default setting for prog + if prog is None: + prog = _os.path.basename(_sys.argv[0]) + + self.prog = prog + self.usage = usage + self.epilog = epilog + self.version = version + self.formatter_class = formatter_class + self.fromfile_prefix_chars = fromfile_prefix_chars + self.add_help = add_help + + add_group = self.add_argument_group + self._positionals = add_group(_('positional arguments')) + self._optionals = add_group(_('optional arguments')) + self._subparsers = None + + # register types + def identity(string): + return string + self.register('type', None, identity) + + # add help and version arguments if necessary + # (using explicit default to override global argument_default) + if '-' in prefix_chars: + default_prefix = '-' + else: + default_prefix = prefix_chars[0] + if self.add_help: + self.add_argument( + default_prefix+'h', default_prefix*2+'help', + action='help', default=SUPPRESS, + help=_('show this help message and exit')) + if self.version: + self.add_argument( + default_prefix+'v', default_prefix*2+'version', + action='version', default=SUPPRESS, + version=self.version, + help=_("show program's version number and exit")) + + # add parent arguments and defaults + for parent in parents: + self._add_container_actions(parent) + try: + defaults = parent._defaults + except AttributeError: + pass + else: + self._defaults.update(defaults) + + # ======================= + # Pretty __repr__ methods + # ======================= + def _get_kwargs(self): + names = [ + 'prog', + 'usage', + 'description', + 'version', + 'formatter_class', + 'conflict_handler', + 'add_help', + ] + return [(name, getattr(self, name)) for name in names] + + # ================================== + # Optional/Positional adding methods + # ================================== + def add_subparsers(self, **kwargs): + if self._subparsers is not None: + self.error(_('cannot have multiple subparser arguments')) + + # add the parser class to the arguments if it's not present + kwargs.setdefault('parser_class', type(self)) + + if 'title' in kwargs or 'description' in kwargs: + title = _(kwargs.pop('title', 'subcommands')) + description = _(kwargs.pop('description', None)) + self._subparsers = self.add_argument_group(title, description) + else: + self._subparsers = self._positionals + + # prog defaults to the usage message of this parser, skipping + # optional arguments and with no "usage:" prefix + if kwargs.get('prog') is None: + formatter = self._get_formatter() + positionals = self._get_positional_actions() + groups = self._mutually_exclusive_groups + formatter.add_usage(self.usage, positionals, groups, '') + kwargs['prog'] = formatter.format_help().strip() + + # create the parsers action and add it to the positionals list + parsers_class = self._pop_action_class(kwargs, 'parsers') + action = parsers_class(option_strings=[], **kwargs) + self._subparsers._add_action(action) + + # return the created parsers action + return action + + def _add_action(self, action): + if action.option_strings: + self._optionals._add_action(action) + else: + self._positionals._add_action(action) + return action + + def _get_optional_actions(self): + return [action + for action in self._actions + if action.option_strings] + + def _get_positional_actions(self): + return [action + for action in self._actions + if not action.option_strings] + + # ===================================== + # Command line argument parsing methods + # ===================================== + def parse_args(self, args=None, namespace=None): + args, argv = self.parse_known_args(args, namespace) + if argv: + msg = _('unrecognized arguments: %s') + self.error(msg % ' '.join(argv)) + return args + + def parse_known_args(self, args=None, namespace=None): + # args default to the system args + if args is None: + args = _sys.argv[1:] + + # default Namespace built from parser defaults + if namespace is None: + namespace = Namespace() + + # add any action defaults that aren't present + for action in self._actions: + if action.dest is not SUPPRESS: + if not hasattr(namespace, action.dest): + if action.default is not SUPPRESS: + setattr(namespace, action.dest, action.default) + + # add any parser defaults that aren't present + for dest in self._defaults: + if not hasattr(namespace, dest): + setattr(namespace, dest, self._defaults[dest]) + + # parse the arguments and exit if there are any errors + try: + namespace, args = self._parse_known_args(args, namespace) + if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR): + args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR)) + delattr(namespace, _UNRECOGNIZED_ARGS_ATTR) + return namespace, args + except ArgumentError: + err = _sys.exc_info()[1] + self.error(str(err)) + + def _parse_known_args(self, arg_strings, namespace): + # replace arg strings that are file references + if self.fromfile_prefix_chars is not None: + arg_strings = self._read_args_from_files(arg_strings) + + # map all mutually exclusive arguments to the other arguments + # they can't occur with + action_conflicts = {} + for mutex_group in self._mutually_exclusive_groups: + group_actions = mutex_group._group_actions + for i, mutex_action in enumerate(mutex_group._group_actions): + conflicts = action_conflicts.setdefault(mutex_action, []) + conflicts.extend(group_actions[:i]) + conflicts.extend(group_actions[i + 1:]) + + # find all option indices, and determine the arg_string_pattern + # which has an 'O' if there is an option at an index, + # an 'A' if there is an argument, or a '-' if there is a '--' + option_string_indices = {} + arg_string_pattern_parts = [] + arg_strings_iter = iter(arg_strings) + for i, arg_string in enumerate(arg_strings_iter): + + # all args after -- are non-options + if arg_string == '--': + arg_string_pattern_parts.append('-') + for arg_string in arg_strings_iter: + arg_string_pattern_parts.append('A') + + # otherwise, add the arg to the arg strings + # and note the index if it was an option + else: + option_tuple = self._parse_optional(arg_string) + if option_tuple is None: + pattern = 'A' + else: + option_string_indices[i] = option_tuple + pattern = 'O' + arg_string_pattern_parts.append(pattern) + + # join the pieces together to form the pattern + arg_strings_pattern = ''.join(arg_string_pattern_parts) + + # converts arg strings to the appropriate and then takes the action + seen_actions = set() + seen_non_default_actions = set() + + def take_action(action, argument_strings, option_string=None): + seen_actions.add(action) + argument_values = self._get_values(action, argument_strings) + + # error if this argument is not allowed with other previously + # seen arguments, assuming that actions that use the default + # value don't really count as "present" + if argument_values is not action.default: + seen_non_default_actions.add(action) + for conflict_action in action_conflicts.get(action, []): + if conflict_action in seen_non_default_actions: + msg = _('not allowed with argument %s') + action_name = _get_action_name(conflict_action) + raise ArgumentError(action, msg % action_name) + + # take the action if we didn't receive a SUPPRESS value + # (e.g. from a default) + if argument_values is not SUPPRESS: + action(self, namespace, argument_values, option_string) + + # function to convert arg_strings into an optional action + def consume_optional(start_index): + + # get the optional identified at this index + option_tuple = option_string_indices[start_index] + action, option_string, explicit_arg = option_tuple + + # identify additional optionals in the same arg string + # (e.g. -xyz is the same as -x -y -z if no args are required) + match_argument = self._match_argument + action_tuples = [] + while True: + + # if we found no optional action, skip it + if action is None: + extras.append(arg_strings[start_index]) + return start_index + 1 + + # if there is an explicit argument, try to match the + # optional's string arguments to only this + if explicit_arg is not None: + arg_count = match_argument(action, 'A') + + # if the action is a single-dash option and takes no + # arguments, try to parse more single-dash options out + # of the tail of the option string + chars = self.prefix_chars + if arg_count == 0 and option_string[1] not in chars: + action_tuples.append((action, [], option_string)) + char = option_string[0] + option_string = char + explicit_arg[0] + new_explicit_arg = explicit_arg[1:] or None + optionals_map = self._option_string_actions + if option_string in optionals_map: + action = optionals_map[option_string] + explicit_arg = new_explicit_arg + else: + msg = _('ignored explicit argument %r') + raise ArgumentError(action, msg % explicit_arg) + + # if the action expect exactly one argument, we've + # successfully matched the option; exit the loop + elif arg_count == 1: + stop = start_index + 1 + args = [explicit_arg] + action_tuples.append((action, args, option_string)) + break + + # error if a double-dash option did not use the + # explicit argument + else: + msg = _('ignored explicit argument %r') + raise ArgumentError(action, msg % explicit_arg) + + # if there is no explicit argument, try to match the + # optional's string arguments with the following strings + # if successful, exit the loop + else: + start = start_index + 1 + selected_patterns = arg_strings_pattern[start:] + arg_count = match_argument(action, selected_patterns) + stop = start + arg_count + args = arg_strings[start:stop] + action_tuples.append((action, args, option_string)) + break + + # add the Optional to the list and return the index at which + # the Optional's string args stopped + assert action_tuples + for action, args, option_string in action_tuples: + take_action(action, args, option_string) + return stop + + # the list of Positionals left to be parsed; this is modified + # by consume_positionals() + positionals = self._get_positional_actions() + + # function to convert arg_strings into positional actions + def consume_positionals(start_index): + # match as many Positionals as possible + match_partial = self._match_arguments_partial + selected_pattern = arg_strings_pattern[start_index:] + arg_counts = match_partial(positionals, selected_pattern) + + # slice off the appropriate arg strings for each Positional + # and add the Positional and its args to the list + for action, arg_count in zip(positionals, arg_counts): + args = arg_strings[start_index: start_index + arg_count] + start_index += arg_count + take_action(action, args) + + # slice off the Positionals that we just parsed and return the + # index at which the Positionals' string args stopped + positionals[:] = positionals[len(arg_counts):] + return start_index + + # consume Positionals and Optionals alternately, until we have + # passed the last option string + extras = [] + start_index = 0 + if option_string_indices: + max_option_string_index = max(option_string_indices) + else: + max_option_string_index = -1 + while start_index <= max_option_string_index: + + # consume any Positionals preceding the next option + next_option_string_index = min([ + index + for index in option_string_indices + if index >= start_index]) + if start_index != next_option_string_index: + positionals_end_index = consume_positionals(start_index) + + # only try to parse the next optional if we didn't consume + # the option string during the positionals parsing + if positionals_end_index > start_index: + start_index = positionals_end_index + continue + else: + start_index = positionals_end_index + + # if we consumed all the positionals we could and we're not + # at the index of an option string, there were extra arguments + if start_index not in option_string_indices: + strings = arg_strings[start_index:next_option_string_index] + extras.extend(strings) + start_index = next_option_string_index + + # consume the next optional and any arguments for it + start_index = consume_optional(start_index) + + # consume any positionals following the last Optional + stop_index = consume_positionals(start_index) + + # if we didn't consume all the argument strings, there were extras + extras.extend(arg_strings[stop_index:]) + + # if we didn't use all the Positional objects, there were too few + # arg strings supplied. + if positionals: + self.error(_('too few arguments')) + + # make sure all required actions were present, and convert defaults. + for action in self._actions: + if action not in seen_actions: + if action.required: + name = _get_action_name(action) + self.error(_('argument %s is required') % name) + else: + # Convert action default now instead of doing it before + # parsing arguments to avoid calling convert functions + # twice (which may fail) if the argument was given, but + # only if it was defined already in the namespace + if (action.default is not None and + isinstance(action.default, basestring) and + hasattr(namespace, action.dest) and + action.default is getattr(namespace, action.dest)): + setattr(namespace, action.dest, + self._get_value(action, action.default)) + + # make sure all required groups had one option present + for group in self._mutually_exclusive_groups: + if group.required: + for action in group._group_actions: + if action in seen_non_default_actions: + break + + # if no actions were used, report the error + else: + names = [_get_action_name(action) + for action in group._group_actions + if action.help is not SUPPRESS] + msg = _('one of the arguments %s is required') + self.error(msg % ' '.join(names)) + + # return the updated namespace and the extra arguments + return namespace, extras + + def _read_args_from_files(self, arg_strings): + # expand arguments referencing files + new_arg_strings = [] + for arg_string in arg_strings: + + # for regular arguments, just add them back into the list + if arg_string[0] not in self.fromfile_prefix_chars: + new_arg_strings.append(arg_string) + + # replace arguments referencing files with the file content + else: + try: + args_file = open(arg_string[1:]) + try: + arg_strings = [] + for arg_line in args_file.read().splitlines(): + for arg in self.convert_arg_line_to_args(arg_line): + arg_strings.append(arg) + arg_strings = self._read_args_from_files(arg_strings) + new_arg_strings.extend(arg_strings) + finally: + args_file.close() + except IOError: + err = _sys.exc_info()[1] + self.error(str(err)) + + # return the modified argument list + return new_arg_strings + + def convert_arg_line_to_args(self, arg_line): + return [arg_line] + + def _match_argument(self, action, arg_strings_pattern): + # match the pattern for this action to the arg strings + nargs_pattern = self._get_nargs_pattern(action) + match = _re.match(nargs_pattern, arg_strings_pattern) + + # raise an exception if we weren't able to find a match + if match is None: + nargs_errors = { + None: _('expected one argument'), + OPTIONAL: _('expected at most one argument'), + ONE_OR_MORE: _('expected at least one argument'), + } + default = _('expected %s argument(s)') % action.nargs + msg = nargs_errors.get(action.nargs, default) + raise ArgumentError(action, msg) + + # return the number of arguments matched + return len(match.group(1)) + + def _match_arguments_partial(self, actions, arg_strings_pattern): + # progressively shorten the actions list by slicing off the + # final actions until we find a match + result = [] + for i in range(len(actions), 0, -1): + actions_slice = actions[:i] + pattern = ''.join([self._get_nargs_pattern(action) + for action in actions_slice]) + match = _re.match(pattern, arg_strings_pattern) + if match is not None: + result.extend([len(string) for string in match.groups()]) + break + + # return the list of arg string counts + return result + + def _parse_optional(self, arg_string): + # if it's an empty string, it was meant to be a positional + if not arg_string: + return None + + # if it doesn't start with a prefix, it was meant to be positional + if not arg_string[0] in self.prefix_chars: + return None + + # if the option string is present in the parser, return the action + if arg_string in self._option_string_actions: + action = self._option_string_actions[arg_string] + return action, arg_string, None + + # if it's just a single character, it was meant to be positional + if len(arg_string) == 1: + return None + + # if the option string before the "=" is present, return the action + if '=' in arg_string: + option_string, explicit_arg = arg_string.split('=', 1) + if option_string in self._option_string_actions: + action = self._option_string_actions[option_string] + return action, option_string, explicit_arg + + # search through all possible prefixes of the option string + # and all actions in the parser for possible interpretations + option_tuples = self._get_option_tuples(arg_string) + + # if multiple actions match, the option string was ambiguous + if len(option_tuples) > 1: + options = ', '.join([option_string + for action, option_string, explicit_arg in option_tuples]) + tup = arg_string, options + self.error(_('ambiguous option: %s could match %s') % tup) + + # if exactly one action matched, this segmentation is good, + # so return the parsed action + elif len(option_tuples) == 1: + option_tuple, = option_tuples + return option_tuple + + # if it was not found as an option, but it looks like a negative + # number, it was meant to be positional + # unless there are negative-number-like options + if self._negative_number_matcher.match(arg_string): + if not self._has_negative_number_optionals: + return None + + # if it contains a space, it was meant to be a positional + if ' ' in arg_string: + return None + + # it was meant to be an optional but there is no such option + # in this parser (though it might be a valid option in a subparser) + return None, arg_string, None + + def _get_option_tuples(self, option_string): + result = [] + + # option strings starting with two prefix characters are only + # split at the '=' + chars = self.prefix_chars + if option_string[0] in chars and option_string[1] in chars: + if '=' in option_string: + option_prefix, explicit_arg = option_string.split('=', 1) + else: + option_prefix = option_string + explicit_arg = None + for option_string in self._option_string_actions: + if option_string.startswith(option_prefix): + action = self._option_string_actions[option_string] + tup = action, option_string, explicit_arg + result.append(tup) + + # single character options can be concatenated with their arguments + # but multiple character options always have to have their argument + # separate + elif option_string[0] in chars and option_string[1] not in chars: + option_prefix = option_string + explicit_arg = None + short_option_prefix = option_string[:2] + short_explicit_arg = option_string[2:] + + for option_string in self._option_string_actions: + if option_string == short_option_prefix: + action = self._option_string_actions[option_string] + tup = action, option_string, short_explicit_arg + result.append(tup) + elif option_string.startswith(option_prefix): + action = self._option_string_actions[option_string] + tup = action, option_string, explicit_arg + result.append(tup) + + # shouldn't ever get here + else: + self.error(_('unexpected option string: %s') % option_string) + + # return the collected option tuples + return result + + def _get_nargs_pattern(self, action): + # in all examples below, we have to allow for '--' args + # which are represented as '-' in the pattern + nargs = action.nargs + + # the default (None) is assumed to be a single argument + if nargs is None: + nargs_pattern = '(-*A-*)' + + # allow zero or one arguments + elif nargs == OPTIONAL: + nargs_pattern = '(-*A?-*)' + + # allow zero or more arguments + elif nargs == ZERO_OR_MORE: + nargs_pattern = '(-*[A-]*)' + + # allow one or more arguments + elif nargs == ONE_OR_MORE: + nargs_pattern = '(-*A[A-]*)' + + # allow any number of options or arguments + elif nargs == REMAINDER: + nargs_pattern = '([-AO]*)' + + # allow one argument followed by any number of options or arguments + elif nargs == PARSER: + nargs_pattern = '(-*A[-AO]*)' + + # all others should be integers + else: + nargs_pattern = '(-*%s-*)' % '-*'.join('A' * nargs) + + # if this is an optional action, -- is not allowed + if action.option_strings: + nargs_pattern = nargs_pattern.replace('-*', '') + nargs_pattern = nargs_pattern.replace('-', '') + + # return the pattern + return nargs_pattern + + # ======================== + # Value conversion methods + # ======================== + def _get_values(self, action, arg_strings): + # for everything but PARSER args, strip out '--' + if action.nargs not in [PARSER, REMAINDER]: + arg_strings = [s for s in arg_strings if s != '--'] + + # optional argument produces a default when not present + if not arg_strings and action.nargs == OPTIONAL: + if action.option_strings: + value = action.const + else: + value = action.default + if isinstance(value, basestring): + value = self._get_value(action, value) + self._check_value(action, value) + + # when nargs='*' on a positional, if there were no command-line + # args, use the default if it is anything other than None + elif (not arg_strings and action.nargs == ZERO_OR_MORE and + not action.option_strings): + if action.default is not None: + value = action.default + else: + value = arg_strings + self._check_value(action, value) + + # single argument or optional argument produces a single value + elif len(arg_strings) == 1 and action.nargs in [None, OPTIONAL]: + arg_string, = arg_strings + value = self._get_value(action, arg_string) + self._check_value(action, value) + + # REMAINDER arguments convert all values, checking none + elif action.nargs == REMAINDER: + value = [self._get_value(action, v) for v in arg_strings] + + # PARSER arguments convert all values, but check only the first + elif action.nargs == PARSER: + value = [self._get_value(action, v) for v in arg_strings] + self._check_value(action, value[0]) + + # all other types of nargs produce a list + else: + value = [self._get_value(action, v) for v in arg_strings] + for v in value: + self._check_value(action, v) + + # return the converted value + return value + + def _get_value(self, action, arg_string): + type_func = self._registry_get('type', action.type, action.type) + if not _callable(type_func): + msg = _('%r is not callable') + raise ArgumentError(action, msg % type_func) + + # convert the value to the appropriate type + try: + result = type_func(arg_string) + + # ArgumentTypeErrors indicate errors + except ArgumentTypeError: + name = getattr(action.type, '__name__', repr(action.type)) + msg = str(_sys.exc_info()[1]) + raise ArgumentError(action, msg) + + # TypeErrors or ValueErrors also indicate errors + except (TypeError, ValueError): + name = getattr(action.type, '__name__', repr(action.type)) + msg = _('invalid %s value: %r') + raise ArgumentError(action, msg % (name, arg_string)) + + # return the converted value + return result + + def _check_value(self, action, value): + # converted value must be one of the choices (if specified) + if action.choices is not None and value not in action.choices: + tup = value, ', '.join(map(repr, action.choices)) + msg = _('invalid choice: %r (choose from %s)') % tup + raise ArgumentError(action, msg) + + # ======================= + # Help-formatting methods + # ======================= + def format_usage(self): + formatter = self._get_formatter() + formatter.add_usage(self.usage, self._actions, + self._mutually_exclusive_groups) + return formatter.format_help() + + def format_help(self): + formatter = self._get_formatter() + + # usage + formatter.add_usage(self.usage, self._actions, + self._mutually_exclusive_groups) + + # description + formatter.add_text(self.description) + + # positionals, optionals and user-defined groups + for action_group in self._action_groups: + formatter.start_section(action_group.title) + formatter.add_text(action_group.description) + formatter.add_arguments(action_group._group_actions) + formatter.end_section() + + # epilog + formatter.add_text(self.epilog) + + # determine help from format above + return formatter.format_help() + + def format_version(self): + import warnings + warnings.warn( + 'The format_version method is deprecated -- the "version" ' + 'argument to ArgumentParser is no longer supported.', + DeprecationWarning) + formatter = self._get_formatter() + formatter.add_text(self.version) + return formatter.format_help() + + def _get_formatter(self): + return self.formatter_class(prog=self.prog) + + # ===================== + # Help-printing methods + # ===================== + def print_usage(self, file=None): + if file is None: + file = _sys.stdout + self._print_message(self.format_usage(), file) + + def print_help(self, file=None): + if file is None: + file = _sys.stdout + self._print_message(self.format_help(), file) + + def print_version(self, file=None): + import warnings + warnings.warn( + 'The print_version method is deprecated -- the "version" ' + 'argument to ArgumentParser is no longer supported.', + DeprecationWarning) + self._print_message(self.format_version(), file) + + def _print_message(self, message, file=None): + if message: + if file is None: + file = _sys.stderr + file.write(message) + + # =============== + # Exiting methods + # =============== + def exit(self, status=0, message=None): + if message: + self._print_message(message, _sys.stderr) + _sys.exit(status) + + def error(self, message): + """error(message: string) + + Prints a usage message incorporating the message to stderr and + exits. + + If you override this in a subclass, it should not return -- it + should either exit or raise an exception. + """ + self.print_usage(_sys.stderr) + self.exit(2, _('%s: error: %s\n') % (self.prog, message)) diff --git a/ui/keycodemapdb/tools/keymap-gen b/ui/keycodemapdb/tools/keymap-gen new file mode 100755 index 00000000..ac4b0574 --- /dev/null +++ b/ui/keycodemapdb/tools/keymap-gen @@ -0,0 +1,1196 @@ +#!/usr/bin/python +# -*- python -*- +# +# Keycode Map Generator +# +# Copyright (C) 2009-2017 Red Hat, Inc. +# +# This file is dual license under the terms of the GPLv2 or later +# and 3-clause BSD licenses. +# + +# Requires >= 2.6 +from __future__ import print_function + +import csv +try: + import argparse +except: + import os, sys + sys.path.append(os.path.join(os.path.dirname(__file__), "../thirdparty")) + import argparse +import hashlib +import time +import sys + +class Database: + + # Linux: linux/input.h + MAP_LINUX = "linux" + + # OS-X: Carbon/HIToolbox/Events.h + MAP_OSX = "osx" + + # AT Set 1: linux/drivers/input/keyboard/atkbd.c + # (atkbd_set2_keycode + atkbd_unxlate_table) + MAP_ATSET1 = "atset1" + + # AT Set 2: linux/drivers/input/keyboard/atkbd.c + # (atkbd_set2_keycode) + MAP_ATSET2 = "atset2" + + # AT Set 3: linux/drivers/input/keyboard/atkbd.c + # (atkbd_set3_keycode) + MAP_ATSET3 = "atset3" + + # Linux RAW: linux/drivers/char/keyboard.c (x86_keycodes) + MAP_XTKBD = "xtkbd" + + # USB HID: linux/drivers/hid/usbhid/usbkbd.c (usb_kbd_keycode) + MAP_USB = "usb" + + # Win32: mingw32/winuser.h + MAP_WIN32 = "win32" + + # XWin XT: xorg-server/hw/xwin/{winkeybd.c,winkeynames.h} + # (xt + manually transcribed) + MAP_XWINXT = "xwinxt" + + # X11: http://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h + MAP_X11 = "x11" + + # XKBD XT: xf86-input-keyboard/src/at_scancode.c + # (xt + manually transcribed) + MAP_XKBDXT = "xkbdxt" + + # Xorg with evdev: linux + an offset + MAP_XORGEVDEV = "xorgevdev" + + # Xorg with kbd: xkbdxt + an offset + MAP_XORGKBD = "xorgkbd" + + # Xorg with OS-X: osx + an offset + MAP_XORGXQUARTZ = "xorgxquartz" + + # Xorg + Cygwin: xwinxt + an offset + MAP_XORGXWIN = "xorgxwin" + + # QEMU key numbers: xtkbd + special re-encoding of high bit + MAP_QNUM = "qnum" + + # HTML codes + MAP_HTML = "html" + + # XKB key names + MAP_XKB = "xkb" + + # QEMU keycodes + MAP_QCODE = "qcode" + + # Sun / Sparc scan codes + # Reference: "SPARC International Keyboard Spec 1", page 7 "US scan set" + MAP_SUN = "sun" + + # Apple Desktop Bus + # Reference: http://www.archive.org/stream/apple-guide-macintosh-family-hardware/Apple_Guide_to_the_Macintosh_Family_Hardware_2e#page/n345/mode/2up + MAP_ADB = "adb" + + MAP_LIST = ( + MAP_LINUX, + MAP_OSX, + MAP_ATSET1, + MAP_ATSET2, + MAP_ATSET3, + MAP_USB, + MAP_WIN32, + MAP_XWINXT, + MAP_XKBDXT, + MAP_X11, + MAP_HTML, + MAP_XKB, + MAP_QCODE, + MAP_SUN, + MAP_ADB, + + # These are derived from maps above + MAP_XTKBD, + MAP_XORGEVDEV, + MAP_XORGKBD, + MAP_XORGXQUARTZ, + MAP_XORGXWIN, + MAP_QNUM, + ) + + CODE_COLUMNS = { + MAP_LINUX: 1, + MAP_OSX: 3, + MAP_ATSET1: 4, + MAP_ATSET2: 5, + MAP_ATSET3: 6, + MAP_USB: 7, + MAP_WIN32: 9, + MAP_XWINXT: 10, + MAP_XKBDXT: 11, + MAP_X11: 13, + MAP_HTML: 14, + MAP_XKB: 15, + MAP_SUN: 17, + MAP_ADB: 18, + } + + ENUM_COLUMNS = { + MAP_QCODE: 14, + } + + NAME_COLUMNS = { + MAP_LINUX: 0, + MAP_OSX: 2, + MAP_WIN32: 8, + MAP_X11: 12, + MAP_HTML: 14, + MAP_XKB: 15, + MAP_QCODE: 16, + } + + ENUM_BOUND = { + MAP_QCODE: "Q_KEY_CODE__MAX", + } + + def __init__(self): + + self.mapto = {} + self.mapfrom = {} + self.mapname = {} + self.mapchecksum = None + + for name in self.MAP_LIST: + # Key is a MAP_LINUX, value is a MAP_XXX + self.mapto[name] = {} + # key is a MAP_XXX, value is a MAP_LINUX + self.mapfrom[name] = {} + + for name in self.NAME_COLUMNS.keys(): + # key is a MAP_LINUX, value is a string + self.mapname[name] = {} + + def _generate_checksum(self, filename): + hash = hashlib.sha256() + with open(filename, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash.update(chunk) + self.mapchecksum = hash.hexdigest() + + def load(self, filename): + self._generate_checksum(filename) + + with open(filename, 'r') as f: + reader = csv.reader(f) + + first = True + + for row in reader: + # Discard column headings + if first: + first = False + continue + + # We special case MAP_LINUX since that is out + # master via which all other mappings are done + linux = self.load_linux(row) + + # Now load all the remaining master data values + self.load_data(row, linux) + + # Then load all the keycode names + self.load_names(row, linux) + + # Finally calculate derived key maps + self.derive_data(row, linux) + + def load_linux(self, row): + col = self.CODE_COLUMNS[self.MAP_LINUX] + linux = row[col] + + if linux.startswith("0x"): + linux = int(linux, 16) + else: + linux = int(linux, 10) + + self.mapto[self.MAP_LINUX][linux] = linux + self.mapfrom[self.MAP_LINUX][linux] = linux + + return linux + + + def load_data(self, row, linux): + for mapname in self.CODE_COLUMNS: + if mapname == self.MAP_LINUX: + continue + + col = self.CODE_COLUMNS[mapname] + val = row[col] + + if val == "": + continue + + if val.startswith("0x"): + val = int(val, 16) + elif val.isdigit(): + val = int(val, 10) + + self.mapto[mapname][linux] = val + self.mapfrom[mapname][val] = linux + + def load_names(self, row, linux): + for mapname in self.NAME_COLUMNS: + col = self.NAME_COLUMNS[mapname] + val = row[col] + + if val == "": + continue + + self.mapname[mapname][linux] = val + + + def derive_data(self, row, linux): + # Linux RAW is XT scan codes with special encoding of the + # 0xe0 scan codes + if linux in self.mapto[self.MAP_ATSET1]: + at1 = self.mapto[self.MAP_ATSET1][linux] + if at1 > 0x7f: + assert((at1 & ~0x7f) == 0xe000) + xtkbd = 0x100 | (at1 & 0x7f) + else: + xtkbd = at1 + self.mapto[self.MAP_XTKBD][linux] = xtkbd + self.mapfrom[self.MAP_XTKBD][xtkbd] = linux + + # Xorg KBD is XKBD XT offset by 8 + if linux in self.mapto[self.MAP_XKBDXT]: + xorgkbd = self.mapto[self.MAP_XKBDXT][linux] + 8 + self.mapto[self.MAP_XORGKBD][linux] = xorgkbd + self.mapfrom[self.MAP_XORGKBD][xorgkbd] = linux + + # Xorg evdev is Linux offset by 8 + self.mapto[self.MAP_XORGEVDEV][linux] = linux + 8 + self.mapfrom[self.MAP_XORGEVDEV][linux + 8] = linux + + # Xorg XQuartx is OS-X offset by 8 + if linux in self.mapto[self.MAP_OSX]: + xorgxquartz = self.mapto[self.MAP_OSX][linux] + 8 + self.mapto[self.MAP_XORGXQUARTZ][linux] = xorgxquartz + self.mapfrom[self.MAP_XORGXQUARTZ][xorgxquartz] = linux + + # Xorg Xwin (aka Cygwin) is XWin XT offset by 8 + if linux in self.mapto[self.MAP_XWINXT]: + xorgxwin = self.mapto[self.MAP_XWINXT][linux] + 8 + self.mapto[self.MAP_XORGXWIN][linux] = xorgxwin + self.mapfrom[self.MAP_XORGXWIN][xorgxwin] = linux + + # QNUM keycodes are XT scan codes with a slightly + # different encoding of 0xe0 scan codes + if linux in self.mapto[self.MAP_ATSET1]: + at1 = self.mapto[self.MAP_ATSET1][linux] + if at1 > 0x7f: + assert((at1 & ~0x7f) == 0xe000) + qnum = 0x80 | (at1 & 0x7f) + else: + qnum = at1 + self.mapto[self.MAP_QNUM][linux] = qnum + self.mapfrom[self.MAP_QNUM][qnum] = linux + + # Hack for compatibility with previous mistakes in handling + # Print/SysRq. The preferred qnum for Print/SysRq is 0x54, + # but QEMU previously allowed 0xb7 too + if qnum == 0x54: + self.mapfrom[self.MAP_QNUM][0xb7] = self.mapfrom[self.MAP_QNUM][0x54] + + if linux in self.mapname[self.MAP_QCODE]: + qcodeenum = self.mapname[self.MAP_QCODE][linux] + qcodeenum = "Q_KEY_CODE_" + qcodeenum.upper() + self.mapto[self.MAP_QCODE][linux] = qcodeenum + self.mapfrom[self.MAP_QCODE][qcodeenum] = linux + +class LanguageGenerator(object): + + def _boilerplate(self, lines): + raise NotImplementedError() + + def generate_header(self, database, args): + self._boilerplate([ + "This file is auto-generated from keymaps.csv", + "Database checksum sha256(%s)" % database.mapchecksum, + "To re-generate, run:", + " %s" % args, + ]) + +class LanguageSrcGenerator(LanguageGenerator): + + TYPE_INT = "integer" + TYPE_STRING = "string" + TYPE_ENUM = "enum" + + def _array_start(self, varname, length, defvalue, fromtype, totype): + raise NotImplementedError() + + def _array_end(self, fromtype, totype): + raise NotImplementedError() + + def _array_entry(self, index, value, comment, fromtype, totype): + raise NotImplementedError() + + def generate_code_map(self, varname, database, frommapname, tomapname): + if frommapname not in database.mapfrom: + raise Exception("Unknown map %s, expected one of %s" % ( + frommapname, ", ".join(database.mapfrom.keys()))) + if tomapname not in database.mapto: + raise Exception("Unknown map %s, expected one of %s" % ( + tomapname, ", ".join(database.mapto.keys()))) + + tolinux = database.mapfrom[frommapname] + fromlinux = database.mapto[tomapname] + + if varname is None: + varname = "code_map_%s_to_%s" % (frommapname, tomapname) + + if frommapname in database.ENUM_COLUMNS: + fromtype = self.TYPE_ENUM + elif type(list(tolinux.keys())[0]) == str: + fromtype = self.TYPE_STRING + else: + fromtype = self.TYPE_INT + + if tomapname in database.ENUM_COLUMNS: + totype = self.TYPE_ENUM + elif type(list(fromlinux.values())[0]) == str: + totype = self.TYPE_STRING + else: + totype = self.TYPE_INT + + keys = list(tolinux.keys()) + keys.sort() + if fromtype == self.TYPE_INT: + keys = range(keys[-1] + 1) + + if fromtype == self.TYPE_ENUM: + keymax = database.ENUM_BOUND[frommapname] + else: + keymax = len(keys) + + defvalue = fromlinux.get(0, None) + if fromtype == self.TYPE_ENUM: + self._array_start(varname, keymax, defvalue, fromtype, totype) + else: + self._array_start(varname, keymax, None, fromtype, totype) + + for src in keys: + linux = tolinux.get(src, None) + if linux is None: + dst = None + else: + dst = fromlinux.get(linux, defvalue) + + comment = "%s -> %s -> %s" % (self._label(database, frommapname, src, linux), + self._label(database, Database.MAP_LINUX, linux, linux), + self._label(database, tomapname, dst, linux)) + self._array_entry(src, dst, comment, fromtype, totype) + self._array_end(fromtype, totype) + + def generate_code_table(self, varname, database, mapname): + if mapname not in database.mapto: + raise Exception("Unknown map %s, expected one of %s" % ( + mapname, ", ".join(database.mapto.keys()))) + + keys = list(database.mapto[Database.MAP_LINUX].keys()) + keys.sort() + names = [database.mapname[Database.MAP_LINUX].get(key, "unnamed") for key in keys] + + if varname is None: + varname = "code_table_%s" % mapname + + if mapname in database.ENUM_COLUMNS: + totype = self.TYPE_ENUM + elif type(list(database.mapto[mapname].values())[0]) == str: + totype = self.TYPE_STRING + else: + totype = self.TYPE_INT + + self._array_start(varname, len(keys), None, self.TYPE_INT, totype) + + defvalue = database.mapto[mapname].get(0, None) + for i in range(len(keys)): + key = keys[i] + dst = database.mapto[mapname].get(key, defvalue) + self._array_entry(i, dst, names[i], self.TYPE_INT, totype) + + self._array_end(self.TYPE_INT, totype) + + def generate_name_map(self, varname, database, frommapname, tomapname): + if frommapname not in database.mapfrom: + raise Exception("Unknown map %s, expected one of %s" % ( + frommapname, ", ".join(database.mapfrom.keys()))) + if tomapname not in database.mapname: + raise Exception("Unknown map %s, expected one of %s" % ( + tomapname, ", ".join(database.mapname.keys()))) + + tolinux = database.mapfrom[frommapname] + fromlinux = database.mapname[tomapname] + + if varname is None: + varname = "name_map_%s_to_%s" % (frommapname, tomapname) + + keys = list(tolinux.keys()) + keys.sort() + if type(keys[0]) == int: + keys = range(keys[-1] + 1) + + if type(keys[0]) == int: + fromtype = self.TYPE_INT + else: + fromtype = self.TYPE_STRING + + self._array_start(varname, len(keys), None, fromtype, self.TYPE_STRING) + + for src in keys: + linux = tolinux.get(src, None) + if linux is None: + dst = None + else: + dst = fromlinux.get(linux, None) + + comment = "%s -> %s -> %s" % (self._label(database, frommapname, src, linux), + self._label(database, Database.MAP_LINUX, linux, linux), + self._label(database, tomapname, dst, linux)) + self._array_entry(src, dst, comment, fromtype, self.TYPE_STRING) + self._array_end(fromtype, self.TYPE_STRING) + + def generate_name_table(self, varname, database, mapname): + if mapname not in database.mapname: + raise Exception("Unknown map %s, expected one of %s" % ( + mapname, ", ".join(database.mapname.keys()))) + + keys = list(database.mapto[Database.MAP_LINUX].keys()) + keys.sort() + names = [database.mapname[Database.MAP_LINUX].get(key, "unnamed") for key in keys] + + if varname is None: + varname = "name_table_%s" % mapname + + self._array_start(varname, len(keys), None, self.TYPE_INT, self.TYPE_STRING) + + for i in range(len(keys)): + key = keys[i] + dst = database.mapname[mapname].get(key, None) + self._array_entry(i, dst, names[i], self.TYPE_INT, self.TYPE_STRING) + + self._array_end(self.TYPE_INT, self.TYPE_STRING) + + def _label(self, database, mapname, val, linux): + if mapname in database.mapname: + return "%s:%s (%s)" % (mapname, val, database.mapname[mapname].get(linux, "unnamed")) + else: + return "%s:%s" % (mapname, val) + +class LanguageDocGenerator(LanguageGenerator): + + def _array_start_name_doc(self, varname, namemap): + raise NotImplementedError() + + def _array_start_code_doc(self, varname, namemap, codemap): + raise NotImplementedError() + + def _array_end(self): + raise NotImplementedError() + + def _array_name_entry(self, value, name): + raise NotImplementedError() + + def _array_code_entry(self, value, name): + raise NotImplementedError() + + def generate_name_docs(self, title, subtitle, database, mapname): + if mapname not in database.mapname: + raise Exception("Unknown map %s, expected one of %s" % ( + mapname, ", ".join(database.mapname.keys()))) + + keys = list(database.mapto[Database.MAP_LINUX].keys()) + keys.sort() + names = [database.mapname[Database.MAP_LINUX].get(key, "unnamed") for key in keys] + + if title is None: + title = mapname + if subtitle is None: + subtitle = "Docs for %s" % mapname + + self._array_start_name_doc(title, subtitle, mapname) + + for i in range(len(keys)): + key = keys[i] + dst = database.mapname[mapname].get(key, None) + self._array_name_entry(key, dst) + + self._array_end() + + + def generate_code_docs(self, title, subtitle, database, mapname): + if mapname not in database.mapfrom: + raise Exception("Unknown map %s, expected one of %s" % ( + mapname, ", ".join(database.mapfrom.keys()))) + + tolinux = database.mapfrom[mapname] + keys = list(tolinux.keys()) + keys.sort() + if mapname in database.mapname: + names = database.mapname[mapname] + namemap = mapname + else: + names = database.mapname[Database.MAP_LINUX] + namemap = Database.MAP_LINUX + + if title is None: + title = mapname + if subtitle is None: + subtitle = "Docs for %s" % mapname + + self._array_start_code_doc(title, subtitle, mapname, namemap) + + for i in range(len(keys)): + key = keys[i] + self._array_code_entry(key, names.get(tolinux[key], "unnamed")) + + self._array_end() + +class CLanguageGenerator(LanguageSrcGenerator): + + def __init__(self, inttypename, strtypename, lentypename): + self.inttypename = inttypename + self.strtypename = strtypename + self.lentypename = lentypename + + def _boilerplate(self, lines): + print("/*") + for line in lines: + print(" * %s" % line) + print("*/") + + def _array_start(self, varname, length, defvalue, fromtype, totype): + self._varname = varname; + totypename = self.strtypename if totype == self.TYPE_STRING else self.inttypename + if fromtype in (self.TYPE_INT, self.TYPE_ENUM): + if type(length) == str: + print("const %s %s[%s] = {" % (totypename, varname, length)) + else: + print("const %s %s[%d] = {" % (totypename, varname, length)) + else: + print("const struct _%s {" % varname) + print(" const %s from;" % self.strtypename) + print(" const %s to;" % totypename) + print("} %s[] = {" % varname) + + if defvalue != None: + if totype == self.TYPE_ENUM: + if type(length) == str: + print(" [0 ... %s-1] = %s," % (length, defvalue)) + else: + print(" [0 ... 0x%x-1] = %s," % (length, defvalue)) + else: + if type(length) == str: + print(" [0 ... %s-1] = 0x%x," % (length, defvalue)) + else: + print(" [0 ... 0x%x-1] = 0x%x," % (length, defvalue)) + + def _array_end(self, fromtype, totype): + print("};") + print("const %s %s_len = sizeof(%s)/sizeof(%s[0]);" % + (self.lentypename, self._varname, self._varname, self._varname)) + + def _array_entry(self, index, value, comment, fromtype, totype): + if value is None: + return + if fromtype == self.TYPE_INT: + indexfmt = "0x%x" + elif fromtype == self.TYPE_ENUM: + indexfmt = "%s" + else: + indexfmt = "\"%s\"" + + if totype == self.TYPE_INT: + valuefmt = "0x%x" + elif totype == self.TYPE_ENUM: + valuefmt = "%s" + else: + valuefmt = "\"%s\"" + + if fromtype != self.TYPE_STRING: + print((" [" + indexfmt + "] = " + valuefmt + ", /* %s */") % (index, value, comment)) + else: + print((" {" + indexfmt + ", " + valuefmt + "}, /* %s */") % (index, value, comment)) + +class StdCLanguageGenerator(CLanguageGenerator): + + def __init__(self): + super(StdCLanguageGenerator, self).__init__("unsigned short", "char *", "unsigned int") + +class GLib2LanguageGenerator(CLanguageGenerator): + + def __init__(self): + super(GLib2LanguageGenerator, self).__init__("guint16", "gchar *", "guint") + +class CHeaderLanguageGenerator(LanguageSrcGenerator): + + def __init__(self, inttypename, strtypename, lentypename): + self.inttypename = inttypename + self.strtypename = strtypename + self.lentypename = lentypename + + def _boilerplate(self, lines): + print("/*") + for line in lines: + print(" * %s" % line) + print("*/") + + def _array_start(self, varname, length, defvalue, fromtype, totype): + self._varname = varname + if fromtype == self.TYPE_STRING: + self._length = 0 + else: + self._length = length + + def _array_end(self, fromtype, totype): + totypename = self.strtypename if totype == self.TYPE_STRING else self.inttypename + if fromtype == self.TYPE_STRING: + vartypename = "struct _%s" % self._varname + print("%s {" % vartypename) + print(" const %s from;" % self.strtypename) + print(" const %s to;" % totypename) + print("};") + else: + vartypename = totypename + if type(self._length) == str: + print("extern const %s %s[%s];" % (vartypename, self._varname, self._length)) + else: + print("extern const %s %s[%d];" % (vartypename, self._varname, self._length)) + print("extern const %s %s_len;" % (self.lentypename, self._varname)) + + def _array_entry(self, index, value, comment, fromtype, totype): + if value is None: + return + if fromtype == self.TYPE_STRING: + self._length += 1 + +class StdCHeaderLanguageGenerator(CHeaderLanguageGenerator): + + def __init__(self): + super(StdCHeaderLanguageGenerator, self).__init__("unsigned short", "char *", "unsigned int") + +class GLib2HeaderLanguageGenerator(CHeaderLanguageGenerator): + + def __init__(self): + super(GLib2HeaderLanguageGenerator, self).__init__("guint16", "gchar *", "guint") + +class CppLanguageGenerator(CLanguageGenerator): + + def _array_start(self, varname, length, defvalue, fromtype, totype): + if fromtype == self.TYPE_ENUM: + raise NotImplementedError("Enums not supported as source in C++ generator") + totypename = "const " + self.strtypename if totype == self.TYPE_STRING else self.inttypename + if fromtype == self.TYPE_INT: + print("#include <vector>") + print("extern const std::vector<%s> %s;" % (totypename, varname)); + print("const std::vector<%s> %s = {" % (totypename, varname)) + else: + print("#include <map>") + print("#include <string>") + print("extern const std::map<const std::string, %s> %s;" % (totypename, varname)) + print("const std::map<const std::string, %s> %s = {" % (totypename, varname)) + + def _array_end(self, fromtype, totype): + print("};") + + # designated initializers not available in C++ + def _array_entry(self, index, value, comment, fromtype, totype): + if fromtype == self.TYPE_STRING: + return super(CppLanguageGenerator, self)._array_entry(index, value, comment, fromtype, totype) + + if value is None: + print(" 0, /* %s */" % comment) + elif totype == self.TYPE_INT: + print(" 0x%x, /* %s */" % (value, comment)) + elif totype == self.TYPE_ENUM: + print(" %s, /* %s */" % (value, comment)) + else: + print(" \"%s\", /* %s */" % (value, comment)) + +class StdCppLanguageGenerator(CppLanguageGenerator): + + def __init__(self): + super(StdCppLanguageGenerator, self).__init__("unsigned short", "char *", "unsigned int") + +class CppHeaderLanguageGenerator(CHeaderLanguageGenerator): + + def _array_start(self, varname, length, defvalue, fromtype, totype): + if fromtype == self.TYPE_ENUM: + raise NotImplementedError("Enums not supported as source in C++ generator") + totypename = "const " + self.strtypename if totype == self.TYPE_STRING else self.inttypename + if fromtype == self.TYPE_INT: + print("#include <vector>") + print("extern const std::vector<%s> %s;" % (totypename, varname)); + else: + print("#include <map>") + print("#include <string>") + print("extern const std::map<const std::string, %s> %s;" % (totypename, varname)) + + def _array_end(self, fromtype, totype): + pass + + # designated initializers not available in C++ + def _array_entry(self, index, value, comment, fromtype, totype): + pass + +class StdCppHeaderLanguageGenerator(CppHeaderLanguageGenerator): + + def __init__(self): + super(StdCppHeaderLanguageGenerator, self).__init__("unsigned short", "char *", "unsigned int") + + +class RustLanguageGenerator(LanguageSrcGenerator): + + def _boilerplate(self, lines): + print("//") + for line in lines: + print("// %s" % line) + print("//") + + def _array_start(self, varname, length, defvalue, fromtype, totype): + if fromtype == self.TYPE_ENUM: + raise NotImplementedError("Enums not supported as source in Rust generator") + + totypename = "&str" if totype == self.TYPE_STRING else "u16" + if fromtype != self.TYPE_STRING: + print("pub static %s: &[%s] = &[" % (varname.upper(), totypename)) + else: + print("pub static %s: phf::Map<&str, %s> = phf::phf_map! {" % + (varname.upper(), totypename)) + + def _array_end(self, fromtype, totype): + if fromtype != self.TYPE_STRING: + print("];") + else: + print("};") + + def _array_entry(self, index, value, comment, fromtype, totype): + none = "\"\"" if totype == self.TYPE_STRING else "0" + if fromtype == self.TYPE_INT: + if value is None: + print(" %s, // %s" % (none, comment)) + elif totype == self.TYPE_INT: + print(" 0x%x, // %s" % (value, comment)) + elif totype == self.TYPE_ENUM: + print(" %s, // %s" % (value, comment)) + else: + print(" \"%s\", // %s" % (value, comment)) + else: + if value is None: + print(" \"%s\" => %s, // %s" % (index, none, comment)) + elif totype == self.TYPE_INT: + print(" \"%s\" => 0x%x, // %s" % (index, value, comment)) + elif totype == self.TYPE_ENUM: + print(" \"%s\" => %s, // %s" % (index, value, comment)) + else: + print(" \"%s\" => \"%s\", // %s" % (index, value, comment)) + + +class PythonLanguageGenerator(LanguageSrcGenerator): + + def _boilerplate(self, lines): + print("#") + for line in lines: + print("# %s" % line) + print("#") + + def _array_start(self, varname, length, defvalue, fromtype, totype): + if fromtype == self.TYPE_ENUM: + raise NotImplementedError("Enums not supported as source in Python generator") + + if fromtype != self.TYPE_STRING: + print("%s = [" % varname) + else: + print("%s = {" % varname) + + def _array_end(self, fromtype, totype): + if fromtype != self.TYPE_STRING: + print("]") + else: + print("}") + + def _array_entry(self, index, value, comment, fromtype, totype): + if fromtype == self.TYPE_INT: + if value is None: + print(" None, # %s" % (comment)) + elif totype == self.TYPE_INT: + print(" 0x%x, # %s" % (value, comment)) + elif totype == self.TYPE_ENUM: + print(" %s, # %s" % (value, comment)) + else: + print(" \"%s\", # %s" % (value, comment)) + else: + if value is None: + print(" \"%s\": None, # %s" % (index, comment)) + elif totype == self.TYPE_INT: + print(" \"%s\": 0x%x, # %s" % (index, value, comment)) + elif totype == self.TYPE_ENUM: + print(" \"%s\": %s, # %s" % (index, value, comment)) + else: + print(" \"%s\": \"%s\", # %s" % (index, value, comment)) + +class PerlLanguageGenerator(LanguageSrcGenerator): + + def _boilerplate(self, lines): + print("#") + for line in lines: + print("# %s" % line) + print("#") + + def _array_start(self, varname, length, defvalue, fromtype, totype): + if fromtype == self.TYPE_ENUN: + raise NotImplementedError("Enums not supported as source in Python generator") + if fromtype == self.TYPE_INT: + print("my @%s = (" % varname) + else: + print("my %%%s = (" % varname) + + def _array_end(self, fromtype, totype): + print(");") + + def _array_entry(self, index, value, comment, fromtype, totype): + if fromtype == self.TYPE_INT: + if value is None: + print(" undef, # %s" % (comment)) + elif totype == self.TYPE_INT: + print(" 0x%x, # %s" % (value, comment)) + elif totype == self.TYPE_ENUM: + print(" %s, # %s" % (value, comment)) + else: + print(" \"%s\", # %s" % (value, comment)) + else: + if value is None: + print(" \"%s\", undef, # %s" % (index, comment)) + elif totype == self.TYPE_INT: + print(" \"%s\", 0x%x, # %s" % (index, value, comment)) + elif totype == self.TYPE_ENUM: + print(" \"%s\", 0x%x, # %s" % (index, value, comment)) + else: + print(" \"%s\", \"%s\", # %s" % (index, value, comment)) + +class JavaScriptLanguageGenerator(LanguageSrcGenerator): + + def _boilerplate(self, lines): + print("/*") + for line in lines: + print(" * %s" % line) + print("*/") + + def _array_start(self, varname, length, defvalue, fromtype, totype): + print("export default {") + + def _array_end(self, fromtype, totype): + print("};") + + def _array_entry(self, index, value, comment, fromtype, totype): + if value is None: + return + + if fromtype == self.TYPE_INT: + fromfmt = "0x%x" + elif fromtype == self.TYPE_ENUM: + fromfmt = "%s" + else: + fromfmt = "\"%s\"" + + if totype == self.TYPE_INT: + tofmt = "0x%x" + elif totype == self.TYPE_ENUM: + tofmt = "%s" + else: + tofmt = "\"%s\"" + + print((" " + fromfmt + ": " + tofmt + ", /* %s */") % (index, value, comment)) + +class PodLanguageGenerator(LanguageDocGenerator): + + def _boilerplate(self, lines): + print("#") + for line in lines: + print("# %s" % line) + print("#") + + def _array_start_name_doc(self, title, subtitle, namemap): + print("=head1 NAME") + print("") + print("%s - %s" % (title, subtitle)) + print("") + print("=head1 DESCRIPTION") + print("") + print("List of %s key code names, with corresponding key code values" % namemap) + print("") + print("=over 4") + print("") + + def _array_start_code_doc(self, title, subtitle, codemap, namemap): + print("=head1 NAME") + print("") + print("%s - %s" % (title, subtitle)) + print("") + print("=head1 DESCRIPTION") + print("") + print("List of %s key code values, with corresponding %s key code names" % (codemap, namemap)) + print("") + print("=over 4") + print("") + + def _array_end(self): + print("=back") + print("") + + def _array_name_entry(self, value, name): + print("=item %s" % name) + print("") + print("Key value %d (0x%x)" % (value, value)) + print("") + + def _array_code_entry(self, value, name): + print("=item %d (0x%x)" % (value, value)) + print("") + print("Key name %s" % name) + print("") + +class RSTLanguageGenerator(LanguageDocGenerator): + + def _boilerplate(self, lines): + print("..") + for line in lines: + print(" %s" % line) + print("") + + def _array_start_name_doc(self, title, subtitle, namemap): + print("=" * len(title)) + print(title) + print("=" * len(title)) + print("") + print("-" * len(subtitle)) + print(subtitle) + print("-" * len(subtitle)) + print("") + print(":Manual section: 7") + print(":Manual group: Virtualization Support") + print("") + print("DESCRIPTION") + print("===========") + print("List of %s key code names, with corresponding key code values" % namemap) + print("") + + def _array_start_code_doc(self, title, subtitle, codemap, namemap): + print("=" * len(title)) + print(title) + print("=" * len(title)) + print("") + print("-" * len(subtitle)) + print(subtitle) + print("-" * len(subtitle)) + print("") + print(":Manual section: 7") + print(":Manual group: Virtualization Support") + print("") + print("DESCRIPTION") + print("===========") + print("List of %s key code values, with corresponding %s key code names" % (codemap, namemap)) + print("") + + def _array_end(self): + print("") + + def _array_name_entry(self, value, name): + print("* %s" % name) + print("") + print(" Key value %d (0x%x)" % (value, value)) + print("") + + def _array_code_entry(self, value, name): + print("* %d (0x%x)" % (value, value)) + print("") + print(" Key name %s" % name) + print("") + +SRC_GENERATORS = { + "stdc": StdCLanguageGenerator(), + "stdc-header": StdCHeaderLanguageGenerator(), + "stdc++": StdCppLanguageGenerator(), + "stdc++-header": StdCppHeaderLanguageGenerator(), + "glib2": GLib2LanguageGenerator(), + "glib2-header": GLib2HeaderLanguageGenerator(), + "python2": PythonLanguageGenerator(), + "python3": PythonLanguageGenerator(), + "perl": PerlLanguageGenerator(), + "js": JavaScriptLanguageGenerator(), + "rust": RustLanguageGenerator(), +} +DOC_GENERATORS = { + "pod": PodLanguageGenerator(), + "rst": RSTLanguageGenerator(), +} + +def code_map(args): + database = Database() + database.load(args.keymaps) + + cliargs = ["keymap-gen", "code-map", "--lang=%s" % args.lang] + if args.varname is not None: + cliargs.append("--varname=%s" % args.varname) + cliargs.extend(["keymaps.csv", args.frommapname, args.tomapname]) + SRC_GENERATORS[args.lang].generate_header(database, " ".join(cliargs)) + + SRC_GENERATORS[args.lang].generate_code_map(args.varname, database, args.frommapname, args.tomapname) + +def code_table(args): + database = Database() + database.load(args.keymaps) + + cliargs = ["keymap-gen", "code-table", "--lang=%s" % args.lang] + if args.varname is not None: + cliargs.append("--varname=%s" % args.varname) + cliargs.extend(["keymaps.csv", args.mapname]) + SRC_GENERATORS[args.lang].generate_header(database, " ".join(cliargs)) + + SRC_GENERATORS[args.lang].generate_code_table(args.varname, database, args.mapname) + +def name_map(args): + database = Database() + database.load(args.keymaps) + + cliargs = ["keymap-gen", "name-map", "--lang=%s" % args.lang] + if args.varname is not None: + cliargs.append("--varname=%s" % args.varname) + cliargs.extend(["keymaps.csv", args.frommapname, args.tomapname]) + SRC_GENERATORS[args.lang].generate_header(database, " ".join(cliargs)) + + SRC_GENERATORS[args.lang].generate_name_map(args.varname, database, args.frommapname, args.tomapname) + +def name_table(args): + database = Database() + database.load(args.keymaps) + + + cliargs = ["keymap-gen", "name-table", "--lang=%s" % args.lang] + if args.varname is not None: + cliargs.append("--varname=%s" % args.varname) + cliargs.extend(["keymaps.csv", args.mapname]) + SRC_GENERATORS[args.lang].generate_header(database, " ".join(cliargs)) + + SRC_GENERATORS[args.lang].generate_name_table(args.varname, database, args.mapname) + +def code_docs(args): + database = Database() + database.load(args.keymaps) + + + cliargs = ["keymap-gen", "code-docs", "--lang=%s" % args.lang] + if args.title is not None: + cliargs.append("--title=%s" % args.title) + if args.subtitle is not None: + cliargs.append("--subtitle=%s" % args.subtitle) + cliargs.extend(["keymaps.csv", args.mapname]) + DOC_GENERATORS[args.lang].generate_header(database, " ".join(cliargs)) + + DOC_GENERATORS[args.lang].generate_code_docs(args.title, args.subtitle, database, args.mapname) + +def name_docs(args): + database = Database() + database.load(args.keymaps) + + + cliargs = ["keymap-gen", "name-docs", "--lang=%s" % args.lang] + if args.title is not None: + cliargs.append("--title=%s" % args.title) + if args.subtitle is not None: + cliargs.append("--subtitle=%s" % args.subtitle) + cliargs.extend(["keymaps.csv", args.mapname]) + DOC_GENERATORS[args.lang].generate_header(database, " ".join(cliargs)) + + DOC_GENERATORS[args.lang].generate_name_docs(args.title, args.subtitle, database, args.mapname) + +def usage(): + print ("Please select a command:") + print (" 'code-map', 'code-table', 'name-map', 'name-table', 'docs'") + sys.exit(1) + +def main(): + parser = argparse.ArgumentParser() + + subparsers = parser.add_subparsers(help="sub-command help") + + codemapparser = subparsers.add_parser("code-map", help="Generate a mapping between code tables") + codemapparser.add_argument("--varname", default=None, help="Data variable name") + codemapparser.add_argument("--lang", default="stdc", + help="Output language (%s)" % ( + ",".join(SRC_GENERATORS.keys()))) + codemapparser.add_argument("keymaps", help="Path to keymap CSV data file") + codemapparser.add_argument("frommapname", help="Source code table name") + codemapparser.add_argument("tomapname", help="Target code table name") + codemapparser.set_defaults(func=code_map) + + codetableparser = subparsers.add_parser("code-table", help="Generate a flat code table") + codetableparser.add_argument("--lang", default="stdc", + help="Output language (%s)" % ( + ",".join(SRC_GENERATORS.keys()))) + codetableparser.add_argument("--varname", default=None, help="Data variable name") + codetableparser.add_argument("keymaps", help="Path to keymap CSV data file") + codetableparser.add_argument("mapname", help="Code table name") + codetableparser.set_defaults(func=code_table) + + namemapparser = subparsers.add_parser("name-map", help="Generate a mapping to names") + namemapparser.add_argument("--lang", default="stdc", + help="Output language (%s)" % ( + ",".join(SRC_GENERATORS.keys()))) + namemapparser.add_argument("--varname", default=None, help="Data variable name") + namemapparser.add_argument("keymaps", help="Path to keymap CSV data file") + namemapparser.add_argument("frommapname", help="Source code table name") + namemapparser.add_argument("tomapname", help="Target name table name") + namemapparser.set_defaults(func=name_map) + + nametableparser = subparsers.add_parser("name-table", help="Generate a flat name table") + nametableparser.add_argument("--lang", default="stdc", + help="Output language, (%s)" % ( + ",".join(SRC_GENERATORS.keys()))) + nametableparser.add_argument("--varname", default=None, help="Data variable name") + nametableparser.add_argument("keymaps", help="Path to keymap CSV data file") + nametableparser.add_argument("mapname", help="Name table name") + nametableparser.set_defaults(func=name_table) + + codedocsparser = subparsers.add_parser("code-docs", help="Generate code documentation") + codedocsparser.add_argument("--lang", default="pod", + help="Output language (%s)" % ( + ",".join(DOC_GENERATORS.keys()))) + codedocsparser.add_argument("--title", default=None, help="Document title") + codedocsparser.add_argument("--subtitle", default=None, help="Document subtitle") + codedocsparser.add_argument("keymaps", help="Path to keymap CSV data file") + codedocsparser.add_argument("mapname", help="Code table name") + codedocsparser.set_defaults(func=code_docs) + + namedocsparser = subparsers.add_parser("name-docs", help="Generate name documentation") + namedocsparser.add_argument("--lang", default="pod", + help="Output language (%s)" % ( + ",".join(DOC_GENERATORS.keys()))) + namedocsparser.add_argument("--title", default=None, help="Document title") + namedocsparser.add_argument("--subtitle", default=None, help="Document subtitle") + namedocsparser.add_argument("keymaps", help="Path to keymap CSV data file") + namedocsparser.add_argument("mapname", help="Name table name") + namedocsparser.set_defaults(func=name_docs) + + args = parser.parse_args() + if hasattr(args, "func"): + args.func(args) + else: + usage() + + +main() diff --git a/ui/keymaps.c b/ui/keymaps.c new file mode 100644 index 00000000..6ceaa970 --- /dev/null +++ b/ui/keymaps.c @@ -0,0 +1,272 @@ +/* + * 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/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 00000000..64734054 --- /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 00000000..c1b137bf --- /dev/null +++ b/ui/meson.build @@ -0,0 +1,183 @@ +softmmu_ss.add(pixman) +specific_ss.add(when: ['CONFIG_SOFTMMU'], if_true: pixman) # for the include path +specific_ss.add(when: ['CONFIG_SOFTMMU'], if_true: opengl) # for the include path + +softmmu_ss.add(png) +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', + 'util.c', +)) +if dbus_display + softmmu_ss.add(files('dbus-module.c')) +endif +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, 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')) + +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 + +softmmu_ss.add(opengl) +if opengl.found() + opengl_ss = ss.source_set() + opengl_ss.add(gbm) + opengl_ss.add(when: [opengl, pixman], + if_true: files('shader.c', 'console-gl.c', 'egl-helpers.c', 'egl-context.c')) + ui_modules += {'opengl' : opengl_ss} +endif + +if opengl.found() and gbm.found() + egl_headless_ss = ss.source_set() + egl_headless_ss.add(when: [opengl, gbm, pixman], + if_true: files('egl-headless.c')) + ui_modules += {'egl-headless' : egl_headless_ss} +endif + +if dbus_display + dbus_ss = ss.source_set() + dbus_display1 = custom_target('dbus-display gdbus-codegen', + output: ['dbus-display1.h', 'dbus-display1.c'], + input: files('dbus-display1.xml'), + command: [gdbus_codegen, '@INPUT@', + '--glib-min-required', '2.64', + '--output-directory', meson.current_build_dir(), + '--interface-prefix', 'org.qemu.', + '--c-namespace', 'QemuDBus', + '--generate-c-code', '@BASENAME@']) + dbus_ss.add(when: [gio, pixman, opengl, gbm], + if_true: [files( + 'dbus-chardev.c', + 'dbus-clipboard.c', + 'dbus-console.c', + 'dbus-error.c', + 'dbus-listener.c', + 'dbus.c', + ), dbus_display1]) + ui_modules += {'dbus' : dbus_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')) + if have_gtk_clipboard + gtk_ss.add(files('gtk-clipboard.c')) + endif + gtk_ss.add(when: x11, if_true: files('x_keymap.c')) + gtk_ss.add(when: opengl, if_true: files('gtk-gl-area.c')) + gtk_ss.add(when: [x11, 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, 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 gio.found() + 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 00000000..3ab7e2e9 --- /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 00000000..095aec16 --- /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 00000000..20f09f56 --- /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 00000000..bfebbdea --- /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 00000000..39cab8cd --- /dev/null +++ b/ui/sdl2-gl.c @@ -0,0 +1,243 @@ +/* + * 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); +} + +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(DisplayGLCtx *dgc, + QEMUGLParams *params) +{ + struct sdl2_console *scon = container_of(dgc, struct sdl2_console, dgc); + 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(DisplayGLCtx *dgc, QEMUGLContext ctx) +{ + SDL_GLContext sdlctx = (SDL_GLContext)ctx; + + SDL_GL_DeleteContext(sdlctx); +} + +int sdl2_gl_make_context_current(DisplayGLCtx *dgc, + QEMUGLContext ctx) +{ + struct sdl2_console *scon = container_of(dgc, struct sdl2_console, dgc); + 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); +} diff --git a/ui/sdl2-input.c b/ui/sdl2-input.c new file mode 100644 index 00000000..f0683822 --- /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 00000000..8cb77416 --- /dev/null +++ b/ui/sdl2.c @@ -0,0 +1,957 @@ +/* + * 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" +#include "qemu/log.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 bool alt_grab; +static bool ctrl_grab; + +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 if (wev->x < 0) { + btn = INPUT_BUTTON_WHEEL_RIGHT; + } else if (wev->x > 0) { + btn = INPUT_BUTTON_WHEEL_LEFT; + } 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, true); + } + 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_scanout_disable = sdl2_gl_scanout_disable, + .dpy_gl_scanout_texture = sdl2_gl_scanout_texture, + .dpy_gl_update = sdl2_gl_scanout_flush, +}; + +static bool +sdl2_gl_is_compatible_dcl(DisplayGLCtx *dgc, + DisplayChangeListener *dcl) +{ + return dcl->ops == &dcl_gl_ops; +} + +static const DisplayGLCtxOps gl_ctx_ops = { + .dpy_gl_ctx_is_compatible_dcl = sdl2_gl_is_compatible_dcl, + .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, +}; +#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; + + if (o->u.sdl.has_grab_mod) { + if (o->u.sdl.grab_mod == HOT_KEY_MOD_LSHIFT_LCTRL_LALT) { + alt_grab = true; + } else if (o->u.sdl.grab_mod == HOT_KEY_MOD_RCTRL) { + ctrl_grab = true; + } + } + + 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; + sdl2_console[i].dgc.ops = display_opengl ? &gl_ctx_ops : NULL; +#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); + if (display_opengl) { + qemu_console_set_display_gl_ctx(con, &sdl2_console[i].dgc); + } + 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 00000000..ab448c41 --- /dev/null +++ b/ui/shader.c @@ -0,0 +1,179 @@ +/* + * 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 = 0; + + 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) { + goto end; + } + + program = qemu_gl_create_link_program(vert_shader, frag_shader); + +end: + 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; + } + glDeleteProgram(gls->texture_blit_prog); + glDeleteProgram(gls->texture_blit_flip_prog); + glDeleteProgram(gls->texture_blit_vao); + g_free(gls); +} diff --git a/ui/shader/meson.build b/ui/shader/meson.build new file mode 100644 index 00000000..592bf596 --- /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 00000000..ba081fa5 --- /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 00000000..bfa202c2 --- /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 00000000..6fe2744d --- /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 00000000..7e71e18d --- /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 00000000..c3ac20ad --- /dev/null +++ b/ui/spice-core.c @@ -0,0 +1,989 @@ +/* + * 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; +} + +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 00000000..494168e7 --- /dev/null +++ b/ui/spice-display.c @@ -0,0 +1,1226 @@ +/* + * 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 */ + +#if SPICE_HAS_ATTACHED_WORKER +static void interface_attached_worker(QXLInstance *sin) +{ + /* nothing to do */ +} +#else +static void interface_attach_worker(QXLInstance *sin, QXLWorker *qxl_worker) +{ + /* nothing to do */ +} +#endif + +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, false); + 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, + +#if SPICE_HAS_ATTACHED_WORKER + .attached_worker = interface_attached_worker, +#else + .attache_worker = interface_attach_worker, +#endif + .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); +} + +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(DisplayGLCtx *dgc, + QEMUGLParams *params) +{ + eglMakeCurrent(qemu_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + qemu_egl_rn_ctx); + return qemu_egl_create_context(dgc, 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_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, +}; + +static bool +qemu_spice_is_compatible_dcl(DisplayGLCtx *dgc, + DisplayChangeListener *dcl) +{ + return dcl->ops == &display_listener_gl_ops; +} + +static const DisplayGLCtxOps gl_ctx_ops = { + .dpy_gl_ctx_is_compatible_dcl = qemu_spice_is_compatible_dcl, + .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, +}; + +#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->dgc.ops = &gl_ctx_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 */ + Error *err = NULL; + char device_address[256] = ""; + if (qemu_console_fill_device_address(con, device_address, 256, &err)) { + spice_qxl_set_device_info(&ssd->qxl, + device_address, + qemu_console_get_head(con), + 1); + } else { + error_report_err(err); + } +#endif + + qemu_spice_create_host_memslot(ssd); + + if (spice_opengl) { + qemu_console_set_display_gl_ctx(con, &ssd->dgc); + } + 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 00000000..bbd50256 --- /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 00000000..32223358 --- /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 00000000..977577fb --- /dev/null +++ b/ui/trace-events @@ -0,0 +1,159 @@ +# 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" +gd_gl_area_create_context(void *ctx, int major, int minor) "ctx=%p, major=%d, minor=%d" +gd_gl_area_destroy_context(void *ctx, void *current_ctx) "ctx=%p, current_ctx=%p" + +# 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'" + +# clipboard.c +clipboard_check_serial(int cur, int recv, bool ok) "cur:%d recv:%d %d" + +# vdagent.c +vdagent_open(void) "" +vdagent_close(void) "" +vdagent_disconnect(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_discard(const char *name, int cur, int recv) "selection %s, cur:%d recv:%d" +vdagent_cb_grab_type(const char *name) "type %s" +vdagent_cb_serial_discard(uint32_t current, uint32_t received) "current=%u, received=%u" + +# dbus.c +dbus_registered_listener(const char *bus_name) "peer %s" +dbus_listener_vanished(const char *bus_name) "peer %s" +dbus_kbd_press(unsigned int keycode) "keycode %u" +dbus_kbd_release(unsigned int keycode) "keycode %u" +dbus_mouse_press(unsigned int button) "button %u" +dbus_mouse_release(unsigned int button) "button %u" +dbus_mouse_set_pos(unsigned int x, unsigned int y) "x=%u, y=%u" +dbus_mouse_rel_motion(int dx, int dy) "dx=%d, dy=%d" +dbus_update(int x, int y, int w, int h) "x=%d, y=%d, w=%d, h=%d" +dbus_clipboard_grab_failed(void) "" +dbus_clipboard_register(const char *bus_name) "peer %s" +dbus_clipboard_unregister(const char *bus_name) "peer %s" diff --git a/ui/trace.h b/ui/trace.h new file mode 100644 index 00000000..a89d7696 --- /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 00000000..cebceb26 --- /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/util.c b/ui/util.c new file mode 100644 index 00000000..7e8fc1ea --- /dev/null +++ b/ui/util.c @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2021 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 "hw/pci/pci.h" +#include "hw/pci/pci_bus.h" +#include "qapi/error.h" +#include "ui/console.h" + +/* + * 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_console_fill_device_address(QemuConsole *con, + char *device_address, + size_t size, + Error **errp) +{ + ERRP_GUARD(); + 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) { + error_setg(errp, "Setting device address of a display device: " + "Not a PCI device."); + return false; + } + + strncpy(device_address, "pci/0000", size); + if (!append_pci_address(device_address, size, pci)) { + error_setg(errp, "Setting device address of a display device: " + "Too many PCI devices in the chain."); + return false; + } + + return true; +} diff --git a/ui/vdagent.c b/ui/vdagent.c new file mode 100644 index 00000000..4bf50f0c --- /dev/null +++ b/ui/vdagent.c @@ -0,0 +1,947 @@ +#include "qemu/osdep.h" +#include "qapi/error.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 CHECK_SPICE_PROTOCOL_VERSION(major, minor, micro) \ + (CONFIG_SPICE_PROTOCOL_MAJOR > (major) || \ + (CONFIG_SPICE_PROTOCOL_MAJOR == (major) && \ + CONFIG_SPICE_PROTOCOL_MINOR > (minor)) || \ + (CONFIG_SPICE_PROTOCOL_MAJOR == (major) && \ + CONFIG_SPICE_PROTOCOL_MINOR == (minor) && \ + CONFIG_SPICE_PROTOCOL_MICRO >= (micro))) + +#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 last_serial[QEMU_CLIPBOARD_SELECTION__COUNT]; + 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 CHECK_SPICE_PROTOCOL_VERSION(0, 14, 0) + [VD_AGENT_CAP_GRAPHICS_DEVICE_INFO] = "graphics-device-info", +#endif +#if CHECK_SPICE_PROTOCOL_VERSION(0, 14, 1) + [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 CHECK_SPICE_PROTOCOL_VERSION(0, 14, 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 CHECK_SPICE_PROTOCOL_VERSION(0, 14, 3) + [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); +#if CHECK_SPICE_PROTOCOL_VERSION(0, 14, 1) + caps->caps[0] |= (1 << VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL); +#endif + } + + 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) + + sizeof(uint32_t)); + 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; + } + +#if CHECK_SPICE_PROTOCOL_VERSION(0, 14, 1) + if (vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL)) { + if (!info->has_serial) { + /* client should win */ + info->serial = vd->last_serial[info->selection]++; + info->has_serial = true; + } + *data = info->serial; + data++; + msg->size += sizeof(uint32_t); + } +#endif + + 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_update_info(VDAgentChardev *vd, + QemuClipboardInfo *info) +{ + 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_reset_serial(VDAgentChardev *vd) +{ + Chardev *chr = CHARDEV(vd); + + /* reopen the agent connection to reset the serial state */ + qemu_chr_be_event(chr, CHR_EVENT_CLOSED); + /* OPENED again after the guest disconnected, see set_fe_open */ +} + +static void vdagent_clipboard_notify(Notifier *notifier, void *data) +{ + VDAgentChardev *vd = + container_of(notifier, VDAgentChardev, cbpeer.notifier); + QemuClipboardNotify *notify = data; + + switch (notify->type) { + case QEMU_CLIPBOARD_UPDATE_INFO: + vdagent_clipboard_update_info(vd, notify->info); + return; + case QEMU_CLIPBOARD_RESET_SERIAL: + vdagent_clipboard_reset_serial(vd); + return; + } +} + +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 CHECK_SPICE_PROTOCOL_VERSION(0, 14, 1) + if (vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL)) { + if (size < sizeof(uint32_t)) { + /* this shouldn't happen! */ + return; + } + + info->has_serial = true; + info->serial = *(uint32_t *)data; + if (info->serial < vd->last_serial[s]) { + trace_vdagent_cb_grab_discard(GET_NAME(sel_name, s), + vd->last_serial[s], info->serial); + /* discard lower-ordering guest grab */ + return; + } + vd->last_serial[s] = info->serial; + data += sizeof(uint32_t); + size -= sizeof(uint32_t); + } +#endif + if (size > sizeof(uint32_t) * 10) { + /* + * spice has 6 types as of 2021. Limiting to 10 entries + * so 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 HOST_BIG_ENDIAN + /* + * 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); + } + + memset(vd->last_serial, 0, sizeof(vd->last_serial)); + + if (have_clipboard(vd) && vd->cbpeer.notifier.notify == NULL) { + vd->cbpeer.name = "vdagent"; + vd->cbpeer.notifier.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) +{ + trace_vdagent_disconnect(); + + 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.notifier.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) +{ + if (!fe_open) { + trace_vdagent_close(); + /* To reset_serial, we CLOSED our side. Make sure the other end knows we + * are ready again. */ + qemu_chr_be_event(chr, CHR_EVENT_OPENED); + 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 00000000..7e1fc473 --- /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 00000000..47fdae5b --- /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 00000000..367b8672 --- /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 00000000..d9c212ff --- /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 00000000..1e354066 --- /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 00000000..8aeadfaa --- /dev/null +++ b/ui/vnc-clipboard.c @@ -0,0 +1,337 @@ +/* + * 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 "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_update_info(VncState *vs, QemuClipboardInfo *info) +{ + 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_notify(Notifier *notifier, void *data) +{ + VncState *vs = container_of(notifier, VncState, cbpeer.notifier); + QemuClipboardNotify *notify = data; + + switch (notify->type) { + case QEMU_CLIPBOARD_UPDATE_INFO: + vnc_clipboard_update_info(vs, notify->info); + return; + case QEMU_CLIPBOARD_RESET_SERIAL: + /* ignore */ + return; + } +} + +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.notifier.notify) { + vs->cbpeer.name = "vnc"; + vs->cbpeer.notifier.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 00000000..0c56262a --- /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 00000000..c763256f --- /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 = g_malloc(VNC_SERVER_FB_BYTES); + last_bg = 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 00000000..09200d71 --- /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_PNG +/* The following define is needed by pngconf.h. Otherwise it won't compile, + because setjmp.h was already included by osdep.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_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_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_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_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_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_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_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_new(VncPalette, 1); + 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_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 00000000..95c3dac0 --- /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 00000000..900ae5b3 --- /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 00000000..bd33b890 --- /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 00000000..c107d8af --- /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 00000000..6535cb2a --- /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 00000000..4afa583e --- /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 00000000..e661ec11 --- /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 00000000..886f9bf6 --- /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 00000000..59f66bcc --- /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 00000000..dc7c0ba9 --- /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 00000000..e9f0eaf7 --- /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 00000000..b4eb3ce7 --- /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 00000000..6d79f3e5 --- /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 00000000..396cacfc --- /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 00000000..88f55cbf --- /dev/null +++ b/ui/vnc.c @@ -0,0 +1,4314 @@ +/* + * 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 "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" +#include "monitor/monitor.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); + } + vnc_unlock_output(vs); + + if (vs->cbpeer.notifier.notify) { + qemu_clipboard_peer_unregister(&vs->cbpeer); + } + + 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_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 */ + +#if HOST_BIG_ENDIAN + 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; + } + uint32_t dlen = abs(read_s32(data, 4)); + if (len == 8) { + 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) { + if (dlen < 4) { + error_report("vnc: malformed payload (header less than 4 bytes)" + " in extended clipboard pseudo-encoding."); + vnc_client_error(vs); + break; + } + vnc_client_cut_text_ext(vs, dlen, 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, false); + 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; + unsigned long offset; + int x; + uint8_t *guest_ptr, *server_ptr; + + struct timeval tv = { 0, 0 }; + + if (!vd->non_adaptive) { + gettimeofday(&tv, NULL); + has_dirty = vnc_update_stats(vd, &tv); + } + + offset = find_next_bit((unsigned long *) &vd->guest.dirty, + height * VNC_DIRTY_BPL(&vd->guest), 0); + if (offset == height * VNC_DIRTY_BPL(&vd->guest)) { + /* no dirty bits in guest surface */ + return has_dirty; + } + + /* + * 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 (;;) { + 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++; + 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; + } + } + 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_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 int vnc_display_get_addresses(QemuOpts *opts, + bool reverse, + SocketAddressList **saddr_list_ret, + SocketAddressList **wsaddr_list_ret, + Error **errp) +{ + SocketAddress *saddr = NULL; + SocketAddress *wsaddr = NULL; + g_autoptr(SocketAddressList) saddr_list = NULL; + SocketAddressList **saddr_tail = &saddr_list; + SocketAddress *single_saddr = NULL; + g_autoptr(SocketAddressList) wsaddr_list = NULL; + SocketAddressList **wsaddr_tail = &wsaddr_list; + 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; + + addr = qemu_opt_get(opts, "vnc"); + if (addr == NULL || g_str_equal(addr, "none")) { + return 0; + } + if (qemu_opt_get(opts, "websocket") && + !qcrypto_hash_supports(QCRYPTO_HASH_ALG_SHA1)) { + error_setg(errp, + "SHA1 hash support is required for websockets"); + return -1; + } + + 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) { + return -1; + } + /* Historical compat - first listen address can be used + * to set the default websocket port + */ + if (displaynum == -1) { + displaynum = rv; + } + QAPI_LIST_APPEND(saddr_tail, saddr); + } + + if (saddr_list && !saddr_list->next) { + single_saddr = saddr_list->value; + } else { + /* + * If we had multiple primary displays, we don't do defaults + * for websocket, and require explicit config instead. + */ + 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) { + return -1; + } + + /* Historical compat - if only a single listen address was + * provided, then this is used to set the default listen + * address for websocket too + */ + if (single_saddr && + single_saddr->type == SOCKET_ADDRESS_TYPE_INET && + wsaddr->type == SOCKET_ADDRESS_TYPE_INET && + g_str_equal(wsaddr->u.inet.host, "") && + !g_str_equal(single_saddr->u.inet.host, "")) { + g_free(wsaddr->u.inet.host); + wsaddr->u.inet.host = g_strdup(single_saddr->u.inet.host); + } + + QAPI_LIST_APPEND(wsaddr_tail, wsaddr); + } + + *saddr_list_ret = g_steal_pointer(&saddr_list); + *wsaddr_list_ret = g_steal_pointer(&wsaddr_list); + return 0; +} + +static int vnc_display_connect(VncDisplay *vd, + SocketAddressList *saddr_list, + SocketAddressList *wsaddr_list, + Error **errp) +{ + /* connect to viewer */ + QIOChannelSocket *sioc = NULL; + if (wsaddr_list) { + error_setg(errp, "Cannot use websockets in reverse mode"); + return -1; + } + if (!saddr_list || saddr_list->next) { + 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_list->value->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_list->value, 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, + SocketAddressList *saddr_list, + SocketAddressList *wsaddr_list, + Error **errp) +{ + SocketAddressList *el; + + if (saddr_list) { + vd->listener = qio_net_listener_new(); + qio_net_listener_set_name(vd->listener, "vnc-listen"); + for (el = saddr_list; el; el = el->next) { + if (qio_net_listener_open_sync(vd->listener, + el->value, 1, + errp) < 0) { + return -1; + } + } + + qio_net_listener_set_client_func(vd->listener, + vnc_listen_io, vd, NULL); + } + + if (wsaddr_list) { + vd->wslistener = qio_net_listener_new(); + qio_net_listener_set_name(vd->wslistener, "vnc-ws-listen"); + for (el = wsaddr_list; el; el = el->next) { + if (qio_net_listener_open_sync(vd->wslistener, + el->value, 1, + errp) < 0) { + return -1; + } + } + + qio_net_listener_set_client_func(vd->wslistener, + vnc_listen_io, vd, NULL); + } + + return 0; +} + +bool vnc_display_update(DisplayUpdateOptionsVNC *arg, Error **errp) +{ + VncDisplay *vd = vnc_display_find(NULL); + + if (!vd) { + error_setg(errp, "Can not find vnc display"); + return false; + } + + if (arg->has_addresses) { + if (vd->listener) { + qio_net_listener_disconnect(vd->listener); + object_unref(OBJECT(vd->listener)); + vd->listener = NULL; + } + + if (vnc_display_listen(vd, arg->addresses, NULL, errp) < 0) { + return false; + } + } + + return true; +} + +void vnc_display_open(const char *id, Error **errp) +{ + VncDisplay *vd = vnc_display_find(id); + QemuOpts *opts = qemu_opts_find(&qemu_vnc_opts, id); + g_autoptr(SocketAddressList) saddr_list = NULL; + g_autoptr(SocketAddressList) wsaddr_list = NULL; + 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_list, &wsaddr_list, + 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 (!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_list == NULL) { + return; + } + + if (reverse) { + if (vnc_display_connect(vd, saddr_list, wsaddr_list, errp) < 0) { + goto fail; + } + } else { + if (vnc_display_listen(vd, saddr_list, wsaddr_list, errp) < 0) { + goto fail; + } + } + + if (qemu_opt_get(opts, "to")) { + vnc_display_print_local_addr(vd); + } + + /* Success */ + return; + +fail: + vnc_display_close(vd); +} + +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 00000000..a60fb131 --- /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_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 00000000..016405e7 --- /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 00000000..1ac237db --- /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 00000000..2ce7b899 --- /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 00000000..0395e335 --- /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 |