experiments
Today

Pelles C, raylib и радости пионера

Если скажу, что для полноценной разработки под практически любую Windows вам не нужна ни сама Windows, ни Visual Studio ни еще какие дурацкие поделки известной корпорации — сильно удивитесь?

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

Пожалуй, даже о двух таких проектах.

Все радости пионера в одной картинке. Слева внизу баг ядра из-за запуска в Wine, справа вверху - голая пионерка.

Pelles C

Когда мне впервые рассказали, что на свете существует современная среда разработки под Windows размером в 30 Мегабайт, я разумеется не поверил, решив что это очередная дурацкая байка из интернета.

Но рассказы продолжались, народ в красках описывал что сие даже не просто одна IDE, а целый тулчейн «все в одном»:

оптимизирующий компилятор, ассемблер, линковщик, редактор ресурсов, отладчик, система сборки и даже собственный инсталлятор (!)

И вся эта красота умещается в столь скромные размеры.

А потом ее мне наконец показали.

И шутки кончились.

35Мб на диске. В качестве пруфа, лишние мегабайты ушли на документацию и примеры.

Короче автор знатно ох#ел удивился, поскольку сей замечательный проект попирает кирзовыми сапогами все его познания в операционных системах, системном программировании, архитектуре и компьютерах вообще.

Итак, встречайте на арене цирка:

Pelles C — небольшая бесплатная интегрированная среда разработки и компилятор для разработки программ на языке C, работающая в операционных системах Windows и поддерживаемая Пелле Ориниусом.

Цитата выше из русскоязычной статьи, потому как английского оригинала по Pelles C в Википедии внезапно нет. По этому интересному проекту вообще крайне мало материалов в сети, несмотря на всю его уникальность.

Не стоит вскрывать эту тему даже задумываться, на какие жертвы (и каким богам) пошел г-н Пелле ради создания этого инженерного шедевра, лишь скромно замечу, что история проекта началась не вчера:

1987–1988: Pelle Orinius begins writing an assembler and DOS/16-bit Windows tools while working at a consulting firm in Sweden.

Затем, уже в конце 90х автор адаптировал свои наработки под современные реалии Windows-разработки, взяв за основу проект Little C Compiler, на котором студентоту учили писать компиляторы в Принстоне.

Увы, но это практически вся доступная информация о проекте Pelles C.


Подготовка

«Pelles C» поддерживает практически любые версии Windows и отлично работает в Wine, что в очередной раз позволит автору использовать FreeBSD, не оскверняя себя работой непосредственно в Windows.

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

И Pelles C не разочаровал:

один единственный файл с лаконичным названием setup.exe скачивается прямо с сайта проекта, с главной страницы.

Запускаем инсталлятор с помощью Wine:

wine64 ~/Downloads/setup.exe

Запустится классический инсталлятор, который вы видели неоднократно, если только не вчера купили свой первый компьютер.

Рекомендую дополнительно установить исходники примеров:

Нет, это не InstallShield, это фирменный инсталлятор от авторов Pelles C.


Стоит установить Pelles C в более короткий путь, прямо на диске C:\ , чтобы было проще работать c этим каталогом с хоста:

"Политрук лжет" (ц) В инсталляторе показывается требуемый размер с большим запасом.


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

Теперь переходим в каталог с установленной средой:

cd ~/.wine/drive_c/PellesC/

И запускаем ее:

wine ./Bin/poide.exe

При первом запуске среды, вас попросят запустить генерацию специальной базы, для того чтобы работали подсказки и подстановки в редакторе:

Просто жмем «Build» и ожидаем завершения.

Так выглядит только что запущенная среда «Pelles C», готовая к работе:

Девственная пустота.

Самое время для новых интересных развлечений!

Hello, world

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


Вызываем меню «File→New→Project», запустится диалог создания нового проекта:

Неплохой выбор для пакета в 30Мб, правда?

Выбираем «Wizard for Windows application» и вводим название, следующим шагом указываем целевую архитектуру и шаблон:

Если у вас вдруг есть Windows на ARM64 - можно проверить и на нем. У автора такового под рукой не оказалось.

После нажатия кнопки «Next» будут перечислены все генерируемые файлы выбранного шаблона, а после завершения диалога настройки будет сгенерирован проект и автоматически загружен в среде разработки:

Классический "Hello, world" на С под Windows. Без использования продукции Microsoft и под FreeBSD.

