shaderir/glsl: Enable dFdx for WebGL

With WebGL1, an extension is required for dFdx. On the other hand,
with WebGL2, GLSL ES 300 is required and the extension is forbidden.
This change fixes shaderir/glsl to switch the output depends on the
WebGL version.

This change also adds a new build tag 'ebitenwebgl1' forcing WebGL 1.

Updates #1404
This commit is contained in:
Hajime Hoshi 2020-10-26 10:33:11 +09:00
parent 2097312a8b
commit b1d7a5f595
11 changed files with 217 additions and 17 deletions

2
doc.go
View File

@ -72,6 +72,8 @@
// //
// `ebitengl` forces to use OpenGL in any environments. // `ebitengl` forces to use OpenGL in any environments.
// //
// `ebitenwebgl1` forces to use WebGL 1 on browsers.
//
// `ebitensinglethread` disables Ebiten's thread safety to unlock maximum performance. If you use this you will have // `ebitensinglethread` disables Ebiten's thread safety to unlock maximum performance. If you use this you will have
// to manage threads yourself. Functions like IsKeyPressed will no longer be concurrent-safe with this build tag. // to manage threads yourself. Functions like IsKeyPressed will no longer be concurrent-safe with this build tag.
// They must be called from the main thread or the same goroutine as the given game's callback functions like Update // They must be called from the main thread or the same goroutine as the given game's callback functions like Update

View File

