|
SDLPainter 1.2.0
SDL3 + OpenGL/Vulkan 2D çizim kütüphanesi
|
Türkçe sürüm | English
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).
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
| 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.
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.
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 (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.
Note there is no transform.h: the transform is a glm::mat3 held inside RenderState, not a class of its own (ADR-007).
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).
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.