Skip to content

Commit 09cdd04

Browse files
SadPencilCopilot
andcommitted
Downgrade to .NET Framework 4.8 + MonoGame 3.8.0.1641, add Polyfill 10.0.0
Co-Authored-By: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 7f1ef3d commit 09cdd04

20 files changed

Lines changed: 121 additions & 275 deletions

File tree

Directory.Build.props

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
<Project>
33

44
<PropertyGroup>
5-
<TargetFramework>net8.0</TargetFramework>
5+
<TargetFrameworks>net8.0;net48;netcoreapp3.1</TargetFrameworks>
6+
<SuppressTfmSupportBuildErrors>true</SuppressTfmSupportBuildErrors>
67
<SolutionDirectory>$(MSBuildThisFileDirectory)</SolutionDirectory>
8+
<LangVersion>14.0</LangVersion>
79
</PropertyGroup>
810

911
<!-- Version Configuration -->
1012
<PropertyGroup>
1113
<Version>0.0.3</Version>
12-
<MonoGameVersion>3.8.4.1</MonoGameVersion>
14+
<MonoGameVersion>3.8.0.1641</MonoGameVersion>
1315
</PropertyGroup>
1416

1517
<PropertyGroup>
@@ -23,6 +25,16 @@
2325
<PackageLicenseExpression>MIT</PackageLicenseExpression>
2426
</PropertyGroup>
2527

28+
<ItemGroup>
29+
<PackageReference Include="Polyfill" Version="10.0.0" PrivateAssets="All" />
30+
<PackageReference Include="System.Memory" Version="4.6.3" />
31+
</ItemGroup>
32+
33+
<PropertyGroup>
34+
<!-- Opt-in to Polyfill's ArgumentException static-method group (ThrowIfNull, ThrowIfNegative, etc.) -->
35+
<PolyArgumentExceptions>true</PolyArgumentExceptions>
36+
</PropertyGroup>
37+
2638
<ItemGroup>
2739
<!-- path must be relative to the individual csproj's not this .targets file -->
2840
<None Include="../../README.md" Pack="true" PackagePath="" />

README.md

Lines changed: 10 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -1,200 +1,22 @@
11
# Forme
22

