载入中...
搜索中...
未找到
Resource.cpp
浏览该文件的文档.
1#include "common/Resource.h"
2#include "common/AsyncWork.h"
3#include "common/Capability.h"
4#include "common/Diagnostic.h"
5#include "common/Exception.h"
6
7#include <memory>
8#include <mutex>
9#include <set>
10#include <utility>
11#include <vector>
12
13namespace eve {
14
16 // Intentionally leaked: cached CPU resources may own third-party handles
17 // (FreeType faces, Assimp scenes, image decode handlers) whose libraries
18 // are torn down at process exit in an unspecified TU order. Destroying
19 // cached entries from the singleton destructor can therefore crash at
20 // exit. Keeping the singleton alive until the OS reclaims it avoids
21 // exit-time destructors entirely; explicit unload()/clear() still release
22 // entries during the run.
23 static ResourceManager* instance = new ResourceManager();
24 instance->ensureRegistered();
25 return *instance;
26}
27
28std::string ResourceManager::normalizePath(std::string path) {
29 for (char &c : path) {
30 if (c == '\\') c = '/';
31 }
32 while (path.size() >= 2 && path[0] == '.' && path[1] == '/') path.erase(0, 2);
33 while (path.size() > 1 && path.back() == '/') path.pop_back();
34 return path;
35}
36
37std::string ResourceManager::makeKey(const std::string &path, const std::string &query) {
38 std::string key = normalizePath(path);
39 if (!query.empty()) {
40 key += '?';
41 key += query;
42 }
43 return key;
44}
45
46std::string ResourceManager::pathOfKey(const std::string &key) {
47 const auto q = key.find('?');
48 return normalizePath(q == std::string::npos ? key : key.substr(0, q));
49}
50
52 std::lock_guard<std::mutex> lock(mu_);
53 if (registered_) return;
54 eve::cap::addListener<eve::caps::IAssetReloader>(this, eve::caps::IAssetReloader::kCache);
55 registered_ = true;
56}
57
58size_t ResourceManager::count() const {
59 std::lock_guard<std::mutex> lock(mu_);
60 return resources.size();
61}
62
63Resource *ResourceManager::loadUncached(const std::string &norm) {
64 Resource *loaded = nullptr;
65 eve::cap::forEachUntil<eve::caps::IAssetReloader>([&](eve::caps::IAssetReloader *r) {
66 if (r == this) return false;
67 if (!r->handlesPath(norm)) return false;
68 loaded = r->load(norm);
69 return loaded != nullptr;
70 });
71 if (loaded) loaded->setUri(norm);
72 return loaded;
73}
74
75void ResourceManager::runLoadJob(std::string norm, uint64_t epoch) {
76 // The candidate is owned from the moment the provider hands it over, so every
77 // early return below releases it exactly once without an explicit delete.
79 std::string error;
80 try {
81 loaded.reset(loadUncached(norm));
82 } catch (const std::exception &ex) {
83 error = ex.what();
84 } catch (...) {
85 error = "unknown resource load failure";
86 }
87
88 std::lock_guard<std::mutex> lock(mu_);
89 auto pendingIt = pending_.find(norm);
90 if (epoch_ != epoch) {
91 if (pendingIt != pending_.end()) {
92 pendingIt->second->done = true;
93 pending_.erase(pendingIt);
94 }
95 cv_.notify_all();
96 return;
97 }
98 if (!error.empty() || !loaded) {
99 if (pendingIt != pending_.end()) {
100 pendingIt->second->done = true;
101 pendingIt->second->failed = true;
102 pendingIt->second->error =
103 error.empty() ? std::string("no provider claimed key") : std::move(error);
104 pending_.erase(pendingIt);
105 }
106 cv_.notify_all();
107 return;
108 }
109 // Check the map before registering: a concurrent load of the same key may have won the
110 // race while this candidate decoded. Registering first and erasing afterwards would
111 // leave the candidate stranded in the registry whenever that erase cannot complete.
112 if (resources.find(norm) != resources.end()) {
113 if (pendingIt != pending_.end()) {
114 pendingIt->second->done = true;
115 pending_.erase(pendingIt);
116 }
117 cv_.notify_all();
118 return; // `loaded` destroys the losing candidate
119 }
120 auto entry = registry_.emplace(std::move(loaded));
121 if (!entry.ok()) {
122 if (pendingIt != pending_.end()) {
123 pendingIt->second->done = true;
124 pendingIt->second->failed = true;
125 pendingIt->second->error = entry.status().describe();
126 pending_.erase(pendingIt);
127 }
128 cv_.notify_all();
129 return;
130 }
131 // No second element can appear for `norm` between the check above and this insert:
132 // both run under mu_.
133 resources.emplace(norm, entry.value());
134 if (pendingIt != pending_.end()) {
135 pendingIt->second->done = true;
136 pending_.erase(pendingIt);
137 }
138 cv_.notify_all();
139}
140
142 auto waited = waitFor(std::move(key));
143 if (waited.ok()) return &waited.value().get();
144 if (waited.code() == eve::StatusCode::NotFound) return nullptr;
145 const std::string detail = waited.status().describe();
146 throw eve::Exception("%s", detail.c_str());
147}
148
150 const std::string norm = makeKey(std::move(key));
151 if (norm.empty()) {
154 }
156
157 std::shared_ptr<AsyncLoad> job;
158 uint64_t epoch = 0;
159 {
160 std::lock_guard<std::mutex> lock(mu_);
161 auto cached = resources.find(norm);
162 if (cached != resources.end())
164 auto pendingIt = pending_.find(norm);
165 if (pendingIt != pending_.end() && !pendingIt->second->done)
167 if (pendingIt != pending_.end()) pending_.erase(pendingIt);
168
169 bool claimed = false;
170 eve::cap::forEachUntil<eve::caps::IAssetReloader>([&](eve::caps::IAssetReloader *r) {
171 if (r == this) return false;
172 if (!r->handlesPath(norm)) return false;
173 claimed = true;
174 return true;
175 });
176 if (!claimed) {
178 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "no provider claimed key", norm));
179 }
180
181 job = std::make_shared<AsyncLoad>();
182 job->epoch = epoch_;
183 epoch = epoch_;
184 pending_.emplace(norm, job);
185 }
186
187 auto *executor = eve::cap::query<eve::caps::IAsyncWorkExecutor>();
188 if (!executor) {
189 runLoadJob(norm, epoch);
190 std::lock_guard<std::mutex> lock(mu_);
191 if (resources.find(norm) != resources.end())
194 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "inline resource load failed", norm));
195 }
196
197 auto submitted = executor->submit([this, norm, epoch]() { runLoadJob(norm, epoch); });
198 if (!submitted.ok()) {
199 std::lock_guard<std::mutex> lock(mu_);
200 pending_.erase(norm);
201 cv_.notify_all();
202 return submitted;
203 }
205}
206
207eve::Result<void> ResourceManager::requestAll(std::span<const std::string> keys) {
208 for (const auto &key : keys) {
209 auto one = request(key);
210 if (!one.ok()) return one;
211 one.ignore("queued or already cached/pending");
212 }
214}
215
217 const std::string norm = makeKey(key);
218 std::lock_guard<std::mutex> lock(mu_);
219 auto it = resources.find(norm);
220 if (it == resources.end()) return std::nullopt;
221 Resource *live = borrowLocked(it->second);
222 if (live == nullptr) return std::nullopt;
223 return std::ref(*live);
224}
225
227 const std::string norm = makeKey(std::move(key));
228 if (norm.empty()) {
231 }
233
234 std::shared_ptr<AsyncLoad> job;
235 {
236 std::unique_lock<std::mutex> lock(mu_);
237 auto cached = resources.find(norm);
238 if (cached != resources.end()) {
239 if (Resource *live = borrowLocked(cached->second)) {
240 return ResultRef<Resource>::success(std::ref(*live));
241 }
242 }
243 auto pendingIt = pending_.find(norm);
244 if (pendingIt != pending_.end()) job = pendingIt->second;
245 if (job) {
246 cv_.wait(lock, [&] { return job->done || resources.find(norm) != resources.end(); });
247 cached = resources.find(norm);
248 if (cached != resources.end()) {
249 if (Resource *live = borrowLocked(cached->second)) {
250 return ResultRef<Resource>::success(std::ref(*live));
251 }
252 }
253 if (job->failed) {
256 job->error.empty() ? "async resource load failed" : job->error, norm));
257 }
258 }
259 }
260
261 if (job && job->done) {
262 std::lock_guard<std::mutex> lock(mu_);
263 auto cached = resources.find(norm);
264 if (cached != resources.end()) {
265 if (Resource *live = borrowLocked(cached->second)) {
266 return ResultRef<Resource>::success(std::ref(*live));
267 }
268 }
269 if (job->failed) {
272 job->error.empty() ? "async resource load failed" : job->error, norm));
273 }
274 }
275
277 try {
278 loaded.reset(loadUncached(norm));
279 } catch (const std::exception &ex) {
282 }
283 if (!loaded) {
285 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "no provider claimed key", norm));
286 }
287
288 std::lock_guard<std::mutex> lock(mu_);
289 // The decode ran outside the lock, so another thread may have cached the key
290 // meanwhile: keep that entry and drop this candidate instead of inserting twice.
291 auto existing = resources.find(norm);
292 if (existing != resources.end()) {
293 if (Resource *live = borrowLocked(existing->second)) {
294 return ResultRef<Resource>::success(std::ref(*live));
295 }
296 }
297 auto entry = registry_.emplace(std::move(loaded));
298 if (!entry.ok()) {
300 eve::Diagnostic::error(eve::DiagnosticCode::Failed, entry.status().describe(), norm));
301 }
302 resources.emplace(norm, entry.value());
303 Resource *live = borrowLocked(entry.value());
304 if (live == nullptr) {
306 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "cached resource could not be resolved", norm));
307 }
308 return ResultRef<Resource>::success(std::ref(*live));
309}
310
312 if (resource == nullptr) {
314 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "cannot pin a null resource"));
315 }
316 std::lock_guard<std::mutex> lock(mu_);
317 for (auto &kv : resources) {
318 if (borrowLocked(kv.second) != resource) continue;
319 return registry_.pin(kv.second);
320 }
321 // Deliberately does not name the resource: the caller's borrowed pointer may already
322 // have been destroyed by a concurrent unload() before this call ran.
324 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "resource is not cached"));
325}
326
328 std::lock_guard<std::mutex> lock(mu_);
329 return pending_.size();
330}
331
332void ResourceManager::unload(std::string key) {
333 std::lock_guard<std::mutex> lock(mu_);
334 auto it = resources.find(makeKey(std::move(key)));
335 if (it == resources.end()) return;
336 // The handle goes stale at once; a pinned payload is destroyed when the last pin is
337 // released, which is what keeps a playing SoundData alive. A failed erase (free-list
338 // bookkeeping could not grow) keeps the payload owned by a reachable slot, so the
339 // mapping stays: dropping it would strand the resource until clear() or process exit.
340 if (registry_.erase(it->second).ok()) resources.erase(it);
341}
342
343void ResourceManager::unloadPath(const std::string &path) {
344 const std::string norm = normalizePath(path);
345 std::lock_guard<std::mutex> lock(mu_);
346 for (auto it = resources.begin(); it != resources.end();) {
347 if (pathOfKey(it->first) != norm) {
348 ++it;
349 continue;
350 }
351 // Same rule as unload(): keep the mapping when the payload could not be released.
352 if (registry_.erase(it->second).ok()) {
353 it = resources.erase(it);
354 } else {
355 ++it;
356 }
357 }
358}
359
361 {
362 std::unique_lock<std::mutex> lock(mu_);
363 ++epoch_;
364 cv_.wait(lock, [&] {
365 for (const auto &kv : pending_) {
366 if (!kv.second->done) return false;
367 }
368 return true;
369 });
370 pending_.clear();
371 registry_.clear();
372 resources.clear();
373 }
374 if (registered_) {
375 eve::cap::removeListener<eve::caps::IAssetReloader>(this);
376 registered_ = false;
377 }
378}
379
380bool ResourceManager::handlesPath(const std::string &normPath) const {
381 const std::string norm = normalizePath(normPath);
382 if (norm.empty()) return false;
383 std::lock_guard<std::mutex> lock(mu_);
384 for (const auto &kv : resources) {
385 if (pathOfKey(kv.first) == norm) return true;
386 }
387 return false;
388}
389
390eve::Result<bool> ResourceManager::reload(const std::string &normPath) {
391 const std::string norm = normalizePath(normPath);
392 if (norm.empty()) return eve::Result<bool>::success(false);
393
394 struct Prepared {
395 std::string key;
396 Resource *cached = nullptr;
397 script::Owned<Resource> replacement;
398 };
399
400 std::vector<std::string> keys;
401 std::set<std::string> selectedKeys;
402 std::set<Resource *> selectedResources;
403 {
404 std::lock_guard<std::mutex> lock(mu_);
405 for (auto &kv : resources) {
406 if (pathOfKey(kv.first) != norm) continue;
407 Resource *cached = borrowLocked(kv.second);
408 if (cached == nullptr) continue;
409 keys.push_back(kv.first);
410 selectedKeys.insert(kv.first);
411 selectedResources.insert(cached);
412 }
413
414 // Close over reverse dependencies before loading anything. This gives
415 // the transaction a stable, root-first order and makes cycles safe.
416 bool expanded = true;
417 while (expanded) {
418 expanded = false;
419 for (auto &kv : resources) {
420 if (selectedKeys.count(kv.first)) continue;
421 Resource *cached = borrowLocked(kv.second);
422 if (cached == nullptr) continue;
423 bool dependsOnSelection = false;
424 for (Resource *dependency : cached->getDependencies()) {
425 if (selectedResources.count(dependency)) {
426 dependsOnSelection = true;
427 break;
428 }
429 }
430 if (!dependsOnSelection) continue;
431 keys.push_back(kv.first);
432 selectedKeys.insert(kv.first);
433 selectedResources.insert(cached);
434 expanded = true;
435 }
436 }
437 }
438 if (keys.empty()) return eve::Result<bool>::success(false);
439
440 // Decode every root and dependent into detached candidates. A single
441 // invalid file aborts the whole graph without touching live objects.
442 std::vector<Prepared> prepared;
443 std::vector<ResourcePin> keepAlive;
444 prepared.reserve(keys.size());
445 keepAlive.reserve(keys.size());
446 for (const auto &key : keys) {
447 Resource *cached = nullptr;
448 {
449 std::lock_guard<std::mutex> lock(mu_);
450 auto it = resources.find(key);
451 if (it == resources.end()) return eve::Result<bool>::success(false);
452 cached = borrowLocked(it->second);
453 if (cached == nullptr) return eve::Result<bool>::success(false);
454 // Pin the entry while the replacement decodes outside the lock: an
455 // unload() racing this reload must not destroy a resource we are
456 // about to refresh in place.
457 auto pinned = registry_.pin(it->second);
458 if (!pinned.ok()) return eve::Result<bool>::success(false);
459 keepAlive.push_back(std::move(pinned).takeValue());
460 }
462 if (!replacement) return eve::Result<bool>::success(false);
463 prepared.push_back({key, cached, std::move(replacement)});
464 }
465
466 std::lock_guard<std::mutex> lock(mu_);
467 for (const auto &item : prepared) {
468 auto it = resources.find(item.key);
469 if (it == resources.end() || borrowLocked(it->second) != item.cached) {
470 return eve::Result<bool>::success(false);
471 }
472 }
473
474 size_t committed = 0;
475 try {
476 for (; committed < prepared.size(); ++committed)
477 prepared[committed].cached->adopt(*prepared[committed].replacement);
478 } catch (...) {
479 // adopt() is a payload swap. Candidates therefore hold the old state
480 // after a successful commit and can restore it in reverse order.
481 while (committed > 0) {
482 --committed;
483 try {
484 prepared[committed].cached->adopt(*prepared[committed].replacement);
485 } catch (...) {
486 // The adopt contract requires a non-mutating failure. Keep
487 // unwinding other entries even if a broken implementation
488 // violates it.
489 }
490 }
491 return eve::Result<bool>::success(false);
492 }
494}
495
497 try {
498 return loadUncached(key);
499 } catch (...) {
500 return nullptr;
501 }
502}
503
504std::vector<Resource *> Resource::getDependencies() const {
505 std::vector<Resource *> borrowed;
506 borrowed.reserve(dependencies.size());
507 for (const auto &pin : dependencies) borrowed.push_back(pin.get());
508 return borrowed;
509}
510
512 // Address-based, never dereferenced: the dependency may have been unloaded since the
513 // caller borrowed its reference.
514 auto pinned = ResourceManager::getInstance().pin(&dependency);
515 if (!pinned.ok()) return eve::Result<void>::failure(pinned.status());
516 dependencies.push_back(std::move(pinned).takeValue());
518}
519
520} // namespace eve
Capability for submitting CPU work onto a worker pool.
Stable, structured diagnostics shared by engine modules.
const GltfImportRequest & request
std::uint32_t key
std::array< double, 10 > q
double r
std::int32_t c
eve::ResourcePin pinned
uint64_t epoch
Definition OnnxGpgpu.cpp:28
const RuntimeTensor * borrowed
std::string error
Definition Package.cpp:60
std::string path
Definition PlayHost.cpp:110
int detail
std::string resource
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
ResourceManager is a singleton that manages all resources in the game. It provides a way to load,...
Definition Resource.h:152
ResourceManager()=default
Resource manager.
Resource * loadUncached(const std::string &norm)
Loads uncached.
Definition Resource.cpp:63
void unloadPath(const std::string &path)
Drop every cache entry whose path matches, whatever its parameters.
Definition Resource.cpp:343
bool handlesPath(const std::string &normPath) const override
Handles path.
Definition Resource.cpp:380
eve::Result< ResourcePin > pin(Resource *resource)
Keeps one cached resource alive across unload()/clear()/reload.
Definition Resource.cpp:311
Resource * loadReplacement(const std::string &key)
Loads replacement.
Definition Resource.cpp:496
void unload(std::string key)
Drop the exact cache entry key (parameters included). The resource stays alive while other holders st...
Definition Resource.cpp:332
ResultRef< Resource > waitFor(std::string key)
Block until key is cached, failed, or unclaimed.
Definition Resource.cpp:226
eve::Result< void > request(std::string key)
Queue a cache miss load without blocking for decode to finish.
Definition Resource.cpp:149
Resource * get(std::string key)
Get the cached resource for key; on a miss, load it through the registered IAssetReloader providers a...
Definition Resource.cpp:141
eve::Result< void > requestAll(std::span< const std::string > keys)
request() every key; stops at the first hard failure.
Definition Resource.cpp:207
static std::string pathOfKey(const std::string &key)
The path part of a cache key (everything before the first '?').
Definition Resource.cpp:46
void ensureRegistered()
Ensure registered.
Definition Resource.cpp:51
static std::string normalizePath(std::string path)
Normalize a VFS path: backslashes to '/', strip leading "./" and trailing '/'.
Definition Resource.cpp:28
size_t pendingCount() const
Number of in-flight request() jobs (including failed-not-yet-consumed).
Definition Resource.cpp:327
void runLoadJob(std::string norm, uint64_t epoch)
Run load job.
Definition Resource.cpp:75
std::unordered_map< std::string, std::shared_ptr< AsyncLoad > > pending_
Definition Resource.h:336
Resource * borrowLocked(const ResourceRef &reference)
Borrows the live resource behind a cache entry.
Definition Resource.h:331
eve::Result< bool > reload(const std::string &normPath) override
Reloads .
Definition Resource.cpp:390
void clear()
Drop every entry and re-arm lazy registration (tests / teardown).
Definition Resource.cpp:360
static std::string makeKey(const std::string &path, const std::string &query="")
Build a cache key from a path and optional ?query parameters.
Definition Resource.cpp:37
OptionalRef< Resource > peek(const std::string &key)
Cache lookup that never starts a load.
Definition Resource.cpp:216
script::RuntimeObjectRegistry< Resource, ResourceCacheTag > registry_
Registry that exclusively owns every cached resource.
Definition Resource.h:334
std::condition_variable cv_
Definition Resource.h:338
size_t count() const
Number of cached entries.
Definition Resource.cpp:58
std::map< std::string, ResourceRef > resources
Definition Resource.h:335
static ResourceManager & getInstance()
Returns the instance.
Definition Resource.cpp:15
Resource is a game object that is managed by the ResourceManager. It can be loaded from a file or gen...
Definition Resource.h:51
void setUri(std::string value)
Set the resource URI (the cache key). Used by ResourceManager.
Definition Resource.h:117
EVENGINE_API_FOUNDATION std::vector< Resource * > getDependencies() const
Borrowed views of the resources this instance keeps alive.
Definition Resource.cpp:504
EVENGINE_API_FOUNDATION eve::Result< void > addDependency(Resource &dependency)
Keeps dependency alive for as long as this instance lives.
Definition Resource.cpp:511
std::vector< ResourcePin > dependencies
Keep-alive pins; see getDependencies()/addDependency().
Definition Resource.h:123
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
EVENGINE_API_FOUNDATION public API.
std::unique_ptr< T > Owned
C++ owning storage used by an object factory.
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
std::optional< std::reference_wrapper< T > > OptionalRef
Optional borrowed reference; it does not extend T's lifetime.
Definition BorrowedRef.h:24