#include "LunarWM.h"

#include "RayExt.h"
#include "common.h"

#include <assert.h>
#include <ctype.h>
#include <math.h>
#include <spawn.h>
#include <stdlib.h>

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#include <GLES3/gl3.h>
#include <GLES3/gl31.h>
#include <openxr/openxr.h>
#include <openxr/openxr_platform.h>

#include <wayland-client-protocol.h>
#include <wayland-egl.h>
#include <wayland-server-core.h>

#include <wlr/backend/headless.h>
#include <wlr/backend/session.h>
#include <wlr/backend/wayland.h>
#include <wlr/render/allocator.h>
#include <wlr/render/egl.h>
#include <wlr/render/gles2.h>
#include <wlr/types/wlr_compositor.h>
#include <wlr/types/wlr_cursor.h>
#include <wlr/types/wlr_data_device.h>
#include <wlr/types/wlr_pointer.h>
#include <wlr/types/wlr_subcompositor.h>
#include <wlr/types/wlr_xdg_shell.h>
#include <wlr/util/log.h>

#include <raylib.h>
#include <raymath.h>
#include <rlgl.h>

#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>

#include "vec.h"

void toplevel_commit_notify(struct wl_listener *l, void *)
{
	auto *tl = wl_container_of(l, (LunarWM_Toplevel *)(NULL), commit);

	if (!tl || !tl->surface)
		return;

	if (!tl->is_xwayland) {
		if (tl->u.xdg && tl->u.xdg->base->initial_commit) {
			wlr_xdg_toplevel_set_size(tl->u.xdg, 0, 0);
			return;
		}
	}

	LunarWM_Toplevel_update(tl);
}

void toplevel_destroy_notify(struct wl_listener *l, void *)
{
	auto *tl = wl_container_of(l, (LunarWM_Toplevel *)(NULL), destroy);

	for (size_t i = 0; i < vector_size(tl->server->wayland.v_toplevels); i++) {
		if (tl == tl->server->wayland.v_toplevels[i]) {
			vector_remove(tl->server->wayland.v_toplevels, i);
			break;
		}
	}
	LunarWM_Toplevel_destroy(tl);
	free(tl);
}

bool LunarWM_Toplevel_init_xdg(
    LunarWM_Toplevel *tl, LunarWM *wm, struct wlr_xdg_toplevel *xdg)
{
	tl->id = vector_size(wm->wayland.v_toplevels) + 1;
	tl->server = wm;
	tl->is_xwayland = false;
	tl->u.xdg = xdg;
	tl->surface = xdg->base->surface;

	assert(tl->surface);

	tl->commit.notify = toplevel_commit_notify;
	wl_signal_add(&tl->surface->events.commit, &tl->commit);

	tl->destroy.notify = toplevel_destroy_notify;
	wl_signal_add(&tl->surface->events.destroy, &tl->destroy);

	return true;
}

bool LunarWM_Toplevel_init_xwayland(
    LunarWM_Toplevel *tl, LunarWM *wm, struct wlr_xwayland_surface *xwl)
{
	tl->id = vector_size(wm->wayland.v_toplevels) + 1;
	tl->server = wm;
	tl->is_xwayland = true;
	tl->u.xwl = xwl;
	tl->surface = xwl->surface;

	assert(tl->surface);

	tl->commit.notify = toplevel_commit_notify;
	wl_signal_add(&tl->surface->events.commit, &tl->commit);

	tl->destroy.notify = toplevel_destroy_notify;
	wl_signal_add(&tl->surface->events.destroy, &tl->destroy);

	return true;
}

bool LunarWM_Toplevel_destroy(LunarWM_Toplevel *this)
{
	wl_list_remove(&this->commit.link);
	wl_list_remove(&this->destroy.link);
	if (this->locked_buffer != nullptr) {
		wlr_buffer_unlock(this->locked_buffer);
	}
	return true;
}

bool LunarWM_Toplevel_update(LunarWM_Toplevel *this)
{
	if (!this || !this->surface)
		return false;

	this->texture = wlr_surface_get_texture(this->surface);
	struct wlr_client_buffer *cl_buf = this->surface->buffer;
	if ((this->texture == nullptr) || (cl_buf == nullptr)) {
		return false;
	}

	if ((this->locked_buffer != nullptr)
	    && this->locked_buffer != &cl_buf->base) {
		wlr_buffer_unlock(this->locked_buffer);
		this->locked_buffer = nullptr;
	}
	if (this->locked_buffer == nullptr) {
		this->locked_buffer = wlr_buffer_lock(&cl_buf->base);
	}

	wlr_gles2_texture_get_attribs(this->texture, &this->attribs);
	this->gles_texture = gles2_get_texture(this->texture);
	if (this->gles_texture == nullptr) {
		return false;
	}

	this->rl_texture.id = (unsigned int)this->attribs.tex;
	this->rl_texture.width = (int)this->texture->width;
	this->rl_texture.height = (int)this->texture->height;
	this->rl_texture.mipmaps = 1;
	this->rl_texture.format = this->gles_texture->has_alpha
	    ? PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
	    : PIXELFORMAT_UNCOMPRESSED_R8G8B8;

	SetTextureFilter(this->rl_texture, TEXTURE_FILTER_BILINEAR);
	SetTextureWrap(this->rl_texture, TEXTURE_WRAP_CLAMP);

	return true;
}

static void clamp_to_hints(
    const struct wlr_xwayland_surface *x, uint16_t *w, uint16_t *h)
{
	xcb_size_hints_t *hints = x->size_hints;
	if (!hints)
		return;

	if ((hints->flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE)) {
		if (*w < hints->min_width)
			*w = hints->min_width;
		if (*h < hints->min_height)
			*h = hints->min_height;
	}
	if ((hints->flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE)) {
		if (hints->max_width > 0 && *w > hints->max_width)
			*w = hints->max_width;
		if (hints->max_height > 0 && *h > hints->max_height)
			*h = hints->max_height;
	}
	if ((hints->flags & XCB_ICCCM_SIZE_HINT_P_RESIZE_INC)) {
		if (hints->width_inc > 0)
			*w = (*w / hints->width_inc) * hints->width_inc;
		if (hints->height_inc > 0)
			*h = (*h / hints->height_inc) * hints->height_inc;
	}
	if ((hints->flags & XCB_ICCCM_SIZE_HINT_BASE_SIZE)) {
		if (*w < hints->base_width)
			*w = hints->base_width;
		if (*h < hints->base_height)
			*h = hints->base_height;
	}
}

struct XwlHooks {
	LunarWM *wm;
	struct wlr_xwayland_surface *xwl;
	struct wl_listener associate;
	struct wl_listener dissociate;
	struct wl_listener destroy;

	struct wl_listener req_configure;
	struct wl_listener req_maximize;
	struct wl_listener req_fullscreen;
	struct wl_listener set_geometry;
};

static void xwayland_ready_notify(struct wl_listener *l, void *data)
{
	LunarWM *wm = wl_container_of(l, wm, wayland.xwayland_ready);
	wlr_xwayland_set_seat(wm->wayland.xwayland, wm->wayland.seat);
	setenv("DISPLAY", wm->wayland.xwayland->display_name, 1);
}

static void handle_associate(struct wl_listener *ll, void *d)
{
	struct wlr_xwayland_surface *x = d;
	LunarWM *wm2 = wl_container_of(ll, wm2, wayland.xwayland_associate_tmp);
	if (!x->surface)
		return;

	LunarWM_Toplevel *tl = calloc(1, sizeof *tl);
	if (!tl)
		return;

	if (LunarWM_Toplevel_init_xwayland(tl, wm2, x)) {
		vector_add(&wm2->wayland.v_toplevels, tl);
	} else {
		free(tl);
	}
	wl_list_remove(&wm2->wayland.xwayland_associate_tmp.link);
}

static void handle_dissociate(struct wl_listener *ll, void *d)
{
	struct wlr_xwayland_surface *x = d;
	LunarWM *wm2 = wl_container_of(ll, wm2, wayland.xwayland_dissociate_tmp);
	for (size_t i = 0; i < vector_size(wm2->wayland.v_toplevels); ++i) {
		LunarWM_Toplevel *tl = wm2->wayland.v_toplevels[i];
		if (tl->is_xwayland && tl->u.xwl == x) {
			vector_remove(wm2->wayland.v_toplevels, i);
			LunarWM_Toplevel_destroy(tl);
			free(tl);
			break;
		}
	}
	wl_list_remove(&wm2->wayland.xwayland_dissociate_tmp.link);
}

static void xwl_hooks_destroy(struct XwlHooks *h)
{
	if (!h)
		return;
	if (h->associate.link.prev || h->associate.link.next)
		wl_list_remove(&h->associate.link);
	if (h->dissociate.link.prev || h->dissociate.link.next)
		wl_list_remove(&h->dissociate.link);
	if (h->destroy.link.prev || h->destroy.link.next)
		wl_list_remove(&h->destroy.link);
	free(h);
}

static void xwl_on_associate(struct wl_listener *ll, void *data)
{
	struct XwlHooks *h = wl_container_of(ll, h, associate);
	struct wlr_xwayland_surface *x = h->xwl;
	if (!x || !x->surface)
		return;

	LunarWM_Toplevel *tl = calloc(1, sizeof *tl);
	if (!tl)
		return;
	if (LunarWM_Toplevel_init_xwayland(tl, h->wm, x)) {
		vector_add(&h->wm->wayland.v_toplevels, tl);
	} else {
		free(tl);
	}
}

static void xwl_unmap_toplevel(LunarWM_Toplevel *tl)
{
	if (!tl)
		return;

	if (tl->commit.link.prev || tl->commit.link.next)
		wl_list_remove(&tl->commit.link);
	if (tl->destroy.link.prev || tl->destroy.link.next)
		wl_list_remove(&tl->destroy.link);

	if (tl->locked_buffer) {
		wlr_buffer_unlock(tl->locked_buffer);
		tl->locked_buffer = NULL;
	}

	tl->texture = NULL;
	tl->gles_texture = NULL;
	tl->rl_texture = (Texture) { 0 };
	tl->surface = NULL;
}

static void xwl_on_dissociate(struct wl_listener *ll, void *data)
{
	struct XwlHooks *h = wl_container_of(ll, h, dissociate);
	LunarWM *wm = h->wm;
	struct wlr_xwayland_surface *x = h->xwl;

	for (size_t i = 0; i < vector_size(wm->wayland.v_toplevels); ++i) {
		LunarWM_Toplevel *tl = wm->wayland.v_toplevels[i];
		if (tl->is_xwayland && tl->u.xwl == x) {
			xwl_unmap_toplevel(tl);
			break;
		}
	}
}

static void xwl_on_request_configure(struct wl_listener *ll, void *data)
{
	struct XwlHooks *xh = wl_container_of(ll, xh, req_configure);
	struct wlr_xwayland_surface_configure_event *ev = data;
	if (!xh->xwl)
		return;

	int16_t x = xh->xwl->x, y = xh->xwl->y;
	uint16_t w = xh->xwl->width, h = xh->xwl->height;

	if (ev->mask & XCB_CONFIG_WINDOW_X)
		x = ev->x;
	if (ev->mask & XCB_CONFIG_WINDOW_Y)
		y = ev->y;
	if (ev->mask & XCB_CONFIG_WINDOW_WIDTH)
		w = ev->width;
	if (ev->mask & XCB_CONFIG_WINDOW_HEIGHT)
		h = ev->height;

	clamp_to_hints(xh->xwl, &w, &h);

	wlr_xwayland_surface_configure(xh->xwl, x, y, w, h);
}

static void xwl_on_request_maximize(struct wl_listener *ll, void *data)
{
	struct XwlHooks *xh = wl_container_of(ll, xh, req_maximize);
	wlr_xwayland_surface_set_maximized(xh->xwl, true, true);
}

static void xwl_on_request_fullscreen(struct wl_listener *ll, void *data)
{
	struct XwlHooks *xh = wl_container_of(ll, xh, req_fullscreen);
	wlr_xwayland_surface_set_fullscreen(xh->xwl, true);
}

static void xwl_on_set_geometry(struct wl_listener *ll, void *data)
{
	(void)ll;
	(void)data;
}

