четверг, 27 декабря 2012 г.

Cowboy Jed: Zombie Apocalypse [Windows 8/RT]

Наконец закончены работы над Windows 8 версией нашей игры Cowboy Jed: Zombie Apocalypse. До этого осенью были выпущены версии под iOS и Android.
В отличие от других платформ, издателем выступаю я лично от лица Jed Games. Заодно сделал перевод на русский, доработал графику, улучшил разрешение задников, зомби и пр. Редизайн затронул и оружейную, и таланты.

Обзор игры: http://wp7forum.ru/kovboj-dzhed-apokalipsis-zombi/
Ссылка в Windows Store: http://apps.microsoft.com/windows/app/e840ccf9-add4-41bc-8ef4-b5763ead312b
Дальше..

четверг, 13 декабря 2012 г.

Classic Solitaire HD in Windows 8 Store

Some time ago I tried to port C# version Classic Solitaire HD from iOS/Mac to Windows 8. There were some issues with absence of System.Drawing namespace and not 100% compatible rendering pipeline (no frame-by-frame operations, only regular XAML objects) but overall porting came surprisingly easy.
That was mostly possible because of multi-layered architecture where game core was written in platform-independent style in Java and then converted to C# with Sharpen. Upper layer (GUI) slightly differs for each platform, but that is not an issue.
Hardest part was implementation of "jumping cards" cause in Win 8/RT world there is no way to get back drawing canvas image from GPU and paint on it once again. To solve this problem I used BitmapCache trick: I group N (let's say 100) jumping cards and move them to separate Canvas object that is set to use BitmapCache. Then Canvas is frozen and all painting is performed in new layer. Of course, we can run into performance and memory problems with lots of layers, but tests shows that modern CPUs/GPUs can handle more than 3000 cards in 20-30 layers without significant performance degradation. Well, that's funny - you need modern PC to play year's 89 game :)
I also implemented fancy settings menu and put in several retro and modern card decks.


There are two builds available: paid for $1.49 (that's weird, but MS set this as minimum price) and free with advertisings. Ads could be removed by entering promo-codes that I can surely give to my subscribers by request.

Links

Paid, $1.49: http://apps.microsoft.com/webpdp/app/eec5728f-182f-4c7c-86ba-99c07ceb9a12
Free: http://apps.microsoft.com/webpdp/app/5b46dba6-eb7e-4baa-8e2b-a3e0aeaf5607

Also, there is nice review of Classic Solitaire HD from win8reviews.com:
http://win8review.com/2012/12/a-review-of-classic-solitaire-hd-for-windows-8/

And now, most funny part: porting to Win 8 and XAML also allowed me to made a port of this game to Silverlight by changing some namespaces and XAML bits. I put the game to Facebook page, however it lacks social features and full FB integration:
http://facebook.com/appcenter/classic_solitaire

Дальше..

четверг, 7 июня 2012 г.

Как отключить iAD не меняя код

В приложениях для iOS довольно логично использовать рекламу от Apple - iAD. Нюансы начинаются, если хочется провести акцию "неделя без рекламы", или вообще отключить ее навсегда, чтобы не досаждала пользователям, или вообще реклама работает в паре с AdMob и показы iAD уменьшают доходы AdMob, а ничего существенного не приносят.
В моем случае была вот такая картина:



Вкратце эта табличка означает, что целевой аудитории (Россия) вообще ни разу не показывалась реклама, а за месяц показа в США и Европе она принесла чуть меньше $50. С таким фил рейтом и eCPM порядка $2 лучше отказаться от iAD совсем. Apple для отключения рекламы говорит следующее:
iTunes Connect Developer Guide page 129:
Once your app has been submitted, iAd cannot be disabled. To remove ads from an app, you will need to submit a new binary with ad functionality removed
Дословно это означает, что надо залить версию без рекламы. Подход отличный, только аппрув новой версии может затянуться на недели.

К счастью, лазейка нашлась в управлении настройками iAD через сайт iTunesConnect: там их минимальное количество, но зато можно указывать фразы, которые будут искаться в ссылках/текстах рекламы и по ним реклама будет фильтроваться:





Собственно, весь трюк видно на этом скриншоте. Фильтры по ключевым словам www, http, com, itunes должны отфильтровать 90-95% рекламных объявлений и ссылок. Покрытие фильтра не 100%, некоторые баннеры могут пробиться, но результат становится заметен буквально уже на следующий день:




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


Дальше..

суббота, 19 мая 2012 г.

Сканворды

Когда выпадает свободная минутка многие обращаются к смартфону или планшету в поисках развлечений: кто раскладывает косынку, кто борется с зомби или форумными троллями, а пытливые умы могут попытать счастья в разгадывании кроссвордов и сканвордов. Программа "Сканворды" появилась почти год назад под Андроид, а недавно еще и была портирована под iOS.


Технические требования: Android 2.1 и выше, разрешение от 320х480; iOS 5.0, iPhone 3gs, iPod 3, iPad, доступ в интернет

В программе 28 выпусков, а это в общей сумме более 750 различных сканвордов и кроссвордов. Один-два раза месяц выходит новый бесплатный выпуск. Тут есть сканворды, американки, кроссворды, с картинками и без, анаграммы и без гласных, клаccические, кейворды и другие.

Другая изюминка программы - онлайн рейтинг игроков, в котором сейчас уже около 30000 участников. Программа привлекает проработанным интерфейсом и поддержкой мультитач для операций pan/zoom.




Стоимость: Бесплатная с рекламой (iOS + Android), $1.99 (iOS)

Обсуждение на форуме 4pda (iOS): http://4pda.ru/forum/index.php?showtopic=340082

Обсуждение на форуме 4pda (Android): http://4pda.ru/forum/index.php?showtopic=248019

Google Play Web: https://play.google.com/store/apps/details?id=com.oxothuk

iTunes Store: http://itunes.apple.com/ru/app/scanwords-free/id520199170?mt=8

iTunes Store, версия без рекламы: http://itunes.apple.com/ru/app/scanwords/id525966130?mt=8

Другие скриншоты:


Остальная часть

Дальше..

понедельник, 14 мая 2012 г.

Классическая Косынка

Не так давно я столкнулся с тем, что пасьянсы под Android все сделаны с «изюминкой» – у многих необычные картинки, разные навороченные функции, собственная таблица очков и пр. Это прекрасно, но мне долгое время не хватало именно минималистической «Косынки», в которой бы так же работал подсчет очков и был привычный внешний вид.


В результате родилась на свет очередная инкарнация пасьяна «Косынка» (Клондайк) под Android. Игра выглядит и работает 1в1 так же, как привычная всем версия под Windows. Точно такая же система подсчета очков, графика. Рекомендуется только для таких консерваторов как я. :)

Особенности:
- минимальный размер (~ 100 Кб)
- нет надуманных функций, странных «адаптированных» картинок карт
- ландшафтрый и портретный режимы
- сдача по одной или три карты, игра на время, стандартный подсчет очков и игра на деньги (Vegas scoring)
- двойное касание переносит карту к тузам
- прыгающие карты при победе
- возможность поворота экрана кнопкой или по сенсору
- поддержка Android 1.5 и выше
- бесплатно, без рекламы!


В следующих версиях (вплоть до 1.0) запланированы:
- кнопка отмены хода
- перевод интерфейса на русский
- различные рубашки и их выбор
- кумулятивный подсчет очков в режиме Vegas

P.S. В этой программе не планируется добавление десятка других пасьянсов, сайта с рейтингом, hi-res карт (их просто неоткуда взять), поддержи телевизоров и Android-микроволновок. Возможно, будет сделана поддержка Nook Touch.

P.P.S. Не смотря на визуальную идентичность, контент из Windows Solitare авторства Сьюзен Кэр не используется.
Колода карт основанна на GPL проекте тов. oxymoron: http://www.waste.org/~oxymoron/cards/
Фоски раскрашены по аналогии с оригинальными. Рубашки, иконка и вспомогательные изображения нарисованы вручную лично мной.

Страница на 4pda: Классическая Косынка
Google Play: Classic Solitaire

Ну и напоследок:


Дальше..

Минувший год


Вот уже больше года прошло с последнего сообщения..
Особо не было времени писать, хотя и прозошло много разного.
Вкратце: поменял работу, перешел полностью на Мак, довел MMT 2 до стадии законченного прототипа и заморозил, сделал универсальный рут/анбрик для Nook 1st, на время забросил ридер, потом вернулся к нему. Побывал в Нидерландах, Доминикане. Последние месяцы пишу игры и программы для мобильных устройств. Подробностей этих проектов пока раскрыть не могу, но когда релизнутся - напишу тут описания и некоторые технические нюансы.

Напоследок, финальный walkthru ролик из MMT 2. Собственно, то, на чем мы остановили разработку и ведем переговоры с инвесторами.

Дальше..

пятница, 8 апреля 2011 г.

MMT Online won Project Bossanova contest

MMT is a MMORPG game prototype developed using bleeding-edge Ungine 3D engine and RunServer middleware. First presented at GDC 2009 in Moscow on Windows, MMT is being reborn on Linux. We have set out to develop a full-featured MMORPG in a fantasy setting with traditional third-view camera and ASWD control.

When Project Bossanova contest began we got only a rough demo of the game available for Windows in Russian, but this demo was ported to Linux, where development will now be focused, and translated into English. Aside from the game engine itself along with its shader technology, which is out of our control, the client will be open source.

RunServer is about 7 years in commercial server development business. We have fast and stable server capable of handling 2500+ online players per cluster node.
Now with Project Bossanova funding we'll spend most of the time developing client content and storyline instead of fighing network, bugs and lags like 99% of other Indie MMO project do.

Most complete profile of our project is available on IndieDB site with screenshots, movies, concepts and developers' comments:
http://www.indiedb.com/games/mmt

Official project page at RunServer:
http://runserver.net/mmt
Дальше..

понедельник, 28 марта 2011 г.

MMT Demo entered Project Bossanova Contest!


Two years ago our company created MMORPG game prototype called MMT in medieval fantasy setting. Demo used bleeding-edge Unigine 3D engine and RunServer middleware. It was presented on Moscow GDC 2009 (KRI) and then suspended. While the project were rather proff of concept than real game, it had a lot of functionality including rich gameplay, combat, magic system, leveling, classes, items, loot, etc.
Now in year 2011 MMT is being reborn on Linux. We have set out to develop a full-featured MMORPG in a fantasy setting with traditional third-view camera and ASWD control. Game engine was updated to recent version with a lot of improvements and visal effects and now we run on OpenGL.

Now we are proud to enter Project Bossanova contest to get sponsorship for further development of this game.
You can vote for MMT on following page:
http://www.projectbossanova.com/comp/vote

There are also several gameplay videos available on our YouTube channel:
http://www.youtube.com/user/NomadRunserver

Some more screenshots:




Дальше..

воскресенье, 27 марта 2011 г.

Дебрикинг Нук

Расстановка

Решил написать историю о том, как я дебрикал Нук (восстанавливал убитый прошивкой до версии 1.0.0), дабы не было мыслей вроде "гад Номад нашел какие кнопки надо зажать для дебрика, а делиться не хочет".
Далее следует стена текста, интересная только железячникам и линуксоидам. Нормальным людям лучше не читать :)

