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 }