Files
DeeJanuz--frametop/screens/handcut.cpp
T
DeeJanuzandClaude Opus 5.5 a31d42b25c Move hand cutouts ahead to where the hands will be
The tracked hands arrive 30-60 ms after the cameras saw them and reach the
displays later still, so holes trailed moving hands. Track each hand's palm
velocity in the room and move its capsules ahead to about when the frame is
on the displays, every tick, so the holes also move smoothly between tracker
updates. Slow hands aren't moved (their velocity is noise). The control
socket gets cutouts predict on|off and cutouts lead <ms>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:18:40 -06:00

575 lines
24 KiB
C++

// Hand cutouts (see handcut.h).
#include "handcut.h"
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#include <drm_fourcc.h>
#include <fcntl.h>
#include <gbm.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
namespace handcut {
namespace {
// The hands file (frame-hands/include/fh_hands.h).
constexpr char kMagic[8] = {'F', 'H', 'H', 'A', 'N', 'D', 'S', '1'};
constexpr size_t kHeader = 64, kHand = 272, kCapsule = 32, kMaxHands = 2, kMaxCapsules = 64;
constexpr size_t kFileSize = kHeader + kMaxHands * kHand + kMaxCapsules * kCapsule;
constexpr int64_t kStaleNs = 300'000'000; // hands older than this are gone
constexpr int64_t kHistoryNs = 1'000'000'000;
constexpr double kNear = 0.12; // metres: nothing closer to an eye than this is cut
constexpr double kMaxSpeed = 2.5; // m/s: faster is a tracking jump, not a hand
constexpr double kStillSpeed = 0.05; // m/s: below this, a hand's velocity is noise
constexpr double kMaxAhead = 0.12; // s: never predict further than this
int64_t MonoNs() {
timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return int64_t(ts.tv_sec) * 1'000'000'000 + ts.tv_nsec;
}
void Apply(const Mat &m, const float p[3], float out[3]) {
for (int i = 0; i < 3; ++i) out[i] = m.m[i][0] * p[0] + m.m[i][1] * p[1] + m.m[i][2] * p[2] + m.m[i][3];
}
// Room -> panel-local: R^T (p - t).
void ToLocal(const Mat &m, const double p[3], double out[3]) {
const double d[3] = {p[0] - m.m[0][3], p[1] - m.m[1][3], p[2] - m.m[2][3]};
for (int i = 0; i < 3; ++i) out[i] = m.m[0][i] * d[0] + m.m[1][i] * d[1] + m.m[2][i] * d[2];
}
} // namespace
// ------------------------------------------------------------------------------- hands
bool Hands::Read() {
if (!map_) {
const int64_t now = MonoNs();
if (now - lastOpenTry_ < 1'000'000'000) return false;
lastOpenTry_ = now;
const char *run = std::getenv("XDG_RUNTIME_DIR");
const std::string path = std::string(run ? run : "/run/user/" + std::to_string(getuid())) + "/frame-hands/hands";
fd_ = open(path.c_str(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (fd_ < 0) return false;
struct stat st;
if (fstat(fd_, &st) < 0 || st.st_uid != getuid() || size_t(st.st_size) < kFileSize) {
close(fd_), fd_ = -1;
return false;
}
void *m = mmap(nullptr, kFileSize, PROT_READ, MAP_SHARED, fd_, 0);
if (m == MAP_FAILED) {
close(fd_), fd_ = -1;
return false;
}
map_ = m;
}
const auto *p = static_cast<const volatile uint8_t *>(map_);
auto u64 = [&](size_t off) { uint64_t v; std::memcpy(&v, const_cast<const uint8_t *>(p) + off, 8); return v; };
const uint64_t s1 = __atomic_load_n(reinterpret_cast<const uint64_t *>(const_cast<const uint8_t *>(p) + 16), __ATOMIC_ACQUIRE);
if ((s1 & 1) || s1 == seq_) return false;
uint8_t copy[kFileSize];
std::memcpy(copy, const_cast<const uint8_t *>(p), kFileSize);
__atomic_thread_fence(__ATOMIC_ACQUIRE);
if (u64(16) != s1 || std::memcmp(copy, kMagic, 8) != 0) return false;
seq_ = s1;
uint64_t capture, publish;
uint32_t nhands, ncaps;
std::memcpy(&capture, copy + 24, 8);
std::memcpy(&publish, copy + 32, 8);
std::memcpy(&nhands, copy + 40, 4);
std::memcpy(&ncaps, copy + 44, 4);
captureNs_ = int64_t(capture), publishNs_ = int64_t(publish);
const Mat head = HeadAt(captureNs_);
// each hand's palm in the room, and its velocity from the last time it was seen
ids_.clear();
std::vector<int> owners; // the hand each capsule belongs to, in file order
for (uint32_t k = 0; k < std::min<uint32_t>(nhands, kMaxHands); ++k) {
const uint8_t *h = copy + kHeader + k * kHand;
uint32_t id, n;
float pts[21][3];
std::memcpy(&id, h, 4);
std::memcpy(pts, h + 16, sizeof pts);
std::memcpy(&n, h + 268, 4);
const int idx = int(ids_.size());
ids_.push_back(id);
owners.insert(owners.end(), std::min<uint32_t>(n, kMaxCapsules), idx);
double palm[3] = {0, 0, 0};
bool ok = true;
for (int j : {0, 5, 9, 13, 17}) {
float w[3];
ok = ok && std::isfinite(pts[j][0]) && std::isfinite(pts[j][1]) && std::isfinite(pts[j][2]);
Apply(head, pts[j], w);
for (int i = 0; i < 3; ++i) palm[i] += w[i] / 5;
}
Motion &m = motion_[id];
const double dt = (captureNs_ - m.ns) / 1e9;
if (!ok) {
m = Motion{};
continue;
}
if (m.ns && dt > 0.005 && dt < 0.2) {
double speed = 0;
for (int i = 0; i < 3; ++i) {
m.v[i] += 0.5 * ((palm[i] - m.palm[i]) / dt - m.v[i]);
speed += m.v[i] * m.v[i];
}
speed = std::sqrt(speed);
if (speed > kMaxSpeed)
for (double &v : m.v) v *= kMaxSpeed / speed;
} else {
m.v[0] = m.v[1] = m.v[2] = 0;
}
m.ns = captureNs_;
std::memcpy(m.palm, palm, sizeof palm);
}
for (auto it = motion_.begin(); it != motion_.end();)
it = captureNs_ - it->second.ns > kStaleNs ? motion_.erase(it) : std::next(it);
if (owners.size() != std::min<uint32_t>(ncaps, kMaxCapsules)) owners.assign(std::min<uint32_t>(ncaps, kMaxCapsules), -1);
base_.clear(), owner_.clear();
for (uint32_t k = 0; k < std::min<uint32_t>(ncaps, kMaxCapsules); ++k) {
float f[8];
std::memcpy(f, copy + kHeader + kMaxHands * kHand + k * kCapsule, sizeof f);
bool ok = true;
for (float v : f) ok = ok && std::isfinite(v) && std::fabs(v) < 10;
if (!ok || f[6] <= 0 || f[7] <= 0) continue;
Capsule c;
Apply(head, f, c.a);
Apply(head, f + 3, c.b);
c.ra = f[6], c.rb = f[7];
base_.push_back(c);
owner_.push_back(owners[k]);
}
return true;
}
void Hands::SetPrediction(bool on, double leadMs) {
predict_ = on;
leadNs_ = int64_t(std::clamp(leadMs, 0.0, 100.0) * 1e6);
}
Mat Hands::HeadAt(int64_t ns) const {
const Past *best = nullptr;
for (const Past &p : history_)
if (!best || std::llabs(p.ns - ns) < std::llabs(best->ns - ns)) best = &p;
return best ? best->head : Mat{};
}
bool Hands::Update(const Mat &head, int64_t nowNs) {
history_.push_back({nowNs, head});
while (!history_.empty() && nowNs - history_.front().ns > kHistoryNs) history_.erase(history_.begin());
Read();
if (nowNs - publishNs_ > kStaleNs) base_.clear(), owner_.clear();
// move each hand ahead to when this frame will be on the displays; a slow hand's
// velocity is mostly tracking noise, so it fades out below kStillSpeed
const double ahead = std::clamp((nowNs + leadNs_ - captureNs_) / 1e9, 0.0, kMaxAhead);
world_ = base_;
for (size_t k = 0; predict_ && k < world_.size(); ++k) {
if (owner_[k] < 0) continue;
const auto m = motion_.find(ids_[owner_[k]]);
if (m == motion_.end()) continue;
const double *v = m->second.v;
const double speed = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
const double gain = std::clamp((speed - kStillSpeed) / kStillSpeed, 0.0, 1.0);
for (int i = 0; i < 3; ++i) {
world_[k].a[i] += float(v[i] * gain * ahead);
world_[k].b[i] += float(v[i] * gain * ahead);
}
}
return !world_.empty();
}
void EyePositions(const Mat &head, double out[2][3]) {
const vr::EVREye eyes[2] = {vr::Eye_Left, vr::Eye_Right};
for (int e = 0; e < 2; ++e) {
const Mat t = vr::VRSystem()->GetEyeToHeadTransform(eyes[e]);
for (int i = 0; i < 3; ++i)
out[e][i] = head.m[i][0] * t.m[0][3] + head.m[i][1] * t.m[1][3] + head.m[i][2] * t.m[2][3] + head.m[i][3];
}
}
// ----------------------------------------------------------------------------- project
namespace {
// Where the line from eye e through point q (both panel-local) meets the panel, as texture
// pixels, and how much a size at q grows there. False if q isn't between the eye and it.
bool OnPanel(const Panel &p, const double e[3], const double q[3], double *x, double *y, double *grow) {
const double d[3] = {q[0] - e[0], q[1] - e[1], q[2] - e[2]};
double s, u, v;
if (p.curve <= 0) {
if (e[2] <= q[2] || q[2] <= 0) return false;
s = e[2] / (e[2] - q[2]);
u = e[0] + s * d[0];
v = e[1] + s * d[1];
} else {
// OpenVR bends a curved panel into a cylinder around (0, *, r), toward its front.
const double r = p.curve, ez = e[2] - r;
const double A = d[0] * d[0] + d[2] * d[2], B = 2 * (e[0] * d[0] + ez * d[2]), C = e[0] * e[0] + ez * ez - r * r;
const double disc = B * B - 4 * A * C;
if (A < 1e-12 || disc < 0) return false;
s = (-B + std::sqrt(disc)) / (2 * A); // the far side: the panel, seen from inside
const double px = e[0] + s * d[0], pz = e[2] + s * d[2];
if (pz > r) return false;
u = r * std::atan2(px, r - pz);
v = e[1] + s * d[1];
}
if (s <= 1) return false; // the hand is behind the panel
*x = (u / p.width + 0.5) * p.pxWidth;
*y = (0.5 - v / p.height) * p.pxHeight;
*grow = s;
return true;
}
} // namespace
bool Project(const Panel &p, const std::vector<Capsule> &caps, const double eyes[2][3], std::vector<Capsule2D> out[2]) {
const double pxPerM = p.pxWidth / p.width;
bool any = false;
for (int e = 0; e < 2; ++e) {
out[e].clear();
double eye[3];
ToLocal(p.pose, eyes[e], eye);
if (eye[2] <= 0.01) continue; // behind the panel
for (const Capsule &c : caps) {
double a[3], b[3];
const double wa[3] = {c.a[0], c.a[1], c.a[2]}, wb[3] = {c.b[0], c.b[1], c.b[2]};
ToLocal(p.pose, wa, a);
ToLocal(p.pose, wb, b);
// a hand pushed through the panel: keep the part in front
const double eps = 0.002;
if (a[2] < eps && b[2] < eps) continue;
if (a[2] < eps || b[2] < eps) {
double *in = a[2] < eps ? b : a, *out3 = a[2] < eps ? a : b;
const double t = (in[2] - eps) / (in[2] - out3[2]);
for (int i = 0; i < 3; ++i) out3[i] = in[i] + t * (out3[i] - in[i]);
}
// and the part near the eye's plane: it would land far across the panel with a
// huge radius, so one bad hand estimate there tears a hole through the screen
const double zmax = eye[2] - kNear;
if (a[2] > zmax && b[2] > zmax) continue;
if (a[2] > zmax || b[2] > zmax) {
double *in = a[2] > zmax ? b : a, *out3 = a[2] > zmax ? a : b;
const double t = (zmax - in[2]) / (out3[2] - in[2]);
for (int i = 0; i < 3; ++i) out3[i] = in[i] + t * (out3[i] - in[i]);
}
double ax, ay, ga, bx, by, gb;
if (!OnPanel(p, eye, a, &ax, &ay, &ga) || !OnPanel(p, eye, b, &bx, &by, &gb)) continue;
const float ra = float(c.ra * ga * pxPerM), rb = float(c.rb * gb * pxPerM);
const float r = std::max(ra, rb);
if (std::max(ax, bx) + r < 0 || std::min(ax, bx) - r > p.pxWidth || std::max(ay, by) + r < 0 ||
std::min(ay, by) - r > p.pxHeight)
continue;
out[e].push_back({float(ax), float(ay), float(bx), float(by), ra, rb});
any = true;
}
}
return any;
}
// ---------------------------------------------------------------------------- renderer
namespace {
PFNEGLGETPLATFORMDISPLAYEXTPROC pGetPlatformDisplay;
PFNEGLCREATEIMAGEKHRPROC pCreateImage;
PFNEGLDESTROYIMAGEKHRPROC pDestroyImage;
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pImageTargetTexture;
PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC pImageTargetRenderbuffer;
const char *kVertex = R"(
attribute vec2 pos; // the unit square
uniform vec4 rect; // where it goes, in pixels of the eye's half: x0 y0 x1 y1
uniform vec2 size; // the half's size in pixels
varying vec2 px;
varying vec2 uv;
void main() {
px = mix(rect.xy, rect.zw, pos);
uv = px / size;
gl_Position = vec4(uv * 2.0 - 1.0, 0.0, 1.0);
})";
// The client's pixels, opaque (its alpha is ignored, as IgnoreTextureAlpha did).
const char *kCopy = R"(
#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform samplerExternalOES tex;
varying vec2 uv;
void main() { gl_FragColor = vec4(texture2D(tex, uv).rgb, 1.0); })";
// Coverage of one tapered capsule; blended to take that much alpha away.
const char *kCut = R"(
precision highp float;
uniform vec2 a, b, r;
uniform float feather;
varying vec2 px;
void main() {
vec2 ab = b - a;
float t = clamp(dot(px - a, ab) / max(dot(ab, ab), 1e-6), 0.0, 1.0);
float d = length(px - (a + t * ab));
float rad = mix(r.x, r.y, t);
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0 - smoothstep(rad - feather, rad + feather, d));
})";
unsigned Shader(GLenum type, const char *src) {
const GLuint s = glCreateShader(type);
glShaderSource(s, 1, &src, nullptr);
glCompileShader(s);
GLint ok = 0;
glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[1024] = "";
glGetShaderInfoLog(s, sizeof log, nullptr, log);
std::fprintf(stderr, "handcut: shader: %s\n", log);
}
return s;
}
unsigned Program(const char *fs) {
const GLuint p = glCreateProgram();
glAttachShader(p, Shader(GL_VERTEX_SHADER, kVertex));
glAttachShader(p, Shader(GL_FRAGMENT_SHADER, fs));
glBindAttribLocation(p, 0, "pos");
glLinkProgram(p);
GLint ok = 0;
glGetProgramiv(p, GL_LINK_STATUS, &ok);
if (!ok) {
char log[1024] = "";
glGetProgramInfoLog(p, sizeof log, nullptr, log);
std::fprintf(stderr, "handcut: program: %s\n", log);
return 0;
}
return p;
}
EGLImageKHR ImageFor(EGLDisplay dpy, const ft_dmabuf &b) {
static const EGLint fd[4] = {EGL_DMA_BUF_PLANE0_FD_EXT, EGL_DMA_BUF_PLANE1_FD_EXT, EGL_DMA_BUF_PLANE2_FD_EXT,
EGL_DMA_BUF_PLANE3_FD_EXT};
static const EGLint off[4] = {EGL_DMA_BUF_PLANE0_OFFSET_EXT, EGL_DMA_BUF_PLANE1_OFFSET_EXT,
EGL_DMA_BUF_PLANE2_OFFSET_EXT, EGL_DMA_BUF_PLANE3_OFFSET_EXT};
static const EGLint pitch[4] = {EGL_DMA_BUF_PLANE0_PITCH_EXT, EGL_DMA_BUF_PLANE1_PITCH_EXT,
EGL_DMA_BUF_PLANE2_PITCH_EXT, EGL_DMA_BUF_PLANE3_PITCH_EXT};
static const EGLint lo[4] = {EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT,
EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT};
static const EGLint hi[4] = {EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT,
EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT, EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT};
EGLint a[64];
int n = 0;
a[n++] = EGL_WIDTH, a[n++] = b.width, a[n++] = EGL_HEIGHT, a[n++] = b.height;
a[n++] = EGL_LINUX_DRM_FOURCC_EXT, a[n++] = EGLint(b.format);
for (int i = 0; i < b.n_planes && i < 4; ++i) {
a[n++] = fd[i], a[n++] = b.fd[i], a[n++] = off[i], a[n++] = EGLint(b.offset[i]);
a[n++] = pitch[i], a[n++] = EGLint(b.stride[i]);
if (b.modifier != DRM_FORMAT_MOD_INVALID) {
a[n++] = lo[i], a[n++] = EGLint(b.modifier & 0xffffffff);
a[n++] = hi[i], a[n++] = EGLint(b.modifier >> 32);
}
}
a[n++] = EGL_NONE;
return pCreateImage(dpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, a);
}
} // namespace
Renderer::~Renderer() {
for (auto &[k, r] : rings_)
for (Output &o : r.out) FreeOutput(o);
for (auto &[k, im] : imported_) {
glDeleteTextures(1, &im.tex);
pDestroyImage(EGLDisplay(dpy_), EGLImageKHR(im.image));
}
if (ctx_) eglDestroyContext(EGLDisplay(dpy_), EGLContext(ctx_));
if (dpy_) eglTerminate(EGLDisplay(dpy_));
if (gbm_) gbm_device_destroy(static_cast<gbm_device *>(gbm_));
if (drm_ >= 0) close(drm_);
}
bool Renderer::Init(const std::vector<uint64_t> &modifiers, std::function<void(const Output *)> released) {
if (ready_) return true;
modifiers_ = modifiers;
released_ = std::move(released);
drm_ = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
if (drm_ < 0) return std::perror("handcut: /dev/dri/renderD128"), false;
gbm_ = gbm_create_device(drm_);
pGetPlatformDisplay = reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(eglGetProcAddress("eglGetPlatformDisplayEXT"));
pCreateImage = reinterpret_cast<PFNEGLCREATEIMAGEKHRPROC>(eglGetProcAddress("eglCreateImageKHR"));
pDestroyImage = reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC>(eglGetProcAddress("eglDestroyImageKHR"));
pImageTargetTexture = reinterpret_cast<PFNGLEGLIMAGETARGETTEXTURE2DOESPROC>(eglGetProcAddress("glEGLImageTargetTexture2DOES"));
pImageTargetRenderbuffer = reinterpret_cast<PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC>(
eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES"));
if (!gbm_ || !pGetPlatformDisplay || !pCreateImage || !pImageTargetTexture || !pImageTargetRenderbuffer) {
std::fprintf(stderr, "handcut: GBM or EGL extensions missing\n");
return false;
}
EGLDisplay dpy = pGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm_, nullptr);
if (dpy == EGL_NO_DISPLAY || !eglInitialize(dpy, nullptr, nullptr)) return std::fprintf(stderr, "handcut: no EGL display\n"), false;
dpy_ = dpy;
eglBindAPI(EGL_OPENGL_ES_API);
const EGLint attrs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
EGLContext ctx = eglCreateContext(dpy, EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT, attrs);
if (ctx == EGL_NO_CONTEXT || !eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx))
return std::fprintf(stderr, "handcut: no surfaceless GLES context\n"), false;
ctx_ = ctx;
copyProg_ = Program(kCopy);
cutProg_ = Program(kCut);
if (!copyProg_ || !cutProg_) return false;
const float quad[] = {0, 0, 1, 0, 0, 1, 1, 1};
glGenBuffers(1, &vbo_);
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
glBufferData(GL_ARRAY_BUFFER, sizeof quad, quad, GL_STATIC_DRAW);
ready_ = true;
return true;
}
unsigned Renderer::Texture(const void *key, const ft_dmabuf &src) {
auto it = imported_.find(key);
if (it != imported_.end()) return it->second.tex;
EGLImageKHR image = ImageFor(EGLDisplay(dpy_), src);
if (image == EGL_NO_IMAGE_KHR) {
std::fprintf(stderr, "handcut: can't import a %dx%d client buffer (format 0x%x modifier 0x%llx)\n", src.width,
src.height, src.format, (unsigned long long)src.modifier);
return 0;
}
GLuint tex;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, tex);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
pImageTargetTexture(GL_TEXTURE_EXTERNAL_OES, image);
imported_[key] = {image, tex};
return tex;
}
void Renderer::Forget(const void *key) {
auto it = imported_.find(key);
if (it == imported_.end()) return;
glDeleteTextures(1, &it->second.tex);
pDestroyImage(EGLDisplay(dpy_), EGLImageKHR(it->second.image));
imported_.erase(it);
}
bool Renderer::MakeOutput(Output &o, int w, int h) {
auto *gbm = static_cast<gbm_device *>(gbm_);
std::vector<uint64_t> mods;
for (uint64_t m : modifiers_)
if (m != DRM_FORMAT_MOD_INVALID) mods.push_back(m);
gbm_bo *bo = mods.empty() ? gbm_bo_create(gbm, w, h, GBM_FORMAT_ABGR8888, GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR)
: gbm_bo_create_with_modifiers2(gbm, w, h, GBM_FORMAT_ABGR8888, mods.data(),
unsigned(mods.size()), GBM_BO_USE_RENDERING);
if (!bo) return std::fprintf(stderr, "handcut: can't allocate a %dx%d output\n", w, h), false;
o.bo = bo;
o.buf = {};
o.buf.width = w, o.buf.height = h;
o.buf.format = DRM_FORMAT_ABGR8888;
o.buf.modifier = mods.empty() ? DRM_FORMAT_MOD_LINEAR : gbm_bo_get_modifier(bo);
o.buf.n_planes = gbm_bo_get_plane_count(bo);
for (int i = 0; i < o.buf.n_planes && i < 4; ++i) {
o.buf.fd[i] = gbm_bo_get_fd_for_plane(bo, i);
o.buf.offset[i] = gbm_bo_get_offset(bo, i);
o.buf.stride[i] = gbm_bo_get_stride_for_plane(bo, i);
}
EGLImageKHR image = ImageFor(EGLDisplay(dpy_), o.buf);
if (image == EGL_NO_IMAGE_KHR) return std::fprintf(stderr, "handcut: can't render to the output\n"), FreeOutput(o), false;
o.image = image;
glGenRenderbuffers(1, &o.rb);
glBindRenderbuffer(GL_RENDERBUFFER, o.rb);
pImageTargetRenderbuffer(GL_RENDERBUFFER, image);
glGenFramebuffers(1, &o.fbo);
glBindFramebuffer(GL_FRAMEBUFFER, o.fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, o.rb);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
return std::fprintf(stderr, "handcut: output framebuffer incomplete\n"), FreeOutput(o), false;
return true;
}
void Renderer::FreeOutput(Output &o) {
if (o.bo && released_) released_(&o);
if (o.fbo) glDeleteFramebuffers(1, &o.fbo);
if (o.rb) glDeleteRenderbuffers(1, &o.rb);
if (o.image) pDestroyImage(EGLDisplay(dpy_), EGLImageKHR(o.image));
for (int i = 0; i < o.buf.n_planes && i < 4; ++i)
if (o.buf.fd[i] >= 0) close(o.buf.fd[i]);
if (o.bo) gbm_bo_destroy(static_cast<gbm_bo *>(o.bo));
o = Output{};
}
void Renderer::DropPanel(int panel) {
auto it = rings_.find(panel);
if (it == rings_.end()) return;
for (Output &o : it->second.out) FreeOutput(o);
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;
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) {
glViewport(e * w, 0, w, h);
glDisable(GL_BLEND);
glUseProgram(copyProg_);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, tex);
glUniform1i(glGetUniformLocation(copyProg_, "tex"), 0);
glUniform4f(glGetUniformLocation(copyProg_, "rect"), 0, 0, float(w), float(h));
glUniform2f(glGetUniformLocation(copyProg_, "size"), float(w), float(h));
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// take alpha away where the hand is; the colour stays (straight alpha)
glEnable(GL_BLEND);
glBlendFuncSeparate(GL_ZERO, GL_ONE, GL_ZERO, GL_ONE_MINUS_SRC_ALPHA);
glUseProgram(cutProg_);
glUniform2f(glGetUniformLocation(cutProg_, "size"), float(w), float(h));
const GLint uRect = glGetUniformLocation(cutProg_, "rect"), uA = glGetUniformLocation(cutProg_, "a"),
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);
glUniform2f(uA, c.ax, c.ay);
glUniform2f(uB, c.bx, c.by);
glUniform2f(uR, c.ra, c.rb);
glUniform1f(uF, feather);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
}
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;
}
} // namespace handcut