gltfio: Introduce the asynchronous API and use it.

We were already using jobs for decoding PNG and JPEG files, but we were
doing a join. This add three methods to ResourceLoader that allow
clients to amortize the decoding process across multiple frames, even on
single-threaded platforms like WebGL.

This PR adds async loading to the following demos:
- samples/gltf_viewer (now shows a progress bar in the UI)
- android/sample-gltf-viewer
- web/samples/helmet.html

Fixes #1876.
This commit is contained in:
Philip Rideout
2020-01-29 11:23:14 -08:00
parent 503e66790b
commit c681cd243a
9 changed files with 280 additions and 77 deletions

View File

@@ -28,15 +28,15 @@
#include <geometry/SurfaceOrientation.h>
#include <math/quat.h>
#include <math/vec3.h>
#include <math/vec4.h>
#include <utils/JobSystem.h>
#include <utils/Log.h>
#include <cgltf.h>
#include <math/quat.h>
#include <math/vec3.h>
#include <math/vec4.h>
#include <tsl/robin_map.h>
#include <string>
@@ -48,7 +48,6 @@ using namespace utils;
static const auto FREE_CALLBACK = [](void* mem, size_t, void*) { free(mem); };
namespace {
struct TextureCacheEntry {
Texture* texture;
std::atomic<stbi_uc*> texels;
@@ -62,6 +61,7 @@ namespace {
using BufferTextureCache = tsl::robin_map<const void*, std::unique_ptr<TextureCacheEntry>>;
using UrlTextureCache = tsl::robin_map<std::string, std::unique_ptr<TextureCacheEntry>>;
using UriDataCache = tsl::robin_map<std::string, gltfio::ResourceLoader::BufferDescriptor>;
}
namespace gltfio {
@@ -69,24 +69,24 @@ namespace gltfio {
struct ResourceLoader::Impl {
ResourceConfiguration mConfig;
// User-provided resource data with URL string keys, populated via addResourceData().
// User-provided resource data with URI string keys, populated with addResourceData().
// This is used on platforms without traditional file systems, such as Android and WebGL.
tsl::robin_map<std::string, BufferDescriptor> mUserCache;
UriDataCache mUriDataCache;
// The resource loader's transient texture cache holds decoded texture data during
// loadResources(). It is discarded when all Filament Texture objects have been created and
// uploaded. Since multiple glTF textures might be loaded from a single URL or buffer pointer,
// the cache prevents needless re-decoding. The cache is split into two maps: one for URL-based
// textures and one for buffer-based textures.
// The two texture caches are populated while textures are being decoded, and they are no longer
// used after all textures have been finalized. Since multiple glTF textures might be loaded
// from a single URL or buffer pointer, these caches prevent needless re-decoding. There are
// two caches: one for URL-based textures and one for buffer-based textures.
BufferTextureCache mBufferTextureCache;
UrlTextureCache mUrlTextureCache;
int mNumNewCacheEntries;
std::atomic<int> mNumReadyEntries;
utils::JobSystem::Job* mDecodingJob = nullptr;
int mNumDecoderTasks;
int mNumDecoderTasksFinished;
utils::JobSystem::Job* mDecoderRootJob = nullptr;
bool createTextures(details::FFilamentAsset* asset, bool async);
void addTextureCacheEntry(const TextureBinding& tb);
void bindTextureToMaterial(const TextureBinding& tb);
void decodeSingleTexture();
void uploadPendingTextures();
~Impl();
};
@@ -157,11 +157,11 @@ static void importSkinningData(Skin& dstSkin, const cgltf_skin& srcSkin) {
}
void ResourceLoader::addResourceData(const char* url, BufferDescriptor&& buffer) {
pImpl->mUserCache.emplace(url, std::move(buffer));
pImpl->mUriDataCache.emplace(url, std::move(buffer));
}
bool ResourceLoader::hasResourceData(const char* url) const {
return pImpl->mUserCache.find(url) != pImpl->mUserCache.end();
return pImpl->mUriDataCache.find(url) != pImpl->mUriDataCache.end();
}
static void convertBytesToShorts(uint16_t* dst, const uint8_t* src, size_t count) {
@@ -227,8 +227,8 @@ bool ResourceLoader::loadResources(FFilamentAsset* asset, bool async) {
return false;
}
} else if (strstr(uri, "://") == nullptr) {
auto iter = pImpl->mUserCache.find(uri);
if (iter == pImpl->mUserCache.end()) {
auto iter = pImpl->mUriDataCache.find(uri);
if (iter == pImpl->mUriDataCache.end()) {
slog.e << "Unable to load external resource: " << uri << io::endl;
missingResources = true;
}
@@ -341,6 +341,68 @@ bool ResourceLoader::loadResources(FFilamentAsset* asset, bool async) {
return pImpl->createTextures(asset, async);
}
bool ResourceLoader::asyncBeginLoad(FilamentAsset* asset) {
return loadResources(upcast(asset), true);
}
float ResourceLoader::asyncGetLoadProgress() const {
const float finished = pImpl->mNumDecoderTasksFinished;
const float total = pImpl->mNumDecoderTasks;
return total == 0 ? 0 : finished / total;
}
void ResourceLoader::asyncUpdateLoad() {
if (!UTILS_HAS_THREADING) {
pImpl->decodeSingleTexture();
}
pImpl->uploadPendingTextures();
}
void ResourceLoader::Impl::decodeSingleTexture() {
assert(!UTILS_HAS_THREADING);
int w, h, c;
// Check if any buffer-based textures haven't been decoded yet.
for (auto& pair : mBufferTextureCache) {
const uint8_t* sourceData = (const uint8_t*) pair.first;
TextureCacheEntry* entry = pair.second.get();
if (entry->texels) {
continue;
}
entry->texels = stbi_load_from_memory(sourceData, entry->bufferSize, &w, &h, &c, 4);
return;
}
// Check if any URL-based textures haven't been decoded yet.
for (auto& pair : mUrlTextureCache) {
auto uri = pair.first;
TextureCacheEntry* entry = pair.second.get();
if (entry->texels) {
continue;
}
// First, check the user-supplied resource cache for this URL.
auto iter = mUriDataCache.find(uri);
if (iter != mUriDataCache.end()) {
const uint8_t* sourceData = (const uint8_t*) iter->second.buffer;
entry->texels = stbi_load_from_memory(sourceData, iter->second.size, &w, &h, &c, 4);
return;
}
// Otherwise load it from the file system if this platform supports it.
#if defined(STBI_NO_STDIO)
slog.e << "Unable to load texture: " << uri << io::endl;
entry->completed = true;
mNumDecoderTasksFinished++;
return;
#else
utils::Path fullpath = this->mConfig.gltfPath.getParent() + uri;
entry->texels = stbi_load(fullpath.c_str(), &w, &h, &c, 4);
return;
#endif
}
}
void ResourceLoader::Impl::uploadPendingTextures() {
auto upload = [this](TextureCacheEntry* entry, Engine& engine) {
Texture* texture = entry->texture;
@@ -352,7 +414,7 @@ void ResourceLoader::Impl::uploadPendingTextures() {
texture->setImage(engine, 0, std::move(pbd));
texture->generateMipmaps(engine);
entry->completed = true;
mNumReadyEntries++;
mNumDecoderTasksFinished++;
}
};
Engine& engine = *mConfig.engine;
@@ -375,7 +437,6 @@ void ResourceLoader::Impl::addTextureCacheEntry(const TextureBinding& tb) {
entry->srgb = tb.srgb;
stbi_info_from_memory(sourceData, tb.totalSize, &entry->width, &entry->height,
&entry->numComponents);
mNumNewCacheEntries++;
entry->bufferSize = tb.totalSize;
return;
}
@@ -390,12 +451,11 @@ void ResourceLoader::Impl::addTextureCacheEntry(const TextureBinding& tb) {
entry->srgb = tb.srgb;
// Check the user-supplied resource cache for this URL, otherwise peek at the file.
auto iter = mUserCache.find(tb.uri);
if (iter != mUserCache.end()) {
auto iter = mUriDataCache.find(tb.uri);
if (iter != mUriDataCache.end()) {
const uint8_t* sourceData = (const uint8_t*) iter->second.buffer;
stbi_info_from_memory(sourceData, iter->second.size, &entry->width,
&entry->height, &entry->numComponents);
mNumNewCacheEntries++;
return;
}
#if defined(STBI_NO_STDIO)
@@ -403,7 +463,6 @@ void ResourceLoader::Impl::addTextureCacheEntry(const TextureBinding& tb) {
#else
utils::Path fullpath = directory + tb.uri;
stbi_info(fullpath.c_str(), &entry->width, &entry->height, &entry->numComponents);
mNumNewCacheEntries++;
#endif
}
@@ -428,12 +487,11 @@ void ResourceLoader::Impl::bindTextureToMaterial(const TextureBinding& tb) {
bool ResourceLoader::Impl::createTextures(details::FFilamentAsset* asset, bool async) {
// If any decoding jobs are still underway, wait for them to finish.
utils::JobSystem* js = utils::JobSystem::getJobSystem();
if (mDecodingJob) {
js->waitAndRelease(mDecodingJob);
mDecodingJob = nullptr;
if (mDecoderRootJob) {
js->waitAndRelease(mDecoderRootJob);
mDecoderRootJob = nullptr;
}
mNumNewCacheEntries = 0;
mBufferTextureCache.clear();
mUrlTextureCache.clear();
@@ -442,6 +500,17 @@ bool ResourceLoader::Impl::createTextures(details::FFilamentAsset* asset, bool a
addTextureCacheEntry(asset->getTextureBindings()[i]);
}
// Tally up the total number of textures that need to be decoded. Zero textures is a special
// case that needs to report 100% progress right away, so we set NumDecoderTasks and Finished
// both to 1. If they were both 0, this would indicate that loading has not started.
mNumDecoderTasks = mBufferTextureCache.size() + mUrlTextureCache.size();
if (mNumDecoderTasks == 0) {
mNumDecoderTasks = 1;
mNumDecoderTasksFinished = 1;
} else {
mNumDecoderTasksFinished = 0;
}
// Next create blank Filament textures.
auto createTexture = [=](TextureCacheEntry* entry) {
entry->texture = Texture::Builder()
@@ -460,8 +529,15 @@ bool ResourceLoader::Impl::createTextures(details::FFilamentAsset* asset, bool a
bindTextureToMaterial(asset->getTextureBindings()[i]);
}
// Before creating jobs for PNG / JPEG decoding, we might need to return early. On single
// threaded systems, it is usually fine to create jobs because the job system will simply
// execute serially. However if the client requests async behavior, then we need to wait
// until subsequent calls to asyncUpdateLoad().
if (!UTILS_HAS_THREADING && async) {
return true;
}
utils::JobSystem::Job* parent = js->createJob();
mNumReadyEntries = 0;
// Kick off jobs that decode texels from buffer pointers.
for (auto& pair : mBufferTextureCache) {
@@ -475,14 +551,14 @@ bool ResourceLoader::Impl::createTextures(details::FFilamentAsset* asset, bool a
js->run(decode);
}
// Kick off jobs that decode texels from URL strings.
// Kick off jobs that decode texels from URI strings.
for (auto& pair : mUrlTextureCache) {
auto uri = pair.first;
TextureCacheEntry* entry = pair.second.get();
// First, check the user-supplied resource cache for this URL.
auto iter = mUserCache.find(uri);
if (iter != mUserCache.end()) {
// First, check the user-supplied resource cache for this URI.
auto iter = mUriDataCache.find(uri);
if (iter != mUriDataCache.end()) {
const uint8_t* sourceData = (const uint8_t*) iter->second.buffer;
utils::JobSystem::Job* decode = utils::jobs::createJob(*js, parent, [=] {
int width, height, comp;
@@ -508,7 +584,7 @@ bool ResourceLoader::Impl::createTextures(details::FFilamentAsset* asset, bool a
}
if (async) {
mDecodingJob = js->runAndRetain(parent);
mDecoderRootJob = js->runAndRetain(parent);
return true;
}
@@ -523,8 +599,8 @@ bool ResourceLoader::Impl::createTextures(details::FFilamentAsset* asset, bool a
ResourceLoader::Impl::~Impl() {
utils::JobSystem* js = utils::JobSystem::getJobSystem();
if (mDecodingJob) {
js->waitAndRelease(mDecodingJob);
if (mDecoderRootJob) {
js->waitAndRelease(mDecoderRootJob);
}
}