Теперь нажимаем кнопку «Build» на панели инструментов, после чего запустится сборка проекта:

Так выглядит успешная сборка "Hello, world".

Для запуска собранного приложения нажимаем на панели инструментов кнопку «Run» и наслаждаемся видом.

Так выглядит в действии только что собранный «Hello, world»:

Понимаю, что вы ожидали большего при такой-то простыне кода, но чистый WinAPI действительно монструозен.

Собранное приложение не имеет зависимостей кроме WinAPI, поэтому запросто запускается даже на Windows 95.

Но разумеется на одном «Hello, world» я не успокоился и ниже расскажу о полноценном проекте с 3D-графикой, который можно разрабатывать с помощью Pelles C.

Архитектура Raylib, официальная схема. Отсюда.

Raylib

Для раскрытия возможностей Pelles C был нужен какой-то интересный, а главное практичный проект, чтобы дорогие читатели одновременно ох#ели обрадовались и смогли бы реально использовать всю эту машинерию в своей разработке.

Хотя-бы в теории.

Недолго думая, мой выбор пал на геймдев и вскоре «острый глаз индейца Джо» заметил одну интересную библиотеку:

raylib is a simple and easy-to-use library to enjoy videogames programming.

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

Допускаю, что причина кроется в столь радикальном описании:

raylib is a programming library to enjoy videogames programming; no fancy interface, no visual helpers, no gui tools or editors... just coding in pure spartan-programmers way. Are you ready to enjoy coding?

Поскольку последние остатки инженерной культуры ныне догрызают нейросети с вайбкодерами, даже публичные рассуждения на тему «enjoy coding» теперь приравниваются к половым девиациям и терроризму.

Но автора это разумеется не смущает.

Так выглядит в работе наше демо на Raylib. Одна из первых версий.

Hello, raylib

Путем недолгих поисков в сети был обнаружен такой интересный пост, где на чистом английском по шагам объяснялось как подключить raylib в Pelles C не привлекая внимания санитаров для полноценной разработки. 

Чтобы итоговое приложение с 3D-графикой можно было собрать, запустить и отлаживать в этой замечательной среде.

К сожалению в означенном посте предлагалось использовать устаревшую версию raylib (5.5) вместо последней, поэтому инструкции пришлось актуализовать.

Итак, что мы будем творить:

возьмем один из красивых примеров, поставляемых с Raylib, немного его доработаем лобзиком, затем соберем и запустим с помощью Pelles C.

Получим в итоге классическое Windows-приложение с 3D-графикой, созданное и работающее без продукции Microsoft.

Круто?

Скачиваем архив с последней на момент создания этой статьи версией 6.2 c официального репозитория на Github, для большей совместимости нужна готовая сборка под Win32:

wget https://github.com/raysan5/raylib/releases/download/6.2/raylib-6.2_win32_msvc16.zip

Создаем на виртуальном диске Wine следующую структуру каталогов:

cd ~/.wine/drive_c
mkdir -p dev/3rdparty/raylib

Распаковываем архив с raylib, убрав корневой каталог, чтобы содержимое архива находилось сразу в каталоге dev/3rdparty/raylib:

Так должна выглядеть распакованная библотека.

Теперь запускаем среду Pelles C и создаем новый проект типа «x86 Console program (exe)» с названием «hello-raylib», указав в Location C:\dev.

Собственно «hello-raylib» станет названием подкаталога в C:\dev , внутри которого будут созданы файлы проекта.

Нажимаем кнопку «New» на панели инструментов (она там самая левая) и вводим следующий код:

#include "raylib.h"
int main(void)
{
 InitWindow(800, 450, "raylib hello");
 SetTargetFPS(60);
 while (!WindowShouldClose())
  {
   BeginDrawing();
   ClearBackground(RAYWHITE);
   DrawText("Hello, raylib!", 190, 200, 40, BLACK);
   EndDrawing();
   } 
 CloseWindow();
 return 0;
}

Сохраняем как main.c, добавив в проект.

Простейший "Hello, world" на Raylib в Pelles C.

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

Открываем меню «Project→Project Options», вкладка Compiler и выставляем следующие настройки:

Главное выбрать в поле «C standard» значение C17 и поставить галочку «Enable Microsoft extensions».

Теперь переходим в раздел «Code generation» и выбираем значение поля «Default calling convention» в __cdecl:

