Render 2D API
Namespace GodotECS.Render2D. Shared color/visibility types (RenderColor, RenderCustom, Culled, RenderFrustum) live in GodotECS.Render — see Render 3D API — and are not redefined here. Source: addons/godotecs/src/render2d/.
SpriteRef
public struct SpriteRef : ISharedComponent
{
public int AtlasGroupId;
}Shared component: one value = one archetype. Identifies the atlas group (texture) the entity draws with, as returned by AtlasGroupManager.Register. Changing it moves the entity across archetypes: main thread, deferred, never in a hot path or job.
SpriteFrame
public struct SpriteFrame : IComponentData
{
public int Index;
public float Time;
public float Fps;
public int Count;
}Animation cursor. Time accumulates dt; Index is derived as floor(Time · Fps) % Count (wrapped, deterministic) — see AnimSystem.Update. Never one AnimatedSprite2D per entity. Fps <= 0 or Count <= 1 freezes the cursor; Count <= 0 is treated as 1 and clamped to the atlas grid.
SpriteSize
public struct SpriteSize : IComponentData
{
public Vector2 Size;
}World-space size. The Transform2D scale derives from this (TransformSync2D.BuildTransforms / BuildRects center Size on the position).
YSort
public struct YSort : IComponentData
{
public int Layer;
}Stable sort key for YSortSystem. Ascending Layer draws first; ties keep spawn order. Never z_index per entity.
AtlasGroupManager
public sealed class AtlasGroupManager : IDisposable
{
public int Register(Texture2D tex);
public void Unregister(int groupId);
public bool TryGetTexture(int groupId, out Texture2D tex);
public MultiMesh GetMultiMesh(int groupId);
public int Count(int groupId);
public void EnsureCapacity(int groupId, int count);
public void SyncBulk2D(int groupId,
ReadOnlySpan<Transform2D> transforms,
ReadOnlySpan<Color> modulates,
ReadOnlySpan<Rect2> uvs);
public void Dispose();
}One group = one texture = one MultiMesh (TransformFormatEnum.Transform2D, UseColors = true, unit QuadMesh).
Register(tex)— throwsObjectDisposedExceptionif disposed,ArgumentNullExceptioniftexis null. Creates theMultiMeshwith capacity 64 andVisibleInstanceCount = 0. Returns the new group id (monotonic from 1).Unregister(groupId)— removes texture and mesh entries (no throw on unknown id;Dictionary.Removesemantics).TryGetTexture(groupId, out tex)—falsefor unknown groups.GetMultiMesh(groupId)— indexer access; throwsKeyNotFoundExceptionfor unknown groups. Attach the result to aMultiMeshInstance2Dwith the same texture.Count(groupId)— currentVisibleInstanceCount, or 0 for unknown groups.EnsureCapacity(groupId, count)— throwsArgumentOutOfRangeExceptionforcount < 0or unknown group. Grows by doubling; never shrinks.SyncBulk2D(groupId, transforms, modulates, uvs)— throwsArgumentOutOfRangeExceptionfor unknown groups,ArgumentExceptionunless all three spans share one length. Grows capacity as needed, setsVisibleInstanceCount = n, then writes one transform + one color per instance. UVs are length-validated; they have no per-instance 2DMultiMeshslot and are resolved in-shader or viaBatchCanvas. No allocations in the loop.Dispose()— idempotent; clears meshes and textures.
BatchCanvas
public sealed class BatchCanvas : IDisposable
{
public int LayerCount => ...;
public Rid GetLayerItem(int layer);
public void ClearLayer(int layer);
public void Clear();
public void DrawLayer(int layer, Texture2D tex,
ReadOnlySpan<Rect2> rects, ReadOnlySpan<Color> modulates);
public void DrawLayerRegion(int layer, Texture2D tex,
ReadOnlySpan<Rect2> dstRects, ReadOnlySpan<Rect2> srcUvs, ReadOnlySpan<Color> modulates);
public void Dispose();
}Heterogeneous path: one RenderingServer canvas item (Rid) per Layer. Main thread only (sync point); headless no-op safe.
LayerCount— number of live layer items.GetLayerItem(layer)— returns the valid cachedRidor lazily creates one viaRenderingServer.CanvasItemCreate(). ThrowsObjectDisposedExceptionwhen disposed.ClearLayer(layer)/Clear()—CanvasItemClearone/all valid layers. Call at the start of the frame; draws accumulate otherwise.DrawLayer(layer, tex, rects, modulates)— throwsArgumentNullExceptionfor nulltex,ArgumentExceptionunlessrects.Length == modulates.Length. One texture per call; issue more calls to accumulate. IssuesCanvasItemAddTextureRect(item, rect, texRid, false, modulate)per entry.DrawLayerRegion(layer, tex, dstRects, srcUvs, modulates)— same guards (all three spans equal length). ConvertssrcUvs(0..1, as written byAnimSystem) to pixels viatex.GetWidth()/GetHeight()and issuesCanvasItemAddTextureRectRegionper entry.Dispose()— idempotent;FreeRids every valid item and clears the map.
AnimSystem
public static class AnimSystem
{
public static Rect2 UvFor(int index, int columns, int rows);
public static void Update(Span<SpriteFrame> frames, Span<Rect2> uvs,
float dt, int columns, int rows);
}Pure CPU math on spans. No Godot API, zero allocations.
UvFor(index, columns, rows)— throwsArgumentOutOfRangeExceptionforcolumns/rows <= 0. Wrapsindexintocolumns × rows(handles negatives) and returns the row-major cell rect in 0..1 UV space (col = idx % columns,row = idx / columns).Update(frames, uvs, dt, columns, rows)— throws on bad grid orframes.Length != uvs.Length. Per entry: clampcountto1..total; ifFps > 0 && count > 1 && dt != 0,Time += dt(floored at 0) andIndex = (int)(Time · Fps) % count; single-frame entries getIndex = 0; always writesuvs[i] = UvFor(Index, columns, rows).
TransformSync2D
public static class TransformSync2D
{
public static void BuildTransforms(ReadOnlySpan<Vector2> positions,
ReadOnlySpan<float> rotations, ReadOnlySpan<Vector2> scales,
Span<Transform2D> dst);
public static void BuildRects(ReadOnlySpan<Vector2> positions,
ReadOnlySpan<Vector2> sizes, Span<Rect2> dst);
public static void Sync(AtlasGroupManager mgr, int groupId,
ReadOnlySpan<Transform2D> transforms,
ReadOnlySpan<Color> modulates,
ReadOnlySpan<Rect2> uvs);
}Final bulk sync (presentation, last, main thread). Build* is pure math (bench-measured, value types only); Sync is the single renderer touchpoint with compacted buffers — never one call per entity.
BuildTransforms—dst[i] = Transform2D((c·sx, s·sx), (−s·sy, c·sy), pos)withc/s = cos/sin(rotation). ThrowsArgumentExceptionif any source is shorter thandst.BuildRects—dst[i] = Rect2(pos − size/2, size). Same length guard.Sync— throwsArgumentNullExceptionfor nullmgr; forwards tomgr.SyncBulk2D.
YSortSystem
public static class YSortSystem
{
public static void Sort(Span<int> order, ReadOnlySpan<YSort> layers, Span<int> scratch);
public static void Sort(Span<int> order, ReadOnlySpan<YSort> layers);
public static bool IsSorted(ReadOnlySpan<int> order, ReadOnlySpan<YSort> layers);
}Stable ascending sort of the dense index array order (init 0..n-1) by layers[order[i]].Layer. Iterative bottom-up merge sort, O(n log n), deterministic; <= keeps the left run first (stable on ties).
Sort(order, layers, scratch)— zero-alloc hot path. ThrowsArgumentExceptionifscratchis shorter thanorder,ArgumentOutOfRangeExceptionif anyorder[i]is outsidelayers.Sort(order, layers)— allocates the scratch internally; tests/tooling only, not the hot path.IsSorted(order, layers)— verification predicate, no mutation.