blob: 28ef1044c988bfa8a5097e483c9733c84dd45922 [file] [log] [blame]
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux,!android
package gldriver
/*
#cgo LDFLAGS: -lEGL -lGLESv2 -lX11
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
char *eglGetErrorStr();
void startDriver();
void processEvents();
void makeCurrent(uintptr_t ctx);
void swapBuffers(uintptr_t ctx);
void doCloseWindow(uintptr_t id);
uintptr_t doNewWindow(int width, int height, char* title, int title_len);
uintptr_t doShowWindow(uintptr_t id);
uintptr_t surfaceCreate();
*/
import "C"
import (
"errors"
"runtime"
"time"
"unsafe"
"golang.org/x/exp/shiny/driver/internal/x11key"
"golang.org/x/exp/shiny/screen"
"golang.org/x/mobile/event/key"
"golang.org/x/mobile/event/mouse"
"golang.org/x/mobile/event/paint"
"golang.org/x/mobile/event/size"
"golang.org/x/mobile/geom"
"golang.org/x/mobile/gl"
)
const useLifecycler = true
var theKeysyms x11key.KeysymTable
func init() {
// It might not be necessary, but it probably doesn't hurt to try to make
// 'the main thread' be 'the X11 / OpenGL thread'.
runtime.LockOSThread()
}
func newWindow(opts *screen.NewWindowOptions) (uintptr, error) {
width, height := optsSize(opts)
title := opts.GetTitle()
ctitle := C.CString(title)
defer C.free(unsafe.Pointer(ctitle))
retc := make(chan uintptr)
uic <- uiClosure{
f: func() uintptr {
return uintptr(C.doNewWindow(C.int(width), C.int(height), ctitle, C.int(len(title))))
},
retc: retc,
}
return <-retc, nil
}
func initWindow(w *windowImpl) {
w.glctx, w.worker = glctx, worker
}
func showWindow(w *windowImpl) {
retc := make(chan uintptr)
uic <- uiClosure{
f: func() uintptr {
return uintptr(C.doShowWindow(C.uintptr_t(w.id)))
},
retc: retc,
}
w.ctx = <-retc
go drawLoop(w)
}
func closeWindow(id uintptr) {
uic <- uiClosure{
f: func() uintptr {
C.doCloseWindow(C.uintptr_t(id))
return 0
},
}
}
func drawLoop(w *windowImpl) {
glcontextc <- w.ctx.(uintptr)
go func() {
for range w.publish {
publishc <- w
}
}()
}
var (
glcontextc = make(chan uintptr)
publishc = make(chan *windowImpl)
uic = make(chan uiClosure)
// TODO: don't assume that there is only one window, and hence only
// one (global) GL context.
//
// TODO: should we be able to make a shiny.Texture before having a
// shiny.Window's GL context? Should something like gl.IsProgram be a
// method instead of a function, and have each shiny.Window have its own
// gl.Context?
glctx gl.Context
worker gl.Worker
)
// uiClosure is a closure to be run on C's UI thread.
type uiClosure struct {
f func() uintptr
retc chan uintptr
}
func main(f func(screen.Screen)) error {
if gl.Version() == "GL_ES_2_0" {
return errors.New("gldriver: ES 3 required on X11")
}
C.startDriver()
glctx, worker = gl.NewContext()
closec := make(chan struct{})
go func() {
f(theScreen)
close(closec)
}()
// heartbeat is a channel that, at regular intervals, directs the select
// below to also consider X11 events, not just Go events (channel
// communications).
//
// TODO: select instead of poll. Note that knowing whether to call
// C.processEvents needs to select on a file descriptor, and the other
// cases below select on Go channels.
heartbeat := time.NewTicker(time.Second / 60)
workAvailable := worker.WorkAvailable()
for {
select {
case <-closec:
return nil
case ctx := <-glcontextc:
// TODO: do we need to synchronize with seeing a size event for
// this window's context before or after calling makeCurrent?
// Otherwise, are we racing with the gl.Viewport call? I've
// occasionally seen a stale viewport, if the window manager sets
// the window width and height to something other than that
// requested by XCreateWindow, but it's not easily reproducible.
C.makeCurrent(C.uintptr_t(ctx))
case w := <-publishc:
C.swapBuffers(C.uintptr_t(w.ctx.(uintptr)))
w.publishDone <- screen.PublishResult{}
case req := <-uic:
ret := req.f()
if req.retc != nil {
req.retc <- ret
}
case <-heartbeat.C:
C.processEvents()
case <-workAvailable:
worker.DoWork()
}
}
}
//export onExpose
func onExpose(id uintptr) {
theScreen.mu.Lock()
w := theScreen.windows[id]
theScreen.mu.Unlock()
if w == nil {
return
}
w.Send(paint.Event{External: true})
}
//export onKeysym
func onKeysym(k, unshifted, shifted uint32) {
theKeysyms[k][0] = unshifted
theKeysyms[k][1] = shifted
}
//export onKey
func onKey(id uintptr, state uint16, detail, dir uint8) {
theScreen.mu.Lock()
w := theScreen.windows[id]
theScreen.mu.Unlock()
if w == nil {
return
}
r, c := theKeysyms.Lookup(detail, state)
w.Send(key.Event{
Rune: r,
Code: c,
Modifiers: x11key.KeyModifiers(state),
Direction: key.Direction(dir),
})
}
//export onMouse
func onMouse(id uintptr, x, y int32, state uint16, button, dir uint8) {
theScreen.mu.Lock()
w := theScreen.windows[id]
theScreen.mu.Unlock()
if w == nil {
return
}
// TODO: should a mouse.Event have a separate MouseModifiers field, for
// which buttons are pressed during a mouse move?
btn := mouse.Button(button)
switch btn {
case 4:
btn = mouse.ButtonWheelUp
case 5:
btn = mouse.ButtonWheelDown
case 6:
btn = mouse.ButtonWheelLeft
case 7:
btn = mouse.ButtonWheelRight
}
if btn.IsWheel() {
if dir != uint8(mouse.DirPress) {
return
}
dir = uint8(mouse.DirStep)
}
w.Send(mouse.Event{
X: float32(x),
Y: float32(y),
Button: btn,
Modifiers: x11key.KeyModifiers(state),
Direction: mouse.Direction(dir),
})
}
//export onFocus
func onFocus(id uintptr, focused bool) {
theScreen.mu.Lock()
w := theScreen.windows[id]
theScreen.mu.Unlock()
if w == nil {
return
}
w.lifecycler.SetFocused(focused)
w.lifecycler.SendEvent(w, w.glctx)
}
//export onConfigure
func onConfigure(id uintptr, x, y, width, height, displayWidth, displayWidthMM int32) {
theScreen.mu.Lock()
w := theScreen.windows[id]
theScreen.mu.Unlock()
if w == nil {
return
}
// TODO: should this really be done on the receiving end of the w.Events()
// channel, in the same goroutine as other GL calls in the app's 'business
// logic'?
go func() {
w.glctxMu.Lock()
// Force a w.glctx.Viewport call.
//
// TODO: is this racy? See also the TODO immediately above.
w.backBufferBound = false
w.glctxMu.Unlock()
}()
w.lifecycler.SetVisible(x+width > 0 && y+height > 0)
w.lifecycler.SendEvent(w, w.glctx)
const (
mmPerInch = 25.4
ptPerInch = 72
)
pixelsPerMM := float32(displayWidth) / float32(displayWidthMM)
sz := size.Event{
WidthPx: int(width),
HeightPx: int(height),
WidthPt: geom.Pt(width),
HeightPt: geom.Pt(height),
PixelsPerPt: pixelsPerMM * mmPerInch / ptPerInch,
}
w.szMu.Lock()
w.sz = sz
w.szMu.Unlock()
w.Send(sz)
}
//export onDeleteWindow
func onDeleteWindow(id uintptr) {
theScreen.mu.Lock()
w := theScreen.windows[id]
theScreen.mu.Unlock()
if w == nil {
return
}
w.lifecycler.SetDead(true)
w.lifecycler.SendEvent(w, w.glctx)
}
func surfaceCreate() error {
if C.surfaceCreate() == 0 {
return errors.New("gldriver: surface creation failed")
}
return nil
}