Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
48541be
Remove outdated glfw dependency entries from go.sum
llgcode Feb 13, 2026
86d4aaf
Initial plan
Copilot Feb 13, 2026
84ddd4a
Add OpenGL ES 2 backend with triangle-based rendering
Copilot Feb 13, 2026
5531d7a
Add tests and fix string rendering functions
Copilot Feb 13, 2026
322d869
Add implementation summary and update draw2dgl notes
Copilot Feb 13, 2026
be3d101
Fix code review issues: remove unused code and fix benchmark
Copilot Feb 13, 2026
77e9355
Update OpenGL version to ES 2.0 in shaders and sample code
llgcode Feb 13, 2026
592c082
Downgrade Go version to 1.23.12 and update golang.org/x/image depende…
llgcode Feb 13, 2026
9359369
Downgrade Go version to 1.23.0 in go.mod
llgcode Feb 13, 2026
fada914
Remove Go version specification and update toolchain in go.mod
llgcode Feb 13, 2026
a8cefe9
Downgrade Go version to 1.20 and update golang.org/x/image dependency…
llgcode Feb 13, 2026
0ab1f2c
Fix polygon vertex collection and add precision qualifiers to shaders
Copilot Feb 13, 2026
10e596e
Fix flattener state persistence bug across multiple paths
Copilot Feb 13, 2026
cb84b1e
Remove trailing whitespace
Copilot Feb 13, 2026
bb671a8
Co-authored-by: llgcode <524083+llgcode@users.noreply.github.com>
Copilot Feb 13, 2026
64aa706
Add VSCode settings for terminal and Go tools environment variables
llgcode Feb 13, 2026
b6ae4e6
Fix FillStroke to use DemuxFlattener for simultaneous fill and stroke
Copilot Feb 13, 2026
d305da6
Add debug logging to diagnose blank screen issue
Copilot Feb 13, 2026
34f4251
Fix triangulation winding order for screen coordinates
Copilot Feb 13, 2026
757e2e1
fix: correct rendering pipeline for OpenGL ES 2.0 backend
llgcode Feb 13, 2026
35e6808
fix: optimize redraw handling and reduce CPU usage in OpenGL ES 2.0 b…
llgcode Feb 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"terminal.integrated.env.windows": {
"path": "${env:path};C:/msys64/mingw64/bin"
},
"go.toolsEnvVars": {
"PATH": "${env:PATH};C:/msys64/mingw64/bin"
}
}
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ draw2d
[![GoDoc](https://godoc.org/github.com/llgcode/draw2d?status.svg)](https://godoc.org/github.com/llgcode/draw2d)
[![BuyMeaBeer](https://img.shields.io/badge/buy_me-a_beer-orange)](https://www.buymeacoffee.com/llgcoffee)

Package draw2d is a [go](http://golang.org) 2D vector graphics library with support for multiple outputs such as [images](http://golang.org/pkg/image) (draw2d), pdf documents (draw2dpdf), opengl (draw2dgl) and svg (draw2dsvg).
Package draw2d is a [go](http://golang.org) 2D vector graphics library with support for multiple outputs such as [images](http://golang.org/pkg/image) (draw2d), pdf documents (draw2dpdf), opengl (draw2dgl, draw2dgles2) and svg (draw2dsvg).
There's also a [Postscript reader](https://github.com/llgcode/ps) that uses draw2d.
draw2d is released under the BSD license.
See the [documentation](http://godoc.org/github.com/llgcode/draw2d) for more details.
Expand Down Expand Up @@ -107,7 +107,11 @@ func main() {

There are more examples here: https://github.com/llgcode/draw2d/tree/master/samples

Drawing on opengl is provided by the draw2dgl package.
Drawing on opengl is provided by two packages:
- **draw2dgl**: Legacy OpenGL 2.1 backend (uses fixed-function pipeline)
- **draw2dgles2**: Modern OpenGL ES 2.0+ backend (uses shaders and VBOs for better performance)

See [draw2dgles2/README.md](draw2dgles2/README.md) for details on the modern OpenGL ES 2 backend.

Testing
-------
Expand Down
9 changes: 9 additions & 0 deletions draw2dgl/notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,12 @@ References:
* http://http.developer.nvidia.com/GPUGems3/gpugems3_ch25.html
* http://research.microsoft.com/en-us/um/people/cloop/loopblinn05.pdf
* https://github.com/openframeworks/openFrameworks/issues/1190

## Note

This backend uses OpenGL 2.1 with the legacy fixed-function pipeline.

For a modern OpenGL ES 2.0+ compatible backend with better performance, see:
**draw2dgles2** - A shader-based renderer with triangle-based rendering and VBOs.

See [../draw2dgles2/README.md](../draw2dgles2/README.md) for details.
191 changes: 191 additions & 0 deletions draw2dgles2/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Where are the OpenGL Draw Calls? Understanding draw2dgl Architecture

## The Original Question

> "Where is the code that actually calls OpenGL? All I can find are a few lines in gc.go that render lines. But where are the triangles getting rendered?"

## The Answer

The original `draw2dgl` backend **does not render triangles**. Instead, it uses an unusual approach:

### How draw2dgl Works

1. **Paths are rasterized to horizontal scanlines** using the freetype rasterizer (`github.com/golang/freetype/raster`)
2. **Scanlines are converted to OpenGL lines** in the `Painter` struct
3. **Lines are rendered using legacy OpenGL** with client-side arrays

#### The Actual OpenGL Calls (draw2dgl/gc.go, lines 82-95)

```go
func (p *Painter) Flush() {
if len(p.vertices) != 0 {
// Enable legacy client-side arrays (deprecated in OpenGL 3.0+)
gl.EnableClientState(gl.COLOR_ARRAY)
gl.EnableClientState(gl.VERTEX_ARRAY)

// Set up pointers to vertex and color data
gl.ColorPointer(4, gl.UNSIGNED_BYTE, 0, gl.Ptr(p.colors))
gl.VertexPointer(2, gl.INT, 0, gl.Ptr(p.vertices))

// THIS IS THE ACTUAL DRAW CALL
// Renders horizontal lines from the rasterized spans
gl.DrawArrays(gl.LINES, 0, int32(len(p.vertices)/2))

gl.DisableClientState(gl.VERTEX_ARRAY)
gl.DisableClientState(gl.COLOR_ARRAY)

// Clear buffers for next batch
p.vertices = p.vertices[0:0]
p.colors = p.colors[0:0]
}
}
```

#### The Rendering Pipeline

```
Vector Path → Stroke/Fill → Rasterizer → Spans → Lines → OpenGL
```

Detailed flow:

1. **User defines path**: `gc.MoveTo()`, `gc.LineTo()`, `gc.CubicCurveTo()`, etc.
2. **Path is stroked or filled**: `gc.Stroke()` or `gc.Fill()` is called
3. **Path is flattened**: Curves converted to line segments
4. **Rasterizer processes path**: Freetype's rasterizer converts to coverage spans
5. **Painter receives spans**: Each span is a horizontal line with alpha coverage
6. **Spans converted to GL vertices**: `Painter.Paint()` adds vertices to buffer
7. **GL renders the lines**: `Painter.Flush()` calls `gl.DrawArrays(gl.LINES, ...)`

### Why No Triangles?

The original implementation chose to reuse the CPU rasterizer from freetype rather than implementing GPU-based triangulation. This means:

- ❌ **No triangle rendering**
- ❌ **No GPU-accelerated rasterization**
- ✅ CPU does all the heavy lifting
- ✅ OpenGL just displays the pre-rasterized result as lines

This approach has several problems:
- Limited to OpenGL 2.1 (client-side arrays are deprecated)
- Inefficient (rasterizing on CPU, then uploading lines to GPU)
- Not compatible with OpenGL ES or modern contexts
- Many draw calls (one per span)

## The Modern Solution: draw2dgles2

The new `draw2dgles2` package addresses these limitations with proper GPU rendering:

### Modern Rendering Pipeline

```
Vector Path → Flatten → Triangulate → Batch → GPU Shaders
```

1. **Paths are flattened** to line segments
2. **Polygons are triangulated** using ear-clipping algorithm
3. **Triangles are batched** in GPU memory
4. **Custom shaders render** the triangles

### Where Are the Triangles in draw2dgles2?

#### Triangulation (draw2dgles2/triangulate.go)

The `Triangulate()` function converts polygons to triangles:

```go
// Ear-clipping algorithm
func Triangulate(vertices []Point2D) []uint16 {
// Returns triangle indices
// Each triplet of indices forms one triangle
}
```

#### Triangle Rendering (draw2dgles2/gc.go)

```go
func (r *Renderer) AddPolygon(vertices []Point2D, c color.Color) {
// 1. Triangulate the polygon
triangleIndices := Triangulate(vertices)

// 2. Add vertices to batch
for _, v := range vertices {
r.vertices = append(r.vertices, v.X, v.Y)
}

// 3. Add colors
for range vertices {
r.colors = append(r.colors, rf, gf, bf, af)
}

// 4. Add triangle indices
for _, idx := range triangleIndices {
r.indices = append(r.indices, baseIdx+idx)
}
}

func (r *Renderer) Flush() {
// Upload to GPU via VBO
gl.BufferData(gl.ARRAY_BUFFER, len(data)*4, gl.Ptr(data), gl.STREAM_DRAW)

// THIS IS THE ACTUAL TRIANGLE DRAW CALL
gl.DrawElements(gl.TRIANGLES, int32(len(r.indices)), gl.UNSIGNED_SHORT, gl.Ptr(r.indices))
}
```

### Comparison Table

| Aspect | draw2dgl (Old) | draw2dgles2 (New) |
|--------|----------------|-------------------|
| **Rasterization** | CPU (freetype) | GPU (triangles) |
| **Primitives** | Horizontal lines | Triangles |
| **OpenGL Calls** | `DrawArrays(LINES)` | `DrawElements(TRIANGLES)` |
| **Memory** | Client arrays | VBOs |
| **Shaders** | None (fixed-function) | Custom GLSL |
| **Batching** | Per-span | All shapes |
| **OpenGL Version** | 2.1 only | ES 2.0 / 3.0+ / WebGL |
| **Performance** | Low | High |

## Code Locations

### draw2dgl (Legacy)

- **Main file**: `draw2dgl/gc.go`
- **OpenGL draw call**: Line 90: `gl.DrawArrays(gl.LINES, ...)`
- **Painter**: Lines 26-120 (converts rasterizer spans to lines)
- **Flush method**: Lines 82-96 (the actual rendering)

### draw2dgles2 (Modern)

- **Main file**: `draw2dgles2/gc.go`
- **OpenGL draw call**: Line ~168: `gl.DrawElements(gl.TRIANGLES, ...)`
- **Triangulation**: `draw2dgles2/triangulate.go`
- **Shaders**: `draw2dgles2/shaders.go`
- **Renderer**: Lines 18-284 (manages GPU resources)

## Key Insights

1. **draw2dgl doesn't use triangles** - it renders rasterized spans as horizontal lines
2. **The "trick" is in the Painter** - it receives coverage spans from the rasterizer and converts them to OpenGL lines
3. **Modern OpenGL requires triangles** - which is why draw2dgles2 was created
4. **Triangulation is necessary** for GPU rendering - the ear-clipping algorithm handles this

## Further Reading

- **Rasterization vs GPU Rendering**:
- CPU: compute pixel coverage → upload to GPU
- GPU: upload geometry → GPU computes coverage

- **Why Triangles**:
- GPUs are optimized for triangle rasterization
- All modern graphics APIs use triangles as the fundamental primitive
- Efficient hardware implementation

- **Modern Approaches**:
- NV_path_rendering (NVIDIA extension for vector graphics)
- Loop-Blinn algorithm (curve rendering via shaders)
- Stencil-and-cover (two-pass rendering)

## Conclusion

The original draw2dgl's OpenGL calls are minimal because it offloads rasterization to the CPU. The new draw2dgles2 backend provides true GPU-accelerated rendering with triangle-based primitives and modern shader support, making it suitable for OpenGL ES 2.0 and beyond.
Loading
Loading