File gnome-shell-xrdesktop.patch of Package gnome-shell-xrdesktop

diff --git a/.gitmodules b/.gitmodules
index bb57dfc6c..5ab4a386d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
 [submodule "subprojects/gvc"]
 	path = subprojects/gvc
-	url = ../../GNOME/libgnome-volume-control.git
+	url = https://gitlab.gnome.org/GNOME/libgnome-volume-control.git
diff --git a/meson.build b/meson.build
index 70ba71106..9fed904d7 100644
--- a/meson.build
+++ b/meson.build
@@ -95,6 +95,9 @@ x11_dep = dependency('x11')
 schemas_dep = dependency('gsettings-desktop-schemas', version: schemas_req)
 gnome_desktop_dep = dependency('gnome-desktop-4', version: gnome_desktop_req)
 
+xrdesktop_dep = dependency('xrdesktop-0.16', required: true)
+inputsynth_dep = dependency('libinputsynth-0.16', required: true)
+
 nm_deps = []
 if get_option('networkmanager')
   nm_deps += dependency('libnm', version: nm_req)
diff --git a/src/main.c b/src/main.c
index dec0efa8c..d65fe31cd 100644
--- a/src/main.c
+++ b/src/main.c
@@ -26,6 +26,9 @@
 #include <elf.h>
 #endif
 
+#include "shell-vr-mirror.h"
+#include "shell-vr-mirror-dbus.h"
+
 #include "shell-global.h"
 #include "shell-global-private.h"
 #include "shell-perf-log.h"
@@ -689,6 +692,8 @@ main (int argc, char **argv)
 
   g_log_set_writer_func (default_log_writer, NULL, NULL);
 
+  shell_vr_mirror_dbus_init ();
+
   shell_profiler_init ();
 
   if (meta_context_get_compositor_type (context) == META_COMPOSITOR_TYPE_WAYLAND)
diff --git a/src/meson.build b/src/meson.build
index 070312bd0..3e3a6eff6 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -66,6 +66,8 @@ gnome_shell_deps = [
   gcr_dep,
   libsystemd_dep,
   libpipewire_dep,
+  xrdesktop_dep,
+  inputsynth_dep
 ]
 
 gnome_shell_deps += nm_deps