Дебют

Началось все с того, что приехал мне брикнутый девайс с просьбой попробовать его починить. Симптомы классические - надпись "Restoring to Factory update" и тишина.. Где-то через три дня пайки, собирания шлейфов и медитации над логами и сорцами, я осознал, как именно выглядит наша проблема. Девайс грузит u-boot, которому вообще все-равно с какой памятью работать, затем должен грузиться turboboot (промежуточное ядро, которое и умеет делать апдейт/откат), а затем уже полноценное ядро и система. Так вот, turboboot от версии 1.0.0 (а за ним и основное ядро) при инициализации MMC ругается и не подхватывают NAND память - она использует протокол MMC 4.3 или 4.4, поддержка которых появилась лишь в ядре Линукса 2.6.36, а для B&N прошивок 1.4 и 1.5 была бекпортнута на наше ядро. Кстати, именно по-этому и не стартует 1.5.0k на новых Нуках - там та же ошибка.
К этому же времени правдами и неправдами мне удалось выполнить собственный код на брикнутом Нуке, но вышла глупая ситуация - в системе не создалось устройство для NAND, потому и починить ничего нельзя. Возможно, будь я супер-спецом по процессорам и железу, смог бы написать код для обращения к NAND напрямую, но для меня это темный лес.. Другой идеей было взять код UBoot, выкусить работу с MMC и написать свою программу. Примерная оценка трудозатрат на это - не меньше недели фулл-тайм работы, включая изучение чужого кода, идеологии его работы, отладку и тестирование. Много часов я пытался собрать драйвер MMC в виде модуля, который бы выгрузил имеющийся в ядре код и заменил его (это не бред, в драйвере тачскрина я так и сделал), но оказалось, что поддержка нашего железа прибита гвоздями к внутренностям ядра и сделать это очень и очень тяжело (даже если откинуть в сторону всякие "мелочи" вроде невозможности поиска и выгрузки шины по имени, т.к. код find_bus закомментирован в линуксе очень давно).

Миттельшпиль

На другой день я узнал о такой сказочной фиче как kexec. В двух словах, это возможность запустить еще одно ядро прямо из выполняющегося. Я думал, сам turboboot так и работает, но был жестоко разочарован - там какой-то проприетарный код для перехода на другое ядро, а само ядро скомпилировано без поддержки kexec.. Перерыв кучу форумов, я нашел проект, в котором люди пытались сделать kexec в виде подгружаемого модуля. Это была смелая идея, но как я понял, проект таки не был доведен до ума и работал только на Motorola Milestone и DroidX. Самое неприятное, что сам модуль собирали для другого ядра (2.6.29 или старше), да еще и с ARMv7 кодом (у нас ARMv6 процессор). Не говоря о том, что поиск таблицы системных вызовов там был закоменчен и стоял адрес, к нам отношения не имеющий, а сам код после раскомментирования толком ничего и не искал. Уже не скажу, сколько пришлось его бекпортить и дорабатывать подручными средствами, но таки модуль собрался, научился искать syscall_table и его .ko файл мне удалось подгрузить прямо в работающее ядро turboboot.
На этом этапе еще немного пришлось повозиться (снова подручные средства) с исполняемым кодом kexec-tools, дабы его таки запустить, и оказалось, что попытка загрузки любого ядра приводит к перезагрузке устройства... По-сути, модуль сыроват и не дописан, направление бесперспективное. С этой пессимистичной мыслью я и пошел спать.
На следующий день, занимаясь основной работой я вдруг подумал - а если ребут не результат бага, а запланированное поведение? Так и оказалось - один из драйверов WM8350 при попытке выгрузки считал, что время перезапускать систему и отключал питание :) Повлиять на сам драйвер не получилось, потому пришлось переделывать код выгрузки всех драйверов, чтобы именно этот игнорировался. Я чувствовал, что от этого еще будут проблемы, но решил рискнуть. Как результат - устройство стало не перегружаться, а просто сказало "Bye!" и зависло :)

Пат

Расстраивался я не долго и перебором нашел, что не смотря на поддержку uImage и zImage, kexec грузит только не арихвированные ядра. Тут бы и истории конец, но оказалось, что все самосборные ядра, а заодно и заводские 1.4-1.5 доходят до инициализации GPIO и зависают.. Чуть дальше загружалось ядро, выкушенное из turboboot, но потом валилось при загрузке драйвера LCD экрана, да и в целом пользы от него было бы не много. Следующие день или два (казалось, что прошла целая вечность) я собирал собственные ядра, выкидывал оттуда все, что можно (зачем нам e-ink или звук, когда надо только перепрошиться?) и комментировал места, где происходили зависания. В общих случаях так делать нельзя - ядро без GPIO и работать-то не должно, но тут мне таки повезло и я собрал супер-обрезанное ядро, которое умело буквально только работать с MMC (поддержку протокола 4.3 я уже бекпортнул из старших версий ядра). Заодно я сделал собственный образ рам диска для этого ядра и написал скрипт, который при загрузке зальет на MMC карту turboboot и остановится. Тут еще оказался мелкий казус с размещением turboboot - из-за каких-то багов скрипт не мог узнать размер MMC карты (кривой бекпорт? особенность запуска из-под kexec?), потому я рассчитал точное размещение и вбил в скрипт фиксированное число. Это было очень опасно - смещение хоть на один байт привело бы к полному окирпичиванию, но все прошло успешно. На устройство залился новый turboboot, который видел внутреннюю память. Это была почти полная победа - оставалось лишь убедить нового turboboot сделать откат на заводскую прошивку. Он теперь видел NAND и корректно развернул туда заводской образ.. только вот этот образ оказался версии 1.0.0..

