manual OpenGL texture instantiation on Android

This commit is contained in:
Nick Fisher
2023-09-22 08:17:50 +08:00
parent 8bdaf0afa8
commit fcad31806f
9 changed files with 798 additions and 2445 deletions

View File

@@ -0,0 +1,26 @@
#include <android/native_window_jni.h>
#include <android/native_activity.h>
extern "C" {
#include "PolyvoxFilamentApi.h"
const void* create_filament_viewer_android(
jobject surface, JNIEnv* env, ResourceLoaderWrapper* loaderWrapper
) {
ANativeWindow* window = ANativeWindow_fromSurface(env, surface);
return create_filament_viewer(window,loaderWrapper);
}
void create_swap_chain_android(
const void* const viewer,
jobject surface,
JNIEnv* env,
uint32_t width,
uint32_t height
) {
ANativeWindow* window = ANativeWindow_fromSurface(env, surface);
create_swap_chain(viewer, window, width, height);
}
}

View File

@@ -1,610 +0,0 @@
/*
* Copyright (C) 2016 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.
*/
// Note: The overhead of SYSTRACE_TAG_JOBSYSTEM is not negligible especially with parallel_for().
#ifndef SYSTRACE_TAG
//#define SYSTRACE_TAG SYSTRACE_TAG_JOBSYSTEM
#define SYSTRACE_TAG SYSTRACE_TAG_NEVER
#endif
// when SYSTRACE_TAG_JOBSYSTEM is used, enables even heavier systraces
#define HEAVY_SYSTRACE 0
// enable for catching hangs waiting on a job to finish
static constexpr bool DEBUG_FINISH_HANGS = false;
#include <utils/JobSystem.h>
#include <utils/compiler.h>
#include <utils/memalign.h>
#include <utils/Panic.h>
#include <utils/Systrace.h>
#include <random>
#include <math.h>
#if !defined(WIN32)
# include <pthread.h>
#endif
#ifdef __ANDROID__
# include <sys/time.h>
# include <sys/resource.h>
# ifndef ANDROID_PRIORITY_URGENT_DISPLAY
# define ANDROID_PRIORITY_URGENT_DISPLAY -8 // see include/system/thread_defs.h
# endif
# ifndef ANDROID_PRIORITY_DISPLAY
# define ANDROID_PRIORITY_DISPLAY -4 // see include/system/thread_defs.h
# endif
# ifndef ANDROID_PRIORITY_NORMAL
# define ANDROID_PRIORITY_NORMAL 0 // see include/system/thread_defs.h
# endif
#elif defined(__linux__)
// There is no glibc wrapper for gettid on linux so we need to syscall it.
# include <unistd.h>
# include <sys/syscall.h>
# define gettid() syscall(SYS_gettid)
#endif
#if HEAVY_SYSTRACE
# define HEAVY_SYSTRACE_CALL() SYSTRACE_CALL()
# define HEAVY_SYSTRACE_NAME(name) SYSTRACE_NAME(name)
# define HEAVY_SYSTRACE_VALUE32(name, v) SYSTRACE_VALUE32(name, v)
#else
# define HEAVY_SYSTRACE_CALL()
# define HEAVY_SYSTRACE_NAME(name)
# define HEAVY_SYSTRACE_VALUE32(name, v)
#endif
namespace utils {
void JobSystem::setThreadName(const char* name) noexcept {
#if defined(__linux__)
pthread_setname_np(pthread_self(), name);
#elif defined(__APPLE__)
pthread_setname_np(name);
#else
// TODO: implement setting thread name on WIN32
#endif
}
void JobSystem::setThreadPriority(Priority priority) noexcept {
#ifdef __ANDROID__
int androidPriority = 0;
switch (priority) {
case Priority::NORMAL:
androidPriority = ANDROID_PRIORITY_NORMAL;
break;
case Priority::DISPLAY:
androidPriority = ANDROID_PRIORITY_DISPLAY;
break;
case Priority::URGENT_DISPLAY:
androidPriority = ANDROID_PRIORITY_URGENT_DISPLAY;
break;
}
setpriority(PRIO_PROCESS, 0, androidPriority);
#endif
}
void JobSystem::setThreadAffinityById(size_t id) noexcept {
#if defined(__linux__)
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(id, &set);
sched_setaffinity(gettid(), sizeof(set), &set);
#endif
}
JobSystem::JobSystem(const size_t userThreadCount, const size_t adoptableThreadsCount) noexcept
: mJobPool("JobSystem Job pool", MAX_JOB_COUNT * sizeof(Job)),
mJobStorageBase(static_cast<Job *>(mJobPool.getAllocator().getCurrent()))
{
SYSTRACE_ENABLE();
int threadPoolCount = userThreadCount;
if (threadPoolCount == 0) {
// default value, system dependant
int hwThreads = std::thread::hardware_concurrency();
if (UTILS_HAS_HYPER_THREADING) {
// For now we avoid using HT, this simplifies profiling.
// TODO: figure-out what to do with Hyper-threading
// since we assumed HT, always round-up to an even number of cores (to play it safe)
hwThreads = (hwThreads + 1) / 2;
}
// one of the thread will be the user thread
threadPoolCount = hwThreads - 1;
}
// make sure we have at least one thread in the thread pool
threadPoolCount = std::max(1, threadPoolCount);
// and also limit the pool to 32 threads
threadPoolCount = std::min(UTILS_HAS_THREADING ? 32 : 0, threadPoolCount);
mThreadStates = aligned_vector<ThreadState>(threadPoolCount + adoptableThreadsCount);
mThreadCount = uint16_t(threadPoolCount);
mParallelSplitCount = (uint8_t)std::ceil((std::log2f(threadPoolCount + adoptableThreadsCount)));
static_assert(std::atomic<bool>::is_always_lock_free);
static_assert(std::atomic<uint16_t>::is_always_lock_free);
std::random_device rd;
const size_t hardwareThreadCount = mThreadCount;
auto& states = mThreadStates;
#pragma nounroll
for (size_t i = 0, n = states.size(); i < n; i++) {
auto& state = states[i];
state.rndGen = default_random_engine(rd());
state.id = (uint32_t)i;
state.js = this;
if (i < hardwareThreadCount) {
// don't start a thread of adoptable thread slots
state.thread = std::thread(&JobSystem::loop, this, &state);
}
}
}
JobSystem::~JobSystem() {
requestExit();
#pragma nounroll
for (auto &state : mThreadStates) {
// adopted threads are not joinable
if (state.thread.joinable()) {
state.thread.join();
}
}
}
inline void JobSystem::incRef(Job const* job) noexcept {
// no action is taken when incrementing the reference counter, therefore we can safely use
// memory_order_relaxed.
job->refCount.fetch_add(1, std::memory_order_relaxed);
}
UTILS_NOINLINE
void JobSystem::decRef(Job const* job) noexcept {
// We must ensure that accesses from other threads happen before deleting the Job.
// To accomplish this, we need to guarantee that no read/writes are reordered after the
// dec-ref, because ANOTHER thread could hold the last reference (after us) and that thread
// needs to see all accesses completed before it deletes the object. This is done
// with memory_order_release.
// Similarly, we need to guarantee that no read/write are reordered before the last decref,
// or some other thread could see a destroyed object before the ref-count is 0. This is done
// with memory_order_acquire.
auto c = job->refCount.fetch_sub(1, std::memory_order_acq_rel);
assert(c > 0);
if (c == 1) {
// This was the last reference, it's safe to destroy the job.
mJobPool.destroy(job);
}
}
void JobSystem::requestExit() noexcept {
mExitRequested.store(true);
std::lock_guard<Mutex> lock(mWaiterLock);
mWaiterCondition.notify_all();
}
inline bool JobSystem::exitRequested() const noexcept {
// memory_order_relaxed is safe because the only action taken is to exit the thread
return mExitRequested.load(std::memory_order_relaxed);
}
inline bool JobSystem::hasActiveJobs() const noexcept {
return mActiveJobs.load(std::memory_order_relaxed) > 0;
}
inline bool JobSystem::hasJobCompleted(JobSystem::Job const* job) noexcept {
return job->runningJobCount.load(std::memory_order_acquire) <= 0;
}
void JobSystem::wait(std::unique_lock<Mutex>& lock, Job* job) noexcept {
if constexpr (!DEBUG_FINISH_HANGS) {
mWaiterCondition.wait(lock);
} else {
do {
// we use a pretty long timeout (4s) so we're very confident that the system is hung
// and nothing else is happening.
std::cv_status status = mWaiterCondition.wait_for(lock,
std::chrono::milliseconds(4000));
if (status == std::cv_status::no_timeout) {
break;
}
// hang debugging...
// we check of we had active jobs or if the job we're waiting on had completed already.
// there is the possibility of a race condition, but our long timeout gives us some
// confidence that we're in an incorrect state.
auto id = getState().id;
auto activeJobs = mActiveJobs.load();
if (job) {
auto runningJobCount = job->runningJobCount.load();
ASSERT_POSTCONDITION(runningJobCount > 0,
"JobSystem(%p, %d): waiting while job %p has completed and %d jobs are active!",
this, id, job, activeJobs);
}
ASSERT_POSTCONDITION(activeJobs <= 0,
"JobSystem(%p, %d): waiting while %d jobs are active!",
this, id, activeJobs);
} while (true);
}
}
void JobSystem::wakeAll() noexcept {
HEAVY_SYSTRACE_CALL();
std::lock_guard<Mutex> lock(mWaiterLock);
// this empty critical section is needed -- it guarantees that notify_all() happens
// after the condition's variables are set.
mWaiterCondition.notify_all();
}
void JobSystem::wakeOne() noexcept {
HEAVY_SYSTRACE_CALL();
std::lock_guard<Mutex> lock(mWaiterLock);
// this empty critical section is needed -- it guarantees that notify_one() happens
// after the condition's variables are set.
mWaiterCondition.notify_one();
}
inline JobSystem::ThreadState& JobSystem::getState() noexcept {
std::lock_guard<utils::SpinLock> lock(mThreadMapLock);
auto iter = mThreadMap.find(std::this_thread::get_id());
ASSERT_PRECONDITION(iter != mThreadMap.end(), "This thread has not been adopted.");
return *iter->second;
}
JobSystem::Job* JobSystem::allocateJob() noexcept {
return mJobPool.make<Job>();
}
void JobSystem::put(WorkQueue& workQueue, Job* job) noexcept {
assert(job);
size_t index = job - mJobStorageBase;
assert(index >= 0 && index < MAX_JOB_COUNT);
// put the job into the queue first
workQueue.push(uint16_t(index + 1));
// then increase our active job count
uint32_t oldActiveJobs = mActiveJobs.fetch_add(1, std::memory_order_relaxed);
// but it's possible that the job has already been picked-up, so oldActiveJobs could be
// negative for instance. We signal only if that's not the case.
if (oldActiveJobs >= 0) {
wakeOne(); // wake-up a thread if needed...
}
}
JobSystem::Job* JobSystem::pop(WorkQueue& workQueue) noexcept {
// decrement mActiveJobs first, this is to ensure that if there is only a single job left
// (and we're about to pick it up), other threads don't loop trying to do the same.
mActiveJobs.fetch_sub(1, std::memory_order_relaxed);
size_t index = workQueue.pop();
assert(index <= MAX_JOB_COUNT);
Job* job = !index ? nullptr : &mJobStorageBase[index - 1];
// if our guess was wrong, i.e. we couldn't pick-up a job (b/c our queue was empty), we
// need to correct mActiveJobs.
if (!job) {
if (mActiveJobs.fetch_add(1, std::memory_order_relaxed) >= 0) {
// and if there are some active jobs, then we need to wake someone up. We know it
// can't be us, because we failed taking a job and we know another thread can't
// have added one in our queue.
wakeOne();
}
}
return job;
}
JobSystem::Job* JobSystem::steal(WorkQueue& workQueue) noexcept {
// decrement mActiveJobs first, this is to ensure that if there is only a single job left
// (and we're about to pick it up), other threads don't loop trying to do the same.
mActiveJobs.fetch_sub(1, std::memory_order_relaxed);
size_t index = workQueue.steal();
assert(index <= MAX_JOB_COUNT);
Job* job = !index ? nullptr : &mJobStorageBase[index - 1];
// if we failed taking a job, we need to correct mActiveJobs
if (!job) {
if (mActiveJobs.fetch_add(1, std::memory_order_relaxed) >= 0) {
// and if there are some active jobs, then we need to wake someone up. We know it
// can't be us, because we failed taking a job and we know another thread can't
// have added one in our queue.
wakeOne();
}
}
return job;
}
inline JobSystem::ThreadState* JobSystem::getStateToStealFrom(JobSystem::ThreadState& state) noexcept {
auto& threadStates = mThreadStates;
// memory_order_relaxed is okay because we don't take any action that has data dependency
// on this value (in particular mThreadStates, is always initialized properly).
uint16_t adopted = mAdoptedThreads.load(std::memory_order_relaxed);
uint16_t const threadCount = mThreadCount + adopted;
JobSystem::ThreadState* stateToStealFrom = nullptr;
// don't try to steal from someone else if we're the only thread (infinite loop)
if (threadCount >= 2) {
do {
// this is biased, but frankly, we don't care. it's fast.
uint16_t index = uint16_t(state.rndGen() % threadCount);
assert(index < threadStates.size());
stateToStealFrom = &threadStates[index];
// don't steal from our own queue
} while (stateToStealFrom == &state);
}
return stateToStealFrom;
}
JobSystem::Job* JobSystem::steal(JobSystem::ThreadState& state) noexcept {
HEAVY_SYSTRACE_CALL();
Job* job = nullptr;
do {
ThreadState* const stateToStealFrom = getStateToStealFrom(state);
if (UTILS_LIKELY(stateToStealFrom)) {
job = steal(stateToStealFrom->workQueue);
}
// nullptr -> nothing to steal in that queue either, if there are active jobs,
// continue to try stealing one.
} while (!job && hasActiveJobs());
return job;
}
bool JobSystem::execute(JobSystem::ThreadState& state) noexcept {
HEAVY_SYSTRACE_CALL();
Job* job = pop(state.workQueue);
if (UTILS_UNLIKELY(job == nullptr)) {
// our queue is empty, try to steal a job
job = steal(state);
}
if (job) {
assert(job->runningJobCount.load(std::memory_order_relaxed) >= 1);
if (UTILS_LIKELY(job->function)) {
HEAVY_SYSTRACE_NAME("job->function");
job->function(job->storage, *this, job);
}
finish(job);
}
return job != nullptr;
}
void JobSystem::loop(ThreadState* state) noexcept {
setThreadName("JobSystem::loop");
setThreadPriority(Priority::DISPLAY);
// set a CPU affinity on each of our JobSystem thread to prevent them from jumping from core
// to core. On Android, it looks like the affinity needs to be reset from time to time.
setThreadAffinityById(state->id);
// record our work queue
mThreadMapLock.lock();
bool inserted = mThreadMap.emplace(std::this_thread::get_id(), state).second;
mThreadMapLock.unlock();
ASSERT_PRECONDITION(inserted, "This thread is already in a loop.");
// run our main loop...
do {
if (!execute(*state)) {
std::unique_lock<Mutex> lock(mWaiterLock);
while (!exitRequested() && !hasActiveJobs()) {
wait(lock);
setThreadAffinityById(state->id);
}
}
} while (!exitRequested());
}
UTILS_NOINLINE
void JobSystem::finish(Job* job) noexcept {
HEAVY_SYSTRACE_CALL();
bool notify = false;
// terminate this job and notify its parent
Job* const storage = mJobStorageBase;
do {
// std::memory_order_release here is needed to synchronize with JobSystem::wait()
// which needs to "see" all changes that happened before the job terminated.
auto runningJobCount = job->runningJobCount.fetch_sub(1, std::memory_order_acq_rel);
assert(runningJobCount > 0);
if (runningJobCount == 1) {
// no more work, destroy this job and notify its parent
notify = true;
Job* const parent = job->parent == 0x7FFF ? nullptr : &storage[job->parent];
decRef(job);
job = parent;
} else {
// there is still work (e.g.: children), we're done.
break;
}
} while (job);
// wake-up all threads that could potentially be waiting on this job finishing
if (notify) {
wakeAll();
}
}
// -----------------------------------------------------------------------------------------------
// public API...
JobSystem::Job* JobSystem::create(JobSystem::Job* parent, JobFunc func) noexcept {
parent = (parent == nullptr) ? mRootJob : parent;
Job* const job = allocateJob();
if (UTILS_LIKELY(job)) {
size_t index = 0x7FFF;
if (parent) {
// add a reference to the parent to make sure it can't be terminated.
// memory_order_relaxed is safe because no action is taken at this point
// (the job is not started yet).
auto parentJobCount = parent->runningJobCount.fetch_add(1, std::memory_order_relaxed);
// can't create a child job of a terminated parent
assert(parentJobCount > 0);
index = parent - mJobStorageBase;
assert(index < MAX_JOB_COUNT);
}
job->function = func;
job->parent = uint16_t(index);
}
return job;
}
void JobSystem::cancel(Job*& job) noexcept {
finish(job);
job = nullptr;
}
JobSystem::Job* JobSystem::retain(JobSystem::Job* job) noexcept {
JobSystem::Job* retained = job;
incRef(retained);
return retained;
}
void JobSystem::release(JobSystem::Job*& job) noexcept {
decRef(job);
job = nullptr;
}
void JobSystem::signal() noexcept {
wakeAll();
}
void JobSystem::run(Job*& job) noexcept {
HEAVY_SYSTRACE_CALL();
ThreadState& state(getState());
put(state.workQueue, job);
// after run() returns, the job is virtually invalid (it'll die on its own)
job = nullptr;
}
JobSystem::Job* JobSystem::runAndRetain(Job* job) noexcept {
JobSystem::Job* retained = retain(job);
run(job);
return retained;
}
void JobSystem::waitAndRelease(Job*& job) noexcept {
SYSTRACE_CALL();
assert(job);
assert(job->refCount.load(std::memory_order_relaxed) >= 1);
ThreadState& state(getState());
do {
if (!execute(state)) {
// test if job has completed first, to possibly avoid taking the lock
if (hasJobCompleted(job)) {
break;
}
// the only way we can be here is if the job we're waiting on it being handled
// by another thread:
// - we returned from execute() which means all queues are empty
// - yet our job hasn't completed yet
// ergo, it's being run in another thread
//
// this could take time however, so we will wait with a condition, and
// continue to handle more jobs, as they get added.
std::unique_lock<Mutex> lock(mWaiterLock);
if (!hasJobCompleted(job) && !hasActiveJobs() && !exitRequested()) {
wait(lock, job);
}
}
} while (!hasJobCompleted(job) && !exitRequested());
if (job == mRootJob) {
mRootJob = nullptr;
}
release(job);
}
void JobSystem::runAndWait(JobSystem::Job*& job) noexcept {
runAndRetain(job);
waitAndRelease(job);
}
void JobSystem::adopt() {
const auto tid = std::this_thread::get_id();
std::unique_lock<utils::SpinLock> lock(mThreadMapLock);
auto iter = mThreadMap.find(tid);
ThreadState* const state = iter == mThreadMap.end() ? nullptr : iter->second;
lock.unlock();
if (state) {
// we're already part of a JobSystem, do nothing.
ASSERT_PRECONDITION(this == state->js,
"Called adopt on a thread owned by another JobSystem (%p), this=%p!",
state->js, this);
return;
}
// memory_order_relaxed is safe because we don't take action on this value.
uint16_t adopted = mAdoptedThreads.fetch_add(1, std::memory_order_relaxed);
size_t index = mThreadCount + adopted;
ASSERT_POSTCONDITION(index < mThreadStates.size(),
"Too many calls to adopt(). No more adoptable threads!");
// all threads adopted by the JobSystem need to run at the same priority
JobSystem::setThreadPriority(JobSystem::Priority::DISPLAY);
// This thread's queue will be selectable immediately (i.e.: before we set its TLS)
// however, it's not a problem since mThreadState is pre-initialized and valid
// (e.g.: the queue is empty).
lock.lock();
mThreadMap[tid] = &mThreadStates[index];
}
void JobSystem::emancipate() {
const auto tid = std::this_thread::get_id();
std::lock_guard<utils::SpinLock> lock(mThreadMapLock);
auto iter = mThreadMap.find(tid);
ThreadState* const state = iter == mThreadMap.end() ? nullptr : iter->second;
ASSERT_PRECONDITION(state, "this thread is not an adopted thread");
ASSERT_PRECONDITION(state->js == this, "this thread is not adopted by us");
mThreadMap.erase(iter);
}
io::ostream& operator<<(io::ostream& out, JobSystem const& js) {
for (auto const& item : js.mThreadStates) {
out << size_t(item.id) << ": " << item.workQueue.getCount() << io::endl;
}
return out;
}
} // namespace utils

