Пишем легаси с нуля на С++, не вызывая подозрение у санитаров. 03 — Начинаем разрабатывать фреймворк

от автора

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

В данной статье я покажу как выводить примитивы с помощью библиотеки 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 rand() % ((max + min) + 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. Обязательно отпишусь об успехах.

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


ссылка на оригинал статьи https://habr.com/ru/articles/874186/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *