[PATCH] Add a FreeRds backend, take 4

Hardening rdp.effort at gmail.com
Wed Jan 8 14:37:22 PST 2014


FreeRDS is a FreeRDP based RDP server, the server handles incoming connections and
talks RDP with the peers. FreeRds cooperates with an "out-service": the out-service creates
the content to display, and FreeRds will take care of encoding the content in the
appropriate format (bitmapUpdate, remoteFx or NsCodec).
To communicate, they use a unix socket for passing commands, and a shared buffer for
the screen content. A vblank signal sent by FreeRds via the command channel allows
to share the framebuffer nicely.
This patch adds a backend to create a FreeRDS compositor and have weston being an out-service
for FreeRds.

Compared to the previous version, this one does some code cleanup. The keycodes are now computed using
FreeRdp buildin functions which simplifies the code much. The SynchronizeKeyboard packets are now
handled so if keyboard modifiers are changed when the RDP window is not focused, they will be resynced when
the RDP client is refocused. The VirtualKeyCode packets are handled too. The missing change on Makefile.am
is also included.
---
 configure.ac             |   10 +
 src/Makefile.am          |   17 +-
 src/compositor-freerds.c | 1124 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 1150 insertions(+), 1 deletion(-)
 create mode 100644 src/compositor-freerds.c

diff --git a/configure.ac b/configure.ac
index 571bf60..b28b528 100644
--- a/configure.ac
+++ b/configure.ac
@@ -206,6 +206,15 @@ if test x$enable_rdp_compositor = xyes; then
   PKG_CHECK_MODULES(RDP_COMPOSITOR, [freerdp >= 1.1.0])
 fi
 