Эндшпиль

Не знаю, как туда попала 1.0.0 (видимо, ошибка B&N при изготовлении Refurbished нука), но получилось, что загружается новый turboboot, а затем грузит старое ядро, которое все еще не видит NAND. Более того, на этом этапе я уже не мог выполнить свой код, а нук не загружался и периодически откатывался на прошивку 1.0.0. Причем, в этой заводской прошивке не было turboboot.img, потому и нельзя было вернуться к предыдущему состоянию, потому ситуация выглядела откровенно плохо. Решив не расстраиваться, я вчитался в логи и заметил, что связка turboboot от 1.5 и kernel и 1.0.0 при каждом старте пытается загрузить файл /init (загрузчик Android), причем если вставить SD карточку, то файл ищется и на ней. Издав ликующий звук я записал туда скрипт для загрузки модуля kexec_load.ko и вызова kexec, но снова уперся головой в стену багов: ядро 1.0.0 отличалось от того, что внутри turboboot 1.0.0 и не имеет поддержки /proc/iomem. Это устройство должно сообщать, по каким адресам расположена физическая память, без чего kexec работать не может. Адреса этой памяти далеко не 0-0x1000000, как можно подумать, но к счастью в тоннах логов нужные адреса нашлись и я прописал их в коде самого kexec.
Загрузилось мое мини-ядро, но теперь я заливал на NAND не turboboot, а целиком bravo_update.dat от прошивки 1.5.0n. Конечно, адреса пришлось заранее высчитать на калькуляторе, но тут ошибка уже не была так страшна, да и в целом все прошло успешно - устройство откатилось на "заводскую" 1.5.0n и стало полностью работоспособным.

Вывод

Как видите, ничего особо сложного в этой операции не было, и если кто-то захочет, то без проблем сможет пройти мой путь, написать нужные патчи, собрать модуль kexec, нужные рамдиски и скрипты, научиться выполнять код на брикнутом нуке. Я только буду этому рад :)
А пока этого не произошло, я готов восстановить любое количество брикнутых нуков за небольшое вознаграждение. Пишите на g.a@ua.fm, договоримся.
Когда же мне это надоест - я выложу свои исходники kexec-mod, kexec-tools, модуля mmc и пр. Может кому-то пригодится.

Дальше..

четверг, 23 декабря 2010 г.

Эвристика в поиске пути

Когда наша команда впервые столкнулась с задачей поиска пути, мы не смогли удержаться от попытки изобрести собственный "велосипед". Путь был тупиковым, что стало ясно уже через несколько недель экспериментов и поиск пути отложили почти на год. И вот через год задачу снова подняли и решили использовать обычный A*. Его описаний в интернете достаточно и в 99% случаев достаточно просто реализовать алгоритм по шаблону. В моем случае именно так и получилось, поиск пути занял свое место в Runserver.Math и в серверах на его основе. Через несколько месяцев появились сообщения, что траектория выходит не очень красивой - агенты довольно неестественно движутся под углами, кратными 45-ти градусам. Через некоторое время мне встретился сайт тов. Amit Pate с описанием и примерами эвристической функции. Вчитавшись я осознал, что в мой код прокралась критическая ошибка..


Теория

Говоря обобщенно, эвристическая функция отвечает за то, чтобы поиск шел в направлении конечной точки, а не во всех направлениях (в отличие от алгоритма Дейкстры, поиска в ширину и других). Неверный баланс этой функции может привести к не-оптимальным результатам, опросу ненужных областей и другим казусам. Функция должна быть сбалансирована со значением g - минимальным расстоянием, которое надо пройти от начала пути к текущей точке.

Манхэттенское расстояние

Достаточно часто используется т.н. Манхэттенский метод, который вычисляет минимальное расстояние, которое надо пройти вдоль осей координат, чтобы попасть в данную точку:
h = D * (|x-fx| + |y-fy|)

x, y - текущие координаты, fx, fy - координаты конечной точки пути, D - вес одного шага.
Главный недостаток этого алгоритма - "ступенчатость" траектории.

Диагональное расстояние

Более сложная разновидность Манхэттенской функции учитывает движение по диагонали между клетками. Формула учитывает, вдоль какой из осей путь будет длиннее:
ax = |x-fx|
ay = |y-fy|
if ax > ay then
h = ax * D * √2 + D * (ax - ay)
else
h = ay * D * √2 + D * (ay - ax)

D - вес шага вдоль осей координат, D * √2 - вес шага по диагонали. Такая функция идеально сбалансирована и не смотря на большое количество исследованных точек, выдает оптимальный путь за приемлимое время.

Евклидовое расстояние

Другой подход - использовать реальное расстояние до конечной точки. В этом случае функция получается не идеально сбалансированной, потому что расстояние g считается с учетом движения в 8-ми направлениях, а от точки до конца пути - напрямик. A* все-равно выдаст оптимальный путь, но проверит чуть большее количество точек:
         _________________
h = D * √(x-fx)² + (y-fy)²


Квадрат евклидового расстояния

Этот метод во многих источниках заклеймен как лженаучный и неверный. Проблема в том, что значение функции будет заметно больше пройденного пути g, потому алгоритм будет искать не оптимальный путь, а как-бы "притягиваться" к конечной точке. Формула выглядит так:
h = D * ((x-fx)² + (y-fy)²)

Именно такую ошибку я и допустил в своей реализации - забыл добавить квадратный корень, но при этом количество проверенных точек получилось в разы меньше, чем у других функций!

Ситуация усложняется при добавлении препятствий. Квадратичная функция:

Диагональная функция:

Во втором случае путь "правильнее", но было проверено очень много лишних точек. По-честному, первый вариант вообще не является A* - он выдает не самый оптимальный путь, а первый найденный. Меня такой вариант устраивал - в реальной жизни персонажи редко движутся идеально, потому для AI такой алгоритм вполне подходит. Нерешенной осталась только проблема, из-за которой я и начал копаться в эвристике - излишняя "геометричность" пути.

Алгоритм компаса

Я не придумал, как правильно сформулировать название этого алгоритма. Автор алгоритма (Amit Patel) описал его так:
A different way to break ties is to prefer paths that are along the straight line from the starting point to the goal

Идея схожа с навигацией по компасу - имея некое направление, после обхода препятствия надо двигаться в том же направлении, что и раньше. Базовая формула эвристики остается прежней, но к ней добавляется еще небольшое смещение:
cross = |(x-fx)*(y-sy) - (y-fy)*(x-sx)|
h = h + cross * k

sx, sy - координаты начальной точки, k - коэффециент смещения, у автора алгоритма 0.001, cross - модуль векторного произведения вектора движения для текущей и начальной точки.
Для открытых пространств такая функция работает очень хорошо, а вот обход препятствий дает некоторую погрешность, которой можно управлять, меняя значение k. Я так и не нашел баланса этого коэффициента, при котором путь остается оптимальным и не появляется артефакт "возврата":


Алгоритм "наведения"

В результате всех экспериментов я вывел собственный алгоритм, схожий с "компасным", но стремящийся не к линии между начальной и конечной точкой, а к линии между предыдущей пройденой и конечной точкой:
cross = |(x-fx)*(y-py) - (y-fy)*(x-px)|
h = h + cross * k
px, py - координаты предыдущей пройденной точки. Такой подход дает не идеальную картину при движении без препятствий, зато при коэффициенте k = 0.5 обход препятствий смотрится вполне корректно:

Подобная эвристика дает оптимальный путь и решает большую часть проблем A*, особенно движение по ровной местности. На мой же взгляд эти плюсы так и не перевесили скорость вычисления "неправильной" квадратичной функции. К примеру, с тем же препятствием, без всяких дополнений она справляется вот так:
Потому я решил продолжить исследования в направлении смещенного баланса и оставить возможность выбора режима работы - "точный" и "быстрый".

"Жадный" алгоритм

