gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/gpu/internal/rendertest/render_test.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package rendertest
     4  
     5  import (
     6  	"image"
     7  	"image/color"
     8  	"math"
     9  	"testing"
    10  
    11  	"golang.org/x/image/colornames"
    12  
    13  	"gioui.org/internal/f32"
    14  	"gioui.org/internal/f32color"
    15  	"gioui.org/op"
    16  	"gioui.org/op/clip"
    17  	"gioui.org/op/paint"
    18  )
    19  
    20  func TestTransformMacro(t *testing.T) {
    21  	// testcase resulting from original bug when rendering layout.Stacked
    22  
    23  	// Build clip-path.
    24  	c := constSqPath()
    25  
    26  	run(t, func(o *op.Ops) {
    27  
    28  		// render the first Stacked item
    29  		m1 := op.Record(o)
    30  		dr := image.Rect(0, 0, 128, 50)
    31  		paint.FillShape(o, black, clip.Rect(dr).Op())
    32  		c1 := m1.Stop()
    33  
    34  		// Render the second stacked item
    35  		m2 := op.Record(o)
    36  		paint.ColorOp{Color: red}.Add(o)
    37  		// Simulate a draw text call
    38  		t := op.Offset(image.Pt(0, 10)).Push(o)
    39  
    40  		// Apply the clip-path.
    41  		cl := c.Push(o)
    42  
    43  		paint.PaintOp{}.Add(o)
    44  		cl.Pop()
    45  		t.Pop()
    46  
    47  		c2 := m2.Stop()
    48  
    49  		// Call each of them in a transform
    50  		t = op.Offset(image.Pt(0, 0)).Push(o)
    51  		c1.Add(o)
    52  		t.Pop()
    53  		t = op.Offset(image.Pt(0, 0)).Push(o)
    54  		c2.Add(o)
    55  		t.Pop()
    56  	}, func(r result) {
    57  		r.expect(5, 15, colornames.Red)
    58  		r.expect(15, 15, colornames.Black)
    59  		r.expect(11, 51, transparent)
    60  	})
    61  }
    62  
    63  func TestRepeatedPaintsZ(t *testing.T) {
    64  	run(t, func(o *op.Ops) {
    65  		// Draw a rectangle
    66  		paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 128, 50)).Op())
    67  
    68  		builder := clip.Path{}
    69  		builder.Begin(o)
    70  		builder.Move(f32.Pt(0, 0))
    71  		builder.Line(f32.Pt(10, 0))
    72  		builder.Line(f32.Pt(0, 10))
    73  		builder.Line(f32.Pt(-10, 0))
    74  		builder.Line(f32.Pt(0, -10))
    75  		p := builder.End()
    76  		defer clip.Outline{
    77  			Path: p,
    78  		}.Op().Push(o).Pop()
    79  		paint.Fill(o, red)
    80  	}, func(r result) {
    81  		r.expect(5, 5, colornames.Red)
    82  		r.expect(11, 15, colornames.Black)
    83  		r.expect(11, 51, transparent)
    84  	})
    85  }
    86  
    87  func TestNoClipFromPaint(t *testing.T) {
    88  	// ensure that a paint operation does not pollute the state
    89  	// by leaving any clip paths in place.
    90  	run(t, func(o *op.Ops) {
    91  		a := f32.Affine2D{}.Rotate(f32.Pt(20, 20), math.Pi/4)
    92  		defer op.Affine(a).Push(o).Pop()
    93  		paint.FillShape(o, red, clip.Rect(image.Rect(10, 10, 30, 30)).Op())
    94  		a = f32.Affine2D{}.Rotate(f32.Pt(20, 20), -math.Pi/4)
    95  		defer op.Affine(a).Push(o).Pop()
    96  
    97  		paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
    98  	}, func(r result) {
    99  		r.expect(1, 1, colornames.Black)
   100  		r.expect(20, 20, colornames.Black)
   101  		r.expect(49, 49, colornames.Black)
   102  		r.expect(51, 51, transparent)
   103  	})
   104  }
   105  
   106  func TestDeferredPaint(t *testing.T) {
   107  	run(t, func(o *op.Ops) {
   108  		cl := clip.Rect(image.Rect(0, 0, 80, 80)).Op().Push(o)
   109  		paint.ColorOp{Color: color.NRGBA{A: 0x60, G: 0xff}}.Add(o)
   110  		paint.PaintOp{}.Add(o)
   111  		cl.Pop()
   112  
   113  		t := op.Affine(f32.Affine2D{}.Offset(f32.Pt(20, 20))).Push(o)
   114  		m := op.Record(o)
   115  		cl2 := clip.Rect(image.Rect(0, 0, 80, 80)).Op().Push(o)
   116  		paint.ColorOp{Color: color.NRGBA{A: 0x60, R: 0xff, G: 0xff}}.Add(o)
   117  		paint.PaintOp{}.Add(o)
   118  		cl2.Pop()
   119  		paintMacro := m.Stop()
   120  		op.Defer(o, paintMacro)
   121  		t.Pop()
   122  
   123  		defer op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Push(o).Pop()
   124  		defer clip.Rect(image.Rect(0, 0, 80, 80)).Op().Push(o).Pop()
   125  		paint.ColorOp{Color: color.NRGBA{A: 0x60, B: 0xff}}.Add(o)
   126  		paint.PaintOp{}.Add(o)
   127  	}, func(r result) {
   128  	})
   129  }
   130  
   131  func constSqPath() clip.Op {
   132  	innerOps := new(op.Ops)
   133  	builder := clip.Path{}
   134  	builder.Begin(innerOps)
   135  	builder.Move(f32.Pt(0, 0))
   136  	builder.Line(f32.Pt(10, 0))
   137  	builder.Line(f32.Pt(0, 10))
   138  	builder.Line(f32.Pt(-10, 0))
   139  	builder.Line(f32.Pt(0, -10))
   140  	p := builder.End()
   141  	return clip.Outline{Path: p}.Op()
   142  }
   143  
   144  func constSqCirc() clip.Op {
   145  	innerOps := new(op.Ops)
   146  	return clip.RRect{Rect: image.Rect(0, 0, 40, 40),
   147  		NW: 20, NE: 20, SW: 20, SE: 20}.Op(innerOps)
   148  }
   149  
   150  func drawChild(ops *op.Ops, text clip.Op) op.CallOp {
   151  	r1 := op.Record(ops)
   152  	cl := text.Push(ops)
   153  	paint.PaintOp{}.Add(ops)
   154  	cl.Pop()
   155  	return r1.Stop()
   156  }
   157  
   158  func TestReuseStencil(t *testing.T) {
   159  	txt := constSqPath()
   160  	run(t, func(ops *op.Ops) {
   161  		c1 := drawChild(ops, txt)
   162  		c2 := drawChild(ops, txt)
   163  
   164  		// lay out the children
   165  		c1.Add(ops)
   166  
   167  		defer op.Offset(image.Pt(0, 50)).Push(ops).Pop()
   168  		c2.Add(ops)
   169  	}, func(r result) {
   170  		r.expect(5, 5, colornames.Black)
   171  		r.expect(5, 55, colornames.Black)
   172  	})
   173  }
   174  
   175  func TestBuildOffscreen(t *testing.T) {
   176  	// Check that something we in one frame build outside the screen
   177  	// still is rendered correctly if moved into the screen in a later
   178  	// frame.
   179  
   180  	txt := constSqCirc()
   181  	draw := func(off int, o *op.Ops) {
   182  		defer op.Offset(image.Pt(0, off)).Push(o).Pop()
   183  		defer txt.Push(o).Pop()
   184  		paint.PaintOp{}.Add(o)
   185  	}
   186  
   187  	multiRun(t,
   188  		frame(
   189  			func(ops *op.Ops) {
   190  				draw(-100, ops)
   191  			}, func(r result) {
   192  				r.expect(5, 5, transparent)
   193  				r.expect(20, 20, transparent)
   194  			}),
   195  		frame(
   196  			func(ops *op.Ops) {
   197  				draw(0, ops)
   198  			}, func(r result) {
   199  				r.expect(2, 2, transparent)
   200  				r.expect(20, 20, colornames.Black)
   201  				r.expect(38, 38, transparent)
   202  			}))
   203  }
   204  
   205  func TestNegativeOverlaps(t *testing.T) {
   206  	t.Skip("test broken; see issue 479")
   207  
   208  	run(t, func(ops *op.Ops) {
   209  		defer clip.RRect{Rect: image.Rect(50, 50, 100, 100)}.Push(ops).Pop()
   210  		clip.Rect(image.Rect(0, 120, 100, 122)).Push(ops).Pop()
   211  		paint.PaintOp{}.Add(ops)
   212  	}, func(r result) {
   213  		r.expect(60, 60, transparent)
   214  		r.expect(60, 110, transparent)
   215  		r.expect(60, 120, transparent)
   216  		r.expect(60, 122, transparent)
   217  	})
   218  }
   219  
   220  func TestDepthOverlap(t *testing.T) {
   221  	run(t, func(ops *op.Ops) {
   222  		paint.FillShape(ops, red, clip.Rect{Max: image.Pt(128, 64)}.Op())
   223  		paint.FillShape(ops, green, clip.Rect{Max: image.Pt(64, 128)}.Op())
   224  	}, func(r result) {
   225  		r.expect(96, 32, colornames.Red)
   226  		r.expect(32, 96, colornames.Green)
   227  		r.expect(32, 32, colornames.Green)
   228  	})
   229  }
   230  
   231  type Gradient struct {
   232  	From, To color.NRGBA
   233  }
   234  
   235  var gradients = []Gradient{
   236  	{From: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}},
   237  	{From: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}},
   238  	{From: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}},
   239  	{From: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}},
   240  	{From: color.NRGBA{R: 0x19, G: 0xFF, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}},
   241  	{From: color.NRGBA{R: 0xFF, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}},
   242  }
   243  
   244  func TestLinearGradient(t *testing.T) {
   245  	t.Skip("linear gradients don't support transformations")
   246  
   247  	const gradienth = 8
   248  	// 0.5 offset from ends to ensure that the center of the pixel
   249  	// aligns with gradient from and to colors.
   250  	pixelAligned := f32.Rect(0.5, 0, 127.5, gradienth)
   251  	samples := []int{0, 12, 32, 64, 96, 115, 127}
   252  
   253  	run(t, func(ops *op.Ops) {
   254  		gr := f32.Rect(0, 0, 128, gradienth)
   255  		for _, g := range gradients {
   256  			paint.LinearGradientOp{
   257  				Stop1:  f32.Pt(gr.Min.X, gr.Min.Y),
   258  				Color1: g.From,
   259  				Stop2:  f32.Pt(gr.Max.X, gr.Min.Y),
   260  				Color2: g.To,
   261  			}.Add(ops)
   262  			cl := clip.RRect{Rect: gr.Round()}.Push(ops)
   263  			t1 := op.Affine(f32.Affine2D{}.Offset(pixelAligned.Min)).Push(ops)
   264  			t2 := scale(pixelAligned.Dx()/128, 1).Push(ops)
   265  			paint.PaintOp{}.Add(ops)
   266  			t2.Pop()
   267  			t1.Pop()
   268  			cl.Pop()
   269  			gr = gr.Add(f32.Pt(0, gradienth))
   270  		}
   271  	}, func(r result) {
   272  		gr := pixelAligned
   273  		for _, g := range gradients {
   274  			from := f32color.LinearFromSRGB(g.From)
   275  			to := f32color.LinearFromSRGB(g.To)
   276  			for _, p := range samples {
   277  				exp := lerp(from, to, float32(p)/float32(r.img.Bounds().Dx()-1))
   278  				r.expect(p, int(gr.Min.Y+gradienth/2), f32color.NRGBAToRGBA(exp.SRGB()))
   279  			}
   280  			gr = gr.Add(f32.Pt(0, gradienth))
   281  		}
   282  	})
   283  }
   284  
   285  func TestLinearGradientAngled(t *testing.T) {
   286  	run(t, func(ops *op.Ops) {
   287  		paint.LinearGradientOp{
   288  			Stop1:  f32.Pt(64, 64),
   289  			Color1: black,
   290  			Stop2:  f32.Pt(0, 0),
   291  			Color2: red,
   292  		}.Add(ops)
   293  		cl := clip.Rect(image.Rect(0, 0, 64, 64)).Push(ops)
   294  		paint.PaintOp{}.Add(ops)
   295  		cl.Pop()
   296  
   297  		paint.LinearGradientOp{
   298  			Stop1:  f32.Pt(64, 64),
   299  			Color1: white,
   300  			Stop2:  f32.Pt(128, 0),
   301  			Color2: green,
   302  		}.Add(ops)
   303  		cl = clip.Rect(image.Rect(64, 0, 128, 64)).Push(ops)
   304  		paint.PaintOp{}.Add(ops)
   305  		cl.Pop()
   306  
   307  		paint.LinearGradientOp{
   308  			Stop1:  f32.Pt(64, 64),
   309  			Color1: black,
   310  			Stop2:  f32.Pt(128, 128),
   311  			Color2: blue,
   312  		}.Add(ops)
   313  		cl = clip.Rect(image.Rect(64, 64, 128, 128)).Push(ops)
   314  		paint.PaintOp{}.Add(ops)
   315  		cl.Pop()
   316  
   317  		paint.LinearGradientOp{
   318  			Stop1:  f32.Pt(64, 64),
   319  			Color1: white,
   320  			Stop2:  f32.Pt(0, 128),
   321  			Color2: magenta,
   322  		}.Add(ops)
   323  		cl = clip.Rect(image.Rect(0, 64, 64, 128)).Push(ops)
   324  		paint.PaintOp{}.Add(ops)
   325  		cl.Pop()
   326  	}, func(r result) {})
   327  }
   328  
   329  func TestZeroImage(t *testing.T) {
   330  	ops := new(op.Ops)
   331  	w := newWindow(t, 10, 10)
   332  	paint.ImageOp{}.Add(ops)
   333  	paint.PaintOp{}.Add(ops)
   334  	if err := w.Frame(ops); err != nil {
   335  		t.Error(err)
   336  	}
   337  }
   338  
   339  func TestImageRGBA(t *testing.T) {
   340  	run(t, func(o *op.Ops) {
   341  		w := newWindow(t, 10, 10)
   342  
   343  		im := image.NewRGBA(image.Rect(0, 0, 5, 5))
   344  		im.Set(3, 3, colornames.Red)
   345  		im.Set(4, 3, colornames.Red)
   346  		im.Set(3, 4, colornames.Red)
   347  		im.Set(4, 4, colornames.Red)
   348  		im = im.SubImage(image.Rect(2, 2, 5, 5)).(*image.RGBA)
   349  		paint.NewImageOp(im).Add(o)
   350  		paint.PaintOp{}.Add(o)
   351  		if err := w.Frame(o); err != nil {
   352  			t.Error(err)
   353  		}
   354  	}, func(r result) {
   355  		r.expect(1, 1, colornames.Red)
   356  		r.expect(2, 1, colornames.Red)
   357  		r.expect(1, 2, colornames.Red)
   358  		r.expect(2, 2, colornames.Red)
   359  	})
   360  }
   361  
   362  func TestImageRGBA_ScaleLinear(t *testing.T) {
   363  	run(t, func(o *op.Ops) {
   364  		w := newWindow(t, 128, 128)
   365  		defer clip.Rect{Max: image.Pt(128, 128)}.Push(o).Pop()
   366  		op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(64, 64))).Add(o)
   367  
   368  		im := image.NewRGBA(image.Rect(0, 0, 2, 2))
   369  		im.Set(0, 0, colornames.Red)
   370  		im.Set(1, 0, colornames.Green)
   371  		im.Set(0, 1, colornames.White)
   372  		im.Set(1, 1, colornames.Black)
   373  
   374  		op := paint.NewImageOp(im)
   375  		op.Filter = paint.FilterLinear
   376  		op.Add(o)
   377  
   378  		paint.PaintOp{}.Add(o)
   379  
   380  		if err := w.Frame(o); err != nil {
   381  			t.Error(err)
   382  		}
   383  	}, func(r result) {
   384  		r.expect(0, 0, colornames.Red)
   385  		r.expect(8, 8, colornames.Red)
   386  
   387  		// TODO: this currently seems to do srgb scaling
   388  		// instead of linear rgb scaling,
   389  		r.expect(64-4, 0, color.RGBA{R: 197, G: 87, B: 0, A: 255})
   390  		r.expect(64+4, 0, color.RGBA{R: 175, G: 98, B: 0, A: 255})
   391  
   392  		r.expect(127, 0, colornames.Green)
   393  		r.expect(127-8, 8, colornames.Green)
   394  	})
   395  }
   396  
   397  func TestImageRGBA_ScaleNearest(t *testing.T) {
   398  	run(t, func(o *op.Ops) {
   399  		w := newWindow(t, 128, 128)
   400  		op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(64, 64))).Add(o)
   401  
   402  		im := image.NewRGBA(image.Rect(0, 0, 2, 2))
   403  		im.Set(0, 0, colornames.Red)
   404  		im.Set(1, 0, colornames.Green)
   405  		im.Set(0, 1, colornames.White)
   406  		im.Set(1, 1, colornames.Black)
   407  
   408  		op := paint.NewImageOp(im)
   409  		op.Filter = paint.FilterNearest
   410  		op.Add(o)
   411  
   412  		paint.PaintOp{}.Add(o)
   413  
   414  		if err := w.Frame(o); err != nil {
   415  			t.Error(err)
   416  		}
   417  	}, func(r result) {
   418  		r.expect(0, 0, colornames.Red)
   419  		r.expect(8, 8, colornames.Red)
   420  
   421  		r.expect(64-4, 0, colornames.Red)
   422  		r.expect(64+4, 0, colornames.Green)
   423  
   424  		r.expect(127, 0, colornames.Green)
   425  		r.expect(127-8, 8, colornames.Green)
   426  	})
   427  }
   428  
   429  func TestGapsInPath(t *testing.T) {
   430  	ops := new(op.Ops)
   431  	var p clip.Path
   432  	p.Begin(ops)
   433  	// Unclosed square 1
   434  	p.MoveTo(f32.Point{X: 10})
   435  	p.LineTo(f32.Point{X: 40})
   436  	p.LineTo(f32.Point{X: 40, Y: 30})
   437  	p.LineTo(f32.Point{X: 10, Y: 30})
   438  
   439  	// Unclosed square 2
   440  	p.MoveTo(f32.Point{X: 50})
   441  	p.LineTo(f32.Point{X: 80})
   442  	p.LineTo(f32.Point{X: 80, Y: 30})
   443  	p.LineTo(f32.Point{X: 50, Y: 30})
   444  
   445  	spec := p.End()
   446  
   447  	t.Run("Stroke", func(t *testing.T) {
   448  		run(t,
   449  			func(ops *op.Ops) {
   450  				stack := clip.Stroke{
   451  					Path:  spec,
   452  					Width: 2,
   453  				}.Op().Push(ops)
   454  				paint.ColorOp{Color: color.NRGBA{R: 255, A: 255}}.Add(ops)
   455  				paint.PaintOp{}.Add(ops)
   456  				stack.Pop()
   457  			},
   458  			func(r result) {
   459  				r.expect(10, 20, color.RGBA{})
   460  				r.expect(50, 20, color.RGBA{})
   461  			},
   462  		)
   463  	})
   464  
   465  	t.Run("Outline", func(t *testing.T) {
   466  		run(t,
   467  			func(ops *op.Ops) {
   468  				stack := clip.Outline{Path: spec}.Op().Push(ops)
   469  				paint.ColorOp{Color: color.NRGBA{R: 255, A: 255}}.Add(ops)
   470  				paint.PaintOp{}.Add(ops)
   471  				stack.Pop()
   472  			},
   473  			func(r result) {
   474  				r.expect(10, 20, colornames.Red)
   475  				r.expect(20, 20, colornames.Red)
   476  				r.expect(50, 20, colornames.Red)
   477  				r.expect(60, 20, colornames.Red)
   478  			},
   479  		)
   480  	})
   481  }
   482  
   483  func TestOpacity(t *testing.T) {
   484  	run(t, func(ops *op.Ops) {
   485  		opc1 := paint.PushOpacity(ops, .3)
   486  		// Fill screen to exercise the glClear optimization.
   487  		paint.FillShape(ops, color.NRGBA{R: 255, A: 255}, clip.Rect{Max: image.Pt(1024, 1024)}.Op())
   488  		opc2 := paint.PushOpacity(ops, .6)
   489  		paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(20, 10), Max: image.Pt(64, 128)}.Op())
   490  		opc2.Pop()
   491  		opc1.Pop()
   492  		opc3 := paint.PushOpacity(ops, .6)
   493  		paint.FillShape(ops, color.NRGBA{B: 255, A: 255}, clip.Ellipse(image.Rectangle{Min: image.Pt(20+20, 10), Max: image.Pt(50+64, 128)}).Op(ops))
   494  		opc3.Pop()
   495  	}, func(r result) {
   496  	})
   497  }
   498  
   499  // lerp calculates linear interpolation with color b and p.
   500  func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
   501  	return f32color.RGBA{
   502  		R: a.R*(1-p) + b.R*p,
   503  		G: a.G*(1-p) + b.G*p,
   504  		B: a.B*(1-p) + b.B*p,
   505  		A: a.A*(1-p) + b.A*p,
   506  	}
   507  }