Files
filament/libs/uberz/src/WritableArchive.cpp
Mathias Agopian 47930edf70 don't use assert_invariant in public headers
this is to eventually suppress the dependency on utils/debug.h from
public headers
2025-05-29 09:56:52 -07:00

350 lines
12 KiB
C++

/*
* Copyright (C) 2022 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 <uberz/WritableArchive.h>
#include <uberz/ReadableArchive.h>
#include <zstd.h>
#include <string_view>
#include <utils/compiler.h>
#include <utils/debug.h>
#include <utils/Log.h>
#include <utils/Panic.h>
using namespace utils;
using namespace std::literals;
using std::string_view;
namespace filament::uberz {
static bool isAlphaNumeric(char c) {
return (c >= '0' && c <= '9') || c == '_'
|| (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
static bool isWhitespace(char c) {
return c == ' ' || c == '\t';
}
// Returns 0 if stringToScan does not start with prefix, otherwise returns length of prefix.
static size_t readPrefix(string_view prefix, string_view stringToScan) {
const size_t prefixSize = prefix.size();
if (UTILS_UNLIKELY(stringToScan.size() < prefixSize)) {
return 0;
}
return prefix == string_view { stringToScan.data(), prefixSize } ? prefixSize : 0;
}
static string_view readArchiveFeature(string_view cursor, ArchiveFeature* feature) {
size_t sz;
if (sz = readPrefix("unsupported"sv, cursor); sz > 0) {
*feature = ArchiveFeature::UNSUPPORTED;
} else if (sz = readPrefix("required"sv, cursor); sz > 0) {
*feature = ArchiveFeature::REQUIRED;
} else if (sz = readPrefix("optional"sv, cursor); sz > 0) {
*feature = ArchiveFeature::OPTIONAL;
}
return { cursor.data(), sz };
}
static string_view readBlendingMode(string_view cursor, BlendingMode* blending) {
size_t sz;
if (sz = readPrefix("opaque"sv, cursor); sz > 0) {
*blending = BlendingMode::OPAQUE;
} else if (sz = readPrefix("transparent"sv, cursor); sz > 0) {
*blending = BlendingMode::TRANSPARENT;
} else if (sz = readPrefix("add"sv, cursor); sz > 0) {
*blending = BlendingMode::ADD;
} else if (sz = readPrefix("masked"sv, cursor); sz > 0) {
*blending = BlendingMode::MASKED;
} else if (sz = readPrefix("fade"sv, cursor); sz > 0) {
*blending = BlendingMode::FADE;
} else if (sz = readPrefix("multiply"sv, cursor); sz > 0) {
*blending = BlendingMode::MULTIPLY;
} else if (sz = readPrefix("screen"sv, cursor); sz > 0) {
*blending = BlendingMode::SCREEN;
} else if (sz = readPrefix("custom"sv, cursor); sz > 0) {
*blending = BlendingMode::CUSTOM;
}
return { cursor.data(), sz };
}
static string_view readShadingModel(string_view cursor, Shading* shading) {
size_t sz;
if (sz = readPrefix("unlit"sv, cursor); sz > 0) {
*shading = Shading::UNLIT;
} else if (sz = readPrefix("lit"sv, cursor); sz > 0) {
*shading = Shading::LIT;
} else if (sz = readPrefix("subsurface"sv, cursor); sz > 0) {
*shading = Shading::SUBSURFACE;
} else if (sz = readPrefix("cloth"sv, cursor); sz > 0) {
*shading = Shading::CLOTH;
} else if (sz = readPrefix("specularGlossiness"sv, cursor); sz > 0) {
*shading = Shading::SPECULAR_GLOSSINESS;
}
return { cursor.data(), sz };
}
static string_view readSymbol(string_view cursor, char symbol) {
return { cursor.data(), (!cursor.empty() && cursor[0] == symbol) ? 1ul : 0ul };
}
static string_view readIdentifier(string_view cursor) {
size_t i = 0;
while (i < cursor.size() && isAlphaNumeric(cursor[i])) i++;
return { cursor.data(), i };
}
static string_view readWhitespace(string_view cursor) {
size_t i = 0;
while (i < cursor.size() && isWhitespace(cursor[i])) i++;
return { cursor.data(), i };
}
void WritableArchive::addMaterial(const char* name, const uint8_t* package, size_t packageSize) {
Material& material = mMaterials[++mMaterialIndex];
material = {
CString(name),
FixedCapacityVector<uint8_t>(packageSize)
};
memcpy(material.package.data(), package, packageSize);
// We use invalid values to denote "not set". e.g. if the spec file
// does not set the blend mode, then it can be used for any blend mode.
material.blendingMode = INVALID_BLENDING;
material.shadingModel = INVALID_SHADING_MODEL;
mLineNumber = 1;
}
void WritableArchive::addSpecLine(string_view line) {
assert_invariant(mMaterialIndex > -1);
Material& material = mMaterials[mMaterialIndex];
string_view cursor = line;
string_view token;
// Don't try to recover, just emit the error message and quit.
auto emitSyntaxError = [&](const char* msg) {
const int column = 1 + line.size() - cursor.size();
PANIC_POSTCONDITION("%s.spec(%d,%d): %s", material.name.c_str(), mLineNumber, column, msg);
};
// Advance the cursor forward each time a token is consumed.
auto advanceCursorByToken = [&]() {
cursor = { cursor.data() + token.size(), cursor.size() - token.size() };
};
// Sometimes we need to advance the cursor forward by a fixed amount.
auto advanceCursorByCount = [&](size_t count) {
cursor = { cursor.data() + count, cursor.size() - count };
};
auto parseFeatureFlag = [&]() {
if (token = readIdentifier(cursor); token.empty()) {
emitSyntaxError("expected identifier");
}
advanceCursorByToken();
CString id(token.data(), token.size());
token = readWhitespace(cursor);
advanceCursorByToken();
if (token = readSymbol(cursor, '='); token.empty()) {
emitSyntaxError("expected equal sign");
}
advanceCursorByToken();
token = readWhitespace(cursor);
advanceCursorByToken();
if (token = readArchiveFeature(cursor, &material.flags[id]); token.empty()) {
emitSyntaxError("expected unsupported / optional / required");
}
advanceCursorByToken();
};
auto parseBlendingMode = [&]() {
token = readWhitespace(cursor);
advanceCursorByToken();
if (token = readSymbol(cursor, '='); token.empty()) {
emitSyntaxError("expected equal sign");
}
advanceCursorByToken();
token = readWhitespace(cursor);
advanceCursorByToken();
if (token = readBlendingMode(cursor, &material.blendingMode); token.empty()) {
emitSyntaxError("expected lowercase blending mode enum");
}
advanceCursorByToken();
};
auto parseShadingModel = [&]() {
token = readWhitespace(cursor);
advanceCursorByToken();
if (token = readSymbol(cursor, '='); token.empty()) {
emitSyntaxError("expected equal sign");
}
advanceCursorByToken();
token = readWhitespace(cursor);
advanceCursorByToken();
if (token = readShadingModel(cursor, &material.shadingModel); token.empty()) {
emitSyntaxError("expected lowercase shading enum");
}
advanceCursorByToken();
};
if (cursor.empty() || cursor[0] == '#') {
++mLineNumber;
return;
}
if (size_t sz = readPrefix("BlendingMode"sv, cursor); sz > 0) {
advanceCursorByCount(sz);
parseBlendingMode();
} else if (size_t sz = readPrefix("ShadingModel"sv, cursor); sz > 0) {
advanceCursorByCount(sz);
parseShadingModel();
} else {
parseFeatureFlag();
}
if (!cursor.empty()) {
emitSyntaxError("unexpected trailing character");
}
++mLineNumber;
}
FixedCapacityVector<uint8_t> WritableArchive::serialize() const {
size_t byteCount = sizeof(ReadableArchive);
for (const auto& mat : mMaterials) {
byteCount += sizeof(ArchiveSpec);
}
size_t flaglistOffset = byteCount;
for (const auto& mat : mMaterials) {
for (const auto& pair : mat.flags) {
byteCount += sizeof(ArchiveFlag);
}
}
size_t nameOffset = byteCount;
for (const auto& mat : mMaterials) {
for (const auto& pair : mat.flags) {
byteCount += pair.first.size() + 1;
}
}
size_t filamatOffset = byteCount;
for (const auto& mat : mMaterials) {
byteCount += mat.package.size();
}
ReadableArchive archive;
archive.magic = 'UBER';
archive.version = 0;
archive.specsCount = mMaterials.size();
archive.specsOffset = sizeof(ReadableArchive);
auto specs = FixedCapacityVector<ArchiveSpec>::with_capacity(mMaterials.size());
size_t flagCount = 0;
for (const auto& mat : mMaterials) {
ArchiveSpec spec = {};
spec.shadingModel = mat.shadingModel;
spec.blendingMode = mat.blendingMode;
spec.flagsCount = mat.flags.size();
spec.flagsOffset = flaglistOffset + flagCount * sizeof(ArchiveFlag);
spec.packageByteCount = mat.package.size();
spec.packageOffset = filamatOffset;
specs.push_back(spec);
filamatOffset += mat.package.size();
flagCount += mat.flags.size();
}
auto flags = FixedCapacityVector<ArchiveFlag>::with_capacity(flagCount);
size_t charCount = 0;
for (const auto& mat : mMaterials) {
for (const auto& pair : mat.flags) {
ArchiveFlag flag;
flag.nameOffset = nameOffset + charCount;
flag.value = pair.second;
charCount += pair.first.size() + 1;
flags.push_back(flag);
}
}
std::string flagNames(charCount, ' ');
char* flagNamesPtr = flagNames.data();
for (const auto& mat : mMaterials) {
for (const auto& pair : mat.flags) {
memcpy(flagNamesPtr, pair.first.data(), pair.first.size() + 1);
flagNamesPtr += pair.first.size() + 1;
}
}
assert(flagNamesPtr - flagNames.data() == flagNames.size());
FixedCapacityVector<uint8_t> outputBuf(byteCount);
uint8_t* writeCursor = outputBuf.data();
memcpy(writeCursor, &archive, sizeof(archive));
writeCursor += sizeof(archive);
memcpy(writeCursor, specs.data(), sizeof(ArchiveSpec) * specs.size());
writeCursor += sizeof(ArchiveSpec) * specs.size();
memcpy(writeCursor, flags.data(), sizeof(ArchiveFlag) * flags.size());
writeCursor += sizeof(ArchiveFlag) * flags.size();
memcpy(writeCursor, flagNames.data(), charCount);
writeCursor += charCount;
for (const auto& mat : mMaterials) {
memcpy(writeCursor, mat.package.data(), mat.package.size());
writeCursor += mat.package.size();
}
assert_invariant(writeCursor - outputBuf.data() == outputBuf.size());
FixedCapacityVector<uint8_t> compressedBuf(ZSTD_compressBound(outputBuf.size()));
// Maximum zstd compression is slow, but that's okay since uberz is invoked during the build,
// not at run time. However in debug builds it is debilitatingly slow, and we're fine with
// larger archives, so we use minimum compression.
#ifdef NDEBUG
const int compressionLevel = ZSTD_maxCLevel();
#else
const int compressionLevel = ZSTD_minCLevel();
#endif
size_t zstdResult = ZSTD_compress(compressedBuf.data(), compressedBuf.size(), outputBuf.data(),
outputBuf.size(), compressionLevel);
if (ZSTD_isError(zstdResult)) {
PANIC_POSTCONDITION("Error during archive compression: %s", ZSTD_getErrorName(zstdResult));
}
compressedBuf.resize(zstdResult);
return compressedBuf;
}
void WritableArchive::setShadingModel(Shading sm) {
assert_invariant(mMaterialIndex > -1);
mMaterials[mMaterialIndex].shadingModel = sm;
}
void WritableArchive::setBlendingModel(BlendingMode bm) {
assert_invariant(mMaterialIndex > -1);
mMaterials[mMaterialIndex].blendingMode = bm;
}
void WritableArchive::setFeatureFlag(const char* key, ArchiveFeature value) {
assert_invariant(mMaterialIndex > -1);
mMaterials[mMaterialIndex].flags[CString(key)] = value;
}
} // namespace filament::uberz