vtgl

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

platform_linux_common.c (14266B)


      1 #define FUTEX_WAIT 0
      2 #define FUTEX_WAKE 1
      3 
      4 #define CLOCK_MONOTONIC 1
      5 
      6 #define PR_SET_NAME 15
      7 
      8 #define PROT_READ   0x01
      9 #define PROT_RW     0x03
     10 
     11 #define MFD_CLOEXEC 0x01
     12 
     13 #define MAP_SHARED  0x01
     14 #define MAP_PRIVATE 0x02
     15 #define MAP_FIXED   0x10
     16 #define MAP_ANON    0x20
     17 
     18 #define MADV_HUGEPAGE 14
     19 
     20 #define O_RDONLY    0x00000
     21 #define O_WRONLY    0x00001
     22 #define O_RDWR      0x00002
     23 #define O_CREAT     0x00040
     24 #define O_NOCTTY    0x00100
     25 #define O_APPEND    0x00400
     26 #define O_NONBLOCK  0x00800
     27 #define O_CLOEXEC   0x80000
     28 
     29 #define IN_CLOSE_WRITE   0x00000008
     30 #define IN_CLOSE_NOWRITE 0x00000010
     31 #define IN_MODIFY        0x00000002
     32 
     33 #define AT_EMPTY_PATH 0x1000
     34 #define AT_FDCWD      (-100)
     35 
     36 #define LINUX_INOTIFY_MASK (IN_CLOSE_WRITE|IN_CLOSE_NOWRITE|IN_MODIFY)
     37 
     38 #define WNOHANG 1
     39 #define W_IF_EXITED(s) (!((s) & 0x7F))
     40 
     41 /* TODO: glibc/gcc indirectly include sys/select.h if you include immintrin.h. If that
     42  * header is removed this can also be removed */
     43 #undef FD_SET
     44 #undef FD_ISSET
     45 
     46 #define FD_SET(d, s)   ((s)[(d) / (8 * sizeof(*(s)))] |= (1ULL << ((d) % (8 * sizeof(*(s))))))
     47 #define FD_ISSET(d, s) ((s)[(d) / (8 * sizeof(*(s)))] &  (1ULL << ((d) % (8 * sizeof(*(s))))))
     48 
     49 typedef __attribute__((aligned(16))) u8 statx_buffer[256];
     50 #define STATX_BUF_MEMBER(sb, t, off) (*(t *)((u8 *)(sb) + off))
     51 #define STATX_INODE(sb)      STATX_BUF_MEMBER(sb, u64,  32)
     52 #define STATX_FILE_SIZE(sb)  STATX_BUF_MEMBER(sb, u64,  40)
     53 
     54 #define STATX_INO            0x00000100U
     55 #define STATX_SIZE           0x00000200U
     56 
     57 #define TIOCSCTTY  0x540E
     58 #define TIOCSWINSZ 0x5414
     59 #define TIOCSPTLCK 0x40045431 /* (un)lock pty   */
     60 #define TIOCGPTN   0x80045430 /* get pty number */
     61 
     62 #ifndef VERSION
     63 #define VERSION "unknown"
     64 #endif
     65 
     66 #define OS_MAP_READ    PROT_READ
     67 #define OS_MAP_PRIVATE MAP_PRIVATE
     68 
     69 struct __attribute__((aligned(16))) stack_base {
     70 	void (*entry)(struct stack_base *stack);
     71 	Arena            thread_arena;
     72 	void            *window;
     73 	TerminalMemory  *terminal_memory;
     74 	TerminalInput   *input;
     75 	i32              work_futex;
     76 	b32              thread_asleep;
     77 };
     78 
     79 typedef struct {
     80 	platform_file_watch_callback_fn *fn;
     81 	u8   *path;
     82 	void *user_ctx;
     83 	u64   inode;
     84 	i32   handle;
     85 } linux_file_watch;
     86 
     87 typedef struct linux_deferred_file_reload_queue {
     88 	struct linux_deferred_file_reload_queue *next;
     89 	struct linux_deferred_file_reload_queue *last;
     90 	i32 index;
     91 	i32 failures;
     92 } linux_deferred_file_reload_queue;
     93 
     94 typedef struct {
     95 	iptr handle;
     96 	iptr process_id;
     97 } linux_platform_process;
     98 
     99 typedef struct {
    100 	Arena  platform_memory;
    101 	void  *window;
    102 
    103 	TerminalMemory memory;
    104 	TerminalInput  input;
    105 
    106 	Stream char_stream;
    107 
    108 	linux_platform_process child;
    109 	i32 inotify_fd;
    110 	i32 win_fd;
    111 
    112 	linux_deferred_file_reload_queue file_reload_queue;
    113 	linux_deferred_file_reload_queue *file_reload_free_list;
    114 	linux_file_watch file_watches[32];
    115 	i32 file_watch_count;
    116 
    117 	Stream error_stream;
    118 
    119 	struct stack_base *render_stack;
    120 
    121 #ifdef _DEBUG
    122 	void *library_handle;
    123 #endif
    124 } PlatformCtx;
    125 static PlatformCtx linux_ctx;
    126 
    127 static void
    128 os_write_err_msg(s8 msg)
    129 {
    130 	syscall3(SYS_write, 2, (iptr)msg.data, msg.len);
    131 }
    132 
    133 __attribute__((noreturn))
    134 static void
    135 os_fatal(s8 msg)
    136 {
    137 	os_write_err_msg(msg);
    138 	syscall1(SYS_exit_group, 1);
    139 	__builtin_unreachable();
    140 }
    141 
    142 static u32
    143 os_file_attribute_to_mode(u32 attr)
    144 {
    145 	u32 result = O_CREAT;
    146 	if (attr & FA_READ && attr & FA_WRITE) {
    147 		result |= O_RDWR;
    148 	} else if (attr & FA_READ) {
    149 		result |= O_RDONLY;
    150 	} else if (attr & FA_WRITE) {
    151 		result |= O_WRONLY;
    152 	}
    153 
    154 	if (attr & FA_APPEND)
    155 		result |= O_APPEND;
    156 
    157 	return result;
    158 }
    159 
    160 static iptr
    161 os_open(u8 *name, u32 attr)
    162 {
    163 	iptr result = syscall4(SYS_openat, AT_FDCWD, (iptr)name, os_file_attribute_to_mode(attr), 0660);
    164 	if (result > -4096UL)
    165 		result = INVALID_FILE;
    166 	return result;
    167 }
    168 
    169 static b32
    170 os_offset_write(iptr file, s8 raw, size offset)
    171 {
    172 	size result = syscall4(SYS_pwrite64, file, (iptr)raw.data, raw.len, offset);
    173 	return result == raw.len;
    174 }
    175 
    176 static PLATFORM_WRITE_FN(os_write)
    177 {
    178 	size result = syscall3(SYS_write, file, (iptr)raw.data, raw.len);
    179 	return result == raw.len;
    180 }
    181 
    182 static void
    183 os_close(iptr file)
    184 {
    185 	syscall1(SYS_close, file);
    186 }
    187 
    188 static PLATFORM_READ_FN(os_read)
    189 {
    190 	size r = 0, remaining = buffer.len, total_bytes_read = 0;
    191 
    192 	do {
    193 		remaining        -= r;
    194 		total_bytes_read += r;
    195 		r = syscall3(SYS_read, file, (iptr)(buffer.data + total_bytes_read), remaining);
    196 	} while (r <= -4096UL && remaining != 0);
    197 
    198 	return total_bytes_read;
    199 }
    200 
    201 static PLATFORM_READ_FILE_FN(os_read_file)
    202 {
    203 	s8 result = {0};
    204 
    205 	statx_buffer sb;
    206 	i32 fd     = syscall4(SYS_openat, AT_FDCWD, (iptr)path, O_RDONLY, 0);
    207 	i64 status = syscall5(SYS_statx, fd, 0, AT_EMPTY_PATH, STATX_SIZE, (iptr)sb);
    208 
    209 	if (fd <= -4096UL && status == 0) {
    210 		result = s8alloc(a, STATX_FILE_SIZE(sb));
    211 		size rlen = os_read(fd, result);
    212 		syscall1(SYS_close, fd);
    213 		if (result.len != rlen)
    214 			result.len = 0;
    215 	}
    216 
    217 	return result;
    218 }
    219 
    220 static MemoryBlock
    221 linux_block_alloc(size requested_size)
    222 {
    223 	MemoryBlock result = {0};
    224 
    225 	/* TODO: query system for HUGETLB support and use those instead of page size */
    226 	size alloc_size = requested_size;
    227 	if (alloc_size % PAGE_SIZE != 0)
    228 		alloc_size += PAGE_SIZE - alloc_size % PAGE_SIZE;
    229 
    230 	i64 memory = syscall6(SYS_mmap, 0, alloc_size, PROT_RW, MAP_ANON|MAP_PRIVATE, -1, 0);
    231 	if (memory <= -4096UL) {
    232 		result.memory = (void *)memory;
    233 		result.size   = alloc_size;
    234 		syscall3(SYS_madvise, memory, alloc_size, MADV_HUGEPAGE);
    235 	}
    236 
    237 	return result;
    238 }
    239 
    240 static f64
    241 os_get_time(void)
    242 {
    243 	i64 timespec[2];
    244 	syscall2(SYS_clock_gettime, CLOCK_MONOTONIC, (iptr)timespec);
    245 	f64 result = timespec[0] + ((f64)timespec[1]) * 1e-9;
    246 	return result;
    247 }
    248 
    249 static os_mapped_file
    250 os_map_file(char *path, i32 mode, i32 perm)
    251 {
    252 	os_mapped_file result = {0};
    253 
    254 	i32 open_mode = 0;
    255 	switch (mode) {
    256 	case OS_MAP_READ: open_mode = O_RDONLY; break;
    257 	default: ASSERT(0);
    258 	}
    259 
    260 	statx_buffer sb;
    261 	i32 fd     = syscall4(SYS_openat, AT_FDCWD, (iptr)path, open_mode, 0);
    262 	i64 status = syscall5(SYS_statx, fd, 0, AT_EMPTY_PATH, STATX_SIZE, (iptr)sb);
    263 
    264 	if (fd <= -4096UL && status == 0) {
    265 		i64 memory = syscall6(SYS_mmap, 0, STATX_FILE_SIZE(sb), mode, perm, fd, 0);
    266 		if (memory <= -4096UL) {
    267 			result.data = (u8 *)memory;
    268 			result.len  = STATX_FILE_SIZE(sb);
    269 		}
    270 		syscall1(SYS_close, fd);
    271 	}
    272 
    273 	return result;
    274 }
    275 
    276 static PLATFORM_ALLOCATE_RING_BUFFER_FN(os_allocate_ring_buffer)
    277 {
    278 	/* TODO: query system for HUGETLB support and use those instead of page size */
    279 	if (capacity % PAGE_SIZE != 0)
    280 		capacity += PAGE_SIZE - capacity % PAGE_SIZE;
    281 	ASSERT(capacity % PAGE_SIZE == 0);
    282 
    283 	i32 fd = syscall2(SYS_memfd_create, (iptr)"vtgl:rb", MFD_CLOEXEC);
    284 	if (fd > -4096UL) os_fatal(s8("os_alloc_ring_buffer: failed to open mem_fd\n"));
    285 	syscall2(SYS_ftruncate, fd, capacity);
    286 
    287 	rb->widx   = 0;
    288 	rb->filled = 0;
    289 	rb->cap    = capacity;
    290 	rb->buf    = (u8 *)syscall6(SYS_mmap, 0, (iptr)(3 * rb->cap), 0, MAP_ANON|MAP_PRIVATE, -1, 0);
    291 	if ((iptr)rb->buf > -4096UL)
    292 		os_fatal(s8("os_alloc_ring_buffer: initial mmap failed\n"));
    293 	syscall3(SYS_madvise, (iptr)rb->buf, 3 * rb->cap, MADV_HUGEPAGE);
    294 
    295 	for (i32 i = 0; i < 3; i++) {
    296 		i64 memory = syscall6(SYS_mmap, (iptr)(rb->buf + i * rb->cap), rb->cap, PROT_RW,
    297 		                      MAP_FIXED|MAP_SHARED, fd, 0);
    298 		if (memory > -4096UL) {
    299 			u8 buf[256];
    300 			Stream err = {.buf = buf, .cap = sizeof(buf)};
    301 			stream_push_s8(&err, s8("os_alloc_ring_buffer: mmap("));
    302 			stream_push_u64(&err, i);
    303 			stream_push_s8(&err, s8(") failed\n"));
    304 			os_fatal(stream_to_s8(&err));
    305 		}
    306 	}
    307 	syscall1(SYS_close, fd);
    308 
    309 	/* NOTE: start in the middle page */
    310 	rb->buf += rb->cap;
    311 }
    312 
    313 static b32
    314 os_child_exited(iptr pid)
    315 {
    316 	i64 status;
    317 	i64 r = syscall4(SYS_wait4, pid, (iptr)&status, WNOHANG, 0);
    318 	return r == pid && W_IF_EXITED(status);
    319 }
    320 
    321 static linux_platform_process
    322 os_fork_child(s8 cmd, c8 **envp)
    323 {
    324 	i32 n = 0;
    325 
    326 	/* NOTE: we open in non-blocking mode so that we can try and fully drain the pipe
    327 	 * before processing. Otherwise a single read will be limited to the page size */
    328 	i64 m = syscall4(SYS_openat, AT_FDCWD, (iptr)"/dev/ptmx", O_RDWR|O_NOCTTY|O_NONBLOCK|O_CLOEXEC, 0);
    329 	if (m > -4096UL) os_fatal(s8("os_fork_child: failed to open master terminal\n"));
    330 	/* NOTE: first unlock the tty, then get a valid pty number */
    331 	if (syscall3(SYS_ioctl, m, TIOCSPTLCK, (iptr)&n) || syscall3(SYS_ioctl, m, TIOCGPTN, (iptr)&n))
    332 		os_fatal(s8("os_fork_child: failed to get a pty number\n"));
    333 
    334 	u8 buffer[20] = {"/dev/pts/"};
    335 	Stream sbuf = {.buf = buffer, .cap = 20, .widx = sizeof("/dev/pts/") - 1};
    336 	stream_push_i64(&sbuf, n);
    337 	stream_push_byte(&sbuf, 0);
    338 
    339 	i64 s = syscall4(SYS_openat, AT_FDCWD, (iptr)sbuf.buf, O_RDWR|O_NOCTTY, 0);
    340 	if (s > -4096UL) os_fatal(s8("os_fork_child: failed to open slave terminal\n"));
    341 
    342 	i64 pid = syscall2(SYS_clone, SIGCHLD, 0);
    343 	if (pid > -4096UL) os_fatal(s8("os_fork_child: failed to fork a child\n"));
    344 
    345 	if (pid == 0) {
    346 		syscall1(SYS_setsid, 0);
    347 		syscall3(SYS_dup3, s, 0, 0);
    348 		syscall3(SYS_dup3, s, 1, 0);
    349 		syscall3(SYS_dup3, s, 2, 0);
    350 		syscall3(SYS_ioctl, s, TIOCSCTTY, 0);
    351 		if (s > 2) syscall1(SYS_close, s);
    352 		ASSERT(cmd.data[cmd.len] == 0);
    353 		u8 *argv[] = {cmd.data, 0};
    354 		syscall3(SYS_execve, (iptr)cmd.data, (iptr)argv, (iptr)envp);
    355 		__builtin_unreachable();
    356 		os_fatal(s8("failed to exec child\n"));
    357 	}
    358 	syscall1(SYS_close, s);
    359 
    360 	return (linux_platform_process){.process_id = pid, .handle = m};
    361 }
    362 
    363 static PLATFORM_SET_TERMINAL_SIZE_FN(os_set_terminal_size)
    364 {
    365 	u16 win_size[4];
    366 	win_size[0] = rows;
    367 	win_size[1] = columns;
    368 	win_size[2] = window_width;
    369 	win_size[3] = window_height;
    370 	if (syscall3(SYS_ioctl, child, TIOCSWINSZ, (iptr)win_size) > -4096UL)
    371 		os_write_err_msg(s8("os_set_term_size\n"));
    372 }
    373 
    374 static PLATFORM_ADD_FILE_WATCH_FN(linux_add_file_watch)
    375 {
    376 	i32 wd  = syscall3(SYS_inotify_add_watch, linux_ctx.inotify_fd, (iptr)path, LINUX_INOTIFY_MASK);
    377 	if (wd <= -4096UL) {
    378 		statx_buffer sb;
    379 		syscall5(SYS_statx, AT_FDCWD, (iptr)path, 0, STATX_INO, (iptr)sb);
    380 
    381 		i32 idx = linux_ctx.file_watch_count++;
    382 		ASSERT(idx < ARRAY_COUNT(linux_ctx.file_watches));
    383 		linux_ctx.file_watches[idx].fn       = fn;
    384 		linux_ctx.file_watches[idx].path     = path;
    385 		linux_ctx.file_watches[idx].handle   = wd;
    386 		linux_ctx.file_watches[idx].inode    = STATX_INODE(sb);
    387 		linux_ctx.file_watches[idx].user_ctx = user_ctx;
    388 	}
    389 }
    390 
    391 static void
    392 try_deferred_file_loads(PlatformCtx *ctx)
    393 {
    394 	linux_deferred_file_reload_queue *file = ctx->file_reload_queue.next;
    395 	while (file) {
    396 		linux_file_watch *fw = ctx->file_watches + file->index;
    397 
    398 		statx_buffer sb;
    399 		syscall5(SYS_statx, AT_FDCWD, (iptr)fw->path, 0, STATX_INO, (iptr)sb);
    400 
    401 		fw->handle = syscall3(SYS_inotify_add_watch, ctx->inotify_fd, (iptr)fw->path,
    402 		                      LINUX_INOTIFY_MASK);
    403 		fw->inode  = STATX_INODE(sb);
    404 
    405 		if (fw->handle <= -4096UL) {
    406 			fw->fn(fw->path, fw->user_ctx);
    407 			file->last->next = file->next;
    408 			file->next       = ctx->file_reload_free_list;
    409 			ctx->file_reload_free_list = file;
    410 			file = file->last;
    411 		} else {
    412 			file->failures++;
    413 			#if 0
    414 			TODO
    415 			if (file->failures > MAX_FILE_RELOAD_TRIES) {
    416 				log
    417 				remove from list
    418 			}
    419 			#endif
    420 		}
    421 		file = file->next;
    422 	}
    423 }
    424 
    425 static b32
    426 defer_file_reload(PlatformCtx *ctx, i32 file_watch_index, statx_buffer *sb)
    427 {
    428 	b32 result = 1;
    429 	linux_file_watch *fw = ctx->file_watches + file_watch_index;
    430 
    431 	fw->inode  = STATX_INODE(*sb);
    432 	fw->handle = syscall3(SYS_inotify_add_watch, ctx->inotify_fd, (iptr)fw->path, LINUX_INOTIFY_MASK);
    433 
    434 	if (fw->handle > -4096UL) {
    435 		result = 0;
    436 
    437 		linux_deferred_file_reload_queue *new = ctx->file_reload_free_list;
    438 		if (new) ctx->file_reload_free_list = new->next;
    439 		else     new = push_struct(&ctx->platform_memory, typeof(*new));
    440 		new->index    = file_watch_index;
    441 		new->failures = 0;
    442 		DLLPushDown(&ctx->file_reload_queue, new);
    443 	}
    444 
    445 	return result;
    446 }
    447 
    448 static void
    449 dispatch_file_watch_events(PlatformCtx *ctx)
    450 {
    451 	struct {
    452 		i32 wd;
    453 		u32 mask, cookie, len;
    454 		c8  name[];
    455 	} *ie;
    456 
    457 	u8 *mem = alloc_(&ctx->platform_memory, 4096, 64, 1);
    458 	s8 buf = {.len = 4096, .data = mem};
    459 
    460 	for (;;) {
    461 		size rlen = syscall3(SYS_read, ctx->inotify_fd, (iptr)buf.data, buf.len);
    462 		if (rlen <= 0)
    463 			break;
    464 
    465 		for (u8 *data = buf.data; data < buf.data + rlen; data += sizeof(*ie) + ie->len) {
    466 			ie = (void *)data;
    467 			for (i32 i = 0; i < ctx->file_watch_count; i++) {
    468 				linux_file_watch *fw = ctx->file_watches + i;
    469 				if (fw->handle != ie->wd)
    470 					continue;
    471 
    472 				b32 file_changed  = (ie->mask & IN_CLOSE_WRITE) != 0;
    473 				file_changed     |= (ie->mask & IN_MODIFY) != 0;
    474 				/* NOTE: some editors and the compiler will rewrite a file
    475 				 * completely and thus the inode will change; here we
    476 				 * detect that and restart the watch */
    477 				statx_buffer sb;
    478 				i64 status = syscall5(SYS_statx, AT_FDCWD, (iptr)fw->path, 0, STATX_INO, (iptr)sb);
    479 
    480 				if (status > -4096UL || fw->inode != STATX_INODE(sb)) {
    481 					syscall2(SYS_inotify_rm_watch, ctx->inotify_fd, fw->handle);
    482 					fw->handle   = INVALID_FILE;
    483 					file_changed = defer_file_reload(ctx, i, &sb);
    484 				}
    485 				if (file_changed)
    486 					fw->fn(fw->path, fw->user_ctx);
    487 			}
    488 		}
    489 	}
    490 }
    491 
    492 static struct stack_base *
    493 new_stack(size capacity)
    494 {
    495 	i64 p = syscall6(SYS_mmap, 0, capacity, PROT_RW, MAP_ANON|MAP_PRIVATE, -1, 0);
    496 	if (p > -4096UL)
    497 		os_fatal(s8("new_stack: mmap failed\n"));
    498 	i64 count = capacity / sizeof(struct stack_base);
    499 	/* NOTE: remember the stack grows down; we want to start at the highest address */
    500 	struct stack_base *result = (struct stack_base *)p + count - 1;
    501 	return result;
    502 }
    503 
    504 static void
    505 usage(char *argv0, Stream *err)
    506 {
    507 	stream_push_s8(err, s8("usage: "));
    508 	stream_push_s8(err, c_str_to_s8(argv0));
    509 	stream_push_s8(err, s8(" [-v] [-g COLxROW]\n"));
    510 	os_fatal(stream_to_s8(err));
    511 }
    512 
    513 static s8
    514 get_default_cmd(char **envp)
    515 {
    516 	s8 result = envp_lookup(s8("SHELL="), envp);
    517 	if (result.len == 0)
    518 		result = s8("/bin/sh");
    519 	return result;
    520 }
    521 
    522 static SLLVariableVector
    523 parse_environment(Arena *a, char **envp)
    524 {
    525 	SLLVariableVector env = {0};
    526 	for (; *envp; envp++) {
    527 		s8 e = c_str_to_s8(*envp);
    528 		if (!s8_prefix_of(s8("TERM="), e)) {
    529 			Variable *var = push_struct(a, Variable);
    530 			var->type = VT_S8;
    531 			var->s8   = e;
    532 			SLLVariableVectorPush(a, &env, var);
    533 		}
    534 	}
    535 
    536 	Variable *var = push_struct(a, Variable);
    537 	var->type = VT_S8;
    538 	/* TODO: don't pretend to be xterm ? */
    539 	var->s8   = s8("TERM=xterm");
    540 	SLLVariableVectorPush(a, &env, var);
    541 
    542 	return env;
    543 }