Комментарии 37
Решение не в модификаторах, и не в "режимах" (это боль), а в том, чтобы разделить событие нажатия клавиши и ввода символа. И иметь 2 кода — код клавиши в латинской раскладке для хоткеев (Ctrl + S независимо от раскладки) и Unicode код введенного символа.
Далее, я рекомендую вам не завязываться на раскладки везде. В Линуксе, в иксах, например, вы должны при имитации ввода (искуственном создании событий нажатий клавиш) указать скан-код клавиши. Это причиняет боль разработчикам серверов вроде VNC, так как с клиента может прийти код отсутствующего в раскладке символа (иероглиф какой-нибудь), и где взять его скан-код на системе, в которой нет японской раскладки? Это приводит к хакам вроде "временно добавить в текущую раскладку японский символ с кодом 255, отправить событие keydown/keyup с кодом 255, убрать символ из раскладки". И вся эта жесть из-за плохого проектирования в иксах, так как это проектировали явно англоязычные разработчики, не понимающие проблем ввода на других языках.
По этой же причине, в дебиане с виртуальной клавиатуры нельзя вводить символы, которых нет в установленных раскладках. Фиг вам, а не японские символы без японской раскладки.
Чтобы спроектировать ввод правильно, надо рассмотреть типичные сценарии использования клавиатурных событий:
- А. побуквенный ввод текста в текстовом редакторе, самое банальное. Нужно получать от системы код Unicode символа, а также коды управляющих клавиш (стрелки, backspace и тд).
- Б. хоткеи. Нужен не зависящий от раскладки код — скан-код либо латинская буква. Последнее лучше, так как не зависит от модели клавиатуры и аппаратной платформы (у экранных клавиатур нет скан-кодов).
- В. виртуальная машина или эмулятор компьютера, игры. Требуется имитирование IBM-совместимой клавиатуры и получение "сырых" нажатий с минимумом воздействий или фильтрации. Например, нажатие shift + A просто должно передаваться в программу как нажатие shift + A и все, без учета раскладок, смены регистра букв, состояния капс-лока и тд.
- Г. VNC-сервер, средства автоматизации — нужно вбрасывать в систему искуственные события нажатия клавиш, пришедших от клиента
- Д. консолечка — нужно корректно обрабатывать комбинации вроде Ctrl + C или Esc + A.
- Е. ввод текста на иностранных языках с использованием IME, а также со всяких умных экранных клавиатур (это когда мы печатаем, появляется список вариантов слова и мы его выбираем) — нужно по мере набора передавать слова или их части, которые могут откатываться или коммититься в редактор.
Исходя из этого, как мне кажется, пригодилась бы такая структура события:
keydown/keyup (нажатие клавиши):
- modifiers
- ibm_scan_code — аппаратный скан-код IBM (для эмуляторов и ВМ)
- latin_code — код клавиши в латинской раскладке без учета модификаторов, например "S", "1" (для хоткеев)
input (ввод символа, коммит слова из IME, нажатие управляющей клавиши):
- modifiers
- text (введенная буква или текст)
- keysym (код управляющей клавиши вроде Enter, если нажата она. Для комбинаций вроде Ctrl + C в keysym копируется latin_code)
typing (ввод пока не набранного до конца слова в IME или вирт. клавиатуре)
- text
Вернемся к сценариям:
Физическая клавиатура генерирует события keydown/up, которые порождают события input. Виртуальная клавиатура, VNC-сервер или скрипты автоматизации генерируют события input, из которых воссоздаются события keydown/up. При этом ввод эмодзи, например, не позволит заполнить поля ibm_scan_code или latin_code в событии keydown, так как для эмодзи нет скан-кодов.
А. Текстовый редактор использует только событие input, и берет либо поле text, либо код клавиши из keysym.
Б. Для хоткеев используются поля latin_code и modifiers в событиях keydown/up. Либо keysym + modifiers в input (?)
В. ВМ и эмуляторы, игры используют событие keydown/up и поле latin_code либо ibm_scan_code
Г. Описано выше, вбрасываются события input, по которым воссоздаются keydown/keyup
Д. Для распознавания нажатий вроде Ctrl + C используется событие input, поле keysym + modifiers, с помощью библиотечной функции, которая по событию возвращает подходящий консольный код. Либо используется событие keydown, поля latin_code + modifiers
Е. Используются события typing и input
Вот как-то так.
И, конечно, не стоит изобретать свои коды клавиш, а стоит взять те же иксы или что-то еще готовое.
Из VNC не приезжают IBM scan codes, приезжают как раз иксовые коды.
Сам по себе IBM scan code — штука мерзкая, а ближе к принтскрину и вообще сумасшедшая, не хочется его применять как опорный.
Может быть, обойтись троицей?
— Нетранслированный x11 code или ascii char без локализации
— char транслированный по полной — если нажат ctrl, то 00-1F, если локальный кеймап, то кириллица/японица, что там — в UTF32. Короче, tty char
— модификаторы
Кажется, эта схема закрывает всё и хорошо ложится на VNC и X11.
Скан-коды я оставил исключительно для сценариев вроде DOS эмуляторов или вирт. машин, где нужно передавать внутрь скан-коды IBM-клавиатуры. Но сейчас я подумал, что в принципе DOS эмулятор может использоваться совместно с VNC или экранной клавиатурой, которые не знают ничего о скан-кодах, и может быть, имеет смысл опираться на latin_code + modifiers и из него формировать скан-коды IBM в таких приложениях с помощью готовой библиотечной функции. Плюс, у пользователя может стоять DVORAK-раскладка и в вирт. машину надо передавать переставленные местами скан-коды.
То, что вы предложили выше "отключать трансляцию при управляющих комбинациях клавиш" не годится, так как иногда "горячие" клавиши не используют модификаторы. Например, в графическом редакторе инструменты переключаются просто буквами без модификаторов.
Потому важно, на мой взгляд:
- разделять события нажатия клавиш и события ввода символов или текста (типичный пример — IME, где текст коммитится только после выбора слова, а не при нажатии каждой клавиши или Compose key, где может быть несколько последовательных нажатий клавиш, которые вставляют один символ в итоге, например Compose + 1 + 2 вставляет ½)
- всегда передавать как latin_code, который нужен для реализации горячих клавиш и управления в играх. Для сценариев вроде вирт. клавиатур или VNC, latin_code должен воссоздаваться из приходящих кодов символов (например, на основе самых популярных раскладок, хотя еще правильнее было бы, если бы VNC-клиент передавал и latin_code, и юникодный символ. Так как в VNC передаются юникодные коды, и так как VNC-сервер не знает раскладку на клиенте, он не может в 100% ситуаций корректно восстановить latin_code и корректно передавать нажатия управляющих комбинаций при любых комбинациях раскладок на клиенте и сервере. Это проблема в дизайне протокола VNC)
- в событии ввода текста передавать введенный текст, который может быть как одним символов, так и целым словом при использовании IME (хотя, можно при выборе слова в IME передавать по событию на каждую букву, но передавать слово целиком в одном событии логичнее)
Единственное, в чем я не уверен — надо ли передавать управляющие комбинации, не генерирующие текст (например, Backspace, Left, Ctrl + C) в событии input или нет (только в keydown/keyup). Если их не передавать все выглядит логичнее, но писать эмулятор консоли будет труднее, так как ему нужен и текст и управляющие клавиши и придется обрабатывать все события и разделять нажатия из них. Передача управляющих клавиш в событии input с пустым текстом решает эту проблему, хоть и делает дизайн событий менее красивым.
Код символа для консоли, думаю, нет смысла передавать отдельным полем, его можно получать с помощью библиотечной функции из других полей. Так как он нужен только в одном-единственном приложении — эмуляторе терминала.
Кстати, скажу вам еще одни грабли в иксах. Если вы назначаете какую-то горячую клавишу на комбинацию только из модификаторов (например, Ctrl + SHift для переключения раскладки), то вы не можете вводить другие комбинации с этими модификаторами, например Ctrl + Shift + A, так как переключение раскладки "проглатывает" нажатие клавиши Shift или Ctrl. Другой пример — использование как горячих клавиш одиночного нажатия на Win и Win + буква будет конфликтовать.
Правильно было бы обрабатывать комбинации только из модификаторов не в момент нажатия, а в момент отпускания, и тогда конфликтов не будет.
keyCode — или код функциональной клавиши (тот же X11 code), или latin1 char. Используется именно как управляющая клавиша в контексте команды и для восстановления кода в эмуляторах. 1:1 соответствует клавише. Не модифицируется раскладкой.
ttyChar — только printable, с трансляцией раскладки. Если нет printable кода, то ноль. По нулю можно переключаться на командные символы из keyCode в строках ввода, где нет командной модальности.
modifiers — все биты альтов и контролов, keyUp и прочие meta.
2. Подскажите, где можно увидеть реализацию rect_add? Никак не могу найти её в репозитории. Какую-то непривычную структуру он (репозиторий) у вас имеет…
2 — github.com/dzavalishin/phantomuserland/blob/master/phantom/libwin/rect_cmp.c
Структура имеет исторические корни. :)
1. Был в Фантоме такой эксперимент, как реализация API KolibriOS. А там в апи входят контролы. Кстати, некоторые приложения Колибри запустить удалось, но подробных спек на апи нет, а разработчики на связь не вышли, так что он пока на паузе.
2. Быстрее
Заодно отмечу твое/ваше визионерство — 10 лет назад про персистентность ОС даже и не думали, а тут появились SSD/FlashMem/Intel идеи итп, что сделало идею ФантомОС [почти] реализуемой…
Но не поддерживаю затею с VMкодом в ОС — она себя за это время дискредитировала — слишком большие затраты ресурсов (10х).
VM будет закрыт JIT-ом.
Не знаю ни одного удачного JITa, все проигрывают AOT. Кстати, мой комментарий был про применения пары JIT/GC, т.к. интерпретация байт кода почти не требует ресурсов, только медленная.
То есть, в теории даже есть сишный код распаковщика и структура заголовка упакованного образа приложения, но сделанная по имеющемуся описанию реализация не работает.
Если вдруг Вы в курсе, как оно должно быть устроено, то, может быть, подскажете? Код вот тут:
github.com/dzavalishin/phantomuserland/blob/master/oldtree/kernel/phantom/elf.c#L131
2. Сравнение JIT/AOT — тема сложная и небанальная.
— Есть много критериев. Объём кода. Объём данных. Скорость работы. Объём данных — если на типовом сценарии вылезаем из кеша — может убить скорость.
— Есть много сценариев. Монотонные операции и работа на сильно разных данных. Во втором случае JIT может давать катастрофический выигрыш за счёт перекомпиляции на ходу с учётом анализа фактического графа исполнения и направлений переходов.
— Есть разный код. Ява способна свернуть константы на пачку функций внутрь цепочки вызовов и оптимизировать то, что статический линкер никак не может.
И т.п.
Ну и тупые тесты показывают, что jvm vs gcc даёт вполне сравнимые результаты. Где-то в пользу си, где-то в пользу Явы.
www.stefankrause.net/wp/?p=4
www.codenet.ru/webmast/java/javavscpp.php
Ну и, собственно, явский JIT не проигрывает прямой компиляции. Да и, в целом, никаких причин для того, чтобы ему проигрывать не видно.
Другое дело, что и до джита Фантому ещё надо дожить. А вот, кстати, АОТ сделать проще.
Т.е. во первых мне ни горячо ни холодно от того, что данные как бы уже находятся в памяти и у меня есть на них указатель, потому что во первых это скорее всего медленная память (hdd, flash), а во вторых они там в непригодном для использования виде. А во вторых быстрой (оперативной) памяти всё равно не хватает и необходимо следить за тем что в неё помещается, а от чего можно отказаться. Причём статистические алгоритмы, встроенные в операционную систему явно будут проигрывать движку игры, которых хотя бы знает с чем работает и как данные связаны друг с другом. Ну и в третьих, существование легковесных алгоритмов сжатия вроде lz4, zsdt может сделать нецелесообразным вытеснение данных в медленную память (swap), т.к. их проще и быстрее декодировать и распарсить из оригинального источника.
В связи с этим хотелось бы понять, какой смысл в персистентности, при учёте, что тут приходится имитировать функции чтения/записи из обычной OS, а так же вручную следить за тем, что находится в быстрой памяти. Или такие системы, как PhantomOS по определению имеют практический смысл только в условиях использования гипотетической так и не выпущенной MRAM, имеющей объём hdd, а ресурс и скорость оперативки?
Изначально сжатые данные приложением разжимаются при начале работы.
Имитировать функции чтения из обычной ОС (ФС?) не приходится. Фантом легко хранит содержимое скомпрессированного объекта просто в строке и из неё же можно распарсить финальный объект.
Вот тут есть пример программы для Фантома: phantomdox.readthedocs.io/en/latest/#example-of-phantom-program
Обратите внимание на строки
bmp = new .internal.bitmap();
bmp.loadFromString(getBackgroundImage());
По сути они сводятся к
bmp.loadFromString(import "../resources/backgrounds/weather_window.ppm");
Конструкция import возвращает строковую константу, инициализированную содержимым файла в момент компиляции.
Эта строка будет содержать в данном случае картинку в файловом формате.
loadFromString парсит и конвертирует в финальный битмап, который и живёт в персистентной переменной.
И нет гарантии, что она подкреплена физической памятью. Ни в какой ОС.Функции VirtualLock в Windows и mlock в unix смотрят на это утверждение с удивлением…
А технически то запереть странички и в Фантоме можно, тут разницы нет.
На самом деле просто есть одна вещь, которую давно хотелось иметь в виде прикладного API, но которую никто не делать. Это обработчик для проецирования в память. Т.е. при создании некоторой области памяти, для неё определяется функция-генератор и один или более источников — файловых дескрипторов или указателей (для фантома только последнее). При обращении к указанной области памяти система вызывает обработчик, который генерирует весь объект или его части. Теперь система не сохраняет содержимое объекта в swap, а просто выбрасывает его и воссоздаёт вызовом функции-генератора по необходимости. В более сложном случае может быть даже определена функция, управляющая очерёдностью выкидывания страниц из памяти. Так же, такие объекты, созданные в режиме только чтение и зависящие только от внешних источников могли бы становиться разделяемыми, если определены в разделяемой библиотеке.
Собственно, практическое использование могло бы быть уже в коде, отвечающим за рендеринг шрифтов. Сейчас вы рендрите шрифты в битмапы, а их храните в памяти для отрисовки. Т.е. количество глифов перемножаем на используемые размеры, перемножаем на цвет символов, перемножаем на количество шрифтов в системе, учитываем, что современный качественный рендеринг шрифтов использует субпиксельное позиционирование, а порой ещё и сглаживание с фоном… Т.е. количество отрисованных глифов быстро становится астрономическим. И всё это хранится в несжатом виде, а потом попадает на диск и лежит там вечно. Ну или придётся городить отдельный уборщик мусора для шрифтов, который будет искать что уже никому не нужно и удалять ссылки, чтобы потом системный сборщик мусора наконец удалил это всё.
Ну или это помещается в такой вот генерируемый объект и выкидывается как только появляется потребность в памяти, а потому рисуется заново по мере необходимости.
Или как второй этап делается собственная библиотека, которая на первом этапе читает содержимое ttf, переводит в более удобный формат и имеет более низкий приоритет на удаление, т.к. эти данные более компактны и чаще востребованы. В вот уже генерируемый объект от этого объекта хранит кеш глифов с алгоритмом удаления, выкидывающим из памяти в первую очередь давно не использованное и редко используемое, чтобы старьё и разовые глифы не занимали память…
Я просто думал, что такое будет естественным для фантома и хотел узнать как его реализовали (на что получился похож API), но увы, до такого похоже ещё очень долго…
Тут бы надо разделять две сущности. Генерацию объекта по факту потребности в нём и memory mapping. Первое вполне возможно без второго, вообще говоря. Запрашиваем глиф, если его нет в кеше рендеринга, то рисуем и выдаём. При исчезновении всех ссылающихся на глиф объектов он остаётся только в кеше. Кеш прореживаем по LRU.
Мне отображение в память больше виделось как инструмент для доступа к сетевым ресурсам.
И нет гарантии, что она подкреплена физической памятью. Ни в какой ОС.
Неправда. Отключите на Линуксе своп и все аллокации на куче будут в RAM.
Вы забыли про overcommit memory в линуксе — до первой записи страницы не мапятся на физическую память.
Вы используете одну общую очередь для событий отрисовки и событий ввода? В этом случае при скоплении тяжелых отрисовочных задач ввод может тормозить.
Битмапы на каждое окно мне не особо нравятся. Вы оцениваете, какой будет расход памяти на экране 4K с 20 полноэкранными окнами? Я знаю, что это тенденция во всех современных дисплейных серверах, но неужели нельзя хотя бы на скрытые окна не тратить память? Это хорошо работает на смартфонах, где окно одно и занимает весь экран, но на десктопе это ведь неэкономично.
А почему вы завязываетесь на древний способ 2д отрисовки — возней с пикселями, копиями, битмапами, кешированием и софтварной растеризацией? Сейчас практически на каждом устройстве есть поддержка как минимум OpenGL ES а это значит что графическую подсистему можно построить так что заниматься растеризацией и прочей возней с пикселями будет видеокарта а со стороны программы или os достаточно просто на каждый фрейм отрисовки передать на gpu массив объектов данных примитивов (если это прямоугольик то координаты вершин или точка + ширина-высота, если кривая безье то координаты контрольных точек и т.д) — и уже на видеокарте в шейдерах будет происходить отрисовка. Причем примитивы можно бесконечно усложнять поскольку шейдеры можно программировать.
Что касается взаимодействия отдельных программ-окон то тут есть 2 варианта
1) Каждое окно будет лично взаимодействовать с видеокартой — вызывать функцию отрисовки (draw-call) но результат будет сохраняться во временный буфер-текстурку а потом os еще раз вызовет отрисовку композируя из нескольких текстурок итоговую картинку. Причем надо заметить что результат отрисовки каждого окна можно хранить на видеокарте а не заниматься копированием между cpu и gpu
2) Можно попробовать построить взаимодействие так чтобы вызов отрисовки (draw-call) на видеокарте был только один — для этого собираем результат от каждого окна и дополнительно препроцессим смещая координаты — либо на уровне массива примитивов (через постпроцессинг либо через некий общий апи рисования) смещая координаты каждого примитива с соотвествии со смещением каждого окна либо на уровне компиляции кода шейдеров добавить дополнительный код который будет относительно каждого окна добавлять смещение для gl_Position
Но — на всё рук не хватает, да и акселерированные драйвера современных видеокарт — это ад и израиль, несколько подходов к снаряду вида «что бы такого втянуть целым кусочком» пока кроме глубокой задумчивости результатов не дали. :)
Так что пока я тактически отступил к 2D.
Присоединяйтесь, сделайте это, будет действительно здорово. Я уже представляю себе шелл с трёхмеркой и окнами, которые сложены в стопочку в углу экрана. :)
Некие мысли по поводу контролов.
Думаю, можно выделить два подхода к построению UI:
- контролы рисует система, повинуясь вызовам API (напр. GDI+). За обработку ввода в этом случае также отвечает система.
- система просто предоставляет буфер, куда приложение рисует своими силами (Qt QML, WPF, UWP итд, а также б-гмерзкие веб-технологии). За обработку ввода внутри окна отвечает приложение.
Оба подхода имеют как свои преимущества, так и недостатки. В последнее время все больше второй подход применяется. С одной стороны — возможность создавать графически насыщенные приложения, не пытаться вписаться в ограничения ОС, а при кроссплатформенности пытаться привести разные графические подсистемы к общему знаменателю. С другой стороны — по-сути, дублирование функциональности, неумение (или плохое) адаптироваться к системной теме (опять-таки костылями).
И глядя на все это безобразие, я иногда думаю, что сейчас проще вообще делать ОС/графическую подсистему, которая не умеет рисовать ничего внутри окна, а только обеспечивать оконный менеджер, композинг, декорации окна итп.
ОС Фантом: оконная подсистема — делаем контролы