mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 04:04:09 +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.
|
||||
|
||||
Join the [Frametop Discord](https://discord.gg/W3X9f7z3Bc) for questions, ideas, and help with your setup.
|
||||
|
||||
## 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.
|
||||
@@ -139,7 +141,7 @@ In a terminal on the headset, run:
|
||||
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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
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-`.
|
||||
|
||||
|
||||
+35
-18
@@ -479,9 +479,24 @@ int main(int argc, char **argv) {
|
||||
double lastEmit = 0;
|
||||
int actionErrors = 0;
|
||||
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) {
|
||||
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;
|
||||
sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, &hp, 1);
|
||||
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
|
||||
// frame so every source reports the same kind of angles.
|
||||
std::string action = "{\"ok\":0}";
|
||||
vr::VRActiveActionSet_t active{};
|
||||
active.ulActionSet = set;
|
||||
active.nPriority = vr::k_nActionSetOverlayGlobalPriorityMin;
|
||||
input->UpdateActionState(&active, sizeof active, 1);
|
||||
vr::VREyeTrackingData_t e{};
|
||||
const vr::EVRInputError ae =
|
||||
input->GetEyeTrackingDataRelativeToNow(gaze, vr::TrackingUniverseStanding, 0, &e, sizeof e);
|
||||
if (ae == vr::VRInputError_None && e.bActive && e.bValid) {
|
||||
const Vec3 o{e.vGazeOrigin.v[0], e.vGazeOrigin.v[1], e.vGazeOrigin.v[2]};
|
||||
const Vec3 t{e.vGazeTarget.v[0], e.vGazeTarget.v[1], e.vGazeTarget.v[2]};
|
||||
const Vec3 dHead = RotateInverse(headNow, Normalize(t - o));
|
||||
char extra[96];
|
||||
std::snprintf(extra, sizeof extra, "\"tracked\":%d,\"dist\":%.3f,", int(e.bTracked), Length(t - o));
|
||||
action = SrcJson(list, headNow, dHead, extra);
|
||||
} else if (ae != lastActionError || (verbose && ++actionErrors % 90 == 1)) {
|
||||
std::fprintf(stderr, "ft-gaze: action: error %d active %d valid %d\n", int(ae), int(e.bActive),
|
||||
int(e.bValid));
|
||||
lastActionError = ae;
|
||||
if (!inGame) {
|
||||
vr::VRActiveActionSet_t active{};
|
||||
active.ulActionSet = set;
|
||||
active.nPriority = vr::k_nActionSetOverlayGlobalPriorityMin;
|
||||
input->UpdateActionState(&active, sizeof active, 1);
|
||||
vr::VREyeTrackingData_t e{};
|
||||
const vr::EVRInputError ae =
|
||||
input->GetEyeTrackingDataRelativeToNow(gaze, vr::TrackingUniverseStanding, 0, &e, sizeof e);
|
||||
if (ae == vr::VRInputError_None && e.bActive && e.bValid) {
|
||||
const Vec3 o{e.vGazeOrigin.v[0], e.vGazeOrigin.v[1], e.vGazeOrigin.v[2]};
|
||||
const Vec3 t{e.vGazeTarget.v[0], e.vGazeTarget.v[1], e.vGazeTarget.v[2]};
|
||||
const Vec3 dHead = RotateInverse(headNow, Normalize(t - o));
|
||||
char extra[96];
|
||||
std::snprintf(extra, sizeof extra, "\"tracked\":%d,\"dist\":%.3f,", int(e.bTracked), Length(t - o));
|
||||
action = SrcJson(list, headNow, dHead, extra);
|
||||
} else if (ae != lastActionError || (verbose && ++actionErrors % 90 == 1)) {
|
||||
std::fprintf(stderr, "ft-gaze: action: error %d active %d valid %d\n", int(ae), int(e.bActive),
|
||||
int(e.bValid));
|
||||
lastActionError = ae;
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
for old in [n for n in nodes.values() if n.path == path]:
|
||||
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
|
||||
node = probe(path)
|
||||
if node and node.volume_keys and not take_volume(node):
|
||||
|
||||
+20
-1
@@ -58,6 +58,7 @@
|
||||
#include <wlr/util/log.h>
|
||||
|
||||
#include "vr.h"
|
||||
#include "controller-click.h"
|
||||
|
||||
#define MAX_SCREENS 24 // screens and spare outputs
|
||||
|
||||
@@ -104,6 +105,7 @@ struct server {
|
||||
struct wl_list buffers; // tracked_buffer
|
||||
struct wl_event_source *tick;
|
||||
struct screen *pointer_focus;
|
||||
struct ft_controller_click controller_click;
|
||||
pid_t child;
|
||||
// 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).
|
||||
@@ -292,6 +294,11 @@ static void handle_vr_event(const struct ft_event *e, void *data) {
|
||||
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 wlr_surface *surface = sc->toplevel->base->surface;
|
||||
const uint32_t t = now_ms();
|
||||
@@ -514,7 +521,18 @@ static int control_readable(int fd, uint32_t mask, void *data) {
|
||||
unsigned code;
|
||||
int value, index, w, h;
|
||||
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
|
||||
// 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;
|
||||
@@ -641,6 +659,7 @@ static bool setup_dmabuf(struct server *s) {
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
struct server s = {0};
|
||||
s.controller_click.threshold = 8;
|
||||
for (int i = 0; i < MAX_SCREENS; ++i) s.scale[i] = 1;
|
||||
s.kb_screen = -1;
|
||||
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;
|
||||
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pImageTargetTexture;
|
||||
PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC pImageTargetRenderbuffer;
|
||||
PFNEGLCREATESYNCKHRPROC pCreateSync;
|
||||
PFNEGLDESTROYSYNCKHRPROC pDestroySync;
|
||||
PFNEGLCLIENTWAITSYNCKHRPROC pClientWaitSync;
|
||||
|
||||
const char *kVertex = R"(
|
||||
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"));
|
||||
pImageTargetRenderbuffer = reinterpret_cast<PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC>(
|
||||
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");
|
||||
return false;
|
||||
}
|
||||
@@ -451,6 +458,9 @@ void Renderer::Forget(const void *key) {
|
||||
glDeleteTextures(1, &it->second.tex);
|
||||
pDestroyImage(EGLDisplay(dpy_), EGLImageKHR(it->second.image));
|
||||
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) {
|
||||
@@ -489,6 +499,7 @@ bool Renderer::MakeOutput(Output &o, int w, int h) {
|
||||
|
||||
void Renderer::FreeOutput(Output &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.rb) glDeleteRenderbuffers(1, &o.rb);
|
||||
if (o.image) pDestroyImage(EGLDisplay(dpy_), EGLImageKHR(o.image));
|
||||
@@ -505,26 +516,68 @@ void Renderer::DropPanel(int panel) {
|
||||
rings_.erase(it);
|
||||
}
|
||||
|
||||
const Output *Renderer::Composite(int panel, const void *key, const ft_dmabuf &src, const std::vector<Capsule2D> eyes[2]) {
|
||||
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;
|
||||
namespace {
|
||||
|
||||
// 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);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||||
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);
|
||||
glDisable(GL_BLEND);
|
||||
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"),
|
||||
uF = glGetUniformLocation(cutProg_, "feather");
|
||||
for (const Capsule2D &c : eyes[e]) {
|
||||
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;
|
||||
glUniform4f(uRect, std::min(c.ax, c.bx) - r, std::min(c.ay, c.by) - r, std::max(c.ax, c.bx) + r,
|
||||
std::max(c.ay, c.by) + r);
|
||||
float b[4];
|
||||
Bounds(c, b);
|
||||
glUniform4f(uRect, b[0], b[1], b[2], b[3]);
|
||||
glUniform2f(uA, c.ax, c.ay);
|
||||
glUniform2f(uB, c.bx, c.by);
|
||||
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);
|
||||
}
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
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();
|
||||
return &o;
|
||||
}
|
||||
|
||||
const Output *Renderer::Composite(int panel, const void *key, uint64_t serial, const ft_dmabuf &src,
|
||||
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
|
||||
+42
-6
@@ -70,7 +70,7 @@ private:
|
||||
std::map<uint32_t, Motion> motion_;
|
||||
std::vector<Capsule> world_; // base_, moved ahead
|
||||
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;
|
||||
const void *map_ = nullptr;
|
||||
uint64_t seq_ = 0;
|
||||
@@ -91,6 +91,22 @@ struct Output {
|
||||
void *bo = nullptr;
|
||||
unsigned fbo = 0, rb = 0;
|
||||
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 {
|
||||
@@ -99,20 +115,34 @@ public:
|
||||
// modifiers: what SteamVR takes for DRM_FORMAT_ABGR8888, the outputs' format.
|
||||
// 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);
|
||||
// Draw client buffer `src` (identified by `key`) into the next output buffer of
|
||||
// panel `panel`, both eyes, cutting out `eyes`. Returns that buffer, or null.
|
||||
const Output *Composite(int panel, const void *key, const ft_dmabuf &src, const std::vector<Capsule2D> eyes[2]);
|
||||
// Draw client buffer `src` (identified by `key`; `serial` counts its frames) into a
|
||||
// buffer of panel `panel`, both eyes, cutting out `eyes`. Returns the newest buffer the
|
||||
// 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.
|
||||
void Forget(const void *key);
|
||||
// A panel is gone: drop its outputs.
|
||||
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_; }
|
||||
CutStats TakeStats() { CutStats s = stats_; stats_ = {}; return s; }
|
||||
|
||||
private:
|
||||
unsigned Texture(const void *key, const ft_dmabuf &src);
|
||||
bool MakeOutput(Output &o, int w, int h);
|
||||
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;
|
||||
int drm_ = -1;
|
||||
void *gbm_ = nullptr, *dpy_ = nullptr, *ctx_ = nullptr;
|
||||
@@ -121,9 +151,15 @@ private:
|
||||
std::function<void(const Output *)> released_;
|
||||
struct Imported { void *image; unsigned tex; };
|
||||
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_;
|
||||
double lastMs_ = 0;
|
||||
CutStats stats_;
|
||||
};
|
||||
|
||||
} // namespace handcut
|
||||
@@ -163,7 +163,7 @@ int main(int argc, char **argv) {
|
||||
handcut::EyePositions(head, eyes);
|
||||
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) {
|
||||
auto it = imports.find(out);
|
||||
if (it == imports.end()) {
|
||||
@@ -177,8 +177,7 @@ int main(int argc, char **argv) {
|
||||
vr::VROverlay()->SetOverlayFlag(ov, vr::VROverlayFlags_SideBySide_Parallel, true);
|
||||
cutting = true;
|
||||
}
|
||||
shown = it->second;
|
||||
vr::VROverlay()->SetOverlayTexture(ov, &tex);
|
||||
if (shown != it->second) shown = it->second, vr::VROverlay()->SetOverlayTexture(ov, &tex);
|
||||
ms += renderer.lastMs(), worst = std::max(worst, renderer.lastMs());
|
||||
++cutFrames;
|
||||
caps2d += eyes2d[0].size() + eyes2d[1].size();
|
||||
@@ -192,10 +191,12 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
++frames;
|
||||
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, "
|
||||
"%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,
|
||||
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);
|
||||
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)
|
||||
ft_dmabuf buf{};
|
||||
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
|
||||
vr::SharedTextureHandle_t cutShown = 0; // ...this one
|
||||
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
|
||||
// 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 (auto &[i, s] : g_screens) {
|
||||
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 (auto o : parts) {
|
||||
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_IgnoreTextureAlpha, true);
|
||||
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
|
||||
@@ -1458,7 +1461,7 @@ void UpdateCutouts() {
|
||||
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(),
|
||||
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;
|
||||
if (!h) {
|
||||
StopCutting(s);
|
||||
@@ -1469,7 +1472,7 @@ void UpdateCutouts() {
|
||||
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_SideBySide_Parallel, 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
|
||||
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).
|
||||
if (!s.cutting) SetScreenTexture(s, 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.button = LinuxButton(ev.data.mouse.button);
|
||||
e.pressed = ev.eventType == vr::VREvent_MouseButtonDown;
|
||||
e.controller = IsHandController(ev.trackedDeviceIndex);
|
||||
at();
|
||||
if (!e.pressed && s.titleCarry) e.x = s.carryX, e.y = s.carryY, s.titleCarry = false;
|
||||
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);
|
||||
else if (std::strcmp(word, "state") != 0)
|
||||
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",
|
||||
g_cutterState > 0 ? "ready" : g_cutterState < 0 ? "unavailable" : "idle", g_cutter.lastMs(),
|
||||
g_hands.predicting() ? "on" : "off", g_hands.leadMs());
|
||||
// Composite's counts since the last state, per second.
|
||||
static auto since = Clock::now();
|
||||
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) {
|
||||
Screen *s = Find(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
|
||||
uint32_t button; // FT_BUTTON: linux BTN_*
|
||||
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)
|
||||
uint32_t key; // FT_KEY: linux KEY_* from our keyboard (pressed: down or up)
|
||||
};
|
||||
|
||||
+18
-3
@@ -6,7 +6,22 @@
|
||||
# `distrobox enter`.
|
||||
box=${FRAME_BOX:-dev}
|
||||
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
|
||||
exec systemd-run --user --scope --quiet --collect --description="$box container (started for Frametop)" \
|
||||
podman start "$box" >/dev/null
|
||||
since=$(date -u +%FT%T)
|
||||
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 '
|
||||
set -euo pipefail
|
||||
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.
|
||||
[ -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.]+$")"
|
||||
' dev "$@"
|
||||
EOF
|
||||
Reference in new issue
Block a user