Files
gio/app/headless/headless.go
T
2020-02-10 18:22:58 +01:00

150 lines
3.0 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
// Package headless implements headless windows for rendering
// an operation list to an image.
package headless
import (
"fmt"
"image"
"runtime"
"gioui.org/app/internal/glimpl"
"gioui.org/app/internal/srgb"
"gioui.org/gpu"
"gioui.org/gpu/gl"
"gioui.org/op"
)
// Window is a headless window.
type Window struct {
size image.Point
ctx context
fbo *srgb.SRGBFBO
gpu *gpu.GPU
}
type context interface {
Functions() *glimpl.Functions
MakeCurrent() error
ReleaseCurrent()
Release()
}
// NewWindow creates a new headless window.
func NewWindow(width, height int) (*Window, error) {
ctx, err := newContext()
if err != nil {
return nil, err
}
w := &Window{
size: image.Point{X: width, Y: height},
ctx: ctx,
}
err = contextDo(ctx, func() error {
f := ctx.Functions()
fbo, err := srgb.NewSRGBFBO(f)
if err != nil {
ctx.Release()
return err
}
if err := fbo.Refresh(width, height); err != nil {
fbo.Release()
ctx.Release()
return err
}
backend, err := gl.NewBackend(f)
if err != nil {
fbo.Release()
ctx.Release()
return err
}
gpu, err := gpu.New(backend)
if err != nil {
fbo.Release()
ctx.Release()
return err
}
w.fbo = fbo
w.gpu = gpu
return err
})
if err != nil {
return nil, err
}
return w, nil
}
// Release resources associated with the window.
func (w *Window) Release() {
contextDo(w.ctx, func() error {
if w.gpu != nil {
w.gpu.Release()
w.gpu = nil
}
if w.fbo != nil {
w.fbo.Release()
w.fbo = nil
}
if w.ctx != nil {
w.ctx.Release()
w.ctx = nil
}
return nil
})
}
// Frame replace the window content and state with the
// operation list.
func (w *Window) Frame(frame *op.Ops) {
contextDo(w.ctx, func() error {
w.gpu.Collect(w.size, frame)
w.gpu.BeginFrame()
w.gpu.EndFrame()
return nil
})
}
// 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})
if len(img.Pix) != w.size.X*w.size.Y*4 {
panic("unexpected RGBA size")
}
contextDo(w.ctx, func() error {
f := w.ctx.Functions()
f.ReadPixels(0, 0, w.size.X, w.size.Y, gl.RGBA, gl.UNSIGNED_BYTE, img.Pix)
if glErr := f.GetError(); glErr != gl.NO_ERROR {
return fmt.Errorf("glReadPixels failed: %d", glErr)
}
return nil
})
// Flip image in y-direction. OpenGL's origin is in the lower
// left corner.
row := make([]uint8, img.Stride)
for y := 0; y < w.size.Y/2; y++ {
y1 := w.size.Y - y - 1
dest := img.PixOffset(0, y1)
src := img.PixOffset(0, y)
copy(row, img.Pix[dest:])
copy(img.Pix[dest:], img.Pix[src:src+len(row)])
copy(img.Pix[src:], row)
}
return img, nil
}
func contextDo(ctx context, f func() error) error {
errCh := make(chan error)
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := ctx.MakeCurrent(); err != nil {
errCh <- err
return
}
defer ctx.ReleaseCurrent()
errCh <- f()
}()
return <-errCh
}