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);
}
}

View File

@@ -1,144 +0,0 @@
/*
* Copyright (C) 2017 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.
*/
package com.google.android.filament;
import androidx.annotation.NonNull;
/**
* A <code>SwapChain</code> represents an Operating System's <b>native</b> renderable surface.
*
* <p>Typically it's a native window or a view. Because a <code>SwapChain</code> is initialized
* from a native object, it is given to filament as an <code>Object</code>, which must be of the
* proper type for each platform filament is running on.</p>
*
* <code>
* SwapChain swapChain = engine.createSwapChain(nativeWindow);
* </code>
*
* <p>The <code>nativeWindow</code> parameter above must be of type:</p>
*
* <center>
* <table border="1">
* <tr><th> Platform </th><th> nativeWindow type </th></tr>
* <tr><td> Android </td><td>{@link android.view.Surface Surface}</td></tr>
* </table>
* </center>
* <p>
*
* <h1>Examples</h1>
*
* <h2>Android</h2>
*
*
* <p>A {@link android.view.Surface Surface} can be retrieved from a
* {@link android.view.SurfaceView SurfaceView} or {@link android.view.SurfaceHolder SurfaceHolder}
* easily using {@link android.view.SurfaceHolder#getSurface SurfaceHolder.getSurface()} and/or
* {@link android.view.SurfaceView#getHolder SurfaceView.getHolder()}.</p>
*
* <p>To use a {@link android.view.TextureView Textureview} as a <code>SwapChain</code>, it is
* necessary to first get its {@link android.graphics.SurfaceTexture SurfaceTexture},
* for instance using {@link android.view.TextureView.SurfaceTextureListener SurfaceTextureListener}
* and then create a {@link android.view.Surface Surface}:</p>
*
* <pre>
* // using a TextureView.SurfaceTextureListener:
* public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
* mSurface = new Surface(surfaceTexture);
* // mSurface can now be used with Engine.createSwapChain()
* }
* </pre>
*
* @see Engine
*/
public class SwapChain {
private final Object mSurface;
private long mNativeObject;
public static final long CONFIG_DEFAULT = 0x0;
/**
* This flag indicates that the <code>SwapChain</code> must be allocated with an
* alpha-channel.
*/
public static final long CONFIG_TRANSPARENT = 0x1;
/**
* This flag indicates that the <code>SwapChain</code> may be used as a source surface
* for reading back render results. This config must be set when creating
* any <code>SwapChain</code> that will be used as the source for a blit operation.
*
* @see Renderer#copyFrame
*/
public static final long CONFIG_READABLE = 0x2;
/**
* Indicates that the native X11 window is an XCB window rather than an XLIB window.
* This is ignored on non-Linux platforms and in builds that support only one X11 API.
*/
public static final long CONFIG_ENABLE_XCB = 0x4;
SwapChain(long nativeSwapChain, Object surface) {
mNativeObject = nativeSwapChain;
mSurface = surface;
}
/**
* @return the native <code>Object</code> this <code>SwapChain</code> was created from or null
* for a headless SwapChain.
*/
public Object getNativeWindow() {
return mSurface;
}
/**
* FrameCompletedCallback is a callback function that notifies an application when a frame's
* contents have completed rendering on the GPU.
*
* <p>
* Use setFrameCompletedCallback to set a callback on an individual SwapChain. Each time a frame
* completes GPU rendering, the callback will be called.
* </p>
*
* <p>
* The FrameCompletedCallback is guaranteed to be called on the main Filament thread.
* </p>
*
* <p>
* Warning: Only Filament's Metal backend supports frame callbacks. Other backends ignore the
* callback (which will never be called) and proceed normally.
* </p>
*
* @param handler A {@link java.util.concurrent.Executor Executor}.
* @param callback The Runnable callback to invoke.
*/
public void setFrameCompletedCallback(@NonNull Object handler, @NonNull Runnable callback) {
nSetFrameCompletedCallback(getNativeObject(), handler, callback);
}
public long getNativeObject() {
if (mNativeObject == 0) {
throw new IllegalStateException("Calling method on destroyed SwapChain");
}
return mNativeObject;
}
void clearNativeObject() {
mNativeObject = 0;
}
private static native void nSetFrameCompletedCallback(long nativeSwapChain, Object handler, Runnable callback);
}

View File

