StbSharp: история ненужного проекта

74a42a3020ed58868b10800ddcf8710b

Введение.

В этой статье я бы хотел рассказать о своем хобби проекте под названием StbSharp.

Итак, в 2016 году мне пришла в голову весьма банальная идея — сделать собственный игровой кросс-платформенный движок на C#. И я озаботился поиском кросс-платформенной же библиотеки для загрузки картинок. Внезапно выяснилось, что подходящей просто не существовало. Было множество платформо-зависимых решений (напр. System.Drawing). А так же имелась SixLabors.ImageSharp. Но она была в состоянии ранней альфы. Мне же хотелось работать с решением, проверенным временем. Так я пришёл к идее портировать stb_image.h (очень популярной в геймдеве single-header библиотеки для загрузки картинок) на C#.

»А разве не легче было написать биндинги для нативной библиотеки? Хоть для той же stb_image? »,- задаст справедливый вопрос читатель. Да, легче. И правильнее. О чём, собственно, и говорит заголовок этой статьи. Конечно, использование биндингов доставляет некоторые неудобства в плане того, что необходимо доставить соответствующий нативный бинарник на устройство конечного пользователя. Однако эти неудобства с лихвой окупаются достоинствами. А именно лучшим перформансом и портируемостью.

Однако, проект показался мне столь интересным, что я проигнорировал эти справедливые возражения.

Как шло портирование

Перво-наперво я героически переименовал stb_image.h в StbImage.cs и попробовал внести нужные правки в студии с помощью замены. Разумеется, эта затея с треском провалилась.

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

Наконец я придумал рабочее решение, которые превратилось в отдельный проект под названием Sichem. Его идея заключалась в том, чтобы построить синтаксическое дерево исходного файла с помощью libclang, а затем обойти построенное дерево и сгенерировать C# код.

Sichem был написан на C# и использовал ClangSharp (биндинги для libclang). Причём мне пришлось сделать для него небольшое расширение под названием SealangSharp, поскольку ClangSharp того времени не умел делать ряд вещей, вроде определения типа оператора.

Таким образом, базовая версия Sichem заняла один-два месяца.  Наконец он стал более ни менее рабочим и даже умудрился переварить stb_image.h. Сгенерированный C# код был ужасен и содержал бесчисленное количество синтаксических ошибок. Однако их — в большинстве случаев — можно было поправить с помощью обычного string.Replace. Более сложные ошибки приходилось править ручками. 

К примеру, много боли принесло портирование указателей на функции. В C# они становились делегатами. Из-за чего структуры их содержащие становились managed. А, значит, с ними уже не работала арифметика указателей (которая в C# поддерживается в unsafe режиме). И соответствующий код приходилось переписывать.

Однако со временем ошибки были исправлены и StbImageSharp наконец стал компилироваться. Работать, впрочем, он не стал. Первые попытки загрузить картинку генерировали самые разные роды исключений. Впрочем, исправить их было несложно. Но даже после этих правок картинки загружаться не хотели. StbImageSharp давал на выход нечто соответствующее оригинальной картинке лишь в размерах.

Передо мною встала очередная задача — исправить несоответствия между работой оригинального stb_image.h и StbImageSharp. Я запускал одновременно две студии. В одной дебажил загрузку картинки через stb_image.h, а в другой — через StbImageSharp. Шаг за шагом я находил расхождения и исправлял. Наконец картинка успешно загрузилась.

Примерно тогда же мне в голову пришла идея автоматического тестера. Которая заключалась в том, чтобы пробежаться по всем картинкам в заданной директории, загрузить каждую через stb_image.h и StbImageSharp. А затем убедиться, что результаты совпадают с точностью до байта.

Тестер был написан и натравлен на коллекцию из примерно 800 картинок. Ещё немалое времени ушло на исправление вновь открывшихся ошибок. И после этого базовая версия StbImageSharp была готова.

Внедрение в MonoGame

После этого я преступил к пиару. А именно создал тему с кратким описанием проекта на форуме MonoGame.

Для тех кто не в курсе, MonoGame — это open-source реализация XNA.Т. е. игровой фреймворк на C#. Он поддерживает множество игровых платформ. Для каждой платформы у него своя сборка, которая использовала платформенное-зависимый способ загрузки картинок.

К примеру, MonoGame.Framework.DesktopGL загружал картинки через System.Drawing. MonoGame.Framework.WindowsDX — через DirectX. И т.д.

Разработчики MonoGame давно уже обсуждали возможность перехода на платформо-независимое решение. Они не хотели переходить на SixLabors.ImageSharp, поскольку он был слишком избыточным для их нужд. Кроме того они не хотели добавлять в проект дополнительную зависимость. StbImageSharp же был хорош тем, что позволял включить себя в виде исходного кода. Поэтому они заинтересовались проектом. Меня спросили о том, насколько производительным он получился.

