diff --git a/src/app_state.h b/src/app_state.h index 150db72..5968e33 100644 --- a/src/app_state.h +++ b/src/app_state.h @@ -37,6 +37,9 @@ struct App_State { #endif String8 ntpl_name; + // This is used: + // - in interactive mode: for aux section management + // - in terminal mode: to know which is the first byte to display u64 base_display_addr; Delta_Time_Accum delta_time_accum; diff --git a/src/argparse.cpp b/src/argparse.cpp index f399733..6fc8263 100644 --- a/src/argparse.cpp +++ b/src/argparse.cpp @@ -12,7 +12,7 @@ void print_help(const char *argv0) "\n\t-l: display LEN bytes (only in terminal mode)" "\n\t-k: print information about the TKeys in the file and exit" "\n\t-n: list the names of the RNTuples found in the file and exit" - "\n\t-s: set first displayed byte to START" + "\n\t-s: set first displayed byte to START (only in terminal mode)" "\n\t-t: no graphics, output to terminal" "\n\t-v: print version and copyright info" "\n\t-w: display WIDTH bytes per column" diff --git a/src/mainloop.cpp b/src/mainloop.cpp index a44718f..0e6a22c 100644 --- a/src/mainloop.cpp +++ b/src/mainloop.cpp @@ -170,11 +170,18 @@ void run_main_loop(GLFWwindow *window, Arena *arena, App_State &app) init_viewer_title(app.viewer, app.fdata, app.inspected_file.name); } - if ((app.user_input.key_state[KEY_ESC] & KEY_STATE_IS_DOWN) || glfwWindowShouldClose(window)) { - app.should_quit = true; // superfluous right now, but set it just in case. - break; + if (app.user_input.key_state[KEY_ESC] & KEY_STATE_JUST_PRESSED) { + if (app.viewer.aux.buf_size) { + // We were viewing a subsection: back to the main view + app.viewer.aux.buf_size = app.base_display_addr = 0; + } else { + app.should_quit = true; + } } + if (glfwWindowShouldClose(window)) + app.should_quit = true; + b32 focused = glfwGetWindowAttrib(window, GLFW_FOCUSED); if (focused) { // NOTE: keyboard is updated via the callback because that's the only way to intercept GLFW_REPEAT events @@ -186,6 +193,7 @@ void run_main_loop(GLFWwindow *window, Arena *arena, App_State &app) u8 *mouse_btn_state = app.user_input.mouse_btn_state; monitor_mouse_btn(window, mouse_btn_state, GLFW_MOUSE_BUTTON_LEFT, MOUSE_BTN_LEFT); monitor_mouse_btn(window, mouse_btn_state, GLFW_MOUSE_BUTTON_RIGHT, MOUSE_BTN_RIGHT); + monitor_mouse_btn(window, mouse_btn_state, GLFW_MOUSE_BUTTON_MIDDLE, MOUSE_BTN_MIDDLE); app.user_input.mouse_pos.x = static_cast<i32>(mposx); app.user_input.mouse_pos.y = static_cast<i32>(mposy); diff --git a/src/render.cpp b/src/render.cpp index c01ab95..4e2552e 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -1,3 +1,7 @@ +// NOTE: we reserve more space than kMAXZIPBUF for the aux buf so we can fit also a TKey +// or other similar pre-section in the buffer. +#define AUX_BUF_SIZE (2 * kMAXZIPBUF) + constexpr f32 max3(f32 a, f32 b, f32 c) { return (a > b) ? (a > c) ? a : (b > c) ? b : c : (b > c) ? b : c; @@ -98,10 +102,10 @@ internal u32 mem_edit_bg_color_fn(const u8 *data, u64 off, void *user_data) { App_State *app = reinterpret_cast<App_State *>(user_data); - off += app->base_display_addr; + u64 abs_off = off + app->base_display_addr; f32 brighten = 0; - if (app->viewer.hovered_range.start <= off && off < app->viewer.hovered_range.end()) + if (app->viewer.hovered_range.start <= abs_off && abs_off < app->viewer.hovered_range.end()) brighten = 0.3; u32 col = highlight_zstd_header(data, off, *app, brighten); @@ -109,7 +113,12 @@ u32 mem_edit_bg_color_fn(const u8 *data, u64 off, void *user_data) return col; i64 hilite_cluster = app->viewer.highlight_cluster ? app->viewer.highlighted_cluster : -1; - Section section = find_section(*app, off, hilite_cluster); + Section section; + if (app->viewer.aux.buf_size && off > app->viewer.aux.section.pre_size) { + section = app->viewer.aux.section; + } else { + section = find_section(*app, abs_off, hilite_cluster); + } if (section.highlighted) return imcol(app->viewer.col_highlight, brighten); @@ -184,6 +193,9 @@ void init_viewer(App_State &app, u16 n_cols) viewer.mem_edit = make_memory_editor(app, (i32)n_cols); init_viewer_title(viewer, app.fdata, app.inspected_file.name); + // This lives as long as the viewer (i.e. forever) + viewer.aux.buf = (u8 *)malloc(AUX_BUF_SIZE); + #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); COL(col_page_start, 200, 0, 200); @@ -361,7 +373,12 @@ void update_and_render(Arena *arena, App_State &app, f32 delta_time_ms) { f32 *c = app.viewer.col_title; - ImGui::TextColored(ImColor(c[0], c[1], c[2]), "%s", app.viewer.title.c()); + String8 title = app.viewer.title; + if (app.viewer.aux.buf_size) + title = push_str8f(scratch.arena, "%s [viewing section \"%s\" at 0x%" PRIX64 "-0x%" PRIX64 "]", title.c(), + section_names[app.viewer.aux.section.id].c(), + app.base_display_addr, app.base_display_addr + app.viewer.aux.section_comp_size); + ImGui::TextColored(ImColor(c[0], c[1], c[2]), "%s", title.c()); } // Draw stats @@ -391,7 +408,7 @@ void update_and_render(Arena *arena, App_State &app, f32 delta_time_ms) ImGui::BeginTable("Hex View", 2, ImGuiTableFlags_Resizable); - u64 content_size = app.inspected_file.size - app.base_display_addr; + u64 content_size = app.viewer.aux.buf_size ? app.viewer.aux.buf_size : app.inspected_file.size - app.base_display_addr; MemoryEditor::Sizes sizes; app.viewer.mem_edit.CalcSizes(sizes, content_size, app.base_display_addr); @@ -406,16 +423,21 @@ void update_and_render(Arena *arena, App_State &app, f32 delta_time_ms) // 0 means "invalid", otherwise the actual offset is `hovered_off - 1`. u64 hovered_off = app.viewer.mem_edit.MouseHovered * (app.viewer.mem_edit.MouseHoveredAddr + 1); - if (LIKELY(app.inspected_file.size)) { - assert(app.base_display_addr < app.inspected_file.size); - void *content = app.inspected_file.mem + app.base_display_addr; - app.last_pinfo = &invalid_pinfo; - app.last_other_root_obj = &invalid_section; - app.viewer.mem_edit.DrawContents(content, content_size, app.base_display_addr); - } else { - ImGui::Text("(File is empty)"); + app.last_pinfo = &invalid_pinfo; + app.last_other_root_obj = &invalid_section; + + void *content = nullptr; + if (app.viewer.aux.buf_size) { + content = app.viewer.aux.buf; + } else if (LIKELY(app.inspected_file.size)) { + content = app.inspected_file.mem; } + if (LIKELY(content)) + app.viewer.mem_edit.DrawContents(content, content_size, 0); + else + ImGui::Text("(File is empty)"); + ImGui::TableNextColumn(); const ImGuiColorEditFlags edit_flags = ImGuiColorEditFlags_NoInputs|ImGuiColorEditFlags_NoLabel; @@ -535,21 +557,30 @@ void update_and_render(Arena *arena, App_State &app, f32 delta_time_ms) if (hovered_off) { - ImGui::TextColored(ImColor(0.f, 0.65f, 0.f), "Offset: 0x%" PRIX64, hovered_off - 1); - Section hovered_section = find_section(app, hovered_off - 1); - b8 hover_display_grouped = !(app.user_input.key_state[KEY_ALT] & KEY_STATE_IS_DOWN); + u64 abs_hovered_off = hovered_off - 1 + app.base_display_addr; + ImGui::TextColored(ImColor(0.f, 0.65f, 0.f), "Offset: 0x%" PRIX64, abs_hovered_off); + + Section hovered_section = app.viewer.aux.buf_size ? app.viewer.aux.section : find_section(app, hovered_off - 1); Section_Id sec_id = hovered_section.id; // NOTE: anchor/header/footer sections all have their *info point to their parent rntuple anchor info. b8 old_version = (sec_id == Sec_RNTuple_Anchor || sec_id == Sec_RNTuple_Header || sec_id == Sec_RNTuple_Footer) && rntuple_is_old_version(((const RNTuple_Anchor_Info *)hovered_section.info)->anchor); - Sec_Hover_Info hover_info = get_section_hover_info(scratch.arena, hovered_section, hovered_off - 1, - app.inspected_file.mem, hover_display_grouped, old_version); + b8 hover_display_grouped = !(app.user_input.key_state[KEY_ALT] & KEY_STATE_IS_DOWN); + const u8 *section_info_data = app.viewer.aux.buf_size ? app.viewer.aux.buf : app.inspected_file.mem; + Sec_Hover_Info hover_info = get_section_hover_info(scratch.arena, hovered_section, hovered_off - 1, + section_info_data, hover_display_grouped, old_version); + ImGui::TextColored(ImColor(0.5f, 0.5f, 0.5f), "(Hint: hold Alt for single-field hover information)"); ImGui::TextColored(ImColor(0.5f, 0.5f, 0.5f), "(Hint: Shift-Click to jump to the start of this section)"); + if (!app.viewer.aux.buf_size) + ImGui::TextColored(ImColor(0.5f, 0.5f, 0.5f), "(Hint: Middle-Click to focus/expand current section)"); + else + ImGui::TextColored(ImColor(0.5f, 0.5f, 0.5f), "(Hint: Escape to go back to full view)"); if (hover_info.desc) imgui_render_string_tree(scratch.arena, hover_info.desc->head, hover_info.highlighted_desc); app.viewer.hovered_range = hover_info.rng; + app.viewer.hovered_range.start += !!app.viewer.aux.buf_size * app.base_display_addr; // Shift-clicking on a page section will update the current page in the legend if ((app.user_input.key_state[KEY_SHIFT] & KEY_STATE_IS_DOWN) && @@ -562,6 +593,38 @@ void update_and_render(Arena *arena, App_State &app, f32 delta_time_ms) viewer_jump_to(app, hovered_section.range.start); } } + + // + // Handle section focusing/expanding. + // This feature isolates a single section and displays it uncompressed, allowing individual values hovering + // even for zipped payloads. + // + if (!app.viewer.aux.buf_size && (app.user_input.mouse_btn_state[MOUSE_BTN_MIDDLE] & MOUSE_BTN_STATE_JUST_PRESSED)) { + // Check if this section is compressed; decompress it if it's the case. + u8 *sec_buf = app.inspected_file.mem + hovered_section.range.start - hovered_section.pre_size; + auto comp = R__getCompressionAlgorithm(sec_buf + hovered_section.pre_size, hovered_section.range.len); + app.viewer.aux.section = hovered_section; + app.base_display_addr = hovered_section.range.start - hovered_section.pre_size; + app.viewer.aux.section.range.start = hovered_section.pre_size; + app.viewer.aux.section_comp_size = hovered_section.range.len + hovered_section.pre_size; + if (comp == ROOT::RCompressionSetting::EAlgorithm::kUndefined) { + // Not a compressed block: just copy it to the aux buffer. + app.viewer.aux.buf_size = hovered_section.range.len + hovered_section.pre_size; + memcpy(app.viewer.aux.buf, sec_buf, min(AUX_BUF_SIZE, app.viewer.aux.buf_size)); + } else { + // Copy over the pre-section (usually the TKey) + memcpy(app.viewer.aux.buf, sec_buf, hovered_section.pre_size); + + // Decompress the block + i32 src_size = hovered_section.range.len; + i32 tgt_size = kMAXZIPBUF; + i32 unzipped_nbytes; + R__unzip(&src_size, sec_buf + hovered_section.pre_size, &tgt_size, + app.viewer.aux.buf + hovered_section.pre_size, &unzipped_nbytes); + app.viewer.aux.buf_size = hovered_section.pre_size + unzipped_nbytes; + app.viewer.aux.section.range.len = unzipped_nbytes; + } + } } else { app.viewer.hovered_range = {}; } diff --git a/src/render.h b/src/render.h index a1d05b9..26a2e3c 100644 --- a/src/render.h +++ b/src/render.h @@ -1,3 +1,13 @@ +// Contains the data of the currently focused/expanded section (middle-click on the section) +struct Viewer_Aux_Section { + // This is an allocation AUX_BUF_SIZE wide. + u8 *buf; + // The aux section (i.e. all the data inside this struct) is considered valid if and only if this is > 0 + u64 buf_size; + Section section; + u64 section_comp_size; +}; + struct Viewer { // Imgui colors f32 col_section[Sec_COUNT][3]; @@ -27,6 +37,8 @@ struct Viewer { // maps glfw keys to our keys Input_Key glfw_key_mapping[GLFW_KEY_LAST]; + + Viewer_Aux_Section aux; #endif }; diff --git a/src/rntviewer.cpp b/src/rntviewer.cpp index ebed1e3..2d711d3 100644 --- a/src/rntviewer.cpp +++ b/src/rntviewer.cpp @@ -158,7 +158,8 @@ int main(int argc, char **argv) } app.ntpl_name = args.ntpl_name; // may be null - app.base_display_addr = args.start_addr; + if (!is_interactive) + app.base_display_addr = args.start_addr; u32 walk_tkeys_flags = WTK_NONE; if (args.print_keys_info) walk_tkeys_flags |= WTK_PRINT_KEYS_INFO; diff --git a/src/window.h b/src/window.h index 147fbe7..6799d07 100644 --- a/src/window.h +++ b/src/window.h @@ -25,6 +25,7 @@ enum Key_State : u8 { enum Mouse_Button { MOUSE_BTN_LEFT, MOUSE_BTN_RIGHT, + MOUSE_BTN_MIDDLE, MOUSE_BTN_COUNT };