Introduce new matedit tool (#7759)
This commit is contained in:
committed by
Benjamin Doherty
parent
ed4154ee0e
commit
d9cba80bcf
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
28
tools/matedit/CMakeLists.txt
Normal file
28
tools/matedit/CMakeLists.txt
Normal 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)
|
||||
423
tools/matedit/src/ExternalCompile.cpp
Normal file
423
tools/matedit/src/ExternalCompile.cpp
Normal 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
|
||||
31
tools/matedit/src/ExternalCompile.h
Normal file
31
tools/matedit/src/ExternalCompile.h
Normal 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
180
tools/matedit/src/main.cpp
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user