diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6483ba4e831f139e97e2e99028f2b62e8dfc629e..50a6bc9b2b87ae27935f601651407f5e5ab23e44 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -19,17 +19,21 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
 include(CMakeDependentOption)
 include(SPIR-V)
 include(GNUInstallDirs)
+if(NOT ${CMAKE_VERSION} VERSION_LESS 3.9)
+	include(CheckIPOSupported)
+	check_ipo_supported(RESULT HAS_IPO)
+endif()
 
 find_package(Eigen3 REQUIRED)
 find_package(Vulkan REQUIRED)
 find_package(EGL)
 find_package(HIDAPI)
 find_package(OpenHMD)
-find_package(OpenCV COMPONENTS core calib3d highgui imgproc imgcodecs features2d video)
+find_package(OpenCV COMPONENTS core calib3d highgui imgproc imgcodecs features2d video CONFIG)
 find_package(Libusb1)
 find_package(JPEG)
-find_package(realsense2)
-find_package(SDL2)
+find_package(realsense2 CONFIG)
+find_package(SDL2 CONFIG)
 find_package(Threads)
 find_package(ZLIB)
 find_package(cJSON)
@@ -64,6 +68,7 @@ else()
 	find_package(OpenGL)
 endif()
 
+cmake_dependent_option(CMAKE_INTERPROCEDURAL_OPTIMIZATION "Enable inter-procedural (link-time) optimization" OFF "HAS_IPO" OFF)
 cmake_dependent_option(XRT_HAVE_WAYLAND "Enable Wayland support" ON "WAYLAND_FOUND AND WAYLAND_SCANNER_FOUND AND WAYLAND_PROTOCOLS_FOUND" OFF)
 cmake_dependent_option(XRT_HAVE_XLIB "Enable xlib support" ON "X11_FOUND" OFF)
 cmake_dependent_option(XRT_HAVE_XCB "Enable xcb support" ON "XCB_FOUND" OFF)
@@ -152,6 +157,10 @@ endif()
 # Default to PIC code
 set(CMAKE_POSITION_INDEPENDENT_CODE ON)
 
+# Describe IPO setting
+if(CMAKE_INTERPROCEDURAL_OPTIMIZATION)
+	message(STATUS "Inter-procedural optimization enabled")
+endif()
 
 ###
 # Decend into madness.
diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md
index 8b1cea0a008982d41bd312417a0a6272e6cb45a2..82c404089252b97bc34e50c51eb0fe9db0c2fef9 100644
--- a/doc/CHANGELOG.md
+++ b/doc/CHANGELOG.md
@@ -172,7 +172,7 @@ SPDX-FileCopyrightText: 2020 Collabora, Ltd. and the Monado contributors
     ([!266](https://gitlab.freedesktop.org/monado/monado/merge_requests/266))
   - u/json: Add bool getter function.
     ([!266](https://gitlab.freedesktop.org/monado/monado/merge_requests/266))
-  - tracking: Expose save function with none hardcode path for calibration data.
+  - tracking: Expose save function with non-hardcoded path for calibration data.
     ([!266](https://gitlab.freedesktop.org/monado/monado/merge_requests/266))
   - tracking: Remove all path hardcoded calibration data loading and saving
     functions.
@@ -180,7 +180,7 @@ SPDX-FileCopyrightText: 2020 Collabora, Ltd. and the Monado contributors
   - threading: New helper functions and structs for doing threaded work, these are
     on a higher level then the one in os wrappers.
     ([!278](https://gitlab.freedesktop.org/monado/monado/merge_requests/278))
-  - threading: Fix missing `#pragma once` in `os/os_threading.h`.
+  - threading: Fix missing `#``pragma once` in `os/os_threading.h`.
     ([!282](https://gitlab.freedesktop.org/monado/monado/merge_requests/282))
   - u/time: Temporarily disable the time skew in time state and used fixed offset
     instead to fix various time issues in `st/oxr`. Will be fixed properly later.
diff --git a/doc/changes/misc_features/mr.330.md b/doc/changes/misc_features/mr.330.md
new file mode 100644
index 0000000000000000000000000000000000000000..120b0cf0fbfecbb9a23ff15a94097cb5ac3773fe
--- /dev/null
+++ b/doc/changes/misc_features/mr.330.md
@@ -0,0 +1 @@
+build: Allow enabling inter-procedural optimization in CMake GUIs, if supported by platform and compiler.
diff --git a/doc/changes/state_trackers/mr.377.md b/doc/changes/state_trackers/mr.377.md
new file mode 100644
index 0000000000000000000000000000000000000000..55313b7b227953126be5706702dd6336b9b38044
--- /dev/null
+++ b/doc/changes/state_trackers/mr.377.md
@@ -0,0 +1,2 @@
+OpenXR: More correctly verify the interactive profile binding data, including
+the given interactive profile is correct and the binding point is valid.
diff --git a/src/xrt/auxiliary/util/u_hashmap.cpp b/src/xrt/auxiliary/util/u_hashmap.cpp
index 325f1cd659c0a5dbbdac0678beaf4a97dae324ba..7d65135ca0ffce92882f5cab0de1767a40651577 100644
--- a/src/xrt/auxiliary/util/u_hashmap.cpp
+++ b/src/xrt/auxiliary/util/u_hashmap.cpp
@@ -73,6 +73,12 @@ u_hashmap_int_erase(struct u_hashmap_int *hmi, uint64_t key)
 	return 0;
 }
 
+bool
+u_hashmap_int_empty(const struct u_hashmap_int *hmi)
+{
+	return hmi->map.empty();
+}
+
 extern "C" void
 u_hashmap_int_clear_and_call_for_each(struct u_hashmap_int *hmi,
                                       u_hashmap_int_callback cb,
diff --git a/src/xrt/auxiliary/util/u_hashmap.h b/src/xrt/auxiliary/util/u_hashmap.h
index a123e1b26af45e68bf921a39405920c22807a0c6..f9afcf0b52d63c1a6ec70312ecbfd727ea1c24ce 100644
--- a/src/xrt/auxiliary/util/u_hashmap.h
+++ b/src/xrt/auxiliary/util/u_hashmap.h
@@ -41,6 +41,12 @@ u_hashmap_int_insert(struct u_hashmap_int *hmi, uint64_t key, void *value);
 int
 u_hashmap_int_erase(struct u_hashmap_int *hmi, uint64_t key);
 
+/*!
+ * Is the hash map empty?
+ */
+bool
+u_hashmap_int_empty(const struct u_hashmap_int *hmi);
+
 /*!
  * First clear the hashmap and then call the given callback with each item that
  * was in the hashmap.
diff --git a/src/xrt/auxiliary/vk/vk_helpers.c b/src/xrt/auxiliary/vk/vk_helpers.c
index 4b9061acae662986adccf12f15f5c816682c9891..c499001d627aa08fb93b71a4a83190a4aa14b2fc 100644
--- a/src/xrt/auxiliary/vk/vk_helpers.c
+++ b/src/xrt/auxiliary/vk/vk_helpers.c
@@ -276,7 +276,8 @@ vk_create_image_from_fd(struct vk_bundle *vk,
                         VkImage *out_image,
                         VkDeviceMemory *out_mem)
 {
-	VkImageUsageFlags image_usage = (VkImageUsageFlags)0;
+	VkImageUsageFlags image_usage =
+	    VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
 	VkImage image = VK_NULL_HANDLE;
 	VkResult ret = VK_SUCCESS;
 
@@ -291,9 +292,6 @@ vk_create_image_from_fd(struct vk_bundle *vk,
 	if ((swapchain_usage & XRT_SWAPCHAIN_USAGE_DEPTH_STENCIL) != 0) {
 		image_usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
 	}
-	if ((swapchain_usage & XRT_SWAPCHAIN_USAGE_UNORDERED_ACCESS) != 0) {
-		image_usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
-	}
 	if ((swapchain_usage & XRT_SWAPCHAIN_USAGE_TRANSFER_SRC) != 0) {
 		image_usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
 	}
@@ -348,6 +346,38 @@ vk_create_image_from_fd(struct vk_bundle *vk,
 	return ret;
 }
 
+VkResult
+vk_create_semaphore_from_fd(struct vk_bundle *vk, int fd, VkSemaphore *out_sem)
+{
+	VkResult ret;
+
+	VkSemaphoreCreateInfo semaphore_create_info = {
+	    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
+	};
+	ret = vk->vkCreateSemaphore(vk->device, &semaphore_create_info, NULL,
+	                            out_sem);
+	if (ret != VK_SUCCESS) {
+		VK_ERROR(vk, "vkCreateSemaphore: %s", vk_result_string(ret));
+		// Nothing to cleanup
+		return ret;
+	}
+
+	VkImportSemaphoreFdInfoKHR import_semaphore_fd_info = {
+	    .sType = VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR,
+	    .semaphore = *out_sem,
+	    .handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT,
+	    .fd = fd,
+	};
+	ret = vk->vkImportSemaphoreFdKHR(vk->device, &import_semaphore_fd_info);
+	if (ret != VK_SUCCESS) {
+		VK_ERROR(vk, "vkImportSemaphoreFdKHR: %s",
+		         vk_result_string(ret));
+		vk->vkDestroySemaphore(vk->device, *out_sem, NULL);
+		return ret;
+	}
+	return ret;
+}
+
 VkResult
 vk_create_sampler(struct vk_bundle *vk, VkSampler *out_sampler)
 {
@@ -497,7 +527,6 @@ VkResult
 vk_submit_cmd_buffer(struct vk_bundle *vk, VkCommandBuffer cmd_buffer)
 {
 	VkResult ret = VK_SUCCESS;
-	VkQueue queue;
 	VkFence fence;
 	VkFenceCreateInfo fence_info = {
 	    .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
@@ -515,9 +544,6 @@ vk_submit_cmd_buffer(struct vk_bundle *vk, VkCommandBuffer cmd_buffer)
 		goto out;
 	}
 
-	// Get the queue.
-	vk->vkGetDeviceQueue(vk->device, vk->queue_family_index, 0, &queue);
-
 	// Create the fence.
 	ret = vk->vkCreateFence(vk->device, &fence_info, NULL, &fence);
 	if (ret != VK_SUCCESS) {
@@ -526,9 +552,9 @@ vk_submit_cmd_buffer(struct vk_bundle *vk, VkCommandBuffer cmd_buffer)
 	}
 
 	// Do the actual submitting.
-	ret = vk->vkQueueSubmit(queue, 1, &submitInfo, fence);
+	ret = vk->vkQueueSubmit(vk->queue, 1, &submitInfo, fence);
 	if (ret != VK_SUCCESS) {
-		VK_ERROR(vk, "Error: Could not submit queue.\n");
+		VK_ERROR(vk, "Error: Could not submit to queue.\n");
 		goto out_fence;
 	}
 
@@ -711,6 +737,8 @@ vk_get_device_functions(struct vk_bundle *vk)
 	vk->vkGetSwapchainImagesKHR       = GET_DEV_PROC(vk, vkGetSwapchainImagesKHR);
 	vk->vkAcquireNextImageKHR         = GET_DEV_PROC(vk, vkAcquireNextImageKHR);
 	vk->vkQueuePresentKHR             = GET_DEV_PROC(vk, vkQueuePresentKHR);
+	vk->vkImportSemaphoreFdKHR        = GET_DEV_PROC(vk, vkImportSemaphoreFdKHR);
+	vk->vkGetSemaphoreFdKHR           = GET_DEV_PROC(vk, vkGetSemaphoreFdKHR);
 	// clang-format on
 
 	return VK_SUCCESS;
@@ -915,6 +943,7 @@ vk_create_device(struct vk_bundle *vk, int forced_index)
 	if (ret != VK_SUCCESS) {
 		goto err_destroy;
 	}
+	vk->vkGetDeviceQueue(vk->device, vk->queue_family_index, 0, &vk->queue);
 
 	return ret;
 
@@ -966,6 +995,8 @@ vk_init_from_given(struct vk_bundle *vk,
 		goto err_memset;
 	}
 
+	vk->vkGetDeviceQueue(vk->device, vk->queue_family_index, 0, &vk->queue);
+
 	// Create the pool.
 	ret = vk_init_cmd_pool(vk);
 	if (ret != VK_SUCCESS) {
@@ -1005,6 +1036,37 @@ vk_get_access_flags(VkImageLayout layout)
 	return 0;
 }
 
+VkAccessFlags
+vk_swapchain_access_flags(enum xrt_swapchain_usage_bits bits)
+{
+	VkAccessFlags result = 0;
+	if ((bits & XRT_SWAPCHAIN_USAGE_UNORDERED_ACCESS) != 0) {
+		result |= VK_ACCESS_INPUT_ATTACHMENT_READ_BIT;
+		if ((bits & XRT_SWAPCHAIN_USAGE_COLOR) != 0) {
+			result |= VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;
+		}
+		if ((bits & XRT_SWAPCHAIN_USAGE_DEPTH_STENCIL) != 0) {
+			result |= VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
+		}
+	}
+	if ((bits & XRT_SWAPCHAIN_USAGE_COLOR) != 0) {
+		result |= VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+	}
+	if ((bits & XRT_SWAPCHAIN_USAGE_DEPTH_STENCIL) != 0) {
+		result |= VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+	}
+	if ((bits & XRT_SWAPCHAIN_USAGE_TRANSFER_SRC) != 0) {
+		result |= VK_ACCESS_TRANSFER_READ_BIT;
+	}
+	if ((bits & XRT_SWAPCHAIN_USAGE_TRANSFER_DST) != 0) {
+		result |= VK_ACCESS_TRANSFER_WRITE_BIT;
+	}
+	if ((bits & XRT_SWAPCHAIN_USAGE_SAMPLED) != 0) {
+		result |= VK_ACCESS_SHADER_READ_BIT;
+	}
+	return result;
+}
+
 bool
 vk_init_descriptor_pool(struct vk_bundle *vk,
                         const VkDescriptorPoolSize *pool_sizes,
diff --git a/src/xrt/auxiliary/vk/vk_helpers.h b/src/xrt/auxiliary/vk/vk_helpers.h
index 324e1fc13ac2919264cc12ba71bd4b4ec85274ed..29954d8ba434a40604b4f6736dcb3ff4320981b6 100644
--- a/src/xrt/auxiliary/vk/vk_helpers.h
+++ b/src/xrt/auxiliary/vk/vk_helpers.h
@@ -40,6 +40,7 @@ struct vk_bundle
 	VkDevice device;
 	uint32_t queue_family_index;
 	uint32_t queue_index;
+	VkQueue queue;
 
 	VkDebugReportCallbackEXT debug_report_cb;
 
@@ -177,6 +178,9 @@ struct vk_bundle
 	PFN_vkGetSwapchainImagesKHR vkGetSwapchainImagesKHR;
 	PFN_vkAcquireNextImageKHR vkAcquireNextImageKHR;
 	PFN_vkQueuePresentKHR vkQueuePresentKHR;
+
+	PFN_vkImportSemaphoreFdKHR vkImportSemaphoreFdKHR;
+	PFN_vkGetSemaphoreFdKHR vkGetSemaphoreFdKHR;
 	// clang-format on
 };
 
@@ -350,6 +354,12 @@ vk_create_image_from_fd(struct vk_bundle *vk,
                         VkImage *out_image,
                         VkDeviceMemory *out_mem);
 
+/*!
+ * @ingroup aux_vk
+ */
+VkResult
+vk_create_semaphore_from_fd(struct vk_bundle *vk, int fd, VkSemaphore *out_sem);
+
 /*!
  * @ingroup aux_vk
  */
@@ -406,6 +416,9 @@ vk_submit_cmd_buffer(struct vk_bundle *vk, VkCommandBuffer cmd_buffer);
 VkAccessFlags
 vk_get_access_flags(VkImageLayout layout);
 
+VkAccessFlags
+vk_swapchain_access_flags(enum xrt_swapchain_usage_bits bits);
+
 bool
 vk_init_descriptor_pool(struct vk_bundle *vk,
                         const VkDescriptorPoolSize *pool_sizes,
diff --git a/src/xrt/compositor/client/comp_gl_client.c b/src/xrt/compositor/client/comp_gl_client.c
index 118d4bf08b7f7d4d89bfcee5b1f3f952627316c0..a231adafcc54686da788f845a55243ac74fb66b0 100644
--- a/src/xrt/compositor/client/comp_gl_client.c
+++ b/src/xrt/compositor/client/comp_gl_client.c
@@ -56,12 +56,13 @@ client_gl_swapchain_destroy(struct xrt_swapchain *xsc)
 }
 
 static xrt_result_t
-client_gl_swapchain_acquire_image(struct xrt_swapchain *xsc, uint32_t *index)
+client_gl_swapchain_acquire_image(struct xrt_swapchain *xsc,
+                                  uint32_t *out_index)
 {
 	struct client_gl_swapchain *sc = client_gl_swapchain(xsc);
 
 	// Pipe down call into fd swapchain.
-	return xrt_swapchain_acquire_image(&sc->xscfd->base, index);
+	return xrt_swapchain_acquire_image(&sc->xscfd->base, out_index);
 }
 
 static xrt_result_t
@@ -154,61 +155,40 @@ client_gl_compositor_layer_begin(struct xrt_compositor *xc,
 }
 
 static xrt_result_t
-client_gl_compositor_layer_stereo_projection(
-    struct xrt_compositor *xc,
-    uint64_t timestamp,
-    struct xrt_device *xdev,
-    enum xrt_input_name name,
-    enum xrt_layer_composition_flags layer_flags,
-    struct xrt_swapchain *l_sc,
-    uint32_t l_image_index,
-    struct xrt_rect *l_rect,
-    uint32_t l_array_index,
-    struct xrt_fov *l_fov,
-    struct xrt_pose *l_pose,
-    struct xrt_swapchain *r_sc,
-    uint32_t r_image_index,
-    struct xrt_rect *r_rect,
-    uint32_t r_array_index,
-    struct xrt_fov *r_fov,
-    struct xrt_pose *r_pose,
-    bool flip_y)
+client_gl_compositor_layer_stereo_projection(struct xrt_compositor *xc,
+                                             struct xrt_device *xdev,
+                                             struct xrt_swapchain *l_xsc,
+                                             struct xrt_swapchain *r_xsc,
+                                             struct xrt_layer_data *data)
 {
 	struct client_gl_compositor *c = client_gl_compositor(xc);
 	struct xrt_swapchain *l_xscfd, *r_xscfd;
 
-	l_xscfd = &client_gl_swapchain(l_sc)->xscfd->base;
-	r_xscfd = &client_gl_swapchain(r_sc)->xscfd->base;
+	assert(data->type == XRT_LAYER_STEREO_PROJECTION);
 
-	return xrt_comp_layer_stereo_projection(
-	    &c->xcfd->base, timestamp, xdev, name, layer_flags, l_xscfd,
-	    l_image_index, l_rect, l_array_index, l_fov, l_pose, r_xscfd,
-	    r_image_index, r_rect, r_array_index, r_fov, r_pose, true);
+	l_xscfd = &client_gl_swapchain(l_xsc)->xscfd->base;
+	r_xscfd = &client_gl_swapchain(r_xsc)->xscfd->base;
+	data->flip_y = true;
+
+	return xrt_comp_layer_stereo_projection(&c->xcfd->base, xdev, l_xscfd,
+	                                        r_xscfd, data);
 }
 
 static xrt_result_t
 client_gl_compositor_layer_quad(struct xrt_compositor *xc,
-                                uint64_t timestamp,
                                 struct xrt_device *xdev,
-                                enum xrt_input_name name,
-                                enum xrt_layer_composition_flags layer_flags,
-                                enum xrt_layer_eye_visibility visibility,
-                                struct xrt_swapchain *sc,
-                                uint32_t image_index,
-                                struct xrt_rect *rect,
-                                uint32_t array_index,
-                                struct xrt_pose *pose,
-                                struct xrt_vec2 *size,
-                                bool flip_y)
+                                struct xrt_swapchain *xsc,
+                                struct xrt_layer_data *data)
 {
 	struct client_gl_compositor *c = client_gl_compositor(xc);
 	struct xrt_swapchain *xscfb;
 
-	xscfb = &client_gl_swapchain(sc)->xscfd->base;
+	assert(data->type == XRT_LAYER_QUAD);
+
+	xscfb = &client_gl_swapchain(xsc)->xscfd->base;
+	data->flip_y = true;
 
-	return xrt_comp_layer_quad(&c->xcfd->base, timestamp, xdev, name,
-	                           layer_flags, visibility, xscfb, image_index,
-	                           rect, array_index, pose, size, true);
+	return xrt_comp_layer_quad(&c->xcfd->base, xdev, xscfb, data);
 }
 
 static xrt_result_t
diff --git a/src/xrt/compositor/client/comp_vk_client.c b/src/xrt/compositor/client/comp_vk_client.c
index aa00ea5d2423b906164d3cdece07cc3db28e4134..59b0939a520348e2cf9a525bac8713b79d246d4a 100644
--- a/src/xrt/compositor/client/comp_vk_client.c
+++ b/src/xrt/compositor/client/comp_vk_client.c
@@ -8,13 +8,14 @@
  * @ingroup comp_client
  */
 
-#include <stdio.h>
-#include <stdlib.h>
-
 #include "util/u_misc.h"
 
 #include "comp_vk_client.h"
 
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+
 /*!
  * Down-cast helper.
  *
@@ -70,12 +71,32 @@ client_vk_swapchain_destroy(struct xrt_swapchain *xsc)
 }
 
 static xrt_result_t
-client_vk_swapchain_acquire_image(struct xrt_swapchain *xsc, uint32_t *index)
+client_vk_swapchain_acquire_image(struct xrt_swapchain *xsc,
+                                  uint32_t *out_index)
 {
 	struct client_vk_swapchain *sc = client_vk_swapchain(xsc);
+	struct vk_bundle *vk = &sc->c->vk;
 
 	// Pipe down call into fd swapchain.
-	return xrt_swapchain_acquire_image(&sc->xscfd->base, index);
+	xrt_result_t xret =
+	    xrt_swapchain_acquire_image(&sc->xscfd->base, out_index);
+	if (xret != XRT_SUCCESS) {
+		return xret;
+	}
+
+	// Acquire ownership and complete layout transition
+	VkSubmitInfo submitInfo = {
+	    .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
+	    .commandBufferCount = 1,
+	    .pCommandBuffers = &sc->base.acquire[*out_index],
+	};
+	VkResult ret =
+	    vk->vkQueueSubmit(vk->queue, 1, &submitInfo, VK_NULL_HANDLE);
+	if (ret != VK_SUCCESS) {
+		VK_ERROR(vk, "Error: Could not submit to queue.\n");
+		return XRT_ERROR_FAILED_TO_SUBMIT_VULKAN_COMMANDS;
+	}
+	return XRT_SUCCESS;
 }
 
 static xrt_result_t
@@ -93,6 +114,20 @@ static xrt_result_t
 client_vk_swapchain_release_image(struct xrt_swapchain *xsc, uint32_t index)
 {
 	struct client_vk_swapchain *sc = client_vk_swapchain(xsc);
+	struct vk_bundle *vk = &sc->c->vk;
+
+	// Release ownership and begin layout transition
+	VkSubmitInfo submitInfo = {
+	    .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
+	    .commandBufferCount = 1,
+	    .pCommandBuffers = &sc->base.release[index],
+	};
+	VkResult ret =
+	    vk->vkQueueSubmit(vk->queue, 1, &submitInfo, VK_NULL_HANDLE);
+	if (ret != VK_SUCCESS) {
+		VK_ERROR(vk, "Error: Could not submit to queue.\n");
+		return XRT_ERROR_FAILED_TO_SUBMIT_VULKAN_COMMANDS;
+	}
 
 	// Pipe down call into fd swapchain.
 	return xrt_swapchain_release_image(&sc->xscfd->base, index);
@@ -118,6 +153,10 @@ client_vk_compositor_destroy(struct xrt_compositor *xc)
 	struct client_vk_compositor *c = client_vk_compositor(xc);
 
 	if (c->vk.cmd_pool != VK_NULL_HANDLE) {
+		// Make sure that any of the command buffers from this command
+		// pool are n used here, this pleases the validation layer.
+		c->vk.vkDeviceWaitIdle(c->vk.device);
+
 		c->vk.vkDestroyCommandPool(c->vk.device, c->vk.cmd_pool, NULL);
 		c->vk.cmd_pool = VK_NULL_HANDLE;
 	}
@@ -190,61 +229,40 @@ client_vk_compositor_layer_begin(struct xrt_compositor *xc,
 }
 
 static xrt_result_t
-client_vk_compositor_layer_stereo_projection(
-    struct xrt_compositor *xc,
-    uint64_t timestamp,
-    struct xrt_device *xdev,
-    enum xrt_input_name name,
-    enum xrt_layer_composition_flags layer_flags,
-    struct xrt_swapchain *l_sc,
-    uint32_t l_image_index,
-    struct xrt_rect *l_rect,
-    uint32_t l_array_index,
-    struct xrt_fov *l_fov,
-    struct xrt_pose *l_pose,
-    struct xrt_swapchain *r_sc,
-    uint32_t r_image_index,
-    struct xrt_rect *r_rect,
-    uint32_t r_array_index,
-    struct xrt_fov *r_fov,
-    struct xrt_pose *r_pose,
-    bool flip_y)
+client_vk_compositor_layer_stereo_projection(struct xrt_compositor *xc,
+                                             struct xrt_device *xdev,
+                                             struct xrt_swapchain *l_xsc,
+                                             struct xrt_swapchain *r_xsc,
+                                             struct xrt_layer_data *data)
 {
 	struct client_vk_compositor *c = client_vk_compositor(xc);
 	struct xrt_swapchain *l_xscfd, *r_xscfd;
 
-	l_xscfd = &client_vk_swapchain(l_sc)->xscfd->base;
-	r_xscfd = &client_vk_swapchain(r_sc)->xscfd->base;
+	assert(data->type == XRT_LAYER_STEREO_PROJECTION);
+
+	l_xscfd = &client_vk_swapchain(l_xsc)->xscfd->base;
+	r_xscfd = &client_vk_swapchain(r_xsc)->xscfd->base;
+	data->flip_y = false;
 
-	return xrt_comp_layer_stereo_projection(
-	    &c->xcfd->base, timestamp, xdev, name, layer_flags, l_xscfd,
-	    l_image_index, l_rect, l_array_index, l_fov, l_pose, r_xscfd,
-	    r_image_index, r_rect, r_array_index, r_fov, r_pose, false);
+	return xrt_comp_layer_stereo_projection(&c->xcfd->base, xdev, l_xscfd,
+	                                        r_xscfd, data);
 }
 
 static xrt_result_t
 client_vk_compositor_layer_quad(struct xrt_compositor *xc,
-                                uint64_t timestamp,
                                 struct xrt_device *xdev,
-                                enum xrt_input_name name,
-                                enum xrt_layer_composition_flags layer_flags,
-                                enum xrt_layer_eye_visibility visibility,
-                                struct xrt_swapchain *sc,
-                                uint32_t image_index,
-                                struct xrt_rect *rect,
-                                uint32_t array_index,
-                                struct xrt_pose *pose,
-                                struct xrt_vec2 *size,
-                                bool flip_y)
+                                struct xrt_swapchain *xsc,
+                                struct xrt_layer_data *data)
 {
 	struct client_vk_compositor *c = client_vk_compositor(xc);
 	struct xrt_swapchain *xscfb;
 
-	xscfb = &client_vk_swapchain(sc)->xscfd->base;
+	assert(data->type == XRT_LAYER_QUAD);
 
-	return xrt_comp_layer_quad(&c->xcfd->base, timestamp, xdev, name,
-	                           layer_flags, visibility, xscfb, image_index,
-	                           rect, array_index, pose, size, false);
+	xscfb = &client_vk_swapchain(xsc)->xscfd->base;
+	data->flip_y = false;
+
+	return xrt_comp_layer_quad(&c->xcfd->base, xdev, xscfb, data);
 }
 
 static xrt_result_t
@@ -288,9 +306,9 @@ client_vk_swapchain_create(struct xrt_compositor *xc,
 	VkImageSubresourceRange subresource_range = {
 	    .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
 	    .baseMipLevel = 0,
-	    .levelCount = 1,
+	    .levelCount = VK_REMAINING_MIP_LEVELS,
 	    .baseArrayLayer = 0,
-	    .layerCount = array_size,
+	    .layerCount = VK_REMAINING_ARRAY_LAYERS,
 	};
 
 	struct client_vk_swapchain *sc =
@@ -316,10 +334,15 @@ client_vk_swapchain_create(struct xrt_compositor *xc,
 			return NULL;
 		}
 
+		/*
+		 * This is only to please the validation layer, that may or may
+		 * not be a bug in the validation layer. That may or may not be
+		 * fixed in the future version of the validation layer.
+		 */
 		vk_set_image_layout(&c->vk, cmd_buffer, sc->base.images[i], 0,
 		                    VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
 		                    VK_IMAGE_LAYOUT_UNDEFINED,
-		                    VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+		                    VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
 		                    subresource_range);
 	}
 
@@ -328,6 +351,90 @@ client_vk_swapchain_create(struct xrt_compositor *xc,
 		return NULL;
 	}
 
+	// Prerecord command buffers for swapchain image ownership/layout
+	// transitions
+	for (uint32_t i = 0; i < xsc->num_images; i++) {
+		ret = vk_init_cmd_buffer(&c->vk, &sc->base.acquire[i]);
+		if (ret != VK_SUCCESS) {
+			return NULL;
+		}
+		ret = vk_init_cmd_buffer(&c->vk, &sc->base.release[i]);
+		if (ret != VK_SUCCESS) {
+			return NULL;
+		}
+
+		VkImageSubresourceRange subresource_range = {
+		    .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+		    .baseMipLevel = 0,
+		    .levelCount = VK_REMAINING_MIP_LEVELS,
+		    .baseArrayLayer = 0,
+		    .layerCount = VK_REMAINING_ARRAY_LAYERS,
+		};
+
+		/*
+		 * The biggest reason is that VK_IMAGE_LAYOUT_PRESENT_SRC_KHR is
+		 * used here is that this is what hello_xr used to barrier to,
+		 * and it worked on a wide verity of drivers. So it's safe.
+		 *
+		 * There might not be a Vulkan renderer on the other endm
+		 * there could be a OpenGL compositor, heck there could be a X
+		 * server even. On Linux VK_IMAGE_LAYOUT_PRESENT_SRC_KHR is what
+		 * you use if you want to "flush" out all of the pixels to the
+		 * memory buffer that has been shared to you from a X11 server.
+		 *
+		 * This is not what the spec says you should do when it comes to
+		 * external images thou. Instead we should use the queue family
+		 * index `VK_QUEUE_FAMILY_EXTERNAL`. And use semaphores to
+		 * synchronize.
+		 */
+		VkImageMemoryBarrier acquire = {
+		    .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+		    .srcAccessMask = 0,
+		    .dstAccessMask = vk_swapchain_access_flags(bits),
+		    .oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
+		    .newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+		    .srcQueueFamilyIndex = c->vk.queue_family_index,
+		    .dstQueueFamilyIndex = c->vk.queue_family_index,
+		    .image = sc->base.images[i],
+		    .subresourceRange = subresource_range,
+		};
+
+		VkImageMemoryBarrier release = {
+		    .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+		    .srcAccessMask = vk_swapchain_access_flags(bits),
+		    .dstAccessMask = 0,
+		    .oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+		    .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
+		    .srcQueueFamilyIndex = c->vk.queue_family_index,
+		    .dstQueueFamilyIndex = c->vk.queue_family_index,
+		    .image = sc->base.images[i],
+		    .subresourceRange = subresource_range,
+		};
+
+		//! @todo less conservative pipeline stage masks based on usage
+		c->vk.vkCmdPipelineBarrier(sc->base.acquire[i],
+		                           VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
+		                           VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+		                           0, 0, NULL, 0, NULL, 1, &acquire);
+		c->vk.vkCmdPipelineBarrier(sc->base.release[i],
+		                           VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+		                           VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
+		                           0, 0, NULL, 0, NULL, 1, &release);
+
+		ret = c->vk.vkEndCommandBuffer(sc->base.acquire[i]);
+		if (ret != VK_SUCCESS) {
+			VK_ERROR(vk, "vkEndCommandBuffer: %s",
+			         vk_result_string(ret));
+			return NULL;
+		}
+		ret = c->vk.vkEndCommandBuffer(sc->base.release[i]);
+		if (ret != VK_SUCCESS) {
+			VK_ERROR(vk, "vkEndCommandBuffer: %s",
+			         vk_result_string(ret));
+			return NULL;
+		}
+	}
+
 	return &sc->base.base;
 }
 
diff --git a/src/xrt/compositor/main/comp_compositor.c b/src/xrt/compositor/main/comp_compositor.c
index 6ca0a045dbb394deccfe55fcdf97e16ec111790a..5ea1ce993dfd04bf2413f08cd59ff409857da7ba 100644
--- a/src/xrt/compositor/main/comp_compositor.c
+++ b/src/xrt/compositor/main/comp_compositor.c
@@ -294,23 +294,10 @@ compositor_layer_begin(struct xrt_compositor *xc,
 
 static xrt_result_t
 compositor_layer_stereo_projection(struct xrt_compositor *xc,
-                                   uint64_t timestamp,
                                    struct xrt_device *xdev,
-                                   enum xrt_input_name name,
-                                   enum xrt_layer_composition_flags layer_flags,
-                                   struct xrt_swapchain *l_sc,
-                                   uint32_t l_image_index,
-                                   struct xrt_rect *l_rect,
-                                   uint32_t l_array_index,
-                                   struct xrt_fov *l_fov,
-                                   struct xrt_pose *l_pose,
-                                   struct xrt_swapchain *r_sc,
-                                   uint32_t r_image_index,
-                                   struct xrt_rect *r_rect,
-                                   uint32_t r_array_index,
-                                   struct xrt_fov *r_fov,
-                                   struct xrt_pose *r_pose,
-                                   bool flip_y)
+                                   struct xrt_swapchain *l_xsc,
+                                   struct xrt_swapchain *r_xsc,
+                                   struct xrt_layer_data *data)
 {
 	struct comp_compositor *c = comp_compositor(xc);
 
@@ -319,16 +306,9 @@ compositor_layer_stereo_projection(struct xrt_compositor *xc,
 	uint32_t layer_id = c->slots[slot_id].num_layers;
 
 	struct comp_layer *layer = &c->slots[slot_id].layers[layer_id];
-	layer->stereo.l.sc = comp_swapchain(l_sc);
-	layer->stereo.l.image_index = l_image_index;
-	layer->stereo.l.array_index = l_array_index;
-	layer->stereo.r.sc = comp_swapchain(r_sc);
-	layer->stereo.r.image_index = r_image_index;
-	layer->stereo.r.array_index = r_array_index;
-
-	layer->flags = layer_flags;
-	layer->flip_y = flip_y;
-	layer->type = COMP_LAYER_STEREO_PROJECTION;
+	layer->scs[0] = comp_swapchain(l_xsc);
+	layer->scs[1] = comp_swapchain(r_xsc);
+	layer->data = *data;
 
 	c->slots[slot_id].num_layers++;
 	return XRT_SUCCESS;
@@ -336,18 +316,9 @@ compositor_layer_stereo_projection(struct xrt_compositor *xc,
 
 static xrt_result_t
 compositor_layer_quad(struct xrt_compositor *xc,
-                      uint64_t timestamp,
                       struct xrt_device *xdev,
-                      enum xrt_input_name name,
-                      enum xrt_layer_composition_flags layer_flags,
-                      enum xrt_layer_eye_visibility visibility,
-                      struct xrt_swapchain *sc,
-                      uint32_t image_index,
-                      struct xrt_rect *rect,
-                      uint32_t array_index,
-                      struct xrt_pose *pose,
-                      struct xrt_vec2 *size,
-                      bool flip_y)
+                      struct xrt_swapchain *xsc,
+                      struct xrt_layer_data *data)
 {
 	struct comp_compositor *c = comp_compositor(xc);
 
@@ -356,17 +327,9 @@ compositor_layer_quad(struct xrt_compositor *xc,
 	uint32_t layer_id = c->slots[slot_id].num_layers;
 
 	struct comp_layer *layer = &c->slots[slot_id].layers[layer_id];
-	layer->quad.sc = comp_swapchain(sc);
-	layer->quad.visibility = visibility;
-	layer->quad.image_index = image_index;
-	layer->quad.rect = *rect;
-	layer->quad.array_index = array_index;
-	layer->quad.pose = *pose;
-	layer->quad.size = *size;
-
-	layer->flags = layer_flags;
-	layer->flip_y = flip_y;
-	layer->type = COMP_LAYER_QUAD;
+	layer->scs[0] = comp_swapchain(xsc);
+	layer->scs[1] = NULL;
+	layer->data = *data;
 
 	c->slots[slot_id].num_layers++;
 	return XRT_SUCCESS;
@@ -388,25 +351,26 @@ compositor_layer_commit(struct xrt_compositor *xc)
 
 	for (uint32_t i = 0; i < num_layers; i++) {
 		struct comp_layer *layer = &c->slots[slot_id].layers[i];
-		switch (layer->type) {
-		case COMP_LAYER_QUAD: {
-			struct comp_layer_quad *quad = &layer->quad;
+		struct xrt_layer_data *data = &layer->data;
+		switch (data->type) {
+		case XRT_LAYER_QUAD: {
+			struct xrt_layer_quad_data *quad = &layer->data.quad;
 			struct comp_swapchain_image *image;
-			image = &quad->sc->images[quad->image_index];
-			comp_renderer_set_quad_layer(c->r, image, &quad->pose,
-			                             &quad->size, layer->flip_y,
-			                             i, quad->array_index);
+			image = &layer->scs[0]->images[quad->sub.image_index];
+			comp_renderer_set_quad_layer(c->r, i, image, data);
 		} break;
-		case COMP_LAYER_STEREO_PROJECTION: {
-			struct comp_layer_stereo *stereo = &layer->stereo;
+		case XRT_LAYER_STEREO_PROJECTION: {
+			struct xrt_layer_stereo_projection_data *stereo =
+			    &data->stereo;
 			struct comp_swapchain_image *right;
 			struct comp_swapchain_image *left;
-			left = &stereo->l.sc->images[stereo->l.image_index];
-			right = &stereo->l.sc->images[stereo->r.image_index];
+			left =
+			    &layer->scs[0]->images[stereo->l.sub.image_index];
+			right =
+			    &layer->scs[1]->images[stereo->r.sub.image_index];
 
-			comp_renderer_set_projection_layer(
-			    c->r, left, right, layer->flip_y, i,
-			    stereo->l.array_index, stereo->r.array_index);
+			comp_renderer_set_projection_layer(c->r, i, left, right,
+			                                   data);
 		} break;
 		}
 	}
diff --git a/src/xrt/compositor/main/comp_compositor.h b/src/xrt/compositor/main/comp_compositor.h
index e9142241a9f8b738584ed860420bdf96f2cde4b6..90c5c7e55356750d206fb1188dcfec4d40ab0743 100644
--- a/src/xrt/compositor/main/comp_compositor.h
+++ b/src/xrt/compositor/main/comp_compositor.h
@@ -77,49 +77,6 @@ struct comp_swapchain
 	 */
 	struct u_index_fifo fifo;
 };
-/*!
- * Tag for distinguishing the union contents of @ref comp_layer.
- */
-enum comp_layer_type
-{
-	//! comp_layer::stereo is initialized
-	COMP_LAYER_STEREO_PROJECTION,
-	//! comp_layer::quad is initialized
-	COMP_LAYER_QUAD,
-};
-
-/*!
- * A quad layer.
- *
- * @ingroup comp_main
- * @see comp_layer
- */
-struct comp_layer_quad
-{
-	struct comp_swapchain *sc;
-	enum xrt_layer_eye_visibility visibility;
-	uint32_t image_index;
-	struct xrt_rect rect;
-	uint32_t array_index;
-	struct xrt_pose pose;
-	struct xrt_vec2 size;
-};
-
-/*!
- * A stereo projection layer.
- *
- * @ingroup comp_main
- * @see comp_layer
- */
-struct comp_layer_stereo
-{
-	struct
-	{
-		struct comp_swapchain *sc;
-		uint32_t image_index;
-		uint32_t array_index;
-	} l, r;
-};
 
 /*!
  * A single layer.
@@ -129,14 +86,17 @@ struct comp_layer_stereo
  */
 struct comp_layer
 {
-	int64_t timestamp;
-	enum xrt_layer_composition_flags flags;
-	enum comp_layer_type type;
-	bool flip_y;
-	union {
-		struct comp_layer_quad quad;
-		struct comp_layer_stereo stereo;
-	};
+	/*!
+	 * Up to two compositor swapchains referenced per layer.
+	 *
+	 * Unused elements should be set to null.
+	 */
+	struct comp_swapchain *scs[2];
+
+	/*!
+	 * All basic (trivially-serializable) data associated with a layer.
+	 */
+	struct xrt_layer_data data;
 };
 
 /*!
diff --git a/src/xrt/compositor/main/comp_layer.c b/src/xrt/compositor/main/comp_layer.c
index 992e1072cecb166b5bb093bdc30c0404f7f7d01f..d444f6f33bdf885fe2a11ed79ddd863913e6861d 100644
--- a/src/xrt/compositor/main/comp_layer.c
+++ b/src/xrt/compositor/main/comp_layer.c
@@ -154,7 +154,7 @@ comp_layer_update_stereo_descriptors(struct comp_render_layer *self,
 static bool
 _init(struct comp_render_layer *self,
       struct vk_bundle *vk,
-      enum comp_layer_type type,
+      enum xrt_layer_type type,
       VkDescriptorSetLayout *layout)
 {
 	self->vk = vk;
@@ -162,6 +162,7 @@ _init(struct comp_render_layer *self,
 	self->type = type;
 
 	self->visible = true;
+	self->view_space = true;
 
 	math_matrix_4x4_identity(&self->model_matrix);
 
@@ -202,7 +203,8 @@ comp_layer_draw(struct comp_render_layer *self,
                 VkPipelineLayout pipeline_layout,
                 VkCommandBuffer cmd_buffer,
                 const struct vk_buffer *vertex_buffer,
-                const struct xrt_matrix_4x4 *vp)
+                const struct xrt_matrix_4x4 *vp_world,
+                const struct xrt_matrix_4x4 *vp_eye)
 {
 	if (!self->visible)
 		return;
@@ -210,11 +212,14 @@ comp_layer_draw(struct comp_render_layer *self,
 	self->vk->vkCmdBindPipeline(cmd_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
 	                            pipeline);
 
+	// Is this layer viewspace or not.
+	const struct xrt_matrix_4x4 *vp = self->view_space ? vp_eye : vp_world;
+
 	switch (self->type) {
-	case COMP_LAYER_STEREO_PROJECTION:
+	case XRT_LAYER_STEREO_PROJECTION:
 		_update_mvp_matrix(self, eye, &proj_scale);
 		break;
-	case COMP_LAYER_QUAD: _update_mvp_matrix(self, eye, vp); break;
+	case XRT_LAYER_QUAD: _update_mvp_matrix(self, eye, vp); break;
 	}
 
 	self->vk->vkCmdBindDescriptorSets(
@@ -230,7 +235,7 @@ comp_layer_draw(struct comp_render_layer *self,
 
 struct comp_render_layer *
 comp_layer_create(struct vk_bundle *vk,
-                  enum comp_layer_type type,
+                  enum xrt_layer_type type,
                   VkDescriptorSetLayout *layout)
 {
 	struct comp_render_layer *q = U_TYPED_CALLOC(struct comp_render_layer);
diff --git a/src/xrt/compositor/main/comp_layer.h b/src/xrt/compositor/main/comp_layer.h
index 35135fa5f03842e3715e55c607ef216a6ae5efb2..c2a60ac1e8d44661b3fbd0b6d179890462542558 100644
--- a/src/xrt/compositor/main/comp_layer.h
+++ b/src/xrt/compositor/main/comp_layer.h
@@ -23,8 +23,9 @@ struct comp_render_layer
 	struct vk_bundle *vk;
 
 	bool visible;
+	bool view_space;
 
-	enum comp_layer_type type;
+	enum xrt_layer_type type;
 
 	struct layer_transformation transformation[2];
 	struct vk_buffer transformation_ubos[2];
@@ -37,7 +38,7 @@ struct comp_render_layer
 
 struct comp_render_layer *
 comp_layer_create(struct vk_bundle *vk,
-                  enum comp_layer_type type,
+                  enum xrt_layer_type type,
                   VkDescriptorSetLayout *layout);
 
 void
@@ -47,7 +48,8 @@ comp_layer_draw(struct comp_render_layer *self,
                 VkPipelineLayout pipeline_layout,
                 VkCommandBuffer cmd_buffer,
                 const struct vk_buffer *vertex_buffer,
-                const struct xrt_matrix_4x4 *vp);
+                const struct xrt_matrix_4x4 *vp_world,
+                const struct xrt_matrix_4x4 *vp_eye);
 
 void
 comp_layer_set_model_matrix(struct comp_render_layer *self,
diff --git a/src/xrt/compositor/main/comp_layer_renderer.c b/src/xrt/compositor/main/comp_layer_renderer.c
index 5abf110dfdf529a266bef166f2cb01a51c5e10c6..705afe019257b85c9b66c95dd22bc63e6f37907d 100644
--- a/src/xrt/compositor/main/comp_layer_renderer.c
+++ b/src/xrt/compositor/main/comp_layer_renderer.c
@@ -381,14 +381,18 @@ _render_eye(struct comp_layer_renderer *self,
             VkCommandBuffer cmd_buffer,
             VkPipelineLayout pipeline_layout)
 {
-	struct xrt_matrix_4x4 vp;
+	struct xrt_matrix_4x4 vp_world;
+	struct xrt_matrix_4x4 vp_eye;
 	math_matrix_4x4_multiply(&self->mat_projection[eye],
-	                         &self->mat_view[eye], &vp);
+	                         &self->mat_world_view[eye], &vp_world);
+	math_matrix_4x4_multiply(&self->mat_projection[eye],
+	                         &self->mat_eye_view[eye], &vp_eye);
 
-	for (uint32_t i = 0; i < self->num_layers; i++)
+	for (uint32_t i = 0; i < self->num_layers; i++) {
 		comp_layer_draw(self->layers[i], eye, self->pipeline,
 		                pipeline_layout, cmd_buffer,
-		                &self->vertex_buffer, &vp);
+		                &self->vertex_buffer, &vp_world, &vp_eye);
+	}
 }
 
 static bool
@@ -451,7 +455,7 @@ comp_layer_renderer_allocate_layers(struct comp_layer_renderer *self,
 
 	for (uint32_t i = 0; i < self->num_layers; i++) {
 		self->layers[i] = comp_layer_create(
-		    vk, COMP_LAYER_QUAD, &self->descriptor_set_layout);
+		    vk, XRT_LAYER_QUAD, &self->descriptor_set_layout);
 	}
 }
 
@@ -484,7 +488,8 @@ _init(struct comp_layer_renderer *self,
 
 	for (uint32_t i = 0; i < 2; i++) {
 		math_matrix_4x4_identity(&self->mat_projection[i]);
-		math_matrix_4x4_identity(&self->mat_view[i]);
+		math_matrix_4x4_identity(&self->mat_world_view[i]);
+		math_matrix_4x4_identity(&self->mat_eye_view[i]);
 	}
 
 	if (!_init_render_pass(vk, format,
@@ -649,7 +654,7 @@ comp_layer_renderer_destroy(struct comp_layer_renderer *self)
 void
 comp_layer_renderer_set_fov(struct comp_layer_renderer *self,
                             const struct xrt_fov *fov,
-                            uint32_t view_id)
+                            uint32_t eye)
 {
 	const float tan_left = tanf(fov->angle_left);
 	const float tan_right = tanf(fov->angle_right);
@@ -670,7 +675,7 @@ comp_layer_renderer_set_fov(struct comp_layer_renderer *self,
 	const float a43 = -(self->far * self->near) / (self->far - self->near);
 
 	// clang-format off
-	self->mat_projection[view_id] = (struct xrt_matrix_4x4) {
+	self->mat_projection[eye] = (struct xrt_matrix_4x4) {
 		.v = {
 			a11, 0, 0, 0,
 			0, a22, 0, 0,
@@ -683,8 +688,10 @@ comp_layer_renderer_set_fov(struct comp_layer_renderer *self,
 
 void
 comp_layer_renderer_set_pose(struct comp_layer_renderer *self,
-                             const struct xrt_pose *pose,
-                             uint32_t view_id)
+                             const struct xrt_pose *eye_pose,
+                             const struct xrt_pose *world_pose,
+                             uint32_t eye)
 {
-	math_matrix_4x4_view_from_pose(pose, &self->mat_view[view_id]);
+	math_matrix_4x4_view_from_pose(eye_pose, &self->mat_eye_view[eye]);
+	math_matrix_4x4_view_from_pose(world_pose, &self->mat_world_view[eye]);
 }
diff --git a/src/xrt/compositor/main/comp_layer_renderer.h b/src/xrt/compositor/main/comp_layer_renderer.h
index 33b5daeb22ab49feaffada3b032e96a53aa670ee..42de72c5b2b5fa5081fe8d7dffcbf9776d10805b 100644
--- a/src/xrt/compositor/main/comp_layer_renderer.h
+++ b/src/xrt/compositor/main/comp_layer_renderer.h
@@ -42,7 +42,8 @@ struct comp_layer_renderer
 	VkPipelineLayout pipeline_layout;
 	VkPipelineCache pipeline_cache;
 
-	struct xrt_matrix_4x4 mat_view[2];
+	struct xrt_matrix_4x4 mat_world_view[2];
+	struct xrt_matrix_4x4 mat_eye_view[2];
 	struct xrt_matrix_4x4 mat_projection[2];
 
 	struct vk_buffer vertex_buffer;
@@ -68,12 +69,13 @@ comp_layer_renderer_draw(struct comp_layer_renderer *self);
 void
 comp_layer_renderer_set_fov(struct comp_layer_renderer *self,
                             const struct xrt_fov *fov,
-                            uint32_t view_id);
+                            uint32_t eye);
 
 void
 comp_layer_renderer_set_pose(struct comp_layer_renderer *self,
-                             const struct xrt_pose *pose,
-                             uint32_t view_id);
+                             const struct xrt_pose *eye_pose,
+                             const struct xrt_pose *world_pose,
+                             uint32_t eye);
 
 void
 comp_layer_renderer_allocate_layers(struct comp_layer_renderer *self,
diff --git a/src/xrt/compositor/main/comp_renderer.c b/src/xrt/compositor/main/comp_renderer.c
index 3b980b2d97dea472689a2fd868f4b41354b035b9..ee114fbddbd3020f32a314d9b095b031626d999c 100644
--- a/src/xrt/compositor/main/comp_renderer.c
+++ b/src/xrt/compositor/main/comp_renderer.c
@@ -416,15 +416,15 @@ _get_view_projection(struct comp_renderer *r)
 
 		comp_layer_renderer_set_fov(r->lr, &fov, i);
 
-		struct xrt_pose view_pose;
+		struct xrt_pose eye_pose;
 		xrt_device_get_view_pose(r->c->xdev, &eye_relation, i,
-		                         &view_pose);
+		                         &eye_pose);
 
-		struct xrt_pose pose;
-		math_pose_openxr_locate(&view_pose, &relation.pose,
-		                        &base_space_pose, &pose);
+		struct xrt_pose world_pose;
+		math_pose_openxr_locate(&eye_pose, &relation.pose,
+		                        &base_space_pose, &world_pose);
 
-		comp_layer_renderer_set_pose(r->lr, &pose, i);
+		comp_layer_renderer_set_pose(r->lr, &eye_pose, &world_pose, i);
 	}
 }
 
@@ -474,44 +474,48 @@ renderer_init(struct comp_renderer *r)
 
 void
 comp_renderer_set_quad_layer(struct comp_renderer *r,
-                             struct comp_swapchain_image *image,
-                             struct xrt_pose *pose,
-                             struct xrt_vec2 *size,
-                             bool flip_y,
                              uint32_t layer,
-                             uint32_t array_index)
+                             struct comp_swapchain_image *image,
+                             struct xrt_layer_data *data)
 {
 	comp_layer_update_descriptors(r->lr->layers[layer], image->sampler,
-	                              image->views[array_index]);
+	                              image->views[data->quad.sub.array_index]);
 
 	struct xrt_matrix_4x4 model_matrix;
-	math_matrix_4x4_quad_model(pose, size, &model_matrix);
+	math_matrix_4x4_quad_model(&data->quad.pose, &data->quad.size,
+	                           &model_matrix);
 
 	comp_layer_set_model_matrix(r->lr->layers[layer], &model_matrix);
 
-	r->lr->layers[layer]->type = COMP_LAYER_QUAD;
-	comp_layer_set_flip_y(r->lr->layers[layer], flip_y);
+	comp_layer_set_flip_y(r->lr->layers[layer], data->flip_y);
+
+	r->lr->layers[layer]->type = XRT_LAYER_QUAD;
+	r->lr->layers[layer]->view_space =
+	    (data->flags & XRT_LAYER_COMPOSITION_VIEW_SPACE_BIT) != 0;
 
 	r->c->vk.vkDeviceWaitIdle(r->c->vk.device);
 }
 
 void
 comp_renderer_set_projection_layer(struct comp_renderer *r,
+                                   uint32_t layer,
                                    struct comp_swapchain_image *left_image,
                                    struct comp_swapchain_image *right_image,
-                                   bool flip_y,
-                                   uint32_t layer,
-                                   uint32_t left_array_index,
-                                   uint32_t right_array_index)
+                                   struct xrt_layer_data *data)
 {
+	uint32_t left_array_index = data->stereo.l.sub.array_index;
+	uint32_t right_array_index = data->stereo.r.sub.array_index;
+
 	comp_layer_update_stereo_descriptors(
 	    r->lr->layers[layer], left_image->sampler, right_image->sampler,
 	    left_image->views[left_array_index],
 	    right_image->views[right_array_index]);
 
-	comp_layer_set_flip_y(r->lr->layers[layer], flip_y);
+	comp_layer_set_flip_y(r->lr->layers[layer], data->flip_y);
 
-	r->lr->layers[layer]->type = COMP_LAYER_STEREO_PROJECTION;
+	r->lr->layers[layer]->type = XRT_LAYER_STEREO_PROJECTION;
+	r->lr->layers[layer]->view_space =
+	    (data->flags & XRT_LAYER_COMPOSITION_VIEW_SPACE_BIT) != 0;
 }
 
 void
diff --git a/src/xrt/compositor/main/comp_renderer.h b/src/xrt/compositor/main/comp_renderer.h
index ec3d60748b177a26f6860537f421e74108340e99..89438edc3f1fc9d003b5b352e914fa72bc8c360e 100644
--- a/src/xrt/compositor/main/comp_renderer.h
+++ b/src/xrt/compositor/main/comp_renderer.h
@@ -56,22 +56,16 @@ comp_renderer_draw(struct comp_renderer *r);
 
 void
 comp_renderer_set_projection_layer(struct comp_renderer *r,
+                                   uint32_t layer,
                                    struct comp_swapchain_image *left_image,
                                    struct comp_swapchain_image *right_image,
-                                   bool flip_y,
-                                   uint32_t layer,
-                                   uint32_t left_array_index,
-                                   uint32_t right_array_index);
+                                   struct xrt_layer_data *data);
 
 void
 comp_renderer_set_quad_layer(struct comp_renderer *r,
-                             struct comp_swapchain_image *image,
-                             struct xrt_pose *pose,
-                             struct xrt_vec2 *size,
-                             bool flip_y,
                              uint32_t layer,
-                             uint32_t array_index);
-
+                             struct comp_swapchain_image *image,
+                             struct xrt_layer_data *data);
 
 void
 comp_renderer_allocate_layers(struct comp_renderer *self, uint32_t num_layers);
diff --git a/src/xrt/compositor/main/comp_swapchain.c b/src/xrt/compositor/main/comp_swapchain.c
index ef7f5ca7e7ec3ea57523480a63d64bbd2dcd91cc..c15ceb6c1e91b78a4e3a7c68c7e2dc5eb2f69298 100644
--- a/src/xrt/compositor/main/comp_swapchain.c
+++ b/src/xrt/compositor/main/comp_swapchain.c
@@ -27,14 +27,14 @@ swapchain_destroy(struct xrt_swapchain *xsc)
 }
 
 static xrt_result_t
-swapchain_acquire_image(struct xrt_swapchain *xsc, uint32_t *index)
+swapchain_acquire_image(struct xrt_swapchain *xsc, uint32_t *out_index)
 {
 	struct comp_swapchain *sc = comp_swapchain(xsc);
 
 	COMP_SPEW(sc->c, "ACQUIRE_IMAGE");
 
 	// Returns negative on empty fifo.
-	int res = u_index_fifo_pop(&sc->fifo, index);
+	int res = u_index_fifo_pop(&sc->fifo, out_index);
 	if (res >= 0) {
 		return XRT_SUCCESS;
 	} else {
diff --git a/src/xrt/compositor/meson.build b/src/xrt/compositor/meson.build
index 84254d2edfce30746e0f2251f47d34ad1887f592..22ed10ac66503cb1a86d1b84c31c236af856bf94 100644
--- a/src/xrt/compositor/meson.build
+++ b/src/xrt/compositor/meson.build
@@ -6,7 +6,7 @@ subdir('shaders')
 comp_include = include_directories('.')
 
 # TODO: Dependency resolution and subsequent configuration could be improved
-compositor_deps = [aux, shaders, vulkan, xrt_config_vulkan]
+compositor_deps = [aux, shaders, vulkan, xrt_config_vulkan, xcb_randr]
 compositor_includes = [xrt_include]
 
 compositor_srcs = [
diff --git a/src/xrt/include/xrt/xrt_compositor.h b/src/xrt/include/xrt/xrt_compositor.h
index cb3b60558fbf8015f2237a5d9421032d95d8b304..de8347643dafe9a4ee5f860b61330d5bb6046ce9 100644
--- a/src/xrt/include/xrt/xrt_compositor.h
+++ b/src/xrt/include/xrt/xrt_compositor.h
@@ -69,6 +69,174 @@ enum xrt_view_type
 	XRT_VIEW_TYPE_STEREO = 2,
 };
 
+/*!
+ * Layer type.
+ *
+ * @ingroup xrt_iface
+ */
+enum xrt_layer_type
+{
+	XRT_LAYER_STEREO_PROJECTION,
+	XRT_LAYER_QUAD,
+};
+
+/*!
+ * Bit field for holding information about how a layer should be composited.
+ *
+ * @ingroup xrt_iface
+ */
+enum xrt_layer_composition_flags
+{
+	XRT_LAYER_COMPOSITION_CORRECT_CHROMATIC_ABERRATION_BIT = 1 << 0,
+	XRT_LAYER_COMPOSITION_BLEND_TEXTURE_SOURCE_ALPHA_BIT = 1 << 1,
+	XRT_LAYER_COMPOSITION_UNPREMULTIPLIED_ALPHA_BIT = 1 << 2,
+	/*!
+	 * The layer is locked to the device and the pose should only be
+	 * adjusted for the IPD.
+	 */
+	XRT_LAYER_COMPOSITION_VIEW_SPACE_BIT = 1 << 3,
+};
+
+/*!
+ * Which view is the layer visible to?
+ *
+ * Used for quad layers.
+ *
+ * @note Doesn't have the same values as the OpenXR counterpart!
+ *
+ * @ingroup xrt_iface
+ */
+enum xrt_layer_eye_visibility
+{
+	XRT_LAYER_EYE_VISIBILITY_NONE = 0x0,
+	XRT_LAYER_EYE_VISIBILITY_LEFT_BIT = 0x1,
+	XRT_LAYER_EYE_VISIBILITY_RIGHT_BIT = 0x2,
+	XRT_LAYER_EYE_VISIBILITY_BOTH = 0x3,
+};
+
+/*!
+ * Specifies a sub-image in a layer.
+ *
+ * @ingroup xrt_iface
+ */
+struct xrt_sub_image
+{
+	//! Image index in the (implicit) swapchain
+	uint32_t image_index;
+	//! Index in image array (for array textures)
+	uint32_t array_index;
+	//! The rectangle in the image to use
+	struct xrt_rect rect;
+};
+
+/*!
+ * All the pure data values associated with a quad layer.
+ *
+ * The @ref xrt_swapchain references and @ref xrt_device are provided outside of
+ * this struct.
+ *
+ * @ingroup xrt_iface
+ */
+struct xrt_layer_quad_data
+{
+	enum xrt_layer_eye_visibility visibility;
+
+	struct xrt_sub_image sub;
+
+	struct xrt_pose pose;
+	struct xrt_vec2 size;
+};
+
+/*!
+ * All of the pure data values associated with a single view in a projection
+ * layer.
+ *
+ * The @ref xrt_swapchain references and @ref xrt_device are provided outside of
+ * this struct.
+ *
+ * @ingroup xrt_iface
+ */
+struct xrt_layer_projection_view_data
+{
+	struct xrt_sub_image sub;
+
+	struct xrt_fov fov;
+	struct xrt_pose pose;
+};
+
+/*!
+ * All the pure data values associated with a stereo projection layer.
+ *
+ * The @ref xrt_swapchain references and @ref xrt_device are provided outside of
+ * this struct.
+ *
+ * @ingroup xrt_iface
+ */
+struct xrt_layer_stereo_projection_data
+{
+	struct xrt_layer_projection_view_data l, r;
+};
+
+/*!
+ * All the pure data values associated with a composition layer.
+ *
+ * The @ref xrt_swapchain references and @ref xrt_device are provided outside of
+ * this struct.
+ *
+ * @ingroup xrt_iface
+ */
+struct xrt_layer_data
+{
+	/*!
+	 * Tag for compositor layer type.
+	 */
+	enum xrt_layer_type type;
+
+	/*!
+	 * Often @ref XRT_INPUT_GENERIC_HEAD_POSE
+	 */
+	enum xrt_input_name name;
+
+	/*!
+	 * "Display no-earlier-than" timestamp for this layer.
+	 *
+	 * The layer may be displayed after this point, but must never be
+	 * displayed before.
+	 */
+	uint64_t timestamp;
+
+	/*!
+	 * Composition flags
+	 */
+	enum xrt_layer_composition_flags flags;
+
+	/*!
+	 * Whether the main compositor should flip the direction of y when
+	 * rendering.
+	 *
+	 * This is actually an input only to the "main" compositor
+	 * comp_compositor. It is overwritten by the various client
+	 * implementations of the @ref xrt_compositor interface depending on the
+	 * conventions of the associated graphics API. Other @ref
+	 * xrt_compositor_fd implementations that are not the main compositor
+	 * just pass this field along unchanged to the "real" compositor.
+	 */
+	bool flip_y;
+
+	/*!
+	 * Union of data values for the various layer types.
+	 *
+	 * The initialized member of this union should match the value of
+	 * xrt_layer_data::type. It also should be clear because of the layer
+	 * function called between xrt_compositor::layer_begin and
+	 * xrt_compositor::layer_commit where this data was passed.
+	 */
+	union {
+		struct xrt_layer_quad_data quad;
+		struct xrt_layer_stereo_projection_data stereo;
+	};
+};
+
 /*!
  * @interface xrt_swapchain
  * Common swapchain interface/base.
@@ -78,7 +246,9 @@ enum xrt_view_type
 struct xrt_swapchain
 {
 	/*!
-	 * Number of images, the images themselves are on the subclasses.
+	 * Number of images.
+	 *
+	 * The images themselves are on the subclasses.
 	 */
 	uint32_t num_images;
 
@@ -88,11 +258,19 @@ struct xrt_swapchain
 	void (*destroy)(struct xrt_swapchain *xsc);
 
 	/*!
-	 * See xrWaitSwapchainImage, must make sure that no image is acquired
-	 * before calling acquire_image.
+	 * Obtain the index of the next image to use, without blocking on being
+	 * able to write to it.
+	 *
+	 * See xrAcquireSwapchainImage.
+	 *
+	 * Caller must make sure that no image is acquired before calling
+	 * acquire_image.
+	 *
+	 * @param xsc Self pointer
+	 * @param[out] out_index Image index to use next
 	 */
 	xrt_result_t (*acquire_image)(struct xrt_swapchain *xsc,
-	                              uint32_t *index);
+	                              uint32_t *out_index);
 
 	/*!
 	 * See xrWaitSwapchainImage, state tracker needs to track index.
@@ -116,9 +294,9 @@ struct xrt_swapchain
  * @public @memberof xrt_swapchain
  */
 static inline xrt_result_t
-xrt_swapchain_acquire_image(struct xrt_swapchain *xsc, uint32_t *index)
+xrt_swapchain_acquire_image(struct xrt_swapchain *xsc, uint32_t *out_index)
 {
-	return xsc->acquire_image(xsc, index);
+	return xsc->acquire_image(xsc, out_index);
 }
 
 /*!
@@ -267,75 +445,30 @@ struct xrt_compositor
 	 * Adds a stereo projection layer for submissions.
 	 *
 	 * @param xc          Self pointer
-	 * @param timestamp     When should this layer be shown.
-	 * @param xdev          The device the layer is relative to.
-	 * @param name          Which pose this layer is relative to.
-	 * @param layer_flags   Flags for this layer, applied to both images.
-	 * @param l_sc          Left swapchain.
-	 * @param l_image_index Left image index as return by acquire_image.
-	 * @param l_rect        Left subimage rect.
-	 * @param l_array_index Left array index.
-	 * @param l_fov         Left fov the left projection rendered with.
-	 * @param l_pose        Left pose the left projection rendered with.
-	 * @param r_sc          Right swapchain.
-	 * @param r_image_index Right image index as return by acquire_image.
-	 * @param r_rect        Right subimage rect.
-	 * @param r_array_index Right array index.
-	 * @param r_fov         Right fov the left projection rendered with.
-	 * @param r_pose        Right pose the left projection rendered with.
-	 * @param flip_y        Flip Y texture coordinates.
+	 * @param xdev        The device the layer is relative to.
+	 * @param l_xsc       Left swapchain.
+	 * @param r_xsc       Right swapchain.
+	 * @param data        All of the pure data bits.
 	 */
-	xrt_result_t (*layer_stereo_projection)(
-	    struct xrt_compositor *xc,
-	    uint64_t timestamp,
-	    struct xrt_device *xdev,
-	    enum xrt_input_name name,
-	    enum xrt_layer_composition_flags layer_flags,
-	    struct xrt_swapchain *l_sc,
-	    uint32_t l_image_index,
-	    struct xrt_rect *l_rect,
-	    uint32_t l_array_index,
-	    struct xrt_fov *l_fov,
-	    struct xrt_pose *l_pose,
-	    struct xrt_swapchain *r_sc,
-	    uint32_t r_image_index,
-	    struct xrt_rect *r_rect,
-	    uint32_t r_array_index,
-	    struct xrt_fov *r_fov,
-	    struct xrt_pose *r_pose,
-	    bool flip_y);
+	xrt_result_t (*layer_stereo_projection)(struct xrt_compositor *xc,
+	                                        struct xrt_device *xdev,
+	                                        struct xrt_swapchain *l_xsc,
+	                                        struct xrt_swapchain *r_xsc,
+	                                        struct xrt_layer_data *data);
 
 	/*!
 	 * Adds a quad layer for submission, the center of the quad is specified
 	 * by the pose and extends outwards from it.
 	 *
 	 * @param xc          Self pointer
-	 * @param timestamp   When should this layer be shown.
 	 * @param xdev        The device the layer is relative to.
-	 * @param name        Which pose this layer is relative to.
-	 * @param layer_flags Flags for this layer.
-	 * @param visibility  Which views are is this layer visible in.
-	 * @param sc          Swapchain.
-	 * @param image_index Image index as return by acquire_image.
-	 * @param rect        Subimage rect.
-	 * @param array_index Array index.
-	 * @param pose        Pose the left projection rendered with.
-	 * @param size        Size of the quad in meters.
-	 * @param flip_y      Flip Y texture coordinates.
+	 * @param xsc         Swapchain.
+	 * @param data        All of the pure data bits.
 	 */
 	xrt_result_t (*layer_quad)(struct xrt_compositor *xc,
-	                           uint64_t timestamp,
 	                           struct xrt_device *xdev,
-	                           enum xrt_input_name name,
-	                           enum xrt_layer_composition_flags layer_flags,
-	                           enum xrt_layer_eye_visibility visibility,
-	                           struct xrt_swapchain *sc,
-	                           uint32_t image_index,
-	                           struct xrt_rect *rect,
-	                           uint32_t array_index,
-	                           struct xrt_pose *pose,
-	                           struct xrt_vec2 *size,
-	                           bool flip_y);
+	                           struct xrt_swapchain *xsc,
+	                           struct xrt_layer_data *data);
 
 	/*!
 	 * Commits all of the submitted layers, it's from this on that the
@@ -493,28 +626,12 @@ xrt_comp_layer_begin(struct xrt_compositor *xc,
  */
 static inline xrt_result_t
 xrt_comp_layer_stereo_projection(struct xrt_compositor *xc,
-                                 uint64_t timestamp,
                                  struct xrt_device *xdev,
-                                 enum xrt_input_name name,
-                                 enum xrt_layer_composition_flags layer_flags,
-                                 struct xrt_swapchain *l_sc,
-                                 uint32_t l_image_index,
-                                 struct xrt_rect *l_rect,
-                                 uint32_t l_array_index,
-                                 struct xrt_fov *l_fov,
-                                 struct xrt_pose *l_pose,
-                                 struct xrt_swapchain *r_sc,
-                                 uint32_t r_image_index,
-                                 struct xrt_rect *r_rect,
-                                 uint32_t r_array_index,
-                                 struct xrt_fov *r_fov,
-                                 struct xrt_pose *r_pose,
-                                 bool flip_y)
-{
-	return xc->layer_stereo_projection(
-	    xc, timestamp, xdev, name, layer_flags, l_sc, l_image_index, l_rect,
-	    l_array_index, l_fov, l_pose, r_sc, r_image_index, r_rect,
-	    r_array_index, r_fov, r_pose, flip_y);
+                                 struct xrt_swapchain *l_xsc,
+                                 struct xrt_swapchain *r_xsc,
+                                 struct xrt_layer_data *data)
+{
+	return xc->layer_stereo_projection(xc, xdev, l_xsc, r_xsc, data);
 }
 
 /*!
@@ -526,22 +643,11 @@ xrt_comp_layer_stereo_projection(struct xrt_compositor *xc,
  */
 static inline xrt_result_t
 xrt_comp_layer_quad(struct xrt_compositor *xc,
-                    uint64_t timestamp,
                     struct xrt_device *xdev,
-                    enum xrt_input_name name,
-                    enum xrt_layer_composition_flags layer_flags,
-                    enum xrt_layer_eye_visibility visibility,
-                    struct xrt_swapchain *sc,
-                    uint32_t image_index,
-                    struct xrt_rect *rect,
-                    uint32_t array_index,
-                    struct xrt_pose *pose,
-                    struct xrt_vec2 *size,
-                    bool flip_y)
-{
-	return xc->layer_quad(xc, timestamp, xdev, name, layer_flags,
-	                      visibility, sc, image_index, rect, array_index,
-	                      pose, size, flip_y);
+                    struct xrt_swapchain *xsc,
+                    struct xrt_layer_data *data)
+{
+	return xc->layer_quad(xc, xdev, xsc, data);
 }
 
 /*!
@@ -643,6 +749,7 @@ xrt_compositor_gl(struct xrt_compositor *xc)
  *
  */
 
+typedef struct VkCommandBuffer_T *VkCommandBuffer;
 #ifdef XRT_64_BIT
 typedef struct VkImage_T *VkImage;
 typedef struct VkDeviceMemory_T *VkDeviceMemory;
@@ -664,6 +771,10 @@ struct xrt_swapchain_vk
 
 	VkImage images[XRT_MAX_SWAPCHAIN_IMAGES];
 	VkDeviceMemory mems[XRT_MAX_SWAPCHAIN_IMAGES];
+
+	// Prerecorded swapchain image ownership/layout transition barriers
+	VkCommandBuffer acquire[XRT_MAX_SWAPCHAIN_IMAGES];
+	VkCommandBuffer release[XRT_MAX_SWAPCHAIN_IMAGES];
 };
 
 /*!
diff --git a/src/xrt/include/xrt/xrt_defines.h b/src/xrt/include/xrt/xrt_defines.h
index 3906f42c50030f99cde7e7d0d79f05a1eb98158f..60773d7f263931bc5e552641d855ef70a72f46e9 100644
--- a/src/xrt/include/xrt/xrt_defines.h
+++ b/src/xrt/include/xrt/xrt_defines.h
@@ -155,32 +155,6 @@ enum xrt_stereo_format
 	XRT_STEREO_FORMAT_OAU,         //!< Over & Under.
 };
 
-/*!
- * Bit field for holding information about how a layer should be composited.
- *
- * @ingroup xrt_iface
- */
-enum xrt_layer_composition_flags
-{
-	XRT_LAYER_COMPOSITION_CORRECT_CHROMATIC_ABERRATION_BIT = 1 << 0,
-	XRT_LAYER_COMPOSITION_BLEND_TEXTURE_SOURCE_ALPHA_BIT = 1 << 1,
-	XRT_LAYER_COMPOSITION_UNPREMULTIPLIED_ALPHA_BIT = 1 << 2,
-};
-
-/*!
- * Which view is they layer visible to, used for quad layers. Doesn't have the
- * same values as the OpenXR counter-parts.
- *
- * @ingroup xrt_iface
- */
-enum xrt_layer_eye_visibility
-{
-	XRT_LAYER_EYE_VISIBILITY_NONE = 0x0,
-	XRT_LAYER_EYE_VISIBILITY_LEFT_BIT = 0x1,
-	XRT_LAYER_EYE_VISIBILITY_RIGHT_BIT = 0x2,
-	XRT_LAYER_EYE_VISIBILITY_BOTH = 0x3,
-};
-
 /*!
  * A quaternion with single floats.
  *
@@ -517,8 +491,8 @@ enum xrt_input_type
  *
  * @param name A xrt_input_name value
  *
- * @see xrt_input_name
- * @see xrt_input_type
+ * @relates xrt_input_name
+ * @returns @ref xrt_input_type
  * @ingroup xrt_iface
  */
 #define XRT_GET_INPUT_TYPE(name) (name & 0xff)
@@ -610,11 +584,9 @@ enum xrt_input_name
 union xrt_input_value {
 	struct xrt_vec1 vec1;
 	struct xrt_vec2 vec2;
-	struct xrt_vec3 vec3;
 	bool boolean;
 };
 
-
 /*!
  * Base type of this output.
  *
diff --git a/src/xrt/include/xrt/xrt_results.h b/src/xrt/include/xrt/xrt_results.h
index f9fc6d17b8089aeb1a94764b86ec961a7367da6f..6818f935a040f23ee2c7a0b2a197d7d694d4face 100644
--- a/src/xrt/include/xrt/xrt_results.h
+++ b/src/xrt/include/xrt/xrt_results.h
@@ -13,5 +13,6 @@ typedef enum xrt_result
 {
 	XRT_SUCCESS = 0,
 	XRT_ERROR_IPC_FAILURE = -1,
-	XRT_ERROR_NO_IMAGE_AVAILABLE = -2
+	XRT_ERROR_NO_IMAGE_AVAILABLE = -2,
+	XRT_ERROR_FAILED_TO_SUBMIT_VULKAN_COMMANDS = -3,
 } xrt_result_t;
diff --git a/src/xrt/ipc/ipc_client_compositor.c b/src/xrt/ipc/ipc_client_compositor.c
index 5d469e4eecf8a567c7749eec71fd8371676d359b..5f2680b7f069315e3d3286a05b84c7cf86157ec2 100644
--- a/src/xrt/ipc/ipc_client_compositor.c
+++ b/src/xrt/ipc/ipc_client_compositor.c
@@ -363,55 +363,24 @@ ipc_compositor_layer_begin(struct xrt_compositor *xc,
 }
 
 static xrt_result_t
-ipc_compositor_layer_stereo_projection(
-    struct xrt_compositor *xc,
-    uint64_t timestamp,
-    struct xrt_device *xdev,
-    enum xrt_input_name name,
-    enum xrt_layer_composition_flags layer_flags,
-    struct xrt_swapchain *l_sc,
-    uint32_t l_image_index,
-    struct xrt_rect *l_rect,
-    uint32_t l_array_index,
-    struct xrt_fov *l_fov,
-    struct xrt_pose *l_pose,
-    struct xrt_swapchain *r_sc,
-    uint32_t r_image_index,
-    struct xrt_rect *r_rect,
-    uint32_t r_array_index,
-    struct xrt_fov *r_fov,
-    struct xrt_pose *r_pose,
-    bool flip_y)
+ipc_compositor_layer_stereo_projection(struct xrt_compositor *xc,
+                                       struct xrt_device *xdev,
+                                       struct xrt_swapchain *l_xsc,
+                                       struct xrt_swapchain *r_xsc,
+                                       struct xrt_layer_data *data)
 {
 	struct ipc_client_compositor *icc = ipc_client_compositor(xc);
 
 	struct ipc_shared_memory *ism = icc->ipc_c->ism;
 	struct ipc_layer_slot *slot = &ism->slots[icc->layers.slot_id];
 	struct ipc_layer_entry *layer = &slot->layers[icc->layers.num_layers];
-	struct ipc_layer_stereo_projection *stereo = &layer->stereo;
-	struct ipc_client_swapchain *l = ipc_client_swapchain(l_sc);
-	struct ipc_client_swapchain *r = ipc_client_swapchain(r_sc);
-
-	stereo->timestamp = timestamp;
-	stereo->xdev_id = 0; //! @todo Real id.
-	stereo->name = name;
-	stereo->layer_flags = layer_flags;
-	stereo->l.swapchain_id = l->id;
-	stereo->l.image_index = l_image_index;
-	stereo->l.rect = *l_rect;
-	stereo->l.array_index = l_array_index;
-	stereo->l.fov = *l_fov;
-	stereo->l.pose = *l_pose;
-	stereo->r.swapchain_id = r->id;
-	stereo->r.image_index = r_image_index;
-	stereo->r.rect = *r_rect;
-	stereo->r.array_index = r_array_index;
-	stereo->r.fov = *r_fov;
-	stereo->r.pose = *r_pose;
-
-	layer->flip_y = flip_y;
-
-	layer->type = IPC_LAYER_STEREO_PROJECTION;
+	struct ipc_client_swapchain *l = ipc_client_swapchain(l_xsc);
+	struct ipc_client_swapchain *r = ipc_client_swapchain(r_xsc);
+
+	layer->xdev_id = 0; //! @todo Real id.
+	layer->swapchain_ids[0] = l->id;
+	layer->swapchain_ids[1] = r->id;
+	layer->data = *data;
 
 	// Increment the number of layers.
 	icc->layers.num_layers++;
@@ -421,40 +390,23 @@ ipc_compositor_layer_stereo_projection(
 
 static xrt_result_t
 ipc_compositor_layer_quad(struct xrt_compositor *xc,
-                          uint64_t timestamp,
                           struct xrt_device *xdev,
-                          enum xrt_input_name name,
-                          enum xrt_layer_composition_flags layer_flags,
-                          enum xrt_layer_eye_visibility visibility,
-                          struct xrt_swapchain *sc,
-                          uint32_t image_index,
-                          struct xrt_rect *rect,
-                          uint32_t array_index,
-                          struct xrt_pose *pose,
-                          struct xrt_vec2 *size,
-                          bool flip_y)
+                          struct xrt_swapchain *xsc,
+                          struct xrt_layer_data *data)
 {
 	struct ipc_client_compositor *icc = ipc_client_compositor(xc);
 
 	struct ipc_shared_memory *ism = icc->ipc_c->ism;
 	struct ipc_layer_slot *slot = &ism->slots[icc->layers.slot_id];
 	struct ipc_layer_entry *layer = &slot->layers[icc->layers.num_layers];
-	struct ipc_layer_quad *quad = &layer->quad;
-	struct ipc_client_swapchain *ics = ipc_client_swapchain(sc);
-
-	quad->timestamp = timestamp;
-	quad->xdev_id = 0; //! @todo Real id.
-	quad->name = name;
-	quad->layer_flags = layer_flags;
-	quad->swapchain_id = ics->id;
-	quad->image_index = image_index;
-	quad->rect = *rect;
-	quad->array_index = array_index;
-	quad->pose = *pose;
-	quad->size = *size;
-
-	layer->flip_y = flip_y;
-	layer->type = IPC_LAYER_QUAD;
+	struct ipc_client_swapchain *ics = ipc_client_swapchain(xsc);
+
+	assert(data->type == XRT_LAYER_QUAD);
+
+	layer->xdev_id = 0; //! @todo Real id.
+	layer->swapchain_ids[0] = ics->id;
+	layer->swapchain_ids[1] = -1;
+	layer->data = *data;
 
 	// Increment the number of layers.
 	icc->layers.num_layers++;
diff --git a/src/xrt/ipc/ipc_protocol.h b/src/xrt/ipc/ipc_protocol.h
index f955f16bde4fcee2afb9201d7b449cb256a08590..3ef5bdeef407de06edcd14100537c938a0a1f117 100644
--- a/src/xrt/ipc/ipc_protocol.h
+++ b/src/xrt/ipc/ipc_protocol.h
@@ -78,58 +78,37 @@ struct ipc_shared_device
 	uint32_t first_output_index;
 };
 
-struct ipc_layer_stereo_projection
-{
-	uint64_t timestamp;
-
-	uint32_t xdev_id;
-	enum xrt_input_name name;
-	enum xrt_layer_composition_flags layer_flags;
-
-	struct
-	{
-		uint32_t swapchain_id;
-		uint32_t image_index;
-		struct xrt_rect rect;
-		uint32_t array_index;
-		struct xrt_fov fov;
-		struct xrt_pose pose;
-	} l, r;
-};
-
-struct ipc_layer_quad
-{
-	uint64_t timestamp;
-
-	uint32_t xdev_id;
-	enum xrt_input_name name;
-	enum xrt_layer_composition_flags layer_flags;
-
-	uint32_t swapchain_id;
-	uint32_t image_index;
-	struct xrt_rect rect;
-	uint32_t array_index;
-	struct xrt_pose pose;
-	struct xrt_vec2 size;
-};
-
-enum ipc_layer_type
-{
-	IPC_LAYER_STEREO_PROJECTION,
-	IPC_LAYER_QUAD,
-};
-
+/*!
+ * Data for a single composition layer.
+ *
+ * Similar in function to @ref comp_layer
+ *
+ * @ingroup ipc
+ */
 struct ipc_layer_entry
 {
-	enum ipc_layer_type type;
-	bool flip_y;
+	//! @todo what is this used for?
+	uint32_t xdev_id;
 
-	union {
-		struct ipc_layer_quad quad;
-		struct ipc_layer_stereo_projection stereo;
-	};
+	/*!
+	 * Up to two indices of swapchains to use.
+	 *
+	 * How many are actually used depends on the value of @p data.type
+	 */
+	uint32_t swapchain_ids[2];
+
+	/*!
+	 * All basic (trivially-serializable) data associated with a layer,
+	 * aside from which swapchain(s) are used.
+	 */
+	struct xrt_layer_data data;
 };
 
+/*!
+ * Render state for a single client, including all layers.
+ *
+ * @ingroup ipc
+ */
 struct ipc_layer_slot
 {
 	enum xrt_blend_mode env_blend_mode;
@@ -215,7 +194,7 @@ struct ipc_compositor_state
 
 /*
  *
- * Reset of protocol is generated.
+ * Rest of protocol is generated.
  *
  */
 
diff --git a/src/xrt/ipc/ipc_server.h b/src/xrt/ipc/ipc_server.h
index d5613646815467b1aa9acd8c70c934ce72c4b1c2..e30b1e081b605fa43f6560f73cc9fb993804b3a2 100644
--- a/src/xrt/ipc/ipc_server.h
+++ b/src/xrt/ipc/ipc_server.h
@@ -85,50 +85,6 @@ struct ipc_swapchain_data
 	bool active;
 };
 
-struct ipc_quad_render_state
-{
-	uint32_t swapchain_index;
-	uint32_t image_index;
-	uint32_t array_index;
-
-	struct xrt_pose pose;
-	struct xrt_vec2 size;
-};
-
-struct ipc_stereo_projection_render_state
-{
-	struct
-	{
-		uint32_t swapchain_index;
-		uint32_t image_index;
-		uint32_t array_index;
-	} l, r;
-};
-
-struct ipc_layer_render_state
-{
-	enum ipc_layer_type type;
-	bool flip_y;
-
-	union {
-		struct ipc_quad_render_state quad;
-		struct ipc_stereo_projection_render_state stereo;
-	};
-};
-
-/*!
- * Render state for a client.
- *
- * @ingroup ipc_server
- */
-struct ipc_render_state
-{
-	bool rendering;
-	enum xrt_blend_mode env_blend_mode;
-	uint32_t num_layers;
-	struct ipc_layer_render_state layers[IPC_MAX_LAYERS];
-	uint32_t slot_id;
-};
 
 struct ipc_queued_event
 {
@@ -163,7 +119,10 @@ struct ipc_client_state
 	int ipc_socket_fd;
 
 	//! State for rendering.
-	struct ipc_render_state render_state;
+	struct ipc_layer_slot render_state;
+
+	//! Whether we are currently rendering @ref render_state
+	bool rendering_state;
 
 	struct xrt_client_state client_state;
 	struct ipc_queued_event queued_events[IPC_EVENT_QUEUE_SIZE];
@@ -252,6 +211,8 @@ ipc_server_client_thread(void *_cs);
  * Create a single wait thread.
  *
  * @ingroup ipc_server
+ * @public @memberof ipc_server
+ * @relatesalso ipc_wait
  */
 int
 ipc_server_wait_alloc(struct ipc_server *s, struct ipc_wait **out_iw);
@@ -260,6 +221,7 @@ ipc_server_wait_alloc(struct ipc_server *s, struct ipc_wait **out_iw);
  * Destroy a wait thread, checks for NULL and sets to NULL.
  *
  * @ingroup ipc_server
+ * @public @memberof ipc_wait
  */
 void
 ipc_server_wait_free(struct ipc_wait **out_iw);
@@ -269,11 +231,21 @@ ipc_server_wait_free(struct ipc_wait **out_iw);
  * wait frame.
  *
  * @ingroup ipc_server
+ * @public @memberof ipc_wait
  */
 void
 ipc_server_wait_add_frame(struct ipc_wait *iw,
                           volatile struct ipc_client_state *cs);
 
+/*!
+ * Reset the wait state for wait frame, after the client disconnected
+ *
+ * @ingroup ipc_server
+ * @public @memberof ipc_wait
+ */
+void
+ipc_server_wait_reset_client(struct ipc_wait *iw,
+                             volatile struct ipc_client_state *cs);
 
 #ifdef __cplusplus
 }
diff --git a/src/xrt/ipc/ipc_server_client.c b/src/xrt/ipc/ipc_server_client.c
index e52ed3e64c50fd43291c54dfcadda7568acd60f0..6a21f1bd97ef358577d6427ac3f6f06c1e07d38d 100644
--- a/src/xrt/ipc/ipc_server_client.c
+++ b/src/xrt/ipc/ipc_server_client.c
@@ -122,40 +122,9 @@ ipc_handle_compositor_layer_sync(volatile struct ipc_client_state *ics,
 	struct ipc_shared_memory *ism = ics->server->ism;
 	struct ipc_layer_slot *slot = &ism->slots[slot_id];
 
-	for (uint32_t i = 0; i < slot->num_layers; i++) {
-		struct ipc_layer_render_state *irs =
-		    &ics->render_state.layers[i];
-		struct ipc_layer_entry *sl = &slot->layers[i];
-
-		irs->type = sl->type;
-		irs->flip_y = sl->flip_y;
-
-		switch (irs->type) {
-		case IPC_LAYER_STEREO_PROJECTION:
-			irs->stereo.l.swapchain_index =
-			    sl->stereo.l.swapchain_id;
-			irs->stereo.l.image_index = sl->stereo.l.image_index;
-			irs->stereo.r.swapchain_index =
-			    sl->stereo.r.swapchain_id;
-			irs->stereo.r.image_index = sl->stereo.r.image_index;
-			irs->stereo.l.array_index = sl->stereo.l.array_index;
-			irs->stereo.r.array_index = sl->stereo.r.array_index;
-
-			break;
-		case IPC_LAYER_QUAD:
-			irs->quad.swapchain_index = sl->quad.swapchain_id;
-			irs->quad.image_index = sl->quad.image_index;
-			irs->quad.pose = sl->quad.pose;
-			irs->quad.size = sl->quad.size;
-			irs->quad.array_index = sl->quad.array_index;
-			break;
-		}
-	}
-
-	ics->render_state.num_layers = slot->num_layers;
-	ics->render_state.slot_id = slot_id;
-	ics->render_state.rendering = true;
-
+	// Copy current slot data to our state.
+	ics->render_state = *slot;
+	ics->rendering_state = true;
 
 	os_mutex_lock(&ics->server->global_state_lock);
 	*out_free_slot_id =
@@ -577,19 +546,24 @@ client_loop(volatile struct ipc_client_state *ics)
 	ics->num_swapchains = 0;
 
 	// Make sure to reset the renderstate fully.
+	ics->rendering_state = false;
 	ics->render_state.num_layers = 0;
-	ics->render_state.rendering = false;
 	for (uint32_t i = 0; i < ARRAY_SIZE(ics->render_state.layers); ++i) {
-		volatile struct ipc_layer_render_state *lrs =
+		volatile struct ipc_layer_entry *rl =
 		    &ics->render_state.layers[i];
 
-		lrs->flip_y = false;
-		lrs->stereo.l.swapchain_index = 0;
-		lrs->stereo.l.image_index = 0;
-		lrs->stereo.r.swapchain_index = 0;
-		lrs->stereo.r.image_index = 0;
-		lrs->quad.swapchain_index = 0;
-		lrs->quad.image_index = 0;
+		rl->swapchain_ids[0] = 0;
+		rl->swapchain_ids[1] = 0;
+		rl->data.flip_y = false;
+		/*!
+		 * @todo this is redundant, we're setting both elements of a
+		 * union. Why? Can we just zero the whole render_state?
+		 */
+		rl->data.stereo.l.sub.image_index = 0;
+		rl->data.stereo.r.sub.image_index = 0;
+		rl->data.quad.sub.image_index = 0;
+
+		//! @todo set rects or array index?
 	}
 
 	// Destroy all swapchains now.
@@ -601,6 +575,8 @@ client_loop(volatile struct ipc_client_state *ics)
 	if (ics->server->exit_on_disconnect) {
 		ics->server->running = false;
 	}
+
+	ipc_server_wait_reset_client(ics->server->iw, ics);
 }
 
 
diff --git a/src/xrt/ipc/ipc_server_process.c b/src/xrt/ipc/ipc_server_process.c
index a4c06b0e94e41e5d1b52f11d01a07bf2a20bab68..8db940b8c291f3779166edab96c1b2ed7caf2758 100644
--- a/src/xrt/ipc/ipc_server_process.c
+++ b/src/xrt/ipc/ipc_server_process.c
@@ -594,11 +594,13 @@ send_client_state(struct ipc_client_state *ics)
 static bool
 _update_projection_layer(struct comp_compositor *c,
                          volatile struct ipc_client_state *active_client,
-                         volatile struct ipc_layer_render_state *layer,
+                         volatile struct ipc_layer_entry *layer,
                          uint32_t i)
 {
-	uint32_t lsi = layer->stereo.l.swapchain_index;
-	uint32_t rsi = layer->stereo.r.swapchain_index;
+	// left
+	uint32_t lsi = layer->swapchain_ids[0];
+	// right
+	uint32_t rsi = layer->swapchain_ids[1];
 
 	if (active_client->xscs[lsi] == NULL ||
 	    active_client->xscs[rsi] == NULL) {
@@ -612,12 +614,15 @@ _update_projection_layer(struct comp_compositor *c,
 
 	struct comp_swapchain_image *l = NULL;
 	struct comp_swapchain_image *r = NULL;
-	l = &cl->images[layer->stereo.l.image_index];
-	r = &cr->images[layer->stereo.r.image_index];
+	l = &cl->images[layer->data.stereo.l.sub.image_index];
+	r = &cr->images[layer->data.stereo.r.sub.image_index];
 
-	comp_renderer_set_projection_layer(c->r, l, r, layer->flip_y, i,
-	                                   layer->stereo.l.array_index,
-	                                   layer->stereo.r.array_index);
+
+	// Cast away volatile.
+	struct xrt_layer_data *data = (struct xrt_layer_data *)&layer->data;
+
+	//! @todo we are ignoring subrect here!
+	comp_renderer_set_projection_layer(c->r, i, l, r, data);
 
 	return true;
 }
@@ -625,10 +630,10 @@ _update_projection_layer(struct comp_compositor *c,
 static bool
 _update_quad_layer(struct comp_compositor *c,
                    volatile struct ipc_client_state *active_client,
-                   volatile struct ipc_layer_render_state *layer,
+                   volatile struct ipc_layer_entry *layer,
                    uint32_t i)
 {
-	uint32_t sci = layer->quad.swapchain_index;
+	uint32_t sci = layer->swapchain_ids[0];
 
 	if (active_client->xscs[sci] == NULL) {
 		fprintf(stderr, "ERROR: Invalid swap chain for quad layer.\n");
@@ -637,13 +642,13 @@ _update_quad_layer(struct comp_compositor *c,
 
 	struct comp_swapchain *sc = comp_swapchain(active_client->xscs[sci]);
 	struct comp_swapchain_image *image = NULL;
-	image = &sc->images[layer->quad.image_index];
+	image = &sc->images[layer->data.quad.sub.image_index];
 
-	struct xrt_pose pose = layer->quad.pose;
-	struct xrt_vec2 size = layer->quad.size;
+	// Cast away volatile.
+	struct xrt_layer_data *data = (struct xrt_layer_data *)&layer->data;
 
-	comp_renderer_set_quad_layer(c->r, image, &pose, &size, layer->flip_y,
-	                             i, layer->quad.array_index);
+	//! @todo we are ignoring subrect here!
+	comp_renderer_set_quad_layer(c->r, i, image, data);
 
 	return true;
 }
@@ -721,16 +726,16 @@ _update_layers(struct comp_compositor *c,
 			for (uint32_t j = 0; j < ics->render_state.num_layers;
 			     j++) {
 
-				volatile struct ipc_layer_render_state *layer =
+				volatile struct ipc_layer_entry *layer =
 				    &ics->render_state.layers[j];
-				switch (layer->type) {
-				case IPC_LAYER_STEREO_PROJECTION: {
+				switch (layer->data.type) {
+				case XRT_LAYER_STEREO_PROJECTION: {
 					_update_projection_layer(c, ics, layer,
 					                         layer_id);
 					layer_id++;
 					break;
 				}
-				case IPC_LAYER_QUAD: {
+				case XRT_LAYER_QUAD: {
 					_update_quad_layer(c, ics, layer,
 					                   layer_id);
 					layer_id++;
@@ -751,8 +756,8 @@ main_loop(struct ipc_server *s)
 	struct xrt_compositor *xc = s->xc;
 	struct comp_compositor *c = comp_compositor(xc);
 
-	// make sure all our client connections have a handle to the compositor
-	// and consistent initial state
+	// make sure all our client connections have a handle to the
+	// compositor and consistent initial state
 
 	uint32_t num_layers = 0;
 
@@ -791,21 +796,18 @@ main_loop(struct ipc_server *s)
 			// swapchain indices and toggle wait to false
 			// when the client calls end_frame, signalling
 			// us to render.
-			volatile struct ipc_render_state *render_state =
-			    &active_client->render_state;
-
-			if (render_state->rendering) {
 
+			if (active_client->rendering_state) {
 				if (!_update_layers(c, s, &num_layers))
 					continue;
 				// set our client state back to waiting.
-				render_state->rendering = false;
+				active_client->rendering_state = false;
 			}
 		}
 
 		comp_renderer_draw(c->r);
-		// we should release any slots that are not used by a rendering
-		// client
+		// we should release any slots that are not used by a
+		// rendering client
 
 
 		// Now is a good time to destroy objects.
@@ -848,8 +850,8 @@ handle_focused_client_events(struct ipc_client_state *ics,
                              int prev_active_id)
 {
 
-	// if our prev active id is -1 and our cur active id is -1, we can bail
-	// out early
+	// if our prev active id is -1 and our cur active id is -1, we
+	// can bail out early
 
 	if (active_id == -1 && prev_active_id == -1) {
 		return;
@@ -868,7 +870,8 @@ handle_focused_client_events(struct ipc_client_state *ics,
 			ics->client_state.session_visible = true;
 		}
 
-		// set visible + focused if we are the primary application
+		// set visible + focused if we are the primary
+		// application
 		if (ics->server_thread_index == active_id) {
 			ics->client_state.session_visible = true;
 			ics->client_state.session_focused = true;
@@ -877,7 +880,8 @@ handle_focused_client_events(struct ipc_client_state *ics,
 		return;
 	}
 
-	// no primary application, set all overlays to synchronised state
+	// no primary application, set all overlays to synchronised
+	// state
 	if (ics->client_state.session_overlay) {
 		ics->client_state.session_focused = false;
 		ics->client_state.session_visible = false;
@@ -914,8 +918,8 @@ void
 update_server_state(struct ipc_server *s)
 {
 
-	pthread_mutex_lock(&s->global_state_lock); // multiple threads could
-	                                           // call this at
+	pthread_mutex_lock(&s->global_state_lock); // multiple threads
+	                                           // could call this at
 	                                           // the same time
 
 	// if our client that is set to active is still active,
@@ -957,8 +961,9 @@ update_server_state(struct ipc_server *s)
 		}
 	}
 
-	// if our currentlly-set active primary application is not actually
-	// active/displayable, use the fallback application instead.
+	// if our currently-set active primary application is not
+	// actually active/displayable, use the fallback application
+	// instead.
 	struct ipc_client_state *ics = &s->thread_state[s->active_client_index];
 	if (!(ics->client_state.session_overlay == false &&
 	      s->active_client_index >= 0 &&
diff --git a/src/xrt/ipc/ipc_server_wait.c b/src/xrt/ipc/ipc_server_wait.c
index 25a565b49561acfe1b6afd2c19b964e057b4f244..aa7bc0d6cd3ce8d9d5620a9aa39b82272da89182 100644
--- a/src/xrt/ipc/ipc_server_wait.c
+++ b/src/xrt/ipc/ipc_server_wait.c
@@ -114,6 +114,28 @@ ipc_server_wait_add_frame(struct ipc_wait *iw,
 	os_thread_helper_unlock(&iw->oth);
 }
 
+void
+ipc_server_wait_reset_client(struct ipc_wait *iw,
+                             volatile struct ipc_client_state *cs)
+{
+	os_thread_helper_lock(&iw->oth);
+
+	/* ipc_server_wait_add_frame would overwrite dangling references,
+	 * but clean them up anyway to be less confusing. */
+	for (int i = 0; i < IPC_MAX_CLIENTS; i++) {
+		if (iw->cs[i] == cs) {
+			iw->cs[i] = NULL;
+		}
+	}
+
+	volatile struct ipc_shared_memory *ism = iw->s->ism;
+	sem_init((sem_t *)&ism->wait_frame.sem, true, 0);
+	ism->wait_frame.predicted_display_period = 0;
+	ism->wait_frame.predicted_display_time = 0;
+
+	os_thread_helper_unlock(&iw->oth);
+}
+
 void
 ipc_server_wait_free(struct ipc_wait **out_iw)
 {
diff --git a/src/xrt/state_trackers/gui/CMakeLists.txt b/src/xrt/state_trackers/gui/CMakeLists.txt
index 863a76f8d1ae5b7e2814cbabe6b74ab775e238be..ca119247c776c894c454abe04e1716693386e02e 100644
--- a/src/xrt/state_trackers/gui/CMakeLists.txt
+++ b/src/xrt/state_trackers/gui/CMakeLists.txt
@@ -1,6 +1,9 @@
 # Copyright 2019-2020, Collabora, Ltd.
 # SPDX-License-Identifier: BSL-1.0
 
+# c-imgui doesn't do well with IPO - lots of warnings.
+set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF)
+
 set(GUI_SOURCE_FILES
 	gui_common.h
 	gui_imgui.h
diff --git a/src/xrt/state_trackers/oxr/CMakeLists.txt b/src/xrt/state_trackers/oxr/CMakeLists.txt
index 6540cd4bfcdb0222e1e7937f8dbbb3e4669ae2c6..9559c0d6b983758eb2002f139618cd4d79e02763 100644
--- a/src/xrt/state_trackers/oxr/CMakeLists.txt
+++ b/src/xrt/state_trackers/oxr/CMakeLists.txt
@@ -1,7 +1,32 @@
 # Copyright 2019-2020, Collabora, Ltd.
 # SPDX-License-Identifier: BSL-1.0
 
+
+###
+# Binding generation
+#
+
+function(bindings_gen output)
+	add_custom_command(OUTPUT ${output}
+		COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/bindings.py
+			${CMAKE_CURRENT_SOURCE_DIR}/bindings.json
+			${output}
+		DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/bindings.py
+			${CMAKE_CURRENT_SOURCE_DIR}/bindings.json
+			)
+endfunction(bindings_gen)
+
+bindings_gen(${CMAKE_CURRENT_BINARY_DIR}/oxr_generated_bindings.h)
+bindings_gen(${CMAKE_CURRENT_BINARY_DIR}/oxr_generated_bindings.c)
+
+
+###
+# Main code
+#
+
 set(OXR_SOURCE_FILES
+	${CMAKE_CURRENT_BINARY_DIR}/oxr_generated_bindings.h
+	${CMAKE_CURRENT_BINARY_DIR}/oxr_generated_bindings.c
 	oxr_api_action.c
 	oxr_api_funcs.h
 	oxr_api_instance.c
@@ -52,4 +77,14 @@ if(XRT_HAVE_EGL)
 endif()
 
 add_library(st_oxr STATIC ${OXR_SOURCE_FILES})
-target_link_libraries(st_oxr PRIVATE xrt-interfaces xrt-external-openxr aux_util aux_math Vulkan::Vulkan comp_client)
+target_link_libraries(st_oxr PRIVATE
+	xrt-interfaces
+	xrt-external-openxr
+	aux_util
+	aux_math
+	Vulkan::Vulkan
+	comp_client
+	)
+target_include_directories(st_oxr PRIVATE
+	${CMAKE_CURRENT_BINARY_DIR}
+	)
diff --git a/src/xrt/state_trackers/oxr/bindings.json b/src/xrt/state_trackers/oxr/bindings.json
new file mode 100644
index 0000000000000000000000000000000000000000..93647e9c8502f0c73fc866c1b0416ca5f6dbfd2d
--- /dev/null
+++ b/src/xrt/state_trackers/oxr/bindings.json
@@ -0,0 +1,245 @@
+{
+	"/interaction_profiles/khr/simple_controller": {
+		"title": "Khronos Simple Controller",
+		"user_paths": [
+			"/user/hand/left",
+			"/user/hand/right"
+		],
+		"components": [
+			{"subpath": "/input/select/click"},
+			{"subpath": "/input/menu/click"},
+			{"subpath": "/input/grip/pose"},
+			{"subpath": "/input/aim/pose"},
+			{"subpath": "/output/haptic"}
+		]
+	},
+
+	"/interaction_profiles/google/daydream_controller": {
+		"title": "Google Daydream Controller",
+		"user_paths": [
+			"/user/hand/left",
+			"/user/hand/right"
+		],
+		"components": [
+			{"subpath": "/input/select/click"},
+			{"subpath": "/input/trackpad/x"},
+			{"subpath": "/input/trackpad/y"},
+			{"subpath": "/input/trackpad/click"},
+			{"subpath": "/input/trackpad/touch"},
+			{"subpath": "/input/grip/pose"},
+			{"subpath": "/input/aim/pose"}
+		]
+	},
+
+	"/interaction_profiles/htc/vive_controller": {
+		"title": "HTC Vive Controller",
+		"user_paths": [
+			"/user/hand/left",
+			"/user/hand/right"
+		],
+		"components": [
+			{"subpath": "/input/system/click", "system": "true"},
+			{"subpath": "/input/squeeze/click"},
+			{"subpath": "/input/menu/click"},
+			{"subpath": "/input/trigger/click"},
+			{"subpath": "/input/trigger/value"},
+			{"subpath": "/input/trackpad/x"},
+			{"subpath": "/input/trackpad/y"},
+			{"subpath": "/input/trackpad/click"},
+			{"subpath": "/input/trackpad/touch"},
+			{"subpath": "/input/grip/pose"},
+			{"subpath": "/input/aim/pose"},
+			{"subpath": "/output/haptic"}
+		]
+	},
+
+	"/interaction_profiles/htc/vive_pro": {
+		"title": "HTC Vive Pro",
+		"user_paths": [
+			"/user/head"
+		],
+		"components": [
+			{"subpath": "/input/system/click", "system": "true"},
+			{"subpath": "/input/volume_up/click"},
+			{"subpath": "/input/volume_down/click"},
+			{"subpath": "/input/mute_mic/click"}
+		]
+	},
+
+	"/interaction_profiles/microsoft/motion_controller": {
+		"title": "Microsoft Mixed Reality Motion Controller",
+		"user_paths": [
+			"/user/hand/left",
+			"/user/hand/right"
+		],
+		"components": [
+			{"subpath": "/input/menu/click"},
+			{"subpath": "/input/squeeze/click"},
+			{"subpath": "/input/trigger/value"},
+			{"subpath": "/input/thumbstick/x"},
+			{"subpath": "/input/thumbstick/y"},
+			{"subpath": "/input/thumbstick/click"},
+			{"subpath": "/input/trackpad/x"},
+			{"subpath": "/input/trackpad/y"},
+			{"subpath": "/input/trackpad/click"},
+			{"subpath": "/input/trackpad/touch"},
+			{"subpath": "/input/grip/pose"},
+			{"subpath": "/input/aim/pose"},
+			{"subpath": "/output/haptic"}
+		]
+	},
+
+	"/interaction_profiles/microsoft/xbox_controller": {
+		"title": "Microsoft Xbox Controller",
+		"user_paths": [
+			"/user/gamepad"
+		],
+		"components": [
+			{"subpath": "/input/menu/click"},
+			{"subpath": "/input/view/click"},
+			{"subpath": "/input/a/click"},
+			{"subpath": "/input/b/click"},
+			{"subpath": "/input/x/click"},
+			{"subpath": "/input/y/click"},
+			{"subpath": "/input/dpad_down/click"},
+			{"subpath": "/input/dpad_right/click"},
+			{"subpath": "/input/dpad_up/click"},
+			{"subpath": "/input/dpad_left/click"},
+			{"subpath": "/input/shoulder_left/click"},
+			{"subpath": "/input/shoulder_right/click"},
+			{"subpath": "/input/thumbstick_left/click"},
+			{"subpath": "/input/thumbstick_right/click"},
+			{"subpath": "/input/trigger_left/value"},
+			{"subpath": "/input/trigger_right/value"},
+			{"subpath": "/input/thumbstick_left/x"},
+			{"subpath": "/input/thumbstick_left/y"},
+			{"subpath": "/input/thumbstick_right/x"},
+			{"subpath": "/input/thumbstick_right/y"},
+			{"subpath": "/output/haptic_left"},
+			{"subpath": "/output/haptic_right"},
+			{"subpath": "/output/haptic_left_trigger"},
+			{"subpath": "/output/haptic_right_trigger"}
+		]
+	},
+
+	"/interaction_profiles/oculus/go_controller": {
+		"title": "Oculus Go Controller",
+		"user_paths": [
+			"/user/hand/left",
+			"/user/hand/right"
+		],
+		"components": [
+			{"subpath": "/input/system/click", "system": "true"},
+			{"subpath": "/input/trigger/click"},
+			{"subpath": "/input/back/click"},
+			{"subpath": "/input/trackpad/x"},
+			{"subpath": "/input/trackpad/y"},
+			{"subpath": "/input/trackpad/click"},
+			{"subpath": "/input/trackpad/touch"},
+			{"subpath": "/input/grip/pose"},
+			{"subpath": "/input/aim/pose"}
+		]
+	},
+
+	"/interaction_profiles/oculus/touch_controller": {
+		"title": "Oculus Touch Controller",
+		"user_paths": [
+			"/user/hand/left",
+			"/user/hand/right"
+		],
+		"components": [
+			{"user_paths": "/user/hand/left", "subpath": "/input/x/click"},
+			{"user_paths": "/user/hand/left", "subpath": "/input/x/touch"},
+			{"user_paths": "/user/hand/left", "subpath": "/input/y/click"},
+			{"user_paths": "/user/hand/left", "subpath": "/input/y/touch"},
+			{"user_paths": "/user/hand/left", "subpath": "/input/menu/click"},
+			{"user_paths": "/user/hand/right", "subpath": "/input/a/click"},
+			{"user_paths": "/user/hand/right", "subpath": "/input/a/touch"},
+			{"user_paths": "/user/hand/right", "subpath": "/input/b/click"},
+			{"user_paths": "/user/hand/right", "subpath": "/input/b/touch"},
+			{"user_paths": "/user/hand/right", "subpath": "/input/system/click", "system": "true"},
+			{"subpath": "/input/squeeze/value"},
+			{"subpath": "/input/trigger/value"},
+			{"subpath": "/input/trigger/touch"},
+			{"subpath": "/input/thumbstick/x"},
+			{"subpath": "/input/thumbstick/y"},
+			{"subpath": "/input/thumbstick/click"},
+			{"subpath": "/input/thumbstick/touch"},
+			{"subpath": "/input/thumbrest/touch"},
+			{"subpath": "/input/grip/pose"},
+			{"subpath": "/input/aim/pose"},
+			{"subpath": "/output/haptic"}
+		]
+	},
+
+	"/interaction_profiles/valve/index_controller": {
+		"title": "Valve Index Controller",
+		"user_paths": [
+			"/user/hand/left",
+			"/user/hand/right"
+		],
+		"components": [
+			{"subpath": "/input/system/click", "system": "true"},
+			{"subpath": "/input/system/touch", "system": "true"},
+			{"subpath": "/input/a/click"},
+			{"subpath": "/input/a/touch"},
+			{"subpath": "/input/b/click"},
+			{"subpath": "/input/b/touch"},
+			{"subpath": "/input/squeeze/value"},
+			{"subpath": "/input/squeeze/force"},
+			{"subpath": "/input/trigger/click"},
+			{"subpath": "/input/trigger/value"},
+			{"subpath": "/input/trigger/touch"},
+			{"subpath": "/input/thumbstick/x"},
+			{"subpath": "/input/thumbstick/y"},
+			{"subpath": "/input/thumbstick/click"},
+			{"subpath": "/input/thumbstick/touch"},
+			{"subpath": "/input/trackpad/x"},
+			{"subpath": "/input/trackpad/y"},
+			{"subpath": "/input/trackpad/force"},
+			{"subpath": "/input/trackpad/touch"},
+			{"subpath": "/input/grip/pose"},
+			{"subpath": "/input/aim/pose"},
+			{"subpath": "/output/haptic"}
+		]
+	},
+
+	"/interaction_profiles/microsoft/hand_interaction": {
+		"title": "Microsoft hand interaction",
+		"extension": "XR_MSFT_hand_interaction",
+		"user_paths": [
+			"/user/hand/left",
+			"/user/hand/right"
+		],
+		"components": [
+			{"subpath": "/input/select/value"},
+			{"subpath": "/input/squeeze/value"},
+			{"subpath": "/input/grip/pose"},
+			{"subpath": "/input/aim/pose"}
+		]
+	},
+
+	"/interaction_profiles/mnd/ball_on_a_stick_controller": {
+		"title": "Monado ball on a stick controller",
+		"extension": "XR_MND_ball_on_a_stick_controller",
+		"user_paths": [
+			"/user/hand/left",
+			"/user/hand/right"
+		],
+		"components": [
+			{"subpath": "/input/system/click", "system": "true"},
+			{"subpath": "/input/menu/click"},
+			{"subpath": "/input/start/click"},
+			{"subpath": "/input/select/click"},
+			{"subpath": "/input/square_mnd/click"},
+			{"subpath": "/input/cross_mnd/click"},
+			{"subpath": "/input/circle_mnd/click"},
+			{"subpath": "/input/triangle_mnd/click"},
+			{"subpath": "/input/trigger/value"},
+			{"subpath": "/input/grip/pose"},
+			{"subpath": "/input/ball_mnd/pose"},
+			{"subpath": "/input/aim/pose"},
+			{"subpath": "/output/haptic"}
+		]
+	}
+}
diff --git a/src/xrt/state_trackers/oxr/bindings.py b/src/xrt/state_trackers/oxr/bindings.py
new file mode 100755
index 0000000000000000000000000000000000000000..0f33961c40ceb1c20885895189654449f596d98d
--- /dev/null
+++ b/src/xrt/state_trackers/oxr/bindings.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python3
+# Copyright 2020, Collabora, Ltd.
+# SPDX-License-Identifier: BSL-1.0
+"""Generate code from a JSON file describing interaction profiles and bindings."""
+
+import json
+import argparse
+
+
+def strip_subpath_end(path):
+    if (path.endswith("/value")):
+        return True, path[:-6]
+    if (path.endswith("/click")):
+        return True, path[:-6]
+    if (path.endswith("/touch")):
+        return True, path[:-6]
+    if (path.endswith("/pose")):
+        return True, path[:-5]
+    if (path.endswith("/x")):
+        return True, path[:-2]
+    if (path.endswith("/y")):
+        return True, path[:-2]
+    if (path.endswith("/output/haptic")):
+        return False, path
+    return False, path
+
+
+class Component:
+
+    @classmethod
+    def parse_array(cls, user_paths, arr):
+        """Turn an array of data into an array of Component objects.
+        Creates a Component for each user_path and stripped subpaths.
+        """
+        check = {}
+        ret = []
+        for elm in arr:
+            ups = user_paths
+            if ("user_paths" in elm):
+                ups = [elm["user_paths"]]
+            for up in ups:
+                subpath = elm["subpath"]
+                did_strip, stripped_path = strip_subpath_end(subpath)
+                fullpath = up + stripped_path
+                if (did_strip and not fullpath in check):
+                    check[fullpath] = True
+                    ret.append(cls(fullpath, elm))
+                fullpath = up + subpath
+                ret.append(cls(fullpath, elm))
+        return ret
+
+    def __init__(self, path, elm):
+        """Construct an component."""
+        self.path = path
+
+
+class Profile:
+    """An interctive bindings profile."""
+    def __init__(self, name, data):
+        """Construct an profile."""
+        self.name = name
+        self.func = name[22:].replace("/", "_")
+        self.components = Component.parse_array(data["user_paths"], data["components"])
+        self.by_length = {}
+        for component in self.components:
+            l = len(component.path)
+            if (l in self.by_length):
+                self.by_length[l].append(component)
+            else:
+                self.by_length[l] = [component]
+
+
+class Bindings:
+    """A group of interactive profiles used in bindings."""
+
+    @classmethod
+    def parse(cls, data):
+        """Parse a dictionary defining a protocol into Profile objects."""
+        return cls(data)
+
+    @classmethod
+    def load_and_parse(cls, file):
+        """Load a JSON file and parse it into Profile objects."""
+        with open(file) as infile:
+            return cls.parse(json.loads(infile.read()))
+
+    def __init__(self, data):
+        """Construct a bindings from a dictionary of profiles."""
+        self.profiles = [Profile(name, call) for name, call in data.items()]
+
+
+header = '''// Copyright 2020, Collabora, Ltd.
+// SPDX-License-Identifier: BSL-1.0
+/*!
+ * @file
+ * @brief  {brief}.
+ * @author Jakob Bornecrantz <jakob@collabora.com>
+ * @ingroup {group}
+ */
+'''
+
+func_start = '''
+bool
+oxr_verify_{func}_subpath(const char *str, size_t length)
+{{
+\tswitch (length) {{
+'''
+
+if_strcmp = '''if (strcmp(str, "{check}") == 0) {{
+\t\t\treturn true;
+\t\t}} else '''
+
+def generate_bindings_c(file, p):
+    """Generate the file to verify subpaths on a interaction profile."""
+    f = open(file, "w")
+    f.write(header.format(brief='Generated bindings data', group='oxr_main'))
+    f.write('''
+#include "xrt/xrt_compiler.h"
+
+#include <string.h>
+
+
+// clang-format off
+''')
+
+    for profile in p.profiles:
+        f.write(func_start.format(func=profile.func))
+        for length in profile.by_length:
+            f.write("\tcase " + str(length) + ":\n\t\t")
+            for component in profile.by_length[length]:
+                f.write(if_strcmp.format(check=component.path))
+            f.write("{\n\t\t\treturn false;\n\t\t}\n")
+        f.write("\tdefault:\n\t\treturn false;\n\t}\n}\n")
+    f.write("\n// clang-format on\n")
+    f.close()
+
+
+def generate_bindings_h(file, p):
+    """Generate header for the verify subpaths functions."""
+    f = open(file, "w")
+    f.write(header.format(brief='Generated bindings data header', group='oxr_api'))
+    f.write('''
+#include "xrt/xrt_compiler.h"
+
+
+// clang-format off
+''')
+
+    for profile in p.profiles:
+        f.write("\nbool\noxr_verify_" + profile.func + "_subpath(const char *str, size_t length);\n")
+    f.write("\n// clang-format on\n")
+    f.close()
+
+
+def main():
+    """Handle command line and generate a file."""
+    parser = argparse.ArgumentParser(description='Bindings generator.')
+    parser.add_argument(
+        'bindings', help='Bindings file to use')
+    parser.add_argument(
+        'output', type=str, nargs='+',
+        help='Output file, uses the name to choose output type')
+    args = parser.parse_args()
+
+    p = Bindings.load_and_parse(args.bindings)
+
+    for output in args.output:
+        if output.endswith("oxr_generated_bindings.c"):
+            generate_bindings_c(output, p)
+        if output.endswith("oxr_generated_bindings.h"):
+            generate_bindings_h(output, p)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/xrt/state_trackers/oxr/meson.build b/src/xrt/state_trackers/oxr/meson.build
index 94f20952af07b923dc45d203d302242a6d5e4516..955c54cd5b5c4e4656c5f6e82b12b926b0b909be 100644
--- a/src/xrt/state_trackers/oxr/meson.build
+++ b/src/xrt/state_trackers/oxr/meson.build
@@ -1,6 +1,27 @@
 # Copyright 2019-2020, Collabora, Ltd.
 # SPDX-License-Identifier: BSL-1.0
 
+
+###
+# Binding generation
+#
+
+prog_python = import('python').find_installation('python3')
+
+generated = custom_target('bindings code',
+	command: [prog_python, '@INPUT@', '@OUTPUT@'],
+	input: ['bindings.py', 'bindings.json'],
+	output: [
+		'oxr_generated_bindings.h',
+		'oxr_generated_bindings.c',
+	]
+)
+
+
+###
+# Main code
+#
+
 compile_args = []
 if build_opengl
 	compile_args += ['-DXR_USE_GRAPHICS_API_OPENGL', '-DXR_USE_GRAPHICS_API_OPENGL_ES']
@@ -16,7 +37,9 @@ endif
 
 lib_st_oxr = static_library(
 	'st_oxr',
-	files(
+	[
+		generated[0],
+		generated[1],
 		'oxr_api_action.c',
 		'oxr_api_funcs.h',
 		'oxr_api_instance.c',
@@ -50,7 +73,7 @@ lib_st_oxr = static_library(
 		'oxr_verify.c',
 		'oxr_vulkan.c',
 		'oxr_xdev.c',
-	),
+	],
 	include_directories: [
 		xrt_include,
 		openxr_include,
diff --git a/src/xrt/state_trackers/oxr/oxr_api_action.c b/src/xrt/state_trackers/oxr/oxr_api_action.c
index 027b354f74a4c060aba042a99cbe193bcc59ca6a..6b4e730b8c6c839a6f73e3a3b2fb7d322fd09a2c 100644
--- a/src/xrt/state_trackers/oxr/oxr_api_action.c
+++ b/src/xrt/state_trackers/oxr/oxr_api_action.c
@@ -15,6 +15,7 @@
 
 #include "oxr_api_funcs.h"
 #include "oxr_api_verify.h"
+#include "oxr_generated_bindings.h"
 
 #include <stdio.h>
 #include <inttypes.h>
@@ -105,6 +106,51 @@ oxr_xrSuggestInteractionProfileBindings(
 		                 "== 0) can not suggest 0 bindings");
 	}
 
+	XrPath ip = suggestedBindings->interactionProfile;
+	const char *str = NULL;
+	size_t length;
+
+	XrResult ret = oxr_path_get_string(&log, inst, ip, &str, &length);
+	if (ret != XR_SUCCESS) {
+		oxr_error(
+		    &log, ret,
+		    "(suggestedBindings->countSuggestedBindings == 0x%08" PRIx64
+		    ") invalid path",
+		    ip);
+	}
+
+	// Used in the loop that verifies the suggested bindings paths.
+	bool (*func)(const char *, size_t) = NULL;
+
+	if (ip == inst->path_cache.khr_simple_controller) {
+		func = oxr_verify_khr_simple_controller_subpath;
+	} else if (ip == inst->path_cache.google_daydream_controller) {
+		func = oxr_verify_google_daydream_controller_subpath;
+	} else if (ip == inst->path_cache.htc_vive_controller) {
+		func = oxr_verify_htc_vive_controller_subpath;
+	} else if (ip == inst->path_cache.htc_vive_pro) {
+		func = oxr_verify_htc_vive_pro_subpath;
+	} else if (ip == inst->path_cache.microsoft_motion_controller) {
+		func = oxr_verify_microsoft_motion_controller_subpath;
+	} else if (ip == inst->path_cache.microsoft_xbox_controller) {
+		func = oxr_verify_microsoft_xbox_controller_subpath;
+	} else if (ip == inst->path_cache.oculus_go_controller) {
+		func = oxr_verify_oculus_go_controller_subpath;
+	} else if (ip == inst->path_cache.oculus_touch_controller) {
+		func = oxr_verify_oculus_touch_controller_subpath;
+	} else if (ip == inst->path_cache.valve_index_controller) {
+		func = oxr_verify_valve_index_controller_subpath;
+	} else if (ip == inst->path_cache.mnd_ball_on_stick_controller) {
+		func = oxr_verify_mnd_ball_on_a_stick_controller_subpath;
+	} else {
+		return oxr_error(
+		    &log, XR_ERROR_PATH_UNSUPPORTED,
+		    "(suggestedBindings->interactionProfile == \"%s\") is not "
+		    "a supported interaction profile",
+		    str);
+	}
+
+
 	for (size_t i = 0; i < suggestedBindings->countSuggestedBindings; i++) {
 		const XrActionSuggestedBinding *s =
 		    &suggestedBindings->suggestedBindings[i];
@@ -112,15 +158,17 @@ oxr_xrSuggestInteractionProfileBindings(
 		struct oxr_action *act;
 		OXR_VERIFY_ACTION_NOT_NULL(&log, s->action, act);
 
-		if (act->act_set->attached) {
+		if (act->act_set->data->attached) {
 			return oxr_error(
 			    &log, XR_ERROR_ACTIONSETS_ALREADY_ATTACHED,
 			    "(suggestedBindings->suggestedBindings[%zu]->"
 			    "action) action '%s/%s' has already been attached",
-			    i, act->act_set->name, act->name);
+			    i, act->act_set->data->name, act->data->name);
 		}
 
-		if (!oxr_path_is_valid(&log, inst, s->binding)) {
+		ret =
+		    oxr_path_get_string(&log, inst, s->binding, &str, &length);
+		if (ret != XR_SUCCESS) {
 			return oxr_error(
 			    &log, XR_ERROR_PATH_INVALID,
 			    "(suggestedBindings->suggestedBindings[%zu]->"
@@ -128,7 +176,13 @@ oxr_xrSuggestInteractionProfileBindings(
 			    i, s->binding);
 		}
 
-		//! @todo verify path (s->binding).
+		if (!func(str, length)) {
+			return oxr_error(
+			    &log, XR_ERROR_PATH_UNSUPPORTED,
+			    "(suggestedBindings->suggestedBindings[%zu]->"
+			    "binding == \"%s\") is not a valid path",
+			    i, str);
+		}
 	}
 
 	return oxr_action_suggest_interaction_profile_bindings(
@@ -283,7 +337,7 @@ oxr_xrCreateAction(XrActionSet actionSet,
 	OXR_VERIFY_ARG_LOCALIZED_NAME(&log, createInfo->localizedActionName);
 	OXR_VERIFY_ARG_NOT_NULL(&log, action);
 
-	if (act_set->attached) {
+	if (act_set->data->attached) {
 		return oxr_error(
 		    &log, XR_ERROR_ACTIONSETS_ALREADY_ATTACHED,
 		    "(actionSet) has been attached and is now immutable");
@@ -303,7 +357,7 @@ oxr_xrCreateAction(XrActionSet actionSet,
 	 * Dup checks.
 	 */
 
-	h_ret = u_hashset_find_c_str(act_set->actions.name_store,
+	h_ret = u_hashset_find_c_str(act_set->data->actions.name_store,
 	                             createInfo->actionName, &d);
 	if (h_ret >= 0) {
 		return oxr_error(
@@ -312,7 +366,7 @@ oxr_xrCreateAction(XrActionSet actionSet,
 		    createInfo->actionName);
 	}
 
-	h_ret = u_hashset_find_c_str(act_set->actions.loc_store,
+	h_ret = u_hashset_find_c_str(act_set->data->actions.loc_store,
 	                             createInfo->localizedActionName, &d);
 	if (h_ret >= 0) {
 		return oxr_error(&log, XR_ERROR_LOCALIZED_NAME_DUPLICATED,
@@ -364,19 +418,20 @@ oxr_xrGetActionStateBoolean(XrSession session,
 	                                 XR_TYPE_ACTION_STATE_GET_INFO);
 	OXR_VERIFY_ACTION_NOT_NULL(&log, getInfo->action, act);
 
-	if (act->action_type != XR_ACTION_TYPE_BOOLEAN_INPUT) {
+	if (act->data->action_type != XR_ACTION_TYPE_BOOLEAN_INPUT) {
 		return oxr_error(&log, XR_ERROR_ACTION_TYPE_MISMATCH,
 		                 "Not created with boolean type");
 	}
 
 	ret = oxr_verify_subaction_path_get(
-	    &log, act->act_set->inst, getInfo->subactionPath, &act->sub_paths,
-	    &sub_paths, "getInfo->subactionPath");
+	    &log, act->act_set->inst, getInfo->subactionPath,
+	    &act->data->sub_paths, &sub_paths, "getInfo->subactionPath");
 	if (ret != XR_SUCCESS) {
 		return ret;
 	}
 
-	return oxr_action_get_boolean(&log, sess, act->key, sub_paths, data);
+	return oxr_action_get_boolean(&log, sess, act->act_key, sub_paths,
+	                              data);
 }
 
 XrResult
@@ -397,19 +452,20 @@ oxr_xrGetActionStateFloat(XrSession session,
 	                                 XR_TYPE_ACTION_STATE_GET_INFO);
 	OXR_VERIFY_ACTION_NOT_NULL(&log, getInfo->action, act);
 
-	if (act->action_type != XR_ACTION_TYPE_FLOAT_INPUT) {
+	if (act->data->action_type != XR_ACTION_TYPE_FLOAT_INPUT) {
 		return oxr_error(&log, XR_ERROR_ACTION_TYPE_MISMATCH,
 		                 "Not created with float type");
 	}
 
 	ret = oxr_verify_subaction_path_get(
-	    &log, act->act_set->inst, getInfo->subactionPath, &act->sub_paths,
-	    &sub_paths, "getInfo->subactionPath");
+	    &log, act->act_set->inst, getInfo->subactionPath,
+	    &act->data->sub_paths, &sub_paths, "getInfo->subactionPath");
 	if (ret != XR_SUCCESS) {
 		return ret;
 	}
 
-	return oxr_action_get_vector1f(&log, sess, act->key, sub_paths, data);
+	return oxr_action_get_vector1f(&log, sess, act->act_key, sub_paths,
+	                               data);
 }
 
 XrResult
@@ -430,19 +486,20 @@ oxr_xrGetActionStateVector2f(XrSession session,
 	                                 XR_TYPE_ACTION_STATE_GET_INFO);
 	OXR_VERIFY_ACTION_NOT_NULL(&log, getInfo->action, act);
 
-	if (act->action_type != XR_ACTION_TYPE_VECTOR2F_INPUT) {
+	if (act->data->action_type != XR_ACTION_TYPE_VECTOR2F_INPUT) {
 		return oxr_error(&log, XR_ERROR_ACTION_TYPE_MISMATCH,
 		                 "Not created with float[2] type");
 	}
 
 	ret = oxr_verify_subaction_path_get(
-	    &log, act->act_set->inst, getInfo->subactionPath, &act->sub_paths,
-	    &sub_paths, "getInfo->subactionPath");
+	    &log, act->act_set->inst, getInfo->subactionPath,
+	    &act->data->sub_paths, &sub_paths, "getInfo->subactionPath");
 	if (ret != XR_SUCCESS) {
 		return ret;
 	}
 
-	return oxr_action_get_vector2f(&log, sess, act->key, sub_paths, data);
+	return oxr_action_get_vector2f(&log, sess, act->act_key, sub_paths,
+	                               data);
 }
 
 XrResult
@@ -462,19 +519,19 @@ oxr_xrGetActionStatePose(XrSession session,
 	                                 XR_TYPE_ACTION_STATE_GET_INFO);
 	OXR_VERIFY_ACTION_NOT_NULL(&log, getInfo->action, act);
 
-	if (act->action_type != XR_ACTION_TYPE_POSE_INPUT) {
+	if (act->data->action_type != XR_ACTION_TYPE_POSE_INPUT) {
 		return oxr_error(&log, XR_ERROR_ACTION_TYPE_MISMATCH,
 		                 "Not created with pose type");
 	}
 
 	ret = oxr_verify_subaction_path_get(
-	    &log, act->act_set->inst, getInfo->subactionPath, &act->sub_paths,
-	    &sub_paths, "getInfo->subactionPath");
+	    &log, act->act_set->inst, getInfo->subactionPath,
+	    &act->data->sub_paths, &sub_paths, "getInfo->subactionPath");
 	if (ret != XR_SUCCESS) {
 		return ret;
 	}
 
-	return oxr_action_get_pose(&log, sess, act->key, sub_paths, data);
+	return oxr_action_get_pose(&log, sess, act->act_key, sub_paths, data);
 }
 
 XrResult
@@ -501,7 +558,7 @@ oxr_xrEnumerateBoundSourcesForAction(
 		                 "been called on this session.");
 	}
 
-	return oxr_action_enumerate_bound_sources(&log, sess, act->key,
+	return oxr_action_enumerate_bound_sources(&log, sess, act->act_key,
 	                                          sourceCapacityInput,
 	                                          sourceCountOutput, sources);
 }
@@ -533,18 +590,18 @@ oxr_xrApplyHapticFeedback(XrSession session,
 
 	ret = oxr_verify_subaction_path_get(
 	    &log, act->act_set->inst, hapticActionInfo->subactionPath,
-	    &act->sub_paths, &sub_paths, "getInfo->subactionPath");
+	    &act->data->sub_paths, &sub_paths, "getInfo->subactionPath");
 	if (ret != XR_SUCCESS) {
 		return ret;
 	}
 
-	if (act->action_type != XR_ACTION_TYPE_VIBRATION_OUTPUT) {
+	if (act->data->action_type != XR_ACTION_TYPE_VIBRATION_OUTPUT) {
 		return oxr_error(&log, XR_ERROR_ACTION_TYPE_MISMATCH,
 		                 "Not created with output vibration type");
 	}
 
-	return oxr_action_apply_haptic_feedback(&log, sess, act->key, sub_paths,
-	                                        hapticEvent);
+	return oxr_action_apply_haptic_feedback(&log, sess, act->act_key,
+	                                        sub_paths, hapticEvent);
 }
 
 XrResult
@@ -564,15 +621,16 @@ oxr_xrStopHapticFeedback(XrSession session,
 
 	ret = oxr_verify_subaction_path_get(
 	    &log, act->act_set->inst, hapticActionInfo->subactionPath,
-	    &act->sub_paths, &sub_paths, "getInfo->subactionPath");
+	    &act->data->sub_paths, &sub_paths, "getInfo->subactionPath");
 	if (ret != XR_SUCCESS) {
 		return ret;
 	}
 
-	if (act->action_type != XR_ACTION_TYPE_VIBRATION_OUTPUT) {
+	if (act->data->action_type != XR_ACTION_TYPE_VIBRATION_OUTPUT) {
 		return oxr_error(&log, XR_ERROR_ACTION_TYPE_MISMATCH,
 		                 "Not created with output vibration type");
 	}
 
-	return oxr_action_stop_haptic_feedback(&log, sess, act->key, sub_paths);
+	return oxr_action_stop_haptic_feedback(&log, sess, act->act_key,
+	                                       sub_paths);
 }
diff --git a/src/xrt/state_trackers/oxr/oxr_api_space.c b/src/xrt/state_trackers/oxr/oxr_api_space.c
index 8cc574ecdff37fc0aa9fe7f42d375dc81cc0c682..09dfe7a89f9142bd4e557a39bd0ac1a756902c52 100644
--- a/src/xrt/state_trackers/oxr/oxr_api_space.c
+++ b/src/xrt/state_trackers/oxr/oxr_api_space.c
@@ -42,7 +42,7 @@ oxr_xrCreateActionSpace(XrSession session,
 
 	struct oxr_space *spc;
 	XrResult ret =
-	    oxr_space_action_create(&log, sess, act->key, createInfo, &spc);
+	    oxr_space_action_create(&log, sess, act->act_key, createInfo, &spc);
 	if (ret != XR_SUCCESS) {
 		return ret;
 	}
diff --git a/src/xrt/state_trackers/oxr/oxr_binding.c b/src/xrt/state_trackers/oxr/oxr_binding.c
index 9cb63dbfd4c4922da4754d401b6a640ac2a13b7f..0d84a481583533906241c217fa884b25369d5f26 100644
--- a/src/xrt/state_trackers/oxr/oxr_binding.c
+++ b/src/xrt/state_trackers/oxr/oxr_binding.c
@@ -14,9 +14,11 @@
 #include "oxr_objects.h"
 #include "oxr_logger.h"
 
+#include "oxr_binding_data.h"
+
 #include <stdio.h>
 
-#include "oxr_binding_data.h"
+
 static void
 setup_paths(struct oxr_logger *log,
             struct oxr_instance *inst,
@@ -84,21 +86,6 @@ setup_outputs(struct oxr_logger *log,
 	}
 }
 
-static bool
-is_valid_interaction_profile(struct oxr_instance *inst, XrPath path)
-{
-	return inst->path_cache.khr_simple_controller == path ||
-	       inst->path_cache.google_daydream_controller == path ||
-	       inst->path_cache.htc_vive_controller == path ||
-	       inst->path_cache.htc_vive_pro == path ||
-	       inst->path_cache.microsoft_motion_controller == path ||
-	       inst->path_cache.microsoft_xbox_controller == path ||
-	       inst->path_cache.oculus_go_controller == path ||
-	       inst->path_cache.oculus_touch_controller == path ||
-	       inst->path_cache.valve_index_controller == path ||
-	       inst->path_cache.mnd_ball_on_stick_controller == path;
-}
-
 static bool
 interaction_profile_find(struct oxr_logger *log,
                          struct oxr_instance *inst,
@@ -360,24 +347,12 @@ oxr_action_suggest_interaction_profile_bindings(
     const XrInteractionProfileSuggestedBinding *suggestedBindings)
 {
 	struct oxr_interaction_profile *p = NULL;
-	XrPath path = suggestedBindings->interactionProfile;
-	const char *str;
-	size_t length;
-
-	// Check if this profile is valid.
-	if (!is_valid_interaction_profile(inst, path)) {
-		oxr_path_get_string(log, inst, path, &str, &length);
-
-		return oxr_error(log, XR_ERROR_PATH_UNSUPPORTED,
-		                 "(suggestedBindings->interactionProfile) "
-		                 "non-supported profile '%s'",
-		                 str);
-	}
 
+	// Path already validated.
+	XrPath path = suggestedBindings->interactionProfile;
 	interaction_profile_find_or_create(log, inst, path, &p);
 
 	// Valid path, but not used.
-	//! @todo Still needs to validate the paths.
 	if (p == NULL) {
 		return XR_SUCCESS;
 	}
@@ -385,7 +360,7 @@ oxr_action_suggest_interaction_profile_bindings(
 	struct oxr_binding *bindings = p->bindings;
 	size_t num_bindings = p->num_bindings;
 
-	//! @todo Validate keys **FIRST** then reset.
+	// Everything is now valid, reset the keys.
 	reset_all_keys(bindings, num_bindings);
 
 	for (size_t i = 0; i < suggestedBindings->countSuggestedBindings; i++) {
@@ -394,13 +369,8 @@ oxr_action_suggest_interaction_profile_bindings(
 		struct oxr_action *act =
 		    XRT_CAST_OXR_HANDLE_TO_PTR(struct oxr_action *, s->action);
 
-#if 0
-		oxr_path_get_string(log, inst, s->binding, &str, &length);
-		fprintf(stderr, "\t\t%s %i -> %s\n", act->name, act->key, str);
-#endif
-
 		add_key_to_matching_bindings(bindings, num_bindings, s->binding,
-		                             act->key);
+		                             act->act_key);
 	}
 
 	return XR_SUCCESS;
diff --git a/src/xrt/state_trackers/oxr/oxr_input.c b/src/xrt/state_trackers/oxr/oxr_input.c
index 725c439a06c0a0cd0eaa4662884004c4c3c6bbd1..9942abee8f4d9c981cbfe3c42558a062df20dfd7 100644
--- a/src/xrt/state_trackers/oxr/oxr_input.c
+++ b/src/xrt/state_trackers/oxr/oxr_input.c
@@ -31,50 +31,162 @@
  */
 
 static void
-oxr_session_get_source_set(struct oxr_session *sess,
-                           XrActionSet actionSet,
-                           struct oxr_source_set **src_set,
-                           struct oxr_action_set **act_set);
+oxr_session_get_action_set_attachment(
+    struct oxr_session *sess,
+    XrActionSet actionSet,
+    struct oxr_action_set_attachment **act_set_attached,
+    struct oxr_action_set **act_set);
 
 static void
-oxr_session_get_source(struct oxr_session *sess,
-                       uint32_t act_key,
-                       struct oxr_source **out_src);
+oxr_session_get_action_attachment(
+    struct oxr_session *sess,
+    uint32_t act_key,
+    struct oxr_action_attachment **out_act_attached);
 
 static void
-oxr_source_cache_update(struct oxr_logger *log,
+oxr_action_cache_update(struct oxr_logger *log,
                         struct oxr_session *sess,
-                        struct oxr_source_cache *cache,
+                        struct oxr_action_cache *cache,
                         int64_t time,
                         bool select);
 
+/*!
+ * @private @memberof oxr_action_attachment
+ */
 static void
-oxr_source_update(struct oxr_logger *log,
-                  struct oxr_session *sess,
-                  struct oxr_source *src,
-                  int64_t time,
-                  struct oxr_sub_paths sub_paths);
+oxr_action_attachment_update(struct oxr_logger *log,
+                             struct oxr_session *sess,
+                             struct oxr_action_attachment *act_attached,
+                             int64_t time,
+                             struct oxr_sub_paths sub_paths);
 
 static void
-oxr_source_bind_inputs(struct oxr_logger *log,
+oxr_action_bind_inputs(struct oxr_logger *log,
                        struct oxr_sink_logger *slog,
                        struct oxr_session *sess,
                        struct oxr_action *act,
-                       struct oxr_source_cache *cache,
+                       struct oxr_action_cache *cache,
                        struct oxr_interaction_profile *profile,
                        enum oxr_sub_action_path sub_path);
 
+/*
+ *
+ * Action attachment functions
+ *
+ */
+
+/*!
+ * De-initialize/de-allocate all dynamic members of @ref oxr_action_cache
+ * @private @memberof oxr_action_cache
+ */
+static void
+oxr_action_cache_teardown(struct oxr_action_cache *cache)
+{
+	free(cache->inputs);
+	cache->inputs = NULL;
+	free(cache->outputs);
+	cache->outputs = NULL;
+}
+
+/*!
+ * Tear down an action attachment struct.
+ *
+ * Does not deallocate the struct itself.
+ *
+ * @public @memberof oxr_action_attachment
+ */
+static void
+oxr_action_attachment_teardown(struct oxr_action_attachment *act_attached)
+{
+	struct oxr_session *sess = act_attached->sess;
+	u_hashmap_int_erase(sess->act_attachments_by_key,
+	                    act_attached->act_key);
+	oxr_action_cache_teardown(&(act_attached->user));
+	oxr_action_cache_teardown(&(act_attached->head));
+	oxr_action_cache_teardown(&(act_attached->left));
+	oxr_action_cache_teardown(&(act_attached->right));
+	oxr_action_cache_teardown(&(act_attached->gamepad));
+	// Unref this action's refcounted data
+	oxr_refcounted_unref(&act_attached->act_ref->base);
+}
+
+/*!
+ * Set up an action attachment struct.
+ *
+ * @public @memberof oxr_action_attachment
+ */
 static XrResult
-oxr_source_destroy_cb(struct oxr_logger *log, struct oxr_handle_base *hb);
+oxr_action_attachment_init(struct oxr_logger *log,
+                           struct oxr_action_set_attachment *act_set_attached,
+                           struct oxr_action_attachment *act_attached,
+                           struct oxr_action *act)
+{
+	struct oxr_session *sess = act_set_attached->sess;
+	act_attached->sess = sess;
+	act_attached->act_set_attached = act_set_attached;
+	u_hashmap_int_insert(sess->act_attachments_by_key, act->act_key,
+	                     act_attached);
+
+	// Reference this action's refcounted data
+	act_attached->act_ref = act->data;
+	oxr_refcounted_ref(&act_attached->act_ref->base);
+
+	// Copy this for efficiency.
+	act_attached->act_key = act->act_key;
+	return XR_SUCCESS;
+}
+
+
+/*
+ *
+ * Action set attachment functions
+ *
+ */
 
+/*!
+ * @public @memberof oxr_action_set_attachment
+ */
 static XrResult
-oxr_source_create(struct oxr_logger *log,
-                  struct oxr_source_set *src_set,
-                  struct oxr_action *act,
-                  struct oxr_interaction_profile *head,
-                  struct oxr_interaction_profile *left,
-                  struct oxr_interaction_profile *right,
-                  struct oxr_interaction_profile *gamepad);
+oxr_action_set_attachment_init(
+    struct oxr_logger *log,
+    struct oxr_session *sess,
+    struct oxr_action_set *act_set,
+    struct oxr_action_set_attachment *act_set_attached)
+{
+	act_set_attached->sess = sess;
+
+	// Reference this action set's refcounted data
+	act_set_attached->act_set_ref = act_set->data;
+	oxr_refcounted_ref(&act_set_attached->act_set_ref->base);
+
+	u_hashmap_int_insert(sess->act_sets_attachments_by_key,
+	                     act_set->act_set_key, act_set_attached);
+
+	// Copy this for efficiency.
+	act_set_attached->act_set_key = act_set->act_set_key;
+
+	return XR_SUCCESS;
+}
+
+void
+oxr_action_set_attachment_teardown(
+    struct oxr_action_set_attachment *act_set_attached)
+{
+	for (size_t i = 0; i < act_set_attached->num_action_attachments; ++i) {
+		oxr_action_attachment_teardown(
+		    &(act_set_attached->act_attachments[i]));
+	}
+	free(act_set_attached->act_attachments);
+	act_set_attached->act_attachments = NULL;
+	act_set_attached->num_action_attachments = 0;
+
+	struct oxr_session *sess = act_set_attached->sess;
+	u_hashmap_int_erase(sess->act_sets_attachments_by_key,
+	                    act_set_attached->act_set_key);
+
+	// Unref this action set's refcounted data
+	oxr_refcounted_unref(&act_set_attached->act_set_ref->base);
+}
 
 
 /*
@@ -82,13 +194,26 @@ oxr_source_create(struct oxr_logger *log,
  * Action set functions
  *
  */
+static void
+oxr_action_set_ref_destroy_cb(struct oxr_refcounted *orc)
+{
+	struct oxr_action_set_ref *act_set_ref =
+	    (struct oxr_action_set_ref *)orc;
+
+	u_hashset_destroy(&act_set_ref->actions.name_store);
+	u_hashset_destroy(&act_set_ref->actions.loc_store);
+
+	free(act_set_ref);
+}
 
 static XrResult
 oxr_action_set_destroy_cb(struct oxr_logger *log, struct oxr_handle_base *hb)
 {
-	//! @todo Move to oxr_objects.h
 	struct oxr_action_set *act_set = (struct oxr_action_set *)hb;
 
+	oxr_refcounted_unref(&act_set->data->base);
+	act_set->data = NULL;
+
 	if (act_set->name_item != NULL) {
 		u_hashset_erase_item(act_set->inst->action_sets.name_store,
 		                     act_set->name_item);
@@ -102,9 +227,6 @@ oxr_action_set_destroy_cb(struct oxr_logger *log, struct oxr_handle_base *hb)
 		act_set->loc_item = NULL;
 	}
 
-	u_hashset_destroy(&act_set->actions.name_store);
-	u_hashset_destroy(&act_set->actions.loc_store);
-
 	free(act_set);
 
 	return XR_SUCCESS;
@@ -120,30 +242,37 @@ oxr_action_set_create(struct oxr_logger *log,
 	static uint32_t key_gen = 1;
 	int h_ret;
 
-	//! @todo Implement more fully.
 	struct oxr_action_set *act_set = NULL;
 	OXR_ALLOCATE_HANDLE_OR_RETURN(log, act_set, OXR_XR_DEBUG_ACTIONSET,
 	                              oxr_action_set_destroy_cb, &inst->handle);
 
-	h_ret = u_hashset_create(&act_set->actions.name_store);
+	struct oxr_action_set_ref *act_set_ref =
+	    U_TYPED_CALLOC(struct oxr_action_set_ref);
+	act_set_ref->base.destroy = oxr_action_set_ref_destroy_cb;
+	oxr_refcounted_ref(&act_set_ref->base);
+	act_set->data = act_set_ref;
+
+	act_set_ref->act_set_key = key_gen++;
+	act_set->act_set_key = act_set_ref->act_set_key;
+
+	act_set->inst = inst;
+
+	h_ret = u_hashset_create(&act_set_ref->actions.name_store);
 	if (h_ret != 0) {
 		oxr_handle_destroy(log, &act_set->handle);
 		return oxr_error(log, XR_ERROR_RUNTIME_FAILURE,
 		                 "Failed to create name_store hashset");
 	}
 
-	h_ret = u_hashset_create(&act_set->actions.loc_store);
+	h_ret = u_hashset_create(&act_set_ref->actions.loc_store);
 	if (h_ret != 0) {
 		oxr_handle_destroy(log, &act_set->handle);
 		return oxr_error(log, XR_ERROR_RUNTIME_FAILURE,
 		                 "Failed to create loc_store hashset");
 	}
 
-	act_set->key = key_gen++;
-
-	act_set->inst = inst;
-	strncpy(act_set->name, createInfo->actionSetName,
-	        sizeof(act_set->name));
+	strncpy(act_set_ref->name, createInfo->actionSetName,
+	        sizeof(act_set_ref->name));
 
 	u_hashset_create_and_insert_str_c(inst->action_sets.name_store,
 	                                  createInfo->actionSetName,
@@ -164,20 +293,29 @@ oxr_action_set_create(struct oxr_logger *log,
  *
  */
 
+static void
+oxr_action_ref_destroy_cb(struct oxr_refcounted *orc)
+{
+	struct oxr_action_ref *act_ref = (struct oxr_action_ref *)orc;
+	free(act_ref);
+}
+
 static XrResult
 oxr_action_destroy_cb(struct oxr_logger *log, struct oxr_handle_base *hb)
 {
-	//! @todo Move to oxr_objects.h
 	struct oxr_action *act = (struct oxr_action *)hb;
 
+	oxr_refcounted_unref(&act->data->base);
+	act->data = NULL;
+
 	if (act->name_item != NULL) {
-		u_hashset_erase_item(act->act_set->actions.name_store,
+		u_hashset_erase_item(act->act_set->data->actions.name_store,
 		                     act->name_item);
 		free(act->name_item);
 		act->name_item = NULL;
 	}
 	if (act->loc_item != NULL) {
-		u_hashset_erase_item(act->act_set->actions.loc_store,
+		u_hashset_erase_item(act->act_set->data->actions.loc_store,
 		                     act->loc_item);
 		free(act->loc_item);
 		act->loc_item = NULL;
@@ -200,24 +338,35 @@ oxr_action_create(struct oxr_logger *log,
 	// Mod music for all!
 	static uint32_t key_gen = 1;
 
-	oxr_classify_sub_action_paths(log, inst,
-	                              createInfo->countSubactionPaths,
-	                              createInfo->subactionPaths, &sub_paths);
+	if (!oxr_classify_sub_action_paths(
+	        log, inst, createInfo->countSubactionPaths,
+	        createInfo->subactionPaths, &sub_paths)) {
+		return XR_ERROR_PATH_UNSUPPORTED;
+	}
 
 	struct oxr_action *act = NULL;
 	OXR_ALLOCATE_HANDLE_OR_RETURN(log, act, OXR_XR_DEBUG_ACTION,
 	                              oxr_action_destroy_cb, &act_set->handle);
-	act->key = key_gen++;
+
+
+	struct oxr_action_ref *act_ref = U_TYPED_CALLOC(struct oxr_action_ref);
+	act_ref->base.destroy = oxr_action_ref_destroy_cb;
+	oxr_refcounted_ref(&act_ref->base);
+	act->data = act_ref;
+
+	act_ref->act_key = key_gen++;
+	act->act_key = act_ref->act_key;
+
 	act->act_set = act_set;
-	act->sub_paths = sub_paths;
-	act->action_type = createInfo->actionType;
+	act_ref->sub_paths = sub_paths;
+	act_ref->action_type = createInfo->actionType;
 
-	strncpy(act->name, createInfo->actionName, sizeof(act->name));
+	strncpy(act_ref->name, createInfo->actionName, sizeof(act_ref->name));
 
-	u_hashset_create_and_insert_str_c(act_set->actions.name_store,
+	u_hashset_create_and_insert_str_c(act_set->data->actions.name_store,
 	                                  createInfo->actionName,
 	                                  &act->name_item);
-	u_hashset_create_and_insert_str_c(act_set->actions.loc_store,
+	u_hashset_create_and_insert_str_c(act_set->data->actions.loc_store,
 	                                  createInfo->localizedActionName,
 	                                  &act->loc_item);
 
@@ -229,11 +378,11 @@ oxr_action_create(struct oxr_logger *log,
 
 /*
  *
- * "Exproted" helper functions.
+ * "Exported" helper functions.
  *
  */
 
-void
+bool
 oxr_classify_sub_action_paths(struct oxr_logger *log,
                               struct oxr_instance *inst,
                               uint32_t num_subaction_paths,
@@ -242,13 +391,14 @@ oxr_classify_sub_action_paths(struct oxr_logger *log,
 {
 	const char *str = NULL;
 	size_t length = 0;
+	bool ret = true;
 
 	// Reset the sub_paths completely.
 	U_ZERO(sub_paths);
 
 	if (num_subaction_paths == 0) {
 		sub_paths->any = true;
-		return;
+		return ret;
 	}
 
 	for (uint32_t i = 0; i < num_subaction_paths; i++) {
@@ -266,50 +416,58 @@ oxr_classify_sub_action_paths(struct oxr_logger *log,
 			sub_paths->right = true;
 		} else if (path == inst->path_cache.gamepad) {
 			sub_paths->gamepad = true;
+		} else if (path == inst->path_cache.treadmill) {
+			sub_paths->treadmill = true;
 		} else {
 			oxr_path_get_string(log, inst, path, &str, &length);
 
 			oxr_warn(log, " unrecognized sub action path '%s'",
 			         str);
+			ret = false;
 		}
 	}
+	return ret;
 }
 
 XrResult
-oxr_source_get_pose_input(struct oxr_logger *log,
+oxr_action_get_pose_input(struct oxr_logger *log,
                           struct oxr_session *sess,
                           uint32_t act_key,
                           const struct oxr_sub_paths *sub_paths,
-                          struct oxr_source_input **out_input)
+                          struct oxr_action_input **out_input)
 {
-	struct oxr_source *src = NULL;
+	struct oxr_action_attachment *act_attached = NULL;
 
-	oxr_session_get_source(sess, act_key, &src);
+	oxr_session_get_action_attachment(sess, act_key, &act_attached);
 
-	if (src == NULL) {
+	if (act_attached == NULL) {
 		return XR_SUCCESS;
 	}
 
 	// Priority of inputs.
-	if (src->head.current.active && (sub_paths->head || sub_paths->any)) {
-		*out_input = src->head.inputs;
+	if (act_attached->head.current.active &&
+	    (sub_paths->head || sub_paths->any)) {
+		*out_input = act_attached->head.inputs;
 		return XR_SUCCESS;
 	}
-	if (src->left.current.active && (sub_paths->left || sub_paths->any)) {
-		*out_input = src->left.inputs;
+	if (act_attached->left.current.active &&
+	    (sub_paths->left || sub_paths->any)) {
+		*out_input = act_attached->left.inputs;
 		return XR_SUCCESS;
 	}
-	if (src->right.current.active && (sub_paths->right || sub_paths->any)) {
-		*out_input = src->right.inputs;
+	if (act_attached->right.current.active &&
+	    (sub_paths->right || sub_paths->any)) {
+		*out_input = act_attached->right.inputs;
 		return XR_SUCCESS;
 	}
-	if (src->gamepad.current.active &&
+	if (act_attached->gamepad.current.active &&
 	    (sub_paths->gamepad || sub_paths->any)) {
-		*out_input = src->gamepad.inputs;
+		*out_input = act_attached->gamepad.inputs;
 		return XR_SUCCESS;
 	}
-	if (src->user.current.active && (sub_paths->user || sub_paths->any)) {
-		*out_input = src->user.inputs;
+	if (act_attached->user.current.active &&
+	    (sub_paths->user || sub_paths->any)) {
+		*out_input = act_attached->user.inputs;
 		return XR_SUCCESS;
 	}
 
@@ -326,7 +484,7 @@ oxr_source_get_pose_input(struct oxr_logger *log,
 static bool
 do_inputs(struct oxr_binding *bind,
           struct xrt_device *xdev,
-          struct oxr_source_input inputs[16],
+          struct oxr_action_input inputs[16],
           uint32_t *num_inputs)
 {
 	struct xrt_input *input = NULL;
@@ -347,7 +505,7 @@ do_inputs(struct oxr_binding *bind,
 static bool
 do_outputs(struct oxr_binding *bind,
            struct xrt_device *xdev,
-           struct oxr_source_output outputs[16],
+           struct oxr_action_output outputs[16],
            uint32_t *num_outputs)
 {
 	struct xrt_output *output = NULL;
@@ -365,18 +523,22 @@ do_outputs(struct oxr_binding *bind,
 	return found;
 }
 
+/*!
+ * Delegate to @ref do_outputs or @ref do_inputs depending on whether the action
+ * is output or input.
+ */
 static bool
 do_io_bindings(struct oxr_binding *b,
                struct oxr_action *act,
                struct xrt_device *xdev,
-               struct oxr_source_input inputs[16],
+               struct oxr_action_input inputs[16],
                uint32_t *num_inputs,
-               struct oxr_source_output outputs[16],
+               struct oxr_action_output outputs[16],
                uint32_t *num_outputs)
 {
 	bool found = false;
 
-	if (act->action_type == XR_ACTION_TYPE_VIBRATION_OUTPUT) {
+	if (act->data->action_type == XR_ACTION_TYPE_VIBRATION_OUTPUT) {
 		found |= do_outputs(b, xdev, outputs, num_outputs);
 	} else {
 		found |= do_inputs(b, xdev, inputs, num_inputs);
@@ -396,16 +558,16 @@ ends_with(const char *str, const char *suffix)
 }
 
 static void
-oxr_source_cache_determine_redirect(struct oxr_logger *log,
+oxr_action_cache_determine_redirect(struct oxr_logger *log,
                                     struct oxr_session *sess,
                                     struct oxr_action *act,
-                                    struct oxr_source_cache *cache,
+                                    struct oxr_action_cache *cache,
                                     XrPath bound_path)
 {
 
 	cache->redirect = INPUT_REDIRECT_DEFAULT;
 
-	struct oxr_source_input *input = &cache->inputs[0];
+	struct oxr_action_input *input = &cache->inputs[0];
 	if (input == NULL)
 		return;
 
@@ -418,7 +580,7 @@ oxr_source_cache_determine_redirect(struct oxr_logger *log,
 	// trackpad/thumbstick data is kept in vec2f values.
 	// When a float action binds to ../trackpad/x or ../thumbstick/y, store
 	// the information which vec2f component to read.
-	if (act->action_type == XR_ACTION_TYPE_FLOAT_INPUT &&
+	if (act->data->action_type == XR_ACTION_TYPE_FLOAT_INPUT &&
 	    t == XRT_INPUT_TYPE_VEC2_MINUS_ONE_TO_ONE) {
 		if (ends_with(str, "/x")) {
 			cache->redirect = INPUT_REDIRECT_VEC2_X_TO_VEC1;
@@ -428,7 +590,7 @@ oxr_source_cache_determine_redirect(struct oxr_logger *log,
 			oxr_log(log,
 			        "No rule to get float from vec2f for action "
 			        "%s, binding %s\n",
-			        act->name, str);
+			        act->data->name, str);
 		}
 	}
 }
@@ -438,7 +600,7 @@ get_matched_xrpath(struct oxr_binding *b, struct oxr_action *act)
 {
 	XrPath preferred_path = XR_NULL_PATH;
 	for (uint32_t i = 0; i < b->num_keys; i++) {
-		if (b->keys[i] == act->key) {
+		if (b->keys[i] == act->act_key) {
 			uint32_t preferred_path_index = XR_NULL_PATH;
 			preferred_path_index =
 			    b->preferred_binding_path_index[i];
@@ -456,9 +618,9 @@ get_binding(struct oxr_logger *log,
             struct oxr_action *act,
             struct oxr_interaction_profile *profile,
             enum oxr_sub_action_path sub_path,
-            struct oxr_source_input inputs[16],
+            struct oxr_action_input inputs[16],
             uint32_t *num_inputs,
-            struct oxr_source_output outputs[16],
+            struct oxr_action_output outputs[16],
             uint32_t *num_outputs,
             XrPath *bound_path)
 {
@@ -512,7 +674,7 @@ get_binding(struct oxr_logger *log,
 	oxr_slog(slog, "\t\tProfile: %s\n", profile_str);
 
 	size_t num = 0;
-	oxr_binding_find_bindings_from_key(log, profile, act->key, bindings,
+	oxr_binding_find_bindings_from_key(log, profile, act->act_key, bindings,
 	                                   &num);
 	if (num == 0) {
 		oxr_slog(slog, "\t\tNo bindings\n");
@@ -547,120 +709,56 @@ get_binding(struct oxr_logger *log,
 }
 
 
-/*
- *
- * Source set functions
- *
- */
-
-static XrResult
-oxr_source_set_destroy_cb(struct oxr_logger *log, struct oxr_handle_base *hb)
-{
-	//! @todo Move to oxr_objects.h
-	struct oxr_source_set *src_set = (struct oxr_source_set *)hb;
-
-	free(src_set);
-
-	return XR_SUCCESS;
-}
-
-static XrResult
-oxr_source_set_create(struct oxr_logger *log,
-                      struct oxr_session *sess,
-                      struct oxr_action_set *act_set,
-                      struct oxr_source_set **out_src_set)
-{
-	struct oxr_source_set *src_set = NULL;
-	OXR_ALLOCATE_HANDLE_OR_RETURN(log, src_set, OXR_XR_DEBUG_SOURCESET,
-	                              oxr_source_set_destroy_cb, &sess->handle);
-
-	src_set->sess = sess;
-	u_hashmap_int_insert(sess->act_sets, act_set->key, src_set);
-
-	src_set->next = sess->src_set_list;
-	sess->src_set_list = src_set;
-
-	*out_src_set = src_set;
 
-	return XR_SUCCESS;
-}
-
-
-/*
- *
- * Source functions
- *
+/*!
+ * @public @memberof oxr_action_attachment
  */
-
 static XrResult
-oxr_source_destroy_cb(struct oxr_logger *log, struct oxr_handle_base *hb)
+oxr_action_attachment_bind(struct oxr_logger *log,
+                           struct oxr_action_attachment *act_attached,
+                           struct oxr_action *act,
+                           struct oxr_interaction_profile *head,
+                           struct oxr_interaction_profile *left,
+                           struct oxr_interaction_profile *right,
+                           struct oxr_interaction_profile *gamepad)
 {
-	//! @todo Move to oxr_objects.h
-	struct oxr_source *src = (struct oxr_source *)hb;
-
-	free(src->user.inputs);
-	free(src->user.outputs);
-	free(src->head.inputs);
-	free(src->head.outputs);
-	free(src->left.inputs);
-	free(src->left.outputs);
-	free(src->right.inputs);
-	free(src->right.outputs);
-	free(src->gamepad.inputs);
-	free(src->gamepad.outputs);
-	free(src);
-
-	return XR_SUCCESS;
-}
-
-static XrResult
-oxr_source_create(struct oxr_logger *log,
-                  struct oxr_source_set *src_set,
-                  struct oxr_action *act,
-                  struct oxr_interaction_profile *head,
-                  struct oxr_interaction_profile *left,
-                  struct oxr_interaction_profile *right,
-                  struct oxr_interaction_profile *gamepad)
-{
-	struct oxr_session *sess = src_set->sess;
-	struct oxr_source *src = NULL;
 	struct oxr_sink_logger slog = {0};
-	OXR_ALLOCATE_HANDLE_OR_RETURN(log, src, OXR_XR_DEBUG_SOURCE,
-	                              oxr_source_destroy_cb, &src_set->handle);
-
-	u_hashmap_int_insert(src_set->sess->sources, act->key, src);
-
-	// Need to copy this.
-	src->action_type = act->action_type;
+	struct oxr_action_ref *act_ref = act->data;
+	struct oxr_session *sess = act_attached->sess;
 
 	// Start logging into a single buffer.
-	oxr_slog(&slog, ": Binding %s/%s\n", act->act_set->name, act->name);
+	oxr_slog(&slog, ": Binding %s/%s\n", act->act_set->data->name,
+	         act_ref->name);
 
-	if (act->sub_paths.user || act->sub_paths.any) {
+	if (act_ref->sub_paths.user || act_ref->sub_paths.any) {
 #if 0
-		oxr_source_bind_inputs(log, slog, sess, act, &src->user, user,
+		oxr_action_bind_inputs(log, slog, sess, act, &act_attached->user, user,
 		                       OXR_SUB_ACTION_PATH_USER);
 #endif
 	}
 
-	if (act->sub_paths.head || act->sub_paths.any) {
-		oxr_source_bind_inputs(log, &slog, sess, act, &src->head, head,
+	if (act_ref->sub_paths.head || act_ref->sub_paths.any) {
+		oxr_action_bind_inputs(log, &slog, sess, act,
+		                       &act_attached->head, head,
 		                       OXR_SUB_ACTION_PATH_HEAD);
 	}
 
-	if (act->sub_paths.left || act->sub_paths.any) {
-		oxr_source_bind_inputs(log, &slog, sess, act, &src->left, left,
+	if (act_ref->sub_paths.left || act_ref->sub_paths.any) {
+		oxr_action_bind_inputs(log, &slog, sess, act,
+		                       &act_attached->left, left,
 		                       OXR_SUB_ACTION_PATH_LEFT);
 	}
 
-	if (act->sub_paths.right || act->sub_paths.any) {
-		oxr_source_bind_inputs(log, &slog, sess, act, &src->right,
-		                       right, OXR_SUB_ACTION_PATH_RIGHT);
+	if (act_ref->sub_paths.right || act_ref->sub_paths.any) {
+		oxr_action_bind_inputs(log, &slog, sess, act,
+		                       &act_attached->right, right,
+		                       OXR_SUB_ACTION_PATH_RIGHT);
 	}
 
-	if (act->sub_paths.gamepad || act->sub_paths.any) {
-		oxr_source_bind_inputs(log, &slog, sess, act, &src->gamepad,
-		                       gamepad, OXR_SUB_ACTION_PATH_GAMEPAD);
+	if (act_ref->sub_paths.gamepad || act_ref->sub_paths.any) {
+		oxr_action_bind_inputs(log, &slog, sess, act,
+		                       &act_attached->gamepad, gamepad,
+		                       OXR_SUB_ACTION_PATH_GAMEPAD);
 	}
 
 	oxr_slog(&slog, "\tDone");
@@ -676,9 +774,9 @@ oxr_source_create(struct oxr_logger *log,
 }
 
 static void
-oxr_source_cache_stop_output(struct oxr_logger *log,
+oxr_action_cache_stop_output(struct oxr_logger *log,
                              struct oxr_session *sess,
-                             struct oxr_source_cache *cache)
+                             struct oxr_action_cache *cache)
 {
 	// Set this as stopped.
 	cache->stop_output_time = 0;
@@ -686,7 +784,7 @@ oxr_source_cache_stop_output(struct oxr_logger *log,
 	union xrt_output_value value = {0};
 
 	for (uint32_t i = 0; i < cache->num_outputs; i++) {
-		struct oxr_source_output *output = &cache->outputs[i];
+		struct oxr_action_output *output = &cache->outputs[i];
 		struct xrt_device *xdev = output->xdev;
 
 		xrt_device_set_output(xdev, output->name, &value);
@@ -694,17 +792,17 @@ oxr_source_cache_stop_output(struct oxr_logger *log,
 }
 
 static void
-oxr_source_cache_update(struct oxr_logger *log,
+oxr_action_cache_update(struct oxr_logger *log,
                         struct oxr_session *sess,
-                        struct oxr_source_cache *cache,
+                        struct oxr_action_cache *cache,
                         int64_t time,
                         bool selected)
 {
-	struct oxr_source_state last = cache->current;
+	struct oxr_action_state last = cache->current;
 
 	if (!selected) {
 		if (cache->stop_output_time > 0) {
-			oxr_source_cache_stop_output(log, sess, cache);
+			oxr_action_cache_stop_output(log, sess, cache);
 		}
 		U_ZERO(&cache->current);
 		return;
@@ -713,7 +811,7 @@ oxr_source_cache_update(struct oxr_logger *log,
 	if (cache->num_outputs > 0) {
 		cache->current.active = true;
 		if (cache->stop_output_time < time) {
-			oxr_source_cache_stop_output(log, sess, cache);
+			oxr_action_cache_stop_output(log, sess, cache);
 		}
 	}
 
@@ -744,31 +842,30 @@ oxr_source_cache_update(struct oxr_logger *log,
 		switch (XRT_GET_INPUT_TYPE(input->name)) {
 		case XRT_INPUT_TYPE_VEC1_ZERO_TO_ONE:
 		case XRT_INPUT_TYPE_VEC1_MINUS_ONE_TO_ONE: {
-			changed = (input->value.vec1.x != last.vec1.x);
-			cache->current.vec1.x = input->value.vec1.x;
+			changed = (input->value.vec1.x != last.value.vec1.x);
+			cache->current.value = input->value;
 			break;
 		}
 		case XRT_INPUT_TYPE_VEC2_MINUS_ONE_TO_ONE: {
-			changed = (input->value.vec2.x != last.vec2.x) ||
-			          (input->value.vec2.y != last.vec2.y);
-			cache->current.vec2.x = input->value.vec2.x;
-			cache->current.vec2.y = input->value.vec2.y;
+			changed = (input->value.vec2.x != last.value.vec2.x) ||
+			          (input->value.vec2.y != last.value.vec2.y);
+			cache->current.value = input->value;
 			break;
 		}
 #if 0
 		case XRT_INPUT_TYPE_VEC3_MINUS_ONE_TO_ONE: {
-			changed = (input->value.vec3.x != last.vec3.x) ||
-			          (input->value.vec3.y != last.vec3.y) ||
-			          (input->value.vec3.z != last.vec3.z);
-			cache->current.vec3.x = input->value.vec3.x;
-			cache->current.vec3.y = input->value.vec3.y;
-			cache->current.vec3.z = input->value.vec3.z;
+			changed = (input->value.vec3.x != last.value.vec3.x) ||
+			          (input->value.vec3.y != last.value.vec3.y) ||
+			          (input->value.vec3.z != last.value.vec3.z);
+			cache->current.value.vec3.x = input->value.vec3.x;
+			cache->current.value.vec3.y = input->value.vec3.y;
+			cache->current.value.vec3.z = input->value.vec3.z;
 			break;
 		}
 #endif
 		case XRT_INPUT_TYPE_BOOLEAN: {
-			changed = (input->value.boolean != last.boolean);
-			cache->current.boolean = input->value.boolean;
+			changed = (input->value.boolean != last.value.boolean);
+			cache->current.value.boolean = input->value.boolean;
 			break;
 		}
 		case XRT_INPUT_TYPE_POSE: return;
@@ -791,42 +888,42 @@ oxr_source_cache_update(struct oxr_logger *log,
 }
 
 #define BOOL_CHECK(NAME)                                                       \
-	if (src->NAME.current.active) {                                        \
+	if (act_attached->NAME.current.active) {                               \
 		active |= true;                                                \
-		value |= src->NAME.current.boolean;                            \
-		timestamp = src->NAME.current.timestamp;                       \
+		value |= act_attached->NAME.current.value.boolean;             \
+		timestamp = act_attached->NAME.current.timestamp;              \
 	}
 #define VEC1_CHECK(NAME)                                                       \
-	if (src->NAME.current.active) {                                        \
+	if (act_attached->NAME.current.active) {                               \
 		active |= true;                                                \
-		if (value < src->NAME.current.vec1.x) {                        \
-			value = src->NAME.current.vec1.x;                      \
-			timestamp = src->NAME.current.timestamp;               \
+		if (value < act_attached->NAME.current.value.vec1.x) {         \
+			value = act_attached->NAME.current.value.vec1.x;       \
+			timestamp = act_attached->NAME.current.timestamp;      \
 		}                                                              \
 	}
 #define VEC2_CHECK(NAME)                                                       \
-	if (src->NAME.current.active) {                                        \
+	if (act_attached->NAME.current.active) {                               \
 		active |= true;                                                \
-		float curr_x = src->NAME.current.vec2.x;                       \
-		float curr_y = src->NAME.current.vec2.y;                       \
+		float curr_x = act_attached->NAME.current.value.vec2.x;        \
+		float curr_y = act_attached->NAME.current.value.vec2.y;        \
 		float curr_d = curr_x * curr_x + curr_y * curr_y;              \
 		if (distance < curr_d) {                                       \
 			x = curr_x;                                            \
 			y = curr_y;                                            \
 			distance = curr_d;                                     \
-			timestamp = src->NAME.current.timestamp;               \
+			timestamp = act_attached->NAME.current.timestamp;      \
 		}                                                              \
 	}
 
 static void
-oxr_source_update(struct oxr_logger *log,
-                  struct oxr_session *sess,
-                  struct oxr_source *src,
-                  int64_t time,
-                  struct oxr_sub_paths sub_paths)
+oxr_action_attachment_update(struct oxr_logger *log,
+                             struct oxr_session *sess,
+                             struct oxr_action_attachment *act_attached,
+                             int64_t time,
+                             struct oxr_sub_paths sub_paths)
 {
 	// This really shouldn't be happening.
-	if (src == NULL) {
+	if (act_attached == NULL) {
 		return;
 	}
 
@@ -839,26 +936,26 @@ oxr_source_update(struct oxr_logger *log,
 	bool select_gamepad = sub_paths.gamepad || sub_paths.any;
 
 	// clang-format off
-	oxr_source_cache_update(log, sess, &src->head, time, select_head);
-	oxr_source_cache_update(log, sess, &src->left, time, select_left);
-	oxr_source_cache_update(log, sess, &src->right, time, select_right);
-	oxr_source_cache_update(log, sess, &src->gamepad, time, select_gamepad);
+	oxr_action_cache_update(log, sess, &act_attached->head, time, select_head);
+	oxr_action_cache_update(log, sess, &act_attached->left, time, select_left);
+	oxr_action_cache_update(log, sess, &act_attached->right, time, select_right);
+	oxr_action_cache_update(log, sess, &act_attached->gamepad, time, select_gamepad);
 	// clang-format on
 
 	if (!select_any) {
-		U_ZERO(&src->any_state);
+		U_ZERO(&act_attached->any_state);
 		return;
 	}
 
 	/*
 	 * Any state.
 	 */
-	struct oxr_source_state last = src->any_state;
+	struct oxr_action_state last = act_attached->any_state;
 	bool active = false;
 	bool changed = false;
 	XrTime timestamp = 0;
 
-	switch (src->action_type) {
+	switch (act_attached->act_ref->action_type) {
 	case XR_ACTION_TYPE_BOOLEAN_INPUT: {
 		bool value = false;
 		BOOL_CHECK(user);
@@ -867,8 +964,8 @@ oxr_source_update(struct oxr_logger *log,
 		BOOL_CHECK(right);
 		BOOL_CHECK(gamepad);
 
-		changed = last.boolean != value;
-		src->any_state.boolean = value;
+		changed = (last.value.boolean != value);
+		act_attached->any_state.value.boolean = value;
 		break;
 	}
 	case XR_ACTION_TYPE_FLOAT_INPUT: {
@@ -879,8 +976,8 @@ oxr_source_update(struct oxr_logger *log,
 		VEC1_CHECK(right);
 		VEC1_CHECK(gamepad);
 
-		changed = last.vec1.x != value;
-		src->any_state.vec1.x = value;
+		changed = last.value.vec1.x != value;
+		act_attached->any_state.value.vec1.x = value;
 		break;
 	}
 	case XR_ACTION_TYPE_VECTOR2F_INPUT: {
@@ -893,9 +990,9 @@ oxr_source_update(struct oxr_logger *log,
 		VEC2_CHECK(right);
 		VEC2_CHECK(gamepad);
 
-		changed = last.vec2.x != x || last.vec2.y != y;
-		src->any_state.vec2.x = x;
-		src->any_state.vec2.y = y;
+		changed = (last.value.vec2.x != x) || (last.value.vec2.y != y);
+		act_attached->any_state.value.vec2.x = x;
+		act_attached->any_state.value.vec2.y = y;
 		break;
 	}
 	default:
@@ -907,34 +1004,34 @@ oxr_source_update(struct oxr_logger *log,
 	}
 
 	if (!active) {
-		U_ZERO(&src->any_state);
+		U_ZERO(&act_attached->any_state);
 	} else if (last.active && changed) {
-		src->any_state.timestamp = timestamp;
-		src->any_state.changed = true;
-		src->any_state.active = true;
+		act_attached->any_state.timestamp = timestamp;
+		act_attached->any_state.changed = true;
+		act_attached->any_state.active = true;
 	} else if (last.active) {
-		src->any_state.timestamp = last.timestamp;
-		src->any_state.changed = false;
-		src->any_state.active = true;
+		act_attached->any_state.timestamp = last.timestamp;
+		act_attached->any_state.changed = false;
+		act_attached->any_state.active = true;
 	} else {
-		src->any_state.timestamp = timestamp;
-		src->any_state.changed = false;
-		src->any_state.active = true;
+		act_attached->any_state.timestamp = timestamp;
+		act_attached->any_state.changed = false;
+		act_attached->any_state.active = true;
 	}
 }
 
 static void
-oxr_source_bind_inputs(struct oxr_logger *log,
+oxr_action_bind_inputs(struct oxr_logger *log,
                        struct oxr_sink_logger *slog,
                        struct oxr_session *sess,
                        struct oxr_action *act,
-                       struct oxr_source_cache *cache,
+                       struct oxr_action_cache *cache,
                        struct oxr_interaction_profile *profile,
                        enum oxr_sub_action_path sub_path)
 {
-	struct oxr_source_input inputs[16] = {0};
+	struct oxr_action_input inputs[16] = {0};
 	uint32_t num_inputs = 0;
-	struct oxr_source_output outputs[16] = {0};
+	struct oxr_action_output outputs[16] = {0};
 	uint32_t num_outputs = 0;
 
 	//! @todo Should this be asserted to be none-null?
@@ -947,7 +1044,7 @@ oxr_source_bind_inputs(struct oxr_logger *log,
 	if (num_inputs > 0) {
 		cache->current.active = true;
 		cache->inputs =
-		    U_TYPED_ARRAY_CALLOC(struct oxr_source_input, num_inputs);
+		    U_TYPED_ARRAY_CALLOC(struct oxr_action_input, num_inputs);
 		for (uint32_t i = 0; i < num_inputs; i++) {
 			cache->inputs[i] = inputs[i];
 		}
@@ -957,14 +1054,14 @@ oxr_source_bind_inputs(struct oxr_logger *log,
 	if (num_outputs > 0) {
 		cache->current.active = true;
 		cache->outputs =
-		    U_TYPED_ARRAY_CALLOC(struct oxr_source_output, num_outputs);
+		    U_TYPED_ARRAY_CALLOC(struct oxr_action_output, num_outputs);
 		for (uint32_t i = 0; i < num_outputs; i++) {
 			cache->outputs[i] = outputs[i];
 		}
 		cache->num_outputs = num_outputs;
 	}
 
-	oxr_source_cache_determine_redirect(log, sess, act, cache, bound_path);
+	oxr_action_cache_determine_redirect(log, sess, act, cache, bound_path);
 }
 
 
@@ -974,35 +1071,63 @@ oxr_source_bind_inputs(struct oxr_logger *log,
  *
  */
 
+/*!
+ * Given an Action Set handle, return the @ref oxr_action_set and the associated
+ * @ref oxr_action_set_attachment in the given Session.
+ *
+ * @private @memberof oxr_session
+ */
 static void
-oxr_session_get_source_set(struct oxr_session *sess,
-                           XrActionSet actionSet,
-                           struct oxr_source_set **src_set,
-                           struct oxr_action_set **act_set)
+oxr_session_get_action_set_attachment(
+    struct oxr_session *sess,
+    XrActionSet actionSet,
+    struct oxr_action_set_attachment **act_set_attached,
+    struct oxr_action_set **act_set)
 {
 	void *ptr = NULL;
 	*act_set =
 	    XRT_CAST_OXR_HANDLE_TO_PTR(struct oxr_action_set *, actionSet);
 
-	int ret = u_hashmap_int_find(sess->act_sets, (*act_set)->key, &ptr);
+	int ret = u_hashmap_int_find(sess->act_sets_attachments_by_key,
+	                             (*act_set)->act_set_key, &ptr);
 	if (ret == 0) {
-		*src_set = (struct oxr_source_set *)ptr;
+		*act_set_attached = (struct oxr_action_set_attachment *)ptr;
 	}
 }
 
+/*!
+ * Given an action act_key, look up the @ref oxr_action_attachment of the
+ * associated action in the given Session.
+ *
+ * @private @memberof oxr_session
+ */
 static void
-oxr_session_get_source(struct oxr_session *sess,
-                       uint32_t act_key,
-                       struct oxr_source **out_src)
+oxr_session_get_action_attachment(
+    struct oxr_session *sess,
+    uint32_t act_key,
+    struct oxr_action_attachment **out_act_attached)
 {
 	void *ptr = NULL;
 
-	int ret = u_hashmap_int_find(sess->sources, act_key, &ptr);
+	int ret =
+	    u_hashmap_int_find(sess->act_attachments_by_key, act_key, &ptr);
 	if (ret == 0) {
-		*out_src = (struct oxr_source *)ptr;
+		*out_act_attached = (struct oxr_action_attachment *)ptr;
 	}
 }
 
+static inline size_t
+oxr_handle_base_get_num_children(struct oxr_handle_base *hb)
+{
+	size_t ret = 0;
+	for (uint32_t i = 0; i < XRT_MAX_HANDLE_CHILDREN; ++i) {
+		if (hb->children[i] != NULL) {
+			++ret;
+		}
+	}
+	return ret;
+}
+
 XrResult
 oxr_session_attach_action_sets(struct oxr_logger *log,
                                struct oxr_session *sess,
@@ -1012,30 +1137,61 @@ oxr_session_attach_action_sets(struct oxr_logger *log,
 	struct oxr_interaction_profile *head = NULL;
 	struct oxr_interaction_profile *left = NULL;
 	struct oxr_interaction_profile *right = NULL;
-	struct oxr_action_set *act_set = NULL;
-	struct oxr_source_set *src_set = NULL;
-	struct oxr_action *act = NULL;
 
 	oxr_find_profile_for_device(log, inst, sess->sys->head, &head);
 	oxr_find_profile_for_device(log, inst, sess->sys->left, &left);
 	oxr_find_profile_for_device(log, inst, sess->sys->right, &right);
+	//! @todo add other subaction paths here
 
-	// Has any of the bound action sets been updated.
-	for (uint32_t i = 0; i < bindInfo->countActionSets; i++) {
-		act_set = XRT_CAST_OXR_HANDLE_TO_PTR(struct oxr_action_set *,
-		                                     bindInfo->actionSets[i]);
-		act_set->attached = true;
+	// Before allocating, make sure nothing has been attached yet.
 
-		oxr_source_set_create(log, sess, act_set, &src_set);
+	for (uint32_t i = 0; i < bindInfo->countActionSets; i++) {
+		struct oxr_action_set *act_set = XRT_CAST_OXR_HANDLE_TO_PTR(
+		    struct oxr_action_set *, bindInfo->actionSets[i]);
+		if (act_set->data->attached) {
+			return XR_ERROR_ACTIONSETS_ALREADY_ATTACHED;
+		}
+	}
 
+	// Allocate room for list.
+	sess->num_action_set_attachments = bindInfo->countActionSets;
+	sess->act_set_attachments = U_TYPED_ARRAY_CALLOC(
+	    struct oxr_action_set_attachment, sess->num_action_set_attachments);
+
+	// Set up the per-session data for these action sets.
+	for (uint32_t i = 0; i < sess->num_action_set_attachments; i++) {
+		struct oxr_action_set *act_set = XRT_CAST_OXR_HANDLE_TO_PTR(
+		    struct oxr_action_set *, bindInfo->actionSets[i]);
+		struct oxr_action_set_ref *act_set_ref = act_set->data;
+		act_set_ref->attached = true;
+		struct oxr_action_set_attachment *act_set_attached =
+		    &sess->act_set_attachments[i];
+		oxr_action_set_attachment_init(log, sess, act_set,
+		                               act_set_attached);
+
+		// Allocate the action attachments for this set.
+		act_set_attached->num_action_attachments =
+		    oxr_handle_base_get_num_children(&act_set->handle);
+		act_set_attached->act_attachments = U_TYPED_ARRAY_CALLOC(
+		    struct oxr_action_attachment,
+		    act_set_attached->num_action_attachments);
+
+		// Set up the per-session data for the actions.
+		uint32_t child_index = 0;
 		for (uint32_t k = 0; k < XRT_MAX_HANDLE_CHILDREN; k++) {
-			act = (struct oxr_action *)act_set->handle.children[k];
+			struct oxr_action *act =
+			    (struct oxr_action *)act_set->handle.children[k];
 			if (act == NULL) {
 				continue;
 			}
 
-			oxr_source_create(log, src_set, act, head, left, right,
-			                  NULL);
+			struct oxr_action_attachment *act_attached =
+			    &act_set_attached->act_attachments[child_index];
+			oxr_action_attachment_init(log, act_set_attached,
+			                           act_attached, act);
+			oxr_action_attachment_bind(log, act_attached, act, head,
+			                           left, right, NULL);
+			++child_index;
 		}
 	}
 
@@ -1061,18 +1217,18 @@ oxr_action_sync_data(struct oxr_logger *log,
                      const XrActiveActionSet *actionSets)
 {
 	struct oxr_action_set *act_set = NULL;
-	struct oxr_source_set *src_set = NULL;
+	struct oxr_action_set_attachment *act_set_attached = NULL;
 
 	// Check that all action sets has been attached.
 	for (uint32_t i = 0; i < countActionSets; i++) {
-		oxr_session_get_source_set(sess, actionSets[i].actionSet,
-		                           &src_set, &act_set);
-		if (src_set == NULL) {
+		oxr_session_get_action_set_attachment(
+		    sess, actionSets[i].actionSet, &act_set_attached, &act_set);
+		if (act_set_attached == NULL) {
 			return oxr_error(
 			    log, XR_ERROR_ACTIONSET_NOT_ATTACHED,
 			    "(actionSets[%i].actionSet) action set '%s' has "
 			    "not been attached to this session",
-			    i, act_set != NULL ? act_set->name : "NULL");
+			    i, act_set != NULL ? act_set->data->name : "NULL");
 		}
 	}
 
@@ -1084,55 +1240,54 @@ oxr_action_sync_data(struct oxr_logger *log,
 		oxr_xdev_update(sess->sys->xdevs[i]);
 	}
 
-	// Reset all requested source sets.
-	src_set = sess->src_set_list;
-	while (src_set != NULL) {
-		U_ZERO(&src_set->requested_sub_paths);
-
-		// Grab the next one.
-		src_set = src_set->next;
+	// Reset all action set attachments.
+	for (size_t i = 0; i < sess->num_action_set_attachments; ++i) {
+		act_set_attached = &sess->act_set_attachments[i];
+		U_ZERO(&act_set_attached->requested_sub_paths);
 	}
 
-	// Go over all action sets and update them.
+	// Go over all requested action sets and update their attachment.
+	//! @todo can be listed more than once with different paths!
 	for (uint32_t i = 0; i < countActionSets; i++) {
 		struct oxr_sub_paths sub_paths;
-		oxr_session_get_source_set(sess, actionSets[i].actionSet,
-		                           &src_set, &act_set);
-		assert(src_set != NULL);
-
-		oxr_classify_sub_action_paths(log, sess->sys->inst, 1,
-		                              &actionSets[i].subactionPath,
-		                              &sub_paths);
+		oxr_session_get_action_set_attachment(
+		    sess, actionSets[i].actionSet, &act_set_attached, &act_set);
+		assert(act_set_attached != NULL);
+
+		if (!oxr_classify_sub_action_paths(log, sess->sys->inst, 1,
+		                                   &actionSets[i].subactionPath,
+		                                   &sub_paths)) {
+			return XR_ERROR_PATH_UNSUPPORTED;
+		}
 
-		src_set->requested_sub_paths.any |= sub_paths.any;
-		src_set->requested_sub_paths.user |= sub_paths.user;
-		src_set->requested_sub_paths.head |= sub_paths.head;
-		src_set->requested_sub_paths.left |= sub_paths.left;
-		src_set->requested_sub_paths.right |= sub_paths.right;
-		src_set->requested_sub_paths.gamepad |= sub_paths.gamepad;
+		act_set_attached->requested_sub_paths.any |= sub_paths.any;
+		act_set_attached->requested_sub_paths.user |= sub_paths.user;
+		act_set_attached->requested_sub_paths.head |= sub_paths.head;
+		act_set_attached->requested_sub_paths.left |= sub_paths.left;
+		act_set_attached->requested_sub_paths.right |= sub_paths.right;
+		act_set_attached->requested_sub_paths.gamepad |=
+		    sub_paths.gamepad;
 	}
 
-	// Reset all source sets.
-	src_set = sess->src_set_list;
-	while (src_set != NULL) {
-		struct oxr_sub_paths sub_paths = src_set->requested_sub_paths;
+	// Now, update all action attachments
+	for (size_t i = 0; i < sess->num_action_set_attachments; ++i) {
+		act_set_attached = &sess->act_set_attachments[i];
+		struct oxr_sub_paths sub_paths =
+		    act_set_attached->requested_sub_paths;
 
 
-		for (uint32_t k = 0; k < XRT_MAX_HANDLE_CHILDREN; k++) {
-			// This assumes that all children of a
-			// source set are actions.
-			struct oxr_source *src =
-			    (struct oxr_source *)src_set->handle.children[k];
+		for (uint32_t k = 0;
+		     k < act_set_attached->num_action_attachments; k++) {
+			struct oxr_action_attachment *act_attached =
+			    &act_set_attached->act_attachments[k];
 
-			if (src == NULL) {
+			if (act_attached == NULL) {
 				continue;
 			}
 
-			oxr_source_update(log, sess, src, now, sub_paths);
+			oxr_action_attachment_update(log, sess, act_attached,
+			                             now, sub_paths);
 		}
-
-		// Grab the next one.
-		src_set = src_set->next;
 	}
 
 
@@ -1147,30 +1302,30 @@ oxr_action_sync_data(struct oxr_logger *log,
  */
 
 static void
-get_state_from_state_bool(struct oxr_source_state *state,
+get_state_from_state_bool(struct oxr_action_state *state,
                           XrActionStateBoolean *data,
                           enum xrt_source_value_redirect redirect)
 {
-	data->currentState = state->boolean;
+	data->currentState = state->value.boolean;
 	data->lastChangeTime = state->timestamp;
 	data->changedSinceLastSync = state->changed;
 	data->isActive = XR_TRUE;
 }
 
 static void
-get_state_from_state_vec1(struct oxr_source_state *state,
+get_state_from_state_vec1(struct oxr_action_state *state,
                           XrActionStateFloat *data,
                           enum xrt_source_value_redirect redirect)
 {
 	switch (redirect) {
 	case INPUT_REDIRECT_VEC2_X_TO_VEC1:
-		data->currentState = state->vec2.x;
+		data->currentState = state->value.vec2.x;
 		break;
 	case INPUT_REDIRECT_VEC2_Y_TO_VEC1:
-		data->currentState = state->vec2.y;
+		data->currentState = state->value.vec2.y;
 		break;
 	case INPUT_REDIRECT_DEFAULT:
-	default: data->currentState = state->vec1.x; break;
+	default: data->currentState = state->value.vec1.x; break;
 	}
 
 	data->lastChangeTime = state->timestamp;
@@ -1179,55 +1334,57 @@ get_state_from_state_vec1(struct oxr_source_state *state,
 }
 
 static void
-get_state_from_state_vec2(struct oxr_source_state *state,
+get_state_from_state_vec2(struct oxr_action_state *state,
                           XrActionStateVector2f *data,
                           enum xrt_source_value_redirect redirect)
 {
-	data->currentState.x = state->vec2.x;
-	data->currentState.y = state->vec2.y;
+	data->currentState.x = state->value.vec2.x;
+	data->currentState.y = state->value.vec2.y;
 	data->lastChangeTime = state->timestamp;
 	data->changedSinceLastSync = state->changed;
 	data->isActive = XR_TRUE;
 }
 
 #define OXR_ACTION_GET_FILLER(TYPE)                                            \
-	if (sub_paths.any && src->any_state.active) {                          \
-		get_state_from_state_##TYPE(&src->any_state, data,             \
+	if (sub_paths.any && act_attached->any_state.active) {                 \
+		get_state_from_state_##TYPE(&act_attached->any_state, data,    \
 		                            INPUT_REDIRECT_DEFAULT);           \
 	}                                                                      \
-	if (sub_paths.user && src->user.current.active) {                      \
-		get_state_from_state_##TYPE(&src->user.current, data,          \
-		                            src->user.redirect);               \
+	if (sub_paths.user && act_attached->user.current.active) {             \
+		get_state_from_state_##TYPE(&act_attached->user.current, data, \
+		                            act_attached->user.redirect);      \
 	}                                                                      \
-	if (sub_paths.head && src->head.current.active) {                      \
-		get_state_from_state_##TYPE(&src->head.current, data,          \
-		                            src->head.redirect);               \
+	if (sub_paths.head && act_attached->head.current.active) {             \
+		get_state_from_state_##TYPE(&act_attached->head.current, data, \
+		                            act_attached->head.redirect);      \
 	}                                                                      \
-	if (sub_paths.left && src->left.current.active) {                      \
-		get_state_from_state_##TYPE(&src->left.current, data,          \
-		                            src->left.redirect);               \
+	if (sub_paths.left && act_attached->left.current.active) {             \
+		get_state_from_state_##TYPE(&act_attached->left.current, data, \
+		                            act_attached->left.redirect);      \
 	}                                                                      \
-	if (sub_paths.right && src->right.current.active) {                    \
-		get_state_from_state_##TYPE(&src->right.current, data,         \
-		                            src->right.redirect);              \
+	if (sub_paths.right && act_attached->right.current.active) {           \
+		get_state_from_state_##TYPE(&act_attached->right.current,      \
+		                            data,                              \
+		                            act_attached->right.redirect);     \
 	}                                                                      \
-	if (sub_paths.gamepad && src->gamepad.current.active) {                \
-		get_state_from_state_##TYPE(&src->gamepad.current, data,       \
-		                            src->gamepad.redirect);            \
+	if (sub_paths.gamepad && act_attached->gamepad.current.active) {       \
+		get_state_from_state_##TYPE(&act_attached->gamepad.current,    \
+		                            data,                              \
+		                            act_attached->gamepad.redirect);   \
 	}
 
 
 XrResult
 oxr_action_get_boolean(struct oxr_logger *log,
                        struct oxr_session *sess,
-                       uint64_t key,
+                       uint32_t act_key,
                        struct oxr_sub_paths sub_paths,
                        XrActionStateBoolean *data)
 {
-	struct oxr_source *src = NULL;
+	struct oxr_action_attachment *act_attached = NULL;
 
-	oxr_session_get_source(sess, key, &src);
-	if (src == NULL) {
+	oxr_session_get_action_attachment(sess, act_key, &act_attached);
+	if (act_attached == NULL) {
 		return oxr_error(
 		    log, XR_ERROR_ACTIONSET_NOT_ATTACHED,
 		    "Action has not been attached to this session");
@@ -1245,14 +1402,14 @@ oxr_action_get_boolean(struct oxr_logger *log,
 XrResult
 oxr_action_get_vector1f(struct oxr_logger *log,
                         struct oxr_session *sess,
-                        uint64_t key,
+                        uint32_t act_key,
                         struct oxr_sub_paths sub_paths,
                         XrActionStateFloat *data)
 {
-	struct oxr_source *src = NULL;
+	struct oxr_action_attachment *act_attached = NULL;
 
-	oxr_session_get_source(sess, key, &src);
-	if (src == NULL) {
+	oxr_session_get_action_attachment(sess, act_key, &act_attached);
+	if (act_attached == NULL) {
 		return oxr_error(
 		    log, XR_ERROR_ACTIONSET_NOT_ATTACHED,
 		    "Action has not been attached to this session");
@@ -1269,14 +1426,14 @@ oxr_action_get_vector1f(struct oxr_logger *log,
 XrResult
 oxr_action_get_vector2f(struct oxr_logger *log,
                         struct oxr_session *sess,
-                        uint64_t key,
+                        uint32_t act_key,
                         struct oxr_sub_paths sub_paths,
                         XrActionStateVector2f *data)
 {
-	struct oxr_source *src = NULL;
+	struct oxr_action_attachment *act_attached = NULL;
 
-	oxr_session_get_source(sess, key, &src);
-	if (src == NULL) {
+	oxr_session_get_action_attachment(sess, act_key, &act_attached);
+	if (act_attached == NULL) {
 		return oxr_error(
 		    log, XR_ERROR_ACTIONSET_NOT_ATTACHED,
 		    "Action has not been attached to this session");
@@ -1293,14 +1450,14 @@ oxr_action_get_vector2f(struct oxr_logger *log,
 XrResult
 oxr_action_get_pose(struct oxr_logger *log,
                     struct oxr_session *sess,
-                    uint64_t key,
+                    uint32_t act_key,
                     struct oxr_sub_paths sub_paths,
                     XrActionStatePose *data)
 {
-	struct oxr_source *src = NULL;
+	struct oxr_action_attachment *act_attached = NULL;
 
-	oxr_session_get_source(sess, key, &src);
-	if (src == NULL) {
+	oxr_session_get_action_attachment(sess, act_key, &act_attached);
+	if (act_attached == NULL) {
 		return oxr_error(
 		    log, XR_ERROR_ACTIONSET_NOT_ATTACHED,
 		    "Action has not been attached to this session");
@@ -1309,19 +1466,19 @@ oxr_action_get_pose(struct oxr_logger *log,
 	data->isActive = XR_FALSE;
 
 	if (sub_paths.user || sub_paths.any) {
-		data->isActive |= src->user.current.active;
+		data->isActive |= act_attached->user.current.active;
 	}
 	if (sub_paths.head || sub_paths.any) {
-		data->isActive |= src->head.current.active;
+		data->isActive |= act_attached->head.current.active;
 	}
 	if (sub_paths.left || sub_paths.any) {
-		data->isActive |= src->left.current.active;
+		data->isActive |= act_attached->left.current.active;
 	}
 	if (sub_paths.right || sub_paths.any) {
-		data->isActive |= src->right.current.active;
+		data->isActive |= act_attached->right.current.active;
 	}
 	if (sub_paths.gamepad || sub_paths.any) {
-		data->isActive |= src->gamepad.current.active;
+		data->isActive |= act_attached->gamepad.current.active;
 	}
 
 	return oxr_session_success_result(sess);
@@ -1335,8 +1492,8 @@ oxr_action_get_pose(struct oxr_logger *log,
  */
 
 static void
-set_source_output_vibration(struct oxr_session *sess,
-                            struct oxr_source_cache *cache,
+set_action_output_vibration(struct oxr_session *sess,
+                            struct oxr_action_cache *cache,
                             int64_t stop,
                             const XrHapticVibration *data)
 {
@@ -1348,7 +1505,7 @@ set_source_output_vibration(struct oxr_session *sess,
 	value.vibration.duration = data->duration;
 
 	for (uint32_t i = 0; i < cache->num_outputs; i++) {
-		struct oxr_source_output *output = &cache->outputs[i];
+		struct oxr_action_output *output = &cache->outputs[i];
 		struct xrt_device *xdev = output->xdev;
 
 		xrt_device_set_output(xdev, output->name, &value);
@@ -1360,14 +1517,14 @@ set_source_output_vibration(struct oxr_session *sess,
 XrResult
 oxr_action_apply_haptic_feedback(struct oxr_logger *log,
                                  struct oxr_session *sess,
-                                 uint64_t key,
+                                 uint32_t act_key,
                                  struct oxr_sub_paths sub_paths,
                                  const XrHapticBaseHeader *hapticEvent)
 {
-	struct oxr_source *src = NULL;
+	struct oxr_action_attachment *act_attached = NULL;
 
-	oxr_session_get_source(sess, key, &src);
-	if (src == NULL) {
+	oxr_session_get_action_attachment(sess, act_key, &act_attached);
+	if (act_attached == NULL) {
 		return oxr_error(
 		    log, XR_ERROR_ACTIONSET_NOT_ATTACHED,
 		    "Action has not been attached to this session");
@@ -1379,20 +1536,20 @@ oxr_action_apply_haptic_feedback(struct oxr_logger *log,
 	int64_t stop = data->duration <= 0 ? now : now + data->duration;
 
 	// clang-format off
-	if (src->user.current.active && (sub_paths.user || sub_paths.any)) {
-		set_source_output_vibration(sess, &src->user, stop, data);
+	if (act_attached->user.current.active && (sub_paths.user || sub_paths.any)) {
+		set_action_output_vibration(sess, &act_attached->user, stop, data);
 	}
-	if (src->head.current.active && (sub_paths.head || sub_paths.any)) {
-		set_source_output_vibration(sess, &src->head, stop, data);
+	if (act_attached->head.current.active && (sub_paths.head || sub_paths.any)) {
+		set_action_output_vibration(sess, &act_attached->head, stop, data);
 	}
-	if (src->left.current.active && (sub_paths.left || sub_paths.any)) {
-		set_source_output_vibration(sess, &src->left, stop, data);
+	if (act_attached->left.current.active && (sub_paths.left || sub_paths.any)) {
+		set_action_output_vibration(sess, &act_attached->left, stop, data);
 	}
-	if (src->right.current.active && (sub_paths.right || sub_paths.any)) {
-		set_source_output_vibration(sess, &src->right, stop, data);
+	if (act_attached->right.current.active && (sub_paths.right || sub_paths.any)) {
+		set_action_output_vibration(sess, &act_attached->right, stop, data);
 	}
-	if (src->gamepad.current.active && (sub_paths.gamepad || sub_paths.any)) {
-		set_source_output_vibration(sess, &src->gamepad, stop, data);
+	if (act_attached->gamepad.current.active && (sub_paths.gamepad || sub_paths.any)) {
+		set_action_output_vibration(sess, &act_attached->gamepad, stop, data);
 	}
 	// clang-format on
 
@@ -1402,33 +1559,33 @@ oxr_action_apply_haptic_feedback(struct oxr_logger *log,
 XrResult
 oxr_action_stop_haptic_feedback(struct oxr_logger *log,
                                 struct oxr_session *sess,
-                                uint64_t key,
+                                uint32_t act_key,
                                 struct oxr_sub_paths sub_paths)
 {
-	struct oxr_source *src = NULL;
+	struct oxr_action_attachment *act_attached = NULL;
 
-	oxr_session_get_source(sess, key, &src);
-	if (src == NULL) {
+	oxr_session_get_action_attachment(sess, act_key, &act_attached);
+	if (act_attached == NULL) {
 		return oxr_error(
 		    log, XR_ERROR_ACTIONSET_NOT_ATTACHED,
 		    "Action has not been attached to this session");
 	}
 
 	// clang-format off
-	if (src->user.current.active && (sub_paths.user || sub_paths.any)) {
-		oxr_source_cache_stop_output(log, sess, &src->user);
+	if (act_attached->user.current.active && (sub_paths.user || sub_paths.any)) {
+		oxr_action_cache_stop_output(log, sess, &act_attached->user);
 	}
-	if (src->head.current.active && (sub_paths.head || sub_paths.any)) {
-		oxr_source_cache_stop_output(log, sess, &src->head);
+	if (act_attached->head.current.active && (sub_paths.head || sub_paths.any)) {
+		oxr_action_cache_stop_output(log, sess, &act_attached->head);
 	}
-	if (src->left.current.active && (sub_paths.left || sub_paths.any)) {
-		oxr_source_cache_stop_output(log, sess, &src->left);
+	if (act_attached->left.current.active && (sub_paths.left || sub_paths.any)) {
+		oxr_action_cache_stop_output(log, sess, &act_attached->left);
 	}
-	if (src->right.current.active && (sub_paths.right || sub_paths.any)) {
-		oxr_source_cache_stop_output(log, sess, &src->right);
+	if (act_attached->right.current.active && (sub_paths.right || sub_paths.any)) {
+		oxr_action_cache_stop_output(log, sess, &act_attached->right);
 	}
-	if (src->gamepad.current.active && (sub_paths.gamepad || sub_paths.any)) {
-		oxr_source_cache_stop_output(log, sess, &src->gamepad);
+	if (act_attached->gamepad.current.active && (sub_paths.gamepad || sub_paths.any)) {
+		oxr_action_cache_stop_output(log, sess, &act_attached->gamepad);
 	}
 	// clang-format on
 
diff --git a/src/xrt/state_trackers/oxr/oxr_instance.c b/src/xrt/state_trackers/oxr/oxr_instance.c
index 7c651b6f65d215315428dd7a681569bb6bc003ed..6c25fb8cfca03cce75ecebade64467d49a565dfa 100644
--- a/src/xrt/state_trackers/oxr/oxr_instance.c
+++ b/src/xrt/state_trackers/oxr/oxr_instance.c
@@ -154,6 +154,7 @@ oxr_instance_create(struct oxr_logger *log,
 	cache_path(log, inst, "/user/hand/left", &inst->path_cache.left);
 	cache_path(log, inst, "/user/hand/right", &inst->path_cache.right);
 	cache_path(log, inst, "/user/gamepad", &inst->path_cache.gamepad);
+	cache_path(log, inst, "/user/treadmill", &inst->path_cache.treadmill);
 	cache_path(log, inst, "/interaction_profiles/khr/simple_controller", &inst->path_cache.khr_simple_controller);
 	cache_path(log, inst, "/interaction_profiles/google/daydream_controller", &inst->path_cache.google_daydream_controller);
 	cache_path(log, inst, "/interaction_profiles/htc/vive_controller", &inst->path_cache.htc_vive_controller);
diff --git a/src/xrt/state_trackers/oxr/oxr_objects.h b/src/xrt/state_trackers/oxr/oxr_objects.h
index fbc32daac4f115f8e19315a8396a52014b26ce5e..28ad48adc2eef55a2e382f6c048144a4d3583d6c 100644
--- a/src/xrt/state_trackers/oxr/oxr_objects.h
+++ b/src/xrt/state_trackers/oxr/oxr_objects.h
@@ -105,12 +105,14 @@ struct oxr_action;
 struct oxr_debug_messenger;
 struct oxr_handle_base;
 struct oxr_sub_paths;
-struct oxr_source;
-struct oxr_source_set;
-struct oxr_source_input;
-struct oxr_source_output;
+struct oxr_action_attachment;
+struct oxr_action_set_attachment;
+struct oxr_action_input;
+struct oxr_action_output;
 struct oxr_binding;
 struct oxr_interaction_profile;
+struct oxr_action_set_ref;
+struct oxr_action_ref;
 
 #define XRT_MAX_HANDLE_CHILDREN 256
 #define OXR_MAX_SWAPCHAIN_IMAGES 8
@@ -363,9 +365,17 @@ oxr_action_to_openxr(struct oxr_action *act)
 /*!
  * Helper function to classify sub_paths.
  *
+ * Sets all members of @p sub_paths ( @ref oxr_sub_paths ) as appropriate based
+ * on the subaction paths found in the list.
+ *
+ * If no paths are provided, @p sub_paths->any will be true.
+ *
+ * @return false if an invalid subaction path is provided.
+ *
  * @public @memberof oxr_instance
+ * @relatesalso oxr_sub_paths
  */
-void
+bool
 oxr_classify_sub_action_paths(struct oxr_logger *log,
                               struct oxr_instance *inst,
                               uint32_t num_subaction_paths,
@@ -378,11 +388,11 @@ oxr_classify_sub_action_paths(struct oxr_logger *log,
  * @public @memberof oxr_session
  */
 XrResult
-oxr_source_get_pose_input(struct oxr_logger *log,
+oxr_action_get_pose_input(struct oxr_logger *log,
                           struct oxr_session *sess,
                           uint32_t key,
                           const struct oxr_sub_paths *sub_paths,
-                          struct oxr_source_input **out_input);
+                          struct oxr_action_input **out_input);
 /*!
  * @public @memberof oxr_instance
  */
@@ -422,7 +432,7 @@ oxr_action_sync_data(struct oxr_logger *log,
 XrResult
 oxr_action_get_boolean(struct oxr_logger *log,
                        struct oxr_session *sess,
-                       uint64_t key,
+                       uint32_t act_key,
                        struct oxr_sub_paths sub_paths,
                        XrActionStateBoolean *data);
 /*!
@@ -431,7 +441,7 @@ oxr_action_get_boolean(struct oxr_logger *log,
 XrResult
 oxr_action_get_vector1f(struct oxr_logger *log,
                         struct oxr_session *sess,
-                        uint64_t key,
+                        uint32_t act_key,
                         struct oxr_sub_paths sub_paths,
                         XrActionStateFloat *data);
 
@@ -441,7 +451,7 @@ oxr_action_get_vector1f(struct oxr_logger *log,
 XrResult
 oxr_action_get_vector2f(struct oxr_logger *log,
                         struct oxr_session *sess,
-                        uint64_t key,
+                        uint32_t act_key,
                         struct oxr_sub_paths sub_paths,
                         XrActionStateVector2f *data);
 /*!
@@ -450,7 +460,7 @@ oxr_action_get_vector2f(struct oxr_logger *log,
 XrResult
 oxr_action_get_pose(struct oxr_logger *log,
                     struct oxr_session *sess,
-                    uint64_t key,
+                    uint32_t act_key,
                     struct oxr_sub_paths sub_paths,
                     XrActionStatePose *data);
 /*!
@@ -459,7 +469,7 @@ oxr_action_get_pose(struct oxr_logger *log,
 XrResult
 oxr_action_apply_haptic_feedback(struct oxr_logger *log,
                                  struct oxr_session *sess,
-                                 uint64_t key,
+                                 uint32_t act_key,
                                  struct oxr_sub_paths sub_paths,
                                  const XrHapticBaseHeader *hapticEvent);
 /*!
@@ -468,7 +478,7 @@ oxr_action_apply_haptic_feedback(struct oxr_logger *log,
 XrResult
 oxr_action_stop_haptic_feedback(struct oxr_logger *log,
                                 struct oxr_session *sess,
-                                uint64_t key,
+                                uint32_t act_key,
                                 struct oxr_sub_paths sub_paths);
 
 /*!
@@ -1123,6 +1133,7 @@ struct oxr_instance
 		XrPath left;
 		XrPath right;
 		XrPath gamepad;
+		XrPath treadmill;
 
 		XrPath khr_simple_controller;
 		XrPath google_daydream_controller;
@@ -1172,11 +1183,27 @@ struct oxr_session
 	bool frame_started;
 	bool exiting;
 
-	struct u_hashmap_int *act_sets;
-	struct u_hashmap_int *sources;
+	/*!
+	 * An array of action set attachments that this session owns.
+	 */
+	struct oxr_action_set_attachment *act_set_attachments;
+	/*!
+	 * Length of @ref oxr_session::act_set_attachments.
+	 */
+	size_t num_action_set_attachments;
+
+	/*!
+	 * A map of action set key to action set attachments.
+	 */
+	struct u_hashmap_int *act_sets_attachments_by_key;
 
-	//! List of created source sets.
-	struct oxr_source_set *src_set_list;
+	/*!
+	 * A map of action key to action attachment.
+	 *
+	 * The action attachments are actually owned by the action set
+	 * attachments, but we own the action set attachments, so this is OK.
+	 */
+	struct u_hashmap_int *act_attachments_by_key;
 
 	//! Has xrAttachSessionActionSets been called?
 	bool actionsAttached;
@@ -1272,7 +1299,10 @@ struct oxr_binding
 };
 
 /*!
- * To carry around a sementic selection of sub action paths.
+ * A parsed equivalent of a list of sub-action paths.
+ *
+ * If @p any is true, then no paths were provided, which typically means any
+ * input is acceptable.
  */
 struct oxr_sub_paths
 {
@@ -1282,51 +1312,84 @@ struct oxr_sub_paths
 	bool left;
 	bool right;
 	bool gamepad;
+	bool treadmill;
 };
 
 /*!
- * Session input source.
+ * The data associated with the attachment of an Action Set (@ref
+ * oxr_action_set) to as Session (@ref oxr_session).
+ *
+ * Action sets are created as children of the Instance, but are primarily used
+ * with one or more Sessions. They may be used with multiple sessions at a time,
+ * so we can't just put the per-session information directly in the action set
+ * or action. Instead, we have the _attachment structures, which mirror the
+ * action sets and actions but are rooted under the Session:
+ *
+ * - For every action set attached to a session, that session owns a @ref
+ *   oxr_action_set_attachment.
+ * - For each action in those attached action sets, the action set attachment
+ *   owns an @ref oxr_action_attachment.
+ *
+ * We go from the public handle to the _attachment structure by using a `key`
+ * value and a hash map: specifically, we look up the oxr_action_set::key and
+ * oxr_action::key in the session.
+ *
+ * This structure has no pointer to the @ref oxr_action_set that created it
+ * because the application is allowed to destroy an action before the session,
+ * which should change nothing except not allow the application to access the
+ * corresponding data anymore.
  *
  * @see oxr_action_set
- * @extends oxr_handle_base
  */
-struct oxr_source_set
+struct oxr_action_set_attachment
 {
-	//! Common structure for things referred to by OpenXR handles.
-	struct oxr_handle_base handle;
-
 	//! Owning session.
 	struct oxr_session *sess;
 
+	//! Action set refcounted data
+	struct oxr_action_set_ref *act_set_ref;
+
+	//! Unique key for the session hashmap.
+	uint32_t act_set_key;
+
 	//! Which sub-action paths are requested on the latest sync.
 	struct oxr_sub_paths requested_sub_paths;
 
-	//! Next source set on this session.
-	struct oxr_source_set *next;
+	//! An array of action attachments we own.
+	struct oxr_action_attachment *act_attachments;
+
+	/*!
+	 * Length of @ref oxr_action_set_attachment::act_attachments.
+	 */
+	size_t num_action_attachments;
 };
 
 /*!
- * The state of a action input source.
+ * De-initialize an action set attachment and its action attachments.
  *
- * @see oxr_source
+ * Frees the action attachments, but does not de-allocate the action set
+ * attachment.
+ *
+ * @public @memberof oxr_action_set_attachment
  */
-struct oxr_source_state
-{
-	union {
-		struct
-		{
-			float x;
-		} vec1;
+void
+oxr_action_set_attachment_teardown(
+    struct oxr_action_set_attachment *act_set_attached);
 
-		struct
-		{
-			float x;
-			float y;
-		} vec2;
 
-		bool boolean;
-	};
+/*!
+ * The state of a action input.
+ *
+ * @see oxr_action_attachment
+ */
+struct oxr_action_state
+{
+	/*!
+	 * The actual value - must interpret using action type
+	 */
+	union xrt_input_value value;
 
+	//! Is this active (bound and providing input)?
 	bool active;
 
 	// Was this changed.
@@ -1337,24 +1400,24 @@ struct oxr_source_state
 };
 
 /*!
- * A input source pair of a @ref xrt_input and a @ref xrt_device.
+ * A input action pair of a @ref xrt_input and a @ref xrt_device.
  *
  * @see xrt_device
  * @see xrt_input
  */
-struct oxr_source_input
+struct oxr_action_input
 {
 	struct xrt_device *xdev;
 	struct xrt_input *input;
 };
 
 /*!
- * A output source pair of a @ref xrt_output_name and a @ref xrt_device.
+ * A output action pair of a @ref xrt_output_name and a @ref xrt_device.
  *
  * @see xrt_device
  * @see xrt_output_name
  */
-struct oxr_source_output
+struct oxr_action_output
 {
 	struct xrt_device *xdev;
 	enum xrt_output_name name;
@@ -1363,43 +1426,57 @@ struct oxr_source_output
 /*!
  * A set of inputs for a single sub action path.
  *
- * @see oxr_source
+ * @see oxr_action_attachment
  */
-struct oxr_source_cache
+struct oxr_action_cache
 {
-	struct oxr_source_state current;
+	struct oxr_action_state current;
 
 	size_t num_inputs;
-	struct oxr_source_input *inputs;
+	struct oxr_action_input *inputs;
 
 	int64_t stop_output_time;
 	size_t num_outputs;
-	struct oxr_source_output *outputs;
+	struct oxr_action_output *outputs;
 
 	enum xrt_source_value_redirect redirect;
 };
 
 /*!
- * Session input source.
+ * Data associated with an Action that has been attached to a Session.
+ *
+ * More information on the action vs action attachment and action set vs action
+ * set attachment parallel is in the docs for @ref oxr_action_set_attachment.
  *
  * @see oxr_action
- * @extends oxr_handle_base
+ * @see oxr_action_set_attachment
  */
-struct oxr_source
+struct oxr_action_attachment
 {
-	//! Common structure for things referred to by OpenXR handles.
-	struct oxr_handle_base handle;
+	//! The owning action set attachment
+	struct oxr_action_set_attachment *act_set_attached;
 
-	//! Type the action this source was created from is.
-	XrActionType action_type;
+	//! This action's refcounted data
+	struct oxr_action_ref *act_ref;
 
-	struct oxr_source_state any_state;
+	/*!
+	 * The corresponding session.
+	 *
+	 * This will always be valid: the session outlives this object because
+	 * it owns act_set_attached.
+	 */
+	struct oxr_session *sess;
+
+	//! Unique key for the session hashmap.
+	uint32_t act_key;
+
+	struct oxr_action_state any_state;
 
-	struct oxr_source_cache user;
-	struct oxr_source_cache head;
-	struct oxr_source_cache left;
-	struct oxr_source_cache right;
-	struct oxr_source_cache gamepad;
+	struct oxr_action_cache user;
+	struct oxr_action_cache head;
+	struct oxr_action_cache left;
+	struct oxr_action_cache right;
+	struct oxr_action_cache gamepad;
 };
 
 /*!
@@ -1514,13 +1591,70 @@ struct oxr_swapchain
 	                          const XrSwapchainImageReleaseInfo *);
 };
 
+struct oxr_refcounted
+{
+	struct xrt_reference base;
+	//! Destruction callback
+	void (*destroy)(struct oxr_refcounted *);
+};
+
+/*!
+ * Increase the reference count of @p orc.
+ */
+static inline void
+oxr_refcounted_ref(struct oxr_refcounted *orc)
+{
+	xrt_reference_inc(&orc->base);
+}
+
+/*!
+ * Decrease the reference count of @p orc, destroying it if it reaches 0.
+ */
+static inline void
+oxr_refcounted_unref(struct oxr_refcounted *orc)
+{
+	if (xrt_reference_dec(&orc->base)) {
+		orc->destroy(orc);
+	}
+}
+
+/*!
+ * The reference-counted data of an action set.
+ *
+ * One or more sessions may still need this data after the application destroys
+ * its XrActionSet handle, so this data is refcounted.
+ *
+ * @see oxr_action_set
+ * @extends oxr_refcounted
+ */
+struct oxr_action_set_ref
+{
+	struct oxr_refcounted base;
+
+	//! Application supplied name of this action.
+	char name[XR_MAX_ACTION_SET_NAME_SIZE];
+
+	//! Has this action set been attached.
+	bool attached;
+
+	//! Unique key for the session hashmap.
+	uint32_t act_set_key;
+
+	struct
+	{
+		struct u_hashset *name_store;
+		struct u_hashset *loc_store;
+	} actions;
+};
+
 /*!
  * A group of actions.
  *
  * Parent type/handle is @ref oxr_instance
  *
- * Note, however, that an action set must be "attached" to a session (@ref
- * oxr_session) to be used and not just configured.
+ * Note, however, that an action set must be "attached" to a session
+ * ( @ref oxr_session ) to be used and not just configured.
+ * The corresponding data is in @ref oxr_action_set_attachment.
  *
  * @obj{XrActionSet}
  * @extends oxr_handle_base
@@ -1533,26 +1667,51 @@ struct oxr_action_set
 	//! Owner of this action set.
 	struct oxr_instance *inst;
 
-	//! Application supplied name of this action.
-	char name[XR_MAX_ACTION_SET_NAME_SIZE];
+	/*!
+	 * The data for this action set that must live as long as any session we
+	 * are attached to.
+	 */
+	struct oxr_action_set_ref *data;
 
-	//! Has this action set been attached.
-	bool attached;
 
-	//! Unique key for the session hashmap.
-	uint32_t key;
+	/*!
+	 * Unique key for the session hashmap.
+	 *
+	 * Duplicated from oxr_action_set_ref::act_set_key for efficiency.
+	 */
+	uint32_t act_set_key;
 
 	//! The item in the name hashset.
 	struct u_hashset_item *name_item;
 
 	//! The item in the localized hashset.
 	struct u_hashset_item *loc_item;
+};
 
-	struct
-	{
-		struct u_hashset *name_store;
-		struct u_hashset *loc_store;
-	} actions;
+/*!
+ * The reference-counted data of an action.
+ *
+ * One or more sessions may still need this data after the application destroys
+ * its XrAction handle, so this data is refcounted.
+ *
+ * @see oxr_action
+ * @extends oxr_refcounted
+ */
+struct oxr_action_ref
+{
+	struct oxr_refcounted base;
+
+	//! Application supplied name of this action.
+	char name[XR_MAX_ACTION_NAME_SIZE];
+
+	//! Unique key for the session hashmap.
+	uint32_t act_key;
+
+	//! Type this action was created with.
+	XrActionType action_type;
+
+	//! Which sub action paths that this action was created with.
+	struct oxr_sub_paths sub_paths;
 };
 
 /*!
@@ -1560,6 +1719,9 @@ struct oxr_action_set
  *
  * Parent type/handle is @ref oxr_action_set
  *
+ * For actual usage, an action is attached to a session: the corresponding data
+ * is in @ref oxr_action_attachment
+ *
  * @obj{XrAction}
  * @extends oxr_handle_base
  */
@@ -1571,17 +1733,16 @@ struct oxr_action
 	//! Owner of this action.
 	struct oxr_action_set *act_set;
 
-	//! Application supplied name of this action.
-	char name[XR_MAX_ACTION_NAME_SIZE];
-
-	//! Unique key for the session hashmap.
-	uint32_t key;
+	//! The data for this action that must live as long as any session we
+	//! are attached to.
+	struct oxr_action_ref *data;
 
-	//! Type this action was created with.
-	XrActionType action_type;
-
-	//! Which sub action paths that this action was created with.
-	struct oxr_sub_paths sub_paths;
+	/*!
+	 * Unique key for the session hashmap.
+	 *
+	 * Duplicated from oxr_action_ref::act_key for efficiency.
+	 */
+	uint32_t act_key;
 
 	//! The item in the name hashset.
 	struct u_hashset_item *name_item;
diff --git a/src/xrt/state_trackers/oxr/oxr_session.c b/src/xrt/state_trackers/oxr/oxr_session.c
index c62e8cab6285ae02772fea83df2479778405196f..d277129b1663a9680069ed531802d65731b27b7a 100644
--- a/src/xrt/state_trackers/oxr/oxr_session.c
+++ b/src/xrt/state_trackers/oxr/oxr_session.c
@@ -776,6 +776,44 @@ verify_projection_layer(struct xrt_compositor *xc,
 	return XR_SUCCESS;
 }
 
+static enum xrt_layer_composition_flags
+convert_layer_flags(XrSwapchainUsageFlags xr_flags)
+{
+	enum xrt_layer_composition_flags flags = 0;
+
+	// clang-format off
+	if ((xr_flags & XR_COMPOSITION_LAYER_CORRECT_CHROMATIC_ABERRATION_BIT) != 0) {
+		flags |= XRT_LAYER_COMPOSITION_CORRECT_CHROMATIC_ABERRATION_BIT;
+	}
+	if ((xr_flags & XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT) != 0) {
+		flags |= XRT_LAYER_COMPOSITION_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
+	}
+	if ((xr_flags & XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT) != 0) {
+		flags |= XRT_LAYER_COMPOSITION_UNPREMULTIPLIED_ALPHA_BIT;
+	}
+	// clang-format on
+
+	return flags;
+}
+
+static enum xrt_layer_eye_visibility
+convert_eye_visibility(XrSwapchainUsageFlags xr_visibility)
+{
+	enum xrt_layer_eye_visibility visibility = 0;
+
+	if (xr_visibility == XR_EYE_VISIBILITY_BOTH) {
+		visibility = XRT_LAYER_EYE_VISIBILITY_BOTH;
+	}
+	if (xr_visibility == XR_EYE_VISIBILITY_LEFT) {
+		visibility = XRT_LAYER_EYE_VISIBILITY_LEFT_BIT;
+	}
+	if (xr_visibility == XR_EYE_VISIBILITY_RIGHT) {
+		visibility = XRT_LAYER_EYE_VISIBILITY_RIGHT_BIT;
+	}
+
+	return visibility;
+}
+
 static XrResult
 submit_quad_layer(struct xrt_compositor *xc,
                   struct oxr_logger *log,
@@ -786,17 +824,47 @@ submit_quad_layer(struct xrt_compositor *xc,
 {
 	struct oxr_swapchain *sc = XRT_CAST_OXR_HANDLE_TO_PTR(
 	    struct oxr_swapchain *, quad->subImage.swapchain);
+	struct oxr_space *spc =
+	    XRT_CAST_OXR_HANDLE_TO_PTR(struct oxr_space *, quad->space);
+
+	enum xrt_layer_composition_flags flags =
+	    convert_layer_flags(quad->layerFlags);
+
+	struct xrt_pose *pose_ptr = (struct xrt_pose *)&quad->pose;
+	struct xrt_pose pose = *pose_ptr;
+
+	if (spc->is_reference && spc->type == XR_REFERENCE_SPACE_TYPE_VIEW) {
+		flags |= XRT_LAYER_COMPOSITION_VIEW_SPACE_BIT;
+		// The space might have a pose, transform that in as well.
+		math_pose_transform(&spc->pose, &pose, &pose);
+	} else {
+		//! @todo Handle action spaces.
+
+		// The space might have a pose, transform that in as well.
+		math_pose_transform(&spc->pose, &pose, &pose);
+
+		// Remove the tracking system origin offset.
+		math_pose_transform(inv_offset, &pose, &pose);
+	}
+
+	struct xrt_layer_data data;
+	U_ZERO(&data);
+	data.type = XRT_LAYER_QUAD;
+	data.name = XRT_INPUT_GENERIC_HEAD_POSE;
+	data.timestamp = timestamp;
+	data.flags = flags;
 
-	struct xrt_pose pose;
+	struct xrt_vec2 *size = (struct xrt_vec2 *)&quad->size;
+	struct xrt_rect *rect = (struct xrt_rect *)&quad->subImage.imageRect;
 
-	math_pose_transform(inv_offset, (struct xrt_pose *)&quad->pose, &pose);
+	data.quad.visibility = convert_eye_visibility(quad->eyeVisibility);
+	data.quad.sub.image_index = sc->released.index;
+	data.quad.sub.array_index = quad->subImage.imageArrayIndex;
+	data.quad.sub.rect = *rect;
+	data.quad.pose = pose;
+	data.quad.size = *size;
 
-	CALL_CHK(xrt_comp_layer_quad(
-	    xc, timestamp, head, XRT_INPUT_GENERIC_HEAD_POSE, quad->layerFlags,
-	    (enum xrt_layer_eye_visibility)quad->eyeVisibility, sc->swapchain,
-	    sc->released.index, (struct xrt_rect *)&quad->subImage.imageRect,
-	    quad->subImage.imageArrayIndex, &pose,
-	    (struct xrt_vec2 *)&quad->size, false));
+	CALL_CHK(xrt_comp_layer_quad(xc, head, sc->swapchain, &data));
 
 	return XR_SUCCESS;
 }
@@ -809,34 +877,70 @@ submit_projection_layer(struct xrt_compositor *xc,
                         struct xrt_pose *inv_offset,
                         uint64_t timestamp)
 {
-	enum xrt_layer_composition_flags flags = 0;
+	struct oxr_space *spc =
+	    XRT_CAST_OXR_HANDLE_TO_PTR(struct oxr_space *, proj->space);
 	struct oxr_swapchain *scs[2];
+	struct xrt_pose *pose_ptr[2];
+	struct xrt_pose pose[2];
 
-	uint32_t num_chains = ARRAY_SIZE(scs);
+	enum xrt_layer_composition_flags flags =
+	    convert_layer_flags(proj->layerFlags);
 
+	uint32_t num_chains = ARRAY_SIZE(scs);
 	for (uint32_t i = 0; i < num_chains; i++) {
 		scs[i] = XRT_CAST_OXR_HANDLE_TO_PTR(
 		    struct oxr_swapchain *, proj->views[i].subImage.swapchain);
+		pose_ptr[i] = (struct xrt_pose *)&proj->views[i].pose;
+		pose[i] = *pose_ptr[i];
 	}
 
-	struct xrt_pose pose[2];
-	math_pose_transform(inv_offset, (struct xrt_pose *)&proj->views[0].pose,
-	                    &pose[0]);
-	math_pose_transform(inv_offset, (struct xrt_pose *)&proj->views[1].pose,
-	                    &pose[1]);
-
-	CALL_CHK(xrt_comp_layer_stereo_projection(
-	    xc, timestamp, head, XRT_INPUT_GENERIC_HEAD_POSE, flags,
-	    scs[0]->swapchain, // Left
-	    scs[0]->released.index,
-	    (struct xrt_rect *)&proj->views[0].subImage.imageRect,
-	    proj->views[0].subImage.imageArrayIndex,
-	    (struct xrt_fov *)&proj->views[0].fov, &pose[0],
-	    scs[1]->swapchain, // Right
-	    scs[1]->released.index,
-	    (struct xrt_rect *)&proj->views[1].subImage.imageRect,
-	    proj->views[1].subImage.imageArrayIndex,
-	    (struct xrt_fov *)&proj->views[1].fov, &pose[1], false));
+	if (spc->is_reference && spc->type == XR_REFERENCE_SPACE_TYPE_VIEW) {
+		flags |= XRT_LAYER_COMPOSITION_VIEW_SPACE_BIT;
+		// The space might have a pose, transform that in as well.
+		math_pose_transform(&spc->pose, &pose[0], &pose[0]);
+		math_pose_transform(&spc->pose, &pose[1], &pose[1]);
+	} else {
+		//! @todo Handle action spaces.
+
+		// The space might have a pose, transform that in as well.
+		math_pose_transform(&spc->pose, &pose[0], &pose[0]);
+		math_pose_transform(&spc->pose, &pose[1], &pose[1]);
+
+		// Remove the tracking system origin offset.
+		math_pose_transform(inv_offset, &pose[0], &pose[0]);
+		math_pose_transform(inv_offset, &pose[1], &pose[1]);
+	}
+
+	struct xrt_rect *l_rect =
+	    (struct xrt_rect *)&proj->views[0].subImage.imageRect;
+	struct xrt_fov *l_fov = (struct xrt_fov *)&proj->views[0].fov;
+	struct xrt_rect *r_rect =
+	    (struct xrt_rect *)&proj->views[1].subImage.imageRect;
+	struct xrt_fov *r_fov = (struct xrt_fov *)&proj->views[1].fov;
+
+	struct xrt_layer_data data;
+	U_ZERO(&data);
+	data.type = XRT_LAYER_STEREO_PROJECTION;
+	data.name = XRT_INPUT_GENERIC_HEAD_POSE;
+	data.timestamp = timestamp;
+	data.flags = flags;
+
+	data.stereo.l.sub.image_index = scs[0]->released.index;
+	data.stereo.l.sub.array_index = proj->views[0].subImage.imageArrayIndex;
+	data.stereo.l.sub.rect = *l_rect;
+	data.stereo.l.fov = *l_fov;
+	data.stereo.l.pose = pose[0];
+
+	data.stereo.r.sub.image_index = scs[1]->released.index;
+	data.stereo.r.sub.array_index = proj->views[1].subImage.imageArrayIndex;
+	data.stereo.r.sub.rect = *r_rect;
+	data.stereo.r.fov = *r_fov;
+	data.stereo.r.pose = pose[1];
+
+	CALL_CHK(xrt_comp_layer_stereo_projection(xc, head,
+	                                          scs[0]->swapchain, // Left
+	                                          scs[1]->swapchain, // Right
+	                                          &data));
 	return XR_SUCCESS;
 }
 
@@ -1023,9 +1127,20 @@ oxr_session_destroy(struct oxr_logger *log, struct oxr_handle_base *hb)
 
 	// Does a null-ptr check.
 	xrt_comp_destroy(&sess->compositor);
+	for (size_t i = 0; i < sess->num_action_set_attachments; ++i) {
+		oxr_action_set_attachment_teardown(
+		    &sess->act_set_attachments[i]);
+	}
+	free(sess->act_set_attachments);
+	sess->act_set_attachments = NULL;
+	sess->num_action_set_attachments = 0;
+
+	// If we tore everything down correctly, these are empty now.
+	assert(u_hashmap_int_empty(sess->act_sets_attachments_by_key));
+	assert(u_hashmap_int_empty(sess->act_attachments_by_key));
 
-	u_hashmap_int_destroy(&sess->act_sets);
-	u_hashmap_int_destroy(&sess->sources);
+	u_hashmap_int_destroy(&sess->act_sets_attachments_by_key);
+	u_hashmap_int_destroy(&sess->act_attachments_by_key);
 
 	free(sess);
 
@@ -1163,8 +1278,8 @@ oxr_session_create(struct oxr_logger *log,
 	oxr_session_change_state(log, sess, XR_SESSION_STATE_IDLE);
 	oxr_session_change_state(log, sess, XR_SESSION_STATE_READY);
 
-	u_hashmap_int_create(&sess->act_sets);
-	u_hashmap_int_create(&sess->sources);
+	u_hashmap_int_create(&sess->act_sets_attachments_by_key);
+	u_hashmap_int_create(&sess->act_attachments_by_key);
 
 	*out_session = sess;
 
diff --git a/src/xrt/state_trackers/oxr/oxr_space.c b/src/xrt/state_trackers/oxr/oxr_space.c
index f9214b9c48e5379642dd3340aa55889a51785c13..e159da78721c670e2df338e0e7e1f0662f61492a 100644
--- a/src/xrt/state_trackers/oxr/oxr_space.c
+++ b/src/xrt/state_trackers/oxr/oxr_space.c
@@ -197,7 +197,7 @@ oxr_space_action_relation(struct oxr_logger *log,
                           XrTime at_time,
                           struct xrt_space_relation *out_relation)
 {
-	struct oxr_source_input *input = NULL;
+	struct oxr_action_input *input = NULL;
 	struct oxr_space *act_spc, *ref_spc = NULL;
 	uint64_t timestamp = 0;
 	bool invert = false;
@@ -235,7 +235,7 @@ oxr_space_action_relation(struct oxr_logger *log,
 		return XR_SUCCESS;
 	}
 
-	oxr_source_get_pose_input(log, sess, act_spc->act_key,
+	oxr_action_get_pose_input(log, sess, act_spc->act_key,
 	                          &act_spc->sub_paths, &input);
 
 	// If the input isn't active.