4coder/linux_4ed.cpp

3479 lines
97 KiB
C++

/*
* Mr. 4th Dimention - Allen Webster
* (Mostly by insofaras)
*
* 14.11.2015
*
* Linux layer for project codename "4ed"
*
*/
// TOP
# include "4ed_defines.h"
#if FRED_SUPER
# define FSTRING_IMPLEMENTATION
# define FSTRING_C
# include "4coder_string.h"
#include "4coder_version.h"
# include "4coder_keycodes.h"
# include "4coder_style.h"
# include "4coder_rect.h"
# include <assert.h>
# include "4coder_mem.h"
// TODO(allen): This is duplicated from 4coder_custom.h
// I need to work out a way to avoid this.
#define VIEW_ROUTINE_SIG(name) void name(struct Application_Links *app, int32_t view_id)
#define GET_BINDING_DATA(name) int32_t name(void *data, int32_t size)
#define _GET_VERSION_SIG(n) int32_t n(int32_t maj, int32_t min, int32_t patch)
typedef VIEW_ROUTINE_SIG(View_Routine_Function);
typedef GET_BINDING_DATA(Get_Binding_Data_Function);
typedef _GET_VERSION_SIG(_Get_Version_Function);
struct Custom_API{
View_Routine_Function *view_routine;
Get_Binding_Data_Function *get_bindings;
_Get_Version_Function *get_alpha_4coder_version;
};
typedef void Custom_Command_Function;
#include "4coder_types.h"
struct Application_Links;
# include "4ed_os_custom_api.h"
//# include "4coder_custom.h"
#else
# include "4coder_default_bindings.cpp"
# define FSTRING_IMPLEMENTATION
# define FSTRING_C
# include "4coder_string.h"
#endif
#include "4ed_math.h"
#include "4ed_system.h"
#include "4ed_rendering.h"
#include "4ed.h"
#include <stdio.h>
#include <math.h>
#include <time.h>
#include <stdlib.h>
#include <locale.h>
#include <memory.h>
#include <dirent.h>
#include <dlfcn.h>
#include <unistd.h>
#include <xmmintrin.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <ucontext.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/timerfd.h>
#include <sys/eventfd.h>
#include <sys/epoll.h>
#include <sys/inotify.h>
#include <X11/Xlib.h>
#include <X11/cursorfont.h>
#include <X11/Xatom.h>
#include <X11/extensions/Xfixes.h>
#include <GL/glx.h>
#include <GL/gl.h>
#include <GL/glext.h>
#include <linux/fs.h>
#include <linux/input.h>
//
// Linux macros
//
#define LINUX_MAX_PASTE_CHARS 0x10000L
#define FPS 60L
#define frame_useconds (1000000UL / FPS)
#define LinuxGetMemory(size) LinuxGetMemory_(size, __LINE__, __FILE__)
#if FRED_INTERNAL
#define LINUX_FN_DEBUG(fmt, ...) do { \
fprintf(stderr, "%s: " fmt "\n", __func__, ##__VA_ARGS__); \
} while(0)
#else
#define LINUX_FN_DEBUG(fmt, ...)
#endif
#if (__cplusplus <= 199711L)
#define static_assert(x, ...)
#endif
#define SUPPORT_DPI 1
#define LINUX_FONTS 1
#define InterlockedCompareExchange(dest, ex, comp) __sync_val_compare_and_swap((dest), (comp), (ex))
#include "filetrack/4tech_file_track_linux.c"
#include "system_shared.h"
//
// Linux structs / enums
//
#if FRED_INTERNAL
struct Sys_Bubble : public Bubble{
i32 line_number;
char *file_name;
};
#endif
enum {
LINUX_4ED_EVENT_X11 = (UINT64_C(1) << 32),
LINUX_4ED_EVENT_X11_INTERNAL = (UINT64_C(2) << 32),
LINUX_4ED_EVENT_STEP = (UINT64_C(3) << 32),
LINUX_4ED_EVENT_STEP_TIMER = (UINT64_C(4) << 32),
LINUX_4ED_EVENT_CLI = (UINT64_C(5) << 32),
};
struct Linux_Coroutine {
Coroutine coroutine;
Linux_Coroutine *next;
ucontext_t ctx, yield_ctx;
stack_t stack;
b32 done;
};
struct Thread_Context{
u32 job_id;
b32 running;
b32 cancel;
Work_Queue *queue;
u32 id;
u32 group_id;
pthread_t handle;
};
struct Thread_Group{
Thread_Context *threads;
i32 count;
Unbounded_Work_Queue queue;
i32 cancel_lock0;
i32 cancel_cv0;
};
struct Linux_Vars{
Display *XDisplay;
Window XWindow;
Render_Target target;
XIM input_method;
XIMStyle input_style;
XIC input_context;
Application_Step_Input input;
String clipboard_contents;
String clipboard_outgoing;
b32 new_clipboard;
Atom atom_TARGETS;
Atom atom_CLIPBOARD;
Atom atom_UTF8_STRING;
Atom atom__NET_WM_STATE;
Atom atom__NET_WM_STATE_MAXIMIZED_HORZ;
Atom atom__NET_WM_STATE_MAXIMIZED_VERT;
Atom atom__NET_WM_STATE_FULLSCREEN;
Atom atom__NET_WM_PING;
Atom atom__NET_WM_WINDOW_TYPE;
Atom atom__NET_WM_WINDOW_TYPE_NORMAL;
Atom atom__NET_WM_PID;
Atom atom_WM_DELETE_WINDOW;
b32 has_xfixes;
int xfixes_selection_event;
int epoll;
int step_timer_fd;
int step_event_fd;
int x11_fd;
int inotify_fd;
u64 last_step;
b32 keep_running;
Application_Mouse_Cursor cursor;
b32 hide_cursor;
Cursor hidden_cursor;
void *app_code;
void *custom;
Thread_Memory *thread_memory;
Thread_Group groups[THREAD_GROUP_COUNT];
Work_Queue queues[THREAD_GROUP_COUNT];
pthread_mutex_t locks[LOCK_COUNT];
pthread_cond_t conds[8];
sem_t thread_semaphore;
Partition font_part;
#if SUPPORT_DPI
i32 dpi_x, dpi_y;
#endif
Plat_Settings settings;
System_Functions system;
App_Functions app;
Custom_API custom_api;
b32 vsync;
#if FRED_INTERNAL
Sys_Bubble internal_bubble;
pthread_mutex_t DEBUG_sysmem_lock;
#endif
Linux_Coroutine coroutine_data[18];
Linux_Coroutine *coroutine_free;
};
//
// Linux globals
//
globalvar Linux_Vars linuxvars;
globalvar Application_Memory memory_vars;
//
// Linux forward declarations
//
internal void LinuxScheduleStep(void);
internal Plat_Handle LinuxSemToHandle(sem_t*);
internal sem_t* LinuxHandleToSem(Plat_Handle);
internal Plat_Handle LinuxFDToHandle(int);
internal int LinuxHandleToFD(Plat_Handle);
internal void LinuxStringDup(String*, void*, size_t);
internal void LinuxToggleFullscreen(Display*, Window);
internal Sys_Acquire_Lock_Sig(system_acquire_lock);
internal Sys_Release_Lock_Sig(system_release_lock);
internal void system_wait_cv(i32, i32);
internal void system_signal_cv(i32, i32);
//
// Linux static assertions
//
static_assert(sizeof(Plat_Handle) >= sizeof(ucontext_t*), "Plat_Handle not big enough");
static_assert(sizeof(Plat_Handle) >= sizeof(sem_t*), "Plat_Handle not big enough");
static_assert(sizeof(Plat_Handle) >= sizeof(int), "Plat_Handle not big enough");
//
// Shared system functions (system_shared.h)
//
internal void*
LinuxGetMemory_(i32 size, i32 line_number, char *file_name){
void *result = 0;
Assert(size != 0);
#if FRED_INTERNAL
result = mmap(0, size + sizeof(Sys_Bubble), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
Sys_Bubble* bubble = (Sys_Bubble*)result;
bubble->flags = 0;
bubble->line_number = line_number;
bubble->file_name = file_name;
bubble->size = size;
pthread_mutex_lock(&linuxvars.DEBUG_sysmem_lock);
insert_bubble(&linuxvars.internal_bubble, bubble);
pthread_mutex_unlock(&linuxvars.DEBUG_sysmem_lock);
result = bubble + 1;
#else
size_t real_size = size + sizeof(size_t);
result = mmap(0, real_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(result == MAP_FAILED){
perror("mmap");
result = NULL;
} else {
memcpy(result, &real_size, sizeof(size_t));
result = (char*)result + sizeof(size_t);
}
#endif
return(result);
}
internal void
LinuxFreeMemory(void *block){
if (block){
#if FRED_INTERNAL
Sys_Bubble *bubble = ((Sys_Bubble*)block) - 1;
size_t size = bubble->size + sizeof(Sys_Bubble);
pthread_mutex_lock(&linuxvars.DEBUG_sysmem_lock);
remove_bubble(bubble);
pthread_mutex_unlock(&linuxvars.DEBUG_sysmem_lock);
munmap(bubble, size);
#else
block = (char*)block - sizeof(size_t);
size_t size = *(size_t*)block;
munmap(block, size);
#endif
}
}
internal
Sys_Get_Memory_Sig(system_get_memory_){
return(LinuxGetMemory_(size, line_number, file_name));
}
internal
Sys_Free_Memory_Sig(system_free_memory){
LinuxFreeMemory(block);
}
internal
Sys_File_Can_Be_Made_Sig(system_file_can_be_made){
b32 result = access(filename, W_OK) == 0;
LINUX_FN_DEBUG("%s = %d", filename, result);
return(result);
}
internal
Sys_Get_Binary_Path_Sig(system_get_binary_path){
ssize_t size = readlink("/proc/self/exe", out->str, out->memory_size - 1);
if(size != -1 && size < out->memory_size - 1){
out->size = size;
remove_last_folder(out);
terminate_with_null(out);
size = out->size;
} else {
size = 0;
}
return size;
}
//
// System Functions (4ed_system.h)
//
//
// Files
//
internal
Sys_Set_File_List_Sig(system_set_file_list){
DIR *d;
struct dirent *entry;
char *fname, *cursor, *cursor_start;
File_Info *info_ptr;
i32 count, file_count, size, required_size;
if(directory.size <= 0){
if(!directory.str){
system_free_memory(file_list->block);
file_list->block = 0;
file_list->block_size = 0;
}
file_list->infos = 0;
file_list->count = 0;
return;
}
char* dir = (char*) alloca(directory.size + 1);
memcpy(dir, directory.str, directory.size);
dir[directory.size] = 0;
LINUX_FN_DEBUG("%s", dir);
d = opendir(dir);
if (d){
count = 0;
file_count = 0;
for (entry = readdir(d);
entry != 0;
entry = readdir(d)){
fname = entry->d_name;
if(fname[0] == '.' && (fname[1] == 0 || (fname[1] == '.' && fname[2] == 0))){
continue;
}
++file_count;
for (size = 0; fname[size]; ++size);
count += size + 1;
}
required_size = count + file_count * sizeof(File_Info);
if (file_list->block_size < required_size){
system_free_memory(file_list->block);
file_list->block = system_get_memory(required_size);
}
file_list->infos = (File_Info*)file_list->block;
cursor = (char*)(file_list->infos + file_count);
rewinddir(d);
info_ptr = file_list->infos;
for (entry = readdir(d);
entry != 0;
entry = readdir(d)){
fname = entry->d_name;
if(fname[0] == '.' && (fname[1] == 0 || (fname[1] == '.' && fname[2] == 0))){
continue;
}
cursor_start = cursor;
for (; *fname; ) *cursor++ = *fname++;
if(entry->d_type == DT_LNK){
struct stat st;
if(stat(entry->d_name, &st) != -1){
info_ptr->folder = S_ISDIR(st.st_mode);
} else {
info_ptr->folder = 0;
}
} else {
info_ptr->folder = entry->d_type == DT_DIR;
}
info_ptr->filename = cursor_start;
info_ptr->filename_len = (int)(cursor - cursor_start);
*cursor++ = 0;
++info_ptr;
}
file_list->count = file_count;
closedir(d);
} else {
system_free_memory(file_list->block);
file_list->block = 0;
file_list->block_size = 0;
file_list->infos = 0;
file_list->count = 0;
}
}
internal
Sys_Get_Canonical_Sig(system_get_canonical){
char* path = (char*) alloca(len + 1);
char* write_p = path;
const char* read_p = filename;
while(read_p < filename + len){
if(read_p == filename || read_p[0] == '/'){
if(read_p[1] == '/'){
++read_p;
} else if(read_p[1] == '.'){
if(read_p[2] == '/' || !read_p[2]){
read_p += 2;
} else if(read_p[2] == '.' && (read_p[3] == '/' || !read_p[3])){
while(write_p > path && *--write_p != '/');
read_p += 3;
} else {
*write_p++ = *read_p++;
}
} else {
*write_p++ = *read_p++;
}
} else {
*write_p++ = *read_p++;
}
}
if(write_p == path) *write_p++ = '/';
if(max >= (write_p - path)){
memcpy(buffer, path, write_p - path);
} else {
write_p = path;
}
#if FRED_INTERNAL
if(len != (write_p - path) || memcmp(filename, path, len) != 0){
LINUX_FN_DEBUG("[%.*s] -> [%.*s]", len, filename, (int)(write_p - path), path);
}
#endif
return write_p - path;
}
internal
Sys_Load_Handle_Sig(system_load_handle){
b32 result = 0;
int fd = open(filename, O_RDONLY);
if(fd == -1){
perror("open");
} else {
*(int*)handle_out = fd;
result = 1;
}
return result;
}
internal
Sys_Load_Size_Sig(system_load_size){
u32 result = 0;
int fd = *(int*)&handle;
struct stat st;
if(fstat(fd, &st) == -1){
perror("fstat");
} else {
result = st.st_size;
}
return result;
}
internal
Sys_Load_File_Sig(system_load_file){
int fd = *(int*)&handle;
do {
ssize_t n = read(fd, buffer, size);
if(n == -1 && errno != EAGAIN){
perror("read");
break;
} else {
size -= n;
buffer += n;
}
} while(size);
return size == 0;
}
internal
Sys_Load_Close_Sig(system_load_close){
b32 result = 1;
int fd = *(int*)&handle;
if(close(fd) == -1){
perror("close");
result = 0;
}
return result;
}
internal
Sys_Save_File_Sig(system_save_file){
b32 result = 0;
int fd = open(filename, O_WRONLY | O_TRUNC | O_CREAT, 00640);
LINUX_FN_DEBUG("%s %d", filename, size);
if(fd < 0){
fprintf(stderr, "system_save_file: open '%s': %s\n", filename, strerror(errno));
} else {
do {
ssize_t written = write(fd, buffer, size);
if(written == -1){
if(errno != EINTR){
perror("system_save_file: write");
break;
}
} else {
size -= written;
buffer += written;
}
} while(size);
close(fd);
}
return (size == 0);
}
//
// Time
//
internal
Sys_Now_Time_Sig(system_now_time){
struct timespec spec;
u64 result;
clock_gettime(CLOCK_REALTIME, &spec);
result = (spec.tv_sec * UINT64_C(1000000)) + (spec.tv_nsec / UINT64_C(1000));
//LINUX_FN_DEBUG("ts: %" PRIu64, result);
return(result);
}
//
// 4coder_custom.h
//
internal
MEMORY_ALLOCATE_SIG(system_memory_allocate){
// NOTE(allen): This must return the exact base of the vpage.
// We will count on the user to keep track of size themselves.
void *result = mmap(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(result == MAP_FAILED){
perror("mmap");
result = NULL;
}
return(result);
}
internal
MEMORY_SET_PROTECTION_SIG(system_memory_set_protection){
// NOTE(allen):
// There is no such thing as "write only" in windows
// so I just made write = write + read in all cases.
bool32 result = 1;
int protect = 0;
flags = flags & 0x7;
switch (flags){
case 0:
protect = PROT_NONE; break;
case MemProtect_Read:
protect = PROT_READ; break;
case MemProtect_Write:
case MemProtect_Read|MemProtect_Write:
protect = PROT_READ | PROT_WRITE; break;
case MemProtect_Execute:
protect = PROT_EXEC; break;
case MemProtect_Execute|MemProtect_Read:
protect = PROT_READ | PROT_EXEC; break;
// NOTE(inso): some W^X protection things might be unhappy about this one
case MemProtect_Execute|MemProtect_Write:
case MemProtect_Execute|MemProtect_Write|MemProtect_Read:
protect = PROT_READ | PROT_WRITE | PROT_EXEC; break;
}
if(mprotect(ptr, size, protect) == -1){
result = 0;
perror("mprotect");
}
return(result);
}
internal
MEMORY_FREE_SIG(system_memory_free){
// NOTE(allen): This must take the exact base of the vpage.
munmap(ptr, size);
}
internal
FILE_EXISTS_SIG(system_file_exists){
int result = 0;
char buff[PATH_MAX] = {};
if(len + 1 > PATH_MAX){
fputs("system_directory_has_file: path too long\n", stderr);
} else {
memcpy(buff, filename, len);
buff[len] = 0;
struct stat st;
result = stat(buff, &st) == 0 && S_ISREG(st.st_mode);
}
LINUX_FN_DEBUG("%s: %d", buff, result);
return(result);
}
internal
DIRECTORY_CD_SIG(system_directory_cd){
String directory = make_string_cap(dir, *len, capacity);
b32 result = 0;
i32 old_size;
if (rel_path[0] != 0){
if (rel_path[0] == '.' && rel_path[1] == 0){
result = 1;
}
else if (rel_path[0] == '.' && rel_path[1] == '.' && rel_path[2] == 0){
result = remove_last_folder(&directory);
terminate_with_null(&directory);
}
else{
if (directory.size + rel_len + 1 > directory.memory_size){
old_size = directory.size;
append_partial_sc(&directory, rel_path);
append_s_char(&directory, '/');
terminate_with_null(&directory);
struct stat st;
if (stat(directory.str, &st) == 0 && S_ISDIR(st.st_mode)){
result = 1;
}
else{
directory.size = old_size;
}
}
}
}
*len = directory.size;
LINUX_FN_DEBUG("%.*s: %d", directory.size, directory.str, result);
return(result);
}
internal
GET_4ED_PATH_SIG(system_get_4ed_path){
String str = make_string_cap(out, 0, capacity);
return(system_get_binary_path(&str));
}
internal
SHOW_MOUSE_CURSOR_SIG(system_show_mouse_cursor){
linuxvars.hide_cursor = !show;
XDefineCursor(linuxvars.XDisplay, linuxvars.XWindow, show ? None : linuxvars.hidden_cursor);
}
internal
TOGGLE_FULLSCREEN_SIG(system_toggle_fullscreen){
LinuxToggleFullscreen(linuxvars.XDisplay, linuxvars.XWindow);
}
internal
IS_FULLSCREEN_SIG(system_is_fullscreen){
b32 result = 0;
Atom type, *prop;
unsigned long nitems, pad;
int fmt;
int ret = XGetWindowProperty(linuxvars.XDisplay,
linuxvars.XWindow,
linuxvars.atom__NET_WM_STATE,
0, 32, False, XA_ATOM,
&type,
&fmt,
&nitems,
&pad,
(unsigned char**)&prop);
if(ret == Success && prop){
result = *prop == linuxvars.atom__NET_WM_STATE_FULLSCREEN;
XFree((unsigned char*)prop);
}
return result;
}
//
// Clipboard
//
internal
Sys_Post_Clipboard_Sig(system_post_clipboard){
LinuxStringDup(&linuxvars.clipboard_outgoing, str.str, str.size);
XSetSelectionOwner(linuxvars.XDisplay, linuxvars.atom_CLIPBOARD, linuxvars.XWindow, CurrentTime);
}
//
// Coroutine
//
internal Linux_Coroutine*
LinuxAllocCoroutine(){
Linux_Coroutine *result = linuxvars.coroutine_free;
Assert(result != 0);
if(getcontext(&result->ctx) == -1){
perror("getcontext");
}
result->ctx.uc_stack = result->stack;
linuxvars.coroutine_free = result->next;
return(result);
}
internal void
LinuxFreeCoroutine(Linux_Coroutine *data){
data->next = linuxvars.coroutine_free;
linuxvars.coroutine_free = data;
}
internal void
LinuxCoroutineMain(void *arg_){
Linux_Coroutine *c = (Linux_Coroutine*)arg_;
c->coroutine.func(&c->coroutine);
c->done = 1;
LinuxFreeCoroutine(c);
setcontext((ucontext_t*)c->coroutine.yield_handle);
}
internal
Sys_Create_Coroutine_Sig(system_create_coroutine){
Linux_Coroutine *c = LinuxAllocCoroutine();
c->done = 0;
makecontext(&c->ctx, (void (*)())LinuxCoroutineMain, 1, &c->coroutine);
*(ucontext_t**)&c->coroutine.plat_handle = &c->ctx;
c->coroutine.func = func;
return(&c->coroutine);
}
internal
Sys_Launch_Coroutine_Sig(system_launch_coroutine){
Linux_Coroutine *c = (Linux_Coroutine*)coroutine;
ucontext_t* ctx = *(ucontext**)&coroutine->plat_handle;
coroutine->yield_handle = &c->yield_ctx;
coroutine->in = in;
coroutine->out = out;
swapcontext(&c->yield_ctx, ctx);
if (c->done){
LinuxFreeCoroutine(c);
coroutine = 0;
}
return(coroutine);
}
internal
Sys_Resume_Coroutine_Sig(system_resume_coroutine){
Linux_Coroutine *c = (Linux_Coroutine*)coroutine;
void *fiber;
Assert(!c->done);
coroutine->yield_handle = &c->yield_ctx;
coroutine->in = in;
coroutine->out = out;
ucontext *ctx = *(ucontext**)&coroutine->plat_handle;
swapcontext(&c->yield_ctx, ctx);
if (c->done){
LinuxFreeCoroutine(c);
coroutine = 0;
}
return(coroutine);
}
internal
Sys_Yield_Coroutine_Sig(system_yield_coroutine){
swapcontext(*(ucontext_t**)&coroutine->plat_handle, (ucontext*)coroutine->yield_handle);
}
//
// CLI
//
internal
Sys_CLI_Call_Sig(system_cli_call){
LINUX_FN_DEBUG("%s %s", path, script_name);
int pipe_fds[2];
if(pipe(pipe_fds) == -1){
perror("system_cli_call: pipe");
return 0;
}
pid_t child_pid = fork();
if(child_pid == -1){
perror("system_cli_call: fork");
return 0;
}
enum { PIPE_FD_READ, PIPE_FD_WRITE };
// child
if(child_pid == 0){
close(pipe_fds[PIPE_FD_READ]);
dup2(pipe_fds[PIPE_FD_WRITE], STDOUT_FILENO);
dup2(pipe_fds[PIPE_FD_WRITE], STDERR_FILENO);
if(chdir(path) == -1){
perror("system_cli_call: chdir");
exit(1);
};
char* argv[] = { "sh", "-c", script_name, NULL };
if(execv("/bin/sh", argv) == -1){
perror("system_cli_call: execv");
}
exit(1);
} else {
close(pipe_fds[PIPE_FD_WRITE]);
*(pid_t*)&cli_out->proc = child_pid;
*(int*)&cli_out->out_read = pipe_fds[PIPE_FD_READ];
*(int*)&cli_out->out_write = pipe_fds[PIPE_FD_WRITE];
struct epoll_event e = {};
e.events = EPOLLIN | EPOLLET;
e.data.u64 = LINUX_4ED_EVENT_CLI;
epoll_ctl(linuxvars.epoll, EPOLL_CTL_ADD, pipe_fds[PIPE_FD_READ], &e);
}
return 1;
}
internal
Sys_CLI_Begin_Update_Sig(system_cli_begin_update){
// NOTE(inso): I don't think anything needs to be done here.
}
internal
Sys_CLI_Update_Step_Sig(system_cli_update_step){
int pipe_read_fd = *(int*)&cli->out_read;
fd_set fds;
FD_ZERO(&fds);
FD_SET(pipe_read_fd, &fds);
struct timeval tv = {};
size_t space_left = max;
char* ptr = dest;
while(space_left > 0 && select(pipe_read_fd + 1, &fds, NULL, NULL, &tv) == 1){
ssize_t num = read(pipe_read_fd, ptr, space_left);
if(num == -1){
perror("system_cli_update_step: read");
} else if(num == 0){
// NOTE(inso): EOF
break;
} else {
ptr += num;
space_left -= num;
}
}
*amount = (ptr - dest);
return (ptr - dest) > 0;
}
internal
Sys_CLI_End_Update_Sig(system_cli_end_update){
pid_t pid = *(pid_t*)&cli->proc;
b32 close_me = 0;
int status;
if(pid && waitpid(pid, &status, WNOHANG) > 0){
close_me = 1;
cli->exit = WEXITSTATUS(status);
struct epoll_event e = {};
epoll_ctl(linuxvars.epoll, EPOLL_CTL_DEL, *(int*)&cli->out_read, &e);
close(*(int*)&cli->out_read);
close(*(int*)&cli->out_write);
}
return close_me;
}
//
// Threads
//
internal
Sys_Acquire_Lock_Sig(system_acquire_lock){
pthread_mutex_lock(linuxvars.locks + id);
}
internal
Sys_Release_Lock_Sig(system_release_lock){
pthread_mutex_unlock(linuxvars.locks + id);
}
internal void
system_wait_cv(i32 lock_id, i32 cv_id){
pthread_cond_wait(linuxvars.conds + cv_id, linuxvars.locks + lock_id);
}
internal void
system_signal_cv(i32 lock_id, i32 cv_id){
pthread_cond_signal(linuxvars.conds + cv_id);
}
internal void*
JobThreadProc(void* lpParameter){
Thread_Context *thread = (Thread_Context*)lpParameter;
Work_Queue *queue = linuxvars.queues + thread->group_id;
Thread_Group *group = linuxvars.groups + thread->group_id;
i32 thread_index = thread->id - 1;
i32 cancel_lock = group->cancel_lock0 + thread_index;
i32 cancel_cv = group->cancel_cv0 + thread_index;
Thread_Memory *thread_memory = linuxvars.thread_memory + thread_index;
if (thread_memory->size == 0){
i32 new_size = Kbytes(64);
thread_memory->data = LinuxGetMemory(new_size);
thread_memory->size = new_size;
}
for (;;){
u32 read_index = queue->read_position;
u32 write_index = queue->write_position;
if (read_index != write_index){
// NOTE(allen): Previously I was wrapping by the job wrap then
// wrapping by the queue wrap. That was super stupid what was that?
// Now it just wraps by the queue wrap.
u32 next_read_index = (read_index + 1) % QUEUE_WRAP;
u32 safe_read_index =
InterlockedCompareExchange(&queue->read_position,
next_read_index, read_index);
if (safe_read_index == read_index){
Full_Job_Data *full_job = queue->jobs + safe_read_index;
// NOTE(allen): This is interlocked so that it plays nice
// with the cancel job routine, which may try to cancel this job
// at the same time that we try to run it
i32 safe_running_thread =
InterlockedCompareExchange(&full_job->running_thread,
thread->id, THREAD_NOT_ASSIGNED);
if (safe_running_thread == THREAD_NOT_ASSIGNED){
thread->job_id = full_job->id;
thread->running = 1;
full_job->job.callback(&linuxvars.system,
thread, thread_memory, full_job->job.data);
LinuxScheduleStep();
//full_job->running_thread = 0;
thread->running = 0;
system_acquire_lock(cancel_lock);
if (thread->cancel){
thread->cancel = 0;
system_signal_cv(cancel_lock, cancel_cv);
}
system_release_lock(cancel_lock);
}
}
}
else{
sem_wait(LinuxHandleToSem(queue->semaphore));
}
}
}
internal void
initialize_unbounded_queue(Unbounded_Work_Queue *source_queue){
i32 max = 512;
source_queue->jobs = (Full_Job_Data*)system_get_memory(max*sizeof(Full_Job_Data));
source_queue->count = 0;
source_queue->max = max;
source_queue->skip = 0;
}
inline i32
get_work_queue_available_space(i32 write, i32 read){
// NOTE(allen): The only time that queue->write_position == queue->read_position
// is allowed is when the queue is empty. Thus if
// queue->write_position+1 == queue->read_position the available space is zero.
// So these computations both end up leaving one slot unused. The only way I can
// think to easily eliminate this is to have read and write wrap at twice the size
// of the underlying array but modulo their values into the array then if write
// has caught up with read it still will not be equal... but lots of modulos... ehh.
i32 available_space = 0;
if (write >= read){
available_space = QUEUE_WRAP - (write - read) - 1;
}
else{
available_space = (read - write) - 1;
}
return(available_space);
}
#define UNBOUNDED_SKIP_MAX 128
internal void
flush_to_direct_queue(Unbounded_Work_Queue *source_queue, Work_Queue *queue, i32 thread_count){
// NOTE(allen): It is understood that read_position may be changed by other
// threads but it will only make more space in the queue if it is changed.
// Meanwhile write_position should not ever be changed by anything but the
// main thread in this system, so it will not be interlocked.
u32 read_position = queue->read_position;
u32 write_position = queue->write_position;
u32 available_space = get_work_queue_available_space(write_position, read_position);
u32 available_jobs = source_queue->count - source_queue->skip;
u32 writable_count = Min(available_space, available_jobs);
if (writable_count > 0){
u32 count1 = writable_count;
if (count1+write_position > QUEUE_WRAP){
count1 = QUEUE_WRAP - write_position;
}
u32 count2 = writable_count - count1;
Full_Job_Data *job_src1 = source_queue->jobs + source_queue->skip;
Full_Job_Data *job_src2 = job_src1 + count1;
Full_Job_Data *job_dst1 = queue->jobs + write_position;
Full_Job_Data *job_dst2 = queue->jobs;
Assert((job_src1->id % QUEUE_WRAP) == write_position);
memcpy(job_dst1, job_src1, sizeof(Full_Job_Data)*count1);
memcpy(job_dst2, job_src2, sizeof(Full_Job_Data)*count2);
queue->write_position = (write_position + writable_count) % QUEUE_WRAP;
source_queue->skip += writable_count;
if (source_queue->skip == source_queue->count){
source_queue->skip = source_queue->count = 0;
}
else if (source_queue->skip > UNBOUNDED_SKIP_MAX){
u32 left_over = source_queue->count - source_queue->skip;
memmove(source_queue->jobs, source_queue->jobs + source_queue->skip,
sizeof(Full_Job_Data)*left_over);
source_queue->count = left_over;
source_queue->skip = 0;
}
}
i32 semaphore_release_count = writable_count;
if (semaphore_release_count > thread_count){
semaphore_release_count = thread_count;
}
// NOTE(allen): platform dependent portion...
// TODO(allen): pull out the duplicated part once I see
// that this is pretty much the same on linux.
for (i32 i = 0; i < semaphore_release_count; ++i){
sem_post(LinuxHandleToSem(queue->semaphore));
}
}
internal void
flush_thread_group(i32 group_id){
Thread_Group *group = linuxvars.groups + group_id;
Work_Queue *queue = linuxvars.queues + group_id;
Unbounded_Work_Queue *source_queue = &group->queue;
flush_to_direct_queue(source_queue, queue, group->count);
}
// Note(allen): post_job puts the job on the unbounded queue.
// The unbounded queue is entirely managed by the main thread.
// The thread safe queue is bounded in size so the unbounded
// queue is periodically flushed into the direct work queue.
internal
Sys_Post_Job_Sig(system_post_job){
Thread_Group *group = linuxvars.groups + group_id;
Unbounded_Work_Queue *queue = &group->queue;
u32 result = queue->next_job_id++;
while (queue->count >= queue->max){
i32 new_max = queue->max*2;
Full_Job_Data *new_jobs = (Full_Job_Data*)
system_get_memory(new_max*sizeof(Full_Job_Data));
memcpy(new_jobs, queue->jobs, queue->count);
system_free_memory(queue->jobs);
queue->jobs = new_jobs;
queue->max = new_max;
}
Full_Job_Data full_job;
full_job.job = job;
full_job.running_thread = THREAD_NOT_ASSIGNED;
full_job.id = result;
queue->jobs[queue->count++] = full_job;
Work_Queue *direct_queue = linuxvars.queues + group_id;
flush_to_direct_queue(queue, direct_queue, group->count);
return(result);
}
internal
Sys_Cancel_Job_Sig(system_cancel_job){
Thread_Group *group = linuxvars.groups + group_id;
Unbounded_Work_Queue *source_queue = &group->queue;
b32 handled_in_unbounded = false;
if (source_queue->skip < source_queue->count){
Full_Job_Data *first_job = source_queue->jobs + source_queue->skip;
if (first_job->id <= job_id){
u32 index = source_queue->skip + (job_id - first_job->id);
Full_Job_Data *job = source_queue->jobs + index;
job->running_thread = 0;
handled_in_unbounded = true;
}
}
if (!handled_in_unbounded){
Work_Queue *queue = linuxvars.queues + group_id;
Full_Job_Data *job = queue->jobs + (job_id % QUEUE_WRAP);
Assert(job->id == job_id);
u32 thread_id =
InterlockedCompareExchange(&job->running_thread,
0, THREAD_NOT_ASSIGNED);
if (thread_id != THREAD_NOT_ASSIGNED && thread_id != 0){
i32 thread_index = thread_id - 1;
i32 cancel_lock = group->cancel_lock0 + thread_index;
i32 cancel_cv = group->cancel_cv0 + thread_index;
Thread_Context *thread = group->threads + thread_index;
system_acquire_lock(cancel_lock);
thread->cancel = 1;
system_release_lock(FRAME_LOCK);
do{
system_wait_cv(cancel_lock, cancel_cv);
}while (thread->cancel == 1);
system_acquire_lock(FRAME_LOCK);
system_release_lock(cancel_lock);
}
}
}
internal
Sys_Check_Cancel_Sig(system_check_cancel){
b32 result = 0;
Thread_Group *group = linuxvars.groups + thread->group_id;
i32 thread_index = thread->id - 1;
i32 cancel_lock = group->cancel_lock0 + thread_index;
system_acquire_lock(cancel_lock);
if (thread->cancel){
result = 1;
}
system_release_lock(cancel_lock);
return(result);
}
internal
Sys_Grow_Thread_Memory_Sig(system_grow_thread_memory){
void *old_data;
i32 old_size, new_size;
system_acquire_lock(CANCEL_LOCK0 + memory->id - 1);
old_data = memory->data;
old_size = memory->size;
new_size = LargeRoundUp(memory->size*2, Kbytes(4));
memory->data = system_get_memory(new_size);
memory->size = new_size;
if (old_data){
memcpy(memory->data, old_data, old_size);
system_free_memory(old_data);
}
system_release_lock(CANCEL_LOCK0 + memory->id - 1);
}
//
// Debug
//
#if FRED_INTERNAL
internal
INTERNAL_Sys_Sentinel_Sig(internal_sentinel){
return (&linuxvars.internal_bubble);
}
#ifdef OLD_JOB_QUEUE
internal
INTERNAL_Sys_Get_Thread_States_Sig(internal_get_thread_states){
Work_Queue *queue = linuxvars.queues + id;
u32 write = queue->write_position;
u32 read = queue->read_position;
if (write < read) write += QUEUE_WRAP;
*pending = (i32)(write - read);
Thread_Group *group = linuxvars.groups + id;
for (i32 i = 0; i < group->count; ++i){
running[i] = (group->threads[i].running != 0);
}
}
#else
internal
INTERNAL_Sys_Get_Thread_States_Sig(internal_get_thread_states){
Thread_Group *group = linuxvars.groups + id;
Unbounded_Work_Queue *source_queue = &group->queue;
Work_Queue *queue = linuxvars.queues + id;
u32 write = queue->write_position;
u32 read = queue->read_position;
if (write < read) write += QUEUE_WRAP;
*pending = (i32)(write - read) + source_queue->count - source_queue->skip;
for (i32 i = 0; i < group->count; ++i){
running[i] = (group->threads[i].running != 0);
}
}
#endif
internal
INTERNAL_Sys_Debug_Message_Sig(internal_debug_message){
fprintf(stderr, "%s", message);
}
#endif
//
// Linux rendering/font system functions
//
#include "system_shared.cpp"
#include "linux_font.cpp"
internal f32
size_change(i32 dpi_x, i32 dpi_y){
// TODO(allen): We're just hoping dpi_x == dpi_y for now I guess.
f32 size_x = dpi_x / 96.f;
f32 size_y = dpi_y / 96.f;
f32 size_max = Max(size_x, size_y);
return(size_max);
}
internal
Font_Load_Sig(system_draw_font_load){
b32 success = 0;
i32 attempts = 0;
LINUX_FN_DEBUG("%s, %dpt, tab_width: %d", filename, pt_size, tab_width);
if (linuxvars.font_part.base == 0){
linuxvars.font_part = sysshared_scratch_partition(Mbytes(8));
}
i32 oversample = 2;
#if SUPPORT_DPI
pt_size = ROUND32(pt_size * size_change(linuxvars.dpi_x, linuxvars.dpi_y));
#endif
for(; attempts < 3; ++attempts){
#if LINUX_FONTS
success = linux_font_load(&linuxvars.font_part, font_out, filename, pt_size, tab_width,
linuxvars.settings.use_hinting);
#else
success = font_load(
&linuxvars.font_part,
font_out,
filename,
pt_size,
tab_width,
oversample,
store_texture
);
#endif
if(success){
break;
} else {
fprintf(stderr, "draw_font_load failed, %p %d\n", linuxvars.font_part.base, linuxvars.font_part.max);
sysshared_partition_double(&linuxvars.font_part);
}
}
return success;
}
//
// End of system funcs
//
//
// Linux init functions
//
internal b32
LinuxLoadAppCode(String* base_dir){
b32 result = 0;
App_Get_Functions *get_funcs = 0;
if(!sysshared_to_binary_path(base_dir, "4ed_app.so")){
return 0;
}
linuxvars.app_code = dlopen(base_dir->str, RTLD_LAZY);
if (linuxvars.app_code){
get_funcs = (App_Get_Functions*)
dlsym(linuxvars.app_code, "app_get_functions");
} else {
fprintf(stderr, "dlopen failed: %s\n", dlerror());
}
if (get_funcs){
result = 1;
linuxvars.app = get_funcs();
}
return(result);
}
internal void
LinuxLoadSystemCode(){
// files
linuxvars.system.set_file_list = system_set_file_list;
linuxvars.system.get_canonical = system_get_canonical;
linuxvars.system.add_listener = system_add_listener;
linuxvars.system.remove_listener = system_remove_listener;
linuxvars.system.get_file_change = system_get_file_change;
linuxvars.system.load_handle = system_load_handle;
linuxvars.system.load_size = system_load_size;
linuxvars.system.load_file = system_load_file;
linuxvars.system.load_close = system_load_close;
linuxvars.system.save_file = system_save_file;
// time
linuxvars.system.now_time = system_now_time;
// 4coder_custom.h
linuxvars.system.memory_allocate = system_memory_allocate;
linuxvars.system.memory_set_protection = system_memory_set_protection;
linuxvars.system.memory_free = system_memory_free;
linuxvars.system.file_exists = system_file_exists;
linuxvars.system.directory_cd = system_directory_cd;
linuxvars.system.get_4ed_path = system_get_4ed_path;
linuxvars.system.show_mouse_cursor = system_show_mouse_cursor;
linuxvars.system.toggle_fullscreen = system_toggle_fullscreen;
linuxvars.system.is_fullscreen = system_is_fullscreen;
// clipboard
linuxvars.system.post_clipboard = system_post_clipboard;
// coroutine
linuxvars.system.create_coroutine = system_create_coroutine;
linuxvars.system.launch_coroutine = system_launch_coroutine;
linuxvars.system.resume_coroutine = system_resume_coroutine;
linuxvars.system.yield_coroutine = system_yield_coroutine;
// cli
linuxvars.system.cli_call = system_cli_call;
linuxvars.system.cli_begin_update = system_cli_begin_update;
linuxvars.system.cli_update_step = system_cli_update_step;
linuxvars.system.cli_end_update = system_cli_end_update;
// threads
linuxvars.system.post_job = system_post_job;
linuxvars.system.cancel_job = system_cancel_job;
linuxvars.system.check_cancel = system_check_cancel;
linuxvars.system.grow_thread_memory = system_grow_thread_memory;
linuxvars.system.acquire_lock = system_acquire_lock;
linuxvars.system.release_lock = system_release_lock;
// debug
#if FRED_INTERNAL
linuxvars.system.internal_sentinel = internal_sentinel;
linuxvars.system.internal_get_thread_states = internal_get_thread_states;
linuxvars.system.internal_debug_message = internal_debug_message;
#endif
// non-function details
linuxvars.system.slash = '/';
}
internal void
LinuxLoadRenderCode(){
linuxvars.target.push_clip = draw_push_clip;
linuxvars.target.pop_clip = draw_pop_clip;
linuxvars.target.push_piece = draw_push_piece;
linuxvars.target.font_set.font_load = system_draw_font_load;
linuxvars.target.font_set.release_font = draw_release_font;
}
//
// Renderer
//
internal void
LinuxRedrawTarget(){
launch_rendering(&linuxvars.target);
//glFlush();
glXSwapBuffers(linuxvars.XDisplay, linuxvars.XWindow);
}
internal void
LinuxResizeTarget(i32 width, i32 height){
if (width > 0 && height > 0){
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, width, height, 0, -1, 1);
glScissor(0, 0, width, height);
linuxvars.target.width = width;
linuxvars.target.height = height;
}
}
//
// OpenGL init
//
// NOTE(allen): Thanks to Casey for providing the linux OpenGL launcher.
static bool ctxErrorOccurred = false;
internal int
ctxErrorHandler( Display *dpy, XErrorEvent *ev )
{
ctxErrorOccurred = true;
return 0;
}
#if FRED_INTERNAL
static void LinuxGLDebugCallback(
GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar* message,
const void* userParam
){
fprintf(stderr, "GL DEBUG: %s\n", message);
}
#endif
internal GLXContext
InitializeOpenGLContext(Display *XDisplay, Window XWindow, GLXFBConfig &bestFbc, b32 &IsLegacy)
{
IsLegacy = false;
typedef GLXContext (*glXCreateContextAttribsARBProc)(Display*, GLXFBConfig, GLXContext, Bool, const int*);
typedef PFNGLXSWAPINTERVALEXTPROC glXSwapIntervalEXTProc;
typedef PFNGLXSWAPINTERVALMESAPROC glXSwapIntervalMESAProc;
typedef PFNGLXGETSWAPINTERVALMESAPROC glXGetSwapIntervalMESAProc;
typedef PFNGLXSWAPINTERVALSGIPROC glXSwapIntervalSGIProc;
const char *glxExts = glXQueryExtensionsString(XDisplay, DefaultScreen(XDisplay));
#define GLXLOAD(x) x ## Proc x = (x ## Proc) glXGetProcAddressARB( (const GLubyte*) #x);
GLXLOAD(glXCreateContextAttribsARB);
GLXContext ctx = 0;
ctxErrorOccurred = false;
int (*oldHandler)(Display*, XErrorEvent*) = XSetErrorHandler(&ctxErrorHandler);
if (!glXCreateContextAttribsARB)
{
fprintf(stderr, "glXCreateContextAttribsARB() not found, using old-style GLX context\n" );
ctx = glXCreateNewContext( XDisplay, bestFbc, GLX_RGBA_TYPE, 0, True );
}
else
{
int context_attribs[] =
{
GLX_CONTEXT_MAJOR_VERSION_ARB, 4,
GLX_CONTEXT_MINOR_VERSION_ARB, 3,
GLX_CONTEXT_PROFILE_MASK_ARB , GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB,
#if FRED_INTERNAL
GLX_CONTEXT_FLAGS_ARB , GLX_CONTEXT_DEBUG_BIT_ARB,
#endif
None
};
fprintf(stderr, "Creating GL 4.3 context...\n");
ctx = glXCreateContextAttribsARB(XDisplay, bestFbc, 0, True, context_attribs);
XSync( XDisplay, False );
if (!ctxErrorOccurred && ctx)
{
fprintf(stderr, "Created GL 4.3 context.\n" );
}
else
{
ctxErrorOccurred = false;
context_attribs[1] = 3;
context_attribs[3] = 2;
fprintf(stderr, "GL 4.3 unavailable, creating GL 3.2 context...\n" );
ctx = glXCreateContextAttribsARB( XDisplay, bestFbc, 0, True, context_attribs );
XSync(XDisplay, False);
if (!ctxErrorOccurred && ctx)
{
fprintf(stderr, "Created GL 3.2 context.\n" );
}
else
{
context_attribs[1] = 1;
context_attribs[3] = 2;
ctxErrorOccurred = false;
fprintf(stderr, "Failed to create GL 3.2 context, using old-style GLX context\n");
ctx = glXCreateContextAttribsARB(XDisplay, bestFbc, 0, True, context_attribs);
IsLegacy = true;
}
}
}
XSync(XDisplay, False);
XSetErrorHandler(oldHandler);
if (ctxErrorOccurred || !ctx)
{
fprintf(stderr, "Failed to create an OpenGL context\n");
exit(1);
}
b32 Direct;
if (!glXIsDirect(XDisplay, ctx))
{
fprintf(stderr, "Indirect GLX rendering context obtained\n");
Direct = 0;
}
else
{
fprintf(stderr, "Direct GLX rendering context obtained\n");
Direct = 1;
}
fprintf(stderr, "Making context current\n");
glXMakeCurrent( XDisplay, XWindow, ctx );
char *Vendor = (char *)glGetString(GL_VENDOR);
char *Renderer = (char *)glGetString(GL_RENDERER);
char *Version = (char *)glGetString(GL_VERSION);
//TODO(inso): glGetStringi is required in core profile if the GL version is >= 3.0
char *Extensions = (char *)glGetString(GL_EXTENSIONS);
fprintf(stderr, "GL_VENDOR: %s\n", Vendor);
fprintf(stderr, "GL_RENDERER: %s\n", Renderer);
fprintf(stderr, "GL_VERSION: %s\n", Version);
// fprintf(stderr, "GL_EXTENSIONS: %s\n", Extensions);
//NOTE(inso): enable vsync if available. this should probably be optional
if(Direct && strstr(glxExts, "GLX_EXT_swap_control ")){
GLXLOAD(glXSwapIntervalEXT);
if(glXSwapIntervalEXT){
glXSwapIntervalEXT(XDisplay, XWindow, 1);
unsigned int swap_val = 0;
glXQueryDrawable(XDisplay, XWindow, GLX_SWAP_INTERVAL_EXT, &swap_val);
linuxvars.vsync = swap_val == 1;
fprintf(stderr, "VSync enabled? %s.\n", linuxvars.vsync ? "Yes" : "No");
}
} else if(Direct && strstr(glxExts, "GLX_MESA_swap_control ")){
GLXLOAD(glXSwapIntervalMESA);
GLXLOAD(glXGetSwapIntervalMESA);
if(glXSwapIntervalMESA){
glXSwapIntervalMESA(1);
if(glXGetSwapIntervalMESA){
linuxvars.vsync = glXGetSwapIntervalMESA();
fprintf(stderr, "VSync enabled? %s (MESA)\n", linuxvars.vsync ? "Yes" : "No");
} else {
// NOTE(inso): assume it worked?
linuxvars.vsync = 1;
fputs("VSync enabled? possibly (MESA)\n", stderr);
}
}
} else if(Direct && strstr(glxExts, "GLX_SGI_swap_control ")){
GLXLOAD(glXSwapIntervalSGI);
if(glXSwapIntervalSGI){
glXSwapIntervalSGI(1);
//NOTE(inso): The SGI one doesn't seem to have a way to confirm we got it...
linuxvars.vsync = 1;
fputs("VSync enabled? hopefully (SGI)\n", stderr);
}
} else {
fputs("VSync enabled? nope, no suitable extension\n", stderr);
}
#if FRED_INTERNAL
typedef PFNGLDEBUGMESSAGECALLBACKARBPROC glDebugMessageCallbackProc;
GLXLOAD(glDebugMessageCallback);
if(glDebugMessageCallback){
fputs("Enabling GL Debug Callback\n", stderr);
glDebugMessageCallback(&LinuxGLDebugCallback, 0);
glEnable(GL_DEBUG_OUTPUT);
}
#endif
glEnable(GL_TEXTURE_2D);
glEnable(GL_SCISSOR_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
#undef GLXLOAD
return(ctx);
}
internal b32
GLXCanUseFBConfig(Display *XDisplay)
{
b32 Result = false;
int GLXMajor, GLXMinor;
char *XVendor = ServerVendor(XDisplay);
fprintf(stderr, "XWindows vendor: %s\n", XVendor);
if(glXQueryVersion(XDisplay, &GLXMajor, &GLXMinor))
{
fprintf(stderr, "GLX version %d.%d\n", GLXMajor, GLXMinor);
if(((GLXMajor == 1 ) && (GLXMinor >= 3)) || (GLXMajor > 1))
{
Result = true;
}
}
return(Result);
}
typedef struct glx_config_result{
b32 Found;
GLXFBConfig BestConfig;
XVisualInfo BestInfo;
} glx_config_result;
internal glx_config_result
ChooseGLXConfig(Display *XDisplay, int XScreenIndex)
{
glx_config_result Result = {0};
int DesiredAttributes[] =
{
GLX_X_RENDERABLE , True,
GLX_DRAWABLE_TYPE , GLX_WINDOW_BIT,
GLX_RENDER_TYPE , GLX_RGBA_BIT,
GLX_X_VISUAL_TYPE , GLX_TRUE_COLOR,
GLX_RED_SIZE , 8,
GLX_GREEN_SIZE , 8,
GLX_BLUE_SIZE , 8,
GLX_ALPHA_SIZE , 8,
GLX_DEPTH_SIZE , 24,
GLX_STENCIL_SIZE , 8,
GLX_DOUBLEBUFFER , True,
//GLX_SAMPLE_BUFFERS , 1,
//GLX_SAMPLES , 4,
None
};
int ConfigCount = 0;
GLXFBConfig *Configs = glXChooseFBConfig(XDisplay,
XScreenIndex,
DesiredAttributes,
&ConfigCount);
if(Configs && ConfigCount > 0)
{
XVisualInfo* VI = glXGetVisualFromFBConfig(XDisplay, Configs[0]);
if(VI)
{
Result.Found = true;
Result.BestConfig = Configs[0];
Result.BestInfo = *VI;
int id = 0;
glXGetFBConfigAttrib(XDisplay, Result.BestConfig, GLX_FBCONFIG_ID, &id);
fprintf(stderr, "Using FBConfig: %d (0x%x)\n", id, id);
XFree(VI);
}
XFree(Configs);
}
return(Result);
}
//
// X11 input / events init
//
struct Init_Input_Result{
XIM input_method;
XIMStyle best_style;
XIC xic;
};
inline Init_Input_Result
init_input_result_zero(){
Init_Input_Result result={0};
return(result);
}
internal Init_Input_Result
LinuxInputInit(Display *dpy, Window XWindow)
{
Init_Input_Result result = {};
XIMStyles *styles = 0;
XIMStyle style;
unsigned long xim_event_mask = 0;
i32 i;
setlocale(LC_ALL, "");
XSetLocaleModifiers("");
fprintf(stderr, "Supported locale?: %s.\n", XSupportsLocale() ? "Yes" : "No");
// TODO(inso): handle the case where it isn't supported somehow?
result.input_method = XOpenIM(dpy, 0, 0, 0);
if (!result.input_method){
// NOTE(inso): Try falling back to the internal XIM implementation that
// should in theory always exist.
XSetLocaleModifiers("@im=none");
result.input_method = XOpenIM(dpy, 0, 0, 0);
}
if (result.input_method){
if (!XGetIMValues(result.input_method, XNQueryInputStyle, &styles, NULL) && styles){
for (i = 0; i < styles->count_styles; ++i){
style = styles->supported_styles[i];
if (style == (XIMPreeditNothing | XIMStatusNothing)){
result.best_style = style;
break;
}
}
}
if (result.best_style){
XFree(styles);
result.xic = XCreateIC(
result.input_method,
XNInputStyle, result.best_style,
XNClientWindow, XWindow,
XNFocusWindow, XWindow,
NULL
);
if (XGetICValues(result.xic, XNFilterEvents, &xim_event_mask, NULL)){
xim_event_mask = 0;
}
}
else{
result = init_input_result_zero();
fputs("Could not get minimum required input style.\n", stderr);
}
}
else{
result = init_input_result_zero();
fputs("Could not open X Input Method.\n", stderr);
}
XSelectInput(
linuxvars.XDisplay,
linuxvars.XWindow,
ExposureMask |
KeyPressMask | KeyReleaseMask |
ButtonPressMask | ButtonReleaseMask |
EnterWindowMask | LeaveWindowMask |
PointerMotionMask |
FocusChangeMask |
StructureNotifyMask |
MappingNotify |
ExposureMask |
VisibilityChangeMask |
xim_event_mask
);
return(result);
}
//
// Keyboard handling funcs
//
globalvar u8 keycode_lookup_table[255];
internal void
LinuxKeycodeInit(Display* dpy){
// NOTE(inso): This looks a bit dumb, but it's the best way I can think of to do it, since:
// KeySyms are the type representing "virtual" keys, like XK_BackSpace, but they are 32-bit ints.
// KeyCodes are guaranteed to fit in 1 byte (and therefore the keycode_lookup_table) but
// have dynamic numbers assigned by the XServer.
// There is XKeysymToKeycode, but it only returns 1 KeyCode for a KeySym. I have my capslock
// rebound to esc, so there are two KeyCodes for the XK_Escape KeyCode but XKeysymToKeycode only
// gets one of them, hence the need for this crazy lookup which works correctly with rebound keys.
memset(keycode_lookup_table, 0, sizeof(keycode_lookup_table));
struct SymMapping {
KeySym sym;
u8 code;
} sym_table[] = {
{ XK_BackSpace, key_back },
{ XK_Delete, key_del },
{ XK_Up, key_up },
{ XK_Down, key_down },
{ XK_Left, key_left },
{ XK_Right, key_right },
{ XK_Insert, key_insert },
{ XK_Home, key_home },
{ XK_End, key_end },
{ XK_Page_Up, key_page_up },
{ XK_Page_Down, key_page_down },
{ XK_Escape, key_esc },
{ XK_F1, key_f1 },
{ XK_F2, key_f2 },
{ XK_F3, key_f3 },
{ XK_F4, key_f4 },
{ XK_F5, key_f5 },
{ XK_F6, key_f6 },
{ XK_F7, key_f7 },
{ XK_F8, key_f8 },
{ XK_F9, key_f9 },
{ XK_F10, key_f10 },
{ XK_F11, key_f11 },
{ XK_F12, key_f12 },
{ XK_F13, key_f13 },
{ XK_F14, key_f14 },
{ XK_F15, key_f15 },
{ XK_F16, key_f16 },
};
const int table_size = sizeof(sym_table) / sizeof(struct SymMapping);
int key_min, key_max, syms_per_code;
XDisplayKeycodes(dpy, &key_min, &key_max);
int key_count = (key_max - key_min) + 1;
KeySym* syms = XGetKeyboardMapping(
dpy,
key_min,
key_count,
&syms_per_code
);
if(!syms) return;
int key = key_min;
for(int i = 0; i < key_count * syms_per_code; ++i){
for(int j = 0; j < table_size; ++j){
if(sym_table[j].sym == syms[i]){
keycode_lookup_table[key + (i/syms_per_code)] = sym_table[j].code;
break;
}
}
}
XFree(syms);
}
internal void
LinuxPushKey(u8 code, u8 chr, u8 chr_nocaps, b8 (*mods)[MDFR_INDEX_COUNT], b32 is_hold)
{
i32 *count;
Key_Event_Data *data;
if(is_hold){
count = &linuxvars.input.keys.hold_count;
data = linuxvars.input.keys.hold;
} else {
count = &linuxvars.input.keys.press_count;
data = linuxvars.input.keys.press;
}
if(*count < KEY_INPUT_BUFFER_SIZE){
data[*count].keycode = code;
data[*count].character = chr;
data[*count].character_no_caps_lock = chr_nocaps;
memcpy(data[*count].modifiers, *mods, sizeof(*mods));
++(*count);
}
}
//
// Misc utility funcs
//
internal Plat_Handle
LinuxSemToHandle(sem_t* sem){
return *(Plat_Handle*)&sem;
}
internal sem_t*
LinuxHandleToSem(Plat_Handle h){
return *(sem_t**)&h;
}
internal Plat_Handle
LinuxFDToHandle(int fd){
return *(Plat_Handle*)&fd;
}
internal int
LinuxHandleToFD(Plat_Handle h){
return *(int*)&h;
}
internal void
LinuxStringDup(String* str, void* data, size_t size){
if(str->memory_size < size){
if(str->str){
LinuxFreeMemory(str->str);
}
str->memory_size = size;
str->str = (char*)LinuxGetMemory(size);
//TODO(inso): handle alloc failure case
}
str->size = size;
memcpy(str->str, data, size);
}
internal void
LinuxScheduleStep(void)
{
u64 now = system_now_time();
u64 diff = (now - linuxvars.last_step);
if(diff > (u64)frame_useconds){
u64 ev = 1;
write(linuxvars.step_event_fd, &ev, sizeof(ev));
} else {
struct itimerspec its = {};
timerfd_gettime(linuxvars.step_timer_fd, &its);
if(its.it_value.tv_sec == 0 && its.it_value.tv_nsec == 0){
its.it_value.tv_nsec = (frame_useconds - diff) * 1000UL;
timerfd_settime(linuxvars.step_timer_fd, 0, &its, NULL);
}
}
}
//
// X11 utility funcs
//
internal void
LinuxSetWMState(Display* d, Window w, Atom one, Atom two, int mode)
{
//NOTE(inso): this will only work after it is mapped
enum { STATE_REMOVE, STATE_ADD, STATE_TOGGLE };
XEvent e = {};
e.xany.type = ClientMessage;
e.xclient.message_type = linuxvars.atom__NET_WM_STATE;
e.xclient.format = 32;
e.xclient.window = w;
e.xclient.data.l[0] = mode;
e.xclient.data.l[1] = one;
e.xclient.data.l[2] = two;
e.xclient.data.l[3] = 1L;
XSendEvent(
d,
RootWindow(d, 0),
0,
SubstructureNotifyMask | SubstructureRedirectMask,
&e
);
}
internal void
LinuxMaximizeWindow(Display* d, Window w, b32 maximize)
{
LinuxSetWMState(d,
w,
linuxvars.atom__NET_WM_STATE_MAXIMIZED_HORZ,
linuxvars.atom__NET_WM_STATE_MAXIMIZED_VERT,
maximize != 0);
}
internal void
LinuxToggleFullscreen(Display* d, Window w)
{
LinuxSetWMState(d, w, linuxvars.atom__NET_WM_STATE_FULLSCREEN, 0, 2);
}
#include "linux_icon.h"
internal void
LinuxSetIcon(Display* d, Window w)
{
Atom WM_ICON = XInternAtom(d, "_NET_WM_ICON", False);
XChangeProperty(
d,
w,
WM_ICON,
XA_CARDINAL,
32,
PropModeReplace,
(unsigned char*)linux_icon,
sizeof(linux_icon) / sizeof(long)
);
}
internal void
LinuxX11ConnectionWatch(Display* dpy, XPointer cdata, int fd, Bool opening, XPointer* wdata)
{
struct epoll_event e = {};
e.events = EPOLLIN | EPOLLET;
e.data.u64 = LINUX_4ED_EVENT_X11_INTERNAL | fd;
int op = opening ? EPOLL_CTL_ADD : EPOLL_CTL_DEL;
epoll_ctl(linuxvars.epoll, op, fd, &e);
}
// NOTE(inso): this was a quick hack, might need some cleanup.
internal void
LinuxFatalErrorMsg(const char* msg)
{
fprintf(stderr, "Fatal Error: %s\n", msg);
Display *dpy = XOpenDisplay(0);
if(!dpy){
exit(1);
}
int win_w = 450;
int win_h = 150 + (strlen(msg) / 40) * 24;
Window w = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, win_w, win_h, 0, 0, 0x2EA44F);
XStoreName(dpy, w, "4coder Error");
XSizeHints* sh = XAllocSizeHints();
sh->flags = PMinSize;
sh->min_width = win_w;
sh->min_height = win_h;
XSetWMNormalHints(dpy, w, sh);
Atom type = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False);
XChangeProperty(dpy, w,
XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False),
XA_ATOM,
32,
PropModeReplace,
(unsigned char*) &type,
1);
Atom WM_DELETE_WINDOW = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
XSetWMProtocols(dpy, w, &WM_DELETE_WINDOW, 1);
XMapRaised(dpy, w);
XSync(dpy, False);
XSelectInput(dpy, w, ExposureMask | StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask);
XFontStruct* font = XLoadQueryFont(dpy, "-*-fixed-*-*-*-*-*-140-*-*-*-*-iso8859-1");
if(!font){
exit(1);
}
XGCValues gcv;
gcv.foreground = WhitePixel(dpy, 0);
gcv.background = 0x2EA44F;
gcv.line_width = 2;
gcv.font = font->fid;
GC gc = XCreateGC(dpy, w, GCForeground | GCBackground | GCFont | GCLineWidth, &gcv);
gcv.foreground = BlackPixel(dpy, 0);
GC gc2 = XCreateGC(dpy, w, GCForeground | GCBackground | GCFont | GCLineWidth, &gcv);
int button_trigger = 0;
int button_hi = 0;
XEvent ev;
while(1){
XNextEvent(dpy, &ev);
int redraw = 0;
if(ev.type == Expose) redraw = 1;
if(ev.type == ConfigureNotify){
redraw = 1;
win_w = ev.xconfigure.width;
win_h = ev.xconfigure.height;
}
XRectangle button_rect = { win_w/2-40, win_h*0.8f, 80, 20 };
if(ev.type == MotionNotify){
int new_hi = (ev.xmotion.x > button_rect.x &&
ev.xmotion.y > button_rect.y &&
ev.xmotion.x < button_rect.x + button_rect.width &&
ev.xmotion.y < button_rect.y + button_rect.height);
if(new_hi != button_hi){
button_hi = new_hi;
redraw = 1;
}
}
if(ev.type == ButtonPress && ev.xbutton.button == Button1){
if(button_hi) button_trigger = 1;
redraw = 1;
}
if(ev.type == ButtonRelease && ev.xbutton.button == Button1){
if(button_trigger){
if(button_hi){
exit(1);
} else {
button_trigger = 0;
}
}
redraw = 1;
}
if(ev.type == ClientMessage && ev.xclient.window == w && (Atom)ev.xclient.data.l[0] == WM_DELETE_WINDOW){
exit(1);
}
if(redraw){
XClearWindow(dpy, w);
const char* line_start = msg;
const char* last_space = NULL;
int y = 30;
{
const char title[] = "4coder - Fatal Error";
int width = XTextWidth(font, title, sizeof(title)-1);
int x = (win_w/2) - (width/2);
XDrawString(dpy, w, gc2, x+2, y+2, title, sizeof(title)-1);
XDrawString(dpy, w, gc, x, y, title, sizeof(title)-1);
}
y += 36;
int width = XTextWidth(font, "x", 1) * 40;
int x = (win_w/2) - (width/2);
for(const char* p = line_start; *p; ++p){
if(*p == ' ') last_space = p;
if(p - line_start > 40){
if(!last_space) last_space = p;
XDrawString(dpy, w, gc2, x+2, y+2, line_start, last_space - line_start);
XDrawString(dpy, w, gc, x, y, line_start, last_space - line_start);
line_start = *last_space == ' ' ? last_space + 1 : p;
last_space = NULL;
y += 18;
}
}
XDrawString(dpy, w, gc2, x+2, y+2, line_start, strlen(line_start));
XDrawString(dpy, w, gc, x, y, line_start, strlen(line_start));
XDrawRectangles(dpy, w, gc, &button_rect, 1);
if(button_hi || button_trigger){
XDrawRectangle(dpy, w, gc2, button_rect.x+1, button_rect.y+1, button_rect.width-2, button_rect.height-2);
}
XDrawString(dpy, w, gc2, button_rect.x + 22, button_rect.y + 17, "Drat!", 5);
XDrawString(dpy, w, gc, button_rect.x + 20, button_rect.y + 15, "Drat!", 5);
}
}
}
internal int
LinuxGetXSettingsDPI(Display* dpy, int screen)
{
struct XSettingHeader {
u8 type;
u8 pad0;
u16 name_len;
char name[0];
};
struct XSettings {
u8 byte_order;
u8 pad[3];
u32 serial;
u32 num_settings;
};
enum { XSettingsTypeInt, XSettingsTypeString, XSettingsTypeColor };
int dpi = -1;
unsigned char* prop = NULL;
char sel_buffer[64];
struct XSettings* xs;
const char* p;
snprintf(sel_buffer, sizeof(sel_buffer), "_XSETTINGS_S%d", screen);
Atom XSET_SEL = XInternAtom(dpy, sel_buffer, True);
Atom XSET_SET = XInternAtom(dpy, "_XSETTINGS_SETTINGS", True);
if(XSET_SEL == None || XSET_SET == None){
fputs("XSETTINGS unavailable.\n", stderr);
return dpi;
}
Window xset_win = XGetSelectionOwner(dpy, XSET_SEL);
if(xset_win == None){
// TODO(inso): listen for the ClientMessage about it becoming available?
// there's not much point atm if DPI scaling is only done at startup
goto out;
}
{
Atom type;
int fmt;
unsigned long pad, num;
if(XGetWindowProperty(dpy, xset_win, XSET_SET, 0, 1024, False, XSET_SET, &type, &fmt, &num, &pad, &prop) != Success){
fputs("XSETTINGS: GetWindowProperty failed.\n", stderr);
goto out;
}
if(fmt != 8){
fputs("XSETTINGS: Wrong format.\n", stderr);
goto out;
}
}
xs = (struct XSettings*)prop;
p = (char*)(xs + 1);
if(xs->byte_order != 0){
fputs("FIXME: XSETTINGS not host byte order?\n", stderr);
goto out;
}
for(int i = 0; i < xs->num_settings; ++i){
struct XSettingHeader* h = (struct XSettingHeader*)p;
// const char* strs[] = { "Int", "String", "Color" };
// printf("%s:\t\"%.*s\"\n", strs[h->type], h->name_len, h->name);
p += sizeof(struct XSettingHeader);
p += h->name_len;
p += ((4 - (h->name_len & 3)) & 3);
p += 4; // serial
switch(h->type){
case XSettingsTypeInt: {
if(strncmp(h->name, "Xft/DPI", h->name_len) == 0){
dpi = *(i32*)p;
if(dpi != -1) dpi /= 1024;
}
p += 4;
} break;
case XSettingsTypeString: {
u32 len = *(u32*)p;
p += 4;
p += len;
p += ((4 - (len & 3)) & 3);
} break;
case XSettingsTypeColor: {
p += 8;
} break;
default: {
fputs("XSETTINGS: Got invalid type...\n", stderr);
goto out;
} break;
}
}
out:
if(prop){
XFree(prop);
}
return dpi;
}
//
// X11 window init
//
internal b32
LinuxX11WindowInit(int argc, char** argv, int* WinWidth, int* WinHeight)
{
// NOTE(allen): Here begins the linux screen setup stuff.
// Behold the true nature of this wonderful OS:
// (thanks again to Casey for providing this stuff)
#define BASE_W 800
#define BASE_H 600
if (linuxvars.settings.set_window_size){
*WinWidth = linuxvars.settings.window_w;
*WinHeight = linuxvars.settings.window_h;
} else {
*WinWidth = BASE_W * size_change(linuxvars.dpi_x, linuxvars.dpi_y);
*WinHeight = BASE_H * size_change(linuxvars.dpi_x, linuxvars.dpi_y);
}
if (!GLXCanUseFBConfig(linuxvars.XDisplay)){
LinuxFatalErrorMsg("Your XServer's GLX version is too old. GLX 1.3+ is required.");
return false;
}
glx_config_result Config = ChooseGLXConfig(linuxvars.XDisplay, DefaultScreen(linuxvars.XDisplay));
if (!Config.Found){
LinuxFatalErrorMsg("Could not get a matching GLX FBConfig. Check your OpenGL drivers are installed correctly.");
return false;
}
XSetWindowAttributes swa = {};
swa.backing_store = WhenMapped;
swa.event_mask = StructureNotifyMask;
swa.bit_gravity = NorthWestGravity;
swa.colormap = XCreateColormap(linuxvars.XDisplay,
RootWindow(linuxvars.XDisplay, Config.BestInfo.screen),
Config.BestInfo.visual, AllocNone);
linuxvars.XWindow =
XCreateWindow(linuxvars.XDisplay,
RootWindow(linuxvars.XDisplay, Config.BestInfo.screen),
0, 0, *WinWidth, *WinHeight,
0, Config.BestInfo.depth, InputOutput,
Config.BestInfo.visual,
CWBackingStore|CWBitGravity|CWBackPixel|CWBorderPixel|CWColormap|CWEventMask, &swa);
if (!linuxvars.XWindow){
LinuxFatalErrorMsg("XCreateWindow failed. Make sure your display is set up correctly.");
return false;
}
//NOTE(inso): Set the window's type to normal
XChangeProperty(
linuxvars.XDisplay,
linuxvars.XWindow,
linuxvars.atom__NET_WM_WINDOW_TYPE,
XA_ATOM,
32,
PropModeReplace,
(unsigned char*)&linuxvars.atom__NET_WM_WINDOW_TYPE_NORMAL,
1
);
//NOTE(inso): window managers want the PID as a window property for some reason.
pid_t pid = getpid();
XChangeProperty(
linuxvars.XDisplay,
linuxvars.XWindow,
linuxvars.atom__NET_WM_PID,
XA_CARDINAL,
32,
PropModeReplace,
(unsigned char*)&pid,
1
);
#define WINDOW_NAME "4coder 4linux: " VERSION
//NOTE(inso): set wm properties
XStoreName(linuxvars.XDisplay, linuxvars.XWindow, WINDOW_NAME);
XSizeHints *sz_hints = XAllocSizeHints();
XWMHints *wm_hints = XAllocWMHints();
XClassHint *cl_hints = XAllocClassHint();
sz_hints->flags = PMinSize | PMaxSize | PWinGravity;
sz_hints->min_width = 50;
sz_hints->min_height = 50;
sz_hints->max_width = sz_hints->max_height = (1UL << 16UL);
/* NOTE(inso): fluxbox thinks this is minimum, so don't set it
sz_hints->base_width = BASE_W;
sz_hints->base_height = BASE_H;
*/
sz_hints->win_gravity = NorthWestGravity;
if (linuxvars.settings.set_window_pos){
sz_hints->flags |= USPosition;
sz_hints->x = linuxvars.settings.window_x;
sz_hints->y = linuxvars.settings.window_y;
}
wm_hints->flags |= InputHint | StateHint;
wm_hints->input = True;
wm_hints->initial_state = NormalState;
cl_hints->res_name = "4coder";
cl_hints->res_class = "4coder";
char* win_name_list[] = { WINDOW_NAME };
XTextProperty win_name;
XStringListToTextProperty(win_name_list, 1, &win_name);
XSetWMProperties(
linuxvars.XDisplay,
linuxvars.XWindow,
&win_name, NULL,
argv, argc,
sz_hints, wm_hints, cl_hints
);
XFree(win_name.value);
XFree(sz_hints);
XFree(wm_hints);
XFree(cl_hints);
LinuxSetIcon(linuxvars.XDisplay, linuxvars.XWindow);
//NOTE(inso): make the window visible
XMapWindow(linuxvars.XDisplay, linuxvars.XWindow);
b32 IsLegacy = false;
GLXContext GLContext =
InitializeOpenGLContext(linuxvars.XDisplay, linuxvars.XWindow, Config.BestConfig, IsLegacy);
XRaiseWindow(linuxvars.XDisplay, linuxvars.XWindow);
if (linuxvars.settings.set_window_pos){
XMoveWindow(
linuxvars.XDisplay,
linuxvars.XWindow,
linuxvars.settings.window_x,
linuxvars.settings.window_y
);
}
if (linuxvars.settings.maximize_window){
LinuxMaximizeWindow(linuxvars.XDisplay, linuxvars.XWindow, 1);
}
else if(linuxvars.settings.fullscreen_window){
LinuxToggleFullscreen(linuxvars.XDisplay, linuxvars.XWindow);
}
XSync(linuxvars.XDisplay, False);
XWindowAttributes WinAttribs;
if (XGetWindowAttributes(linuxvars.XDisplay, linuxvars.XWindow, &WinAttribs))
{
*WinWidth = WinAttribs.width;
*WinHeight = WinAttribs.height;
}
Atom wm_protos[] = {
linuxvars.atom_WM_DELETE_WINDOW,
linuxvars.atom__NET_WM_PING
};
XSetWMProtocols(linuxvars.XDisplay, linuxvars.XWindow, wm_protos, 2);
}
internal void
LinuxHandleX11Events(void)
{
static XEvent PrevEvent = {};
b32 should_step = 0;
while(XPending(linuxvars.XDisplay))
{
XEvent Event;
XNextEvent(linuxvars.XDisplay, &Event);
if (XFilterEvent(&Event, None) == True){
continue;
}
switch (Event.type){
case KeyPress: {
should_step = 1;
b32 is_hold = (PrevEvent.type == KeyRelease &&
PrevEvent.xkey.time == Event.xkey.time &&
PrevEvent.xkey.keycode == Event.xkey.keycode);
b8 mods[MDFR_INDEX_COUNT] = {};
mods[MDFR_HOLD_INDEX] = is_hold;
if(Event.xkey.state & ShiftMask) mods[MDFR_SHIFT_INDEX] = 1;
if(Event.xkey.state & ControlMask) mods[MDFR_CONTROL_INDEX] = 1;
if(Event.xkey.state & LockMask) mods[MDFR_CAPS_INDEX] = 1;
if(Event.xkey.state & Mod1Mask) mods[MDFR_ALT_INDEX] = 1;
Event.xkey.state &= ~(ControlMask);
Status status;
KeySym keysym = NoSymbol;
char buff[32] = {};
Xutf8LookupString(
linuxvars.input_context,
&Event.xkey,
buff,
sizeof(buff) - 1,
&keysym,
&status
);
if(status == XBufferOverflow){
//TODO(inso): handle properly
Xutf8ResetIC(linuxvars.input_context);
XSetICFocus(linuxvars.input_context);
fputs("FIXME: XBufferOverflow from LookupString.\n", stderr);
}
u8 key = *buff, key_no_caps = key;
if(mods[MDFR_CAPS_INDEX] && status == XLookupBoth && Event.xkey.keycode){
char buff_no_caps[32] = {};
Event.xkey.state &= ~(LockMask);
XLookupString(
&Event.xkey,
buff_no_caps,
sizeof(buff_no_caps) - 1,
NULL,
NULL
);
if(*buff_no_caps){
key_no_caps = *buff_no_caps;
}
}
if(key == '\r') key = '\n';
if(key_no_caps == '\r') key_no_caps = '\n';
// don't push modifiers
if(keysym >= XK_Shift_L && keysym <= XK_Hyper_R){
break;
}
if(keysym == XK_ISO_Left_Tab){
key = key_no_caps = '\t';
mods[MDFR_SHIFT_INDEX] = 1;
}
u8 special_key = keycode_lookup_table[(u8)Event.xkey.keycode];
if(special_key){
LinuxPushKey(special_key, 0, 0, &mods, is_hold);
} else if(key < 128){
LinuxPushKey(key, key, key_no_caps, &mods, is_hold);
} else {
LinuxPushKey(0, 0, 0, &mods, is_hold);
}
}break;
case KeyRelease: {
should_step = 1;
}break;
case MotionNotify: {
should_step = 1;
linuxvars.input.mouse.x = Event.xmotion.x;
linuxvars.input.mouse.y = Event.xmotion.y;
}break;
case ButtonPress: {
should_step = 1;
switch(Event.xbutton.button){
case Button1: {
linuxvars.input.mouse.press_l = 1;
linuxvars.input.mouse.l = 1;
} break;
case Button3: {
linuxvars.input.mouse.press_r = 1;
linuxvars.input.mouse.r = 1;
} break;
//NOTE(inso): scroll up
case Button4: {
linuxvars.input.mouse.wheel = 1;
}break;
//NOTE(inso): scroll down
case Button5: {
linuxvars.input.mouse.wheel = -1;
}break;
}
}break;
case ButtonRelease: {
should_step = 1;
switch(Event.xbutton.button){
case Button1: {
linuxvars.input.mouse.release_l = 1;
linuxvars.input.mouse.l = 0;
} break;
case Button3: {
linuxvars.input.mouse.release_r = 1;
linuxvars.input.mouse.r = 0;
} break;
}
}break;
case EnterNotify: {
should_step = 1;
linuxvars.input.mouse.out_of_window = 0;
}break;
case LeaveNotify: {
should_step = 1;
linuxvars.input.mouse.out_of_window = 1;
}break;
case FocusIn:
case FocusOut: {
should_step = 1;
linuxvars.input.mouse.l = 0;
linuxvars.input.mouse.r = 0;
}break;
case ConfigureNotify: {
should_step = 1;
i32 w = Event.xconfigure.width, h = Event.xconfigure.height;
if(w != linuxvars.target.width || h != linuxvars.target.height){
LinuxResizeTarget(Event.xconfigure.width, Event.xconfigure.height);
}
}break;
case MappingNotify: {
if(Event.xmapping.request == MappingModifier || Event.xmapping.request == MappingKeyboard){
XRefreshKeyboardMapping(&Event.xmapping);
LinuxKeycodeInit(linuxvars.XDisplay);
}
}break;
case ClientMessage: {
if ((Atom)Event.xclient.data.l[0] == linuxvars.atom_WM_DELETE_WINDOW) {
should_step = 1;
linuxvars.keep_running = 0;
}
else if ((Atom)Event.xclient.data.l[0] == linuxvars.atom__NET_WM_PING) {
Event.xclient.window = DefaultRootWindow(linuxvars.XDisplay);
XSendEvent(
linuxvars.XDisplay,
Event.xclient.window,
False,
SubstructureRedirectMask | SubstructureNotifyMask,
&Event
);
}
}break;
// NOTE(inso): Someone wants us to give them the clipboard data.
case SelectionRequest: {
XSelectionRequestEvent request = Event.xselectionrequest;
XSelectionEvent response = {};
response.type = SelectionNotify;
response.requestor = request.requestor;
response.selection = request.selection;
response.target = request.target;
response.time = request.time;
response.property = None;
if (
linuxvars.clipboard_outgoing.size &&
request.selection == linuxvars.atom_CLIPBOARD &&
request.property != None &&
request.display &&
request.requestor
){
Atom atoms[] = {
XA_STRING,
linuxvars.atom_UTF8_STRING
};
if(request.target == linuxvars.atom_TARGETS){
XChangeProperty(
request.display,
request.requestor,
request.property,
XA_ATOM,
32,
PropModeReplace,
(u8*)atoms,
ArrayCount(atoms)
);
response.property = request.property;
} else {
b32 found = false;
for(int i = 0; i < ArrayCount(atoms); ++i){
if(request.target == atoms[i]){
found = true;
break;
}
}
if(found){
XChangeProperty(
request.display,
request.requestor,
request.property,
request.target,
8,
PropModeReplace,
(u8*)linuxvars.clipboard_outgoing.str,
linuxvars.clipboard_outgoing.size
);
response.property = request.property;
}
}
}
XSendEvent(request.display, request.requestor, True, 0, (XEvent*)&response);
}break;
// NOTE(inso): Another program is now the clipboard owner.
case SelectionClear: {
if(Event.xselectionclear.selection == linuxvars.atom_CLIPBOARD){
linuxvars.clipboard_outgoing.size = 0;
}
}break;
// NOTE(inso): A program is giving us the clipboard data we asked for.
case SelectionNotify: {
XSelectionEvent* e = (XSelectionEvent*)&Event;
if(
e->selection == linuxvars.atom_CLIPBOARD &&
e->target == linuxvars.atom_UTF8_STRING &&
e->property != None
){
Atom type;
int fmt;
unsigned long nitems, bytes_left;
u8 *data;
int result = XGetWindowProperty(
linuxvars.XDisplay,
linuxvars.XWindow,
linuxvars.atom_CLIPBOARD,
0L,
LINUX_MAX_PASTE_CHARS/4L,
False,
linuxvars.atom_UTF8_STRING,
&type,
&fmt,
&nitems,
&bytes_left,
&data
);
if(result == Success && fmt == 8){
LinuxStringDup(&linuxvars.clipboard_contents, data, nitems);
should_step = 1;
linuxvars.new_clipboard = 1;
XFree(data);
XDeleteProperty(linuxvars.XDisplay, linuxvars.XWindow, linuxvars.atom_CLIPBOARD);
}
}
}break;
case Expose:
case VisibilityNotify: {
should_step = 1;
}break;
default: {
if(Event.type == linuxvars.xfixes_selection_event){
XFixesSelectionNotifyEvent* sne = (XFixesSelectionNotifyEvent*)&Event;
if(sne->subtype == XFixesSelectionNotify && sne->owner != linuxvars.XWindow){
XConvertSelection(
linuxvars.XDisplay,
linuxvars.atom_CLIPBOARD,
linuxvars.atom_UTF8_STRING,
linuxvars.atom_CLIPBOARD,
linuxvars.XWindow,
CurrentTime
);
}
}
}break;
}
PrevEvent = Event;
}
if(should_step){
LinuxScheduleStep();
}
}
//
// Entry point
//
int
main(int argc, char **argv)
{
//
// System & Memory init
//
#if FRED_INTERNAL
linuxvars.internal_bubble.next = &linuxvars.internal_bubble;
linuxvars.internal_bubble.prev = &linuxvars.internal_bubble;
linuxvars.internal_bubble.flags = 0;
pthread_mutex_init(&linuxvars.DEBUG_sysmem_lock, 0);
#endif
char base_dir_mem[PATH_MAX];
String base_dir = make_fixed_width_string(base_dir_mem);
if (!LinuxLoadAppCode(&base_dir)){
LinuxFatalErrorMsg("Could not load '4ed_app.so'. This file should be in the same directory as the main '4ed' executable.");
return 99;
}
LinuxLoadSystemCode();
LinuxLoadRenderCode();
memory_vars.vars_memory_size = Mbytes(2);
memory_vars.vars_memory = system_get_memory(memory_vars.vars_memory_size);
memory_vars.target_memory_size = Mbytes(512);
memory_vars.target_memory = system_get_memory(memory_vars.target_memory_size);
memory_vars.user_memory_size = Mbytes(2);
memory_vars.user_memory = system_get_memory(memory_vars.user_memory_size);
linuxvars.target.max = Mbytes(1);
linuxvars.target.push_buffer = (char*)system_get_memory(linuxvars.target.max);
if(memory_vars.vars_memory == NULL || memory_vars.target_memory == NULL || memory_vars.user_memory == NULL || linuxvars.target.push_buffer == NULL){
LinuxFatalErrorMsg("Could not allocate sufficient memory. Please make sure you have atleast 512Mb of RAM free. (This requirement will be relaxed in the future).");
exit(1);
}
init_shared_vars();
//
// Read command line
//
char* cwd = get_current_dir_name();
if(!cwd){
char buf[1024];
snprintf(buf, sizeof(buf), "Call to get_current_dir_name failed: %s", strerror(errno));
LinuxFatalErrorMsg(buf);
return 1;
}
String current_directory = make_string_slowly(cwd);
Command_Line_Parameters clparams;
clparams.argv = argv;
clparams.argc = argc;
char **files;
i32 *file_count;
i32 output_size;
output_size =
linuxvars.app.read_command_line(&linuxvars.system,
&memory_vars,
current_directory,
&linuxvars.settings,
&files, &file_count,
clparams);
if (output_size > 0){
// TODO(allen): crt free version
fprintf(stdout, "%.*s", output_size, (char*)memory_vars.target_memory);
}
if (output_size != 0){
LinuxFatalErrorMsg("Error reading command-line arguments.");
return 1;
}
sysshared_filter_real_files(files, file_count);
//
// Custom layer linkage
//
#ifdef FRED_SUPER
char *custom_file_default = "4coder_custom.so";
sysshared_to_binary_path(&base_dir, custom_file_default);
custom_file_default = base_dir.str;
char *custom_file;
if (linuxvars.settings.custom_dll){
custom_file = linuxvars.settings.custom_dll;
} else {
custom_file = custom_file_default;
}
linuxvars.custom = dlopen(custom_file, RTLD_LAZY);
if (!linuxvars.custom && custom_file != custom_file_default){
if (!linuxvars.settings.custom_dll_is_strict){
linuxvars.custom = dlopen(custom_file_default, RTLD_LAZY);
}
}
if (linuxvars.custom){
linuxvars.custom_api.get_alpha_4coder_version = (_Get_Version_Function*)
dlsym(linuxvars.custom, "get_alpha_4coder_version");
if (linuxvars.custom_api.get_alpha_4coder_version == 0 ||
linuxvars.custom_api.get_alpha_4coder_version(MAJOR, MINOR, PATCH) == 0){
LinuxFatalErrorMsg("Failed to load '4coder_custom.so': Version mismatch. Try rebuilding it with 'buildsuper.sh'.");
exit(1);
}
else{
linuxvars.custom_api.get_bindings = (Get_Binding_Data_Function*)
dlsym(linuxvars.custom, "get_bindings");
linuxvars.custom_api.view_routine = (View_Routine_Function*)
dlsym(linuxvars.custom, "view_routine");
if (linuxvars.custom_api.get_bindings == 0){
LinuxFatalErrorMsg("Failed to load '4coder_custom.so': "
"It does not export the required 'get_bindings' function. "
"Try rebuilding it with 'buildsuper.sh'.");
exit(1);
}
else{
fprintf(stderr, "Successfully loaded 4coder_custom.so\n");
}
}
} else {
char buf[4096];
const char* error = dlerror();
snprintf(buf, sizeof(buf), "Error loading custom: %s. "
"Make sure this file is in the same directory as the main '4ed' executable.",
error ? error : "'4coder_custom.so' missing");
LinuxFatalErrorMsg(buf);
exit(1);
}
#else
linuxvars.custom_api.get_bindings = get_bindings;
#endif
linuxvars.custom_api.view_routine = 0;
#if 0
if (linuxvars.custom_api.view_routine == 0){
linuxvars.custom_api.view_routine = view_routine;
}
#endif
//
// Coroutine / Thread / Semaphore / Mutex init
//
linuxvars.coroutine_free = linuxvars.coroutine_data;
for (i32 i = 0; i+1 < ArrayCount(linuxvars.coroutine_data); ++i){
linuxvars.coroutine_data[i].next = linuxvars.coroutine_data + i + 1;
}
const size_t stack_size = Mbytes(2);
for (i32 i = 0; i < ArrayCount(linuxvars.coroutine_data); ++i){
linuxvars.coroutine_data[i].stack.ss_size = stack_size;
linuxvars.coroutine_data[i].stack.ss_sp = system_get_memory(stack_size);
}
Thread_Context background[4] = {};
linuxvars.groups[BACKGROUND_THREADS].threads = background;
linuxvars.groups[BACKGROUND_THREADS].count = ArrayCount(background);
linuxvars.groups[BACKGROUND_THREADS].cancel_lock0 = CANCEL_LOCK0;
linuxvars.groups[BACKGROUND_THREADS].cancel_cv0 = 0;
Thread_Memory thread_memory[ArrayCount(background)];
linuxvars.thread_memory = thread_memory;
sem_init(&linuxvars.thread_semaphore, 0, 0);
linuxvars.queues[BACKGROUND_THREADS].semaphore = LinuxSemToHandle(&linuxvars.thread_semaphore);
for(i32 i = 0; i < linuxvars.groups[BACKGROUND_THREADS].count; ++i){
Thread_Context *thread = linuxvars.groups[BACKGROUND_THREADS].threads + i;
thread->id = i + 1;
thread->group_id = BACKGROUND_THREADS;
Thread_Memory *memory = linuxvars.thread_memory + i;
*memory = thread_memory_zero();
memory->id = thread->id;
thread->queue = &linuxvars.queues[BACKGROUND_THREADS];
pthread_create(&thread->handle, NULL, &JobThreadProc, thread);
}
initialize_unbounded_queue(&linuxvars.groups[BACKGROUND_THREADS].queue);
for(i32 i = 0; i < LOCK_COUNT; ++i){
pthread_mutex_init(linuxvars.locks + i, NULL);
}
for(i32 i = 0; i < ArrayCount(linuxvars.conds); ++i){
pthread_cond_init(linuxvars.conds + i, NULL);
}
//
// X11 init
//
linuxvars.XDisplay = XOpenDisplay(0);
if(!linuxvars.XDisplay){
// NOTE(inso): probably not worth trying the popup in this case...
fprintf(stderr, "Can't open display!\n");
return 1;
}
#define LOAD_ATOM(x) linuxvars.atom_##x = XInternAtom(linuxvars.XDisplay, #x, False);
LOAD_ATOM(TARGETS);
LOAD_ATOM(CLIPBOARD);
LOAD_ATOM(UTF8_STRING);
LOAD_ATOM(_NET_WM_STATE);
LOAD_ATOM(_NET_WM_STATE_MAXIMIZED_HORZ);
LOAD_ATOM(_NET_WM_STATE_MAXIMIZED_VERT);
LOAD_ATOM(_NET_WM_STATE_FULLSCREEN);
LOAD_ATOM(_NET_WM_PING);
LOAD_ATOM(_NET_WM_WINDOW_TYPE);
LOAD_ATOM(_NET_WM_WINDOW_TYPE_NORMAL);
LOAD_ATOM(_NET_WM_PID);
LOAD_ATOM(WM_DELETE_WINDOW);
#undef LOAD_ATOM
#if SUPPORT_DPI
linuxvars.dpi_x = linuxvars.dpi_y = LinuxGetXSettingsDPI(linuxvars.XDisplay, DefaultScreen(linuxvars.XDisplay));
// fallback
if(linuxvars.dpi_x == -1){
int scr = DefaultScreen(linuxvars.XDisplay);
int dw = DisplayWidth(linuxvars.XDisplay, scr);
int dh = DisplayHeight(linuxvars.XDisplay, scr);
int dw_mm = DisplayWidthMM(linuxvars.XDisplay, scr);
int dh_mm = DisplayHeightMM(linuxvars.XDisplay, scr);
linuxvars.dpi_x = dw_mm ? dw / (dw_mm / 25.4) : 96;
linuxvars.dpi_y = dh_mm ? dh / (dh_mm / 25.4) : 96;
fprintf(stderr, "%dx%d - %dmmx%dmm DPI: %dx%d\n", dw, dh, dw_mm, dh_mm, linuxvars.dpi_x, linuxvars.dpi_y);
} else {
fprintf(stderr, "DPI from XSETTINGS: %d\n", linuxvars.dpi_x);
}
#endif
int WinWidth, WinHeight;
if(!LinuxX11WindowInit(argc, argv, &WinWidth, &WinHeight)){
return 1;
}
int xfixes_version_unused, xfixes_err_unused;
linuxvars.has_xfixes = XQueryExtension(
linuxvars.XDisplay,
"XFIXES",
&xfixes_version_unused,
&linuxvars.xfixes_selection_event,
&xfixes_err_unused
) == True;
if(linuxvars.has_xfixes){
XFixesSelectSelectionInput(
linuxvars.XDisplay,
linuxvars.XWindow,
linuxvars.atom_CLIPBOARD,
XFixesSetSelectionOwnerNotifyMask
);
} else {
fputs("Your X server doesn't support XFIXES, mention this fact if you report any clipboard-related issues.\n", stderr);
}
Init_Input_Result input_result =
LinuxInputInit(linuxvars.XDisplay, linuxvars.XWindow);
linuxvars.input_method = input_result.input_method;
linuxvars.input_style = input_result.best_style;
linuxvars.input_context = input_result.xic;
LinuxKeycodeInit(linuxvars.XDisplay);
Cursor xcursors[APP_MOUSE_CURSOR_COUNT] = {
None,
XCreateFontCursor(linuxvars.XDisplay, XC_arrow),
XCreateFontCursor(linuxvars.XDisplay, XC_xterm),
XCreateFontCursor(linuxvars.XDisplay, XC_sb_h_double_arrow),
XCreateFontCursor(linuxvars.XDisplay, XC_sb_v_double_arrow)
};
{
char data = 0;
XColor c = {};
Pixmap p = XCreateBitmapFromData(linuxvars.XDisplay, linuxvars.XWindow, &data, 1, 1);
linuxvars.hidden_cursor = XCreatePixmapCursor(linuxvars.XDisplay, p, p, &c, &c, 0, 0);
XFreePixmap(linuxvars.XDisplay, p);
}
//
// Epoll init
//
linuxvars.x11_fd = ConnectionNumber(linuxvars.XDisplay);
linuxvars.inotify_fd = inotify_init1(IN_NONBLOCK);
linuxvars.step_event_fd = eventfd(0, EFD_NONBLOCK);
linuxvars.step_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
linuxvars.epoll = epoll_create(16);
{
struct epoll_event e = {};
e.events = EPOLLIN | EPOLLET;
e.data.u64 = LINUX_4ED_EVENT_X11;
epoll_ctl(linuxvars.epoll, EPOLL_CTL_ADD, linuxvars.x11_fd, &e);
e.data.u64 = LINUX_4ED_EVENT_STEP;
epoll_ctl(linuxvars.epoll, EPOLL_CTL_ADD, linuxvars.step_event_fd, &e);
e.data.u64 = LINUX_4ED_EVENT_STEP_TIMER;
epoll_ctl(linuxvars.epoll, EPOLL_CTL_ADD, linuxvars.step_timer_fd, &e);
}
//
// App init
//
XAddConnectionWatch(linuxvars.XDisplay, &LinuxX11ConnectionWatch, NULL);
linuxvars.app.init(&linuxvars.system,
&linuxvars.target,
&memory_vars,
linuxvars.clipboard_contents,
current_directory,
linuxvars.custom_api);
LinuxResizeTarget(WinWidth, WinHeight);
//
// Main loop
//
system_acquire_lock(FRAME_LOCK);
LinuxScheduleStep();
linuxvars.keep_running = 1;
linuxvars.input.first_step = 1;
linuxvars.input.dt = (frame_useconds / 1000000.f);
while(1){
if(XEventsQueued(linuxvars.XDisplay, QueuedAlready)){
LinuxHandleX11Events();
}
system_release_lock(FRAME_LOCK);
struct epoll_event events[16];
int num_events = epoll_wait(linuxvars.epoll, events, ArrayCount(events), -1);
system_acquire_lock(FRAME_LOCK);
if(num_events == -1){
if(errno != EINTR){
perror("epoll_wait");
}
continue;
}
b32 do_step = 0;
for(int i = 0; i < num_events; ++i){
int fd = events[i].data.u64 & UINT32_MAX;
u64 type = events[i].data.u64 & ~fd;
switch(type){
case LINUX_4ED_EVENT_X11: {
LinuxHandleX11Events();
} break;
case LINUX_4ED_EVENT_X11_INTERNAL: {
XProcessInternalConnection(linuxvars.XDisplay, fd);
} break;
case LINUX_4ED_EVENT_STEP: {
u64 ev;
int ret;
do {
ret = read(linuxvars.step_event_fd, &ev, 8);
} while(ret != -1 || errno != EAGAIN);
do_step = 1;
} break;
case LINUX_4ED_EVENT_STEP_TIMER: {
u64 count;
int ret;
do {
ret = read(linuxvars.step_timer_fd, &count, 8);
} while(ret != -1 || errno != EAGAIN);
do_step = 1;
} break;
case LINUX_4ED_EVENT_CLI: {
LinuxScheduleStep();
} break;
}
}
if(do_step){
linuxvars.last_step = system_now_time();
if(linuxvars.input.first_step || !linuxvars.has_xfixes){
XConvertSelection(
linuxvars.XDisplay,
linuxvars.atom_CLIPBOARD,
linuxvars.atom_UTF8_STRING,
linuxvars.atom_CLIPBOARD,
linuxvars.XWindow,
CurrentTime
);
}
Application_Step_Result result = {};
result.mouse_cursor_type = APP_MOUSE_CURSOR_DEFAULT;
result.trying_to_kill = !linuxvars.keep_running;
if(linuxvars.new_clipboard){
linuxvars.input.clipboard = linuxvars.clipboard_contents;
linuxvars.new_clipboard = 0;
} else {
linuxvars.input.clipboard = null_string;
}
linuxvars.app.step(
&linuxvars.system,
&linuxvars.target,
&memory_vars,
&linuxvars.input,
&result
);
if(result.perform_kill){
break;
} else {
linuxvars.keep_running = 1;
}
if(result.animating){
LinuxScheduleStep();
}
LinuxRedrawTarget();
if(result.mouse_cursor_type != linuxvars.cursor && !linuxvars.input.mouse.l){
Cursor c = xcursors[result.mouse_cursor_type];
if(!linuxvars.hide_cursor){
XDefineCursor(linuxvars.XDisplay, linuxvars.XWindow, c);
}
linuxvars.cursor = result.mouse_cursor_type;
}
flush_thread_group(BACKGROUND_THREADS);
linuxvars.input.first_step = 0;
linuxvars.input.keys = key_input_data_zero();
linuxvars.input.mouse.press_l = 0;
linuxvars.input.mouse.release_l = 0;
linuxvars.input.mouse.press_r = 0;
linuxvars.input.mouse.release_r = 0;
linuxvars.input.mouse.wheel = 0;
}
}
return 0;
}
// BOTTOM
// vim: expandtab:ts=4:sts=4:sw=4