Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,35 @@ sampling to pre-baked 10-step glyph variants for A/B comparison.

These are implemented for OpenGL and Vulkan backends. Other backends ignore them for now.

## Font Cache Management

Font size/style changes generate different `FontId` / glyph hashes by design.
If you want to explicitly clear cached glyphs and font entries, use:

- `clearFontCache(font: FigFont)`
- `clearTypefaceCache(typefaceId: TypefaceId)`
- `clearAllFontCaches()`

Common-level usage (`import figdraw/commons`) returns the removed glyph `ImageId`s:

```nim
let removedGlyphs = clearFontCache(uiFont)
let removedTypefaceGlyphs = clearTypefaceCache(uiFont.typefaceId)
let removedAllGlyphs = clearAllFontCaches()
```

Renderer-level usage (`import figdraw/figrender`) also drops matching atlas entries
from the live backend context and returns how many entries were removed:

```nim
let droppedForFont = renderer.clearFontCache(uiFont)
let droppedForTypeface = renderer.clearTypefaceCache(uiFont.typefaceId)
let droppedAll = renderer.clearAllFontCaches()
```

Note: cache clearing removes lookup entries; atlas space is not compacted in place.
Freed regions are not repacked until atlas recreation/growth.

## Thread Safety Notes

- Rendering is structured so that preparing render lists/trees can be done off-thread.
Expand Down
1 change: 1 addition & 0 deletions src/figdraw/common/fontglyphs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ proc generateGlyph*(
let
variant = clampGlyphVariantSubpixelStep(subpixelVariant)
hashFill = glyph.hash(lcdFiltering = lcdFiltering, subpixelVariant = variant)
trackGlyphImage(glyph.fontId, hashFill.ImageId)

if (not force) and hasImage(hashFill.ImageId):
return nil
Expand Down
38 changes: 35 additions & 3 deletions src/figdraw/common/fontutils.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import std/[os, unicode, sequtils, tables, strutils, sets, hashes, math]
import std/[os, unicode, sequtils, tables, strutils, sets, hashes, math, locks]
import std/isolation

import pkg/vmath
Expand All @@ -14,7 +14,35 @@ import ./fonttypes
import ./typefaces
import ./fontglyphs

export loadTypeface, convertFont, registerStaticTypeface
export loadTypeface, convertFont, registerStaticTypeface, fontCacheId

proc clearFontCache*(font: FigFont): seq[ImageId] =
## Clears cached font metadata and generated glyph image ids for a font.
let fontId = font.fontCacheId()
withLock(fontLock):
if fontId in fontTable:
fontTable.del(fontId)
result = clearGlyphImagesForFonts([fontId])

proc clearTypefaceCache*(typefaceId: TypefaceId): seq[ImageId] =
## Clears a typeface and every cached font/glyph that depends on it.
var fontIds: seq[FontId]
withLock(fontLock):
for fontId, cachedFont in fontTable.pairs():
if cachedFont.typefaceId == typefaceId:
fontIds.add(fontId)
for fontId in fontIds:
fontTable.del(fontId)
if typefaceId in typefaceTable:
typefaceTable.del(typefaceId)
result = clearGlyphImagesForFonts(fontIds)

proc clearAllFontCaches*(): seq[ImageId] =
## Clears all cached font/typeface metadata and glyph image ids.
withLock(fontLock):
fontTable.clear()
typefaceTable.clear()
result = clearGlyphImagesForAllFonts()

proc calcMinMaxContent(
textLayout: GlyphArrangement
Expand Down Expand Up @@ -99,7 +127,11 @@ proc typeset*(
pfs.add(pf)
spans.add(newSpan(txt, pf))
assert not pf.typeface.isNil
let lineHeight = if pf.lineHeight >= 0: pf.lineHeight else: pf.defaultLineHeight()
let lineHeight =
if pf.lineHeight >= 0:
pf.lineHeight
else:
pf.defaultLineHeight()
let lineGap = (lineHeight / pf.scale) - pf.typeface.ascent + pf.typeface.descent
let baselineOffset = round((pf.typeface.ascent + lineGap / 2) * pf.scale)
gfonts.add GlyphFont(
Expand Down
32 changes: 32 additions & 0 deletions src/figdraw/common/imgutils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type
var
imageChan* = newRChan[ImgObj](1000)
imageCached*: HashSet[ImageId]
glyphImageIdsByFont*: Table[FontId, HashSet[ImageId]]
imageCachedLock*: Lock

imageCachedLock.initLock()
Expand Down Expand Up @@ -75,6 +76,7 @@ proc readImage*(filePath: string): Flippy =

proc toImgObj*(image: Flippy): ImgObj =
result = ImgObj(kind: FlippyImg, flippy: image)

proc toImgObj*(image: Image): ImgObj =
result = ImgObj(kind: PixieImg, pimg: image)

Expand Down Expand Up @@ -106,3 +108,33 @@ proc loadImage*(filePath: string): ImageId =
proc loadImage*(id: ImageId, image: Image) =
var imgObj = ImgObj(id: id, kind: PixieImg, pimg: image)
sendImage(imgObj)

proc trackGlyphImage*(fontId: FontId, imageId: ImageId) =
withLock imageCachedLock:
if fontId notin glyphImageIdsByFont:
glyphImageIdsByFont[fontId] = initHashSet[ImageId]()
glyphImageIdsByFont[fontId].incl(imageId)

proc clearGlyphImagesForFonts*(fontIds: openArray[FontId]): seq[ImageId] =
var deduped = initHashSet[ImageId]()
withLock imageCachedLock:
for fontId in fontIds:
if fontId notin glyphImageIdsByFont:
continue
for imageId in glyphImageIdsByFont[fontId]:
imageCached.excl(imageId)
if imageId notin deduped:
deduped.incl(imageId)
result.add(imageId)
glyphImageIdsByFont.del(fontId)

proc clearGlyphImagesForAllFonts*(): seq[ImageId] =
var deduped = initHashSet[ImageId]()
withLock imageCachedLock:
for _, imageIds in glyphImageIdsByFont.pairs():
for imageId in imageIds:
imageCached.excl(imageId)
if imageId notin deduped:
deduped.incl(imageId)
result.add(imageId)
glyphImageIdsByFont.clear()
5 changes: 4 additions & 1 deletion src/figdraw/common/typefaces.nim
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,12 @@ proc loadTypeface*(name, data: string, kind: TypeFaceKinds): FontId =
typefaceTable[id] = typeface
result = id

proc fontCacheId*(font: FigFont): FontId {.inline.} =
FontId(hash((font.getId(), figUiScale())))

proc pixieFont(font: FigFont): (FontId, Font) =
let
id = FontId(hash((font.getId(), figUiScale())))
id = font.fontCacheId()
typeface = typefaceTable[font.typefaceId]

var pxfont = newFont(typeface)
Expand Down
26 changes: 26 additions & 0 deletions src/figdraw/figrender.nim
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,32 @@ type FigRenderer*[BackendState = NoRendererBackendState] = ref object
template entries*(ctx: BackendContext): untyped =
ctx.entriesPtr()[]

proc dropCachedImageEntries(ctx: BackendContext, imageIds: openArray[ImageId]): int =
let entryTable = ctx.entriesPtr()
if entryTable.isNil:
return 0
for imageId in imageIds:
let key = imageId.Hash
if key in entryTable[]:
entryTable[].del(key)
inc result

proc clearFontCache*[BackendState](
renderer: FigRenderer[BackendState], font: FigFont
): int =
let imageIds = clearFontCache(font)
result = renderer.ctx.dropCachedImageEntries(imageIds)

proc clearTypefaceCache*[BackendState](
renderer: FigRenderer[BackendState], typefaceId: TypefaceId
): int =
let imageIds = clearTypefaceCache(typefaceId)
result = renderer.ctx.dropCachedImageEntries(imageIds)

proc clearAllFontCaches*[BackendState](renderer: FigRenderer[BackendState]): int =
let imageIds = clearAllFontCaches()
result = renderer.ctx.dropCachedImageEntries(imageIds)

proc backendKind*[BackendState](
renderer: FigRenderer[BackendState]
): RendererBackendKind =
Expand Down
108 changes: 105 additions & 3 deletions tests/tfontutils.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import std/[os, unittest, tables, locks, unicode]
import std/[os, unittest, tables, unicode]

import pkg/pixie
import pkg/pixie/fonts
Expand All @@ -10,12 +10,11 @@ import figdraw/common/fontglyphs
import figdraw/extras/systemfonts

proc resetFontState() =
discard clearGlyphImagesForAllFonts()
typefaceTable = initTable[TypefaceId, Typeface]()
fontTable = initTable[FontId, FigFont]()
staticTypefaceTable =
initTable[string, tuple[name: string, data: string, kind: TypeFaceKinds]]()
#withLock imageCachedLock:
# imageCached.clear()

suite "fontutils":
setup:
Expand Down Expand Up @@ -123,6 +122,109 @@ suite "fontutils":
break
check checked

test "changing font size creates new glyph ids and clearFontCache evicts old size":
let fontData = readFile(figDataDir() / "Ubuntu.ttf")
let typefaceId = loadTypeface("Ubuntu.ttf", fontData, TTF)
let font18 = FigFont(typefaceId: typefaceId, size: 18.0'f32)
let font24 = FigFont(typefaceId: typefaceId, size: 24.0'f32)
let box = rect(0, 0, 240, 60)

let arr18 = typeset(
box,
[(fs(font18), "A")],
hAlign = Left,
vAlign = Top,
minContent = false,
wrap = false,
)
let arr24 = typeset(
box,
[(fs(font24), "A")],
hAlign = Left,
vAlign = Top,
minContent = false,
wrap = false,
)

var glyph18 = Hash(0)
for glyph in arr18.glyphs():
if glyph.rune.isWhiteSpace:
continue
glyph18 = glyph.hash()
break

var glyph24 = Hash(0)
for glyph in arr24.glyphs():
if glyph.rune.isWhiteSpace:
continue
glyph24 = glyph.hash()
break

check glyph18 != 0
check glyph24 != 0
check glyph18 != glyph24
check hasImage(glyph18.ImageId)
check hasImage(glyph24.ImageId)

let removed = clearFontCache(font18)
check removed.len > 0
check font18.fontCacheId() notin fontTable
check font24.fontCacheId() in fontTable
check not hasImage(glyph18.ImageId)
check hasImage(glyph24.ImageId)

test "clearTypefaceCache evicts all cached font sizes for the typeface":
let fontData = readFile(figDataDir() / "Ubuntu.ttf")
let typefaceId = loadTypeface("Ubuntu.ttf", fontData, TTF)
let font18 = FigFont(typefaceId: typefaceId, size: 18.0'f32)
let font24 = FigFont(typefaceId: typefaceId, size: 24.0'f32)
let box = rect(0, 0, 240, 60)

let arr18 = typeset(
box,
[(fs(font18), "A")],
hAlign = Left,
vAlign = Top,
minContent = false,
wrap = false,
)
let arr24 = typeset(
box,
[(fs(font24), "B")],
hAlign = Left,
vAlign = Top,
minContent = false,
wrap = false,
)

var glyph18 = Hash(0)
for glyph in arr18.glyphs():
if glyph.rune.isWhiteSpace:
continue
glyph18 = glyph.hash()
break

var glyph24 = Hash(0)
for glyph in arr24.glyphs():
if glyph.rune.isWhiteSpace:
continue
glyph24 = glyph.hash()
break

check hasImage(glyph18.ImageId)
check hasImage(glyph24.ImageId)
check typefaceId in typefaceTable
check font18.fontCacheId() in fontTable
check font24.fontCacheId() in fontTable

let removed = clearTypefaceCache(typefaceId)
check removed.len >= 2
check typefaceId notin typefaceTable
check font18.fontCacheId() notin fontTable
check font24.fontCacheId() notin fontTable
check not hasImage(glyph18.ImageId)
check not hasImage(glyph24.ImageId)

test "glyph-variant subpixel step maps fractional x to 10 steps":
check toGlyphVariantSubpixelStep(0.0'f32) == 0
check toGlyphVariantSubpixelStep(0.09'f32) == 0
Expand Down
Loading