Compare commits

...

13 Commits

Author SHA1 Message Date
Bertrand Jung
3bd9149d4b
Merge 1dd96726c4 into a786f23e28 2024-09-09 15:54:30 +03:00
Hajime Hoshi
a786f23e28 cmd/ebitenmobile: add comments 2024-09-09 20:55:03 +09:00
Hajime Hoshi
2a4374e012 cmd/ebitenmobile: remove setStrictContextRestoration from EbitenViewController 2024-09-09 18:05:18 +09:00
Hajime Hoshi
7cc2f8ffcd cmd/ebitenmobile: add comments 2024-09-09 17:41:17 +09:00
Hajime Hoshi
07d29fa729 cmd/ebitenmobile: bug fix: consider EbitenSurfaceView recreation
On Android Emulator (Small Desktop API 32), EbitenRenderer can be
easily recreated by resizing the window. Thus, EbitenRenderer should
not have any flags like strictContextRestoration. Also, the flag
onceSurfaceCreated_ doesn't work there.
2024-09-09 16:42:57 +09:00
Hajime Hoshi
fcef0a7c29 cmd/ebitenmobile: call setPreserveEGLContextOnPause(true) whatever the option is
We found that an app freezes for a little while when resuming it.
In order to improve user experience, always use
setPreserveEGLContextOnPause(true) whichever StrictContextRestoration is
true or false.
2024-09-09 15:20:04 +09:00
Zyko
1dd96726c4 Add a benchmark + fix sub image allocations 2024-08-15 19:48:36 +02:00
Zyko
30157b5dea Add license header 2024-08-05 20:41:04 +02:00
Zyko
b20692f523 Fixed colorscale mode 2024-08-05 20:33:53 +02:00
Zyko
2eebe55b90 Restore go1.19 2024-08-05 20:27:36 +02:00
Zyko
ec06c68fa3 Re-use internal/packing logic and remove external dep 2024-08-05 20:25:54 +02:00
Zyko
4601cffaba Cleanup 2024-07-27 18:01:06 +02:00
Zyko
5e8d969034 PoC text/v2 glyph atlas 2024-07-27 17:41:53 +02:00
14 changed files with 400 additions and 59 deletions

View File

