diff --git a/README.md b/README.md index 7623475..84c2c6f 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/figdraw/common/fontglyphs.nim b/src/figdraw/common/fontglyphs.nim index bb8f829..a57593a 100644 --- a/src/figdraw/common/fontglyphs.nim +++ b/src/figdraw/common/fontglyphs.nim @@ -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 diff --git a/src/figdraw/common/fontutils.nim b/src/figdraw/common/fontutils.nim index a517d42..b511367 100644 --- a/src/figdraw/common/fontutils.nim +++ b/src/figdraw/common/fontutils.nim @@ -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 @@ -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 @@ -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( diff --git a/src/figdraw/common/imgutils.nim b/src/figdraw/common/imgutils.nim index 8941d24..b096dd8 100644 --- a/src/figdraw/common/imgutils.nim +++ b/src/figdraw/common/imgutils.nim @@ -28,6 +28,7 @@ type var imageChan* = newRChan[ImgObj](1000) imageCached*: HashSet[ImageId] + glyphImageIdsByFont*: Table[FontId, HashSet[ImageId]] imageCachedLock*: Lock imageCachedLock.initLock() @@ -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) @@ -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() diff --git a/src/figdraw/common/typefaces.nim b/src/figdraw/common/typefaces.nim index 565976c..3ee96d0 100644 --- a/src/figdraw/common/typefaces.nim +++ b/src/figdraw/common/typefaces.nim @@ -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) diff --git a/src/figdraw/figrender.nim b/src/figdraw/figrender.nim index 2e95aa2..30e6394 100644 --- a/src/figdraw/figrender.nim +++ b/src/figdraw/figrender.nim @@ -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 = diff --git a/tests/tfontutils.nim b/tests/tfontutils.nim index 08eb335..b980aa2 100644 --- a/tests/tfontutils.nim +++ b/tests/tfontutils.nim @@ -1,4 +1,4 @@ -import std/[os, unittest, tables, locks, unicode] +import std/[os, unittest, tables, unicode] import pkg/pixie import pkg/pixie/fonts @@ -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: @@ -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