+AC_ARG_ENABLE([freerds-compositor], [  --enable-freerds-compositor],,
+              enable_freerds_compositor=no)
+AM_CONDITIONAL([ENABLE_FREERDS_COMPOSITOR],
+               [test x$enable_freerds_compositor = xyes])
+if test x$enable_freerds_compositor = xyes; then
+  AC_DEFINE([BUILD_FREERDS_COMPOSITOR], [1], [Build the FreeRDS compositor])
+  PKG_CHECK_MODULES(FREERDS_COMPOSITOR, [freerds-backend])
+fi
+
 AC_ARG_WITH(cairo,
 	    AS_HELP_STRING([--with-cairo=@<:@image|gl|glesv2@:>@]
 			   [Which Cairo renderer to use for the clients]),
@@ -526,6 +535,7 @@ AC_MSG_RESULT([
 	RPI Compositor			${enable_rpi_compositor}
 	FBDEV Compositor		${enable_fbdev_compositor}
 	RDP Compositor			${enable_rdp_compositor}
+	FreeRDS Compositor		${enable_freerds_compositor}
 
 	Raspberry Pi BCM headers	${have_bcm_host}
 
diff --git a/src/Makefile.am b/src/Makefile.am
index 446639c..588625e 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -117,7 +117,9 @@ module_LTLIBRARIES =				\
 	$(wayland_backend)			\
 	$(headless_backend)			\
 	$(fbdev_backend)			\
-	$(rdp_backend)
+	$(rdp_backend)				\
+	$(freerds_backend)
+	
 
 if INSTALL_RPI_COMPOSITOR
 module_LTLIBRARIES += $(rpi_backend)
@@ -280,6 +282,19 @@ rdp_backend_la_CFLAGS =			\
 rdp_backend_la_SOURCES = compositor-rdp.c
 endif
 
+if ENABLE_FREERDS_COMPOSITOR
+freerds_backend = freerds-backend.la
+freerds_backend_la_LDFLAGS = -module -avoid-version
+freerds_backend_la_LIBADD = $(COMPOSITOR_LIBS) \
+	$(FREERDS_COMPOSITOR_LIBS) \
+	../shared/libshared.la -lwinpr-input -lfreerdp-core
+freerds_backend_la_CFLAGS =	\
+	$(COMPOSITOR_CFLAGS)			\
+	$(FREERDS_COMPOSITOR_CFLAGS) \
+	$(GCC_CFLAGS)
+freerds_backend_la_SOURCES = compositor-freerds.c
+endif
+
 if HAVE_LCMS
 cms_static = cms-static.la
 cms_static_la_LDFLAGS = -module -avoid-version
diff --git a/src/compositor-freerds.c b/src/compositor-freerds.c
new file mode 100644
index 0000000..cda1bff
--- /dev/null
+++ b/src/compositor-freerds.c
@@ -0,0 +1,1124 @@
+/**
+ * Copyright © 2013 Hardening <contact at hardening-consulting.com>
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and
+ * its documentation for any purpose is hereby granted without fee, provided
+ * that the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation, and that the name of the copyright holders not be used in
+ * advertising or publicity pertaining to distribution of the software
+ * without specific, written prior permission.  The copyright holders make
+ * no representations about the suitability of this software for any
+ * purpose.  It is provided "as is" without express or implied warranty.
+ *
+ * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
+ * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
+ * CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/shm.h>
+#include <sys/mman.h>
+#include <linux/input.h>
+
+#include <freerds/backend.h>
+
+#include <freerdp/freerdp.h>
+#include <freerdp/update.h>
+#include <freerdp/input.h>
+#include <freerdp/locale/keyboard.h>
+
+#include <winpr/input.h>
+#include <winpr/stream.h>
+
+#include "compositor.h"
+#include "pixman-renderer.h"
+
+
+#define DEFAULT_AXIS_STEP_DISTANCE wl_fixed_from_int(10)
+#define FREERDS_COMMON_LENGTH 10
+#define FREERDS_MODE_FPS 60 * 1000
+
+
+struct freerds_compositor_config {
+	int width;
+	int height;
+	char *freerds_named_pipe;
+};
+
+struct freerds_compositor;
+struct freerds_output;
+
+/** @brief state of the module protocol automata */
+enum StreamState {
+	STREAM_WAITING_COMMON_HEADER,
+	STREAM_WAITING_DATA
+};
+
+struct freerds_compositor {
+	struct weston_compositor base;
+
+	struct freerds_output *output;
+	struct weston_seat seat;
+
+	bool have_seat;
+	int listening_fd;
+	struct wl_event_source *server_event_source;
+	int	client_fd;
+	struct wl_event_source *client_event_source;
+	enum StreamState streamState;
+	wStream *in_stream;
+	wStream *out_stream;
+	int expected_bytes;
+	UINT32 keyboard_layout;
+	UINT32 keyboard_type;
+	xkb_mod_index_t capslock_mod_index;
+	xkb_mod_index_t numlock_mod_index;
+	xkb_mod_index_t scrolllock_mod_index;
+
+
+	RDS_MSG_COMMON header_common;
+	RDS_MSG_SHARED_FRAMEBUFFER rds_shared_fb;
+	RDS_MSG_BEGIN_UPDATE rds_begin_update;
+	RDS_MSG_END_UPDATE rds_end_update;
+	RDS_MSG_PAINT_RECT rds_paint_rect;
+	RDS_MSG_SET_SYSTEM_POINTER rds_set_system_pointer;
+};
+
+struct freerds_output {
+	struct weston_output base;
+	struct freerds_compositor *compositor;
+	struct wl_event_source *finish_frame_timer;
+
+	int shmid;
+	void *buffer;
+	pixman_image_t *shadow_surface;
+	pixman_region32_t damagedRegion;
+	bool waitingVBlank;
+	bool outputActive;
+};
+
+static void
+freerds_compositor_config_init(struct freerds_compositor_config *config) {
+	config->width = 640;
+	config->height = 480;
+	config->freerds_named_pipe = 0;
+}
+
+static int
+freerds_send_stream(struct freerds_compositor *c, wStream *s) {
+	int toWrite, written;
+	const char *ptr;
+
+	toWrite = Stream_Length(s);
+	ptr = (const char *)Stream_Buffer(s);
+	while (toWrite) {
+		written = write(c->client_fd, ptr, toWrite);
+		if (written <= 0)
+			return -1;
+		toWrite -= written;
+		ptr += written;
+	}
+	return 0;
+}
+
+#define ROUND_DOWN(V, B) ((V) - ((V) % (B)))
+#define ROUND_UP(V, B) ((V + (B - ((V) % B))))
+
+static void
+freerds_update_framebuffer(struct freerds_compositor *c, pixman_box32_t *rect) {
+	struct freerds_output *output = c->output;
+	unsigned char *src = (unsigned char *)pixman_image_get_data(output->shadow_surface) +
+			(pixman_image_get_stride(output->shadow_surface) * rect->y1) +
+			(rect->x1 * 4);
+	unsigned char *dst = (unsigned char *)output->buffer +
+			(c->rds_shared_fb.scanline * rect->y1) +
+			(rect->x1 * 4);
+	int widthBytes = (rect->x2 - rect->x1) * 4;
+	int y;
+
+	for (y = rect->y1; y < rect->y2; y++) {
+		memcpy(dst, src, widthBytes);
+		src += pixman_image_get_stride(output->shadow_surface);
+		dst += c->rds_shared_fb.scanline;
+	}
+}
+
+static void
+freerds_refresh_region(struct freerds_compositor *c, pixman_region32_t *region)
+{
+	int nrects, i;
+	pixman_box32_t *rect;
+	RDS_MSG_PAINT_RECT *paint = &c->rds_paint_rect;
+
+	Stream_SetPosition(c->out_stream, 0);
+	freerds_server_message_write(c->out_stream, (RDS_MSG_COMMON *)&c->rds_begin_update);
+
+	rect = pixman_region32_rectangles(region, &nrects);
+	for (i = 0; i < nrects; i++, rect++) {
+		//weston_log("refresh_rect(%d,%d,%d,%d)\n", rect->x1, rect->y1, rect->x2, rect->y2);
+		freerds_update_framebuffer(c, rect);
+
+		paint->nXSrc = paint->nLeftRect = ROUND_DOWN(rect->x1, 16);
+		paint->nYSrc = paint->nTopRect = ROUND_DOWN(rect->y1, 16);
+		paint->nWidth = ROUND_UP(rect->x2, 16) - paint->nLeftRect;
+		paint->nHeight = ROUND_UP(rect->y2, 16) - paint->nTopRect;
+		freerds_server_message_write(c->out_stream, (RDS_MSG_COMMON *)&c->rds_paint_rect);
+	}
+
+	freerds_server_message_write(c->out_stream, (RDS_MSG_COMMON *)&c->rds_end_update);
+
+	Stream_SealLength(c->out_stream);
+	freerds_send_stream(c, c->out_stream);
+
+	pixman_region32_clear(region);
+
+	c->output->waitingVBlank = true;
+}
+
+
+static void
+freerds_output_start_repaint_loop(struct weston_output *output)
+{
+	uint32_t msec;
+	struct timeval tv;
+
+	gettimeofday(&tv, NULL);
+	msec = tv.tv_sec * 1000 + tv.tv_usec / 1000;
+	weston_output_finish_frame(output, msec);
+}
+
+static int
+freerds_output_repaint(struct weston_output *output_base, pixman_region32_t *damage)
+{
+	struct freerds_output *output = container_of(output_base, struct freerds_output, base);
+	struct weston_compositor *ec = output->base.compositor;
+
+	pixman_region32_union(&output->damagedRegion, &output->damagedRegion, damage);
+
+	pixman_renderer_output_set_buffer(output_base, output->shadow_surface);
+	ec->renderer->repaint_output(&output->base, damage);
+
+	if (!output->waitingVBlank && output->outputActive)
+		freerds_refresh_region(output->compositor, &output->damagedRegion);
+
+	pixman_region32_subtract(&ec->primary_plane.damage, &ec->primary_plane.damage, damage);
+
+	wl_event_source_timer_update(output->finish_frame_timer, 16);
+	return 0;
+}
+
+static void
+freerds_output_destroy(struct weston_output *output_base)
+{
+	struct freerds_output *output = (struct freerds_output *)output_base;
+
+	wl_event_source_remove(output->finish_frame_timer);
+	free(output);
+}
+
+static int
+finish_frame_handler(void *data)
+{
+	freerds_output_start_repaint_loop(data);
+	return 1;
+}
+
+static bool
+freerds_update_shm(struct freerds_output *output, int width, int height) {
+	struct freerds_compositor *c = output->compositor;
+	if (output->shmid >= 0) {
+		/* cleanup the previous shared memory if there were one */
+		if (shmdt(output->buffer) < 0)
+			weston_log("error detaching segment 0x%x, error=%s", output->shmid, strerror(errno));
+		if (shmctl(output->shmid, IPC_RMID, 0) < 0)
+			weston_log("error removing shmId %d, error=%s", output->shmid, strerror(errno));
+
+		output->shmid = -1;
+		output->buffer = 0;
+		c->rds_shared_fb.segmentId = -1;
+	}
+
+	c->rds_shared_fb.segmentId = output->shmid = shmget(IPC_PRIVATE, width * height * 4,
+			IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR);
+	if (output->shmid < 0) {
+		weston_log("unable to create a SHM, error=%s\n", strerror(errno));
+		return false;
+	}
+
+	output->buffer = shmat(output->shmid, 0, 0);
+	if (output->buffer == (void *)-1) {
+		weston_log("unable to attach to SHM, error=%s\n", strerror(errno));
+		if (shmctl(output->shmid, IPC_RMID, 0) < 0)
+				weston_log("error removing shmId %d, error=%s", output->shmid, strerror(errno));
+		return false;
+	}
+	return true;
+}
+
+
+static struct weston_mode *
+freerds_insert_new_mode(struct weston_output *output, int width, int height, int rate) {
+	struct weston_mode *ret;
+	ret = zalloc(sizeof *ret);
+	if(!ret)
+		return ret;
+	ret->width = width;
+	ret->height = height;
+	ret->refresh = rate;
+	wl_list_insert(&output->mode_list, &ret->link);
+	return ret;
+}
+
+static struct weston_mode *
+ensure_matching_mode(struct weston_output *output, struct weston_mode *target) {
+	struct weston_mode *local;
+
+	wl_list_for_each(local, &output->mode_list, link) {
+		if((local->width == target->width) && (local->height == target->height))
+			return local;
+	}
+
+	return freerds_insert_new_mode(output, target->width, target->height, FREERDS_MODE_FPS);
+}
+
+static int
+freerds_send_shared_framebuffer(struct freerds_compositor *c) {
+	Stream_SetPosition(c->out_stream, 0);
+	freerds_server_message_write(c->out_stream, (RDS_MSG_COMMON *)&c->rds_shared_fb);
+	Stream_SealLength(c->out_stream);
+
+	return freerds_send_stream(c, c->out_stream);
+}
+
+
+static int
+freerds_switch_mode(struct weston_output *output, struct weston_mode *mode)
+{
+	struct weston_mode *localMode;
+	pixman_image_t *new_shadow_buffer;
+	struct freerds_output *rdsOutput = container_of(output, struct freerds_output, base);
+	struct freerds_compositor *c = rdsOutput->compositor;
+
+	localMode = ensure_matching_mode(output, mode);
+	if (!localMode) {
+		weston_log("unable to ensure the requested mode\n");
+		return -ENOMEM;
+	}
+
+	if(localMode == output->current_mode)
+		return 0;
+
+	output->current_mode->flags &= ~WL_OUTPUT_MODE_CURRENT;
+
+	output->current_mode = localMode;
+	output->current_mode->flags |= WL_OUTPUT_MODE_CURRENT;
+
+	pixman_renderer_output_destroy(output);
+	pixman_renderer_output_create(output);
+
+	new_shadow_buffer = pixman_image_create_bits(PIXMAN_x8r8g8b8, mode->width,
+			mode->height, 0, mode->width * 4);
+	pixman_image_composite32(PIXMAN_OP_SRC, rdsOutput->shadow_surface, 0,
+			new_shadow_buffer, 0, 0,
+			0, 0,
+			0, 0, mode->width, mode->height
+	);
+
+	pixman_image_unref(rdsOutput->shadow_surface);
+	rdsOutput->shadow_surface = new_shadow_buffer;
+
+	c->rds_shared_fb.width = mode->width;
+	c->rds_shared_fb.height = mode->height;
+	c->rds_shared_fb.scanline = mode->width * 4;
+
+	if (c->client_fd >= 0) {
+		// we have a connected peer so we have to inform it of the new configuration
+		if (!freerds_update_shm(rdsOutput, mode->width, mode->height)) {
+			weston_log("unable to change the SHM configuration\n");
+			return -1;
+		}
+
+		freerds_send_shared_framebuffer(c);
+	}
+	return 0;
+}
+
+struct freerds_simple_mode {
+	int width;
+	int height;
+};
+static struct freerds_simple_mode standard_modes[] = {
+		{640, 480},
+		{800, 600},
+		{1024, 768},
+		{1280, 1024},
+
+		{0, 0}, /* /!\ the last one /!\ */
+};
+
+
+static int
+freerds_compositor_create_output(struct freerds_compositor *c, int width, int height)
+{
+	int i;
+	struct freerds_output *output;
+	struct wl_event_loop *loop;
+	struct weston_mode *currentMode, *next, *extraMode;
+	RDS_MSG_SHARED_FRAMEBUFFER *shared_fb;
+	RDS_MSG_PAINT_RECT *rect;
+	RDS_MSG_SET_SYSTEM_POINTER *system_pointer;
+
+	output = zalloc(sizeof *output);
+	if (output == NULL)
+		return -1;
+
+	wl_list_init(&output->base.mode_list);
+
+	currentMode = freerds_insert_new_mode(&output->base, width, height, FREERDS_MODE_FPS);
+	if(!currentMode)
+		goto out_free_output;
+	currentMode->flags = WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED;
+
+	for (i = 0; standard_modes[i].width; i++) {
+		if (standard_modes[i].width == width && standard_modes[i].height == height)
+			continue;
+
+		extraMode = freerds_insert_new_mode(&output->base,
+				standard_modes[i].width,
+				standard_modes[i].height, FREERDS_MODE_FPS
+		);
+		if(!extraMode)
+			goto out_output;
+	}
+
+	output->base.current_mode = output->base.native_mode = currentMode;
+	weston_output_init(&output->base, &c->base, 0, 0, width, height,
+			   WL_OUTPUT_TRANSFORM_NORMAL, 1);
+
+	output->base.make = "weston";
+	output->base.model = "freerds";
+
+	output->shmid = -1;
+	output->buffer = 0;
+
+	shared_fb = &c->rds_shared_fb;
+	shared_fb->type = RDS_SERVER_SHARED_FRAMEBUFFER;
+	shared_fb->width = width;
+	shared_fb->height = height;
+	shared_fb->bitsPerPixel = 32;
+	shared_fb->bytesPerPixel = 4;
+	shared_fb->flags = RDS_FRAMEBUFFER_FLAG_ATTACH;
+	shared_fb->scanline = width * 4;
+
+	rect = &c->rds_paint_rect;
+	rect->type = RDS_SERVER_PAINT_RECT;
+	rect->fbSegmentId = output->shmid;
+	rect->bitmapData = NULL;
+	rect->bitmapDataLength = 0;
+	rect->framebuffer = 0;
+
+	system_pointer = &c->rds_set_system_pointer;
+	system_pointer->type = RDS_SERVER_SET_SYSTEM_POINTER;
+	system_pointer->ptrType = SYSPTR_NULL;
+
+	c->rds_begin_update.type = RDS_SERVER_BEGIN_UPDATE;
+	c->rds_end_update.type = RDS_SERVER_END_UPDATE;
+
+	output->waitingVBlank = true;
+	output->outputActive = true;
+
+	pixman_region32_init(&output->damagedRegion);
+	output->shadow_surface = pixman_image_create_bits(PIXMAN_a8r8g8b8,
+			width, height,
+		    NULL,
+		    width * 4);
+	if (output->shadow_surface == NULL) {
+		weston_log("Failed to create surface for frame buffer.\n");
+		goto out_output;
+	}
+
+	if (pixman_renderer_output_create(&output->base) < 0)
+		goto out_shadow_surface;
+
+	loop = wl_display_get_event_loop(c->base.wl_display);
+	output->finish_frame_timer = wl_event_loop_add_timer(loop, finish_frame_handler, output);
+
+	output->base.start_repaint_loop = freerds_output_start_repaint_loop;
+	output->base.repaint = freerds_output_repaint;
+	output->base.destroy = freerds_output_destroy;
+	output->base.assign_planes = NULL;
+	output->base.set_backlight = NULL;
+	output->base.set_dpms = NULL;
+	output->base.switch_mode = freerds_switch_mode;
+	output->compositor = c;
+	c->output = output;
+
+	wl_list_insert(c->base.output_list.prev, &output->base.link);
+	return 0;
+
+out_shadow_surface:
+	pixman_image_unref(output->shadow_surface);
+out_output:
+	weston_output_destroy(&output->base);
+
+	wl_list_for_each_safe(currentMode, next, &output->base.mode_list, link)
+		free(currentMode);
+out_free_output:
+	free(output);
+	return -1;
+}
+
+static void
+freerds_restore(struct weston_compositor *ec)
+{
+}
+
+static void
+freerds_destroy(struct weston_compositor *ec)
+{
+	weston_compositor_shutdown(ec);
+
+	free(ec);
+}
+
+
+static void
+freerds_mouse_event(struct freerds_compositor *c, DWORD x, DWORD y, DWORD flags) {
+	wl_fixed_t wl_x, wl_y, axis;
+	uint32_t button = 0;
+
+	//weston_log("mouse event: x=%ld y=%ld flags=0x%x\n", x, y, flags);
+	if (flags & PTR_FLAGS_MOVE) {
+		if((int)x < c->output->base.width && (int)y < c->output->base.height) {
+			wl_x = wl_fixed_from_int((int)x);
+			wl_y = wl_fixed_from_int((int)y);
+			notify_motion_absolute(&c->seat, weston_compositor_get_time(), wl_x, wl_y);
+		}
+	}
+
+	if (flags & PTR_FLAGS_BUTTON1)
+		button = BTN_LEFT;
+	else if (flags & PTR_FLAGS_BUTTON2)
+		button = BTN_RIGHT;
+	else if (flags & PTR_FLAGS_BUTTON3)
+		button = BTN_MIDDLE;
+
+	if(button) {
+		notify_button(&c->seat, weston_compositor_get_time(), button,
+			(flags & PTR_FLAGS_DOWN) ? WL_POINTER_BUTTON_STATE_PRESSED : WL_POINTER_BUTTON_STATE_RELEASED
+		);
+	}
+
+	if (flags & PTR_FLAGS_WHEEL) {
+		/* DEFAULT_AXIS_STEP_DISTANCE is stolen from compositor-x11.c
+		 * The RDP specs says the lower bits of flags contains the "the number of rotation
+		 * units the mouse wheel was rotated".
+		 *
+		 * http://blogs.msdn.com/b/oldnewthing/archive/2013/01/23/10387366.aspx explains the 120 value
+		 */
+		axis = (DEFAULT_AXIS_STEP_DISTANCE * (flags & 0xff)) / 120;
+		if (flags & PTR_FLAGS_WHEEL_NEGATIVE)
+			axis = -axis;
+
+		notify_axis(&c->seat, weston_compositor_get_time(),
+					    WL_POINTER_AXIS_VERTICAL_SCROLL,
+					    axis);
+	}
+}
+
+static void
+freerds_scancode_keyboard_event(struct freerds_compositor *c, UINT32 flags, UINT32 code,
+		UINT32 keyboardType)
+{
+	uint32_t vk_code, full_code, key_code;
+	enum wl_keyboard_key_state keyState;
+	int notify = 0;
+
+	//weston_log("code=%d flags=0x%x keyb=%d\n", code, flags, keyboardType);
+	if (flags & KBD_FLAGS_DOWN) {
+		keyState = WL_KEYBOARD_KEY_STATE_PRESSED;
+		notify = 1;
+	} else if (flags & KBD_FLAGS_RELEASE) {
+		keyState = WL_KEYBOARD_KEY_STATE_RELEASED;
+		notify = 1;
+	}
+
+	if(notify) {
+		full_code = code;
+		if(flags & KBD_FLAGS_EXTENDED)
+			full_code |= KBD_FLAGS_EXTENDED;
+
+		vk_code = GetVirtualKeyCodeFromVirtualScanCode(full_code, keyboardType);
+		if(flags & KBD_FLAGS_EXTENDED)
+			vk_code |= KBDEXT;
+
+		key_code = GetKeycodeFromVirtualKeyCode(vk_code, KEYCODE_TYPE_EVDEV);
+
+		/*weston_log("code=%x ext=%d vk_code=%x scan_code=%x\n", code, (flags & KBD_FLAGS_EXTENDED) ? 1 : 0,
+				vk_code, scan_code);*/
+		notify_key(&c->seat, weston_compositor_get_time(), key_code-8, keyState,
+				STATE_UPDATE_AUTOMATIC);
+	}
+}
+
+static void
+freerds_virtual_keyboard_event(struct freerds_compositor *c, UINT32 flags, UINT32 vk_code)
+{
+	uint32_t key_code;
+	enum wl_keyboard_key_state keyState;
+	int notify = 0;
+
+	//weston_log("vk_code=%d flags=0x%x\n", vk_code, flags);
+	if (flags & KBD_FLAGS_DOWN) {
+		keyState = WL_KEYBOARD_KEY_STATE_PRESSED;
+		notify = 1;
+	} else if (flags & KBD_FLAGS_RELEASE) {
+		keyState = WL_KEYBOARD_KEY_STATE_RELEASED;
+		notify = 1;
+	}
+
+	if(notify) {
+		if(flags & KBD_FLAGS_EXTENDED)
+			vk_code |= KBDEXT;
+
+		key_code = GetKeycodeFromVirtualKeyCode(vk_code, KEYCODE_TYPE_EVDEV);
+
+		/*weston_log("code=%x ext=%d vk_code=%x scan_code=%x\n", code, (flags & KBD_FLAGS_EXTENDED) ? 1 : 0,
+				vk_code, scan_code);*/
+		notify_key(&c->seat, weston_compositor_get_time(), key_code, keyState,
+				STATE_UPDATE_AUTOMATIC);
+	}
+}
+
+
+struct rdp_to_xkb_keyboard_layout {
+	UINT32 rdpLayoutCode;
+	char *xkbLayout;
+};
+
+
+/* table reversed from
+ 	 https://github.com/awakecoding/FreeRDP/blob/master/libfreerdp/locale/xkb_layout_ids.c#L811 */
+static
+struct rdp_to_xkb_keyboard_layout rdp_keyboards[] = {
+		{KBD_ARABIC_101, "ara"},
+		{KBD_BULGARIAN, "bg"},
+		{KBD_CHINESE_TRADITIONAL_US, 0},
+		{KBD_CZECH, "cz"},
+		{KBD_DANISH, "dk"},
+		{KBD_GERMAN, "de"},
+		{KBD_GREEK, "gr"},
+		{KBD_US, "us"},
+		{KBD_SPANISH, "es"},
+		{KBD_FINNISH, "fi"},
+		{KBD_FRENCH, "fr"},
+		{KBD_HEBREW, "il"},
+		{KBD_HUNGARIAN, "hu"},
+		{KBD_ICELANDIC, "is"},
+		{KBD_ITALIAN, "it"},
+		{KBD_JAPANESE, "jp"},
+		{KBD_KOREAN, "kr"},
+		{KBD_DUTCH, "nl"},
+		{KBD_NORWEGIAN, "no"},
+		{KBD_POLISH_PROGRAMMERS, 0},
+//		{KBD_PORTUGUESE_BRAZILIAN_ABN0416, 0},
+		{KBD_ROMANIAN, 0},
+		{KBD_RUSSIAN, "ru"},
+		{KBD_CROATIAN, 0},
+		{KBD_SLOVAK, 0},
+		{KBD_ALBANIAN, 0},
+		{KBD_SWEDISH, 0},
+		{KBD_THAI_KEDMANEE, 0},
+		{KBD_TURKISH_Q, 0},
+		{KBD_URDU, 0},
+		{KBD_UKRAINIAN, 0},
+		{KBD_BELARUSIAN, 0},
+		{KBD_SLOVENIAN, 0},
+		{KBD_ESTONIAN, "ee"},
+		{KBD_LATVIAN, 0},
+		{KBD_LITHUANIAN_IBM, 0},
+		{KBD_FARSI, 0},
+		{KBD_VIETNAMESE, 0},
+		{KBD_ARMENIAN_EASTERN, 0},
+		{KBD_AZERI_LATIN, 0},
+		{KBD_FYRO_MACEDONIAN, 0},
+		{KBD_GEORGIAN, 0},
+		{KBD_FAEROESE, 0},
+		{KBD_DEVANAGARI_INSCRIPT, 0},
+		{KBD_MALTESE_47_KEY, 0},
+		{KBD_NORWEGIAN_WITH_SAMI, 0},
+		{KBD_KAZAKH, 0},
+		{KBD_KYRGYZ_CYRILLIC, 0},
+		{KBD_TATAR, 0},
+		{KBD_BENGALI, 0},
+		{KBD_PUNJABI, 0},
+		{KBD_GUJARATI, 0},
+		{KBD_TAMIL, 0},
+		{KBD_TELUGU, 0},
+		{KBD_KANNADA, 0},
+		{KBD_MALAYALAM, 0},
+		{KBD_MARATHI, 0},
+		{KBD_MONGOLIAN_CYRILLIC, 0},
+		{KBD_UNITED_KINGDOM_EXTENDED, 0},
+		{KBD_SYRIAC, 0},
+		{KBD_NEPALI, 0},
+		{KBD_PASHTO, 0},
+		{KBD_DIVEHI_PHONETIC, 0},
+		{KBD_LUXEMBOURGISH, 0},
+		{KBD_MAORI, 0},
+		{KBD_CHINESE_SIMPLIFIED_US, 0},
+		{KBD_SWISS_GERMAN, 0},
+		{KBD_UNITED_KINGDOM, 0},
+		{KBD_LATIN_AMERICAN, 0},
+		{KBD_BELGIAN_FRENCH, 0},
+		{KBD_BELGIAN_PERIOD, 0},
+		{KBD_PORTUGUESE, 0},
+		{KBD_SERBIAN_LATIN, 0},
+		{KBD_AZERI_CYRILLIC, 0},
+		{KBD_SWEDISH_WITH_SAMI, 0},
+		{KBD_UZBEK_CYRILLIC, 0},
+		{KBD_INUKTITUT_LATIN, 0},
+		{KBD_CANADIAN_FRENCH_LEGACY, "fr-legacy"},
+		{KBD_SERBIAN_CYRILLIC, 0},
+		{KBD_CANADIAN_FRENCH, 0},
+		{KBD_SWISS_FRENCH, "ch"},
+		{KBD_BOSNIAN, "unicode"},
+		{KBD_IRISH, 0},
+		{KBD_BOSNIAN_CYRILLIC, 0},
+
+		{0x00000000, 0},
+};
+
+/* taken from 2.2.7.1.6 Input Capability Set (TS_INPUT_CAPABILITYSET) */
+static char *rdp_keyboard_types[] = {
+	"",	/* 0: unused */
+	"", /* 1: IBM PC/XT or compatible (83-key) keyboard */
+	"", /* 2: Olivetti "ICO" (102-key) keyboard */
+	"", /* 3: IBM PC/AT (84-key) or similar keyboard */
+	"pc105",/* 4: IBM enhanced (101- or 102-key) keyboard */
+	"", /* 5: Nokia 1050 and similar keyboards */
+	"",	/* 6: Nokia 9140 and similar keyboards */
+	"jp106"	/* 7: Japanese keyboard */
+};
+
+static struct xkb_keymap *
+freerds_retrieve_keymap(UINT32 rdpKbLayout, UINT32 rdpKbType) {
+	struct xkb_context *xkbContext;
+	struct xkb_rule_names xkbRuleNames;
+	struct xkb_keymap *keymap;
+	int i;
+
+	memset(&xkbRuleNames, 0, sizeof(xkbRuleNames));
+	if(rdpKbType <= 7 && rdpKbType > 0)
+		xkbRuleNames.model = rdp_keyboard_types[rdpKbType];
+	else
+		xkbRuleNames.model = "pc105";
+
+	for(i = 0; rdp_keyboards[i].rdpLayoutCode; i++) {
+		if(rdp_keyboards[i].rdpLayoutCode == rdpKbLayout) {
+			xkbRuleNames.layout = rdp_keyboards[i].xkbLayout;
+			break;
+		}
+	}
+
+	keymap = NULL;
+	if(xkbRuleNames.layout) {
+		xkbContext = xkb_context_new(0);
+		if(!xkbContext) {
+			weston_log("unable to create a xkb_context\n");
+			return NULL;
+		}
+
+		weston_log("looking for keymap %s\n", xkbRuleNames.layout);
+		keymap = xkb_keymap_new_from_names(xkbContext, &xkbRuleNames, 0);
+	}
+	return keymap;
+}
+
+static void
+freerds_configure_keyboard(struct freerds_compositor *c, UINT32 layout, UINT32 keyboard_type) {
+	//weston_log("%s: layout=0x%x keyboard_type=%d\n", __FUNCTION__, layout, keyboard_type);
+	if (c->keyboard_layout == layout && c->keyboard_type == keyboard_type)
+		return;
+
+	weston_seat_init_keyboard(&c->seat,
+			freerds_retrieve_keymap(layout, keyboard_type)
+	);
+
+	c->keyboard_layout = layout;
+	c->keyboard_type = keyboard_type;
+}
+
+static void
+freerds_update_keyboard_modifiers(struct freerds_compositor *c, bool capsLock,
+		bool numLock, bool scrollLock, bool kanaLock)
+{
+	uint32_t mods_depressed, mods_latched, mods_locked, group;
+	uint32_t serial;
+	int numMask, capsMask, scrollMask;
+
+	struct weston_keyboard *keyboard = c->seat.keyboard;
+	struct xkb_state *state = keyboard->xkb_state.state;
+	struct weston_xkb_info *xkb_info = keyboard->xkb_info;
+
+	mods_depressed = xkb_state_serialize_mods(state, XKB_STATE_DEPRESSED);
+	mods_latched = xkb_state_serialize_mods(state, XKB_STATE_LATCHED);
+	mods_locked = xkb_state_serialize_mods(state, XKB_STATE_LOCKED);
+	group = xkb_state_serialize_group(state, XKB_STATE_EFFECTIVE);
+
+	numMask = (1 << xkb_info->mod2_mod);
+	capsMask = (1 << xkb_info->caps_mod);
+	scrollMask = (1 << xkb_info->scroll_led); // TODO: don't rely on the led status
+
+	mods_locked = capsLock ? (mods_locked | capsMask) : (mods_locked & ~capsMask);
+	mods_locked = numLock ? (mods_locked | numMask) : (mods_locked & ~numLock);
+	mods_locked = scrollLock ? (mods_locked | scrollMask) : (mods_locked & ~scrollMask);
+
+	xkb_state_update_mask(state, mods_depressed, mods_latched, mods_locked, 0, 0, group);
+
+	serial = wl_display_next_serial(c->base.wl_display);
+	notify_modifiers(&c->seat, serial);
+}
+
+static int
+freerds_send_disable_pointer(struct freerds_compositor *c) {
+	Stream_SetPosition(c->out_stream, 0);
+	freerds_server_message_write(c->out_stream, (RDS_MSG_COMMON *)&c->rds_set_system_pointer);
+	Stream_SealLength(c->out_stream);
+	return freerds_send_stream(c, c->out_stream);
+}
+
+
+static int
+freerds_treat_message(struct freerds_compositor *c, wStream *s) {
+	RDS_MSG_CAPABILITIES capabilities;
+	RDS_MSG_MOUSE_EVENT mouse_event;
+	RDS_MSG_SUPPRESS_OUTPUT suppress_output;
+	RDS_MSG_SCANCODE_KEYBOARD_EVENT scancode_event;
+	RDS_MSG_VIRTUAL_KEYBOARD_EVENT vk_event;
+	RDS_MSG_SYNCHRONIZE_KEYBOARD_EVENT sync_keyboard_event;
+	RDS_MSG_REFRESH_RECT refresh_rect_event;
+
+
+	RECTANGLE_16 *rect16;
+	unsigned int i;
+	struct weston_mode *currentMode, targetMode;
+
+	//weston_log("message type %d\n", c->header_common.type);
+	switch (c->header_common.type) {
+	case RDS_CLIENT_CAPABILITIES:
+		if (freerds_read_capabilities(s, &capabilities) < 0) {
+			weston_log("invalid capabilities message\n");
+			return -1;
+		}
+		weston_seat_init(&c->seat, &c->base, "freerds");
+		weston_seat_init_pointer(&c->seat);
+
+		freerds_configure_keyboard(c, capabilities.KeyboardLayout, capabilities.KeyboardType);
+		c->have_seat = true;
+
+		currentMode = c->output->base.current_mode;
+		if (capabilities.DesktopWidth != currentMode->width || capabilities.DesktopHeight != currentMode->height) {
+			// mode switching will send the shared framebuffer
+			targetMode.width = capabilities.DesktopWidth;
+			targetMode.height = capabilities.DesktopHeight;
+			weston_output_switch_mode(&c->output->base, &targetMode, 1, WESTON_MODE_SWITCH_SET_NATIVE);
+		} else {
+			if (c->output->shmid < 0)
+				freerds_update_shm(c->output, capabilities.DesktopWidth, capabilities.DesktopHeight);
+			if (freerds_send_shared_framebuffer(c) < 0)
+				weston_log("unable to send shared framebuffer, errno=%d\n", errno);
+		}
+
+		if (freerds_send_disable_pointer(c) < 0)
+			weston_log("unable to disable client-side pointer, errno=%d\n", errno);
+		break;
+
+	case RDS_CLIENT_MOUSE_EVENT:
+		if (freerds_read_mouse_event(s, &mouse_event) < 0) {
+			weston_log("invalid mouse event message\n");
+			return -1;
+		}
+		freerds_mouse_event(c, mouse_event.x, mouse_event.y, mouse_event.flags);
+		break;
+
+	case RDS_CLIENT_SCANCODE_KEYBOARD_EVENT:
+		if (freerds_read_scancode_keyboard_event(s, &scancode_event) < 0) {
+			weston_log("invalid scancode keyboard event message\n");
+			return -1;
+		}
+		freerds_scancode_keyboard_event(c, scancode_event.flags, scancode_event.code,
+				scancode_event.keyboardType);
+		break;
+
+	case RDS_CLIENT_VIRTUAL_KEYBOARD_EVENT:
+		if (freerds_read_virtual_keyboard_event(s, &vk_event) < 0) {
+			weston_log("invalid virtual keycode event message\n");
+			return -1;
+		}
+		freerds_virtual_keyboard_event(c, vk_event.flags, vk_event.code);
+		break;
+
+	case RDS_CLIENT_VBLANK_EVENT:
+		c->output->waitingVBlank = false;
+		if (c->output->outputActive && pixman_region32_not_empty(&c->output->damagedRegion))
+			freerds_refresh_region(c, &c->output->damagedRegion);
+		break;
+
+	case RDS_CLIENT_REFRESH_RECT:
+		if (freerds_read_refresh_rect(s, &refresh_rect_event) < 0) {
+			weston_log("invalid refresh_rect message\n");
+			return -1;
+		}
+
+		rect16 = refresh_rect_event.areasToRefresh;
+		for (i = 0; i < refresh_rect_event.numberOfAreas; i++, rect16++) {
+			pixman_region32_union_rect(&c->output->damagedRegion, &c->output->damagedRegion,
+					rect16->left, rect16->top,
+					(rect16->right - rect16->left),
+					(rect16->bottom - rect16->top));
+		}
+
+		if (!c->output->waitingVBlank && c->output->outputActive)
+			freerds_refresh_region(c, &c->output->damagedRegion);
+		break;
+
+	case RDS_CLIENT_SUPPRESS_OUTPUT:
+		if (freerds_read_suppress_output(s, &suppress_output) < 0) {
+			weston_log("invalid suppress_output message\n");
+			return -1;
+		}
+
+		c->output->outputActive = suppress_output.activeOutput;
+		// TODO: freeze the compositor when output is suppressed ?
+		break;
+
+	case RDS_CLIENT_SYNCHRONIZE_KEYBOARD_EVENT:
+		if (freerds_read_synchronize_keyboard_event(s, &sync_keyboard_event) < 0) {
+			weston_log("invalid synchronize keyboard message\n");
+			return -1;
+		}
+
+		freerds_update_keyboard_modifiers(c,
+				sync_keyboard_event.flags & KBD_SYNC_CAPS_LOCK,
+				sync_keyboard_event.flags & KBD_SYNC_NUM_LOCK,
+				sync_keyboard_event.flags & KBD_SYNC_SCROLL_LOCK,
+				sync_keyboard_event.flags & KBD_SYNC_KANA_LOCK
+		);
+		break;
+
+	case RDS_CLIENT_UNICODE_KEYBOARD_EVENT:
+	case RDS_CLIENT_EXTENDED_MOUSE_EVENT:
+	default:
+		weston_log("not handled yet, %d\n", c->header_common.type);
+		break;
+	}
+	return 0;
+}
+
+static void
+freerds_kill_client(struct freerds_compositor *c) {
+	wl_event_source_remove(c->client_event_source);
+	c->client_event_source = 0;
+	if (c->have_seat) {
+		weston_seat_release_pointer(&c->seat);
+		weston_seat_release_keyboard(&c->seat);
+		weston_seat_release(&c->seat);
+	}
+
+	close(c->client_fd);
+	c->client_fd = -1;
+
+	c->expected_bytes = FREERDS_COMMON_LENGTH;
+	c->streamState = STREAM_WAITING_COMMON_HEADER;
+	c->keyboard_layout = 0;
+	c->keyboard_type = 0;
+	c->have_seat = false;
+	c->output->waitingVBlank = true;
+	c->output->outputActive = true;
+}
+
+static int
+freerds_client_activity(int fd, uint32_t mask, void *data) {
+	struct freerds_compositor *c = (struct freerds_compositor *)data;
+	int ret;
+
+	if (!(mask & WL_EVENT_READABLE))
+		return 0;
+
+	ret = read(fd, Stream_Pointer(c->in_stream), c->expected_bytes);
+	if (ret <= 0) {
+		weston_log("connection closed fd=%d client_fd=%d\n", fd, c->client_fd);
+		freerds_kill_client(c);
+		return 0;
+	}
+
+	Stream_Seek(c->in_stream, ret);
+	c->expected_bytes -= ret;
+
+	if (c->expected_bytes)
+		return 0;
+
+	Stream_SealLength(c->in_stream);
+	Stream_SetPosition(c->in_stream, 0);
+
+	switch (c->streamState) {
+	case STREAM_WAITING_COMMON_HEADER:
+		//weston_log("reading %d for common header\n", ret);
+		freerds_read_common_header(c->in_stream, &c->header_common);
+		if (c->header_common.length > FREERDS_COMMON_LENGTH) {
+			c->streamState = STREAM_WAITING_DATA;
+			c->expected_bytes = c->header_common.length - FREERDS_COMMON_LENGTH;
+			Stream_EnsureCapacity(c->in_stream, c->header_common.length);
+			Stream_SetPosition(c->in_stream, FREERDS_COMMON_LENGTH);
+		} else {
+			freerds_treat_message(c, c->in_stream);
+
+			Stream_SetPosition(c->in_stream, 0);
+			c->expected_bytes = FREERDS_COMMON_LENGTH;
+		}
+		break;
+	case STREAM_WAITING_DATA:
+		Stream_SetPosition(c->in_stream, FREERDS_COMMON_LENGTH);
+		freerds_treat_message(c, c->in_stream);
+
+		Stream_SetPosition(c->in_stream, 0);
+		c->streamState = STREAM_WAITING_COMMON_HEADER;
+		c->expected_bytes = FREERDS_COMMON_LENGTH;
+		break;
+	default:
+		weston_log("unknown module protocol state\n");
+		return -1;
+	}
+	return 0;
+}
+
+static int
+freerds_named_pipe_activity(int fd, uint32_t mask, void *data) {
+	struct freerds_compositor *c = (struct freerds_compositor *)data;
+	struct wl_event_loop *loop;
+
+	if (c->client_fd != -1) {
+		weston_log("dropping existing client");
+		freerds_kill_client(c);
+	}
+
+	c->client_fd = accept(c->listening_fd, 0, 0);
+	if (c->client_fd >= 0) {
+		loop = wl_display_get_event_loop(c->base.wl_display);
+		c->client_event_source = wl_event_loop_add_fd(loop, c->client_fd, WL_EVENT_READABLE,
+				freerds_client_activity, c);
+	}
+	return 0;
+}
+
+
+static struct weston_compositor *
+freerds_compositor_create(struct wl_display *display,
+		struct freerds_compositor_config *config,
+		int *argc, char *argv[], struct weston_config *wconfig)
+{
+	struct freerds_compositor *c;
+	struct wl_event_loop *loop;
+	struct sockaddr_un remote;
+	int len;
+
+	c = zalloc(sizeof *c);
+	if (c == NULL)
+		return NULL;
+
+	if (weston_compositor_init(&c->base, display, argc, argv, wconfig) < 0)
+		goto err_free;
+
+	c->base.destroy = freerds_destroy;
+	c->base.restore = freerds_restore;
+	c->client_fd = -1;
+
+	if (pixman_renderer_init(&c->base) < 0)
+		goto err_compositor;
+
+	if (freerds_compositor_create_output(c, config->width, config->height) < 0)
+		goto err_compositor;
+
+	if (!config->freerds_named_pipe) {
+		weston_log("no socket path given");
+		goto err_output;
+	}
+
+	c->listening_fd = socket(AF_UNIX, SOCK_STREAM, 0);
+	if (c->listening_fd < 0) {
+		weston_log("unable to create the listening socket\n");
+		goto err_output;
+	}
+
+	memset(&remote, 0, sizeof(remote));
+	remote.sun_family = AF_UNIX;
+	strcpy(remote.sun_path, config->freerds_named_pipe);
+	len = strlen(remote.sun_path) + sizeof(remote.sun_family);
+	if (bind(c->listening_fd, (struct sockaddr *)&remote, len) < 0) {
+		weston_log("unable to bind the named pipe, errno=%d path=%s\n",
+				errno, config->freerds_named_pipe);
+		goto err_socket;
+	}
+
+	if (!listen(c->listening_fd, 1) < 0) {
+		weston_log("unable to listen on the named pipe, errno=%d path=%s\n",
+				errno, config->freerds_named_pipe);
+		goto err_socket;
+	}
+
+	c->in_stream = Stream_New(NULL, FREERDS_COMMON_LENGTH);
+	c->out_stream = Stream_New(NULL, 65536);
+	c->expected_bytes = FREERDS_COMMON_LENGTH;
+	c->streamState = STREAM_WAITING_COMMON_HEADER;
+	loop = wl_display_get_event_loop(c->base.wl_display);
+	c->server_event_source = wl_event_loop_add_fd(loop, c->listening_fd, WL_EVENT_READABLE,
+													freerds_named_pipe_activity, c);
+
+	return &c->base;
+
+err_socket:
+	close(c->listening_fd);
+err_output:
+	weston_output_destroy(&c->output->base);
+err_compositor:
+	weston_compositor_shutdown(&c->base);
+err_free:
+	free(c);
+	return NULL;
+}
+
+WL_EXPORT struct weston_compositor *
+backend_init(struct wl_display *display, int *argc, char *argv[],
+	     struct weston_config *wconfig)
+{
+	struct freerds_compositor_config config;
+	freerds_compositor_config_init(&config);
+	int major, minor, revision;
+
+	freerdp_get_version(&major, &minor, &revision);
+	weston_log("using FreeRDP version %d.%d.%d\n", major, minor, revision);
+
+	const struct weston_option freerds_options[] = {
+		{ WESTON_OPTION_STRING, "freerds-pipe", 0, &config.freerds_named_pipe },
+		{ WESTON_OPTION_UNSIGNED_INTEGER, "width", 0, &config.width },
+		{ WESTON_OPTION_UNSIGNED_INTEGER, "height", 0, &config.height },
+	};
+
+	parse_options(freerds_options, ARRAY_LENGTH(freerds_options), argc, argv);
+	return freerds_compositor_create(display, &config, argc, argv, wconfig);
+}
-- 
1.8.1.2



More information about the wayland-devel mailing list