mirror of
https://github.com/spyjacob/steam-frame-chroma-key.git
synced 2026-10-17 19:01:25 +02:00
- Passthrough slider in the panel; mrkey.sh passthru patches the two camera shaders so the room behind the game is as bright as SteamVR's pass-through view (tools/gen_cam_spv.py), with adjustable brightness - the panel keeps forceSceneAlphaBlending on only while a PC stream runs, so standalone apps and Room View are stock (no faint passthrough bleed) - layer: read-only camera-pass probe in the compositor log - README: new panel screenshot and the new features Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VavVgDGiQzkiSpHzFhuqk
504 lines
22 KiB
C++
504 lines
22 KiB
C++
// mrtoggle_overlay — SteamVR dashboard overlay: chroma-key on/off, key colour,
|
|
// similarity / edge softness / spill, passthrough brightness. Public OpenVR overlay API only.
|
|
//
|
|
// Drives the same scripts as the PC panel:
|
|
// mrtoggle.sh toggle|status on/off (keeps the generated shader)
|
|
// mrkey.sh chroma R G B thr soft spill regenerate + hot-reload (turns keying on)
|
|
// and polls mrtoggle.sh status / mrkey.state so PC-side changes show up here.
|
|
// The UI is software-rendered (FreeType text) and uploaded with SetOverlayRaw.
|
|
#include <openvr.h>
|
|
#include <ft2build.h>
|
|
#include FT_FREETYPE_H
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <dirent.h>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <sys/stat.h>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
using namespace vr;
|
|
using Clock = std::chrono::steady_clock;
|
|
|
|
static const int W = 1024, H = 930;
|
|
|
|
// ---------------------------------------------------------------- settings
|
|
struct Key {
|
|
float r = 0, g = 1, b = 0;
|
|
float thr = 0.32f, soft = 0.12f, spill = 0.5f;
|
|
float bright = 1.0f; // passthrough brightness (mrkey.sh passthru), not part of the key shader
|
|
};
|
|
struct Range { const char *label; float Key::*field; float lo, hi; };
|
|
static const Range kSliders[] = {
|
|
{"Similarity", &Key::thr, 0.02f, 0.70f},
|
|
{"Edge softness", &Key::soft, 0.005f, 0.30f},
|
|
{"Spill removal", &Key::spill, 0.0f, 1.0f},
|
|
{"Passthrough", &Key::bright, 0.5f, 2.5f},
|
|
};
|
|
static const int kNumSliders = 4, kBrightSlider = 3;
|
|
// keyed view draws the camera at opacity 0.6 vs 1.0 in pass-only; this gain matches them
|
|
static const char *kPassthruGain = "1.6667";
|
|
struct Preset { const char *name; float r, g, b; };
|
|
static const Preset kPresets[] = {
|
|
{"Green", 0, 1, 0}, {"Blue", 0, 0, 1}, {"Magenta", 1, 0, 1}, {"Red", 1, 0, 0}, {"Cyan", 0, 1, 1},
|
|
};
|
|
|
|
static std::string g_home, g_toggle, g_mrkey, g_state, g_passthru;
|
|
|
|
static std::string runLine(const std::string &cmd) {
|
|
FILE *p = popen(cmd.c_str(), "r");
|
|
if (!p) return "";
|
|
char buf[256] = {};
|
|
if (!fgets(buf, sizeof buf, p)) buf[0] = 0;
|
|
pclose(p);
|
|
std::string s(buf);
|
|
while (!s.empty() && isspace((unsigned char)s.back())) s.pop_back();
|
|
return s;
|
|
}
|
|
|
|
// The key shader only patches the streamed (vrlink) scene pass. In standalone the
|
|
// compositor draws native apps / SteamVR Home with other shaders whose alpha is not 1,
|
|
// so camera/forceSceneAlphaBlending would bleed passthrough through everything.
|
|
// Keep it on only while a vrlink stream is running.
|
|
static bool streaming() {
|
|
DIR *d = opendir("/proc");
|
|
if (!d) return false;
|
|
bool found = false;
|
|
while (dirent *e = readdir(d)) {
|
|
if (!isdigit((unsigned char)e->d_name[0])) continue;
|
|
// argv[0], not comm: vrlink renames its main thread to "vrlinkrunthread"
|
|
char path[64], arg0[256] = {};
|
|
snprintf(path, sizeof path, "/proc/%s/cmdline", e->d_name);
|
|
if (FILE *f = fopen(path, "r")) {
|
|
size_t n = fread(arg0, 1, sizeof arg0 - 1, f);
|
|
arg0[n] = 0;
|
|
const char *base = strrchr(arg0, '/');
|
|
if (!strcmp(base ? base + 1 : arg0, "vrlink")) found = true;
|
|
fclose(f);
|
|
}
|
|
if (found) break;
|
|
}
|
|
closedir(d);
|
|
return found;
|
|
}
|
|
static void syncSceneAlpha() {
|
|
struct stat st;
|
|
if (stat((g_home + "/.config/mrchroma/enable-compositor").c_str(), &st) != 0) return; // not ours to manage
|
|
bool want = streaming();
|
|
EVRSettingsError se = VRSettingsError_None;
|
|
bool cur = VRSettings()->GetBool("camera", "forceSceneAlphaBlending", &se); // unset reads as false
|
|
if (cur == want) return;
|
|
if (want) VRSettings()->SetBool("camera", "forceSceneAlphaBlending", true, &se);
|
|
else VRSettings()->RemoveKeyInSection("camera", "forceSceneAlphaBlending", &se);
|
|
fprintf(stderr, "forceSceneAlphaBlending -> %d (%s)\n", want, want ? "streaming" : "standalone");
|
|
}
|
|
|
|
static bool readState(Key &k) {
|
|
FILE *f = fopen(g_state.c_str(), "r");
|
|
if (!f) return false;
|
|
char mode[32];
|
|
Key n = k; // keeps bright, which lives in passthru.state
|
|
int got = fscanf(f, "%31s %f %f %f %f %f %f", mode, &n.r, &n.g, &n.b, &n.thr, &n.soft, &n.spill);
|
|
fclose(f);
|
|
if (got == 7 && !strcmp(mode, "chroma")) { k = n; return true; }
|
|
return false;
|
|
}
|
|
|
|
static void readPassthru(Key &k) { // "<gain> <brightness>" or "off" (written by mrkey.sh passthru)
|
|
float g, b;
|
|
FILE *f = fopen(g_passthru.c_str(), "r");
|
|
k.bright = 1.0f;
|
|
if (!f) return;
|
|
if (fscanf(f, "%f %f", &g, &b) == 2) k.bright = b;
|
|
fclose(f);
|
|
}
|
|
|
|
static time_t mtimeOf(const std::string &p) {
|
|
struct stat st;
|
|
return stat(p.c_str(), &st) == 0 ? st.st_mtime : 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- drawing
|
|
struct Canvas {
|
|
std::vector<uint8_t> px = std::vector<uint8_t>(W * H * 4);
|
|
FT_Library ft = nullptr;
|
|
FT_Face bold = nullptr, reg = nullptr;
|
|
|
|
void blend(int x, int y, uint32_t rgb, float a) {
|
|
if (x < 0 || y < 0 || x >= W || y >= H || a <= 0) return;
|
|
uint8_t *p = &px[(y * W + x) * 4];
|
|
float c[3] = {float(rgb >> 16 & 255), float(rgb >> 8 & 255), float(rgb & 255)};
|
|
float da = p[3] / 255.f, oa = a + da * (1 - a);
|
|
for (int i = 0; i < 3; i++) p[i] = uint8_t((c[i] * a + p[i] * da * (1 - a)) / std::max(oa, 1e-4f));
|
|
p[3] = uint8_t(oa * 255);
|
|
}
|
|
void clear() { std::fill(px.begin(), px.end(), 0); }
|
|
// filled rounded rect with 1px AA edge
|
|
void rrect(float x0, float y0, float x1, float y1, float rad, uint32_t rgb, float a = 1) {
|
|
for (int y = int(y0); y < int(ceil(y1)); y++)
|
|
for (int x = int(x0); x < int(ceil(x1)); x++) {
|
|
float cx = std::clamp(x + .5f, x0 + rad, x1 - rad), cy = std::clamp(y + .5f, y0 + rad, y1 - rad);
|
|
float d = hypotf(x + .5f - cx, y + .5f - cy) - rad;
|
|
blend(x, y, rgb, a * std::clamp(0.5f - d, 0.f, 1.f));
|
|
}
|
|
}
|
|
void ring(float x0, float y0, float x1, float y1, float rad, float w, uint32_t rgb) {
|
|
for (int y = int(y0 - 1); y < int(y1 + 1); y++)
|
|
for (int x = int(x0 - 1); x < int(x1 + 1); x++) {
|
|
float cx = std::clamp(x + .5f, x0 + rad, x1 - rad), cy = std::clamp(y + .5f, y0 + rad, y1 - rad);
|
|
float d = fabsf(hypotf(x + .5f - cx, y + .5f - cy) - rad) - w / 2;
|
|
blend(x, y, rgb, std::clamp(0.5f - d, 0.f, 1.f));
|
|
}
|
|
}
|
|
void circle(float cx, float cy, float r, uint32_t rgb) {
|
|
for (int y = int(cy - r - 1); y <= int(cy + r + 1); y++)
|
|
for (int x = int(cx - r - 1); x <= int(cx + r + 1); x++)
|
|
blend(x, y, rgb, std::clamp(r + 0.5f - hypotf(x + .5f - cx, y + .5f - cy), 0.f, 1.f));
|
|
}
|
|
// returns advance width; align: 0 left, 1 centre, 2 right
|
|
int text(const std::string &s, int x, int y, int size, uint32_t rgb, bool b = false, int align = 0) {
|
|
FT_Face f = b ? bold : reg;
|
|
if (!f) return 0;
|
|
FT_Set_Pixel_Sizes(f, 0, size);
|
|
int w = 0;
|
|
for (char c : s) { if (!FT_Load_Char(f, c, FT_LOAD_DEFAULT)) w += f->glyph->advance.x >> 6; }
|
|
int pen = align == 1 ? x - w / 2 : align == 2 ? x - w : x;
|
|
for (char c : s) {
|
|
if (FT_Load_Char(f, c, FT_LOAD_RENDER)) continue;
|
|
FT_Bitmap &bm = f->glyph->bitmap;
|
|
for (unsigned yy = 0; yy < bm.rows; yy++)
|
|
for (unsigned xx = 0; xx < bm.width; xx++)
|
|
blend(pen + f->glyph->bitmap_left + xx, y - f->glyph->bitmap_top + yy, rgb,
|
|
bm.buffer[yy * bm.pitch + xx] / 255.f);
|
|
pen += f->glyph->advance.x >> 6;
|
|
}
|
|
return w;
|
|
}
|
|
};
|
|
|
|
static uint32_t rgbOf(float r, float g, float b) {
|
|
auto c = [](float v) { return uint32_t(std::clamp(v, 0.f, 1.f) * 255 + 0.5f); };
|
|
return c(r) << 16 | c(g) << 8 | c(b);
|
|
}
|
|
static void hsv(float h, float &r, float &g, float &b) {
|
|
h = fmodf(h, 1.f) * 6;
|
|
float x = 1 - fabsf(fmodf(h, 2.f) - 1);
|
|
int i = int(h);
|
|
float t[6][3] = {{1, x, 0}, {x, 1, 0}, {0, 1, x}, {0, x, 1}, {x, 0, 1}, {1, 0, x}};
|
|
r = t[i % 6][0]; g = t[i % 6][1]; b = t[i % 6][2];
|
|
}
|
|
static float hueOf(float r, float g, float b) {
|
|
float mx = std::max({r, g, b}), mn = std::min({r, g, b}), d = mx - mn;
|
|
if (d < 1e-4f) return 0;
|
|
float h = mx == r ? fmodf((g - b) / d, 6.f) : mx == g ? (b - r) / d + 2 : (r - g) / d + 4;
|
|
h /= 6;
|
|
return h < 0 ? h + 1 : h;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- layout
|
|
struct Rect { float x0, y0, x1, y1; bool in(float x, float y) const { return x >= x0 && x < x1 && y >= y0 && y < y1; } };
|
|
static const Rect kToggle = {664, 28, 996, 128};
|
|
static Rect presetRect(int i) { return {300.f + i * 140, 168, 300.f + i * 140 + 120, 248}; }
|
|
static const Rect kHue = {300, 290, 996, 350};
|
|
static Rect sliderRect(int i) { return {300, 418.f + i * 100, 996, 466.f + i * 100}; }
|
|
static const Rect kBgOn = {300, 806, 644, 872}, kBgOff = {652, 806, 996, 872};
|
|
|
|
// PC home background: wanted state lives on the headset, the PC helper acks what it applied
|
|
struct PcBg { bool on = false; std::string applied; };
|
|
enum Sync { SyncOk, SyncWaiting, SyncStale };
|
|
|
|
enum Drag { DragNone = -1, DragHue = 10 }; // 0..3 = slider index
|
|
|
|
static void render(Canvas &cv, const Key &k, const std::string &status, bool busy, int drag, const PcBg &bg, Sync sync) {
|
|
cv.clear();
|
|
cv.rrect(0, 0, W, H, 40, 0x1c1d22, 0.94f);
|
|
cv.ring(4, 4, W - 4, H - 4, 36, 4, 0x4a4d57);
|
|
cv.text("CHROMA KEY", 40, 98, 56, 0xe8e8e8, true);
|
|
|
|
bool on = status == "on";
|
|
uint32_t tc = busy ? 0x9a9a9a : on ? 0x5ae678 : 0xeb5f55;
|
|
cv.rrect(kToggle.x0, kToggle.y0, kToggle.x1, kToggle.y1, 50, busy ? 0x34343a : on ? 0x173a22 : 0x3d1d1d);
|
|
cv.ring(kToggle.x0, kToggle.y0, kToggle.x1, kToggle.y1, 50, 6, tc);
|
|
cv.text(busy ? "..." : on ? "ON" : "OFF", (kToggle.x0 + kToggle.x1) / 2, 103, 64, tc, true, 1);
|
|
|
|
// key colour
|
|
cv.text("Key colour", 40, 200, 34, 0xc8c8c8);
|
|
cv.rrect(40, 214, 250, 272, 18, rgbOf(k.r, k.g, k.b));
|
|
cv.ring(40, 214, 250, 272, 18, 3, 0xe8e8e8);
|
|
for (int i = 0; i < 5; i++) {
|
|
Rect r = presetRect(i);
|
|
const Preset &p = kPresets[i];
|
|
bool sel = fabsf(p.r - k.r) + fabsf(p.g - k.g) + fabsf(p.b - k.b) < 0.02f;
|
|
cv.rrect(r.x0, r.y0, r.x1, r.y1, 16, rgbOf(p.r, p.g, p.b));
|
|
cv.ring(r.x0, r.y0, r.x1, r.y1, 16, sel ? 6 : 2, sel ? 0xffffff : 0x6a6d77);
|
|
cv.text(p.name, (r.x0 + r.x1) / 2, r.y1 + 30, 24, 0xb0b0b0, false, 1);
|
|
}
|
|
for (int x = int(kHue.x0); x < int(kHue.x1); x++) {
|
|
float r, g, b;
|
|
hsv((x - kHue.x0) / (kHue.x1 - kHue.x0), r, g, b);
|
|
for (int y = int(kHue.y0) + 10; y < int(kHue.y1) - 10; y++) cv.blend(x, y, rgbOf(r, g, b), 1);
|
|
}
|
|
float hx = kHue.x0 + hueOf(k.r, k.g, k.b) * (kHue.x1 - kHue.x0);
|
|
cv.ring(hx - 10, kHue.y0, hx + 10, kHue.y1, 8, drag == DragHue ? 6 : 4, 0xffffff);
|
|
cv.text("Hue", 40, 335, 30, 0xb0b0b0);
|
|
|
|
for (int i = 0; i < kNumSliders; i++) {
|
|
const Range &s = kSliders[i];
|
|
Rect r = sliderRect(i);
|
|
float v = k.*(s.field), t = (v - s.lo) / (s.hi - s.lo);
|
|
float cy = (r.y0 + r.y1) / 2;
|
|
cv.text(s.label, 40, int(cy + 12), 34, 0xc8c8c8);
|
|
cv.rrect(r.x0, cy - 7, r.x1, cy + 7, 7, 0x3a3c44);
|
|
cv.rrect(r.x0, cy - 7, r.x0 + t * (r.x1 - r.x0), cy + 7, 7, 0x5a9cff);
|
|
cv.circle(r.x0 + t * (r.x1 - r.x0), cy, drag == i ? 24 : 20, 0xf0f0f0);
|
|
char buf[16];
|
|
snprintf(buf, sizeof buf, "%.2f", v);
|
|
cv.text(buf, int(r.x1), int(r.y0 - 6), 26, 0x9a9a9a, false, 2);
|
|
}
|
|
// desktop (PC home) background
|
|
cv.text("Desktop", 40, 834, 34, 0xc8c8c8);
|
|
cv.text("background", 40, 870, 34, 0xc8c8c8);
|
|
for (int i = 0; i < 2; i++) {
|
|
const Rect &r = i == 0 ? kBgOn : kBgOff;
|
|
bool sel = (i == 0) == bg.on;
|
|
cv.rrect(r.x0, r.y0, r.x1, r.y1, 18, sel ? 0x2b4f8a : 0x2a2c33);
|
|
cv.ring(r.x0, r.y0, r.x1, r.y1, 18, sel ? 4 : 2, sel ? 0x8fb8ff : 0x4a4d57);
|
|
if (i == 0) cv.rrect(r.x0 + 18, r.y0 + 20, r.x0 + 44, r.y1 - 20, 6, rgbOf(k.r, k.g, k.b));
|
|
cv.text(i == 0 ? "Key colour" : "Mountains", (r.x0 + r.x1) / 2 + (i == 0 ? 14 : 0), int(r.y1 - 21), 30,
|
|
sel ? 0xffffff : 0xa0a0a0, sel, 1);
|
|
}
|
|
const char *st = sync == SyncOk ? "PC in sync" : sync == SyncWaiting ? "waiting for PC..." : "PC helper not responding";
|
|
cv.text(st, 996, 800, 22, sync == SyncOk ? 0x5ae678 : sync == SyncWaiting ? 0xe0b050 : 0xeb5f55, false, 2);
|
|
cv.text(on ? "Changes apply when you release the trigger"
|
|
: "Changing colour or sliders turns keying back on", W / 2, H - 22, 24, 0x80838c, false, 1);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- main
|
|
int main(int argc, char **argv) {
|
|
const char *home = getenv("HOME");
|
|
g_home = home ? home : "/home/steamos";
|
|
g_toggle = argc > 1 && argv[1][0] != '-' ? argv[1] : g_home + "/mr-chroma/mrtoggle.sh";
|
|
std::string dir = g_toggle.substr(0, g_toggle.rfind('/'));
|
|
g_mrkey = dir + "/mrkey.sh";
|
|
g_state = g_home + "/.local/share/mrchroma/mrkey.state";
|
|
g_passthru = g_home + "/.local/share/mrchroma/passthru.state";
|
|
std::string self = argv[0];
|
|
char real[4096];
|
|
if (realpath("/proc/self/exe", real)) self = real;
|
|
std::string resDir = self.substr(0, self.rfind('/'));
|
|
|
|
Canvas cv;
|
|
FT_Init_FreeType(&cv.ft);
|
|
FT_New_Face(cv.ft, "/usr/share/fonts/noto/NotoSans-Bold.ttf", 0, &cv.bold);
|
|
FT_New_Face(cv.ft, "/usr/share/fonts/noto/NotoSans-Regular.ttf", 0, &cv.reg);
|
|
if (!cv.reg) cv.reg = cv.bold;
|
|
|
|
if (argc > 2 && !strcmp(argv[1], "--render")) { // layout check without SteamVR: --render out.ppm
|
|
Key k;
|
|
readState(k);
|
|
render(cv, k, "on", false, DragNone, PcBg{true, ""}, SyncOk);
|
|
FILE *f = fopen(argv[2], "wb");
|
|
fprintf(f, "P6 %d %d 255\n", W, H);
|
|
for (int i = 0; i < W * H; i++) {
|
|
float a = cv.px[i * 4 + 3] / 255.f; // composite over mid grey
|
|
for (int c = 0; c < 3; c++) fputc(int(cv.px[i * 4 + c] * a + 96 * (1 - a)), f);
|
|
}
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
|
|
EVRInitError ie;
|
|
VR_Init(&ie, VRApplication_Overlay);
|
|
if (ie != VRInitError_None) {
|
|
fprintf(stderr, "VR_Init: %s\n", VR_GetVRInitErrorAsEnglishDescription(ie));
|
|
return 1;
|
|
}
|
|
IVROverlay *ov = VROverlay();
|
|
VROverlayHandle_t main = k_ulOverlayHandleInvalid, thumb = k_ulOverlayHandleInvalid;
|
|
EVROverlayError oe = ov->CreateDashboardOverlay("mrchroma.toggle", "Chroma Key", &main, &thumb);
|
|
if (oe != VROverlayError_None) {
|
|
fprintf(stderr, "CreateDashboardOverlay: %s\n", ov->GetOverlayErrorNameFromEnum(oe));
|
|
VR_Shutdown();
|
|
return 1;
|
|
}
|
|
ov->SetOverlayWidthInMeters(main, 2.0f);
|
|
ov->SetOverlayInputMethod(main, VROverlayInputMethod_Mouse);
|
|
HmdVector2_t scale = {{float(W), float(H)}};
|
|
ov->SetOverlayMouseScale(main, &scale);
|
|
ov->SetOverlayFromFile(thumb, (resDir + "/icon.png").c_str());
|
|
|
|
Key key;
|
|
readState(key);
|
|
readPassthru(key);
|
|
time_t stateMtime = mtimeOf(g_state), passthruMtime = mtimeOf(g_passthru);
|
|
std::string status = runLine(g_toggle + " status");
|
|
const std::string bgWantPath = g_home + "/.config/mrchroma/pcbg";
|
|
const std::string bgAckPath = g_home + "/.local/share/mrchroma/pcbg.applied";
|
|
auto readWord = [](const std::string &p) {
|
|
std::string s;
|
|
if (FILE *f = fopen(p.c_str(), "r")) {
|
|
char b[64] = {};
|
|
if (fgets(b, sizeof b, f)) s = b;
|
|
fclose(f);
|
|
}
|
|
while (!s.empty() && isspace((unsigned char)s.back())) s.pop_back();
|
|
return s;
|
|
};
|
|
PcBg bg;
|
|
bg.on = readWord(bgWantPath) == "on";
|
|
bg.applied = readWord(bgAckPath);
|
|
auto mismatchSince = Clock::now();
|
|
// does the PC's ack match what the headset wants? (colour compared within 1/255 per channel)
|
|
auto inSync = [&]() {
|
|
if (!bg.on) return bg.applied == "off";
|
|
unsigned r, g, b;
|
|
if (sscanf(bg.applied.c_str(), "on #%2x%2x%2x", &r, &g, &b) != 3) return false;
|
|
auto near = [](unsigned v, float f) { return abs(int(v) - int(lroundf(f * 255))) <= 1; };
|
|
return near(r, key.r) && near(g, key.g) && near(b, key.b);
|
|
};
|
|
|
|
// background worker: runs one command at a time, latest request wins
|
|
std::mutex m;
|
|
std::string pending;
|
|
std::atomic<bool> busy{false};
|
|
bool quit = false;
|
|
std::thread worker([&] {
|
|
while (true) {
|
|
std::string cmd;
|
|
{
|
|
std::lock_guard<std::mutex> l(m);
|
|
if (quit && pending.empty()) return;
|
|
cmd.swap(pending);
|
|
}
|
|
if (cmd.empty()) { std::this_thread::sleep_for(std::chrono::milliseconds(20)); continue; }
|
|
busy = true;
|
|
if (system((cmd + " >/dev/null 2>&1").c_str()) != 0) fprintf(stderr, "failed: %s\n", cmd.c_str());
|
|
busy = false;
|
|
}
|
|
});
|
|
auto submit = [&](const std::string &cmd) {
|
|
std::lock_guard<std::mutex> l(m);
|
|
pending = cmd;
|
|
busy = true;
|
|
};
|
|
auto applyKey = [&] {
|
|
char buf[256];
|
|
snprintf(buf, sizeof buf, "%s chroma %.4f %.4f %.4f %.4f %.4f %.4f", g_mrkey.c_str(), key.r, key.g, key.b,
|
|
key.thr, key.soft, key.spill);
|
|
submit(buf);
|
|
};
|
|
|
|
int drag = DragNone;
|
|
bool dirty = true, wasBusy = false;
|
|
auto lastPoll = Clock::now(), lastDraw = Clock::time_point();
|
|
auto setFromX = [&](int which, float x) {
|
|
if (which == DragHue) {
|
|
float h = std::clamp((x - kHue.x0) / (kHue.x1 - kHue.x0), 0.f, 0.999f);
|
|
hsv(h, key.r, key.g, key.b);
|
|
} else {
|
|
const Range &s = kSliders[which];
|
|
Rect r = sliderRect(which);
|
|
float t = std::clamp((x - r.x0) / (r.x1 - r.x0), 0.f, 1.f);
|
|
key.*(s.field) = s.lo + t * (s.hi - s.lo);
|
|
}
|
|
dirty = true;
|
|
};
|
|
|
|
while (true) {
|
|
VREvent_t ev;
|
|
while (ov->PollNextOverlayEvent(main, &ev, sizeof ev)) {
|
|
float x = ev.data.mouse.x, y = H - ev.data.mouse.y; // OpenVR mouse origin is bottom-left
|
|
switch (ev.eventType) {
|
|
case VREvent_MouseButtonDown:
|
|
if (kHue.in(x, y)) { drag = DragHue; setFromX(DragHue, x); }
|
|
for (int i = 0; i < kNumSliders; i++) {
|
|
Rect r = sliderRect(i);
|
|
Rect hit = {r.x0 - 30, r.y0 - 20, r.x1 + 30, r.y1 + 20};
|
|
if (hit.in(x, y)) { drag = i; setFromX(i, x); }
|
|
}
|
|
break;
|
|
case VREvent_MouseMove:
|
|
if (drag != DragNone) setFromX(drag, x);
|
|
break;
|
|
case VREvent_MouseButtonUp:
|
|
if (drag == kBrightSlider) {
|
|
setFromX(drag, x);
|
|
drag = DragNone;
|
|
char buf[256];
|
|
snprintf(buf, sizeof buf, "%s passthru %s %.3f", g_mrkey.c_str(), kPassthruGain, key.bright);
|
|
submit(buf);
|
|
} else if (drag != DragNone) {
|
|
setFromX(drag, x);
|
|
drag = DragNone;
|
|
applyKey();
|
|
status = "on";
|
|
} else if (kToggle.in(x, y)) {
|
|
submit(g_toggle + " toggle");
|
|
} else if (kBgOn.in(x, y) || kBgOff.in(x, y)) {
|
|
bg.on = kBgOn.in(x, y);
|
|
submit(g_toggle + (bg.on ? " bg on" : " bg off"));
|
|
mismatchSince = Clock::now();
|
|
} else {
|
|
for (int i = 0; i < 5; i++)
|
|
if (presetRect(i).in(x, y)) {
|
|
key.r = kPresets[i].r; key.g = kPresets[i].g; key.b = kPresets[i].b;
|
|
applyKey();
|
|
status = "on";
|
|
}
|
|
}
|
|
dirty = true;
|
|
break;
|
|
}
|
|
}
|
|
while (VRSystem()->PollNextEvent(&ev, sizeof ev))
|
|
if (ev.eventType == VREvent_Quit) {
|
|
VRSystem()->AcknowledgeQuit_Exiting();
|
|
std::lock_guard<std::mutex> l(m);
|
|
quit = true;
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> l(m);
|
|
if (quit) break;
|
|
}
|
|
|
|
auto now = Clock::now();
|
|
if (wasBusy != busy) { wasBusy = busy; dirty = true; }
|
|
if (!busy && drag == DragNone && now - lastPoll > std::chrono::milliseconds(700)) {
|
|
lastPoll = now;
|
|
syncSceneAlpha();
|
|
std::string st = runLine(g_toggle + " status");
|
|
if (st != status) { status = st; dirty = true; }
|
|
time_t mt = mtimeOf(g_state);
|
|
if (mt != stateMtime) { stateMtime = mt; if (readState(key)) dirty = true; }
|
|
mt = mtimeOf(g_passthru);
|
|
if (mt != passthruMtime) { passthruMtime = mt; readPassthru(key); dirty = true; }
|
|
bool want = readWord(bgWantPath) == "on";
|
|
std::string ack = readWord(bgAckPath);
|
|
if (want != bg.on || ack != bg.applied) { bg.on = want; bg.applied = ack; dirty = true; }
|
|
}
|
|
static bool lastSynced = true;
|
|
bool synced = inSync();
|
|
if (synced) mismatchSince = now;
|
|
Sync sync = synced ? SyncOk : now - mismatchSince > std::chrono::seconds(6) ? SyncStale : SyncWaiting;
|
|
static Sync lastSync = SyncOk;
|
|
if (sync != lastSync || synced != lastSynced) { lastSync = sync; lastSynced = synced; dirty = true; }
|
|
if (dirty && now - lastDraw > std::chrono::milliseconds(50)) {
|
|
render(cv, key, status, busy, drag, bg, sync);
|
|
ov->SetOverlayRaw(main, cv.px.data(), W, H, 4);
|
|
lastDraw = now;
|
|
dirty = false;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
|
}
|
|
worker.join();
|
|
VR_Shutdown();
|
|
return 0;
|
|
}
|