Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
4d2f1e1
Add TextShaper and optimize OT shaping
JimBobSquarePants Jul 23, 2026
2df90da
Optimize glyph shaping pipeline and lookup caching
JimBobSquarePants Jul 23, 2026
428b10e
Introduce PositionedGlyphMetrics wrapper type
JimBobSquarePants Jul 23, 2026
60a0db3
Remove render-time glyph metrics cloning
JimBobSquarePants Jul 23, 2026
b1bea16
Checkpoint inherited optimizations: subtable digests, closure-free ma…
JimBobSquarePants Jul 23, 2026
8924994
Flatten shaping stage pipeline: List-based stages, no per-call hashin…
JimBobSquarePants Jul 23, 2026
56b3bae
Cut fixed-pipeline shaping allocations: struct offset pairs and array…
JimBobSquarePants Jul 23, 2026
da42797
Port the HarfBuzz buffer memory model: pooled shaping pipeline state
JimBobSquarePants Jul 23, 2026
14b67d8
Move the shaping pipeline into TextShaper
JimBobSquarePants Jul 23, 2026
394ce1e
Seal pooling inside ShapeText and make the shaped result inert
JimBobSquarePants Jul 23, 2026
d90c731
Replace the shaping collections with a single flat-array ShapingBuffer
JimBobSquarePants Jul 24, 2026
3aa1613
Add Devanagari benchmark scenario and HarfBuzz differential tests
JimBobSquarePants Jul 24, 2026
37c8f47
Store syllable classification in per-glyph SyllableInfo structs
JimBobSquarePants Jul 24, 2026
a088e97
Query feature lookups directly for Indic substitution probes
JimBobSquarePants Jul 24, 2026
86474cd
Cache the generated universal shaping decompositions dictionary
JimBobSquarePants Jul 24, 2026
1bf8db8
Add plan and stage sub-phase counters to the shaping probe
JimBobSquarePants Jul 24, 2026
6d6a96a
Count graphemes once per run build with a boundary-only walk
JimBobSquarePants Jul 24, 2026
a995598
Pass the count-only flag positionally
JimBobSquarePants Jul 24, 2026
129c097
Register direction features per direction span
JimBobSquarePants Jul 24, 2026
3a0b939
Add ticks-only probe mode
JimBobSquarePants Jul 24, 2026
ec766da
Apply stage groups in lookup-index order with combined masks
JimBobSquarePants Jul 24, 2026
d1f8d1b
Make the merge scratch lists public like the rest of the buffer surface
JimBobSquarePants Jul 24, 2026
d9701d0
Make cross-type members of internal types public
JimBobSquarePants Jul 24, 2026
364535f
Front the metrics seeding with a direct-mapped cache on the buffer
JimBobSquarePants Jul 24, 2026
d63f967
Move placeholder bidi runs off the glyph record
JimBobSquarePants Jul 24, 2026
06fa7c1
Pack the syllable classification into a single word
JimBobSquarePants Jul 24, 2026
40bdf3e
Pack the boolean shaping state into one flags word
JimBobSquarePants Jul 24, 2026
5f61a32
Narrow the direction and shaping-class cache storage
JimBobSquarePants Jul 24, 2026
26875d7
Name the packed record bits and expand summary docs to multiple lines
JimBobSquarePants Jul 24, 2026
9149c82
Store the text run on the glyph record as a run-table index
JimBobSquarePants Jul 24, 2026
a739426
Shape and seed a single run in place instead of copying buffers
JimBobSquarePants Jul 24, 2026
dca2926
Drive positioning from the substitution pass's plan
JimBobSquarePants Jul 24, 2026
ba18848
Skip the lookahead decode for fonts without variation sequences
JimBobSquarePants Jul 24, 2026
e008dab
Front glyph id lookups with a direct-mapped cache on the buffer
JimBobSquarePants Jul 24, 2026
18cb44f
Split shaping state into info and position streams
JimBobSquarePants Jul 24, 2026
9bbb31a
Narrow the shaping record to one cache line
JimBobSquarePants Jul 24, 2026
65ebe85
Add buffer-local glyph class cache
JimBobSquarePants Jul 24, 2026
cc11f8f
Make syllable machine matching allocation free
JimBobSquarePants Jul 24, 2026
1895244
Front the substitution feature resolution with a buffer cache
JimBobSquarePants Jul 24, 2026
9069631
Add ASCII fast path to grapheme counting
JimBobSquarePants Jul 24, 2026
5b6d423
Skip parallel stream mirroring during substitution edits
JimBobSquarePants Jul 24, 2026
97b7007
Derive syllable shaping properties once per glyph
JimBobSquarePants Jul 24, 2026
205909b
Cache class lookups on range-based class definitions
JimBobSquarePants Jul 24, 2026
a34a131
Reuse the bidi analysis data per thread
JimBobSquarePants Jul 24, 2026
6aaac18
Reuse shaper instances across segments and passes
JimBobSquarePants Jul 24, 2026
a255770
Add ASCII fast path to buffer population
JimBobSquarePants Jul 24, 2026
5e98126
Rename buffer cache accessors to standard naming
JimBobSquarePants Jul 24, 2026
e8e7a6b
Revert "Add ASCII fast path to buffer population"
JimBobSquarePants Jul 24, 2026
7a3cb3a
Introduce the shape plan
JimBobSquarePants Jul 24, 2026
87e53ad
Move feature mask ownership into the shape plan
JimBobSquarePants Jul 24, 2026
96c5a74
Port the global-bit feature classification model
JimBobSquarePants Jul 24, 2026
a546de9
Demote Hangul contextual alternates instead of disabling them
JimBobSquarePants Jul 25, 2026
ffc7b3e
Narrow the per-glyph feature masks to 32 bits
JimBobSquarePants Jul 25, 2026
db7a483
Short-circuit single-scalar ASCII grapheme clusters
JimBobSquarePants Jul 25, 2026
1031782
Seed the global feature bit at glyph record initialization
JimBobSquarePants Jul 25, 2026
335eabe
Classify ASCII bidi properties from compile-time constant tables
JimBobSquarePants Jul 25, 2026
35419ba
Generate the ASCII bidi tables from the Unicode source data
JimBobSquarePants Jul 25, 2026
0536469
Express the ASCII fast-path gates through the shared helpers
JimBobSquarePants Jul 25, 2026
723e0f3
Stream substitution through an output pass
JimBobSquarePants Jul 25, 2026
8a3013c
Hide default ignorables after positioning
JimBobSquarePants Jul 25, 2026
4c60de6
Shape into a caller-owned buffer
JimBobSquarePants Jul 25, 2026
3ae5fa4
Eliminate the remaining per-call shaping allocations
JimBobSquarePants Jul 25, 2026
fc9b725
Resolve joiner transparency inside sequence matching
JimBobSquarePants Jul 26, 2026
d4b10dc
Update BenchmarkDotNet and benchmark the host runtime
JimBobSquarePants Jul 26, 2026
1e684f7
Collect feature registrations once per plan
JimBobSquarePants Jul 26, 2026
64aedac
Remove the shaping probe instrumentation
JimBobSquarePants Jul 26, 2026
e5ad84b
Apply nested lookups at the positions the context matched
JimBobSquarePants Jul 26, 2026
30a7223
Form fractions from the fraction slash without being asked
JimBobSquarePants Jul 26, 2026
eb3729f
Match backtrack against the records a pass has produced
JimBobSquarePants Jul 26, 2026
8d2f82d
Stream nested lookups through the pass cursor
JimBobSquarePants Jul 26, 2026
5f8c30c
Keep the pass bookkeeping off the per-glyph path
JimBobSquarePants Jul 26, 2026
cca1868
Map characters through the one subtable a font intends
JimBobSquarePants Jul 26, 2026
6c78ede
Improve OpenType shaping parity
JimBobSquarePants Jul 27, 2026
db4fcfc
Add Khmer shaping and kern format 2 support
JimBobSquarePants Jul 27, 2026
9262309
Add Arabic fallback shaping and positioning
JimBobSquarePants Jul 27, 2026
85c85a5
Split line and run shaping contracts
JimBobSquarePants Jul 27, 2026
6050189
Fix Arabic shaping in vertical layout
JimBobSquarePants Jul 27, 2026
ec016d3
Improve complex text shaping parity
JimBobSquarePants Jul 27, 2026
47707d4
Improve Arabic and Indic shaping parity
JimBobSquarePants Jul 27, 2026
aa752b5
Improve contextual shaping parity
JimBobSquarePants Jul 27, 2026
feb68cd
Optimize culture run comparisons
JimBobSquarePants Jul 27, 2026
92901e2
Share shaping run boundaries
JimBobSquarePants Jul 27, 2026
e3fd4ec
Match variable font metrics
JimBobSquarePants Jul 27, 2026
a02e753
Handle hard breaks in text shaping
JimBobSquarePants Jul 27, 2026
9ee0a6a
Optimize normalization lookup
JimBobSquarePants Jul 28, 2026
742b815
Optimize shaping hot paths
JimBobSquarePants Jul 28, 2026
f57371f
Benchmark all shaping engines
JimBobSquarePants Jul 28, 2026
ec59a4b
Exclude unsupported HarfBuzz corpus fonts
JimBobSquarePants Jul 28, 2026
b2a12fb
Add public text shaping API
JimBobSquarePants Jul 28, 2026
f9e9540
Document shaped buffer font contract
JimBobSquarePants Jul 28, 2026
4ccee1e
Load corpus font collections correctly
JimBobSquarePants Jul 28, 2026
373b65b
Exclude unsupported AAT corpus font
JimBobSquarePants Jul 28, 2026
e2a450d
Add Linux HarfBuzz test assets
JimBobSquarePants Jul 28, 2026
c694a59
Align text layout with browser engines
JimBobSquarePants Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,4 @@ SixLabors.Shapes.Coverage.xml

/coverage.*.xml
*.lcov
BenchmarkDotNet.Artifacts/
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "shared-infrastructure"]
path = shared-infrastructure
url = https://github.com/SixLabors/SharedInfrastructure
[submodule "tests/harfbuzz"]
path = tests/harfbuzz
url = https://github.com/harfbuzz/harfbuzz.git
2 changes: 1 addition & 1 deletion src/SixLabors.Fonts/ArraySlice{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public void CopyTo(ArraySlice<T> destination)
/// Thrown when the specified <paramref name="start"/> or end index is not in range (&lt;0 or &gt;Length).
/// </exception>
public ArraySlice<T> Slice(int start, int length)
=> new(this.data, start, length);
=> new(this.data, this.Start + start, length);

/// <inheritdoc/>
public IEnumerator<T> GetEnumerator() => new Enumerator(this);
Expand Down
324 changes: 324 additions & 0 deletions src/SixLabors.Fonts/BidiReordering.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using SixLabors.Fonts.Unicode;

namespace SixLabors.Fonts;

/// <summary>
/// Reorders positioned glyph records from logical order to visual order for one
/// resolved line.
/// </summary>
/// <remarks>
/// Implements rule L2 of the
/// <see href="https://www.unicode.org/reports/tr9/#L2">Unicode Bidirectional Algorithm</see>.
/// </remarks>
internal static class BidiReordering
{
/// <summary>
/// Supplies level lookup and range reversal for a glyph storage representation.
/// Static operations let the JIT specialize the shared loop without delegates,
/// closures, or interface instances on the shaping hot path.
/// </summary>
/// <typeparam name="TState">The reordered storage.</typeparam>
private interface IBidiReorderingOperations<TState>
{
/// <summary>
/// Gets the resolved embedding level of one glyph.
/// </summary>
/// <param name="state">The reordered storage.</param>
/// <param name="index">The glyph index.</param>
/// <returns>The resolved embedding level.</returns>
public static abstract int GetLevel(TState state, int index);

/// <summary>
/// Reverses one half-open range while keeping each glyph record intact.
/// </summary>
/// <param name="state">The reordered storage.</param>
/// <param name="start">The first glyph index.</param>
/// <param name="end">The index after the final glyph.</param>
public static abstract void Reverse(TState state, int start, int end);
}

/// <summary>
/// Reorders layout run fragments for one line after its final source boundary
/// is known.
/// </summary>
/// <param name="glyphs">The logically ordered layout entries.</param>
/// <param name="layoutMode">The orientation used to finalize each directional run.</param>
public static void Reorder(List<GlyphLayoutData> glyphs, LayoutMode layoutMode)
{
// Browsers slice each already-visual shaped run by source range before
// line items are reordered. Composition keeps
// source-codepoint containers logical for line breaking, so first arrange
// those complete containers in the same visual order as their shaped run.
// The positioned glyphs inside each container already retain projected
// visual order and are never reversed here.
int fragmentStart = 0;
while (fragmentStart < glyphs.Count)
{
int fragmentEnd = FindFragmentEnd(glyphs, fragmentStart, glyphs.Count);
if ((glyphs[fragmentStart].BidiRun.Level & 1) != 0)
{
if (layoutMode.IsVertical())
{
// The public ShapeRun contract is HarfBuzz-verified for upright
// bottom-to-top runs: graphemes reverse, while positioned glyph
// order inside each grapheme remains unchanged.
glyphs.Reverse(fragmentStart, fragmentEnd - fragmentStart);
int graphemeStart = fragmentStart;
while (graphemeStart < fragmentEnd)
{
int graphemeIndex = glyphs[graphemeStart].GraphemeIndex;
int graphemeEnd = graphemeStart + 1;
while (graphemeEnd < fragmentEnd
&& glyphs[graphemeEnd].GraphemeIndex == graphemeIndex)
{
graphemeEnd++;
}

glyphs.Reverse(graphemeStart, graphemeEnd - graphemeStart);
graphemeStart = graphemeEnd;
}
}
else
{
// Horizontal and mixed-vertical backward runs store HarfBuzz's
// complete backward glyph stream: shaped source positions appear
// in reverse source order while the glyphs inside one source
// position already carry the stream's visual order and offsets.
// Every entry is one complete source position holding its slice
// of that stream, so reversing entry order alone reproduces the
// stored stream exactly and no glyph inside an entry ever moves.
// This is the browser's fragment discipline: sliced fragments
// stay visual and only whole units reorder.
glyphs.Reverse(fragmentStart, fragmentEnd - fragmentStart);
}
}

fragmentStart = fragmentEnd;
}

int maximumLevel = 0;
int minimumOddLevel = int.MaxValue;
for (fragmentStart = 0; fragmentStart < glyphs.Count;)
{
int level = glyphs[fragmentStart].BidiRun.Level;
maximumLevel = Math.Max(maximumLevel, level);
if ((level & 1) != 0)
{
minimumOddLevel = Math.Min(minimumOddLevel, level);
}

fragmentStart = FindFragmentEnd(glyphs, fragmentStart, glyphs.Count);
}

if (minimumOddLevel == int.MaxValue)
{
return;
}

// Browsers perform this step only after line breaking and reorder whole
// runs rather than characters. Apply UAX #9 L2 to the
// complete fragments, preserving the visual glyph order within each one.
for (int level = maximumLevel; level >= minimumOddLevel; level--)
{
int sequenceStart = 0;
while (sequenceStart < glyphs.Count)
{
int fragmentEnd = FindFragmentEnd(glyphs, sequenceStart, glyphs.Count);
while (sequenceStart < glyphs.Count
&& glyphs[sequenceStart].BidiRun.Level < level)
{
sequenceStart = fragmentEnd;
fragmentEnd = sequenceStart < glyphs.Count
? FindFragmentEnd(glyphs, sequenceStart, glyphs.Count)
: sequenceStart;
}

if (sequenceStart == glyphs.Count)
{
break;
}

int sequenceEnd = sequenceStart;
int fragmentCount = 0;
while (sequenceEnd < glyphs.Count
&& glyphs[sequenceEnd].BidiRun.Level >= level)
{
sequenceEnd = FindFragmentEnd(glyphs, sequenceEnd, glyphs.Count);
fragmentCount++;
}

if (fragmentCount > 1)
{
// Reversing the complete storage range reverses both fragment
// order and each fragment's contents. Reverse each now-contiguous
// fragment once more to retain its already-visual glyph order.
// This moves values in place and needs no per-run owner, copied
// glyph array, permutation map, or temporary collection.
glyphs.Reverse(sequenceStart, sequenceEnd - sequenceStart);
int restoredStart = sequenceStart;
while (restoredStart < sequenceEnd)
{
int restoredEnd = FindFragmentEnd(glyphs, restoredStart, sequenceEnd);
glyphs.Reverse(restoredStart, restoredEnd - restoredStart);
restoredStart = restoredEnd;
}
}

sequenceStart = sequenceEnd;
}
}
}

/// <summary>
/// Reorders positioned shaping records for one line.
/// </summary>
/// <param name="glyphs">The logically ordered shaping records.</param>
/// <param name="bidiRuns">The resolved bidirectional runs covering the source text.</param>
/// <param name="bidiMap">The source codepoint to bidirectional-run mapping.</param>
public static void Reorder(ShapingBuffer glyphs, BidiRun[] bidiRuns, int[] bidiMap)
=> Reorder<ShapingGlyphState, ShapingGlyphOperations>(new(glyphs, bidiRuns, bidiMap), 0, glyphs.Count);

/// <summary>
/// Reorders a half-open range of positioned shaping records for one line.
/// </summary>
/// <param name="glyphs">The logically ordered shaping records.</param>
/// <param name="bidiRuns">The resolved bidirectional runs covering the source text.</param>
/// <param name="bidiMap">The source codepoint to bidirectional-run mapping.</param>
/// <param name="start">The first glyph record in the line.</param>
/// <param name="end">The glyph record immediately after the line.</param>
public static void Reorder(ShapingBuffer glyphs, BidiRun[] bidiRuns, int[] bidiMap, int start, int end)
=> Reorder<ShapingGlyphState, ShapingGlyphOperations>(new(glyphs, bidiRuns, bidiMap), start, end);

/// <summary>
/// Applies rule L2 of the Unicode Bidirectional Algorithm to one line.
/// </summary>
/// <typeparam name="TState">The reordered storage.</typeparam>
/// <typeparam name="TOperations">The specialized operations for that storage.</typeparam>
/// <param name="state">The reordered storage and any level-mapping state it needs.</param>
/// <param name="start">The first glyph record in the line.</param>
/// <param name="end">The glyph record immediately after the line.</param>
private static void Reorder<TState, TOperations>(TState state, int start, int end)
where TOperations : struct, IBidiReorderingOperations<TState>
{
int maximumLevel = 0;
int minimumOddLevel = int.MaxValue;
for (int i = start; i < end; i++)
{
int level = TOperations.GetLevel(state, i);
maximumLevel = Math.Max(maximumLevel, level);
if ((level & 1) != 0)
{
minimumOddLevel = Math.Min(minimumOddLevel, level);
}
}

if (minimumOddLevel == int.MaxValue)
{
return;
}

// UAX #9 rule L2 reverses each maximal contiguous sequence whose level is
// at least the current level, walking down from the highest resolved level
// to the lowest odd one. Operating directly on the destination storage
// keeps every glyph's identity, positioning, and source index together and
// requires no temporary permutation or per-run allocation.
for (int level = maximumLevel; level >= minimumOddLevel; level--)
{
int sequenceStart = start;
while (sequenceStart < end)
{
while (sequenceStart < end && TOperations.GetLevel(state, sequenceStart) < level)
{
sequenceStart++;
}

int sequenceEnd = sequenceStart;
while (sequenceEnd < end && TOperations.GetLevel(state, sequenceEnd) >= level)
{
sequenceEnd++;
}

if (sequenceEnd - sequenceStart > 1)
{
TOperations.Reverse(state, sequenceStart, sequenceEnd);
}

sequenceStart = sequenceEnd + 1;
}
}
}

/// <summary>
/// Finds the exclusive end of the directional run fragment beginning at the
/// supplied layout index.
/// </summary>
/// <param name="glyphs">The line's layout storage.</param>
/// <param name="start">The first entry in the fragment.</param>
/// <param name="end">The exclusive search limit.</param>
/// <returns>The first entry after the fragment.</returns>
private static int FindFragmentEnd(List<GlyphLayoutData> glyphs, int start, int end)
{
BidiRun bidiRun = glyphs[start].BidiRun;
int index = start + 1;
while (index < end && glyphs[index].BidiRun.Equals(bidiRun))
{
index++;
}

return index;
}

/// <summary>
/// Holds a shaping buffer together with the source mapping needed to recover
/// each glyph's resolved embedding level.
/// </summary>
private readonly struct ShapingGlyphState
{
/// <summary>
/// Initializes a new instance of the <see cref="ShapingGlyphState"/> struct.
/// </summary>
/// <param name="glyphs">The positioned shaping records.</param>
/// <param name="bidiRuns">The resolved bidirectional runs.</param>
/// <param name="bidiMap">The source codepoint to bidirectional-run mapping.</param>
public ShapingGlyphState(ShapingBuffer glyphs, BidiRun[] bidiRuns, int[] bidiMap)
{
this.Glyphs = glyphs;
this.BidiRuns = bidiRuns;
this.BidiMap = bidiMap;
}

/// <summary>
/// Gets the positioned shaping records.
/// </summary>
public ShapingBuffer Glyphs { get; }

/// <summary>
/// Gets the resolved bidirectional runs.
/// </summary>
public BidiRun[] BidiRuns { get; }

/// <summary>
/// Gets the source codepoint to bidirectional-run mapping.
/// </summary>
public int[] BidiMap { get; }
}

/// <summary>
/// Adapts positioned shaping records to the shared reordering loop.
/// </summary>
private readonly struct ShapingGlyphOperations : IBidiReorderingOperations<ShapingGlyphState>
{
/// <inheritdoc/>
public static int GetLevel(ShapingGlyphState state, int index)
{
int codePointIndex = state.Glyphs[index].CodePointIndex;
return state.BidiRuns[state.BidiMap[codePointIndex]].Level;
}

/// <inheritdoc/>
public static void Reverse(ShapingGlyphState state, int start, int end) => state.Glyphs.ReverseRange(start, end);
}
}
12 changes: 12 additions & 0 deletions src/SixLabors.Fonts/Exceptions/FontsThrowHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,16 @@ public static T ThrowGlyphMissingException<T>(CodePoint codePoint)
[MethodImpl(MethodImplOptions.NoInlining)]
public static void ThrowDefaultInstance()
=> throw new FontException("Cannot use the default value type instance to create a font.");

/// <summary>
/// Throws an <see cref="InvalidOperationException"/> recording that shaping
/// state was queried outside a segment window, where no shape plan is current.
/// Kept out of the accessor so it stays inlinable.
/// </summary>
/// <typeparam name="T">The declared result type of the failed accessor.</typeparam>
/// <returns>Never returns; the type satisfies the caller's flow analysis.</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
public static T ThrowNoCurrentShapePlan<T>()
=> throw new InvalidOperationException(
"No shape plan is current; the operation is only valid while a segment is being shaped.");
}
Loading
Loading