Files
gio-cmd/gogio/internal/custom/testdata.go
T
Chris Waldon ecebd405a7 gogio: implement custom rendering test
This commit adds an end to end test for the custom rendering
use-case. I confirmed that the new test failed when custom
rendering frame lifecycle was broken, and succeeds now.

However, the old X11 tests started failing when the new
one started passing. I'm not sure how they interfere with
one another, but I'm out of time to investigate.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-06-28 21:57:19 +02:00

368 lines
9.0 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
//go:build linux
// +build linux
// This program demonstrates the use of a custom OpenGL ES context with
// app.Window.
package main
import (
"errors"
"fmt"
"image"
"image/color"
"log"
"os"
"runtime"
"strings"
"unsafe"
"gioui.org/app"
"gioui.org/gpu"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
)
/*
#cgo linux pkg-config: egl wayland-egl
#cgo freebsd openbsd CFLAGS: -I/usr/local/include
#cgo openbsd CFLAGS: -I/usr/X11R6/include
#cgo freebsd LDFLAGS: -L/usr/local/lib
#cgo openbsd LDFLAGS: -L/usr/X11R6/lib
#cgo freebsd openbsd LDFLAGS: -lwayland-egl
#cgo CFLAGS: -DEGL_NO_X11
#cgo LDFLAGS: -lEGL -lGLESv2
#include <EGL/egl.h>
#include <wayland-client.h>
#include <wayland-egl.h>
#include <GLES3/gl3.h>
#define EGL_EGLEXT_PROTOTYPES
#include <EGL/eglext.h>
*/
import "C"
func getDisplay(ve app.ViewEvent) C.EGLDisplay {
switch ve := ve.(type) {
case app.X11ViewEvent:
return C.eglGetDisplay(C.EGLNativeDisplayType(ve.Display))
case app.WaylandViewEvent:
return C.eglGetDisplay(C.EGLNativeDisplayType(ve.Display))
}
panic("no display available")
}
func nativeViewFor(e app.ViewEvent, size image.Point) (C.EGLNativeWindowType, func()) {
switch e := e.(type) {
case app.X11ViewEvent:
return C.EGLNativeWindowType(uintptr(e.Window)), func() {}
case app.WaylandViewEvent:
eglWin := C.wl_egl_window_create((*C.struct_wl_surface)(e.Surface), C.int(size.X), C.int(size.Y))
return C.EGLNativeWindowType(uintptr(unsafe.Pointer(eglWin))), func() {
C.wl_egl_window_destroy(eglWin)
}
}
panic("no native view available")
}
type (
C = layout.Context
D = layout.Dimensions
)
type notifyFrame int
const (
notifyNone notifyFrame = iota
notifyInvalidate
notifyPrint
)
// notify keeps track of whether we want to print to stdout to notify the user
// when a frame is ready. Initially we want to notify about the first frame.
var notify = notifyInvalidate
type eglContext struct {
disp C.EGLDisplay
ctx C.EGLContext
surf C.EGLSurface
cleanup func()
}
func main() {
go func() {
// Set CustomRenderer so we can provide our own rendering context.
w := app.NewWindow(app.CustomRenderer(true))
if err := loop(w); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main()
}
func loop(w *app.Window) error {
var ops op.Ops
var (
ctx *eglContext
gioCtx gpu.GPU
ve app.ViewEvent
init bool
size image.Point
)
recreateContext := func() {
w.Run(func() {
if gioCtx != nil {
gioCtx.Release()
gioCtx = nil
}
if ctx != nil {
C.eglMakeCurrent(ctx.disp, nil, nil, nil)
ctx.Release()
ctx = nil
}
c, err := createContext(ve, size)
if err != nil {
log.Fatal(err)
}
ctx = c
})
if ok := C.eglMakeCurrent(ctx.disp, ctx.surf, ctx.surf, ctx.ctx); ok != C.EGL_TRUE {
err := fmt.Errorf("eglMakeCurrent failed (%#x)", C.eglGetError())
log.Fatal(err)
}
glGetString := func(e C.GLenum) string {
return C.GoString((*C.char)(unsafe.Pointer(C.glGetString(e))))
}
fmt.Printf("GL_VERSION: %s\nGL_RENDERER: %s\n", glGetString(C.GL_VERSION), glGetString(C.GL_RENDERER))
var err error
gioCtx, err = gpu.New(gpu.OpenGL{ES: true, Shared: true})
if err != nil {
log.Fatal(err)
}
}
topLeft := quarterWidget{
color: color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff},
}
topRight := quarterWidget{
color: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff},
}
botLeft := quarterWidget{
color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff},
}
botRight := quarterWidget{
color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x80},
}
// eglMakeCurrent binds a context to an operating system thread. Prevent Go from switching thread.
runtime.LockOSThread()
for e := range w.Events() {
switch e := e.(type) {
case app.ViewEvent:
ve = e
init = true
if size != (image.Point{}) {
recreateContext()
}
case system.DestroyEvent:
return e.Err
case system.FrameEvent:
if init && size != e.Size {
size = e.Size
recreateContext()
}
if gioCtx == nil || !init {
break
}
// Build ops.
gtx := layout.NewContext(&ops, e)
// Clear background to white, even on embedded platforms such as webassembly.
paint.Fill(gtx.Ops, color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
// r1c1
layout.Flexed(1, func(gtx C) D { return topLeft.Layout(gtx) }),
// r1c2
layout.Flexed(1, func(gtx C) D { return topRight.Layout(gtx) }),
)
}),
layout.Flexed(1, func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
// r2c1
layout.Flexed(1, func(gtx C) D { return botLeft.Layout(gtx) }),
// r2c2
layout.Flexed(1, func(gtx C) D { return botRight.Layout(gtx) }),
)
}),
)
op.InvalidateOp{}.Add(gtx.Ops)
log.Println("frame")
// Trigger window resize detection in ANGLE.
C.eglWaitClient()
// Draw custom OpenGL content.
drawGL()
// Render drawing ops.
if err := gioCtx.Frame(gtx.Ops, gpu.OpenGLRenderTarget{}, e.Size); err != nil {
log.Fatal(fmt.Errorf("render failed: %v", err))
}
// Process non-drawing ops.
e.Frame(gtx.Ops)
switch notify {
case notifyInvalidate:
notify = notifyPrint
w.Invalidate()
case notifyPrint:
notify = notifyNone
fmt.Println("gio frame ready")
}
if ok := C.eglSwapBuffers(ctx.disp, ctx.surf); ok != C.EGL_TRUE {
log.Fatal(fmt.Errorf("swap failed: %v", C.eglGetError()))
}
}
}
return nil
}
func drawGL() {
C.glClearColor(0, 0, 0, 1)
C.glClear(C.GL_COLOR_BUFFER_BIT | C.GL_DEPTH_BUFFER_BIT)
}
func createContext(ve app.ViewEvent, size image.Point) (*eglContext, error) {
view, cleanup := nativeViewFor(ve, size)
var nilv C.EGLNativeWindowType
if view == nilv {
return nil, fmt.Errorf("failed creating native view")
}
disp := getDisplay(ve)
if disp == 0 {
return nil, fmt.Errorf("eglGetPlatformDisplay failed: 0x%x", C.eglGetError())
}
var major, minor C.EGLint
if ok := C.eglInitialize(disp, &major, &minor); ok != C.EGL_TRUE {
return nil, fmt.Errorf("eglInitialize failed: 0x%x", C.eglGetError())
}
exts := strings.Split(C.GoString(C.eglQueryString(disp, C.EGL_EXTENSIONS)), " ")
srgb := hasExtension(exts, "EGL_KHR_gl_colorspace")
attribs := []C.EGLint{
C.EGL_RENDERABLE_TYPE, C.EGL_OPENGL_ES2_BIT,
C.EGL_SURFACE_TYPE, C.EGL_WINDOW_BIT,
C.EGL_BLUE_SIZE, 8,
C.EGL_GREEN_SIZE, 8,
C.EGL_RED_SIZE, 8,
C.EGL_CONFIG_CAVEAT, C.EGL_NONE,
}
if srgb {
// Some drivers need alpha for sRGB framebuffers to work.
attribs = append(attribs, C.EGL_ALPHA_SIZE, 8)
}
attribs = append(attribs, C.EGL_NONE)
var (
cfg C.EGLConfig
numCfgs C.EGLint
)
if ok := C.eglChooseConfig(disp, &attribs[0], &cfg, 1, &numCfgs); ok != C.EGL_TRUE {
return nil, fmt.Errorf("eglChooseConfig failed: 0x%x", C.eglGetError())
}
if numCfgs == 0 {
supportsNoCfg := hasExtension(exts, "EGL_KHR_no_config_context")
if !supportsNoCfg {
return nil, errors.New("eglChooseConfig returned no configs")
}
}
ctxAttribs := []C.EGLint{
C.EGL_CONTEXT_CLIENT_VERSION, 3,
C.EGL_NONE,
}
ctx := C.eglCreateContext(disp, cfg, nil, &ctxAttribs[0])
if ctx == nil {
return nil, fmt.Errorf("eglCreateContext failed: 0x%x", C.eglGetError())
}
var surfAttribs []C.EGLint
if srgb {
surfAttribs = append(surfAttribs, C.EGL_GL_COLORSPACE, C.EGL_GL_COLORSPACE_SRGB)
}
surfAttribs = append(surfAttribs, C.EGL_NONE)
surf := C.eglCreateWindowSurface(disp, cfg, view, &surfAttribs[0])
if surf == nil {
return nil, fmt.Errorf("eglCreateWindowSurface failed (0x%x)", C.eglGetError())
}
return &eglContext{disp: disp, ctx: ctx, surf: surf, cleanup: cleanup}, nil
}
func (c *eglContext) Release() {
if c.ctx != nil {
C.eglDestroyContext(c.disp, c.ctx)
}
if c.surf != nil {
C.eglDestroySurface(c.disp, c.surf)
}
if c.cleanup != nil {
c.cleanup()
}
*c = eglContext{}
}
func hasExtension(exts []string, ext string) bool {
for _, e := range exts {
if ext == e {
return true
}
}
return false
}
// quarterWidget paints a quarter of the screen with one color. When clicked, it
// turns red, going back to its normal color when clicked again.
type quarterWidget struct {
color color.NRGBA
clicked bool
}
var red = color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
func (w *quarterWidget) Layout(gtx layout.Context) layout.Dimensions {
var color color.NRGBA
if w.clicked {
color = red
} else {
color = w.color
}
r := image.Rectangle{Max: gtx.Constraints.Max}
paint.FillShape(gtx.Ops, color, clip.Rect(r).Op())
defer clip.Rect(image.Rectangle{
Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y),
}).Push(gtx.Ops).Pop()
pointer.InputOp{
Tag: w,
Types: pointer.Press,
}.Add(gtx.Ops)
for _, e := range gtx.Events(w) {
if e, ok := e.(pointer.Event); ok && e.Type == pointer.Press {
w.clicked = !w.clicked
// notify when we're done updating the frame.
notify = notifyInvalidate
}
}
return layout.Dimensions{Size: gtx.Constraints.Max}
}