canvas/backend/xmobilebackend/xmobile.go
2019-07-10 11:36:02 +02:00

610 lines
18 KiB
Go
Executable file

package xmobilebackend
import (
"fmt"
"image/color"
"math"
"reflect"
"unsafe"
"github.com/tfriedel6/canvas/backend/backendbase"
"golang.org/x/mobile/gl"
)
const alphaTexSize = 2048
var zeroes [alphaTexSize]byte
// GLContext is a context that contains all the
// shaders and buffers necessary for rendering
type GLContext struct {
glctx gl.Context
buf gl.Buffer
shadowBuf gl.Buffer
alphaTex gl.Texture
sr solidShader
lgr linearGradientShader
rgr radialGradientShader
ipr imagePatternShader
sar solidAlphaShader
rgar radialGradientAlphaShader
lgar linearGradientAlphaShader
ipar imagePatternAlphaShader
ir imageShader
gauss15r gaussianShader
gauss63r gaussianShader
gauss127r gaussianShader
offscr1 offscreenBuffer
offscr2 offscreenBuffer
imageBufTex gl.Texture
imageBuf []byte
ptsBuf []float32
glChan chan func()
}
// NewGLContext creates all the necessary GL resources,
// like shaders and buffers
func NewGLContext(glctx gl.Context) (*GLContext, error) {
ctx := &GLContext{
glctx: glctx,
ptsBuf: make([]float32, 0, 4096),
glChan: make(chan func()),
}
var err error
b := &XMobileBackend{GLContext: ctx}
b.glctx.GetError() // clear error state
err = loadShader(b, solidVS, solidFS, &ctx.sr.shaderProgram)
if err != nil {
return nil, err
}
ctx.sr.shaderProgram.mustLoadLocations(&ctx.sr)
if err = glError(b); err != nil {
return nil, err
}
err = loadShader(b, linearGradientVS, linearGradientFS, &ctx.lgr.shaderProgram)
if err != nil {
return nil, err
}
ctx.lgr.shaderProgram.mustLoadLocations(&ctx.lgr)
if err = glError(b); err != nil {
return nil, err
}
err = loadShader(b, radialGradientVS, radialGradientFS, &ctx.rgr.shaderProgram)
if err != nil {
return nil, err
}
ctx.rgr.shaderProgram.mustLoadLocations(&ctx.rgr)
if err = glError(b); err != nil {
return nil, err
}
err = loadShader(b, imagePatternVS, imagePatternFS, &ctx.ipr.shaderProgram)
if err != nil {
return nil, err
}
ctx.ipr.shaderProgram.mustLoadLocations(&ctx.ipr)
if err = glError(b); err != nil {
return nil, err
}
err = loadShader(b, solidAlphaVS, solidAlphaFS, &ctx.sar.shaderProgram)
if err != nil {
return nil, err
}
ctx.sar.shaderProgram.mustLoadLocations(&ctx.sar)
if err = glError(b); err != nil {
return nil, err
}
err = loadShader(b, linearGradientAlphaVS, linearGradientFS, &ctx.lgar.shaderProgram)
if err != nil {
return nil, err
}
ctx.lgar.shaderProgram.mustLoadLocations(&ctx.lgar)
if err = glError(b); err != nil {
return nil, err
}
err = loadShader(b, radialGradientAlphaVS, radialGradientAlphaFS, &ctx.rgar.shaderProgram)
if err != nil {
return nil, err
}
ctx.rgar.shaderProgram.mustLoadLocations(&ctx.rgar)
if err = glError(b); err != nil {
return nil, err
}
err = loadShader(b, imagePatternAlphaVS, imagePatternAlphaFS, &ctx.ipar.shaderProgram)
if err != nil {
return nil, err
}
ctx.ipar.shaderProgram.mustLoadLocations(&ctx.ipar)
if err = glError(b); err != nil {
return nil, err
}
err = loadShader(b, imageVS, imageFS, &ctx.ir.shaderProgram)
if err != nil {
return nil, err
}
ctx.ir.shaderProgram.mustLoadLocations(&ctx.ir)
if err = glError(b); err != nil {
return nil, err
}
err = loadShader(b, gaussian15VS, gaussian15FS, &ctx.gauss15r.shaderProgram)
if err != nil {
return nil, err
}
ctx.gauss15r.shaderProgram.mustLoadLocations(&ctx.gauss15r)
if err = glError(b); err != nil {
return nil, err
}
err = loadShader(b, gaussian63VS, gaussian63FS, &ctx.gauss63r.shaderProgram)
if err != nil {
return nil, err
}
ctx.gauss63r.shaderProgram.mustLoadLocations(&ctx.gauss63r)
if err = glError(b); err != nil {
return nil, err
}
err = loadShader(b, gaussian127VS, gaussian127FS, &ctx.gauss127r.shaderProgram)
if err != nil {
return nil, err
}
ctx.gauss127r.shaderProgram.mustLoadLocations(&ctx.gauss127r)
if err = glError(b); err != nil {
return nil, err
}
ctx.buf = b.glctx.CreateBuffer()
if err = glError(b); err != nil {
return nil, err
}
ctx.shadowBuf = b.glctx.CreateBuffer()
if err = glError(b); err != nil {
return nil, err
}
b.glctx.ActiveTexture(gl.TEXTURE0)
ctx.alphaTex = b.glctx.CreateTexture()
b.glctx.BindTexture(gl.TEXTURE_2D, ctx.alphaTex)
b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
b.glctx.TexImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, alphaTexSize, alphaTexSize, gl.ALPHA, gl.UNSIGNED_BYTE, nil)
// todo should use gl.RED on OpenGL, gl.ALPHA on OpenGL ES
b.glctx.Enable(gl.BLEND)
b.glctx.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
b.glctx.Enable(gl.STENCIL_TEST)
b.glctx.StencilMask(0xFF)
b.glctx.Clear(gl.STENCIL_BUFFER_BIT)
b.glctx.StencilOp(gl.KEEP, gl.KEEP, gl.KEEP)
b.glctx.StencilFunc(gl.EQUAL, 0, 0xFF)
b.glctx.Disable(gl.SCISSOR_TEST)
return ctx, nil
}
// XMobileBackend is a canvas backend using Go-GL
type XMobileBackend struct {
x, y, w, h int
fx, fy, fw, fh float64
*GLContext
activateFn func()
disableTextureRenderTarget func()
}
type offscreenBuffer struct {
tex gl.Texture
w int
h int
renderStencilBuf gl.Renderbuffer
frameBuf gl.Framebuffer
alpha bool
}
// New returns a new canvas backend. x, y, w, h define the target
// rectangle in the window. ctx is a GLContext created with
// NewGLContext
func New(x, y, w, h int, ctx *GLContext) (*XMobileBackend, error) {
b := &XMobileBackend{
w: w,
h: h,
fw: float64(w),
fh: float64(h),
GLContext: ctx,
}
b.activateFn = func() {
b.glctx.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{Value: 0})
b.glctx.Viewport(b.x, b.y, b.w, b.h)
// todo reapply clipping since another application may have used the stencil buffer
}
b.disableTextureRenderTarget = func() {
b.glctx.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{Value: 0})
}
return b, nil
}
// XMobileBackendOffscreen is a canvas backend using an offscreen
// texture
type XMobileBackendOffscreen struct {
XMobileBackend
TextureID gl.Texture
offscrBuf offscreenBuffer
offscrImg Image
}
// NewOffscreen returns a new offscreen canvas backend. w, h define
// the size of the offscreen texture. ctx is a GLContext created
// with NewGLContext
func NewOffscreen(w, h int, alpha bool, ctx *GLContext) (*XMobileBackendOffscreen, error) {
b, err := New(0, 0, w, h, ctx)
if err != nil {
return nil, err
}
bo := &XMobileBackendOffscreen{XMobileBackend: *b}
bo.offscrBuf.alpha = alpha
bo.offscrImg.flip = true
bo.activateFn = func() {
bo.enableTextureRenderTarget(&bo.offscrBuf)
b.glctx.Viewport(0, 0, bo.w, bo.h)
bo.offscrImg.w = bo.offscrBuf.w
bo.offscrImg.h = bo.offscrBuf.h
bo.offscrImg.tex = bo.offscrBuf.tex
bo.TextureID = bo.offscrBuf.tex
}
bo.disableTextureRenderTarget = func() {
bo.enableTextureRenderTarget(&bo.offscrBuf)
}
return bo, nil
}
// SetBounds updates the bounds of the canvas. This would
// usually be called for example when the window is resized
func (b *XMobileBackend) SetBounds(x, y, w, h int) {
b.x, b.y = x, y
b.fx, b.fy = float64(x), float64(y)
b.w, b.h = w, h
b.fw, b.fh = float64(w), float64(h)
if b == activeContext {
b.glctx.Viewport(0, 0, b.w, b.h)
b.glctx.Clear(gl.STENCIL_BUFFER_BIT)
}
}
// SetSize updates the size of the offscreen texture
func (b *XMobileBackendOffscreen) SetSize(w, h int) {
b.XMobileBackend.SetBounds(0, 0, w, h)
b.offscrImg.w = b.offscrBuf.w
b.offscrImg.h = b.offscrBuf.h
}
// Size returns the size of the window or offscreen
// texture
func (b *XMobileBackend) Size() (int, int) {
return b.w, b.h
}
func glError(b *XMobileBackend) error {
glErr := b.glctx.GetError()
if glErr != gl.NO_ERROR {
return fmt.Errorf("GL Error: %x", glErr)
}
return nil
}
// Activate only needs to be called if there is other
// code also using the GL state
func (b *XMobileBackend) Activate() {
b.activate()
}
var activeContext *XMobileBackend
func (b *XMobileBackend) activate() {
if activeContext != b {
activeContext = b
b.activateFn()
}
b.runGLQueue()
}
func (b *XMobileBackend) runGLQueue() {
for {
select {
case f := <-b.glChan:
f()
default:
return
}
}
}
// Delete deletes the offscreen texture. After calling this
// the backend can no longer be used
func (b *XMobileBackendOffscreen) Delete() {
b.glctx.DeleteTexture(b.offscrBuf.tex)
b.glctx.DeleteFramebuffer(b.offscrBuf.frameBuf)
b.glctx.DeleteRenderbuffer(b.offscrBuf.renderStencilBuf)
}
// CanUseAsImage returns true if the given backend can be
// directly used by this backend to avoid a conversion.
// Used internally
func (b *XMobileBackend) CanUseAsImage(b2 backendbase.Backend) bool {
_, ok := b2.(*XMobileBackendOffscreen)
return ok
}
// AsImage returns nil, since this backend cannot be directly
// used as an image. Used internally
func (b *XMobileBackend) AsImage() backendbase.Image {
return nil
}
// AsImage returns an implementation of the Image interface
// that can be used to render this offscreen texture
// directly. Used internally
func (b *XMobileBackendOffscreen) AsImage() backendbase.Image {
return &b.offscrImg
}
type glColor struct {
r, g, b, a float64
}
func colorGoToGL(c color.RGBA) glColor {
var glc glColor
glc.r = float64(c.R) / 255
glc.g = float64(c.G) / 255
glc.b = float64(c.B) / 255
glc.a = float64(c.A) / 255
return glc
}
func (b *XMobileBackend) useShader(style *backendbase.FillStyle) (vertexLoc gl.Attrib) {
if lg := style.LinearGradient; lg != nil {
lg := lg.(*LinearGradient)
b.glctx.ActiveTexture(gl.TEXTURE0)
b.glctx.BindTexture(gl.TEXTURE_2D, lg.tex)
b.glctx.UseProgram(b.lgr.ID)
from := vec{style.Gradient.X0, style.Gradient.Y0}
to := vec{style.Gradient.X1, style.Gradient.Y1}
dir := to.sub(from)
length := dir.len()
dir = dir.scale(1 / length)
b.glctx.Uniform2f(b.lgr.CanvasSize, float32(b.fw), float32(b.fh))
b.glctx.Uniform2f(b.lgr.From, float32(from[0]), float32(from[1]))
b.glctx.Uniform2f(b.lgr.Dir, float32(dir[0]), float32(dir[1]))
b.glctx.Uniform1f(b.lgr.Len, float32(length))
b.glctx.Uniform1i(b.lgr.Gradient, 0)
b.glctx.Uniform1f(b.lgr.GlobalAlpha, float32(style.Color.A)/255)
return b.lgr.Vertex
}
if rg := style.RadialGradient; rg != nil {
rg := rg.(*RadialGradient)
b.glctx.ActiveTexture(gl.TEXTURE0)
b.glctx.BindTexture(gl.TEXTURE_2D, rg.tex)
b.glctx.UseProgram(b.rgr.ID)
from := vec{style.Gradient.X0, style.Gradient.Y0}
to := vec{style.Gradient.X1, style.Gradient.Y1}
b.glctx.Uniform2f(b.rgr.CanvasSize, float32(b.fw), float32(b.fh))
b.glctx.Uniform2f(b.rgr.From, float32(from[0]), float32(from[1]))
b.glctx.Uniform2f(b.rgr.To, float32(to[0]), float32(to[1]))
b.glctx.Uniform1f(b.rgr.RadFrom, float32(style.Gradient.RadFrom))
b.glctx.Uniform1f(b.rgr.RadTo, float32(style.Gradient.RadTo))
b.glctx.Uniform1i(b.rgr.Gradient, 0)
b.glctx.Uniform1f(b.rgr.GlobalAlpha, float32(style.Color.A)/255)
return b.rgr.Vertex
}
if ip := style.ImagePattern; ip != nil {
ipd := ip.(*ImagePattern).data
img := ipd.Image.(*Image)
b.glctx.UseProgram(b.ipr.ID)
b.glctx.ActiveTexture(gl.TEXTURE0)
b.glctx.BindTexture(gl.TEXTURE_2D, img.tex)
b.glctx.Uniform2f(b.ipr.CanvasSize, float32(b.fw), float32(b.fh))
b.glctx.Uniform2f(b.ipr.ImageSize, float32(img.w), float32(img.h))
b.glctx.Uniform1i(b.ipr.Image, 0)
var f32mat [9]float32
for i, v := range ipd.Transform {
f32mat[i] = float32(v)
}
b.glctx.UniformMatrix3fv(b.ipr.ImageTransform, f32mat[:])
switch ipd.Repeat {
case backendbase.Repeat:
b.glctx.Uniform2f(b.ipr.Repeat, 1, 1)
case backendbase.RepeatX:
b.glctx.Uniform2f(b.ipr.Repeat, 1, 0)
case backendbase.RepeatY:
b.glctx.Uniform2f(b.ipr.Repeat, 0, 1)
case backendbase.NoRepeat:
b.glctx.Uniform2f(b.ipr.Repeat, 0, 0)
}
b.glctx.Uniform1f(b.ipr.GlobalAlpha, float32(style.Color.A)/255)
return b.ipr.Vertex
}
b.glctx.UseProgram(b.sr.ID)
b.glctx.Uniform2f(b.sr.CanvasSize, float32(b.fw), float32(b.fh))
c := colorGoToGL(style.Color)
b.glctx.Uniform4f(b.sr.Color, float32(c.r), float32(c.g), float32(c.b), float32(c.a))
b.glctx.Uniform1f(b.sr.GlobalAlpha, 1)
return b.sr.Vertex
}
func (b *XMobileBackend) useAlphaShader(style *backendbase.FillStyle, alphaTexSlot int) (vertexLoc, alphaTexCoordLoc gl.Attrib) {
if lg := style.LinearGradient; lg != nil {
lg := lg.(*LinearGradient)
b.glctx.ActiveTexture(gl.TEXTURE0)
b.glctx.BindTexture(gl.TEXTURE_2D, lg.tex)
b.glctx.UseProgram(b.lgar.ID)
from := vec{style.Gradient.X0, style.Gradient.Y0}
to := vec{style.Gradient.X1, style.Gradient.Y1}
dir := to.sub(from)
length := dir.len()
dir = dir.scale(1 / length)
b.glctx.Uniform2f(b.lgar.CanvasSize, float32(b.fw), float32(b.fh))
b.glctx.Uniform2f(b.lgar.From, float32(from[0]), float32(from[1]))
b.glctx.Uniform2f(b.lgar.Dir, float32(dir[0]), float32(dir[1]))
b.glctx.Uniform1f(b.lgar.Len, float32(length))
b.glctx.Uniform1i(b.lgar.Gradient, 0)
b.glctx.Uniform1i(b.lgar.AlphaTex, alphaTexSlot)
b.glctx.Uniform1f(b.lgar.GlobalAlpha, float32(style.Color.A)/255)
return b.lgar.Vertex, b.lgar.AlphaTexCoord
}
if rg := style.RadialGradient; rg != nil {
rg := rg.(*RadialGradient)
b.glctx.ActiveTexture(gl.TEXTURE0)
b.glctx.BindTexture(gl.TEXTURE_2D, rg.tex)
b.glctx.UseProgram(b.rgar.ID)
from := vec{style.Gradient.X0, style.Gradient.Y0}
to := vec{style.Gradient.X1, style.Gradient.Y1}
b.glctx.Uniform2f(b.rgar.CanvasSize, float32(b.fw), float32(b.fh))
b.glctx.Uniform2f(b.rgar.From, float32(from[0]), float32(from[1]))
b.glctx.Uniform2f(b.rgar.To, float32(to[0]), float32(to[1]))
b.glctx.Uniform1f(b.rgar.RadFrom, float32(style.Gradient.RadFrom))
b.glctx.Uniform1f(b.rgar.RadTo, float32(style.Gradient.RadTo))
b.glctx.Uniform1i(b.rgar.Gradient, 0)
b.glctx.Uniform1i(b.rgar.AlphaTex, alphaTexSlot)
b.glctx.Uniform1f(b.rgar.GlobalAlpha, float32(style.Color.A)/255)
return b.rgar.Vertex, b.rgar.AlphaTexCoord
}
if ip := style.ImagePattern; ip != nil {
ipd := ip.(*ImagePattern).data
img := ipd.Image.(*Image)
b.glctx.UseProgram(b.ipar.ID)
b.glctx.ActiveTexture(gl.TEXTURE0)
b.glctx.BindTexture(gl.TEXTURE_2D, img.tex)
b.glctx.Uniform2f(b.ipar.CanvasSize, float32(b.fw), float32(b.fh))
b.glctx.Uniform2f(b.ipar.ImageSize, float32(img.w), float32(img.h))
b.glctx.Uniform1i(b.ipar.Image, 0)
var f32mat [9]float32
for i, v := range ipd.Transform {
f32mat[i] = float32(v)
}
b.glctx.UniformMatrix3fv(b.ipr.ImageTransform, f32mat[:])
switch ipd.Repeat {
case backendbase.Repeat:
b.glctx.Uniform2f(b.ipr.Repeat, 1, 1)
case backendbase.RepeatX:
b.glctx.Uniform2f(b.ipr.Repeat, 1, 0)
case backendbase.RepeatY:
b.glctx.Uniform2f(b.ipr.Repeat, 0, 1)
case backendbase.NoRepeat:
b.glctx.Uniform2f(b.ipr.Repeat, 0, 0)
}
b.glctx.Uniform1i(b.ipar.AlphaTex, alphaTexSlot)
b.glctx.Uniform1f(b.ipar.GlobalAlpha, float32(style.Color.A)/255)
return b.ipar.Vertex, b.ipar.AlphaTexCoord
}
b.glctx.UseProgram(b.sar.ID)
b.glctx.Uniform2f(b.sar.CanvasSize, float32(b.fw), float32(b.fh))
c := colorGoToGL(style.Color)
b.glctx.Uniform4f(b.sar.Color, float32(c.r), float32(c.g), float32(c.b), float32(c.a))
b.glctx.Uniform1i(b.sar.AlphaTex, alphaTexSlot)
b.glctx.Uniform1f(b.sar.GlobalAlpha, 1)
return b.sar.Vertex, b.sar.AlphaTexCoord
}
func (b *XMobileBackend) enableTextureRenderTarget(offscr *offscreenBuffer) {
if offscr.w == b.w && offscr.h == b.h {
b.glctx.BindFramebuffer(gl.FRAMEBUFFER, offscr.frameBuf)
return
}
if b.w == 0 || b.h == 0 {
return
}
if offscr.w != 0 && offscr.h != 0 {
b.glctx.DeleteTexture(offscr.tex)
b.glctx.DeleteFramebuffer(offscr.frameBuf)
b.glctx.DeleteRenderbuffer(offscr.renderStencilBuf)
}
offscr.w = b.w
offscr.h = b.h
b.glctx.ActiveTexture(gl.TEXTURE0)
offscr.tex = b.glctx.CreateTexture()
b.glctx.BindTexture(gl.TEXTURE_2D, offscr.tex)
// todo do non-power-of-two textures work everywhere?
if offscr.alpha {
b.glctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, b.w, b.h, gl.RGBA, gl.UNSIGNED_BYTE, nil)
} else {
b.glctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RGB, b.w, b.h, gl.RGB, gl.UNSIGNED_BYTE, nil)
}
b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
offscr.frameBuf = b.glctx.CreateFramebuffer()
b.glctx.BindFramebuffer(gl.FRAMEBUFFER, offscr.frameBuf)
offscr.renderStencilBuf = b.glctx.CreateRenderbuffer()
b.glctx.BindRenderbuffer(gl.RENDERBUFFER, offscr.renderStencilBuf)
b.glctx.RenderbufferStorage(gl.RENDERBUFFER, gl.STENCIL_INDEX8, b.w, b.h)
b.glctx.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, offscr.renderStencilBuf)
b.glctx.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, offscr.tex, 0)
if err := b.glctx.CheckFramebufferStatus(gl.FRAMEBUFFER); err != gl.FRAMEBUFFER_COMPLETE {
// todo this should maybe not panic
panic(fmt.Sprintf("Failed to set up framebuffer for offscreen texture: %x", err))
}
b.glctx.Clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
}
type vec [2]float64
func (v vec) sub(v2 vec) vec {
return vec{v[0] - v2[0], v[1] - v2[1]}
}
func (v vec) len() float64 {
return math.Sqrt(v[0]*v[0] + v[1]*v[1])
}
func (v vec) scale(f float64) vec {
return vec{v[0] * f, v[1] * f}
}
func byteSlice(ptr unsafe.Pointer, size int) []byte {
var buf []byte
sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
sh.Cap = size
sh.Len = size
sh.Data = uintptr(ptr)
return buf
}