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