Приветствую, Хабравчане!

Новость о проекте на OpenNet.ru

Синопсис

Недавно вышла библиотека SDL3. Добавили много разных фич, но опять дропнули 100500 "устаревших систем", что они там себе позволяют:). Мне очень нравится данная библиотека, она достаточно простая и удобная. Но вот дропы старых систем очень огорчают. Конечно я понимаю, что какого либо практического смысла в их поддержке нет и большинство пользователей вообще этого не заметили. Все используют современные версии Window и Linux и о боже Macos.

Сначала решил попробовать портировать SDL3 под Windows 98. Два дня я крутил сишные сорцы библиотеки. Что то менял, смотрел, но в итоге это оказалось для меня сверх трудозатратным. Тогда я решил пойти другим путём, а что если начать реализацию SDL3 с нуля, опыт работы с графикой есть. Да и не сказать, что новые фичи SDL3 прям уж такие новые и не реализуемые на старых платформах. Я отдаю себе отчёт в том, что в SDL3 просто тонны кода. Поэтому хочу реализовать минимально достаточный функционал.

Основной функционал библиотеки это абстрагирование от окна, событий ос. Простой 2D рендер, работа с файловой системой, файлами, звуком.

То, что реально может затруднить реализацию это работа с камерами, HDPI и новый универсальный 3D API основанный на принципах Vulkan и DirectX12, с очередями команд и портянками кода для вывода треугольника:) Из-за сложности их реализации, для начала решил реализовать простые функции, создание окна и обработку событий, 2D рендер и поддержку OpenGL и Vulkan. Далее по мере готовности, работу с файловой системой, файлами и т.д

Проект назвал SDL3Lite. Рад буду помощи. Проект открытый, лицензия boost software.

Саму библиотеку пишу на С++ 98, возможно имело бы смысл так же юзать С и прямо копировать код в проект и дорабатывая его. Лично для меня программировать на С, прям таки тоскливо. А С++ даже 98, дает большинство фич для удобного кодинга. Поэтому внутри библиотеки я использую ООП, конструкторы и деструкторы, а наружу торчит совместимый SDL3 C API.

Производительность

Я нацелен поддерживать SDL3Lite не только для старые системы типа Windows 95, но так же и на новых системах Windows и Linux. И как раз разработка под старые системы с тем самым слабым железом позволяет с самого начала строить архитектору так, что бы оно могло выполняться к примеру на 386sx 33 mhz с поддержкой софт рендера.

Запускал примеры, на железе 386sx 33 mhz и 8мб озу. Оно даже шевелится:)

Не в огород C# и Net платформы. Но, что нужно что бы написать пинг понг на net? Дистр последней версии, С# + Net на пол гига. Потом фреймворк по типу авалония и в итоге оно все будет работать только на новом железе. Я ни как не хочу задеть чувства С# программистов, так как сам им являюсь. Вы можете взять как пример С++ и qt, Java и spring. Идея в том, что для того что бы сделать что то простое, нужна куча пакетов, поддерживаемые версии linux, определенный компилятор и куча всего остального чего я не знаю, что бы вывести две полоски и кружочек. Больше ресурсов занимает не вывод линий, а весь обслуживающий код.

И ещё я не в восторге, от современных подходов к программированию, когда для вывода кнопки требуется Directx 12 и оно ещё может тормозить и жрать ОЗУ как не в себя. Меня довольно сильно огорчает вообще ситуация в софте и айти, поэтому по возможности стараюсь внести свой вклад.

Итоговая цель

Создать совместимую с оригинальной SDL3 библиотеку, но с урезанным функционалом. А именно в части поддержки абстрактного SDL3 GPU API. Для меня реализация сложно и я не ставлю её в приоритет. Сейчас добавляю работу с 2D графикой, звуком, файлами и абстракцией над ОС. А так же обеспечение совместимости с заголовками SDL3, что позволит без переделки библиотек расширений, собрать тот же SDL3_ttf, SDL3_image, SDL3_Mixer и т.д

