VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
renderPassForward.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 09.02.2026.
5//
6#include "renderPassForward.h"
7
8#include <cassert>
9#include <unordered_map>
10
11#include "renderer.h"
12#include "spdlog/spdlog.h"
14#include "scene/scene.h"
15
16namespace visutwin::canvas
17{
18 RenderPassForward::RenderPassForward(const std::shared_ptr<GraphicsDevice>& device,
19 LayerComposition* layerComposition, Scene* scene, Renderer* renderer)
20 : RenderPass(device), _layerComposition(layerComposition), _scene(scene), _renderer(renderer)
21 {
22 }
23
25 {
26 if (renderAction) {
27 _renderActions.push_back(renderAction);
28 }
29 }
30
31 void RenderPassForward::updateClears()
32 {
33 if (_renderActions.empty()) {
34 return;
35 }
36
37 const auto* ra = _renderActions.front();
38 const auto* cameraComp = ra ? ra->camera : nullptr;
39 const auto* camera = cameraComp ? cameraComp->camera() : nullptr;
40 if (!camera) {
41 return;
42 }
43
44 // RenderPassForward uses clear flags precomputed on the first render action.
45 // This avoids re-clearing when a pass starts in the middle of camera action sequence.
46 const bool clearColor = ra->clearColor;
47 const bool clearDepth = ra->clearDepth;
48 const bool clearStencil = ra->clearStencil;
49
50 if (clearColor) {
51 const auto clear = camera->clearColor();
52 setClearColor(&clear);
53 } else {
54 setClearColor(nullptr);
55 }
56
57 if (clearDepth) {
58 const float clearDepthValue = 1.0f;
59 setClearDepth(&clearDepthValue);
60 } else {
61 setClearDepth(nullptr);
62 }
63
64 if (clearStencil) {
65 const int clearStencilValue = 0;
66 setClearStencil(&clearStencilValue);
67 } else {
68 setClearStencil(nullptr);
69 }
70 }
71
73 {
74 if (_beforeCalled) {
75 spdlog::error("RenderPassForward parity violation: before() called twice without matching after()");
76 assert(!_beforeCalled && "RenderPassForward::before called while pass is already active");
77 }
78 _beforeCalled = true;
79 _executeCalled = false;
80
81 // when CameraFrame is active, the scene forward passes
82 // output linear HDR; the compose pass handles tonemapping and gamma.
83 // The flag is set on the graphics device so the draw() call can propagate
84 // it as a runtime uniform bit (not a compile-time variant).
85 if (_hdrPass) {
86 device()->setHdrPass(true);
87 }
88
89 refreshCameraUseFlags();
90
91 if (!validateRenderActionOrder()) {
92 spdlog::error("RenderPassForward parity violation: invalid render action ordering");
93 assert(false && "Invalid render action order");
94 }
95
96 updateClears();
97
98 for (const auto* ra : _renderActions) {
99 if (ra && ra->firstCameraUse && _scene) {
100 _scene->fire("prerender", ra->camera);
101 }
102 }
103 }
104
105 void RenderPassForward::renderRenderAction(RenderAction* renderAction, const bool firstRenderAction)
106 {
107 (void)firstRenderAction;
108 if (!_beforeCalled) {
109 spdlog::error("RenderPassForward parity violation: renderRenderAction() called before before()");
110 assert(_beforeCalled && "Render action executed outside pass lifecycle");
111 }
112 if (!renderAction || !renderAction->camera || !_renderer) {
113 return;
114 }
115
116 auto* camera = renderAction->camera->camera();
117 auto* layer = renderAction->layer;
118 auto* target = renderAction->renderTarget.get();
119
120 if (_scene) {
121 _scene->fire("prerender:layer", renderAction->camera, layer, renderAction->transparent);
122 }
123
124 _renderer->renderForwardLayer(camera, target, layer, renderAction->transparent);
125
126 if (_scene) {
127 _scene->fire("postrender:layer", renderAction->camera, layer, renderAction->transparent);
128 }
129 }
130
132 {
133 if (!_beforeCalled) {
134 spdlog::error("RenderPassForward parity violation: execute() called before before()");
135 assert(_beforeCalled && "RenderPassForward::execute requires before()");
136 }
137 _executeCalled = true;
138
139 for (size_t i = 0; i < _renderActions.size(); ++i) {
140 auto* ra = _renderActions[i];
141 if (!ra || !_layerComposition || !_layerComposition->isEnabled(ra->layer, ra->transparent)) {
142 continue;
143 }
144 renderRenderAction(ra, i == 0);
145 }
146 }
147
149 {
150 if (!_beforeCalled) {
151 spdlog::error("RenderPassForward parity violation: after() called before before()");
152 assert(_beforeCalled && "RenderPassForward::after requires before()");
153 }
154 if (!_executeCalled) {
155 spdlog::error("RenderPassForward parity violation: after() called before execute()");
156 assert(_executeCalled && "RenderPassForward::after requires execute()");
157 }
158
159 for (const auto* ra : _renderActions) {
160 if (ra && ra->lastCameraUse && _scene) {
161 _scene->fire("postrender", ra->camera);
162 }
163 }
164
165 // restore normal shader output after HDR pass completes.
166 if (_hdrPass) {
167 device()->setHdrPass(false);
168 }
169
170 _beforeCalled = false;
171 _executeCalled = false;
172 }
173
174 void RenderPassForward::refreshCameraUseFlags()
175 {
176 for (size_t i = 0; i < _renderActions.size(); ++i) {
177 auto* ra = _renderActions[i];
178 if (!ra || !ra->camera) {
179 continue;
180 }
181
182 const auto* camera = ra->camera;
183 bool hasPreviousForCamera = false;
184 bool hasNextForCamera = false;
185
186 for (size_t j = 0; j < i; ++j) {
187 if (_renderActions[j] && _renderActions[j]->camera == camera) {
188 hasPreviousForCamera = true;
189 break;
190 }
191 }
192
193 for (size_t j = i + 1; j < _renderActions.size(); ++j) {
194 if (_renderActions[j] && _renderActions[j]->camera == camera) {
195 hasNextForCamera = true;
196 break;
197 }
198 }
199
200 ra->firstCameraUse = !hasPreviousForCamera;
201 ra->lastCameraUse = !hasNextForCamera;
202 }
203 }
204
205 bool RenderPassForward::validateRenderActionOrder() const
206 {
207 std::unordered_map<const CameraComponent*, bool> activeCameraSpan;
208
209 for (const auto* ra : _renderActions) {
210 if (!ra || !ra->camera || !ra->layer) {
211 return false;
212 }
213
214 const auto* camera = ra->camera;
215 const bool isActive = activeCameraSpan.contains(camera) && activeCameraSpan.at(camera);
216
217 if (ra->firstCameraUse && isActive) {
218 return false;
219 }
220 if (!ra->firstCameraUse && !isActive) {
221 return false;
222 }
223
224 if (ra->firstCameraUse) {
225 activeCameraSpan[camera] = true;
226 }
227
228 if (ra->lastCameraUse) {
229 if (!activeCameraSpan.contains(camera) || !activeCameraSpan[camera]) {
230 return false;
231 }
232 activeCameraSpan[camera] = false;
233 }
234 }
235
236 for (const auto& [camera, active] : activeCameraSpan) {
237 (void)camera;
238 if (active) {
239 return false;
240 }
241 }
242 return true;
243 }
244}
EventHandler * fire(const std::string &name, Args &&... args)
void addRenderAction(RenderAction *renderAction)
RenderPassForward(const std::shared_ptr< GraphicsDevice > &device, LayerComposition *layerComposition, Scene *scene, Renderer *renderer)
void setClearColor(const Color *color=nullptr)
void setClearDepth(const float *depthValue=nullptr)
void setClearStencil(const int *stencilValue=nullptr)
std::shared_ptr< GraphicsDevice > device() const
Definition renderPass.h:124
RenderPass(const std::shared_ptr< GraphicsDevice > &device)
Definition renderPass.h:66
Container for the scene graph, lighting environment, fog, skybox, and layer composition.
Definition scene.h:29
std::shared_ptr< RenderTarget > renderTarget