v / cmd / tools
Raw file | 302 loc (276 sloc) | 9.32 KB | Latest commit hash 017ace6ea
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.
15import os
16import io.util
17import flag
18import net.http
19
20const (
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
29const (
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
65struct Options {
66 show_help bool
67 verbose bool
68 force_update bool
69 slangs []string
70}
71
72struct CompileOptions {
73 verbose bool
74 slangs []string
75 invoke_path string
76}
77
78fn 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.
114fn 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.
125fn 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.
143fn 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.
180fn 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`.
214fn 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.
234fn 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.
259fn 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.
264fn 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}