Cogs.Core
ShaderBuilderPostProcessWebGPU.cpp
1#include "ShaderBuilderPostProcess.h"
2#include "Context.h"
3#include "Utilities/Strings.h"
4#include "Utilities/Preprocessor.h"
5#include "ResourceStore.h"
6#include "Renderer/EnginePermutations.h"
7#include "Renderer/Renderer.h"
8#include "Rendering/IGraphicsDevice.h"
9#include "Renderer/Tasks/PostProcessTask.h"
10#include "Foundation/Logging/Logger.h"
11#include "Rendering/IContext.h"
12#include "Rendering/IBuffers.h"
13#include "Renderer/IRenderer.h"
14#include "Renderer/RenderTarget.h"
15
16#include <glm/gtc/type_ptr.hpp>
17
18#include <array>
19#include <sstream>
20#include <map>
21
22
23namespace {
24 constexpr size_t InitialBufferCapasity = 4096u;
25
26 const Cogs::Logging::Log logger = Cogs::Logging::getLogger("ShaderBuilderPostProcessWebGPU");
27
28 constexpr std::array engineSamplers
29 {
30 "linearSampler",
31 "linearClampSampler",
32 "pointSampler",
33 "pointClampSampler",
34 };
35
36 void changeSuffix(std::string& dst, const std::string_view& from, const std::string_view& to)
37 {
38 auto pos = dst.find(from);
39 if (pos == std::string::npos) return;
40 dst.replace(pos, to.length(), to);
41 }
42
43 void removeSuffix(std::string& dst, const std::string_view& suffix)
44 {
45 auto pos = dst.find(suffix);
46 if (pos == std::string::npos) return;
47 dst.erase(pos);
48 }
49 using namespace Cogs::Core;
50 [[nodiscard]] std::string_view parameterType(const ParsedDataType type)
51 {
52 switch (type) {
53 case ParsedDataType::Float: return "f32"; break;
54 case ParsedDataType::Float2: return "vec2f"; break;
55 case ParsedDataType::Float3: return "vec3f"; break;
56 case ParsedDataType::Float4: return "vec4f"; break;
57 case ParsedDataType::Float4x4: return "mat4x4f"; break;
58 case ParsedDataType::Int: return "i32"; break;
59 case ParsedDataType::Int2: return "vec2i"; break;
60 case ParsedDataType::Int3: return "vec3i"; break;
61 case ParsedDataType::Int4: return "vec4i"; break;
62 case ParsedDataType::UInt: return "u32"; break;
63 case ParsedDataType::UInt2: return "vec2u"; break;
64 case ParsedDataType::UInt3: return "vec3u"; break;
65 case ParsedDataType::UInt4: return "vec4u"; break;
66 default:
67 LOG_ERROR(logger, "Unsupported attribute type %d", int(type));
68 return "<illegal>";
69 break;
70 }
71 }
72
73 [[nodiscard]]
74 std::string convertDefinesToConstExpressions(const std::string& s) {
75 std::map<std::string, std::string> addedDefines;
76 std::string result;
77 result.reserve(s.size());
78
79 std::istringstream iss(s);
80
81 std::string identifier;
82 std::string replacement;
83 for (std::string line; std::getline(iss, line); )
84 {
85 identifier.clear();
86 replacement.clear();
87
88 std::string_view sv(line);
89 auto pos = sv.find_first_not_of(" \t");
90 if (pos == std::string_view::npos || sv.substr(pos, 7) != "#define") {
91 result += line + "\n";
92 continue;
93 }
94 sv = sv.substr(pos + 7);
95
96 pos = sv.find_first_not_of(" \t");
97 if (pos == std::string_view::npos) {
98 result += line + "\n";
99 continue;
100 }
101 sv = sv.substr(pos);
102
103 auto end = sv.find_first_of(" \t");
104 if (end == std::string_view::npos) {
105 identifier = sv;
106 replacement = "1";
107 } else {
108 identifier = sv.substr(0, end);
109 sv = sv.substr(end);
110 pos = sv.find_first_not_of(" \t");
111 if (pos == std::string_view::npos) {
112 replacement = "1";
113 } else {
114 auto last = sv.find_last_not_of(" \t");
115 replacement = sv.substr(pos, last - pos + 1);
116 }
117 }
118
119 auto it = addedDefines.find(identifier);
120 if (it != addedDefines.end()) {
121 if (replacement != (*it).second) {
122 LOG_WARNING(logger, "Shader generation with inconsistent preprosessor define %s set to %s conflicts with previous define %s.", identifier.c_str(), replacement.c_str(), (*it).second.c_str());
123 }
124 continue;
125 }
126 result += "const " + identifier + " = " + replacement + ";\n";
127 addedDefines.try_emplace(identifier, replacement);
128 }
129 return result;
130 }
131
132 void addInterface(std::string& src) {
133 src.append(
134 R"(
135struct VertexIn {
136 @builtin(position) position: vec4f,
137 @location(0) TexCoords: vec2f,
138 @location(1) NormalizedCoords: vec2f,
139};
140
141)");
142 }
143
144 void addDefine(std::string& output, const std::string &name, const std::string &value)
145 {
146 output.append("#define ");
147 output.append(name);
148 output.append(" ");
149 output.append(value);
150 output.append("\n");
151 }
152
153 void addDefines(std::string& output, const std::vector<std::pair<std::string, std::string>>& definitions)
154 {
155 for (const std::pair<std::string, std::string>& define : definitions) {
156 addDefine(output, define.first, define.second);
157 }
158 }
159
160 void addUniforms(std::string& src, const std::vector<ProcessTaskProperty>& properties, int group, int& bindings) {
161 std::string parameterUBOsrc;
162
163 bool hasParameters = false;
164 parameterUBOsrc.append("");
165 parameterUBOsrc.append("struct EffectParameters_t {\n");
166
167 for (const ProcessTaskProperty& p : properties) {
168 switch (p.definition->type) {
169 case ParsedDataType::Float:
170 case ParsedDataType::Float2:
171 case ParsedDataType::Float3:
172 case ParsedDataType::Float4:
173 case ParsedDataType::Float4x4:
174 case ParsedDataType::Int:
175 case ParsedDataType::Int2:
176 case ParsedDataType::Int3:
177 case ParsedDataType::Int4:
178 case ParsedDataType::UInt:
179 case ParsedDataType::UInt2:
180 case ParsedDataType::UInt3:
181 case ParsedDataType::UInt4:
182 parameterUBOsrc.append(" " + p.definition->key + " : ");
183 parameterUBOsrc.append(parameterType(p.definition->type));
184 parameterUBOsrc.append(",\n");
185 hasParameters = true;
186 break;
187
188 case ParsedDataType::Texture2D:
189 {
190 std::string texType = "texture";
191 if ((p.definition->texture.flags & ParsedValueTextureFlags::DepthTexture) != 0) {
192 texType.append("_depth");
193 }
194 if (p.definition->texture.samples > 1) {
195 texType.append("_multisampled");
196 LOG_ERROR(logger, "Sampling from multisampled textures is not allowed on this platform, well, it is, just not implemented");
197 }
198 texType.append("_2d");
199 std::string_view texture_template = "<f32>";
200 switch (p.definition->texture.dataType) {
201 case ParsedDataType::Int:
202 case ParsedDataType::Int2:
203 case ParsedDataType::Int3:
204 case ParsedDataType::Int4:
205 texture_template = "<i32>";
206 break;
207 case ParsedDataType::UInt:
208 case ParsedDataType::UInt2:
209 case ParsedDataType::UInt3:
210 case ParsedDataType::UInt4:
211 texture_template = "<u32>";
212 break;
213 default:
214 break;
215 }
216 if ((p.definition->texture.flags & ParsedValueTextureFlags::DepthTexture) == 0) { // depth textures are always f32 and must be declared without template argument
217 texType.append(texture_template);
218 }
219 src.append("@group(");
220 src.append(std::to_string(group));
221 src.append(") @binding(");
222 src.append(std::to_string(++bindings));
223 src.append(") var");
224 if (false) {
225 src.append("<uniform>");
226 }
227 src.append(" ");
228 src.append(p.definition->key);
229 src.append(" : " + texType);
230 src.append(";\n");
231
232 // Not sure if we will support these in the future, seems to only take extra resources when the effect author knows which sampler to use
233 src.append("@group(");
234 src.append(std::to_string(group));
235 src.append(") @binding(");
236 src.append(std::to_string(++bindings));
237 src.append(") var");
238 if (false) {
239 src.append("<uniform>");
240 }
241 src.append(" ");
242 src.append(p.definition->key);
243 src.append("Sampler : sampler");
244 src.append(";\n");
245 }
246 break;
247 case ParsedDataType::Buffer:
248 LOG_ERROR(logger, "Render buffers not supported on this platform");
249 break;
250 case ParsedDataType::ConstantBuffer:
251 if (p.definition->key == "SceneBuffer") {
252 src.append("@group(" + std::to_string(group) + ") @binding(" + std::to_string(++bindings) +") var<uniform> SceneBuffer : SceneBuffer_t; \n");
253 src.append("#define COGS_VIEWGETTERS_REFERENCED 1\n");
254 }
255 else if (p.definition->value.starts_with("Cogs.")) {
256 std::string bufferName = p.definition->value.substr(5);
257 src.append("#include \"Engine/" + bufferName + ".wgsl\"\n");
258 src.append("@group(" + std::to_string(group) + ") @binding(" + std::to_string(++bindings) + ") var<uniform> " + p.definition->key + " : " + bufferName + "_t; \n");
259 }
260 break;
261 default:
262 break;
263 }
264 }
265 parameterUBOsrc.append("};\n");
266 if (hasParameters) {
267 src.append(parameterUBOsrc);
268 src.append("@group(" + std::to_string(group) + ") @binding(" + std::to_string(++bindings) + ") var<uniform> EffectParameters : EffectParameters_t;\n");
269 }
270 }
271
272 void addEngineSamplers(std::string& src/*, const std::unordered_set<Cogs::Core::StringRef>& identifiersSeen*/, int group, int& bindings) {
273 for (auto sampler : engineSamplers) {
274 src.append("@group(" + std::to_string(group) + ") @binding(" + std::to_string(++bindings) + ") var " + sampler + " : sampler;\n");
275 }
276 }
277
278 void addOutputStruct(std::string& src, const PipelineOptions& options, bool writeDepth) {
279 bool hasCustomTargets = false;
280 std::string prefix = "target_";
281 src.append("struct FragmentOut {\n");
282 if (writeDepth) {
283 src.append(" @builtin(frag_depth) fragDepth: f32,\n");
284 }
285 for (auto o : options) {
286 auto key = o.first;
287
288 for (auto& c : key) {
289 c = static_cast<decltype(key)::value_type>(std::tolower(c));
290 }
291 if (key.substr(0, prefix.size()) == prefix) {
292 if (!hasCustomTargets) {
293 hasCustomTargets = true;
294 }
295 int location = std::stoi(key.substr(prefix.size()));
296 TokenStream tokens;
297 Cogs::Core::split(o.second, " ", tokens);
298 if (tokens.size() != 2) {
299 LOG_ERROR(logger, "Unable to parse pipeline output option %s", o.second.c_str());
300 continue;
301 }
302 std::string_view datatype = "Undefined";
303 switch (tokens[1].hashLowercase()) {
304 case Cogs::hash("float"):
305 datatype = parameterType(ParsedDataType::Float);
306 break;
307 case Cogs::hash("float2"):
308 datatype = parameterType(ParsedDataType::Float2);
309 break;
310 case Cogs::hash("float3"):
311 datatype = parameterType(ParsedDataType::Float3);
312 break;
313 case Cogs::hash("float4"):
314 datatype = parameterType(ParsedDataType::Float4);
315 break;
316 case Cogs::hash("uint"):
317 datatype = parameterType(ParsedDataType::UInt);
318 break;
319 case Cogs::hash("uint2"):
320 datatype = parameterType(ParsedDataType::UInt2);
321 break;
322 case Cogs::hash("uint3"):
323 datatype = parameterType(ParsedDataType::UInt3);
324 break;
325 case Cogs::hash("uint4"):
326 datatype = parameterType(ParsedDataType::UInt4);
327 break;
328 case Cogs::hash("int"):
329 datatype = parameterType(ParsedDataType::Int);
330 break;
331 case Cogs::hash("int2"):
332 datatype = parameterType(ParsedDataType::Int2);
333 break;
334 case Cogs::hash("int3"):
335 datatype = parameterType(ParsedDataType::Int3);
336 break;
337 case Cogs::hash("int4"):
338 datatype = parameterType(ParsedDataType::Int4);
339 break;
340 }
341 src.append(" @location(" + std::to_string(location) + ") ");
342 src.append(tokens[0]);
343 src.append(" : ");
344 src.append(datatype);
345 src.append(",\n");
346 }
347 }
348 if (!hasCustomTargets) {
349 src.append(" @location(0) fragColor : vec4f,\n");
350 }
351 src.append("};\n\n");
352 }
353}
354
355namespace Cogs
356{
357 namespace Core {
358
359 bool buildPostProcessEffectWebGPU(RenderTaskContext* context,
360 EffectDescription& desc, PostProcessTask* task) {
361 int bindGroup = 0;
362 int bindings = 0;
363 std::string src;
364 addInterface(src);
365 addDefine(src, "COGS_MAX_LIGHTS", std::to_string(context->renderer->getMaxLights()));
366 addDefines(src, desc.definitions);
367
368 bool SceneBufferRef = false;
369 for (const ProcessTaskProperty& p : task->properties) {
370 if (p.definition->key == "SceneBuffer") {
371 SceneBufferRef = true;
372 }
373 }
374
375 addUniforms(src, task->properties, bindGroup, bindings);
376 addEngineSamplers(src/*, const std::unordered_set<Cogs::Core::StringRef>&identifiersSeen*/, bindGroup, bindings);
377 addOutputStruct(src, task->options, task->writeDepth || task->depthTest);
378 if(SceneBufferRef){
379 src.append("#include \"Engine/SceneBuffer.wgsl\"\n\n");
380 }
381
382 std::string prefix_ps = desc.ps;
383 removeSuffix(prefix_ps, ".hlsl");
384 src.append("#include \"" + prefix_ps + ".wgsl\"\n");
385 auto code = hash(src);
386 std::string file_prefix = prefix_ps + "_" + std::to_string(code);
387
389 pp.processed.reserve(::InitialBufferCapasity);
390
391 if (!pp.process(context->context, src)) return false;
392
393 src.swap(pp.processed);
394 src = convertDefinesToConstExpressions(src);
395 context->context->resourceStore->addResource(file_prefix + ".wgsl", src);
396 desc.ps = file_prefix + ".hlsl";
397 desc.definitions.clear();
398 return true;
399 }
400 }
401}
std::unique_ptr< class ResourceStore > resourceStore
ResourceStore service instance.
Definition: Context.h:210
unsigned getMaxLights() const override
Get the maximum number of lights.
Definition: Renderer.h:73
Log implementation class.
Definition: LogManager.h:140
Contains the Engine, Renderer, resource managers and other systems needed to run Cogs....
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:181
Contains all Cogs related functionality.
Definition: FieldSetter.h:23
constexpr size_t hash() noexcept
Simple getter function that returns the initial value for fnv1a hashing.
Definition: HashFunctions.h:62
COGSFOUNDATION_API size_t hashLowercase(std::string_view str, size_t hashValue=Cogs::hash()) noexcept
Get the hash code of the string converted to lowercase.
Partial C preprocessor.
Definition: Preprocessor.h:42
bool process(Context *context, const StringView input)
Run a text block through the preprocessor.
std::string processed
Resulting processed text.
Definition: Preprocessor.h:48