@@ -1,202 +0,0 @@
// /*
// * Copyright (C) 2020 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.
// */
// package com.google.android.filament.android;
// import android.content.Context;
// import android.hardware.display.DisplayManager;
// import android.os.Build;
// import android.os.Handler;
// import android.view.Display;
// import com.google.android.filament.Renderer;
// import androidx.annotation.NonNull;
// import androidx.annotation.Nullable;
// /**
// * DisplayHelper is here to help managing a Display, for instance being notified when its
// * resolution or refresh rate changes.
// */
// public class DisplayHelper {
// private Handler mHandler = null;
// private DisplayManager mDisplayManager;
// private Display mDisplay;
// private Renderer mRenderer;
// private DisplayManager.DisplayListener mListener;
// /**
// * Creates a DisplayHelper which helps managing a {@link Display}.
// *
// * The {@link Display} to manage is specified with {@link #attach}
// *
// * @param context a {@link Context} to used to retrieve the {@link DisplayManager}
// */
// public DisplayHelper(@NonNull Context context) {
// mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
// }
// /**
// * Creates a DisplayHelper which helps manage a {@link Display} and provides a Handler
// * where callbacks can execute filament code. Use this method if filament is executing
// * on another thread.
// *
// * @param context a {@link Context} to used to retrieve teh {@link DisplayManager}
// * @param handler a {@link Handler} used to run callbacks accessing filament
// */
// public DisplayHelper(@NonNull Context context, @NonNull Handler handler) {
// this(context);
// mHandler = handler;
// }
// @Override
// protected void finalize() throws Throwable {
// try {
// // just for safety
// detach();
// } finally {
// super.finalize();
// }
// }
// /**
// * Sets the filament {@link Renderer} associated to the {@link Display}, from this point
// * on, {@link Renderer.DisplayInfo} will be automatically updated when the {@link Display}
// * properties change.
// *
// * This is typically called from {@link UiHelper.RendererCallback#onNativeWindowChanged}.
// *
// * @param renderer a filament {@link Renderer} instance
// * @param display a {@link Display} to be associated with the {@link Renderer}
// */
// public void attach(@NonNull Renderer renderer, @NonNull Display display) {
// if (renderer == mRenderer && display == mDisplay) {
// return;
// }
// mRenderer = renderer;
// mDisplay = display;
// mListener = new DisplayManager.DisplayListener() {
// @Override
// public void onDisplayAdded(int displayId) {
// }
// @Override
// public void onDisplayRemoved(int displayId) {
// }
// @Override
// public void onDisplayChanged(int displayId) {
// if (displayId == display.getDisplayId()) {
// updateDisplayInfo();
// }
// }
// };
// mDisplayManager.registerDisplayListener(mListener, mHandler);
// // always invoke the callback when it's registered
// if (mHandler != null) {
// mHandler.post(new Runnable() {
// @Override
// public void run() {
// updateDisplayInfo();
// }
// });
// } else {
// updateDisplayInfo();
// }
// }
// /**
// * Disconnect the previously set {@link Renderer} from {@link Display}
// * This is typically called from {@link UiHelper.RendererCallback#onDetachedFromSurface}.
// */
// public void detach() {
// if (mListener != null) {
// mDisplayManager.unregisterDisplayListener(mListener);
// mListener = null;
// mDisplay = null;
// mRenderer = null;
// }
// }
// private void updateDisplayInfo() {
// mRenderer.setDisplayInfo(
// DisplayHelper.getDisplayInfo(mDisplay, mRenderer.getDisplayInfo()));
// }
// /**
// * Returns the {@link Display} currently monitored
// * @return the {@link Display} set in {@link #attach} or null
// */
// public Display getDisplay() {
// return mDisplay;
// }
// /**
// * Populate a {@link Renderer.DisplayInfo} with properties from the given {@link Display}
// *
// * @param display {@link Display} to get {@link Renderer.DisplayInfo} from
// * @param info an instance of {@link Renderer.DisplayInfo} or null
// * @return an populated instance of {@link Renderer.DisplayInfo}
// */
// @NonNull
// public static Renderer.DisplayInfo getDisplayInfo(@NonNull Display display, @Nullable Renderer.DisplayInfo info) {
// if (info == null) {
// info = new Renderer.DisplayInfo();
// }
// info.refreshRate = DisplayHelper.getRefreshRate(display);
// return info;
// }
// /**
// * @return the {@link Display} application vsync offset 0 if not supported
// * @see Display#getAppVsyncOffsetNanos
// */
// public static long getAppVsyncOffsetNanos(@NonNull Display display) {
// if (Build.VERSION.SDK_INT >= 29) {
// return display.getAppVsyncOffsetNanos();
// }
// return 0;
// }
// /**
// * @return the {@link Display} presentation deadline before the h/w vsync event in nanoseconds
// * @see Display#getPresentationDeadlineNanos
// */
// public static long getPresentationDeadlineNanos(@NonNull Display display) {
// if (Build.VERSION.SDK_INT >= 29) {
// return display.getPresentationDeadlineNanos();
// }
// // not supported, pick something reasonable
// return 11_600_000;
// }
// /**
// * @return the {@link Display} refresh rate in Hz
// * @see Display#getRefreshRate
// */
// public static float getRefreshRate(@NonNull Display display) {
// return display.getRefreshRate();
// }
// /**
// * Returns a {@link Display}'s refresh period in nanoseconds
// * @param display the {@link Display} to get the refresh period from
// * @return the {@link Display} refresh period in nanoseconds
// */
// public static long getRefreshPeriodNanos(@NonNull Display display) {
// return (long) (1_000_000_000.0 / display.getRefreshRate());
// }
// }