@@ -130,7 +132,9 @@ libshell_private_headers = [
   'shell-global-private.h',
   'shell-tray-icon-private.h',
   'shell-window-tracker-private.h',
-  'shell-wm-private.h'
+  'shell-wm-private.h',
+  'shell-vr-mirror.h',
+  'shell-vr-mirror-dbus.h'
 ]
 
 libshell_sources = [
@@ -161,7 +165,9 @@ libshell_sources = [
   'shell-window-preview-layout.c',
   'shell-window-tracker.c',
   'shell-wm.c',
-  'shell-workspace-background.c'
+  'shell-workspace-background.c',
+  'shell-vr-mirror.c',
+  'shell-vr-mirror-dbus.c'
 ]
 
 if have_networkmanager
diff --git a/src/shell-vr-mirror-dbus.c b/src/shell-vr-mirror-dbus.c
new file mode 100644
index 000000000..261bdbe90
--- /dev/null
+++ b/src/shell-vr-mirror-dbus.c
@@ -0,0 +1,121 @@
+#include "shell-vr-mirror-dbus.h"
+#include "shell-vr-mirror.h"
+
+#define BUS_NAME "org.gnome.Shell.XR"
+static const gchar introspection_xml[] =
+  "<node>"
+  "  <interface name='org.gnome.Shell.XR'>"
+  "    <property name='enabled' type='b' access='readwrite' />"
+  "  </interface>"
+  "</node>";
+
+static void
+_vrshell_set (gboolean enable)
+{
+  ShellVRMirror *vr_mirror = shell_vr_mirror_get_instance ();
+  if (enable)
+    {
+      if (vr_mirror)
+        {
+          g_print ("DBus: Enable XR (already enabled)\n");
+        }
+      else
+        {
+          g_print ("DBus: Enable XR\n");
+          shell_vr_mirror_create_instance ();
+        }
+    }
+  else
+    {
+      g_print ("DBus: Disable XR\n");
+      if (vr_mirror)
+        {
+          shell_vr_mirror_destroy_instance ();
+        }
+    }
+}
+
+static GVariant *
+_handle_dbus_property_get (GDBusConnection *connection,
+                           const gchar *sender,
+                           const gchar *object_path,
+                           const gchar *interface_name,
+                           const gchar *property_name,
+                           GError **error,
+                           gpointer user_data)
+{
+  if (g_strcmp0 (property_name, "enabled") == 0)
+    {
+      ShellVRMirror *instance = shell_vr_mirror_get_instance ();
+      gboolean vr_running = (instance != NULL);
+      GVariant *ret = g_variant_new_boolean (vr_running);
+      return ret;
+    }
+  else
+    {
+      g_print ("Error, only XR enabled property supported!\n");
+      return NULL;
+    }
+}
+
+static gboolean
+_handle_dbus_property_set (GDBusConnection *connection,
+                           const gchar *sender,
+                           const gchar *object_path,
+                           const gchar *interface_name,
+                           const gchar *property_name,
+                           GVariant *value,
+                           GError **error,
+                           gpointer user_data)
+{
+  if (g_strcmp0 (property_name, "enabled") == 0)
+    {
+      if (!g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
+        {
+          g_print ("Error, XR enabled property must be bool!\n");
+          return FALSE;
+        }
+      gboolean setTo = g_variant_get_boolean (value);
+      _vrshell_set (setTo);
+      return TRUE;
+    }
+  return FALSE;
+}
+
+static const GDBusInterfaceVTable interface_vtable =
+{
+  NULL,
+  _handle_dbus_property_get,
+  _handle_dbus_property_set
+};
+
+static void
+_on_dbus_bus_aquired (GDBusConnection *connection,
+                      const gchar     *name,
+                      gpointer         user_data)
+{
+  GDBusNodeInfo *introspection_data = g_dbus_node_info_new_for_xml (
+    introspection_xml, NULL);
+
+  g_dbus_connection_register_object (connection,
+                                     "/org/gnome/Shell/XR",
+                                     introspection_data->interfaces[0],
+                                     &interface_vtable,
+                                     NULL,  /* user_data */
+                                     NULL,  /* user_data_free_func */
+                                     NULL); /* GError** */
+}
+
+void
+shell_vr_mirror_dbus_init (void)
+{
+  g_bus_own_name (G_BUS_TYPE_SESSION,
+                  BUS_NAME,
+                  G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT |
+                  G_BUS_NAME_OWNER_FLAGS_REPLACE,
+                  _on_dbus_bus_aquired,
+                  NULL,
+                  NULL,
+                  NULL,
+                  NULL);
+}
diff --git a/src/shell-vr-mirror-dbus.h b/src/shell-vr-mirror-dbus.h
new file mode 100644
index 000000000..1ee4da3c7
--- /dev/null
+++ b/src/shell-vr-mirror-dbus.h
@@ -0,0 +1,3 @@
+
+void
+shell_vr_mirror_dbus_init (void);
diff --git a/src/shell-vr-mirror.c b/src/shell-vr-mirror.c
new file mode 100644
index 000000000..6c5dc8a32
--- /dev/null
+++ b/src/shell-vr-mirror.c
@@ -0,0 +1,1786 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+#include "config.h"
+
+#include <clutter/clutter.h>
+#include <glib/gprintf.h>
+
+#include <graphene.h>
+#include <glib.h>
+
+#include <inputsynth.h>
+
+#include <meta/meta-shaped-texture.h>
+#include <meta/display.h>
+
+#include "shell-vr-mirror.h"
+#include "shell-global.h"
+
+#include <meta/util.h>
+
+#include <GL/gl.h>
+
+#include <meta/meta-cursor-tracker.h>
+#include <meta/meta-workspace-manager.h>
+
+/* singleton variable for shell_vr_mirror_get_instance() */
+static ShellVRMirror *shell_vr_mirror_instance = NULL;
+
+/* 1 pixel/meter = 0.0254 dpi */
+#define SHELL_VR_PIXELS_PER_METER 720.0f
+#define SHELL_VR_DESKTOP_PLANE_DISTANCE 3.5f
+#define SHELL_VR_LAYER_DISTANCE 0.1f
+
+#define DEFAULT_LEVEL 0.5
+
+#define SHELL_VR_COPY_COMPUTE 1
+
+// core gl prototypes
+#undef GL_VERSION_1_1 // hack
+#include <GL/glcorearb.h>
+
+// macro to generate a static global declaration like static GLenum (*_glGetError) = NULL;
+#define GL_DECL(TYPE, FUNC) static TYPE _ ## FUNC = NULL;
+
+// macro that invokes the argument with each (opengl function prototype, opengl function name) tuple we load
+#define FOR_EACH_GL_FUNC(_)                                                                        \
+    _(PFNGLDELETEFRAMEBUFFERSPROC, glDeleteFramebuffers)                                             \
+    _(PFNGLDEBUGMESSAGECALLBACKPROC, glDebugMessageCallback)                                         \
+    _(PFNGLGENFRAMEBUFFERSPROC, glGenFramebuffers)                                                   \
+    _(PFNGLCREATESHADERPROC, glCreateShader)                                                         \
+    _(PFNGLSHADERSOURCEPROC, glShaderSource)                                                         \
+    _(PFNGLCOMPILESHADERPROC, glCompileShader)                                                       \
+    _(PFNGLGETSHADERIVPROC, glGetShaderiv)                                                           \
+    _(PFNGLGETSHADERINFOLOGPROC, glGetShaderInfoLog)                                                 \
+    _(PFNGLCREATEPROGRAMPROC, glCreateProgram)                                                       \
+    _(PFNGLATTACHSHADERPROC, glAttachShader)                                                         \
+    _(PFNGLLINKPROGRAMPROC, glLinkProgram)                                                           \
+    _(PFNGLGETPROGRAMIVPROC, glGetProgramiv)                                                         \
+    _(PFNGLGETPROGRAMINFOLOGPROC, glGetProgramInfoLog)                                               \
+    _(PFNGLDELETESHADERPROC, glDeleteShader)                                                         \
+    _(PFNGLGENBUFFERSPROC, glGenBuffers)                                                             \
+    _(PFNGLGENVERTEXARRAYSPROC, glGenVertexArrays)                                                   \
+    _(PFNGLBINDVERTEXARRAYPROC, glBindVertexArray)                                                   \
+    _(PFNGLBINDBUFFERPROC, glBindBuffer)                                                             \
+    _(PFNGLBUFFERDATAPROC, glBufferData)                                                             \
+    _(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer)                                           \
+    _(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray)                                   \
+    _(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation)                                             \
+    _(PFNGLBINDFRAMEBUFFERPROC, glBindFramebuffer)                                                   \
+    _(PFNGLFRAMEBUFFERTEXTURE2DPROC, glFramebufferTexture2D)                                         \
+    _(PFNGLUSEPROGRAMPROC, glUseProgram)                                                             \
+    _(PFNGLUNIFORMMATRIX4FVPROC, glUniformMatrix4fv)                                                 \
+    _(PFNGLBLITNAMEDFRAMEBUFFERPROC, glBlitNamedFramebuffer)                                         \
+    _(PFNGLUNIFORM3FPROC, glUniform3f)                                                               \
+    _(PFNGLUNIFORM4FPROC, glUniform4f) \
+    _(PFNGLGETERRORPROC, glGetError) \
+    _(PFNGLFINISHPROC, glFinish) \
+    _(PFNGLFLUSHPROC, glFlush) \
+    _(PFNGLGETSTRINGPROC, glGetString) \
+    _(PFNGLGETTEXLEVELPARAMETERIVPROC, glGetTexLevelParameteriv) \
+    _(PFNGLGENTEXTURESPROC, glGenTextures) \
+    _(PFNGLDELETETEXTURESPROC, glDeleteTextures) \
+    _(PFNGLTEXPARAMETERIPROC, glTexParameteri) \
+    _(PFNGLBINDTEXTUREPROC, glBindTexture) \
+    _(PFNGLCREATEMEMORYOBJECTSEXTPROC, glCreateMemoryObjectsEXT) \
+    _(PFNGLDELETEMEMORYOBJECTSEXTPROC, glDeleteMemoryObjectsEXT) \
+    _(PFNGLMEMORYOBJECTPARAMETERIVEXTPROC, glMemoryObjectParameterivEXT) \
+    _(PFNGLGETMEMORYOBJECTPARAMETERIVEXTPROC, glGetMemoryObjectParameterivEXT) \
+    _(PFNGLIMPORTMEMORYFDEXTPROC, glImportMemoryFdEXT) \
+    _(PFNGLTEXSTORAGEMEM2DEXTPROC, glTexStorageMem2DEXT) \
+    _(PFNGLCOPYIMAGESUBDATAPROC, glCopyImageSubData) \
+    _(PFNGLNAMEDFRAMEBUFFERTEXTUREPROC, glNamedFramebufferTexture) \
+    _(PFNGLNAMEDFRAMEBUFFERTEXTURELAYERPROC, glNamedFramebufferTextureLayer) \
+    _(PFNGLCHECKNAMEDFRAMEBUFFERSTATUSPROC, glCheckNamedFramebufferStatus) \
+    _(PFNGLCREATEFRAMEBUFFERSPROC, glCreateFramebuffers) \
+    _(PFNGLDRAWARRAYSPROC, glDrawArrays) \
+    _(PFNGLUNIFORM1IPROC, glUniform1i) \
+    _(PFNGLUNIFORM1FPROC, glUniform1f) \
+    _(PFNGLBINDTEXTUREUNITPROC, glBindTextureUnit) \
+    _(PFNGLBINDIMAGETEXTUREPROC, glBindImageTexture) \
+    _(PFNGLDISPATCHCOMPUTEPROC, glDispatchCompute) \
+    _(PFNGLMEMORYBARRIERPROC, glMemoryBarrier) \
+
+
+// generates a global declaration for each gl func listed in FOR_EACH_GL_FUNC
+FOR_EACH_GL_FUNC(GL_DECL)
+
+// macro to load an opengl function pointer with cogl_get_proc_address
+#define LOAD_GL_FUNC(TYPE, FUNC) \
+    _ ## FUNC = (TYPE) cogl_get_proc_address(#FUNC); \
+    if (_ ## FUNC == NULL) \
+    { \
+            g_printerr("Failed to load OpenGL function " #FUNC ); \
+        return FALSE; \
+    } \
+
+static gboolean
+_load_gl_functions(void)
+{
+    FOR_EACH_GL_FUNC(LOAD_GL_FUNC)
+    return TRUE;
+}
+
+#define ENUM_TO_STR(r) case r: return #r
+
+static const gchar*
+gl_error_string (GLenum code)
+{
+  switch (code)
+    {
+      ENUM_TO_STR(GL_NO_ERROR);
+      ENUM_TO_STR(GL_INVALID_ENUM);
+      ENUM_TO_STR(GL_INVALID_VALUE);
+      ENUM_TO_STR(GL_INVALID_OPERATION);
+      ENUM_TO_STR(GL_INVALID_FRAMEBUFFER_OPERATION);
+      ENUM_TO_STR(GL_OUT_OF_MEMORY);
+      ENUM_TO_STR(GL_STACK_UNDERFLOW);
+      ENUM_TO_STR(GL_STACK_OVERFLOW);
+      default:
+        return "UNKNOWN GL Error";
+    }
+}
+
+static gboolean
+is_grab_op_valid (MetaGrabOp grab_op)
+{
+  switch (grab_op)
+    {
+      case META_GRAB_OP_NONE:
+      case META_GRAB_OP_MOVING:
+      case META_GRAB_OP_RESIZING_NW:
+      case META_GRAB_OP_RESIZING_N:
+      case META_GRAB_OP_RESIZING_NE:
+      case META_GRAB_OP_RESIZING_E:
+      case META_GRAB_OP_RESIZING_SW:
+      case META_GRAB_OP_RESIZING_S:
+      case META_GRAB_OP_RESIZING_SE:
+      case META_GRAB_OP_RESIZING_W:
+      case META_GRAB_OP_KEYBOARD_MOVING:
+      case META_GRAB_OP_KEYBOARD_RESIZING_UNKNOWN:
+      case META_GRAB_OP_KEYBOARD_RESIZING_NW:
+      case META_GRAB_OP_KEYBOARD_RESIZING_N:
+      case META_GRAB_OP_KEYBOARD_RESIZING_NE:
+      case META_GRAB_OP_KEYBOARD_RESIZING_E:
+      case META_GRAB_OP_KEYBOARD_RESIZING_SW:
+      case META_GRAB_OP_KEYBOARD_RESIZING_S:
+      case META_GRAB_OP_KEYBOARD_RESIZING_SE:
+      case META_GRAB_OP_KEYBOARD_RESIZING_W:
+        return TRUE;
+      default:
+        return FALSE;
+    }
+}
+
+static void
+gl_check_error_func (const char *prefix, const char *file, const int line)
+{
+  GLenum err = 0;
+  while ((err = _glGetError ()) != GL_NO_ERROR)
+    {
+      printf ("%s:%i: GL ERROR: %s - %s\n", file, line, prefix,
+              gl_error_string (err));
+    }
+}
+
+#define gl_check_error(PREFIX) gl_check_error_func (PREFIX, __FILE__, __LINE__)
+
+static const gchar *
+gl_get_vendor (void)
+{
+  static const gchar *vendor = NULL;
+  if (!vendor)
+    vendor = (const gchar *) _glGetString (GL_VENDOR);
+  return vendor;
+}
+
+static const char* shader =
+        "// Copyright 2021-2023, Collabora Ltd.\n" \
+        "// Author: Jakob Bornecrantz <jakob@collabora.com>\n" \
+        "// SPDX-License-Identifier: MIT or BSL-1.0 or Apache-2.0\n" \
+
+        "#version 450\n" \
+
+        "layout (local_size_x = 16, local_size_y = 16) in;\n" \
+
+        "layout (binding = 0) uniform sampler2D source;\n" \
+        "layout (rgba8, binding = 0) uniform image2D target;\n" \
+
+        "vec2 position_to_uv(ivec2 extent, uint ix, uint iy)\n" \
+        "{\n" \
+        "    // Turn the index into floating point.\n" \
+        "    vec2 xy = vec2(float(ix), float(iy));\n" \
+
+        "    // The inverse of the extent of the target image is the pixel size in [0 .. 1] space.\n" \
+        "    vec2 extent_pixel_size = vec2(1.0 / float(extent.x), 1.0 / float(extent.y));\n" \
+
+        "    // Per-target pixel we move the size of the pixels.\n" \
+        "    vec2 uv = xy * extent_pixel_size;\n" \
+
+        "    // Emulate a triangle sample position by offset half target pixel size.\n" \
+        "    uv = uv + extent_pixel_size / 2.0;\n" \
+
+        "    return uv;\n" \
+        "}\n" \
+
+        "void main() {\n" \
+        "    uint ix = gl_GlobalInvocationID.x;\n" \
+        "    uint iy = gl_GlobalInvocationID.y;\n" \
+
+        "    ivec2 extent = imageSize(target);\n" \
+        "    if (ix >= extent.x || iy >= extent.y) {\n" \
+        "        return;\n" \
+        "    }\n" \
+
+        "    // Get the UV we should sample from.\n" \
+        "    vec2 uv = position_to_uv(extent, ix, iy);\n" \
+
+        "    // Do the sample.\n" \
+        "    vec4 rgba = texture(source, uv);\n" \
+
+        "    // And finally write out.\n" \
+        "    imageStore(target, ivec2(ix, iy), rgba);\n" \
+        "}\n";
+
+struct gl_copy_helper
+{
+    GLuint shader_program_id;
+};
+
+static gboolean
+gl_copy_helper_init (struct gl_copy_helper *self)
+{
+    GLuint shader_id = _glCreateShader(GL_COMPUTE_SHADER);
+    const GLchar* shader_source[1];
+    shader_source[0] = shader;
+    // printf("Compute Shader:\n%s\n", shader_source);
+    _glShaderSource(shader_id, 1, shader_source, NULL);
+    gl_check_error ("_glShaderSource");
+
+    _glCompileShader(shader_id);
+    gl_check_error ("_glCompileShader");
+
+    int compute_compile_res;
+    _glGetShaderiv(shader_id, GL_COMPILE_STATUS, &compute_compile_res);
+    gl_check_error ("_glGetShaderiv");
+
+    if (!compute_compile_res) {
+        char info_log[512];
+        _glGetShaderInfoLog(shader_id, 512, NULL, info_log);
+        gl_check_error ("_glGetShaderInfoLog");
+
+        printf("Compute Shader failed to compile: %s\n", info_log);
+        return FALSE;
+    } else {
+        printf("Successfully compiled compute shader!\n");
+    }
+
+    self->shader_program_id = _glCreateProgram();
+    _glAttachShader(self->shader_program_id, shader_id);
+    gl_check_error ("_glAttachShader");
+
+    _glLinkProgram(self->shader_program_id);
+    gl_check_error ("_glLinkProgram");
+
+    GLint shader_program_res;
+    _glGetProgramiv(self->shader_program_id, GL_LINK_STATUS, &shader_program_res);
+    gl_check_error ("_glGetProgramiv");
+
+    if (!shader_program_res) {
+        char info_log[512];
+        _glGetProgramInfoLog(self->shader_program_id, 512, NULL, info_log);
+        printf("Shader Program failed to link: %s\n", info_log);
+        return 1;
+    } else {
+        printf("Successfully linked shader program!\n");
+    }
+
+    _glDeleteShader(shader_id);
+
+    return TRUE;
+};
+
+
+struct _ShellVRMirror
+{
+  GObject parent;
+
+  InputSynth *vr_input;
+
+  XrdShell *shell;
+  G3kContext *g3k;
+
+  MetaCursorTracker *cursor_tracker;
+  GLuint cursor_gl_shared_texture;
+
+  uint32_t top_layer;
+
+  gboolean nvidia;
+  struct gl_copy_helper gl_copy_helper;
+
+  GSList *grabbed_windows;
+
+  gboolean shutdown;
+
+  bool rendering;
+  bool framecycle;
+};
+
+struct _ShellVREffect {
+  ClutterEffect parent_instance;
+
+  ClutterActor *actor;
+  XrdWindow *xrd_win;
+};
+
+G_DEFINE_TYPE (ShellVREffect, shell_vr_effect, CLUTTER_TYPE_EFFECT);
+
+typedef struct
+{
+  MetaWindowActor *meta_window_actor;
+
+  bool keep_above_restore;
+  bool keep_below_restore;
+
+  /* We draw every frame of this window again to this shared GL texture */
+  GLuint gl_shared_texture;
+} ShellVRWindow;
+
+G_DEFINE_TYPE (ShellVRMirror, shell_vr_mirror, G_TYPE_OBJECT);
+
+
+
+static void
+shell_vr_effect_init (ShellVREffect *self)
+{
+  self->actor = NULL;
+  self->xrd_win = NULL;
+}
+
+static void
+shell_vr_effect_set_actor (ClutterActorMeta *meta,
+                           ClutterActor     *actor)
+{
+  ShellVREffect *self = SHELL_VR_EFFECT (meta);
+
+  // Maintain a pointer to the actor
+  self->actor = actor;
+
+  // If we've been detached by the actor then we should just bail out here
+  if (self->actor == NULL)
+    return;
+}
+
+static gboolean
+_upload_window (ShellVRMirror *self, XrdWindow *xrd_win);
+
+
+static void
+shell_vr_effect_paint (ClutterEffect           *effect,
+                       ClutterPaintNode        *node,
+                       ClutterPaintContext     *paint_context,
+                       ClutterEffectPaintFlags  flags)
+{
+  ShellVREffect *self = SHELL_VR_EFFECT (effect);
+  if (!self->actor || !CLUTTER_IS_ACTOR(self->actor))
+    return;
+
+  clutter_actor_continue_paint (self->actor, paint_context);
+
+  // XXXXXXX
+//  if (!(flags & CLUTTER_EFFECT_PAINT_ACTOR_DIRTY))
+//    return;
+
+  ShellVRMirror *mirror = shell_vr_mirror_get_instance ();
+  if (!mirror || !self->xrd_win)
+    return;
+  _upload_window (mirror, self->xrd_win);
+}
+
+
+static void
+shell_vr_effect_pick (ClutterEffect      *effect,
+                      ClutterPickContext *pick_context)
+{
+  ShellVREffect *self = SHELL_VR_EFFECT (effect);
+  if (self->actor == NULL || !CLUTTER_IS_ACTOR(self->actor))
+    return;
+
+  clutter_actor_continue_pick (self->actor, pick_context);
+}
+
+static void
+shell_vr_effect_class_init (ShellVREffectClass *klass)
+{
+  ClutterActorMetaClass *meta_class = CLUTTER_ACTOR_META_CLASS (klass);
+
+  meta_class->set_actor = shell_vr_effect_set_actor;
+
+  ClutterEffectClass *effect_class = CLUTTER_EFFECT_CLASS (klass);
+  effect_class->paint = shell_vr_effect_paint;
+  effect_class->pick = shell_vr_effect_pick;
+}
+
+static GLint
+_to_sized_format(GLint format)
+{
+  if (format == GL_RGBA8)
+    return GL_RGBA8;
+
+  if (format == GL_RGB)
+    return GL_RGBA8; // Three component variant not writeable by compute shader.
+  if (format == GL_RGBA)
+    return GL_RGBA8;
+  if (format == GL_BGR)
+    return GL_RGBA8;
+
+  g_printerr("Format 0x%x not implemented, using GL_RGBA8\n", format);
+  return GL_RGBA8;
+}
+
+/**
+ * @brief Creates a new GulkanTexture, OpenGL texture pair that shares video memory.
+ *
+ * The new OpenGL texture out_gl_shared_texture will match the properties of gl_texture.
+ *
+ * @param self
+ * @param gulkan
+ * @param gl_texture
+ * @param gl_target_type
+ * @param extent
+ * @param out_gl_shared_texture
+ * @return
+ */
+static GulkanTexture*
+_allocate_external_memory (ShellVRMirror *self,
+                           GulkanContext *gulkan,
+                           GLuint         gl_texture,
+                           GLenum         gl_target_type,
+                           VkExtent2D     extent,
+                           GLuint        *out_gl_shared_texture)
+{
+  g_print ("Reallocating %dx%d vulkan texture\n", extent.width, extent.height);
+
+  _glBindTexture (gl_target_type, gl_texture);
+  char buf[2048];
+  snprintf(buf, 2048, "glBindTexture target type %x, texture %d", gl_target_type, gl_texture);
+  gl_check_error (buf);
+
+  GLint internal_format;
+  _glGetTexLevelParameteriv (GL_TEXTURE_2D, 0,
+                             GL_TEXTURE_INTERNAL_FORMAT, &internal_format);
+  gl_check_error ("_glGetTexLevelParameteriv");
+
+#if SHELL_VR_COPY_COMPUTE
+  internal_format = _to_sized_format (internal_format);
+#endif
+
+  gsize size;
+  int fd;
+
+  VkImageLayout layout = g3k_context_get_upload_layout (self->g3k);
+  GulkanTexture *texture =
+    gulkan_texture_new_export_fd (gulkan, extent, VK_FORMAT_R8G8B8A8_SRGB,
+                                  layout, &size, &fd);
+  if (texture == NULL)
+    {
+      g_printerr ("Error: Unable to initialize Vulkan texture.\n");
+      return NULL;
+    }
+  gl_check_error ("gulkan_texture_new_export_fd");
+
+
+  GLint gl_dedicated_mem = GL_TRUE;
+  GLuint gl_mem_object;
+  _glCreateMemoryObjectsEXT (1, &gl_mem_object);
+  gl_check_error ("_glCreateMemoryObjectsEXT");
+
+  _glMemoryObjectParameterivEXT (gl_mem_object,
+                                 GL_DEDICATED_MEMORY_OBJECT_EXT,
+                                &gl_dedicated_mem);
+  gl_check_error ("_glMemoryObjectParameterivEXT");
+
+  _glGetMemoryObjectParameterivEXT (gl_mem_object,
+                                    GL_DEDICATED_MEMORY_OBJECT_EXT,
+                                   &gl_dedicated_mem);
+  gl_check_error ("_glGetMemoryObjectParameterivEXT");
+
+  _glImportMemoryFdEXT (gl_mem_object, size,
+                        GL_HANDLE_TYPE_OPAQUE_FD_EXT, fd);
+  gl_check_error ("_glImportMemoryFdEXT");
+
+  _glGenTextures (1, out_gl_shared_texture);
+  gl_check_error ("_glGenTextures");
+
+  _glBindTexture (GL_TEXTURE_2D, *out_gl_shared_texture);
+  gl_check_error ("_glBindTexture");
+
+  _glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_TILING_EXT, GL_OPTIMAL_TILING_EXT);
+  gl_check_error ("glTexParameteri GL_TEXTURE_TILING_EXT");
+  _glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+  gl_check_error ("glTexParameteri GL_TEXTURE_MIN_FILTER");
+  _glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+  gl_check_error ("glTexParameteri GL_TEXTURE_MAG_FILTER");
+
+  if (self->nvidia)
+    internal_format = GL_RGBA8;
+
+  _glTexStorageMem2DEXT (GL_TEXTURE_2D, 1, internal_format,
+                         extent.width, extent.height, gl_mem_object, 0);
+  gl_check_error ("_glTexStorageMem2DEXT");
+
+  _glFinish ();
+
+  _glDeleteMemoryObjectsEXT (1, &gl_mem_object);
+  gl_check_error ("_glDeleteMemoryObjectsEXT");
+
+  return texture;
+}
+
+#if SHELL_VR_COPY_COMPUTE
+/**
+ * @brief Creates a new GulkanTexture that is shared with a new out_gl_handle OpenGL texture.
+ *
+ * If out_gl_handle is != 0, it is destroyed.
+ *
+ * @param self
+ * @param extent
+ * @param gl_texture
+ * @param gl_texture_type
+ * @param out_gl_handle
+ * @return The new GulkanTexture
+ */
+static GulkanTexture *
+_recreate_texture (ShellVRMirror *self,
+              VkExtent2D extent,
+              GLuint gl_texture,
+              GLenum gl_texture_type,
+              GLuint *out_gl_handle)
+{
+  if (*out_gl_handle != 0)
+    {
+      _glDeleteTextures (1, out_gl_handle);
+      gl_check_error ("_glDeleteTextures");
+    }
+
+  GulkanContext *gulkan = g3k_context_get_gulkan (self->g3k);
+  GulkanTexture *texture = _allocate_external_memory (self, gulkan, gl_texture,
+                                                      gl_texture_type, extent,
+                                                      out_gl_handle);
+  gl_check_error ("_allocate_external_memory");
+
+  _glFinish ();
+  gl_check_error ("_glFinish");
+
+  return texture;
+}
+
+/**
+ * @brief copies gl_texture to target_texture with a compute shader.
+ *
+ * @param gch
+ * @param source_texture
+ * @param target_texture
+ * @param extent
+ */
+static void
+_copy_texture (struct gl_copy_helper *gch,
+            GLuint source_texture,
+            GLuint target_texture,
+            VkExtent2D extent)
+{
+  GLint restore_program;
+  glGetIntegerv (GL_CURRENT_PROGRAM, &restore_program);
+  gl_check_error ("glGetIntegerv GL_CURRENT_PROGRAM");
+
+  GLint restore_active_texture;
+  glGetIntegerv (GL_ACTIVE_TEXTURE, &restore_active_texture);
+  gl_check_error ("glGetIntegerv GL_ACTIVE_TEXTURE");
+
+  glActiveTexture (GL_TEXTURE0);
+  gl_check_error ("glActiveTexture");
+
+  // TODO: restore code assumes only 2d textures are bound
+
+  GLint restore_unit0;
+  glGetIntegerv (GL_TEXTURE_BINDING_2D, &restore_unit0);
+  gl_check_error ("glGetIntegerv GL_TEXTURE_BINDING_2D 0");
+
+
+  // Set the state we are interested in.
+
+  _glUseProgram (gch->shader_program_id);
+  gl_check_error ("_glUseProgram");
+
+  _glBindTextureUnit (0, source_texture);
+  gl_check_error ("_glBindTextureUnit");
+
+  _glBindImageTexture (0, target_texture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
+  gl_check_error ("_glBindImageTexture");
+
+  GLuint widthDiv = (extent.width + 15) / 16;
+  GLuint heightDiv = (extent.height + 15) / 16;
+
+  _glDispatchCompute (widthDiv, heightDiv, 1);
+  gl_check_error ("glDispatchCompute");
+
+  _glMemoryBarrier (GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
+  gl_check_error ("glMemoryBarrier");
+
+
+  // Restore or unbind state.
+  _glBindImageTexture (0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8);
+  gl_check_error ("_glBindImageTexture (restore)");
+
+  _glBindTextureUnit (0, restore_unit0);
+  gl_check_error ("_glBindTextureUnit (restore)");
+
+  _glUseProgram (restore_program);
+  gl_check_error ("glUseProgram (restore)");
+
+  glActiveTexture (restore_active_texture);
+  gl_check_error ("glActiveTexture (restore)");
+
+  // TODO: import semaphore from vulkan and signal
+  glFinish();
+  gl_check_error ("glFinish");
+}
+#else
+static GulkanTexture*
+_recreate_subdata (ShellVRMirror *self,
+                   VkExtent2D extent,
+                   GLuint meta_tex,
+                   GLenum meta_target,
+                   GLuint *out_gl_handle)
+{
+  if (*out_gl_handle != 0)
+    _glDeleteTextures (1, out_gl_handle);
+
+  GulkanContext *gulkan = g3k_context_get_gulkan (self->g3k);
+  GulkanTexture *texture =
+    _allocate_external_memory (self, gulkan, meta_tex, meta_target,
+                               extent, out_gl_handle);
+
+  return texture;
+}
+
+static void
+_update_subdata (VkExtent2D extent,
+                 GLuint meta_tex,
+                 GLenum meta_target,
+                 GLuint dst_name)
+{
+  _glCopyImageSubData (meta_tex, meta_target, 0, 0, 0, 0,
+                       dst_name, GL_TEXTURE_2D, 0, 0, 0, 0,
+                       extent.width, extent.height, 1);
+  gl_check_error ("_glCopyImageSubData");
+  _glFinish ();
+}
+#endif
+
+static void
+_cursor_changed_cb (MetaCursorTracker *cursor_tracker,
+                    gpointer          _self)
+{
+  ShellVRMirror *self = _self;
+  if (!self)
+    return;
+
+  CoglTexture *cogl_texture = meta_cursor_tracker_get_sprite (cursor_tracker);
+  int hotspot_x, hotspot_y;
+  meta_cursor_tracker_get_hot (cursor_tracker, &hotspot_x, &hotspot_y);
+
+
+  if (cogl_texture == NULL || !cogl_is_texture (cogl_texture))
+    {
+      g_printerr ("Cursor Error: Could not CoglTexture.\n");
+      return;
+    }
+
+  GLuint meta_tex;
+  GLenum meta_target_type;
+  if (!cogl_texture_get_gl_texture (cogl_texture, &meta_tex, &meta_target_type))
+    {
+      g_printerr ("Cursor Error: Could not get GL handle.\n");
+      return;
+    }
+
+  VkExtent2D cursor_extent = {
+    .width = (uint32_t) cogl_texture_get_width (cogl_texture),
+    .height = (uint32_t) cogl_texture_get_width (cogl_texture)
+  };
+
+  G3kCursor *cursor = xrd_shell_get_desktop_cursor (self->shell);
+  g3k_cursor_set_hotspot (cursor, hotspot_x, hotspot_y);
+
+  gboolean extent_changed = TRUE;
+  GulkanTexture *texture = g3k_plane_get_texture (G3K_PLANE (cursor));
+  if (texture)
+    {
+      VkExtent2D extent = gulkan_texture_get_extent (texture);
+      extent_changed = (cursor_extent.width != extent.width ||
+                        cursor_extent.height != extent.height);
+    }
+
+  if (extent_changed)
+    {
+      GulkanTexture *new_texture;
+#if SHELL_VR_COPY_COMPUTE
+      new_texture = _recreate_texture(self, cursor_extent, meta_tex, meta_target_type,
+                                 &self->cursor_gl_shared_texture);
+#else
+      new_texture = _recreate_subdata (self, cursor_extent, meta_tex,
+                                       meta_target_type, &self->cursor_gl_shared_texture);
+#endif
+      g3k_plane_set_texture (G3K_PLANE (cursor), new_texture);
+    }
+
+#if SHELL_VR_COPY_COMPUTE
+  _copy_texture (&self->gl_copy_helper, meta_tex, self->cursor_gl_shared_texture, cursor_extent);
+#else
+  _update_subdata (cursor_extent, meta_tex, meta_target_type,
+                   self->cursor_gl_shared_texture);
+#endif
+}
+
+static void
+_grab_op_begin_cb (MetaDisplay *display1,
+                   MetaDisplay *display2,
+                   MetaWindow  *grab_window,
+                   MetaGrabOp   op,
+                   gpointer     _self)
+{
+  // yes, this does happen
+  if (!grab_window)
+    return;
+
+  if (!is_grab_op_valid(op))
+    {
+      g_warning("_grab_op_begin_cb: Invalid grab op %d received.\n", op);
+      return;
+    }
+
+  ShellVRMirror *self = _self;
+  if (!g_slist_find (self->grabbed_windows, grab_window))
+    self->grabbed_windows = g_slist_append (self->grabbed_windows, grab_window);
+
+  g_debug ("Start grab window %s\n", meta_window_get_title (grab_window));
+}
+
+static void
+_grab_op_end_cb (MetaDisplay *display1,
+                 MetaDisplay *display2,
+                 MetaWindow  *grab_window,
+                 MetaGrabOp   op,
+                 gpointer     _self)
+{
+  if (!grab_window)
+    return;
+
+  if (!is_grab_op_valid(op))
+    {
+      g_warning("_grab_op_end_cb: Invalid grab op %d received.\n", op);
+      return;
+    }
+
+  ShellVRMirror *self = _self;
+  self->grabbed_windows = g_slist_remove (self->grabbed_windows, grab_window);
+
+  g_debug ("End Grab window %s\n", meta_window_get_title (grab_window));
+}
+
+static void
+shell_vr_mirror_init (ShellVRMirror *self)
+{
+  self->top_layer = 0;
+  self->vr_input = NULL;
+  self->shell = NULL;
+  self->shutdown = FALSE;
+}
+
+ShellVRMirror *
+shell_vr_mirror_new (void)
+{
+  return g_object_new (SHELL_TYPE_VR_MIRROR, NULL);
+}
+
+ShellVRMirror *
+shell_vr_mirror_get_instance (void)
+{
+  return shell_vr_mirror_instance;
+}
+
+ShellVRMirror *
+shell_vr_mirror_create_instance (void)
+{
+  shell_vr_mirror_instance = shell_vr_mirror_new ();
+  shell_vr_mirror_initialize (shell_vr_mirror_instance);
+  return shell_vr_mirror_instance;
+}
+
+/* Coordinate space: 0 == x: left, y == 0: top */
+static graphene_point_t
+_window_to_desktop_coords (MetaWindow       *meta_win,
+                           graphene_point_t *window_pixels)
+ {
+  MetaRectangle window_rect;
+  meta_window_get_buffer_rect (meta_win, &window_rect);
+
+  graphene_point_t desktop_coords = {
+    .x = window_rect.x + window_pixels->x,
+    .y = window_rect.y + window_pixels->y
+  };
+  return desktop_coords;
+}
+
+static MetaWindow *
+_get_validated_window (MetaWindowActor *actor)
+{
+  if (actor == NULL || !META_IS_WINDOW_ACTOR (actor))
+    {
+      g_printerr ("Error: Actor for move cursor not available.\n");
+      return NULL;
+    }
+
+  MetaWindow *meta_win = meta_window_actor_get_meta_window (actor);
+  if (meta_win == NULL || !META_IS_WINDOW (meta_win))
+    {
+      g_printerr ("Error: No window to move\n");
+      return NULL;
+    }
+
+  if (meta_window_get_display (meta_win) == NULL)
+    {
+      g_printerr ("Error: window has no display?!\n");
+      return NULL;
+    }
+
+  return meta_win;
+}
+
+static void
+_ensure_focused (MetaWindow *meta_win)
+{
+  /* mutter asserts that we don't mess with override_redirect windows */
+  if (meta_window_is_override_redirect (meta_win))
+    return;
+
+  meta_window_raise (meta_win);
+  if (!meta_window_has_focus (meta_win))
+    meta_window_focus (meta_win, CurrentTime);
+}
+
+static void
+_ensure_on_workspace (MetaWindow *meta_win)
+{
+  if (meta_window_is_on_all_workspaces (meta_win))
+    return;
+
+  MetaDisplay *display = meta_window_get_display (meta_win);
+
+  MetaWorkspaceManager *manager = meta_display_get_workspace_manager (display);
+  MetaWorkspace *ws_current =
+    meta_workspace_manager_get_active_workspace (manager);
+
+  MetaWorkspace *ws_window = meta_window_get_workspace (meta_win);
+  if (!ws_window || !META_IS_WORKSPACE (ws_window))
+    return;
+
+  if (ws_current == ws_window)
+    return;
+
+  guint32 timestamp = meta_display_get_current_time_roundtrip (display);
+  meta_workspace_activate_with_focus (ws_window, meta_win, timestamp);
+}
+
+static void
+_click_cb (XrdShell      *shell,
+           XrdClickEvent *event,
+           ShellVRMirror *self)
+{
+  ShellVRWindow *shell_win;
+  g_object_get (event->object, "native", &shell_win, NULL);
+  if (!shell_win)
+    return;
+
+  MetaWindowActor *actor = (MetaWindowActor*) shell_win->meta_window_actor;
+  MetaWindow *meta_win = _get_validated_window (actor);
+  if (!meta_win)
+    return;
+
+  _ensure_on_workspace (meta_win);
+  _ensure_focused (meta_win);
+
+  graphene_point_t desktop_coords =
+    _window_to_desktop_coords (meta_win, event->position);
+
+  input_synth_click (self->vr_input,
+                     desktop_coords.x, desktop_coords.y,
+                     event->button, event->state);
+}
+
+static void
+_move_cursor_cb (XrdShell           *shell,
+                 XrdMoveCursorEvent *event,
+                 ShellVRMirror      *self)
+{
+  ShellVRWindow *shell_win;
+  g_object_get (event->window, "native", &shell_win, NULL);
+  if (!shell_win)
+    return;
+
+  MetaWindowActor *actor = (MetaWindowActor*) shell_win->meta_window_actor;
+  MetaWindow *meta_win = _get_validated_window (actor);
+  if (!meta_win)
+    return;
+
+  /* do not move mouse cursor while the window is grabbed (in "move" mode) */
+  if (g_slist_find (self->grabbed_windows, meta_win))
+    return;
+
+  _ensure_on_workspace (meta_win);
+  _ensure_focused (meta_win);
+
+  graphene_point_t desktop_coords =
+    _window_to_desktop_coords (meta_win, event->position);
+  input_synth_move_cursor (self->vr_input, desktop_coords.x, desktop_coords.y);
+}
+
+static void
+_keyboard_press_cb (XrdShell      *shell,
+                    G3kKeyEvent   *event,
+                    ShellVRMirror *self)
+{
+  XrdWindow* keyboard_xrd_win = xrd_shell_get_keyboard_window (shell);
+  if (!keyboard_xrd_win)
+    {
+      g_print ("ERROR: No keyboard window!\n");
+      return;
+    }
+
+  ShellVRWindow *shell_win;
+  g_object_get (keyboard_xrd_win, "native", &shell_win, NULL);
+  if (!shell_win)
+    return;
+
+  MetaWindowActor *actor = (MetaWindowActor*) shell_win->meta_window_actor;
+  MetaWindow *meta_win = _get_validated_window (actor);
+  if (!meta_win)
+    return;
+
+  _ensure_on_workspace (meta_win);
+  _ensure_focused (meta_win);
+
+  if (event->keyval == 0)
+    {
+      input_synth_characters (self->vr_input, event->string);
+    }
+  else
+    {
+      // TODO: keyval input
+    }
+}
+
+static void
+_disconnect_signals (ShellVRMirror *self);
+static void
+_connect_signals (ShellVRMirror *self);
+
+static void
+_destroy_textures(ShellVRMirror *self)
+{
+  G3kObjectManager *manager = g3k_context_get_manager (self->g3k);
+  GSList *objects = g3k_object_manager_get_objects (manager);
+  for (GSList *l = objects; l != NULL; l = l->next) {
+    XrdWindow *xrd_win = XRD_WINDOW (l->data);
+
+    ShellVRWindow *shell_win;
+    g_object_get (xrd_win, "native", &shell_win, NULL);
+
+    // external memory texture, has to be recreated
+    _glDeleteTextures (1, &shell_win->gl_shared_texture);
+    shell_win->gl_shared_texture = 0;
+  }
+
+  _glDeleteTextures (1, &self->cursor_gl_shared_texture);
+  self->cursor_gl_shared_texture = 0;
+}
+
+static void
+_detach_paint_cbs (XrdShell *shell)
+{
+  G3kContext *g3k = xrd_shell_get_g3k (shell);
+  G3kObjectManager *manager = g3k_context_get_manager (g3k);
+  GSList *objects = g3k_object_manager_get_objects (manager);
+  for (GSList *l = objects; l != NULL; l = l->next)
+    {
+      XrdWindow *xrd_win = XRD_WINDOW (l->data);
+
+      ShellVRWindow *shell_win;
+      g_object_get (xrd_win, "native", &shell_win, NULL);
+      clutter_actor_clear_effects (CLUTTER_ACTOR(shell_win->meta_window_actor));
+    }
+}
+
+static void
+_state_change_cb (GxrContext          *gxr,
+                  GxrStateChangeEvent *event,
+                  gpointer             _self)
+{
+  g_print ("Handling VR state change event\n");
+
+  ShellVRMirror *self = SHELL_VR_MIRROR (_self);
+
+  switch (event->state_change)
+  {
+    case GXR_STATE_SHUTDOWN:
+    {
+      g_print("State change event: VR Shutdown\n");
+      _detach_paint_cbs (self->shell);
+      _destroy_textures(self);
+      shell_vr_mirror_destroy_instance ();
+    } break;
+    case GXR_STATE_FRAMECYCLE_START:
+      self->framecycle = TRUE;
+      g_debug ("State change event: frame cycle started");
+      break;
+    case GXR_STATE_FRAMECYCLE_STOP:
+      self->framecycle = FALSE;
+      g_debug ("State change event: frame cycle stopped");
+      break;
+    case GXR_STATE_RENDERING_START:
+      self->rendering = TRUE;
+      g_debug ("State change event: rendering started");
+      break;
+    case GXR_STATE_RENDERING_STOP:
+      self->rendering = TRUE;
+      g_debug ("State change event: rendering stopped");
+      break;
+  }
+}
+
+static void
+_init_input (ShellVRMirror *self)
+{
+  if (meta_is_wayland_compositor ())
+    self->vr_input = input_synth_new (INPUTSYNTH_BACKEND_WAYLAND_CLUTTER);
+  else
+    self->vr_input = input_synth_new (INPUTSYNTH_BACKEND_XDO);
+}
+
+static gboolean
+_upload_gl_external_memory (ShellVRMirror     *self,
+                            GulkanContext      *gulkan,
+                            XrdWindow         *xrd_win,
+                            MetaShapedTexture *mst,
+                            MetaRectangle     *rect)
+{
+  CoglTexture *cogl_texture = meta_shaped_texture_get_texture (mst);
+
+  if (cogl_texture == NULL || !cogl_is_texture (cogl_texture))
+    {
+      g_printerr ("Error: Could not CoglTexture from MetaShapedTexture.\n");
+      return FALSE;
+    }
+
+  GLuint meta_tex;
+  GLenum meta_target;
+  if (!cogl_texture_get_gl_texture (cogl_texture, &meta_tex, &meta_target))
+    {
+      g_printerr ("Error: Could not get GL handle from CoglTexture.\n");
+      return FALSE;
+    }
+
+  ShellVRWindow *shell_win;
+  g_object_get (xrd_win, "native", &shell_win, NULL);
+  if (!shell_win)
+    return FALSE;
+
+  VkExtent2D window_extent = {
+    .width = (uint32_t) rect->width,
+    .height = (uint32_t) rect->height
+  };
+
+  GulkanTexture *texture = g3k_plane_get_texture (G3K_PLANE (xrd_win));
+  gboolean extent_changed = TRUE;
+  if (texture)
+    {
+      VkExtent2D extent = gulkan_texture_get_extent (texture);
+      extent_changed = (window_extent.width != extent.width ||
+                        window_extent.height != extent.height);
+    }
+
+  if (extent_changed)
+    {
+      GulkanTexture *new_texture;
+#if SHELL_VR_COPY_COMPUTE
+      new_texture = _recreate_texture(self, window_extent, meta_tex, meta_target,
+                                &shell_win->gl_shared_texture);
+#else
+      new_texture = _recreate_subdata (self, window_extent, meta_tex,
+                                       meta_target, &shell_win->gl_shared_texture);
+#endif
+      g3k_plane_set_texture (G3K_PLANE (xrd_win), new_texture);
+    }
+
+#if SHELL_VR_COPY_COMPUTE
+  _copy_texture (&self->gl_copy_helper, meta_tex, shell_win->gl_shared_texture, window_extent);
+#else
+  _update_subdata (window_extent, meta_tex, meta_target, shell_win->gl_shared_texture);
+#endif
+
+  return TRUE;
+}
+
+static gboolean
+_upload_raw_cairo (ShellVRMirror     *self,
+                   GulkanContext      *gulkan,
+                   XrdWindow         *xrd_win,
+                   MetaShapedTexture *mst,
+                   MetaRectangle     *rect)
+{
+  cairo_rectangle_int_t cairo_rect = {
+    .x = 0,
+    .y = 0,
+    .width = rect->width,
+    .height = rect->height
+  };
+
+  cairo_surface_t *sf = meta_shaped_texture_get_image (mst, &cairo_rect);
+  if (sf == NULL)
+    {
+      g_printerr ("Error: Could not get Cairo surface"
+                  " from MetaShapedTexture.\n");
+      return FALSE;
+    }
+
+  ShellVRWindow *shell_win;
+  g_object_get (xrd_win, "native", &shell_win, NULL);
+  if (!shell_win)
+    return FALSE;
+
+  VkImageLayout upload_layout = g3k_context_get_upload_layout (self->g3k);
+
+  int texture_width;
+  int texture_height;
+  g_object_get (xrd_win, "texture-width", &texture_width, NULL);
+  g_object_get (xrd_win, "texture_height", &texture_height, NULL);
+
+  GulkanTexture *texture = g3k_plane_get_texture (G3K_PLANE (xrd_win));
+
+  if (rect->width != texture_width ||
+      rect->height != texture_height ||
+      texture == NULL)
+    {
+      g_print ("Reallocating %dx%d vulkan texture\n",
+               rect->width, rect->height);
+      texture =
+        gulkan_texture_new_from_cairo_surface (gulkan, sf,
+                                               VK_FORMAT_B8G8R8A8_SRGB,
+                                               upload_layout);
+
+     if (!GULKAN_IS_TEXTURE (texture))
+        {
+          g_printerr ("Error creating texture for window!\n");
+          return FALSE;
+        }
+      g3k_plane_set_texture (G3K_PLANE (xrd_win), texture);
+    }
+  else
+    {
+      gulkan_texture_upload_cairo_surface (texture, sf, upload_layout);
+    }
+
+  cairo_surface_destroy (sf);
+
+  return TRUE;
+}
+
+static gboolean
+_upload_window (ShellVRMirror *self, XrdWindow *xrd_win)
+{
+  ShellVRWindow *shell_win;
+  g_object_get (xrd_win, "native", &shell_win, NULL);
+  if (!shell_win)
+    return FALSE;
+
+  MetaWindowActor* actor = shell_win->meta_window_actor;
+
+  MetaWindow *meta_win = _get_validated_window (actor);
+  MetaRectangle rect;
+  meta_window_get_buffer_rect (meta_win, &rect);
+
+  /* skip upload of small buffers */
+  if (rect.width <= 10 && rect.height <= 10)
+    return FALSE;
+
+  MetaShapedTexture* mst =
+    (MetaShapedTexture*) meta_window_actor_get_texture (actor);
+
+  GulkanContext *gulkan = g3k_context_get_gulkan (self->g3k);
+
+//  CoglTextureComponents components;
+//  if (self->nvidia)
+//    {
+//      CoglTexture *cogl_texture = meta_shaped_texture_get_texture (mst);
+//      if (cogl_texture == NULL || !cogl_is_texture (cogl_texture))
+//        {
+//          g_printerr ("Error: Could not CoglTexture from MetaShapedTexture.\n");
+//          return FALSE;
+//        }
+//      components = cogl_texture_get_components (cogl_texture);
+//    }
+
+  static GMutex upload_mutex;
+  g_mutex_lock (&upload_mutex);
+
+  gboolean ret;
+  /* Use cairo upload as fallback on NVIDIA for RGB buffers */
+//  if (self->nvidia && components == COGL_TEXTURE_COMPONENTS_RGB && false)
+//    ret = _upload_raw_cairo (self, gulkan, xrd_win, mst, &rect);
+//  else
+    ret = _upload_gl_external_memory (self, gulkan, xrd_win, mst, &rect);
+  g_mutex_unlock (&upload_mutex);
+
+  return ret;
+}
+
+static gboolean
+_is_excluded_from_mirroring (MetaWindow *meta_win)
+{
+  MetaWindowType type = meta_window_get_window_type (meta_win);
+  return
+    type == META_WINDOW_DESKTOP ||
+    type == META_WINDOW_DOCK ||
+    type == META_WINDOW_DND;
+}
+
+static void
+_mirror_current_windows (ShellVRMirror *self)
+{
+  ShellGlobal *global = shell_global_get ();
+  GList *window = shell_global_get_window_actors (global);
+  while (window != NULL)
+    {
+      MetaWindowActor *actor = window->data;
+      shell_vr_mirror_map_actor (self, actor);
+      window = window->next;
+    }
+}
+
+static void
+_apply_desktop_position (MetaWindow *meta_win,
+                         XrdWindow  *xrd_win,
+                         uint32_t    layer)
+{
+  ShellGlobal *global = shell_global_get ();
+  MetaDisplay *display = shell_global_get_display (global);
+
+  int screen_w, screen_h;
+  meta_display_get_size (display, &screen_w, &screen_h);
+
+  MetaRectangle rect;
+  meta_window_get_buffer_rect (meta_win, &rect);
+
+  float x =            rect.x - screen_w / 2.0f + rect.width  / 2.0f;
+  float y = screen_h - rect.y - screen_h / 4.0f - rect.height / 2.0f;
+
+  graphene_point3d_t point = {
+    .x = x / SHELL_VR_PIXELS_PER_METER,
+    .y = y / SHELL_VR_PIXELS_PER_METER + DEFAULT_LEVEL,
+    .z = -SHELL_VR_DESKTOP_PLANE_DISTANCE + SHELL_VR_LAYER_DISTANCE * layer
+  };
+
+  graphene_matrix_t transform;
+  graphene_matrix_init_translate (&transform, &point);
+
+  g3k_object_set_matrix (G3K_OBJECT (xrd_win), &transform);
+
+  G3kContext *g3k = g3k_object_get_context (G3K_OBJECT (xrd_win));
+  G3kObjectManager *manager = g3k_context_get_manager (g3k);
+  g3k_object_manager_save_reset_transformation (manager, G3K_OBJECT (xrd_win));
+}
+
+static void
+_arrange_windows_by_desktop_position (ShellVRMirror *self)
+{
+  G3kObjectManager *manager = g3k_context_get_manager (self->g3k);
+  GSList *objects = g3k_object_manager_get_objects (manager);
+
+  GSList *meta_win_list = NULL;
+
+  for (; objects; objects = objects->next)
+    {
+      XrdWindow *xrd_win = XRD_WINDOW (objects->data);
+      ShellVRWindow *shell_win;
+      g_object_get (xrd_win, "native", &shell_win, NULL);
+      if (!shell_win)
+        continue;
+
+      MetaWindow *meta_win =
+        meta_window_actor_get_meta_window (shell_win->meta_window_actor);
+
+      if (!_is_excluded_from_mirroring (meta_win))
+        meta_win_list = g_slist_append (meta_win_list, meta_win);
+    }
+
+  ShellGlobal *global = shell_global_get ();
+  MetaDisplay *display = shell_global_get_display (global);
+
+  GSList *sorted_windows =
+    meta_display_sort_windows_by_stacking (display, meta_win_list);
+
+  for (self->top_layer = 0; sorted_windows;
+       sorted_windows = sorted_windows->next)
+    {
+      XrdWindow *xrd_window = xrd_shell_lookup_window (self->shell,
+                                                       META_WINDOW (sorted_windows->data));
+      if (!xrd_window)
+        {
+          g_printerr ("_arrange_windows_by_desktop_position: Error looking up "
+                      "MetaWindow %p\n",
+                      (void*) sorted_windows->data);
+          continue;
+        }
+
+      _apply_desktop_position (sorted_windows->data,
+                               xrd_window, self->top_layer);
+      self->top_layer++;
+    }
+  g_slist_free (sorted_windows);
+}
+
+static void
+_connect_signals (ShellVRMirror *self)
+{
+  g_signal_connect (self->shell, "keyboard-press-event",
+                    (GCallback) _keyboard_press_cb, self);
+
+  g_signal_connect (self->shell, "click-event",
+                    (GCallback) _click_cb, self);
+
+  g_signal_connect (self->shell, "move-cursor-event",
+                    (GCallback) _move_cursor_cb, self);
+
+  GxrContext *gxr = g3k_context_get_gxr (self->g3k);
+  g_signal_connect (gxr, "state-change-event",
+                    (GCallback) _state_change_cb, self);
+
+  ShellGlobal *global = shell_global_get ();
+  MetaDisplay *display = shell_global_get_display (global);
+
+  g_signal_connect (display, "grab-op-begin",
+                    (GCallback) _grab_op_begin_cb, self);
+
+  g_signal_connect (display, "grab-op-end",
+                    (GCallback) _grab_op_end_cb, self);
+
+
+  self->cursor_tracker =
+    meta_cursor_tracker_get_for_display (display);
+  g_signal_connect (self->cursor_tracker, "cursor-changed",
+                    (GCallback) _cursor_changed_cb, self);
+}
+
+static void
+_disconnect_signals (ShellVRMirror *self)
+{
+  g_signal_handlers_disconnect_matched (
+    self->shell, G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
+    _keyboard_press_cb, NULL);
+  g_signal_handlers_disconnect_matched (
+    self->shell, G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
+    _click_cb, NULL);
+  g_signal_handlers_disconnect_matched (
+    self->shell, G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
+    _move_cursor_cb, NULL);
+  g_signal_handlers_disconnect_matched (
+    self->shell, G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
+    _state_change_cb, NULL);
+
+  ShellGlobal *global = shell_global_get ();
+  MetaDisplay *display = shell_global_get_display (global);
+
+  g_signal_handlers_disconnect_matched (
+    display, G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
+    _grab_op_begin_cb, NULL);
+
+  g_signal_handlers_disconnect_matched (
+    display, G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
+    _grab_op_end_cb, NULL);
+
+  g_signal_handlers_disconnect_matched (
+    self->cursor_tracker, G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
+    _cursor_changed_cb, NULL);
+}
+
+
+void
+shell_vr_mirror_initialize (ShellVRMirror *self)
+{
+  g_print ("shell: Loading XR with XR_RUNTIME_JSON: %s\n",
+           g_getenv ("XR_RUNTIME_JSON"));
+  self->shell = xrd_shell_new ();
+  if (!self->shell)
+    {
+      g_print ("Failed to initialize xrdesktop!\n");
+      g_print ("Usually this is caused by a problem with the VR runtime.\n");
+
+      g_clear_object (&shell_vr_mirror_instance);
+
+      return;
+    }
+
+  self->g3k = xrd_shell_get_g3k (self->shell);
+
+  g_print ("== Started xrdesktop ==\n");
+
+  self->cursor_gl_shared_texture = 0;
+
+  if (!_load_gl_functions ())
+    g_printerr ("Error: Could not load GL function pointers"
+                " for external memory upload method.\n");
+
+  if (!gl_copy_helper_init (&self->gl_copy_helper))
+    {
+      g_printerr ("Failed to init framebuffer renderer\n");
+      return;
+    }
+
+//  self->nvidia = g_strcmp0 (gl_get_vendor (), "NVIDIA Corporation") == 0;
+
+  self->grabbed_windows = NULL;
+
+  _init_input (self);
+
+  _mirror_current_windows (self);
+  _arrange_windows_by_desktop_position (self);
+
+
+
+  _connect_signals (self);
+
+  g3k_context_start_renderer (self->g3k);
+}
+
+/*
+ * Menus, ContextMenus, etc. that should be fixed to a parent window and may
+ * not be individually moved
+ */
+
+static void
+_get_offset (MetaWindow *parent, MetaWindow *child, graphene_point_t *offset)
+{
+  MetaRectangle parent_rect;
+  meta_window_get_buffer_rect (parent, &parent_rect);
+
+  MetaRectangle child_rect;
+  meta_window_get_buffer_rect (child, &child_rect);
+
+  int parent_center_x = parent_rect.x + parent_rect.width / 2;
+  int parent_center_y = parent_rect.y + parent_rect.height / 2;
+
+  int child_center_x = child_rect.x + child_rect.width / 2;
+  int child_center_y = child_rect.y + child_rect.height / 2;
+
+  int offset_x = child_center_x - parent_center_x;
+  int offset_y = child_center_y - parent_center_y;
+
+  offset->x = offset_x;
+  offset->y = - offset_y;
+
+  g_print ("child at %d,%d to parent at %d,%d, offset %d,%d\n",
+           child_center_x, child_center_y,
+           parent_center_x, parent_center_y,
+           offset_x, offset_y);
+}
+
+static gboolean
+_is_child_window (MetaWindow *meta_win)
+{
+  MetaWindowType t = meta_window_get_window_type (meta_win);
+  return
+    t == META_WINDOW_POPUP_MENU ||
+    t == META_WINDOW_DROPDOWN_MENU ||
+    t == META_WINDOW_TOOLTIP ||
+    t == META_WINDOW_MODAL_DIALOG ||
+    t == META_WINDOW_COMBO;
+}
+
+static XrdWindow*
+_get_valid_xrd_parent (ShellVRMirror *self,
+                       MetaWindow    *meta_parent)
+{
+  XrdWindow *xrd_parent = NULL;
+  if (meta_parent != NULL && !_is_excluded_from_mirroring (meta_parent))
+    {
+      xrd_parent = xrd_shell_lookup_window (self->shell, meta_parent);
+      if (!xrd_parent)
+        {
+          g_printerr ("_get_valid_xrd_parent: Could not find XrdWindow for "
+                      "parent MetaWindow %p\n", (void*) meta_parent);
+        }
+    }
+  return xrd_parent;
+}
+
+static bool
+_find_valid_parent (ShellVRMirror *self,
+                    MetaWindow    *child,
+                    MetaWindow   **meta_parent,
+                    XrdWindow    **xrd_parent)
+{
+  /* Try transient first */
+  *meta_parent = meta_window_get_transient_for (child);
+  *xrd_parent = _get_valid_xrd_parent (self, *meta_parent);
+  if (*xrd_parent != NULL)
+    return TRUE;
+
+  /* If this doesn't work out try the root ancestor */
+  *meta_parent = meta_window_find_root_ancestor (child);
+  *xrd_parent = _get_valid_xrd_parent (self, *meta_parent);
+  if (*xrd_parent != NULL)
+    return TRUE;
+
+  /* Last try, check if anything is focused and make that our parent */
+  ShellGlobal *global = shell_global_get ();
+  MetaDisplay *display = shell_global_get_display (global);
+  *meta_parent = meta_display_get_focus_window (display);
+  *xrd_parent = _get_valid_xrd_parent (self, *meta_parent);
+  if (*xrd_parent != NULL)
+    return TRUE;
+
+  /* Didn't find anything */
+  const gchar *title = meta_window_get_title (child);
+  g_warning ("Could not find a parent for `%s`", title);
+
+  return FALSE;
+
+}
+
+gboolean
+shell_vr_mirror_map_actor (ShellVRMirror   *self,
+                           MetaWindowActor *actor)
+{
+  if (!shell_vr_mirror_instance)
+    return FALSE;
+
+  MetaWindow *meta_win = _get_validated_window (actor);
+  if (_is_excluded_from_mirroring (meta_win))
+    return FALSE;
+
+  MetaRectangle rect;
+  meta_window_get_buffer_rect (meta_win, &rect);
+
+  gboolean is_child = _is_child_window (meta_win);
+  XrdWindow *xrd_parent = NULL;
+  MetaWindow *meta_parent = NULL;
+
+  if (is_child)
+    {
+      if (_find_valid_parent (self, meta_win, &meta_parent, &xrd_parent))
+        {
+          // TODO: Port this to new XrdWindow API.
+          // xrdesktop commit 9354945fa1c94386a72b4ca159d40ebcb09437df
+        }
+    }
+
+  const gchar *title = meta_window_get_title (meta_win);
+  const gchar *description = meta_window_get_description (meta_win);
+  g_print ("Map window %p: %s (%s)\n", actor, title, description);
+
+  ShellVRWindow *shell_win = g_malloc (sizeof (ShellVRWindow));
+  shell_win->meta_window_actor = actor;
+  shell_win->gl_shared_texture = 0;
+
+  VkExtent2D size_pixels = {rect.width, rect.height};
+  graphene_size_t size_meters = g3k_extent_to_size (&size_pixels,
+                                                    SHELL_VR_PIXELS_PER_METER);
+
+  XrdWindow *xrd_win = xrd_window_new (self->g3k, title, shell_win, size_pixels,
+                                       &size_meters);
+
+  gboolean draggable = !(is_child && meta_parent != NULL && xrd_parent != NULL);
+
+  if (is_child && !draggable)
+    {
+      graphene_point_t offset;
+      _get_offset (meta_parent, meta_win, &offset);
+
+      graphene_point3d_t offset_point = {
+        .x = offset.x,
+        .y = offset.y,
+        .z = 0.1
+      };
+      G3kPose offset_pose = g3k_pose_new (&offset_point, NULL);
+
+      g3k_object_set_local_pose (G3K_OBJECT (xrd_win), &offset_pose);
+
+      xrd_window_add_child (xrd_parent, xrd_win);
+    }
+  else if (is_child && xrd_parent == NULL)
+    g_warning ("Can't add window '%s' as child. No parent candidate!\n", title);
+
+  if (!is_child)
+    {
+      _apply_desktop_position (meta_win, xrd_win, self->top_layer);
+      self->top_layer++;
+    }
+
+  xrd_shell_add_window (self->shell, xrd_win, G3K_OBJECT (xrd_parent),
+                        draggable, meta_win);
+
+  ShellVREffect *effect = g_object_new (SHELL_TYPE_VR_EFFECT, NULL);
+  effect->xrd_win = xrd_win;
+
+  clutter_actor_meta_set_name (CLUTTER_ACTOR_META (effect), title);
+
+  clutter_actor_add_effect (CLUTTER_ACTOR(actor),
+                            CLUTTER_EFFECT(effect));
+
+  return TRUE;
+}
+
+gboolean
+shell_vr_mirror_destroy_actor (ShellVRMirror   *self,
+                               MetaWindowActor *actor)
+{
+  if (!shell_vr_mirror_instance)
+    return FALSE;
+
+  MetaWindow *meta_win = _get_validated_window (actor);
+  g_print ("WINDOW CLOSED: %s: %s\n",
+           meta_window_get_wm_class (meta_win),
+           meta_window_get_title (meta_win));
+
+  XrdWindow *xrd_win = xrd_shell_lookup_window (self->shell, meta_win);
+  if (xrd_win)
+    {
+      ShellVRWindow *shell_win;
+      g_object_get (xrd_win, "native", &shell_win, NULL);
+      if (!shell_win)
+        return FALSE;
+
+      // TODO: Removing one effect does not work. This could cause problems
+      // if there are other effects than ours on the window actor.
+      // The meta of type 'ShellVREffect' with name 'Name' is not attached
+      // to the actor 'MetaWindowActorX11'
+      //clutter_actor_remove_effect (CLUTTER_ACTOR(actor),
+      //                             CLUTTER_EFFECT(shell_win->effect));
+
+      clutter_actor_clear_effects (CLUTTER_ACTOR(shell_win->meta_window_actor));
+
+      if (shell_win->gl_shared_texture != 0)
+        _glDeleteTextures (1, &shell_win->gl_shared_texture);
+
+      xrd_shell_remove_window (self->shell, xrd_win);
+
+      xrd_window_close (xrd_win);
+
+      g_object_unref (xrd_win);
+      g_free (shell_win);
+    }
+  else
+    {
+      g_printerr ("Could not destroy null xrd win.\n");
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+gboolean
+shell_vr_mirror_actor_size_changed (ShellVRMirror   *self,
+                                    MetaWindowActor *actor)
+{
+  if (!shell_vr_mirror_instance)
+    return FALSE;
+
+  MetaWindow *meta_win = _get_validated_window (actor);
+
+  MetaRectangle rect;
+  meta_window_get_buffer_rect (meta_win, &rect);
+  g_print ("Window Size Changed: %s: [%d,%d] %dx%d\n",
+           meta_window_get_title (meta_win),
+           rect.x, rect.y, rect.width, rect.height);
+
+  if (rect.width < 10 || rect.height < 10)
+    {
+      g_debug ("Skipping window '%s', since rect size is below 10x10.",
+               meta_window_get_title (meta_win));
+      return FALSE;
+    }
+
+  XrdWindow *xrd_win = xrd_shell_lookup_window (self->shell, meta_win);
+  if (xrd_win == NULL)
+    {
+      g_printerr ("actor_size_changed: Could not find XrdWindow for MetaWindow %p\n",
+                  (void*) meta_win);
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+static void
+shell_vr_mirror_finalize (GObject *object)
+{
+  ShellVRMirror *self = SHELL_VR_MIRROR (object);
+
+  g_print ("== Disabling xrdesktop ==\n");
+  if (self->vr_input)
+    g_object_unref (self->vr_input);
+  if (self->shell)
+    g_object_unref (self->shell);
+
+  g_slist_free (self->grabbed_windows);
+
+  G_OBJECT_CLASS (shell_vr_mirror_parent_class)->finalize (object);
+}
+
+static void
+shell_vr_mirror_class_init (ShellVRMirrorClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  gobject_class->finalize = shell_vr_mirror_finalize;
+}
+
+static void
+_destroy_vr_window (G3kObject     *g3k_object,
+                    ShellVRMirror *self)
+{
+  ShellVRWindow *shell_win;
+  g_object_get (g3k_object, "native", &shell_win, NULL);
+  if (shell_win)
+    shell_vr_mirror_destroy_actor (self, shell_win->meta_window_actor);
+}
+
+void
+shell_vr_mirror_destroy_instance (void)
+{
+  ShellVRMirror *self = shell_vr_mirror_instance;
+  if (!self)
+    {
+      g_printerr ("No ShellVRMirror instance to destroy!\n");
+      return;
+    }
+
+  _disconnect_signals (self);
+
+  self->shutdown = TRUE;
+
+  /*
+   * We have to clean up windows first because it will only clean up as long
+   * as there is an active shell_vr_mirror_instance
+   */
+  G3kObjectManager *manager = g3k_context_get_manager (self->g3k);
+
+  GSList *windows = g3k_object_manager_get_objects (manager);
+  g_slist_foreach (windows, (GFunc) _destroy_vr_window, self);
+
+  /* Let the outside world know that the single instance is gone. */
+  shell_vr_mirror_instance = NULL;
+
+  g_object_unref (self);
+}
+
diff --git a/src/shell-vr-mirror.h b/src/shell-vr-mirror.h
new file mode 100644
index 000000000..9480e8343
--- /dev/null
+++ b/src/shell-vr-mirror.h
@@ -0,0 +1,52 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+#ifndef __SHELL_VR_MIRROR_H__
+#define __SHELL_VR_MIRROR_H__
+
+#include <clutter/clutter.h>
+#include "st.h"
+
+#include "shell-global.h"
+#include <xrd.h>
+
+/* TODO: generate enum header with gschemas */
+typedef enum {
+  UPLOAD_METHOD_VK_SHARED_GL_MEMORY = 1,
+  UPLOAD_METHOD_VK_UPLOAD_RAW
+} UploadMethod;
+
+G_BEGIN_DECLS
+
+#define SHELL_TYPE_VR_MIRROR (shell_vr_mirror_get_type ())
+G_DECLARE_FINAL_TYPE (ShellVRMirror, shell_vr_mirror,
+                      SHELL, VR_MIRROR, GObject)
+
+#define SHELL_TYPE_VR_EFFECT (shell_vr_effect_get_type ())
+G_DECLARE_FINAL_TYPE (ShellVREffect, shell_vr_effect,
+                      SHELL, VR_EFFECT, ClutterEffect)
+
+ShellVRMirror *shell_vr_mirror_new          (void);
+ShellVRMirror *shell_vr_mirror_get_instance (void);
+ShellVRMirror *shell_vr_mirror_create_instance (void);
+void shell_vr_mirror_destroy_instance (void);
+
+void shell_vr_mirror_initialize (ShellVRMirror *self);
+
+gboolean
+shell_vr_mirror_map_actor (ShellVRMirror   *self,
+                           MetaWindowActor *actor);
+
+gboolean
+shell_vr_mirror_destroy_actor (ShellVRMirror   *self,
+                               MetaWindowActor *actor);
+
+gboolean
+shell_vr_mirror_actor_size_changed (ShellVRMirror   *self,
+                                    MetaWindowActor *actor);
+
+void
+shell_dbus_xr_init (void);
+
+G_END_DECLS
+
+#endif /* __SHELL_VR_MIRROR_H__ */
diff --git a/src/shell-wm.c b/src/shell-wm.c
index 5663b8647..d7eed62b3 100644
--- a/src/shell-wm.c
+++ b/src/shell-wm.c
@@ -6,9 +6,11 @@
 
 #include <meta/meta-enum-types.h>
 #include <meta/keybindings.h>
+#include <meta/meta-shaped-texture.h>
 
 #include "shell-wm-private.h"
 #include "shell-global.h"
+#include "shell-vr-mirror.h"
 
 struct _ShellWM {
   GObject parent;
@@ -273,6 +275,19 @@ void
 shell_wm_completed_map (ShellWM         *wm,
                         MetaWindowActor *actor)
 {
+  ShellVRMirror *vr_mirror = shell_vr_mirror_get_instance ();
+  if (vr_mirror)
+    shell_vr_mirror_map_actor (vr_mirror, actor);
+
+  MetaWindow *window = meta_window_actor_get_meta_window (actor);
+  g_print ("shell_wm_completed_map: %s: %s\n",
+           meta_window_get_wm_class (window),
+           meta_window_get_title (window));
+
+  MetaRectangle rect;
+  meta_window_get_buffer_rect (window, &rect);
+  g_print ("[%d,%d] %dx%d\n", rect.x, rect.y, rect.width, rect.height);
+
   meta_plugin_map_completed (wm->plugin, actor);
 }
 
@@ -287,6 +302,10 @@ void
 shell_wm_completed_destroy (ShellWM         *wm,
                             MetaWindowActor *actor)
 {
+  g_print ("shell_wm_completed_destroy\n");
+  ShellVRMirror *vr_mirror = shell_vr_mirror_get_instance ();
+  if (vr_mirror)
+    shell_vr_mirror_destroy_actor (vr_mirror, actor);
   meta_plugin_destroy_completed (wm->plugin, actor);
 }
 
@@ -376,6 +395,10 @@ void
 _shell_wm_size_changed (ShellWM         *wm,
                         MetaWindowActor *actor)
 {
+  ShellVRMirror *vr_mirror = shell_vr_mirror_get_instance ();
+  if (vr_mirror)
+    shell_vr_mirror_actor_size_changed (vr_mirror, actor);
+
   g_signal_emit (wm, shell_wm_signals[SIZE_CHANGED], 0, actor);
 }
 
diff --git a/tools/gnome-shell-set-vr.py b/tools/gnome-shell-set-vr.py
new file mode 100755
index 000000000..bfeffef8f
--- /dev/null
+++ b/tools/gnome-shell-set-vr.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+
+available_methods = [ "enable", "disable" ]
+
+import sys
+if len(sys.argv) < 2 or (not sys.argv[1] in available_methods):
+    print("Usage: {} [enable|disable]".format(sys.argv[0]))
+    sys.exit()
+method = sys.argv[1]
+
+from gi.repository import Gio, GLib
+
+bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
+proxy = Gio.DBusProxy.new_sync(bus,
+                               Gio.DBusProxyFlags.DO_NOT_AUTO_START,
+                               None,
+                               'org.gnome.Shell.XR',
+                               '/org/gnome/Shell/XR',
+                               'org.gnome.Shell.XR',
+                               None)
+
+is_enabled = proxy.get_cached_property("enabled")
+
+# print("Is:", is_enabled)
+
+prop_proxy = Gio.DBusProxy.new_sync(bus,
+                                    Gio.DBusProxyFlags.DO_NOT_AUTO_START,
+                                    None,
+                                    'org.gnome.Shell.XR',
+                                    '/org/gnome/Shell/XR',
+                                    'org.freedesktop.DBus.Properties',
+                                    None)
+
+to_set = method == "enable"
+
+# print("Set to:", to_set)
+
+set_variant = GLib.Variant('(ssv)',("org.gnome.Shell.XR", "enabled",
+                           GLib.Variant.new_boolean(to_set)))
+
+prop_proxy.call_sync("Set", set_variant, Gio.DBusCallFlags.NONE, -1, None)
+
+get_variant = GLib.Variant('(ss)',("org.gnome.Shell.XR", "enabled"))
+
+res = prop_proxy.call_sync("Get", get_variant, Gio.DBusCallFlags.NONE, -1, None)
+
+# print ("Is:", res)
openSUSE Build Service is sponsored by