Результатом дальнейших поисков стала функция, напрочь отрицающая баланс g и h. Это сделало алгоритм из A* явно выраженный Greed Search, который, как ни странно, работает очень быстро и для AI выглядит вполне естественно.
Формула такова:
ax = |x-fx|
ay = |y-fy|
if ax > ay then
h = ax * D * n
else
h = ay * D * n
cross = |(x-fx)*(y-sy) - (y-fy)*(x-sx)|
h = h + cross * k
n - весовой фактор, k - коэффициент "наведения".
В приведенной иллюстрации коэффициенты равны 100 и 1 соответственно.

Заключение

Это исследование не несет никакой агитационной цели. Для наших задач я решил использовать одновременно и обычный A*, и "жадный" алгоритм, но во многих ситуациях нужна максимальная точность и Greed алгоритмы не применимы. В любом случае, статья будет полезна тем, кто хочет понять работу А* и возможности его эвристической функции.

P.S. Напоследок несколько "живых" примеров работы Greed алгоритма.

Средняя плотность. Расчет занял 228мс, 2689 точек, 5289 проверок видимости:

Большая плотность. Расчет занял 274мс, 2177 точек, 4110 проверок видимости:


Дальше..

суббота, 27 ноября 2010 г.

Как сделать невозможное?

Ответ на вопрос прост - не останавливаться, какой бы сложной не была задача. Даже если все вокруг говорят, что это невозможно.
Моя задача была заменить драйвер устройства в Linux, вшитый намертво в ядро. Были бы исходники этого ядра - все решалось бы сравнительно просто, но в нашем случае (ядро читалки Nook) исходники были не рабочие и годичной давности. На x86 я бы дизассемблировал место инициализации драйвера в ядре и забил NOPами, но тут уже ARM архитектура и ядро в zImage..

Сам процесс написания и отладки драйвера был довольно долгим - пришлось допиливать исходники ядра, чтобы устройство хоть как-то загружалось, но это тема для другого рассказа. Важно то, что получился собственный драйвер (бекпорт с другого устройства с дополнительными фозможностями), который может работать как модуль ядра и инициализируется 1в1 так же, как и тот, что надо заменить.

Первые эксперименты были прозаичны - хитрые вызовы insmod/rmmod, шаманство с именем драйвера и пр. Затем я обратился за советом к друзьям-линуксоидам и они подтвердили, что стандартными средствами такого не добиться. Исключения очень редки - если драйвер может принимать параметры с адресами устройств, командами включиться/выключиться и пр., то можно еще что-то сделать, но в нашем случае никаких параметров не было.

Затем начались эксперименты на уровне исходников. Мое внимание привлек такой код:

static int __devinit synaptics_ts_init(void)
{
synaptics_wq = create_singlethread_workqueue("synaptics_wq");
if (!synaptics_wq)
return -ENOMEM;
return i2c_add_driver(&synaptics_ts_driver);
}

static void __exit synaptics_ts_exit(void)
{
i2c_del_driver(&synaptics_ts_driver);
if (synaptics_wq)
destroy_workqueue(synaptics_wq);
}

Если существует функция i2c_del_driver для выгрузки драйвера, то почему нельзя вызвать ее из своего модуля, чтобы выгрузить чужой? К сожалению, в ядре Linux есть ситуации, когда не имея указателя никак нельзя обратиться к обьекту. Например, метод destroy_workqueue можно вызывать только с указателем очереди, а он есть лишь у того, кто эту очередь создал и без получить его малореально - никаких find_workqueue в природе нет.

В нашем же случае шанс был - пусть и i2c не позволяет искать драйвер по имени, но по коду i2c-core.c стало понятно, что используются низкоуровневые вызовы device_register/device_unregister из linux/device.h. Там же нашлась функция driver_find, которая ищет драйвер по имени и идентификатору шины. Имя встроенного драйвера известно, а шина используется i2c и ее идентификатор в переменной i2c_bus_type:

struct device_driver * other;

other = driver_find(SYNAPTICS_I2C_RMI_NAME, &i2c_bus_type);

if (other)
{
printk("Previous driver found: %s\n", other->name);
return -ENOMEM;
}

Этот код корректно находит предыдущий драйвер и выводит его имя. Конечно же, этого было мало, потому я добавил driver_unregister(other) и получил Kernel Panic :)
Логично предположить, что раз мы регистрируем драйвер через i2c_add_driver, то и удалять его надо через i2c_del_driver, а не напрямую. Из описания структуры i2c_driver видно, что она содержит device_driver, которая и возвращается функцией driver_find. Для преобразования из указателя на вложенную структуру к родительской есть несколько подходов, но проще всего использовать стандартную конверсию to_i2c_driver. Получился вот такой код:

struct i2c_driver * otherDriver;
struct device_driver * other;

other = driver_find(SYNAPTICS_I2C_RMI_NAME, &i2c_bus_type);

if (other)
{
otherDriver = to_i2c_driver(other);
printk(KERN_ERR "Previous driver found: %s, addr 0x%x, owner %x\n", other->name, (int)otherDriver, (int)other->owner);
i2c_del_driver(otherDriver);
}

Что характерно, он тоже приводит к Kernel Panic. На этом этапе я уже было решил, что дерегистрация невозможна, но случайно заметил в drivers/base/core.c, что после вызова driver_find обычно вызывается функция put_driver. Оказалось, что у драйверов, как обектов ядра, есть интрузивный подсчет ссылок и после driver_find счетчик увеличивается на единицу, что не дает выгрузить драйвер во время i2c_del_driver. Добавление этого вызова поставило все на свои места и встроенный драйвер стал корректно выгружаться.

Конечно, сразу все не заработало, т.к. еще существовала workqueue с таким же именем, да и встроенный драйвер оказался "нечист на руку" - не удалял sysfs файлы при выгрузке, но это уже все было решаемо.

Результатом этих "танцев с бубном" стал собственный драйвер тачскрина для Nook с поддержкой Multitouch. Подробнее о самом драйвере можно почитать по этим ссылкам:
http://nookdevs.com/Multitouch
http://www.the-ebook.org/forum/viewtopic.php?t=16728

Дальше..

вторник, 23 ноября 2010 г.

Книжкая полка для Nook

Некоторое время назад я написал для Nook простенький менеджер файлов, без возможности удаления, копирования и пр. Потом этот браузер научился фильтровать файлы по типу, а затем и извлекать описания из книг, показывать обложки и пр. Сейчас это полноценная книжкая полка с Cover Flow (листанием обложек), которая при необходимости показывает все файлы устройства, позволяет устанавливать программы, хранит список последних книг.

Вот небольше видео, как это работает:




Хочу напомнить, что программа - не полноценный файловый менеджер, у нее нет возможности что-либо удалять или копировать. В отличие от Trook и NookFileManager, используется верхний экран для отображения файлов.

Пара скриншотов:





Дополнительно программа позволяет выбрать, что именно показывать:
- все файлы (в т.ч. скрытые и системные);
- только документы (картинки, музыку, книги и пр.);
- только книги;

При выходе из программы сохраняется последняя выбранная папка, а каждый открытый документ сохраняется в виртуальную папку "Последние документы".
Заодно, программа позволяет выбрать .apk файл и установить его. Протестирована совместимость с встроеными ридерами epub/pdf/pdb файлов, а также с портом FBReader от mynook. Из файлов epub/fb2/fb2.zip извлекаются данные об авторе и названии книги, а также о ее серии. Открытые через программу книги затем корректно открываются через Reading Now. Также в программе есть список последних 9ти открытых документов и режим листания обложек (Cover Flow). Можно прятать выбраный файл, после чего он будет показываться только в режиме "Просмотр скрытых файлов". Также есть возможность просмотра изображений в форматах PNG и JPEG.

В данный момент стабильная версия программы имеет номер 1.2. Это значит, что в ней реализовано все, что задумывалось изначально, а также некоторые другие возможности вроде просмотра картинок, иконок для папок и пр. Полка работает под любыми версиями Android от 1.5 до 2.2, но в основном расчитана под экран Nook разрешением 600х944.

Программа доступна в двух версиях - как отдельное приложение и как замена B&N Library. При запуске в виде замены B&N Library пользователя спросят, какую библиотеку запустить - собственную или оригинальную. При использовании патченного framework-res.apk можно выбрать галочку "использовать эту программу постоянно".

