diff --git a/src/cli.c b/src/cli.c index 493c1e0..4095ec9 100644 --- a/src/cli.c +++ b/src/cli.c @@ -22,6 +22,7 @@ #include #include #include +#include // We can emit HLSL as a destination, so let's redefine the shader format enum. typedef enum ShaderCross_DestinationFormat { @@ -30,34 +31,45 @@ typedef enum ShaderCross_DestinationFormat { SHADERFORMAT_DXBC, SHADERFORMAT_DXIL, SHADERFORMAT_MSL, - SHADERFORMAT_HLSL + SHADERFORMAT_METALLIB, + SHADERFORMAT_HLSL, } ShaderCross_ShaderFormat; +typedef enum ShaderCross_Platform { + PLATFORM_METAL_MACOS, + PLATFORM_METAL_IOS, +} ShaderCross_Platform; + +bool check_for_metal_tools(void); +int compile_metallib(ShaderCross_Platform platform, const char *outputFilename); + void print_help(void) { int column_width = 32; SDL_Log("Usage: shadercross [options]"); SDL_Log("Required options:\n"); SDL_Log(" %-*s %s", column_width, "-s | --source ", "Source language format. May be inferred from the filename. Values: [SPIRV, HLSL]"); - SDL_Log(" %-*s %s", column_width, "-d | --dest ", "Destination format. May be inferred from the filename. Values: [DXBC, DXIL, MSL, SPIRV, HLSL]"); + SDL_Log(" %-*s %s", column_width, "-d | --dest ", "Destination format. May be inferred from the filename. Values: [SPIRV, DXBC, DXIL, MSL, METALLIB, HLSL]"); SDL_Log(" %-*s %s", column_width, "-t | --stage ", "Shader stage. May be inferred from the filename. Values: [vertex, fragment, compute]"); SDL_Log(" %-*s %s", column_width, "-e | --entrypoint ", "Entrypoint function name. Default: \"main\"."); SDL_Log(" %-*s %s", column_width, "-m | --shadermodel ", "HLSL Shader Model. Only used with HLSL destination. Values: [5.0, 6.0]"); + SDL_Log(" %-*s %s", column_width, "-p | --platform ", "Target platform. Only used with METALLIB destination. Values: [macOS, iOS]"); SDL_Log(" %-*s %s", column_width, "-o | --output ", "Output file."); } int main(int argc, char *argv[]) { - bool sourceValid = false; bool destinationValid = false; bool stageValid = false; bool shaderModelValid = false; // only required for HLSL destination + bool platformValid = false; // only required for METALLIB destination bool spirvSource = false; ShaderCross_ShaderFormat destinationFormat = SHADERFORMAT_INVALID; SDL_ShaderCross_ShaderStage shaderStage = SDL_SHADERCROSS_SHADERSTAGE_VERTEX; SDL_ShaderCross_ShaderModel shaderModel; + ShaderCross_Platform platform; char *outputFilename = NULL; char *entrypointName = "main"; @@ -113,8 +125,11 @@ int main(int argc, char *argv[]) } else if (SDL_strcasecmp(argv[i], "HLSL") == 0) { destinationFormat = SHADERFORMAT_HLSL; destinationValid = true; + } else if (SDL_strcasecmp(argv[i], "METALLIB") == 0) { + destinationFormat = SHADERFORMAT_METALLIB; + destinationValid = true; } else { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unrecognized destination input %s, destination must be DXBC, DXIL, MSL or SPIRV!", argv[i]); + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unrecognized destination input %s, destination must be SPIRV, DXBC, DXIL, MSL, METALLIB, or HLSL!", argv[i]); print_help(); return 1; } @@ -165,6 +180,24 @@ int main(int argc, char *argv[]) print_help(); return 1; } + } else if (SDL_strcmp(arg, "-p") == 0 || SDL_strcmp(arg, "--platform") == 0) { + if (i + 1 >= argc) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s requires an argument", arg); + print_help(); + return 1; + } + i += 1; + if (SDL_strcasecmp(argv[i], "macOS") == 0) { + platform = PLATFORM_METAL_MACOS; + platformValid = true; + } else if (SDL_strcasecmp(argv[i], "iOS") == 0) { + platform = PLATFORM_METAL_IOS; + platformValid = true; + } else { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s is not a recognized platform!", argv[i]); + print_help(); + return 1; + } } else if (SDL_strcmp(arg, "-o") == 0 || SDL_strcmp(arg, "--output") == 0) { if (i + 1 >= argc) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s requires an argument", arg); @@ -229,6 +262,8 @@ int main(int argc, char *argv[]) destinationFormat = SHADERFORMAT_DXIL; } else if (SDL_strstr(outputFilename, ".msl")) { destinationFormat = SHADERFORMAT_MSL; + } else if (SDL_strstr(outputFilename, ".metallib")) { + destinationFormat = SHADERFORMAT_METALLIB; } else if (SDL_strstr(outputFilename, ".spv")) { destinationFormat = SHADERFORMAT_SPIRV; } else if (SDL_strstr(outputFilename, ".hlsl")) { @@ -255,7 +290,6 @@ int main(int argc, char *argv[]) } SDL_IOStream *outputIO = SDL_IOFromFile(outputFilename, "w"); - if (outputIO == NULL) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s", SDL_GetError()); return 1; @@ -300,6 +334,42 @@ int main(int argc, char *argv[]) break; } + case SHADERFORMAT_METALLIB: { + if (!platformValid) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s", "METALLIB destination requires a target platform!"); + print_help(); + return 1; + } + + if (!check_for_metal_tools()) + { + return 1; + } + + // We won't generate the metallib file directly... + SDL_CloseIO(outputIO); + outputIO = NULL; + + // ...instead we'll send the MSL to a temp file and then compile that. + SDL_IOStream *tempFileIO = SDL_IOFromFile("tmp.metal", "w"); + char *buffer = SDL_ShaderCross_TranspileMSLFromSPIRV( + fileData, + fileSize, + entrypointName, + shaderStage); + SDL_IOprintf(tempFileIO, "%s", buffer); + SDL_free(buffer); + SDL_CloseIO(tempFileIO); + + int exitcode = compile_metallib(platform, outputFilename); + if (exitcode != 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Metal library creation failed with error code %d", exitcode); + return 1; + } + + break; + } + case SHADERFORMAT_HLSL: { if (!shaderModelValid) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s", "HLSL destination requires a shader model specification!"); @@ -373,6 +443,53 @@ int main(int argc, char *argv[]) break; } + case SHADERFORMAT_METALLIB: { + if (!platformValid) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s", "METALLIB destination requires a target platform!"); + print_help(); + return 1; + } + + if (!check_for_metal_tools()) + { + return 1; + } + + void *spirv = SDL_ShaderCross_CompileSPIRVFromHLSL( + fileData, + entrypointName, + shaderStage, + &bytecodeSize); + if (spirv == NULL) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s", "Failed to compile SPIR-V!"); + return 1; + } + + // We won't generate the metallib file directly... + SDL_CloseIO(outputIO); + outputIO = NULL; + + // ...instead we'll send the MSL to a temp file and then compile that. + SDL_IOStream *tempFileIO = SDL_IOFromFile("tmp.metal", "w"); + char *buffer = SDL_ShaderCross_TranspileMSLFromSPIRV( + spirv, + bytecodeSize, + entrypointName, + shaderStage); + SDL_IOprintf(tempFileIO, "%s", buffer); + SDL_free(spirv); + SDL_free(buffer); + SDL_CloseIO(tempFileIO); + + int exitcode = compile_metallib(platform, outputFilename); + if (exitcode != 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Metal library creation failed with error code %d", exitcode); + return 1; + } + + break; + } + case SHADERFORMAT_SPIRV: { Uint8 *buffer = SDL_ShaderCross_CompileSPIRVFromHLSL( fileData, @@ -427,3 +544,77 @@ int main(int argc, char *argv[]) SDL_ShaderCross_Quit(); return 0; } + +bool check_for_metal_tools(void) +{ +#if defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_WINDOWS) + +#if defined(SDL_PLATFORM_MACOS) + char *compilerName = "xcrun"; + char *cantFindMessage = "Install Xcode or the Xcode Command Line Tools."; +#else + char *compilerName = "metal"; + char *cantFindMessage = "Install Metal Developer Tools for Windows 5.0 beta 2 or newer (https://developer.apple.com/download/all/?q=metal%20developer%20tools%20for%20windows) and add \"C:\\Program Files\\Metal Developer Tools\\bin\" to your PATH."; +#endif + + // Check for the Metal Developer Tools... + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, (char*[]){ compilerName, "--help", NULL }); + SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_NULL); + SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_NULL); + SDL_Process *process = SDL_CreateProcessWithProperties(props); + SDL_DestroyProperties(props); + + if (process == NULL) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s not found! %s", compilerName, cantFindMessage); + return false; + } + + SDL_DestroyProcess(process); + return true; + +#else + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Compiling to METALLIB is not supported on this platform!"); + return false; +#endif +} + +int compile_metallib(ShaderCross_Platform platform, const char *outputFilename) +{ + const char *stdString; + const char *minversion; + if (platform == PLATFORM_METAL_MACOS) { + stdString = "-std=macos-metal2.0"; + minversion = "-mmacosx-version-min=10.13"; + } else { + stdString = "-std=ios-metal2.0"; + minversion = "-miphoneos-version-min=13.0"; + } + +#if defined(SDL_PLATFORM_MACOS) + const char* sdkString = (platform == PLATFORM_METAL_MACOS) ? "macosx" : "iphoneos"; + const char* compileToIRCommand[] = { "xcrun", "-sdk", sdkString, "metal", stdString, minversion, "-Wall", "-O3", "-c", "tmp.metal", "-o", "tmp.ir", NULL }; + const char* compileToMetallibCommand[] = { "xcrun", "-sdk", sdkString, "metallib", "tmp.ir", "-o", outputFilename, NULL }; +#else + const char* compileToIRCommand[] = { "metal", stdString, minversion, "-Wall", "-O3", "-c", "tmp.metal", "-o", "tmp.ir", NULL}; + const char* compileToMetallibCommand[] = { "metallib", "tmp.ir", "-o", outputFilename, NULL}; +#endif + + int exitcode; + SDL_Process *process = SDL_CreateProcess(compileToIRCommand, true); + SDL_WaitProcess(process, true, &exitcode); + SDL_RemovePath("tmp.metal"); + if (exitcode != 0) { + return exitcode; + } + SDL_DestroyProcess(process); + + process = SDL_CreateProcess(compileToMetallibCommand, true); + SDL_WaitProcess(process, true, &exitcode); + SDL_RemovePath("tmp.ir"); + if (exitcode != 0) { + return exitcode; + } + SDL_DestroyProcess(process); + return 0; +}