View File

@@ -1,581 +0,0 @@
/*
* Copyright (C) 2015 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.
*/
package com.google.android.filament.android;
import android.graphics.PixelFormat;
import android.graphics.SurfaceTexture;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import com.google.android.filament.SwapChain;
/**
* UiHelper is a simple class that can manage either a SurfaceView, TextureView, or a SurfaceHolder
* so it can be used to render into with Filament.
*
* Here is a simple example with a SurfaceView. The code would be exactly the same with a
* TextureView:
*
* <pre>
* public class FilamentActivity extends Activity {
* private UiHelper mUiHelper;
* private SurfaceView mSurfaceView;
*
* // Filament specific APIs
* private Engine mEngine;
* private Renderer mRenderer;
* private View mView; // com.google.android.filament.View, not android.view.View
* private SwapChain mSwapChain;
*
* public void onCreate(Bundle savedInstanceState) {
* super.onCreate(savedInstanceState);
*
* // Create a SurfaceView and add it to the activity
* mSurfaceView = new SurfaceView(this);
* setContentView(mSurfaceView);
*
* // Create the Filament UI helper
* mUiHelper = new UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK);
*
* // Attach the SurfaceView to the helper, you could do the same with a TextureView
* mUiHelper.attachTo(mSurfaceView);
*
* // Set a rendering callback that we will use to invoke Filament
* mUiHelper.setRenderCallback(new UiHelper.RendererCallback() {
* public void onNativeWindowChanged(Surface surface) {
* if (mSwapChain != null) mEngine.destroySwapChain(mSwapChain);
* mSwapChain = mEngine.createSwapChain(surface, mUiHelper.getSwapChainFlags());
* }
*
* // The native surface went away, we must stop rendering.
* public void onDetachedFromSurface() {
* if (mSwapChain != null) {
* mEngine.destroySwapChain(mSwapChain);
*
* // Required to ensure we don't return before Filament is done executing the
* // destroySwapChain command, otherwise Android might destroy the Surface
* // too early
* mEngine.flushAndWait();
*
* mSwapChain = null;
* }
* }
*
* // The native surface has changed size. This is always called at least once
* // after the surface is created (after onNativeWindowChanged() is invoked).
* public void onResized(int width, int height) {
* // Compute camera projection and set the viewport on the view
* }
* });
*
* mEngine = Engine.create();
* mRenderer = mEngine.createRenderer();
* mView = mEngine.createView();
* // Create scene, camera, etc.
* }
*
* public void onDestroy() {
* super.onDestroy();
* // Always detach the surface before destroying the engine
* mUiHelper.detach();
*
* mEngine.destroy();
* }
*
* // This is an example of a render function. You will most likely invoke this from
* // a Choreographer callback to trigger rendering at vsync.
* public void render() {
* if (mUiHelper.isReadyToRender) {
* // If beginFrame() returns false you should skip the frame
* // This means you are sending frames too quickly to the GPU
* if (mRenderer.beginFrame(swapChain)) {
* mRenderer.render(mView);
* mRenderer.endFrame();
* }
* }
* }
* }
* </pre>
*/
public class UiHelper {
private static final String LOG_TAG = "UiHelper";
private static final boolean LOGGING = false;
private int mDesiredWidth;
private int mDesiredHeight;
private Object mNativeWindow;
private RendererCallback mRenderCallback;
private boolean mHasSwapChain;
private RenderSurface mRenderSurface;
private boolean mOpaque = true;
private boolean mOverlay = false;
/**
* Enum used to decide whether UiHelper should perform extra error checking.
*
* @see UiHelper#UiHelper(ContextErrorPolicy)
*/
public enum ContextErrorPolicy {
/** Check for extra errors. */
CHECK,
/** Do not check for extra errors. */
DONT_CHECK
}
/**
* Interface used to know when the native surface is created, destroyed or resized.
*
* @see #setRenderCallback(RendererCallback)
*/
public interface RendererCallback {
/**
* Called when the underlying native window has changed.
*/
void onNativeWindowChanged(Surface surface);
/**
* Called when the surface is going away. After this call <code>isReadyToRender()</code>
* returns false. You MUST have stopped drawing when returning.
* This is called from detach() or if the surface disappears on its own.
*/
void onDetachedFromSurface();
/**
* Called when the underlying native window has been resized.
*/
void onResized(int width, int height);
}
private interface RenderSurface {
void resize(int width, int height);
void detach();
}
private static class SurfaceViewHandler implements RenderSurface {
private SurfaceView mSurfaceView;
SurfaceViewHandler(SurfaceView surface) {
mSurfaceView = surface;
}
@Override
public void resize(int width, int height) {
mSurfaceView.getHolder().setFixedSize(width, height);
}
@Override
public void detach() {
}
}
private static class SurfaceHolderHandler implements RenderSurface {
private SurfaceHolder mSurfaceHolder;
SurfaceHolderHandler(SurfaceHolder surface) {
mSurfaceHolder = surface;
}
@Override
public void resize(int width, int height) {
mSurfaceHolder.setFixedSize(width, height);
}
@Override
public void detach() {
}
}
private class TextureViewHandler implements RenderSurface {
private TextureView mTextureView;
private Surface mSurface;
TextureViewHandler(TextureView surface) { mTextureView = surface; }
@Override
public void resize(int width, int height) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
mTextureView.getSurfaceTexture().setDefaultBufferSize(width, height);
}
// the call above won't cause TextureView.onSurfaceTextureSizeChanged()
mRenderCallback.onResized(width, height);
}
@Override
public void detach() {
setSurface(null);
}
void setSurface(Surface surface) {
if (surface == null) {
if (mSurface != null) {
mSurface.release();
}
}
mSurface = surface;
}
}
/**
* Creates a UiHelper which will help manage the native surface provided by a
* SurfaceView or a TextureView.
*/
public UiHelper() {
this(ContextErrorPolicy.CHECK);
}
/**
* Creates a UiHelper which will help manage the native surface provided by a
* SurfaceView or a TextureView.
*
* @param policy The error checking policy to use.
*/
public UiHelper(ContextErrorPolicy policy) {
// TODO: do something with policy
}
/**
* Sets the renderer callback that will be notified when the native surface is
* created, destroyed or resized.
*
* @param renderCallback The callback to register.
*/
public void setRenderCallback(@Nullable RendererCallback renderCallback) {
mRenderCallback = renderCallback;
}
/**
* Returns the current render callback associated with this UiHelper.
*/
@Nullable
public RendererCallback getRenderCallback() {
return mRenderCallback;
}
/**
* Free resources associated to the native window specified in {@link #attachTo(SurfaceView)},
* {@link #attachTo(TextureView)}, or {@link #attachTo(SurfaceHolder)}.
*/
public void detach() {
destroySwapChain();
mNativeWindow = null;
mRenderSurface = null;
}
/**
* Checks whether we are ready to render into the attached surface.
*
* Using OpenGL ES when this returns true, will result in drawing commands being lost,
* HOWEVER, GLES state will be preserved. This is useful to initialize the engine.
*
* @return true: rendering is possible, false: rendering is not possible.
*/
public boolean isReadyToRender() {
return mHasSwapChain;
}
/**
* Set the size of the render target buffers of the native surface.
*/
public void setDesiredSize(int width, int height) {
mDesiredWidth = width;
mDesiredHeight = height;
if (mRenderSurface != null) {
mRenderSurface.resize(width, height);
}
}
/**
* Returns the requested width for the native surface.
*/
public int getDesiredWidth() {
return mDesiredWidth;
}
/**
* Returns the requested height for the native surface.
*/
public int getDesiredHeight() {
return mDesiredHeight;
}
/**
* Returns true if the render target is opaque.
*/
public boolean isOpaque() {
return mOpaque;
}
/**
* Controls whether the render target (SurfaceView or TextureView) is opaque or not.
* The render target is considered opaque by default.
*
* Must be called before calling {@link #attachTo(SurfaceView)}, {@link #attachTo(TextureView)},
* or {@link #attachTo(SurfaceHolder)}.
*
* @param opaque Indicates whether the render target should be opaque. True by default.
*/
public void setOpaque(boolean opaque) {
mOpaque = opaque;
}
/**
* Returns true if the SurfaceView used as a render target should be positioned above
* other surfaces but below the activity's surface. False by default.
*/
public boolean isMediaOverlay() {
return mOverlay;
}
/**
* Controls whether the surface of the SurfaceView used as a render target should be
* positioned above other surfaces but below the activity's surface. This property
* only has an effect when used in combination with {@link #setOpaque(boolean) setOpaque(false)}
* and does not affect TextureView targets.
*
* Must be called before calling {@link #attachTo(SurfaceView)}
* or {@link #attachTo(TextureView)}.
*
* Has no effect when using {@link #attachTo(SurfaceHolder)}.
*
* @param overlay Indicates whether the render target should be rendered below the activity's
* surface when transparent.
*/
public void setMediaOverlay(boolean overlay) {
mOverlay = overlay;
}
/**
* Returns the flags to pass to
* {@link com.google.android.filament.Engine#createSwapChain(Object, long)} to honor all
* the options set on this UiHelper.
*/
public long getSwapChainFlags() {
return isOpaque() ? SwapChain.CONFIG_DEFAULT : SwapChain.CONFIG_TRANSPARENT;
}
/**
* Associate UiHelper with a SurfaceView.
*
* As soon as SurfaceView is ready (i.e. has a Surface), we'll create the
* EGL resources needed, and call user callbacks if needed.
*/
public void attachTo(@NonNull SurfaceView view) {
if (attach(view)) {
boolean translucent = !isOpaque();
// setZOrderOnTop() and setZOrderMediaOverlay() override each other,
// we must only call one of them
if (isMediaOverlay()) {
view.setZOrderMediaOverlay(translucent);
} else {
view.setZOrderOnTop(translucent);
}
int format = isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT;
view.getHolder().setFormat(format);
mRenderSurface = new SurfaceViewHandler(view);
final SurfaceHolder.Callback callback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (LOGGING) Log.d(LOG_TAG, "surfaceCreated()");
createSwapChain(holder.getSurface());
}
@Override
public void surfaceChanged(
SurfaceHolder holder, int format, int width, int height) {
// Note: this is always called at least once after surfaceCreated()
if (LOGGING) Log.d(LOG_TAG, "surfaceChanged(" + width + ", " + height + ")");
mRenderCallback.onResized(width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (LOGGING) Log.d(LOG_TAG, "surfaceDestroyed()");
destroySwapChain();
}
};
SurfaceHolder holder = view.getHolder();
holder.addCallback(callback);
if (mDesiredWidth > 0 && mDesiredHeight > 0) {
holder.setFixedSize(mDesiredWidth, mDesiredHeight);
}
// in case the SurfaceView's surface already existed
final Surface surface = holder.getSurface();
if (surface != null && surface.isValid()) {
callback.surfaceCreated(holder);
callback.surfaceChanged(holder, format,
holder.getSurfaceFrame().width(), holder.getSurfaceFrame().height());
}
}
}
/**
* Associate UiHelper with a TextureView.
*
* As soon as TextureView is ready (i.e. has a buffer), we'll create the
* EGL resources needed, and call user callbacks if needed.
*/
public void attachTo(@NonNull TextureView view) {
if (attach(view)) {
view.setOpaque(isOpaque());
mRenderSurface = new TextureViewHandler(view);
TextureView.SurfaceTextureListener listener = new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(
SurfaceTexture surfaceTexture, int width, int height) {
if (LOGGING) Log.d(LOG_TAG, "onSurfaceTextureAvailable()");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
if (mDesiredWidth > 0 && mDesiredHeight > 0) {
surfaceTexture.setDefaultBufferSize(mDesiredWidth, mDesiredHeight);
}
}
Surface surface = new Surface(surfaceTexture);
TextureViewHandler textureViewHandler = (TextureViewHandler) mRenderSurface;
textureViewHandler.setSurface(surface);
createSwapChain(surface);
// Call this the first time because onSurfaceTextureSizeChanged()
// isn't called at initialization time
mRenderCallback.onResized(width, height);
}
@Override
public void onSurfaceTextureSizeChanged(
SurfaceTexture surfaceTexture, int width, int height) {
if (LOGGING) Log.d(LOG_TAG, "onSurfaceTextureSizeChanged()");
if (mDesiredWidth > 0 && mDesiredHeight > 0) {
surfaceTexture.setDefaultBufferSize(mDesiredWidth, mDesiredHeight);
mRenderCallback.onResized(mDesiredWidth, mDesiredHeight);
} else {
mRenderCallback.onResized(width, height);
}
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
if (LOGGING) Log.d(LOG_TAG, "onSurfaceTextureDestroyed()");
destroySwapChain();
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) { }
};
view.setSurfaceTextureListener(listener);
// in case the View's SurfaceTexture already existed
if (view.isAvailable()) {
SurfaceTexture surfaceTexture = view.getSurfaceTexture();
listener.onSurfaceTextureAvailable(surfaceTexture, mDesiredWidth, mDesiredHeight);
}
}
}
/**
* Associate UiHelper with a SurfaceHolder.
*
* As soon as a Surface is created, we'll create the
* EGL resources needed, and call user callbacks if needed.
*/
public void attachTo(@NonNull SurfaceHolder holder) {
if (attach(holder)) {
int format = isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT;
holder.setFormat(format);
mRenderSurface = new SurfaceHolderHandler(holder);
final SurfaceHolder.Callback callback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
if (LOGGING) Log.d(LOG_TAG, "surfaceCreated()");
createSwapChain(holder.getSurface());
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// Note: this is always called at least once after surfaceCreated()
if (LOGGING) Log.d(LOG_TAG, "surfaceChanged(" + width + ", " + height + ")");
mRenderCallback.onResized(width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
if (LOGGING) Log.d(LOG_TAG, "surfaceDestroyed()");
destroySwapChain();
}
};
holder.addCallback(callback);
if (mDesiredWidth > 0 && mDesiredHeight > 0) {
holder.setFixedSize(mDesiredWidth, mDesiredHeight);
}
// in case the SurfaceHolder's surface already existed
final Surface surface = holder.getSurface();
if (surface != null && surface.isValid()) {
callback.surfaceCreated(holder);
callback.surfaceChanged(holder, format,
holder.getSurfaceFrame().width(), holder.getSurfaceFrame().height());
}
}
}
private boolean attach(@NonNull Object nativeWindow) {
if (mNativeWindow != null) {
// we are already attached to a native window
if (mNativeWindow == nativeWindow) {
// nothing to do
return false;
}
destroySwapChain();
}
mNativeWindow = nativeWindow;
return true;
}
private void createSwapChain(@NonNull Surface surface) {
mRenderCallback.onNativeWindowChanged(surface);
mHasSwapChain = true;
}
private void destroySwapChain() {
if (mRenderSurface != null) {
mRenderSurface.detach();
}
mRenderCallback.onDetachedFromSurface();
mHasSwapChain = false;
}
}

