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/6] 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/6] 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 From ec06c68fa3a8803e38de9cc28b9dc70299bdf007 Mon Sep 17 00:00:00 2001 From: Zyko <13394516+Zyko0@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:25:54 +0200 Subject: [PATCH 3/6] Re-use internal/packing logic and remove external dep --- go.mod | 1 - go.sum | 11 +- text/v2/atlas.go | 278 +++++++++++++++++++++++++++++++++--- text/v2/glyph.go | 5 +- text/v2/gotext.go | 5 +- text/v2/gotextfacesource.go | 3 +- text/v2/gotextseg.go | 3 +- text/v2/gox.go | 7 +- text/v2/layout.go | 7 +- text/v2/text.go | 3 +- 10 files changed, 276 insertions(+), 47 deletions(-) diff --git a/go.mod b/go.mod index b8914bd5d..07dbb7fd5 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ 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 44de873b2..a111c4bd6 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,3 @@ -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= @@ -21,6 +11,7 @@ github.com/gen2brain/mpeg v0.3.2-0.20240412154320-a2ac4fc8a46f/go.mod h1:i/ebyRR github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo= github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY= +github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hajimehoshi/bitmapfont/v3 v3.2.0-alpha.3 h1:gSxacUXbVZcffXOHl/BWGvf61LpJYLoEaZlsiLlWXPk= diff --git a/text/v2/atlas.go b/text/v2/atlas.go index 733c99d4a..0c2e70565 100644 --- a/text/v2/atlas.go +++ b/text/v2/atlas.go @@ -1,36 +1,282 @@ package text import ( - "github.com/Zyko0/Ebiary/atlas" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/internal/packing" ) type glyphAtlas struct { - atlas *atlas.Atlas + page *packing.Page + image *ebiten.Image +} + +type glyphImage struct { + atlas *glyphAtlas + node *packing.Node +} + +func (i *glyphImage) Image() *ebiten.Image { + return i.atlas.image.SubImage(i.node.Region()).(*ebiten.Image) } 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), + page: packing.NewPage(128, 128, 1024), // TODO: not 1024 + image: ebiten.NewImage(128, 128), } } -func (g *glyphAtlas) NewImage(w, h int) *atlas.Image { - if img := g.atlas.NewImage(w, h); img != nil { - return img +func (g *glyphAtlas) NewImage(w, h int) *glyphImage { + n := g.page.Alloc(w, h) + pw, ph := g.page.Size() + if pw > g.image.Bounds().Dx() || ph > g.image.Bounds().Dy() { + newImage := ebiten.NewImage(pw, ph) + newImage.DrawImage(g.image, nil) + g.image = newImage } - // 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) + return &glyphImage{ + atlas: g, + node: n, + } } -func (g *glyphAtlas) Free(img *atlas.Image) { - g.atlas.Free(img) +func (g *glyphAtlas) Free(img *glyphImage) { + g.page.Free(img.node) } + + +type drawRange struct { + atlas *glyphAtlas + end int +} + +// drawList stores triangle versions of DrawImage calls when +// all images are sub-images of an atlas. +// Temporary vertices and indices can be re-used after calling +// Flush, so it is more efficient to keep a reference to a drawList +// instead of creating a new one every frame. +type drawList struct { + ranges []drawRange + vx []ebiten.Vertex + ix []uint16 +} + +// drawCommand is the equivalent of the regular DrawImageOptions +// but only including options that will not break batching. +// Filter, Address, Blend and AntiAlias are determined at Flush() +type drawCommand struct { + Image *glyphImage + + ColorScale ebiten.ColorScale + GeoM ebiten.GeoM +} + +var rectIndices = [6]uint16{0, 1, 2, 1, 2, 3} + +type point struct { + X, Y float32 +} + +func pt(x, y float64) point { + return point{ + X: float32(x), + Y: float32(y), + } +} + +type rectOpts struct { + Dsts [4]point + SrcX0, SrcY0 float32 + SrcX1, SrcY1 float32 + R, G, B, A float32 +} + +// adjustDestinationPixel is the original ebitengine implementation found here: +// https://github.com/hajimehoshi/ebiten/blob/v2.8.0-alpha.1/internal/graphics/vertex.go#L102-L126 +func adjustDestinationPixel(x float32) float32 { + // Avoid the center of the pixel, which is problematic (#929, #1171). + // Instead, align the vertices with about 1/3 pixels. + // + // The intention here is roughly this code: + // + // float32(math.Floor((float64(x)+1.0/6.0)*3) / 3) + // + // The actual implementation is more optimized than the above implementation. + ix := float32(int(x)) + if x < 0 && x != ix { + ix -= 1 + } + frac := x - ix + switch { + case frac < 3.0/16.0: + return ix + case frac < 8.0/16.0: + return ix + 5.0/16.0 + case frac < 13.0/16.0: + return ix + 11.0/16.0 + default: + return ix + 16.0/16.0 + } +} + +func appendRectVerticesIndices(vertices []ebiten.Vertex, indices []uint16, index int, opts *rectOpts) ([]ebiten.Vertex, []uint16) { + sx0, sy0, sx1, sy1 := opts.SrcX0, opts.SrcY0, opts.SrcX1, opts.SrcY1 + r, g, b, a := opts.R, opts.G, opts.B, opts.A + vertices = append(vertices, + ebiten.Vertex{ + DstX: adjustDestinationPixel(opts.Dsts[0].X), + DstY: adjustDestinationPixel(opts.Dsts[0].Y), + SrcX: sx0, + SrcY: sy0, + ColorR: r, + ColorG: g, + ColorB: b, + ColorA: a, + }, + ebiten.Vertex{ + DstX: adjustDestinationPixel(opts.Dsts[1].X), + DstY: adjustDestinationPixel(opts.Dsts[1].Y), + SrcX: sx1, + SrcY: sy0, + ColorR: r, + ColorG: g, + ColorB: b, + ColorA: a, + }, + ebiten.Vertex{ + DstX: adjustDestinationPixel(opts.Dsts[2].X), + DstY: adjustDestinationPixel(opts.Dsts[2].Y), + SrcX: sx0, + SrcY: sy1, + ColorR: r, + ColorG: g, + ColorB: b, + ColorA: a, + }, + ebiten.Vertex{ + DstX: adjustDestinationPixel(opts.Dsts[3].X), + DstY: adjustDestinationPixel(opts.Dsts[3].Y), + SrcX: sx1, + SrcY: sy1, + ColorR: r, + ColorG: g, + ColorB: b, + ColorA: a, + }, + ) + + indiceCursor := uint16(index * 4) + indices = append(indices, + rectIndices[0]+indiceCursor, + rectIndices[1]+indiceCursor, + rectIndices[2]+indiceCursor, + rectIndices[3]+indiceCursor, + rectIndices[4]+indiceCursor, + rectIndices[5]+indiceCursor, + ) + + return vertices, indices +} + +// Add adds DrawImage commands to the DrawList, images from multiple +// atlases can be added but they will break the previous batch bound to +// a different atlas, requiring an additional draw call internally. +// So, it is better to have the maximum of consecutive DrawCommand images +// sharing the same atlas. +func (dl *drawList) Add(commands ...*drawCommand) { + if len(commands) == 0 { + return + } + + var batch *drawRange + + if len(dl.ranges) > 0 { + batch = &dl.ranges[len(dl.ranges)-1] + } else { + dl.ranges = append(dl.ranges, drawRange{ + atlas: commands[0].Image.atlas, + }) + batch = &dl.ranges[0] + } + // Add vertices and indices + opts := &rectOpts{} + for _, cmd := range commands { + if cmd.Image.atlas != batch.atlas { + dl.ranges = append(dl.ranges, drawRange{ + atlas: cmd.Image.atlas, + }) + batch = &dl.ranges[len(dl.ranges)-1] + } + + // Dst attributes + bounds := cmd.Image.node.Region() + opts.Dsts[0] = pt(cmd.GeoM.Apply(0, 0)) + opts.Dsts[1] = pt(cmd.GeoM.Apply( + float64(bounds.Dx()), 0, + )) + opts.Dsts[2] = pt(cmd.GeoM.Apply( + 0, float64(bounds.Dy()), + )) + opts.Dsts[3] = pt(cmd.GeoM.Apply( + float64(bounds.Dx()), float64(bounds.Dy()), + )) + + // Color and source attributes + opts.R = cmd.ColorScale.R() + opts.G = cmd.ColorScale.G() + opts.B = cmd.ColorScale.B() + opts.A = cmd.ColorScale.A() + opts.SrcX0 = float32(bounds.Min.X) + opts.SrcY0 = float32(bounds.Min.Y) + opts.SrcX1 = float32(bounds.Max.X) + opts.SrcY1 = float32(bounds.Max.Y) + + dl.vx, dl.ix = appendRectVerticesIndices( + dl.vx, dl.ix, batch.end, opts, + ) + + batch.end++ + } +} + +// DrawOptions are additional options that will be applied to +// all draw commands from the draw list when calling Flush(). +type drawOptions struct { + ColorScaleMode ebiten.ColorScaleMode + Blend ebiten.Blend + Filter ebiten.Filter + Address ebiten.Address + AntiAlias bool +} + +// Flush executes all the draw commands as the smallest possible +// amount of draw calls, and then clears the list for next uses. +func (dl *drawList) Flush(dst *ebiten.Image, opts *drawOptions) { + var topts *ebiten.DrawTrianglesOptions + if opts != nil { + topts = &ebiten.DrawTrianglesOptions{ + ColorScaleMode: opts.ColorScaleMode, + Blend: opts.Blend, + Filter: opts.Filter, + Address: opts.Address, + AntiAlias: opts.AntiAlias, + } + } + index := 0 + for _, r := range dl.ranges { + dst.DrawTriangles( + dl.vx[index*4:(index+r.end)*4], + dl.ix[index*6:(index+r.end)*6], + r.atlas.image, + topts, + ) + index += r.end + } + // Clear buffers + dl.ranges = dl.ranges[:0] + dl.vx = dl.vx[:0] + dl.ix = dl.ix[:0] +} \ No newline at end of file diff --git a/text/v2/glyph.go b/text/v2/glyph.go index 9b08c345b..f32b0ce99 100644 --- a/text/v2/glyph.go +++ b/text/v2/glyph.go @@ -18,7 +18,6 @@ import ( "math" "sync" - "github.com/Zyko0/Ebiary/atlas" "github.com/hajimehoshi/ebiten/v2/internal/hook" ) @@ -38,7 +37,7 @@ func init() { } type glyphImageCacheEntry struct { - image *atlas.Image + image *glyphImage atime int64 } @@ -49,7 +48,7 @@ type glyphImageCache[Key comparable] struct { m sync.Mutex } -func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func(a *glyphAtlas) *atlas.Image) *atlas.Image { +func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func(a *glyphAtlas) *glyphImage) *glyphImage { g.m.Lock() defer g.m.Unlock() diff --git a/text/v2/gotext.go b/text/v2/gotext.go index 9fb81869a..8dd971f93 100644 --- a/text/v2/gotext.go +++ b/text/v2/gotext.go @@ -19,7 +19,6 @@ 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" @@ -333,7 +332,7 @@ func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffse return glyphs } -func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*atlas.Image, int, int) { +func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*glyphImage, int, int) { if g.direction().isHorizontal() { origin.X = adjustGranularity(origin.X, g) origin.Y &^= ((1 << 6) - 1) @@ -353,7 +352,7 @@ func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*atlas.Ima yoffset: subpixelOffset.Y, variations: g.ensureVariationsString(), } - img := g.Source.getOrCreateGlyphImage(g, key, func(a *glyphAtlas) *atlas.Image { + img := g.Source.getOrCreateGlyphImage(g, key, func(a *glyphAtlas) *glyphImage { return segmentsToImage(a, glyph.scaledSegments, subpixelOffset, b) }) diff --git a/text/v2/gotextfacesource.go b/text/v2/gotextfacesource.go index 9c15789f0..002e34e26 100644 --- a/text/v2/gotextfacesource.go +++ b/text/v2/gotextfacesource.go @@ -19,7 +19,6 @@ 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" @@ -281,7 +280,7 @@ func (g *GoTextFaceSource) scale(size float64) float64 { return size / float64(g.f.Upem()) } -func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goTextGlyphImageCacheKey, create func(a *glyphAtlas) *atlas.Image) *atlas.Image { +func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goTextGlyphImageCacheKey, create func(a *glyphAtlas) *glyphImage) *glyphImage { if g.glyphImageCache == nil { g.glyphImageCache = map[float64]*glyphImageCache[goTextGlyphImageCacheKey]{} } diff --git a/text/v2/gotextseg.go b/text/v2/gotextseg.go index 7579e9bc0..62605242c 100644 --- a/text/v2/gotextseg.go +++ b/text/v2/gotextseg.go @@ -19,7 +19,6 @@ import ( "image/draw" "math" - "github.com/Zyko0/Ebiary/atlas" gvector "golang.org/x/image/vector" "github.com/go-text/typesetting/opentype/api" @@ -76,7 +75,7 @@ func segmentsToBounds(segs []api.Segment) fixed.Rectangle26_6 { } } -func segmentsToImage(a *glyphAtlas, segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *atlas.Image { +func segmentsToImage(a *glyphAtlas, segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *glyphImage { if len(segs) == 0 { return nil } diff --git a/text/v2/gox.go b/text/v2/gox.go index 19d7160a6..f8cefa228 100644 --- a/text/v2/gox.go +++ b/text/v2/gox.go @@ -21,7 +21,6 @@ import ( "golang.org/x/image/font" "golang.org/x/image/math/fixed" - "github.com/Zyko0/Ebiary/atlas" "github.com/hajimehoshi/ebiten/v2/vector" ) @@ -133,7 +132,7 @@ func (s *GoXFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset i return glyphs } -func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*atlas.Image, int, int, fixed.Int26_6) { +func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*glyphImage, int, int, fixed.Int26_6) { // Assume that GoXFace's direction is always horizontal. origin.X = adjustGranularity(origin.X, s) origin.Y &^= ((1 << 6) - 1) @@ -147,7 +146,7 @@ func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*atlas.Image, int, rune: r, xoffset: subpixelOffset.X, } - img := s.glyphImageCache.getOrCreate(s, key, func(a *glyphAtlas) *atlas.Image { + img := s.glyphImageCache.getOrCreate(s, key, func(a *glyphAtlas) *glyphImage { return s.glyphImageImpl(a, r, subpixelOffset, b) }) imgX := (origin.X + b.Min.X).Floor() @@ -155,7 +154,7 @@ func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*atlas.Image, int, return img, imgX, imgY, a } -func (s *GoXFace) glyphImageImpl(a *glyphAtlas, r rune, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *atlas.Image { +func (s *GoXFace) glyphImageImpl(a *glyphAtlas, r rune, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *glyphImage { w, h := (glyphBounds.Max.X - glyphBounds.Min.X).Ceil(), (glyphBounds.Max.Y - glyphBounds.Min.Y).Ceil() if w == 0 || h == 0 { return nil diff --git a/text/v2/layout.go b/text/v2/layout.go index 0dcad471a..6f0f8506b 100644 --- a/text/v2/layout.go +++ b/text/v2/layout.go @@ -17,7 +17,6 @@ package text import ( "strings" - "github.com/Zyko0/Ebiary/atlas" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) @@ -112,8 +111,8 @@ func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) { geoM := drawOp.GeoM - dl := &atlas.DrawList{} - dc := &atlas.DrawCommand{} + dl := &drawList{} + dc := &drawCommand{} for _, g := range AppendGlyphs(nil, text, face, &layoutOp) { if g.Image == nil { continue @@ -125,7 +124,7 @@ func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) { dc.Image = g.img dl.Add(dc) } - dl.Flush(dst, &atlas.DrawOptions{ + dl.Flush(dst, &drawOptions{ Blend: drawOp.Blend, Filter: drawOp.Filter, }) diff --git a/text/v2/text.go b/text/v2/text.go index 5c17cae69..e47d89d0f 100644 --- a/text/v2/text.go +++ b/text/v2/text.go @@ -22,7 +22,6 @@ import ( "golang.org/x/image/math/fixed" - "github.com/Zyko0/Ebiary/atlas" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) @@ -119,7 +118,7 @@ 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 + img *glyphImage // StartIndexInBytes is the start index in bytes for the given string at AppendGlyphs. StartIndexInBytes int From 2eebe55b90db41621f5a62172a7b047e5baa1755 Mon Sep 17 00:00:00 2001 From: Zyko <13394516+Zyko0@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:27:36 +0200 Subject: [PATCH 4/6] Restore go1.19 --- go.mod | 2 +- go.sum | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 07dbb7fd5..ac8c82711 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/hajimehoshi/ebiten/v2 -go 1.21.1 +go 1.19 require ( github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 diff --git a/go.sum b/go.sum index a111c4bd6..7131682fc 100644 --- a/go.sum +++ b/go.sum @@ -11,7 +11,6 @@ github.com/gen2brain/mpeg v0.3.2-0.20240412154320-a2ac4fc8a46f/go.mod h1:i/ebyRR github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo= github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY= -github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hajimehoshi/bitmapfont/v3 v3.2.0-alpha.3 h1:gSxacUXbVZcffXOHl/BWGvf61LpJYLoEaZlsiLlWXPk= From b20692f523311e419587edf764af3c63d6625fc0 Mon Sep 17 00:00:00 2001 From: Zyko <13394516+Zyko0@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:33:53 +0200 Subject: [PATCH 5/6] Fixed colorscale mode --- text/v2/layout.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/text/v2/layout.go b/text/v2/layout.go index 6f0f8506b..c08dec2a4 100644 --- a/text/v2/layout.go +++ b/text/v2/layout.go @@ -125,8 +125,9 @@ func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) { dl.Add(dc) } dl.Flush(dst, &drawOptions{ - Blend: drawOp.Blend, - Filter: drawOp.Filter, + Blend: drawOp.Blend, + Filter: drawOp.Filter, + ColorScaleMode: ebiten.ColorScaleModePremultipliedAlpha, }) } From 30157b5dea795abeb0429c517f7a40269c44f37d Mon Sep 17 00:00:00 2001 From: Zyko <13394516+Zyko0@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:41:04 +0200 Subject: [PATCH 6/6] Add license header --- text/v2/atlas.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/text/v2/atlas.go b/text/v2/atlas.go index 0c2e70565..3543592b9 100644 --- a/text/v2/atlas.go +++ b/text/v2/atlas.go @@ -1,3 +1,17 @@ +// Copyright 2024 The Ebitengine Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package text import ( @@ -47,7 +61,6 @@ func (g *glyphAtlas) Free(img *glyphImage) { g.page.Free(img.node) } - type drawRange struct { atlas *glyphAtlas end int @@ -279,4 +292,4 @@ func (dl *drawList) Flush(dst *ebiten.Image, opts *drawOptions) { dl.ranges = dl.ranges[:0] dl.vx = dl.vx[:0] dl.ix = dl.ix[:0] -} \ No newline at end of file +}