Skip to content

Latest commit

 

History

History
909 lines (687 loc) · 30.5 KB

File metadata and controls

909 lines (687 loc) · 30.5 KB

Rendering Backend Research for Splot

Date: 2026-01-20 Purpose: Evaluate rendering backends for a high-performance plotting library


Executive Summary

Does Any Single Library Meet ALL Requirements?

No. After extensive research, no single library satisfies all requirements:

Requirement Needed Best Available
C++17/20 Many options
Cross-platform (Linux/macOS/Windows) Many options
WebAssembly/WebGL Sokol, bgfx, Skia
GPU acceleration Many options
Millions of points @ 50Hz Datoviz only
Qt independence Most options
Permissive license Most options
All combined None

The gap: Libraries optimized for millions of points (Datoviz) lack WASM support. Libraries with WASM support (Sokol, bgfx) are rendering abstractions, not plotting libraries.

Conclusion: A custom plotting library built on top of a rendering abstraction (Sokol or bgfx) is the correct approach.


Requirements Checklist

From docs/requirements.md:

  • Language: C++17/20
  • Build: CMake
  • Package Manager: Conan
  • Platforms: Linux, macOS, Windows, WebAssembly
  • Backends: OpenGL, WebGL, Metal, Vulkan (optional)
  • Performance: Millions of points at 50 Hz
  • Architecture: Qt-independent core with optional Qt backend
  • License: Permissive (MIT, BSD, zlib)

Category 1: Rendering Abstraction Libraries

These provide a unified graphics API across multiple backends. You build your plotting logic on top.

1.1 Sokol ⭐ RECOMMENDED

Attribute Value
Repository https://github.com/floooh/sokol
Language C (C++ compatible)
License zlib/libpng (permissive) ✅
Backends OpenGL 3.3, OpenGL ES 2/3, WebGL2, D3D11, Metal
WebGPU Experimental backend in development
Vulkan ❌ Not supported
WASM ✅ First-class citizen
Compute Shaders ❌ Not yet (planned)

Components:

  • sokol_gfx.h - 3D API wrapper (main rendering)
  • sokol_app.h - Cross-platform window/context/input
  • sokol_gl.h - OpenGL 1.x-style immediate mode API
  • sokol_time.h - Time measurement
  • sokol_debugtext.h - Debug text rendering

Pros:

  • Explicitly designed for minimal WASM footprint
  • Single-header files, trivial to integrate
  • Very clean, easy-to-understand API
  • Active development by experienced author (Andre Weissflog)
  • No external dependencies
  • Excellent documentation and examples

Cons:

  • No Vulkan backend
  • No compute shader support (yet)
  • Less battle-tested than bgfx in production games

Why recommended for Splot:

  • WASM is a first-class citizen matching your web deployment needs
  • Minimal footprint keeps the library lightweight
  • Simple API lets you focus on plotting, not graphics plumbing
  • WebGPU backend coming for future-proofing

Resources:


1.2 bgfx ⭐ MATURE ALTERNATIVE

Attribute Value
Repository https://github.com/bkaradzic/bgfx
Language C/C++
License BSD-2-Clause (permissive) ✅
Backends D3D9, D3D11, D3D12, Metal, OpenGL 2.1-4.0, OpenGL ES 2-3.1, Vulkan, WebGL 1/2, WebGPU
WASM ✅ Supported via Emscripten
Compute Shaders ✅ Supported
Maturity 12+ years, used in commercial games

Pros:

  • Most comprehensive backend support (including Vulkan)
  • Battle-tested in production (games on Switch, PS4, Xbox)
  • Excellent shader cross-compilation tooling (shaderc)
  • WebGPU backend available
  • Large community and extensive examples

Cons:

  • Larger than Sokol
  • More complex API ("mid-level" abstraction)
  • Heavier integration burden

Why consider for Splot:

  • If Vulkan support is mandatory
  • If you want the most production-proven solution
  • If compute shaders are needed for data processing

Resources:


1.3 WebGPU Native (wgpu-native / Dawn)

Attribute Value
wgpu-native https://github.com/gfx-rs/wgpu-native
Dawn (Google) https://dawn.googlesource.com/dawn
Language C API (Rust or C++ implementation)
License Apache 2.0 / BSD-3-Clause (permissive) ✅
Backends Vulkan, Metal, D3D12, OpenGL ES (fallback)
Browser Native WebGPU API
WASM ✅ Via browser's WebGPU

