From a96153805e960ae475392305c98bc839653fb7d3 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 22 Jun 2025 23:12:10 +0900 Subject: [PATCH] internal/shader: add a builtin function frontfacing Closes #3264 --- .../graphicsdriver/directx/shader_windows.go | 2 +- internal/shader/expr.go | 11 +- internal/shader/shader_test.go | 4 +- .../shader/testdata/array2.expected.metal | 4 +- internal/shader/testdata/atan.expected.metal | 4 +- internal/shader/testdata/out.expected.metal | 4 +- .../testdata/vertex_fragment.expected.metal | 3 +- internal/shaderir/glsl/glsl.go | 3 + internal/shaderir/hlsl/hlsl.go | 112 ++++++++++-------- internal/shaderir/ir_test.go | 8 +- internal/shaderir/msl/msl.go | 7 ++ internal/shaderir/program.go | 4 +- internal/shaderlister/main.go | 2 +- shader_test.go | 53 +++++++++ 14 files changed, 155 insertions(+), 66 deletions(-) diff --git a/internal/graphicsdriver/directx/shader_windows.go b/internal/graphicsdriver/directx/shader_windows.go index c2d8c0bdb..2a02c6ed0 100644 --- a/internal/graphicsdriver/directx/shader_windows.go +++ b/internal/graphicsdriver/directx/shader_windows.go @@ -102,7 +102,7 @@ func compileShader(program *shaderir.Program) (vsh, psh *_ID3DBlob, ferr error) return vsh, psh, nil } - vs, ps, _ := hlsl.Compile(program) + vs, ps, _, _ := hlsl.Compile(program) var flag uint32 = uint32(_D3DCOMPILE_OPTIMIZATION_LEVEL3) var wg errgroup.Group diff --git a/internal/shader/expr.go b/internal/shader/expr.go index 075f160f8..14a5a1d18 100644 --- a/internal/shader/expr.go +++ b/internal/shader/expr.go @@ -421,6 +421,16 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar return nil, nil, nil, false } finalType = shaderir.Type{Main: shaderir.Vec4} + case shaderir.FrontFacing: + if len(args) != 0 { + cs.addError(e.Pos(), fmt.Sprintf("number of %s's arguments must be 0 but %d", callee.BuiltinFunc, len(args))) + return nil, nil, nil, false + } + if fname != cs.fragmentEntry { + cs.addError(e.Pos(), fmt.Sprintf("frontfacing is available only in %s", cs.fragmentEntry)) + return nil, nil, nil, false + } + finalType = shaderir.Type{Main: shaderir.Bool} case shaderir.DiscardF: if len(args) != 0 { cs.addError(e.Pos(), fmt.Sprintf("number of %s's arguments must be 0 but %d", callee.BuiltinFunc, len(args))) @@ -434,7 +444,6 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar Type: shaderir.Discard, }) return nil, nil, stmts, true - case shaderir.Clamp, shaderir.Mix, shaderir.Smoothstep, shaderir.Faceforward, shaderir.Refract: // 3 arguments if len(args) != 3 { diff --git a/internal/shader/shader_test.go b/internal/shader/shader_test.go index 968fe637d..124ef9aa9 100644 --- a/internal/shader/shader_test.go +++ b/internal/shader/shader_test.go @@ -180,8 +180,8 @@ func TestCompile(t *testing.T) { } if tc.HLSL != nil { - vs, _, prelude := hlsl.Compile(s) - if got, want := hlslNormalize(vs, prelude), hlslNormalize(string(tc.HLSL), prelude); got != want { + vs, _, vertexPrelude, _ := hlsl.Compile(s) + if got, want := hlslNormalize(vs, vertexPrelude), hlslNormalize(string(tc.HLSL), vertexPrelude); got != want { compare(t, "HLSL", got, want) } } diff --git a/internal/shader/testdata/array2.expected.metal b/internal/shader/testdata/array2.expected.metal index 1ef317379..59fcd706c 100644 --- a/internal/shader/testdata/array2.expected.metal +++ b/internal/shader/testdata/array2.expected.metal @@ -1,6 +1,6 @@ -void F0(thread array& l0); +void F0(bool front_facing, thread array& l0); -void F0(thread array& l0) { +void F0(bool front_facing, thread array& l0) { array l1 = {}; array l2 = {}; { diff --git a/internal/shader/testdata/atan.expected.metal b/internal/shader/testdata/atan.expected.metal index 3735dd154..a801175f9 100644 --- a/internal/shader/testdata/atan.expected.metal +++ b/internal/shader/testdata/atan.expected.metal @@ -1,6 +1,6 @@ -bool F0(float l0, float l1); +bool F0(bool front_facing, float l0, float l1); -bool F0(float l0, float l1) { +bool F0(bool front_facing, float l0, float l1) { float l2 = float(0); float l3 = float(0); l2 = atan((l1) / (l0)); diff --git a/internal/shader/testdata/out.expected.metal b/internal/shader/testdata/out.expected.metal index 70c3652ff..4c42e0704 100644 --- a/internal/shader/testdata/out.expected.metal +++ b/internal/shader/testdata/out.expected.metal @@ -1,6 +1,6 @@ -void F0(thread float& l0, thread array& l1, thread float4& l2); +void F0(bool front_facing, thread float& l0, thread array& l1, thread float4& l2); -void F0(thread float& l0, thread array& l1, thread float4& l2) { +void F0(bool front_facing, thread float& l0, thread array& l1, thread float4& l2) { l0 = float(0); l1 = {}; l2 = float4(0); diff --git a/internal/shader/testdata/vertex_fragment.expected.metal b/internal/shader/testdata/vertex_fragment.expected.metal index 6aedec0dc..ecdc0a3fa 100644 --- a/internal/shader/testdata/vertex_fragment.expected.metal +++ b/internal/shader/testdata/vertex_fragment.expected.metal @@ -29,6 +29,7 @@ vertex Varyings Vertex( fragment float4 Fragment( Varyings varyings [[stage_in]], - constant Uniforms& uniforms [[buffer(0)]]) { + constant Uniforms& uniforms [[buffer(0)]], + bool front_facing [[front_facing]]) { return float4((varyings.Position).x, (varyings.M0).y, (varyings.M1).z, 1.0); } diff --git a/internal/shaderir/glsl/glsl.go b/internal/shaderir/glsl/glsl.go index 86a7d0b86..d5239dc73 100644 --- a/internal/shaderir/glsl/glsl.go +++ b/internal/shaderir/glsl/glsl.go @@ -520,6 +520,9 @@ func (c *compileContext) block(p *shaderir.Program, topBlock, block *shaderir.Bl } return result } + if callee.BuiltinFunc == shaderir.FrontFacing { + return "gl_FrontFacing" + } } // Using parentheses at the callee is illegal. return fmt.Sprintf("%s(%s)", expr(&callee), strings.Join(args, ", ")) diff --git a/internal/shaderir/hlsl/hlsl.go b/internal/shaderir/hlsl/hlsl.go index 0a4133a68..e4340ca89 100644 --- a/internal/shaderir/hlsl/hlsl.go +++ b/internal/shaderir/hlsl/hlsl.go @@ -80,73 +80,84 @@ float4x4 float4x4FromScalar(float x) { return float4x4(x, 0, 0, 0, 0, x, 0, 0, 0, 0, x, 0, 0, 0, 0, x); }` -func Compile(p *shaderir.Program) (vertexShader, pixelShader, prelude string) { +func Compile(p *shaderir.Program) (vertexShader, pixelShader, vertexPrelude, pixelPrelude string) { offsets := UniformVariableOffsetsInDwords(p) c := &compileContext{ unit: p.Unit, } - var lines []string - lines = append(lines, strings.Split(utilFuncs, "\n")...) + appendPrelude := func(lines []string, vertex bool) []string { + lines = append(lines, strings.Split(utilFuncs, "\n")...) - lines = append(lines, "", "struct Varyings {") - lines = append(lines, "\tfloat4 Position : SV_POSITION;") - if len(p.Varyings) > 0 { - for i, v := range p.Varyings { - switch i { - case 0: - lines = append(lines, fmt.Sprintf("\tfloat2 M%d : TEXCOORD;", i)) - case 1: - lines = append(lines, fmt.Sprintf("\tfloat4 M%d : COLOR0;", i)) - default: - // Use COLOR[n] as a general purpose varying. - if v.Main != shaderir.Vec4 { - lines = append(lines, fmt.Sprintf("\t?(unexpected type: %s) M%d : COLOR%d;", v, i, i-1)) - } else { - lines = append(lines, fmt.Sprintf("\tfloat4 M%d : COLOR%d;", i, i-1)) + lines = append(lines, "", "struct Varyings {") + lines = append(lines, "\tfloat4 Position : SV_POSITION;") + if len(p.Varyings) > 0 { + for i, v := range p.Varyings { + switch i { + case 0: + lines = append(lines, fmt.Sprintf("\tfloat2 M%d : TEXCOORD;", i)) + case 1: + lines = append(lines, fmt.Sprintf("\tfloat4 M%d : COLOR0;", i)) + default: + // Use COLOR[n] as a general purpose varying. + if v.Main != shaderir.Vec4 { + lines = append(lines, fmt.Sprintf("\t?(unexpected type: %s) M%d : COLOR%d;", v, i, i-1)) + } else { + lines = append(lines, fmt.Sprintf("\tfloat4 M%d : COLOR%d;", i, i-1)) + } } } } + if !vertex { + lines = append(lines, "\tbool FrontFacing : SV_IsFrontFace;") + } + lines = append(lines, "};") + return lines } - lines = append(lines, "};") - prelude = strings.Join(lines, "\n") - lines = append(lines, "", "{{.Structs}}") + var vslines, pslines []string + vslines = appendPrelude(vslines, true) + pslines = appendPrelude(pslines, false) - if len(p.Uniforms) > 0 { - lines = append(lines, "") - lines = append(lines, "cbuffer Uniforms : register(b0) {") - for i, t := range p.Uniforms { - // packingoffset is not mandatory, but this is useful to ensure the correct offset is used. - offset := fmt.Sprintf("c%d", offsets[i]/UniformVariableBoundaryInDwords) - switch offsets[i] % UniformVariableBoundaryInDwords { - case 1: - offset += ".y" - case 2: - offset += ".z" - case 3: - offset += ".w" + vertexPrelude = strings.Join(vslines, "\n") + pixelPrelude = strings.Join(pslines, "\n") + + appendGlobalVariables := func(lines []string) []string { + lines = append(lines, "", "{{.Structs}}") + + if len(p.Uniforms) > 0 { + lines = append(lines, "") + lines = append(lines, "cbuffer Uniforms : register(b0) {") + for i, t := range p.Uniforms { + // packingoffset is not mandatory, but this is useful to ensure the correct offset is used. + offset := fmt.Sprintf("c%d", offsets[i]/UniformVariableBoundaryInDwords) + switch offsets[i] % UniformVariableBoundaryInDwords { + case 1: + offset += ".y" + case 2: + offset += ".z" + case 3: + offset += ".w" + } + lines = append(lines, fmt.Sprintf("\t%s : packoffset(%s);", c.varDecl(p, &t, fmt.Sprintf("U%d", i)), offset)) } - lines = append(lines, fmt.Sprintf("\t%s : packoffset(%s);", c.varDecl(p, &t, fmt.Sprintf("U%d", i)), offset)) + lines = append(lines, "}") } - lines = append(lines, "}") - } - if p.TextureCount > 0 { - lines = append(lines, "") - for i := 0; i < p.TextureCount; i++ { - lines = append(lines, fmt.Sprintf("Texture2D T%[1]d : register(t%[1]d);", i)) - } - if c.unit == shaderir.Texels { - lines = append(lines, "SamplerState samp : register(s0);") + if p.TextureCount > 0 { + lines = append(lines, "") + for i := 0; i < p.TextureCount; i++ { + lines = append(lines, fmt.Sprintf("Texture2D T%[1]d : register(t%[1]d);", i)) + } + if c.unit == shaderir.Texels { + lines = append(lines, "SamplerState samp : register(s0);") + } } + return lines } - - vslines := make([]string, len(lines)) - copy(vslines, lines) - pslines := make([]string, len(lines)) - copy(pslines, lines) + vslines = appendGlobalVariables(vslines) + pslines = appendGlobalVariables(pslines) var vsfuncs []*shaderir.Func if p.VertexFunc.Block != nil { @@ -522,6 +533,9 @@ func (c *compileContext) block(p *shaderir.Program, topBlock, block *shaderir.Bl } return result } + if callee.Type == shaderir.BuiltinFuncExpr && callee.BuiltinFunc == shaderir.FrontFacing { + return fmt.Sprintf("%s.FrontFacing", vsOut) + } return fmt.Sprintf("%s(%s)", expr(&e.Exprs[0]), strings.Join(args, ", ")) case shaderir.FieldSelector: return fmt.Sprintf("(%s).%s", expr(&e.Exprs[0]), expr(&e.Exprs[1])) diff --git a/internal/shaderir/ir_test.go b/internal/shaderir/ir_test.go index a248402af..96046cdac 100644 --- a/internal/shaderir/ir_test.go +++ b/internal/shaderir/ir_test.go @@ -819,9 +819,9 @@ void F0(in float l0, in float l1, out float l2) { }`, Metal: msl.Prelude(shaderir.Pixels) + ` -void F0(float l0, float l1, thread float& l2); +void F0(bool front_facing, float l0, float l1, thread float& l2); -void F0(float l0, float l1, thread float& l2) { +void F0(bool front_facing, float l0, float l1, thread float& l2) { for (int l3 = 0; l3 < 100; l3++) { int l4 = 0; l2 = l4; @@ -916,9 +916,9 @@ void F0(in float l0, in float l1, out float l2) { }`, Metal: msl.Prelude(shaderir.Pixels) + ` -void F0(float l0, float l1, thread float& l2); +void F0(bool front_facing, float l0, float l1, thread float& l2); -void F0(float l0, float l1, thread float& l2) { +void F0(bool front_facing, float l0, float l1, thread float& l2) { for (int l3 = 0; l3 < 100; l3++) { int l4 = 0; l2 = l4; diff --git a/internal/shaderir/msl/msl.go b/internal/shaderir/msl/msl.go index 2109eaa16..9a0143380 100644 --- a/internal/shaderir/msl/msl.go +++ b/internal/shaderir/msl/msl.go @@ -156,6 +156,8 @@ func Compile(p *shaderir.Program) (shader string) { lines[len(lines)-1] += "," lines = append(lines, fmt.Sprintf("\ttexture2d T%[1]d [[texture(%[1]d)]]", i)) } + lines[len(lines)-1] += "," + lines = append(lines, "\tbool front_facing [[front_facing]]") lines[len(lines)-1] += ") {" lines = append(lines, c.block(p, p.FragmentFunc.Block, p.FragmentFunc.Block, 0)...) lines = append(lines, "}") @@ -244,6 +246,7 @@ func (c *compileContext) function(p *shaderir.Program, f *shaderir.Func, prototy for i := 0; i < p.TextureCount; i++ { args = append(args, fmt.Sprintf("texture2d T%d", i)) } + args = append(args, "bool front_facing") var idx int for _, t := range f.InParams { @@ -404,6 +407,7 @@ func (c *compileContext) block(p *shaderir.Program, topBlock, block *shaderir.Bl for i := 0; i < p.TextureCount; i++ { args = append(args, fmt.Sprintf("T%d", i)) } + args = append(args, "front_facing") } for _, exp := range e.Exprs[1:] { args = append(args, expr(&exp)) @@ -425,6 +429,9 @@ func (c *compileContext) block(p *shaderir.Program, topBlock, block *shaderir.Bl } return result } + if callee.Type == shaderir.BuiltinFuncExpr && callee.BuiltinFunc == shaderir.FrontFacing { + return "(front_facing)" + } return fmt.Sprintf("%s(%s)", expr(&callee), strings.Join(args, ", ")) case shaderir.FieldSelector: return fmt.Sprintf("(%s).%s", expr(&e.Exprs[0]), expr(&e.Exprs[1])) diff --git a/internal/shaderir/program.go b/internal/shaderir/program.go index 88202c044..1a87dceeb 100644 --- a/internal/shaderir/program.go +++ b/internal/shaderir/program.go @@ -294,6 +294,7 @@ const ( Fwidth BuiltinFunc = "fwidth" DiscardF BuiltinFunc = "discard" TexelAt BuiltinFunc = "__texelAt" + FrontFacing BuiltinFunc = "frontfacing" ) func ParseBuiltinFunc(str string) (BuiltinFunc, bool) { @@ -351,7 +352,8 @@ func ParseBuiltinFunc(str string) (BuiltinFunc, bool) { Dfdy, Fwidth, DiscardF, - TexelAt: + TexelAt, + FrontFacing: return BuiltinFunc(str), true } return "", false diff --git a/internal/shaderlister/main.go b/internal/shaderlister/main.go index 6020bb38b..85008be9c 100644 --- a/internal/shaderlister/main.go +++ b/internal/shaderlister/main.go @@ -497,7 +497,7 @@ func compile(shader *Shader, targets []string) error { Fragment: fs, } case "hlsl": - vs, ps, _ := hlsl.Compile(ir) + vs, ps, _, _ := hlsl.Compile(ir) shader.HLSL = &HLSL{ Vertex: vs, Pixel: ps, diff --git a/shader_test.go b/shader_test.go index 9bd526f2c..f33f405cd 100644 --- a/shader_test.go +++ b/shader_test.go @@ -3010,3 +3010,56 @@ func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 { } } } + +func TestShaderFrontFacing(t *testing.T) { + 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 { + if frontfacing() { + return vec4(0.5, 0, 0, 1) + } + return vec4(0, 0.5, 0, 1) +} +`)) + if err != nil { + t.Fatal(err) + } + + vs := []ebiten.Vertex{ + { + DstX: 0, + DstY: 0, + }, + { + DstX: w, + DstY: 0, + }, + { + DstX: 0, + DstY: h, + }, + { + DstX: w, + DstY: h, + }, + } + op := &ebiten.DrawTrianglesShaderOptions{} + op.Blend = ebiten.BlendLighter + dst.DrawTrianglesShader32(vs, []uint32{0, 1, 2, 1, 2, 3}, s, op) + dst.DrawTrianglesShader32(vs, []uint32{2, 1, 0, 3, 2, 1}, s, op) + + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + got := dst.At(i, j).(color.RGBA) + want := color.RGBA{R: 0x80, G: 0x80, B: 0x00, A: 0xff} + if !sameColors(got, want, 2) { + t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } +}