forked from joejulian/gio
app/headless: implement headless windows
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package headless
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"gioui.org/app/internal/gl"
|
||||||
|
"gioui.org/app/internal/gpu"
|
||||||
|
"gioui.org/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Window is a headless window.
|
||||||
|
type Window struct {
|
||||||
|
size image.Point
|
||||||
|
ctx context
|
||||||
|
fbo *gl.SRGBFBO
|
||||||
|
gpu *gpu.GPU
|
||||||
|
}
|
||||||
|
|
||||||
|
type context interface {
|
||||||
|
Functions() *gl.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 := gl.NewSRGBFBO(f)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Release()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := fbo.Refresh(width, height); err != nil {
|
||||||
|
fbo.Release()
|
||||||
|
ctx.Release()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gpu, err := gpu.New(f)
|
||||||
|
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(false, w.size, frame)
|
||||||
|
w.gpu.Frame(false, w.size)
|
||||||
|
w.gpu.EndFrame(false)
|
||||||
|
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
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package headless
|
||||||
|
|
||||||
|
import "gioui.org/app/internal/gl"
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c
|
||||||
|
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
#include "headless_darwin.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
type nsContext struct {
|
||||||
|
c *gl.Functions
|
||||||
|
ctx C.CFTypeRef
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContext() (context, error) {
|
||||||
|
ctx := C.gio_headless_newContext()
|
||||||
|
return &nsContext{ctx: ctx, c: new(gl.Functions)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nsContext) MakeCurrent() error {
|
||||||
|
C.gio_headless_makeCurrentContext(c.ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nsContext) ReleaseCurrent() {
|
||||||
|
C.gio_headless_clearCurrentContext(c.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nsContext) Functions() *gl.Functions {
|
||||||
|
return c.c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *nsContext) Release() {
|
||||||
|
if d.ctx != 0 {
|
||||||
|
C.gio_headless_releaseContext(d.ctx)
|
||||||
|
d.ctx = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_headless_newContext(void);
|
||||||
|
__attribute__ ((visibility ("hidden"))) void gio_headless_releaseContext(CFTypeRef ctxRef);
|
||||||
|
__attribute__ ((visibility ("hidden"))) void gio_headless_clearCurrentContext(CFTypeRef ctxRef);
|
||||||
|
__attribute__ ((visibility ("hidden"))) void gio_headless_makeCurrentContext(CFTypeRef ctxRef);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
// +build linux freebsd windows
|
||||||
|
|
||||||
|
package headless
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gioui.org/app/internal/egl"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newContext() (context, error) {
|
||||||
|
return egl.NewContext(egl.EGL_DEFAULT_DISPLAY)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
// +build darwin,ios
|
||||||
|
|
||||||
|
@import OpenGLES;
|
||||||
|
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
#include "headless_darwin.h"
|
||||||
|
|
||||||
|
void gio_headless_releaseContext(CFTypeRef ctxRef) {
|
||||||
|
CFBridgingRelease(ctxRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
CFTypeRef gio_headless_newContext(void) {
|
||||||
|
EAGLContext *ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
|
||||||
|
if (ctx == nil) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
return CFBridgingRetain(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void gio_headless_clearCurrentContext(CFTypeRef ctxRef) {
|
||||||
|
[EAGLContext setCurrentContext:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
void gio_headless_makeCurrentContext(CFTypeRef ctxRef) {
|
||||||
|
EAGLContext *ctx = (__bridge EAGLContext *)ctxRef;
|
||||||
|
[EAGLContext setCurrentContext:ctx];
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package headless
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"syscall/js"
|
||||||
|
|
||||||
|
"gioui.org/app/internal/gl"
|
||||||
|
)
|
||||||
|
|
||||||
|
type jsContext struct {
|
||||||
|
ctx js.Value
|
||||||
|
f *gl.Functions
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContext() (*jsContext, error) {
|
||||||
|
version := 2
|
||||||
|
doc := js.Global().Get("document")
|
||||||
|
cnv := doc.Call("createElement", "canvas")
|
||||||
|
ctx := cnv.Call("getContext", "webgl2")
|
||||||
|
if ctx.IsNull() {
|
||||||
|
version = 1
|
||||||
|
ctx = cnv.Call("getContext", "webgl")
|
||||||
|
}
|
||||||
|
if ctx.IsNull() {
|
||||||
|
return nil, errors.New("headless: webgl is not supported")
|
||||||
|
}
|
||||||
|
f := &gl.Functions{Ctx: ctx}
|
||||||
|
if err := f.Init(version); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c := &jsContext{
|
||||||
|
ctx: ctx,
|
||||||
|
f: f,
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *jsContext) Functions() *gl.Functions {
|
||||||
|
return c.f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *jsContext) Release() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *jsContext) ReleaseCurrent() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *jsContext) MakeCurrent() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
// +build darwin,!ios
|
||||||
|
|
||||||
|
@import AppKit;
|
||||||
|
@import OpenGL;
|
||||||
|
@import OpenGL.GL;
|
||||||
|
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
#include "headless_darwin.h"
|
||||||
|
|
||||||
|
void gio_headless_releaseContext(CFTypeRef ctxRef) {
|
||||||
|
CFBridgingRelease(ctxRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
CFTypeRef gio_headless_newContext(void) {
|
||||||
|
NSOpenGLPixelFormatAttribute attr[] = {
|
||||||
|
NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
|
||||||
|
NSOpenGLPFAColorSize, 24,
|
||||||
|
NSOpenGLPFAAccelerated,
|
||||||
|
// Opt-in to automatic GPU switching. CGL-only property.
|
||||||
|
kCGLPFASupportsAutomaticGraphicsSwitching,
|
||||||
|
NSOpenGLPFAAllowOfflineRenderers,
|
||||||
|
0
|
||||||
|
};
|
||||||
|
NSOpenGLPixelFormat *pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];
|
||||||
|
if (pixFormat == nil) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
NSOpenGLContext *ctx = [[NSOpenGLContext alloc] initWithFormat:pixFormat shareContext:nil];
|
||||||
|
return CFBridgingRetain(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void gio_headless_clearCurrentContext(CFTypeRef ctxRef) {
|
||||||
|
NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef;
|
||||||
|
CGLUnlockContext([ctx CGLContextObj]);
|
||||||
|
[NSOpenGLContext clearCurrentContext];
|
||||||
|
}
|
||||||
|
|
||||||
|
void gio_headless_makeCurrentContext(CFTypeRef ctxRef) {
|
||||||
|
NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef;
|
||||||
|
[ctx makeCurrentContext];
|
||||||
|
glEnable(GL_FRAMEBUFFER_SRGB);
|
||||||
|
CGLLockContext([ctx CGLContextObj]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package headless
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gioui.org/f32"
|
||||||
|
"gioui.org/op"
|
||||||
|
"gioui.org/op/paint"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHeadless(t *testing.T) {
|
||||||
|
sz := image.Point{X: 800, Y: 600}
|
||||||
|
w, err := NewWindow(sz.X, sz.Y)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("headless windows not supported: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
col := color.RGBA{A: 0xff, R: 0xcc, G: 0xcc}
|
||||||
|
var ops op.Ops
|
||||||
|
paint.ColorOp{Color: col}.Add(&ops)
|
||||||
|
paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{
|
||||||
|
X: float32(sz.X),
|
||||||
|
Y: float32(sz.Y),
|
||||||
|
}}}.Add(&ops)
|
||||||
|
w.Frame(&ops)
|
||||||
|
|
||||||
|
img, err := w.Screenshot()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if isz := img.Bounds().Size(); isz != sz {
|
||||||
|
t.Errorf("got %v screenshot, expected %v", isz, sz)
|
||||||
|
}
|
||||||
|
if got := img.RGBAAt(0, 0); got != col {
|
||||||
|
t.Errorf("got color %v, expected %v", got, col)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ var (
|
|||||||
nilEGLContext _EGLContext
|
nilEGLContext _EGLContext
|
||||||
nilEGLConfig _EGLConfig
|
nilEGLConfig _EGLConfig
|
||||||
nilEGLNativeWindowType NativeWindowType
|
nilEGLNativeWindowType NativeWindowType
|
||||||
|
EGL_DEFAULT_DISPLAY NativeDisplayType
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -71,6 +72,7 @@ func (c *Context) Release() {
|
|||||||
eglReleaseThread()
|
eglReleaseThread()
|
||||||
c.eglCtx = nil
|
c.eglCtx = nil
|
||||||
}
|
}
|
||||||
|
c.disp = nilEGLDisplay
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) Present() error {
|
func (c *Context) Present() error {
|
||||||
@@ -116,7 +118,7 @@ func (c *Context) ReleaseSurface() {
|
|||||||
}
|
}
|
||||||
// Make sure any in-flight GL commands are complete.
|
// Make sure any in-flight GL commands are complete.
|
||||||
c.c.Finish()
|
c.c.Finish()
|
||||||
eglMakeCurrent(c.disp, nilEGLSurface, nilEGLSurface, nilEGLContext)
|
c.ReleaseCurrent()
|
||||||
eglDestroySurface(c.disp, c.eglSurf)
|
eglDestroySurface(c.disp, c.eglSurf)
|
||||||
c.eglSurf = nilEGLSurface
|
c.eglSurf = nilEGLSurface
|
||||||
}
|
}
|
||||||
@@ -134,6 +136,12 @@ func (c *Context) CreateSurface(win NativeWindowType, width, height int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) ReleaseCurrent() {
|
||||||
|
if c.disp != nilEGLDisplay {
|
||||||
|
eglMakeCurrent(c.disp, nilEGLSurface, nilEGLSurface, nilEGLContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Context) MakeCurrent() error {
|
func (c *Context) MakeCurrent() error {
|
||||||
if c.eglSurf == nilEGLSurface && !c.eglCtx.surfaceless {
|
if c.eglSurf == nilEGLSurface && !c.eglCtx.surfaceless {
|
||||||
return errors.New("no surface created yet EGL_KHR_surfaceless_context is not supported")
|
return errors.New("no surface created yet EGL_KHR_surfaceless_context is not supported")
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const (
|
|||||||
LUMINANCE = 0x1909
|
LUMINANCE = 0x1909
|
||||||
MAX_TEXTURE_SIZE = 0xd33
|
MAX_TEXTURE_SIZE = 0xd33
|
||||||
NEAREST = 0x2600
|
NEAREST = 0x2600
|
||||||
|
NO_ERROR = 0x0
|
||||||
ONE = 0x1
|
ONE = 0x1
|
||||||
ONE_MINUS_SRC_ALPHA = 0x303
|
ONE_MINUS_SRC_ALPHA = 0x303
|
||||||
QUERY_RESULT = 0x8866
|
QUERY_RESULT = 0x8866
|
||||||
|
|||||||
Reference in New Issue
Block a user