Поэтому я доработал автоматический тестер так, чтобы он ещё и измерял производительность. И наконец узнал ответ на самый популярный вопрос, связанный с StbImageSharpом. А именно, что StbImageSharp работает примерно на 25% медленнее, чем stb_image.h.

Производительность оказалась приемлемой, поэтому меня попросили сделать PR с заменой загрузки картинок через System.Drawing на StbImageSharp для сборки MonoGame.Framework.DesktopGL. Планировалось для начала внедрить StbImageSharp только в эту сборку. И если она покажет себя хорошо, то в дальнейшем внедрить и в остальные открытые сборки (у MonoGame есть ещё и ряд закрытых сборок для консолей).

Таким образом, MonoGame 3.7 вышла с StbImageSharp для сборки DesktopGL. Ей начали пользоваться множество людей для реальных проектов. Что вскрыло новые баги. Например, у StbImageSharp возникали проблемы при асинхронной загрузке картинок. Баги оперативно правились. И, в целом, проект достойно показал себя. Поэтому в MonoGame 3.8 он был внедрён во все открытые сборки. И остаётся там по сей день.

Использование в Unity3D

Как известно у великого и ужасного Unity3D функция Texture2d.LoadImage обладает целым рядом проблем:

  1. Она позволяет загружать картинки только в форматах Jpg и Png.

  2. Она даёт на выход текстуру, а не картинку в обычной памяти. Что делает сложным внесение в неё изменений (нужно вначале выгрузить текстуру в обычную память, внести изменения, а потом загрузить результат назад в видеопамять).

  3. Её можно запускать только в основном треде.

Поэтому не удивительно, что некоторые разработчики искали альтернативные способы загрузки картинок в ран-тайме и находили StbImageSharp.

По крайней мере, именно так поступил проект TriLib.

А другой разработчик, посмотрев на TriLib, создал целую библиотеку, внедряющую StbImageSharp в Unity3D: https://github.com/mochi-neko/StbImageSharpForUnity

Причём он написал статью, где подробно обосновал своё решение: https://synamon.hatenablog.com/entry/unity_image_loading

Статья на японском, но через переводчик можно понять о чём идёт речь.

Остальные порты

Из вышенаписанного может сложиться, что на C# была портирована только stb_image.h. На самом деле, библиотек stb было портировано куда больше (полный список можно посмотреть, если проследовать ссылке в начале статьи).

Наибольшее внимание уделялось и уделяется StbImageSharp, StbImageWriteSharp и StbTrueTypeSharp. Остальным внимание уделяется постольку-поскольку.

Следует отметить, что StbImageWriteSharp так же был внедрен в MonoGame. А благодаря StbTrueTypeSharp на свет появился другой весьма популярный в среде MonoGame/FNA проект под названием FontStashSharp.

На базе StbImageSharp была создана его safe версия — SafeStbImageSharp. Который использовал специальный класс FakePtr для симуляции арифметики указателей.

А SafeStbImageSharp — в свою очередь — породил StbImageJava, проект настолько непопулярный, что порой даже я забываю про его существование.

Впрочем, его непопулярность объясняется тем, что Java из коробки умеет загружать картинки. Хотя, вроде бы, её Image API не работает на андроиде.

Кроме того, я — будучи 100% C# программистом — так и не разобрался с тем как правильно работать с Javaским аналогом nuget.org и куда нужно закачивать пакеты. Поэтому на данный момент проект доступен только в виде исходного кода.

Так же, StbImageSharp породил StbImageBeef — порт stb_image.h на язык Beef. У проекта появился как минимум один активный пользователь, который запилил уже 3 PR. Я думал портануть на этот язык и несколько других библиотек stb. Однако выяснилось, что подобный проект уже существует. Впрочем, он выглядит не слишком активным. Поэтому возможно я вернусь к своей затее.

Наконец, последние порты были сделаны на язык Rust: https://github.com/StbRust

Стоит отметить, что они так же являются ненужными. Поскольку у раста давно есть как библиотека работы с картинками https://docs.rs/image/latest/image/, так и с загрузкой фонтов https://docs.rs/truetype/latest/truetype/

Однако, я их сделал чтобы проверить возможности своей новой утилиты по портированию С кода под названием Hebron. Которая является наследником Sichem. Но в отличии от него, умеет портировать не только на C#.

Эпилог

Данная статья поведала об истории StbSharp. Проекте, которых не хватал звёзд с неба, но всё же нашёл свою скромную нишу.

© Habrahabr.ru