Cogs.Core
ShaderBuilderPostProcessWebGPU.cpp
1#include "ShaderBuilderPostProcess.h"
2#include "ShaderBuilderHelpersWebGPU.h"
3#include "Context.h"
4#include "Utilities/Strings.h"
5#include "Utilities/Preprocessor.h"
6#include "ResourceStore.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/IEffects.h"
13#include "Renderer/RenderTarget.h"
14
15#include <cassert>
16#include <sstream>
17#include <map>
18#include <algorithm>
19
20namespace {
21 constexpr size_t InitialBufferCapacity = 4096u;
22
23 const Cogs::Logging::Log logger = Cogs::Logging::getLogger("ShaderBuilderPostProcessWebGPU");
24
25 using namespace Cogs::Core;
26
27 void removeSuffix(std::string& dst, const std::string_view& suffix)
28 {
29 auto pos = dst.find(suffix);
30 if (pos == std::string::npos) return;
31 dst.erase(pos);
32 }
33
34 [[nodiscard]] std::string_view parameterType(const ParsedDataType type)
35 {
36 switch (type) {
37 case ParsedDataType::Float: return "f32"; break;
38 case ParsedDataType::Float2: return "vec2f"; break;
39 case ParsedDataType::Float3: return "vec3f"; break;
40 case ParsedDataType::Float4: return "vec4f"; break;
41 case ParsedDataType::Float4x4: return "mat4x4f"; break;
42 case ParsedDataType::Int: return "i32"; break;
43 case ParsedDataType::Int2: return "vec2i"; break;
44 case ParsedDataType::Int3: return "vec3i"; break;
45 case ParsedDataType::Int4: return "vec4i"; break;
46 case ParsedDataType::UInt: return "u32"; break;
47 case ParsedDataType::UInt2: return "vec2u"; break;
48 case ParsedDataType::UInt3: return "vec3u"; break;
49 case ParsedDataType::UInt4: return "vec4u"; break;
50 default:
51 LOG_ERROR(logger, "Unsupported attribute type %d", int(type));
52 return "<illegal>";
53 break;
54 }
55 }
56
57 [[nodiscard]]
58 Cogs::BindingTextureSampleType bindingTextureSampleType(const ParsedDataType type, const bool isDepthTexture)
59 {
60 if (isDepthTexture) {
61 return Cogs::BindingTextureSampleType::Depth;
62 }
63
64 switch (type) {
65 case ParsedDataType::Int:
66 case ParsedDataType::Int2:
67 case ParsedDataType::Int3:
68 case ParsedDataType::Int4:
69 return Cogs::BindingTextureSampleType::Sint;
70 case ParsedDataType::UInt:
71 case ParsedDataType::UInt2:
72 case ParsedDataType::UInt3:
73 case ParsedDataType::UInt4:
74 return Cogs::BindingTextureSampleType::Uint;
75 default:
76 return Cogs::BindingTextureSampleType::Float;
77 }
78 }
79
82 [[nodiscard]]
83 std::string convertDefinesToConstExpressions(const std::string& s) {
84 std::map<std::string, std::string> addedDefines;
85 std::string result;
86 result.reserve(s.size());
87
88 std::istringstream iss(s);
89
90 std::string identifier;
91 std::string replacement;
92 for (std::string line; std::getline(iss, line); )
93 {
94 identifier.clear();
95 replacement.clear();
96
97 std::string_view sv(line);
98 auto pos = sv.find_first_not_of(" \t");
99 if (pos == std::string_view::npos || sv.substr(pos, 7) != "#define") {
100 result += line + "\n";
101 continue;
102 }
103 sv = sv.substr(pos + 7);
104
105 pos = sv.find_first_not_of(" \t");
106 if (pos == std::string_view::npos) {
107 result += line + "\n";
108 continue;
109 }
110 sv = sv.substr(pos);
111
112 auto end = sv.find_first_of(" \t");
113 if (end == std::string_view::npos) {
114 identifier = sv;
115 replacement = "1";
116 } else {
117 identifier = sv.substr(0, end);
118 sv = sv.substr(end);
119 pos = sv.find_first_not_of(" \t");
120 if (pos == std::string_view::npos) {
121 replacement = "1";
122 } else {
123 auto last = sv.find_last_not_of(" \t");
124 replacement = sv.substr(pos, last - pos + 1);
125 }
126 }
127
128 auto it = addedDefines.find(identifier);
129 if (it != addedDefines.end()) {
130 if (replacement != (*it).second) {
131 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());
132 }
133 continue;
134 }
135 result += "const " + identifier + " = " + replacement + ";\n";
136 addedDefines.try_emplace(identifier, replacement);
137 }
138 return result;
139 }
140
141 void addInterface(std::string& src) {
142 src.append(
143 R"(
144struct VertexIn {
145 @builtin(position) position: vec4f,
146 @location(0) TexCoords: vec2f,
147 @location(1) NormalizedCoords: vec2f,
148};
149
150)");
151 }
152
153 void addDefine(std::string& output, const std::string& name, const std::string& value)
154 {
155 output.append("#define ");
156 output.append(name);
157 output.append(" ");
158 output.append(value);
159 output.append("\n");
160 }
161
162 void addDefines(std::string& output, const std::vector<std::pair<std::string, std::string>>& definitions)
163 {
164 for (const std::pair<std::string, std::string>& define : definitions) {
165 addDefine(output, define.first, define.second);
166 }
167 }
168
169 void addOutputStruct(std::string& src, const PipelineOptions& options, bool writeDepth) {
170 bool hasCustomTargets = false;
171 src.append("struct FragmentOut {\n");
172 if (writeDepth) {
173 src.append(" @builtin(frag_depth) fragDepth: f32,\n");
174 }
175 std::string prefix = "target_";
176 for (auto o : options) {
177 auto key = o.first;
178
179 for (auto& c : key) {
180 c = static_cast<decltype(key)::value_type>(std::tolower(c));
181 }
182 if (key.substr(0, prefix.size()) != prefix) continue;
183 hasCustomTargets = true;
184
185 int location = std::stoi(key.substr(prefix.size()));
186 TokenStream tokens;
187 Cogs::Core::split(o.second, " ", tokens);
188 if (tokens.size() != 2) {
189 LOG_ERROR(logger, "Unable to parse pipeline output option %s", o.second.c_str());
190 continue;
191 }
192 std::string_view datatype = "Undefined";
193 switch (tokens[1].hashLowercase()) {
194 case Cogs::hash("float"): datatype = parameterType(ParsedDataType::Float); break;
195 case Cogs::hash("float2"): datatype = parameterType(ParsedDataType::Float2); break;
196 case Cogs::hash("float3"): datatype = parameterType(ParsedDataType::Float3); break;
197 case Cogs::hash("float4"): datatype = parameterType(ParsedDataType::Float4); break;
198 case Cogs::hash("uint"): datatype = parameterType(ParsedDataType::UInt); break;
199 case Cogs::hash("uint2"): datatype = parameterType(ParsedDataType::UInt2); break;
200 case Cogs::hash("uint3"): datatype = parameterType(ParsedDataType::UInt3); break;
201 case Cogs::hash("uint4"): datatype = parameterType(ParsedDataType::UInt4); break;
202 case Cogs::hash("int"): datatype = parameterType(ParsedDataType::Int); break;
203 case Cogs::hash("int2"): datatype = parameterType(ParsedDataType::Int2); break;
204 case Cogs::hash("int3"): datatype = parameterType(ParsedDataType::Int3); break;
205 case Cogs::hash("int4"): datatype = parameterType(ParsedDataType::Int4); break;
206 }
207 src.append(" @location(" + std::to_string(location) + ") ");
208 src.append(tokens[0]);
209 src.append(" : ");
210 src.append(datatype);
211 src.append(",\n");
212 }
213 if (!hasCustomTargets) {
214 src.append(" @location(0) fragColor : vec4f,\n");
215 }
216 src.append("};\n\n");
217 }
218
220 struct EffectParameterField
221 {
222 std::string name;
223 ParsedDataType type = ParsedDataType::Unknown;
224 };
225
226 void emitEffectParametersStruct(std::string& src, const std::vector<EffectParameterField>& fields)
227 {
228 if (fields.empty()) return;
229
230 src.append("struct EffectParameters_t {\n");
231 for (const EffectParameterField& field : fields) {
232 src.append(" ");
233 src.append(field.name);
234 src.append(" : ");
235 src.append(parameterType(field.type));
236 src.append(",\n");
237 }
238 src.append("};\n");
239 }
240
246 void addTaskUniforms(std::string& uniforms, Cogs::BindGroupSetDescription& bindings, const std::vector<ProcessTaskProperty>& properties)
247 {
248 constexpr BindGroup group = BindGroup::Default;
249 Cogs::BindGroupDescription& defaultGroup = bindings.groups[static_cast<size_t>(group)];
250 bindings.numGroups = std::max(bindings.numGroups, static_cast<uint16_t>(static_cast<size_t>(group) + 1));
251
252 std::vector<EffectParameterField> parameterFields;
253
254 for (const ProcessTaskProperty& p : properties) {
255 switch (p.definition->type) {
256 case ParsedDataType::Float:
257 case ParsedDataType::Float2:
258 case ParsedDataType::Float3:
259 case ParsedDataType::Float4:
260 case ParsedDataType::Float4x4:
261 case ParsedDataType::Int:
262 case ParsedDataType::Int2:
263 case ParsedDataType::Int3:
264 case ParsedDataType::Int4:
265 case ParsedDataType::UInt:
266 case ParsedDataType::UInt2:
267 case ParsedDataType::UInt3:
268 case ParsedDataType::UInt4:
269 parameterFields.push_back({ p.definition->key, p.definition->type });
270 break;
271
272 case ParsedDataType::Texture2D:
273 {
274 assert(defaultGroup.numEntries + 2 <= Cogs::MaxBindGroupEntries);
275 const bool isDepthTexture = (p.definition->texture.flags & ParsedValueTextureFlags::DepthTexture) != 0;
276 if (p.definition->texture.samples > 1) {
277 LOG_ERROR(logger, "Sampling from multisampled textures is not implemented for WebGPU post-process effects (%s)", p.definition->key.c_str());
278 }
279
280 Cogs::BindGroupEntryDescription& textureEntry = defaultGroup.entries[defaultGroup.numEntries];
281 textureEntry.binding = defaultGroup.numEntries++;
282 textureEntry.nameHash = Cogs::hash(p.definition->key.c_str());
283 textureEntry.resourceType = Cogs::BindingResourceType::Texture;
284 textureEntry.visibility = Cogs::BindingVisibilityFragment;
285 textureEntry.textureDimension = Cogs::ResourceDimensions::Texture2D;
286 textureEntry.textureSampleType = bindingTextureSampleType(p.definition->texture.dataType, isDepthTexture);
287 textureEntry.isDepthTexture = isDepthTexture;
288 addTextureToSource(uniforms, p.definition->key, group, textureEntry);
289
290 const std::string samplerName = p.definition->key + "Sampler";
291 Cogs::BindGroupEntryDescription& samplerEntry = defaultGroup.entries[defaultGroup.numEntries];
292 samplerEntry.binding = defaultGroup.numEntries++;
293 samplerEntry.nameHash = Cogs::hash(samplerName.c_str());
294 samplerEntry.resourceType = Cogs::BindingResourceType::Sampler;
295 samplerEntry.visibility = Cogs::BindingVisibilityFragment;
296 samplerEntry.samplerBindingType = isDepthTexture ? Cogs::BindingSamplerBindingType::NonFiltering : Cogs::BindingSamplerBindingType::Filtering;
297 samplerEntry.isDepthTexture = isDepthTexture;
298 addSamplerToSource(uniforms, samplerName, group, samplerEntry);
299 break;
300 }
301
302 case ParsedDataType::Buffer:
303 LOG_ERROR(logger, "Render buffers are not supported for WebGPU post-process effects (%s)", p.definition->key.c_str());
304 break;
305
306 case ParsedDataType::ConstantBuffer:
307 // Engine-provided constant buffers (e.g. a "ConstantBuffer Cogs.SceneBuffer" property)
308 // are picked up automatically from shader usage further down, so nothing to do here.
309 break;
310
311 default:
312 break;
313 }
314 }
315
316 if (!parameterFields.empty()) {
317 assert(defaultGroup.numEntries < Cogs::MaxBindGroupEntries);
318 Cogs::BindGroupEntryDescription& entry = defaultGroup.entries[defaultGroup.numEntries];
319 entry.binding = defaultGroup.numEntries++;
320 entry.nameHash = Cogs::hash("EffectParameters");
321 entry.resourceType = Cogs::BindingResourceType::UniformBuffer;
322 entry.visibility = Cogs::BindingVisibilityFragment;
323
324 emitEffectParametersStruct(uniforms, parameterFields);
325 uniforms.append("@group(");
326 uniforms.append(std::to_string(static_cast<size_t>(group)));
327 uniforms.append(") @binding(");
328 uniforms.append(std::to_string(entry.binding));
329 uniforms.append(") var<uniform> EffectParameters : EffectParameters_t;\n");
330 }
331 }
332}
333
334namespace Cogs
335{
336 namespace Core {
337
338 bool buildPostProcessEffectWebGPU(RenderTaskContext* context,
339 EffectDescription& desc, PostProcessTask* task) {
340 // 1. Fetch the engine's bind group layout so post-process effects share the exact same
341 // groups/bindings as the rest of the engine (SceneBuffer, samplers, textures, ...).
342 Cogs::BindGroupSetDescription bindings = getEngineBindGroupDescription();
343
344 std::string body;
345 addInterface(body);
346 addDefine(body, "COGS_MAX_LIGHTS", std::to_string(context->renderer->getMaxLights()));
347 addDefines(body, desc.definitions);
348 addOutputStruct(body, task->options, task->writeDepth || task->depthTest);
349
350 // 2. Add the bindings needed by this specific post-process effect into the Default bind
351 // group and emit their declarations.
352 std::string uniforms;
353 addTaskUniforms(uniforms, bindings, task->properties);
354 body.append(uniforms);
355
356 std::string prefix_ps = desc.ps;
357 removeSuffix(prefix_ps, ".hlsl");
358 body.append("#include \"" + prefix_ps + ".wgsl\"\n");
359
360 // 3. Run the partial preprocessor to resolve includes/#ifdefs and learn which identifiers
361 // (engine buffers, samplers, textures) the effect's shader source actually references.
363 pp.processed.reserve(::InitialBufferCapacity);
364 if (!pp.process(context->context, body)) return false;
365 body.swap(pp.processed);
366 pp.processed.clear();
367
368 // 4. Fill a source string with the generated engine bindings that were actually referenced.
369 std::string engineUniforms;
370 std::vector<bool> bindGroupUsed;
371 addEngineUniformsToSourceWebGPU(engineUniforms, pp.identifiersSeen, bindGroupUsed);
372 if (!engineUniforms.empty()) {
373 if (!pp.process(context->context, engineUniforms)) return false;
374 engineUniforms.swap(pp.processed);
375 }
376
377 bindGroupUsed.resize(static_cast<size_t>(BindGroup::Count), false);
378 if (bindings.groups[static_cast<size_t>(BindGroup::Default)].numEntries > 0) {
379 bindGroupUsed[static_cast<size_t>(BindGroup::Default)] = true;
380 }
381
383 for (size_t g = 0; g < bindGroupUsed.size(); ++g) {
384 if (bindGroupUsed[g]) {
385 usedBindings.groups[g] = bindings.groups[g];
386 usedBindings.numGroups = static_cast<uint16_t>(g + 1);
387 }
388 }
389 desc.bindGroupLayoutDesc = usedBindings;
390
391 std::string all = engineUniforms + body;
392 all = convertDefinesToConstExpressions(all);
393
394 auto code = hash(all);
395 std::string file_prefix = prefix_ps + "_" + std::to_string(code);
396 context->context->resourceStore->addResource(file_prefix + ".wgsl", all);
397 desc.ps = file_prefix + ".hlsl";
398 desc.definitions.clear();
399 return true;
400 }
401 }
402}
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:75
Log implementation class.
Definition: LogManager.h:140
Contains the Engine, Renderer, resource managers and other systems needed to run Cogs....
void addSamplerToSource(std::string &source, const std::string &name, BindGroup group, const Cogs::BindGroupEntryDescription &entry)
Emits a valid WGSL sampler declaration ("@group()@binding() var") for an engine-provided sampler entr...
void addTextureToSource(std::string &source, const std::string &name, BindGroup group, const Cogs::BindGroupEntryDescription &entry)
Emits a valid WGSL texture declaration ("@group()@binding() var") for an engine-provided texture entr...
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::unordered_set< StringRef > identifiersSeen
Set of identifiers encountered in active text.
Definition: Preprocessor.h:51
std::string processed
Resulting processed text.
Definition: Preprocessor.h:48