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

В данной статье я покажу как выводить примитивы с помощью библиотеки xlib, поговорим о системных вызовах Linux и заложим базу для кроссплатформенного фреймворка.

Наш мини фреймворк будет называться LDL - Little DirectMedia Layer. Как вы поняли это отсылка к библиотеке SDL.

У меня на гитхабе уже есть две реализации LDL.

Первый вариант оказался слишком тяжелым, я в него добавил форматы аудио файлов, вывод текста с помощью FreeType, загрузку основных форматов изображений и в итоге библиотека весит около двух мегабайт. Теперь я понимаю, что такие зависимости должны быть внешними.

Второй вариант, слишком низкоуровневый не использует namespace и шаблоны. И написан на С с классами.

В итоге я решил, что LDL разрабатываемый в данном цикле статей будет финальной версией. В нем не будет тяжелых зависимостей, только минимальный каркас. Все остальные расширения будут внешними. Минимальная библиотека легче переносится на другие системы и платформы. Так же будет использован один namespace LDL для всей библиотеки.

Приступим.

Основная репа RetroFan.

Это уже кроссплатформенный пример вывода случайных по размеру раскрашенных прямоугольников, работает на Windows и Linux. Имеет единый API. Под Windows использует GDI, под Linux XLib.

#include <LDL/LDL.hpp>
#include <stdlib.h>

int random(unsigned int min, unsigned int max)
{
    return min + rand() % (max - min);
}

int main()
{
	size_t rnd;

	srand(rnd);
    
	LDL::Window window(LDL::Vec2i(0, 0), LDL::Vec2i(800, 600));
	LDL::Render render(window);
    LDL::Event  report;

	while (window.Running())
	{
		while (window.GetEvent(report))
		{
			if (report.Type == LDL::Event::IsQuit)
			{
				window.StopEvent();
			}
		}

		render.Begin();

		for (size_t i = 0; i < 50; i++)
		{
			render.SetColor(LDL::Color(random(0, 255), random(0, 255), random(0, 255)));

			LDL::Vec2i pos  = LDL::Vec2i(random(0, 800), random(0, 600));
			LDL::Vec2i size = LDL::Vec2i(random(25, 50), random(25, 50));

			render.Fill(pos, size);
		}

		render.End();

		window.Update();
		window.PollEvents();
	}

    return 0;
}

Это максимально простой пример, пока библиотека LDL умеет рисовать линию и прямоугольник. Но уже собирается под две ОС и их старые вариации.

Так выглядит в Windows

Так выглядит в Linux

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

По примеру SDL2 разделил окно и рендер, что упрощает архитектуру.

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

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

namespace LDL
{
	class MainWindow
	{
	public:
		MainWindow(const Vec2i& pos, const Vec2i& size);
		~MainWindow();
		void Update();
		void StopEvent();
		bool Running();
		void PollEvents();
		bool GetEvent(Event& event);
	};
}

Для каждой версии private часть будет своей, но уже здесь видны простые методы. Для любой ОС, при создании окна, ОС направляет события в очередь. И когда пользовательский код в цикле опрашивает окно, он получает уже типизированные сообщения фреймворка. На данный момент я добавил обработку только одного события это закрытие окна.

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

			if (report.Type == LDL::Event::IsQuit)
			{
				window.StopEvent();
			}

После трансформации события в событие библиотеки, оно помещается в очередь. Очередь сделана на шаблоне статического кольцевого буфера.

Интерфейс рендера выглядит так:

namespace LDL
{
	class GdiRender
	{
	public:
		GdiRender(MainWindow& window);
		const Color& GetColor();
		void SetColor(const Color& color);
		void Begin();
		void End();
		void Clear();
		void Line(const Vec2i& first, const Vec2i& last);
		void Fill(const Vec2i& pos, const Vec2i& size);
	};
}

Старался делать максимально простым и понятным. Задать общий цвет и нарисовать примитив. Важно, все рисование происходит между двумя методами Begin и End.

Для простоты восприятия кода, я не стал пока применять идиому PIMPL, что бы скрыть реализацию. Поэтому при сборке в зависимости от определения компилятором конкретной ОС, выбирается реализация:

#if defined(_WIN32)
    #include <LDL/Windows/MainWin.hpp>
#elif defined (__unix__)
    #include <LDL/UNIX/MainWin.hpp>
#endif

namespace LDL
{
	typedef MainWindow Window;
}
#if defined(_WIN32)
    #include <LDL/Windows/GdiRndr.hpp>
#elif defined (__unix__)
    #include <LDL/UNIX/XLibRndr.hpp>
