/*
* Copyright 2019 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/utils/SkShaderUtils.h"
#include "include/core/SkString.h"
#include "include/private/base/SkTArray.h"
#include "src/core/SkStringUtils.h"
#include "src/sksl/SkSLProgramSettings.h"
#include "src/sksl/SkSLString.h"
#include <cstddef>
#include <iomanip>
#include <sstream>
using
namespace skia_private;
namespace SkShaderUtils {
class GLSLPrettyPrint {
public:
GLSLPrettyPrint() {}
std::string prettify(
const std::string& string) {
fTabs =
0;
fFreshline = true;
// If a string breaks while in the middle 'parse until' we need to continue parsing on the
// next string
fInParseUntilNewline =
false;
fInParseUntil =
false;
int parensDepth =
0;
// setup pretty state
fIndex =
0;
fLength = string.length();
fInput = string.c_str();
while (fLength > fIndex) {
/* The heart and soul of our prettification algorithm. The rules should hopefully
* be self explanatory. For '#' and '//' tokens, we parse until we reach a newline.
*
* For long style comments like this one, we search for the ending token. We also
* preserve whitespace in these comments WITH THE CAVEAT that we do the newlines
* ourselves. This allows us to remain in control of line numbers, and matching
* tabs. Existing tabs in the input string are copied over too, but this will look
* funny.
*
* '{' and '}' are handled in basically the same way. We add a newline if we aren't
* on a fresh line, dirty the line, then add a second newline, i.e. braces are always
* on their own lines indented properly.
*
* '(' and ')' are basically ignored, except as a sign that we need to ignore ';', since
* we want to keep for loops on a single line.
*
* ';' means add a new line. If the previous character was a '}', we make sure that the
* semicolon comes directly after the brace, not on a newline.
*
* ',' doesn't add a new line, but does have special handling to ensure it is on the
* same line as a '}', much like the semicolon.
*
* '\t' and '\n' are ignored in general parsing for backwards compatibility with
* existing shader code. We also have a special case for handling whitespace at the
* beginning of fresh lines.
*
* Otherwise, just add the new character to the pretty string, indenting if
* necessary.
*/
if (fInParseUntilNewline) {
this->parseUntilNewline();
continue;
}
if (fInParseUntil) {
this->parseUntil(fInParseUntilToken);
continue;
}
if (this->hasToken(
"#") || this->hasToken(
"//")) {
this->parseUntilNewline();
continue;
}
if (this->hasToken(
"/*")) {
this->parseUntil(
"*/");
continue;
}
if (fInput[fIndex] ==
'{') {
this->newline();
this->appendChar(
'{');
fTabs++;
this->newline();
continue;
}
if (fInput[fIndex] ==
'}') {
fTabs--;
this->newline();
this->appendChar(
'}');
this->newline();
continue;
}
if (fFreshline && fInput[fIndex] ==
';') {
this->undoNewlineAfter(
'}');
this->appendChar(fInput[fIndex]);
this->newline();
continue;
}
if (fFreshline && fInput[fIndex] ==
',') {
this->undoNewlineAfter(
'}');
this->appendChar(fInput[fIndex]);
continue;
}
if (this->hasToken(
")")) {
parensDepth--;
continue;
}
if (this->hasToken(
"(")) {
parensDepth++;
continue;
}
if (this->hasToken(
")")) {
parensDepth--;
continue;
}
if (!parensDepth && this->hasToken(
";")) {
this->newline();
continue;
}
if (fInput[fIndex] ==
'\t' || fInput[fIndex] ==
'\n' ||
(fFreshline && fInput[fIndex] ==
' ')) {
fIndex++;
continue;
}
this->appendChar(fInput[fIndex]);
}
return fPretty;
}
private:
void appendChar(
char c) {
this->tabString();
fPretty += fInput[fIndex++];
fFreshline =
false;
}
// hasToken automatically consumes the next token, if it is a match, and then tabs
// if necessary, before inserting the token into the pretty string
bool hasToken(
const char* token) {
size_t i = fIndex;
for (size_t j =
0; token[j] && fLength > i; i++, j++) {
if (token[j] != fInput[i]) {
return false;
}
}
this->tabString();
fIndex = i;
fPretty.append(token);
fFreshline =
false;
return true;
}
void parseUntilNewline() {
while (fLength > fIndex) {
if (fInput[fIndex] ==
'\n') {
fIndex++;
this->newline();
fInParseUntilNewline =
false;
break;
}
fPretty += fInput[fIndex++];
fInParseUntilNewline = true;
}
}
// this code assumes it is not actually searching for a newline. If you need to search for a
// newline, then use the function above. If you do search for a newline with this function
// it will consume the entire string and the output will certainly not be prettified
void parseUntil(
const char* token) {
while (fLength > fIndex) {
// For embedded newlines, this code will make sure to embed the newline in the
// pretty string, increase the linecount, and tab out the next line to the appropriate
// place
if (fInput[fIndex] ==
'\n') {
this->newline();
this->tabString();
fIndex++;
}
if (this->hasToken(token)) {
fInParseUntil =
false;
break;
}
fFreshline =
false;
fPretty += fInput[fIndex++];
fInParseUntil = true;
fInParseUntilToken = token;
}
}
// We only tab if on a newline, otherwise consider the line tabbed
void tabString() {
if (fFreshline) {
for (
int t =
0; t < fTabs; t++) {
fPretty +=
'\t';
}
}
}
// newline is really a request to add a newline, if we are on a fresh line there is no reason
// to add another newline
void newline() {
if (!fFreshline) {
fFreshline = true;
fPretty +=
'\n';
}
}
// undoNewlineAfter() attempts to undo the effects of newline(), if the last character before
// the newline matches `c`.
void undoNewlineAfter(
char c) {
if (fFreshline) {
if (fPretty.size() >=
2 && fPretty.rbegin()[
0] ==
'\n' && fPretty.rbegin()[
1] == c) {
fFreshline =
false;
fPretty.pop_back();
}
}
}
bool fFreshline;
int fTabs;
size_t fIndex, fLength;
const char* fInput;
std::string fPretty;
// Some helpers for parseUntil when we go over a string length
bool fInParseUntilNewline;
bool fInParseUntil;
const char* fInParseUntilToken;
};
std::string PrettyPrint(
const std::string& string) {
GLSLPrettyPrint pp;
return pp.prettify(string);
}
void VisitLineByLine(
const std::string& text,
const std::function<
void(
int lineNumber,
const char* lineText)>& visitFn) {
TArray<SkString> lines;
SkStrSplit(text.c_str(),
"\n", kStrict_SkStrSplitMode, &lines);
for (
int i =
0; i < lines.size(); ++i) {
visitFn(i +
1, lines[i].c_str());
}
}
std::string SpirvAsHexStream(SkSpan<
const uint32_t> spirv) {
std::ostringstream result;
result <<
"Paste the following SPIR-V binary in https://www.khronos.org/spir/visualizer/\n";
result <<
" or pass to `spirv-dis` (optionally with `--comment --nested-indent`)\n";
constexpr size_t kIndicesPerRow =
10;
size_t rowOffset =
0;
for (size_t index =
0; index < spirv.size(); ++index, ++rowOffset) {
if (rowOffset == kIndicesPerRow) {
result <<
"\n";
rowOffset =
0;
}
result <<
"0x" << std::uppercase << std::setfill(
'0') << std::setw(
8) << std::hex
<< spirv[index] <<
",";
}
return result.str();
}
std::string BuildShaderErrorMessage(
const char* shader,
const char* errors) {
std::string abortText{
"Shader compilation error\n"
"------------------------\n"};
VisitLineByLine(shader, [&](
int lineNumber,
const char* lineText)
{
SkSL::String::appendf(&abortText, "%4i\t%s\n", lineNumber, lineText);
});
SkSL::String::appendf(&abortText, "Errors:\n%s", errors);
return abortText;
}
void PrintShaderBanner(SkSL::ProgramKind programKind) {
const char* typeName = "Unknown";
if (SkSL::ProgramConfig::IsVertex(programKind)) {
typeName = "Vertex";
} else if (SkSL::ProgramConfig::IsFragment(programKind)) {
typeName = "Fragment";
}
SkDebugf("---- %s shader ----------------------------------------------------\n", typeName);
}
} // namespace SkShaderUtils