К примеру многие 2D игры не используют эти ваши RTX им не нужна производительность i9 процессоров, и единожды написанные на SDL3, могут быть с минимальными усилиями портированы под старые системы Linux, Windows, старые консоли, микроконтроллеры (они бывают довольно жирные)

Конечно сейчас проект довольно сырой. Но как пример, почему я это делаю, это прошлый мой проект, по похожей библиотеке LDL.Много сил на него потратил, но он вообще не взлетел и оказался не особо полезным. Но есть и положительный момент, немного прокачал скилы по графике и частями тяну код из него, с переделкой под реалии SDL3.

В планах есть портирование под MS-DOS и Windows 3.1 c этим поможет OpenWatcom. Раз я решился, поддерживать старое железо, нужно использовать все возможности.

Архитектура

Наружу торчит совместимый С API.

В основном библиотека зависит от stb_image для загрузки изображений и header only библиотека для инициализации OpenGL вплоть по 4.6, не стал использовать GLAD и другие аналоги, просто написал свой простой вариант. Кстати его можно использовать и вне контекста проекта. Вдруг кому пригодится.

Код пишу на С++ 98. Это довольно старый формат, но его удобства вполне хватает для многих вещей. И это единственный способ без боли, поддержать старые системы. Использую STL, но не исключения. Так как не на всех системах есть поддержка исключений. А вот шаблоны есть, некая золотая середина.

На, что способно старое железо.

Ранее я упомянул библиотеку LDL. Так вот какие тесты я получил на эмуляторе 86box: pentium 166 mhz и видеокарта Voodoo Banshe, вышедшая в 1998 году. Конечно это эмулятор, но он умеет довольно честно эмулировать именно ту производительность железа.

Сама карта, 1 текстура, вывод через OpenGL 1.2 Вывод батчами, когда сначала формируется геометрия и одним вызовом уходит на видеокарту. Тестировал на Windows 98 SE.

Карта формируется из атласа

Карта 100x100 тайлов это 2 треугольника на тайл. 100 100 2 = 20000 треугольников.

Проц со встройкой i5-11400
Если рисовать по одному при разрешении экрана 1920x1080 fps 50
Если рисовать батчером при разрешении экрана 1920x1080 fps 500

Для pentium 166 mhz + voodoo banshee в разрешении 800x600 около 8-9 fps.

Для железа 25+ лет, это не плохо. А теперь представим, что это текстура со шрифтами, иконками, элементами интерфейса, спрайты и т.д

Уже тогда можно было визуализировать достаточно быстро и при этом не трогать процессор.

Так и задумаешься, прям заговор какой-то:)

Не только игры

Программирование не ограничивается, играми. Это и сопутствующий софт. Не имеет смысла переписывать весь софт, так как он использует библиотеки. Если оптимизировать библиотеку, то и софт её использующий станет меньше тупить. Конечно это больше идеализм, но в итоге может сработать. Когда миллиарды устройств используют очень неэффективные библиотеки, то улучшение производительности на 10% экономит гигаватты электричества по всему миру. Экономия, батареи телефона в итоге. Быстрее загрузка, выполнение и т.д

Присоединяйтесь

Буду рад помощи в программировании проекта. У меня в проекте нет правил или ограничений. Код стайла, бюрократии. Просто вносите свой вклад если вам это интересно. Особенно нужна помощь, с реализацией аудио подсистемы, что то как то она мне не дается. Главное что бы код был на С++ и был переносим.

Поехали

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

#include <SDL3/SDL.h>

#define WINDOW_WIDTH  (640)
#define WINDOW_HEIGTH (480)

