VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
renderPassCameraFrame.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4//
6
7#include <algorithm>
8#include <cassert>
9
10#include "renderPassBloom.h"
11#include "renderPassColorGrab.h"
12#include "renderPassCompose.h"
13#include "renderPassDof.h"
15#include "renderPassPrepass.h"
16#include "renderPassSsao.h"
17#include "renderPassTAA.h"
19#include "scene/constants.h"
22
23namespace visutwin::canvas
24{
25 namespace
26 {
27 const CameraFrameOptions defaultOptions{};
28
29 bool formatsEqual(const std::vector<PixelFormat>& a, const std::vector<PixelFormat>& b)
30 {
31 return a == b;
32 }
33 }
34
35 RenderPassCameraFrame::RenderPassCameraFrame(const std::shared_ptr<GraphicsDevice>& device,
36 LayerComposition* layerComposition, Scene* scene, Renderer* renderer, const std::vector<RenderAction*>& sourceActions,
37 CameraComponent* cameraComponent, const std::shared_ptr<RenderTarget>& targetRenderTarget)
38 : RenderPass(device), _layerComposition(layerComposition), _scene(scene), _renderer(renderer),
39 _cameraComponent(cameraComponent), _targetRenderTarget(targetRenderTarget), _sourceActions(sourceActions)
40 {
41 // Do NOT call init(nullptr) here. CameraFrame is a container for its
42 // before-passes (prepass, scene, TAA, compose, after). If init(nullptr)
43 // is called, the CameraFrame's own render() opens a back-buffer render
44 // pass (with an empty execute()) that may clear or overwrite the compose
45 // output. Leaving _renderTargetInitialized = false causes render() to
46 // skip the start/execute/end sequence, which is the correct behavior.
47
48 CameraFrameOptions options = defaultOptions;
49 if (_cameraComponent) {
50 const auto& taa = _cameraComponent->taa();
51 const auto& dof = _cameraComponent->dof();
52 const auto& ssao = _cameraComponent->ssao();
53 const auto& rendering = _cameraComponent->rendering();
54 options.taaEnabled = taa.enabled;
55 options.dofEnabled = dof.enabled;
56 options.dofNearBlur = dof.nearBlur;
57 options.dofHighQuality = dof.highQuality;
58 if (ssao.enabled) {
60 } else {
61 options.ssaoType = SSAOTYPE_NONE;
62 }
63 options.ssaoBlurEnabled = ssao.blurEnabled;
64 options.bloomEnabled = rendering.bloomIntensity > 0.0f;
65 options.bloomIntensity = rendering.bloomIntensity;
66 options.sharpness = rendering.sharpness;
67 options.vignetteEnabled = rendering.vignetteEnabled;
68 options.vignetteInner = rendering.vignetteInner;
69 options.vignetteOuter = rendering.vignetteOuter;
70 options.vignetteCurvature = rendering.vignetteCurvature;
71 options.vignetteIntensity = rendering.vignetteIntensity;
72 }
73
74 _options = sanitizeOptions(options);
75 setupRenderPasses(_options);
76 }
77
79 {
80 // Clear device-side raw texture pointers that reference textures owned by this
81 // CameraFrame before resetting the textures themselves. If these are not cleared,
82 // the GraphicsDevice holds dangling pointers that are dereferenced during the next
83 // forward render pass (bindSceneTextures slot 7 for depth, slot 18 for SSAO), which
84 // causes a SIGSEGV when the CameraFrame is destroyed mid-session (e.g. SSAO toggle).
85 if (const auto gd = device()) {
86 // Clear sceneDepthMap only when it still points at our depth texture;
87 // another CameraFrame (e.g. a second camera) must not be cleared.
88 if (_sceneDepthTexture && gd->sceneDepthMap() == _sceneDepthTexture.get()) {
89 gd->setSceneDepthMap(nullptr);
90 }
91 // Clear ssaoForwardTexture unconditionally — it is set by this CameraFrame's
92 // frameUpdate() and must not outlive the SSAO texture it references.
93 if (_ssaoPass && gd->ssaoForwardTexture() == _ssaoPass->ssaoTexture()) {
94 gd->setSsaoForwardTexture(nullptr);
95 }
96 }
97 reset();
98 }
99
101 {
102 _sceneTexture.reset();
103 _sceneDepthTexture.reset();
104 _sceneTextureHalf.reset();
105 _sceneRenderTarget.reset();
106 _sceneHalfRenderTarget.reset();
107
109
110 _prePass.reset();
111 _scenePass.reset();
112 _colorGrabPass.reset();
113 _scenePassTransparent.reset();
114 _ssaoPass.reset();
115 _taaPass.reset();
116 _scenePassHalf.reset();
117 _bloomPass.reset();
118 _dofPass.reset();
119 _composePass.reset();
120 _afterPass.reset();
121
122 _sceneTextureResolved = nullptr;
123 _ownedActions.clear();
124 }
125
127 {
128 CameraFrameOptions sanitized = options;
129 if (sanitized.taaEnabled || sanitized.ssaoType != SSAOTYPE_NONE || sanitized.dofEnabled) {
130 sanitized.prepassEnabled = true;
131 }
132 return sanitized;
133 }
134
136 {
137 const auto& current = _options;
138 return options.ssaoType != current.ssaoType ||
139 options.ssaoBlurEnabled != current.ssaoBlurEnabled ||
140 options.taaEnabled != current.taaEnabled ||
141 options.samples != current.samples ||
142 options.stencil != current.stencil ||
143 options.bloomEnabled != current.bloomEnabled ||
144 options.prepassEnabled != current.prepassEnabled ||
145 options.sceneColorMap != current.sceneColorMap ||
146 options.dofEnabled != current.dofEnabled ||
147 options.dofNearBlur != current.dofNearBlur ||
148 options.dofHighQuality != current.dofHighQuality ||
149 !formatsEqual(options.formats, current.formats);
150 }
151
153 {
154 auto sanitized = sanitizeOptions(options);
155 if (needsReset(sanitized) || _needsReset) {
156 _needsReset = false;
157 reset();
158 }
159
160 _options = sanitized;
161 if (!_sceneTexture) {
162 setupRenderPasses(_options);
163 }
164 }
165
166 void RenderPassCameraFrame::updateSourceActions(const std::vector<RenderAction*>& sourceActions,
167 LayerComposition* layerComposition, Scene* scene, Renderer* renderer,
168 const std::shared_ptr<RenderTarget>& targetRenderTarget)
169 {
170 _sourceActions = sourceActions;
171 _layerComposition = layerComposition;
172 _scene = scene;
173 _renderer = renderer;
174 _targetRenderTarget = targetRenderTarget;
175
176 // Clear sub-passes but preserve GPU textures and post-processing passes.
177 // Scene passes that reference render actions must be recreated each frame.
178 // Post-processing passes (_ssaoPass, _bloomPass, _scenePassHalf, _dofPass)
179 // are persisted to avoid per-frame Metal texture allocation/deallocation
180 // which causes GPU memory growth.
182 _ownedActions.clear();
183
184 _prePass.reset();
185 _scenePass.reset();
186 _colorGrabPass.reset();
187 _scenePassTransparent.reset();
188 _afterPass.reset();
189 _composePass.reset();
190 _sceneTextureResolved = nullptr;
191 // Note: _ssaoPass, _taaPass, _scenePassHalf, _bloomPass, _dofPass are NOT
192 // reset here — they persist across frames to avoid per-frame GPU texture
193 // allocation (each creates Metal textures + render targets in constructors).
194
195 // Also apply any option changes from CameraComponent (e.g. TAA/SSAO toggled).
196 if (_cameraComponent) {
197 CameraFrameOptions options;
198 const auto& taa = _cameraComponent->taa();
199 const auto& dof = _cameraComponent->dof();
200 const auto& ssao = _cameraComponent->ssao();
201 const auto& rendering = _cameraComponent->rendering();
202 options.taaEnabled = taa.enabled;
203 options.dofEnabled = dof.enabled;
204 options.dofNearBlur = dof.nearBlur;
205 options.dofHighQuality = dof.highQuality;
206 if (ssao.enabled) {
208 } else {
209 options.ssaoType = SSAOTYPE_NONE;
210 }
211 options.ssaoBlurEnabled = ssao.blurEnabled;
212 options.bloomEnabled = rendering.bloomIntensity > 0.0f;
213 options.bloomIntensity = rendering.bloomIntensity;
214 options.sharpness = rendering.sharpness;
215 options.vignetteEnabled = rendering.vignetteEnabled;
216 options.vignetteInner = rendering.vignetteInner;
217 options.vignetteOuter = rendering.vignetteOuter;
218 options.vignetteCurvature = rendering.vignetteCurvature;
219 options.vignetteIntensity = rendering.vignetteIntensity;
220 auto sanitized = sanitizeOptions(options);
221
222 if (needsReset(sanitized)) {
223 // Options changed (e.g. TAA toggled on/off) — full reset including textures.
224 reset();
225 _options = sanitized;
226 setupRenderPasses(_options);
227 return;
228 }
229 _options = sanitized;
230 }
231
232 // Rebuild sub-passes using existing textures.
233 createPasses(_options);
234
235 for (const auto& pass : collectPasses()) {
236 if (pass) {
237 addBeforePass(pass);
238 }
239 }
240 }
241
243 {
244 _renderTargetScale = value;
245 if (_sceneOptions) {
246 _sceneOptions->scaleX = value;
247 _sceneOptions->scaleY = value;
248 }
249 }
250
251 std::shared_ptr<RenderTarget> RenderPassCameraFrame::createRenderTarget(const std::string& name, const bool depth,
252 const bool stencil, const int samples) const
253 {
254 const auto gd = device();
255 if (!gd) {
256 return nullptr;
257 }
258
259 TextureOptions textureOptions;
260 textureOptions.name = name;
261 textureOptions.width = 4;
262 textureOptions.height = 4;
263 textureOptions.format = _hdrFormat;
264 textureOptions.mipmaps = false;
265 textureOptions.minFilter = FilterMode::FILTER_LINEAR;
266 textureOptions.magFilter = FilterMode::FILTER_LINEAR;
267 auto colorTexture = std::make_shared<Texture>(gd.get(), textureOptions);
268 colorTexture->setAddressU(AddressMode::ADDRESS_CLAMP_TO_EDGE);
269 colorTexture->setAddressV(AddressMode::ADDRESS_CLAMP_TO_EDGE);
270
271 RenderTargetOptions rtOptions;
272 rtOptions.graphicsDevice = gd.get();
273 rtOptions.colorBuffer = colorTexture.get();
274 rtOptions.depth = depth;
275 rtOptions.stencil = stencil;
276 rtOptions.samples = samples;
277 rtOptions.name = name;
278 return gd->createRenderTarget(rtOptions);
279 }
280
281 std::shared_ptr<RenderAction> RenderPassCameraFrame::cloneActionWithTarget(const RenderAction* source,
282 const std::shared_ptr<RenderTarget>& renderTarget)
283 {
284 if (!source) {
285 return nullptr;
286 }
287 auto cloned = std::make_shared<RenderAction>(*source);
288 cloned->renderTarget = renderTarget;
289 return cloned;
290 }
291
292 int RenderPassCameraFrame::findActionIndex(const int targetLayerId, const bool targetTransparent, const int fromIndex) const
293 {
294 for (int i = std::max(fromIndex, 0); i < static_cast<int>(_sourceActions.size()); ++i) {
295 const auto* action = _sourceActions[i];
296 if (!action || !action->layer || action->layer->id() == LAYERID_DEPTH) {
297 continue;
298 }
299 if (action->layer->id() == targetLayerId && action->transparent == targetTransparent) {
300 return i;
301 }
302 }
303 return -1;
304 }
305
306 int RenderPassCameraFrame::appendActionsToPass(const std::shared_ptr<RenderPassForward>& pass, const int fromIndex,
307 const int toIndex, const std::shared_ptr<RenderTarget>& target, const bool firstLayerClears)
308 {
309 if (!pass || fromIndex < 0 || toIndex < fromIndex || _sourceActions.empty()) {
310 return fromIndex - 1;
311 }
312
313 bool isFirst = true;
314 int lastAddedIndex = fromIndex - 1;
315 const int clampedTo = std::min(toIndex, static_cast<int>(_sourceActions.size()) - 1);
316 for (int i = fromIndex; i <= clampedTo; ++i) {
317 auto* action = _sourceActions[i];
318 if (!action || !action->layer || action->layer->id() == LAYERID_DEPTH) {
319 continue;
320 }
321 auto cloned = cloneActionWithTarget(action, target);
322 if (!cloned) {
323 continue;
324 }
325
326 // when firstLayerClears is false, the first action in
327 // this pass should only use layer-level clear flags (not camera flags).
328 // This prevents the after-pass from clearing the compose output on the
329 // back buffer.
330 if (isFirst && !firstLayerClears) {
331 cloned->setupClears(nullptr, action->layer);
332 isFirst = false;
333 }
334
335 pass->addRenderAction(cloned.get());
336 _ownedActions.push_back(cloned);
337 lastAddedIndex = i;
338 }
339 return lastAddedIndex;
340 }
341
342 void RenderPassCameraFrame::setupRenderPasses(const CameraFrameOptions& options)
343 {
344 if (!_cameraComponent || !_renderer || !_scene) {
345 return;
346 }
347
348 const auto gd = device();
349 if (!gd) {
350 return;
351 }
352
354 _bloomEnabled = options.bloomEnabled && _hdrFormat != PixelFormat::PIXELFORMAT_RGBA8;
355 _sceneHalfEnabled = _bloomEnabled || options.dofEnabled;
356
357 // Create the scene color texture explicitly so it survives beyond the helper.
358 // The createRenderTarget() helper creates a local Texture that is destroyed when
359 // the method returns (RenderTarget stores only a raw pointer). We must keep
360 // the color texture alive ourselves.
361 // Start at the device size (not 4×4) so the first frame isn't too small.
362 const auto [initW, initH] = gd->size();
363 {
364 TextureOptions colorOpts;
365 colorOpts.name = "SceneColor";
366 colorOpts.width = std::max(initW, 1);
367 colorOpts.height = std::max(initH, 1);
368 colorOpts.format = _hdrFormat;
369 colorOpts.mipmaps = false;
370 colorOpts.minFilter = FilterMode::FILTER_LINEAR;
371 colorOpts.magFilter = FilterMode::FILTER_LINEAR;
372 _sceneTexture = std::make_shared<Texture>(gd.get(), colorOpts);
373 _sceneTexture->setAddressU(AddressMode::ADDRESS_CLAMP_TO_EDGE);
374 _sceneTexture->setAddressV(AddressMode::ADDRESS_CLAMP_TO_EDGE);
375 }
376
377 TextureOptions sceneDepthOptions;
378 sceneDepthOptions.name = "CameraFrameSceneDepth";
379 sceneDepthOptions.width = std::max(_sceneTexture ? static_cast<int>(_sceneTexture->width()) : 1, 1);
380 sceneDepthOptions.height = std::max(_sceneTexture ? static_cast<int>(_sceneTexture->height()) : 1, 1);
381 sceneDepthOptions.format = PixelFormat::PIXELFORMAT_DEPTH;
382 sceneDepthOptions.mipmaps = false;
383 sceneDepthOptions.minFilter = FilterMode::FILTER_NEAREST;
384 sceneDepthOptions.magFilter = FilterMode::FILTER_NEAREST;
385 _sceneDepthTexture = std::make_shared<Texture>(gd.get(), sceneDepthOptions);
386 _sceneDepthTexture->setAddressU(AddressMode::ADDRESS_CLAMP_TO_EDGE);
387 _sceneDepthTexture->setAddressV(AddressMode::ADDRESS_CLAMP_TO_EDGE);
388
389 RenderTargetOptions sceneTargetOptions;
390 sceneTargetOptions.graphicsDevice = gd.get();
391 sceneTargetOptions.colorBuffer = _sceneTexture.get();
392 sceneTargetOptions.depthBuffer = _sceneDepthTexture.get();
393 sceneTargetOptions.stencil = options.stencil;
394 sceneTargetOptions.samples = options.samples;
395 sceneTargetOptions.name = "CameraFrameSceneTarget";
396 _sceneRenderTarget = gd->createRenderTarget(sceneTargetOptions);
397
398 if (_sceneHalfEnabled) {
399 // Create half-resolution color texture explicitly (same lifetime fix as _sceneTexture).
400 TextureOptions halfOpts;
401 halfOpts.name = "SceneColorHalf";
402 halfOpts.width = 4;
403 halfOpts.height = 4;
404 halfOpts.format = _hdrFormat;
405 halfOpts.mipmaps = false;
406 halfOpts.minFilter = FilterMode::FILTER_LINEAR;
407 halfOpts.magFilter = FilterMode::FILTER_LINEAR;
408 _sceneTextureHalf = std::make_shared<Texture>(gd.get(), halfOpts);
409 _sceneTextureHalf->setAddressU(AddressMode::ADDRESS_CLAMP_TO_EDGE);
410 _sceneTextureHalf->setAddressV(AddressMode::ADDRESS_CLAMP_TO_EDGE);
411
412 RenderTargetOptions halfTargetOptions;
413 halfTargetOptions.graphicsDevice = gd.get();
414 halfTargetOptions.colorBuffer = _sceneTextureHalf.get();
415 halfTargetOptions.depth = false;
416 halfTargetOptions.stencil = false;
417 halfTargetOptions.samples = 1;
418 halfTargetOptions.name = "SceneColorHalf";
419 _sceneHalfRenderTarget = gd->createRenderTarget(halfTargetOptions);
420 }
421
422 _sceneOptions = std::make_shared<RenderPassOptions>();
423 if (_targetRenderTarget && _targetRenderTarget->colorBuffer()) {
424 _sceneOptions->resizeSource = std::shared_ptr<Texture>(_targetRenderTarget->colorBuffer(), [](Texture*) {});
425 }
426 _sceneOptions->scaleX = _renderTargetScale;
427 _sceneOptions->scaleY = _renderTargetScale;
428
429 createPasses(options);
430
432 for (const auto& pass : collectPasses()) {
433 if (pass) {
434 addBeforePass(pass);
435 }
436 }
437 }
438
439 std::vector<std::shared_ptr<RenderPass>> RenderPassCameraFrame::collectPasses() const
440 {
441 // DEVIATION: upstream orders SSAO before the scene pass because the prepass
442 // writes valid depth data first. Our prepass execute() is a stub (full depth-only
443 // mesh submission not yet ported), so the depth texture is empty when SSAO runs.
444 // Moving SSAO after the scene passes ensures it reads valid depth from the
445 // opaque+transparent scene render. This produces correct AO from final scene depth.
446 return {_prePass, _scenePass, _colorGrabPass, _scenePassTransparent, _ssaoPass, _taaPass, _scenePassHalf,
447 _bloomPass, _dofPass, _composePass, _afterPass};
448 }
449
450 void RenderPassCameraFrame::createPasses(const CameraFrameOptions& options)
451 {
452 setupScenePrepass(options);
453 setupSsaoPass(options);
454 const auto scenePassesInfo = setupScenePass(options);
455 auto* sceneTextureWithTaa = setupTaaPass(options);
456 setupSceneHalfPass(options, sceneTextureWithTaa);
457 setupBloomPass(options, _sceneTextureHalf.get());
458 setupDofPass(options, _sceneTexture.get(), _sceneTextureHalf.get());
459 setupComposePass(options);
460 setupAfterPass(options, scenePassesInfo);
461 }
462
463 void RenderPassCameraFrame::setupScenePrepass(const CameraFrameOptions& options)
464 {
465 if (options.prepassEnabled) {
466 _prePass = std::make_shared<RenderPassPrepass>(device(), _scene, _renderer, _cameraComponent,
467 _sceneDepthTexture.get(), _sceneOptions);
468 }
469 }
470
471 RenderPassCameraFrame::ScenePassesInfo RenderPassCameraFrame::setupScenePass(const CameraFrameOptions& options)
472 {
473 ScenePassesInfo info;
474 if (!_layerComposition || !_sceneRenderTarget) {
475 return info;
476 }
477
478 _scenePass = std::make_shared<RenderPassForward>(device(), _layerComposition, _scene, _renderer);
479 _scenePass->setHdrPass(true);
480 _scenePass->init(_sceneRenderTarget, _sceneOptions);
481
482 const int lastLayerId = options.sceneColorMap ? options.lastGrabLayerId : options.lastSceneLayerId;
483 const bool lastLayerTransparent = options.sceneColorMap ? options.lastGrabLayerIsTransparent : options.lastSceneLayerIsTransparent;
484 const int sceneEndIndex = findActionIndex(lastLayerId, lastLayerTransparent, 0);
485
486 if (sceneEndIndex < 0) {
487 return info;
488 }
489
490 info.lastAddedIndex = appendActionsToPass(_scenePass, 0, sceneEndIndex, _sceneRenderTarget);
491 info.clearRenderTarget = false;
492
493 if (options.sceneColorMap) {
494 _colorGrabPass = std::make_shared<RenderPassColorGrab>(device());
495 _colorGrabPass->setSource(_sceneRenderTarget);
496
497 const int transparentEndIndex = findActionIndex(options.lastSceneLayerId, options.lastSceneLayerIsTransparent,
498 std::max(info.lastAddedIndex + 1, 0));
499 if (transparentEndIndex >= 0) {
500 _scenePassTransparent = std::make_shared<RenderPassForward>(device(), _layerComposition, _scene, _renderer);
501 _scenePassTransparent->setHdrPass(true);
502 _scenePassTransparent->init(_sceneRenderTarget);
503 info.lastAddedIndex = appendActionsToPass(_scenePassTransparent, info.lastAddedIndex + 1, transparentEndIndex,
504 _sceneRenderTarget);
505 }
506 }
507
508 return info;
509 }
510
511 void RenderPassCameraFrame::setupSsaoPass(const CameraFrameOptions& options)
512 {
513 if (options.ssaoType != SSAOTYPE_NONE && _sceneTexture && _cameraComponent) {
514 // Reuse existing SSAO pass to avoid per-frame Metal texture allocation.
515 // The pass creates Texture + RenderTarget objects in its constructor,
516 // so recreating it every frame causes GPU memory growth.
517 if (!_ssaoPass) {
518 _ssaoPass = std::make_shared<RenderPassSsao>(device(), _sceneTexture.get(), _cameraComponent, options.ssaoBlurEnabled);
519 }
520
521 // Update SSAO pass parameters from CameraComponent settings each frame
522 const auto& ssao = _cameraComponent->ssao();
523 _ssaoPass->radius = ssao.radius;
524 _ssaoPass->intensity = ssao.intensity;
525 _ssaoPass->power = ssao.power;
526 _ssaoPass->sampleCount = ssao.samples;
527 _ssaoPass->minAngle = ssao.minAngle;
528 _ssaoPass->randomize = ssao.randomize;
529 if (ssao.scale != _ssaoPass->scale()) {
530 _ssaoPass->setScale(ssao.scale);
531 }
532 } else {
533 _ssaoPass.reset();
534 }
535 }
536
537 Texture* RenderPassCameraFrame::setupTaaPass(const CameraFrameOptions& options)
538 {
539 _sceneTextureResolved = _sceneTexture.get();
540 if (options.taaEnabled && _sceneTexture) {
541 _taaPass = _cameraComponent->ensureTaaPass(device(), _sceneTexture.get());
542 if (_taaPass) {
543 _taaPass->setSourceTexture(_sceneTexture.get());
544 _taaPass->setDepthTexture(_sceneDepthTexture.get());
545 _sceneTextureResolved = _taaPass->historyTexture().get();
546 }
547 }
548 return _sceneTextureResolved;
549 }
550
551 void RenderPassCameraFrame::setupSceneHalfPass(const CameraFrameOptions& options, Texture* sourceTexture)
552 {
553 (void)options;
554 if (_sceneHalfEnabled && _sceneHalfRenderTarget && _sceneTexture) {
555 // Reuse existing downsample pass to avoid per-frame object churn.
556 if (!_scenePassHalf) {
557 RenderPassDownsample::Options downsampleOptions;
558 downsampleOptions.boxFilter = true;
559 downsampleOptions.removeInvalid = true;
560 _scenePassHalf = std::make_shared<RenderPassDownsample>(device(), _sceneTexture.get(), downsampleOptions);
561 auto passOptions = std::make_shared<RenderPassOptions>();
562 passOptions->resizeSource = std::shared_ptr<Texture>(sourceTexture, [](Texture*) {});
563 passOptions->scaleX = 0.5f;
564 passOptions->scaleY = 0.5f;
565 _scenePassHalf->init(_sceneHalfRenderTarget, passOptions);
566 const Color clearBlack(0.0f, 0.0f, 0.0f, 1.0f);
567 _scenePassHalf->setClearColor(&clearBlack);
568 }
569 } else {
570 _scenePassHalf.reset();
571 }
572 }
573
574 void RenderPassCameraFrame::setupBloomPass(const CameraFrameOptions& options, Texture* inputTexture)
575 {
576 (void)options;
577 if (_bloomEnabled && inputTexture) {
578 // Reuse existing bloom pass to avoid per-frame Metal texture allocation.
579 // The pass creates Texture + RenderTarget objects in its constructor and
580 // additional ones in frameUpdate(), so recreating every frame causes
581 // GPU memory growth.
582 if (!_bloomPass) {
583 _bloomPass = std::make_shared<RenderPassBloom>(device(), inputTexture, _hdrFormat);
584 }
585 } else {
586 _bloomPass.reset();
587 }
588 }
589
590 void RenderPassCameraFrame::setupDofPass(const CameraFrameOptions& options, Texture* inputTexture,
591 Texture* inputTextureHalf)
592 {
593 // Single-pass DOF: the compose shader reads the depth buffer directly and applies
594 // a Poisson-disc blur. The multi-pass DOF pipeline (CoC → Downsample → Blur) is
595 // NOT used — creating the _dofPass with its three sub-passes causes a black screen
596 // because the parent RenderPassDof has no render target, which corrupts the Metal
597 // render encoder state. DOF parameters are passed to the compose pass instead.
598 (void)inputTexture;
599 (void)inputTextureHalf;
600 _dofPass.reset();
601 }
602
603 void RenderPassCameraFrame::setupComposePass(const CameraFrameOptions& options)
604 {
605 _composePass = std::make_shared<RenderPassCompose>(device());
606 _composePass->sceneTexture = _sceneTextureResolved;
607 _composePass->bloomTexture = _bloomPass ? _bloomPass->bloomTexture() : nullptr;
608 _composePass->bloomIntensity = options.bloomIntensity;
609 _composePass->taaEnabled = options.taaEnabled;
610 _composePass->cocTexture = nullptr; // multi-pass DOF disabled; single-pass uses depth directly
611 _composePass->blurTexture = nullptr;
612 _composePass->blurTextureUpscale = false;
613 _composePass->dofEnabled = options.dofEnabled;
614 _composePass->ssaoTexture = options.ssaoType == SSAOTYPE_COMBINE && _ssaoPass ? _ssaoPass->ssaoTexture() : nullptr;
615 _composePass->sharpness = options.sharpness;
616 _composePass->toneMapping = _scene ? _scene->toneMapping() : TONEMAP_LINEAR;
617 _composePass->exposure = _scene ? _scene->exposure() : 1.0f;
618
619 // Single-pass DOF: pass depth texture and DOF settings to compose
620 if (options.dofEnabled && _cameraComponent) {
621 const auto& dof = _cameraComponent->dof();
622 _composePass->depthTexture = _sceneDepthTexture.get();
623 _composePass->dofFocusDistance = dof.focusDistance;
624 _composePass->dofFocusRange = dof.focusRange;
625 _composePass->dofBlurRadius = dof.blurRadius;
626 if (auto* camera = _cameraComponent->camera()) {
627 _composePass->dofCameraNear = camera->nearClip();
628 _composePass->dofCameraFar = camera->farClip();
629 }
630 }
631
632 // Vignette
633 _composePass->vignetteEnabled = options.vignetteEnabled;
634 _composePass->vignetteInner = options.vignetteInner;
635 _composePass->vignetteOuter = options.vignetteOuter;
636 _composePass->vignetteCurvature = options.vignetteCurvature;
637 _composePass->vignetteIntensity = options.vignetteIntensity;
638
639 _composePass->init(_targetRenderTarget);
640 }
641
642 void RenderPassCameraFrame::setupAfterPass(const CameraFrameOptions& options, const ScenePassesInfo& scenePassesInfo)
643 {
644 (void)options;
645 if (!_layerComposition) {
646 return;
647 }
648
649 const int fromIndex = scenePassesInfo.lastAddedIndex + 1;
650 if (fromIndex < 0 || fromIndex >= static_cast<int>(_sourceActions.size())) {
651 return;
652 }
653
654 _afterPass = std::make_shared<RenderPassForward>(device(), _layerComposition, _scene, _renderer);
655 _afterPass->init(_targetRenderTarget);
656
657 // the after-pass renders on top of the compose output
658 // so it must NOT clear the back buffer. upstream achieves this by calling
659 // addLayers(... firstLayerClears=false ...) which only uses layer-level clear
660 // flags for the first action. We replicate this by overriding camera-level
661 // clears on the first cloned action.
662 const int appended = appendActionsToPass(_afterPass, fromIndex, static_cast<int>(_sourceActions.size()) - 1, _targetRenderTarget,
663 false /* firstLayerClears */);
664 if (appended < fromIndex) {
665 _afterPass.reset();
666 }
667 }
668
670 {
672
673 const auto gd = device();
674
675 // When rendering to the back buffer (no explicit target), the child passes
676 // need their offscreen render targets sized to the device/window dimensions.
677 // RenderPass::frameUpdate() skips the resize when resizeSource is null and
678 // the back buffer has no color buffer texture, so we handle it here.
679 if (gd && _sceneRenderTarget && !_targetRenderTarget) {
680 const auto [devW, devH] = gd->size();
681 if (devW > 0 && devH > 0 &&
682 (_sceneRenderTarget->width() != devW || _sceneRenderTarget->height() != devH)) {
683 _sceneRenderTarget->resize(devW, devH);
684 }
685 }
686
687 if (gd && _sceneDepthTexture) {
688 gd->setSceneDepthMap(_sceneDepthTexture.get());
689 }
690
691 // When SSAO type is "lighting", bind the SSAO texture on the device
692 // so the forward pass fragment shader can sample it (VT_FEATURE_SSAO).
693 if (gd) {
694 gd->setSsaoForwardTexture(
695 _options.ssaoType == SSAOTYPE_LIGHTING && _ssaoPass
696 ? _ssaoPass->ssaoTexture() : nullptr);
697 }
698
699 if (_sceneTexture && _sceneDepthTexture) {
700 assert(_sceneTexture->width() == _sceneDepthTexture->width() &&
701 _sceneTexture->height() == _sceneDepthTexture->height() &&
702 "CameraFrame scene color/depth textures must stay dimension-matched.");
703 }
704
705 if (_taaPass) {
706 _taaPass->setDepthTexture(_sceneDepthTexture.get());
707 }
708
709 if (_composePass) {
710 auto* resolvedTexture = _sceneTexture.get();
711 if (_taaPass) {
712 auto taaTexture = _taaPass->update();
713 resolvedTexture = _taaPass->historyValid()
714 ? (taaTexture ? taaTexture.get() : _sceneTexture.get())
715 : _sceneTexture.get();
716 }
717
718 _sceneTextureResolved = resolvedTexture;
719 _composePass->sceneTexture = resolvedTexture;
720 if (_scenePassHalf) {
721 _scenePassHalf->setSourceTexture(resolvedTexture);
722 }
723 }
724 }
725}
CameraFrameOptions sanitizeOptions(const CameraFrameOptions &options) const
void updateSourceActions(const std::vector< RenderAction * > &sourceActions, LayerComposition *layerComposition, Scene *scene, Renderer *renderer, const std::shared_ptr< RenderTarget > &targetRenderTarget)
bool needsReset(const CameraFrameOptions &options) const
RenderPassCameraFrame(const std::shared_ptr< GraphicsDevice > &device, LayerComposition *layerComposition, Scene *scene, Renderer *renderer, const std::vector< RenderAction * > &sourceActions, CameraComponent *cameraComponent, const std::shared_ptr< RenderTarget > &targetRenderTarget)
void update(const CameraFrameOptions &options)
std::shared_ptr< RenderTarget > renderTarget() const
Definition renderPass.h:98
virtual void frameUpdate() const
std::shared_ptr< GraphicsDevice > device() const
Definition renderPass.h:124
RenderPass(const std::shared_ptr< GraphicsDevice > &device)
Definition renderPass.h:66
void addBeforePass(const std::shared_ptr< RenderPass > &renderPass)
Container for the scene graph, lighting environment, fog, skybox, and layer composition.
Definition scene.h:29
GPU texture resource supporting 2D, cubemap, volume, and array formats with mipmap management.
Definition texture.h:57
constexpr int LAYERID_DEPTH
Definition constants.h:18
constexpr std::string_view SSAOTYPE_NONE
constexpr std::string_view SSAOTYPE_COMBINE
constexpr std::string_view SSAOTYPE_LIGHTING