diff --git a/src/app_state.h b/src/app_state.h index 7b09dcb..d1df5b8 100644 --- a/src/app_state.h +++ b/src/app_state.h @@ -17,6 +17,8 @@ struct Inspected_File { }; struct App_State { + u8 should_quit; + Window_Data win_data; User_Input user_input; RNTuple_Data rndata; diff --git a/src/mainloop.cpp b/src/mainloop.cpp index 8f63ccb..88565b9 100644 --- a/src/mainloop.cpp +++ b/src/mainloop.cpp @@ -52,8 +52,7 @@ void run_main_loop(GLFWwindow *window, Arena *arena, App_State &app) app.delta_time_accum.max = 100; app.delta_time_accum.base = arena_push_array(arena, app.delta_time_accum.max); - b8 running = true; - while (running) { + while (!app.should_quit) { chr::time_point frame_start = chr::high_resolution_clock::now(); u64 time_since_prev_frame_us = chr::duration_cast(frame_start - last_saved_time).count(); delta_time_ms = time_since_prev_frame_us * 0.001f; @@ -86,7 +85,7 @@ void run_main_loop(GLFWwindow *window, Arena *arena, App_State &app) } if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS || glfwWindowShouldClose(window)) { - running = false; + app.should_quit = true; break; } diff --git a/src/platform_linux.h b/src/platform_linux.h index 433ad8b..b20e6df 100644 --- a/src/platform_linux.h +++ b/src/platform_linux.h @@ -11,11 +11,11 @@ i32 os_page_size() } internal -bool os_open_and_map_file(const char *fname, App_State &app) +bool os_open_and_map_file(String8 fname, App_State &app) { - FILE *file = fopen(fname, "rb"); + FILE *file = fopen(fname.c(), "rb"); if (!file) { - fprintf(stderr, "Failed to open file '%s' for reading: %s (%d)\n", fname, strerror(errno), errno); + fprintf(stderr, "Failed to open file '%s' for reading: %s (%d)\n", fname.c(), strerror(errno), errno); return false; } int fd = fileno(file); @@ -23,11 +23,11 @@ bool os_open_and_map_file(const char *fname, App_State &app) void *fmem = mmap(0, fsize, PROT_READ, MAP_SHARED_VALIDATE, fd, 0); if (!fmem) { - fprintf(stderr, "Failed to open file %s\n", fname); + fprintf(stderr, "Failed to open file %s\n", fname.c()); return false; } - app.inspected_file.name = str8(fname); + app.inspected_file.name = fname; app.inspected_file.stream = file; app.inspected_file.size = fsize; app.inspected_file.mem = reinterpret_cast(fmem); @@ -43,12 +43,12 @@ void os_unmap_file(u8 *&mem, u64 size) } internal -void os_start_file_watch(const char *fname, App_State &app) +void os_start_file_watch(String8 fname, App_State &app) { int inot = inotify_init1(IN_NONBLOCK); if (inot == -1) fprintf(stderr, "Failed to init inotify: %s (%d)\n", strerror(errno), errno); - if (inotify_add_watch(inot, fname, IN_MODIFY) == -1) + if (inotify_add_watch(inot, fname.c(), IN_MODIFY) == -1) fprintf(stderr, "Failed to add inotify watch: %s (%d)\n", strerror(errno), errno); app.inspected_file.inot = inot; diff --git a/src/render.cpp b/src/render.cpp index 9341566..a69566c 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -1,8 +1,8 @@ internal -std::optional get_section_range(const App_State &app, Section_Id sec) +Byte_Range get_section_range(const App_State &app, Section_Id sec) { switch (sec) { - default: return {}; + default: return { 0, 0 }; case Sec_TFile_Header: return Byte_Range { 0, app.tfile_data.root_file_header_size }; case Sec_TFile_Object: return app.tfile_data.rng_root_file_obj; case Sec_TFile_Info: return app.tfile_data.rng_root_file_info; @@ -127,6 +127,12 @@ Section find_section(App_State &app, u64 off) return {}; } +internal +ImColor imcol(const f32 col[3]) +{ + return ImColor(col[0], col[1], col[2]); +} + internal u32 mem_edit_bg_color_fn(const u8 *, u64 off, void *user_data) { @@ -135,13 +141,11 @@ u32 mem_edit_bg_color_fn(const u8 *, u64 off, void *user_data) Section section = find_section(*app, off); -#define COL(c) (ImColor((c)[0], (c)[1], (c)[2])) - if (section.highlighted) return COL(app->viewer.col_highlight); - if (section.id == Sec_Page && off == section.range.start) return COL(app->viewer.col_page_start); - if (off < section.range.start) return COL(app->viewer.col_key); - if (section.range.end() - section.post_size <= off && off < section.range.end()) return COL(app->viewer.col_checksum); - return COL(app->viewer.col_section[section.id]); -#undef COL + if (section.highlighted) return imcol(app->viewer.col_highlight); + if (section.id == Sec_Page && off == section.range.start) return imcol(app->viewer.col_page_start); + if (off < section.range.start) return imcol(app->viewer.col_key); + if (section.range.end() - section.post_size <= off && off < section.range.end()) return imcol(app->viewer.col_checksum); + return imcol(app->viewer.col_section[section.id]); } internal @@ -150,10 +154,10 @@ void mem_edit_interact_fn(const u8 *, u64 off, void *user_data) } internal -MemoryEditor make_memory_editor(App_State &app) +MemoryEditor make_memory_editor(App_State &app, i32 n_cols) { MemoryEditor mem_edit; - mem_edit.Cols = 32; + mem_edit.Cols = n_cols ? n_cols : 32; mem_edit.OptShowDataPreview = true; // Do nothing on write. // Note that we don't use ReadOnly = true because that disables selecting bytes @@ -166,10 +170,10 @@ MemoryEditor make_memory_editor(App_State &app) } internal -void make_viewer(App_State &app) +void make_viewer(App_State &app, u16 n_cols) { Viewer viewer {}; - viewer.mem_edit = make_memory_editor(app); + viewer.mem_edit = make_memory_editor(app, (i32)n_cols); #define COL(c, r, g, b) viewer.c[0] = r/255.0, viewer.c[1] = g/255.0, viewer.c[2] = b/255.0 COL(col_key, 0, 100, 50); @@ -306,17 +310,17 @@ void update_and_render(Arena *arena, App_State &app, f32 delta_time_ms) // Unique sections: just display a button that jumps to the start of it and show their size for (u32 i = 0; i < Sec_COUNT; ++i) { - std::optional range = get_section_range(app, static_cast(i)); - if (!range) continue; + Byte_Range range = get_section_range(app, static_cast(i)); + if (!range.len) continue; String8 sec_name = section_names[i]; String8 col_label = push_str8f(scratch.arena, "_%s", sec_name.c()); ImGui::ColorEdit3(col_label.c(), app.viewer.col_section[i], flags); ImGui::SameLine(); if (ImGui::Button(sec_name.c())) - viewer_jump_to(app.viewer, range->start); + viewer_jump_to(app.viewer, range.start); ImGui::SameLine(); - ImGui::Text("%s", to_pretty_size(scratch.arena, range->len).c()); + ImGui::Text("%s", to_pretty_size(scratch.arena, range.len).c()); } // Repeated sections: allow jumping to the N-th @@ -389,3 +393,143 @@ void update_and_render(Arena *arena, App_State &app, f32 delta_time_ms) ImGui::End(); } } + +internal +Term_Viewer make_term_viewer(u16 n_cols) +{ + Term_Viewer viewer {}; + viewer.max_cols = n_cols ? n_cols : 32; + + viewer.col_key = ACol_Green; + viewer.col_page_start = ACol_Bright_Magenta; + viewer.col_checksum = ACol_Yellow; + viewer.col_highlight = ACol_Bright_White; + viewer.col_section[Sec_RNTuple_Anchor] = ACol_Bright_Yellow; + viewer.col_section[Sec_RNTuple_Header] = ACol_Cyan; + viewer.col_section[Sec_RNTuple_Footer] = ACol_Blue; + viewer.col_section[Sec_TFile_Header] = ACol_White; + viewer.col_section[Sec_TFile_Object] = ACol_Grey; + viewer.col_section[Sec_TFile_Info] = ACol_Red; + viewer.col_section[Sec_TFile_FreeList] = ACol_Bright_Yellow; + viewer.col_section[Sec_Page] = ACol_Magenta; + viewer.col_section[Sec_Page_List] = ACol_Bright_Cyan; + viewer.col_section[Sec_TKey_List] = ACol_Bright_Green; + return viewer; +} + +internal +Ansi_Color viewer_to_ansi_color(const Viewer &viewer, const Term_Viewer &tviewer, u32 col) +{ + if (col == imcol(viewer.col_key)) return tviewer.col_key; + if (col == imcol(viewer.col_page_start)) return tviewer.col_page_start; + if (col == imcol(viewer.col_checksum)) return tviewer.col_checksum; + if (col == imcol(viewer.col_highlight)) return tviewer.col_highlight; + for (u64 section = 0; section < Sec_COUNT; ++section) + if (col == imcol(viewer.col_section[section])) + return tviewer.col_section[section]; + return ACol_None; +} + +internal +String8 render_legend_to_string(Arena *arena, const Term_Viewer &viewer) +{ + Temp scratch = scratch_begin(&arena, 1); + defer { scratch_end(scratch); }; + + struct String8_Node { + String8_Node *next; + String8 str; + } *head = nullptr, *tail = nullptr; + + u64 tot_len = 0; + for (u64 section = 0; section < Sec_COUNT; ++section) { + Ansi_Color color = viewer.col_section[section]; + String8 color_str = ansi_color_table[color]; + String8 sec_name = section_names[section]; + String8 s = push_str8f(scratch.arena, "%s%s ", color_str.c(), sec_name.c()); + String8_Node *node = arena_push(scratch.arena); + node->str = s; + if (!tail) { + head = node; + } else { + tail->next = node; + } + tail = node; + tot_len += s.size; + } + + String8 legend { arena_push_array_nozero(arena, tot_len + 1), tot_len + 1 }; + legend.str[tot_len] = 0; + u64 cur_size = 0; + + for (String8_Node *node = head; node; node = node->next) { + memcpy(legend.str + cur_size, node->str.str, node->str.size); + cur_size += node->str.size; + } + + return legend; +} + +internal +String8 render_range_to_string(Arena *arena, App_State &app, u64 len, u64 n_cols) +{ + Term_Viewer viewer = make_term_viewer(n_cols); + + String8 legend = render_legend_to_string(arena, viewer); + + // NOTE: +3 because we need 2 chars for each byte + 1 whitespace. + u64 buf_size = (ACOL_MAX_LEN + 3) * len; + u64 n_newlines = len / viewer.max_cols; + buf_size += n_newlines; + buf_size += 2; // two newlines + buf_size += legend.size; + buf_size += 1; // trailing zero + u8 *buf = arena_push_array_nozero(arena, buf_size); + buf[len] = 0; + + u64 start = app.viewer.base_display_addr; + + // We need to properly initialize this before calling mem_edit_bg_color_fn! + app.last_pinfo = &invalid_pinfo; + + const u8 *data = app.inspected_file.mem; + u64 max_addr = app.inspected_file.size; + assert(start <= max_addr); + len = min(len, max_addr - start); + + Temp scratch = scratch_begin(&arena, 1); + defer { scratch_end(scratch); }; + + u8 *cur = buf; + for (u64 i = 0; i < len; ++i) { + u64 off = start + i; + + /// Select color + u32 byte_col = mem_edit_bg_color_fn(data, off, &app); + // piggyback off mem_edit_bg_color_fn instead of rewriting the color-choosing logic. + // Kinda dumb code but we don't need speed here since we're printing this one-off. + Ansi_Color acol = viewer_to_ansi_color(app.viewer, viewer, byte_col); + String8 acol_str = ansi_color_table[acol]; + memcpy(cur, acol_str.str, acol_str.size); + cur += acol_str.size; + + /// Write the human-readable byte + String8 byte_str = push_str8f(scratch.arena, "%02X ", data[off]); + memcpy(cur, byte_str.str, byte_str.size); + cur += byte_str.size; + + if ((i + 1) % viewer.max_cols == 0) + *cur++ = '\n'; + + assert((u64)(cur - buf) < (u64)buf_size); + } + + *cur++ = '\n'; + *cur++ = '\n'; + memcpy(cur, legend.str, legend.size); + cur += legend.size; + + *cur = 0; + + return { buf, (u64)(cur - buf) }; +} diff --git a/src/render.h b/src/render.h index 57421b6..359c4cf 100644 --- a/src/render.h +++ b/src/render.h @@ -1,6 +1,7 @@ struct Viewer { MemoryEditor mem_edit; + // Imgui colors f32 col_section[Sec_COUNT][3]; f32 col_key[3]; f32 col_checksum[3]; @@ -23,3 +24,56 @@ struct Edit_Bg_Color_Data { Viewer viewer; }; + +// ---------------------------------------------------- +// TERMINAL VIEW +// ---------------------------------------------------- +enum Ansi_Color { + ACol_None, + ACol_Red, + ACol_Green, + ACol_Yellow, + ACol_Blue, + ACol_Magenta, + ACol_Cyan, + ACol_White, + ACol_Grey, + ACol_Bright_Red, + ACol_Bright_Green, + ACol_Bright_Yellow, + ACol_Bright_Blue, + ACol_Bright_Magenta, + ACol_Bright_Cyan, + ACol_Bright_White, + ACol_COUNT +}; +const u64 ACOL_MAX_LEN = 6; +internal String8 ansi_color_table[ACol_COUNT] = { + str8("\033[0m"), + str8("\033[31m"), + str8("\033[32m"), + str8("\033[33m"), + str8("\033[34m"), + str8("\033[35m"), + str8("\033[36m"), + str8("\033[37m"), + str8("\033[90m"), + str8("\033[91m"), + str8("\033[92m"), + str8("\033[93m"), + str8("\033[94m"), + str8("\033[95m"), + str8("\033[96m"), + str8("\033[97m") +}; + +struct Term_Viewer { + Ansi_Color col_section[Sec_COUNT]; + Ansi_Color col_key; + Ansi_Color col_checksum; + Ansi_Color col_highlight; + Ansi_Color col_page_start; + + // How many bytes to render per line + u16 max_cols; +}; diff --git a/src/rntuple.h b/src/rntuple.h index fd0362b..a6fc00f 100644 --- a/src/rntuple.h +++ b/src/rntuple.h @@ -110,7 +110,7 @@ struct Section { b8 highlighted; }; -inline const String8 section_names[Sec_COUNT] = { +internal const String8 section_names[Sec_COUNT] = { str8("None"), str8("TFile Header"), str8("TFile Object"), diff --git a/src/rntviewer.cpp b/src/rntviewer.cpp index d9ef6bd..9d77290 100644 --- a/src/rntviewer.cpp +++ b/src/rntviewer.cpp @@ -75,21 +75,124 @@ void app_cleanup(App_State &app) if (app.inspected_file.stream) fclose(app.inspected_file.stream); } +internal +void print_help(const char *argv0) +{ + fprintf(stderr, + "Usage: %s [-t] [-s START] [-l LEN] [-w WIDTH] " + "\n" + "\n\t-t: no graphics, output to terminal" + "\n\t-s: set first displayed byte to START" + "\n\t-l: display LEN bytes (only in terminal mode)" + "\n\t-w: display WIDTH bytes per column" + "\n" + , argv0); +} + +struct Cmdline_Args { + b8 print_to_terminal; + b8 show_help_and_exit; + u64 start_addr; + u64 nbytes_displayed; + u16 n_cols; + + String8 ntpl_name; + String8 file_name; +}; + +struct Conv_Res { + u64 num; + b8 error; +}; + +internal +Conv_Res str_to_u64(String8 s) +{ + Conv_Res res {}; + for (u64 i = 0; i < s.size; ++i) { + u8 c = s.str[i]; + if (c >= '0' && c <= '9') { + res.num *= 10; + res.num += c - '0'; + } else { + res.error = true; + break; + } + } + return res; +} + +internal +void parse_int_arg(i32 &cur_arg_idx, i32 argc, char **argv, u64 &out) +{ + const char *arg = argv[cur_arg_idx]; + if (cur_arg_idx < argc - 1) { + String8 nxt_arg = str8_from_c(argv[++cur_arg_idx]); + Conv_Res res = str_to_u64(nxt_arg); + if (res.error) + fprintf(stderr, "Invalid integer after %s flag.\n", arg); + else + out = res.num; + } else { + fprintf(stderr, "Argument required after %s flag.\n", arg); + } +} + +internal +Cmdline_Args parse_args(i32 argc, char **argv) +{ + Cmdline_Args args {}; + if (argc < 3) { + args.show_help_and_exit = true; + return args; + } + +// TODO: refactor this probably + for (i32 i = 1; i < argc; ++i) { + String8 arg = str8_from_c(argv[i]); + if (arg.str[0] == '-') { + if (arg.size == 2) { + if (arg.str[1] == 't') + args.print_to_terminal = true; + else if (arg.str[1] == 's') + parse_int_arg(i, argc, argv, args.start_addr); + else if (arg.str[1] == 'l') + parse_int_arg(i, argc, argv, args.nbytes_displayed); + else if (arg.str[1] == 'w') { + u64 n_cols = 0; + parse_int_arg(i, argc, argv, n_cols); + args.n_cols = (u16)n_cols; + } + else + args.show_help_and_exit = true; + } else { + args.show_help_and_exit = true; + } + } else if (args.ntpl_name.size) { + if (args.file_name.size) + args.show_help_and_exit = true; + else + args.file_name = arg; + } else { + args.ntpl_name = arg; + } + } + return args; +} + int main(int argc, char **argv) { Thread_Ctx tctx; tctx_init(tctx); defer { tctx_release(); }; - if ((argc > 1 && argv[1][0] == '-') || argc < 3) { - fprintf(stderr, "Usage: %s \n", argv[0]); + // Parse cmdline + Cmdline_Args args = parse_args(argc, argv); + if (args.show_help_and_exit || !args.ntpl_name.size || !args.file_name.size) { + print_help(argv[0]); return 1; } - // Collect arguments - const char *ntpl_name = argc > 1 ? argv[1] : nullptr; - const char *fname = argc > 2 ? argv[2] : nullptr; - // Allocate program memory Arena *arena = arena_alloc(); if (!arena) { @@ -115,19 +218,29 @@ int main(int argc, char **argv) defer { app_cleanup(app); }; // Open and map the file - if (fname) { - if (!os_open_and_map_file(fname, app)) + if (args.file_name.size) { + if (!os_open_and_map_file(args.file_name, app)) return 1; // Watch file for changes (to adapt the displayed file size - otherwise // we may try to access invalid memory when the file gets shrunk) - os_start_file_watch(fname, app); + os_start_file_watch(args.file_name, app); } - app.ntpl_name = str8(ntpl_name); + app.ntpl_name = args.ntpl_name; app.tfile_data = get_tfile_data(app.inspected_file); app.rndata = get_rntuple_data(arena, app.inspected_file, app.ntpl_name); - make_viewer(app); + make_viewer(app, args.n_cols); + app.viewer.base_display_addr = args.start_addr; + + if (args.print_to_terminal) { + u64 nbytes_displayed = args.nbytes_displayed; + if (!nbytes_displayed) + nbytes_displayed = 1000; + String8 rendered = render_range_to_string(arena, app, nbytes_displayed, args.n_cols); + printf("%s\n", rendered.c()); + return 0; + } // Start main loop run_main_loop(window, arena, app); diff --git a/src/str.cpp b/src/str.cpp index 3493bc5..589aeb3 100644 --- a/src/str.cpp +++ b/src/str.cpp @@ -20,3 +20,9 @@ String8 push_str8f(Arena *arena, const char *fmt, ...) va_end(args); return result; } + +String8 str8_from_c(const char *str) +{ + u64 size = strlen(str); + return String8 { (u8*)str, size }; +} diff --git a/src/str.h b/src/str.h index 8c88a4d..1ddb483 100644 --- a/src/str.h +++ b/src/str.h @@ -7,4 +7,5 @@ struct String8 { #define str8(s) String8 { (u8*)(s), sizeof(s) - 1 } +String8 str8_from_c(const char *str); String8 push_str8f(Arena *arena, char *fmt, ...);