improved perfs and event management
This commit is contained in:
1 parent
667ed0c100
commit
bb99be5889
17 files changed
+626
-59
No files matched your search
+136
-21
@@ -215,6 +215,10 @@ bool go_live_again(App& app) {
|
||||
app.sync_last_b.clear();
|
||||
app.rows->set_trace_alpha(1.0f);
|
||||
rebuild_signal_panels(app);
|
||||
// Nothing is on screen from the cache any more, so no row may claim to be. In place, for the
|
||||
// reason `on_event_row_activated()` gives -- and this is why the mark went stale here before:
|
||||
// rebuilding the list was too expensive to do for it.
|
||||
mark_events_on_screen(app);
|
||||
set_notice(app, "live", 3);
|
||||
return true;
|
||||
}
|
||||
@@ -361,7 +365,14 @@ bool build_event_group(App& app, const std::vector<EventRecord>& records, EventG
|
||||
|
||||
app.model->set_target_span(span);
|
||||
app.model->set_signals(std::move(signals));
|
||||
for (std::size_t i = 0; i < cols * layers; ++i) app.model->set_signal_enabled(i, true);
|
||||
// One call, so the group's eight channels are planned once: a loop of single enables
|
||||
// re-planned -- and reallocated -- the whole buffer per channel (see
|
||||
// `ScopeModel::set_signals_enabled`), which is most of what made opening a comparison slow.
|
||||
{
|
||||
std::vector<std::size_t> all(cols * layers);
|
||||
for (std::size_t i = 0; i < all.size(); ++i) all[i] = i;
|
||||
app.model->set_signals_enabled(all, true);
|
||||
}
|
||||
|
||||
const f32 gap = std::numeric_limits<f32>::quiet_NaN();
|
||||
std::vector<f32> values(cols * layers, 0.0f);
|
||||
@@ -556,8 +567,24 @@ void on_group_clicked(GtkButton* button, gpointer user) {
|
||||
|
||||
EventThumbCache* event_thumb_cache(App& app) {
|
||||
if (app.events_list == nullptr) return nullptr;
|
||||
return static_cast<EventThumbCache*>(
|
||||
auto* cache = static_cast<EventThumbCache*>(
|
||||
g_object_get_data(G_OBJECT(app.events_list), "udpscope-event-thumbs"));
|
||||
if (cache != nullptr) return cache;
|
||||
// Created on first use and owned by the list. Nothing used to create it, and a missing cache is
|
||||
// not a slow path here but a *blank* one: `event_preview_texture()` answers from the cache or not
|
||||
// at all, so every row came up without a picture -- which is what the previews were reported for.
|
||||
// Owned by the list because nothing else has the pane's lifetime: textures of a list that no
|
||||
// longer exists are so much held memory.
|
||||
cache = new EventThumbCache();
|
||||
g_object_set_data_full(G_OBJECT(app.events_list), "udpscope-event-thumbs", cache,
|
||||
[](gpointer p) {
|
||||
auto* dead = static_cast<EventThumbCache*>(p);
|
||||
for (auto& entry : *dead) {
|
||||
if (entry.second != nullptr) g_object_unref(entry.second);
|
||||
}
|
||||
delete dead;
|
||||
});
|
||||
return cache;
|
||||
}
|
||||
|
||||
/// The stored preview scaled to a row's size. Scaled here, once, for two reasons: a `GtkPicture`'s
|
||||
@@ -609,13 +636,84 @@ GdkTexture* event_preview_texture(App& app, const std::string& path) {
|
||||
const auto found = cache->find(path);
|
||||
if (found != cache->end()) return found->second;
|
||||
GdkTexture* stored = gdk_texture_new_from_filename(path.c_str(), nullptr);
|
||||
GdkTexture* texture =
|
||||
stored != nullptr ? scale_texture(stored, kEventThumbW, kEventThumbH) : nullptr;
|
||||
if (stored != nullptr) g_object_unref(stored);
|
||||
GdkTexture* texture = nullptr;
|
||||
if (stored != nullptr) {
|
||||
// The stored picture's *own* proportions, fitted to the row's width: the preview is now as tall
|
||||
// as its signal count makes it, and squeezing an eight-strip picture into the height a one-strip
|
||||
// picture gets is what made a row unreadable. Capped, because the row still has to be a row.
|
||||
const int sw = gdk_texture_get_width(stored);
|
||||
const int sh = gdk_texture_get_height(stored);
|
||||
const int height =
|
||||
sw > 0 ? std::clamp(kEventThumbW * sh / sw, kEventThumbH, kEventThumbMaxH) : kEventThumbH;
|
||||
texture = scale_texture(stored, kEventThumbW, height);
|
||||
g_object_unref(stored);
|
||||
}
|
||||
cache->emplace(path, texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
/// Hovering a row's thumbnail shows the stored preview at full size. The row's picture is 138 pixels
|
||||
/// wide and the stored one is 480: the small one says *which* event this is, and this says what is in
|
||||
/// it, which is the whole reason a preview is worth keeping.
|
||||
///
|
||||
/// Through GTK's own tooltip rather than a popover of ours, for lifetime: a tooltip is shown, placed,
|
||||
/// dismissed and destroyed by GTK, and it takes no pointer grab -- a popover per row would be a
|
||||
/// parented widget to unparent on every rebuild of the pane, and a grab to lose (see the GTK
|
||||
/// invariants on both). The texture is decoded in the handler and dropped with the tooltip's picture,
|
||||
/// so a hover costs nothing once it is over and the row cache keeps holding only the small one.
|
||||
gboolean on_preview_query_tooltip(GtkWidget*, int, int, gboolean, GtkTooltip* tooltip, gpointer user) {
|
||||
const char* path = static_cast<const char*>(user);
|
||||
if (path == nullptr || *path == '\0' || tooltip == nullptr) return FALSE;
|
||||
GdkTexture* full = gdk_texture_new_from_filename(path, nullptr);
|
||||
if (full == nullptr) return FALSE;
|
||||
GtkWidget* picture = gtk_picture_new_for_paintable(GDK_PAINTABLE(full));
|
||||
g_object_unref(full);
|
||||
// `can_shrink` FALSE: the point is the full size, and a tooltip would otherwise be given the
|
||||
// smallest size its content allows.
|
||||
gtk_picture_set_can_shrink(GTK_PICTURE(picture), FALSE);
|
||||
gtk_tooltip_set_custom(tooltip, picture);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void attach_preview_zoom(GtkWidget* frame, const std::string& path) {
|
||||
if (frame == nullptr || path.empty()) return;
|
||||
gtk_widget_set_has_tooltip(frame, TRUE);
|
||||
// The path outlives the signal: owned by the closure and freed with the widget, because the record
|
||||
// it came from is a loop variable.
|
||||
g_signal_connect_data(frame, "query-tooltip", G_CALLBACK(on_preview_query_tooltip),
|
||||
g_strdup(path.c_str()), [](gpointer p, GClosure*) { g_free(p); },
|
||||
GConnectFlags(0));
|
||||
}
|
||||
|
||||
/// A row's first line: when the event was latched, how long it is, and whether it is the one on
|
||||
/// screen. One function because two callers write it -- the row builder and the in-place
|
||||
/// re-mark -- and a mark that reads differently depending on which of them last ran would be
|
||||
/// worse than no mark.
|
||||
std::string event_row_title(App& app, const EventRecord& r) {
|
||||
const std::time_t t = static_cast<std::time_t>(r.wall_time);
|
||||
std::tm tm{};
|
||||
localtime_r(&t, &tm);
|
||||
char clock[32];
|
||||
std::strftime(clock, sizeof(clock), "%H:%M:%S", &tm);
|
||||
char title[160];
|
||||
std::snprintf(title, sizeof(title), "%s %.3f s%s", clock, r.duration_s,
|
||||
std::string(app.waveform_name) == r.file ? " (on screen)" : "");
|
||||
return title;
|
||||
}
|
||||
|
||||
void mark_events_on_screen(App& app) {
|
||||
if (app.events_list == nullptr) return;
|
||||
for (GtkWidget* row = gtk_widget_get_first_child(app.events_list); row != nullptr;
|
||||
row = gtk_widget_get_next_sibling(row)) {
|
||||
const auto* record =
|
||||
static_cast<const EventRecord*>(g_object_get_data(G_OBJECT(row), "udpscope-event"));
|
||||
GtkWidget* name = GTK_WIDGET(g_object_get_data(G_OBJECT(row), "udpscope-event-title"));
|
||||
// The "no events yet" placeholder is a row with neither: skipped, not asserted about.
|
||||
if (record == nullptr || name == nullptr) continue;
|
||||
gtk_label_set_text(GTK_LABEL(name), event_row_title(app, *record).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
/// The events pane's rows, newest first: each row is the picture of *that* event beside the
|
||||
/// sidecar's numbers, because what tells one event from another is its shape, and one picture for
|
||||
/// the whole pane describes whichever event happens to be newest rather than the one being read. The
|
||||
@@ -631,7 +729,8 @@ void refresh_events_pane(App& app) {
|
||||
// the one that finds out.
|
||||
app.events_thumb = nullptr;
|
||||
if (app.event_store == nullptr) return;
|
||||
const std::vector<EventRecord> records = app.event_store->list();
|
||||
// Not const: a record whose preview had to be redrawn learns its thumbnail's path here.
|
||||
std::vector<EventRecord> records = app.event_store->list();
|
||||
if (records.empty()) {
|
||||
GtkWidget* empty = gtk_label_new("no events yet -- arm the trigger");
|
||||
gtk_widget_add_css_class(empty, "dim-label");
|
||||
@@ -657,10 +756,8 @@ void refresh_events_pane(App& app) {
|
||||
}
|
||||
}
|
||||
std::size_t unreadable = 0;
|
||||
for (const EventRecord& r : records) {
|
||||
for (EventRecord& r : records) {
|
||||
GtkWidget* row = gtk_list_box_row_new();
|
||||
g_object_set_data_full(G_OBJECT(row), "udpscope-event",
|
||||
new EventRecord(r), [](gpointer p) { delete static_cast<EventRecord*>(p); });
|
||||
|
||||
GtkWidget* line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_margin_start(line, 6);
|
||||
@@ -688,8 +785,24 @@ void refresh_events_pane(App& app) {
|
||||
gtk_widget_add_css_class(frame, "udpscope-event-thumb");
|
||||
gtk_box_append(GTK_BOX(frame), shot);
|
||||
GdkTexture* texture = event_preview_texture(app, r.thumbnail);
|
||||
if (texture == nullptr && app.event_store != nullptr) {
|
||||
// Drawn again from the samples, which are still there: a row with no picture is the state the
|
||||
// user reported as "sometimes missing", and the `.npy` is the event -- the `.bmp` is a picture
|
||||
// of it. Once per row and never in a loop: a second failure is a real one, and the cache keeps
|
||||
// the null so the next refresh does not read the file again.
|
||||
const std::string redrawn = app.event_store->redraw_preview(r);
|
||||
if (!redrawn.empty()) {
|
||||
if (EventThumbCache* cache = event_thumb_cache(app)) cache->erase(redrawn);
|
||||
r.thumbnail = redrawn;
|
||||
texture = event_preview_texture(app, redrawn);
|
||||
}
|
||||
}
|
||||
if (texture != nullptr) {
|
||||
gtk_picture_set_paintable(GTK_PICTURE(shot), GDK_PAINTABLE(texture));
|
||||
// The frame takes the *texture's* height, so a tall preview gets a tall row rather than being
|
||||
// squashed into the floor. Its width is fixed: the rest of the row belongs to the text.
|
||||
gtk_widget_set_size_request(frame, kEventThumbW, gdk_texture_get_height(texture));
|
||||
attach_preview_zoom(frame, r.thumbnail);
|
||||
} else {
|
||||
++unreadable;
|
||||
}
|
||||
@@ -702,15 +815,11 @@ void refresh_events_pane(App& app) {
|
||||
gtk_widget_set_valign(box, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_hexpand(box, TRUE);
|
||||
|
||||
const std::time_t t = static_cast<std::time_t>(r.wall_time);
|
||||
std::tm tm{};
|
||||
localtime_r(&t, &tm);
|
||||
char clock[32];
|
||||
std::strftime(clock, sizeof(clock), "%H:%M:%S", &tm);
|
||||
char title[160];
|
||||
std::snprintf(title, sizeof(title), "%s %.3f s%s", clock, r.duration_s,
|
||||
std::string(app.waveform_name) == r.file ? " (on screen)" : "");
|
||||
GtkWidget* name = gtk_label_new(title);
|
||||
GtkWidget* name = gtk_label_new(event_row_title(app, r).c_str());
|
||||
// Kept on the row: the title is the one part of a row that changes without the cache
|
||||
// changing -- the "(on screen)" mark follows a replay -- and `mark_events_on_screen()`
|
||||
// rewrites it through this instead of rebuilding the list.
|
||||
g_object_set_data(G_OBJECT(row), "udpscope-event-title", name);
|
||||
gtk_widget_set_halign(name, GTK_ALIGN_START);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END);
|
||||
// A cap, not only an ellipsis: `max-width-chars` bounds a label's *natural* width, and the row's
|
||||
@@ -741,6 +850,10 @@ void refresh_events_pane(App& app) {
|
||||
gtk_box_append(GTK_BOX(box), extra);
|
||||
gtk_box_append(GTK_BOX(line), box);
|
||||
|
||||
// Attached *after* the thumbnail was resolved: a preview redrawn a moment ago is part of this
|
||||
// record now, and the copy the row carries is what every later reader of the row sees.
|
||||
g_object_set_data_full(G_OBJECT(row), "udpscope-event", new EventRecord(r),
|
||||
[](gpointer p) { delete static_cast<EventRecord*>(p); });
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), line);
|
||||
gtk_list_box_append(GTK_LIST_BOX(app.events_list), row);
|
||||
}
|
||||
@@ -759,9 +872,11 @@ void on_event_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
|
||||
auto* record = static_cast<EventRecord*>(g_object_get_data(G_OBJECT(row), "udpscope-event"));
|
||||
if (record == nullptr) return;
|
||||
replay_event(app, *record);
|
||||
// Rebuilt, not for the picture -- the row already has it -- but for the "on screen" mark the
|
||||
// replay just changed.
|
||||
refresh_events_pane(app);
|
||||
// The mark the replay just moved, rewritten on the rows that are already there. Rebuilding
|
||||
// the list -- which is what this did -- discarded the scroll position and the selection to
|
||||
// restate one word, so a click on an event scrolled the list back to the top and dropped the
|
||||
// pair a comparison was being assembled from.
|
||||
mark_events_on_screen(app);
|
||||
}
|
||||
|
||||
/// The pane itself: a collapsible column on the left, a header and the list of events -- each row
|
||||
|
||||
Reference in new issue
Block a user