DrawableTexture2D: writes, mipmaps and painting
Initialization, complete C# examples, multiple outputs and correct blending.
DrawableTexture2D is a texture supporting GPU writes through BlitRect/BlitRectMulti. Display it with TextureRect or Sprite2D like another Texture2D. Writes happen when an operation is called: the material passed to blit does not become a persistent texture effect.
Initialize before the first write
Call Setup with positive dimensions from 1 through 16384, a format and an explicit initial color. Use Colors.Transparent for a transparent canvas: an omitted/null Setup color means white, not transparent.
Save the complete DrawableOps.cs helper. It creates the checked RGBA8 resource, copies a complete source and performs the two-source/four-output example:
using System;
using Reckfell;
public static class DrawableOps
{
public static DrawableTexture2D Create(Vector2I size, Color color, bool mipmaps = false)
{
if (size.X < 1 || size.Y < 1 || size.X > 16384 || size.Y > 16384)
throw new ArgumentOutOfRangeException(nameof(size));
var target = new DrawableTexture2D();
target.Setup(size.X, size.Y, DrawableTexture2D.DrawableFormat.Rgba8, color, mipmaps);
return target;
}
public static void Copy(DrawableTexture2D target, Texture2D source, Material material)
{
if (target.GetRid() == source.GetRid())
throw new ArgumentException("Source and destination must be different textures.", nameof(source));
target.BlitRect(new Rect2I(0, 0, target.GetWidth(), target.GetHeight()), source, Colors.White, 0, material);
}
public static void Split(DrawableTexture2D target, DrawableTexture2D output1,
DrawableTexture2D output2, DrawableTexture2D output3, Texture2D first,
Texture2D second, Rect2I rect, Material material)
{
var outputs = new[] { target, output1, output2, output3 };
for (int i = 0; i < outputs.Length; i++)
for (int j = 0; j < i; j++)
if (outputs[i].GetRid() == outputs[j].GetRid())
throw new ArgumentException("Output textures must be distinct.");
foreach (var output in outputs)
{
if (output.GetWidth() != target.GetWidth() || output.GetHeight() != target.GetHeight())
throw new ArgumentException("Output textures must have the same size.");
if (output.GetRid() == first.GetRid() || output.GetRid() == second.GetRid())
throw new ArgumentException("A source cannot also be an output.");
}
var sources = new Reckfell.Collections.Array<Texture2D> { first, second };
using var sourcesOwner = (Reckfell.Collections.Array)sources;
var extras = new Reckfell.Collections.Array<DrawableTexture2D> { output1, output2, output3 };
using var extrasOwner = (Reckfell.Collections.Array)extras;
target.BlitRectMulti(rect, sources, extras, Colors.White, 0, material);
}
}For a first result, create DrawableOps.Create(new Vector2I(256, 256), Colors.Transparent), assign it to TextureRect.Texture and call target.BlitRect(new Rect2I(20, 30, 40, 40), null, Colors.Red). A null source selects white, so modulate determines the color. This draws a red rectangle.
Calling Setup again recreates contents and erases previous drawing. The tested RD resource keeps its RID, not its old pixels. SetFormat and SetUseMipmaps change resource settings without reallocating existing storage; use Setup again for a new format/mipmap chain. GetUseMipmaps does not prove physical levels exist after changing the flag alone.
Destination rectangle and blending
rect is the destination region, not a source crop. The whole source scales into it; change UV in a custom shader to crop. Pixels outside the rectangle remain unchanged. A source cannot also be a destination, including extra outputs. Use two textures and swap their roles for successive effects.
With no explicit material, the built-in blend_mix combines translucent red over blue. A custom texture_blit without render_mode uses blend_disabled and replaces RGBA. MODULATE multiplies the standard shader result; it is not a channel write mask. For example, (0,0,1,0) with blend_disabled writes blue with zero alpha and clears R/G; with ordinary blend_mix, alpha=0 contributes no visible color.
BlitMaterial selects a built-in copy using the checked Mix/Add/Sub/Mul/Disabled modes. It replaces the passed material rather than overriding an arbitrary custom shader's render_mode. Use a texture_blit ShaderMaterial for your own formula. See the shader reference.
Mipmap levels
Allocate the chain with Setup(..., useMipmaps:true). Writing level 0 does not automatically refresh every lower level. Call GenerateMipmaps after a batch of operations when lower levels must match the base.
mipmap selects the destination level. rect still uses the full base dimensions: on a 16×16 texture, (0,0,16,16) fills all of the 8×8 level 1. Writing level 1 leaves the base unchanged. GenerateMipmaps then overwrites lower levels from the base, including a manually painted level 1. Never select a level absent from the allocated chain.
Multiple sources and results
BlitRectMulti takes 1–4 sources and up to three extra destinations: 1–4 outputs including the current texture. All destinations must have equal dimensions and the requested mipmap level. Sources must not alias any destination; do not repeat output resources.
The Split helper above supplies a complete C# call with two sources and four destinations. Use BlitMulti.reckfellshader from the reference. COLOR0 writes target; COLOR1/2/3 write output1/2/3. hint_blit_source0/1 inputs need not mathematically correspond to outputs: your shader determines that mapping.
Reckfell.Collections.Array<T> does not directly implement IDisposable. The helper explicitly casts and disposes its owned underlying Array; this does not dispose the texture resources. Do not place borrowed resources still being displayed in short using scopes.
Complete TextureRect painting example
Save DrawablePaint.cs beside DrawableOps.cs and attach it to a nonzero-sized TextureRect. It stretches the whole canvas into the control, maps local control coordinates to texture pixels and draws a square brush with the left button. Motion fills gaps between received positions.
using Reckfell;
public partial class DrawablePaint : TextureRect
{
[Export] public Vector2I CanvasSize { get; set; } = new Vector2I(256, 256);
[Export] public int BrushSize { get; set; } = 12;
[Export] public Color BrushColor { get; set; } = Colors.Red;
public DrawableTexture2D Canvas { get; private set; } = null!;
private bool drawing;
private Vector2 last;
public override void _Ready()
{
Canvas = DrawableOps.Create(CanvasSize, Colors.Transparent);
Texture = Canvas;
ExpandMode = ExpandModeEnum.IgnoreSize;
StretchMode = StretchModeEnum.Scale;
}
public override void _GuiInput(InputEvent input)
{
if (Size.X <= 0 || Size.Y <= 0) return;
if (input is InputEventMouseButton button && button.ButtonIndex == MouseButton.Left)
{
drawing = button.Pressed;
if (drawing)
{
last = ToPixel(button.Position);
Stamp(last);
}
}
else if (input is InputEventMouseMotion motion)
{
if ((motion.ButtonMask & MouseButtonMask.Left) == 0) { drawing = false; return; }
if (!drawing) return;
Vector2 current = ToPixel(motion.Position);
int steps = Mathf.Max(1, Mathf.CeilToInt(last.DistanceTo(current) / Mathf.Max(1, BrushSize / 2)));
for (int i = 1; i <= steps; i++) Stamp(last.Lerp(current, (float)i / steps));
last = current;
}
}
private Vector2 ToPixel(Vector2 point) => point / Size * new Vector2(CanvasSize.X, CanvasSize.Y);
private void Stamp(Vector2 point)
{
int width = Mathf.Max(1, BrushSize);
var rect = new Rect2I(Mathf.FloorToInt(point.X - width / 2f), Mathf.FloorToInt(point.Y - width / 2f), width, width);
Canvas.BlitRect(rect, null!, BrushColor);
}
}Do not change CanvasSize after _Ready without recreating the canvas. The example requires StretchMode.Scale and ExpandMode.IgnoreSize; KeepAspect, letterboxing, rotated brushes or atlas textures need corresponding coordinate mapping. BrushSize uses texture pixels, not screen pixels.
Updates, readback and constraints
Changing a uniform does not replay a previous blit. TIME is sampled for a new operation, so the texture does not animate by itself. Repeat blits deliberately when data changes rather than unconditionally every frame.
GetImage reads results from the GPU into a caller-owned Image. Dispose it after use; do not read back every stroke when the data is needed only on the GPU. Rgba8, Rgba8Srgb, Rgbah and Rgbaf differ in color interpretation/precision. The ordinary RGBA8 example does not prove identical arithmetic for every HDR/sRGB path.
The current resource API does not support AtlasTexture sources and substitutes white with a warning. Use the atlas itself and explicit UV mapping. For premultiplied alpha, use the checked ShaderMaterial with blend_premul_alpha: the current BlitMaterial cache does not cover its declared PremultipliedAlpha enum value, so do not select that BlitMaterial path in this version.
Troubleshooting
| Symptom | Check |
|---|---|
| Empty or white result | Setup, initial color, valid source and an executed BlitRect. |
| Translucent color does not blend | Built-in blend_mix versus custom blend_disabled. |
| Uniform edits do nothing | Execute a new blit operation. |
| Offset or incorrectly sized brush | Control Size to CanvasSize mapping and stretch mode. |
| Right button starts painting or drawing never stops | ButtonIndex, Pressed and ButtonMask; do not toggle on every click. |
| Small-scale display shows old pixels | Mipmap-chain freshness. |
| Custom mipmap disappears after GenerateMipmaps | Generation replaces manually written lower levels. |
| Multiple outputs fail | Counts, equal dimensions, allocated levels and no source/destination aliasing. |
Windows Forward+/Vulkan checks inspect actual Image pixels for copying, blending, multiple outputs, mipmaps and the example's _GuiInput calls. These calls inject events programmatically, not through a physical mouse. All platforms/renderers, HDR/sRGB, AtlasTexture and the unsafe BlitMaterial PremultipliedAlpha path are not separately exercised here. Functional test frames are not published as beauty illustrations.