From 77a9d14867a299ca65ac075d04193809e0af5120 Mon Sep 17 00:00:00 2001 From: Thomas Friedel Date: Sun, 24 Feb 2019 13:19:21 +0100 Subject: [PATCH] added an xmobile backend that can be generated from the gogl backend --- backend/gogl/gogl.go | 6 +- backend/gogl/shader.go | 5 +- backend/xmobile/clip.go | 66 +++++ backend/xmobile/fill.go | 305 +++++++++++++++++++++ backend/xmobile/gen/gen.go | 401 ++++++++++++++++++++++++++++ backend/xmobile/gradients.go | 116 ++++++++ backend/xmobile/imagedata.go | 94 +++++++ backend/xmobile/images.go | 263 ++++++++++++++++++ backend/xmobile/shader.go | 180 +++++++++++++ backend/xmobile/shaders.go | 468 ++++++++++++++++++++++++++++++++ backend/xmobile/xmobile.go | 498 +++++++++++++++++++++++++++++++++++ examples/shiny/shiny.go | 11 +- 12 files changed, 2404 insertions(+), 9 deletions(-) create mode 100755 backend/xmobile/clip.go create mode 100755 backend/xmobile/fill.go create mode 100644 backend/xmobile/gen/gen.go create mode 100755 backend/xmobile/gradients.go create mode 100755 backend/xmobile/imagedata.go create mode 100755 backend/xmobile/images.go create mode 100755 backend/xmobile/shader.go create mode 100755 backend/xmobile/shaders.go create mode 100755 backend/xmobile/xmobile.go diff --git a/backend/gogl/gogl.go b/backend/gogl/gogl.go index a3990bd..22bb964 100644 --- a/backend/gogl/gogl.go +++ b/backend/gogl/gogl.go @@ -64,8 +64,6 @@ func New(x, y, w, h int) (*GoGLBackend, error) { return nil, err } - gl.GetError() // clear error state - b := &GoGLBackend{ w: w, h: h, @@ -74,6 +72,8 @@ func New(x, y, w, h int) (*GoGLBackend, error) { ptsBuf: make([]float32, 0, 4096), } + gl.GetError() // clear error state + err = loadShader(solidVS, solidFS, &b.sr.shaderProgram) if err != nil { return nil, err @@ -450,7 +450,7 @@ func (b *GoGLBackend) enableTextureRenderTarget(offscr *offscreenBuffer) { gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, int32(b.w), int32(b.h)) gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, offscr.renderStencilBuf) - gl.FramebufferTexture(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, offscr.tex, 0) + gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, offscr.tex, 0) if err := gl.CheckFramebufferStatus(gl.FRAMEBUFFER); err != gl.FRAMEBUFFER_COMPLETE { // todo this should maybe not panic diff --git a/backend/gogl/shader.go b/backend/gogl/shader.go index f7e62da..b6d8f5a 100644 --- a/backend/gogl/shader.go +++ b/backend/gogl/shader.go @@ -13,8 +13,9 @@ import ( type shaderProgram struct { ID, vs, fs uint32 - attribs map[string]uint32 - uniforms map[string]int32 + + attribs map[string]uint32 + uniforms map[string]int32 } func loadShader(vs, fs string, sp *shaderProgram) error { diff --git a/backend/xmobile/clip.go b/backend/xmobile/clip.go new file mode 100755 index 0000000..b088e87 --- /dev/null +++ b/backend/xmobile/clip.go @@ -0,0 +1,66 @@ +package xmobilebackend + +import ( + "unsafe" + + "golang.org/x/mobile/gl" +) + +func (b *XMobileBackend) ClearClip() { + b.activate() + + b.glctx.StencilMask(0xFF) + b.glctx.Clear(gl.STENCIL_BUFFER_BIT) +} + +func (b *XMobileBackend) Clip(pts [][2]float64) { + b.activate() + + b.ptsBuf = b.ptsBuf[:0] + b.ptsBuf = append(b.ptsBuf, + 0, 0, + 0, float32(b.fh), + float32(b.fw), float32(b.fh), + float32(b.fw), 0) + for _, pt := range pts { + b.ptsBuf = append(b.ptsBuf, float32(pt[0]), float32(pt[1])) + } + + mode := gl.Enum(gl.TRIANGLES) + if len(pts) == 4 { + mode = gl.TRIANGLE_FAN + } + + b.glctx.BindBuffer(gl.ARRAY_BUFFER, b.buf) + b.glctx.BufferData(gl.ARRAY_BUFFER, byteSlice(unsafe.Pointer(&b.ptsBuf[0]), len(b.ptsBuf)*4), gl.STREAM_DRAW) + b.glctx.VertexAttribPointer(b.sr.Vertex, 2, gl.FLOAT, false, 0, 0) + + b.glctx.UseProgram(b.sr.ID) + b.glctx.Uniform4f(b.sr.Color, 1, 1, 1, 1) + b.glctx.Uniform2f(b.sr.CanvasSize, float32(b.fw), float32(b.fh)) + b.glctx.EnableVertexAttribArray(b.sr.Vertex) + + b.glctx.ColorMask(false, false, false, false) + + b.glctx.StencilMask(0x04) + b.glctx.StencilFunc(gl.ALWAYS, 4, 0x04) + b.glctx.StencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE) + b.glctx.DrawArrays(mode, 4, len(pts)) + + b.glctx.StencilMask(0x02) + b.glctx.StencilFunc(gl.EQUAL, 0, 0x06) + b.glctx.StencilOp(gl.KEEP, gl.INVERT, gl.INVERT) + b.glctx.DrawArrays(gl.TRIANGLE_FAN, 0, 4) + + b.glctx.StencilMask(0x04) + b.glctx.StencilFunc(gl.ALWAYS, 0, 0x04) + b.glctx.StencilOp(gl.ZERO, gl.ZERO, gl.ZERO) + b.glctx.DrawArrays(gl.TRIANGLE_FAN, 0, 4) + + b.glctx.DisableVertexAttribArray(b.sr.Vertex) + + b.glctx.ColorMask(true, true, true, true) + b.glctx.StencilOp(gl.KEEP, gl.KEEP, gl.KEEP) + b.glctx.StencilMask(0xFF) + b.glctx.StencilFunc(gl.EQUAL, 0, 0xFF) +} diff --git a/backend/xmobile/fill.go b/backend/xmobile/fill.go new file mode 100755 index 0000000..15fb54c --- /dev/null +++ b/backend/xmobile/fill.go @@ -0,0 +1,305 @@ +package xmobilebackend + +import ( + "image" + "math" + "unsafe" + + "github.com/tfriedel6/canvas/backend/backendbase" + "golang.org/x/mobile/gl" +) + +func (b *XMobileBackend) Clear(pts [4][2]float64) { + b.activate() + + // first check if the four points are aligned to form a nice rectangle, which can be more easily + // cleared using glScissor and glClear + aligned := pts[0][0] == pts[1][0] && pts[2][0] == pts[3][0] && pts[0][1] == pts[3][1] && pts[1][1] == pts[2][1] + if !aligned { + aligned = pts[0][0] == pts[3][0] && pts[1][0] == pts[2][0] && pts[0][1] == pts[1][1] && pts[2][1] == pts[3][1] + } + if aligned { + minX := math.Floor(math.Min(pts[0][0], pts[2][0])) + maxX := math.Ceil(math.Max(pts[0][0], pts[2][0])) + minY := math.Floor(math.Min(pts[0][1], pts[2][1])) + maxY := math.Ceil(math.Max(pts[0][1], pts[2][1])) + b.clearRect(int(minX), int(minY), int(maxX)-int(minX), int(maxY)-int(minY)) + return + } + + data := [8]float32{ + float32(pts[0][0]), float32(pts[0][1]), + float32(pts[1][0]), float32(pts[1][1]), + float32(pts[2][0]), float32(pts[2][1]), + float32(pts[3][0]), float32(pts[3][1])} + + b.glctx.UseProgram(b.sr.ID) + b.glctx.Uniform2f(b.sr.CanvasSize, float32(b.fw), float32(b.fh)) + b.glctx.Uniform4f(b.sr.Color, 0, 0, 0, 0) + b.glctx.Uniform1f(b.sr.GlobalAlpha, 1) + + b.glctx.Disable(gl.BLEND) + + b.glctx.BindBuffer(gl.ARRAY_BUFFER, b.buf) + b.glctx.BufferData(gl.ARRAY_BUFFER, byteSlice(unsafe.Pointer(&data[0]), len(data)*4), gl.STREAM_DRAW) + + b.glctx.StencilFunc(gl.EQUAL, 0, 0xFF) + + b.glctx.VertexAttribPointer(b.sr.Vertex, 2, gl.FLOAT, false, 0, 0) + b.glctx.EnableVertexAttribArray(b.sr.Vertex) + b.glctx.DrawArrays(gl.TRIANGLE_FAN, 0, 4) + b.glctx.DisableVertexAttribArray(b.sr.Vertex) + + b.glctx.StencilFunc(gl.ALWAYS, 0, 0xFF) + + b.glctx.Enable(gl.BLEND) +} + +func (b *XMobileBackend) clearRect(x, y, w, h int) { + b.glctx.Enable(gl.SCISSOR_TEST) + + var box [4]int32 + b.glctx.GetIntegerv(box[:], gl.SCISSOR_BOX) + + b.glctx.Scissor(int32(x), int32(b.h-y-h), int32(w), int32(h)) + b.glctx.ClearColor(0, 0, 0, 0) + b.glctx.Clear(gl.COLOR_BUFFER_BIT) + b.glctx.Scissor(box[0], box[1], box[2], box[3]) + + b.glctx.Disable(gl.SCISSOR_TEST) +} + +func (b *XMobileBackend) Fill(style *backendbase.FillStyle, pts [][2]float64) { + b.activate() + + if style.Blur > 0 { + b.offscr1.alpha = true + b.enableTextureRenderTarget(&b.offscr1) + b.glctx.ClearColor(0, 0, 0, 0) + b.glctx.Clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT) + } + + b.ptsBuf = b.ptsBuf[:0] + b.ptsBuf = append(b.ptsBuf, + 0, 0, + 0, float32(b.fh), + float32(b.fw), float32(b.fh), + float32(b.fw), 0) + for _, pt := range pts { + b.ptsBuf = append(b.ptsBuf, float32(pt[0]), float32(pt[1])) + } + + mode := gl.Enum(gl.TRIANGLES) + if len(pts) == 4 { + mode = gl.TRIANGLE_FAN + } + + b.glctx.BindBuffer(gl.ARRAY_BUFFER, b.buf) + b.glctx.BufferData(gl.ARRAY_BUFFER, byteSlice(unsafe.Pointer(&b.ptsBuf[0]), len(b.ptsBuf)*4), gl.STREAM_DRAW) + + if style.Color.A >= 255 { + vertex := b.useShader(style) + + b.glctx.EnableVertexAttribArray(vertex) + b.glctx.VertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0) + b.glctx.DrawArrays(mode, 4, len(pts)) + b.glctx.DisableVertexAttribArray(vertex) + } else { + b.glctx.ColorMask(false, false, false, false) + b.glctx.StencilFunc(gl.ALWAYS, 1, 0xFF) + b.glctx.StencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE) + b.glctx.StencilMask(0x01) + + b.glctx.UseProgram(b.sr.ID) + b.glctx.Uniform4f(b.sr.Color, 0, 0, 0, 0) + b.glctx.Uniform2f(b.sr.CanvasSize, float32(b.fw), float32(b.fh)) + + b.glctx.EnableVertexAttribArray(b.sr.Vertex) + b.glctx.VertexAttribPointer(b.sr.Vertex, 2, gl.FLOAT, false, 0, 0) + b.glctx.DrawArrays(mode, 4, len(pts)) + b.glctx.DisableVertexAttribArray(b.sr.Vertex) + + b.glctx.ColorMask(true, true, true, true) + + b.glctx.StencilFunc(gl.EQUAL, 1, 0xFF) + + vertex := b.useShader(style) + b.glctx.EnableVertexAttribArray(vertex) + b.glctx.VertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0) + + b.ptsBuf = append(b.ptsBuf[:0], 0, 0, float32(b.fw), 0, float32(b.fw), float32(b.fh), 0, float32(b.fh)) + b.glctx.DrawArrays(gl.TRIANGLE_FAN, 0, 4) + b.glctx.DisableVertexAttribArray(vertex) + + b.glctx.StencilOp(gl.KEEP, gl.KEEP, gl.KEEP) + b.glctx.StencilFunc(gl.ALWAYS, 0, 0xFF) + + b.glctx.Clear(gl.STENCIL_BUFFER_BIT) + b.glctx.StencilMask(0xFF) + } + + if style.Blur > 0 { + b.drawBlurred(style.Blur) + } +} + +func (b *XMobileBackend) FillImageMask(style *backendbase.FillStyle, mask *image.Alpha, pts [][2]float64) { + b.activate() + + w, h := mask.Rect.Dx(), mask.Rect.Dy() + + b.glctx.ActiveTexture(gl.TEXTURE1) + b.glctx.BindTexture(gl.TEXTURE_2D, b.alphaTex) + for y := 0; y < h; y++ { + off := y * mask.Stride + b.glctx.TexSubImage2D(gl.TEXTURE_2D, 0, 0, alphaTexSize-1-y, w, 1, gl.ALPHA, gl.UNSIGNED_BYTE, mask.Pix[off:]) + } + + if style.Blur > 0 { + b.offscr1.alpha = true + b.enableTextureRenderTarget(&b.offscr1) + b.glctx.ClearColor(0, 0, 0, 0) + b.glctx.Clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT) + } + + b.glctx.StencilFunc(gl.EQUAL, 0, 0xFF) + + b.glctx.BindBuffer(gl.ARRAY_BUFFER, b.buf) + + vertex, alphaTexCoord := b.useAlphaShader(style, 1) + + b.glctx.EnableVertexAttribArray(vertex) + b.glctx.EnableVertexAttribArray(alphaTexCoord) + + tw := float64(w) / alphaTexSize + th := float64(h) / alphaTexSize + var buf [16]float32 + data := buf[:0] + for _, pt := range pts { + data = append(data, float32(pt[0]), float32(pt[1])) + } + data = append(data, 0, 1, 0, float32(1-th), float32(tw), float32(1-th), float32(tw), 1) + + b.glctx.BufferData(gl.ARRAY_BUFFER, byteSlice(unsafe.Pointer(&data[0]), len(data)*4), gl.STREAM_DRAW) + + b.glctx.VertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0) + b.glctx.VertexAttribPointer(alphaTexCoord, 2, gl.FLOAT, false, 0, 8*4) + b.glctx.DrawArrays(gl.TRIANGLE_FAN, 0, 4) + + b.glctx.DisableVertexAttribArray(vertex) + b.glctx.DisableVertexAttribArray(alphaTexCoord) + + b.glctx.ActiveTexture(gl.TEXTURE1) + b.glctx.BindTexture(gl.TEXTURE_2D, b.alphaTex) + + b.glctx.StencilFunc(gl.ALWAYS, 0, 0xFF) + + for y := 0; y < h; y++ { + b.glctx.TexSubImage2D(gl.TEXTURE_2D, 0, 0, alphaTexSize-1-y, w, 1, gl.ALPHA, gl.UNSIGNED_BYTE, zeroes[0:]) + } + + b.glctx.ActiveTexture(gl.TEXTURE0) + + if style.Blur > 0 { + b.drawBlurred(style.Blur) + } +} + +func (b *XMobileBackend) drawBlurred(blur float64) { + b.glctx.BlendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA) + + var kernel []float32 + var kernelBuf [255]float32 + var gs *gaussianShader + if blur < 3 { + gs = &b.gauss15r + kernel = kernelBuf[:15] + } else if blur < 12 { + gs = &b.gauss63r + kernel = kernelBuf[:63] + } else { + gs = &b.gauss127r + kernel = kernelBuf[:127] + } + + gaussianKernel(blur, kernel) + + b.offscr2.alpha = true + b.enableTextureRenderTarget(&b.offscr2) + b.glctx.ClearColor(0, 0, 0, 0) + b.glctx.Clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT) + + b.glctx.StencilFunc(gl.EQUAL, 0, 0xFF) + + b.glctx.BindBuffer(gl.ARRAY_BUFFER, b.shadowBuf) + data := [16]float32{0, 0, 0, float32(b.h), float32(b.w), float32(b.h), float32(b.w), 0, 0, 0, 0, 1, 1, 1, 1, 0} + b.glctx.BufferData(gl.ARRAY_BUFFER, byteSlice(unsafe.Pointer(&data[0]), len(data)*4), gl.STREAM_DRAW) + + b.glctx.ActiveTexture(gl.TEXTURE0) + b.glctx.BindTexture(gl.TEXTURE_2D, b.offscr1.tex) + + b.glctx.UseProgram(gs.ID) + b.glctx.Uniform1i(gs.Image, 0) + b.glctx.Uniform2f(gs.CanvasSize, float32(b.fw), float32(b.fh)) + b.glctx.Uniform2f(gs.KernelScale, 1.0/float32(b.fw), 0.0) + b.glctx.Uniform1fv(gs.Kernel, kernel) + b.glctx.VertexAttribPointer(gs.Vertex, 2, gl.FLOAT, false, 0, 0) + b.glctx.VertexAttribPointer(gs.TexCoord, 2, gl.FLOAT, false, 0, 8*4) + b.glctx.EnableVertexAttribArray(gs.Vertex) + b.glctx.EnableVertexAttribArray(gs.TexCoord) + b.glctx.DrawArrays(gl.TRIANGLE_FAN, 0, 4) + b.glctx.DisableVertexAttribArray(gs.Vertex) + b.glctx.DisableVertexAttribArray(gs.TexCoord) + + b.glctx.StencilFunc(gl.ALWAYS, 0, 0xFF) + + b.disableTextureRenderTarget() + + b.glctx.StencilFunc(gl.EQUAL, 0, 0xFF) + + b.glctx.BindBuffer(gl.ARRAY_BUFFER, b.shadowBuf) + data = [16]float32{0, 0, 0, float32(b.h), float32(b.w), float32(b.h), float32(b.w), 0, 0, 0, 0, 1, 1, 1, 1, 0} + b.glctx.BufferData(gl.ARRAY_BUFFER, byteSlice(unsafe.Pointer(&data[0]), len(data)*4), gl.STREAM_DRAW) + + b.glctx.ActiveTexture(gl.TEXTURE0) + b.glctx.BindTexture(gl.TEXTURE_2D, b.offscr2.tex) + + b.glctx.UseProgram(gs.ID) + b.glctx.Uniform1i(gs.Image, 0) + b.glctx.Uniform2f(gs.CanvasSize, float32(b.fw), float32(b.fh)) + b.glctx.Uniform2f(gs.KernelScale, 0.0, 1.0/float32(b.fh)) + b.glctx.Uniform1fv(gs.Kernel, kernel) + b.glctx.VertexAttribPointer(gs.Vertex, 2, gl.FLOAT, false, 0, 0) + b.glctx.VertexAttribPointer(gs.TexCoord, 2, gl.FLOAT, false, 0, 8*4) + b.glctx.EnableVertexAttribArray(gs.Vertex) + b.glctx.EnableVertexAttribArray(gs.TexCoord) + b.glctx.DrawArrays(gl.TRIANGLE_FAN, 0, 4) + b.glctx.DisableVertexAttribArray(gs.Vertex) + b.glctx.DisableVertexAttribArray(gs.TexCoord) + + b.glctx.StencilFunc(gl.ALWAYS, 0, 0xFF) + + b.glctx.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) +} + +func gaussianKernel(stddev float64, target []float32) { + stddevSqr := stddev * stddev + center := float64(len(target) / 2) + factor := 1.0 / math.Sqrt(2*math.Pi*stddevSqr) + for i := range target { + x := float64(i) - center + target[i] = float32(factor * math.Pow(math.E, -x*x/(2*stddevSqr))) + } + // normalizeKernel(target) +} + +func normalizeKernel(kernel []float32) { + var sum float32 + for _, v := range kernel { + sum += v + } + factor := 1.0 / sum + for i := range kernel { + kernel[i] *= factor + } +} diff --git a/backend/xmobile/gen/gen.go b/backend/xmobile/gen/gen.go new file mode 100644 index 0000000..569d504 --- /dev/null +++ b/backend/xmobile/gen/gen.go @@ -0,0 +1,401 @@ +package main + +import ( + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" +) + +func main() { + { // make sure we are in the right directory + dir, err := os.Getwd() + if err != nil { + log.Fatalf("Failed to get working directory: %v", err) + } + d1 := filepath.Base(dir) + d2 := filepath.Base(filepath.Dir(dir)) + if d2 != "backend" || d1 != "xmobile" { + log.Fatalln("This must be run in the backend/xmobile directory") + } + } + + { // delete existing files + fis, err := ioutil.ReadDir(".") + if err != nil { + log.Fatalf("Failed to read current dir: %v", err) + } + + for _, fi := range fis { + if fi.IsDir() || fi.Name() == "shader.go" { + continue + } + err = os.Remove(fi.Name()) + if err != nil { + log.Fatalf("Failed to delete file %s: %v", fi.Name(), err) + } + } + } + + { // copy gogl files + fis, err := ioutil.ReadDir("../gogl") + if err != nil { + log.Fatalf("Failed to read dir ../gogl: %v", err) + } + + for _, fi := range fis { + if !strings.HasSuffix(fi.Name(), ".go") || fi.Name() == "shader.go" { + continue + } + path := filepath.Join("../gogl", fi.Name()) + data, err := ioutil.ReadFile(path) + if err != nil { + log.Fatalf("Failed to read file %s: %v", path, err) + } + + filename, rewritten := rewrite(fi.Name(), string(data)) + + err = ioutil.WriteFile(filename, ([]byte)(rewritten), 0777) + if err != nil { + log.Fatalf("Failed to write file %s: %v", fi.Name(), err) + } + } + } + + err := exec.Command("go", "fmt").Run() + if err != nil { + log.Fatalf("Failed to run go fmt: %v", err) + } +} + +func rewrite(filename, src string) (string, string) { + src = strings.Replace(src, `package goglbackend`, `package xmobilebackend`, 1) + src = strings.Replace(src, `"github.com/go-gl/gl/v3.2-core/gl"`, `"golang.org/x/mobile/gl"`, 1) + src = strings.Replace(src, "\tgl.", "\tb.glctx.", -1) + src = strings.Replace(src, "GoGLBackend", "XMobileBackend", -1) + src = strings.Replace(src, "uint32(gl.TRIANGLES)", "gl.Enum(gl.TRIANGLES)", -1) + + src = strings.Replace(src, `func (g *gradient) Delete() {`, + `func (g *gradient) Delete() { + b := g.b`, -1) + src = strings.Replace(src, `func (g *gradient) load(stops backendbase.Gradient) {`, + `func (g *gradient) load(stops backendbase.Gradient) { + b := g.b`, -1) + src = strings.Replace(src, `func (img *Image) Delete() {`, + `func (img *Image) Delete() { + b := img.b`, -1) + src = strings.Replace(src, `func (img *Image) Replace(src image.Image) error {`, + `func (img *Image) Replace(src image.Image) error { + b := img.b`, -1) + + src = strings.Replace(src, `imageBufTex == 0`, `imageBufTex.Value == 0`, -1) + + src = strings.Replace(src, + `loadImage(src image.Image, tex uint32)`, + `loadImage(b *XMobileBackend, src image.Image, tex gl.Texture)`, -1) + src = strings.Replace(src, + `loadImageRGBA(src *image.RGBA, tex uint32)`, + `loadImageRGBA(b *XMobileBackend, src *image.RGBA, tex gl.Texture)`, -1) + src = strings.Replace(src, + `loadImageGray(src *image.Gray, tex uint32)`, + `loadImageGray(b *XMobileBackend, src *image.Gray, tex gl.Texture)`, -1) + src = strings.Replace(src, + `loadImageConverted(src image.Image, tex uint32)`, + `loadImageConverted(b *XMobileBackend, src image.Image, tex gl.Texture)`, -1) + + src = strings.Replace(src, + `func loadShader(vs, fs string, sp *shaderProgram) error {`, + `func loadShader(b *XMobileBackend, vs, fs string, sp *shaderProgram) error { + sp.b = b`, -1) + + src = strings.Replace(src, `func glError() error { + glErr := gl.GetError() +`, `func glError(b *XMobileBackend) error { + glErr := b.glctx.GetError() +`, -1) + src = rewriteCalls(src, "glError", func(params []string) string { + return "glError(b)" + }) + + src = regexp.MustCompile(`[ \t]+tex[ ]+uint32`).ReplaceAllString(src, "\ttex gl.Texture") + + src = rewriteCalls(src, "b.glctx.BufferData", func(params []string) string { + return "b.glctx.BufferData(" + params[0] + ", byteSlice(" + params[2] + ", " + params[1] + "), " + params[3] + ")" + }) + src = rewriteCalls(src, "b.glctx.VertexAttribPointer", func(params []string) string { + params[5] = strings.Replace(params[5], "gl.PtrOffset(", "", 1) + params[5] = strings.Replace(params[5], ")", "", 1) + params[5] = strings.Replace(params[5], "nil", "0", 1) + return "b.glctx.VertexAttribPointer(" + strings.Join(params, ",") + ")" + }) + src = rewriteCalls(src, "b.glctx.DrawArrays", func(params []string) string { + if strings.HasPrefix(params[2], "int32(") { + params[2] = params[2][6 : len(params[2])-1] + } + return "b.glctx.DrawArrays(" + strings.Join(params, ",") + ")" + }) + src = rewriteCalls(src, "b.glctx.Uniform1fv", func(params []string) string { + params[2] = params[2][1 : len(params[2])-3] + return "b.glctx.Uniform1fv(" + params[0] + ", " + params[2] + ")" + }) + src = rewriteCalls(src, "b.glctx.TexImage2D", func(params []string) string { + params = append(params[:5], params[6:]...) + for i, param := range params { + if strings.HasPrefix(param, "int32(") { + params[i] = param[6 : len(param)-1] + } else if strings.HasPrefix(param, "gl.Ptr(") { + params[i] = param[8:len(param)-2] + ":]" + } + } + return "b.glctx.TexImage2D(" + strings.Join(params, ", ") + ")" + }) + src = rewriteCalls(src, "b.glctx.TexSubImage2D", func(params []string) string { + for i, param := range params { + if strings.HasPrefix(param, "int32(") { + params[i] = param[6 : len(param)-1] + } else if strings.HasPrefix(param, "gl.Ptr(") { + params[i] = param[8:len(param)-2] + ":]" + } + } + return "b.glctx.TexSubImage2D(" + strings.Join(params, ", ") + ")" + }) + src = rewriteCalls(src, "b.glctx.GetIntegerv", func(params []string) string { + return "b.glctx.GetIntegerv(" + params[1][1:len(params[1])-2] + ":], " + params[0] + ")" + }) + src = rewriteCalls(src, "b.glctx.GenTextures", func(params []string) string { + return params[1][1:] + " = b.glctx.CreateTexture()" + }) + src = rewriteCalls(src, "b.glctx.DeleteTextures", func(params []string) string { + return "b.glctx.DeleteTexture(" + params[1][1:] + ")" + }) + src = rewriteCalls(src, "b.glctx.GenBuffers", func(params []string) string { + return params[1][1:] + " = b.glctx.CreateBuffer()" + }) + src = rewriteCalls(src, "b.glctx.GenFramebuffers", func(params []string) string { + return params[1][1:] + " = b.glctx.CreateFramebuffer()" + }) + src = rewriteCalls(src, "b.glctx.DeleteFramebuffers", func(params []string) string { + return "b.glctx.DeleteFramebuffer(" + params[1][1:] + ")" + }) + src = rewriteCalls(src, "b.glctx.GenRenderbuffers", func(params []string) string { + return params[1][1:] + " = b.glctx.CreateRenderbuffer()" + }) + src = rewriteCalls(src, "b.glctx.DeleteRenderbuffers", func(params []string) string { + return "b.glctx.DeleteRenderbuffer(" + params[1][1:] + ")" + }) + src = rewriteCalls(src, "b.glctx.RenderbufferStorage", func(params []string) string { + for i, param := range params { + if strings.HasPrefix(param, "int32(") { + params[i] = param[6 : len(param)-1] + } + } + return "b.glctx.RenderbufferStorage(" + strings.Join(params, ", ") + ")" + }) + src = rewriteCalls(src, "gl.CheckFramebufferStatus", func(params []string) string { + return "b.glctx.CheckFramebufferStatus(" + strings.Join(params, ", ") + ")" + }) + src = rewriteCalls(src, "b.glctx.BindFramebuffer", func(params []string) string { + if params[1] == "0" { + params[1] = "gl.Framebuffer{Value: 0}" + } + return "b.glctx.BindFramebuffer(" + strings.Join(params, ", ") + ")" + }) + src = rewriteCalls(src, "b.glctx.ReadPixels", func(params []string) string { + for i, param := range params { + if strings.HasPrefix(param, "int32(") { + params[i] = param[6 : len(param)-1] + } else if strings.HasPrefix(param, "gl.Ptr(") { + params[i] = param[8:len(param)-2] + ":]" + } + } + return "b.glctx.ReadPixels(" + params[6] + ", " + strings.Join(params[:len(params)-1], ", ") + ")" + }) + src = rewriteCalls(src, "b.glctx.Viewport", func(params []string) string { + for i, param := range params { + if strings.HasPrefix(param, "int32(") { + params[i] = param[6 : len(param)-1] + } + } + return "b.glctx.Viewport(" + strings.Join(params, ", ") + ")" + }) + src = rewriteCalls(src, "loadImage", func(params []string) string { + return "loadImage(b, " + strings.Join(params, ", ") + ")" + }) + src = rewriteCalls(src, "loadImageRGBA", func(params []string) string { + return "loadImageRGBA(b, " + strings.Join(params, ", ") + ")" + }) + src = rewriteCalls(src, "loadImageGray", func(params []string) string { + return "loadImageGray(b, " + strings.Join(params, ", ") + ")" + }) + src = rewriteCalls(src, "loadImageConverted", func(params []string) string { + return "loadImageConverted(b, " + strings.Join(params, ", ") + ")" + }) + src = rewriteCalls(src, "loadShader", func(params []string) string { + return "loadShader(b, " + strings.Join(params, ", ") + ")" + }) + + if filename == "gogl.go" { + filename = "xmobile.go" + src = rewriteMain(src) + } else if filename == "shaders.go" { + src = rewriteShaders(src) + } + + return filename, src +} + +func rewriteMain(src string) string { + src = strings.Replace(src, "type XMobileBackend struct {\n", + "type XMobileBackend struct {\n\tglctx gl.Context\n\n", 1) + src = strings.Replace(src, "func New(x, y, w, h int) (*XMobileBackend, error) {", + "func New(glctx gl.Context, x, y, w, h int) (*XMobileBackend, error) {", 1) + src = strings.Replace(src, "func NewOffscreen(w, h int, alpha bool) (*XMobileBackend, error)", + "func NewOffscreen(glctx gl.Context, w, h int, alpha bool) (*XMobileBackend, error)", 1) + + src = rewriteCalls(src, "New", func(params []string) string { + return "New(glctx, " + strings.Join(params, ", ") + ")" + }) + + src = strings.Replace(src, + ` err := gl.Init() + if err != nil { + return nil, err + } + +`, ` var err error + +`, 1) + src = strings.Replace(src, "\tb := &XMobileBackend{\n", + "\tb := &XMobileBackend{\n\t\tglctx: glctx,\n\n", 1) + + src = strings.Replace(src, + ` buf uint32 + shadowBuf uint32 + alphaTex uint32 +`, + ` buf gl.Buffer + shadowBuf gl.Buffer + alphaTex gl.Texture +`, 1) + + src = strings.Replace(src, `imageBufTex uint32`, `imageBufTex gl.Texture`, 1) + + src = strings.Replace(src, + `type offscreenBuffer struct { + tex gl.Texture + w int + h int + renderStencilBuf uint32 + frameBuf uint32 + alpha bool +} +`, + `type offscreenBuffer struct { + tex gl.Texture + w int + h int + renderStencilBuf gl.Renderbuffer + frameBuf gl.Framebuffer + alpha bool +} +`, 1) + + src = src + ` +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 +} +` + src = strings.Replace(src, "import (\n", + `import ( + "unsafe" + "reflect" + `, 1) + src = strings.Replace(src, "vertexLoc uint32", "vertexLoc gl.Attrib", 1) + src = strings.Replace(src, "alphaTexSlot int32", "alphaTexSlot int", 1) + src = strings.Replace(src, "vertexLoc, alphaTexCoordLoc uint32", "vertexLoc, alphaTexCoordLoc gl.Attrib", 1) + + return src +} + +func rewriteShaders(src string) string { + src = strings.Replace(src, + `import ( + "bytes" + "fmt" + "strings" +) +`, + `import ( + "bytes" + "fmt" + "strings" + + "golang.org/x/mobile/gl" +) +`, 1) + + src = strings.Replace(src, "uint32", "gl.Attrib", -1) + src = strings.Replace(src, "int32", "gl.Uniform", -1) + + return src +} + +func rewriteCalls(src, funcName string, fn func([]string) string) string { + rewritten := "" + pos := 0 + for { + idx := strings.Index(src[pos:], funcName) + if idx == -1 { + rewritten += src[pos:] + break + } + idx += pos + rewritten += src[pos:idx] + parenStart := idx + len(funcName) + if idx > 5 && src[idx-5:idx] == "func " { + rewritten += src[idx:parenStart] + pos = parenStart + continue + } + if src[parenStart] != '(' { + rewritten += src[idx:parenStart] + pos = parenStart + continue + } + + params := make([]string, 0, 10) + + parenDepth := 0 + paramStart := 0 + paramsStr := src[parenStart+1:] + paramloop: + for i, rn := range paramsStr { + switch rn { + case '(': + parenDepth++ + case ')': + parenDepth-- + if parenDepth == -1 { + params = append(params, strings.TrimSpace(paramsStr[paramStart:i])) + pos = parenStart + i + 2 + break paramloop + } + case ',': + params = append(params, strings.TrimSpace(paramsStr[paramStart:i])) + paramStart = i + 1 + } + } + + rewritten += fn(params) + } + + return rewritten +} diff --git a/backend/xmobile/gradients.go b/backend/xmobile/gradients.go new file mode 100755 index 0000000..5868c58 --- /dev/null +++ b/backend/xmobile/gradients.go @@ -0,0 +1,116 @@ +package xmobilebackend + +import ( + "runtime" + + "github.com/tfriedel6/canvas/backend/backendbase" + "golang.org/x/mobile/gl" +) + +// LinearGradient is a gradient with any number of +// stops and any number of colors. The gradient will +// be drawn such that each point on the gradient +// will correspond to a straight line +type LinearGradient struct { + gradient +} + +// RadialGradient is a gradient with any number of +// stops and any number of colors. The gradient will +// be drawn such that each point on the gradient +// will correspond to a circle +type RadialGradient struct { + gradient +} + +type gradient struct { + b *XMobileBackend + tex gl.Texture + loaded bool + deleted bool + opaque bool +} + +func (b *XMobileBackend) LoadLinearGradient(data backendbase.Gradient) backendbase.LinearGradient { + b.activate() + + lg := &LinearGradient{ + gradient: gradient{b: b, opaque: true}, + } + lg.tex = b.glctx.CreateTexture() + b.glctx.ActiveTexture(gl.TEXTURE0) + b.glctx.BindTexture(gl.TEXTURE_2D, lg.tex) + b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + 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) + lg.load(data) + runtime.SetFinalizer(lg, func(lg *LinearGradient) { + b.glChan <- func() { + b.glctx.DeleteTexture(lg.tex) + } + }) + return lg +} + +func (b *XMobileBackend) LoadRadialGradient(data backendbase.Gradient) backendbase.RadialGradient { + b.activate() + + rg := &RadialGradient{ + gradient: gradient{b: b, opaque: true}, + } + rg.tex = b.glctx.CreateTexture() + b.glctx.ActiveTexture(gl.TEXTURE0) + b.glctx.BindTexture(gl.TEXTURE_2D, rg.tex) + b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + 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) + rg.load(data) + runtime.SetFinalizer(rg, func(rg *RadialGradient) { + b.glChan <- func() { + b.glctx.DeleteTexture(rg.tex) + } + }) + return rg +} + +// Delete explicitly deletes the gradient +func (g *gradient) Delete() { + b := g.b + g.b.activate() + + b.glctx.DeleteTexture(g.tex) + g.deleted = true +} + +func (g *gradient) IsDeleted() bool { return g.deleted } +func (g *gradient) IsOpaque() bool { return g.opaque } + +func (lg *LinearGradient) Replace(data backendbase.Gradient) { lg.load(data) } +func (rg *RadialGradient) Replace(data backendbase.Gradient) { rg.load(data) } + +func (g *gradient) load(stops backendbase.Gradient) { + b := g.b + if g.loaded { + return + } + + g.b.activate() + + b.glctx.ActiveTexture(gl.TEXTURE0) + b.glctx.BindTexture(gl.TEXTURE_2D, g.tex) + var pixels [2048 * 4]byte + pp := 0 + for i := 0; i < 2048; i++ { + c := stops.ColorAt(float64(i) / 2047) + pixels[pp] = c.R + pixels[pp+1] = c.G + pixels[pp+2] = c.B + pixels[pp+3] = c.A + pp += 4 + } + + b.glctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2048, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels[0:]) + g.loaded = true +} diff --git a/backend/xmobile/imagedata.go b/backend/xmobile/imagedata.go new file mode 100755 index 0000000..86bd625 --- /dev/null +++ b/backend/xmobile/imagedata.go @@ -0,0 +1,94 @@ +package xmobilebackend + +import ( + "image" + "image/color" + "unsafe" + + "golang.org/x/mobile/gl" +) + +// GetImageData returns an RGBA image of the current image +func (b *XMobileBackend) GetImageData(x, y, w, h int) *image.RGBA { + b.activate() + + if x < 0 { + w += x + x = 0 + } + if y < 0 { + h += y + y = 0 + } + if w > b.w { + w = b.w + } + if h > b.h { + h = b.h + } + if len(b.imageBuf) < w*h*3 { + b.imageBuf = make([]byte, w*h*3) + } + + b.glctx.ReadPixels(b.imageBuf[0:], x, y, w, h, gl.RGB, gl.UNSIGNED_BYTE) + rgba := image.NewRGBA(image.Rect(x, y, x+w, y+h)) + bp := 0 + for cy := y; cy < y+h; cy++ { + for cx := x; cx < x+w; cx++ { + rgba.SetRGBA(cx, y+h-1-cy, color.RGBA{R: b.imageBuf[bp], G: b.imageBuf[bp+1], B: b.imageBuf[bp+2], A: 255}) + bp += 3 + } + } + return rgba +} + +// PutImageData puts the given image at the given x/y coordinates +func (b *XMobileBackend) PutImageData(img *image.RGBA, x, y int) { + b.activate() + + b.glctx.ActiveTexture(gl.TEXTURE0) + if b.imageBufTex.Value == 0 { + b.imageBufTex = b.glctx.CreateTexture() + b.glctx.BindTexture(gl.TEXTURE_2D, b.imageBufTex) + b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + 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) + } else { + b.glctx.BindTexture(gl.TEXTURE_2D, b.imageBufTex) + } + + w, h := img.Bounds().Dx(), img.Bounds().Dy() + + if img.Stride == img.Bounds().Dx()*4 { + b.glctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, gl.RGBA, gl.UNSIGNED_BYTE, img.Pix[0:]) + } else { + data := make([]uint8, 0, w*h*4) + for cy := 0; cy < h; cy++ { + start := cy * img.Stride + end := start + w*4 + data = append(data, img.Pix[start:end]...) + } + b.glctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, gl.RGBA, gl.UNSIGNED_BYTE, data[0:]) + } + + dx, dy := float32(x), float32(y) + dw, dh := float32(w), float32(h) + + b.glctx.BindBuffer(gl.ARRAY_BUFFER, b.buf) + data := [16]float32{dx, dy, dx + dw, dy, dx + dw, dy + dh, dx, dy + dh, + 0, 0, 1, 0, 1, 1, 0, 1} + b.glctx.BufferData(gl.ARRAY_BUFFER, byteSlice(unsafe.Pointer(&data[0]), len(data)*4), gl.STREAM_DRAW) + + b.glctx.UseProgram(b.ir.ID) + b.glctx.Uniform1i(b.ir.Image, 0) + b.glctx.Uniform2f(b.ir.CanvasSize, float32(b.fw), float32(b.fh)) + b.glctx.Uniform1f(b.ir.GlobalAlpha, 1) + b.glctx.VertexAttribPointer(b.ir.Vertex, 2, gl.FLOAT, false, 0, 0) + b.glctx.VertexAttribPointer(b.ir.TexCoord, 2, gl.FLOAT, false, 0, 8*4) + b.glctx.EnableVertexAttribArray(b.ir.Vertex) + b.glctx.EnableVertexAttribArray(b.ir.TexCoord) + b.glctx.DrawArrays(gl.TRIANGLE_FAN, 0, 4) + b.glctx.DisableVertexAttribArray(b.ir.Vertex) + b.glctx.DisableVertexAttribArray(b.ir.TexCoord) +} diff --git a/backend/xmobile/images.go b/backend/xmobile/images.go new file mode 100755 index 0000000..1e7a344 --- /dev/null +++ b/backend/xmobile/images.go @@ -0,0 +1,263 @@ +package xmobilebackend + +import ( + "errors" + "image" + "runtime" + "unsafe" + + "github.com/tfriedel6/canvas/backend/backendbase" + "golang.org/x/mobile/gl" +) + +// Image represents a loaded image that can be used in various drawing functions +type Image struct { + b *XMobileBackend + w, h int + tex gl.Texture + deleted bool + opaque bool +} + +func (b *XMobileBackend) LoadImage(src image.Image) (backendbase.Image, error) { + b.activate() + + var tex gl.Texture + tex = b.glctx.CreateTexture() + b.glctx.ActiveTexture(gl.TEXTURE0) + b.glctx.BindTexture(gl.TEXTURE_2D, tex) + if src == nil { + return &Image{tex: tex}, nil + } + + img, err := loadImage(b, src, tex) + if err != nil { + return nil, err + } + + runtime.SetFinalizer(img, func(img *Image) { + if !img.deleted { + b.glChan <- func() { + b.glctx.DeleteTexture(img.tex) + } + } + }) + + return img, nil +} + +func loadImage(b *XMobileBackend, src image.Image, tex gl.Texture) (*Image, error) { + var img *Image + var err error + switch v := src.(type) { + case *image.RGBA: + img, err = loadImageRGBA(b, v, tex) + if err != nil { + return nil, err + } + case *image.Gray: + img, err = loadImageGray(b, v, tex) + if err != nil { + return nil, err + } + case image.Image: + img, err = loadImageConverted(b, v, tex) + if err != nil { + return nil, err + } + default: + return nil, errors.New("Unsupported source type") + } + return img, nil +} + +func loadImageRGBA(b *XMobileBackend, src *image.RGBA, tex gl.Texture) (*Image, error) { + img := &Image{tex: tex, w: src.Bounds().Dx(), h: src.Bounds().Dy(), opaque: true} + +checkOpaque: + for y := 0; y < img.h; y++ { + off := src.PixOffset(0, y) + 3 + for x := 0; x < img.w; x++ { + if src.Pix[off] < 255 { + img.opaque = false + break checkOpaque + } + off += 4 + } + } + + b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR) + b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + 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) + if err := glError(b); err != nil { + return nil, err + } + if src.Stride == img.w*4 { + b.glctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, img.w, img.h, gl.RGBA, gl.UNSIGNED_BYTE, src.Pix[0:]) + } else { + data := make([]uint8, 0, img.w*img.h*4) + for y := 0; y < img.h; y++ { + start := y * src.Stride + end := start + img.w*4 + data = append(data, src.Pix[start:end]...) + } + b.glctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, img.w, img.h, gl.RGBA, gl.UNSIGNED_BYTE, data[0:]) + } + if err := glError(b); err != nil { + return nil, err + } + b.glctx.GenerateMipmap(gl.TEXTURE_2D) + if err := glError(b); err != nil { + return nil, err + } + return img, nil +} + +func loadImageGray(b *XMobileBackend, src *image.Gray, tex gl.Texture) (*Image, error) { + img := &Image{tex: tex, w: src.Bounds().Dx(), h: src.Bounds().Dy()} + b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR) + b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + 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) + if err := glError(b); err != nil { + return nil, err + } + if src.Stride == img.w { + b.glctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, img.w, img.h, gl.RED, gl.UNSIGNED_BYTE, src.Pix[0:]) + } else { + data := make([]uint8, 0, img.w*img.h) + for y := 0; y < img.h; y++ { + start := y * src.Stride + end := start + img.w + data = append(data, src.Pix[start:end]...) + } + b.glctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RED, img.w, img.h, gl.RED, gl.UNSIGNED_BYTE, data[0:]) + } + if err := glError(b); err != nil { + return nil, err + } + b.glctx.GenerateMipmap(gl.TEXTURE_2D) + if err := glError(b); err != nil { + return nil, err + } + return img, nil +} + +func loadImageConverted(b *XMobileBackend, src image.Image, tex gl.Texture) (*Image, error) { + img := &Image{tex: tex, w: src.Bounds().Dx(), h: src.Bounds().Dy(), opaque: true} + b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR) + b.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + 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) + if err := glError(b); err != nil { + return nil, err + } + data := make([]uint8, 0, img.w*img.h*4) + for y := 0; y < img.h; y++ { + for x := 0; x < img.w; x++ { + ir, ig, ib, ia := src.At(x, y).RGBA() + r, g, b, a := uint8(ir>>8), uint8(ig>>8), uint8(ib>>8), uint8(ia>>8) + data = append(data, r, g, b, a) + if a < 255 { + img.opaque = false + } + } + } + b.glctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, img.w, img.h, gl.RGBA, gl.UNSIGNED_BYTE, data[0:]) + if err := glError(b); err != nil { + return nil, err + } + b.glctx.GenerateMipmap(gl.TEXTURE_2D) + if err := glError(b); err != nil { + return nil, err + } + return img, nil +} + +// Width returns the width of the image +func (img *Image) Width() int { return img.w } + +// Height returns the height of the image +func (img *Image) Height() int { return img.h } + +// Size returns the width and height of the image +func (img *Image) Size() (int, int) { return img.w, img.h } + +// Delete deletes the image from memory. Any draw calls +// with a deleted image will not do anything +func (img *Image) Delete() { + b := img.b + img.b.activate() + + b.glctx.DeleteTexture(img.tex) + img.deleted = true +} + +// IsDeleted returns true if the Delete function has been +// called on this image +func (img *Image) IsDeleted() bool { return img.deleted } + +// Replace replaces the image with the new one +func (img *Image) Replace(src image.Image) error { + b := img.b + img.b.activate() + + b.glctx.ActiveTexture(gl.TEXTURE0) + b.glctx.BindTexture(gl.TEXTURE_2D, img.tex) + newImg, err := loadImage(b, src, img.tex) + if err != nil { + return err + } + *img = *newImg + return nil +} + +// IsOpaque returns true if all pixels in the image +// have a full alpha value +func (img *Image) IsOpaque() bool { return img.opaque } + +func (b *XMobileBackend) DrawImage(dimg backendbase.Image, sx, sy, sw, sh float64, pts [4][2]float64, alpha float64) { + b.activate() + + img := dimg.(*Image) + + sx /= float64(img.w) + sy /= float64(img.h) + sw /= float64(img.w) + sh /= float64(img.h) + + var buf [16]float32 + data := buf[:0] + for _, pt := range pts { + data = append(data, float32(pt[0]), float32(pt[1])) + } + data = append(data, + float32(sx), float32(sy), + float32(sx), float32(sy+sh), + float32(sx+sw), float32(sy+sh), + float32(sx+sw), float32(sy), + ) + + b.glctx.StencilFunc(gl.EQUAL, 0, 0xFF) + + b.glctx.BindBuffer(gl.ARRAY_BUFFER, b.buf) + b.glctx.BufferData(gl.ARRAY_BUFFER, byteSlice(unsafe.Pointer(&data[0]), len(data)*4), gl.STREAM_DRAW) + + b.glctx.ActiveTexture(gl.TEXTURE0) + b.glctx.BindTexture(gl.TEXTURE_2D, img.tex) + + b.glctx.UseProgram(b.ir.ID) + b.glctx.Uniform1i(b.ir.Image, 0) + b.glctx.Uniform2f(b.ir.CanvasSize, float32(b.fw), float32(b.fh)) + b.glctx.Uniform1f(b.ir.GlobalAlpha, float32(alpha)) + b.glctx.VertexAttribPointer(b.ir.Vertex, 2, gl.FLOAT, false, 0, 0) + b.glctx.VertexAttribPointer(b.ir.TexCoord, 2, gl.FLOAT, false, 0, 8*4) + b.glctx.EnableVertexAttribArray(b.ir.Vertex) + b.glctx.EnableVertexAttribArray(b.ir.TexCoord) + b.glctx.DrawArrays(gl.TRIANGLE_FAN, 0, 4) + b.glctx.DisableVertexAttribArray(b.ir.Vertex) + b.glctx.DisableVertexAttribArray(b.ir.TexCoord) + + b.glctx.StencilFunc(gl.ALWAYS, 0, 0xFF) +} diff --git a/backend/xmobile/shader.go b/backend/xmobile/shader.go new file mode 100755 index 0000000..254ed55 --- /dev/null +++ b/backend/xmobile/shader.go @@ -0,0 +1,180 @@ +package xmobilebackend + +import ( + "errors" + "fmt" + "reflect" + "strings" + "unicode" + "unicode/utf8" + + "golang.org/x/mobile/gl" +) + +type shaderProgram struct { + b *XMobileBackend + ID gl.Program + vs, fs gl.Shader + + attribs map[string]gl.Attrib + uniforms map[string]gl.Uniform +} + +func loadShader(b *XMobileBackend, vs, fs string, sp *shaderProgram) error { + sp.b = b + glError(b) // clear the current error + + // compile vertex shader + { + sp.vs = b.glctx.CreateShader(gl.VERTEX_SHADER) + b.glctx.ShaderSource(sp.vs, vs) + b.glctx.CompileShader(sp.vs) + + status := b.glctx.GetShaderi(sp.vs, gl.COMPILE_STATUS) + if status != gl.TRUE { + clog := b.glctx.GetShaderInfoLog(sp.vs) + b.glctx.DeleteShader(sp.vs) + return fmt.Errorf("failed to compile vertex shader:\n\n%s", clog) + } + if err := glError(b); err != nil { + return fmt.Errorf("gl error after compiling vertex shader: %v", err) + } + } + + // compile fragment shader + { + sp.fs = b.glctx.CreateShader(gl.FRAGMENT_SHADER) + b.glctx.ShaderSource(sp.fs, fs) + b.glctx.CompileShader(sp.fs) + + status := b.glctx.GetShaderi(sp.fs, gl.COMPILE_STATUS) + if status != gl.TRUE { + clog := b.glctx.GetShaderInfoLog(sp.fs) + b.glctx.DeleteShader(sp.fs) + return fmt.Errorf("failed to compile fragment shader:\n\n%s", clog) + } + if err := glError(b); err != nil { + return fmt.Errorf("gl error after compiling fragment shader: %v", err) + } + } + + // link shader program + { + sp.ID = b.glctx.CreateProgram() + b.glctx.AttachShader(sp.ID, sp.vs) + b.glctx.AttachShader(sp.ID, sp.fs) + b.glctx.LinkProgram(sp.ID) + + status := b.glctx.GetProgrami(sp.ID, gl.LINK_STATUS) + if status != gl.TRUE { + clog := b.glctx.GetProgramInfoLog(sp.ID) + b.glctx.DeleteProgram(sp.ID) + b.glctx.DeleteShader(sp.vs) + b.glctx.DeleteShader(sp.fs) + return fmt.Errorf("failed to link shader program:\n\n%s", clog) + } + if err := glError(b); err != nil { + return fmt.Errorf("gl error after linking shader: %v", err) + } + } + + b.glctx.UseProgram(sp.ID) + // load the attributes + count := b.glctx.GetProgrami(sp.ID, gl.ACTIVE_ATTRIBUTES) + sp.attribs = make(map[string]gl.Attrib, int(count)) + for i := 0; i < count; i++ { + name, _, _ := b.glctx.GetActiveAttrib(sp.ID, uint32(i)) + sp.attribs[name] = b.glctx.GetAttribLocation(sp.ID, name) + } + + // load the uniforms + count = b.glctx.GetProgrami(sp.ID, gl.ACTIVE_UNIFORMS) + sp.uniforms = make(map[string]gl.Uniform, int(count)) + for i := 0; i < count; i++ { + name, _, _ := b.glctx.GetActiveUniform(sp.ID, uint32(i)) + sp.uniforms[name] = b.glctx.GetUniformLocation(sp.ID, name) + } + + return nil +} + +func (sp *shaderProgram) use() { + sp.b.glctx.UseProgram(sp.ID) +} + +func (sp *shaderProgram) delete() { + sp.b.glctx.DeleteProgram(sp.ID) + sp.b.glctx.DeleteShader(sp.vs) + sp.b.glctx.DeleteShader(sp.fs) +} + +func (sp *shaderProgram) loadLocations(target interface{}) error { + val := reflect.ValueOf(target) + if val.Kind() != reflect.Ptr { + panic("target must be a pointer to a struct") + } + val = val.Elem() + if val.Kind() != reflect.Struct { + panic("target must be a pointer to a struct") + } + + sp.b.glctx.UseProgram(sp.ID) + + var errs strings.Builder + + for name, loc := range sp.attribs { + field := val.FieldByName(sp.structName(name)) + if field == (reflect.Value{}) { + fmt.Fprintf(&errs, "field for attribute \"%s\" not found; ", name) + } else if field.Type() != reflect.TypeOf(gl.Attrib{}) { + fmt.Fprintf(&errs, "field for attribute \"%s\" must have type gl.Attrib; ", name) + } else { + field.Set(reflect.ValueOf(loc)) + } + } + + for name, loc := range sp.uniforms { + field := val.FieldByName(sp.structName(name)) + if field == (reflect.Value{}) { + fmt.Fprintf(&errs, "field for uniform \"%s\" not found; ", name) + } else if field.Type() != reflect.TypeOf(gl.Uniform{}) { + fmt.Fprintf(&errs, "field for uniform \"%s\" must have type gl.Uniform; ", name) + } else { + field.Set(reflect.ValueOf(loc)) + } + } + + if errs.Len() > 0 { + return errors.New(strings.TrimSpace(errs.String())) + } + return nil +} + +func (sp *shaderProgram) structName(name string) string { + rn, sz := utf8.DecodeRuneInString(name) + name = fmt.Sprintf("%c%s", unicode.ToUpper(rn), name[sz:]) + idx := strings.IndexByte(name, '[') + if idx > 0 { + name = name[:idx] + } + return name +} + +func (sp *shaderProgram) mustLoadLocations(target interface{}) { + err := sp.loadLocations(target) + if err != nil { + panic(err) + } +} + +func (sp *shaderProgram) enableAllVertexAttribArrays() { + for _, loc := range sp.attribs { + sp.b.glctx.EnableVertexAttribArray(loc) + } +} + +func (sp *shaderProgram) disableAllVertexAttribArrays() { + for _, loc := range sp.attribs { + sp.b.glctx.DisableVertexAttribArray(loc) + } +} diff --git a/backend/xmobile/shaders.go b/backend/xmobile/shaders.go new file mode 100755 index 0000000..9112465 --- /dev/null +++ b/backend/xmobile/shaders.go @@ -0,0 +1,468 @@ +package xmobilebackend + +import ( + "bytes" + "fmt" + "strings" + + "golang.org/x/mobile/gl" +) + +var imageVS = ` +attribute vec2 vertex, texCoord; +uniform vec2 canvasSize; +varying vec2 v_texCoord; +void main() { + v_texCoord = texCoord; + vec2 glp = vertex * 2.0 / canvasSize - 1.0; + gl_Position = vec4(glp.x, -glp.y, 0.0, 1.0); +}` +var imageFS = ` +#ifdef GL_ES +precision mediump float; +#endif +varying vec2 v_texCoord; +uniform sampler2D image; +uniform float globalAlpha; +void main() { + vec4 col = texture2D(image, v_texCoord); + col.a *= globalAlpha; + gl_FragColor = col; +}` + +var solidVS = ` +attribute vec2 vertex; +uniform vec2 canvasSize; +void main() { + vec2 glp = vertex * 2.0 / canvasSize - 1.0; + gl_Position = vec4(glp.x, -glp.y, 0.0, 1.0); +}` +var solidFS = ` +#ifdef GL_ES +precision mediump float; +#endif +uniform vec4 color; +uniform float globalAlpha; +void main() { + vec4 col = color; + col.a *= globalAlpha; + gl_FragColor = col; +}` + +var linearGradientVS = ` +attribute vec2 vertex; +uniform vec2 canvasSize; +varying vec2 v_cp; +void main() { + v_cp = vertex; + vec2 glp = vertex * 2.0 / canvasSize - 1.0; + gl_Position = vec4(glp.x, -glp.y, 0.0, 1.0); +}` +var linearGradientFS = ` +#ifdef GL_ES +precision mediump float; +#endif +varying vec2 v_cp; +uniform sampler2D gradient; +uniform vec2 from, dir; +uniform float len; +uniform float globalAlpha; +void main() { + vec2 v = v_cp - from; + float r = dot(v, dir) / len; + r = clamp(r, 0.0, 1.0); + vec4 col = texture2D(gradient, vec2(r, 0.0)); + col.a *= globalAlpha; + gl_FragColor = col; +}` + +var radialGradientVS = ` +attribute vec2 vertex; +uniform vec2 canvasSize; +varying vec2 v_cp; +void main() { + v_cp = vertex; + vec2 glp = vertex * 2.0 / canvasSize - 1.0; + gl_Position = vec4(glp.x, -glp.y, 0.0, 1.0); +}` +var radialGradientFS = ` +#ifdef GL_ES +precision mediump float; +#endif +varying vec2 v_cp; +uniform sampler2D gradient; +uniform vec2 from, to; +uniform float radFrom, radTo; +uniform float globalAlpha; +bool isNaN(float v) { + return v < 0.0 || 0.0 < v || v == 0.0 ? false : true; +} +void main() { + float o_a = 0.5 * sqrt( + pow(-2.0*from.x*from.x+2.0*from.x*to.x+2.0*from.x*v_cp.x-2.0*to.x*v_cp.x-2.0*from.y*from.y+2.0*from.y*to.y+2.0*from.y*v_cp.y-2.0*to.y*v_cp.y+2.0*radFrom*radFrom-2.0*radFrom*radTo, 2.0) + -4.0*(from.x*from.x-2.0*from.x*v_cp.x+v_cp.x*v_cp.x+from.y*from.y-2.0*from.y*v_cp.y+v_cp.y*v_cp.y-radFrom*radFrom) + *(from.x*from.x-2.0*from.x*to.x+to.x*to.x+from.y*from.y-2.0*from.y*to.y+to.y*to.y-radFrom*radFrom+2.0*radFrom*radTo-radTo*radTo) + ); + float o_b = (from.x*from.x-from.x*to.x-from.x*v_cp.x+to.x*v_cp.x+from.y*from.y-from.y*to.y-from.y*v_cp.y+to.y*v_cp.y-radFrom*radFrom+radFrom*radTo); + float o_c = (from.x*from.x-2.0*from.x*to.x+to.x*to.x+from.y*from.y-2.0*from.y*to.y+to.y*to.y-radFrom*radFrom+2.0*radFrom*radTo-radTo*radTo); + float o1 = (-o_a + o_b) / o_c; + float o2 = (o_a + o_b) / o_c; + if (isNaN(o1) && isNaN(o2)) { + gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); + return; + } + float o = max(o1, o2); + //float r = radFrom + o * (radTo - radFrom); + vec4 col = texture2D(gradient, vec2(o, 0.0)); + col.a *= globalAlpha; + gl_FragColor = col; +}` + +var imagePatternVS = ` +attribute vec2 vertex; +uniform vec2 canvasSize; +varying vec2 v_cp; +void main() { + v_cp = vertex; + vec2 glp = vertex * 2.0 / canvasSize - 1.0; + gl_Position = vec4(glp.x, -glp.y, 0.0, 1.0); +}` +var imagePatternFS = ` +#ifdef GL_ES +precision mediump float; +#endif +varying vec2 v_cp; +uniform vec2 imageSize; +uniform sampler2D image; +uniform float globalAlpha; +void main() { + vec4 col = texture2D(image, mod(v_cp / imageSize, 1.0)); + col.a *= globalAlpha; + gl_FragColor = col; +}` + +var solidAlphaVS = ` +attribute vec2 vertex, alphaTexCoord; +uniform vec2 canvasSize; +varying vec2 v_atc; +void main() { + v_atc = alphaTexCoord; + vec2 glp = vertex * 2.0 / canvasSize - 1.0; + gl_Position = vec4(glp.x, -glp.y, 0.0, 1.0); +}` +var solidAlphaFS = ` +#ifdef GL_ES +precision mediump float; +#endif +varying vec2 v_atc; +uniform vec4 color; +uniform sampler2D alphaTex; +uniform float globalAlpha; +void main() { + vec4 col = color; + col.a *= texture2D(alphaTex, v_atc).a * globalAlpha; + gl_FragColor = col; +}` + +var linearGradientAlphaVS = ` +attribute vec2 vertex, alphaTexCoord; +uniform vec2 canvasSize; +varying vec2 v_cp; +varying vec2 v_atc; +void main() { + v_cp = vertex; + v_atc = alphaTexCoord; + vec2 glp = vertex * 2.0 / canvasSize - 1.0; + gl_Position = vec4(glp.x, -glp.y, 0.0, 1.0); +}` +var linearGradientAlphaFS = ` +#ifdef GL_ES +precision mediump float; +#endif +varying vec2 v_cp; +varying vec2 v_atc; +varying vec2 v_texCoord; +uniform sampler2D gradient; +uniform vec2 from, dir; +uniform float len; +uniform sampler2D alphaTex; +uniform float globalAlpha; +void main() { + vec2 v = v_cp - from; + float r = dot(v, dir) / len; + r = clamp(r, 0.0, 1.0); + vec4 col = texture2D(gradient, vec2(r, 0.0)); + col.a *= texture2D(alphaTex, v_atc).a * globalAlpha; + gl_FragColor = col; +}` + +var radialGradientAlphaVS = ` +attribute vec2 vertex, alphaTexCoord; +uniform vec2 canvasSize; +varying vec2 v_cp; +varying vec2 v_atc; +void main() { + v_cp = vertex; + v_atc = alphaTexCoord; + vec2 glp = vertex * 2.0 / canvasSize - 1.0; + gl_Position = vec4(glp.x, -glp.y, 0.0, 1.0); +}` +var radialGradientAlphaFS = ` +#ifdef GL_ES +precision mediump float; +#endif +varying vec2 v_cp; +varying vec2 v_atc; +uniform sampler2D gradient; +uniform vec2 from, to; +uniform float radFrom, radTo; +uniform sampler2D alphaTex; +uniform float globalAlpha; +bool isNaN(float v) { + return v < 0.0 || 0.0 < v || v == 0.0 ? false : true; +} +void main() { + float o_a = 0.5 * sqrt( + pow(-2.0*from.x*from.x+2.0*from.x*to.x+2.0*from.x*v_cp.x-2.0*to.x*v_cp.x-2.0*from.y*from.y+2.0*from.y*to.y+2.0*from.y*v_cp.y-2.0*to.y*v_cp.y+2.0*radFrom*radFrom-2.0*radFrom*radTo, 2.0) + -4.0*(from.x*from.x-2.0*from.x*v_cp.x+v_cp.x*v_cp.x+from.y*from.y-2.0*from.y*v_cp.y+v_cp.y*v_cp.y-radFrom*radFrom) + *(from.x*from.x-2.0*from.x*to.x+to.x*to.x+from.y*from.y-2.0*from.y*to.y+to.y*to.y-radFrom*radFrom+2.0*radFrom*radTo-radTo*radTo) + ); + float o_b = (from.x*from.x-from.x*to.x-from.x*v_cp.x+to.x*v_cp.x+from.y*from.y-from.y*to.y-from.y*v_cp.y+to.y*v_cp.y-radFrom*radFrom+radFrom*radTo); + float o_c = (from.x*from.x-2.0*from.x*to.x+to.x*to.x+from.y*from.y-2.0*from.y*to.y+to.y*to.y-radFrom*radFrom+2.0*radFrom*radTo-radTo*radTo); + float o1 = (-o_a + o_b) / o_c; + float o2 = (o_a + o_b) / o_c; + if (isNaN(o1) && isNaN(o2)) { + gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); + return; + } + float o = max(o1, o2); + float r = radFrom + o * (radTo - radFrom); + vec4 col = texture2D(gradient, vec2(o, 0.0)); + col.a *= texture2D(alphaTex, v_atc).a * globalAlpha; + gl_FragColor = col; +}` + +var imagePatternAlphaVS = ` +attribute vec2 vertex, alphaTexCoord; +uniform vec2 canvasSize; +varying vec2 v_cp; +varying vec2 v_atc; +void main() { + v_cp = vertex; + v_atc = alphaTexCoord; + vec2 glp = vertex * 2.0 / canvasSize - 1.0; + gl_Position = vec4(glp.x, -glp.y, 0.0, 1.0); +}` +var imagePatternAlphaFS = ` +#ifdef GL_ES +precision mediump float; +#endif +varying vec2 v_cp; +varying vec2 v_atc; +uniform vec2 imageSize; +uniform sampler2D image; +uniform sampler2D alphaTex; +uniform float globalAlpha; +void main() { + vec4 col = texture2D(image, mod(v_cp / imageSize, 1.0)); + col.a *= texture2D(alphaTex, v_atc).a * globalAlpha; + gl_FragColor = col; +}` + +var gaussian15VS = ` +attribute vec2 vertex, texCoord; +uniform vec2 canvasSize; +varying vec2 v_texCoord; +void main() { + v_texCoord = texCoord; + vec2 glp = vertex * 2.0 / canvasSize - 1.0; + gl_Position = vec4(glp.x, -glp.y, 0.0, 1.0); +}` +var gaussian15FS = ` +#ifdef GL_ES +precision mediump float; +#endif +varying vec2 v_texCoord; +uniform vec2 kernelScale; +uniform sampler2D image; +uniform float kernel[15]; +void main() { + vec4 color = vec4(0.0, 0.0, 0.0, 0.0); +_SUM_ + gl_FragColor = color; +}` + +var gaussian63VS = ` +attribute vec2 vertex, texCoord; +uniform vec2 canvasSize; +varying vec2 v_texCoord; +void main() { + v_texCoord = texCoord; + vec2 glp = vertex * 2.0 / canvasSize - 1.0; + gl_Position = vec4(glp.x, -glp.y, 0.0, 1.0); +}` +var gaussian63FS = ` +#ifdef GL_ES +precision mediump float; +#endif +varying vec2 v_texCoord; +uniform vec2 kernelScale; +uniform sampler2D image; +uniform float kernel[63]; +void main() { + vec4 color = vec4(0.0, 0.0, 0.0, 0.0); +_SUM_ + gl_FragColor = color; +}` + +var gaussian127VS = ` +attribute vec2 vertex, texCoord; +uniform vec2 canvasSize; +varying vec2 v_texCoord; +void main() { + v_texCoord = texCoord; + vec2 glp = vertex * 2.0 / canvasSize - 1.0; + gl_Position = vec4(glp.x, -glp.y, 0.0, 1.0); +}` +var gaussian127FS = ` +#ifdef GL_ES +precision mediump float; +#endif +varying vec2 v_texCoord; +uniform vec2 kernelScale; +uniform sampler2D image; +uniform float kernel[127]; +void main() { + vec4 color = vec4(0.0, 0.0, 0.0, 0.0); +_SUM_ + gl_FragColor = color; +}` + +func init() { + fstr := "\tcolor += texture2D(image, v_texCoord + vec2(%.1f * kernelScale.x, %.1f * kernelScale.y)) * kernel[%d];\n" + bb := bytes.Buffer{} + for i := 0; i < 127; i++ { + off := float64(i) - 63 + fmt.Fprintf(&bb, fstr, off, off, i) + } + gaussian127FS = strings.Replace(gaussian127FS, "_SUM_", bb.String(), -1) + bb.Reset() + for i := 0; i < 63; i++ { + off := float64(i) - 31 + fmt.Fprintf(&bb, fstr, off, off, i) + } + gaussian63FS = strings.Replace(gaussian63FS, "_SUM_", bb.String(), -1) + bb.Reset() + for i := 0; i < 15; i++ { + off := float64(i) - 7 + fmt.Fprintf(&bb, fstr, off, off, i) + } + gaussian15FS = strings.Replace(gaussian15FS, "_SUM_", bb.String(), -1) +} + +type solidShader struct { + shaderProgram + Vertex gl.Attrib + CanvasSize gl.Uniform + Color gl.Uniform + GlobalAlpha gl.Uniform +} + +type imageShader struct { + shaderProgram + Vertex gl.Attrib + TexCoord gl.Attrib + CanvasSize gl.Uniform + Image gl.Uniform + GlobalAlpha gl.Uniform +} + +type linearGradientShader struct { + shaderProgram + Vertex gl.Attrib + CanvasSize gl.Uniform + Gradient gl.Uniform + From gl.Uniform + Dir gl.Uniform + Len gl.Uniform + GlobalAlpha gl.Uniform +} + +type radialGradientShader struct { + shaderProgram + Vertex gl.Attrib + CanvasSize gl.Uniform + Gradient gl.Uniform + From gl.Uniform + To gl.Uniform + RadFrom gl.Uniform + RadTo gl.Uniform + GlobalAlpha gl.Uniform +} + +type imagePatternShader struct { + shaderProgram + Vertex gl.Attrib + CanvasSize gl.Uniform + ImageSize gl.Uniform + Image gl.Uniform + GlobalAlpha gl.Uniform +} + +type solidAlphaShader struct { + shaderProgram + Vertex gl.Attrib + AlphaTexCoord gl.Attrib + CanvasSize gl.Uniform + Color gl.Uniform + AlphaTex gl.Uniform + GlobalAlpha gl.Uniform +} + +type linearGradientAlphaShader struct { + shaderProgram + Vertex gl.Attrib + AlphaTexCoord gl.Attrib + CanvasSize gl.Uniform + Gradient gl.Uniform + From gl.Uniform + Dir gl.Uniform + Len gl.Uniform + AlphaTex gl.Uniform + GlobalAlpha gl.Uniform +} + +type radialGradientAlphaShader struct { + shaderProgram + Vertex gl.Attrib + AlphaTexCoord gl.Attrib + CanvasSize gl.Uniform + Gradient gl.Uniform + From gl.Uniform + To gl.Uniform + RadFrom gl.Uniform + RadTo gl.Uniform + AlphaTex gl.Uniform + GlobalAlpha gl.Uniform +} + +type imagePatternAlphaShader struct { + shaderProgram + Vertex gl.Attrib + AlphaTexCoord gl.Attrib + CanvasSize gl.Uniform + ImageSize gl.Uniform + Image gl.Uniform + AlphaTex gl.Uniform + GlobalAlpha gl.Uniform +} + +type gaussianShader struct { + shaderProgram + Vertex gl.Attrib + TexCoord gl.Attrib + CanvasSize gl.Uniform + KernelScale gl.Uniform + Image gl.Uniform + Kernel gl.Uniform +} diff --git a/backend/xmobile/xmobile.go b/backend/xmobile/xmobile.go new file mode 100755 index 0000000..83ce1c1 --- /dev/null +++ b/backend/xmobile/xmobile.go @@ -0,0 +1,498 @@ +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 + +type XMobileBackend struct { + glctx gl.Context + + x, y, w, h int + fx, fy, fw, fh float64 + + 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 + + offscreen bool + offscrBuf offscreenBuffer + offscrImg Image + + glChan chan func() +} + +type offscreenBuffer struct { + tex gl.Texture + w int + h int + renderStencilBuf gl.Renderbuffer + frameBuf gl.Framebuffer + alpha bool +} + +func New(glctx gl.Context, x, y, w, h int) (*XMobileBackend, error) { + var err error + + b := &XMobileBackend{ + glctx: glctx, + + w: w, + h: h, + fw: float64(w), + fh: float64(h), + ptsBuf: make([]float32, 0, 4096), + } + + b.glctx.GetError() // clear error state + + err = loadShader(b, solidVS, solidFS, &b.sr.shaderProgram) + if err != nil { + return nil, err + } + b.sr.shaderProgram.mustLoadLocations(&b.sr) + if err = glError(b); err != nil { + return nil, err + } + + err = loadShader(b, linearGradientVS, linearGradientFS, &b.lgr.shaderProgram) + if err != nil { + return nil, err + } + b.lgr.shaderProgram.mustLoadLocations(&b.lgr) + if err = glError(b); err != nil { + return nil, err + } + + err = loadShader(b, radialGradientVS, radialGradientFS, &b.rgr.shaderProgram) + if err != nil { + return nil, err + } + b.rgr.shaderProgram.mustLoadLocations(&b.rgr) + if err = glError(b); err != nil { + return nil, err + } + + err = loadShader(b, imagePatternVS, imagePatternFS, &b.ipr.shaderProgram) + if err != nil { + return nil, err + } + b.ipr.shaderProgram.mustLoadLocations(&b.ipr) + if err = glError(b); err != nil { + return nil, err + } + + err = loadShader(b, solidAlphaVS, solidAlphaFS, &b.sar.shaderProgram) + if err != nil { + return nil, err + } + b.sar.shaderProgram.mustLoadLocations(&b.sar) + if err = glError(b); err != nil { + return nil, err + } + + err = loadShader(b, linearGradientAlphaVS, linearGradientFS, &b.lgar.shaderProgram) + if err != nil { + return nil, err + } + b.lgar.shaderProgram.mustLoadLocations(&b.lgar) + if err = glError(b); err != nil { + return nil, err + } + + err = loadShader(b, radialGradientAlphaVS, radialGradientAlphaFS, &b.rgar.shaderProgram) + if err != nil { + return nil, err + } + b.rgar.shaderProgram.mustLoadLocations(&b.rgar) + if err = glError(b); err != nil { + return nil, err + } + + err = loadShader(b, imagePatternAlphaVS, imagePatternAlphaFS, &b.ipar.shaderProgram) + if err != nil { + return nil, err + } + b.ipar.shaderProgram.mustLoadLocations(&b.ipar) + if err = glError(b); err != nil { + return nil, err + } + + err = loadShader(b, imageVS, imageFS, &b.ir.shaderProgram) + if err != nil { + return nil, err + } + b.ir.shaderProgram.mustLoadLocations(&b.ir) + if err = glError(b); err != nil { + return nil, err + } + + err = loadShader(b, gaussian15VS, gaussian15FS, &b.gauss15r.shaderProgram) + if err != nil { + return nil, err + } + b.gauss15r.shaderProgram.mustLoadLocations(&b.gauss15r) + if err = glError(b); err != nil { + return nil, err + } + + err = loadShader(b, gaussian63VS, gaussian63FS, &b.gauss63r.shaderProgram) + if err != nil { + return nil, err + } + b.gauss63r.shaderProgram.mustLoadLocations(&b.gauss63r) + if err = glError(b); err != nil { + return nil, err + } + + err = loadShader(b, gaussian127VS, gaussian127FS, &b.gauss127r.shaderProgram) + if err != nil { + return nil, err + } + b.gauss127r.shaderProgram.mustLoadLocations(&b.gauss127r) + if err = glError(b); err != nil { + return nil, err + } + + b.buf = b.glctx.CreateBuffer() + if err = glError(b); err != nil { + return nil, err + } + + b.shadowBuf = b.glctx.CreateBuffer() + if err = glError(b); err != nil { + return nil, err + } + + b.glctx.ActiveTexture(gl.TEXTURE0) + b.alphaTex = b.glctx.CreateTexture() + b.glctx.BindTexture(gl.TEXTURE_2D, b.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) + + 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 b, nil +} + +func NewOffscreen(glctx gl.Context, w, h int, alpha bool) (*XMobileBackend, error) { + b, err := New(glctx, 0, 0, w, h) + if err != nil { + return nil, err + } + b.offscreen = true + b.offscrBuf.alpha = alpha + return b, 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) { + if !b.offscreen { + 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) + } +} + +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 +} + +var activeContext *XMobileBackend + +func (b *XMobileBackend) activate() { + if activeContext != b { + activeContext = b + if b.offscreen { + b.glctx.Viewport(0, 0, b.w, b.h) + b.enableTextureRenderTarget(&b.offscrBuf) + b.offscrImg.w = b.offscrBuf.w + b.offscrImg.h = b.offscrBuf.h + b.offscrImg.tex = b.offscrBuf.tex + } else { + b.glctx.Viewport(b.x, b.y, b.w, b.h) + b.disableTextureRenderTarget() + } + } + +loop: + for { + select { + case f := <-b.glChan: + f() + default: + break loop + } + } +} + +func (b *XMobileBackend) DeleteOffscreen() { + if !b.offscreen { + return + } + b.glctx.DeleteTexture(b.offscrBuf.tex) + b.glctx.DeleteFramebuffer(b.offscrBuf.frameBuf) + b.glctx.DeleteRenderbuffer(b.offscrBuf.renderStencilBuf) + b.offscreen = false + + b.activate() +} + +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 img := style.Image; img != nil { + img := img.(*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) + 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 img := style.Image; img != nil { + img := img.(*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) + 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 { + 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.DEPTH24_STENCIL8, b.w, b.h) + b.glctx.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_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) + } else { + b.glctx.BindFramebuffer(gl.FRAMEBUFFER, offscr.frameBuf) + } +} + +func (b *XMobileBackend) disableTextureRenderTarget() { + if b.offscreen { + b.enableTextureRenderTarget(&b.offscrBuf) + } else { + b.glctx.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{Value: 0}) + } +} + +type vec [2]float64 + +func (v1 vec) sub(v2 vec) vec { + return vec{v1[0] - v2[0], v1[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 +} diff --git a/examples/shiny/shiny.go b/examples/shiny/shiny.go index e0def8b..0568fa1 100644 --- a/examples/shiny/shiny.go +++ b/examples/shiny/shiny.go @@ -4,7 +4,7 @@ import ( "log" "github.com/tfriedel6/canvas" - "github.com/tfriedel6/canvas/glimpl/xmobile" + "github.com/tfriedel6/canvas/backend/xmobile" "golang.org/x/exp/shiny/driver/gldriver" "golang.org/x/exp/shiny/screen" "golang.org/x/exp/shiny/widget" @@ -19,10 +19,13 @@ func main() { gldriver.Main(func(s screen.Screen) { glw := glwidget.NewGL(draw) sheet = widget.NewSheet(glw) - canvas.LoadGL(glimplxmobile.New(glw.Ctx)) - cv = canvas.New(0, 0, 600, 600) + backend, err := xmobilebackend.New(glw.Ctx, 0, 0, 600, 600) + if err != nil { + log.Fatal(err) + } + cv = canvas.New(backend) - err := widget.RunWindow(s, sheet, &widget.RunWindowOptions{ + err = widget.RunWindow(s, sheet, &widget.RunWindowOptions{ NewWindowOptions: screen.NewWindowOptions{ Title: "Shiny Canvas Example", Width: 600,