Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 196 additions & 5 deletions src/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <SDL3_gpu_shadercross/SDL_gpu_shadercross.h>
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_iostream.h>
#include <SDL3/SDL_process.h>

// We can emit HLSL as a destination, so let's redefine the shader format enum.
typedef enum ShaderCross_DestinationFormat {
Expand All @@ -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 <input> [options]");
SDL_Log("Required options:\n");
SDL_Log(" %-*s %s", column_width, "-s | --source <value>", "Source language format. May be inferred from the filename. Values: [SPIRV, HLSL]");
SDL_Log(" %-*s %s", column_width, "-d | --dest <value>", "Destination format. May be inferred from the filename. Values: [DXBC, DXIL, MSL, SPIRV, HLSL]");
SDL_Log(" %-*s %s", column_width, "-d | --dest <value>", "Destination format. May be inferred from the filename. Values: [SPIRV, DXBC, DXIL, MSL, METALLIB, HLSL]");
SDL_Log(" %-*s %s", column_width, "-t | --stage <value>", "Shader stage. May be inferred from the filename. Values: [vertex, fragment, compute]");
SDL_Log(" %-*s %s", column_width, "-e | --entrypoint <value>", "Entrypoint function name. Default: \"main\".");
SDL_Log(" %-*s %s", column_width, "-m | --shadermodel <value>", "HLSL Shader Model. Only used with HLSL destination. Values: [5.0, 6.0]");
SDL_Log(" %-*s %s", column_width, "-p | --platform <value>", "Target platform. Only used with METALLIB destination. Values: [macOS, iOS]");
SDL_Log(" %-*s %s", column_width, "-o | --output <value>", "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";

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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")) {
Expand All @@ -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;
Expand Down Expand Up @@ -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!");
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Comment on lines +604 to +605
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has a chance to deadlock when metallib/xcrun echoes lots of data (when the pipe buffer is full, further writes will sleep).
Perhaps it's better to use SDL_CreateProcessWithProperties and set a null stdin/stdout/stderr?

Or, if that does not work, close stdin and read all stdout using SDL_ReadProcess

(Same applies to the next SDL_CreateProcess)

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;
}