static void xwl_on_destroy(struct wl_listener *ll, void *data)
{
	struct XwlHooks *xh = wl_container_of(ll, xh, destroy);
	LunarWM *wm = xh->wm;
	struct wlr_xwayland_surface *x = xh->xwl;

	if (xh->req_configure.link.prev)
		wl_list_remove(&xh->req_configure.link);
	if (xh->req_maximize.link.prev)
		wl_list_remove(&xh->req_maximize.link);
	if (xh->req_fullscreen.link.prev)
		wl_list_remove(&xh->req_fullscreen.link);
	if (xh->set_geometry.link.prev)
		wl_list_remove(&xh->set_geometry.link);
	if (xh->associate.link.prev)
		wl_list_remove(&xh->associate.link);
	if (xh->dissociate.link.prev)
		wl_list_remove(&xh->dissociate.link);
	if (xh->destroy.link.prev)
		wl_list_remove(&xh->destroy.link);

	for (size_t i = 0; i < vector_size(wm->wayland.v_toplevels); ++i) {
		LunarWM_Toplevel *tl = wm->wayland.v_toplevels[i];
		if (tl->is_xwayland && tl->u.xwl == x) {
			vector_remove(wm->wayland.v_toplevels, i);
			free(tl);
			break;
		}
	}

	free(xh);
}

static void xwayland_new_surface_notify(struct wl_listener *l, void *data)
{
	LunarWM *wm = wl_container_of(l, wm, wayland.xwayland_new_surface);
	struct wlr_xwayland_surface *xwl = data;

	if (xwl->surface) {
		LunarWM_Toplevel *tl = calloc(1, sizeof *tl);
		if (tl && LunarWM_Toplevel_init_xwayland(tl, wm, xwl))
			vector_add(&wm->wayland.v_toplevels, tl);
		else
			free(tl);
	}

	struct XwlHooks *h = calloc(1, sizeof *h);
	if (!h)
		return;
	h->wm = wm;
	h->xwl = xwl;

	h->associate.notify = xwl_on_associate;
	wl_signal_add(&xwl->events.associate, &h->associate);

	h->dissociate.notify = xwl_on_dissociate;
	wl_signal_add(&xwl->events.dissociate, &h->dissociate);

	h->req_configure.notify = xwl_on_request_configure;
	wl_signal_add(&xwl->events.request_configure, &h->req_configure);

	h->req_maximize.notify = xwl_on_request_maximize;
	wl_signal_add(&xwl->events.request_maximize, &h->req_maximize);

	h->req_fullscreen.notify = xwl_on_request_fullscreen;
	wl_signal_add(&xwl->events.request_fullscreen, &h->req_fullscreen);

	h->set_geometry.notify = xwl_on_set_geometry;
	wl_signal_add(&xwl->events.set_geometry, &h->set_geometry);

	h->destroy.notify = xwl_on_destroy;
	wl_signal_add(&xwl->events.destroy, &h->destroy);

	xwl->data = h;
}

void LunarWM_Toplevel_focus(LunarWM_Toplevel *this)
{
	if (!this)
		return;

	LunarWM *wm = this->server;
	struct wlr_seat *seat = wm->wayland.seat;
	struct wlr_surface *prev_surface = seat->keyboard_state.focused_surface;
	struct wlr_surface *surface = this->surface;
	if (prev_surface == surface) {
		return;
	}
	if (prev_surface) {
		struct wlr_xdg_toplevel *prev_tl
		    = wlr_xdg_toplevel_try_from_wlr_surface(prev_surface);
		if (prev_tl)
			wlr_xdg_toplevel_set_activated(prev_tl, false);
		struct wlr_xwayland_surface *prev_x
		    = wlr_xwayland_surface_try_from_wlr_surface(prev_surface);
		if (prev_x)
			wlr_xwayland_surface_activate(prev_x, false);
	}
	struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat);

	if (this->is_xwayland) {
		wlr_xwayland_surface_offer_focus(this->u.xwl);
		wlr_xwayland_surface_activate(this->u.xwl, true);
	} else {
		wlr_xdg_toplevel_set_activated(this->u.xdg, true);
	}

	if (keyboard != NULL) {
		wlr_seat_keyboard_notify_enter(seat, surface, keyboard->keycodes,
		    keyboard->num_keycodes, &keyboard->modifiers);
	}
}

static void keysym_name(xkb_keysym_t sym, char *buf, size_t bufsz)
{
	if (bufsz == 0)
		return;
	buf[0] = 0;

	char tmp[128] = { 0 };
	int n = xkb_keysym_get_name(sym, tmp, sizeof tmp);
	if (n <= 0) {
		snprintf(buf, bufsz, "Unknown");
		return;
	}

	if (strlen(tmp) == 1) {
		buf[0] = (char)toupper((unsigned char)tmp[0]);
		buf[1] = 0;
	} else {
		snprintf(buf, bufsz, "%s", tmp);
	}
}

static void Keyboard_modifiers_notify(struct wl_listener *listener, void *)
{
	auto *kbd
	    = wl_container_of(listener, (LunarWM_Keyboard *)(NULL), modifiers);
	if (!kbd->server || !kbd->server->wayland.seat) {
		return;
	}
	wlr_seat_set_keyboard(kbd->server->wayland.seat, kbd->wlr_keyboard);
	wlr_seat_keyboard_notify_modifiers(
	    kbd->server->wayland.seat, &kbd->wlr_keyboard->modifiers);
}

static void Keyboard_key_notify(struct wl_listener *listener, void *data)
{
	auto *kbd = wl_container_of(listener, (LunarWM_Keyboard *)(NULL), key);
	if (!kbd->server || !kbd->server->wayland.seat) {
		return;
	}
	auto *server = kbd->server;
	auto *event = (struct wlr_keyboard_key_event *)data;
	struct wlr_seat *seat = server->wayland.seat;

	uint32_t const keycode = event->keycode + 8;
	xkb_keysym_t const *syms = nullptr;
	int const nsyms
	    = xkb_state_key_get_syms(kbd->wlr_keyboard->xkb_state, keycode, &syms);
	xkb_keysym_t const keysym
	    = xkb_state_key_get_one_sym(kbd->wlr_keyboard->xkb_state, keycode);

	bool handled = false;
	uint32_t const modifiers = wlr_keyboard_get_modifiers(kbd->wlr_keyboard);

	if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
		if (server->wayland.session && keysym >= XKB_KEY_XF86Switch_VT_1
		    && keysym <= XKB_KEY_XF86Switch_VT_12) {
			unsigned const vt = keysym - XKB_KEY_XF86Switch_VT_1 + 1;
			wlr_session_change_vt(server->wayland.session, vt);
			return;
		}

		for (int i = 0; i < server->cman->cfg.keybindings.count; i++) {
			BindingRef ref = server->cman->cfg.keybindings.items[i];
			if (ref.mods_mask == 0 || ref.sym == XKB_KEY_NoSymbol)
				continue;

			bool sym_match = false;
			if (syms && nsyms > 0) {
				xkb_keysym_t want = xkb_keysym_to_lower(ref.sym);
				for (int s = 0; s < nsyms; ++s) {
					if (syms[s] == want) {
						sym_match = true;
						break;
					}
					if (xkb_keysym_to_lower(syms[s]) == want) {
						sym_match = true;
						break;
					}
				}
			}

			if (((modifiers & ref.mods_mask) == ref.mods_mask) && sym_match) {
				config_trigger_ref(
				    server->cman->L, &server->cman->cfg, ref.action_ref);
				handled = true;
				break;
			}
		}
	}

	if (!handled) {
		if (!seat)
			return;
		if (!kbd->wlr_keyboard)
			return;
		wlr_seat_set_keyboard(seat, kbd->wlr_keyboard);
		wlr_seat_keyboard_notify_key(
		    seat, event->time_msec, event->keycode, event->state);
	}
}

static void Keyboard_destroy_notify(struct wl_listener *listener, void *)
{
	auto *kbd = wl_container_of(listener, (LunarWM_Keyboard *)(NULL), destroy);
	wl_list_remove(&kbd->modifiers.link);
	wl_list_remove(&kbd->key.link);
	wl_list_remove(&kbd->destroy.link);
	wl_list_remove(&kbd->link);
	free(kbd);
}

static void new_input_listener_notify(struct wl_listener *listener, void *data)
{
	LunarWM *wm = wl_container_of(listener, wm, wayland.new_input_listener);
	auto *dev = (struct wlr_input_device *)data;
	if (dev->type == WLR_INPUT_DEVICE_KEYBOARD) {
		struct wlr_keyboard *wlr_keyboard = wlr_keyboard_from_input_device(dev);

		LunarWM_Keyboard *keyboard = calloc(1, sizeof(*keyboard));
		keyboard->server = wm;
		keyboard->wlr_keyboard = wlr_keyboard;

		struct xkb_rule_names const rule_names = {
			.options = wm->cman->cfg.input.keyboard.xkb_options,
		};

		wlr_log(LOG_INFO, "xkb_options=%s",
		    wm->cman->cfg.input.keyboard.xkb_options);

		struct xkb_context *context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
		struct xkb_keymap *keymap = xkb_keymap_new_from_names(
		    context, &rule_names, XKB_KEYMAP_COMPILE_NO_FLAGS);

		wlr_keyboard_set_keymap(wlr_keyboard, keymap);
		xkb_keymap_unref(keymap);
		xkb_context_unref(context);
		wlr_keyboard_set_repeat_info(wlr_keyboard, 25, 600);

		keyboard->modifiers.notify = Keyboard_modifiers_notify;
		wl_signal_add(&wlr_keyboard->events.modifiers, &keyboard->modifiers);

		keyboard->key.notify = Keyboard_key_notify;
		wl_signal_add(&wlr_keyboard->events.key, &keyboard->key);

		keyboard->destroy.notify = Keyboard_destroy_notify;
		wl_signal_add(&dev->events.destroy, &keyboard->destroy);

		wlr_seat_set_keyboard(wm->wayland.seat, keyboard->wlr_keyboard);

		wl_list_insert(&wm->wayland.keyboards, &keyboard->link);
	} else if (dev->type == WLR_INPUT_DEVICE_POINTER) {
		wlr_cursor_attach_input_device(wm->wayland.cursor, dev);
	}

	uint32_t caps = WL_SEAT_CAPABILITY_POINTER;
	if (!wl_list_empty(&wm->wayland.keyboards)) {
		caps |= WL_SEAT_CAPABILITY_KEYBOARD;
	}
	assert(wm->wayland.seat);
	wlr_seat_set_capabilities(wm->wayland.seat, caps);
}

static void new_xdg_popup_listener_notify(struct wl_listener *, void *)
{
	// FIXME: Add support for popups
}

static void new_xdg_toplevel_listener_notify(
    struct wl_listener *listener, void *data)
{
	LunarWM *wm
	    = wl_container_of(listener, wm, wayland.new_xdg_toplevel_listener);
	auto *xdg_tl = (struct wlr_xdg_toplevel *)data;

	LunarWM_Toplevel *tl = (LunarWM_Toplevel *)calloc(1, sizeof(*tl));
	if (!tl) {
		wlr_log(WLR_ERROR, "oom");
		return;
	}

	if (LunarWM_Toplevel_init_xdg(tl, wm, xdg_tl)) {
		vector_add(&wm->wayland.v_toplevels, tl);
	} else {
		wlr_log(WLR_ERROR, "Failed to initialize Toplevel.");
		free(tl);
	}
}

struct vo_client_res {
	struct wl_resource *res;
	struct wl_list link; // vo_client_res.link
};

static void vo_send_initial(struct virtual_output *vo, struct wl_resource *res)
{
	// wl_output v1..v4 ordering: geometry, mode, scale (v2+), name/desc (v4),
	// done (v2+)
	wl_output_send_geometry(res, vo->x, vo->y, vo->phys_w_mm, vo->phys_h_mm,
	    vo->subpixel, vo->make, vo->model, vo->transform);

	uint32_t flags = WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED;
	wl_output_send_mode(res, flags, vo->width, vo->height, vo->refresh_mhz);

	if (wl_resource_get_version(res) >= 2)
		wl_output_send_scale(res, vo->scale);

	if (wl_resource_get_version(res) >= 4) {
		wl_output_send_name(res, vo->name);
		wl_output_send_description(res, vo->desc);
	}

	if (wl_resource_get_version(res) >= 2)
		wl_output_send_done(res);
}

