Ejemplo n.º 1
0
//export onResize
func onResize(w, h int) {
	// TODO(nigeltao): don't assume 72 DPI. DisplayWidth and DisplayWidthMM
	// is probably the best place to start looking.
	pixelsPerPt = 1
	eventsIn <- event.Config{
		Width:       geom.Pt(w),
		Height:      geom.Pt(h),
		PixelsPerPt: pixelsPerPt,
	}

	// This gl.Viewport call has to be in a separate goroutine because any gl
	// call can block until gl.DoWork is called, but this goroutine is the one
	// responsible for calling gl.DoWork.
	// TODO: does this (GL-using) code belong here in the x/mobile/app
	// package?? See similar TODOs in the Android x/mobile/app implementation.
	c := make(chan struct{})
	go func() {
		gl.Viewport(0, 0, w, h)
		close(c)
	}()
	for {
		select {
		case <-gl.WorkAvailable:
			gl.DoWork()
		case <-c:
			return
		}
	}
}
Ejemplo n.º 2
0
func main(f func(App)) {
	runtime.LockOSThread()
	C.createWindow()

	// TODO: send lifecycle events when e.g. the X11 window is iconified or moved off-screen.
	sendLifecycle(lifecycle.StageFocused)

	donec := make(chan struct{})
	go func() {
		f(app{})
		close(donec)
	}()

	// TODO: can we get the actual vsync signal?
	ticker := time.NewTicker(time.Second / 60)
	defer ticker.Stop()
	tc := ticker.C

	for {
		select {
		case <-donec:
			return
		case <-gl.WorkAvailable:
			gl.DoWork()
		case <-endPaint:
			C.swapBuffers()
			tc = ticker.C
		case <-tc:
			tc = nil
			eventsIn <- paint.Event{}
		}
		C.processEvents()
	}
}
Ejemplo n.º 3
0
func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (done bool) {
	// Android can send a windowRedrawNeeded event any time, including
	// in the middle of a paint cycle. The redraw event may have changed
	// the size of the screen, so any partial painting is now invalidated.
	// We must also not return to Android (via sending on windowRedrawDone)
	// until a complete paint with the new configuration is complete.
	//
	// When a windowRedrawNeeded request comes in, we increment redrawGen
	// (Gen is short for generation number), and do not make a paint cycle
	// visible on <-endPaint unless paintGen agrees. If possible,
	// windowRedrawDone is signalled, allowing onNativeWindowRedrawNeeded
	// to return.
	var redrawGen, paintGen uint32
	for {
		processEvents(queue)
		select {
		case <-donec:
			return true
		case cfg := <-windowConfigChange:
			// TODO save orientation
			pixelsPerPt = cfg.pixelsPerPt
		case w := <-windowRedrawNeeded:
			sendLifecycle(lifecycle.StageFocused)
			widthPx := int(C.ANativeWindow_getWidth(w))
			heightPx := int(C.ANativeWindow_getHeight(w))
			eventsIn <- config.Event{
				WidthPx:     widthPx,
				HeightPx:    heightPx,
				WidthPt:     geom.Pt(float32(widthPx) / pixelsPerPt),
				HeightPt:    geom.Pt(float32(heightPx) / pixelsPerPt),
				PixelsPerPt: pixelsPerPt,
			}
			if paintGen == 0 {
				paintGen++
				C.createEGLWindow(w)
				eventsIn <- paint.Event{}
			}
			redrawGen++
		case <-windowDestroyed:
			sendLifecycle(lifecycle.StageAlive)
			return false
		case <-gl.WorkAvailable:
			gl.DoWork()
		case <-endPaint:
			if paintGen == redrawGen {
				// eglSwapBuffers blocks until vsync.
				C.eglSwapBuffers(C.display, C.surface)
				select {
				case windowRedrawDone <- struct{}{}:
				default:
				}
			}
			paintGen = redrawGen
			eventsIn <- paint.Event{}
		}
	}
}
Ejemplo n.º 4
0
// loop is the primary drawing loop.
//
// After Cocoa has captured the initial OS thread for processing Cocoa
// events in runApp, it starts loop on another goroutine. It is locked
// to an OS thread for its OpenGL context.
//
// Two Cocoa threads deliver draw signals to loop. The primary source of
// draw events is the CVDisplayLink timer, which is tied to the display
// vsync. Secondary draw events come from [NSView drawRect:] when the
// window is resized.
func loop(ctx C.GLintptr) {
	runtime.LockOSThread()
	C.makeCurrentContext(ctx)

	for {
		select {
		case <-gl.WorkAvailable:
			gl.DoWork()
		case <-draw:
		loop1:
			for {
				select {
				case <-gl.WorkAvailable:
					gl.DoWork()
				case <-publish:
					C.CGLFlushDrawable(C.CGLGetCurrentContext())
					publishResult <- PublishResult{}
					break loop1
				}
			}
			drawDone <- struct{}{}
		}
	}
}
Ejemplo n.º 5
0
// drawLoop is the primary drawing loop.
//
// After Cocoa has created an NSWindow on the initial OS thread for
// processing Cocoa events in newWindow, it starts drawLoop on another
// goroutine. It is locked to an OS thread for its OpenGL context.
//
// Two Cocoa threads deliver draw signals to drawLoop. The primary
// source of draw events is the CVDisplayLink timer, which is tied to
// the display vsync. Secondary draw events come from [NSView drawRect:]
// when the window is resized.
func (w *windowImpl) drawLoop(ctx uintptr) {
	runtime.LockOSThread()
	// TODO(crawshaw): there are several problematic issues around having
	// a draw loop per window, but resolving them requires some thought.
	// Firstly, nothing should race on gl.DoWork, so only one person can
	// do that at a time. Secondly, which GL ctx we use matters. A ctx
	// carries window-specific state (for example, the current glViewport
	// value), so we only want to run GL commands on the right context
	// between a <-w.draw and a <-w.drawDone. Thirdly, some GL functions
	// can be legitimately called outside of a window draw cycle, for
	// example, gl.CreateTexture. It doesn't matter which GL ctx we use
	// for that, but we have to use a valid one. So if a window gets
	// closed, it's important we swap the default ctx. More work needed.
	C.makeCurrentContext(C.uintptr_t(ctx))

	// TODO(crawshaw): exit this goroutine on Release.
	for {
		select {
		case <-gl.WorkAvailable:
			gl.DoWork()
		case <-w.draw:
			w.Send(paint.Event{})
		loop:
			for {
				select {
				case <-gl.WorkAvailable:
					gl.DoWork()
				case <-w.endPaint:
					C.CGLFlushDrawable(C.CGLGetCurrentContext())
					break loop
				}
			}
			w.drawDone <- struct{}{}
		}
	}
}
Ejemplo n.º 6
0
//export drawgl
func drawgl(ctx uintptr) {
	if !startedgl {
		startedgl = true
		C.setContext(unsafe.Pointer(ctx))
		// TODO(crawshaw): not just on process start.
		sendLifecycle(lifecycle.StageFocused)
	}

	eventsIn <- paint.Event{}

	for {
		select {
		case <-gl.WorkAvailable:
			gl.DoWork()
		case <-endDraw:
			return
		}
	}
}
Ejemplo n.º 7
0
// loop is the primary drawing loop.
//
// After Cocoa has captured the initial OS thread for processing Cocoa
// events in runApp, it starts loop on another goroutine. It is locked
// to an OS thread for its OpenGL context.
//
// Two Cocoa threads deliver draw signals to loop. The primary source of
// draw events is the CVDisplayLink timer, which is tied to the display
// vsync. Secondary draw events come from [NSView drawRect:] when the
// window is resized.
func loop(ctx C.GLintptr) {
	runtime.LockOSThread()
	C.makeCurrentContext(ctx)

	for range draw {
		eventsIn <- paint.Event{}
	loop1:
		for {
			select {
			case <-gl.WorkAvailable:
				gl.DoWork()
			case <-endPaint:
				C.CGLFlushDrawable(C.CGLGetCurrentContext())
				break loop1
			}
		}
		drawDone <- struct{}{}
	}
}
Ejemplo n.º 8
0
func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (done bool) {
	C.createEGLWindow(w)

	// TODO: is this needed if we also have the "case <-windowRedrawNeeded:" below??
	sendLifecycle(lifecycle.StageFocused)
	eventsIn <- config.Event{
		Width:       geom.Pt(float32(C.windowWidth) / pixelsPerPt),
		Height:      geom.Pt(float32(C.windowHeight) / pixelsPerPt),
		PixelsPerPt: pixelsPerPt,
	}
	if firstWindowDraw {
		firstWindowDraw = false
		// TODO: be more principled about when to send a paint event.
		eventsIn <- paint.Event{}
	}

	for {
		processEvents(queue)
		select {
		case <-donec:
			return true
		case <-windowRedrawNeeded:
			// Re-query the width and height.
			C.querySurfaceWidthAndHeight()
			sendLifecycle(lifecycle.StageFocused)
			eventsIn <- config.Event{
				Width:       geom.Pt(float32(C.windowWidth) / pixelsPerPt),
				Height:      geom.Pt(float32(C.windowHeight) / pixelsPerPt),
				PixelsPerPt: pixelsPerPt,
			}
		case <-windowDestroyed:
			sendLifecycle(lifecycle.StageAlive)
			return false
		case <-gl.WorkAvailable:
			gl.DoWork()
		case <-endDraw:
			// eglSwapBuffers blocks until vsync.
			C.eglSwapBuffers(C.display, C.surface)
			eventsIn <- paint.Event{}
		}
	}
}
Ejemplo n.º 9
0
//export drawgl
func drawgl(ctx uintptr) {
	if !startedgl {
		startedgl = true
		C.setContext(unsafe.Pointer(ctx))
		// TODO(crawshaw): not just on process start.
		sendLifecycle(lifecycle.StageFocused)
	}

	// TODO(crawshaw): don't send a paint.Event unconditionally. Only send one
	// if the window actually needs redrawing.
	eventsIn <- paint.Event{}

	for {
		select {
		case <-gl.WorkAvailable:
			gl.DoWork()
		case <-publish:
			publishResult <- PublishResult{}
			return
		}
	}
}
Ejemplo n.º 10
0
func main(f func(App)) {
	runtime.LockOSThread()
	C.createWindow()

	// TODO: send lifecycle events when e.g. the X11 window is iconified or moved off-screen.
	sendLifecycle(lifecycle.StageFocused)

	// TODO: translate X11 expose events to shiny paint events, instead of
	// sending this synthetic paint event as a hack.
	eventsIn <- paint.Event{}

	donec := make(chan struct{})
	go func() {
		f(app{})
		close(donec)
	}()

	// TODO: can we get the actual vsync signal?
	ticker := time.NewTicker(time.Second / 60)
	defer ticker.Stop()
	var tc <-chan time.Time

	for {
		select {
		case <-donec:
			return
		case <-gl.WorkAvailable:
			gl.DoWork()
		case <-publish:
			C.swapBuffers()
			tc = ticker.C
		case <-tc:
			tc = nil
			publishResult <- PublishResult{}
		}
		C.processEvents()
	}
}
Ejemplo n.º 11
0
func mainUI(vm, jniEnv, ctx uintptr) error {
	env := (*C.JNIEnv)(unsafe.Pointer(jniEnv)) // not a Go heap pointer

	donec := make(chan struct{})
	go func() {
		mainUserFn(app{})
		close(donec)
	}()

	var q *C.AInputQueue
	var pixelsPerPt float32
	var orientation size.Orientation

	// Android can send a windowRedrawNeeded event any time, including
	// in the middle of a paint cycle. The redraw event may have changed
	// the size of the screen, so any partial painting is now invalidated.
	// We must also not return to Android (via sending on windowRedrawDone)
	// until a complete paint with the new configuration is complete.
	//
	// When a windowRedrawNeeded request comes in, we increment redrawGen
	// (Gen is short for generation number), and do not make a paint cycle
	// visible on <-endPaint unless Generation agrees. If possible,
	// windowRedrawDone is signalled, allowing onNativeWindowRedrawNeeded
	// to return.
	var redrawGen uint32

	for {
		if q != nil {
			processEvents(env, q)
		}
		select {
		case <-windowCreated:
		case q = <-inputQueue:
		case <-donec:
			return nil
		case cfg := <-windowConfigChange:
			pixelsPerPt = cfg.pixelsPerPt
			orientation = cfg.orientation
		case w := <-windowRedrawNeeded:
			if C.surface == nil {
				if errStr := C.createEGLSurface(w); errStr != nil {
					return fmt.Errorf("%s (%s)", C.GoString(errStr), eglGetError())
				}
			}
			sendLifecycle(lifecycle.StageFocused)
			widthPx := int(C.ANativeWindow_getWidth(w))
			heightPx := int(C.ANativeWindow_getHeight(w))
			eventsIn <- size.Event{
				WidthPx:     widthPx,
				HeightPx:    heightPx,
				WidthPt:     geom.Pt(float32(widthPx) / pixelsPerPt),
				HeightPt:    geom.Pt(float32(heightPx) / pixelsPerPt),
				PixelsPerPt: pixelsPerPt,
				Orientation: orientation,
			}
			redrawGen++
			eventsIn <- paint.Event{redrawGen}
		case <-windowDestroyed:
			if C.surface != nil {
				if errStr := C.destroyEGLSurface(); errStr != nil {
					return fmt.Errorf("%s (%s)", C.GoString(errStr), eglGetError())
				}
			}
			C.surface = nil
			sendLifecycle(lifecycle.StageAlive)
		case <-gl.WorkAvailable:
			gl.DoWork()
		case p := <-endPaint:
			if p.Generation != redrawGen {
				continue
			}
			if C.surface != nil {
				// eglSwapBuffers blocks until vsync.
				if C.eglSwapBuffers(C.display, C.surface) == C.EGL_FALSE {
					log.Printf("app: failed to swap buffers (%s)", eglGetError())
				}
			}
			select {
			case windowRedrawDone <- struct{}{}:
			default:
			}
			if C.surface != nil {
				redrawGen++
				eventsIn <- paint.Event{redrawGen}
			}
		}
	}
}
Ejemplo n.º 12
0
func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (done bool) {
	C.createEGLWindow(w)

	// TODO: is this needed if we also have the "case <-windowRedrawNeeded:" below??
	sendLifecycle(event.LifecycleStageFocused)
	eventsIn <- event.Config{
		Width:       geom.Pt(float32(C.windowWidth) / pixelsPerPt),
		Height:      geom.Pt(float32(C.windowHeight) / pixelsPerPt),
		PixelsPerPt: pixelsPerPt,
	}
	if firstWindowDraw {
		firstWindowDraw = false
		// TODO: be more principled about when to send a draw event.
		eventsIn <- event.Draw{}
	}

	for {
		processEvents(queue)
		select {
		case <-donec:
			return true
		case <-windowRedrawNeeded:
			// Re-query the width and height.
			C.querySurfaceWidthAndHeight()
			sendLifecycle(event.LifecycleStageFocused)
			eventsIn <- event.Config{
				Width:       geom.Pt(float32(C.windowWidth) / pixelsPerPt),
				Height:      geom.Pt(float32(C.windowHeight) / pixelsPerPt),
				PixelsPerPt: pixelsPerPt,
			}
			// This gl.Viewport call has to be in a separate goroutine because any gl
			// call can block until gl.DoWork is called, but this goroutine is the one
			// responsible for calling gl.DoWork.
			// TODO: again, should x/mobile/app be responsible for calling GL code, or
			// should package gl instead call event.RegisterFilter?
			{
				c := make(chan struct{})
				go func() {
					gl.Viewport(0, 0, int(C.windowWidth), int(C.windowHeight))
					close(c)
				}()
			loop1:
				for {
					select {
					case <-gl.WorkAvailable:
						gl.DoWork()
					case <-c:
						break loop1
					}
				}
			}
		case <-windowDestroyed:
			sendLifecycle(event.LifecycleStageAlive)
			return false
		case <-gl.WorkAvailable:
			gl.DoWork()
		case <-endDraw:
			// eglSwapBuffers blocks until vsync.
			C.eglSwapBuffers(C.display, C.surface)
			eventsIn <- event.Draw{}
		}
	}
}
Ejemplo n.º 13
0
func main(f func(App)) {
	// Preserve this OS thread for the GL context created below.
	runtime.LockOSThread()

	donec := make(chan struct{})
	go func() {
		f(app{})
		close(donec)
	}()

	var q *C.AInputQueue

	// Android can send a windowRedrawNeeded event any time, including
	// in the middle of a paint cycle. The redraw event may have changed
	// the size of the screen, so any partial painting is now invalidated.
	// We must also not return to Android (via sending on windowRedrawDone)
	// until a complete paint with the new configuration is complete.
	//
	// When a windowRedrawNeeded request comes in, we increment redrawGen
	// (Gen is short for generation number), and do not make a paint cycle
	// visible on <-endPaint unless Generation agrees. If possible,
	// windowRedrawDone is signalled, allowing onNativeWindowRedrawNeeded
	// to return.
	var redrawGen uint32

	for {
		if q != nil {
			processEvents(q)
		}
		select {
		case <-windowCreated:
		case q = <-inputQueue:
		case <-donec:
			return
		case cfg := <-windowConfigChange:
			// TODO save orientation
			pixelsPerPt = cfg.pixelsPerPt
		case w := <-windowRedrawNeeded:
			newWindow := C.surface == nil
			if newWindow {
				if errStr := C.createEGLSurface(w); errStr != nil {
					log.Printf("app: %s (%s)", C.GoString(errStr), eglGetError())
					return
				}
			}
			sendLifecycle(lifecycle.StageFocused)
			widthPx := int(C.ANativeWindow_getWidth(w))
			heightPx := int(C.ANativeWindow_getHeight(w))
			eventsIn <- config.Event{
				WidthPx:     widthPx,
				HeightPx:    heightPx,
				WidthPt:     geom.Pt(float32(widthPx) / pixelsPerPt),
				HeightPt:    geom.Pt(float32(heightPx) / pixelsPerPt),
				PixelsPerPt: pixelsPerPt,
			}
			redrawGen++
			if newWindow {
				// New window, begin paint loop.
				eventsIn <- paint.Event{redrawGen}
			}
		case <-windowDestroyed:
			if C.surface != nil {
				if errStr := C.destroyEGLSurface(); errStr != nil {
					log.Printf("app: %s (%s)", C.GoString(errStr), eglGetError())
					return
				}
			}
			C.surface = nil
			sendLifecycle(lifecycle.StageAlive)
		case <-gl.WorkAvailable:
			gl.DoWork()
		case p := <-endPaint:
			if p.Generation != redrawGen {
				continue
			}
			if C.surface != nil {
				// eglSwapBuffers blocks until vsync.
				if C.eglSwapBuffers(C.display, C.surface) == C.EGL_FALSE {
					log.Printf("app: failed to swap buffers (%s)", eglGetError())
				}
			}
			select {
			case windowRedrawDone <- struct{}{}:
			default:
			}
			if C.surface != nil {
				redrawGen++
				eventsIn <- paint.Event{redrawGen}
			}
		}
	}
}
Ejemplo n.º 14
0
func TestImage(t *testing.T) {
	done := make(chan struct{})
	defer close(done)
	go func() {
		runtime.LockOSThread()
		ctx := createContext()
		for {
			select {
			case <-gl.WorkAvailable:
				gl.DoWork()
			case <-done:
				ctx.destroy()
				return
			}
		}
	}()
	start()
	defer stop()

	// GL testing strategy:
	// 	1. Create an offscreen framebuffer object.
	// 	2. Configure framebuffer to render to a GL texture.
	//	3. Run test code: use glimage to draw testdata.
	//	4. Copy GL texture back into system memory.
	//	5. Compare to a pre-computed image.

	f, err := os.Open("../../../testdata/testpattern.png")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	src, _, err := image.Decode(f)
	if err != nil {
		t.Fatal(err)
	}

	const (
		pixW = 100
		pixH = 100
		ptW  = geom.Pt(50)
		ptH  = geom.Pt(50)
	)
	cfg := config.Event{
		WidthPx:     pixW,
		HeightPx:    pixH,
		WidthPt:     ptW,
		HeightPt:    ptH,
		PixelsPerPt: float32(pixW) / float32(ptW),
	}

	fBuf := gl.CreateFramebuffer()
	gl.BindFramebuffer(gl.FRAMEBUFFER, fBuf)
	colorBuf := gl.CreateRenderbuffer()
	gl.BindRenderbuffer(gl.RENDERBUFFER, colorBuf)
	// https://www.khronos.org/opengles/sdk/docs/man/xhtml/glRenderbufferStorage.xml
	// says that the internalFormat "must be one of the following symbolic constants:
	// GL_RGBA4, GL_RGB565, GL_RGB5_A1, GL_DEPTH_COMPONENT16, or GL_STENCIL_INDEX8".
	gl.RenderbufferStorage(gl.RENDERBUFFER, gl.RGB565, pixW, pixH)
	gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, colorBuf)

	if status := gl.CheckFramebufferStatus(gl.FRAMEBUFFER); status != gl.FRAMEBUFFER_COMPLETE {
		t.Fatalf("framebuffer create failed: %v", status)
	}

	allocs := testing.AllocsPerRun(100, func() {
		gl.ClearColor(0, 0, 1, 1) // blue
	})
	if allocs != 0 {
		t.Errorf("unexpected allocations from calling gl.ClearColor: %f", allocs)
	}
	gl.Clear(gl.COLOR_BUFFER_BIT)
	gl.Viewport(0, 0, pixW, pixH)

	m := NewImage(src.Bounds().Dx(), src.Bounds().Dy())
	b := m.RGBA.Bounds()
	draw.Draw(m.RGBA, b, src, src.Bounds().Min, draw.Src)
	m.Upload()
	b.Min.X += 10
	b.Max.Y /= 2

	// All-integer right-angled triangles offsetting the
	// box: 24-32-40, 12-16-20.
	ptTopLeft := geom.Point{0, 24}
	ptTopRight := geom.Point{32, 0}
	ptBottomLeft := geom.Point{12, 24 + 16}
	ptBottomRight := geom.Point{12 + 32, 16}
	m.Draw(cfg, ptTopLeft, ptTopRight, ptBottomLeft, b)

	// For unknown reasons, a windowless OpenGL context renders upside-
	// down. That is, a quad covering the initial viewport spans:
	//
	//	(-1, -1) ( 1, -1)
	//	(-1,  1) ( 1,  1)
	//
	// To avoid modifying live code for tests, we flip the rows
	// recovered from the renderbuffer. We are not the first:
	//
	// http://lists.apple.com/archives/mac-opengl/2010/Jun/msg00080.html
	got := image.NewRGBA(image.Rect(0, 0, pixW, pixH))
	upsideDownPix := make([]byte, len(got.Pix))
	gl.ReadPixels(upsideDownPix, 0, 0, pixW, pixH, gl.RGBA, gl.UNSIGNED_BYTE)
	for y := 0; y < pixH; y++ {
		i0 := (pixH - 1 - y) * got.Stride
		i1 := i0 + pixW*4
		copy(got.Pix[y*got.Stride:], upsideDownPix[i0:i1])
	}

	drawCross(got, 0, 0)
	drawCross(got, int(ptTopLeft.X.Px(cfg.PixelsPerPt)), int(ptTopLeft.Y.Px(cfg.PixelsPerPt)))
	drawCross(got, int(ptBottomRight.X.Px(cfg.PixelsPerPt)), int(ptBottomRight.Y.Px(cfg.PixelsPerPt)))
	drawCross(got, pixW-1, pixH-1)

	const wantPath = "../../../testdata/testpattern-window.png"
	f, err = os.Open(wantPath)
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	wantSrc, _, err := image.Decode(f)
	if err != nil {
		t.Fatal(err)
	}
	want, ok := wantSrc.(*image.RGBA)
	if !ok {
		b := wantSrc.Bounds()
		want = image.NewRGBA(b)
		draw.Draw(want, b, wantSrc, b.Min, draw.Src)
	}

	if !imageEq(got, want) {
		// Write out the image we got.
		f, err = ioutil.TempFile("", "testpattern-window-got")
		if err != nil {
			t.Fatal(err)
		}
		f.Close()
		gotPath := f.Name() + ".png"
		f, err = os.Create(gotPath)
		if err != nil {
			t.Fatal(err)
		}
		if err := png.Encode(f, got); err != nil {
			t.Fatal(err)
		}
		if err := f.Close(); err != nil {
			t.Fatal(err)
		}
		t.Errorf("got\n%s\nwant\n%s", gotPath, wantPath)
	}
}