From bd43b42ee5a8354d36be969a6f56ab72aa6d034c Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Tue, 6 Sep 2022 18:21:33 +0900 Subject: [PATCH] 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 --- .../directx/graphics_windows.go | 12 ++-- .../graphicsdriver/metal/graphics_darwin.go | 2 +- .../graphicsdriver/metal/shader_darwin.go | 22 ++++--- internal/ui/context.go | 66 ++++++++++++++++++- 4 files changed, 84 insertions(+), 18 deletions(-) diff --git a/internal/graphicsdriver/directx/graphics_windows.go b/internal/graphicsdriver/directx/graphics_windows.go index c8010eab2..6f0fe8044 100644 --- a/internal/graphicsdriver/directx/graphics_windows.go +++ b/internal/graphicsdriver/directx/graphics_windows.go @@ -1407,7 +1407,7 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcs [graphics.Sh } else { if evenOdd { - s, err := shader.pipelineState(mode, prepareStencil) + s, err := shader.pipelineState(mode, prepareStencil, dst.screen) if err != nil { return err } @@ -1415,7 +1415,7 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcs [graphics.Sh return err } - s, err = shader.pipelineState(mode, drawWithStencil) + s, err = shader.pipelineState(mode, drawWithStencil, dst.screen) if err != nil { return err } @@ -1423,7 +1423,7 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcs [graphics.Sh return err } } else { - s, err := shader.pipelineState(mode, noStencil) + s, err := shader.pipelineState(mode, noStencil, dst.screen) if err != nil { return err } @@ -1866,6 +1866,7 @@ const ( type pipelineStateKey struct { compositeMode graphicsdriver.CompositeMode stencilMode stencilMode + screen bool } 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{ compositeMode: compositeMode, stencilMode: stencilMode, + screen: screen, } if state, ok := s.pipelineStates[key]; ok { 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 { return nil, err } diff --git a/internal/graphicsdriver/metal/graphics_darwin.go b/internal/graphicsdriver/metal/graphics_darwin.go index 2a2944ffc..b13948a78 100644 --- a/internal/graphicsdriver/metal/graphics_darwin.go +++ b/internal/graphicsdriver/metal/graphics_darwin.go @@ -924,7 +924,7 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcIDs [graphics. noStencil, } { 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 { return err } diff --git a/internal/graphicsdriver/metal/shader_darwin.go b/internal/graphicsdriver/metal/shader_darwin.go index 9a22349a0..b457d8214 100644 --- a/internal/graphicsdriver/metal/shader_darwin.go +++ b/internal/graphicsdriver/metal/shader_darwin.go @@ -26,6 +26,7 @@ import ( type shaderRpsKey struct { compositeMode graphicsdriver.CompositeMode stencilMode stencilMode + screen bool } type Shader struct { @@ -85,11 +86,13 @@ func (s *Shader) init(device mtl.Device) error { return nil } -func (s *Shader) RenderPipelineState(device mtl.Device, compositeMode graphicsdriver.CompositeMode, stencilMode stencilMode) (mtl.RenderPipelineState, error) { - if rps, ok := s.rpss[shaderRpsKey{ +func (s *Shader) RenderPipelineState(view view, compositeMode graphicsdriver.CompositeMode, stencilMode stencilMode, screen bool) (mtl.RenderPipelineState, error) { + key := shaderRpsKey{ compositeMode: compositeMode, stencilMode: stencilMode, - }]; ok { + screen: screen, + } + if rps, ok := s.rpss[key]; ok { 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. - rpld.ColorAttachments[0].PixelFormat = mtl.PixelFormatRGBA8UNorm + pix := mtl.PixelFormatRGBA8UNorm + if screen { + pix = view.colorPixelFormat() + } + rpld.ColorAttachments[0].PixelFormat = pix rpld.ColorAttachments[0].BlendingEnabled = true src, dst := compositeMode.Operations() @@ -116,14 +123,11 @@ func (s *Shader) RenderPipelineState(device mtl.Device, compositeMode graphicsdr rpld.ColorAttachments[0].WriteMask = mtl.ColorWriteMaskAll } - rps, err := device.MakeRenderPipelineState(rpld) + rps, err := view.getMTLDevice().MakeRenderPipelineState(rpld) if err != nil { return mtl.RenderPipelineState{}, err } - s.rpss[shaderRpsKey{ - compositeMode: compositeMode, - stencilMode: stencilMode, - }] = rps + s.rpss[key] = rps return rps, nil } diff --git a/internal/ui/context.go b/internal/ui/context.go index b97694f18..9f1ed2439 100644 --- a/internal/ui/context.go +++ b/internal/ui/context.go @@ -30,6 +30,40 @@ import ( "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 { NewOffscreenImage(width, height int) *Image Layout(outsideWidth, outsideHeight int) (int, int) @@ -49,6 +83,8 @@ type context struct { outsideWidth float64 outsideHeight float64 + screenShader *Shader + 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). if w, h := c.layoutGame(outsideWidth, outsideHeight, deviceScaleFactor); w == 0 || h == 0 { return nil @@ -186,15 +231,16 @@ func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics) { gty += offsetY var filter graphicsdriver.Filter + var screenFilter bool switch { case !theGlobalState.isScreenFilterEnabled(): filter = graphicsdriver.FilterNearest case math.Floor(s) == s: filter = graphicsdriver.FilterNearest case s > 1: - filter = graphicsdriver.FilterScreen + screenFilter = true 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). filter = graphicsdriver.FilterLinear } @@ -213,7 +259,21 @@ func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics) { is := graphics.QuadIndices() 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) {