static void vo_resource_destroy(struct wl_resource *res)
{
	struct vo_client_res *cr = wl_resource_get_user_data(res);
	if (!cr)
		return;
	wl_list_remove(&cr->link);
	free(cr);
}

// wl_output requests (only release exists in v3+)
static void output_release(struct wl_client *client, struct wl_resource *res)
{
	(void)client;
	wl_resource_destroy(res);
}

static const struct wl_output_interface output_impl = {
	.release = output_release,
};

static void vo_bind(
    struct wl_client *client, void *data, uint32_t version, uint32_t id)
{
	struct virtual_output *vo = data;
	uint32_t ver = version;
	if (ver > 4)
		ver = 4; // we implement up to v4

	struct wl_resource *res
	    = wl_resource_create(client, &wl_output_interface, ver, id);
	if (!res) {
		wl_client_post_no_memory(client);
		return;
	}

	struct vo_client_res *cr = calloc(1, sizeof *cr);
	if (!cr) {
		wl_client_post_no_memory(client);
		wl_resource_destroy(res);
		return;
	}
	cr->res = res;
	wl_resource_set_implementation(res, &output_impl, cr, vo_resource_destroy);
	wl_list_insert(&vo->clients, &cr->link);

	vo_send_initial(vo, res);
}

static void vo_broadcast_mode(struct virtual_output *vo)
{
	struct vo_client_res *cr;
	wl_list_for_each(cr, &vo->clients, link)
	{
		struct wl_resource *res = cr->res;
		uint32_t flags = WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED;
		wl_output_send_mode(res, flags, vo->width, vo->height, vo->refresh_mhz);
		if (wl_resource_get_version(res) >= 2)
			wl_output_send_done(res);
	}
}

static void vo_broadcast_scale(struct virtual_output *vo)
{
	struct vo_client_res *cr;
	wl_list_for_each(cr, &vo->clients, link)
	{
		struct wl_resource *res = cr->res;
		if (wl_resource_get_version(res) >= 2) {
			wl_output_send_scale(res, vo->scale);
			wl_output_send_done(res);
		}
	}
}

static void vo_destroy(struct virtual_output *vo)
{
	// destroy client resources
	struct vo_client_res *cr, *tmp;
	wl_list_for_each_safe(cr, tmp, &vo->clients, link)
	{
		wl_resource_destroy(cr->res); // calls vo_resource_destroy
	}
	if (vo->global) {
		wl_global_destroy(vo->global);
		vo->global = NULL;
	}
	free(vo);
}

static struct virtual_output *vo_create(struct wl_display *display,
    char const *name, char const *desc, int32_t w, int32_t h,
    int32_t refresh_mhz, int32_t scale, char const *make, char const *model)
{
	struct virtual_output *vo = calloc(1, sizeof *vo);
	if (!vo)
		return NULL;
	vo->display = display;
	wl_list_init(&vo->clients);

	vo->x = 0;
	vo->y = 0;
	vo->phys_w_mm = 0;
	vo->phys_h_mm = 0; // unknown; set if you care
	vo->width = w;
	vo->height = h;
	vo->refresh_mhz = refresh_mhz;
	vo->scale = scale > 0 ? scale : 1;
	vo->subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN;
	vo->transform = WL_OUTPUT_TRANSFORM_NORMAL;
	vo->make = make ? make : "Lunar";
	vo->model = model ? model : "Virtual";
	vo->name = name;
	vo->desc = desc ? desc : name;

	vo->global
	    = wl_global_create(display, &wl_output_interface, 4, vo, vo_bind);
	if (!vo->global) {
		free(vo);
		return NULL;
	}

	return vo;
}

static bool init_wayland(LunarWM *this)
{
	wlr_log_init(WLR_DEBUG, nullptr);

	this->wayland.v_toplevels = vector_create();

	this->wayland.display = wl_display_create();
	if (this->wayland.display == nullptr) {
		return false;
		wlr_log(WLR_ERROR, "Failed to create wayland display");
	}

	this->wayland.event_loop = wl_display_get_event_loop(this->wayland.display);
	if (this->wayland.event_loop == nullptr) {
		wlr_log(WLR_ERROR, "Failed to get wayland event loop");
		return false;
	}

	this->wayland.backend
	    = wlr_backend_autocreate(this->wayland.event_loop, nullptr);
	if (this->wayland.backend == nullptr) {
		wlr_log(WLR_ERROR, "Failed to create wlroots backend");
		return false;
	}

	setenv("WLR_RENDERER", "gles2", 1);
	this->wayland.renderer = wlr_renderer_autocreate(this->wayland.backend);
	if (this->wayland.renderer == nullptr) {
		wlr_log(WLR_ERROR, "Failed to create wlroots renderer");
		return false;
	}

	this->wayland.session = wlr_session_create(this->wayland.event_loop);
	if (this->wayland.session == nullptr) {
		wlr_log(WLR_ERROR, "Failed to create session");
		return false;
	}

	this->wayland.egl = wlr_gles2_renderer_get_egl(this->wayland.renderer);
	if (this->wayland.egl == nullptr) {
		wlr_log(WLR_ERROR, "Failed to get egl information from renderer");
		return false;
	}

	this->wayland.egl_display = wlr_egl_get_display(this->wayland.egl);
	this->wayland.egl_context = wlr_egl_get_context(this->wayland.egl);
	this->wayland.egl_config = EGL_NO_CONFIG_KHR;

	if (!wlr_renderer_init_wl_display(
	        this->wayland.renderer, this->wayland.display)) {
		wlr_log(
		    WLR_ERROR, "Failed to initialize renderer with wayland display");
		return false;
	}

	this->wayland.allocator = wlr_allocator_autocreate(
	    this->wayland.backend, this->wayland.renderer);
	if (this->wayland.allocator == nullptr) {
		wlr_log(WLR_ERROR, "Failed to create wlroots allocator");
	}

	this->wayland.compositor = wlr_compositor_create(
	    this->wayland.display, 5, this->wayland.renderer);
	if (this->wayland.compositor == nullptr) {
		wlr_log(WLR_ERROR, "Failed to create wlroots compositor");
	}

	this->wayland.subcompositor
	    = wlr_subcompositor_create(this->wayland.display);
	if (this->wayland.subcompositor == nullptr) {
		wlr_log(WLR_ERROR, "Failed to create wlroots subcompositor");
	}

	this->wayland.data_device_manager
	    = wlr_data_device_manager_create(this->wayland.display);
	if (this->wayland.data_device_manager == nullptr) {
		wlr_log(WLR_ERROR, "Failed to create wlroots data device manager");
	}

	this->wayland.cursor = wlr_cursor_create();

	wl_list_init(&this->wayland.keyboards);

	this->wayland.new_input_listener.notify = new_input_listener_notify;
	wl_signal_add(&this->wayland.backend->events.new_input,
	    &this->wayland.new_input_listener);

	this->wayland.seat = wlr_seat_create(this->wayland.display, "seat0");
	if (this->wayland.seat == nullptr) {
		wlr_log(WLR_ERROR, "Failed to create wlroots seat");
		return false;
	}

	this->wayland.xdg_shell = wlr_xdg_shell_create(this->wayland.display, 3);

	this->wayland.new_xdg_toplevel_listener.notify
	    = new_xdg_toplevel_listener_notify;
	wl_signal_add(&this->wayland.xdg_shell->events.new_toplevel,
	    &this->wayland.new_xdg_toplevel_listener);

	this->wayland.new_xdg_popup_listener.notify = new_xdg_popup_listener_notify;
	wl_signal_add(&this->wayland.xdg_shell->events.new_popup,
	    &this->wayland.new_xdg_popup_listener);

	if (this->cman->cfg.xwayland.enabled) {
		this->wayland.xwayland = wlr_xwayland_create(this->wayland.display,
		    this->wayland.compositor, this->cman->cfg.xwayland.lazy);

		this->wayland.xwayland_ready.notify = xwayland_ready_notify;
		wl_signal_add(&this->wayland.xwayland->events.ready,
		    &this->wayland.xwayland_ready);

		this->wayland.xwayland_new_surface.notify = xwayland_new_surface_notify;
		wl_signal_add(&this->wayland.xwayland->events.new_surface,
		    &this->wayland.xwayland_new_surface);
	}

	return true;
}

static uint32_t get_swapchain_image(
    LunarWM *this, int swapchain_images_i, uint32_t index)
{
	return this->xr.swapchain_images[swapchain_images_i].a_imgs[index].image;
}

