From be2c37ac8c8e317eb7e05829ff2078c1b3bbce4e Mon Sep 17 00:00:00 2001 From: Paul Oliver Date: Tue, 12 May 2026 22:52:37 +0200 Subject: Reimplement client with ImGui and ImPlot (scaffold) --- core/logger.c | 24 +-- data/client.c | 522 ------------------------------------------------------- data/client.cpp | 528 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ data/plots.c | 35 ---- data/server.c | 1 - salis.py | 29 ++-- ui/curses/ui.c | 10 +- 7 files changed, 562 insertions(+), 587 deletions(-) delete mode 100644 data/client.c create mode 100644 data/client.cpp delete mode 100644 data/plots.c diff --git a/core/logger.c b/core/logger.c index 880c231..efff3dc 100644 --- a/core/logger.c +++ b/core/logger.c @@ -9,8 +9,8 @@ #define LOG_LINE_SIZE 1024 enum LogLevel { - LOG_INFO, - LOG_WARN, + INFO, + WARN, }; char g_log_buff[LOG_LINE_SIZE]; @@ -18,7 +18,7 @@ char g_log_buff[LOG_LINE_SIZE]; void log_msg_to_buff(char *out, int size, enum LogLevel level, bool colored, const char *format, va_list args) { assert(out); assert(size); - assert(level == LOG_INFO || level == LOG_WARN); + assert(level == INFO || level == WARN); assert(format); struct timespec ts; @@ -26,26 +26,26 @@ void log_msg_to_buff(char *out, int size, enum LogLevel level, bool colored, con long msec = ts.tv_nsec / 1000000; struct tm tm = *localtime(&ts.tv_sec); pid_t pid = getpid(); - char *level_str = NULL; + const char *level_str = NULL; switch (level) { - case LOG_INFO: + case INFO: level_str = "INFO"; break; - case LOG_WARN: + case WARN: level_str = "WARN"; break; default: assert(false); } - char *color_code = NULL; + const char *color_code = NULL; if (colored) { switch (level) { - case LOG_INFO: + case INFO: color_code = "\033[1;32m"; break; - case LOG_WARN: + case WARN: color_code = "\033[1;33m"; break; default: @@ -75,7 +75,7 @@ void log_msg_to_buff(char *out, int size, enum LogLevel level, bool colored, con } void log_msg(enum LogLevel level, bool colored, const char *format, va_list args) { - assert(level == LOG_INFO || level == LOG_WARN); + assert(level == INFO || level == WARN); assert(format); log_msg_to_buff(g_log_buff, LOG_LINE_SIZE, level, colored, format, args); @@ -87,7 +87,7 @@ void log_info(const char *format, ...) { va_list args; va_start(args, format); - log_msg(LOG_INFO, true, format, args); + log_msg(INFO, true, format, args); va_end(args); } @@ -96,6 +96,6 @@ void log_warn(const char *format, ...) { va_list args; va_start(args, format); - log_msg(LOG_WARN, true, format, args); + log_msg(WARN, true, format, args); va_end(args); } diff --git a/data/client.c b/data/client.c deleted file mode 100644 index d5e8e56..0000000 --- a/data/client.c +++ /dev/null @@ -1,522 +0,0 @@ -#include -#include -#include -#include - -#include "logger.c" -#include "plots.c" -#include "tui.c" - -#define UI_AVAIL_PLOTS_COL PANE_AND_MARGIN_WIDTH -#define UI_WINDOWS_COL (PANE_AND_MARGIN_WIDTH * 2) -#define UI_PLOTS_COL (PANE_AND_MARGIN_WIDTH * 3) - -#define MAX_WINDOW_ROWS 4 -#define MAX_WINDOW_COLS 4 -#define MAX_WINDOW_PLOTS (MAX_WINDOW_ROWS * MAX_WINDOW_COLS) - -#define CTRL(x) (x & 0x1f) - -enum { - PAIR_HEADER = 1, - PAIR_SELECTED = 2, - PAIR_TO_BE_CREATED = 3, - PAIR_TO_BE_UPDATED = 4, - PAIR_TO_BE_REMOVED = 5, -}; - -enum UIColumn { - UICOL_AVAIL_PLOTS, - UICOL_WINDOWS, - UICOL_PLOTS, - UICOL_COUNT, -}; - -enum WindowState { - WINDOW_TO_BE_CREATED, - WINDOW_TO_BE_UPDATED, - WINDOW_IS_LIVE, - WINDOW_TO_BE_REMOVED, -}; - -enum WindowPlotState { - PLOT_TO_BE_CREATED, - PLOT_IS_LIVE, - PLOT_TO_BE_REMOVED, -}; - -struct WindowHandle { - size_t wid; - size_t rows; - size_t cols; - size_t rows_update; - size_t cols_update; - enum WindowState state; - size_t plot_count; - size_t plot_sel; - enum WindowPlotState plot_states[MAX_WINDOW_PLOTS]; - struct PlotDef *plot_defs[MAX_WINDOW_PLOTS]; -}; - -// Globals -bool g_exit; -enum UIColumn g_col_sel; -size_t g_apsel; -size_t g_wsel; -size_t g_apscroll; -size_t g_wscroll; -size_t g_pscroll; -size_t g_wid_count; -struct WindowHandle *g_window_handles; -size_t g_window_count; -size_t g_window_cap; - -// ---------------------------------------------------------------------------- -// UI functions -// ---------------------------------------------------------------------------- -void ui_print_sim_description(void) { - // Simulation desciption - int l = 1; - - tui_line(false, l++, PAIR_HEADER, A_BOLD, "SALIS DATA CLIENT"); - tui_str_field(l++, "name", NAME); - tui_ulx_field(l++, "seed", SEED); - tui_str_field(l++, "conn", IP ":" PORT_STR); - tui_str_field(l++, "anc", ANC); - tui_str_field(l++, "arch", ARCH); - tui_ulx_field(l++, "asav", AUTOSAVE_INTERVAL); - tui_ulx_field(l++, "cres", CORES); -#if defined(MUTA_FLIP) - tui_str_field(l++, "mflp", "true"); -#else - tui_str_field(l++, "mflp", "false"); -#endif - tui_ulx_field(l++, "mrng", MUTA_RANGE); - tui_ulx_field(l++, "size", MVEC_SIZE); -#if defined(MVEC_LOOP) - tui_str_field(l++, "loop", "true"); -#else - tui_str_field(l++, "loop", "false"); -#endif -#if defined(COMPRESS) - tui_str_field(l++, "xsav", "enabled"); -#else - tui_str_field(l++, "xsav", "disabled"); -#endif - tui_ulx_field(l++, "dpsi", DATA_PUSH_INTERVAL); - - // Window summary - l++; - - tui_line(false, l++, PAIR_HEADER, A_BOLD, "SUMMARY"); - tui_uld_field(l++, "wcnt", g_window_count); - tui_uld_field(l++, "wcap", g_window_cap); - tui_uld_field(l++, "widc", g_wid_count); -} - -void ui_print_avail_plots(void) { - int l = 1; - int pair = g_col_sel == UICOL_AVAIL_PLOTS ? PAIR_SELECTED : PAIR_HEADER; - - tui_field(l++, UI_AVAIL_PLOTS_COL, pair, A_BOLD, "AVAIL PLOTS [%ld:%ld]", g_apsel, g_apscroll); - - for (size_t i = g_apscroll; i < g_general_plots_count; i++) { - pair = i == g_apsel ? PAIR_SELECTED : PAIR_NORMAL; - tui_field(l++, UI_AVAIL_PLOTS_COL, pair, A_NORMAL, g_general_plots_def[i].name); - } -} - -void ui_print_windows(void) { - int l = 1; - int pair = g_col_sel == UICOL_WINDOWS ? PAIR_SELECTED : PAIR_HEADER; - - struct WindowHandle *whdl = &g_window_handles[g_wsel]; - tui_field(l++, UI_WINDOWS_COL, pair, A_BOLD, "WINDOWS [%ld:%ld]", whdl->wid, g_wscroll); - - for (size_t i = g_wscroll; i < g_window_count; i++) { - whdl = &g_window_handles[i]; - char mark = ' '; - - switch (whdl->state) { - case WINDOW_TO_BE_CREATED: - pair = PAIR_TO_BE_CREATED; - mark = '+'; - break; - case WINDOW_TO_BE_UPDATED: - pair = PAIR_TO_BE_UPDATED; - mark = 'u'; - break; - case WINDOW_IS_LIVE: - pair = PAIR_NORMAL; - break; - case WINDOW_TO_BE_REMOVED: - pair = PAIR_TO_BE_REMOVED; - mark = 'd'; - break; - } - - pair = i == g_wsel ? PAIR_SELECTED : pair; - tui_field(l++, UI_WINDOWS_COL, pair, A_NORMAL, "wid:%ld [%dx%d] %c", whdl->wid, whdl->rows_update, whdl->cols_update, mark); - } -} - -void ui_print_plots(void) { - int l = 1; - int pair = g_col_sel == UICOL_PLOTS ? PAIR_SELECTED : PAIR_HEADER; - - struct WindowHandle *whdl = &g_window_handles[g_wsel]; - tui_field(l++, UI_PLOTS_COL, pair, A_BOLD, "PLOTS [%ld:%ld]", whdl->plot_sel, g_pscroll); - - if (!g_window_count) return; - - for (size_t i = g_pscroll; i < whdl->plot_count; i++) { - struct PlotDef *plot_def = whdl->plot_defs[i]; - enum WindowPlotState plot_state = whdl->plot_states[i]; - char mark = ' '; - - switch (plot_state) { - case PLOT_TO_BE_CREATED: - pair = PAIR_TO_BE_CREATED; - mark = '+'; - break; - case PLOT_IS_LIVE: - pair = PAIR_NORMAL; - break; - case PLOT_TO_BE_REMOVED: - pair = PAIR_TO_BE_REMOVED; - mark = 'd'; - break; - } - - pair = i == whdl->plot_sel ? PAIR_SELECTED : pair; - tui_field(l++, UI_PLOTS_COL, pair, A_NORMAL, "%s %c", plot_def->name, mark); - plot_def++; - } -} - -void ui_print_footer(void) { - tui_field(LINES - 1, 1, PAIR_NORMAL, A_NORMAL, "[+] new window | [enter] add plot | [x] delete elem | [ctrl+c] quit"); -} - -void ui_print(void) { - ui_print_sim_description(); - ui_print_avail_plots(); - ui_print_windows(); - ui_print_plots(); - ui_print_footer(); -} - -// ---------------------------------------------------------------------------- -// Control function -// ---------------------------------------------------------------------------- -void ev_scroll(int ev) { - size_t *scroll_var = NULL; - - switch (g_col_sel) { - case UICOL_AVAIL_PLOTS: - scroll_var = &g_apscroll; - break; - case UICOL_WINDOWS: - scroll_var = &g_wscroll; - break; - case UICOL_PLOTS: - scroll_var = &g_pscroll; - break; - default:; - } - - assert(scroll_var); - - switch (ev) { - case 'w': - *scroll_var += 1; - break; - case 's': - *scroll_var -= (*scroll_var ? 1 : 0); - break; - case 'q': - *scroll_var = 0; - break; - } -} - -void ev_new_window(void) { - if (g_window_count == g_window_cap) { - // Reallocate dynamic array - size_t new_window_cap = g_window_cap * 2; - struct WindowHandle *new_window_handles = calloc(new_window_cap, sizeof(struct WindowHandle)); - memcpy(new_window_handles, g_window_handles, sizeof(struct WindowHandle) * g_window_count); - free(g_window_handles); - g_window_cap = new_window_cap; - g_window_handles = new_window_handles; - } - - g_window_count++; - g_window_handles[g_window_count - 1] = (struct WindowHandle){ - .wid = g_wid_count++, - .rows = 2, - .cols = 2, - .rows_update = 2, - .cols_update = 2, - .state = WINDOW_TO_BE_CREATED, - }; -} - -void ev_delete_window_handle(size_t widx) { - assert(g_window_count); - assert(widx < g_window_count); - assert(g_window_handles[widx].state == WINDOW_TO_BE_CREATED); - - for (size_t i = widx; i < g_window_count - 1; i++) { - memcpy(&g_window_handles[i], &g_window_handles[i + 1], sizeof(struct WindowHandle)); - } - - g_window_handles[g_window_count - 1] = (struct WindowHandle){ 0 }; - g_window_count--; - g_wsel -= (g_window_count && g_wsel == g_window_count) ? 1 : 0; -} - -void ev_add_plot(void) { - assert(g_col_sel == UICOL_AVAIL_PLOTS); - assert(g_window_count); - - struct WindowHandle *whdl = &g_window_handles[g_wsel]; - size_t max_plots = whdl->rows_update * whdl->cols_update; - size_t pidx = 0; - - do { - if (!whdl->plot_defs[pidx]) break; - pidx++; - } while (pidx < max_plots); - - if (pidx == max_plots) return; - - whdl->plot_count++; - whdl->plot_states[pidx] = PLOT_TO_BE_CREATED; - whdl->plot_defs[pidx] = &g_general_plots_def[g_apsel]; -} - -void ev_delete_plot(size_t widx) { - assert(g_window_count); - assert(widx < g_window_count); - - struct WindowHandle *whdl = &g_window_handles[g_wsel]; - assert(whdl->plot_count); - assert(whdl->plot_sel < whdl->plot_count); - assert(whdl->plot_states[whdl->plot_sel] == PLOT_TO_BE_CREATED); - - for (size_t i = whdl->plot_sel; i < whdl->plot_count - 1; i++) { - whdl->plot_states[i] = whdl->plot_states[i + 1]; - whdl->plot_defs[i] = whdl->plot_defs[i + 1]; - } - - whdl->plot_states[whdl->plot_count - 1] = 0; - whdl->plot_defs[whdl->plot_count - 1] = NULL; - whdl->plot_count--; - - if (!whdl->plot_count) { - whdl->plot_sel = 0; - } else if (whdl->plot_sel >= whdl->plot_count) { - whdl->plot_sel = whdl->plot_count - 1; - } -} - -void ev_delete_elem(void) { - if (g_col_sel == UICOL_WINDOWS && g_window_count) { - struct WindowHandle *whdl = &g_window_handles[g_wsel]; - - switch (whdl->state) { - case WINDOW_TO_BE_CREATED: - ev_delete_window_handle(g_wsel); - break; - case WINDOW_TO_BE_UPDATED: - break; - case WINDOW_IS_LIVE: - break; - case WINDOW_TO_BE_REMOVED: - break; - } - } - - if (g_col_sel == UICOL_PLOTS && g_window_count && g_window_handles[g_wsel].plot_count) { - struct WindowHandle *whdl = &g_window_handles[g_wsel]; - - switch (whdl->plot_states[whdl->plot_sel]) { - case PLOT_TO_BE_CREATED: - ev_delete_plot(g_wsel); - break; - case PLOT_IS_LIVE: - break; - case PLOT_TO_BE_REMOVED: - break; - } - } -} - -void ev_resize_window(int ev) { - assert(g_col_sel == UICOL_WINDOWS); - assert(g_window_count); - - struct WindowHandle *whdl = &g_window_handles[g_wsel]; - - switch (ev) { - case 'A': - if (whdl->cols_update > 1 && (whdl->rows_update * (whdl->cols_update - 1)) >= whdl->plot_count) whdl->cols_update--; - break; - case 'D': - whdl->cols_update += whdl->cols_update < MAX_WINDOW_COLS ? 1 : 0; - break; - case 'W': - if (whdl->rows_update > 1 && (whdl->cols_update * (whdl->rows_update - 1)) >= whdl->plot_count) whdl->rows_update--; - break; - case 'S': - whdl->rows_update += whdl->rows_update < MAX_WINDOW_ROWS ? 1 : 0; - break; - } -} - -void ev_swap_plots(int ev) { - assert(g_col_sel == UICOL_PLOTS); - assert(g_window_count); - - struct WindowHandle *whdl = &g_window_handles[g_wsel]; - size_t pidx1; - size_t pidx2; - - switch (ev) { - case 'W': - if (!whdl->plot_count || !whdl->plot_sel) return; - pidx1 = whdl->plot_sel; - pidx2 = whdl->plot_sel - 1; - whdl->plot_sel--; - break; - case 'S': - if (!whdl->plot_count || whdl->plot_sel >= whdl->plot_count - 1) return; - pidx1 = whdl->plot_sel; - pidx2 = whdl->plot_sel + 1; - whdl->plot_sel++; - break; - } - - enum WindowPlotState tmp_state = whdl->plot_states[pidx1]; - whdl->plot_states[pidx1] = whdl->plot_states[pidx2]; - whdl->plot_states[pidx2] = tmp_state; - - struct PlotDef *tmp_def = whdl->plot_defs[pidx1]; - whdl->plot_defs[pidx1] = whdl->plot_defs[pidx2]; - whdl->plot_defs[pidx2] = tmp_def; -} - -void ev_handle(void) { - int ev = getch(); - - switch (ev) { - case CTRL('c'): - g_exit = true; - break; - case KEY_RESIZE: - tui_line_buff_resize(); - break; - case 'w': - case 's': - case 'q': - ev_scroll(ev); - break; - case 'Q': - g_apscroll = 0; - g_wscroll = 0; - g_pscroll = 0; - break; - case KEY_LEFT: - g_col_sel -= g_col_sel ? 1 : 0; - break; - case KEY_RIGHT: - g_col_sel += g_col_sel < UICOL_COUNT - 1 ? 1 : 0; - break; - case KEY_UP: - if (g_col_sel == UICOL_AVAIL_PLOTS && g_apsel) g_apsel--; - if (g_col_sel == UICOL_WINDOWS && g_wsel) g_wsel--; - if (g_col_sel == UICOL_PLOTS && g_window_count && g_window_handles[g_wsel].plot_sel) g_window_handles[g_wsel].plot_sel--; - break; - case KEY_DOWN: - if (g_col_sel == UICOL_AVAIL_PLOTS && g_apsel < g_general_plots_count - 1) g_apsel++; - if (g_col_sel == UICOL_WINDOWS && g_window_count && g_wsel < g_window_count - 1) g_wsel++; - if (g_col_sel == UICOL_PLOTS && g_window_count && g_window_handles[g_wsel].plot_count && g_window_handles[g_wsel].plot_sel < g_window_handles[g_wsel].plot_count - 1) g_window_handles[g_wsel].plot_sel++; - break; - case '+': - ev_new_window(); - break; - case '\n': - if (g_col_sel == UICOL_AVAIL_PLOTS && g_window_count) ev_add_plot(); - break; - case 'x': - ev_delete_elem(); - break; - case 'A': - case 'D': - case 'W': - case 'S': - if (g_col_sel == UICOL_WINDOWS && g_window_count) ev_resize_window(ev); - if (g_col_sel == UICOL_PLOTS && g_window_count) ev_swap_plots(ev); - break; - default: - break; - } -} - -// ---------------------------------------------------------------------------- -// Main functions -// ---------------------------------------------------------------------------- -void init(void) { - log_info("Initializing salis data client"); - - setlocale(LC_ALL, ""); - - initscr(); - raw(); - noecho(); - curs_set(0); - keypad(stdscr, TRUE); - - start_color(); - init_color(COLOR_BLACK, 0, 0, 0); - - init_pair(PAIR_NORMAL, COLOR_WHITE, COLOR_BLACK); - init_pair(PAIR_HEADER, COLOR_BLUE, COLOR_BLACK); - init_pair(PAIR_SELECTED, COLOR_YELLOW, COLOR_BLACK); - init_pair(PAIR_TO_BE_CREATED, COLOR_GREEN, COLOR_BLACK); - init_pair(PAIR_TO_BE_UPDATED, COLOR_GREEN, COLOR_BLACK); - init_pair(PAIR_TO_BE_REMOVED, COLOR_RED, COLOR_BLACK); - - tui_line_buff_resize(); - - g_window_count = 0; - g_window_cap = 1; - g_window_handles = calloc(g_window_cap, sizeof(struct WindowHandle)); -} - -void exec(void) { - while (!g_exit) { - ui_print(); - ev_handle(); - clear(); - } -} - -void quit(void) { - tui_line_buff_free(); - endwin(); - log_info("Shutting down salis data client"); -} - -int main(void) { - init(); - exec(); - quit(); - - free(g_window_handles); - g_window_count = 0; - g_window_cap = 0; - - return 0; -} diff --git a/data/client.cpp b/data/client.cpp new file mode 100644 index 0000000..74f2bc5 --- /dev/null +++ b/data/client.cpp @@ -0,0 +1,528 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "logger.c" + +#define COLOR_BG ImVec4(0.f, 0.f, 0.f, 1.f) +#define FONT_SIZE 12.f +#define FONT_SOURCE "/usr/share/fonts/droid/DroidSansMono.ttf" +#define GLSL_VERSION "#version 130" +#define WINDOW_STYLE (ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings) + +#define DEFAULT_ENTRIES 2000 +#define DEFAULT_NTH 1 +#define DEFAULT_X_AXIS 0 +#define DEFAULT_X_LOW 0 +#define DEFAULT_X_HIGH INT_MAX +#define DEFAULT_HM_LEFT 0 +#define DEFAULT_HM_PIXEL_COUNT 0x400 + +#define PLOT_MIN_COLS 1 +#define PLOT_MAX_COLS 8 + +enum Status { + STATUS_STOPPED, + STATUS_RUNNING, + STATUS_STOPPING, +}; + +struct Plot { + Plot(const char *name, const char *section, std::initializer_list cols) + : name(name), section(section), cols(cols.begin()) {} + + const char *name; + const char *section; + const char *const *cols; +}; + +// Globals +GLFWwindow *g_window; +ImGuiIO *g_io; +ImGuiStyle *g_imgui_style; +ImPlotStyle *g_implot_style; +int g_status; + +// Data col +bool g_data_col_visible = true; +float g_data_col_width; + +const char *g_x_axes[] = { + "rowid", + "steps", +#define FOR_CORE(i) "cycl_" #i, + FOR_CORES +#undef FOR_CORE +}; + +int g_entries = DEFAULT_ENTRIES; +int g_nth = DEFAULT_NTH; +int g_x_axis = DEFAULT_X_AXIS; +int g_x_low = DEFAULT_X_LOW; +int g_x_high = DEFAULT_X_HIGH; +int g_hm_left = DEFAULT_HM_LEFT; +int g_hm_pixel_count = DEFAULT_HM_PIXEL_COUNT; +int g_hm_pixel_pow; // calculate on startup +bool g_data_touched; + +// Plots +Plot g_plots[] = { + Plot("cycl", "general", { +#define FOR_CORE(i) "cycl_" #i, + FOR_CORES +#undef FOR_CORE + nullptr, + }), + Plot("mall", "general", { +#define FOR_CORE(i) "mall_" #i, + FOR_CORES +#undef FOR_CORE + nullptr, + }), + Plot("pnum", "general", { +#define FOR_CORE(i) "pnum_" #i, + FOR_CORES +#undef FOR_CORE + nullptr, + }), + Plot("ppop", "general", { +#define FOR_CORE(i) "pfst_" #i, "plst_" #i, + FOR_CORES +#undef FOR_CORE + nullptr, + }), + Plot("ambs", "general", { +#define FOR_CORE(i) "amb0_" #i, "amb1_" #i, + FOR_CORES +#undef FOR_CORE + nullptr, + }), + Plot("eevs", "general", { +#define FOR_CORE(i) "emb0_" #i, "emb1_" #i, "eliv_" #i, "edea_" #i, + FOR_CORES +#undef FOR_CORE + nullptr, + }), +}; + +#define PLOT_COUNT (int)(sizeof(g_plots) / sizeof(g_plots[0])) + +// Layout +int g_plot_cols = 2; +int g_plot_col_selected; +int g_plot_row_selected; +Plot *g_plot_cells[PLOT_MAX_COLS][PLOT_COUNT]; +Plot *g_plot_selected = &g_plots[0]; +bool g_plot_maximized; + +// ---------------------------------------------------------------------------- +// Data functions +// ---------------------------------------------------------------------------- +int data_calc_max_hm_pixel_pow(void) { + return (int)floor(log2((float)(MVEC_SIZE - g_hm_left) / (float)g_hm_pixel_count)); +} + +void data_clamp(int *field, int low, int high) { + assert(field); + if (*field < low) *field = low; + if (*field > high) *field = high; +} + +void data_validate(void) { + data_clamp(&g_entries, 1, DEFAULT_ENTRIES); + data_clamp(&g_nth, DEFAULT_NTH, INT_MAX); + data_clamp(&g_x_low, DEFAULT_X_LOW, INT_MAX); + data_clamp(&g_x_high, g_x_low + 1, DEFAULT_X_HIGH); + #if !defined(MVEC_LOOP) + data_clamp(&g_hm_left, DEFAULT_HM_LEFT, MVEC_SIZE); + #endif + data_clamp(&g_hm_pixel_count, 1, DEFAULT_HM_PIXEL_COUNT); + data_clamp(&g_hm_pixel_pow, 0, data_calc_max_hm_pixel_pow()); + g_data_touched = false; +} + +void data_reset_values(void) { + g_entries = DEFAULT_ENTRIES; + g_nth = DEFAULT_NTH; + g_x_axis = DEFAULT_X_AXIS; + g_x_low = DEFAULT_X_LOW; + g_x_high = DEFAULT_X_HIGH; + g_hm_left = DEFAULT_HM_LEFT; + g_hm_pixel_count = DEFAULT_HM_PIXEL_COUNT; + g_hm_pixel_pow = data_calc_max_hm_pixel_pow(); +} + +void data_reset_plot_cells(void) { + for (size_t i = 0; i < PLOT_MAX_COLS; i++) { + for (size_t j = 0; j < PLOT_COUNT; j++) { + g_plot_cells[i][j] = nullptr; + } + } +} + +void data_start_fetching(void) { + assert(g_status == STATUS_RUNNING); + log_info("Starting data fetching thread"); + // start data fetching thread +} + +void data_stop_fetching(void) { + assert(g_status == STATUS_STOPPING); + log_info("Stopping data fetching thread"); + // join data fetching thread (set STATUS_STOPPED from within thread) + g_status = STATUS_STOPPED; +} + +// ---------------------------------------------------------------------------- +// GUI functions +// ---------------------------------------------------------------------------- +void gui_print_data_col(void) { + const ImGuiViewport *viewport = ImGui::GetMainViewport(); + const ImVec2 next_win_pos = viewport->Pos; + const ImVec2 next_win_size = ImVec2(-1.f, viewport->Size.y); + + ImGui::SetNextWindowPos(next_win_pos); + ImGui::SetNextWindowSize(next_win_size); + ImGui::Begin("data-col", nullptr, WINDOW_STYLE); + g_data_col_width = ImGui::GetWindowWidth(); + + ImGui::SeparatorText("SALIS data client"); + ImGui::LabelText("name", NAME); + ImGui::LabelText("seed", "%#lx", SEED); + ImGui::LabelText("server", IP ":" PORT_STR); + ImGui::LabelText("arch", ARCH); + ImGui::LabelText("cores", "%d", CORES); + ImGui::LabelText("mvec-size", "%#lx", MVEC_SIZE); + #if defined(MVEC_LOOP) + ImGui::LabelText("mvec-loop", "true"); + #else + ImGui::LabelText("mvec-loop", "false"); + #endif + ImGui::LabelText("data-push", "%#lx", DATA_PUSH_INTERVAL); + + switch (g_status) { + case STATUS_STOPPED: + ImGui::LabelText("status", "%s", "stopped"); + break; + case STATUS_RUNNING: + ImGui::LabelText("status", "%s", "running"); + break; + case STATUS_STOPPING: + ImGui::LabelText("status", "%s", "stopping"); + break; + } + + ImGui::SeparatorText("Data fields"); + + switch (g_status) { + case STATUS_STOPPED: + if (ImGui::InputInt("entries", &g_entries, 0, 0, ImGuiInputTextFlags_CharsDecimal)) g_data_touched = true; + if (ImGui::InputInt("nth", &g_nth, 0, 0, ImGuiInputTextFlags_CharsDecimal)) g_data_touched = true; + + if (ImGui::BeginCombo("x-axis", g_x_axes[g_x_axis])) { + for (int i = 0; i < CORES + 2; i++) { + if (ImGui::Selectable(g_x_axes[i], g_x_axis == i)) { + g_x_axis = i; + g_data_touched = true; + } + } + + ImGui::EndCombo(); + } + + if (ImGui::InputInt("x-low", &g_x_low, 0, 0, ImGuiInputTextFlags_CharsDecimal)) g_data_touched = true; + if (ImGui::InputInt("x-high", &g_x_high, 0, 0, ImGuiInputTextFlags_CharsDecimal)) g_data_touched = true; + if (ImGui::InputInt("hm-left", &g_hm_left, 0, 0, ImGuiInputTextFlags_CharsDecimal)) g_data_touched = true; + if (ImGui::InputInt("hm-pxl-count", &g_hm_pixel_count, 0, 0, ImGuiInputTextFlags_CharsDecimal)) g_data_touched = true; + if (ImGui::InputInt("hm-pxl-pow", &g_hm_pixel_pow, 0, 0, ImGuiInputTextFlags_CharsDecimal)) g_data_touched = true; + + if (ImGui::Button("Run", ImVec2(-1.f, 0.f))) { + g_status = STATUS_RUNNING; + data_start_fetching(); + } + + if (ImGui::Button("Reset", ImVec2(-1.f, 0.f))) data_reset_values(); + + break; + case STATUS_RUNNING: + case STATUS_STOPPING: + ImGui::LabelText("entries", "%d", g_entries); + ImGui::LabelText("nth", "%d", g_nth); + ImGui::LabelText("x-axis", "%s", g_x_axes[g_x_axis]); + ImGui::LabelText("x-low", "%d", g_x_low); + ImGui::LabelText("x-high", "%d", g_x_high); + ImGui::LabelText("hm-left", "%d", g_hm_left); + ImGui::LabelText("hm-pxl-count", "%d", g_hm_pixel_count); + ImGui::LabelText("hm-pxl-pow", "%d", g_hm_pixel_pow); + + if (g_status == STATUS_RUNNING) { + if (ImGui::Button("Stop", ImVec2(-1.f, 0.f))) { + g_status = STATUS_STOPPING; + data_stop_fetching(); + } + } + } + + ImGui::SeparatorText("Layout"); + ImGui::SliderInt("cols", &g_plot_cols, PLOT_MIN_COLS, PLOT_MAX_COLS); + + ImGui::End(); +} + +void gui_print_plots(void) { + const char *section_current = g_plots[0].section; + const char *section_next = nullptr; + bool plots_covered[PLOT_COUNT] = { 0 }; + + const ImGuiViewport *viewport = ImGui::GetMainViewport(); + const ImVec2 next_win_pos = g_data_col_visible ? ImVec2(g_data_col_width, viewport->Pos.y) : viewport->Pos; + const ImVec2 next_win_size = g_data_col_visible ? ImVec2(viewport->Size.x - g_data_col_width, -1.f) : ImVec2(viewport->Size.x, -1.f); + + ImGui::SetNextWindowPos(next_win_pos); + ImGui::SetNextWindowSize(next_win_size); + ImGui::Begin("plots", nullptr, WINDOW_STYLE); + + int col = 0; + int row = 0; + + while (section_current) { + ImGui::SeparatorText(section_current); + ImGui::BeginTable("plots-table", g_plot_cols); + + for (int i = 0; i < PLOT_COUNT; i++) { + if (g_plots[i].section != section_current) { + section_next = (!section_next && !plots_covered[i]) ? g_plots[i].section : section_next; + continue; + } + + ImGui::TableNextColumn(); + + if (&g_plots[i] == g_plot_selected) { + g_plot_col_selected = col; + g_plot_row_selected = row; + g_implot_style->Colors[ImPlotCol_FrameBg] = g_imgui_style->Colors[ImGuiCol_FrameBg]; + } + + if (ImPlot::BeginPlot(g_plots[i].name)) { + int test_x[] = {0,1,2,3}; + int test_y1[] = {1,2,3,4}; + int test_y2[] = {2,4,8,16}; + ImPlot::PlotLine("test1", test_x, test_y1, 4); + ImPlot::PlotLine("test2", test_x, test_y2, 4); + ImPlot::EndPlot(); + } + + g_implot_style->Colors[ImPlotCol_FrameBg] = COLOR_BG; + + g_plot_cells[col][row] = &g_plots[i]; + col = (col + 1) % g_plot_cols; + row += col ? 0 : 1; + + plots_covered[i] = true; + } + + section_current = section_next; + section_next = nullptr; + ImGui::EndTable(); + + row += col ? 1 : 0; + col = 0; + } + + ImGui::End(); +} + +void gui_print_plot_maximized(void) { + const ImGuiViewport *viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->Pos); + ImGui::SetNextWindowSize(viewport->Size); + ImGui::Begin("plot-fullscreen", nullptr, WINDOW_STYLE); + + if (ImPlot::BeginPlot(g_plot_selected->name, viewport->Size)) { + int test_x[] = {0,1,2,3}; + int test_y1[] = {1,2,3,4}; + int test_y2[] = {2,4,8,16}; + ImPlot::PlotLine("test1", test_x, test_y1, 4); + ImPlot::PlotLine("test2", test_x, test_y2, 4); + ImPlot::EndPlot(); + } + + ImGui::End(); +} + +void gui_print(void) { + if (g_plot_maximized) { + gui_print_plot_maximized(); + } else { + if (g_data_col_visible) gui_print_data_col(); + gui_print_plots(); + } +} + +// ---------------------------------------------------------------------------- +// Main functions +// ---------------------------------------------------------------------------- +void glfw_error_callback(int error, const char* description) { + log_warn("GLFW error %d: %s", error, description); +} + +void glfw_key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { + (void)window; + (void)scancode; + + if (action != GLFW_PRESS) return; + + if (g_plot_maximized) { + switch (mods) { + case GLFW_MOD_CONTROL: + switch (key) { + case GLFW_KEY_C: + glfwSetWindowShouldClose(g_window, GLFW_TRUE); + break; + } + + break; + + case 0: + switch (key) { + case GLFW_KEY_F: + g_plot_maximized = !g_plot_maximized; + break; + } + + break; + } + + return; + } + + switch (mods) { + case GLFW_MOD_CONTROL: + switch (key) { + case GLFW_KEY_C: + glfwSetWindowShouldClose(g_window, GLFW_TRUE); + break; + case GLFW_KEY_N: + g_data_col_visible = !g_data_col_visible; + break; + case GLFW_KEY_LEFT: + g_plot_cols -= g_plot_cols > 1 ? 1 : 0; + data_reset_plot_cells(); + break; + case GLFW_KEY_RIGHT: + g_plot_cols += g_plot_cols < PLOT_MAX_COLS ? 1 : 0; + data_reset_plot_cells(); + break; + } + + break; + + case 0: + switch (key) { + case GLFW_KEY_LEFT: + g_plot_col_selected -= g_plot_col_selected ? 1 : 0; + g_plot_selected = g_plot_cells[g_plot_col_selected][g_plot_row_selected]; + break; + case GLFW_KEY_RIGHT: + g_plot_col_selected += (g_plot_col_selected < PLOT_MAX_COLS - 1 && g_plot_cells[g_plot_col_selected + 1][g_plot_row_selected]) ? 1 : 0; + g_plot_selected = g_plot_cells[g_plot_col_selected][g_plot_row_selected]; + break; + case GLFW_KEY_UP: + g_plot_row_selected -= g_plot_row_selected ? 1 : 0; + g_plot_selected = g_plot_cells[g_plot_col_selected][g_plot_row_selected]; + break; + case GLFW_KEY_DOWN: + g_plot_row_selected += (g_plot_row_selected < PLOT_COUNT - 1 && g_plot_cells[g_plot_col_selected][g_plot_row_selected + 1]) ? 1 : 0; + g_plot_selected = g_plot_cells[g_plot_col_selected][g_plot_row_selected]; + break; + case GLFW_KEY_F: + g_plot_maximized = !g_plot_maximized; + break; + } + + break; + } +} + +int main(int argc, char **argv) { + (void)argc; + (void)argv; + + log_info("Starting SALIS data client"); + + log_info("Initializing GLFW"); + glfwSetErrorCallback(glfw_error_callback); + glfwInitHint(GLFW_WAYLAND_LIBDECOR, GLFW_WAYLAND_DISABLE_LIBDECOR); + if (!glfwInit()) assert(false); + + float scale = ImGui_ImplGlfw_GetContentScaleForMonitor(glfwGetPrimaryMonitor()); + g_window = glfwCreateWindow((int)(800 * scale), (int)(600 * scale), "SALIS data client", nullptr, nullptr); + assert(g_window); + glfwSetKeyCallback(g_window, glfw_key_callback); + glfwMakeContextCurrent(g_window); + glfwSwapInterval(1); // enable vsync + + log_info("Initializing ImGui"); + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImPlot::CreateContext(); + + g_io = &ImGui::GetIO(); + g_io->Fonts->AddFontFromFileTTF(FONT_SOURCE); + g_io->IniFilename = nullptr; + + g_imgui_style = &ImGui::GetStyle(); + g_imgui_style->Colors[ImGuiCol_WindowBg] = COLOR_BG; + g_imgui_style->FontScaleDpi = scale; + g_imgui_style->FontSizeBase = FONT_SIZE; + g_imgui_style->ItemSpacing = ImVec2(g_imgui_style->ItemSpacing.x, 2.f); + g_imgui_style->ScaleAllSizes(scale); + + g_implot_style = &ImPlot::GetStyle(); + g_implot_style->Colors[ImPlotCol_FrameBg] = COLOR_BG; + + ImGui_ImplGlfw_InitForOpenGL(g_window, true); + ImGui_ImplOpenGL3_Init(GLSL_VERSION); + + g_hm_pixel_pow = data_calc_max_hm_pixel_pow(); + + // Main loop + while (!glfwWindowShouldClose(g_window)) { + glfwPollEvents(); + + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + gui_print(); + + if (g_data_touched) { + data_validate(); + } + + ImGui::Render(); + int display_w, display_h; + glfwGetFramebufferSize(g_window, &display_w, &display_h); + glViewport(0, 0, display_w, display_h); + glClearColor(0.f, 0.f, 0.f, 1.f); + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + glfwSwapBuffers(g_window); + } + + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImPlot::DestroyContext(); + ImGui::DestroyContext(); + + log_info("Stopping SALIS data client"); + glfwDestroyWindow(g_window); + glfwTerminate(); + return 0; +} diff --git a/data/plots.c b/data/plots.c deleted file mode 100644 index 211346f..0000000 --- a/data/plots.c +++ /dev/null @@ -1,35 +0,0 @@ -enum PlotType { - PLOT_LINES, -}; - -struct PlotDef { - const char *name; - const char *table; - enum PlotType plot_type; - const char **cols; -}; - -struct PlotDef g_general_plots_def[] = { - { .name = "cycl", .table = "general", .plot_type = PLOT_LINES, .cols = (const char *[]){ -#define FOR_CORE(i) "cycl_" #i, - FOR_CORES -#undef FOR_CORE - }}, - { .name = "mall", .table = "general", .plot_type = PLOT_LINES, .cols = (const char *[]){ -#define FOR_CORE(i) "mall_" #i, - FOR_CORES -#undef FOR_CORE - }}, - { .name = "pnum", .table = "general", .plot_type = PLOT_LINES, .cols = (const char *[]){ -#define FOR_CORE(i) "pnum_" #i, - FOR_CORES -#undef FOR_CORE - }}, - { .name = "ppop", .table = "general", .plot_type = PLOT_LINES, .cols = (const char *[]){ -#define FOR_CORE(i) "pfst_" #i, "plst_" #i, - FOR_CORES -#undef FOR_CORE - }}, -}; - -size_t g_general_plots_count = sizeof(g_general_plots_def) / sizeof(struct PlotDef); diff --git a/data/server.c b/data/server.c index f7f1121..c2fd276 100644 --- a/data/server.c +++ b/data/server.c @@ -25,7 +25,6 @@ void respond_name(int client_fd) { json_object_to_fd(client_fd, sim_name, JSON_C_TO_STRING_PRETTY); json_object_put(sim_name); } -break; void respond_opts(int client_fd) { log_info("Client requested simulation options"); diff --git a/salis.py b/salis.py index f8f6ce0..7569430 100755 --- a/salis.py +++ b/salis.py @@ -23,7 +23,7 @@ epilog = f"Use '-h' to show command arguments; e.g. '{prog} new -h'" parser = ArgumentParser(description="Salis: Simple A-Life Simulator", epilog=epilog, formatter_class=RawTextHelpFormatter, prog=prog) sub_parsers = parser.add_subparsers(dest="command", required=True) -formatter_class = lambda prog: ArgumentDefaultsHelpFormatter(max_help_position=32, prog=prog) +formatter_class = lambda prog: ArgumentDefaultsHelpFormatter(max_help_position=48, prog=prog) new = sub_parsers.add_parser("new", formatter_class=formatter_class, help="create new simulation") load = sub_parsers.add_parser("load", formatter_class=formatter_class, help="load saved simulation") @@ -62,13 +62,15 @@ options = { (("C", "clones"), (new,), fmt_id): {"metavar": "N", "help": "number of ancestor clones on each core", "default": 1, "required": False, "type": nat}, (("c", "cores"), (new,), fmt_id): {"metavar": "N", "help": "number of simulator cores", "default": 2, "required": False, "type": nat}, (("d", "data-push-pow"), (new,), fmt_id): {"metavar": "POW", "help": "data aggregation interval exponent; interval = 2^{POW} >= {sync-pow}; a value of 0 disables data aggregation; requires 'sqlite' and 'zlib'", "default": 28, "required": False, "type": pos}, - (("F", "muta-flip"), (new,), fmt_id): {"action": "store_true", "help": "cosmic rays flip bits instead of randomizing whole bytes", "required": False}, (("f", "force"), (new,), fmt_id): {"action": "store_true", "help": "overwrite existing simulation of given name", "required": False}, - (("G", "compiler-flags"), (new, load, server, client), fmt_id): {"metavar": "FLAGS", "help": "base set of flags to pass to C compiler", "default": "-Wall -Wextra -Werror -pedantic", "required": False, "type": str}, - (("g", "compiler"), (new, load, server, client), fmt_id): {"metavar": "CC", "help": "C compiler to use", "default": "gcc", "required": False, "type": str}, + (("F", "muta-flip"), (new,), fmt_id): {"action": "store_true", "help": "cosmic rays flip bits instead of randomizing whole bytes", "required": False}, + (("g", "c-compiler"), (new, load, server, client), fmt_id): {"metavar": "CC", "help": "C compiler to use", "default": "gcc", "required": False, "type": str}, + (("G", "c-compiler-flags"), (new, load, server, client), fmt_id): {"metavar": "FLAGS", "help": "base set of flags to pass to C compiler", "default": "-Wall -Wextra -Werror -pedantic", "required": False, "type": str}, + (("g++", "cpp-compiler"), (client,), fmt_id): {"metavar": "CXX", "help": "C++ compiler to use", "default": "g++", "required": False, "type": str}, + (("G++", "cpp-compiler-flags"), (client,), fmt_id): {"metavar": "FLAGS", "help": "base set of flags to pass to C++ compiler", "default": "-Wall -Wextra -Werror -pedantic", "required": False, "type": str}, (("H", "home"), (new, load, server), fmt_id): {"metavar": "PATH", "help": "salis home directory", "default": os.path.join(os.environ["HOME"], ".salis"), "required": False, "type": str}, - (("M", "muta-pow"), (new,), fmt_id): {"metavar": "POW", "help": "mutator range exponent; each step a cosmic ray hits addr, where addr = rand_uint64() %% 2^{POW}; lower values of POW mean higher mutation rates", "default": 32, "required": False, "type": pos}, (("i", "ip"), (client,), fmt_id): {"metavar": "IP", "help": "ip address of server", "default": "127.0.0.1", "required": False, "type": str}, + (("M", "muta-pow"), (new,), fmt_id): {"metavar": "POW", "help": "mutator range exponent; each step a cosmic ray hits addr, where addr = rand_uint64() %% 2^{POW}; lower values of POW mean higher mutation rates", "default": 32, "required": False, "type": pos}, (("m", "mvec-pow"), (new,), fmt_id): {"metavar": "POW", "help": "memory core size exponent; size = 2^{POW}", "default": 20, "required": False, "type": pos}, (("n", "name"), (new, load, server), fmt_id): {"metavar": "NAME", "help": "name of new or loaded simulation", "default": "def.sim", "required": False, "type": str}, (("o", "optimized"), (new, load, server, client), fmt_id): {"action": "store_true", "help": "build with optimizations", "required": False}, @@ -93,22 +95,22 @@ args = parser.parse_args() # Build class # ------------------------------------------------------------------------------ class Build: - def __init__(self, path, log, library=False): + def __init__(self, path, log, library=False, cpp=False): self.log = log self.library = library self.tempdir = TemporaryDirectory(prefix="salis_", delete=not args.keep_temp_dir) - self.log.info(f"Generated temporary directory for C builds at: {self.tempdir.name}") + self.log.info(f"Generated temporary directory for builds at: {self.tempdir.name}") self.name = os.path.splitext(os.path.basename(path))[0] self.binfile = os.path.join(self.tempdir.name, f"{self.name}{".so" if library else ""}") self.argsfile = os.path.join(self.tempdir.name, f"{self.name}.arg") - self.flags = {*args.compiler_flags.split(), *({"-shared", "-fPIC"} if library else set()), *({"-O3"} if args.optimized else {"-ggdb"})} + self.flags = {*(args.cpp_compiler_flags if cpp else args.c_compiler_flags).split(), *({"-shared", "-fPIC"} if library else set()), *({"-O3"} if args.optimized else {"-ggdb"})} self.defines = {"-DNDEBUG"} if args.optimized else set() self.links = set() - self.build_cmd = [args.compiler, f"@{self.argsfile}", path, "-o", self.binfile] + self.build_cmd = [args.cpp_compiler if cpp else args.c_compiler, f"@{self.argsfile}", path, "-o", self.binfile] self.log.info(f"Build class initialized for {"library" if library else "executable"}: {path}") self.log.info(f"Compiler flags stored at: {self.argsfile}") @@ -431,12 +433,15 @@ if args.command == "server": # Populate for client if args.command == "client": - ns.b = Build("data/client.c", log) + ns.b = Build("data/client.cpp", log, cpp=True) pop_net_vars() pop_general() ns.b.defines.add(f"-DIP=\"{args.ip}\"") - ns.b.defines.add("-DNCURSES_WIDECHAR=1") - ns.b.links.add("-lcurses") + ns.b.links.add("-lGL") + ns.b.links.add("-lglfw") + ns.b.links.add("-limgui") + ns.b.links.add("-limplot") + ns.b.links.add("-lm") # ------------------------------------------------------------------------------ # Build and launch executable diff --git a/ui/curses/ui.c b/ui/curses/ui.c index 9542287..287745b 100644 --- a/ui/curses/ui.c +++ b/ui/curses/ui.c @@ -718,10 +718,10 @@ void tui_info_impl(const char *format, ...) { va_list args; va_start(args, format); - log_msg_to_buff(g_logs[g_log_ptr], LOG_LINE_SIZE, LOG_INFO, false, format, args); + log_msg_to_buff(g_logs[g_log_ptr], LOG_LINE_SIZE, INFO, false, format, args); va_end(args); - g_log_levels[g_log_ptr] = LOG_INFO; + g_log_levels[g_log_ptr] = INFO; g_log_cnt++; g_log_ptr = (g_log_ptr + 1) % LOG_LINE_COUNT; } @@ -731,10 +731,10 @@ void tui_warn_impl(const char *format, ...) { va_list args; va_start(args, format); - log_msg_to_buff(g_logs[g_log_ptr], LOG_LINE_SIZE, LOG_WARN, false, format, args); + log_msg_to_buff(g_logs[g_log_ptr], LOG_LINE_SIZE, WARN, false, format, args); va_end(args); - g_log_levels[g_log_ptr] = LOG_WARN; + g_log_levels[g_log_ptr] = WARN; g_log_cnt++; g_log_ptr = (g_log_ptr + 1) % LOG_LINE_COUNT; } @@ -751,7 +751,7 @@ void tui_print_log_line(unsigned lptr, int line) { tui_clear_log_line(line); if (strlen(g_logs[lptr])) { - tui_field(line, PANE_AND_MARGIN_WIDTH, g_log_levels[lptr] == LOG_INFO ? PAIR_INFO : PAIR_WARN, A_NORMAL, "%.38s", g_logs[lptr]); + tui_field(line, PANE_AND_MARGIN_WIDTH, g_log_levels[lptr] == INFO ? PAIR_INFO : PAIR_WARN, A_NORMAL, "%.38s", g_logs[lptr]); tui_field(line, PANE_AND_MARGIN_WIDTH + 38, PAIR_NORMAL, A_NORMAL, g_logs[lptr] + 38); } } -- cgit v1.3