Compare commits

...
Author SHA1 Message Date
DeeJanuzandClaude Opus 5.5 64c4eec59f Screens: hand cutouts without waiting for the GPU
The cutout composite drew each screen's whole client buffer into a side-by-side buffer on
every tick a hand was in front of it, then waited for the GPU with glFinish (1-6 ms, the
likely cause of the VR frame drops on 2026-10-01). Now:

- A drawn buffer gets a fence and is shown from a later tick once the fence has passed, so
  ft-screens never waits for the GPU (except for a panel's first buffer after a pause, so a
  stale one never shows). The prediction lead goes from 25 to 36 ms for that tick.
- Nothing is drawn when the client frame and the cutouts haven't changed, and SteamVR
  isn't handed the same buffer again.
- When only the cutouts moved, a buffer that holds the same client frame is drawn again
  only around the old and new cutouts (scissored).
- "cutouts state" reports draws, partial draws, unchanged ticks, busy ticks, waits and CPU
  time per second since the last state; ft-handtest prints the same counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:14:54 -06:00
DeeJanuzandClaude Opus 5.5 ff36356992 Merge branch gaze-games (PR #13, narrowed) into experimental
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:18:26 -06:00
4d50739393 Gaze: leave SteamVR's gaze action alone during VR games
From curiousjtuber's PR #13: with the gaze service running, SteamVR
restarted its eye tracker every 10 to 13 s of Beat Saber, as if the
headset came off, and each restart took input focus from the game.
The PR stopped every read in a game. Only the action path reaches
SteamVR (UpdateActionState on the gaze set at overlay-global priority,
then GetEyeTrackingDataRelativeToNow); the mmap and our tracker are
read-only files. So only the action is skipped while a scene app runs,
and gaze keeps moving the pointer over the dashboard in a game. The
action source is only used with --source action.

Co-Authored-By: CuriousJ <curious.j.tuber@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:15:57 -06:00
DeeJanuzandClaude Opus 5.5 f60da63702 Merge PR #12 and #14 (relay udev retry, catcher crash) into experimental
From curiousjtuber's PRs: the relay leaves a new input node for the next
scan until udev gives it to the input group, rather than marking it seen
after a failed open; and ft-screens' catcher takes a screen's overlays
from one copy of All() instead of begin() and end() of two temporaries,
which crashed libc++ builds on the first click.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:15:04 -06:00
CuriousJ 82d2107e1d Screens: take a screen's overlays from one copy in the catcher
While a button pressed on a screen is held, UpdateCatcher checks every tick
whether the laser is still on one of the screen's overlays. It built that list
from s.All().begin() and s.All().end(), but All() returns a std::array by
value: iterators into two different temporaries, which is undefined behaviour.
A clang build of ft-screens got a garbage length, threw std::length_error, and
aborted on the first click, taking KWin and the desktop with it.
2026-10-02 09:11:46 -06:00
CuriousJ 12f2e844d5 Input relay: retry a new device until udev gives it to the input group
A new /dev/input node is root:root 0600 until udev applies GROUP=input. The
scan probed each new node once and marked it seen even when the open failed, so
a node caught in that gap was never opened. Behind a KVM, a switch brings back a
hub of devices at once: on the Frame, four nodes failed with EACCES in one switch,
the keyboard was never grabbed, and its keys went to gamescope instead of the
desktop screens. A node that isn't readable yet now waits for the next scan.
2026-10-02 09:11:46 -06:00
DeeJanuzandClaude Opus 5.5 ee2ce1a8d2 Merge PR #11 (controller click stability) into experimental
From jlneal's PR: a trigger press on a desktop screen stays put until the
laser moves more than 8 logical pixels, so controller jitter doesn't turn
a click into a drag. On top of it, only a hand controller's press starts
the filter: the 3D mouse's laser reaches the screens the same way, and the
PR as sent turned the mouse's short drags into clicks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:20:06 -06:00
6 changed files with 232 additions and 61 deletions

No files matched your search

+35 -18
View File
@@ -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";
+5
View File
@@ -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):
+124 -24
View File
@@ -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
View File
@@ -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
+6 -5
View File
@@ -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;
}
+20 -8
View File
@@ -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;
@@ -2130,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);