Add --renderer for displays with no usable GPU; gate the header reading

GTK4 defaults to Vulkan, and an X11-forwarded display has no device to make a
swapchain from; the plot is a GtkGLArea, so it then asks for a GL context that
is not there either. --renderer cairo is the software preset for that display:
it exports GSK_RENDERER=cairo and LIBGL_ALWAYS_SOFTWARE=1 in main() before
gtk_init()/adw_init(), because both are read when the renderer and the context
are created. --renderer gl is the faster choice when indirect GL works, and
--renderer vulkan makes the default explicit. Nothing is exported, and GTK's
own choice stands, when the option is absent.

The once-a-second header line is now behind UDPSCOPE_PROBE_HEADER, following
the [notrace] rule: its widths are still sampled for the exit dump, but the
line is detail behind that reading rather than output every run prints.
UDPSCOPE_PROBE_RENDERER prints the exported pair before GTK starts and the
GskRenderer GTK actually built.

README, the manual's overview and docs/claude record the two symptoms and the
cure.
This commit is contained in:
Martino Ferrari committed 2026-10-28 16:03:05 +01:00
1 parent ac054a30c9
commit fcf268d249
9 files changed
+151 -7

No files matched your search

+26 -3
View File
@@ -3465,6 +3465,22 @@ gboolean tick(gpointer user) {
ScopeModel& model = *app->model;
const gint64 tick_start_us = g_get_monotonic_time();
// The renderer GTK actually built, printed once and only under the probe: `--renderer` exports
// `GSK_RENDERER` before any GTK call, and this is the other half of that claim -- the class GTK
// chose, not the name it was asked for. The renderer is null until the window is realized, so a
// tick that finds none simply tries again.
{
static const bool renderer_probe = g_getenv("UDPSCOPE_PROBE_RENDERER") != nullptr;
static bool renderer_printed = false;
if (renderer_probe && !renderer_printed) {
if (GskRenderer* r = gtk_native_get_renderer(GTK_NATIVE(app->window))) {
std::fprintf(stderr, "[renderer] realized=%s\n", G_OBJECT_TYPE_NAME(r));
std::fflush(stderr);
renderer_printed = true;
}
}
}
// The paint probe's clock, claimed on the first tick that finds one: a widget has no
// frame clock until it is realized, and the area is realized on its first draw.
if (app->dbg_clock == nullptr && app->area_widget != nullptr) {
@@ -3500,14 +3516,21 @@ gboolean tick(gpointer user) {
// the title's centre box, i.e. window_button_w tracked the window width).
if (app->window_button != nullptr) {
static gint64 next_header_log = 0;
// The widths stay sampled on this one-second cadence whatever the environment, because the
// exit dump's `header:` line reads them; only the *line* is behind a variable. A reading
// printed on every run is a log flood and not a reading -- the same rule the `[notrace]`
// per-paint line follows.
static const bool header_print = g_getenv("UDPSCOPE_PROBE_HEADER") != nullptr;
const gint64 now = g_get_monotonic_time();
if (now >= next_header_log) {
next_header_log = now + G_TIME_SPAN_SECOND;
app->dbg_win_btn_w = gtk_widget_get_width(app->window_button);
app->dbg_bar_w = gtk_widget_get_width(app->window);
std::fprintf(stderr, "[udpscope] header: window_button_w=%d bar_w=%d\n",
app->dbg_win_btn_w, app->dbg_bar_w);
std::fflush(stderr);
if (header_print) {
std::fprintf(stderr, "[udpscope] header: window_button_w=%d bar_w=%d\n",
app->dbg_win_btn_w, app->dbg_bar_w);
std::fflush(stderr);
}
}
}
// The help layer's own readings and the bounds the help's annotated figures crop, on the same