diff --git a/gpu/gpu.go b/gpu/gpu.go index 5cd17f78..fc6511cb 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -186,10 +186,16 @@ type material struct { uvTrans f32.Affine2D } +const ( + filterLinear = 0 + filterNearest = 1 +) + // imageOpData is the shadow of paint.ImageOp. type imageOpData struct { src *image.RGBA handle interface{} + filter byte } type linearGradientOpData struct { @@ -207,6 +213,7 @@ func decodeImageOp(data []byte, refs []interface{}) imageOpData { return imageOpData{ src: refs[0].(*image.RGBA), handle: handle, + filter: data[1], } } @@ -454,19 +461,41 @@ func (g *gpu) Profile() string { } func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture { + type cachekey struct { + filter byte + handle any + } + key := cachekey{ + filter: data.filter, + handle: data.handle, + } + var tex *texture - t, exists := cache.get(data.handle) + t, exists := cache.get(key) if !exists { t = &texture{ src: data.src, } - cache.put(data.handle, t) + cache.put(key, t) } tex = t.(*texture) if tex.tex != nil { return tex.tex } - handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA, data.src.Bounds().Dx(), data.src.Bounds().Dy(), driver.FilterLinearMipmapLinear, driver.FilterLinear, driver.BufferBindingTexture) + + var minFilter, magFilter driver.TextureFilter + switch data.filter { + case filterLinear: + minFilter, magFilter = driver.FilterLinearMipmapLinear, driver.FilterLinear + case filterNearest: + minFilter, magFilter = driver.FilterNearest, driver.FilterNearest + } + + handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA, + data.src.Bounds().Dx(), data.src.Bounds().Dy(), + minFilter, magFilter, + driver.BufferBindingTexture, + ) if err != nil { panic(err) } diff --git a/gpu/internal/rendertest/refs/TestImageRGBA_ScaleLinear.png b/gpu/internal/rendertest/refs/TestImageRGBA_ScaleLinear.png new file mode 100644 index 00000000..bc0cba29 Binary files /dev/null and b/gpu/internal/rendertest/refs/TestImageRGBA_ScaleLinear.png differ diff --git a/gpu/internal/rendertest/refs/TestImageRGBA_ScaleNearest.png b/gpu/internal/rendertest/refs/TestImageRGBA_ScaleNearest.png new file mode 100644 index 00000000..ae3ce430 Binary files /dev/null and b/gpu/internal/rendertest/refs/TestImageRGBA_ScaleNearest.png differ diff --git a/gpu/internal/rendertest/render_test.go b/gpu/internal/rendertest/render_test.go index 00900204..fc5c7fa8 100644 --- a/gpu/internal/rendertest/render_test.go +++ b/gpu/internal/rendertest/render_test.go @@ -359,6 +359,73 @@ func TestImageRGBA(t *testing.T) { }) } +func TestImageRGBA_ScaleLinear(t *testing.T) { + run(t, func(o *op.Ops) { + w := newWindow(t, 128, 128) + defer clip.Rect{Max: image.Pt(128, 128)}.Push(o).Pop() + op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(64, 64))).Add(o) + + im := image.NewRGBA(image.Rect(0, 0, 2, 2)) + im.Set(0, 0, colornames.Red) + im.Set(1, 0, colornames.Green) + im.Set(0, 1, colornames.White) + im.Set(1, 1, colornames.Black) + + op := paint.NewImageOp(im) + op.Filter = paint.FilterLinear + op.Add(o) + + paint.PaintOp{}.Add(o) + + if err := w.Frame(o); err != nil { + t.Error(err) + } + }, func(r result) { + r.expect(0, 0, colornames.Red) + r.expect(8, 8, colornames.Red) + + // TODO: this currently seems to do srgb scaling + // instead of linear rgb scaling, + r.expect(64-4, 0, color.RGBA{R: 197, G: 87, B: 0, A: 255}) + r.expect(64+4, 0, color.RGBA{R: 175, G: 98, B: 0, A: 255}) + + r.expect(127, 0, colornames.Green) + r.expect(127-8, 8, colornames.Green) + }) +} + +func TestImageRGBA_ScaleNearest(t *testing.T) { + run(t, func(o *op.Ops) { + w := newWindow(t, 128, 128) + op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(64, 64))).Add(o) + + im := image.NewRGBA(image.Rect(0, 0, 2, 2)) + im.Set(0, 0, colornames.Red) + im.Set(1, 0, colornames.Green) + im.Set(0, 1, colornames.White) + im.Set(1, 1, colornames.Black) + + op := paint.NewImageOp(im) + op.Filter = paint.FilterNearest + op.Add(o) + + paint.PaintOp{}.Add(o) + + if err := w.Frame(o); err != nil { + t.Error(err) + } + }, func(r result) { + r.expect(0, 0, colornames.Red) + r.expect(8, 8, colornames.Red) + + r.expect(64-4, 0, colornames.Red) + r.expect(64+4, 0, colornames.Green) + + r.expect(127, 0, colornames.Green) + r.expect(127-8, 8, colornames.Green) + }) +} + func TestGapsInPath(t *testing.T) { ops := new(op.Ops) var p clip.Path diff --git a/internal/ops/ops.go b/internal/ops/ops.go index 0ba83a71..adac510f 100644 --- a/internal/ops/ops.go +++ b/internal/ops/ops.go @@ -142,7 +142,7 @@ const ( TypePushOpacityLen = 1 + 4 TypePopOpacityLen = 1 TypeRedrawLen = 1 + 8 - TypeImageLen = 1 + TypeImageLen = 1 + 1 TypePaintLen = 1 TypeColorLen = 1 + 4 TypeLinearGradientLen = 1 + 8*2 + 4*2 diff --git a/op/paint/paint.go b/op/paint/paint.go index 59583ed7..1ccb3d93 100644 --- a/op/paint/paint.go +++ b/op/paint/paint.go @@ -15,8 +15,20 @@ import ( "gioui.org/op/clip" ) +// ImageFilter is the scaling filter for images. +type ImageFilter byte + +const ( + // FilterLinear uses linear interpolation for scaling. + FilterLinear ImageFilter = iota + // FilterNearest uses nearest neighbor interpolation for scaling. + FilterNearest +) + // ImageOp sets the brush to an image. type ImageOp struct { + Filter ImageFilter + uniform bool color color.NRGBA src *image.RGBA @@ -103,6 +115,7 @@ func (i ImageOp) Add(o *op.Ops) { } data := ops.Write2(&o.Internal, ops.TypeImageLen, i.src, i.handle) data[0] = byte(ops.TypeImage) + data[1] = byte(i.Filter) } func (c ColorOp) Add(o *op.Ops) {