Browser Support (fully available as of late 2025):

  • Chrome: April 2023
  • Edge: April 2023
  • Firefox: July 2025 (v141)
  • Safari: June 2025 (v26)

Pros:

  • Modern API designed for next-generation graphics
  • Same code runs native and in browser
  • Strong industry backing (Google, Mozilla, Apple)
  • Excellent compute shader support
  • Future-proof (expected to replace WebGL)

Cons:

  • Younger ecosystem than OpenGL-based solutions
  • More verbose than OpenGL for simple 2D
  • wgpu-native is Rust with C bindings (not pure C++)

Resources:


1.4 Diligent Engine

Attribute Value
Repository https://github.com/DiligentGraphics/DiligentEngine
Language C++
License Apache 2.0 (permissive) ✅
Backends D3D11, D3D12, OpenGL, OpenGL ES, Vulkan, Metal, WebGPU
WASM ✅ Via Emscripten

Pros:

  • Modern C++ API designed for next-gen APIs
  • Same source code works across all platforms without macros
  • Efficient shader resource binding
  • Multithreaded command recording

Cons:

  • More complex than Sokol/bgfx
  • Smaller community
  • Heavier weight

Resources:


1.5 SDL_gpu

Attribute Value
Repository https://github.com/grimfang4/sdl-gpu
Language C
License MIT (permissive) ✅
Backends OpenGL 1.1-4.0, OpenGL ES 1.1-3.0
WASM ✅ Via Emscripten

Note: SDL3 includes a new GPU API that may supersede this.

Pros:

  • Pure C, extremely portable
  • Minimal dependencies
  • Very small codebase

Cons:

  • No Vulkan/Metal
  • Less actively maintained than alternatives

Category 2: Scientific Visualization Libraries

These are complete visualization solutions, not just rendering backends.

2.1 Datoviz ⭐ PERFORMANCE REFERENCE

Attribute Value
Repository https://github.com/datoviz/datoviz
Documentation https://datoviz.org/
Language C/C++
License MIT (permissive) ✅
Backend Vulkan only
WASM ❌ Not supported

Performance Benchmarks (2019 high-end NVIDIA GPU):

Visualization Data Size Frame Rate
2D scatter plot 10M points 250 FPS
3D mesh 10M triangles 400 FPS
1000 signals 30M vertices total 200 FPS

Comparison with Matplotlib:

  • Datoviz is up to 10,000× faster
  • Matplotlib becomes sluggish or fails with large datasets

Pros:

  • Exactly the performance profile you need
  • Written by VisPy creator (GPU visualization expert)
  • Proven with millions of points
  • Clean C API

Cons:

  • Vulkan only - no WebGL/WASM support
  • More of a complete library than a backend
  • Would require significant work to add WebGL backend

Why important for Splot:


2.2 ImPlot

Attribute Value
Repository https://github.com/epezent/implot
Language C++
License MIT (permissive) ✅
Depends on Dear ImGui
Performance Tens to hundreds of thousands of points

Limitations:

  • "Don't expect millions to be a buttery smooth experience"
  • Requires downsampling for very large datasets
  • 16-bit index limit by default (must enable 32-bit)

Pros:

  • Excellent immediate-mode API design
  • Good reference for plotting API patterns
  • Easy integration with ImGui applications

Cons:

  • Cannot handle millions of points without downsampling
  • Tied to Dear ImGui ecosystem

Experimental GPU backend: Available in backends branch but not production-ready.


2.3 Matplot++

Attribute Value
Repository https://github.com/alandefreitas/matplotplusplus
Language C++
License MIT (permissive) ✅
Backends GnuPlot, experimental OpenGL

Not recommended: Primarily GnuPlot-based, not suitable for real-time high-performance rendering.


Category 3: 2D Graphics Engines

General-purpose 2D rendering, not specialized for data visualization.

3.1 Skia (CanvasKit)

Attribute Value
Repository https://skia.org/
Language C++
License BSD-3-Clause (permissive) ✅
Backends OpenGL, Vulkan, Metal, CPU, WebGL (CanvasKit)
WASM ✅ Via CanvasKit
Used by Chrome, Android, Flutter, Firefox

