Compare commits

...
Author SHA1 Message Date
DeeJanuzandClaude Opus 5.5 d8c2ed0c58 Screens: frame rates by attention, ticks in step with the display
ft-screens gave KWin a frame callback for every committed screen on each tick, and the tick
was an 11 ms timer set again after each run, so it slid through the display's frame and came
about 85 times a second at 90 Hz: the desktop repeated a frame several times a second (judder
in scrolling and video), and KWin drew every screen in one burst at a random point of
vrcompositor's frame. Hidden screens got the same 90 Hz unless Frametop was paused for a game.

- Ticks run on a timerfd at absolute times, once per display frame, 1 ms after the vsync
  (IVRSystem::GetTimeSinceLastVsync and the HMD's display frequency, read once a second), so
  KWin gets its callbacks early in the frame. Measured with --no-vr: 91 wakeups a second
  instead of about 85. On the Frame the vsync times SteamVR reports lie on a 90 Hz grid.
- Each screen's callbacks come at a rate for how much of it you see (vr.cpp,
  UpdateAttention): every frame while focused (within 12 degrees of where your head points,
  a laser or the mouse on it in the last 1.5 s, carried, or typed on), 15 a second for the
  rest of what you see (within 60 degrees), and 1 a second when hidden, behind you, or
  paused. Levels rise at once and fall after 1.5 s (focused) or 0.5 s (in view). KWin draws
  a screen only after its callback and its apps wait for theirs, so this throttles the apps
  too. A screen where nothing changes costs nothing at any rate, as before.
