12#define TINYGLTF_NO_STB_IMAGE
13#define TINYGLTF_NO_STB_IMAGE_WRITE
17#include "spdlog/spdlog.h"
28 _worker = std::thread(&ResourceLoader::workerLoop,
this);
29 spdlog::info(
"ResourceLoader: background I/O thread started");
39 _handlers[type] = std::move(handler);
44 _handlers.erase(type);
50 _pendingCount.fetch_add(1, std::memory_order_relaxed);
52 std::lock_guard lock(_requestMutex);
53 _requests.push_back({url, type, std::move(onSuccess), std::move(onError)});
55 _requestCV.notify_one();
64 std::deque<Completion> batch;
66 std::lock_guard lock(_completionMutex);
67 if (maxCompletions <= 0 ||
static_cast<int>(_completions.size()) <= maxCompletions) {
68 batch.swap(_completions);
71 auto end = _completions.begin() + maxCompletions;
72 batch.assign(std::make_move_iterator(_completions.begin()),
73 std::make_move_iterator(end));
74 _completions.erase(_completions.begin(), end);
78 for (
auto& c : batch) {
79 if (!c.error.empty()) {
83 spdlog::error(
"ResourceLoader: unhandled error: {}", c.error);
87 c.onSuccess(std::move(c.data));
95 if (!_running.exchange(
false)) {
99 _requestCV.notify_all();
100 if (_worker.joinable()) {
103 spdlog::info(
"ResourceLoader: background I/O thread stopped");
108 return _pendingCount.load(std::memory_order_relaxed) > 0;
111 void ResourceLoader::workerLoop()
116 std::unique_lock lock(_requestMutex);
117 _requestCV.wait(lock, [
this] {
118 return !_requests.empty() || !_running;
121 if (!_running && _requests.empty()) {
124 if (_requests.empty()) {
128 req = std::move(_requests.front());
129 _requests.pop_front();
132 Completion completion;
133 completion.onSuccess = std::move(req.onSuccess);
134 completion.onError = std::move(req.onError);
137 auto it = _handlers.find(req.type);
138 if (it == _handlers.end()) {
139 completion.error =
"No resource handler registered for type '" + req.type +
"'";
140 spdlog::error(
"ResourceLoader: {}", completion.error);
143 auto loaded = it->second->load(req.url);
145 completion.data = std::move(loaded);
147 completion.error =
"Handler returned null for '" + req.url +
"'";
149 }
catch (
const std::exception& e) {
150 completion.error =
"Exception loading '" + req.url +
"': " + e.what();
151 spdlog::error(
"ResourceLoader: {}", completion.error);
156 std::lock_guard lock(_completionMutex);
157 _completions.push_back(std::move(completion));
159 _pendingCount.fetch_sub(1, std::memory_order_relaxed);
168 stbi_set_flip_vertically_on_load_thread(
false);
170 const bool isHdr = url.size() >= 4 &&
171 url.compare(url.size() - 4, 4,
".hdr") == 0;
173 auto result = std::make_unique<LoadedData>();
182 if (!hdrPixels || pd.
width <= 0 || pd.
height <= 0) {
183 spdlog::error(
"TextureResourceHandler: failed to decode HDR '{}'", url);
184 if (hdrPixels) stbi_image_free(hdrPixels);
188 const size_t pixelCount =
static_cast<size_t>(pd.
width) *
static_cast<size_t>(pd.
height);
190 for (
size_t i = 0; i < pixelCount; ++i) {
198 stbi_image_free(hdrPixels);
200 spdlog::info(
"TextureResourceHandler: decoded HDR '{}' {}x{} ch={}",
204 stbi_uc* pixels = stbi_load(url.c_str(), &pd.
width, &pd.
height, &pd.
channels, STBI_rgb_alpha);
206 spdlog::error(
"TextureResourceHandler: failed to decode '{}'", url);
207 if (pixels) stbi_image_free(pixels);
211 const size_t dataSize =
static_cast<size_t>(pd.
width) *
static_cast<size_t>(pd.
height) * 4;
212 pd.
pixels.assign(pixels, pixels + dataSize);
213 stbi_image_free(pixels);
215 spdlog::info(
"TextureResourceHandler: decoded '{}' {}x{} ch={}",
219 result->pixelData = std::move(pd);
227 bool hasExtension(
const std::string& path,
const std::string& ext)
229 if (path.size() < ext.size())
return false;
230 auto pathExt = path.substr(path.size() - ext.size());
231 std::transform(pathExt.begin(), pathExt.end(), pathExt.begin(), ::tolower);
232 return pathExt == ext;
238 std::ifstream file(url, std::ios::binary | std::ios::ate);
240 spdlog::error(
"ContainerResourceHandler: cannot open '{}'", url);
244 const auto size = file.tellg();
247 auto result = std::make_unique<LoadedData>();
249 result->bytes.resize(
static_cast<size_t>(size));
250 file.read(
reinterpret_cast<char*
>(result->bytes.data()), size);
253 spdlog::error(
"ContainerResourceHandler: read error for '{}'", url);
257 spdlog::info(
"ContainerResourceHandler: read {} bytes from '{}'", result->bytes.size(), url);
264 const bool isGlb = hasExtension(url,
".glb");
265 const bool isGltf = hasExtension(url,
".gltf");
267 if ((isGlb || isGltf) && !result->bytes.empty()) {
268 auto model = std::make_shared<tinygltf::Model>();
269 tinygltf::TinyGLTF loader;
271 std::string warn, err;
275 ok = loader.LoadBinaryFromMemory(
276 model.get(), &err, &warn,
277 result->bytes.data(),
278 static_cast<unsigned int>(result->bytes.size()));
281 const std::string gltfString(
282 reinterpret_cast<const char*
>(result->bytes.data()),
283 result->bytes.size());
286 if (
auto pos = url.find_last_of(
"/\\"); pos != std::string::npos) {
287 baseDir = url.substr(0, pos + 1);
289 ok = loader.LoadASCIIFromString(
290 model.get(), &err, &warn,
292 static_cast<unsigned int>(gltfString.size()),
297 spdlog::warn(
"ContainerResourceHandler: tinygltf warning [{}]: {}", url, warn);
303 auto prepared = std::make_shared<PreparedGlbData>(
305 result->preparsed = std::move(model);
306 result->preparedData = std::move(prepared);
307 spdlog::info(
"ContainerResourceHandler: pre-parsed + prepared {} on bg thread [{}]",
308 isGlb ?
"GLB" :
"GLTF", url);
311 spdlog::warn(
"ContainerResourceHandler: bg pre-parse failed [{}]: {}", url, err);
322 std::ifstream file(url, std::ios::binary | std::ios::ate);
324 spdlog::error(
"FontResourceHandler: cannot open '{}'", url);
328 const auto size = file.tellg();
331 auto result = std::make_unique<LoadedData>();
333 result->bytes.resize(
static_cast<size_t>(size));
334 file.read(
reinterpret_cast<char*
>(result->bytes.data()), size);
337 spdlog::error(
"FontResourceHandler: read error for '{}'", url);
341 spdlog::info(
"FontResourceHandler: read {} bytes from '{}'", result->bytes.size(), url);
std::unique_ptr< LoadedData > load(const std::string &url) override
std::unique_ptr< LoadedData > load(const std::string &url) override
static bool loadImageData(tinygltf::Image *image, int imageIndex, std::string *err, std::string *warn, int reqWidth, int reqHeight, const unsigned char *bytes, int size, void *userData)
static PreparedGlbData prepareFromModel(tinygltf::Model &model)
ResourceLoader(const std::shared_ptr< Engine > &engine)
void processCompletions(int maxCompletions=0)
void removeHandler(const std::string &type)
void load(const std::string &url, const std::string &type, LoadSuccessCallback onSuccess, LoadErrorCallback onError=nullptr)
void addHandler(const std::string &type, std::unique_ptr< ResourceHandler > handler)
std::unique_ptr< LoadedData > load(const std::string &url) override
std::function< void(std::unique_ptr< LoadedData >)> LoadSuccessCallback
std::function< void(const std::string &error)> LoadErrorCallback
Decoded pixel data — populated only by TextureResourceHandler.
std::vector< uint8_t > pixels
LDR: RGBA8 interleaved.
std::vector< float > hdrPixels
HDR: RGBA32F interleaved.