Files
filament/libs/filament-matp/src/MaterialParser.cpp
T
yein 95128d5f55 Follow up of material parser PR (#9029)
- Fix CMakeLists to correctly include src files in matp test
- Replace std::string to std::string_view
- Also update the test_includer.cpp
2025-08-11 18:05:10 +00:00

521 lines
19 KiB
C++

/*
* Copyright (C) 2025 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 <filament-matp/MaterialParser.h>
#include <memory>
#include <iostream>
#include <utility>
#include <filamat/MaterialBuilder.h>
#include <filamat/Enums.h>
#include <utils/Log.h>
#include <utils/JobSystem.h>
#include "DirIncluder.h"
#include "JsonishLexer.h"
#include "JsonishParser.h"
#include "MaterialLexeme.h"
#include "MaterialLexer.h"
#include "ParametersProcessor.h"
#include <filament-matp/Config.h>
using namespace utils;
using namespace filamat;
using namespace std::placeholders;
namespace matp {
static constexpr const char* CONFIG_KEY_MATERIAL= "material";
static constexpr const char* CONFIG_KEY_VERTEX_SHADER = "vertex";
static constexpr const char* CONFIG_KEY_FRAGMENT_SHADER = "fragment";
static constexpr const char* CONFIG_KEY_COMPUTE_SHADER = "compute";
static constexpr const char* CONFIG_KEY_TOOL = "tool";
MaterialParser::MaterialParser() {
mConfigProcessor[CONFIG_KEY_MATERIAL] = &MaterialParser::processMaterial;
mConfigProcessor[CONFIG_KEY_VERTEX_SHADER] = &MaterialParser::processVertexShader;
mConfigProcessor[CONFIG_KEY_FRAGMENT_SHADER] = &MaterialParser::processFragmentShader;
mConfigProcessor[CONFIG_KEY_COMPUTE_SHADER] = &MaterialParser::processComputeShader;
mConfigProcessor[CONFIG_KEY_TOOL] = &MaterialParser::ignoreLexeme;
mConfigProcessorJSON[CONFIG_KEY_MATERIAL] = &MaterialParser::processMaterialJSON;
mConfigProcessorJSON[CONFIG_KEY_VERTEX_SHADER] = &MaterialParser::processVertexShaderJSON;
mConfigProcessorJSON[CONFIG_KEY_FRAGMENT_SHADER] = &MaterialParser::processFragmentShaderJSON;
mConfigProcessorJSON[CONFIG_KEY_COMPUTE_SHADER] = &MaterialParser::processComputeShaderJSON;
mConfigProcessorJSON[CONFIG_KEY_TOOL] = &MaterialParser::ignoreLexemeJSON;
}
bool MaterialParser::processMaterial(const MaterialLexeme& jsonLexeme,
MaterialBuilder& builder) const noexcept {
JsonishLexer jlexer;
jlexer.lex(jsonLexeme.getStart(), jsonLexeme.getSize(), jsonLexeme.getLine());
JsonishParser parser(jlexer.getLexemes());
std::unique_ptr<JsonishObject> const json = parser.parse();
if (json == nullptr) {
std::cerr << "JsonishParser error (see above)." << std::endl;
return false;
}
ParametersProcessor parametersProcessor;
bool const ok = parametersProcessor.process(builder, *json);
if (!ok) {
std::cerr << "Error while processing material." << std::endl;
return false;
}
return true;
}
bool MaterialParser::processVertexShader(const MaterialLexeme& lexeme,
MaterialBuilder& builder) const noexcept {
MaterialLexeme const trimmedLexeme = lexeme.trimBlockMarkers();
std::string const shaderStr = trimmedLexeme.getStringValue();
// getLine() returns a line number, with 1 being the first line, but .material wants a 0-based
// line number offset, where 0 is the first line.
builder.materialVertex(shaderStr.c_str(), trimmedLexeme.getLine() - 1);
return true;
}
bool MaterialParser::processFragmentShader(const MaterialLexeme& lexeme,
MaterialBuilder& builder) const noexcept {
MaterialLexeme const trimmedLexeme = lexeme.trimBlockMarkers();
std::string const shaderStr = trimmedLexeme.getStringValue();
// getLine() returns a line number, with 1 being the first line, but .material wants a 0-based
// line number offset, where 0 is the first line.
builder.material(shaderStr.c_str(), trimmedLexeme.getLine() - 1);
return true;
}
bool MaterialParser::processComputeShader(const MaterialLexeme& lexeme,
MaterialBuilder& builder) const noexcept {
return MaterialParser::processFragmentShader(lexeme, builder);
}
bool MaterialParser::ignoreLexeme(const MaterialLexeme&, MaterialBuilder&) const noexcept {
return true;
}
static bool reflectParameters(const MaterialBuilder& builder) {
uint8_t const count = builder.getParameterCount();
const MaterialBuilder::ParameterList& parameters = builder.getParameters();
std::cout << "{" << std::endl;
std::cout << " \"parameters\": [" << std::endl;
for (uint8_t i = 0; i < count; i++) {
const MaterialBuilder::Parameter& parameter = parameters[i];
std::cout << " {" << std::endl;
std::cout << R"( "name": ")" << parameter.name.c_str() << "\"," << std::endl;
if (parameter.isSampler()) {
std::cout << R"( "type": ")" <<
Enums::toString(parameter.samplerType) << "\"," << std::endl;
std::cout << R"( "format": ")" <<
Enums::toString(parameter.format) << "\"," << std::endl;
std::cout << R"( "precision": ")" <<
Enums::toString(parameter.precision) << "\"," << std::endl;
std::cout << R"( "multisample": ")" <<
(parameter.multisample ? "true" : "false")<< "\"" << std::endl;
} else if (parameter.isUniform()) {
std::cout << R"( "type": ")" <<
Enums::toString(parameter.uniformType) << "\"," << std::endl;
std::cout << R"( "size": ")" << parameter.size << "\"" << std::endl;
} else if (parameter.isSubpass()) {
std::cout << R"( "type": ")" <<
Enums::toString(parameter.subpassType) << "\"," << std::endl;
std::cout << R"( "format": ")" <<
Enums::toString(parameter.format) << "\"," << std::endl;
std::cout << R"( "precision": ")" <<
Enums::toString(parameter.precision) << "\"" << std::endl;
}
std::cout << " }";
if (i < count - 1) std::cout << ",";
std::cout << std::endl;
}
std::cout << " ]" << std::endl;
std::cout << "}" << std::endl;
return true;
}
bool MaterialParser::processMaterialJSON(const JsonishValue* value,
filamat::MaterialBuilder& builder) const noexcept {
if (!value) {
std::cerr << "'material' block does not have a value, one is required." << std::endl;
return false;
}
if (value->getType() != JsonishValue::OBJECT) {
std::cerr << "'material' block has an invalid type: "
<< JsonishValue::typeToString(value->getType())
<< ", should be OBJECT."
<< std::endl;
return false;
}
ParametersProcessor parametersProcessor;
bool const ok = parametersProcessor.process(builder, *value->toJsonObject());
if (!ok) {
std::cerr << "Error while processing material." << std::endl;
return false;
}
return true;
}
bool MaterialParser::processVertexShaderJSON(const JsonishValue* value,
filamat::MaterialBuilder& builder) const noexcept {
if (!value) {
std::cerr << "'vertex' block does not have a value, one is required." << std::endl;
return false;
}
if (value->getType() != JsonishValue::STRING) {
std::cerr << "'vertex' block has an invalid type: "
<< JsonishValue::typeToString(value->getType())
<< ", should be STRING."
<< std::endl;
return false;
}
builder.materialVertex(value->toJsonString()->getString().c_str());
return true;
}
bool MaterialParser::processFragmentShaderJSON(const JsonishValue* value,
filamat::MaterialBuilder& builder) const noexcept {
if (!value) {
std::cerr << "'fragment' block does not have a value, one is required." << std::endl;
return false;
}
if (value->getType() != JsonishValue::STRING) {
std::cerr << "'fragment' block has an invalid type: "
<< JsonishValue::typeToString(value->getType())
<< ", should be STRING."
<< std::endl;
return false;
}
builder.material(value->toJsonString()->getString().c_str());
return true;
}
bool MaterialParser::processComputeShaderJSON(const JsonishValue* value,
filamat::MaterialBuilder& builder) const noexcept {
if (!value) {
std::cerr << "'compute' block does not have a value, one is required." << std::endl;
return false;
}
if (value->getType() != JsonishValue::STRING) {
std::cerr << "'compute' block has an invalid type: "
<< JsonishValue::typeToString(value->getType())
<< ", should be STRING."
<< std::endl;
return false;
}
builder.material(value->toJsonString()->getString().c_str());
return true;
}
bool MaterialParser::ignoreLexemeJSON(const JsonishValue*,
filamat::MaterialBuilder&) const noexcept {
return true;
}
bool MaterialParser::isValidJsonStart(const char* buffer, size_t size) const noexcept {
// Skip all whitespace characters.
const char* end = buffer + size;
while (buffer != end && isspace(buffer[0])) {
buffer++;
}
// A buffer made only of whitespace is not a valid JSON start.
if (buffer == end) {
return false;
}
const char c = buffer[0];
// Take care of block, array, and string
if (c == '{' || c == '[' || c == '"') {
return true;
}
// boolean true
if (c == 't' && (end - buffer) > 3 && strncmp(buffer, "true", 4) != 0) {
return true;
}
// boolean false
if (c == 'f' && (end - buffer) > 4 && strncmp(buffer, "false", 5) != 0) {
return true;
}
// null literal
return c == 'n' && (end - buffer) > 3 && strncmp(buffer, "null", 5) != 0;
}
bool MaterialParser::parseMaterialAsJSON(const char* buffer, size_t size,
filamat::MaterialBuilder& builder) const noexcept {
JsonishLexer jlexer;
jlexer.lex(buffer, size, 1);
JsonishParser parser(jlexer.getLexemes());
std::unique_ptr<JsonishObject> json = parser.parse();
if (json == nullptr) {
std::cerr << "Could not parse JSON material file" << std::endl;
return false;
}
for (auto& entry : json->getEntries()) {
const std::string& key = entry.first;
if (mConfigProcessorJSON.find(key) == mConfigProcessorJSON.end()) {
std::cerr << "Unknown identifier '" << key << "'" << std::endl;
return false;
}
// Retrieve function member pointer
MaterialConfigProcessorJSON const p = mConfigProcessorJSON.at(key);
// Call it.
bool const ok = (*this.*p)(entry.second, builder);
if (!ok) {
std::cerr << "Error while processing block with key:'" << key << "'" << std::endl;
return false;
}
}
return true;
}
bool MaterialParser::parseMaterial(const char* buffer, size_t size,
filamat::MaterialBuilder& builder) const noexcept {
MaterialLexer materialLexer;
materialLexer.lex(buffer, size);
auto lexemes = materialLexer.getLexemes();
// Make sure the lexer did not stumble upon unknown character. This could mean we received
// a binary file.
for (auto lexeme : lexemes) {
if (lexeme.getType() == MaterialType::UNKNOWN) {
std::cerr << "Unexpected character at line:" << lexeme.getLine()
<< " position:" << lexeme.getLinePosition() << std::endl;
return false;
}
}
// Make a first quick pass just to make sure the format was respected (the material format is
// a series of IDENTIFIER, BLOCK pairs).
if (lexemes.size() < 2) {
std::cerr << "Input MUST be an alternation of [identifier, block] pairs." << std::endl;
return false;
}
for (size_t i = 0; i < lexemes.size(); i += 2) {
auto lexeme = lexemes.at(i);
if (lexeme.getType() != MaterialType::IDENTIFIER) {
std::cerr << "An identifier was expected at line:" << lexeme.getLine()
<< " position:" << lexeme.getLinePosition() << std::endl;
return false;
}
if (i == lexemes.size() - 1) {
std::cerr << "Identifier at line:" << lexeme.getLine()
<< " position:" << lexeme.getLinePosition()
<< " must be followed by a block." << std::endl;
return false;
}
auto nextLexeme = lexemes.at(i + 1);
if (nextLexeme.getType() != MaterialType::BLOCK) {
std::cerr << "A block was expected at line:" << lexeme.getLine()
<< " position:" << lexeme.getLinePosition() << std::endl;
return false;
}
}
std::string identifier;
for (auto lexeme : lexemes) {
if (lexeme.getType() == MaterialType::IDENTIFIER) {
identifier = lexeme.getStringValue();
if (mConfigProcessor.find(identifier) == mConfigProcessor.end()) {
std::cerr << "Unknown identifier '"
<< identifier
<< "' at line:" << lexeme.getLine()
<< " position:" << lexeme.getLinePosition() << std::endl;
return false;
}
} else if (lexeme.getType() == MaterialType::BLOCK) {
MaterialConfigProcessor const processor = mConfigProcessor.at(identifier);
if (!(*this.*processor)(lexeme, builder)) {
std::cerr << "Error while processing block with key:'" << identifier << "'"
<< std::endl;
return false;
}
}
}
return true;
}
bool MaterialParser::processMaterialParameters(filamat::MaterialBuilder& builder,
const Config& config) const {
ParametersProcessor parametersProcessor;
bool ok = true;
for (const auto& param : config.getMaterialParameters()) {
ok &= parametersProcessor.process(builder, param.first, param.second);
}
return ok;
}
bool MaterialParser::processTemplateSubstitutions(
const Config& config, ssize_t& size, std::unique_ptr<const char[]>& buffer) {
const auto& templateMap = config.getTemplateMap();
ssize_t modifiedSize = size;
bool modified = false;
if (!templateMap.empty()) {
for (ssize_t cursor = 0, n = size - 1; cursor < n; ++cursor) {
if (UTILS_LIKELY(buffer[cursor] != '$' || buffer[cursor + 1] != '{')) {
continue;
}
cursor += 2;
ssize_t endCursor = cursor;
while (true) {
if (endCursor == size) {
std::cerr << "Unexpected end of file" << std::endl;
return false;
}
if (buffer[endCursor] == '}') {
break;
}
++endCursor;
}
// At this point, cursor points to the F in ${FOO} and endCursor points to the }.
std::string_view const macro(&buffer[cursor], endCursor - cursor);
if (auto iter = templateMap.find(macro); iter != templateMap.end()) {
modifiedSize -= macro.size() + 3;
modifiedSize += iter->second.size();
} else {
std::cerr << "Undefined template macro:" << macro << std::endl;
return false;
}
modified = true;
}
}
// Second pass of template substitution allocates a new buffer and performs the actual
// substitutions. Since the first pass did not return early, we can safely assume no errors.
if (modified) {
auto modifiedBuffer = std::make_unique<char[]>(modifiedSize);
for (ssize_t cursor = 0, dstCursor = 0; cursor < size; ++cursor) {
const ssize_t next = cursor + 1;
if (UTILS_LIKELY(buffer[cursor] != '$' || next >= size || buffer[next] != '{')) {
modifiedBuffer[dstCursor++] = buffer[cursor];
continue;
}
cursor += 2;
ssize_t endCursor = cursor;
while (buffer[endCursor] != '}') ++endCursor;
// At this point, cursor points to the F in ${FOO} and endCursor points to the }.
std::string_view const macro(&buffer[cursor], endCursor - cursor);
const std::string& val = templateMap.find(macro)->second;
for (size_t i = 0, n = val.size(); i < n; ++i, ++dstCursor) {
modifiedBuffer[dstCursor] = val[i];
}
cursor = endCursor;
}
buffer = std::move(modifiedBuffer);
size = modifiedSize;
}
return true;
}
bool MaterialParser::parse(filamat::MaterialBuilder& builder,
const Config& config,
Config::Input* input, ssize_t& size, std::unique_ptr<const char[]>& buffer) {
utils::Path const materialFilePath = utils::Path(input->getName()).getAbsolutePath();
assert(materialFilePath.isFile());
if (builder.getFeatureLevel() > config.getFeatureLevel()) {
std::cerr << "Material feature level (" << +builder.getFeatureLevel()
<< ") is higher than maximum allowed (" << +config.getFeatureLevel() << ")" << std::endl;
return false;
}
// Before attempting an expensive lex, let's find out if we were sent pure JSON.
bool parsed;
if (isValidJsonStart(buffer.get(), size_t(size))) {
parsed = parseMaterialAsJSON(buffer.get(), size_t(size), builder);
} else {
parsed = parseMaterial(buffer.get(), size_t(size), builder);
}
if (!parsed) {
return false;
}
switch (config.getReflectionTarget()) {
case Config::Metadata::NONE:
break;
case Config::Metadata::PARAMETERS:
return reflectParameters(builder);
}
// Set the root include directory to the directory containing the material file.
DirIncluder includer;
includer.setIncludeDirectory(materialFilePath.getParent());
builder
.noSamplerValidation(config.noSamplerValidation())
.includeEssl1(config.includeEssl1())
.includeCallback(includer)
.fileName(materialFilePath.getName().c_str())
.platform(config.getPlatform())
.targetApi(config.getTargetApi())
.optimization(config.getOptimizationLevel())
.printShaders(config.printShaders())
.saveRawVariants(config.saveRawVariants())
.generateDebugInfo(config.isDebug())
.variantFilter(config.getVariantFilter() | builder.getVariantFilter());
for (const auto& define : config.getDefines()) {
builder.shaderDefine(define.first.c_str(), define.second.c_str());
}
if (!processMaterialParameters(builder, config)) {
std::cerr << "Error while processing material parameters." << std::endl;
return false;
}
return true;
}
} // namespace matp