diff --git a/client/src/app.c b/client/src/app.c index cbc1e994..7024cbbd 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -469,7 +469,8 @@ void app_handleButtonPress(int button) return; } - if (!core_inputEnabled() || !g_cursor.inView || !g_cursor.viewReq) + if (!core_inputEnabled() || g_cursor.exitWait || + !g_cursor.inView || !g_cursor.viewReq) return; if (!purespice_mousePress(button)) @@ -492,7 +493,7 @@ void app_handleButtonRelease(int button) return; } - if (!core_inputEnabled()) + if (!core_inputEnabled() || g_cursor.exitWait) return; if (!purespice_mouseRelease(button)) diff --git a/client/src/core.c b/client/src/core.c index 9365cfc3..d558656a 100644 --- a/client/src/core.c +++ b/client/src/core.c @@ -62,18 +62,21 @@ static int exitPos(double pos, int edge) static void cancelExit(const char * why) { - if (!g_cursor.exit) + if (!g_cursor.exit && !g_cursor.exitWait) return; MTRACE("exit cancel=%s target=%.3f,%.3f", why, g_cursor.exitPos.x, g_cursor.exitPos.y); - g_cursor.exit = false; + g_cursor.exit = false; + g_cursor.exitWait = false; + g_cursor.exitRetry = false; + g_cursor.exitDelta = (struct DoublePoint) { 0 }; } -static void finishExit(void) +static bool finishExit(void) { if (!g_cursor.exit || g_cursor.viewReq) - return; + return false; const int x = exitPos(g_cursor.exitPos.x, g_state.dstRect.x); const int y = exitPos(g_cursor.exitPos.y, g_state.dstRect.y); @@ -82,21 +85,27 @@ static void finishExit(void) MTRACE("exit warp target=%d,%d projected=%.3f,%.3f inWin=%d", x, y, g_cursor.exitPos.x, g_cursor.exitPos.y, g_cursor.inWindow); - if (x < 0 || x >= g_state.windowW || - y < 0 || y >= g_state.windowH) + if (g_cursor.exitWait) + g_cursor.viewReq = true; + else if (x < 0 || x >= g_state.windowW || + y < 0 || y >= g_state.windowH) g_cursor.inWindow = false; core_warpPointer(x, y, true); + return g_cursor.exitWait; } -static void startExit(double x, double y) +static void startExit(double x, double y, bool wait) { - g_cursor.exit = true; - g_cursor.exitPos = (struct DoublePoint) { x, y }; + g_cursor.exit = true; + g_cursor.exitWait = wait; + g_cursor.exitRetry = false; + g_cursor.exitPos = (struct DoublePoint) { x, y }; + g_cursor.exitDelta = (struct DoublePoint) { 0 }; core_setCursorInView(false); } -static bool moveExit(double ex, double ey) +static struct DoublePoint exitMove(double ex, double ey) { if (g_cursor.useScale && g_params.scaleMouseInput) { @@ -106,18 +115,115 @@ static bool moveExit(double ex, double ey) struct DoublePoint move = {.x = ex, .y = ey}; util_rotatePoint(&move); - g_cursor.exitPos.x += move.x; - g_cursor.exitPos.y += move.y; + return move; +} - const bool inside = - g_cursor.exitPos.x >= g_state.dstRect.x && - g_cursor.exitPos.x < g_state.dstRect.x + g_state.dstRect.w && - g_cursor.exitPos.y >= g_state.dstRect.y && - g_cursor.exitPos.y < g_state.dstRect.y + g_state.dstRect.h; +static struct Point exitDir(void) +{ + return (struct Point) { + .x = g_cursor.exitPos.x < g_state.dstRect.x ? -1 : + g_cursor.exitPos.x >= g_state.dstRect.x + g_state.dstRect.w ? 1 : 0, + .y = g_cursor.exitPos.y < g_state.dstRect.y ? -1 : + g_cursor.exitPos.y >= g_state.dstRect.y + g_state.dstRect.h ? 1 : 0, + }; +} - MTRACE("exit move=%.3f,%.3f target=%.3f,%.3f inside=%d", - move.x, move.y, g_cursor.exitPos.x, g_cursor.exitPos.y, inside); - return inside; +static bool exitInward(struct DoublePoint move) +{ + const struct Point dir = exitDir(); + return move.x * dir.x < 0 || move.y * dir.y < 0; +} + +static struct DoublePoint exitInput(struct DoublePoint move) +{ + double temp; + switch((g_state.rotate + g_params.winRotate) % LG_ROTATE_MAX) + { + case LG_ROTATE_0: + break; + + case LG_ROTATE_90: + temp = move.x; + move.x = -move.y; + move.y = temp; + break; + + case LG_ROTATE_180: + move.x = -move.x; + move.y = -move.y; + break; + + case LG_ROTATE_270: + temp = move.x; + move.x = move.y; + move.y = -temp; + break; + } + + if (g_cursor.useScale && g_params.scaleMouseInput) + { + move.x /= g_cursor.scale.x; + move.y /= g_cursor.scale.y; + } + + return move; +} + +static struct DoublePoint clampExit(double ex, double ey) +{ + struct DoublePoint move = exitMove(ex, ey); + const struct Point dir = exitDir(); + if (move.x * dir.x > 0) + move.x = 0; + if (move.y * dir.y > 0) + move.y = 0; + + const struct DoublePoint input = exitInput(move); + MTRACE("exit clamp input=%.3f,%.3f move=%.3f,%.3f dir=%d,%d", + input.x, input.y, move.x, move.y, dir.x, dir.y); + return input; +} + +static bool moveExit(double ex, double ey) +{ + const struct DoublePoint move = exitMove(ex, ey); + const bool inward = exitInward(move); + + if (!inward) + { + g_cursor.exitPos.x += move.x; + g_cursor.exitPos.y += move.y; + } + + MTRACE("exit move=%.3f,%.3f target=%.3f,%.3f inward=%d", + move.x, move.y, g_cursor.exitPos.x, g_cursor.exitPos.y, inward); + return inward; +} + +static void startRetry(void) +{ + if (!g_cursor.exitWait || g_cursor.exit || g_cursor.exitRetry) + return; + + MTRACE("exit retry target=%.3f,%.3f", g_cursor.exitPos.x, + g_cursor.exitPos.y); + g_cursor.pos.x = util_clamp(g_cursor.exitPos.x, 0, + g_state.windowW - 1); + g_cursor.pos.y = util_clamp(g_cursor.exitPos.y, 0, + g_state.windowH - 1); + g_cursor.exitRetry = true; + g_cursor.realign = true; + g_state.ds->grabPointer(); +} + +static void retryExit(double ex, double ey) +{ + const struct DoublePoint input = clampExit(ex, ey); + g_cursor.exitDelta.x += input.x; + g_cursor.exitDelta.y += input.y; + + if (input.x != 0 || input.y != 0) + startRetry(); } bool core_inputEnabled(void) @@ -193,9 +299,17 @@ void core_setCursorInView(bool enable) enable, g_cursor.viewReq, g_cursor.inView, g_state.focused, g_cursor.inWindow, g_cursor.grab); - if (g_cursor.exit && - (enable || !g_state.focused || !g_cursor.inWindow || - g_state.ignoreInput || !g_state.posInfoValid || app_isOverlayMode())) + if (enable && g_cursor.exitWait && !g_cursor.exit) + { + startRetry(); + return; + } + + if ((g_cursor.exit && enable) || + ((g_cursor.exit || g_cursor.exitWait) && + (!g_state.focused || !g_cursor.inWindow || g_state.ignoreInput || + !g_state.posInfoValid || app_isOverlayMode())) || + (g_cursor.exitWait && !g_cursor.exit && !enable)) cancelExit(enable ? "view" : "state"); if (enable && !g_state.focused) @@ -253,10 +367,25 @@ void core_handleGrabEvent(bool active) warpSupport == LG_DS_WARP_NONE) return; + struct DoublePoint replay = { 0 }; if (!active) - finishExit(); + { + if (finishExit() || g_cursor.exitWait) + return; + } + else if (g_cursor.exitRetry) + { + replay = g_cursor.exitDelta; + g_cursor.exitWait = false; + g_cursor.exitRetry = false; + g_cursor.exitDelta = (struct DoublePoint) { 0 }; + g_cursor.warpState = WARP_STATE_ON; + } applyView(active, false); + + if (replay.x != 0 || replay.y != 0) + core_handleMouseNormal(replay.x, replay.y); } void core_setGrab(bool enable) @@ -282,6 +411,7 @@ void core_setGrabQuiet(bool enable) if (g_cursor.grab == enable) return; + cancelExit("capture"); g_cursor.grab = enable; g_cursor.acc.x = 0.0; g_cursor.acc.y = 0.0; @@ -389,7 +519,10 @@ void core_onWindowSizeChanged(unsigned width, unsigned height) void core_updatePositionInfo(void) { - cancelExit("geometry"); + if (g_cursor.exitWait && !g_cursor.exit) + startRetry(); + else + cancelExit("geometry"); if (g_params.setGuestRes && g_state.transportFeatures & LG_TRANSPORT_FEATURE_WINDOW_SIZE) @@ -630,7 +763,8 @@ void core_handleGuestMouseUpdate(void) } const bool overlay = app_isOverlayMode(); - if (overlay || !g_cursor.inView || !g_cursor.viewReq) + if (overlay || g_cursor.exitWait || + !g_cursor.inView || !g_cursor.viewReq) { MTRACE("guest drop=%s guest=%d,%d local=%.3f,%.3f inWin=%d " "inView=%d req=%d", overlay ? "overlay" : "view", @@ -712,6 +846,12 @@ void core_handleMouseNormal(double ex, double ey) if (!core_inputEnabled()) return; + if (g_cursor.exitWait && !g_cursor.exit) + { + retryExit(ex, ey); + return; + } + if (g_cursor.viewReq != g_cursor.inView) { if (g_cursor.viewReq) @@ -885,7 +1025,9 @@ fallback: break; case LG_DS_WARP_SURFACE: - startExit(local.x, local.y); + startExit(local.x, local.y, + tx < 0 || tx >= g_state.windowW || + ty < 0 || ty >= g_state.windowH); return; case LG_DS_WARP_SCREEN: @@ -893,7 +1035,7 @@ fallback: g_state.windowPos.x + g_state.border.left + tx, g_state.windowPos.y + g_state.border.top + ty)) { - startExit(local.x, local.y); + startExit(local.x, local.y, false); return; } } diff --git a/client/src/main.c b/client/src/main.c index 138ba42d..4da40f9c 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -133,6 +133,9 @@ static void lgInit(void) g_cursor.inView = false; g_cursor.viewReq = false; g_cursor.exit = false; + g_cursor.exitWait = false; + g_cursor.exitRetry = false; + g_cursor.exitDelta = (struct DoublePoint) { 0 }; g_cursor.guest.valid = false; // if spice is not in use, hide the local cursor diff --git a/client/src/main.h b/client/src/main.h index 8aef380b..1bfd11a1 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -301,9 +301,18 @@ struct CursorState /* true if a pointer exit is waiting for confinement to release */ bool exit; + /* true if a surface exit is waiting for a pointer leave */ + bool exitWait; + + /* true if confinement is being restored after a failed exit */ + bool exitRetry; + /* the local pointer exit target */ struct DoublePoint exitPos; + /* input retained while confinement is restored */ + struct DoublePoint exitDelta; + /* true if the guest should be realigned to the host when next drawn */ bool realign; diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 57e9e233..07f3c646 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -66,6 +66,7 @@ target_link_libraries(mouse-tests ) set(MOUSE_CASES inset-exit + surface-edge exit-delay exit-guest exit-cancel diff --git a/client/tests/mouse_test.c b/client/tests/mouse_test.c index 645c058e..69300838 100644 --- a/client/tests/mouse_test.c +++ b/client/tests/mouse_test.c @@ -329,6 +329,76 @@ static void setConf(bool active) core_handleGrabEvent(active); } +static void setEdge(enum LG_DSWarpSupport support) +{ + reset(); + m.support = support; + g_state.windowW = 100; + g_state.windowH = 80; + g_state.dstRect = (LG_RendererRect) { + .valid = true, + .x = 0, + .y = 0, + .w = 100, + .h = 80, + }; + setLocal(99, 40); +} + +static void testSurfaceEdge(void) +{ + setEdge(LG_DS_WARP_SURFACE); + + core_handleMouseNormal(1, 0); + + const int move = first(EV_WARP); + CHECK(move >= 0); + CHECK(m.ev[move].x == 100); + CHECK(m.ev[move].y == 40); + CHECK(g_cursor.inWindow); + CHECK(g_cursor.inView); + CHECK(g_cursor.viewReq); + CHECK(g_cursor.exitWait); + CHECK(!m.conf.req); + + core_handleGuestMouseUpdate(); + CHECK(count(EV_GUEST) == 0); + + core_handleMouseNormal(1, 0); + CHECK(!m.conf.req); + CHECK(!g_cursor.exitRetry); + + g_cursor.pos.x = 104; + core_handleMouseNormal(1, 2); + CHECK(m.conf.req); + CHECK(g_cursor.pos.x == 99); + CHECK(g_cursor.inView); + CHECK(g_cursor.exitWait); + CHECK(g_cursor.exitRetry); + CHECK(count(EV_MOTION) == 0); + + setConf(true); + CHECK(g_cursor.inView); + CHECK(!g_cursor.exitWait); + CHECK(!g_cursor.exitRetry); + CHECK(count(EV_MOTION) == 1); + CHECK(m.ev[first(EV_MOTION)].x == 0); + CHECK(m.ev[first(EV_MOTION)].y == 2); + + setEdge(LG_DS_WARP_SURFACE); + core_handleMouseNormal(1, 0); + g_cursor.inWindow = false; + core_setCursorInView(false); + CHECK(!g_cursor.inWindow); + CHECK(!g_cursor.inView); + CHECK(!g_cursor.viewReq); + CHECK(!g_cursor.exitWait); + + setEdge(LG_DS_WARP_SCREEN); + core_handleMouseNormal(1, 0); + CHECK(!g_cursor.inWindow); +} + static void startExit(void) { reset(); @@ -382,7 +452,7 @@ static void testExitCancel(void) CHECK(g_cursor.exit); CHECK(!g_cursor.viewReq); - core_handleMouseNormal(-3, 0); + core_handleMouseNormal(-0.25, 0); CHECK(!g_cursor.exit); CHECK(g_cursor.viewReq); CHECK(m.conf.req); @@ -742,6 +812,7 @@ struct Test static const struct Test tests[] = { { "inset-exit" , testInsetExit }, + { "surface-edge" , testSurfaceEdge }, { "exit-delay" , testExitWait }, { "exit-guest" , testExitGuest }, { "exit-cancel" , testExitCancel }, diff --git a/obs/frame_scheduler.c b/obs/frame_scheduler.c index 4fb8508c..59a295b6 100644 --- a/obs/frame_scheduler.c +++ b/obs/frame_scheduler.c @@ -24,13 +24,79 @@ #include -#define FRAME_SCHEDULER_LEASE_MS 1000U -#define FRAME_SCHEDULER_RENEW_NS 250000000ULL -#define FRAME_SCHEDULER_FEEDBACK_NS 50000000ULL -#define FRAME_SCHEDULER_TICK_GRACE_NS 500000000ULL -#define FRAME_SCHEDULER_TARGET_SLACK_NS 1000000ULL -#define FRAME_SCHEDULER_MIN_PERIOD_NS 2000000ULL -#define FRAME_SCHEDULER_MAX_PERIOD_NS 1000000000ULL +#define FRAME_SCHEDULER_LEASE_MS 1000U +#define FRAME_SCHEDULER_RENEW_NS 250000000ULL +#define FRAME_SCHEDULER_FEEDBACK_NS 50000000ULL +#define FRAME_SCHEDULER_TICK_GRACE_NS 500000000ULL +#define FRAME_SCHEDULER_TARGET_SLACK_NS 1000000ULL +#define FRAME_SCHEDULER_MIN_PERIOD_NS 2000000ULL +#define FRAME_SCHEDULER_MAX_PERIOD_NS 1000000000ULL +#define FRAME_SCHEDULER_CONTROL_RETRY_NS 10000000ULL +#define FRAME_SCHEDULER_CONTROL_RETRY_MAX 250000000ULL + +static void lgFrameSchedulerControlBackoff( + LGFrameScheduler * scheduler, uint64_t now) +{ + uint64_t delay = scheduler->controlRetryDelay; + if (!delay) + delay = FRAME_SCHEDULER_CONTROL_RETRY_NS; + + scheduler->nextControlCheck = now + delay; + scheduler->controlRetryDelay = + delay < FRAME_SCHEDULER_CONTROL_RETRY_MAX / 2 ? + delay * 2 : FRAME_SCHEDULER_CONTROL_RETRY_MAX; +} + +static bool lgFrameSchedulerControlReady(LGFrameScheduler * scheduler, + PLGMPClientQueue queue, uint64_t now) +{ + if (!scheduler->controlPending) + return now >= scheduler->nextControlCheck; + + if (now < scheduler->nextControlCheck) + return false; + + uint32_t serial; + const LGMP_STATUS status = lgmpClientGetSerial(queue, &serial); + if (status != LGMP_OK || + (int32_t)(serial - scheduler->controlSerial) < 0) + { + if (status != LGMP_OK) + scheduler->controlFaulted = true; + lgFrameSchedulerControlBackoff(scheduler, now); + return false; + } + + scheduler->controlPending = false; + scheduler->nextControlCheck = 0; + scheduler->controlRetryDelay = FRAME_SCHEDULER_CONTROL_RETRY_NS; + if (scheduler->controlFaulted) + { + scheduler->controlFaulted = false; + scheduler->immediatePending = true; + } + return true; +} + +static bool lgFrameSchedulerSend(LGFrameScheduler * scheduler, + PLGMPClientQueue queue, const KVMFRFrameSchedule * message, + uint64_t now) +{ + uint32_t serial; + if (lgmpClientSendData( + queue, message, sizeof(*message), &serial) != LGMP_OK) + { + lgFrameSchedulerControlBackoff(scheduler, now); + return false; + } + + scheduler->controlPending = true; + scheduler->controlSerial = serial; + scheduler->nextControlCheck = + now + FRAME_SCHEDULER_CONTROL_RETRY_NS; + scheduler->controlRetryDelay = FRAME_SCHEDULER_CONTROL_RETRY_NS; + return true; +} static void lgFrameSchedulerResetFeedback(LGFrameScheduler * scheduler) { @@ -51,9 +117,10 @@ void lgFrameSchedulerInit(LGFrameScheduler * scheduler, bool supported, uint32_t clientID) { memset(scheduler, 0, sizeof(*scheduler)); - scheduler->supported = supported; - scheduler->immediatePending = supported; - scheduler->clientID = clientID; + scheduler->supported = supported; + scheduler->immediatePending = supported; + scheduler->clientID = clientID; + scheduler->controlRetryDelay = FRAME_SCHEDULER_CONTROL_RETRY_NS; } void lgFrameSchedulerSetActive(LGFrameScheduler * scheduler, bool active) @@ -175,6 +242,9 @@ void lgFrameSchedulerUpdate(LGFrameScheduler * scheduler, if (!scheduler->supported || !queue) return; + if (!lgFrameSchedulerControlReady(scheduler, queue, now)) + return; + const bool tickStale = !scheduler->lastTick || now - scheduler->lastTick > FRAME_SCHEDULER_TICK_GRACE_NS; if (!scheduler->sourceActive || tickStale || !scheduler->period) @@ -189,7 +259,7 @@ void lgFrameSchedulerUpdate(LGFrameScheduler * scheduler, .flags = KVMFR_FRAME_SCHEDULE_RELEASE, }; - if (lgmpClientSendData(queue, &message, sizeof(message), NULL) != LGMP_OK) + if (!lgFrameSchedulerSend(scheduler, queue, &message, now)) return; scheduler->active = false; @@ -234,7 +304,7 @@ void lgFrameSchedulerUpdate(LGFrameScheduler * scheduler, .lease = FRAME_SCHEDULER_LEASE_MS, }; - if (lgmpClientSendData(queue, &message, sizeof(message), NULL) != LGMP_OK) + if (!lgFrameSchedulerSend(scheduler, queue, &message, now)) return; scheduler->active = true; diff --git a/obs/frame_scheduler.h b/obs/frame_scheduler.h index 08cc7f0e..d66bb390 100644 --- a/obs/frame_scheduler.h +++ b/obs/frame_scheduler.h @@ -34,11 +34,16 @@ typedef struct LGFrameScheduler bool resetPending; bool immediatePending; bool feedbackDirty; + bool controlPending; + bool controlFaulted; uint32_t clientID; uint32_t generation; + uint32_t controlSerial; uint64_t period; uint64_t lastSend; uint64_t lastTick; + uint64_t nextControlCheck; + uint64_t controlRetryDelay; int64_t phaseError; uint32_t feedbackFrameSerial;