Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add METALLIB output support for the CLI #40

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
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);
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;
}