diff --git a/gpu/headless/backend_test.go b/gpu/headless/backend_test.go index b91929dc..2429b7d5 100644 --- a/gpu/headless/backend_test.go +++ b/gpu/headless/backend_test.go @@ -26,7 +26,7 @@ func TestFramebufferClear(t *testing.T) { b := newBackend(t) sz := image.Point{X: 800, Y: 600} fbo := setupFBO(t, b, sz) - img := screenshot(t, fbo, sz) + img := screenshot(t, b, fbo, sz) if got := img.RGBAAt(0, 0); got != clearColExpect { t.Errorf("got color %v, expected %v", got, clearColExpect) } @@ -43,7 +43,7 @@ func TestSimpleShader(t *testing.T) { defer p.Release() b.BindProgram(p) b.DrawArrays(driver.DrawModeTriangles, 0, 3) - img := screenshot(t, fbo, sz) + img := screenshot(t, b, fbo, sz) if got := img.RGBAAt(0, 0); got != clearColExpect { t.Errorf("got color %v, expected %v", got, clearColExpect) } @@ -90,7 +90,7 @@ func TestInputShader(t *testing.T) { defer layout.Release() b.BindInputLayout(layout) b.DrawArrays(driver.DrawModeTriangles, 0, 3) - img := screenshot(t, fbo, sz) + img := screenshot(t, b, fbo, sz) if got := img.RGBAAt(0, 0); got != clearColExpect { t.Errorf("got color %v, expected %v", got, clearColExpect) } @@ -115,11 +115,11 @@ func TestFramebuffers(t *testing.T) { b.Clear(fcol1.Float32()) b.BindFramebuffer(fbo2) b.Clear(fcol2.Float32()) - img := screenshot(t, fbo1, sz) + img := screenshot(t, b, fbo1, sz) if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col1) { t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col1)) } - img = screenshot(t, fbo2, sz) + img = screenshot(t, b, fbo2, sz) if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col2) { t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col2)) } @@ -184,12 +184,8 @@ func newBackend(t *testing.T) driver.Device { return b } -func screenshot(t *testing.T, fbo driver.Framebuffer, size image.Point) *image.RGBA { - img := image.NewRGBA(image.Rectangle{Max: size}) - err := fbo.ReadPixels( - image.Rectangle{ - Max: image.Point{X: size.X, Y: size.Y}, - }, img.Pix) +func screenshot(t *testing.T, d driver.Device, fbo driver.Framebuffer, size image.Point) *image.RGBA { + img, err := driver.DownloadImage(d, fbo, image.Rectangle{Max: size}) if err != nil { t.Fatal(err) } diff --git a/gpu/headless/headless.go b/gpu/headless/headless.go index f04d959c..a9b9e344 100644 --- a/gpu/headless/headless.go +++ b/gpu/headless/headless.go @@ -118,12 +118,11 @@ func (w *Window) Frame(frame *op.Ops) error { // Screenshot returns an image with the content of the window. func (w *Window) Screenshot() (*image.RGBA, error) { - img := image.NewRGBA(image.Rectangle{Max: w.size}) + var img *image.RGBA err := contextDo(w.ctx, func() error { - return w.fbo.ReadPixels( - image.Rectangle{ - Max: image.Point{X: w.size.X, Y: w.size.Y}, - }, img.Pix) + var err error + img, err = driver.DownloadImage(w.dev, w.fbo, image.Rectangle{Max: w.size}) + return err }) if err != nil { return nil, err diff --git a/gpu/internal/driver/driver.go b/gpu/internal/driver/driver.go index 1227745b..7a45443e 100644 --- a/gpu/internal/driver/driver.go +++ b/gpu/internal/driver/driver.go @@ -138,8 +138,11 @@ type DepthFunc uint8 type Features uint type Caps struct { - Features Features - MaxTextureSize int + // BottomLeftOrigin is true if the driver has the origin in the lower left + // corner. The OpenGL driver returns true. + BottomLeftOrigin bool + Features Features + MaxTextureSize int } type Program interface { @@ -233,6 +236,33 @@ func (f Features) Has(feats Features) bool { return f&feats == feats } +func DownloadImage(d Device, f Framebuffer, r image.Rectangle) (*image.RGBA, error) { + img := image.NewRGBA(r) + if err := f.ReadPixels(r, img.Pix); err != nil { + return nil, err + } + if d.Caps().BottomLeftOrigin { + // OpenGL origin is in the lower-left corner. Flip the image to + // match. + flipImageY(r.Dx()*4, r.Dy(), img.Pix) + } + return img, nil +} + +func flipImageY(stride, height int, pixels []byte) { + // Flip image in y-direction. OpenGL's origin is in the lower + // left corner. + row := make([]uint8, stride) + for y := 0; y < height/2; y++ { + y1 := height - y - 1 + dest := y1 * stride + src := y * stride + copy(row, pixels[dest:]) + copy(pixels[dest:], pixels[src:src+len(row)]) + copy(pixels[src:], row) + } +} + func UploadImage(t Texture, offset image.Point, img *image.RGBA) { var pixels []byte size := img.Bounds().Size() diff --git a/gpu/internal/opengl/backend.go b/gpu/internal/opengl/backend.go index 79bb7145..ecb45550 100644 --- a/gpu/internal/opengl/backend.go +++ b/gpu/internal/opengl/backend.go @@ -154,6 +154,7 @@ func newOpenGLDevice(api driver.OpenGL) (driver.Device, error) { alphaTriple: alphaTripleFor(ver), srgbaTriple: srgbaTriple, } + b.feats.BottomLeftOrigin = true if ffboErr == nil { b.feats.Features |= driver.FeatureFloatRenderTargets } @@ -771,26 +772,9 @@ func (f *gpuFramebuffer) ReadPixels(src image.Rectangle, pixels []byte) error { return errors.New("unexpected RGBA size") } f.backend.funcs.ReadPixels(src.Min.X, src.Min.Y, src.Dx(), src.Dy(), gl.RGBA, gl.UNSIGNED_BYTE, pixels) - // OpenGL origin is in the lower-left corner. Flip the image to - // match. - flipImageY(src.Dx()*4, src.Dy(), pixels) return glErr(f.backend.funcs) } -func flipImageY(stride int, height int, pixels []byte) { - // Flip image in y-direction. OpenGL's origin is in the lower - // left corner. - row := make([]uint8, stride) - for y := 0; y < height/2; y++ { - y1 := height - y - 1 - dest := y1 * stride - src := y * stride - copy(row, pixels[dest:]) - copy(pixels[dest:], pixels[src:src+len(row)]) - copy(pixels[src:], row) - } -} - func (b *Backend) BindFramebuffer(fbo driver.Framebuffer) { b.funcs.BindFramebuffer(gl.FRAMEBUFFER, fbo.(*gpuFramebuffer).obj) }