vtgl

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

platform_linux_common.c (14637B)


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