ebiten: guarantee invalid color values are not clamped

Closes #2798
This commit is contained in:
Hajime Hoshi 2024-03-13 11:52:48 +09:00
parent bb6430d3ba
commit 6cdabf09d1
3 changed files with 111 additions and 0 deletions

View File

@ -587,6 +587,9 @@ var _ [len(DrawTrianglesShaderOptions{}.Images) - graphics.ShaderImageCount]stru
//
// If a specified uniform variable's length or type doesn't match with an expected one, DrawTrianglesShader panics.
//
// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values,
// the value is kept and is not clamped.
//
// When the image i is disposed, DrawTrianglesShader does nothing.
func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader *Shader, options *DrawTrianglesShaderOptions) {
i.copyCheck()
@ -742,6 +745,9 @@ var _ [len(DrawRectShaderOptions{}.Images)]struct{} = [graphics.ShaderImageCount
// If no source images are specified, imageSrc0Size returns a valid size only when the unit is pixels,
// but always returns 0 when the unit is texels (default).
//
// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values,
// the value is kept and is not clamped.
//
// When the image i is disposed, DrawRectShader does nothing.
func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawRectShaderOptions) {
i.copyCheck()
@ -954,6 +960,9 @@ func (i *Image) at(x, y int) (r, g, b, a byte) {
//
// Set implements the standard draw.Image's Set.
//
// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values,
// the value is kept and is not clamped.
//
// If the image is disposed, Set does nothing.
func (i *Image) Set(x, y int, clr color.Color) {
i.copyCheck()
@ -1028,6 +1037,9 @@ func (i *Image) Deallocate() {
//
// WritePixels also works on a sub-image.
//
// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values,
// the value is kept and is not clamped.
//
// When the image is disposed, WritePixels does nothing.
func (i *Image) WritePixels(pixels []byte) {
i.copyCheck()

View File

@ -4582,3 +4582,44 @@ func TestImageDrawImageAfterDeallocation(t *testing.T) {
}
}
}
// Issue #2798
func TestImageInvalidPremultipliedAlphaColor(t *testing.T) {
// This test checks the rendering result when Set and WritePixels use an invalid premultiplied alpha color.
// The result values are kept and not clamped.
const (
w = 16
h = 16
)
dst := ebiten.NewImage(w, h)
dst.Set(0, 0, color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x40})
dst.Set(0, 1, color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x00})
if got, want := dst.At(0, 0).(color.RGBA), (color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x40}); got != want {
t.Errorf("got: %v, want: %v", got, want)
}
if got, want := dst.At(0, 1).(color.RGBA), (color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x00}); got != want {
t.Errorf("got: %v, want: %v", got, want)
}
pix := make([]byte, 4*w*h)
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
pix[4*(j*16+i)] = byte(i)
pix[4*(j*16+i)+1] = byte(j)
pix[4*(j*16+i)+2] = 0x80
pix[4*(j*16+i)+3] = byte(i - j)
}
}
dst.WritePixels(pix)
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
got := dst.At(i, j)
want := color.RGBA{R: byte(i), G: byte(j), B: 0x80, A: byte(i - j)}
if got != want {
t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
}

View File

@ -2429,3 +2429,61 @@ func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
}
}
}
// Issue #2798
func TestShaderInvalidPremultipliedAlphaColor(t *testing.T) {
// This test checks the rendering result when the shader returns an invalid premultiplied alpha color.
// The result values are kept and not clamped.
const w, h = 16, 16
dst := ebiten.NewImage(w, h)
s, err := ebiten.NewShader([]byte(`//kage:unit pixels
package main
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
return vec4(1, 0.75, 0.5, 0.25)
}
`))
if err != nil {
t.Fatal(err)
}
dst.DrawRectShader(w, h, s, nil)
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
got := dst.At(i, j).(color.RGBA)
want := color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x40}
if !sameColors(got, want, 2) {
t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
dst.Clear()
s, err = ebiten.NewShader([]byte(`//kage:unit pixels
package main
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
return vec4(1, 0.75, 0.5, 0)
}
`))
if err != nil {
t.Fatal(err)
}
dst.DrawRectShader(w, h, s, nil)
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
got := dst.At(i, j).(color.RGBA)
want := color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0}
if !sameColors(got, want, 2) {
t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
}