#endif

namespace LDL
{

#if defined(_WIN32)
	typedef GdiRender Render;
#elif defined (__unix__)
	typedef XLibRender Render;
#endif

}

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

Примеры рисования примитивов нативными средствами ОС.

Windows GDI

void GdiRender::Line(const Vec2i& first, const Vec2i& last)
{
	MoveToEx(_window._handleDeviceContext, first.x, first.y, NULL);
	LineTo(_window._handleDeviceContext, last.x, last.y);
}

void GdiRender::Fill(const Vec2i& pos, const Vec2i& size)
{
	RECT rect;

	rect.left   = pos.x;
	rect.top    = pos.y;
	rect.right  = size.x;
	rect.bottom = size.y;

	HBRUSH brush = CreateSolidBrush(RGB(_baseRender.GetColor().r, _baseRender.GetColor().g, _baseRender.GetColor().b));

	FillRect(_window._handleDeviceContext, &rect, brush);

	DeleteObject(brush);
}

Linux XLib

void XLibRender::Line(const Vec2i& first, const Vec2i& last)
{
	uint32_t rgb = MakeRgb(_baseRender.GetColor().r, _baseRender.GetColor().g, _baseRender.GetColor().b);

	XSetForeground(_window._display, _graphics, rgb);
	XDrawLine(_window._display, _window._window, _graphics, first.x, first.y, last.x, last.y);
}

void XLibRender::Fill(const Vec2i& pos, const Vec2i& size)
{
	uint32_t rgb = MakeRgb(_baseRender.GetColor().r, _baseRender.GetColor().g, _baseRender.GetColor().b);

	XSetForeground(_window._display, _graphics, rgb);
	XFillRectangle(_window._display, _window._window, _graphics, pos.x, pos.y, size.x, size.y);
}

Если говорить о коде рисования примитивов, у него много проблем, потому, что при изменении цвета в обоих библиотеках приходится вызывать функцию изменения цвета, что снижает производительность. Лучше его кэшировать и вызывать функции, только при реальном изменении. Уверен, что такого рода оптимизации не актуальны для современных компьютеров. Но для поддержки старого железа они полезны.

В Linux оторвать libc оказалось чуть сложнее. Я использовал следующее руководство. Для каждой архитектуры нужно создать свой асм файл c реализацией функции start, с которой и начинается выполнение программы.

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

Так выглядит вызов системной функции write в linux. Я пока не дошел до реализации malloc и free в Linux. Но уже в процессе.

intptr LinuxApi::write(int fd, void const* data, uintptr nbytes)
{
    return (uintptr)syscall3(SYS_write, (void*)(intptr)fd, (void*)data, (void*)nbytes);
}

void* LinuxApi::brk(uintptr nbytes)
{
    return syscall1(SYS_brk, (void*)nbytes);
}

На досуге, хочу нативно собрать под Windows 3.1 и старый Linux. Обязательно отпишусь об успехах.

Рад буду, предложениям, критике.

Обновление:

Исправил функцию random. Спасибо пользователю kov-serg.