View File

@@ -1,105 +1,102 @@
package app.polyvox.filament package app.polyvox.filament
import com.sun.jna.Library
import com.sun.jna.Native
import com.sun.jna.Pointer
import com.sun.jna.ptr.PointerByReference import com.sun.jna.ptr.PointerByReference
import com.sun.jna.ptr.IntByReference import com.sun.jna.ptr.IntByReference
import com.sun.jna.Structure
import com.sun.jna.NativeLibrary
import com.sun.jna.StringArray
import com.sun.jna.JNIEnv
import android.view.Surface import android.view.Surface
import android.content.res.AssetManager import android.content.res.AssetManager
import com.sun.jna.*
import java.nio.ByteBuffer open class ResourceBuffer: Structure(), Structure.ByValue {
@JvmField var data: Pointer = Pointer(0);
interface FilamentInterop : Library { @JvmField var size: Int = 0;
@JvmField var id: Int = 0;
fun create_filament_viewer_android( override fun getFieldOrder(): List<String> {
layer:Object, return listOf("data", "size", "id")
env:JNIEnv, }
am:AssetManager }
) : Pointer;
interface LoadResourceFromOwner : Callback {
fun delete_filament_viewer( fun loadResourceFromOwner(resourceName: String?, owner: Pointer?): ResourceBuffer
viewer:Pointer, }
) : Pointer;
interface FreeResourceFromOwner : Callback {
fun load_skybox(viewer:Pointer, skyboxPath:String) : Pointer; fun freeResourceFromOwner(rb: ResourceBuffer, owner: Pointer?)
}
fun load_ibl(viewer:Pointer, skyboxPath:String) : Pointer; interface FilamentInterop : Library {
fun load_glb(viewer:Pointer, uri:String) : Pointer; fun create_swap_chain(viewer: Pointer, window:Pointer?, width:Int, height:Int);
fun load_gltf(viewer:Pointer, uri:String, relativeResourcePath:String) : Pointer; fun create_swap_chain_android(viewer: Pointer, surface:Object,
env:JNIEnv, width:Int, height:Int);
fun set_camera(viewer:Pointer, asset:Pointer, nodeName:String) : Boolean; fun create_filament_viewer_android(
surface:Object,
fun render(viewer:Pointer, frameTimeInNanos:Long); env:JNIEnv,
resourceLoader:Pointer
fun set_frame_interval(viewer:Pointer, interval:Float); ) : Pointer;
fun delete_filament_viewer(viewer: Any?)
fun create_swap_chain(viewer:Pointer, surface:Surface, env:JNIEnv); fun get_asset_manager(viewer: Any?): Any?
fun create_render_target(viewer: Any?, texture_id: Int, width: Int, height: Int)
fun destroy_swap_chain(viewer:Pointer); fun clear_background_image(viewer: Any?)
fun set_background_image(viewer: Any?, path: String, fill_height: Boolean)
fun update_viewport_and_camera_projection(viewer:Pointer, width:Int, height:Int, scaleFactor:Float); fun set_background_image_position(viewer: Any?, x: Float, y: Float, clamp: Boolean)
fun set_background_color(viewer: Any?, r: Float, g: Float, b: Float, a: Float)
fun scroll_begin(viewer:Pointer); fun set_tone_mapping(viewer: Any?, tone_mapping: Int)
fun scroll_update(viewer:Pointer, x:Float, y:Float, delta:Float); fun set_bloom(viewer: Any?, strength: Float)
fun scroll_end(viewer:Pointer); fun load_skybox(viewer: Any?, skybox_path: String)
fun load_ibl(viewer: Any?, ibl_path: String, intensity: Float)
fun grab_begin(viewer:Pointer, x:Float, y:Float, pan:Boolean) fun remove_skybox(viewer: Any?)
fun remove_ibl(viewer: Any?)
fun grab_update(viewer:Pointer, x:Float, y:Float) fun add_light(viewer: Any?, type: Byte, colour: Float, intensity: Float, pos_x: Float, pos_y: Float, pos_z: Float, dir_x: Float, dir_y: Float, dir_z: Float, shadows: Boolean): EntityId
fun remove_light(viewer: Any?, entity_id: EntityId)
fun grab_end(viewer:Pointer) fun clear_lights(viewer: Any?)
fun load_glb(asset_manager: Any?, asset_path: String, unlit: Boolean): EntityId
fun apply_weights(asset:Pointer, weights:FloatArray, size:Int); fun load_gltf(asset_manager: Any?, asset_path: String, relative_path: String): EntityId
fun set_camera(viewer: Any?, asset: EntityId, node_name: String): Boolean
fun set_animation(asset:Pointer, frames:FloatArray, numWeights:Int, numFrames:Int, frameRate:Float); fun render(viewer: Any?, frame_time_in_nanos: Long)
fun get_morph_target_name_count(asset:Pointer, meshName:String) : Int; fun destroy_swap_chain(viewer: Any?)
fun set_frame_interval(viewer: Any?, interval: Float)
fun get_morph_target_name(asset:Pointer, meshName:String, outPtr:Pointer, index:Int); fun update_viewport_and_camera_projection(viewer: Any?, width: UInt, height: UInt, scale_factor: Float)
fun scroll_begin(viewer: Any?)
fun get_animation_count(asset:Pointer) : Int; fun scroll_update(viewer: Any?, x: Float, y: Float, z: Float)
fun get_animation_name(asset:Pointer, outPtr:Pointer, index:Int); fun scroll_end(viewer: Any?)
fun grab_begin(viewer: Any?, x: Float, y: Float, pan: Boolean)
fun play_animation(asset:Pointer, index:Int, loop:Boolean, reverse:Boolean); fun grab_update(viewer: Any?, x: Float, y: Float)
fun stop_animation(asset:Pointer, index:Int); fun grab_end(viewer: Any?)
fun apply_weights(asset_manager: Any?, asset: EntityId, entity_name: String, weights: FloatArray, count: Int)
fun free_pointer(ptr:Pointer, size:Int); fun set_morph_target_weights(asset_manager: Any?, asset: EntityId, entity_name: String, morph_data: FloatArray, num_weights: Int)
fun set_morph_animation(asset_manager: Any?, asset: EntityId, entity_name: String, morph_data: FloatArray, morph_indices: IntArray, num_morph_targets: Int, num_frames: Int, frame_length_in_ms: Int): Boolean
fun remove_asset(viewer:Pointer, asset:Pointer); fun set_bone_animation(asset_manager: Any?, asset: EntityId, frame_data: FloatArray, num_frames: Int, num_bones: Int, bone_names: Array<String>, mesh_name: Array<String>, num_mesh_targets: Int, frame_length_in_ms: Int)
fun play_animation(asset_manager: Any?, asset: EntityId, index: Int, loop: Boolean, reverse: Boolean, replace_active: Boolean, crossfade: Float)
fun clear_assets(viewer:Pointer); fun set_animation_frame(asset_manager: Any?, asset: EntityId, animation_index: Int, animation_frame: Int)
fun stop_animation(asset_manager: Any?, asset: EntityId, index: Int)
fun remove_skybox(viewer:Pointer); fun get_animation_count(asset_manager: Any?, asset: EntityId): Int
fun remove_ibl(viewer:Pointer); fun get_animation_name(asset_manager: Any?, asset: EntityId, out_ptr: String, index: Int)
fun get_animation_duration(asset_manager: Any?, asset: EntityId, index: Int): Float
fun add_light(viewer:Pointer, type:Int, colour:Float, intensity:Float, posX:Float, posY:Float, posZ:Float, dirX:Float, dirY:Float, dirZ:Float, shadows:Boolean) : Int; fun get_morph_target_name(asset_manager: Any?, asset: EntityId, mesh_name: String, out_ptr: String, index: Int)
fun remove_light(viewer:Pointer, entityId:Int); fun get_morph_target_name_count(asset_manager: Any?, asset: EntityId, mesh_name: String): Int
fun clear_lights(viewer:Pointer); fun remove_asset(viewer: Any?, asset: EntityId)
fun clear_assets(viewer: Any?)
fun set_background_image(viewer:Pointer, path:String); fun load_texture(asset_manager: Any?, asset: EntityId, asset_path: String, renderable_index: Int)
fun set_background_image_position(viewer:Pointer, x:Float, y:Float, clamp:Boolean); fun set_texture(asset_manager: Any?, asset: EntityId)
fun set_material_color(asset_manager: Any?, asset: EntityId, mesh_name: String, material_index: Int, r: Float, g: Float, b: Float, a: Float): Boolean
fun load_texture(asset:Pointer, path:String, renderableIndex:Int); fun transform_to_unit_cube(asset_manager: Any?, asset: EntityId)
fun set_texture(asset:Pointer); fun set_position(asset_manager: Any?, asset: EntityId, x: Float, y: Float, z: Float)
fun set_rotation(asset_manager: Any?, asset: EntityId, rads: Float, x: Float, y: Float, z: Float)
fun transform_to_unit_cube(asset:Pointer); fun set_scale(asset_manager: Any?, asset: EntityId, scale: Float)
fun set_camera_exposure(viewer: Any?, aperture: Float, shutter_speed: Float, sensitivity: Float)
fun set_position(asset:Pointer, x:Float, y:Float, z:Float); fun set_camera_position(viewer: Any?, x: Float, y: Float, z: Float)
fun set_rotation(asset:Pointer, rads:Float, x:Float, y:Float, z:Float); fun set_camera_rotation(viewer: Any?, rads: Float, x: Float, y: Float, z: Float)
fun set_camera_model_matrix(viewer: Any?, matrix: FloatArray)
fun set_camera_position(asset:Pointer, x:Float, y:Float, z:Float); fun set_camera_focal_length(viewer: Any?, focal_length: Float)
fun set_camera_rotation(asset:Pointer, rads:Float, x:Float, y:Float, z:Float); fun set_camera_focus_distance(viewer: Any?, focus_distance: Float)
fun set_camera_focal_length(asset:Pointer, focalLength:Float); fun hide_mesh(asset_manager: Any?, asset: EntityId, mesh_name: String): Int
fun set_camera_focus_distance(asset:Pointer, focusDistance:Float); fun reveal_mesh(asset_manager: Any?, asset: EntityId, mesh_name: String): Int
fun set_scale(asset:Pointer, scale:Float); fun ios_dummy()
fun create_filament_viewer(context:Long, loader:Pointer) : Pointer;
fun make_resource_loader(loadResourceFromOwner: LoadResourceFromOwner, freeResource: FreeResourceFromOwner, owner:Pointer?) : Pointer;
} }