Files
filament/libs/gltfio/src/StbProvider.cpp
Powei Feng b0a584c915 Small compilation fixes (#7363)
- gltfio: Enable -Wall -Werror for gltfio_core
 - gltfio: Fix various errors that were missed warnings
 - matdbg: switch from std::atomic_uint64_t to
   std::atomic<uint64_t> for older clang
2023-11-14 13:26:48 -08:00

283 lines
9.8 KiB
C++

/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <gltfio/TextureProvider.h>
#include <string>
#include <vector>
#include <utils/JobSystem.h>
#include <utils/Log.h>
#include <filament/Engine.h>
#include <filament/Texture.h>
#include <stb_image.h>
using namespace filament;
using namespace utils;
using std::atomic;
using std::vector;
using std::unique_ptr;
namespace filament::gltfio {
class StbProvider final : public TextureProvider {
public:
StbProvider(Engine* engine);
~StbProvider();
Texture* pushTexture(const uint8_t* data, size_t byteCount,
const char* mimeType, TextureFlags flags) final;
Texture* popTexture() final;
void updateQueue() final;
void waitForCompletion() final;
void cancelDecoding() final;
const char* getPushMessage() const final;
const char* getPopMessage() const final;
size_t getPushedCount() const final { return mPushedCount; }
size_t getPoppedCount() const final { return mPoppedCount; }
size_t getDecodedCount() const final { return mDecodedCount; }
private:
enum class TextureState {
DECODING, // Texture has been pushed, mipmap levels are not yet complete.
READY, // Mipmap levels are available but texture has not been popped yet.
POPPED, // Client has popped the texture from the queue.
};
struct TextureInfo {
Texture* texture;
TextureState state;
atomic<intptr_t> decodedTexelsBaseMipmap;
vector<uint8_t> sourceBuffer;
JobSystem::Job* decoderJob;
};
// Declare some sentinel values for the "decodedTexelsBaseMipmap" field.
// Note that the "state" field can be modified only on the foreground thread.
static const intptr_t DECODING_NOT_READY = 0x0;
static const intptr_t DECODING_ERROR = 0x1;
void decodeSingleTexture();
size_t mPushedCount = 0;
size_t mPoppedCount = 0;
size_t mDecodedCount = 0;
vector<unique_ptr<TextureInfo> > mTextures;
JobSystem::Job* mDecoderRootJob;
std::string mRecentPushMessage;
std::string mRecentPopMessage;
Engine* const mEngine;
};
Texture* StbProvider::pushTexture(const uint8_t* data, size_t byteCount,
const char* mimeType, TextureFlags flags) {
int width, height, numComponents;
if (!stbi_info_from_memory(data, byteCount, &width, &height, &numComponents)) {
mRecentPushMessage = std::string("Unable to parse texture: ") + stbi_failure_reason();
return nullptr;
}
using InternalFormat = Texture::InternalFormat;
Texture* texture = Texture::Builder()
.width(width)
.height(height)
.levels(0xff)
.format(any(flags & TextureFlags::sRGB) ? InternalFormat::SRGB8_A8 : InternalFormat::RGBA8)
.build(*mEngine);
if (texture == nullptr) {
mRecentPushMessage = "Unable to build Texture object.";
return nullptr;
}
mRecentPushMessage.clear();
TextureInfo* info = mTextures.emplace_back(new TextureInfo).get();
++mPushedCount;
info->texture = texture;
info->state = TextureState::DECODING;
info->sourceBuffer.assign(data, data + byteCount);
info->decodedTexelsBaseMipmap.store(DECODING_NOT_READY);
// On single threaded systems, it is usually fine to create jobs because the job system will
// simply execute serially. However in our case, we wish to amortize the decoder cost across
// several frames, so we instead use the updateQueue() method to perform decoding.
if constexpr (!UTILS_HAS_THREADING) {
info->decoderJob = nullptr;
return texture;
}
JobSystem* js = &mEngine->getJobSystem();
info->decoderJob = jobs::createJob(*js, mDecoderRootJob, [info] {
auto& source = info->sourceBuffer;
int width, height, comp;
// Test asynchronous loading by uncommenting this line.
// std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 10000));
stbi_uc* texels = stbi_load_from_memory(source.data(), source.size(),
&width, &height, &comp, 4);
source.clear();
source.shrink_to_fit();
info->decodedTexelsBaseMipmap.store(texels ? intptr_t(texels) : DECODING_ERROR);
});
js->runAndRetain(info->decoderJob);
return texture;
}
Texture* StbProvider::popTexture() {
// We don't bother shrinking the mTextures vector here, instead we periodically clean it up in
// the updateQueue method, since popTexture is typically called more frequently. Textures
// can become ready in non-deterministic order due to concurrency.
for (auto& texture : mTextures) {
if (texture->state == TextureState::READY) {
texture->state = TextureState::POPPED;
++mPoppedCount;
const intptr_t ptr = texture->decodedTexelsBaseMipmap.load();
if (ptr == DECODING_ERROR || ptr == DECODING_NOT_READY) {
mRecentPopMessage = "Texture is incomplete";
} else {
mRecentPopMessage.clear();
}
return texture->texture;
}
}
return nullptr;
}
void StbProvider::updateQueue() {
if (!UTILS_HAS_THREADING) {
decodeSingleTexture();
}
JobSystem* js = &mEngine->getJobSystem();
for (auto& info : mTextures) {
if (info->state != TextureState::DECODING) {
continue;
}
Texture* texture = info->texture;
if (intptr_t data = info->decodedTexelsBaseMipmap.load()) {
if (info->decoderJob) {
js->waitAndRelease(info->decoderJob);
}
if (data == DECODING_ERROR) {
info->state = TextureState::READY;
++mDecodedCount;
continue;
}
Texture::PixelBufferDescriptor pbd((uint8_t*) data,
texture->getWidth() * texture->getHeight() * 4, Texture::Format::RGBA,
Texture::Type::UBYTE, [](void* mem, size_t, void*) { stbi_image_free(mem); });
texture->setImage(*mEngine, 0, std::move(pbd));
// Call generateMipmaps unconditionally to fulfill the promise of the TextureProvider
// interface. Providers of hierarchical images (e.g. KTX) call this only if needed.
texture->generateMipmaps(*mEngine);
info->state = TextureState::READY;
++mDecodedCount;
}
}
// Here we periodically clean up the "queue" (which is really just a vector) by removing unused
// items from the front. This might ignore a popped texture that occurs in the middle of the
// vector, but that's okay, it will be cleaned up eventually.
decltype(mTextures)::iterator last = mTextures.begin();
while (last != mTextures.end() && (*last)->state == TextureState::POPPED) ++last;
mTextures.erase(mTextures.begin(), last);
}
void StbProvider::waitForCompletion() {
JobSystem& js = mEngine->getJobSystem();
for (auto& info : mTextures) {
if (info->decoderJob) {
js.waitAndRelease(info->decoderJob);
}
}
}
void StbProvider::cancelDecoding() {
// TODO: Currently, StbProvider runs jobs eagerly and JobSystem does not allow cancellation of
// in-flight jobs. We should consider throttling the number of simultaneous decoder jobs, which
// would allow for actual cancellation.
waitForCompletion();
// For cancelled jobs, we need to set the TextureInfo to the popped state and free the decoded
// data.
for (auto& info : mTextures) {
if (info->state != TextureState::DECODING) {
continue;
}
// Deleting data here should be safe thread-wise as the only other place where
// decodedTexelsBaseMipmap is loaded is in the job threads, and we have waited them to
// completion above. We also expect the TextureProvider API calls to be made only from one
// thread.
if (intptr_t data = info->decodedTexelsBaseMipmap.load()) {
delete [] (uint8_t*) data;
}
info->state = TextureState::POPPED;
}
}
const char* StbProvider::getPushMessage() const {
return mRecentPushMessage.empty() ? nullptr : mRecentPushMessage.c_str();
}
const char* StbProvider::getPopMessage() const {
return mRecentPopMessage.empty() ? nullptr : mRecentPopMessage.c_str();
}
void StbProvider::decodeSingleTexture() {
assert_invariant(!UTILS_HAS_THREADING);
for (auto& info : mTextures) {
if (info->state == TextureState::DECODING) {
auto& source = info->sourceBuffer;
int width, height, comp;
stbi_uc* texels = stbi_load_from_memory(source.data(), source.size(),
&width, &height, &comp, 4);
source.clear();
source.shrink_to_fit();
info->decodedTexelsBaseMipmap.store(texels ? intptr_t(texels) : DECODING_ERROR);
break;
}
}
}
StbProvider::StbProvider(Engine* engine) : mEngine(engine) {
mDecoderRootJob = mEngine->getJobSystem().createJob();
#ifndef NDEBUG
slog.i << "Texture Decoder has "
<< mEngine->getJobSystem().getThreadCount()
<< " background threads." << io::endl;
#endif
}
StbProvider::~StbProvider() {
cancelDecoding();
mEngine->getJobSystem().release(mDecoderRootJob);
}
TextureProvider* createStbProvider(Engine* engine) {
return new StbProvider(engine);
}
} // namespace filament::gltfio