#define VK_NO_PROTOTYPES #include typedef struct _XDisplay Display; typedef unsigned long Window; typedef unsigned long VisualID; #include #include #include #include "xdg-shell-client-protocol.h" #include "presenter.h" #include "xr_tracking_file.h" #include "xr_frame_channel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "openvr_driver.h" namespace droiddeck { namespace { void Log(const char *fmt, ...) __attribute__((format(printf, 1, 2))); void Log(const char *fmt, ...) { char buf[1024]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); if (vr::VRDriverContext() && vr::VRDriverLog()) vr::VRDriverLog()->Log(buf); fprintf(stderr, "[droiddeck] %s\n", buf); } struct X11 { void *lib = nullptr; Display *(*OpenDisplay)(const char *); Window (*DefaultRootWindow)(Display *); int (*DefaultScreen)(Display *); int (*DisplayWidth)(Display *, int); int (*DisplayHeight)(Display *, int); Window (*CreateSimpleWindow)(Display *, Window, int, int, unsigned, unsigned, unsigned, unsigned long, unsigned long); unsigned long (*InternAtom)(Display *, const char *, int); int (*ChangeProperty)(Display *, Window, unsigned long, unsigned long, int, int, const unsigned char *, int); int (*MapWindow)(Display *, Window); int (*StoreName)(Display *, Window, const char *); int (*Flush)(Display *); bool Load() { lib = dlopen("libX11.so.6", RTLD_NOW | RTLD_LOCAL); if (!lib) return false; #define X(name) if (!(*(void **)&name = dlsym(lib, "X" #name))) return false; X(OpenDisplay) X(DefaultRootWindow) X(DefaultScreen) X(DisplayWidth) X(DisplayHeight) X(CreateSimpleWindow) X(InternAtom) X(ChangeProperty) X(MapWindow) X(StoreName) X(Flush) #undef X return true; } }; #define VK_GLOBAL(X) X(CreateInstance) #define VK_INSTANCE(X) \ X(DestroyInstance) X(EnumeratePhysicalDevices) X(GetPhysicalDeviceQueueFamilyProperties) \ X(GetPhysicalDeviceMemoryProperties) X(CreateDevice) X(GetDeviceProcAddr) X(CreateXlibSurfaceKHR) \ X(CreateWaylandSurfaceKHR) \ X(GetPhysicalDeviceSurfaceSupportKHR) X(GetPhysicalDeviceSurfaceCapabilitiesKHR) \ X(GetPhysicalDeviceSurfaceFormatsKHR) X(GetPhysicalDeviceSurfacePresentModesKHR) X(GetPhysicalDeviceProperties) X(DestroySurfaceKHR) #define VK_DEVICE(X) \ X(GetDeviceQueue) X(CreateSwapchainKHR) X(DestroySwapchainKHR) X(GetSwapchainImagesKHR) X(AcquireNextImageKHR) \ X(QueuePresentKHR) X(CreateCommandPool) X(AllocateCommandBuffers) X(BeginCommandBuffer) X(EndCommandBuffer) \ X(ResetCommandBuffer) X(CmdPipelineBarrier) X(CmdBlitImage) X(CmdClearColorImage) X(QueueSubmit) X(CreateFence) \ X(WaitForFences) X(ResetFences) X(CreateSemaphore) X(CreateImage) X(DestroyImage) X(GetImageMemoryRequirements) \ X(AllocateMemory) X(FreeMemory) X(BindImageMemory) X(DeviceWaitIdle) X(DestroyDevice) X(DestroyFence) X(DestroySemaphore) \ X(DestroyCommandPool) X(CreateBuffer) X(DestroyBuffer) X(GetBufferMemoryRequirements) X(BindBufferMemory) \ X(MapMemory) X(UnmapMemory) X(InvalidateMappedMemoryRanges) X(CmdCopyImageToBuffer) struct Vk { void *lib = nullptr; PFN_vkGetInstanceProcAddr GetInstanceProcAddr = nullptr; #define X(n) PFN_vk##n n = nullptr; VK_GLOBAL(X) VK_INSTANCE(X) VK_DEVICE(X) #undef X }; struct Imported { uint64_t handle; VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; uint32_t width = 0, height = 0; int dmaFd = -1; }; struct PendingImport { uint64_t handle; int fd; uint32_t format, width, height; }; VkFormat UnormOf(uint32_t f) { switch (f) { case VK_FORMAT_R8G8B8A8_SRGB: return VK_FORMAT_R8G8B8A8_UNORM; case VK_FORMAT_B8G8R8A8_SRGB: return VK_FORMAT_B8G8R8A8_UNORM; default: return (VkFormat)f; } } class Impl { public: static Impl &Get() { static Impl impl; return impl; } void Add(const uint64_t handles[3], const int fds[3], uint32_t format, uint32_t w, uint32_t h) { std::lock_guard lock(mutex_); if (failed_ || exit_) return; for (int i = 0; i < 3; i++) { if (fds[i] < 0) continue; int fd = dup(fds[i]); if (fd >= 0) pending_.push_back({handles[i], fd, format, w, h}); } EnsureThread(); cv_.notify_all(); } void Remove(uint64_t handle) { std::lock_guard lock(mutex_); removals_.push_back(handle); cv_.notify_all(); } void Submit(const EyeFrame &f) { std::unique_lock lock(mutex_); if (failed_ || exit_) return; bool blocking = FrameChannelRequested(); if (blocking) cv_.wait(lock, [this] { return failed_ || exit_ || (!haveFrame_ && !inFlight_); }); if (failed_ || exit_) return; EnsureThread(); frame_ = f; frameAt_ = std::chrono::steady_clock::now(); haveFrame_ = true; submitted_++; uint64_t ticket = ++ticket_; cv_.notify_all(); if (blocking) cv_.wait(lock, [this, ticket] { return failed_ || completed_ >= ticket; }); } void Shutdown() { { std::lock_guard lock(mutex_); exit_ = true; cv_.notify_all(); } if (thread_.joinable()) thread_.join(); } private: static bool FrameChannelRequested() { const char *mode = getenv("DROIDDECK_VR_PRESENT"); return mode && !strcmp(mode, "questframes"); } void Fail() { std::lock_guard lock(mutex_); failed_ = true; for (const auto &pending : pending_) close(pending.fd); pending_.clear(); cv_.notify_all(); } void EnsureThread() { if (!started_) { started_ = true; thread_ = std::thread([this] { try { Run(); } catch (const std::exception &error) { Log("presenter: %s", error.what()); Fail(); } }); } } void Run() { const char *mode = getenv("DROIDDECK_VR_PRESENT"); frameChannel_ = FrameChannelRequested(); if (!frameChannel_) wayland_ = !(mode && !strcmp(mode, "x11")) && InitWayland(); if (!frameChannel_ && !wayland_ && !InitWindow()) { Log("presenter: unavailable, frames are not shown"); Fail(); return; } if (!InitVulkan()) { Log("presenter: unavailable, frames are not shown"); Fail(); return; } uint64_t shown = 0; auto lastLog = std::chrono::steady_clock::now(); while (true) { EyeFrame f; uint64_t ticket = 0; std::chrono::steady_clock::time_point at; std::vector imports; std::vector removals; { std::unique_lock lock(mutex_); cv_.wait(lock, [this] { return exit_ || haveFrame_ || !pending_.empty() || !removals_.empty(); }); if (exit_) break; imports.swap(pending_); removals.swap(removals_); f = frame_; at = frameAt_; bool draw = haveFrame_; haveFrame_ = false; inFlight_ = draw; ticket = ticket_; if (!draw) f.texture[0] = f.texture[1] = 0; } if (wl_) { wl_display_dispatch_pending(wl_); wl_display_flush(wl_); } for (auto &p : imports) Import(p); for (auto h : removals) Forget(h); if (f.texture[0] && !frameChannel_) std::this_thread::sleep_until(at + guard_); if (f.texture[0] && (frameChannel_ ? Capture(f) : Draw(f))) shown++; { std::lock_guard lock(mutex_); inFlight_ = false; completed_ = ticket; cv_.notify_all(); if (failed_) break; } auto now = std::chrono::steady_clock::now(); if (now - lastLog > std::chrono::seconds(10)) { Log("presenter: %llu frames shown, %llu submitted (last 10 s)", (unsigned long long)shown, (unsigned long long)submitted_.exchange(0)); shown = 0; lastLog = now; } } if (dev_) vk_.DeviceWaitIdle(dev_); DestroyCapture(); while (!textures_.empty()) Forget(textures_.back().handle); if (dev_) { if (swapchain_) vk_.DestroySwapchainKHR(dev_, swapchain_, nullptr); if (acquired_) vk_.DestroySemaphore(dev_, acquired_, nullptr); if (rendered_) vk_.DestroySemaphore(dev_, rendered_, nullptr); if (fence_) vk_.DestroyFence(dev_, fence_, nullptr); if (pool_) vk_.DestroyCommandPool(dev_, pool_, nullptr); vk_.DestroyDevice(dev_, nullptr); dev_ = VK_NULL_HANDLE; } if (surface_) vk_.DestroySurfaceKHR(inst_, surface_, nullptr); if (inst_) vk_.DestroyInstance(inst_, nullptr); Fail(); } std::chrono::microseconds guard_{4000}; struct wl_display *wl_ = nullptr; struct wl_compositor *wlComp_ = nullptr; struct xdg_wm_base *wm_ = nullptr; struct wl_surface *wlSurf_ = nullptr; struct xdg_surface *xdgSurf_ = nullptr; struct xdg_toplevel *toplevel_ = nullptr; bool wayland_ = false, configured_ = false; int32_t cfgW_ = 0, cfgH_ = 0; static void OnGlobal(void *data, struct wl_registry *reg, uint32_t name, const char *iface, uint32_t version) { Impl *self = (Impl *)data; if (!strcmp(iface, wl_compositor_interface.name)) self->wlComp_ = (struct wl_compositor *)wl_registry_bind(reg, name, &wl_compositor_interface, version < 4 ? version : 4); else if (!strcmp(iface, xdg_wm_base_interface.name)) self->wm_ = (struct xdg_wm_base *)wl_registry_bind(reg, name, &xdg_wm_base_interface, 1); } static void OnGlobalRemove(void *, struct wl_registry *, uint32_t) {} static void OnPing(void *, struct xdg_wm_base *wm, uint32_t serial) { xdg_wm_base_pong(wm, serial); } static void OnSurfaceConfigure(void *data, struct xdg_surface *xs, uint32_t serial) { xdg_surface_ack_configure(xs, serial); ((Impl *)data)->configured_ = true; } static void OnToplevelConfigure(void *data, struct xdg_toplevel *, int32_t w, int32_t h, struct wl_array *) { Impl *self = (Impl *)data; if (w > 0 && h > 0) { self->cfgW_ = w; self->cfgH_ = h; } } static void OnToplevelClose(void *, struct xdg_toplevel *) {} bool InitWayland() { const char *name = getenv("DROIDDECK_VR_WAYLAND_DISPLAY"); if (!name || !*name) name = "wayland-0"; wl_ = wl_display_connect(name); if (!wl_) { Log("presenter: no Wayland display %s; using the X11 overlay", name); return false; } static const struct wl_registry_listener regListener = {OnGlobal, OnGlobalRemove}; struct wl_registry *reg = wl_display_get_registry(wl_); wl_registry_add_listener(reg, ®Listener, this); wl_display_roundtrip(wl_); if (!wlComp_ || !wm_) { Log("presenter: %s lacks wl_compositor or xdg_wm_base", name); return false; } static const struct xdg_wm_base_listener wmListener = {OnPing}; xdg_wm_base_add_listener(wm_, &wmListener, this); wlSurf_ = wl_compositor_create_surface(wlComp_); xdgSurf_ = xdg_wm_base_get_xdg_surface(wm_, wlSurf_); static const struct xdg_surface_listener xsListener = {OnSurfaceConfigure}; xdg_surface_add_listener(xdgSurf_, &xsListener, this); toplevel_ = xdg_surface_get_toplevel(xdgSurf_); static const struct xdg_toplevel_listener tlListener = {OnToplevelConfigure, OnToplevelClose, nullptr, nullptr}; xdg_toplevel_add_listener(toplevel_, &tlListener, this); xdg_toplevel_set_title(toplevel_, "DroidDeck Cardboard VR"); xdg_toplevel_set_app_id(toplevel_, "droiddeck-cardboard-vr"); xdg_toplevel_set_fullscreen(toplevel_, nullptr); wl_surface_commit(wlSurf_); for (int i = 0; i < 20 && !configured_; i++) wl_display_roundtrip(wl_); width_ = height_ = 0; int fd = open("/run/droiddeck/droiddeck-headpose", O_RDONLY | O_CLOEXEC); if (fd >= 0) { uint32_t eye[2] = {0, 0}; if (pread(fd, eye, sizeof(eye), 72) == (ssize_t)sizeof(eye) && eye[0] && eye[1]) { width_ = eye[0] * 2; height_ = eye[1]; } close(fd); } XrTrackingFile xrFile; DroidDeckXrTracking tracking; if (xrFile.Read(&tracking) && XrTrackingFile::Fresh(tracking)) { width_ = tracking.eyes[0].width * 2; height_ = tracking.eyes[0].height; } if (!width_ && cfgW_ > 0 && cfgH_ > 0) { width_ = (uint32_t)cfgW_; height_ = (uint32_t)cfgH_; } if (!width_) { width_ = 1920; height_ = 1080; } Log("presenter: %ux%u fullscreen window on Wayland display %s (configured %s)", width_, height_, name, configured_ ? "yes" : "no"); return true; } bool InitWindow() { if (const char *g = getenv("DROIDDECK_VR_PRESENT_GUARD_MS")) guard_ = std::chrono::microseconds((long)(atof(g) * 1000)); if (!x_.Load()) { Log("presenter: no libX11"); return false; } const char *name = getenv("DROIDDECK_VR_DISPLAY"); if (!name || !*name) name = ":0"; dpy_ = x_.OpenDisplay(name); if (!dpy_) { Log("presenter: cannot open X display %s", name); return false; } int scr = x_.DefaultScreen(dpy_); width_ = (uint32_t)x_.DisplayWidth(dpy_, scr); height_ = (uint32_t)x_.DisplayHeight(dpy_, scr); win_ = x_.CreateSimpleWindow(dpy_, x_.DefaultRootWindow(dpy_), 0, 0, width_, height_, 0, 0, 0); x_.StoreName(dpy_, win_, "DroidDeck Cardboard VR"); const char *atom = getenv("DROIDDECK_VR_OVERLAY_ATOM"); if (!atom || !*atom) atom = "STEAM_OVERLAY"; unsigned long cardinal = 6; unsigned long one = 1; x_.ChangeProperty(dpy_, win_, x_.InternAtom(dpy_, atom, 0), cardinal, 32, 0 , (const unsigned char *)&one, 1); x_.MapWindow(dpy_, win_); x_.Flush(dpy_); Log("presenter: %ux%u overlay window (%s) on X display %s", width_, height_, atom, name); return true; } bool InitVulkan() { vk_.lib = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_LOCAL); if (!vk_.lib) { Log("presenter: no libvulkan"); return false; } vk_.GetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)dlsym(vk_.lib, "vkGetInstanceProcAddr"); #define X(n) vk_.n = (PFN_vk##n)vk_.GetInstanceProcAddr(VK_NULL_HANDLE, "vk" #n); VK_GLOBAL(X) #undef X const char *iexts[] = {"VK_KHR_surface", wayland_ ? "VK_KHR_wayland_surface" : "VK_KHR_xlib_surface"}; VkApplicationInfo app = {VK_STRUCTURE_TYPE_APPLICATION_INFO, nullptr, "droiddeck-vr-presenter", 1, nullptr, 0, VK_API_VERSION_1_1}; VkInstanceCreateInfo ici = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, nullptr, 0, &app, 0, nullptr, frameChannel_ ? 0u : 2u, frameChannel_ ? nullptr : iexts}; VkResult r = vk_.CreateInstance(&ici, nullptr, &inst_); if (r != VK_SUCCESS) { Log("presenter: vkCreateInstance %d", r); return false; } #define X(n) vk_.n = (PFN_vk##n)vk_.GetInstanceProcAddr(inst_, "vk" #n); VK_INSTANCE(X) #undef X uint32_t n = 1; VkPhysicalDevice pds[4]; n = 4; if (vk_.EnumeratePhysicalDevices(inst_, &n, pds) < 0 || n == 0) { Log("presenter: no GPU"); return false; } pd_ = pds[0]; VkPhysicalDeviceProperties props; vk_.GetPhysicalDeviceProperties(pd_, &props); vk_.GetPhysicalDeviceMemoryProperties(pd_, &mem_); if (!frameChannel_ && wayland_) { VkWaylandSurfaceCreateInfoKHR wci = {VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR, nullptr, 0, wl_, wlSurf_}; if ((r = vk_.CreateWaylandSurfaceKHR(inst_, &wci, nullptr, &surface_)) != VK_SUCCESS) { Log("presenter: wayland surface %d", r); return false; } } else if (!frameChannel_) { VkXlibSurfaceCreateInfoKHR sci = {VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR, nullptr, 0, dpy_, win_}; if ((r = vk_.CreateXlibSurfaceKHR(inst_, &sci, nullptr, &surface_)) != VK_SUCCESS) { Log("presenter: xlib surface %d", r); return false; } } uint32_t nq = 0; vk_.GetPhysicalDeviceQueueFamilyProperties(pd_, &nq, nullptr); std::vector qf(nq); vk_.GetPhysicalDeviceQueueFamilyProperties(pd_, &nq, qf.data()); qfam_ = UINT32_MAX; for (uint32_t i = 0; i < nq; i++) { VkBool32 present = VK_FALSE; if (frameChannel_) present = VK_TRUE; else vk_.GetPhysicalDeviceSurfaceSupportKHR(pd_, i, surface_, &present); if ((qf[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && present) { qfam_ = i; break; } } if (qfam_ == UINT32_MAX) { Log("presenter: no queue can present"); return false; } float prio = 1.0f; VkDeviceQueueCreateInfo qci = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, nullptr, 0, qfam_, 1, &prio}; const char *dexts[] = {"VK_KHR_external_memory_fd", "VK_KHR_swapchain"}; VkDeviceCreateInfo dci = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, nullptr, 0, 1, &qci, 0, nullptr, frameChannel_ ? 1u : 2u, dexts, nullptr}; if ((r = vk_.CreateDevice(pd_, &dci, nullptr, &dev_)) != VK_SUCCESS) { Log("presenter: vkCreateDevice %d", r); return false; } #define X(n) vk_.n = (PFN_vk##n)vk_.GetDeviceProcAddr(dev_, "vk" #n); VK_DEVICE(X) #undef X vk_.GetDeviceQueue(dev_, qfam_, 0, &queue_); VkCommandPoolCreateInfo pci = {VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, nullptr, VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, qfam_}; vk_.CreateCommandPool(dev_, &pci, nullptr, &pool_); VkCommandBufferAllocateInfo cai = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, nullptr, pool_, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1}; vk_.AllocateCommandBuffers(dev_, &cai, &cmd_); VkFenceCreateInfo fci = {VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, nullptr, VK_FENCE_CREATE_SIGNALED_BIT}; vk_.CreateFence(dev_, &fci, nullptr, &fence_); VkSemaphoreCreateInfo semci = {VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, nullptr, 0}; vk_.CreateSemaphore(dev_, &semci, nullptr, &acquired_); vk_.CreateSemaphore(dev_, &semci, nullptr, &rendered_); if (!frameChannel_ && !CreateSwapchain()) return false; Log("presenter: Vulkan on %s, queue family %u", props.deviceName, qfam_); return true; } bool CreateSwapchain() { VkSurfaceCapabilitiesKHR caps; vk_.GetPhysicalDeviceSurfaceCapabilitiesKHR(pd_, surface_, &caps); uint32_t nf = 0; vk_.GetPhysicalDeviceSurfaceFormatsKHR(pd_, surface_, &nf, nullptr); std::vector fmts(nf); vk_.GetPhysicalDeviceSurfaceFormatsKHR(pd_, surface_, &nf, fmts.data()); VkSurfaceFormatKHR fmt = fmts.empty() ? VkSurfaceFormatKHR{VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR} : fmts[0]; for (auto &f : fmts) if (f.format == VK_FORMAT_B8G8R8A8_UNORM || f.format == VK_FORMAT_R8G8B8A8_UNORM) { fmt = f; break; } uint32_t npm = 0; vk_.GetPhysicalDeviceSurfacePresentModesKHR(pd_, surface_, &npm, nullptr); std::vector pms(npm); vk_.GetPhysicalDeviceSurfacePresentModesKHR(pd_, surface_, &npm, pms.data()); VkPresentModeKHR pm = VK_PRESENT_MODE_FIFO_KHR; for (auto m : pms) if (m == VK_PRESENT_MODE_MAILBOX_KHR) pm = m; VkExtent2D ext = caps.currentExtent.width != UINT32_MAX ? caps.currentExtent : VkExtent2D{width_, height_}; extent_ = ext; uint32_t count = caps.minImageCount + 1; if (caps.maxImageCount && count > caps.maxImageCount) count = caps.maxImageCount; VkCompositeAlphaFlagBitsKHR alpha = (caps.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) ? VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR : (VkCompositeAlphaFlagBitsKHR)(caps.supportedCompositeAlpha & -caps.supportedCompositeAlpha); VkSwapchainCreateInfoKHR sci = {VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; sci.surface = surface_; sci.minImageCount = count; sci.imageFormat = fmt.format; sci.imageColorSpace = fmt.colorSpace; sci.imageExtent = ext; sci.imageArrayLayers = 1; sci.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; sci.preTransform = caps.currentTransform; sci.compositeAlpha = alpha; sci.presentMode = pm; sci.clipped = VK_TRUE; VkResult r = vk_.CreateSwapchainKHR(dev_, &sci, nullptr, &swapchain_); if (r != VK_SUCCESS) { Log("presenter: vkCreateSwapchainKHR %d", r); return false; } uint32_t ni = 0; vk_.GetSwapchainImagesKHR(dev_, swapchain_, &ni, nullptr); images_.resize(ni); vk_.GetSwapchainImagesKHR(dev_, swapchain_, &ni, images_.data()); Log("presenter: swapchain %ux%u, format %d, %u images, %s", ext.width, ext.height, fmt.format, ni, pm == VK_PRESENT_MODE_MAILBOX_KHR ? "mailbox" : "fifo"); return true; } int MemoryType(uint32_t bits) { for (uint32_t i = 0; i < mem_.memoryTypeCount; i++) if ((bits & (1u << i)) && (mem_.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) return (int)i; for (uint32_t i = 0; i < mem_.memoryTypeCount; i++) if (bits & (1u << i)) return (int)i; return -1; } void Import(const PendingImport &p) { Imported im{p.handle}; im.width = p.width; im.height = p.height; if (frameChannel_) im.dmaFd = dup(p.fd); const char *fl = getenv("DROIDDECK_VR_IMPORT_FORMAT_LIST"); const bool formatList = !fl || strcmp(fl, "0") != 0; VkFormat formats[2] = {(VkFormat)p.format, UnormOf(p.format)}; VkImageFormatListCreateInfo list = {VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO, nullptr, 2, formats}; VkExternalMemoryImageCreateInfo ext = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO, formatList ? &list : nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT}; VkImageCreateInfo ici = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, &ext, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT, VK_IMAGE_TYPE_2D, UnormOf(p.format), {p.width, p.height, 1}, 1, 1, VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT, VK_SHARING_MODE_EXCLUSIVE, 0, nullptr, VK_IMAGE_LAYOUT_UNDEFINED}; VkResult r = vk_.CreateImage(dev_, &ici, nullptr, &im.image); if (r != VK_SUCCESS) { Log("presenter: import image %d", r); close(p.fd); if (im.dmaFd >= 0) close(im.dmaFd); return; } VkMemoryRequirements req; vk_.GetImageMemoryRequirements(dev_, im.image, &req); VkImportMemoryFdInfoKHR imp = {VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT, p.fd}; VkMemoryDedicatedAllocateInfo ded = {VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO, &imp, im.image, VK_NULL_HANDLE}; int memoryType = MemoryType(req.memoryTypeBits); if (memoryType < 0) { vk_.DestroyImage(dev_, im.image, nullptr); close(p.fd); if (im.dmaFd >= 0) close(im.dmaFd); return; } VkMemoryAllocateInfo mai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, &ded, req.size, (uint32_t)memoryType}; r = vk_.AllocateMemory(dev_, &mai, nullptr, &im.memory); if (r != VK_SUCCESS) { Log("presenter: import memory (fd %d, %llu bytes) %d", p.fd, (unsigned long long)req.size, r); close(p.fd); if (im.dmaFd >= 0) close(im.dmaFd); vk_.DestroyImage(dev_, im.image, nullptr); return; } if (vk_.BindImageMemory(dev_, im.image, im.memory, 0) != VK_SUCCESS) { vk_.DestroyImage(dev_, im.image, nullptr); vk_.FreeMemory(dev_, im.memory, nullptr); if (im.dmaFd >= 0) close(im.dmaFd); return; } textures_.push_back(im); Log("presenter: imported texture 0x%llx (%ux%u, format %u)", (unsigned long long)p.handle, p.width, p.height, p.format); } void Forget(uint64_t handle) { for (auto it = textures_.begin(); it != textures_.end(); ++it) { if (it->handle == handle) { vk_.DeviceWaitIdle(dev_); vk_.DestroyImage(dev_, it->image, nullptr); vk_.FreeMemory(dev_, it->memory, nullptr); if (it->dmaFd >= 0) close(it->dmaFd); textures_.erase(it); return; } } } const Imported *Find(uint64_t h) const { for (auto &t : textures_) if (t.handle == h) return &t; return nullptr; } static VkImageMemoryBarrier Barrier(VkImage img, VkImageLayout from, VkImageLayout to, VkAccessFlags src, VkAccessFlags dst, uint32_t srcQ = VK_QUEUE_FAMILY_IGNORED, uint32_t dstQ = VK_QUEUE_FAMILY_IGNORED) { VkImageMemoryBarrier b = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, nullptr, src, dst, from, to, srcQ, dstQ, img, {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}}; return b; } bool ProducerReady(const Imported &image) { struct { uint32_t flags; int32_t fd; } exported{1u, -1}; bool ready = false; if (image.dmaFd >= 0 && ioctl(image.dmaFd, _IOWR('b', 2, decltype(exported)), &exported) == 0 && exported.fd >= 0) { struct SyncInfo { char name[32]; int32_t status; uint32_t flags, count, padding; uint64_t fences; } info{}; if (ioctl(exported.fd, _IOWR('>', 4, SyncInfo), &info) == 0 && info.count > 0) { pollfd signal{exported.fd, POLLIN, 0}; int64_t deadline = xr_frame_now() + 100000000LL; while (xr_frame_now() < deadline) { int64_t remaining = deadline - xr_frame_now(); if (remaining <= 0) break; int result = poll(&signal, 1, static_cast((remaining + 999999) / 1000000)); if (result < 0 && errno == EINTR) continue; ready = result > 0 && (signal.revents & POLLIN) && !(signal.revents & (POLLERR | POLLNVAL)); break; } } close(exported.fd); } if (!ready) { static unsigned said = 0; if (said++ < 3) Log("presenter: refusing Quest frame without a nonempty successful producer fence (texture 0x%llx)", (unsigned long long)image.handle); } return ready; } void DestroyCapture() { if (!dev_) return; if (captureImage_) vk_.DestroyImage(dev_, captureImage_, nullptr); if (captureMemory_) vk_.FreeMemory(dev_, captureMemory_, nullptr); if (readbackBuffer_) vk_.DestroyBuffer(dev_, readbackBuffer_, nullptr); if (readbackMemory_) vk_.FreeMemory(dev_, readbackMemory_, nullptr); captureImage_ = VK_NULL_HANDLE; captureMemory_ = readbackMemory_ = VK_NULL_HANDLE; readbackBuffer_ = VK_NULL_HANDLE; captureWidth_ = captureHeight_ = 0; } bool PrepareCapture(const DroidDeckXrFrameHeader &header) { if (captureWidth_ == header.width && captureHeight_ == header.height) return true; DestroyCapture(); auto fail = [this](VkResult result) { Log("presenter: capture allocation failed %d", result); DestroyCapture(); return false; }; VkImageCreateInfo image{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; image.imageType = VK_IMAGE_TYPE_2D; image.format = VK_FORMAT_R8G8B8A8_UNORM; image.extent = {header.width, header.height, 1}; image.mipLevels = image.arrayLayers = 1; image.samples = VK_SAMPLE_COUNT_1_BIT; image.tiling = VK_IMAGE_TILING_OPTIMAL; image.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; VkResult result = vk_.CreateImage(dev_, &image, nullptr, &captureImage_); if (result != VK_SUCCESS) return fail(result); VkMemoryRequirements requirements{}; vk_.GetImageMemoryRequirements(dev_, captureImage_, &requirements); int type = MemoryType(requirements.memoryTypeBits); if (type < 0) return fail(VK_ERROR_FEATURE_NOT_PRESENT); VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = type; result = vk_.AllocateMemory(dev_, &allocation, nullptr, &captureMemory_); if (result != VK_SUCCESS) return fail(result); result = vk_.BindImageMemory(dev_, captureImage_, captureMemory_, 0); if (result != VK_SUCCESS) return fail(result); VkBufferCreateInfo buffer{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; buffer.size = header.bytes; buffer.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; result = vk_.CreateBuffer(dev_, &buffer, nullptr, &readbackBuffer_); if (result != VK_SUCCESS) return fail(result); vk_.GetBufferMemoryRequirements(dev_, readbackBuffer_, &requirements); type = -1; for (uint32_t index = 0; index < mem_.memoryTypeCount; index++) { if ((requirements.memoryTypeBits & (1u << index)) && (mem_.memoryTypes[index].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT)) { type = index; if (mem_.memoryTypes[index].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) break; } } if (type < 0) return fail(VK_ERROR_FEATURE_NOT_PRESENT); allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = type; result = vk_.AllocateMemory(dev_, &allocation, nullptr, &readbackMemory_); if (result != VK_SUCCESS) return fail(result); result = vk_.BindBufferMemory(dev_, readbackBuffer_, readbackMemory_, 0); if (result != VK_SUCCESS) return fail(result); captureWidth_ = header.width; captureHeight_ = header.height; return true; } bool Capture(const EyeFrame &frame) { if (frame.layers != 1 || !xr_frame_valid(frame.render)) { static unsigned said = 0; if (said++ < 3) Log("presenter: refusing Quest frame with unsupported layer count or render geometry (%u layers)", frame.layers); return false; } const Imported *eye[2] = {Find(frame.texture[0]), Find(frame.texture[1])}; if (!eye[0] || !eye[1]) return false; for (int index = 0; index < 2; index++) { for (float value : frame.bounds[index]) if (!std::isfinite(value) || value < 0 || value > 1) return false; if (frame.bounds[index][0] == frame.bounds[index][2] || frame.bounds[index][1] == frame.bounds[index][3]) return false; if ((index == 0 || eye[0] != eye[1]) && !ProducerReady(*eye[index])) return false; } VkResult result = vk_.WaitForFences(dev_, 1, &fence_, VK_TRUE, UINT64_MAX); if (result != VK_SUCCESS) { Fail(); return false; } if (!PrepareCapture(frame.render)) return false; if (vk_.ResetCommandBuffer(cmd_, 0) != VK_SUCCESS) { Fail(); return false; } VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; if (vk_.BeginCommandBuffer(cmd_, &begin) != VK_SUCCESS) { Fail(); return false; } VkImageMemoryBarrier before[] = { Barrier(captureImage_, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT), Barrier(eye[0]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_TRANSFER_READ_BIT, VK_QUEUE_FAMILY_EXTERNAL, qfam_), Barrier(eye[1]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_TRANSFER_READ_BIT, VK_QUEUE_FAMILY_EXTERNAL, qfam_)}; vk_.CmdPipelineBarrier(cmd_, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, eye[0] == eye[1] ? 2 : 3, before); int32_t half = frame.render.width / 2; for (int index = 0; index < 2; index++) { const float *bounds = frame.bounds[index]; VkImageBlit blit{}; blit.srcSubresource = blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; blit.srcOffsets[0] = {static_cast(bounds[0] * eye[index]->width), static_cast(bounds[1] * eye[index]->height), 0}; blit.srcOffsets[1] = {static_cast(bounds[2] * eye[index]->width), static_cast(bounds[3] * eye[index]->height), 1}; blit.dstOffsets[0] = {index * half, 0, 0}; blit.dstOffsets[1] = {(index + 1) * half, static_cast(frame.render.height), 1}; vk_.CmdBlitImage(cmd_, eye[index]->image, VK_IMAGE_LAYOUT_GENERAL, captureImage_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); } VkImageMemoryBarrier after[] = { Barrier(captureImage_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT), Barrier(eye[0]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_TRANSFER_READ_BIT, 0, qfam_, VK_QUEUE_FAMILY_EXTERNAL), Barrier(eye[1]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_TRANSFER_READ_BIT, 0, qfam_, VK_QUEUE_FAMILY_EXTERNAL)}; vk_.CmdPipelineBarrier(cmd_, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, eye[0] == eye[1] ? 2 : 3, after); VkBufferImageCopy copy{}; copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; copy.imageExtent = {frame.render.width, frame.render.height, 1}; vk_.CmdCopyImageToBuffer(cmd_, captureImage_, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readbackBuffer_, 1, ©); VkBufferMemoryBarrier host{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; host.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; host.dstAccessMask = VK_ACCESS_HOST_READ_BIT; host.srcQueueFamilyIndex = host.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; host.buffer = readbackBuffer_; host.size = VK_WHOLE_SIZE; vk_.CmdPipelineBarrier(cmd_, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1, &host, 0, nullptr); if (vk_.EndCommandBuffer(cmd_) != VK_SUCCESS || vk_.ResetFences(dev_, 1, &fence_) != VK_SUCCESS) { Fail(); return false; } VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd_; result = vk_.QueueSubmit(queue_, 1, &submit, fence_); if (result != VK_SUCCESS) { Fail(); return false; } result = vk_.WaitForFences(dev_, 1, &fence_, VK_TRUE, UINT64_MAX); if (result != VK_SUCCESS) { vk_.DeviceWaitIdle(dev_); Fail(); return false; } void *mapping = nullptr; if (vk_.MapMemory(dev_, readbackMemory_, 0, VK_WHOLE_SIZE, 0, &mapping) != VK_SUCCESS) return false; VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE}; range.memory = readbackMemory_; range.size = VK_WHOLE_SIZE; result = vk_.InvalidateMappedMemoryRanges(dev_, 1, &range); bool sent = result == VK_SUCCESS && sender_.Send("/run/droiddeck/" DROIDDECK_XR_FRAME_SOCKET, frame.render, static_cast(mapping)); vk_.UnmapMemory(dev_, readbackMemory_); return sent; } bool Draw(const EyeFrame &f) { const Imported *eye[2] = {Find(f.texture[0]), Find(f.texture[1])}; if (!eye[0] || !eye[1]) return false; vk_.WaitForFences(dev_, 1, &fence_, VK_TRUE, UINT64_MAX); uint32_t idx = 0; VkResult r = vk_.AcquireNextImageKHR(dev_, swapchain_, 100000000ull, acquired_, VK_NULL_HANDLE, &idx); if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) { static int said; if (said++ < 3) Log("presenter: acquire %d", r); return false; } vk_.ResetFences(dev_, 1, &fence_); vk_.ResetCommandBuffer(cmd_, 0); VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, nullptr, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT}; vk_.BeginCommandBuffer(cmd_, &bi); VkImage dst = images_[idx]; VkImageMemoryBarrier pre[3] = { Barrier(dst, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT), Barrier(eye[0]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_TRANSFER_READ_BIT, VK_QUEUE_FAMILY_EXTERNAL, qfam_), Barrier(eye[1]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_TRANSFER_READ_BIT, VK_QUEUE_FAMILY_EXTERNAL, qfam_)}; vk_.CmdPipelineBarrier(cmd_, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, eye[0] == eye[1] ? 2 : 3, pre); VkClearColorValue black = {{0, 0, 0, 1}}; VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; vk_.CmdClearColorImage(cmd_, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); VkMemoryBarrier mb = {VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT}; vk_.CmdPipelineBarrier(cmd_, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &mb, 0, nullptr, 0, nullptr); const int32_t half = (int32_t)extent_.width / 2; for (int e = 0; e < 2; e++) { const float *b = f.bounds[e]; VkImageBlit blit = {}; blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; blit.srcOffsets[0] = {(int32_t)(b[0] * eye[e]->width), (int32_t)(b[1] * eye[e]->height), 0}; blit.srcOffsets[1] = {(int32_t)(b[2] * eye[e]->width), (int32_t)(b[3] * eye[e]->height), 1}; blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; blit.dstOffsets[0] = {e * half, 0, 0}; blit.dstOffsets[1] = {e * half + half, (int32_t)extent_.height, 1}; vk_.CmdBlitImage(cmd_, eye[e]->image, VK_IMAGE_LAYOUT_GENERAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); } VkImageMemoryBarrier post[3] = { Barrier(dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_ACCESS_TRANSFER_WRITE_BIT, 0), Barrier(eye[0]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_TRANSFER_READ_BIT, 0, qfam_, VK_QUEUE_FAMILY_EXTERNAL), Barrier(eye[1]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_TRANSFER_READ_BIT, 0, qfam_, VK_QUEUE_FAMILY_EXTERNAL)}; vk_.CmdPipelineBarrier(cmd_, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, eye[0] == eye[1] ? 2 : 3, post); vk_.EndCommandBuffer(cmd_); VkPipelineStageFlags wait = VK_PIPELINE_STAGE_TRANSFER_BIT; VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO, nullptr, 1, &acquired_, &wait, 1, &cmd_, 1, &rendered_}; if ((r = vk_.QueueSubmit(queue_, 1, &si, fence_)) != VK_SUCCESS) { Log("presenter: submit %d", r); return false; } VkPresentInfoKHR pi = {VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, nullptr, 1, &rendered_, 1, &swapchain_, &idx, nullptr}; r = vk_.QueuePresentKHR(queue_, &pi); if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) { static int said; if (said++ < 3) Log("presenter: present %d", r); } return true; } std::mutex mutex_; std::condition_variable cv_; std::vector pending_; std::vector removals_; EyeFrame frame_{}; std::chrono::steady_clock::time_point frameAt_{}; bool haveFrame_ = false, exit_ = false, started_ = false, inFlight_ = false, failed_ = false; uint64_t ticket_ = 0, completed_ = 0; bool frameChannel_ = false; XrFrameSender sender_; VkImage captureImage_ = VK_NULL_HANDLE; VkDeviceMemory captureMemory_ = VK_NULL_HANDLE, readbackMemory_ = VK_NULL_HANDLE; VkBuffer readbackBuffer_ = VK_NULL_HANDLE; uint32_t captureWidth_ = 0, captureHeight_ = 0; std::atomic submitted_{0}; std::thread thread_; X11 x_; Display *dpy_ = nullptr; Window win_ = 0; uint32_t width_ = 0, height_ = 0; Vk vk_; VkInstance inst_ = VK_NULL_HANDLE; VkPhysicalDevice pd_ = VK_NULL_HANDLE; VkPhysicalDeviceMemoryProperties mem_{}; VkSurfaceKHR surface_ = VK_NULL_HANDLE; uint32_t qfam_ = 0; VkDevice dev_ = VK_NULL_HANDLE; VkQueue queue_ = VK_NULL_HANDLE; VkCommandPool pool_ = VK_NULL_HANDLE; VkCommandBuffer cmd_ = VK_NULL_HANDLE; VkFence fence_ = VK_NULL_HANDLE; VkSemaphore acquired_ = VK_NULL_HANDLE, rendered_ = VK_NULL_HANDLE; VkSwapchainKHR swapchain_ = VK_NULL_HANDLE; VkExtent2D extent_{}; std::vector images_; std::vector textures_; }; } void Presenter::AddTextures(const uint64_t h[3], const int fds[3], uint32_t f, uint32_t w, uint32_t hh) { Impl::Get().Add(h, fds, f, w, hh); } void Presenter::RemoveTexture(uint64_t h) { Impl::Get().Remove(h); } void Presenter::Submit(const EyeFrame &f) { Impl::Get().Submit(f); } void Presenter::Shutdown() { Impl::Get().Shutdown(); } }