vtgl

terminal emulator implemented in OpenGL
git clone anongit@rnpnr.xyz:vtgl.git
Log | Files | Refs | Feed | LICENSE

platform_linux_x11.c (17122B)


      1 /* See LICENSE for copyright details */
      2 #define GL_GLEXT_PROTOTYPES
      3 #include <GLFW/glfw3.h>
      4 
      5 /* TODO: fix glfw */
      6 typedef void *RROutput;
      7 typedef void *RRCrtc;
      8 typedef void *Display;
      9 typedef void *Window;
     10 
     11 #define GLFW_EXPOSE_NATIVE_X11
     12 #define GLFW_NATIVE_INCLUDE_NONE
     13 #include <GLFW/glfw3native.h>
     14 
     15 #include "vtgl.h"
     16 
     17 i32 XConnectionNumber(void *display);
     18 i32 XPending(void *display);
     19 
     20 #ifndef _DEBUG
     21 #define do_debug(...)
     22 #include "vtgl.c"
     23 #else
     24 #include <dlfcn.h>
     25 
     26 #define DEBUG_LIB_NAME "./vtgl.so"
     27 
     28 #define LIB_FNS                  \
     29 	X(vtgl_active_selection) \
     30 	X(vtgl_initialize)       \
     31 	X(vtgl_render_frame)     \
     32 	X(vtgl_handle_keys)      \
     33 	X(vtgl_frame_step)
     34 
     35 #define X(name) static name ## _fn *name;
     36 LIB_FNS
     37 #undef X
     38 
     39 static PLATFORM_FILE_WATCH_CALLBACK_FN(debug_reload_library)
     40 {
     41 	PlatformCtx *ctx = user_ctx;
     42 
     43 	if (ctx->input.executable_reloaded)
     44 		return;
     45 
     46 	/* NOTE(rnp): spin until render thread finishes its work */
     47 	while (!ctx->render_stack->thread_asleep);
     48 
     49 	ctx->input.executable_reloaded = 1;
     50 	s8 nl = s8("\n");
     51 	/* NOTE: glibc sucks and will crash if this is NULL */
     52 	if (ctx->library_handle)
     53 		dlclose(ctx->library_handle);
     54 	ctx->library_handle = dlopen((c8 *)path, RTLD_NOW|RTLD_LOCAL);
     55 	if (!ctx->library_handle)
     56 		stream_push_s8s(&ctx->error_stream, 3,
     57 		                (s8 []){s8("dlopen: "), c_str_to_s8(dlerror()), nl});
     58 
     59 	#define X(name) \
     60 		name = dlsym(ctx->library_handle, #name); \
     61 		if (!name) stream_push_s8s(&ctx->error_stream, 3, (s8 []){s8("dlsym: "), \
     62 			                                                  c_str_to_s8(dlerror()), nl});
     63 	LIB_FNS
     64 	#undef X
     65 
     66 	stream_push_s8(&ctx->error_stream, s8("Reloaded Main Program\n"));
     67 
     68 	os_write_err_msg(stream_to_s8(&ctx->error_stream));
     69 	ctx->error_stream.widx = 0;
     70 }
     71 #endif /* _DEBUG */
     72 
     73 static void
     74 glfw_error_callback(int code, const char *desc)
     75 {
     76 	u8 buf[256];
     77 	Stream err = {.cap = sizeof(buf), .buf = buf};
     78 	stream_push_s8(&err, s8("GLFW Error (0x"));
     79 	stream_push_hex_u64(&err, code);
     80 	stream_push_s8(&err, s8("): "));
     81 	os_write_err_msg(stream_to_s8(&err));
     82 	os_write_err_msg(c_str_to_s8((char *)desc));
     83 	os_write_err_msg(s8("\n"));
     84 }
     85 
     86 static void
     87 char_callback(GLFWwindow *win, u32 codepoint)
     88 {
     89 	PlatformCtx *ctx = glfwGetWindowUserPointer(win);
     90 	stream_push_s8(&ctx->char_stream, utf8_encode(codepoint));
     91 }
     92 
     93 /* NOTE: called when the window was resized */
     94 static void
     95 fb_callback(GLFWwindow *win, i32 w, i32 h)
     96 {
     97 	PlatformCtx *ctx       = glfwGetWindowUserPointer(win);
     98 	ctx->input.window_size = (iv2){.w = w, .h = h};
     99 }
    100 
    101 static void
    102 scroll_callback(GLFWwindow *win, f64 xoff, f64 yoff)
    103 {
    104 	PlatformCtx *ctx          = glfwGetWindowUserPointer(win);
    105 	ctx->input.mouse_scroll.x = xoff;
    106 	ctx->input.mouse_scroll.y = yoff;
    107 }
    108 
    109 static void
    110 key_callback(GLFWwindow *win, i32 key, i32 scancode, i32 action, i32 modifiers)
    111 {
    112 	PlatformCtx *ctx     = glfwGetWindowUserPointer(win);
    113 	TerminalInput *input = &ctx->input;
    114 
    115 	/* TODO: base this on X11 keys directly */
    116 	switch (key) {
    117 	case GLFW_KEY_LEFT_SHIFT:
    118 		button_action(input->keys + KEY_LEFT_SHIFT, action == GLFW_PRESS);
    119 		if (action == GLFW_RELEASE) input->modifiers &= ~MOD_SHIFT;
    120 		else                        input->modifiers |=  MOD_SHIFT;
    121 		break;
    122 	case GLFW_KEY_LEFT_CONTROL:
    123 		button_action(input->keys + KEY_LEFT_CONTROL, action == GLFW_PRESS);
    124 		if (action == GLFW_RELEASE) input->modifiers &= ~MOD_CONTROL;
    125 		else                        input->modifiers |=  MOD_CONTROL;
    126 		break;
    127 	case GLFW_KEY_LEFT_ALT:
    128 		button_action(input->keys + KEY_LEFT_ALT, action == GLFW_PRESS);
    129 		if (action == GLFW_RELEASE) input->modifiers &= ~MOD_ALT;
    130 		else                        input->modifiers |=  MOD_ALT;
    131 		break;
    132 	case GLFW_KEY_LEFT_SUPER:
    133 		button_action(input->keys + KEY_LEFT_SUPER, action == GLFW_PRESS);
    134 		if (action == GLFW_RELEASE) input->modifiers &= ~MOD_SUPER;
    135 		else                        input->modifiers |=  MOD_SUPER;
    136 		break;
    137 	case GLFW_KEY_RIGHT_SHIFT:
    138 		button_action(input->keys + KEY_RIGHT_SHIFT, action == GLFW_PRESS);
    139 		if (action == GLFW_RELEASE) input->modifiers &= ~MOD_SHIFT;
    140 		else                        input->modifiers |=  MOD_SHIFT;
    141 		break;
    142 	case GLFW_KEY_RIGHT_CONTROL:
    143 		button_action(input->keys + KEY_RIGHT_CONTROL, action == GLFW_PRESS);
    144 		if (action == GLFW_RELEASE) input->modifiers &= ~MOD_CONTROL;
    145 		else                        input->modifiers |=  MOD_CONTROL;
    146 		break;
    147 	case GLFW_KEY_RIGHT_ALT:
    148 		button_action(input->keys + KEY_RIGHT_ALT, action == GLFW_PRESS);
    149 		if (action == GLFW_RELEASE) input->modifiers &= ~MOD_ALT;
    150 		else                        input->modifiers |=  MOD_ALT;
    151 		break;
    152 	case GLFW_KEY_RIGHT_SUPER:
    153 		button_action(input->keys + KEY_RIGHT_SUPER, action == GLFW_PRESS);
    154 		if (action == GLFW_RELEASE) input->modifiers &= ~MOD_SUPER;
    155 		else                        input->modifiers |=  MOD_SUPER;
    156 		break;
    157 	case GLFW_KEY_MENU:
    158 		button_action(input->keys + KEY_MENU, action == GLFW_PRESS);
    159 		break;
    160 	}
    161 	vtgl_handle_keys(&ctx->memory, &ctx->input, key, action, modifiers);
    162 }
    163 
    164 static void
    165 mouse_button_callback(GLFWwindow *win, i32 button, i32 action, i32 modifiers)
    166 {
    167 	PlatformCtx *ctx     = glfwGetWindowUserPointer(win);
    168 	TerminalInput *input = &ctx->input;
    169 
    170 	switch (button) {
    171 	case GLFW_MOUSE_BUTTON_LEFT:
    172 		button_action(input->keys + MOUSE_LEFT, action == GLFW_PRESS);
    173 		break;
    174 	case GLFW_MOUSE_BUTTON_RIGHT:
    175 		button_action(input->keys + MOUSE_RIGHT, action == GLFW_PRESS);
    176 		break;
    177 	case GLFW_MOUSE_BUTTON_MIDDLE:
    178 		button_action(input->keys + MOUSE_MIDDLE, action == GLFW_PRESS);
    179 		break;
    180 	case GLFW_MOUSE_BUTTON_4:
    181 		button_action(input->keys + MOUSE_EXTENDED_0, action == GLFW_PRESS);
    182 		break;
    183 	case GLFW_MOUSE_BUTTON_5:
    184 		button_action(input->keys + MOUSE_EXTENDED_1, action == GLFW_PRESS);
    185 		break;
    186 	}
    187 }
    188 
    189 static void
    190 focus_callback(GLFWwindow *win, i32 focused)
    191 {
    192 	PlatformCtx *ctx            = glfwGetWindowUserPointer(win);
    193 	ctx->input.window_focused   = focused;
    194 	/* NOTE: force a refresh as well when the focus changes */
    195 	ctx->input.window_refreshed = 1;
    196 }
    197 
    198 static void
    199 refresh_callback(GLFWwindow *win)
    200 {
    201 	PlatformCtx *ctx            = glfwGetWindowUserPointer(win);
    202 	ctx->input.window_refreshed = 1;
    203 }
    204 
    205 static GLFWwindow *
    206 init_window(PlatformCtx *ctx, iv2 window_size)
    207 {
    208 	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    209 	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    210 	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
    211 	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    212 
    213 	#ifdef _DEBUG
    214 	glfwWindowHint(GLFW_CONTEXT_DEBUG, GLFW_TRUE);
    215 	#endif
    216 
    217 	/* NOTE: we initially hide the window so that it can be freely resized behind the
    218 	 * back of the window manager prior to showing */
    219 	glfwWindowHint(GLFW_VISIBLE,   GLFW_FALSE);
    220 	glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
    221 
    222 	GLFWwindow *window = glfwCreateWindow(window_size.w, window_size.h, "vtgl", NULL, NULL);
    223 	if (!window) {
    224 		glfwTerminate();
    225 		os_fatal(s8("Failed to spawn GLFW window\n"));
    226 	}
    227 	glfwMakeContextCurrent(window);
    228 	glfwSetWindowUserPointer(window, ctx);
    229 
    230 	/* TODO: swap interval is not needed because we will sleep on waiting for terminal input */
    231 	glfwSwapInterval(1);
    232 
    233 	glfwSetCharCallback(window,            char_callback);
    234 	glfwSetFramebufferSizeCallback(window, fb_callback);
    235 	glfwSetKeyCallback(window,             key_callback);
    236 	glfwSetMouseButtonCallback(window,     mouse_button_callback);
    237 	glfwSetScrollCallback(window,          scroll_callback);
    238 	glfwSetWindowFocusCallback(window,     focus_callback);
    239 	glfwSetWindowRefreshCallback(window,   refresh_callback);
    240 
    241 	ctx->win_fd = XConnectionNumber(glfwGetX11Display());
    242 
    243 	return window;
    244 }
    245 
    246 static void
    247 update_input(PlatformCtx *ctx)
    248 {
    249 	TerminalInput *input = &ctx->input;
    250 	ctx->input.executable_reloaded = 0;
    251 	ctx->input.window_refreshed    = 0;
    252 
    253 	/* NOTE: mouse */
    254 	input->last_mouse   = input->mouse;
    255 	input->mouse_scroll = (v2){0};
    256 
    257 	f64 mouse_x, mouse_y;
    258 	glfwGetCursorPos(ctx->window, &mouse_x, &mouse_y);
    259 	input->mouse.x = mouse_x;
    260 	input->mouse.y = input->window_size.h - mouse_y;
    261 
    262 	for (u32 i = 0; i < ARRAY_COUNT(input->keys); i++)
    263 		input->keys[i].transitions = 0;
    264 
    265 	ctx->char_stream.widx = 0;
    266 
    267 	i64 timeout[2] = {0, 25e6};
    268 	if (input->pending_updates) {
    269 		timeout[1]             = 0;
    270 		input->pending_updates = 0;
    271 	}
    272 
    273 	sys_fd_set rfd = {0};
    274 	FD_SET(ctx->child.handle, rfd);
    275 	FD_SET(ctx->inotify_fd,   rfd);
    276 	FD_SET(ctx->win_fd,       rfd);
    277 
    278 	i32 max_fd = MAX(ctx->inotify_fd, ctx->child.handle);
    279 	max_fd     = MAX(max_fd, ctx->win_fd);
    280 	syscall6(SYS_pselect, max_fd + 1, (iptr)rfd, 0, 0, (iptr)timeout, 0);
    281 
    282 	input->data_available = FD_ISSET(ctx->child.handle, rfd) != 0;
    283 
    284 	try_deferred_file_loads(ctx);
    285 	if (FD_ISSET(ctx->inotify_fd, rfd))
    286 		dispatch_file_watch_events(ctx);
    287 
    288 	glfwPollEvents();
    289 
    290 	/* NOTE: char_stream was filled in char callback */
    291 	input->character_input = stream_to_s8(&ctx->char_stream);
    292 }
    293 
    294 static PLATFORM_CLIPBOARD_FN(x11_get_clipboard)
    295 {
    296 	/* NOTE: this does a bunch of extra copying and other garbage. both GLFW and X11 are
    297 	 * at fault. The API is designed to do what the terminal wants and  not be constrained
    298 	 * by GLFW and X11 garbage */
    299 	ASSERT(buffer);
    300 	char *text = 0;
    301 	switch (clipboard) {
    302 	case CLIPBOARD_0: text = (c8 *)glfwGetClipboardString(0);   break;
    303 	case CLIPBOARD_1: text = (c8 *)glfwGetX11SelectionString(); break;
    304 	}
    305 	if (text) {
    306 		/* TODO: we may need to replace '\n' with '\r\n' */
    307 		stream_push_s8(buffer, c_str_to_s8(text));
    308 	}
    309 
    310 	return !buffer->errors;
    311 }
    312 
    313 static PLATFORM_CLIPBOARD_FN(x11_set_clipboard)
    314 {
    315 	ASSERT(buffer);
    316 	stream_push_byte(buffer, 0);
    317 
    318 	if (!buffer->errors) {
    319 		switch (clipboard) {
    320 		case CLIPBOARD_0: glfwSetClipboardString(0, (c8 *)buffer->buf); break;
    321 		case CLIPBOARD_1: glfwSetX11SelectionString((c8 *)buffer->buf); break;
    322 		}
    323 	}
    324 	return !buffer->errors;
    325 }
    326 
    327 static PLATFORM_WINDOW_TITLE_FN(x11_get_window_title)
    328 {
    329 	ASSERT(buffer);
    330 	char *title = (c8 *)glfwGetWindowTitle(linux_ctx.window);
    331 	if (title) stream_push_s8(buffer, c_str_to_s8(title));
    332 }
    333 
    334 static PLATFORM_WINDOW_TITLE_FN(x11_set_window_title)
    335 {
    336 	ASSERT(buffer);
    337 	stream_push_byte(buffer, 0);
    338 	if (!buffer->errors)
    339 		glfwSetWindowTitle(linux_ctx.window, (c8 *)buffer->buf);
    340 }
    341 
    342 static void
    343 linux_render_thread_entry(struct stack_base *stack)
    344 {
    345 	{
    346 		/* NOTE: set thread name */
    347 		char name[16] = "[render]";
    348 		syscall2(SYS_prctl, PR_SET_NAME, (iptr)name);
    349 		/* NOTE: halt until main thread is ready to hand off gl context */
    350 		syscall4(SYS_futex, (iptr)&stack->work_futex, FUTEX_WAIT, 0, 0);
    351 
    352 		glfwMakeContextCurrent(stack->window);
    353 	}
    354 
    355 	for (;;) {
    356 		stack->thread_asleep = 1;
    357 		syscall4(SYS_futex, (iptr)&stack->work_futex, FUTEX_WAIT, 0, 0);
    358 		stack->thread_asleep = 0;
    359 		vtgl_render_frame(stack->terminal_memory, stack->input, stack->thread_arena);
    360 		glfwSwapBuffers(stack->window);
    361 	}
    362 
    363 	__builtin_unreachable();
    364 }
    365 
    366 i32
    367 main(i32 argc, char *argv[], char *envp[])
    368 {
    369 	linux_ctx.platform_memory = arena_from_memory_block(linux_block_alloc(MB(2)));
    370 	linux_ctx.error_stream    = stream_alloc(&linux_ctx.platform_memory, KB(256));
    371 
    372 	iv2 cells = {.x = -1, .y = -1};
    373 
    374 	char *argv0 = *argv++;
    375 	argc--;
    376 	struct conversion_result cres;
    377 	for (i32 i = 0; i < argc; i++) {
    378 		char *arg = argv[i];
    379 		if (!arg || !arg[0])
    380 			usage(argv0, &linux_ctx.error_stream);
    381 		if (arg[0] != '-')
    382 			break;
    383 		arg++;
    384 		switch (arg[0]) {
    385 		case 'g':
    386 			if (!argv[i + 1])
    387 				usage(argv0, &linux_ctx.error_stream);
    388 			cres = i32_from_cstr(argv[i + 1], 'x');
    389 			if (cres.status == CR_SUCCESS)
    390 				cells.w = cres.i;
    391 			cres = i32_from_cstr(cres.unparsed, 0);
    392 			if (cres.status == CR_SUCCESS)
    393 				cells.h = cres.i;
    394 			if (cells.w <= 0 || cells.h <= 0) {
    395 				stream_push_s8(&linux_ctx.error_stream, s8("ignoring malformed geometry: "));
    396 				stream_push_s8(&linux_ctx.error_stream, c_str_to_s8(argv[i + 1]));
    397 				stream_push_byte(&linux_ctx.error_stream, '\n');
    398 			}
    399 			argv++;
    400 			argc--;
    401 			break;
    402 		case 'v':
    403 			stream_push_s8s(&linux_ctx.error_stream, 2,
    404 			                (s8 []){c_str_to_s8(argv0), s8(" " VERSION "\n")});
    405 			os_fatal(stream_to_s8(&linux_ctx.error_stream));
    406 		default:
    407 			usage(argv0, &linux_ctx.error_stream);
    408 		}
    409 	}
    410 	if (linux_ctx.error_stream.widx) {
    411 		os_write_err_msg(stream_to_s8(&linux_ctx.error_stream));
    412 		linux_ctx.error_stream.widx = 0;
    413 	}
    414 
    415 	linux_ctx.render_stack                = new_stack(KB(256));
    416 	linux_ctx.render_stack->entry         = linux_render_thread_entry;
    417 	linux_ctx.render_stack->thread_asleep = 1;
    418 	new_thread(linux_ctx.render_stack);
    419 
    420 	{
    421 		Arena tmp = linux_ctx.platform_memory;
    422 		SLLVariableVector environment_block = parse_environment(&tmp, envp);
    423 
    424 		/* TODO: build up argv for the child as well */
    425 		c8 **child_envp = construct_c_str_array(&tmp, environment_block);
    426 		linux_ctx.child = os_fork_child(get_default_cmd(envp), child_envp);
    427 	}
    428 
    429 	{
    430 		MemoryBlock terminal_memory  = linux_block_alloc(MB(32));
    431 		linux_ctx.memory.memory      = terminal_memory.memory;
    432 		linux_ctx.memory.memory_size = terminal_memory.size;
    433 #ifdef _DEBUG
    434 		MemoryBlock debug_memory           = linux_block_alloc(MB(128));
    435 		linux_ctx.memory.debug_memory      = debug_memory.memory;
    436 		linux_ctx.memory.debug_memory_size = debug_memory.size;
    437 #endif
    438 	}
    439 
    440 	linux_ctx.inotify_fd = syscall1(SYS_inotify_init1, O_NONBLOCK|O_CLOEXEC);
    441 
    442 #ifdef _DEBUG
    443 	debug_reload_library((u8 *)DEBUG_LIB_NAME, &linux_ctx);
    444 	linux_add_file_watch((u8 *)DEBUG_LIB_NAME, debug_reload_library, &linux_ctx);
    445 #endif
    446 
    447 	linux_ctx.memory.platform_api.add_file_watch       = linux_add_file_watch;
    448 	linux_ctx.memory.platform_api.allocate_ring_buffer = os_allocate_ring_buffer;
    449 	linux_ctx.memory.platform_api.get_clipboard        = x11_get_clipboard;
    450 	linux_ctx.memory.platform_api.set_clipboard        = x11_set_clipboard;
    451 	linux_ctx.memory.platform_api.read_file            = os_read_file;
    452 	linux_ctx.memory.platform_api.read                 = os_read;
    453 	linux_ctx.memory.platform_api.set_terminal_size    = os_set_terminal_size;
    454 	linux_ctx.memory.platform_api.get_window_title     = x11_get_window_title;
    455 	linux_ctx.memory.platform_api.set_window_title     = x11_set_window_title;
    456 	linux_ctx.memory.platform_api.write                = os_write;
    457 	linux_ctx.memory.platform_api.path_separator       = '/';
    458 
    459 	if (!glfwInit())
    460 		os_fatal(s8("Failed to init GLFW\n"));
    461 	glfwSetErrorCallback(glfw_error_callback);
    462 
    463 	GLFWmonitor *mon = glfwGetPrimaryMonitor();
    464 	if (!mon) {
    465 		glfwTerminate();
    466 		os_fatal(s8("Failed to find any monitors!\n"));
    467 	}
    468 	iv2 monitor_size;
    469 	glfwGetMonitorWorkarea(mon, NULL, NULL, &monitor_size.w, &monitor_size.h);
    470 
    471 	linux_ctx.char_stream = arena_stream(linux_ctx.platform_memory);
    472 
    473 	iv2 window_size  = {.w = 1280, .h = 720};
    474 	linux_ctx.window = init_window(&linux_ctx, window_size);
    475 
    476 	iv2 requested_size = vtgl_initialize(&linux_ctx.memory, linux_ctx.child.handle, cells, monitor_size);
    477 	if (requested_size.w > 0 && requested_size.h > 0 &&
    478 	    (requested_size.w != window_size.w || requested_size.h != window_size.h))
    479 	{
    480 		glfwSetWindowAttrib(linux_ctx.window, GLFW_FLOATING, GLFW_TRUE);
    481 		i32 x       = ABS(window_size.w - requested_size.w) / 2;
    482 		i32 y       = ABS(window_size.h - requested_size.h) / 2;
    483 		window_size = requested_size;
    484 		glfwSetWindowMonitor(linux_ctx.window, 0, x, y, window_size.w, window_size.h, GLFW_DONT_CARE);
    485 		/* NOTE: resizable must be set after the window is shown; otherwise tiling window
    486 		 * managers will forcibly resize us even if we are supposed to be floating */
    487 		glfwShowWindow(linux_ctx.window);
    488 		glfwSetWindowAttrib(linux_ctx.window, GLFW_RESIZABLE, GLFW_TRUE);
    489 	} else {
    490 		/* NOTE: on the other hand we should let the window be resized if no size was
    491 		 * explicitly requested */
    492 		glfwSetWindowAttrib(linux_ctx.window, GLFW_RESIZABLE, GLFW_TRUE);
    493 		glfwSetWindowPos(linux_ctx.window,
    494 		                 ABS(monitor_size.w - window_size.w) / 2,
    495 		                 ABS(monitor_size.h - window_size.h) / 2);
    496 		glfwShowWindow(linux_ctx.window);
    497 	}
    498 	glfwMakeContextCurrent(0);
    499 
    500 	linux_ctx.input.window_size = window_size;
    501 
    502 	linux_ctx.render_stack->input           = &linux_ctx.input;
    503 	linux_ctx.render_stack->terminal_memory = &linux_ctx.memory;
    504 	linux_ctx.render_stack->thread_arena    = arena_from_memory_block(linux_block_alloc(MB(8)));
    505 	linux_ctx.render_stack->window          = linux_ctx.window;
    506 	syscall3(SYS_futex, (iptr)&linux_ctx.render_stack->work_futex, FUTEX_WAKE, 1);
    507 
    508 	Range last_sel   = {0};
    509 	f64   last_time  = os_get_time();
    510 	while (!glfwWindowShouldClose(linux_ctx.window)) {
    511 		if (os_child_exited(linux_ctx.child.process_id))
    512 			break;
    513 
    514 		/* TODO: cpu time excluding waiting for the vblank */
    515 		f64 current_time   = os_get_time();
    516 		linux_ctx.input.dt = current_time - last_time;
    517 		last_time          = current_time;
    518 
    519 		update_input(&linux_ctx);
    520 		if (vtgl_frame_step(&linux_ctx.memory, &linux_ctx.input))
    521 			syscall3(SYS_futex, (iptr)&linux_ctx.render_stack->work_futex, FUTEX_WAKE, 1);
    522 
    523 		Range current_sel = vtgl_active_selection(&linux_ctx.memory, 0);
    524 		if (is_valid_range(current_sel) && !equal_range(current_sel, last_sel)) {
    525 			Stream buf = arena_stream(linux_ctx.platform_memory);
    526 			vtgl_active_selection(&linux_ctx.memory, &buf);
    527 			stream_push_byte(&buf, 0);
    528 			if (!buf.errors)
    529 				glfwSetX11SelectionString((c8 *)buf.buf);
    530 			last_sel = current_sel;
    531 		}
    532 	}
    533 
    534 	return 0;
    535 }