Следующим шагом переходим в раздел «General→Folders», где необходимо настроить использование каталогов raylib с заголовочными файлами и библиотеками.

В поле «Type» выбираем «Libraries», затем жмем крайне всратую кнопку «New folder» и указываем каталог c:\dev\3rdparty\raylib\lib.

Так это выглядит вживую:

Затем аналогичным образом выбираем из выпадающего списка «Includes» и добавляем каталог c:\dev\3rdparty\raylib\lib.

Наконец в качестве последнего шага эпопеи с настройкой среды разработки выбираем раздел «Linker», затем вводим в поле «Library and object files» следующую строку:

kernel32.lib advapi32.lib delayimp.lib raylibdll.lib opengl32.lib gdi32.lib winmm.lib user32.lib shell32.lib

Это ни что иное как список библиотек, разделенных пробелами, с которыми линкуется наше тестовое приложение:

После этого финального шага можно пробовать собрать наш «Hello, raylib»:

нажимаем кнопку «Build» на панели инструментов и наблюдаем как сборка успешно завершается.

Сборка завершается да, но только собранное приложение.. не работает.

При попытке запуска (кнопка «Run» на все той же панели инструментов) вас ждет очередное интересное сообщение об ошибке:

Информативно и локанично, да.

Проблема заключается в том, что статичная библиотека raylibdll.lib, которую мы линковали выше на самом деле лишь обертка, которая при запуске самостоятельно подгружает отдельную DLL с движком raylib.

Для того чтобы собранное приложение все же заработало необходимо скопировать raylib.dll из каталога dev/3rdparty/raylib/lib в корневой каталог проекта.

Так это выглядит на моей машине:

После трюка с копированием библиотеки, наш «Hello, raylib» наконец запускается прямо из среды разработки:

Победа? Нет, лишь хорошее начало.

Для самых упоротых

Вполне очевидно (нормальным людям), что библиотека для разработки видеоигр предполагает наличие на компьютере аппаратного 3D-ускорения.

Поскольку мы работаем в Wine, по идее должно работать аппаратное ускорение на хосте (где у меня FreeBSD), которое с помощью специальных оберток используется из запущенного Win32-приложения.

Но так как автор — радикальный технолуддит фанат винтажной техники, эта замечательная статья вместе с примерами и тестами создается на машине 2006 года, без дискретной видеокарты.

Другими словами:

поддержки аппаратного 3D-ускорения тут нет, не было и уже не будет.

Хотя в последнем я не уверен.

Чтобы все же запускать приложения, использующие raylib в столь упоротом окружении, необходимо выставить две переменные окружения:

export LIBGL_ALWAYS_SOFTWARE=true 
export GALLIUM_DRIVER=llvmpipe

После чего запускаем в Wine наш Pelles C описанным выше способом:

cd ~/.wine/drive_c/PellesC
wine ./Bin/poide.exe

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

Демо в действии.

Демо-проект

Если вы, дорогой читатель, дошли до этого места и до сих пор трезвы не утратили интерес — поздравляю, вы наш клиент. И теперь у вас на руках есть настроенная среда разработки Pelles C, а также работающий пример приложения с 3D-графикой.

Можно наконец заняться настоящим делом.

В официальном репозитории проекта raylib есть много разных интересных примеров, один из которых автор и решил взять в качестве демо — для сборки и запуска с помощью Pelles C.

Пример называется «raylib [core] example — 3d camera first person» и демонстрирует различные режимы работы с камерой. Бонусом идет полноценная 3D-сцена с генерацией простеньких объектов и работа с интерфейсом.

Создаем новый проект, повторяем все шаги настройки, описанные выше для «Hello, raylib», добавляем новый файл main.c с таким кодом:

#include "raylib.h"

#define MAX_COLUMNS 20

