#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 "console.h" #include "sys.h" #include "tick.h" /* FIXME: remove this (testing) */ #include "sound.h" #include "mixer.h" /* NOTE: If vsync is enabled and lower then this will be ignored */ #define USER_FPS 300 struct view { f32 px_per_unit; struct v2 center; f32 zoom; f32 rot; }; GLOBAL struct { b32 shutdown; struct sys_thread user_thread; struct sys_window *window; struct renderer_canvas *world_canvas; struct renderer_canvas *ui_canvas; struct view world_view; struct tick blend_ticks[2]; b32 debug_camera; b32 debug_camera_panning; struct v2 debug_camera_panning_from; 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_center; struct v2 screen_mouse; } L = { 0 } DEBUG_LVAR(L_user); /* ========================== * * Bind state * ========================== */ struct bind_state { b32 pressed; /* Is this bind held down this frame */ u32 num_presses; /* How many times was this bind pressed since last frame */ }; GLOBAL struct bind_state g_bind_states[USER_BIND_KIND_COUNT] = { 0 }; /* TODO: Remove this */ GLOBAL 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_F6] = USER_BIND_KIND_DEBUG_DRAW, [SYS_BTN_F7] = USER_BIND_KIND_DEBUG_CAMERA, [SYS_BTN_MWHEELUP] = USER_BIND_KIND_ZOOM_IN, [SYS_BTN_MWHEELDOWN] = USER_BIND_KIND_ZOOM_OUT }; /* ========================== * * Window -> user communication * ========================== */ INTERNAL void window_event_callback(struct sys_event event) { sys_mutex_lock(&L.sys_events_mutex); { struct sys_event *write_event = arena_push(&L.sys_events_arena, struct sys_event); *write_event = event; } sys_mutex_unlock(&L.sys_events_mutex); } INTERNAL struct sys_event_array pull_sys_events(struct arena *arena) { struct sys_event_array array = { 0 }; if (L.sys_events_arena.pos > 0) { sys_mutex_lock(&L.sys_events_mutex); { struct buffer events_buff = arena_to_buffer(&L.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(&L.sys_events_arena); } sys_mutex_unlock(&L.sys_events_mutex); } return array; } /* ========================== * * Game -> user communication * ========================== */ struct interp_ticks { struct tick *from_tick; struct tick *to_tick; }; INTERNAL struct interp_ticks pull_interp_ticks(void) { /* Function is currently hard-coded for 2 blend ticks */ ASSERT(ARRAY_COUNT(L.blend_ticks) == 2); /* Determine from & to tick from existing ticks */ struct tick *from_tick = NULL; struct tick *to_tick = NULL; if (L.blend_ticks[1].id > L.blend_ticks[0].id) { from_tick = &L.blend_ticks[0]; to_tick = &L.blend_ticks[1]; } else { from_tick = &L.blend_ticks[1]; to_tick = &L.blend_ticks[0]; } /* Check for new tick from game thread */ u64 latest_tick_id = game_get_latest_tick_id(); if (latest_tick_id > to_tick->id) { /* Swap pointers */ struct tick *temp = from_tick; from_tick = to_tick; to_tick = temp; /* Pull game tick */ game_get_latest_tick(to_tick); } 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 push_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); } /* ========================== * * View * ========================== */ INTERNAL struct mat3x3 view_get_xform(struct view view) { f32 scale = view.zoom * view.px_per_unit; struct trs trs = TRS( .t = v2_sub(L.screen_center, view.center), .r = view.rot, .s = V2(scale, scale) ); struct v2 pivot = view.center; struct mat3x3 res = mat3x3_from_translate(pivot); res = mat3x3_trs_pivot_rs(res, trs, pivot); return res; } INTERNAL struct v2 view_xform_point(struct view view, struct v2 p) { struct mat3x3 mtx = view_get_xform(view); return mat3x3_mul_v2(mtx, p); } INTERNAL struct v2 view_inverse_xform_point(struct view view, struct v2 p) { struct mat3x3 mtx_inverse = mat3x3_inverse(view_get_xform(view)); return mat3x3_mul_v2(mtx_inverse, p); } INTERNAL struct v2 view_mouse_pos(struct view view) { return view_inverse_xform_point(view, L.screen_mouse); } /* ========================== * * Update * ========================== */ /* TODO: remove this (testing) */ INTERNAL void debug_draw_xform(struct mat3x3 mtx) { f32 thickness = 2.f / PIXELS_PER_UNIT / L.world_view.zoom; 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 = mat3x3_get_pos(mtx); struct v2 x_ray = mat3x3_get_right(mtx); struct v2 y_ray = mat3x3_get_up(mtx); struct quad quad = quad_from_rect(RECT(0, 0, 1, -1)); quad = quad_mul_mat3x3(quad_scale(quad, 0.075), mtx); draw_solid_ray(L.world_canvas, pos, x_ray, thickness, color_x); draw_solid_ray(L.world_canvas, pos, y_ray, thickness, color_y); draw_solid_quad(L.world_canvas, quad, color); } /* TODO: remove this (testing) */ INTERNAL void debug_draw_movement(struct entity *ent) { f32 thickness = 2.f / PIXELS_PER_UNIT / L.world_view.zoom; u32 color_vel = RGBA_F(1, 0.5, 0, 1); u32 color_acc = RGBA_F(1, 1, 0.5, 1); struct v2 pos = mat3x3_get_pos(ent->world_xform); struct v2 vel_ray = ent->velocity; struct v2 acc_ray = ent->acceleration; draw_solid_ray(L.world_canvas, pos, vel_ray, thickness, color_vel); draw_solid_ray(L.world_canvas, pos, acc_ray, thickness, 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()); L.dt = max_f64(0.0, cur_time - L.time); L.time += L.dt; /* Get screen dimensions */ L.screen_size = sys_window_get_size(L.window); /* Make screen_size an even number */ L.screen_size = V2(math_round_f32(L.screen_size.x), math_round_f32(L.screen_size.y)); L.screen_size = V2( math_round_f32(L.screen_size.x) % 2 == 0 ? L.screen_size.x : L.screen_size.x + 1, math_round_f32(L.screen_size.y) % 2 == 0 ? L.screen_size.y : L.screen_size.y + 1 ); L.screen_center = v2_mul(L.screen_size, 0.5); /* Read input */ L.screen_mouse = sys_window_get_mouse_pos(L.window); struct sys_event_array events = pull_sys_events(scratch.arena); /* ========================== * * Read sys events * ========================== */ /* Reset bind states "was_pressed" */ for (u32 i = 0; i < ARRAY_COUNT(g_bind_states); ++i) { g_bind_states[i] = (struct bind_state) { .pressed = g_bind_states[i].pressed }; } for (u64 entity_index = 0; entity_index < events.count; ++entity_index) { struct sys_event *event = &events.events[entity_index]; /* Send event to console. Skip if consumed. */ if (console_process_event(*event)) { continue; } if (event->kind == SYS_EVENT_KIND_QUIT) { app_quit(); } /* Move camera/view */ if (event->button == SYS_BTN_M3) { if (event->kind == SYS_EVENT_KIND_BUTTON_DOWN) { L.debug_camera_panning = true; L.debug_camera_panning_from = view_mouse_pos(L.world_view); } else if (event->kind == SYS_EVENT_KIND_BUTTON_UP) { L.debug_camera_panning = false; } } if (event->kind == SYS_EVENT_KIND_BUTTON_UP) { #if DEVELOPER /* Escape quit */ if (event->button == SYS_BTN_ESC) { app_quit(); } #endif } /* Text */ if (event->kind == SYS_EVENT_KIND_TEXT) { /* TODO: remove this (sound test) */ { //u32 flags = SOUND_FLAG_STEREO; u32 flags = 0; struct sound *sound = sound_load(STR("res/sounds/test.mp3"), flags); if (sound) { mixer_play_ex(sound, MIXER_DESC(.volume = 1.0, .speed = 1.0)); } } } /* 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; g_bind_states[bind].pressed = pressed; if (pressed) { ++g_bind_states[bind].num_presses; } } } } /* ========================== * * Process 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.pressed && 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; } } queue_game_cmd(&cmd_list, (struct game_cmd) { .kind = GAME_CMD_KIND_PLAYER_MOVE, .dir = v2_norm(input_move_dir) }); } /* ========================== * * Update view from debug camera * ========================== */ if (g_bind_states[USER_BIND_KIND_DEBUG_DRAW].num_presses > 0) { L.debug_draw = !L.debug_draw; } if (g_bind_states[USER_BIND_KIND_DEBUG_CAMERA].num_presses > 0) { L.debug_camera = !L.debug_camera; } if (L.debug_camera) { /* Pan view */ if (L.debug_camera_panning) { struct v2 offset = v2_sub(L.debug_camera_panning_from, view_mouse_pos(L.world_view)); L.world_view.center = v2_add(L.world_view.center, offset); L.debug_camera_panning_from = view_mouse_pos(L.world_view); } /* 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) { i32 dir = input_zooms >= 0 ? 1 : -1; u32 zooms_abs = input_zooms >= 0 ? input_zooms : -input_zooms; /* Zoom camera/view */ f32 zoom_rate = 2; f32 zoom_min = 1.f / 128.f; f32 zoom_max = 1.f * 128.f; f32 new_zoom = L.world_view.zoom; for (u32 i = 0; i < zooms_abs; ++i) { if (dir > 0) { new_zoom *= zoom_rate; } else { new_zoom *= 1.0f / zoom_rate; } new_zoom = clamp_f32(new_zoom, zoom_min, zoom_max); } struct v2 old_mouse = view_mouse_pos(L.world_view); L.world_view.zoom = new_zoom; struct v2 new_mouse = view_mouse_pos(L.world_view); /* Offset view to zoom in on mouse */ struct v2 offset = v2_sub(old_mouse, new_mouse); L.world_view.center = v2_add(L.world_view.center, offset); } } /* ========================== * * Produce interpolated tick * ========================== */ /* Pull ticks */ struct interp_ticks interp_ticks = pull_interp_ticks(); struct tick *t0 = interp_ticks.from_tick; struct tick *t1 = interp_ticks.to_tick; /* Produce interpolated tick */ struct tick *tick = arena_push_zero(scratch.arena, struct tick); { __profscope(produce_interpolated_tick); sys_timestamp_t tick_publish_delta = t1->published_ts - t0->published_ts; f32 tick_blend = (f32)((sys_timestamp() - t1->published_ts) / (f64)tick_publish_delta); tick_cpy(tick, t1); #if 1 /* Blend time */ tick->time = math_lerp_f64(t0->time, t1->time, (f64)tick_blend); /* Blend entities */ u64 num_entities = min_u64(t0->entities_count, t1->entities_count); for (u64 i = 0; i < num_entities; ++i) { struct entity *e = &tick->entities[i]; struct entity *e0 = &t0->entities[i]; struct entity *e1 = &t1->entities[i]; if (e0->handle.gen == e1->handle.gen && e0->continuity_gen == e1->continuity_gen) { e->rel_trs = trs_lerp(e0->rel_trs, e1->rel_trs, tick_blend); e->world_xform = mat3x3_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_f32(e0->player_acceleration, e1->player_acceleration, tick_blend); e->sprite_trs = trs_lerp(e0->sprite_trs, e1->sprite_trs, tick_blend); e->sprite_pivot_norm = v2_lerp(e0->sprite_pivot_norm, e1->sprite_pivot_norm, tick_blend); e->camera_rot = math_lerp_angle(e0->camera_rot, e1->camera_rot, tick_blend); e->camera_zoom = math_lerp_f32(e0->camera_zoom, e1->camera_zoom, tick_blend); } } #else (UNUSED)tick_blend; #endif } /* ========================== * * Update view from game camera * ========================== */ /* Find camera */ for (u64 entity_index = 0; entity_index < tick->entities_count; ++entity_index) { struct entity *ent = &tick->entities[entity_index]; if (!ent->valid) continue; if (entity_has_prop(ent, ENTITY_PROP_CAMERA) && ent->camera_active && !L.debug_camera) { struct v2 center = mat3x3_get_pos(ent->world_xform); f32 rot = ent->camera_rot; f32 zoom = ent->camera_zoom; zoom = zoom > 0 ? zoom : 1; L.world_view.center = center; L.world_view.rot = rot; L.world_view.zoom = zoom; } } /* ========================== * * Draw test grid * ========================== */ { f32 thickness = 3.f / PIXELS_PER_UNIT / L.world_view.zoom; 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 = 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 = V2(col, starty); draw_solid_ray(L.world_canvas, pos, col_ray, thickness, line_color); } struct v2 row_ray = 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 = V2(startx, row); draw_solid_ray(L.world_canvas, pos, row_ray, thickness, line_color); } } /* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */ /* ========================== * * Iterate entities * ========================== */ /* Iterate entities */ for (u64 entity_index = 0; entity_index < tick->entities_count; ++entity_index) { struct entity *ent = &tick->entities[entity_index]; if (!ent->valid) continue; b32 is_camera = entity_has_prop(ent, ENTITY_PROP_CAMERA); /* Draw sprite */ if (ent->sprite_name.len > 0) { struct string tex_name = ent->sprite_name; /* Draw texture */ struct quad quad = QUAD_UNIT_SQUARE_CENTERED; struct mat3x3 mtx; { struct v2 scale_div_2 = v2_mul(ent->sprite_trs.s, 0.5f); struct v2 pivot = v2_mul_v2(ent->sprite_pivot_norm, scale_div_2); mtx = mat3x3_trs_pivot_r(ent->world_xform, ent->sprite_trs, pivot); quad = quad_mul_mat3x3(quad, mtx); } struct texture *texture = texture_load_async(tex_name); if (texture) { u32 tint = ent->sprite_tint; struct draw_texture_params params = DRAW_TEXTURE_PARAMS(.texture = texture, .tint = tint); struct sheet *sheet = sheet_load(ent->sprite_name); if (sheet) { struct sheet_tag tag = sheet_get_tag(sheet, ent->sprite_tag_name); struct sheet_frame frame = sheet_get_frame(sheet, tag.start); if (entity_has_prop(ent, ENTITY_PROP_ANIMATING)) { b32 looping = ent->animation_looping; f64 time_in_anim = tick->time - ent->animation_start_time; u64 frame_index = tag.start; while (time_in_anim > 0) { frame = sheet_get_frame(sheet, frame_index); time_in_anim -= frame.duration; ++frame_index; if (frame_index > tag.end) { frame_index = tag.start; if (!looping) { break; } } } } params.clip = frame.clip; } draw_texture_quad(L.world_canvas, params, quad); } if (L.debug_draw && !is_camera) { #if 0 /* Debug draw sprite quad */ { f32 thickness = 2.f; u32 color = RGBA_F(1, 1, 0, 0.25); draw_solid_quad_line(L.world_canvas, quad, (thickness / PIXELS_PER_UNIT / L.world_view.zoom), color); } #endif #if 0 /* Debug draw sprite transform */ { debug_draw_xform(mtx); } #endif /* Debug draw sprite pivot */ { u32 color = RGBA_F(1, 0, 0, 1); struct mat3x3 mtx_pre_pivot = ent->world_xform; mtx_pre_pivot = mat3x3_trs(mtx_pre_pivot, ent->sprite_trs); draw_solid_circle(L.world_canvas, mat3x3_get_pos(mtx_pre_pivot), 0.02, color, 20); } } } /* Debug draw info */ if (L.debug_draw && !is_camera) { struct font *disp_font = font_load_async(STR("res/fonts/fixedsys.ttf"), 12.0f); if (disp_font) { struct mat3x3 mtx = ent->world_xform; struct trs trs = trs_from_mat3x3(mtx); struct v2 velocity = ent->velocity; struct v2 acceleration = ent->acceleration; f32 offset = 1; struct v2 pos = v2_add(mat3x3_get_pos(mtx), v2_mul(V2(0, -1), offset)); pos = view_xform_point(L.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(scratch.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(L.ui_canvas, disp_font, pos, text); } debug_draw_xform(ent->world_xform); debug_draw_movement(ent); } } /* ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */ /* Update listener using world view */ mixer_set_listener(L.world_view.center, V2(-math_sin(L.world_view.rot), -math_cos(L.world_view.rot))); /* Send world mouse pos */ struct v2 world_mouse = view_mouse_pos(L.world_view); queue_game_cmd(&cmd_list, (struct game_cmd) { .kind = GAME_CMD_KIND_PLAYER_FOCUS, .pos = world_mouse }); /* Debug draw info */ if (L.debug_draw) { f32 spacing = 20; struct v2 pos = V2(10, 8); struct font *font = font_load(STR("res/fonts/fixedsys.ttf"), 12.0f); draw_text(L.ui_canvas, font, pos, string_format(scratch.arena, STR("time: %F"), FMT_FLOAT((f64)L.time))); pos.y += spacing; draw_text(L.ui_canvas, font, pos, string_format(scratch.arena, STR("screen_size: (%F, %F)"), FMT_FLOAT((f64)L.screen_size.x), FMT_FLOAT((f64)L.screen_size.y))); pos.y += spacing; draw_text(L.ui_canvas, font, pos, string_format(scratch.arena, STR("screen_center: (%F, %F)"), FMT_FLOAT((f64)L.screen_center.x), FMT_FLOAT((f64)L.screen_center.y))); pos.y += spacing; draw_text(L.ui_canvas, font, pos, string_format(scratch.arena, STR("screen_mouse: (%F, %F)"), FMT_FLOAT((f64)L.screen_mouse.x), FMT_FLOAT((f64)L.screen_mouse.y))); pos.y += spacing; draw_text(L.ui_canvas, font, pos, string_format(scratch.arena, STR("world_view.center: (%F, %F)"), FMT_FLOAT((f64)L.world_view.center.x), FMT_FLOAT((f64)L.world_view.center.y))); pos.y += spacing; draw_text(L.ui_canvas, font, pos, string_format(scratch.arena, STR("world_view.rot: %F"), FMT_FLOAT((f64)L.world_view.rot))); pos.y += spacing; draw_text(L.ui_canvas, font, pos, string_format(scratch.arena, STR("world_view.zoom: %F"), FMT_FLOAT((f64)L.world_view.zoom))); pos.y += spacing; draw_text(L.ui_canvas, font, pos, string_format(scratch.arena, STR("world_mouse: (%F, %F)"), FMT_FLOAT((f64)world_mouse.x), FMT_FLOAT((f64)world_mouse.y))); pos.y += spacing; draw_text(L.ui_canvas, font, pos, string_format(scratch.arena, STR("debug_camera: %F"), FMT_STR(L.debug_camera ? STR("true") : STR("false")))); pos.y += spacing; } /* Push game cmds */ push_game_cmds(&cmd_list); /* ========================== * * Present * ========================== */ /* Send canvases to GPU */ renderer_canvas_send_to_gpu(L.world_canvas); renderer_canvas_send_to_gpu(L.ui_canvas); /* Set canvas views before presenting */ renderer_canvas_set_view(L.world_canvas, view_get_xform(L.world_view)); renderer_canvas_set_view(L.ui_canvas, view_get_xform((struct view) {.px_per_unit = 1, .center = L.screen_center, .rot = 0, .zoom = 1})); /* Present */ i32 vsync = VSYNC_ENABLED; struct renderer_canvas **canvases = NULL; u64 canvases_count = 0; if (t0->id <= 0 || t1->id <= 0) { /* Don't render world on first frame */ struct renderer_canvas *present_canvases[] = { L.ui_canvas }; canvases = present_canvases; canvases_count = ARRAY_COUNT(present_canvases); } else { struct renderer_canvas *present_canvases[] = { L.world_canvas, L.ui_canvas }; canvases = present_canvases; canvases_count = ARRAY_COUNT(present_canvases); } renderer_canvas_present(canvases, canvases_count, L.screen_size.x, L.screen_size.y, vsync); scratch_end(scratch); } /* ========================== * * Startup * ========================== */ INTERNAL void user_thread_entry_point(void *arg) { (UNUSED)arg; sys_timestamp_t last_frame_ts = 0; f64 target_dt = USER_FPS > 0 ? (1.0 / USER_FPS) : 0; while (!L.shutdown) { __profscope(user_update_w_sleep); sleep_frame(last_frame_ts, target_dt); last_frame_ts = sys_timestamp(); user_update(); } } void user_startup(struct sys_window *window) { L.window = window; sys_window_register_event_callback(L.window, &window_event_callback); L.sys_events_mutex = sys_mutex_alloc(); L.sys_events_arena = arena_alloc(GIGABYTE(64)); L.world_canvas = renderer_canvas_alloc(); L.world_view = (struct view) { .px_per_unit = PIXELS_PER_UNIT, .center = V2(0, 0), .rot = 0, .zoom = 1 }; L.ui_canvas = renderer_canvas_alloc(); L.user_thread = sys_thread_init(&user_thread_entry_point, NULL, STR("[P1] User thread")); } void user_shutdown(void) { L.shutdown = true; sys_thread_join(&L.user_thread); }