1 | // Copyright (c) 2021 Lars Pontoppidan. All rights reserved. |
2 | // Use of this source code is governed by an MIT license |
3 | // that can be found in the LICENSE file. |
4 | // |
5 | // vshader aids in generating special shader code C headers via sokol-shdc's 'annotated GLSL' format to any |
6 | // supported target formats that sokol_gfx supports internally. |
7 | // |
8 | // vshader bootstraps itself by downloading it's own dependencies to a system cache directory on first run. |
9 | // |
10 | // Please see https://github.com/floooh/sokol-tools/blob/master/docs/sokol-shdc.md#feature-overview |
11 | // for a more in-depth overview of the specific tool in use. |
12 | // |
13 | // The shader language used is, as described on the overview page linked above, an 'annotated GLSL' |
14 | // and 'modern GLSL' (v450) shader language format. |
15 | import os |
16 | import io.util |
17 | import flag |
18 | import net.http |
19 | |
20 | const ( |
21 | shdc_full_hash = '33d2e4cc26088c6c28eaef5467990f8940d15aab' |
22 | tool_version = '0.0.1' |
23 | tool_description = "Compile shaders in sokol's annotated GLSL format to C headers for use with sokol based apps" |
24 | tool_name = os.file_name(os.executable()) |
25 | cache_dir = os.join_path(os.cache_dir(), 'v', tool_name) |
26 | runtime_os = os.user_os() |
27 | ) |
28 | |
29 | const ( |
30 | supported_hosts = ['linux', 'macos', 'windows'] |
31 | supported_slangs = [ |
32 | 'glsl330', // desktop GL |
33 | 'glsl100', // GLES2 / WebGL |
34 | 'glsl300es', // GLES3 / WebGL2 |
35 | 'hlsl4', // D3D11 |
36 | 'hlsl5', // D3D11 |
37 | 'metal_macos', // Metal on macOS |
38 | 'metal_ios', // Metal on iOS device |
39 | 'metal_sim', // Metal on iOS simulator |
40 | 'wgpu', // WebGPU |
41 | ] |
42 | default_slangs = [ |
43 | 'glsl330', |
44 | 'glsl100', |
45 | 'glsl300es', |
46 | // 'hlsl4', and hlsl5 can't be used at the same time |
47 | 'hlsl5', |
48 | 'metal_macos', |
49 | 'metal_ios', |
50 | 'metal_sim', |
51 | 'wgpu', |
52 | ] |
53 | |
54 | shdc_version = shdc_full_hash[0..8] |
55 | shdc_urls = { |
56 | 'windows': 'https://github.com/floooh/sokol-tools-bin/raw/${shdc_full_hash}/bin/win32/sokol-shdc.exe' |
57 | 'macos': 'https://github.com/floooh/sokol-tools-bin/raw/${shdc_full_hash}/bin/osx/sokol-shdc' |
58 | 'linux': 'https://github.com/floooh/sokol-tools-bin/raw/${shdc_full_hash}/bin/linux/sokol-shdc' |
59 | } |
60 | shdc_version_file = os.join_path(cache_dir, 'sokol-shdc.version') |
61 | shdc = shdc_exe() |
62 | shdc_exe_name = 'sokol-shdc.exe' |
63 | ) |
64 | |
65 | struct Options { |
66 | show_help bool |
67 | verbose bool |
68 | force_update bool |
69 | slangs []string |
70 | } |
71 | |
72 | struct CompileOptions { |
73 | verbose bool |
74 | slangs []string |
75 | invoke_path string |
76 | } |
77 | |
78 | fn main() { |
79 | if os.args.len == 1 { |
80 | println('Usage: ${tool_name} PATH \n${tool_description}\n${tool_name} -h for more help...') |
81 | exit(1) |
82 | } |
83 | mut fp := flag.new_flag_parser(os.args[1..]) |
84 | fp.application(tool_name) |
85 | fp.version(tool_version) |
86 | fp.description(tool_description) |
87 | fp.arguments_description('PATH [PATH]...') |
88 | fp.skip_executable() |
89 | // Collect tool options |
90 | opt := Options{ |
91 | show_help: fp.bool('help', `h`, false, 'Show this help text.') |
92 | force_update: fp.bool('force-update', `u`, false, 'Force update of the sokol-shdc tool.') |
93 | verbose: fp.bool('verbose', `v`, false, 'Be verbose about the tools progress.') |
94 | slangs: fp.string_multi('slang', `l`, 'Shader dialects to generate code for. Default is all.\n Available dialects: ${supported_slangs}') |
95 | } |
96 | if opt.show_help { |
97 | println(fp.usage()) |
98 | exit(0) |
99 | } |
100 | |
101 | ensure_external_tools(opt) or { panic(err) } |
102 | |
103 | input_paths := fp.finalize() or { panic(err) } |
104 | |
105 | for path in input_paths { |
106 | if os.exists(path) { |
107 | compile_shaders(opt, path) or { panic(err) } |
108 | } |
109 | } |
110 | } |
111 | |
112 | // shader_program_name returns the name of the program from `shader_file`. |
113 | // shader_program_name returns a blank string if no @program entry could be found. |
114 | fn shader_program_name(shader_file string) string { |
115 | shader_program := os.read_lines(shader_file) or { return '' } |
116 | for line in shader_program { |
117 | if line.contains('@program ') { |
118 | return line.all_after('@program ').all_before(' ') |
119 | } |
120 | } |
121 | return '' |
122 | } |
123 | |
124 | // validate_shader_file returns an error if `shader_file` isn't valid. |
125 | fn validate_shader_file(shader_file string) ! { |
126 | shader_program := os.read_lines(shader_file) or { |
127 | return error('shader program at "${shader_file}" could not be opened for reading') |
128 | } |
129 | mut has_program_directive := false |
130 | for line in shader_program { |
131 | if line.contains('@program ') { |
132 | has_program_directive = true |
133 | break |
134 | } |
135 | } |
136 | if !has_program_directive { |
137 | return error('shader program at "${shader_file}" is missing a "@program" directive.') |
138 | } |
139 | } |
140 | |
141 | // compile_shaders compiles all `*.glsl` files found in `input_path` |
142 | // to their C header file representatives. |
143 | fn compile_shaders(opt Options, input_path string) ! { |
144 | mut path := os.real_path(input_path) |
145 | path = path.trim_right('/') |
146 | if os.is_file(path) { |
147 | path = os.dir(path) |
148 | } |
149 | |
150 | mut shader_files := []string{} |
151 | collect(path, mut shader_files) |
152 | |
153 | if shader_files.len == 0 { |
154 | if opt.verbose { |
155 | eprintln('${tool_name} found no shader files to compile for "${path}"') |
156 | } |
157 | return |
158 | } |
159 | |
160 | for shader_file in shader_files { |
161 | // It could be the user has WIP shader files lying around not used, |
162 | // so we just report that there's something wrong |
163 | validate_shader_file(shader_file) or { |
164 | eprintln(err) |
165 | continue |
166 | } |
167 | co := CompileOptions{ |
168 | verbose: opt.verbose |
169 | slangs: opt.slangs |
170 | invoke_path: path |
171 | } |
172 | // Currently sokol-shdc allows for multiple --input flags |
173 | // - but it's only the last entry that's actually compiled/used |
174 | // Given this fact - we can only compile one '.glsl' file to one C '.h' header |
175 | compile_shader(co, shader_file)! |
176 | } |
177 | } |
178 | |
179 | // compile_shader compiles `shader_file` to a C header file. |
180 | fn compile_shader(opt CompileOptions, shader_file string) ! { |
181 | path := opt.invoke_path |
182 | // The output convetion, for now, is to use the name of the .glsl file |
183 | mut out_file := os.file_name(shader_file).all_before_last('.') + '.h' |
184 | out_file = os.join_path(path, out_file) |
185 | |
186 | mut slangs := opt.slangs.clone() |
187 | if opt.slangs.len == 0 { |
188 | slangs = default_slangs.clone() |
189 | } |
190 | |
191 | header_name := os.file_name(out_file) |
192 | if opt.verbose { |
193 | eprintln('${tool_name} generating shader code for ${slangs} in header "${header_name}" in "${path}" from ${shader_file}') |
194 | } |
195 | |
196 | cmd := |
197 | '${os.quoted_path(shdc)} --input ${os.quoted_path(shader_file)} --output ${os.quoted_path(out_file)} --slang ' + |
198 | os.quoted_path(slangs.join(':')) |
199 | if opt.verbose { |
200 | eprintln('${tool_name} executing:\n${cmd}') |
201 | } |
202 | res := os.execute(cmd) |
203 | if res.exit_code != 0 { |
204 | eprintln('${tool_name} failed generating shader includes:\n ${res.output}\n ${cmd}') |
205 | exit(1) |
206 | } |
207 | if opt.verbose { |
208 | program_name := shader_program_name(shader_file) |
209 | eprintln('${tool_name} usage example in V:\n\nimport sokol.gfx\n\n#include "${header_name}"\n\nfn C.${program_name}_shader_desc(gfx.Backend) &gfx.ShaderDesc\n') |
210 | } |
211 | } |
212 | |
213 | // collect recursively collects `.glsl` file entries from `path` in `list`. |
214 | fn collect(path string, mut list []string) { |
215 | if !os.is_dir(path) { |
216 | return |
217 | } |
218 | mut files := os.ls(path) or { return } |
219 | for file in files { |
220 | p := os.join_path(path, file) |
221 | if os.is_dir(p) && !os.is_link(p) { |
222 | collect(p, mut list) |
223 | } else if os.exists(p) { |
224 | if os.file_ext(p) == '.glsl' { |
225 | list << os.real_path(p) |
226 | } |
227 | } |
228 | } |
229 | return |
230 | } |
231 | |
232 | // ensure_external_tools returns nothing if the external |
233 | // tools can be setup or is already in place. |
234 | fn ensure_external_tools(opt Options) ! { |
235 | if !os.exists(cache_dir) { |
236 | os.mkdir_all(cache_dir)! |
237 | } |
238 | if opt.force_update { |
239 | download_shdc(opt)! |
240 | return |
241 | } |
242 | |
243 | is_shdc_available := os.is_file(shdc) |
244 | is_shdc_executable := os.is_executable(shdc) |
245 | if is_shdc_available && is_shdc_executable { |
246 | if opt.verbose { |
247 | version := os.read_file(shdc_version_file) or { 'unknown' } |
248 | eprintln('${tool_name} using sokol-shdc version ${version} at "${shdc}"') |
249 | } |
250 | return |
251 | } |
252 | |
253 | download_shdc(opt)! |
254 | } |
255 | |
256 | // shdc_exe returns an absolute path to the `sokol-shdc` tool. |
257 | // Please note that the tool isn't guaranteed to actually be present, nor is |
258 | // it guaranteed that it can be invoked. |
259 | fn shdc_exe() string { |
260 | return os.join_path(cache_dir, shdc_exe_name) |
261 | } |
262 | |
263 | // download_shdc downloads the `sokol-shdc` tool to an OS specific cache directory. |
264 | fn download_shdc(opt Options) ! { |
265 | // We want to use the same, runtime, OS type as this tool is invoked on. |
266 | download_url := shdc_urls[runtime_os] or { '' } |
267 | if download_url == '' { |
268 | return error('${tool_name} failed to download an external dependency "sokol-shdc" for ${runtime_os}.\nThe supported host platforms for shader compilation is ${supported_hosts}') |
269 | } |
270 | update_to_shdc_version := os.read_file(shdc_version_file) or { shdc_version } |
271 | file := shdc_exe() |
272 | if opt.verbose { |
273 | if shdc_version != update_to_shdc_version && os.exists(file) { |
274 | eprintln('${tool_name} updating sokol-shdc to version ${update_to_shdc_version} ...') |
275 | } else { |
276 | eprintln('${tool_name} installing sokol-shdc version ${update_to_shdc_version} ...') |
277 | } |
278 | } |
279 | if os.exists(file) { |
280 | os.rm(file)! |
281 | } |
282 | |
283 | mut dtmp_file, dtmp_path := util.temp_file(util.TempFileOptions{ path: os.dir(file) })! |
284 | dtmp_file.close() |
285 | if opt.verbose { |
286 | eprintln('${tool_name} downloading sokol-shdc from ${download_url}') |
287 | } |
288 | http.download_file(download_url, dtmp_path) or { |
289 | os.rm(dtmp_path)! |
290 | return error('${tool_name} failed to download sokol-shdc needed for shader compiling: ${err}') |
291 | } |
292 | // Make it executable |
293 | os.chmod(dtmp_path, 0o775)! |
294 | // Move downloaded file in place |
295 | os.mv(dtmp_path, file)! |
296 | if runtime_os in ['linux', 'macos'] { |
297 | // Use the .exe file ending to minimize platform friction. |
298 | os.mv(file, shdc)! |
299 | } |
300 | // Update internal version file |
301 | os.write_file(shdc_version_file, update_to_shdc_version)! |
302 | } |