static bool init_xr(LunarWM *this)
{
	XrResult res = XR_SUCCESS;

	XrApplicationInfo app_info = {
		.applicationVersion = 1,
		.engineVersion = 1,
		.apiVersion = XR_CURRENT_API_VERSION,
	};
	strncpy((char *)app_info.applicationName, "LunarWM",
	    XR_MAX_APPLICATION_NAME_SIZE);
	strncpy(
	    (char *)app_info.engineName, "LunarWM Engine", XR_MAX_ENGINE_NAME_SIZE);

	char const *instance_extensions[] = {
		XR_EXT_DEBUG_UTILS_EXTENSION_NAME,
		XR_MNDX_EGL_ENABLE_EXTENSION_NAME,
		XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME,
		XR_EXT_HAND_TRACKING_EXTENSION_NAME,
	};
	char const **v_active_instance_extensions = vector_create();

	uint32_t extension_properties_count = 0;
	XrExtensionProperties *extension_properties;
	if (xrEnumerateInstanceExtensionProperties(
	        nullptr, 0, &extension_properties_count, nullptr)
	    != XR_SUCCESS) {
		wlr_log(WLR_ERROR,
		    "Failed to enumerate OpenXR instance extension properties (count)");
		return false;
	}
	extension_properties
	    = malloc(sizeof(*extension_properties) * extension_properties_count);
	for (uint32_t i = 0; i < extension_properties_count; ++i) {
		extension_properties[i].type = XR_TYPE_EXTENSION_PROPERTIES;
		extension_properties[i].next = NULL;
	}

	if (xrEnumerateInstanceExtensionProperties(nullptr,
	        extension_properties_count, &extension_properties_count,
	        extension_properties)
	    != XR_SUCCESS) {
		wlr_log(WLR_ERROR,
		    "Failed to enumerate OpenXR instance extension properties");
		return false;
	}

	for (size_t i = 0; i < ARRAY_SZ(instance_extensions); i++) {
		char const *requested_instance_extension = instance_extensions[i];
		bool found = false;
		for (int j = 0; j < extension_properties_count; j++) {
			if (strcmp(requested_instance_extension,
			        extension_properties[j].extensionName)
			    != 0) {
				continue;
			}

			vector_add(
			    &v_active_instance_extensions, requested_instance_extension);
			found = true;
			break;
		}

		if (!found) {
			wlr_log(WLR_ERROR, "Failed to find OpenXR instance extension: %s",
			    requested_instance_extension);
			return false;
		}
	}

	{
		XrInstanceCreateInfo const ci = {
			.type = XR_TYPE_INSTANCE_CREATE_INFO,
			.next = nullptr,
			.createFlags = 0,
			.applicationInfo = app_info,
			.enabledApiLayerCount = 0,
			.enabledApiLayerNames = nullptr,
			.enabledExtensionCount
			= (uint32_t)vector_size(v_active_instance_extensions),
			.enabledExtensionNames = v_active_instance_extensions,
		};

		XrInstance instance = nullptr;
		if (xrCreateInstance(&ci, &instance) != XR_SUCCESS) {
			wlr_log(WLR_ERROR, "Failed to create OpenXR instance");
			return false;
		}
		this->xr.instance = instance;

		res = xrGetInstanceProcAddr(this->xr.instance, "xrCreateHandTrackerEXT",
		    (PFN_xrVoidFunction *)&this->xr.CreateHandTrackerEXT);
		if (res != XR_SUCCESS) {
			wlr_log(
			    WLR_ERROR, "Failed to get proc addr xrCreateHandTrackerEXT");
			return false;
		}
		res = xrGetInstanceProcAddr(this->xr.instance,
		    "xrDestroyHandTrackerEXT",
		    (PFN_xrVoidFunction *)&this->xr.DestroyHandTrackerEXT);
		if (res != XR_SUCCESS) {
			wlr_log(
			    WLR_ERROR, "Failed to get proc addr xrDestroyHandTrackerEXT");
			return false;
		}
		res = xrGetInstanceProcAddr(this->xr.instance, "xrLocateHandJointsEXT",
		    (PFN_xrVoidFunction *)&this->xr.LocateHandJointsEXT);
		if (res != XR_SUCCESS) {
			wlr_log(WLR_ERROR, "Failed to get proc addr xrLocateHandJointsEXT");
			return false;
		}
	}

	{
		XrSystemGetInfo gi = {
			.type = XR_TYPE_SYSTEM_GET_INFO,
			.next = nullptr,
		};

		XrFormFactor const factors[2] = {
			XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY,
			XR_FORM_FACTOR_HANDHELD_DISPLAY,
		};
		for (size_t i = 0; i < ARRAY_SZ(factors); i++) {
			auto factor = factors[i];

			gi.formFactor = factor;
			XrSystemId system_id = 0;
			if (xrGetSystem(this->xr.instance, &gi, &system_id) == XR_SUCCESS) {
				this->xr.system_id = system_id;
				break;
			}
		}

		if (!this->xr.system_id) {
			wlr_log(WLR_ERROR, "Failed to find valid form factor");
			return false;
		}
	}

	{
		XrSystemProperties system_props = {
			.type = XR_TYPE_SYSTEM_PROPERTIES,
			.next = &this->xr.hand_tracking_system_properties,
		};
		res = xrGetSystemProperties(
		    this->xr.instance, this->xr.system_id, &system_props);
		if (res != XR_SUCCESS) {
			wlr_log(WLR_ERROR, "xrGetSystemProperties failed: %d", res);
			return false;
		}
	}

	XrGraphicsRequirementsOpenGLESKHR reqs = {
		.type = XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR,
		.next = nullptr,
	};
	PFN_xrGetOpenGLESGraphicsRequirementsKHR
	    xrGetOpenGLESGraphicsRequirementsKHR
	    = nullptr;
	xrGetInstanceProcAddr(this->xr.instance,
	    "xrGetOpenGLESGraphicsRequirementsKHR",
	    (PFN_xrVoidFunction *)&xrGetOpenGLESGraphicsRequirementsKHR);
	if (xrGetOpenGLESGraphicsRequirementsKHR(
	        this->xr.instance, this->xr.system_id, &reqs)
	    != XR_SUCCESS) {
		wlr_log(WLR_ERROR, "Failed to get GLES graphics requirements");
		return false;
	}
	wlr_log(WLR_INFO, "OpenGL ES range: %d.%d.%d - %d.%d.%d\n",
	    XR_VERSION_MAJOR(reqs.minApiVersionSupported),
	    XR_VERSION_MINOR(reqs.minApiVersionSupported),
	    XR_VERSION_PATCH(reqs.minApiVersionSupported),
	    XR_VERSION_MAJOR(reqs.maxApiVersionSupported),
	    XR_VERSION_MINOR(reqs.maxApiVersionSupported),
	    XR_VERSION_PATCH(reqs.maxApiVersionSupported));

	glEnable(GL_DEBUG_OUTPUT_KHR);
	glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS_KHR);

	{
		XrGraphicsBindingEGLMNDX gbind = {
			.type = XR_TYPE_GRAPHICS_BINDING_EGL_MNDX,
			.next = nullptr,
			.getProcAddress = eglGetProcAddress,
			.display = this->wayland.egl_display,
			.config = this->wayland.egl_config,
			.context = this->wayland.egl_context,
		};

		XrSessionCreateInfo const ci = {
			.type = XR_TYPE_SESSION_CREATE_INFO,
			.next = &gbind,
			.createFlags = 0,
			.systemId = this->xr.system_id,
		};

		if (xrCreateSession(this->xr.instance, &ci, &this->xr.session)
		    != XR_SUCCESS) {
			wlr_log(WLR_ERROR, "Failed to create OpenXR session");
			return false;
		}
	}

	// Swapchain time!
	XrViewConfigurationType *a_view_config_types;
	uint32_t view_config_types_count = 0;
	{
		if (xrEnumerateViewConfigurations(this->xr.instance, this->xr.system_id,
		        0, &view_config_types_count, nullptr)
		    != XR_SUCCESS) {
			wlr_log(WLR_ERROR,
			    "Failed to get amount of OpenXR view configurations");
			return false;
		}
		a_view_config_types
		    = malloc(sizeof(*a_view_config_types) * view_config_types_count);
		for (uint32_t i = 0; i < this->xr.view_configuration_views_count; ++i) {
			this->xr.a_view_configuration_views[i].type
			    = XR_TYPE_VIEW_CONFIGURATION_VIEW;
			this->xr.a_view_configuration_views[i].next = NULL;
		}
		if (xrEnumerateViewConfigurations(this->xr.instance, this->xr.system_id,
		        view_config_types_count, &view_config_types_count,
		        a_view_config_types)
		    != XR_SUCCESS) {
			wlr_log(
			    WLR_ERROR, "Failed to enumerate OpenXR view configurations");
			return false;
		}
	}

	{
		bool found = false;
		for (size_t i = 0; i < view_config_types_count; i++) {
			if (a_view_config_types[i]
			    == XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO) {
				found = true;
				break;
			}
		}
		if (!found) {
			wlr_log(WLR_ERROR,
			    "XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO not present");
			return false;
		}
	}

	{
		if (xrEnumerateViewConfigurationViews(this->xr.instance,
		        this->xr.system_id, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
		        0, &this->xr.view_configuration_views_count, nullptr)
		    != XR_SUCCESS) {
			wlr_log(WLR_ERROR,
			    "Failed to get amount of OpenXR view configuration views");
			return false;
		}
		this->xr.a_view_configuration_views
		    = malloc(sizeof(*this->xr.a_view_configuration_views)
		        * this->xr.view_configuration_views_count);
		for (uint32_t i = 0; i < this->xr.view_configuration_views_count; ++i) {
			this->xr.a_view_configuration_views[i].type
			    = XR_TYPE_VIEW_CONFIGURATION_VIEW;
			this->xr.a_view_configuration_views[i].next = NULL;
		}
		if (xrEnumerateViewConfigurationViews(this->xr.instance,
		        this->xr.system_id, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
		        this->xr.view_configuration_views_count,
		        &this->xr.view_configuration_views_count,
		        this->xr.a_view_configuration_views)
		    != XR_SUCCESS) {
			wlr_log(WLR_ERROR,
			    "Failed to enumerate OpenXR view configuration views");
			return false;
		}
	}

	int64_t *a_swapchain_formats;
	uint32_t a_swapchain_formats_count = 0;
	{
		if (xrEnumerateSwapchainFormats(
		        this->xr.session, 0, &a_swapchain_formats_count, nullptr)
		    != XR_SUCCESS) {
			wlr_log(
			    WLR_ERROR, "Failed to get amount of OpenXR swapchain formats");
			return false;
		}
		a_swapchain_formats
		    = malloc(sizeof(*a_swapchain_formats) * a_swapchain_formats_count);
		if (xrEnumerateSwapchainFormats(this->xr.session,
		        a_swapchain_formats_count, &a_swapchain_formats_count,
		        a_swapchain_formats)
		    != XR_SUCCESS) {
			wlr_log(WLR_ERROR, "Failed to enumerate OpenXR swapchain formats");
			return false;
		}
	}

	{
		bool found = false;
		for (size_t i = 0; i < a_swapchain_formats_count; i++) {
			if (a_swapchain_formats[i] == GL_DEPTH_COMPONENT16) {
				found = true;
				break;
			}
		}
		if (!found) {
			wlr_log(
			    WLR_ERROR, "Failed to find satisfying depth swapchain format");
			return false;
		}
	}

	auto const swapchain_format_depth = GL_DEPTH_COMPONENT16;
	auto const swapchain_format_color = GL_SRGB8_ALPHA8;

	{
		bool found = false;
		for (size_t i = 0; i < a_swapchain_formats_count; i++) {
			if (a_swapchain_formats[i] == GL_SRGB8_ALPHA8) {
				found = true;
				break;
			}
		}
		if (!found) {
			wlr_log(
			    WLR_ERROR, "Failed to find satisfying color swapchain format");
			return false;
		}
	}

	uint32_t const view_count = this->xr.view_configuration_views_count;
	uint32_t const eye_w
	    = this->xr.a_view_configuration_views[0].recommendedImageRectWidth;
	uint32_t const eye_h
	    = this->xr.a_view_configuration_views[0].recommendedImageRectHeight;
	uint32_t const buf_w = eye_w * view_count;

	if (!this->xr.swapchains.v_color) {
		this->xr.swapchains.v_color = vector_create();
	}
	if (!this->xr.swapchains.v_depth) {
		this->xr.swapchains.v_depth = vector_create();
	}

	vector_add(&this->xr.swapchains.v_color, (LunarWM_SwapchainInfo) {});
	vector_add(&this->xr.swapchains.v_depth, (LunarWM_SwapchainInfo) {});

	{
		auto *color_sc = &this->xr.swapchains.v_color[0];
		auto *depth_sc = &this->xr.swapchains.v_depth[0];
		auto *vcv = &this->xr.a_view_configuration_views[0];

		{ // Create color swapchain
			XrSwapchainCreateInfo const ci = {
				.type = XR_TYPE_SWAPCHAIN_CREATE_INFO,
				.next = nullptr,
				.createFlags = 0,
				.usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT
				    | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT,
				.format = swapchain_format_color,
				.sampleCount = vcv->recommendedSwapchainSampleCount,
				.width = buf_w,
				.height = eye_h,
				.faceCount = 1,
				.arraySize = 1,
				.mipCount = 1,
			};
			color_sc->swapchain_format = ci.format;

			if (xrCreateSwapchain(this->xr.session, &ci, &color_sc->swapchain)
			    != XR_SUCCESS) {
				wlr_log(WLR_ERROR, "Failed to create OpenXR color swapchain");
				return false;
			}
		}

		{ // Create depth swapchain
			XrSwapchainCreateInfo const ci = {
				.type = XR_TYPE_SWAPCHAIN_CREATE_INFO,
				.next = nullptr,
				.createFlags = 0,
				.usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT
				    | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
				.format = swapchain_format_depth,
				.sampleCount = vcv->recommendedSwapchainSampleCount,
				.width = buf_w,
				.height = eye_h,
				.faceCount = 1,
				.arraySize = 1,
				.mipCount = 1,
			};
			depth_sc->swapchain_format = ci.format;

			if (xrCreateSwapchain(this->xr.session, &ci, &depth_sc->swapchain)
			    != XR_SUCCESS) {
				wlr_log(WLR_ERROR, "Failed to create OpenXR depth swapchain");
				return false;
			}
		}

		// Enumerate swapchain images
		{ // Color
			this->xr.swapchain_images[0].handle = color_sc->swapchain;
			if (xrEnumerateSwapchainImages(color_sc->swapchain, 0,
			        &this->xr.swapchain_images[0].a_imgs_count, nullptr)
			    != XR_SUCCESS) {
				wlr_log(
				    WLR_ERROR, "Failed to get Color Swapchain Images count.");
				return false;
			}
			this->xr.swapchain_images[0].a_imgs
			    = malloc(sizeof(*this->xr.swapchain_images[0].a_imgs)
			        * this->xr.swapchain_images[0].a_imgs_count);
			for (uint32_t i = 0; i < this->xr.swapchain_images[0].a_imgs_count;
			    ++i) {
				this->xr.swapchain_images[0].a_imgs[i].type
				    = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR;
				this->xr.swapchain_images[0].a_imgs[i].next = NULL;
			}
			if (xrEnumerateSwapchainImages(color_sc->swapchain,
			        this->xr.swapchain_images[0].a_imgs_count,
			        &this->xr.swapchain_images[0].a_imgs_count,
			        (XrSwapchainImageBaseHeader *)this->xr.swapchain_images[0]
			            .a_imgs)
			    != XR_SUCCESS) {
				wlr_log(
				    WLR_ERROR, "Failed to enumerate color swapchain images.");
				return false;
			}
		}
		{ // Depth
			this->xr.swapchain_images[1].handle = depth_sc->swapchain;
			if (xrEnumerateSwapchainImages(depth_sc->swapchain, 0,
			        &this->xr.swapchain_images[1].a_imgs_count, nullptr)
			    != XR_SUCCESS) {
				wlr_log(
				    WLR_ERROR, "Failed to get depth Swapchain Images count.");
				return false;
			}
			this->xr.swapchain_images[1].a_imgs
			    = malloc(sizeof(*this->xr.swapchain_images[1].a_imgs)
			        * this->xr.swapchain_images[1].a_imgs_count);
			for (uint32_t i = 0; i < this->xr.swapchain_images[1].a_imgs_count;
			    ++i) {
				this->xr.swapchain_images[1].a_imgs[i].type
				    = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR;
				this->xr.swapchain_images[1].a_imgs[i].next = NULL;
			}
			if (xrEnumerateSwapchainImages(depth_sc->swapchain,
			        this->xr.swapchain_images[1].a_imgs_count,
			        &this->xr.swapchain_images[1].a_imgs_count,
			        (XrSwapchainImageBaseHeader *)this->xr.swapchain_images[1]
			            .a_imgs)
			    != XR_SUCCESS) {
				wlr_log(
				    WLR_ERROR, "Failed to enumerate depth swapchain images.");
				return false;
			}
		}

		// Get image views
		for (uint32_t j = 0; j < this->xr.swapchain_images[0].a_imgs_count;
		    j++) {
			GLuint framebuffer = 0;
			glGenFramebuffers(1, &framebuffer);

			GLenum const attachment = GL_COLOR_ATTACHMENT0;

			glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
			glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, attachment,
			    GL_TEXTURE_2D, get_swapchain_image(this, 0, j), 0);

			GLenum const result = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
			if (result != GL_FRAMEBUFFER_COMPLETE) {
				wlr_log(WLR_ERROR, "Failed to create color framebuffer");
				return false;
			}
			glBindFramebuffer(GL_FRAMEBUFFER, 0);

			if (!color_sc->v_image_views) {
				color_sc->v_image_views = vector_create();
			}
			vector_add(&color_sc->v_image_views, framebuffer);
		}

		for (uint32_t j = 0; j < this->xr.swapchain_images[1].a_imgs_count;
		    j++) {
			GLuint framebuffer = 0;
			glGenFramebuffers(1, &framebuffer);

			GLenum const attachment = GL_DEPTH_ATTACHMENT;

			glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
			glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, attachment,
			    GL_TEXTURE_2D, get_swapchain_image(this, 1, j), 0);

			GLenum const result = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
			if (result != GL_FRAMEBUFFER_COMPLETE) {
				wlr_log(WLR_ERROR, "Failed to create depth framebuffer");
				return false;
			}
			glBindFramebuffer(GL_FRAMEBUFFER, 0);

			if (!depth_sc->v_image_views) {
				depth_sc->v_image_views = vector_create();
			}
			vector_add(&depth_sc->v_image_views, framebuffer);
		}
	}

	assert(this->xr.instance);
	assert(this->xr.system_id);

	XrEnvironmentBlendMode *a_environment_blend_modes;
	uint32_t a_environment_blend_modes_count = 0;
	{ // Get available blend modes
		if (xrEnumerateEnvironmentBlendModes(this->xr.instance,
		        this->xr.system_id, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
		        0, &a_environment_blend_modes_count, nullptr)
		    != XR_SUCCESS) {
			wlr_log(
			    WLR_ERROR, "Failed to get OpenXR environment blend mode count");
			return false;
		}
		a_environment_blend_modes = malloc(sizeof(*a_environment_blend_modes)
		    * a_environment_blend_modes_count);
		if (xrEnumerateEnvironmentBlendModes(this->xr.instance,
		        this->xr.system_id, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
		        a_environment_blend_modes_count,
		        &a_environment_blend_modes_count, a_environment_blend_modes)
		    != XR_SUCCESS) {
			wlr_log(WLR_ERROR, "Failed to get XR environment blend modes");
			return false;
		}
	}
	XrEnvironmentBlendMode const requested_environment_blend_modes[] = {
		XR_ENVIRONMENT_BLEND_MODE_OPAQUE,
		XR_ENVIRONMENT_BLEND_MODE_ADDITIVE,
		XR_ENVIRONMENT_BLEND_MODE_MAX_ENUM,
	};

	for (size_t i = 0; i < ARRAY_SZ(requested_environment_blend_modes); i++) {
		this->xr.environment_blend_mode = requested_environment_blend_modes[i];
		bool found = false;
		for (size_t j = 0; j < a_environment_blend_modes_count; j++) {
			if (requested_environment_blend_modes[i]
			    == a_environment_blend_modes[j]) {
				found = true;
				break;
			}
		}
		if (found) {
			break;
		}
	}
	if (this->xr.environment_blend_mode == XR_ENVIRONMENT_BLEND_MODE_MAX_ENUM) {
		wlr_log(WLR_INFO,
		    "Failed to find a compatible blend mode. Defaulting to "
		    "XR_ENVIRONMENT_BLEND_MODE_OPAQUE.");
		this->xr.environment_blend_mode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
	}

	{ // Reference space
		XrReferenceSpaceCreateInfo const ci = {
			.type = XR_TYPE_REFERENCE_SPACE_CREATE_INFO,
			.next = nullptr,
			.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL,
			.poseInReferenceSpace = { .orientation = { 0.0f, 0.0f, 0.0f, 1.0f },
			    .position = { 0.0f, 0.0f, 0.0f } },
		};

		if (xrCreateReferenceSpace(this->xr.session, &ci, &this->xr.local_space)
		    != XR_SUCCESS) {
			wlr_log(WLR_ERROR, "Failed to create OpenXR reference space");
			return false;
		}
	}

	{ // View reference space
		XrReferenceSpaceCreateInfo const ci = {
			.type = XR_TYPE_REFERENCE_SPACE_CREATE_INFO,
			.next = nullptr,
			.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW,
			.poseInReferenceSpace = { .orientation = { 0.0f, 0.0f, 0.0f, 1.0f },
			    .position = { 0.0f, 0.0f, 0.0f } },
		};

		if (xrCreateReferenceSpace(this->xr.session, &ci, &this->xr.view_space)
		    != XR_SUCCESS) {
			wlr_log(WLR_ERROR, "Failed to create OpenXR reference space");
			return false;
		}
	}

	{ // Create hand trackers
		for (size_t i = 0; i < 2; i++) {
			auto *hand = &this->xr.hands[i];

			XrHandTrackerCreateInfoEXT const ci = {
				.type = XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT,
				.hand = i == 0 ? XR_HAND_LEFT_EXT : XR_HAND_RIGHT_EXT,
				.handJointSet = XR_HAND_JOINT_SET_DEFAULT_EXT,
			};

			res = this->xr.CreateHandTrackerEXT(
			    this->xr.session, &ci, &hand->hand_tracker);
			if (res != XR_SUCCESS) {
				wlr_log(WLR_ERROR, "Failed to create hand tracker: %d", res);
				return false;
			}
		}
	}

	free(extension_properties);
	free(a_view_config_types);
	free(a_swapchain_formats);
	free(a_environment_blend_modes);
	vector_free(v_active_instance_extensions);

	return true;
}