@ -94,7 +94,7 @@ const (
) )
var ( var (
isWebGL2Available = js.Global().Get("WebGL2RenderingContext").Truthy() isWebGL2Available = !forceWebGL1 && js.Global().Get("WebGL2RenderingContext").Truthy()
) )
type contextImpl struct { type contextImpl struct {
@ -148,6 +148,10 @@ func (c *context) reset() error {
c.blendFunc(driver.CompositeModeSourceOver) c.blendFunc(driver.CompositeModeSourceOver)
f := gl.Call("getParameter", gles.FRAMEBUFFER_BINDING) f := gl.Call("getParameter", gles.FRAMEBUFFER_BINDING)
c.screenFramebuffer = framebufferNative(f) c.screenFramebuffer = framebufferNative(f)
if !isWebGL2Available {
gl.Call("getExtension", "OES_standard_derivatives")
}
return nil return nil
} }

View File

@ -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.
// +build !ebitenwebgl1
package opengl
const forceWebGL1 = false

View File

@ -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.
// +build ebitenwebgl1
package opengl
const forceWebGL1 = true

View File

@ -52,7 +52,7 @@ func (s *Shader) Dispose() {
} }
func (s *Shader) compile() error { func (s *Shader) compile() error {
vssrc, fssrc := glsl.Compile(s.ir) vssrc, fssrc := glsl.Compile(s.ir, glslVersion())
vs, err := s.graphics.context.newShader(vertexShader, vssrc) vs, err := s.graphics.context.newShader(vertexShader, vssrc)
if err != nil { if err != nil {

View File

@ -0,0 +1,26 @@
// 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 opengl
import (
"github.com/hajimehoshi/ebiten/v2/internal/shaderir/glsl"
)
func glslVersion() glsl.GLSLVersion {
if isWebGL2Available {
return glsl.GLSLVersionES300
}
return glsl.GLSLVersionWebGL1
}

View File

@ -0,0 +1,25 @@
// 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.
// +build !js
package opengl
import (
"github.com/hajimehoshi/ebiten/v2/internal/shaderir/glsl"
)
func glslVersion() glsl.GLSLVersion {
return glsl.GLSLVersionDefault
}

View File

@ -30,8 +30,9 @@ import (
) )
func glslNormalize(str string) string { func glslNormalize(str string) string {
if strings.HasPrefix(str, glsl.FragmentPrelude) { p := glsl.FragmentPrelude(glsl.GLSLVersionDefault)
str = str[len(glsl.FragmentPrelude):] if strings.HasPrefix(str, p) {
str = str[len(p):]
} }
return strings.TrimSpace(str) return strings.TrimSpace(str)
} }
@ -156,7 +157,7 @@ func TestCompile(t *testing.T) {
} }
// GLSL // GLSL
vs, fs := glsl.Compile(s) vs, fs := glsl.Compile(s, glsl.GLSLVersionDefault)
if got, want := glslNormalize(vs), glslNormalize(string(tc.VS)); got != want { if got, want := glslNormalize(vs), glslNormalize(string(tc.VS)); got != want {
compare(t, "GLSL Vertex", got, want) compare(t, "GLSL Vertex", got, want)
} }

View File

@ -24,15 +24,40 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/shaderir" "github.com/hajimehoshi/ebiten/v2/internal/shaderir"
) )
const FragmentPrelude = `#if defined(GL_ES) type GLSLVersion int
const (
GLSLVersionDefault GLSLVersion = iota
GLSLVersionWebGL1
GLSLVersionES300
)
func VertexPrelude(version GLSLVersion) string {
if version == GLSLVersionES300 {
return `#version 300 es`
}
return ""
}
func FragmentPrelude(version GLSLVersion) string {
var prefix string
switch version {
case GLSLVersionWebGL1:
prefix = `#extension GL_OES_standard_derivatives : enable` + "\n\n"
case GLSLVersionES300:
prefix = `#version 300 es` + "\n\n"
}
return prefix + `#if defined(GL_ES)
precision highp float; precision highp float;
#else #else
#define lowp #define lowp
#define mediump #define mediump
#define highp #define highp
#endif` #endif`
}
type compileContext struct { type compileContext struct {
version GLSLVersion
structNames map[string]string structNames map[string]string
structTypes []shaderir.Type structTypes []shaderir.Type
} }
@ -51,14 +76,16 @@ func (c *compileContext) structName(p *shaderir.Program, t *shaderir.Type) strin
return n return n
} }
func Compile(p *shaderir.Program) (vertexShader, fragmentShader string) { func Compile(p *shaderir.Program, version GLSLVersion) (vertexShader, fragmentShader string) {
c := &compileContext{ c := &compileContext{
version: version,
structNames: map[string]string{}, structNames: map[string]string{},
} }
// Vertex func // Vertex func
var vslines []string var vslines []string
{ {
vslines = append(vslines, strings.Split(VertexPrelude(version), "\n")...)
vslines = append(vslines, "{{.Structs}}") vslines = append(vslines, "{{.Structs}}")
if len(p.Uniforms) > 0 || p.TextureNum > 0 || len(p.Attributes) > 0 || len(p.Varyings) > 0 { if len(p.Uniforms) > 0 || p.TextureNum > 0 || len(p.Attributes) > 0 || len(p.Varyings) > 0 {
vslines = append(vslines, "") vslines = append(vslines, "")
@ -69,10 +96,18 @@ func Compile(p *shaderir.Program) (vertexShader, fragmentShader string) {
vslines = append(vslines, fmt.Sprintf("uniform sampler2D T%d;", i)) vslines = append(vslines, fmt.Sprintf("uniform sampler2D T%d;", i))
} }
for i, t := range p.Attributes { for i, t := range p.Attributes {
vslines = append(vslines, fmt.Sprintf("attribute %s;", c.glslVarDecl(p, &t, fmt.Sprintf("A%d", i)))) keyword := "attribute"
if version == GLSLVersionES300 {
keyword = "in"
}
vslines = append(vslines, fmt.Sprintf("%s %s;", keyword, c.glslVarDecl(p, &t, fmt.Sprintf("A%d", i))))
} }
for i, t := range p.Varyings { for i, t := range p.Varyings {
vslines = append(vslines, fmt.Sprintf("varying %s;", c.glslVarDecl(p, &t, fmt.Sprintf("V%d", i)))) keyword := "varying"
if version == GLSLVersionES300 {
keyword = "out"
}
vslines = append(vslines, fmt.Sprintf("%s %s;", keyword, c.glslVarDecl(p, &t, fmt.Sprintf("V%d", i))))
} }
} }
if len(p.Funcs) > 0 { if len(p.Funcs) > 0 {
@ -99,7 +134,7 @@ func Compile(p *shaderir.Program) (vertexShader, fragmentShader string) {
// Fragment func // Fragment func
var fslines []string var fslines []string
{ {
fslines = append(fslines, strings.Split(FragmentPrelude, "\n")...) fslines = append(fslines, strings.Split(FragmentPrelude(version), "\n")...)
fslines = append(fslines, "", "{{.Structs}}") fslines = append(fslines, "", "{{.Structs}}")
if len(p.Uniforms) > 0 || p.TextureNum > 0 || len(p.Varyings) > 0 { if len(p.Uniforms) > 0 || p.TextureNum > 0 || len(p.Varyings) > 0 {
fslines = append(fslines, "") fslines = append(fslines, "")
@ -110,9 +145,17 @@ func Compile(p *shaderir.Program) (vertexShader, fragmentShader string) {
fslines = append(fslines, fmt.Sprintf("uniform sampler2D T%d;", i)) fslines = append(fslines, fmt.Sprintf("uniform sampler2D T%d;", i))
} }
for i, t := range p.Varyings { for i, t := range p.Varyings {
fslines = append(fslines, fmt.Sprintf("varying %s;", c.glslVarDecl(p, &t, fmt.Sprintf("V%d", i)))) keyword := "varying"
if version == GLSLVersionES300 {
keyword = "in"
}
fslines = append(fslines, fmt.Sprintf("%s %s;", keyword, c.glslVarDecl(p, &t, fmt.Sprintf("V%d", i))))
} }
} }
if version == GLSLVersionES300 {
fslines = append(fslines, "out vec4 fragColor;")
}
if len(p.Funcs) > 0 { if len(p.Funcs) > 0 {
fslines = append(fslines, "") fslines = append(fslines, "")
for _, f := range p.Funcs { for _, f := range p.Funcs {
@ -273,7 +316,7 @@ func constantToNumberLiteral(t shaderir.ConstType, v constant.Value) string {
return fmt.Sprintf("?(unexpected literal: %s)", v) return fmt.Sprintf("?(unexpected literal: %s)", v)
} }
func localVariableName(p *shaderir.Program, topBlock, block *shaderir.Block, idx int) string { func (c *compileContext) localVariableName(p *shaderir.Program, topBlock, block *shaderir.Block, idx int) string {
switch topBlock { switch topBlock {
case p.VertexFunc.Block: case p.VertexFunc.Block:
na := len(p.Attributes) na := len(p.Attributes)
@ -296,6 +339,9 @@ func localVariableName(p *shaderir.Program, topBlock, block *shaderir.Block, idx
case idx < nv+1: case idx < nv+1:
return fmt.Sprintf("V%d", idx-1) return fmt.Sprintf("V%d", idx-1)
case idx == nv+1: case idx == nv+1:
if c.version == GLSLVersionES300 {
return "fragColor"
}
return "gl_FragColor" return "gl_FragColor"
default: default:
return fmt.Sprintf("l%d", idx-(nv+2)) return fmt.Sprintf("l%d", idx-(nv+2))
@ -307,7 +353,7 @@ func localVariableName(p *shaderir.Program, topBlock, block *shaderir.Block, idx
func (c *compileContext) initVariable(p *shaderir.Program, topBlock, block *shaderir.Block, index int, decl bool, level int) []string { func (c *compileContext) initVariable(p *shaderir.Program, topBlock, block *shaderir.Block, index int, decl bool, level int) []string {
idt := strings.Repeat("\t", level+1) idt := strings.Repeat("\t", level+1)
name := localVariableName(p, topBlock, block, index) name := c.localVariableName(p, topBlock, block, index)
t := p.LocalVariableType(topBlock, block, index) t := p.LocalVariableType(topBlock, block, index)
var lines []string var lines []string
@ -352,11 +398,11 @@ func (c *compileContext) glslBlock(p *shaderir.Program, topBlock, block *shaderi
case shaderir.TextureVariable: case shaderir.TextureVariable:
return fmt.Sprintf("T%d", e.Index) return fmt.Sprintf("T%d", e.Index)
case shaderir.LocalVariable: case shaderir.LocalVariable:
return localVariableName(p, topBlock, block, e.Index) return c.localVariableName(p, topBlock, block, e.Index)
case shaderir.StructMember: case shaderir.StructMember:
return fmt.Sprintf("M%d", e.Index) return fmt.Sprintf("M%d", e.Index)
case shaderir.BuiltinFuncExpr: case shaderir.BuiltinFuncExpr:
return builtinFuncString(e.BuiltinFunc) return c.builtinFuncString(e.BuiltinFunc)
case shaderir.SwizzlingExpr: case shaderir.SwizzlingExpr:
if !shaderir.IsValidSwizzling(e.Swizzling) { if !shaderir.IsValidSwizzling(e.Swizzling) {
return fmt.Sprintf("?(unexpected swizzling: %s)", e.Swizzling) return fmt.Sprintf("?(unexpected swizzling: %s)", e.Swizzling)
@ -433,7 +479,7 @@ func (c *compileContext) glslBlock(p *shaderir.Program, topBlock, block *shaderi
ct = shaderir.ConstTypeFloat ct = shaderir.ConstTypeFloat
} }
v := localVariableName(p, topBlock, block, s.ForVarIndex) v := c.localVariableName(p, topBlock, block, s.ForVarIndex)
var delta string var delta string
switch val, _ := constant.Float64Val(s.ForDelta); val { switch val, _ := constant.Float64Val(s.ForDelta); val {
case 0: case 0:

View File

@ -63,7 +63,7 @@ func basicTypeString(t shaderir.BasicType) string {
} }
} }
func builtinFuncString(f shaderir.BuiltinFunc) string { func (c *compileContext) builtinFuncString(f shaderir.BuiltinFunc) string {
switch f { switch f {
case shaderir.Atan2: case shaderir.Atan2:
return "atan" return "atan"
@ -71,6 +71,11 @@ func builtinFuncString(f shaderir.BuiltinFunc) string {
return "dFdx" return "dFdx"
case shaderir.Dfdy: case shaderir.Dfdy:
return "dFdy" return "dFdy"
case shaderir.Texture2DF:
if c.version == GLSLVersionES300 {
return "texture"
}
return "texture2D"
default: default:
return string(f) return string(f)
} }

View File

@ -924,3 +924,56 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 {
testPixels("DrawTrianglesShader", dst) testPixels("DrawTrianglesShader", dst)
}) })
} }
// Issue #1404
func TestShaderDerivatives(t *testing.T) {
const w, h = 16, 16
s, err := NewShader([]byte(`package main
func Fragment(position vec4, texCoord vec2, color vec4) vec4 {
p := imageSrc0At(texCoord)
return vec4(abs(dfdx(p.r)), abs(dfdy(p.g)), 0, 1)
}
`))
if err != nil {
t.Fatal(err)
}
dst := NewImage(w, h)
src := NewImage(w, h)
pix := make([]byte, 4*w*h)
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
if i < w/2 {
pix[4*(j*w+i)] = 0xff
}
if j < h/2 {
pix[4*(j*w+i)+1] = 0xff
}
pix[4*(j*w+i)+3] = 0xff
}
}
src.ReplacePixels(pix)
op := &DrawRectShaderOptions{}
op.Images[0] = src
dst.DrawRectShader(w, h, s, op)
// The results of the edges might be unreliable. Skip the edges.
for j := 1; j < h-1; j++ {
for i := 1; i < w-1; i++ {
got := dst.At(i, j).(color.RGBA)
want := color.RGBA{0, 0, 0, 0xff}
if i == w/2-1 || i == w/2 {
want.R = 0xff
}
if j == h/2-1 || j == h/2 {
want.G = 0xff
}
if got != want {
t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
}