159 lines
7.7 KiB
C++
159 lines
7.7 KiB
C++
#pragma once
|
|
|
|
#include <QFile>
|
|
#include <QImage>
|
|
#include <QSaveFile>
|
|
#include <QtEndian>
|
|
#include <time.h>
|
|
#include "input/input.h"
|
|
#include "streamutils.h"
|
|
|
|
// Linux X11 single-seat native cursor. Called only by the SDL main thread.
|
|
// The producer starts cursor-free before this session's first captured frame.
|
|
class VdiNativeCursor {
|
|
public:
|
|
VdiNativeCursor(SDL_Window* window, SdlInputHandler* input) :
|
|
m_Window(window), m_Input(input), m_Path(qgetenv("VDI_NATIVE_CURSOR_STATE")),
|
|
m_Epoch(qgetenv("VDI_NATIVE_CURSOR_EPOCH").toULongLong()) {}
|
|
|
|
bool enabled() const { return !m_Path.isEmpty(); }
|
|
|
|
void observe(const SDL_Event& event) {
|
|
if (event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEBUTTONDOWN ||
|
|
event.type == SDL_MOUSEBUTTONUP) {
|
|
m_LastInput = nowNs();
|
|
++m_InputSequence;
|
|
}
|
|
}
|
|
|
|
bool tick() {
|
|
if (!enabled()) return true;
|
|
if (QByteArray(SDL_GetCurrentVideoDriver()) != "x11") return fail("requires X11 cursor backend");
|
|
const quint64 pollTime = nowNs();
|
|
if (pollTime - m_LastPoll < 4000000) return true;
|
|
m_LastPoll = pollTime;
|
|
QFile f(QString::fromUtf8(m_Path));
|
|
if (!f.open(QIODevice::ReadOnly) || f.size() < 76 || f.size() > 65608)
|
|
return fail("missing or oversized state");
|
|
const QByteArray packet = f.readAll();
|
|
// The producer may atomically replace state after pollTime. Compare
|
|
// against a timestamp taken AFTER the read, never before opening it.
|
|
const quint64 now = nowNs();
|
|
const auto* p = reinterpret_cast<const uchar*>(packet.constData());
|
|
auto u32 = [p](int offset) { return qFromLittleEndian<quint32>(p + offset); };
|
|
auto u64 = [p](int offset) { return qFromLittleEndian<quint64>(p + offset); };
|
|
if (packet.left(8) != QByteArray("VDICUR1\0", 8) || u64(8) != m_Epoch ||
|
|
u64(16) < m_Sequence || u64(24) > now || now - u64(24) > 2000000000)
|
|
return fail("stale or mismatched lease");
|
|
const int w = u32(32), h = u32(36), hx = u32(40), hy = u32(44);
|
|
const int x = qint32(u32(48)), y = qint32(u32(52));
|
|
const int sw = u32(56), sh = u32(60);
|
|
if (w < 1 || w > 128 || h < 1 || h > 128 || hx < 0 || hx >= w ||
|
|
hy < 0 || hy >= h || sw != 2560 || sh != 1440 ||
|
|
x < 0 || x >= sw || y < 0 || y >= sh || packet.size() != 72 + w*h*4 ||
|
|
!m_Input->isNativeCursorAbsoluteMode())
|
|
return fail("unsupported shape, display or input mode");
|
|
m_Sequence = u64(16);
|
|
int ww, wh;
|
|
SDL_GetWindowSize(m_Window, &ww, &wh);
|
|
SDL_Rect src = {0, 0, sw, sh}, dst = {0, 0, ww, wh};
|
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
|
if (dst.w <= 0 || dst.h <= 0) return fail("empty viewport");
|
|
const int cw = qMax(1, qRound(double(w) * dst.w / sw));
|
|
const int ch = qMax(1, qRound(double(h) * dst.h / sh));
|
|
if (cw > 256 || ch > 256) return fail("oversized scaled cursor");
|
|
const quint64 acknowledged = u64(64 + w*h*4);
|
|
const QByteArray pixels = packet.mid(64, w*h*4);
|
|
if (pixels != m_Pixels || w != m_Width || h != m_Height || hx != m_HotX ||
|
|
hy != m_HotY || dst.w != m_ViewWidth || dst.h != m_ViewHeight) {
|
|
QImage image(reinterpret_cast<const uchar*>(pixels.constData()), w, h,
|
|
w * 4, QImage::Format_RGBA8888);
|
|
image = image.scaled(cw, ch, Qt::IgnoreAspectRatio, Qt::FastTransformation);
|
|
// SDL2's X11 Xcursor backend copies pixels without premultiplying.
|
|
// XRender requires premultiplied channels, including antialiased edges.
|
|
image = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
|
SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormatFrom(
|
|
image.bits(), cw, ch, 32, image.bytesPerLine(), SDL_PIXELFORMAT_RGBA32);
|
|
if (!surface) return fail("SDL surface failed");
|
|
SDL_Cursor* next = SDL_CreateColorCursor(surface,
|
|
qMin(cw - 1, qRound(double(hx) * dst.w / sw)),
|
|
qMin(ch - 1, qRound(double(hy) * dst.h / sh)));
|
|
SDL_FreeSurface(surface);
|
|
if (!next) return fail("SDL cursor failed");
|
|
SDL_SetCursor(next);
|
|
if (m_Cursor) SDL_FreeCursor(m_Cursor);
|
|
m_Cursor = next;
|
|
m_Pixels = pixels;
|
|
m_Width = w; m_Height = h; m_HotX = hx; m_HotY = hy;
|
|
m_ViewWidth = dst.w; m_ViewHeight = dst.h;
|
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
"VDI native shape: %dx%d hotspot=%d,%d sequence=%llu",
|
|
w, h, hx, hy, (unsigned long long)m_Sequence);
|
|
}
|
|
int mx, my;
|
|
const auto buttons = SDL_GetMouseState(&mx, &my);
|
|
const bool inside = mx >= dst.x && my >= dst.y && mx < dst.x + dst.w && my < dst.y + dst.h;
|
|
const bool focused = SDL_GetKeyboardFocus() == m_Window;
|
|
if (m_PublishedInput != m_InputSequence) {
|
|
QByteArray message(16, '\0');
|
|
qToLittleEndian<quint64>(m_InputSequence, reinterpret_cast<uchar*>(message.data()));
|
|
qToLittleEndian<qint32>(qBound(0, (mx-dst.x)*sw/dst.w, sw-1),
|
|
reinterpret_cast<uchar*>(message.data()+8));
|
|
qToLittleEndian<qint32>(qBound(0, (my-dst.y)*sh/dst.h, sh-1),
|
|
reinterpret_cast<uchar*>(message.data()+12));
|
|
QSaveFile outgoing(QString::fromUtf8(m_Path)+".input");
|
|
if (!outgoing.open(QIODevice::WriteOnly) || outgoing.write(message) != 16 || !outgoing.commit())
|
|
return fail("input acknowledgement file failed");
|
|
m_PublishedInput = m_InputSequence;
|
|
}
|
|
m_Input->enableNativeCursorVisibility();
|
|
SDL_SetCursor(focused && inside ? m_Cursor : SDL_GetDefaultCursor());
|
|
// Initial alignment, and host-initiated warps only after local input has
|
|
// settled. Never pull the pointer back using delayed motion echoes.
|
|
if (focused && inside && !buttons && acknowledged == m_InputSequence &&
|
|
now - m_LastInput > 500000000) {
|
|
const int tx = dst.x + qRound(double(x) * dst.w / sw);
|
|
const int ty = dst.y + qRound(double(y) * dst.h / sh);
|
|
if (qAbs(tx - mx) > 2 || qAbs(ty - my) > 2) {
|
|
// SDL2's X11 in-window warp is a no-op with pointer barriers
|
|
// (used by fullscreen absolute mode). Global warp honors this
|
|
// in-viewport target while actually moving the native pointer.
|
|
int wx, wy;
|
|
SDL_GetWindowPosition(m_Window, &wx, &wy);
|
|
if (SDL_WarpMouseGlobal(wx+tx, wy+ty) < 0)
|
|
return fail("host cursor warp failed");
|
|
m_LastInput = now;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void shutdown() {
|
|
if (m_Cursor) {
|
|
SDL_SetCursor(SDL_GetDefaultCursor());
|
|
SDL_FreeCursor(m_Cursor);
|
|
m_Cursor = nullptr;
|
|
}
|
|
}
|
|
|
|
private:
|
|
static quint64 nowNs() {
|
|
struct timespec t;
|
|
clock_gettime(CLOCK_MONOTONIC, &t);
|
|
return quint64(t.tv_sec)*1000000000 + t.tv_nsec;
|
|
}
|
|
bool fail(const char* reason) {
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "VDI native cursor ended: %s", reason);
|
|
shutdown();
|
|
return false;
|
|
}
|
|
SDL_Window* m_Window;
|
|
SdlInputHandler* m_Input;
|
|
QByteArray m_Path, m_Pixels;
|
|
quint64 m_Epoch, m_Sequence = 0, m_LastPoll = 0, m_LastInput = nowNs();
|
|
quint64 m_InputSequence = 0, m_PublishedInput = 0;
|
|
SDL_Cursor* m_Cursor = nullptr;
|
|
int m_Width = 0, m_Height = 0, m_HotX = 0, m_HotY = 0;
|
|
int m_ViewWidth = 0, m_ViewHeight = 0;
|
|
};
|