VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
programLibrary.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4// Created by Arnis Lektauers on 11.10.2025.
5//
6#include "programLibrary.h"
7
8#include <assert.h>
9#include <array>
10#include <cstdint>
11#include <filesystem>
12#include <fstream>
13#include <optional>
14#include <sstream>
15#include <vector>
16
18#include "spdlog/spdlog.h"
20
21namespace visutwin::canvas
22{
23 namespace
24 {
25 DeviceCache programLibraryDeviceCache;
26 std::unordered_map<GraphicsDevice*, std::shared_ptr<ProgramLibrary>> programLibraries;
27
28 uint64_t fnv1a64(const std::string& text)
29 {
30 uint64_t hash = 1469598103934665603ull;
31 for (const char c : text) {
32 hash ^= static_cast<uint8_t>(c);
33 hash *= 1099511628211ull;
34 }
35 return hash;
36 }
37
38 void appendFeatureDefine(std::string& output, const char* name, const bool enabled)
39 {
40 output += "#define ";
41 output += name;
42 output += enabled ? " 1\n" : " 0\n";
43 }
44
45 struct ShaderChunkRegistry
46 {
47 std::unordered_map<std::string, std::string> chunkSources;
48 std::filesystem::path rootPath;
49 };
50
51 std::string readTextFile(const std::filesystem::path& path)
52 {
53 std::ifstream in(path, std::ios::in | std::ios::binary);
54 if (!in.is_open()) {
55 return {};
56 }
57
58 std::ostringstream buffer;
59 buffer << in.rdbuf();
60 return buffer.str();
61 }
62
63 std::filesystem::path projectRootFromThisSource()
64 {
65 auto path = std::filesystem::path(__FILE__).parent_path();
66 for (int i = 0; i < 4; ++i) {
67 path = path.parent_path();
68 }
69 return path;
70 }
71
72 std::optional<ShaderChunkRegistry> loadShaderChunks()
73 {
74 const auto sourceRoot = projectRootFromThisSource();
75 const auto cwd = std::filesystem::current_path();
76 const std::array<std::filesystem::path, 4> chunkRoots = {
77 sourceRoot / "engine/shaders/metal/chunks",
78 cwd / "engine/shaders/metal/chunks",
79 cwd.parent_path() / "engine/shaders/metal/chunks",
80 cwd.parent_path().parent_path() / "engine/shaders/metal/chunks"
81 };
82
83 for (const auto& root : chunkRoots) {
84 ShaderChunkRegistry registry;
85 registry.rootPath = root;
86
87 if (!std::filesystem::exists(root) || !std::filesystem::is_directory(root)) {
88 continue;
89 }
90
91 for (const auto& entry : std::filesystem::directory_iterator(root)) {
92 if (!entry.is_regular_file() || entry.path().extension() != ".metal") {
93 continue;
94 }
95
96 const auto chunkName = entry.path().stem().string();
97 const auto chunkSource = readTextFile(entry.path());
98 if (!chunkSource.empty()) {
99 registry.chunkSources[chunkName] = chunkSource;
100 }
101 }
102
103 if (!registry.chunkSources.empty()) {
104 spdlog::info("Loaded shader chunks from {}", root.string());
105 return registry;
106 }
107 }
108
109 return std::nullopt;
110 }
111
112 const ShaderChunkRegistry* getShaderChunks()
113 {
114 static std::optional<ShaderChunkRegistry> chunks = loadShaderChunks();
115 return chunks ? &*chunks : nullptr;
116 }
117
118 const Material::ParameterValue* getMaterialParameter(const Material* material, std::initializer_list<const char*> names)
119 {
120 if (!material) {
121 return nullptr;
122 }
123 for (const char* name : names) {
124 if (const auto* value = material->parameter(name)) {
125 return value;
126 }
127 }
128 return nullptr;
129 }
130
131 bool readParameterBool(const Material::ParameterValue* value, bool& out)
132 {
133 if (!value) {
134 return false;
135 }
136 if (const auto* v = std::get_if<bool>(value)) {
137 out = *v;
138 return true;
139 }
140 if (const auto* v = std::get_if<int32_t>(value)) {
141 out = *v != 0;
142 return true;
143 }
144 if (const auto* v = std::get_if<uint32_t>(value)) {
145 out = *v != 0u;
146 return true;
147 }
148 if (const auto* v = std::get_if<float>(value)) {
149 out = *v != 0.0f;
150 return true;
151 }
152 return false;
153 }
154
155 bool readParameterInt(const Material::ParameterValue* value, int& out)
156 {
157 if (!value) {
158 return false;
159 }
160 if (const auto* v = std::get_if<int32_t>(value)) {
161 out = static_cast<int>(*v);
162 return true;
163 }
164 if (const auto* v = std::get_if<uint32_t>(value)) {
165 out = static_cast<int>(*v);
166 return true;
167 }
168 if (const auto* v = std::get_if<float>(value)) {
169 out = static_cast<int>(*v);
170 return true;
171 }
172 if (const auto* v = std::get_if<bool>(value)) {
173 out = *v ? 1 : 0;
174 return true;
175 }
176 return false;
177 }
178
179 bool hasTextureParameter(const Material* material, std::initializer_list<const char*> names)
180 {
181 if (const auto* value = getMaterialParameter(material, names)) {
182 if (const auto* texture = std::get_if<Texture*>(value)) {
183 return *texture != nullptr;
184 }
185 }
186 return false;
187 }
188 }
189
190 ProgramLibrary::ProgramLibrary(const std::shared_ptr<GraphicsDevice>& device, StandardMaterial* standardMaterial) : _device(device)
191 {
192 (void)standardMaterial;
193 // Mirrors upstream program registration model (program -> ordered chunk keys).
194 registerProgram("forward", {
195 "common",
196 "forward-vertex",
197 "forward-fragment-head",
198 "forward-fragment-lighting",
199 "forward-fragment-tail"
200 });
201 registerProgram("skybox", {
202 "common",
203 "forward-vertex",
204 "forward-fragment-head",
205 "forward-fragment-lighting",
206 "forward-fragment-tail"
207 });
208 registerProgram("shadow", {
209 "common",
210 "shadow-vertex",
211 "shadow-fragment"
212 });
213 }
214
215 void ProgramLibrary::registerProgram(const std::string& name, const std::vector<std::string>& chunkOrder)
216 {
217 if (name.empty() || chunkOrder.empty()) {
218 spdlog::error("ProgramLibrary::registerProgram rejected invalid program registration");
219 return;
220 }
221 _registeredPrograms[name] = chunkOrder;
222 }
223
224 bool ProgramLibrary::hasProgram(const std::string& name) const
225 {
226 return _registeredPrograms.find(name) != _registeredPrograms.end();
227 }
228
229 void setProgramLibrary(const std::shared_ptr<GraphicsDevice>& device, const std::shared_ptr<ProgramLibrary>& library)
230 {
231 assert(library != nullptr && "ProgramLibrary cannot be null");
232 programLibraries[device.get()] = library;
233 programLibraryDeviceCache.get<ProgramLibrary>(device, [library] {
234 return library;
235 });
236 }
237
238 std::shared_ptr<ProgramLibrary> getProgramLibrary(const std::shared_ptr<GraphicsDevice>& device)
239 {
240 const auto it = programLibraries.find(device.get());
241 return it != programLibraries.end() ? it->second : nullptr;
242 }
243
244 ProgramLibrary::ShaderVariantOptions ProgramLibrary::buildForwardVariantOptions(const Material* material,
245 const bool transparentPass, const bool dynamicBatch) const
246 {
247 ShaderVariantOptions options{};
248 options.transparentPass = transparentPass;
249 options.skybox = material && material->isSkybox();
250 options.alphaTest = material && material->alphaMode() == AlphaMode::MASK;
251
252 // StandardMaterial: read properties directly from typed accessors.
253 const auto* stdMat = dynamic_cast<const StandardMaterial*>(material);
254
255 CullMode effectiveCullMode = material ? material->cullMode() : CullMode::CULLFACE_BACK;
256 if (stdMat) {
257 // StandardMaterial stores twoSidedLighting as a separate flag.
258 options.doubleSided = effectiveCullMode == CullMode::CULLFACE_NONE || stdMat->twoSidedLighting();
259 } else {
260 if (int cullModeValue = static_cast<int>(effectiveCullMode);
261 readParameterInt(getMaterialParameter(material, {"material_cullMode", "cullMode"}), cullModeValue)) {
262 if (cullModeValue >= static_cast<int>(CullMode::CULLFACE_NONE) &&
263 cullModeValue <= static_cast<int>(CullMode::CULLFACE_FRONTANDBACK)) {
264 effectiveCullMode = static_cast<CullMode>(cullModeValue);
265 }
266 }
267 options.doubleSided = effectiveCullMode == CullMode::CULLFACE_NONE;
268 }
269
270 if (stdMat) {
271 // Prefer StandardMaterial-specific textures, fall back to base Material typed properties.
272 options.baseColorMap = (stdMat->diffuseMap() || stdMat->baseColorTexture());
273 options.normalMap = (stdMat->normalMap() || stdMat->normalTexture());
274 options.metallicRoughnessMap = (stdMat->metalnessMap() || stdMat->metallicRoughnessTexture());
275 options.occlusionMap = (stdMat->aoMap() || stdMat->occlusionTexture());
276 options.emissiveMap = (stdMat->emissiveMap() || stdMat->emissiveTexture());
277 } else {
278 options.baseColorMap = (material && material->hasBaseColorTexture()) ||
279 hasTextureParameter(material, {"texture_baseColorMap", "texture_diffuseMap", "baseColorTexture"});
280 options.normalMap = (material && material->hasNormalTexture()) ||
281 hasTextureParameter(material, {"texture_normalMap", "normalTexture"});
282 options.metallicRoughnessMap = (material && material->hasMetallicRoughnessTexture()) ||
283 hasTextureParameter(material, {"texture_metallicRoughnessMap", "metallicRoughnessTexture"});
284 options.occlusionMap = (material && material->hasOcclusionTexture()) ||
285 hasTextureParameter(material, {"texture_occlusionMap", "occlusionTexture"});
286 options.emissiveMap = (material && material->hasEmissiveTexture()) ||
287 hasTextureParameter(material, {"texture_emissiveMap", "emissiveTexture"});
288 }
289
290 if (!stdMat) {
291 bool skyboxOverride = options.skybox;
292 if (readParameterBool(getMaterialParameter(material, {"material_isSkybox", "isSkybox"}), skyboxOverride)) {
293 options.skybox = skyboxOverride;
294 }
295 }
296
297 // Feature flags from StandardMaterial typed properties or shaderVariantKey bits.
298 const uint64_t variantBits = material ? material->shaderVariantKey() : 0ull;
299 options.shadowMapping = !options.skybox || ((variantBits & (1ull << 10)) != 0ull);
300 if (stdMat) {
301 options.fog = stdMat->useFog() && !options.skybox;
302 } else {
303 options.fog = !options.skybox || ((variantBits & (1ull << 11)) != 0ull);
304 }
305 // parallax from StandardMaterial heightMap or shaderVariantKey.
306 if (stdMat) {
307 options.parallax = (stdMat->heightMap() != nullptr);
308 } else {
309 options.parallax = (variantBits & (1ull << 12)) != 0ull;
310 }
311 // clearcoat from StandardMaterial (clearCoat > 0) or shaderVariantKey.
312 if (stdMat) {
313 options.clearcoat = stdMat->clearCoat() > 0.0f;
314 } else {
315 options.clearcoat = (variantBits & (1ull << 13)) != 0ull;
316 }
317 // anisotropy from StandardMaterial or shaderVariantKey.
318 if (stdMat) {
319 options.anisotropy = (stdMat->anisotropy() != 0.0f);
320 } else {
321 options.anisotropy = (variantBits & (1ull << 14)) != 0ull;
322 }
323 // sheen from StandardMaterial or shaderVariantKey.
324 if (stdMat) {
325 options.sheen = (stdMat->sheenRoughness() > 0.0f ||
326 stdMat->sheenColor() != Color(0.0f, 0.0f, 0.0f, 1.0f));
327 } else {
328 options.sheen = (variantBits & (1ull << 15)) != 0ull;
329 }
330 // iridescence from StandardMaterial or shaderVariantKey.
331 if (stdMat) {
332 options.iridescence = (stdMat->iridescenceIntensity() > 0.0f);
333 } else {
334 options.iridescence = (variantBits & (1ull << 16)) != 0ull;
335 }
336 // transmission from StandardMaterial or shaderVariantKey.
337 if (stdMat) {
338 options.transmission = (stdMat->transmissionFactor() > 0.0f);
339 } else {
340 options.transmission = (variantBits & (1ull << 17)) != 0ull;
341 }
342 options.lightClustering = _clusteredLightingEnabled || (variantBits & (1ull << 18)) != 0ull;
343 options.ssao = _ssaoEnabled || (variantBits & (1ull << 19)) != 0ull;
344 options.lightProbes = (variantBits & (1ull << 20)) != 0ull;
345 options.vertexColors = (variantBits & (1ull << 21)) != 0ull;
346 options.skinning = (variantBits & (1ull << 22)) != 0ull;
347 options.morphing = (variantBits & (1ull << 23)) != 0ull;
348 // spec-gloss from StandardMaterial or shaderVariantKey.
349 if (stdMat) {
350 options.specGloss = (stdMat->specGlossMap() != nullptr);
351 } else {
352 options.specGloss = (variantBits & (1ull << 24)) != 0ull;
353 }
354 // Oren-Nayar from StandardMaterial or shaderVariantKey.
355 if (stdMat) {
356 options.orenNayar = stdMat->useOrenNayar();
357 } else {
358 options.orenNayar = (variantBits & (1ull << 25)) != 0ull;
359 }
360 // detail normals from StandardMaterial or shaderVariantKey.
361 if (stdMat) {
362 options.detailNormals = (stdMat->detailNormalMap() != nullptr);
363 } else {
364 options.detailNormals = (variantBits & (1ull << 26)) != 0ull;
365 }
366 // displacement from StandardMaterial or shaderVariantKey.
367 if (stdMat) {
368 options.displacement = (stdMat->displacementMap() != nullptr);
369 } else {
370 options.displacement = (variantBits & (1ull << 27)) != 0ull;
371 }
372 options.atmosphere = (_atmosphereEnabled && options.skybox) || (variantBits & (1ull << 28)) != 0ull;
373 options.pointSpotAttenuation = !options.skybox || ((variantBits & (1ull << 29)) != 0ull);
374 options.multiLight = !options.skybox || ((variantBits & (1ull << 30)) != 0ull);
375 options.instancing = (variantBits & (1ull << 33)) != 0ull;
376 options.pointSize = (variantBits & (1ull << 31)) != 0ull;
377 options.unlit = (variantBits & (1ull << 32)) != 0ull;
378
379 // shadow catcher flag from StandardMaterial
380 if (stdMat) {
381 options.shadowCatcher = stdMat->shadowCatcher();
382 }
383
384 // when a skybox cubemap is available, compile the
385 // skybox shader with the cubemap sampling path instead of envAtlas.
386 if (options.skybox && _skyCubemapAvailable) {
387 options.skyCubemap = true;
388 }
389
390 // DEVIATION: planar reflection is handled at the application level as a script;
391 // here it's a material property that triggers a shader variant.
392 if (stdMat) {
393 options.planarReflection = (stdMat->reflectionMap() != nullptr);
394 }
395
396 // depth pass flag set by renderer from camera state.
397 // When active, fragment shader outputs distance-from-plane instead of PBR.
398 options.planarReflectionDepthPass = _planarReflectionDepthPass;
399
400 // Local light shadows: enabled when any local light has castShadows.
401 // Set by the renderer before the draw loop.
402 options.localShadows = _localShadowsEnabled && !options.skybox;
403
404 // Omni cubemap shadows: enabled when any omni light has castShadows.
405 options.omniShadows = _omniShadowsEnabled && !options.skybox;
406
407 // Area lights: enabled when any area rect light is in the scene.
408 // Set by the renderer before the draw loop.
409 options.areaLights = _areaLightsEnabled && !options.skybox;
410
411 // Dynamic batching: per-vertex bone index + matrix palette.
412 // Set by the renderer from MeshInstance::isDynamicBatch().
413 options.dynamicBatch = dynamicBatch;
414
415 return options;
416 }
417
418 std::string ProgramLibrary::resolveProgramName(const ShaderVariantOptions& options)
419 {
420 return options.skybox ? "skybox" : "forward";
421 }
422
423 uint64_t ProgramLibrary::makeVariantKey(const std::string& programName, const ShaderVariantOptions& options, const Material* material) const
424 {
425 (void)material;
426 // Build key entirely from resolved ShaderVariantOptions — do NOT fold in
427 // the raw material shaderVariantKey, because the options already capture
428 // every flag that affects the compiled shader. Including the raw key was
429 // creating spurious unique variants (different materials mapping to the
430 // same set of options but different shaderVariantKey values) and hitting
431 // the AGX compiled-variants footprint limit.
432 uint64_t key = fnv1a64(programName);
433 key ^= options.transparentPass ? (1ull << 63) : 0ull;
434 key ^= options.skybox ? (1ull << 62) : 0ull;
435 key ^= options.baseColorMap ? (1ull << 0) : 0ull;
436 key ^= options.normalMap ? (1ull << 1) : 0ull;
437 key ^= options.metallicRoughnessMap ? (1ull << 2) : 0ull;
438 key ^= options.occlusionMap ? (1ull << 3) : 0ull;
439 key ^= options.emissiveMap ? (1ull << 4) : 0ull;
440 key ^= options.alphaTest ? (1ull << 5) : 0ull;
441 key ^= options.doubleSided ? (1ull << 6) : 0ull;
442 key ^= options.shadowMapping ? (1ull << 8) : 0ull;
443 key ^= options.fog ? (1ull << 9) : 0ull;
444 key ^= options.vertexColors ? (1ull << 10) : 0ull;
445 key ^= options.pointSpotAttenuation ? (1ull << 11) : 0ull;
446 key ^= options.multiLight ? (1ull << 12) : 0ull;
447 key ^= options.envAtlas ? (1ull << 13) : 0ull;
448 // Stubbed feature flags (parallax, clearcoat, etc.) are included for
449 // correctness but are never true in practice yet, so they don't add
450 // extra variants.
451 key ^= options.parallax ? (1ull << 14) : 0ull;
452 key ^= options.clearcoat ? (1ull << 15) : 0ull;
453 key ^= options.anisotropy ? (1ull << 16) : 0ull;
454 key ^= options.sheen ? (1ull << 17) : 0ull;
455 key ^= options.iridescence ? (1ull << 18) : 0ull;
456 key ^= options.transmission ? (1ull << 19) : 0ull;
457 key ^= options.lightClustering ? (1ull << 20) : 0ull;
458 key ^= options.ssao ? (1ull << 21) : 0ull;
459 key ^= options.lightProbes ? (1ull << 22) : 0ull;
460 key ^= options.skinning ? (1ull << 23) : 0ull;
461 key ^= options.morphing ? (1ull << 24) : 0ull;
462 key ^= options.specGloss ? (1ull << 25) : 0ull;
463 key ^= options.orenNayar ? (1ull << 26) : 0ull;
464 key ^= options.detailNormals ? (1ull << 27) : 0ull;
465 key ^= options.displacement ? (1ull << 28) : 0ull;
466 key ^= options.atmosphere ? (1ull << 29) : 0ull;
467 key ^= options.shadowCatcher ? (1ull << 30) : 0ull;
468 key ^= options.skyCubemap ? (1ull << 31) : 0ull;
469 key ^= options.instancing ? (1ull << 32) : 0ull;
470 key ^= options.planarReflection ? (1ull << 33) : 0ull;
471 key ^= options.planarReflectionDepthPass ? (1ull << 34) : 0ull;
472 key ^= options.localShadows ? (1ull << 35) : 0ull;
473 key ^= options.omniShadows ? (1ull << 36) : 0ull;
474 key ^= options.dynamicBatch ? (1ull << 37) : 0ull;
475 key ^= options.pointSize ? (1ull << 38) : 0ull;
476 key ^= options.areaLights ? (1ull << 40) : 0ull;
477 key ^= options.unlit ? (1ull << 39) : 0ull;
478 return key;
479 }
480
481 std::string ProgramLibrary::composeProgramVariantMetalSource(const std::string& programName, const ShaderVariantOptions& options,
482 const std::string& vertexEntry, const std::string& fragmentEntry)
483 {
484 const auto* chunks = getShaderChunks();
485 if (!chunks) {
486 spdlog::error("Failed to load shader chunks from engine/shaders/metal/chunks.");
487 return {};
488 }
489 const auto programChunks = _registeredPrograms.find(programName);
490 if (programChunks == _registeredPrograms.end() || programChunks->second.empty()) {
491 spdlog::error("ProgramLibrary is missing registered chunk order for program '{}'.", programName);
492 return {};
493 }
494
495 std::string source;
496 source.reserve(24 * 1024);
497
498 appendFeatureDefine(source, "VT_FEATURE_SKYBOX", options.skybox);
499 appendFeatureDefine(source, "VT_FEATURE_TRANSPARENT_PASS", options.transparentPass);
500 appendFeatureDefine(source, "VT_FEATURE_ALPHA_TEST", options.alphaTest);
501 appendFeatureDefine(source, "VT_FEATURE_DOUBLE_SIDED", options.doubleSided);
502 appendFeatureDefine(source, "VT_FEATURE_BASE_COLOR_MAP", options.baseColorMap);
503 appendFeatureDefine(source, "VT_FEATURE_NORMAL_MAP", options.normalMap);
504 appendFeatureDefine(source, "VT_FEATURE_METAL_ROUGHNESS_MAP", options.metallicRoughnessMap);
505 appendFeatureDefine(source, "VT_FEATURE_OCCLUSION_MAP", options.occlusionMap);
506 appendFeatureDefine(source, "VT_FEATURE_EMISSIVE_MAP", options.emissiveMap);
507 appendFeatureDefine(source, "VT_FEATURE_ENV_ATLAS", options.envAtlas);
508
509 appendFeatureDefine(source, "VT_FEATURE_SHADOWS", options.shadowMapping);
510 appendFeatureDefine(source, "VT_FEATURE_FOG", options.fog);
511 appendFeatureDefine(source, "VT_FEATURE_PARALLAX", options.parallax);
512 appendFeatureDefine(source, "VT_FEATURE_CLEARCOAT", options.clearcoat);
513 appendFeatureDefine(source, "VT_FEATURE_ANISOTROPY", options.anisotropy);
514 appendFeatureDefine(source, "VT_FEATURE_SHEEN", options.sheen);
515 appendFeatureDefine(source, "VT_FEATURE_IRIDESCENCE", options.iridescence);
516 appendFeatureDefine(source, "VT_FEATURE_TRANSMISSION", options.transmission);
517 appendFeatureDefine(source, "VT_FEATURE_LIGHT_CLUSTERING", options.lightClustering);
518 appendFeatureDefine(source, "VT_FEATURE_SSAO", options.ssao);
519 appendFeatureDefine(source, "VT_FEATURE_LIGHT_PROBES", options.lightProbes);
520 appendFeatureDefine(source, "VT_FEATURE_VERTEX_COLORS", options.vertexColors);
521 appendFeatureDefine(source, "VT_FEATURE_SKINNING", options.skinning);
522 appendFeatureDefine(source, "VT_FEATURE_MORPHS", options.morphing);
523 appendFeatureDefine(source, "VT_FEATURE_SPEC_GLOSS", options.specGloss);
524 appendFeatureDefine(source, "VT_FEATURE_OREN_NAYAR", options.orenNayar);
525 appendFeatureDefine(source, "VT_FEATURE_DETAIL_NORMALS", options.detailNormals);
526 appendFeatureDefine(source, "VT_FEATURE_DISPLACEMENT", options.displacement);
527 appendFeatureDefine(source, "VT_FEATURE_ATMOSPHERE", options.atmosphere);
528 appendFeatureDefine(source, "VT_FEATURE_AREA_LIGHTS", options.areaLights);
529 appendFeatureDefine(source, "VT_FEATURE_POINT_SPOT_ATTENUATION", options.pointSpotAttenuation);
530 appendFeatureDefine(source, "VT_FEATURE_MULTI_LIGHT", options.multiLight);
531 appendFeatureDefine(source, "VT_FEATURE_SHADOW_CATCHER", options.shadowCatcher);
532 appendFeatureDefine(source, "VT_FEATURE_SKY_CUBEMAP", options.skyCubemap);
533 appendFeatureDefine(source, "VT_FEATURE_SURFACE_LIC", options.surfaceLIC);
534 appendFeatureDefine(source, "VT_FEATURE_INSTANCING", options.instancing);
535 appendFeatureDefine(source, "VT_FEATURE_PLANAR_REFLECTION", options.planarReflection);
536 appendFeatureDefine(source, "VT_FEATURE_PLANAR_REFLECTION_DEPTH_PASS", options.planarReflectionDepthPass);
537 appendFeatureDefine(source, "VT_FEATURE_LOCAL_SHADOWS", options.localShadows);
538 appendFeatureDefine(source, "VT_FEATURE_OMNI_SHADOWS", options.omniShadows);
539 appendFeatureDefine(source, "VT_FEATURE_DYNAMIC_BATCH", options.dynamicBatch);
540 appendFeatureDefine(source, "VT_FEATURE_POINT_SIZE", options.pointSize);
541 appendFeatureDefine(source, "VT_FEATURE_UNLIT", options.unlit);
542 // VT_FEATURE_HDR_PASS is not emitted as a compile-time define.
543 // It is passed as a runtime uniform bit in LightingData.flagsAndPad
544 // to avoid doubling the number of compiled shader variants.
545
546 source += "\n#define VT_VERTEX_ENTRY ";
547 source += vertexEntry;
548 source += "\n#define VT_FRAGMENT_ENTRY ";
549 source += fragmentEntry;
550 source += "\n\n";
551
552 for (const auto& chunkName : programChunks->second) {
553 const auto chunkIt = chunks->chunkSources.find(chunkName);
554 if (chunkIt == chunks->chunkSources.end()) {
555 spdlog::error("ProgramLibrary chunk '{}' is missing in '{}'.",
556 chunkName, chunks->rootPath.string());
557 return {};
558 }
559 source += chunkIt->second;
560 source += "\n";
561 }
562
563 return source;
564 }
565
566 std::shared_ptr<Shader> ProgramLibrary::buildForwardShaderVariant(const std::string& programName,
567 const ShaderVariantOptions& options, const uint64_t variantKey)
568 {
569 ShaderDefinition definition;
570 definition.name = "program-" + programName;
571 definition.name += options.transparentPass ? "-transparent" : "-opaque";
572 definition.name += "-" + std::to_string(variantKey);
573 const auto entryPrefix = programName == "shadow" ? "pcShadow" : "pcForward";
574 definition.vshader = entryPrefix + std::string("VS_") + std::to_string(variantKey);
575 definition.fshader = entryPrefix + std::string("FS_") + std::to_string(variantKey);
576 const std::string sourceCode = composeProgramVariantMetalSource(programName, options, definition.vshader, definition.fshader);
577 if (sourceCode.empty()) {
578 return nullptr;
579 }
580 return createShader(_device.get(), definition, sourceCode);
581 }
582
583 std::shared_ptr<Shader> ProgramLibrary::getForwardShader(const Material* material, const bool transparentPass,
584 const bool dynamicBatch)
585 {
586 if (!_device) {
587 return nullptr;
588 }
589
590 const ShaderVariantOptions options = buildForwardVariantOptions(material, transparentPass, dynamicBatch);
591 const std::string programName = resolveProgramName(options);
592 if (!hasProgram(programName)) {
593 spdlog::error("ProgramLibrary has no registered program '{}'.", programName);
594 return nullptr;
595 }
596 const uint64_t key = makeVariantKey(programName, options, material);
597
598 const auto cached = _forwardShaderCache.find(key);
599 if (cached != _forwardShaderCache.end()) {
600 return cached->second;
601 }
602
603 auto warnFeature = [&](const char* featureName, const bool enabled) {
604 if (!enabled) {
605 return;
606 }
607 if (_warnedFeatureFlags.insert(featureName).second) {
608 spdlog::warn("Shader variant feature '{}' enabled but only chunk scaffolding is present. Full shader chunk port is pending.",
609 featureName);
610 }
611 };
612
613 // parallax: fully implemented — no warning needed.
614 // clearcoat: fully implemented — no warning needed.
615 // anisotropy: fully implemented — no warning needed.
616 // sheen: fully implemented — no warning needed.
617 // iridescence: fully implemented — no warning needed.
618 // transmission: fully implemented — no warning needed.
619 // lightClustering: fully implemented — no warning needed.
620 // ssao: fully implemented — no warning needed.
621 warnFeature("lightProbes", options.lightProbes);
622 // vertexColors: fully implemented — no warning needed.
623 warnFeature("skinning", options.skinning);
624 warnFeature("morphing", options.morphing);
625 warnFeature("specGloss", options.specGloss);
626 warnFeature("orenNayar", options.orenNayar);
627 warnFeature("detailNormals", options.detailNormals);
628 warnFeature("displacement", options.displacement);
629 // atmosphere: fully implemented — no warning needed.
630
631 auto shader = buildForwardShaderVariant(programName, options, key);
632 if (!shader) {
633 spdlog::error("Failed to build shader variant '{}' (key={:#x}, localShadows={}, shadows={}, envAtlas={})",
634 programName, key, options.localShadows, options.shadowMapping, options.envAtlas);
635 }
636 _forwardShaderCache[key] = shader;
637 return shader;
638 }
639
640 std::shared_ptr<Shader> ProgramLibrary::getShadowShader(const bool dynamicBatch)
641 {
642 if (!_device || !hasProgram("shadow")) {
643 return nullptr;
644 }
645
646 ShaderVariantOptions options{};
647 options.skybox = false;
648 options.transparentPass = false;
649 options.alphaTest = false;
650 options.doubleSided = false;
651 options.shadowMapping = true;
652 options.fog = false;
653 options.multiLight = false;
654 options.dynamicBatch = dynamicBatch;
655
656 const uint64_t key = makeVariantKey("shadow", options, nullptr);
657 const auto cached = _forwardShaderCache.find(key);
658 if (cached != _forwardShaderCache.end()) {
659 return cached->second;
660 }
661
662 auto shader = buildForwardShaderVariant("shadow", options, key);
663 _forwardShaderCache[key] = shader;
664 return shader;
665 }
666
667 void ProgramLibrary::bindMaterial(const std::shared_ptr<GraphicsDevice>& device, const Material* material,
668 const bool transparentPass, const bool dynamicBatch)
669 {
670 if (!device) {
671 return;
672 }
673
674 auto shader = material ? material->shaderOverride() : nullptr;
675 if (!shader) {
676 shader = getForwardShader(material, transparentPass, dynamicBatch);
677 }
678
679 auto blendState = material ? material->blendState() : nullptr;
680 auto depthState = material ? material->depthState() : nullptr;
681
682 if (shader) {
683 device->setShader(shader);
684 }
685 if (blendState) {
686 device->setBlendState(blendState);
687 }
688 if (depthState) {
689 device->setDepthState(depthState);
690 }
691 device->setMaterial(material);
692 }
693}
Base class for GPU materials — owns uniform data, texture bindings, blend/depth state,...
Definition material.h:143
const std::shared_ptr< Shader > & shaderOverride() const
Definition material.h:160
std::variant< float, int32_t, uint32_t, bool, Color, Vector2, Vector3, Vector4, Matrix4, Texture * > ParameterValue
Definition material.h:145
const std::shared_ptr< DepthState > & depthState() const
Definition material.h:166
const std::shared_ptr< BlendState > & blendState() const
Definition material.h:163
bool hasProgram(const std::string &name) const
void registerProgram(const std::string &name, const std::vector< std::string > &chunkOrder)
std::shared_ptr< Shader > getForwardShader(const Material *material, bool transparentPass, bool dynamicBatch=false)
ProgramLibrary(const std::shared_ptr< GraphicsDevice > &device, StandardMaterial *standardMaterial)
void bindMaterial(const std::shared_ptr< GraphicsDevice > &device, const Material *material, bool transparentPass, bool dynamicBatch=false)
std::shared_ptr< Shader > getShadowShader(bool dynamicBatch=false)
Full PBR material with metalness/roughness workflow and advanced surface features.
std::shared_ptr< Shader > createShader(GraphicsDevice *graphicsDevice, const ShaderDefinition &definition, const std::string &sourceCode)
Definition shader.cpp:39
void setProgramLibrary(const std::shared_ptr< GraphicsDevice > &device, const std::shared_ptr< ProgramLibrary > &library)
std::shared_ptr< ProgramLibrary > getProgramLibrary(const std::shared_ptr< GraphicsDevice > &device)