hlsl.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 package main
4
5 import (
6 "bytes"
7 "fmt"
8 "io"
9 "io/ioutil"
10 "os/exec"
11 "path/filepath"
12 "runtime"
13 "strings"
14 )
15
16 // FXC is hlsl compiler that targets ShaderModel 5.x and lower.
17 type FXC struct {
18 Bin string
19 WorkDir WorkDir
20 }
21
22 func NewFXC() *FXC { return &FXC{Bin: "fxc.exe"} }
23
24 // Compile compiles the input shader.
25 func (fxc *FXC) Compile(path, variant string, input []byte, entryPoint string, profileVersion string) (string, error) {
26 base := fxc.WorkDir.Path(filepath.Base(path), variant, profileVersion)
27 pathin := base + ".in"
28 pathout := base + ".out"
29 result := pathout
30
31 if err := fxc.WorkDir.WriteFile(pathin, input); err != nil {
32 return "", fmt.Errorf("unable to write shader to disk: %w", err)
33 }
34
35 cmd := exec.Command(fxc.Bin)
36 if runtime.GOOS != "windows" {
37 cmd = exec.Command("wine", fxc.Bin)
38 if err := winepath(&pathin, &pathout); err != nil {
39 return "", err
40 }
41 }
42
43 var profile string
44 switch filepath.Ext(path) {
45 case ".frag":
46 profile = "ps_" + profileVersion
47 case ".vert":
48 profile = "vs_" + profileVersion
49 case ".comp":
50 profile = "cs_" + profileVersion
51 default:
52 return "", fmt.Errorf("unrecognized shader type %s", path)
53 }
54
55 cmd.Args = append(cmd.Args,
56 "/Fo", pathout,
57 "/T", profile,
58 "/E", entryPoint,
59 pathin,
60 )
61
62 output, err := cmd.CombinedOutput()
63 if err != nil {
64 info := ""
65 if runtime.GOOS != "windows" {
66 info = "If the fxc tool cannot be found, set WINEPATH to the Windows path for the Windows SDK.\n"
67 }
68 return "", fmt.Errorf("%s\n%sfailed to run %v: %w", output, info, cmd.Args, err)
69 }
70
71 compiled, err := ioutil.ReadFile(result)
72 if err != nil {
73 return "", fmt.Errorf("unable to read output %q: %w", pathout, err)
74 }
75
76 return string(compiled), nil
77 }
78
79 // DXC is hlsl compiler that targets ShaderModel 6.0 and newer.
80 type DXC struct {
81 Bin string
82 WorkDir WorkDir
83 }
84
85 func NewDXC() *DXC { return &DXC{Bin: "dxc"} }
86
87 // Compile compiles the input shader.
88 func (dxc *DXC) Compile(path, variant string, input []byte, entryPoint string, profile string) (string, error) {
89 base := dxc.WorkDir.Path(filepath.Base(path), variant, profile)
90 pathin := base + ".in"
91 pathout := base + ".out"
92 result := pathout
93
94 if err := dxc.WorkDir.WriteFile(pathin, input); err != nil {
95 return "", fmt.Errorf("unable to write shader to disk: %w", err)
96 }
97
98 cmd := exec.Command(dxc.Bin)
99
100 cmd.Args = append(cmd.Args,
101 "-Fo", pathout,
102 "-T", profile,
103 "-E", entryPoint,
104 "-Qstrip_reflect",
105 pathin,
106 )
107
108 output, err := cmd.CombinedOutput()
109 if err != nil {
110 return "", fmt.Errorf("%s\nfailed to run %v: %w", output, cmd.Args, err)
111 }
112
113 compiled, err := ioutil.ReadFile(result)
114 if err != nil {
115 return "", fmt.Errorf("unable to read output %q: %w", pathout, err)
116 }
117
118 return string(compiled), nil
119 }
120
121 // winepath uses the winepath tool to convert a paths to Windows format.
122 // The returned path can be used as arguments for Windows command line tools.
123 func winepath(paths ...*string) error {
124 winepath := exec.Command("winepath", "--windows")
125 for _, path := range paths {
126 winepath.Args = append(winepath.Args, *path)
127 }
128 // Use a pipe instead of Output, because winepath may have left wineserver
129 // running for several seconds as a grandchild.
130 out, err := winepath.StdoutPipe()
131 if err != nil {
132 return fmt.Errorf("unable to start winepath: %w", err)
133 }
134 if err := winepath.Start(); err != nil {
135 return fmt.Errorf("unable to start winepath: %w", err)
136 }
137 var buf bytes.Buffer
138 if _, err := io.Copy(&buf, out); err != nil {
139 return fmt.Errorf("unable to run winepath: %w", err)
140 }
141 winPaths := strings.Split(strings.TrimSpace(buf.String()), "\n")
142 for i, path := range paths {
143 *path = winPaths[i]
144 }
145 return nil
146 }
147