Приветствую, Хабравчане!
В данной статье я покажу как выводить примитивы с помощью библиотеки 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)
Getequ
21.01.2025 11:30Ох, ньюби от программирования всё ещё думает что С++ равно легаси... Вероятно это выпускник Яндекс.практикума - у них так и написано:
https://education.yandex.ru/journal/legacy-kod-chto-eto-i-pochemy-s-nim-klassno-rabotat
Что такое легаси-код
Самое общее определение легаси — это код, который используется кем-то помимо его автора. Другие определения:
код, написанный на старой версии языка или с использованием устаревшего фреймворка;
По вашим меркам все актуальные операционные системы это легаси? Ведь они написаны на С/++ и ассемблере. Такая что-ли логика? Ну вы хотя бы ознакомьтесь с терминами
JordanCpp Автор
21.01.2025 11:30Термин Легаси переводится как наследие. И использую я его именно в этом контексте. Поддерживая в том числе и старые ОС, типа windows 3.1 и windows 95. Этим и обусловлен замысел статей, gdi это Легаси api.
Mutilator
21.01.2025 11:30Старый Linux - поддерживаю, интересно. Что-нибудь в районе kernel 2.2.x, XFree86 3.3.5 ? А вот смысла поддерживать Win 3.1 не вижу: у него даже среди ретро-геймеров очень невысокая популярность (имхо).
JordanCpp Автор
21.01.2025 11:30Данный код должен собраться под debian 3. Нужно проверить. Лично мне интересна поддержка windows 3.1. По сути, что бы собрать данный код, нужен 16 битный компилятор с поддержкой namespace и шаблонами. Остальной код это WinAPI, и windows 3.1 его поддерживает. Нужно расставить правильно #ifdef _WIN16 и должно собраться и работать.
Jijiki
21.01.2025 11:30спасибо интересно, не знаю где достать windows 3 или debian 3, отсюда вопрос в слепую на долгую, а модельки если можно выводить будет 3д как организовывать в старых версиях, старые версии библиотек придётся искать? или как? а если таких нет всё вручную парсить? понятно что держателю формата проще приспособить формат по типо MDL, но всё же, тоесть если через блендер это очень обширно получается ради переноса вниз поддержки - например анимаций, потом там старый XLib - тут я не профи, интересно, но скептично настроен, сейчас задумался о полном сдк на яве (чтоб 3д модельки были - простенький формат с анимациями и свитч анимаций на сцену), но это всё охватить всё равно обширно выйдет
и перенос на бсд не тривиален у явы, там знать надо
JordanCpp Автор
21.01.2025 11:30Я придерживаюсь С++ 98. Так как существуют +- старые компиляторы которые могут собрать такой код нативно на старой ОС. Но ограничение только С++ 98 только для самой библиотеки. Вы же можете её собирать с любым новым стандартом С++ и с любой новой библиотекой.
К примеру я собираю компилятором msvc 2022 и gcc 13. Но он так же может быть собран компилятором намного старее, к примеру gcc 3.
Поддержку 3d ещё нужно встроить, OpenGL как пример. Что бы можно было создать окно с контекстом OpenGL. Я это запланировал но в будущих статьях.
Ответ: ничего искать не нужно, берете любую С/С++ библиотеку и используете. Намеренно искать старый компилятор не нужно.
Jijiki
21.01.2025 11:30понял, спасибо (я сейчас понял как важно тащить функционал. например 3д создание и свитч тут же на сцену, типо 2 окна или вида - выбрали примитив или загруженную модельку в 1 вид она на сцене и в виде анимаций просматриваема и типо простенькие вещи тут же делать и каким-то формтом своим переносным навернуть - но это мечты, ну и получается уже ближе к 3д движку сейчас типо Юнити, там упор другой визуальное программирование даже форматом или каким-то аргументом не оправдан, а при переносе вниз уже оправдано). в целом получается только конфиги будут другими, тоесть всё таки вроде классно если такую реализацию как вы делаете иметь - это вроде тоже аргумент
JordanCpp Автор
21.01.2025 11:30Вы говорите о более функциональном фреймворке, что то близкое к движку. Я же пишу, библиотеку похожую на SDL, уж очень она мне нравится. Но на С++ и с поддержкой в том числе старых систем. Минимальный каркас абстрагирующий от нижележащей ОС.
unreal_undead2
Может ещё DOS бэкенд (скажем, поверх VBE) добавить? В SDL была такая активность, но похоже всё закончилось.
JordanCpp Автор
Dos тоже добавлю, но когда буду делать рендер для рисования в буфер ОЗУ. Сейчас именно статья о встроенных средствах ОС.
Вообще планирую следующие рендеры.
В буфер ОЗУ, все рисование на цп.
OpenGL
Glide
DirectX
Vulkan, если осилю:)