static void sync_config(LunarWM *this)
{
	if (this->cman->cfg.cubemap) {
		Skybox_init(&this->renderer.skybox, this->cman->cfg.cubemap);
	} else {
		Skybox_destroy(&this->renderer.skybox);
	}

	if (IsTextureValid(this->renderer.hud_rt.texture)) {
		UnloadTexture(this->renderer.hud_rt.texture);
		this->renderer.hud_rt.texture.id = 0;
		this->renderer.hud_rt.texture.width = 0;
		this->renderer.hud_rt.texture.height = 0;
	}

	if (this->wayland.custom_out_virtual) {
		vo_destroy(this->wayland.custom_out_virtual);
		this->wayland.custom_out_virtual = NULL;
	}
	if (this->wayland.custom_out_hud) {
		vo_destroy(this->wayland.custom_out_hud);
		this->wayland.custom_out_hud = NULL;
	}

	int vw = (int)this->cman->cfg.displays.virtual.resolution.x;
	int vh = (int)this->cman->cfg.displays.virtual.resolution.y;
	int hud = this->cman->cfg.displays.hud.size;

	this->wayland.custom_out_virtual = vo_create(this->wayland.display,
	    "Virtual", "Virtual output", vw, vh, 60000, 1, "LunarWM", "Virtual");
	this->wayland.custom_out_hud = vo_create(this->wayland.display, "HUD",
	    "HUD output", hud, hud, 60000, 1, "LunarWM", "HUD");
}

extern char **environ;
static int l_exec(lua_State *L)
{
	char const *cmd = luaL_checkstring(L, 1);

	pid_t pid;
	char *argv[] = { (char *)"sh", (char *)"-c", (char *)cmd, NULL };

	int rc = posix_spawnp(&pid, "sh", NULL, NULL, argv, environ);
	if (rc != 0) {
		lua_pushnil(L);
		lua_pushfstring(L, "posix_spawnp failed: %s", strerror(rc));
		return 2;
	}

	lua_pushinteger(L, (lua_Integer)pid);
	return 1;
}

static int l_cycle_next(lua_State *L)
{
	if (vector_size(g_wm.wayland.v_toplevels) == 0) {
		lua_pushnil(L);
		return 1;
	}

	g_wm.wayland.current_focus++;
	if (g_wm.wayland.current_focus >= vector_size(g_wm.wayland.v_toplevels)) {
		g_wm.wayland.current_focus = 0;
	}

	LunarWM_Toplevel *tl = g_wm.wayland.v_toplevels[g_wm.wayland.current_focus];
	LunarWM_Toplevel_focus(tl);

	lua_pushnil(L);
	return 1;
}

static int l_recenter(lua_State *L)
{
	(void)L;

	Vector3 pos = g_wm.renderer.camera.position;
	Vector3 fwd
	    = Vector3Normalize(Vector3Subtract(g_wm.renderer.camera.target, pos));

	float len_xz = sqrtf(fwd.x * fwd.x + fwd.z * fwd.z);
	float yaw = (len_xz > 1e-6f) ? atan2f(fwd.x, fwd.z) : 0.0f;

	Quaternion q_step = QuaternionFromAxisAngle((Vector3) { 0, 1, 0 }, -yaw);
	Vector3 t_step = Vector3Negate(Vector3RotateByQuaternion(pos, q_step));

	Quaternion q_total = QuaternionMultiply(q_step, g_wm.xr.recenter_rot);
	Vector3 t_total = Vector3Add(
	    Vector3RotateByQuaternion(g_wm.xr.recenter_trans, q_step), t_step);

	g_wm.xr.recenter_rot = q_total;
	g_wm.xr.recenter_trans = t_total;
	g_wm.xr.recenter_active = true;

	lua_pushnil(L);
	return 1;
}

static int l_reload_config(lua_State *L)
{
	ConfigManager *cm = g_wm.cman;
	if (!cm) {
		lua_pushnil(L);
		return 1;
	}

	config_manager_reload(cm);
	sync_config(&g_wm);
	lua_pushnil(L);
	return 1;
}

static int l_quit_compositor(lua_State *L)
{
	LunarWM_terminate(&g_wm);
	lua_pushnil(L);
	return 1;
}

