mirror of
https://git.sr.ht/~eliasnaur/gio-cmd
synced 2026-07-01 15:45:36 +00:00
03a1ada8ac
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
372 lines
9.1 KiB
Go
372 lines
9.1 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/event"
|
|
"gioui.org/io/pointer"
|
|
"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 := new(app.Window)
|
|
w.Option(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 {
|
|
switch e := w.Event().(type) {
|
|
case app.ViewEvent:
|
|
ve = e
|
|
init = true
|
|
if size != (image.Point{}) {
|
|
recreateContext()
|
|
}
|
|
case app.DestroyEvent:
|
|
return e.Err
|
|
case app.FrameEvent:
|
|
if init && size != e.Size {
|
|
size = e.Size
|
|
recreateContext()
|
|
}
|
|
if gioCtx == nil || !init {
|
|
break
|
|
}
|
|
// Build ops.
|
|
gtx := app.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) }),
|
|
)
|
|
}),
|
|
)
|
|
gtx.Execute(op.InvalidateCmd{})
|
|
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()
|
|
event.Op(gtx.Ops, w)
|
|
for {
|
|
e, ok := gtx.Event(pointer.Filter{
|
|
Target: w,
|
|
Kinds: pointer.Press,
|
|
})
|
|
if !ok {
|
|
break
|
|
}
|
|
if e, ok := e.(pointer.Event); ok && e.Kind == pointer.Press {
|
|
w.clicked = !w.clicked
|
|
// notify when we're done updating the frame.
|
|
notify = notifyInvalidate
|
|
}
|
|
}
|
|
return layout.Dimensions{Size: gtx.Constraints.Max}
|
|
}
|