Программа в виде замены Library

Ссылка: http://runserver.net/nook/nookFileBrowser.apk
Размер: 133593
md5: 3b4906752f89859f17de4ac55e2e385d

Отдельное приложение:

Ссылка: http://runserver.net/nook/nookFileBrowser_standalone.apk
Размер: 133573
md5: d04bfe255d19f751af3c4f7d981065aa

Дальше..

вторник, 19 октября 2010 г.

Создание патча из двух директорий

Задача сравнения двух директорий возникает более-менее часто и стандартные diff+patch под Unix позволяют с ней справиться довольно хорошо, но в моем случае надо было сделать именно архив изменений. На C++/C# программу можно было бы написать за несколько минут, но я задался целью использовать полноценное Unix решение.

Сначала я довольно долго ковырялся с diff, KDiff3 и похожими программами, но результат был очень далек от поставленной задачи, потому было решено писать .sh скрипт. Я совсем не Unix/Linux гуру, потому каждый шаг сопровождался длительным гуглением. :)

Список файлов

Для начала надо было получить списки. Вариации команды lr -R выдает нужные файлы, но в неудачном виде и их надо обрабатывать. Дойдя до вида ls -lR|tr -s ' '|cut -d ' ' -f 9, я решил, что терпение закончилось, плюнул на ls и использовал find. У этой команды недостаток в том, что к каждому файлу в добавляется относительный путь. Этого можно избежать, если сначала зайти в папку и делать find ., но тогда по непонятной причине из списка пропадают симлинки, причем использование параметра -type fl не помогает. Нормальным решением оказалось обрезание "лишнего" пути через sed.
find $2 | sed -e "s:^$2/::" > files.txt

Перебор и сравнение

Я решил перебирать все файлы из второй папки и проверять, есть ли они в первой. Довольно простой цикл с вызовом cmp:
for f in $(cat files.txt)
do
if [ -f "$2/$f" ]
then
if ! cmp -s "$1/$f" "$2/$f"; then
echo $f >> toadd.txt
fi
fi
done
Проверка [ -f "$2/$f" ] нужна, чтобы отсеять директории. Если файл не существует по пути "$1/$f", то он тоже попадает в список, что меня вполне устроило.

Архив

Полученный список toadd.txt скармливается команде tar. Чтобы пути в архиве были корректными, приходится делать дополнительный cd:
cd $2
tar -czvf ../result.tar.gz --files-from ../toadd.txt
cd ..

Эпилог

В результате потраченного часа времени, у меня теперь есть утилита, позволяющая сравнить, к примеру, две прошивки для какого-то устройства, разные версии репозитариев и пр., а на выходе получить tar.gz (или любой другой) архив с этими отличиями. Конечно, использование diff и patch дало бы гораздо меньший размер, но зато полученный результат гораздо прозрачнее и легче в использовании. Из заметных недостатков этого решения - в архив не попадут пустые директории.
Полный файл скрипта: http://runserver.net/temp/ddiff.sh

Дальше..

суббота, 16 октября 2010 г.

Рутинг Nook любой версии

Камрад cdump нашел метод записать произвольный файл на нук любой верии, в т.ч. и 1.4.1/1.4.2, которые ранее считались не рутабельными. Это значит, что реально "руссифицировать", т.е. залить свои программы на любое устройство. Детальное описание есть на сайте mynook.ru, а я же попробую описать как именно это делать в домашних условиях.

Подмена данных

Для рутинга нужно иметь WiFi сеть и доступ к интернету из нее. По адресу 217.20.163.111 я разместил собственную реализацию скриптов, которые имитируют апдейт от B&N. Достаточно сделать так, чтобы при проверке обновлений устройство обратилось к этому серверу вместо sync.barnesandnoble.com и edmfiletransfer.barnesandnoble.com.

Настройка под Windows

Под Windows мы не полезем в дебри серверных Routing Services, а воспользуемся программой TFTPD. Она может работать одновременно как DHCP и DNS сервер.

DHCP

Если у Вас есть возможность прописать сервер в настройках DHCP на роутере - эту чаcть можно пропустить и перейти к настройке DNS. Операция очень проста - запускаем TFTPD, нажимаем Settings, включаем DHCP Server и на закладке DHCP настраиваем примерно как на скриншоте. В IP Pool Starting Address пишем свободный адрес в Вашей сети, в Size of pool число больше 0, в WINS/DNS Server - адрес своего компьютера, в Default router - адрес шлюза.
Если все сделано верно, то Nook при следующем подключении к WiFi выберет IP адрес из указанного пула.

DNS

Для DNS сервера используем тот же TFTPD. Настроек в программе нет, достаточно только включить галочку DNS Server. Затем надо подправить файл hosts. Находится он обычно в папке Windows\System32\Drivers\etc\hosts. В конец файла надо дописать такие две строки:
217.20.163.111 sync.barnesandnoble.com
217.20.163.111 edmfiletransfer.barnesandnoble.com


Настройка для Linux/Unix/MacOS X

Пользователи Linux/Unix/MacOS X могут сами добавить в DNS сервер, например BIND. Если Вы не знаете, как это делается - лучше найдите компьютер с Windows. Пример записи в named.conf:
zone "barnesandnoble.com" {
type master;
file "named.bn";
};


Файл named.bn:
$TTL    86400
@ IN SOA barnesandnoble.com. root.barnesandnoble.com. (
2 ; Serial
28800 ; Refresh
14400 ; Retry
3600000 ; Expire
86400 ) ; Minimum
@ IN NS barnesandnoble.com.
@ IN A 217.20.163.111
* IN A 217.20.163.111
@ IN AAAA ::1


Процедура рутинга

Дальнейшая процедура рутинга довольно проста - надо убедить устройство проверить наличие обновлений. У меня это получилось только после регистрации устройства, через My Library. Причем, регистрацию надо выполнить с нормальным DNS сервером, а не с подменой. После этого нажатие на Check for new B&N Content привело к появлению записи Don't Blink от 29го сентября (физически эта запись считывается с моего сервера, а не с B&N) и затем началась загрузка обновления, сразу же закончившаяся ошибкой Error downloading update. Ошибка тут вполне нормальна, никаких проблем с этим нет. После выключения WiFi будет выполнен собственный скрипт, который и рутнет устройство - запустит adbd.

Что дальше?

Имея запущенный adbd, можно подключаться к устройству через adb по WiFi и делать с ним что-угодно. Если Вам это ни о чем не говорит или не хочется разбираться с adb и установкой программ вручную, есть альтернативный вариант. В скрипте кроме вызова adbd я добавил еще и выполнение некого файла root.sh. Файл должен лежать на встроенной карте рядом с папками my documents и другими. Без особых проблем можно сделать, чтобы этот файл устанавливал нужные программы, менял настройки или вообще приводил систему к виду прошивки Mynook.ru. Именно такой пакет доступен по ссылке ниже - он сделан на основе сравнения двух прошивок и записывает все недостающие файлы.
Для примера я сделал пакет, который делает следующие действия:
  • устанавливает лаунчер от Mynook.ru;
  • устанавливает библиотеку Nookdevs;
  • устанавливает NookFileManager;
  • устанавливает FBReader;
  • заменяет Home.apk пропатченой версией от Mynook;
  • заменяет Browser.apk пропатченой версией;
  • удаляет FirmwareUpdateService.apk;
  • записывает корректный wifi_stop.sh;
  • устанавливает busybox;
  • разрешает установку сторонних программ;
  • включает запуск adbd при старте;
Пакет доступен по этой ссылке: http://runserver.net/nook/mynook_package.zip
Для установки перед рутингом подключите Nook по USB и разверните содержимое архива (3 файла) в корень главного диска устройства (рядом с папками my documents и др). При рутинге после выключения WiFi подождите некоторое время и устройство само перезагрузится. После запуска у Вас будет альтернативный лаунчер, книжная полка с поддержкой FB2 и возможность устанавливать программы через NookFileManager.

Сам пакет может быть не идеальным, но такие пакеты можно делать в любом количестве и я расчитываю, что ребята из mynook.ru сделают вскоре более полную версию, с русской клавиатурой и остальными программами.

