mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-02 07:57:29 +00:00
2c7aba9e7c
The convert program is only used by the shaders from package gpu, and we're about to make the backend package imported by the program internal to package gpu. Move the converter below package gpu. Signed-off-by: Elias Naur <mail@eliasnaur.com>
399 lines
10 KiB
Go
399 lines
10 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"text/template"
|
|
|
|
"gioui.org/gpu/backend"
|
|
)
|
|
|
|
func main() {
|
|
packageName := flag.String("package", "", "specify Go package name")
|
|
workdir := flag.String("work", "", "temporary working directory (default TEMP)")
|
|
shadersDir := flag.String("dir", "shaders", "shaders directory")
|
|
directCompute := flag.Bool("directcompute", false, "enable compiling DirectCompute shaders")
|
|
|
|
flag.Parse()
|
|
|
|
var work WorkDir
|
|
cleanup := func() {}
|
|
if *workdir == "" {
|
|
tempdir, err := ioutil.TempDir("", "shader-convert")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "failed to create tempdir: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
cleanup = func() { os.RemoveAll(tempdir) }
|
|
defer cleanup()
|
|
|
|
work = WorkDir(tempdir)
|
|
} else {
|
|
if abs, err := filepath.Abs(*workdir); err == nil {
|
|
*workdir = abs
|
|
}
|
|
work = WorkDir(*workdir)
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
conv := NewConverter(work, *packageName, *shadersDir, *directCompute)
|
|
if err := conv.Run(&out); err != nil {
|
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
|
cleanup()
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err := ioutil.WriteFile("shaders.go", out.Bytes(), 0644); err != nil {
|
|
fmt.Fprintf(os.Stderr, "failed to create shaders: %v\n", err)
|
|
cleanup()
|
|
os.Exit(1)
|
|
}
|
|
|
|
cmd := exec.Command("gofmt", "-s", "-w", "shaders.go")
|
|
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "formatting shaders.go failed: %v\n", err)
|
|
cleanup()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
type Converter struct {
|
|
workDir WorkDir
|
|
shadersDir string
|
|
directCompute bool
|
|
|
|
packageName string
|
|
|
|
glslcc *GLSLCC
|
|
glslvalidator *GLSLValidator
|
|
spirv *SPIRVCross
|
|
fxc *FXC
|
|
dxc *DXC
|
|
}
|
|
|
|
func NewConverter(workDir WorkDir, packageName, shadersDir string, directCompute bool) *Converter {
|
|
if abs, err := filepath.Abs(shadersDir); err == nil {
|
|
shadersDir = abs
|
|
}
|
|
|
|
conv := &Converter{}
|
|
conv.workDir = workDir
|
|
conv.shadersDir = shadersDir
|
|
conv.directCompute = directCompute
|
|
|
|
conv.packageName = packageName
|
|
|
|
conv.glslcc = NewGLSLCC()
|
|
conv.glslvalidator = NewGLSLValidator()
|
|
conv.spirv = NewSPIRVCross()
|
|
conv.fxc = NewFXC()
|
|
conv.dxc = NewDXC()
|
|
|
|
verifyBinaryPath(&conv.glslcc.Bin)
|
|
verifyBinaryPath(&conv.glslvalidator.Bin)
|
|
verifyBinaryPath(&conv.spirv.Bin)
|
|
// we cannot check fxc/dxc, since they may depend on wine
|
|
|
|
conv.glslcc.IncludeDir = shadersDir
|
|
conv.glslcc.WorkDir = workDir.Dir("glslcc")
|
|
conv.glslvalidator.WorkDir = workDir.Dir("glslvalidator")
|
|
conv.fxc.WorkDir = workDir.Dir("fxc")
|
|
conv.dxc.WorkDir = workDir.Dir("dxc")
|
|
|
|
return conv
|
|
}
|
|
|
|
func verifyBinaryPath(bin *string) {
|
|
new, err := exec.LookPath(*bin)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "unable to find %q: %v\n", *bin, err)
|
|
} else {
|
|
*bin = new
|
|
}
|
|
}
|
|
|
|
func (conv *Converter) Run(out io.Writer) error {
|
|
shaders, err := filepath.Glob(filepath.Join(conv.shadersDir, "*"))
|
|
if len(shaders) == 0 || err != nil {
|
|
return fmt.Errorf("failed to list shaders in %q: %w", conv.shadersDir, err)
|
|
}
|
|
|
|
sort.Strings(shaders)
|
|
|
|
var workers Workers
|
|
|
|
type ShaderResult struct {
|
|
Path string
|
|
Shaders []backend.ShaderSources
|
|
Error error
|
|
}
|
|
shaderResults := make([]ShaderResult, len(shaders))
|
|
|
|
for i, shaderPath := range shaders {
|
|
i, shaderPath := i, shaderPath
|
|
|
|
switch filepath.Ext(shaderPath) {
|
|
case ".vert", ".frag":
|
|
workers.Go(func() {
|
|
shaders, err := conv.Shader(shaderPath)
|
|
shaderResults[i] = ShaderResult{
|
|
Path: shaderPath,
|
|
Shaders: shaders,
|
|
Error: err,
|
|
}
|
|
})
|
|
case ".comp":
|
|
workers.Go(func() {
|
|
shaders, err := conv.ComputeShader(shaderPath)
|
|
shaderResults[i] = ShaderResult{
|
|
Path: shaderPath,
|
|
Shaders: shaders,
|
|
Error: err,
|
|
}
|
|
})
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
|
|
workers.Wait()
|
|
|
|
var allErrors string
|
|
for _, r := range shaderResults {
|
|
if r.Error != nil {
|
|
if len(allErrors) > 0 {
|
|
allErrors += "\n\n"
|
|
}
|
|
allErrors += "--- " + r.Path + " --- \n\n" + r.Error.Error() + "\n"
|
|
}
|
|
}
|
|
if len(allErrors) > 0 {
|
|
return errors.New(allErrors)
|
|
}
|
|
|
|
fmt.Fprintf(out, "// Code generated by build.go. DO NOT EDIT.\n\n")
|
|
fmt.Fprintf(out, "package %s\n\n", conv.packageName)
|
|
fmt.Fprintf(out, "import %q\n\n", "gioui.org/gpu/backend")
|
|
|
|
fmt.Fprintf(out, "var (\n")
|
|
|
|
for _, r := range shaderResults {
|
|
if len(r.Shaders) == 0 {
|
|
continue
|
|
}
|
|
|
|
name := filepath.Base(r.Path)
|
|
name = strings.ReplaceAll(name, ".", "_")
|
|
fmt.Fprintf(out, "\tshader_%s = ", name)
|
|
|
|
multiVariant := len(r.Shaders) > 1
|
|
if multiVariant {
|
|
fmt.Fprintf(out, "[...]backend.ShaderSources{\n")
|
|
}
|
|
|
|
for _, src := range r.Shaders {
|
|
fmt.Fprintf(out, "backend.ShaderSources{\n")
|
|
fmt.Fprintf(out, "Name: %#v,\n", src.Name)
|
|
if len(src.Inputs) > 0 {
|
|
fmt.Fprintf(out, "Inputs: %#v,\n", src.Inputs)
|
|
}
|
|
if u := src.Uniforms; len(u.Blocks) > 0 {
|
|
fmt.Fprintf(out, "Uniforms: backend.UniformsReflection{\n")
|
|
fmt.Fprintf(out, "Blocks: %#v,\n", u.Blocks)
|
|
fmt.Fprintf(out, "Locations: %#v,\n", u.Locations)
|
|
fmt.Fprintf(out, "Size: %d,\n", u.Size)
|
|
fmt.Fprintf(out, "},\n")
|
|
}
|
|
if len(src.Textures) > 0 {
|
|
fmt.Fprintf(out, "Textures: %#v,\n", src.Textures)
|
|
}
|
|
if len(src.GLSL100ES) > 0 {
|
|
fmt.Fprintf(out, "GLSL100ES: %#v,\n", src.GLSL100ES)
|
|
}
|
|
if len(src.GLSL300ES) > 0 {
|
|
fmt.Fprintf(out, "GLSL300ES: %#v,\n", src.GLSL300ES)
|
|
}
|
|
if len(src.GLSL310ES) > 0 {
|
|
fmt.Fprintf(out, "GLSL310ES: %#v,\n", src.GLSL310ES)
|
|
}
|
|
if len(src.GLSL130) > 0 {
|
|
fmt.Fprintf(out, "GLSL130: %#v,\n", src.GLSL130)
|
|
}
|
|
if len(src.GLSL150) > 0 {
|
|
fmt.Fprintf(out, "GLSL150: %#v,\n", src.GLSL150)
|
|
}
|
|
if len(src.HLSL) > 0 {
|
|
fmt.Fprintf(out, "HLSL: %#v,\n", src.HLSL)
|
|
}
|
|
fmt.Fprintf(out, "}")
|
|
if multiVariant {
|
|
fmt.Fprintf(out, ",")
|
|
}
|
|
fmt.Fprintf(out, "\n")
|
|
}
|
|
if multiVariant {
|
|
fmt.Fprintf(out, "}\n")
|
|
}
|
|
}
|
|
fmt.Fprintf(out, ")\n")
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conv *Converter) Shader(shaderPath string) ([]backend.ShaderSources, error) {
|
|
type Variant struct {
|
|
FetchColorExpr string
|
|
Header string
|
|
}
|
|
variantArgs := [...]Variant{
|
|
{
|
|
FetchColorExpr: `_color`,
|
|
Header: `layout(binding=0) uniform Color { vec4 _color; };`,
|
|
},
|
|
{
|
|
FetchColorExpr: `mix(_color1, _color2, clamp(vUV.x, 0.0, 1.0))`,
|
|
Header: `layout(binding=0) uniform Gradient { vec4 _color1; vec4 _color2; };`,
|
|
},
|
|
{
|
|
FetchColorExpr: `texture(tex, vUV)`,
|
|
Header: `layout(binding=0) uniform sampler2D tex;`,
|
|
},
|
|
}
|
|
|
|
shaderTemplate, err := template.ParseFiles(shaderPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse template %q: %w", shaderPath, err)
|
|
}
|
|
|
|
var variants []backend.ShaderSources
|
|
for i, variantArg := range variantArgs {
|
|
variantName := strconv.Itoa(i)
|
|
var buf bytes.Buffer
|
|
err := shaderTemplate.Execute(&buf, variantArg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute template %q with %#v: %w", shaderPath, variantArg, err)
|
|
}
|
|
|
|
var sources backend.ShaderSources
|
|
sources.Name = filepath.Base(shaderPath)
|
|
|
|
// Ignore error; some shaders are not meant to run in GLSL 1.00.
|
|
sources.GLSL100ES, _, _ = conv.glslcc.Convert(shaderPath, variantName, buf.Bytes(), "gles", "100")
|
|
|
|
var metadata Metadata
|
|
sources.GLSL300ES, metadata, err = conv.glslcc.Convert(shaderPath, variantName, buf.Bytes(), "gles", "300")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert GLSL300ES:\n%w", err)
|
|
}
|
|
|
|
sources.GLSL130, _, err = conv.glslcc.Convert(shaderPath, variantName, buf.Bytes(), "glsl", "130")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert GLSL130:\n%w", err)
|
|
}
|
|
|
|
hlsl, _, err := conv.glslcc.Convert(shaderPath, variantName, buf.Bytes(), "hlsl", "40")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert HLSL:\n%w", err)
|
|
}
|
|
|
|
sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName, []byte(hlsl), "main", "4_0_level_9_1")
|
|
if err != nil {
|
|
// Attempt shader model 4.0. Only the app/headless
|
|
// test shaders use features not supported by level
|
|
// 9.1.
|
|
sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName, []byte(hlsl), "main", "4_0")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to compile HLSL: %w", err)
|
|
}
|
|
}
|
|
|
|
// OpenGL 3.2 Core only accepts GLSL version 1.50, but is
|
|
// otherwise compatible with version 1.30.
|
|
sources.GLSL150 = strings.Replace(sources.GLSL130, "#version 130", "#version 150", 1)
|
|
|
|
sources.Uniforms = metadata.Uniforms
|
|
sources.Inputs = metadata.Inputs
|
|
sources.Textures = metadata.Textures
|
|
sources.StorageBuffers = metadata.StorageBuffers
|
|
|
|
variants = append(variants, sources)
|
|
}
|
|
|
|
// If the shader don't use the variant arguments, output only a single version.
|
|
if variants[0].GLSL100ES == variants[1].GLSL100ES {
|
|
variants = variants[:1]
|
|
}
|
|
|
|
return variants, nil
|
|
}
|
|
|
|
func (conv *Converter) ComputeShader(shaderPath string) ([]backend.ShaderSources, error) {
|
|
shader, err := ioutil.ReadFile(shaderPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load shader %q: %w", shaderPath, err)
|
|
}
|
|
|
|
spirv, err := conv.glslvalidator.ConvertCompute(shaderPath, shader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert compute shader %q: %w", shaderPath, err)
|
|
}
|
|
|
|
var sources backend.ShaderSources
|
|
sources.Name = filepath.Base(shaderPath)
|
|
|
|
sources.GLSL310ES, err = conv.spirv.Convert(spirv, "es", "310")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert es compute shader %q: %w", shaderPath, err)
|
|
}
|
|
sources.GLSL310ES = unixLineEnding(sources.GLSL310ES)
|
|
|
|
if conv.directCompute {
|
|
hlslSource, err := conv.spirv.Convert(spirv, "hlsl", "50")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert hlsl compute shader %q: %w", shaderPath, err)
|
|
}
|
|
|
|
sources.HLSL, err = conv.dxc.Compile(shaderPath, "0", []byte(hlslSource), "main", "cs_5_0")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to compile hlsl compute shader %q: %w", shaderPath, err)
|
|
}
|
|
}
|
|
|
|
return []backend.ShaderSources{sources}, nil
|
|
}
|
|
|
|
// Workers implements wait group with synchronous logging.
|
|
type Workers struct {
|
|
running sync.WaitGroup
|
|
}
|
|
|
|
func (lg *Workers) Go(fn func()) {
|
|
lg.running.Add(1)
|
|
go func() {
|
|
defer lg.running.Done()
|
|
fn()
|
|
}()
|
|
}
|
|
|
|
func (lg *Workers) Wait() {
|
|
lg.running.Wait()
|
|
}
|
|
|
|
func unixLineEnding(s string) string {
|
|
return strings.ReplaceAll(s, "\r\n", "\n")
|
|
}
|