mirror of
https://git.sr.ht/~eliasnaur/gio-cmd
synced 2026-07-01 07:35:37 +00:00
ecebd405a7
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>
368 lines
9.0 KiB
Go
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}
|
|
}
|