VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
metalGraphicsDevice.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 13.09.2025.
5//
7
8#include <algorithm>
9#include <cstring>
10#include "metalCoCPass.h"
11#include "metalComposePass.h"
13#include "metalDofBlurPass.h"
14#include "metalSsaoPass.h"
15#include "metalTaaPass.h"
16#include "metalTexture.h"
18#include "metalIndexBuffer.h"
19#include "metalShader.h"
20#include "metalRenderPipeline.h"
21#include "metalRenderTarget.h"
22#include "metalUtils.h"
23#include "metalVertexBuffer.h"
28#include "spdlog/spdlog.h"
29
30namespace visutwin::canvas
31{
32 // Import metal utility functions into this translation unit for brevity.
37
38 namespace
39 {
40 // Per-draw uniform structs — defined at file scope so they can be used
41 // by both setTransformUniforms() and the ring buffer sizing in the constructor.
42 struct SceneData
43 {
44 simd::float4x4 projViewMatrix;
45 };
46
47 struct ModelData
48 {
49 simd::float4x4 modelMatrix;
50 simd::float4x4 normalMatrix;
51 float normalSign;
52 float _pad[3];
53 };
54 }
55
56 constexpr int INDIRECT_ENTRY_BYTE_SIZE = 5 * 4; // 5 x 32bit
57
59 _window(options.window),
60 _device(nullptr),
61 _commandQueue(nullptr),
62 _metalLayer(nullptr)
63 {
64 _renderPassEncoder = nullptr;
65
66 _metalLayer = static_cast<CA::MetalLayer*>(options.swapChain);
67 assert(_metalLayer && "Missing CAMetalLayer swap chain");
68
69 _device = _metalLayer->device();
70 assert(_device && "CAMetalLayer has no Metal device");
71
72 _metalLayer->setDevice(_device);
73 _metalLayer->setPixelFormat(MTL::PixelFormatBGRA8Unorm);
74 // DEVIATION: WebGL uses a persistent back buffer that survives across
75 // render passes. With Metal, multiple back-buffer passes in one frame (compose →
76 // afterPass) need the drawable to be loadable across command buffers. Setting
77 // framebufferOnly to false allows LoadActionLoad to reliably load previous content.
78 _metalLayer->setFramebufferOnly(false);
79
80 _commandQueue = _device->newCommandQueue();
81 assert(_commandQueue && "Failed to create Metal command queue");
82
83 auto* samplerDesc = MTL::SamplerDescriptor::alloc()->init();
84 samplerDesc->setMinFilter(MTL::SamplerMinMagFilterLinear);
85 samplerDesc->setMagFilter(MTL::SamplerMinMagFilterLinear);
86 samplerDesc->setSAddressMode(MTL::SamplerAddressModeRepeat);
87 samplerDesc->setTAddressMode(MTL::SamplerAddressModeRepeat);
88 _defaultSampler = _device->newSamplerState(samplerDesc);
89 samplerDesc->release();
90
91 auto* depthDesc = MTL::DepthStencilDescriptor::alloc()->init();
92 // Default depth function is FUNC_LESSEQUAL.
93 // This is critical for the skybox, which renders at depth ≈ 1.0 and needs
94 // LESSEQUAL to pass the depth test against the cleared depth buffer (also 1.0).
95 depthDesc->setDepthCompareFunction(MTL::CompareFunctionLessEqual);
96 depthDesc->setDepthWriteEnabled(true);
97 _defaultDepthStencilState = _device->newDepthStencilState(depthDesc);
98
99 // depth-test-only state (no depth writes).
100 // Used by transparent materials and the skybox that need depth testing
101 // but should not write to the depth buffer.
102 depthDesc->setDepthWriteEnabled(false);
103 _noWriteDepthStencilState = _device->newDepthStencilState(depthDesc);
104 depthDesc->release();
105
106 _renderPipeline = std::make_unique<MetalRenderPipeline>(this);
107 _computePipeline = std::make_unique<MetalComputePipeline>(this);
108
109 // Triple-buffered ring buffers for per-draw uniform data.
110 // ModelData is 136B → aligns to 256B slots.
111 // LightingUniforms is the largest struct at ~544B → aligns to 768B slots.
112 // MaterialUniforms (~48B) also uses the uniform ring (fits within 768B slot).
113 //
114 // _transformRing: 1 allocation per draw call (ModelData at slot 2).
115 // _uniformRing: 2 allocations per draw call (MaterialUniforms at slot 3 +
116 // LightingUniforms at slot 4), so needs 2× the draw capacity.
117 static constexpr size_t kMaxDrawsPerFrame = 4096;
118 _transformRing = std::make_unique<MetalUniformRingBuffer>(
119 _device, kMaxDrawsPerFrame, sizeof(ModelData), "TransformRing");
120 _uniformRing = std::make_unique<MetalUniformRingBuffer>(
121 _device, kMaxDrawsPerFrame * 2, sizeof(MetalUniformBinder::LightingUniforms), "UniformRing");
122
123 // Variable-size bump-allocator ring buffer for dynamic batch matrix palettes.
124 // 256KB per frame region supports up to 4096 total instances across all batches.
125 _paletteRing = std::make_unique<MetalPaletteRingBuffer>(_device, "PaletteRing");
126
127 RenderTargetOptions backBufferOptions;
128 backBufferOptions.graphicsDevice = this;
129 backBufferOptions.name = "MetalBackBuffer";
130 backBufferOptions.samples = 1;
131 setBackBuffer(std::make_shared<MetalBackBufferRenderTarget>(backBufferOptions));
132 }
133
135 {
136 if (_renderPassEncoder) {
137 _renderPassEncoder->endEncoding();
138 _renderPassEncoder = nullptr;
139 }
140
141 // _pipelineState is a non-owning pointer; the render pipeline cache
142 // owns pipeline states and releases them in its destructor.
143 _pipelineState = nullptr;
144
145 if (_defaultSampler) {
146 _defaultSampler->release();
147 _defaultSampler = nullptr;
148 }
149
150 if (_defaultDepthStencilState) {
151 _defaultDepthStencilState->release();
152 _defaultDepthStencilState = nullptr;
153 }
154 if (_noWriteDepthStencilState) {
155 _noWriteDepthStencilState->release();
156 _noWriteDepthStencilState = nullptr;
157 }
158 // _taaPass and _composePass destructors handle their own depth stencil state release.
159 _taaPass.reset();
160 _composePass.reset();
161
162 if (_backBufferDepthTexture) {
163 _backBufferDepthTexture->release();
164 _backBufferDepthTexture = nullptr;
165 }
166
167 if (_clusterLightBuffer) {
168 _clusterLightBuffer->release();
169 _clusterLightBuffer = nullptr;
170 }
171 if (_clusterCellBuffer) {
172 _clusterCellBuffer->release();
173 _clusterCellBuffer = nullptr;
174 }
175
176 if (_framePool) {
177 _framePool->release();
178 _framePool = nullptr;
179 }
180
181 if (_commandQueue) {
182 _commandQueue->release();
183 _commandQueue = nullptr;
184 }
185
186 if (_device && _ownsDevice) {
187 _device->release();
188 _device = nullptr;
189 }
190 }
191
192 std::pair<int, int> MetalGraphicsDevice::size() const
193 {
194 int w, h;
195 SDL_GetWindowSizeInPixels(_window, &w, &h);
196 return {w, h};
197 }
198
200 {
201 if (!_metalLayer) {
202 return;
203 }
204
205 // Drain the previous frame's autorelease pool to release Metal-cpp
206 // autoreleased objects (command buffers, render pass descriptors, etc.)
207 // that accumulated during the last frame.
208 if (_framePool) {
209 _framePool->release();
210 _framePool = nullptr;
211 }
212 _framePool = NS::AutoreleasePool::alloc()->init();
213
214 // Advance ring buffers to next frame region. This blocks if the GPU
215 // hasn't finished with the region we're about to write to.
216 _transformRing->beginFrame();
217 _uniformRing->beginFrame();
218 _paletteRing->beginFrame();
219 _pendingPaletteOffset = SIZE_MAX;
220
221 // Release the previous frame's cached drawable so the first back-buffer
222 // render pass of this frame acquires a fresh one.
223 _frameDrawable = nullptr;
224
225 const auto [w, h] = size();
226 const CGSize drawableSize = _metalLayer->drawableSize();
227 if (static_cast<int>(drawableSize.width) != w || static_cast<int>(drawableSize.height) != h) {
228 _metalLayer->setDrawableSize(CGSize{static_cast<CGFloat>(w), static_cast<CGFloat>(h)});
229 }
230 }
231
233 {
234 // Always commit an end-of-frame command buffer with ring buffer completion
235 // handlers so the dispatch semaphores are signaled. If no back-buffer pass
236 // occurred this frame (e.g. post-processing toggled off with stale render
237 // actions), _frameDrawable may be null. We must still signal the semaphores
238 // — otherwise beginFrame() blocks forever after kMaxInflightFrames.
239 if (_commandQueue) {
240 auto* endBuffer = _commandQueue->commandBuffer();
241 if (endBuffer) {
242 // Register ring buffer completion handlers on this command buffer.
243 // This is the LAST command buffer committed per frame, so the
244 // semaphore signals correctly track whole-frame GPU completion.
245 _transformRing->endFrame(endBuffer);
246 _uniformRing->endFrame(endBuffer);
247 _paletteRing->endFrame(endBuffer);
248
249 if (_frameDrawable) {
250 endBuffer->presentDrawable(_frameDrawable);
251 }
252 endBuffer->commit();
253 }
254 }
255 // The frame drawable is released at the start of the next frame.
256 }
257
258 int MetalGraphicsDevice::submitVertexBuffer(const std::shared_ptr<VertexBuffer>& vertexBuffer, int slot)
259 {
260 if (!vertexBuffer || !_renderPassEncoder) {
261 return 0;
262 }
263 auto* vbBuffer = static_cast<MTL::Buffer*>(vertexBuffer->nativeBuffer());
264 if (!vbBuffer) {
265 spdlog::warn("Vertex buffer has no native Metal buffer bound");
266 return 0;
267 }
268
269 _renderPassEncoder->setVertexBuffer(vbBuffer, 0, static_cast<NS::UInteger>(slot));
270 return 1;
271 }
272
273 std::shared_ptr<Shader> MetalGraphicsDevice::createShader(const ShaderDefinition& definition,
274 const std::string& sourceCode)
275 {
276 return std::make_shared<MetalShader>(this, definition, sourceCode);
277 }
278
279 std::unique_ptr<gpu::HardwareTexture> MetalGraphicsDevice::createGPUTexture(Texture* texture)
280 {
281 auto hwTexture = std::make_unique<gpu::MetalTexture>(texture);
282 hwTexture->create(this);
283 return hwTexture;
284 }
285
286 std::shared_ptr<VertexBuffer> MetalGraphicsDevice::createVertexBuffer(const std::shared_ptr<VertexFormat>& format,
287 const int numVertices, const VertexBufferOptions& options)
288 {
289 return std::make_shared<MetalVertexBuffer>(this, format, numVertices, options);
290 }
291
293 const std::shared_ptr<VertexFormat>& format,
294 int numVertices, MTL::Buffer* externalBuffer)
295 {
296 return std::make_shared<MetalVertexBuffer>(this, format, numVertices, externalBuffer);
297 }
298
300 const std::shared_ptr<VertexFormat>& format,
301 int numVertices, void* nativeBuffer)
302 {
303 return createVertexBufferFromMTLBuffer(format, numVertices, static_cast<MTL::Buffer*>(nativeBuffer));
304 }
305
306 std::shared_ptr<IndexBuffer> MetalGraphicsDevice::createIndexBuffer(const IndexFormat format, const int numIndices,
307 const std::vector<uint8_t>& data)
308 {
309 auto indexBuffer = std::make_shared<MetalIndexBuffer>(this, format, numIndices);
310 if (!data.empty()) {
311 indexBuffer->setData(data);
312 }
313 return indexBuffer;
314 }
315
316 std::shared_ptr<RenderTarget> MetalGraphicsDevice::createRenderTarget(const RenderTargetOptions& options)
317 {
318 return std::make_shared<MetalRenderTarget>(options);
319 }
320
322 {
323 if (!_renderPassEncoder) return;
324 if (!_composePass) _composePass = std::make_unique<MetalComposePass>(this);
325 _composePass->execute(_renderPassEncoder, params, _renderPipeline.get(), renderTarget(),
326 _bindGroupFormats, _defaultSampler);
327 }
328
329 void MetalGraphicsDevice::executeTAAPass(Texture* sourceTexture, Texture* historyTexture, Texture* depthTexture,
330 const Matrix4& viewProjectionPrevious, const Matrix4& viewProjectionInverse,
331 const std::array<float, 4>& jitters, const std::array<float, 4>& cameraParams, const bool highQuality,
332 const bool historyValid)
333 {
334 if (!_renderPassEncoder) return;
335 if (!_composePass) _composePass = std::make_unique<MetalComposePass>(this);
336 if (!_taaPass) _taaPass = std::make_unique<MetalTaaPass>(this, _composePass.get());
337 _taaPass->execute(_renderPassEncoder, sourceTexture, historyTexture, depthTexture,
338 viewProjectionPrevious, viewProjectionInverse, jitters, cameraParams,
339 highQuality, historyValid, _renderPipeline.get(), renderTarget(),
340 _bindGroupFormats, _defaultSampler, _defaultDepthStencilState);
341 }
342
344 {
345 if (!_renderPassEncoder) return;
346 if (!_composePass) _composePass = std::make_unique<MetalComposePass>(this);
347 if (!_ssaoPass) _ssaoPass = std::make_unique<MetalSsaoPass>(this, _composePass.get());
348 _ssaoPass->execute(_renderPassEncoder, params, _renderPipeline.get(), renderTarget(),
349 _bindGroupFormats, _defaultSampler, _defaultDepthStencilState);
350 }
351
353 {
354 if (!_renderPassEncoder) return;
355 if (!_composePass) _composePass = std::make_unique<MetalComposePass>(this);
356 if (!_cocPass) _cocPass = std::make_unique<MetalCoCPass>(this, _composePass.get());
357 _cocPass->execute(_renderPassEncoder, params, _renderPipeline.get(), renderTarget(),
358 _bindGroupFormats, _defaultSampler, _defaultDepthStencilState);
359 }
360
362 {
363 if (!_renderPassEncoder) return;
364 if (!_composePass) _composePass = std::make_unique<MetalComposePass>(this);
365 if (!_dofBlurPass) _dofBlurPass = std::make_unique<MetalDofBlurPass>(this, _composePass.get());
366 _dofBlurPass->execute(_renderPassEncoder, params, _renderPipeline.get(), renderTarget(),
367 _bindGroupFormats, _defaultSampler, _defaultDepthStencilState);
368 }
369
371 {
372 if (!_renderPassEncoder) return;
373 if (!_composePass) _composePass = std::make_unique<MetalComposePass>(this);
374 if (horizontal) {
375 if (!_blurPassH) _blurPassH = std::make_unique<MetalDepthAwareBlurPass>(this, _composePass.get(), true);
376 _blurPassH->execute(_renderPassEncoder, params, _renderPipeline.get(), renderTarget(),
377 _bindGroupFormats, _defaultSampler, _defaultDepthStencilState);
378 } else {
379 if (!_blurPassV) _blurPassV = std::make_unique<MetalDepthAwareBlurPass>(this, _composePass.get(), false);
380 _blurPassV->execute(_renderPassEncoder, params, _renderPipeline.get(), renderTarget(),
381 _bindGroupFormats, _defaultSampler, _defaultDepthStencilState);
382 }
383 }
384
385 void MetalGraphicsDevice::computeDispatch(const std::vector<Compute*>& computes, const std::string& label)
386 {
387 if (computes.empty() || !_commandQueue || !_device) {
388 return;
389 }
390
392 spdlog::warn("Skipping compute dispatch while a render/compute encoder is active");
393 return;
394 }
395
396 auto* commandBuffer = _commandQueue->commandBuffer();
397 if (!commandBuffer) {
398 spdlog::warn("Failed to allocate command buffer for compute dispatch");
399 return;
400 }
401
402 auto* encoder = commandBuffer->computeCommandEncoder();
403 if (!encoder) {
404 spdlog::warn("Failed to allocate compute encoder");
405 return;
406 }
407
408 if (!label.empty()) {
409 encoder->pushDebugGroup(NS::String::string(label.c_str(), NS::UTF8StringEncoding));
410 }
411
412 for (const auto* compute : computes) {
413 if (!compute || !compute->shader()) {
414 continue;
415 }
416
417 auto* pipelineState = _computePipeline->get(compute->shader());
418 if (!pipelineState) {
419 continue;
420 }
421
422 encoder->setComputePipelineState(pipelineState);
423
424 // DEVIATION: C++ port does not yet have compute bind group
425 // declarations/reflection, so we bind textures in deterministic name order.
426 std::vector<std::pair<std::string, Texture*>> textureBindings;
427 textureBindings.reserve(compute->textureParameters().size());
428 for (const auto& [name, texture] : compute->textureParameters()) {
429 textureBindings.emplace_back(name, texture);
430 }
431 std::sort(textureBindings.begin(), textureBindings.end(),
432 [](const auto& a, const auto& b) { return a.first < b.first; });
433
434 uint32_t slot = 0u;
435 for (const auto& [_, texture] : textureBindings) {
436 auto* hwTexture = texture ? dynamic_cast<gpu::MetalTexture*>(texture->impl()) : nullptr;
437 encoder->setTexture(hwTexture ? hwTexture->raw() : nullptr, static_cast<NS::UInteger>(slot));
438 ++slot;
439 }
440
441 const MTL::Size threadgroups(
442 static_cast<NS::UInteger>(compute->dispatchX()),
443 static_cast<NS::UInteger>(compute->dispatchY()),
444 static_cast<NS::UInteger>(compute->dispatchZ())
445 );
446
447 // DEVIATION: workgroup size is currently fixed to 8x8x1 for parity with
448 // Edge-detect compute shader.
449 const MTL::Size threadsPerThreadgroup(8, 8, 1);
450 encoder->dispatchThreadgroups(threadgroups, threadsPerThreadgroup);
451 }
452
453 if (!label.empty()) {
454 encoder->popDebugGroup();
455 }
456
457 encoder->endEncoding();
458 commandBuffer->commit();
459 }
460
461 void MetalGraphicsDevice::setDepthBias(const float depthBias, const float slopeScale, const float clamp)
462 {
463 if (!_renderPassEncoder) return;
464 _renderPassEncoder->setDepthBias(depthBias, slopeScale, clamp);
465 }
466
468 {
469 _indirectDrawBuffer = static_cast<MTL::Buffer*>(nativeBuffer);
470 }
471
472 void MetalGraphicsDevice::setDynamicBatchPalette(const void* data, const size_t size)
473 {
474 // Allocate from the palette ring buffer (variable-size bump allocator).
475 // This replaces the previous setVertexBytes() path which was limited to 4KB.
476 // The ring buffer supports arbitrary palette sizes within the 256KB/frame budget.
477 _pendingPaletteOffset = _paletteRing->allocate(data, size);
478 }
479
480 void MetalGraphicsDevice::setClusterBuffers(const void* lightData, const size_t lightSize,
481 const void* cellData, const size_t cellSize)
482 {
483 if (!lightData || lightSize == 0 || !cellData || cellSize == 0) {
484 _clusterBuffersSet = false;
485 return;
486 }
487
488 // Grow light buffer if needed.
489 if (!_clusterLightBuffer || _clusterLightBufferCapacity < lightSize) {
490 if (_clusterLightBuffer) {
491 _clusterLightBuffer->release();
492 }
493 _clusterLightBufferCapacity = lightSize * 2; // over-allocate to reduce reallocations
494 _clusterLightBuffer = _device->newBuffer(_clusterLightBufferCapacity,
495 MTL::ResourceStorageModeShared);
496 }
497
498 // Grow cell buffer if needed.
499 if (!_clusterCellBuffer || _clusterCellBufferCapacity < cellSize) {
500 if (_clusterCellBuffer) {
501 _clusterCellBuffer->release();
502 }
503 _clusterCellBufferCapacity = cellSize * 2;
504 _clusterCellBuffer = _device->newBuffer(_clusterCellBufferCapacity,
505 MTL::ResourceStorageModeShared);
506 }
507
508 // Copy data.
509 std::memcpy(_clusterLightBuffer->contents(), lightData, lightSize);
510 std::memcpy(_clusterCellBuffer->contents(), cellData, cellSize);
511
512 _clusterBuffersSet = true;
513 }
514
515 void MetalGraphicsDevice::setClusterGridParams(const float* boundsMin, const float* boundsRange,
516 const float* cellsCountByBoundsSize,
517 const int cellsX, const int cellsY, const int cellsZ, const int maxLightsPerCell,
518 const int numClusteredLights)
519 {
520 _uniformBinder.setClusterParams(boundsMin, boundsRange, cellsCountByBoundsSize,
521 cellsX, cellsY, cellsZ, maxLightsPerCell, numClusteredLights);
522 }
523
524 void MetalGraphicsDevice::setTransformUniforms(const Matrix4& viewProjection, const Matrix4& model)
525 {
526 if (!_renderPassEncoder) return;
527 _uniformBinder.setTransformUniforms(_renderPassEncoder, _transformRing.get(), viewProjection, model);
528 }
529
530 void MetalGraphicsDevice::setLightingUniforms(const Color& ambientColor, const std::vector<GpuLightData>& lights,
531 const Vector3& cameraPosition, const bool enableNormalMaps, const float exposure,
532 const FogParams& fogParams, const ShadowParams& shadowParams, const int toneMapping)
533 {
534 _uniformBinder.setLightingUniforms(ambientColor, lights, cameraPosition,
535 enableNormalMaps, exposure, fogParams, shadowParams, toneMapping);
536 }
537
538 void MetalGraphicsDevice::setEnvironmentUniforms(Texture* envAtlas, const float skyboxIntensity,
539 const float skyboxMip, const Vector3& skyDomeCenter, const bool isDome,
540 Texture* skyboxCubeMap)
541 {
542 _uniformBinder.setEnvironmentUniforms(envAtlas, skyboxIntensity, skyboxMip,
543 skyDomeCenter, isDome, skyboxCubeMap);
544 }
545
546 void MetalGraphicsDevice::setAtmosphereUniforms(const void* data, const size_t size)
547 {
548 _uniformBinder.setAtmosphereUniforms(data, size);
549 }
550
551 void MetalGraphicsDevice::draw(const Primitive& primitive, const std::shared_ptr<IndexBuffer>& indexBuffer,
552 int numInstances, const int indirectSlot, const bool first, const bool last)
553 {
554 if (!_shader || !_renderPassEncoder) {
555 spdlog::warn("Draw skipped: shader or render encoder is not set");
556 return;
557 }
558
559 MTL::RenderCommandEncoder* passEncoder = _renderPassEncoder;
560 assert(passEncoder != nullptr);
561
562 MTL::RenderPipelineState* pipelineState = _pipelineState;
563
564 // Get vertex buffers — use const references to avoid shared_ptr refcount churn.
565 const auto& vb0 = _vertexBuffers.size() > 0 ? _vertexBuffers[0] : _nullVertexBuffer;
566 const auto& vb1 = _vertexBuffers.size() > 1 ? _vertexBuffers[1] : _nullVertexBuffer;
567
568 // Hardware instancing: find VB with isInstancing() format (set at slot 5 by renderer).
569 // Use raw pointer — the shared_ptr in _vertexBuffers keeps the object alive.
570 const std::shared_ptr<VertexBuffer>* instancingVBPtr = nullptr;
571 for (size_t i = 2; i < _vertexBuffers.size(); ++i) {
572 if (_vertexBuffers[i] && _vertexBuffers[i]->format() &&
573 _vertexBuffers[i]->format()->isInstancing()) {
574 instancingVBPtr = &_vertexBuffers[i];
575 break;
576 }
577 }
578
579 if (first) {
580 // Submit vertex buffers
581 if (vb0) {
582 int vbSlot = submitVertexBuffer(vb0, 0);
583 if (vb1) {
584 //validateVBLocations(vb0, vb1);
585 submitVertexBuffer(vb1, vbSlot);
586 }
587 }
588
589 // Submit instancing vertex buffer at slot 5 for vertex descriptor layout(5).
590 if (instancingVBPtr) {
591 submitVertexBuffer(*instancingVBPtr, 5);
592 }
593
594 // Validate attributes
595 //validateAttributes(_shader, vb0, vb1);
596
597 // Get or create a render pipeline (includes instancing format for perInstance step function)
598 const int ibFormat = indexBuffer ? indexBuffer->format() : -1;
599 const auto instFmt = instancingVBPtr ? (*instancingVBPtr)->format() : nullptr;
600 pipelineState = _renderPipeline->get(primitive, vb0 != nullptr ? vb0->format() : nullptr,
601 vb1 != nullptr ? vb1->format() : nullptr,
602 ibFormat, _shader, _renderTarget, _bindGroupFormats, _blendState, _depthState,
604 if (!pipelineState) {
605 spdlog::error("Draw skipped: failed to create/render pipeline state");
606 return;
607 }
608
609 // Set the pipeline state if changed.
610 // NOTE: _pipelineState is a non-owning (borrowing) pointer — the render
611 // pipeline cache (_renderPipeline) owns pipeline states and releases them
612 // in its destructor. Do NOT call release() here; the previous code did
613 // so and caused a double-release (cache destructor also releases).
614 if (_pipelineState != pipelineState) {
615 _pipelineState = pipelineState;
616 passEncoder->setRenderPipelineState(pipelineState);
617 }
618 }
619
620 MTL::Buffer* ibBuffer = nullptr;
621 MTL::IndexType indexType = MTL::IndexTypeUInt16;
622 if (indexBuffer) {
623 ibBuffer = static_cast<MTL::Buffer*>(indexBuffer->nativeBuffer());
624 if (!ibBuffer) {
625 spdlog::warn("Draw skipped: index buffer has no native Metal buffer");
626 return;
627 }
628 switch (indexBuffer->format()) {
630 indexType = MTL::IndexTypeUInt16;
631 break;
633 indexType = MTL::IndexTypeUInt32;
634 break;
636 // Metal does not support uint8 indices directly.
637 spdlog::warn("Draw skipped: uint8 index buffers are not supported on Metal");
638 return;
639 default:
640 indexType = MTL::IndexTypeUInt16;
641 break;
642 }
643 }
644
645 // ── Common setup (always runs) ───────────────────────────────
646
648 return;
649 }
650 // glTF (and upstream/WebGL) use counter-clockwise front faces by default.
651 passEncoder->setFrontFacingWinding(MTL::WindingCounterClockwise);
652 passEncoder->setCullMode(toMetalCullMode(_cullMode));
653
654 MaterialUniforms materialUniforms;
655 const auto* boundMaterial = material();
656
657 // Uniform data: use customUniformData() if available (e.g., globe tiles
658 // with extended uniforms), otherwise fall back to standard updateUniforms().
659 const void* uniformData = &materialUniforms;
660 size_t uniformSize = sizeof(MaterialUniforms);
661
662 if (boundMaterial) {
663 size_t customSize = 0;
664 const void* customData = boundMaterial->customUniformData(customSize);
665 if (customData && customSize > 0) {
666 uniformData = customData;
667 uniformSize = customSize;
668 } else {
669 boundMaterial->updateUniforms(materialUniforms);
670 }
671
672 // Skip texture rebinding when same material is still bound.
673 if (_uniformBinder.isMaterialChanged(boundMaterial)) {
674 std::vector<TextureSlot> textureSlots;
675 boundMaterial->getTextureSlots(textureSlots);
676 _textureBinder.bindMaterialTextures(passEncoder, textureSlots);
677 }
678 } else {
679 _textureBinder.clearAllMaterialSlots(passEncoder);
680 }
681
682 if (quadRenderActive()) {
683 _textureBinder.bindQuadTextures(passEncoder, quadTextureBindings());
684 } else {
685 _textureBinder.bindSceneTextures(passEncoder,
686 _uniformBinder.envAtlasTexture(), _uniformBinder.shadowTexture(),
687 sceneDepthMap(), _uniformBinder.skyboxCubeMapTexture(),
689 _textureBinder.bindLocalShadowTextures(passEncoder,
690 _uniformBinder.localShadowTexture0(), _uniformBinder.localShadowTexture1());
691 _textureBinder.bindOmniShadowTextures(passEncoder,
692 _uniformBinder.omniShadowCube0(), _uniformBinder.omniShadowCube1());
693 }
694
695 // Pack screen inverse resolution for planar reflection screen-space UV.
696 _uniformBinder.setScreenResolution(vw(), vh());
697
698 // Pack blurred planar reflection parameters.
699 {
700 const auto& rbp = reflectionBlurParams();
701 _uniformBinder.setReflectionBlurParams(
702 rbp.intensity, rbp.blurAmount, rbp.fadeStrength, rbp.angleFade,
703 rbp.fadeColor.r, rbp.fadeColor.g, rbp.fadeColor.b);
704 _uniformBinder.setReflectionDepthParams(rbp.planeDistance, rbp.heightRange);
705 }
706
707 _uniformBinder.submitPerDrawUniforms(passEncoder, _uniformRing.get(),
708 boundMaterial, uniformData, uniformSize, hdrPass());
709
710 // Bind atmosphere uniforms at fragment slot 9 for skybox draws when atmosphere is enabled.
711 if (atmosphereEnabled() && boundMaterial && boundMaterial->isSkybox()) {
712 const auto& atmoUniforms = _uniformBinder.atmosphereUniforms();
713 passEncoder->setFragmentBytes(&atmoUniforms, sizeof(atmoUniforms), 9);
714 }
715
716 _textureBinder.bindSamplerCached(passEncoder, _defaultSampler);
717
718 // After the first draw in a pass has established all texture/sampler state,
719 // subsequent draws can rely on the cache for deduplication.
720 _textureBinder.markClean();
721
722 // per-draw-call depth stencil state switching.
723 // Transparent materials (e.g. shadow catcher) may need depthWrite disabled.
724 const bool needsNoWrite = _depthState && !_depthState->depthWrite();
725 if (needsNoWrite && _noWriteDepthStencilState) {
726 passEncoder->setDepthStencilState(_noWriteDepthStencilState);
727 }
728
729 // ── Dynamic batch palette binding (slot 6) ─────────────────────
730 // Update the palette ring buffer offset for this draw call.
731 // The buffer itself is bound once per render pass in startRenderPass();
732 // here we only update the offset (cheap, no validation).
733 // Uses Metal buffer (slot 6) for bone data.
734 if (_pendingPaletteOffset != SIZE_MAX) {
735 passEncoder->setVertexBufferOffset(_pendingPaletteOffset, 6);
736 _pendingPaletteOffset = SIZE_MAX;
737 }
738
739 // ── Draw dispatch (branch: indirect vs direct) ────────────────
740
741 const auto primitiveType = toMetalPrimitiveType(primitive.type);
742
743 if (indirectSlot >= 0 && _indirectDrawBuffer) {
744 // GPU-driven indirect draw: instance count comes from the GPU.
745 const auto indirectOffset = static_cast<NS::UInteger>(indirectSlot * INDIRECT_ENTRY_BYTE_SIZE);
746 if (indexBuffer) {
747 passEncoder->drawIndexedPrimitives(
748 primitiveType, indexType, ibBuffer, 0,
749 _indirectDrawBuffer, indirectOffset);
750 } else {
751 passEncoder->drawPrimitives(
752 primitiveType, _indirectDrawBuffer, indirectOffset);
753 }
754 _indirectDrawBuffer = nullptr; // Consumed
755 } else {
756 // Direct draw (standard path)
757 if (indexBuffer) {
758 const auto indexElementSize = (indexType == MTL::IndexTypeUInt32) ? 4 : 2;
759 const auto indexBufferOffset = static_cast<NS::UInteger>(primitive.base * indexElementSize);
760 passEncoder->drawIndexedPrimitives(
761 primitiveType,
762 static_cast<NS::UInteger>(primitive.count),
763 indexType,
764 ibBuffer,
765 indexBufferOffset,
766 static_cast<NS::UInteger>(numInstances),
767 static_cast<NS::Integer>(primitive.baseVertex),
768 0
769 );
770 } else {
771 passEncoder->drawPrimitives(
772 primitiveType,
773 static_cast<NS::UInteger>(primitive.base),
774 static_cast<NS::UInteger>(primitive.count),
775 static_cast<NS::UInteger>(numInstances)
776 );
777 }
778 }
779
780 // Restore default depth stencil state after draw
781 if (needsNoWrite && _defaultDepthStencilState) {
782 passEncoder->setDepthStencilState(_defaultDepthStencilState);
783 }
784
786
787 if (last) {
788 // Clear vertex buffer array
790 _pipelineState = nullptr;
791 }
792 }
793
795 {
796 if (!_commandQueue || !_metalLayer || _renderPassEncoder) {
797 spdlog::warn("Cannot start render pass: queue/layer invalid or encoder already active");
798 return;
799 }
800
801 const std::shared_ptr<RenderTarget> activeTarget =
802 (renderPass && renderPass->renderTarget()) ? renderPass->renderTarget() : backBuffer();
803 setRenderTarget(activeTarget);
804 const auto offscreenTarget = std::dynamic_pointer_cast<MetalRenderTarget>(activeTarget);
805 const bool isBackBufferPass = activeTarget == backBuffer();
806 if (!isBackBufferPass && !offscreenTarget) {
807 spdlog::error("Non-backbuffer render target is not a MetalRenderTarget");
808 return;
809 }
810 if (offscreenTarget) {
811 offscreenTarget->ensureAttachments();
812 }
813
814 _currentDrawable = nullptr;
815 if (isBackBufferPass) {
816 // Reuse the frame's cached drawable so that multiple back-buffer render
817 // passes within one frame write to the same drawable texture. Metal's
818 // nextDrawable() returns a *different* drawable each call (unlike WebGL's
819 // persistent back buffer), so acquiring one per pass would cause only the
820 // last pass's content to be visible.
821 if (_frameDrawable) {
822 _currentDrawable = _frameDrawable;
823 spdlog::trace("Reusing cached CAMetalDrawable for back-buffer pass");
824 } else {
825 _currentDrawable = _metalLayer->nextDrawable();
826 if (!_currentDrawable) {
827 spdlog::warn("Failed to acquire CAMetalDrawable");
828 return;
829 }
830 _frameDrawable = _currentDrawable;
831 spdlog::trace("Acquired new CAMetalDrawable for frame");
832 }
833 }
834
835 _commandBuffer = _commandQueue->commandBuffer();
836 if (!_commandBuffer) {
837 spdlog::error("Failed to create Metal command buffer");
838 _currentDrawable = nullptr;
839 return;
840 }
841
842 auto* passDesc = MTL::RenderPassDescriptor::alloc()->init();
843
844 const auto& colorOpsArray = renderPass ? renderPass->colorArrayOps() : std::vector<std::shared_ptr<ColorAttachmentOps>>{};
845 const auto depthOps = renderPass ? renderPass->depthStencilOps() : nullptr;
846 const bool canResolve = activeTarget && activeTarget->samples() > 1 && activeTarget->autoResolve();
847
848 auto resolveColorStoreAction = [](bool store, bool resolve) {
849 if (resolve) {
850 return store ? MTL::StoreActionStoreAndMultisampleResolve : MTL::StoreActionMultisampleResolve;
851 }
852 return store ? MTL::StoreActionStore : MTL::StoreActionDontCare;
853 };
854
855 if (isBackBufferPass) {
856 auto* colorAttachment = passDesc->colorAttachments()->object(0);
857 colorAttachment->setTexture(_currentDrawable->texture());
858
859 const auto colorOps = renderPass ? renderPass->colorOps() : nullptr;
860 if (colorOps && colorOps->clear) {
861 const auto c = colorOps->clearValue;
862 colorAttachment->setLoadAction(MTL::LoadActionClear);
863 colorAttachment->setClearColor(MTL::ClearColor::Make(c.r, c.g, c.b, c.a));
864 } else {
865 colorAttachment->setLoadAction(MTL::LoadActionLoad);
866 }
867 colorAttachment->setStoreAction(colorOps && colorOps->store ? MTL::StoreActionStore : MTL::StoreActionDontCare);
868
869 const auto drawableWidth = static_cast<int>(_currentDrawable->texture()->width());
870 const auto drawableHeight = static_cast<int>(_currentDrawable->texture()->height());
871 if (!_backBufferDepthTexture ||
872 _backBufferDepthWidth != drawableWidth ||
873 _backBufferDepthHeight != drawableHeight) {
874 if (_backBufferDepthTexture) {
875 _backBufferDepthTexture->release();
876 _backBufferDepthTexture = nullptr;
877 }
878 _backBufferDepthTexture = createDepthTexture(_device, drawableWidth, drawableHeight);
879 _backBufferDepthWidth = drawableWidth;
880 _backBufferDepthHeight = drawableHeight;
881 }
882
883 if (_backBufferDepthTexture) {
884 auto* depthAttachment = passDesc->depthAttachment();
885 depthAttachment->setTexture(_backBufferDepthTexture);
886 if (depthOps && depthOps->clearDepth) {
887 depthAttachment->setLoadAction(MTL::LoadActionClear);
888 depthAttachment->setClearDepth(depthOps->clearDepthValue);
889 } else {
890 depthAttachment->setLoadAction(MTL::LoadActionLoad);
891 }
892 depthAttachment->setStoreAction(depthOps && depthOps->storeDepth
893 ? MTL::StoreActionStore
894 : MTL::StoreActionDontCare);
895 }
896 } else {
897 const auto& colorAttachments = offscreenTarget->colorAttachments();
898 for (size_t i = 0; i < colorAttachments.size(); ++i) {
899 const auto& attachment = colorAttachments[i];
900 if (!attachment || !attachment->texture) {
901 continue;
902 }
903
904 auto* colorAttachment = passDesc->colorAttachments()->object(static_cast<NS::UInteger>(i));
905 const bool multisampled = attachment->multisampledBuffer != nullptr;
906 colorAttachment->setTexture(multisampled ? attachment->multisampledBuffer : attachment->texture);
907
908 const auto ops = i < colorOpsArray.size() ? colorOpsArray[i] : nullptr;
909 if (ops && ops->clear) {
910 const auto c = ops->clearValue;
911 colorAttachment->setLoadAction(MTL::LoadActionClear);
912 colorAttachment->setClearColor(MTL::ClearColor::Make(c.r, c.g, c.b, c.a));
913 } else {
914 colorAttachment->setLoadAction(MTL::LoadActionLoad);
915 }
916
917 const bool resolve = multisampled && canResolve && ops && ops->resolve;
918 if (resolve) {
919 colorAttachment->setResolveTexture(attachment->texture);
920 }
921 colorAttachment->setStoreAction(resolveColorStoreAction(ops ? ops->store : true, resolve));
922 }
923
924 const auto& depthAttachmentData = offscreenTarget->depthAttachment();
925 if (depthAttachmentData && depthAttachmentData->depthTexture) {
926 auto* depthAttachment = passDesc->depthAttachment();
927 const bool depthMsaa = depthAttachmentData->multisampledDepthBuffer != nullptr;
928 MTL::Texture* depthTex = depthMsaa ? depthAttachmentData->multisampledDepthBuffer : depthAttachmentData->depthTexture;
929 depthAttachment->setTexture(depthTex);
930
931 // Cubemap face rendering: when the depth texture is a cubemap, target a
932 // specific face via the slice parameter. This enables rendering to individual
933 // faces of a point-light shadow cubemap.
934 if (depthTex->textureType() == MTL::TextureTypeCube && activeTarget->face() >= 0) {
935 depthAttachment->setSlice(static_cast<NS::UInteger>(activeTarget->face()));
936 }
937
938 if (depthOps && depthOps->clearDepth) {
939 depthAttachment->setLoadAction(MTL::LoadActionClear);
940 depthAttachment->setClearDepth(depthOps->clearDepthValue);
941 } else {
942 depthAttachment->setLoadAction(MTL::LoadActionLoad);
943 }
944
945 const bool resolveDepth = depthMsaa && canResolve && depthOps && depthOps->resolveDepth;
946 if (resolveDepth) {
947 depthAttachment->setResolveTexture(depthAttachmentData->depthTexture);
948 }
949 depthAttachment->setStoreAction(resolveColorStoreAction(depthOps ? depthOps->storeDepth : true, resolveDepth));
950
951 if (depthAttachmentData->hasStencil) {
952 auto* stencilAttachment = passDesc->stencilAttachment();
953 stencilAttachment->setTexture(depthMsaa ? depthAttachmentData->multisampledDepthBuffer : depthAttachmentData->depthTexture);
954 if (depthOps && depthOps->clearStencil) {
955 stencilAttachment->setLoadAction(MTL::LoadActionClear);
956 stencilAttachment->setClearStencil(depthOps->clearStencilValue);
957 } else {
958 stencilAttachment->setLoadAction(MTL::LoadActionLoad);
959 }
960 stencilAttachment->setStoreAction(depthOps && depthOps->storeStencil ? MTL::StoreActionStore : MTL::StoreActionDontCare);
961 }
962 }
963 }
964
965 _renderPassEncoder = _commandBuffer->renderCommandEncoder(passDesc);
966 if (!_renderPassEncoder) {
967 spdlog::error("Failed to create Metal render command encoder");
968 _commandBuffer = nullptr;
969 _currentDrawable = nullptr;
970 _insideRenderPass = false;
971 } else {
972 if (_defaultDepthStencilState) {
973 _renderPassEncoder->setDepthStencilState(_defaultDepthStencilState);
974 }
975 const int targetWidth = activeTarget ? activeTarget->width() : size().first;
976 const int targetHeight = activeTarget ? activeTarget->height() : size().second;
977 if (targetWidth > 0 && targetHeight > 0) {
978 setViewport(0.0f, 0.0f, static_cast<float>(targetWidth), static_cast<float>(targetHeight));
979 setScissor(0, 0, targetWidth, targetHeight);
980 }
981
982 // Bind ring buffers once per render pass. Per-draw calls will only
983 // update the offset (setVertexBufferOffset), which is much cheaper
984 // than rebinding the buffer + validating it each time.
985 _renderPassEncoder->setVertexBuffer(_transformRing->buffer(), 0, 2);
986 _renderPassEncoder->setFragmentBuffer(_uniformRing->buffer(), 0, 3);
987 _renderPassEncoder->setVertexBuffer(_uniformRing->buffer(), 0, 3);
988 _renderPassEncoder->setFragmentBuffer(_uniformRing->buffer(), 0, 4);
989 _renderPassEncoder->setVertexBuffer(_paletteRing->buffer(), 0, 6);
990
991 // Bind clustered lighting buffers at fragment slots 7 (lights) and 8 (cells).
992 if (_clusterBuffersSet && _clusterLightBuffer && _clusterCellBuffer) {
993 _renderPassEncoder->setFragmentBuffer(_clusterLightBuffer, 0, 7);
994 _renderPassEncoder->setFragmentBuffer(_clusterCellBuffer, 0, 8);
995 }
996
997 // Reset per-pass deduplication state for uniforms and textures.
998 _uniformBinder.resetPassState();
999 _textureBinder.resetPassState();
1000
1001 _insideRenderPass = true;
1002 }
1003 passDesc->release();
1004 }
1005
1007 {
1008 if (_renderPassEncoder) {
1009 _renderPassEncoder->endEncoding();
1010 _renderPassEncoder = nullptr;
1011 }
1012 _insideRenderPass = false;
1013
1014 const auto activeTarget = renderTarget();
1015 const auto offscreenTarget = std::dynamic_pointer_cast<MetalRenderTarget>(activeTarget);
1016 if (_commandBuffer && offscreenTarget && renderPass) {
1017 bool needsMipmaps = false;
1018 const auto& colorOpsArray = renderPass->colorArrayOps();
1019 for (size_t i = 0; i < colorOpsArray.size(); ++i) {
1020 const auto ops = colorOpsArray[i];
1021 const auto* colorBuffer = activeTarget && i < static_cast<size_t>(activeTarget->colorBufferCount())
1022 ? activeTarget->getColorBuffer(i) : nullptr;
1023 if (ops && ops->genMipmaps && colorBuffer && colorBuffer->mipmaps()) {
1024 needsMipmaps = true;
1025 break;
1026 }
1027 }
1028
1029 if (needsMipmaps) {
1030 auto* blitEncoder = _commandBuffer->blitCommandEncoder();
1031 if (blitEncoder) {
1032 for (size_t i = 0; i < colorOpsArray.size(); ++i) {
1033 const auto ops = colorOpsArray[i];
1034 const auto* colorBuffer = activeTarget && i < static_cast<size_t>(activeTarget->colorBufferCount())
1035 ? activeTarget->getColorBuffer(i) : nullptr;
1036 if (!(ops && ops->genMipmaps && colorBuffer && colorBuffer->mipmaps())) {
1037 continue;
1038 }
1039 auto* hwTexture = dynamic_cast<gpu::MetalTexture*>(colorBuffer->impl());
1040 if (hwTexture && hwTexture->raw()) {
1041 blitEncoder->generateMipmaps(hwTexture->raw());
1042 }
1043 }
1044 blitEncoder->endEncoding();
1045 }
1046 }
1047 }
1048
1049 if (_commandBuffer) {
1050 // DEVIATION: in WebGL/WebGPU, the back buffer persists across
1051 // render passes within a frame and is presented once at frame end (swap).
1052 // In Metal, each back-buffer render pass gets a separate command buffer.
1053 // We defer presentDrawable() to onFrameEnd() so that only the final
1054 // back-buffer command buffer presents the drawable. Calling it here would
1055 // cause the compose pass to present before the after-pass finishes.
1056 spdlog::trace("Committing Metal command buffer (present deferred to frame end)");
1057 _commandBuffer->commit();
1058 } else {
1059 spdlog::warn("Render pass ended without a valid command buffer");
1060 }
1061
1062 _commandBuffer = nullptr;
1063 _currentDrawable = nullptr;
1064 }
1065
1066 void MetalGraphicsDevice::setResolution(int width, int height)
1067 {
1068 if (_metalLayer) {
1069 _metalLayer->setDrawableSize(CGSize{static_cast<CGFloat>(width), static_cast<CGFloat>(height)});
1070 }
1071 }
1072
1073 void MetalGraphicsDevice::setViewport(float x, float y, float w, float h)
1074 {
1075 GraphicsDevice::setViewport(x, y, w, h);
1076 if (_renderPassEncoder && w > 0.0f && h > 0.0f) {
1077 MTL::Viewport viewport;
1078 viewport.originX = static_cast<double>(x);
1079 viewport.originY = static_cast<double>(y);
1080 viewport.width = static_cast<double>(w);
1081 viewport.height = static_cast<double>(h);
1082 viewport.znear = 0.0;
1083 viewport.zfar = 1.0;
1084 _renderPassEncoder->setViewport(viewport);
1085 }
1086 }
1087
1088 void MetalGraphicsDevice::setScissor(int x, int y, int w, int h)
1089 {
1090 GraphicsDevice::setScissor(x, y, w, h);
1091 if (_renderPassEncoder && w > 0 && h > 0) {
1092 MTL::ScissorRect scissor;
1093 scissor.x = static_cast<NS::UInteger>(x);
1094 scissor.y = static_cast<NS::UInteger>(y);
1095 scissor.width = static_cast<NS::UInteger>(w);
1096 scissor.height = static_cast<NS::UInteger>(h);
1097 _renderPassEncoder->setScissorRect(scissor);
1098 }
1099 }
1100}
std::shared_ptr< RenderTarget > renderTarget() const
std::shared_ptr< StencilParameters > _stencilBack
void setRenderTarget(const std::shared_ptr< RenderTarget > &target)
std::shared_ptr< RenderTarget > backBuffer() const
const ReflectionBlurParams & reflectionBlurParams() const
void setBackBuffer(const std::shared_ptr< RenderTarget > &target)
const std::array< Texture *, 8 > & quadTextureBindings() const
std::shared_ptr< StencilParameters > _stencilFront
std::shared_ptr< RenderTarget > _renderTarget
virtual void setScissor(int x, int y, int w, int h)
std::shared_ptr< Shader > _shader
std::vector< std::shared_ptr< VertexBuffer > > _vertexBuffers
virtual void setViewport(float x, float y, float w, float h)
std::shared_ptr< BlendState > _blendState
const Material * material() const
std::shared_ptr< DepthState > _depthState
void startRenderPass(RenderPass *renderPass) override
void setClusterGridParams(const float *boundsMin, const float *boundsRange, const float *cellsCountByBoundsSize, int cellsX, int cellsY, int cellsZ, int maxLightsPerCell, int numClusteredLights) override
void executeDofBlurPass(const DofBlurPassParams &params) override
void endRenderPass(RenderPass *renderPass) override
void setIndirectDrawBuffer(void *nativeBuffer) override
void computeDispatch(const std::vector< Compute * > &computes, const std::string &label="") override
std::shared_ptr< RenderTarget > createRenderTarget(const RenderTargetOptions &options) override
void executeDepthAwareBlurPass(const DepthAwareBlurPassParams &params, bool horizontal) override
void setDynamicBatchPalette(const void *data, size_t size) override
MTL::ComputeCommandEncoder * _computePassEncoder
void draw(const Primitive &primitive, const std::shared_ptr< IndexBuffer > &indexBuffer=nullptr, int numInstances=1, int indirectSlot=-1, bool first=true, bool last=true) override
void setTransformUniforms(const Matrix4 &viewProjection, const Matrix4 &model) override
std::unique_ptr< gpu::HardwareTexture > createGPUTexture(Texture *texture) override
void executeCoCPass(const CoCPassParams &params) override
void setResolution(int width, int height) override
void setClusterBuffers(const void *lightData, size_t lightSize, const void *cellData, size_t cellSize) override
void executeSsaoPass(const SsaoPassParams &params) override
std::shared_ptr< Shader > createShader(const ShaderDefinition &definition, const std::string &sourceCode="") override
std::shared_ptr< IndexBuffer > createIndexBuffer(IndexFormat format, int numIndices, const std::vector< uint8_t > &data={}) override
MetalGraphicsDevice(const GraphicsDeviceOptions &options)
std::shared_ptr< VertexBuffer > createVertexBufferFromMTLBuffer(const std::shared_ptr< VertexFormat > &format, int numVertices, MTL::Buffer *externalBuffer)
std::shared_ptr< VertexBuffer > createVertexBufferFromNativeBuffer(const std::shared_ptr< VertexFormat > &format, int numVertices, void *nativeBuffer) override
void executeTAAPass(Texture *sourceTexture, Texture *historyTexture, Texture *depthTexture, const Matrix4 &viewProjectionPrevious, const Matrix4 &viewProjectionInverse, const std::array< float, 4 > &jitters, const std::array< float, 4 > &cameraParams, bool highQuality, bool historyValid) override
MTL::RenderCommandEncoder * _renderPassEncoder
void setViewport(float x, float y, float w, float h) override
void setDepthBias(float depthBias, float slopeScale, float clamp) override
std::pair< int, int > size() const override
void executeComposePass(const ComposePassParams &params) override
void setAtmosphereUniforms(const void *data, size_t size) override
std::shared_ptr< VertexBuffer > createVertexBuffer(const std::shared_ptr< VertexFormat > &format, int numVertices, const VertexBufferOptions &options=VertexBufferOptions{}) override
void setScissor(int x, int y, int w, int h) override
void setEnvironmentUniforms(Texture *envAtlas, float skyboxIntensity, float skyboxMip, const Vector3 &skyDomeCenter=Vector3(0, 0, 0), bool isDome=false, Texture *skyboxCubeMap=nullptr) override
void setLightingUniforms(const Color &ambientColor, const std::vector< GpuLightData > &lights, const Vector3 &cameraPosition, bool enableNormalMaps, float exposure, const FogParams &fogParams=FogParams{}, const ShadowParams &shadowParams=ShadowParams{}, int toneMapping=0) override
const std::vector< std::shared_ptr< ColorAttachmentOps > > & colorArrayOps() const
Definition renderPass.h:100
std::shared_ptr< DepthStencilAttachmentOps > depthStencilOps() const
Definition renderPass.h:102
std::shared_ptr< RenderTarget > renderTarget() const
Definition renderPass.h:98
std::shared_ptr< ColorAttachmentOps > colorOps() const
Backbuffer sentinel target — preserves JS renderTarget/backBuffer identity semantics.
Definition metalUtils.h:90
MTL::Texture * createDepthTexture(MTL::Device *device, const int width, const int height)
Create a Depth32Float texture for the back buffer depth attachment.
Definition metalUtils.h:67
MTL::PrimitiveType toMetalPrimitiveType(const PrimitiveType primitiveType)
Map engine PrimitiveType to Metal PrimitiveType.
Definition metalUtils.h:32
MTL::CullMode toMetalCullMode(const CullMode cullMode)
Map engine CullMode to Metal CullMode.
Definition metalUtils.h:52
MTL::Texture * createDepthTexture(MTL::Device *device, const int width, const int height)
Create a Depth32Float texture for the back buffer depth attachment.
Definition metalUtils.h:67
constexpr int INDIRECT_ENTRY_BYTE_SIZE
MTL::PrimitiveType toMetalPrimitiveType(const PrimitiveType primitiveType)
Map engine PrimitiveType to Metal PrimitiveType.
Definition metalUtils.h:32
MTL::CullMode toMetalCullMode(const CullMode cullMode)
Map engine CullMode to Metal CullMode.
Definition metalUtils.h:52
RGBA color with floating-point components in [0, 1].
Definition color.h:18
4x4 column-major transformation matrix with SIMD acceleration.
Definition matrix4.h:31
Describes how vertex and index data should be interpreted for a draw call.
Definition mesh.h:33
PrimitiveType type
Definition mesh.h:34
3D vector for positions, directions, and normals with multi-backend SIMD acceleration.
Definition vector3.h:29