main.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 package main
4
5 import (
6 "bytes"
7 "errors"
8 "flag"
9 "fmt"
10 "io"
11 "io/ioutil"
12 "os"
13 "os/exec"
14 "path/filepath"
15 "sort"
16 "strconv"
17 "strings"
18 "sync"
19 "text/template"
20
21 "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver"
22 )
23
24 func main() {
25 packageName := flag.String("package", "", "specify Go package name")
26 workdir := flag.String("work", "", "temporary working directory (default TEMP)")
27 shadersDir := flag.String("dir", "shaders", "shaders directory")
28 directCompute := flag.Bool("directcompute", false, "enable compiling DirectCompute shaders")
29
30 flag.Parse()
31
32 var work WorkDir
33 cleanup := func() {}
34 if *workdir == "" {
35 tempdir, err := ioutil.TempDir("", "shader-convert")
36 if err != nil {
37 fmt.Fprintf(os.Stderr, "failed to create tempdir: %v\n", err)
38 os.Exit(1)
39 }
40 cleanup = func() { os.RemoveAll(tempdir) }
41 defer cleanup()
42
43 work = WorkDir(tempdir)
44 } else {
45 if abs, err := filepath.Abs(*workdir); err == nil {
46 *workdir = abs
47 }
48 work = WorkDir(*workdir)
49 }
50
51 var out bytes.Buffer
52 conv := NewConverter(work, *packageName, *shadersDir, *directCompute)
53 if err := conv.Run(&out); err != nil {
54 fmt.Fprintf(os.Stderr, "%v\n", err)
55 cleanup()
56 os.Exit(1)
57 }
58
59 if err := ioutil.WriteFile("shaders.go", out.Bytes(), 0644); err != nil {
60 fmt.Fprintf(os.Stderr, "failed to create shaders: %v\n", err)
61 cleanup()
62 os.Exit(1)
63 }
64
65 cmd := exec.Command("gofmt", "-s", "-w", "shaders.go")
66 cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
67 if err := cmd.Run(); err != nil {
68 fmt.Fprintf(os.Stderr, "formatting shaders.go failed: %v\n", err)
69 cleanup()
70 os.Exit(1)
71 }
72 }
73
74 type Converter struct {
75 workDir WorkDir
76 shadersDir string
77 directCompute bool
78
79 packageName string
80
81 glslvalidator *GLSLValidator
82 spirv *SPIRVCross
83 fxc *FXC
84 }
85
86 func NewConverter(workDir WorkDir, packageName, shadersDir string, directCompute bool) *Converter {
87 if abs, err := filepath.Abs(shadersDir); err == nil {
88 shadersDir = abs
89 }
90
91 conv := &Converter{}
92 conv.workDir = workDir
93 conv.shadersDir = shadersDir
94 conv.directCompute = directCompute
95
96 conv.packageName = packageName
97
98 conv.glslvalidator = NewGLSLValidator()
99 conv.spirv = NewSPIRVCross()
100 conv.fxc = NewFXC()
101
102 verifyBinaryPath(&conv.glslvalidator.Bin)
103 verifyBinaryPath(&conv.spirv.Bin)
104 // We cannot check fxc since it may depend on wine.
105
106 conv.glslvalidator.WorkDir = workDir.Dir("glslvalidator")
107 conv.fxc.WorkDir = workDir.Dir("fxc")
108 conv.spirv.WorkDir = workDir.Dir("spirv")
109
110 return conv
111 }
112
113 func verifyBinaryPath(bin *string) {
114 new, err := exec.LookPath(*bin)
115 if err != nil {
116 fmt.Fprintf(os.Stderr, "unable to find %q: %v\n", *bin, err)
117 } else {
118 *bin = new
119 }
120 }
121
122 func (conv *Converter) Run(out io.Writer) error {
123 shaders, err := filepath.Glob(filepath.Join(conv.shadersDir, "*"))
124 if len(shaders) == 0 || err != nil {
125 return fmt.Errorf("failed to list shaders in %q: %w", conv.shadersDir, err)
126 }
127
128 sort.Strings(shaders)
129
130 var workers Workers
131
132 type ShaderResult struct {
133 Path string
134 Shaders []driver.ShaderSources
135 Error error
136 }
137 shaderResults := make([]ShaderResult, len(shaders))
138
139 for i, shaderPath := range shaders {
140 i, shaderPath := i, shaderPath
141
142 switch filepath.Ext(shaderPath) {
143 case ".vert", ".frag":
144 workers.Go(func() {
145 shaders, err := conv.Shader(shaderPath)
146 shaderResults[i] = ShaderResult{
147 Path: shaderPath,
148 Shaders: shaders,
149 Error: err,
150 }
151 })
152 case ".comp":
153 workers.Go(func() {
154 shaders, err := conv.ComputeShader(shaderPath)
155 shaderResults[i] = ShaderResult{
156 Path: shaderPath,
157 Shaders: shaders,
158 Error: err,
159 }
160 })
161 default:
162 continue
163 }
164 }
165
166 workers.Wait()
167
168 var allErrors string
169 for _, r := range shaderResults {
170 if r.Error != nil {
171 if len(allErrors) > 0 {
172 allErrors += "\n\n"
173 }
174 allErrors += "--- " + r.Path + " --- \n\n" + r.Error.Error() + "\n"
175 }
176 }
177 if len(allErrors) > 0 {
178 return errors.New(allErrors)
179 }
180
181 fmt.Fprintf(out, "// Code generated by build.go. DO NOT EDIT.\n\n")
182 fmt.Fprintf(out, "package %s\n\n", conv.packageName)
183 fmt.Fprintf(out, "import %q\n\n", "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver")
184
185 fmt.Fprintf(out, "var (\n")
186
187 for _, r := range shaderResults {
188 if len(r.Shaders) == 0 {
189 continue
190 }
191
192 name := filepath.Base(r.Path)
193 name = strings.ReplaceAll(name, ".", "_")
194 fmt.Fprintf(out, "\tshader_%s = ", name)
195
196 multiVariant := len(r.Shaders) > 1
197 if multiVariant {
198 fmt.Fprintf(out, "[...]driver.ShaderSources{\n")
199 }
200
201 for _, src := range r.Shaders {
202 fmt.Fprintf(out, "driver.ShaderSources{\n")
203 fmt.Fprintf(out, "Name: %#v,\n", src.Name)
204 if len(src.Inputs) > 0 {
205 fmt.Fprintf(out, "Inputs: %#v,\n", src.Inputs)
206 }
207 if u := src.Uniforms; len(u.Blocks) > 0 {
208 fmt.Fprintf(out, "Uniforms: driver.UniformsReflection{\n")
209 fmt.Fprintf(out, "Blocks: %#v,\n", u.Blocks)
210 fmt.Fprintf(out, "Locations: %#v,\n", u.Locations)
211 fmt.Fprintf(out, "Size: %d,\n", u.Size)
212 fmt.Fprintf(out, "},\n")
213 }
214 if len(src.Textures) > 0 {
215 fmt.Fprintf(out, "Textures: %#v,\n", src.Textures)
216 }
217 if len(src.GLSL100ES) > 0 {
218 fmt.Fprintf(out, "GLSL100ES: `%s`,\n", src.GLSL100ES)
219 }
220 if len(src.GLSL300ES) > 0 {
221 fmt.Fprintf(out, "GLSL300ES: `%s`,\n", src.GLSL300ES)
222 }
223 if len(src.GLSL310ES) > 0 {
224 fmt.Fprintf(out, "GLSL310ES: `%s`,\n", src.GLSL310ES)
225 }
226 if len(src.GLSL130) > 0 {
227 fmt.Fprintf(out, "GLSL130: `%s`,\n", src.GLSL130)
228 }
229 if len(src.GLSL150) > 0 {
230 fmt.Fprintf(out, "GLSL150: `%s`,\n", src.GLSL150)
231 }
232 if len(src.HLSL) > 0 {
233 fmt.Fprintf(out, "HLSL: %q,\n", src.HLSL)
234 }
235 fmt.Fprintf(out, "}")
236 if multiVariant {
237 fmt.Fprintf(out, ",")
238 }
239 fmt.Fprintf(out, "\n")
240 }
241 if multiVariant {
242 fmt.Fprintf(out, "}\n")
243 }
244 }
245 fmt.Fprintf(out, ")\n")
246
247 return nil
248 }
249
250 func (conv *Converter) Shader(shaderPath string) ([]driver.ShaderSources, error) {
251 type Variant struct {
252 FetchColorExpr string
253 Header string
254 }
255 variantArgs := [...]Variant{
256 {
257 FetchColorExpr: `_color.color`,
258 Header: `layout(binding=0) uniform Color { vec4 color; } _color;`,
259 },
260 {
261 FetchColorExpr: `mix(_gradient.color1, _gradient.color2, clamp(vUV.x, 0.0, 1.0))`,
262 Header: `layout(binding=0) uniform Gradient { vec4 color1; vec4 color2; } _gradient;`,
263 },
264 {
265 FetchColorExpr: `texture(tex, vUV)`,
266 Header: `layout(binding=0) uniform sampler2D tex;`,
267 },
268 }
269
270 shaderTemplate, err := template.ParseFiles(shaderPath)
271 if err != nil {
272 return nil, fmt.Errorf("failed to parse template %q: %w", shaderPath, err)
273 }
274
275 var variants []driver.ShaderSources
276 for i, variantArg := range variantArgs {
277 variantName := strconv.Itoa(i)
278 var buf bytes.Buffer
279 err := shaderTemplate.Execute(&buf, variantArg)
280 if err != nil {
281 return nil, fmt.Errorf("failed to execute template %q with %#v: %w", shaderPath, variantArg, err)
282 }
283
284 var sources driver.ShaderSources
285 sources.Name = filepath.Base(shaderPath)
286
287 // Ignore error; some shaders are not meant to run in GLSL 1.00.
288 sources.GLSL100ES, _, _ = conv.ShaderVariant(shaderPath, variantName, buf.Bytes(), "es", "100")
289
290 var metadata Metadata
291 sources.GLSL300ES, metadata, err = conv.ShaderVariant(shaderPath, variantName, buf.Bytes(), "es", "300")
292 if err != nil {
293 return nil, fmt.Errorf("failed to convert GLSL300ES:\n%w", err)
294 }
295
296 sources.GLSL130, _, err = conv.ShaderVariant(shaderPath, variantName, buf.Bytes(), "glsl", "130")
297 if err != nil {
298 return nil, fmt.Errorf("failed to convert GLSL130:\n%w", err)
299 }
300
301 hlsl, _, err := conv.ShaderVariant(shaderPath, variantName, buf.Bytes(), "hlsl", "40")
302 if err != nil {
303 return nil, fmt.Errorf("failed to convert HLSL:\n%w", err)
304 }
305 sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName, []byte(hlsl), "main", "4_0_level_9_1")
306 if err != nil {
307 // Attempt shader model 4.0. Only the gpu/headless
308 // test shaders use features not supported by level
309 // 9.1.
310 sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName, []byte(hlsl), "main", "4_0")
311 if err != nil {
312 return nil, fmt.Errorf("failed to compile HLSL: %w", err)
313 }
314 }
315
316 sources.GLSL150, _, err = conv.ShaderVariant(shaderPath, variantName, buf.Bytes(), "glsl", "150")
317 if err != nil {
318 return nil, fmt.Errorf("failed to convert GLSL150:\n%w", err)
319 }
320
321 sources.Uniforms = metadata.Uniforms
322 sources.Inputs = metadata.Inputs
323 sources.Textures = metadata.Textures
324
325 variants = append(variants, sources)
326 }
327
328 // If the shader don't use the variant arguments, output only a single version.
329 if variants[0].GLSL100ES == variants[1].GLSL100ES {
330 variants = variants[:1]
331 }
332
333 return variants, nil
334 }
335
336 func (conv *Converter) ShaderVariant(shaderPath, variant string, src []byte, lang, profile string) (string, Metadata, error) {
337 spirv, err := conv.glslvalidator.Convert(shaderPath, variant, lang == "hlsl", src)
338 if err != nil {
339 return "", Metadata{}, fmt.Errorf("failed to generate SPIR-V for %q: %w", shaderPath, err)
340 }
341
342 dst, err := conv.spirv.Convert(shaderPath, variant, spirv, lang, profile)
343 if err != nil {
344 return "", Metadata{}, fmt.Errorf("failed to convert shader %q: %w", shaderPath, err)
345 }
346
347 meta, err := conv.spirv.Metadata(shaderPath, variant, spirv)
348 if err != nil {
349 return "", Metadata{}, fmt.Errorf("failed to extract metadata for shader %q: %w", shaderPath, err)
350 }
351
352 return dst, meta, nil
353 }
354
355 func (conv *Converter) ComputeShader(shaderPath string) ([]driver.ShaderSources, error) {
356 shader, err := ioutil.ReadFile(shaderPath)
357 if err != nil {
358 return nil, fmt.Errorf("failed to load shader %q: %w", shaderPath, err)
359 }
360
361 spirv, err := conv.glslvalidator.Convert(shaderPath, "", false, shader)
362 if err != nil {
363 return nil, fmt.Errorf("failed to convert compute shader %q: %w", shaderPath, err)
364 }
365
366 var sources driver.ShaderSources
367 sources.Name = filepath.Base(shaderPath)
368
369 sources.GLSL310ES, err = conv.spirv.Convert(shaderPath, "", spirv, "es", "310")
370 if err != nil {
371 return nil, fmt.Errorf("failed to convert es compute shader %q: %w", shaderPath, err)
372 }
373 sources.GLSL310ES = unixLineEnding(sources.GLSL310ES)
374
375 hlslSource, err := conv.spirv.Convert(shaderPath, "", spirv, "hlsl", "50")
376 if err != nil {
377 return nil, fmt.Errorf("failed to convert hlsl compute shader %q: %w", shaderPath, err)
378 }
379
380 dxil, err := conv.fxc.Compile(shaderPath, "0", []byte(hlslSource), "main", "5_0")
381 if err != nil {
382 return nil, fmt.Errorf("failed to compile hlsl compute shader %q: %w", shaderPath, err)
383 }
384 if conv.directCompute {
385 sources.HLSL = dxil
386 }
387
388 return []driver.ShaderSources{sources}, nil
389 }
390
391 // Workers implements wait group with synchronous logging.
392 type Workers struct {
393 running sync.WaitGroup
394 }
395
396 func (lg *Workers) Go(fn func()) {
397 lg.running.Add(1)
398 go func() {
399 defer lg.running.Done()
400 fn()
401 }()
402 }
403
404 func (lg *Workers) Wait() {
405 lg.running.Wait()
406 }
407
408 func unixLineEnding(s string) string {
409 return strings.ReplaceAll(s, "\r\n", "\n")
410 }
411