---
title: "Shape many runs without allocating"
url: https://recursiveai.org/onefont/cookbook/shape-without-allocating/
description: "Shape thousands of strings in C# with no garbage per call: reuse a ShapedRun and a ShapingOptions, and read clusters and glyphs from spans."
version: "1.3.0"
---

# Shape many runs without allocating

Shape thousands of strings in C# with no garbage per call: reuse a ShapedRun and a ShapingOptions, and read clusters and glyphs from spans.

`TextShaping.ShapeRun` returns a list with one object per cluster. That is convenient, and fine for a heading or a label. A paragraph engine, a table of cells or a server that measures text all day can pass a `ShapedRun` instead: flat arrays that grow once and are reused on every call.

```csharp
using OneFont;

FontFace face = FontFace.Load(File.ReadAllBytes("NotoSans-Regular.ttf"));
string[] cells = Enumerable.Range(1, 10_000).Select(i => $"Row {i}: invoice total {i * 37.5:F2} USD").ToArray();

// The list API: one object per cluster.
long before = GC.GetAllocatedBytesForCurrentThread();
long width1 = 0;
foreach (string cell in cells)
    foreach (ShapedCluster c in TextShaping.ShapeRun(cell, face))
        width1 += c.Advance;
long listBytes = GC.GetAllocatedBytesForCurrentThread() - before;

// A reusable ShapedRun: flat arrays that grow once and are kept between calls.
var run = new ShapedRun();
var options = new ShapingOptions { Kerning = KerningMode.Normal };    // reuse one options object too
TextShaping.ShapeRun(cells.MaxBy(c => c.Length), face, run, options: options);   // warm up on the longest run
before = GC.GetAllocatedBytesForCurrentThread();
long width2 = 0;
foreach (string cell in cells)
{
    TextShaping.ShapeRun(cell, face, run, options: options);
    foreach (ShapedClusterRange c in run.Clusters)
        width2 += c.Advance;
}
long runBytes = GC.GetAllocatedBytesForCurrentThread() - before;

int chars = cells.Sum(c => c.Length);
Console.WriteLine($"{cells.Length} runs, {chars} characters, same total width: {width1 == width2}");
Console.WriteLine($"list API:  {listBytes / (double)chars:F1} bytes allocated per character");
Console.WriteLine($"ShapedRun: {runBytes} bytes allocated in total");
```

```text
10000 runs, 365934 characters, same total width: True
list API:  83.5 bytes allocated per character
ShapedRun: 0 bytes allocated in total
```

- **The same answer.** Cluster `i` of a `ShapedRun` has the start, length, face, advance, missing-glyph flag and glyphs of cluster `i` of the list, so the totals match.
- **Reading it.** `run.Clusters` is a span of `ShapedClusterRange`. `run.GlyphsOf(cluster)` gives that cluster’s glyphs, and `run.Glyphs` holds all of them, cluster after cluster in logical order. `run.Origins` gives the first code point each glyph stands for.
- **Valid until the next call.** Each call clears the result and refills it, so copy out what you need first. If a call throws `FontFormatException`, the result is left empty.
- **Warm up.** The arrays grow to the longest run they have held, and the shaper keeps a little working state per thread. Shape the longest text once before measuring. Warming up on the shortest cell instead allocated 3,432 bytes in this run, as longer cells grew the arrays.
- **Threads.** A `ShapedRun` belongs to one thread at a time. Any number of threads may shape at once, each with its own. A `ShapingOptions` can be shared as long as nobody changes it, and it caches its feature set, so reuse one rather than creating one per call.
- **Fallback.** `FontFallbackChain.ShapeRun(text, run)` fills a `ShapedRun` the same way, with `FaceIndex` on each cluster.

The per-character figure for the list API depends on the text. The changelog gives about 75 bytes per character for Latin, and this sample’s mix of words and numbers came to 83.5.

For callers that cut paragraphs into runs themselves and keep their own cluster numbers, `TextShaping.ShapeCodePoints` with a reusable `CodePointRun` works the same way on code points. It is described in [Shaping, bidi and segmentation](https://recursiveai.org/onefont/docs/shaping/).
