mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 07:00:14 +02:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64c4eec59f | ||
|
|
ff36356992 | ||
|
|
4d50739393 | ||
|
|
f60da63702 | ||
|
|
82d2107e1d | ||
|
|
12f2e844d5 | ||
|
|
ee2ce1a8d2 | ||
|
|
540d8c425c | ||
|
|
ba6ccdfbd1 | ||
|
|
77f28f0922 | ||
|
|
85532a54f3 | ||
|
|
3864741f53 |
No files matched your search
@@ -13,6 +13,8 @@ Two settings apps come with it: Frametop Display Settings for the screens, profi
|
|||||||
|
|
||||||
Frametop is an independent project, not made by or affiliated with Valve.
|
Frametop is an independent project, not made by or affiliated with Valve.
|
||||||
|
|
||||||
|
Join the [Frametop Discord](https://discord.gg/W3X9f7z3Bc) for questions, ideas, and help with your setup.
|
||||||
|
|
||||||
## Install on the headset
|
## Install on the headset
|
||||||
|
|
||||||
You need a Steam Frame with an internet connection, a keyboard (Bluetooth, or the on-screen one), and about 3 GB of free space.
|
You need a Steam Frame with an internet connection, a keyboard (Bluetooth, or the on-screen one), and about 3 GB of free space.
|
||||||
@@ -139,7 +141,7 @@ In a terminal on the headset, run:
|
|||||||
cd ~/frametop && scripts/report.sh
|
cd ~/frametop && scripts/report.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
This writes `frametop-report-<date>.txt` with version numbers, service states, settings, and recent logs. Bluetooth addresses and the headset's serial number are masked. Then [open an issue](https://github.com/DeeJanuz/frametop/issues), describe what you did, what you expected, and what happened, and attach the file.
|
This writes `frametop-report-<date>.txt` with version numbers, service states, settings, and recent logs. Bluetooth addresses and the headset's serial number are masked. Then [open an issue](https://github.com/DeeJanuz/frametop/issues), describe what you did, what you expected, and what happened, and attach the file. Quick questions can go to [Discord](https://discord.gg/W3X9f7z3Bc) instead.
|
||||||
|
|
||||||
## Update
|
## Update
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Controller desktop click stability
|
||||||
|
|
||||||
|
Trigger presses reach KDE immediately, but controller motion within 8 logical
|
||||||
|
pixels of the press stays at that position until release. Releasing without a motion outside this
|
||||||
|
zone delivers the click at the original position, even if the hand moved during
|
||||||
|
release. Moving outside the zone begins a normal drag immediately; returning to
|
||||||
|
the zone does not turn it back into a click. There is no hold-duration timer.
|
||||||
|
|
||||||
|
This filters overlay pointer content events on desktop monitors only, and only
|
||||||
|
presses from hand controllers start it. The 3D mouse (whose laser comes from the
|
||||||
|
`ft_pointer` virtual controller), SteamVR UI, separate screen grab bars and
|
||||||
|
floating-app title-bar carrying are unaffected. Multi-button gestures keep their
|
||||||
|
existing behavior. A motion onto another desktop monitor starts a drag;
|
||||||
|
cross-monitor motion is not stabilized.
|
||||||
|
|
||||||
|
CLI (runtime preferences, reset to 8 on desktop restart):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
input/ft-clickctl status
|
||||||
|
input/ft-clickctl threshold 8
|
||||||
|
input/ft-clickctl threshold 0 # disable without a restart
|
||||||
|
```
|
||||||
|
|
||||||
|
Thresholds are 0–64 logical pixels, normalized to each panel's KDE scale.
|
||||||
|
Status reports held state, suppressed motions, stabilized clicks and drags.
|
||||||
|
Changing the threshold while a controller button is held is refused.
|
||||||
|
|
||||||
|
This is a separate contribution from desktop mouse/controller ownership. Its
|
||||||
|
hardware validation must check small controls, intentional text selection,
|
||||||
|
long presses, cross-monitor dragging and simultaneous mouse use. The existing
|
||||||
|
renderer laser remains tracked; this change stabilizes desktop input rather
|
||||||
|
than smoothing the visual laser. Default threshold is a starting point to test.
|
||||||
|
|
||||||
|
Run `scripts/test-controller-click.sh` for the isolated gesture-state tests.
|
||||||
+1
-1
@@ -175,7 +175,7 @@ Flatpak apps need `XDG_DATA_DIRS` to include Flatpak's exports, or Plasma opens
|
|||||||
|
|
||||||
The private runtime directory also moves the session's document portal to `$XDG_RUNTIME_DIR/frametop/doc`, and that broke saving and uploading in Flatpak apps. The file picker (xdg-desktop-portal 1.18.4 on SteamOS) gives a sandboxed app the host path of the file it picked, `/run/user/1000/frametop/doc/ID/NAME`. Inside the sandbox the portal is at `/run/flatpak/doc`, and `/run/user/1000` is a private per-app folder (`.flatpak/APP/xdg-run` in the runtime directory). So Brave created the missing folder there, "finished" the download into it, and the file vanished when the session cleaned up. The session script now links that path to `/run/flatpak/doc` in each installed app's folder before Plasma starts. Upstream xdg-desktop-portal fixed this after 1.22.1 (commit `69ba5e1`) by handing Flatpak apps `/run/flatpak/doc` paths, after which the links go unused.
|
The private runtime directory also moves the session's document portal to `$XDG_RUNTIME_DIR/frametop/doc`, and that broke saving and uploading in Flatpak apps. The file picker (xdg-desktop-portal 1.18.4 on SteamOS) gives a sandboxed app the host path of the file it picked, `/run/user/1000/frametop/doc/ID/NAME`. Inside the sandbox the portal is at `/run/flatpak/doc`, and `/run/user/1000` is a private per-app folder (`.flatpak/APP/xdg-run` in the runtime directory). So Brave created the missing folder there, "finished" the download into it, and the file vanished when the session cleaned up. The session script now links that path to `/run/flatpak/doc` in each installed app's folder before Plasma starts. Upstream xdg-desktop-portal fixed this after 1.22.1 (commit `69ba5e1`) by handing Flatpak apps `/run/flatpak/doc` paths, after which the links go unused.
|
||||||
|
|
||||||
A podman container's monitor process (conmon) stays in the cgroup of whatever started the container, and `distrobox enter` starts it on demand. When a Frametop service happened to start the `dev` container, stopping that service stopped the container and everything in it, including the desktop's compositor. `scripts/container-up.sh` starts the container in a systemd scope of its own before anything enters it.
|
A podman container's monitor process (conmon) stays in the cgroup of whatever started the container, and `distrobox enter` starts it on demand. When a Frametop service happened to start the `dev` container, stopping that service stopped the container and everything in it, including the desktop's compositor. `scripts/container-up.sh` starts the container in a systemd scope of its own before anything enters it. It then waits for distrobox-init to log `container_setup_done`, as `distrobox enter` does only for containers it starts itself. A new container's first start takes a minute or more (it installs distrobox's dependencies and sets up passwordless sudo), and an install that entered right away met a sudo password prompt with no terminal to answer it ([#9](https://github.com/DeeJanuz/frametop/issues/9)).
|
||||||
|
|
||||||
Program names stay within 15 characters, because Linux truncates process names there and the scripts find programs with `pgrep -x` and `pkill -x`. That's why the prefix is `ft-`.
|
Program names stay within 15 characters, because Linux truncates process names there and the scripts find programs with `pgrep -x` and `pkill -x`. That's why the prefix is `ft-`.
|
||||||
|
|
||||||
|
|||||||
+35
-18
@@ -479,9 +479,24 @@ int main(int argc, char **argv) {
|
|||||||
double lastEmit = 0;
|
double lastEmit = 0;
|
||||||
int actionErrors = 0;
|
int actionErrors = 0;
|
||||||
vr::EVRInputError lastActionError = vr::VRInputError_None;
|
vr::EVRInputError lastActionError = vr::VRInputError_None;
|
||||||
|
// During a VR game, SteamVR's gaze action is left alone. With ft-gaze reading the eyes, SteamVR
|
||||||
|
// restarted its eye tracker every 10 s or so in a game, as if the headset came off, and each
|
||||||
|
// restart took input focus from the game: Beat Saber paused (PR #13). Of what ft-gaze reads,
|
||||||
|
// only the action reaches SteamVR (the mmap and our tracker are files), so gaze still works
|
||||||
|
// over the dashboard. Games are told apart the way ft-screens does it, by the scene app.
|
||||||
|
bool inGame = false;
|
||||||
|
double nextGameCheck = 0;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const double now = NowRaw();
|
const double now = NowRaw();
|
||||||
|
if (now >= nextGameCheck) {
|
||||||
|
nextGameCheck = now + 0.5;
|
||||||
|
const bool game = vr::VRApplications()->GetCurrentSceneProcessId() != 0;
|
||||||
|
if (game != inGame)
|
||||||
|
std::fprintf(stderr, "ft-gaze: %s\n",
|
||||||
|
game ? "a VR game is running: SteamVR's gaze action left alone" : "the VR game ended");
|
||||||
|
inGame = game;
|
||||||
|
}
|
||||||
vr::TrackedDevicePose_t hp;
|
vr::TrackedDevicePose_t hp;
|
||||||
sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, &hp, 1);
|
sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, &hp, 1);
|
||||||
if (hp.bPoseIsValid) history.Add(now, hp.mDeviceToAbsoluteTracking);
|
if (hp.bPoseIsValid) history.Add(now, hp.mDeviceToAbsoluteTracking);
|
||||||
@@ -502,24 +517,26 @@ int main(int argc, char **argv) {
|
|||||||
// SteamVR's action: a room-space origin and fixation point, turned into the head
|
// SteamVR's action: a room-space origin and fixation point, turned into the head
|
||||||
// frame so every source reports the same kind of angles.
|
// frame so every source reports the same kind of angles.
|
||||||
std::string action = "{\"ok\":0}";
|
std::string action = "{\"ok\":0}";
|
||||||
vr::VRActiveActionSet_t active{};
|
if (!inGame) {
|
||||||
active.ulActionSet = set;
|
vr::VRActiveActionSet_t active{};
|
||||||
active.nPriority = vr::k_nActionSetOverlayGlobalPriorityMin;
|
active.ulActionSet = set;
|
||||||
input->UpdateActionState(&active, sizeof active, 1);
|
active.nPriority = vr::k_nActionSetOverlayGlobalPriorityMin;
|
||||||
vr::VREyeTrackingData_t e{};
|
input->UpdateActionState(&active, sizeof active, 1);
|
||||||
const vr::EVRInputError ae =
|
vr::VREyeTrackingData_t e{};
|
||||||
input->GetEyeTrackingDataRelativeToNow(gaze, vr::TrackingUniverseStanding, 0, &e, sizeof e);
|
const vr::EVRInputError ae =
|
||||||
if (ae == vr::VRInputError_None && e.bActive && e.bValid) {
|
input->GetEyeTrackingDataRelativeToNow(gaze, vr::TrackingUniverseStanding, 0, &e, sizeof e);
|
||||||
const Vec3 o{e.vGazeOrigin.v[0], e.vGazeOrigin.v[1], e.vGazeOrigin.v[2]};
|
if (ae == vr::VRInputError_None && e.bActive && e.bValid) {
|
||||||
const Vec3 t{e.vGazeTarget.v[0], e.vGazeTarget.v[1], e.vGazeTarget.v[2]};
|
const Vec3 o{e.vGazeOrigin.v[0], e.vGazeOrigin.v[1], e.vGazeOrigin.v[2]};
|
||||||
const Vec3 dHead = RotateInverse(headNow, Normalize(t - o));
|
const Vec3 t{e.vGazeTarget.v[0], e.vGazeTarget.v[1], e.vGazeTarget.v[2]};
|
||||||
char extra[96];
|
const Vec3 dHead = RotateInverse(headNow, Normalize(t - o));
|
||||||
std::snprintf(extra, sizeof extra, "\"tracked\":%d,\"dist\":%.3f,", int(e.bTracked), Length(t - o));
|
char extra[96];
|
||||||
action = SrcJson(list, headNow, dHead, extra);
|
std::snprintf(extra, sizeof extra, "\"tracked\":%d,\"dist\":%.3f,", int(e.bTracked), Length(t - o));
|
||||||
} else if (ae != lastActionError || (verbose && ++actionErrors % 90 == 1)) {
|
action = SrcJson(list, headNow, dHead, extra);
|
||||||
std::fprintf(stderr, "ft-gaze: action: error %d active %d valid %d\n", int(ae), int(e.bActive),
|
} else if (ae != lastActionError || (verbose && ++actionErrors % 90 == 1)) {
|
||||||
int(e.bValid));
|
std::fprintf(stderr, "ft-gaze: action: error %d active %d valid %d\n", int(ae), int(e.bActive),
|
||||||
lastActionError = ae;
|
int(e.bValid));
|
||||||
|
lastActionError = ae;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string m1 = "{\"ok\":0}", m2 = m1, left = m1, right = m1, eye = "null";
|
std::string m1 = "{\"ok\":0}", m2 = m1, left = m1, right = m1, eye = "null";
|
||||||
|
|||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Controller desktop click stability over local IPC; no SteamVR client."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import socket
|
||||||
|
|
||||||
|
def request(command):
|
||||||
|
with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as client:
|
||||||
|
client.bind(''); client.settimeout(2)
|
||||||
|
client.sendto(command.encode(), '\0ft_screens')
|
||||||
|
response=client.recv(8192).decode()
|
||||||
|
if response.startswith('error'): raise RuntimeError(response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser=argparse.ArgumentParser(description=__doc__)
|
||||||
|
sub=parser.add_subparsers(dest='command', required=True)
|
||||||
|
sub.add_parser('status')
|
||||||
|
sub.add_parser('threshold').add_argument('pixels', type=float)
|
||||||
|
args=parser.parse_args()
|
||||||
|
try:
|
||||||
|
if args.command == 'threshold':
|
||||||
|
if not math.isfinite(args.pixels) or not 0 <= args.pixels <= 64:
|
||||||
|
raise ValueError('Threshold must be 0–64 logical pixels; 0 disables stabilization')
|
||||||
|
if request(f'controller-click {args.pixels:g}') != 'ok': raise RuntimeError('Threshold rejected')
|
||||||
|
print(json.dumps(json.loads(request('controller-click?')), indent=2))
|
||||||
|
except (OSError, ValueError, RuntimeError) as error: parser.exit(1, str(error)+'\n')
|
||||||
|
if __name__ == '__main__': main()
|
||||||
@@ -1139,6 +1139,11 @@ def main():
|
|||||||
# New here: a new device, or one that came back in the same place.
|
# New here: a new device, or one that came back in the same place.
|
||||||
for old in [n for n in nodes.values() if n.path == path]:
|
for old in [n for n in nodes.values() if n.path == path]:
|
||||||
drop(old, "replaced by a new device node")
|
drop(old, "replaced by a new device node")
|
||||||
|
# A new node is root's alone until udev gives it to the input group, a moment
|
||||||
|
# after it appears. Opened in that gap, it would fail and never be tried
|
||||||
|
# again: leave it for the next scan instead.
|
||||||
|
if not os.access(path, os.R_OK):
|
||||||
|
continue
|
||||||
seen[path] = ino
|
seen[path] = ino
|
||||||
node = probe(path)
|
node = probe(path)
|
||||||
if node and node.volume_keys and not take_volume(node):
|
if node and node.volume_keys and not take_volume(node):
|
||||||
|
|||||||
+20
-1
@@ -58,6 +58,7 @@
|
|||||||
#include <wlr/util/log.h>
|
#include <wlr/util/log.h>
|
||||||
|
|
||||||
#include "vr.h"
|
#include "vr.h"
|
||||||
|
#include "controller-click.h"
|
||||||
|
|
||||||
#define MAX_SCREENS 24 // screens and spare outputs
|
#define MAX_SCREENS 24 // screens and spare outputs
|
||||||
|
|
||||||
@@ -104,6 +105,7 @@ struct server {
|
|||||||
struct wl_list buffers; // tracked_buffer
|
struct wl_list buffers; // tracked_buffer
|
||||||
struct wl_event_source *tick;
|
struct wl_event_source *tick;
|
||||||
struct screen *pointer_focus;
|
struct screen *pointer_focus;
|
||||||
|
struct ft_controller_click controller_click;
|
||||||
pid_t child;
|
pid_t child;
|
||||||
// Where typing goes: the screens after a click on one, Steam after a click on another
|
// Where typing goes: the screens after a click on one, Steam after a click on another
|
||||||
// panel. The input relay grabs the keyboards while it's the screens (see keys_update).
|
// panel. The input relay grabs the keyboards while it's the screens (see keys_update).
|
||||||
@@ -292,6 +294,11 @@ static void handle_vr_event(const struct ft_event *e, void *data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e->screen < 0 || e->screen >= MAX_SCREENS || !s->screens[e->screen]) return;
|
if (e->screen < 0 || e->screen >= MAX_SCREENS || !s->screens[e->screen]) return;
|
||||||
|
struct ft_event filtered = *e;
|
||||||
|
if (e->screen < s->n_config &&
|
||||||
|
!ft_controller_click_filter(&s->controller_click, &filtered, s->scale[e->screen])) return;
|
||||||
|
e = &filtered;
|
||||||
|
if (e->screen < 0 || e->screen >= MAX_SCREENS || !s->screens[e->screen]) return;
|
||||||
struct screen *sc = s->screens[e->screen];
|
struct screen *sc = s->screens[e->screen];
|
||||||
struct wlr_surface *surface = sc->toplevel->base->surface;
|
struct wlr_surface *surface = sc->toplevel->base->surface;
|
||||||
const uint32_t t = now_ms();
|
const uint32_t t = now_ms();
|
||||||
@@ -514,7 +521,18 @@ static int control_readable(int fd, uint32_t mask, void *data) {
|
|||||||
unsigned code;
|
unsigned code;
|
||||||
int value, index, w, h;
|
int value, index, w, h;
|
||||||
double scale;
|
double scale;
|
||||||
if (sscanf(buf, "size %d %d %d", &index, &w, &h) == 3) {
|
char tail;
|
||||||
|
if (strcmp(buf, "controller-click?") == 0) {
|
||||||
|
snprintf(reply, sizeof reply,
|
||||||
|
"{\"supported\":true,\"threshold\":%.3f,\"held\":%s,\"dragging\":%s,\"suppressedMotions\":%lu,\"clicks\":%lu,\"drags\":%lu}",
|
||||||
|
s->controller_click.threshold, s->controller_click.held ? "true" : "false",
|
||||||
|
s->controller_click.dragging ? "true" : "false", s->controller_click.suppressed,
|
||||||
|
s->controller_click.clicks, s->controller_click.drags);
|
||||||
|
} else if (sscanf(buf, "controller-click %lf %c", &scale, &tail) == 1) {
|
||||||
|
if (!isfinite(scale) || scale < 0 || scale > 64 || s->controller_click.buttons)
|
||||||
|
snprintf(reply, sizeof reply, "error threshold or held controller button");
|
||||||
|
else { s->controller_click.threshold = scale; snprintf(reply, sizeof reply, "ok"); }
|
||||||
|
} else if (sscanf(buf, "size %d %d %d", &index, &w, &h) == 3) {
|
||||||
// A new resolution for a screen, live: KWin resizes the screen to match. (KWin makes
|
// A new resolution for a screen, live: KWin resizes the screen to match. (KWin makes
|
||||||
// it this size times its scale; ft-floatd sends spares' sizes divided by theirs.)
|
// it this size times its scale; ft-floatd sends spares' sizes divided by theirs.)
|
||||||
const int min_w = index - 1 < s->n_config ? 320 : 64, min_h = index - 1 < s->n_config ? 200 : 64;
|
const int min_w = index - 1 < s->n_config ? 320 : 64, min_h = index - 1 < s->n_config ? 200 : 64;
|
||||||
@@ -641,6 +659,7 @@ static bool setup_dmabuf(struct server *s) {
|
|||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
struct server s = {0};
|
struct server s = {0};
|
||||||
|
s.controller_click.threshold = 8;
|
||||||
for (int i = 0; i < MAX_SCREENS; ++i) s.scale[i] = 1;
|
for (int i = 0; i < MAX_SCREENS; ++i) s.scale[i] = 1;
|
||||||
s.kb_screen = -1;
|
s.kb_screen = -1;
|
||||||
const char *socket_name = "ft-screens-0", *control_name = "ft_screens";
|
const char *socket_name = "ft-screens-0", *control_name = "ft_screens";
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#ifndef FT_CONTROLLER_CLICK_H
|
||||||
|
#define FT_CONTROLLER_CLICK_H
|
||||||
|
#include <math.h>
|
||||||
|
#include <linux/input-event-codes.h>
|
||||||
|
#include "vr.h"
|
||||||
|
struct ft_controller_click {
|
||||||
|
bool held, dragging;
|
||||||
|
int screen;
|
||||||
|
uint32_t buttons;
|
||||||
|
double x, y, threshold, radius;
|
||||||
|
unsigned long suppressed, clicks, drags;
|
||||||
|
};
|
||||||
|
// Hold the desktop position at press time until movement exceeds a logical-pixel
|
||||||
|
// radius. No timer or delayed button-down. Only a hand controller's press starts
|
||||||
|
// it: the 3D mouse drives a laser too, and its short drags must stay drags.
|
||||||
|
static inline bool ft_controller_click_filter(struct ft_controller_click *c,
|
||||||
|
struct ft_event *e, double scale) {
|
||||||
|
if (e->type == FT_BUTTON && e->button >= BTN_LEFT && e->button < BTN_LEFT+8) {
|
||||||
|
uint32_t bit = 1u << (e->button-BTN_LEFT);
|
||||||
|
if (e->pressed) c->buttons |= bit; else c->buttons &= ~bit;
|
||||||
|
}
|
||||||
|
if (e->type == FT_BUTTON && e->pressed) {
|
||||||
|
if (e->button == BTN_LEFT && e->controller && !c->held && c->threshold > 0) {
|
||||||
|
c->held = true; c->dragging = false; c->screen = e->screen;
|
||||||
|
c->x = e->x; c->y = e->y; c->radius = c->threshold * scale;
|
||||||
|
} else if (e->button != BTN_LEFT) {
|
||||||
|
c->held = false; // Multi-button gestures retain their usual semantics.
|
||||||
|
}
|
||||||
|
} else if (e->type == FT_MOTION && c->held && !c->dragging) {
|
||||||
|
if (e->screen == c->screen && hypot(e->x-c->x, e->y-c->y) <= c->radius) {
|
||||||
|
++c->suppressed;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
c->dragging = true; ++c->drags;
|
||||||
|
} else if (e->type == FT_BUTTON && e->button == BTN_LEFT && !e->pressed && c->held) {
|
||||||
|
if (!c->dragging) {
|
||||||
|
e->screen = c->screen; e->x = c->x; e->y = c->y;
|
||||||
|
++c->clicks;
|
||||||
|
}
|
||||||
|
c->held = false; c->dragging = false;
|
||||||
|
} else if (e->type == FT_LEAVE && c->held) {
|
||||||
|
return false; // Preserve the implicit grab through tiny edge excursions.
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
+124
-24
@@ -279,6 +279,9 @@ PFNEGLCREATEIMAGEKHRPROC pCreateImage;
|
|||||||
PFNEGLDESTROYIMAGEKHRPROC pDestroyImage;
|
PFNEGLDESTROYIMAGEKHRPROC pDestroyImage;
|
||||||
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pImageTargetTexture;
|
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pImageTargetTexture;
|
||||||
PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC pImageTargetRenderbuffer;
|
PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC pImageTargetRenderbuffer;
|
||||||
|
PFNEGLCREATESYNCKHRPROC pCreateSync;
|
||||||
|
PFNEGLDESTROYSYNCKHRPROC pDestroySync;
|
||||||
|
PFNEGLCLIENTWAITSYNCKHRPROC pClientWaitSync;
|
||||||
|
|
||||||
const char *kVertex = R"(
|
const char *kVertex = R"(
|
||||||
attribute vec2 pos; // the unit square
|
attribute vec2 pos; // the unit square
|
||||||
@@ -400,7 +403,11 @@ bool Renderer::Init(const std::vector<uint64_t> &modifiers, std::function<void(c
|
|||||||
pImageTargetTexture = reinterpret_cast<PFNGLEGLIMAGETARGETTEXTURE2DOESPROC>(eglGetProcAddress("glEGLImageTargetTexture2DOES"));
|
pImageTargetTexture = reinterpret_cast<PFNGLEGLIMAGETARGETTEXTURE2DOESPROC>(eglGetProcAddress("glEGLImageTargetTexture2DOES"));
|
||||||
pImageTargetRenderbuffer = reinterpret_cast<PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC>(
|
pImageTargetRenderbuffer = reinterpret_cast<PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC>(
|
||||||
eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES"));
|
eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES"));
|
||||||
if (!gbm_ || !pGetPlatformDisplay || !pCreateImage || !pImageTargetTexture || !pImageTargetRenderbuffer) {
|
pCreateSync = reinterpret_cast<PFNEGLCREATESYNCKHRPROC>(eglGetProcAddress("eglCreateSyncKHR"));
|
||||||
|
pDestroySync = reinterpret_cast<PFNEGLDESTROYSYNCKHRPROC>(eglGetProcAddress("eglDestroySyncKHR"));
|
||||||
|
pClientWaitSync = reinterpret_cast<PFNEGLCLIENTWAITSYNCKHRPROC>(eglGetProcAddress("eglClientWaitSyncKHR"));
|
||||||
|
if (!gbm_ || !pGetPlatformDisplay || !pCreateImage || !pImageTargetTexture || !pImageTargetRenderbuffer ||
|
||||||
|
!pCreateSync || !pDestroySync || !pClientWaitSync) {
|
||||||
std::fprintf(stderr, "handcut: GBM or EGL extensions missing\n");
|
std::fprintf(stderr, "handcut: GBM or EGL extensions missing\n");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -451,6 +458,9 @@ void Renderer::Forget(const void *key) {
|
|||||||
glDeleteTextures(1, &it->second.tex);
|
glDeleteTextures(1, &it->second.tex);
|
||||||
pDestroyImage(EGLDisplay(dpy_), EGLImageKHR(it->second.image));
|
pDestroyImage(EGLDisplay(dpy_), EGLImageKHR(it->second.image));
|
||||||
imported_.erase(it);
|
imported_.erase(it);
|
||||||
|
for (auto &[k, r] : rings_) // a new buffer at the same address isn't this one
|
||||||
|
for (Output &o : r.out)
|
||||||
|
if (o.key == key) o.drawn = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Renderer::MakeOutput(Output &o, int w, int h) {
|
bool Renderer::MakeOutput(Output &o, int w, int h) {
|
||||||
@@ -489,6 +499,7 @@ bool Renderer::MakeOutput(Output &o, int w, int h) {
|
|||||||
|
|
||||||
void Renderer::FreeOutput(Output &o) {
|
void Renderer::FreeOutput(Output &o) {
|
||||||
if (o.bo && released_) released_(&o);
|
if (o.bo && released_) released_(&o);
|
||||||
|
if (o.fence) pDestroySync(EGLDisplay(dpy_), EGLSyncKHR(o.fence));
|
||||||
if (o.fbo) glDeleteFramebuffers(1, &o.fbo);
|
if (o.fbo) glDeleteFramebuffers(1, &o.fbo);
|
||||||
if (o.rb) glDeleteRenderbuffers(1, &o.rb);
|
if (o.rb) glDeleteRenderbuffers(1, &o.rb);
|
||||||
if (o.image) pDestroyImage(EGLDisplay(dpy_), EGLImageKHR(o.image));
|
if (o.image) pDestroyImage(EGLDisplay(dpy_), EGLImageKHR(o.image));
|
||||||
@@ -505,26 +516,68 @@ void Renderer::DropPanel(int panel) {
|
|||||||
rings_.erase(it);
|
rings_.erase(it);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Output *Renderer::Composite(int panel, const void *key, const ft_dmabuf &src, const std::vector<Capsule2D> eyes[2]) {
|
namespace {
|
||||||
if (!ready_) return nullptr;
|
|
||||||
const auto t0 = std::chrono::steady_clock::now();
|
|
||||||
const int w = src.width, h = src.height;
|
|
||||||
Ring &ring = rings_[panel];
|
|
||||||
if (ring.w != w || ring.h != h) {
|
|
||||||
for (Output &old : ring.out) FreeOutput(old);
|
|
||||||
ring.w = w, ring.h = h, ring.next = 0;
|
|
||||||
}
|
|
||||||
Output &o = ring.out[ring.next];
|
|
||||||
if (!o.bo && !MakeOutput(o, 2 * w, h)) return nullptr;
|
|
||||||
const GLuint tex = Texture(key, src);
|
|
||||||
if (!tex) return nullptr;
|
|
||||||
ring.next = (ring.next + 1) % 3;
|
|
||||||
|
|
||||||
|
// The pixels a cutout's quad covers (see Draw), as x0 y0 x1 y1 in the eye's half.
|
||||||
|
void Bounds(const Capsule2D &c, float b[4]) {
|
||||||
|
const float feather = std::max(1.5f, 0.15f * std::min(c.ra, c.rb));
|
||||||
|
const float r = std::max(c.ra, c.rb) + feather;
|
||||||
|
b[0] = std::min(c.ax, c.bx) - r, b[1] = std::min(c.ay, c.by) - r;
|
||||||
|
b[2] = std::max(c.ax, c.bx) + r, b[3] = std::max(c.ay, c.by) + r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Within a quarter pixel: the same picture.
|
||||||
|
bool SameSpots(const std::vector<Capsule2D> a[2], const std::vector<Capsule2D> b[2]) {
|
||||||
|
for (int e = 0; e < 2; ++e) {
|
||||||
|
if (a[e].size() != b[e].size()) return false;
|
||||||
|
for (size_t i = 0; i < a[e].size(); ++i) {
|
||||||
|
const Capsule2D &p = a[e][i], &q = b[e][i];
|
||||||
|
for (float d : {p.ax - q.ax, p.ay - q.ay, p.bx - q.bx, p.by - q.by, p.ra - q.ra, p.rb - q.rb})
|
||||||
|
if (std::fabs(d) > 0.25f) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t SteadyNs() {
|
||||||
|
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool Renderer::Passed(Output &o, int64_t timeoutNs) {
|
||||||
|
if (!o.fence) return true;
|
||||||
|
const EGLint r = pClientWaitSync(EGLDisplay(dpy_), EGLSyncKHR(o.fence), 0, EGLTimeKHR(timeoutNs));
|
||||||
|
if (r == EGL_TIMEOUT_EXPIRED_KHR) return false;
|
||||||
|
pDestroySync(EGLDisplay(dpy_), EGLSyncKHR(o.fence)); // passed, or failed: don't wait on it again
|
||||||
|
o.fence = nullptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws one buffer. Partial: the buffer holds this client frame already, with o.spots cut
|
||||||
|
// out, so each eye is drawn again only inside the box around those and the new cutouts.
|
||||||
|
void Renderer::Draw(Output &o, unsigned tex, int w, int h, const std::vector<Capsule2D> eyes[2], bool partial) {
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, o.fbo);
|
glBindFramebuffer(GL_FRAMEBUFFER, o.fbo);
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
|
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
|
||||||
glEnableVertexAttribArray(0);
|
glEnableVertexAttribArray(0);
|
||||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||||||
for (int e = 0; e < 2; ++e) {
|
for (int e = 0; e < 2; ++e) {
|
||||||
|
if (partial) {
|
||||||
|
float box[4] = {1e9f, 1e9f, -1e9f, -1e9f}, b[4];
|
||||||
|
const std::vector<Capsule2D> *lists[2] = {&o.spots[e], &eyes[e]};
|
||||||
|
for (const std::vector<Capsule2D> *list : lists)
|
||||||
|
for (const Capsule2D &c : *list) {
|
||||||
|
Bounds(c, b);
|
||||||
|
box[0] = std::min(box[0], b[0]), box[1] = std::min(box[1], b[1]);
|
||||||
|
box[2] = std::max(box[2], b[2]), box[3] = std::max(box[3], b[3]);
|
||||||
|
}
|
||||||
|
// Window y is the buffer's row, the same way down as the cutouts' y (see kVertex).
|
||||||
|
const int x0 = std::clamp(int(std::floor(box[0])) - 1, 0, w), y0 = std::clamp(int(std::floor(box[1])) - 1, 0, h);
|
||||||
|
const int x1 = std::clamp(int(std::ceil(box[2])) + 1, 0, w), y1 = std::clamp(int(std::ceil(box[3])) + 1, 0, h);
|
||||||
|
if (x1 <= x0 || y1 <= y0) continue; // no cutout in this eye, then or now
|
||||||
|
glEnable(GL_SCISSOR_TEST);
|
||||||
|
glScissor(e * w + x0, y0, x1 - x0, y1 - y0);
|
||||||
|
}
|
||||||
glViewport(e * w, 0, w, h);
|
glViewport(e * w, 0, w, h);
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
glUseProgram(copyProg_);
|
glUseProgram(copyProg_);
|
||||||
@@ -543,22 +596,69 @@ const Output *Renderer::Composite(int panel, const void *key, const ft_dmabuf &s
|
|||||||
uB = glGetUniformLocation(cutProg_, "b"), uR = glGetUniformLocation(cutProg_, "r"),
|
uB = glGetUniformLocation(cutProg_, "b"), uR = glGetUniformLocation(cutProg_, "r"),
|
||||||
uF = glGetUniformLocation(cutProg_, "feather");
|
uF = glGetUniformLocation(cutProg_, "feather");
|
||||||
for (const Capsule2D &c : eyes[e]) {
|
for (const Capsule2D &c : eyes[e]) {
|
||||||
const float feather = std::max(1.5f, 0.15f * std::min(c.ra, c.rb));
|
float b[4];
|
||||||
const float r = std::max(c.ra, c.rb) + feather;
|
Bounds(c, b);
|
||||||
glUniform4f(uRect, std::min(c.ax, c.bx) - r, std::min(c.ay, c.by) - r, std::max(c.ax, c.bx) + r,
|
glUniform4f(uRect, b[0], b[1], b[2], b[3]);
|
||||||
std::max(c.ay, c.by) + r);
|
|
||||||
glUniform2f(uA, c.ax, c.ay);
|
glUniform2f(uA, c.ax, c.ay);
|
||||||
glUniform2f(uB, c.bx, c.by);
|
glUniform2f(uB, c.bx, c.by);
|
||||||
glUniform2f(uR, c.ra, c.rb);
|
glUniform2f(uR, c.ra, c.rb);
|
||||||
glUniform1f(uF, feather);
|
glUniform1f(uF, std::max(1.5f, 0.15f * std::min(c.ra, c.rb)));
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
}
|
}
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
}
|
}
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
// SteamVR reads the buffer from another process and GPU queue; make sure it's done.
|
}
|
||||||
glFinish();
|
|
||||||
lastMs_ = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count();
|
const Output *Renderer::Composite(int panel, const void *key, uint64_t serial, const ft_dmabuf &src,
|
||||||
return &o;
|
const std::vector<Capsule2D> eyes[2]) {
|
||||||
|
if (!ready_) return nullptr;
|
||||||
|
const int64_t t0 = SteadyNs();
|
||||||
|
const int w = src.width, h = src.height;
|
||||||
|
Ring &ring = rings_[panel];
|
||||||
|
if (ring.w != w || ring.h != h) {
|
||||||
|
for (Output &old : ring.out) FreeOutput(old);
|
||||||
|
ring.w = w, ring.h = h, ring.shown = ring.before = ring.drawing = -1;
|
||||||
|
}
|
||||||
|
// After a pause the panel showed its client buffer, so nothing of ours is on it.
|
||||||
|
if (t0 - ring.lastCall > 30'000'000) ring.shown = ring.before = -1;
|
||||||
|
ring.lastCall = t0;
|
||||||
|
auto promote = [&ring] {
|
||||||
|
ring.before = ring.shown, ring.shown = ring.drawing, ring.drawing = -1;
|
||||||
|
};
|
||||||
|
if (ring.drawing >= 0 && Passed(ring.out[ring.drawing], 0)) promote();
|
||||||
|
|
||||||
|
const int newest = ring.drawing >= 0 ? ring.drawing : ring.shown;
|
||||||
|
const Output *n = newest >= 0 ? &ring.out[newest] : nullptr;
|
||||||
|
if (n && n->key == key && n->serial == serial && SameSpots(n->spots, eyes)) {
|
||||||
|
++stats_.same;
|
||||||
|
} else if (ring.drawing >= 0) {
|
||||||
|
++stats_.busy; // drawn on a later tick, from what's current then
|
||||||
|
} else {
|
||||||
|
int i = 0;
|
||||||
|
while (i == ring.shown || i == ring.before) ++i;
|
||||||
|
Output &o = ring.out[i];
|
||||||
|
if (!o.bo && !MakeOutput(o, 2 * w, h)) return nullptr;
|
||||||
|
const GLuint tex = Texture(key, src);
|
||||||
|
if (!tex) return nullptr;
|
||||||
|
const bool partial = o.drawn && o.key == key && o.serial == serial;
|
||||||
|
Draw(o, tex, w, h, eyes, partial);
|
||||||
|
o.fence = pCreateSync(EGLDisplay(dpy_), EGL_SYNC_FENCE_KHR, nullptr);
|
||||||
|
glFlush();
|
||||||
|
if (!o.fence) glFinish(); // no fence: wait here, as before
|
||||||
|
o.key = key, o.serial = serial, o.drawn = true;
|
||||||
|
for (int e = 0; e < 2; ++e) o.spots[e] = eyes[e];
|
||||||
|
ring.drawing = i;
|
||||||
|
++stats_.draws, stats_.partial += partial;
|
||||||
|
}
|
||||||
|
// Nothing of ours to show yet: wait for this one rather than show none.
|
||||||
|
if (ring.shown < 0 && ring.drawing >= 0) {
|
||||||
|
++stats_.waits;
|
||||||
|
if (Passed(ring.out[ring.drawing], 50'000'000)) promote();
|
||||||
|
}
|
||||||
|
lastMs_ = (SteadyNs() - t0) / 1e6;
|
||||||
|
stats_.cpuMs += lastMs_, stats_.worstMs = std::max(stats_.worstMs, lastMs_);
|
||||||
|
return ring.shown >= 0 ? &ring.out[ring.shown] : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace handcut
|
} // namespace handcut
|
||||||
+42
-6
@@ -70,7 +70,7 @@ private:
|
|||||||
std::map<uint32_t, Motion> motion_;
|
std::map<uint32_t, Motion> motion_;
|
||||||
std::vector<Capsule> world_; // base_, moved ahead
|
std::vector<Capsule> world_; // base_, moved ahead
|
||||||
bool predict_ = true;
|
bool predict_ = true;
|
||||||
int64_t leadNs_ = 25'000'000;
|
int64_t leadNs_ = 36'000'000; // 25 ms to the displays, plus the tick a cutout buffer waits for its fence
|
||||||
int fd_ = -1;
|
int fd_ = -1;
|
||||||
const void *map_ = nullptr;
|
const void *map_ = nullptr;
|
||||||
uint64_t seq_ = 0;
|
uint64_t seq_ = 0;
|
||||||
@@ -91,6 +91,22 @@ struct Output {
|
|||||||
void *bo = nullptr;
|
void *bo = nullptr;
|
||||||
unsigned fbo = 0, rb = 0;
|
unsigned fbo = 0, rb = 0;
|
||||||
void *image = nullptr;
|
void *image = nullptr;
|
||||||
|
// What's drawn in it: the client buffer and its frame, and the cutouts (a later draw
|
||||||
|
// with the same frame only redraws around the old and new cutouts).
|
||||||
|
void *fence = nullptr; // the GPU is still drawing it
|
||||||
|
const void *key = nullptr;
|
||||||
|
uint64_t serial = 0;
|
||||||
|
bool drawn = false;
|
||||||
|
std::vector<Capsule2D> spots[2];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Composite's counts since the last TakeStats.
|
||||||
|
struct CutStats {
|
||||||
|
int draws = 0, partial = 0; // buffers drawn, of them only around the cutouts
|
||||||
|
int same = 0; // nothing changed: the newest buffer stays
|
||||||
|
int busy = 0; // the GPU hadn't finished the last one: drawn next tick
|
||||||
|
int waits = 0; // a panel's first buffer, waited for
|
||||||
|
double cpuMs = 0, worstMs = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Renderer {
|
class Renderer {
|
||||||
@@ -99,20 +115,34 @@ public:
|
|||||||
// modifiers: what SteamVR takes for DRM_FORMAT_ABGR8888, the outputs' format.
|
// modifiers: what SteamVR takes for DRM_FORMAT_ABGR8888, the outputs' format.
|
||||||
// released: an output is about to be freed (drop its SteamVR import).
|
// released: an output is about to be freed (drop its SteamVR import).
|
||||||
bool Init(const std::vector<uint64_t> &modifiers, std::function<void(const Output *)> released);
|
bool Init(const std::vector<uint64_t> &modifiers, std::function<void(const Output *)> released);
|
||||||
// Draw client buffer `src` (identified by `key`) into the next output buffer of
|
// Draw client buffer `src` (identified by `key`; `serial` counts its frames) into a
|
||||||
// panel `panel`, both eyes, cutting out `eyes`. Returns that buffer, or null.
|
// buffer of panel `panel`, both eyes, cutting out `eyes`. Returns the newest buffer the
|
||||||
const Output *Composite(int panel, const void *key, const ft_dmabuf &src, const std::vector<Capsule2D> eyes[2]);
|
// GPU has finished, or null.
|
||||||
|
//
|
||||||
|
// It doesn't wait for the GPU: a buffer is drawn, fenced, and returned from a later call
|
||||||
|
// once the fence has passed, so what SteamVR shows is a tick behind. Each panel has three
|
||||||
|
// buffers: the one shown, the one shown before it (SteamVR may still be reading it), and
|
||||||
|
// the one being drawn. Nothing is drawn when the frame and the cutouts are what the newest
|
||||||
|
// buffer has, and when only the cutouts moved, a buffer that holds the same client frame
|
||||||
|
// is drawn again only around them. The first call after a pause (no call for 30 ms, about
|
||||||
|
// 3 ticks: the panel showed its client buffer meanwhile) waits for its buffer, so a stale
|
||||||
|
// one never shows.
|
||||||
|
const Output *Composite(int panel, const void *key, uint64_t serial, const ft_dmabuf &src,
|
||||||
|
const std::vector<Capsule2D> eyes[2]);
|
||||||
// A client buffer is going away.
|
// A client buffer is going away.
|
||||||
void Forget(const void *key);
|
void Forget(const void *key);
|
||||||
// A panel is gone: drop its outputs.
|
// A panel is gone: drop its outputs.
|
||||||
void DropPanel(int panel);
|
void DropPanel(int panel);
|
||||||
// How long the last Composite took, ms (it waits for the GPU).
|
// How long the last Composite took on the CPU, ms.
|
||||||
double lastMs() const { return lastMs_; }
|
double lastMs() const { return lastMs_; }
|
||||||
|
CutStats TakeStats() { CutStats s = stats_; stats_ = {}; return s; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
unsigned Texture(const void *key, const ft_dmabuf &src);
|
unsigned Texture(const void *key, const ft_dmabuf &src);
|
||||||
bool MakeOutput(Output &o, int w, int h);
|
bool MakeOutput(Output &o, int w, int h);
|
||||||
void FreeOutput(Output &o);
|
void FreeOutput(Output &o);
|
||||||
|
bool Passed(Output &o, int64_t timeoutNs);
|
||||||
|
void Draw(Output &o, unsigned tex, int w, int h, const std::vector<Capsule2D> eyes[2], bool partial);
|
||||||
bool ready_ = false;
|
bool ready_ = false;
|
||||||
int drm_ = -1;
|
int drm_ = -1;
|
||||||
void *gbm_ = nullptr, *dpy_ = nullptr, *ctx_ = nullptr;
|
void *gbm_ = nullptr, *dpy_ = nullptr, *ctx_ = nullptr;
|
||||||
@@ -121,9 +151,15 @@ private:
|
|||||||
std::function<void(const Output *)> released_;
|
std::function<void(const Output *)> released_;
|
||||||
struct Imported { void *image; unsigned tex; };
|
struct Imported { void *image; unsigned tex; };
|
||||||
std::map<const void *, Imported> imported_;
|
std::map<const void *, Imported> imported_;
|
||||||
struct Ring { Output out[3]; int next = 0; int w = 0, h = 0; };
|
struct Ring {
|
||||||
|
Output out[3];
|
||||||
|
int shown = -1, before = -1, drawing = -1; // indices into out
|
||||||
|
int w = 0, h = 0;
|
||||||
|
int64_t lastCall = 0; // steady clock ns
|
||||||
|
};
|
||||||
std::map<int, Ring> rings_;
|
std::map<int, Ring> rings_;
|
||||||
double lastMs_ = 0;
|
double lastMs_ = 0;
|
||||||
|
CutStats stats_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace handcut
|
} // namespace handcut
|
||||||
@@ -163,7 +163,7 @@ int main(int argc, char **argv) {
|
|||||||
handcut::EyePositions(head, eyes);
|
handcut::EyePositions(head, eyes);
|
||||||
cut = handcut::Project(panel, hands.capsules(), eyes, eyes2d);
|
cut = handcut::Project(panel, hands.capsules(), eyes, eyes2d);
|
||||||
}
|
}
|
||||||
const handcut::Output *out = cut ? renderer.Composite(0, bo, client, eyes2d) : nullptr;
|
const handcut::Output *out = cut ? renderer.Composite(0, bo, 1, client, eyes2d) : nullptr;
|
||||||
if (out) {
|
if (out) {
|
||||||
auto it = imports.find(out);
|
auto it = imports.find(out);
|
||||||
if (it == imports.end()) {
|
if (it == imports.end()) {
|
||||||
@@ -177,8 +177,7 @@ int main(int argc, char **argv) {
|
|||||||
vr::VROverlay()->SetOverlayFlag(ov, vr::VROverlayFlags_SideBySide_Parallel, true);
|
vr::VROverlay()->SetOverlayFlag(ov, vr::VROverlayFlags_SideBySide_Parallel, true);
|
||||||
cutting = true;
|
cutting = true;
|
||||||
}
|
}
|
||||||
shown = it->second;
|
if (shown != it->second) shown = it->second, vr::VROverlay()->SetOverlayTexture(ov, &tex);
|
||||||
vr::VROverlay()->SetOverlayTexture(ov, &tex);
|
|
||||||
ms += renderer.lastMs(), worst = std::max(worst, renderer.lastMs());
|
ms += renderer.lastMs(), worst = std::max(worst, renderer.lastMs());
|
||||||
++cutFrames;
|
++cutFrames;
|
||||||
caps2d += eyes2d[0].size() + eyes2d[1].size();
|
caps2d += eyes2d[0].size() + eyes2d[1].size();
|
||||||
@@ -192,10 +191,12 @@ int main(int argc, char **argv) {
|
|||||||
}
|
}
|
||||||
++frames;
|
++frames;
|
||||||
if (now - lastReport > 2'000'000'000) {
|
if (now - lastReport > 2'000'000'000) {
|
||||||
|
const handcut::CutStats st = renderer.TakeStats();
|
||||||
std::printf("%.0f s: %d ticks, %d with a cutout (%.1f capsules per eye), composite %.2f ms avg %.2f ms worst, "
|
std::printf("%.0f s: %d ticks, %d with a cutout (%.1f capsules per eye), composite %.2f ms avg %.2f ms worst, "
|
||||||
"%zu hand capsules known\n",
|
"%zu hand capsules known; %d draws (%d partial), %d same, %d busy\n",
|
||||||
(now - start) / 1e9, frames, cutFrames, cutFrames ? caps2d / 2.0 / cutFrames : 0.0,
|
(now - start) / 1e9, frames, cutFrames, cutFrames ? caps2d / 2.0 / cutFrames : 0.0,
|
||||||
cutFrames ? ms / cutFrames : 0.0, worst, hands.capsules().size());
|
cutFrames ? ms / cutFrames : 0.0, worst, hands.capsules().size(), st.draws, st.partial, st.same,
|
||||||
|
st.busy);
|
||||||
std::fflush(stdout);
|
std::fflush(stdout);
|
||||||
lastReport = now, frames = cutFrames = 0, ms = worst = 0, caps2d = 0;
|
lastReport = now, frames = cutFrames = 0, ms = worst = 0, caps2d = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include "../controller-click.h"
|
||||||
|
static struct ft_event event(enum ft_event_type t, bool down, double x, double y) {
|
||||||
|
return (struct ft_event){.type=t,.screen=0,.button=BTN_LEFT,.pressed=down,.controller=true,.x=x,.y=y};
|
||||||
|
}
|
||||||
|
int main(void) {
|
||||||
|
struct ft_controller_click c={.threshold=8};
|
||||||
|
struct ft_event e=event(FT_BUTTON,true,100,100);
|
||||||
|
assert(ft_controller_click_filter(&c,&e,2));
|
||||||
|
e=event(FT_MOTION,false,112,108);
|
||||||
|
assert(!ft_controller_click_filter(&c,&e,2));
|
||||||
|
assert(c.held && !c.dragging && c.suppressed==1);
|
||||||
|
e=event(FT_BUTTON,false,116,111);
|
||||||
|
assert(ft_controller_click_filter(&c,&e,2));
|
||||||
|
assert(e.x==100 && e.y==100 && c.clicks==1 && !c.held);
|
||||||
|
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
|
||||||
|
e=event(FT_MOTION,false,109,100);assert(ft_controller_click_filter(&c,&e,1));
|
||||||
|
assert(c.dragging && c.drags==1);
|
||||||
|
e=event(FT_MOTION,false,101,100);assert(ft_controller_click_filter(&c,&e,1));
|
||||||
|
e=event(FT_BUTTON,false,103,100);assert(ft_controller_click_filter(&c,&e,1));
|
||||||
|
assert(e.x==103 && c.clicks==1 && !c.held && !c.dragging);
|
||||||
|
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
|
||||||
|
e=event(FT_LEAVE,false,0,0);assert(!ft_controller_click_filter(&c,&e,1));
|
||||||
|
e=event(FT_BUTTON,false,800,900);e.screen=1;ft_controller_click_filter(&c,&e,1);
|
||||||
|
assert(e.screen==0 && e.x==100 && c.clicks==2);
|
||||||
|
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
|
||||||
|
e=event(FT_MOTION,false,101,100);e.screen=1;
|
||||||
|
assert(ft_controller_click_filter(&c,&e,1) && c.dragging);
|
||||||
|
e=event(FT_BUTTON,false,101,100);ft_controller_click_filter(&c,&e,1);
|
||||||
|
c.threshold=0;
|
||||||
|
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
|
||||||
|
e=event(FT_MOTION,false,101,100);assert(ft_controller_click_filter(&c,&e,1) && !c.held);
|
||||||
|
c.threshold=8;
|
||||||
|
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
|
||||||
|
e=event(FT_BUTTON,true,100,100);e.button=BTN_RIGHT;ft_controller_click_filter(&c,&e,1);
|
||||||
|
assert(!c.held);
|
||||||
|
// The 3D mouse's laser: a short drag stays a drag, and the release stays where it was.
|
||||||
|
e=event(FT_BUTTON,true,100,100);e.controller=false;assert(ft_controller_click_filter(&c,&e,1) && !c.held);
|
||||||
|
e=event(FT_MOTION,false,104,100);assert(ft_controller_click_filter(&c,&e,1));
|
||||||
|
e=event(FT_BUTTON,false,106,100);e.controller=false;
|
||||||
|
assert(ft_controller_click_filter(&c,&e,1) && e.x==106 && c.clicks==2 && c.suppressed==1);
|
||||||
|
}
|
||||||
+21
-8
@@ -270,7 +270,9 @@ struct Screen {
|
|||||||
const void *key = nullptr; // the client buffer on it now, and its dmabuf (for cutouts)
|
const void *key = nullptr; // the client buffer on it now, and its dmabuf (for cutouts)
|
||||||
ft_dmabuf buf{};
|
ft_dmabuf buf{};
|
||||||
vr::SharedTextureHandle_t plain = 0; // that buffer's SteamVR import
|
vr::SharedTextureHandle_t plain = 0; // that buffer's SteamVR import
|
||||||
|
uint64_t frames = 0; // client frames presented (a cutout buffer's "serial")
|
||||||
bool cutting = false; // showing a cutout buffer (side by side) instead
|
bool cutting = false; // showing a cutout buffer (side by side) instead
|
||||||
|
vr::SharedTextureHandle_t cutShown = 0; // ...this one
|
||||||
double chrome = 0.3; // the bar's width; the other controls follow it (ChromeSize)
|
double chrome = 0.3; // the bar's width; the other controls follow it (ChromeSize)
|
||||||
double grip = 0.04; // the corner tab's and the round buttons' size
|
double grip = 0.04; // the corner tab's and the round buttons' size
|
||||||
// A floating window's panel (see the top): the window's rectangle in the buffer, its
|
// A floating window's panel (see the top): the window's rectangle in the buffer, its
|
||||||
@@ -1330,7 +1332,8 @@ void UpdateCatcher() {
|
|||||||
for (int k = 0; k < 3; ++k) params.vSource.v[k] = l.m[k][3], params.vDirection.v[k] = -l.m[k][2];
|
for (int k = 0; k < 3; ++k) params.vSource.v[k] = l.m[k][3], params.vDirection.v[k] = -l.m[k][2];
|
||||||
for (auto &[i, s] : g_screens) {
|
for (auto &[i, s] : g_screens) {
|
||||||
if (!s.visible) continue;
|
if (!s.visible) continue;
|
||||||
std::vector<vr::VROverlayHandle_t> parts(s.All().begin(), s.All().end());
|
const auto all = s.All(); // one copy: two calls give two temporaries, not one range
|
||||||
|
std::vector<vr::VROverlayHandle_t> parts(all.begin(), all.end());
|
||||||
for (const auto &[k, sub] : s.subs) parts.push_back(sub.overlay);
|
for (const auto &[k, sub] : s.subs) parts.push_back(sub.overlay);
|
||||||
for (auto o : parts) {
|
for (auto o : parts) {
|
||||||
vr::VROverlayIntersectionResults_t hit;
|
vr::VROverlayIntersectionResults_t hit;
|
||||||
@@ -1437,7 +1440,7 @@ void StopCutting(Screen &s) {
|
|||||||
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_SideBySide_Parallel, false);
|
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_SideBySide_Parallel, false);
|
||||||
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_IgnoreTextureAlpha, true);
|
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_IgnoreTextureAlpha, true);
|
||||||
if (s.plain) SetScreenTexture(s, s.plain);
|
if (s.plain) SetScreenTexture(s, s.plain);
|
||||||
s.cutting = false;
|
s.cutting = false, s.cutShown = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each tick: for each visible screen with a hand in front of it (for either eye), draw its
|
// Each tick: for each visible screen with a hand in front of it (for either eye), draw its
|
||||||
@@ -1458,7 +1461,7 @@ void UpdateCutouts() {
|
|||||||
bool cut = hands && s.visible && !s.floating && s.key && s.width > 0 && ScreenPose(s, &p) &&
|
bool cut = hands && s.visible && !s.floating && s.key && s.width > 0 && ScreenPose(s, &p) &&
|
||||||
handcut::Project({p, s.metres, s.heightMetres(), s.curve, s.width, s.height}, g_hands.capsules(),
|
handcut::Project({p, s.metres, s.heightMetres(), s.curve, s.width, s.height}, g_hands.capsules(),
|
||||||
eyes, spots);
|
eyes, spots);
|
||||||
const handcut::Output *out = cut && CutterReady() ? g_cutter.Composite(i, s.key, s.buf, spots) : nullptr;
|
const handcut::Output *out = cut && CutterReady() ? g_cutter.Composite(i, s.key, s.frames, s.buf, spots) : nullptr;
|
||||||
const vr::SharedTextureHandle_t h = out ? ImportCutout(out) : 0;
|
const vr::SharedTextureHandle_t h = out ? ImportCutout(out) : 0;
|
||||||
if (!h) {
|
if (!h) {
|
||||||
StopCutting(s);
|
StopCutting(s);
|
||||||
@@ -1469,7 +1472,7 @@ void UpdateCutouts() {
|
|||||||
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_SideBySide_Parallel, true);
|
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_SideBySide_Parallel, true);
|
||||||
s.cutting = true;
|
s.cutting = true;
|
||||||
}
|
}
|
||||||
SetScreenTexture(s, h);
|
if (h != s.cutShown) SetScreenTexture(s, h), s.cutShown = h; // the same buffer stays: no new frame for SteamVR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1698,7 +1701,7 @@ bool ft_vr_screen_present(int index, const void *key, const struct ft_dmabuf *b)
|
|||||||
PlaceChrome(s); // the height changed
|
PlaceChrome(s); // the height changed
|
||||||
std::printf("screen %d: %dx%d\n", index + 1, s.width, s.height);
|
std::printf("screen %d: %dx%d\n", index + 1, s.width, s.height);
|
||||||
}
|
}
|
||||||
s.key = key, s.buf = *b, s.plain = it->second;
|
s.key = key, s.buf = *b, s.plain = it->second, ++s.frames;
|
||||||
// While cutting, the next tick draws the new buffer with the cutouts (never floating).
|
// While cutting, the next tick draws the new buffer with the cutouts (never floating).
|
||||||
if (!s.cutting) SetScreenTexture(s, it->second);
|
if (!s.cutting) SetScreenTexture(s, it->second);
|
||||||
vr::SharedTextureHandle_t handle = it->second;
|
vr::SharedTextureHandle_t handle = it->second;
|
||||||
@@ -1741,6 +1744,7 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
|
|||||||
e.type = FT_BUTTON;
|
e.type = FT_BUTTON;
|
||||||
e.button = LinuxButton(ev.data.mouse.button);
|
e.button = LinuxButton(ev.data.mouse.button);
|
||||||
e.pressed = ev.eventType == vr::VREvent_MouseButtonDown;
|
e.pressed = ev.eventType == vr::VREvent_MouseButtonDown;
|
||||||
|
e.controller = IsHandController(ev.trackedDeviceIndex);
|
||||||
at();
|
at();
|
||||||
if (!e.pressed && s.titleCarry) e.x = s.carryX, e.y = s.carryY, s.titleCarry = false;
|
if (!e.pressed && s.titleCarry) e.x = s.carryX, e.y = s.carryY, s.titleCarry = false;
|
||||||
if (e.pressed) {
|
if (e.pressed) {
|
||||||
@@ -2129,9 +2133,18 @@ void ft_vr_command(const char *cmd, char *reply, int size) {
|
|||||||
g_hands.SetPrediction(g_hands.predicting(), ms);
|
g_hands.SetPrediction(g_hands.predicting(), ms);
|
||||||
else if (std::strcmp(word, "state") != 0)
|
else if (std::strcmp(word, "state") != 0)
|
||||||
return (void)std::snprintf(reply, size, "error cutouts on|off|state|predict on|off|lead <ms>");
|
return (void)std::snprintf(reply, size, "error cutouts on|off|state|predict on|off|lead <ms>");
|
||||||
std::snprintf(reply, size, "ok %s %s %.2f ms, predict %s lead %.0f ms", g_cutouts ? "on" : "off",
|
// Composite's counts since the last state, per second.
|
||||||
g_cutterState > 0 ? "ready" : g_cutterState < 0 ? "unavailable" : "idle", g_cutter.lastMs(),
|
static auto since = Clock::now();
|
||||||
g_hands.predicting() ? "on" : "off", g_hands.leadMs());
|
const double dt = std::max(1e-3, std::chrono::duration<double>(Clock::now() - since).count());
|
||||||
|
since = Clock::now();
|
||||||
|
const handcut::CutStats c = g_cutter.TakeStats();
|
||||||
|
const int calls = c.draws + c.same + c.busy;
|
||||||
|
std::snprintf(reply, size,
|
||||||
|
"ok %s %s %.2f ms, predict %s lead %.0f ms; per s: %.1f draws (%.1f partial), %.1f same, %.1f busy, "
|
||||||
|
"%.1f waits; CPU %.2f ms avg %.2f worst",
|
||||||
|
g_cutouts ? "on" : "off", g_cutterState > 0 ? "ready" : g_cutterState < 0 ? "unavailable" : "idle",
|
||||||
|
g_cutter.lastMs(), g_hands.predicting() ? "on" : "off", g_hands.leadMs(), c.draws / dt,
|
||||||
|
c.partial / dt, c.same / dt, c.busy / dt, c.waits / dt, calls ? c.cpuMs / calls : 0.0, c.worstMs);
|
||||||
} else if (int x0, y0, w0, h0, t0; std::sscanf(cmd, "float %d %lf %d %d %d %d %d", &n, &w, &x0, &y0, &w0, &h0, &t0) == 7) {
|
} else if (int x0, y0, w0, h0, t0; std::sscanf(cmd, "float %d %lf %d %d %d %d %d", &n, &w, &x0, &y0, &w0, &h0, &t0) == 7) {
|
||||||
Screen *s = Find(n);
|
Screen *s = Find(n);
|
||||||
if (!s || !s->floating) return (void)std::snprintf(reply, size, "error no floating window panel %d", n);
|
if (!s || !s->floating) return (void)std::snprintf(reply, size, "error no floating window panel %d", n);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ struct ft_event {
|
|||||||
double x, y; // FT_MOTION: buffer pixels from the top left
|
double x, y; // FT_MOTION: buffer pixels from the top left
|
||||||
uint32_t button; // FT_BUTTON: linux BTN_*
|
uint32_t button; // FT_BUTTON: linux BTN_*
|
||||||
bool pressed;
|
bool pressed;
|
||||||
|
bool controller; // FT_BUTTON: from a hand controller's laser (not the 3D mouse's)
|
||||||
double dx, dy; // FT_SCROLL: notches (positive dy: scroll down)
|
double dx, dy; // FT_SCROLL: notches (positive dy: scroll down)
|
||||||
uint32_t key; // FT_KEY: linux KEY_* from our keyboard (pressed: down or up)
|
uint32_t key; // FT_KEY: linux KEY_* from our keyboard (pressed: down or up)
|
||||||
};
|
};
|
||||||
|
|||||||
+18
-3
@@ -6,7 +6,22 @@
|
|||||||
# `distrobox enter`.
|
# `distrobox enter`.
|
||||||
box=${FRAME_BOX:-dev}
|
box=${FRAME_BOX:-dev}
|
||||||
export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
|
export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
|
||||||
[ "$(podman container inspect -f '{{.State.Running}}' "$box" 2>/dev/null)" = true ] && exit 0
|
running() { [ "$(podman container inspect -f '{{.State.Running}}' "$box" 2>/dev/null)" = true ]; }
|
||||||
|
running && exit 0
|
||||||
podman container exists "$box" 2>/dev/null || exit 0 # not created yet: setup/dev-container.sh does that
|
podman container exists "$box" 2>/dev/null || exit 0 # not created yet: setup/dev-container.sh does that
|
||||||
exec systemd-run --user --scope --quiet --collect --description="$box container (started for Frametop)" \
|
since=$(date -u +%FT%T)
|
||||||
podman start "$box" >/dev/null
|
systemd-run --user --scope --quiet --collect --description="$box container (started for Frametop)" \
|
||||||
|
podman start "$box" >/dev/null || exit
|
||||||
|
|
||||||
|
# Every start runs distrobox-init in the container, and `distrobox enter` only waits for it
|
||||||
|
# when it starts the container itself. The first start installs what distrobox needs and sets
|
||||||
|
# up passwordless sudo, which takes a minute or more; until then sudo in the container asks
|
||||||
|
# for a password (issue #9). Later starts take a few seconds.
|
||||||
|
for i in $(seq 600); do
|
||||||
|
podman logs --since "$since" "$box" 2>&1 | grep -q '^container_setup_done' && exit 0
|
||||||
|
running || break
|
||||||
|
[ "$i" = 10 ] && echo "setting up the $box container (the first start takes a few minutes)" >&2
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "the $box container didn't finish starting; see: podman logs $box" >&2
|
||||||
|
exit 1
|
||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
"$root/scripts/frame.sh" -C screens 'mkdir -p build; gcc -std=c11 -Wall -Wextra -Werror tests/controller-click-test.c -lm -o build/controller-click-test && build/controller-click-test'
|
||||||
@@ -45,9 +45,9 @@ fi
|
|||||||
"$distrobox" enter dev -- bash -c '
|
"$distrobox" enter dev -- bash -c '
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
echo "installing ${#@} packages (already-installed ones are skipped)"
|
echo "installing ${#@} packages (already-installed ones are skipped)"
|
||||||
sudo dnf install -y -q "$@" 2>&1 | { grep -vE "is already installed|^Nothing to do|^$" || true; }
|
sudo -n dnf install -y -q "$@" 2>&1 | { grep -vE "is already installed|^Nothing to do|^$" || true; }
|
||||||
# OpenVR programs built here (the pointer helper and probe) look for the runtime at /opt/steamvr.
|
# OpenVR programs built here (the pointer helper and probe) look for the runtime at /opt/steamvr.
|
||||||
[ -e /opt/steamvr ] || sudo ln -s /run/host/opt/steamvr /opt/steamvr
|
[ -e /opt/steamvr ] || sudo -n ln -s /run/host/opt/steamvr /opt/steamvr
|
||||||
echo "dev container ready: $(. /etc/os-release; echo $PRETTY_NAME), glibc $(ldd --version | head -1 | grep -oE "[0-9.]+$")"
|
echo "dev container ready: $(. /etc/os-release; echo $PRETTY_NAME), glibc $(ldd --version | head -1 | grep -oE "[0-9.]+$")"
|
||||||
' dev "$@"
|
' dev "$@"
|
||||||
EOF
|
EOF
|
||||||
Reference in new issue
Block a user