mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 07:00:14 +02:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64c4eec59f | ||
|
|
ff36356992 | ||
|
|
4d50739393 | ||
|
|
f60da63702 | ||
|
|
82d2107e1d | ||
|
|
12f2e844d5 | ||
|
|
ee2ce1a8d2 | ||
|
|
540d8c425c | ||
|
|
ba6ccdfbd1 | ||
|
|
77f28f0922 |
No files matched your search
@@ -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.
|
||||
+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)
|
||||
};
|
||||
|
||||
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'
|
||||
Reference in new issue
Block a user