Files
gio/app/headless/headless.go
T
Elias Naur be04dfae83 app/headless: bind output framebuffer at render, not creation
Bind the framebuffer as late as possible to lessen the risk of
confusing global state (current framebuffer). Commit
25a19481e3 removed the assumption
that the framebuffer current at gpu.New would always be the output
framebuffer

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2021-03-04 14:08:39 +01:00

148 lines
2.8 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
// Package headless implements headless windows for rendering
// an operation list to an image.
package headless
import (
"image"
"image/color"
"runtime"
"gioui.org/gpu"
"gioui.org/gpu/backend"
"gioui.org/op"
)
// Window is a headless window.
type Window struct {
size image.Point
ctx context
backend backend.Device
gpu gpu.GPU
fboTex backend.Texture
fbo backend.Framebuffer
}
type context interface {
Backend() (backend.Device, error)
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 {
dev, err := ctx.Backend()
if err != nil {
return err
}
dev.Viewport(0, 0, width, height)
fboTex, err := dev.NewTexture(
backend.TextureFormatSRGB,
width, height,
backend.FilterNearest, backend.FilterNearest,
backend.BufferBindingFramebuffer,
)
if err != nil {
return nil
}
const depthBits = 16
fbo, err := dev.NewFramebuffer(fboTex, depthBits)
if err != nil {
fboTex.Release()
return err
}
gp, err := gpu.New(dev)
if err != nil {
fbo.Release()
fboTex.Release()
return err
}
w.fboTex = fboTex
w.fbo = fbo
w.gpu = gp
w.backend = dev
return err
})
if err != nil {
ctx.Release()
return nil, err
}
return w, nil
}
// Release resources associated with the window.
func (w *Window) Release() {
contextDo(w.ctx, func() error {
if w.fbo != nil {
w.fbo.Release()
w.fbo = nil
}
if w.fboTex != nil {
w.fboTex.Release()
w.fboTex = nil
}
if w.gpu != nil {
w.gpu.Release()
w.gpu = nil
}
return nil
})
if w.ctx != nil {
w.ctx.Release()
w.ctx = nil
}
}
// Frame replace the window content and state with the
// operation list.
func (w *Window) Frame(frame *op.Ops) error {
return contextDo(w.ctx, func() error {
w.backend.BindFramebuffer(w.fbo)
w.gpu.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
w.gpu.Collect(w.size, frame)
return w.gpu.Frame()
})
}
// 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})
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)
})
if err != nil {
return nil, err
}
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
}
err := f()
ctx.ReleaseCurrent()
errCh <- err
}()
return <-errCh
}