SDLPainter 1.4.0
SDL3 + OpenGL/Vulkan 2D çizim kütüphanesi
Yüklüyor...
Arıyor...
Eşleşme Yok
architecture

Türkçe sürüm | English

SDLPainter — Architecture Overview

This document describes SDLPainter's layered architecture, the dependencies between components and the flow of data, from a high level. For class-level detail see the Class Diagram; for runtime sequences see the Flow Diagrams.


1. The layers

SDLPainter is four independent layers. Each layer depends only on the interface of the one below it, never on a concrete implementation.

flowchart TB
    subgraph user["User application"]
        APP["Demo / editor / game"]
    end

    subgraph api["1) Public API layer"]
        PAINTER["Painter<br/>"]
        STYLE["Pen / Brush / Color<br/>Image / Font"]
    end

    subgraph mid["2) Geometry layer (backend-agnostic)"]
        TESS["Tessellator<br/>(shape → vertices)"]
        STATE["RenderState<br/>(transform stack entry)"]
        BATCH["RenderBatcher<br/>(draw call coalescing)"]
    end

    subgraph backend["3) Backend abstraction"]
        IRENDER["IRenderer<br/>(pure interface)"]
    end

    subgraph impl["4) Backend implementations"]
        OGL["OpenGLRenderer<br/>(GLAD + GLSL 330)"]
        VK["VulkanRenderer<br/>(Vulkan 1.1 + SPIR-V)"]
    end

    subgraph plat["Platform layer"]
        SDL["SDL3<br/>(window, context, input)"]
        GPU["GPU driver"]
    end

    APP --> PAINTER
    PAINTER --> STYLE
    PAINTER --> TESS
    PAINTER --> STATE
    PAINTER --> BATCH
    BATCH --> IRENDER
    TESS -.produces.-> BATCH
    OGL -.implements.-> IRENDER
    VK -.implements.-> IRENDER
    OGL --> SDL
    VK --> SDL
    OGL --> GPU
    VK --> GPU

    classDef apiCls fill:#1f6feb,stroke:#0d3a8c,color:#fff
    classDef midCls fill:#2da44e,stroke:#1a6b30,color:#fff
    classDef backCls fill:#bf8700,stroke:#7a5a00,color:#fff
    classDef implCls fill:#cf222e,stroke:#7a0a14,color:#fff
    classDef platCls fill:#6e7781,stroke:#3a3f44,color:#fff

    class PAINTER,STYLE apiCls
    class TESS,STATE,BATCH midCls
    class IRENDER backCls
    class OGL,VK implCls
    class SDL,GPU platCls

What each layer owns

Layer Responsibility What it does NOT know
Public API user-facing API, style and transform state the GPU, vertex formats, batching
Geometry turns shapes into vertices, accumulates draw calls OpenGL/Vulkan commands
Backend abstraction the renderer contract any implementation detail
Backend impl. GPU commands, shaders, buffers, textures tessellation, style, transform stack

The concrete pay-off: swapping OpenGLRenderer for VulkanRenderer means changing one constructor argument. Not a single line changes in Painter, Tessellator, RenderBatcher or RenderState.


2. Component dependencies

Which component uses which:

graph LR
    Painter --> RenderState
    Painter --> Tessellator
    Painter --> RenderBatcher
    Painter --> IRenderer
    Painter --> Image
    Painter --> Font

    RenderState --> Mat3["glm::mat3 (transform)"]
    RenderState --> Pen
    RenderState --> Brush
    RenderState --> Rect

    Tessellator --> Vertex
    Tessellator --> TexturedVertex
    Tessellator --> Geometry["Point / Rect"]

    RenderBatcher --> IRenderer
    RenderBatcher --> Vertex
    RenderBatcher --> TexturedVertex

    Image --> stbImage["stb_image"]
    Image --> Texture["Texture (RAII)"]
    Texture --> IRenderer

    Font --> SDLttf["SDL_ttf"]
    Font --> Glyph
    Glyph --> Texture

    OpenGLRenderer -.implements.-> IRenderer
    VulkanRenderer -.implements.-> IRenderer
    OpenGLRenderer --> ShaderProgram
    OpenGLRenderer --> SDL3
    VulkanRenderer --> VkContext
    VulkanRenderer --> VkSwapchain
    VulkanRenderer --> VkFrameSync
    VulkanRenderer --> VulkanPipeline
    VulkanRenderer --> VulkanTexturedPipeline
    VulkanRenderer --> SDL3

    style IRenderer fill:#bf8700,color:#fff
    style Painter fill:#1f6feb,color:#fff
    style OpenGLRenderer fill:#cf222e,color:#fff
    style VulkanRenderer fill:#cf222e,color:#fff

Solid arrows are direct use; dashed arrows are interface implementation.


3. Data flow: one DrawRect call

The path painter.DrawRect(x, y, w, h) takes inside the library:

flowchart LR
    A["User:<br/>painter.DrawRect(...)"]
    B["Read RenderState<br/>(pen, opacity, transform)"]
    C["Tessellator::<br/>TessellateStrokedRect"]
    D["std::vector<Vertex><br/>(4 quads / 8 triangles)"]
    E["RenderBatcher::<br/>PushTriangles"]
    F{"Mode/opacity<br/>changed?<br/>OR buffer<br/>full?"}
    G["Flush():<br/>renderer.DrawTriangles"]
    H["Append to buffer<br/>(NO GPU call)"]
    I["OpenGL/Vulkan<br/>vertex upload + draw"]

    A --> B --> C --> D --> E --> F
    F -- "No" --> H
    F -- "Yes" --> G --> I
    H -.later, on flush.-> G

    style A fill:#1f6feb,color:#fff
    style I fill:#cf222e,color:#fff
    style F fill:#bf8700,color:#fff

