You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

288 lines
7.6 KiB

// SPDX-License-Identifier: Unlicense OR MIT
//go:build darwin || windows || linux
// +build darwin windows linux
// This program demonstrates the use of a custom OpenGL ES context with
// app.Window. It is similar to the GLFW example, but uses Gio's window
// implementation instead of the one in GLFW.
//
// The example runs on Linux using the normal EGL and X11 libraries, so
// no additional libraries need to be installed.
//
// The example runs on macOS and Windows using ANGLE:
//
// $ CGO_CFLAGS=-I<path-to-ANGLE>/include CGO_LDFLAGS=-L<path-to-angle-libraries> go build -o opengl.exe ./opengl
//
// You'll need the ANGLE libraries (EGL and GLESv2) in your library search path. On macOS:
//
// $ DYLD_LIBRARY_PATH=<path-to-ANGLE-libraries> ./opengl.exe
//
package main
import (
"bytes"
"errors"
"fmt"
"image"
"image/png"
"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/widget"
"gioui.org/widget/material"
"gioui.org/font/gofont"
)
/*
#cgo CFLAGS: -DEGL_NO_X11
#cgo LDFLAGS: -lEGL -lGLESv2
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#define EGL_EGLEXT_PROTOTYPES
#include <EGL/eglext.h>
*/
import "C"
type eglContext struct {
disp C.EGLDisplay
ctx C.EGLContext
surf C.EGLSurface
}
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()
}
var btnScreenshot widget.Clickable
func loop(w *app.Window) error {
th := material.NewTheme(gofont.Collection())
var ops op.Ops
var (
ctx *eglContext
gioCtx gpu.GPU
)
for e := range w.Events() {
switch e := e.(type) {
case app.ViewEvent:
w.Run(func() {
if gioCtx != nil {
gioCtx.Release()
gioCtx = nil
}
if ctx != nil {
C.eglMakeCurrent(ctx.disp, nil, nil, nil)
ctx.Release()
ctx = nil
}
view := nativeViewFor(e)
var nilv C.EGLNativeWindowType
if view == nilv {
return
}
c, err := createContext(e)
if err != nil {
log.Fatal(err)
}
ctx = c
// eglMakeCurrent binds a context to an operating system thread. Prevent Go from switching thread.
runtime.LockOSThread()
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))
gioCtx, err = gpu.New(gpu.OpenGL{ES: true, Shared: true})
if err != nil {
log.Fatal(err)
}
})
case system.DestroyEvent:
return e.Err
case system.FrameEvent:
if gioCtx == nil {
break
}
// Build ops.
gtx := layout.NewContext(&ops, e)
// Catch pointer events not hitting UI.
types := pointer.Move | pointer.Press | pointer.Release
pointer.InputOp{Tag: w, Types: types}.Add(gtx.Ops)
for _, e := range gtx.Events(w) {
log.Println("Event:", e)
}
drawUI(th, gtx)
w.Run(func() {
// 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))
}
if ok := C.eglSwapBuffers(ctx.disp, ctx.surf); ok != C.EGL_TRUE {
log.Fatal(fmt.Errorf("swap failed: %v", C.eglGetError()))
}
if btnScreenshot.Clicked() {
if err := screenshot(gioCtx, e.Size, gtx.Ops); err != nil {
log.Fatal(err)
}
}
})
// Process non-drawing ops.
e.Frame(gtx.Ops)
}
}
return nil
}
func screenshot(ctx gpu.GPU, size image.Point, ops *op.Ops) error {
var tex C.GLuint
C.glGenTextures(1, &tex)
defer C.glDeleteTextures(1, &tex)
C.glBindTexture(C.GL_TEXTURE_2D, tex)
C.glTexImage2D(C.GL_TEXTURE_2D, 0, C.GL_RGBA, C.GLint(size.X), C.GLint(size.Y), 0, C.GL_RGBA, C.GL_UNSIGNED_BYTE, nil)
var fbo C.GLuint
C.glGenFramebuffers(1, &fbo)
defer C.glDeleteFramebuffers(1, &fbo)
C.glBindFramebuffer(C.GL_FRAMEBUFFER, fbo)
defer C.glBindFramebuffer(C.GL_FRAMEBUFFER, 0)
C.glFramebufferTexture2D(C.GL_FRAMEBUFFER, C.GL_COLOR_ATTACHMENT0, C.GL_TEXTURE_2D, tex, 0)
if st := C.glCheckFramebufferStatus(C.GL_FRAMEBUFFER); st != C.GL_FRAMEBUFFER_COMPLETE {
return fmt.Errorf("screenshot: framebuffer incomplete (%#x)", st)
}
drawGL()
if err := ctx.Frame(ops, gpu.OpenGLRenderTarget{V: uint(fbo)}, size); err != nil {
return fmt.Errorf("screenshot: %w", err)
}
r := image.Rectangle{Max: size}
ss := image.NewRGBA(r)
C.glReadPixels(C.GLint(r.Min.X), C.GLint(r.Min.Y), C.GLint(r.Dx()), C.GLint(r.Dy()), C.GL_RGBA, C.GL_UNSIGNED_BYTE, unsafe.Pointer(&ss.Pix[0]))
var buf bytes.Buffer
if err := png.Encode(&buf, ss); err != nil {
return fmt.Errorf("screenshot: %w", err)
}
const file = "screenshot.png"
if err := os.WriteFile(file, buf.Bytes(), 0644); err != nil {
return fmt.Errorf("screenshot: %w", err)
}
fmt.Printf("wrote %q\n", file)
return nil
}
func drawGL() {
C.glClearColor(.5, .5, 0, 1)
C.glClear(C.GL_COLOR_BUFFER_BIT | C.GL_DEPTH_BUFFER_BIT)
}
func drawUI(th *material.Theme, gtx layout.Context) layout.Dimensions {
return layout.Center.Layout(gtx,
material.Button(th, &btnScreenshot, "Screenshot").Layout,
)
}
func createContext(ve app.ViewEvent) (*eglContext, error) {
view := nativeViewFor(ve)
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}, 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)
}
*c = eglContext{}
}
func hasExtension(exts []string, ext string) bool {
for _, e := range exts {
if ext == e {
return true
}
}
return false
}