int main(void)
{
    // Initialization
    const int screenWidth = 800;
    const int screenHeight = 450;

    InitWindow(screenWidth, screenHeight, 
          "0x08: raylib on Pelles C");

    // Define the camera to look into our 3d world (position, 
    // target, up vector)
    Camera camera = { 0 };
    // Camera position
    camera.position = (Vector3){ 0.0f, 2.0f, 4.0f };    
    // Camera looking at point
    camera.target = (Vector3){ 0.0f, 2.0f, 0.0f };      
    // Camera up vector (rotation towards target)
    camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };          
     // Camera field-of-view Y
    camera.fovy = 80.0f;                               
    // Camera projection type
    camera.projection = CAMERA_PERSPECTIVE;             
    
    int cameraMode = CAMERA_ORBITAL;

    // Generates some random columns
    float heights[MAX_COLUMNS] = { 0 };
    Vector3 positions[MAX_COLUMNS] = { 0 };
    Color colors[MAX_COLUMNS] = { 0 };

    for (int i = 0; i < MAX_COLUMNS; i++)
    {
        heights[i] = (float)GetRandomValue(1, 12);
        positions[i] = (Vector3){ (float)GetRandomValue(-15, 15), 
        heights[i]/2.0f, (float)GetRandomValue(-15, 15) };
        colors[i] = (Color){ GetRandomValue(20, 255), 
                             GetRandomValue(10, 55), 30, 255 };
    }

    // Limit cursor to relative movement inside the window
    DisableCursor();                    
    
    // Set our game to run at 60 frames-per-second
    SetTargetFPS(60);                   
    
    // Main game loop
    // Detect window close button or ESC key    
    while (!WindowShouldClose())        
    {
        // Update
        // Switch camera mode
        if (IsKeyPressed(KEY_ONE))
        {
            cameraMode = CAMERA_FREE;
            camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; // Reset roll
        }

        if (IsKeyPressed(KEY_TWO))
        {
            cameraMode = CAMERA_FIRST_PERSON;
            camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; // Reset roll
        }

        if (IsKeyPressed(KEY_THREE))
        {
            cameraMode = CAMERA_THIRD_PERSON;
            camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; // Reset roll
        }

        if (IsKeyPressed(KEY_FOUR))
        {
            cameraMode = CAMERA_ORBITAL;
            camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; // Reset roll
        }

        // Switch camera projection
        if (IsKeyPressed(KEY_P))
        {
            if (camera.projection == CAMERA_PERSPECTIVE)
            {
                // Create isometric view
                cameraMode = CAMERA_THIRD_PERSON;
                // Note: The target distance is related to the render
                // distance in the orthographic projection
                camera.position = (Vector3){ 0.0f, 2.0f, -100.0f };
                camera.target = (Vector3){ 0.0f, 2.0f, 0.0f };
                camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
                camera.projection = CAMERA_ORTHOGRAPHIC;
                // near plane width in 
                // CAMERA_ORTHOGRAPHIC
                camera.fovy = 20.0f; 
                CameraYaw(&camera, -135*DEG2RAD, true);
                CameraPitch(&camera, -45*DEG2RAD, true, true, false);
            }
            else if (camera.projection == CAMERA_ORTHOGRAPHIC)
            {
                // Reset to default view
                cameraMode = CAMERA_THIRD_PERSON;
                camera.position = (Vector3){ 0.0f, 2.0f, 10.0f };
                camera.target = (Vector3){ 0.0f, 2.0f, 0.0f };
                camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
                camera.projection = CAMERA_PERSPECTIVE;
                camera.fovy = 60.0f;
            }
        }

        // Update camera computes movement internally depending 
        // on the camera mode
        // Some default standard keyboard/mouse inputs are hardcoded 
        // to simplify use
        // For advanced camera controls, it's recommended to compute 
        // camera movement manually
        UpdateCamera(&camera, cameraMode);               
     
        // Draw
        BeginDrawing();

        ClearBackground(RAYWHITE);

        BeginMode3D(camera);

        DrawPlane((Vector3){ 0.0f, 0.0f, 0.0f }, 
                (Vector2){ 32.0f, 32.0f }, LIGHTGRAY); // Draw ground
        DrawCube((Vector3){ -16.0f, 2.5f, 0.0f },
                 1.0f, 5.0f, 32.0f, BLUE);     // Draw a blue wall
        DrawCube((Vector3){ 16.0f, 2.5f, 0.0f },
                 1.0f, 5.0f, 32.0f, LIME);      // Draw a green wall
        DrawCube((Vector3){ 0.0f, 2.5f, 16.0f }, 
                32.0f, 5.0f, 1.0f, GOLD);      // Draw a yellow wall

        // Draw some cubes around
        for (int i = 0; i < MAX_COLUMNS; i++)
                {
                 DrawCube(positions[i], 2.0f, heights[i], 2.0f, colors[i]);
                 DrawCubeWires(positions[i], 2.0f, heights[i], 2.0f, MAROON);
                }

                // Draw player cube
        if (cameraMode == CAMERA_THIRD_PERSON)
                {
                 DrawCube(camera.target, 0.5f, 0.5f, 0.5f, PURPLE);
                 DrawCubeWires(camera.target, 0.5f, 0.5f, 0.5f, DARKPURPLE);
                }

        EndMode3D();

        // Draw info boxes
        DrawRectangle(5, 5, 330, 100, Fade(SKYBLUE, 0.5f));
        DrawRectangleLines(5, 5, 330, 100, BLUE);

            DrawText("Camera controls:", 15, 15, 10, BLACK);
            DrawText("- Move keys: W, A, S, D, Space, Left-Ctrl", 
            15, 30, 10, BLACK);
            DrawText("- Look around: arrow keys or mouse",
             15, 45, 10, BLACK);
            DrawText("- Camera mode keys: 1, 2, 3, 4", 
            15, 60, 10, BLACK);
            DrawText("- Zoom keys: num-plus, num-minus or mouse scroll", 
            15, 75, 10, BLACK);
            DrawText("- Camera projection key: P", 15, 90, 10, BLACK);

            DrawRectangle(600, 5, 195, 100, Fade(SKYBLUE, 0.5f));
            DrawRectangleLines(600, 5, 195, 100, BLUE);

            DrawText("Camera status:", 610, 15, 10, BLACK);
            DrawText(TextFormat("- Mode: %s",
             (cameraMode == CAMERA_FREE) ? "FREE" :
             (cameraMode == CAMERA_FIRST_PERSON) ? "FIRST_PERSON" :
             (cameraMode == CAMERA_THIRD_PERSON) ? "THIRD_PERSON" :
             (cameraMode == CAMERA_ORBITAL) ? "ORBITAL" : "CUSTOM"), 
             610, 30, 10, BLACK);
            DrawText(TextFormat("- Projection: %s", 
            (camera.projection == CAMERA_PERSPECTIVE) ? "PERSPECTIVE" :
            (camera.projection == CAMERA_ORTHOGRAPHIC) ? 
             "ORTHOGRAPHIC" : "CUSTOM"), 610, 45, 10, BLACK);
            DrawText(TextFormat("- Position: (%06.3f, %06.3f, %06.3f)", 
            camera.position.x, camera.position.y, camera.position.z), 
            610, 60, 10, BLACK);
            DrawText(TextFormat("- Target: (%06.3f, %06.3f, %06.3f)", 
            camera.target.x, camera.target.y, camera.target.z), 
            610, 75, 10, BLACK);
            DrawText(TextFormat("- Up: (%06.3f, %06.3f, %06.3f)", 
            camera.up.x, camera.up.y, camera.up.z), 610, 90, 10, BLACK);

        EndDrawing();
    }

    // De-Initialization
    // Close window and OpenGL context
    CloseWindow();        
    
    return 0;
}

