text/glyph_cache.go

Functions Structs

Functions

func Clear

Clear removes all entries from the cache.

func (c *GlyphCache) Clear() {
	for i := 0; i < numShards; i++ {
		shard := c.shards[i]
		shard.mu.Lock()
		shard.entries = make(map[OutlineCacheKey]*glyphEntry, shard.maxEntries)
		shard.head = nil
		shard.tail = nil
		shard.count = 0
		shard.mu.Unlock()
	}
}

func CurrentFrame

CurrentFrame returns the current frame number.

func (c *GlyphCache) CurrentFrame() uint64 {
	return c.currentFrame.Load()
}

func DefaultGlyphCacheConfig

DefaultGlyphCacheConfig returns the default cache configuration.

func DefaultGlyphCacheConfig() GlyphCacheConfig {
	return GlyphCacheConfig{
		MaxEntries:	4096,
		FrameLifetime:	64,
	}
}

func Delete

Delete removes an entry from the cache.

func (c *GlyphCache) Delete(key OutlineCacheKey) {
	shard := c.getShard(key)

	shard.mu.Lock()
	defer shard.mu.Unlock()

	entry, ok := shard.entries[key]
	if !ok {
		return
	}

	shard.remove(entry)
	delete(shard.entries, key)
	shard.count--
}

func Get

Get retrieves a cached glyph outline.

Returns nil if not found.

func (c *GlyphCache) Get(key OutlineCacheKey) *GlyphOutline {
	shard := c.getShard(key)
	frame := c.currentFrame.Load()

	shard.mu.Lock()
	entry, ok := shard.entries[key]
	if !ok {
		shard.mu.Unlock()
		c.stats.Misses.Add(1)
		return nil
	}

	// Update access time and move to front
	entry.lastAccessFrame = frame
	shard.moveToFront(entry)
	outline := entry.outline
	shard.mu.Unlock()

	c.stats.Hits.Add(1)
	return outline
}

func Get

Get retrieves a cache from the pool.

func (p *GlyphCachePool) Get() *GlyphCache {
	return p.pool.Get().(*GlyphCache)
}

func GetGlobalGlyphCache

GetGlobalGlyphCache returns the global shared glyph cache.

func GetGlobalGlyphCache() *GlyphCache {
	return globalGlyphCache
}

func GetOrCreate

GetOrCreate retrieves a cached outline or creates one using the provided function.

This is an atomic operation that avoids redundant creation.

func (c *GlyphCache) GetOrCreate(key OutlineCacheKey, create func() *GlyphOutline) *GlyphOutline {
	// First try a fast read-only lookup
	shard := c.getShard(key)
	frame := c.currentFrame.Load()

	shard.mu.RLock()
	if _, ok := shard.entries[key]; ok {
		shard.mu.RUnlock()
		// Upgrade to write lock to update access time
		shard.mu.Lock()
		if entry, ok := shard.entries[key]; ok {
			entry.lastAccessFrame = frame
			shard.moveToFront(entry)
			outline := entry.outline
			shard.mu.Unlock()
			c.stats.Hits.Add(1)
			return outline
		}
		shard.mu.Unlock()
	} else {
		shard.mu.RUnlock()
	}

	// Need to create
	if create == nil {
		c.stats.Misses.Add(1)
		return nil
	}

	outline := create()
	if outline != nil {
		c.Set(key, outline)
	} else {
		c.stats.Misses.Add(1)
	}
	return outline
}

func HitRate

HitRate returns the cache hit rate as a percentage.

Returns 0 if there are no accesses.

func (c *GlyphCache) HitRate() float64 {
	hits := c.stats.Hits.Load()
	misses := c.stats.Misses.Load()
	total := hits + misses
	if total == 0 {
		return 0
	}
	return float64(hits) / float64(total) * 100
}

func Len

Len returns the total number of cached entries.

func (c *GlyphCache) Len() int {
	total := 0
	for i := 0; i < numShards; i++ {
		shard := c.shards[i]
		shard.mu.RLock()
		total += shard.count
		shard.mu.RUnlock()
	}
	return total
}

func Maintain

Maintain performs periodic maintenance on the cache.

It evicts entries that haven't been accessed for FrameLifetime frames.

Call this once per frame for frame-based eviction.

func (c *GlyphCache) Maintain() {
	frame := c.currentFrame.Add(1)
	// FrameLifetime is validated to be positive in NewGlyphCacheWithConfig
	frameLifetime := max(c.config.FrameLifetime, 1)

	// Avoid underflow when frame < FrameLifetime
	// frameLifetime is guaranteed >= 1 so conversion is safe
	frameLifetimeU64 := uint64(frameLifetime)	//nolint:gosec // validated >= 1 above
	if frame < frameLifetimeU64 {
		return
	}
	threshold := frame - frameLifetimeU64

	for i := 0; i < numShards; i++ {
		shard := c.shards[i]
		shard.mu.Lock()

		// Walk from tail (oldest) and evict stale entries
		entry := shard.tail
		for entry != nil && entry.lastAccessFrame < threshold {
			prev := entry.prev
			delete(shard.entries, entry.key)
			shard.remove(entry)
			shard.count--
			c.stats.Evictions.Add(1)
			entry = prev
		}

		shard.mu.Unlock()
	}
}

func NewGlyphCache

NewGlyphCache creates a new glyph cache with default configuration.