bool LunarWM_init(LunarWM *this)
{
	{ // Init defaults
		memset(this, 0, sizeof(*this));
		this->xr.session = XR_NULL_HANDLE;
		this->xr.session_state = XR_SESSION_STATE_UNKNOWN;
		this->xr.environment_blend_mode = XR_ENVIRONMENT_BLEND_MODE_MAX_ENUM;
		this->xr.local_space = this->xr.view_space = XR_NULL_HANDLE;
		this->xr.hand_tracking_system_properties.type
		    = XR_TYPE_SYSTEM_HAND_TRACKING_PROPERTIES_EXT;
		this->renderer.camera.position = (Vector3) { 0, 0, 0 };
		this->renderer.camera.target = (Vector3) { 0, 0, 1 };
		this->renderer.camera.up = (Vector3) { 0, 1, 0 };
		this->renderer.camera.fovy = 45;
		this->renderer.camera.projection = CAMERA_PERSPECTIVE;
		this->xr.recenter_rot = (Quaternion) { 0, 0, 0, 1 };
		this->xr.recenter_trans = (Vector3) { 0, 0, 0 };
		this->xr.recenter_active = false;
	}

	this->cman = config_manager_create(get_config_path());
	assert(this->cman);

	{
		lua_State *L = this->cman->L;

		lua_newtable(L);
		lua_pushcfunction(L, l_quit_compositor);
		lua_setfield(L, -2, "quit_compositor");
		lua_pushcfunction(L, l_reload_config);
		lua_setfield(L, -2, "reload_config");
		lua_pushcfunction(L, l_recenter);
		lua_setfield(L, -2, "recenter");
		lua_pushcfunction(L, l_cycle_next);
		lua_setfield(L, -2, "cycle_next");
		lua_pushcfunction(L, l_exec);
		lua_setfield(L, -2, "exec");
		lua_setglobal(L, "lunar");

		config_manager_reload(this->cman);
	}

	if (getenv("DISPLAY") != nullptr || getenv("WAYLAND_DISPLAY") != nullptr) {
		wlr_log(WLR_ERROR, "This compositor can only be ran in DRM mode.");
		return false;
	}

	if (!init_wayland(this)) {
		wlr_log(WLR_ERROR, "Failed to initialize wlroots");
		return false;
	}

	auto *draw = eglGetCurrentSurface(EGL_DRAW);
	auto *read = eglGetCurrentSurface(EGL_READ);
	if (eglMakeCurrent(this->wayland.egl_display, EGL_NO_SURFACE,
	        EGL_NO_SURFACE, this->wayland.egl_context)
	    == EGL_FALSE) {
		wlr_log(WLR_ERROR, "Failed to eglMakeCurrent");
		return false;
	}

	if (!init_xr(this)) {
		wlr_log(WLR_ERROR, "Failed to initialize OpenXR");
		return false;
	}

	wlr_log(WLR_INFO, "OpenGL ES version: %s", glGetString(GL_VERSION));
	InitWindow(0, 0, "");

	if (eglMakeCurrent(
	        this->wayland.egl_display, draw, read, this->wayland.egl_context)
	    == EGL_FALSE) {
		wlr_log(WLR_ERROR, "Failed to eglMakeCurrent");
		return false;
	}

	sync_config(this);

	this->initialized = true;

	return true;
}

static void free_swapchain_info(LunarWM_SwapchainInfo *sc)
{
	if (!sc)
		return;
	if (sc->v_image_views) {
		for (size_t i = 0; i < vector_size(sc->v_image_views); ++i) {
			GLuint fb = sc->v_image_views[i];
			if (fb)
				glDeleteFramebuffers(1, &fb);
		}
		vector_free(sc->v_image_views);
		sc->v_image_views = NULL;
	}
	if (sc->swapchain) {
		xrDestroySwapchain(sc->swapchain);
		sc->swapchain = XR_NULL_HANDLE;
	}
}

static void cleanup_xr(LunarWM *this)
{
	for (int i = 0; i < 2; ++i) {
		if (this->xr.swapchain_images[i].a_imgs) {
			free(this->xr.swapchain_images[i].a_imgs);
			this->xr.swapchain_images[i].a_imgs = NULL;
			this->xr.swapchain_images[i].a_imgs_count = 0;
		}
	}

	if (this->xr.swapchains.v_color) {
		for (size_t i = 0; i < vector_size(this->xr.swapchains.v_color); ++i)
			free_swapchain_info(&this->xr.swapchains.v_color[i]);
		vector_free(this->xr.swapchains.v_color);
		this->xr.swapchains.v_color = NULL;
	}
	if (this->xr.swapchains.v_depth) {
		for (size_t i = 0; i < vector_size(this->xr.swapchains.v_depth); ++i)
			free_swapchain_info(&this->xr.swapchains.v_depth[i]);
		vector_free(this->xr.swapchains.v_depth);
		this->xr.swapchains.v_depth = NULL;
	}

	if (this->renderer.fbo) {
		glDeleteFramebuffers(1, &this->renderer.fbo);
		this->renderer.fbo = 0;
	}
	this->renderer.tmp_rt = (RenderTexture2D) { 0 };

	if (this->xr.view_space)
		xrDestroySpace(this->xr.view_space),
		    this->xr.view_space = XR_NULL_HANDLE;
	if (this->xr.local_space)
		xrDestroySpace(this->xr.local_space),
		    this->xr.local_space = XR_NULL_HANDLE;

	for (size_t i = 0; i < 2; ++i) {
		if (this->xr.hands[i].hand_tracker && this->xr.DestroyHandTrackerEXT)
			this->xr.DestroyHandTrackerEXT(this->xr.hands[i].hand_tracker);
		this->xr.hands[i].hand_tracker = XR_NULL_HANDLE;
	}

	if (this->xr.a_view_configuration_views) {
		free(this->xr.a_view_configuration_views);
		this->xr.a_view_configuration_views = NULL;
		this->xr.view_configuration_views_count = 0;
	}

	if (this->xr.session != XR_NULL_HANDLE) {
		xrDestroySession(this->xr.session);
		this->xr.session = XR_NULL_HANDLE;
		this->xr.session_running = false;
	}

	if (this->xr.instance)
		xrDestroyInstance(this->xr.instance),
		    this->xr.instance = XR_NULL_HANDLE;
}

static void cleanup_wayland(LunarWM *this)
{
	assert(this);

	if (this->wayland.custom_out_virtual) {
		vo_destroy(this->wayland.custom_out_virtual);
		this->wayland.custom_out_virtual = NULL;
	}
	if (this->wayland.custom_out_hud) {
		vo_destroy(this->wayland.custom_out_hud);
		this->wayland.custom_out_hud = NULL;
	}

	if (this->wayland.xwayland) {
		if (this->wayland.xwayland_new_surface.link.prev
		    || this->wayland.xwayland_new_surface.link.next) {
			wl_list_remove(&this->wayland.xwayland_new_surface.link);
			this->wayland.xwayland_new_surface.notify = NULL;
		}
		if (this->wayland.xwayland_ready.link.prev
		    || this->wayland.xwayland_ready.link.next) {
			wl_list_remove(&this->wayland.xwayland_ready.link);
			this->wayland.xwayland_ready.notify = NULL;
		}
		wlr_xwayland_destroy(this->wayland.xwayland);
		this->wayland.xwayland = NULL;
	}

	if (this->wayland.new_xdg_toplevel_listener.link.prev
	    || this->wayland.new_xdg_toplevel_listener.link.next) {
		wl_list_remove(&this->wayland.new_xdg_toplevel_listener.link);
		this->wayland.new_xdg_toplevel_listener.notify = NULL;
	}
	if (this->wayland.new_xdg_popup_listener.link.prev
	    || this->wayland.new_xdg_popup_listener.link.next) {
		wl_list_remove(&this->wayland.new_xdg_popup_listener.link);
		this->wayland.new_xdg_popup_listener.notify = NULL;
	}
	if (this->wayland.new_input_listener.link.prev
	    || this->wayland.new_input_listener.link.next) {
		wl_list_remove(&this->wayland.new_input_listener.link);
		this->wayland.new_input_listener.notify = NULL;
	}

	if (this->wayland.v_toplevels) {
		for (size_t i = 0; i < vector_size(this->wayland.v_toplevels); ++i) {
			if (this->wayland.v_toplevels[i]) {
				LunarWM_Toplevel_destroy(this->wayland.v_toplevels[i]);
				free(this->wayland.v_toplevels[i]);
			}
		}
		vector_free(this->wayland.v_toplevels);
		this->wayland.v_toplevels = NULL;
	}

	if (this->wayland.cursor) {
		wlr_cursor_destroy(this->wayland.cursor);
		this->wayland.cursor = NULL;
	}

	if (this->wayland.backend) {
		wlr_backend_destroy(this->wayland.backend);
		this->wayland.backend = NULL;
	}

	if (this->wayland.seat) {
		wlr_seat_destroy(this->wayland.seat);
		this->wayland.seat = NULL;
	}

	if (this->wayland.allocator) {
		wlr_allocator_destroy(this->wayland.allocator);
		this->wayland.allocator = NULL;
	}
	if (this->wayland.renderer) {
		wlr_renderer_destroy(this->wayland.renderer);
		this->wayland.renderer = NULL;
	}
	if (this->wayland.display) {
		wl_display_destroy(this->wayland.display);
		this->wayland.display = NULL;
	}
}

static void cleanup_lua_cfg(LunarWM *this)
{
	if (this->cman) {
		config_manager_destroy(this->cman);
		this->cman = NULL;
	}
}

static void cleanup_raylib_egl(LunarWM *this)
{
	if (IsWindowReady()) {
		CloseWindow();
	}
}

void LunarWM_destroy(LunarWM *this)
{
	if (!this)
		return;

	eglMakeCurrent(this->wayland.egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
	    this->wayland.egl_context);

	cleanup_xr(this);
	cleanup_raylib_egl(this);
	cleanup_wayland(this);
	cleanup_lua_cfg(this);

	memset(this, 0, sizeof(*this));
}

void LunarWM_terminate(LunarWM *this)
{
	wlr_log(WLR_INFO, "Stopping compositor");
	this->running = false;
}

static void poll_events_xr(LunarWM *this)
{
	XrEventDataBuffer event_data = {
		.type = XR_TYPE_EVENT_DATA_BUFFER,
	};

	while (xrPollEvent(this->xr.instance, &event_data) == XR_SUCCESS) {
		switch (event_data.type) {
		case XR_TYPE_EVENT_DATA_EVENTS_LOST: {
			auto *el = (XrEventDataEventsLost *)&event_data;
			wlr_log(WLR_INFO, "OPENXR: Events Lost: %d", el->lostEventCount);
			break;
		}
		case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING: {
			auto *ilp = (XrEventDataInstanceLossPending *)&event_data;
			wlr_log(WLR_INFO, "OPENXR: Instance Loss Pending at: %ld",
			    ilp->lossTime);
			this->xr.session_running = false;
			this->running = false;
			break;
		}
		case XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED: {
			auto *ipc = (XrEventDataInteractionProfileChanged *)&event_data;
			wlr_log(WLR_INFO,
			    "OPENXR: Interaction Profile changed for Session: "
			    "%p",
			    ipc->session);
			if (ipc->session != this->xr.session) {
				wlr_log(WLR_ERROR,
				    "XrEventDataInteractionProfileChanged for "
				    "unknown Session");
				break;
			}
			break;
		}
		case XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING: {
			auto *scp = (XrEventDataReferenceSpaceChangePending *)&event_data;
			wlr_log(WLR_INFO,
			    "OPENXR: Reference Space Change pending for "
			    "Session: %p",
			    scp->session);
			if (scp->session != this->xr.session) {
				wlr_log(WLR_ERROR,
				    "XrEventDataReferenceSpaceChangePending for "
				    "unknown "
				    "Session");
				break;
			}
			break;
		}
		case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: {
			auto *sc = (XrEventDataSessionStateChanged *)&event_data;
			if (sc->session != this->xr.session) {
				wlr_log(WLR_ERROR,
				    "XrEventDataSessionStateChanged for unknown "
				    "Session");
				break;
			}

			if (sc->state == XR_SESSION_STATE_READY) {
				XrSessionBeginInfo bi = { .type = XR_TYPE_SESSION_BEGIN_INFO };
				bi.primaryViewConfigurationType
				    = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
				bi.primaryViewConfigurationType
				    = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
				if (xrBeginSession(this->xr.session, &bi) != XR_SUCCESS) {
					wlr_log(WLR_ERROR, "Failed to begin session");
				} else {
					this->xr.session_running = true;
				}
			}
			if (sc->state == XR_SESSION_STATE_STOPPING) {
				if (xrEndSession(this->xr.session) != XR_SUCCESS) {
					wlr_log(WLR_ERROR, "Failed to end session");
				}
				this->xr.session_running = false;
			}
			if (sc->state == XR_SESSION_STATE_EXITING) {
				this->xr.session_running = false;
				this->running = false;
			}
			if (sc->state == XR_SESSION_STATE_LOSS_PENDING) {
				this->xr.session_running = false;
				this->running = false;
			}
			this->xr.session_state = sc->state;
			break;
		}
		default: {
			break;
		}
		}
	}
}

