internal/shader: add a builtin function frontfacing

Closes #3264
This commit is contained in:
Hajime Hoshi
2025-06-22 23:12:10 +09:00
parent 5f18a80a0f
commit a96153805e
14 changed files with 155 additions and 66 deletions

View File

@@ -102,7 +102,7 @@ func compileShader(program *shaderir.Program) (vsh, psh *_ID3DBlob, ferr error)
return vsh, psh, nil return vsh, psh, nil
} }
vs, ps, _ := hlsl.Compile(program) vs, ps, _, _ := hlsl.Compile(program)
var flag uint32 = uint32(_D3DCOMPILE_OPTIMIZATION_LEVEL3) var flag uint32 = uint32(_D3DCOMPILE_OPTIMIZATION_LEVEL3)
var wg errgroup.Group var wg errgroup.Group

View File

@@ -421,6 +421,16 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
return nil, nil, nil, false return nil, nil, nil, false
} }
finalType = shaderir.Type{Main: shaderir.Vec4} 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: case shaderir.DiscardF:
if len(args) != 0 { if len(args) != 0 {
cs.addError(e.Pos(), fmt.Sprintf("number of %s's arguments must be 0 but %d", callee.BuiltinFunc, len(args))) 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, Type: shaderir.Discard,
}) })
return nil, nil, stmts, true return nil, nil, stmts, true
case shaderir.Clamp, shaderir.Mix, shaderir.Smoothstep, shaderir.Faceforward, shaderir.Refract: case shaderir.Clamp, shaderir.Mix, shaderir.Smoothstep, shaderir.Faceforward, shaderir.Refract:
// 3 arguments // 3 arguments
if len(args) != 3 { if len(args) != 3 {

View File

@@ -180,8 +180,8 @@ func TestCompile(t *testing.T) {
} }
if tc.HLSL != nil { if tc.HLSL != nil {
vs, _, prelude := hlsl.Compile(s) vs, _, vertexPrelude, _ := hlsl.Compile(s)
if got, want := hlslNormalize(vs, prelude), hlslNormalize(string(tc.HLSL), prelude); got != want { if got, want := hlslNormalize(vs, vertexPrelude), hlslNormalize(string(tc.HLSL), vertexPrelude); got != want {
compare(t, "HLSL", got, want) compare(t, "HLSL", got, want)
} }
} }

View File

@@ -1,6 +1,6 @@
void F0(thread array<float2, 3>& l0); void F0(bool front_facing, thread array<float2, 3>& l0);
void F0(thread array<float2, 3>& l0) { void F0(bool front_facing, thread array<float2, 3>& l0) {
array<float2, 2> l1 = {}; array<float2, 2> l1 = {};
array<float2, 3> l2 = {}; array<float2, 3> l2 = {};
{ {

View File

@@ -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 l2 = float(0);
float l3 = float(0); float l3 = float(0);
l2 = atan((l1) / (l0)); l2 = atan((l1) / (l0));

View File

@@ -1,6 +1,6 @@
void F0(thread float& l0, thread array<float, 4>& l1, thread float4& l2); void F0(bool front_facing, thread float& l0, thread array<float, 4>& l1, thread float4& l2);
void F0(thread float& l0, thread array<float, 4>& l1, thread float4& l2) { void F0(bool front_facing, thread float& l0, thread array<float, 4>& l1, thread float4& l2) {
l0 = float(0); l0 = float(0);
l1 = {}; l1 = {};
l2 = float4(0); l2 = float4(0);

View File

@@ -29,6 +29,7 @@ vertex Varyings Vertex(
fragment float4 Fragment( fragment float4 Fragment(
Varyings varyings [[stage_in]], 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); return float4((varyings.Position).x, (varyings.M0).y, (varyings.M1).z, 1.0);
} }

View File

@@ -520,6 +520,9 @@ func (c *compileContext) block(p *shaderir.Program, topBlock, block *shaderir.Bl
} }
return result return result
} }
if callee.BuiltinFunc == shaderir.FrontFacing {
return "gl_FrontFacing"
}
} }
// Using parentheses at the callee is illegal. // Using parentheses at the callee is illegal.
return fmt.Sprintf("%s(%s)", expr(&callee), strings.Join(args, ", ")) return fmt.Sprintf("%s(%s)", expr(&callee), strings.Join(args, ", "))

View File

@@ -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); 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) offsets := UniformVariableOffsetsInDwords(p)
c := &compileContext{ c := &compileContext{
unit: p.Unit, unit: p.Unit,
} }
var lines []string appendPrelude := func(lines []string, vertex bool) []string {
lines = append(lines, strings.Split(utilFuncs, "\n")...) lines = append(lines, strings.Split(utilFuncs, "\n")...)
lines = append(lines, "", "struct Varyings {") lines = append(lines, "", "struct Varyings {")
lines = append(lines, "\tfloat4 Position : SV_POSITION;") lines = append(lines, "\tfloat4 Position : SV_POSITION;")
if len(p.Varyings) > 0 { if len(p.Varyings) > 0 {
for i, v := range p.Varyings { for i, v := range p.Varyings {
switch i { switch i {
case 0: case 0:
lines = append(lines, fmt.Sprintf("\tfloat2 M%d : TEXCOORD;", i)) lines = append(lines, fmt.Sprintf("\tfloat2 M%d : TEXCOORD;", i))
case 1: case 1:
lines = append(lines, fmt.Sprintf("\tfloat4 M%d : COLOR0;", i)) lines = append(lines, fmt.Sprintf("\tfloat4 M%d : COLOR0;", i))
default: default:
// Use COLOR[n] as a general purpose varying. // Use COLOR[n] as a general purpose varying.
if v.Main != shaderir.Vec4 { if v.Main != shaderir.Vec4 {
lines = append(lines, fmt.Sprintf("\t?(unexpected type: %s) M%d : COLOR%d;", v, i, i-1)) lines = append(lines, fmt.Sprintf("\t?(unexpected type: %s) M%d : COLOR%d;", v, i, i-1))
} else { } else {
lines = append(lines, fmt.Sprintf("\tfloat4 M%d : COLOR%d;", i, i-1)) 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 { vertexPrelude = strings.Join(vslines, "\n")
lines = append(lines, "") pixelPrelude = strings.Join(pslines, "\n")
lines = append(lines, "cbuffer Uniforms : register(b0) {")
for i, t := range p.Uniforms { appendGlobalVariables := func(lines []string) []string {
// packingoffset is not mandatory, but this is useful to ensure the correct offset is used. lines = append(lines, "", "{{.Structs}}")
offset := fmt.Sprintf("c%d", offsets[i]/UniformVariableBoundaryInDwords)
switch offsets[i] % UniformVariableBoundaryInDwords { if len(p.Uniforms) > 0 {
case 1: lines = append(lines, "")
offset += ".y" lines = append(lines, "cbuffer Uniforms : register(b0) {")
case 2: for i, t := range p.Uniforms {
offset += ".z" // packingoffset is not mandatory, but this is useful to ensure the correct offset is used.
case 3: offset := fmt.Sprintf("c%d", offsets[i]/UniformVariableBoundaryInDwords)
offset += ".w" 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 { if p.TextureCount > 0 {
lines = append(lines, "") lines = append(lines, "")
for i := 0; i < p.TextureCount; 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, fmt.Sprintf("Texture2D T%[1]d : register(t%[1]d);", i))
} }
if c.unit == shaderir.Texels { if c.unit == shaderir.Texels {
lines = append(lines, "SamplerState samp : register(s0);") lines = append(lines, "SamplerState samp : register(s0);")
}
} }
return lines
} }
vslines = appendGlobalVariables(vslines)
vslines := make([]string, len(lines)) pslines = appendGlobalVariables(pslines)
copy(vslines, lines)
pslines := make([]string, len(lines))
copy(pslines, lines)
var vsfuncs []*shaderir.Func var vsfuncs []*shaderir.Func
if p.VertexFunc.Block != nil { if p.VertexFunc.Block != nil {
@@ -522,6 +533,9 @@ func (c *compileContext) block(p *shaderir.Program, topBlock, block *shaderir.Bl
} }
return result 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, ", ")) return fmt.Sprintf("%s(%s)", expr(&e.Exprs[0]), strings.Join(args, ", "))
case shaderir.FieldSelector: case shaderir.FieldSelector:
return fmt.Sprintf("(%s).%s", expr(&e.Exprs[0]), expr(&e.Exprs[1])) return fmt.Sprintf("(%s).%s", expr(&e.Exprs[0]), expr(&e.Exprs[1]))

View File

@@ -819,9 +819,9 @@ void F0(in float l0, in float l1, out float l2) {
}`, }`,
Metal: msl.Prelude(shaderir.Pixels) + ` 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++) { for (int l3 = 0; l3 < 100; l3++) {
int l4 = 0; int l4 = 0;
l2 = l4; l2 = l4;
@@ -916,9 +916,9 @@ void F0(in float l0, in float l1, out float l2) {
}`, }`,
Metal: msl.Prelude(shaderir.Pixels) + ` 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++) { for (int l3 = 0; l3 < 100; l3++) {
int l4 = 0; int l4 = 0;
l2 = l4; l2 = l4;

View File

@@ -156,6 +156,8 @@ func Compile(p *shaderir.Program) (shader string) {
lines[len(lines)-1] += "," lines[len(lines)-1] += ","
lines = append(lines, fmt.Sprintf("\ttexture2d<float> T%[1]d [[texture(%[1]d)]]", i)) lines = append(lines, fmt.Sprintf("\ttexture2d<float> T%[1]d [[texture(%[1]d)]]", i))
} }
lines[len(lines)-1] += ","
lines = append(lines, "\tbool front_facing [[front_facing]]")
lines[len(lines)-1] += ") {" lines[len(lines)-1] += ") {"
lines = append(lines, c.block(p, p.FragmentFunc.Block, p.FragmentFunc.Block, 0)...) lines = append(lines, c.block(p, p.FragmentFunc.Block, p.FragmentFunc.Block, 0)...)
lines = append(lines, "}") 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++ { for i := 0; i < p.TextureCount; i++ {
args = append(args, fmt.Sprintf("texture2d<float> T%d", i)) args = append(args, fmt.Sprintf("texture2d<float> T%d", i))
} }
args = append(args, "bool front_facing")
var idx int var idx int
for _, t := range f.InParams { 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++ { for i := 0; i < p.TextureCount; i++ {
args = append(args, fmt.Sprintf("T%d", i)) args = append(args, fmt.Sprintf("T%d", i))
} }
args = append(args, "front_facing")
} }
for _, exp := range e.Exprs[1:] { for _, exp := range e.Exprs[1:] {
args = append(args, expr(&exp)) args = append(args, expr(&exp))
@@ -425,6 +429,9 @@ func (c *compileContext) block(p *shaderir.Program, topBlock, block *shaderir.Bl
} }
return result return result
} }
if callee.Type == shaderir.BuiltinFuncExpr && callee.BuiltinFunc == shaderir.FrontFacing {
return "(front_facing)"
}
return fmt.Sprintf("%s(%s)", expr(&callee), strings.Join(args, ", ")) return fmt.Sprintf("%s(%s)", expr(&callee), strings.Join(args, ", "))
case shaderir.FieldSelector: case shaderir.FieldSelector:
return fmt.Sprintf("(%s).%s", expr(&e.Exprs[0]), expr(&e.Exprs[1])) return fmt.Sprintf("(%s).%s", expr(&e.Exprs[0]), expr(&e.Exprs[1]))

View File

@@ -294,6 +294,7 @@ const (
Fwidth BuiltinFunc = "fwidth" Fwidth BuiltinFunc = "fwidth"
DiscardF BuiltinFunc = "discard" DiscardF BuiltinFunc = "discard"
TexelAt BuiltinFunc = "__texelAt" TexelAt BuiltinFunc = "__texelAt"
FrontFacing BuiltinFunc = "frontfacing"
) )
func ParseBuiltinFunc(str string) (BuiltinFunc, bool) { func ParseBuiltinFunc(str string) (BuiltinFunc, bool) {
@@ -351,7 +352,8 @@ func ParseBuiltinFunc(str string) (BuiltinFunc, bool) {
Dfdy, Dfdy,
Fwidth, Fwidth,
DiscardF, DiscardF,
TexelAt: TexelAt,
FrontFacing:
return BuiltinFunc(str), true return BuiltinFunc(str), true
} }
return "", false return "", false

View File

@@ -497,7 +497,7 @@ func compile(shader *Shader, targets []string) error {
Fragment: fs, Fragment: fs,
} }
case "hlsl": case "hlsl":
vs, ps, _ := hlsl.Compile(ir) vs, ps, _, _ := hlsl.Compile(ir)
shader.HLSL = &HLSL{ shader.HLSL = &HLSL{
Vertex: vs, Vertex: vs,
Pixel: ps, Pixel: ps,

View File

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