- A video in view keeps every frame: 8 commits in a row that each redraw 6% or more of the
  screen, at 10 a second or more, count as one (from the surface's buffer damage).
- "rates F V H" / --rates set the three rates (default 0 15 1, 0 = every frame), "rates?"
  shows them and each screen's level, "watch S" gives everything full rate for S seconds for
  a remote viewer (vnc-bridge.sh renews it), and "phase MS" moves the ticks for tuning.
- ft-screens' main thread runs at nice -5 after the session starts: SteamOS allows down to
  -8 once the soft RLIMIT_NICE is raised, and KWin waits on these ticks. It had spent nearly
  3 times as long waiting to run as running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:16:46 -06:00
DeeJanuzandClaude Opus 5.5 06c4ff9556 Merge branch game-pause (Game optimization page) into experimental
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:47:44 -06:00
DeeJanuzandClaude Opus 5.5 76e5c4932e Merge branch game-pause (update-check: still controllers) into experimental
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:37:18 -06:00
DeeJanuzandClaude Opus 5.5 b0415cf150 Merge branch game-pause (pause Frametop for VR games, gaze idle) into experimental
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:34:50 -06:00
6 changed files with 272 additions and 20 deletions

No files matched your search

+1 -1
View File
@@ -193,7 +193,7 @@ Staying awake while charging uses Steam's own setting rather than a logind sleep
Hiding the screens during a game kept them out of view, but Frametop kept using the headset. Measured on 2026-10-02 with gaze mode off and no game running, in shares of one core: our eye tracker (ft-eyes) about 60%, ft-eyegrab, ft-gaze and ft-gazed about 3 to 4% each; remote desktop (krdpserver, FreeRDP, Xvnc) about 2 cores while it ran; KWin about 13%, ft-screens about 4%. The gaze service ran at full rate whether gaze mode was on or not; now it idles while the gaze isn't used (gaze/README.md), and pausing stops it outright. Reading SteamVR's eye tracking 90 times a second also made it restart every 10 to 13 seconds during Beat Saber, and each restart took input focus from the game, which paused it (PR #13; since then ft-gaze skips SteamVR's gaze action during games, but our own tracker kept running). So pausing stops what costs the most and leaves windows where they are.
- A hidden screen still cost as much as a visible one. ft-screens sent every committed screen its frame callback at 90 Hz whether its overlay showed or not, so KWin kept drawing, and its apps with it. Paused, ft-screens sends the callbacks once a second. A Wayland client draws again only after its last frame's callback, so KWin's output stalls, KWin's own clients stop getting theirs, and the whole desktop idles, without anything losing its connection. A second's pace, rather than none, keeps any client that waits on a callback from waiting forever. Stopping KWin or the apps with SIGSTOP would free the same, but a Wayland peer that stops reading overflows the other side's 4 KB socket buffer, which ends the connection: that's how the live desktop died once when its KWin stalled (`Data too big for buffer`). They also sit in different cgroups (KWin under steam.service when the VR launcher starts it, ft-screens in the dev container's), so no single freeze stops them together.
- A hidden screen still cost as much as a visible one. ft-screens sent every committed screen its frame callback at 90 Hz whether its overlay showed or not (since then, a hidden screen always gets one a second; see the frame rates in reference.md), so KWin kept drawing, and its apps with it. Paused, ft-screens sends the callbacks once a second. A Wayland client draws again only after its last frame's callback, so KWin's output stalls, KWin's own clients stop getting theirs, and the whole desktop idles, without anything losing its connection. A second's pace, rather than none, keeps any client that waits on a callback from waiting forever. Stopping KWin or the apps with SIGSTOP would free the same, but a Wayland peer that stops reading overflows the other side's 4 KB socket buffer, which ends the connection: that's how the live desktop died once when its KWin stalled (`Data too big for buffer`). They also sit in different cgroups (KWin under steam.service when the VR launcher starts it, ft-screens in the dev container's), so no single freeze stops them together.
- The relay does the pausing because it's the one part that always runs, and the pointer helper keeps running because stopping it leaves its virtual controller connected with its last pose (the driver has no staleness timeout), maybe holding a hand role, with the 3D mouse dead. Releasing it does the job. The helper already checks for a scene app twice a second, so it's what tells the relay a game started.
- The gesture has to work during a game, but SteamVR input reaches only the app with input focus, and an overlay with global input (`steamvr/globalActionSetPriority`) takes the buttons it binds from the game. vrserver's web socket on 127.0.0.1:27062, which its controller binding page uses for the live view, reports every controller component whatever has focus, and reading it takes nothing. The game sees the clicks too, so the default is a gesture games hardly use: both thumbsticks, together, twice. "Together" means within 0.3 seconds of each other, so a stick held down to sprint while the other clicks doesn't count. The stream is about 160 messages a second, nearly all capacitive sensing, so the reader parses only the few that mention a gesture's button. A controller's root path changes while the 3D mouse holds its hand role (`/devices/cv/<serial>` instead of `/user/hand/right`), so the reader looks the controllers up again every 3 seconds.
- Resuming starts remote desktop through `systemd-run --scope`: started straight from the relay, it would join the relay's cgroup and end with the next relay restart.
+4 -1
View File
@@ -69,8 +69,11 @@ visibility always|dashboard|gesture|toggle wrist degrees gesture left|righ
hide | show | toggle controllers always|outside_games|dashboard ingames hide|visible pause on|off|state
conceal N|all reveal N|all concealed cutouts on|off|state cutouts predict on|off cutouts lead ms
float N mpp x y w h title unfloat N pose N matrix sub N k x y w h | sub N k off minimized N 0|1 carry N
rates focused in_view hidden rates? watch seconds phase ms
```
Each screen draws at a frame rate for how much of it you see. KWin draws a screen only after ft-screens gives it a frame callback, and its apps wait for theirs, so the rate of callbacks is the screen's frame rate, for KWin and the apps on it alike. A screen is focused while you look at it (within 12 degrees of where your head points), while a laser or the mouse is on it or was in the last 1.5 seconds, while it's carried, and while you type on it; it gets every display frame. The rest of what you can see (within 60 degrees) gets 15 frames a second, and a hidden screen, one behind you, and everything while paused get one a second. A level goes up at once and comes down after a moment (1.5 s from focused, 0.5 s from in view). A video, or anything moving over a large part of a screen (6% or more of it, redrawn on 8 commits in a row, 10 or more a second), keeps every frame while in view. A floating window is a screen of its own here. Nothing that stands still costs anything at any rate: KWin sends a frame only when something on the screen changed. `rates F V H` sets the three rates in Hz (0: every display frame; default `0 15 1`, also `ft-screens --rates 0,15,1`), and `rates?` shows them, the display's rate, and for each screen its level, its milliseconds between frames, and whether it counts as a video. `watch S` gives every screen full rate for S seconds: remote desktop renews it while a VNC client is connected, since a viewer sees what KWin draws. The ticks (SteamVR events and the callbacks) come once per display frame, 1 ms after the vsync (`phase ms` changes that, for tuning), in step with the display rather than on a timer that drifted through the frame.
`conceal` and `reveal` hide and show one screen on its own (`ft-layout hide` and `show` send them), and `concealed` lists those screens. `pause on` (from the input relay, when Frametop pauses for a VR game) hides every screen and floating window whatever else says, and slows the desktop down; `pause off` undoes it. `cutouts` turns the hand cutouts on and off (`ft-handsctl cutouts`). The last line is ft-floatd's, for floating windows: N is a floating window's panel, numbered on from the screens, one per spare output. `float` gives the window's rectangle in its output, metres per pixel, and the title bar's height, and shows the panel; `unfloat` hides it. `pose` places it (a 3x4 matrix, standing universe), `sub` shows popup or dialog k over it, `minimized` hides it while its window is minimized, and `carry` moves it with the laser that pressed the window's own title bar.
## Input relay
@@ -206,7 +209,7 @@ Paused, Frametop leaves the headset's CPU and GPU to a VR game. The input relay
- The gaze service stops (`frametop-gaze`: ft-gazed, ft-gaze, our own eye tracker, the gaze panel), so nothing reads SteamVR's eye tracking. Our frame grabber, the root service `ft-eyegrab`, goes idle by itself 3 seconds after our eye tracker stops asking it for frames.
- Hand tracking stops if it runs (`frametop-camd`, `frametop-hands`).
- The desktop, as the Game optimization page of Frametop Input Settings says (`pause_desktop`): hidden (the default) or closed. Hidden, ft-screens hides every screen and floating window whatever the visibility mode, the hotkey, or the dashboard says, and gives KWin a frame callback once a second instead of 90 times. KWin draws a screen only after its frame callback, and its apps wait for theirs, so the desktop hardly draws, but its windows stay open. Remote desktop stops if it runs (`session/remote-ctl.sh`). Closed, `desktops.sh stop` closes the desktop and its windows, and resuming starts it again (about 12 seconds), in its start profile if it has one.
- The desktop, as the Game optimization page of Frametop Input Settings says (`pause_desktop`): hidden (the default) or closed. Hidden, ft-screens hides every screen and floating window whatever the visibility mode, the hotkey, or the dashboard says, and gives KWin a frame callback once a second instead of every display frame. KWin draws a screen only after its frame callback, and its apps wait for theirs, so the desktop hardly draws, but its windows stay open. Remote desktop stops if it runs (`session/remote-ctl.sh`). Closed, `desktops.sh stop` closes the desktop and its windows, and resuming starts it again (about 12 seconds), in its start profile if it has one.
- The relay lets go of the 3D mouse and feeds pointer devices to its virtual mouse and keyboard, as with `POINTER=0`. Typing goes to Steam. Mapped buttons and key combinations do nothing but pausing, the Steam menu, and commands; a key combination that does nothing is typed as usual.
Resuming starts again only what pausing stopped, and plays a second sound. The pointer helper and ft-powerd keep running: they cost little, the helper is what says a game started, and stopping it would leave its virtual controller connected with its last pose.
+1 -1
View File
@@ -23,6 +23,6 @@ $cxx -c -o build/handcut.o handcut.cpp
$cxx -c -o build/handtest.o handtest.cpp
vrlibs="$(pkg-config --libs egl glesv2 gbm) -L/opt/steamvr/bin/linuxarm64 -lopenvr_api -Wl,-rpath,/opt/steamvr/bin/linuxarm64"
g++ -o build/ft-screens build/compositor.o build/vr.o build/keyboard.o build/handcut.o \
$(pkg-config --libs wlroots-0.20 wayland-server xkbcommon) $vrlibs
$(pkg-config --libs wlroots-0.20 wayland-server xkbcommon pixman-1) $vrlibs
g++ -o build/ft-handtest build/handtest.o build/handcut.o $vrlibs
echo "built build/ft-screens build/ft-handtest"'
+170 -16
View File
@@ -13,7 +13,7 @@
// scale first ("scale <screen> <s>", from ft-layout).
//
// Usage: ft-screens [--socket NAME] [--control NAME] [--no-vr] [--screen WxH@METRES]...
// [--spares N] [-- COMMAND ARGS...]
// [--spares N] [--rates F,V,H] [-- COMMAND ARGS...]
// --socket Wayland socket name in $XDG_RUNTIME_DIR (default ft-screens-0)
// --control the control socket's abstract name (default ft_screens)
// --no-vr run without SteamVR, for tests next to the running desktop: no panels, no
@@ -22,6 +22,8 @@
// --screen one per screen, in KWin's order (default: 3440x1440@2.4)
// --spares KWin's outputs after the screens: spares for floating windows (ft-floatd
// turns them on and sizes them; see docs/floating-windows.md)
// --rates frame rates in Hz for screens you look at, the rest you see, and hidden ones
// (0: every display frame; default 0,15,1; see frame_interval)
// COMMAND run with WAYLAND_DISPLAY set to our socket (e.g. the Frametop session)
// Runs in the dev container (wlroots 0.20); KWin connects from the host.
#define _GNU_SOURCE
@@ -33,7 +35,9 @@
#include <string.h>
#include <stddef.h>
#include <sys/socket.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/timerfd.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <time.h>
@@ -61,6 +65,12 @@
#include "controller-click.h"
#define MAX_SCREENS 24 // screens and spare outputs
// A screen counts as playing a video while its last VIDEO_COMMITS commits each redrew at
// least VIDEO_AREA percent of it, at VIDEO_HZ or more; for VIDEO_HOLD ms after that stops.
#define VIDEO_COMMITS 8
#define VIDEO_AREA 6
#define VIDEO_HZ 10
#define VIDEO_HOLD 1500
struct config {
int width, height;
@@ -80,6 +90,13 @@ struct screen {
int buffer_width, buffer_height;
struct wlr_xdg_toplevel_decoration_v1 *decoration; // answered on the first commit
struct wl_listener commit, destroy, decoration_destroy, set_title;
// Its frame rate (see tick): when its last frame callback went (ms), and what its recent
// commits looked like, to tell a video playing on it.
uint32_t frame_sent;
uint32_t commit_ms[VIDEO_COMMITS];
unsigned commit_at;
unsigned big; // a bit per recent commit: it redrew at least VIDEO_AREA percent
uint32_t video_until; // counts as a video until then (ms)
};
// Per client buffer: forget its import when it goes away.
@@ -104,6 +121,14 @@ struct server {
int spares;
struct wl_list buffers; // tracked_buffer
struct wl_event_source *tick;
int tick_fd; // timerfd, at absolute times in step with the display's vsync (see schedule)
int64_t period_ns, base_ns, synced_ns; // the display's frame time, a tick time, last sync
double phase_ms; // ticks come this long after a vsync
// Frame rates in Hz for each attention level (0: every display frame), and a remote
// viewer's lease: until then (ms) every screen gets full rate ("watch").
int rate[3];
uint32_t watch_until;
uint32_t typed_ms; // the last key sent to the desktop: its screen counts as focused
struct screen *pointer_focus;
struct ft_controller_click controller_click;
pid_t child;
@@ -149,6 +174,25 @@ static void track_buffer(struct server *s, struct wlr_buffer *buffer) {
// ---------------------------------------------------------------- screens
// A video (or anything moving over a large area) on a screen you don't look at keeps the
// full frame rate (see frame_interval): its commits keep redrawing much of it, and keep
// coming as fast as its rate lets them. A cursor blinking or a spinner turning redraws a
// small part, and a page changing now and then doesn't keep coming.
static void note_damage(struct screen *sc, struct wlr_surface *surface, struct wlr_buffer *buffer) {
int n = 0;
const pixman_box32_t *r = pixman_region32_rectangles(&surface->buffer_damage, &n);
int64_t area = 0;
for (int i = 0; i < n; ++i) area += (int64_t)(r[i].x2 - r[i].x1) * (r[i].y2 - r[i].y1);
const bool big = area * 100 >= (int64_t)buffer->width * buffer->height * VIDEO_AREA;
const unsigned all = (1u << VIDEO_COMMITS) - 1;
sc->big = ((sc->big << 1) | big) & all;
const uint32_t t = now_ms();
const uint32_t oldest = sc->commit_ms[sc->commit_at]; // the commit VIDEO_COMMITS ago
sc->commit_ms[sc->commit_at] = t;
sc->commit_at = (sc->commit_at + 1) % VIDEO_COMMITS;
if (sc->big == all && t - oldest <= VIDEO_COMMITS * 1000 / VIDEO_HZ) sc->video_until = t + VIDEO_HOLD;
}
static void screen_commit(struct wl_listener *l, void *data) {
struct screen *sc = wl_container_of(l, sc, commit);
struct wlr_xdg_surface *xdg = sc->toplevel->base;
@@ -172,6 +216,7 @@ static void screen_commit(struct wl_listener *l, void *data) {
if (!buffer) return;
sc->frame_pending = true;
++sc->commits;
note_damage(sc, xdg->surface, buffer);
sc->buffer_width = buffer->width, sc->buffer_height = buffer->height;
if (buffer == sc->held) return;
struct wlr_dmabuf_attributes a;
@@ -381,12 +426,64 @@ static void keys_update(struct server *s) {
offsetof(struct sockaddr_un, sun_path) + 1 + sizeof name - 1);
}
// Every ~11 ms (90 Hz): SteamVR events, and frame callbacks for screens that committed. While
// Frametop is paused for a VR game (everything hidden), every 100 ms, and the frame callbacks
// once a second: KWin draws a screen only after its callback, and its apps wait for theirs, so
// the desktop hardly draws until it's resumed.
static int tick(void *data) {
// How often a screen gets its frame callback (ms; 0 every tick). KWin draws a screen only
// after its callback, and its apps wait for theirs, so this is the screen's frame rate:
// full where you look (ft_vr_screen_attention) and for a video, lower for the rest of what
// you see, and about once a second for what you don't (hidden, behind you, paused for a VR
// game). A screen nothing changes on costs nothing at any rate: KWin commits only when
// something on it moved. A remote viewer ("watch") sees every screen at full rate.
static uint32_t frame_interval(struct server *s, struct screen *sc, uint32_t t) {
if ((int32_t)(s->watch_until - t) > 0) return 0;
enum ft_attention a = ft_vr_screen_attention(sc->index);
if (a == FT_IN_VIEW && (int32_t)(sc->video_until - t) > 0) a = FT_FOCUSED;
if (a != FT_FOCUSED && t - s->typed_ms < 1500 &&
s->seat->keyboard_state.focused_surface == sc->toplevel->base->surface)
a = FT_FOCUSED; // typing on it while looking elsewhere
const int hz = s->rate[a];
return hz > 0 ? 1000 / hz : 0;
}
static int64_t mono_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000000000LL + ts.tv_nsec;
}
// Ticks come once per display frame, phase_ms after its vsync: KWin gets its frame callbacks
// early in the frame and has the rest of it to draw before vrcompositor takes the panels.
// The vsync is read from SteamVR once a second, and the ticks run at absolute times between
// reads, so they keep their place (a timer set again after each tick, as before, slid
// through the frame and came about 85 times a second at 90 Hz). Without SteamVR's timing
// they still come at the display's rate (90 Hz until known). While paused, every 100 ms.
static void schedule(struct server *s) {
const int64_t now = mono_ns();
int64_t next;
if (ft_vr_paused()) {
next = now + 100000000LL;
} else {
double since, hz;
if (now - s->synced_ns >= 1000000000LL) {
s->synced_ns = now;
if (ft_vr_vsync(&since, &hz)) {
const int64_t period = (int64_t)(1e9 / hz);
if (llabs(period - s->period_ns) > 100000) wlr_log(WLR_INFO, "display at %.1f Hz", hz);
s->period_ns = period;
s->base_ns = now - (int64_t)(since * 1e9) + (int64_t)(s->phase_ms * 1e6);
while (s->base_ns > now) s->base_ns -= s->period_ns;
}
}
next = s->base_ns + ((now - s->base_ns) / s->period_ns + 1) * s->period_ns;
}
struct itimerspec its = {.it_value = {.tv_sec = next / 1000000000LL, .tv_nsec = next % 1000000000LL}};
timerfd_settime(s->tick_fd, TFD_TIMER_ABSTIME, &its, NULL);
}
// Each tick: SteamVR events, and frame callbacks for screens that committed and are due.
static int tick(int fd, uint32_t mask, void *data) {
struct server *s = data;
uint64_t expirations;
const ssize_t got = read(fd, &expirations, sizeof expirations); // clears it; how many doesn't matter
(void)got;
ft_vr_poll(handle_vr_event, s);
if (++s->ticks % 9 == 0) keys_update(s);
if (s->kb_close_at && s->ticks >= s->kb_close_at) {
@@ -398,15 +495,17 @@ static int tick(void *data) {
}
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
const bool paused = ft_vr_paused();
for (int i = 0; i < MAX_SCREENS && (!paused || s->ticks % 10 == 0); ++i) {
const uint32_t t = now_ms();
// Due within half a tick counts as due, so 15 Hz is every 6th tick at 90 Hz, not 7th.
const uint32_t slack = ft_vr_paused() ? 50 : (uint32_t)(s->period_ns / 2000000);
for (int i = 0; i < MAX_SCREENS; ++i) {
struct screen *sc = s->screens[i];
if (sc && sc->frame_pending) {
sc->frame_pending = false;
wlr_surface_send_frame_done(sc->toplevel->base->surface, &now);
}
if (!sc || !sc->frame_pending || t - sc->frame_sent + slack < frame_interval(s, sc, t)) continue;
sc->frame_pending = false;
sc->frame_sent = t;
wlr_surface_send_frame_done(sc->toplevel->base->surface, &now);
}
wl_event_source_timer_update(s->tick, paused ? 100 : 11);
schedule(s);
return 0;
}
@@ -431,6 +530,7 @@ static bool key_held(const struct wlr_keyboard *kb, uint32_t code) {
// One key to the focused screen, through the seat's keyboard so its xkb state and
// modifiers stay right.
static void send_key(struct server *s, uint32_t code, int pressed) {
s->typed_ms = now_ms();
struct wlr_keyboard_key_event ev = {
.time_msec = now_ms(), .keycode = code, .update_state = true,
.state = pressed ? WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED};
@@ -584,6 +684,38 @@ static int control_readable(int fd, uint32_t mask, void *data) {
handle_vr_event(&e, s);
snprintf(reply, sizeof reply, "ok");
}
} else if (sscanf(buf, "watch %d", &value) == 1) {
// A remote viewer is watching for that many seconds (vnc-bridge.sh renews it).
s->watch_until = now_ms() + (uint32_t)(value < 0 ? 0 : value > 60 ? 60 : value) * 1000;
snprintf(reply, sizeof reply, "ok");
} else if (sscanf(buf, "rates %d %d %d", &index, &w, &h) == 3) {
// Frame rates in Hz: focused, in view, hidden (0: every display frame).
if (index < 0 || w < 0 || h < 0 || index > 240 || w > 240 || h > 240) {
snprintf(reply, sizeof reply, "error rates <focused> <in view> <hidden> (Hz, 0 full)");
} else {
s->rate[FT_FOCUSED] = index, s->rate[FT_IN_VIEW] = w, s->rate[FT_HIDDEN] = h;
wlr_log(WLR_INFO, "frame rates: focused %d, in view %d, hidden %d (0 full)", index, w, h);
snprintf(reply, sizeof reply, "ok");
}
} else if (sscanf(buf, "phase %lf", &scale) == 1) {
// Ticks this long after the vsync (ms), for tuning.
if (!(scale >= 0 && scale < 20)) snprintf(reply, sizeof reply, "error phase <ms after vsync>");
else s->phase_ms = scale, s->synced_ns = 0, snprintf(reply, sizeof reply, "ok");
} else if (strcmp(buf, "rates?") == 0) {
// "ok <focused> <in view> <hidden> <display Hz> <phase ms> <watched>" and a line per
// screen: "<screen> hidden|view|focused <ms between frames> <video 0|1>".
static const char *names[] = {"hidden", "view", "focused"};
const uint32_t t = now_ms();
int at = snprintf(reply, sizeof reply, "ok %d %d %d %.1f %.1f %d", s->rate[FT_FOCUSED],
s->rate[FT_IN_VIEW], s->rate[FT_HIDDEN], 1e9 / s->period_ns, s->phase_ms,
(int32_t)(s->watch_until - t) > 0);
for (int i = 0; i < MAX_SCREENS && at < (int)sizeof reply; ++i) {
struct screen *sc = s->screens[i];
if (!sc) continue;
at += snprintf(reply + at, sizeof reply - at, "\n%d %s %u %d", i + 1,
names[ft_vr_screen_attention(i)], frame_interval(s, sc, t),
(int32_t)(sc->video_until - t) > 0);
}
} else if (strcmp(buf, "toplevels") == 0) {
int n = 0, at = 0;
for (int i = 0; i < MAX_SCREENS; ++i) n += s->screens[i] != NULL;
@@ -666,6 +798,9 @@ int main(int argc, char **argv) {
s.controller_click.threshold = 8;
for (int i = 0; i < MAX_SCREENS; ++i) s.scale[i] = 1;
s.kb_screen = -1;
s.rate[FT_FOCUSED] = 0, s.rate[FT_IN_VIEW] = 15, s.rate[FT_HIDDEN] = 1;
s.period_ns = 1000000000LL / 90;
s.phase_ms = 1;
const char *socket_name = "ft-screens-0", *control_name = "ft_screens";
char **command = NULL;
s.vr = true;
@@ -678,6 +813,11 @@ int main(int argc, char **argv) {
s.spares = atoi(argv[++i]);
} else if (strcmp(argv[i], "--no-vr") == 0) {
s.vr = false;
} else if (strcmp(argv[i], "--rates") == 0 && i + 1 < argc) {
if (sscanf(argv[++i], "%d,%d,%d", &s.rate[FT_FOCUSED], &s.rate[FT_IN_VIEW], &s.rate[FT_HIDDEN]) != 3) {
fprintf(stderr, "bad --rates %s (want FOCUSED,IN_VIEW,HIDDEN in Hz, 0 full)\n", argv[i]);
return 2;
}
} else if (strcmp(argv[i], "--screen") == 0 && i + 1 < argc && s.n_config < MAX_SCREENS) {
struct config *c = &s.config[s.n_config];
c->metres = 0;
@@ -693,7 +833,7 @@ int main(int argc, char **argv) {
} else {
fprintf(stderr,
"usage: %s [--socket NAME] [--control NAME] [--no-vr] [--screen WxH@METRES]... "
"[--spares N] [-- COMMAND ARGS...]\n",
"[--spares N] [--rates F,V,H] [-- COMMAND ARGS...]\n",
argv[0]);
return 2;
}
@@ -745,8 +885,10 @@ int main(int argc, char **argv) {
s.relay_fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
wl_event_loop_add_fd(s.loop, control, WL_EVENT_READABLE, control_readable, &s);
s.tick = wl_event_loop_add_timer(s.loop, tick, &s);
wl_event_source_timer_update(s.tick, 11);
s.tick_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
s.base_ns = mono_ns();
s.tick = wl_event_loop_add_fd(s.loop, s.tick_fd, WL_EVENT_READABLE, tick, &s);
schedule(&s);
wl_event_loop_add_signal(s.loop, SIGINT, stop, &s);
wl_event_loop_add_signal(s.loop, SIGTERM, stop, &s);
wl_event_loop_add_signal(s.loop, SIGCHLD, child_exited, &s);
@@ -761,6 +903,18 @@ int main(int argc, char **argv) {
}
}
// Ahead of the desktop's programs for the CPU, after the session started (it would
// inherit it): KWin waits for our ticks to draw. SteamOS lets users go to nice -8
// (RLIMIT_NICE 28) once they raise their soft limit; without that, as before.
if (s.vr) {
struct rlimit rl;
if (getrlimit(RLIMIT_NICE, &rl) == 0 && rl.rlim_cur < rl.rlim_max) {
rl.rlim_cur = rl.rlim_max;
setrlimit(RLIMIT_NICE, &rl);
}
if (setpriority(PRIO_PROCESS, 0, -5) != 0) wlr_log(WLR_INFO, "can't raise our priority (nice -5); running at 0");
}
wl_display_run(s.display);
wlr_log(WLR_INFO, "stopping");
+88 -1
View File
@@ -46,7 +46,8 @@
// - paused ("pause on", from the input relay when Frametop pauses for a VR game,
// input/game_pause.py): every screen and floating window hides whatever the mode, the
// hotkey, or the dashboard says, and compositor.c gives KWin a frame callback once a
// second instead of 90 times, so KWin and its apps hardly draw. "pause off" undoes it.
// second, as for any hidden screen, so KWin and its apps hardly draw. "pause off" undoes
// it.
// - hand cutouts (handcut.cpp): where ft-hands (hands/) tracks a hand between an eye and a
// screen, that eye sees through the screen (to Room View). Only then is the screen
// drawn by us, into a side-by-side buffer (one half per eye); otherwise its client
@@ -291,6 +292,11 @@ struct Screen {
long resizeSent = 0; // g_tick of the last resize request (they're throttled)
int resizeW = 0, resizeH = 0; // ...and its size
std::map<int, Sub> subs;
// Attention (UpdateAttention): what ft_vr_screen_attention answers, and until when (ms)
// it stays focused or in view after the last reason for it.
ft_attention attention = FT_FOCUSED;
int64_t inputMs = INT64_MIN / 2; // the last pointer event on it or its controls
int64_t focusUntil = 0, viewUntil = 0;
double heightMetres() const {
if (floating && cropW > 0) return metres * cropH / cropW;
return width > 0 ? metres * height / width : metres * 9 / 16;
@@ -816,6 +822,66 @@ void UpdateVisibility() {
}
}
// Attention, for each screen's frame rate (compositor.c gives KWin frame callbacks at a
// rate for each level): focused while you look at the screen (within kFocusAngle of where
// your head points), a laser or the mouse is on it, or it's being carried; in view while
// any of it is within kViewAngle; hidden otherwise, or while it isn't shown. A level stays
// for a moment after its reason goes, so a glance away doesn't make it stutter, and goes up
// at once.
constexpr double kFocusAngle = 12, kViewAngle = 60; // degrees
constexpr int64_t kFocusLinger = 1500, kViewLinger = 500, kInputFocus = 1500; // ms
bool RayOnPlane(const Mat &p, const Mat &d, double *x, double *y);
int64_t NowMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count();
}
// The smallest angle between where the head points and the screen: to the point of its
// rectangle nearest where the head's ray meets its plane, its centre, and its corners. A
// curved screen counts as flat; the angles hardly differ.
double AngleToScreen(const Screen &s, const Mat &p, const Mat &head) {
const double hw = s.metres / 2, hh = s.heightMetres() / 2;
double f[3];
Column(head, 2, f); // the head's +Z points backward
auto angleTo = [&](double u, double v) {
double to[3];
for (int i = 0; i < 3; ++i) to[i] = p.m[i][3] + u * p.m[i][0] + v * p.m[i][1] - head.m[i][3];
const double len = std::sqrt(Dot3(to, to)) + 1e-9;
return std::acos(std::clamp(-Dot3(f, to) / len, -1.0, 1.0)) * 180 / M_PI;
};
double best = 180, x, y;
if (RayOnPlane(p, head, &x, &y)) best = angleTo(std::clamp(x, -hw, hw), std::clamp(y, -hh, hh));
for (double u : {-hw, 0.0, hw})
for (double v : {-hh, 0.0, hh}) best = std::min(best, angleTo(u, v));
return best;
}
void UpdateAttention() {
const int64_t now = NowMs();
Mat head;
const bool haveHead = DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head);
for (auto &[i, s] : g_screens) {
if (!s.visible) {
s.attention = FT_HIDDEN;
s.focusUntil = s.viewUntil = 0;
continue;
}
bool focus = s.drag != Drag::None || now - s.inputMs < kInputFocus, view = focus;
Mat p;
if (!haveHead) {
view = true; // nothing to go by
} else if (ScreenPose(s, &p)) {
const double a = AngleToScreen(s, p, head);
focus = focus || a <= kFocusAngle;
view = view || a <= kViewAngle;
}
if (focus) s.focusUntil = now + kFocusLinger;
if (view) s.viewUntil = now + kViewLinger;
s.attention = now < s.focusUntil ? FT_FOCUSED : now < s.viewUntil ? FT_IN_VIEW : FT_HIDDEN;
}
}
// Controllers' lasers on the screens (see the top): the flag follows the mode and whether a
// VR game runs.
@@ -1577,6 +1643,24 @@ int ft_vr_modifiers(uint32_t format, uint64_t *out, int max) {
bool ft_vr_screens_shown(void) { return g_vr && ModeVisible(); }
bool ft_vr_paused(void) { return g_paused; }
enum ft_attention ft_vr_screen_attention(int index) {
const auto it = g_screens.find(index);
return g_vr && it != g_screens.end() ? it->second.attention : FT_FOCUSED;
}
bool ft_vr_vsync(double *since, double *hz) {
if (!g_vr) return false;
float s = 0;
uint64_t frame = 0;
if (!vr::VRSystem()->GetTimeSinceLastVsync(&s, &frame)) return false;
vr::ETrackedPropertyError err = vr::TrackedProp_Success;
const float f = vr::VRSystem()->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd,
vr::Prop_DisplayFrequency_Float, &err);
if (err != vr::TrackedProp_Success || !(f >= 30 && f <= 240) || !(s >= 0 && s < 1)) return false;
*since = s, *hz = f;
return true;
}
} // extern "C"
namespace {
@@ -1735,6 +1819,7 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
auto panelEvent = [&](const vr::VREvent_t &ev, bool sub) {
ft_event e{};
e.screen = index;
if (ev.eventType != vr::VREvent_FocusLeave) s.inputMs = NowMs();
auto at = [&] { s.ToBuffer(ev.data.mouse.x, ev.data.mouse.y, &e.x, &e.y); };
switch (ev.eventType) {
case vr::VREvent_MouseMove:
@@ -1787,6 +1872,7 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
// The controls light up under a laser.
auto hover = [&](int k) {
const bool on = ev.eventType == vr::VREvent_MouseMove || ev.eventType == vr::VREvent_FocusEnter;
if (on) s.inputMs = NowMs();
if (!on && ev.eventType != vr::VREvent_FocusLeave) return;
if (s.hover[k] != on) s.hover[k] = on, ApplyAlpha(s);
};
@@ -1899,6 +1985,7 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
UpdateGame();
UpdateArrange();
UpdateVisibility();
UpdateAttention();
UpdateLasers();
UpdateControls();
UpdateGuides();
+8
View File
@@ -38,6 +38,14 @@ int ft_vr_modifiers(uint32_t format, uint64_t *out, int max);
bool ft_vr_screens_shown(void);
// Frametop is paused for a VR game ("pause on"): everything is hidden, and KWin slows down.
bool ft_vr_paused(void);
// How much of a screen you see, for its frame rate (compositor.c): hidden (or out of view),
// in view, or focused (you look at it, or a laser or the mouse is on it). Focused without
// SteamVR (--no-vr) or for an unknown screen.
enum ft_attention { FT_HIDDEN, FT_IN_VIEW, FT_FOCUSED };
enum ft_attention ft_vr_screen_attention(int index);
// The display's refresh rate and the time since its last vsync, in seconds. False without
// them (no SteamVR, or the headset isn't reporting).
bool ft_vr_vsync(double *since, double *hz);
// A panel for screen `index`, width in metres, placed in a row in front of the head.
void ft_vr_screen_create(int index, double metres, int count);
void ft_vr_screen_destroy(int index);