Код выше — чуть порезанная версия оригинального примера библиотеки raylib, из которого был удален блок работы с pro-камерой, не особо актуальный для простого демо.

Еще я чуть-чуть увеличил fov и сменил тип камеры по-умолчанию, чтобы сразу получить вращение камеры вокруг сцены.

Так выглядит финальный результат в динамике:

Не забываем, что все это крутится на FreeBSD, на машине 2006 года.

Нативная сборка для FreeBSD

На сладкое покажу сборку нашего демо-проекта непосредственно на FreeBSD, причем без изменения исходного кода.

Оказалось, в пакетах FreeBSD есть готовая сборка библиотеки raylib, поэтому достаточно выполнить:

pkg install raylib

Теперь переходим в каталог с проектом:

cd ~/.wine/drive_c/dev/raylib-demo

Запускаем сборку с помощью обычного gcc14 уже на хосте:

gcc14 main.c -o raylib_bsd -I/usr/local/include -L/usr/local/lib -lraylib -Wno-implicit-function-declaration

Опция -Wno-implicit-function-declaration нужна для пропуска ругани компилятора на вызываемые методы raylib.

В оригинальном примере подключается заголовок rcamera.h, являющийся частью исходников самого raylib и по этой причине отсутствующий в готовом пакете.

Получаем готовую сборку демо-проекта для FreeBSD:

Эту сборку можно запускать на хосте, без всяких Wine: