PunchKano begins diphtheria immunisation in five Kano LGsRTP DesportoFrancisco Cabral na final de pares em HangzhouESPN DeportesGrecia derrotó a Alemania por primera vez en la historia y golpeó a Klopp en su debut ante su públicoThe Jerusalem PostIsrair awaits final approval for Tokyo, Miami flights, adds new European destinations for 2027Daily MaverickWe worried AI would make things up, we should also worry when it doesn’tInquirerRains to continue in Visayas, Mindanao due to ITCZ until Sept. 30Bollywood HungamaVinod Kapri's Pyre to release in theaters on October 23, 2026BlickNächstes Amt abgelegt: Jens Spahn zieht sich aus Haushaltsausschuss zurückRapplerPhilippines should fix tax gaps and procurement, not raise tax rates – WBSportstarIndia in Athletics LIVE Updates, Asian Games 2026: Vithya Ramraj breaks National Record to win 400m hurdles bronze; Javelin throw final at 4:45 PM ISTIl Fatto QuotidianoMorto Stefano Milani, il tifoso del Milan diventato famoso su X. Da Bertolucci a Valenti: “Ha lottato come nessuno, mai un passo indietro”7sur7“Pourquoi je perds toujours?”: quand André Agassi taquine Alexander Zverev
The Daily Newsstand · Free, Always
Monday, September 28, 2026

Почему на Mac «плывёт» прицел: macOS отдаёт мышь раз в кадр экрана

Translate

Я играю в Counter-Strike 2 на MacBook Pro 14 с M3 Pro. Кадров хватает, 100 с лишним в секунду, а прицел всё равно ощущается ватным: мелкие доводки то недолетают, то перелетают. Сначала я грешил на ускорение указателя, потом на лимит кадров, потом на саму игру. Не помогало ничего, и в какой-то момент я просто записал, как события мыши доходят до приложения.

Замер

Две ленты с одного и того же движения мыши, снятые одновременно:

  • системный тап (CGEventTap) с аппаратными метками времени, то есть то, что на самом деле шлёт мышь;

  • поток NSEvent в окне обычного Cocoa-приложения, то есть то, что macOS отдаёт программе.

Мышь Logitech через USB-приёмник с опросом 1000 Гц, экран 120 Гц (кадр 8,33 мс), macOS Tahoe. Steam и остальное в фоне я не закрывал, условия обычные. Двадцать секунд непрерывного движения.

мышь шлёт

приложение получает

событий за 20,1 с

13 452

2 269

медиана интервала

1,02 мс

8,08 мс

p90 / p99

2,19 / 7,77 мс

9,90 / 17,06 мс

интервалов короче 2 мс

82,6 %

0,2 %

интервалов 6–10 мс, около одного кадра

1,0 %

88,6 %

Одни и те же 100 мс: сверху события от мыши, снизу события в приложении; ниже гистограммы интервалов за все 20 секунд

Одни и те же 100 мс: сверху события от мыши, снизу события в приложении; ниже гистограммы интервалов за все 20 секунд

За одни и те же 100 мс мышь прислала 81 событие, а приложение получило 13. Всё, что пришло за кадр экрана, система складывает в одно событие и отдаёт его раз в кадр.

Я тут не первый. На форуме разработчиков Apple есть тред Changes in Mouse Event Reporting Frequency in macOS 26.2, где прямо написано, что система прореживает события мыши до частоты экрана. У движка sokol (чистый C, никаких прослоек) про то же самое issue #1344. Так что дело в системе, а не в конкретной игре.

Почему на рабочем столе этого не видно, а в игре видно

Курсор рабочего стола перерисовывается с той же частотой, с какой приходят пачки, поэтому пачка просто становится одним шагом курсора. У игры свой темп кадров, и с темпом пачек он почти никогда не совпадает.

Простая арифметика для 100 fps: кадр длится 10 мс, пачка приходит раз в 8,33 мс. За 50 мс проходит 5 кадров и 6 пачек, значит четыре кадра получают по одной пачке, а пятый две. Если вести мышь ровно, на каждом пятом кадре прицел прыгает вдвое дальше, двадцать раз в секунду. На 80 fps кадры получают попеременно одну и две пачки. Это расчёт, а не замер, но ощущается ровно так. Лимит кадров переставляет рывки с места на место, а убрать их не может.

Что пробовал

Ускорение указателя стоит выключить в любом случае (Системные настройки → Мышь): с ним одно и то же движение руки поворачивает на разный угол. К пачкам оно отношения не имеет.

NSEvent.isMouseCoalescingEnabled = false советуют во всех старых тредах. В том же issue у sokol его проверили: на Tahoe с выключенным склеиванием события приходят с провалами по 50–150 мс, и они вернулись к доставке раз в кадр. Сам я этот флаг не мерил.

Сработало чтение мыши напрямую с устройства, мимо очереди событий.

Как читать каждый отчёт мыши

IOKit умеет подписаться на HID-значения устройства. Коллбэк вызывается на каждый отчёт, с относительным смещением по X или Y и отметкой времени отчёта:

import Foundation
import IOKit.hid

let manager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone))
IOHIDManagerSetDeviceMatching(manager, [kIOHIDDeviceUsagePageKey: kHIDPage_GenericDesktop,
                                        kIOHIDDeviceUsageKey: kHIDUsage_GD_Mouse] as NSDictionary as CFDictionary)
IOHIDManagerRegisterInputValueCallback(manager, { _, _, _, value in
    let element = IOHIDValueGetElement(value)
    guard IOHIDElementGetUsagePage(element) == UInt32(kHIDPage_GenericDesktop),
          IOHIDElementIsRelative(element) else { return }
    let usage = IOHIDElementGetUsage(element)        // kHIDUsage_GD_X или kHIDUsage_GD_Y
    let delta = IOHIDValueGetIntegerValue(value)     // сырые отсчёты, без кривой ускорения
    let stamp = IOHIDValueGetTimeStamp(value)        // mach time отчёта
    // X и Y одного отчёта приходят с одной отметкой времени: копим, пока она не сменится.
}, nil)
IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue)
IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone))

Что выяснилось по дороге:

  • Нужно разрешение «Мониторинг ввода» (IOHIDCheckAccess / IOHIDRequestAccess(kIOHIDRequestTypeListenEvent)). Без него коллбэк просто молчит, ошибки нет. Разрешение привязано к подписи приложения: с ad-hoc подписью каждая пересборка тихо его сбрасывает. У меня из-за этого одна сессия CS2 прошла без прямого чтения, и я не сразу понял почему.

  • Значения сырые, без системной кривой ускорения. Для шутера это как раз то, что нужно.

  • Некоторые мыши шлют движение через два HID-интерфейса, и одно смещение приходит дважды. Фильтровать надо по устройству.

  • Коллбэк лучше повесить на отдельный поток со своим run loop: главный занят отрисовкой и событиями окна.

  • Читать напрямую стоит, только пока игра прячет курсор. Когда курсор виден, пусть работает обычный путь, иначе щелчки по интерфейсу разъедутся с курсором.

Зачем мне это

Я делаю Uncork, приложение для Windows-игр из Steam на маках с Apple Silicon, и всё это началось с CS2. Теперь мышь там читается так, как описано выше, пока игра держит курсор.

Если вы упирались в то же самое в своей игре или эмуляторе под macOS, напишите, как обходили. Особенно интересно, помогает ли кому-нибудь isMouseCoalescingEnabled на Tahoe.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.