github.com/c-darwin/mobile@v0.0.0-20160313183840-ff625c46f7c9/exp/gl/glutil/glimage_test.go (about) 1 // Copyright 2014 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // +build darwin linux,!android 6 7 // TODO(crawshaw): Run tests on other OSs when more contexts are supported. 8 9 package glutil 10 11 import ( 12 "image" 13 "image/color" 14 "image/draw" 15 "image/png" 16 "io/ioutil" 17 "os" 18 "runtime" 19 "testing" 20 21 "github.com/c-darwin/mobile/event/size" 22 "github.com/c-darwin/mobile/geom" 23 "github.com/c-darwin/mobile/gl" 24 ) 25 26 func TestImage(t *testing.T) { 27 done := make(chan error) 28 defer close(done) 29 go func() { 30 runtime.LockOSThread() 31 ctx, err := createContext() 32 done <- err 33 for { 34 select { 35 case <-gl.WorkAvailable: 36 gl.DoWork() 37 case <-done: 38 ctx.destroy() 39 return 40 } 41 } 42 }() 43 if err := <-done; err != nil { 44 t.Fatalf("cannot create GL context: %v", err) 45 } 46 47 start() 48 defer stop() 49 50 // GL testing strategy: 51 // 1. Create an offscreen framebuffer object. 52 // 2. Configure framebuffer to render to a GL texture. 53 // 3. Run test code: use glimage to draw testdata. 54 // 4. Copy GL texture back into system memory. 55 // 5. Compare to a pre-computed image. 56 57 f, err := os.Open("../../../testdata/testpattern.png") 58 if err != nil { 59 t.Fatal(err) 60 } 61 defer f.Close() 62 src, _, err := image.Decode(f) 63 if err != nil { 64 t.Fatal(err) 65 } 66 67 const ( 68 pixW = 100 69 pixH = 100 70 ptW = geom.Pt(50) 71 ptH = geom.Pt(50) 72 ) 73 sz := size.Event{ 74 WidthPx: pixW, 75 HeightPx: pixH, 76 WidthPt: ptW, 77 HeightPt: ptH, 78 PixelsPerPt: float32(pixW) / float32(ptW), 79 } 80 81 fBuf := gl.CreateFramebuffer() 82 gl.BindFramebuffer(gl.FRAMEBUFFER, fBuf) 83 colorBuf := gl.CreateRenderbuffer() 84 gl.BindRenderbuffer(gl.RENDERBUFFER, colorBuf) 85 // https://www.khronos.org/opengles/sdk/docs/man/xhtml/glRenderbufferStorage.xml 86 // says that the internalFormat "must be one of the following symbolic constants: 87 // GL_RGBA4, GL_RGB565, GL_RGB5_A1, GL_DEPTH_COMPONENT16, or GL_STENCIL_INDEX8". 88 gl.RenderbufferStorage(gl.RENDERBUFFER, gl.RGB565, pixW, pixH) 89 gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, colorBuf) 90 91 if status := gl.CheckFramebufferStatus(gl.FRAMEBUFFER); status != gl.FRAMEBUFFER_COMPLETE { 92 t.Fatalf("framebuffer create failed: %v", status) 93 } 94 95 allocs := testing.AllocsPerRun(100, func() { 96 gl.ClearColor(0, 0, 1, 1) // blue 97 }) 98 if allocs != 0 { 99 t.Errorf("unexpected allocations from calling gl.ClearColor: %f", allocs) 100 } 101 gl.Clear(gl.COLOR_BUFFER_BIT) 102 gl.Viewport(0, 0, pixW, pixH) 103 104 m := NewImage(src.Bounds().Dx(), src.Bounds().Dy()) 105 b := m.RGBA.Bounds() 106 draw.Draw(m.RGBA, b, src, src.Bounds().Min, draw.Src) 107 m.Upload() 108 b.Min.X += 10 109 b.Max.Y /= 2 110 111 // All-integer right-angled triangles offsetting the 112 // box: 24-32-40, 12-16-20. 113 ptTopLeft := geom.Point{0, 24} 114 ptTopRight := geom.Point{32, 0} 115 ptBottomLeft := geom.Point{12, 24 + 16} 116 ptBottomRight := geom.Point{12 + 32, 16} 117 m.Draw(sz, ptTopLeft, ptTopRight, ptBottomLeft, b) 118 119 // For unknown reasons, a windowless OpenGL context renders upside- 120 // down. That is, a quad covering the initial viewport spans: 121 // 122 // (-1, -1) ( 1, -1) 123 // (-1, 1) ( 1, 1) 124 // 125 // To avoid modifying live code for tests, we flip the rows 126 // recovered from the renderbuffer. We are not the first: 127 // 128 // http://lists.apple.com/archives/mac-opengl/2010/Jun/msg00080.html 129 got := image.NewRGBA(image.Rect(0, 0, pixW, pixH)) 130 upsideDownPix := make([]byte, len(got.Pix)) 131 gl.ReadPixels(upsideDownPix, 0, 0, pixW, pixH, gl.RGBA, gl.UNSIGNED_BYTE) 132 for y := 0; y < pixH; y++ { 133 i0 := (pixH - 1 - y) * got.Stride 134 i1 := i0 + pixW*4 135 copy(got.Pix[y*got.Stride:], upsideDownPix[i0:i1]) 136 } 137 138 drawCross(got, 0, 0) 139 drawCross(got, int(ptTopLeft.X.Px(sz.PixelsPerPt)), int(ptTopLeft.Y.Px(sz.PixelsPerPt))) 140 drawCross(got, int(ptBottomRight.X.Px(sz.PixelsPerPt)), int(ptBottomRight.Y.Px(sz.PixelsPerPt))) 141 drawCross(got, pixW-1, pixH-1) 142 143 const wantPath = "../../../testdata/testpattern-window.png" 144 f, err = os.Open(wantPath) 145 if err != nil { 146 t.Fatal(err) 147 } 148 defer f.Close() 149 wantSrc, _, err := image.Decode(f) 150 if err != nil { 151 t.Fatal(err) 152 } 153 want, ok := wantSrc.(*image.RGBA) 154 if !ok { 155 b := wantSrc.Bounds() 156 want = image.NewRGBA(b) 157 draw.Draw(want, b, wantSrc, b.Min, draw.Src) 158 } 159 160 if !imageEq(got, want) { 161 // Write out the image we got. 162 f, err = ioutil.TempFile("", "testpattern-window-got") 163 if err != nil { 164 t.Fatal(err) 165 } 166 f.Close() 167 gotPath := f.Name() + ".png" 168 f, err = os.Create(gotPath) 169 if err != nil { 170 t.Fatal(err) 171 } 172 if err := png.Encode(f, got); err != nil { 173 t.Fatal(err) 174 } 175 if err := f.Close(); err != nil { 176 t.Fatal(err) 177 } 178 t.Errorf("got\n%s\nwant\n%s", gotPath, wantPath) 179 } 180 } 181 182 func drawCross(m *image.RGBA, x, y int) { 183 c := color.RGBA{0xff, 0, 0, 0xff} // red 184 m.SetRGBA(x+0, y-2, c) 185 m.SetRGBA(x+0, y-1, c) 186 m.SetRGBA(x-2, y+0, c) 187 m.SetRGBA(x-1, y+0, c) 188 m.SetRGBA(x+0, y+0, c) 189 m.SetRGBA(x+1, y+0, c) 190 m.SetRGBA(x+2, y+0, c) 191 m.SetRGBA(x+0, y+1, c) 192 m.SetRGBA(x+0, y+2, c) 193 } 194 195 func eqEpsilon(x, y uint8) bool { 196 const epsilon = 9 197 return x-y < epsilon || y-x < epsilon 198 } 199 200 func colorEq(c0, c1 color.RGBA) bool { 201 return eqEpsilon(c0.R, c1.R) && eqEpsilon(c0.G, c1.G) && eqEpsilon(c0.B, c1.B) && eqEpsilon(c0.A, c1.A) 202 } 203 204 func imageEq(m0, m1 *image.RGBA) bool { 205 b0 := m0.Bounds() 206 b1 := m1.Bounds() 207 if b0 != b1 { 208 return false 209 } 210 badPx := 0 211 for y := b0.Min.Y; y < b0.Max.Y; y++ { 212 for x := b0.Min.X; x < b0.Max.X; x++ { 213 c0, c1 := m0.At(x, y).(color.RGBA), m1.At(x, y).(color.RGBA) 214 if !colorEq(c0, c1) { 215 badPx++ 216 } 217 } 218 } 219 badFrac := float64(badPx) / float64(b0.Dx()*b0.Dy()) 220 return badFrac < 0.01 221 }