bool acquire_wait(XrSwapchain sc, uint32_t *idx)
{
	XrSwapchainImageAcquireInfo ai
	    = { .type = XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO, .next = NULL };
	if (xrAcquireSwapchainImage(sc, &ai, idx) != XR_SUCCESS) {
		return false;
	}

	XrSwapchainImageWaitInfo wi = { .type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO,
		.next = NULL,
		.timeout = XR_INFINITE_DURATION };
	return xrWaitSwapchainImage(sc, &wi) == XR_SUCCESS;
}

static inline Matrix xr_matrix(XrPosef const pose)
{
	Matrix const translation
	    = MatrixTranslate(pose.position.x, pose.position.y, pose.position.z);
	Matrix const rotation = QuaternionToMatrix((Quaternion) {
	    pose.orientation.x,
	    pose.orientation.y,
	    pose.orientation.z,
	    pose.orientation.w,
	});
	return MatrixMultiply(rotation, translation);
}

static inline Matrix xr_projection_matrix(XrFovf const fov)
{
	static_assert(RL_CULL_DISTANCE_FAR > RL_CULL_DISTANCE_NEAR);

	Matrix matrix = {};

	auto const near = (float)RL_CULL_DISTANCE_NEAR;
	auto const far = (float)RL_CULL_DISTANCE_FAR;

	float const tan_angle_left = tanf(fov.angleLeft);
	float const tan_angle_right = tanf(fov.angleRight);

	float const tan_angle_down = tanf(fov.angleDown);
	float const tan_angle_up = tanf(fov.angleUp);

	float const tan_angle_width = tan_angle_right - tan_angle_left;
	float const tan_angle_height = tan_angle_up - tan_angle_down;

	matrix.m0 = 2 / tan_angle_width;
	matrix.m4 = 0;
	matrix.m8 = (tan_angle_right + tan_angle_left) / tan_angle_width;
	matrix.m12 = 0;

	matrix.m1 = 0;
	matrix.m5 = 2 / tan_angle_height;
	matrix.m9 = (tan_angle_up + tan_angle_down) / tan_angle_height;
	matrix.m13 = 0;

	matrix.m2 = 0;
	matrix.m6 = 0;
	matrix.m10 = -(far + near) / (far - near);
	matrix.m14 = -(far * (near + near)) / (far - near);

	matrix.m3 = 0;
	matrix.m7 = 0;
	matrix.m11 = -1;
	matrix.m15 = 0;

	return matrix;
}

static void DrawBillboardNoShear(
    Camera3D const cam, Texture2D tex, Vector3 pos, float scale, Color tint)
{
	Rectangle const src = { 0, 0, tex.width, tex.height };

	Vector2 const size = { scale * fabsf(src.width / src.height), -scale };
	Vector2 const origin = { size.x * 0.5f, size.y * 0.5f };

	DrawBillboardPro(cam, tex, src, pos, cam.up, size, origin, 0.0f, tint);
}

// pass yFlip from tl->attribs.invert_y
static void DrawTexture3D(
    Texture2D tex, Vector3 position, Vector3 target, float scale, bool y_flip)
{
	if (!tex.id)
		return;

	Vector3 fwd = Vector3Normalize(Vector3Subtract(target, position));
	Vector3 up_ref = (fabsf(fwd.y) > 0.99f) ? (Vector3) { 0, 0, 1 }
	                                        : (Vector3) { 0, 1, 0 };
	Vector3 right = Vector3Normalize(Vector3CrossProduct(fwd, up_ref));
	Vector3 up = Vector3CrossProduct(right, fwd);

	float half_w = 0.5f * (float)tex.width * scale;
	float half_h = 0.5f * (float)tex.height * scale;

	Vector3 tl
	    = Vector3Add(Vector3Subtract(position, Vector3Scale(right, half_w)),
	        Vector3Scale(up, half_h));
	Vector3 tr = Vector3Add(Vector3Add(position, Vector3Scale(right, half_w)),
	    Vector3Scale(up, half_h));
	Vector3 br
	    = Vector3Subtract(Vector3Add(position, Vector3Scale(right, half_w)),
	        Vector3Scale(up, half_h));
	Vector3 bl = Vector3Subtract(
	    Vector3Subtract(position, Vector3Scale(right, half_w)),
	    Vector3Scale(up, half_h));

	float vt = y_flip ? 1.0f : 0.0f;
	float vb = y_flip ? 0.0f : 1.0f;

	rlDisableBackfaceCulling(); // ensure visible regardless of winding
	rlSetTexture(tex.id);
	rlBegin(RL_QUADS);
	rlNormal3f(fwd.x, fwd.y, fwd.z);

	// TL, TR, BR, BL   with canonical UVs
	rlTexCoord2f(1.0f, vt);
	rlVertex3f(tl.x, tl.y, tl.z);
	rlTexCoord2f(0.0f, vt);
	rlVertex3f(tr.x, tr.y, tr.z);
	rlTexCoord2f(0.0f, vb);
	rlVertex3f(br.x, br.y, br.z);
	rlTexCoord2f(1.0f, vb);
	rlVertex3f(bl.x, bl.y, bl.z);
	rlEnd();
	rlSetTexture(0);
	rlEnableBackfaceCulling();
}

void render_hud(LunarWM *this, float /*dt*/, int hud_size)
{
	ClearBackground((Color) { 0, 0, 0, 0 });

	float const text_size = this->cman->cfg.displays.hud.font_size;

	char const *txt
	    = TextFormat("WAYLAND_DISPLAY=%s", getenv("WAYLAND_DISPLAY"));
	auto txt_w = MeasureText(txt, 24);
	DrawText(
	    txt, hud_size / 2 - txt_w / 2, hud_size - text_size, text_size, WHITE);

	txt = TextFormat("DISPLAY=%s", getenv("DISPLAY"));
	txt_w = MeasureText(txt, text_size);
	DrawText(txt, hud_size / 2 - txt_w / 2, hud_size - text_size * 2, text_size,
	    WHITE);

	{
		time_t t = time(NULL);
		struct tm *tm_info = localtime(&t);

		int hours = tm_info->tm_hour;
		int minutes = tm_info->tm_min;
		txt = TextFormat("%02d:%02d", hours, minutes);
		txt_w = MeasureText(txt, 32);
		DrawText(txt, hud_size / 2 - txt_w / 2, 0, text_size, WHITE);
	}
}

void render_3d(LunarWM *this, float /*dt*/)
{
	Skybox_draw(this->renderer.skybox, Vector3Zero());

	// rlDisableBackfaceCulling();
	for (size_t i = 0; i < vector_size(this->wayland.v_toplevels); i++) {
		auto *tl = this->wayland.v_toplevels[i];

		if (!tl || !tl->surface)
			continue;

		// if (!LunarWM_Toplevel_update(tl))
		//	continue;

		DrawTexture3D(tl->rl_texture,
		    (Vector3) { 0, 0, this->cman->cfg.space.radius - 0.01 * (float)i },
		    Vector3Zero(), this->cman->cfg.space.window_scale, false);
	}
	// rlEnableBackfaceCulling();

	for (int h = 0; h < 2; ++h) {
		auto *hand_info = &this->xr.hands[h];
		for (size_t k = 0; k < XR_HAND_JOINT_COUNT_EXT; ++k) {
			auto const *jl = &hand_info->joint_locations[k];
			Vector3 const pos = {
				jl->pose.position.x,
				jl->pose.position.y,
				jl->pose.position.z,
			};
			DrawSphere(pos, jl->radius, (Color) { 255, 0, 0, 255 });
		}
	}

	if (IsTextureValid(this->renderer.hud_rt.texture)) {
		rlDrawRenderBatchActive();
		rlDisableDepthTest();

		Vector3 camPos = this->renderer.camera.position;
		Vector3 camDir = Vector3Normalize(
		    Vector3Subtract(this->renderer.camera.target, camPos));
		Vector3 up = this->renderer.camera.up;
		Vector3 right = Vector3Normalize(Vector3CrossProduct(camDir, up));
		up = Vector3CrossProduct(right, camDir);

		Vector3 center = Vector3Add(camPos, Vector3Scale(camDir, 0.6f));

		float heightMeters = 0.10f;

		DrawBillboardNoShear(this->renderer.camera,
		    this->renderer.hud_rt.texture, center, heightMeters * 5, WHITE);
		rlDrawRenderBatchActive();

		rlEnableDepthTest();
	}
}

static inline Vector3 RecenterPoint(LunarWM *wm, Vector3 p)
{
	if (!wm->xr.recenter_active)
		return p;
	return Vector3Add(Vector3RotateByQuaternion(p, wm->xr.recenter_rot),
	    wm->xr.recenter_trans);
}

static inline Quaternion RecenterOrient(LunarWM *wm, Quaternion q)
{
	if (!wm->xr.recenter_active)
		return q;
	return QuaternionMultiply(wm->xr.recenter_rot, q);
}

