VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
engine.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 18.07.2025.
5//
6#include "engine.h"
7
8#include <cassert>
9
17#include "scene/frameGraph.h"
18#include "spdlog/spdlog.h"
19
20namespace visutwin::canvas
21{
22 std::unordered_map<std::string, std::shared_ptr<Engine>> Engine::_engines;
23
24 MakeTickCallback makeTick(const std::shared_ptr<Engine>& app) {
25 return [app](double timestamp, void* xrFrame) {
26 if (!app || !app->_graphicsDevice) {
27 return;
28 }
29
30 // Cancel any hanging request
31 if (app->_frameRequestId) {
32 app->_frameRequestId = nullptr;
33 }
34
35 app->_inFrameUpdate = true;
36
37 double currentTime = app->processTimestamp(timestamp);
38 if (currentTime == 0) {
39 auto now = std::chrono::high_resolution_clock::now();
40 currentTime = std::chrono::duration<double, std::milli>(now.time_since_epoch()).count();
41 }
42
43 double ms = currentTime - (app->_time > 0 ? app->_time : currentTime);
44 float dt = static_cast<float>(ms / 1000.0);
45 dt = std::clamp(dt, 0.0f, app->_maxDeltaTime);
46 dt *= app->_timeScale;
47
48 app->_time = currentTime;
49
50 // Submit a request to queue up a new animation frame
51 if (app->_xr && app->_xr->active()) {
52 app->_frameRequestId = app->_xr->requestAnimationFrame(app->_tick);
53 } else {
54 // Would use platform-specific requestAnimationFrame
55 app->_frameRequestId = reinterpret_cast<void*>(1); // Placeholder
56 }
57
58 if (app->_graphicsDevice->contextLost()) {
59 app->_inFrameUpdate = false;
60 return;
61 }
62
63 app->fillFrameStatsBasic(currentTime, dt, static_cast<float>(ms));
64 app->fillFrameStats();
65
66 app->fire("frameupdate", ms);
67
68 bool skipUpdate = false;
69
70 if (xrFrame && app->_xr) {
71 skipUpdate = !app->_xr->update(xrFrame);
72 }
73
74 if (!skipUpdate) {
75 app->update(dt);
76
77 app->fire("framerender");
78
79 if (app->_autoRender || app->_renderNextFrame) {
80 app->render();
81 app->_renderNextFrame = false;
82 }
83
84 app->fire("frameend");
85 }
86
87 app->_inFrameUpdate = false;
88
89 if (app->_destroyRequested) {
90 app->destroy();
91 }
92 };
93 }
94
96 {
97 destroy();
98 }
99
101 {
102 if (_inFrameUpdate) {
103 _destroyRequested = true;
104 return;
105 }
106
107 fire("destroy");
108
109 // Cleanup root entity
110 if (_root) {
111 _root.reset();
112 }
113
114 // Cleanup input devices
115 if (_mouse) {
116 _mouse->detach();
117 _mouse.reset();
118 }
119
120 if (_keyboard) {
121 _keyboard->detach();
122 _keyboard.reset();
123 }
124
125 if (_touch) {
126 _touch->detach();
127 _touch.reset();
128 }
129
130 if (_elementInput) {
131 _elementInput->detach();
132 _elementInput.reset();
133 }
134
135 if (_gamepads) {
136 _gamepads.reset();
137 }
138
139 // Cleanup systems
140 if (_systems) {
141 _systems.reset();
142 }
143
144 // Cleanup assets
145 if (_assets) {
146 for (auto assetList = _assets->list(); auto& asset : assetList) {
147 asset->unload();
148 }
149 _assets.reset();
150 }
151
152 // Cleanup other components
153 if (_bundles) {
154 _bundles.reset();
155 }
156
157 if (_i18n) {
158 _i18n.reset();
159 }
160
161 if (_loader) {
162 _loader->shutdown();
163 _loader.reset();
164 }
165
166 if (_scene) {
167 _scene.reset();
168 }
169
170 if (_scripts) {
171 _scripts.reset();
172 }
173
174 if (_scenes) {
175 _scenes.reset();
176 }
177
178 if (_lightmapper) {
179 _lightmapper.reset();
180 }
181
182 if (_batcher) {
183 _batcher.reset();
184 }
185
186 _entityIndex.clear();
187
188 if (_xr) {
189 _xr->end();
190 _xr.reset();
191 }
192
193 if (_renderer) {
194 _renderer.reset();
195 }
196
197 if (_graphicsDevice) {
198 _graphicsDevice.reset();
199 }
200
201 _tick = nullptr;
202
203 // Remove from the applications registry
204 _engines.clear();
205 }
206
207 void Engine::init(const AppOptions& appOptions)
208 {
209 _graphicsDevice = appOptions.graphicsDevice;
210 if (!_graphicsDevice) {
211 throw std::runtime_error("The application cannot be created without a valid GraphicsDevice");
212 }
213
214 _root = std::make_shared<Entity>();
215 _root->setEngine(this);
216 // The root entity has no parent, so _enabledInHierarchy must be set
217 // explicitly — onInsertChild never runs for it.
218 // Matches upstream: `this.root._enabledInHierarchy = true;`
219 _root->setEnabledInHierarchy(true);
220
221 Asset::setDefaultGraphicsDevice(_graphicsDevice);
222
223 initDefaultMaterial();
224 initProgramLibrary();
225
226 _stats = std::make_shared<ApplicationStats>(_graphicsDevice);
227 _scene = std::make_shared<Scene>(_graphicsDevice);
228 registerSceneImmediate(_scene);
229
230 _loader = std::make_shared<ResourceLoader>(shared_from_this());
231 _loader->addHandler(AssetType::TEXTURE, std::make_unique<TextureResourceHandler>());
232 _loader->addHandler(AssetType::CONTAINER, std::make_unique<ContainerResourceHandler>());
233 _loader->addHandler(AssetType::FONT, std::make_unique<FontResourceHandler>());
234 _assets = std::make_shared<AssetRegistry>(_loader);
235 _bundles = std::make_shared<BundleRegistry>(_assets);
236 _scenes = std::make_shared<SceneRegistry>(shared_from_this());
237 _scripts = std::make_shared<ScriptRegistry>(shared_from_this());
238
239 _systems = std::make_shared<ComponentSystemRegistry>();
240 for (auto componentSystem : appOptions.componentSystems)
241 {
242 _systems->add(componentSystem(this));
243 }
244
245 _i18n = std::make_shared<I18n>(shared_from_this());
246
247 _scriptsOrder = appOptions.scriptsOrder;
248
249 // Create default layers
250 _defaultLayerWorld = std::make_shared<Layer>("World", 1);
251 _defaultLayerDepth = std::make_shared<Layer>("Depth", 2);
252 _defaultLayerSkybox = std::make_shared<Layer>("Skybox", 3);
253 _defaultLayerUi = std::make_shared<Layer>("UI", 4);
254 _defaultLayerImmediate = std::make_shared<Layer>("Immediate", 5);
255
256 // Create default layer composition
257 auto defaultLayerComposition = std::make_shared<LayerComposition>("default");
258 defaultLayerComposition->pushOpaque(_defaultLayerWorld);
259 defaultLayerComposition->pushOpaque(_defaultLayerDepth);
260 defaultLayerComposition->pushOpaque(_defaultLayerSkybox);
261 defaultLayerComposition->pushTransparent(_defaultLayerWorld);
262 defaultLayerComposition->pushOpaque(_defaultLayerImmediate);
263 defaultLayerComposition->pushTransparent(_defaultLayerImmediate);
264 defaultLayerComposition->pushTransparent(_defaultLayerUi);
265 _scene->setLayers(defaultLayerComposition);
266
267 _renderer = std::make_shared<ForwardRenderer>(_graphicsDevice, _scene);
268
269 if (appOptions.lightmapper) {
270 _lightmapper = appOptions.lightmapper;
271 }
272
273 if (appOptions.batchManager) {
274 _batcher = appOptions.batchManager;
275 } else {
276 _batcher = std::make_shared<BatchManager>(_graphicsDevice.get());
277 }
278
279 _keyboard = appOptions.keyboard;
280 _mouse = appOptions.mouse;
281 _touch = appOptions.touch;
282 _gamepads = appOptions.gamepads;
283 _elementInput = appOptions.elementInput;
284 if (_elementInput) {
285 _elementInput->setEngine(shared_from_this());
286 }
287
288 _xr = appOptions.xr;
289 _scriptPrefix = appOptions.scriptPrefix;
290
291 // Create a tick function
292 _tick = makeTick(shared_from_this());
293 }
294
295 void Engine::initDefaultMaterial()
296 {
297 auto material = std::make_shared<StandardMaterial>();
298 material->setName("Default Material");
299 setDefaultMaterial(_graphicsDevice, material);
300 }
301
302 void Engine::initProgramLibrary()
303 {
304 auto library = std::make_shared<ProgramLibrary>(_graphicsDevice, new StandardMaterial());
305 setProgramLibrary(_graphicsDevice, library);
306 }
307
309 {
310 _frame = 0;
311 tick();
312 }
313
315 {
316 assert(_graphicsDevice && "Engine::render requires a valid graphics device");
317 _frameStartCalled = false;
318 _renderCompositionCalled = false;
319 _frameEndCalled = false;
320
321 _graphicsDevice->frameStart();
322 _frameStartCalled = true;
323 renderComposition();
324 _renderCompositionCalled = true;
325 fire("postrender");
326
327 if (_graphicsDevice->insideRenderPass()) {
328 spdlog::error("Frame parity violation: render() reached frameEnd while still inside a render pass");
329 assert(!_graphicsDevice->insideRenderPass() && "Unbalanced render pass before frameEnd");
330 }
331
332 _graphicsDevice->frameEnd();
333 _frameEndCalled = true;
334
335 if (!(_frameStartCalled && _renderCompositionCalled && _frameEndCalled)) {
336 spdlog::error("Frame parity violation: expected frameStart -> render passes -> frameEnd sequence");
337 assert(false && "Invalid frame lifecycle ordering");
338 }
339 }
340
341 void Engine::tick()
342 {
343 if (_tick) {
344 auto now = std::chrono::high_resolution_clock::now();
345 const double ms = std::chrono::duration<double, std::milli>(now.time_since_epoch()).count();
346 _tick(ms, nullptr);
347 }
348 }
349
350 void Engine::renderComposition()
351 {
352 if (!_frameStartCalled || _frameEndCalled) {
353 spdlog::error("Frame parity violation: renderComposition called outside active frame scope");
354 assert(false && "renderComposition must run after frameStart and before frameEnd");
355 }
356
357 if (!_renderer || !_scene || !_graphicsDevice) {
358 return;
359 }
360
361 const auto& layerComposition = _scene->layers();
362 if (!layerComposition) {
363 return;
364 }
365
366 FrameGraph frameGraph;
367 _renderer->buildFrameGraph(&frameGraph, layerComposition.get());
368 frameGraph.render(_graphicsDevice.get());
369 }
370
371 void Engine::registerSceneImmediate(const std::shared_ptr<Scene>& scene) {
372 if (_scene && _scene->immediate()) {
373 on("postrender", [scene]() {
374 scene->immediate()->onPostRender();
375 });
376 }
377 }
378
379 void Engine::fillFrameStatsBasic(double now, float dt, float ms)
380 {
381 if (_stats) {
382 auto& stats = _stats->frame();
383 stats.dt = dt;
384 stats.ms = ms;
385 if (now > stats.timeToCountFrames) {
386 stats.fps = stats.fpsAccum;
387 stats.fpsAccum = 0;
388 stats.timeToCountFrames = now + 1000;
389 } else {
390 stats.fpsAccum++;
391 }
392
393 _stats->drawCalls().total = _graphicsDevice->drawCallsPerFrame();
394 _graphicsDevice->resetDrawCallsPerFrame();
395
396 stats.gsplats = _renderer->_gsplatCount;
397 }
398 }
399
400 void Engine::setCanvasFillMode(FillMode mode, int width, int height)
401 {
402 _fillMode = mode;
403 resizeCanvas(width, height);
404 }
405
406 std::pair<int, int> Engine::resizeCanvas(int width, int height)
407 {
408 if (!_allowResize) {
409 return {0, 0};
410 }
411
412 // Prevent resizing when in XR session
413 if (_xr && _xr->active()) {
414 return {0, 0};
415 }
416
417 // Get window dimensions (simplified)
418 auto windowSize = _graphicsDevice->size(); // Would get from an actual window
419
420 if (_fillMode == FillMode::FILLMODE_KEEP_ASPECT) {
421 float r = static_cast<float>(windowSize.first) / windowSize.second;
422 float winR = static_cast<float>(windowSize.first) / windowSize.second;
423
424 if (r > winR) {
425 width = windowSize.first;
426 height = static_cast<int>(width / r);
427 } else {
428 height = windowSize.second;
429 width = static_cast<int>(height * r);
430 }
431 } else if (_fillMode == FillMode::FILLMODE_FILL_WINDOW) {
432 width = windowSize.first;
433 height = windowSize.second;
434 }
435
436 // Set canvas style (would interact with actual canvas)
438
439 return {width, height};
440 }
441
442 void Engine::setCanvasResolution(ResolutionMode mode, int width, int height)
443 {
444 _resolutionMode = mode;
445
446 // In AUTO mode the resolution is the same as the canvas size, unless specified
447 if (mode == ResolutionMode::RESOLUTION_AUTO && (width == 0)) {
448 auto size = _graphicsDevice->size();
449 width = size.first;
450 height = size.second;
451 }
452
453 _graphicsDevice->resizeCanvas(width, height);
454 }
455
456 void Engine::fillFrameStats()
457 {
458 auto& stats = _stats->frame();
459
460 // Render stats
461 stats.cameras = _renderer->_camerasRendered;
462 stats.materials = _renderer->_materialSwitches;
463 stats.shaders = _graphicsDevice->_shaderSwitchesPerFrame;
464 stats.shadowMapUpdates = _renderer->_shadowMapUpdates;
465 stats.shadowMapTime = _renderer->_shadowMapTime;
466 stats.depthMapTime = _renderer->_depthMapTime;
467 stats.forwardTime = _renderer->_forwardTime;
468
469 auto& prims = _graphicsDevice->_primsPerFrame;
470 if (prims.size() <= static_cast<size_t>(PRIMITIVE_TRIFAN)) {
471 prims.resize(static_cast<size_t>(PRIMITIVE_TRIFAN) + 1, 0);
472 }
473 stats.triangles = prims[PRIMITIVE_TRIANGLES] / 3 +
474 std::max(prims[PRIMITIVE_TRISTRIP] - 2, 0) +
475 std::max(prims[PRIMITIVE_TRIFAN] - 2, 0);
476
477 stats.cullTime = _renderer->_cullTime;
478 stats.sortTime = _renderer->_sortTime;
479 stats.skinTime = _renderer->_skinTime;
480 stats.morphTime = _renderer->_morphTime;
481 stats.lightClusters = _renderer->_lightClusters;
482 stats.lightClustersTime = _renderer->_lightClustersTime;
483 stats.otherPrimitives = 0;
484
485 for (int i = 0; i < prims.size(); i++) {
486 if (i < PRIMITIVE_TRIANGLES) {
487 stats.otherPrimitives += prims[i];
488 }
489 prims[i] = 0;
490 }
491
492 _renderer->_camerasRendered = 0;
493 _renderer->_materialSwitches = 0;
494 _renderer->_shadowMapUpdates = 0;
495 _graphicsDevice->_shaderSwitchesPerFrame = 0;
496 _renderer->_cullTime = 0;
497 _renderer->_layerCompositionUpdateTime = 0;
498 _renderer->_lightClustersTime = 0;
499 _renderer->_sortTime = 0;
500 _renderer->_skinTime = 0;
501 _renderer->_morphTime = 0;
502 _renderer->_shadowMapTime = 0;
503 _renderer->_depthMapTime = 0;
504 _renderer->_forwardTime = 0;
505
506 // Draw call stats
507 auto& drawCallstats = _stats->drawCalls();
508 drawCallstats.forward = _renderer->_forwardDrawCalls;
509 drawCallstats.depth = 0;
510 drawCallstats.shadow = _renderer->_shadowDrawCalls;
511 drawCallstats.skinned = _renderer->_skinDrawCalls;
512 drawCallstats.immediate = 0;
513 drawCallstats.instanced = 0;
514 drawCallstats.removedByInstancing = 0;
515 drawCallstats.misc = drawCallstats.total - (drawCallstats.forward + drawCallstats.shadow);
516
517 _renderer->_shadowDrawCalls = 0;
518 _renderer->_forwardDrawCalls = 0;
519 _renderer->_numDrawCallsCulled = 0;
520 _renderer->_skinDrawCalls = 0;
521 _renderer->_instancedDrawCalls = 0;
522
523 _stats->misc().renderTargetCreationTime = _graphicsDevice->_renderTargetCreationTime;
524
525 auto& particleStats = _stats->particles();
526 particleStats.updatesPerFrame = particleStats._updatesPerFrame;
527 particleStats.frameTime = particleStats._frameTime;
528 particleStats._updatesPerFrame = 0;
529 particleStats._frameTime = 0;
530 }
531
532 void Engine::inputUpdate(float dt)
533 {
534 if (_controller) {
535 _controller->update();
536 }
537 if (_mouse) {
538 _mouse->update();
539 }
540 if (_keyboard) {
541 _keyboard->update();
542 }
543 if (_gamepads) {
544 _gamepads->update();
545 }
546 }
547
548 void Engine::update(float dt)
549 {
550 _frame++;
551
552 if (_stats) {
553 _stats->frame().fixedUpdateTime = 0.0;
554 }
555
556 _graphicsDevice->update();
557
558 // Dispatch completed async resource load callbacks on the main thread.
559 // Limit to 1 per frame so heavy callbacks (e.g. 70 MB GLB parsing)
560 // don't stall the event loop and cause a spinning-wait cursor.
561 if (_loader) {
562 _loader->processCompletions(1);
563 }
564
565 auto updateStart = std::chrono::high_resolution_clock::now();
566 bool hasScriptSystem = false;
567 bool scriptUpdateCalled = false;
568 bool scriptPostUpdateCalled = false;
569
570 _systems->fire(_inTools ? "toolsUpdate" : "update", dt);
571 _systems->fire("animationUpdate", dt);
572
573 if (auto* scriptSystemBase = _systems->getByComponentType<ScriptComponent>()) {
574 if (auto* scriptSystem = dynamic_cast<ScriptComponentSystem*>(scriptSystemBase)) {
575 hasScriptSystem = true;
576 scriptSystem->update(dt);
577 scriptUpdateCalled = true;
578 }
579 }
580
581 _systems->fire("postUpdate", dt);
582
583 if (auto* scriptSystemBase = _systems->getByComponentType<ScriptComponent>()) {
584 if (auto* scriptSystem = dynamic_cast<ScriptComponentSystem*>(scriptSystemBase)) {
585 hasScriptSystem = true;
586 if (!scriptUpdateCalled) {
587 spdlog::error("Script lifecycle parity violation: postUpdateScripts called before updateScripts");
588 assert(scriptUpdateCalled && "Script postUpdate invoked before script update");
589 }
590 scriptSystem->postUpdate(dt);
591 scriptPostUpdateCalled = true;
592 }
593 }
594
595 if (hasScriptSystem && !(scriptUpdateCalled && scriptPostUpdateCalled)) {
596 spdlog::error("Script lifecycle parity violation: expected script update + postUpdate each frame");
597 assert(false && "Incomplete script lifecycle in frame update");
598 }
599
600 // Update dynamic batch matrix palettes and AABBs.
601 // called per frame after
602 // scripts have updated transforms (FK, animation, etc.).
603 if (_batcher) {
604 _batcher->updateAll();
605 }
606
607 fire("update", dt);
608
609 // Fixed timestep accumulator — run simulation substeps at constant rate
610 _fixedTimeAccumulator += dt;
611 int substeps = 0;
612 while (_fixedTimeAccumulator >= _fixedDeltaTime
613 && substeps < _maxFixedSubSteps) {
614 fixedUpdate(_fixedDeltaTime);
615 _fixedTimeAccumulator -= _fixedDeltaTime;
616 substeps++;
617 }
618 // Clamp accumulator to prevent unbounded growth if substep cap was hit
619 if (_fixedTimeAccumulator > _fixedDeltaTime) {
620 _fixedTimeAccumulator = _fixedDeltaTime;
621 }
622 _fixedTimeAlpha = _fixedDeltaTime > 0.0f
623 ? static_cast<float>(_fixedTimeAccumulator / _fixedDeltaTime)
624 : 0.0f;
625
626 inputUpdate(dt);
627
628 auto updateEnd = std::chrono::high_resolution_clock::now();
629 auto updateTime = std::chrono::duration<float, std::milli>(updateEnd - updateStart).count();
630
631 if (_stats) {
632 _stats->frame().updateTime = updateTime;
633 }
634 }
635
636 void Engine::fixedUpdate(float fixedDt)
637 {
638 auto fixedStart = std::chrono::high_resolution_clock::now();
639
640 _systems->fire("fixedUpdate", fixedDt);
641
642 if (auto* scriptSystemBase = _systems->getByComponentType<ScriptComponent>()) {
643 if (auto* scriptSystem = dynamic_cast<ScriptComponentSystem*>(scriptSystemBase)) {
644 scriptSystem->fixedUpdate(fixedDt);
645 }
646 }
647
648 fire("fixedUpdate", fixedDt);
649
650 auto fixedEnd = std::chrono::high_resolution_clock::now();
651 if (_stats) {
652 _stats->frame().fixedUpdateTime +=
653 std::chrono::duration<double, std::milli>(fixedEnd - fixedStart).count();
654 }
655 }
656
658 {
659 // Don't update if we are in VR or XR
660 if ((!_allowResize) || (_xr != nullptr && _xr->active())) {
661 return;
662 }
663
664 // In AUTO mode the resolution is changed to match the canvas size
665 if (_resolutionMode == ResolutionMode::RESOLUTION_AUTO) {
666 int w, h;
667 SDL_GetWindowSizeInPixels(_window, &w, &h);
668 _graphicsDevice->resizeCanvas(w, h);
669 }
670 }
671}
static void setDefaultGraphicsDevice(const std::shared_ptr< GraphicsDevice > &graphicsDevice)
Definition asset.cpp:30
void setCanvasResolution(ResolutionMode mode, int width=0, int height=0)
Definition engine.cpp:442
void update(float dt)
Definition engine.cpp:548
std::pair< int, int > resizeCanvas(int width=0, int height=0)
Definition engine.cpp:406
friend MakeTickCallback makeTick(const std::shared_ptr< Engine > &engine)
Definition engine.cpp:24
void init(const AppOptions &appOptions)
Definition engine.cpp:207
void setCanvasFillMode(FillMode mode, int width=0, int height=0)
Definition engine.cpp:400
const std::shared_ptr< Scene > & scene() const
Definition engine.h:52
void fixedUpdate(float fixedDt)
Definition engine.cpp:636
void inputUpdate(float dt)
Definition engine.cpp:532
EventHandler * fire(const std::string &name, Args &&... args)
EventHandle * on(const std::string &name, HandleEventCallback callback, void *scope=nullptr)
constexpr const char * CONTAINER
Definition asset.h:25
constexpr const char * FONT
Definition asset.h:28
constexpr const char * TEXTURE
Definition asset.h:40
void setProgramLibrary(const std::shared_ptr< GraphicsDevice > &device, const std::shared_ptr< ProgramLibrary > &library)
std::function< void(double, void *)> MakeTickCallback
Definition xrManager.h:14
MakeTickCallback makeTick(const std::shared_ptr< Engine > &app)
Definition engine.cpp:24
@ PRIMITIVE_TRISTRIP
Definition mesh.h:24
@ PRIMITIVE_TRIFAN
Definition mesh.h:25
@ PRIMITIVE_TRIANGLES
Definition mesh.h:23
void setDefaultMaterial(const std::shared_ptr< GraphicsDevice > &device, const std::shared_ptr< Material > &material)
Definition material.cpp:372
std::vector< ComponentSystemCreator > componentSystems
Definition appOptions.h:41
std::shared_ptr< ElementInput > elementInput
Definition appOptions.h:56
std::shared_ptr< XrManager > xr
Definition appOptions.h:57
std::shared_ptr< Mouse > mouse
Definition appOptions.h:53
std::shared_ptr< BatchManager > batchManager
Definition appOptions.h:50
std::vector< std::string > scriptsOrder
Definition appOptions.h:45
std::shared_ptr< GamePads > gamepads
Definition appOptions.h:54
std::shared_ptr< GraphicsDevice > graphicsDevice
Definition appOptions.h:43
std::shared_ptr< Keyboard > keyboard
Definition appOptions.h:52
std::shared_ptr< TouchDevice > touch
Definition appOptions.h:55
std::shared_ptr< Lightmapper > lightmapper
Definition appOptions.h:48