From 1a0d92267b1a021a1e0f4636fc794e8782316f75 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 18 May 2020 03:45:58 +0900 Subject: [PATCH] driver: Add shader API and implement it on OpenGL Updates #482 --- internal/driver/graphics.go | 16 + internal/graphicscommand/command.go | 145 +++++++-- internal/graphicscommand/export_test.go | 19 ++ internal/graphicscommand/image.go | 30 +- internal/graphicscommand/image_test.go | 241 ++++++++++++++ internal/graphicscommand/shader.go | 41 +++ internal/graphicsdriver/metal/graphics.go | 9 + .../graphicsdriver/opengl/defaultshader.go | 286 ++++++++++++++++ internal/graphicsdriver/opengl/graphics.go | 61 +++- internal/graphicsdriver/opengl/shader.go | 308 +++--------------- 10 files changed, 864 insertions(+), 292 deletions(-) create mode 100644 internal/graphicscommand/export_test.go create mode 100644 internal/graphicscommand/shader.go create mode 100644 internal/graphicsdriver/opengl/defaultshader.go diff --git a/internal/driver/graphics.go b/internal/driver/graphics.go index cd6e68e0a..797736153 100644 --- a/internal/driver/graphics.go +++ b/internal/driver/graphics.go @@ -18,6 +18,7 @@ import ( "errors" "github.com/hajimehoshi/ebiten/internal/affine" + "github.com/hajimehoshi/ebiten/internal/shaderir" "github.com/hajimehoshi/ebiten/internal/thread" ) @@ -37,6 +38,14 @@ type Graphics interface { IsGL() bool HasHighPrecisionFloat() bool MaxImageSize() int + + NewShader(program *shaderir.Program) (Shader, error) + + // DrawShader draws the shader. + // + // uniforms represents a colletion of uniform variables. The values must be one of these types: + // float32, []float32, or ImageID. + DrawShader(dst ImageID, shader ShaderID, indexLen int, indexOffset int, mode CompositeMode, uniforms map[int]interface{}) error } // GraphicsNotReady represents that the graphics driver is not ready for recovering from the context lost. @@ -66,3 +75,10 @@ const ( Upward YDirection = iota Downward ) + +type Shader interface { + ID() ShaderID + Dispose() +} + +type ShaderID int diff --git a/internal/graphicscommand/command.go b/internal/graphicscommand/command.go index 1cf0b3c6b..a1455d18e 100644 --- a/internal/graphicscommand/command.go +++ b/internal/graphicscommand/command.go @@ -21,6 +21,7 @@ import ( "github.com/hajimehoshi/ebiten/internal/affine" "github.com/hajimehoshi/ebiten/internal/driver" "github.com/hajimehoshi/ebiten/internal/graphics" + "github.com/hajimehoshi/ebiten/internal/shaderir" ) var theGraphicsDriver driver.Graphics @@ -52,7 +53,7 @@ type command interface { NumIndices() int AddNumVertices(n int) AddNumIndices(n int) - CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) bool + CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, shader *Shader) bool } type size struct { @@ -121,7 +122,7 @@ func (q *commandQueue) appendIndices(indices []uint16, offset uint16) { } // EnqueueDrawTrianglesCommand enqueues a drawing-image command. -func (q *commandQueue) EnqueueDrawTrianglesCommand(dst, src *Image, vertices []float32, indices []uint16, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) { +func (q *commandQueue) EnqueueDrawTrianglesCommand(dst, src *Image, vertices []float32, indices []uint16, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, shader *Shader, uniforms map[int]interface{}) { if len(indices) > graphics.IndicesNum { panic(fmt.Sprintf("graphicscommand: len(indices) must be <= graphics.IndicesNum but not at EnqueueDrawTrianglesCommand: len(indices): %d, graphics.IndicesNum: %d", len(indices), graphics.IndicesNum)) } @@ -134,20 +135,25 @@ func (q *commandQueue) EnqueueDrawTrianglesCommand(dst, src *Image, vertices []f } n := len(vertices) / graphics.VertexFloatNum - iw, ih := src.InternalSize() - q.appendVertices(vertices, float32(iw), float32(ih)) + if src != nil { + iw, ih := src.InternalSize() + q.appendVertices(vertices, float32(iw), float32(ih)) + } else { + q.appendVertices(vertices, 1, 1) + } q.appendIndices(indices, uint16(q.nextIndex)) q.nextIndex += n q.tmpNumIndices += len(indices) // TODO: If dst is the screen, reorder the command to be the last. if !split && 0 < len(q.commands) { - if last := q.commands[len(q.commands)-1]; last.CanMergeWithDrawTrianglesCommand(dst, src, color, mode, filter, address) { + if last := q.commands[len(q.commands)-1]; last.CanMergeWithDrawTrianglesCommand(dst, src, color, mode, filter, address, shader) { last.AddNumVertices(len(vertices)) last.AddNumIndices(len(indices)) return } } + c := &drawTrianglesCommand{ dst: dst, src: src, @@ -157,6 +163,8 @@ func (q *commandQueue) EnqueueDrawTrianglesCommand(dst, src *Image, vertices []f mode: mode, filter: filter, address: address, + shader: shader, + uniforms: uniforms, } q.commands = append(q.commands, c) } @@ -298,6 +306,8 @@ type drawTrianglesCommand struct { mode driver.CompositeMode filter driver.Filter address driver.Address + shader *Shader + uniforms map[int]interface{} } func (c *drawTrianglesCommand) String() string { @@ -333,6 +343,15 @@ func (c *drawTrianglesCommand) String() string { panic(fmt.Sprintf("graphicscommand: invalid composite mode: %d", c.mode)) } + dst := fmt.Sprintf("%d", c.dst.id) + if c.dst.screen { + dst += " (screen)" + } + + if c.shader != nil { + return fmt.Sprintf("draw-triangles: dst: %s, shader, num of indices: %d, mode %s", dst, c.nindices, mode) + } + filter := "" switch c.filter { case driver.FilterNearest: @@ -355,10 +374,6 @@ func (c *drawTrianglesCommand) String() string { panic(fmt.Sprintf("graphicscommand: invalid address: %d", c.address)) } - dst := fmt.Sprintf("%d", c.dst.id) - if c.dst.screen { - dst += " (screen)" - } src := fmt.Sprintf("%d", c.src.id) if c.src.screen { src += " (screen)" @@ -374,10 +389,10 @@ func (c *drawTrianglesCommand) Exec(indexOffset int) error { return nil } - if err := theGraphicsDriver.Draw(c.dst.image.ID(), c.src.image.ID(), c.nindices, indexOffset, c.mode, c.color, c.filter, c.address); err != nil { - return err + if c.shader != nil { + return theGraphicsDriver.DrawShader(c.dst.image.ID(), c.shader.shader.ID(), c.nindices, indexOffset, c.mode, c.uniforms) } - return nil + return theGraphicsDriver.Draw(c.dst.image.ID(), c.src.image.ID(), c.nindices, indexOffset, c.mode, c.color, c.filter, c.address) } func (c *drawTrianglesCommand) NumVertices() int { @@ -398,7 +413,13 @@ func (c *drawTrianglesCommand) AddNumIndices(n int) { // CanMergeWithDrawTrianglesCommand returns a boolean value indicating whether the other drawTrianglesCommand can be merged // with the drawTrianglesCommand c. -func (c *drawTrianglesCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) bool { +func (c *drawTrianglesCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, shader *Shader) bool { + // If a shader is used, commands are not merged. + // + // TODO: Merge shader commands considering uniform variables. + if c.shader != nil || shader != nil { + return false + } if c.dst != dst { return false } @@ -450,7 +471,7 @@ func (c *replacePixelsCommand) AddNumVertices(n int) { func (c *replacePixelsCommand) AddNumIndices(n int) { } -func (c *replacePixelsCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) bool { +func (c *replacePixelsCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, shader *Shader) bool { return false } @@ -487,40 +508,73 @@ func (c *pixelsCommand) AddNumVertices(n int) { func (c *pixelsCommand) AddNumIndices(n int) { } -func (c *pixelsCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) bool { +func (c *pixelsCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, shader *Shader) bool { return false } -// disposeCommand represents a command to dispose an image. -type disposeCommand struct { +// disposeImageCommand represents a command to dispose an image. +type disposeImageCommand struct { target *Image } -func (c *disposeCommand) String() string { - return fmt.Sprintf("dispose: target: %d", c.target.id) +func (c *disposeImageCommand) String() string { + return fmt.Sprintf("dispose-image: target: %d", c.target.id) } -// Exec executes the disposeCommand. -func (c *disposeCommand) Exec(indexOffset int) error { +// Exec executes the disposeImageCommand. +func (c *disposeImageCommand) Exec(indexOffset int) error { c.target.image.Dispose() return nil } -func (c *disposeCommand) NumVertices() int { +func (c *disposeImageCommand) NumVertices() int { return 0 } -func (c *disposeCommand) NumIndices() int { +func (c *disposeImageCommand) NumIndices() int { return 0 } -func (c *disposeCommand) AddNumVertices(n int) { +func (c *disposeImageCommand) AddNumVertices(n int) { } -func (c *disposeCommand) AddNumIndices(n int) { +func (c *disposeImageCommand) AddNumIndices(n int) { } -func (c *disposeCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) bool { +func (c *disposeImageCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, shader *Shader) bool { + return false +} + +// disposeShaderCommand represents a command to dispose a shader. +type disposeShaderCommand struct { + target *Shader +} + +func (c *disposeShaderCommand) String() string { + return fmt.Sprintf("dispose-shader: target") +} + +// Exec executes the disposeShaderCommand. +func (c *disposeShaderCommand) Exec(indexOffset int) error { + c.target.shader.Dispose() + return nil +} + +func (c *disposeShaderCommand) NumVertices() int { + return 0 +} + +func (c *disposeShaderCommand) NumIndices() int { + return 0 +} + +func (c *disposeShaderCommand) AddNumVertices(n int) { +} + +func (c *disposeShaderCommand) AddNumIndices(n int) { +} + +func (c *disposeShaderCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, shader *Shader) bool { return false } @@ -559,7 +613,7 @@ func (c *newImageCommand) AddNumVertices(n int) { func (c *newImageCommand) AddNumIndices(n int) { } -func (c *newImageCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) bool { +func (c *newImageCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, shader *Shader) bool { return false } @@ -595,7 +649,42 @@ func (c *newScreenFramebufferImageCommand) AddNumVertices(n int) { func (c *newScreenFramebufferImageCommand) AddNumIndices(n int) { } -func (c *newScreenFramebufferImageCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) bool { +func (c *newScreenFramebufferImageCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, shader *Shader) bool { + return false +} + +// newShaderCommand is a command to create a shader. +type newShaderCommand struct { + result *Shader + ir *shaderir.Program +} + +func (c *newShaderCommand) String() string { + return fmt.Sprintf("new-shader-image") +} + +// Exec executes a newShaderCommand. +func (c *newShaderCommand) Exec(indexOffset int) error { + var err error + c.result.shader, err = theGraphicsDriver.NewShader(c.ir) + return err +} + +func (c *newShaderCommand) NumVertices() int { + return 0 +} + +func (c *newShaderCommand) NumIndices() int { + return 0 +} + +func (c *newShaderCommand) AddNumVertices(n int) { +} + +func (c *newShaderCommand) AddNumIndices(n int) { +} + +func (c *newShaderCommand) CanMergeWithDrawTrianglesCommand(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address, shader *Shader) bool { return false } diff --git a/internal/graphicscommand/export_test.go b/internal/graphicscommand/export_test.go new file mode 100644 index 000000000..06ebb030d --- /dev/null +++ b/internal/graphicscommand/export_test.go @@ -0,0 +1,19 @@ +// Copyright 2020 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package graphicscommand + +func IsGL() bool { + return theGraphicsDriver.IsGL() +} diff --git a/internal/graphicscommand/image.go b/internal/graphicscommand/image.go index 8e42cf8c8..e71cff81f 100644 --- a/internal/graphicscommand/image.go +++ b/internal/graphicscommand/image.go @@ -110,7 +110,7 @@ func (i *Image) resolveBufferedReplacePixels() { } func (i *Image) Dispose() { - c := &disposeCommand{ + c := &disposeImageCommand{ target: i, } theCommandQueue.Enqueue(c) @@ -156,7 +156,33 @@ func (i *Image) DrawTriangles(src *Image, vertices []float32, indices []uint16, src.resolveBufferedReplacePixels() i.resolveBufferedReplacePixels() - theCommandQueue.EnqueueDrawTrianglesCommand(i, src, vertices, indices, clr, mode, filter, address) + theCommandQueue.EnqueueDrawTrianglesCommand(i, src, vertices, indices, clr, mode, filter, address, nil, nil) + + if i.lastCommand == lastCommandNone && !i.screen { + i.lastCommand = lastCommandClear + } else { + i.lastCommand = lastCommandDrawTriangles + } +} + +func (i *Image) DrawShader(shader *Shader, vertices []float32, indices []uint16, mode driver.CompositeMode, uniforms map[int]interface{}) { + if i.lastCommand == lastCommandNone { + panic("graphicscommand: the image must be cleared first before DrawShader") + } + + us := map[int]interface{}{} + for k, v := range uniforms { + switch v := v.(type) { + case *Image: + v.resolveBufferedReplacePixels() + us[k] = v.image + default: + us[k] = v + } + } + i.resolveBufferedReplacePixels() + + theCommandQueue.EnqueueDrawTrianglesCommand(i, nil, vertices, indices, nil, mode, 0, 0, shader, us) if i.lastCommand == lastCommandNone && !i.screen { i.lastCommand = lastCommandClear diff --git a/internal/graphicscommand/image_test.go b/internal/graphicscommand/image_test.go index 3fa99edfa..a0c6385b6 100644 --- a/internal/graphicscommand/image_test.go +++ b/internal/graphicscommand/image_test.go @@ -21,6 +21,7 @@ import ( "github.com/hajimehoshi/ebiten/internal/driver" "github.com/hajimehoshi/ebiten/internal/graphics" . "github.com/hajimehoshi/ebiten/internal/graphicscommand" + "github.com/hajimehoshi/ebiten/internal/shaderir" t "github.com/hajimehoshi/ebiten/internal/testing" ) @@ -78,3 +79,243 @@ func TestReplacePixelsPartAfterDrawTriangles(t *testing.T) { dst.DrawTriangles(src, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressClampToZero) dst.ReplacePixels(make([]byte, 4), 0, 0, 1, 1) } + +func TestShader(t *testing.T) { + if !IsGL() { + t.Skip("shader is not implemented on non-GL environment") + } + + const w, h = 16, 16 + clr := NewImage(w, h) + dst := NewImage(w, h) + vs := quadVertices(w, h) + is := graphics.QuadIndices() + dst.DrawTriangles(clr, vs, is, nil, driver.CompositeModeClear, driver.FilterNearest, driver.AddressClampToZero) + + mat := shaderir.Expr{ + Type: shaderir.Call, + Exprs: []shaderir.Expr{ + { + Type: shaderir.BuiltinFuncExpr, + BuiltinFunc: shaderir.Mat4F, + }, + { + Type: shaderir.Binary, + Op: shaderir.Div, + Exprs: []shaderir.Expr{ + { + Type: shaderir.FloatExpr, + Float: 2, + }, + { + Type: shaderir.FieldSelector, + Exprs: []shaderir.Expr{ + { + Type: shaderir.UniformVariable, + Index: 0, + }, + { + Type: shaderir.SwizzlingExpr, + Swizzling: "x", + }, + }, + }, + }, + }, + { + Type: shaderir.FloatExpr, + Float: 0, + }, + { + Type: shaderir.FloatExpr, + Float: 0, + }, + { + Type: shaderir.FloatExpr, + Float: 0, + }, + { + Type: shaderir.FloatExpr, + Float: 0, + }, + { + Type: shaderir.Binary, + Op: shaderir.Div, + Exprs: []shaderir.Expr{ + { + Type: shaderir.FloatExpr, + Float: 2, + }, + { + Type: shaderir.FieldSelector, + Exprs: []shaderir.Expr{ + { + Type: shaderir.UniformVariable, + Index: 0, + }, + { + Type: shaderir.SwizzlingExpr, + Swizzling: "y", + }, + }, + }, + }, + }, + { + Type: shaderir.FloatExpr, + Float: 0, + }, + { + Type: shaderir.FloatExpr, + Float: 0, + }, + { + Type: shaderir.FloatExpr, + Float: 0, + }, + { + Type: shaderir.FloatExpr, + Float: 0, + }, + { + Type: shaderir.FloatExpr, + Float: 1, + }, + { + Type: shaderir.FloatExpr, + Float: 0, + }, + { + Type: shaderir.FloatExpr, + Float: -1, + }, + { + Type: shaderir.FloatExpr, + Float: -1, + }, + { + Type: shaderir.FloatExpr, + Float: 0, + }, + { + Type: shaderir.FloatExpr, + Float: 1, + }, + }, + } + pos := shaderir.Expr{ + Type: shaderir.Call, + Exprs: []shaderir.Expr{ + { + Type: shaderir.BuiltinFuncExpr, + BuiltinFunc: shaderir.Vec4F, + }, + { + Type: shaderir.LocalVariable, + Index: 0, + }, + { + Type: shaderir.FloatExpr, + Float: 0, + }, + { + Type: shaderir.FloatExpr, + Float: 1, + }, + }, + } + red := shaderir.Expr{ + Type: shaderir.Call, + Exprs: []shaderir.Expr{ + { + Type: shaderir.BuiltinFuncExpr, + BuiltinFunc: shaderir.Vec4F, + }, + { + Type: shaderir.FloatExpr, + Float: 1, + }, + { + Type: shaderir.FloatExpr, + Float: 0, + }, + { + Type: shaderir.FloatExpr, + Float: 0, + }, + { + Type: shaderir.FloatExpr, + Float: 1, + }, + }, + } + s := NewShader(&shaderir.Program{ + Uniforms: []shaderir.Type{ + {Main: shaderir.Vec2}, + }, + Attributes: []shaderir.Type{ + {Main: shaderir.Vec2}, + {Main: shaderir.Vec2}, + {Main: shaderir.Vec4}, + {Main: shaderir.Vec4}, + }, + VertexFunc: shaderir.VertexFunc{ + Block: shaderir.Block{ + Stmts: []shaderir.Stmt{ + { + Type: shaderir.Assign, + Exprs: []shaderir.Expr{ + { + Type: shaderir.LocalVariable, + Index: 4, + }, + { + Type: shaderir.Binary, + Op: shaderir.Mul, + Exprs: []shaderir.Expr{ + mat, + pos, + }, + }, + }, + }, + }, + }, + }, + FragmentFunc: shaderir.FragmentFunc{ + Block: shaderir.Block{ + Stmts: []shaderir.Stmt{ + { + Type: shaderir.Assign, + Exprs: []shaderir.Expr{ + { + Type: shaderir.LocalVariable, + Index: 1, + }, + red, + }, + }, + }, + }, + }, + }) + us := map[int]interface{}{ + 0: []float32{w, h}, + } + dst.DrawShader(s, vs, is, driver.CompositeModeSourceOver, us) + + pix, err := dst.Pixels() + if err != nil { + t.Fatal(err) + } + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + idx := 4 * (i + w*j) + got := color.RGBA{pix[idx], pix[idx+1], pix[idx+2], pix[idx+3]} + want := color.RGBA{0xff, 0, 0, 0xff} + if got != want { + t.Errorf("dst.At(%d, %d) after DrawTriangles: got %v, want: %v", i, j, got, want) + } + } + } +} diff --git a/internal/graphicscommand/shader.go b/internal/graphicscommand/shader.go new file mode 100644 index 000000000..19c9d3455 --- /dev/null +++ b/internal/graphicscommand/shader.go @@ -0,0 +1,41 @@ +// Copyright 2020 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package graphicscommand + +import ( + "github.com/hajimehoshi/ebiten/internal/driver" + "github.com/hajimehoshi/ebiten/internal/shaderir" +) + +type Shader struct { + shader driver.Shader +} + +func NewShader(ir *shaderir.Program) *Shader { + s := &Shader{} + c := &newShaderCommand{ + result: s, + ir: ir, + } + theCommandQueue.Enqueue(c) + return s +} + +func (s *Shader) Dispose() { + c := &disposeShaderCommand{ + target: s, + } + theCommandQueue.Enqueue(c) +} diff --git a/internal/graphicsdriver/metal/graphics.go b/internal/graphicsdriver/metal/graphics.go index f4fdcca14..943bda9dd 100644 --- a/internal/graphicsdriver/metal/graphics.go +++ b/internal/graphicsdriver/metal/graphics.go @@ -26,6 +26,7 @@ import ( "github.com/hajimehoshi/ebiten/internal/graphics" "github.com/hajimehoshi/ebiten/internal/graphicsdriver/metal/ca" "github.com/hajimehoshi/ebiten/internal/graphicsdriver/metal/mtl" + "github.com/hajimehoshi/ebiten/internal/shaderir" "github.com/hajimehoshi/ebiten/internal/thread" ) @@ -758,6 +759,14 @@ func (g *Graphics) MaxImageSize() int { return m } +func (g *Graphics) NewShader(program *shaderir.Program) (driver.Shader, error) { + panic("metal: NewShader is not implemented") +} + +func (g *Graphics) DrawShader(dst driver.ImageID, shader driver.ShaderID, indexLen int, indexOffset int, mode driver.CompositeMode, uniforms map[int]interface{}) error { + panic("metal: DrawShader is not implemented") +} + type Image struct { id driver.ImageID graphics *Graphics diff --git a/internal/graphicsdriver/opengl/defaultshader.go b/internal/graphicsdriver/opengl/defaultshader.go new file mode 100644 index 000000000..ba29885b4 --- /dev/null +++ b/internal/graphicsdriver/opengl/defaultshader.go @@ -0,0 +1,286 @@ +// Copyright 2014 Hajime Hoshi +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package opengl + +import ( + "fmt" + "regexp" + "strings" + + "github.com/hajimehoshi/ebiten/internal/driver" +) + +// glslReservedKeywords is a set of reserved keywords that cannot be used as an indentifier on some environments. +// See https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.4.60.pdf. +var glslReservedKeywords = map[string]struct{}{ + "common": {}, "partition": {}, "active": {}, + "asm": {}, + "class": {}, "union": {}, "enum": {}, "typedef": {}, "template": {}, "this": {}, + "resource": {}, + "goto": {}, + "inline": {}, "noinline": {}, "public": {}, "static": {}, "extern": {}, "external": {}, "interface": {}, + "long": {}, "short": {}, "half": {}, "fixed": {}, "unsigned": {}, "superp": {}, + "input": {}, "output": {}, + "hvec2": {}, "hvec3": {}, "hvec4": {}, "fvec2": {}, "fvec3": {}, "fvec4": {}, + "filter": {}, + "sizeof": {}, "cast": {}, + "namespace": {}, "using": {}, + "sampler3DRect": {}, +} + +var glslIdentifier = regexp.MustCompile(`[_a-zA-Z][_a-zA-Z0-9]*`) + +func checkGLSL(src string) { + for _, l := range strings.Split(src, "\n") { + if strings.Contains(l, "//") { + l = l[:strings.Index(l, "//")] + } + for _, token := range glslIdentifier.FindAllString(l, -1) { + if _, ok := glslReservedKeywords[token]; ok { + panic(fmt.Sprintf("opengl: %q is a reserved keyword", token)) + } + } + } +} + +func vertexShaderStr() string { + src := shaderStrVertex + checkGLSL(src) + return src +} + +func fragmentShaderStr(useColorM bool, filter driver.Filter, address driver.Address) string { + replaces := map[string]string{ + "{{.AddressClampToZero}}": fmt.Sprintf("%d", driver.AddressClampToZero), + "{{.AddressRepeat}}": fmt.Sprintf("%d", driver.AddressRepeat), + } + src := shaderStrFragment + for k, v := range replaces { + src = strings.Replace(src, k, v, -1) + } + + var defs []string + + if useColorM { + defs = append(defs, "#define USE_COLOR_MATRIX") + } + + switch filter { + case driver.FilterNearest: + defs = append(defs, "#define FILTER_NEAREST") + case driver.FilterLinear: + defs = append(defs, "#define FILTER_LINEAR") + case driver.FilterScreen: + defs = append(defs, "#define FILTER_SCREEN") + default: + panic(fmt.Sprintf("opengl: invalid filter: %d", filter)) + } + + switch address { + case driver.AddressClampToZero: + defs = append(defs, "#define ADDRESS_CLAMP_TO_ZERO") + case driver.AddressRepeat: + defs = append(defs, "#define ADDRESS_REPEAT") + default: + panic(fmt.Sprintf("opengl: invalid address: %d", address)) + } + + src = strings.Replace(src, "{{.Definitions}}", strings.Join(defs, "\n"), -1) + + checkGLSL(src) + return src +} + +const ( + shaderStrVertex = ` +uniform vec2 viewport_size; +attribute vec2 vertex; +attribute vec2 tex; +attribute vec4 tex_region; +attribute vec4 color_scale; +varying vec2 varying_tex; +varying vec4 varying_tex_region; +varying vec4 varying_color_scale; + +void main(void) { + varying_tex = tex; + varying_tex_region = tex_region; + varying_color_scale = color_scale; + + mat4 projection_matrix = mat4( + vec4(2.0 / viewport_size.x, 0, 0, 0), + vec4(0, 2.0 / viewport_size.y, 0, 0), + vec4(0, 0, 1, 0), + vec4(-1, -1, 0, 1) + ); + gl_Position = projection_matrix * vec4(vertex, 0, 1); +} +` + shaderStrFragment = ` +#if defined(GL_ES) +precision mediump float; +#else +#define lowp +#define mediump +#define highp +#endif + +{{.Definitions}} + +uniform sampler2D texture; + +#if defined(USE_COLOR_MATRIX) +uniform mat4 color_matrix_body; +uniform vec4 color_matrix_translation; +#endif + +uniform highp vec2 source_size; + +#if defined(FILTER_SCREEN) +uniform highp float scale; +#endif + +varying highp vec2 varying_tex; +varying highp vec4 varying_tex_region; +varying highp vec4 varying_color_scale; + +// adjustTexel adjusts the two texels and returns the adjusted second texel. +// When p1 - p0 is exactly equal to the texel size, jaggy can happen on macOS (#669). +// In order to avoid this jaggy, subtract a little bit from the second texel. +highp vec2 adjustTexel(highp vec2 p0, highp vec2 p1) { + highp vec2 texel_size = 1.0 / source_size; + if (fract((p1.x-p0.x)*source_size.x) == 0.0) { + p1.x -= texel_size.x / 512.0; + } + if (fract((p1.y-p0.y)*source_size.y) == 0.0) { + p1.y -= texel_size.y / 512.0; + } + return p1; +} + +highp float floorMod(highp float x, highp float y) { + if (x < 0.0) { + return y - (-x - y * floor(-x/y)); + } + return x - y * floor(x/y); +} + +highp vec2 adjustTexelByAddress(highp vec2 p, highp vec4 tex_region) { +#if defined(ADDRESS_CLAMP_TO_ZERO) + return p; +#endif + +#if defined(ADDRESS_REPEAT) + highp vec2 o = vec2(tex_region[0], tex_region[1]); + highp vec2 size = vec2(tex_region[2] - tex_region[0], tex_region[3] - tex_region[1]); + return vec2(floorMod((p.x - o.x), size.x) + o.x, floorMod((p.y - o.y), size.y) + o.y); +#endif +} + +void main(void) { + highp vec2 pos = varying_tex; + +#if defined(FILTER_NEAREST) + vec4 color; + pos = adjustTexelByAddress(pos, varying_tex_region); + if (varying_tex_region[0] <= pos.x && + varying_tex_region[1] <= pos.y && + pos.x < varying_tex_region[2] && + pos.y < varying_tex_region[3]) { + color = texture2D(texture, pos); + } else { + color = vec4(0, 0, 0, 0); + } +#endif + +#if defined(FILTER_LINEAR) + vec4 color; + highp vec2 texel_size = 1.0 / source_size; + highp vec2 p0 = pos - texel_size / 2.0; + highp vec2 p1 = pos + texel_size / 2.0; + + p1 = adjustTexel(p0, p1); + p0 = adjustTexelByAddress(p0, varying_tex_region); + p1 = adjustTexelByAddress(p1, varying_tex_region); + + vec4 c0 = texture2D(texture, p0); + vec4 c1 = texture2D(texture, vec2(p1.x, p0.y)); + vec4 c2 = texture2D(texture, vec2(p0.x, p1.y)); + vec4 c3 = texture2D(texture, p1); + if (p0.x < varying_tex_region[0]) { + c0 = vec4(0, 0, 0, 0); + c2 = vec4(0, 0, 0, 0); + } + if (p0.y < varying_tex_region[1]) { + c0 = vec4(0, 0, 0, 0); + c1 = vec4(0, 0, 0, 0); + } + if (varying_tex_region[2] <= p1.x) { + c1 = vec4(0, 0, 0, 0); + c3 = vec4(0, 0, 0, 0); + } + if (varying_tex_region[3] <= p1.y) { + c2 = vec4(0, 0, 0, 0); + c3 = vec4(0, 0, 0, 0); + } + + vec2 rate = fract(p0 * source_size); + color = mix(mix(c0, c1, rate.x), mix(c2, c3, rate.x), rate.y); +#endif + +#if defined(FILTER_SCREEN) + highp vec2 texel_size = 1.0 / source_size; + highp vec2 half_scaled_texel_size = texel_size / 2.0 / scale; + highp vec2 p0 = pos - half_scaled_texel_size; + highp vec2 p1 = pos + half_scaled_texel_size; + + p1 = adjustTexel(p0, p1); + + vec4 c0 = texture2D(texture, p0); + vec4 c1 = texture2D(texture, vec2(p1.x, p0.y)); + vec4 c2 = texture2D(texture, vec2(p0.x, p1.y)); + vec4 c3 = texture2D(texture, p1); + // Texels must be in the source rect, so it is not necessary to check that like linear filter. + + vec2 rate_center = vec2(1.0, 1.0) - half_scaled_texel_size; + vec2 rate = clamp(((fract(p0 * source_size) - rate_center) * scale) + rate_center, 0.0, 1.0); + gl_FragColor = mix(mix(c0, c1, rate.x), mix(c2, c3, rate.x), rate.y); + // Assume that a color matrix and color vector values are not used with FILTER_SCREEN. + +#else + +#if defined(USE_COLOR_MATRIX) + // Un-premultiply alpha. + // When the alpha is 0, 1.0 - sign(alpha) is 1.0, which means division does nothing. + color.rgb /= color.a + (1.0 - sign(color.a)); + // Apply the color matrix or scale. + color = (color_matrix_body * color) + color_matrix_translation; + color *= varying_color_scale; + // Premultiply alpha + color.rgb *= color.a; +#else + vec4 s = varying_color_scale; + color *= vec4(s.r, s.g, s.b, 1.0) * s.a; +#endif + + color = min(color, color.a); + + gl_FragColor = color; + +#endif + +} +` +) diff --git a/internal/graphicsdriver/opengl/graphics.go b/internal/graphicsdriver/opengl/graphics.go index 358e8a073..25258b6a0 100644 --- a/internal/graphicsdriver/opengl/graphics.go +++ b/internal/graphicsdriver/opengl/graphics.go @@ -20,6 +20,7 @@ import ( "github.com/hajimehoshi/ebiten/internal/affine" "github.com/hajimehoshi/ebiten/internal/driver" "github.com/hajimehoshi/ebiten/internal/graphics" + "github.com/hajimehoshi/ebiten/internal/shaderir" "github.com/hajimehoshi/ebiten/internal/thread" ) @@ -30,11 +31,15 @@ func Get() *Graphics { } type Graphics struct { + state openGLState + context context + nextImageID driver.ImageID - state openGLState - context context images map[driver.ImageID]*Image + nextShaderID driver.ShaderID + shaders map[driver.ShaderID]*Shader + // drawCalled is true just after Draw is called. This holds true until ReplacePixels is called. drawCalled bool } @@ -79,6 +84,12 @@ func (g *Graphics) genNextImageID() driver.ImageID { return id } +func (g *Graphics) genNextShaderID() driver.ShaderID { + id := g.nextShaderID + g.nextShaderID++ + return id +} + func (g *Graphics) NewImage(width, height int) (driver.Image, error) { i := &Image{ id: g.genNextImageID(), @@ -218,3 +229,49 @@ func (g *Graphics) HasHighPrecisionFloat() bool { func (g *Graphics) MaxImageSize() int { return g.context.getMaxTextureSize() } + +func (g *Graphics) NewShader(program *shaderir.Program) (driver.Shader, error) { + s, err := NewShader(g.genNextShaderID(), g, program) + if err != nil { + return nil, err + } + g.addShader(s) + return s, nil +} + +func (g *Graphics) addShader(shader *Shader) { + if g.shaders == nil { + g.shaders = map[driver.ShaderID]*Shader{} + } + if _, ok := g.shaders[shader.id]; ok { + panic(fmt.Sprintf("opengl: shader ID %d was already registered", shader.id)) + } + g.shaders[shader.id] = shader +} + +func (g *Graphics) removeShader(shader *Shader) { + delete(g.shaders, shader.id) +} + +func (g *Graphics) DrawShader(dst driver.ImageID, shader driver.ShaderID, indexLen int, indexOffset int, mode driver.CompositeMode, uniforms map[int]interface{}) error { + d := g.images[dst] + s := g.shaders[shader] + + g.drawCalled = true + + if err := d.setViewport(); err != nil { + return err + } + g.context.blendFunc(mode) + + us := map[string]interface{}{} + for k, v := range uniforms { + us[fmt.Sprintf("U%d", k)] = v + } + if err := g.useProgram(s.p, us); err != nil { + return err + } + g.context.drawElements(indexLen, indexOffset*2) // 2 is uint16 size in bytes + + return nil +} diff --git a/internal/graphicsdriver/opengl/shader.go b/internal/graphicsdriver/opengl/shader.go index ba29885b4..8fbd07308 100644 --- a/internal/graphicsdriver/opengl/shader.go +++ b/internal/graphicsdriver/opengl/shader.go @@ -1,4 +1,4 @@ -// Copyright 2014 Hajime Hoshi +// Copyright 2020 The Ebiten Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,272 +15,60 @@ package opengl import ( - "fmt" - "regexp" - "strings" - "github.com/hajimehoshi/ebiten/internal/driver" + "github.com/hajimehoshi/ebiten/internal/shaderir" ) -// glslReservedKeywords is a set of reserved keywords that cannot be used as an indentifier on some environments. -// See https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.4.60.pdf. -var glslReservedKeywords = map[string]struct{}{ - "common": {}, "partition": {}, "active": {}, - "asm": {}, - "class": {}, "union": {}, "enum": {}, "typedef": {}, "template": {}, "this": {}, - "resource": {}, - "goto": {}, - "inline": {}, "noinline": {}, "public": {}, "static": {}, "extern": {}, "external": {}, "interface": {}, - "long": {}, "short": {}, "half": {}, "fixed": {}, "unsigned": {}, "superp": {}, - "input": {}, "output": {}, - "hvec2": {}, "hvec3": {}, "hvec4": {}, "fvec2": {}, "fvec3": {}, "fvec4": {}, - "filter": {}, - "sizeof": {}, "cast": {}, - "namespace": {}, "using": {}, - "sampler3DRect": {}, +type Shader struct { + id driver.ShaderID + graphics *Graphics + + ir *shaderir.Program + p program } -var glslIdentifier = regexp.MustCompile(`[_a-zA-Z][_a-zA-Z0-9]*`) - -func checkGLSL(src string) { - for _, l := range strings.Split(src, "\n") { - if strings.Contains(l, "//") { - l = l[:strings.Index(l, "//")] - } - for _, token := range glslIdentifier.FindAllString(l, -1) { - if _, ok := glslReservedKeywords[token]; ok { - panic(fmt.Sprintf("opengl: %q is a reserved keyword", token)) - } - } +func NewShader(id driver.ShaderID, graphics *Graphics, program *shaderir.Program) (*Shader, error) { + s := &Shader{ + id: id, + graphics: graphics, + ir: program, } -} - -func vertexShaderStr() string { - src := shaderStrVertex - checkGLSL(src) - return src -} - -func fragmentShaderStr(useColorM bool, filter driver.Filter, address driver.Address) string { - replaces := map[string]string{ - "{{.AddressClampToZero}}": fmt.Sprintf("%d", driver.AddressClampToZero), - "{{.AddressRepeat}}": fmt.Sprintf("%d", driver.AddressRepeat), + if err := s.compile(); err != nil { + return nil, err } - src := shaderStrFragment - for k, v := range replaces { - src = strings.Replace(src, k, v, -1) + return s, nil +} + +func (s *Shader) ID() driver.ShaderID { + return s.id +} + +func (s *Shader) Dispose() { + s.graphics.context.deleteProgram(s.p) + s.graphics.removeShader(s) +} + +func (s *Shader) compile() error { + vssrc, fssrc := s.ir.Glsl() + println(vssrc, fssrc) + + vs, err := s.graphics.context.newShader(vertexShader, vssrc) + if err != nil { + return err + } + defer s.graphics.context.deleteShader(vs) + + fs, err := s.graphics.context.newShader(fragmentShader, fssrc) + if err != nil { + return err + } + defer s.graphics.context.deleteShader(vs) + + p, err := s.graphics.context.newProgram([]shader{vs, fs}, theArrayBufferLayout.names()) + if err != nil { + return err } - var defs []string - - if useColorM { - defs = append(defs, "#define USE_COLOR_MATRIX") - } - - switch filter { - case driver.FilterNearest: - defs = append(defs, "#define FILTER_NEAREST") - case driver.FilterLinear: - defs = append(defs, "#define FILTER_LINEAR") - case driver.FilterScreen: - defs = append(defs, "#define FILTER_SCREEN") - default: - panic(fmt.Sprintf("opengl: invalid filter: %d", filter)) - } - - switch address { - case driver.AddressClampToZero: - defs = append(defs, "#define ADDRESS_CLAMP_TO_ZERO") - case driver.AddressRepeat: - defs = append(defs, "#define ADDRESS_REPEAT") - default: - panic(fmt.Sprintf("opengl: invalid address: %d", address)) - } - - src = strings.Replace(src, "{{.Definitions}}", strings.Join(defs, "\n"), -1) - - checkGLSL(src) - return src + s.p = p + return nil } - -const ( - shaderStrVertex = ` -uniform vec2 viewport_size; -attribute vec2 vertex; -attribute vec2 tex; -attribute vec4 tex_region; -attribute vec4 color_scale; -varying vec2 varying_tex; -varying vec4 varying_tex_region; -varying vec4 varying_color_scale; - -void main(void) { - varying_tex = tex; - varying_tex_region = tex_region; - varying_color_scale = color_scale; - - mat4 projection_matrix = mat4( - vec4(2.0 / viewport_size.x, 0, 0, 0), - vec4(0, 2.0 / viewport_size.y, 0, 0), - vec4(0, 0, 1, 0), - vec4(-1, -1, 0, 1) - ); - gl_Position = projection_matrix * vec4(vertex, 0, 1); -} -` - shaderStrFragment = ` -#if defined(GL_ES) -precision mediump float; -#else -#define lowp -#define mediump -#define highp -#endif - -{{.Definitions}} - -uniform sampler2D texture; - -#if defined(USE_COLOR_MATRIX) -uniform mat4 color_matrix_body; -uniform vec4 color_matrix_translation; -#endif - -uniform highp vec2 source_size; - -#if defined(FILTER_SCREEN) -uniform highp float scale; -#endif - -varying highp vec2 varying_tex; -varying highp vec4 varying_tex_region; -varying highp vec4 varying_color_scale; - -// adjustTexel adjusts the two texels and returns the adjusted second texel. -// When p1 - p0 is exactly equal to the texel size, jaggy can happen on macOS (#669). -// In order to avoid this jaggy, subtract a little bit from the second texel. -highp vec2 adjustTexel(highp vec2 p0, highp vec2 p1) { - highp vec2 texel_size = 1.0 / source_size; - if (fract((p1.x-p0.x)*source_size.x) == 0.0) { - p1.x -= texel_size.x / 512.0; - } - if (fract((p1.y-p0.y)*source_size.y) == 0.0) { - p1.y -= texel_size.y / 512.0; - } - return p1; -} - -highp float floorMod(highp float x, highp float y) { - if (x < 0.0) { - return y - (-x - y * floor(-x/y)); - } - return x - y * floor(x/y); -} - -highp vec2 adjustTexelByAddress(highp vec2 p, highp vec4 tex_region) { -#if defined(ADDRESS_CLAMP_TO_ZERO) - return p; -#endif - -#if defined(ADDRESS_REPEAT) - highp vec2 o = vec2(tex_region[0], tex_region[1]); - highp vec2 size = vec2(tex_region[2] - tex_region[0], tex_region[3] - tex_region[1]); - return vec2(floorMod((p.x - o.x), size.x) + o.x, floorMod((p.y - o.y), size.y) + o.y); -#endif -} - -void main(void) { - highp vec2 pos = varying_tex; - -#if defined(FILTER_NEAREST) - vec4 color; - pos = adjustTexelByAddress(pos, varying_tex_region); - if (varying_tex_region[0] <= pos.x && - varying_tex_region[1] <= pos.y && - pos.x < varying_tex_region[2] && - pos.y < varying_tex_region[3]) { - color = texture2D(texture, pos); - } else { - color = vec4(0, 0, 0, 0); - } -#endif - -#if defined(FILTER_LINEAR) - vec4 color; - highp vec2 texel_size = 1.0 / source_size; - highp vec2 p0 = pos - texel_size / 2.0; - highp vec2 p1 = pos + texel_size / 2.0; - - p1 = adjustTexel(p0, p1); - p0 = adjustTexelByAddress(p0, varying_tex_region); - p1 = adjustTexelByAddress(p1, varying_tex_region); - - vec4 c0 = texture2D(texture, p0); - vec4 c1 = texture2D(texture, vec2(p1.x, p0.y)); - vec4 c2 = texture2D(texture, vec2(p0.x, p1.y)); - vec4 c3 = texture2D(texture, p1); - if (p0.x < varying_tex_region[0]) { - c0 = vec4(0, 0, 0, 0); - c2 = vec4(0, 0, 0, 0); - } - if (p0.y < varying_tex_region[1]) { - c0 = vec4(0, 0, 0, 0); - c1 = vec4(0, 0, 0, 0); - } - if (varying_tex_region[2] <= p1.x) { - c1 = vec4(0, 0, 0, 0); - c3 = vec4(0, 0, 0, 0); - } - if (varying_tex_region[3] <= p1.y) { - c2 = vec4(0, 0, 0, 0); - c3 = vec4(0, 0, 0, 0); - } - - vec2 rate = fract(p0 * source_size); - color = mix(mix(c0, c1, rate.x), mix(c2, c3, rate.x), rate.y); -#endif - -#if defined(FILTER_SCREEN) - highp vec2 texel_size = 1.0 / source_size; - highp vec2 half_scaled_texel_size = texel_size / 2.0 / scale; - highp vec2 p0 = pos - half_scaled_texel_size; - highp vec2 p1 = pos + half_scaled_texel_size; - - p1 = adjustTexel(p0, p1); - - vec4 c0 = texture2D(texture, p0); - vec4 c1 = texture2D(texture, vec2(p1.x, p0.y)); - vec4 c2 = texture2D(texture, vec2(p0.x, p1.y)); - vec4 c3 = texture2D(texture, p1); - // Texels must be in the source rect, so it is not necessary to check that like linear filter. - - vec2 rate_center = vec2(1.0, 1.0) - half_scaled_texel_size; - vec2 rate = clamp(((fract(p0 * source_size) - rate_center) * scale) + rate_center, 0.0, 1.0); - gl_FragColor = mix(mix(c0, c1, rate.x), mix(c2, c3, rate.x), rate.y); - // Assume that a color matrix and color vector values are not used with FILTER_SCREEN. - -#else - -#if defined(USE_COLOR_MATRIX) - // Un-premultiply alpha. - // When the alpha is 0, 1.0 - sign(alpha) is 1.0, which means division does nothing. - color.rgb /= color.a + (1.0 - sign(color.a)); - // Apply the color matrix or scale. - color = (color_matrix_body * color) + color_matrix_translation; - color *= varying_color_scale; - // Premultiply alpha - color.rgb *= color.a; -#else - vec4 s = varying_color_scale; - color *= vec4(s.r, s.g, s.b, 1.0) * s.a; -#endif - - color = min(color, color.a); - - gl_FragColor = color; - -#endif - -} -` -)