static bool render_layer(LunarWM *this, LunarWM_RenderLayerInfo *info, float dt)
{
	auto const view_count = (uint32_t)this->xr.view_configuration_views_count;
	assert(view_count == 2);

	XrView views[2] = {};
	for (int i = 0; i < ARRAY_SZ(views); i++) {
		views[i] = (XrView) {
			.type = XR_TYPE_VIEW,
		};
	}

	XrViewLocateInfo locInfo = {
		.type = XR_TYPE_VIEW_LOCATE_INFO,
		.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
		.displayTime = info->predicted_display_time,
		.space = this->xr.local_space,
	};

	XrViewState viewState = { .type = XR_TYPE_VIEW_STATE };
	uint32_t located = 0;
	if (xrLocateViews(
	        this->xr.session, &locInfo, &viewState, view_count, &located, views)
	        != XR_SUCCESS
	    || located != view_count) {
		wlr_log(WLR_ERROR, "Failed to locate views");
		return false;
	}

	// acquire swapchain images
	auto *color_sc = &this->xr.swapchains.v_color[0];
	auto *depth_sc = &this->xr.swapchains.v_depth[0];

	uint32_t col_idx = 0;
	uint32_t dep_idx = 0;
	if (!acquire_wait(color_sc->swapchain, &col_idx)
	    || !acquire_wait(depth_sc->swapchain, &dep_idx)) {
		wlr_log(WLR_ERROR, "Swap-chain acquire failed");
		return false;
	}

	GLuint color_tex = get_swapchain_image(this, 0, col_idx);
	GLuint depth_tex = get_swapchain_image(this, 1, dep_idx);

	// build FBO
	if (this->renderer.fbo == 0u) {
		glGenFramebuffers(1, &this->renderer.fbo);
	}
	glBindFramebuffer(GL_FRAMEBUFFER, this->renderer.fbo);
	rlFramebufferAttach(this->renderer.fbo, color_tex,
	    RL_ATTACHMENT_COLOR_CHANNEL0, RL_ATTACHMENT_TEXTURE2D, 0);
	rlFramebufferAttach(this->renderer.fbo, depth_tex, RL_ATTACHMENT_DEPTH,
	    RL_ATTACHMENT_TEXTURE2D, 0);
	assert(rlFramebufferComplete(this->renderer.fbo));

	uint32_t const eye_w
	    = this->xr.a_view_configuration_views[0].recommendedImageRectWidth;
	uint32_t const eye_h
	    = this->xr.a_view_configuration_views[0].recommendedImageRectHeight;

	this->renderer.tmp_rt = (RenderTexture2D) {
		.id = this->renderer.fbo,
		.texture = { color_tex, (int)eye_w * view_count, (int)eye_h, 1, -1 },
		.depth = { depth_tex, (int)eye_w * view_count, (int)eye_h, 1, -1 },
	};

	// head-space view matrix (matches rlOpenXR)
	XrSpaceLocation headLoc = { .type = XR_TYPE_SPACE_LOCATION };
	xrLocateSpace(this->xr.view_space, this->xr.local_space,
	    info->predicted_display_time, &headLoc);
	auto const head_view = MatrixInvert(xr_matrix(headLoc.pose));

	// per-eye projection + view-offset
	Matrix const view_off_l
	    = MatrixMultiply(xr_matrix(views[0].pose), head_view);
	Matrix const view_off_r
	    = MatrixMultiply(xr_matrix(views[1].pose), head_view);

	Matrix const proj_r = xr_projection_matrix(views[0].fov);
	Matrix const proj_l = xr_projection_matrix(views[1].fov);

	int const hud_size = this->cman->cfg.displays.hud.size;
	if (!IsTextureValid(this->renderer.hud_rt.texture)) {
		this->renderer.hud_rt = LoadRenderTexture(hud_size, hud_size);
	}

	if (IsTextureValid(this->renderer.hud_rt.texture)) {
		BeginTextureMode(this->renderer.hud_rt);
		{
			render_hud(this, dt, hud_size);
		}
		EndTextureMode();
	}

	// draw

	if (!IsTextureValid(this->renderer.main_rt.texture)) {
		this->renderer.main_rt = LoadRenderTexture(eye_w * view_count, eye_h);
	}

	BeginTextureMode(this->renderer.main_rt);

	rlEnableStereoRender();
	rlSetMatrixProjectionStereo(proj_r, proj_l);
	rlSetMatrixViewOffsetStereo(view_off_r, view_off_l);

	glViewport(0, 0, (GLsizei)eye_w * view_count, (GLsizei)eye_h);
	rlClearColor(0, 0, 10, 255);
	rlClearScreenBuffers();

	for (int i = 0; i < 1; i++) {
		XrTime const time = info->predicted_display_time;

		XrSpaceLocation view_location = { .type = XR_TYPE_SPACE_LOCATION };
		XrResult const result = xrLocateSpace(
		    this->xr.view_space, this->xr.local_space, time, &view_location);
		if (result != XR_SUCCESS) {
			break;
		}

		if ((view_location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT)
		    != 0u) {
			auto const pos = view_location.pose.position;
			this->renderer.camera.position = (Vector3) { pos.x, pos.y, pos.z };
		}
		if ((view_location.locationFlags
		        & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)
		    != 0u) {
			auto const rot = view_location.pose.orientation;
			auto const forward
			    = Vector3RotateByQuaternion((Vector3) { 0, 0, -1 },
			        (Quaternion) { rot.x, rot.y, rot.z, rot.w });
			auto const up = Vector3RotateByQuaternion((Vector3) { 0, 1, 0 },
			    (Quaternion) { rot.x, rot.y, rot.z, rot.w });
			this->renderer.camera.target
			    = Vector3Add(this->renderer.camera.position, forward);
			this->renderer.camera.up = up;
		}

		if (this->xr.recenter_active) {
			Vector3 pos = this->renderer.camera.position;
			Vector3 fwd = Vector3Normalize(
			    Vector3Subtract(this->renderer.camera.target, pos));
			Vector3 up = this->renderer.camera.up;

			pos = Vector3Add(
			    Vector3RotateByQuaternion(pos, this->xr.recenter_rot),
			    this->xr.recenter_trans);
			fwd = Vector3RotateByQuaternion(fwd, this->xr.recenter_rot);
			up = Vector3RotateByQuaternion(up, this->xr.recenter_rot);

			this->renderer.camera.position = pos;
			this->renderer.camera.target = Vector3Add(pos, fwd);
			this->renderer.camera.up = up;
		}
	}

	BeginMode3D(this->renderer.camera);
	{
		render_3d(this, dt);
	}
	EndMode3D();

	rlDisableStereoRender();
	EndTextureMode();

	if (!IsShaderValid(this->renderer.linear_srgb)) {
		static char const linear_srgb[] = {
#embed "../assets/linear_srgb.fs"
			, 0
		};
		this->renderer.linear_srgb = LoadShaderFromMemory(NULL, linear_srgb);
	}

	BeginTextureMode(this->renderer.tmp_rt);
	rlDisableColorBlend();
	ClearBackground(BLACK);
	BeginShaderMode(this->renderer.linear_srgb);
	DrawTexturePro(this->renderer.main_rt.texture,
	    (Rectangle) {
	        0,
	        0,
	        this->renderer.main_rt.texture.width,
	        -this->renderer.main_rt.texture.height,
	    },
	    (Rectangle) {
	        0,
	        0,
	        this->renderer.tmp_rt.texture.width,
	        this->renderer.tmp_rt.texture.height,
	    },
	    (Vector2) { 0, 0 }, 0, WHITE);
	EndShaderMode();
	EndTextureMode();
	rlEnableColorBlend();

	// release swapchain images
	XrSwapchainImageReleaseInfo const ri
	    = { .type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
	xrReleaseSwapchainImage(color_sc->swapchain, &ri);
	xrReleaseSwapchainImage(depth_sc->swapchain, &ri);

	// fill projection layer
	info->layer_projection_views_count = view_count;

	for (uint32_t i = 0; i < view_count; ++i) {
		int32_t const xOff = i * eye_w;
		auto *pv = &info->layer_projection_views[i];
		pv->pose = views[i].pose;
		pv->fov = views[i].fov;
		pv->subImage.swapchain = color_sc->swapchain;
		pv->subImage.imageRect.offset = (XrOffset2Di) { .x = xOff, .y = 0 };
		pv->subImage.imageRect.extent
		    = (XrExtent2Di) { .width = eye_w, .height = eye_h };
		pv->subImage.imageArrayIndex = 0;
	}

	info->layer_projection = (XrCompositionLayerProjection) {
		.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION,
		.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT
		    | XR_COMPOSITION_LAYER_CORRECT_CHROMATIC_ABERRATION_BIT,
		.space = this->xr.local_space,
		.viewCount = view_count,
		.views = info->layer_projection_views,
	};

	info->layers_count = 0;
	info->layers[info->layers_count++]
	    = (XrCompositionLayerBaseHeader *)&info->layer_projection;

	return true;
}

void LunarWM_run(LunarWM *this)
{
	assert(this);
	assert(this->initialized);

	if (!wlr_backend_start(this->wayland.backend)) {
		wlr_log(WLR_ERROR, "Failed to start backend");
		return;
	}

	auto const *socket = wl_display_add_socket_auto(this->wayland.display);
	if (socket == nullptr) {
		wlr_log(WLR_ERROR, "Failed to add wayland socket to display");
		return;
	}

	setenv("WAYLAND_DISPLAY", socket, 1);
	wlr_log(LOG_INFO, "Running on WAYLAND_DISPLAY=%s", socket);

	this->running = true;

	struct timespec last, now;
	clock_gettime(CLOCK_MONOTONIC, &last);
	while (this->running) {
		clock_gettime(CLOCK_MONOTONIC, &now);
		float dt = (now.tv_sec - last.tv_sec)
		    + (now.tv_nsec - last.tv_nsec) / 1000000000.0f;
		last = now;

		for (size_t i = 0; i < vector_size(this->wayland.v_toplevels); i++) {
			LunarWM_Toplevel *tl = this->wayland.v_toplevels[i];
			if (tl->surface) {
				wlr_surface_send_frame_done(tl->surface, &now);
			}
		}

		wl_display_flush_clients(this->wayland.display);
		wl_event_loop_dispatch(this->wayland.event_loop, 0);

		auto *draw = eglGetCurrentSurface(EGL_DRAW);
		auto *read = eglGetCurrentSurface(EGL_READ);
		if (eglMakeCurrent(this->wayland.egl_display, EGL_NO_SURFACE,
		        EGL_NO_SURFACE, this->wayland.egl_context)
		    == EGL_FALSE) {
			wlr_log(WLR_ERROR, "Failed to eglMakeCurrent");
			return;
		}

		WindowShouldClose();
		BeginDrawing();

		poll_events_xr(this);

		if (!this->xr.session_running) {
			EndDrawing();
			continue;
		}

		XrFrameState frame_state = {
			.type = XR_TYPE_FRAME_STATE,
		};
		XrFrameWaitInfo const frame_wait_info = {
			.type = XR_TYPE_FRAME_WAIT_INFO,
			.next = nullptr,
		};
		if (xrWaitFrame(this->xr.session, &frame_wait_info, &frame_state)
		    != XR_SUCCESS) {
			wlr_log(WLR_ERROR, "Failed to wait for OpenXR frame");
			return;
		}

		XrFrameBeginInfo const frame_begin_info = {
			.type = XR_TYPE_FRAME_BEGIN_INFO,
			.next = nullptr,
		};
		XrResult res = xrBeginFrame(this->xr.session, &frame_begin_info);
		if (res != XR_FRAME_DISCARDED && res != XR_SUCCESS) {
			wlr_log(WLR_ERROR, "Failed to begin the OpenXR Frame: %d", res);
			return;
		}

		if (this->xr.hand_tracking_system_properties.supportsHandTracking) {
			XrActionStateGetInfo const si = {
				.type = XR_TYPE_ACTION_STATE_GET_INFO,
			};
			for (size_t i = 0; i < 2; i++) {
				LunarWM_Hand *hand = &this->xr.hands[i];
				bool const unobstructed = true;

				XrHandJointsMotionRangeInfoEXT mri = {
					.type = XR_TYPE_HAND_JOINTS_MOTION_RANGE_INFO_EXT,
				};
				if (unobstructed) {
					mri.handJointsMotionRange
					    = XR_HAND_JOINTS_MOTION_RANGE_UNOBSTRUCTED_EXT;
				} else {
					mri.handJointsMotionRange
					    = XR_HAND_JOINTS_MOTION_RANGE_CONFORMING_TO_CONTROLLER_EXT;
				}

				XrHandJointsLocateInfoEXT const li = {
					.type = XR_TYPE_HAND_JOINTS_LOCATE_INFO_EXT,
					.next = &mri,
					.baseSpace = this->xr.local_space,
					.time = frame_state.predictedDisplayTime,
				};

				XrHandJointLocationsEXT hji = {
					.type = XR_TYPE_HAND_JOINT_LOCATIONS_EXT,
					.jointCount = XR_HAND_JOINT_COUNT_EXT,
					.jointLocations = hand->joint_locations,
				};

				if (this->xr.LocateHandJointsEXT(hand->hand_tracker, &li, &hji)
				    != XR_SUCCESS) {
					wlr_log(WLR_ERROR, "Failed to locate hand joints");
					return;
				}
			}

			for (int h = 0; h < 2; h++) {
				auto *hand_info = &this->xr.hands[h];
				for (size_t k = 0; k < XR_HAND_JOINT_COUNT_EXT; ++k) {
					auto *jl = &hand_info->joint_locations[k];

					Vector3 pos = { jl->pose.position.x, jl->pose.position.y,
						jl->pose.position.z };
					pos = RecenterPoint(this, pos);

					Quaternion q
					    = { jl->pose.orientation.x, jl->pose.orientation.y,
						      jl->pose.orientation.z, jl->pose.orientation.w };
					q = RecenterOrient(this, q);

					jl->pose.position.x = pos.x;
					jl->pose.position.y = pos.y;
					jl->pose.position.z = pos.z;

					jl->pose.orientation.x = q.x;
					jl->pose.orientation.y = q.y;
					jl->pose.orientation.z = q.z;
					jl->pose.orientation.w = q.w;

					DrawSphere(pos, jl->radius, (Color) { 255, 0, 0, 255 });
				}
			}
		}

		LunarWM_RenderLayerInfo render_layer_info = {
			.predicted_display_time = frame_state.predictedDisplayTime,
			.layer_projection.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION,
			.layer_projection.next = nullptr,
		};
		bool const session_active
		    = (this->xr.session_state == XR_SESSION_STATE_SYNCHRONIZED
		        || this->xr.session_state == XR_SESSION_STATE_VISIBLE
		        || this->xr.session_state == XR_SESSION_STATE_FOCUSED);
		if (session_active && (frame_state.shouldRender != 0u)) {
			auto rendered = render_layer(this, &render_layer_info, dt);
			if (rendered) {
				render_layer_info.layers[render_layer_info.layers_count]
				    = (XrCompositionLayerBaseHeader *)&render_layer_info
				          .layer_projection;
			}
		}

		XrFrameEndInfo const frame_end_info = {
			.type = XR_TYPE_FRAME_END_INFO,
			.displayTime = frame_state.predictedDisplayTime,
			.environmentBlendMode = this->xr.environment_blend_mode,
			.layerCount = render_layer_info.layers_count,
			.layers
			= (XrCompositionLayerBaseHeader const **)render_layer_info.layers,
		};
		if (xrEndFrame(this->xr.session, &frame_end_info) != XR_SUCCESS) {
			wlr_log(WLR_ERROR, "Failed to end OpenXR frame");
			return;
		}

		EndDrawing();
	}
}
