internal/ui: reland the screen shader in Kage

This change relads a part of the change to use the screen shader
instead of FilterScreen, but with the issue on iOS fixed.

Let's remove FilterScreen later.

Updates #2282
This commit is contained in:
Hajime Hoshi 2022-09-06 18:21:33 +09:00
parent 62127e432e
commit bd43b42ee5
4 changed files with 84 additions and 18 deletions

View File

@ -1407,7 +1407,7 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcs [graphics.Sh
} else { } else {
if evenOdd { if evenOdd {
s, err := shader.pipelineState(mode, prepareStencil) s, err := shader.pipelineState(mode, prepareStencil, dst.screen)
if err != nil { if err != nil {
return err return err
} }
@ -1415,7 +1415,7 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcs [graphics.Sh
return err return err
} }
s, err = shader.pipelineState(mode, drawWithStencil) s, err = shader.pipelineState(mode, drawWithStencil, dst.screen)
if err != nil { if err != nil {
return err return err
} }
@ -1423,7 +1423,7 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcs [graphics.Sh
return err return err
} }
} else { } else {
s, err := shader.pipelineState(mode, noStencil) s, err := shader.pipelineState(mode, noStencil, dst.screen)
if err != nil { if err != nil {
return err return err
} }
@ -1866,6 +1866,7 @@ const (
type pipelineStateKey struct { type pipelineStateKey struct {
compositeMode graphicsdriver.CompositeMode compositeMode graphicsdriver.CompositeMode
stencilMode stencilMode stencilMode stencilMode
screen bool
} }
type Shader struct { type Shader struct {
@ -1902,16 +1903,17 @@ func (s *Shader) disposeImpl() {
} }
} }
func (s *Shader) pipelineState(compositeMode graphicsdriver.CompositeMode, stencilMode stencilMode) (*_ID3D12PipelineState, error) { func (s *Shader) pipelineState(compositeMode graphicsdriver.CompositeMode, stencilMode stencilMode, screen bool) (*_ID3D12PipelineState, error) {
key := pipelineStateKey{ key := pipelineStateKey{
compositeMode: compositeMode, compositeMode: compositeMode,
stencilMode: stencilMode, stencilMode: stencilMode,
screen: screen,
} }
if state, ok := s.pipelineStates[key]; ok { if state, ok := s.pipelineStates[key]; ok {
return state, nil return state, nil
} }
state, err := s.graphics.pipelineStates.newPipelineState(s.graphics.device, s.vertexShader, s.pixelShader, compositeMode, stencilMode, false) state, err := s.graphics.pipelineStates.newPipelineState(s.graphics.device, s.vertexShader, s.pixelShader, compositeMode, stencilMode, screen)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -924,7 +924,7 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcIDs [graphics.
noStencil, noStencil,
} { } {
var err error var err error
rpss[stencil], err = g.shaders[shaderID].RenderPipelineState(g.view.getMTLDevice(), mode, stencil) rpss[stencil], err = g.shaders[shaderID].RenderPipelineState(g.view, mode, stencil, dst.screen)
if err != nil { if err != nil {
return err return err
} }

View File

@ -26,6 +26,7 @@ import (
type shaderRpsKey struct { type shaderRpsKey struct {
compositeMode graphicsdriver.CompositeMode compositeMode graphicsdriver.CompositeMode
stencilMode stencilMode stencilMode stencilMode
screen bool
} }
type Shader struct { type Shader struct {
@ -85,11 +86,13 @@ func (s *Shader) init(device mtl.Device) error {
return nil return nil
} }
func (s *Shader) RenderPipelineState(device mtl.Device, compositeMode graphicsdriver.CompositeMode, stencilMode stencilMode) (mtl.RenderPipelineState, error) { func (s *Shader) RenderPipelineState(view view, compositeMode graphicsdriver.CompositeMode, stencilMode stencilMode, screen bool) (mtl.RenderPipelineState, error) {
if rps, ok := s.rpss[shaderRpsKey{ key := shaderRpsKey{
compositeMode: compositeMode, compositeMode: compositeMode,
stencilMode: stencilMode, stencilMode: stencilMode,
}]; ok { screen: screen,
}
if rps, ok := s.rpss[key]; ok {
return rps, nil return rps, nil
} }
@ -102,7 +105,11 @@ func (s *Shader) RenderPipelineState(device mtl.Device, compositeMode graphicsdr
} }
// TODO: For the precise pixel format, whether the render target is the screen or not must be considered. // TODO: For the precise pixel format, whether the render target is the screen or not must be considered.
rpld.ColorAttachments[0].PixelFormat = mtl.PixelFormatRGBA8UNorm pix := mtl.PixelFormatRGBA8UNorm
if screen {
pix = view.colorPixelFormat()
}
rpld.ColorAttachments[0].PixelFormat = pix
rpld.ColorAttachments[0].BlendingEnabled = true rpld.ColorAttachments[0].BlendingEnabled = true
src, dst := compositeMode.Operations() src, dst := compositeMode.Operations()
@ -116,14 +123,11 @@ func (s *Shader) RenderPipelineState(device mtl.Device, compositeMode graphicsdr
rpld.ColorAttachments[0].WriteMask = mtl.ColorWriteMaskAll rpld.ColorAttachments[0].WriteMask = mtl.ColorWriteMaskAll
} }
rps, err := device.MakeRenderPipelineState(rpld) rps, err := view.getMTLDevice().MakeRenderPipelineState(rpld)
if err != nil { if err != nil {
return mtl.RenderPipelineState{}, err return mtl.RenderPipelineState{}, err
} }
s.rpss[shaderRpsKey{ s.rpss[key] = rps
compositeMode: compositeMode,
stencilMode: stencilMode,
}] = rps
return rps, nil return rps, nil
} }

View File

@ -30,6 +30,40 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/hooks" "github.com/hajimehoshi/ebiten/v2/internal/hooks"
) )
const screenShader = `package main
var Scale vec2
func Fragment(position vec4, texCoord vec2, color vec4) vec4 {
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.
// As all the vertex positions are aligned to 1/16 [pixel], this shiting should work in most cases.
pos := texCoord
p0 := pos - halfScaledTexelSize + (texelSize / 512)
p1 := pos + halfScaledTexelSize + (texelSize / 512)
// Texels must be in the source rect, so it is not necessary to check.
c0 := imageSrc0UnsafeAt(p0)
c1 := imageSrc0UnsafeAt(vec2(p1.x, p0.y))
c2 := imageSrc0UnsafeAt(vec2(p0.x, p1.y))
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)
// rate indicates how much the 4 colors are mixed. rate is in between [0, 1].
//
// 0 <= p <= 1/Scale: The rate is in between [0, 1]
// 1/Scale < p: Don't care. Adjacent colors (e.g. c0 vs c1 in an X direction) should be the same.
rate := clamp(p*Scale, 0, 1)
return mix(mix(c0, c1, rate.x), mix(c2, c3, rate.x), rate.y)
}
`
type Game interface { type Game interface {
NewOffscreenImage(width, height int) *Image NewOffscreenImage(width, height int) *Image
Layout(outsideWidth, outsideHeight int) (int, int) Layout(outsideWidth, outsideHeight int) (int, int)
@ -49,6 +83,8 @@ type context struct {
outsideWidth float64 outsideWidth float64
outsideHeight float64 outsideHeight float64
screenShader *Shader
m sync.Mutex m sync.Mutex
} }
@ -108,6 +144,15 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update
} }
}() }()
// Create a shader for the screen if necessary.
if c.screenShader == nil {
ir, err := graphics.CompileShader([]byte(screenShader))
if err != nil {
return err
}
c.screenShader = NewShader(ir)
}
// ForceUpdate can be invoked even if the context is not initialized yet (#1591). // ForceUpdate can be invoked even if the context is not initialized yet (#1591).
if w, h := c.layoutGame(outsideWidth, outsideHeight, deviceScaleFactor); w == 0 || h == 0 { if w, h := c.layoutGame(outsideWidth, outsideHeight, deviceScaleFactor); w == 0 || h == 0 {
return nil return nil
@ -186,15 +231,16 @@ func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics) {
gty += offsetY gty += offsetY
var filter graphicsdriver.Filter var filter graphicsdriver.Filter
var screenFilter bool
switch { switch {
case !theGlobalState.isScreenFilterEnabled(): case !theGlobalState.isScreenFilterEnabled():
filter = graphicsdriver.FilterNearest filter = graphicsdriver.FilterNearest
case math.Floor(s) == s: case math.Floor(s) == s:
filter = graphicsdriver.FilterNearest filter = graphicsdriver.FilterNearest
case s > 1: case s > 1:
filter = graphicsdriver.FilterScreen screenFilter = true
default: default:
// FilterScreen works with >=1 scale, but does not well with <1 scale. // screenShader works with >=1 scale, but does not well with <1 scale.
// Use regular FilterLinear instead so far (#669). // Use regular FilterLinear instead so far (#669).
filter = graphicsdriver.FilterLinear filter = graphicsdriver.FilterLinear
} }
@ -213,7 +259,21 @@ func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics) {
is := graphics.QuadIndices() is := graphics.QuadIndices()
srcs := [graphics.ShaderImageCount]*Image{c.offscreen} srcs := [graphics.ShaderImageCount]*Image{c.offscreen}
c.screen.DrawTriangles(srcs, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, filter, graphicsdriver.AddressUnsafe, dstRegion, graphicsdriver.Region{}, [graphics.ShaderImageCount - 1][2]float32{}, nil, nil, false, true)
var shader *Shader
var uniforms [][]float32
if screenFilter {
shader = c.screenShader
dstWidth, dstHeight := c.screen.width, c.screen.height
srcWidth, srcHeight := c.offscreen.width, c.offscreen.height
uniforms = shader.ConvertUniforms(map[string]interface{}{
"Scale": []float32{
float32(dstWidth) / float32(srcWidth),
float32(dstHeight) / float32(srcHeight),
},
})
}
c.screen.DrawTriangles(srcs, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, filter, graphicsdriver.AddressUnsafe, dstRegion, graphicsdriver.Region{}, [graphics.ShaderImageCount - 1][2]float32{}, shader, uniforms, false, true)
} }
func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFactor float64) (int, int) { func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFactor float64) (int, int) {