Files
gio-patched/gpu/build.go
T
Elias Naur ac7029fa24 gpu/internal/shaders: generate shader variants
We're about to add Direct3D support, where shaders are written in
HLSL. Rather than write shaders twice (or more), convert them to
a GLSL variant understood by the glslcc cross-compiler and generate
the OpenGL ES 2.0 and HLSL variants. The HLSL is used by a future
change.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-02-27 20:34:22 +01:00

362 lines
9.0 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
// +build ignore
package main
import (
"bytes"
"encoding/json"
"fmt"
"go/format"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"text/template"
)
// This program generates shader variants for
// multiple GPU backends (OpenGL ES, Direct3D 11...)
// from a single source.
type shaderArgs struct {
FetchColorExpr string
Header string
}
// InputLocation matches gpu.InputLocation.
type InputLocation struct {
Name string
Location int
Semantic string
SemanticIndex int
Type DataType
Size int
}
type DataType uint8
// UniformLocation matches gpu.UniformLocation.
type UniformLocation struct {
Name string
Type DataType
Size int
Offset int
}
const (
DataTypeFloat DataType = iota
DataTypeShort
)
func main() {
if err := generate(); err != nil {
fmt.Fprintf(os.Stderr, "gpu generate: %v\n", err)
os.Exit(1)
}
}
func generate() error {
tmp, err := ioutil.TempDir("", "gpu-generate")
if err != nil {
return err
}
defer os.RemoveAll(tmp)
glslcc, err := exec.LookPath("glslcc")
if err != nil {
return err
}
fxc, err := exec.LookPath("fxc")
fxcFound := err == nil
shaders, err := filepath.Glob("shaders/*")
if err != nil {
return err
}
var out bytes.Buffer
out.WriteString("// Code generated by build.go. DO NOT EDIT.\n\n")
out.WriteString("package gpu\n\n")
out.WriteString("var (\n")
for _, shader := range shaders {
const nvariants = 2
var variants [nvariants]struct {
gles2 string
hlslSrc string
hlsl []byte
inputs []InputLocation
uniforms []UniformLocation
}
args := [nvariants]shaderArgs{
{
FetchColorExpr: `color.color`,
Header: `layout(binding=0) uniform Color { vec4 color; } color;`,
},
{
FetchColorExpr: `texture(tex, vUV)`,
Header: `layout(binding=0) uniform sampler2D tex;`,
},
}
for i := range args {
gles2, reflect, err := convertShader(tmp, glslcc, shader, "gles", "100", &args[i], false)
if err != nil {
return err
}
// Make the GL ES 2 source compatible with desktop GL 3.
gles2 = "#version 100\n" + gles2
inputs, uniforms, err := parseReflection(reflect)
if err != nil {
return err
}
hlsl, _, err := convertShader(tmp, glslcc, shader, "hlsl", "40", &args[i], false)
if err != nil {
return err
}
var hlslProf string
switch filepath.Ext(shader) {
case ".frag":
hlslProf = "ps"
case ".vert":
hlslProf = "vs"
default:
return fmt.Errorf("unrecognized shader type %s", shader)
}
var hlslc []byte
if fxcFound {
hlslc, err = compileHLSL(tmp, fxc, hlsl, "main", hlslProf+"_4_0")
if err != nil {
return err
}
}
variants[i].gles2 = gles2
variants[i].hlslSrc = hlsl
variants[i].hlsl = hlslc
variants[i].inputs = inputs
variants[i].uniforms = uniforms
}
name := filepath.Base(shader)
name = strings.ReplaceAll(name, ".", "_")
fmt.Fprintf(&out, "\tshader_%s = ", name)
// If the shader don't use the variant arguments, output
// only a single version.
multiVariant := variants[0].gles2 != variants[1].gles2
if multiVariant {
fmt.Fprintf(&out, "[...]ShaderSources{\n")
}
for _, src := range variants {
fmt.Fprintf(&out, "ShaderSources{\n")
if len(src.inputs) > 0 {
fmt.Fprintf(&out, "Inputs: []InputLocation{\n")
for _, inp := range src.inputs {
fmt.Fprintf(&out, "{Name: %q, Location: %d, Semantic: %q, ", inp.Name, inp.Location, inp.Semantic)
fmt.Fprintf(&out, "SemanticIndex: %d, Type: %d, Size: %d},\n", inp.SemanticIndex, inp.Type, inp.Size)
}
fmt.Fprintf(&out, "},\n")
}
if len(src.uniforms) > 0 {
fmt.Fprintf(&out, "Uniforms: []UniformLocation{\n")
for _, u := range src.uniforms {
fmt.Fprintf(&out, "{Name: %q, Type: %d, Size: %d, Offset: %d},\n", u.Name, u.Type, u.Size, u.Offset)
}
fmt.Fprintf(&out, "},\n")
}
fmt.Fprintf(&out, "GLES2: %#v,\n", src.gles2)
fmt.Fprintf(&out, "/*\n%s\n*/\n", src.hlslSrc)
fmt.Fprintf(&out, "HLSL: %#v,\n", src.hlsl)
fmt.Fprintf(&out, "}")
if multiVariant {
fmt.Fprintf(&out, ",")
}
fmt.Fprintf(&out, "\n")
if !multiVariant {
break
}
}
if multiVariant {
fmt.Fprintf(&out, "}\n")
}
}
out.WriteString(")")
gosrc, err := format.Source(out.Bytes())
if err != nil {
return fmt.Errorf("shader.go: %v", err)
}
return ioutil.WriteFile("shaders.go", gosrc, 0644)
}
func parseReflection(jsonData []byte) ([]InputLocation, []UniformLocation, error) {
type InputReflection struct {
ID int `json:"id"`
Name string `json:"name"`
Location int `json:"location"`
Semantic string `json:"semantic"`
SemanticIndex int `json:"semantic_index"`
Type string `json:"type"`
}
type UniformMemberReflection struct {
Name string `json:"name"`
Type string `json:"type"`
Offset int `json:"offset"`
Size int `json:"size"`
}
type UniformBufferReflection struct {
ID int `json:"id"`
Name string `json:"name"`
Set int `json:"set"`
Binding int `json:"binding"`
Size int `json:"block_size"`
Members []UniformMemberReflection `json:"members"`
}
type shaderReflection struct {
Inputs []InputReflection `json:"inputs"`
UniformBuffers []UniformBufferReflection `json:"uniform_buffers"`
}
type shaderMetadata struct {
VS shaderReflection `json:"vs"`
FS shaderReflection `json:"fs"`
}
var reflect shaderMetadata
if err := json.Unmarshal(jsonData, &reflect); err != nil {
return nil, nil, fmt.Errorf("parseReflection: %v", err)
}
var inputs []InputLocation
inputRef := reflect.VS.Inputs
for _, input := range inputRef {
dataType, dataSize, err := parseDataType(input.Type)
if err != nil {
return nil, nil, err
}
inputs = append(inputs, InputLocation{
Name: input.Name,
Location: input.Location,
Semantic: input.Semantic,
SemanticIndex: input.SemanticIndex,
Type: dataType,
Size: dataSize,
})
}
sort.Slice(inputs, func(i, j int) bool {
return inputs[i].Location < inputs[j].Location
})
var ublocks []UniformLocation
shaderBlocks := reflect.VS.UniformBuffers
if len(shaderBlocks) == 0 {
shaderBlocks = reflect.FS.UniformBuffers
}
for _, block := range shaderBlocks {
for _, member := range block.Members {
dataType, size, err := parseDataType(member.Type)
if err != nil {
return nil, nil, err
}
ublocks = append(ublocks, UniformLocation{
// Synthetic name generated by glslcc.
Name: fmt.Sprintf("_%d.%s", block.ID, member.Name),
Type: dataType,
Size: size,
Offset: member.Offset,
})
}
}
return inputs, ublocks, nil
}
func parseDataType(t string) (DataType, int, error) {
switch t {
case "float":
return DataTypeFloat, 1, nil
case "float2":
return DataTypeFloat, 2, nil
case "float3":
return DataTypeFloat, 3, nil
case "float4":
return DataTypeFloat, 4, nil
default:
return 0, 0, fmt.Errorf("unsupported input data type: %s", t)
}
}
func compileHLSL(tmp, fxc, src, entry, profile string) ([]byte, error) {
tmpfile := filepath.Join(tmp, "shader.hlsl")
if err := ioutil.WriteFile(tmpfile, []byte(src), 0644); err != nil {
return nil, err
}
outFile := filepath.Join(tmp, "shader.bin")
cmd := exec.Command(fxc,
"/T", profile,
"/E", entry,
"/nologo",
"/Fo", outFile,
tmpfile,
)
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return nil, err
}
return ioutil.ReadFile(outFile)
}
func convertShader(tmp, glslcc, path, lang, profile string, args *shaderArgs, flattenUBOs bool) (string, []byte, error) {
shaderTmpl, err := template.ParseFiles(path)
if err != nil {
return "", nil, err
}
var buf bytes.Buffer
if err := shaderTmpl.Execute(&buf, args); err != nil {
return "", nil, err
}
tmppath := filepath.Join(tmp, filepath.Base(path))
if err := ioutil.WriteFile(tmppath, buf.Bytes(), 0644); err != nil {
return "", nil, err
}
defer os.Remove(tmppath)
var progFlag string
var progSuffix string
switch filepath.Ext(path) {
case ".vert":
progFlag = "--vert"
progSuffix = "vs"
case ".frag":
progFlag = "--frag"
progSuffix = "fs"
default:
return "", nil, fmt.Errorf("unrecognized shader type: %s", path)
}
cmd := exec.Command(glslcc,
"--silent",
"--optimize",
"--reflect",
"--output", filepath.Join(tmp, "shader"),
"--lang", lang,
"--profile", profile,
progFlag, tmppath,
)
if flattenUBOs {
cmd.Args = append(cmd.Args, "--flatten-ubos")
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", nil, fmt.Errorf("%s: %v", path, err)
}
f, err := os.Open(filepath.Join(tmp, "shader_"+progSuffix))
if err != nil {
return "", nil, err
}
defer f.Close()
defer os.Remove(f.Name())
src, err := ioutil.ReadAll(f)
if err != nil {
return "", nil, err
}
reflect, err := ioutil.ReadFile(filepath.Join(tmp, "shader_"+progSuffix+".json"))
if err != nil {
return "", nil, err
}
return string(src), reflect, nil
}