mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 15:45:38 +00:00
be04dfae83
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>
148 lines
2.8 KiB
Go
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
|
|
}
|