LCOV - code coverage report
Current view: top level - source/graphics - PreprocessorWrapper.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 108 138 78.3 %
Date: 2023-01-19 00:18:29 Functions: 10 14 71.4 %

          Line data    Source code
       1             : /* Copyright (C) 2021 Wildfire Games.
       2             :  * This file is part of 0 A.D.
       3             :  *
       4             :  * 0 A.D. is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 2 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * 0 A.D. is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #include "precompiled.h"
      19             : 
      20             : #include "PreprocessorWrapper.h"
      21             : 
      22             : #include "graphics/ShaderDefines.h"
      23             : #include "ps/CLogger.h"
      24             : #include "ps/Profile.h"
      25             : 
      26             : #include <cctype>
      27             : #include <deque>
      28             : #include <string>
      29             : #include <string_view>
      30             : #include <vector>
      31             : 
      32             : namespace
      33             : {
      34             : 
      35             : struct MatchIncludeResult
      36             : {
      37             :     bool found;
      38             :     bool error;
      39             :     size_t nextLineStart;
      40             :     size_t pathFirst, pathLast;
      41             : 
      42         441 :     static MatchIncludeResult MakeNotFound(const std::string_view& source, size_t pos)
      43             :     {
      44         825 :         while (pos < source.size() && source[pos] != '\n')
      45         384 :             ++pos;
      46             :         return MatchIncludeResult{
      47          57 :             false, false, pos < source.size() ? pos + 1 : source.size(), 0, 0};
      48             :     }
      49             : 
      50           1 :     static MatchIncludeResult MakeError(
      51             :         const char* message, const std::string_view& source, const size_t lineStart, const size_t currentPos)
      52             :     {
      53           1 :         ENSURE(currentPos >= lineStart);
      54           1 :         size_t lineEnd = currentPos;
      55          17 :         while (lineEnd < source.size() && source[lineEnd] != '\n' && source[lineEnd] != '\r')
      56           8 :             ++lineEnd;
      57           1 :         const std::string_view line = source.substr(lineStart, lineEnd - lineStart);
      58           1 :         while (lineEnd < source.size() && source[lineEnd] != '\n')
      59           0 :             ++lineEnd;
      60           1 :         const size_t nextLineStart = lineEnd < source.size() ? lineEnd + 1 : source.size();
      61           1 :         LOGERROR("Preprocessor error: %s: '%s'\n", message, std::string(line).c_str());
      62           1 :         return MatchIncludeResult{false, true, nextLineStart, 0, 0};
      63             :     }
      64             : };
      65             : 
      66          67 : MatchIncludeResult MatchIncludeUntilEOLorEOS(const std::string_view& source, const size_t lineStart)
      67             : {
      68             :     // We need to match a line like this:
      69             :     // ^[ \t]*#[ \t]*include[ \t]*"[^"]+".*$
      70             :     //  ^     ^^     ^      ^     ^^    ^
      71             :     //  1     23     4      5     67    8    <- steps
      72         134 :     const CStr INCLUDE = "include";
      73          67 :     size_t pos = lineStart;
      74             :     // Matching step #1.
      75         329 :     while (pos < source.size() && std::isblank(source[pos]))
      76         131 :         ++pos;
      77             :     // Matching step #2.
      78          67 :     if (pos == source.size() || source[pos] != '#')
      79          19 :         return MatchIncludeResult::MakeNotFound(source, pos);
      80          48 :     ++pos;
      81             :     // Matching step #3.
      82          48 :     while (pos < source.size() && std::isblank(source[pos]))
      83           0 :         ++pos;
      84             :     // Matching step #4.
      85          48 :     if (pos + INCLUDE.size() >= source.size() || source.substr(pos, INCLUDE.size()) != INCLUDE)
      86          38 :         return MatchIncludeResult::MakeNotFound(source, pos);
      87          10 :     pos += INCLUDE.size();
      88             :     // Matching step #5.
      89          30 :     while (pos < source.size() && std::isblank(source[pos]))
      90          10 :         ++pos;
      91             :     // Matching step #6.
      92          10 :     if (pos == source.size() || source[pos] != '"')
      93           1 :         return MatchIncludeResult::MakeError("#include should be followed by quote", source, lineStart, pos);
      94           9 :     ++pos;
      95             :     // Matching step #7.
      96           9 :     const size_t pathFirst = pos;
      97         131 :     while (pos < source.size() && source[pos] != '"' && source[pos] != '\n')
      98          61 :         ++pos;
      99           9 :     const size_t pathLast = pos;
     100             :     // Matching step #8.
     101           9 :     if (pos == source.size() || source[pos] != '"')
     102           0 :         return MatchIncludeResult::MakeError("#include has invalid quote pair", source, lineStart, pos);
     103           9 :     if (pathLast - pathFirst <= 1)
     104           0 :         return MatchIncludeResult::MakeError("#include path shouldn't be empty", source, lineStart, pos);
     105          73 :     while (pos < source.size() && source[pos] != '\n')
     106          32 :         ++pos;
     107           9 :     return MatchIncludeResult{true, false, pos < source.size() ? pos + 1 : source.size(), pathFirst, pathLast};
     108             : }
     109             : 
     110          14 : bool ResolveIncludesImpl(
     111             :     std::string_view currentPart,
     112             :     std::unordered_map<CStr, CStr>& includeCache, const CPreprocessorWrapper::IncludeRetrieverCallback& includeCallback,
     113             :     std::deque<std::string>& chunks, std::vector<std::string_view>& processedParts)
     114             : {
     115          14 :     static const CStr lineDirective = "#line ";
     116          79 :     for (size_t lineStart = 0, line = 1; lineStart < currentPart.size(); ++line)
     117             :     {
     118          67 :         MatchIncludeResult match = MatchIncludeUntilEOLorEOS(currentPart, lineStart);
     119          67 :         if (match.error)
     120           3 :             return {};
     121          66 :         else if (!match.found)
     122             :         {
     123         111 :             if (lineStart + lineDirective.size() < currentPart.size() &&
     124          54 :                 currentPart.substr(lineStart, lineDirective.size()) == lineDirective)
     125             :             {
     126          16 :                 size_t newLineNumber = 0;
     127          16 :                 size_t pos = lineStart + lineDirective.size();
     128          48 :                 while (pos < match.nextLineStart && std::isdigit(currentPart[pos]))
     129             :                 {
     130          16 :                     newLineNumber = newLineNumber * 10 + (currentPart[pos] - '0');
     131          16 :                     ++pos;
     132             :                 }
     133          16 :                 if (newLineNumber > 0)
     134          16 :                     line = newLineNumber - 1;
     135             :             }
     136             : 
     137          57 :             lineStart = match.nextLineStart;
     138         114 :             continue;
     139             :         }
     140          17 :         const std::string path(currentPart.substr(match.pathFirst, match.pathLast - match.pathFirst));
     141           9 :         auto it = includeCache.find(path);
     142           9 :         if (it == includeCache.end())
     143             :         {
     144           9 :             CStr includeContent;
     145           5 :             if (!includeCallback(path, includeContent))
     146             :             {
     147           1 :                 LOGERROR("Preprocessor error: line %zu: Can't load #include file: '%s'", line, path.c_str());
     148           1 :                 return false;
     149             :             }
     150           4 :             it = includeCache.emplace(path, std::move(includeContent)).first;
     151             :         }
     152             :         // We need to insert #line directives to have correct line numbers in errors.
     153           8 :         chunks.emplace_back(lineDirective + "1\n" + it->second + "\n" + lineDirective + CStr::FromUInt(line + 1) + "\n");
     154           8 :         processedParts.emplace_back(currentPart.substr(0, lineStart));
     155           8 :         if (!ResolveIncludesImpl(chunks.back(), includeCache, includeCallback, chunks, processedParts))
     156           0 :             return false;
     157           8 :         currentPart = currentPart.substr(match.nextLineStart);
     158           8 :         lineStart = 0;
     159             :     }
     160          12 :     if (!currentPart.empty())
     161          11 :         processedParts.emplace_back(currentPart);
     162          12 :     return true;
     163             : }
     164             : 
     165             : } // anonymous namespace
     166             : 
     167           2 : void CPreprocessorWrapper::PyrogenesisShaderError(int iLine, const char* iError, const Ogre::CPreprocessor::Token* iToken)
     168             : {
     169           2 :     if (iToken)
     170           0 :         LOGERROR("Preprocessor error: line %d: %s: '%s'\n", iLine, iError, std::string(iToken->String, iToken->Length).c_str());
     171             :     else
     172           2 :         LOGERROR("Preprocessor error: line %d: %s\n", iLine, iError);
     173           2 : }
     174             : 
     175           0 : CPreprocessorWrapper::CPreprocessorWrapper()
     176           0 :     : CPreprocessorWrapper(IncludeRetrieverCallback{})
     177             : {
     178           0 : }
     179             : 
     180           6 : CPreprocessorWrapper::CPreprocessorWrapper(const IncludeRetrieverCallback& includeCallback)
     181           6 :     : m_IncludeCallback(includeCallback)
     182             : {
     183           6 :     Ogre::CPreprocessor::ErrorHandler = CPreprocessorWrapper::PyrogenesisShaderError;
     184           6 : }
     185             : 
     186           0 : void CPreprocessorWrapper::AddDefine(const char* name, const char* value)
     187             : {
     188           0 :     m_Preprocessor.Define(name, strlen(name), value, strlen(value));
     189           0 : }
     190             : 
     191           0 : void CPreprocessorWrapper::AddDefines(const CShaderDefines& defines)
     192             : {
     193           0 :     std::map<CStrIntern, CStrIntern> map = defines.GetMap();
     194           0 :     for (std::map<CStrIntern, CStrIntern>::const_iterator it = map.begin(); it != map.end(); ++it)
     195           0 :         m_Preprocessor.Define(it->first.c_str(), it->first.length(), it->second.c_str(), it->second.length());
     196           0 : }
     197             : 
     198           0 : bool CPreprocessorWrapper::TestConditional(const CStr& expr)
     199             : {
     200             :     // Construct a dummy program so we can trigger the preprocessor's expression
     201             :     // code without modifying its public API.
     202             :     // Be careful that the API buggily returns a statically allocated pointer
     203             :     // (which we will try to free()) if the input just causes it to append a single
     204             :     // sequence of newlines to the output; the "\n" after the "#endif" is enough
     205             :     // to avoid this case.
     206           0 :     CStr input = "#if ";
     207           0 :     input += expr;
     208           0 :     input += "\n1\n#endif\n";
     209             : 
     210           0 :     size_t len = 0;
     211           0 :     char* output = m_Preprocessor.Parse(input.c_str(), input.size(), len);
     212             : 
     213           0 :     if (!output)
     214             :     {
     215           0 :         LOGERROR("Failed to parse conditional expression '%s'", expr.c_str());
     216           0 :         return false;
     217             :     }
     218             : 
     219           0 :     bool ret = (memchr(output, '1', len) != NULL);
     220             : 
     221             :     // Free output if it's not inside the source string
     222           0 :     if (!(output >= input.c_str() && output < input.c_str() + input.size()))
     223           0 :         free(output);
     224             : 
     225           0 :     return ret;
     226             : 
     227             : }
     228             : 
     229           6 : CStr CPreprocessorWrapper::ResolveIncludes(const CStr& source)
     230             : {
     231             :     // Stores intermediate blocks of text to avoid additional copying. Should
     232             :     // be constructed before views and destroyed after (currently guaranteed
     233             :     // by stack).
     234             :     // Short String Optimisation can make views point to container-managed data,
     235             :     // so push_back must not invalidate pointers (std::deque guarantees that).
     236          12 :     std::deque<std::string> chunks;
     237             :     // After resolving the following vector should contain a complete list
     238             :     // to concatenate.
     239          12 :     std::vector<std::string_view> processedParts;
     240           6 :     if (!ResolveIncludesImpl(source, m_IncludeCache, m_IncludeCallback, chunks, processedParts))
     241           2 :         return {};
     242           4 :     std::size_t totalSize = 0;
     243          23 :     for (const std::string_view& part : processedParts)
     244          19 :         totalSize += part.size();
     245           8 :     std::string processedSource;
     246           4 :     processedSource.reserve(totalSize);
     247          23 :     for (const std::string_view& part : processedParts)
     248          19 :         processedSource.append(part);
     249           4 :     return processedSource;
     250             : }
     251             : 
     252           6 : CStr CPreprocessorWrapper::Preprocess(const CStr& input)
     253             : {
     254          12 :     PROFILE("Preprocess shader source");
     255             : 
     256          12 :     CStr source = ResolveIncludes(input);
     257             : 
     258           6 :     size_t len = 0;
     259           6 :     char* output = m_Preprocessor.Parse(source.c_str(), source.size(), len);
     260             : 
     261           6 :     if (!output)
     262             :     {
     263           2 :         LOGERROR("Shader preprocessing failed");
     264           2 :         return "";
     265             :     }
     266             : 
     267           8 :     CStr ret(output, len);
     268             : 
     269             :     // Free output if it's not inside the source string
     270           4 :     if (!(output >= source.c_str() && output < source.c_str() + source.size()))
     271           2 :         free(output);
     272             : 
     273           4 :     return ret;
     274           3 : }

Generated by: LCOV version 1.13