Я же на днях выложу аналогичный пакет с пропатчеными мной оригинальными программами (подробнее о моих патчах в этой статье: http://blog.runserver.net/2010/10/nook.html).

Update: пакет программ заменен. теперь это полная копия прошивки 1.5.1 от Mynook.ru

Дальше..

пятница, 15 октября 2010 г.

Не канонический взгляд на Nook

Предыстория


Мое общение с электронными книгами началось в 2006м году с LBook V8; этим ридером я пользуюсь и по сей день, не смотря на разваливающий корпус и темноватый экран. Когда же товарищ попросил совета по выбору читалки, оказалось, что аналогичные PocketBook, Sony и пр. стоят чуть ли не больше, чем было уплачено тогда, а изменился по-сути только экран. Бонусы вроде "более быстрое листание" или "поддержка формата CHM" на мой взгляд тоже того не стоят. На этом фоне B&N Nook за $149+доставка показался чуть ли не даром богов :) Погуглив несколько дней я убедился, что устройство действительно уникально, а если и есть нюансы, то прошивка от Nookdevs или русская от Mynook.ru позволит их рано или поздно решить.
Устройство с EBay доставили за неделю, а вот с перепрошивкой не сложилось - именно эта серия устройств пока что не рутабельна. Зато оказалось, что заводская прошивка нормально браузит русские сайты и читает EPUB книги с встроенными шрифтами. О дополнительных программах можно забыть, но именно читать книги можно без проблем. В целом уствойство очень напомнило продукцию Apple - белое исполнение, удачный дизайн корпуса и п/о, минимум доступных настроек.



Второй ридер я уже брал для себя, с гарантированной возможностью перепрошивки - захотелось изучить Андроид изнутри, попробовать написать пару программ, может даже разобраться с взломом непрошиваемых устройств. За это пришлось переплатить, но в результате я стал обладателем перепрошиваемого Nook с 3G, потратив почти в полтора раза меньше, чем на "отечественные" аналоги. Тут и закончились первоначальные восторги и начались суровые будни. :)

Альтернативные прошивки


Для начала я залил прошивку 2.4.1 от Nookdevs: http://nookdevs.com/Softroot
Приятный интерфейс домашнего экрана сменился на самописный лаунчер, появилась альтернативная книжная полка и браузер. Рискую начать холивар, но все эти программы показались гораздо ниже качеством, чем оригинальные.. Работать стало откровенно неприятно, полезли баги. Возможность ставить дополнительные программы хороша, но адаптированных под Nook их отковенно мало, а качество тоже не блещет.

Вторым заходом была залита прошивка 1.5.1 от MyNook.ru: http://mynook.ru/firmware/
На устройстве появился еще один собственный лаунчер, FBReader и часть программ от nookdevs. Общее впечатление стало получше, но все-равно привкус кустарщины остался.. Русской экранной клавиатурой пользовался один раз при написании письма, а FB2 файлы по скорости и качеству читаются точно так же, как и EPUB, но навигация чуть менее удобна.

По-честному, на этом этапе надо было либо вернуться к оригинакльной прошивке, или остаться на русской, но.. как говорится, у меня "був час та натхнення". :)

Собственная прошивка


На устройстве оказался классический набор - Linux 2.6.27 + Android 1.5 Cupcake, предположительно Revision 3. Сама процедура прошивки реализована довольно добротно, с RSA подписью от SHA-256 хеша тела апдейта и только баг в самых первых прошивках позволяет рутнуть устройство через даунгрейд и без вскрытия. Поискав для приличия другие варианты взлома, я убедился, что основные дыры закрыты достаточно хорошо, а заодно получил некоторые навыки декомпиляции и повторной сборки Android программ.
Для собственных нужд была сделана собственная копия официальной прошивки 1.4.0 с одним только изменением - включен автозапуск adbd в /init.rc. После перепрошивки мы получаем 1 в 1 оригинальное устройство, но с доступом через adb, а значит и с возможностью устанавливать/удалять программы, ковыряться в системных настройках и пр.

Программы


Следующим шагом был подбор программ, а с учетом моей привередливости, еще и допиливание тех, которым не хватало "перламутровых пуговиц". Ссылки на упомянутые программы есть в конце этой статьи: Ссылки

Лаунчер


Через adb можно ставить программы, но запускать их проблематично - это нужно делать либо через adb, либо через самописные лаунчеры, от которых и я бежал. :)
Решение пришло в виде программы NookAppManager, которая не отягощена лишним интерфейсом и может запустить или удалить любую из установленных программ. Осталось решить, как запускать именно ее саму. На данный момент я не придумал ничего лучше, как перенаправить одну из дефолтных иконок в оригинальном лаунчере. Немного ковыряния в коде, backsmali + smali, и иконка шахмат стала моим бекдором для запуска чего-угодно и как-угодно. Решение не элегантно, но зато в повседневной жизни можно пользоваться стандартным интерфейсом и программами, а если нужен сторонний софт или шахматы - запускать AppManager и уже из него что-то еще.
На данный момент написана версия домашнего экрана, которая вместо папки Games делает папку Apps и показывает там все установленые приложения.

Браузер


К браузеру на ридере требования всего два: он должен браузить и качать файлы. Оригинальный браузер из прошивки 1.4.0 довольно хорош, но файлы не качает. Продукт от Nookdevs файлы качает, но заметно менее удобен и давно не обновлялся. А по-честному, он удовлетворяет только второму требованию. :) Заглянув в код оригинального браузера, я с удивлением обнаружил некие наработки, а главное UI для скачивания файлов. Несколько дней ушло на то, чтобы разобраться в том, как оно должно и могло бы работать, а затем на то, чтобы реализовать недостающие части, но в результате мы получили то, что хотели -удобный браузер с поддержкой скачивания.

Ридер RSS


На форумах для чтения фидов на устройстве рекомендуют использовать либо веб-аггрегаторы, либо адаптированный Trook. Последняя программа довольно неплоха, но меня не впечатлила, да и аггрегаторы как-то не пришлись по душе. В стандартный набор входит программа The Daily, которая в идеале должна читать новости с сайта Barnes & Noble, но на самом деле читает несколько малополезных фидов и русскоязычными пользователями не используется вообще. Первая версия моей модификации использовала оригинальный формат фидов и php транслятор на внешнем сервере, но затем у меня получилось полностью переписать загрузку фидов и теперь The Daily стал полноценным RSS ридером, а список фидов читается из файла feeds.xml на встроенной карте. У него есть заметные недостатки, например фиды читаются целиком при первом запуске, что занимает некоторое время, да и Atom не поддерживается, но для большей части фидов он отлично подходит.

Утилиты и сторонние программы


Кроме уже упомянутого Nook App Manager, довольно полезными оказались Nook Task Manager, NookTerm и Nook WiFi Locker. Теперь после перепрошивки устройства (бывает необходимо после экспериментов) я запускаю простенький .bat файл, который устанавливает эти утилиты, заменяет модифицированные мной программы и применяет некоторые оптимизации.
Что интересно, на читалку можно поставить почти любую Android программу, но для управления ей надо будет запустить VNC сервер или переназначить клавиши управления. Это дает надежду, что рано или поздно мы найдем метод управлять такими программами без VNC, а для самых полезных будут сделаны собственные порты под это устройство. К примеру, большие надежды возлагаются на CoolReader, автор которого ознакомился с реалиями нашего устройства и планирует его поддержку в скором времени.

Оптимизация


Дабы заряд тратился не так быстро, довольно полезно бывает убрать неиспользуемые сервисы. Сейчас сообщество на форуме The Ebook активно изучает, что можно безболезненно отключить, дабы продлить срок жизни ридера, но гарантированно безопасно только отключение FirmwareUpdateService. Деинсталировать его не получится, потому кардинальное решение - удалить:
adb shell rm /system/app/FirmwareUpdateService.apk

Также можно удалить SyncService.apk и ContactsProvider.apk, но тут уже могут быть нюансы вроде нерабочести покупки в официальном магазине B&N и пр.
Другое направление оптимизации - применение патчей для самого Android. На данный момент я пропатчил LocationManager и KeyInputQueue в файле services.jar, но еще нет статистики, насколько это помогло и помогло ли вообще.

Эпилог


