SDLPainter 1.2.0
SDL3 + OpenGL/Vulkan 2B ç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 *(both in Turkish)*.


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.

</blockquote>

3. Data flow: one <tt>DrawRect</tt> 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&lt;Vertex&gt;<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 *(in Turkish)*.

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, Size
│ ├── 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