Skip to content
Merged
4 changes: 4 additions & 0 deletions src/Compositor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3099,3 +3099,7 @@ std::optional<unsigned int> CCompositor::getVTNr() {

return ttynum;
}

bool CCompositor::isVRRActiveOnAnyMonitor() const {
return std::ranges::any_of(m_monitors, [](const PHLMONITOR& m) { return m->m_vrrActive; });
}
1 change: 1 addition & 0 deletions src/Compositor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ class CCompositor {
void onNewMonitor(SP<Aquamarine::IOutput> output);
void ensurePersistentWorkspacesPresent(const std::vector<SWorkspaceRule>& rules, PHLWORKSPACE pWorkspace = nullptr);
std::optional<unsigned int> getVTNr();
bool isVRRActiveOnAnyMonitor() const;

NColorManagement::PImageDescription getPreferredImageDescription();
NColorManagement::PImageDescription getHDRImageDescription();
Expand Down
2 changes: 1 addition & 1 deletion src/config/ConfigManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3017,7 +3017,7 @@ bool CConfigManager::shouldUseSoftwareCursors(PHLMONITOR pMonitor) {
switch (*PNOHW) {
case 0: return false;
case 1: return true;
case 2: return g_pHyprRenderer->isNvidia() && g_pHyprRenderer->isMgpu();
case 2: return g_pHyprRenderer->isNvidia() && (g_pHyprRenderer->isMgpu() || g_pCompositor->isVRRActiveOnAnyMonitor());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check seems wrong to me.

if you have vrr=1 at all, then this will just force sw cursor all the time, it should only do it if vrr is active + fullscreen afaik

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check seems wrong to me.

if you have vrr=1 at all, then this will just force sw cursor all the time, it should only do it if vrr is active + fullscreen afaik

On my setup moving a hardware cursor on the second monitor makes the VRR monitor go black (framerate spike switches mode to non-vrr for a second). That is not usable.

The condition could definitely be better (especially in case of 1 monitor).
I have tried switching to software cursor for the secondary monitor, but it's very pretty wonky on the edges of the monitors.

Ultimately I want to look into making hardware cursors work correctly on NVIDIA.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to hear that your monitor modesets when going in and out of VRR, but that is definitely not the norm. This PR was also specific to fixing the variable no_break_fs_vrr which is specific to FS.

Anyway I've just set no hw cursor to 0 for now

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to hear that your monitor modesets when going in and out of VRR, but that is definitely not the norm. This PR was also specific to fixing the variable no_break_fs_vrr which is specific to FS.

Anyway I've just set no hw cursor to 0 for now

It was a thing on 2/2 ASUS monitors I owned, so I think it's pretty common. Not a very reputable brand so I'd believe if you said it's not a thing on others.

The PR ended up being about removing most instances of VRR breaking (not just the ones caused by removal of mouse input hack), that's why the hardware cursor check was added too... hopefully temporarily.

Copy link
Copy Markdown
Contributor

@njdom24 njdom24 Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK the AMDGPU driver behaves such that, when a display supports VRR, it's always sending a VRR signal. Even with VRR disabled, it just keeps the FPS locked at max, so it doesn't need to modeset.

NVIDIA probably doesn't do that. In which case it might be necessary to emulate that behavior within Hyprland.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NVIDIA probably doesn't do that.

nvidia also does that unless you use a specific kernel param to outright disable vrr

default: break;
}

Expand Down
15 changes: 1 addition & 14 deletions src/desktop/view/Window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2595,20 +2595,7 @@ void CWindow::commitWindow() {

const auto PMONITOR = m_monitor.lock();

if (PMONITOR)
PMONITOR->debugLastPresentation(g_pSeatManager->m_isPointerFrameCommit ? "listener_commitWindow skip" : "listener_commitWindow");

if (g_pSeatManager->m_isPointerFrameCommit) {
g_pSeatManager->m_isPointerFrameSkipped = false;
g_pSeatManager->m_isPointerFrameCommit = false;
} else
g_pHyprRenderer->damageSurface(wlSurface()->resource(), m_realPosition->goal().x, m_realPosition->goal().y, m_isX11 ? 1.0 / m_X11SurfaceScaledBy : 1.0);

if (g_pSeatManager->m_isPointerFrameSkipped) {
g_pPointerManager->sendStoredMovement();
g_pSeatManager->sendPointerFrame();
g_pSeatManager->m_isPointerFrameCommit = true;
}
g_pHyprRenderer->damageSurface(wlSurface()->resource(), m_realPosition->goal().x, m_realPosition->goal().y, m_isX11 ? 1.0 / m_X11SurfaceScaledBy : 1.0);

if (!m_isX11) {
m_subsurfaceHead->recheckDamageForSubsurfaces();
Expand Down
6 changes: 4 additions & 2 deletions src/helpers/Monitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1043,8 +1043,10 @@ bool CMonitor::shouldSkipScheduleFrameOnMouseEvent() {
static auto PMINRR = CConfigValue<Hyprlang::INT>("cursor:min_refresh_rate");

// skip scheduling extra frames for fullsreen apps with vrr
const auto FS_WINDOW = getFullscreenWindow();
const bool shouldSkip = FS_WINDOW && (*PNOBREAK == 1 || (*PNOBREAK == 2 && FS_WINDOW->getContentType() == CONTENT_TYPE_GAME)) && m_output->state->state().adaptiveSync;
const auto FS_WINDOW = getFullscreenWindow();
const bool shouldRenderCursor = g_pHyprRenderer->shouldRenderCursor();
const bool noBreak = FS_WINDOW && (*PNOBREAK == 1 || (*PNOBREAK == 2 && FS_WINDOW->getContentType() == CONTENT_TYPE_GAME));
const bool shouldSkip = (!shouldRenderCursor || noBreak) && m_output->state->state().adaptiveSync;

// keep requested minimum refresh rate
if (shouldSkip && *PMINRR && m_lastPresentationTimer.getMillis() > 1000.0f / *PMINRR) {
Expand Down
58 changes: 17 additions & 41 deletions src/managers/PointerManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -738,17 +738,26 @@ Vector2D CPointerManager::closestValid(const Vector2D& pos) {
}

void CPointerManager::damageIfSoftware() {
if (g_pCompositor->m_unsafeState)
return;

auto b = getCursorBoxGlobal().expand(4);

for (auto const& mw : m_monitorStates) {
if (mw->monitor.expired() || !mw->monitor->m_output)
auto monitor = mw->monitor.lock();
if (!monitor || !monitor->m_output || monitor->isMirror())
continue;

if ((mw->softwareLocks > 0 || mw->hardwareFailed || g_pConfigManager->shouldUseSoftwareCursors(mw->monitor.lock())) &&
b.overlaps({mw->monitor->m_position, mw->monitor->m_size})) {
g_pHyprRenderer->damageBox(b, mw->monitor->shouldSkipScheduleFrameOnMouseEvent());
break;
}
auto usesSoftwareCursor = (mw->softwareLocks > 0 || mw->hardwareFailed || g_pConfigManager->shouldUseSoftwareCursors(monitor));
if (!usesSoftwareCursor)
continue;

auto shouldAddDamage = !monitor->shouldSkipScheduleFrameOnMouseEvent() && b.overlaps({monitor->m_position, monitor->m_size});
if (!shouldAddDamage)
continue;

CBox damageBox = b.copy().translate(-monitor->m_position).scale(monitor->m_scale).round();
monitor->addDamage(damageBox);
}
}

Expand Down Expand Up @@ -925,20 +934,6 @@ void CPointerManager::attachPointer(SP<IPointer> pointer) {
PROTO::idle->onActivity();
});

listener->frame = pointer->m_pointerEvents.frame.listen([] {
bool shouldSkip = false;
if (!g_pSeatManager->m_mouse.expired() && g_pInputManager->isLocked()) {
auto PMONITOR = Desktop::focusState()->monitor().get();
if (PMONITOR && PMONITOR->shouldSkipScheduleFrameOnMouseEvent()) {
auto fsWindow = PMONITOR->m_activeWorkspace->getFullscreenWindow();
shouldSkip = fsWindow && fsWindow->m_isX11;
}
}
g_pSeatManager->m_isPointerFrameSkipped = shouldSkip;
if (!g_pSeatManager->m_isPointerFrameSkipped)
g_pSeatManager->sendPointerFrame();
});

listener->swipeBegin = pointer->m_pointerEvents.swipeBegin.listen([](const IPointer::SSwipeBeginEvent& event) {
g_pInputManager->onSwipeBegin(event);

Expand Down Expand Up @@ -1090,7 +1085,7 @@ void CPointerManager::detachTablet(SP<CTablet> tablet) {
std::erase_if(m_tabletListeners, [tablet](const auto& e) { return e->tablet.expired() || e->tablet == tablet; });
}

void CPointerManager::damageCursor(PHLMONITOR pMonitor) {
void CPointerManager::damageCursor(PHLMONITOR pMonitor, bool skipFrameSchedule) {
for (auto const& mw : m_monitorStates) {
if (mw->monitor != pMonitor)
continue;
Expand All @@ -1100,7 +1095,7 @@ void CPointerManager::damageCursor(PHLMONITOR pMonitor) {
if (b.empty())
return;

g_pHyprRenderer->damageBox(b);
g_pHyprRenderer->damageBox(b, skipFrameSchedule);

return;
}
Expand All @@ -1109,22 +1104,3 @@ void CPointerManager::damageCursor(PHLMONITOR pMonitor) {
Vector2D CPointerManager::cursorSizeLogical() {
return m_currentCursorImage.size / m_currentCursorImage.scale;
}

void CPointerManager::storeMovement(uint64_t time, const Vector2D& delta, const Vector2D& deltaUnaccel) {
m_storedTime = time;
m_storedDelta += delta;
m_storedUnaccel += deltaUnaccel;
}

void CPointerManager::setStoredMovement(uint64_t time, const Vector2D& delta, const Vector2D& deltaUnaccel) {
m_storedTime = time;
m_storedDelta = delta;
m_storedUnaccel = deltaUnaccel;
}

void CPointerManager::sendStoredMovement() {
PROTO::relativePointer->sendRelativeMotion(m_storedTime * 1000, m_storedDelta, m_storedUnaccel);
m_storedTime = 0;
m_storedDelta = Vector2D{};
m_storedUnaccel = Vector2D{};
}
10 changes: 1 addition & 9 deletions src/managers/PointerManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,11 @@ class CPointerManager {
// this is needed e.g. during screensharing where
// the software cursors aren't locked during the cursor move, but they
// are rendered later.
void damageCursor(PHLMONITOR pMonitor);
void damageCursor(PHLMONITOR pMonitor, bool skipFrameSchedule = false);

//
Vector2D position();
Vector2D cursorSizeLogical();
void storeMovement(uint64_t time, const Vector2D& delta, const Vector2D& deltaUnaccel);
void setStoredMovement(uint64_t time, const Vector2D& delta, const Vector2D& deltaUnaccel);
void sendStoredMovement();

void recheckEnteredOutputs();

Expand Down Expand Up @@ -95,7 +92,6 @@ class CPointerManager {
CHyprSignalListener motionAbsolute;
CHyprSignalListener button;
CHyprSignalListener axis;
CHyprSignalListener frame;

CHyprSignalListener swipeBegin;
CHyprSignalListener swipeEnd;
Expand Down Expand Up @@ -154,10 +150,6 @@ class CPointerManager {

Vector2D m_pointerPos = {0, 0};

uint64_t m_storedTime = 0;
Vector2D m_storedDelta = {0, 0};
Vector2D m_storedUnaccel = {0, 0};

struct SMonitorPointerState {
SMonitorPointerState(const PHLMONITOR& m) : monitor(m) {}
~SMonitorPointerState() = default;
Expand Down
3 changes: 0 additions & 3 deletions src/managers/SeatManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,6 @@ class CSeatManager {
void setGrab(SP<CSeatGrab> grab); // nullptr removes
SP<CSeatGrab> m_seatGrab;

bool m_isPointerFrameSkipped = false;
bool m_isPointerFrameCommit = false;

private:
struct SSeatResourceContainer {
SSeatResourceContainer(SP<CWLSeatResource>);
Expand Down
2 changes: 1 addition & 1 deletion src/managers/animation/AnimationManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ static void handleUpdate(CAnimatedVariable<VarType>& av, bool warp) {
}

// manually schedule a frame
if (PMONITOR)
if (PMONITOR && !PMONITOR->inFullscreenMode())
g_pCompositor->scheduleFrameForMonitor(PMONITOR, Aquamarine::IOutput::AQ_SCHEDULE_ANIMATION);
}

Expand Down
13 changes: 6 additions & 7 deletions src/managers/input/InputManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,16 +130,10 @@ void CInputManager::onMouseMoved(IPointer::SMotionEvent e) {

const auto DELTA = *PNOACCEL == 1 ? unaccel : delta;

if (g_pSeatManager->m_isPointerFrameSkipped)
g_pPointerManager->storeMovement(e.timeMs, DELTA, unaccel);
else
g_pPointerManager->setStoredMovement(e.timeMs, DELTA, unaccel);

PROTO::relativePointer->sendRelativeMotion(sc<uint64_t>(e.timeMs) * 1000, DELTA, unaccel);

if (e.mouse)
recheckMouseWarpOnMouseInput();

PROTO::relativePointer->sendRelativeMotion(sc<uint64_t>(e.timeMs) * 1000, delta, unaccel);
g_pPointerManager->move(DELTA);

mouseMoveUnified(e.timeMs, false, e.mouse);
Expand All @@ -151,6 +145,8 @@ void CInputManager::onMouseMoved(IPointer::SMotionEvent e) {

if (e.mouse)
m_lastMousePos = getMouseCoordsInternal();

g_pSeatManager->sendPointerFrame();
}

void CInputManager::onMouseWarp(IPointer::SMotionAbsoluteEvent e) {
Expand Down Expand Up @@ -676,6 +672,8 @@ void CInputManager::onMouseButton(IPointer::SButtonEvent e) {
m_focusHeldByButtons = false;
m_refocusHeldByButtons = false;
}

g_pSeatManager->sendPointerFrame();
}

void CInputManager::processMouseRequest(const CSeatManager::SSetCursorEvent& event) {
Expand Down Expand Up @@ -954,6 +952,7 @@ void CInputManager::onMouseWheel(IPointer::SAxisEvent e, SP<IPointer> pointer) {
int32_t deltaDiscrete = std::abs(discrete) != 0 && std::abs(discrete) < 1 ? std::copysign(1, discrete) : std::round(discrete);

g_pSeatManager->sendPointerAxis(e.timeMs, e.axis, delta, deltaDiscrete, value120, e.source, WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL);
g_pSeatManager->sendPointerFrame();
}

Vector2D CInputManager::getMouseCoordsInternal() {
Expand Down
52 changes: 26 additions & 26 deletions src/render/Renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1914,22 +1914,33 @@ void CHyprRenderer::damageSurface(SP<CWLSurfaceResource> pSurface, double x, dou
if (g_pCompositor->m_unsafeState)
return;

const auto WLSURF = Desktop::View::CWLSurface::fromResource(pSurface);
CRegion damageBox = WLSURF ? WLSURF->computeDamage() : CRegion{};
const auto WLSURF = Desktop::View::CWLSurface::fromResource(pSurface);
if (!WLSURF) {
Log::logger->log(Log::ERR, "BUG THIS: No CWLSurface for surface in damageSurface!!!");
return;
}

if (scale != 1.0)
damageBox.scale(scale);
// hack: schedule frame events
if (!WLSURF->resource()->m_current.callbacks.empty() && pSurface->m_hlSurface) {
const auto BOX = pSurface->m_hlSurface->getSurfaceBoxGlobal();
if (BOX && !BOX->empty()) {
for (auto const& m : g_pCompositor->m_monitors) {
if (!m->m_output)
continue;

// schedule frame events
g_pCompositor->scheduleFrameForMonitor(g_pCompositor->getMonitorFromVector(Vector2D(x, y)), Aquamarine::IOutput::AQ_SCHEDULE_DAMAGE);
if (BOX->overlaps(m->logicalBox()))
g_pCompositor->scheduleFrameForMonitor(m, Aquamarine::IOutput::AQ_SCHEDULE_NEEDS_FRAME);
}
}
}

CRegion damageBox = WLSURF->computeDamage();
if (damageBox.empty())
return;

if (scale != 1.0)
damageBox.scale(scale);

damageBox.translate({x, y});

CRegion damageBoxForEach;
Expand Down Expand Up @@ -2049,7 +2060,7 @@ void CHyprRenderer::renderDragIcon(PHLMONITOR pMonitor, const Time::steady_tp& t
}

void CHyprRenderer::setCursorSurface(SP<Desktop::View::CWLSurface> surf, int hotspotX, int hotspotY, bool force) {
m_cursorHasSurface = surf;
m_cursorHasSurface = surf && surf->resource();

m_lastCursorData.name = "";
m_lastCursorData.surf = surf;
Expand Down Expand Up @@ -2140,30 +2151,19 @@ void CHyprRenderer::ensureCursorRenderingMode() {
if (HIDE == m_cursorHidden)
return;

if (HIDE) {
if (HIDE)
Log::logger->log(Log::DEBUG, "Hiding the cursor (hl-mandated)");

for (auto const& m : g_pCompositor->m_monitors) {
if (!g_pPointerManager->softwareLockedFor(m))
continue;

damageMonitor(m); // TODO: maybe just damage the cursor area?
}

setCursorHidden(true);

} else {
else
Log::logger->log(Log::DEBUG, "Showing the cursor (hl-mandated)");

for (auto const& m : g_pCompositor->m_monitors) {
if (!g_pPointerManager->softwareLockedFor(m))
continue;

damageMonitor(m); // TODO: maybe just damage the cursor area?
}
for (auto const& m : g_pCompositor->m_monitors) {
if (!g_pPointerManager->softwareLockedFor(m))
continue;

setCursorHidden(false);
g_pPointerManager->damageCursor(m, m->shouldSkipScheduleFrameOnMouseEvent());
}

setCursorHidden(HIDE);
}

void CHyprRenderer::setCursorHidden(bool hide) {
Expand Down
2 changes: 1 addition & 1 deletion src/render/decorations/CHyprBorderDecoration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ void CHyprBorderDecoration::updateWindow(PHLWINDOW) {
}

void CHyprBorderDecoration::damageEntire() {
if (!validMapped(m_window))
if (!validMapped(m_window) || m_window->isFullscreen())
return;

auto surfaceBox = m_window->getWindowMainSurfaceBox();
Expand Down
Loading