From 5e8d969034591df390339c99b17d7ce9c27a72f3 Mon Sep 17 00:00:00 2001 From: Zyko <13394516+Zyko0@users.noreply.github.com> Date: Sat, 27 Jul 2024 17:41:53 +0200 Subject: [PATCH 1/2] PoC text/v2 glyph atlas --- go.mod | 3 ++- go.sum | 10 ++++++++++ text/v2/atlas.go | 32 ++++++++++++++++++++++++++++++++ text/v2/glyph.go | 11 +++++++---- text/v2/gotext.go | 14 ++++++++++---- text/v2/gotextfacesource.go | 5 ++--- text/v2/gotextseg.go | 19 ++++++++++++++++--- text/v2/gox.go | 18 +++++++++++------- text/v2/layout.go | 17 +++++++++++++---- text/v2/text.go | 6 ++++++ 10 files changed, 109 insertions(+), 26 deletions(-) create mode 100644 text/v2/atlas.go diff --git a/go.mod b/go.mod index ac8c82711..b8914bd5d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/hajimehoshi/ebiten/v2 -go 1.19 +go 1.21.1 require ( github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 @@ -23,6 +23,7 @@ require ( ) require ( + github.com/Zyko0/Ebiary/atlas v0.0.0-20240727152911-c0be754219b9 // indirect github.com/jfreymuth/vorbis v1.0.2 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect golang.org/x/mod v0.19.0 // indirect diff --git a/go.sum b/go.sum index 7131682fc..44de873b2 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,13 @@ +github.com/Zyko0/Ebiary/atlas v0.0.0-20240715185308-15c9f3ed18e4 h1:hw0xjh6636KyizJh10Akyym5q+gJT/JZaG5YoOECpCo= +github.com/Zyko0/Ebiary/atlas v0.0.0-20240715185308-15c9f3ed18e4/go.mod h1:TqaiWLulZjjwPydGAqz6EqHELgtnn/swD/0PlPovCp8= +github.com/Zyko0/Ebiary/atlas v0.0.0-20240727132256-058f84c22395 h1:4kBG4iBHZD8ZnwzYarhCYhBP3bTwTRXHYqjo/TxAvUI= +github.com/Zyko0/Ebiary/atlas v0.0.0-20240727132256-058f84c22395/go.mod h1:3Uar+fYP2hzgYiXkoReBociscUtpaBMXvjAEjph13pk= +github.com/Zyko0/Ebiary/atlas v0.0.0-20240727143531-e678f4f326f6 h1:sRjhOIw0+bqFvKOw+cUmIw0LFkhbo95KlKeIDupVp6c= +github.com/Zyko0/Ebiary/atlas v0.0.0-20240727143531-e678f4f326f6/go.mod h1:3Uar+fYP2hzgYiXkoReBociscUtpaBMXvjAEjph13pk= +github.com/Zyko0/Ebiary/atlas v0.0.0-20240727145901-a622e72da2b1 h1:Tv7NzEiyRLfo2PJNo+IQRf0VdCskblash/RGnSZsQJQ= +github.com/Zyko0/Ebiary/atlas v0.0.0-20240727145901-a622e72da2b1/go.mod h1:3Uar+fYP2hzgYiXkoReBociscUtpaBMXvjAEjph13pk= +github.com/Zyko0/Ebiary/atlas v0.0.0-20240727152911-c0be754219b9 h1:qfCLi8fCRFO3zVs9c60Ey2xJa+8VfVRKJlgbQVjYfpk= +github.com/Zyko0/Ebiary/atlas v0.0.0-20240727152911-c0be754219b9/go.mod h1:3Uar+fYP2hzgYiXkoReBociscUtpaBMXvjAEjph13pk= github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 h1:48bCqKTuD7Z0UovDfvpCn7wZ0GUZ+yosIteNDthn3FU= github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895/go.mod h1:XZdLv05c5hOZm3fM2NlJ92FyEZjnslcMcNRrhxs8+8M= github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= diff --git a/text/v2/atlas.go b/text/v2/atlas.go new file mode 100644 index 000000000..ad7c47fac --- /dev/null +++ b/text/v2/atlas.go @@ -0,0 +1,32 @@ +package text + +import ( + "github.com/Zyko0/Ebiary/atlas" +) + +type glyphAtlas struct { + atlas *atlas.Atlas +} + +func newGlyphAtlas() *glyphAtlas { + return &glyphAtlas{ + // Note: 128x128 is arbitrary, maybe a better value can be inferred + // from the font size or something + atlas: atlas.New(128, 128, nil), + } +} + +func (g *glyphAtlas) NewImage(w, h int) *atlas.Image { + if img := g.atlas.NewImage(w, h); img != nil { + return img + } + + // Grow atlas + old := g.atlas.Image() + + aw, ah := g.atlas.Bounds().Dx()*2, g.atlas.Bounds().Dy()*2 + g.atlas = atlas.New(aw, ah, nil) + g.atlas.Image().DrawImage(old, nil) + + return g.NewImage(w, h) +} diff --git a/text/v2/glyph.go b/text/v2/glyph.go index cdf03df26..9af846af6 100644 --- a/text/v2/glyph.go +++ b/text/v2/glyph.go @@ -18,7 +18,7 @@ import ( "math" "sync" - "github.com/hajimehoshi/ebiten/v2" + "github.com/Zyko0/Ebiary/atlas" "github.com/hajimehoshi/ebiten/v2/internal/hook" ) @@ -38,17 +38,18 @@ func init() { } type glyphImageCacheEntry struct { - image *ebiten.Image + image *atlas.Image atime int64 } type glyphImageCache[Key comparable] struct { + atlas *glyphAtlas cache map[Key]*glyphImageCacheEntry atime int64 m sync.Mutex } -func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func() *ebiten.Image) *ebiten.Image { +func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func(a *glyphAtlas) *atlas.Image) *atlas.Image { g.m.Lock() defer g.m.Unlock() @@ -61,10 +62,11 @@ func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func() *eb } if g.cache == nil { + g.atlas = newGlyphAtlas() g.cache = map[Key]*glyphImageCacheEntry{} } - img := create() + img := create(g.atlas) e = &glyphImageCacheEntry{ image: img, } @@ -91,6 +93,7 @@ func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func() *eb continue } delete(g.cache, key) + g.atlas.atlas.Free(e.image) } } } diff --git a/text/v2/gotext.go b/text/v2/gotext.go index 7b42576dd..9fb81869a 100644 --- a/text/v2/gotext.go +++ b/text/v2/gotext.go @@ -19,6 +19,7 @@ import ( "encoding/binary" "fmt" + "github.com/Zyko0/Ebiary/atlas" "github.com/go-text/typesetting/di" glanguage "github.com/go-text/typesetting/language" "github.com/go-text/typesetting/opentype/api/font" @@ -310,11 +311,16 @@ func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffse })) // Append a glyph even if img is nil. // This is necessary to return index information for control characters. + var ebitenImage *ebiten.Image + if img != nil { + ebitenImage = img.Image() + } glyphs = append(glyphs, Glyph{ + img: img, StartIndexInBytes: indexOffset + glyph.startIndex, EndIndexInBytes: indexOffset + glyph.endIndex, GID: uint32(glyph.shapingGlyph.GlyphID), - Image: img, + Image: ebitenImage, X: float64(imgX), Y: float64(imgY), }) @@ -327,7 +333,7 @@ func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffse return glyphs } -func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Image, int, int) { +func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*atlas.Image, int, int) { if g.direction().isHorizontal() { origin.X = adjustGranularity(origin.X, g) origin.Y &^= ((1 << 6) - 1) @@ -347,8 +353,8 @@ func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Im yoffset: subpixelOffset.Y, variations: g.ensureVariationsString(), } - img := g.Source.getOrCreateGlyphImage(g, key, func() *ebiten.Image { - return segmentsToImage(glyph.scaledSegments, subpixelOffset, b) + img := g.Source.getOrCreateGlyphImage(g, key, func(a *glyphAtlas) *atlas.Image { + return segmentsToImage(a, glyph.scaledSegments, subpixelOffset, b) }) imgX := (origin.X + b.Min.X).Floor() diff --git a/text/v2/gotextfacesource.go b/text/v2/gotextfacesource.go index 3e84adde6..9c15789f0 100644 --- a/text/v2/gotextfacesource.go +++ b/text/v2/gotextfacesource.go @@ -19,6 +19,7 @@ import ( "io" "sync" + "github.com/Zyko0/Ebiary/atlas" "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/language" "github.com/go-text/typesetting/opentype/api" @@ -26,8 +27,6 @@ import ( "github.com/go-text/typesetting/opentype/loader" "github.com/go-text/typesetting/shaping" "golang.org/x/image/math/fixed" - - "github.com/hajimehoshi/ebiten/v2" ) type goTextOutputCacheKey struct { @@ -282,7 +281,7 @@ func (g *GoTextFaceSource) scale(size float64) float64 { return size / float64(g.f.Upem()) } -func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goTextGlyphImageCacheKey, create func() *ebiten.Image) *ebiten.Image { +func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goTextGlyphImageCacheKey, create func(a *glyphAtlas) *atlas.Image) *atlas.Image { if g.glyphImageCache == nil { g.glyphImageCache = map[float64]*glyphImageCache[goTextGlyphImageCacheKey]{} } diff --git a/text/v2/gotextseg.go b/text/v2/gotextseg.go index 4875bb4ba..3d7fa9e9d 100644 --- a/text/v2/gotextseg.go +++ b/text/v2/gotextseg.go @@ -16,12 +16,15 @@ package text import ( "image" + "image/color" "image/draw" "math" + "github.com/Zyko0/Ebiary/atlas" + gvector "golang.org/x/image/vector" + "github.com/go-text/typesetting/opentype/api" "golang.org/x/image/math/fixed" - gvector "golang.org/x/image/vector" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" @@ -75,7 +78,14 @@ func segmentsToBounds(segs []api.Segment) fixed.Rectangle26_6 { } } -func segmentsToImage(segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *ebiten.Image { +var whiteImage = ebiten.NewImage(3, 3) + +func init() { + whiteImage.Fill(color.White) + //whiteImage.Set(1, 1, color.White) +} + +func segmentsToImage(a *glyphAtlas, segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *atlas.Image { if len(segs) == 0 { return nil } @@ -122,7 +132,10 @@ func segmentsToImage(segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBo dst := image.NewRGBA(image.Rect(0, 0, w, h)) rast.Draw(dst, dst.Bounds(), image.Opaque, image.Point{}) - return ebiten.NewImageFromImage(dst) + img := a.NewImage(w, h) + img.Image().WritePixels(dst.Pix) + + return img } func appendVectorPathFromSegments(path *vector.Path, segs []api.Segment, x, y float32) { diff --git a/text/v2/gox.go b/text/v2/gox.go index 625215d82..19d7160a6 100644 --- a/text/v2/gox.go +++ b/text/v2/gox.go @@ -21,7 +21,7 @@ import ( "golang.org/x/image/font" "golang.org/x/image/math/fixed" - "github.com/hajimehoshi/ebiten/v2" + "github.com/Zyko0/Ebiary/atlas" "github.com/hajimehoshi/ebiten/v2/vector" ) @@ -119,9 +119,10 @@ func (s *GoXFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset i // Append a glyph even if img is nil. // This is necessary to return index information for control characters. glyphs = append(glyphs, Glyph{ + img: img, StartIndexInBytes: indexOffset + i, EndIndexInBytes: indexOffset + i + size, - Image: img, + Image: img.Image(), X: float64(imgX), Y: float64(imgY), }) @@ -132,7 +133,7 @@ func (s *GoXFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset i return glyphs } -func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int, int, fixed.Int26_6) { +func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*atlas.Image, int, int, fixed.Int26_6) { // Assume that GoXFace's direction is always horizontal. origin.X = adjustGranularity(origin.X, s) origin.Y &^= ((1 << 6) - 1) @@ -146,15 +147,15 @@ func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int rune: r, xoffset: subpixelOffset.X, } - img := s.glyphImageCache.getOrCreate(s, key, func() *ebiten.Image { - return s.glyphImageImpl(r, subpixelOffset, b) + img := s.glyphImageCache.getOrCreate(s, key, func(a *glyphAtlas) *atlas.Image { + return s.glyphImageImpl(a, r, subpixelOffset, b) }) imgX := (origin.X + b.Min.X).Floor() imgY := (origin.Y + b.Min.Y).Floor() return img, imgX, imgY, a } -func (s *GoXFace) glyphImageImpl(r rune, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *ebiten.Image { +func (s *GoXFace) glyphImageImpl(a *glyphAtlas, r rune, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *atlas.Image { w, h := (glyphBounds.Max.X - glyphBounds.Min.X).Ceil(), (glyphBounds.Max.Y - glyphBounds.Min.Y).Ceil() if w == 0 || h == 0 { return nil @@ -178,7 +179,10 @@ func (s *GoXFace) glyphImageImpl(r rune, subpixelOffset fixed.Point26_6, glyphBo } d.DrawString(string(r)) - return ebiten.NewImageFromImage(rgba) + img := a.NewImage(w, h) + img.Image().WritePixels(rgba.Pix) + + return img } // direction implements Face. diff --git a/text/v2/layout.go b/text/v2/layout.go index fe874e01a..0dcad471a 100644 --- a/text/v2/layout.go +++ b/text/v2/layout.go @@ -17,6 +17,7 @@ package text import ( "strings" + "github.com/Zyko0/Ebiary/atlas" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) @@ -111,15 +112,23 @@ func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) { geoM := drawOp.GeoM + dl := &atlas.DrawList{} + dc := &atlas.DrawCommand{} for _, g := range AppendGlyphs(nil, text, face, &layoutOp) { if g.Image == nil { continue } - drawOp.GeoM.Reset() - drawOp.GeoM.Translate(g.X, g.Y) - drawOp.GeoM.Concat(geoM) - dst.DrawImage(g.Image, &drawOp) + dc.GeoM.Reset() + dc.GeoM.Translate(g.X, g.Y) + dc.GeoM.Concat(geoM) + dc.ColorScale = drawOp.ColorScale + dc.Image = g.img + dl.Add(dc) } + dl.Flush(dst, &atlas.DrawOptions{ + Blend: drawOp.Blend, + Filter: drawOp.Filter, + }) } // AppendGlyphs appends glyphs to the given slice and returns a slice. diff --git a/text/v2/text.go b/text/v2/text.go index 32d750b71..5c17cae69 100644 --- a/text/v2/text.go +++ b/text/v2/text.go @@ -22,6 +22,7 @@ import ( "golang.org/x/image/math/fixed" + "github.com/Zyko0/Ebiary/atlas" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) @@ -115,6 +116,11 @@ func adjustGranularity(x fixed.Int26_6, face Face) fixed.Int26_6 { // Glyph represents one glyph to render. type Glyph struct { + // Image is a rasterized glyph image. + // Image is a grayscale image i.e. RGBA values are the same. + // Image should be used as a render source and should not be modified. + img *atlas.Image + // StartIndexInBytes is the start index in bytes for the given string at AppendGlyphs. StartIndexInBytes int From 4601cffabafa7bfadfd0330122fdef87a7dd675c Mon Sep 17 00:00:00 2001 From: Zyko <13394516+Zyko0@users.noreply.github.com> Date: Sat, 27 Jul 2024 18:01:06 +0200 Subject: [PATCH 2/2] Cleanup --- text/v2/atlas.go | 4 ++++ text/v2/glyph.go | 2 +- text/v2/gotextseg.go | 9 --------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/text/v2/atlas.go b/text/v2/atlas.go index ad7c47fac..733c99d4a 100644 --- a/text/v2/atlas.go +++ b/text/v2/atlas.go @@ -30,3 +30,7 @@ func (g *glyphAtlas) NewImage(w, h int) *atlas.Image { return g.NewImage(w, h) } + +func (g *glyphAtlas) Free(img *atlas.Image) { + g.atlas.Free(img) +} diff --git a/text/v2/glyph.go b/text/v2/glyph.go index 9af846af6..9b08c345b 100644 --- a/text/v2/glyph.go +++ b/text/v2/glyph.go @@ -93,7 +93,7 @@ func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func(a *gl continue } delete(g.cache, key) - g.atlas.atlas.Free(e.image) + g.atlas.Free(e.image) } } } diff --git a/text/v2/gotextseg.go b/text/v2/gotextseg.go index 3d7fa9e9d..7579e9bc0 100644 --- a/text/v2/gotextseg.go +++ b/text/v2/gotextseg.go @@ -16,7 +16,6 @@ package text import ( "image" - "image/color" "image/draw" "math" @@ -26,7 +25,6 @@ import ( "github.com/go-text/typesetting/opentype/api" "golang.org/x/image/math/fixed" - "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) @@ -78,13 +76,6 @@ func segmentsToBounds(segs []api.Segment) fixed.Rectangle26_6 { } } -var whiteImage = ebiten.NewImage(3, 3) - -func init() { - whiteImage.Fill(color.White) - //whiteImage.Set(1, 1, color.White) -} - func segmentsToImage(a *glyphAtlas, segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *atlas.Image { if len(segs) == 0 { return nil