Предыстория
Несколько месяцев назад я решил попробовать сыграть в Microsoft Flight Simulator 2020. Копия нашей планеты, созданная Asobo при помощи фотограмметрии и машинного обучения, казалась подходящим местом для отдыха в эти трудные времена.
Я подключил свой верный беспроводной джойстик Logitech Freedom 2.4 и поднялся в небеса.
Спустя несколько часов полётов вокруг моей альма-матер и дома, в которой я провёл детство, настало время закругляться. Я настроил свой компьютер так, чтобы мониторы отключались через несколько минут неактивности, поэтому быстро понял, что больше они не отключаются.
Такое поведение уже было мне знакомо. Иногда причина заключалась в открытой вкладке браузера, где в фоновом режиме проигрывалось видео, иногда проблема возникала с веб-приложением, а иногда вина была за системной задачей, решившей, что она слишком важна, чтобы отпускать машину в состояние ожидания/сна. В Windows приложения могут запросить эту привилегию у Kernel-Mode Power Manager операционной системы. Это полезно, ведь мы не хотим, чтобы машина уходила в сон или отключала монитор, пока мы смотрим фильм, играем в игру или копируем файл. Такие запросы можно увидеть, открыв командную строку с повышенными правами и запустив powercfg /requests.
Пусто.
Однако у меня возникла идея. Раньше я писал код для проекта под названием procrastitracker — потрясающего компактного приложения для контроля времени под Windows. В нём я реализовал распознавание активности XInput. Видите ли, я использовал это приложение для того, чтобы отслеживать время, которое трачу на игры, а при использовании контроллера Xbox машина считалась находящейся в состоянии простоя. Поэтому моя задача заключалась в том, чтобы приложение распознавало XInput как пользовательскую активность. Я заметил, что Windows использует ввод с контроллеров (не только с мыши и клавиатуры), чтобы определять, что машина используется, и не отправлять дисплей в сон. Также я заметил, что один из аналоговых вводов моего контроллера довольно сильно колеблется, поэтому простая реализация кода без «мёртвых» зон не сработала бы, потому что procrastitracker считала бы, что я использую контроллер, даже когда сплю. «Мёртвые» зоны решили проблему с procrastitracker и контроллер больше не мешал Windows уходить в сон, так что всё было здорово. Виноват ли в нынешней проблеме мой старый джойстик Logitech? Ему сейчас уже 15 лет. Я отключил приёмник и спустя несколько минут дисплей ушёл в сон. Загадка решена!
Но не будем торопиться.
На сцене появляется NVIDIA
С тех пор я обновлял свой компьютер, в том числе купил джойстик получше, рычаг управления двигателем и педали. После их подключения Windows по-прежнему не позволяла дисплею уходить в сон, но я решил изучить проблему внимательнее. Взглянув на панель управления контроллерами USB с подключенным новым джойстиком, я не увидел никаких колебаний. Никакой аналоговой нестабильности и спонтанного ввода. Машине определённо мешала уйти в сон не активность устройства, теперь я был в этом уверен.
Это скриншот, но в реальности всё выглядит так же. Ничего не движется.
Я решил сделать то, что хорошо помогало раньше — обратился к Google.
И Google знал ответ. Люди отследили корни проблемы до оверлея NVIDIA GeForce Experience, иногда называемого ShadowPlay (или NVIDIA Share). Это программа, позволяющая использовать кодировщик NVENC графических карт NVIDIA для записи сжатого видео в реальном времени. Её используют для того, чтобы делиться интересными моментами видеоигр, и это удобно, потому что NVENC хорош. Сжатие видео с высоким разрешением и частотой кадров в реальном времени на CPU с сохранением качества было бы довольно сложной задачей, особенно на машине с уже запущенной видеоигрой, а NVENC обеспечивает качественный вывод без особой дополнительной нагрузки на машину благодаря использованию оборудования кодирования GPU с фиксированными функциями. Это крутая штука, поэтому я не хотел просто так отказываться от неё.
Итак, проблема в следующем: если к компьютеру подключен джойстик и включен оверлей GeForce Experience, то дисплей не уходит в сон. Если отключить джойстик, дисплей засыпает. Если отключить оверлей, то дисплей засыпает. Можно иметь только что-то одно.
Пользователи не просто отследили корни проблемы — они сделали это три года назад!
Я поверить не мог, что эта проблема оставалась неразрешённой так долго. Поэтому я отправил отчёт о баге.
Я был уверен, что разработчики разберутся, но хотел понять причины сам. При включении оверлея запускается множество процессов — все процессы NVIDIA находятся наверху.
Каждый из них загружает множество модулей:
Сначала я думал, что оверлей опрашивает контроллеры на наличие ввода и преобразует эти события в сообщения Windows. Если он инъецировал сообщения в один из своих процессов, допустим, в события клавиатуры, то вероятно, какая-то стандартная процедура обработки событий сбрасывала состояние простоя системы. У меня не было доказательств, но я знал, что причина не только в действиях Windows. Проблему вызывало ПО NVIDIA, но оверлей, тем не менее, не реагировал на ввод с джойстика, поэтому я засомневался, что это случайный побочный эффект кода, написанного для обработки ввода с джойстика. Неправильное использование Win32 часто встречается среди производителей GPU, поэтому я ожидал, что это будет что-то странное. Здесь стоит заметить, что я не специалист по Win32. С другой стороны, у меня есть книга Реймонда Чена и я её читал. Ещё есть отличный блог The Old New Thing, но в нём я немного запутался.
Впрочем, мы отвлеклись. Сначала мне нужно было найти способ обнаружения момента возникновения проблемы, не ожидая засыпания дисплея, поэтому я быстро написал простое приложение, сбрасывающее дамп вывода GetLastInputInfo. Я не ожидал, что эта функция будет иметь полномочия над состоянием простоя системы — для этого нужно получить SYSTEM_POWER_INFORMATION от CallNtPowerInformation, но оказалось, что она действует.
Я подключился к NVIDIA Share.exe в x64dbg и начал искать то, что относится к вводу.
Я знал, что проблему вызывает не Xinput — он используется только для контроллеров Xbox и эмулирующих их, плюс я знал, что procrastitracker и так опрашивает в фоновом режиме Xinput, и дело не в нём. Однако я заметил, что даже когда процесс приостанавливается в отладчике (и в моём маленьком приложении, отслеживающем состояние простоя), видно, что выполняется сброс. Я решил, что создал много процессов, поэтому начал приостанавливать их один за другим (простое завершение по одному не работает, они мгновенно перезапускаются). Состояние простоя продолжало сбрасываться. Это стало важной уликой, она означала, что для этого приложение не выполняет никакого кода. Я не инъецировал сообщения и не делал ничего подобного. Причина была в чем-то, что оно делает при инициализации.
Я хотел посмотреть, как инициализируется NVIDIA Share в отладчике, но это было сложно. Нельзя запустить его напрямую, обязательно запускать его через nvcontainer.exe. Он запускает три копии NVIDIA Share, каждую со своими параметрами. Вероятно, они также общаются друг с другом, поэтому для их запуска нужно аккуратно управлять их средой. Это вполне реализуемо, но можно было попробовать и другие вещи. Я подумал, что было бы здорово, если бы удалось подключить x64dbg сразу после запуска процесса, а рекомендации подсказали мне смотреть в сторону утилиты gflags.exe WinDbg.
Теоретически, можно использовать её для заброса в реестр ключа, приказывающего Windows выполнить конкретный «образ» (исполняемый файл) при его обнаружении с отладчиком. Но мне не удалось этого сделать, вероятно, потому что процесс порождается nvcontainer, а может, я делал что-то неправильно.
К счастью, у нас есть Ghidra. Я проделал те же самые глупые действия, что и в отладчике — загрузил самый очевидный исполняемый файл (NVIDIA Share.exe) и задал самый очевидный вопрос.
«Здесь вообще есть какой-нибудь ввод?»
И я сразу же обнаружил нечто многообещающее! Но для начала нужно было выполнить считывание. Мне плохо знаком «сырой» ввод. В старые добрые времена существовал DirectInput. DirectInput позволяет реализовывать тактильную обратную связь (force feedback), DirectInput позволяет получить доступ к куче кнопок и осей, и по крайней мере под Windows он сильно упрощал работу с игровыми контроллерами по сравнению с прошлым, когда игры должны были поддерживать твой конкретный тип контроллера (или драйверы твоего контроллера должны были эмулировать другой, более популярный контроллер). После DirectInput появился Xinput, а Xinput сильно связан с контроллером Xbox. Вы не можете получить больше кнопок или осей, чем есть у контроллера Xbox. Вы не можете подключить больше контроллеров, чем может подключать Xbox. Всё «просто работает», но подобный API не поддерживает таких штук:
Фото не моё.
Всё, что соответствует стандарту HID, будет передавать свои события, а наша задача как разработчика приложения — обеспечить поддержку тех страниц HID usage, которые мы сочтём нужными. Особенно мне нравится то, что посередине страницы Simulations Control (0x02) есть usage ID для симуляции ковра-самолёта (Magic Carpet Simulation, 0x0B). Комитеты по стандартам продумывают всё.
Итак, что же NVIDIA Share делает с «сырым» вводом? RegisterRawInputDevices.
Не волнуйтесь, я подчищу лишнее:
Он регистрирует свой дескриптор окна, чтобы всегда получать «сырые» события от клавиатуры (вне зависимости от того, какое окно активно). Увы, клавиатура, не джойстик. Но это дало мне идею. Что если расширить моё небольшое приложение, чтобы оно требовало и «сырой» ввод? Как насчёт DirectInput? Можно ли воспроизвести проблему без ПО NVIDIA? Я потратил один вечер и день на реализацию различных способов ввода, заново изучив Win32 и научившись DirectInput… и COM… снова.
Мне удалось воспроизвести проблему.
Из-за включения Raw Input для джойстиков устройства не позволяют системе перейти в режим простоя
Мои рекомендации Microsoft:
- чётче изложить это в документации
- приложение, запрашивающее «сырой» ввод, должно отображаться в
powercfg /requests
и в WPA.
Приложение, которое я написал для демонстрации проблемы, находится здесь: https://github.com/nuzayets/rawinput-debug/.
Однако NVIDIA Share не запрашивал «сырой» ввод с джойстика.
По крайней мере, не напрямую. NVIDIA Share частично создан на основе CEF, Chromium Embedded Framework. Зачем довольствоваться эзотерической десктопной разработкой, если можно добавить ещё и щепотку раздражающей веб-разработки? Я считаю, чем больше, тем веселее. Свободная память ведь нам всё равно не нужна.
NVIDIA Share загружает Chromium Embedded Framework как модуль под названием libcef.dll на сто с лишним мегабайт. Для его анализа Ghidra потребовалось довольно много времени, зато я обнаружил интересный фрагмент.
В драйвере геймпада он запрашивает «сырой» ввод, что логично. Для настройки его параметров всегда вызывается FUN_1842af9b4. Вот эта функция:
Если вы не говорите на декомпиляторском, то вот примерный перевод:
К счастью, никакой код патчить не пришлось. Значения для usage ID находятся в разделе
.rdata
исполняемого файла (это DAT_1861e16e8
в декомпиляции Ghidra).Файл находится в
C:\Program Files\NVIDIA Corporation\NVIDIA GeForce Experience\libcef.dll
и в моей версии GeForce Experience (3.20.5.70) виновный байт прячется по адресу 0x61e0ae8
. Замена 0x04
на 0x06
означает, что вместо того, чтобы пытаться получить «сырой» ввод с джойстиков, библиотека получает его от клавиатуры. Я всё ещё не понимаю, зачем оверлей NVIDIA запрашивает «сырой» ввод с джойстиков у Chromium.Я потратил на это два дня, и в результате оказалось, что проблема заключается в единственном байте в конце. По крайней мере, теперь мой компьютер может спокойно засыпать.
Как устранить эту проблему на своей машине
Если вы не хотите мучиться с шестнадцатеричным редактором, то этот скрипт Powershell сделает всё за вас.
Сначала отключите оверлей и запустите Powershell с правами администратора, чтобы он мог выполнять запись в папку.
Playa
Помню, был один баг, когда наше приложение намертво зависало, если на компьютере установлен punto switcher и пользователь меняет раскладку. Оказалось тоже виноват chromium.