fibers wip
This commit is contained in:
parent
ca94dbec3e
commit
514c2a6496
1
build.c
1
build.c
@ -843,7 +843,6 @@ void OnBuild(StringList cli_args)
|
||||
StringBeginsWith(name, Lit("ttf_"))) {
|
||||
if (PlatformWindows) {
|
||||
ignore = !(StringEqual(name, Lit("sys_win32.c")) ||
|
||||
StringEqual(name, Lit("sys_win32-old.c")) ||
|
||||
StringEqual(name, Lit("sock_win32.c")) ||
|
||||
StringEqual(name, Lit("gp_dx12.c")) ||
|
||||
StringEqual(name, Lit("playback_wasapi.c")) ||
|
||||
|
||||
63
profile.bat
63
profile.bat
@ -1,63 +0,0 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: Unpack arguments
|
||||
for %%a in (%*) do set "%%a=1"
|
||||
|
||||
set app_path=%1
|
||||
|
||||
if NOT "%escalate%" == "1" goto skipEscalate
|
||||
:: This enables tracy sampling by running the executable in administrator mode.
|
||||
:: BatchGotAdmin
|
||||
:: https://stackoverflow.com/a/10052222
|
||||
:-------------------------------------
|
||||
:: --> Check for permissions
|
||||
if "%PROCESSOR_ARCHITECTURE%" EQU "amd64" (
|
||||
>nul 2>&1 "%SYSTEMROOT%\SysWOW64\cacls.exe" "%SYSTEMROOT%\SysWOW64\config\system"
|
||||
) else (
|
||||
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"
|
||||
)
|
||||
|
||||
:: --> If error flag set, we do not have admin.
|
||||
if '%errorlevel%' NEQ '0' (
|
||||
echo Requesting administrative privileges...
|
||||
goto UACPrompt
|
||||
) else ( goto gotAdmin )
|
||||
|
||||
:UACPrompt
|
||||
echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
|
||||
set params= %*
|
||||
echo UAC.ShellExecute "cmd.exe", "/c ""%~s0"" %params:"=""%", "", "runas", 1 >> "%temp%\getadmin.vbs"
|
||||
|
||||
"%temp%\getadmin.vbs"
|
||||
del "%temp%\getadmin.vbs"
|
||||
exit /B
|
||||
|
||||
:gotAdmin
|
||||
pushd "%CD%"
|
||||
CD /D "%~dp0"
|
||||
:--------------------------------------
|
||||
:skipEscalate
|
||||
|
||||
:: `ping` is being used in place of `TIMEOUT`
|
||||
:: https://www.ibm.com/support/pages/timeout-command-run-batch-job-exits-immediately-and-returns-error-input-redirection-not-supported-exiting-process-immediately
|
||||
|
||||
taskkill /im tracy-profiler.exe /f 2> nul
|
||||
|
||||
start tracy-capture.exe -o .tracy -f
|
||||
|
||||
echo Launching app "%app_path%"...
|
||||
%app_path%
|
||||
|
||||
if NOT %errorlevel% == 0 (
|
||||
echo.
|
||||
echo Program failed
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: Give time for trace file to finish before opening tracy
|
||||
ping -n 2 127.0.0.1 >NUL
|
||||
|
||||
echo Launching tracy...
|
||||
start "" "tracy-profiler.exe" ".tracy"
|
||||
@ -247,17 +247,16 @@ void sys_app_entry(struct string args_str)
|
||||
|
||||
struct string *worker_names = arena_push_array(scratch.arena, struct string, worker_count);
|
||||
for (i32 i = 0; i < worker_count; ++i) {
|
||||
struct string prefix = string_from_int(scratch.arena, worker_count - i, 10, 2); /* For profiler sorting order */
|
||||
struct string id = string_from_int(scratch.arena, i, 10, 2);
|
||||
struct string *name = &worker_names[i];
|
||||
if (i == APP_DEDICATED_WORKER_ID_USER) {
|
||||
*name = string_format(scratch.arena, LIT("[W%F] Worker #%F (User)"), FMT_STR(prefix), FMT_STR(id));
|
||||
*name = string_format(scratch.arena, LIT("Worker #%F (User)"), FMT_STR(id));
|
||||
} else if (i == APP_DEDICATED_WORKER_ID_SIM) {
|
||||
*name = string_format(scratch.arena, LIT("[W%F] Worker #%F (Sim)"), FMT_STR(prefix), FMT_STR(id));
|
||||
*name = string_format(scratch.arena, LIT("Worker #%F (Sim)"), FMT_STR(id));
|
||||
} else if (i == APP_DEDICATED_WORKER_ID_AUDIO) {
|
||||
*name = string_format(scratch.arena, LIT("[W%F] Worker #%F (Audio)"), FMT_STR(prefix), FMT_STR(id));
|
||||
*name = string_format(scratch.arena, LIT("Worker #%F (Audio)"), FMT_STR(id));
|
||||
} else {
|
||||
*name = string_format(scratch.arena, LIT("[W%F] Worker #%F"), FMT_STR(prefix), FMT_STR(id));
|
||||
*name = string_format(scratch.arena, LIT("Worker #%F"), FMT_STR(id));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -656,6 +656,15 @@ INLINE f64 clamp_f64(f64 v, f64 min, f64 max) { return v < min ? min : v > max ?
|
||||
|
||||
#include "prof_tracy.h"
|
||||
|
||||
#define PROF_THREAD_GROUP_RUNNERS -8000
|
||||
#define PROF_THREAD_GROUP_FIBERS -7000
|
||||
#define PROF_THREAD_GROUP_WORKERS -6000
|
||||
#define PROF_THREAD_GROUP_IO -5
|
||||
#define PROF_THREAD_GROUP_WINDOW -4
|
||||
#define PROF_THREAD_GROUP_EVICTORS -3
|
||||
#define PROF_THREAD_GROUP_APP -2
|
||||
#define PROF_THREAD_GROUP_MAIN -1
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
16
src/config.h
16
src/config.h
@ -76,22 +76,6 @@
|
||||
/* If enabled, things like network writes & memory allocations will be tracked in a global statistics struct */
|
||||
#define GSTAT_ENABLED 1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#define FIBERS_TEST 1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* ========================== *
|
||||
* Settings
|
||||
* ========================== */
|
||||
|
||||
@ -390,7 +390,7 @@ struct gp_startup_receipt gp_startup(void)
|
||||
|
||||
/* Start evictor thread */
|
||||
G.evictor_thread_wake_event = CreateEvent(NULL, false, false, NULL);
|
||||
G.evictor_thread = sys_thread_alloc(evictor_thread_entry_point, NULL, LIT("[P2] GPU evictor thread"));
|
||||
G.evictor_thread = sys_thread_alloc(evictor_thread_entry_point, NULL, LIT("GPU resource evictor thread"), PROF_THREAD_GROUP_EVICTORS);
|
||||
|
||||
struct gp_startup_receipt res = ZI;
|
||||
return res;
|
||||
|
||||
@ -199,7 +199,7 @@ struct host *host_alloc(u16 listen_port)
|
||||
host->sock = sock_alloc(listen_port, MEGABYTE(2), MEGABYTE(2));
|
||||
|
||||
host->rcv_buffer_write_mutex = sys_mutex_alloc();
|
||||
host->receiver_thread = sys_thread_alloc(&host_receiver_thread_entry_point, host, LIT("[P5] Host receiver"));
|
||||
host->receiver_thread = sys_thread_alloc(&host_receiver_thread_entry_point, host, LIT("Host receiver"), PROF_THREAD_GROUP_IO);
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ void job_startup(i32 num_workers, struct string *worker_names)
|
||||
G.num_worker_threads = num_workers;
|
||||
for (i32 i = 0; i < G.num_worker_threads; ++i) {
|
||||
struct string name = worker_names[i];
|
||||
G.worker_threads[i] = sys_thread_alloc(worker_thread_entry_point, (void *)(i64)i, name);
|
||||
G.worker_threads[i] = sys_thread_alloc(worker_thread_entry_point, (void *)(i64)i, name, PROF_THREAD_GROUP_WORKERS + i);
|
||||
}
|
||||
atomic_i32_fetch_set(&G.num_idle_worker_threads, num_workers);
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ INLINE void __prof_zone_cleanup_func(TracyCZoneCtx *ctx) { TracyCZoneEnd(*ctx) }
|
||||
#define __proffree(ptr) TracyCFree((ptr))
|
||||
#define __profmsg(txt, len, col) TracyCMessageC((txt), (len), BGR32(col))
|
||||
#define __profframe(name) TracyCFrameMarkNamed((name))
|
||||
#define __profthread(name) TracyCSetThreadName((name))
|
||||
#define __profthread(name, group_hint) TracyCSetThreadNameWithHint((name), (group_hint))
|
||||
|
||||
enum __prof_plot_type {
|
||||
__prof_plot_type_number = TracyPlotFormatNumber,
|
||||
@ -64,7 +64,7 @@ enum __prof_plot_type {
|
||||
#define __proffree(ptr)
|
||||
#define __profmsg(txt, len, col)
|
||||
#define __profframe(name)
|
||||
#define __profthread(name)
|
||||
#define __profthread(name, group_hint)
|
||||
#define __prof_plot_init(name, type, step, fill, color)
|
||||
#define __prof_plot(name, val)
|
||||
#define __prof_plot_i(name, val)
|
||||
@ -136,10 +136,10 @@ INLINE void __prof_dx12_zone_cleanup_func(TracyCD3D12ZoneCtx *ctx) { ___tracy_d3
|
||||
#endif /* PROFILING_CAPTURE_FRAME_IMAGE */
|
||||
|
||||
#ifdef TRACY_FIBERS
|
||||
# define __prof_fiber_enter(fiber_name) ___tracy_fiber_enter(fiber_name)
|
||||
# define __prof_fiber_leave ___tracy_fiber_leave()
|
||||
# define __prof_fiber_enter(fiber_name, profiler_group) TracyCFiberEnterWithHint(fiber_name, profiler_group)
|
||||
# define __prof_fiber_leave TracyCFiberLeave
|
||||
#else
|
||||
# define __prof_fiber_enter(fiber_name)
|
||||
# define __prof_fiber_enter(fiber_name, profiler_group)
|
||||
# define __prof_fiber_leave
|
||||
#endif
|
||||
|
||||
|
||||
@ -77,8 +77,8 @@ struct resource_startup_receipt resource_startup(void)
|
||||
G.watch_dispatcher_cv = sys_condition_variable_alloc();
|
||||
|
||||
app_register_exit_callback(&resource_shutdown);
|
||||
G.resource_watch_monitor_thread = sys_thread_alloc(resource_watch_monitor_thread_entry_point, NULL, LIT("[P2] Resource watch monitor"));
|
||||
G.resource_watch_dispatch_thread = sys_thread_alloc(resource_watch_dispatcher_thread_entry_point, NULL, LIT("[P2] Resource watch dispatcher"));
|
||||
G.resource_watch_monitor_thread = sys_thread_alloc(resource_watch_monitor_thread_entry_point, NULL, LIT("Resource watch monitor"), PROF_THREAD_GROUP_IO);
|
||||
G.resource_watch_dispatch_thread = sys_thread_alloc(resource_watch_dispatcher_thread_entry_point, NULL, LIT("Resource watch dispatcher"), PROF_THREAD_GROUP_IO);
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
@ -257,7 +257,7 @@ struct sprite_startup_receipt sprite_startup(struct gp_startup_receipt *gp_sr,
|
||||
G.evictor_scheduler_mutex = sys_mutex_alloc();
|
||||
G.evictor_scheduler_shutdown_cv = sys_condition_variable_alloc();
|
||||
|
||||
G.evictor_scheduler_thread = sys_thread_alloc(sprite_evictor_scheduler_thread_entry_point, NULL, LIT("[P2] Sprite evictor scheduler"));
|
||||
G.evictor_scheduler_thread = sys_thread_alloc(sprite_evictor_scheduler_thread_entry_point, NULL, LIT("Sprite evictor scheduler"), PROF_THREAD_GROUP_EVICTORS);
|
||||
|
||||
app_register_exit_callback(&sprite_shutdown);
|
||||
resource_register_watch_callback(&sprite_resource_watch_callback);
|
||||
|
||||
42
src/sys.h
42
src/sys.h
@ -396,8 +396,6 @@ void sys_condition_variable_broadcast(struct sys_condition_variable *sys_cv);
|
||||
* Threads
|
||||
* ========================== */
|
||||
|
||||
#define SYS_THREAD_STACK_SIZE MEGABYTE(4)
|
||||
|
||||
#define SYS_THREAD_DEF(name, arg_name) void name(void *arg_name)
|
||||
typedef SYS_THREAD_DEF(sys_thread_func, data);
|
||||
|
||||
@ -405,7 +403,8 @@ typedef SYS_THREAD_DEF(sys_thread_func, data);
|
||||
struct sys_thread *sys_thread_alloc(
|
||||
sys_thread_func *entry_point,
|
||||
void *thread_data, /* Passed as arg to `entry_point` */
|
||||
struct string thread_name
|
||||
struct string thread_name,
|
||||
i32 profiler_group
|
||||
);
|
||||
|
||||
void sys_thread_wait_release(struct sys_thread *thread);
|
||||
@ -489,6 +488,30 @@ b32 sys_run_command(struct string cmd);
|
||||
|
||||
i32 sys_current_fiber_id(void);
|
||||
|
||||
void sys_yield(void);
|
||||
|
||||
/* ========================== *
|
||||
* Job
|
||||
* ========================== */
|
||||
|
||||
enum sys_job_priority {
|
||||
SYS_JOB_PRIORITY_HIGH = 0,
|
||||
SYS_JOB_PRIORITY_NORMAL = 1,
|
||||
SYS_JOB_PRIORITY_BACKGROUND = 2,
|
||||
|
||||
NUM_SYS_JOB_PRIORITIES
|
||||
};
|
||||
|
||||
struct sys_job_data {
|
||||
i32 id;
|
||||
void *sig;
|
||||
};
|
||||
|
||||
#define SYS_JOB_DEF(job_name, arg_name) void job_name(struct sys_job_data arg_name)
|
||||
typedef SYS_JOB_DEF(sys_job_func, job_data);
|
||||
|
||||
void sys_run(i32 count, sys_job_func *func, enum sys_job_priority priority);
|
||||
|
||||
/* ========================== *
|
||||
* Scratch context
|
||||
* ========================== */
|
||||
@ -501,19 +524,6 @@ struct sys_scratch_ctx {
|
||||
|
||||
struct sys_scratch_ctx *sys_scratch_ctx_from_fiber_id(i32 fiber_id);
|
||||
|
||||
/* ========================== *
|
||||
* Job
|
||||
* ========================== */
|
||||
|
||||
struct sys_job_data {
|
||||
i32 id;
|
||||
void *sig;
|
||||
};
|
||||
|
||||
#define SYS_JOB_DEF(job_name, arg_name) void job_name(struct sys_job_data arg_name)
|
||||
typedef SYS_JOB_DEF(sys_job_func, job_data);
|
||||
|
||||
|
||||
/* ========================== *
|
||||
* App entry point
|
||||
* ========================== */
|
||||
|
||||
2902
src/sys_win32-old.c
2902
src/sys_win32-old.c
File diff suppressed because it is too large
Load Diff
478
src/sys_win32.c
478
src/sys_win32.c
@ -1,5 +1,3 @@
|
||||
#if FIBERS_TEST
|
||||
|
||||
#include "sys.h"
|
||||
#include "memory.h"
|
||||
#include "arena.h"
|
||||
@ -32,6 +30,9 @@
|
||||
#define SYS_WINDOW_EVENT_LISTENERS_MAX 512
|
||||
#define WINDOW_CLASS_NAME L"power_play_window_class"
|
||||
|
||||
#define THREAD_STACK_SIZE MEGABYTE(4)
|
||||
#define FIBER_STACK_SIZE MEGABYTE(4)
|
||||
|
||||
struct win32_mutex {
|
||||
SRWLOCK srwlock;
|
||||
struct win32_mutex *next_free;
|
||||
@ -59,6 +60,7 @@ struct win32_thread {
|
||||
void *thread_data;
|
||||
char thread_name_cstr[256];
|
||||
wchar_t thread_name_wstr[256];
|
||||
i32 profiler_group;
|
||||
|
||||
struct win32_thread *next;
|
||||
struct win32_thread *prev;
|
||||
@ -112,18 +114,84 @@ struct win32_window {
|
||||
|
||||
|
||||
|
||||
#define FIBER_NAME_PREFIX_CSTR "[F] Fiber "
|
||||
#define FIBER_NAME_MAX_SIZE 64
|
||||
|
||||
enum yield_kind {
|
||||
YIELD_KIND_NONE,
|
||||
YIELD_KIND_DONE,
|
||||
YIELD_KIND_COOPERATIVE,
|
||||
YIELD_KIND_SLEEP,
|
||||
|
||||
NUM_YIELD_KINDS
|
||||
};
|
||||
|
||||
struct alignas(64) fiber {
|
||||
char *name_cstr; /* 8 bytes */
|
||||
/* ========================================================================= */
|
||||
i32 id; /* 4 bytes */
|
||||
i32 parent_id; /* 4 bytes */
|
||||
/* ========================================================================= */
|
||||
void *addr; /* 8 bytes */
|
||||
/* ========================================================================= */
|
||||
sys_job_func *job_func; /* 8 bytes */
|
||||
/* ========================================================================= */
|
||||
void *job_sig; /* 8 bytes */
|
||||
/* ========================================================================= */
|
||||
i32 job_id; /* 4 bytes */
|
||||
u8 pad0[4]; /* 4 bytes (padding) */
|
||||
/* ========================================================================= */
|
||||
enum yield_kind yield_kind; /* 4 bytes */
|
||||
u8 pad1[4]; /* 4 bytes (padding) */
|
||||
/* ========================================================================= */
|
||||
u8 pad_end[8]; /* 8 bytes (padding) */
|
||||
};
|
||||
STATIC_ASSERT(sizeof(struct fiber) == 64); /* Assume fiber fits in one cache line (increase if necessary) */
|
||||
STATIC_ASSERT(alignof(struct fiber) == 64); /* Avoid false sharing */
|
||||
|
||||
#define FIBER_CTX_SLEEP_TIMER_INIT_MAGIC ((HANDLE)0x48e87857650169c8)
|
||||
struct alignas(64) fiber_ctx {
|
||||
HANDLE sleep_timer; /* 8 bytes */
|
||||
/* ========================================================================= */
|
||||
struct sys_scratch_ctx scratch_ctx; /* 16 bytes */
|
||||
u8 pad[40]; /* 40 bytes */
|
||||
/* ========================================================================= */
|
||||
u8 pad[40]; /* 40 bytes (padding) */
|
||||
};
|
||||
STATIC_ASSERT(sizeof(struct fiber_ctx) == 64); /* Assume ctx fits in one cache line (increase if necessary) */
|
||||
STATIC_ASSERT(alignof(struct fiber_ctx) == 64); /* Avoid false sharing */
|
||||
|
||||
|
||||
struct alignas(64) runner_ctx {
|
||||
i32 id;
|
||||
HANDLE sleep_timer;
|
||||
};
|
||||
|
||||
struct job_info {
|
||||
i32 num_dispatched;
|
||||
|
||||
i32 count;
|
||||
sys_job_func *func;
|
||||
void *sig;
|
||||
|
||||
struct job_info *next;
|
||||
};
|
||||
|
||||
struct alignas(64) job_queue {
|
||||
struct atomic_i32 lock;
|
||||
struct arena *arena;
|
||||
|
||||
struct job_info *first;
|
||||
struct job_info *last;
|
||||
|
||||
struct job_info *first_free;
|
||||
};
|
||||
|
||||
enum job_queue_kind {
|
||||
JOB_QUEUE_KIND_HIGH_PRIORITY,
|
||||
JOB_QUEUE_KIND_NORMAL_PRIORITY,
|
||||
JOB_QUEUE_KIND_BACKGROUND,
|
||||
|
||||
NUM_JOB_QUEUE_KINDS
|
||||
};
|
||||
|
||||
|
||||
|
||||
/* ========================== *
|
||||
@ -180,30 +248,123 @@ GLOBAL struct {
|
||||
|
||||
|
||||
/* Fibers */
|
||||
struct atomic_i32 num_fibers;
|
||||
i32 num_fibers;
|
||||
i32 first_free_fiber_id;
|
||||
struct arena *fiber_names_arena;
|
||||
alignas(64) struct atomic_i32 fibers_lock;
|
||||
struct fiber fibers[SYS_MAX_FIBERS];
|
||||
struct fiber_ctx fiber_contexts[SYS_MAX_FIBERS];
|
||||
|
||||
/* Jobs */
|
||||
struct job_queue job_queues[NUM_JOB_QUEUE_KINDS];
|
||||
|
||||
/* Runners */
|
||||
struct atomic_i32 runners_shutdown;
|
||||
i32 num_runner_threads;
|
||||
struct arena *runner_threads_arena;
|
||||
struct sys_thread **runner_threads;
|
||||
struct runner_ctx *runner_contexts;
|
||||
|
||||
struct atomic_i32 runner_wake_gen;
|
||||
|
||||
} G = ZI, DEBUG_ALIAS(G, G_sys_win32);
|
||||
|
||||
|
||||
|
||||
|
||||
/* ========================== *
|
||||
* Fibers
|
||||
* ========================== */
|
||||
|
||||
INTERNAL i32 fiber_ctx_init(void)
|
||||
enum fiber_kind {
|
||||
FIBER_KIND_CONVERTED_THREAD,
|
||||
FIBER_KIND_JOB_RUNNER
|
||||
};
|
||||
|
||||
INTERNAL void job_fiber_entry(void *id_ptr);
|
||||
|
||||
INTERNAL struct fiber *fiber_alloc(enum fiber_kind kind)
|
||||
{
|
||||
i32 id = atomic_i32_fetch_add(&G.num_fibers, 1);
|
||||
if (id >= SYS_MAX_FIBERS) {
|
||||
i32 fiber_id = 0;
|
||||
struct fiber *fiber = NULL;
|
||||
char *new_name_cstr = NULL;
|
||||
{
|
||||
while (atomic_i32_fetch_test_set(&G.fibers_lock, 0, 1) != 0) ix_pause();
|
||||
{
|
||||
fiber_id = G.first_free_fiber_id;
|
||||
if (fiber_id && kind == FIBER_KIND_JOB_RUNNER) {
|
||||
fiber = &G.fibers[fiber_id];
|
||||
G.first_free_fiber_id = fiber->parent_id;
|
||||
} else {
|
||||
fiber_id = G.num_fibers++;
|
||||
if (fiber_id >= SYS_MAX_FIBERS) {
|
||||
sys_panic(LIT("Max fibers reached"));
|
||||
}
|
||||
struct fiber_ctx *ctx = &G.fiber_contexts[id];
|
||||
{
|
||||
ctx->sleep_timer = FIBER_CTX_SLEEP_TIMER_INIT_MAGIC;
|
||||
fiber = &G.fibers[fiber_id];
|
||||
new_name_cstr = arena_push_array(G.fiber_names_arena, char, FIBER_NAME_MAX_SIZE);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
atomic_i32_fetch_set(&G.fibers_lock, 0);
|
||||
}
|
||||
if (new_name_cstr != NULL) {
|
||||
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 a = id_chars[rev_start];
|
||||
char b = id_chars[rev_end];
|
||||
id_chars[rev_start] = b;
|
||||
id_chars[rev_end] = a;
|
||||
++rev_start;
|
||||
--rev_end;
|
||||
}
|
||||
|
||||
/* Concat fiber name */
|
||||
i32 name_size = 1;
|
||||
STATIC_ASSERT(sizeof(sizeof(FIBER_NAME_PREFIX_CSTR)) <= FIBER_NAME_MAX_SIZE);
|
||||
MEMCPY(new_name_cstr, FIBER_NAME_PREFIX_CSTR, sizeof(FIBER_NAME_PREFIX_CSTR));
|
||||
name_size += sizeof(FIBER_NAME_PREFIX_CSTR) - 2;
|
||||
MEMCPY(new_name_cstr + name_size, id_chars, id_chars_len);
|
||||
fiber->name_cstr = new_name_cstr;
|
||||
|
||||
/* Init win32 fiber */
|
||||
if (kind == FIBER_KIND_JOB_RUNNER) {
|
||||
fiber->addr = CreateFiber(FIBER_STACK_SIZE, job_fiber_entry, (void *)(i64)fiber_id);
|
||||
} else {
|
||||
fiber->addr = ConvertThreadToFiber((void *)(i64)fiber_id);
|
||||
}
|
||||
}
|
||||
fiber->job_func = 0;
|
||||
fiber->job_sig = 0;
|
||||
fiber->job_id = 0;
|
||||
fiber->yield_kind = 0;
|
||||
fiber->parent_id = 0;
|
||||
return fiber;
|
||||
}
|
||||
|
||||
INTERNAL void fiber_release(struct fiber *fiber, i32 fiber_id)
|
||||
{
|
||||
while (atomic_i32_fetch_test_set(&G.fibers_lock, 0, 1) != 0) ix_pause();
|
||||
{
|
||||
fiber->parent_id = G.first_free_fiber_id;
|
||||
G.first_free_fiber_id = fiber_id;
|
||||
}
|
||||
atomic_i32_fetch_set(&G.fibers_lock, 0);
|
||||
}
|
||||
|
||||
INTERNAL struct fiber *fiber_from_id(i32 id)
|
||||
{
|
||||
ASSERT(id >= 0 && id < SYS_MAX_FIBERS);
|
||||
return &G.fibers[id];
|
||||
}
|
||||
|
||||
INTERNAL struct fiber_ctx *fiber_ctx_from_id(i32 id)
|
||||
@ -212,11 +373,248 @@ INTERNAL struct fiber_ctx *fiber_ctx_from_id(i32 id)
|
||||
return &G.fiber_contexts[id];
|
||||
}
|
||||
|
||||
/* ========================== *
|
||||
* Test job
|
||||
* ========================== */
|
||||
|
||||
i32 sys_current_fiber_id(void)
|
||||
{
|
||||
return (i32)(i64)GetFiberData();
|
||||
}
|
||||
|
||||
INTERNAL void yield(enum yield_kind kind)
|
||||
{
|
||||
struct fiber *fiber = fiber_from_id(sys_current_fiber_id());
|
||||
i32 parent_fiber_id = fiber->parent_id;
|
||||
if (parent_fiber_id <= 0) {
|
||||
sys_panic(LIT("A top level fiber tried to yield"));
|
||||
}
|
||||
struct fiber *parent_fiber = fiber_from_id(parent_fiber_id);
|
||||
{
|
||||
__prof_fiber_leave;
|
||||
fiber->yield_kind = kind;
|
||||
SwitchToFiber(parent_fiber->addr);
|
||||
__prof_fiber_enter(fiber->name_cstr, PROF_THREAD_GROUP_FIBERS + fiber->id);
|
||||
}
|
||||
}
|
||||
|
||||
void sys_yield(void)
|
||||
{
|
||||
yield(YIELD_KIND_COOPERATIVE);
|
||||
}
|
||||
|
||||
void sys_run(i32 count, sys_job_func *func, enum sys_job_priority priority)
|
||||
{
|
||||
//priority = min_i32(priority, current_fiber_job_priority); /* Jobs can't create higher priority jobs */
|
||||
if (count >= 1 && func && priority >= 0 && priority < NUM_SYS_JOB_PRIORITIES) {
|
||||
STATIC_ASSERT((i32)NUM_SYS_JOB_PRIORITIES == (i32)NUM_JOB_QUEUE_KINDS); /* Enums must have 1:1 mapping */
|
||||
enum job_queue_kind queue_kind = (enum job_queue_kind)priority;
|
||||
struct job_queue *queue = &G.job_queues[queue_kind];
|
||||
while (atomic_i32_fetch_test_set(&queue->lock, 0, 1) != 0) ix_pause();
|
||||
{
|
||||
struct job_info *info = NULL;
|
||||
if (queue->first_free) {
|
||||
info = queue->first_free;
|
||||
queue->first_free = info->next;
|
||||
} else {
|
||||
info = arena_push(queue->arena, struct job_info);
|
||||
}
|
||||
info->count = count;
|
||||
info->func = func;
|
||||
if (queue->last) {
|
||||
queue->last->next = info;
|
||||
} else {
|
||||
queue->first = info;
|
||||
}
|
||||
queue->last = info;
|
||||
}
|
||||
atomic_i32_fetch_set(&queue->lock, 0);
|
||||
} else {
|
||||
/* Invalid job parameters */
|
||||
sys_panic(LIT("Invalid job parameters"));
|
||||
}
|
||||
}
|
||||
|
||||
struct bla_job_sig {
|
||||
i32 _;
|
||||
};
|
||||
|
||||
INTERNAL SYS_JOB_DEF(bla_job, job)
|
||||
{
|
||||
__prof;
|
||||
(UNUSED)job;
|
||||
Sleep(20);
|
||||
{
|
||||
__profscope(Do tha yield);
|
||||
yield(YIELD_KIND_SLEEP);
|
||||
}
|
||||
Sleep(20);
|
||||
}
|
||||
|
||||
|
||||
/* ========================== *
|
||||
* Job fiber func
|
||||
* ========================== */
|
||||
|
||||
INTERNAL void job_fiber_entry(void *id_ptr)
|
||||
{
|
||||
i32 id = (i32)(i64)id_ptr;
|
||||
struct fiber *fiber = fiber_from_id(id);
|
||||
while (true) {
|
||||
__prof_fiber_enter(fiber->name_cstr, PROF_THREAD_GROUP_FIBERS + fiber->id);
|
||||
i32 parent_id = fiber->parent_id;
|
||||
struct fiber *parent_fiber = fiber_from_id(parent_id);
|
||||
void *parent_fiber_addr = parent_fiber->addr;
|
||||
{
|
||||
/* Run job */
|
||||
fiber->yield_kind = YIELD_KIND_NONE;
|
||||
struct sys_job_data data = ZI;
|
||||
data.id = fiber->job_id;
|
||||
data.sig = fiber->job_sig;
|
||||
fiber->job_func(data);
|
||||
fiber->yield_kind = YIELD_KIND_DONE;
|
||||
}
|
||||
__prof_fiber_leave;
|
||||
SwitchToFiber(parent_fiber_addr);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================== *
|
||||
* Test runners
|
||||
* ========================== */
|
||||
|
||||
INTERNAL SYS_THREAD_DEF(runner_entry, runner_ctx_arg)
|
||||
{
|
||||
struct runner_ctx *ctx = runner_ctx_arg;
|
||||
(UNUSED)ctx;
|
||||
|
||||
i32 runner_fiber_id = sys_current_fiber_id();
|
||||
|
||||
struct job_queue *queues[countof(G.job_queues)] = ZI;
|
||||
for (u32 i = 0; i < countof(G.job_queues); ++i) {
|
||||
queues[i] = &G.job_queues[i];
|
||||
}
|
||||
|
||||
while (!atomic_i32_fetch(&G.runners_shutdown)) {
|
||||
/* Pull job from queue */
|
||||
i32 job_id = 0;
|
||||
sys_job_func *job_func = 0;
|
||||
void *job_sig = 0;
|
||||
for (u32 queue_index = 0; queue_index < countof(queues) && !job_func; ++queue_index) {
|
||||
struct job_queue *queue = queues[queue_index];
|
||||
if (queue) {
|
||||
while (atomic_i32_fetch_test_set(&queue->lock, 0, 1) != 0) ix_pause();
|
||||
struct job_info *info = queue->first;
|
||||
while (info && !job_func) {
|
||||
struct job_info *next = info->next;
|
||||
job_id = info->num_dispatched++;
|
||||
if (job_id < info->count) {
|
||||
/* Pick job */
|
||||
job_func = info->func;
|
||||
job_sig = info->sig;
|
||||
if (job_id == (info->count - 1)) {
|
||||
/* We're picking up the last dispatch, so dequeue the job */
|
||||
if (!next) {
|
||||
queue->last = NULL;
|
||||
}
|
||||
queue->first = next;
|
||||
info->next = queue->first_free;
|
||||
queue->first_free = info;
|
||||
}
|
||||
}
|
||||
info = next;
|
||||
}
|
||||
atomic_i32_fetch_set(&queue->lock, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Run job */
|
||||
if (job_func) {
|
||||
__profscope(Run job);
|
||||
struct fiber *fiber = fiber_alloc(FIBER_KIND_JOB_RUNNER);
|
||||
fiber->job_func = job_func;
|
||||
fiber->job_sig = job_sig;
|
||||
fiber->job_id = job_id;
|
||||
fiber->parent_id = runner_fiber_id;
|
||||
b32 done = false;
|
||||
while (!done) {
|
||||
SwitchToFiber(fiber->addr);
|
||||
enum yield_kind yield_kind = fiber->yield_kind;
|
||||
switch (yield_kind) {
|
||||
default:
|
||||
{
|
||||
/* Invalid yield kind */
|
||||
sys_panic(LIT("Fiber yielded with unknown yield kind"));
|
||||
} break;
|
||||
|
||||
case YIELD_KIND_SLEEP:
|
||||
{
|
||||
__profscope(Job sleep);
|
||||
Sleep(100);
|
||||
} break;
|
||||
|
||||
case YIELD_KIND_DONE:
|
||||
{
|
||||
fiber_release(fiber, fiber->id);
|
||||
done = true;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
INTERNAL SYS_THREAD_DEF(test_entry, _)
|
||||
{
|
||||
struct arena_temp scratch = scratch_begin_no_conflict();
|
||||
(UNUSED)_;
|
||||
|
||||
/* Init job queues */
|
||||
for (u32 i = 0; i < countof(G.job_queues); ++i) {
|
||||
struct job_queue *queue = &G.job_queues[i];
|
||||
queue->arena = arena_alloc(GIGABYTE(64));
|
||||
}
|
||||
|
||||
/* Start runners */
|
||||
G.num_runner_threads = 6;
|
||||
G.runner_threads_arena = arena_alloc(GIGABYTE(64));
|
||||
G.runner_threads = arena_push_array(G.runner_threads_arena, struct sys_thread *, G.num_runner_threads);
|
||||
G.runner_contexts = arena_push_array(G.runner_threads_arena, struct runner_ctx, G.num_runner_threads);
|
||||
for (i32 i = 0; i < G.num_runner_threads; ++i) {
|
||||
struct runner_ctx *ctx = &G.runner_contexts[i];
|
||||
ctx->id = i;
|
||||
ctx->sleep_timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
||||
struct string id_str = string_from_int(scratch.arena, i, 10, 2);
|
||||
struct string name = string_format(scratch.arena, LIT("Runner %F"), FMT_STR(id_str));
|
||||
G.runner_threads[i] = sys_thread_alloc(runner_entry, ctx, name, PROF_THREAD_GROUP_RUNNERS + i);
|
||||
}
|
||||
|
||||
/* Push test job */
|
||||
Sleep(300);
|
||||
sys_run(2, bla_job, SYS_JOB_PRIORITY_NORMAL);
|
||||
|
||||
/* Wait on runners */
|
||||
for (i32 i = 0; i < G.num_runner_threads; ++i) {
|
||||
struct sys_thread *runner_thread = G.runner_threads[i];
|
||||
sys_thread_wait_release(runner_thread);
|
||||
}
|
||||
|
||||
scratch_end(scratch);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* ========================== *
|
||||
* Scratch context
|
||||
* ========================== */
|
||||
@ -249,6 +647,7 @@ struct sys_scratch_ctx *sys_scratch_ctx_from_fiber_id(i32 id)
|
||||
|
||||
|
||||
|
||||
|
||||
/* ========================== *
|
||||
* Events
|
||||
* ========================== */
|
||||
@ -1143,7 +1542,7 @@ INTERNAL struct win32_window *win32_window_alloc(void)
|
||||
window->event_callbacks_mutex = sys_mutex_alloc();
|
||||
|
||||
/* Start window thread for processing events */
|
||||
window->event_thread = sys_thread_alloc(&window_thread_entry_point, window, LIT("[P4] Window thread"));
|
||||
window->event_thread = sys_thread_alloc(&window_thread_entry_point, window, LIT("Window thread"), PROF_THREAD_GROUP_WINDOW);
|
||||
|
||||
/* Wait for event thread to create actual window */
|
||||
sync_flag_wait(&window->ready_sf);
|
||||
@ -1983,10 +2382,9 @@ INTERNAL void win32_thread_release(struct win32_thread *t)
|
||||
INTERNAL DWORD WINAPI win32_thread_proc(LPVOID vt)
|
||||
{
|
||||
struct win32_thread *t = (struct win32_thread *)vt;
|
||||
__profthread(t->thread_name_cstr);
|
||||
__profthread(t->thread_name_cstr, t->profiler_group);
|
||||
|
||||
i32 fiber_id = fiber_ctx_init();
|
||||
ConvertThreadToFiber((void *)(i64)fiber_id);
|
||||
fiber_alloc(FIBER_KIND_CONVERTED_THREAD);
|
||||
|
||||
/* Initialize COM */
|
||||
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
|
||||
@ -2007,7 +2405,7 @@ INTERNAL DWORD WINAPI win32_thread_proc(LPVOID vt)
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct sys_thread *sys_thread_alloc(sys_thread_func *entry_point, void *thread_data, struct string thread_name)
|
||||
struct sys_thread *sys_thread_alloc(sys_thread_func *entry_point, void *thread_data, struct string thread_name, i32 profiler_group)
|
||||
{
|
||||
__prof;
|
||||
struct arena_temp scratch = scratch_begin_no_conflict();
|
||||
@ -2019,6 +2417,7 @@ struct sys_thread *sys_thread_alloc(sys_thread_func *entry_point, void *thread_d
|
||||
struct win32_thread *t = win32_thread_alloc();
|
||||
t->entry_point = entry_point;
|
||||
t->thread_data = thread_data;
|
||||
t->profiler_group = profiler_group;
|
||||
|
||||
/* Copy thread name to params */
|
||||
{
|
||||
@ -2035,7 +2434,7 @@ struct sys_thread *sys_thread_alloc(sys_thread_func *entry_point, void *thread_d
|
||||
|
||||
t->handle = CreateThread(
|
||||
NULL,
|
||||
SYS_THREAD_STACK_SIZE,
|
||||
THREAD_STACK_SIZE,
|
||||
win32_thread_proc,
|
||||
t,
|
||||
0,
|
||||
@ -2345,13 +2744,9 @@ INTERNAL void win32_precise_sleep_legacy(f64 seconds)
|
||||
void sys_sleep_precise(f64 seconds)
|
||||
{
|
||||
__prof;
|
||||
struct fiber_ctx *ctx = fiber_ctx_from_id(sys_current_fiber_id());
|
||||
/* FIXME: Enable this */
|
||||
#if 0
|
||||
HANDLE timer = ctx->sleep_timer;
|
||||
if (timer == FIBER_CTX_SLEEP_TIMER_INIT_MAGIC) {
|
||||
__profscope(Create high resolution timer);
|
||||
timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
||||
ctx->sleep_timer = timer;
|
||||
}
|
||||
if (timer) {
|
||||
/* Use newer sleeping method */
|
||||
win32_precise_sleep_timer(timer, seconds);
|
||||
@ -2360,6 +2755,10 @@ void sys_sleep_precise(f64 seconds)
|
||||
* is not available due to older windows version */
|
||||
win32_precise_sleep_legacy(seconds);
|
||||
}
|
||||
#else
|
||||
(UNUSED)win32_precise_sleep_timer;
|
||||
win32_precise_sleep_legacy(seconds);
|
||||
#endif
|
||||
}
|
||||
|
||||
void sys_sleep(f64 seconds)
|
||||
@ -2409,7 +2808,6 @@ int CALLBACK wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev_instance,
|
||||
(UNUSED)show_code;
|
||||
|
||||
#if PROFILING
|
||||
/* Launch profiler */
|
||||
{
|
||||
__profscope(Launch profiler);
|
||||
STARTUPINFO si = ZI;
|
||||
@ -2420,14 +2818,19 @@ int CALLBACK wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev_instance,
|
||||
DeleteFileW(PROFILING_FILE_WSTR);
|
||||
b32 success = CreateProcessW(NULL, cmd, NULL, NULL, FALSE, DETACHED_PROCESS, NULL, NULL, &si, &pi);
|
||||
if (!success) {
|
||||
MessageBoxExW(NULL, L"Failed to launch capture using command '" PROFILING_CMD_WSTR L"'. Is the app in your path?", L"Error", MB_ICONSTOP | MB_SETFOREGROUND | MB_TOPMOST, 0);
|
||||
MessageBoxExW(NULL, L"Failed to launch profiler using command '" PROFILING_CMD_WSTR L"'.", L"Error", MB_ICONSTOP | MB_SETFOREGROUND | MB_TOPMOST, 0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
__profthread("Main thread", PROF_THREAD_GROUP_MAIN);
|
||||
|
||||
/* Init fibers */
|
||||
G.num_fibers = 1; /* Fiber at index 0 always nil */
|
||||
G.fiber_names_arena = arena_alloc(GIGABYTE(64));
|
||||
|
||||
/* Convert main thread to fiber */
|
||||
i32 fiber_id = fiber_ctx_init();
|
||||
ConvertThreadToFiber((void *)(i64)fiber_id);
|
||||
fiber_alloc(FIBER_KIND_CONVERTED_THREAD);
|
||||
|
||||
u64 cmdline_len = wstr_len(cmdline_wstr, countof(G.cmdline_args_wstr) - 1);
|
||||
MEMCPY(G.cmdline_args_wstr, cmdline_wstr, cmdline_len * sizeof(*cmdline_wstr));
|
||||
@ -2535,7 +2938,10 @@ int CALLBACK wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev_instance,
|
||||
/* Call app thread and wait for return */
|
||||
{
|
||||
/* Start app thread */
|
||||
struct sys_thread *app_thread = sys_thread_alloc(&win32_app_thread_entry_point, NULL, LIT("[P0] App thread"));
|
||||
struct sys_thread *app_thread = sys_thread_alloc(&win32_app_thread_entry_point, NULL, LIT("App thread"), PROF_THREAD_GROUP_APP);
|
||||
|
||||
/* Start test thread */
|
||||
struct sys_thread *test_thread = sys_thread_alloc(test_entry, NULL, LIT("Test thread"), PROF_THREAD_GROUP_APP);
|
||||
|
||||
/* Get app thread handle */
|
||||
HANDLE app_thread_handle = 0;
|
||||
@ -2546,6 +2952,7 @@ int CALLBACK wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev_instance,
|
||||
}
|
||||
sys_mutex_unlock(&lock);
|
||||
|
||||
|
||||
/* Wait for either app thread exit or panic */
|
||||
if (app_thread_handle) {
|
||||
HANDLE wait_handles[] = {
|
||||
@ -2559,6 +2966,10 @@ int CALLBACK wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev_instance,
|
||||
ASSERT(res != WAIT_FAILED);
|
||||
(UNUSED)res;
|
||||
}
|
||||
|
||||
/* Shutdown test thread */
|
||||
atomic_i32_fetch_set(&G.runners_shutdown, 1);
|
||||
sys_thread_wait_release(test_thread);
|
||||
}
|
||||
|
||||
/* Find any dangling threads that haven't exited gracefully by now */
|
||||
@ -2629,6 +3040,3 @@ void __stdcall wWinMainCRTStartup(void)
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
#endif /* !CRTLIB */
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
@ -204,8 +204,6 @@ struct ttf_decode_result ttf_decode(struct arena *arena, struct string encoded,
|
||||
|
||||
/* Compute glyph metrics */
|
||||
DWRITE_GLYPH_METRICS glyph_metrics = ZI;
|
||||
|
||||
|
||||
error = font_face->GetDesignGlyphMetrics(&i, 1, &glyph_metrics, false);
|
||||
|
||||
f32 off_x = (f32)bounding_box.left - raster_target_x;
|
||||
|
||||
27
src/user.c
27
src/user.c
@ -1689,29 +1689,32 @@ INTERNAL void user_update(void)
|
||||
if (!G.debug_camera) {
|
||||
__profscope(Draw crosshair);
|
||||
struct v2 crosshair_pos = G.user_cursor;
|
||||
|
||||
struct sprite_tag crosshair_tag = sprite_tag_from_path(LIT("sprite/crosshair.ase"));
|
||||
struct sprite_texture *t = sprite_texture_from_tag_async(sprite_frame_scope, crosshair_tag);
|
||||
|
||||
struct sprite_tag crosshair = sprite_tag_from_path(LIT("sprite/crosshair.ase"));
|
||||
struct sprite_texture *t = sprite_texture_from_tag_async(sprite_frame_scope, crosshair);
|
||||
struct v2 size = V2(t->width, t->height);
|
||||
struct xform xf = XFORM_TRS(.t = crosshair_pos, .s = size);
|
||||
draw_texture(G.ui_gp_flow, DRAW_TEXTURE_PARAMS(.xf = xf, .sprite = crosshair_tag));
|
||||
draw_texture(G.ui_gp_flow, DRAW_TEXTURE_PARAMS(.xf = xf, .sprite = crosshair));
|
||||
}
|
||||
|
||||
/* FIXME: Enable this */
|
||||
#if 0
|
||||
{
|
||||
__profscope(Update window cursor);
|
||||
if (G.debug_camera) {
|
||||
sys_window_cursor_disable_clip(G.window);
|
||||
sys_window_cursor_show(G.window);
|
||||
} else {
|
||||
struct sprite_texture *t = sprite_texture_from_tag_async(sprite_frame_scope, sprite_tag_from_path(LIT("sprite/crosshair.ase")));
|
||||
struct v2 size = V2(t->width, t->height);
|
||||
struct rect cursor_clip = RECT_FROM_V2(G.user_screen_offset, G.user_size);
|
||||
cursor_clip.pos = v2_add(cursor_clip.pos, v2_mul(size, 0.5f));
|
||||
cursor_clip.pos = v2_add(cursor_clip.pos, V2(1, 1));
|
||||
cursor_clip.size = v2_sub(cursor_clip.size, size);
|
||||
sys_window_cursor_hide(G.window);
|
||||
sys_window_cursor_enable_clip(G.window, cursor_clip);
|
||||
#endif
|
||||
} else {
|
||||
__profscope(Update windows cursor);
|
||||
#if 0
|
||||
sys_window_cursor_disable_clip(G.window);
|
||||
sys_window_cursor_show(G.window);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ========================== *
|
||||
* Create user sim cmd
|
||||
|
||||
Loading…
Reference in New Issue
Block a user