Files
filament/libs/filamat/src/ShaderMinifier.cpp
T
Romain Guy 4a116e6791 Improve size optimizations when compiling material (#6721)
* Improve size optimizations when compiling material

This changes the behavior of the size optimizer in matc (-S), but
only for GLSL and MSL. With this change we gain a ~65% size reduction
on a lit material compiled for OpenGL. To get those gains we generate
extra SPIRV debug information to preserve variable names and better
utilize the line dictionary. Unfortunately this break the SPIRV
optimizer so we skip it and instead rely on a simple DCE pass provided
by glslang. We also enhance the whitespace removal pass of the GLSL
minifier to move lone { and } to the previous line, which avoids
generating an extra index in each shader variant. Each index being
at least as big as the character itself, this is quite wasteful.

When generating SPIRV for Vulkan, we rely on spirv-opt for size
optimizations as before.
2023-04-07 09:34:43 -07:00

339 lines
12 KiB
C++

/*
* Copyright (C) 2020 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 "ShaderMinifier.h"
#include <utils/Log.h>
namespace filamat {
static bool isIdCharNondigit(char c) {
return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
static bool isIdChar(char c) {
return isIdCharNondigit(c) || (c >= '0' && c <= '9');
};
// Checks if a GLSL identifier lives at the given index in the given codeline.
// If so, returns the identifier and moves the given index to point
// to the first character after the identifier.
static bool getId(std::string_view codeline, size_t* pindex, std::string_view* id) {
size_t index = *pindex;
if (index >= codeline.size()) {
return false;
}
if (!isIdCharNondigit(codeline[index])) {
return false;
}
++index;
while (index < codeline.size() && isIdChar(codeline[index])) {
++index;
}
*id = codeline.substr(*pindex, index - *pindex);
*pindex = index;
return true;
}
// Checks if the given string lives anywhere after the given index in the given codeline.
// If so, moves the given index to point to the first character after the string.
static bool getString(std::string_view codeline, size_t* pindex, std::string_view s) {
size_t index = codeline.find(s, *pindex);
if (index == std::string_view::npos) {
return false;
}
*pindex = index + s.size();
return true;
}
// Checks if the given character is at the last position of the string.
static bool getLastChar(std::string_view codeline, size_t index, char c) {
if (index != codeline.size() - 1) {
return false;
}
return codeline[index] == c;
}
// Checks if whitespace lives at the given position of a codeline; if so, updates the given index.
static bool getWhitespace(std::string_view codeline, size_t* pindex) {
size_t index = *pindex;
while (index < codeline.size() && (codeline[index] == ' ' || codeline[index] == '\t')) {
++index;
}
if (index == *pindex) {
return false;
}
*pindex = index;
return true;
}
// Skips past an optional precision qualifier that is possibly surrounded by whitespace.
static void ignorePrecision(std::string_view codeline, size_t* pindex) {
static std::string tokens[3] = {"lowp", "mediump", "highp"};
const size_t i = *pindex;
getWhitespace(codeline, pindex);
for (const auto& token : tokens) {
const size_t n = token.size();
if (codeline.substr(i, i + n) == token) {
*pindex += n;
break;
}
}
getWhitespace(codeline, pindex);
}
// Checks if the given string lives has an array size at the given codeline.
// If so, moves the given index to point to the first character after the array size.
// Always returns true for convenient daisy-chaining in the parser.
static bool ignoreArraySize(std::string_view codeline, size_t* pindex) {
size_t index = *pindex;
if (index >= codeline.size()) {
return true;
}
if (codeline[index] != '[') {
return true;
}
++index;
while (index < codeline.size() && codeline[index] != ']') {
++index;
}
*pindex = (index == codeline.size()) ? index : index + 1;
return true;
}
static void replaceAll(std::string& result, std::string_view from, std::string to) {
size_t n = 0;
while ((n = result.find(from, n)) != std::string::npos) {
result.replace(n, from.size(), to);
n += to.size();
}
}
namespace {
enum ParserState {
OUTSIDE,
STRUCT_OPEN,
STRUCT_DEFN,
};
}
/**
* Shrinks the specified string and returns a new string as the result.
* To shrink the string, this method performs the following transforms:
* - Remove leading white spaces at the beginning of each line
* - Remove empty lines
*/
std::string ShaderMinifier::removeWhitespace(const std::string& s, bool mergeBraces) const {
size_t cur = 0;
std::string r;
r.reserve(s.length());
while (cur < s.length()) {
size_t pos = cur;
size_t len = 0;
while (s[cur] != '\n') {
cur++;
len++;
}
size_t newPos = s.find_first_not_of(" \t", pos);
if (newPos == std::string::npos) newPos = pos;
// If we have a single { or } on a line, move it to the previous line instead
size_t subLen = len - (newPos - pos);
if (mergeBraces && subLen == 1 && (s[newPos] == '{' || s[newPos] == '}')) {
r.replace(r.size() - 1, 1, 1, s[newPos]);
} else {
r.append(s, newPos, subLen);
}
r += '\n';
while (s[cur] == '\n') {
cur++;
}
}
return r;
}
/**
* Uniform block definitions can be quite big so this compresses them as follows.
* First, the uniform struct definitions are found, new field names are generated, and a mapping
* table is built. Second, all uses are replaced by applying the mapping table.
*
* The struct definition must be a sequence of tokens with the following pattern. This is fairly
* constrained (e.g. no comments or nesting) but this is designed to operate on generated code.
*
* "uniform" TypeIdentifier
* {
* OptionalPrecQual TypeIdentifier FieldIdentifier OptionalArraySize ;
* OptionalPrecQual TypeIdentifier FieldIdentifier OptionalArraySize ;
* OptionalPrecQual TypeIdentifier FieldIdentifier OptionalArraySize ;
* } StructIdentifier ;
*/
std::string ShaderMinifier::renameStructFields(const std::string& source) {
std::string_view sv = source;
size_t first = 0;
mCodelines.clear();
while (first < sv.size()) {
const size_t second = sv.find_first_of('\n', first);
if (first != second) {
mCodelines.emplace_back(sv.substr(first, second - first));
}
if (second == std::string_view::npos) {
break;
}
first = second + 1;
}
buildFieldMapping();
return applyFieldMapping();
}
void ShaderMinifier::buildFieldMapping() {
mStructFieldMap.clear();
mStructDefnMap.clear();
std::string currentStructPrefix;
std::vector<std::string_view> currentStructFields;
ParserState state = OUTSIDE;
for (std::string_view codeline : mCodelines) {
size_t cursor = 0;
std::string_view typeId;
std::string_view fieldName;
switch (state) {
case OUTSIDE:
if (getString(codeline, &cursor, "uniform") &&
getWhitespace(codeline, &cursor) &&
getId(codeline, &cursor, &typeId) &&
cursor == codeline.size()) {
currentStructPrefix = std::string(typeId) + ".";
state = STRUCT_OPEN;
}
continue;
case STRUCT_OPEN:
state = getLastChar(codeline, 0, '{') ? STRUCT_DEFN : OUTSIDE;
continue;
case STRUCT_DEFN: {
std::string_view structName;
if (getString(codeline, &cursor, "}")) {
if (!getWhitespace(codeline, &cursor) ||
!getId(codeline, &cursor, &structName) ||
!getLastChar(codeline, cursor, ';')) {
break;
}
const std::string structNamePrefix = std::string(structName) + ".";
std::string generatedFieldName = "a";
for (auto field : currentStructFields) {
const std::string sField(field);
const std::string key = structNamePrefix + sField;
const std::string val = structNamePrefix + generatedFieldName;
mStructFieldMap.push_back({key, val});
mStructDefnMap[currentStructPrefix + sField] = generatedFieldName;
if (generatedFieldName[0] == 'z') {
generatedFieldName = "a" + generatedFieldName;
} else {
generatedFieldName[0]++;
}
}
currentStructFields.clear();
state = OUTSIDE;
break;
}
ignorePrecision(codeline, &cursor);
if (!getId(codeline, &cursor, &typeId) ||
!getWhitespace(codeline, &cursor) ||
!getId(codeline, &cursor, &fieldName) ||
!ignoreArraySize(codeline, &cursor) ||
!getLastChar(codeline, cursor, ';')) {
break;
}
currentStructFields.push_back(fieldName);
break;
}
}
}
// Sort keys from longest to shortest because we want to replace "fogColorFromIbl" before
// replacing "fogColor".
const auto& compare = [](const RenameEntry& a, const RenameEntry& b) {
return a.first.length() > b.first.length();
};
std::sort(mStructFieldMap.begin(), mStructFieldMap.end(), compare);
}
std::string ShaderMinifier::applyFieldMapping() const {
std::string result;
ParserState state = OUTSIDE;
std::string currentStructPrefix;
for (std::string_view codeline : mCodelines) {
std::string modified(codeline);
std::string_view fieldName;
std::string_view structName;
std::string_view typeId;
size_t cursor = 0;
switch (state) {
case OUTSIDE: {
if (getString(codeline, &cursor, "uniform") &&
getWhitespace(codeline, &cursor) &&
getId(codeline, &cursor, &typeId) &&
cursor == codeline.size()) {
currentStructPrefix = std::string(typeId) + ".";
state = STRUCT_OPEN;
break;
}
for (const auto& key : mStructFieldMap) {
replaceAll(modified, key.first, key.second);
}
break;
}
case STRUCT_OPEN:
state = getLastChar(codeline, 0, '{') ? STRUCT_DEFN : OUTSIDE;
break;
case STRUCT_DEFN: {
if (getString(codeline, &cursor, "}") &&
getWhitespace(codeline, &cursor) &&
getId(codeline, &cursor, &structName) &&
getLastChar(codeline, cursor, ';')) {
state = OUTSIDE;
break;
}
ignorePrecision(codeline, &cursor);
if (!getId(codeline, &cursor, &typeId) ||
!getWhitespace(codeline, &cursor) ||
!getId(codeline, &cursor, &fieldName) ||
!ignoreArraySize(codeline, &cursor) ||
!getLastChar(codeline, cursor, ';')) {
break;
}
std::string key = currentStructPrefix + std::string(fieldName);
auto iter = mStructDefnMap.find(key);
if (iter == mStructDefnMap.end()) {
utils::slog.e << "ShaderMinifier error: " << key << utils::io::endl;
break;
}
replaceAll(modified, fieldName, iter->second);
break;
}
}
result += modified + "\n";
}
return result;
}
} // namespace filamat