ebiten/internal/shaderir/hlsl/hlsl.go
Hajime Hoshi 81bb5044ea internal/shaderir: revert the refactoring to generalize the memory layout logic
Unfortunately, the memory layout was not so universal. For example,
the memory layout for mat2 is different between Metal and DirectX.
2024-11-24 23:57:30 +09:00

616 lines
18 KiB
Go

// Copyright 2022 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hlsl
import (
"fmt"
"go/constant"
"go/token"
"math"
"regexp"
"strings"
"github.com/hajimehoshi/ebiten/v2/internal/shaderir"
)
const (
vsOut = "varyings"
)
type compileContext struct {
structNames map[string]string
structTypes []shaderir.Type
unit shaderir.Unit
}
func (c *compileContext) structName(p *shaderir.Program, t *shaderir.Type) string {
if t.Main != shaderir.Struct {
panic("hlsl: the given type at structName must be a struct")
}
s := t.String()
if n, ok := c.structNames[s]; ok {
return n
}
n := fmt.Sprintf("S%d", len(c.structNames))
if c.structNames == nil {
c.structNames = map[string]string{}
}
c.structNames[s] = n
c.structTypes = append(c.structTypes, *t)
return n
}
const utilFuncs = `float mod(float x, float y) {
return x - y * floor(x/y);
}
float2 mod(float2 x, float2 y) {
return x - y * floor(x/y);
}
float3 mod(float3 x, float3 y) {
return x - y * floor(x/y);
}
float4 mod(float4 x, float4 y) {
return x - y * floor(x/y);
}
float2x2 float2x2FromScalar(float x) {
return float2x2(x, 0, 0, x);
}
float3x3 float3x3FromScalar(float x) {
return float3x3(x, 0, 0, 0, x, 0, 0, 0, x);
}
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) {
offsets := UniformVariableOffsetsInDWords(p)
c := &compileContext{
unit: p.Unit,
}
var lines []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, "};")
prelude = strings.Join(lines, "\n")
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, "}")
}
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);")
}
}
vslines := make([]string, len(lines))
copy(vslines, lines)
pslines := make([]string, len(lines))
copy(pslines, lines)
var vsfuncs []*shaderir.Func
if p.VertexFunc.Block != nil {
vsfuncs = p.ReachableFuncsFromBlock(p.VertexFunc.Block)
} else {
// Use all the functions for testing.
vsfuncs = make([]*shaderir.Func, 0, len(p.Funcs))
for _, f := range p.Funcs {
f := f
vsfuncs = append(vsfuncs, &f)
}
}
if len(vsfuncs) > 0 {
vslines = append(vslines, "")
for _, f := range vsfuncs {
vslines = append(vslines, c.function(p, f, true)...)
}
for _, f := range vsfuncs {
if len(vslines) > 0 && vslines[len(vslines)-1] != "" {
vslines = append(vslines, "")
}
vslines = append(vslines, c.function(p, f, false)...)
}
}
if p.VertexFunc.Block != nil && len(p.VertexFunc.Block.Stmts) > 0 {
vslines = append(vslines, "")
var args []string
for i, a := range p.Attributes {
switch i {
case 0:
args = append(args, fmt.Sprintf("float2 A%d : POSITION", i))
case 1:
args = append(args, fmt.Sprintf("float2 A%d : TEXCOORD", i))
case 2:
args = append(args, fmt.Sprintf("float4 A%d : COLOR0", i))
default:
// Use COLOR[n] as a general purpose varying.
if a.Main != shaderir.Vec4 {
args = append(args, fmt.Sprintf("?(unexpected type: %s) A%d : COLOR%d", a, i, i-2))
} else {
args = append(args, fmt.Sprintf("float4 A%d : COLOR%d", i, i-2))
}
}
}
vslines = append(vslines, "Varyings VSMain("+strings.Join(args, ", ")+") {")
vslines = append(vslines, fmt.Sprintf("\tVaryings %s;", vsOut))
vslines = append(vslines, c.block(p, p.VertexFunc.Block, p.VertexFunc.Block, 0)...)
if last := fmt.Sprintf("\treturn %s;", vsOut); vslines[len(vslines)-1] != last {
vslines = append(vslines, last)
}
vslines = append(vslines, "}")
}
var psfuncs []*shaderir.Func
if p.FragmentFunc.Block != nil {
psfuncs = p.ReachableFuncsFromBlock(p.FragmentFunc.Block)
} else {
// Use all the functions for testing.
psfuncs = make([]*shaderir.Func, 0, len(p.Funcs))
for _, f := range p.Funcs {
f := f
psfuncs = append(psfuncs, &f)
}
}
if len(psfuncs) > 0 {
pslines = append(pslines, "")
for _, f := range psfuncs {
pslines = append(pslines, c.function(p, f, true)...)
}
for _, f := range psfuncs {
if len(pslines) > 0 && pslines[len(pslines)-1] != "" {
pslines = append(pslines, "")
}
pslines = append(pslines, c.function(p, f, false)...)
}
}
if p.FragmentFunc.Block != nil && len(p.FragmentFunc.Block.Stmts) > 0 {
pslines = append(pslines, "")
pslines = append(pslines, fmt.Sprintf("float4 PSMain(Varyings %s) : SV_TARGET {", vsOut))
pslines = append(pslines, c.block(p, p.FragmentFunc.Block, p.FragmentFunc.Block, 0)...)
pslines = append(pslines, "}")
}
vertexShader = strings.Join(vslines, "\n")
pixelShader = strings.Join(pslines, "\n")
// Struct types are determined after converting the program.
shaders := []string{vertexShader, pixelShader}
for i, shader := range shaders {
if len(c.structTypes) > 0 {
var stlines []string
for i, t := range c.structTypes {
stlines = append(stlines, fmt.Sprintf("struct S%d {", i))
for j, st := range t.Sub {
stlines = append(stlines, fmt.Sprintf("\t%s;", c.varDecl(p, &st, fmt.Sprintf("M%d", j))))
}
stlines = append(stlines, "};")
}
st := strings.Join(stlines, "\n")
shader = strings.ReplaceAll(shader, "{{.Structs}}", st)
} else {
shader = strings.ReplaceAll(shader, "{{.Structs}}", "")
}
nls := regexp.MustCompile(`\n\n+`)
shader = nls.ReplaceAllString(shader, "\n\n")
shader = strings.TrimSpace(shader) + "\n"
shaders[i] = shader
}
vertexShader = shaders[0]
pixelShader = shaders[1]
return
}
func (c *compileContext) typ(p *shaderir.Program, t *shaderir.Type) (string, string) {
switch t.Main {
case shaderir.None:
return "void", ""
case shaderir.Struct:
return c.structName(p, t), ""
default:
return typeString(t)
}
}
func (c *compileContext) varDecl(p *shaderir.Program, t *shaderir.Type, varname string) string {
switch t.Main {
case shaderir.None:
return "?(none)"
case shaderir.Struct:
return fmt.Sprintf("%s %s", c.structName(p, t), varname)
default:
t0, t1 := typeString(t)
return fmt.Sprintf("%s %s%s", t0, varname, t1)
}
}
func (c *compileContext) varInit(p *shaderir.Program, t *shaderir.Type) string {
switch t.Main {
case shaderir.None:
return "?(none)"
case shaderir.Array:
init := c.varInit(p, &t.Sub[0])
es := make([]string, 0, t.Length)
for i := 0; i < t.Length; i++ {
es = append(es, init)
}
t0, t1 := typeString(t)
return fmt.Sprintf("%s%s(%s)", t0, t1, strings.Join(es, ", "))
case shaderir.Struct:
panic("not implemented")
case shaderir.Bool:
return "false"
case shaderir.Int, shaderir.IVec2, shaderir.IVec3, shaderir.IVec4:
return "0"
case shaderir.Float, shaderir.Vec2, shaderir.Vec3, shaderir.Vec4, shaderir.Mat2, shaderir.Mat3, shaderir.Mat4:
return "0.0"
default:
t0, t1 := c.typ(p, t)
panic(fmt.Sprintf("?(unexpected type: %s%s)", t0, t1))
}
}
func (c *compileContext) function(p *shaderir.Program, f *shaderir.Func, prototype bool) []string {
var args []string
var idx int
for _, t := range f.InParams {
args = append(args, "in "+c.varDecl(p, &t, fmt.Sprintf("l%d", idx)))
idx++
}
for _, t := range f.OutParams {
args = append(args, "out "+c.varDecl(p, &t, fmt.Sprintf("l%d", idx)))
idx++
}
argsstr := "void"
if len(args) > 0 {
argsstr = strings.Join(args, ", ")
}
t0, t1 := c.typ(p, &f.Return)
sig := fmt.Sprintf("%s%s F%d(%s)", t0, t1, f.Index, argsstr)
var lines []string
if prototype {
lines = append(lines, fmt.Sprintf("%s;", sig))
return lines
}
lines = append(lines, fmt.Sprintf("%s {", sig))
lines = append(lines, c.block(p, f.Block, f.Block, 0)...)
lines = append(lines, "}")
return lines
}
func constantToNumberLiteral(v constant.Value) string {
switch v.Kind() {
case constant.Bool:
if constant.BoolVal(v) {
return "true"
}
return "false"
case constant.Int:
x, _ := constant.Int64Val(v)
return fmt.Sprintf("%d", x)
case constant.Float:
x, _ := constant.Float64Val(v)
if i := math.Floor(x); i == x {
return fmt.Sprintf("%d.0", int64(i))
}
return fmt.Sprintf("%.10e", x)
}
return fmt.Sprintf("?(unexpected literal: %s)", v)
}
func (c *compileContext) localVariableName(p *shaderir.Program, topBlock *shaderir.Block, idx int) string {
switch topBlock {
case p.VertexFunc.Block:
na := len(p.Attributes)
nv := len(p.Varyings)
switch {
case idx < na:
return fmt.Sprintf("A%d", idx)
case idx == na:
return fmt.Sprintf("%s.Position", vsOut)
case idx < na+nv+1:
return fmt.Sprintf("%s.M%d", vsOut, idx-na-1)
default:
return fmt.Sprintf("l%d", idx-(na+nv+1))
}
case p.FragmentFunc.Block:
nv := len(p.Varyings)
switch {
case idx == 0:
return fmt.Sprintf("%s.Position", vsOut)
case idx < nv+1:
return fmt.Sprintf("%s.M%d", vsOut, idx-1)
default:
return fmt.Sprintf("l%d", idx-(nv+1))
}
default:
return fmt.Sprintf("l%d", idx)
}
}
func (c *compileContext) initVariable(p *shaderir.Program, topBlock, block *shaderir.Block, index int, decl bool, level int) []string {
idt := strings.Repeat("\t", level+1)
name := c.localVariableName(p, topBlock, index)
t := p.LocalVariableType(topBlock, block, index)
var lines []string
switch t.Main {
case shaderir.Array:
if decl {
lines = append(lines, fmt.Sprintf("%s%s;", idt, c.varDecl(p, &t, name)))
}
init := c.varInit(p, &t.Sub[0])
for i := 0; i < t.Length; i++ {
lines = append(lines, fmt.Sprintf("%s%s[%d] = %s;", idt, name, i, init))
}
case shaderir.None:
// The type is None e.g., when the variable is a for-loop counter.
default:
if decl {
lines = append(lines, fmt.Sprintf("%s%s = %s;", idt, c.varDecl(p, &t, name), c.varInit(p, &t)))
} else {
lines = append(lines, fmt.Sprintf("%s%s = %s;", idt, name, c.varInit(p, &t)))
}
}
return lines
}
func (c *compileContext) block(p *shaderir.Program, topBlock, block *shaderir.Block, level int) []string {
if block == nil {
return nil
}
var lines []string
for i := range block.LocalVars {
lines = append(lines, c.initVariable(p, topBlock, block, block.LocalVarIndexOffset+i, true, level)...)
}
var expr func(e *shaderir.Expr) string
expr = func(e *shaderir.Expr) string {
switch e.Type {
case shaderir.NumberExpr:
return constantToNumberLiteral(e.Const)
case shaderir.UniformVariable:
return fmt.Sprintf("U%d", e.Index)
case shaderir.TextureVariable:
return fmt.Sprintf("T%d", e.Index)
case shaderir.LocalVariable:
return c.localVariableName(p, topBlock, e.Index)
case shaderir.StructMember:
return fmt.Sprintf("M%d", e.Index)
case shaderir.BuiltinFuncExpr:
return c.builtinFuncString(e.BuiltinFunc)
case shaderir.SwizzlingExpr:
if !shaderir.IsValidSwizzling(e.Swizzling) {
return fmt.Sprintf("?(unexpected swizzling: %s)", e.Swizzling)
}
return e.Swizzling
case shaderir.FunctionExpr:
return fmt.Sprintf("F%d", e.Index)
case shaderir.Unary:
var op string
switch e.Op {
case shaderir.Add, shaderir.Sub, shaderir.NotOp:
op = opString(e.Op)
default:
op = fmt.Sprintf("?(unexpected op: %d)", e.Op)
}
return fmt.Sprintf("%s(%s)", op, expr(&e.Exprs[0]))
case shaderir.Binary:
switch e.Op {
case shaderir.VectorEqualOp:
return fmt.Sprintf("all((%s) == (%s))", expr(&e.Exprs[0]), expr(&e.Exprs[1]))
case shaderir.VectorNotEqualOp:
return fmt.Sprintf("!all((%s) == (%s))", expr(&e.Exprs[0]), expr(&e.Exprs[1]))
case shaderir.MatrixMul:
// If either is a matrix, use the mul function.
// Swap the order of the lhs and the rhs since matrices are row-major in HLSL.
return fmt.Sprintf("mul(%s, %s)", expr(&e.Exprs[1]), expr(&e.Exprs[0]))
}
return fmt.Sprintf("(%s) %s (%s)", expr(&e.Exprs[0]), opString(e.Op), expr(&e.Exprs[1]))
case shaderir.Selection:
return fmt.Sprintf("(%s) ? (%s) : (%s)", expr(&e.Exprs[0]), expr(&e.Exprs[1]), expr(&e.Exprs[2]))
case shaderir.Call:
callee := e.Exprs[0]
var args []string
for _, exp := range e.Exprs[1:] {
args = append(args, expr(&exp))
}
if callee.Type == shaderir.BuiltinFuncExpr {
switch callee.BuiltinFunc {
case shaderir.Vec2F, shaderir.Vec3F, shaderir.Vec4F, shaderir.IVec2F, shaderir.IVec3F, shaderir.IVec4F:
if len(args) == 1 {
// Use casting. For example, `float4(1)` doesn't work.
return fmt.Sprintf("(%s)(%s)", expr(&e.Exprs[0]), args[0])
}
case shaderir.Mat2F:
if len(args) == 1 {
// In HSLS, casting a scalar to a matrix initializes all the components.
// There seems no easy way to have an identity matrix.
return fmt.Sprintf("float2x2FromScalar(%s)", args[0])
}
case shaderir.Mat3F:
if len(args) == 1 {
return fmt.Sprintf("float3x3FromScalar(%s)", args[0])
}
case shaderir.Mat4F:
if len(args) == 1 {
return fmt.Sprintf("float4x4FromScalar(%s)", args[0])
}
case shaderir.TexelAt:
switch c.unit {
case shaderir.Pixels:
return fmt.Sprintf("%s.Load(int3(%s, 0))", args[0], strings.Join(args[1:], ", "))
case shaderir.Texels:
return fmt.Sprintf("%s.Sample(samp, %s)", args[0], strings.Join(args[1:], ", "))
default:
panic(fmt.Sprintf("hlsl: unexpected unit: %d", p.Unit))
}
}
}
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]))
case shaderir.Index:
return fmt.Sprintf("(%s)[%s]", expr(&e.Exprs[0]), expr(&e.Exprs[1]))
default:
return fmt.Sprintf("?(unexpected expr: %d)", e.Type)
}
}
idt := strings.Repeat("\t", level+1)
for _, s := range block.Stmts {
switch s.Type {
case shaderir.ExprStmt:
lines = append(lines, fmt.Sprintf("%s%s;", idt, expr(&s.Exprs[0])))
case shaderir.BlockStmt:
lines = append(lines, idt+"{")
lines = append(lines, c.block(p, topBlock, s.Blocks[0], level+1)...)
lines = append(lines, idt+"}")
case shaderir.Assign:
lhs := s.Exprs[0]
rhs := s.Exprs[1]
if lhs.Type == shaderir.LocalVariable {
if t := p.LocalVariableType(topBlock, block, lhs.Index); t.Main == shaderir.Array {
for i := 0; i < t.Length; i++ {
lines = append(lines, fmt.Sprintf("%[1]s%[2]s[%[3]d] = %[4]s[%[3]d];", idt, expr(&lhs), i, expr(&rhs)))
}
continue
}
}
lines = append(lines, fmt.Sprintf("%s%s = %s;", idt, expr(&lhs), expr(&rhs)))
case shaderir.Init:
lines = append(lines, c.initVariable(p, topBlock, block, s.InitIndex, false, level)...)
case shaderir.If:
lines = append(lines, fmt.Sprintf("%sif (%s) {", idt, expr(&s.Exprs[0])))
lines = append(lines, c.block(p, topBlock, s.Blocks[0], level+1)...)
if len(s.Blocks) > 1 {
lines = append(lines, fmt.Sprintf("%s} else {", idt))
lines = append(lines, c.block(p, topBlock, s.Blocks[1], level+1)...)
}
lines = append(lines, fmt.Sprintf("%s}", idt))
case shaderir.For:
v := c.localVariableName(p, topBlock, s.ForVarIndex)
var delta string
switch val, _ := constant.Float64Val(s.ForDelta); val {
case 0:
delta = fmt.Sprintf("?(unexpected delta: %v)", s.ForDelta)
case 1:
delta = fmt.Sprintf("%s++", v)
case -1:
delta = fmt.Sprintf("%s--", v)
default:
d := s.ForDelta
if val > 0 {
delta = fmt.Sprintf("%s += %s", v, constantToNumberLiteral(d))
} else {
d = constant.UnaryOp(token.SUB, d, 0)
delta = fmt.Sprintf("%s -= %s", v, constantToNumberLiteral(d))
}
}
var op string
switch s.ForOp {
case shaderir.LessThanOp, shaderir.LessThanEqualOp, shaderir.GreaterThanOp, shaderir.GreaterThanEqualOp, shaderir.EqualOp, shaderir.NotEqualOp:
op = opString(s.ForOp)
default:
op = fmt.Sprintf("?(unexpected op: %d)", s.ForOp)
}
t := s.ForVarType
init := constantToNumberLiteral(s.ForInit)
end := constantToNumberLiteral(s.ForEnd)
t0, t1 := typeString(&t)
lines = append(lines, fmt.Sprintf("%sfor (%s %s%s = %s; %s %s %s; %s) {", idt, t0, v, t1, init, v, op, end, delta))
lines = append(lines, c.block(p, topBlock, s.Blocks[0], level+1)...)
lines = append(lines, fmt.Sprintf("%s}", idt))
case shaderir.Continue:
lines = append(lines, idt+"continue;")
case shaderir.Break:
lines = append(lines, idt+"break;")
case shaderir.Return:
switch {
case topBlock == p.VertexFunc.Block:
lines = append(lines, fmt.Sprintf("%sreturn %s;", idt, vsOut))
case len(s.Exprs) == 0:
lines = append(lines, idt+"return;")
default:
lines = append(lines, fmt.Sprintf("%sreturn %s;", idt, expr(&s.Exprs[0])))
}
case shaderir.Discard:
// 'discard' is invoked only in the fragment shader entry point.
lines = append(lines, idt+"discard;", idt+"return float4(0.0, 0.0, 0.0, 0.0);")
default:
lines = append(lines, fmt.Sprintf("%s?(unexpected stmt: %d)", idt, s.Type))
}
}
return lines
}