|
SDLPainter 1.4.0
SDL3 + OpenGL/Vulkan 2D çizim kütüphanesi
|
Türkçe sürüm | English
This document is not SDLPainter's technical feature list; it covers why the library is designed the way it is. Architectural decisions, design patterns, performance choices and scope management are examined from a software engineering perspective.
The library consists of four layers and each layer has a single responsibility:
In practice this separation means: when replacing OpenGLRenderer with VulkanRenderer, not a single line changes in the Painter, Tessellator or RenderState files. The boundary of change is the IRenderer interface.
The backend choice is resolved in the constructor, not at compile time. Writing Painter(window, RendererBackend::kVulkan) is enough; the upper layers are completely unaware of the backend.
Adding a new backend comes down to writing a class that implements IRenderer — no existing code needs to change (Open/Closed Principle).
The Save() / Restore() pair works with push/pop semantics on a std::vector<RenderState>. RenderState carries the transform matrix, pen, brush, opacity and clip rect together; this guarantees state consistency in nested transform blocks.
A DrawCircle call does not go to the GPU immediately. RenderBatcher accumulates vertices in a buffer; Flush() is triggered only when:
This makes the number of draw calls independent of the number of shapes in the scene; performance scales with the frequency of state changes, not with individual GPU commands.
All methods of the Tessellator class are static and hold no state. TessellateFilledCircle called with the same parameters always produces the same vertex list. This makes it possible to write unit tests without a GPU: input coordinates → the expected vertex count and values are verified.
| Decision | Choice | Why |
|---|---|---|
| Image loading | stb_image | Header-only, zero transitive dependencies; SDL_image needs extra installation |
| OpenGL loading | GLAD | Only the 3.3 Core functions are included; no full GLEW load needed |
| Font | SDL_ttf | Optional; not part of the build until Phase 4 |
| Vulkan | Optional Conan package | With with_vulkan=False the Vulkan loader is never downloaded |
In conanfile.py the dependencies are split into required and optional ones. The Vulkan loader, SDL_ttf and GTest are pulled in only when the relevant option is enabled. This split keeps CI time and image size small.
In the OpenGL 3.3 Core Profile glLineWidth(x > 1.0f) is deprecated, and its maximum value, which varies from driver to driver, cannot be guaranteed. Thick lines are instead drawn with TessellateThickLine, which turns every segment into two triangles (a quad):
This keeps the pixel width consistent across Linux, Windows and a headless Docker environment. The Tessellator performs this conversion; the renderer only receives a list of triangles.
Circles and ellipses are drawn with a segment count that depends on the radius:
Small circles use few segments (efficiency), large circles many (quality). A fixed segment count cannot strike this balance.
TessellateFilledPolygon checks convexity first. Convex polygons are handled with an O(n) triangle fan; concave ones are routed to O(n²) ear clipping. For typical polygon sizes in 2D UI drawing (n < 200) the difference is negligible.
Since Tessellator is stateless and independent of IRenderer, its unit tests run purely on the CPU. The vertex count generated for a circle, the coordinates of the thick line quad corners and the triangle count from ear clipping can be verified by direct numeric comparison.
Since RenderBatcher and Painter work purely through the interface, tests can use a mock IRenderer instead of real OpenGL/Vulkan. The number of draw calls and their parameters become testable.
The linux-debug-asan CMake preset enables AddressSanitizer and UBSan. In the CI pipeline a separate test:unit:asan job runs through this preset. Memory errors and undefined behavior are caught at this stage even when the normal tests pass.
Features deliberately left out of scope are documented. The list describes today's limits; Path/Bézier curves and gradient fills, which were once listed here, were added in this release (Path + DrawPath/FillPath, Brush::LinearGradient/RadialGradient) and removed from the list.
| Feature | Status | Rationale |
|---|---|---|
| Fills with holes (even-odd / nonzero) | Out of scope | Ear clipping does not support holes; it would require Delaunay CDT. FillPath fills every subpath independently |
| Multi-stop and conical gradients | Out of scope | A two-stop transition comes for free from vertex colors; anything more means shader and batch cost |
| Path-based clipping | Out of scope | Clipping is done with scissor; a path clip would require a stencil buffer |
| Full anti-aliasing | Partial (MSAA) | Analytic AA requires a path-based rasterizer |
| Print / PDF output | Out of scope | The library is designed for drawing to the screen |
Our important decisions in the project are each tied to an ADR file:
| ADR | Decision |
|---|---|
| ADR-001 | 3.3 Core Profile instead of OpenGL 4.5 or ES |
| ADR-003 | Geometry quads instead of glLineWidth |
| ADR-004 | Backend-agnostic design of the Tessellator |
| ADR-005 | stb_image instead of SDL_image |
| ADR-006 | Ear clipping for triangulation |
ADR format: context → alternatives considered → decision → rationale → consequences. It answers "why was it done", not "what was done".
The CMakePresets.json + Conan 2 combination ensures that exactly the same toolchain and dependency versions are used when a developer runs the linux-debug preset, when the CI pipeline runs and when the Docker container runs. It structurally eliminates the "works on my machine" problem.
The ci Docker stage is started with the SDL_VIDEODRIVER=offscreen environment variable. In this mode SDL3 does not look for a real display server; the OpenGL context is created offscreen. Since GitLab runners run headless, without this the renderer tests would crash at context creation.
The second advantage of the multi-stage structure is cache efficiency: the ci and windows-cross stages are based on builder; the GCC, Clang and Conan installation layers are not downloaded again on every build.
The quality:clang-format job runs with --Werror and breaks the pipeline; no code to be merged can carry a format violation. clang-tidy, on the other hand, is marked allow_failure: true in the early phases — silencing every warning in the early stages slows development down. This setup is a deliberate trade-off.
| Principle | Its counterpart in SDLPainter |
|---|---|
| Single Responsibility | The Tessellator knows geometry, the renderer knows the API, Painter knows style |
| Open/Closed | New backend → just implement IRenderer, existing code does not change |
| Liskov Substitution | OpenGLRenderer and VulkanRenderer are interchangeable for Painter |
| Dependency Inversion | Painter depends on the IRenderer abstraction, not on a concrete renderer |
| Scope Control | Documented limits; no speculative features |