C++ kullanarak 2B uygulama geliştirme için biricik yardımcınız olacak olan sdl-painter maceramıza devam ediyoruz. Okumayanlar için ilk yazımın bağlantısını aşağıya koyuyorum. Bir göz atmanızda fayda var.
Özetle, sdl-painter, SDL3 + OpenGL 3.3 Core ve Vulkan 1.1 ile modern C++ kullanılarak , çoklu platform desteği sunan, sağlam ve güncel bir yazılım altyapısı ve araçları ile geliştirilen 2B çizim kütüphanesidir.
Yazılarımı ve sdlpainter içerisindeki örneklerimi fazlara böldüm. Yazı ve kod içerisinde bunlara çok rast gelebilirsiniz. Neden “faz”lara böldüğümü de söyleyeyim: açıkçası, kütüphaneyi tek hamlede anlatmak yerine, her yazıda çalışan, elle tutulur bir parça ekleyerek ilerlemeyi tercih ettim. Her faz bir öncekinin üstüne biniyor olacak
Geri dönecek olursak, bir önceki yazımda, çizimlere yönelik çok detaya girmemiştik. Bu yazımda da pek giremeyeceğiz ne yazık ki 🙂 Neden peki? Öncelikle temelimizi biraz daha güçlendirmemizde fayda var, bunun için de bu yazımı da bu konuya ayıracağım. Bununla birlikte bir sonraki yazımda, artık can cana giriş yapıyoruz.
Bu arada şunu da söylememe izin verin lütfen, sdl-painter’ı başka işlerimde de aktif olarak kullananmaya devam ediyorum. Bu yazıların gecikmesinin sebebi de biraz o işler oluyor ama kullanım sırasında da bir çok iyileştirme veye eklenebilecek yeni kabilyetlerin de farkın avarıyorum. Onun için aslında kütüphanenin palazlanması ve biraz hırpalanmasında fayda var. Bu kullanımlar o açıdan iyi oluyor.
Peki bu yazımın kapsmaı nedir? Hemen sıralayayım, bu yazıda mimariyi, kullanılan temel tipleri, yazılım oluşturma (build) sistemini, OpenGL’I ve tabi ki CI/CD alt yapısını ele alacağız. Bu arada, burada bahsi geçen altyapısal bir çok mekanizmayı kendi projelerinizde de kullanabilirsiniz.
O zaman ilk başlık ile başlayalım.
İçerik
Proje Mimarisi
Bir önceki yazımda da bahsettiğim gibi, sdlpainter, katmanlı bir yapıya sahip ve her katmanın ayrı sorumlulukları bulunmakta:
|
1 2 3 4 5 6 |
SDLPainter → Transform Stack / State → Shape Tessellator → IRenderer → OpenGLRenderer | VulkanRenderer → SDL3 Platform Layer |
Katmanları teker teker inceleyelim:
- Painter (Public API): Geliştiricinin gördüğü katman. DrawCircle, FillRect, SetPen gibi çağrılar bu katman üzerinden yapılıyor. Pen, Brush, transform stack burada bulunuyor. Çoğu zaman kütüphaneye yönelik kullanacağınız kısım burası olacak. Burayı olabildiğince sade tutmak amacım,
- Shape Tessellator: Şekilleri (daire, dikdörtgen, poligon) backend’in (burada opengl ve vulkan oluyor) anlayacağı vertex listelerine dönüştürüyor. Bu katman ile gelen üst seviye çizim isteklerini alıp, bunları bir seviye ayrıştırıp şekil koordinatlarına çeviriyoruz. Bunu yaparken de kullanılacak görselleştirme backendinden bunu bağımsız yapıyoruz. Bir diğer ifade ile şu an için OpenGL ve Vulkan ama ileride DirectX olsa da bunu orada kullanabilecektik. Bu açıdan önemli bir katman,
- IRenderer: Şimdi geldik asıl aksiyonun döndüğü katmana, backend soyutlama katmanına. Bu katman ile geliştiriciden aldığımız üst seviye çizim isteklerini DrawTriangles, CreateTexture, SetProjectionMatrix gibi daha alt seviye çağrılara dönüştürüyoruz. Tabi bu seviyede artık, bu çağrılar OpenGL ve Vulkan’a göre farklılık gösteriyor. Bir diğer ifadeyle, IRenderer arayüzünü bu backend’ler için implement ediyor. Bu katmanlama sayesinde hem OpenGL hem de Vulkan backend’ini destekliyoruz.
- SDL3 Platform Katmanı: Bu seviye de artık SDL ile pencere yönetimi, OpenGL context oluşturma, girdilerin kotarılması ve benzeri işleri yapıyoruz.
IRenderer: Backend Soyutlama Arayüzü
sdl-painter’ın merkezinde IRenderer arayüz sınıfı bulunuyor.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
namespace sdl_painter { /// @brief Backend bağımsız soyut renderer arayüzü. /// /// OpenGL ve Vulkan implementasyonları bu arayüzü uygular. /// Painter, IRenderer üzerinden çalışır — backend detaylarını bilmez. class IRenderer { public: virtual ~IRenderer() = default; // Yaşam döngüsü virtual bool Initialize(SDL_Window* window) = 0; virtual void Shutdown() = 0; virtual void BeginFrame() = 0; virtual void EndFrame() = 0; // Durum virtual void SetViewport(int32_t x, int32_t y, int32_t width, int32_t height) = 0; virtual void SetScissor(int32_t x, int32_t y, int32_t width, int32_t height) = 0; virtual void ClearScissor() = 0; virtual void Clear(const Color& color) = 0; virtual void SetOpacity(float alpha) = 0; // Çizim primitifleri (tessellated vertex'ler) virtual void DrawTriangles(const std::vector<Vertex>& vertices) = 0; // Texture işlemleri virtual TextureHandle CreateTexture(const uint8_t* data, int32_t width, int32_t height, int32_t channels) = 0; virtual void DestroyTexture(TextureHandle handle) = 0; virtual void DrawTextured(const std::vector<TexturedVertex>& vertices, TextureHandle texture) = 0; // Transform virtual RendererBackend GetBackend() const = 0; virtual void SetProjectionMatrix(const float* mat4) = 0; virtual void SetModelMatrix(const float* mat3) = 0; }; /// @brief Seçilen backend için IRenderer örnği oluştur. std::unique_ptr<IRenderer> CreateRenderer(RendererBackend backend); } // namespace sdl_painter |
RendererBackend enum’u ile hangi backend kullanılacağını ifade ediyoruz:
|
1 2 3 4 |
enum class RendererBackend { kOpenGL, kVulkan, }; |
CreateRenderer factory fonksiyonu ile ilgili backend implementasyonu nu heap’te oluşturup unique_ptr olarak döndürüyor. Geliştirici IRenderer* üzerinden gerekli işlevleri gerçekleştiriyor; OpenGLRenderer ya da VulkanRenderer’ı doğrudan görmüyor.
TextureHandle ise yine benzer şekilde backend-agnostic bir tanımlayıcı (uint32_t alias’ı), OpenGL’de texture ID, Vulkan’da descriptor index olarak kullanılıyor olacak:
|
1 2 3 4 5 |
/// @brief Texture tanımlayıcısı — backend-agnostic opak tip. using TextureHandle = uint32_t; /// @brief Geçersiz/boş texture tanımlayıcısı. constexpr TextureHandle kInvalidTexture = 0; |
Bu arayüzdeki çizim API’lerine inşallah sonraki yazılarımda gireceğiz, şimdilik burada bırakıyoruz ve sonraki başlığa geçiyoruz.
Temel Tipler
Bu başlık altında sdl-painter kullanarak geliştireceğiniz uygulamalarda, sıklıkla ihtiyaç duyacağınız veya karşılaşacağınız tiplere değineceğiz, bunların çoğu zaten kendini açıklar nitelikte, bu sebeple detaylara çok girmeyeceğim. İlki ile başlayalım
Color
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct Color { uint8_t r{0}; uint8_t g{0}; uint8_t b{0}; uint8_t a{255}; // varsayılan: tam opak constexpr Color(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255) : r(r), g(g), b(b), a(a) {} float RedF() const { return r / 255.0f; } // ... diğer kanallar static constexpr Color Black() { return {0, 0, 0}; } static constexpr Color White() { return {255, 255, 255}; } static constexpr Color Transparent() { return {0, 0, 0, 0}; } }; |
RedF() fonksiyonları, shader uniform’larına float göndermek için kullanıyoruz, her kanal [0, 255] aralığında uint8_t normalde.
Pen ve Brush
sdl-painter API’lerinde, Pen çizgi stilini, Brush dolgu stilini temsil eder.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Pen { public: explicit Pen(const Color& color, float width = 1.0f) : mColor(color), mWidth(width) {} const Color& GetColor() const { return mColor; } float GetWidth() const { return mWidth; } bool IsVisible() const { return mColor.a > 0 && mWidth > 0.0f; } static Pen NoPen() { return Pen(Color::Transparent(), 0.0f); } private: Color mColor{Color::Black()}; float mWidth{1.0f}; }; |
NoPen() ve NoBrush() factory metodları, “çizme ama doldur” ya da “doldurma ama çiz” senaryoları için kullanıyor olacağız, temel olarak içi boş dikdörtgen ve çerçevesiz ama içi dolu dikdörtgen.
Vertex ve TexturedVertex
IRenderer’ın aldığı ham vertex tipleri:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct Vertex { float x{0.0f}; float y{0.0f}; uint8_t r{255}, g{255}, b{255}, a{255}; constexpr Vertex(float x, float y, uint8_t r, uint8_t g, uint8_t b, uint8_t a) : x(x), y(y), r(r), g(g), b(b), a(a) {} }; struct TexturedVertex { float x{0.0f}; float y{0.0f}; float u{0.0f}; // UV koordinatı float v{0.0f}; uint8_t r{255}, g{255}, b{255}, a{255}; }; |
Renk bilgisi de vertex’in içinde taşınıyor — bu sayede Tessellator her vertex’e Pen/Brush rengini doğrudan yazabiliyor ve ayrı bir uniform güncellemesi gerekmiyor.
Point, Rect, Size
Geometri tipleri de temelde aşağıdaki gibi tanımlanmakta:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
struct Point { float x{0.0f}; float y{0.0f}; }; struct Rect { float x{0.0f}, y{0.0f}, w{0.0f}, h{0.0f}; float Right() const { return x + w; } float Bottom() const { return y + h; } Point Center() const { return {x + w * 0.5f, y + h * 0.5f}; } bool Contains(float px, float py) const; }; struct Size { float w{0.0f}; float h{0.0f}; }; |
Build Sistemi: CMake + Conan 2
Conan 2 — Bağımlılık Yönetimi
conanfile.py tüm bağımlılıkları tanımlar ve ilgili paketleri otomatik bir şekilde conan-center’dan çekilmesine imkan sağlar. Daha önce, template ve uengine projelerinde en çok vakit alan kısımlar aslında bu bağımlılıkları kurmak oluyordu. Burada o yükü conan yükleniyor.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def requirements(self): self.requires("sdl/3.2.14") # SDL3 — pencere + input self.requires("glad/0.1.36") # OpenGL 3.3 function loader self.requires("stb/cci.20240531") # stb_image — header-only self.requires("spdlog/1.15.3", options={"spdlog/*:header_only": True}) # loglama self.requires("sdl_ttf/3.2.2") # Phase 4: metin çizimi if self.options.with_vulkan: self.requires("vulkan-loader/1.3.290.0") self.requires("vulkan-headers/1.3.290.0") if self.options.build_tests: self.requires("gtest/1.15.0") |
CMakeLists.txt — Ana Yapı
CMakelist’in genel yapısı daha önce sizler ile paylaştığım, CMake şablonlarına çok benziyor:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
cmake_minimum_required(VERSION 3.20) project(SDLPainter VERSION 0.1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # clang-tidy için ... add_library(sdl_painter ${SDLPAINTER_SOURCES}) target_link_libraries(sdl_painter PUBLIC SDL3::SDL3 PUBLIC OpenGL::GL PRIVATE glad::glad PRIVATE stb::stb PRIVATE spdlog::spdlog_header_only ) |
PUBLIC / PRIVATE ayrımına dikkat etmenizde fayda var dostlar. SDL3 ve OpenGL public, çünkü geliştirici kodu da bu header’lara dolaylı olarak bağımlı. glad ve stb ise gerçekleme detayı olduğu için, kullanıcı uygulamalarına geçirilmesine gerek yok.
OpenGL ve Vulkan uygulamalarında özellikle ihtiyaç duyacağınız shader dosyaları için de satırlar mevcut:
|
1 2 3 4 5 6 7 8 |
function(sdlpainter_copy_shaders target) # MSVC: $<TARGET_FILE_DIR> ile multi-config desteği # Ninja/Make: configure zamanında sabit dizine kopyala endfunction() function(sdlpainter_copy_vulkan_shaders target) # ... endfunction() |
CMake Presetleri
Projedeki tüm yapılandırmaları CMakePresets.json içinde tanımladık. Bunun güzel yanı: lokalde çalıştırılan komutlar CI’da çalışan komutlar ile birebir aynı. Bu sayede iki ortam arasında oluşabilecek farklılıkların önüne geçmiş oluyoruz.
| Preset | Açıklama |
|---|---|
linux-debug / linux-release |
Ninja + GCC, CI’daki ana build’ler |
linux-debug-asan |
AddressSanitizer + UBSan aktif debug build |
windows-debug / windows-release |
Visual Studio 2022 (MSVC), native Windows |
windows-mingw-debug / windows-mingw-release |
Linux’tan MinGW-w64 ile Windows cross-compile |
Her preset kendi build/<preset-adı>/ dizinine, Conan’ın o preset için ürettiği toolchain dosyasını göstererek yapılandırılıyoruz. Yani bir preset = bir Conan kurulumu + bir build dizini.
Derleme için izlenebilecek örnek adımları şöyle sıralayabiliriz:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# Conan profili (ilk seferinde) conan profile detect # Debug bağımlılıkları yükle conan install . --output-folder=build/linux-debug/generators --build=missing -s build_type=Debug # Configure + build cmake --preset linux-debug cmake --build --preset linux-debug # Testleri çalıştır ctest --preset linux-debug |
Bu arada dikkatli takipçilerim, bu komutları bu şekilde çağırmanıza gerek olmadığını da fark etmiştir. /scripts dizini altında farklı platformlar için gerekli betikler mevcut 😉
Bunun yanında kullanabileceğiniz temel bir takım profil dosyalarını da sizlerle ilerleyen dönemde paylaşacağım.
Neden OpenGL 3.3?
İlk yazıda kısaca değindiğim bir diğer konu da opengl 3.3 kullanımıydı.
Mimari kararları kayıt altına almak için bildiğiniz gibi ADR (Architecture Decision Record) formatını kullanıyoruz (buna yönelik de bir yazım mevcut). Bu konuya yönelik de ADR-001’i oluşturduk. Detaylara ilgili dokümandan ulaşabilirsiniz. Kısaca alternatiflere bakacak olursak:
| Seçenek | Neden reddedildi |
|---|---|
| OpenGL 2.1 | Deprecated; fixed-function pipeline; VAO yok |
| OpenGL 4.5+ | macOS desteği yok (Apple 4.1’de takılı) |
| OpenGL ES 2.0 | Desktop’ta sınırlı destek |
| Vulkan (Phase 0) | Yüksek kurulum maliyeti; 2D için fazla karmaşık |
OpenGL 3.3 Core Profile seçiminin gerekçesi:
- 2012 sonrası tüm masaüstü GPU’larda destekleniyor,
- VAO, VBO, UBO, GLSL 330 — 2D kütüphane için gereken her şey mevcut,
- Core Profile: fixed-function pipeline kaldırılmış amd davranış tahmin edilebilir.
Faz 0 Demomuz, İlk SDL Penceresi
Aslında bu yazımda CI/CD konularına da girecektim ama yazı çok uzadı, onun için faz 0 örnek uygulaması ile tamamlamaya karar verdim.
Faz 0 demosu aslında gerçek çizim yapmıyor, bununla birlikte altyapının derlendiğini ve çalıştığını doğruluyor. Aynı zamanda loglama sistemini de test ediyor:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
static void InitLogger() { #ifdef _WIN32 // Windows Terminal'de ANSI renk kodlarını etkinleştir HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); DWORD dwMode = 0; GetConsoleMode(hOut, &dwMode); SetConsoleMode(hOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); #endif auto sink = std::make_shared<spdlog::sinks::ansicolor_stdout_sink_mt>(); sink->set_pattern("[%H:%M:%S][%L] %v"); auto logger = std::make_shared<spdlog::logger>("sdlpainter", sink); logger->set_level(spdlog::level::trace); spdlog::set_default_logger(logger); } int main() { InitLogger(); spdlog::info("SDLPainter Phase 0 demo starting..."); if (!SDL_Init(SDL_INIT_VIDEO)) { spdlog::error("SDL_Init failed: {}", SDL_GetError()); return 1; } SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_Window* window = SDL_CreateWindow( "SDLPainter — Phase 0 Demo", 800, 600, SDL_WINDOW_OPENGL); if (!window) { spdlog::error("SDL_CreateWindow failed: {}", SDL_GetError()); SDL_Quit(); return 1; } spdlog::info("Window created (800x600). Press ESC to exit."); bool running = true; while (running) { SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_EVENT_QUIT) running = false; if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE) running = false; } } SDL_DestroyWindow(window); SDL_Quit(); return 0; } |
Dikkat ettiyseniz SDL3’te SDL_Init başarısız olduğunda false döndürüyor — SDL2’deki negatif integer yerine, buna da SDL3’e yönelik yazımda değinmiştim. Fakat, bu API değişiklikleri genelde SDL3’e geçişte karşılaşılan en yaygın derleme hatalarından birisi.
Bu arada, loglama için despdlog kullanıyoruz. [%H:%M:%S][%L] formatı zamanı bize veriyor ve kayıtları daha okunabilir kılıyor.
Uygulamayı çalıştırdığınızda çıktı şöyle olmalı:
|
1 2 3 4 5 6 |
.\build\windows-debug\examples\Debug\phase0_demo.exe [00:16:40][I] SDLPainter Phase 0 demo starting... [00:16:40][I] SDL initialized successfully. [00:16:41][I] Window created (800x600). [00:16:41][W] This is a warn-level log sample. [00:16:41][I] Close the window or press ESC to exit. |
Sonuç
İlk yazımızla faz-0 aşamasını tamamlıyoruz. Peki nelere değindik bu yazımda:
- Katmanlı bir mimarimiz var — Painter, Tessellator, IRenderer, Backend
IRendererarayüzü önemli — OpenGL ve Vulkan için ortak sözleşme- Temel tiplerimiz — Color, Pen, Brush, Vertex, Point/Rect/Size
- CMake + Conan 2 build sistemi — cross-platform, preset destekli,
- OpenGL 3.3 Core seçim kararımız,
- Çalışan bir SDL3 penceresi.
Önemli konuları inceledikten sonra artık çizim ve diğer konulara girmeye hazırız. Bir sonraki yazımda görüşmek üzere, bol kodlu günler.