3-
![Example of the GPU Rendering Form Demo](./docs/screnshot-01.png)
4-
5-
Forme renders text directly from quadratic Bezier glyph outline data on the GPU without precomputed textures, distance fields, or rasterized atlases. At any size and any scale, text remains sharp. Forme also provides a CPU rasterization path that produces standard MonoGame `SpriteFont` objects for cases where traditional bitmap fonts are preferred.
6-
7-
## Why the name Forme
8-
9-
According to the Slug creator:
10-
11-
> The name Slug comes from the history of typography. A full line of text cast as one piece of hot lead by a Linotype machine was called a slug. The primary function of our software is to lay out and render lines of text.
12-
13-
So to follow in this spirit of naming, Forme was chosen because in traditional letterpress printing, a forme is the complete assembly of individual pieces of type, letters, spacing, and lines, locked together into a single unit, ready for printing.
14-
15-
Where a slug represents a single cast line of text, a forme represents the next step: the composition of many lines into a structured, finalized layout.
16-
17-
## Libraries
18-
19-
The repository is split across three libraries and a content pipeline extension.
20-
21-
### Forme
22-
23-
**Forme** is the core font processing library with no dependency on MonoGame or any graphics framework. It reads TrueType and OpenType font files, extracts glyph outlines, builds the curve and band textures required by the Slug algorithm, and handles text layout including word wrapping, alignment, and ellipsis truncation. It can save and load processed font data to a `.forme` binary file so that the TTF does not need to be reprocessed on every launch.
24-
25-
### Forme.MonoGame
26-
27-
**Forme.MonoGame** is the MonoGame-specific rendering layer. It uploads curve and band texture data to the GPU, manages the compiled Slug shaders (one for OpenGL, one for DirectX 11, both embedded in the assembly and selected automatically at runtime), and exposes `FormeRenderer` for GPU-accelerated text rendering. It also exposes `FormeSpriteFont` for creating standard `SpriteFont` objects from TTF data at runtime.
28-
29-
### Forme.MonoGame.Content.Pipeline
30-
31-
**Forme.MonoGame.Content.Pipeline** is the MGCB content pipeline extension. It adds importers and processors for TTF and `.forme` files so that fonts can be compiled as content and loaded through the standard `Content.Load<FormeFont>()` API.
32-
33-
## Installation
34-
35-
Install via NuGet.
36-
37-
```
38-
dotnet add package AristurtleDev.Forme
39-
dotnet add package AristurtleDev.Forme.MonoGame
40-
```
41-
42-
To use the content pipeline extension with MonoGame, add the reference the following NuGet package in your project and add the dll reference to your Content.mgcb file as usual.
43-
44-
```
45-
dotnet add package AristurtleDev.Forme.MonoGame.Content.Pipeline
46-
```
47-
48-
## Usage
49-
50-
### Loading a Font
51-
52-
Load from a TTF file at runtime:
53-
54-
```csharp
55-
using Forme;
56-
57-
byte[] ttfData = File.ReadAllBytes("NotoSans-Regular.ttf");
58-
FormeFont font = FormeFont.FromTtf(ttfData, CharacterSet.Ascii);
59-
```
60-
61-
Save the processed font data to a `.forme` file to avoid reprocessing:
62-
63-
```csharp
64-
font.Save("NotoSans-Regular.forme");
65-
```
66-
67-
Load from a previously saved `.forme` file:
68-
69-
```csharp
70-
FormeFont font = FormeFont.FromFile("NotoSans-Regular.forme");
71-
```
72-
73-
Load via the MonoGame content pipeline (requires Forme.MonoGame.Content.Pipeline):
74-
75-
```csharp
76-
FormeFont font = Content.Load<FormeFont>("NotoSans-Regular");
77-
```
78-
79-
### GPU Rendering
80-
81-
Upload the font to the GPU, then use `FormeRenderer` to draw:
82-
83-
```csharp
84-
using Forme.MonoGame;
85-
86-
FormeFontDevice fontDevice = new FormeFontDevice(GraphicsDevice, font);
87-
FormeRenderer renderer = new FormeRenderer(GraphicsDevice);
88-
89-
// In Draw():
90-
renderer.Begin();
91-
renderer.DrawString(fontDevice, "Hello, world!", new Vector2(100, 100), Color.White, sizePixels: 32);
92-
renderer.End();
93-
```
94-
95-
### Text Layout
96-
97-
Measure text before drawing:
3+
This is a .NET Framework 4.8 fork from [AristurtleDev/Forme](https://github.com/AristurtleDev/Forme). Forme renders text directly from quadratic Bezier glyph outline data on the GPU without precomputed textures, distance fields, or rasterized atlases. At any size and any scale, text remains sharp. Forme also provides a CPU rasterization path that produces standard MonoGame `SpriteFont` objects for cases where traditional bitmap fonts are preferred.
984

99-
```csharp
100-
FormeTextBounds bounds = font.MeasureString("Hello, world!", sizePixels: 32);
101-
```
102-
103-
Use `TextLayoutOptions` for word wrapping, alignment, and ellipsis:
104-
105-
```csharp
106-
TextLayoutOptions options = new TextLayoutOptions
107-
{
108-
MaxWidth = 400,
109-
Alignment = TextHorizontalAlignment.Center,
110-
EllipsisMode = EllipsisMode.Word
111-
};
112-
113-
FormeTextBounds bounds = font.MeasureString("Hello, world!", sizePixels: 32, options);
114-
IReadOnlyList<GlyphPlacement> glyphs = font.GetGlyphs("Hello, world!", sizePixels: 32, options);
115-
```
116-
117-
### SpriteFont (CPU Rasterization)
118-
119-
Create a standard MonoGame `SpriteFont` from TTF data at runtime:
120-
121-
```csharp
122-
using Forme.MonoGame;
123-
124-
byte[] ttfData = File.ReadAllBytes("NotoSans-Regular.ttf");
125-
SpriteFont spriteFont = FormeSpriteFont.Create(GraphicsDevice, ttfData, sizePixels: 24, CharacterSet.Ascii);
126-
127-
// Use with SpriteBatch as normal:
128-
spriteBatch.DrawString(spriteFont, "Hello, world!", new Vector2(100, 100), Color.White);
129-
```
130-
131-
## Demos
132-
133-
### GPU Rendering Demo
134-
135-
Located in [`demo/GPURenderingDemo`](./demo/GPURenderingDemo/). Demonstrates GPU accelerated text rendering using the Slug algorithm. Both a DesktopGL and a WindowsDX project are included. The DesktopGL project loads fonts through the content pipeline. The WindowsDX project loads fonts at runtime via `FormeFont.FromTtf()`.
136-
137-
The demo shows text at multiple sizes, word-wrapped and aligned text, character and line spacing, all three ellipsis modes, and optional visual overlays for bounding boxes and baselines.
138-
139-
### SpriteFont Rendering Demo
140-
141-
Located in [`demo/SpriteFontRenderingDemo`](./demo/SpriteFontRenderingDemo/). Demonstrates creating MonoGame `SpriteFont` objects from TTF data using `FormeSpriteFont.Create()`. Both a DesktopGL and a WindowsDX project are included. The demo shows fonts at multiple sizes rendered through the standard `SpriteBatch` API.
142-
143-
## Shader Implementation Notes
144-
145-
The Slug reference shaders are written for DirectX with full Shader Model 5 features available. MonoGame's OpenGL backend compiles HLSL to GLSL via [MojoShader](https://icculus.org/mojoshader/), which only supports Shader Model 3. SM3 has no integer arithmetic, no integer textures, no integer loop variables, and no direct texel load instructions. The Forme shader targets SM3 for OpenGL and SM4 for DirectX 11 from a single `.fx` source file, gating the profile with `#if OPENGL` preprocessor blocks.
146-
147-
The following describes what had to change from the reference and why.
148-
149-
### No integer bitwise operations.
150-
151-
The reference `CalcRootCode()` determines which roots of a quadratic contribute to coverage by extracting the signs of three y-coordinates via `asuint()` and a packed bit lookup table (`0x2E74U >> shift`). SM3 has no integer instructions at all, including `asuint`. Forme replaces this with `CalcRootEligibility()`, which computes the same sign-combination logic using float comparisons (`> 0.0`), multiplies, and `saturate`. The result is semantically identical but expressed entirely in floating-point arithmetic.
152-
153-
### No integer textures.
154-
155-
The reference band texture is typed `Texture2D<uint4>` and read with `.Load(int3(x, y, 0))`, which requires SM4. MojoShader cannot express typed integer textures or direct texel loads at SM3. The band texture is instead formatted as `RG32F`, storing unsigned 16-bit integers packed as floats. All texture reads use `tex2Dlod` with computed UV coordinates of the form `(float2(tx, ty) + 0.5) / texSize` rather than direct integer pixel coordinates. This requires helper functions (`FetchBandTexel`, `FetchCurveTexel`) that convert a flat linear texel index into a 2D UV before sampling.
156-
157-
### No integer band addressing.
158-
159-
The reference `CalcBandLoc()` wraps a linear texel offset back to a 2D coordinate using right-shift and bitwise AND, both integer-only operations. Forme uses `fmod()` and `floor()` instead to perform the same wrap-around arithmetic in float.
160-
161-
### No integer loop counters.
162-
163-
SM3 does not support `for` loops with integer counters when integer instructions are absent. Forme uses a `float` loop variable with the `[loop]` hint and `+= 1.0` increments.
164-
165-
### Vertex data packed differently.
166-
167-
The reference vertex shader unpacks band texture coordinates from a 32-bit integer stored in `tex.z` via `asuint()` and bit shifts. Without integer casting, this is not possible in SM3. Forme instead stores the band texture X and Y coordinates as a single packed float (y * textureWidth + x) passed through `tex.z`, and unpacks them with `fmod` and `floor` in the pixel shader, where `bandTexSize` is available.
168-
169-
### Dynamic dilation adapted for orthographic projection.
170-
171-
The reference vertex shader applies sub-pixel outward dilation (Lengyel 2017, Section 4) by pushing each vertex 0.5 screen pixels outward along its corner normal and adjusting the em-space sample coordinate by the same amount using the inverse Jacobian. The reference computes the inverse Jacobian from the perspective projection matrix, which does not apply to orthographic rendering.
172-
173-
Forme computes the inverse Jacobian per glyph on the CPU from the ratio of the glyph's em-space bounding box dimensions to its screen-space pixel dimensions (`invJxx = (ex1 - ex0) / (px1 - px0)`, `invJyy = (ey0 - ey1) / (py1 - py0)`). These values are constant across all four vertices of a glyph quad, so they are packed into the vertex stream alongside the band LUT UV in TEXCOORD2.
174-
175-
Because TEXCOORD2 was previously used to pass the per-glyph band transform (four floats) directly through the vertex stream, and SM3 provides only four vertex attribute slots, the band transform is instead stored in a per-font RGBA32F LUT texture (`FormeFontDevice.BandLutTexture`). The pixel shader fetches it using the LUT UV passed through TEXCOORD2, freeing two floats in that slot for the inverse Jacobian values.
176-
177-
### Near-linear epsilon widened.
178-
179-
The reference `SolveHorizPoly()` falls back to a linear solve when the quadratic coefficient `a.y` is smaller than `1.0 / 65536.0`. This threshold is too tight for GLSL produced by MojoShader. Curves with `a.y` slightly above that threshold caused near-division-by-zero, producing extreme t values and horizontal banding artifacts. The epsilon is widened to `0.0001`, which eliminates the artifacts without affecting any visible curve.
180-
181-
### Vertical pass unified with horizontal pass.
182-
183-
The reference has separate `SolveHorizPoly` and `SolveVertPoly` functions. Forme eliminates `SolveVertPoly` by swapping the x and y components of the control points before passing them to `SolveHorizPoly`, reducing code duplication.
184-
185-
## Third-Party Credits
5+
![Example of the GPU Rendering Form Demo](./docs/screnshot-01.png)
1866

187-
### Slug Algorithm
7+
For detailed README, refer to the [original repository](https://github.com/AristurtleDev/Forme).
1888

189-
[**Slug Algorithm**](https://github.com/EricLengyel/Slug) by Eric Lengyel (Terathon Software). GPU antialiased text rendering from quadratic Bezier glyph outlines. Published in the [Journal of Computer Graphics Techniques, Vol. 6, No. 2, 2017](https://jcgt.org/published/0006/02/02/).
9+
## Prerequisites (changed from original)
19010

191-
[Now in the public domain](https://terathon.com/blog/decade-slug.html) as of Match 17, 2026, licensed under MIT or Apache. Forme is an independent implementation of this algorithm; Terathon Software is not affiliated with this project.
11+
- .NET Framework 4.8
12+
- MonoGame 3.8.0.1641
19213

193-
For license detail on Slug, please see the [THIRD_PARTY_NOTICES](THIRD_PARTY_NOTICES).
14+
If you are working on content pipeline extensions, test with real content and document any new processor parameters.
19415

195-
### StbTrueTypeSharp
16+
If you are modifying the shader, recompile the .mgfxo files using src/Forme.MonoGame/compile-shaders.sh and commit the updated binaries alongside your changes. DirectX 11 shader compilation requires Windows.
19617

197-
[**StbTrueTypeSharp**](https://github.com/StbSharp/StbTrueTypeSharp) by the StbSharp project maintained by [Roman Shapiro](https://github.com/rds1983). C# port of Sean Barrett's stb_truetype.h for reading and parsing TrueType and OpenType font files. Used by the core Forme library to extract glyph metrics and outlines.
18+
## Our downgrade approach
19+
Retargets all projects from `net8.0` to `net48` and downgrades MonoGame from `3.8.4.1` to `3.8.0.1641`. Recompiles the shaders. Adds `Polyfill 10.0.0` + `System.Memory 4.6.3` to preserve full C# 14 feature usage (`ReadOnlySpan`, `init`, collection expressions, `ArgumentNullException.ThrowIfNull`, etc.) on .NET Framework 4.8.
19820

19921
## License
20022

demo/GPURenderingDemo/.config/dotnet-tools.json

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,16 @@
33
"isRoot": true,
44
"tools": {
55
"dotnet-mgcb": {
6-
"version": "3.8.4.1",
6+
"version": "3.8.0.1641",
77
"commands": [
88
"mgcb"
99
]
1010
},
1111
"dotnet-mgcb-editor": {
12-
"version": "3.8.4.1",
12+
"version": "3.8.0.1641",
1313
"commands": [
1414
"mgcb-editor"
1515
]
16-
},
17-
"dotnet-mgcb-editor-linux": {
18-
"version": "3.8.4.1",
19-
"commands": [
20-
"mgcb-editor-linux"
21-
]
22-
},
23-
"dotnet-mgcb-editor-windows": {
24-
"version": "3.8.4.1",
25-
"commands": [
26-
"mgcb-editor-windows"
27-
]
28-
},
29-
"dotnet-mgcb-editor-mac": {
30-
"version": "3.8.4.1",
31-
"commands": [
32-
"mgcb-editor-mac"
33-
]
3416
}
3517
}
3618
}

demo/GPURenderingDemo/Forme.MonoGame.GPU.Demo.DesktopGL/Forme.MonoGame.GPU.Demo.DesktopGL.csproj

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<OutputType>WinExe</OutputType>
4-
<TargetFramework>net8.0</TargetFramework>
5-
<RollForward>Major</RollForward>
6-
<PublishReadyToRun>false</PublishReadyToRun>
7-
<TieredCompilation>false</TieredCompilation>
4+
<TargetFrameworks>net8.0;net48</TargetFrameworks>
85
</PropertyGroup>
96
<PropertyGroup>
107
<ApplicationManifest>app.manifest</ApplicationManifest>

demo/GPURenderingDemo/Forme.MonoGame.GPU.Demo.Shared/Content/Content.mgcb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
#-------------------------------- References --------------------------------#
1212

13-
/reference:..\..\..\..\.artifacts\src\bin\Forme.MonoGame.Content.Pipeline\debug\Forme.MonoGame.Content.Pipeline.dll
13+
/reference:..\..\..\..\.artifacts\src\bin\Forme.MonoGame.Content.Pipeline\debug_netcoreapp3.1\Forme.MonoGame.Content.Pipeline.dll
1414

1515
#---------------------------------- Content ---------------------------------#
1616

demo/GPURenderingDemo/Forme.MonoGame.GPU.Demo.Shared/Forme.MonoGame.GPU.Demo.Shared.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFramework>net8.0</TargetFramework>
3+
<TargetFrameworks>net8.0;net48</TargetFrameworks>
44
<Platforms>AnyCPU;x64</Platforms>
55
</PropertyGroup>
66
<ItemGroup>

demo/GPURenderingDemo/Forme.MonoGame.GPU.Demo.WindowsDX/Forme.MonoGame.GPU.Demo.WindowsDX.csproj

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<OutputType>WinExe</OutputType>
4-
<TargetFramework>net8.0-windows</TargetFramework>
5-
<RollForward>Major</RollForward>
6-
<PublishReadyToRun>false</PublishReadyToRun>
7-
<TieredCompilation>false</TieredCompilation>
8-
<UseWindowsForms>true</UseWindowsForms>
4+
<TargetFrameworks>net8.0-windows;net48</TargetFrameworks>
95
</PropertyGroup>
106
<PropertyGroup>
117
<ApplicationManifest>app.manifest</ApplicationManifest>

demo/SpriteFontRenderingDemo/.config/dotnet-tools.json

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,16 @@
33
"isRoot": true,
44
"tools": {
55
"dotnet-mgcb": {
6-
"version": "3.8.4.1",
6+
"version": "3.8.0.1641",
77
"commands": [
88
"mgcb"
99
]
1010
},
1111
"dotnet-mgcb-editor": {
12-
"version": "3.8.4.1",
12+
"version": "3.8.0.1641",
1313
"commands": [
1414
"mgcb-editor"
1515
]
16-
},
17-
"dotnet-mgcb-editor-linux": {
18-
"version": "3.8.4.1",
19-
"commands": [
20-
"mgcb-editor-linux"
21-
]
22-
},
23-
"dotnet-mgcb-editor-windows": {
24-
"version": "3.8.4.1",
25-
"commands": [
26-
"mgcb-editor-windows"
27-
]
28-
},
29-
"dotnet-mgcb-editor-mac": {
30-
"version": "3.8.4.1",
31-
"commands": [
32-
"mgcb-editor-mac"
33-
]
3416
}
3517
}
3618
}

demo/SpriteFontRenderingDemo/Forme.MonoGame.SpriteFont.Demo.DesktopGL/Forme.MonoGame.SpriteFont.Demo.DesktopGL.csproj

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<OutputType>WinExe</OutputType>
4-
<TargetFramework>net8.0</TargetFramework>
5-
<RollForward>Major</RollForward>
6-
<PublishReadyToRun>false</PublishReadyToRun>
7-
<TieredCompilation>false</TieredCompilation>
4+
<TargetFrameworks>net8.0;net48</TargetFrameworks>
85
</PropertyGroup>
96
<PropertyGroup>
107
<ApplicationManifest>app.manifest</ApplicationManifest>

demo/SpriteFontRenderingDemo/Forme.MonoGame.SpriteFont.Demo.Shared/Content/Content.mgcb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
#-------------------------------- References --------------------------------#
1212

13-
/reference:..\..\..\..\.artifacts\src\bin\Forme.MonoGame.Content.Pipeline\debug\Forme.MonoGame.Content.Pipeline.dll
13+
/reference:..\..\..\..\.artifacts\src\bin\Forme.MonoGame.Content.Pipeline\debug_netcoreapp3.1\Forme.MonoGame.Content.Pipeline.dll
1414

1515
#---------------------------------- Content ---------------------------------#
1616

0 commit comments

Comments
 (0)