VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
metalUniformBinder.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4// Uniform packing, ring-buffer allocation, and per-pass deduplication.
5// Extracted from MetalGraphicsDevice for single-responsibility decomposition.
6//
8
9#include <algorithm>
10#include <cmath>
11#include <cstring>
13#include "metalUtils.h"
14#include "core/math/color.h"
15#include "core/math/matrix4.h"
16#include "core/math/vector3.h"
17#include "core/utils.h"
20#include "scene/constants.h"
22
23namespace visutwin::canvas
24{
26
27 namespace
28 {
29 struct SceneData
30 {
31 simd::float4x4 projViewMatrix;
32 };
33
34 struct ModelData
35 {
36 simd::float4x4 modelMatrix;
37 simd::float4x4 normalMatrix;
38 float normalSign;
39 float _pad[3];
40 };
41 }
42
43 // -----------------------------------------------------------------------
44 // Transform uniforms
45 // -----------------------------------------------------------------------
46
47 void MetalUniformBinder::setTransformUniforms(MTL::RenderCommandEncoder* encoder,
48 MetalUniformRingBuffer* transformRing,
49 const Matrix4& viewProjection, const Matrix4& model)
50 {
51 if (!encoder) {
52 return;
53 }
54
55 // SceneData (VP matrix) at slot 1 — only re-send when the VP changes.
56 // Within a single camera/layer pass the VP is identical for all draws,
57 // but multi-layer passes may switch cameras with different VP matrices.
58 const SceneData sceneData{toSimdMatrix(viewProjection)};
59 if (!_sceneDataBoundThisPass ||
60 std::memcmp(&sceneData.projViewMatrix, &_cachedSceneVP, sizeof(simd::float4x4)) != 0) {
61 encoder->setVertexBytes(&sceneData, sizeof(SceneData), 1);
62 _cachedSceneVP = sceneData.projViewMatrix;
63 _sceneDataBoundThisPass = true;
64 }
65
66 // ModelData at slot 2 — changes per draw, allocated from ring buffer.
67 // Normal matrix = (M^-1)^T of the upper-left 3x3.
68 // Computed via 3x3 cofactors instead of full 4x4 inverse().transpose() (~5-7x cheaper).
69 // The cofactor matrix C satisfies: (M^-1)^T = C / det(M).
70 // Since the vertex shader multiplies by float4(normal, 0.0), only the 3x3 matters.
71 const float m00 = model.getElement(0, 0);
72 const float m01 = model.getElement(0, 1);
73 const float m02 = model.getElement(0, 2);
74 const float m10 = model.getElement(1, 0);
75 const float m11 = model.getElement(1, 1);
76 const float m12 = model.getElement(1, 2);
77 const float m20 = model.getElement(2, 0);
78 const float m21 = model.getElement(2, 1);
79 const float m22 = model.getElement(2, 2);
80
81 // 3x3 cofactors (reused for both determinant and normal matrix).
82 const float c00 = m11 * m22 - m12 * m21;
83 const float c01 = m12 * m20 - m10 * m22;
84 const float c02 = m10 * m21 - m11 * m20;
85 const float c10 = m02 * m21 - m01 * m22;
86 const float c11 = m00 * m22 - m02 * m20;
87 const float c12 = m01 * m20 - m00 * m21;
88 const float c20 = m01 * m12 - m02 * m11;
89 const float c21 = m02 * m10 - m00 * m12;
90 const float c22 = m00 * m11 - m01 * m10;
91
92 const float det3 = m00 * c00 + m01 * c01 + m02 * c02;
93 const float normalSign = det3 < 0.0f ? -1.0f : 1.0f;
94 const float invDet = (std::abs(det3) > 1e-8f) ? (1.0f / det3) : 0.0f;
95
96 // Pack cofactor/det into simd::float4x4 directly (avoids Matrix4 intermediary).
97 const simd::float4x4 normalMatrix = simd::float4x4(
98 simd::float4{c00 * invDet, c01 * invDet, c02 * invDet, 0.0f},
99 simd::float4{c10 * invDet, c11 * invDet, c12 * invDet, 0.0f},
100 simd::float4{c20 * invDet, c21 * invDet, c22 * invDet, 0.0f},
101 simd::float4{0.0f, 0.0f, 0.0f, 1.0f}
102 );
103
104 const ModelData modelData{
105 toSimdMatrix(model),
106 normalMatrix,
107 normalSign,
108 {0.0f, 0.0f, 0.0f}
109 };
110 // Allocate ModelData from ring buffer and update offset (slot 2).
111 // The ring buffer itself was bound to slot 2 in startRenderPass().
112 const size_t transformOffset = transformRing->allocate(&modelData, sizeof(ModelData));
113 encoder->setVertexBufferOffset(transformOffset, 2);
114 }
115
116 // -----------------------------------------------------------------------
117 // Lighting uniforms
118 // -----------------------------------------------------------------------
119
121 const std::vector<GpuLightData>& lights, const Vector3& cameraPosition,
122 const bool enableNormalMaps, const float exposure,
123 const FogParams& fogParams, const ShadowParams& shadowParams,
124 const int toneMapping)
125 {
126 // scene ambient color is authored in sRGB and converted to linear.
127 Color ambientLinear;
128 ambientLinear.linear(&ambientColor);
129 _lightingUniforms.ambientColor[0] = ambientLinear.r;
130 _lightingUniforms.ambientColor[1] = ambientLinear.g;
131 _lightingUniforms.ambientColor[2] = ambientLinear.b;
132 _lightingUniforms.ambientColor[3] = 0.0f;
133
134 const size_t maxLights = std::size(_lightingUniforms.lights);
135 const size_t lightCount = std::min(lights.size(), maxLights);
136 _lightingUniforms.lightCountAndFlags[0] = static_cast<uint32_t>(lightCount);
137 _lightingUniforms.lightCountAndFlags[1] = 0u;
138 _lightingUniforms.lightCountAndFlags[2] = 0u;
139 _lightingUniforms.lightCountAndFlags[3] = 0u;
140
141 for (size_t i = 0; i < maxLights; ++i) {
142 auto& dst = _lightingUniforms.lights[i];
143 if (i < lightCount) {
144 const auto& src = lights[i];
145 Color lightLinear;
146 lightLinear.linear(&src.color);
147 dst.positionRange[0] = src.position.getX();
148 dst.positionRange[1] = src.position.getY();
149 dst.positionRange[2] = src.position.getZ();
150 dst.positionRange[3] = src.range;
151 dst.directionCone[0] = src.direction.getX();
152 dst.directionCone[1] = src.direction.getY();
153 dst.directionCone[2] = src.direction.getZ();
154 dst.colorIntensity[0] = lightLinear.r;
155 dst.colorIntensity[1] = lightLinear.g;
156 dst.colorIntensity[2] = lightLinear.b;
157 dst.colorIntensity[3] = src.intensity;
158 if (src.type == GpuLightType::AreaRect) {
159 // Area rect: repurpose cone slots for half-extents + right axis.
160 // directionCone[3] = areaHalfWidth (was outerConeCos)
161 // coneAngles[0] = areaHalfHeight (was innerConeCos)
162 // coneAngles[1..3] = areaRight.xyz (was outerConeCos/pad/pad)
163 dst.directionCone[3] = src.areaHalfWidth;
164 dst.coneAngles[0] = src.areaHalfHeight;
165 dst.coneAngles[1] = src.areaRight.getX();
166 dst.coneAngles[2] = src.areaRight.getY();
167 dst.coneAngles[3] = src.areaRight.getZ();
168 } else {
169 dst.directionCone[3] = src.outerConeCos;
170 dst.coneAngles[0] = src.innerConeCos;
171 dst.coneAngles[1] = src.outerConeCos;
172 dst.coneAngles[2] = 0.0f;
173 dst.coneAngles[3] = 0.0f;
174 }
175 dst.typeCastShadows[0] = static_cast<uint32_t>(src.type);
176 dst.typeCastShadows[1] = src.castShadows ? 1u : 0u;
177 dst.typeCastShadows[2] = src.falloffModeLinear ? 1u : 0u;
178 // Local shadow map index: 0 or 1 → texture slots 11/12. Encoded as uint.
179 dst.typeCastShadows[3] = (src.shadowMapIndex >= 0)
180 ? static_cast<uint32_t>(src.shadowMapIndex) : 0u;
181 } else {
182 dst = GpuLightUniform{};
183 }
184 }
185
186 if (enableNormalMaps) {
187 _lightingUniforms.flagsAndPad[0] |= (1u << 2);
188 } else {
189 _lightingUniforms.flagsAndPad[0] &= ~(1u << 2);
190 }
191 _lightingUniforms.cameraPositionSkyboxIntensity[0] = cameraPosition.getX();
192 _lightingUniforms.cameraPositionSkyboxIntensity[1] = cameraPosition.getY();
193 _lightingUniforms.cameraPositionSkyboxIntensity[2] = cameraPosition.getZ();
194 _lightingUniforms.skyboxMipAndPad[1] = exposure;
195 // forward-fragment-tail uses this to select the tone mapping curve
196 // when CameraFrame is not active (non-deferred path).
197 _lightingUniforms.skyboxMipAndPad[2] = static_cast<float>(toneMapping);
198
199 Color fogLinear;
200 fogLinear.linear(&fogParams.color);
201 _lightingUniforms.fogColorDensity[0] = fogLinear.r;
202 _lightingUniforms.fogColorDensity[1] = fogLinear.g;
203 _lightingUniforms.fogColorDensity[2] = fogLinear.b;
204 _lightingUniforms.fogColorDensity[3] = fogParams.density;
205 _lightingUniforms.fogStartEndType[0] = fogParams.start;
206 _lightingUniforms.fogStartEndType[1] = fogParams.end;
207 _lightingUniforms.fogStartEndType[2] = fogParams.enabled ? 1.0f : 0.0f;
208 _lightingUniforms.fogStartEndType[3] = 0.0f;
209
210 _lightingUniforms.shadowBiasNormalStrength[0] = shadowParams.bias;
211 _lightingUniforms.shadowBiasNormalStrength[1] = shadowParams.normalBias;
212 _lightingUniforms.shadowBiasNormalStrength[2] = shadowParams.strength;
213 _lightingUniforms.shadowBiasNormalStrength[3] = shadowParams.enabled ? 1.0f : 0.0f;
214
215 // CSM: pack cascade matrix palette, distances, and params.
216 //lines 279-282.
217 std::memcpy(_lightingUniforms.shadowMatrixPalette,
218 shadowParams.shadowMatrixPalette,
219 sizeof(_lightingUniforms.shadowMatrixPalette));
220 std::memcpy(_lightingUniforms.shadowCascadeDistances,
221 shadowParams.shadowCascadeDistances,
222 sizeof(_lightingUniforms.shadowCascadeDistances));
223 _lightingUniforms.shadowCascadeParams[0] = static_cast<float>(shadowParams.numCascades);
224 _lightingUniforms.shadowCascadeParams[1] = shadowParams.cascadeBlend;
225 _lightingUniforms.shadowCascadeParams[2] = 0.0f;
226 _lightingUniforms.shadowCascadeParams[3] = 0.0f;
227
228 _shadowTexture = shadowParams.shadowMap;
229
230 // Local light shadows (spot/point): pack VP matrices and per-light params.
231 // Omni lights use cubemap shadow textures (bound separately); spot lights use 2D.
232 _localShadowTexture0 = nullptr;
233 _localShadowTexture1 = nullptr;
234 _omniShadowCube0 = nullptr;
235 _omniShadowCube1 = nullptr;
236
237 // Helper lambda to pack a local shadow entry.
238 auto packLocalShadow = [&](int idx, const ShadowParams::LocalShadow& ls) {
239 float* matDst = (idx == 0) ? _lightingUniforms.localShadowMatrix0 : _lightingUniforms.localShadowMatrix1;
240 float* paramsDst = (idx == 0) ? _lightingUniforms.localShadowParams0 : _lightingUniforms.localShadowParams1;
241
242 if (ls.isOmni) {
243 // Omni: bind cubemap texture, pack omni-specific params.
244 // The shader-side depth bias must be very small because perspective
245 // depth is highly compressed near 1.0 for cubemap shadow maps.
246 // The primary self-shadowing prevention is hardware polygon offset
247 // (setDepthBias in the shadow render pass), so the shader bias is
248 // only a secondary guard. Use a fixed small value (0.001) rather
249 // than the light's shadowBias which is meant for polygon offset
250 // (scaled by ×1000 in the render pass).
251 constexpr float omniShaderBias = 0.001f;
252 const float farClip = ls.viewProjection.getElement(0, 0);
253 if (idx == 0) {
254 _omniShadowCube0 = ls.shadowMap;
255 _lightingUniforms.omniShadowParams0[0] = 0.01f; // near
256 _lightingUniforms.omniShadowParams0[1] = farClip; // far (stored in VP[0][0] by renderer)
257 _lightingUniforms.omniShadowParams0[2] = omniShaderBias;
258 _lightingUniforms.omniShadowParams0[3] = ls.normalBias;
259 _lightingUniforms.omniShadowParams0Extra[0] = ls.intensity;
260 } else {
261 _omniShadowCube1 = ls.shadowMap;
262 _lightingUniforms.omniShadowParams1[0] = 0.01f;
263 _lightingUniforms.omniShadowParams1[1] = farClip;
264 _lightingUniforms.omniShadowParams1[2] = omniShaderBias;
265 _lightingUniforms.omniShadowParams1[3] = ls.normalBias;
266 _lightingUniforms.omniShadowParams1Extra[0] = ls.intensity;
267 }
268 // Don't set 2D shadow texture for omni lights.
269 std::memset(matDst, 0, 16 * sizeof(float));
270 } else {
271 // Spot: bind 2D texture, pack VP matrix.
272 if (idx == 0) {
273 _localShadowTexture0 = ls.shadowMap;
274 } else {
275 _localShadowTexture1 = ls.shadowMap;
276 }
277 const auto& m = ls.viewProjection;
278 for (int col = 0; col < 4; ++col) {
279 for (int row = 0; row < 4; ++row) {
280 matDst[col * 4 + row] = m.getElement(row, col);
281 }
282 }
283 }
284 paramsDst[0] = ls.bias;
285 paramsDst[1] = ls.normalBias;
286 paramsDst[2] = ls.intensity;
287 paramsDst[3] = ls.isOmni ? 1.0f : 0.0f; // Flag: 1=omni cubemap, 0=spot 2D
288 };
289
290 for (int i = 0; i < ShadowParams::kMaxLocalShadows; ++i) {
291 if (i < shadowParams.localShadowCount) {
292 packLocalShadow(i, shadowParams.localShadows[i]);
293 } else {
294 // Clear unused slots.
295 float* matDst = (i == 0) ? _lightingUniforms.localShadowMatrix0 : _lightingUniforms.localShadowMatrix1;
296 float* paramsDst = (i == 0) ? _lightingUniforms.localShadowParams0 : _lightingUniforms.localShadowParams1;
297 std::memset(matDst, 0, 16 * sizeof(float));
298 paramsDst[0] = 0.0001f;
299 paramsDst[1] = 0.0f;
300 paramsDst[2] = 1.0f;
301 paramsDst[3] = 0.0f;
302 }
303 }
304 }
305
306 // -----------------------------------------------------------------------
307 // Environment uniforms
308 // -----------------------------------------------------------------------
309
310 void MetalUniformBinder::setEnvironmentUniforms(Texture* envAtlas, const float skyboxIntensity,
311 const float skyboxMip, const Vector3& skyDomeCenter, const bool isDome,
312 Texture* skyboxCubeMap)
313 {
314 _envAtlasTexture = envAtlas;
315 _skyboxCubeMapTexture = skyboxCubeMap;
316 _lightingUniforms.cameraPositionSkyboxIntensity[3] = skyboxIntensity;
317 _lightingUniforms.skyboxMipAndPad[0] = skyboxMip;
318
319 // pack dome center for SKYTYPE_DOME/BOX
320 _lightingUniforms.skyDomeCenter[0] = skyDomeCenter.getX();
321 _lightingUniforms.skyDomeCenter[1] = skyDomeCenter.getY();
322 _lightingUniforms.skyDomeCenter[2] = skyDomeCenter.getZ();
323 _lightingUniforms.skyDomeCenter[3] = isDome ? 1.0f : 0.0f;
324 if (_envAtlasTexture) {
325 _lightingUniforms.flagsAndPad[0] |= (1u << 1);
326 if (_envAtlasTexture->encoding() == TextureEncoding::RGBP) {
327 _lightingUniforms.flagsAndPad[0] |= (1u << 3);
328 } else {
329 _lightingUniforms.flagsAndPad[0] &= ~(1u << 3);
330 }
331 if (_envAtlasTexture->encoding() == TextureEncoding::RGBM) {
332 _lightingUniforms.flagsAndPad[0] |= (1u << 4);
333 } else {
334 _lightingUniforms.flagsAndPad[0] &= ~(1u << 4);
335 }
336 } else {
337 _lightingUniforms.flagsAndPad[0] &= ~(1u << 1);
338 _lightingUniforms.flagsAndPad[0] &= ~(1u << 3);
339 _lightingUniforms.flagsAndPad[0] &= ~(1u << 4);
340 }
341 }
342
343 // -----------------------------------------------------------------------
344 // Atmosphere uniforms
345 // -----------------------------------------------------------------------
346
347 void MetalUniformBinder::setAtmosphereUniforms(const void* data, const size_t size)
348 {
349 if (data && size <= sizeof(_atmosphereUniforms)) {
350 std::memcpy(&_atmosphereUniforms, data, size);
351 }
352 }
353
354 // -----------------------------------------------------------------------
355 // Per-draw uniform submission with deduplication
356 // -----------------------------------------------------------------------
357
358 void MetalUniformBinder::submitPerDrawUniforms(MTL::RenderCommandEncoder* encoder,
359 MetalUniformRingBuffer* uniformRing,
360 const Material* currentMaterial,
361 const void* uniformData,
362 const size_t uniformSize,
363 const bool hdrPass)
364 {
365 // Material uniforms at slot 3 — skip ring allocation when same material is
366 // bound as previous draw (consecutive draws sharing a material produce
367 // identical uniform data, so we can reuse the previous ring offset).
368 size_t materialOffset;
369 if (_materialBoundThisPass && currentMaterial == _lastBoundMaterial) {
370 materialOffset = _lastMaterialOffset;
371 } else {
372 materialOffset = uniformRing->allocate(uniformData, uniformSize);
373 _lastBoundMaterial = currentMaterial;
374 _lastMaterialOffset = materialOffset;
375 _materialBoundThisPass = true;
376 }
377 encoder->setFragmentBufferOffset(materialOffset, 3);
378 encoder->setVertexBufferOffset(materialOffset, 3);
379
380 // Set HDR pass flag (bit 5) — forward shaders check this at runtime
381 // to skip tonemapping + gamma when CameraFrame handles them.
382 if (hdrPass) {
383 _lightingUniforms.flagsAndPad[0] |= (1u << 5);
384 } else {
385 _lightingUniforms.flagsAndPad[0] &= ~(1u << 5);
386 }
387
388 // LightingUniforms at slot 4 — hash-based deduplication to skip ring
389 // allocation when lighting data hasn't changed. 95%+ of draws within a
390 // layer have identical lighting because all mesh instances default to
391 // MASK_AFFECT_DYNAMIC = 1 and all lights use the same default mask.
392 const uint32_t lightingHash = hash32Fnv1a(
393 reinterpret_cast<const uint32_t*>(&_lightingUniforms),
394 sizeof(LightingUniforms) / sizeof(uint32_t));
395 size_t lightingOffset;
396 if (_lightingBoundThisPass && lightingHash == _lastLightingHash) {
397 lightingOffset = _lastLightingOffset;
398 } else {
399 lightingOffset = uniformRing->allocate(&_lightingUniforms, sizeof(LightingUniforms));
400 _lastLightingHash = lightingHash;
401 _lastLightingOffset = lightingOffset;
402 _lightingBoundThisPass = true;
403 }
404 encoder->setFragmentBufferOffset(lightingOffset, 4);
405 }
406
407 // -----------------------------------------------------------------------
408 // Pass lifecycle
409 // -----------------------------------------------------------------------
410
412 {
413 _sceneDataBoundThisPass = false;
414 _lightingBoundThisPass = false;
415 _materialBoundThisPass = false;
416 _lastBoundMaterial = nullptr;
417 }
418}
Base class for GPU materials — owns uniform data, texture bindings, blend/depth state,...
Definition material.h:143
void setEnvironmentUniforms(Texture *envAtlas, float skyboxIntensity, float skyboxMip, const Vector3 &skyDomeCenter, bool isDome, Texture *skyboxCubeMap)
Pack environment uniforms (skybox, env atlas) into LightingUniforms.
void setTransformUniforms(MTL::RenderCommandEncoder *encoder, MetalUniformRingBuffer *transformRing, const Matrix4 &viewProjection, const Matrix4 &model)
Pack transform uniforms (SceneData VP + ModelData per draw).
void setLightingUniforms(const Color &ambientColor, const std::vector< GpuLightData > &lights, const Vector3 &cameraPosition, bool enableNormalMaps, float exposure, const FogParams &fogParams, const ShadowParams &shadowParams, int toneMapping=0)
Pack lighting uniforms into the internal LightingUniforms struct.
void submitPerDrawUniforms(MTL::RenderCommandEncoder *encoder, MetalUniformRingBuffer *uniformRing, const Material *currentMaterial, const void *uniformData, size_t uniformSize, bool hdrPass)
void setAtmosphereUniforms(const void *data, size_t size)
Pack atmosphere uniforms (Nishita scattering) into AtmosphereUniforms.
size_t allocate(const void *data, size_t dataSize)
GPU texture resource supporting 2D, cubemap, volume, and array formats with mipmap management.
Definition texture.h:57
AtmosphereUniforms _atmosphereUniforms
simd::float4x4 toSimdMatrix(const Matrix4 &matrix)
Convert a column-major Matrix4 to a SIMD float4x4.
Definition metalUtils.h:20
uint32_t hash32Fnv1a(const uint32_t *array, size_t length)
Definition utils.cpp:10
simd::float4x4 toSimdMatrix(const Matrix4 &matrix)
Convert a column-major Matrix4 to a SIMD float4x4.
Definition metalUtils.h:20
RGBA color with floating-point components in [0, 1].
Definition color.h:18
Color & linear(const Color *src=nullptr)
Definition color.cpp:89
4x4 column-major transformation matrix with SIMD acceleration.
Definition matrix4.h:31
float getElement(const int col, int row) const
Definition matrix4.h:355
struct visutwin::canvas::ShadowParams::LocalShadow localShadows[kMaxLocalShadows]
static constexpr int kMaxLocalShadows
3D vector for positions, directions, and normals with multi-backend SIMD acceleration.
Definition vector3.h:29