diff options
| author | Paul Oliver <contact@pauloliver.dev> | 2026-06-09 15:32:05 +0200 |
|---|---|---|
| committer | Paul Oliver <contact@pauloliver.dev> | 2026-06-11 05:49:12 +0200 |
| commit | 5d46c96cd93460b4d00eadf3b0237824ca297284 (patch) | |
| tree | 41b9b77be530c6420a63e392ecb73d55d3e10609 | |
| parent | cf7daaf435538c9848dfb0fb8dd0e5c9a4c74af2 (diff) | |
Adds final refinements to data client & server
| -rw-r--r-- | core/client.cpp | 738 | ||||
| -rw-r--r-- | core/server.c | 1 | ||||
| -rwxr-xr-x | salis.py | 5 |
3 files changed, 489 insertions, 255 deletions
diff --git a/core/client.cpp b/core/client.cpp index 9883f50..f24b2dd 100644 --- a/core/client.cpp +++ b/core/client.cpp @@ -1,4 +1,29 @@ +// [SECTION] Includes +// [SECTION] Defines +// [SECTION] Status enum +// [SECTION] String comparator declaration +// [SECTION] Trace declarations +// [SECTION] Plot declarations +// [SECTION] Plots +// [SECTION] Heatmap colormap +// [SECTION] Global variables +// [SECTION] String comparator definition +// [SECTION] Trace (base) definition +// [SECTION] TraceNamed definition +// [SECTION] TraceHeatmap definition +// [SECTION] Plot (base) definition +// [SECTION] PlotLines definition +// [SECTION] PlotStacked definition +// [SECTION] PlotHeatmap definition +// [SECTION] Data functions +// [SECTION] GUI functions +// [SECTION] Main functions + +// ---------------------------------------------------------------------------- +// [SECTION] Includes +// ---------------------------------------------------------------------------- #include <arpa/inet.h> +#include <GL/glu.h> #include <GLFW/glfw3.h> #include <imgui_impl_glfw.h> #include <imgui_impl_opengl3.h> @@ -16,7 +41,7 @@ #include "logger.c" // ---------------------------------------------------------------------------- -// Defines +// [SECTION] Defines // ---------------------------------------------------------------------------- #define COLOR_BLACK ImVec4(0.f, 0.f, 0.f, 1.f) #define FONT_SIZE 12.f @@ -52,20 +77,27 @@ #define DEFVAL_HM_PIXEL_COUNT 0x400l // must equal HM_PIXEL_COUNT in server.c // ---------------------------------------------------------------------------- -// State declaration +// [SECTION] Status enum // ---------------------------------------------------------------------------- enum Status { STATUS_STOPPED, STATUS_RUNNING, + STATUS_FETCHING, STATUS_STOPPING, }; // ---------------------------------------------------------------------------- -// Trace declarations +// [SECTION] String comparator declaration +// ---------------------------------------------------------------------------- +struct StringComparator { + bool operator()(const char *a, const char *b) const; +}; + +// ---------------------------------------------------------------------------- +// [SECTION] Trace declarations // ---------------------------------------------------------------------------- template <class T> -class Trace { -public: +struct Trace : public std::vector<T> { Trace(); virtual ~Trace(); virtual size_t start_offset() const; @@ -73,37 +105,28 @@ public: #if !defined(NDEBUG) virtual void validate() const; #endif - - size_t size() const; - T &back(); - T &operator[](size_t n); + void clear(); + void push_back(T value); T *start(); - T max() const; + T &operator[](size_t n); T operator[](size_t n) const; - void clear(); - void push_back(T); - protected: - void trim_spec(int mult = 1); - - std::vector<T> m_data; - size_t m_max_index; + void trim_spec(int64_t multiplier = 1); }; template <class T> -class TraceNamed : public Trace<T> { -public: - TraceNamed(const char *name, const char *name_fmt); - +struct TraceNamed : public Trace<T> { + TraceNamed(const char *name, const char *label); + const char *get_name() const; + const char *get_label() const; +private: const char *m_name; - const char *m_name_fmt; + const char *m_label; }; template <class T> -class TraceHeatmap : public TraceNamed<T> { -public: - TraceHeatmap(const char *name, const char *name_fmt); - +struct TraceHeatmap : public TraceNamed<T> { + TraceHeatmap(const char *name, const char *label); size_t start_offset() const; void trim(); #if !defined(NDEBUG) @@ -112,67 +135,71 @@ public: }; // ---------------------------------------------------------------------------- -// Plot declarations +// [SECTION] Plot declarations // ---------------------------------------------------------------------------- typedef int (*AxisFormatter)(double value, char *buff, int size, void *data); -class Plot { -public: +struct Plot { Plot(const char *name, const char *section); virtual ~Plot(); - void render(ImVec2 frame_size); - + virtual void flag_for_reset(); + const char *get_name() const; + const char *get_section() const; + void render(const ImVec2 &frame_size); + bool m_visible; +private: + static int hex_formatter(double value, char *buff, int size, void *data); + virtual AxisFormatter x_formatter() const; + virtual AxisFormatter y_formatter() const; + virtual float right_margin() const; + virtual int flags() const; + virtual void render_internal(const ImVec2 &frame_size) = 0; + virtual void render_post(const ImVec2 &frame_size); const char *m_name; const char *m_section; - -private: - static int hex_axis_formatter(double value, char *buff, int size, void *data); - virtual AxisFormatter x_axis_formatter(); - virtual AxisFormatter y_axis_formatter(); - virtual float frame_right_margin(); - virtual int plot_flags(); - virtual void render_internal() = 0; - virtual void render_post(ImVec2 frame_size); }; -class PlotLines : public Plot { -public: +struct PlotLines : public Plot { PlotLines(const char *name, const char *section, std::vector<const char *> trace_keys); - std::vector<const char *> m_trace_keys; - private: - void render_internal(); + void render_internal(const ImVec2 &frame_size); + std::vector<const char *> m_trace_keys; }; -class PlotStacked : public Plot { -public: +struct PlotStacked : public Plot { PlotStacked(const char *name, const char *section, std::vector<const char *> trace_keys); - - std::vector<ImS64> m_totals; - std::vector<double> m_old_trace; - std::vector<double> m_new_trace; - std::vector<const char *> m_trace_keys; - + void flag_for_reset(); private: - AxisFormatter y_axis_formatter(); - static int percent_axis_formatter(double value, char *buff, int size, void *data); - void render_internal(); + static int percent_formatter(double value, char *buff, int size, void *data); + AxisFormatter y_formatter() const; + void render_internal(const ImVec2 &frame_size); + std::vector<const char *> m_trace_keys; + std::vector<bool> m_trace_states; + Trace<ImS64> m_trace_totals; + std::vector<Trace<double>> m_trace_normals; + bool m_needs_reset; }; -class PlotHeatmap : public Plot { -public: +struct PlotHeatmap : public Plot { PlotHeatmap(const char *name, const char *section, const char *trace_key); - const char *m_trace_key; - + void flag_for_reset(); private: - float frame_right_margin(); - int plot_flags(); - void render_internal(); - void render_post(ImVec2 frame_size); + float right_margin() const; + int flags() const; + void render_internal(const ImVec2 &frame_size); + void render_end(const ImVec2 &frame_size); + void render_post(const ImVec2 &frame_size); + const char *m_trace_key; + float m_tex_scale_high; + size_t m_tex_rendered_last; + size_t m_rows_rendered; + GLuint m_tex_id; + std::vector<uint8_t> m_tex_render; + bool m_needs_reset; }; // ---------------------------------------------------------------------------- -// Plots +// [SECTION] Plots // ---------------------------------------------------------------------------- #include "arch_plots.cpp" @@ -253,9 +280,9 @@ std::array g_core_plots_heatmaps = std::to_array<PlotHeatmap>({ }); // ---------------------------------------------------------------------------- -// Heatmap colormap +// [SECTION] Heatmap colormap // ---------------------------------------------------------------------------- -std::array g_hm_colormap_cols = std::to_array<ImVec4>({ +std::array g_hm_colormap = std::to_array<ImVec4>({ {0.000f, 0.000f, 0.016f, 1.f}, {0.106f, 0.047f, 0.255f, 1.f}, {0.290f, 0.047f, 0.420f, 1.f}, @@ -269,14 +296,13 @@ std::array g_hm_colormap_cols = std::to_array<ImVec4>({ }); // ---------------------------------------------------------------------------- -// Globals +// [SECTION] Global variables // ---------------------------------------------------------------------------- GLFWwindow *g_window; ImGuiIO *g_imgui_io; ImGuiStyle *g_imgui_style; ImPlotStyle *g_implot_style; -// Data std::array g_x_axes = std::to_array<const char *>({ "rowid", "step", @@ -295,14 +321,12 @@ int64_t g_hm_left = DEFVAL_HM_LEFT; int64_t g_hm_pixel_count = DEFVAL_HM_PIXEL_COUNT; int64_t g_hm_pixel_pow; // calculate on init int64_t g_x_current = -1l; - -int g_trace_len; -int g_trace_offset; +int64_t g_trace_len; +int64_t g_trace_offset; thrd_t g_fetching_thread; mtx_t g_fetching_mutex; -// Layout bool g_data_col_visible = true; bool g_plot_maximized; bool g_plot_scroll; @@ -316,133 +340,122 @@ std::vector<bool> g_plots_covered; Plot *g_plot_selected; Plot *g_plot_hovered; int g_plot_cols = 2; -int g_plot_col_selected; -int g_plot_row_selected; +size_t g_plot_col_selected; +size_t g_plot_row_selected; float g_plot_height = 300.f; float g_hm_scale_pow = HM_SCALE_POW_MIN; -ImPlotColormap g_hm_colormap; +ImPlotColormap g_hm_colormap_id; -// Plots -struct CompStr { - bool operator()(const char *a, const char *b) const { - return strcmp(a, b) < 0; - } -}; - -std::map<const char *, TraceNamed<ImS64> *, CompStr> g_trace_map; +std::map<const char *, TraceNamed<ImS64> *, StringComparator> g_trace_map; std::vector<TraceNamed<ImS64> *> g_traces; std::vector<Plot *> g_plots; -Trace<double> g_x_axis_normal; +Trace<double> g_x_axis_double; +Trace<double> g_zero_trace; + +PFNGLCOPYIMAGESUBDATAPROC glCopyImageSubData; + +// ---------------------------------------------------------------------------- +// [SECTION] String comparator definition +// ---------------------------------------------------------------------------- +bool StringComparator::operator()(const char *a, const char *b) const { + return strcmp(a, b) < 0; +} // ---------------------------------------------------------------------------- -// Trace definitions +// [SECTION] Trace (base) definition // ---------------------------------------------------------------------------- template <class T> -Trace<T>::Trace() : m_data({}), m_max_index(0) {} +Trace<T>::Trace() : std::vector<T>() {} template <class T> Trace<T>::~Trace() {} template <class T> size_t Trace<T>::start_offset() const { - return g_trace_offset; + return (size_t)g_trace_offset; } template <class T> void Trace<T>::trim() { - trim_spec(1); + trim_spec(); } #if !defined(NDEBUG) template <class T> void Trace<T>::validate() const { - assert(size() == g_traces[0]->size()); + assert(this->size() == g_traces[0]->size()); } #endif template <class T> -size_t Trace<T>::size() const { - return m_data.size(); -} - -template <class T> -T &Trace<T>::back() { - return m_data.back(); +void Trace<T>::clear() { + std::vector<T>::clear(); } template <class T> -T &Trace<T>::operator[](size_t n) { -#if !defined(NDEBUG) - return m_data.at(n); -#else - return m_data[n]; -#endif +void Trace<T>::push_back(T value) { + std::vector<T>::push_back(value); } template <class T> T *Trace<T>::start() { - return size() ? &operator[](start_offset()) : nullptr; + return this->size() ? &operator[](start_offset()) : nullptr; } template <class T> -T Trace<T>::max() const { - return size() ? operator[](m_max_index) : 0; +T &Trace<T>::operator[](size_t n) { +#if !defined(NDEBUG) + return this->at(n); +#else + return std::vector<T>::operator[](n); +#endif } template <class T> T Trace<T>::operator[](size_t n) const { #if !defined(NDEBUG) - return m_data.at(n); + return this->at(n); #else - return m_data[n]; + return std::vector<T>::operator[](n); #endif } template <class T> -void Trace<T>::clear() { - m_data.clear(); - m_max_index = 0; +void Trace<T>::trim_spec(int64_t multiplier) { + assert((int64_t)this->size() >= g_entries * multiplier * 2); + this->erase(this->begin(), this->end() - (g_entries * multiplier)); } +// ---------------------------------------------------------------------------- +// [SECTION] TraceNamed definition +// ---------------------------------------------------------------------------- template <class T> -void Trace<T>::push_back(T value) { - m_data.push_back(value); - - if (value > operator[](m_max_index)) { - m_max_index = size() - 1; - } -} +TraceNamed<T>::TraceNamed(const char *name, const char *label) : m_name(name), m_label(label) {} template <class T> -void Trace<T>::trim_spec(int mult) { - assert((int64_t)size() >= g_entries * mult * 2); - m_data.erase(m_data.begin(), m_data.end() - (g_entries * mult)); - - for (size_t i = 0; i < size(); i++) { - m_max_index = std::max(operator[](m_max_index), operator[](i)); - } +const char *TraceNamed<T>::get_name() const { + return m_name; } -// ---------------------------------------------------------------------------- -// TraceNamed definitions -// ---------------------------------------------------------------------------- template <class T> -TraceNamed<T>::TraceNamed(const char *name, const char *name_fmt) : m_name(name), m_name_fmt(name_fmt) {} +const char *TraceNamed<T>::get_label() const { + return m_label; +} // ---------------------------------------------------------------------------- -// TraceHeatmap definitions +// [SECTION] TraceHeatmap definition // ---------------------------------------------------------------------------- template <class T> TraceHeatmap<T>::TraceHeatmap(const char *name, const char *name_fmt) : TraceNamed<T>(name, name_fmt) {} template <class T> size_t TraceHeatmap<T>::start_offset() const { - return g_trace_offset * g_hm_pixel_count; + return (size_t)(g_trace_offset * g_hm_pixel_count); } template <class T> void TraceHeatmap<T>::trim() { - this->trim_spec(g_hm_pixel_count); + Trace<T>::trim_spec(g_hm_pixel_count); } #if !defined(NDEBUG) @@ -453,152 +466,304 @@ void TraceHeatmap<T>::validate() const { #endif // ---------------------------------------------------------------------------- -// Plot definitions +// [SECTION] Plot (base) definition // ---------------------------------------------------------------------------- -Plot::Plot(const char *name, const char *section) : m_name(name), m_section(section) {} +Plot::Plot(const char *name, const char *section) : m_visible(true), m_name(name), m_section(section) {} Plot::~Plot() {} -void Plot::render(ImVec2 frame_size) { - if (ImPlot::BeginPlot(m_name, ImVec2(frame_size.x - frame_right_margin(), frame_size.y), plot_flags())) { +void Plot::flag_for_reset() {} + +const char *Plot::get_name() const { + return m_name; +} + +const char *Plot::get_section() const { + return m_section; +} + +void Plot::render(const ImVec2 &frame_size) { + if (ImPlot::BeginPlot(m_name, ImVec2(frame_size.x - right_margin(), frame_size.y), flags())) { int axis_flags = ImPlotAxisFlags_Foreground | (g_status != STATUS_STOPPED ? ImPlotAxisFlags_AutoFit : 0); ImPlot::SetupAxes(nullptr, nullptr, axis_flags, axis_flags); - ImPlot::SetupAxisFormat(ImAxis_X1, x_axis_formatter()); - ImPlot::SetupAxisFormat(ImAxis_Y1, y_axis_formatter()); + ImPlot::SetupAxisFormat(ImAxis_X1, x_formatter()); + ImPlot::SetupAxisFormat(ImAxis_Y1, y_formatter()); if (ImPlot::IsPlotHovered()) g_plot_hovered = this; - render_internal(); + render_internal(frame_size); ImPlot::EndPlot(); } render_post(frame_size); } -int Plot::hex_axis_formatter(double value, char *buff, int size, void *data) { +int Plot::hex_formatter(double value, char *buff, int size, void *data) { (void)data; snprintf(buff, size, "%s%#lx", value < 0. ? "-" : "", abs((int64_t)value)); return 0; } -AxisFormatter Plot::x_axis_formatter() { - return Plot::hex_axis_formatter; +AxisFormatter Plot::x_formatter() const { + return Plot::hex_formatter; } -AxisFormatter Plot::y_axis_formatter() { - return Plot::hex_axis_formatter; +AxisFormatter Plot::y_formatter() const { + return Plot::hex_formatter; } -float Plot::frame_right_margin() { +float Plot::right_margin() const { return 0.f; } -int Plot::plot_flags() { +int Plot::flags() const { return 0; } -void Plot::render_post(ImVec2 frame_size) { +void Plot::render_post(const ImVec2 &frame_size) { (void)frame_size; } // ---------------------------------------------------------------------------- -// PlotLines definitions +// [SECTION] PlotLines definition // ---------------------------------------------------------------------------- PlotLines::PlotLines(const char *name, const char *section, std::vector<const char *> trace_keys) : Plot(name, section), m_trace_keys(trace_keys) {} -void PlotLines::render_internal() { +void PlotLines::render_internal(const ImVec2 &frame_size) { + (void)frame_size; + ImS64 *x = g_trace_map[g_x_axes[g_x_axis]]->start(); for (auto &trace : m_trace_keys) { TraceNamed<ImS64> *trace_obj = g_trace_map[trace]; ImS64 *y = trace_obj->start(); - ImPlot::PlotLine(trace_obj->m_name_fmt, x, y, g_trace_len); + ImPlot::PlotLine(trace_obj->get_label(), x, y, g_trace_len); } } // ---------------------------------------------------------------------------- -// PlotStacked definitions +// [SECTION] PlotStacked definition // ---------------------------------------------------------------------------- -PlotStacked::PlotStacked(const char *name, const char *section, std::vector<const char *> trace_keys) : Plot(name, section), m_trace_keys(trace_keys) {} +PlotStacked::PlotStacked(const char *name, const char *section, std::vector<const char *> trace_keys) : Plot(name, section), m_trace_keys(trace_keys), m_trace_states(trace_keys.size(), true), m_trace_totals(), m_trace_normals(trace_keys.size()), m_needs_reset(true) {} -AxisFormatter PlotStacked::y_axis_formatter() { - return PlotStacked::percent_axis_formatter; +void PlotStacked::flag_for_reset() { + m_needs_reset = true; } -int PlotStacked::percent_axis_formatter(double value, char *buff, int size, void *data) { +int PlotStacked::percent_formatter(double value, char *buff, int size, void *data) { (void)data; snprintf(buff, size, "%3.0f%%", value * 100.); return 0; } -void PlotStacked::render_internal() { - m_totals.resize(g_trace_len); - m_old_trace.resize(g_trace_len); - m_new_trace.resize(g_trace_len); +AxisFormatter PlotStacked::y_formatter() const { + return PlotStacked::percent_formatter; +} + +void PlotStacked::render_internal(const ImVec2 &frame_size) { + (void)frame_size; for (size_t i = 0; i < m_trace_keys.size(); i++) { - ImPlot::PlotDummy(g_trace_map[m_trace_keys[i]]->m_name_fmt); + ImPlot::PlotDummy(g_trace_map[m_trace_keys[i]]->get_label()); } - for (int i = 0; i < (int)m_trace_keys.size(); i++) { - if (GImPlot->CurrentPlot->Items.GetLegendItem(i)->Show) { - for (int j = 0; j < g_trace_len; j++) { - m_totals[j] += g_trace_map[m_trace_keys[i]]->start()[j]; + for (size_t i = 0; i < m_trace_keys.size(); i++) { + bool trace_visible = GImPlot->CurrentPlot->Items.GetLegendItem(i)->Show; + + if (m_trace_states[i] != trace_visible) { + m_trace_states[i] = trace_visible; + m_needs_reset = true; + } + } + + if (m_needs_reset) { + m_trace_totals.clear(); + for (auto &trace_normal : m_trace_normals) trace_normal.clear(); + m_needs_reset = false; + } + + size_t trace_size = m_trace_totals.size(); + + for (size_t i = trace_size; i < g_traces[0]->size(); i++) { + m_trace_totals.push_back(0l); + + for (size_t j = 0; j < m_trace_keys.size(); j++) { + if (GImPlot->CurrentPlot->Items.GetLegendItem(j)->Show) { + m_trace_totals.back() += g_trace_map[m_trace_keys[j]]->operator[](i); } } } - for (int i = 0; i < (int)m_trace_keys.size(); i++) { - if (GImPlot->CurrentPlot->Items.GetLegendItem(i)->Show) { - ImPlotSpec spec = ImPlotSpec(ImPlotProp_FillAlpha, GImPlot->CurrentPlot->Items.GetLegendItem(i)->LegendHovered ? 1.f : 0.9f); - TraceNamed<ImS64> *trace = g_trace_map[m_trace_keys[i]]; - const char *trace_name = trace->m_name_fmt; + for (size_t i = trace_size; i < g_traces[0]->size(); i++) { + if (m_trace_totals[i]) { + double normal = 0.; - for (int j = 0; j < g_trace_len; j++) { - m_new_trace[j] = m_totals[j] ? (m_old_trace[j] + (double)trace->start()[j] / (double)m_totals[j]) : 0.; + for (size_t j = 0; j < m_trace_keys.size(); j++) { + if (GImPlot->CurrentPlot->Items.GetLegendItem(j)->Show) { + normal += (double)g_trace_map[m_trace_keys[j]]->operator[](i) / (double)m_trace_totals[i]; + m_trace_normals[j].push_back(normal); + } } + } else { + for (size_t j = 0; j < m_trace_keys.size(); j++) { + if (GImPlot->CurrentPlot->Items.GetLegendItem(j)->Show) { + m_trace_normals[j].push_back(0.); + } + } + } + } + +#if !defined(NDEBUG) + m_trace_totals.validate(); - ImPlot::PlotShaded(trace_name, g_x_axis_normal.start(), m_old_trace.data(), m_new_trace.data(), g_trace_len, spec); - ImPlot::PlotLine(trace_name, g_x_axis_normal.start(), m_old_trace.data(), g_trace_len); - ImPlot::PlotLine(trace_name, g_x_axis_normal.start(), m_new_trace.data(), g_trace_len); - std::swap(m_old_trace, m_new_trace); + for (size_t i = 0; i < m_trace_keys.size(); i++) { + if (GImPlot->CurrentPlot->Items.GetLegendItem(i)->Show) { + m_trace_normals[i].validate(); + } else { + assert(m_trace_normals[i].empty()); } } +#endif + + Trace<double> *prev_trace = &g_zero_trace; - m_totals.clear(); - m_old_trace.clear(); - m_new_trace.clear(); + for (size_t i = 0; i < m_trace_keys.size(); i++) { + assert(m_trace_states[i] == GImPlot->CurrentPlot->Items.GetLegendItem(i)->Show); + + if (m_trace_states[i]) { + ImPlotSpec spec = ImPlotSpec(ImPlotProp_FillAlpha, GImPlot->CurrentPlot->Items.GetLegendItem(i)->LegendHovered ? 1.f : 0.9f); + const char *trace_name = g_trace_map[m_trace_keys[i]]->get_label(); + Trace<double> *current_trace = &m_trace_normals[i]; + ImPlot::PlotShaded(trace_name, g_x_axis_double.start(), prev_trace->start(), current_trace->start(), g_trace_len, spec); + ImPlot::PlotLine(trace_name, g_x_axis_double.start(), prev_trace->start(), g_trace_len); + ImPlot::PlotLine(trace_name, g_x_axis_double.start(), current_trace->start(), g_trace_len); + prev_trace = current_trace; + } + } } // ---------------------------------------------------------------------------- -// PlotHeatmap definitions +// [SECTION] PlotHeatmap definition // ---------------------------------------------------------------------------- -PlotHeatmap::PlotHeatmap(const char *name, const char *section, const char *trace_key) : Plot(name, section), m_trace_key(trace_key) {} +PlotHeatmap::PlotHeatmap(const char *name, const char *section, const char *trace_key) : Plot(name, section), m_trace_key(trace_key), m_tex_scale_high(0.f), m_tex_rendered_last(0), m_rows_rendered(0), m_tex_id(0), m_tex_render(), m_needs_reset(true) {} -float PlotHeatmap::frame_right_margin() { +void PlotHeatmap::flag_for_reset() { + m_needs_reset = true; +} + +float PlotHeatmap::right_margin() const { return HM_COLORSCALE_WIDTH; } -int PlotHeatmap::plot_flags() { +int PlotHeatmap::flags() const { return ImPlotFlags_NoLegend; } -void PlotHeatmap::render_internal() { - TraceNamed<ImS64> *trace = g_trace_map[m_trace_key]; - ImPlot::PushColormap(g_hm_colormap); - double scale_max = g_hm_scale_pow == HM_SCALE_POW_MIN ? 0. : pow(2., (double)g_hm_scale_pow); - ImPlot::PlotHeatmap(trace->m_name_fmt, trace->start(), g_trace_len, g_hm_pixel_count, 0., scale_max, nullptr, ImPlotPoint(0, g_x_current), ImPlotPoint(MVEC_SIZE, 0)); - ImPlot::PopColormap(); +void PlotHeatmap::render_internal(const ImVec2 &frame_size) { + Trace<ImS64> *trace = g_trace_map[m_trace_key]; + + if (m_needs_reset) { + m_tex_scale_high = 0.f; + m_tex_rendered_last = trace->start_offset(); + m_rows_rendered = 0; + glDeleteTextures(1, &m_tex_id); + m_tex_id = 0; + m_needs_reset = false; + } + + if (m_tex_rendered_last == trace->size()) { + render_end(frame_size); + return; + } + + assert(m_tex_render.empty()); + assert((trace->size() - m_tex_rendered_last) % g_hm_pixel_count == 0); + size_t rows_to_append = (trace->size() - m_tex_rendered_last) / (size_t)g_hm_pixel_count; + size_t rows_to_chop_off = (m_rows_rendered + rows_to_append) > (size_t)g_trace_len ? (m_rows_rendered + rows_to_append) - (size_t)g_trace_len : 0; + + if (rows_to_chop_off > m_rows_rendered) { + m_needs_reset = true; + render_internal(frame_size); + return; + } + + if (g_hm_scale_pow == HM_SCALE_POW_MIN) { + ImS64 max = 0; + + for (size_t i = trace->start_offset(); i < trace->size(); i++) { + max = std::max(max, trace->operator[](i)); + } + + m_tex_scale_high = (float)max; + } else { + m_tex_scale_high = pow(2.f, g_hm_scale_pow); + } + + m_tex_render.reserve((trace->size() - m_tex_rendered_last) * 3); + + for (size_t i = m_tex_rendered_last; i < trace->size(); i++) { + ImS64 value = trace->operator[](i); + float normal = std::min((float)value / m_tex_scale_high, 1.f); + assert(normal >= 0.f && normal <= 1.f); + ImVec4 color = ImPlot::SampleColormap(normal, g_hm_colormap_id); + m_tex_render.push_back((uint8_t)(color.x * 255.f)); // red + m_tex_render.push_back((uint8_t)(color.y * 255.f)); // green + m_tex_render.push_back((uint8_t)(color.z * 255.f)); // blue + } + + GLuint new_tex_id; + glGenTextures(1, &new_tex_id); + glBindTexture(GL_TEXTURE_2D, new_tex_id); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, g_hm_pixel_count, g_trace_len, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr); + assert(glGetError() == GL_NO_ERROR); + assert(new_tex_id); + + if (m_tex_id) { + glCopyImageSubData( + m_tex_id, GL_TEXTURE_2D, 0, 0, rows_to_chop_off, 0, + new_tex_id, GL_TEXTURE_2D, 0, 0, 0, 0, + g_hm_pixel_count, m_rows_rendered - rows_to_chop_off, 1 + ); + assert(glGetError() == GL_NO_ERROR); + glDeleteTextures(1, &m_tex_id); + } + + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, m_rows_rendered - rows_to_chop_off, g_hm_pixel_count, rows_to_append, GL_RGB, GL_UNSIGNED_BYTE, m_tex_render.data()); + assert(glGetError() == GL_NO_ERROR); + + render_end(frame_size); + + m_tex_rendered_last = trace->size(); + m_rows_rendered += rows_to_append - rows_to_chop_off; + assert(m_rows_rendered <= (size_t)g_trace_len); + m_tex_id = new_tex_id; + m_tex_render.clear(); } -void PlotHeatmap::render_post(ImVec2 frame_size) { +void PlotHeatmap::render_end(const ImVec2 &frame_size) { + if (g_x_axis_double.empty()) return; + + ImVec2 bmin(g_hm_left, (float)*g_x_axis_double.start()); + ImVec2 bmax(g_hm_left + g_hm_pixel_count * pow(2.f, g_hm_pixel_pow), (float)g_x_axis_double.back()); + ImVec2 uv0(0.f, 1.f); + ImVec2 uv1(1.f, 0.f); + double scale_max = g_hm_scale_pow == HM_SCALE_POW_MIN ? (double)m_tex_scale_high : pow(2., (double)g_hm_scale_pow); + + ImPlot::PlotImage(get_name(), (ImTextureID)m_tex_id, bmin, bmax, uv0, uv1); ImGui::SameLine(); - ImPlot::PushColormap(g_hm_colormap); - double scale_max = g_hm_scale_pow == HM_SCALE_POW_MIN ? (double)g_trace_map[m_trace_key]->max() : pow(2., (double)g_hm_scale_pow); + + ImPlot::PushColormap(g_hm_colormap_id); ImPlot::ColormapScale("##hm-scale", 0., scale_max, ImVec2(HM_COLORSCALE_WIDTH, frame_size.y), "%.1e"); ImPlot::PopColormap(); } +void PlotHeatmap::render_post(const ImVec2 &frame_size) { + (void)frame_size; +} + // ---------------------------------------------------------------------------- -// Data functions +// [SECTION] Data functions // ---------------------------------------------------------------------------- int64_t data_max_hm_pixel_pow(void) { return (int64_t)floor(log2((double)(MVEC_SIZE - g_hm_left) / (double)g_hm_pixel_count)); @@ -616,11 +781,13 @@ void data_on_field_change(void) { g_hm_pixel_pow = std::clamp(g_hm_pixel_pow, 0l, data_max_hm_pixel_pow()); g_x_current = -1l; - g_trace_len = 0; - g_trace_offset = 0; + g_trace_len = 0l; + g_trace_offset = 0;; for (auto &trace : g_traces) trace->clear(); - g_x_axis_normal.clear(); + for (auto &plot: g_plots) plot->flag_for_reset(); + g_x_axis_double.clear(); + g_zero_trace.clear(); } void data_reset_fields(void) { @@ -643,6 +810,9 @@ void data_reset_plot_cells(void) { } void data_fetch(void) { + assert(g_status == STATUS_RUNNING); + g_status = STATUS_FETCHING; + json_object *request = json_object_new_object(); json_object_object_add(request, "request", json_object_new_string("data")); json_object_object_add(request, "entries", json_object_new_int64(g_entries)); @@ -679,7 +849,8 @@ void data_fetch(void) { for (size_t i = 0; i < new_rows; i++) { ImS64 point = json_object_get_int64(json_object_array_get_idx(value, i)); - g_x_axis_normal.push_back((double)point); + g_x_axis_double.push_back((double)point); + g_zero_trace.push_back(0.); } } @@ -697,15 +868,22 @@ void data_fetch(void) { #if !defined(NDEBUG) for (auto &trace : g_traces) trace->validate(); + g_x_axis_double.validate(); + g_zero_trace.validate(); #endif if ((int64_t)g_traces[0]->size() >= g_entries * 2) { + log_info("Trimming traces & flagging plots for reset"); for (auto &trace : g_traces) trace->trim(); - g_x_axis_normal.trim(); + for (auto &plot : g_plots) plot->flag_for_reset(); + g_x_axis_double.trim(); + g_zero_trace.trim(); } #if !defined(NDEBUG) for (auto &trace : g_traces) trace->validate(); + g_x_axis_double.validate(); + g_zero_trace.validate(); #endif int64_t current_size = g_traces[0]->size(); @@ -713,13 +891,14 @@ void data_fetch(void) { g_trace_offset = current_size > g_entries ? current_size - g_entries : 0l; mtx_unlock(&g_fetching_mutex); + g_status = STATUS_RUNNING; } int data_fetching_thread(void *data) { (void)data; assert(!data); - assert(g_status == STATUS_RUNNING || g_status == STATUS_STOPPING); + assert(g_status == STATUS_RUNNING); while (g_status == STATUS_RUNNING) { data_fetch(); @@ -741,14 +920,13 @@ void data_start_fetching(void) { } void data_stop_fetching(void) { - assert(g_status == STATUS_RUNNING); + assert(g_status == STATUS_RUNNING || g_status == STATUS_FETCHING); log_info("Stopping data fetching thread"); g_status = STATUS_STOPPING; - thrd_join(g_fetching_thread, nullptr); } // ---------------------------------------------------------------------------- -// GUI functions +// [SECTION] GUI functions // ---------------------------------------------------------------------------- void gui_render_data_input(const char *label, int64_t *target) { assert(target); @@ -808,6 +986,7 @@ void gui_render_data_col(void) { gui_render_data_input("hm-pxl-pow", &g_hm_pixel_pow); break; case STATUS_RUNNING: + case STATUS_FETCHING: case STATUS_STOPPING: ImGui::LabelText("entries", "%#lx", g_entries); ImGui::LabelText("nth", "%#lx", g_nth); @@ -821,23 +1000,21 @@ void gui_render_data_col(void) { switch (g_status) { case STATUS_STOPPED: - if (ImGui::Button("Run", ImVec2(-1.f, 0.f))) { - data_start_fetching(); - } - - if (ImGui::Button("Reset", ImVec2(-1.f, 0.f))) { - data_reset_fields(); - } - + if (ImGui::Button("Run", ImVec2(-1.f, 0.f))) data_start_fetching(); + if (ImGui::Button("Reset", ImVec2(-1.f, 0.f))) data_reset_fields(); break; case STATUS_RUNNING: - if (ImGui::Button("Stop", ImVec2(-1.f, 0.f))) { - data_stop_fetching(); - } - + if (ImGui::Button("Stop", ImVec2(-1.f, 0.f))) data_stop_fetching(); ImGui::LabelText("##", "Running"); break; + case STATUS_FETCHING: + if (ImGui::Button("Stop", ImVec2(-1.f, 0.f))) data_stop_fetching(); + ImGui::LabelText("##", "Fetching ..."); + break; case STATUS_STOPPING: + ImGui::BeginDisabled(); + ImGui::Button("Stop", ImVec2(-1.f, 0.f)); + ImGui::EndDisabled(); ImGui::LabelText("##", "Stopping"); break; } @@ -847,19 +1024,30 @@ void gui_render_data_col(void) { ImGui::DragFloat("plot-height", &g_plot_height, PLOT_HEIGHT_INTERVAL, PLOT_MIN_HEIGHT, PLOT_MAX_HEIGHT, "%.0f"); ImGui::DragFloat("hm-pow", &g_hm_scale_pow, HM_SCALE_POW_INTERVAL, HM_SCALE_POW_MIN, HM_SCALE_POW_MAX, "%.0f"); + ImGui::SeparatorText("Plots"); + if (ImGui::Button("Show all", ImVec2(-1.f, 0.f))) for (auto &plot : g_plots) plot->m_visible = true; + if (ImGui::Button("Hide all", ImVec2(-1.f, 0.f))) for (auto &plot : g_plots) plot->m_visible = false; + ImGui::BeginTable("plot-visibility-table", 3); + + for (auto &plot : g_plots) { + ImGui::TableNextColumn(); + ImGui::Checkbox(plot->get_name(), &plot->m_visible); + } + + ImGui::EndTable(); ImGui::End(); } -int gui_plot_cell_index(int row, int col) { - int index = row * PLOT_MAX_COLS + col; - assert(index < (int)g_plot_cells.size()); - assert(index < (int)g_plot_cells_top.size()); - assert(index < (int)g_plot_cells_bottom.size()); +size_t gui_plot_cell_index(size_t row, size_t col) { + size_t index = row * PLOT_MAX_COLS + col; + assert(index < g_plot_cells.size()); + assert(index < g_plot_cells_top.size()); + assert(index < g_plot_cells_bottom.size()); return index; } -int gui_plot_cell_row_up() { - for (int row = g_plot_row_selected - 1; row >= 0; row--) { +size_t gui_plot_cell_row_up() { + for (size_t row = g_plot_row_selected - 1; row < g_plot_row_selected; row--) { if (g_plot_cells[gui_plot_cell_index(row, g_plot_col_selected)]) { return row; } @@ -868,8 +1056,8 @@ int gui_plot_cell_row_up() { return g_plot_row_selected; } -int gui_plot_cell_row_down() { - for (int row = g_plot_row_selected + 1; row < (int)g_plots.size(); row++) { +size_t gui_plot_cell_row_down() { + for (size_t row = g_plot_row_selected + 1; row < g_plots.size(); row++) { if (g_plot_cells[gui_plot_cell_index(row, g_plot_col_selected)]) { return row; } @@ -879,7 +1067,7 @@ int gui_plot_cell_row_down() { } void gui_render_plots(void) { - const char *section_current = g_plots[0]->m_section; + const char *section_current = g_plots[0]->get_section(); const char *section_next = nullptr; g_plots_covered.clear(); @@ -901,8 +1089,20 @@ void gui_render_plots(void) { g_plot_scroll_current = ImGui::GetScrollY(); g_plot_hovered = nullptr; - int row = 0; - int col = 0; + if (!g_plot_selected->m_visible) { + g_plot_selected = g_plots[0]; + g_plot_col_selected = 0; + g_plot_row_selected = 0; + + for (auto &plot : g_plots) { + if (plot->m_visible) { + g_plot_selected = plot; + } + } + } + + size_t row = 0; + size_t col = 0; mtx_lock(&g_fetching_mutex); @@ -911,31 +1111,33 @@ void gui_render_plots(void) { ImGui::BeginTable("plots-table", g_plot_cols); for (size_t i = 0; i < g_plots.size(); i++) { - if (strcmp(g_plots[i]->m_section, section_current)) { - section_next = (!section_next && !g_plots_covered[i]) ? g_plots[i]->m_section : section_next; + if (strcmp(g_plots[i]->get_section(), section_current)) { + section_next = (!section_next && !g_plots_covered[i]) ? g_plots[i]->get_section() : section_next; continue; } - ImGui::TableNextColumn(); - ImVec2 frame_size = ImVec2(ImGui::GetContentRegionAvail().x, g_plot_height); - g_plot_cells[gui_plot_cell_index(row, col)] = g_plots[i]; - g_plot_cells_top[gui_plot_cell_index(row, col)] = ImGui::GetCursorPosY(); + if (g_plots[i]->m_visible) { + ImGui::TableNextColumn(); + ImVec2 frame_size = ImVec2(ImGui::GetContentRegionAvail().x, g_plot_height); + g_plot_cells[gui_plot_cell_index(row, col)] = g_plots[i]; + g_plot_cells_top[gui_plot_cell_index(row, col)] = ImGui::GetCursorPosY(); - 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 (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]; + } - g_plots[i]->render(frame_size); + g_plots[i]->render(frame_size); - if (g_plots[i] == g_plot_selected) { - g_implot_style->Colors[ImPlotCol_FrameBg] = COLOR_BLACK; - } + if (g_plots[i] == g_plot_selected) { + g_implot_style->Colors[ImPlotCol_FrameBg] = COLOR_BLACK; + } - g_plot_cells_bottom[gui_plot_cell_index(row, col)] = ImGui::GetCursorPosY(); - col = (col + 1) % g_plot_cols; - row += col ? 0 : 1; + g_plot_cells_bottom[gui_plot_cell_index(row, col)] = ImGui::GetCursorPosY(); + col = (col + 1) % g_plot_cols; + row += col ? 0 : 1; + } g_plots_covered[i] = true; } @@ -948,7 +1150,6 @@ void gui_render_plots(void) { } mtx_unlock(&g_fetching_mutex); - ImGui::End(); } @@ -963,17 +1164,18 @@ void gui_render_plot_maximized(void) { viewport->Size.y - g_imgui_style->WindowPadding.y * 2, }; + mtx_lock(&g_fetching_mutex); g_plot_selected->render(frame_size); - + mtx_unlock(&g_fetching_mutex); ImGui::End(); } void gui_plot_queue_scroll_to_position(bool increased_plot_height) { - int row = 0; + size_t row = 0; float selected_plot_top = 0.f; - for (row = 0; row < (int)g_plots.size(); row++) { - for (int col = 0; col < PLOT_MAX_COLS; col++) { + for (row = 0; row < g_plots.size(); row++) { + for (size_t col = 0; col < PLOT_MAX_COLS; col++) { if (g_plot_selected == g_plot_cells[gui_plot_cell_index(row, col)]) { selected_plot_top = g_plot_cells_top[gui_plot_cell_index(row, col)]; goto loop_exit; @@ -1014,14 +1216,14 @@ void gui_render(void) { } // ---------------------------------------------------------------------------- -// Main functions +// [SECTION] Main functions // ---------------------------------------------------------------------------- void app_sig_handler(int signo) { (void)signo; log_warn("Signal received, will stop SALIS data client..."); - if (g_status == STATUS_RUNNING) { + if (g_status == STATUS_RUNNING || g_status == STATUS_FETCHING) { data_stop_fetching(); } @@ -1038,6 +1240,7 @@ void app_toggle_state() { data_start_fetching(); break; case STATUS_RUNNING: + case STATUS_FETCHING: data_stop_fetching(); break; } @@ -1052,9 +1255,18 @@ void app_key_callback_plot_maximized(int key, int mods) { break; case GLFW_KEY_COMMA: g_hm_scale_pow = std::max(g_hm_scale_pow - HM_SCALE_POW_INTERVAL, HM_SCALE_POW_MIN); + for (auto &heatmap : g_core_plots_heatmaps) heatmap.flag_for_reset(); + for (auto &heatmap : g_arch_plots_heatmaps) heatmap.flag_for_reset(); break; case GLFW_KEY_PERIOD: g_hm_scale_pow = std::min(g_hm_scale_pow + HM_SCALE_POW_INTERVAL, HM_SCALE_POW_MAX); + for (auto &heatmap : g_core_plots_heatmaps) heatmap.flag_for_reset(); + for (auto &heatmap : g_arch_plots_heatmaps) heatmap.flag_for_reset(); + break; + case GLFW_KEY_0: + g_hm_scale_pow = HM_SCALE_POW_MIN; + for (auto &heatmap : g_core_plots_heatmaps) heatmap.flag_for_reset(); + for (auto &heatmap : g_arch_plots_heatmaps) heatmap.flag_for_reset(); break; } @@ -1118,9 +1330,18 @@ void app_key_callback(GLFWwindow* window, int key, int scancode, int action, int break; case GLFW_KEY_COMMA: g_hm_scale_pow = std::max(g_hm_scale_pow - HM_SCALE_POW_INTERVAL, HM_SCALE_POW_MIN); + for (auto &heatmap : g_core_plots_heatmaps) heatmap.flag_for_reset(); + for (auto &heatmap : g_arch_plots_heatmaps) heatmap.flag_for_reset(); break; case GLFW_KEY_PERIOD: g_hm_scale_pow = std::min(g_hm_scale_pow + HM_SCALE_POW_INTERVAL, HM_SCALE_POW_MAX); + for (auto &heatmap : g_core_plots_heatmaps) heatmap.flag_for_reset(); + for (auto &heatmap : g_arch_plots_heatmaps) heatmap.flag_for_reset(); + break; + case GLFW_KEY_0: + g_hm_scale_pow = HM_SCALE_POW_MIN; + for (auto &heatmap : g_core_plots_heatmaps) heatmap.flag_for_reset(); + for (auto &heatmap : g_arch_plots_heatmaps) heatmap.flag_for_reset(); break; } @@ -1129,7 +1350,7 @@ void app_key_callback(GLFWwindow* window, int key, int scancode, int action, int case 0: switch (key) { case GLFW_KEY_LEFT: - g_plot_col_selected = std::max(g_plot_col_selected - 1, 0); + g_plot_col_selected -= g_plot_col_selected ? 1 : 0; g_plot_selected = g_plot_cells[gui_plot_cell_index(g_plot_row_selected, g_plot_col_selected)]; break; case GLFW_KEY_RIGHT: @@ -1147,7 +1368,7 @@ void app_key_callback(GLFWwindow* window, int key, int scancode, int action, int gui_plot_queue_scroll_to_selected(); break; case GLFW_KEY_F: - g_plot_maximized = !g_plot_maximized; + if (g_plot_selected->m_visible) g_plot_maximized = !g_plot_maximized; break; case GLFW_KEY_SPACE: app_toggle_state(); @@ -1188,7 +1409,14 @@ void init() { glfwSetKeyCallback(g_window, app_key_callback); glfwSetMouseButtonCallback(g_window, app_mouse_button_callback); glfwMakeContextCurrent(g_window); - glfwSwapInterval(1); // enable vsync +#if defined(VSYNC) + glfwSwapInterval(1); +#else + glfwSwapInterval(0); +#endif + + glCopyImageSubData = (PFNGLCOPYIMAGESUBDATAPROC)glfwGetProcAddress("glCopyImageSubData"); + assert(glCopyImageSubData); log_info("Initializing ImGui"); IMGUI_CHECKVERSION(); @@ -1208,7 +1436,7 @@ void init() { g_implot_style = &ImPlot::GetStyle(); g_implot_style->Colors[ImPlotCol_FrameBg] = COLOR_BLACK; - g_hm_colormap = ImPlot::AddColormap("Inferno", g_hm_colormap_cols.data(), g_hm_colormap_cols.size(), false); + g_hm_colormap_id = ImPlot::AddColormap("heatmap", g_hm_colormap.data(), g_hm_colormap.size(), false); ImGui_ImplGlfw_InitForOpenGL(g_window, true); ImGui_ImplOpenGL3_Init(GLSL_VERSION); @@ -1222,7 +1450,7 @@ void init() { for (auto &plot : g_arch_plots_stacked) g_plots.push_back(&plot); for (auto &plot : g_core_plots_heatmaps) g_plots.push_back(&plot); for (auto &plot : g_arch_plots_heatmaps) g_plots.push_back(&plot); - for (auto &i : g_traces) g_trace_map[i->m_name] = i; + for (auto &i : g_traces) g_trace_map[i->get_name()] = i; g_plot_cells = std::vector<Plot *>(g_plots.size() * PLOT_MAX_COLS); g_plot_cells_top = std::vector<float>(g_plots.size() * PLOT_MAX_COLS); diff --git a/core/server.c b/core/server.c index 292c3d9..2c52ea4 100644 --- a/core/server.c +++ b/core/server.c @@ -303,6 +303,7 @@ int main(void) { signal(SIGINT, sig_handler); signal(SIGTERM, sig_handler); + signal(SIGPIPE, SIG_IGN); // ignore broken pipes log_info("Creating response header"); g_response_header = json_object_new_object(); @@ -77,6 +77,7 @@ options = { (("T", "keep-temp-dir"), (new, load, server, client), fmt_id): {"action": "store_true", "help": "keep temporary directory on exit", "required": False}, (("t", "thread-gap"), (new, load), fmt_hex): {"metavar": "N", "help": "memory gap between core elements in bytes; may help reduce cache misses", "default": 0x100, "required": False, "type": nat}, (("u", "ui"), (new, load), fmt_id): {"choices": uis, "help": "user interface", "default": "curses", "required": False, "type": str}, + (("v", "no-vsync"), (client,), fmt_id): {"action": "store_true", "help": "disable vsync", "required": False}, (("x", "cpp-compiler"), (client,), fmt_id): {"metavar": "CXX", "help": "C++ compiler to use", "default": "g++", "required": False, "type": str}, (("X", "cpp-compiler-flags"), (client,), fmt_id): {"metavar": "FLAGS", "help": "base set of flags to pass to C++ compiler", "default": "-Wall -Wextra -Werror -pedantic -std=c++20 -lstdc++", "required": False, "type": str}, (("y", "sync-pow"), (new,), fmt_id): {"metavar": "POW", "help": "core sync interval exponent; sync events occur every N steps, where N = 2^{POW}", "default": 20, "required": False, "type": pos}, @@ -412,6 +413,10 @@ if args.command == "client": ns.b = Build("core/client.cpp", log, cpp=True) pop_net_vars() pop_general() + + if not args.no_vsync: + ns.b.defines.add("-DVSYNC") + ns.b.defines.add(f"-DIP=\"{args.ip}\"") ns.b.links.add("-lGL") ns.b.links.add("-lglfw") |
