mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 15:45:38 +00:00
adb950cbf3
Signed-off-by: Elias Naur <mail@eliasnaur.com>
150 lines
3.0 KiB
Go
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
|
|
}
|