Table of Contents

Asset Loading and Management - Research Report

Date: December 2024 Purpose: Evaluate approaches to loading and managing game assets (textures, models, audio, fonts) in a cross-platform C# engine

Executive Summary

For a cross-platform C# game engine, pure managed libraries without native dependencies should be preferred for portability and simplicity. The recommended stack is:

  • Textures: StbImageSharp (runtime) + BCnEncoder.NET (asset pipeline)
  • Models: SharpGLTF (glTF as primary runtime format)
  • Fonts: FontStashSharp (built on StbTrueTypeSharp)
  • Audio: NVorbis (Ogg Vorbis) + platform-specific playback

This combination provides full cross-platform support, no native dependencies, and active maintenance.


Image Loading Libraries

StbImageSharp

Attribute Value
Repository github.com/StbSharp/StbImageSharp
NuGet StbImageSharp
Latest Version 2.30.15 (September 2024)
License MIT/Public Domain

Strengths:

  • Pure C# - No native dependencies, port of stb_image.h
  • Lightweight - Minimal overhead, focused on loading only
  • Format support - PNG, JPG, BMP, TGA, GIF, PSD, HDR
  • Safe version available - SafeStbImageSharp for no-unsafe contexts
  • Game engine proven - Used by MonoGame, Unity extensions

Weaknesses:

  • No DDS/KTX support (GPU-compressed formats)
  • No image manipulation features
  • No encoding support (loading only)

API Example:

using StbImageSharp;

using var stream = File.OpenRead("texture.png");
var image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha);

// image.Data contains raw RGBA bytes
// image.Width, image.Height for dimensions

SixLabors.ImageSharp

Attribute Value
Repository github.com/SixLabors/ImageSharp
NuGet SixLabors.ImageSharp
Latest Version 3.x (active development)
License Apache 2.0 (Six Labors Split License for commercial)

Strengths:

  • Full-featured - Loading, saving, processing, manipulation
  • Pure managed - No native dependencies
  • SIMD optimized - Near-native performance with hardware acceleration
  • Extensive format support - PNG, JPEG, GIF, BMP, TIFF, WebP, and more
  • Image processing - Resize, crop, filters, transformations

Weaknesses:

  • Heavier - More overhead than StbImageSharp for simple loading
  • Licensing concerns - Commercial license required for revenue > $1M
  • Overkill for runtime - Better suited for asset pipeline tools

Best Use Case: Asset pipeline tooling, thumbnail generation, image processing


BCnEncoder.NET

Attribute Value
Repository github.com/Nominom/BCnEncoder.NET
NuGet BCnEncoder.Net
Latest Version 2.2.1
License MIT / Unlicense (dual)

Strengths:

  • GPU texture compression - BC1-3 (DXT), BC4-5 (RGTC), BC6-7 (BPTC)
  • Pure C# - .NET Standard 2.1 compatible
  • Output formats - KTX and DDS file formats
  • Quality settings - Fast, Balanced, BestQuality compression modes
  • ImageSharp integration - Works with SixLabors.ImageSharp

Weaknesses:

  • Requires ImageSharp as dependency
  • Compression is CPU-intensive (batch offline, not runtime)

Best Use Case: Asset pipeline for cooking textures to GPU-compressed formats

API Example:

using BCnEncoder.Encoder;
using BCnEncoder.Shared;

using var image = Image.Load<Rgba32>("texture.png");
var encoder = new BcEncoder
{
    OutputOptions =
    {
        GenerateMipMaps = true,
        Quality = CompressionQuality.Balanced,
        Format = CompressionFormat.Bc1,
        FileFormat = OutputFileFormat.Ktx
    }
};

using var output = File.Create("texture.ktx");
encoder.EncodeToStream(image, output);

Model Loading Libraries

SharpGLTF

Attribute Value
Repository github.com/vpenades/SharpGLTF
NuGet SharpGLTF.Core
Stars 538
Latest Version 1.0.5 (October 2024)
License MIT

Strengths:

  • Pure C# - No native dependencies
  • glTF 2.0 complete - Full spec support including extensions
  • Read and write - Both loading and saving models
  • Runtime helpers - SharpGLTF.Runtime namespace for GPU upload
  • Active development - Regular updates through 2024
  • Toolkit available - SharpGLTF.Toolkit for model manipulation

Weaknesses:

  • glTF format only (but glTF is the recommended runtime format)
  • More complex API than simple OBJ loaders

Why glTF?

  • Open standard maintained by Khronos Group
  • Stores mesh data in GPU-ready binary format
  • 5x smaller and 10x faster to load than text formats
  • Supports PBR materials, animations, skinning
  • Called the "JPEG of 3D"