Добавил голосование в свете данного диалога.

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


  1. unreal_undead2
    21.01.2025 11:30

    Может ещё DOS бэкенд (скажем, поверх VBE) добавить? В SDL была такая активность, но похоже всё закончилось.


    1. JordanCpp Автор
      21.01.2025 11:30

      Dos тоже добавлю, но когда буду делать рендер для рисования в буфер ОЗУ. Сейчас именно статья о встроенных средствах ОС.

      Вообще планирую следующие рендеры.

      1. В буфер ОЗУ, все рисование на цп.

      2. OpenGL

      3. Glide

      4. DirectX

      5. Vulkan, если осилю:)


  1. Getequ
    21.01.2025 11:30

    Ох, ньюби от программирования всё ещё думает что С++ равно легаси... Вероятно это выпускник Яндекс.практикума - у них так и написано:

    https://education.yandex.ru/journal/legacy-kod-chto-eto-i-pochemy-s-nim-klassno-rabotat

    Что такое легаси-код

    Самое общее определение легаси — это код, который используется кем-то помимо его автора. Другие определения:

    • код, написанный на старой версии языка или с использованием устаревшего фреймворка;

    По вашим меркам все актуальные операционные системы это легаси? Ведь они написаны на С/++ и ассемблере. Такая что-ли логика? Ну вы хотя бы ознакомьтесь с терминами


    1. JordanCpp Автор
      21.01.2025 11:30

      Это игра слов. Не нужно так серьезно относиться.


      1. Getequ
        21.01.2025 11:30

        Нет, это не игра слов, а подмена понятий. Фраза "я начинаю писать легаси" это оксюморон


        1. JordanCpp Автор
          21.01.2025 11:30

          это оксюморон

          Это каламбур.

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


    1. JordanCpp Автор
      21.01.2025 11:30

      Термин Легаси переводится как наследие. И использую я его именно в этом контексте. Поддерживая в том числе и старые ОС, типа windows 3.1 и windows 95. Этим и обусловлен замысел статей, gdi это Легаси api.


  1. Mutilator
    21.01.2025 11:30

    Старый Linux - поддерживаю, интересно. Что-нибудь в районе kernel 2.2.x, XFree86 3.3.5 ? А вот смысла поддерживать Win 3.1 не вижу: у него даже среди ретро-геймеров очень невысокая популярность (имхо).


    1. JordanCpp Автор
      21.01.2025 11:30

      Данный код должен собраться под debian 3. Нужно проверить. Лично мне интересна поддержка windows 3.1. По сути, что бы собрать данный код, нужен 16 битный компилятор с поддержкой namespace и шаблонами. Остальной код это WinAPI, и windows 3.1 его поддерживает. Нужно расставить правильно #ifdef _WIN16 и должно собраться и работать.


  1. B13nerd
    21.01.2025 11:30

    Очень странная формула: return rand() % ((max + min) + min);


    1. JordanCpp Автор
      21.01.2025 11:30

      Да мне написали уже корректный вариан на гитхаб. Возможно это вы? Просто ещё руки не дошли поправить и сделать коммит.


  1. Jijiki
    21.01.2025 11:30

    спасибо интересно, не знаю где достать windows 3 или debian 3, отсюда вопрос в слепую на долгую, а модельки если можно выводить будет 3д как организовывать в старых версиях, старые версии библиотек придётся искать? или как? а если таких нет всё вручную парсить? понятно что держателю формата проще приспособить формат по типо MDL, но всё же, тоесть если через блендер это очень обширно получается ради переноса вниз поддержки - например анимаций, потом там старый XLib - тут я не профи, интересно, но скептично настроен, сейчас задумался о полном сдк на яве (чтоб 3д модельки были - простенький формат с анимациями и свитч анимаций на сцену), но это всё охватить всё равно обширно выйдет

    и перенос на бсд не тривиален у явы, там знать надо


    1. JordanCpp Автор
      21.01.2025 11:30

      Я придерживаюсь С++ 98. Так как существуют +- старые компиляторы которые могут собрать такой код нативно на старой ОС. Но ограничение только С++ 98 только для самой библиотеки. Вы же можете её собирать с любым новым стандартом С++ и с любой новой библиотекой.

      К примеру я собираю компилятором msvc 2022 и gcc 13. Но он так же может быть собран компилятором намного старее, к примеру gcc 3.

      Поддержку 3d ещё нужно встроить, OpenGL как пример. Что бы можно было создать окно с контекстом OpenGL. Я это запланировал но в будущих статьях.

      Ответ: ничего искать не нужно, берете любую С/С++ библиотеку и используете. Намеренно искать старый компилятор не нужно.


      1. Jijiki
        21.01.2025 11:30

        понял, спасибо (я сейчас понял как важно тащить функционал. например 3д создание и свитч тут же на сцену, типо 2 окна или вида - выбрали примитив или загруженную модельку в 1 вид она на сцене и в виде анимаций просматриваема и типо простенькие вещи тут же делать и каким-то формтом своим переносным навернуть - но это мечты, ну и получается уже ближе к 3д движку сейчас типо Юнити, там упор другой визуальное программирование даже форматом или каким-то аргументом не оправдан, а при переносе вниз уже оправдано). в целом получается только конфиги будут другими, тоесть всё таки вроде классно если такую реализацию как вы делаете иметь - это вроде тоже аргумент


        1. JordanCpp Автор
          21.01.2025 11:30

          Вы говорите о более функциональном фреймворке, что то близкое к движку. Я же пишу, библиотеку похожую на SDL, уж очень она мне нравится. Но на С++ и с поддержкой в том числе старых систем. Минимальный каркас абстрагирующий от нижележащей ОС.


  1. JordanCpp Автор
    21.01.2025 11:30

    Добавил голосование. В свете данного диалога.


  1. MasterMentor
    21.01.2025 11:30

    слово "фреймворк" уже вызывает подозрение санитаров :)


  1. XViivi
    21.01.2025 11:30

    В коде LinuxApi::write почему идёт каст в uintptr, а возвращается intptr?