diff --git a/src/ase.c b/src/ase.c index 22b113d3..e1466d6a 100644 --- a/src/ase.c +++ b/src/ase.c @@ -828,8 +828,8 @@ struct ase_decode_sheet_result ase_decode_sheet(struct arena *arena, struct buff u64 image_height = frame_height * frames_y; make_image_dimensions_squareish(&ase_header, &frames_x, &frames_y, &image_width, &image_height); - u32 num_tags = 0; - struct ase_tag *tag_head = NULL; + u32 num_spans = 0; + struct ase_span *span_head = NULL; u32 num_frames = 0; struct ase_frame *frame_head = NULL; @@ -877,27 +877,27 @@ struct ase_decode_sheet_result ase_decode_sheet(struct arena *arena, struct buff switch (chunk_type) { case CHUNK_TYPE_TAGS: { - u16 frame_tag_count = br_read_u16(&br); + u16 frame_span_count = br_read_u16(&br); br_seek(&br, 8); - for (u16 k = 0; k < frame_tag_count; ++k) { - struct ase_tag *tag = arena_push_zero(arena, struct ase_tag); - tag->next = tag_head; - tag_head = tag; + for (u16 k = 0; k < frame_span_count; ++k) { + struct ase_span *span = arena_push_zero(arena, struct ase_span); + span->next = span_head; + span_head = span; - tag->start = br_read_u16(&br); - tag->end = br_read_u16(&br); + span->start = br_read_u16(&br); + span->end = br_read_u16(&br); br_seek(&br, 13); /* TODO: Decode utf-8 */ u16 str_len = br_read_u16(&br); u8 *str_bytes = br_read_raw(&br, str_len); - tag->name = (struct string) { + span->name = (struct string) { str_len, arena_push_array(arena, u8, str_len) }; - MEMCPY(tag->name.text, str_bytes, str_len); - ++num_tags; + MEMCPY(span->name.text, str_bytes, str_len); + ++num_spans; } } break; @@ -920,9 +920,9 @@ struct ase_decode_sheet_result ase_decode_sheet(struct arena *arena, struct buff res.image_size = V2(image_width, image_height); res.frame_size = V2(frame_width, frame_height); res.num_frames = num_frames; - res.num_tags = num_tags; + res.num_spans = num_spans; res.frame_head = frame_head; - res.tag_head = tag_head; + res.span_head = span_head; return res; } diff --git a/src/ase.h b/src/ase.h index cd013920..9e2b1a88 100644 --- a/src/ase.h +++ b/src/ase.h @@ -12,11 +12,11 @@ struct ase_error_list { struct ase_error *last; }; -struct ase_tag { +struct ase_span { struct string name; u32 start; u32 end; - struct ase_tag *next; + struct ase_span *next; }; struct ase_frame { @@ -35,9 +35,9 @@ struct ase_decode_sheet_result { struct v2 image_size; struct v2 frame_size; u32 num_frames; - u32 num_tags; + u32 num_spans; struct ase_frame *frame_head; - struct ase_tag *tag_head; + struct ase_span *span_head; struct ase_error_list errors; }; diff --git a/src/entity.h b/src/entity.h index cb9d37f5..68112402 100644 --- a/src/entity.h +++ b/src/entity.h @@ -65,9 +65,9 @@ struct entity { /* ====================================================================== */ /* Sprite */ - struct xform sprite_quad_xform; struct string sprite_name; - struct string sprite_tag_name; + struct string sprite_span_name; + struct xform sprite_quad_xform; u32 sprite_tint; /* ====================================================================== */ diff --git a/src/font.h b/src/font.h index 3757f99f..89d2ea72 100644 --- a/src/font.h +++ b/src/font.h @@ -4,7 +4,6 @@ #include "texture.h" #include "util.h" - struct asset; struct work_startup_receipt; struct renderer_startup_receipt; diff --git a/src/game.c b/src/game.c index aeb1b78b..110db521 100644 --- a/src/game.c +++ b/src/game.c @@ -134,7 +134,7 @@ INTERNAL void game_update(void) e->rel_xform = XFORM_TRS(.t = pos, .r = r, .s = size); struct string sprite_name = STR("res/graphics/tim.ase"); - struct string sprite_tag_name = STR("UNARMED"); + struct string sprite_span_name = STR("UNARMED"); struct sheet *sheet = sheet_load(sprite_name); f32 meters_width = sheet->frame_size.x / (f32)PIXELS_PER_UNIT; f32 meters_height = sheet->frame_size.y / (f32)PIXELS_PER_UNIT; @@ -157,7 +157,7 @@ INTERNAL void game_update(void) e->sprite_quad_xform = sprite_xf; e->sprite_name = sprite_name; - e->sprite_tag_name = sprite_tag_name; + e->sprite_span_name = sprite_span_name; e->sprite_tint = COLOR_WHITE; entity_enable_prop(e, ENTITY_PROP_PLAYER_CONTROLLED); @@ -185,7 +185,7 @@ INTERNAL void game_update(void) e->rel_xform = XFORM_TRS(.t = pos, .r = r, .s = size); struct string sprite_name = STR("res/graphics/tim.ase"); - struct string sprite_tag_name = STR("UNARMED"); + struct string sprite_span_name = STR("UNARMED"); struct sheet *sheet = sheet_load(sprite_name); f32 meters_width = sheet->frame_size.x / (f32)PIXELS_PER_UNIT; f32 meters_height = sheet->frame_size.y / (f32)PIXELS_PER_UNIT; @@ -208,7 +208,7 @@ INTERNAL void game_update(void) e->sprite_quad_xform = sprite_xf; e->sprite_name = sprite_name; - e->sprite_tag_name = sprite_tag_name; + e->sprite_span_name = sprite_span_name; e->sprite_tint = RGBA_F(0.5, 0.5, 0, 1); //entity_enable_prop(e, ENTITY_PROP_PLAYER_CONTROLLED); @@ -339,31 +339,33 @@ INTERNAL void game_update(void) * Update animation * ========================== */ +#if 0 if (entity_has_prop(ent, ENTITY_PROP_ANIMATING)) { f64 time_in_frame = ent->animation_time_in_frame + G.world.dt; - u64 tag_frame_offset = ent->animation_frame; + u64 span_frame_offset = ent->animation_frame; struct sheet *sheet = sheet_load(ent->sprite_name); if (sheet) { - struct sheet_tag tag = sheet_get_tag(sheet, ent->sprite_tag_name); - u64 frame_index = tag.start + tag_frame_offset; + struct sheet_span span = sheet_get_span(sheet, ent->sprite_span_name); + u64 frame_index = span.start + span_frame_offset; struct sheet_frame frame = sheet_get_frame(sheet, frame_index); while (time_in_frame > frame.duration) { time_in_frame -= frame.duration; ++frame_index; - if (frame_index > tag.end) { + if (frame_index > span.end) { /* Loop animation */ - frame_index = tag.start; + frame_index = span.start; } frame = sheet_get_frame(sheet, frame_index); } - tag_frame_offset = frame_index - tag.start; + span_frame_offset = frame_index - span.start; } ent->animation_time_in_frame = time_in_frame; - ent->animation_frame = tag_frame_offset; + ent->animation_frame = span_frame_offset; } +#endif /* ========================== * * Test diff --git a/src/log.h b/src/log.h index af746069..1a5b1f06 100644 --- a/src/log.h +++ b/src/log.h @@ -38,7 +38,7 @@ struct log_level_settings { struct log_event { i32 level; - struct string msg; /* Lifetime is only as long as the callback function call */ + struct string msg; /* Lifetime is only as long as the callback function call receiving the event */ struct log_level_settings settings; /* These will be nulled if LOG_INCLUDE_SOURCE_LOCATION is disabled */ diff --git a/src/sheet.c b/src/sheet.c index b2a06157..8cba6b5f 100644 --- a/src/sheet.c +++ b/src/sheet.c @@ -1,3 +1,5 @@ +#if 1 + #include "sheet.h" #include "arena.h" #include "log.h" @@ -31,7 +33,6 @@ GLOBAL struct { struct sheet_task_params_store params; } G = { 0 }, DEBUG_ALIAS(G, G_sheet); - /* ========================== * * Startup * ========================== */ @@ -106,19 +107,19 @@ INTERNAL struct sheet sheet_from_ase(struct arena *arena, struct ase_decode_shee }; } - /* Init tags */ - sheet.tags_count = ase.num_tags; - if (ase.num_tags > 0) { - sheet.tags_dict = fixed_dict_init(arena, (u64)(ase.num_tags * SHEET_LOOKUP_TABLE_CAPACITY_FACTOR)); - for (struct ase_tag *ase_tag = ase.tag_head; ase_tag; ase_tag = ase_tag->next) { - struct string name = string_copy(arena, ase_tag->name); - struct sheet_tag *tag = arena_push(arena, struct sheet_tag); - *tag = (struct sheet_tag) { + /* Init spans */ + sheet.spans_count = ase.num_spans; + if (ase.num_spans > 0) { + sheet.spans_dict = fixed_dict_init(arena, (u64)(ase.num_spans * SHEET_LOOKUP_TABLE_CAPACITY_FACTOR)); + for (struct ase_span *ase_span = ase.span_head; ase_span; ase_span = ase_span->next) { + struct string name = string_copy(arena, ase_span->name); + struct sheet_span *span = arena_push(arena, struct sheet_span); + *span = (struct sheet_span) { .name = name, - .start = ase_tag->start, - .end = ase_tag->end + .start = ase_span->start, + .end = ase_span->end }; - fixed_dict_set(arena, &sheet.tags_dict, name, tag); + fixed_dict_set(arena, &sheet.spans_dict, name, span); } } @@ -278,13 +279,11 @@ struct sheet *sheet_load(struct string path) * Sheet data * ========================== */ -GLOBAL READONLY struct sheet_tag g_default_tag = { 0 }; - -struct sheet_tag sheet_get_tag(struct sheet *sheet, struct string name) +struct sheet_span sheet_get_span(struct sheet *sheet, struct string name) { - struct sheet_tag res = g_default_tag; - if (sheet->tags_count > 0) { - struct sheet_tag *entry = fixed_dict_get(&sheet->tags_dict, name); + struct sheet_span res = { 0 }; + if (sheet->spans_count > 0) { + struct sheet_span *entry = fixed_dict_get(&sheet->spans_dict, name); if (entry) { res = *entry; } @@ -297,3 +296,308 @@ struct sheet_frame sheet_get_frame(struct sheet *sheet, u32 index) index = min_u32(sheet->frames_count - 1, index); return sheet->frames[index]; } + +#else + +#include "sheet.h" +#include "arena.h" +#include "log.h" +#include "sys.h" +#include "scratch.h" +#include "resource.h" +#include "asset_cache.h" +#include "ase.h" +#include "util.h" +#include "work.h" + +#define SHEET_LOAD_THREAD_COUNT_MAX 4 + +struct sheet_task_params { + struct sheet_task_params *next_free; + + struct asset *asset; + u64 path_len; + char path_cstr[1024]; +}; + +struct sheet_task_params_store { + struct sheet_task_params *head_free; + struct arena arena; + struct sys_mutex mutex; +}; + +/* ========================== * + * Global state + * ========================== */ + +GLOBAL struct { + struct sheet_task_params_store params; + struct sys_thread load_threads[SHEET_LOAD_THREAD_COUNT_MAX]; + +} G = { 0 }, DEBUG_ALIAS(G, G_sheet); + +/* ========================== * + * Startup + * ========================== */ + +struct sheet_startup_receipt sheet_startup(struct work_startup_receipt *work_sr, + struct asset_cache_startup_receipt *asset_cache_sr, + struct resource_startup_receipt *resource_sr) +{ + (UNUSED)work_sr; + (UNUSED)asset_cache_sr; + (UNUSED)resource_sr; + + G.params.arena = arena_alloc(GIGABYTE(64)); + G.params.mutex = sys_mutex_alloc(); + + return (struct sheet_startup_receipt) { 0 }; +} + +/* ========================== * + * Load task param store + * ========================== */ + +INTERNAL struct sheet_task_params *sheet_task_params_alloc(void) +{ + struct sheet_task_params *p = NULL; + sys_mutex_lock(&G.params.mutex); + { + if (G.params.head_free) { + p = G.params.head_free; + G.params.head_free = p->next_free; + } else { + p = arena_push_zero(&G.params.arena, struct sheet_task_params); + } + } + sys_mutex_unlock(&G.params.mutex); + return p; +} + +INTERNAL void sheet_task_params_release(struct sheet_task_params *p) +{ + sys_mutex_lock(&G.params.mutex); + { + p->next_free = G.params.head_free; + G.params.head_free = p; + } + sys_mutex_unlock(&G.params.mutex); +} + +/* ========================== * + * Init + * ========================== */ + +#define SHEET_LOOKUP_TABLE_CAPACITY_FACTOR 2.0 + +INTERNAL struct sheet sheet_from_ase(struct arena *arena, struct ase_decode_sheet_result ase) +{ + struct sheet sheet = { 0 }; + + ASSERT(ase.num_frames >= 1); + + /* Init frames */ + sheet.image_size = ase.image_size; + sheet.frame_size = ase.frame_size; + sheet.frames = arena_push_array_zero(arena, struct sheet_frame, ase.num_frames); + sheet.frames_count = ase.num_frames; + for (struct ase_frame *ase_frame = ase.frame_head; ase_frame; ase_frame = ase_frame->next) { + u32 index = ase_frame->index; + sheet.frames[index] = (struct sheet_frame) { + .index = index, + .duration = ase_frame->duration, + .clip = ase_frame->clip + }; + } + + /* Init spans */ + sheet.spans_count = ase.num_spans; + if (ase.num_spans > 0) { + sheet.spans_dict = fixed_dict_init(arena, (u64)(ase.num_spans * SHEET_LOOKUP_TABLE_CAPACITY_FACTOR)); + for (struct ase_span *ase_span = ase.span_head; ase_span; ase_span = ase_span->next) { + struct string name = string_copy(arena, ase_span->name); + struct sheet_span *span = arena_push(arena, struct sheet_span); + *span = (struct sheet_span) { + .name = name, + .start = ase_span->start, + .end = ase_span->end + }; + fixed_dict_set(arena, &sheet.spans_dict, name, span); + } + + } + + return sheet; +} + +INTERNAL struct sheet sheet_default(struct arena *arena) +{ + struct sheet sheet = { 0 }; + sheet.frames_count = 1; + sheet.frames = arena_push(arena, struct sheet_frame); + sheet.frames[0] = (struct sheet_frame) { + .index = 0, + .duration = 0, + .clip = CLIP_ALL + }; + return sheet; +} + +/* ========================== * + * Load + * ========================== */ + +INTERNAL WORK_TASK_FUNC_DEF(sheet_load_asset_task, vparams) +{ + __prof; + struct sheet_task_params *params = (struct sheet_task_params *)vparams; + struct temp_arena scratch = scratch_begin_no_conflict(); + struct string path = string_from_cstr_len(params->path_cstr, params->path_len); + struct asset *asset = params->asset; + + logf_info("Loading sheet \"%F\"", FMT_STR(path)); + sys_timestamp_t start_ts = sys_timestamp(); + + b32 success = false; + struct string error_msg = STR("Unknown error"); + + ASSERT(string_ends_with(path, STR(".ase"))); + if (resource_exists(path)) { + /* Decode */ + struct resource sheet_rs = resource_open(path); + struct ase_decode_sheet_result decoded = ase_decode_sheet(scratch.arena, sheet_rs.bytes); + resource_close(sheet_rs); + + /* Failure paths */ + if (decoded.errors.count > 0) { + /* FIXME: Read all errors from decode */ + struct string msg = decoded.errors.first->msg; + if (msg.len > 0) { + error_msg = msg; + } + goto abort; + } else { + success = true; + } + + /* Initialize sheet & its data into store */ + struct sheet *sheet = NULL; + { + struct asset_cache_store store = asset_cache_store_open(); + sheet = arena_push(store.arena, struct sheet); + *sheet = sheet_from_ase(store.arena, decoded); + asset_cache_store_close(&store); + } + + logf_info("Finished loading sheet \"%F\" in %F seconds", + FMT_STR(path), + FMT_FLOAT(sys_timestamp_seconds(sys_timestamp() - start_ts))); + + asset_cache_mark_ready(asset, sheet); + } else { + success = false; + error_msg = STR("Resource not found"); + goto abort; + } + + abort: + + if (!success) { + logf_error("Error loading sheet \"%F\": %F", FMT_STR(path), FMT_STR(error_msg)); + + /* Store */ + struct sheet *sheet = NULL; + { + struct asset_cache_store store = asset_cache_store_open(); + sheet = arena_push(store.arena, struct sheet); + *sheet = sheet_default(store.arena); + asset_cache_store_close(&store); + } + + asset_cache_mark_ready(asset, sheet); + } + + sheet_task_params_release(params); + + /* Decommit decoded sheet data */ + scratch_end_and_decommit(scratch); +} + +struct asset *sheet_load_asset(struct string path, b32 help) +{ + __prof; + struct temp_arena scratch = scratch_begin_no_conflict(); + + struct string key = string_cat(scratch.arena, path, STR("_sheet")); + u64 hash = asset_cache_hash(key); + b32 is_first_touch; + struct asset *asset = asset_cache_touch(key, hash, &is_first_touch); + + if (is_first_touch) { + /* Assemble task params */ + struct sheet_task_params *params = sheet_task_params_alloc(); + if (path.len > (sizeof(params->path_cstr) - 1)) { + sys_panic(string_format(scratch.arena, + STR("Sheet path \"%F\" too long!"), + FMT_STR(path))); + } + cstr_buff_from_string(BUFFER_FROM_ARRAY(params->path_cstr), path); + params->path_len = path.len; + params->asset = asset; + + /* Push task */ + asset_cache_mark_loading(asset); + struct work_handle wh = { 0 }; + if (help) { + wh = work_push_task_and_help(&sheet_load_asset_task, params, WORK_PRIORITY_NORMAL); + } else { + wh = work_push_task(&sheet_load_asset_task, params, WORK_PRIORITY_NORMAL); + } + asset_cache_set_work(asset, &wh); + } + + scratch_end(scratch); + return asset; +} + +struct sheet *sheet_load_async(struct string path) +{ + __prof; + struct asset *asset = sheet_load_asset(path, false); + struct sheet *sheet = (struct sheet *)asset_cache_get_store_data(asset); + return sheet; +} + + +struct sheet *sheet_load(struct string path) +{ + __prof; + struct asset *asset = sheet_load_asset(path, true); + asset_cache_wait(asset); + struct sheet *sheet = (struct sheet *)asset_cache_get_store_data(asset); + return sheet; +} + +/* ========================== * + * Sheet data + * ========================== */ + +struct sheet_span sheet_get_span(struct sheet *sheet, struct string name) +{ + struct sheet_span res = { 0 }; + if (sheet->spans_count > 0) { + struct sheet_span *entry = fixed_dict_get(&sheet->spans_dict, name); + if (entry) { + res = *entry; + } + } + return res; +} + +struct sheet_frame sheet_get_frame(struct sheet *sheet, u32 index) +{ + index = min_u32(sheet->frames_count - 1, index); + return sheet->frames[index]; +} + +#endif diff --git a/src/sheet.h b/src/sheet.h index 285a25e9..e0ce9464 100644 --- a/src/sheet.h +++ b/src/sheet.h @@ -14,11 +14,15 @@ struct sheet { struct v2 frame_size; u32 frames_count; struct sheet_frame *frames; - u32 tags_count; - struct fixed_dict tags_dict; + u32 spans_count; + struct fixed_dict spans_dict; }; struct sheet_tag { + struct string path; +}; + +struct sheet_span { struct string name; u32 start; u32 end; @@ -39,7 +43,7 @@ struct asset *sheet_load_asset(struct string path, b32 wait); struct sheet *sheet_load_async(struct string path); struct sheet *sheet_load(struct string path); -struct sheet_tag sheet_get_tag(struct sheet *sheet, struct string name); +struct sheet_span sheet_get_span(struct sheet *sheet, struct string name); struct sheet_frame sheet_get_frame(struct sheet *sheet, u32 index); #endif diff --git a/src/sys.h b/src/sys.h index 567cddf1..7e33b200 100644 --- a/src/sys.h +++ b/src/sys.h @@ -482,7 +482,11 @@ void sys_panic(struct string msg); * Sleep * ========================== */ +/* Sleep for precisely the amount of time specified (more cpu intensive) */ void sys_sleep_precise(f64 seconds); -void sys_sleep_imprecise(f64 seconds); + +/* Sleep for the amount of time specified rounded to the OS scheduler period + * (less cpu intensive) */ +void sys_sleep(f64 seconds); #endif diff --git a/src/sys_win32.c b/src/sys_win32.c index 9a625988..b9a60d99 100644 --- a/src/sys_win32.c +++ b/src/sys_win32.c @@ -1882,10 +1882,10 @@ void sys_sleep_precise(f64 seconds) } } -void sys_sleep_imprecise(f64 seconds) +void sys_sleep(f64 seconds) { __prof; - u32 ms = math_round((f32)seconds); + u32 ms = max_u32(1, math_round((f32)(seconds * 1000.0))); Sleep(ms); } diff --git a/src/user.c b/src/user.c index 6f67f5b6..a13e0e1e 100644 --- a/src/user.c +++ b/src/user.c @@ -1,3 +1,5 @@ +#if 1 + #include "user.h" #include "renderer.h" #include "font.h" @@ -713,18 +715,18 @@ INTERNAL void user_update(void) struct sheet *sheet = sheet_load(ent->sprite_name); if (sheet) { - struct sheet_tag tag = sheet_get_tag(sheet, ent->sprite_tag_name); + struct sheet_span span = sheet_get_span(sheet, ent->sprite_span_name); - u64 frame_index = tag.start + ent->animation_frame; + u64 frame_index = span.start + ent->animation_frame; struct sheet_frame frame = sheet_get_frame(sheet, frame_index); if (entity_has_prop(ent, ENTITY_PROP_ANIMATING)) { f64 time_in_frame = ent->animation_time_in_frame; while (time_in_frame > frame.duration) { time_in_frame -= frame.duration; ++frame_index; - if (frame_index > tag.end) { + if (frame_index > span.end) { /* Loop animation */ - frame_index = tag.start; + frame_index = span.start; } frame = sheet_get_frame(sheet, frame_index); } @@ -1081,3 +1083,1100 @@ void user_shutdown(void) G.shutdown = true; sys_thread_join(&G.user_thread); } + +#else +#include "user.h" +#include "renderer.h" +#include "font.h" +#include "texture.h" +#include "draw.h" +#include "intrinsics.h" +#include "app.h" +#include "game.h" +#include "asset_cache.h" +#include "string.h" +#include "scratch.h" +#include "math.h" +#include "sys.h" +#include "world.h" +#include "entity.h" +#include "mixer.h" + +struct bind_state { + b32 is_held; /* Is this bind held down this frame */ + u32 num_presses; /* How many times was this bind pressed since last frame */ + u32 num_releases; /* How many times was this bind released since last frame */ +}; + +struct blend_tick { + struct blend_tick *next; + struct blend_tick *prev; + struct world world; +}; + +GLOBAL struct { + b32 shutdown; + struct sys_thread user_thread; + + struct arena arena; + + struct sys_window *window; + struct renderer_canvas *world_canvas; + struct renderer_canvas *viewport_bg_canvas; + struct renderer_canvas *viewport_canvas; + struct xform world_view; + + struct blend_tick *head_free_blend_tick; + struct blend_tick *head_blend_tick; + struct world world; + + struct bind_state bind_states[USER_BIND_KIND_COUNT]; + + b32 debug_camera; + b32 debug_camera_panning; + struct v2 debug_camera_pan_start; + + b32 debug_draw; + + /* User thread input */ + struct sys_mutex sys_events_mutex; + struct arena sys_events_arena; + + /* Per-frame */ + f64 time; + f64 dt; + struct v2 screen_size; + struct v2 screen_cursor; + struct v2 viewport_screen_offset; + struct v2 viewport_size; + struct v2 viewport_center; + struct v2 viewport_cursor; + struct v2 world_cursor; +} G = { 0 }, DEBUG_ALIAS(G, G_user); + +/* ========================== * + * Bind state + * ========================== */ + +/* TODO: Remove this */ + +GLOBAL READONLY enum user_bind_kind g_binds[SYS_BTN_COUNT] = { + [SYS_BTN_W] = USER_BIND_KIND_MOVE_UP, + [SYS_BTN_S] = USER_BIND_KIND_MOVE_DOWN, + [SYS_BTN_A] = USER_BIND_KIND_MOVE_LEFT, + [SYS_BTN_D] = USER_BIND_KIND_MOVE_RIGHT, + + /* Testing */ + + [SYS_BTN_C] = USER_BIND_KIND_DEBUG_CLEAR, + [SYS_BTN_F6] = USER_BIND_KIND_DEBUG_DRAW, + [SYS_BTN_F7] = USER_BIND_KIND_DEBUG_CAMERA, + [SYS_BTN_F11] = USER_BIND_KIND_FULLSCREEN, + [SYS_BTN_MWHEELUP] = USER_BIND_KIND_ZOOM_IN, + [SYS_BTN_MWHEELDOWN] = USER_BIND_KIND_ZOOM_OUT, + [SYS_BTN_M3] = USER_BIND_KIND_PAN, + [SYS_BTN_CTRL] = USER_BIND_KIND_CTRL_TEST +}; + +/* ========================== * + * Window -> user communication + * ========================== */ + +INTERNAL struct sys_event_array pop_sys_events(struct arena *arena) +{ + struct sys_event_array array = { 0 }; + sys_mutex_lock(&G.sys_events_mutex); + { + struct buffer events_buff = arena_to_buffer(&G.sys_events_arena); + arena_align(arena, alignof(struct sys_event)); + array.events = (struct sys_event *)arena_push_array(arena, u8, events_buff.size); + array.count = events_buff.size / sizeof(struct sys_event); + MEMCPY(array.events, events_buff.data, events_buff.size); + arena_reset(&G.sys_events_arena); + } + sys_mutex_unlock(&G.sys_events_mutex); + return array; +} + +INTERNAL SYS_WINDOW_EVENT_CALLBACK_DEF(window_event_callback, event) +{ + sys_mutex_lock(&G.sys_events_mutex); + { + *arena_push(&G.sys_events_arena, struct sys_event) = event; + } + sys_mutex_unlock(&G.sys_events_mutex); +} + +/* ========================== * + * Game -> user communication + * ========================== */ + +INTERNAL struct blend_tick *blend_tick_alloc(void) +{ + struct blend_tick *bt = NULL; + if (G.head_free_blend_tick) { + bt = G.head_free_blend_tick; + G.head_free_blend_tick = bt->next; + *bt = (struct blend_tick) { + .world = bt->world + }; + } else { + bt = arena_push_zero(&G.arena, struct blend_tick); + world_alloc(&bt->world); + } + if (G.head_blend_tick) { + bt->next = G.head_blend_tick; + G.head_blend_tick->prev = bt; + } + G.head_blend_tick = bt; + return bt; +} + +INTERNAL void blend_tick_release(struct blend_tick *bt) +{ + struct blend_tick *next = bt->next; + struct blend_tick *prev = bt->prev; + + /* Remove from list */ + if (next) { + next->prev = prev; + } + if (prev) { + prev->next = next; + } + if (bt == G.head_blend_tick) { + G.head_blend_tick = next; + } + + /* Add to free list */ + bt->next = G.head_free_blend_tick; + bt->prev = NULL; + G.head_free_blend_tick = bt; +} + +struct interp_ticks { + struct world *from_tick; + struct world *to_tick; +}; + +INTERNAL struct interp_ticks pull_ticks(f64 blend_time) +{ + __prof; + + /* Find newest stored tick */ + struct world *newest_tick = NULL; + for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) { + if (!newest_tick || bt->world.tick_id > newest_tick->tick_id) { + newest_tick = &bt->world; + } + } + + /* Pull new tick from game thread if necessary */ + if (!newest_tick || game_get_latest_tick_id() > newest_tick->tick_id) { + struct blend_tick *latest_bt = blend_tick_alloc(); + newest_tick = &latest_bt->world; + game_get_latest_tick(newest_tick); + } + + /* Find oldest tick */ + struct world *oldest_tick = NULL; + for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) { + if (!oldest_tick || bt->world.tick_id < oldest_tick->tick_id) { + oldest_tick = &bt->world; + } + } + + /* Find closest ticks to blend time */ + struct world *from_tick = oldest_tick; + struct world *to_tick = newest_tick; + for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) { + f64 bt_time = sys_timestamp_seconds(bt->world.tick_ts); + + if (bt_time < blend_time && bt_time > sys_timestamp_seconds(from_tick->tick_ts)) { + from_tick = &bt->world; + } + + if (bt_time > blend_time && bt_time < sys_timestamp_seconds(to_tick->tick_ts)) { + to_tick = &bt->world; + } + } + + /* Free any unused old ticks */ + { + struct temp_arena scratch = scratch_begin_no_conflict(); + struct blend_tick **bts_to_free = arena_dry_push(scratch.arena, struct blend_tick *); + u64 bts_to_free_count = 0; + + for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) { + f64 bt_time = sys_timestamp_seconds(bt->world.tick_ts); + if (bt_time < sys_timestamp_seconds(from_tick->tick_ts)) { + *arena_push(scratch.arena, struct blend_tick *) = bt; + ++bts_to_free_count; + } + } + + for (u64 i = 0; i < bts_to_free_count; ++i) { + blend_tick_release(bts_to_free[i]); + } + + scratch_end(scratch); + } + + return (struct interp_ticks) { + .from_tick = from_tick, + .to_tick = to_tick + }; +} + +/* ========================== * + * User -> game communication + * ========================== */ + +struct game_cmd_node { + struct game_cmd cmd; + struct game_cmd_node *next; +}; + +struct game_cmd_list { + struct arena *arena; + struct game_cmd_node *first; + struct game_cmd_node *last; +}; + +INTERNAL void queue_game_cmd(struct game_cmd_list *list, struct game_cmd cmd) +{ + struct game_cmd_node *node = arena_push_zero(list->arena, struct game_cmd_node); + node->cmd = cmd; + if (list->first) { + list->last->next = node; + } else { + list->first = node; + } + list->last = node; +} + +INTERNAL void pubilsh_game_cmds(struct game_cmd_list *list) +{ + struct temp_arena scratch = scratch_begin(list->arena); + + /* Construct array */ + struct game_cmd_array array = { .cmds = arena_dry_push(scratch.arena, struct game_cmd) }; + for (struct game_cmd_node *node = list->first; node; node = node->next) { + struct game_cmd *cmd = arena_push(scratch.arena, struct game_cmd); + *cmd = node->cmd; + ++array.count; + } + + /* Push array to game thread */ + if (array.count > 0) { + game_push_cmds(array); + } + + scratch_end(scratch); +} + +/* ========================== * + * Update + * ========================== */ + +/* TODO: remove this (testing) */ +INTERNAL void debug_draw_xform(struct xform xf) +{ + f32 thickness = 2.f; + f32 arrowhead_len = 15.f; + u32 color = RGBA_F(0, 1, 1, 0.3); + u32 color_x = RGBA_F(1, 0, 0, 0.3); + u32 color_y = RGBA_F(0, 1, 0, 0.3); + + struct v2 pos = xform_mul_v2(G.world_view, xf.og); + struct v2 x_ray = xform_basis_mul_v2(G.world_view, xform_get_right(xf)); + struct v2 y_ray = xform_basis_mul_v2(G.world_view, xform_get_up(xf)); + + struct quad quad = quad_from_rect(RECT(0, 0, 1, -1)); + quad = quad_mul_xform(quad_scale(quad, 0.075), xf); + + draw_solid_arrow_ray(G.viewport_canvas, pos, x_ray, thickness, arrowhead_len, color_x); + draw_solid_arrow_ray(G.viewport_canvas, pos, y_ray, thickness, arrowhead_len, color_y); + draw_solid_quad(G.viewport_canvas, quad, color); +} + +/* TODO: remove this (testing) */ +INTERNAL void debug_draw_movement(struct entity *ent) +{ + f32 thickness = 2.f; + f32 arrow_len = 15.f; + + u32 color_vel = RGBA_F(1, 0.5, 0, 1); + u32 color_acc = RGBA_F(1, 1, 0.5, 1); + + struct v2 pos = xform_mul_v2(G.world_view, ent->world_xform.og); + struct v2 vel_ray = xform_basis_mul_v2(G.world_view, ent->velocity); + struct v2 acc_ray = xform_basis_mul_v2(G.world_view, ent->acceleration); + + draw_solid_arrow_ray(G.viewport_canvas, pos, vel_ray, thickness, arrow_len, color_vel); + draw_solid_arrow_ray(G.viewport_canvas, pos, acc_ray, thickness, arrow_len, color_acc); +} + +INTERNAL void user_update(void) +{ + struct temp_arena scratch = scratch_begin_no_conflict(); + struct game_cmd_list cmd_list = { + .arena = scratch.arena + }; + + /* Get time */ + f64 cur_time = sys_timestamp_seconds(sys_timestamp()); + G.dt = max_f64(0.0, cur_time - G.time); + G.time += G.dt; + G.screen_size = sys_window_get_size(G.window); + + /* ========================== * + * Begin dynamic asset scopes + * ========================== */ + + struct sheet_scope sheet_scope = sheet_scope_begin(); + + /* ========================== * + * Produce interpolated tick + * ========================== */ + + b32 tick_is_first_frame = false; + { + __profscope(produce_interpolated_tick); + +#if USER_INTERP_ENABLED + f64 blend_time_offset = (1.0 / GAME_FPS) * USER_INTERP_OFFSET_TICK_RATIO; + f64 blend_time = G.time > blend_time_offset ? G.time - blend_time_offset : 0; + + /* Pull ticks */ + struct interp_ticks interp_ticks = pull_ticks(blend_time); + struct world *t0 = interp_ticks.from_tick; + struct world *t1 = interp_ticks.to_tick; + + tick_is_first_frame = (t0->tick_id == 0 || t1->tick_id == 0); + + f32 tick_blend = 0; + { + f64 t0_time = sys_timestamp_seconds(t0->tick_ts); + f64 t1_time = sys_timestamp_seconds(t1->tick_ts); + if (t1_time > t0_time) { + tick_blend = (f32)((blend_time - t0_time) / (t1_time - t0_time)); + } + tick_blend = clamp_f32(tick_blend, 0.0f, 1.0f); + } + + world_copy_replace(&G.world, t1); + + /* Blend time */ + G.world.time = math_lerp_f64(t0->time, t1->time, (f64)tick_blend); + + /* Blend entities */ + struct entity_array t0_entities = entity_store_as_array(&t0->entity_store); + struct entity_array t1_entities = entity_store_as_array(&t1->entity_store); + struct entity_array world_entities = entity_store_as_array(&G.world.entity_store); + + u64 num_entities = min_u64(t0_entities.count, t1_entities.count); + for (u64 i = 0; i < num_entities; ++i) { + struct entity *e0 = &t0_entities.entities[i]; + struct entity *e1 = &t1_entities.entities[i]; + struct entity *e = &world_entities.entities[i]; + if (e0->handle.gen == e1->handle.gen && e0->continuity_gen == e1->continuity_gen) { + e->rel_xform = xform_lerp(e0->rel_xform, e1->rel_xform, tick_blend); + e->world_xform = xform_lerp(e0->world_xform, e1->world_xform, tick_blend); + + e->acceleration = v2_lerp(e0->acceleration, e1->acceleration, tick_blend); + e->velocity = v2_lerp(e0->velocity, e1->velocity, tick_blend); + e->player_acceleration = math_lerp(e0->player_acceleration, e1->player_acceleration, tick_blend); + e->player_aim = v2_lerp(e0->player_aim, e1->player_aim, tick_blend); + + e->sprite_quad_xform = xform_lerp(e0->sprite_quad_xform, e1->sprite_quad_xform, tick_blend); + e->animation_time_in_frame = math_lerp_f64(e0->animation_time_in_frame, e1->animation_time_in_frame, (f64)tick_blend); + + e->camera_quad_xform = xform_lerp(e0->camera_quad_xform, e1->camera_quad_xform, tick_blend); + e->camera_rel_xform_target = xform_lerp(e0->camera_rel_xform_target, e1->camera_rel_xform_target, tick_blend); + } + } +#else + struct interp_ticks interp_ticks = pull_ticks(G.time); + world_copy_replace(&G.world, interp_ticks.to_tick); + tick_is_first_frame = G.world.tick_id == 0; +#endif + } + struct entity_array entities_array = entity_store_as_array(&G.world.entity_store); + + /* ========================== * + * Find important entities + * ========================== */ + + struct entity *player = entity_nil(); + struct entity *active_camera = entity_nil(); + + for (u64 entity_index = 0; entity_index < entities_array.count; ++entity_index) { + struct entity *ent = &entities_array.entities[entity_index]; + if (!ent->valid) continue; + + /* Player */ + if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) { + player = ent; + } + + /* Active camera */ + if (entity_has_prop(ent, ENTITY_PROP_CAMERA) && entity_has_prop(ent, ENTITY_PROP_CAMERA_ACTIVE)) { + active_camera = ent; + } + } + + /* ========================== * + * Read sys events + * ========================== */ + + struct sys_event_array events = pop_sys_events(scratch.arena); + + /* Reset bind states "was_pressed" */ + for (u32 i = 0; i < ARRAY_COUNT(G.bind_states); ++i) { + G.bind_states[i] = (struct bind_state) { + .is_held = G.bind_states[i].is_held + }; + } + + for (u64 entity_index = 0; entity_index < events.count; ++entity_index) { + struct sys_event *event = &events.events[entity_index]; + + if (event->kind == SYS_EVENT_KIND_QUIT) { + app_quit(); + } + + if (event->kind == SYS_EVENT_KIND_BUTTON_UP) { +#if DEVELOPER + /* Escape quit */ + if (event->button == SYS_BTN_ESC) { + app_quit(); + } +#endif + } + + /* Update mouse pos */ + if (event->kind == SYS_EVENT_KIND_CURSOR_MOVE) { + G.screen_cursor = event->cursor_position; + } + + /* Update bind states */ + if ((event->kind == SYS_EVENT_KIND_BUTTON_DOWN || event->kind == SYS_EVENT_KIND_BUTTON_UP) && !event->is_repeat) { + enum sys_btn button = event->button; + button = button >= SYS_BTN_COUNT ? SYS_BTN_NONE : button; + enum user_bind_kind bind = g_binds[button]; + if (bind) { + b32 pressed = event->kind == SYS_EVENT_KIND_BUTTON_DOWN; + b32 out_of_bounds = button >= SYS_BTN_M1 && button <= SYS_BTN_M5 && + (G.viewport_cursor.x < 0 || + G.viewport_cursor.y < 0 || + G.viewport_cursor.x > G.viewport_size.x || + G.viewport_cursor.y > G.viewport_size.y); + G.bind_states[bind].is_held = pressed && !out_of_bounds; + if (pressed) { + if (!out_of_bounds) { + ++G.bind_states[bind].num_presses; + } + } else { + ++G.bind_states[bind].num_releases; + } + } + } + } + + /* ========================== * + * Debug commands + * ========================== */ + + /* Test fullscreen */ + { + struct bind_state state = G.bind_states[USER_BIND_KIND_FULLSCREEN]; + if (state.num_presses) { + struct sys_window_settings settings = sys_window_get_settings(G.window); + settings.flags ^= SYS_WINDOW_SETTINGS_FLAG_FULLSCREEN; + sys_window_update_settings(G.window, &settings); + } + } + + /* Test clear world */ + { + struct bind_state state = G.bind_states[USER_BIND_KIND_DEBUG_CLEAR]; + if (state.num_presses || state.is_held) { + queue_game_cmd(&cmd_list, (struct game_cmd) { + .kind = GAME_CMD_KIND_CLEAR_ALL + }); + } + } + + if (G.bind_states[USER_BIND_KIND_DEBUG_DRAW].num_presses > 0) { + G.debug_draw = !G.debug_draw; + } + + if (G.bind_states[USER_BIND_KIND_DEBUG_CAMERA].num_presses > 0) { + G.debug_camera = !G.debug_camera; + } + + + /* ========================== * + * Update viewport + * ========================== */ + + /* Calculate screen viewport dimensions */ + if (G.debug_camera) { + G.viewport_size = G.screen_size; + G.viewport_screen_offset = V2(0, 0); + } else { + + /* Determine viewport size by camera & window dimensions */ + f32 aspect_ratio = 1.0; + { + struct xform quad_xf = xform_mul(active_camera->world_xform, active_camera->camera_quad_xform); + struct v2 camera_size = xform_get_scale(quad_xf); + if (!v2_eq(camera_size, V2(0, 0))) { + aspect_ratio = camera_size.x / camera_size.y; + } + } + f32 width = G.screen_size.x; + f32 height = G.screen_size.y; + if (width / height > aspect_ratio) { + width = height * aspect_ratio; + } else { + height = (f32)math_ceil(width / aspect_ratio); + } + G.viewport_size = V2(width, height); + + /* Center viewport in window */ + f32 x = 0; + f32 y = 0; + x = math_round(G.screen_size.x / 2 - width / 2); + y = math_round(G.screen_size.y / 2 - height / 2); + G.viewport_screen_offset = V2(x, y); + } + + G.viewport_center = v2_mul(G.viewport_size, 0.5); + G.viewport_cursor = v2_sub(G.screen_cursor, G.viewport_screen_offset); + + /* ========================== * + * Update view + * ========================== */ + + if (G.debug_camera) { + G.world_view = xform_with_rotation(G.world_view, 0); + + /* Pan view */ + if (G.bind_states[USER_BIND_KIND_PAN].is_held) { + if (!G.debug_camera_panning) { + G.debug_camera_pan_start = xform_invert_mul_v2(G.world_view, G.viewport_cursor); + } + G.debug_camera_panning = true; + struct v2 offset = v2_sub(G.debug_camera_pan_start, xform_invert_mul_v2(G.world_view, G.viewport_cursor)); + G.world_view = xform_translate(G.world_view, v2_neg(offset)); + G.debug_camera_pan_start = xform_invert_mul_v2(G.world_view, G.viewport_cursor); + } else { + G.debug_camera_panning = false; + } + + /* Zoom view */ + i32 input_zooms = G.bind_states[USER_BIND_KIND_ZOOM_IN].num_presses - G.bind_states[USER_BIND_KIND_ZOOM_OUT].num_presses; + if (input_zooms != 0) { + /* Zoom to cursor */ + f32 zoom_rate = 2; + f32 zoom = math_pow(zoom_rate, input_zooms); + struct v2 world_cursor = xform_invert_mul_v2(G.world_view, G.viewport_cursor); + G.world_view = xform_translate(G.world_view, world_cursor); + G.world_view = xform_scale(G.world_view, V2(zoom, zoom)); + G.world_view = xform_translate(G.world_view, v2_neg(world_cursor)); + } + } else { + struct v2 center = active_camera->world_xform.og; + f32 rot = xform_get_rotation(active_camera->world_xform); + + /* Scale view into viewport based on camera size */ + struct v2 size = G.viewport_size; + { + struct xform quad_xf = xform_mul(active_camera->world_xform, active_camera->camera_quad_xform); + struct v2 camera_size = xform_get_scale(quad_xf); + if (!v2_eq(camera_size, V2(0, 0))) { + size = v2_div_v2(size, camera_size); + } + } + f32 scale = min_f32(size.x, size.y); + + struct trs trs = TRS( + .t = v2_sub(G.viewport_center, center), + .r = rot, + .s = V2(scale, scale) + ); + + struct v2 pivot = center; + G.world_view = XFORM_IDENT; + G.world_view = xform_translate(G.world_view, pivot); + G.world_view = xform_trs_pivot_rs(G.world_view, trs, pivot); + } + G.world_cursor = xform_invert_mul_v2(G.world_view, G.viewport_cursor); + + /* ========================== * + * Update listener + * ========================== */ + + { + struct v2 up = V2(0, -1); + struct v2 listener_pos = xform_invert_mul_v2(G.world_view, G.viewport_center); + struct v2 listener_dir = v2_norm(xform_basis_invert_mul_v2(G.world_view, up)); + mixer_set_listener(listener_pos, listener_dir); + } + + /* ========================== * + * Draw test BG + * ========================== */ + + { + u32 color = RGBA_F(0.2f, 0.2f, 0.2f, 1.f); + draw_solid_rect(G.viewport_bg_canvas, RECT(0, 0, G.viewport_size.x, G.viewport_size.y), color); + } + + /* ========================== * + * Draw test grid + * ========================== */ + + { + f32 thickness = 3.f; + u32 color = RGBA(0x3f, 0x3f, 0x3f, 0xFF); + u32 x_color = RGBA(0x3f, 0, 0, 0xFF); + u32 y_color = RGBA(0, 0x3f, 0, 0xFF); + + i64 startx = -10; + i64 starty = -10; + i64 rows = 20; + i64 cols = 20; + + /* Draw column lines */ + struct v2 col_ray = xform_basis_mul_v2(G.world_view, V2(0, rows)); + for (i64 col = starty; col <= (starty + cols); ++col) { + u32 line_color = color; + if (col == 0) { + line_color = y_color; + } + + struct v2 pos = xform_mul_v2(G.world_view, V2(col, starty)); + draw_solid_ray(G.viewport_bg_canvas, pos, col_ray, thickness, line_color); + } + + struct v2 row_ray = xform_basis_mul_v2(G.world_view, V2(cols, 0)); + for (i64 row = startx; row <= (startx + rows); ++row) { + u32 line_color = color; + if (row == 0) { + line_color = x_color; + } + struct v2 pos = xform_mul_v2(G.world_view, V2(startx, row)); + draw_solid_ray(G.viewport_bg_canvas, pos, row_ray, thickness, line_color); + } + } + + /* ---------------------------------------------------------------------- */ + /* ---------------------------------------------------------------------- */ + + /* ========================== * + * Iterate entities + * ========================== */ + + /* Iterate entities */ + for (u64 entity_index = 0; entity_index < entities_array.count; ++entity_index) { + __profscope(user_entity_iter); + + struct entity *ent = &entities_array.entities[entity_index]; + if (!ent->valid) continue; + + b32 skip_debug_draw = !G.debug_camera && ent == active_camera; + b32 skip_debug_draw_transform = ent == active_camera; + + /* Draw sprite */ + if (ent->sprite_name.len > 0) { + struct string tex_name = ent->sprite_name; + + /* Draw texture */ + struct texture *texture = texture_load_async(tex_name); + if (texture) { + struct xform xf = xform_mul(ent->world_xform, ent->sprite_quad_xform); + struct quad quad = quad_mul_xform(QUAD_UNIT_SQUARE_CENTERED, xf); + + u32 tint = ent->sprite_tint; + struct draw_texture_params params = DRAW_TEXTURE_PARAMS(.texture = texture, .tint = tint); + + struct sheet_tag sheet_tag = sheet_tag_from_path(ent->sprite_name); + struct sheet *sheet = sheet_from_tag(sheet_scope, sheet_tag); + + struct sheet_span span = sheet_get_span(sheet, ent->sprite_span_name); + u64 frame_index = span.start + ent->animation_frame; + struct sheet_frame frame = sheet_get_frame(sheet, frame_index); + if (entity_has_prop(ent, ENTITY_PROP_ANIMATING)) { + f64 time_in_frame = ent->animation_time_in_frame; + while (time_in_frame > frame.duration) { + time_in_frame -= frame.duration; + ++frame_index; + if (frame_index > span.end) { + /* Loop animation */ + frame_index = span.start; + } + frame = sheet_get_frame(sheet, frame_index); + } + } + + params.clip = frame.clip; + + draw_texture_quad(G.world_canvas, params, quad); + +#if 0 + if (G.debug_draw && !skip_debug_draw) { + /* Debug draw sprite quad */ + { + f32 thickness = 2.f; + u32 color = RGBA_F(1, 1, 0, 0.25); + draw_solid_quad_line(G.world_canvas, quad, (thickness / PIXELS_PER_UNIT / G.world_view.zoom), color); + } + + /* Debug draw sprite transform */ + { + debug_draw_xform(xf); + } + + /* Debug draw sprite pivot */ + { + u32 color = RGBA_F(1, 0, 0, 1); + draw_solid_circle(G.world_canvas, ent->world_xform.og, 0.02, color, 20); + } + } +#endif + } + } + + /* Debug draw info */ + if (G.debug_draw && !skip_debug_draw) { + struct temp_arena temp = arena_temp_begin(scratch.arena); + +#if 0 + struct font *disp_font = font_load_async(STR("res/fonts/fixedsys.ttf"), 12.0f); + if (disp_font) { + struct xform xf = ent->world_xform; + struct trs trs = trs_from_xform(xf); + struct v2 velocity = ent->velocity; + struct v2 acceleration = ent->acceleration; + + f32 offset = 1; + struct v2 pos = v2_add(xf.og, v2_mul(V2(0, -1), offset)); + pos = xform_mul_v2(G.world_view, pos); + pos = v2_round(pos); + + struct string disp_name = ent->sprite_name; + + struct string fmt = STR( + "sprite name: \"%F\"\n" + "pos: (%F, %F)\n" + "scale: (%F, %F)\n" + "rot: %F\n" + "velocity: (%F, %F)\n" + "acceleration: (%F, %F)\n" + ); + struct string text = string_format(temp.arena, fmt, + FMT_STR(disp_name), + FMT_FLOAT((f64)trs.t.x), FMT_FLOAT((f64)trs.t.y), + FMT_FLOAT((f64)trs.s.x), FMT_FLOAT((f64)trs.s.y), + FMT_FLOAT((f64)trs.r), + FMT_FLOAT((f64)velocity.x), FMT_FLOAT((f64)velocity.y), + FMT_FLOAT((f64)acceleration.x), FMT_FLOAT((f64)acceleration.y) + ); + + + draw_text(G.viewport_canvas, disp_font, pos, text); + } +#endif + + debug_draw_movement(ent); + + if (!skip_debug_draw_transform) { + debug_draw_xform(ent->world_xform); + } + + /* Draw hierarchy */ + struct entity *parent = entity_from_handle(&G.world.entity_store, ent->parent); + if (parent->valid) { + u32 color = RGBA_F(0.6, 0.6, 1, 0.75); + f32 thickness = 5; + f32 arrow_height = 15; + + struct v2 start = xform_mul_v2(G.world_view, ent->world_xform.og); + struct v2 end = xform_mul_v2(G.world_view, parent->world_xform.og); + draw_solid_arrow_line(G.viewport_canvas, start, end, thickness, arrow_height, color); + } + + /* Draw aim */ + if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) { + u32 color = RGBA_F(0.75, 0, 0.75, 0.5); + f32 thickness = 3; + f32 arrow_height = 10; + struct v2 pos = xform_mul_v2(G.world_view, ent->world_xform.og); + struct v2 aim_ray = xform_basis_mul_v2(G.world_view, ent->player_aim); + draw_solid_arrow_ray(G.viewport_canvas, pos, aim_ray, thickness, arrow_height, color); + } + + /* Draw camera rect */ + if (entity_has_prop(ent, ENTITY_PROP_CAMERA)) { + u32 color = ent == active_camera ? RGBA_F(1, 1, 1, 0.5) : RGBA_F(0, 0.75, 0, 0.5); + f32 thickness = 3; + + + struct xform quad_xf = xform_mul(ent->world_xform, ent->camera_quad_xform); + struct quad quad = quad_mul_xform(QUAD_UNIT_SQUARE_CENTERED, quad_xf); + quad = quad_mul_xform(quad, G.world_view); + + draw_solid_quad_line(G.viewport_canvas, quad, thickness, color); + } + + arena_temp_end(temp); + } + } + + /* Draw crosshair or show cursor */ + if (!G.debug_camera) { + struct v2 crosshair_pos = G.viewport_cursor; + u32 tint = RGBA_F(1, 1, 1, 1); + + struct v2 size = V2(0, 0); + struct texture *t = texture_load_async(STR("res/graphics/crosshair.ase")); + if (t) { + size = t->size; + struct xform xf = XFORM_TRS(.t = crosshair_pos, .s = size); + struct quad quad = quad_mul_xform(QUAD_UNIT_SQUARE_CENTERED, xf); + draw_texture_quad(G.viewport_canvas, DRAW_TEXTURE_PARAMS(.texture = t, .tint = tint), quad); + } + + struct rect cursor_clip = RECT_FROM_V2(G.viewport_screen_offset, G.viewport_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); + } else { + sys_window_cursor_disable_clip(G.window); + sys_window_cursor_show(G.window); + } + + /* ========================== * + * Construct movement input + * ========================== */ + + /* Movement */ + struct v2 input_move_dir = { 0 }; + { + for (enum user_bind_kind bind = 0; bind < (i32)ARRAY_COUNT(G.bind_states); ++bind) { + struct bind_state state = G.bind_states[bind]; + + if (!state.is_held && state.num_presses <= 0) { + continue; + } + + switch (bind) { + /* Movement */ + case USER_BIND_KIND_MOVE_UP: { + input_move_dir.y -= 1; + } break; + + case USER_BIND_KIND_MOVE_DOWN: { + input_move_dir.y += 1; + } break; + + case USER_BIND_KIND_MOVE_LEFT: { + input_move_dir.x -= 1; + } break; + + case USER_BIND_KIND_MOVE_RIGHT: { + input_move_dir.x += 1; + } break; + + default: break; + } + } + + input_move_dir = xform_basis_invert_mul_v2(G.world_view, input_move_dir); /* Make move dir relative to world view */ + input_move_dir = v2_norm(input_move_dir); + } + + /* Aim */ + struct v2 input_aim = player->player_aim; + if (!G.debug_camera) { + input_aim = v2_sub(G.world_cursor, player->world_xform.og); + } + + /* Queue cmd */ + queue_game_cmd(&cmd_list, (struct game_cmd) { + .kind = GAME_CMD_KIND_PLAYER_MOVE, + .move_dir = input_move_dir, + .aim = input_aim + }); + + /* ---------------------------------------------------------------------- */ + /* ---------------------------------------------------------------------- */ + + /* Debug draw info */ + if (G.debug_draw) { + struct temp_arena temp = arena_temp_begin(scratch.arena); + + f32 spacing = 20; + struct v2 pos = V2(10, 8); + struct font *font = font_load(STR("res/fonts/fixedsys.ttf"), 12.0f); + + draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("time: %F"), FMT_FLOAT((f64)G.time))); + pos.y += spacing; + + draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("screen_size: (%F, %F)"), FMT_FLOAT((f64)G.screen_size.x), FMT_FLOAT((f64)G.screen_size.y))); + pos.y += spacing; + + draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("screen_cursor: (%F, %F)"), FMT_FLOAT((f64)G.screen_cursor.x), FMT_FLOAT((f64)G.screen_cursor.y))); + pos.y += spacing; + + draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("viewport_screen_offset: (%F, %F)"), FMT_FLOAT((f64)G.viewport_screen_offset.x), FMT_FLOAT((f64)G.viewport_screen_offset.y))); + pos.y += spacing; + + draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("viewport_size: (%F, %F)"), FMT_FLOAT((f64)G.viewport_size.x), FMT_FLOAT((f64)G.viewport_size.y))); + pos.y += spacing; + + draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("viewport_center: (%F, %F)"), FMT_FLOAT((f64)G.viewport_center.x), FMT_FLOAT((f64)G.viewport_center.y))); + pos.y += spacing; + + draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("viewport_cursor: (%F, %F)"), FMT_FLOAT((f64)G.viewport_cursor.x), FMT_FLOAT((f64)G.viewport_cursor.y))); + pos.y += spacing; + + draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("world_view.og: (%F, %F)"), FMT_FLOAT((f64)G.world_view.og.x), FMT_FLOAT((f64)G.world_view.og.y))); + pos.y += spacing; + + draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("world_view rotation: %F"), FMT_FLOAT((f64)xform_get_rotation(G.world_view)))); + pos.y += spacing; + + draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("world_view scale: (%F, %F)"), FMT_FLOAT((f64)xform_get_scale(G.world_view).x), FMT_FLOAT((f64)xform_get_scale(G.world_view).x))); + pos.y += spacing; + + draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("world_cursor: (%F, %F)"), FMT_FLOAT((f64)G.world_cursor.x), FMT_FLOAT((f64)G.world_cursor.y))); + pos.y += spacing; + + draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("debug_camera: %F"), FMT_STR(G.debug_camera ? STR("true") : STR("false")))); + pos.y += spacing; + + arena_temp_end(temp); + } + + /* Push game cmds */ + pubilsh_game_cmds(&cmd_list); + + /* ========================== * + * Present + * ========================== */ + + /* Send canvases to GPU */ + renderer_canvas_send_to_gpu(G.viewport_bg_canvas); + renderer_canvas_send_to_gpu(G.world_canvas); + renderer_canvas_send_to_gpu(G.viewport_canvas); + + /* Set canvas views before presenting */ + renderer_canvas_set_view(G.viewport_bg_canvas, XFORM_IDENT); + renderer_canvas_set_view(G.world_canvas, G.world_view); + renderer_canvas_set_view(G.viewport_canvas, XFORM_IDENT); + + /* Present */ + i32 vsync = VSYNC_ENABLED; + + struct renderer_canvas **canvases = arena_dry_push(scratch.arena, struct renderer_canvas *); + u64 canvases_count = 0; + { + /* Viewport background canvas */ + *arena_push(scratch.arena, struct renderer_canvas *) = G.viewport_bg_canvas; + ++canvases_count; + + /* World canvas */ + if (!tick_is_first_frame) { + /* Only render world if not on first frame */ + *arena_push(scratch.arena, struct renderer_canvas *) = G.world_canvas; + ++canvases_count; + } + + /* Viewport canvas */ + *arena_push(scratch.arena, struct renderer_canvas *) = G.viewport_canvas; + ++canvases_count; + } + + renderer_canvas_present(canvases, canvases_count, G.screen_size, RECT_FROM_V2(G.viewport_screen_offset, G.viewport_size), vsync); + + /* ========================== * + * End dynamic asset scopes + * ========================== */ + + sheet_scope_end(sheet_scope); + + scratch_end(scratch); +} + +/* ========================== * + * Startup + * ========================== */ + +INTERNAL SYS_THREAD_FUNC_DEF(user_thread_entry_point, arg) +{ + (UNUSED)arg; + + sys_timestamp_t last_frame_ts = 0; + f64 target_dt = USER_FRAME_LIMIT > (0) ? (1.0 / USER_FRAME_LIMIT) : 0; + + while (!G.shutdown) { + __profscope(user_update_w_sleep); + sleep_frame(last_frame_ts, target_dt); + last_frame_ts = sys_timestamp(); + user_update(); + } +} + +struct user_startup_receipt user_startup(struct work_startup_receipt *work_sr, + struct renderer_startup_receipt *renderer_sr, + struct font_startup_receipt *font_sr, + struct texture_startup_receipt *texture_sr, + struct draw_startup_receipt *draw_sr, + struct game_startup_receipt *game_sr, + struct asset_cache_startup_receipt *asset_cache_sr, + struct mixer_startup_receipt *mixer_sr, + struct sys_window *window) +{ + (UNUSED)work_sr; + (UNUSED)renderer_sr; + (UNUSED)font_sr; + (UNUSED)texture_sr; + (UNUSED)draw_sr; + (UNUSED)game_sr; + (UNUSED)asset_cache_sr; + (UNUSED)mixer_sr; + + G.arena = arena_alloc(GIGABYTE(64)); + + G.sys_events_mutex = sys_mutex_alloc(); + G.sys_events_arena = arena_alloc(GIGABYTE(64)); + + world_alloc(&G.world); + + G.world_canvas = renderer_canvas_alloc(); + G.world_view = XFORM_TRS(.t = V2(0, 0), .r = 0, .s = V2(PIXELS_PER_UNIT, PIXELS_PER_UNIT)); + + G.viewport_bg_canvas = renderer_canvas_alloc(); + G.viewport_canvas = renderer_canvas_alloc(); + + G.window = window; + sys_window_register_event_callback(G.window, &window_event_callback); + + G.user_thread = sys_thread_init(&user_thread_entry_point, NULL, STR("[P1] User thread")); + + return (struct user_startup_receipt) { 0 }; +} + +void user_shutdown(void) +{ + G.shutdown = true; + sys_thread_join(&G.user_thread); +} + +#endif