View File

@@ -1,261 +0,0 @@
/*
* 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 <filament/Engine.h>
#include <filament/Texture.h>
#define STB_IMAGE_IMPLEMENTATION
#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, uint64_t 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, FlagBits 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;
const FlagBits sRGB = FlagBits(Flags::sRGB);
Texture* texture = Texture::Builder()
.width(width)
.height(height)
.levels(0xff)
.format((flags & 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, [this, 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();
}
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();
}
StbProvider::~StbProvider() {
cancelDecoding();
mEngine->getJobSystem().release(mDecoderRootJob);
}
TextureProvider* createStbProvider(Engine* engine) {
return new StbProvider(engine);
}
} // namespace filament::gltfio

View File

@@ -1,90 +0,0 @@
#include "FilamentViewer.hpp"
#include "SceneAsset.hpp"
#include "ResourceBuffer.hpp"
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
#include <android/native_window_jni.h>
#include <android/log.h>
#include <android/native_activity.h>
#include <map>
using namespace polyvox;
using namespace std;
static AAssetManager* am;
static map<uint32_t, AAsset*> _apk_assets;
static map<uint32_t, void*> _file_assets;
static uint32_t _i = 0;
static ResourceBuffer loadResource(const char* name) {
string name_str(name);
auto id = _i++;
if (name_str.rfind("file://", 0) == 0) {
streampos length;
ifstream is(name_str.substr(7), ios::binary);
is.seekg (0, ios::end);
length = is.tellg();
char * buffer;
buffer = new char [length];
is.seekg (0, ios::beg);
is.read (buffer, length);
is.close();
_file_assets[id] = buffer;
return ResourceBuffer(buffer, length, id);
} else {
AAsset *asset = AAssetManager_open(am, name, AASSET_MODE_BUFFER);
if(asset == nullptr) {
__android_log_print(ANDROID_LOG_VERBOSE, "filament_api", "Couldn't locate asset [ %s ]", name);
return ResourceBuffer(nullptr, 0, 0);
}
__android_log_print(ANDROID_LOG_VERBOSE, "filament_api", "Loading asset [ %s ]", name);
off_t length = AAsset_getLength(asset);
const void * buffer = AAsset_getBuffer(asset);
__android_log_print(ANDROID_LOG_VERBOSE, "filament_api", "Read [ %lu ] bytes into buffer", length);
_apk_assets[id] = asset;
__android_log_print(ANDROID_LOG_VERBOSE, "filament_api", "Loaded asset [ %s ] of length %zu at index %d", name, length, id);
return ResourceBuffer(buffer, length, id);
}
}
static void freeResource(uint32_t id) {
__android_log_print(ANDROID_LOG_VERBOSE, "filament_api", "Freeing loaded resource at index [ %d ] ", id);
auto apk_it = _apk_assets.find(id);
if (apk_it != _apk_assets.end()) {
AAsset_close(apk_it->second);
__android_log_print(ANDROID_LOG_VERBOSE, "filament_api", "Closed Android asset");
} else {
auto file_it = _file_assets.find(id);
if (file_it != _file_assets.end()) {
free(file_it->second);
__android_log_print(ANDROID_LOG_VERBOSE, "filament_api", "Freed asset from filesystem.");
} else {
__android_log_print(ANDROID_LOG_VERBOSE, "filament_api", "FATAL - could not find Android or filesystem (hot reload) asset under id %d", id);
}
}
}
extern "C" {
void* create_filament_viewer_android(
jobject surface,
JNIEnv* env,
jobject assetManager
) {
ANativeWindow* layer = ANativeWindow_fromSurface(env, surface);
am = AAssetManager_fromJava(env, assetManager);
return new FilamentViewer((void*)layer, loadResource, freeResource);
}
}