The point: in a typical frame many DrawRect/DrawCircle calls arrive with the same opacity. Flush() is not triggered, and every vertex goes out in a single GPU draw call. Neither colour nor transform breaks a batch: both are baked into the vertex data — which is why u_model is always identity. Details: Flow Diagrams → Batch flush conditions.

Measured, not assumed — see examples/benchmarks/README.md: with a per-shape Translate+Rotate, 2000 shapes went from 2000 draw calls (9.44 ms) to 2 draw calls (0.30 ms). Painter::GetFrameStats() exposes the same counters at runtime; Application draws them on screen with F1.


4. Module map — source tree

sdl-painter/
├── include/sdl_painter/ ← Public API (what the user sees)
│ ├── painter.h · main drawing class
│ ├── pen.h, brush.h, color.h · style types
│ ├── geometry.h · Point, Rect
│ ├── image.h, texture.h · image + RAII texture wrapper
│ ├── font.h · SDL_ttf wrapper, Alignment
│ ├── renderer.h · IRenderer interface + RendererBackend enum
│ ├── version.h · compile-time version constants
│ └── vertex.h · Vertex, TexturedVertex
│
├── src/ ← Implementation (not user-facing)
│ ├── painter.cpp · API ↔ batcher/tessellator bridge
│ ├── render_batcher.{h,cpp} · draw call coalescing
│ ├── tessellator.{h,cpp} · shape → vertices
│ ├── renderer.cpp · CreateRenderer factory
│ ├── opengl/ · OpenGL 3.3 backend
│ │ ├── opengl_renderer.{h,cpp}
│ │ ├── shader_program.{h,cpp}
│ │ └── shaders/*.{vert,frag}
│ └── vulkan/ · Vulkan 1.1 backend
│ ├── vulkan_renderer.{h,cpp} · IRenderer impl
│ ├── vk_context.{h,cpp} · instance, device, queue
│ ├── vk_swapchain.{h,cpp} · swapchain + image views
│ ├── vk_frame_sync.{h,cpp} · semaphores, fences
│ ├── vk_memory.{h,cpp} · buffer/image memory
│ ├── vulkan_pipeline.{h,cpp} · untextured pipeline
│ ├── vulkan_textured_pipeline.{h,cpp}
│ ├── vulkan_buffer.{h,cpp} · vertex ring buffer
│ ├── vulkan_texture.{h,cpp} · texture + descriptor set
│ └── shaders/spirv/*.spv · compiled SPIR-V, embedded at build time
│
├── examples/ ← demo applications
├── packaging/consumer/ ← external-consumer verification project
└── tests/ ← GTest unit tests
├── mock_renderer.h · IRenderer mock
├── test_tessellator.cpp
├── test_render_batcher.cpp
└── ...

Note there is no transform.h: the transform is a glm::mat3 held inside RenderState, not a class of its own (ADR-007).

Public vs. private

  • include/sdl_painter/*.h → public, with an API/ABI stability goal.
  • src/*.h (tessellator.h, render_batcher.h) → internal; users must not include these.
  • src/opengl/*.h, src/vulkan/*.h → backend-private types.

5. Dependencies (Conan 2)

graph TB
    Painter["sdl_painter"]

    subgraph required["Required — always fetched"]
        SDL3["SDL3 (sdl/3.2.x)"]
        GLAD["GLAD (glad/0.1.x)"]
        STB["stb_image (stb/cci.*)"]
        GLM["GLM (transform matrix)"]
        TTF["SDL_ttf (sdl_ttf/3.2.x)"]
    end

    subgraph opt["Optional — enabled by an option"]
        VK["Vulkan loader/headers (with_vulkan)"]
        GTEST["GTest (build_tests)"]
    end

    Painter --> SDL3
    Painter --> GLAD
    Painter --> STB
    Painter --> GLM
    Painter --> TTF
    Painter -.opt.-> VK
    Painter -.opt.-> GTEST

    style required fill:#2da44e20,stroke:#2da44e
    style opt fill:#bf870020,stroke:#bf8700

With with_vulkan=False the Vulkan loader is never downloaded, which keeps CI time and container images small. See conanfile.py and Building from source.

The Vulkan SDK is not a build requirement: the compiled SPIR-V lives in the repository and is embedded into the library, so glslc is only needed by people editing the shader sources (ADR-009).


6. Invariants

The rules components rely on to keep the architecture coherent:

Contract Meaning
Tessellator is stateless same input → same output, always. No GPU dependency.
IRenderer is a pure interface holds no state (no transform or opacity stack); it only receives commands.
Painter owns its IRenderer unique_ptr<IRenderer>; its lifetime is the Painter's.
RenderBatcher belongs to Painter not reachable from outside; its interface is internal.
Texture handles are opaque a uint32_t the backend resolves in its own map.
Y grows downward in Painter Y = 0 is at the top; the flip for OpenGL's scissor happens in ApplyScissor.
Frames are bounded by Begin/End state accumulates between the two calls and flushes at End.
Only real GPU state breaks a batch opacity, scissor and draw mode do; colour and transform must not — they travel in the vertex data.

Break these and the architecture's benefits — testability, backend interchangeability, predictable performance — go with them.


7. Where to go next