Pros:

  • Extremely mature and battle-tested
  • Excellent 2D path rendering and antialiasing
  • Rich text rendering
  • CanvasKit provides WASM support

Cons:

  • Large WASM binary (~10MB)
  • Not optimized for millions of data points
  • Complex build system
  • Overkill for plotting (designed for general UI)

3.2 Blend2D

Attribute Value
Repository https://blend2d.com/
Language C++
License zlib (permissive) ✅
Rendering CPU with JIT optimization
GPU ❌ Not supported

Not recommended: CPU-only, doesn't meet GPU acceleration requirement.


Category 4: Line Rendering Techniques

Critical for your use case - rendering millions of line segments efficiently.

The Problem

"Drawing lines might not sound like rocket science, but it's damn difficult to do well in OpenGL, particularly WebGL." — Matt DesLauriers

OpenGL GL_LINES limitations:

  • No line joins or caps
  • Max width often ~10px
  • Non-integer widths not supported
  • Poor/inconsistent antialiasing

Solution: Instanced Quad Rendering

Each line segment = 2 triangles (6 vertices) with:

  • Position, width, color as vertex attributes
  • Fragment shader for antialiasing

Performance (AMD FirePro W5000):

Segments Frame Rate
500,000 60 FPS
2,400,000 Max capacity

Fragment Shader Antialiasing

Instead of MSAA, calculate distance to line in fragment shader:

  • 100× faster than hardware MSAA
  • Better quality
  • Works consistently across all GPUs

Min-Max Tree Integration

From your requirements doc - reduces millions of points to ~1000-2000 vertical line segments:

  1. One vertical line per pixel column
  2. Line spans min to max Y in that column's X range
  3. Query any range in O(log n)
  4. Append new points in O(log n)

Combined approach for Splot:

  1. Min-Max Tree decimation → ~2000 segments
  2. Instanced quad rendering → GPU draws segments
  3. Fragment shader AA → smooth lines

Key Resources


Comparison Matrix

Backend Libraries

Library License WASM Vulkan Metal WebGPU Size Maturity
Sokol zlib ✅ ✅⭐ 🔄 Tiny Medium
bgfx BSD-2 ✅ Medium High ⭐
WebGPU Apache ✅ ✅⭐ ✅⭐ Medium Low
Diligent Apache ✅ Large Medium
SDL_gpu MIT ✅ Tiny Low

Visualization Libraries

Library License WASM Millions pts Real-time
Datoviz MIT ✅ ✅⭐ ✅⭐
ImPlot MIT ✅
Skia BSD-3 ✅

Final Recommendation

Primary Choice: Sokol

Splot Architecture with Sokol
─────────────────────────────

┌─────────────────────────────────────┐
│           Application               │
│      (PlotJuggler or other)         │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│           Splot Core                │  ← Platform-agnostic
│  • Data series interfaces           │
│  • Min-Max Tree decimation          │
│  • Scale/axis calculations          │
│  • Coordinate transforms            │
│  • Event abstraction                │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│         Splot Renderer              │  ← GPU rendering logic
│  • Instanced line rendering         │
│  • Point/marker shaders             │
│  • Text rendering                   │
│  • Fragment shader AA               │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│          sokol_gfx.h                │  ← Backend abstraction
├─────────────────────────────────────┤
│  GL3.3  │  WebGL2  │  Metal  │ D3D11│
└─────────────────────────────────────┘

Why Sokol over bgfx:

  1. WASM-first design - explicitly optimized for web deployment
  2. Simpler API - focus on plotting logic, not graphics complexity
  3. Smaller footprint - important for web bundle size
  4. WebGPU coming - future-proof without switching libraries

When to choose bgfx instead:

  1. Vulkan backend is mandatory
  2. Compute shaders needed for data processing
  3. Maximum production maturity required
  4. Targeting game consoles

Study Datoviz for:

  1. GPU buffer management patterns
  2. Efficient data upload strategies
  3. Vulkan-specific optimizations (if adding Vulkan later)
  4. Scientific visualization best practices

Next Steps

  1. Prototype with Sokol - Create minimal line rendering demo
  2. Implement Min-Max Tree - Core data structure for decimation
  3. Develop line shaders - Instanced quads with fragment AA
  4. Benchmark - Verify millions of points at 50Hz is achievable
  5. Add backends - Qt integration, then evaluate WebGPU

