Cogs.Core
AssetPipeCommand.cpp
1#include <queue>
2#include <fstream>
3#include <unordered_set>
4
5#include "AssetPipeCommand.h"
6
7#include "Batch.h"
8
9#include "Foundation/Platform/Atomic.h"
10#include "Foundation/Logging/Logger.h"
11#include "Foundation/Platform/IO.h"
12#include "Foundation/Platform/Timer.h"
13
14#include "Rendering/VertexFormat.h"
15
16#include "Services/TaskManager.h"
17#include "Resources/AssetManager.h"
18#include "Resources/ModelManager.h"
19#include "Resources/MaterialInstance.h"
20#include "Serialization/AssetWriter.h"
21#include "Serialization/ModelWriter.h"
22
23#include "Components/Core/AssetComponent.h" // AssetFlags
24
25#include "zstd.h"
26#include "rapidjson/stringbuffer.h"
27#include "rapidjson/prettywriter.h"
28
29#include "PackMeshCommand.h"
30
31namespace {
32 using namespace Cogs::Core;
33
34 Cogs::Logging::Log logger = Cogs::Logging::getLogger("AssetPipe");
35
36 constexpr uint32_t NoValue = static_cast<uint32_t>(-1);
37 const Cogs::Core::StringRef bboxString = Cogs::Core::Strings::add("bbox");
38 const Cogs::Core::StringRef errorsString = Cogs::Core::Strings::add("errors");
39 const Cogs::Core::StringRef byteSizeString = Cogs::Core::Strings::add("byteSize");
40
41 struct IdRange
42 {
43 uint32_t min;
44 uint32_t max;
45 };
46 typedef std::vector<IdRange> IdRanges;
47
48 struct AssetItem
49 {
50 enum struct State {
51 None,
52 Issued,
53 Loading,
54 IssueDependencies,
55 WaitingForDeps,
56 Processing,
57 Done,
58 Failed
59 } state = State::None;
60
61 AssetHandle handle;
62 std::string sourcePath;
63 std::string targetPath;
64 std::vector<uint32_t> resourceDependencies;
65 TaskId task = NoTask;
66 uint32_t resourceIndex = ~0u;
67 bool useRelativePaths = true;
68 };
69
70 struct ModelItem
71 {
72 enum struct State {
73 None,
74 Issued,
75 Loading,
76 Packing,
77 Writing,
78 Done,
79 Failed
80 } state = State::None;
81
82 ModelHandle handle;
83 std::vector<glm::mat4> meshTransforms;
84 std::vector<IdRanges> partIdRanges;
85 std::string sourcePath;
86 std::string targetPath;
87 TaskId task = NoTask;
88 uint32_t resourceIndex = ~0u;
89 };
90
91 struct Ctx
92 {
93 Ctx() {}
94 // Just to make sure we do not get any unintended copies while capturing lambdas etc.
95 Ctx(const Ctx&) = delete;
96 Ctx& operator=(const Ctx&) = delete;
97
98 Context* context = nullptr;
99 std::string destination;
100 TaskId processGroup = NoTask;
101
102 Cogs::Atomic<bool> failure = false;
103
104 struct {
105 Cogs::Mutex lock;
106 std::queue<std::unique_ptr<ModelItem>> queue;
107 size_t count = 0;
108 } modelQueue;
109
110 struct {
111 Cogs::Mutex lock;
112 std::queue<std::unique_ptr<AssetItem>> queue;
113 size_t count = 0;
114 } assetQueue;
115
116 struct {
117 Cogs::Mutex lock;
118 std::unordered_map<std::string, uint32_t> map;
119 std::vector<bool> processed;
120 std::vector<std::vector<uint32_t>> partSizes;
121 std::vector<std::vector<IdRanges>> resourcePartIdRanges;
122 } resources;
123
124
125 TaskId group = NoTask;
126
127 PackMeshCommand::Options packMeshOptions{};
128
129 bool prettyPrint = false;
130 bool compressAsFile = true; // Requires at least cogsengine 1576 from 2023-06-21
131 uint32_t trackIdRanges = 0; // Number of id ranges to track, zero to not track id ranges
132
133 uint32_t assetsProcessed = 0;
134 uint32_t modelsProcessed = 0;
135
136 };
137
138 void validateIdRanges(const Ctx* /*ctx*/, const IdRanges& ranges)
139 {
140 size_t n = ranges.size();
141
142 // Ranges should be well-formed
143 for (size_t i = 0; i < n; i++) {
144 assert(ranges[i].min <= ranges[i].max);
145 }
146
147 // Ranges should be separated by at least one item
148 for (size_t i = 0; i + 1 < n; i++) {
149 assert(ranges[i].max + 1 < ranges[i + 1].min);
150 }
151 }
152
153 struct Gap
154 {
155 uint32_t size;
156 uint32_t offset;
157 };
158
159 void restrictIdRangeCount(const Ctx* ctx, IdRanges& ranges)
160 {
161 size_t n = ranges.size();
162 if (n <= ctx->trackIdRanges) return;
163 assert(1 <= ctx->trackIdRanges);
164
165 validateIdRanges(ctx, ranges);
166
167 // The entire range covering both inputs, used to check result at function end.
168 IdRange overall{
169 .min = ranges.front().min,
170 .max = ranges.back().max
171 };
172
173 std::vector<Gap> gaps;
174 for (size_t i = 0; i + 1 < ranges.size(); i++) {
175 gaps.push_back({
176 .size = ranges[i + 1].min - ranges[i].max,
177 .offset = static_cast<uint32_t>(i)
178 });
179 }
180 std::sort(gaps.begin(), gaps.end(), [](const Gap& a, const Gap& b) { return a.size < b.size; });
181 assert(gaps.front().size <= gaps.back().size);
182
183 auto moo0 = ranges;
184
185 // Make ranges adjacent to a gap to be removed identical covering the union
186 size_t toRemove = n - ctx->trackIdRanges;
187 for (size_t i = 0; i < toRemove; i++) {
188 const Gap& gap = gaps[i];
189 ranges[gap.offset + 1].min = ranges[gap.offset].min;
190 ranges[gap.offset].max = ranges[gap.offset + 1].max;
191 }
192 assert(ranges.front().min == overall.min);
193 assert(ranges.back().max == overall.max);
194 auto moo1 = ranges;
195
196 // Remove overlaps
197 {
198 size_t d = 0; // The "current" range we write to. d will never be larger than i.
199 for (size_t i = 1; i < n; i++) {
200 if (ranges[d].max < ranges[i].min) { // No overlap
201 d = d + 1; // The current "current" range won't grow anymore, leave it
202 ranges[d] = ranges[i]; // Initialize new "current" range with i.
203 }
204 else {
205 ranges[d].max = ranges[i].max; // Overlap, just extend range at d
206 }
207 }
208 ranges.resize(d + 1); // Resize to include just the "current" range.
209 }
210
211 assert(ranges.size() == ctx->trackIdRanges);
212
213 validateIdRanges(ctx, ranges);
214
215 // Verify that the result cover at least the entire range of the inputs.
216 assert(ranges.front().min == overall.min);
217 assert(ranges.back().max == overall.max);
218 }
219
220 IdRanges mergeIdRanges(const Ctx* ctx, const IdRanges& a, const IdRanges& b)
221 {
222 // Validate sanity of inputs
223 validateIdRanges(ctx, a);
224 validateIdRanges(ctx, b);
225
226 if (a.empty()) {
227 return b;
228 }
229 else if (b.empty()) {
230 return a;
231 }
232 assert(!a.empty() && !b.empty());
233
234 // The entire range covering both inputs, used to check result at function end.
235 IdRange overall{
236 .min = std::min(a.front().min, b.front().min),
237 .max = std::max(a.back().max, b.back().max)
238 };
239
240 size_t na = a.size();
241 size_t nb = b.size();
242 size_t ia = 0;
243 size_t ib = 0;
244
245 IdRanges d;
246
247 // Merge the smallest angle
248 while (true) {
249
250 bool any_a = ia < na;
251 bool any_b = ib < nb;
252
253 // Choose the range with smallest min
254 IdRange t;
255 if (any_a && ((any_b && (a[ia].min <= b[ib].min)) || !any_b)) {
256 t = a[ia]; ia++;
257 }
258 else if(any_b) {
259 t = b[ib]; ib++;
260 }
261 else {
262 assert(!any_a && !any_b);
263 break;
264 }
265
266 // add or extend range
267 if (d.empty() || d.back().max + 1 < t.min) {
268 d.push_back(t); // new range
269 }
270 else {
271 d.back().max = std::max(d.back().max, t.max); // just extent current range
272 }
273 }
274
275 // Verify that the output is well-formed.
276 validateIdRanges(ctx, d);
277
278 // Verify that the result cover at least the entire range of the inputs.
279 assert(d.front().min == overall.min);
280 assert(d.back().max == overall.max);
281
282 return d;
283 }
284
285 void findIdRanges(Ctx* /*ctx*/, IdRanges& idRanges, std::vector<uint32_t>& ids)
286 {
287 // Find id ranges in model
288 std::sort(ids.begin(), ids.end());
289
290 if (!ids.empty()) {
291
292 idRanges.push_back(IdRange{ .min = ids.front(), .max = ids.front() });
293 for (uint32_t id : ids) {
294
295 // Grow current range by 0 or 1
296 if (id <= idRanges.back().max + 1) {
297 idRanges.back().max = id;
298 }
299 else {
300 // Create new range
301 assert(idRanges.back().max + 1 < id);
302 idRanges.push_back(IdRange{ .min = id, .max = id });
303 }
304 }
305 }
306 }
307
308 void markResourceAsDone(Ctx* ctx, std::vector<uint32_t>&& partSizes, std::vector<IdRanges>&& partIdRanges, uint32_t resourceIndex)
309 {
310 if (resourceIndex == ::NoValue) return;
311
312 Cogs::LockGuard guard(ctx->resources.lock);
313 assert(ctx->resources.processed[resourceIndex] == false);
314 assert(ctx->resources.map.size() == ctx->resources.processed.size());
315 assert(ctx->resources.map.size() == ctx->resources.partSizes.size());
316 assert(ctx->resources.map.size() == ctx->resources.resourcePartIdRanges.size());
317 ctx->resources.processed[resourceIndex] = true;
318 ctx->resources.partSizes[resourceIndex] = std::move(partSizes);
319 ctx->resources .resourcePartIdRanges[resourceIndex] = std::move(partIdRanges);
320 }
321
322 [[nodiscard]] bool enqueueModel(Ctx* ctx, const std::string& sourcePath, const std::string& targetPath, uint32_t resourceIndex)
323 {
324 if (ctx->failure) return false;
325
326 std::unique_ptr<ModelItem> modelItem = std::make_unique<ModelItem>(ModelItem{
327 .state = ModelItem::State::Issued,
328 .sourcePath = sourcePath,
329 .targetPath = targetPath,
330 .resourceIndex = resourceIndex
331 });
332
333 Cogs::LockGuard guard(ctx->modelQueue.lock);
334 ctx->modelQueue.queue.push(std::move(modelItem));
335 ctx->modelQueue.count++;
336 return true;
337 }
338
339 [[nodiscard]] bool enqueueAsset(Ctx* ctx, const std::string& sourcePath, const std::string& targetPath, uint32_t resourceIndex, bool useRelativePaths)
340 {
341 if (ctx->failure) return false;
342
343 std::unique_ptr<AssetItem> assetItem = std::make_unique<AssetItem>(AssetItem{
344 .state = AssetItem::State::Issued,
345 .sourcePath = sourcePath,
346 .targetPath = targetPath,
347 .resourceIndex = resourceIndex,
348 .useRelativePaths = useRelativePaths
349 });
350
351 //LOG_DEBUG(logger, "Enqued asset: %s -> %s", sourcePath.c_str(), targetPath.c_str());
352
353 Cogs::LockGuard guard(ctx->assetQueue.lock);
354 ctx->assetQueue.queue.push(std::move(assetItem));
355 ctx->assetQueue.count++;
356 return true;
357 }
358
359 [[nodiscard]] bool packModel(Ctx* ctx, ModelItem* item)
360 {
361 // Meshes might disappear due to quantization of vertex positions, as
362 // degenerate triangles are pruned. That is not an error, but an empty
363 // mesh handle is returned. In this case, the list of meshes is
364 // compacted by removing empty meshes, and the mesh indices are updated.
365
366 Model* model = item->handle.resolve();
367 const size_t initialMeshCount = model->meshes.size();
368
369 std::vector<MeshHandle> newMeshes;
370 newMeshes.reserve(initialMeshCount);
371
372 std::vector<uint32_t> newMeshIndices(initialMeshCount, NoIndex);
373
374 std::vector<IdRanges> meshIdRanges(initialMeshCount);
375
376 for (size_t i = 0; i < initialMeshCount; i++) {
377
378 MeshHandle mesh = std::move(model->meshes[i]);
379
380 if (ctx->trackIdRanges) {
381 std::vector<uint32_t> ids;
382 if (mesh->hasStream(VertexDataType::TexCoords0)) {
383 const DataStream& dataStream = mesh->getStream(VertexDataType::TexCoords0);
384 ids.reserve(dataStream.numElements);
385 MappedStreamReadOnly<glm::vec2> tex = mesh->mapReadOnly<glm::vec2>(VertexDataType::TexCoords0, VertexFormats::Tex2f, 0, dataStream.numElements);
386 for (size_t k = 0; k < dataStream.numElements; k++) {
387 uint32_t id = uint32_t(tex[k].x);
388 if (ids.empty() || ids.back() != id) {
389 ids.push_back(id);
390 }
391 }
392 IdRanges idRanges;
393 findIdRanges(ctx, idRanges, ids);
394 restrictIdRangeCount(ctx, idRanges);
395 meshIdRanges[i] = std::move(idRanges);
396 }
397 }
398
399 glm::mat4 transform(1.f);
400 std::string message;
401 if (!PackMeshCommand::pack(ctx->context, message, transform, mesh, ctx->packMeshOptions)) {
402 LOG_ERROR(logger, "PackMeshCommand failed: %s", message.c_str());
403 return false;
404 }
405 else if (!message.empty()) {
406 //LOG_DEBUG(logger, "%s", message.c_str());
407 }
408
409 // If mesh was empty, we skip it (and update part mesh index below)
410 if (HandleIsValid(mesh)) {
411 newMeshIndices[i] = static_cast<uint32_t>(newMeshes.size());
412 newMeshes.emplace_back(std::move(mesh));
413 item->meshTransforms.emplace_back(transform);
414 }
415 }
416 model->meshes = std::move(newMeshes);
417
418 // Update part mesh index with possibly compacted mesh indices
419 size_t partCount = model->parts.size();
420 item->partIdRanges.resize(partCount);
421 for (size_t i = 0; i < partCount; i++) {
422 ModelPart& part = model->parts[i];
423 if (part.meshIndex != NoIndex) {
424 assert(part.meshIndex < newMeshIndices.size());
425 item->partIdRanges[i] = std::move(meshIdRanges[part.meshIndex]);
426 part.meshIndex = newMeshIndices[part.meshIndex];
427 }
428 }
429
430 assert(item->meshTransforms.size() == model->meshes.size());
431 return true;
432 }
433
434
435 [[nodiscard]] bool processModel(Ctx* ctx, ModelItem* item)
436 {
437 Model* model = item->handle.resolve();
438 const size_t partCount = model->parts.size();
439
440
441 const std::vector<glm::mat4>& meshTransforms = item->meshTransforms;
442
443 std::vector<uint32_t> partSizes(partCount);
444 for (size_t i = 0; i < partCount; i++) {
445 ModelPart& part = model->parts[i];
446
447 // Mesh bytesize
448 if (part.meshIndex != NoIndex) {
449 assert(part.meshIndex < model->meshes.size());
450
451 const glm::mat4& tt = meshTransforms[part.meshIndex];
452 if (part.transformIndex != NoIndex) {
453 std::span<float> values = model->properties.getFloatArray(part.transformIndex);
454 assert(values.size() == 16);
455
456 glm::mat4 transform = glm::make_mat4(values.data()) * tt;
457 std::memcpy(values.data(), glm::value_ptr(transform), 16 * sizeof(float));
458 }
459 else {
460 part.transformIndex = model->properties.addProperty("t", std::span(glm::value_ptr(tt), 16));
461 }
462
463 const Mesh* mesh = model->meshes[part.meshIndex].resolve();
464
465 size_t vertexByteSize = 0;
466 const MeshStreamsLayout& layout = mesh->getStreamsLayout();
467 for (size_t k = 0; k < layout.numStreams; k++) {
468 vertexByteSize += Cogs::getSize(layout.vertexFormats[k]);
469 }
470
471 if (mesh->isIndexed()) {
472 uint32_t minIndex = ~0u;
473 uint32_t maxIndex = 0u;
474 const DataStream& indexStream = mesh->getStream(VertexDataType::Indexes);
475 if (indexStream.stride == sizeof(uint16_t)) {
476 const uint16_t* indices = reinterpret_cast<const uint16_t*>(indexStream.data());
477 for (size_t k = 0; k < indexStream.numElements; k++) {
478 minIndex = std::min(minIndex, uint32_t(indices[k]));
479 maxIndex = std::max(maxIndex, uint32_t(indices[k]));
480 }
481 }
482 else if (indexStream.stride == sizeof(uint32_t)) {
483 const uint32_t* indices = reinterpret_cast<const uint32_t*>(indexStream.data());
484 for (size_t k = 0; k < indexStream.numElements; k++) {
485 minIndex = std::min(minIndex, indices[k]);
486 maxIndex = std::max(maxIndex, indices[k]);
487 }
488 }
489 else return false;
490 if (minIndex < maxIndex) {
491 partSizes[i] = uint32_t(vertexByteSize * (maxIndex - minIndex) + indexStream.stride * indexStream.numElements);
492 }
493 }
494 else {
495 uint32_t startIndex = std::min(part.startIndex, mesh->getCount());
496 uint32_t vertexCount = std::min(part.vertexCount, mesh->getCount() - startIndex);
497 partSizes[i] = uint32_t(vertexByteSize * vertexCount);
498 }
499 }
500 }
501
502
503
504#if 0
505 if (!Cogs::IO::copyFile(item->sourcePath, item->targetPath)) return false;
506#else
507 uint32_t numVerticess = 0;
508 uint32_t numIndices = 0;
509 WriteModelSettings settings{
510 .flags = (ctx->compressAsFile ? WriteModelFlags::COMPRESS_ZSTD_AS_FILE : WriteModelFlags::COMPRESS_ZSTD) | WriteModelFlags::COMPRESS_MAX
511 };
512 if (!writeModel(ctx->context, numVerticess, numIndices, item->targetPath, model, &settings)) return false;
513 //LOG_DEBUG(logger, "Wrote %s -> %s", item->sourcePath.c_str(), item->targetPath.c_str());
514#endif
515
516 markResourceAsDone(ctx, std::move(partSizes), std::move(item->partIdRanges), item->resourceIndex);
517
518 return true;
519 }
520
521 [[nodiscard]] bool getSourcePath(std::string& sourcePath, const std::string& sourceDirectoryPath, const AssetDefinition& definition, uint32_t sourcePropertyIndex, AssetResourceType resourceKind, bool relativePath)
522 {
523 if (sourcePropertyIndex == ::NoValue) {
524 LOG_ERROR(logger, "Source property is neither uint nor string");
525 return false;
526 }
527
528 const PropertyInfo& pathItem = definition.scene.properties.getPropertyByIndex(sourcePropertyIndex);
529 if (pathItem.type == PropertyType::UnsignedInteger) {
530 sourcePath = createAssetResourcePathFromIndex(pathItem.uintValue, resourceKind);
531 }
532 else if (pathItem.type == PropertyType::String) {
533 sourcePath = definition.scene.properties.getString(pathItem).to_string();
534 }
535 else {
536 LOG_ERROR(logger, "Source property is neither uint nor string");
537 return false;
538 }
539
540 if (relativePath && Cogs::IO::isRelative(sourcePath)) {
541 sourcePath = Cogs::IO::combine(sourceDirectoryPath, sourcePath);
542 }
543
544 return true;
545 }
546
547 auto checkResourceIndex(Ctx* ctx, const std::string& sourcePath)
548 {
549 uint32_t resourceIndex = ~0u;
550 bool isProcessed = false;
551 bool doEnqueue = false;
552
553 Cogs::LockGuard guard(ctx->resources.lock);
554 if (auto it = ctx->resources.map.find(sourcePath); it != ctx->resources.map.end()) {
555 resourceIndex = it->second;
556 isProcessed = ctx->resources.processed[resourceIndex];
557 }
558 else {
559 resourceIndex = uint32_t(ctx->resources.map.size());
560 ctx->resources.map[sourcePath] = resourceIndex;
561 ctx->resources.processed.emplace_back(false);
562 ctx->resources.partSizes.emplace_back();
563 ctx->resources.resourcePartIdRanges.emplace_back();
564 assert(ctx->resources.map.size() == ctx->resources.processed.size());
565 assert(ctx->resources.map.size() == ctx->resources.partSizes.size());
566 assert(ctx->resources.map.size() == ctx->resources.resourcePartIdRanges.size());
567 doEnqueue = true;
568 }
569 return std::make_tuple(resourceIndex, isProcessed, doEnqueue);
570 }
571
572
573 [[nodiscard]] bool issueAssetDependencies(Ctx* ctx, AssetItem* item)
574 {
575 assert(HandleIsValid(item->handle) && item->handle->isLoaded());
576 Asset* asset = item->handle.resolve();
577 AssetDefinition& definition = asset->definition;
578
579 const Cogs::Reflection::Type& assetCompType = Cogs::Reflection::TypeDatabase::getType<AssetComponent>();
580 const Cogs::Reflection::TypeId assetCompTypeId = assetCompType.getTypeId();
581
582 const Cogs::Reflection::Field* assetCompAssetFieldType = assetCompType.getField("asset");
583 const Cogs::Reflection::FieldId assetCompAssetFieldId = assetCompAssetFieldType ? assetCompType.getFieldId(assetCompAssetFieldType) : Cogs::Reflection::NoField;
584
585 // Keep track of which deps we already have issued
586 std::unordered_set<uint32_t> depSet;
587
588 std::string sourceDirectoryPath = Cogs::IO::parentPath(item->sourcePath);
589 for (size_t i = 0; i < definition.scene.entities.size(); ++i) {
590 SceneEntityDefinition& entityDef = definition.scene.entities[i];
591
592 if (entityDef.isModel()) {
593
594 std::string sourcePath;
595 if (!getSourcePath(sourcePath, sourceDirectoryPath, definition, entityDef.model.index, AssetResourceType::Model, item->useRelativePaths)) return false;
596
597 auto [resourceIndex, isProcessed, doEnqueue] = checkResourceIndex(ctx, sourcePath);
598
599 PropertyInfo& pathItem = definition.scene.properties.getPropertyByIndex(entityDef.model.index);
600 pathItem.type = PropertyType::UnsignedInteger;
601 pathItem.uintValue = resourceIndex;
602
603 // Avoid creating duplicate dependencies when a model is referenced multiple times
604 if(!depSet.contains(resourceIndex)) {
605 depSet.insert(resourceIndex);
606
607 if (!isProcessed) {
608 item->resourceDependencies.push_back(resourceIndex);
609 }
610
611 std::string targetPath = Cogs::IO::combine(ctx->destination, createAssetResourcePathFromIndex(resourceIndex, AssetResourceType::Model));
612
613 if (doEnqueue && !enqueueModel(ctx, sourcePath, targetPath, resourceIndex)) return false;
614 }
615
616 }
617 else if (entityDef.isAsset()) {
618
619 std::string sourcePath;
620 if (!getSourcePath(sourcePath, sourceDirectoryPath, definition, entityDef.model.index, AssetResourceType::Asset, item->useRelativePaths)) return false;
621
622 auto [resourceIndex, isProcessed, doEnqueue] = checkResourceIndex(ctx, sourcePath);
623
624 PropertyInfo& pathItem = definition.scene.properties.getPropertyByIndex(entityDef.model.index);
625 pathItem.type = PropertyType::UnsignedInteger;
626 pathItem.uintValue = resourceIndex;
627
628 if (!depSet.contains(resourceIndex)) {
629 depSet.insert(resourceIndex);
630
631 if (!isProcessed) {
632 item->resourceDependencies.push_back(resourceIndex);
633 }
634
635 std::string targetPath = Cogs::IO::combine(ctx->destination, createAssetResourcePathFromIndex(resourceIndex, AssetResourceType::Asset));
636
637 bool useRelative = entityDef.asset.flags & uint32_t(AssetFlags::RelativePaths);
638 entityDef.asset.flags |= uint32_t(AssetFlags::RelativePaths);
639
640 if (doEnqueue && !enqueueAsset(ctx, sourcePath, targetPath, resourceIndex, useRelative)) return false;
641 }
642 }
643 else {
644
645 if (entityDef.numFields) {
646 for (FieldValue& entry : std::span(definition.scene.fieldValues).subspan(entityDef.firstField, entityDef.numFields)) {
647
648 if (entry.componentId == assetCompTypeId && entry.fieldId == assetCompAssetFieldId && entry.type == DefaultValueType::Asset) {
649
650 std::string sourcePath = entry.value;
651 if(item->useRelativePaths && Cogs::IO::isRelative(sourcePath)) {
652 sourcePath = Cogs::IO::combine(sourceDirectoryPath, sourcePath);
653 }
654 if (!Cogs::IO::exists(sourcePath)) {
655 LOG_ERROR(logger, "Path %s does not exist", sourcePath.c_str());
656 return false;
657 }
658
659 auto [resourceIndex, isProcessed, doEnqueue] = checkResourceIndex(ctx, sourcePath);
660 entry.value = Cogs::IO::combine(ctx->destination, createAssetResourcePathFromIndex(resourceIndex, AssetResourceType::Asset));;
661
662 if (!depSet.contains(resourceIndex)) {
663 depSet.insert(resourceIndex);
664
665 if (!isProcessed) {
666 item->resourceDependencies.push_back(resourceIndex);
667 }
668
669 if (doEnqueue && !enqueueAsset(ctx, sourcePath, entry.value, resourceIndex, item->useRelativePaths)) return false;
670 }
671
672 }
673 }
674 }
675
676 }
677 }
678
679 return true;
680 }
681
682 [[nodiscard]] bool processAsset(Ctx* ctx, AssetItem* item)
683 {
684
685 Asset* asset = item->handle.resolve();
686 AssetDefinition& definition = asset->definition;
687
688 PropertyStore newProperties;
689
690 // Propagate id ranges from individual model parts upwards in the hierarchy.
691 std::vector<IdRanges> assetIdRanges(1); // Id ranges for this asset
692 std::vector<IdRanges> entityIdRanges(definition.scene.entities.size()); // Id ranges for each entity in this asset
693 if(ctx->trackIdRanges) {
694 for (size_t i = 0; i < definition.scene.entities.size(); i++) {
695
696 IdRanges itemIdRanges;
697
698 SceneEntityDefinition& entityDef = definition.scene.entities[i];
699 if (entityDef.isModel()) {
700 const PropertyInfo& pathItem = definition.scene.properties.getPropertyByIndex(entityDef.model.index);
701 assert(pathItem.type == PropertyType::UnsignedInteger);
702
703 size_t part = 0;
704 if (entityDef.model.part != NoValue) {
705 part = entityDef.model.part;
706 }
707
708 // Get ranges for model part
709 if (ctx->trackIdRanges) {
710 Cogs::LockGuard guard(ctx->resources.lock);
711 assert(pathItem.uintValue < ctx->resources.resourcePartIdRanges.size());
712 assert(part < ctx->resources.resourcePartIdRanges[pathItem.uintValue].size());
713 itemIdRanges = ctx->resources.resourcePartIdRanges[pathItem.uintValue][part];
714 assert(!itemIdRanges.empty());
715 }
716 }
717
718 // Add ranges to this asset
719 assetIdRanges[0] = mergeIdRanges(ctx, assetIdRanges[0], itemIdRanges);
720 restrictIdRangeCount(ctx, assetIdRanges[0]);
721
722
723 // Propagate ranges to parents
724 if (!itemIdRanges.empty()) {
725 uint32_t parentIndex = static_cast<uint32_t>(i);
726 do {
727 entityIdRanges[parentIndex] = mergeIdRanges(ctx, entityIdRanges[parentIndex], itemIdRanges);
728 restrictIdRangeCount(ctx, entityIdRanges[parentIndex]);
729 parentIndex = definition.scene.entities[parentIndex].parentIndex;
730 } while (parentIndex != SceneEntityDefinition::NoIndex);
731 }
732 }
733 }
734
735 for (size_t i = 0; i < definition.scene.entities.size(); ++i) {
736
737 uint32_t firstProperty = newProperties.size();
738
739 SceneEntityDefinition& entityDef = definition.scene.entities[i];
740 entityDef.nameIndex = ::NoValue;
741
742 const PropertyRange propertyRange = definition.scene.getProperties(entityDef);
743
744 if (entityDef.isModel()) {
745 assert(entityDef.model.index != ::NoValue); // Should be caught in issueAssetDepencies
746
747
748 const PropertyInfo& pathItem = definition.scene.properties.getPropertyByIndex(entityDef.model.index);
749 assert(pathItem.type == PropertyType::UnsignedInteger);
750
751 entityDef.model.index = newProperties.addProperty(definition.scene.properties, entityDef.model.index);
752
753 IdRanges itemIdRanges;
754 std::vector<uint32_t> partSizes;
755 {
756 Cogs::LockGuard guard(ctx->resources.lock);
757 assert(pathItem.uintValue < ctx->resources.partSizes.size());
758 assert(ctx->resources.processed[pathItem.uintValue]);
759 partSizes = ctx->resources.partSizes[pathItem.uintValue];
760 }
761
762 if (entityDef.model.part != NoValue) {
763 if (partSizes.size() <= entityDef.model.part) {
764 LOG_ERROR(logger, "Illegal part index %u, model has %zu parts", entityDef.model.part, partSizes.size());
765 return false;
766 }
767 }
768
769 // Forward bounding box if the model has this property.
770 if (uint32_t ix = propertyRange.getPropertyIndex(bboxString); ix != PropertyStore::NoProperty) {
771 newProperties.addProperty(definition.scene.properties, ix);
772 }
773 }
774
775 else if (entityDef.isAsset()) {
776
777 entityDef.asset.index = newProperties.addProperty(definition.scene.properties, entityDef.asset.index);
778
779 }
780
781 else if (entityDef.isLodGroup()) {
782
783 if (ctx->trackIdRanges && !entityIdRanges[i].empty()) {
784 size_t n = entityIdRanges[i].size();
785 std::vector<uint32_t> ids(2 * n);
786 for (size_t k = 0; k < n; k++) {
787 ids[2 * k + 0] = entityIdRanges[i][k].min;
788 ids[2 * k + 1] = entityIdRanges[i][k].max;
789 }
790 newProperties.addProperty("ids", ids);
791 }
792 newProperties.addProperty(definition.scene.properties, propertyRange.getPropertyIndex(bboxString));
793 newProperties.addProperty(definition.scene.properties, propertyRange.getPropertyIndex(errorsString));
794 }
795
796 // Update range
797 entityDef.firstProperty = firstProperty;
798 entityDef.numProperties = newProperties.size()-firstProperty;
799 }
800
801 // Replace properties
802 definition.scene.properties = std::move(newProperties);
803
804 if (!writeAsset(ctx->context,
805 item->targetPath.c_str(),
806 item->handle,
807 ctx->prettyPrint ? AssetWriteFlags::PrettyPrint : (AssetWriteFlags::Compress | AssetWriteFlags::Strip)))
808 {
809 LOG_ERROR(logger, "Failed to write %s -> %s", item->sourcePath.c_str(), item->targetPath.c_str());
810 return false;
811 }
812
813 std::vector<uint32_t> partSizes(1); // FIXME: Fill
814 markResourceAsDone(ctx, std::move(partSizes), std::move(assetIdRanges), item->resourceIndex);
815
816 return true;
817 }
818
819 void manageActiveModels(Ctx* ctx,
820 std::vector<std::unique_ptr<ModelItem>>& loadingModels,
821 std::vector<std::unique_ptr<ModelItem>>& loadingModelsNext)
822 {
823 for (std::unique_ptr<ModelItem>& item : loadingModels) {
824
825 bool keep = true;
826 switch (item->state) {
827
828 case ModelItem::State::Loading:
829 if (item->handle->isLoaded()) { // Wait for model is loaded
830
831 // Wait for subresources to be done
832 bool done = true;
833 const Model* model = item->handle.resolve();
834
835 for (auto& mesh : model->meshes) {
836 if (!mesh->isLoaded()) {
837 done = false;
838 if (mesh->hasFailedLoad()) {
839 ctx->failure = true;
840 keep = false;
841 }
842 }
843 }
844
845 for (auto& material : model->materials) {
846 if (!material->isLoaded()) {
847 done = false;
848 if (material->hasFailedLoad()) {
849 ctx->failure = true;
850 keep = false;
851 }
852 }
853 }
854
855 if(done) {
856 item->task = ctx->context->taskManager->enqueueChild(ctx->group,
857 [ctx, item_=item.get()]()
858 {
859 if (!packModel(ctx, item_)) ctx->failure = true;
860 });
861 if (item->task.isValid()) {
862 item->state = ModelItem::State::Packing;
863 }
864 else {
865 ctx->failure = true;
866 keep = false;
867 }
868 }
869 }
870 else if (item->handle->hasFailedLoad()) {
871 ctx->failure = true;
872 keep = false;
873 }
874 break;
875
876 case ModelItem::State::Packing:
877 if (!ctx->context->taskManager->isActive(item->task)) {
878
879 bool done = true;
880 const Model* model = item->handle.resolve();
881 for (auto& mesh : model->meshes) {
882 if (!mesh->isLoaded()) {
883 done = false;
884 if (mesh->hasFailedLoad()) {
885 ctx->failure = true;
886 keep = false;
887 }
888 }
889 }
890
891 if (done) {
892 ctx->context->taskManager->wait(item->task);
893
894 item->task = ctx->context->taskManager->enqueueChild(ctx->group,
895 [ctx, item_ = item.get()]()
896 {
897 if (!processModel(ctx, item_)) ctx->failure = true;
898 });
899 if (item->task.isValid()) {
900 item->state = ModelItem::State::Writing;
901 }
902 else {
903 ctx->failure = true;
904 keep = false;
905 }
906 }
907 }
908 break;
909
910
911 case ModelItem::State::Writing:
912 if (!ctx->context->taskManager->isActive(item->task)) {
913 ctx->context->taskManager->wait(item->task);
914 item->task = NoTask;
915 item->state = ModelItem::State::Done;
916 keep = false;
917 }
918 break;
919
920 default:
921 assert(false && "Unexpected state");
922 keep = false;
923 break;
924 }
925
926 if (keep) loadingModelsNext.emplace_back(std::move(item));
927 else ctx->modelsProcessed++;
928 }
929 }
930
931 void addNewActiveModels(Ctx* ctx,
932 std::vector<std::unique_ptr<ModelItem>>& loadingModelsNext,
933 size_t maxCount)
934 {
935 while (loadingModelsNext.size() < maxCount) {
936 std::unique_ptr<ModelItem> item;
937 {
938 Cogs::LockGuard guard(ctx->modelQueue.lock);
939 if (ctx->modelQueue.queue.empty()) break;
940 item = std::move(ctx->modelQueue.queue.front());
941 ctx->modelQueue.queue.pop();
942 ctx->modelQueue.count--;
943 }
944
945 item->state = ModelItem::State::Loading;
946 item->handle = ctx->context->modelManager->loadModel(item->sourcePath, NoResourceId, ModelLoadFlags::None);
947
948 if (!Cogs::IO::exists(item->targetPath)) {
949 Cogs::IO::createDirectories(item->targetPath);
950 }
951 loadingModelsNext.emplace_back(std::move(item));
952 }
953 }
954
955 void manageActiveAssets(Ctx* ctx,
956 std::vector<std::unique_ptr<AssetItem>>& loadingAssets,
957 std::vector<std::unique_ptr<AssetItem>>& loadingAssetsNext)
958 {
959 for (std::unique_ptr<AssetItem>& item : loadingAssets) {
960
961 bool keep = true;
962 switch (item->state) {
963
964 case AssetItem::State::Loading:
965 if (item->handle->isLoaded()) {
966 item->task = ctx->context->taskManager->enqueueChild(ctx->group,
967 [ctx, item_=item.get()]()
968 {
969 if (!issueAssetDependencies(ctx, item_)) ctx->failure = true;
970 });
971 if (item->task.isValid()) {
972 item->state = AssetItem::State::IssueDependencies;
973 }
974 else {
975 ctx->failure = true;
976 keep = false;
977 }
978 }
979 else if (item->handle->hasFailedLoad()) {
980 ctx->failure = true;
981 keep = false;
982 }
983 break;
984
985 case AssetItem::State::IssueDependencies:
986 if (!ctx->context->taskManager->isActive(item->task)) {
987 ctx->context->taskManager->wait(item->task);
988 item->task = NoTask;
989 item->state = AssetItem::State::WaitingForDeps;
990 }
991 break;
992
993 case AssetItem::State::WaitingForDeps:
994
995 if (!item->resourceDependencies.empty()) {
996
997 bool depchange = false;
998 Cogs::LockGuard guard(ctx->resources.lock);
999 for (size_t i = 0; i < item->resourceDependencies.size();) {
1000 if (ctx->resources.processed[item->resourceDependencies[i]]) {
1001 std::swap(item->resourceDependencies[i], item->resourceDependencies.back());
1002 item->resourceDependencies.pop_back();
1003 depchange = true;
1004 }
1005 else {
1006 i++;
1007 }
1008 }
1009
1010 if (depchange) {
1011 //LOG_DEBUG(logger, "%s: waiting for %zu deps", item->sourcePath.c_str(), item->resourceDependencies.size());
1012 }
1013 }
1014
1015 if (item->resourceDependencies.empty()) {
1016 item->task = ctx->context->taskManager->enqueueChild(ctx->group,
1017 [ctx, item_=item.get()]()
1018 {
1019 if (!processAsset(ctx, item_)) ctx->failure = true;
1020 });
1021 if (item->task.isValid()) {
1022 item->state = AssetItem::State::Processing;
1023 }
1024 else {
1025 ctx->failure = true;
1026 keep = false;
1027 }
1028 }
1029 break;
1030
1031 case AssetItem::State::Processing:
1032 if (!ctx->context->taskManager->isActive(item->task)) {
1033 item->state = AssetItem::State::Done;
1034 keep = false;
1035 }
1036 break;
1037
1038 default:
1039 assert(false && "Unexpected state");
1040 keep = false;
1041 break;
1042 }
1043
1044 if (keep) loadingAssetsNext.emplace_back(std::move(item));
1045 else ctx->assetsProcessed++;
1046 }
1047 }
1048
1049 void addNewActiveAssets(Ctx* ctx, std::vector<std::unique_ptr<AssetItem>>& loadingAssetsNext)
1050 {
1051 while (true) {
1052
1053 std::unique_ptr<AssetItem> item;
1054 {
1055 Cogs::LockGuard guard(ctx->assetQueue.lock);
1056 if (ctx->assetQueue.queue.empty()) break;
1057 item = std::move(ctx->assetQueue.queue.front());
1058 ctx->assetQueue.queue.pop();
1059 ctx->assetQueue.count--;
1060 }
1061
1062 item->state = AssetItem::State::Loading;
1063 item->handle = ctx->context->assetManager->loadAsset(item->sourcePath, NoResourceId, AssetLoadFlags::ForceUnique);
1064 if (!Cogs::IO::exists(item->targetPath)) {
1065 Cogs::IO::createDirectories(item->targetPath);
1066 }
1067 loadingAssetsNext.emplace_back(std::move(item));
1068 }
1069 }
1070
1071}
1072
1073
1075{
1076 LOG_ERROR(logger, "Use in post");
1077}
1078
1079void Cogs::Core::AssetPipeCommand::undo()
1080{
1081 LOG_ERROR(logger, "Use in post");
1082}
1083
1084void Cogs::Core::AssetPipeCommand::applyPost()
1085{
1086 Ctx ctx;
1087 ctx.context = context;
1088 ctx.group = context->taskManager->createGroup(TaskManager::ResourceQueue);
1089 ctx.destination = IO::absolute(properties.getProperty("destination", StringView()).to_string());
1090 ctx.packMeshOptions.allowIdOffset = properties.getProperty("allowIdOffset", ctx.packMeshOptions.allowIdOffset);
1091 ctx.packMeshOptions.allowSeparateIdStream = properties.getProperty("allowSeparateIdStream", ctx.packMeshOptions.allowSeparateIdStream);
1092 ctx.packMeshOptions.optimizeTriangleOrder = properties.getProperty("optimizeTriangleOrder", ctx.packMeshOptions.optimizeTriangleOrder);
1093 ctx.packMeshOptions.optimizeVertexOrder = properties.getProperty("optimizeVertexOrder", ctx.packMeshOptions.optimizeVertexOrder);
1094 ctx.prettyPrint = properties.getProperty("prettyPrint", ctx.prettyPrint);
1095 ctx.compressAsFile = properties.getProperty("compressAsFile", ctx.compressAsFile);
1096 ctx.trackIdRanges = properties.getProperty("trackIdRanges", ctx.trackIdRanges);
1097
1098 LOG_DEBUG(logger, "destination=%s", ctx.destination.c_str());
1099 LOG_DEBUG(logger, "packMeshOptions: allowIdOffset=%d allowSeparateIdStream=%d optimizeTriangleOrder=%d optimizeVertexOrder=%d",
1100 ctx.packMeshOptions.allowIdOffset ? 1 : 0,
1101 ctx.packMeshOptions.allowSeparateIdStream ? 1 : 0,
1102 ctx.packMeshOptions.optimizeTriangleOrder ? 1 : 0,
1103 ctx.packMeshOptions.optimizeVertexOrder ? 1 : 0);
1104 LOG_DEBUG(logger, "prettyPrint=%d compressAsFile=%d trackIdRanges=%u", ctx.prettyPrint ? 1 : 0, ctx.compressAsFile ? 1 : 0, ctx.trackIdRanges);
1105
1106 if (const PropertyInfo propInfo = properties.getProperty("target"); propInfo.type != PropertyType::Unknown) {
1107
1108 const StringView target = properties.getString(propInfo);
1109 switch (target.hashLowercase()) {
1110 case Cogs::hash("webgl1"):
1111 case Cogs::hash("webgl1-lo"):
1112 case Cogs::hash("webgl1-low"):
1113 ctx.packMeshOptions.target = PackMeshCommand::Target::WebGL1_Low;
1114 LOG_DEBUG(logger, "PackMesh target WebGL1_Low");
1115 break;
1116
1117 case Cogs::hash("webgl2-lo"):
1118 case Cogs::hash("webgl2-low"):
1119 ctx.packMeshOptions.target = PackMeshCommand::Target::WebGL2_Low;
1120 LOG_DEBUG(logger, "PackMesh target WebGL2_Low");
1121 break;
1122
1123 case Cogs::hash("webgl2"):
1124 case Cogs::hash("webgl2-med"):
1125 case Cogs::hash("webgl2-medium"):
1126 ctx.packMeshOptions.target = PackMeshCommand::Target::WebGL2_Med;
1127 LOG_DEBUG(logger, "PackMesh target WebGL2_Med");
1128 break;
1129
1130 default:
1131 LOG_ERROR(logger, "Unrecognized target value '%.*s'", StringViewFormat(target));
1132 break;
1133 }
1134 }
1135
1136
1137 const std::string source = IO::absolute(properties.getProperty("source", StringView()).to_string());
1138 if (!IO::isFile(source)) {
1139 LOG_ERROR(logger, "'source' (=\"%s\") must point to the root asset file.", source.c_str());
1140 return;
1141 }
1142
1143 // Build output name of root asset
1144 std::string outfile = IO::fileName(source);
1145 const char* stripEndings[] = { ".asset", ".zst" };
1146 for (const char* ending : stripEndings) {
1147 if (size_t o = outfile.find(ending); o != std::string::npos) {
1148 outfile = outfile.substr(0, o);
1149 }
1150 }
1151
1152 // Issue root
1153 (void)enqueueAsset(&ctx, source, IO::combine(ctx.destination, outfile + ".asset"), ~0u, true);
1154
1155 size_t maxConcurrentModelLoads = (2 * Cogs::Thread::hardware_concurrency() + 1);
1156
1157 bool done = false;
1158 double lastReport = -1000.0;
1159 Timer timer = Timer::startNew();
1160 std::vector<std::unique_ptr<ModelItem>> loadingModels, loadingModelsNext;
1161 std::vector<std::unique_ptr<AssetItem>> loadingAssets, loadingAssetsNext;
1162 do {
1163 runFrames(context, 1, true, false);
1164
1165 loadingModelsNext.clear();
1166 manageActiveModels(&ctx, loadingModels, loadingModelsNext);
1167 addNewActiveModels(&ctx, loadingModelsNext, maxConcurrentModelLoads);
1168 loadingModels.swap(loadingModelsNext);
1169
1170 loadingAssetsNext.clear();
1171 manageActiveAssets(&ctx, loadingAssets, loadingAssetsNext);
1172 addNewActiveAssets(&ctx, loadingAssetsNext);
1173 loadingAssets.swap(loadingAssetsNext);
1174
1175 done = loadingModels.empty() && loadingAssets.empty();
1176 double elapsed = timer.elapsedSeconds();
1177 if (done || std::floor(lastReport) != std::floor(elapsed)) {
1178 lastReport = elapsed;
1179
1180 size_t assetQueueCount = 0;
1181 {
1182 Cogs::LockGuard guard(ctx.assetQueue.lock);
1183 assetQueueCount = ctx.assetQueue.count;
1184 }
1185 size_t modelQueueCount = 0;
1186 {
1187 Cogs::LockGuard guard(ctx.modelQueue.lock);
1188 modelQueueCount = ctx.modelQueue.count;
1189 }
1190
1191 LOG_DEBUG(logger, "%.0fs: assets: done=%u act=%zu queue=%zu, models: done=%u act=%zu queue=%zu",
1192 std::floor(elapsed),
1193 ctx.assetsProcessed, loadingAssets.size(), assetQueueCount,
1194 ctx.modelsProcessed, loadingModels.size(), modelQueueCount);
1195 }
1196 }
1197 while (!done);
1198
1199 context->taskManager->wait(ctx.group);
1200 LOG_DEBUG(logger, "Done, failure=%s", ctx.failure ? "true" : "false");
1201}
A Context instance contains all the services, systems and runtime components needed to use Cogs.
Definition: Context.h:83
static constexpr TaskQueueId ResourceQueue
Resource task queue.
Definition: TaskManager.h:232
Log implementation class.
Definition: LogManager.h:139
Field definition describing a single data member of a data structure.
Definition: Field.h:68
Represents a discrete type definition, describing a native type class.
Definition: Type.h:89
FieldId getFieldId(const Field *field) const
Get the Reflection::FieldId of the given field.
Definition: Type.cpp:65
const Field * getField(const Name &name) const
Get a pointer to the field info of the field with the given name.
Definition: Type.cpp:53
constexpr TypeId getTypeId() const
Get the unique Reflection::TypeId of this instance.
Definition: Type.h:325
Provides a weakly referenced view over the contents of a string.
Definition: StringView.h:24
size_t hashLowercase(size_t hashValue=Cogs::hash()) const noexcept
Get the hash code of the string converted to lowercase.
Definition: StringView.cpp:13
std::string to_string() const
String conversion method.
Definition: StringView.cpp:9
Old timer class.
Definition: Timer.h:37
Contains the Engine, Renderer, resource managers and other systems needed to run Cogs....
bool HandleIsValid(const ResourceHandle_t< T > &handle)
Check if the given resource is valid, that is not equal to NoHandle or InvalidHandle.
AssetResourceType
Utility function to format a resource index as a filename.
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:180
uint16_t TypeId
Built in type used to uniquely identify a single type instance.
Definition: Name.h:48
uint16_t FieldId
Type used to index fields.
Definition: Name.h:54
constexpr FieldId NoField
No field id.
Definition: Name.h:60
Contains all Cogs related functionality.
Definition: FieldSetter.h:23
constexpr size_t hash() noexcept
Simple getter function that returns the initial value for fnv1a hashing.
Definition: HashFunctions.h:62
void apply() override
Run the command.
Contains a stream of data used by Mesh resources.
Definition: Mesh.h:80
uint32_t numElements
Number of elements of the type given by format contained in data.
Definition: Mesh.h:108
uint32_t stride
Element stride.
Definition: Mesh.h:105
Defines a value to apply to a field.
Wrapper for read-only access to mapped stream data.
Definition: Mesh.h:197
VertexFormatHandle vertexFormats[maxStreams]
Meshes contain streams of vertex data in addition to index data and options defining geometry used fo...
Definition: Mesh.h:265
MappedStreamReadOnly< Element > mapReadOnly(const VertexDataType::EVertexDataType type, VertexFormatHandle format, const size_t start, const size_t count)
Maps the data stream corresponding to the given type for read-only access.
Definition: Mesh.h:857
bool isIndexed() const
If the mesh uses indexed geometry.
Definition: Mesh.h:953
bool hasStream(VertexDataType::EVertexDataType type) const
Check if the Mesh has a DataStream for the given type.
Definition: Mesh.h:933
DataStream & getStream(const VertexDataType::EVertexDataType dataType)
Get the stream corresponding to the given dataType.
Definition: Mesh.cpp:85
uint32_t getCount() const
Get the vertex count of the mesh.
Definition: Mesh.h:1012
Model resources define a template for a set of connected entities, with resources such as meshes,...
Definition: Model.h:56
static constexpr uint32_t NoProperty
Return from findProperty if key not found.
struct Cogs::Core::SceneEntityDefinition::@28::@31 model
SceneEntityFlags::Model is set.
uint32_t flags
Really enum of SceneEntityFlags.
struct Cogs::Core::SceneEntityDefinition::@28::@30 asset
SceneEntityFlags::Asset is set.
Task id struct used to identify unique Task instances.
Definition: TaskManager.h:20