На данный момент устройство выглядит привлекательно по цене и дизайну, но очень страдает от потребления батареи различными программами, в т.ч. использующими WiFi и 3G. Сообщество разработчиков как таковое почти отсутствет, официальное API не выпущено, да еще и портит картину наличие не-рутабельных устройств. С другой стороны, рано или поздно такие устройства будут таки взломаны, а увеличение количества разработчиков позволит оптимизировать п/о и юзабельность.


Ссылки


Прошивка 1.4.0 с разблокированным adbd: http://runserver.net/nook/bravo_update.dat
Прошивка 1.4.1 с разблокированным adbd: http://runserver.net/nook/bravo_update_1.4.1.7z
Домашний экран с отображением установленых программ: http://runserver.net/nook/Home.apk
Браузер с поддержкой скачивания: http://blog.runserver.net/2010/09/nook-2.html
The Daily с поддержкой чтения RSS: http://blog.runserver.net/2010/09/nook-3.html
Патченый services.jar: http://runserver.net/nook/services.7z

Update: Добавил ссылку на новую модификацию Home.apk с папкой Apps вместо Games
Update 2: Добавил ссылку на прошивку 1.4.1. Подходит для старых устройств.

Дальше..

вторник, 12 октября 2010 г.

Информация о системе в .Net

Этот пост скорее просто памятка .Net программистам о том, как получать некоторые важные свойства системы - версию Runtime, 64 или 32-битная система, сколько используется процессоров и пр. Все эти проверки можно найти в интернете или копанием в классах вроде System.Environment, но лично мне удобно держать все в одном вспомогательном классе.

Класс целиком из ядра RunServer:

/// <summary>
/// System information class
/// </summary>
public static class SystemInfo
{
/// <summary>
/// Is there Mono Runtime available?
/// </summary>
public static readonly bool IsMono =
Type.GetType("Mono.Runtime") != null;

/// <summary>
/// Are we on x86 architecture?
/// </summary>
public static readonly bool IsX86 = IntPtr.Size == 4;

/// <summary>
/// Are we on x64 architecture?
/// </summary>
public static readonly bool IsX64 = IntPtr.Size == 8;

/// <summary>
/// Are we using Server Garbage Collector?
/// </summary>
public static readonly bool IsServerGC = GCSettings.IsServerGC;

/// <summary>
/// Is there console windows attached or we are running in
/// service/GUI app?
/// </summary>
public static readonly bool IsConsole =
Console.OpenStandardOutput() != Stream.Null;

/// <summary>
/// Assembly was compiled in Debug mode?
/// </summary>
public static readonly bool IsDebug =
#if DEBUG
true;
#else
false;
#endif

/// <summary>
/// Runtime version
/// </summary>
public static readonly Version RuntimeVersion =
Environment.Version;

/// <summary>
/// Operating System version
/// </summary>
public static readonly Version OSVersion =
Environment.OSVersion.Version;

/// <summary>
/// CPU count
/// </summary>
public static readonly int ProcessorCount =
Environment.ProcessorCount;

/// <summary>
/// Dumps system information the specified stream.
/// </summary>
/// <param name="stream">Output stream</param>
public static void Dump(StreamWriter stream)
{
stream.WriteLine("Is Mono: {0}", IsMono);
stream.WriteLine("Is x86: {0}", IsX86);
stream.WriteLine("Is x64: {0}", IsX64);
stream.WriteLine("Is ServerGC: {0}", IsServerGC);
stream.WriteLine("Is Console: {0}", IsConsole);
stream.WriteLine("Is Debug: {0}", IsDebug);
stream.WriteLine("Runtime Version: {0}", RuntimeVersion);
stream.WriteLine("OS Version: {0}", OSVersion);
stream.WriteLine("CPU Count: {0}", ProcessorCount);
}
}

Дальше..

пятница, 8 октября 2010 г.

Tips for Geeks. Часть 2

Оптимизация строковых операций - чуть ли не первое, что делает среднестатистический гик-оптимизатор. Эти оптимизации и собственные классы идут по популярности сразу после собственных коллекций. Впрочем, и программисты со стажем зачастую не прочь побаловаться со стрингами :)

В этой статье я рассмотрю довольно распостраненный случай - множественную конкатенацию константных строк в С++.


Представим, что нам надо составлять длинную строку из некого количества маленьких константных строк. К примеру, это могут быть опции, дефайны, запросы и пр.

Для примера пусть это будет выглядеть так:
void initString(int i, string & str)
{
str.clear();
str += "SOME_TEST_DATA";
if (i % 2)
str += ",DATA_MOD_02";
if (i % 3)
str += ",DATA_MOD_03";
if (i % 5)
str += ",DATA_MOD_05";
if (i % 7)
str += ",DATA_MOD_07";
if (i % 11)
str += ",DATA_MOD_11";
if (i % 13)
str += ",DATA_MOD_13";
if (i % 15)
str += ",MOD_15_EXTRA_DATA";
if (i % 16)
str += ",MOD_16_EXTRA_DATA";
if (i % 21)
str += ",MOD_21_EXTRA_DATA";
if (i % 24)
str += ",MOD_24_EXTRA_DATA";
if (i % 25)
str += ",MOD_25_EXTRA_DATA";
}

Операцию эту выполним в цикле, увеличивая i от 0 до 1000000:

string paramString;
paramString.reserve(1000);
int result = 0;
for(int i=0; i<1000000;i++)
{
initString(i, paramString);
result += paramString.size();
}
Сюда же добавим измерения скорости выполнения. Сразу предупрежу, что результаты заметно зависят от компилятора, процессора и системы в целом. Интересны они лишь для сравнения разных подходов.
С использованием std::string на Athlon 4200 и MS VC++ 2008 мы получаем ~600мс времени выполнения. Много это или мало? Как по мне, для миллиона итераций этого вполне достаточно. Но серия статей не зря называется Tips for Geeks :)

Что происходит, когда к переменной str добавляется "SOME_TEST_DATA"? Если места в буфере достаточно, то произойдет всего три действия: измерение длины добавляемой строки, копирование данных и смещение индекса в переменной str. Мы не сможем оптимизировать копирование и изменение индекса, но так ли нужно в этом случае измерение длин строк (т.е. перебор всех их символов в поисках '\0')? Ведь длину константной строки компилятор знает еще на этапе компиляции.

Для экспериментов сделаем собственный класс custom_string:

class custom_string
{
char * m_data;
int m_allocated;
int m_size;
public:

void reserve(int size)
{
...
}

custom_string & operator += (const char * str)
{
int size = strlen(str);
reserve(size);
memcpy(m_data + m_size, str, size + 1);
m_size += size;
return *this;
}
};

Метод reserve проверяет, достаточно ли места в буфере и выделяет новый при необходимости. Код этого метода, как и конструктор с деструктором я опустил, чтобы они не отвлекали от основной задачи.

Наш тест с таким классом выполняется за те же самые ~600мс, как и с std::string. Есть как минимум два метода "подстегнуть" его работу.
Первый метод очень прост, но может отказаться работать при каких-либо дополнительных условиях, параметрах компиляции и пр. Он состоит в том, чтобы сделать код оператора более привлекательным для инлайнинга. Сделаем так:

void append(const char * str, int size)
{
reserve(size);
memcpy(m_data + m_size, str, size + 1);
m_size += size;
}

custom_string & operator += (const char * str)
{
append(str, strlen(str));
return *this;
}

Не смотря на то, что визуально код почти не изменился, время выполнения его на моем компьютере теперь составляет ~100мс. Я не смотрел в генерируемый ассемблерный код и не изучал причины, но первое, что приходит в голову - оператор += инлайнится, его вызовы заменяются на вызовы append, и расчет длины строки происходит именно на этапе компиляции. Так это или нет, не суть важно, потому что стабильность такой реализации под вопросом - оптимизация как появилась, так может и пропасть для Unicode строк, при сборке под другую ОС и пр., вне зависимости от бубнов __forceinline и нашего желания.

Более универсальный метод основывается на том, что константная строка в С++ может быть представлена в виде массива символов фиксированной длины. Такая запись вполне валидна:

const char something [] = "something";

Для этого трюка мы используем темплейты:

template<int N>
custom_string & operator += (const char (&str)[N])
{
reserve(N - 1);
memcpy(m_data + m_size, str, N);
m_size += N - 1;
return *this;
}