References

Primary Documentation

Academic Papers

Blog Posts & Tutorials

Benchmarks & Performance


Appendix A: In-Depth Datoviz Analysis

Source: Code analysis of https://github.com/datoviz/datoviz (cloned locally)

A.1 Why Datoviz is So Fast

Datoviz achieves 10M points @ 250 FPS through a combination of architectural decisions and GPU-optimized techniques:

1. Custom Memory Allocator (Slab Allocation)

File: src/alloc.c

Instead of creating many small Vulkan buffers (expensive), Datoviz allocates a few large shared buffers and sub-allocates regions internally.

// Linked-list based allocator with coalescing
struct DvzAlloc {
    DvzSize total_size;
    DvzSize alignment;
    Block* blocks;        // Linked list of free/allocated blocks
    DvzSize allocated_size;
};

Key features:

  • First-fit allocation with alignment support
  • Automatic coalescing of adjacent free blocks on deallocation
  • Auto-resize (doubles buffer size when full)
  • Reduces Vulkan API overhead dramatically

2. Dual System (CPU-GPU Synchronization)

File: src/scene/dual.c

The "Dual" abstraction links CPU arrays with GPU buffers and tracks dirty regions.

struct DvzDual {
    DvzBatch* batch;
    DvzArray* array;      // CPU-side data
    DvzId dat;            // GPU-side buffer ID
    uint32_t dirty_first; // First dirty element
    uint32_t dirty_last;  // Last dirty element
};

Optimization: Only uploads the changed portion of data:

void dvz_dual_update(DvzDual* dual) {
    // Only upload dirty range, not entire buffer
    DvzSize offset = dual->dirty_first * item_size;
    DvzSize size = (dirty_last - dirty_first) * item_size;
    dvz_upload_dat(batch, dat, offset, size, data, 0);
}

This is critical for streaming data - appending 1000 new points only uploads those 1000 points, not the entire million.

3. Baker System (Data Preprocessing)

File: src/scene/baker.c

The Baker transforms user data into GPU-optimal vertex buffers before upload. This "baking" step:

  • Multiplexes multiple attributes into interleaved vertex buffers
  • Handles data repetition (e.g., one color for many vertices)
  • Manages index buffers for instanced rendering
// Column-wise data setting with repetition
void dvz_baker_repeat(baker, attr_idx, first, count, repeats, data);

// Quad triangulation (6 vertices per quad)
void dvz_baker_quads(baker, attr_idx, first, count, tl_br);

4. Batch Rendering

Datoviz renders collections of similar objects in single draw calls:

  • 10M points = 1 draw call (not 10M draw calls)
  • All attributes (position, color, size) packed into vertex buffers
  • GPU processes all points in parallel

5. Fragment Shader Antialiasing

File: include/datoviz/scene/glsl/antialias.glsl

Instead of expensive MSAA, Datoviz computes antialiasing analytically in the fragment shader:

vec3 compute_distance(float distance, float linewidth) {
    float t = linewidth / 2.0 - antialias;
    float border_distance = abs(distance) - t;
    float alpha = border_distance / antialias;
    alpha = exp(-alpha * alpha);  // Gaussian falloff
    return vec3(signed_distance, border_distance, alpha);
}

vec4 stroke(float distance, float linewidth, vec4 fg_color) {
    vec3 dis = compute_distance(distance, linewidth);
    if (dis.y < 0.0)
        return fg_color;
    else
        return vec4(fg_color.rgb, fg_color.a * dis.z);  // Alpha blend at edges
}

Result: 100× faster than hardware MSAA with better quality.

6. SDF-Based Markers

File: include/datoviz/scene/glsl/markers.glsl, src/scene/glsl/graphics_marker.frag

Markers (circles, squares, stars, etc.) are rendered using Signed Distance Functions:

float marker_disc(vec2 P, float size) {
    return length(P) - size / 2;
}

float marker_diamond(vec2 P, float size) {
    float x = M_SQRT2 / 2.0 * (P.x - P.y);
    float y = M_SQRT2 / 2.0 * (P.x + P.y);
    return max(abs(x), abs(y)) - size / (2.0 * M_SQRT2);
}

Benefits:

  • One quad per marker (4 vertices)
  • Shape computed in fragment shader
  • Perfect antialiasing at any size
  • 20+ marker shapes with no texture overhead

