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  }