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
This commit is contained in:
Hajime Hoshi 2023-04-16 18:56:14 +09:00
parent b5ca404c42
commit 49582519c1
31 changed files with 544 additions and 206 deletions

View File

@ -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

View File

@ -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)

View File

@ -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...

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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].
//

View File

@ -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).

View File

@ -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.
//

View File

@ -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}}

View File

@ -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"

View File

@ -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)

View File

@ -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,

View File

@ -42,3 +42,7 @@ func (s *Shader) Dispose() {
}
theCommandQueue.Enqueue(c)
}
func (s *Shader) unit() shaderir.Unit {
return s.ir.Unit
}

View File

@ -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{

View File

@ -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,

View File

@ -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)

View File

@ -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.

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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:

View File

@ -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)

View File

@ -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, ", "))

View File

@ -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},
},

View File

@ -47,16 +47,22 @@ func (c *compileContext) structName(p *shaderir.Program, t *shaderir.Type) strin
return n
}
const Prelude = `#include <metal_stdlib>
func Prelude(unit shaderir.Unit) string {
str := `#include <metal_stdlib>
using namespace metal;
constexpr sampler texture_sampler{filter::nearest};
template<typename T, typename U>
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<uint2>(%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:

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}
}