7. Path Rendering (Miter Joins)

File: src/scene/glsl/graphics_path.vert

For connected lines (paths), the vertex shader:

  1. Takes 4 consecutive points (p0, p1, p2, p3)
  2. Computes miter joins at p1-p2 connection
  3. Expands each segment into a quad
  4. Handles caps at path endpoints
// Per-segment: 4 vertices forming a quad
// Input: p0 (prev), p1 (start), p2 (end), p3 (next)
vec2 miter_a = normalize(n0 + n1);  // Miter at p1
vec2 miter_b = normalize(n1 + n2);  // Miter at p2
float length_a = w / dot(miter_a, n1);  // Miter length

8. MSDF Text Rendering

File: src/scene/font.c, shaders use msdfgen

Text uses Multi-channel Signed Distance Fields (MSDF):

  • Pre-generated texture atlas of glyphs
  • Sharp text at any zoom level
  • Single texture lookup per fragment
// MSDF sampling in marker shader
vec3 msd = texture(tex, P + .5).rgb;
float sd = median(msd.r, msd.g, msd.b);
distance = 4 * sd * size_ / params.tex_scale - 2;

9. Asynchronous Transfers

File: src/transfers.c

CPU-GPU data transfers run on a dedicated thread:

// Separate thread for uploads/downloads
static void* _thread_transfers(void* user_data) {
    DvzTransfers* transfers = (DvzTransfers*)user_data;
    dvz_deq_dequeue_loop(transfers->deq, DVZ_TRANSFER_PROC_UD);
    return NULL;
}

Queues:

  • DVZ_TRANSFER_DEQ_UL - Upload queue
  • DVZ_TRANSFER_DEQ_DL - Download queue
  • DVZ_TRANSFER_DEQ_COPY - GPU-GPU copy queue

This prevents rendering stalls while data is being uploaded.

10. Vulkan-Specific Optimizations

  • Explicit memory management - No driver overhead
  • Command buffer recording - Pre-recorded, reusable
  • Pipeline caching - Compiled once, reused
  • Descriptor sets - Efficient uniform binding

A.2 Architecture Layers

┌─────────────────────────────────────────────────────────┐
│                    Scene API                             │
│  (Figure, Panel, Visual, Transform, Axes)               │
├─────────────────────────────────────────────────────────┤
│                 Visuals Library                          │
│  (Marker, Path, Image, Mesh, Glyph, Volume)             │
├─────────────────────────────────────────────────────────┤
│              Baker + Dual + Array                        │
│  (CPU-GPU sync, data transformation, dirty tracking)    │
├─────────────────────────────────────────────────────────┤
│          Datoviz Intermediate Protocol                   │
│  (Message-based requests: create/update/delete)         │
├─────────────────────────────────────────────────────────┤
│                    Renderer                              │
│  (Request processor, command recording, presentation)   │
├─────────────────────────────────────────────────────────┤
│                     vklite                               │
│  (Thin Vulkan wrapper: buffers, pipelines, commands)    │
├─────────────────────────────────────────────────────────┤
│                   Vulkan API                             │
└─────────────────────────────────────────────────────────┘

A.3 Key Techniques to Adopt for Splot

Technique Datoviz Implementation Splot Adaptation
Slab Allocation Custom allocator for GPU buffers Implement similar for Sokol buffers
Dirty Tracking Dual system with dirty_first/last Track modified ranges, upload only changes
Fragment AA Gaussian distance falloff Port shaders to GLSL 330 / WebGL2
SDF Markers 20+ shapes via distance functions Implement subset for plotting
Batch Rendering One draw call per visual type Group points/lines by style
MSDF Text msdfgen atlas generation Use msdf-atlas-gen or similar
Async Transfers Dedicated transfer thread Use Sokol's async buffer updates

A.4 What Datoviz Does NOT Have (Relevant to Splot)

  1. No Min-Max Tree - Datoviz renders all points, relies on raw GPU power
  2. No LOD/Decimation - No automatic downsampling for large datasets
  3. No WebGL/WASM - Vulkan only

For Splot, you need to add Min-Max Tree decimation on top of Datoviz-style rendering techniques to handle millions of points efficiently on WebGL.


Appendix B: NanoVG Analysis

Repository: https://github.com/memononen/nanovg

B.1 What is NanoVG?

