Skip to content

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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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) — throws ObjectDisposedException if disposed, ArgumentNullException if tex is null. Creates the MultiMesh with capacity 64 and VisibleInstanceCount = 0. Returns the new group id (monotonic from 1).
  • Unregister(groupId) — removes texture and mesh entries (no throw on unknown id; Dictionary.Remove semantics).
  • TryGetTexture(groupId, out tex)false for unknown groups.
  • GetMultiMesh(groupId) — indexer access; throws KeyNotFoundException for unknown groups. Attach the result to a MultiMeshInstance2D with the same texture.
  • Count(groupId) — current VisibleInstanceCount, or 0 for unknown groups.
  • EnsureCapacity(groupId, count) — throws ArgumentOutOfRangeException for count < 0 or unknown group. Grows by doubling; never shrinks.
  • SyncBulk2D(groupId, transforms, modulates, uvs) — throws ArgumentOutOfRangeException for unknown groups, ArgumentException unless all three spans share one length. Grows capacity as needed, sets VisibleInstanceCount = n, then writes one transform + one color per instance. UVs are length-validated; they have no per-instance 2D MultiMesh slot and are resolved in-shader or via BatchCanvas. No allocations in the loop.
  • Dispose() — idempotent; clears meshes and textures.

BatchCanvas

csharp
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 cached Rid or lazily creates one via RenderingServer.CanvasItemCreate(). Throws ObjectDisposedException when disposed.
  • ClearLayer(layer) / Clear()CanvasItemClear one/all valid layers. Call at the start of the frame; draws accumulate otherwise.
  • DrawLayer(layer, tex, rects, modulates) — throws ArgumentNullException for null tex, ArgumentException unless rects.Length == modulates.Length. One texture per call; issue more calls to accumulate. Issues CanvasItemAddTextureRect(item, rect, texRid, false, modulate) per entry.
  • DrawLayerRegion(layer, tex, dstRects, srcUvs, modulates) — same guards (all three spans equal length). Converts srcUvs (0..1, as written by AnimSystem) to pixels via tex.GetWidth()/GetHeight() and issues CanvasItemAddTextureRectRegion per entry.
  • Dispose() — idempotent; FreeRids every valid item and clears the map.

AnimSystem

csharp
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) — throws ArgumentOutOfRangeException for columns/rows <= 0. Wraps index into columns × 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 or frames.Length != uvs.Length. Per entry: clamp count to 1..total; if Fps > 0 && count > 1 && dt != 0, Time += dt (floored at 0) and Index = (int)(Time · Fps) % count; single-frame entries get Index = 0; always writes uvs[i] = UvFor(Index, columns, rows).

TransformSync2D

csharp
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.

  • BuildTransformsdst[i] = Transform2D((c·sx, s·sx), (−s·sy, c·sy), pos) with c/s = cos/sin(rotation). Throws ArgumentException if any source is shorter than dst.
  • BuildRectsdst[i] = Rect2(pos − size/2, size). Same length guard.
  • Sync — throws ArgumentNullException for null mgr; forwards to mgr.SyncBulk2D.

YSortSystem

csharp
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. Throws ArgumentException if scratch is shorter than order, ArgumentOutOfRangeException if any order[i] is outside layers.
  • Sort(order, layers) — allocates the scratch internally; tests/tooling only, not the hot path.
  • IsSorted(order, layers) — verification predicate, no mutation.

GodotECS docs — version 0.1.0. All rights reserved until a license is added.