SDL_Renderer — a six-function floor, and a hint for what the abstraction cannot decide
Category: minimal device abstraction. Last reviewed: August 23, 2026. Pinned at b53f1b06.
The smallest honest version of "one drawing API, many devices" in this survey: fourteen backends behind a vtable whose mandatory core is six queue functions, one of which (QueueGeometry) can stand in for three of the others. It is the floor case — read for how little a seam can declare and still work, and for what it declines to abstract at all.
| Field | Value |
|---|---|
| Language | C99 |
| License | zlib (LICENSE.txt) |
| Repository | libsdl-org/SDL |
| Documentation | SDL3 wiki, CategoryRender |
| Category | minimal device abstraction |
| Pinned revision | b53f1b06447cfe699e2649afc52a1a54e5f19f71 (2026-06-07) |
| Public seam header | include/SDL3/SDL_render.h · backend seam SDL_sysrender.h · framework SDL_render.c |
| Version at revision | SDL 3.5.0 (development) |
| Backends shipped | 14 drivers: d3d11, d3d12, d3d, metal, ngage, opengl, opengles2, opengles, ps2, psp, vitagxm, vulkan, gpu, software |
| Public draw calls | 19 drawing entry points (24 SDL_Render* functions in all) |
| Mandatory driver core | 6 queue functions + RunCommandQueue |
| Text support | none, except an explicitly-disclaimed 8×8 debug bitmap font |
Overview
What it solves
Fill rectangles, blit textures and draw triangles onto whatever the platform provides — Direct3D 12, Metal, a PlayStation 2, or a scanline software rasterizer when nothing else is there. The software driver (SDL_render_sw.c) is a first-class member of the driver table rather than a degraded mode, which is what makes SDL relevant here: like sparkles:ui, it has a backend that genuinely cannot do what the others can.
Design philosophy
The header states its own ceiling and points elsewhere when you exceed it — SDL_render.h:
* This API supports the following features:
*
* - single pixel points - single pixel lines - filled rectangles
* - texture images - 2D polygons
* ...
* This API is designed to accelerate simple 2D operations. You may want more
* functionality such as 3D polygons and particle effects, and in that case
* you should use SDL's OpenGL/Direct3D support, the SDL3 GPU API, or one of
* the many good 3D engines.Five primitives, a named exit. There is no path object, no gradient, no stroke width, no antialiasing control, no font — a case-insensitive search of SDL_render.h for linewidth, antialias, gradient or stroke returns zero hits. The abstraction's scope is a decision, published, in the first paragraph a backend author reads.
How it works
Drawing is two-phase. A public SDL_Render* call lowers to one or more SDL_RenderCommand values appended to a per-frame linked list; the driver's RunCommandQueue walks that list once at flush. The command is a tag plus a union (SDL_sysrender.h):
typedef enum
{
SDL_RENDERCMD_NO_OP, SDL_RENDERCMD_SETVIEWPORT, SDL_RENDERCMD_SETCLIPRECT,
SDL_RENDERCMD_SETDRAWCOLOR, SDL_RENDERCMD_CLEAR, SDL_RENDERCMD_DRAW_POINTS,
SDL_RENDERCMD_DRAW_LINES, SDL_RENDERCMD_FILL_RECTS, SDL_RENDERCMD_COPY,
SDL_RENDERCMD_COPY_EX, SDL_RENDERCMD_GEOMETRY
} SDL_RenderCommandType; // reformatted here; one per line upstream
typedef struct SDL_RenderCommand
{
SDL_RenderCommandType command;
union
{
struct { size_t first; SDL_Rect rect; } viewport;
struct { bool enabled; SDL_Rect rect; } cliprect;
struct { size_t first; size_t count; float color_scale; SDL_FColor color;
SDL_BlendMode blend; SDL_Texture *texture; /* … */ } draw;
struct { size_t first; float color_scale; SDL_FColor color; } color;
} data;
struct SDL_RenderCommand *next;
} SDL_RenderCommand;Eleven tags, four union arms: the draw arm serves six of them. Variable payload — the vertices — is not in the command at all. It lives in a per-frame arena on the renderer, grown by SDL_AllocateRenderVertices and referenced by the size_t first offset; FlushRenderCommands hands the driver (renderer->vertex_data, renderer->vertex_data_used) alongside the command list and then resets vertex_data_used = 0 (SDL_render.c).
The backend seam is struct SDL_Renderer itself — a plain struct whose first ~30 members are function pointers, immediately followed by the renderer's own state; there is no separate vtable type and no capability struct. Its mandatory core is stated as an assertion, with a comment doing the work an interface declaration would:
static SDL_INLINE void VerifyDrawQueueFunctions(const SDL_Renderer *renderer)
{
/* all of these functions are required to be implemented, even as no-ops, so we don't
have to check that they aren't NULL over and over. */
SDL_assert(renderer->QueueSetViewport != NULL);
SDL_assert(renderer->QueueSetDrawColor != NULL);
SDL_assert(renderer->QueueDrawPoints != NULL);
SDL_assert(renderer->QueueDrawLines != NULL || renderer->QueueGeometry != NULL);
SDL_assert(renderer->QueueFillRects != NULL || renderer->QueueGeometry != NULL);
SDL_assert(renderer->QueueCopy != NULL || renderer->QueueGeometry != NULL);
SDL_assert(renderer->RunCommandQueue != NULL);
}Three of the six are || QueueGeometry — triangles are the declared universal lowering target, so a new backend can implement points, geometry and a command-queue walk and get lines, rects and blits for free. "Required to be implemented, even as no-ops" is the software driver's literal practice (QueueSetViewport and QueueSetDrawColor are both SW_QueueNoOp, SDL_render_sw.c). Everything else — GetOutputSize, SupportsBlendMode, SetRenderTarget, GetMetalLayer — is optional-by-NULL, checked at each use.
Q1 — measurement units, and who answers
Answered by omission, and the omission is the design. SDL_Renderer has no text primitive and no measurement call. It has exactly one text-shaped function, and SDL_render.h spends a paragraph disowning it:
* this is a convenience function for debugging, with severe limitations, and
* not intended to be used for production apps and games.
* ...
* - It accepts UTF-8 strings, but will only renders ASCII characters.
* - It has a single, tiny size (8x8 pixels). ...
* - It uses a simple, hardcoded bitmap font. ...
* For serious text rendering, there are several good options, such as
* SDL_ttf, stb_truetype, or other external libraries.Advance is a compile-time constant. SDL_RenderDebugText steps the pen with curx += SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE;, where that macro is 8 (SDL_render.c, SDL_render.h). Measurement, had SDL offered it, would be strlen(s) * 8 — structurally the same fiction as SkiaCanvas.measure returning cellsOf(text) under friction §1.
The difference is that SDL names it a fiction in the header, refuses to build on it, and keeps it out of the driver seam entirely — no driver ever sees a string. That is F1 pushed to its limit, with a caveat sparkles:ui must take seriously: SDL can refuse text because its consumer is an application that will link SDLttf. A _toolkit cannot refuse; it must own the seam between measurement and drawing, because that seam is where its layout lives.
Q2 — is the contract stated in one place?
Partially, and in four different registers — which is more structure than isCanvas has, and less than Qt's.
A mandatory floor, asserted.
VerifyDrawQueueFunctionsabove. It is not a type-level declaration and it only fires in a debug build, but it is written down in one place and names the substitution rule.A per-domain negotiable set with a refusable answer. Blend modes are split into a required floor and a queried remainder (
SDL_render.c):cswitch (blendMode) { // These are required to be supported by all renderers case SDL_BLENDMODE_NONE: case SDL_BLENDMODE_BLEND: case SDL_BLENDMODE_BLEND_PREMULTIPLIED: case SDL_BLENDMODE_ADD: case SDL_BLENDMODE_ADD_PREMULTIPLIED: case SDL_BLENDMODE_MOD: case SDL_BLENDMODE_MUL: return true; default: return renderer->SupportsBlendMode && renderer->SupportsBlendMode(renderer, blendMode); }Seven modes are guaranteed; anything from
SDL_ComposeCustomBlendMode(SDL_blendmode.h) is negotiated, andSDL_SetRenderDrawBlendModereturnsSDL_Unsupported()rather than silently approximating. A driver that leaves the hookNULLrefuses everything above the floor — the software,ps2andpspdrivers do exactly that, while the other eleven implement it. Texture formats work the same way, declared per driver viaSDL_AddSupportedTextureFormatand exposed asSDL_PROP_RENDERER_TEXTURE_FORMATS_POINTER.A property bag,
SDL_GetRendererProperties— nominally the capability channel; read against the source, mostly not one. Of the thirty-oddSDL_PROP_RENDERER_*keys inSDL_render.h, onlyMAX_TEXTURE_SIZE_NUMBER,TEXTURE_FORMATS_POINTER,TEXTURE_WRAPPING_BOOLEANand the three HDR keys describe what the renderer can do. The rest are identity and escape hatches (D3D12_DEVICE_POINTER,VULKAN_PHYSICAL_DEVICE_POINTER,GPU_DEVICE_POINTER) — stringly typed, driver-specific, and dominantly used to hand the application the native device so it can leave the abstraction.A hint, for what the abstraction could not decide — the honest one. Because the platforms disagree about line rasterization, SDL does not pick; it exposes the choice as an environment variable (
SDL_hints.h):c* - "0": Use the default line drawing method (Bresenham's line algorithm) * - "1": Use the driver point API using Bresenham's line algorithm (correct, * draws many points) * - "2": Use the driver line API (occasionally misses line endpoints based on * hardware driver quirks * - "3": Use the driver geometry API (correct, draws thicker diagonal lines)SDL_RenderLinesthen dispatches onrenderer->line_methodbetween triangle emission,RenderLinesWithRectsF, and the driver's nativeQueueDrawLines(SDL_render.c). Software renderers are pinned toSDL_RENDERLINEMETHOD_LINES"for speed" regardless of the hint.
IMPORTANT
Point 4 is the finding. A device abstraction over genuinely disagreeing devices has residue that no capability enum can express — SDL's residue is "which of three visually different, all-defensible lines do you want?" — and SDL's answer is to make it a named, documented, user-settable policy rather than to hide it. sparkles:ui has the same residue at every RuleEdge and has no name for it.
Q3 — semantic operations, and where degradation lives
The public API is semantic-ish; the seam is not, and the gap between them is where all lowering happens. SDL_RenderTexture9Grid is a nine-patch — a frame/border widget concept — and it reaches no driver: it is nine SDL_RenderTexture calls in SDL_render.c. SDL_RenderRect (an outline) becomes SDL_RenderLines over five points. SDL_RenderTextureTiled, SDL_RenderTextureRotated, SDL_RenderDebugText — all framework-side. Nineteen public drawing calls collapse into six queue functions, and a driver can never learn that a nine-grid was intended.
This answers F4's "where the lowering lives" axis as framework, by construction, and more cleanly than Qt — QPainter emulates only when the engine declines, whereas SDL lowers unconditionally. The price inverts friction §3: no backend can render a nine-patch natively even where the hardware would do it better, because the seam has no word for it.
Q4 — command shape
A tagged union, coarsely arm-grouped. Eleven tags share four payload shapes, because the arms are grouped by payload shape rather than by tag: DRAW_POINTS, DRAW_LINES, FILL_RECTS, COPY, COPY_EX and GEOMETRY all carry {first, count, color, blend, texture, …} and differ only in how RunCommandQueue interprets the vertex range.
That refines F3, which holds that reifying the stream is right while its encoding stays a live trade. SDL shows an encoding does not need one arm per kind: eleven tags, four shapes.
DrawOp takes the other route — a closed sum with one arm per kind, eight of them: FillRect, TextRun, Glyph, Line, Rule, Scrollbar, PushClip, PopClip. Four of those describe nearly the same record. TextRun, Glyph and Line each carry a position, an Ink and a Slot, and FillRect differs only in storing its own colour fields plus a const(BoxChrome)* where the others store an Ink. Grouped SDL's way they are one arm with a discriminant, and Scrollbar's fourteen fields are the one payload that genuinely differs.
The price is paid in the accessors. visualOf reconstructs a Visual per payload precisely because a fill reports box chrome and a run reports text chrome; merged, that split moves inside the arm and the documented lossiness stops being legible from the type. Each of DrawOp's seventeen member accessors — kind, rect, text, to, slot, the seven bar* ones — is one match! arm per kind returning either the field or a neutral value; merged, each becomes a match! over five arms with an inner switch on the discriminant, and DrawOp.kind, derived by an eight-arm match! so that no stored tag can disagree with the payload, reads that discriminant instead. Exhaustiveness moves from the sum's arms to a final switch the writer has to remember. The 64-byte budget is indifferent: it is a bound rather than an equality, and the merged arm stays governed by TextRun, the widest payload either way. RecordingCanvas is indifferent too — the ops stay values and stay pairwise comparable.
The argument therefore stands as a maintenance one: arm count is a cost the size budget does not price, and SDL is the survey's demonstration that a seam can group by shape and stay legible. It also shows the discipline that makes a shared arm safe: SDL_RENDERCMD_NO_OP exists as a first-class tag, and a partially-built command is retracted by retagging it rather than by unwinding the queue (cmd->command = SDL_RENDERCMD_NO_OP; when a driver's Queue* hook fails).
Q5 — sub-unit placement, and the coarse-unit toolkit
SDL is the only surveyed subject that has our problem and answers it with a first-class API rather than by having float coordinates and moving on. It has both. Coordinates are float throughout (SDL_FRect, SDL_FPoint), so sub-pixel placement is expressible; but an application that wants a coarse fixed unit declares one, once, with SDL_SetRenderLogicalPresentation(renderer, w, h, mode), choosing how it maps to the device — STRETCH, LETTERBOX, OVERSCAN or INTEGER_SCALE (SDL_render.h). Every subsequent draw call is in logical units; SDL_RenderViewState carries logical_scale, logical_offset and a precomputed current_scale, applied before queueing. The mapping is invertible and the inverse is public — SDL_RenderCoordinatesFromWindow and SDL_ConvertEventToRenderCoordinates rewrite input events into logical space, a service sparkles:ui needs and does not name.
And the escape from the coarse unit is per-frame, not per-application:
* You can disable logical coordinates by setting the mode to
* SDL_LOGICAL_PRESENTATION_DISABLED, and in that case you get the full pixel
* resolution of the render target; it is safe to toggle logical presentation
* during the rendering of a frame: perhaps most of the rendering is done to
* specific dimensions but to make fonts look sharp, the app turns off logical
* presentation while drawing text, for example.IMPORTANT
That sentence is the closest thing in the survey to a direct answer for a cell-space toolkit on a pixel backend. Draw the chrome in the coarse unit; turn the coarse unit off for the text. It sharpens F6, which holds that continuous coordinates relocate the sub-unit problem rather than dissolving it: SDL has continuous coordinates and keeps a declared coarse unit, because the coarse unit is what the content was authored against. The two are orthogonal, and sparkles:ui conflates them.
Note also what SDL never does: enumerate positions. There is no RuleEdge, because a hairline is SDL_RenderLine at whatever float coordinates the caller computed after asking the view for its scale.
Q6 — resolved or semantic styling
Fully resolved, and — the interesting part — carried as stream state, not as op payload. Colour reaches the driver as its own command, SDL_RENDERCMD_SETDRAWCOLOR, and QueueCmdSetDrawColor emits one only when the value actually changed, comparing against renderer->last_queued_color (SDL_render.c). The clip rect is deduplicated the same way against last_queued_cliprect. A run of a thousand same-coloured rects queues one colour command.
Colourspace is resolved above the seam too — SDL_RenderingLinearSpace decides from the render target and SDL_ConvertToLinear converts before queueing, with color_scale (the HDR/SDR-white-point factor) riding on the command. Nothing semantic — no slot, no role — reaches a driver.
This is a fifth answer to Q6 and the cheapest surveyed. DrawOp stores the resolved appearance each primitive paints from — an Ink on the four content payloads, colour fields plus a const(BoxChrome)* on FillRect — and a Slot beside it on six of the eight payloads (friction §6); SDL carries neither on most ops, because appearance is a property of stream position. That Visual is derived from the payload through visualOf rather than stored makes the hedge F9 describes cheaper without making it a decision. The trade-off is real: a state-carrying stream is not order-independent, so ops cannot be reordered, culled individually or compared pairwise — which is exactly what RecordingCanvas and the op-stream parity harness do.
Q7 — payload ownership
Two mechanisms, neither of which is reference counting.
Bulk payload goes in a frame arena. Vertices are copied into renderer->vertex_data via SDL_AllocateRenderVertices; the command retains only size_t first and size_t count. The arena is realloc-growable, so the header warns that "Pointers returned here are only valid until the next call" (SDL_sysrender.h) — offsets, not pointers, are the durable handle. FlushRenderCommands resets vertex_data_used to zero and returns the command list to render_commands_pool for reuse next frame.
Borrowed references are protected by a generation counter. A queued SDL_RENDERCMD_COPY holds a raw SDL_Texture *. SDL stamps texture->last_command_generation when a texture is queued and, before any operation that would mutate it, checks:
static bool FlushRenderCommandsIfTextureNeeded(SDL_Texture *texture)
{
SDL_Renderer *renderer = texture->renderer;
if (texture->last_command_generation == renderer->render_command_generation) {
// the current command queue depends on this texture, flush the queue now before it changes
return FlushRenderCommands(renderer);
}The same guard exists for palettes and GPU render states — a write-barrier answer to the borrowed-payload hazard: the borrow stays a borrow, and mutating a still-referenced payload forces the frame to be submitted first.
NOTE
This is F8 at its most interesting: every subject copies, refcounts or arena-allocates, and SDL borrows a texture handle while staying correct by making the borrow's invalidation observable. For friction §7 — DrawOp.text as a slice — CmdBuffer.textRun already copies each run into sparkles.ui.arena's FrameArena, so the bytes belong to the arena and a scope source is safe. What SDL adds is the shape of the handle: an offset into the arena rather than a slice of it, which is what would make DrawOp trivially copyable, sendable across a thread, and free of the launder cast and the @trusted opAssign that friction §4 records.
Q8 — extent query
The device declares it, with a three-step fallback (SDL_render.c): SDL_GetRenderOutputSize calls the driver's optional GetOutputSize hook; failing that asks the window (SDL_GetWindowSizeInPixels); failing that returns true with zero, commented "this might be an offscreen-only renderer". SDL_GetCurrentRenderOutputSize answers from the current target's SDL_RenderViewState.pixel_w/pixel_h, so a texture target reports its own size. Nothing derives extent from the command stream, and nothing could — the stream is flushed and its arena reset several times per frame. That bounds F7: surface, layout and ink extent are three different questions, and a subject whose stream does not survive the frame can answer only the first, and only from the device. SDL adds the offscreen case: "no output size" is a legitimate state, returned as true with zero.
Strengths
- The floor is written down and machine-checked, in one place, naming its substitution rule (
|| QueueGeometry) — and triangles are the declared universal lowering target, decoupling seam width from backend cost. - Framework-side lowering keeps the seam narrower than the API: nineteen drawing calls, six queue hooks; new convenience operations cost backends nothing.
- Negotiation is per-domain and refusable — a required floor plus a queried remainder, refusal being a real
false+SDL_Unsupported(). - Payload lifetime is solved without ownership transfer — a frame arena for bulk data, a generation counter for borrowed handles.
- The coarse-unit problem has a named API, invertible for input and toggleable mid-frame; and the residue nobody can abstract is exposed as documented policy rather than hidden.
Weaknesses
- The capability channel is stringly typed and mostly not about capabilities: not exhaustively matchable, not compile-time checkable, dominated by native-handle escape hatches.
- The floor is asserts and a comment, not a type. A release build of a broken driver crashes on a
NULLcall rather than failing to compile, and optional-by-NULLscales badly across ~30 ungrouped function pointers. - State-carrying commands are not order-independent, so the stream cannot be reordered, diffed or replayed out of context.
- Nothing semantic survives to the backend, so a backend can never beat the framework's lowering — a nine-patch cannot become a native one; and clipping is a single mandatory rect, with no stack and no opt-out.
- No text, by design — coherent for a game library, unavailable to a UI toolkit.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
Six mandatory queue hooks, three satisfiable by QueueGeometry | A new backend is cheap; the porting floor is one page | The floor is an SDL_assert, invisible to the type system and absent in release builds |
| Command as tag + 4-arm union, vertices in a side arena | Fixed-size commands, pooled and reused; payload lifetime is the arena's | The union arm does not determine validity — RunCommandQueue must trust the tag |
Semantic operations lowered in the framework (RenderTexture9Grid) | Backends stay six functions wide however rich the public API grows | No backend can render a nine-patch, tiled fill or outline natively |
Blend modes: a required floor plus SupportsBlendMode | Callers can rely on seven modes unconditionally and negotiate the rest | A driver that omits the hook refuses everything above the floor with no way to say why |
| Line rasterization chosen by hint, not by capability | The devices genuinely disagree and none of the three answers is wrong | Rendering differs between machines by environment variable; goldens are not portable |
| Coarse logical unit as a view transform, toggleable mid-frame | Content authored to a fixed size scales to any output, and input maps back | Two coordinate systems live at once; the app must know which one it is in |
| Borrowed handles guarded by a generation counter | No refcount traffic, no interning, and mutation-during-queue is impossible | Mutating a queued texture silently forces a flush — a latency cliff with no diagnostic |
| No text at all | Shaping, fallback and hinting are a different library's problem | Unavailable to a toolkit, whose layout is the measurement seam |
Bearing on the proposal
- Take the offset, not just the arena (friction §7).
CmdBuffer.textRuncopies each run into aFrameArena, so the copy half of SDL's answer is in place; whatTextRunkeeps is aconst(char)[]into that arena, and the slice is what costs thelaundercast and pins the operation to the frame. SDL's durable handle is(first, count)intorenderer->vertex_data, deliberately not a pointer, because the arena reallocs. An offset pair onTextRunis the stronger form F8 names — trivially copyable, sendable to another thread — at the cost of giving every reader ofop.textan arena to resolve against, includingRecordingCanvas, which interns on the collected heap so its ops outlive the call that drew them.UI-O4is open on exactly that retain boundary. - State the floor in one place, with substitution rules.
VerifyDrawQueueFunctionsis what friction §2 is missing — not a capability enum, but one declaration saying which primitives are mandatory and which are satisfiable by another.isCanvasshould name all eightOpKinds and markrule,scrollbarand the clip pair optional in the concept, with each one's fallback. D can do this properly where C could only assert. - Make degradation refusable per domain, not globally.
IsSupportedBlendModeconfirms F5 from a second subject and adds that the query belongs to the feature domain, not to one global "supported?" call: a golden test asking for a true hairline should be refused by the hairline domain, not by a capability bitmask. - Adopt a declared coarse unit as a view transform, and let it be toggled.
SDL_SetRenderLogicalPresentationplus the mid-frame-toggle note is the least disruptive route out of friction §1 and §5 that keeps cell-space layout: the toolkit lays out in cells, the canvas owns the cell→device transform, and text (or a focus ring, or a two-pixel inset) opts out per op. This confirms F6, which holds that continuous coordinates relocate the sub-unit problem rather than dissolving it — SDL has continuous coordinates and keeps a coarse declared unit, because the unit encodes authorial intent rather than a rendering limitation. - Group the sum-type arms by payload shape, not by kind. F3 leaves the encoding a live trade; SDL's eleven-tags-four-arms command shows the arm count can sit far below the kind count.
FillRect,TextRun,GlyphandLineshare a shape and could share one arm, withScrollbarthe outlier that earns its own — friction §4 and §3 resolved together. The bill isvisualOfand the seventeen member accessors, each of which trades onematch!arm per kind for an inner switch on a discriminant, plusDrawOp.kind, which is derived from the arm rather than read from a stored tag. The claim stands on maintenance, which the 64-byte budget does not price. - Treat "appearance as stream state" as a fork, not a recommendation (friction §6). SDL's
SETDRAWCOLORdedup is the cheapest Q6 answer surveyed and is incompatible with treating each op as an independently comparable value — which is whatRecordingCanvasand the parity harness rely on, and what F12 says reification buys. Decide what the harness needs first. - Name the residue.
SDL_HINT_RENDER_LINE_METHODsays that where backends legitimately disagree about fidelity, the seam should expose the choice under a name rather than pick silently per backend.RuleEdge's real content is a fidelity policy ("one device pixel" vs "a whole cell") — the same instinct F6 reaches from Notcurses. Two subjects converge on it. - Do not copy the property bag. Untyped, non-exhaustive, mostly escape hatch. The real capability declarations in SDL are the asserted floor, the per-domain queries, and the driver's texture-format list — not
SDL_GetRendererProperties.
Sources
All citations pinned to commit b53f1b06447cfe699e2649afc52a1a54e5f19f71 of libsdl-org/SDL (local clone; revision from git rev-parse HEAD, every path verified with git cat-file -e). SDL version at that revision: 3.5.0 development.
include/SDL3/SDL_render.h— the public seam: the API scope statement,SDL_RendererLogicalPresentation,SDL_GetRendererPropertiesand theSDL_PROP_RENDERER_*keys,SDL_RenderGeometryRaw,SDL_RenderTexture9Grid,SDL_RenderDebugText.src/render/SDL_sysrender.h— the backend seam:struct SDL_Renderer,SDL_RenderCommandType,SDL_RenderCommand,SDL_RenderViewState,SDL_RenderDriver,SDL_AllocateRenderVertices.src/render/SDL_render.c— the framework:render_drivers[],VerifyDrawQueueFunctions,IsSupportedBlendMode,SDL_GetRenderLineMethod,SDL_RenderLines,SDL_RenderTexture9Grid,QueueCmdSetDrawColor,QueueCmdSetClipRect,FlushRenderCommands,FlushRenderCommandsIfTextureNeeded,SDL_GetRenderOutputSize.src/render/software/SDL_render_sw.c— the fallback backend's vtable wiring (SW_QueueNoOp);include/SDL3/SDL_hints.h—SDL_HINT_RENDER_LINE_METHOD;include/SDL3/SDL_blendmode.h—SDL_ComposeCustomBlendMode;src/render/vulkan/SDL_render_vulkan.candsrc/render/gpu/SDL_render_gpu.c— two of the eleven drivers implementingSupportsBlendMode;LICENSE.txt— zlib.