@ -59,12 +59,13 @@ class EbitenSurfaceView extends GLSurfaceView implements Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
if (!onceSurfaceCreated_) {
onceSurfaceCreated_ = true;
// As EbitenSurfaceView can be recreated anytime, this flag for strict context restoration must be checked every time.
if (Ebitenmobileview.usesStrictContextRestoration()) {
Ebitenmobileview.onContextLost();
return;
}
if (hasStrictContextRestoration()) {
Ebitenmobileview.onContextLost();
if (!onceSurfaceCreated_) {
onceSurfaceCreated_ = true;
return;
}
contextLost_ = true;
@ -81,8 +82,6 @@ class EbitenSurfaceView extends GLSurfaceView implements Renderer {
}
}
private boolean strictContextRestoration_ = false;
public EbitenSurfaceView(Context context) {
super(context);
initialize();
@ -96,8 +95,8 @@ class EbitenSurfaceView extends GLSurfaceView implements Renderer {
private void initialize() {
setEGLContextClientVersion(3);
setEGLConfigChooser(8, 8, 8, 8, 0, 0);
// setRenderer must be called before setRenderRequester.
// Or, the application crashes.
setPreserveEGLContextOnPause(true);
// setRenderer must be called before setRenderer. Or, setRenderMode in setExplicitRenderingMode will crash.
setRenderer(new EbitenRenderer());
Ebitenmobileview.setRenderer(this);
@ -115,6 +114,8 @@ class EbitenSurfaceView extends GLSurfaceView implements Renderer {
@Override
public synchronized void setExplicitRenderingMode(boolean explicitRendering) {
// TODO: Remove this logic when FPSModeVsyncOffMinimum is removed.
// This doesn't work when EbitenSurfaceView is recreated anyway.
if (explicitRendering) {
setRenderMode(RENDERMODE_WHEN_DIRTY);
} else {
@ -122,16 +123,6 @@ class EbitenSurfaceView extends GLSurfaceView implements Renderer {
}
}
@Override
public synchronized void setStrictContextRestoration(boolean strictContextRestoration) {
strictContextRestoration_ = strictContextRestoration;
setPreserveEGLContextOnPause(!strictContextRestoration);
}
private synchronized boolean hasStrictContextRestoration() {
return strictContextRestoration_;
}
@Override
public synchronized void requestRenderIfNeeded() {
if (getRenderMode() == RENDERMODE_WHEN_DIRTY) {

View File

@ -364,10 +364,6 @@
}
}
- (void)setStrictContextRestoration:(BOOL)strictContextRestoration {
// Do nothing.
}
- (void)setExplicitRenderingMode:(BOOL)explicitRendering {
@synchronized(self) {
explicitRendering_ = explicitRendering;

View File

@ -101,7 +101,7 @@ type userInterfaceImpl struct {
fpsMode atomic.Int32
renderer Renderer
strictContextRestoration bool
strictContextRestoration atomic.Bool
strictContextRestorationOnce sync.Once
m sync.RWMutex
@ -156,11 +156,11 @@ func (u *UserInterface) runMobile(game Game, options *RunOptions) (err error) {
u.graphicsDriver = g
u.setGraphicsLibrary(lib)
close(u.graphicsLibraryInitCh)
u.strictContextRestoration = options.StrictContextRestoration
if !u.strictContextRestoration {
if options.StrictContextRestoration {
u.strictContextRestoration.Store(true)
} else {
restorable.Disable()
}
u.renderer.SetStrictContextRestoration(u.strictContextRestoration)
for {
if err := u.update(); err != nil {
@ -312,7 +312,6 @@ func (u *UserInterface) UpdateInput(keys map[Key]struct{}, runes []rune, touches
type Renderer interface {
SetExplicitRenderingMode(explicitRendering bool)
SetStrictContextRestoration(strictContextRestoration bool)
RequestRenderIfNeeded()
}
@ -331,6 +330,10 @@ func (u *UserInterface) updateIconIfNeeded() error {
return nil
}
func (u *UserInterface) UsesStrictContextRestoration() bool {
return u.strictContextRestoration.Load()
}
func IsScreenTransparentAvailable() bool {
return false
}

View File

@ -132,3 +132,7 @@ func SetRenderer(renderer Renderer) {
func SetSetGameNotifier(setGameNotifier SetGameNotifier) {
theState.setSetGameNotifier(setGameNotifier)
}
func UsesStrictContextRestoration() bool {
return ui.Get().UsesStrictContextRestoration()
}

13
run.go
View File

@ -302,17 +302,20 @@ type RunGameOptions struct {
//
// StrictContextRestration is available only on Android. Otherwise, StrictContextRestration is ignored.
//
// When StrictContextRestration is false, Ebitengine tries to rely on the OS to restore the context.
// In Android, Ebitengien uses `GLSurfaceView`'s `setPreserveEGLContextOnPause(true)`.
// This works in most cases, but it is still possible that the context is lost in some minor cases.
// With StrictContextRestration false, the activity's launch mode should be singleInstance,
// or the activity no longer works correctly after the context is lost.
//
// When StrictContextRestration is true, Ebitengine tries to restore the context more strictly.
// This is useful when you want to restore the context in any case.
// When StrictContextRestration is true, Ebitengine tries to restore the context more strictly
// for such minor cases.
// However, this might cause a performance issue since Ebitengine tries to keep all the information
// to restore the context.
//
// When StrictContextRestration is false, Ebitengine does nothing special to restore the context and
// relies on the OS's behavior.
//
// As a side note, especially when StrictContextRestration is false, the activity's launch mode should
// be singleInstance, or the activity no longer works correctly after the context is lost.
//
// The default (zero) value is false.
StrictContextRestration bool
}

297
text/v2/atlas.go Normal file
View File

@ -0,0 +1,297 @@
// 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 (
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/internal/packing"
)
type glyphAtlas struct {
page *packing.Page
image *ebiten.Image
}
type glyphImage struct {
atlas *glyphAtlas
node *packing.Node
img *ebiten.Image
}
func (i *glyphImage) Image() *ebiten.Image {
return i.img
}
func newGlyphAtlas() *glyphAtlas {
return &glyphAtlas{
// Note: 128x128 is arbitrary, maybe a better value can be inferred
// from the font size or something
page: packing.NewPage(128, 128, 1024), // TODO: not 1024
image: ebiten.NewImage(128, 128),
}
}
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
}
return &glyphImage{
atlas: g,
node: n,
img: g.image.SubImage(n.Region()).(*ebiten.Image),
}
}
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]
}

View File

@ -18,7 +18,6 @@ import (
"math"
"sync"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/internal/hook"
)
@ -38,17 +37,18 @@ func init() {
}
type glyphImageCacheEntry struct {
image *ebiten.Image
image *glyphImage
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) *glyphImage) *glyphImage {
g.m.Lock()
defer g.m.Unlock()
@ -61,10 +61,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 +92,7 @@ func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func() *eb
continue
}
delete(g.cache, key)
g.atlas.Free(e.image)
}
}
}

View File

@ -311,11 +311,16 @@ func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffse
img, imgX, imgY := g.glyphImage(glyph, o)
// 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),
OriginX: fixed26_6ToFloat64(origin.X),
@ -332,7 +337,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) (*glyphImage, int, int) {
if g.direction().isHorizontal() {
origin.X = adjustGranularity(origin.X, g)
origin.Y &^= ((1 << 6) - 1)
@ -352,8 +357,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) *glyphImage {
return segmentsToImage(a, glyph.scaledSegments, subpixelOffset, b)
})
imgX := (origin.X + b.Min.X).Floor()

View File

@ -26,8 +26,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 +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() *ebiten.Image) *ebiten.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]{}
}

View File

@ -19,11 +19,11 @@ import (
"image/draw"
"math"
"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/go-text/typesetting/opentype/api"
"golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2/vector"
)
@ -75,7 +75,7 @@ func segmentsToBounds(segs []api.Segment) fixed.Rectangle26_6 {
}
}
func segmentsToImage(segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *ebiten.Image {
func segmentsToImage(a *glyphAtlas, segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *glyphImage {
if len(segs) == 0 {
return nil
}
@ -122,7 +122,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) {

View File

@ -21,7 +21,6 @@ import (
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
)
@ -119,9 +118,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),
OriginX: fixed26_6ToFloat64(origin.X),
@ -136,7 +136,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) (*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)
@ -150,15 +150,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) *glyphImage {
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) *glyphImage {
w, h := (glyphBounds.Max.X - glyphBounds.Min.X).Ceil(), (glyphBounds.Max.Y - glyphBounds.Min.Y).Ceil()
if w == 0 || h == 0 {
return nil
@ -182,7 +182,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.

View File

@ -111,15 +111,24 @@ func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) {
geoM := drawOp.GeoM
dl := &drawList{}
dc := &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, &drawOptions{
Blend: drawOp.Blend,
Filter: drawOp.Filter,
ColorScaleMode: ebiten.ColorScaleModePremultipliedAlpha,
})
}
// AppendGlyphs appends glyphs to the given slice and returns a slice.

View File

@ -115,6 +115,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 *glyphImage
// StartIndexInBytes is the start index in bytes for the given string at AppendGlyphs.
StartIndexInBytes int

View File

@ -15,6 +15,7 @@
package text_test
import (
"bytes"
"image"
"image/color"
"regexp"
@ -23,6 +24,7 @@ import (
"github.com/hajimehoshi/bitmapfont/v3"
"golang.org/x/image/font"
"golang.org/x/image/font/gofont/goregular"
"golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2"
@ -371,3 +373,23 @@ func TestDrawOptionsNotModified(t *testing.T) {
t.Errorf("got: %v, want: %v", got, want)
}
}
func BenchmarkDrawText(b *testing.B) {
var txt string
for i := 0; i < 32; i++ {
txt += "The quick brown fox jumps over the lazy dog.\n"
}
screen := ebiten.NewImage(16, 16)
source, err := text.NewGoTextFaceSource(bytes.NewReader(goregular.TTF))
if err != nil {
b.Fatal(err)
}
f := &text.GoTextFace{
Source: source,
Size: 10,
}
op := &text.DrawOptions{}
for i := 0; i < b.N; i++ {
text.Draw(screen, txt, f, op)
}
}