Cogs.Core
ResourceStore.cpp
1#include "ResourceStore.h"
2#include "ResourceStoreStorage.h"
3
4#include "DataFetcherManager.h"
5#include "Engine.h"
6#include "Context.h"
7#include "MemoryContext.h"
8#include "Services/Variables.h"
9
10#include "Rendering/IEffects.h"
11
12#include "Foundation/Logging/Logger.h"
13#include "Foundation/Platform/FileContents.h"
14#include "Foundation/Platform/IO.h"
15#include "Foundation/StringView.h"
16
17#include <functional>
18
19namespace
20{
21 using namespace Cogs::Core;
22
23 Cogs::Logging::Log logger = Cogs::Logging::getLogger("ResourceStore");
24
25 ResourceBuffer emptyBytes;
26
27 struct ResourceLookup
28 {
29 constexpr operator bool() const
30 {
31 return found;
32 }
33 ResourceBuffer buffer;
34 bool found = false;
35 };
36
37 struct PathAndProtocol
38 {
39 const std::string_view path;
40 ResourceProtocol protocol;
41 };
42
43 PathAndProtocol getPathAndProtocol(std::string_view resourceName)
44 {
45 auto protocol = ResourceProtocol::None;
46
47 if (resourceName.substr(0, 10) == "archive://") protocol = ResourceProtocol::Archive;
48 else if (resourceName.substr(0, 7) == "file://") protocol = ResourceProtocol::File;
49 else if (resourceName.substr(0, 7) == "http://") protocol = ResourceProtocol::Http;
50 else {
51 auto offset = resourceName.find("://");
52 if (offset != std::string_view::npos) {
53 return { resourceName.substr(offset + 3), ResourceProtocol::Other };
54 }
55 }
56
57 if (protocol == ResourceProtocol::File) return { resourceName.substr(7), protocol };
58 else if (protocol == ResourceProtocol::Archive) return { resourceName.substr(10), protocol };
59 else return { resourceName, protocol };
60 }
61
62 ResourceLookup getCacheBytes(ResourceStoreStorage* data, const std::string& resourceName)
63 {
64 Cogs::LockGuard lock(data->cache->lock);
65 if (auto resourceIt = data->cache->resources.find(resourceName); resourceIt != data->cache->resources.end()) {
66 return ResourceLookup{ resourceIt->second.bytes , true };
67 }
68
69 return ResourceLookup{};
70 }
71
72#ifndef EMSCRIPTEN
73 ResourceLookup getFileBytes(ResourceStoreStorage* data, const std::string& resourceName, bool useCache)
74 {
75 auto contents = DataFetcherManager::fetchSync(resourceName);
76
77 if (contents) {
78 if (!useCache) {
79 return ResourceLookup{ {std::make_shared<Cogs::Memory::MemoryBuffer>(contents->take())}, true };
80 }
81 else {
82 Cogs::LockGuard lock(data->cache->lock);
83
84 Resource& cached = data->cache->resources[resourceName] = Resource{ resourceName, resourceName, {std::make_shared<Cogs::Memory::MemoryBuffer>(contents->take())} };
85
86 return ResourceLookup{ cached.bytes, true };
87 }
88 }
89
90 return ResourceLookup{};
91 }
92#endif
93
94 ResourceLookup getArchiveBytes(ResourceStoreStorage* data, const std::string& resourceName)
95 {
96 for (auto& archive : data->archives) {
97 if (archive.hasResource(resourceName)) {
98 Cogs::Memory::MemoryBuffer contents = archive.getResource(resourceName);
99
100 {
101 Cogs::LockGuard lock(data->cache->lock);
102
103 Resource& cached = data->cache->resources[resourceName] = Resource{ resourceName, resourceName, {std::make_shared<Cogs::Memory::MemoryBuffer>(std::move(contents))} };
104
105 return ResourceLookup{ cached.bytes, true };
106 }
107 }
108 }
109
110 return ResourceLookup{};
111 }
112
113 void loadFromArchive(ResourceStoreStorage* data, const std::string& resourceName, const std::function<void(Cogs::Core::ResourceBuffer data)>& callback)
114 {
115 std::string name(resourceName);
116
117 std::replace(name.begin(), name.end(), '\\', '/');
118
119 ResourceLookup archiveContents = getArchiveBytes(data, name);
120
121 if (archiveContents) {
122 callback(archiveContents.buffer);
123 data->context->engine->setDirty();
124 return;
125 }
126 }
127
128 void readFileAsync(ResourceStoreStorage* data, const std::string& resourceName, std::function<void(const ResourceBuffer data)> callback)
129 {
130 ResourceLookup contents = getCacheBytes(data, resourceName);
131
132 if (contents) {
133 callback(contents.buffer);
134 return;
135 }
136
137#ifdef EMSCRIPTEN
138 auto path = (resourceName.find("https://") == std::string::npos) ? data->dataDir + resourceName : resourceName;
139#else
140 // Note that isRelative can't replace find here.
141 std::string path = !Cogs::IO::isAbsolute(resourceName) && (resourceName.find("../") == std::string::npos) && (resourceName.find("..\\") == std::string::npos)
142 ? Cogs::IO::combine(data->dataDir, resourceName) : resourceName;
143
144 if (!Cogs::IO::isFile(path)) {
145 loadFromArchive(data, resourceName, callback);
146 return;
147 }
148#endif
149
150 DataFetcherManager::fetchAsync(data->context, path, [=](std::unique_ptr<Cogs::FileContents> contents)
151 {
152 if (contents) {
153 ResourceBuffer result;
154
155 {
156 Cogs::LockGuard lock(data->cache->lock);
157
158 Resource& c = data->cache->resources[resourceName] = Resource{ resourceName, path, { std::make_shared<Cogs::Memory::MemoryBuffer>(contents->take()) } };
159
160 LOG_TRACE(logger, "Cached file %s. Size: %zd", resourceName.c_str(), c.bytes.size());
161
162 result = c.bytes;
163 }
164
165 callback(result);
166 data->context->engine->setDirty();
167 }
168 else {
169#ifdef EMSCRIPTEN
170 loadFromArchive(data, resourceName, callback);
171#endif
172 }
173 });
174 }
175
176}
177
178
179namespace Cogs::Core {
180
182 {
183 IOHandler(Context* context, ResourceStore* store) :
184 context(context),
185 store(store)
186 {}
187
188 bool resolveFile(const Cogs::StringView& source, const Cogs::StringView& fileName, std::string& content) override
189 {
190 if (source.size()) {
191 // We might add the path of the source here as a temporary search path to use while resolving this
192 // resource.
193 }
194
195 auto resolvedContents = store->getResourceContents(fileName);
196
197 if (resolvedContents.size()) {
198 content.append(resolvedContents.toStringView());
199
200 return true;
201 }
202 else {
203 // Let resource loading continue with empty contents.
204 return true;
205 }
206 }
207
208 bool openFile(const Cogs::StringView& fileName, std::string& content) override
209 {
210 return resolveFile("", fileName, content);
211 }
212
213 void pushSearchPaths() override { }
214
215 void addSearchPath(const Cogs::StringView& path) override
216 {
217 store->addSearchPath(std::string(path));
218 }
219
220 void popSearchPaths() override { }
221
222 std::string getPath(FileType type, const std::string& fileName) const
223 {
224 if (type == IIOHandler::FileType::ShaderCache) {
225 std::string path = context->variables->get("cache.shaderCache", R"(%TEMP%\Kongsberg Digital\Cogs\ShaderCache\)");
226 return Cogs::IO::expandPath(Cogs::IO::combine(path, fileName));
227 }
228 else if (type == IIOHandler::FileType::ShaderDump) {
229 std::string path = context->variables->get("effects.dumpPath", R"(%TEMP%\Kongsberg Digital\Cogs\ShaderDumps\)");
230 return Cogs::IO::expandPath(Cogs::IO::combine(path, fileName));
231 }
232
233 return{};
234 }
235
236 bool exists(FileType type, const Cogs::StringView& fileName) override
237 {
238 auto content = store->getResourceContents(getPath(type, std::string(fileName)), ResourceStoreFlags::QueryOnly);
239
240 return !content.empty();
241 }
242
243 bool writeBinaryFile(FileType type, const Cogs::StringView& fileName, const void* content, size_t contentSize) override
244 {
245#ifdef EMSCRIPTEN
246 (void)type;
247 (void)fileName;
248 (void)content;
249 (void)contentSize;
250 return false;
251#else
252 return Cogs::IO::writeBinaryFile(getPath(type, std::string(fileName)), content, contentSize);
253#endif
254 }
255
256 bool openBinaryFile(FileType type, const Cogs::StringView& fileName, std::vector<uint8_t>& content) override
257 {
258 auto buffer = store->getResourceContents(getPath(type, std::string(fileName)));
259 if (!buffer.size()) return false;
260
261 const auto view = buffer.toSpan<uint8_t>();
262 content.assign(view.begin(), view.end());
263
264 return true;
265 }
266
267 Context* context = nullptr;
268 ResourceStore* store = nullptr;
269 };
270
271}
272
273Cogs::Core::ResourceStoreStorage::ResourceStoreStorage(Context* context, ResourceStore* resourceStore) :
274 context(context),
275 cache(std::make_unique<ResourceCache>()),
276 ioHandler(std::make_unique<IOHandler>(context, resourceStore))
277{}
278
279
280Cogs::Core::ResourceStore::ResourceStore(Context * context) : data(std::make_unique<ResourceStoreStorage>(context, this))
281{
282 // Load resources from zip as default since anything else is application specific.
283 // Note that Cogs Demos and Example will inject --resources.dataDir=../Data/ as command line argument for you.
284 // This is useful for experimenting with resources on disk without rebuilding.
285 auto dataDirSetting = context->variables->get("resources.dataDir", "");
286 LOG_DEBUG(logger, "Setting data directory to %s.", dataDirSetting);
287 data->dataDir = dataDirSetting;
288 // Emscripten needs to see the correct path first, since it can't easily check existence of files etc.
289 addSearchPath(data->dataDir);
290 // In case ../Data/ is passed in as dataDir, we still want to find extension resource files.
291 // This does not apply to Cogs.js since it currently relies on preloading of extension resources.
292 addSearchPath("");
293}
294
295
296// This must be explicitly defined to make forward declaring ResourceCache in header possible.
297Cogs::Core::ResourceStore::~ResourceStore() = default;
298
299
300void Cogs::Core::ResourceStore::preloadResources(const std::vector<std::string> & resourceNames)
301{
302 for (const std::string& name : resourceNames) {
303 {
304 LockGuard lock(data->cache->lock);
305
306 // Skip resources already being loaded, or finished loading.
307 if (data->cache->loading.contains(name)) continue;
308 if (data->cache->resources.contains(name)) continue;
309
310 data->cache->loading.insert(name);
311 }
312
313 readFileAsync(data.get(), name, [this, name](const ResourceBuffer& bytes)
314 {
315 if (bytes.size()) {
316 LOG_DEBUG(logger, "Preloaded file %s.", name.c_str());
317 } else {
318 LOG_DEBUG(logger, "Failed preloading file %s.", name.c_str());
319 }
320
321 {
322 LockGuard lock(data->cache->lock);
323 data->cache->loading.erase(data->cache->loading.find(name));
324 }
325 data->context->engine->setDirty();
326 });
327 }
328}
329
330bool Cogs::Core::ResourceStore::hasResources(const std::vector<std::string> & resourceNames)
331{
332 bool loaded = true;
333
334 for (auto & name : resourceNames) {
335 loaded &= hasResource(name);
336 }
337
338 return loaded;
339}
340
341bool Cogs::Core::ResourceStore::hasResource(const std::string & resourceName)
342{
343 {
344 LockGuard lock(data->cache->lock);
345 if (auto resourceIt = data->cache->resources.find(resourceName); resourceIt != data->cache->resources.end()) {
346 return true;
347 }
348 }
349
350#ifdef EMSCRIPTEN
351 for (auto & path : data->orderedSearchPaths) {
352 const auto resourcePath = path + resourceName;
353
354 for (auto & archive : data->archives) {
355 if (archive.hasResource(resourcePath)) return true;
356 }
357 }
358#endif
359 return false;
360}
361
363{
364 for (auto & path : data->orderedSearchPaths) {
365 std::string resourcePath = IO::combine(path, std::string(resourceName));
366 std::replace(resourcePath.begin(), resourcePath.end(), '\\', '/');
367
368#ifndef EMSCRIPTEN
370#else
371 {
372#endif
373 auto cacheContents = getCacheBytes(data.get(), resourcePath);
374
375 if (cacheContents) {
376 return cacheContents.buffer;
377 }
378 }
379
380#ifndef EMSCRIPTEN
381 if (!IO::exists(resourcePath)) {
383 auto cacheContents = getCacheBytes(data.get(), resourcePath);
384 if (cacheContents) {
385 return cacheContents.buffer;
386 }
387 }
388 continue;
389 }
390
391 bool useCache = (flags & ResourceStoreFlags::NoCachedContent) == 0;
392 auto contents = getFileBytes(data.get(), resourcePath, useCache);
393
394 if (contents) {
395 return contents.buffer;
396 }
397#endif
398 }
399
400 for (auto & path : data->orderedSearchPaths) {
401 std::string resourcePath = IO::combine(path, std::string(resourceName));
402
403 std::replace(resourcePath.begin(), resourcePath.end(), '\\', '/');
404
405 auto archiveContents = getArchiveBytes(data.get(), resourcePath);
406
407 if (archiveContents) {
408 return archiveContents.buffer;
409 }
410 }
411
412 if ((flags & ResourceStoreFlags::QueryOnly) == 0) {
413 LOG_ERROR(logger, "Could not resolve resource: %.*s.", StringViewFormat(resourceName));
414 }
415
416 return emptyBytes;
417}
418
419std::string Cogs::Core::ResourceStore::getResourceContentString(std::string_view resourceName, ResourceStoreFlags flags) const
420{
421
422 auto bytes = getResourceContents(resourceName, flags);
423
424 std::string result;
425 if (bytes.size()) {
426 std::string_view view = bytes.toStringView();
427 result+= view;
428 }
429
430 return result;
431}
432
434{
435 return data->dataDir;
436}
437
438
439void Cogs::Core::ResourceStore::addResource(std::string_view resourceName, std::string_view content)
440{
441 Memory::MemoryBuffer buffer(content.size(), data->context->memory->ioAllocator, MemBlockType::IOResource);
442 std::memcpy(buffer.data(), content.data(), buffer.size());
443
444 std::string name(resourceName);
445 std::replace(name.begin(), name.end(), '\\', '/');
446
447 LockGuard lock(data->cache->lock);
448 data->cache->resources[name] = Resource{ std::string(resourceName), std::string(resourceName), {std::make_shared<Memory::MemoryBuffer>(std::move(buffer))} };
449}
450
451void Cogs::Core::ResourceStore::addResource(std::string_view resourceName, const void* content_data, size_t content_size)
452{
453 Memory::MemoryBuffer buffer(content_size, data->context->memory->ioAllocator, MemBlockType::IOResource);
454 std::memcpy(buffer.data(), content_data, std::min(content_size, buffer.size()));
455
456 std::string name(resourceName);
457 std::replace(name.begin(), name.end(), '\\', '/');
458
459 LockGuard lock(data->cache->lock);
460 data->cache->resources[name] = Resource{ std::string(resourceName), std::string(resourceName), {std::make_shared<Memory::MemoryBuffer>(std::move(buffer))} };
461}
462
463void Cogs::Core::ResourceStore::addResourceArchive(const std::string & archiveName, bool prepend)
464{
465 if (prepend) {
466 data->archives.emplace(data->archives.begin(), this, archiveName);
467 }
468 else {
469 data->archives.emplace_back(this, archiveName);
470 }
471}
472
473std::string Cogs::Core::ResourceStore::getResourcePath(std::string_view resourceName, ResourceStoreFlags flags) const
474{
475 for (auto & searchPath : data->orderedSearchPaths) {
476 std::string path = IO::combine(searchPath, std::string(resourceName));
477
478 std::replace(path.begin(), path.end(), '\\', '/');
479
480 if ((flags & ResourceStoreFlags::NoCachedContent) == 0) {
481 auto cacheContents = getCacheBytes(data.get(), path);
482
483 if (cacheContents) return path;
484 }
485
486 if (IO::exists(path) && IO::isFile(path)) {
487 return path;
488 }
489 }
490
491 return {};
492}
493
495{
496 if (resourceName.empty()) return {};
497
498 auto [resourcePath, protocol] = getPathAndProtocol(resourceName);
499
500 if (protocol == ResourceProtocol::File || protocol == ResourceProtocol::None) {
501 std::string str(resourcePath);
502
503 if (protocol == ResourceProtocol::None && !IO::isRelative(str)) {
504 if ((flags & ResourceStoreFlags::NoCachedContent) == 0) {
505 auto cacheContents = getCacheBytes(data.get(), str);
506
507 if (cacheContents) return { str, ResourceProtocol::Cache };
508 }
509
510 return { str, ResourceProtocol::File };
511 } else {
512 for (auto & searchPath : data->orderedSearchPaths) {
513 const auto path = IO::combine(searchPath, str);
514
515 if ((flags & ResourceStoreFlags::NoCachedContent) == 0) {
516 auto cacheContents = getCacheBytes(data.get(), path);
517
518 if (cacheContents) return { path, ResourceProtocol::Cache };
519 }
520
521 if (IO::exists(path) && IO::isFile(path)) {
522 if (protocol == ResourceProtocol::None) {
523 return { path, ResourceProtocol::File };
524 }
525 }
526 }
527 }
528 }
529
530 if (protocol == ResourceProtocol::None || protocol == ResourceProtocol::Archive) {
531 for (auto & searchPath : data->orderedSearchPaths) {
532 const auto archivePath = IO::combine(searchPath, std::string(resourcePath));
533
534 for (auto & archive : data->archives) {
535 if (archive.hasResource(archivePath)) {
536 return { archivePath, ResourceProtocol::Archive };
537 }
538 }
539 }
540 }
541
542 return { std::string(resourceName), ResourceProtocol::None };
543}
544
545void Cogs::Core::ResourceStore::addSearchPath(const std::string & path, bool prepend)
546{
547 if (!data->searchPaths.contains(path)) {
548 data->searchPaths.insert(path);
549 if (prepend) {
550 data->orderedSearchPaths.insert(data->orderedSearchPaths.begin(), path);
551 }
552 else {
553 data->orderedSearchPaths.emplace_back(path);
554 }
555 }
556}
557
558void Cogs::Core::ResourceStore::purge(const std::string & resourceName)
559{
560 LockGuard lock(data->cache->lock);
561 if (auto it = data->cache->resources.find(resourceName); it != data->cache->resources.end()) {
562 data->cache->resources.erase(it);
563 }
564
565 auto it = data->archives.begin();
566 for (auto& archive : data->archives)
567 {
568 if (archive.hasResource(resourceName))
569 {
570 data->archives.erase(it);
571 return;
572 }
573 ++it;
574 }
575}
576
578{
579 LockGuard lock(data->cache->lock);
580 data->cache->resources.clear();
581}
582
584{
585 return data->ioHandler.get();
586}
A Context instance contains all the services, systems and runtime components needed to use Cogs.
Definition: Context.h:83
std::unique_ptr< class Variables > variables
Variables service instance.
Definition: Context.h:180
Provides handling of reading and caching of external resources.
const std::string & getDataDir() const
Get the path of the data directory.
void addResource(std::string_view resourceName, const std::string_view content)
Add the given resource string to the resource store.
void addResourceArchive(const std::string &archiveName, bool prepend=false)
Add the given resource archive to the store.
ResourceQueryResult getResourceLocation(std::string_view resourceName, ResourceStoreFlags flags=ResourceStoreFlags::None) const
Get location data for the given resource.
IIOHandler * getIOHandler()
Retrieve the IO handler interface.
std::string getResourceContentString(std::string_view resourceName, ResourceStoreFlags flags=ResourceStoreFlags::None) const
Get the contents of the resource with the given copied into a string.
void addSearchPath(const std::string &path, bool prepend=false)
Add the given path to the set of search paths used to look up resources.
bool hasResource(const std::string &resourceName)
Check if the given resource is present in the resource store.
bool hasResources(const std::vector< std::string > &resourceNames)
Check if all the given resources are present in the resource store.
void purge(const std::string &resourceName)
Purge the given file from the cache.
ResourceBuffer getResourceContents(std::string_view resourceName, ResourceStoreFlags flags=ResourceStoreFlags::None) const
Get the contents of the resource with the given name.
void preloadResources(const std::vector< std::string > &resourceNames)
Tells the resource system to fetch and cache the given resources.
void clear()
Clear the cache.
Log implementation class.
Definition: LogManager.h:139
Provides a weakly referenced view over the contents of a string.
Definition: StringView.h:24
Contains the Engine, Renderer, resource managers and other systems needed to run Cogs....
@ PreferUncachedContent
Try fetching data before resorting to cached data.
@ NoCachedContent
Never use cached data.
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:180
STL namespace.
bool resolveFile(const Cogs::StringView &source, const Cogs::StringView &fileName, std::string &content) override
Callback method used to resolve include statements in shader code and get the contents to include.
bool openFile(const Cogs::StringView &fileName, std::string &content) override
Callback method used to open files.
std::span< const T > toSpan(size_t offset=0) const
Definition: ResourceStore.h:50
I/O handler.
Definition: IEffects.h:106