func NewGlyphCache() *GlyphCache {
	return NewGlyphCacheWithConfig(DefaultGlyphCacheConfig())
}

func NewGlyphCachePool

NewGlyphCachePool creates a new pool of glyph caches.

func NewGlyphCachePool() *GlyphCachePool {
	return &GlyphCachePool{
		pool: sync.Pool{
			New: func() any {
				return NewGlyphCache()
			},
		},
	}
}

func NewGlyphCacheWithConfig

NewGlyphCacheWithConfig creates a new glyph cache with the given configuration.

func NewGlyphCacheWithConfig(config GlyphCacheConfig) *GlyphCache {
	if config.MaxEntries <= 0 {
		config.MaxEntries = 4096
	}
	if config.FrameLifetime <= 0 {
		config.FrameLifetime = 64
	}

	c := &GlyphCache{
		config: config,
	}

	// Divide entries among shards
	entriesPerShard := (config.MaxEntries + numShards - 1) / numShards

	for i := 0; i < numShards; i++ {
		c.shards[i] = &glyphShard{
			entries:	make(map[OutlineCacheKey]*glyphEntry, entriesPerShard),
			maxEntries:	entriesPerShard,
		}
	}

	return c
}

func Put

Put returns a cache to the pool.

func (p *GlyphCachePool) Put(c *GlyphCache) {
	if c != nil {
		c.Clear()
		p.pool.Put(c)
	}
}

func ResetStats

ResetStats resets the cache statistics.

func (c *GlyphCache) ResetStats() {
	c.stats.Hits.Store(0)
	c.stats.Misses.Store(0)
	c.stats.Evictions.Store(0)
	c.stats.Insertions.Store(0)
}

func Set

Set stores a glyph outline in the cache.

If the cache is full, the least recently used entry is evicted.

func (c *GlyphCache) Set(key OutlineCacheKey, outline *GlyphOutline) {
	if outline == nil {
		return
	}

	shard := c.getShard(key)
	frame := c.currentFrame.Load()

	shard.mu.Lock()
	defer shard.mu.Unlock()

	// Check if already exists
	if existing, ok := shard.entries[key]; ok {
		existing.outline = outline
		existing.lastAccessFrame = frame
		shard.moveToFront(existing)
		return
	}

	// Create new entry
	entry := &glyphEntry{
		key:			key,
		outline:		outline,
		lastAccessFrame:	frame,
	}

	// Evict if necessary
	for shard.count >= shard.maxEntries && shard.tail != nil {
		shard.removeTail()
		c.stats.Evictions.Add(1)
	}

	// Add to cache
	shard.entries[key] = entry
	shard.addToFront(entry)
	shard.count++
	c.stats.Insertions.Add(1)
}

func SetGlobalGlyphCache

SetGlobalGlyphCache replaces the global glyph cache.

The old cache is returned for cleanup if needed.

func SetGlobalGlyphCache(cache *GlyphCache) *GlyphCache {
	if cache == nil {
		cache = NewGlyphCache()
	}
	old := globalGlyphCache
	globalGlyphCache = cache
	return old
}

func Stats

Stats returns cache statistics.

func (c *GlyphCache) Stats() (hits, misses, evictions, insertions uint64) {
	return c.stats.Hits.Load(),
		c.stats.Misses.Load(),
		c.stats.Evictions.Load(),
		c.stats.Insertions.Load()
}

Structs

type GlyphCacheConfig struct

GlyphCacheConfig holds configuration for GlyphCache.

type GlyphCacheConfig struct {
	// MaxEntries is the maximum number of cached glyph outlines.
	// Default: 4096
	MaxEntries	int

	// FrameLifetime is the number of frames an entry can be unused
	// before being eligible for eviction during Maintain().
	// Default: 64
	FrameLifetime	int
}

type OutlineCacheKey struct

OutlineCacheKey uniquely identifies a cached glyph outline.

type OutlineCacheKey struct {
	// FontID is a unique identifier for the font.
	FontID	uint64

	// GID is the glyph index within the font.
	GID	GlyphID

	// Size is the font size in ppem (pixels per em).
	// We use int16 for efficiency; sizes above 32K are rare.
	Size	int16

	// Hinting indicates the hinting mode used.
	Hinting	Hinting
}

type GlyphCache struct

GlyphCache is a thread-safe LRU cache for glyph outlines.

It provides fast lookups with automatic eviction of least recently used entries.

 

The cache is sharded to reduce lock contention in concurrent access patterns.

It supports both capacity-based eviction (when MaxEntries is reached) and

frame-based eviction (during Maintain() calls).

 

GlyphCache is safe for concurrent use.

type GlyphCache struct {
	// shards hold the actual cache entries
	shards	[numShards]*glyphShard

	// config holds cache configuration
	config	GlyphCacheConfig

	// currentFrame is the current frame counter for frame-based eviction
	currentFrame	atomic.Uint64

	// stats holds cache statistics
	stats	GlyphCacheStats
}

type GlyphCacheStats struct

GlyphCacheStats holds cache statistics.

type GlyphCacheStats struct {
	Hits		atomic.Uint64
	Misses		atomic.Uint64
	Evictions	atomic.Uint64
	Insertions	atomic.Uint64
}

type GlyphCachePool struct

GlyphCachePool manages a pool of GlyphCaches for per-thread usage.

This can further reduce contention in highly concurrent scenarios.

type GlyphCachePool struct {
	pool sync.Pool
}