From afe292986e71f81a1c1b68d7befdee9901df5ffe Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 18 Apr 2024 12:18:29 -0400 Subject: [PATCH] Introduce new matedit tool (#7759) --- CMakeLists.txt | 3 + NEW_RELEASE_NOTES.md | 2 + libs/filamat/src/MaterialBuilder.cpp | 9 +- libs/filamat/src/eiff/BlobDictionary.cpp | 4 +- libs/filamat/src/eiff/BlobDictionary.h | 2 +- libs/filamat/src/eiff/Flattener.h | 7 + libs/filamat/src/eiff/ShaderEntry.h | 2 +- libs/matdbg/src/ShaderReplacer.cpp | 2 +- libs/utils/CMakeLists.txt | 5 + tools/matedit/CMakeLists.txt | 28 ++ tools/matedit/src/ExternalCompile.cpp | 423 +++++++++++++++++++++++ tools/matedit/src/ExternalCompile.h | 31 ++ tools/matedit/src/main.cpp | 180 ++++++++++ 13 files changed, 690 insertions(+), 8 deletions(-) create mode 100644 tools/matedit/CMakeLists.txt create mode 100644 tools/matedit/src/ExternalCompile.cpp create mode 100644 tools/matedit/src/ExternalCompile.h create mode 100644 tools/matedit/src/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ed32949873..f67e61d4ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/NEW_RELEASE_NOTES.md b/NEW_RELEASE_NOTES.md index 4a1a9c7fa7..2594ce4088 100644 --- a/NEW_RELEASE_NOTES.md +++ b/NEW_RELEASE_NOTES.md @@ -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 diff --git a/libs/filamat/src/MaterialBuilder.cpp b/libs/filamat/src/MaterialBuilder.cpp index d6c56b882f..9acf657353 100644 --- a/libs/filamat/src/MaterialBuilder.cpp +++ b/libs/filamat/src/MaterialBuilder.cpp @@ -964,12 +964,15 @@ bool MaterialBuilder::generateShaders(JobSystem& jobSystem, const std::vector d(reinterpret_cast(spirv.data()), + reinterpret_cast(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 spirv = std::move(s.data); + std::vector spirv = std::move(s.data); s.dictionaryIndex = spirvDictionary.addBlob(spirv); } for (const auto& s : metalEntries) { diff --git a/libs/filamat/src/eiff/BlobDictionary.cpp b/libs/filamat/src/eiff/BlobDictionary.cpp index d13227a599..692884b0d9 100644 --- a/libs/filamat/src/eiff/BlobDictionary.cpp +++ b/libs/filamat/src/eiff/BlobDictionary.cpp @@ -20,8 +20,8 @@ namespace filamat { -size_t BlobDictionary::addBlob(const std::vector& vblob) noexcept { - std::string_view blob((char*) vblob.data(), vblob.size() * 4); +size_t BlobDictionary::addBlob(const std::vector& vblob) noexcept { + std::string_view blob((char*) vblob.data(), vblob.size()); auto iter = mBlobIndices.find(blob); if (iter != mBlobIndices.end()) { return iter->second; diff --git a/libs/filamat/src/eiff/BlobDictionary.h b/libs/filamat/src/eiff/BlobDictionary.h index c204f92658..b642f188d8 100644 --- a/libs/filamat/src/eiff/BlobDictionary.h +++ b/libs/filamat/src/eiff/BlobDictionary.h @@ -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& blob) noexcept; + size_t addBlob(const std::vector& blob) noexcept; size_t getBlobCount() const noexcept { return mBlobs.size(); diff --git a/libs/filamat/src/eiff/Flattener.h b/libs/filamat/src/eiff/Flattener.h index 2e3cfa2235..e43cdb7681 100644 --- a/libs/filamat/src/eiff/Flattener.h +++ b/libs/filamat/src/eiff/Flattener.h @@ -127,6 +127,13 @@ public: mCursor += nbytes; } + void writeRaw(const char* raw, size_t nbytes) { + if (mStart != nullptr) { + memcpy(reinterpret_cast(mCursor), raw, nbytes); + } + mCursor += nbytes; + } + void writeSizePlaceholder() { mSizePlaceholders.push_back(mCursor); if (mStart != nullptr) { diff --git a/libs/filamat/src/eiff/ShaderEntry.h b/libs/filamat/src/eiff/ShaderEntry.h index 6a015e458f..b8e2067609 100644 --- a/libs/filamat/src/eiff/ShaderEntry.h +++ b/libs/filamat/src/eiff/ShaderEntry.h @@ -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 data; + std::vector data; }; } // namespace filamat diff --git a/libs/matdbg/src/ShaderReplacer.cpp b/libs/matdbg/src/ShaderReplacer.cpp index 215328d868..9cd8ba4de0 100644 --- a/libs/matdbg/src/ShaderReplacer.cpp +++ b/libs/matdbg/src/ShaderReplacer.cpp @@ -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(ptr, ptr + src.size() / 4)); + record.dictionaryIndex = blobs.addBlob(vector(ptr, ptr + src.size())); } // Adjust start cursor of flatteners to match alignment of output stream. diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt index 4f693f24ce..e19a943c18 100644 --- a/libs/utils/CMakeLists.txt +++ b/libs/utils/CMakeLists.txt @@ -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) diff --git a/tools/matedit/CMakeLists.txt b/tools/matedit/CMakeLists.txt new file mode 100644 index 0000000000..e88aad24b0 --- /dev/null +++ b/tools/matedit/CMakeLists.txt @@ -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) diff --git a/tools/matedit/src/ExternalCompile.cpp b/tools/matedit/src/ExternalCompile.cpp new file mode 100644 index 0000000000..ee0374bc2d --- /dev/null +++ b/tools/matedit/src/ExternalCompile.cpp @@ -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 +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +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(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& 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 +static std::vector getShaderRecords(const filaflat::ChunkContainer& container, + const filaflat::BlobDictionary& dictionary, filamat::ChunkType chunkType) { + if (!container.hasChunk(chunkType)) { + return {}; + } + std::vector 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) { + shaderRecords.push_back({ shaderModel, variant, stage, std::move(source) }); + } + if constexpr (std::is_same_v) { + filamat::BinaryEntry e {}; + e.shaderModel = shaderModel; + e.variant = variant; + e.stage = stage; + e.dictionaryIndex = 0; + e.data = std::vector(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& userArgs, backend::ShaderStage stage, + backend::ShaderModel model, utils::Path inputPath, utils::Path outputPath) { + assert_invariant(!userArgs.empty()); + + std::vector argv; + + // The first argument is the path to the script + argv.push_back(const_cast(userArgs[0].c_str())); + + // Temporary input and output files + argv.push_back(const_cast(inputPath.c_str())); + argv.push_back(const_cast(outputPath.c_str())); + argv.push_back(const_cast(toString(stage).c_str())); + argv.push_back(const_cast(toString(model).c_str())); + + // Optional user-supplied arguments + for (int i = 1; i < userArgs.size(); i++) { + argv.push_back(const_cast(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(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& mslEntries, + std::vector& metalBinaryEntries, + const std::vector& 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 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 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(getFileSize(input.c_str())); + + std::vector buffer(static_cast(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( + container, stringBlobs, filamat::ChunkType::MaterialMetal); + auto glslEntries = getShaderRecords( + container, stringBlobs, filamat::ChunkType::MaterialGlsl); + auto essl1Entries = getShaderRecords( + container, stringBlobs, filamat::ChunkType::MaterialEssl1); + auto spirvEntries = getShaderRecords( + container, spirvBinaryBlobs, filamat::ChunkType::MaterialSpirv); + + // Ask the user script to compile the MSL shaders into .metallib files. + filamat::BlobDictionary metalBinaryDictionary; + std::vector 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 spirv = std::move(s.data); + s.dictionaryIndex = spirvDictionary.addBlob(spirv); + } + + // Generate the Metal library dictionary. + for (auto& e : metalBinaryEntries) { + std::vector 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( + reinterpret_cast(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( + std::move(textDictionary), filamat::ChunkType::DictionaryText); + + // Re-emit GLSL chunk (MaterialTextChunk). + if (!glslEntries.empty()) { + outputChunks.push(std::move(glslEntries), + dictionaryChunk.getDictionary(), filamat::ChunkType::MaterialGlsl); + } + + // Re-emit ESSL1 chunk (MaterialTextChunk). + if (!essl1Entries.empty()) { + outputChunks.push(std::move(essl1Entries), + dictionaryChunk.getDictionary(), filamat::ChunkType::MaterialEssl1); + } + } + + // Add the SPIRV chunks. + if (!spirvEntries.empty()) { + const bool stripInfo = true; + outputChunks.push(std::move(spirvDictionary), stripInfo); + outputChunks.push( + std::move(spirvEntries), filamat::ChunkType::MaterialSpirv); + } + + // Add the new Metal binary chunks. + outputChunks.push(std::move(metalBinaryDictionary)); + outputChunks.push( + 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 diff --git a/tools/matedit/src/ExternalCompile.h b/tools/matedit/src/ExternalCompile.h new file mode 100644 index 0000000000..d24907c690 --- /dev/null +++ b/tools/matedit/src/ExternalCompile.h @@ -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 + +#include +#include + +namespace matedit { + +int externalCompile(utils::Path input, utils::Path output, std::vector args); + +} // namespace matedit + +#endif diff --git a/tools/matedit/src/main.cpp b/tools/matedit/src/main.cpp new file mode 100644 index 0000000000..0adc980877 --- /dev/null +++ b/tools/matedit/src/main.cpp @@ -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 + +#include "ExternalCompile.h" + +#include + +#include +#include +#include + +struct Config { + utils::Path inputFile; + utils::Path outputFile; + std::vector 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 -i external-compile --