// Copyright (c) 2021 Lars Pontoppidan. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. // // vshader aids in generating special shader code C headers via sokol-shdc's 'annotated GLSL' format to any // supported target formats that sokol_gfx supports internally. // // vshader bootstraps itself by downloading it's own dependencies to a system cache directory on first run. // // Please see https://github.com/floooh/sokol-tools/blob/master/docs/sokol-shdc.md#feature-overview // for a more in-depth overview of the specific tool in use. // // The shader language used is, as described on the overview page linked above, an 'annotated GLSL' // and 'modern GLSL' (v450) shader language format. import os import io.util import flag import net.http const ( shdc_full_hash = '33d2e4cc26088c6c28eaef5467990f8940d15aab' tool_version = '0.0.1' tool_description = "Compile shaders in sokol's annotated GLSL format to C headers for use with sokol based apps" tool_name = os.file_name(os.executable()) cache_dir = os.join_path(os.cache_dir(), 'v', tool_name) runtime_os = os.user_os() ) const ( supported_hosts = ['linux', 'macos', 'windows'] supported_slangs = [ 'glsl330', // desktop GL 'glsl100', // GLES2 / WebGL 'glsl300es', // GLES3 / WebGL2 'hlsl4', // D3D11 'hlsl5', // D3D11 'metal_macos', // Metal on macOS 'metal_ios', // Metal on iOS device 'metal_sim', // Metal on iOS simulator 'wgpu', // WebGPU ] default_slangs = [ 'glsl330', 'glsl100', 'glsl300es', // 'hlsl4', and hlsl5 can't be used at the same time 'hlsl5', 'metal_macos', 'metal_ios', 'metal_sim', 'wgpu', ] shdc_version = shdc_full_hash[0..8] shdc_urls = { 'windows': 'https://github.com/floooh/sokol-tools-bin/raw/${shdc_full_hash}/bin/win32/sokol-shdc.exe' 'macos': 'https://github.com/floooh/sokol-tools-bin/raw/${shdc_full_hash}/bin/osx/sokol-shdc' 'linux': 'https://github.com/floooh/sokol-tools-bin/raw/${shdc_full_hash}/bin/linux/sokol-shdc' } shdc_version_file = os.join_path(cache_dir, 'sokol-shdc.version') shdc = shdc_exe() shdc_exe_name = 'sokol-shdc.exe' ) struct Options { show_help bool verbose bool force_update bool slangs []string } struct CompileOptions { verbose bool slangs []string invoke_path string } fn main() { if os.args.len == 1 { println('Usage: ${tool_name} PATH \n${tool_description}\n${tool_name} -h for more help...') exit(1) } mut fp := flag.new_flag_parser(os.args[1..]) fp.application(tool_name) fp.version(tool_version) fp.description(tool_description) fp.arguments_description('PATH [PATH]...') fp.skip_executable() // Collect tool options opt := Options{ show_help: fp.bool('help', `h`, false, 'Show this help text.') force_update: fp.bool('force-update', `u`, false, 'Force update of the sokol-shdc tool.') verbose: fp.bool('verbose', `v`, false, 'Be verbose about the tools progress.') slangs: fp.string_multi('slang', `l`, 'Shader dialects to generate code for. Default is all.\n Available dialects: ${supported_slangs}') } if opt.show_help { println(fp.usage()) exit(0) } ensure_external_tools(opt) or { panic(err) } input_paths := fp.finalize() or { panic(err) } for path in input_paths { if os.exists(path) { compile_shaders(opt, path) or { panic(err) } } } } // shader_program_name returns the name of the program from `shader_file`. // shader_program_name returns a blank string if no @program entry could be found. fn shader_program_name(shader_file string) string { shader_program := os.read_lines(shader_file) or { return '' } for line in shader_program { if line.contains('@program ') { return line.all_after('@program ').all_before(' ') } } return '' } // validate_shader_file returns an error if `shader_file` isn't valid. fn validate_shader_file(shader_file string) ! { shader_program := os.read_lines(shader_file) or { return error('shader program at "${shader_file}" could not be opened for reading') } mut has_program_directive := false for line in shader_program { if line.contains('@program ') { has_program_directive = true break } } if !has_program_directive { return error('shader program at "${shader_file}" is missing a "@program" directive.') } } // compile_shaders compiles all `*.glsl` files found in `input_path` // to their C header file representatives. fn compile_shaders(opt Options, input_path string) ! { mut path := os.real_path(input_path) path = path.trim_right('/') if os.is_file(path) { path = os.dir(path) } mut shader_files := []string{} collect(path, mut shader_files) if shader_files.len == 0 { if opt.verbose { eprintln('${tool_name} found no shader files to compile for "${path}"') } return } for shader_file in shader_files { // It could be the user has WIP shader files lying around not used, // so we just report that there's something wrong validate_shader_file(shader_file) or { eprintln(err) continue } co := CompileOptions{ verbose: opt.verbose slangs: opt.slangs invoke_path: path } // Currently sokol-shdc allows for multiple --input flags // - but it's only the last entry that's actually compiled/used // Given this fact - we can only compile one '.glsl' file to one C '.h' header compile_shader(co, shader_file)! } } // compile_shader compiles `shader_file` to a C header file. fn compile_shader(opt CompileOptions, shader_file string) ! { path := opt.invoke_path // The output convetion, for now, is to use the name of the .glsl file mut out_file := os.file_name(shader_file).all_before_last('.') + '.h' out_file = os.join_path(path, out_file) mut slangs := opt.slangs.clone() if opt.slangs.len == 0 { slangs = default_slangs.clone() } header_name := os.file_name(out_file) if opt.verbose { eprintln('${tool_name} generating shader code for ${slangs} in header "${header_name}" in "${path}" from ${shader_file}') } cmd := '${os.quoted_path(shdc)} --input ${os.quoted_path(shader_file)} --output ${os.quoted_path(out_file)} --slang ' + os.quoted_path(slangs.join(':')) if opt.verbose { eprintln('${tool_name} executing:\n${cmd}') } res := os.execute(cmd) if res.exit_code != 0 { eprintln('${tool_name} failed generating shader includes:\n ${res.output}\n ${cmd}') exit(1) } if opt.verbose { program_name := shader_program_name(shader_file) 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') } } // collect recursively collects `.glsl` file entries from `path` in `list`. fn collect(path string, mut list []string) { if !os.is_dir(path) { return } mut files := os.ls(path) or { return } for file in files { p := os.join_path(path, file) if os.is_dir(p) && !os.is_link(p) { collect(p, mut list) } else if os.exists(p) { if os.file_ext(p) == '.glsl' { list << os.real_path(p) } } } return } // ensure_external_tools returns nothing if the external // tools can be setup or is already in place. fn ensure_external_tools(opt Options) ! { if !os.exists(cache_dir) { os.mkdir_all(cache_dir)! } if opt.force_update { download_shdc(opt)! return } is_shdc_available := os.is_file(shdc) is_shdc_executable := os.is_executable(shdc) if is_shdc_available && is_shdc_executable { if opt.verbose { version := os.read_file(shdc_version_file) or { 'unknown' } eprintln('${tool_name} using sokol-shdc version ${version} at "${shdc}"') } return } download_shdc(opt)! } // shdc_exe returns an absolute path to the `sokol-shdc` tool. // Please note that the tool isn't guaranteed to actually be present, nor is // it guaranteed that it can be invoked. fn shdc_exe() string { return os.join_path(cache_dir, shdc_exe_name) } // download_shdc downloads the `sokol-shdc` tool to an OS specific cache directory. fn download_shdc(opt Options) ! { // We want to use the same, runtime, OS type as this tool is invoked on. download_url := shdc_urls[runtime_os] or { '' } if download_url == '' { 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}') } update_to_shdc_version := os.read_file(shdc_version_file) or { shdc_version } file := shdc_exe() if opt.verbose { if shdc_version != update_to_shdc_version && os.exists(file) { eprintln('${tool_name} updating sokol-shdc to version ${update_to_shdc_version} ...') } else { eprintln('${tool_name} installing sokol-shdc version ${update_to_shdc_version} ...') } } if os.exists(file) { os.rm(file)! } mut dtmp_file, dtmp_path := util.temp_file(util.TempFileOptions{ path: os.dir(file) })! dtmp_file.close() if opt.verbose { eprintln('${tool_name} downloading sokol-shdc from ${download_url}') } http.download_file(download_url, dtmp_path) or { os.rm(dtmp_path)! return error('${tool_name} failed to download sokol-shdc needed for shader compiling: ${err}') } // Make it executable os.chmod(dtmp_path, 0o775)! // Move downloaded file in place os.mv(dtmp_path, file)! if runtime_os in ['linux', 'macos'] { // Use the .exe file ending to minimize platform friction. os.mv(file, shdc)! } // Update internal version file os.write_file(shdc_version_file, update_to_shdc_version)! }