API Example:

using SharpGLTF.Schema2;

var model = ModelRoot.Load("model.gltf");

foreach (var mesh in model.LogicalMeshes)
{
    foreach (var primitive in mesh.Primitives)
    {
        var positions = primitive.GetVertexAccessor("POSITION").AsVector3Array();
        var indices = primitive.GetIndexAccessor().AsIndicesArray();
        // Upload to GPU...
    }
}

AssimpNet

Attribute Value
Repository github.com/assimp/assimp-net
NuGet AssimpNet
Latest Version 4.1.0 (stable), 5.0.0-beta1 (preview)
License BSD 3-Clause

Strengths:

  • 40+ formats - FBX, OBJ, glTF, 3DS, Collada, and many more
  • Industry standard - Used widely in game development
  • Post-processing - Mesh optimization, tangent generation, triangulation
  • High-level API - Clean C# API wrapping native library

Weaknesses:

  • Native dependency - Requires Assimp native DLL per platform
  • Large binary - Native library adds significant size
  • Distribution complexity - Must bundle platform-specific binaries
  • Not mobile-friendly - Native compilation challenges

Best Use Case: Asset pipeline/import tool, not runtime loading

Recommendation: Use AssimpNet in asset pipeline to convert formats → glTF, then use SharpGLTF at runtime


Format Comparison: glTF vs FBX

Aspect glTF FBX
Licensing Open (Khronos) Proprietary (Autodesk)
Runtime Loading Excellent (GPU-ready) Requires processing
File Size Smaller (binary buffers) Larger
Animation Good Excellent (complex rigs)
PBR Materials Native support Historic lighting model
C# Libraries Pure managed available Requires native SDK
Recommendation Runtime format Authoring exchange format

Font Loading Libraries

FontStashSharp

Attribute Value
Repository github.com/FontStashSharp/FontStashSharp
NuGet FontStashSharp
License MIT

Strengths:

  • Pure C# - Built on StbTrueTypeSharp (no native deps)
  • Dynamic atlas - Glyphs rendered on demand, no character range needed
  • Multiple fonts - One FontSystem can hold Latin + Japanese + Emoji
  • Rich text - Colored text, blur, stroke effects
  • Game engine integration - MonoGame, FNA, Stride backends
  • Custom renderers - Can implement for any graphics API

Weaknesses:

  • Designed around SpriteBatch-style rendering
  • May need custom renderer for raw OpenGL/Vulkan

API Example:

using FontStashSharp;

var fontSystem = new FontSystem();
fontSystem.AddFont(File.ReadAllBytes("arial.ttf"));

var font = fontSystem.GetFont(24); // 24pt size
font.DrawText(spriteBatch, "Hello World", new Vector2(100, 100), Color.White);

StbTrueTypeSharp

Attribute Value
Repository github.com/StbSharp/StbTrueTypeSharp
License MIT/Public Domain

Strengths:

  • Pure C# - Port of stb_truetype.h
  • Minimal - Just font loading and glyph rasterization
  • Foundation for FontStashSharp - Proven by higher-level libraries

Weaknesses:

  • Low-level API - Need to build atlas management yourself

Recommendation: Use FontStashSharp which wraps this with a nice API


SharpFont (FreeType Wrapper)

Attribute Value
Repository github.com/Robmaister/SharpFont
Status Unmaintained - Seeking maintainer
Last Release 4.0.1 (2016)

Issues:

  • No releases in 8+ years
  • Native FreeType dependency
  • Unclear maintenance future

Recommendation: Avoid - Use FontStashSharp instead


Audio Loading Libraries

NVorbis

Attribute Value
Repository github.com/NVorbis/NVorbis
NuGet NVorbis
Latest Version 0.10.5
License MIT

Strengths:

  • Pure C# - Fully managed Ogg Vorbis decoder
  • No P/Invoke - Runs in partial trust environments
  • Streaming support - Efficient for large audio files
  • .NET Standard 2.0 - Cross-platform compatible
  • NAudio integration - NAudio.Vorbis package available

Weaknesses:

  • Ogg Vorbis only (no MP3, no WAV decoding)
  • Decoding only (no encoding)

API Example:

using NVorbis;

using var vorbis = new VorbisReader("music.ogg");
var buffer = new float[vorbis.Channels * vorbis.SampleRate]; // 1 second buffer
int samplesRead = vorbis.ReadSamples(buffer, 0, buffer.Length);
// Submit to audio API...

NAudio

Attribute Value
Repository github.com/naudio/NAudio
NuGet NAudio
Latest Version 2.2.1
License MIT