В переменную N компилятор положит длину массива символов, включая и trailing zero.
Тест с таким методом выполняется на моем компьютере примерно за 140мс, что лежит в пределах статистической погрешности от предыдущего метода, но гарантированно будет работать, т.к. соответствует стандарту C++. Единственный недостаток в том, что для каждой длины строки будет создана новая копия метода конкатенации.

Можно ли таким образом "улучшить" std::string? Мои эксперименты показали, что прирост получается не существенный, порядка 50мс на исходном тесте. Такой трюк лучше использовать в собственных строковых классах, а по-честному лучше не использовать вообще (как и самописные классы для примитивов). Не стоит забывать, что сложение миллионов строк - экстраординарная ситуация, которая если и возникает, то редко бывает настолько критичной по времени. Использование же собственных строк, а тем более методов с необычными конструкциями заметно понижает читабельность кода, а значит и возможность его поддержки как автором, так и сторонними специалистами.


Дальше..

среда, 6 октября 2010 г.

Треугольники и память

В эру гига- и терабайтов как-то стало не принято заботиться о том, сколько памяти потребляют данные в памяти. Работы по оптимизации зачастую намного дороже дополнительной ОЗУ и программисты чувствуют себя вольготно, пока не происходит что-то экстраординарное, вроде достижения лимита памяти на процесс на x86 ОС. В такие моменты мы хватаемся за голову (или профайлер) и смотрим, что же поглотило заветные мегабайты.
Эта история о подобном случае, когда хранение 3D моделей в памяти стало неоправданно дорогим из-за избыточности самого распространенного примитива - треугольника.

Для удобства чтения я буду писать код на C#, хотя все описанное можно повторить и в C++, и в Delphi и пр., за исключением последнего абзаца.

Все началось с того, что модели надо не только хранить, но и обрабатывать, потому к каждому треугольнику добавились данные вроде вектора нормали, граничных точек, коэффициенты и пр.:

struct Triangle
{
public Vector A;
public Vector B;
public Vector C;
public bool TwoSided;

float ND;
float Reci;
Vector Normal;
byte K;
byte U;
byte V;
Vector LowerBound;
Vector UpperBound;
}

Значения K, U, V - номера сторон треугольника, Normal - вектор нормали, LowerBound и UpperBound - границы описанного параллелепипеда, а остальные коэффициенты нужны для просчета коллизий. Vector содержит три переменные типа float, т.е. занимает 12 байт, а вся указанная структура занимает 88 байт*. При наличии 10 миллионов треугольников (а это вполне возможно, если мы храним не одну сцену, а весь игровой мир) они все занимают 880+ мегабайт ОЗУ. Если это слишком много, то самым правильным и простым решением будет переход на 64-битную архитектуру, в которой это значение - лишь капля в море. Но если же нам дорог каждый мегабайт, то можно заняться неблагодарным делом оптимизации :)

Первое, что сразу приходит в голову - использовать внешний массив с вершинами - треугольники в модели почти всегда имеют 2-3 общие вершины с соседями. Мы испольузуем три индекса и одну ссылку на массив вершин. Получится всего 16 байт вместо 36 на вершины, но надо не забывать, что сами вершины от этого не исчезнут и должны хранится в неких внешних массивах. При соотношении вершин и треугольников 1 к 3 выходит, что у нас есть отдельно 40 мегабайт вершин, а треугольники занимают теперь 680 мегабайт.
Затем мы можем убрать вектор нормали и вычислять его при необходимости. Также известно, что значения U и V могут быть вычислены из значения K. Таким образом, структура будет занимать уже 56 байт, значит мы уменьшили потребление с 880 до 600 мегабайт, т.е. примерно на 32%, что уже очень неплохо. В общих случаях на такой структуре можно было бы и остановиться:

struct Triangle
{
public int A;
public int B;
public int C;
public Vector [] VertexBuffer;
public bool TwoSided;

float ND;
float Reci;
byte K;
Vector LowerBound;
Vector UpperBound;
}

Следующие шаги уже довольно нетривиальны. У нас есть Bounding Box треугольника, заданный двумя точками - LowerBound и UpperBound. Координаты этих точек составляются из шести координат вершин (наибольшее и наименьше значение X, Y и Z), потому мы можем сказать, что для описания этого бокса достаточно хранить 6 индексов:

int lbx = MinIndex(A.X, B.X, C.X);
int lby = MinIndex(A.Y, B.Y, C.Y);
int lbz = MinIndex(A.Z, B.Z, C.Z);
int ubx = MaxIndex(A.X, B.X, C.X);
int uby = MaxIndex(A.Y, B.Y, C.Y);
int ubz = MaxIndex(A.Z, B.Z, C.Z);

int MaxIndex(float a, float b, float c)
{
return a > b ? a > c ? 0 : 2 : b > c ? : 1 : 2;
}

int MinIndex(float a, float b, float c)
{
return a < b ? a < c ? 0 : 2 : b < c ? : 1 : 2;
}

Эти индексы принимают значения от 0 до 2, т.е. занимают по 2 бита каждый и могут быть упакованы в два байта данных:

LowerBoundPack = (byte) ((lbx) | (lby << 2) | (lbz << 4));
UpperBoundPack = (byte) ((ubx) | (uby << 2) | (ubz << 4));

Назад в векторы эта данные превращаются довольно просто:

Vector[] abc = {A, B, C};

byte lbx = (byte) (m_lowerBoundPack & 0x3);
byte lby = (byte) ((m_lowerBoundPack >> 2) & 0x3);
byte lbz = (byte) ((m_lowerBoundPack >> 4) & 0x3);

byte ubx = (byte) (m_upperBoundPack & 0x3);
byte uby = (byte) ((m_upperBoundPack >> 2) & 0x3);
byte ubz = (byte) ((m_upperBoundPack >> 4) & 0x3);

Vector lb = new Vector(abc[lbx].X, abc[lby].Y, abc[lbz].Z);
Vector ub = new Vector(abc[ubx].X, abc[uby].Y, abc[ubz].Z);


Но при этом индексы занимают по 6 бит из 8ми, потому мы можем спокойно записать флаг TwoSided и значение K в неиспользованные биты:

LowerBoundPack |= (byte)(K << 6);
if (TwoSided)
UpperBoundPack |= 0x40;

Таким образом структуру мы сократили до 28 байт:

struct Triangle
{
public int A;
public int B;
public int C;
public Vector [] VertexBuffer;

float ND;
float Reci;
byte LowerBoundPack;
byte UpperBoundPack;
}

В памяти наши треугольники будут занимать 280 мегабайт + 40 мегабайт вершин, т.е. 320 мегабайт, что на 63% меньше, чем изначальный вариант.
На этом можно было бы и остановиться, но есть еще один интересный нюанс. По статистике, модели с количеством вершин более 65536 составляют примерно 2% от общего числа, а около 95% моделей содержит от 256 до 65535 с вершин. Выходит, что использовать 32-битные индексы откровенно расточительно. Также нет особой необходимости хранить ссылку на буфер с вершинами для каждого треугольника - это свойство самой модели и должно храниться в ней.

На помощь придут темплейты в C++ или дженерики в C# **:

struct Triangle<T> where T : struct, IConvertible
{
public T A;
public T B;
public T C;

byte LowerBoundPack;
byte UpperBoundPack;
float ND;
float Reci;
}

Такая структура потребует некоторых нюансов в обработке, а также придется использовать ссылку на VertexBuffer из объекта модели. В случае T = short размер структуры будет 16 байт, а для T = int - 24 байта. Таким образом, в памяти наши треугольники с вершинами займут ~200 мегабайт, что в 4.4 раза меньше, чем в изначальной задаче, т.е. оптимизация составила 77% использования памяти.

Стоило ли это того? На первый взгляд стоило, но код обработки таких треугольников стал выполнятся немного медленее и заметно потерял в читаемости. Вывод прост - надо знать меру во всем, даже в оптимизациях. :)



* Все размеры структур в памяти даны с учетом выравнивания и указанного порядка переменных. К примеру, "ручной" подсчет в первой структуре дает 83 байта, т.е. простая перестановка позволила бы получить размер структуры 84 байта
** Для параметра T нужно требование IConvertible, чтобы можно было привести переменные типа T к int с помощью подобной конструкции:
int a = Convert.ToInt32(A);

Дальше..