diff --git a/code/game/CMakeLists.txt b/code/game/CMakeLists.txt index ad1658b..0e193db 100644 --- a/code/game/CMakeLists.txt +++ b/code/game/CMakeLists.txt @@ -17,6 +17,8 @@ add_executable(eco2d source/packet.c source/player.c source/signal_handling.c + source/profiler.c + source/debug_ui.c source/utils/options.c diff --git a/code/game/header/debug_ui.h b/code/game/header/debug_ui.h new file mode 100644 index 0000000..87dcbc8 --- /dev/null +++ b/code/game/header/debug_ui.h @@ -0,0 +1,4 @@ +#pragma once +#include "system.h" + +void debug_draw(void); \ No newline at end of file diff --git a/code/game/header/platform.h b/code/game/header/platform.h index 5fc6b47..98dfc20 100644 --- a/code/game/header/platform.h +++ b/code/game/header/platform.h @@ -6,5 +6,7 @@ void platform_shutdown(); float platform_frametime(); uint8_t platform_is_running(); +float platform_zoom_get(void); + void platform_input(); void platform_render(); diff --git a/code/game/header/profiler.h b/code/game/header/profiler.h new file mode 100644 index 0000000..bff7e69 --- /dev/null +++ b/code/game/header/profiler.h @@ -0,0 +1,32 @@ +#pragma once +#include "system.h" + +typedef enum { + PROF_MAIN_LOOP, + PROF_WORLD_WRITE, + PROF_RENDER, + PROF_ENTITY_LERP, + + MAX_PROF, + PROF_FORCE_UINT8 = UINT8_MAX +} profiler_kind; + +typedef struct { + profiler_kind id; + char const *name; + + uint32_t num_invocations; + double start_time; + double delta_time; + double total_time; +} profiler; + +void profiler_reset(profiler_kind id); +void profiler_start(profiler_kind id); +void profiler_stop(profiler_kind id); +void profiler_collate(void); + +double profiler_delta(profiler_kind id); +char const *profiler_name(profiler_kind id); + +#define profile(id) defer(profiler_start(id), profiler_stop(id)) diff --git a/code/game/header/system.h b/code/game/header/system.h index dd4a6a7..87affdb 100644 --- a/code/game/header/system.h +++ b/code/game/header/system.h @@ -1,8 +1,15 @@ #pragma once +#include #include #include #include #define ZPL_NANO #include "zpl.h" + +#define defer_var ZPL_CONCAT(_i_,__LINE__) +#define defer(s,e) for ( \ +uint32_t defer_var = (s, 0); \ +!defer_var; \ +(defer_var += 1), e) \ diff --git a/code/game/header/utils/raylib_helpers.h b/code/game/header/utils/raylib_helpers.h index ac6c2e0..a329a66 100644 --- a/code/game/header/utils/raylib_helpers.h +++ b/code/game/header/utils/raylib_helpers.h @@ -2,6 +2,7 @@ #include "system.h" #include "raylib.h" #include "world/blocks.h" +#include "assets.h" static inline void DrawTextEco(const char *text, float posX, float posY, int fontSize, Color color, float spacing) { diff --git a/code/game/source/debug_ui.c b/code/game/source/debug_ui.c new file mode 100644 index 0000000..a9ade56 --- /dev/null +++ b/code/game/source/debug_ui.c @@ -0,0 +1,203 @@ +#include "debug_ui.h" +#include "raylib.h" + +typedef enum { + DITEM_RAW, + DITEM_TEXT, + DITEM_BUTTON, + DITEM_SLIDER, + DITEM_LIST, + DITEM_END, + + DITEM_FORCE_UINT8 = UINT8_MAX +} debug_kind; + +typedef struct { + float x, y; +} debug_draw_result; + +#define DBG_FONT_SIZE 22 +#define DBG_FONT_SPACING DBG_FONT_SIZE * 1.2f +#define DBG_START_XPOS 15 +#define DBG_START_YPOS 200 +#define DBG_LIST_XPOS_OFFSET 10 +#define DBG_SHADOW_OFFSET_XPOS 1 +#define DBG_SHADOW_OFFSET_YPOS 1 + +static uint8_t is_shadow_rendered; + +typedef struct debug_item { + debug_kind kind; + char const *name; + float name_width; + union { + union { + char const *text; + uint64_t val; + }; + + struct { + struct debug_item *items; + uint8_t is_collapsed; + } list; + + void (*on_click)(void); + }; + + debug_draw_result (*proc)(struct debug_item*, float, float); +} debug_item; + +typedef enum { + DAREA_OUTSIDE, + DAREA_HOVER, + DAREA_HELD, + DAREA_PRESS, + + DAREA_FORCE_UINT8 = UINT8_MAX +} debug_area_status; + +debug_area_status check_mouse_area(float xpos, float ypos, float w, float h); +bool is_btn_pressed(float xpos, float ypos, float w, float h, Color *color); + +void UIDrawText(const char *text, float posX, float posY, int fontSize, Color color); +int UIMeasureText(const char *text, int fontSize); + +#include "debug_ui_widgets.c" + +static debug_item items[] = { + { + .kind = DITEM_LIST, + .name = "general", + .list = { + .items = (debug_item[]) { + { .kind = DITEM_TEXT, .name = "delta time", .proc = DrawDeltaTime }, + { .kind = DITEM_TEXT, .name = "random literal", .text = "hello", .proc = DrawLiteral }, + { .kind = DITEM_TEXT, .name = "zoom", .proc = DrawZoom }, + { .kind = DITEM_END }, + } + } + }, + { + .kind = DITEM_LIST, + .name = "profilers", + .list = { + .items = (debug_item[]) { + { .kind = DITEM_RAW, .val = PROF_MAIN_LOOP, .proc = DrawProfilerDelta }, + { .kind = DITEM_RAW, .val = PROF_WORLD_WRITE, .proc = DrawProfilerDelta }, + { .kind = DITEM_RAW, .val = PROF_RENDER, .proc = DrawProfilerDelta }, + { .kind = DITEM_RAW, .val = PROF_ENTITY_LERP, .proc = DrawProfilerDelta }, + { .kind = DITEM_END }, + }, + //.is_collapsed = 1 + } + }, + {.kind = DITEM_END}, +}; + +debug_draw_result debug_draw_list(debug_item *list, float xpos, float ypos, bool is_shadow) { + is_shadow_rendered = is_shadow; + for (debug_item *it = list; it->kind != DITEM_END; it += 1) { + switch (it->kind) { + case DITEM_LIST: { + // NOTE(zaklaus): calculate and cache name width for future use + if (it->name_width == 0) { + it->name_width = UIMeasureText(it->name, DBG_FONT_SIZE); + } + Color color = RAYWHITE; + if (is_btn_pressed(xpos, ypos, it->name_width, DBG_FONT_SIZE, &color)) { + it->list.is_collapsed = !it->list.is_collapsed; + } + + UIDrawText(it->name, xpos, ypos, DBG_FONT_SIZE, color); + ypos += DBG_FONT_SPACING; + if (it->list.is_collapsed) break; + debug_draw_result res = debug_draw_list(it->list.items, xpos+DBG_LIST_XPOS_OFFSET, ypos, is_shadow); + ypos = res.y; + }break; + + case DITEM_TEXT: { + char const *text = TextFormat("%s: ", it->name); + if (it->name_width == 0) { + it->name_width = UIMeasureText(text, DBG_FONT_SIZE); + } + UIDrawText(text, xpos, ypos, DBG_FONT_SIZE, RAYWHITE); + assert(it->proc); + + debug_draw_result res = it->proc(it, xpos + it->name_width, ypos); + ypos = res.y; + }break; + + case DITEM_RAW: { + assert(it->proc); + + debug_draw_result res = it->proc(it, xpos, ypos); + ypos = res.y; + }break; + + default: { + + }break; + } + } + + return (debug_draw_result){xpos, ypos}; +} + +void debug_draw(void) { + debug_draw_list(items, DBG_START_XPOS+DBG_SHADOW_OFFSET_XPOS, DBG_START_YPOS+DBG_SHADOW_OFFSET_YPOS, 1); // NOTE(zaklaus): draw shadow + debug_draw_list(items, DBG_START_XPOS, DBG_START_YPOS, 0); +} + +debug_area_status check_mouse_area(float xpos, float ypos, float w, float h) { + if (is_shadow_rendered) return DAREA_OUTSIDE; + bool is_inside = CheckCollisionPointRec(GetMousePosition(), (Rectangle){xpos, ypos, w, h}); + + if (is_inside) { + return IsMouseButtonReleased(MOUSE_LEFT_BUTTON) ? DAREA_PRESS : IsMouseButtonDown(MOUSE_LEFT_BUTTON) ? DAREA_HELD : DAREA_HOVER; + } + return DAREA_OUTSIDE; +} + +bool is_btn_pressed(float xpos, float ypos, float w, float h, Color *color) { + assert(color); + *color = RAYWHITE; + debug_area_status area = check_mouse_area(xpos, ypos, w, h); + if (area == DAREA_PRESS) { + *color = RED; + return true; + } else if (area == DAREA_HOVER) { + *color = YELLOW; + } else if (area == DAREA_HELD) { + *color = RED; + } + + return false; +} + +static inline +void UIDrawText(const char *text, float posX, float posY, int fontSize, Color color) { + // Check if default font has been loaded + if (GetFontDefault().texture.id != 0) { + Vector2 position = { (float)posX , (float)posY }; + + int defaultFontSize = 10; // Default Font chars height in pixel + int new_spacing = fontSize/defaultFontSize; + + DrawTextEx(GetFontDefault(), text, position, (float)fontSize , (float)new_spacing , is_shadow_rendered ? BLACK : color); + } +} + +static inline +int UIMeasureText(const char *text, int fontSize) { + Vector2 vec = { 0.0f, 0.0f }; + + // Check if default font has been loaded + if (GetFontDefault().texture.id != 0) { + int defaultFontSize = 10; // Default Font chars height in pixel + int new_spacing = fontSize/defaultFontSize; + + vec = MeasureTextEx(GetFontDefault(), text, (float)fontSize, (float)new_spacing); + } + + return (int)vec.x; +} diff --git a/code/game/source/debug_ui_widgets.c b/code/game/source/debug_ui_widgets.c new file mode 100644 index 0000000..091c29d --- /dev/null +++ b/code/game/source/debug_ui_widgets.c @@ -0,0 +1,48 @@ +#include "debug_ui.h" +#include "raylib.h" +#include "platform.h" +#include "profiler.h" + +//~ NOTE(zaklaus): helpers + +static inline debug_draw_result +DrawFloat(float xpos, float ypos, float val) { + char const *text = TextFormat("%.02f\n", val); + UIDrawText(text, xpos, ypos, DBG_FONT_SIZE, RAYWHITE); + return (debug_draw_result){.x = xpos + UIMeasureText(text, DBG_FONT_SIZE), .y = ypos + DBG_FONT_SPACING}; +} + +static inline debug_draw_result +DrawFormattedText(float xpos, float ypos, char const *text) { + assert(text); + UIDrawText(text, xpos, ypos, DBG_FONT_SIZE, RAYWHITE); + return (debug_draw_result){.x = xpos + UIMeasureText(text, DBG_FONT_SIZE), .y = ypos + DBG_FONT_SPACING}; +} + +//~ NOTE(zaklaus): widgets + +static inline debug_draw_result +DrawDeltaTime(debug_item *it, float xpos, float ypos) { + (void)it; + float dt = GetFrameTime(); + return DrawFormattedText(xpos, ypos, TextFormat("%.02f (%.02f fps)", dt * 1000.0f, 1.0f/dt)); +} + +static inline debug_draw_result +DrawZoom(debug_item *it, float xpos, float ypos) { + (void)it; + + return DrawFloat(xpos, ypos, platform_zoom_get()); +} + +static inline debug_draw_result +DrawLiteral(debug_item *it, float xpos, float ypos) { + assert(it->text); + return DrawFormattedText(xpos, ypos, it->text); +} + +static inline debug_draw_result +DrawProfilerDelta(debug_item *it, float xpos, float ypos) { + float dt = profiler_delta(it->val); + return DrawFormattedText(xpos, ypos, TextFormat("%s: %.02f ms", profiler_name(it->val), dt * 1000.0f)); +} diff --git a/code/game/source/main.c b/code/game/source/main.c index 40152fa..025d14d 100644 --- a/code/game/source/main.c +++ b/code/game/source/main.c @@ -5,6 +5,7 @@ #include "entity.h" #include "utils/options.h" #include "signal_handling.h" +#include "profiler.h" #include "flecs/flecs.h" #include "flecs/flecs_dash.h" @@ -85,9 +86,13 @@ int main(int argc, char** argv) } while (game_is_running()) { - game_input(); - game_update(); - game_render(); + profile (PROF_MAIN_LOOP) { + game_input(); + game_update(); + game_render(); + } + + profiler_collate(); } game_shutdown(); diff --git a/code/game/source/platform_raylib.c b/code/game/source/platform_raylib.c index c262659..2e3319b 100644 --- a/code/game/source/platform_raylib.c +++ b/code/game/source/platform_raylib.c @@ -9,6 +9,8 @@ #include "math.h" #include "world/blocks.h" #include "assets.h" +#include "profiler.h" +#include "debug_ui.h" #include "utils/raylib_helpers.h" uint16_t screenWidth = 1600; @@ -114,8 +116,10 @@ void do_entity_fadeinout(uint64_t key, entity_view * data); float zpl_lerp(float,float,float); void platform_render() { - game_world_view_active_entity_map(lerp_entity_positions); - game_world_view_active_entity_map(do_entity_fadeinout); + profile(PROF_ENTITY_LERP) { + game_world_view_active_entity_map(lerp_entity_positions); + game_world_view_active_entity_map(do_entity_fadeinout); + } render_camera.zoom = zpl_lerp(render_camera.zoom, target_zoom, 0.18); camera_update(); @@ -124,12 +128,16 @@ void platform_render() { zoom_overlay_tran = zpl_lerp(zoom_overlay_tran, (target_zoom <= CAM_OVERLAY_ZOOM_LEVEL) ? 1.0f : 0.0f, GetFrameTime()*2.0f); BeginDrawing(); - ClearBackground(GetColor(0x222034)); - BeginMode2D(render_camera); - game_world_view_active_entity_map(DEBUG_draw_ground); - game_world_view_active_entity_map(DEBUG_draw_entities); - EndMode2D(); - display_conn_status(); + profile (PROF_RENDER) { + ClearBackground(GetColor(0x222034)); + BeginMode2D(render_camera); + game_world_view_active_entity_map(DEBUG_draw_ground); + game_world_view_active_entity_map(DEBUG_draw_entities); + EndMode2D(); + + display_conn_status(); + debug_draw(); + } EndDrawing(); } @@ -143,9 +151,6 @@ void display_conn_status() { } else { DrawText("Connection: single-player", 5, 5, 12, BLUE); } - - DrawFPS(0, 20); - DrawText(TextFormat("Zoom: %.02f", target_zoom), 0, 45, 20, WHITE); } void DEBUG_draw_ground(uint64_t key, entity_view * data) { @@ -262,3 +267,7 @@ void do_entity_fadeinout(uint64_t key, entity_view * data) { default: break; } } + +float platform_zoom_get(void) { + return target_zoom; +} \ No newline at end of file diff --git a/code/game/source/profiler.c b/code/game/source/profiler.c new file mode 100644 index 0000000..580264b --- /dev/null +++ b/code/game/source/profiler.c @@ -0,0 +1,53 @@ +#include "profiler.h" +#include "raylib.h" +#include + +#define PROF_COLLATE_WINDOW 0.5 + +// NOTE(zaklaus): KEEP ORDER IN SYNC WITH profiler_kind ENUM !!! +static profiler profilers[] = { + { .id = PROF_MAIN_LOOP, .name = "measured time" }, + { .id = PROF_WORLD_WRITE, .name = "world write" }, + { .id = PROF_RENDER, .name = "render" }, + { .id = PROF_ENTITY_LERP, .name = "entity lerp" }, +}; + +static_assert((sizeof(profilers)/sizeof(profilers[0])) == MAX_PROF, "mismatched profilers"); + +void profiler_reset(profiler_kind id) { + profilers[id].num_invocations = 0; + profilers[id].total_time = 0.0; +} + +void profiler_start(profiler_kind id) { + profilers[id].start_time = GetTime(); +} + +void profiler_stop(profiler_kind id) { + profilers[id].num_invocations += 1; + profilers[id].total_time += GetTime() - profilers[id].start_time; + profilers[id].start_time = 0.0; +} + +void profiler_collate() { + static double frame_counter = 0.0; + + frame_counter += GetFrameTime(); + + if (frame_counter >= PROF_COLLATE_WINDOW) { + for (uint32_t i = 0; i < MAX_PROF; i += 1) { + profiler *p = &profilers[i]; + p->delta_time = p->num_invocations == 0 ? 0.0 : p->total_time / (double)p->num_invocations; + } + + frame_counter = 0.0; + } +} + +double profiler_delta(profiler_kind id) { + return profilers[id].delta_time; +} + +char const *profiler_name(profiler_kind id) { + return profilers[id].name; +} diff --git a/code/game/source/world/world.c b/code/game/source/world/world.c index 8d3da77..4c743cc 100644 --- a/code/game/source/world/world.c +++ b/code/game/source/world/world.c @@ -7,6 +7,7 @@ #include "entity_view.h" #include "world/worldgen/worldgen.h" #include "platform.h" +#include "profiler.h" #include "packets/pkt_send_librg_update.h" @@ -165,34 +166,36 @@ static void world_tracker_update(uint8_t ticker, uint32_t freq, uint8_t radius) if (world.tracker_update[ticker] > zpl_time_rel_ms()) return; world.tracker_update[ticker] = zpl_time_rel_ms() + freq; - ECS_IMPORT(world.ecs, General); - ECS_IMPORT(world.ecs, Net); - - ecs_iter_t it = ecs_query_iter(world.ecs_update); - static char buffer[WORLD_LIBRG_BUFSIZ] = {0}; - world.active_layer_id = ticker; - - while (ecs_query_next(&it)) { - ClientInfo *p = ecs_column(&it, ClientInfo, 1); - - for (int i = 0; i < it.count; i++) { - size_t datalen = WORLD_LIBRG_BUFSIZ; + profile(PROF_WORLD_WRITE) { + ECS_IMPORT(world.ecs, General); + ECS_IMPORT(world.ecs, Net); - // TODO(zaklaus): SUPER TEMPORARY HOT !!! simulate variable radius queries - { - librg_entity_radius_set(world_tracker(), p[i].peer, radius); + ecs_iter_t it = ecs_query_iter(world.ecs_update); + static char buffer[WORLD_LIBRG_BUFSIZ] = {0}; + world.active_layer_id = ticker; + + while (ecs_query_next(&it)) { + ClientInfo *p = ecs_column(&it, ClientInfo, 1); + + for (int i = 0; i < it.count; i++) { + size_t datalen = WORLD_LIBRG_BUFSIZ; + + // TODO(zaklaus): SUPER TEMPORARY HOT !!! simulate variable radius queries + { + librg_entity_radius_set(world_tracker(), p[i].peer, radius); + } + + // TODO(zaklaus): push radius once librg patch comes in + int32_t result = librg_world_write(world_tracker(), p[i].peer, buffer, &datalen, NULL); + + if (result > 0) { + zpl_printf("[info] buffer size was not enough, please increase it by at least: %d\n", result); + } else if (result < 0) { + zpl_printf("[error] an error happened writing the world %d\n", result); + } + + pkt_send_librg_update((uint64_t)p[i].peer, p[i].view_id, ticker, buffer, datalen); } - - // TODO(zaklaus): push radius once librg patch comes in - int32_t result = librg_world_write(world_tracker(), p[i].peer, buffer, &datalen, NULL); - - if (result > 0) { - zpl_printf("[info] buffer size was not enough, please increase it by at least: %d\n", result); - } else if (result < 0) { - zpl_printf("[error] an error happened writing the world %d\n", result); - } - - pkt_send_librg_update((uint64_t)p[i].peer, p[i].view_id, ticker, buffer, datalen); } } }