From 49582519c117fa2cd028f596efae633c91f44b2d Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 16 Apr 2023 18:56:14 +0900 Subject: [PATCH] all: add a compiler directive kage:unit This change adds a new compiler directive 'kage:unit' to Kage. This takes one of these two values: 'pixel' and 'texel'. The default value is 'texel'. With the pixel-unit mode, all the built-in functions treats pixels instead of texels, and the texCoord argument of Fragment is in pixels. This simplifies shader programs as programs no longer have the notion of texels. With the texel-unit mode, the behavior is the same as the current behavior. Closes #1431 --- examples/shader/chromaticaberration.go | 10 +- examples/shader/default.go | 7 +- examples/shader/dissolve.go | 3 +- examples/shader/lighting.go | 8 +- examples/shader/main.go | 5 +- examples/shader/radialblur.go | 11 +- examples/shader/texel.go | 4 +- examples/shader/water.go | 21 ++- gameforui.go | 20 ++- internal/atlas/image.go | 15 +- internal/atlas/shader.go | 4 + internal/builtinshader/shader.go | 15 +- internal/graphics/shader.go | 49 ++++--- internal/graphicscommand/command.go | 17 ++- internal/graphicscommand/image_test.go | 16 +-- internal/graphicscommand/shader.go | 4 + internal/restorable/image.go | 24 +--- internal/restorable/images_test.go | 74 +++++----- internal/restorable/shader.go | 8 +- internal/restorable/shader_test.go | 10 +- internal/shader/shader.go | 43 ++++++ internal/shader/shader_test.go | 6 +- internal/shader/syntax_test.go | 86 +++++++++++ internal/shaderir/glsl/glsl.go | 8 +- internal/shaderir/glsl/type.go | 3 + internal/shaderir/hlsl/hlsl.go | 18 ++- internal/shaderir/ir_test.go | 33 ++++- internal/shaderir/msl/msl.go | 23 ++- internal/shaderir/program.go | 8 ++ internal/testing/shader.go | 8 +- shader_test.go | 189 ++++++++++++++++++++----- 31 files changed, 544 insertions(+), 206 deletions(-) diff --git a/examples/shader/chromaticaberration.go b/examples/shader/chromaticaberration.go index 41546eb1b..516f785ff 100644 --- a/examples/shader/chromaticaberration.go +++ b/examples/shader/chromaticaberration.go @@ -14,17 +14,17 @@ //go:build ignore +//kage:unit pixel + package main var Time float var Cursor vec2 -var ScreenSize vec2 func Fragment(position vec4, texCoord vec2, color vec4) vec4 { - center := ScreenSize / 2 - // Convert a pixel to a texel by dividing by the texture size. - // TODO: This is confusing. Add a function to treat pixels (#1431). - amount := (center - Cursor) / 10 / imageSrcTextureSize() + _, dstSize := imageDstRegionOnTexture() + center := dstSize / 2 + amount := (center - Cursor) / 10 var clr vec3 clr.r = imageSrc2At(texCoord + amount).r clr.g = imageSrc2UnsafeAt(texCoord).g diff --git a/examples/shader/default.go b/examples/shader/default.go index 75dcb695f..15d5d2ca8 100644 --- a/examples/shader/default.go +++ b/examples/shader/default.go @@ -14,16 +14,17 @@ //go:build ignore +//kage:unit pixel + package main var Time float var Cursor vec2 -var ScreenSize vec2 func Fragment(position vec4, texCoord vec2, color vec4) vec4 { dstOrigin, dstSize := imageDstRegionOnTexture() - pos := (position.xy/imageDstTextureSize() - dstOrigin) / dstSize - pos += Cursor / ScreenSize / 4 + pos := (position.xy - dstOrigin) / dstSize + pos += Cursor / dstSize / 4 clr := 0.0 clr += sin(pos.x*cos(Time/15)*80) + cos(pos.y*cos(Time/15)*10) clr += sin(pos.y*sin(Time/10)*40) + cos(pos.x*sin(Time/25)*40) diff --git a/examples/shader/dissolve.go b/examples/shader/dissolve.go index 49852263b..765ed565d 100644 --- a/examples/shader/dissolve.go +++ b/examples/shader/dissolve.go @@ -14,11 +14,12 @@ //go:build ignore +//kage:unit pixel + package main var Time float var Cursor vec2 -var ScreenSize vec2 func Fragment(position vec4, texCoord vec2, color vec4) vec4 { // Triangle wave to go 0-->1-->0... diff --git a/examples/shader/lighting.go b/examples/shader/lighting.go index fb2b22e00..01236910f 100644 --- a/examples/shader/lighting.go +++ b/examples/shader/lighting.go @@ -14,16 +14,16 @@ //go:build ignore +//kage:unit pixel + package main var Time float var Cursor vec2 -var ScreenSize vec2 func Fragment(position vec4, texCoord vec2, color vec4) vec4 { - srcOrigin, srcSize := imageSrcRegionOnTexture() - pos := (texCoord - srcOrigin) / srcSize - pos *= ScreenSize + dstOrigin, _ := imageDstRegionOnTexture() + pos := position.xy - dstOrigin lightpos := vec3(Cursor, 50) lightdir := normalize(lightpos - vec3(pos, 0)) diff --git a/examples/shader/main.go b/examples/shader/main.go index 67e634fa8..a274e5aea 100644 --- a/examples/shader/main.go +++ b/examples/shader/main.go @@ -146,9 +146,8 @@ func (g *Game) Draw(screen *ebiten.Image) { op := &ebiten.DrawRectShaderOptions{} op.Uniforms = map[string]any{ - "Time": float32(g.time) / 60, - "Cursor": []float32{float32(cx), float32(cy)}, - "ScreenSize": []float32{float32(w), float32(h)}, + "Time": float32(g.time) / 60, + "Cursor": []float32{float32(cx), float32(cy)}, } op.Images[0] = gopherImage op.Images[1] = normalImage diff --git a/examples/shader/radialblur.go b/examples/shader/radialblur.go index 4b50d3757..55d0e1f6a 100644 --- a/examples/shader/radialblur.go +++ b/examples/shader/radialblur.go @@ -14,16 +14,16 @@ //go:build ignore +//kage:unit pixel + package main var Time float var Cursor vec2 -var ScreenSize vec2 func Fragment(position vec4, texCoord vec2, color vec4) vec4 { - srcOrigin, srcSize := imageSrcRegionOnTexture() - pos := (texCoord - srcOrigin) / srcSize - pos *= ScreenSize + dstOrigin, _ := imageDstRegionOnTexture() + pos := position.xy - dstOrigin dir := normalize(pos - Cursor) clr := imageSrc2UnsafeAt(texCoord) @@ -33,8 +33,7 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { } sum := clr for i := 0; i < len(samples); i++ { - pos := texCoord + dir*samples[i]/imageSrcTextureSize() - sum += imageSrc2At(pos) + sum += imageSrc2At(texCoord + dir*samples[i]) } sum /= 10 + 1 diff --git a/examples/shader/texel.go b/examples/shader/texel.go index 5a2000e13..7b8d4dab0 100644 --- a/examples/shader/texel.go +++ b/examples/shader/texel.go @@ -14,10 +14,12 @@ //go:build ignore +//kage:unit pixel + package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { - pos := position.xy / imageDstTextureSize() + pos := position.xy origin, size := imageDstRegionOnTexture() pos -= origin pos /= size diff --git a/examples/shader/water.go b/examples/shader/water.go index 4dcc76501..cb57c3e11 100644 --- a/examples/shader/water.go +++ b/examples/shader/water.go @@ -14,31 +14,28 @@ //go:build ignore +//kage:unit pixel + package main var Time float var Cursor vec2 -var ScreenSize vec2 func Fragment(position vec4, texCoord vec2, color vec4) vec4 { - srcOrigin, srcSize := imageSrcRegionOnTexture() - pos := (texCoord - srcOrigin) / srcSize - pos *= ScreenSize + dstOrigin, dstSize := imageDstRegionOnTexture() + pos := position.xy - dstOrigin - border := ScreenSize.y*0.6 + 4*cos(Time*3+pos.y/10) + border := dstSize.y*0.6 + 4*cos(Time*3+pos.y/10) if pos.y < border { return imageSrc2UnsafeAt(texCoord) } - // Convert a pixel to a texel by dividing by the texture size. - // TODO: This is confusing. Add a function to treat pixels (#1431). - srcTexSize := imageSrcTextureSize() - xoffset := (4 / srcTexSize.x) * cos(Time*3+pos.y/10) - yoffset := (20 / srcTexSize.y) * (1 + cos(Time*3+pos.y/40)) - bordertex := border / srcTexSize.y + xoffset := 4 * cos(Time*3+pos.y/10) + yoffset := 20 * (1 + cos(Time*3+pos.y/40)) + srcOrigin, _ := imageSrcRegionOnTexture() clr := imageSrc2At(vec2( texCoord.x+xoffset, - -(texCoord.y+yoffset-srcOrigin.y)+bordertex*2+srcOrigin.y, + -(texCoord.y+yoffset-srcOrigin.y)+border*2+srcOrigin.y, )).rgb overlay := vec3(0.5, 1, 1) diff --git a/gameforui.go b/gameforui.go index f75569d57..edcb27d34 100644 --- a/gameforui.go +++ b/gameforui.go @@ -24,23 +24,19 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/ui" ) -const screenShaderSrc = `package main +const screenShaderSrc = `//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { - // TODO: Calculate the scale in the shader after pixels become the main unit in shaders (#1431) _, dr := imageDstRegionOnTexture() _, sr := imageSrcRegionOnTexture() - scale := (imageDstTextureSize() * dr) / (imageSrcTextureSize() * sr) + scale := dr/sr - sourceSize := imageSrcTextureSize() - // texelSize is one pixel size in texel sizes. - texelSize := 1 / sourceSize - halfScaledTexelSize := texelSize / 2 / scale - - // Shift 1/512 [texel] to avoid the tie-breaking issue. + // Shift 1/512 [pixel] to avoid the tie-breaking issue. pos := texCoord - p0 := pos - halfScaledTexelSize + (texelSize / 512) - p1 := pos + halfScaledTexelSize + (texelSize / 512) + p0 := pos - 1/2.0 + 1/512.0 + p1 := pos + 1/2.0 + 1/512.0 // Texels must be in the source rect, so it is not necessary to check. c0 := imageSrc0UnsafeAt(p0) @@ -49,7 +45,7 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { c3 := imageSrc0UnsafeAt(p1) // p is the p1 value in one pixel assuming that the pixel's upper-left is (0, 0) and the lower-right is (1, 1). - p := fract(p1 * sourceSize) + p := fract(p1) // rate indicates how much the 4 colors are mixed. rate is in between [0, 1]. // diff --git a/internal/atlas/image.go b/internal/atlas/image.go index c5e90c41e..05e203c84 100644 --- a/internal/atlas/image.go +++ b/internal/atlas/image.go @@ -24,6 +24,7 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/packing" "github.com/hajimehoshi/ebiten/v2/internal/restorable" + "github.com/hajimehoshi/ebiten/v2/internal/shaderir" ) var ( @@ -455,14 +456,20 @@ func (i *Image) drawTriangles(srcs [graphics.ShaderImageCount]*Image, vertices [ ox, oy, _, _ := srcs[0].regionWithPadding() ps := srcs[0].paddingSize() oxf, oyf = float32(ox+ps), float32(oy+ps) - sw, sh := srcs[0].backend.restorable.InternalSize() - swf, shf := float32(sw), float32(sh) n := len(vertices) for i := 0; i < n; i += graphics.VertexFloatCount { vertices[i] += dx vertices[i+1] += dy - vertices[i+2] = (vertices[i+2] + oxf) / swf - vertices[i+3] = (vertices[i+3] + oyf) / shf + vertices[i+2] += oxf + vertices[i+3] += oyf + } + if shader.unit() == shaderir.Texel { + sw, sh := srcs[0].backend.restorable.InternalSize() + swf, shf := float32(sw), float32(sh) + for i := 0; i < n; i += graphics.VertexFloatCount { + vertices[i+2] /= swf + vertices[i+3] /= shf + } } // srcRegion can be deliberately empty when this is not needed in order to avoid unexpected // performance issue (#1293). diff --git a/internal/atlas/shader.go b/internal/atlas/shader.go index 0fc9fa7f7..77585c10a 100644 --- a/internal/atlas/shader.go +++ b/internal/atlas/shader.go @@ -36,6 +36,10 @@ func NewShader(ir *shaderir.Program) *Shader { return s } +func (s *Shader) unit() shaderir.Unit { + return s.shader.Unit() +} + // MarkDisposed marks the shader as disposed. The actual operation is deferred. // MarkDisposed can be called from finalizers. // diff --git a/internal/builtinshader/shader.go b/internal/builtinshader/shader.go index d1a875d95..f07d9d156 100644 --- a/internal/builtinshader/shader.go +++ b/internal/builtinshader/shader.go @@ -52,7 +52,9 @@ var ( shadersM sync.Mutex ) -var tmpl = template.Must(template.New("tmpl").Parse(`package main +var tmpl = template.Must(template.New("tmpl").Parse(`//kage:unit pixel + +package main {{if .UseColorM}} var ColorMBody mat4 @@ -76,12 +78,9 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { clr := imageSrc0At(adjustTexelForAddressRepeat(texCoord)) {{end}} {{else if eq .Filter .FilterLinear}} - sourceSize := imageSrcTextureSize() - texelSize := 1 / sourceSize - - // Shift 1/512 [texel] to avoid the tie-breaking issue (#1212). - p0 := texCoord - texelSize/2 + texelSize/512 - p1 := texCoord + texelSize/2 + texelSize/512 + // Shift 1/512 [pixel] to avoid the tie-breaking issue (#1212). + p0 := texCoord - 1/2.0 + 1/512.0 + p1 := texCoord + 1/2.0 + 1/512.0 {{if eq .Address .AddressRepeat}} p0 = adjustTexelForAddressRepeat(p0) @@ -100,7 +99,7 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { c3 := imageSrc0At(p1) {{end}} - rate := fract(p0 * sourceSize) + rate := fract(p0) clr := mix(mix(c0, c1, rate.x), mix(c2, c3, rate.x), rate.y) {{end}} diff --git a/internal/graphics/shader.go b/internal/graphics/shader.go index f37256524..704728103 100644 --- a/internal/graphics/shader.go +++ b/internal/graphics/shader.go @@ -22,10 +22,8 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/shaderir" ) -var shaderSuffix string - -func init() { - shaderSuffix = ` +func shaderSuffix(unit shaderir.Unit) (string, error) { + shaderSuffix := ` var __imageDstTextureSize vec2 // imageSrcTextureSize returns the destination image's texture size in pixels. @@ -39,36 +37,36 @@ var __textureSizes [%[1]d]vec2 // imageSrcTextureSize returns the source image's texture size in pixels. // As an image is a part of internal texture, the texture is usually bigger than the image. -// The texture's size is useful when you want to calculate pixels from texels. +// The texture's size is useful when you want to calculate pixels from texels in the texel mode. func imageSrcTextureSize() vec2 { return __textureSizes[0] } -// The unit is the source texture's texel. +// The unit is the source texture's pixel or texel. var __textureDestinationRegionOrigin vec2 -// The unit is the source texture's texel. +// The unit is the source texture's pixel or texel. var __textureDestinationRegionSize vec2 // imageDstRegionOnTexture returns the destination image's region (the origin and the size) on its texture. -// The unit is the source texture's texel. +// The unit is the source texture's pixel or texel. // // As an image is a part of internal texture, the image can be located at an arbitrary position on the texture. func imageDstRegionOnTexture() (vec2, vec2) { return __textureDestinationRegionOrigin, __textureDestinationRegionSize } -// The unit is the source texture's texel. +// The unit is the source texture's pixel or texel. var __textureSourceOffsets [%[2]d]vec2 -// The unit is the source texture's texel. +// The unit is the source texture's pixel or texel. var __textureSourceRegionOrigin vec2 -// The unit is the source texture's texel. +// The unit is the source texture's pixel or texel. var __textureSourceRegionSize vec2 // imageSrcRegionOnTexture returns the source image's region (the origin and the size) on its texture. -// The unit is the source texture's texel. +// The unit is the source texture's pixel or texel. // // As an image is a part of internal texture, the image can be located at an arbitrary position on the texture. func imageSrcRegionOnTexture() (vec2, vec2) { @@ -79,18 +77,25 @@ func imageSrcRegionOnTexture() (vec2, vec2) { for i := 0; i < ShaderImageCount; i++ { pos := "pos" if i >= 1 { - // Convert the position in texture0's texels to the target texture texels. - pos = fmt.Sprintf("(pos + __textureSourceOffsets[%d]) * __textureSizes[0] / __textureSizes[%d]", i-1, i) + // Convert the position in texture0's positions to the target texture positions. + switch unit { + case shaderir.Pixel: + pos = fmt.Sprintf("pos + __textureSourceOffsets[%d]", i-1) + case shaderir.Texel: + pos = fmt.Sprintf("(pos + __textureSourceOffsets[%d]) * __textureSizes[0] / __textureSizes[%d]", i-1, i) + default: + return "", fmt.Errorf("graphics: unexpected unit: %d", unit) + } } // __t%d is a special variable for a texture variable. shaderSuffix += fmt.Sprintf(` func imageSrc%[1]dUnsafeAt(pos vec2) vec4 { - // pos is the position in texels of the source texture (= 0th image's texture). + // pos is the position in positions of the source texture (= 0th image's texture). return texture2D(__t%[1]d, %[2]s) } func imageSrc%[1]dAt(pos vec2) vec4 { - // pos is the position in texels of the source texture (= 0th image's texture). + // pos is the position of the source texture (= 0th image's texture). // If pos is in the region, the result is (1, 1). Otherwise, either element is 0. in := step(__textureSourceRegionOrigin, pos) - step(__textureSourceRegionOrigin + __textureSourceRegionSize, pos) return texture2D(__t%[1]d, %[2]s) * in.x * in.y @@ -105,12 +110,22 @@ func __vertex(position vec2, texCoord vec2, color vec4) (vec4, vec2, vec4) { return __projectionMatrix * vec4(position, 0, 1), texCoord, color } ` + return shaderSuffix, nil } func CompileShader(src []byte) (*shaderir.Program, error) { + unit, err := shader.ParseCompilerDirectives(src) + if err != nil { + return nil, err + } + suffix, err := shaderSuffix(unit) + if err != nil { + return nil, err + } + var buf bytes.Buffer buf.Write(src) - buf.WriteString(shaderSuffix) + buf.WriteString(suffix) const ( vert = "__vertex" diff --git a/internal/graphicscommand/command.go b/internal/graphicscommand/command.go index d00c6e370..977a02a7d 100644 --- a/internal/graphicscommand/command.go +++ b/internal/graphicscommand/command.go @@ -599,16 +599,23 @@ func (q *commandQueue) prependPreservedUniforms(uniforms []uint32, shader *Shade } idx += len(srcs) * 2 + if shader.unit() == shaderir.Texel { + dstRegion.X /= float32(dw) + dstRegion.Y /= float32(dh) + dstRegion.Width /= float32(dw) + dstRegion.Height /= float32(dh) + } + // Set the destination region. - uniforms[idx+0] = math.Float32bits(dstRegion.X / float32(dw)) - uniforms[idx+1] = math.Float32bits(dstRegion.Y / float32(dh)) + uniforms[idx+0] = math.Float32bits(dstRegion.X) + uniforms[idx+1] = math.Float32bits(dstRegion.Y) idx += 2 - uniforms[idx+0] = math.Float32bits(dstRegion.Width / float32(dw)) - uniforms[idx+1] = math.Float32bits(dstRegion.Height / float32(dh)) + uniforms[idx+0] = math.Float32bits(dstRegion.Width) + uniforms[idx+1] = math.Float32bits(dstRegion.Height) idx += 2 - if srcs[0] != nil { + if shader.unit() == shaderir.Texel && srcs[0] != nil { w, h := srcs[0].InternalSize() srcRegion.X /= float32(w) srcRegion.Y /= float32(h) diff --git a/internal/graphicscommand/image_test.go b/internal/graphicscommand/image_test.go index 72d7d7fa2..be0936a22 100644 --- a/internal/graphicscommand/image_test.go +++ b/internal/graphicscommand/image_test.go @@ -41,14 +41,12 @@ func TestMain(m *testing.M) { etesting.MainWithRunLoop(m) } -func quadVertices(srcImage *graphicscommand.Image, w, h float32) []float32 { - sw, sh := srcImage.InternalSize() - swf, shf := float32(sw), float32(sh) +func quadVertices(w, h float32) []float32 { return []float32{ 0, 0, 0, 0, 1, 1, 1, 1, - w, 0, w / swf, 0, 1, 1, 1, 1, - 0, w, 0, h / shf, 1, 1, 1, 1, - w, h, w / swf, h / shf, 1, 1, 1, 1, + w, 0, w, 0, 1, 1, 1, 1, + 0, w, 0, h, 1, 1, 1, 1, + w, h, w, h, 1, 1, 1, 1, } } @@ -57,7 +55,7 @@ func TestClear(t *testing.T) { src := graphicscommand.NewImage(w/2, h/2, false) dst := graphicscommand.NewImage(w, h, false) - vs := quadVertices(src, w/2, h/2) + vs := quadVertices(w/2, h/2) is := graphics.QuadIndices() dr := graphicsdriver.Region{ X: 0, @@ -88,7 +86,7 @@ func TestWritePixelsPartAfterDrawTriangles(t *testing.T) { clr := graphicscommand.NewImage(w, h, false) src := graphicscommand.NewImage(w/2, h/2, false) dst := graphicscommand.NewImage(w, h, false) - vs := quadVertices(src, w/2, h/2) + vs := quadVertices(w/2, h/2) is := graphics.QuadIndices() dr := graphicsdriver.Region{ X: 0, @@ -107,7 +105,7 @@ func TestShader(t *testing.T) { const w, h = 16, 16 clr := graphicscommand.NewImage(w, h, false) dst := graphicscommand.NewImage(w, h, false) - vs := quadVertices(clr, w, h) + vs := quadVertices(w, h) is := graphics.QuadIndices() dr := graphicsdriver.Region{ X: 0, diff --git a/internal/graphicscommand/shader.go b/internal/graphicscommand/shader.go index 55c51e20a..21625f3b2 100644 --- a/internal/graphicscommand/shader.go +++ b/internal/graphicscommand/shader.go @@ -42,3 +42,7 @@ func (s *Shader) Dispose() { } theCommandQueue.Enqueue(c) } + +func (s *Shader) unit() shaderir.Unit { + return s.ir.Unit +} diff --git a/internal/restorable/image.go b/internal/restorable/image.go index 6a0658f1f..632f71d6d 100644 --- a/internal/restorable/image.go +++ b/internal/restorable/image.go @@ -176,7 +176,7 @@ func (i *Image) Extend(width, height int) *Image { srcs := [graphics.ShaderImageCount]*Image{i} var offsets [graphics.ShaderImageCount - 1][2]float32 sw, sh := i.image.InternalSize() - vs := quadVertices(i, 0, 0, float32(sw), float32(sh), 0, 0, float32(sw), float32(sh), 1, 1, 1, 1) + vs := quadVertices(0, 0, float32(sw), float32(sh), 0, 0, float32(sw), float32(sh), 1, 1, 1, 1) is := graphics.QuadIndices() dr := graphicsdriver.Region{ X: 0, @@ -191,22 +191,12 @@ func (i *Image) Extend(width, height int) *Image { } // quadVertices returns vertices to render a quad. These values are passed to graphicscommand.Image. -func quadVertices(src *Image, dx0, dy0, dx1, dy1, sx0, sy0, sx1, sy1, cr, cg, cb, ca float32) []float32 { - if src == nil { - return []float32{ - dx0, dy0, 0, 0, cr, cg, cb, ca, - dx1, dy0, 0, 0, cr, cg, cb, ca, - dx0, dy1, 0, 0, cr, cg, cb, ca, - dx1, dy1, 0, 0, cr, cg, cb, ca, - } - } - sw, sh := src.InternalSize() - swf, shf := float32(sw), float32(sh) +func quadVertices(dx0, dy0, dx1, dy1, sx0, sy0, sx1, sy1, cr, cg, cb, ca float32) []float32 { return []float32{ - dx0, dy0, sx0 / swf, sy0 / shf, cr, cg, cb, ca, - dx1, dy0, sx1 / swf, sy0 / shf, cr, cg, cb, ca, - dx0, dy1, sx0 / swf, sy1 / shf, cr, cg, cb, ca, - dx1, dy1, sx1 / swf, sy1 / shf, cr, cg, cb, ca, + dx0, dy0, sx0, sy0, cr, cg, cb, ca, + dx1, dy0, sx1, sy0, cr, cg, cb, ca, + dx0, dy1, sx0, sy1, cr, cg, cb, ca, + dx1, dy1, sx1, sy1, cr, cg, cb, ca, } } @@ -214,7 +204,7 @@ func clearImage(i *graphicscommand.Image) { // This needs to use 'InternalSize' to render the whole region, or edges are unexpectedly cleared on some // devices. dw, dh := i.InternalSize() - vs := quadVertices(nil, 0, 0, float32(dw), float32(dh), 0, 0, 0, 0, 0, 0, 0, 0) + vs := quadVertices(0, 0, float32(dw), float32(dh), 0, 0, 0, 0, 0, 0, 0, 0) is := graphics.QuadIndices() var offsets [graphics.ShaderImageCount - 1][2]float32 dstRegion := graphicsdriver.Region{ diff --git a/internal/restorable/images_test.go b/internal/restorable/images_test.go index 8cf78ce38..d20fb5d04 100644 --- a/internal/restorable/images_test.go +++ b/internal/restorable/images_test.go @@ -99,7 +99,7 @@ func TestRestoreWithoutDraw(t *testing.T) { } } -func quadVertices(srcImage *restorable.Image, sw, sh, x, y int) []float32 { +func quadVertices(sw, sh, x, y int) []float32 { dx0 := float32(x) dy0 := float32(y) dx1 := float32(x + sw) @@ -108,17 +108,11 @@ func quadVertices(srcImage *restorable.Image, sw, sh, x, y int) []float32 { sy0 := float32(0) sx1 := float32(sw) sy1 := float32(sh) - iswf := float32(1) - ishf := float32(1) - if srcImage != nil { - isw, ish := srcImage.InternalSize() - iswf, ishf = float32(isw), float32(ish) - } return []float32{ - dx0, dy0, sx0 / iswf, sy0 / ishf, 1, 1, 1, 1, - dx1, dy0, sx1 / iswf, sy0 / ishf, 1, 1, 1, 1, - dx0, dy1, sx0 / iswf, sy1 / ishf, 1, 1, 1, 1, - dx1, dy1, sx1 / iswf, sy1 / ishf, 1, 1, 1, 1, + dx0, dy0, sx0, sy0, 1, 1, 1, 1, + dx1, dy0, sx1, sy0, 1, 1, 1, 1, + dx0, dy1, sx0, sy1, 1, 1, 1, 1, + dx1, dy1, sx1, sy1, 1, 1, 1, 1, } } @@ -137,7 +131,7 @@ func TestRestoreChain(t *testing.T) { clr := color.RGBA{A: 0xff} imgs[0].WritePixels([]byte{clr.R, clr.G, clr.B, clr.A}, 0, 0, 1, 1) for i := 0; i < num-1; i++ { - vs := quadVertices(imgs[i], 1, 1, 0, 0) + vs := quadVertices(1, 1, 0, 0) is := graphics.QuadIndices() dr := graphicsdriver.Region{ X: 0, @@ -193,10 +187,10 @@ func TestRestoreChain2(t *testing.T) { Width: w, Height: h, } - imgs[8].DrawTriangles([graphics.ShaderImageCount]*restorable.Image{imgs[7]}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(imgs[7], w, h, 0, 0), is, graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) - imgs[9].DrawTriangles([graphics.ShaderImageCount]*restorable.Image{imgs[8]}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(imgs[8], w, h, 0, 0), is, graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) + imgs[8].DrawTriangles([graphics.ShaderImageCount]*restorable.Image{imgs[7]}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(w, h, 0, 0), is, graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) + imgs[9].DrawTriangles([graphics.ShaderImageCount]*restorable.Image{imgs[8]}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(w, h, 0, 0), is, graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) for i := 0; i < 7; i++ { - imgs[i+1].DrawTriangles([graphics.ShaderImageCount]*restorable.Image{imgs[i]}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(imgs[i], w, h, 0, 0), is, graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) + imgs[i+1].DrawTriangles([graphics.ShaderImageCount]*restorable.Image{imgs[i]}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(w, h, 0, 0), is, graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) } if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting(), false); err != nil { @@ -242,10 +236,10 @@ func TestRestoreOverrideSource(t *testing.T) { Width: w, Height: h, } - img2.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img1}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(img1, w, h, 0, 0), is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) - img3.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img2}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(img2, w, h, 0, 0), is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) + img2.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img1}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(w, h, 0, 0), is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) + img3.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img2}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(w, h, 0, 0), is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) img0.WritePixels([]byte{clr1.R, clr1.G, clr1.B, clr1.A}, 0, 0, w, h) - img1.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img0}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(img0, w, h, 0, 0), is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) + img1.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img0}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(w, h, 0, 0), is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting(), false); err != nil { t.Fatal(err) } @@ -330,23 +324,23 @@ func TestRestoreComplexGraph(t *testing.T) { Height: h, } var offsets [graphics.ShaderImageCount - 1][2]float32 - vs := quadVertices(img0, w, h, 0, 0) + vs := quadVertices(w, h, 0, 0) img3.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img0}, offsets, vs, is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) - vs = quadVertices(img1, w, h, 1, 0) + vs = quadVertices(w, h, 1, 0) img3.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img1}, offsets, vs, is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) - vs = quadVertices(img1, w, h, 1, 0) + vs = quadVertices(w, h, 1, 0) img4.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img1}, offsets, vs, is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) - vs = quadVertices(img2, w, h, 2, 0) + vs = quadVertices(w, h, 2, 0) img4.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img2}, offsets, vs, is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) - vs = quadVertices(img3, w, h, 0, 0) + vs = quadVertices(w, h, 0, 0) img5.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img3}, offsets, vs, is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) - vs = quadVertices(img3, w, h, 0, 0) + vs = quadVertices(w, h, 0, 0) img6.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img3}, offsets, vs, is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) - vs = quadVertices(img4, w, h, 1, 0) + vs = quadVertices(w, h, 1, 0) img6.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img4}, offsets, vs, is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) - vs = quadVertices(img2, w, h, 0, 0) + vs = quadVertices(w, h, 0, 0) img7.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img2}, offsets, vs, is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) - vs = quadVertices(img3, w, h, 2, 0) + vs = quadVertices(w, h, 2, 0) img7.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img3}, offsets, vs, is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting(), false); err != nil { t.Fatal(err) @@ -445,8 +439,8 @@ func TestRestoreRecursive(t *testing.T) { Width: w, Height: h, } - img1.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img0}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(img0, w, h, 1, 0), is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) - img0.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img1}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(img1, w, h, 1, 0), is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) + img1.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img0}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(w, h, 1, 0), is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) + img0.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img1}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(w, h, 1, 0), is, graphicsdriver.BlendSourceOver, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting(), false); err != nil { t.Fatal(err) } @@ -541,7 +535,7 @@ func TestDrawTrianglesAndWritePixels(t *testing.T) { img1 := restorable.NewImage(2, 1, restorable.ImageTypeRegular) defer img1.Dispose() - vs := quadVertices(img0, 1, 1, 0, 0) + vs := quadVertices(1, 1, 0, 0) is := graphics.QuadIndices() dr := graphicsdriver.Region{ X: 0, @@ -592,8 +586,8 @@ func TestDispose(t *testing.T) { Width: 1, Height: 1, } - img1.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img2}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(img2, 1, 1, 0, 0), is, graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) - img0.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img1}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(img1, 1, 1, 0, 0), is, graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) + img1.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img2}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(1, 1, 0, 0), is, graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) + img0.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img1}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(1, 1, 0, 0), is, graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, restorable.NearestFilterShader, nil, false) img1.Dispose() if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting(), false); err != nil { @@ -699,7 +693,7 @@ func TestWritePixelsOnly(t *testing.T) { img0.WritePixels([]byte{1, 2, 3, 4}, i%w, i/w, 1, 1) } - vs := quadVertices(img0, 1, 1, 0, 0) + vs := quadVertices(1, 1, 0, 0) is := graphics.QuadIndices() dr := graphicsdriver.Region{ X: 0, @@ -758,7 +752,7 @@ func TestReadPixelsFromVolatileImage(t *testing.T) { pix[i] = 0xff } src.WritePixels(pix, 0, 0, w, h) - vs := quadVertices(src, 1, 1, 0, 0) + vs := quadVertices(1, 1, 0, 0) is := graphics.QuadIndices() dr := graphicsdriver.Region{ X: 0, @@ -787,7 +781,7 @@ func TestAllowWritePixelsAfterDrawTriangles(t *testing.T) { src := restorable.NewImage(w, h, restorable.ImageTypeRegular) dst := restorable.NewImage(w, h, restorable.ImageTypeRegular) - vs := quadVertices(src, w, h, 0, 0) + vs := quadVertices(w, h, 0, 0) is := graphics.QuadIndices() dr := graphicsdriver.Region{ X: 0, @@ -811,7 +805,7 @@ func TestAllowWritePixelsForPartAfterDrawTriangles(t *testing.T) { } src.WritePixels(pix, 0, 0, w, h) - vs := quadVertices(src, w, h, 0, 0) + vs := quadVertices(w, h, 0, 0) is := graphics.QuadIndices() dr := graphicsdriver.Region{ X: 0, @@ -910,7 +904,7 @@ func TestDrawTrianglesAndExtend(t *testing.T) { src.WritePixels(pix, 0, 0, w, h) orig := restorable.NewImage(w, h, restorable.ImageTypeRegular) - vs := quadVertices(src, w, h, 0, 0) + vs := quadVertices(w, h, 0, 0) is := graphics.QuadIndices() dr := graphicsdriver.Region{ X: 0, @@ -964,7 +958,7 @@ func TestMutateSlices(t *testing.T) { } src.WritePixels(pix, 0, 0, w, h) - vs := quadVertices(src, w, h, 0, 0) + vs := quadVertices(w, h, 0, 0) is := make([]uint16, len(graphics.QuadIndices())) copy(is, graphics.QuadIndices()) dr := graphicsdriver.Region{ @@ -1162,7 +1156,7 @@ func TestDrawTrianglesAndReadPixels(t *testing.T) { src.WritePixels([]byte{0x80, 0x80, 0x80, 0x80}, 0, 0, 1, 1) - vs := quadVertices(src, w, h, 0, 0) + vs := quadVertices(w, h, 0, 0) is := graphics.QuadIndices() dr := graphicsdriver.Region{ X: 0, @@ -1191,7 +1185,7 @@ func TestWritePixelsAndDrawTriangles(t *testing.T) { dst.WritePixels([]byte{0x40, 0x40, 0x40, 0x40}, 0, 0, 1, 1) // Call DrawTriangles at a different region second. - vs := quadVertices(src, 1, 1, 1, 0) + vs := quadVertices(1, 1, 1, 0) is := graphics.QuadIndices() dr := graphicsdriver.Region{ X: 1, diff --git a/internal/restorable/shader.go b/internal/restorable/shader.go index 80bfdce6b..6407ce0a3 100644 --- a/internal/restorable/shader.go +++ b/internal/restorable/shader.go @@ -50,6 +50,10 @@ func (s *Shader) restore() { s.shader = graphicscommand.NewShader(s.ir) } +func (s *Shader) Unit() shaderir.Unit { + return s.ir.Unit +} + var ( NearestFilterShader *Shader LinearFilterShader *Shader @@ -76,7 +80,9 @@ func init() { return nil }) wg.Go(func() error { - ir, err := graphics.CompileShader([]byte(`package main + ir, err := graphics.CompileShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { return vec4(0) diff --git a/internal/restorable/shader_test.go b/internal/restorable/shader_test.go index 7b423e8c7..8f97f5884 100644 --- a/internal/restorable/shader_test.go +++ b/internal/restorable/shader_test.go @@ -64,7 +64,7 @@ func TestShader(t *testing.T) { Width: 1, Height: 1, } - img.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(nil, 1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, s, nil, false) + img.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, s, nil, false) if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting(), false); err != nil { t.Fatal(err) @@ -99,7 +99,7 @@ func TestShaderChain(t *testing.T) { Width: 1, Height: 1, } - imgs[i+1].DrawTriangles([graphics.ShaderImageCount]*restorable.Image{imgs[i]}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(imgs[i], 1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, s, nil, false) + imgs[i+1].DrawTriangles([graphics.ShaderImageCount]*restorable.Image{imgs[i]}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, s, nil, false) } if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting(), false); err != nil { @@ -137,7 +137,7 @@ func TestShaderMultipleSources(t *testing.T) { Width: 1, Height: 1, } - dst.DrawTriangles(srcs, offsets, quadVertices(srcs[0], 1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, s, nil, false) + dst.DrawTriangles(srcs, offsets, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, s, nil, false) // Clear one of the sources after DrawTriangles. dst should not be affected. clearImage(srcs[0], 1, 1) @@ -178,7 +178,7 @@ func TestShaderMultipleSourcesOnOneTexture(t *testing.T) { Width: 1, Height: 1, } - dst.DrawTriangles(srcs, offsets, quadVertices(srcs[0], 1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, s, nil, false) + dst.DrawTriangles(srcs, offsets, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, s, nil, false) // Clear one of the sources after DrawTriangles. dst should not be affected. clearImage(srcs[0], 3, 1) @@ -208,7 +208,7 @@ func TestShaderDispose(t *testing.T) { Width: 1, Height: 1, } - img.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(nil, 1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, s, nil, false) + img.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{}, [graphics.ShaderImageCount - 1][2]float32{}, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, graphicsdriver.Region{}, s, nil, false) // Dispose the shader. This should invalidate all the images using this shader i.e., all the images become // stale. diff --git a/internal/shader/shader.go b/internal/shader/shader.go index 916b8b873..c6cdb8b3a 100644 --- a/internal/shader/shader.go +++ b/internal/shader/shader.go @@ -15,11 +15,14 @@ package shader import ( + "bufio" + "bytes" "fmt" "go/ast" gconstant "go/constant" "go/parser" "go/token" + "regexp" "strings" "github.com/hajimehoshi/ebiten/v2/internal/shaderir" @@ -49,6 +52,7 @@ type compileState struct { vertexEntry string fragmentEntry string + unit shaderir.Unit ir shaderir.Program @@ -182,6 +186,11 @@ func (p *ParseError) Error() string { } func Compile(src []byte, vertexEntry, fragmentEntry string, textureCount int) (*shaderir.Program, error) { + unit, err := ParseCompilerDirectives(src) + if err != nil { + return nil, err + } + fs := token.NewFileSet() f, err := parser.ParseFile(fs, "", src, parser.AllErrors) if err != nil { @@ -192,6 +201,7 @@ func Compile(src []byte, vertexEntry, fragmentEntry string, textureCount int) (* fs: fs, vertexEntry: vertexEntry, fragmentEntry: fragmentEntry, + unit: unit, } s.global.ir = &shaderir.Block{} s.parse(f) @@ -209,12 +219,45 @@ func Compile(src []byte, vertexEntry, fragmentEntry string, textureCount int) (* return &s.ir, nil } +func ParseCompilerDirectives(src []byte) (shaderir.Unit, error) { + // TODO: Change the unit to pixels in v3 (#2645). + unit := shaderir.Texel + + reUnit := regexp.MustCompile(`^//kage:unit\s+(.+)$`) + var unitParsed bool + + buf := bytes.NewBuffer(src) + s := bufio.NewScanner(buf) + for s.Scan() { + m := reUnit.FindStringSubmatch(s.Text()) + if m == nil { + continue + } + if unitParsed { + return 0, fmt.Errorf("shader: at most one //kage:unit can exist in a shader") + } + switch m[1] { + case "pixel": + unit = shaderir.Pixel + case "texel": + unit = shaderir.Texel + default: + return 0, fmt.Errorf("shader: invalid value for //kage:unit: %s", m[1]) + } + unitParsed = true + } + + return unit, nil +} + func (s *compileState) addError(pos token.Pos, str string) { p := s.fs.Position(pos) s.errs = append(s.errs, fmt.Sprintf("%s: %s", p, str)) } func (cs *compileState) parse(f *ast.File) { + cs.ir.Unit = cs.unit + // Parse GenDecl for global variables, and then parse functions. for _, d := range f.Decls { if _, ok := d.(*ast.FuncDecl); !ok { diff --git a/internal/shader/shader_test.go b/internal/shader/shader_test.go index 48810a5c2..1199aeff7 100644 --- a/internal/shader/shader_test.go +++ b/internal/shader/shader_test.go @@ -23,6 +23,7 @@ import ( "testing" "github.com/hajimehoshi/ebiten/v2/internal/shader" + "github.com/hajimehoshi/ebiten/v2/internal/shaderir" "github.com/hajimehoshi/ebiten/v2/internal/shaderir/glsl" "github.com/hajimehoshi/ebiten/v2/internal/shaderir/hlsl" "github.com/hajimehoshi/ebiten/v2/internal/shaderir/msl" @@ -52,8 +53,9 @@ func hlslNormalize(str string) string { } func metalNormalize(str string) string { - if strings.HasPrefix(str, msl.Prelude) { - str = str[len(msl.Prelude):] + prelude := msl.Prelude(shaderir.Texel) + if strings.HasPrefix(str, prelude) { + str = str[len(prelude):] } return strings.TrimSpace(str) } diff --git a/internal/shader/syntax_test.go b/internal/shader/syntax_test.go index 55668f617..93327a738 100644 --- a/internal/shader/syntax_test.go +++ b/internal/shader/syntax_test.go @@ -3036,3 +3036,89 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { } } } + +func TestCompilerDirective(t *testing.T) { + cases := []struct { + src string + unit shaderir.Unit + err bool + }{ + { + src: `package main + +func Fragment(position vec4, texCoord vec2, color vec4) vec4 { + return position +}`, + unit: shaderir.Texel, + err: false, + }, + { + src: `//kage:unit texel + +package main + +func Fragment(position vec4, texCoord vec2, color vec4) vec4 { + return position +}`, + unit: shaderir.Texel, + err: false, + }, + { + src: `//kage:unit pixel + +package main + +func Fragment(position vec4, texCoord vec2, color vec4) vec4 { + return position +}`, + unit: shaderir.Pixel, + err: false, + }, + { + src: `//kage:unit foo + +package main + +func Fragment(position vec4, texCoord vec2, color vec4) vec4 { + return position +}`, + err: true, + }, + { + src: `//kage:unit pixel +//kage:unit pixel + +package main + +func Fragment(position vec4, texCoord vec2, color vec4) vec4 { + return position +}`, + err: true, + }, + { + src: `//kage:unit pixel +//kage:unit texel + +package main + +func Fragment(position vec4, texCoord vec2, color vec4) vec4 { + return position +}`, + err: true, + }, + } + for _, c := range cases { + ir, err := compileToIR([]byte(c.src)) + if err == nil && c.err { + t.Errorf("Compile(%q) must return an error but does not", c.src) + } else if err != nil && !c.err { + t.Errorf("Compile(%q) must not return nil but returned %v", c.src, err) + } + if err != nil || c.err { + continue + } + if got, want := ir.Unit, c.unit; got != want { + t.Errorf("Compile(%q).Unit: got: %d, want: %d", c.src, got, want) + } + } +} diff --git a/internal/shaderir/glsl/glsl.go b/internal/shaderir/glsl/glsl.go index 956886322..73f9ecb3f 100644 --- a/internal/shaderir/glsl/glsl.go +++ b/internal/shaderir/glsl/glsl.go @@ -98,6 +98,7 @@ type compileContext struct { version GLSLVersion structNames map[string]string structTypes []shaderir.Type + unit shaderir.Unit } func (c *compileContext) structName(p *shaderir.Program, t *shaderir.Type) string { @@ -120,6 +121,7 @@ func Compile(p *shaderir.Program, version GLSLVersion) (vertexShader, fragmentSh c := &compileContext{ version: version, structNames: map[string]string{}, + unit: p.Unit, } // Vertex func @@ -513,8 +515,12 @@ func (c *compileContext) block(p *shaderir.Program, topBlock, block *shaderir.Bl for _, exp := range e.Exprs[1:] { args = append(args, expr(&exp)) } + f := expr(&e.Exprs[0]) + if f == "texelFetch" { + return fmt.Sprintf("%s(%s, ivec2(%s), 0)", f, args[0], args[1]) + } // Using parentheses at the callee is illegal. - return fmt.Sprintf("%s(%s)", expr(&e.Exprs[0]), strings.Join(args, ", ")) + return fmt.Sprintf("%s(%s)", f, strings.Join(args, ", ")) case shaderir.FieldSelector: return fmt.Sprintf("(%s).%s", expr(&e.Exprs[0]), expr(&e.Exprs[1])) case shaderir.Index: diff --git a/internal/shaderir/glsl/type.go b/internal/shaderir/glsl/type.go index 128931a13..2b3b9e3d2 100644 --- a/internal/shaderir/glsl/type.go +++ b/internal/shaderir/glsl/type.go @@ -122,6 +122,9 @@ func (c *compileContext) builtinFuncString(f shaderir.BuiltinFunc) string { case shaderir.Dfdy: return "dFdy" case shaderir.Texture2DF: + if c.unit == shaderir.Pixel { + return "texelFetch" + } return "texture" default: return string(f) diff --git a/internal/shaderir/hlsl/hlsl.go b/internal/shaderir/hlsl/hlsl.go index aac947223..ad251b5b0 100644 --- a/internal/shaderir/hlsl/hlsl.go +++ b/internal/shaderir/hlsl/hlsl.go @@ -31,6 +31,7 @@ const ( type compileContext struct { structNames map[string]string structTypes []shaderir.Type + unit shaderir.Unit } func (c *compileContext) structName(p *shaderir.Program, t *shaderir.Type) string { @@ -87,7 +88,9 @@ float4x4 float4x4FromScalar(float x) { func Compile(p *shaderir.Program) (vertexShader, pixelShader string, offsets []int) { offsets = calculateMemoryOffsets(p.Uniforms) - c := &compileContext{} + c := &compileContext{ + unit: p.Unit, + } var lines []string lines = append(lines, strings.Split(Prelude, "\n")...) @@ -117,7 +120,9 @@ func Compile(p *shaderir.Program) (vertexShader, pixelShader string, offsets []i for i := 0; i < p.TextureCount; i++ { lines = append(lines, fmt.Sprintf("Texture2D T%[1]d : register(t%[1]d);", i)) } - lines = append(lines, "SamplerState samp : register(s0);") + if c.unit == shaderir.Texel { + lines = append(lines, "SamplerState samp : register(s0);") + } } vslines := make([]string, len(lines)) @@ -472,7 +477,14 @@ func (c *compileContext) block(p *shaderir.Program, topBlock, block *shaderir.Bl return fmt.Sprintf("float4x4FromScalar(%s)", args[0]) } case shaderir.Texture2DF: - return fmt.Sprintf("%s.Sample(samp, %s)", args[0], strings.Join(args[1:], ", ")) + switch c.unit { + case shaderir.Pixel: + return fmt.Sprintf("%s.Load(int3(%s, 0))", args[0], strings.Join(args[1:], ", ")) + case shaderir.Texel: + return fmt.Sprintf("%s.Sample(samp, %s)", args[0], strings.Join(args[1:], ", ")) + default: + panic(fmt.Sprintf("hlsl: unexpected unit: %d", p.Unit)) + } } } return fmt.Sprintf("%s(%s)", expr(&e.Exprs[0]), strings.Join(args, ", ")) diff --git a/internal/shaderir/ir_test.go b/internal/shaderir/ir_test.go index 18c730290..4f823852b 100644 --- a/internal/shaderir/ir_test.go +++ b/internal/shaderir/ir_test.go @@ -163,14 +163,17 @@ func TestOutput(t *testing.T) { Metal string }{ { - Name: "Empty", - Program: shaderir.Program{}, - GlslVS: glsl.VertexPrelude(glsl.GLSLVersionDefault), - GlslFS: glsl.FragmentPrelude(glsl.GLSLVersionDefault), + Name: "Empty", + Program: shaderir.Program{ + Unit: shaderir.Pixel, + }, + GlslVS: glsl.VertexPrelude(glsl.GLSLVersionDefault), + GlslFS: glsl.FragmentPrelude(glsl.GLSLVersionDefault), }, { Name: "Uniform", Program: shaderir.Program{ + Unit: shaderir.Pixel, Uniforms: []shaderir.Type{ {Main: shaderir.Float}, }, @@ -183,6 +186,7 @@ uniform float U0;`, { Name: "UniformStruct", Program: shaderir.Program{ + Unit: shaderir.Pixel, Uniforms: []shaderir.Type{ { Main: shaderir.Struct, @@ -208,6 +212,7 @@ uniform S0 U0;`, { Name: "Vars", Program: shaderir.Program{ + Unit: shaderir.Pixel, Uniforms: []shaderir.Type{ {Main: shaderir.Float}, }, @@ -229,6 +234,7 @@ in vec3 V0;`, { Name: "Func", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -249,6 +255,7 @@ void F0(void) { { Name: "FuncParams", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -277,6 +284,7 @@ void F0(in float l0, in vec2 l1, in vec4 l2, out mat4 l3) { { Name: "FuncReturn", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -310,6 +318,7 @@ float F0(in float l0) { { Name: "FuncLocals", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -344,6 +353,7 @@ void F0(in float l0, out float l1) { { Name: "FuncBlocks", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -398,6 +408,7 @@ void F0(in float l0, out float l1) { { Name: "Add", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -439,6 +450,7 @@ void F0(in float l0, in float l1, out float l2) { { Name: "Selection", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -481,6 +493,7 @@ void F0(in bool l0, in float l1, in float l2, out float l3) { { Name: "Call", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -529,6 +542,7 @@ void F0(in float l0, in float l1, out vec2 l2) { { Name: "BuiltinFunc", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -570,6 +584,7 @@ void F0(in float l0, in float l1, out float l2) { { Name: "FieldSelector", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -609,6 +624,7 @@ void F0(in vec4 l0, out vec2 l1) { { Name: "If", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -673,6 +689,7 @@ void F0(in float l0, in float l1, out float l2) { { Name: "For", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -728,6 +745,7 @@ void F0(in float l0, in float l1, out float l2) { { Name: "For2", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -783,7 +801,7 @@ void F0(in float l0, in float l1, out float l2) { l2 = l4; } }`, - Metal: msl.Prelude + ` + Metal: msl.Prelude(shaderir.Pixel) + ` void F0(float l0, float l1, thread float& l2); @@ -797,6 +815,7 @@ void F0(float l0, float l1, thread float& l2) { { Name: "For3", Program: shaderir.Program{ + Unit: shaderir.Pixel, Funcs: []shaderir.Func{ { Index: 0, @@ -879,7 +898,7 @@ void F0(in float l0, in float l1, out float l2) { l2 = l5; } }`, - Metal: msl.Prelude + ` + Metal: msl.Prelude(shaderir.Pixel) + ` void F0(float l0, float l1, thread float& l2); @@ -897,6 +916,7 @@ void F0(float l0, float l1, thread float& l2) { { Name: "VertexFunc", Program: shaderir.Program{ + Unit: shaderir.Pixel, Uniforms: []shaderir.Type{ {Main: shaderir.Float}, }, @@ -949,6 +969,7 @@ in vec2 V1;`, { Name: "FragmentFunc", Program: shaderir.Program{ + Unit: shaderir.Pixel, Uniforms: []shaderir.Type{ {Main: shaderir.Float}, }, diff --git a/internal/shaderir/msl/msl.go b/internal/shaderir/msl/msl.go index 0b7395829..c0dd14049 100644 --- a/internal/shaderir/msl/msl.go +++ b/internal/shaderir/msl/msl.go @@ -47,16 +47,22 @@ func (c *compileContext) structName(p *shaderir.Program, t *shaderir.Type) strin return n } -const Prelude = `#include +func Prelude(unit shaderir.Unit) string { + str := `#include using namespace metal; -constexpr sampler texture_sampler{filter::nearest}; - template T mod(T x, U y) { return x - y * floor(x/y); }` + if unit == shaderir.Texel { + str += ` + +constexpr sampler texture_sampler{filter::nearest};` + } + return str +} func Compile(p *shaderir.Program, vertex, fragment string) (shader string) { c := &compileContext{ @@ -64,7 +70,7 @@ func Compile(p *shaderir.Program, vertex, fragment string) (shader string) { } var lines []string - lines = append(lines, strings.Split(Prelude, "\n")...) + lines = append(lines, strings.Split(Prelude(p.Unit), "\n")...) lines = append(lines, "", "{{.Structs}}") if len(p.Attributes) > 0 { @@ -396,7 +402,14 @@ func (c *compileContext) block(p *shaderir.Program, topBlock, block *shaderir.Bl args = append(args, expr(&exp)) } if callee.Type == shaderir.BuiltinFuncExpr && callee.BuiltinFunc == shaderir.Texture2DF { - return fmt.Sprintf("%s.sample(texture_sampler, %s)", args[0], strings.Join(args[1:], ", ")) + switch p.Unit { + case shaderir.Texel: + return fmt.Sprintf("%s.sample(texture_sampler, %s)", args[0], strings.Join(args[1:], ", ")) + case shaderir.Pixel: + return fmt.Sprintf("%s.read(static_cast(%s))", args[0], strings.Join(args[1:], ", ")) + default: + panic(fmt.Sprintf("msl: unexpected unit: %d", p.Unit)) + } } return fmt.Sprintf("%s(%s)", expr(&callee), strings.Join(args, ", ")) case shaderir.FieldSelector: diff --git a/internal/shaderir/program.go b/internal/shaderir/program.go index bad3b7a66..efde255dd 100644 --- a/internal/shaderir/program.go +++ b/internal/shaderir/program.go @@ -22,6 +22,13 @@ import ( "strings" ) +type Unit int + +const ( + Texel Unit = iota + Pixel +) + type Program struct { UniformNames []string Uniforms []Type @@ -31,6 +38,7 @@ type Program struct { Funcs []Func VertexFunc VertexFunc FragmentFunc FragmentFunc + Unit Unit reachableUniforms []bool uniformUint32Counts []int diff --git a/internal/testing/shader.go b/internal/testing/shader.go index b4e2c09a8..479b20afa 100644 --- a/internal/testing/shader.go +++ b/internal/testing/shader.go @@ -24,7 +24,9 @@ import ( // ShaderProgramFill returns a shader source to fill the frambuffer. func ShaderProgramFill(r, g, b, a byte) *shaderir.Program { - ir, err := graphics.CompileShader([]byte(fmt.Sprintf(`package main + ir, err := graphics.CompileShader([]byte(fmt.Sprintf(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { return vec4(%0.9f, %0.9f, %0.9f, %0.9f) @@ -47,7 +49,9 @@ func ShaderProgramImages(numImages int) *shaderir.Program { exprs = append(exprs, fmt.Sprintf("imageSrc%dUnsafeAt(texCoord)", i)) } - ir, err := graphics.CompileShader([]byte(fmt.Sprintf(`package main + ir, err := graphics.CompileShader([]byte(fmt.Sprintf(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { return %s diff --git a/shader_test.go b/shader_test.go index 9f4cd74b5..d88d5d913 100644 --- a/shader_test.go +++ b/shader_test.go @@ -28,7 +28,9 @@ func TestShaderFill(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { return vec4(1, 0, 0, 1) @@ -58,7 +60,9 @@ func TestShaderFillWithDrawImage(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { return vec4(1, 0, 0, 1) @@ -93,7 +97,9 @@ func TestShaderWithDrawImageDoesNotWreckTextureUnits(t *testing.T) { rect := image.Rectangle{Max: image.Point{X: w, Y: h}} dst := ebiten.NewImageWithOptions(rect, &ebiten.NewImageOptions{Unmanaged: true}) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { return imageSrc0At(texCoord) @@ -161,7 +167,9 @@ func TestShaderFillWithDrawTriangles(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { return vec4(1, 0, 0, 1) @@ -236,7 +244,9 @@ func TestShaderFunction(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func clr(red float) (float, float, float, float) { return red, 0, 0, 1 @@ -267,7 +277,9 @@ func TestShaderUninitializedUniformVariables(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main var U vec4 @@ -296,7 +308,9 @@ func TestShaderMatrix(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { var a, b mat4 @@ -334,7 +348,9 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { func TestShaderSubImage(t *testing.T) { const w, h = 16, 16 - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { r := imageSrc0At(texCoord).r @@ -460,7 +476,9 @@ func TestShaderDerivatives(t *testing.T) { const w, h = 16, 16 - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { p := imageSrc0At(texCoord) @@ -515,7 +533,9 @@ func TestShaderDerivatives2(t *testing.T) { const w, h = 16, 16 - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main // This function uses dfdx and then should not be in GLSL's vertex shader (#1701). func Foo(p vec4) vec4 { @@ -578,7 +598,9 @@ func TestShaderUniformFirstElement(t *testing.T) { }{ { Name: "float array", - Shader: `package main + Shader: `//kage:unit pixel + +package main var C [2]float @@ -591,7 +613,9 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { }, { Name: "float one-element array", - Shader: `package main + Shader: `//kage:unit pixel + +package main var C [1]float @@ -604,7 +628,9 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { }, { Name: "matrix array", - Shader: `package main + Shader: `//kage:unit pixel + +package main var C [2]mat2 @@ -646,7 +672,9 @@ func TestShaderFuncMod(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { r := mod(-0.25, 1.0) @@ -680,7 +708,9 @@ func TestShaderMatrixInitialize(t *testing.T) { src.Fill(color.RGBA{R: 0x10, G: 0x20, B: 0x30, A: 0xff}) dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { return mat4(2) * imageSrc0At(texCoord); @@ -710,7 +740,9 @@ func TestShaderModVectorAndFloat(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { r := mod(vec3(0.25, 0.5, 0.75), 0.5) @@ -741,7 +773,9 @@ func TestShaderTextureAt(t *testing.T) { src.Fill(color.RGBA{R: 0x10, G: 0x20, B: 0x30, A: 0xff}) dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func textureAt(uv vec2) vec4 { return imageSrc0UnsafeAt(uv) @@ -777,7 +811,9 @@ func TestShaderAtan2(t *testing.T) { src.Fill(color.RGBA{R: 0x10, G: 0x20, B: 0x30, A: 0xff}) dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { y := vec4(1, 1, 1, 1) @@ -809,7 +845,9 @@ func TestShaderUniformMatrix2(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main var Mat2 mat2 var F float @@ -847,7 +885,9 @@ func TestShaderUniformMatrix2Array(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main var Mat2 [2]mat2 var F float @@ -887,7 +927,9 @@ func TestShaderUniformMatrix3(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main var Mat3 mat3 var F float @@ -926,7 +968,9 @@ func TestShaderUniformMatrix3Array(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main var Mat3 [2]mat3 var F float @@ -968,7 +1012,9 @@ func TestShaderUniformMatrix4(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main var Mat4 mat4 var F float @@ -1008,7 +1054,9 @@ func TestShaderUniformMatrix4Array(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main var Mat4 [2]mat4 var F float @@ -1051,7 +1099,9 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { func TestShaderOptionsNegativeBounds(t *testing.T) { const w, h = 16, 16 - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { r := imageSrc0At(texCoord).r @@ -1179,7 +1229,9 @@ func TestShaderVectorEqual(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { a := vec3(1) @@ -1227,7 +1279,9 @@ func TestShaderDiscard(t *testing.T) { } src.WritePixels(pix) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { p := imageSrc0At(texCoord) @@ -1272,7 +1326,9 @@ func TestShaderDrawRect(t *testing.T) { dst := ebiten.NewImage(dstW, dstH) src := ebiten.NewImage(srcW, srcH) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { // Adjust texCoord into [0, 1]. @@ -1319,7 +1375,9 @@ func TestShaderDrawRectColorScale(t *testing.T) { const w, h = 16, 16 dst := ebiten.NewImage(w, h) - s, err := ebiten.NewShader([]byte(`package main + s, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { return color @@ -1349,7 +1407,9 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { } func TestShaderUniformInt(t *testing.T) { - const ints = `package main + const ints = `//kage:unit pixel + +package main var U0 int var U1 int @@ -1361,7 +1421,9 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { } ` - const intArray = `package main + const intArray = `//kage:unit pixel + +package main var U [4]int @@ -1370,7 +1432,9 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { } ` - const intVec = `package main + const intVec = `//kage:unit pixel + +package main var U0 ivec4 var U1 [2]ivec3 @@ -1545,7 +1609,9 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { // Issue #2463 func TestShaderUniformVec3Array(t *testing.T) { - const shader = `package main + const shader = `//kage:unit pixel + +package main var U [4]vec3 @@ -1616,7 +1682,9 @@ return vec4(b)/255`, } for _, tc := range cases { - shader := fmt.Sprintf(`package main + shader := fmt.Sprintf(`//kage:unit pixel + +package main func Fragment(position vec4, texCoord vec2, color vec4) vec4 { %s @@ -1640,3 +1708,56 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { } } } + +func TestShaderTexelAndPixel(t *testing.T) { + const dstW, dstH = 13, 17 + const srcW, srcH = 19, 23 + dstTexel := ebiten.NewImage(dstW, dstH) + dstPixel := ebiten.NewImage(dstW, dstH) + src := ebiten.NewImage(srcW, srcH) + + shaderTexel, err := ebiten.NewShader([]byte(fmt.Sprintf(`//kage:unit texel + +package main + +func Fragment(position vec4, texCoord vec2, color vec4) vec4 { + orig, size := imageSrcRegionOnTexture() + pos := (texCoord - orig) / size + pos *= vec2(%d, %d) + pos /= 255 + return vec4(pos.x, pos.y, 0, 1) +} +`, srcW, srcH))) + if err != nil { + t.Fatal(err) + } + shaderPixel, err := ebiten.NewShader([]byte(`//kage:unit pixel + +package main + +func Fragment(position vec4, texCoord vec2, color vec4) vec4 { + orig, _ := imageSrcRegionOnTexture() + pos := texCoord - orig + pos /= 255 + return vec4(pos.x, pos.y, 0, 1) +} +`)) + if err != nil { + t.Fatal(err) + } + + op := &ebiten.DrawRectShaderOptions{} + op.Images[0] = src + dstTexel.DrawRectShader(src.Bounds().Dx(), src.Bounds().Dy(), shaderTexel, op) + dstPixel.DrawRectShader(src.Bounds().Dx(), src.Bounds().Dy(), shaderPixel, op) + + for j := 0; j < dstH; j++ { + for i := 0; i < dstW; i++ { + c0 := dstTexel.At(i, j).(color.RGBA) + c1 := dstPixel.At(i, j).(color.RGBA) + if !sameColors(c0, c1, 1) { + t.Errorf("dstTexel.At(%d, %d) %v != dstPixel.At(%d, %d) %v", i, j, c0, i, j, c1) + } + } + } +}