From f26339ffc38293897d8ff9d74c03ac9c286a6a02 Mon Sep 17 00:00:00 2001 From: jacob Date: Wed, 2 Jul 2025 13:12:31 -0500 Subject: [PATCH] fiber testing --- build.c | 1 + src/arena.h | 2 +- src/config.h | 6 +- src/gp_dx12.c | 26 - src/prof_tracy.h | 11 +- src/sys_win32-old.c | 2598 +++++++++++++++++++++++++++++++++++++++++++ src/sys_win32.c | 305 +++++ 7 files changed, 2920 insertions(+), 29 deletions(-) create mode 100644 src/sys_win32-old.c diff --git a/build.c b/build.c index 9ccf87c5..1fa87313 100644 --- a/build.c +++ b/build.c @@ -843,6 +843,7 @@ void OnBuild(StringList cli_args) StringBeginsWith(name, Lit("ttf_"))) { if (PlatformWindows) { ignore = !(StringEqual(name, Lit("sys_win32.c")) || + StringEqual(name, Lit("sys_win32-old.c")) || StringEqual(name, Lit("sock_win32.c")) || StringEqual(name, Lit("gp_dx12.c")) || StringEqual(name, Lit("playback_wasapi.c")) || diff --git a/src/arena.h b/src/arena.h index a034399c..d5faa7d4 100644 --- a/src/arena.h +++ b/src/arena.h @@ -4,7 +4,7 @@ #include "memory.h" #define ARENA_HEADER_SIZE 256 -#define ARENA_BLOCK_SIZE 4096 +#define ARENA_BLOCK_SIZE 16384 #define arena_push(a, type) ((type *)arena_push_bytes((a), sizeof(type), alignof(type))) #define arena_push_no_zero(a, type) ((type *)arena_push_bytes_no_zero((a), sizeof(type), alignof(type))) diff --git a/src/config.h b/src/config.h index 3b07856b..5743bb01 100644 --- a/src/config.h +++ b/src/config.h @@ -80,7 +80,11 @@ -#define DX12_TEST 1 + + + +#define FIBERS_TEST 0 + diff --git a/src/gp_dx12.c b/src/gp_dx12.c index 16aa3120..273c10fd 100644 --- a/src/gp_dx12.c +++ b/src/gp_dx12.c @@ -1,5 +1,3 @@ -#if DX12_TEST - #include "gp.h" #include "sys.h" #include "arena.h" @@ -2924,27 +2922,3 @@ INTERNAL SYS_THREAD_DEF(evictor_thread_entry_point, arg) scratch_end(scratch); } - - - - - - - - - - - - - - - - - - - - - - - -#endif diff --git a/src/prof_tracy.h b/src/prof_tracy.h index 3d7490de..1abb7c9f 100644 --- a/src/prof_tracy.h +++ b/src/prof_tracy.h @@ -10,11 +10,12 @@ #define PROFILING_SYSTEM_TRACE 0 #define PROFILING_CAPTURE_FRAME_IMAGE 0 #define PROFILING_LOCKS 0 -#define PROFILING_D3D 1 +#define PROFILING_D3D 0 #define PROFILING_CMD_WSTR L"tracy-profiler.exe -a 127.0.0.1" /* Tracy defines */ #define TRACY_ENABLE +#define TRACY_FIBERS #if !PROFILING_SYSTEM_TRACE # define TRACY_NO_CALLSTACK # define TRACY_NO_SYSTEM_TRACING @@ -133,4 +134,12 @@ INLINE void __prof_dx12_zone_cleanup_func(TracyCD3D12ZoneCtx *ctx) { ___tracy_d3 # define __profframeimage(image, width, height, offset, flipped) #endif /* PROFILING_CAPTURE_FRAME_IMAGE */ +#ifdef TRACY_FIBERS +# define __prof_fiber_enter(fiber_name) ___tracy_fiber_enter(fiber_name) +# define __prof_fiber_leave ___tracy_fiber_leave() +#else +# define __prof_fiber_enter(fiber_name) +# define __prof_fiber_leave +#endif + #endif diff --git a/src/sys_win32-old.c b/src/sys_win32-old.c new file mode 100644 index 00000000..211e3b1e --- /dev/null +++ b/src/sys_win32-old.c @@ -0,0 +1,2598 @@ +#if !FIBERS_TEST + +#include "sys.h" +#include "memory.h" +#include "app.h" +#include "string.h" +#include "arena.h" +#include "scratch.h" +#include "atomic.h" +#include "log.h" +#include "math.h" +#include "util.h" +#include "thread_local.h" +#include "uni.h" + +#pragma warning(push, 0) +# define UNICODE +# include +# include +# include +# include +# include +# include +#pragma warning(pop) + +#pragma comment(lib, "kernel32") +#pragma comment(lib, "user32") +#pragma comment(lib, "shell32") +#pragma comment(lib, "ole32") +#pragma comment(lib, "winmm") +#pragma comment(lib, "dwmapi") +#pragma comment(lib, "bcrypt") + +#define SYS_WINDOW_EVENT_LISTENERS_MAX 512 +#define WINDOW_CLASS_NAME L"power_play_window_class" + +struct win32_mutex { + SRWLOCK srwlock; + struct win32_mutex *next_free; + +#if PROFILING_LOCKS + __proflock_ctx(profiling_ctx); +#endif + +#if RTC + u64 owner_tid; + struct atomic_i64 count; +#endif +}; + +struct win32_condition_variable { + CONDITION_VARIABLE condition_variable; + struct win32_condition_variable *next_free; +#if RTC + struct atomic_i64 num_waiters; +#endif +}; + +struct win32_thread { + sys_thread_func *entry_point; + void *thread_data; + char thread_name_cstr[256]; + wchar_t thread_name_wstr[256]; + + struct win32_thread *next; + struct win32_thread *prev; + + HANDLE handle; +}; + +enum win32_window_cursor_set_flag { + WIN32_WINDOW_CURSOR_SET_FLAG_NONE = 0x0, + WIN32_WINDOW_CURSOR_SET_FLAG_POSITION = 0x1, + WIN32_WINDOW_CURSOR_SET_FLAG_HIDE = 0x2, + WIN32_WINDOW_CURSOR_SET_FLAG_SHOW = 0x4, + WIN32_WINDOW_CURSOR_SET_FLAG_ENABLE_CLIP = 0X8, + WIN32_WINDOW_CURSOR_SET_FLAG_DISABLE_CLIP = 0X10 +}; + +struct win32_window { + u32 flags; + + HWND hwnd; + u32 tid; + struct sync_flag ready_sf; + + u16 utf16_high_surrogate_last_input; + + struct sys_mutex *settings_mutex; + struct sys_window_settings settings; + + i32 monitor_width; + i32 monitor_height; + /* NOTE: width & height are unaffected by window minimization (they retain + * their pre-minimized values) */ + i32 x, y, width, height; + + /* FIXME: Use a cmd buffer for updating cursor (and maybe also settings). + * Current setup means cursor_set calls can be applied out of order */ + u32 cursor_set_flags; + struct v2 cursor_set_position; + struct rect cursor_clip_bounds; + + struct atomic_i32 event_thread_shutdown; + struct sys_thread *event_thread; + + struct sys_mutex *event_callbacks_mutex; + sys_window_event_callback_func *event_callbacks[SYS_WINDOW_EVENT_LISTENERS_MAX]; + u64 event_callbacks_count; + + struct win32_window *next_free; +}; + +/* ========================== * + * Global state + * ========================== */ + +GLOBAL struct { + SYSTEM_INFO info; + i64 timer_start_qpc; + i64 qpc_per_second; + i64 ns_per_qpc; + i32 scheduler_period_ms; + DWORD thread_tls_index; + u32 main_thread_id; + + wchar_t cmdline_args_wstr[8192]; + + /* Panic */ + struct atomic_i32 panicking; + wchar_t panic_wstr[4096]; + HANDLE panic_event; + + /* Lookup tables */ + enum sys_btn vk_btn_table[256]; + + /* Mutexes pool */ + struct sys_mutex *mutexes_mutex; + struct arena *mutexes_arena; + struct win32_mutex *first_free_mutex; + + /* Condition variables pool */ + struct sys_mutex *condition_variables_mutex; + struct arena *condition_variables_arena; + struct win32_condition_variable *first_free_condition_variable; + + /* Threads pool */ + struct sys_mutex *threads_mutex; + struct arena *threads_arena; + struct win32_thread *threads_first; + struct win32_thread *threads_last; + struct win32_thread *threads_first_free; + + /* Watches pool */ + struct sys_mutex *watches_mutex; + struct arena *watches_arena; + struct win32_watch *watches_first_free; + + /* Windows pool */ + WNDCLASSEXW window_class; + struct sys_mutex *windows_mutex; + struct arena *windows_arena; + struct win32_window *first_free_window; +} G = ZI, DEBUG_ALIAS(G, G_sys_win32); + +/* ========================== * + * Events + * ========================== */ + +INTERNAL void win32_init_vk_btn_table(void) +{ + MEMZERO_ARRAY(G.vk_btn_table); + + for (u32 i = 'A', j = SYS_BTN_A; i <= 'Z'; ++i, ++j) { + G.vk_btn_table[i] = (enum sys_btn)j; + } + for (u32 i = '0', j = SYS_BTN_0; i <= '9'; ++i, ++j) { + G.vk_btn_table[i] = (enum sys_btn)j; + } + for (u32 i = VK_F1, j = SYS_BTN_F1; i <= VK_F24; ++i, ++j) { + G.vk_btn_table[i] = (enum sys_btn)j; + } + + G.vk_btn_table[VK_ESCAPE] = SYS_BTN_ESC; + G.vk_btn_table[VK_OEM_3] = SYS_BTN_GRAVE_ACCENT; + G.vk_btn_table[VK_OEM_MINUS] = SYS_BTN_MINUS; + G.vk_btn_table[VK_OEM_PLUS] = SYS_BTN_EQUAL; + G.vk_btn_table[VK_BACK] = SYS_BTN_BACKSPACE; + G.vk_btn_table[VK_TAB] = SYS_BTN_TAB; + G.vk_btn_table[VK_SPACE] = SYS_BTN_SPACE; + G.vk_btn_table[VK_RETURN] = SYS_BTN_ENTER; + G.vk_btn_table[VK_CONTROL] = SYS_BTN_CTRL; + G.vk_btn_table[VK_SHIFT] = SYS_BTN_SHIFT; + G.vk_btn_table[VK_MENU] = SYS_BTN_ALT; + G.vk_btn_table[VK_UP] = SYS_BTN_UP; + G.vk_btn_table[VK_LEFT] = SYS_BTN_LEFT; + G.vk_btn_table[VK_DOWN] = SYS_BTN_DOWN; + G.vk_btn_table[VK_RIGHT] = SYS_BTN_RIGHT; + G.vk_btn_table[VK_DELETE] = SYS_BTN_DELETE; + G.vk_btn_table[VK_PRIOR] = SYS_BTN_PAGE_UP; + G.vk_btn_table[VK_NEXT] = SYS_BTN_PAGE_DOWN; + G.vk_btn_table[VK_HOME] = SYS_BTN_HOME; + G.vk_btn_table[VK_END] = SYS_BTN_END; + G.vk_btn_table[VK_OEM_2] = SYS_BTN_FORWARD_SLASH; + G.vk_btn_table[VK_OEM_PERIOD] = SYS_BTN_PERIOD; + G.vk_btn_table[VK_OEM_COMMA] = SYS_BTN_COMMA; + G.vk_btn_table[VK_OEM_7] = SYS_BTN_QUOTE; + G.vk_btn_table[VK_OEM_4] = SYS_BTN_LEFT_BRACKET; + G.vk_btn_table[VK_OEM_6] = SYS_BTN_RIGHT_BRACKET; + G.vk_btn_table[VK_INSERT] = SYS_BTN_INSERT; + G.vk_btn_table[VK_OEM_1] = SYS_BTN_SEMICOLON; +} + +/* ========================== * + * Memory + * ========================== */ + +void *sys_memory_reserve(u64 size) +{ + void *ptr = VirtualAlloc(NULL, size, MEM_RESERVE, PAGE_NOACCESS); + return ptr; +} + +void sys_memory_release(void *address) +{ + VirtualFree(address, 0, MEM_RELEASE); +} + +void *sys_memory_commit(void *address, u64 size) +{ + void *ptr = VirtualAlloc(address, size, MEM_COMMIT, PAGE_READWRITE); + return ptr; +} + +void sys_memory_decommit(void *address, u64 size) +{ + VirtualFree(address, size, MEM_DECOMMIT); +} + +void sys_memory_set_committed_readonly(void *address, u64 size) +{ + DWORD old; + VirtualProtect(address, size, PAGE_READONLY, &old); +} + +void sys_memory_set_committed_readwrite(void *address, u64 size) +{ + DWORD old; + VirtualProtect(address, size, PAGE_READWRITE, &old); +} + +/* ========================== * + * Time + * ========================== */ + +INTERNAL struct sys_datetime win32_time_to_sys_time(SYSTEMTIME st) +{ + return (struct sys_datetime) + { + .year = st.wYear, + .month = st.wMonth, + .day_of_week = st.wDayOfWeek, + .day = st.wDay, + .hour = st.wHour, + .minute = st.wMinute, + .second = st.wSecond, + .milliseconds = st.wMilliseconds + }; +} + +struct sys_datetime sys_local_time(void) +{ + SYSTEMTIME lt; + GetLocalTime(<); + return win32_time_to_sys_time(lt); +} + +i64 sys_time_ns(void) +{ + LARGE_INTEGER qpc; + QueryPerformanceCounter(&qpc); + i64 res = (qpc.QuadPart - G.timer_start_qpc) * G.ns_per_qpc; + return res; +} + +/* ========================== * + * File system + * ========================== */ + +INTERNAL struct string string_from_win32_path(struct arena *arena, wchar_t *src) +{ + struct string res = { + .len = 0, + .text = arena_push_dry(arena, u8) + }; + + while (*src) { + struct string16 decode_str = { .len = *(src + 1) ? 2 : 1, .text = src }; + struct uni_decode_utf16_result decoded = uni_decode_utf16(decode_str); + struct uni_encode_utf8_result encoded = uni_encode_utf8(decoded.codepoint); + u8 *dest = arena_push_array_no_zero(arena, u8, encoded.count8); + for (u32 i = 0; i < encoded.count8; ++i) { + u8 byte = encoded.chars8[i]; + if (byte == '\\') { + byte = '/'; + } + dest[i] = byte; + } + res.len += encoded.count8; + src += decoded.advance16; + } + + return res; +} + +struct string sys_get_write_path(struct arena *arena) +{ + u16 *p = NULL; + /* TODO: cache this? */ + HRESULT res = SHGetKnownFolderPath( + &FOLDERID_LocalAppData, + 0, + NULL, + &p + ); + struct string path = ZI; + if (res == S_OK) { + path = string_from_win32_path(arena, p); + } + CoTaskMemFree(p); + return path; +} + +b32 sys_is_file(struct string path) +{ + __prof; + struct arena_temp scratch = scratch_begin_no_conflict(); + wchar_t *path_wstr = wstr_from_string(scratch.arena, path); + DWORD attributes = GetFileAttributesW(path_wstr); + scratch_end(scratch); + return attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY); +} + +b32 sys_is_dir(struct string path) +{ + struct arena_temp scratch = scratch_begin_no_conflict(); + wchar_t *path_wstr = wstr_from_string(scratch.arena, path); + DWORD attributes = GetFileAttributesW(path_wstr); + scratch_end(scratch); + return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY); +} + +void sys_mkdir(struct string path) +{ + __prof; + struct arena_temp scratch = scratch_begin_no_conflict(); + wchar_t *path_wstr = wstr_from_string(scratch.arena, path); + int err_code = SHCreateDirectory(NULL, path_wstr); + struct string err = ZI; + switch (err_code) { + case ERROR_BAD_PATHNAME: { + err = LIT("Bad path name"); + } break; + + case ERROR_FILENAME_EXCED_RANGE: { + err = LIT("Path name too long"); + } break; + + case ERROR_FILE_EXISTS: { + err = LIT("A file already exists at this location"); + } break; + + case ERROR_CANCELLED: { + err = LIT("User canceled the operation"); + } break; + + default: break; + } + if (err.len > 0) { + struct string msg = string_format(scratch.arena, + LIT("Failed to create directory \"%F\": %F"), + FMT_STR(path), + FMT_STR(err)); + sys_panic(msg); + } + scratch_end(scratch); +} + +struct sys_file sys_file_open_read(struct string path) +{ + __prof; + struct arena_temp scratch = scratch_begin_no_conflict(); + struct sys_file file = ZI; + + wchar_t *path_wstr = wstr_from_string(scratch.arena, path); + HANDLE handle = CreateFileW( + path_wstr, + GENERIC_READ, + FILE_SHARE_READ, + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL + ); + file.handle = (u64)handle; + file.valid = handle != INVALID_HANDLE_VALUE; + + scratch_end(scratch); + return file; +} + +struct sys_file sys_file_open_read_wait(struct string path) +{ + __prof; + struct arena_temp scratch = scratch_begin_no_conflict(); + struct sys_file file = ZI; + + wchar_t *path_wstr = wstr_from_string(scratch.arena, path); + i32 delay_ms = 1; + HANDLE handle; + while ((handle = CreateFileW(path_wstr, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE) { + if (GetLastError() == ERROR_SHARING_VIOLATION) { + __profscope(File share conflict delay); + Sleep(delay_ms); + if (delay_ms < 1024) { + delay_ms *= 2; + } + } else { + break; + } + } + file.handle = (u64)handle; + file.valid = handle != INVALID_HANDLE_VALUE; + + scratch_end(scratch); + return file; +} + +struct sys_file sys_file_open_write(struct string path) +{ + __prof; + struct arena_temp scratch = scratch_begin_no_conflict(); + struct sys_file file = ZI; + + wchar_t *path_wstr = wstr_from_string(scratch.arena, path); + HANDLE handle = CreateFileW( + path_wstr, + GENERIC_WRITE, + 0, + NULL, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL + ); + file.handle = (u64)handle; + file.valid = handle != INVALID_HANDLE_VALUE; + + scratch_end(scratch); + return file; +} + +struct sys_file sys_file_open_append(struct string path) +{ + __prof; + struct arena_temp scratch = scratch_begin_no_conflict(); + struct sys_file file = ZI; + + wchar_t *path_wstr = wstr_from_string(scratch.arena, path); + HANDLE handle = CreateFileW( + path_wstr, + FILE_APPEND_DATA, + FILE_SHARE_READ, + NULL, + OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL + ); + file.handle = (u64)handle; + file.valid = handle != INVALID_HANDLE_VALUE; + + scratch_end(scratch); + return file; +} + +void sys_file_close(struct sys_file file) +{ + __prof; + if (file.handle) { + CloseHandle((HANDLE)file.handle); + } +} + +struct string sys_file_read_all(struct arena *arena, struct sys_file file) +{ + __prof; + i64 size = 0; + GetFileSizeEx((HANDLE)file.handle, (PLARGE_INTEGER)&size); + + struct string s = { + .len = size, + .text = NULL + }; + + if (size > 0) { + /* ReadFile returns non-zero on success */ + /* TODO: error checking */ + arena_align(arena, 16); + s.text = arena_push_array_no_zero(arena, u8, size); + (UNUSED)ReadFile( + (HANDLE)file.handle, + s.text, + (DWORD)s.len, + NULL, /* lpNumberOfBytesRead */ + NULL + ); + } + + return s; +} + +void sys_file_write(struct sys_file file, struct string data) +{ + __prof; + /* TODO: Check what the real data limit is and chunk sequentially based on + * that (rather than failing) */ + if (data.len >= 0x7FFF) { + struct arena_temp scratch = scratch_begin_no_conflict(); + sys_panic(string_format(scratch.arena, + LIT("Tried to write too many bytes to disk (%F)"), + FMT_UINT(data.len))); + scratch_end(scratch); + } + + /* WriteFile returns TRUE on success */ + (UNUSED)WriteFile( + (HANDLE)file.handle, + data.text, + (DWORD)data.len, + NULL, /* lpNumberOfBytesWritten */ + NULL + ); +} + +u64 sys_file_get_size(struct sys_file file) +{ + LARGE_INTEGER li_file_size; + GetFileSizeEx((HANDLE)file.handle, &li_file_size); + return (u64)(li_file_size.QuadPart > 0 ? li_file_size.QuadPart : 0); +} + +struct sys_file_time sys_file_get_time(struct sys_file file) +{ + __prof; + + /* Get file times */ + FILETIME ft_created; + FILETIME ft_accessed; + FILETIME ft_modified; + b32 success = !!GetFileTime((HANDLE)file.handle, &ft_created, &ft_accessed, &ft_modified); + if (success) { + /* Convert file times to local file time */ + FileTimeToLocalFileTime(&ft_created, &ft_created); + FileTimeToLocalFileTime(&ft_accessed, &ft_accessed); + FileTimeToLocalFileTime(&ft_modified, &ft_modified); + + /* Convert local file times to system times */ + SYSTEMTIME st_created; + SYSTEMTIME st_accessed; + SYSTEMTIME st_modified; + FileTimeToSystemTime(&ft_created, &st_created); + FileTimeToSystemTime(&ft_accessed, &st_accessed); + FileTimeToSystemTime(&ft_modified, &st_modified); + + return (struct sys_file_time) { + .created = win32_time_to_sys_time(st_created), + .accessed = win32_time_to_sys_time(st_accessed), + .modified = win32_time_to_sys_time(st_modified) + }; + } else { + return (struct sys_file_time) { 0 }; + } +} + +/* ========================== * + * File map + * ========================== */ + +struct sys_file_map sys_file_map_open_read(struct sys_file file) +{ + __prof; + struct sys_file_map map = ZI; + + u64 size = sys_file_get_size(file); + u8 *base_ptr = NULL; + HANDLE map_handle = 0; + + if (size > 0) { + map_handle = CreateFileMappingW( + (HANDLE)file.handle, + NULL, + PAGE_READONLY, + 0, + 0, + NULL + ); + if (map_handle != INVALID_HANDLE_VALUE) { + base_ptr = MapViewOfFile( + map_handle, + FILE_MAP_READ, + 0, + 0, + 0 + ); + if (base_ptr == NULL) { + /* Failed to create view */ + CloseHandle(map_handle); + map_handle = INVALID_HANDLE_VALUE; + } + } + } + if (map_handle == INVALID_HANDLE_VALUE) { + size = 0; + } + map.handle = (u64)map_handle; + map.mapped_memory = STRING(size, base_ptr); + map.valid = map_handle != INVALID_HANDLE_VALUE && base_ptr != NULL; + + + return map; +} + +void sys_file_map_close(struct sys_file_map map) +{ + if (map.mapped_memory.text) { + UnmapViewOfFile(map.mapped_memory.text); + } + if (map.handle) { + CloseHandle((HANDLE)map.handle); + } +} + +struct string sys_file_map_data(struct sys_file_map map) +{ + return map.mapped_memory; +} + +/* ========================== * + * File iter + * ========================== */ + +struct win32_file_filter { + HANDLE find_handle; + wchar_t *filter_wstr; +}; + +struct sys_file_filter sys_file_filter_begin(struct arena *arena, struct string pattern) +{ + struct sys_file_filter filter = ZI; + struct win32_file_filter *filter_internal = arena_push(arena, struct win32_file_filter); + filter_internal->filter_wstr = wstr_from_string(arena, pattern); + filter.handle = (u64)filter_internal; + return filter; +} + +b32 sys_file_filter_next(struct arena *arena, struct sys_file_filter *filter) +{ + struct win32_file_filter *filter_internal = (struct win32_file_filter *)filter->handle; + + WIN32_FIND_DATAW find_file_data = ZI; + b32 found = false; + + if (filter_internal->find_handle) { + found = FindNextFileW(filter_internal->find_handle, &find_file_data); + } else { + filter_internal->find_handle = FindFirstFileExW(filter_internal->filter_wstr, FindExInfoStandard, &find_file_data, FindExSearchNameMatch, NULL, FIND_FIRST_EX_CASE_SENSITIVE | FIND_FIRST_EX_LARGE_FETCH); + found = filter_internal->find_handle != INVALID_HANDLE_VALUE; + } + + if (found) { + struct string file_name = string_from_wstr_no_limit(arena, find_file_data.cFileName); + if (string_eq(file_name, LIT(".")) || string_eq(file_name, LIT(".."))) { + /* Skip initial '.' and '..' matches */ + found = sys_file_filter_next(arena, filter); + } else { + filter->info.is_dir = find_file_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY; + filter->info.filename = string_copy(arena, file_name); + } + } + + return found; +} + +void sys_file_filter_end(struct sys_file_filter *filter) +{ + struct win32_file_filter *filter_internal = (struct win32_file_filter *)filter->handle; + if (filter_internal->find_handle) { + FindClose(filter_internal->find_handle); + } +} + +/* ========================== * + * Watch + * ========================== */ + +struct win32_watch { + HANDLE dir_handle; + HANDLE wake_handle; + struct win32_watch *next_free; + u8 results_buff[KILOBYTE(64)]; +}; + +struct sys_watch *sys_watch_alloc(struct string dir_path) +{ + struct arena_temp scratch = scratch_begin_no_conflict(); + + struct win32_watch *w32_watch = NULL; + { + struct sys_lock lock = sys_mutex_lock_e(G.watches_mutex); + { + if (G.watches_first_free) { + w32_watch = G.watches_first_free; + G.watches_first_free = w32_watch->next_free; + } else { + w32_watch = arena_push_no_zero(G.watches_arena, struct win32_watch); + } + } + sys_mutex_unlock(&lock); + } + MEMZERO_STRUCT(w32_watch); + + wchar_t *dir_path_wstr = wstr_from_string(scratch.arena, dir_path); + w32_watch->dir_handle = CreateFileW( + dir_path_wstr, + FILE_LIST_DIRECTORY, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, + NULL + ); + + w32_watch->wake_handle = CreateEventW(NULL, FALSE, FALSE, NULL); + + scratch_end(scratch); + return (struct sys_watch *)w32_watch; +} + +void sys_watch_release(struct sys_watch *dw) +{ + struct win32_watch *w32_watch = (struct win32_watch *)dw; + CloseHandle(w32_watch->dir_handle); + CloseHandle(w32_watch->wake_handle); + + struct sys_lock lock = sys_mutex_lock_e(G.watches_mutex); + { + w32_watch->next_free = G.watches_first_free; + G.watches_first_free = w32_watch; + } + sys_mutex_unlock(&lock); +} + +struct sys_watch_info_list sys_watch_wait(struct arena *arena, struct sys_watch *dw) +{ + __prof; + struct win32_watch *w32_watch = (struct win32_watch *)dw; + struct sys_watch_info_list list = ZI; + + DWORD filter = FILE_NOTIFY_CHANGE_FILE_NAME | + FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_LAST_WRITE | + FILE_NOTIFY_CHANGE_CREATION; + + b32 done = false; + while (!done) { + OVERLAPPED ov = ZI; + ov.hEvent = CreateEventW(NULL, FALSE, FALSE, NULL); + ASSERT(ov.hEvent); + + BOOL success = ReadDirectoryChangesW(w32_watch->dir_handle, + w32_watch->results_buff, + ARRAY_COUNT(w32_watch->results_buff), + true, + filter, + NULL, + &ov, + NULL); + (UNUSED)success; + ASSERT(success); + + HANDLE handles[] = { + ov.hEvent, + w32_watch->wake_handle + }; + DWORD wait_res = WaitForMultipleObjects(2, handles, FALSE, INFINITE); + + if (wait_res == WAIT_OBJECT_0) { + i64 offset = 0; + while (!done) { + FILE_NOTIFY_INFORMATION *res = (FILE_NOTIFY_INFORMATION *)(w32_watch->results_buff + offset); + + struct sys_watch_info *info = arena_push(arena, struct sys_watch_info); + if (list.last) { + list.last->next = info; + info->prev = list.last; + } else { + list.first = info; + } + list.last = info; + ++list.count; + + struct string16 name16 = ZI; + name16.text = res->FileName; + name16.len = res->FileNameLength / sizeof(wchar_t); + + info->name = string_from_string16(arena, name16); + for (u64 i = 0; i < info->name.len; ++i) { + if (info->name.text[i] == '\\') { + info->name.text[i] = '/'; + } + } + + switch (res->Action) { + case FILE_ACTION_ADDED: + { + info->kind = SYS_WATCH_INFO_KIND_ADDED; + } break; + + case FILE_ACTION_REMOVED: + { + info->kind = SYS_WATCH_INFO_KIND_REMOVED; + } break; + + case FILE_ACTION_MODIFIED: + { + info->kind = SYS_WATCH_INFO_KIND_MODIFIED; + } break; + + case FILE_ACTION_RENAMED_OLD_NAME: + { + info->kind = SYS_WATCH_INFO_KIND_REMOVED; + } break; + + case FILE_ACTION_RENAMED_NEW_NAME: + { + info->kind = SYS_WATCH_INFO_KIND_RENAMED_OLD; + } break; + + default: + { + info->kind = SYS_WATCH_INFO_KIND_RENAMED_NEW; + } break; + } + + if (res->NextEntryOffset == 0) { + done = true; + } else { + offset += res->NextEntryOffset; + } + } + } else if (wait_res == WAIT_OBJECT_0 + 1) { + ResetEvent(w32_watch->wake_handle); + done = true; + } else { + ASSERT(false); + } + } + + return list; +} + +void sys_watch_wake(struct sys_watch *dw) +{ + struct win32_watch *w32_watch = (struct win32_watch *)dw; + SetEvent(w32_watch->wake_handle); +} + +struct sys_watch_info_list sys_watch_info_copy(struct arena *arena, struct sys_watch_info_list src_list) +{ + struct sys_watch_info_list dst_list = ZI; + for (struct sys_watch_info *src = src_list.first; src; src = src->next) { + struct sys_watch_info *dst = arena_push(arena, struct sys_watch_info); + dst->kind = src->kind; + dst->name = string_copy(arena, src->name); + if (dst_list.last) { + dst_list.last->next = dst; + dst->prev = dst_list.last; + dst_list.last = dst; + } else { + dst_list.first = dst; + dst_list.last = dst; + } + dst_list.count = src_list.count; + } + return dst_list; +} + +/* ========================== * + * Window + * ========================== */ + +INTERNAL void win32_update_window_from_system(struct win32_window *window); +INTERNAL void win32_window_wake(struct win32_window *window); + +INTERNAL void win32_window_process_event(struct win32_window *window, struct sys_event event) +{ + __prof; + struct sys_lock lock = sys_mutex_lock_e(window->event_callbacks_mutex); + for (u64 i = 0; i < window->event_callbacks_count; ++i) { + window->event_callbacks[i](event); + } + sys_mutex_unlock(&lock); +} + +INTERNAL HWND win32_create_window(struct win32_window *window) +{ + /* + * From martins (https://gist.github.com/mmozeiko/5e727f845db182d468a34d524508ad5f#file-win32_d3d11-c-L66-L70): + * WS_EX_NOREDIRECTIONBITMAP flag here is needed to fix ugly bug with Windows 10 + * when window is resized and DXGI swap chain uses FLIP presentation model + * DO NOT use it if you choose to use non-FLIP presentation model + * read about the bug here: https://stackoverflow.com/q/63096226 and here: https://stackoverflow.com/q/53000291 + */ + DWORD exstyle = WS_EX_APPWINDOW | WS_EX_NOREDIRECTIONBITMAP; + + /* TODO: Check for hwnd success */ + HWND hwnd = CreateWindowExW( + exstyle, + G.window_class.lpszClassName, + L"", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + NULL, + NULL, + G.window_class.hInstance, + NULL + ); + + /* Dark mode */ + BOOL dark_mode = true; + DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, (LPCVOID)&dark_mode, sizeof(dark_mode)); + + /* Set window as userdata */ + SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)window); + + return hwnd; +} + +INTERNAL SYS_THREAD_DEF(window_thread_entry_point, arg) +{ + struct win32_window *window = (struct win32_window *)arg; + + /* Win32 limitation: Window must be initialized on same thread that processes events */ + window->hwnd = win32_create_window(window); + window->tid = sys_thread_id(); + win32_update_window_from_system(window); + BringWindowToTop(window->hwnd); + + sync_flag_set(&window->ready_sf); + + while (!atomic_i32_eval(&window->event_thread_shutdown)) { + MSG msg = ZI; + { + GetMessageW(&msg, 0, 0, 0); + } + { + __profscope(Process window message); + if (atomic_i32_eval(&window->event_thread_shutdown)) { + break; + } + + /* Update cursor */ + if (GetFocus() == window->hwnd) { + u32 cursor_flags = window->cursor_set_flags; + + /* Hide cursor */ + if (cursor_flags & WIN32_WINDOW_CURSOR_SET_FLAG_HIDE) { + while(ShowCursor(false) >= 0); + } + + /* Show cursor */ + if (cursor_flags & WIN32_WINDOW_CURSOR_SET_FLAG_SHOW) { + while(ShowCursor(true) < 0); + } + + /* Update position */ + if (cursor_flags & WIN32_WINDOW_CURSOR_SET_FLAG_POSITION) { + struct v2 window_space_pos = window->cursor_set_position; + POINT p = { window_space_pos.x, window_space_pos.y }; + ClientToScreen(window->hwnd, &p); + SetCursorPos(p.x, p.y); + } + + /* Stop clipping cursor */ + if (cursor_flags & WIN32_WINDOW_CURSOR_SET_FLAG_DISABLE_CLIP) { + ClipCursor(NULL); + } + + /* Clip cursor in window window */ + if (cursor_flags & WIN32_WINDOW_CURSOR_SET_FLAG_ENABLE_CLIP) { + i32 left = window->x + math_round_to_int(window->cursor_clip_bounds.x); + i32 right = left + math_round_to_int(window->cursor_clip_bounds.width); + i32 top = window->y + math_round_to_int(window->cursor_clip_bounds.y); + i32 bottom = top + math_round_to_int(window->cursor_clip_bounds.height); + RECT clip = { + .left = clamp_i32(left, window->x, window->x + window->width), + .right = clamp_i32(right, window->x, window->x + window->width), + .top = clamp_i32(top, window->y, window->y + window->height), + .bottom = clamp_i32(bottom, window->y, window->y + window->height) + }; + ClipCursor(&clip); + } + + window->cursor_set_flags = 0; + } + + /* Process message */ + switch (msg.message) { + case WM_QUIT: { + win32_window_process_event(window, (struct sys_event) { .kind = SYS_EVENT_KIND_QUIT }); + } break; + + default: { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } break; + } + } + } + + /* Destroy window hwnd */ + DestroyWindow(window->hwnd); +} + +INTERNAL struct win32_window *win32_window_alloc(void) +{ + struct win32_window *window = NULL; + { + struct sys_lock lock = sys_mutex_lock_e(G.windows_mutex); + if (G.first_free_window) { + window = G.first_free_window; + G.first_free_window = window->next_free; + } else { + window = arena_push_no_zero(G.windows_arena, struct win32_window); + } + sys_mutex_unlock(&lock); + } + MEMZERO_STRUCT(window); + + /* Allocate sync flag */ + window->ready_sf = sync_flag_alloc(); + + /* Allocate mutexes */ + window->settings_mutex = sys_mutex_alloc(); + window->event_callbacks_mutex = sys_mutex_alloc(); + + /* Start window thread for processing events */ + window->event_thread = sys_thread_alloc(&window_thread_entry_point, window, LIT("[P4] Window thread")); + + /* Wait for event thread to create actual window */ + sync_flag_wait(&window->ready_sf); + + return window; +} + +INTERNAL void win32_window_release(struct win32_window *window) +{ + struct sys_lock lock = sys_mutex_lock_e(G.windows_mutex); + + window->next_free = G.first_free_window; + G.first_free_window = window; + + /* Stop window thread */ + atomic_i32_eval_exchange(&window->event_thread_shutdown, 1); + win32_window_wake(window); + sys_thread_wait_release(window->event_thread); + + /* Release mutexes */ + sys_mutex_release(window->event_callbacks_mutex); + sys_mutex_release(window->settings_mutex); + + /* Release sync flag */ + sync_flag_release(&window->ready_sf); + + sys_mutex_unlock(&lock); +} + +INTERNAL void win32_update_window_from_system(struct win32_window *window) +{ + HWND hwnd = window->hwnd; + + RECT window_rect = ZI; + GetWindowRect(hwnd, &window_rect); + + RECT client_rect = ZI; + GetClientRect(hwnd, (LPRECT)&client_rect); + ClientToScreen(hwnd, (LPPOINT)&client_rect.left); + ClientToScreen(hwnd, (LPPOINT)&client_rect.right); + + /* TODO: Error if we can't get monitor info */ + /* Screen dimensions */ + MONITORINFO monitor_info = { .cbSize = sizeof(monitor_info) }; + GetMonitorInfo(MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY), &monitor_info); + RECT monitor_rect = monitor_info.rcMonitor; + window->monitor_width = monitor_rect.right - monitor_rect.left; + window->monitor_height = monitor_rect.bottom - monitor_rect.top; + + /* Minimized / maximized */ + if (window->flags & SYS_WINDOW_FLAG_SHOWING) { + WINDOWPLACEMENT placement = { .length = sizeof(placement) }; + GetWindowPlacement(hwnd, &placement); + if (placement.showCmd == SW_SHOWMINIMIZED) { + window->settings.flags |= SYS_WINDOW_SETTINGS_FLAG_MINIMIZED; + } else { + window->settings.flags &= ~SYS_WINDOW_SETTINGS_FLAG_MINIMIZED; + } + if (placement.showCmd == SW_SHOWMAXIMIZED + || ((window->settings.flags & SYS_WINDOW_SETTINGS_FLAG_MINIMIZED) && ((placement.flags & WPF_RESTORETOMAXIMIZED) != 0))) { + window->settings.flags |= SYS_WINDOW_SETTINGS_FLAG_MAXIMIZED; + } else { + window->settings.flags &= ~SYS_WINDOW_SETTINGS_FLAG_MAXIMIZED; + } + } + + /* Window dimensions */ + i32 x = client_rect.left; + i32 y = client_rect.top; + i32 width = client_rect.right - client_rect.left; + i32 height = client_rect.bottom - client_rect.top; + if (!(window->settings.flags & SYS_WINDOW_SETTINGS_FLAG_MINIMIZED)) { + window->x = x; + window->y = y; + window->width = width; + window->height = height; + if (!(window->settings.flags & (SYS_WINDOW_SETTINGS_FLAG_MAXIMIZED | SYS_WINDOW_SETTINGS_FLAG_FULLSCREEN))) { + /* Treat a window resize in non maximized/fullscreen mode as a + * settings change. + * + * TODO: make sure we check for fullscreen here too if we ever + * allow it. */ + window->settings.floating_x = x; + window->settings.floating_y = y; + window->settings.floating_width = width; + window->settings.floating_height = height; + } + } +} + +INTERNAL void win32_update_window_from_settings(struct win32_window *window, struct sys_window_settings *settings) +{ + HWND hwnd = window->hwnd; + + struct sys_window_settings old_settings = window->settings; + window->settings = *settings; + + i32 show_cmd = SW_HIDE; + if (window->flags & SYS_WINDOW_FLAG_SHOWING) { + show_cmd = SW_NORMAL; + if (settings->flags & SYS_WINDOW_SETTINGS_FLAG_MAXIMIZED) { + show_cmd = SW_SHOWMAXIMIZED; + } else if (settings->flags & SYS_WINDOW_SETTINGS_FLAG_MINIMIZED) { + show_cmd = SW_MINIMIZE; + } + } + + RECT rect = ZI; + + b32 old_fullscreen = old_settings.flags & SYS_WINDOW_SETTINGS_FLAG_FULLSCREEN; + b32 fullscreen = settings->flags & SYS_WINDOW_SETTINGS_FLAG_FULLSCREEN; + + if (fullscreen) { + if (!old_fullscreen) { + /* Entering fullscreen */ + SetWindowLongPtrW(hwnd, GWL_STYLE, WS_POPUP); + } + rect = (RECT) { + .left = 0, + .top = 0, + .right = window->monitor_width, + .bottom = window->monitor_height + }; + } else { + if (old_fullscreen) { + /* Leaving fullscreen */ + SetWindowLongPtrW(hwnd, GWL_STYLE, WS_OVERLAPPEDWINDOW); + } + rect = (RECT) { + .left = settings->floating_x, + .top = settings->floating_y, + .right = settings->floating_x + settings->floating_width, + .bottom = settings->floating_y + settings->floating_height + }; + AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false); + } + + WINDOWPLACEMENT wp = { + .length = sizeof(WINDOWPLACEMENT), + .showCmd = show_cmd, + .rcNormalPosition = rect + }; + SetWindowPlacement(hwnd, &wp); + + /* Make window always on top when debugging */ +#if 0 +#if RTC + SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); +#endif +#endif + + { + struct arena_temp scratch = scratch_begin_no_conflict(); + wchar_t *title_wstr = wstr_from_string(scratch.arena, string_from_cstr_no_limit(settings->title)); + SetWindowTextW(hwnd, title_wstr); + scratch_end(scratch); + } +} + +INTERNAL void win32_window_wake(struct win32_window *window) +{ + /* Post a blank message to the window's thread message queue to wake it. */ + PostMessageW(window->hwnd, WM_NULL, 0, 0); +} + +INTERNAL LRESULT CALLBACK win32_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) +{ + __prof; + struct win32_window *window = (struct win32_window *)GetWindowLongPtrW(hwnd, GWLP_USERDATA); + + if (!window) { + return DefWindowProcW(hwnd, msg, wparam, lparam); + } + + LRESULT result = 0; + b32 is_release = false; + switch (msg) { + case WM_CLOSE: + case WM_DESTROY: { + win32_window_process_event(window, (struct sys_event) { .kind = SYS_EVENT_KIND_QUIT }); + } break; + + case WM_PAINT: { + result = DefWindowProcW(hwnd, msg, wparam, lparam); + } break; + + case WM_ENTERSIZEMOVE: + case WM_MOVE: + case WM_MOVING: + case WM_SIZE: + case WM_SIZING: { + win32_update_window_from_system(window); + result = DefWindowProcW(hwnd, msg, wparam, lparam); + } break; + + /* Keyboard buttons */ + case WM_SYSKEYUP: + case WM_SYSKEYDOWN: { + if (LOWORD(wparam) != VK_MENU) { + result = DefWindowProcW(hwnd, msg, wparam, lparam); + } + } FALLTHROUGH; + case WM_KEYUP: + case WM_KEYDOWN: { + WORD vk_code = LOWORD(wparam); + b32 is_repeat = false; + + enum sys_event_kind event_kind = SYS_EVENT_KIND_NONE; + if (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN) { + event_kind = SYS_EVENT_KIND_BUTTON_DOWN; + is_repeat = (lparam & 0x40000000) != 0; + } else if (msg == WM_KEYUP || msg == WM_SYSKEYUP) { + event_kind = SYS_EVENT_KIND_BUTTON_UP; + } + + enum sys_btn button = SYS_BTN_NONE; + if (vk_code < ARRAY_COUNT(G.vk_btn_table)) { + button = G.vk_btn_table[vk_code]; + } + + win32_window_process_event( + window, + (struct sys_event) { + .kind = event_kind, + .button = button, + .is_repeat = is_repeat + } + ); + } break; + + /* Text */ + case WM_SYSCHAR: + case WM_CHAR: { + u16 utf16_char = (u32)wparam; + + /* Decode */ + u32 codepoint = 0; + if (uni_is_utf16_high_surrogate(utf16_char)) { + window->utf16_high_surrogate_last_input = utf16_char; + } else if (uni_is_utf16_low_surrogate(utf16_char)) { + u16 high = window->utf16_high_surrogate_last_input; + u16 low = utf16_char; + if (high) { + u16 utf16_pair_bytes[2] = { high, low }; + struct uni_decode_utf16_result decoded = uni_decode_utf16((struct string16) { .len = ARRAY_COUNT(utf16_pair_bytes), .text = utf16_pair_bytes }); + if (decoded.advance16 == 2 && decoded.codepoint < U32_MAX) { + codepoint = decoded.codepoint; + } + } + window->utf16_high_surrogate_last_input = 0; + } else { + window->utf16_high_surrogate_last_input = 0; + codepoint = utf16_char; + } + + if (codepoint) { + if (codepoint == '\r') { + codepoint = '\n'; /* Just treat all \r as newline */ + } + if((codepoint >= 32 && codepoint != 127) || codepoint == '\t' || codepoint == '\n') { + win32_window_process_event( + window, + (struct sys_event) { + .kind = SYS_EVENT_KIND_TEXT, + .text_codepoint = codepoint + } + ); + } + } + + + } break; + + /* Mouse buttons */ + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + case WM_XBUTTONUP: { + ReleaseCapture(); + is_release = true; + } FALLTHROUGH; + case WM_LBUTTONDOWN: + case WM_MBUTTONDOWN: + case WM_RBUTTONDOWN: + case WM_XBUTTONDOWN: { + if(!is_release) { + SetCapture(hwnd); + } + + enum sys_event_kind event_kind = is_release ? SYS_EVENT_KIND_BUTTON_UP : SYS_EVENT_KIND_BUTTON_DOWN; + enum sys_btn button = 0; + + switch(msg) { + case WM_LBUTTONUP: case WM_LBUTTONDOWN: button = SYS_BTN_M1; break; + case WM_RBUTTONUP: case WM_RBUTTONDOWN: button = SYS_BTN_M2; break; + case WM_MBUTTONUP: case WM_MBUTTONDOWN: button = SYS_BTN_M3; break; + case WM_XBUTTONUP: case WM_XBUTTONDOWN: { + u32 wparam_xbutton = GET_XBUTTON_WPARAM(wparam); + if (wparam_xbutton == XBUTTON1) { + button = SYS_BTN_M4; + } else if (wparam_xbutton == XBUTTON2) { + button = SYS_BTN_M5; + } + } break; + } + + if (button) { + win32_window_process_event( + window, + (struct sys_event) { + .kind = event_kind, + .button = button + } + ); + } + } break; + + /* Mouse wheel */ + case WM_MOUSEWHEEL: { + int delta = GET_WHEEL_DELTA_WPARAM(wparam); + i32 dir = delta >= 0 ? 1 : -1; + enum sys_btn button = dir >= 0 ? SYS_BTN_MWHEELUP : SYS_BTN_MWHEELDOWN; + for (i32 i = 0; i < (dir * delta); i += WHEEL_DELTA) { + /* Send a button down & button up event simultaneously */ + win32_window_process_event(window, (struct sys_event) { .kind = SYS_EVENT_KIND_BUTTON_DOWN, .button = button }); + win32_window_process_event(window, (struct sys_event) { .kind = SYS_EVENT_KIND_BUTTON_UP, .button = button }); + } + } break; + + /* Mouse move */ + case WM_MOUSEMOVE: { + i32 x = GET_X_LPARAM(lparam); + i32 y = GET_Y_LPARAM(lparam); + win32_window_process_event( + window, + (struct sys_event) { + .kind = SYS_EVENT_KIND_CURSOR_MOVE, + .cursor_position = V2(x, y) + } + ); + } break; + + /* Raw mouse move */ + case WM_INPUT: { + struct arena_temp scratch = scratch_begin_no_conflict(); + + /* Read raw input buffer */ + UINT buff_size; + GetRawInputData((HRAWINPUT)lparam, RID_INPUT, NULL, &buff_size, sizeof(RAWINPUTHEADER)); + u8 *buff = arena_push_array(scratch.arena, u8, buff_size); + if (GetRawInputData((HRAWINPUT)lparam, RID_INPUT, buff, &buff_size, sizeof(RAWINPUTHEADER)) != buff_size) { + logf_error("GetRawInputData did not return correct size"); + break; + } + RAWINPUT raw = ZI; + MEMCPY(&raw, buff, sizeof(RAWINPUT)); + + if (raw.header.dwType == RIM_TYPEMOUSE) { + i32 x = raw.data.mouse.lLastX; + i32 y = raw.data.mouse.lLastY; + struct v2 delta = V2(x, y); + win32_window_process_event( + window, + (struct sys_event) { + .kind = SYS_EVENT_KIND_MOUSE_MOVE, + .mouse_delta = delta + } + ); + } + + scratch_end(scratch); + } break; + + /* Minmax info */ + case WM_GETMINMAXINFO: { + /* Set minimum window size */ + LPMINMAXINFO mmi = (LPMINMAXINFO)lparam; + mmi->ptMinTrackSize.x = 100; + mmi->ptMinTrackSize.y = 100; + } break; + + default: { + result = DefWindowProcW(hwnd, msg, wparam, lparam); + } break; + } + + return result; +} + +struct sys_window *sys_window_alloc(void) +{ + __prof; + return (struct sys_window *)win32_window_alloc(); +} + +void sys_window_release(struct sys_window *sys_window) +{ + __prof; + struct win32_window *window = (struct win32_window *)sys_window; + win32_window_release(window); +} + +void sys_window_register_event_callback(struct sys_window *sys_window, sys_window_event_callback_func *func) +{ + struct win32_window *window = (struct win32_window *)sys_window; + struct sys_lock lock = sys_mutex_lock_e(window->event_callbacks_mutex); + { + if (window->event_callbacks_count + 1 > ARRAY_COUNT(window->event_callbacks)) { + sys_panic(LIT("Too many window event callbacks registered")); + } else { + window->event_callbacks[window->event_callbacks_count++] = func; + } + } + sys_mutex_unlock(&lock); +} + +void sys_window_unregister_event_callback(struct sys_window *sys_window, sys_window_event_callback_func *func) +{ + struct win32_window *window = (struct win32_window *)sys_window; + + struct sys_lock lock = sys_mutex_lock_e(window->event_callbacks_mutex); + { + u64 count = window->event_callbacks_count; + sys_window_event_callback_func *last = count > 0 ? window->event_callbacks[count - 1] : NULL; + + for (u64 i = 0; i < window->event_callbacks_count; ++i) { + if (window->event_callbacks[i] == func) { + if (func != last) { + /* Swap with last element */ + window->event_callbacks[i] = last; + } + --window->event_callbacks_count; + } + } + } + sys_mutex_unlock(&lock); +} + +void sys_window_update_settings(struct sys_window *sys_window, struct sys_window_settings *settings) +{ + __prof; + struct win32_window *window = (struct win32_window *)sys_window; + struct sys_lock lock = sys_mutex_lock_e(window->settings_mutex); + { + win32_update_window_from_settings(window, settings); + } + sys_mutex_unlock(&lock); +} + +/* FIXME: Lock settings mutex for these functions */ + +struct sys_window_settings sys_window_get_settings(struct sys_window *sys_window) +{ + struct win32_window *window = (struct win32_window *)sys_window; + return window->settings; +} + +void sys_window_show(struct sys_window *sys_window) +{ + struct win32_window *window = (struct win32_window *)sys_window; + HWND hwnd = window->hwnd; + struct sys_lock lock = sys_mutex_lock_e(window->settings_mutex); + { + i32 show_cmd = SW_NORMAL; + struct sys_window_settings *settings = &window->settings; + if (settings->flags & SYS_WINDOW_SETTINGS_FLAG_MAXIMIZED) { + show_cmd = SW_SHOWMAXIMIZED; + } else if (settings->flags & SYS_WINDOW_SETTINGS_FLAG_MINIMIZED) { + show_cmd = SW_MINIMIZE; + } + window->flags |= SYS_WINDOW_FLAG_SHOWING; + ShowWindow(hwnd, show_cmd); + BringWindowToTop(hwnd); + } + sys_mutex_unlock(&lock); +} + +struct v2 sys_window_get_size(struct sys_window *sys_window) +{ + struct win32_window *window = (struct win32_window *)sys_window; + return V2((f32)window->width, (f32)window->height); +} + +struct v2 sys_window_get_monitor_size(struct sys_window *sys_window) +{ + struct win32_window *window = (struct win32_window *)sys_window; + return V2((f32)window->monitor_width, (f32)window->monitor_height); +} + +u64 sys_window_get_internal_handle(struct sys_window *sys_window) +{ + struct win32_window *window = (struct win32_window *)sys_window; + return (u64)window->hwnd; +} + +void sys_window_cursor_set_pos(struct sys_window *sys_window, struct v2 pos) +{ + struct win32_window *window = (struct win32_window *)sys_window; + window->cursor_set_position = pos; + window->cursor_set_flags |= WIN32_WINDOW_CURSOR_SET_FLAG_POSITION; + win32_window_wake(window); +} + +void sys_window_cursor_show(struct sys_window *sys_window) +{ + struct win32_window *window = (struct win32_window *)sys_window; + window->cursor_set_flags |= WIN32_WINDOW_CURSOR_SET_FLAG_SHOW; + win32_window_wake(window); +} + +void sys_window_cursor_hide(struct sys_window *sys_window) +{ + struct win32_window *window = (struct win32_window *)sys_window; + window->cursor_set_flags |= WIN32_WINDOW_CURSOR_SET_FLAG_HIDE; + win32_window_wake(window); +} + +void sys_window_cursor_enable_clip(struct sys_window *sys_window, struct rect bounds) +{ + struct win32_window *window = (struct win32_window *)sys_window; + window->cursor_clip_bounds = bounds; + window->cursor_set_flags |= WIN32_WINDOW_CURSOR_SET_FLAG_ENABLE_CLIP; + win32_window_wake(window); +} + +void sys_window_cursor_disable_clip(struct sys_window *sys_window) +{ + struct win32_window *window = (struct win32_window *)sys_window; + window->cursor_set_flags |= WIN32_WINDOW_CURSOR_SET_FLAG_DISABLE_CLIP; + win32_window_wake(window); +} + +/* ========================== * + * Mutex + * ========================== */ + +INTERNAL void win32_mutex_init(struct win32_mutex *m) +{ +#if PROFILING_LOCKS + __proflock_ctx(profiling_ctx) = m->profiling_ctx; +#endif + MEMZERO_STRUCT(m); + m->srwlock = (SRWLOCK)SRWLOCK_INIT; +#if PROFILING_LOCKS + if (!profiling_ctx) { + __proflock_alloc(profiling_ctx); + } + m->profiling_ctx = profiling_ctx; +#endif +} + +struct sys_mutex *sys_mutex_alloc(void) +{ + __prof; + struct win32_mutex *m = NULL; + { + struct sys_lock lock = sys_mutex_lock_e(G.mutexes_mutex); + if (G.first_free_mutex) { + m = G.first_free_mutex; + G.first_free_mutex = m->next_free; + } else { + m = arena_push_no_zero(G.mutexes_arena, struct win32_mutex); + } + sys_mutex_unlock(&lock); + } + win32_mutex_init(m); + return (struct sys_mutex *)m; +} + +void sys_mutex_release(struct sys_mutex *mutex) +{ + __prof; + struct win32_mutex *m = (struct win32_mutex *)mutex; + ASSERT(atomic_i64_eval(&m->count) == 0); /* Mutex should be unlocked */ + { + struct sys_lock lock = sys_mutex_lock_e(G.mutexes_mutex); + m->next_free = G.first_free_mutex; + G.first_free_mutex = m; + sys_mutex_unlock(&lock); + } +} + +struct sys_lock sys_mutex_lock_e(struct sys_mutex *mutex) +{ + struct win32_mutex *m = (struct win32_mutex *)mutex; + __proflock_before_exclusive_lock(m->profiling_ctx); + AcquireSRWLockExclusive((SRWLOCK *)&m->srwlock); + __proflock_after_exclusive_lock(m->profiling_ctx); +#if RTC + m->owner_tid = (u64)GetCurrentThreadId(); + atomic_i64_eval_add(&m->count, 1); +#endif + struct sys_lock lock = ZI; + lock.exclusive = true; + lock.mutex = mutex; + return lock; +} + +struct sys_lock sys_mutex_lock_s(struct sys_mutex *mutex) +{ + struct win32_mutex *m = (struct win32_mutex *)mutex; + __proflock_before_shared_lock(m->profiling_ctx); + AcquireSRWLockShared((SRWLOCK *)&m->srwlock); + __proflock_after_shared_lock(m->profiling_ctx); +#if RTC + atomic_i64_eval_add(&m->count, 1); +#endif + struct sys_lock lock = ZI; + lock.mutex = mutex; + return lock; +} + +void sys_mutex_unlock(struct sys_lock *lock) +{ + struct win32_mutex *m = (struct win32_mutex *)lock->mutex; +#if RTC + atomic_i64_eval_add(&m->count, -1); + m->owner_tid = 0; +#endif + if (lock->exclusive) { + ReleaseSRWLockExclusive((SRWLOCK *)&m->srwlock); + __proflock_after_exclusive_unlock(m->profiling_ctx); + } else { + ReleaseSRWLockShared((SRWLOCK *)&m->srwlock); + __proflock_after_shared_unlock(m->profiling_ctx); + } + MEMZERO_STRUCT(lock); +} + +#if RTC +void sys_assert_locked_e(struct sys_lock *lock, struct sys_mutex *mutex) +{ + ASSERT(lock->mutex == mutex); + ASSERT(lock->exclusive == true); +} + +void sys_assert_locked_e_or_s(struct sys_lock *lock, struct sys_mutex *mutex) +{ + ASSERT(lock->mutex == mutex); +} +#endif + +/* ========================== * + * Condition variable + * ========================== */ + +INTERNAL struct win32_condition_variable *win32_condition_variable_alloc(void) +{ + __prof; + struct win32_condition_variable *cv = NULL; + { + struct sys_lock lock = sys_mutex_lock_e(G.condition_variables_mutex); + if (G.first_free_condition_variable) { + cv = G.first_free_condition_variable; + G.first_free_condition_variable = cv->next_free; + } else { + cv = arena_push(G.condition_variables_arena, struct win32_condition_variable); + } + sys_mutex_unlock(&lock); + } + + MEMZERO_STRUCT(cv); + InitializeConditionVariable(&cv->condition_variable); + + return cv; +} + +INTERNAL void win32_condition_variable_release(struct win32_condition_variable *w32cv) +{ + __prof; + struct sys_lock lock = sys_mutex_lock_e(G.condition_variables_mutex); + w32cv->next_free = G.first_free_condition_variable; + G.first_free_condition_variable = w32cv; + sys_mutex_unlock(&lock); +} + +struct sys_condition_variable *sys_condition_variable_alloc(void) +{ + __prof; + return (struct sys_condition_variable *)win32_condition_variable_alloc(); +} + +void sys_condition_variable_release(struct sys_condition_variable *sys_cv) +{ + __prof; + struct win32_condition_variable *cv = (struct win32_condition_variable *)sys_cv; + /* Condition variable must not have any sleepers (signal before releasing) */ + ASSERT(atomic_i64_eval(&cv->num_waiters) == 0); + win32_condition_variable_release(cv); +} + +void sys_condition_variable_wait(struct sys_condition_variable *sys_cv, struct sys_lock *lock) +{ + struct win32_condition_variable *cv = (struct win32_condition_variable *)sys_cv; + struct win32_mutex *m = (struct win32_mutex *)lock->mutex; + b32 exclusive = lock->exclusive; +#if RTC + atomic_i64_eval_add(&cv->num_waiters, 1); + if (exclusive) { + m->owner_tid = 0; + } + atomic_i64_eval_add(&m->count, -1); +#endif + + /* TODO: Correct profiling of internal condition variable sleep / wait mutex state */ + if (exclusive) { + __proflock_after_exclusive_unlock(m->profiling_ctx); + } else { + __proflock_after_shared_unlock(m->profiling_ctx); + } + SleepConditionVariableSRW(&cv->condition_variable, (SRWLOCK *)&m->srwlock, INFINITE, exclusive ? 0 : CONDITION_VARIABLE_LOCKMODE_SHARED); + if (exclusive) { + __proflock_before_exclusive_lock(m->profiling_ctx); + __proflock_after_exclusive_lock(m->profiling_ctx); + } else { + __proflock_before_shared_lock(m->profiling_ctx); + __proflock_after_shared_lock(m->profiling_ctx); + } + +#if RTC + atomic_i64_eval_add(&m->count, 1); + if (exclusive) { + m->owner_tid = (u64)GetCurrentThreadId(); + } + atomic_i64_eval_add(&cv->num_waiters, -1); +#endif +} + +void sys_condition_variable_wait_time(struct sys_condition_variable *sys_cv, struct sys_lock *lock, f64 seconds) +{ + struct win32_condition_variable *cv = (struct win32_condition_variable *)sys_cv; + struct win32_mutex *m = (struct win32_mutex *)lock->mutex; + b32 exclusive = lock->exclusive; +#if RTC + atomic_i64_eval_add(&cv->num_waiters, 1); + if (exclusive) { + m->owner_tid = 0; + } + atomic_i64_eval_add(&m->count, -1); +#endif + u32 ms = (u32)math_round_to_int((f32)seconds * 1000.f); + + /* TODO: Correct profiling of internal condition variable sleep / wait mutex state */ + if (exclusive) { + __proflock_after_exclusive_unlock(m->profiling_ctx); + } else { + __proflock_after_shared_unlock(m->profiling_ctx); + } + SleepConditionVariableSRW(&cv->condition_variable, (SRWLOCK *)&m->srwlock, ms, exclusive ? 0 : CONDITION_VARIABLE_LOCKMODE_SHARED); + if (exclusive) { + __proflock_before_exclusive_lock(m->profiling_ctx); + __proflock_after_exclusive_lock(m->profiling_ctx); + } else { + __proflock_before_shared_lock(m->profiling_ctx); + __proflock_after_shared_lock(m->profiling_ctx); + } + +#if RTC + atomic_i64_eval_add(&m->count, 1); + if (exclusive) { + m->owner_tid = (u64)GetCurrentThreadId(); + } + atomic_i64_eval_add(&cv->num_waiters, -1); +#endif +} + +void sys_condition_variable_signal(struct sys_condition_variable *sys_cv, u32 count) +{ + struct win32_condition_variable *cv = (struct win32_condition_variable *)sys_cv; + /* Windows will wake all waiters if many single-wakes occur anyway, so we + * might as well wake all ourselves. + * https://devblogs.microsoft.com/oldnewthing/20180201-00/?p=97946 */ + if (count <= 24) { + for (u32 i = 0; i < count; ++i) { + WakeConditionVariable(&cv->condition_variable); + } + } else { + WakeAllConditionVariable(&cv->condition_variable); + } +} + +void sys_condition_variable_broadcast(struct sys_condition_variable *sys_cv) +{ + struct win32_condition_variable *cv = (struct win32_condition_variable *)sys_cv; + WakeAllConditionVariable(&cv->condition_variable); +} + +/* ========================== * + * Thread local storage + * ========================== */ + +struct win32_tls { + HANDLE sleep_timer; + struct thread_local_store store; +}; + +INTERNAL void win32_thread_set_tls(struct win32_tls *ctx) +{ + TlsSetValue(G.thread_tls_index, (LPVOID)ctx); +} + +INTERNAL struct win32_tls *win32_thread_get_tls(void) +{ + return TlsGetValue(G.thread_tls_index); +} + +INTERNAL struct win32_tls win32_tls_alloc(void) +{ + struct win32_tls tls = ZI; + tls.sleep_timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); + tls.store = thread_local_store_alloc(); + return tls; +} + +INTERNAL void win32_tls_release(struct win32_tls *tls) +{ + thread_local_store_release(&tls->store); + CloseHandle(tls->sleep_timer); +} + +struct thread_local_store *sys_thread_get_thread_local_store(void) +{ + struct win32_tls *thread_ctx = (struct win32_tls *)win32_thread_get_tls(); + return &thread_ctx->store; +} + +/* ========================== * + * Threads + * ========================== */ + +INTERNAL struct win32_thread *win32_thread_alloc(void) +{ + struct win32_thread *t = NULL; + struct sys_lock lock = sys_mutex_lock_e(G.threads_mutex); + { + if (G.threads_first_free) { + t = G.threads_first_free; + G.threads_first_free = t->next; + } else { + t = arena_push_no_zero(G.threads_arena, struct win32_thread); + } + MEMZERO_STRUCT(t); + if (!G.threads_first) { + G.threads_first = t; + } else { + G.threads_last->next = t; + } + t->prev = G.threads_last; + G.threads_last = t; + } + sys_mutex_unlock(&lock); + return t; +} + +INTERNAL void win32_thread_release(struct win32_thread *t) +{ + struct sys_lock lock = sys_mutex_lock_e(G.threads_mutex); + { + if (t->prev) { + t->prev->next = t->next; + } + if (t->next) { + t->next->prev = t->prev; + } + if (G.threads_first == t) { + G.threads_first = t->next; + } + if (G.threads_last == t) { + G.threads_last = t->prev; + } + t->next = G.threads_first_free; + } + sys_mutex_unlock(&lock); +} + +INTERNAL DWORD WINAPI win32_thread_proc(LPVOID vt) +{ + struct win32_thread *t = (struct win32_thread *)vt; + __profthread(t->thread_name_cstr); + + /* Initialize COM */ + CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + + /* Initialize TLS */ + struct win32_tls tls = win32_tls_alloc(); + win32_thread_set_tls(&tls); + + /* Set thread name */ + if (t->thread_name_wstr[0] != 0) { + SetThreadDescription(GetCurrentThread(), t->thread_name_wstr); + } + + logf_info("New thread \"%F\" created with ID %F", FMT_STR(string_from_cstr_no_limit(t->thread_name_cstr)), FMT_UINT(sys_thread_id())); + + /* Enter thread entry point */ + t->entry_point(t->thread_data); + + /* Release TLS */ + win32_tls_release(&tls); + + /* Uninitialize COM */ + CoUninitialize(); + + return 0; +} + +struct sys_thread *sys_thread_alloc(sys_thread_func *entry_point, void *thread_data, struct string thread_name) +{ + __prof; + struct arena_temp scratch = scratch_begin_no_conflict(); + ASSERT(entry_point != NULL); + logf_info("Creating thread \"%F\"", FMT_STR(thread_name)); + + + /* Allocate thread object */ + struct win32_thread *t = win32_thread_alloc(); + t->entry_point = entry_point; + t->thread_data = thread_data; + + /* Copy thread name to params */ + { + u64 cstr_len = min_u64((ARRAY_COUNT(t->thread_name_cstr) - 1), thread_name.len); + MEMCPY(t->thread_name_cstr, thread_name.text, cstr_len * sizeof(*t->thread_name_cstr)); + t->thread_name_cstr[cstr_len] = 0; + } + { + struct string16 thread_name16 = string16_from_string(scratch.arena, thread_name); + u64 wstr_len = min_u64((ARRAY_COUNT(t->thread_name_wstr) - 1), thread_name16.len); + MEMCPY(t->thread_name_wstr, thread_name16.text, wstr_len * sizeof(*t->thread_name_wstr)); + t->thread_name_wstr[wstr_len] = 0; + } + + t->handle = CreateThread( + NULL, + SYS_THREAD_STACK_SIZE, + win32_thread_proc, + t, + 0, + NULL + ); + + if (!t->handle) { + sys_panic(LIT("Failed to create thread")); + } + + scratch_end(scratch); + return (struct sys_thread *)t; +} + +void sys_thread_wait_release(struct sys_thread *thread) +{ + __prof; + b32 success = sys_thread_try_release(thread, F32_INFINITY); + ASSERT(success); + (UNUSED)success; +} + +b32 sys_thread_try_release(struct sys_thread *thread, f32 timeout_seconds) +{ + __prof; + b32 success = false; + struct win32_thread *t = (struct win32_thread *)thread; + HANDLE handle = t->handle; + + /* Wait for thread to stop */ + if (handle) { + DWORD timeout_ms = (timeout_seconds == F32_INFINITY) ? INFINITE : math_round_to_int(timeout_seconds * 1000); + DWORD wait_res = WaitForSingleObject(handle, timeout_ms); + if (wait_res == WAIT_OBJECT_0) { + success = true; + CloseHandle(handle); + win32_thread_release(t); + } + } + + return success; +} + +void sys_thread_force_release(struct sys_thread *thread) +{ + __prof; + struct win32_thread *t = (struct win32_thread *)thread; + HANDLE handle = t->handle; + + /* Wait for thread to stop */ + if (handle) { + DWORD res = WaitForSingleObject(handle, INFINITE); + TerminateThread(handle, 0); + CloseHandle(handle); + + ASSERT(res != WAIT_FAILED); + (UNUSED)res; + } + + /* Release thread struct */ + win32_thread_release(t); +} + +u32 sys_thread_id(void) +{ + return GetCurrentThreadId(); +} + +#if RTC +void sys_thread_assert(u32 tid) +{ + ASSERT(sys_thread_id() == tid); +} +#endif + +/* ========================== * + * Message box + * ========================== */ + +void sys_message_box(enum sys_message_box_kind kind, struct string message) +{ + struct arena_temp scratch = scratch_begin_no_conflict(); + + wchar_t *message_wstr = wstr_from_string(scratch.arena, message); + const wchar_t *title = L""; + UINT mbox_type = MB_SETFOREGROUND; + + switch (kind) { + case SYS_MESSAGE_BOX_KIND_OK: { + mbox_type |= MB_ICONINFORMATION; + } break; + + case SYS_MESSAGE_BOX_KIND_WARNING: { + title = L"Warning"; + mbox_type |= MB_ICONWARNING; + } break; + + case SYS_MESSAGE_BOX_KIND_ERROR: { + title = L"Error"; + mbox_type |= MB_ICONERROR; + } break; + + case SYS_MESSAGE_BOX_KIND_FATAL: { + title = L"Fatal error"; + mbox_type |= MB_ICONSTOP; + } break; + } + + logf_info("Showing message box kind %F with text \"%F\"", FMT_SINT(kind), FMT_STR(message)); + MessageBoxExW(NULL, message_wstr, title, mbox_type, 0); + + scratch_end(scratch); +} + +/* ========================== * + * Clipboard + * ========================== */ + +void sys_set_clipboard_text(struct string str) +{ + if (OpenClipboard(0)) { + struct arena_temp scratch = scratch_begin_no_conflict(); + struct string16 str16 = string16_from_string(scratch.arena, str); + u64 str16_size_bytes = str16.len * 2; + EmptyClipboard(); + HANDLE handle = GlobalAlloc(GMEM_MOVEABLE, str16_size_bytes + 1); + if (handle) { + u16 *dest_wstr = (u16 *)GlobalLock(handle); + MEMCPY(dest_wstr, str16.text, str16_size_bytes); + dest_wstr[str16.len] = 0; + GlobalUnlock(handle); + SetClipboardData(CF_UNICODETEXT, handle); + } + CloseClipboard(); + scratch_end(scratch); + } +} + +struct string sys_get_clipboard_text(struct arena *arena) +{ + struct string res = ZI; + if (IsClipboardFormatAvailable(CF_UNICODETEXT) && OpenClipboard(0)) { + HANDLE handle = GetClipboardData(CF_UNICODETEXT); + if (handle) { + u16 *src_wstr = (u16 *)GlobalLock(handle); + res = string_from_string16(arena, string16_from_wstr_no_limit(src_wstr)); + GlobalUnlock(handle); + } + CloseClipboard(); + } + return res; +} + +/* ========================== * + * RNG + * ========================== */ + +void sys_true_rand(struct string b) +{ + BCryptGenRandom(BCRYPT_RNG_ALG_HANDLE, (PUCHAR)b.text, b.len, 0); +} + +u32 sys_num_logical_processors(void) +{ + return GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); +} + +void sys_exit(void) +{ + ExitProcess(1); +} + +void sys_panic(struct string msg) +{ + if (atomic_i32_eval_compare_exchange(&G.panicking, 0, 1) == 0) { + log_panic(msg); + + wchar_t *wstr = G.panic_wstr; + u64 wstr_len = 0; + + wchar_t prefix[] = L"A fatal error has occured and the application needs to exit:\n\n"; + MEMCPY(wstr, prefix, min_u64(ARRAY_COUNT(G.panic_wstr), (ARRAY_COUNT(prefix) << 1))); + wstr_len += ARRAY_COUNT(prefix) - 1; + + /* Perform manual string encode to avoid any implicit memory + * allocation (in case allocation is unreliable) */ + struct string str8 = msg; + u64 pos8 = 0; + while (pos8 < str8.len) { + struct string str8_remaining = { .len = (str8.len - pos8), .text = str8.text + pos8 }; + struct uni_decode_utf8_result decoded = uni_decode_utf8(str8_remaining); + struct uni_encode_utf16_result encoded = uni_encode_utf16(decoded.codepoint); + u64 wstr_new_len = wstr_len + encoded.count16; + if (wstr_new_len < (ARRAY_COUNT(G.panic_wstr) - 1)) { + u16 *dest = wstr + wstr_len; + MEMCPY(dest, encoded.chars16, (encoded.count16 << 1)); + wstr_len = wstr_new_len; + pos8 += decoded.advance8; + } else { + break; + } + } + + wstr[wstr_len] = 0; + +#if RTC + MessageBoxExW(NULL, wstr, L"Fatal error", MB_ICONSTOP | MB_SETFOREGROUND | MB_TOPMOST, 0); + ASSERT(false); +#endif + + WRITE_BARRIER(); + SetEvent(G.panic_event); + + /* Wait for thread to be terminated */ + if (GetCurrentThreadId() != G.main_thread_id) { + Sleep(INFINITE); + } + } +} + +/* ========================== * + * Sleep + * ========================== */ + +/* https://blog.bearcats.nl/perfect-sleep-function/ */ + +INTERNAL void win32_precise_sleep_timer(f64 seconds, HANDLE timer) +{ + __prof; + + /* TODO: Does the high frequency timer even require setting / scaling of + * timeBeginPeriod/scheduler_period_ms? There isn't much documentation. */ + + i64 qpc_per_second = G.qpc_per_second; + i32 scheduler_period_ms = G.scheduler_period_ms; + + LARGE_INTEGER qpc; + QueryPerformanceCounter(&qpc); + i64 target_qpc = (i64)(qpc.QuadPart + seconds * qpc_per_second); + + /* TODO: Maybe increase tolerance for higher precision but more power usage */ + //const f64 tolerance = scheduler_period_ms * 0.001200; + const f64 tolerance = scheduler_period_ms * 0.000520; + //const f64 tolerance = scheduler_period_ms * 1; + + i64 max_ticks = (i64)scheduler_period_ms * 9500; + while (true) { + __profscope(Sleep part); + /* Break sleep up into parts that are lower than scheduler period */ + f64 remaining_seconds = (f64)(target_qpc - qpc.QuadPart) / (f64)qpc_per_second; + i64 sleep_ticks = (i64)((remaining_seconds - tolerance) * 10000000); + if (sleep_ticks <= 0) { + break; + } + LARGE_INTEGER due; + due.QuadPart = -(sleep_ticks > max_ticks ? max_ticks : sleep_ticks); + SetWaitableTimerEx(timer, &due, 0, NULL, NULL, NULL, 0); + WaitForSingleObject(timer, INFINITE); + QueryPerformanceCounter(&qpc); + } + + /* Spin for any remaining time */ + { + __profscope(Sleep spin); + while (qpc.QuadPart < target_qpc) { + YieldProcessor(); + QueryPerformanceCounter(&qpc); + } + } +} + +INTERNAL void win32_precise_sleep_legacy(f64 seconds) +{ + __prof; + i64 qpc_per_second = G.qpc_per_second; + i32 scheduler_period_ms = G.scheduler_period_ms; + + LARGE_INTEGER qpc; + QueryPerformanceCounter(&qpc); + i64 target_qpc = (i64)(qpc.QuadPart + seconds * qpc_per_second); + + /* TODO: Calculate tolerance */ + + /* TODO: Maybe increase tolerance for higher precision but more power usage */ + //const double tolerance = 1.02; + const double tolerance = 0.52 * scheduler_period_ms; + + /* Sleep */ + f64 sleep_ms = (seconds * 1000) - tolerance; + i32 sleep_slices = (i32)(sleep_ms / scheduler_period_ms); + if (sleep_slices > 0) { + __profscope(Legacy sleep part); + Sleep((DWORD)sleep_slices * scheduler_period_ms); + } + QueryPerformanceCounter(&qpc); + + /* Spin for any remaining time */ + { + __profscope(Legacy sleep spin); + while (qpc.QuadPart < target_qpc) { + YieldProcessor(); + QueryPerformanceCounter(&qpc); + } + } +} + +void sys_sleep_precise(f64 seconds) +{ + __prof; + HANDLE timer = win32_thread_get_tls()->sleep_timer; + if (timer) { + /* Use newer sleeping method */ + win32_precise_sleep_timer(seconds, timer); + } else { + /* Fall back to older sleep method if CREATE_WAITABLE_TIMER_HIGH_RESOLUTION + * is not available due to older windows version */ + win32_precise_sleep_legacy(seconds); + } +} + +void sys_sleep(f64 seconds) +{ + __prof; + u32 ms = max_u32(1, math_round_to_int((f32)(seconds * 1000.0))); + Sleep(ms); +} + +/* ========================== * + * Command line + * ========================== */ + +b32 sys_run_command(struct string cmd) +{ + b32 success = false; + { + struct arena_temp scratch = scratch_begin_no_conflict(); + wchar_t *cmd_wstr = wstr_from_string(scratch.arena, cmd); + STARTUPINFO si = ZI; + si.cb = sizeof(si); + PROCESS_INFORMATION pi = ZI; + success = CreateProcessW(NULL, cmd_wstr, NULL, NULL, FALSE, DETACHED_PROCESS, NULL, NULL, &si, &pi); + scratch_end(scratch); + } + return success; +} + +/* ========================== * + * Entry point + * ========================== */ + +INTERNAL SYS_THREAD_DEF(win32_app_thread_entry_point, arg) +{ + (UNUSED)arg; + struct arena_temp scratch = scratch_begin_no_conflict(); + struct string cmdline_args = string_from_wstr(scratch.arena, G.cmdline_args_wstr, ARRAY_COUNT(G.cmdline_args_wstr)); + app_entry_point(cmdline_args); + scratch_end(scratch); +} + +int CALLBACK wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev_instance, _In_ LPWSTR cmdline_wstr, _In_ int show_code) +{ + (UNUSED)instance; + (UNUSED)prev_instance; + (UNUSED)cmdline_wstr; + (UNUSED)show_code; + + u64 cmdline_len = wstr_len(cmdline_wstr, ARRAY_COUNT(G.cmdline_args_wstr) - 1); + MEMCPY(G.cmdline_args_wstr, cmdline_wstr, cmdline_len * sizeof(*cmdline_wstr)); + G.cmdline_args_wstr[cmdline_len] = 0; + + const wchar_t *error_msg = NULL; + + /* ========================== * + * Win32 setup + * ========================== */ + + G.main_thread_id = GetCurrentThreadId(); + SetThreadDescription(GetCurrentThread(), L"Main thread"); + + /* Set up panic event */ + G.panic_event = CreateEventW(NULL, true, false, NULL); + + /* Query system info */ + GetSystemInfo(&G.info); + + LARGE_INTEGER qpf; + QueryPerformanceFrequency(&qpf); + G.qpc_per_second = qpf.QuadPart; + G.ns_per_qpc = 1000000000 / qpf.QuadPart; + + LARGE_INTEGER qpc; + QueryPerformanceCounter(&qpc); + G.timer_start_qpc = qpc.QuadPart; + + TIMECAPS caps; + timeGetDevCaps(&caps, sizeof(caps)); + G.scheduler_period_ms = (i32)caps.wPeriodMin; + + /* Set up timing period */ + timeBeginPeriod(G.scheduler_period_ms); + + /* Setup mutexes */ + G.mutexes_arena = arena_alloc(GIGABYTE(64)); + struct win32_mutex *first_mutex = arena_push(G.mutexes_arena, struct win32_mutex); + win32_mutex_init(first_mutex); + G.mutexes_mutex = (struct sys_mutex *)first_mutex; + + /* Set up condition variables */ + G.condition_variables_mutex = sys_mutex_alloc(); + G.condition_variables_arena = arena_alloc(GIGABYTE(64)); + + /* Set up threads */ + G.threads_mutex = sys_mutex_alloc(); + G.threads_arena = arena_alloc(GIGABYTE(64)); + + /* Set up watches */ + G.watches_mutex = sys_mutex_alloc(); + G.watches_arena = arena_alloc(GIGABYTE(64)); + + /* Set up windows */ + G.windows_mutex = sys_mutex_alloc(); + G.windows_arena = arena_alloc(GIGABYTE(64)); + + /* Set up TLS index */ + G.thread_tls_index = TlsAlloc(); + if (G.thread_tls_index == TLS_OUT_OF_INDEXES) { + /* TODO: GetLastError */ + error_msg = L"Platform initialization error: TLS_OUT_OF_INDEXES"; + goto abort; + } + + /* Initialize vk table */ + win32_init_vk_btn_table(); + + /* Create window class */ + { + /* Register the window class */ + WNDCLASSEXW *wc = &G.window_class; + wc->cbSize = sizeof(WNDCLASSEX); + wc->lpszClassName = WINDOW_CLASS_NAME; + wc->hCursor = LoadCursor(NULL, IDC_ARROW); + wc->style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; + //wc->hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); + wc->lpfnWndProc = win32_window_proc; + wc->hInstance = instance; + + /* Use first icon resource as window icon (same as explorer) */ + wchar_t path[4096] = ZI; + GetModuleFileNameW(instance, path, ARRAY_COUNT(path)); + ExtractIconExW(path, 0, &wc->hIcon, &wc->hIconSm, 1); + + if (!RegisterClassExW(wc)) { + /* TODO: GetLastError */ + error_msg = L"Failed to register window class"; + goto abort; + } + } + + /* Register raw input */ + { + RAWINPUTDEVICE rid = (RAWINPUTDEVICE) { + .usUsagePage = 0x01, /* HID_USAGE_PAGE_GENERIC */ + .usUsage = 0x02, /* HID_USAGE_GENERIC_MOUSE */ + //.dwFlags = RIDEV_NOLEGACY /* Adds mouse and also ignores legacy mouse messages */ + }; + + if (!RegisterRawInputDevices(&rid, 1, sizeof(rid))) { + /* TODO: GetLastError */ + error_msg = L"Failed to register raw input device"; + goto abort; + } + } + + /* ========================== * + * App thread setup + * ========================== */ + + /* Initialize main thread context. */ + struct win32_tls main_thread_tls = win32_tls_alloc(); + win32_thread_set_tls(&main_thread_tls); + + /* Call app thread and wait for return */ + { + /* Start app thread */ + struct sys_thread *app_thread = sys_thread_alloc(&win32_app_thread_entry_point, NULL, LIT("[P0] App thread")); + + /* Get app thread handle */ + HANDLE app_thread_handle = 0; + struct sys_lock lock = sys_mutex_lock_s(G.threads_mutex); + { + struct win32_thread *wt = (struct win32_thread *)app_thread; + app_thread_handle = wt->handle; + } + sys_mutex_unlock(&lock); + + /* Wait for either app thread exit or panic */ + if (app_thread_handle) { + HANDLE wait_handles[] = { + app_thread_handle, + G.panic_event + }; + DWORD res = WaitForMultipleObjects(ARRAY_COUNT(wait_handles), wait_handles, false, INFINITE); + if (res == WAIT_OBJECT_0) { + sys_thread_force_release(app_thread); + } + ASSERT(res != WAIT_FAILED); + (UNUSED)res; + } + } + + /* Find any dangling threads that haven't exited gracefully by now */ + if (!atomic_i32_eval(&G.panicking)) { + struct sys_lock lock = sys_mutex_lock_s(G.threads_mutex); + if (G.threads_first) { + struct arena_temp scratch = scratch_begin_no_conflict(); + u64 num_dangling_threads = 0; + struct string threads_msg = ZI; + threads_msg.text = arena_push_dry(scratch.arena, u8); + for (struct win32_thread *t = G.threads_first; t; t = t->next) { + struct string name = string_from_cstr(t->thread_name_cstr, ARRAY_COUNT(t->thread_name_cstr)); + threads_msg.len += string_format(scratch.arena, LIT(" \"%F\"\n"), FMT_STR(name)).len; + ++num_dangling_threads; + } + threads_msg = string_format(scratch.arena, LIT("%F dangling thread(s):\n%F"), FMT_UINT(num_dangling_threads), FMT_STR(threads_msg)); + sys_panic(threads_msg); + scratch_end(scratch); + } + sys_mutex_unlock(&lock); + } + + /* Check if panicking */ + if (atomic_i32_eval(&G.panicking)) { + /* Wait for panic message to be ready */ + WaitForSingleObject(G.panic_event, INFINITE); + /* Set error and abort */ + error_msg = G.panic_wstr; + goto abort; + } + + /* Release main thread context */ + win32_tls_release(&main_thread_tls); + + /* ========================== * + * Abort + * ========================== */ + + abort: + + if (error_msg) { + MessageBoxExW(NULL, error_msg, L"Fatal error", MB_ICONSTOP | MB_SETFOREGROUND | MB_TOPMOST, 0); + ASSERT(false); + return 1; + } + +#if PROFILING + /* Launch profiler */ + if (!__prof_is_connected()) { + __profscope(Launch profiler); + STARTUPINFO si = ZI; + si.cb = sizeof(si); + PROCESS_INFORMATION pi = ZI; + wchar_t cmd[sizeof(PROFILING_CMD_WSTR)] = ZI; + MEMCPY(cmd, PROFILING_CMD_WSTR, sizeof(PROFILING_CMD_WSTR)); + b32 success = CreateProcessW(NULL, cmd, NULL, NULL, FALSE, DETACHED_PROCESS, NULL, NULL, &si, &pi); + if (success) { + while (!__prof_is_connected()) { + ix_pause(); + } + } else { + MessageBoxExW(NULL, L"Failed to launch tracy profiler using command " PROFILING_CMD_WSTR, L"Error", MB_ICONSTOP | MB_SETFOREGROUND | MB_TOPMOST, 0); + } + } +#endif + + return 0; +} + +/* ========================== * + * CRT Stub + * ========================== */ + +#if !CRTLIB + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wmissing-variable-declarations" +#pragma clang diagnostic ignored "-Wmissing-prototypes" + +/* Enable floating point */ +__attribute((used)) +int _fltused; + +__attribute((used)) +void __stdcall wWinMainCRTStartup(void) +{ + int result = wWinMain(GetModuleHandle(0), 0, GetCommandLineW(), 0); + ExitProcess(result); +} + +#pragma clang diagnostic pop + +#endif /* !CRTLIB */ + +#endif diff --git a/src/sys_win32.c b/src/sys_win32.c index 3ed4a506..0340b518 100644 --- a/src/sys_win32.c +++ b/src/sys_win32.c @@ -1,3 +1,5 @@ +#if FIBERS_TEST + #include "sys.h" #include "memory.h" #include "app.h" @@ -2336,6 +2338,301 @@ b32 sys_run_command(struct string cmd) } return success; } + + + + + + + + + + + + + +/* ========================== * + * Testing + * ========================== */ + +#define FIBER_STACK_SIZE MEGABYTE(4) + +#define TJOB_DEF(name, arg) void name(void *arg) +typedef TJOB_DEF(tjob_func, arg); + +enum fiber_yield_reason { + FIBER_YIELD_REASON_NONE, + + FIBER_YIELD_REASON_SLEEP, + + FIBER_YIELD_REASON_DONE +}; + +struct fiber { + const char *name; + void *addr; + + struct tjob *current_job; + + enum fiber_yield_reason yield_reason; +}; + +struct tjob { + tjob_func *func; + void *arg; + + struct tjob *next; +}; + + + +GLOBAL struct { + void *runner_fiber_addr; + struct fiber f1; + + struct arena *arena; + b32 shutdown; + struct atomic_i32 lock; + struct tjob *first_free_job; + struct tjob *first_job; + struct tjob *last_job; +} g_test; + + + +INTERNAL void atomic_lock(void) +{ + while (atomic_i32_eval_compare_exchange(&g_test.lock, 0, 1) != 0) { + ix_pause(); + } +} + +INTERNAL void atomic_unlock(void) +{ + atomic_i32_eval_exchange(&g_test.lock, 0); +} + +INTERNAL void push_job(tjob_func *func, void *arg) +{ + atomic_lock(); + { + struct tjob *job = NULL; + if (g_test.first_free_job) { + job = g_test.first_free_job; + g_test.first_free_job = job->next; + } else { + job = arena_push_no_zero(g_test.arena, struct tjob); + } + *job = (struct tjob) { .func = func, .arg = arg }; + if (g_test.last_job) { + g_test.last_job->next = job; + } else { + g_test.first_job = job; + } + g_test.last_job = job; + } + atomic_unlock(); +} + + + + + + +INTERNAL void yield(struct fiber *fiber, enum fiber_yield_reason reason) +{ + fiber->yield_reason = reason; + __prof_fiber_leave; + SwitchToFiber(g_test.runner_fiber_addr); + __prof_fiber_enter(fiber->name); +} + +INTERNAL TJOB_DEF(test_job, arg) +{ + __prof; + struct fiber *fiber = arg; + (UNUSED)fiber; + (UNUSED)yield; + +#if 1 + Sleep(50); + yield(fiber, FIBER_YIELD_REASON_SLEEP); + Sleep(50); +#else + Sleep(50); +#endif +} + + + + + + + +INTERNAL void fiber_proc(void *vfiber) +{ + DEBUGBREAKABLE; + struct fiber *fiber = vfiber; + (UNUSED)fiber; + while (true) { + __prof_fiber_enter(fiber->name); + { + fiber->yield_reason = FIBER_YIELD_REASON_NONE; + fiber->current_job->func(fiber->current_job->arg); + fiber->yield_reason = FIBER_YIELD_REASON_DONE; + } + __prof_fiber_leave; + SwitchToFiber(g_test.runner_fiber_addr); + } +} + +struct runner_thread_param { + i32 id; +}; + +INTERNAL DWORD WINAPI runner_thread_proc(LPVOID tparam) +{ + __prof; + struct runner_thread_param *param = tparam; + struct arena *arena = arena_alloc(GIGABYTE(64)); + + /* Set thread name */ + { + struct string id_str = string_from_int(arena, param->id, 10, 1); + struct string name = string_format(arena, LIT("Runner thread %F"), FMT_STR(id_str)); + wchar_t *name_wstr = wstr_from_string(arena, name); + SetThreadDescription(GetCurrentThread(), name_wstr); + } + + g_test.runner_fiber_addr = ConvertThreadToFiber(NULL); + g_test.f1.name = "Fiber 1"; + g_test.f1.addr = CreateFiber(FIBER_STACK_SIZE, fiber_proc, &g_test.f1); + + b32 shutdown = false; + while (!shutdown) { + Sleep(100); + struct arena_temp temp = arena_temp_begin(arena); + + atomic_lock(); + shutdown = g_test.shutdown; + if (!shutdown && g_test.first_job) { + /* Pull job */ + struct tjob local_job = ZI; + { + struct tjob *job = g_test.first_job; + local_job = *job; + struct tjob *next = job->next; + g_test.first_job = next; + if (!next) { + g_test.last_job = NULL; + } + } + /* Run job */ + atomic_unlock(); + { + __profscope(Run job); + struct fiber *fiber = &g_test.f1; + fiber->current_job = &local_job; + fiber->yield_reason = FIBER_YIELD_REASON_NONE; + b32 done = false; + while (!done) { + SwitchToFiber(fiber->addr); + enum fiber_yield_reason yield_reason = fiber->yield_reason; + switch (yield_reason) { + default: break; + + case FIBER_YIELD_REASON_SLEEP: + { + __profscope(Sleep); + Sleep(100); + } break; + + case FIBER_YIELD_REASON_DONE: + { + done = true; + } break; + } + } + } + atomic_lock(); + } + atomic_unlock(); + arena_temp_end(temp); + } + + arena_release(arena); +} + +INTERNAL void test_main(struct string cmdline) +{ + __prof; + (UNUSED)cmdline; + struct arena *arena = arena_alloc(GIGABYTE(64)); + { + g_test.arena = arena_alloc(GIGABYTE(64)); + + Sleep(1000); + for (u32 i = 0; i < 50; ++i) { + push_job(test_job, &g_test.f1); + } + + u32 num_runners = NUM_RUNNERS; + HANDLE *runner_thread_handles = arena_push_array(arena, HANDLE, num_runners); + struct runner_thread_param *tparams = arena_push_array(arena, struct runner_thread_param, num_runners); + for (u32 i = 0; i < num_runners; ++i) { + struct runner_thread_param *tparam = &tparams[i]; + tparam->id = i; + runner_thread_handles[i] = CreateThread(NULL, MEGABYTE(1), runner_thread_proc, tparam, 0, NULL); + //SetThreadDescription(GetCurrentThread(), t->thread_name_wstr); + } + + struct sys_thread *runner_thread = sys_thread_alloc(runner_thread_entry_point, NULL, LIT("Runner thread")); + Sleep(5000); + + { + atomic_lock(); + g_test.shutdown = true; + atomic_unlock(); + } + + for (u32 i = 0; i < num_runners; ++i) { + HANDLE handle = runner_thread_handles[i]; + DWORD wait_res = WaitForSingleObject(handle, INFINITE); + if (wait_res == WAIT_OBJECT_0) { + CloseHandle(handle); + } + } + } + arena_release(arena); +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + /* ========================== * * Entry point @@ -2346,7 +2643,12 @@ INTERNAL SYS_THREAD_DEF(win32_app_thread_entry_point, arg) (UNUSED)arg; struct arena_temp scratch = scratch_begin_no_conflict(); struct string cmdline_args = string_from_wstr(scratch.arena, G.cmdline_args_wstr, ARRAY_COUNT(G.cmdline_args_wstr)); +#if 0 + (UNUSED)test_main; app_entry_point(cmdline_args); +#else + test_main(cmdline_args); +#endif scratch_end(scratch); } @@ -2592,3 +2894,6 @@ void __stdcall wWinMainCRTStartup(void) #pragma clang diagnostic pop #endif /* !CRTLIB */ + + +#endif