Improved symbol handling in whole-program serialization
Previously, a dehydrated SkSL program would include all symbols, even module builtins. The rehydration API also required a vector of shared elements, which made the API essentially impossible to use outside of the very artificial situation in our test cases, where we retained the original program so we could simply grab the elements from it. We obviously cannot rely on the original program still being around in order to successfully rehydrate it. This CL eliminates the parent symbol tables, referring to module builtins by name instead. This reduces the size of a small dehydrated program by roughly 95%. We also encode the shared elements directly into the output, which both simplifies the API and makes it work in real-world cases. Change-Id: I8e5dddf9316fe0886e6b97e7d29638fff8f9f499 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/505816 Reviewed-by: John Stiles <johnstiles@google.com> Commit-Queue: Ethan Nicholas <ethannicholas@google.com>
This commit is contained in:
parent
654f09a5ad
commit
b86ab9d097
@ -79,6 +79,16 @@ private:
|
||||
Dehydrator* fDehydrator;
|
||||
};
|
||||
|
||||
void Dehydrator::writeId(const Symbol* s) {
|
||||
uint16_t id = this->symbolId(s);
|
||||
if (id) {
|
||||
this->writeU16(id);
|
||||
} else {
|
||||
this->writeU16(Rehydrator::kBuiltin_Symbol);
|
||||
this->write(s->name());
|
||||
}
|
||||
}
|
||||
|
||||
void Dehydrator::write(Layout l) {
|
||||
if (l == Layout()) {
|
||||
this->writeCommand(Rehydrator::kDefaultLayout_Command);
|
||||
@ -135,12 +145,13 @@ void Dehydrator::write(std::string s) {
|
||||
}
|
||||
|
||||
void Dehydrator::write(const Symbol& s) {
|
||||
uint16_t id = this->symbolId(&s, false);
|
||||
uint16_t id = this->symbolId(&s);
|
||||
if (id) {
|
||||
this->writeCommand(Rehydrator::kSymbolRef_Command);
|
||||
this->writeU16(id);
|
||||
return;
|
||||
}
|
||||
this->allocSymbolId(&s);
|
||||
switch (s.kind()) {
|
||||
case Symbol::Kind::kFunctionDeclaration: {
|
||||
const FunctionDeclaration& f = s.as<FunctionDeclaration>();
|
||||
@ -246,7 +257,7 @@ void Dehydrator::write(const SymbolTable& symbols) {
|
||||
if (!found) {
|
||||
// we should only fail to find builtin types
|
||||
SkASSERT(p.second->is<Type>() && p.second->as<Type>().isInBuiltinTypes());
|
||||
this->writeU16(Rehydrator::kBuiltinType_Symbol);
|
||||
this->writeU16(Rehydrator::kBuiltin_Symbol);
|
||||
this->write(p.second->name());
|
||||
}
|
||||
}
|
||||
@ -605,23 +616,26 @@ void Dehydrator::write(const std::vector<std::unique_ptr<ProgramElement>>& eleme
|
||||
|
||||
void Dehydrator::write(const Program& program) {
|
||||
this->writeCommand(Rehydrator::kProgram_Command);
|
||||
|
||||
// Collect the symbol tables so we can write out the count
|
||||
std::vector<SymbolTable*> symbolTables;
|
||||
SymbolTable* symbols = program.fSymbols.get();
|
||||
while (symbols) {
|
||||
symbolTables.push_back(symbols);
|
||||
symbols = symbols->fParent.get();
|
||||
}
|
||||
this->writeU8(symbolTables.size());
|
||||
|
||||
// Write the symbol tables from the root down
|
||||
for (int i = symbolTables.size() - 1; i >= 0; --i) {
|
||||
this->write(*symbolTables[i]);
|
||||
}
|
||||
this->writeU8((int)program.fConfig->fKind);
|
||||
this->write(*program.fSymbols);
|
||||
|
||||
// Write the elements
|
||||
this->write(program.fOwnedElements);
|
||||
this->writeCommand(Rehydrator::kElements_Command);
|
||||
for (const auto& e : program.fSharedElements) {
|
||||
this->writeCommand(Rehydrator::kSharedFunction_Command);
|
||||
const FunctionDefinition& f = e->as<FunctionDefinition>();
|
||||
const FunctionDeclaration& decl = f.declaration();
|
||||
this->writeU8(decl.parameters().size());
|
||||
for (const Variable* param : decl.parameters()) {
|
||||
this->write(*param);
|
||||
}
|
||||
this->write(f.declaration());
|
||||
this->write(*e);
|
||||
}
|
||||
for (const auto& e : program.fOwnedElements) {
|
||||
this->write(*e);
|
||||
}
|
||||
this->writeCommand(Rehydrator::kElementsComplete_Command);
|
||||
|
||||
// Write the inputs
|
||||
struct KnownSkSLProgramInputs { bool useRTFlipUniform; };
|
||||
|
@ -88,21 +88,20 @@ private:
|
||||
fBody.write32(i);
|
||||
}
|
||||
|
||||
void writeId(const Symbol* s) {
|
||||
if (!symbolId(s, false)) {
|
||||
fSymbolMap.back()[s] = fNextId++;
|
||||
}
|
||||
this->writeU16(symbolId(s));
|
||||
void allocSymbolId(const Symbol* s) {
|
||||
SkASSERT(!symbolId(s));
|
||||
fSymbolMap.back()[s] = fNextId++;
|
||||
}
|
||||
|
||||
uint16_t symbolId(const Symbol* s, bool required = true) {
|
||||
void writeId(const Symbol* s);
|
||||
|
||||
uint16_t symbolId(const Symbol* s) {
|
||||
for (const auto& symbols : fSymbolMap) {
|
||||
auto found = symbols.find(s);
|
||||
if (found != symbols.end()) {
|
||||
return found->second;
|
||||
}
|
||||
}
|
||||
SkASSERT(!required);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -82,9 +82,10 @@ private:
|
||||
std::shared_ptr<SymbolTable> fOldSymbols;
|
||||
};
|
||||
|
||||
Rehydrator::Rehydrator(const Compiler& compiler, const uint8_t* src, size_t length,
|
||||
Rehydrator::Rehydrator(Compiler& compiler, const uint8_t* src, size_t length,
|
||||
std::shared_ptr<SymbolTable> symbols)
|
||||
: fContext(compiler.fContext)
|
||||
: fCompiler(compiler)
|
||||
, fContext(compiler.fContext)
|
||||
, fSymbolTable(symbols ? std::move(symbols) : compiler.makeGLSLRootSymbolTable())
|
||||
SkDEBUGCODE(, fEnd(src + length)) {
|
||||
SkASSERT(fSymbolTable);
|
||||
@ -266,30 +267,26 @@ const Type* Rehydrator::type() {
|
||||
return (const Type*) result;
|
||||
}
|
||||
|
||||
std::unique_ptr<Program> Rehydrator::program(
|
||||
const std::vector<const ProgramElement*>* sharedElements) {
|
||||
std::unique_ptr<Program> Rehydrator::program() {
|
||||
[[maybe_unused]] uint8_t command = this->readU8();
|
||||
SkASSERT(command == kProgram_Command);
|
||||
uint8_t symbolTableCount = this->readU8();
|
||||
ProgramConfig* oldConfig = fContext->fConfig;
|
||||
ModifiersPool* oldModifiersPool = fContext->fModifiersPool;
|
||||
auto config = std::make_unique<ProgramConfig>();
|
||||
config->fKind = (ProgramKind)this->readU8();
|
||||
fContext->fConfig = config.get();
|
||||
fSymbolTable = fCompiler.moduleForProgramKind(config->fKind).fSymbols;
|
||||
auto modifiers = std::make_unique<ModifiersPool>();
|
||||
fContext->fModifiersPool = modifiers.get();
|
||||
for (int i = 0; i < symbolTableCount; ++i) {
|
||||
this->symbolTable();
|
||||
}
|
||||
this->symbolTable();
|
||||
std::vector<std::unique_ptr<ProgramElement>> elements = this->elements();
|
||||
fContext->fConfig = oldConfig;
|
||||
fContext->fModifiersPool = oldModifiersPool;
|
||||
if (!sharedElements) {
|
||||
sharedElements = &ThreadContext::SharedElements();
|
||||
}
|
||||
Program::Inputs inputs;
|
||||
inputs.fUseFlipRTUniform = this->readU8();
|
||||
return std::make_unique<Program>(nullptr, std::move(config), fContext, std::move(elements),
|
||||
*sharedElements, std::move(modifiers), fSymbolTable, /*pool=*/nullptr, inputs);
|
||||
/*sharedElements=*/std::vector<const ProgramElement*>(), std::move(modifiers),
|
||||
fSymbolTable, /*pool=*/nullptr, inputs);
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<ProgramElement>> Rehydrator::elements() {
|
||||
@ -339,6 +336,18 @@ std::unique_ptr<ProgramElement> Rehydrator::element() {
|
||||
SkASSERT(type && type->is<Type>());
|
||||
return std::make_unique<StructDefinition>(/*line=*/-1, type->as<Type>());
|
||||
}
|
||||
case Rehydrator::kSharedFunction_Command: {
|
||||
int count = this->readU8();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
[[maybe_unused]] const Symbol* param = this->symbol();
|
||||
SkASSERT(param->is<Variable>());
|
||||
}
|
||||
[[maybe_unused]] const Symbol* decl = this->symbol();
|
||||
SkASSERT(decl->is<FunctionDeclaration>());
|
||||
std::unique_ptr<ProgramElement> result = this->element();
|
||||
SkASSERT(result->is<FunctionDefinition>());
|
||||
return result;
|
||||
}
|
||||
case Rehydrator::kElementsComplete_Command:
|
||||
return nullptr;
|
||||
default:
|
||||
@ -529,9 +538,19 @@ std::unique_ptr<Expression> Rehydrator::expression() {
|
||||
}
|
||||
case Rehydrator::kFunctionCall_Command: {
|
||||
const Type* type = this->type();
|
||||
const FunctionDeclaration* f = this->symbolRef<FunctionDeclaration>(
|
||||
Symbol::Kind::kFunctionDeclaration);
|
||||
const Symbol* symbol = this->possiblyBuiltinSymbolRef();
|
||||
ExpressionArray args = this->expressionArray();
|
||||
const FunctionDeclaration* f;
|
||||
if (symbol->is<FunctionDeclaration>()) {
|
||||
f = &symbol->as<FunctionDeclaration>();
|
||||
} else if (symbol->is<UnresolvedFunction>()) {
|
||||
const UnresolvedFunction& unresolved = symbol->as<UnresolvedFunction>();
|
||||
f = FunctionCall::FindBestFunctionForCall(*fContext, unresolved.functions(), args);
|
||||
SkASSERT(f);
|
||||
} else {
|
||||
SkASSERT(false);
|
||||
return nullptr;
|
||||
}
|
||||
return FunctionCall::Make(*fContext, /*line=*/-1, type, *f, std::move(args));
|
||||
}
|
||||
case Rehydrator::kIndex_Command: {
|
||||
@ -607,7 +626,7 @@ std::shared_ptr<SymbolTable> Rehydrator::symbolTable() {
|
||||
symbols.reserve(symbolCount);
|
||||
for (int i = 0; i < symbolCount; ++i) {
|
||||
int index = this->readU16();
|
||||
if (index != kBuiltinType_Symbol) {
|
||||
if (index != kBuiltin_Symbol) {
|
||||
fSymbolTable->addWithoutOwnership(ownedSymbols[index]);
|
||||
} else {
|
||||
std::string_view name = this->readString();
|
||||
|
@ -34,7 +34,7 @@ class Type;
|
||||
*/
|
||||
class Rehydrator {
|
||||
public:
|
||||
static constexpr uint16_t kVersion = 6;
|
||||
static constexpr uint16_t kVersion = 7;
|
||||
|
||||
enum Command {
|
||||
// uint16 id, Type componentType, uint8 count
|
||||
@ -117,6 +117,9 @@ public:
|
||||
kReturn_Command,
|
||||
// String name, Expression value
|
||||
kSetting_Command,
|
||||
// uint8_t parameterCount, Variable[] parameters, FunctionDeclaration decl,
|
||||
// FunctionDefinition defn
|
||||
kSharedFunction_Command,
|
||||
// uint16 id, Type structType
|
||||
kStructDefinition_Command,
|
||||
// uint16 id, String name, uint8 fieldCount, (Modifiers, String, Type)[] fields
|
||||
@ -149,7 +152,7 @@ public:
|
||||
};
|
||||
|
||||
// src must remain in memory as long as the objects created from it do
|
||||
Rehydrator(const Compiler& compiler, const uint8_t* src, size_t length,
|
||||
Rehydrator(Compiler& compiler, const uint8_t* src, size_t length,
|
||||
std::shared_ptr<SymbolTable> base = nullptr);
|
||||
|
||||
#ifdef SK_DEBUG
|
||||
@ -162,15 +165,13 @@ public:
|
||||
// Reads a collection of program elements and returns it
|
||||
std::vector<std::unique_ptr<ProgramElement>> elements();
|
||||
|
||||
// Reads an entire program. If the sharedElements are not provided, they will be pulled from the
|
||||
// current ThreadContext.
|
||||
std::unique_ptr<Program> program(
|
||||
const std::vector<const ProgramElement*>* sharedElements = nullptr);
|
||||
// Reads an entire program.
|
||||
std::unique_ptr<Program> program();
|
||||
|
||||
private:
|
||||
// If this ID appears in a symbol table, it means the corresponding symbol isn't actually
|
||||
// present in the file as it's a builtin type.
|
||||
static constexpr uint16_t kBuiltinType_Symbol = 65535;
|
||||
// If this ID appears in place of a symbol ID, it means the corresponding symbol isn't actually
|
||||
// present in the file as it's a builtin. The string name of the symbol follows.
|
||||
static constexpr uint16_t kBuiltin_Symbol = 65535;
|
||||
|
||||
int8_t readS8() {
|
||||
SkASSERT(fIP < fEnd);
|
||||
@ -224,6 +225,24 @@ private:
|
||||
return (T*) fSymbols[result];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads either a symbol belonging to this program, or a named reference to a builtin symbol.
|
||||
* This has to be a separate method from symbolRef() because builtin symbols can be const, and
|
||||
* thus this method must have a const return, but there is at least one case in which we
|
||||
* specifically require a non-const return value.
|
||||
*/
|
||||
const Symbol* possiblyBuiltinSymbolRef() {
|
||||
uint16_t id = this->readU16();
|
||||
if (id == kBuiltin_Symbol) {
|
||||
std::string_view name = this->readString();
|
||||
const Symbol* result = (*fSymbolTable)[name];
|
||||
SkASSERTF(result, "symbol '%s' not found", std::string(name).c_str());
|
||||
return result;
|
||||
}
|
||||
SkASSERT(fSymbols.size() > id);
|
||||
return fSymbols[id];
|
||||
}
|
||||
|
||||
Layout layout();
|
||||
|
||||
Modifiers modifiers();
|
||||
@ -244,6 +263,7 @@ private:
|
||||
|
||||
ModifiersPool& modifiersPool() const { return *fContext->fModifiersPool; }
|
||||
|
||||
Compiler& fCompiler;
|
||||
std::shared_ptr<Context> fContext;
|
||||
std::shared_ptr<SymbolTable> fSymbolTable;
|
||||
std::vector<const Symbol*> fSymbols;
|
||||
|
@ -1,4 +1,4 @@
|
||||
static uint8_t SKSL_INCLUDE_sksl_frag[] = {6,0,96,0,
|
||||
static uint8_t SKSL_INCLUDE_sksl_frag[] = {7,0,96,0,
|
||||
12,115,107,95,70,114,97,103,67,111,111,114,100,
|
||||
6,102,108,111,97,116,52,
|
||||
12,115,107,95,67,108,111,99,107,119,105,115,101,
|
||||
@ -7,52 +7,52 @@ static uint8_t SKSL_INCLUDE_sksl_frag[] = {6,0,96,0,
|
||||
5,104,97,108,102,52,
|
||||
16,115,107,95,76,97,115,116,70,114,97,103,67,111,108,111,114,
|
||||
21,115,107,95,83,101,99,111,110,100,97,114,121,70,114,97,103,67,111,108,111,114,
|
||||
50,1,5,0,
|
||||
54,1,0,
|
||||
51,1,5,0,
|
||||
55,1,0,
|
||||
37,
|
||||
36,0,2,0,0,255,255,255,255,255,255,255,15,0,255,16,2,0,
|
||||
51,2,0,15,0,0,
|
||||
54,3,0,
|
||||
52,2,0,15,0,0,
|
||||
55,3,0,
|
||||
37,
|
||||
36,0,2,0,0,255,255,255,255,255,255,255,17,0,255,16,22,0,
|
||||
51,4,0,35,0,0,
|
||||
54,5,0,
|
||||
52,4,0,35,0,0,
|
||||
55,5,0,
|
||||
37,
|
||||
36,144,2,0,0,0,255,255,255,255,0,255,17,39,255,32,40,0,
|
||||
51,6,0,53,0,0,
|
||||
54,7,0,
|
||||
52,6,0,53,0,0,
|
||||
55,7,0,
|
||||
37,
|
||||
36,0,2,0,0,255,255,255,255,255,255,255,24,39,255,0,59,0,
|
||||
49,6,0,0,
|
||||
54,8,0,
|
||||
50,6,0,0,
|
||||
55,8,0,
|
||||
37,
|
||||
36,0,2,0,0,255,255,255,255,255,255,255,28,39,255,32,76,0,
|
||||
49,6,0,0,5,0,
|
||||
50,6,0,0,5,0,
|
||||
1,0,
|
||||
2,0,
|
||||
0,0,
|
||||
3,0,
|
||||
4,0,
|
||||
20,
|
||||
56,
|
||||
55,1,0,
|
||||
49,2,0,0,
|
||||
58,
|
||||
56,
|
||||
55,3,0,
|
||||
49,4,0,0,
|
||||
58,
|
||||
56,
|
||||
55,5,0,
|
||||
49,6,0,0,
|
||||
58,
|
||||
56,
|
||||
55,7,0,
|
||||
49,6,0,0,
|
||||
58,
|
||||
56,
|
||||
55,8,0,
|
||||
49,6,0,0,
|
||||
58,
|
||||
57,
|
||||
56,1,0,
|
||||
50,2,0,0,
|
||||
59,
|
||||
57,
|
||||
56,3,0,
|
||||
50,4,0,0,
|
||||
59,
|
||||
57,
|
||||
56,5,0,
|
||||
50,6,0,0,
|
||||
59,
|
||||
57,
|
||||
56,7,0,
|
||||
50,6,0,0,
|
||||
59,
|
||||
57,
|
||||
56,8,0,
|
||||
50,6,0,0,
|
||||
59,
|
||||
21,};
|
||||
static constexpr size_t SKSL_INCLUDE_sksl_frag_LENGTH = sizeof(SKSL_INCLUDE_sksl_frag);
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,16 +1,16 @@
|
||||
static uint8_t SKSL_INCLUDE_sksl_rt_shader[] = {6,0,20,0,
|
||||
static uint8_t SKSL_INCLUDE_sksl_rt_shader[] = {7,0,20,0,
|
||||
12,115,107,95,70,114,97,103,67,111,111,114,100,
|
||||
6,102,108,111,97,116,52,
|
||||
50,1,1,0,
|
||||
54,1,0,
|
||||
51,1,1,0,
|
||||
55,1,0,
|
||||
37,
|
||||
36,0,2,0,0,255,255,255,255,255,255,255,15,0,255,0,2,0,
|
||||
51,2,0,15,0,0,1,0,
|
||||
52,2,0,15,0,0,1,0,
|
||||
0,0,
|
||||
20,
|
||||
56,
|
||||
55,1,0,
|
||||
49,2,0,0,
|
||||
58,
|
||||
57,
|
||||
56,1,0,
|
||||
50,2,0,0,
|
||||
59,
|
||||
21,};
|
||||
static constexpr size_t SKSL_INCLUDE_sksl_rt_shader_LENGTH = sizeof(SKSL_INCLUDE_sksl_rt_shader);
|
||||
|
@ -1,4 +1,4 @@
|
||||
static uint8_t SKSL_INCLUDE_sksl_vert[] = {6,0,82,0,
|
||||
static uint8_t SKSL_INCLUDE_sksl_vert[] = {7,0,82,0,
|
||||
12,115,107,95,80,101,114,86,101,114,116,101,120,
|
||||
11,115,107,95,80,111,115,105,116,105,111,110,
|
||||
6,102,108,111,97,116,52,
|
||||
@ -8,42 +8,42 @@ static uint8_t SKSL_INCLUDE_sksl_vert[] = {6,0,82,0,
|
||||
3,105,110,116,
|
||||
13,115,107,95,73,110,115,116,97,110,99,101,73,68,
|
||||
0,
|
||||
50,1,6,0,
|
||||
46,1,0,2,0,2,
|
||||
51,1,6,0,
|
||||
47,1,0,2,0,2,
|
||||
37,
|
||||
36,0,2,0,0,255,255,255,255,255,255,255,0,0,255,0,15,0,
|
||||
51,2,0,27,0,
|
||||
52,2,0,27,0,
|
||||
37,
|
||||
36,0,2,0,0,255,255,255,255,255,255,255,1,0,255,0,34,0,
|
||||
51,3,0,47,0,1,
|
||||
54,4,0,
|
||||
52,3,0,47,0,1,
|
||||
55,4,0,
|
||||
37,
|
||||
16,32,2,0,
|
||||
49,1,0,0,
|
||||
50,1,0,0,
|
||||
23,4,0,0,
|
||||
23,4,0,1,
|
||||
54,5,0,
|
||||
55,7,0,
|
||||
37,
|
||||
36,0,2,0,0,255,255,255,255,255,255,255,42,0,255,16,53,0,
|
||||
51,6,0,65,0,0,
|
||||
54,7,0,
|
||||
52,8,0,65,0,0,
|
||||
55,9,0,
|
||||
37,
|
||||
36,0,2,0,0,255,255,255,255,255,255,255,43,0,255,16,69,0,
|
||||
49,6,0,0,4,0,
|
||||
50,8,0,0,4,0,
|
||||
5,0,
|
||||
3,0,
|
||||
2,0,
|
||||
4,0,
|
||||
20,
|
||||
34,
|
||||
49,4,0,2,0,83,0,0,
|
||||
56,
|
||||
55,5,0,
|
||||
49,6,0,0,
|
||||
58,
|
||||
56,
|
||||
55,7,0,
|
||||
49,6,0,0,
|
||||
58,
|
||||
50,4,0,2,0,83,0,0,
|
||||
57,
|
||||
56,7,0,
|
||||
50,8,0,0,
|
||||
59,
|
||||
57,
|
||||
56,9,0,
|
||||
50,8,0,0,
|
||||
59,
|
||||
21,};
|
||||
static constexpr size_t SKSL_INCLUDE_sksl_vert_LENGTH = sizeof(SKSL_INCLUDE_sksl_vert);
|
||||
|
@ -46,6 +46,11 @@ public:
|
||||
const FunctionDeclaration& function,
|
||||
ExpressionArray arguments);
|
||||
|
||||
static const FunctionDeclaration* FindBestFunctionForCall(
|
||||
const Context& context,
|
||||
const std::vector<const FunctionDeclaration*>& functions,
|
||||
const ExpressionArray& arguments);
|
||||
|
||||
const FunctionDeclaration& function() const {
|
||||
return fFunction;
|
||||
}
|
||||
@ -69,11 +74,6 @@ private:
|
||||
const FunctionDeclaration& function,
|
||||
const ExpressionArray& arguments);
|
||||
|
||||
static const FunctionDeclaration* FindBestFunctionForCall(
|
||||
const Context& context,
|
||||
const std::vector<const FunctionDeclaration*>& functions,
|
||||
const ExpressionArray& arguments);
|
||||
|
||||
const FunctionDeclaration& fFunction;
|
||||
ExpressionArray fArguments;
|
||||
|
||||
|
@ -237,7 +237,7 @@ static void test_rehydrate(skiatest::Reporter* r, const char* testFile) {
|
||||
|
||||
SkSL::Rehydrator rehydrator(compiler, (const uint8_t*) stream.str().data(),
|
||||
stream.str().length());
|
||||
std::unique_ptr<SkSL::Program> rehydrated = rehydrator.program(&program->fSharedElements);
|
||||
std::unique_ptr<SkSL::Program> rehydrated = rehydrator.program();
|
||||
REPORTER_ASSERT(r, rehydrated->description() == program->description(),
|
||||
"Mismatch between original and dehydrated/rehydrated:\n-- Original:\n%s\n"
|
||||
"-- Rehydrated:\n%s", program->description().c_str(),
|
||||
|
Loading…
Reference in New Issue
Block a user