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 }