int main()
{
    SDL_Window* window     = NULL;
    SDL_Renderer* renderer = NULL;
    bool done              = false;

    if (!SDL_Init(SDL_INIT_VIDEO))
    {
        SDL_Log("Init error: %s\n", SDL_GetError());
        return 1;
    }

    window = SDL_CreateWindow("Renderer", WINDOW_WIDTH, WINDOW_HEIGTH, SDL_WINDOW_OPENGL);
    if (window == NULL)
    {
        SDL_Log("Create window error: %s\n", SDL_GetError());
        return 1;
    }

    renderer = SDL_CreateRenderer(window, NULL);
    if (renderer == NULL)
    {
        SDL_Log("Create renderer error: %s\n", SDL_GetError());
        return 1;
    }

    SDL_SetRenderDrawColor(renderer, 237, 28, 36, 0);

    while (!done)
    {
        SDL_Event event;

        while (SDL_PollEvent(&event))
        {
            if (event.type == SDL_EVENT_QUIT)
            {
                done = true;
            }
        }

        SDL_RenderClear(renderer);
        SDL_RenderPresent(renderer);
    }

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

Создание окна выглядит так

SDL_Window* SDL_CreateWindow(const char* title, int w, int h, size_t flags)
{
	return SDL_CreateWindowImplementation(
		SDL::GetApplication().GetWindows(),
		SDL::GetApplication().GetOpenGLAttributes(),
		SDL::GetApplication().GetResult(),
		SDL::GetApplication().GetEventHandler(),
		title, w, h, flags);
}

SDL::GetApplication это ссылка на глобальный объект, который содержит массив созданных окон, метаинформацию об экземпляре приложения, так же хранит информацию о последней ошибке.

Если не считать new и delete, то это единственный глобальный объект на библиотеку. Для каждого класса или функции я его передаю из вне.

Если углубимся, то увидим

SDL_Window* SDL_CreateWindowImplementation(std::vector<SDL_Window*>& windows, SDL::OpenGLAttributes& openGLAttributes, SDL::Result& result, SDL::EventHandler& eventHandler, const char* title, int w, int h, SDL_WindowFlags flags)
{
	SDL_Window* window = NULL;

	if (flags == SDL_WINDOW_OPENGL)
	{
		window = new SDL::OpenGLWindow(openGLAttributes, result, eventHandler, SDL::Vec2i(0, 0), SDL::Vec2i(w, h), title, flags);
	}
	else
	{
		window = new SDL::SoftwareWindow(result, eventHandler, SDL::Vec2i(0, 0), SDL::Vec2i(w, h), title, flags);
	}

	if (window)
	{
		windows.push_back(window);
	}

	return window;
}

В зависимости от переданного режима, выбирается тип окна.

У каждого окна своя ОС зависимая реализация, но окно наследуется от абстрактного класса

struct SDL_Window
{
public:
	virtual ~SDL_Window() {};
	virtual SDL::Surface* GetSurface() = 0;
	virtual const SDL::Vec2i& GetPos() = 0;
	virtual void SetPos(const SDL::Vec2i& pos) = 0;
	virtual const SDL::Vec2i& GetSize() = 0;
	virtual void SetSize(const SDL::Vec2i& size) = 0;
	virtual const std::string& GetTitle() = 0;
	virtual void SetTitle(const std::string& title) = 0;
	virtual SDL_WindowFlags GetFlags() = 0;
	virtual void PollEvents() = 0;
	virtual bool Present() = 0;
};

Который уже дергает функции SDL_Window_xxx

Любой функциональный объект в SDL3Lite, написан и работает по вышеописанному способу. Это упрощает внедрение несколько типов окон, рендеров в библиотеку.

Так выглядит глобальный объект с данными.

После всех манипуляций, наружу виден только SDL3 C API. На данный момент бинарная совместимость не готова и думаю, что не получится её реазизовать. По крайней мере, можно будет перекомпилировать проект не переписывая код, я к этому стремлюсь.

На данный момент, поддерживаются два типа рендера для отрисовки примитивов Software и OpenGL1.2. Но поддерживается Любая версия OpenGL если требуется использовать, только GL функции.

Вывод треугольника в OpenGL 3.1

Я стараюсь, придерживаться принципу SOLID и поэтому каждый функционал в основном находится в своем классе. Пример.

Библиотека умеет рисовать в свое окно или на переданную поверхность, сейчас рассматривается только софт рендер.

Для того, что бы рисовать попиксельно в массив байт, я создал два класса.

  1. Умеет рисовать основные примитивы, линии, пиксели, квадраты

namespace SDL
{
	class PixelPainter
	{
	public:
		void Clear(Surface* dest, const Color& color);
		void FillRect(Surface* dest, const Vec2f& pos, const Vec2f& size, const Color& color);
		void Line(Surface* dest, const Vec2f& first, const Vec2f& last, const Color& color);
	private:
	};
}

2. Умеет копировать поверхности. Нужен для копирования изображений.

namespace SDL
{
	class PixelCopier
	{
	public:
		void Copy(uint8_t* dstPixels, int dstBpp, const Vec2i& dstArea, const Vec2i& dstPos, const Vec2i& dstSize, uint8_t* srcPixels, int srcBpp, const Vec2i& srcArea, const Vec2i& srcPos, const Vec2i& srcSize);
	private:
	};
}

И уже эти классы вызываются в других рендерах, где нужно рисовать или копировать попиксельно.

Что бы не усложнять код внешними зависимостями, создал простую функцию тестирования.

void SDL3LiteTest(bool expression, const char* file, int line, const char* detail)
{
    if (!expression)
    {
        printf("Test fail! Expression: %s File: %s Line: %d \n", detail, file, line);
    }
}

#define SDL_TEST(expression) SDL3LiteTest(expression, __FILE__, __LINE__, #expression)

И просто пишу тесты. При падении, мне всегда понятно, что именно сломалось.

void TestBaseWindow()
{
    BaseWindow baseWindow(Vec2i(10, 15), Vec2i(640, 480), "Create BaseWindow");
    SDL_TEST(baseWindow.GetPos().x  == 10);
    SDL_TEST(baseWindow.GetPos().y  == 15);
    SDL_TEST(baseWindow.GetSize().x == 640);
    SDL_TEST(baseWindow.GetSize().y == 480);
    SDL_TEST(baseWindow.GetTitle()  == "Create BaseWindow");

    baseWindow.SetPos(Vec2i(35, 45));
    SDL_TEST(baseWindow.GetPos().x == 35);
    SDL_TEST(baseWindow.GetPos().y == 45);

    baseWindow.SetSize(Vec2i(500, 600));
    SDL_TEST(baseWindow.GetSize().x == 500);
    SDL_TEST(baseWindow.GetSize().y == 600);

    baseWindow.SetTitle("Change BaseWindow");
    SDL_TEST(baseWindow.GetTitle() == "Change BaseWindow");
}

void TestMainWindow(SDL_WindowFlags flags)
{
    Result       result;
    EventHandler eventHandler;
    MainWindow   mainWindow(result, eventHandler, Vec2i(10, 15), Vec2i(640, 480), "Create MainWindow", flags);
    SDL_TEST(result.Ok() == true);
    SDL_TEST(mainWindow.GetPos().x  == 10);
    SDL_TEST(mainWindow.GetPos().y  == 15);
    SDL_TEST(mainWindow.GetSize().x == 640);
    SDL_TEST(mainWindow.GetSize().y == 480);
    SDL_TEST(mainWindow.GetTitle()  == "Create MainWindow");
    SDL_TEST(mainWindow.GetFlags()  == flags);

    mainWindow.SetPos(Vec2i(35, 45));
    SDL_TEST(mainWindow.GetPos().x == 35);
    SDL_TEST(mainWindow.GetPos().y == 45);

    mainWindow.SetSize(Vec2i(500, 600));
    SDL_TEST(mainWindow.GetSize().x == 500);
    SDL_TEST(mainWindow.GetSize().y == 600);

    mainWindow.SetTitle("Change MainWindow");
    SDL_TEST(mainWindow.GetTitle() == "Change MainWindow");
}

void TestOpenGLWindow(SDL_WindowFlags flags)
{
    OpenGLAttributes openGLAttributes;
    Result           result;
    EventHandler     eventHandler;
    OpenGLWindow     openGLWindow(openGLAttributes, result, eventHandler, Vec2i(10, 15), Vec2i(640, 480), "Create OpenGL1Window", flags);
    SDL_TEST(result.Ok() == true);
    SDL_TEST(openGLWindow.GetPos().x  == 10);
    SDL_TEST(openGLWindow.GetPos().y  == 15);
    SDL_TEST(openGLWindow.GetSize().x == 640);
    SDL_TEST(openGLWindow.GetSize().y == 480);
    SDL_TEST(openGLWindow.GetTitle()  == "Create OpenGL1Window");
    SDL_TEST(openGLWindow.GetFlags()  == flags);

    openGLWindow.SetPos(Vec2i(35, 45));
    SDL_TEST(openGLWindow.GetPos().x == 35);
    SDL_TEST(openGLWindow.GetPos().y == 45);

    openGLWindow.SetSize(Vec2i(500, 600));
    SDL_TEST(openGLWindow.GetSize().x == 500);
    SDL_TEST(openGLWindow.GetSize().y == 600);

    openGLWindow.SetTitle("Change OpenGL1Window");
    SDL_TEST(openGLWindow.GetTitle() == "Change OpenGL1Window");
}

void TestSoftwareWindow(SDL_WindowFlags flags)
{
    Result         result;
    EventHandler   eventHandler;
    SoftwareWindow softwareWindow(result, eventHandler, Vec2i(10, 15), Vec2i(640, 480), "Create SoftwareWindow", flags);
    SDL_TEST(result.Ok() == true);
    SDL_TEST(softwareWindow.GetPos().x  == 10);
    SDL_TEST(softwareWindow.GetPos().y  == 15);
    SDL_TEST(softwareWindow.GetSize().x == 640);
    SDL_TEST(softwareWindow.GetSize().y == 480);
    SDL_TEST(softwareWindow.GetTitle()  == "Create SoftwareWindow");
    SDL_TEST(softwareWindow.GetFlags()  == flags);

    softwareWindow.SetPos(Vec2i(35, 45));
    SDL_TEST(softwareWindow.GetPos().x == 35);
    SDL_TEST(softwareWindow.GetPos().y == 45);

    softwareWindow.SetSize(Vec2i(500, 600));
    SDL_TEST(softwareWindow.GetSize().x == 500);
    SDL_TEST(softwareWindow.GetSize().y == 600);

    softwareWindow.SetTitle("Change SoftwareWindow");
    SDL_TEST(softwareWindow.GetTitle() == "Change SoftwareWindow");
}

Простые утверждения и проверка на корректность значений.

Рендеры уже полностью не зависимы от системы. Рендер с поддержкой OpenGL 1.2 или 3.1 используется для всех систем. В будущем добавлю поддержку Vulkan, в виде подключаемого API, так и ещё одну реализацию 2D графики. Что бы в зависимости от доступности графического API выбирался самый эффективный вариант. Это расточительно, рисовать на ЦПУ, когда есть аппаратное ускорение графики.

Объём кода требующийся для поддержки OpenGL 1.2 это примерно 500 строк. Для других API будет больше, но если сравнивать с общей кодо базой, то это копейки, но эти копейки позволяют работать максимально быстро. Клево же?

SDL3 реализует свои функции которые не зависят от стандартной библиотеки. Потому, приходится некоторые вещи переопределять обратно. Все ради совместимости.

double SDL_sin(double x)
{
	return sin(x);
}

float SDL_sinf(float x)
{
	return (float)sin((double)(x));
}

float SDL_cosf(float x)
{
	return (float)cos((double)(x));
}

float SDL_randf(void)
{
	return (float)(rand()) / (float)(RAND_MAX);
}

Sint32 SDL_rand(Sint32 n)
{
	return 0 + rand() % (n - 0);
}

Как итог, пару скриншотов примеров работающих на SDL3Lite, из оригинальной SDL3.

Ну и скриншот ради чего все затевалось.

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

Мне нравится старое железо, есть в нём некое очарование!

Комментарии (0)