3774 lines
117 KiB
C
3774 lines
117 KiB
C
P_W32_SharedCtx P_W32_shared_ctx = ZI;
|
|
|
|
////////////////////////////////
|
|
//~ Win32 libs
|
|
|
|
#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")
|
|
#pragma comment(lib, "synchronization")
|
|
#pragma comment(lib, "avrt")
|
|
#pragma comment(lib, "ws2_32.lib")
|
|
|
|
////////////////////////////////
|
|
//~ Win32 ticket mutex
|
|
|
|
void P_W32_LockTicketMutex(P_W32_TicketMutex *tm)
|
|
{
|
|
i64 ticket = Atomic64FetchAdd(&tm->ticket.v, 1);
|
|
while (Atomic64Fetch(&tm->serving.v) != ticket)
|
|
{
|
|
IxPause();
|
|
}
|
|
}
|
|
|
|
void P_W32_UnlockTicketMutex(P_W32_TicketMutex *tm)
|
|
{
|
|
Atomic64FetchAdd(&tm->serving.v, 1);
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Win32 thread
|
|
|
|
DWORD WINAPI P_W32_Win32ThreadProc(LPVOID vt)
|
|
{
|
|
P_W32_AllocFiber(0);
|
|
|
|
P_W32_Thread *t = (P_W32_Thread *)vt;
|
|
__profthread(t->thread_name_cstr, t->profiler_group);
|
|
|
|
/* Initialize COM */
|
|
CoInitializeEx(0, COINIT_MULTITHREADED);
|
|
|
|
/* Set thread name */
|
|
if (t->thread_name_wstr[0] != 0)
|
|
{
|
|
SetThreadDescription(GetCurrentThread(), t->thread_name_wstr);
|
|
}
|
|
|
|
P_LogInfoF("New thread \"%F\" created with ID %F", FmtString(StringFromCstrNoLimit(t->thread_name_cstr)), FmtUint(P_GetThreadId()));
|
|
|
|
/* Enter thread entry point */
|
|
t->entry_point(t->thread_data);
|
|
|
|
/* Uninitialize COM */
|
|
CoUninitialize();
|
|
|
|
return 0;
|
|
}
|
|
|
|
P_W32_Thread *P_W32_AllocThread(P_W32_ThreadFunc *entry_point, void *thread_data, String thread_name, i32 profiler_group)
|
|
{
|
|
__prof;
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
Assert(entry_point != 0);
|
|
P_LogInfoF("Creating thread \"%F\"", FmtString(thread_name));
|
|
|
|
|
|
/* Allocate thread object */
|
|
P_W32_Thread *t = 0;
|
|
{
|
|
P_Lock lock = P_LockE(&g->threads_mutex);
|
|
if (g->first_free_thread)
|
|
{
|
|
t = g->first_free_thread;
|
|
g->first_free_thread = t->next;
|
|
}
|
|
else
|
|
{
|
|
t = PushStructNoZero(g->threads_arena, P_W32_Thread);
|
|
}
|
|
ZeroStruct(t);
|
|
if (g->last_thread)
|
|
{
|
|
g->last_thread->next = t;
|
|
t->prev = g->last_thread;
|
|
}
|
|
else
|
|
{
|
|
g->first_thread = t;
|
|
}
|
|
g->last_thread = t;
|
|
P_Unlock(&lock);
|
|
}
|
|
|
|
|
|
t->entry_point = entry_point;
|
|
t->thread_data = thread_data;
|
|
t->profiler_group = profiler_group;
|
|
|
|
/* CopyStruct thread name to params */
|
|
{
|
|
u64 CstrLen = MinU64((countof(t->thread_name_cstr) - 1), thread_name.len);
|
|
CopyBytes(t->thread_name_cstr, thread_name.text, CstrLen * sizeof(*t->thread_name_cstr));
|
|
t->thread_name_cstr[CstrLen] = 0;
|
|
}
|
|
{
|
|
String16 thread_name16 = String16FromString(scratch.arena, thread_name);
|
|
u64 WstrLen = MinU64((countof(t->thread_name_wstr) - 1), thread_name16.len);
|
|
CopyBytes(t->thread_name_wstr, thread_name16.text, WstrLen * sizeof(*t->thread_name_wstr));
|
|
t->thread_name_wstr[WstrLen] = 0;
|
|
}
|
|
|
|
t->handle = CreateThread(
|
|
0,
|
|
P_W32_ThreadStackSize,
|
|
P_W32_Win32ThreadProc,
|
|
t,
|
|
0,
|
|
0
|
|
);
|
|
|
|
if (!t->handle)
|
|
{
|
|
P_Panic(Lit("Failed to create thread"));
|
|
}
|
|
|
|
EndScratch(scratch);
|
|
return (P_W32_Thread *)t;
|
|
}
|
|
|
|
/* Returns 0 if the thread could not release in specified timeout (e.g. because it is still running) */
|
|
b32 P_W32_TryReleaseThread(P_W32_Thread *thread, f32 timeout_seconds)
|
|
{
|
|
__prof;
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
b32 success = 0;
|
|
P_W32_Thread *t = (P_W32_Thread *)thread;
|
|
HANDLE handle = t->handle;
|
|
if (handle)
|
|
{
|
|
/* Wait for thread to stop */
|
|
DWORD timeout_ms = (timeout_seconds > 10000000) ? INFINITE : RoundF32ToI32(timeout_seconds * 1000);
|
|
DWORD wait_res = WaitForSingleObject(handle, timeout_ms);
|
|
if (wait_res == WAIT_OBJECT_0)
|
|
{
|
|
/* Release thread */
|
|
success = 1;
|
|
CloseHandle(handle);
|
|
{
|
|
P_Lock lock = P_LockE(&g->threads_mutex);
|
|
{
|
|
P_W32_Thread *prev = t->prev;
|
|
P_W32_Thread *next = t->next;
|
|
if (prev)
|
|
{
|
|
prev->next = next;
|
|
}
|
|
else
|
|
{
|
|
g->first_thread = next;
|
|
}
|
|
if (next)
|
|
{
|
|
next->prev = prev;
|
|
}
|
|
else
|
|
{
|
|
g->last_thread = prev;
|
|
}
|
|
t->next = g->first_free_thread;
|
|
g->first_free_thread = t;
|
|
}
|
|
P_Unlock(&lock);
|
|
}
|
|
}
|
|
}
|
|
return success;
|
|
}
|
|
|
|
void P_W32_WaitReleaseThread(P_W32_Thread *thread)
|
|
{
|
|
__prof;
|
|
b32 success = P_W32_TryReleaseThread(thread, F32Infinity);
|
|
Assert(success);
|
|
(UNUSED)success;
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Win32 wait list
|
|
|
|
/* REQUIRED: Caller must have acquired `wake_lock` for each fiber in array */
|
|
void P_W32_WakeLockedFibers(i32 num_fibers, P_W32_Fiber **fibers)
|
|
{
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
|
|
/* Update wait lists */
|
|
for (i32 i = 0; i < num_fibers; ++i)
|
|
{
|
|
P_W32_Fiber *fiber = fibers[i];
|
|
u64 wait_addr = fiber->wait_addr;
|
|
u64 wait_time = fiber->wait_time;
|
|
|
|
/* Lock & search wait bins */
|
|
/* TODO: Cache these in parameters since caller has one of them already calculated */
|
|
P_W32_WaitBin *wait_addr_bin = 0;
|
|
P_W32_WaitBin *wait_time_bin = 0;
|
|
P_W32_WaitList *wait_addr_list = 0;
|
|
P_W32_WaitList *wait_time_list = 0;
|
|
if (wait_addr != 0)
|
|
{
|
|
wait_addr_bin = &g->wait_addr_bins[wait_addr % P_W32_NumWaitAddrBins];
|
|
P_W32_LockTicketMutex(&wait_addr_bin->lock);
|
|
for (P_W32_WaitList *tmp = wait_addr_bin->first_wait_list; tmp && !wait_addr_list; tmp = tmp->next_in_bin)
|
|
{
|
|
if (tmp->value == (u64)wait_addr)
|
|
{
|
|
wait_addr_list = tmp;
|
|
}
|
|
}
|
|
}
|
|
if (wait_time != 0)
|
|
{
|
|
wait_time_bin = &g->wait_time_bins[wait_time % P_W32_NumWaitTimeBins];
|
|
P_W32_LockTicketMutex(&wait_time_bin->lock);
|
|
for (P_W32_WaitList *tmp = wait_time_bin->first_wait_list; tmp && !wait_time_list; tmp = tmp->next_in_bin)
|
|
{
|
|
if (tmp->value == (u64)wait_time)
|
|
{
|
|
wait_time_list = tmp;
|
|
}
|
|
}
|
|
}
|
|
{
|
|
/* Remove from addr list */
|
|
if (wait_addr_list)
|
|
{
|
|
if (--wait_addr_list->num_waiters == 0)
|
|
{
|
|
/* Free addr list */
|
|
P_W32_WaitList *prev = wait_addr_list->prev_in_bin;
|
|
P_W32_WaitList *next = wait_addr_list->next_in_bin;
|
|
if (prev)
|
|
{
|
|
prev->next_in_bin = next;
|
|
}
|
|
else
|
|
{
|
|
wait_addr_bin->first_wait_list = next;
|
|
}
|
|
if (next)
|
|
{
|
|
next->prev_in_bin = prev;
|
|
}
|
|
else
|
|
{
|
|
wait_addr_bin->last_wait_list = prev;
|
|
}
|
|
wait_addr_list->next_in_bin = wait_addr_bin->first_free_wait_list;
|
|
wait_addr_bin->first_free_wait_list = wait_addr_list;
|
|
}
|
|
else
|
|
{
|
|
i16 prev_id = fiber->prev_addr_waiter;
|
|
i16 next_id = fiber->next_addr_waiter;
|
|
if (prev_id)
|
|
{
|
|
P_W32_FiberFromId(prev_id)->next_addr_waiter = next_id;
|
|
}
|
|
else
|
|
{
|
|
wait_addr_list->first_waiter = next_id;
|
|
}
|
|
if (next_id)
|
|
{
|
|
P_W32_FiberFromId(next_id)->prev_addr_waiter = prev_id;
|
|
}
|
|
else
|
|
{
|
|
wait_addr_list->last_waiter = prev_id;
|
|
}
|
|
}
|
|
fiber->wait_addr = 0;
|
|
fiber->prev_addr_waiter = 0;
|
|
fiber->next_addr_waiter = 0;
|
|
}
|
|
/* Remove from time list */
|
|
if (wait_time_list)
|
|
{
|
|
if (--wait_time_list->num_waiters == 0)
|
|
{
|
|
/* Free time list */
|
|
P_W32_WaitList *prev = wait_time_list->prev_in_bin;
|
|
P_W32_WaitList *next = wait_time_list->next_in_bin;
|
|
if (prev)
|
|
{
|
|
prev->next_in_bin = next;
|
|
}
|
|
else
|
|
{
|
|
wait_time_bin->first_wait_list = next;
|
|
}
|
|
if (next)
|
|
{
|
|
next->prev_in_bin = prev;
|
|
}
|
|
else
|
|
{
|
|
wait_time_bin->last_wait_list = prev;
|
|
}
|
|
wait_time_list->next_in_bin = wait_time_bin->first_free_wait_list;
|
|
wait_time_bin->first_free_wait_list = wait_time_list;
|
|
}
|
|
else
|
|
{
|
|
i16 prev_id = fiber->prev_time_waiter;
|
|
i16 next_id = fiber->next_time_waiter;
|
|
if (prev_id)
|
|
{
|
|
P_W32_FiberFromId(prev_id)->next_time_waiter = next_id;
|
|
}
|
|
else
|
|
{
|
|
wait_time_list->first_waiter = next_id;
|
|
}
|
|
if (next_id)
|
|
{
|
|
P_W32_FiberFromId(next_id)->prev_time_waiter = prev_id;
|
|
}
|
|
else
|
|
{
|
|
wait_time_list->last_waiter = prev_id;
|
|
}
|
|
}
|
|
fiber->wait_time = 0;
|
|
fiber->prev_time_waiter = 0;
|
|
fiber->next_time_waiter = 0;
|
|
}
|
|
/* Unlock fiber */
|
|
Atomic32FetchSet(&fiber->wake_lock, 0);
|
|
}
|
|
/* Unlock wait bins */
|
|
if (wait_time_bin != 0) P_W32_UnlockTicketMutex(&wait_time_bin->lock);
|
|
if (wait_addr_bin != 0) P_W32_UnlockTicketMutex(&wait_addr_bin->lock);
|
|
}
|
|
|
|
/* Resume jobs */
|
|
/* TODO: Batch submit waiters based on queue kind rather than one at a time */
|
|
i32 job_counts_per_pool[P_Pool_Count] = ZI;
|
|
for (i32 i = 0; i < num_fibers; ++i)
|
|
{
|
|
P_W32_Fiber *fiber = fibers[i];
|
|
P_Pool pool_kind = fiber->job_pool;
|
|
++job_counts_per_pool[pool_kind];
|
|
P_W32_JobPool *pool = &g->job_pools[pool_kind];
|
|
P_W32_JobQueue *queue = &pool->job_queues[fiber->job_priority];
|
|
P_W32_LockTicketMutex(&queue->lock);
|
|
{
|
|
P_W32_JobInfo *info = 0;
|
|
if (queue->first_free)
|
|
{
|
|
info = queue->first_free;
|
|
queue->first_free = info->next;
|
|
}
|
|
else
|
|
{
|
|
info = PushStructNoZero(queue->arena, P_W32_JobInfo);
|
|
}
|
|
ZeroStruct(info);
|
|
info->count = 1;
|
|
info->num_dispatched = fiber->job_id;
|
|
info->func = fiber->job_func;
|
|
info->sig = fiber->job_sig;
|
|
info->counter = fiber->job_counter;
|
|
info->fiber_id = fiber->id;
|
|
if (queue->first)
|
|
{
|
|
info->next = queue->first;
|
|
}
|
|
else
|
|
{
|
|
queue->last = info;
|
|
}
|
|
queue->first = info;
|
|
}
|
|
P_W32_UnlockTicketMutex(&queue->lock);
|
|
}
|
|
|
|
/* Wake workers */
|
|
if (num_fibers > 0)
|
|
{
|
|
for (P_Pool pool_kind = 0; pool_kind < (i32)countof(job_counts_per_pool); ++pool_kind)
|
|
{
|
|
i32 job_count = job_counts_per_pool[pool_kind];
|
|
if (job_count > 0)
|
|
{
|
|
P_W32_JobPool *pool = &g->job_pools[pool_kind];
|
|
P_W32_LockTicketMutex(&pool->workers_wake_lock);
|
|
{
|
|
Atomic64FetchAdd(&pool->num_jobs_in_queue.v, job_count);
|
|
if (job_count >= P_W32_WakeAllThreshold)
|
|
{
|
|
WakeByAddressAll(&pool->num_jobs_in_queue);
|
|
}
|
|
else
|
|
{
|
|
for (i32 i = 0; i < job_count; ++i)
|
|
{
|
|
WakeByAddressSingle(&pool->num_jobs_in_queue);
|
|
}
|
|
}
|
|
}
|
|
P_W32_UnlockTicketMutex(&pool->workers_wake_lock);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void P_W32_WakeByAddress(void *addr, i32 count)
|
|
{
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
|
|
u64 wait_addr_bin_index = (u64)addr % P_W32_NumWaitAddrBins;
|
|
P_W32_WaitBin *wait_addr_bin = &g->wait_addr_bins[wait_addr_bin_index];
|
|
P_W32_WaitList *wait_addr_list = 0;
|
|
|
|
/* Get list of waiting fibers */
|
|
i32 num_fibers = 0;
|
|
P_W32_Fiber **fibers = 0;
|
|
{
|
|
P_W32_LockTicketMutex(&wait_addr_bin->lock);
|
|
{
|
|
/* Search for wait addr list */
|
|
for (P_W32_WaitList *tmp = wait_addr_bin->first_wait_list; tmp && !wait_addr_list; tmp = tmp->next_in_bin)
|
|
{
|
|
if (tmp->value == (u64)addr)
|
|
{
|
|
wait_addr_list = tmp;
|
|
}
|
|
}
|
|
|
|
/* Lock fibers & build array */
|
|
if (wait_addr_list)
|
|
{
|
|
fibers = PushStructsNoZero(scratch.arena, P_W32_Fiber *, wait_addr_list->num_waiters);
|
|
for (P_W32_Fiber *fiber = P_W32_FiberFromId(wait_addr_list->first_waiter); fiber && num_fibers < count; fiber = P_W32_FiberFromId(fiber->next_addr_waiter))
|
|
{
|
|
if (Atomic32FetchTestSet(&fiber->wake_lock, 0, 1) == 0)
|
|
{
|
|
fibers[num_fibers] = fiber;
|
|
++num_fibers;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
P_W32_UnlockTicketMutex(&wait_addr_bin->lock);
|
|
}
|
|
|
|
if (num_fibers > 0)
|
|
{
|
|
P_W32_WakeLockedFibers(num_fibers, fibers);
|
|
}
|
|
|
|
/* Wake win32 blocking thread waiters */
|
|
if (count >= P_W32_WakeAllThreshold)
|
|
{
|
|
WakeByAddressAll(addr);
|
|
}
|
|
else
|
|
{
|
|
for (i32 i = 0; i < count; ++i)
|
|
{
|
|
WakeByAddressSingle(addr);
|
|
}
|
|
}
|
|
|
|
EndScratch(scratch);
|
|
}
|
|
|
|
void P_W32_WakeByTime(u64 time)
|
|
{
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
|
|
u64 wait_time_bin_index = (u64)time % P_W32_NumWaitTimeBins;
|
|
P_W32_WaitBin *wait_time_bin = &g->wait_time_bins[wait_time_bin_index];
|
|
P_W32_WaitList *wait_time_list = 0;
|
|
|
|
/* Build list of waiters to resume */
|
|
i32 num_fibers = 0;
|
|
P_W32_Fiber **fibers = 0;
|
|
{
|
|
P_W32_LockTicketMutex(&wait_time_bin->lock);
|
|
{
|
|
/* Search for wait time list */
|
|
for (P_W32_WaitList *tmp = wait_time_bin->first_wait_list; tmp && !wait_time_list; tmp = tmp->next_in_bin)
|
|
{
|
|
if (tmp->value == (u64)time)
|
|
{
|
|
wait_time_list = tmp;
|
|
}
|
|
}
|
|
|
|
if (wait_time_list)
|
|
{
|
|
/* Set waiter wake status & build fibers list */
|
|
fibers = PushStructsNoZero(scratch.arena, P_W32_Fiber *, wait_time_list->num_waiters);
|
|
for (P_W32_Fiber *fiber = P_W32_FiberFromId(wait_time_list->first_waiter); fiber; fiber = P_W32_FiberFromId(fiber->next_time_waiter))
|
|
{
|
|
if (Atomic32FetchTestSet(&fiber->wake_lock, 0, 1) == 0)
|
|
{
|
|
fibers[num_fibers] = fiber;
|
|
++num_fibers;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
P_W32_UnlockTicketMutex(&wait_time_bin->lock);
|
|
}
|
|
|
|
P_W32_WakeLockedFibers(num_fibers, fibers);
|
|
|
|
EndScratch(scratch);
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Win32 fiber
|
|
|
|
/* If `pool` is 0, then the currently running thread will be converted into a fiber */
|
|
P_W32_Fiber *P_W32_AllocFiber(P_W32_JobPool *pool)
|
|
{
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
i16 fiber_id = 0;
|
|
P_W32_Fiber *fiber = 0;
|
|
char *new_name_cstr = 0;
|
|
{
|
|
if (pool != 0)
|
|
{
|
|
P_W32_LockTicketMutex(&pool->free_fibers_lock);
|
|
if (pool->first_free_fiber_id)
|
|
{
|
|
fiber_id = pool->first_free_fiber_id;
|
|
fiber = &g->fibers[fiber_id];
|
|
pool->first_free_fiber_id = fiber->parent_id;
|
|
}
|
|
P_W32_UnlockTicketMutex(&pool->free_fibers_lock);
|
|
}
|
|
if (!fiber_id)
|
|
{
|
|
P_W32_LockTicketMutex(&g->fibers_lock);
|
|
{
|
|
{
|
|
fiber_id = g->num_fibers++;
|
|
if (fiber_id >= MaxFibers)
|
|
{
|
|
P_Panic(Lit("Max fibers reached"));
|
|
}
|
|
fiber = &g->fibers[fiber_id];
|
|
new_name_cstr = PushStructs(g->fiber_names_arena, char, P_W32_FiberNameMaxSize);
|
|
}
|
|
}
|
|
P_W32_UnlockTicketMutex(&g->fibers_lock);
|
|
}
|
|
|
|
}
|
|
if (new_name_cstr != 0)
|
|
{
|
|
__profn("Initialize fiber");
|
|
fiber->id = fiber_id;
|
|
|
|
/* Id to ASCII */
|
|
i32 id_div = fiber_id;
|
|
char id_chars[64] = ZI;
|
|
i32 id_chars_len = 0;
|
|
do
|
|
{
|
|
i32 digit = id_div % 10;
|
|
id_div /= 10;
|
|
id_chars[id_chars_len] = ("0123456789")[digit];
|
|
++id_chars_len;
|
|
} while (id_div > 0);
|
|
i32 rev_start = 0;
|
|
i32 rev_end = id_chars_len - 1;
|
|
while (rev_start < rev_end)
|
|
{
|
|
char swp = id_chars[rev_start];
|
|
id_chars[rev_start] = id_chars[rev_end];
|
|
id_chars[rev_end] = swp;
|
|
++rev_start;
|
|
--rev_end;
|
|
}
|
|
|
|
/* Concat fiber name */
|
|
i32 name_size = 1;
|
|
Assert(sizeof(sizeof(P_W32_FiberNamePrefixCstr)) <= P_W32_FiberNameMaxSize);
|
|
CopyBytes(new_name_cstr, P_W32_FiberNamePrefixCstr, sizeof(P_W32_FiberNamePrefixCstr));
|
|
name_size += sizeof(P_W32_FiberNamePrefixCstr) - 2;
|
|
CopyBytes(new_name_cstr + name_size, id_chars, id_chars_len);
|
|
name_size += id_chars_len;
|
|
CopyBytes(new_name_cstr + name_size, P_W32_FiberNameSuffixCstr, sizeof(P_W32_FiberNameSuffixCstr));
|
|
name_size += sizeof(P_W32_FiberNameSuffixCstr) - 2;
|
|
|
|
fiber->name_cstr = new_name_cstr;
|
|
|
|
/* Init win32 fiber */
|
|
if (pool != 0)
|
|
{
|
|
__profn("CreateFiber");
|
|
fiber->addr = CreateFiber(P_W32_FiberStackSize, P_W32_FiberEntryPoint, (void *)(i64)fiber_id);
|
|
}
|
|
else
|
|
{
|
|
/* Fiber is not a part of a job pool, convert thread to fiber */
|
|
__profn("ConvertThreadToFiber");
|
|
fiber->addr = ConvertThreadToFiber((void *)(i64)fiber_id);
|
|
}
|
|
}
|
|
fiber->wait_addr = 0;
|
|
fiber->wait_time = 0;
|
|
fiber->prev_addr_waiter = 0;
|
|
fiber->next_addr_waiter = 0;
|
|
fiber->prev_time_waiter = 0;
|
|
fiber->next_time_waiter = 0;
|
|
fiber->job_func = 0;
|
|
fiber->job_sig = 0;
|
|
fiber->job_id = 0;
|
|
fiber->job_pool = 0;
|
|
fiber->job_priority = 0;
|
|
fiber->job_counter = 0;
|
|
fiber->yield_param = 0;
|
|
fiber->parent_id = 0;
|
|
return fiber;
|
|
}
|
|
|
|
void P_W32_ReleaseFiber(P_W32_JobPool *pool, P_W32_Fiber *fiber)
|
|
{
|
|
P_W32_LockTicketMutex(&pool->free_fibers_lock);
|
|
{
|
|
i16 fiber_id = fiber->id;
|
|
fiber->parent_id = pool->first_free_fiber_id;
|
|
pool->first_free_fiber_id = fiber_id;
|
|
}
|
|
P_W32_UnlockTicketMutex(&pool->free_fibers_lock);
|
|
}
|
|
|
|
ForceInline P_W32_Fiber *P_W32_FiberFromId(i16 id)
|
|
{
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
if (id <= 0)
|
|
{
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
return &g->fibers[id];
|
|
}
|
|
}
|
|
|
|
ForceNoInline void P_W32_FiberResume(P_W32_Fiber *fiber)
|
|
{
|
|
MemoryBarrier();
|
|
SwitchToFiber(fiber->addr);
|
|
MemoryBarrier();
|
|
}
|
|
|
|
void P_W32_YieldFiber(P_W32_Fiber *fiber, P_W32_Fiber *parent_fiber)
|
|
{
|
|
(UNUSED)fiber;
|
|
Assert(fiber->id == FiberId());
|
|
Assert(parent_fiber->id == fiber->parent_id);
|
|
Assert(parent_fiber->id > 0);
|
|
{
|
|
__prof_fiber_leave();
|
|
P_W32_FiberResume(parent_fiber);
|
|
__prof_fiber_enter(fiber->name_cstr, PROF_THREAD_GROUP_FIBERS - Mebi(fiber->job_pool) + Kibi(1) + fiber->id);
|
|
}
|
|
}
|
|
|
|
void P_W32_FiberEntryPoint(void *id_ptr)
|
|
{
|
|
i16 id = (i32)(i64)id_ptr;
|
|
volatile P_W32_Fiber *fiber = P_W32_FiberFromId(id);
|
|
__prof_fiber_enter(fiber->name_cstr, PROF_THREAD_GROUP_FIBERS - Mebi(fiber->job_pool) + Kibi(1) + fiber->id);
|
|
for (;;)
|
|
{
|
|
/* Run job */
|
|
{
|
|
P_W32_YieldParam *yield_param = fiber->yield_param;
|
|
yield_param->kind = P_W32_YieldKind_None;
|
|
P_JobData data = ZI;
|
|
data.id = fiber->job_id;
|
|
data.sig = fiber->job_sig;
|
|
{
|
|
MemoryBarrier();
|
|
fiber->job_func(data);
|
|
MemoryBarrier();
|
|
}
|
|
}
|
|
/* Job completed, yield */
|
|
{
|
|
/* Decrement job counter */
|
|
P_Counter *job_counter = fiber->job_counter;
|
|
if (job_counter)
|
|
{
|
|
P_CounterAdd(job_counter, -1);
|
|
}
|
|
/* Yield to worker */
|
|
fiber->yield_param->kind = P_W32_YieldKind_Done;
|
|
P_W32_Fiber *parent_fiber = P_W32_FiberFromId(fiber->parent_id);
|
|
P_W32_YieldFiber((P_W32_Fiber *)fiber, parent_fiber);
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Win32 job worker
|
|
|
|
P_W32_ThreadDef(P_W32_JobWorkerEntryFunc, worker_ctx_arg)
|
|
{
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
P_W32_WorkerCtx *ctx = worker_ctx_arg;
|
|
P_Pool pool_kind = ctx->pool_kind;
|
|
P_W32_JobPool *pool = &g->job_pools[pool_kind];
|
|
(UNUSED)ctx;
|
|
|
|
{
|
|
/* TODO: Heuristic pinning */
|
|
/* TODO: Pin non-worker threads to other cores */
|
|
HANDLE thread_handle = GetCurrentThread();
|
|
|
|
if (pool->thread_priority)
|
|
{
|
|
__profn("Set priority");
|
|
b32 success = SetThreadPriority(thread_handle, pool->thread_priority) != 0;
|
|
Assert(success);
|
|
(UNUSED)success;
|
|
}
|
|
|
|
#if 0
|
|
if (pool->thread_affinity_mask)
|
|
{
|
|
__profn("Set affinity");
|
|
b32 success = SetThreadAffinityMask(thread_handle, pool->thread_affinity_mask) != 0;
|
|
#if RtcIsEnabled || ProfilingIsEnabled
|
|
{
|
|
/* Retry until external tools can set correct process affinity */
|
|
i32 delay_ms = 16;
|
|
while (!success && delay_ms <= 1024)
|
|
{
|
|
__profn("Affinity retry");
|
|
Sleep(delay_ms);
|
|
success = SetThreadAffinityMask(thread_handle, pool->thread_affinity_mask) != 0;
|
|
delay_ms *= 2;
|
|
}
|
|
}
|
|
#endif
|
|
Assert(success);
|
|
(UNUSED)success;
|
|
}
|
|
#endif
|
|
|
|
if (pool->thread_is_audio)
|
|
{
|
|
/* https://learn.microsoft.com/en-us/windows/win32/procthread/multimedia-class-scheduler-service#registry-settings */
|
|
__profn("Set mm thread characteristics");
|
|
DWORD task = 0;
|
|
HANDLE mmc_handle = AvSetMmThreadCharacteristics(L"Pro Audio", &task);
|
|
Assert(mmc_handle);
|
|
(UNUSED)mmc_handle;
|
|
}
|
|
}
|
|
|
|
i32 worker_fiber_id = FiberId();
|
|
|
|
P_W32_Fiber *job_fiber = 0;
|
|
b32 shutdown = 0;
|
|
while (!shutdown)
|
|
{
|
|
/* Pull job from queue */
|
|
P_Priority job_priority = 0;
|
|
i16 job_fiber_id = 0;
|
|
i32 job_id = 0;
|
|
P_JobFunc *job_func = 0;
|
|
void *job_sig = 0;
|
|
P_Counter *job_counter = 0;
|
|
{
|
|
//__profnc("Pull job", Rgb32F(0.75, 0.75, 0));
|
|
for (P_Priority priority = 0; priority < (i32)countof(pool->job_queues) && !job_func; ++priority)
|
|
{
|
|
P_W32_JobQueue *queue = &pool->job_queues[priority];
|
|
if (queue)
|
|
{
|
|
P_W32_LockTicketMutex(&queue->lock);
|
|
{
|
|
P_W32_JobInfo *info = queue->first;
|
|
while (info && !job_func)
|
|
{
|
|
P_W32_JobInfo *next = info->next;
|
|
b32 dequeue = 0;
|
|
if (info->fiber_id <= 0)
|
|
{
|
|
job_id = info->num_dispatched++;
|
|
if (job_id < info->count)
|
|
{
|
|
/* Pick job */
|
|
Atomic64FetchAdd(&pool->num_jobs_in_queue.v, -1);
|
|
job_priority = priority;
|
|
job_func = info->func;
|
|
job_sig = info->sig;
|
|
job_counter = info->counter;
|
|
if (job_id == (info->count - 1))
|
|
{
|
|
/* We're picking up the last dispatch, so dequeue the job */
|
|
dequeue = 1;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* This job is to be resumed from a yield */
|
|
Atomic64FetchAdd(&pool->num_jobs_in_queue.v, -1);
|
|
job_fiber_id = info->fiber_id;
|
|
job_priority = priority;
|
|
job_id = info->num_dispatched;
|
|
job_func = info->func;
|
|
job_sig = info->sig;
|
|
job_counter = info->counter;
|
|
dequeue = 1;
|
|
}
|
|
if (dequeue)
|
|
{
|
|
if (!next)
|
|
{
|
|
queue->last = 0;
|
|
}
|
|
queue->first = next;
|
|
info->next = queue->first_free;
|
|
queue->first_free = info;
|
|
}
|
|
info = next;
|
|
}
|
|
}
|
|
P_W32_UnlockTicketMutex(&queue->lock);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Use resumed fiber if present */
|
|
if (job_fiber_id > 0)
|
|
{
|
|
if (job_fiber)
|
|
{
|
|
P_W32_ReleaseFiber(pool, job_fiber);
|
|
}
|
|
job_fiber = P_W32_FiberFromId(job_fiber_id);
|
|
}
|
|
|
|
/* Run fiber */
|
|
if (job_func)
|
|
{
|
|
if (!job_fiber)
|
|
{
|
|
job_fiber = P_W32_AllocFiber(pool);
|
|
}
|
|
job_fiber_id = job_fiber->id;
|
|
{
|
|
__profnc("Run fiber", Rgb32F(1, 1, 1));
|
|
__profvalue(job_fiber->id);
|
|
P_W32_YieldParam yield = ZI;
|
|
job_fiber->parent_id = worker_fiber_id;
|
|
job_fiber->job_func = job_func;
|
|
job_fiber->job_sig = job_sig;
|
|
job_fiber->job_id = job_id;
|
|
job_fiber->job_pool = pool_kind;
|
|
job_fiber->job_priority = job_priority;
|
|
job_fiber->job_counter = job_counter;
|
|
job_fiber->yield_param = &yield;
|
|
b32 done = 0;
|
|
while (!done)
|
|
{
|
|
P_W32_FiberResume(job_fiber);
|
|
switch (yield.kind)
|
|
{
|
|
default:
|
|
{
|
|
/* Invalid yield kind */
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
P_Panic(StringFormat(scratch.arena, Lit("Invalid fiber yield kind \"%F\""), FmtSint(yield.kind)));
|
|
EndScratch(scratch);
|
|
} break;
|
|
|
|
case P_W32_YieldKind_Wait:
|
|
{
|
|
__profn("Process fiber wait");
|
|
volatile void *wait_addr = yield.wait.addr;
|
|
void *wait_cmp = yield.wait.cmp;
|
|
u32 wait_size = yield.wait.size;
|
|
i64 wait_timeout_ns = yield.wait.timeout_ns;
|
|
i64 wait_time = 0;
|
|
if (wait_timeout_ns > 0 && wait_timeout_ns < I64Max)
|
|
{
|
|
u64 current_scheduler_cycle = Atomic64Fetch(&g->current_scheduler_cycle.v);
|
|
i64 current_scheduler_cycle_period_ns = Atomic64Fetch(&g->current_scheduler_cycle_period_ns.v);
|
|
wait_time = current_scheduler_cycle + MaxI64((i64)((f64)wait_timeout_ns / (f64)current_scheduler_cycle_period_ns), 1);
|
|
}
|
|
|
|
u64 wait_addr_bin_index = (u64)wait_addr % P_W32_NumWaitAddrBins;
|
|
u64 wait_time_bin_index = (u64)wait_time % P_W32_NumWaitTimeBins;
|
|
P_W32_WaitBin *wait_addr_bin = &g->wait_addr_bins[wait_addr_bin_index];
|
|
P_W32_WaitBin *wait_time_bin = &g->wait_time_bins[wait_time_bin_index];
|
|
|
|
if (wait_addr != 0) P_W32_LockTicketMutex(&wait_addr_bin->lock);
|
|
{
|
|
if (wait_time != 0) P_W32_LockTicketMutex(&wait_time_bin->lock);
|
|
{
|
|
b32 cancel_wait = wait_addr == 0 && wait_time == 0;
|
|
if (wait_addr != 0)
|
|
{
|
|
switch (wait_size)
|
|
{
|
|
case 1: cancel_wait = (u8)_InterlockedCompareExchange8(wait_addr, 0, 0) != *(u8 *)wait_cmp; break;
|
|
case 2: cancel_wait = (u16)_InterlockedCompareExchange16(wait_addr, 0, 0) != *(u16 *)wait_cmp; break;
|
|
case 4: cancel_wait = (u32)_InterlockedCompareExchange(wait_addr, 0, 0) != *(u32 *)wait_cmp; break;
|
|
case 8: cancel_wait = (u64)_InterlockedCompareExchange64(wait_addr, 0, 0) != *(u64 *)wait_cmp; break;
|
|
default: cancel_wait = 1; Assert(0); break; /* Invalid wait size */
|
|
}
|
|
}
|
|
if (wait_time != 0 && !cancel_wait)
|
|
{
|
|
cancel_wait = wait_time <= Atomic64Fetch(&g->current_scheduler_cycle.v);
|
|
}
|
|
if (!cancel_wait)
|
|
{
|
|
if (wait_addr != 0)
|
|
{
|
|
/* Search for wait addr list in bin */
|
|
P_W32_WaitList *wait_addr_list = 0;
|
|
for (P_W32_WaitList *tmp = wait_addr_bin->first_wait_list; tmp && !wait_addr_list; tmp = tmp->next_in_bin)
|
|
{
|
|
if (tmp->value == (u64)wait_addr)
|
|
{
|
|
wait_addr_list = tmp;
|
|
}
|
|
}
|
|
/* Allocate new wait addr list */
|
|
if (!wait_addr_list)
|
|
{
|
|
if (wait_addr_bin->first_free_wait_list)
|
|
{
|
|
wait_addr_list = wait_addr_bin->first_free_wait_list;
|
|
wait_addr_bin->first_free_wait_list = wait_addr_list->next_in_bin;
|
|
}
|
|
else
|
|
{
|
|
P_W32_LockTicketMutex(&g->wait_lists_arena_lock);
|
|
{
|
|
wait_addr_list = PushStructNoZero(g->wait_lists_arena, P_W32_WaitList);
|
|
}
|
|
P_W32_UnlockTicketMutex(&g->wait_lists_arena_lock);
|
|
}
|
|
ZeroStruct(wait_addr_list);
|
|
wait_addr_list->value = (u64)wait_addr;
|
|
if (wait_addr_bin->last_wait_list)
|
|
{
|
|
wait_addr_bin->last_wait_list->next_in_bin = wait_addr_list;
|
|
wait_addr_list->prev_in_bin = wait_addr_bin->last_wait_list;
|
|
}
|
|
else
|
|
{
|
|
wait_addr_bin->first_wait_list = wait_addr_list;
|
|
}
|
|
wait_addr_bin->last_wait_list = wait_addr_list;
|
|
}
|
|
/* Insert fiber into wait addr list */
|
|
job_fiber->wait_addr = (u64)wait_addr;
|
|
if (wait_addr_list->last_waiter)
|
|
{
|
|
P_W32_FiberFromId(wait_addr_list->last_waiter)->next_addr_waiter = job_fiber_id;
|
|
job_fiber->prev_addr_waiter = wait_addr_list->last_waiter;
|
|
}
|
|
else
|
|
{
|
|
wait_addr_list->first_waiter = job_fiber_id;
|
|
}
|
|
wait_addr_list->last_waiter = job_fiber_id;
|
|
++wait_addr_list->num_waiters;
|
|
}
|
|
if (wait_time != 0)
|
|
{
|
|
/* Search for wait time list in bin */
|
|
P_W32_WaitList *wait_time_list = 0;
|
|
for (P_W32_WaitList *tmp = wait_time_bin->first_wait_list; tmp && !wait_time_list; tmp = tmp->next_in_bin)
|
|
{
|
|
if (tmp->value == (u64)wait_time)
|
|
{
|
|
wait_time_list = tmp;
|
|
}
|
|
}
|
|
/* Allocate new wait time list */
|
|
if (!wait_time_list)
|
|
{
|
|
if (wait_time_bin->first_free_wait_list)
|
|
{
|
|
wait_time_list = wait_time_bin->first_free_wait_list;
|
|
wait_time_bin->first_free_wait_list = wait_time_list->next_in_bin;
|
|
}
|
|
else
|
|
{
|
|
P_W32_LockTicketMutex(&g->wait_lists_arena_lock);
|
|
{
|
|
wait_time_list = PushStructNoZero(g->wait_lists_arena, P_W32_WaitList);
|
|
}
|
|
P_W32_UnlockTicketMutex(&g->wait_lists_arena_lock);
|
|
}
|
|
ZeroStruct(wait_time_list);
|
|
wait_time_list->value = wait_time;
|
|
if (wait_time_bin->last_wait_list)
|
|
{
|
|
wait_time_bin->last_wait_list->next_in_bin = wait_time_list;
|
|
wait_time_list->prev_in_bin = wait_time_bin->last_wait_list;
|
|
}
|
|
else
|
|
{
|
|
wait_time_bin->first_wait_list = wait_time_list;
|
|
}
|
|
wait_time_bin->last_wait_list = wait_time_list;
|
|
}
|
|
/* Insert fiber into wait time list */
|
|
job_fiber->wait_time = wait_time;
|
|
if (wait_time_list->last_waiter)
|
|
{
|
|
P_W32_FiberFromId(wait_time_list->last_waiter)->next_time_waiter = job_fiber_id;
|
|
job_fiber->prev_time_waiter = wait_time_list->last_waiter;
|
|
}
|
|
else
|
|
{
|
|
wait_time_list->first_waiter = job_fiber_id;
|
|
}
|
|
wait_time_list->last_waiter = job_fiber_id;
|
|
++wait_time_list->num_waiters;
|
|
}
|
|
|
|
/* PopStruct worker's job fiber */
|
|
job_fiber = 0;
|
|
done = 1;
|
|
}
|
|
}
|
|
if (wait_time != 0) P_W32_UnlockTicketMutex(&wait_time_bin->lock);
|
|
}
|
|
if (wait_addr != 0) P_W32_UnlockTicketMutex(&wait_addr_bin->lock);
|
|
} break;
|
|
|
|
case P_W32_YieldKind_Done:
|
|
{
|
|
done = 1;
|
|
} break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Wait for job */
|
|
i64 num_jobs_in_queue = Atomic64Fetch(&pool->num_jobs_in_queue.v);
|
|
shutdown = Atomic32Fetch(&pool->workers_shutdown.v);
|
|
if (num_jobs_in_queue <= 0 && !shutdown)
|
|
{
|
|
//__profnc("Wait for job", Rgb32F(0.75, 0.75, 0));
|
|
P_W32_LockTicketMutex(&pool->workers_wake_lock);
|
|
{
|
|
num_jobs_in_queue = Atomic64Fetch(&pool->num_jobs_in_queue.v);
|
|
shutdown = Atomic32Fetch(&pool->workers_shutdown.v);
|
|
while (num_jobs_in_queue <= 0 && !shutdown)
|
|
{
|
|
{
|
|
P_W32_UnlockTicketMutex(&pool->workers_wake_lock);
|
|
WaitOnAddress(&pool->num_jobs_in_queue, &num_jobs_in_queue, sizeof(num_jobs_in_queue), INFINITE);
|
|
P_W32_LockTicketMutex(&pool->workers_wake_lock);
|
|
}
|
|
shutdown = Atomic32Fetch(&pool->workers_shutdown.v);
|
|
num_jobs_in_queue = Atomic64Fetch(&pool->num_jobs_in_queue.v);
|
|
}
|
|
}
|
|
P_W32_UnlockTicketMutex(&pool->workers_wake_lock);
|
|
}
|
|
}
|
|
|
|
/* Worker shutdown */
|
|
if (job_fiber)
|
|
{
|
|
P_W32_ReleaseFiber(pool, job_fiber);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Win32 job scheduler
|
|
|
|
P_W32_ThreadDef(P_W32_JobSchedulerEntryFunc, _)
|
|
{
|
|
struct P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
|
|
(UNUSED)_;
|
|
{
|
|
i32 priority = THREAD_PRIORITY_TIME_CRITICAL;
|
|
b32 success = SetThreadPriority(GetCurrentThread(), priority);
|
|
(UNUSED)success;
|
|
Assert(success);
|
|
}
|
|
|
|
/* Create high resolution timer */
|
|
HANDLE timer = CreateWaitableTimerExW(0, 0, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
|
if (!timer)
|
|
{
|
|
P_Panic(Lit("Failed to create high resolution timer"));
|
|
}
|
|
|
|
/* Create rolling buffer of scheduler cycles initialized to default value */
|
|
i32 periods_index = 0;
|
|
i64 periods[P_W32_NumRollingSchedulerPeriods] = ZI;
|
|
for (i32 i = 0; i < (i32)countof(periods); ++i)
|
|
{
|
|
periods[i] = P_W32_DefaultSchedulerPeriodNs;
|
|
}
|
|
|
|
i64 last_cycle_ns = 0;
|
|
while (!Atomic32Fetch(&g->shutdown))
|
|
{
|
|
__profn("Job scheduler cycle");
|
|
{
|
|
__profn("Job scheduler wait");
|
|
LARGE_INTEGER due = ZI;
|
|
due.QuadPart = -1;
|
|
//due.QuadPart = -10000;
|
|
//due.QuadPart = -32000;
|
|
//due.QuadPart = -12000;
|
|
//due.QuadPart = -8000;
|
|
SetWaitableTimerEx(timer, &due, 0, 0, 0, 0, 0);
|
|
WaitForSingleObject(timer, INFINITE);
|
|
}
|
|
|
|
/* Calculate mean period */
|
|
i64 now_ns = P_TimeNs();
|
|
i64 period_ns = last_cycle_ns == 0 ? P_W32_DefaultSchedulerPeriodNs : now_ns - last_cycle_ns;
|
|
last_cycle_ns = now_ns;
|
|
|
|
/* Calculate mean period */
|
|
{
|
|
periods[periods_index++] = period_ns;
|
|
if (periods_index == countof(periods))
|
|
{
|
|
periods_index = 0;
|
|
}
|
|
f64 periods_sum_ns = 0;
|
|
for (i32 i = 0; i < (i32)countof(periods); ++i)
|
|
{
|
|
periods_sum_ns += (f64)periods[i];
|
|
}
|
|
f64 mean_ns = periods_sum_ns / (f64)countof(periods);
|
|
Atomic64FetchSet(&g->current_scheduler_cycle_period_ns.v, RoundF64ToI64(mean_ns));
|
|
}
|
|
|
|
{
|
|
__profn("Job scheduler run");
|
|
i64 current_cycle = Atomic64FetchAdd(&g->current_scheduler_cycle.v, 1) + 1;
|
|
P_W32_WakeByTime((u64)current_cycle);
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Win32 time
|
|
|
|
P_DateTime P_W32_DateTimeFromWin32SystemTime(SYSTEMTIME st)
|
|
{
|
|
return (P_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
|
|
};
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Win32 file system
|
|
|
|
String P_W32_StringFromWin32Path(Arena *arena, wchar_t *src)
|
|
{
|
|
String res = {
|
|
.len = 0,
|
|
.text = PushDry(arena, u8)
|
|
};
|
|
|
|
while (*src)
|
|
{
|
|
String16 decode_str = { .len = *(src + 1) ? 2 : 1, .text = src };
|
|
Utf16DecodeResult decoded = DecodeUtf16(decode_str);
|
|
Utf8EncodeResult encoded = EncodeUtf8(decoded.codepoint);
|
|
u8 *dest = PushStructsNoZero(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;
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Win32 window
|
|
|
|
P_W32_Window *P_W32_AllocWindow(void)
|
|
{
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
P_W32_Window *window = 0;
|
|
{
|
|
P_Lock lock = P_LockE(&g->windows_mutex);
|
|
if (g->first_free_window)
|
|
{
|
|
window = g->first_free_window;
|
|
g->first_free_window = window->next_free;
|
|
}
|
|
else
|
|
{
|
|
window = PushStructNoZero(g->windows_arena, P_W32_Window);
|
|
}
|
|
P_Unlock(&lock);
|
|
}
|
|
ZeroStruct(window);
|
|
|
|
window->event_arenas[0] = AllocArena(Gibi(64));
|
|
window->event_arenas[1] = AllocArena(Gibi(64));
|
|
|
|
/* Start window event thread */
|
|
/* NOTE: This thread must finish building for the window to actually be
|
|
* created and receive a HWND, because on Windows a the event proc must run on
|
|
* the same thread that created the window. */
|
|
P_CounterAdd(&window->ready_fence, 1);
|
|
window->window_thread = P_W32_AllocThread(&P_W32_WindowThreadEntryFunc, window, Lit("Window thread"), PROF_THREAD_GROUP_WINDOW);
|
|
P_WaitOnCounter(&window->ready_fence);
|
|
|
|
return window;
|
|
}
|
|
|
|
void P_W32_ReleaseWindow(P_W32_Window *window)
|
|
{
|
|
/* Stop window threads */
|
|
Atomic32FetchSet(&window->shutdown, 1);
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
P_W32_WakeWindow(window);
|
|
P_W32_WaitReleaseThread(window->window_thread);
|
|
|
|
P_Lock lock = P_LockE(&g->windows_mutex);
|
|
{
|
|
window->next_free = g->first_free_window;
|
|
g->first_free_window = window;
|
|
}
|
|
P_Unlock(&lock);
|
|
}
|
|
|
|
HWND P_W32_InitWindow(P_W32_Window *window)
|
|
{
|
|
struct P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
|
|
/*
|
|
* 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,
|
|
0,
|
|
0,
|
|
g->window_class.hInstance,
|
|
0
|
|
);
|
|
|
|
/* Dark mode */
|
|
BOOL dark_mode = 1;
|
|
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;
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Win32 window settings
|
|
|
|
void P_W32_UpdateWindowFromSystem(P_W32_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 & P_WindowFlag_Showing)
|
|
{
|
|
WINDOWPLACEMENT placement = { .length = sizeof(placement) };
|
|
GetWindowPlacement(hwnd, &placement);
|
|
if (placement.showCmd == SW_SHOWMINIMIZED)
|
|
{
|
|
window->settings.flags |= P_WindowSettingsFlag_Minimized;
|
|
}
|
|
else
|
|
{
|
|
window->settings.flags &= ~P_WindowSettingsFlag_Minimized;
|
|
}
|
|
if (placement.showCmd == SW_SHOWMAXIMIZED
|
|
|| ((window->settings.flags & P_WindowSettingsFlag_Minimized) && ((placement.flags & WPF_RESTORETOMAXIMIZED) != 0)))
|
|
{
|
|
window->settings.flags |= P_WindowSettingsFlag_Maximized;
|
|
}
|
|
else
|
|
{
|
|
window->settings.flags &= ~P_WindowSettingsFlag_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 & P_WindowSettingsFlag_Minimized))
|
|
{
|
|
window->x = x;
|
|
window->y = y;
|
|
window->width = width;
|
|
window->height = height;
|
|
if (!(window->settings.flags & (P_WindowSettingsFlag_Maximized | P_WindowSettingsFlag_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;
|
|
}
|
|
}
|
|
}
|
|
|
|
void P_W32_UpdateWindowFromSettings(P_W32_Window *window, P_WindowSettings *settings)
|
|
{
|
|
HWND hwnd = window->hwnd;
|
|
|
|
P_WindowSettings old_settings = window->settings;
|
|
window->settings = *settings;
|
|
|
|
i32 show_cmd = SW_HIDE;
|
|
if (window->flags & P_WindowFlag_Showing)
|
|
{
|
|
show_cmd = SW_NORMAL;
|
|
if (settings->flags & P_WindowSettingsFlag_Maximized)
|
|
{
|
|
show_cmd = SW_SHOWMAXIMIZED;
|
|
}
|
|
else if (settings->flags & P_WindowSettingsFlag_Minimized)
|
|
{
|
|
show_cmd = SW_MINIMIZE;
|
|
}
|
|
}
|
|
|
|
RECT rect = ZI;
|
|
|
|
b32 old_fullscreen = old_settings.flags & P_WindowSettingsFlag_Fullscreen;
|
|
b32 fullscreen = settings->flags & P_WindowSettingsFlag_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, 0);
|
|
}
|
|
|
|
WINDOWPLACEMENT wp = {
|
|
.length = sizeof(WINDOWPLACEMENT),
|
|
.showCmd = show_cmd,
|
|
.rcNormalPosition = rect
|
|
};
|
|
SetWindowPlacement(hwnd, &wp);
|
|
|
|
{
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
wchar_t *title_wstr = WstrFromString(scratch.arena, StringFromCstrNoLimit(settings->title));
|
|
SetWindowTextW(hwnd, title_wstr);
|
|
EndScratch(scratch);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Win32 window thread
|
|
|
|
P_W32_ThreadDef(P_W32_WindowThreadEntryFunc, arg)
|
|
{
|
|
P_W32_Window *window = (P_W32_Window *)arg;
|
|
|
|
/* Win32 limitation: Window must be initialized on same thread that processes events */
|
|
window->hwnd = P_W32_InitWindow(window);
|
|
P_W32_UpdateWindowFromSystem(window);
|
|
BringWindowToTop(window->hwnd);
|
|
P_CounterAdd(&window->ready_fence, -1);
|
|
|
|
while (!Atomic32Fetch(&window->shutdown))
|
|
{
|
|
MSG msg = ZI;
|
|
{
|
|
GetMessageW(&msg, 0, 0, 0);
|
|
}
|
|
{
|
|
__profn("Process window message");
|
|
if (!Atomic32Fetch(&window->shutdown))
|
|
{
|
|
TranslateMessage(&msg);
|
|
DispatchMessageW(&msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Destroy window hwnd */
|
|
DestroyWindow(window->hwnd);
|
|
}
|
|
|
|
void P_W32_ProcessWindowEvent(P_W32_Window *window, P_WindowEvent event)
|
|
{
|
|
__prof;
|
|
P_Lock lock = P_LockE(&window->event_arena_swp_mutex);
|
|
{
|
|
*PushStruct(window->event_arenas[window->current_event_arena_index], P_WindowEvent) = event;
|
|
}
|
|
P_Unlock(&lock);
|
|
}
|
|
|
|
void P_W32_WakeWindow(P_W32_Window *window)
|
|
{
|
|
/* Post a blank message to the window's thread message queue to wake it. */
|
|
PostMessageW(window->hwnd, 0, 0, 0);
|
|
}
|
|
|
|
LRESULT CALLBACK P_W32_Win32WindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
|
|
{
|
|
__prof;
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
P_W32_Window *window = (P_W32_Window *)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
|
|
|
|
if (!window)
|
|
{
|
|
return DefWindowProcW(hwnd, msg, wparam, lparam);
|
|
}
|
|
|
|
/* Update cursor */
|
|
if (GetFocus() == window->hwnd)
|
|
{
|
|
u32 cursor_flags = window->cursor_set_flags;
|
|
|
|
/* Hide cursor */
|
|
if (cursor_flags & P_W32_CursorFlag_Hide)
|
|
{
|
|
while (ShowCursor(0) >= 0);
|
|
}
|
|
|
|
/* Show cursor */
|
|
if (cursor_flags & P_W32_CursorFlag_Show)
|
|
{
|
|
while (ShowCursor(1) < 0);
|
|
}
|
|
|
|
/* Update position */
|
|
if (cursor_flags & P_W32_CursorFlag_Position)
|
|
{
|
|
Vec2 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 & P_W32_CursorFlag_DisableClip)
|
|
{
|
|
ClipCursor(0);
|
|
}
|
|
|
|
/* Clip cursor in window window */
|
|
if (cursor_flags & P_W32_CursorFlag_EnableClip)
|
|
{
|
|
i32 left = window->x + RoundF32ToI32(window->cursor_clip_bounds.x);
|
|
i32 right = left + RoundF32ToI32(window->cursor_clip_bounds.width);
|
|
i32 top = window->y + RoundF32ToI32(window->cursor_clip_bounds.y);
|
|
i32 bottom = top + RoundF32ToI32(window->cursor_clip_bounds.height);
|
|
RECT clip = {
|
|
.left = ClampI32(left, window->x, window->x + window->width),
|
|
.right = ClampI32(right, window->x, window->x + window->width),
|
|
.top = ClampI32(top, window->y, window->y + window->height),
|
|
.bottom = ClampI32(bottom, window->y, window->y + window->height)
|
|
};
|
|
ClipCursor(&clip);
|
|
}
|
|
|
|
window->cursor_set_flags = 0;
|
|
}
|
|
|
|
/* Update always on top */
|
|
{
|
|
u32 toggles = Atomic32FetchSet(&window->topmost_toggles, 0);
|
|
if (toggles % 2 != 0)
|
|
{
|
|
b32 new_topmost = !window->is_topmost;
|
|
if (new_topmost)
|
|
{
|
|
SetWindowText(hwnd, L"============================= TOP =============================");
|
|
}
|
|
else
|
|
{
|
|
SetWindowText(hwnd, L"");
|
|
}
|
|
SetWindowPos(hwnd, new_topmost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
|
|
window->is_topmost = new_topmost;
|
|
}
|
|
}
|
|
|
|
LRESULT result = 0;
|
|
b32 is_release = 0;
|
|
switch (msg)
|
|
{
|
|
case WM_QUIT:
|
|
case WM_CLOSE:
|
|
case WM_DESTROY:
|
|
{
|
|
P_W32_ProcessWindowEvent(window, (P_WindowEvent) { .kind = P_WindowEventKind_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:
|
|
{
|
|
P_W32_UpdateWindowFromSystem(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 = 0;
|
|
|
|
P_WindowEventKind event_kind = P_WindowEventKind_None;
|
|
if (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN)
|
|
{
|
|
event_kind = P_WindowEventKind_ButtonDown;
|
|
is_repeat = (lparam & 0x40000000) != 0;
|
|
}
|
|
else if (msg == WM_KEYUP || msg == WM_SYSKEYUP)
|
|
{
|
|
event_kind = P_WindowEventKind_ButtonUp;
|
|
}
|
|
|
|
P_Btn button = P_Btn_None;
|
|
if (vk_code < countof(g->vk_btn_table))
|
|
{
|
|
button = g->vk_btn_table[vk_code];
|
|
}
|
|
|
|
P_W32_ProcessWindowEvent(
|
|
window,
|
|
(P_WindowEvent)
|
|
{
|
|
.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 (IsUtf16HighSurrogate(utf16_char))
|
|
{
|
|
window->utf16_high_surrogate_last_input = utf16_char;
|
|
}
|
|
else if (IsUtf16LowSurrogate(utf16_char))
|
|
{
|
|
u16 high = window->utf16_high_surrogate_last_input;
|
|
u16 low = utf16_char;
|
|
if (high)
|
|
{
|
|
u16 utf16_pair_bytes[2] = { high, low };
|
|
Utf16DecodeResult decoded = DecodeUtf16((String16) { .len = countof(utf16_pair_bytes), .text = utf16_pair_bytes });
|
|
if (decoded.advance16 == 2 && decoded.codepoint < U32Max)
|
|
{
|
|
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')
|
|
{
|
|
P_W32_ProcessWindowEvent(
|
|
window,
|
|
(P_WindowEvent)
|
|
{
|
|
.kind = P_WindowEventKind_Text,
|
|
.text_codepoint = codepoint
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
} break;
|
|
|
|
/* Mouse buttons */
|
|
case WM_LBUTTONUP:
|
|
case WM_MBUTTONUP:
|
|
case WM_RBUTTONUP:
|
|
case WM_XBUTTONUP:
|
|
{
|
|
ReleaseCapture();
|
|
is_release = 1;
|
|
} FALLTHROUGH;
|
|
case WM_LBUTTONDOWN:
|
|
case WM_MBUTTONDOWN:
|
|
case WM_RBUTTONDOWN:
|
|
case WM_XBUTTONDOWN:
|
|
{
|
|
if (!is_release)
|
|
{
|
|
SetCapture(hwnd);
|
|
}
|
|
|
|
P_WindowEventKind event_kind = is_release ? P_WindowEventKind_ButtonUp : P_WindowEventKind_ButtonDown;
|
|
P_Btn button = 0;
|
|
|
|
switch (msg)
|
|
{
|
|
case WM_LBUTTONUP: case WM_LBUTTONDOWN: button = P_Btn_M1; break;
|
|
case WM_RBUTTONUP: case WM_RBUTTONDOWN: button = P_Btn_M2; break;
|
|
case WM_MBUTTONUP: case WM_MBUTTONDOWN: button = P_Btn_M3; break;
|
|
case WM_XBUTTONUP: case WM_XBUTTONDOWN:
|
|
{
|
|
u32 wparam_xbutton = GET_XBUTTON_WPARAM(wparam);
|
|
if (wparam_xbutton == XBUTTON1)
|
|
{
|
|
button = P_Btn_M4;
|
|
}
|
|
else if (wparam_xbutton == XBUTTON2)
|
|
{
|
|
button = P_Btn_M5;
|
|
}
|
|
} break;
|
|
}
|
|
|
|
if (button)
|
|
{
|
|
P_W32_ProcessWindowEvent(
|
|
window,
|
|
(P_WindowEvent)
|
|
{
|
|
.kind = event_kind,
|
|
.button = button
|
|
}
|
|
);
|
|
}
|
|
} break;
|
|
|
|
/* Mouse wheel */
|
|
case WM_MOUSEWHEEL:
|
|
{
|
|
int delta = GET_WHEEL_DELTA_WPARAM(wparam);
|
|
i32 dir = delta >= 0 ? 1 : -1;
|
|
P_Btn button = dir >= 0 ? P_Btn_MWheelUp : P_Btn_MWheelDown;
|
|
for (i32 i = 0; i < (dir * delta); i += WHEEL_DELTA)
|
|
{
|
|
/* Send a button down & button up event simultaneously */
|
|
P_W32_ProcessWindowEvent(window, (P_WindowEvent) { .kind = P_WindowEventKind_ButtonDown, .button = button });
|
|
P_W32_ProcessWindowEvent(window, (P_WindowEvent) { .kind = P_WindowEventKind_ButtonUp, .button = button });
|
|
}
|
|
} break;
|
|
|
|
/* Mouse move */
|
|
case WM_MOUSEMOVE:
|
|
{
|
|
i32 x = GET_X_LPARAM(lparam);
|
|
i32 y = GET_Y_LPARAM(lparam);
|
|
P_W32_ProcessWindowEvent(
|
|
window,
|
|
(P_WindowEvent)
|
|
{
|
|
.kind = P_WindowEventKind_CursorMove,
|
|
.cursor_position = VEC2(x, y)
|
|
}
|
|
);
|
|
} break;
|
|
|
|
/* Raw mouse move */
|
|
case WM_INPUT:
|
|
{
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
|
|
/* Read raw input buffer */
|
|
UINT buff_size;
|
|
GetRawInputData((HRAWINPUT)lparam, RID_INPUT, 0, &buff_size, sizeof(RAWINPUTHEADER));
|
|
u8 *buff = PushStructs(scratch.arena, u8, buff_size);
|
|
if (GetRawInputData((HRAWINPUT)lparam, RID_INPUT, buff, &buff_size, sizeof(RAWINPUTHEADER)) != buff_size)
|
|
{
|
|
P_LogErrorF("GetRawInputData did not return correct size");
|
|
break;
|
|
}
|
|
RAWINPUT raw = ZI;
|
|
CopyBytes(&raw, buff, sizeof(RAWINPUT));
|
|
|
|
if (raw.header.dwType == RIM_TYPEMOUSE)
|
|
{
|
|
i32 x = raw.data.mouse.lLastX;
|
|
i32 y = raw.data.mouse.lLastY;
|
|
Vec2 delta = VEC2(x, y);
|
|
P_W32_ProcessWindowEvent(
|
|
window,
|
|
(P_WindowEvent)
|
|
{
|
|
.kind = P_WindowEventKind_MouseMove,
|
|
.mouse_delta = delta
|
|
}
|
|
);
|
|
}
|
|
|
|
EndScratch(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;
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Win32 Address
|
|
|
|
P_W32_Address P_W32_Win32AddressFromPlatformAddress(P_Address addr)
|
|
{
|
|
P_W32_Address res = ZI;
|
|
if (addr.family == P_AddressFamily_Ipv4)
|
|
{
|
|
res.family = AF_INET;
|
|
res.size = sizeof(struct sockaddr_in);
|
|
res.sin.sin_port = addr.portnb;
|
|
res.sin.sin_family = res.family;
|
|
CopyBytes(&res.sin.sin_addr, addr.ipnb, 4);
|
|
}
|
|
else
|
|
{
|
|
res.family = AF_INET6;
|
|
res.sin6.sin6_port = addr.portnb;
|
|
res.sin6.sin6_family = res.family;
|
|
res.size = sizeof(struct sockaddr_in6);
|
|
CopyBytes(&res.sin6.sin6_addr.s6_addr, addr.ipnb, 16);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/* If supplied address has ip INADDR_ANY (0), convert ip to localhost */
|
|
P_W32_Address P_W32_ConvertAnyaddrToLocalhost(P_W32_Address addr)
|
|
{
|
|
if (addr.family == AF_INET)
|
|
{
|
|
u8 *bytes = (u8 *)&addr.sin.sin_addr;
|
|
b32 is_any = 1;
|
|
for (u64 i = 0; i < 4; ++i)
|
|
{
|
|
if (bytes[i] != 0)
|
|
{
|
|
is_any = 0;
|
|
break;
|
|
}
|
|
}
|
|
if (is_any)
|
|
{
|
|
bytes[0] = 127;
|
|
bytes[3] = 1;
|
|
}
|
|
}
|
|
else if (addr.family == AF_INET6)
|
|
{
|
|
u8 *bytes = (u8 *)&addr.sin.sin_addr;
|
|
b32 is_any = 1;
|
|
for (u64 i = 0; i < 16; ++i)
|
|
{
|
|
if (bytes[i] != 0)
|
|
{
|
|
is_any = 0;
|
|
break;
|
|
}
|
|
}
|
|
if (is_any)
|
|
{
|
|
bytes[15] = 1;
|
|
}
|
|
}
|
|
return addr;
|
|
}
|
|
|
|
P_Address P_W32_PlatformAddressFromWin32Address(P_W32_Address ws_addr)
|
|
{
|
|
P_Address res = ZI;
|
|
if (ws_addr.family == AF_INET)
|
|
{
|
|
res.family = P_AddressFamily_Ipv4;
|
|
res.portnb = ws_addr.sin.sin_port;
|
|
CopyBytes(res.ipnb, &ws_addr.sin.sin_addr, 4);
|
|
res.valid = 1;
|
|
}
|
|
else if (ws_addr.family == AF_INET6)
|
|
{
|
|
res.family = P_AddressFamily_Ipv6;
|
|
res.portnb = ws_addr.sin6.sin6_port;
|
|
CopyBytes(res.ipnb, &ws_addr.sin6.sin6_addr.s6_addr, 16);
|
|
res.valid = 1;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Wait / wake
|
|
|
|
void P_Wait(volatile void *addr, void *cmp, u32 size, i64 timeout_ns)
|
|
{
|
|
P_W32_Fiber *fiber = P_W32_FiberFromId(FiberId());
|
|
i16 parent_id = fiber->parent_id;
|
|
if (parent_id != 0)
|
|
{
|
|
*fiber->yield_param = (P_W32_YieldParam) {
|
|
.kind = P_W32_YieldKind_Wait,
|
|
.wait = {
|
|
.addr = addr,
|
|
.cmp = cmp,
|
|
.size = size,
|
|
.timeout_ns = timeout_ns
|
|
}
|
|
};
|
|
P_W32_YieldFiber(fiber, P_W32_FiberFromId(parent_id));
|
|
}
|
|
else
|
|
{
|
|
i32 timeout_ms = 0;
|
|
if (timeout_ns > 10000000000000000ll)
|
|
{
|
|
timeout_ms = INFINITE;
|
|
}
|
|
else if (timeout_ns != 0)
|
|
{
|
|
timeout_ms = timeout_ns / 1000000;
|
|
timeout_ms += (timeout_ms == 0) * SignF32(timeout_ns);
|
|
}
|
|
if (addr == 0)
|
|
{
|
|
Sleep(timeout_ms);
|
|
}
|
|
else
|
|
{
|
|
WaitOnAddress(addr, cmp, size, timeout_ms);
|
|
}
|
|
}
|
|
}
|
|
|
|
void P_Wake(void *addr, i32 count)
|
|
{
|
|
P_W32_WakeByAddress(addr, count);
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Job
|
|
|
|
void P_Run(i32 count, P_JobFunc *func, void *sig, P_Pool pool_kind, P_Priority priority, P_Counter *counter)
|
|
{
|
|
__prof;
|
|
struct P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
if (count > 0)
|
|
{
|
|
if (counter)
|
|
{
|
|
P_CounterAdd(counter, count);
|
|
}
|
|
P_W32_Fiber *fiber = P_W32_FiberFromId(FiberId());
|
|
priority = ClampI32(priority, fiber->job_priority, P_Priority_Count - 1); /* A job cannot create a job with a higher priority than itself */
|
|
if (pool_kind == P_Pool_Inherit)
|
|
{
|
|
pool_kind = fiber->job_pool;
|
|
}
|
|
P_W32_JobPool *pool = &g->job_pools[pool_kind];
|
|
P_W32_JobQueue *queue = &pool->job_queues[priority];
|
|
P_W32_LockTicketMutex(&queue->lock);
|
|
{
|
|
P_W32_JobInfo *info = 0;
|
|
if (queue->first_free)
|
|
{
|
|
info = queue->first_free;
|
|
queue->first_free = info->next;
|
|
}
|
|
else
|
|
{
|
|
info = PushStructNoZero(queue->arena, P_W32_JobInfo);
|
|
}
|
|
ZeroStruct(info);
|
|
info->count = count;
|
|
info->func = func;
|
|
info->sig = sig;
|
|
info->counter = counter;
|
|
if (queue->last)
|
|
{
|
|
queue->last->next = info;
|
|
}
|
|
else
|
|
{
|
|
queue->first = info;
|
|
}
|
|
queue->last = info;
|
|
}
|
|
P_W32_UnlockTicketMutex(&queue->lock);
|
|
|
|
/* Wake workers */
|
|
{
|
|
P_W32_LockTicketMutex(&pool->workers_wake_lock);
|
|
{
|
|
Atomic64FetchAdd(&pool->num_jobs_in_queue.v, count);
|
|
if (count >= P_W32_WakeAllThreshold)
|
|
{
|
|
WakeByAddressAll(&pool->num_jobs_in_queue);
|
|
}
|
|
else
|
|
{
|
|
for (i32 i = 0; i < count; ++i)
|
|
{
|
|
WakeByAddressSingle(&pool->num_jobs_in_queue);
|
|
}
|
|
}
|
|
}
|
|
P_W32_UnlockTicketMutex(&pool->workers_wake_lock);
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Time
|
|
|
|
P_DateTime P_LocalTime(void)
|
|
{
|
|
SYSTEMTIME lt;
|
|
GetLocalTime(<);
|
|
return P_W32_DateTimeFromWin32SystemTime(lt);
|
|
}
|
|
|
|
i64 P_TimeNs(void)
|
|
{
|
|
struct P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
LARGE_INTEGER qpc;
|
|
QueryPerformanceCounter(&qpc);
|
|
i64 res = (qpc.QuadPart - g->timer_start_qpc) * g->ns_per_qpc;
|
|
return res;
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ File system
|
|
|
|
String P_GetWritePath(Arena *arena)
|
|
{
|
|
u16 *p = 0;
|
|
/* TODO: cache this? */
|
|
HRESULT res = SHGetKnownFolderPath(
|
|
&FOLDERID_LocalAppData,
|
|
0,
|
|
0,
|
|
&p
|
|
);
|
|
String path = ZI;
|
|
if (res == S_OK)
|
|
{
|
|
path = P_W32_StringFromWin32Path(arena, p);
|
|
}
|
|
CoTaskMemFree(p);
|
|
return path;
|
|
}
|
|
|
|
b32 P_IsFile(String path)
|
|
{
|
|
__prof;
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
wchar_t *path_wstr = WstrFromString(scratch.arena, path);
|
|
DWORD attributes = GetFileAttributesW(path_wstr);
|
|
EndScratch(scratch);
|
|
return attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY);
|
|
}
|
|
|
|
b32 P_IsDir(String path)
|
|
{
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
wchar_t *path_wstr = WstrFromString(scratch.arena, path);
|
|
DWORD attributes = GetFileAttributesW(path_wstr);
|
|
EndScratch(scratch);
|
|
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY);
|
|
}
|
|
|
|
void P_MkDir(String path)
|
|
{
|
|
__prof;
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
wchar_t *path_wstr = WstrFromString(scratch.arena, path);
|
|
int err_code = SHCreateDirectory(0, path_wstr);
|
|
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)
|
|
{
|
|
String msg = StringFormat(scratch.arena,
|
|
Lit("Failed to create directory \"%F\": %F"),
|
|
FmtString(path),
|
|
FmtString(err));
|
|
P_Panic(msg);
|
|
}
|
|
EndScratch(scratch);
|
|
}
|
|
|
|
P_File P_OpenFileRead(String path)
|
|
{
|
|
__prof;
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
P_File file = ZI;
|
|
|
|
wchar_t *path_wstr = WstrFromString(scratch.arena, path);
|
|
HANDLE handle = CreateFileW(
|
|
path_wstr,
|
|
GENERIC_READ,
|
|
FILE_SHARE_READ,
|
|
0,
|
|
OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_NORMAL,
|
|
0
|
|
);
|
|
file.handle = (u64)handle;
|
|
file.valid = handle != INVALID_HANDLE_VALUE;
|
|
|
|
EndScratch(scratch);
|
|
return file;
|
|
}
|
|
|
|
P_File P_OpenFileReadWait(String path)
|
|
{
|
|
__prof;
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
P_File file = ZI;
|
|
|
|
wchar_t *path_wstr = WstrFromString(scratch.arena, path);
|
|
i32 delay_ms = 1;
|
|
HANDLE handle;
|
|
while ((handle = CreateFileW(path_wstr, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0)) == INVALID_HANDLE_VALUE)
|
|
{
|
|
if (GetLastError() == ERROR_SHARING_VIOLATION)
|
|
{
|
|
__profn("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;
|
|
|
|
EndScratch(scratch);
|
|
return file;
|
|
}
|
|
|
|
P_File P_OpenFileWrite(String path)
|
|
{
|
|
__prof;
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
P_File file = ZI;
|
|
|
|
wchar_t *path_wstr = WstrFromString(scratch.arena, path);
|
|
HANDLE handle = CreateFileW(
|
|
path_wstr,
|
|
GENERIC_WRITE,
|
|
0,
|
|
0,
|
|
CREATE_ALWAYS,
|
|
FILE_ATTRIBUTE_NORMAL,
|
|
0
|
|
);
|
|
file.handle = (u64)handle;
|
|
file.valid = handle != INVALID_HANDLE_VALUE;
|
|
|
|
EndScratch(scratch);
|
|
return file;
|
|
}
|
|
|
|
P_File P_OpenFileAppend(String path)
|
|
{
|
|
__prof;
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
P_File file = ZI;
|
|
|
|
wchar_t *path_wstr = WstrFromString(scratch.arena, path);
|
|
HANDLE handle = CreateFileW(
|
|
path_wstr,
|
|
FILE_APPEND_DATA,
|
|
FILE_SHARE_READ,
|
|
0,
|
|
OPEN_ALWAYS,
|
|
FILE_ATTRIBUTE_NORMAL,
|
|
0
|
|
);
|
|
file.handle = (u64)handle;
|
|
file.valid = handle != INVALID_HANDLE_VALUE;
|
|
|
|
EndScratch(scratch);
|
|
return file;
|
|
}
|
|
|
|
void P_CloseFIle(P_File file)
|
|
{
|
|
__prof;
|
|
if (file.handle)
|
|
{
|
|
CloseHandle((HANDLE)file.handle);
|
|
}
|
|
}
|
|
|
|
String P_ReadFile(Arena *arena, P_File file)
|
|
{
|
|
__prof;
|
|
i64 size = 0;
|
|
GetFileSizeEx((HANDLE)file.handle, (PLARGE_INTEGER)&size);
|
|
|
|
String s = {
|
|
.len = size,
|
|
.text = 0
|
|
};
|
|
|
|
if (size > 0)
|
|
{
|
|
/* ReadFile returns non-zero on success */
|
|
/* TODO: error checking */
|
|
AlignArena(arena, 16);
|
|
s.text = PushStructsNoZero(arena, u8, size);
|
|
(UNUSED)ReadFile(
|
|
(HANDLE)file.handle,
|
|
s.text,
|
|
(DWORD)s.len,
|
|
0, /* lpNumberOfBytesRead */
|
|
0
|
|
);
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
void P_WriteFile(P_File file, String data)
|
|
{
|
|
__prof;
|
|
/* TODO: Check what the real data limit is and chunk sequentially based on
|
|
* that (rather than failing) */
|
|
if (data.len >= 0x7FFF)
|
|
{
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
P_Panic(StringFormat(scratch.arena,
|
|
Lit("Tried to write too many bytes to disk (%F)"),
|
|
FmtUint(data.len)));
|
|
EndScratch(scratch);
|
|
}
|
|
|
|
/* WriteFile returns TRUE on success */
|
|
(UNUSED)WriteFile(
|
|
(HANDLE)file.handle,
|
|
data.text,
|
|
(DWORD)data.len,
|
|
0, /* lpNumberOfBytesWritten */
|
|
0
|
|
);
|
|
}
|
|
|
|
u64 P_GetFileSize(P_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);
|
|
}
|
|
|
|
P_FileTime P_GetFileTime(P_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 (P_FileTime)
|
|
{
|
|
.created = P_W32_DateTimeFromWin32SystemTime(st_created),
|
|
.accessed = P_W32_DateTimeFromWin32SystemTime(st_accessed),
|
|
.modified = P_W32_DateTimeFromWin32SystemTime(st_modified)
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return (P_FileTime) { 0 };
|
|
}
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ File map
|
|
|
|
P_FileMap P_OpenFileMap(P_File file)
|
|
{
|
|
__prof;
|
|
P_FileMap map = ZI;
|
|
|
|
u64 size = P_GetFileSize(file);
|
|
u8 *base_ptr = 0;
|
|
HANDLE map_handle = 0;
|
|
|
|
if (size > 0)
|
|
{
|
|
map_handle = CreateFileMappingW(
|
|
(HANDLE)file.handle,
|
|
0,
|
|
PAGE_READONLY,
|
|
0,
|
|
0,
|
|
0
|
|
);
|
|
if (map_handle != INVALID_HANDLE_VALUE)
|
|
{
|
|
base_ptr = MapViewOfFile(
|
|
map_handle,
|
|
FILE_MAP_READ,
|
|
0,
|
|
0,
|
|
0
|
|
);
|
|
if (base_ptr == 0)
|
|
{
|
|
/* 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 != 0;
|
|
|
|
|
|
return map;
|
|
}
|
|
|
|
void P_CloseFileMap(P_FileMap map)
|
|
{
|
|
if (map.mapped_memory.text)
|
|
{
|
|
UnmapViewOfFile(map.mapped_memory.text);
|
|
}
|
|
if (map.handle)
|
|
{
|
|
CloseHandle((HANDLE)map.handle);
|
|
}
|
|
}
|
|
|
|
String P_GetFileMapData(P_FileMap map)
|
|
{
|
|
return map.mapped_memory;
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Watch
|
|
|
|
P_Watch *P_AllocWatch(String dir_path)
|
|
{
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
struct P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
|
|
P_W32_Watch *w32_watch = 0;
|
|
{
|
|
P_Lock lock = P_LockE(&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 = PushStructNoZero(g->watches_arena, P_W32_Watch);
|
|
}
|
|
}
|
|
P_Unlock(&lock);
|
|
}
|
|
ZeroStruct(w32_watch);
|
|
|
|
wchar_t *dir_path_wstr = WstrFromString(scratch.arena, dir_path);
|
|
w32_watch->dir_handle = CreateFileW(
|
|
dir_path_wstr,
|
|
FILE_LIST_DIRECTORY,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
0,
|
|
OPEN_EXISTING,
|
|
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
|
|
0
|
|
);
|
|
|
|
w32_watch->wake_handle = CreateEventW(0, 0, 0, 0);
|
|
|
|
EndScratch(scratch);
|
|
return (P_Watch *)w32_watch;
|
|
}
|
|
|
|
void P_ReleaseWatch(P_Watch *dw)
|
|
{
|
|
P_W32_Watch *w32_watch = (P_W32_Watch *)dw;
|
|
struct P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
CloseHandle(w32_watch->dir_handle);
|
|
CloseHandle(w32_watch->wake_handle);
|
|
|
|
P_Lock lock = P_LockE(&g->watches_mutex);
|
|
{
|
|
w32_watch->next_free = g->watches_first_free;
|
|
g->watches_first_free = w32_watch;
|
|
}
|
|
P_Unlock(&lock);
|
|
}
|
|
|
|
P_WatchInfoList P_ReadWatchWait(Arena *arena, P_Watch *dw)
|
|
{
|
|
__prof;
|
|
P_W32_Watch *w32_watch = (P_W32_Watch *)dw;
|
|
P_WatchInfoList 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 = 0;
|
|
while (!done)
|
|
{
|
|
OVERLAPPED ov = ZI;
|
|
ov.hEvent = CreateEventW(0, 0, 0, 0);
|
|
Assert(ov.hEvent);
|
|
|
|
BOOL success = ReadDirectoryChangesW(w32_watch->dir_handle,
|
|
w32_watch->results_buff,
|
|
countof(w32_watch->results_buff),
|
|
1,
|
|
filter,
|
|
0,
|
|
&ov,
|
|
0);
|
|
(UNUSED)success;
|
|
Assert(success);
|
|
|
|
HANDLE handles[] = {
|
|
ov.hEvent,
|
|
w32_watch->wake_handle
|
|
};
|
|
DWORD wait_res = WaitForMultipleObjects(2, handles, 0, INFINITE);
|
|
|
|
if (wait_res == WAIT_OBJECT_0)
|
|
{
|
|
i64 offset = 0;
|
|
while (!done)
|
|
{
|
|
FILE_NOTIFY_INFORMATION *res = (FILE_NOTIFY_INFORMATION *)(w32_watch->results_buff + offset);
|
|
|
|
P_WatchInfo *info = PushStruct(arena, P_WatchInfo);
|
|
if (list.last)
|
|
{
|
|
list.last->next = info;
|
|
info->prev = list.last;
|
|
}
|
|
else
|
|
{
|
|
list.first = info;
|
|
}
|
|
list.last = info;
|
|
++list.count;
|
|
|
|
String16 name16 = ZI;
|
|
name16.text = res->FileName;
|
|
name16.len = res->FileNameLength / sizeof(wchar_t);
|
|
|
|
info->name = StringFromString16(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 = P_WatchInfoKind_Added;
|
|
} break;
|
|
|
|
case FILE_ACTION_REMOVED:
|
|
{
|
|
info->kind = P_WatchInfoKind_Removed;
|
|
} break;
|
|
|
|
case FILE_ACTION_MODIFIED:
|
|
{
|
|
info->kind = P_WatchInfoKind_Modified;
|
|
} break;
|
|
|
|
case FILE_ACTION_RENAMED_OLD_NAME:
|
|
{
|
|
info->kind = P_WatchInfoKind_RenamedOld;
|
|
} break;
|
|
|
|
case FILE_ACTION_RENAMED_NEW_NAME:
|
|
{
|
|
info->kind = P_WatchInfoKind_RenamedNew;
|
|
} break;
|
|
|
|
default:
|
|
{
|
|
info->kind = P_WatchInfoKind_Unknown;
|
|
} break;
|
|
}
|
|
|
|
if (res->NextEntryOffset == 0)
|
|
{
|
|
done = 1;
|
|
}
|
|
else
|
|
{
|
|
offset += res->NextEntryOffset;
|
|
}
|
|
}
|
|
}
|
|
else if (wait_res == WAIT_OBJECT_0 + 1)
|
|
{
|
|
ResetEvent(w32_watch->wake_handle);
|
|
done = 1;
|
|
}
|
|
else
|
|
{
|
|
Assert(0);
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
void P_WakeWatch(P_Watch *dw)
|
|
{
|
|
P_W32_Watch *w32_watch = (P_W32_Watch *)dw;
|
|
SetEvent(w32_watch->wake_handle);
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Window
|
|
|
|
P_Window *P_AllocWindow(void)
|
|
{
|
|
__prof;
|
|
return (P_Window *)P_W32_AllocWindow();
|
|
}
|
|
|
|
void P_ReleaseWindow(P_Window *p_window)
|
|
{
|
|
__prof;
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
P_W32_ReleaseWindow(window);
|
|
}
|
|
|
|
//- Window events
|
|
P_WindowEventArray P_PopWindowEvents(Arena *arena, P_Window *p_window)
|
|
{
|
|
__prof;
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
i32 event_arena_index = 0;
|
|
{
|
|
/* Swap event buffers */
|
|
P_Lock lock = P_LockE(&window->event_arena_swp_mutex);
|
|
event_arena_index = window->current_event_arena_index;
|
|
window->current_event_arena_index = 1 - window->current_event_arena_index;
|
|
P_Unlock(&lock);
|
|
}
|
|
Arena *events_arena = window->event_arenas[event_arena_index];
|
|
P_WindowEventArray events = ZI;
|
|
events.count = events_arena->pos / sizeof(P_WindowEvent);
|
|
events.events = PushStructsNoZero(arena, P_WindowEvent, events.count);
|
|
CopyBytes(events.events, ArenaBase(events_arena), events_arena->pos);
|
|
ResetArena(events_arena);
|
|
return events;
|
|
}
|
|
|
|
//- Window settings
|
|
void P_UpdateWindowSettings(P_Window *p_window, P_WindowSettings *settings)
|
|
{
|
|
__prof;
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
P_Lock lock = P_LockE(&window->settings_mutex);
|
|
{
|
|
P_W32_UpdateWindowFromSettings(window, settings);
|
|
}
|
|
P_Unlock(&lock);
|
|
}
|
|
|
|
/* FIXME: Lock settings mutex for these functions */
|
|
|
|
P_WindowSettings P_GetWindowSettings(P_Window *p_window)
|
|
{
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
return window->settings;
|
|
}
|
|
|
|
void P_ShowWindow(P_Window *p_window)
|
|
{
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
HWND hwnd = window->hwnd;
|
|
P_Lock lock = P_LockE(&window->settings_mutex);
|
|
{
|
|
i32 show_cmd = SW_NORMAL;
|
|
P_WindowSettings *settings = &window->settings;
|
|
if (settings->flags & P_WindowSettingsFlag_Maximized)
|
|
{
|
|
show_cmd = SW_SHOWMAXIMIZED;
|
|
}
|
|
else if (settings->flags & P_WindowSettingsFlag_Minimized)
|
|
{
|
|
show_cmd = SW_MINIMIZE;
|
|
}
|
|
window->flags |= P_WindowFlag_Showing;
|
|
ShowWindow(hwnd, show_cmd);
|
|
BringWindowToTop(hwnd);
|
|
}
|
|
P_Unlock(&lock);
|
|
}
|
|
|
|
void P_SetWindowCursorPos(P_Window *p_window, Vec2 pos)
|
|
{
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
window->cursor_set_position = pos;
|
|
window->cursor_set_flags |= P_W32_CursorFlag_Position;
|
|
P_W32_WakeWindow(window);
|
|
}
|
|
|
|
void P_ShowWindowCursor(P_Window *p_window)
|
|
{
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
window->cursor_set_flags |= P_W32_CursorFlag_Show;
|
|
P_W32_WakeWindow(window);
|
|
}
|
|
|
|
void P_HideWindowCursor(P_Window *p_window)
|
|
{
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
window->cursor_set_flags |= P_W32_CursorFlag_Hide;
|
|
P_W32_WakeWindow(window);
|
|
}
|
|
|
|
void P_EnableWindoweCursorClip(P_Window *p_window, Rect bounds)
|
|
{
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
window->cursor_clip_bounds = bounds;
|
|
window->cursor_set_flags |= P_W32_CursorFlag_EnableClip;
|
|
P_W32_WakeWindow(window);
|
|
}
|
|
|
|
void P_DisableWindoweCursorClip(P_Window *p_window)
|
|
{
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
window->cursor_set_flags |= P_W32_CursorFlag_DisableClip;
|
|
P_W32_WakeWindow(window);
|
|
}
|
|
|
|
void P_ToggleWindowTopmost(P_Window *p_window)
|
|
{
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
Atomic32FetchAdd(&window->topmost_toggles, 1);
|
|
P_W32_WakeWindow(window);
|
|
}
|
|
|
|
//- Window info
|
|
Vec2 P_GetWindowSize(P_Window *p_window)
|
|
{
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
return VEC2((f32)window->width, (f32)window->height);
|
|
}
|
|
|
|
Vec2 P_GetWindowMonitorSize(P_Window *p_window)
|
|
{
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
return VEC2((f32)window->monitor_width, (f32)window->monitor_height);
|
|
}
|
|
|
|
u64 P_GetInternalWindowHandle(P_Window *p_window)
|
|
{
|
|
P_W32_Window *window = (P_W32_Window *)p_window;
|
|
return (u64)window->hwnd;
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Address
|
|
|
|
P_Address P_AddressFromIpPortCstr(char *ip_cstr, char *port_cstr)
|
|
{
|
|
P_Address res = ZI;
|
|
|
|
struct addrinfo hints = ZI;
|
|
hints.ai_family = AF_UNSPEC;
|
|
hints.ai_socktype = SOCK_DGRAM;
|
|
hints.ai_flags = AI_PASSIVE;
|
|
|
|
struct addrinfo *ai_res = 0;
|
|
i32 status = getaddrinfo(ip_cstr, port_cstr, &hints, &ai_res);
|
|
if (status == 0)
|
|
{
|
|
while (ai_res)
|
|
{
|
|
if (ai_res->ai_family == AF_INET)
|
|
{
|
|
struct sockaddr_in *sockaddr = (struct sockaddr_in *)ai_res->ai_addr;
|
|
res.valid = 1;
|
|
res.family = P_AddressFamily_Ipv4;
|
|
res.portnb = sockaddr->sin_port;
|
|
StaticAssert(sizeof(sockaddr->sin_addr) == 4);
|
|
CopyBytes(res.ipnb, (void *)&sockaddr->sin_addr, 4);
|
|
break;
|
|
}
|
|
else if (ai_res->ai_family == AF_INET6)
|
|
{
|
|
/* TODO: Enable ipv6 */
|
|
#if 0
|
|
struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)ai_res->ai_addr;
|
|
res.valid = 1;
|
|
res.family = P_AddressFamily_Ipv6;
|
|
res.portnb = sockaddr->sin6_port;
|
|
StaticAssert(sizeof(sockaddr->sin6_addr) == 16);
|
|
CopyBytes(res.ipnb, (void *)&sockaddr->sin6_addr, 16);
|
|
break;
|
|
#endif
|
|
}
|
|
ai_res = ai_res->ai_next;
|
|
}
|
|
freeaddrinfo(ai_res);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
P_Address P_AddressFromString(String str)
|
|
{
|
|
/* Parse string into ip & port */
|
|
u8 ip_buff[1024];
|
|
u8 port_buff[countof(ip_buff)];
|
|
char *ip_cstr = 0;
|
|
char *port_cstr = 0;
|
|
{
|
|
u64 colon_count = 0;
|
|
for (u64 i = 0; i < str.len; ++i)
|
|
{
|
|
u8 c = str.text[i];
|
|
if (c == ':')
|
|
{
|
|
++colon_count;
|
|
}
|
|
}
|
|
u64 ip_len = 0;
|
|
u64 port_len = 0;
|
|
u64 parse_len = MinU64(MinU64(str.len, countof(ip_buff) - 1), countof(port_buff) - 1);
|
|
if (colon_count > 1 && str.text[0] == '[')
|
|
{
|
|
/* Parse ipv6 with port */
|
|
b32 parse_addr = 1;
|
|
for (u64 i = 1; i < parse_len; ++i)
|
|
{
|
|
u8 c = str.text[i];
|
|
if (parse_addr)
|
|
{
|
|
if (c == ']')
|
|
{
|
|
parse_addr = 0;
|
|
}
|
|
else
|
|
{
|
|
ip_buff[ip_len] = c;
|
|
++ip_len;
|
|
}
|
|
}
|
|
else if (c != ':')
|
|
{
|
|
port_buff[port_len] = c;
|
|
++port_len;
|
|
}
|
|
}
|
|
}
|
|
else if (colon_count == 1)
|
|
{
|
|
/* Parse address with port */
|
|
b32 parse_addr = 1;
|
|
for (u64 i = 0; i < parse_len; ++i)
|
|
{
|
|
u8 c = str.text[i];
|
|
if (parse_addr)
|
|
{
|
|
if (c == ':')
|
|
{
|
|
parse_addr = 0;
|
|
}
|
|
else
|
|
{
|
|
ip_buff[ip_len] = c;
|
|
++ip_len;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
port_buff[port_len] = c;
|
|
++port_len;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* CopyStruct address without port */
|
|
ip_len = MinU64(str.len, countof(ip_buff) - 1);
|
|
CopyBytes(ip_buff, str.text, ip_len);
|
|
}
|
|
if (ip_len > 0)
|
|
{
|
|
ip_buff[ip_len] = 0;
|
|
ip_cstr = (char *)ip_buff;
|
|
}
|
|
if (port_len > 0)
|
|
{
|
|
port_buff[port_len] = 0;
|
|
port_cstr = (char *)port_buff;
|
|
|
|
}
|
|
}
|
|
|
|
P_Address res = P_AddressFromIpPortCstr(ip_cstr, port_cstr);
|
|
return res;
|
|
}
|
|
|
|
P_Address P_AddressFromPort(u16 port)
|
|
{
|
|
u8 port_buff[128];
|
|
char *port_cstr = 0;
|
|
{
|
|
u8 port_buff_reverse[countof(port_buff)];
|
|
u64 port_len = 0;
|
|
while (port > 0 && port_len < (countof(port_buff) - 1))
|
|
{
|
|
u8 digit = port % 10;
|
|
port /= 10;
|
|
port_buff_reverse[port_len] = '0' + digit;
|
|
++port_len;
|
|
}
|
|
for (u64 i = 0; i < port_len; ++i)
|
|
{
|
|
u64 j = port_len - 1 - i;
|
|
port_buff[i] = port_buff_reverse[j];
|
|
}
|
|
if (port_len > 0)
|
|
{
|
|
port_buff[port_len] = 0;
|
|
port_cstr = (char *)port_buff;
|
|
}
|
|
}
|
|
|
|
P_Address res = P_AddressFromIpPortCstr(0, port_cstr);
|
|
|
|
return res;
|
|
}
|
|
|
|
String P_StringFromAddress(Arena *arena, P_Address address)
|
|
{
|
|
String res = ZI;
|
|
|
|
if (address.family == P_AddressFamily_Ipv6)
|
|
{
|
|
/* TODO */
|
|
}
|
|
else
|
|
{
|
|
u8 ip[4];
|
|
for (u32 i = 0; i < 4; ++i)
|
|
{
|
|
ip[i] = ntohs(address.ipnb[i]);
|
|
}
|
|
u16 port = ntohs(address.portnb);
|
|
res = StringFormat(arena, Lit("%F.%F.%F.%F:%F"), FmtUint(ip[0]), FmtUint(ip[1]), FmtUint(ip[2]), FmtUint(ip[3]), FmtUint(port));
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
b32 P_AddressIsEqual(P_Address a, P_Address b)
|
|
{
|
|
return EqStruct(&a, &b);
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Sock
|
|
|
|
P_Sock *P_AllocSock(u16 listen_port, u64 sndbuf_size, u64 rcvbuf_size)
|
|
{
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
P_W32_Sock *ws = 0;
|
|
{
|
|
P_Lock lock = P_LockE(&g->socks_mutex);
|
|
if (g->first_free_sock)
|
|
{
|
|
ws = g->first_free_sock;
|
|
g->first_free_sock = ws->next_free;
|
|
}
|
|
else
|
|
{
|
|
ws = PushStructNoZero(g->socks_arena, P_W32_Sock);
|
|
}
|
|
P_Unlock(&lock);
|
|
}
|
|
ZeroStruct(ws);
|
|
|
|
P_Address addr = P_AddressFromPort(listen_port);
|
|
P_W32_Address bind_address = P_W32_Win32AddressFromPlatformAddress(addr);
|
|
ws->sock = socket(bind_address.family, SOCK_DGRAM, IPPROTO_UDP);
|
|
{
|
|
i32 sb = sndbuf_size;
|
|
i32 rb = rcvbuf_size;
|
|
u32 imode = 1;
|
|
setsockopt(ws->sock, SOL_SOCKET, SO_SNDBUF, (char *)&sb, sizeof(sb));
|
|
setsockopt(ws->sock, SOL_SOCKET, SO_RCVBUF, (char *)&rb, sizeof(rb));
|
|
ioctlsocket(ws->sock, FIONBIO, (unsigned long *)&imode);
|
|
}
|
|
bind(ws->sock, &bind_address.sa, bind_address.size);
|
|
|
|
return (P_Sock *)ws;
|
|
}
|
|
|
|
void P_ReleaseSock(P_Sock *sock)
|
|
{
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
P_W32_Sock *ws = (P_W32_Sock *)sock;
|
|
closesocket(ws->sock);
|
|
P_Lock lock = P_LockE(&g->socks_mutex);
|
|
{
|
|
ws->next_free = g->first_free_sock;
|
|
g->first_free_sock = ws;
|
|
}
|
|
P_Unlock(&lock);
|
|
}
|
|
|
|
P_SockReadResult P_ReadSock(Arena *arena, P_Sock *sock)
|
|
{
|
|
P_W32_Sock *ws = (P_W32_Sock *)sock;
|
|
|
|
u64 read_buff_size = Kibi(64);
|
|
String read_buff = ZI;
|
|
read_buff.len = read_buff_size;
|
|
read_buff.text = PushStructsNoZero(arena, u8, read_buff_size);
|
|
|
|
P_SockReadResult res = ZI;
|
|
|
|
P_W32_Address ws_addr = ZI;
|
|
ws_addr.size = sizeof(ws_addr.sas);
|
|
|
|
i32 size = recvfrom(ws->sock, (char *)read_buff.text, read_buff.len, 0, &ws_addr.sa, &ws_addr.size);
|
|
ws_addr.family = ws_addr.sin.sin_family;
|
|
|
|
res.address = P_W32_PlatformAddressFromWin32Address(ws_addr);
|
|
if (size >= 0)
|
|
{
|
|
AddGstat(GSTAT_SOCK_BYTES_RECEIVED, size);
|
|
res.data.text = read_buff.text;
|
|
res.data.len = size;
|
|
res.valid = 1;
|
|
|
|
/* PopStruct arena back to end of msg */
|
|
PopTo(arena, arena->pos - read_buff_size + size);
|
|
}
|
|
else
|
|
{
|
|
#if RtcIsEnabled
|
|
i32 err = WSAGetLastError();
|
|
if (err != WSAEWOULDBLOCK && err != WSAETIMEDOUT && err != WSAECONNRESET)
|
|
{
|
|
Assert(0);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
void P_WriteSock(P_Sock *sock, P_Address address, String data)
|
|
{
|
|
P_W32_Sock *ws = (P_W32_Sock *)sock;
|
|
P_W32_Address ws_addr = P_W32_Win32AddressFromPlatformAddress(address);
|
|
i32 size = sendto(ws->sock, (char *)data.text, data.len, 0, &ws_addr.sa, ws_addr.size);
|
|
if (size > 0)
|
|
{
|
|
AddGstat(GSTAT_SOCK_BYTES_SENT, size);
|
|
}
|
|
#if RtcIsEnabled
|
|
if (size != (i32)data.len)
|
|
{
|
|
i32 err = WSAGetLastError();
|
|
(UNUSED)err;
|
|
Assert(0);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Util
|
|
|
|
void P_MessageBox(P_MessageBoxKind kind, String message)
|
|
{
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
|
|
wchar_t *message_wstr = WstrFromString(scratch.arena, message);
|
|
const wchar_t *title = L"";
|
|
UINT mbox_type = MB_SETFOREGROUND;
|
|
|
|
switch (kind)
|
|
{
|
|
case P_MessageBoxKind_Ok:
|
|
{
|
|
mbox_type |= MB_ICONINFORMATION;
|
|
} break;
|
|
|
|
case P_MessageBoxKind_Warning:
|
|
{
|
|
title = L"Warning";
|
|
mbox_type |= MB_ICONWARNING;
|
|
} break;
|
|
|
|
case P_MessageBoxKind_Error:
|
|
{
|
|
title = L"Error";
|
|
mbox_type |= MB_ICONERROR;
|
|
} break;
|
|
|
|
case P_MessageBoxKind_Fatal:
|
|
{
|
|
title = L"Fatal error";
|
|
mbox_type |= MB_ICONSTOP;
|
|
} break;
|
|
}
|
|
|
|
P_LogDebugF("Showing message box kind %F with text \"%F\"", FmtSint(kind), FmtString(message));
|
|
MessageBoxExW(0, message_wstr, title, mbox_type, 0);
|
|
|
|
EndScratch(scratch);
|
|
}
|
|
|
|
void P_SetClipboardText(String str)
|
|
{
|
|
if (OpenClipboard(0))
|
|
{
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
String16 str16 = String16FromString(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);
|
|
CopyBytes(dest_wstr, str16.text, str16_size_bytes);
|
|
dest_wstr[str16.len] = 0;
|
|
GlobalUnlock(handle);
|
|
SetClipboardData(CF_UNICODETEXT, handle);
|
|
}
|
|
CloseClipboard();
|
|
EndScratch(scratch);
|
|
}
|
|
}
|
|
|
|
String P_GetClipboardText(Arena *arena)
|
|
{
|
|
String res = ZI;
|
|
if (IsClipboardFormatAvailable(CF_UNICODETEXT) && OpenClipboard(0))
|
|
{
|
|
HANDLE handle = GetClipboardData(CF_UNICODETEXT);
|
|
if (handle)
|
|
{
|
|
u16 *src_wstr = (u16 *)GlobalLock(handle);
|
|
res = StringFromString16(arena, String16FromWstrNoLimit(src_wstr));
|
|
GlobalUnlock(handle);
|
|
}
|
|
CloseClipboard();
|
|
}
|
|
return res;
|
|
}
|
|
|
|
u32 P_GetLogicalProcessorCount(void)
|
|
{
|
|
return GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
|
|
}
|
|
|
|
u32 P_GetThreadId(void)
|
|
{
|
|
return GetCurrentThreadId();
|
|
}
|
|
|
|
i64 P_GetCurrentSchedulerPeriodNs(void)
|
|
{
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
return Atomic64Fetch(&g->current_scheduler_cycle_period_ns.v);
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Sleep
|
|
|
|
void P_SleepPrecise(i64 sleep_time_ns)
|
|
{
|
|
__prof;
|
|
|
|
i64 big_sleep = P_GetCurrentSchedulerPeriodNs();
|
|
i64 tolerance = (f64)big_sleep * 0.5;
|
|
//i64 tolerance = 1000000000;
|
|
|
|
i64 now_ns = P_TimeNs();
|
|
i64 target_ns = now_ns + sleep_time_ns;
|
|
|
|
/* Sleep */
|
|
while (now_ns < target_ns - big_sleep - tolerance)
|
|
{
|
|
__profn("Sleep part");
|
|
P_Wait(0, 0, 0, big_sleep);
|
|
now_ns = P_TimeNs();
|
|
}
|
|
|
|
/* Spin */
|
|
{
|
|
__profn("Sleep spin");
|
|
while (now_ns < target_ns)
|
|
{
|
|
IxPause();
|
|
now_ns = P_TimeNs();
|
|
}
|
|
}
|
|
}
|
|
|
|
void P_SleepFrame(i64 last_frame_time_ns, i64 target_dt_ns)
|
|
{
|
|
if (last_frame_time_ns != 0 && target_dt_ns > 0)
|
|
{
|
|
i64 now_ns = P_TimeNs();
|
|
i64 last_frame_dt_ns = now_ns - last_frame_time_ns;
|
|
i64 sleep_time_ns = target_dt_ns - last_frame_dt_ns;
|
|
if (sleep_time_ns > 0)
|
|
{
|
|
P_SleepPrecise(sleep_time_ns);
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Exit
|
|
|
|
void P_OnExit(P_ExitFunc *func)
|
|
{
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
i32 index = Atomic32FetchAdd(&g->num_exit_funcs, 1);
|
|
if (index >= P_W32_MaxOnExitFuncs)
|
|
{
|
|
P_Panic(Lit("Maximum on exit functions registered"));
|
|
}
|
|
g->exit_funcs[index] = func;
|
|
}
|
|
|
|
void P_Exit(void)
|
|
{
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
SetEvent(g->exit_begin_event);
|
|
}
|
|
|
|
void P_Panic(String msg)
|
|
{
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
if (Atomic32FetchTestSet(&g->panicking, 0, 1) == 0)
|
|
{
|
|
log_panic(msg);
|
|
|
|
wchar_t *wstr = g->panic_wstr;
|
|
u64 WstrLen = 0;
|
|
|
|
wchar_t prefix[] = L"A fatal error has occured and the application needs to exit:\n\n";
|
|
CopyBytes(wstr, prefix, MinU64(countof(g->panic_wstr), (countof(prefix) << 1)));
|
|
WstrLen += countof(prefix) - 1;
|
|
|
|
/* Perform manual string encode to avoid any implicit memory
|
|
* allocation (in case allocation is unreliable) */
|
|
String str8 = msg;
|
|
u64 pos8 = 0;
|
|
while (pos8 < str8.len)
|
|
{
|
|
String str8_remaining = { .len = (str8.len - pos8), .text = str8.text + pos8 };
|
|
Utf8DecodeResult decoded = DecodeUtf8(str8_remaining);
|
|
Utf16EncodeResult encoded = EncodeUtf16(decoded.codepoint);
|
|
u64 wstr_new_len = WstrLen + encoded.count16;
|
|
if (wstr_new_len < (countof(g->panic_wstr) - 1))
|
|
{
|
|
u16 *dest = wstr + WstrLen;
|
|
CopyBytes(dest, encoded.chars16, (encoded.count16 << 1));
|
|
WstrLen = wstr_new_len;
|
|
pos8 += decoded.advance8;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
wstr[WstrLen] = 0;
|
|
|
|
#if RtcIsEnabled
|
|
MessageBoxExW(0, wstr, L"Fatal error", MB_ICONSTOP | MB_SETFOREGROUND | MB_TOPMOST, 0);
|
|
Assert(0);
|
|
#endif
|
|
|
|
SetEvent(g->panic_event);
|
|
|
|
/* Wait for process termination */
|
|
if (GetCurrentThreadId() != g->main_thread_id)
|
|
{
|
|
Sleep(INFINITE);
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////
|
|
//~ Win32 entry point
|
|
|
|
void P_W32_InitBtnTable(void)
|
|
{
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
ZeroArray(g->vk_btn_table);
|
|
|
|
for (u32 i = 'A', j = P_Btn_A; i <= 'Z'; ++i, ++j)
|
|
{
|
|
g->vk_btn_table[i] = (P_Btn)j;
|
|
}
|
|
for (u32 i = '0', j = P_Btn_0; i <= '9'; ++i, ++j)
|
|
{
|
|
g->vk_btn_table[i] = (P_Btn)j;
|
|
}
|
|
for (u32 i = VK_F1, j = P_Btn_F1; i <= VK_F24; ++i, ++j)
|
|
{
|
|
g->vk_btn_table[i] = (P_Btn)j;
|
|
}
|
|
|
|
g->vk_btn_table[VK_ESCAPE] = P_Btn_ESC;
|
|
g->vk_btn_table[VK_OEM_3] = P_Btn_GraveAccent;
|
|
g->vk_btn_table[VK_OEM_MINUS] = P_Btn_Minus;
|
|
g->vk_btn_table[VK_OEM_PLUS] = P_Btn_Equal;
|
|
g->vk_btn_table[VK_BACK] = P_Btn_Backspace;
|
|
g->vk_btn_table[VK_TAB] = P_Btn_Tab;
|
|
g->vk_btn_table[VK_SPACE] = P_Btn_Space;
|
|
g->vk_btn_table[VK_RETURN] = P_Btn_Enter;
|
|
g->vk_btn_table[VK_CONTROL] = P_Btn_Ctrl;
|
|
g->vk_btn_table[VK_SHIFT] = P_Btn_Shift;
|
|
g->vk_btn_table[VK_MENU] = P_Btn_Alt;
|
|
g->vk_btn_table[VK_UP] = P_Btn_Up;
|
|
g->vk_btn_table[VK_LEFT] = P_Btn_Left;
|
|
g->vk_btn_table[VK_DOWN] = P_Btn_Down;
|
|
g->vk_btn_table[VK_RIGHT] = P_Btn_Right;
|
|
g->vk_btn_table[VK_DELETE] = P_Btn_Delete;
|
|
g->vk_btn_table[VK_PRIOR] = P_Btn_PageUp;
|
|
g->vk_btn_table[VK_NEXT] = P_Btn_PageDown;
|
|
g->vk_btn_table[VK_HOME] = P_Btn_Home;
|
|
g->vk_btn_table[VK_END] = P_Btn_End;
|
|
g->vk_btn_table[VK_OEM_2] = P_Btn_ForwardSlash;
|
|
g->vk_btn_table[VK_OEM_PERIOD] = P_Btn_Period;
|
|
g->vk_btn_table[VK_OEM_COMMA] = P_Btn_Comma;
|
|
g->vk_btn_table[VK_OEM_7] = P_Btn_Quote;
|
|
g->vk_btn_table[VK_OEM_4] = P_Btn_LeftBracket;
|
|
g->vk_btn_table[VK_OEM_6] = P_Btn_RightBracket;
|
|
g->vk_btn_table[VK_INSERT] = P_Btn_Insert;
|
|
g->vk_btn_table[VK_OEM_1] = P_Btn_Semicolon;
|
|
}
|
|
|
|
P_JobDef(P_W32_AppStartupJob, _)
|
|
{
|
|
(UNUSED)_;
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
{
|
|
String cmdline_args = StringFromWstr(scratch.arena, g->cmdline_args_wstr, countof(g->cmdline_args_wstr));
|
|
P_AppStartup(cmdline_args);
|
|
SetEvent(g->startup_end_event);
|
|
}
|
|
EndScratch(scratch);
|
|
}
|
|
|
|
P_JobDef(P_W32_AppShutdownJob, _)
|
|
{
|
|
__prof;
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
(UNUSED)_;
|
|
i32 num_funcs = Atomic32Fetch(&g->num_exit_funcs);
|
|
for (i32 i = num_funcs - 1; i >= 0; --i)
|
|
{
|
|
P_ExitFunc *func = g->exit_funcs[i];
|
|
func();
|
|
}
|
|
SetEvent(g->exit_end_event);
|
|
}
|
|
|
|
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;
|
|
|
|
__profthread("Main thread", PROF_THREAD_GROUP_MAIN);
|
|
P_W32_SharedCtx *g = &P_W32_shared_ctx;
|
|
|
|
#if ProfilingIsEnabled
|
|
/* Start profiler */
|
|
{
|
|
__profn("Launch profiler");
|
|
STARTUPINFO si = ZI;
|
|
si.cb = sizeof(si);
|
|
PROCESS_INFORMATION pi = ZI;
|
|
wchar_t cmd[sizeof(ProfilingIsEnabled_CMD_WSTR)] = ZI;
|
|
CopyBytes(cmd, ProfilingIsEnabled_CMD_WSTR, sizeof(ProfilingIsEnabled_CMD_WSTR));
|
|
DeleteFileW(ProfilingIsEnabled_FILE_WSTR);
|
|
b32 success = CreateProcessW(0, cmd, 0, 0, 0, DETACHED_PROCESS, 0, 0, &si, &pi);
|
|
if (!success)
|
|
{
|
|
MessageBoxExW(0, L"Failed to launch profiler using command '" ProfilingIsEnabled_CMD_WSTR L"'.", L"Error", MB_ICONSTOP | MB_SETFOREGROUND | MB_TOPMOST, 0);
|
|
}
|
|
}
|
|
/* Set internal profiler thread affinities */
|
|
{
|
|
__profn("Set profiler thread affinities");
|
|
wchar_t *prefix_name_wstr = PROFILER_THREAD_PREFIX_WSTR;
|
|
u64 prefix_name_wstr_len = ((i32)sizeof(PROFILER_THREAD_PREFIX_WSTR) >> 1) - 1;
|
|
if (prefix_name_wstr_len > 0 && PROFILER_THREAD_AFFINITY_MASK != 0)
|
|
{
|
|
DWORD proc_id = GetCurrentProcessId();
|
|
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
|
|
if (snapshot != INVALID_HANDLE_VALUE)
|
|
{
|
|
THREADENTRY32 te = ZI;
|
|
te.dwSize = sizeof(THREADENTRY32);
|
|
if (Thread32First(snapshot, &te))
|
|
{
|
|
do
|
|
{
|
|
if (te.th32OwnerProcessID == proc_id)
|
|
{
|
|
i32 thread_id = te.th32ThreadID;
|
|
HANDLE thread = OpenThread(THREAD_ALL_ACCESS, FALSE, thread_id);
|
|
if (thread)
|
|
{
|
|
wchar_t *thread_name_wstr = 0;
|
|
HRESULT hr = GetThreadDescription(thread, &thread_name_wstr);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
u64 thread_name_len = WstrLenNoLimit(thread_name_wstr);
|
|
if (thread_name_len >= prefix_name_wstr_len && EqBytes(thread_name_wstr, prefix_name_wstr, prefix_name_wstr_len))
|
|
{
|
|
__profn("Set profiler thread affinity");
|
|
b32 success = SetThreadAffinityMask(thread, PROFILER_THREAD_AFFINITY_MASK) != 0;
|
|
{
|
|
/* Retry until external tools can set correct process affinity */
|
|
i32 delay_ms = 16;
|
|
while (!success && delay_ms <= 1024)
|
|
{
|
|
__profn("Profiler thread affinity retry");
|
|
Sleep(delay_ms);
|
|
success = SetThreadAffinityMask(thread, PROFILER_THREAD_AFFINITY_MASK) != 0;
|
|
delay_ms *= 2;
|
|
}
|
|
}
|
|
Assert(success);
|
|
(UNUSED)success;
|
|
}
|
|
}
|
|
CloseHandle(thread);
|
|
}
|
|
}
|
|
} while (Thread32Next(snapshot, &te));
|
|
}
|
|
}
|
|
CloseHandle(snapshot);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* Set up exit events */
|
|
g->panic_event = CreateEventW(0, 1, 0, 0);
|
|
g->startup_end_event = CreateEventW(0, 1, 0, 0);
|
|
g->exit_begin_event = CreateEventW(0, 1, 0, 0);
|
|
g->exit_end_event = CreateEventW(0, 1, 0, 0);
|
|
|
|
/* Init timer */
|
|
{
|
|
LARGE_INTEGER qpf;
|
|
QueryPerformanceFrequency(&qpf);
|
|
g->ns_per_qpc = 1000000000 / qpf.QuadPart;
|
|
}
|
|
{
|
|
LARGE_INTEGER qpc;
|
|
QueryPerformanceCounter(&qpc);
|
|
g->timer_start_qpc = qpc.QuadPart;
|
|
}
|
|
|
|
/* Init fibers */
|
|
g->num_fibers = 1; /* Fiber at index 0 always nil */
|
|
g->fiber_names_arena = AllocArena(Gibi(64));
|
|
|
|
/* Init wait lists */
|
|
g->wait_lists_arena = AllocArena(Gibi(64));
|
|
|
|
/* Convert main thread to fiber */
|
|
P_W32_AllocFiber(0);
|
|
|
|
/* Init job pools */
|
|
for (P_Pool pool_kind = 0; pool_kind < (i32)countof(g->job_pools); ++pool_kind)
|
|
{
|
|
P_W32_JobPool *pool = &g->job_pools[pool_kind];
|
|
|
|
/* Init queues */
|
|
for (P_Priority priority = 0; priority < (i32)countof(pool->job_queues); ++priority)
|
|
{
|
|
P_W32_JobQueue *queue = &pool->job_queues[priority];
|
|
queue->arena = AllocArena(Gibi(64));
|
|
}
|
|
}
|
|
|
|
u64 cmdline_len = WstrLen(cmdline_wstr, countof(g->cmdline_args_wstr) - 1);
|
|
CopyBytes(g->cmdline_args_wstr, cmdline_wstr, cmdline_len * sizeof(*cmdline_wstr));
|
|
g->cmdline_args_wstr[cmdline_len] = 0;
|
|
|
|
g->main_thread_id = GetCurrentThreadId();
|
|
SetThreadDescription(GetCurrentThread(), L"Main thread");
|
|
|
|
/* Query system info */
|
|
GetSystemInfo(&g->info);
|
|
|
|
/* Initialize vk table */
|
|
P_W32_InitBtnTable();
|
|
|
|
/* Create window class */
|
|
{
|
|
/* Register the window class */
|
|
WNDCLASSEXW *wc = &g->window_class;
|
|
wc->cbSize = sizeof(WNDCLASSEX);
|
|
wc->lpszClassName = P_W32_WindowClassName;
|
|
wc->hCursor = LoadCursor(0, IDC_ARROW);
|
|
wc->style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
|
|
//wc->hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
|
|
wc->lpfnWndProc = P_W32_Win32WindowProc;
|
|
wc->hInstance = instance;
|
|
|
|
/* Use first icon resource as window icon (same as explorer) */
|
|
wchar_t path[4096] = ZI;
|
|
GetModuleFileNameW(instance, path, countof(path));
|
|
ExtractIconExW(path, 0, &wc->hIcon, &wc->hIconSm, 1);
|
|
|
|
if (!RegisterClassExW(wc))
|
|
{
|
|
P_Panic(Lit("Failed to register window class"));
|
|
}
|
|
}
|
|
|
|
/* Register raw input */
|
|
if (!Atomic32Fetch(&g->panicking))
|
|
{
|
|
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 */
|
|
};
|
|
|
|
b32 success = RegisterRawInputDevices(&rid, 1, sizeof(rid));
|
|
Assert(success);
|
|
(UNUSED)success;
|
|
}
|
|
|
|
/* Init threads pool */
|
|
g->threads_arena = AllocArena(Gibi(64));
|
|
|
|
/* Init watches pool */
|
|
g->watches_arena = AllocArena(Gibi(64));
|
|
|
|
/* Init windows pool */
|
|
g->windows_arena = AllocArena(Gibi(64));
|
|
|
|
/* Init winsock */
|
|
WSAStartup(MAKEWORD(2, 2), &g->wsa_data);
|
|
g->socks_arena = AllocArena(Gibi(64));
|
|
|
|
/* Start job scheduler */
|
|
Atomic64FetchSet(&g->current_scheduler_cycle_period_ns.v, P_W32_DefaultSchedulerPeriodNs);
|
|
P_W32_Thread *scheduler_thread = P_W32_AllocThread(P_W32_JobSchedulerEntryFunc, 0, Lit("Scheduler thread"), PROF_THREAD_GROUP_SCHEDULER);
|
|
|
|
//- Start job workers
|
|
/* TODO: Heuristic worker counts & affinities */
|
|
{
|
|
__profn("Start job workers");
|
|
for (P_Pool pool_kind = 0; pool_kind < (i32)countof(g->job_pools); ++pool_kind)
|
|
{
|
|
P_W32_JobPool *pool = &g->job_pools[pool_kind];
|
|
String name_fmt = ZI;
|
|
i32 prof_group = PROF_THREAD_GROUP_FIBERS - Mebi(pool_kind);
|
|
switch (pool_kind)
|
|
{
|
|
default: Assert(0); break;
|
|
|
|
case P_Pool_Sim:
|
|
{
|
|
name_fmt = Lit("Sim worker #%F");
|
|
pool->num_worker_threads = 4;
|
|
pool->thread_affinity_mask = 0x000000000000000Full;
|
|
pool->thread_priority = THREAD_PRIORITY_TIME_CRITICAL;
|
|
} break;
|
|
|
|
case P_Pool_User:
|
|
{
|
|
name_fmt = Lit("User worker #%F");
|
|
pool->num_worker_threads = 4;
|
|
pool->thread_affinity_mask = 0x00000000000000F0ull;
|
|
pool->thread_priority = THREAD_PRIORITY_TIME_CRITICAL;
|
|
} break;
|
|
|
|
case P_Pool_Audio:
|
|
{
|
|
name_fmt = Lit("Audio worker #%F");
|
|
pool->num_worker_threads = 2;
|
|
pool->thread_affinity_mask = 0x0000000000000300ull;
|
|
pool->thread_priority = THREAD_PRIORITY_TIME_CRITICAL;
|
|
pool->thread_is_audio = 1;
|
|
} break;
|
|
|
|
case P_Pool_Background:
|
|
{
|
|
name_fmt = Lit("Background worker #%F");
|
|
pool->num_worker_threads = 2;
|
|
pool->thread_affinity_mask = 0x0000000000000C00ull;
|
|
} break;
|
|
|
|
case P_Pool_Floating:
|
|
{
|
|
name_fmt = Lit("Floating worker #%F");
|
|
pool->num_worker_threads = 8;
|
|
pool->thread_affinity_mask = 0x0000000000000FFFull;
|
|
} break;
|
|
}
|
|
pool->worker_threads_arena = AllocArena(Gibi(64));
|
|
pool->worker_threads = PushStructs(pool->worker_threads_arena, P_W32_Thread *, pool->num_worker_threads);
|
|
pool->worker_contexts = PushStructs(pool->worker_threads_arena, P_W32_WorkerCtx, pool->num_worker_threads);
|
|
for (i32 i = 0; i < pool->num_worker_threads; ++i)
|
|
{
|
|
P_W32_WorkerCtx *ctx = &pool->worker_contexts[i];
|
|
ctx->pool_kind = pool_kind;
|
|
ctx->id = i;
|
|
String name = StringFormat(pool->worker_threads_arena, name_fmt, FmtSint(i));
|
|
pool->worker_threads[i] = P_W32_AllocThread(P_W32_JobWorkerEntryFunc, ctx, name, prof_group + i);
|
|
}
|
|
}
|
|
}
|
|
|
|
//- App startup
|
|
|
|
/* Run app start job */
|
|
if (!Atomic32Fetch(&g->panicking))
|
|
{
|
|
P_Run(1, P_W32_AppStartupJob, 0, P_Pool_Floating, P_Priority_High, 0);
|
|
}
|
|
|
|
/* Wait for startup end or panic */
|
|
if (!Atomic32Fetch(&g->panicking))
|
|
{
|
|
HANDLE handles[] = {
|
|
g->startup_end_event,
|
|
g->panic_event
|
|
};
|
|
WaitForMultipleObjects(countof(handles), handles, 0, INFINITE);
|
|
}
|
|
|
|
/* Wait for exit start or panic */
|
|
if (!Atomic32Fetch(&g->panicking))
|
|
{
|
|
HANDLE handles[] = {
|
|
g->exit_begin_event,
|
|
g->panic_event
|
|
};
|
|
WaitForMultipleObjects(countof(handles), handles, 0, INFINITE);
|
|
}
|
|
|
|
//- App shutdown
|
|
|
|
/* Run exit callbacks job */
|
|
if (!Atomic32Fetch(&g->panicking))
|
|
{
|
|
P_Run(1, P_W32_AppShutdownJob, 0, P_Pool_Floating, P_Priority_High, 0);
|
|
}
|
|
|
|
/* Wait for exit end or panic */
|
|
if (!Atomic32Fetch(&g->panicking))
|
|
{
|
|
HANDLE handles[] = {
|
|
g->exit_end_event,
|
|
g->panic_event
|
|
};
|
|
WaitForMultipleObjects(countof(handles), handles, 0, INFINITE);
|
|
}
|
|
|
|
/* Signal shutdown */
|
|
if (!Atomic32Fetch(&g->panicking))
|
|
{
|
|
Atomic32FetchSet(&g->shutdown, 1);
|
|
for (P_Pool pool_kind = 0; pool_kind < (i32)countof(g->job_pools); ++pool_kind)
|
|
{
|
|
P_W32_JobPool *pool = &g->job_pools[pool_kind];
|
|
P_W32_LockTicketMutex(&pool->workers_wake_lock);
|
|
{
|
|
Atomic32FetchSet(&pool->workers_shutdown.v, 1);
|
|
Atomic64FetchSet(&pool->num_jobs_in_queue.v, -100000);
|
|
WakeByAddressAll(&pool->num_jobs_in_queue);
|
|
}
|
|
P_W32_UnlockTicketMutex(&pool->workers_wake_lock);
|
|
}
|
|
}
|
|
|
|
/* Wait on worker threads */
|
|
if (!Atomic32Fetch(&g->panicking))
|
|
{
|
|
for (P_Pool pool_kind = 0; pool_kind < (i32)countof(g->job_pools); ++pool_kind)
|
|
{
|
|
P_W32_JobPool *pool = &g->job_pools[pool_kind];
|
|
for (i32 i = 0; i < pool->num_worker_threads; ++i)
|
|
{
|
|
P_W32_Thread *worker_thread = pool->worker_threads[i];
|
|
P_W32_WaitReleaseThread(worker_thread);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Wait on scheduler thread */
|
|
if (!Atomic32Fetch(&g->panicking))
|
|
{
|
|
P_W32_WaitReleaseThread(scheduler_thread);
|
|
}
|
|
|
|
/* Find any dangling threads that haven't exited gracefully by now */
|
|
if (!Atomic32Fetch(&g->panicking))
|
|
{
|
|
P_Lock lock = P_LockS(&g->threads_mutex);
|
|
if (g->first_thread)
|
|
{
|
|
TempArena scratch = BeginScratchNoConflict();
|
|
u64 num_dangling_threads = 0;
|
|
String threads_msg = ZI;
|
|
threads_msg.text = PushDry(scratch.arena, u8);
|
|
for (P_W32_Thread *t = g->first_thread; t; t = t->next)
|
|
{
|
|
String name = StringFromCstr(t->thread_name_cstr, countof(t->thread_name_cstr));
|
|
threads_msg.len += StringFormat(scratch.arena, Lit(" \"%F\"\n"), FmtString(name)).len;
|
|
++num_dangling_threads;
|
|
}
|
|
threads_msg = StringFormat(scratch.arena, Lit("%F dangling thread(s):\n%F"), FmtUint(num_dangling_threads), FmtString(threads_msg));
|
|
P_Panic(threads_msg);
|
|
EndScratch(scratch);
|
|
}
|
|
P_Unlock(&lock);
|
|
}
|
|
|
|
/* Exit */
|
|
i32 exit_code = 0;
|
|
if (Atomic32Fetch(&g->panicking))
|
|
{
|
|
WaitForSingleObject(g->panic_event, INFINITE);
|
|
MessageBoxExW(0, g->panic_wstr, L"Fatal error", MB_ICONSTOP | MB_SETFOREGROUND | MB_TOPMOST, 0);
|
|
exit_code = 1;
|
|
}
|
|
return exit_code;
|
|
}
|
|
|
|
//- CRT stub
|
|
|
|
#if !CrtlibIsEnabled
|
|
|
|
#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 /* !CrtlibIsEnabled */
|