From 69d3249c8d79ce1ad1a0ecd86d51036672c6b3a3 Mon Sep 17 00:00:00 2001 From: Felitendo Date: Tue, 15 Sep 2026 22:52:12 +0200 Subject: [PATCH] modrinth-enhanced, modrinth-enhanced-bin: preload vblank shim for the full refresh rate on nvidia --- modrinth-enhanced-bin/.SRCINFO | 7 +- modrinth-enhanced-bin/PKGBUILD | 26 +- modrinth-enhanced-bin/modrinth-enhanced.sh | 9 + modrinth-enhanced-bin/pkg.sh | 18 +- modrinth-enhanced-bin/vblank-shim.c | 437 +++++++++++++++++++++ modrinth-enhanced/.SRCINFO | 7 +- modrinth-enhanced/PKGBUILD | 25 +- modrinth-enhanced/modrinth-enhanced.sh | 1 + modrinth-enhanced/pkg.sh | 6 +- modrinth-enhanced/vblank-shim.c | 1 + 10 files changed, 519 insertions(+), 18 deletions(-) create mode 100644 modrinth-enhanced-bin/modrinth-enhanced.sh create mode 100644 modrinth-enhanced-bin/vblank-shim.c create mode 120000 modrinth-enhanced/modrinth-enhanced.sh create mode 120000 modrinth-enhanced/vblank-shim.c diff --git a/modrinth-enhanced-bin/.SRCINFO b/modrinth-enhanced-bin/.SRCINFO index 179dfea..752bd44 100644 --- a/modrinth-enhanced-bin/.SRCINFO +++ b/modrinth-enhanced-bin/.SRCINFO @@ -1,7 +1,7 @@ pkgbase = modrinth-enhanced-bin pkgdesc = Modrinth App without ads or telemetry, with offline and Ely.by accounts and Linux fixes (upstream binary) pkgver = 0.21.2 - pkgrel = 1 + pkgrel = 2 url = https://github.com/Felitendo/Modrinth-Enhanced arch = x86_64 license = GPL-3.0-only @@ -12,6 +12,7 @@ pkgbase = modrinth-enhanced-bin depends = glibc depends = gtk3 depends = hicolor-icon-theme + depends = libdrm depends = libgcc depends = libsoup3 depends = webkit2gtk-4.1 @@ -28,6 +29,10 @@ pkgbase = modrinth-enhanced-bin options = !strip options = !debug source = modrinth-enhanced-bin-0.21.2.deb::https://github.com/Felitendo/Modrinth-Enhanced/releases/download/v0.21.2/Modrinth.Enhanced_0.21.2_amd64.deb + source = vblank-shim.c + source = modrinth-enhanced.sh sha256sums = 32ed32575050b4ac1a990b54a894e82c17860a48d5a228ce0d1ce7fc2c9d5d6f + sha256sums = 0190921b8ff2fb1deec3209cba71c01541ca1f013e3ba41680f1636e185fd455 + sha256sums = b24872f82645c52ee4804599cd678d138876fb193c754403fa9c5cf92bd745b9 pkgname = modrinth-enhanced-bin diff --git a/modrinth-enhanced-bin/PKGBUILD b/modrinth-enhanced-bin/PKGBUILD index 946ca3c..4bb0c23 100644 --- a/modrinth-enhanced-bin/PKGBUILD +++ b/modrinth-enhanced-bin/PKGBUILD @@ -4,13 +4,13 @@ pkgname=modrinth-enhanced-bin pkgver=0.21.2 -pkgrel=1 +pkgrel=2 pkgdesc="Modrinth App without ads or telemetry, with offline and Ely.by accounts and Linux fixes (upstream binary)" arch=('x86_64') url="https://github.com/Felitendo/Modrinth-Enhanced" license=('GPL-3.0-only') depends=('cairo' 'dbus' 'gdk-pixbuf2' 'glib2' 'glibc' 'gtk3' 'hicolor-icon-theme' - 'libgcc' 'libsoup3' 'webkit2gtk-4.1' + 'libdrm' 'libgcc' 'libsoup3' 'webkit2gtk-4.1' # loaded by Minecraft, not by the launcher 'libgl' 'libpulse' 'libx11' 'libxcursor' 'libxext' 'libxxf86vm') optdepends=('xorg-xrandr: for older Minecraft versions') @@ -21,12 +21,30 @@ options=('!strip' '!debug') # v0.21.2-2, which pkgver spells 0.21.2.r2; see pkg.sh. _tag="v0.21.2" _asset="Modrinth.Enhanced_0.21.2_amd64.deb" -source=("${pkgname}-${pkgver}.deb::https://github.com/Felitendo/Modrinth-Enhanced/releases/download/${_tag}/${_asset}") +# With the NVIDIA driver WebKitGTK paces the app with a 60 fps timer whatever +# the monitor's refresh rate. vblank-shim.c, preloaded by modrinth-enhanced.sh, +# paces it at the monitor's rate instead and keeps the app on X11, the only +# place that works; it is not part of upstream's release. +source=("${pkgname}-${pkgver}.deb::https://github.com/Felitendo/Modrinth-Enhanced/releases/download/${_tag}/${_asset}" + "vblank-shim.c" + "modrinth-enhanced.sh") noextract=("${pkgname}-${pkgver}.deb") -sha256sums=('32ed32575050b4ac1a990b54a894e82c17860a48d5a228ce0d1ce7fc2c9d5d6f') +sha256sums=('32ed32575050b4ac1a990b54a894e82c17860a48d5a228ce0d1ce7fc2c9d5d6f' '0190921b8ff2fb1deec3209cba71c01541ca1f013e3ba41680f1636e185fd455' 'b24872f82645c52ee4804599cd678d138876fb193c754403fa9c5cf92bd745b9') + +build() { + # GLib for its headers only: the shim looks GTK and GLib up at run time + gcc $CPPFLAGS $CFLAGS $LDFLAGS -shared -fPIC -o libwebkit-vblank-shim.so vblank-shim.c \ + $(pkg-config --cflags glib-2.0) $(pkg-config --cflags --libs libdrm) +} package() { bsdtar -xOf "$srcdir/${pkgname}-${pkgver}.deb" 'data.tar.*' \ | bsdtar -xpf - -C "$pkgdir" usr + + # the binary moves behind the script that preloads the shim + install -d "$pkgdir/usr/lib/modrinth-enhanced" + mv "$pkgdir/usr/bin/ModrinthEnhanced" "$pkgdir/usr/lib/modrinth-enhanced/" + install -Dm755 libwebkit-vblank-shim.so -t "$pkgdir/usr/lib/modrinth-enhanced/" + install -Dm755 modrinth-enhanced.sh "$pkgdir/usr/bin/ModrinthEnhanced" ln -s ModrinthEnhanced "$pkgdir/usr/bin/modrinth-enhanced" } diff --git a/modrinth-enhanced-bin/modrinth-enhanced.sh b/modrinth-enhanced-bin/modrinth-enhanced.sh new file mode 100644 index 0000000..173fc0d --- /dev/null +++ b/modrinth-enhanced-bin/modrinth-enhanced.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Starts Modrinth Enhanced with the vblank shim preloaded (see vblank-shim.c). +# With the NVIDIA driver the shim only lifts the frame rate under X11, so it is +# asked to keep GTK on X11 there. MODRINTH_VBLANK_SHIM=0 starts the app without. +if [ "${MODRINTH_VBLANK_SHIM:-1}" != 0 ]; then + [ -d /sys/module/nvidia_drm ] && export VBLANK_SHIM_GDK_BACKEND=x11 + export LD_PRELOAD="/usr/lib/modrinth-enhanced/libwebkit-vblank-shim.so${LD_PRELOAD:+:$LD_PRELOAD}" +fi +exec /usr/lib/modrinth-enhanced/ModrinthEnhanced "$@" diff --git a/modrinth-enhanced-bin/pkg.sh b/modrinth-enhanced-bin/pkg.sh index 73ce24a..07b400e 100644 --- a/modrinth-enhanced-bin/pkg.sh +++ b/modrinth-enhanced-bin/pkg.sh @@ -2,9 +2,10 @@ # the Modrinth App (Tauri) with a series of patches applied: no ads, no # telemetry, offline and Ely.by accounts, Linux fixes. # -# Upstream publishes a bundled .deb per release, so there is no build step -# here: on a new version only pkgver, the tag, the asset name and the checksum -# are refreshed and the PKGBUILD repackages the deb directly. +# Upstream publishes a bundled .deb per release, so the app is not built here: +# on a new version only pkgver, the tag, the asset name and the checksums are +# refreshed and the PKGBUILD repackages the deb directly. The only thing built +# is vblank-shim.c, which ships with this package (see the PKGBUILD). # # Releases are tagged after the Modrinth App release they are built on # (v0.21.2). When the patches change without a new Modrinth App release, the @@ -18,6 +19,9 @@ UPSTREAM_REPO="Felitendo/Modrinth-Enhanced" +# Installed in CI before the makepkg test build, for the shim's headers. +BUILD_DEPS=(glib2 libdrm) + latest_version() { gh api "repos/$UPSTREAM_REPO/releases/latest" --jq '.tag_name' | sed 's/^v//; s/-/.r/' } @@ -25,7 +29,11 @@ latest_version() { # refresh_checksums refresh_checksums() { local ver="$1" pkgbuild="$2" - local tag="v${ver/.r/-}" asset sha + local tag="v${ver/.r/-}" dir asset sha sha_shim sha_script + + dir="$(dirname "$pkgbuild")" + sha_shim="$(sha256sum "$dir/vblank-shim.c" | cut -d' ' -f1)" + sha_script="$(sha256sum "$dir/modrinth-enhanced.sh" | cut -d' ' -f1)" asset="$(gh api "repos/$UPSTREAM_REPO/releases/tags/$tag" \ --jq '.assets[].name | select(endswith("_amd64.deb"))')" @@ -40,6 +48,6 @@ refresh_checksums() { sed -i \ -e "s|^_tag=.*|_tag=\"$tag\"|" \ -e "s|^_asset=.*|_asset=\"$asset\"|" \ - -e "s|^sha256sums=.*|sha256sums=('$sha')|" \ + -e "s|^sha256sums=.*|sha256sums=('$sha' '$sha_shim' '$sha_script')|" \ "$pkgbuild" } diff --git a/modrinth-enhanced-bin/vblank-shim.c b/modrinth-enhanced-bin/vblank-shim.c new file mode 100644 index 0000000..2570fa6 --- /dev/null +++ b/modrinth-enhanced-bin/vblank-shim.c @@ -0,0 +1,437 @@ +// LD_PRELOAD shim that lets Modrinth Enhanced (WebKitGTK 4.1 on GTK3) draw at +// the monitor's refresh rate with the NVIDIA driver instead of 60 Hz. +// +// Three things hold it at 60 Hz there: +// 1. WebKitGTK looks for the DRM CRTC of the monitor a window is on by +// physical size, and the NVIDIA DRM connector reports slightly different +// millimetres than GDK does, so no CRTC is found. +// 2. The NVIDIA DRM driver does not implement DRM_IOCTL_WAIT_VBLANK, so even a +// found CRTC cannot be waited on. WebKit then paces with a 60 fps timer. +// 3. GTK3 paces its own paints from the refresh interval the display server +// reports, and assumes 60 Hz without one. KWin reports none to it, on X11 +// (no _NET_WM_FRAME_TIMINGS for XWayland clients) and on Wayland alike. +// +// This shim: +// 1. corrects the connector size to what GDK reports for the same monitor, +// found by output name on X11 ("DP-1") or EDID model name on Wayland; +// 2. emulates blocking vblank waits at the CRTC's refresh rate when the +// driver refuses them; +// 3. fills in the monitor refresh interval on GTK3 frame timings that were +// left without one. +// +// Each step only stands in for what is missing, so with drivers that report +// all of it the shim changes nothing. +// +// The frame rate only comes through on X11: on Wayland something in GTK3 or +// WebKit still asks for 60 frames a second. VBLANK_SHIM_GDK_BACKEND=x11 keeps +// GTK on X11 without setting GDK_BACKEND, which every program the app starts +// would inherit. A GDK_BACKEND the user set still wins. +// +// Build: gcc -shared -fPIC -O2 -o libwebkit-vblank-shim.so vblank-shim.c $(pkg-config --cflags glib-2.0) $(pkg-config --cflags --libs libdrm) +// Debug: VBLANK_SHIM_DEBUG=1 +// Knobs: VBLANK_SHIM_RATE_MULTIPLIER (vblank emulation), VBLANK_SHIM_REFRESH_HZ +// (GTK frame timings, default: fastest monitor), VBLANK_SHIM_FRAME_CLOCK=0 +// (leave GTK frame timings alone) + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct _GdkDisplay GdkDisplay; +typedef struct _GdkMonitor GdkMonitor; +typedef struct _GdkWindow GdkWindow; +typedef struct _GdkFrameClock GdkFrameClock; +typedef struct _GdkFrameTimings GdkFrameTimings; + +static int debug_enabled(void) { + static int value = -1; + if (value < 0) value = getenv("VBLANK_SHIM_DEBUG") != NULL; + return value; +} + +#define debug(...) do { if (debug_enabled()) fprintf(stderr, "vblank-shim: " __VA_ARGS__); } while (0) + +// --- 0. Window system ------------------------------------------------------- + +static char gdk_backend[16]; + +// Read on load, while nothing else runs, and taken out of the environment. +__attribute__((constructor)) +static void take_gdk_backend(void) { + const char *backend = getenv("VBLANK_SHIM_GDK_BACKEND"); + if (!backend) return; + snprintf(gdk_backend, sizeof gdk_backend, "%s", backend); + unsetenv("VBLANK_SHIM_GDK_BACKEND"); +} + +static void select_gdk_backend(void) { + if (!gdk_backend[0] || getenv("GDK_BACKEND")) return; + void (*set_allowed_backends)(const char *) = dlsym(RTLD_DEFAULT, "gdk_set_allowed_backends"); + if (set_allowed_backends) { + set_allowed_backends(gdk_backend); + debug("GDK limited to %s\n", gdk_backend); + } + gdk_backend[0] = '\0'; +} + +int gtk_init_check(int *argc, char ***argv) { + static int (*real)(int *, char ***); + if (!real) real = dlsym(RTLD_NEXT, "gtk_init_check"); + select_gdk_backend(); + return real(argc, argv); +} + +void gtk_init(int *argc, char ***argv) { + static void (*real)(int *, char ***); + if (!real) real = dlsym(RTLD_NEXT, "gtk_init"); + select_gdk_backend(); + real(argc, argv); +} + +// WebKit waits for vblank and GTK paints in the process the shim is preloaded +// into. Its children - WebKit's helper processes, and Minecraft started from +// the launcher - have no use for it, so it leaves LD_PRELOAD on load and keeps +// whatever else is in there. +__attribute__((constructor)) +static void leave_ld_preload(void) { + const char *preload = getenv("LD_PRELOAD"); + Dl_info info; + if (!preload || !dladdr((void *)leave_ld_preload, &info) || !info.dli_fname) return; + const char *self = strrchr(info.dli_fname, '/'); + self = self ? self + 1 : info.dli_fname; + + char *entries = strdup(preload), *kept = calloc(1, strlen(preload) + 1); + if (entries && kept) { + char *save = NULL; + for (char *entry = strtok_r(entries, ": ", &save); entry; entry = strtok_r(NULL, ": ", &save)) { + const char *name = strrchr(entry, '/'); + if (strcmp(name ? name + 1 : entry, self) == 0) continue; + if (*kept) strcat(kept, ":"); + strcat(kept, entry); + } + if (*kept) setenv("LD_PRELOAD", kept, 1); + else unsetenv("LD_PRELOAD"); + } + free(entries); + free(kept); +} + +// GTK and GLib are looked up at run time rather than linked, so the shim loads +// into processes without them (such as the shell script that starts the +// AppImage) and still finds them when they are loaded later with dlopen. +static struct { + GdkDisplay *(*display_get_default)(void); + int (*display_get_n_monitors)(GdkDisplay *); + GdkMonitor *(*display_get_monitor)(GdkDisplay *, int); + const char *(*monitor_get_model)(GdkMonitor *); + int (*monitor_get_width_mm)(GdkMonitor *); + int (*monitor_get_height_mm)(GdkMonitor *); + int (*monitor_get_refresh_rate)(GdkMonitor *); + GType (*frame_clock_get_type)(void); + GdkFrameTimings *(*frame_clock_get_current_timings)(GdkFrameClock *); + gint64 (*frame_timings_get_refresh_interval)(GdkFrameTimings *); + gpointer (*type_class_ref)(GType); + guint (*signal_lookup)(const gchar *, GType); + gulong (*signal_add_emission_hook)(guint, GQuark, GSignalEmissionHook, gpointer, GDestroyNotify); + gpointer (*value_get_object)(const GValue *); +} gtk; + +static int gtk_resolved(void) { + if (gtk.value_get_object) return 1; + #define RESOLVE(field, name) if (!(gtk.field = dlsym(RTLD_DEFAULT, name))) return 0 + RESOLVE(display_get_default, "gdk_display_get_default"); + RESOLVE(display_get_n_monitors, "gdk_display_get_n_monitors"); + RESOLVE(display_get_monitor, "gdk_display_get_monitor"); + RESOLVE(monitor_get_model, "gdk_monitor_get_model"); + RESOLVE(monitor_get_width_mm, "gdk_monitor_get_width_mm"); + RESOLVE(monitor_get_height_mm, "gdk_monitor_get_height_mm"); + RESOLVE(monitor_get_refresh_rate, "gdk_monitor_get_refresh_rate"); + RESOLVE(frame_clock_get_type, "gdk_frame_clock_get_type"); + RESOLVE(frame_clock_get_current_timings, "gdk_frame_clock_get_current_timings"); + RESOLVE(frame_timings_get_refresh_interval, "gdk_frame_timings_get_refresh_interval"); + RESOLVE(type_class_ref, "g_type_class_ref"); + RESOLVE(signal_lookup, "g_signal_lookup"); + RESOLVE(signal_add_emission_hook, "g_signal_add_emission_hook"); + RESOLVE(value_get_object, "g_value_get_object"); + #undef RESOLVE + return 1; +} + +// --- 1. Connector size ------------------------------------------------------ + +static const char *connector_prefix(uint32_t type) { + switch (type) { + case DRM_MODE_CONNECTOR_DisplayPort: return "DP"; + case DRM_MODE_CONNECTOR_HDMIA: return "HDMI-A"; + case DRM_MODE_CONNECTOR_HDMIB: return "HDMI-B"; + case DRM_MODE_CONNECTOR_DVID: return "DVI-D"; + case DRM_MODE_CONNECTOR_DVII: return "DVI-I"; + case DRM_MODE_CONNECTOR_eDP: return "eDP"; + case DRM_MODE_CONNECTOR_VGA: return "VGA"; + default: return NULL; + } +} + +// The monitor name descriptor (0xFC) of the connector's EDID, which is what +// GDK's Wayland backend reports as the monitor model. +static void edid_model_name(int fd, uint32_t connector_id, char *out, size_t size) { + out[0] = '\0'; + drmModeObjectPropertiesPtr props = drmModeObjectGetProperties(fd, connector_id, DRM_MODE_OBJECT_CONNECTOR); + if (!props) return; + for (uint32_t i = 0; i < props->count_props && !out[0]; i++) { + drmModePropertyPtr prop = drmModeGetProperty(fd, props->props[i]); + if (prop && strcmp(prop->name, "EDID") == 0 && props->prop_values[i]) { + drmModePropertyBlobPtr blob = drmModeGetPropertyBlob(fd, props->prop_values[i]); + if (blob && blob->length >= 128) { + const unsigned char *edid = blob->data; + for (int d = 54; d <= 108; d += 18) { + if (edid[d] || edid[d + 1] || edid[d + 2] || edid[d + 3] != 0xFC) continue; + size_t n = 0; + for (int k = 5; k < 18 && n + 1 < size && edid[d + k] != '\n'; k++) out[n++] = edid[d + k]; + while (n && isspace((unsigned char)out[n - 1])) n--; + out[n] = '\0'; + break; + } + } + drmModeFreePropertyBlob(blob); + } + drmModeFreeProperty(prop); + } + drmModeFreeObjectProperties(props); +} + +drmModeConnectorPtr drmModeGetConnector(int fd, uint32_t connector_id) { + static drmModeConnectorPtr (*real)(int, uint32_t); + if (!real) real = dlsym(RTLD_NEXT, "drmModeGetConnector"); + drmModeConnectorPtr connector = real(fd, connector_id); + if (!connector || connector->connection != DRM_MODE_CONNECTED || !gtk_resolved()) return connector; + + GdkDisplay *display = gtk.display_get_default(); + if (!display) return connector; + + char output_name[32] = ""; + const char *prefix = connector_prefix(connector->connector_type); + if (prefix) snprintf(output_name, sizeof output_name, "%s-%u", prefix, connector->connector_type_id); + char edid_name[32]; + edid_model_name(fd, connector_id, edid_name, sizeof edid_name); + + for (int i = 0; i < gtk.display_get_n_monitors(display); i++) { + GdkMonitor *monitor = gtk.display_get_monitor(display, i); + const char *model = gtk.monitor_get_model(monitor); + if (!model) continue; + if (!(output_name[0] && strcmp(model, output_name) == 0) && !(edid_name[0] && strcmp(model, edid_name) == 0)) + continue; + int w = gtk.monitor_get_width_mm(monitor), h = gtk.monitor_get_height_mm(monitor); + if (w > 0 && h > 0 && ((uint32_t)w != connector->mmWidth || (uint32_t)h != connector->mmHeight)) { + debug("%s (%s): connector size %ux%u mm -> %dx%d mm (as GDK reports)\n", output_name, edid_name, + connector->mmWidth, connector->mmHeight, w, h); + connector->mmWidth = w; + connector->mmHeight = h; + } + break; + } + return connector; +} + +// --- 2. Vblank emulation ---------------------------------------------------- + +struct emulated_crtc { + int fd; + unsigned index; + long long period_ns; + struct timespec base; +}; + +static struct emulated_crtc crtcs[16]; +static int crtc_count; +static pthread_mutex_t crtcs_lock = PTHREAD_MUTEX_INITIALIZER; + +static long long to_ns(const struct timespec *t) { + return (long long)t->tv_sec * 1000000000LL + t->tv_nsec; +} + +static double rate_multiplier(void) { + const char *value = getenv("VBLANK_SHIM_RATE_MULTIPLIER"); + double multiplier = value ? atof(value) : 1.0; + return multiplier >= 1.0 ? multiplier : 1.0; +} + +static long long refresh_period_ns(int fd, unsigned index) { + long long period = (long long)(1e9 / (60 * rate_multiplier())); + drmModeResPtr resources = drmModeGetResources(fd); + if (!resources) return period; + if ((int)index < resources->count_crtcs) { + drmModeCrtcPtr crtc = drmModeGetCrtc(fd, resources->crtcs[index]); + if (crtc && crtc->mode_valid && crtc->mode.htotal && crtc->mode.vtotal) { + const drmModeModeInfo *mode = &crtc->mode; + double hz = mode->clock * 1000.0 / ((double)mode->htotal * mode->vtotal); + if (mode->flags & DRM_MODE_FLAG_INTERLACE) hz *= 2; + if (mode->flags & DRM_MODE_FLAG_DBLSCAN) hz /= 2; + if (mode->vscan > 1) hz /= mode->vscan; + if (hz > 1) period = (long long)(1e9 / (hz * rate_multiplier())); + debug("emulating vblank for crtc index %u at %.3f Hz x %.1f\n", index, hz, rate_multiplier()); + } + drmModeFreeCrtc(crtc); + } + drmModeFreeResources(resources); + return period; +} + +static struct emulated_crtc *emulated_crtc_for(int fd, unsigned index) { + pthread_mutex_lock(&crtcs_lock); + struct emulated_crtc *found = NULL; + for (int i = 0; i < crtc_count; i++) { + if (crtcs[i].fd == fd && crtcs[i].index == index) { + found = &crtcs[i]; + break; + } + } + if (!found && crtc_count < (int)(sizeof crtcs / sizeof crtcs[0])) { + found = &crtcs[crtc_count++]; + found->fd = fd; + found->index = index; + found->period_ns = refresh_period_ns(fd, index); + clock_gettime(CLOCK_MONOTONIC, &found->base); + } + pthread_mutex_unlock(&crtcs_lock); + return found; +} + +int drmWaitVBlank(int fd, drmVBlankPtr vbl) { + static int (*real)(int, drmVBlankPtr); + if (!real) real = dlsym(RTLD_NEXT, "drmWaitVBlank"); + + drmVBlankReq request = vbl->request; + int ret = real(fd, vbl); + if (ret == 0) return 0; + + int error = errno; + // Only stand in for a driver that does not do vblank waits at all, and only + // for plain blocking waits. + if ((error != EOPNOTSUPP && error != EPERM) || (request.type & (DRM_VBLANK_EVENT | DRM_VBLANK_SIGNAL))) { + errno = error; + return ret; + } + + unsigned index = (request.type & DRM_VBLANK_SECONDARY) + ? 1 + : (request.type & DRM_VBLANK_HIGH_CRTC_MASK) >> DRM_VBLANK_HIGH_CRTC_SHIFT; + struct emulated_crtc *crtc = emulated_crtc_for(fd, index); + if (!crtc) { + errno = error; + return ret; + } + + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + long long base = to_ns(&crtc->base); + long long current = (to_ns(&now) - base) / crtc->period_ns; + long long target; + if (request.type & DRM_VBLANK_RELATIVE) { + target = current + request.sequence; + } else { + target = request.sequence; + if (target <= current && (request.type & DRM_VBLANK_NEXTONMISS)) target = current + 1; + } + if (target < current) target = current; + + long long when = base + target * crtc->period_ns; + if (target > current) { + struct timespec until = { when / 1000000000LL, when % 1000000000LL }; + while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &until, NULL) == EINTR) { + } + } + + vbl->reply.type = request.type; + vbl->reply.sequence = (unsigned)target; + vbl->reply.tval_sec = when / 1000000000LL; + vbl->reply.tval_usec = (when % 1000000000LL) / 1000; + return 0; +} + +// --- 3. GTK3 frame timings -------------------------------------------------- + +static gint64 frame_refresh_interval_us; +// Offset of the refresh_interval field inside GdkFrameTimings, found through +// the public getter rather than assumed. -1: not found yet, -2: give up. +static long refresh_interval_offset = -1; + +static gint64 monitor_refresh_interval_us(void) { + const char *forced = getenv("VBLANK_SHIM_REFRESH_HZ"); + if (forced && atof(forced) > 1) return (gint64)(1e6 / atof(forced)); + GdkDisplay *display = gtk.display_get_default(); + if (!display) return 0; + int fastest = 0; + for (int i = 0; i < gtk.display_get_n_monitors(display); i++) { + int rate = gtk.monitor_get_refresh_rate(gtk.display_get_monitor(display, i)); + if (rate > fastest) fastest = rate; + } + // GDK reports milli-hertz. + return fastest > 0 ? (gint64)(1000000000LL / fastest) : 0; +} + +static long find_refresh_interval_offset(GdkFrameTimings *timings) { + gint64 *slots = (gint64 *)timings; + // The struct starts with a guint ref_count; the gint64 fields follow it. + for (long offset = 8; offset <= 96; offset += 8) { + gint64 saved = slots[offset / 8]; + slots[offset / 8] = 0x5eed5eed5eedLL; + gboolean match = gtk.frame_timings_get_refresh_interval(timings) == 0x5eed5eed5eedLL; + slots[offset / 8] = saved; + if (match) return offset; + } + return -2; +} + +static gboolean after_paint_hook(GSignalInvocationHint *hint, guint n_params, const GValue *params, gpointer data) { + (void)hint; + (void)data; + if (n_params < 1 || refresh_interval_offset == -2) return TRUE; + GdkFrameTimings *timings = gtk.frame_clock_get_current_timings(gtk.value_get_object(¶ms[0])); + // Only frames the backend left without a refresh interval. + if (!timings || gtk.frame_timings_get_refresh_interval(timings) != 0) return TRUE; + + if (!frame_refresh_interval_us) frame_refresh_interval_us = monitor_refresh_interval_us(); + if (frame_refresh_interval_us <= 0) return TRUE; + if (refresh_interval_offset == -1) { + refresh_interval_offset = find_refresh_interval_offset(timings); + if (refresh_interval_offset < 0) { + debug("could not locate refresh_interval in GdkFrameTimings, leaving GTK alone\n"); + return TRUE; + } + debug("GTK frame timings: refresh interval %" G_GINT64_FORMAT " us (offset %ld)\n", + frame_refresh_interval_us, refresh_interval_offset); + } + ((gint64 *)timings)[refresh_interval_offset / 8] = frame_refresh_interval_us; + return TRUE; +} + +static void install_frame_clock_hook(void) { + const char *enabled = getenv("VBLANK_SHIM_FRAME_CLOCK"); + if (enabled && strcmp(enabled, "0") == 0) return; + if (!gtk_resolved()) return; + GType type = gtk.frame_clock_get_type(); + gtk.type_class_ref(type); // signals exist once the class does; keep it alive + guint signal = gtk.signal_lookup("after-paint", type); + if (!signal) return; + gtk.signal_add_emission_hook(signal, 0, after_paint_hook, NULL, NULL); + debug("installed GTK after-paint hook\n"); +} + +GdkFrameClock *gdk_window_get_frame_clock(GdkWindow *window) { + static GdkFrameClock *(*real)(GdkWindow *); + static pthread_once_t once = PTHREAD_ONCE_INIT; + if (!real) real = dlsym(RTLD_NEXT, "gdk_window_get_frame_clock"); + GdkFrameClock *clock = real(window); + if (clock) pthread_once(&once, install_frame_clock_hook); + return clock; +} diff --git a/modrinth-enhanced/.SRCINFO b/modrinth-enhanced/.SRCINFO index efbf609..1fb8dda 100644 --- a/modrinth-enhanced/.SRCINFO +++ b/modrinth-enhanced/.SRCINFO @@ -1,7 +1,7 @@ pkgbase = modrinth-enhanced pkgdesc = Modrinth App without ads or telemetry, with offline and Ely.by accounts and Linux fixes pkgver = 0.21.2 - pkgrel = 1 + pkgrel = 2 url = https://github.com/Felitendo/Modrinth-Enhanced arch = x86_64 license = GPL-3.0-only @@ -19,6 +19,7 @@ pkgbase = modrinth-enhanced depends = glibc depends = gtk3 depends = hicolor-icon-theme + depends = libdrm depends = libgcc depends = libsoup3 depends = webkit2gtk-4.1 @@ -34,7 +35,11 @@ pkgbase = modrinth-enhanced options = !debug source = modrinth-enhanced-v0.21.2.tar.gz::https://github.com/Felitendo/Modrinth-Enhanced/archive/refs/tags/v0.21.2.tar.gz source = modrinth-code-v0.21.2.tar.gz::https://github.com/modrinth/code/archive/refs/tags/v0.21.2.tar.gz + source = vblank-shim.c + source = modrinth-enhanced.sh sha256sums = a87ddcef0a39ab4a2c56ece806e381c6b398c755da00a9734b0162abc377e5a6 sha256sums = 583e085cd7ae64a7020656af4ecc64ecfcc82fd97519f74cc3ab114d49574a5e + sha256sums = 0190921b8ff2fb1deec3209cba71c01541ca1f013e3ba41680f1636e185fd455 + sha256sums = b24872f82645c52ee4804599cd678d138876fb193c754403fa9c5cf92bd745b9 pkgname = modrinth-enhanced diff --git a/modrinth-enhanced/PKGBUILD b/modrinth-enhanced/PKGBUILD index 9895682..c10892b 100644 --- a/modrinth-enhanced/PKGBUILD +++ b/modrinth-enhanced/PKGBUILD @@ -4,13 +4,13 @@ pkgname=modrinth-enhanced pkgver=0.21.2 -pkgrel=1 +pkgrel=2 pkgdesc="Modrinth App without ads or telemetry, with offline and Ely.by accounts and Linux fixes" arch=('x86_64') url="https://github.com/Felitendo/Modrinth-Enhanced" license=('GPL-3.0-only') depends=('cairo' 'dbus' 'gdk-pixbuf2' 'glib2' 'glibc' 'gtk3' 'hicolor-icon-theme' - 'libgcc' 'libsoup3' 'webkit2gtk-4.1' + 'libdrm' 'libgcc' 'libsoup3' 'webkit2gtk-4.1' # loaded by Minecraft, not by the launcher 'libgl' 'libpulse' 'libx11' 'libxcursor' 'libxext' 'libxxf86vm') # git replays the patches the way upstream's scripts/prepare.sh does; the JDK @@ -27,14 +27,20 @@ options=('!lto' '!debug') _tag="v0.21.2" _upstream="v0.21.2" _base="v0.21.2" +# With the NVIDIA driver WebKitGTK paces the app with a 60 fps timer whatever +# the monitor's refresh rate. vblank-shim.c, preloaded by modrinth-enhanced.sh, +# paces it at the monitor's rate instead and keeps the app on X11, the only +# place that works; it is not part of upstream's release. source=("${pkgname}-${_tag}.tar.gz::https://github.com/Felitendo/Modrinth-Enhanced/archive/refs/tags/${_tag}.tar.gz" - "modrinth-code-${_upstream}.tar.gz::https://github.com/modrinth/code/archive/refs/tags/${_upstream}.tar.gz") + "modrinth-code-${_upstream}.tar.gz::https://github.com/modrinth/code/archive/refs/tags/${_upstream}.tar.gz" + "vblank-shim.c" + "modrinth-enhanced.sh") noextract=("modrinth-code-${_upstream}.tar.gz") if [[ "$_base" != "$_upstream" ]]; then source+=("modrinth-code-${_base}.tar.gz::https://github.com/modrinth/code/archive/refs/tags/${_base}.tar.gz") noextract+=("modrinth-code-${_base}.tar.gz") fi -sha256sums=('a87ddcef0a39ab4a2c56ece806e381c6b398c755da00a9734b0162abc377e5a6' '583e085cd7ae64a7020656af4ecc64ecfcc82fd97519f74cc3ab114d49574a5e') +sha256sums=('a87ddcef0a39ab4a2c56ece806e381c6b398c755da00a9734b0162abc377e5a6' '583e085cd7ae64a7020656af4ecc64ecfcc82fd97519f74cc3ab114d49574a5e' '0190921b8ff2fb1deec3209cba71c01541ca1f013e3ba41680f1636e185fd455' 'b24872f82645c52ee4804599cd678d138876fb193c754403fa9c5cf92bd745b9') prepare() { # What upstream's scripts/prepare.sh does with two shallow tags: apply the @@ -88,6 +94,10 @@ prepare() { } build() { + # GLib for its headers only: the shim looks GTK and GLib up at run time + gcc $CPPFLAGS $CFLAGS $LDFLAGS -shared -fPIC -o libwebkit-vblank-shim.so vblank-shim.c \ + $(pkg-config --cflags glib-2.0) $(pkg-config --cflags --libs libdrm) + cd app export RUSTUP_TOOLCHAIN=stable @@ -106,8 +116,11 @@ build() { package() { cd app - # the same files, under the same names, as upstream's .deb - install -Dm755 target/release/ModrinthEnhanced -t "$pkgdir/usr/bin/" + # the same files, under the same names, as modrinth-enhanced-bin: the binary + # sits behind the script that preloads the shim + install -Dm755 target/release/ModrinthEnhanced -t "$pkgdir/usr/lib/modrinth-enhanced/" + install -Dm755 "$srcdir/libwebkit-vblank-shim.so" -t "$pkgdir/usr/lib/modrinth-enhanced/" + install -Dm755 "$srcdir/modrinth-enhanced.sh" "$pkgdir/usr/bin/ModrinthEnhanced" ln -s ModrinthEnhanced "$pkgdir/usr/bin/modrinth-enhanced" install -Dm644 apps/app/icons/128x128.png \ diff --git a/modrinth-enhanced/modrinth-enhanced.sh b/modrinth-enhanced/modrinth-enhanced.sh new file mode 120000 index 0000000..ee23fae --- /dev/null +++ b/modrinth-enhanced/modrinth-enhanced.sh @@ -0,0 +1 @@ +../modrinth-enhanced-bin/modrinth-enhanced.sh \ No newline at end of file diff --git a/modrinth-enhanced/pkg.sh b/modrinth-enhanced/pkg.sh index 11f9093..927d742 100644 --- a/modrinth-enhanced/pkg.sh +++ b/modrinth-enhanced/pkg.sh @@ -23,7 +23,7 @@ UPSTREAM_REPO="Felitendo/Modrinth-Enhanced" # Installed in CI (pacman) before the makepkg test build. makepkg runs with # -d there, so the libraries the binary links against are listed too. -BUILD_DEPS=(rust git jdk17-openjdk node-gyp nodejs npm pnpm webkit2gtk-4.1 gtk3 libsoup3) +BUILD_DEPS=(rust git jdk17-openjdk node-gyp nodejs npm pnpm webkit2gtk-4.1 gtk3 libsoup3 libdrm) latest_version() { gh api "repos/$UPSTREAM_REPO/releases/latest" --jq '.tag_name' | sed 's/^v//; s/-/.r/' @@ -52,7 +52,11 @@ refresh_checksums() { | sha256sum | cut -d' ' -f1)" sha_upstream="$(curl -sfL "https://github.com/modrinth/code/archive/refs/tags/$upstream.tar.gz" \ | sha256sum | cut -d' ' -f1)" + # vblank-shim.c and modrinth-enhanced.sh ship with the package, as symlinks + # to modrinth-enhanced-bin's copies; they come before the optional base tag sums="'$sha_patches' '$sha_upstream'" + sums+=" '$(sha256sum "$(dirname "$pkgbuild")/vblank-shim.c" | cut -d' ' -f1)'" + sums+=" '$(sha256sum "$(dirname "$pkgbuild")/modrinth-enhanced.sh" | cut -d' ' -f1)'" if [[ "$base" != "$upstream" ]]; then sums+=" '$(curl -sfL "https://github.com/modrinth/code/archive/refs/tags/$base.tar.gz" \ | sha256sum | cut -d' ' -f1)'" diff --git a/modrinth-enhanced/vblank-shim.c b/modrinth-enhanced/vblank-shim.c new file mode 120000 index 0000000..083affb --- /dev/null +++ b/modrinth-enhanced/vblank-shim.c @@ -0,0 +1 @@ +../modrinth-enhanced-bin/vblank-shim.c \ No newline at end of file