NanoVG is a small antialiased 2D vector graphics library with an HTML5 Canvas-like API. It's designed for UI rendering and visualizations.

B.2 Potential Use Cases for Splot

Use Case NanoVG Suitability Recommendation
Line Antialiasing Good but not optimal Use custom fragment shader AA instead (faster)
Text Rendering Good (stb_truetype) Consider MSDF for sharper scaling
UI Elements Excellent Good for legends, labels, tooltips
Millions of Points ❌ Poor Not designed for this

B.3 Technical Details

Antialiasing Approach:

  • Geometry-based AA (expands geometry to include AA fringe)
  • Can use MSAA as alternative
  • Uses stencil buffer for complex shapes

Backends:

  • nanovg_gl.h - OpenGL 2.0, 3.2, ES 2.0, ES 3.0, WebGL

Text:

  • Uses stb_truetype for font rendering
  • Font atlas texture
  • Not as sharp as MSDF at extreme scales

B.4 Recommendation for Splot

Don't use NanoVG for core curve rendering. Here's why:

  1. Performance: NanoVG expands geometry for AA, creating many more vertices. For millions of points, this is expensive.

  2. Better Alternative: Fragment shader antialiasing (like Datoviz) is:

    • 100× faster
    • Better quality
    • Works with instanced rendering

Consider NanoVG (or similar) for:

  • Axis labels and tick marks
  • Legend rendering
  • Tooltips and annotations
  • UI overlays

Better alternatives for text:

  • MSDF text rendering (like Datoviz uses) - sharper at all sizes
  • Sokol_fontstash - integrates with Sokol
  • stb_truetype directly - if you want full control

B.5 nanovgXC (Improved Fork)

There's an improved fork with exact coverage antialiasing:

B.6 Conclusion on NanoVG

For Use
Curve rendering Custom shaders (Datoviz-style)
Point rendering SDF markers in fragment shader
Text rendering MSDF atlas or sokol_fontstash
UI/Legends NanoVG acceptable, but not required

Final verdict: NanoVG adds complexity without solving your core performance problem. Build custom shaders based on Datoviz patterns instead.


Appendix C: Implementation Priorities

Based on this research, here's the recommended implementation order:

Phase 1: Foundation

  1. Set up Sokol integration
  2. Implement basic line rendering with fragment shader AA
  3. Implement Min-Max Tree for decimation
  4. Benchmark: target 1M points @ 60 FPS

Phase 2: Core Visuals

  1. Implement SDF markers (disc, square, triangle, cross)
  2. Add text rendering (MSDF or sokol_fontstash)
  3. Implement axis system with tick generation
  4. Add grid lines

Phase 3: Interaction

  1. Pan/zoom with coordinate transforms
  2. Mouse tracking / crosshair
  3. Rectangle zoom selection

Phase 4: Polish

  1. Legend system
  2. Export (PNG, SVG)
  3. Qt backend integration
  4. WebAssembly build

Phase 5: Optimization

  1. Profile and optimize hot paths
  2. Add streaming data support
  3. Implement dirty tracking (Dual-style)
  4. Multi-curve batching

Appendix D: Dependency Decisions

D.1 Text Rendering

Decision: Use sokol_fontstash.h (part of Sokol), NOT NanoVG.

Rationale:

  • sokol_fontstash uses the same stb_truetype as NanoVG
  • Zero additional dependencies (already using Sokol)
  • Simpler integration
  • Same text quality

Future upgrade path: If sharper text at extreme zooms is needed, migrate to MSDF text rendering (like Datoviz). This is an optimization, not a starting requirement.

NanoVG rejected because:

  • Adds unnecessary dependency
  • No quality advantage over sokol_fontstash for text
  • Geometry-based approach not needed when we have fragment shader AA

D.2 Confirmed Dependencies

Component Library Reason
Graphics abstraction Sokol WASM-first, minimal, clean API
Text rendering sokol_fontstash.h Part of Sokol, uses stb_truetype
Font parsing stb_truetype Pulled in by fontstash
Math cglm or custom Lightweight, header-only

D.3 Rejected Dependencies

Library Reason for Rejection
NanoVG Redundant with sokol_fontstash, adds complexity
Skia Too large (~10MB WASM), overkill
bgfx More complex than needed (Sokol preferred)
ImPlot Can't handle millions of points