Introduce new matedit tool (#7759)

This commit is contained in:
Ben Doherty
2024-04-18 12:18:29 -04:00
committed by Benjamin Doherty
parent ed4154ee0e
commit d9cba80bcf
13 changed files with 690 additions and 8 deletions

View File

@@ -819,6 +819,9 @@ if (IS_HOST_PLATFORM)
add_subdirectory(${TOOLS}/glslminifier)
add_subdirectory(${TOOLS}/matc)
add_subdirectory(${TOOLS}/matinfo)
if (NOT WIN32) # matedit not yet supported on Windows
add_subdirectory(${TOOLS}/matedit)
endif()
add_subdirectory(${TOOLS}/mipgen)
add_subdirectory(${TOOLS}/normal-blending)
add_subdirectory(${TOOLS}/resgen)

View File

@@ -7,3 +7,5 @@ for next branch cut* header.
appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md).
## Release notes for next branch cut
- Add new matedit tool

View File

@@ -964,12 +964,15 @@ bool MaterialBuilder::generateShaders(JobSystem& jobSystem, const std::vector<Va
glslEntries.push_back(glslEntry);
}
break;
case TargetApi::VULKAN:
case TargetApi::VULKAN: {
assert(!spirv.empty());
const std::vector<uint8_t> d(reinterpret_cast<uint8_t*>(spirv.data()),
reinterpret_cast<uint8_t*>(spirv.data() + spirv.size()));
spirvEntry.stage = v.stage;
spirvEntry.data = std::move(spirv);
spirvEntry.data = std::move(d);
spirvEntries.push_back(spirvEntry);
break;
}
case TargetApi::METAL:
assert(!spirv.empty());
assert(msl.length() > 0);
@@ -1019,7 +1022,7 @@ bool MaterialBuilder::generateShaders(JobSystem& jobSystem, const std::vector<Va
textDictionary.addText(s.shader);
}
for (auto& s : spirvEntries) {
std::vector<uint32_t> spirv = std::move(s.data);
std::vector<uint8_t> spirv = std::move(s.data);
s.dictionaryIndex = spirvDictionary.addBlob(spirv);
}
for (const auto& s : metalEntries) {

View File

@@ -20,8 +20,8 @@
namespace filamat {
size_t BlobDictionary::addBlob(const std::vector<uint32_t>& vblob) noexcept {
std::string_view blob((char*) vblob.data(), vblob.size() * 4);
size_t BlobDictionary::addBlob(const std::vector<uint8_t>& vblob) noexcept {
std::string_view blob((char*) vblob.data(), vblob.size());
auto iter = mBlobIndices.find(blob);
if (iter != mBlobIndices.end()) {
return iter->second;

View File

@@ -36,7 +36,7 @@ public:
BlobDictionary(BlobDictionary&&) = default;
// Adds a blob if it's not already a duplicate and returns its index.
size_t addBlob(const std::vector<uint32_t>& blob) noexcept;
size_t addBlob(const std::vector<uint8_t>& blob) noexcept;
size_t getBlobCount() const noexcept {
return mBlobs.size();

View File

@@ -127,6 +127,13 @@ public:
mCursor += nbytes;
}
void writeRaw(const char* raw, size_t nbytes) {
if (mStart != nullptr) {
memcpy(reinterpret_cast<char*>(mCursor), raw, nbytes);
}
mCursor += nbytes;
}
void writeSizePlaceholder() {
mSizePlaceholders.push_back(mCursor);
if (mStart != nullptr) {

View File

@@ -41,7 +41,7 @@ struct BinaryEntry {
size_t dictionaryIndex; // maps to an index in the blob dictionary
// temporarily holds this entry's binary data until added to the dictionary
std::vector<uint32_t> data;
std::vector<uint8_t> data;
};
} // namespace filamat

View File

@@ -378,7 +378,7 @@ void BlobIndex::writeChunks(ostream& stream) {
const auto& src = mDataBlobs[record.dictionaryIndex];
assert(src.size() % 4 == 0);
const uint32_t* ptr = (const uint32_t*) src.data();
record.dictionaryIndex = blobs.addBlob(vector<uint32_t>(ptr, ptr + src.size() / 4));
record.dictionaryIndex = blobs.addBlob(vector<uint8_t>(ptr, ptr + src.size()));
}
// Adjust start cursor of flatteners to match alignment of output stream.

View File

@@ -116,6 +116,11 @@ if (WIN32)
target_link_libraries(${TARGET} PUBLIC Shlwapi)
endif()
if (APPLE)
# Needed for NSTemporaryDirectory()
target_link_libraries(${TARGET} PRIVATE "-framework Foundation")
endif()
if (LINUX)
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)

View File

@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.19)
project(matedit)
set(TARGET matedit)
# ==================================================================================================
# Sources and headers
# ==================================================================================================
set(SRCS
src/main.cpp
src/ExternalCompile.cpp
)
# ==================================================================================================
# Target definitions
# ==================================================================================================
add_executable(${TARGET} ${SRCS})
target_include_directories(${TARGET} PRIVATE ${filamat_SOURCE_DIR}/src)
target_link_libraries(${TARGET} matdbg getopt)
set_target_properties(${TARGET} PROPERTIES FOLDER Tools)
# ==================================================================================================
# Installation
# ==================================================================================================
install(TARGETS ${TARGET} RUNTIME DESTINATION bin)

View File

@@ -0,0 +1,423 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ExternalCompile.h"
#include "backend/DriverEnums.h"
#include "eiff/BlobDictionary.h"
#include "eiff/ChunkContainer.h"
#include "eiff/DictionaryMetalLibraryChunk.h"
#include "eiff/DictionarySpirvChunk.h"
#include "eiff/DictionaryTextChunk.h"
#include "eiff/LineDictionary.h"
#include "eiff/MaterialBinaryChunk.h"
#include "eiff/MaterialTextChunk.h"
#include "eiff/ShaderEntry.h"
#include <filaflat/ChunkContainer.h>
#include <filaflat/DictionaryReader.h>
#include <filaflat/MaterialChunk.h>
#include <matdbg/ShaderExtractor.h>
#include <matdbg/ShaderInfo.h>
#include <filamat/Package.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fstream>
#include <iostream>
using filamat::Flattener;
using filamat::Package;
using namespace filament;
class PassthroughChunk final : public filamat::Chunk {
public:
explicit PassthroughChunk(const char* data, size_t size, filamat::ChunkType type)
: filamat::Chunk(type), data(data), size(size) {}
~PassthroughChunk() = default;
private:
void flatten(Flattener& f) override { f.writeRaw(data, size); }
const char* data;
size_t size;
};
namespace matedit {
static std::ifstream::pos_type getFileSize(const char* filename) {
std::ifstream in(filename, std::ifstream::ate | std::ifstream::binary);
return in.tellg();
}
static void dumpBinary(const uint8_t* data, size_t size, utils::Path filename) {
std::ofstream out(filename, std::ofstream::binary);
out.write(reinterpret_cast<const char*>(data), size);
}
static void dumpString(const std::string& data, utils::Path filename) {
std::ofstream out(filename, std::ofstream::binary);
out << data;
}
static bool readBinary(utils::Path filename, std::vector<uint8_t>& buffer) {
std::ifstream in(filename, std::ifstream::binary | std::ifstream::in);
if (!in) {
return false;
}
in.seekg(0, std::ios::end);
std::ifstream::pos_type size = in.tellg();
in.seekg(0);
buffer.resize(size);
if (!in.read((char*)buffer.data(), size)) {
return false;
}
return true;
}
template <typename T>
static std::vector<T> getShaderRecords(const filaflat::ChunkContainer& container,
const filaflat::BlobDictionary& dictionary, filamat::ChunkType chunkType) {
if (!container.hasChunk(chunkType)) {
return {};
}
std::vector<T> shaderRecords;
filaflat::MaterialChunk materialChunk(container);
materialChunk.initialize(chunkType);
materialChunk.visitShaders(
[&materialChunk, &dictionary, &shaderRecords](
backend::ShaderModel shaderModel, Variant variant, backend::ShaderStage stage) {
filaflat::ShaderContent content;
UTILS_UNUSED_IN_RELEASE bool success =
materialChunk.getShader(content, dictionary, shaderModel, variant, stage);
std::string source { content.data(), content.data() + content.size() - 1u };
assert_invariant(success);
if constexpr (std::is_same_v<T, filamat::TextEntry>) {
shaderRecords.push_back({ shaderModel, variant, stage, std::move(source) });
}
if constexpr (std::is_same_v<T, filamat::BinaryEntry>) {
filamat::BinaryEntry e {};
e.shaderModel = shaderModel;
e.variant = variant;
e.stage = stage;
e.dictionaryIndex = 0;
e.data = std::vector<uint8_t>(content.begin(), content.end());
shaderRecords.push_back(std::move(e));
}
});
return shaderRecords;
}
static std::string toString(backend::ShaderModel model) {
switch (model) {
case backend::ShaderModel::DESKTOP:
return "desktop";
case backend::ShaderModel::MOBILE:
return "mobile";
}
}
static std::string toString(backend::ShaderStage stage) {
switch (stage) {
case backend::ShaderStage::VERTEX:
return "vertex";
case backend::ShaderStage::FRAGMENT:
return "fragment";
case backend::ShaderStage::COMPUTE:
return "compute";
}
}
static std::string toString(Variant variant) { return std::to_string(variant.key); }
static bool invokeScript(const std::vector<std::string>& userArgs, backend::ShaderStage stage,
backend::ShaderModel model, utils::Path inputPath, utils::Path outputPath) {
assert_invariant(!userArgs.empty());
std::vector<char*> argv;
// The first argument is the path to the script
argv.push_back(const_cast<char*>(userArgs[0].c_str()));
// Temporary input and output files
argv.push_back(const_cast<char*>(inputPath.c_str()));
argv.push_back(const_cast<char*>(outputPath.c_str()));
argv.push_back(const_cast<char*>(toString(stage).c_str()));
argv.push_back(const_cast<char*>(toString(model).c_str()));
// Optional user-supplied arguments
for (int i = 1; i < userArgs.size(); i++) {
argv.push_back(const_cast<char*>(userArgs[i].c_str()));
}
// execvp expects a null as the last element of the arguments array
argv.push_back(nullptr);
std::cout << "Invoking script: ";
for (const char* a : argv) {
if (a) {
std::cout << a << " ";
}
}
std::cout << std::endl;
pid_t pid = fork();
if (pid == -1) {
// The fork() command failed
std::cerr << "Unable to fork process." << std::endl;
return false;
} else if (pid > 0) {
// Parent process
int status;
waitpid(pid, &status, 0); // Wait for the child to finish
if (WIFEXITED(status)) {
if (WEXITSTATUS(status) != 0) {
std::cerr << "Script exited with status: " << WEXITSTATUS(status) << std::endl;
return false;
}
}
} else {
// Child process
execvp(argv[0], argv.data());
// If execvp returns, it failed
std::cerr << "Unable to execute script: " << argv[0] << std::endl;
exit(1);
}
return true;
}
class ScopedTempFile {
public:
ScopedTempFile(utils::Path&& path) noexcept {
auto segments = path.split();
auto ext = path.getExtension();
segments[segments.size() - 1] = path.getNameWithoutExtension() + ".XXXXXX." + ext;
utils::Path pathTemplate;
for (const auto& s : segments) {
pathTemplate += s;
}
std::string pathString = pathTemplate.getPath();
int fd = mkstemps(const_cast<char*>(pathString.c_str()), ext.size() + 1);
if (fd == -1) {
std::cerr << "Error creating temporary file: " << pathString << std::endl;
exit(1);
}
close(fd); // close the file, it's been created for us
mPath = pathString;
}
~ScopedTempFile() noexcept { mPath.unlinkFile(); }
const utils::Path& getPath() const noexcept { return mPath; }
ScopedTempFile(const ScopedTempFile& rhs) = delete;
ScopedTempFile(ScopedTempFile&& rhs) = delete;
ScopedTempFile& operator=(const ScopedTempFile& rhs) = delete;
ScopedTempFile& operator=(ScopedTempFile&& rhs) = delete;
private:
utils::Path mPath;
};
bool compileMetalShaders(const std::vector<filamat::TextEntry>& mslEntries,
std::vector<filamat::BinaryEntry>& metalBinaryEntries,
const std::vector<std::string>& userArgs) {
const utils::Path tempDir = utils::Path::getTemporaryDirectory();
for (const auto& mslEntry : mslEntries) {
const std::string fileName = toString(mslEntry.shaderModel) + "_" +
toString(mslEntry.stage) + "_" + toString(mslEntry.variant);
const std::string inputFileName = fileName + ".metal";
const std::string outputFileName = fileName + ".metallib";
ScopedTempFile inputFile = tempDir + inputFileName;
ScopedTempFile outputFile = tempDir + outputFileName;
dumpString(mslEntry.shader, inputFile.getPath());
if (!invokeScript(userArgs, mslEntry.stage, mslEntry.shaderModel, inputFile.getPath(),
outputFile.getPath())) {
return false;
}
std::vector<uint8_t> buffer;
if (!readBinary(outputFile.getPath(), buffer)) {
std::cerr << "Could not read output file " << outputFile.getPath() << std::endl;
return false;
}
if (buffer.empty()) {
std::cerr << "Output file " << outputFile.getPath() << " is empty" << std::endl;
return false;
}
filamat::BinaryEntry metalBinaryEntry {};
metalBinaryEntry.shaderModel = mslEntry.shaderModel;
metalBinaryEntry.variant = mslEntry.variant;
metalBinaryEntry.stage = mslEntry.stage;
metalBinaryEntry.data = std::move(buffer);
metalBinaryEntries.push_back(metalBinaryEntry);
}
return true;
}
int externalCompile(utils::Path input, utils::Path output, std::vector<std::string> args) {
std::ifstream in(input.c_str(), std::ifstream::in | std::ios::binary);
if (!in.is_open()) {
std::cerr << "Could not open the source material " << input << std::endl;
return 1;
}
const long fileSize = static_cast<long>(getFileSize(input.c_str()));
std::vector<char> buffer(static_cast<unsigned long>(fileSize));
if (!in.read(buffer.data(), fileSize)) {
std::cerr << "Could not read the source material." << std::endl;
return 1;
}
filaflat::ChunkContainer container(buffer.data(), buffer.size());
if (!container.parse()) {
return 1;
}
// Get all shaders from the input material.
filaflat::BlobDictionary stringBlobs;
filaflat::BlobDictionary spirvBinaryBlobs;
filaflat::DictionaryReader reader;
if (container.hasChunk(filamat::ChunkType::DictionaryText)) {
reader.unflatten(container, filamat::ChunkType::DictionaryText, stringBlobs);
}
if (container.hasChunk(filamat::ChunkType::DictionarySpirv)) {
reader.unflatten(container, filamat::ChunkType::DictionarySpirv, spirvBinaryBlobs);
}
auto mslEntries = getShaderRecords<filamat::TextEntry>(
container, stringBlobs, filamat::ChunkType::MaterialMetal);
auto glslEntries = getShaderRecords<filamat::TextEntry>(
container, stringBlobs, filamat::ChunkType::MaterialGlsl);
auto essl1Entries = getShaderRecords<filamat::TextEntry>(
container, stringBlobs, filamat::ChunkType::MaterialEssl1);
auto spirvEntries = getShaderRecords<filamat::BinaryEntry>(
container, spirvBinaryBlobs, filamat::ChunkType::MaterialSpirv);
// Ask the user script to compile the MSL shaders into .metallib files.
filamat::BlobDictionary metalBinaryDictionary;
std::vector<filamat::BinaryEntry> metalBinaryEntries;
if (!compileMetalShaders(mslEntries, metalBinaryEntries, args)) {
return 1;
}
// Since we're modifying text shaders, we'll need to regenerate the text dictionary.
// We'll also need to re-emit text based shaders that rely on the dictionary.
// Here we ONLY add GLSL and ESSL 1 types, as we're removing MSL completely.
filamat::LineDictionary textDictionary;
for (const auto& s : glslEntries) {
textDictionary.addText(s.shader);
}
for (const auto& s : essl1Entries) {
textDictionary.addText(s.shader);
}
// We'll also need to regenerate the SPIRV dictionary and SPIRV shaders.
// This is required, as the SPIRV blobs have alignment requirements. Since we're modifying other
// chunks, their alignment might have changed.
filamat::BlobDictionary spirvDictionary;
for (auto& s : spirvEntries) {
std::vector<uint8_t> spirv = std::move(s.data);
s.dictionaryIndex = spirvDictionary.addBlob(spirv);
}
// Generate the Metal library dictionary.
for (auto& e : metalBinaryEntries) {
std::vector<uint8_t> data = std::move(e.data);
e.dictionaryIndex = metalBinaryDictionary.addBlob(data);
}
// Pass through chunks that don't need to change.
filamat::ChunkContainer outputChunks;
for (int i = 0; i < container.getChunkCount(); i++) {
filaflat::ChunkContainer::Chunk c = container.getChunk(i);
if (c.type == filamat::ChunkType::MaterialMetal) {
// This chunk is being removed, skip it.
continue;
}
if (c.type == filamat::ChunkType::MaterialGlsl ||
c.type == filamat::ChunkType::MaterialEssl1 ||
c.type == filamat::ChunkType::MaterialSpirv ||
c.type == filamat::ChunkType::DictionarySpirv ||
c.type == filamat::ChunkType::DictionaryText) {
// These shader / dictionary chunks will be re-added below.
continue;
}
outputChunks.push<PassthroughChunk>(
reinterpret_cast<const char*>(c.desc.start), c.desc.size, c.type);
}
// Add the re-generated text dictionary chunk and text-based shaders.
if (!textDictionary.isEmpty()) {
const auto& dictionaryChunk = outputChunks.push<filamat::DictionaryTextChunk>(
std::move(textDictionary), filamat::ChunkType::DictionaryText);
// Re-emit GLSL chunk (MaterialTextChunk).
if (!glslEntries.empty()) {
outputChunks.push<filamat::MaterialTextChunk>(std::move(glslEntries),
dictionaryChunk.getDictionary(), filamat::ChunkType::MaterialGlsl);
}
// Re-emit ESSL1 chunk (MaterialTextChunk).
if (!essl1Entries.empty()) {
outputChunks.push<filamat::MaterialTextChunk>(std::move(essl1Entries),
dictionaryChunk.getDictionary(), filamat::ChunkType::MaterialEssl1);
}
}
// Add the SPIRV chunks.
if (!spirvEntries.empty()) {
const bool stripInfo = true;
outputChunks.push<filamat::DictionarySpirvChunk>(std::move(spirvDictionary), stripInfo);
outputChunks.push<filamat::MaterialBinaryChunk>(
std::move(spirvEntries), filamat::ChunkType::MaterialSpirv);
}
// Add the new Metal binary chunks.
outputChunks.push<filamat::DictionaryMetalLibraryChunk>(std::move(metalBinaryDictionary));
outputChunks.push<filamat::MaterialBinaryChunk>(
std::move(metalBinaryEntries), filamat::ChunkType::MaterialMetalLibrary);
// Flatten into a Package and write to disk.
Package package(outputChunks.getSize());
Flattener f { package.getData() };
outputChunks.flatten(f);
assert_invariant(package.isValid());
dumpBinary(package.getData(), package.getSize(), output);
return 0;
}
} // namespace matedit

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef TNT_MATEDIT_EXTERNALCOMPILE_H
#define TNT_MATEDIT_EXTERNALCOMPILE_H
#include <utils/Path.h>
#include <string>
#include <vector>
namespace matedit {
int externalCompile(utils::Path input, utils::Path output, std::vector<std::string> args);
} // namespace matedit
#endif

180
tools/matedit/src/main.cpp Normal file
View File

@@ -0,0 +1,180 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <getopt/getopt.h>
#include "ExternalCompile.h"
#include <utils/Path.h>
#include <iostream>
#include <string>
#include <vector>
struct Config {
utils::Path inputFile;
utils::Path outputFile;
std::vector<std::string> commandArgs;
};
static void printUsage(const char* name) {
std::string execName(utils::Path(name).getName());
std::string usage(
"MATEDIT allows editing material files compiled with matc\n"
"\n"
"Caution! MATEDIT was designed to operate on trusted inputs. To minimize the risk of triggering\n"
"memory corruption vulnerabilities, please make sure that the files passed to MATEDIT come from a\n"
"trusted source, or run MATEDIT in a sandboxed environment.\n"
"\n"
"Usage:\n"
" MATEDIT [options] -o <output file> -i <input file> external-compile -- <script> [<script args>...]\n"
"\n"
"Options:\n"
" --help, -h\n"
" Print this message\n"
"\n"
" --input=[input file], -i\n"
" Specify path to input compiled material file\n"
"\n"
" --output=[output file], -o\n"
" Specify path to output compiled material file file\n"
"\n"
" --type=[shader type], -t\n"
" Specify the shader type, currently only metal is supported\n"
"\n"
"Commands:\n"
" external-compile\n"
" Transforms all the text-based shaders of the specified type in the input file into binaries\n"
" by using the provided script. For each text shader, script is invoked as follows:\n"
"\n"
" script <input shader> <output binary> <shader stage> <shader platform> [<script args>...]\n"
"\n"
" The arguments to the script are as follows:\n"
" ${1} : a temporary input file containing the shader text contents\n"
" ${2} : a temporary output file the script should write the compiled shader to\n"
" ${3} : the shader stage for this shader, either 'vertex', 'fragment', or 'compute'\n"
" ${4} : the platform for this shader, either 'desktop' or 'mobile'\n"
" ${@:5} : user-provided arguments <script args> passed to MATEDIT\n"
"\n"
" script should compile the shader located at <input shader> into binary, writing its results\n"
" to the <output binary> file. The specifics of the binary format depend on the shader type:\n"
"\n"
" metal: precompiled Metal library (.metallib)\n"
" <input shader> is guaranteed to have a .metal extension.\n"
" <output binary> is guaranteed to have a .metallib extension.\n"
"\n"
" This command will remove the text-based shaders when writing the output material file.\n"
"\n"
" If script exits with a non-zero exit code, MATEDIT will terminate with error. Multiple\n"
" invocations of script may be launched in parallel.\n"
"\n"
"Example:\n"
" MATEDIT -o out.cmat -i in.cmat --type metal external-compile -- ./my_compile-script.sh --sdk iphones\n"
);
const std::string from("MATEDIT");
for (size_t pos = usage.find(from); pos != std::string::npos; pos = usage.find(from, pos)) {
usage.replace(pos, from.length(), execName);
}
printf("%s", usage.c_str());
}
static int handleArguments(int argc, char* argv[], Config* config) {
static constexpr const char* OPTSTR = "hi:o:t:";
static const struct option OPTIONS[] = {
{ "help", no_argument, nullptr, 'h' },
{ "input", required_argument, nullptr, 'i' },
{ "output", required_argument, nullptr, 'o' },
{ "type", required_argument, nullptr, 't' },
{ nullptr, 0, nullptr, 0 } // termination of the option list
};
int opt;
int optionIndex = 0;
while ((opt = getopt_long(argc, argv, OPTSTR, OPTIONS, &optionIndex)) >= 0) {
std::string arg(optarg ? optarg : "");
switch (opt) {
default:
case 'h':
printUsage(argv[0]);
exit(0);
case 'i':
config->inputFile = arg;
break;
case 'o':
config->outputFile = arg;
break;
case 't':
if (arg == "metal") {
// no-op for now
} else {
std::cerr << "Unrecognized shader type flag: '" << arg << "'. Must be 'metal'"
<< std::endl;
exit(1);
}
}
}
return optind;
}
int main(int argc, char* argv[]) {
Config config;
int optionIndex = handleArguments(argc, argv, &config);
if (config.inputFile.isEmpty() || config.outputFile.isEmpty()) {
printUsage(argv[0]);
return 1;
}
if (!config.inputFile.exists()) {
std::cerr << "The source material " << config.inputFile << " does not exist." << std::endl;
return 1;
}
int numArgs = argc - optionIndex;
if (numArgs < 1) {
printUsage(argv[0]);
return 1;
}
const std::string command = argv[optionIndex++];
if (command != "external-compile") {
std::cerr << "Unrecognized command: '" << command << "'. Must be 'external-compile'"
<< std::endl;
return 1;
}
// Ignore any "--" arguments between "external-compile" and the name of the script.
int i = optionIndex;
while (i < argc && strcmp(argv[i], "--") == 0) {
i++;
}
for (; i < argc; ++i) {
config.commandArgs.emplace_back(argv[i]);
}
if (config.commandArgs.empty()) {
std::cerr << "external-compile requires the path to a script to execute" << std::endl
<< std::endl;
printUsage(argv[0]);
return 1;
}
return matedit::externalCompile(config.inputFile, config.outputFile, config.commandArgs);
}