Strengths:

  • Comprehensive - MP3, WAV, AIFF, WMA decoding
  • Windows audio APIs - WASAPI, WaveIn/Out, ASIO, MediaFoundation
  • Recording - Audio capture support
  • Effects - DSP, mixing, sample rate conversion

Weaknesses:

  • Windows only - P/Invoke to Windows audio APIs
  • Not cross-platform - Linux/macOS not supported
  • Large dependency for just file loading

Recommendation: Use only for Windows-specific audio features or asset pipeline tools on Windows


Cross-Platform Audio Alternatives

Library Description Native Deps Notes
OpenAL (Silk.NET) 3D audio API Yes Cross-platform playback
SDL2 Audio Simple audio Yes Via Silk.NET.SDL
ManagedBass BASS wrapper Yes Feature-rich, unclear docs
libsoundio-sharp Low-level I/O Yes Cross-platform, MIT license

Strategy: Use NVorbis for decoding Ogg files, platform-specific APIs for playback (OpenAL via Silk.NET is a good choice for cross-platform 3D audio).


Asset Pipeline Architecture

Development vs Release Workflow

┌─────────────────────────────────────────────────────────────────────────┐
│                           DEVELOPMENT                                    │
├─────────────────────────────────────────────────────────────────────────┤
│  Source Assets          →    Asset Pipeline    →    Development Build   │
│  ─────────────              ──────────────         ─────────────────    │
│  • textures/*.png           • Validate            • Load source formats │
│  • models/*.fbx             • (Optional cook)     • Hot reload support  │
│  • audio/*.wav              • Watch for changes   • Debug-friendly      │
│  • fonts/*.ttf                                                          │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│                             RELEASE                                      │
├─────────────────────────────────────────────────────────────────────────┤
│  Source Assets          →    Asset Pipeline    →    Release Build       │
│  ─────────────              ──────────────         ─────────────────    │
│  • textures/*.png           • Compress (BC1-7)    • GPU-compressed tex  │
│  • models/*.fbx             • Convert (→ glTF)    • Binary model data   │
│  • audio/*.wav              • Transcode (→ Ogg)   • Compressed audio    │
│  • fonts/*.ttf              • Generate mipmaps    • PAK archives        │
│                             • Build manifests     • Fast loading        │
└─────────────────────────────────────────────────────────────────────────┘

Asset Cooking Recommendations

Asset Type Source Format Cooked Format Tool
Textures PNG, TGA, PSD KTX (BC1-BC7) BCnEncoder.NET
Models FBX, OBJ glTF Binary (.glb) AssimpNet → SharpGLTF
Audio WAV Ogg Vorbis External (oggenc)
Fonts TTF/OTF TTF (unchanged)
Data JSON, YAML Binary MessagePack, Protobuf

Virtual File System (VFS)

A VFS provides abstraction over file access, enabling:

  • Mounting - Map directories or archives to virtual paths
  • Layering - Mod files override base game files
  • Portability - Same API for files, archives, network resources

Recommended C# Library: Lurler's VirtualFileSystem

VFS Architecture:

public interface IFileSystem
{
    Stream Open(string path);
    bool Exists(string path);
    IEnumerable<string> List(string directory);
}

public class VirtualFileSystem : IFileSystem
{
    private readonly List<IMount> mounts = new();

    public void Mount(string virtualPath, IMount mount) { ... }

    // Later mounts override earlier ones
    // mount("/", baseGame)
    // mount("/", modPack1)  // overrides baseGame files
}

Mount Types:

  • DirectoryMount - Physical filesystem directory
  • ZipMount - ZIP archive (PAK file with ZIP format)
  • MemoryMount - In-memory files (testing, generated content)

Async Loading Pattern

Goal: Load assets without blocking the main thread, upload to GPU when ready.

┌──────────┐     ┌────────────────┐     ┌─────────────┐     ┌───────────┐
│  Request │ ──▶ │ Loading Queue  │ ──▶ │ Background  │ ──▶ │  GPU      │
│          │     │                │     │   Thread    │     │  Upload   │
└──────────┘     └────────────────┘     └─────────────┘     └───────────┘
                                              │                    │
                                              │ Decode/Process     │ Main Thread
                                              │                    │ Time-sliced
                                              ▼                    │
                                        ┌───────────┐              ▼
                                        │ CPU Data  │ ──────▶  Ready!
                                        │  Buffer   │
                                        └───────────┘

Key Principles:

  1. Disk I/O on background threads - Never block main thread on file reads
  2. Processing on background threads - Decode images, parse models off main
  3. GPU upload on main thread - Most APIs require main thread for GL calls
  4. Time-sliced uploads - Budget upload time per frame (e.g., 2-10ms)
  5. Ring buffer for staging - Reuse memory for pending uploads

API Design:

public interface IAssetLoader
{
    // Fire and forget - asset will be ready eventually
    AssetHandle<T> LoadAsync<T>(string path) where T : IAsset;

    // Check if ready
    bool IsLoaded(AssetHandle handle);

    // Get loaded asset (throws if not ready)
    T Get<T>(AssetHandle<T> handle) where T : IAsset;

    // Pump GPU uploads - call once per frame
    void ProcessPendingUploads(TimeSpan budget);
}

Decision Matrix

Image Loading

Criteria Weight StbImageSharp ImageSharp BCnEncoder
Pure C# High 10 10 10
Performance High 9 8 7
Simplicity Medium 9 6 7
Format Support Medium 7 9 6
Maintenance Medium 8 9 7
Use Case - Runtime Pipeline Pipeline

Model Loading

Criteria Weight SharpGLTF AssimpNet
Pure C# High 10 3
Format Support Medium 5 10
Maintenance High 9 7
Runtime Suitable High 10 4
Recommendation - Runtime Pipeline

Font Loading

Criteria Weight FontStashSharp SharpFont (FreeType)
Pure C# High 10 3
Maintenance High 9 1
Features Medium 8 9
Recommendation - Use this Avoid

Audio Loading

Criteria Weight NVorbis NAudio
Pure C# High 10 5
Cross-Platform High 10 2
Format Support Medium 4 9
Recommendation - Decoding Windows pipeline

Recommendations Summary

Runtime Asset Loading Stack

Asset Type Library Why
Textures StbImageSharp Pure C#, fast, proven
GPU Textures Custom KTX/DDS loader Load pre-compressed textures
Models SharpGLTF Pure C#, glTF is ideal runtime format
Fonts FontStashSharp Pure C#, dynamic atlas, rich features
Audio NVorbis Pure C#, streaming Ogg Vorbis

Asset Pipeline Stack

Task Library Why
Image Processing ImageSharp Full manipulation API
Texture Compression BCnEncoder.NET GPU formats (BC1-7)
Model Import AssimpNet 40+ formats support
Model Export SharpGLTF Write optimized glTF
Audio Conversion External (oggenc) Quality Vorbis encoding

All recommended runtime libraries are pure C#:

  • ✅ StbImageSharp
  • ✅ SharpGLTF
  • ✅ FontStashSharp
  • ✅ NVorbis

This ensures:

  • Single assembly deployment
  • Mobile platform compatibility
  • No platform-specific DLLs to manage
  • Easier debugging and testing

Integration Notes for KeenEyes

Asset Components

[Component]
public partial struct TextureRef
{
    public AssetHandle<Texture2D> Handle;
}

[Component]
public partial struct MeshRef
{
    public AssetHandle<Mesh> Handle;
}

[Component]
public partial struct AudioSource
{
    public AssetHandle<AudioClip> Clip;
    public float Volume;
    public bool Loop;
}

Asset Loading System

public class AssetUploadSystem : SystemBase
{
    private readonly IAssetLoader loader;
    private readonly TimeSpan uploadBudget = TimeSpan.FromMilliseconds(4);

    public override void Update(float deltaTime)
    {
        // Time-slice GPU uploads to avoid frame drops
        loader.ProcessPendingUploads(uploadBudget);
    }
}

Query for Renderables

public class RenderSystem : SystemBase
{
    public override void Update(float deltaTime)
    {
        foreach (var entity in World.Query<Transform, MeshRef, TextureRef>())
        {
            ref readonly var transform = ref World.Get<Transform>(entity);
            ref readonly var mesh = ref World.Get<MeshRef>(entity);
            ref readonly var texture = ref World.Get<TextureRef>(entity);

            if (!AssetLoader.IsLoaded(mesh.Handle)) continue;
            if (!AssetLoader.IsLoaded(texture.Handle)) continue;

            // Submit draw call with loaded assets
            Renderer.Draw(
                AssetLoader.Get(mesh.Handle),
                AssetLoader.Get(texture.Handle),
                transform
            );
        }
    }
}

Next Steps

  • [ ] Benchmark StbImageSharp vs ImageSharp load times
  • [ ] Prototype async asset loader with GPU upload queue
  • [ ] Create KTX/DDS loader for GPU-compressed textures
  • [ ] Design AssetHandle with staleness detection
  • [ ] Evaluate VFS libraries for mod support
  • [ ] Create asset pipeline CLI tool

Sources

Image Loading

Model Loading

Font Loading

Audio Loading

Asset Pipeline