Compare commits

...

7 Commits

Author SHA1 Message Date
Powei Feng
69fc1f302c vk: testing a robin_map key hash failure
Executable on device with:

./build.sh -p android -q arm64-v8a release test_compiler && adb push out/cmake-android-release-aarch64/filament/test/test_compiler /data/local/tmp/ && adb shell chmod 777 /data/local/tmp/test_compiler && adb shell /data/local/tmp/test_compiler
2024-07-26 12:23:28 +08:00
Mathias Agopian
e7c96cd124 better handle skipped frames and cache eviction
Add Renderer::skipFrame() which should be called when intentionally 
skipping frames, for instance because the screen content hasn't changed,
allowing Filament to performance needed periodic tasks, such as
cache garbage collection and callback dispatches.

We also improve the ResourceAllocator cache eviction policy:
- the cache is aggressively purged when skipping a frame
- we aggressively evict entries older than 3 frames

The default Config is now set to a more agressive setting.
2024-07-25 17:23:04 -07:00
Mathias Agopian
730bc99025 Move the ResourceAllocator from Engine to Renderer
The ResourceAllocator used to be global and owned by the Engine, this
was causing some issues when using several Renderers because each
one could cause the eviction of cache data for another.

We now have a ResourceAllocator per Renderer, which makes more sense
because most resources are allocated by the FrameGraph.

We also introduce a ResourceAllocatorDisposer class, which is used
for checking in and out a texture from the cache, and destroy the
texture when it's checked-out. That objet is still global.
2024-07-25 17:23:04 -07:00
Powei Feng
7441e878bb gltfio: enable escaped unicode for node name (#7989)
Fixes #7846
2024-07-25 09:06:56 +00:00
Grant Commodore
ef4c4a76fc Only sets the layercount if the view has stereo and the stereoscopic type is multiview (#7987)
Co-authored-by: Powei Feng <powei@google.com>
2024-07-25 07:31:51 +00:00
Mathias Agopian
be4f287b07 More improvements to the JobSystem (#7988)
* improve parallel_for a bit

We get about 40% performance increase. The gain comes from not having
to copy the JobData structure each time we create a job, by using a
new emplaceJob() method, we can create the structure directly into
its destination.

* avoid calling wakeAll() when possible

wakeAll() is very expensive and not always needed when a job finishes
because there may not be anyone waiting on that job.

We now maintain a waiter count per job, and use that to determine if
we need to notify or not.

And now that the JobSystem overhead is lower, we can decrease the size
of the jobs, which improves the load balancing.

* mActiveJobs fixes

some comments claimed mActiveJobs needed to be modified before or after
accessing the WorkQueue; this couldn't be correct because there were no
guaranteed global ordering with the workQueue.
2024-07-24 15:34:36 -07:00
Mathias Agopian
d3a35de386 fix a crash with bloom when screen dimension smaller than 16px 2024-07-24 11:49:38 -07:00
33 changed files with 662 additions and 354 deletions

View File

@@ -28,6 +28,14 @@
using namespace filament;
using namespace backend;
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_Renderer_nSkipFrame(JNIEnv *, jclass, jlong nativeRenderer,
jlong vsyncSteadyClockTimeNano) {
Renderer *renderer = (Renderer *) nativeRenderer;
renderer->skipFrame(uint64_t(vsyncSteadyClockTimeNano));
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_google_android_filament_Renderer_nBeginFrame(JNIEnv *, jclass, jlong nativeRenderer,
jlong nativeSwapChain, jlong frameTimeNanos) {

View File

@@ -531,3 +531,12 @@ Java_com_google_android_filament_View_nGetFogEntity(JNIEnv *env, jclass clazz,
View *view = (View *) nativeView;
return (jint)view->getFogEntity().getId();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_View_nClearFrameHistory(JNIEnv *env, jclass clazz,
jlong nativeView, jlong nativeEngine) {
View *view = (View *) nativeView;
Engine *engine = (Engine *) nativeEngine;
view->clearFrameHistory(*engine);
}

View File

@@ -425,9 +425,12 @@ public class Engine {
public long resourceAllocatorCacheSizeMB = 64;
/*
* This value determines for how many frames are texture entries kept in the cache.
* This value determines how many frames texture entries are kept for in the cache. This
* is a soft limit, meaning some texture older than this are allowed to stay in the cache.
* Typically only one texture is evicted per frame.
* The default is 1.
*/
public long resourceAllocatorCacheMaxAge = 2;
public long resourceAllocatorCacheMaxAge = 1;
/*
* Disable backend handles use-after-free checks.

View File

@@ -297,6 +297,20 @@ public class Renderer {
nSetVsyncTime(getNativeObject(), steadyClockTimeNano);
}
/**
* Call skipFrame when momentarily skipping frames, for instance if the content of the
* scene doesn't change.
*
* @param vsyncSteadyClockTimeNano The time in nanoseconds when the frame started being rendered,
* in the {@link System#nanoTime()} timebase. Divide this value by 1000000 to
* convert it to the {@link android.os.SystemClock#uptimeMillis()}
* time base. This typically comes from
* {@link android.view.Choreographer.FrameCallback}.
*/
public void skipFrame(long vsyncSteadyClockTimeNano) {
nSkipFrame(getNativeObject(), vsyncSteadyClockTimeNano);
}
/**
* Sets up a frame for this <code>Renderer</code>.
* <p><code>beginFrame</code> manages frame pacing, and returns whether or not a frame should be
@@ -716,6 +730,7 @@ public class Renderer {
private static native void nSetPresentationTime(long nativeObject, long monotonicClockNanos);
private static native void nSetVsyncTime(long nativeObject, long steadyClockTimeNano);
private static native void nSkipFrame(long nativeObject, long vsyncSteadyClockTimeNano);
private static native boolean nBeginFrame(long nativeRenderer, long nativeSwapChain, long frameTimeNanos);
private static native void nEndFrame(long nativeRenderer);
private static native void nRender(long nativeRenderer, long nativeView);

View File

@@ -1233,6 +1233,18 @@ public class View {
return nGetFogEntity(getNativeObject());
}
/**
* When certain temporal features are used (e.g.: TAA or Screen-space reflections), the view
* keeps a history of previous frame renders associated with the Renderer the view was last
* used with. When switching Renderer, it may be necessary to clear that history by calling
* this method. Similarly, if the whole content of the screen change, like when a cut-scene
* starts, clearing the history might be needed to avoid artifacts due to the previous frame
* being very different.
*/
public void clearFrameHistory(Engine engine) {
nClearFrameHistory(getNativeObject(), engine.getNativeObject());
}
public long getNativeObject() {
if (mNativeObject == 0) {
throw new IllegalStateException("Calling method on destroyed View");
@@ -1294,7 +1306,7 @@ public class View {
private static native void nSetMaterialGlobal(long nativeView, int index, float x, float y, float z, float w);
private static native void nGetMaterialGlobal(long nativeView, int index, float[] out);
private static native int nGetFogEntity(long nativeView);
private static native void nClearFrameHistory(long nativeView, long nativeEngine);
/**
* List of available ambient occlusion techniques.

View File

@@ -350,9 +350,12 @@ public:
uint32_t resourceAllocatorCacheSizeMB = 64;
/*
* This value determines for how many frames are texture entries kept in the cache.
* This value determines how many frames texture entries are kept for in the cache. This
* is a soft limit, meaning some texture older than this are allowed to stay in the cache.
* Typically only one texture is evicted per frame.
* The default is 1.
*/
uint32_t resourceAllocatorCacheMaxAge = 2;
uint32_t resourceAllocatorCacheMaxAge = 1;
/*
* Disable backend handles use-after-free checks.

View File

@@ -271,6 +271,14 @@ public:
*/
void setVsyncTime(uint64_t steadyClockTimeNano) noexcept;
/**
* Call skipFrame when momentarily skipping frames, for instance if the content of the
* scene doesn't change.
*
* @param vsyncSteadyClockTimeNano
*/
void skipFrame(uint64_t vsyncSteadyClockTimeNano = 0u);
/**
* Set-up a frame for this Renderer.
*

View File

@@ -40,6 +40,7 @@ class CallbackHandler;
class Camera;
class ColorGrading;
class Engine;
class MaterialInstance;
class RenderTarget;
class Scene;
@@ -878,6 +879,17 @@ public:
*/
utils::Entity getFogEntity() const noexcept;
/**
* When certain temporal features are used (e.g.: TAA or Screen-space reflections), the view
* keeps a history of previous frame renders associated with the Renderer the view was last
* used with. When switching Renderer, it may be necessary to clear that history by calling
* this method. Similarly, if the whole content of the screen change, like when a cut-scene
* starts, clearing the history might be needed to avoid artifacts due to the previous frame
* being very different.
*/
void clearFrameHistory(Engine& engine) noexcept;
/**
* List of available ambient occlusion techniques
* @deprecated use AmbientOcclusionOptions::enabled instead

View File

@@ -21,6 +21,9 @@
#include <fg/FrameGraphTexture.h>
#include <math/mat4.h>
#include <math/vec2.h>
#include <stdint.h>
namespace filament {

View File

@@ -1965,8 +1965,8 @@ PostProcessManager::BloomPassOutput PostProcessManager::bloom(FrameGraph& fg,
// - visible bloom size changes with dynamic resolution in non-homogenous mode
// This allows us to use the 9 sample downsampling filter (instead of 13)
// for at least 4 levels.
uint32_t width = std::max(1u, uint32_t(std::floor(bloomWidth)));
uint32_t height = std::max(1u, uint32_t(std::floor(bloomHeight)));
uint32_t width = std::max(16u, uint32_t(std::floor(bloomWidth)));
uint32_t height = std::max(16u, uint32_t(std::floor(bloomHeight)));
width &= ~((1 << 4) - 1); // at least 4 levels
height &= ~((1 << 4) - 1);
bloomWidth = float(width);
@@ -1978,6 +1978,8 @@ PostProcessManager::BloomPassOutput PostProcessManager::bloom(FrameGraph& fg,
// we don't need to do the fireflies reduction if we have TAA (it already does it)
bool fireflies = threshold && !taaOptions.enabled;
assert_invariant(bloomWidth && bloomHeight);
while (2 * bloomWidth < float(desc.width) || 2 * bloomHeight < float(desc.height)) {
if (inoutBloomOptions.quality == QualityLevel::LOW ||
inoutBloomOptions.quality == QualityLevel::MEDIUM) {

View File

@@ -221,7 +221,7 @@ void RenderPass::appendCommands(FEngine& engine,
work(vr.first, vr.size());
} else {
auto* jobCommandsParallel = jobs::parallel_for(js, nullptr, vr.first, (uint32_t)vr.size(),
std::cref(work), jobs::CountSplitter<JOBS_PARALLEL_FOR_COMMANDS_COUNT, 5>());
std::cref(work), jobs::CountSplitter<JOBS_PARALLEL_FOR_COMMANDS_COUNT>());
js.runAndWait(jobCommandsParallel);
}

View File

@@ -417,7 +417,7 @@ private:
void instanceify(FEngine& engine, Arena& arena, int32_t eyeCount) noexcept;
// We choose the command count per job to minimize JobSystem overhead.
static constexpr size_t JOBS_PARALLEL_FOR_COMMANDS_COUNT = 1024;
static constexpr size_t JOBS_PARALLEL_FOR_COMMANDS_COUNT = 128;
static constexpr size_t JOBS_PARALLEL_FOR_COMMANDS_SIZE =
sizeof(Command) * JOBS_PARALLEL_FOR_COMMANDS_COUNT;

View File

@@ -45,6 +45,10 @@ void Renderer::setPresentationTime(int64_t monotonic_clock_ns) {
downcast(this)->setPresentationTime(monotonic_clock_ns);
}
void Renderer::skipFrame(uint64_t vsyncSteadyClockTimeNano) {
downcast(this)->skipFrame(vsyncSteadyClockTimeNano);
}
bool Renderer::beginFrame(SwapChain* swapChain, uint64_t vsyncSteadyClockTimeNano) {
return downcast(this)->beginFrame(downcast(swapChain), vsyncSteadyClockTimeNano);
}

View File

@@ -172,7 +172,7 @@ FrameGraphId<FrameGraphTexture> RendererUtils::colorPass(
data.color = builder.write(data.color, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
data.depth = builder.write(data.depth, FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
if (engine.getConfig().stereoscopicType == StereoscopicType::MULTIVIEW) {
if (view.hasStereo() && engine.getConfig().stereoscopicType == StereoscopicType::MULTIVIEW) {
layerCount = engine.getConfig().stereoscopicEyeCount;
}

View File

@@ -27,15 +27,18 @@
#include "private/backend/DriverApi.h"
#include <utils/algorithm.h>
#include <utils/bitset.h>
#include <utils/compiler.h>
#include <utils/debug.h>
#include <utils/FixedCapacityVector.h>
#include <utils/Log.h>
#include <utils/ostream.h>
#include <array>
#include <algorithm>
#include <iterator>
#include <memory>
#include <optional>
#include <utility>
#include <stddef.h>
@@ -89,8 +92,15 @@ void ResourceAllocator::AssociativeContainer<K, V, H>::emplace(ARGS&& ... args)
}
// ------------------------------------------------------------------------------------------------
ResourceAllocatorInterface::~ResourceAllocatorInterface() = default;
// ------------------------------------------------------------------------------------------------
ResourceAllocatorDisposerInterface::~ResourceAllocatorDisposerInterface() = default;
// ------------------------------------------------------------------------------------------------
size_t ResourceAllocator::TextureKey::getSize() const noexcept {
size_t const pixelCount = width * height * depth;
size_t size = pixelCount * FTexture::getFormatSize(format);
@@ -110,16 +120,22 @@ size_t ResourceAllocator::TextureKey::getSize() const noexcept {
ResourceAllocator::ResourceAllocator(Engine::Config const& config, DriverApi& driverApi) noexcept
: mCacheMaxAge(config.resourceAllocatorCacheMaxAge),
mBackend(driverApi) {
mBackend(driverApi),
mDisposer(std::make_shared<ResourceAllocatorDisposer>(driverApi)) {
}
ResourceAllocator::ResourceAllocator(std::shared_ptr<ResourceAllocatorDisposer> disposer,
Engine::Config const& config, DriverApi& driverApi) noexcept
: mCacheMaxAge(config.resourceAllocatorCacheMaxAge),
mBackend(driverApi),
mDisposer(std::move(disposer)) {
}
ResourceAllocator::~ResourceAllocator() noexcept {
assert_invariant(!mTextureCache.size());
assert_invariant(!mInUseTextures.size());
assert_invariant(mTextureCache.empty());
}
void ResourceAllocator::terminate() noexcept {
assert_invariant(!mInUseTextures.size());
auto& textureCache = mTextureCache;
for (auto it = textureCache.begin(); it != textureCache.end();) {
mBackend.destroyTexture(it->second.handle);
@@ -174,7 +190,7 @@ backend::TextureHandle ResourceAllocator::createTexture(const char* name,
swizzle[0], swizzle[1], swizzle[2], swizzle[3]);
}
}
mInUseTextures.emplace(handle, key);
mDisposer->checkout(handle, key);
} else {
if (swizzle == defaultSwizzle) {
handle = mBackend.createTexture(
@@ -190,58 +206,99 @@ backend::TextureHandle ResourceAllocator::createTexture(const char* name,
void ResourceAllocator::destroyTexture(TextureHandle h) noexcept {
if constexpr (mEnabled) {
// find the texture in the in-use list (it must be there!)
auto it = mInUseTextures.find(h);
assert_invariant(it != mInUseTextures.end());
// move it to the cache
const TextureKey key = it->second;
uint32_t const size = key.getSize();
mTextureCache.emplace(key, TextureCachePayload{ h, mAge, size });
mCacheSize += size;
// remove it from the in-use list
mInUseTextures.erase(it);
auto const key = mDisposer->checkin(h);
if (UTILS_LIKELY(key.has_value())) {
uint32_t const size = key.value().getSize();
mTextureCache.emplace(key.value(), TextureCachePayload{ h, mAge, size });
mCacheSize += size;
mCacheSizeHiWaterMark = std::max(mCacheSizeHiWaterMark, mCacheSize);
}
} else {
mBackend.destroyTexture(h);
}
}
void ResourceAllocator::gc() noexcept {
// this is called regularly -- usually once per frame of each Renderer
ResourceAllocatorDisposerInterface& ResourceAllocator::getDisposer() noexcept {
return *mDisposer;
}
// increase our age
const size_t age = mAge++;
void ResourceAllocator::gc(bool skippedFrame) noexcept {
// this is called regularly -- usually once per frame
// increase our age at each (non-skipped) frame
const size_t age = mAge;
if (!skippedFrame) {
mAge++;
}
// Purging strategy:
// - remove entries that are older than a certain age
// - remove only one entry per gc(),
// - remove all entries older than MAX_AGE_SKIPPED_FRAME when skipping a frame
// - remove entries older than mCacheMaxAgeSoft
// - remove only MAX_EVICTION_COUNT entry per gc(),
// - look for the number of unique resource ages present in the cache (this basically gives
// us how many buckets of resources we have corresponding to previous frames.
// - remove all resources that have an age older than the MAX_UNIQUE_AGE_COUNT'th bucket
auto& textureCache = mTextureCache;
// when skipping a frame, the maximum age to keep in the cache
constexpr size_t MAX_AGE_SKIPPED_FRAME = 1;
// maximum entry count to evict per GC, under the mCacheMaxAgeSoft limit
constexpr size_t MAX_EVICTION_COUNT = 1;
// maximum number of unique ages in the cache
constexpr size_t MAX_UNIQUE_AGE_COUNT = 3;
utils::bitset32 ages;
uint32_t evictedCount = 0;
for (auto it = textureCache.begin(); it != textureCache.end();) {
const size_t ageDiff = age - it->second.age;
if (ageDiff >= mCacheMaxAge) {
size_t const ageDiff = age - it->second.age;
if ((ageDiff >= MAX_AGE_SKIPPED_FRAME && skippedFrame) ||
(ageDiff >= mCacheMaxAge && evictedCount < MAX_EVICTION_COUNT)) {
evictedCount++;
purge(it);
// only purge a single entry per gc
break;
} else {
// build the set of ages present in the cache after eviction
ages.set(std::min(size_t(31), ageDiff));
++it;
}
}
// if we have MAX_UNIQUE_AGE_COUNT ages or more, we evict all the resources that
// are older than the MAX_UNIQUE_AGE_COUNT'th age.
if (!skippedFrame && ages.count() >= MAX_UNIQUE_AGE_COUNT) {
uint32_t bits = ages.getValue();
// remove from the set the ages we keep
for (size_t i = 0; i < MAX_UNIQUE_AGE_COUNT - 1; i++) {
bits &= ~(1 << utils::ctz(bits));
}
size_t const maxAge = utils::ctz(bits);
for (auto it = textureCache.begin(); it != textureCache.end();) {
const size_t ageDiff = age - it->second.age;
if (ageDiff >= maxAge) {
purge(it);
} else {
++it;
}
}
}
}
UTILS_NOINLINE
void ResourceAllocator::dump(bool brief) const noexcept {
slog.d << "# entries=" << mTextureCache.size() << ", sz=" << mCacheSize / float(1u << 20u)
<< " MiB" << io::endl;
constexpr float MiB = 1.0f / float(1u << 20u);
slog.d << "# entries=" << mTextureCache.size()
<< ", sz=" << (float)mCacheSize * MiB << " MiB"
<< ", max=" << (float)mCacheSizeHiWaterMark * MiB << " MiB"
<< io::endl;
if (!brief) {
for (auto const& it : mTextureCache) {
auto w = it.first.width;
auto h = it.first.height;
auto f = FTexture::getFormatSize(it.first.format);
slog.d << it.first.name << ": w=" << w << ", h=" << h << ", f=" << f << ", sz="
<< it.second.size / float(1u << 20u) << io::endl;
<< (float)it.second.size * MiB << io::endl;
}
}
}
@@ -254,4 +311,46 @@ void ResourceAllocator::purge(
mTextureCache.erase(pos);
}
// ------------------------------------------------------------------------------------------------
ResourceAllocatorDisposer::ResourceAllocatorDisposer(DriverApi& driverApi) noexcept
: mBackend(driverApi) {
}
ResourceAllocatorDisposer::~ResourceAllocatorDisposer() noexcept {
assert_invariant(mInUseTextures.empty());
}
void ResourceAllocatorDisposer::terminate() noexcept {
assert_invariant(mInUseTextures.empty());
}
void ResourceAllocatorDisposer::destroy(backend::TextureHandle handle) noexcept {
if (handle) {
auto r = checkin(handle);
if (r.has_value()) {
mBackend.destroyTexture(handle);
}
}
}
void ResourceAllocatorDisposer::checkout(backend::TextureHandle handle,
ResourceAllocator::TextureKey key) {
mInUseTextures.emplace(handle, key);
}
std::optional<ResourceAllocator::TextureKey> ResourceAllocatorDisposer::checkin(
backend::TextureHandle handle) {
// find the texture in the in-use list (it must be there!)
auto it = mInUseTextures.find(handle);
assert_invariant(it != mInUseTextures.end());
if (it == mInUseTextures.end()) {
return std::nullopt;
}
TextureKey const key = it->second;
// remove it from the in-use list
mInUseTextures.erase(it);
return key;
}
} // namespace filament

View File

@@ -28,16 +28,29 @@
#include <utils/Hash.h>
#include <array>
#include <vector>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#include <cstddef>
#include <stddef.h>
#include <stdint.h>
namespace filament {
class ResourceAllocatorDisposer;
// The only reason we use an interface here is for unit-tests, so we can mock this allocator.
// This is not too time-critical, so that's okay.
class ResourceAllocatorDisposerInterface {
public:
virtual void destroy(backend::TextureHandle handle) noexcept = 0;
protected:
virtual ~ResourceAllocatorDisposerInterface();
};
class ResourceAllocatorInterface {
public:
virtual backend::RenderTargetHandle createRenderTarget(const char* name,
@@ -60,15 +73,20 @@ public:
virtual void destroyTexture(backend::TextureHandle h) noexcept = 0;
virtual ResourceAllocatorDisposerInterface& getDisposer() noexcept = 0;
protected:
virtual ~ResourceAllocatorInterface();
};
class ResourceAllocator final : public ResourceAllocatorInterface {
public:
explicit ResourceAllocator(std::shared_ptr<ResourceAllocatorDisposer> disposer,
Engine::Config const& config, backend::DriverApi& driverApi) noexcept;
explicit ResourceAllocator(
Engine::Config const& config, backend::DriverApi& driverApi) noexcept;
~ResourceAllocator() noexcept override;
void terminate() noexcept;
@@ -93,7 +111,9 @@ public:
void destroyTexture(backend::TextureHandle h) noexcept override;
void gc() noexcept;
ResourceAllocatorDisposerInterface& getDisposer() noexcept override;
void gc(bool skippedFrame = false) noexcept;
private:
size_t const mCacheMaxAge;
@@ -181,6 +201,7 @@ private:
using value_type = typename Container::value_type::second_type;
size_t size() const { return mContainer.size(); }
bool empty() const { return size() == 0; }
iterator begin() { return mContainer.begin(); }
const_iterator begin() const { return mContainer.begin(); }
iterator end() { return mContainer.end(); }
@@ -193,16 +214,36 @@ private:
};
using CacheContainer = AssociativeContainer<TextureKey, TextureCachePayload>;
using InUseContainer = AssociativeContainer<backend::TextureHandle, TextureKey>;
void purge(ResourceAllocator::CacheContainer::iterator const& pos);
backend::DriverApi& mBackend;
std::shared_ptr<ResourceAllocatorDisposer> mDisposer;
CacheContainer mTextureCache;
InUseContainer mInUseTextures;
size_t mAge = 0;
uint32_t mCacheSize = 0;
uint32_t mCacheSizeHiWaterMark = 0;
static constexpr bool mEnabled = true;
friend class ResourceAllocatorDisposer;
};
class ResourceAllocatorDisposer final : public ResourceAllocatorDisposerInterface {
using TextureKey = ResourceAllocator::TextureKey;
public:
explicit ResourceAllocatorDisposer(backend::DriverApi& driverApi) noexcept;
~ResourceAllocatorDisposer() noexcept override;
void terminate() noexcept;
void destroy(backend::TextureHandle handle) noexcept override;
private:
friend class ResourceAllocator;
void checkout(backend::TextureHandle handle, TextureKey key);
std::optional<TextureKey> checkin(backend::TextureHandle handle);
using InUseContainer = ResourceAllocator::AssociativeContainer<backend::TextureHandle, TextureKey>;
backend::DriverApi& mBackend;
InUseContainer mInUseTextures;
};
} // namespace filament

View File

@@ -15,6 +15,8 @@
*/
#include "details/View.h"
#include "filament/View.h"
namespace filament {
@@ -312,4 +314,8 @@ utils::Entity View::getFogEntity() const noexcept {
return downcast(this)->getFogEntity();
}
void View::clearFrameHistory(Engine& engine) noexcept {
downcast(this)->clearFrameHistory(downcast(engine));
}
} // namespace filament

View File

@@ -163,8 +163,7 @@ FEngine* FEngine::getEngine(void* token) {
FILAMENT_CHECK_PRECONDITION(ThreadUtils::isThisThread(instance->mMainThreadId))
<< "Engine::createAsync() and Engine::getEngine() must be called on the same thread.";
// we use mResourceAllocator as a proxy for "am I already initialized"
if (!instance->mResourceAllocator) {
if (!instance->mInitialized) {
if (UTILS_UNLIKELY(!instance->mDriver)) {
// something went horribly wrong during driver initialization
instance->mDriverThread.join();
@@ -262,7 +261,7 @@ void FEngine::init() {
slog.i << "FEngine feature level: " << int(mActiveFeatureLevel) << io::endl;
mResourceAllocator = new ResourceAllocator(mConfig, driverApi);
mResourceAllocatorDisposer = std::make_shared<ResourceAllocatorDisposer>(driverApi);
mFullScreenTriangleVb = downcast(VertexBuffer::Builder()
.vertexCount(3)
@@ -425,11 +424,13 @@ void FEngine::init() {
}
});
});
mInitialized = true;
}
FEngine::~FEngine() noexcept {
SYSTRACE_CALL();
delete mResourceAllocator;
assert_invariant(!mResourceAllocatorDisposer);
delete mDriver;
if (mOwnPlatform) {
PlatformFactory::destroy(&mPlatform);
@@ -440,7 +441,7 @@ void FEngine::shutdown() {
SYSTRACE_CALL();
// by construction this should never be nullptr
assert_invariant(mResourceAllocator);
assert_invariant(mResourceAllocatorDisposer);
FILAMENT_CHECK_PRECONDITION(ThreadUtils::isThisThread(mMainThreadId))
<< "Engine::shutdown() called from the wrong thread!";
@@ -460,7 +461,8 @@ void FEngine::shutdown() {
*/
mPostProcessManager.terminate(driver); // free-up post-process manager resources
mResourceAllocator->terminate();
mResourceAllocatorDisposer->terminate();
mResourceAllocatorDisposer.reset();
mDFG.terminate(*this); // free-up the DFG
mRenderableManager.terminate(); // free-up all renderables
mLightManager.terminate(); // free-up all lights

View File

@@ -86,6 +86,7 @@ namespace filament {
class Renderer;
class MaterialParser;
class ResourceAllocatorDisposer;
namespace backend {
class Driver;
@@ -254,9 +255,13 @@ public:
}
}
ResourceAllocator& getResourceAllocator() noexcept {
assert_invariant(mResourceAllocator);
return *mResourceAllocator;
ResourceAllocatorDisposer& getResourceAllocatorDisposer() noexcept {
assert_invariant(mResourceAllocatorDisposer);
return *mResourceAllocatorDisposer;
}
std::shared_ptr<ResourceAllocatorDisposer> const& getSharedResourceAllocatorDisposer() noexcept {
return mResourceAllocatorDisposer;
}
void* streamAlloc(size_t size, size_t alignment) noexcept;
@@ -490,7 +495,7 @@ private:
FTransformManager mTransformManager;
FLightManager mLightManager;
FCameraManager mCameraManager;
ResourceAllocator* mResourceAllocator = nullptr;
std::shared_ptr<ResourceAllocatorDisposer> mResourceAllocatorDisposer;
HwVertexBufferInfoFactory mHwVertexBufferInfoFactory;
ResourceList<FBufferObject> mBufferObjects{ "BufferObject" };
@@ -562,6 +567,8 @@ private:
std::thread::id mMainThreadId{};
bool mInitialized = false;
// Creation parameters
Config mConfig;

View File

@@ -60,6 +60,7 @@
#include <chrono>
#include <limits>
#include <memory>
#include <utility>
#include <stddef.h>
@@ -88,7 +89,11 @@ FRenderer::FRenderer(FEngine& engine) :
mHdrQualityMedium(TextureFormat::R11F_G11F_B10F),
mHdrQualityHigh(TextureFormat::RGB16F),
mIsRGB8Supported(false),
mUserEpoch(engine.getEngineEpoch())
mUserEpoch(engine.getEngineEpoch()),
mResourceAllocator(std::make_unique<ResourceAllocator>(
engine.getSharedResourceAllocatorDisposer(),
engine.getConfig(),
engine.getDriverApi()))
{
FDebugRegistry& debugRegistry = engine.getDebugRegistry();
debugRegistry.registerProperty("d.renderer.doFrameCapture",
@@ -176,6 +181,7 @@ void FRenderer::terminate(FEngine& engine) {
}
mFrameInfoManager.terminate(driver);
mFrameSkipper.terminate(driver);
mResourceAllocator->terminate();
}
void FRenderer::resetUserTime() {
@@ -236,6 +242,39 @@ void FRenderer::setVsyncTime(uint64_t steadyClockTimeNano) noexcept {
mVsyncSteadyClockTimeNano = steadyClockTimeNano;
}
void FRenderer::skipFrame(uint64_t vsyncSteadyClockTimeNano) {
SYSTRACE_CALL();
FILAMENT_CHECK_PRECONDITION(!mSwapChain) <<
"skipFrame() can't be called between beginFrame() and endFrame()";
if (!vsyncSteadyClockTimeNano) {
vsyncSteadyClockTimeNano = mVsyncSteadyClockTimeNano;
mVsyncSteadyClockTimeNano = 0;
}
FEngine& engine = mEngine;
FEngine::DriverApi& driver = engine.getDriverApi();
// Gives the backend a chance to execute periodic tasks. This must be called before
// the frame skipper.
driver.tick();
// do this before engine.flush()
mResourceAllocator->gc(true);
// Run the component managers' GC in parallel
// WARNING: while doing this we can't access any component manager
auto& js = engine.getJobSystem();
auto *job = js.runAndRetain(jobs::createJob(js, nullptr, &FEngine::gc, &engine)); // gc all managers
engine.flush(); // flush command stream
// make sure we're done with the gcs
js.waitAndRelease(job);
}
bool FRenderer::beginFrame(FSwapChain* swapChain, uint64_t vsyncSteadyClockTimeNano) {
assert_invariant(swapChain);
@@ -380,7 +419,7 @@ void FRenderer::endFrame() {
}
// do this before engine.flush()
engine.getResourceAllocator().gc();
mResourceAllocator->gc();
// Run the component managers' GC in parallel
// WARNING: while doing this we can't access any component manager
@@ -750,7 +789,7 @@ void FRenderer::renderJob(RootArenaScope& rootArenaScope, FView& view) {
* Frame graph
*/
FrameGraph fg(engine.getResourceAllocator(),
FrameGraph fg(*mResourceAllocator,
isProtectedContent ? FrameGraph::Mode::PROTECTED : FrameGraph::Mode::UNPROTECTED);
auto& blackboard = fg.getBlackboard();

View File

@@ -46,6 +46,7 @@
#include <algorithm>
#include <chrono>
#include <functional>
#include <memory>
#include <utility>
#include <stddef.h>
@@ -53,6 +54,8 @@
namespace filament {
class ResourceAllocator;
namespace backend {
class Driver;
} // namespace backend
@@ -88,6 +91,9 @@ public:
void setVsyncTime(uint64_t steadyClockTimeNano) noexcept;
// skip a frame
void skipFrame(uint64_t vsyncSteadyClockTimeNano);
// start a frame
bool beginFrame(FSwapChain* swapChain, uint64_t vsyncSteadyClockTimeNano);
@@ -206,6 +212,7 @@ private:
tsl::robin_set<FRenderTarget*> mPreviousRenderTargets;
std::function<void()> mBeginFrameInternal;
uint64_t mVsyncSteadyClockTimeNano = 0;
std::unique_ptr<ResourceAllocator> mResourceAllocator{};
};
FILAMENT_DOWNCAST(Renderer)

View File

@@ -248,7 +248,7 @@ void FScene::prepare(utils::JobSystem& js,
auto* renderableJob = jobs::parallel_for(js, rootJob,
renderableInstances.data(), renderableInstances.size(),
std::cref(renderableWork), jobs::CountSplitter<128, 5>());
std::cref(renderableWork), jobs::CountSplitter<64>());
auto* lightJob = jobs::parallel_for(js, rootJob,
lightInstances.data(), lightInstances.size(),

View File

@@ -17,6 +17,7 @@
#include "details/View.h"
#include "Culler.h"
#include "FrameHistory.h"
#include "Froxelizer.h"
#include "RenderPrimitive.h"
#include "ResourceAllocator.h"
@@ -118,7 +119,7 @@ void FView::terminate(FEngine& engine) {
DriverApi& driver = engine.getDriverApi();
driver.destroyBufferObject(mLightUbh);
driver.destroyBufferObject(mRenderableUbh);
drainFrameHistory(engine);
clearFrameHistory(engine);
ShadowMapManager::terminate(engine, mShadowMapManager);
mPerViewUniforms.terminate(driver);
@@ -1007,20 +1008,29 @@ FrameGraphId<FrameGraphTexture> FView::renderShadowMaps(FEngine& engine, FrameGr
void FView::commitFrameHistory(FEngine& engine) noexcept {
// Here we need to destroy resources in mFrameHistory.back()
auto& disposer = engine.getResourceAllocatorDisposer();
auto& frameHistory = mFrameHistory;
FrameHistoryEntry& last = frameHistory.back();
last.taa.color.destroy(engine.getResourceAllocator());
last.ssr.color.destroy(engine.getResourceAllocator());
disposer.destroy(last.taa.color.handle);
disposer.destroy(last.ssr.color.handle);
last.taa.color.handle.clear();
last.ssr.color.handle.clear();
// and then push the new history entry to the history stack
frameHistory.commit();
}
void FView::drainFrameHistory(FEngine& engine) noexcept {
void FView::clearFrameHistory(FEngine& engine) noexcept {
// make sure we free all resources in the history
for (size_t i = 0; i < mFrameHistory.size(); ++i) {
commitFrameHistory(engine);
auto& disposer = engine.getResourceAllocatorDisposer();
auto& frameHistory = mFrameHistory;
for (size_t i = 0; i < frameHistory.size(); ++i) {
FrameHistoryEntry& last = frameHistory[i];
disposer.destroy(last.taa.color.handle);
disposer.destroy(last.ssr.color.handle);
last.taa.color.handle.clear();
last.ssr.color.handle.clear();
}
}

View File

@@ -422,6 +422,10 @@ public:
// (e.g.: after the FrameGraph execution).
void commitFrameHistory(FEngine& engine) noexcept;
// Clean-up the whole history, free all resources. This is typically called when the View is
// being terminated. Or we're changing Renderer.
void clearFrameHistory(FEngine& engine) noexcept;
// create the picking query
View::PickingQuery& pick(uint32_t x, uint32_t y, backend::CallbackHandler* handler,
View::PickingQueryResultCallback callback) noexcept;
@@ -485,10 +489,6 @@ private:
Culler::result_type* visibleMask,
size_t count);
// Clean-up the whole history, free all resources. This is typically called when the View is
// being terminated.
void drainFrameHistory(FEngine& engine) noexcept;
// we don't inline this one, because the function is quite large and there is not much to
// gain from inlining.
static FScene::RenderableSoa::iterator partition(

View File

@@ -24,6 +24,7 @@
#include "FrameGraphPass.h"
#include "FrameGraphRenderPass.h"
#include "FrameGraphTexture.h"
#include "ResourceAllocator.h"
#include "details/Engine.h"

View File

@@ -62,6 +62,7 @@ if (ANDROID)
target_link_libraries(test_compiler PRIVATE utils)
target_link_libraries(test_compiler PRIVATE EGL)
target_link_libraries(test_compiler PRIVATE GLESv3)
target_link_libraries(test_compiler PRIVATE bluevk)
endif()
add_executable(test_material_parser

View File

@@ -1,181 +1,82 @@
/*
* Copyright (C) 2023 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 <iostream>
#include <utils/Hash.h>
#include <tsl/robin_map.h>
#include <bluevk/BlueVK.h>
#include <utils/Log.h>
namespace {
#include <GLES3/gl3.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <gtest/gtest.h>
using namespace utils;
using namespace std::literals;
class CompilerTest : public testing::Test {
protected:
void SetUp() override {
EGLBoolean success;
EGLint major, minor;
dpy = eglGetDisplay(EGL_DEFAULT_DISPLAY);
ASSERT_NE(dpy, EGL_NO_DISPLAY);
EGLBoolean const initialized = eglInitialize(dpy, &major, &minor);
ASSERT_TRUE(initialized);
EGLint const contextAttribs[] = {
EGL_CONTEXT_CLIENT_VERSION, 3,
EGL_NONE
};
EGLint configsCount;
EGLint configAttribs[] = {
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR, // 0
EGL_RED_SIZE, 8, // 2
EGL_GREEN_SIZE, 8, // 4
EGL_BLUE_SIZE, 8, // 6
EGL_NONE // 14
};
success = eglChooseConfig(dpy, configAttribs, &config, 1, &configsCount);
ASSERT_TRUE(success);
context = eglCreateContext(dpy, config, EGL_NO_CONTEXT, contextAttribs);
ASSERT_NE(context, EGL_NO_CONTEXT);
success = eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, context);
ASSERT_TRUE(success);
ASSERT_EQ(eglGetError(), EGL_SUCCESS);
}
void TearDown() override {
eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglDestroyContext(dpy, context);
eglTerminate(dpy);
}
private:
EGLDisplay dpy;
EGLContext context;
EGLConfig config;
struct T {
};
TEST_F(CompilerTest, Simple) {
auto shader = R"(
#version 300 es
void main()
{
})"sv;
constexpr uint8_t const UNIQUE_DESCRIPTOR_SET_COUNT = 3;
constexpr uint8_t const SHADER_TYPE_COUNT = 3;
using Timestamp = uint64_t;
using VulkanResourceAllocator = T;
const char* const src = shader.data();
GLint const len = (GLint)shader.size();
struct PushConstantKey {
uint8_t stage;// We have one set of push constant per shader stage (fragment, vertex, etc).
uint8_t size;
};
GLuint const id = glCreateShader(GL_VERTEX_SHADER);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
struct PipelineLayoutKey {
using DescriptorSetLayoutArray = std::array<VkDescriptorSetLayout, UNIQUE_DESCRIPTOR_SET_COUNT>;
DescriptorSetLayoutArray descSetLayouts = {}; // 8 * 3
PushConstantKey pushConstant[SHADER_TYPE_COUNT] = {};// 2 * 3
uint16_t padding = 0;
};
static_assert(sizeof(PipelineLayoutKey) == 32);
glShaderSource(id, 1, &src, &len);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
using PipelineLayoutKeyHashFn = utils::hash::MurmurHashFn<PipelineLayoutKey>;
struct PipelineLayoutKeyEqual {
bool operator()(PipelineLayoutKey const& k1, PipelineLayoutKey const& k2) const {
return 0 == memcmp((const void*) &k1, (const void*) &k2, sizeof(PipelineLayoutKey));
}
};
glCompileShader(id);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
struct PipelineLayoutCacheEntry {
VkPipelineLayout handle;
Timestamp lastUsed;
};
GLint result = 0;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
EXPECT_EQ(result, GL_TRUE);
using PipelineLayoutMap = tsl::robin_map<PipelineLayoutKey, PipelineLayoutCacheEntry,
PipelineLayoutKeyHashFn, PipelineLayoutKeyEqual>;
glDeleteShader(id);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
class TestClass {
public:
VkDevice mDevice;
VulkanResourceAllocator* mAllocator;
Timestamp mTimestamp;
PipelineLayoutMap layout;
};
void insert(PipelineLayoutMap& layout, uint64_t val) {
PipelineLayoutKey key{};
key.descSetLayouts[0] = (VkDescriptorSetLayout) val;
layout[key] = {(VkPipelineLayout) val, val};
}
TEST_F(CompilerTest, CrashPVRUniFlexCompileToHw) {
// Some PowerVR driver crash with this shader
auto shader = R"(
#version 300 es
layout(location = 0) in vec4 mesh_position;
layout(std140) uniform FrameUniforms {
vec2 i;
} frameUniforms;
void main() {
gl_Position = mesh_position;
gl_Position.z = dot(gl_Position.zw, frameUniforms.i);
})"sv;
const char* const src = shader.data();
GLint const len = (GLint)shader.size();
GLuint const id = glCreateShader(GL_VERTEX_SHADER);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
glShaderSource(id, 1, &src, &len);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
glCompileShader(id);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
GLint result = 0;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
EXPECT_EQ(result, GL_TRUE);
glDeleteShader(id);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
TEST_F(CompilerTest, ConstParameters) {
// Some PowerVR driver fail to compile this shader
auto shader = R"(
#version 300 es
highp mat3 m;
void buggy(const mediump vec3 n) {
m*n;
}
void main() {
})"sv;
const char* const src = shader.data();
GLint const len = (GLint)shader.size();
GLuint const id = glCreateShader(GL_FRAGMENT_SHADER);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
glShaderSource(id, 1, &src, &len);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
glCompileShader(id);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
GLint result = 0;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
EXPECT_EQ(result, GL_TRUE);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
glDeleteShader(id);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
}// namespace
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
constexpr uint32_t START = 1000;
TestClass c;
auto& layout = c.layout;
uint64_t count = 0;
for (; count < 20; count++) {
insert(layout, count + START);
}
for (uint64_t t = 0; t < count; ++t) {
PipelineLayoutKey key = {};
key.descSetLayouts[0] = (VkDescriptorSetLayout)(START + t);
if (layout.find(key) != layout.end()) {
std::cout << "found " << t << std::endl;
} else {
std::cout << "not found" << t << std::endl;
}
}
return 0;
}

View File

@@ -34,6 +34,10 @@ using namespace backend;
class MockResourceAllocator : public ResourceAllocatorInterface {
uint32_t handle = 0;
struct MockDisposer : public ResourceAllocatorDisposerInterface {
void destroy(backend::TextureHandle) noexcept override {}
} disposer;
public:
backend::RenderTargetHandle createRenderTarget(const char* name,
backend::TargetBufferFlags targetBufferFlags,
@@ -60,6 +64,10 @@ public:
void destroyTexture(backend::TextureHandle h) noexcept override {
}
ResourceAllocatorDisposerInterface& getDisposer() noexcept override {
return disposer;
}
};
class FrameGraphTest : public testing::Test {

View File

@@ -58,6 +58,8 @@
#include "downcast.h"
#include <codecvt>
#include <locale>
#include <memory>
using namespace filament;
@@ -88,12 +90,57 @@ static constexpr cgltf_material kDefaultMat = {
},
};
static const char* getNodeName(const cgltf_node* node, const char* defaultNodeName) {
if (node->name) return node->name;
if (node->mesh && node->mesh->name) return node->mesh->name;
if (node->light && node->light->name) return node->light->name;
if (node->camera && node->camera->name) return node->camera->name;
return defaultNodeName;
static std::string getNodeName(cgltf_node const* node, char const* defaultNodeName) {
auto const getNameImpl = [node, defaultNodeName]() -> char const* {
if (node->name) return node->name;
if (node->mesh && node->mesh->name) return node->mesh->name;
if (node->light && node->light->name) return node->light->name;
if (node->camera && node->camera->name) return node->camera->name;
return defaultNodeName;
};
std::string strOrig(getNameImpl());
// We handle the potential case of escaped characters in the JSON which should be properly
// interpreted as unicode. See spec:
// https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#json-encoding
// Also see spec for escaped strings in JSON (Section 2.5) https://www.ietf.org/rfc/rfc4627.txt
std::string strEscaped;
size_t cur = 0, idx = 0;
std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> conv;
auto const addUnencodedSubstr = [&](size_t cursor, size_t nextPoint) {
assert_invariant(nextPoint >= cursor);
if (cursor == nextPoint) {
return;
}
strEscaped += strOrig.substr(cursor, nextPoint - cursor);
};
while ((idx = strOrig.find("\\u", cur)) != std::string::npos) {
if (idx + 6 > strOrig.length()) {
utils::slog.w << "gltfio: Unable to interpret node name=" << strOrig
<< " as proper unicode encoding." << utils::io::endl;
return strOrig;
}
// Turns string of the form \u0062 to 0x0062
std::string const hexStr = strOrig.substr(idx + 2, 4);
if (hexStr.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos) {
utils::slog.w << "gltfio: Unable to interpret node name=" << strOrig
<< " as proper unicode encoding." << utils::io::endl;
return strOrig;
}
addUnencodedSubstr(cur, idx);
strEscaped += conv.to_bytes((char32_t) std::stoul(hexStr, nullptr, 16));
cur = idx + 6;
}
addUnencodedSubstr(cur, strOrig.length());
return strEscaped;
}
static bool primitiveHasVertexColor(const cgltf_primitive& inPrim) {
@@ -504,7 +551,8 @@ FFilamentAsset* FAssetLoader::createRootAsset(const cgltf_data* srcAsset) {
}
void FAssetLoader::recursePrimitives(const cgltf_node* node, FFilamentAsset* fAsset) {
const char* name = getNodeName(node, mDefaultNodeName);
auto nameStr = getNodeName(node, mDefaultNodeName);
const char* name = nameStr.c_str();
name = name ? name : "node";
if (node->mesh) {
@@ -573,7 +621,8 @@ void FAssetLoader::recurseEntities(const cgltf_node* node, SceneMask scenes, Ent
instance->mEntities.push_back(entity);
instance->mNodeMap[node - srcAsset->nodes] = entity;
const char* name = getNodeName(node, mDefaultNodeName);
auto nameStr = getNodeName(node, mDefaultNodeName);
const char* name = nameStr.c_str();
if (name) {
fAsset->mNameToEntity[name].push_back(entity);

View File

@@ -44,7 +44,9 @@
namespace utils {
class JobSystem {
static constexpr size_t MAX_JOB_COUNT = 16384;
static constexpr size_t MAX_JOB_COUNT = 1 << 14; // 16384
static constexpr uint32_t JOB_COUNT_MASK = MAX_JOB_COUNT - 1;
static constexpr uint32_t WAITER_COUNT_SHIFT = 24;
static_assert(MAX_JOB_COUNT <= 0x7FFE, "MAX_JOB_COUNT must be <= 0x7FFE");
using WorkQueue = WorkStealingDequeue<uint16_t, MAX_JOB_COUNT>;
using Mutex = utils::Mutex;
@@ -81,14 +83,18 @@ public:
void* storage[JOB_STORAGE_SIZE_WORDS]; // 48 | 48
JobFunc function; // 4 | 8
uint16_t parent; // 2 | 2
std::atomic<uint16_t> runningJobCount = { 1 }; // 2 | 2
mutable std::atomic<uint16_t> refCount = { 1 }; // 2 | 2
mutable ThreadId id = invalidThreadId; // 1 | 1
// 1 | 1 (padding)
mutable std::atomic<uint8_t> refCount = { 1 }; // 1 | 1
std::atomic<uint32_t> runningJobCount = { 1 }; // 4 | 4
// 4 | 0 (padding)
// 64 | 64
};
#ifndef WIN32
// on windows std::function<void()> is bigger and forces the whole structure to be larger
static_assert(sizeof(Job) == 64);
#endif
explicit JobSystem(size_t threadCount = 0, size_t adoptableThreadsCount = 1) noexcept;
~JobSystem();
@@ -207,6 +213,21 @@ public:
return job;
}
// creates a job from a KNOWN method pointer w/ object passed by value
template<typename T, void(T::*method)(JobSystem&, Job*), typename ... ARGS>
Job* emplaceJob(Job* parent, ARGS&& ... args) noexcept {
static_assert(sizeof(T) <= sizeof(Job::storage), "user data too large");
Job* job = create(parent, [](void* storage, JobSystem& js, Job* job) {
T* const that = static_cast<T*>(storage);
(that->*method)(js, job);
that->~T();
});
if (job) {
new(job->storage) T(std::forward<ARGS>(args)...);
}
return job;
}
// creates a job from a functor passed by value
template<typename T>
Job* createJob(Job* parent, T functor) noexcept {
@@ -222,6 +243,21 @@ public:
return job;
}
// creates a job from a functor passed by value
template<typename T, typename ... ARGS>
Job* emplaceJob(Job* parent, ARGS&& ... args) noexcept {
static_assert(sizeof(T) <= sizeof(Job::storage), "functor too large");
Job* job = create(parent, [](void* storage, JobSystem& js, Job* job){
T* const that = static_cast<T*>(storage);
that->operator()(js, job);
that->~T();
});
if (job) {
new(job->storage) T(std::forward<ARGS>(args)...);
}
return job;
}
/*
* Jobs are normally finished automatically, this can be used to cancel a job before it is run.
@@ -345,6 +381,16 @@ private:
static constexpr uint32_t m = 0x7fffffffu;
uint32_t mState; // must be 0 < seed < 0x7fffffff
public:
using result_type = uint32_t;
static constexpr result_type min() noexcept {
return 1;
}
static constexpr result_type max() noexcept {
return m - 1;
}
inline constexpr explicit default_random_engine(uint32_t seed = 1u) noexcept
: mState(((seed % m) == 0u) ? 1u : seed % m) {
}
@@ -389,7 +435,9 @@ private:
Job* pop(WorkQueue& workQueue) noexcept;
Job* steal(WorkQueue& workQueue) noexcept;
void wait(std::unique_lock<Mutex>& lock, Job* job = nullptr) noexcept;
[[nodiscard]]
uint32_t wait(std::unique_lock<Mutex>& lock, Job* job) noexcept;
void wait(std::unique_lock<Mutex>& lock) noexcept;
void wakeAll() noexcept;
void wakeOne() noexcept;
@@ -418,7 +466,7 @@ private:
uint8_t mParallelSplitCount = 0; // # of split allowable in parallel_for
Job* mRootJob = nullptr;
utils::Mutex mThreadMapLock; // this should have very little contention
Mutex mThreadMapLock; // this should have very little contention
tsl::robin_map<std::thread::id, ThreadState *> mThreadMap;
};
@@ -436,12 +484,13 @@ template<typename CALLABLE, typename ... ARGS>
JobSystem::Job* createJob(JobSystem& js, JobSystem::Job* parent,
CALLABLE&& func, ARGS&&... args) noexcept {
struct Data {
explicit Data(std::function<void()> f) noexcept: f(std::move(f)) {}
std::function<void()> f;
// Renaming the method below could cause an Arrested Development.
void gob(JobSystem&, JobSystem::Job*) noexcept { f(); }
} user{ std::bind(std::forward<CALLABLE>(func),
std::forward<ARGS>(args)...) };
return js.createJob<Data, &Data::gob>(parent, std::move(user));
};
return js.emplaceJob<Data, &Data::gob>(parent,
std::bind(std::forward<CALLABLE>(func), std::forward<ARGS>(args)...));
}
template<typename CALLABLE, typename T, typename ... ARGS,
@@ -452,12 +501,13 @@ template<typename CALLABLE, typename T, typename ... ARGS,
JobSystem::Job* createJob(JobSystem& js, JobSystem::Job* parent,
CALLABLE&& func, T&& o, ARGS&&... args) noexcept {
struct Data {
explicit Data(std::function<void()> f) noexcept: f(std::move(f)) {}
std::function<void()> f;
// Renaming the method below could cause an Arrested Development.
void gob(JobSystem&, JobSystem::Job*) noexcept { f(); }
} user{ std::bind(std::forward<CALLABLE>(func), std::forward<T>(o),
std::forward<ARGS>(args)...) };
return js.createJob<Data, &Data::gob>(parent, std::move(user));
};
return js.emplaceJob<Data, &Data::gob>(parent,
std::bind(std::forward<CALLABLE>(func), std::forward<T>(o), std::forward<ARGS>(args)...));
}
@@ -486,8 +536,8 @@ struct ParallelForJobData {
right_side:
if (splitter.split(splits, count)) {
const size_type lc = count / 2;
JobData ld(start, lc, splits + uint8_t(1), functor, splitter);
JobSystem::Job* l = js.createJob<JobData, &JobData::parallelWithJobs>(parent, std::move(ld));
JobSystem::Job* l = js.emplaceJob<JobData, &JobData::parallelWithJobs>(parent,
start, lc, splits + uint8_t(1), functor, splitter);
if (UTILS_UNLIKELY(l == nullptr)) {
// couldn't create a job, just pretend we're done splitting
goto execute;
@@ -527,8 +577,8 @@ template<typename S, typename F>
JobSystem::Job* parallel_for(JobSystem& js, JobSystem::Job* parent,
uint32_t start, uint32_t count, F functor, const S& splitter) noexcept {
using JobData = details::ParallelForJobData<S, F>;
JobData jobData(start, count, 0, std::move(functor), splitter);
return js.createJob<JobData, &JobData::parallelWithJobs>(parent, std::move(jobData));
return js.emplaceJob<JobData, &JobData::parallelWithJobs>(parent,
start, count, 0, std::move(functor), splitter);
}
// parallel jobs with pointer/count
@@ -539,8 +589,8 @@ JobSystem::Job* parallel_for(JobSystem& js, JobSystem::Job* parent,
f(data + s, c);
};
using JobData = details::ParallelForJobData<S, decltype(user)>;
JobData jobData(0, count, 0, std::move(user), splitter);
return js.createJob<JobData, &JobData::parallelWithJobs>(parent, std::move(jobData));
return js.emplaceJob<JobData, &JobData::parallelWithJobs>(parent,
0, count, 0, std::move(user), splitter);
}
// parallel jobs on a Slice<>

View File

@@ -35,7 +35,13 @@ namespace utils {
* steal() push(), pop()
* any thread main thread
*
*
* References:
* - This code is largely inspired from
* https://blog.molecular-matters.com/2015/09/25/job-system-2-0-lock-free-work-stealing-part-3-going-lock-free/
* - other implementations
* https://github.com/ConorWilliams/ConcurrentDeque/blob/main/include/riften/deque.hpp
* https://github.com/ssbl/concurrent-deque/blob/master/include/deque.hpp
* https://github.com/taskflow/work-stealing-queue/blob/master/wsq.hpp
*/
template <typename TYPE, size_t COUNT>
class WorkStealingDequeue {
@@ -117,7 +123,7 @@ TYPE WorkStealingDequeue<TYPE, COUNT>::pop() noexcept {
index_t top = mTop.load(std::memory_order_seq_cst);
if (top < bottom) {
// Queue isn't empty and it's not the last item, just return it, this is the common case.
// Queue isn't empty, and it's not the last item, just return it, this is the common case.
return getItemAt(bottom);
}
@@ -132,13 +138,13 @@ TYPE WorkStealingDequeue<TYPE, COUNT>::pop() noexcept {
if (mTop.compare_exchange_strong(top, top + 1,
std::memory_order_seq_cst,
std::memory_order_relaxed)) {
// success: we stole our last item from ourself, meaning that a concurrent steal()
// Success: we stole our last item from ourselves, meaning that a concurrent steal()
// would have failed.
// mTop now equals top + 1, we adjust top to make the queue empty.
top++;
} else {
// failure: mTop was not equal to top, which means the item was stolen under our feet.
// top now equals to mTop. Simply discard the item we just popped.
// Failure: mTop was not equal to top, which means the item was stolen under our feet.
// `top` now equals to mTop. Simply discard the item we just popped.
// The queue is now empty.
item = TYPE();
}
@@ -149,7 +155,7 @@ TYPE WorkStealingDequeue<TYPE, COUNT>::pop() noexcept {
}
// std::memory_order_relaxed used because we're not publishing any data.
// no concurrent writes to mBottom possible, it's always safe to write mBottom.
// No concurrent writes to mBottom possible, it's always safe to write mBottom.
mBottom.store(top, std::memory_order_relaxed);
return item;
}
@@ -194,6 +200,8 @@ TYPE WorkStealingDequeue<TYPE, COUNT>::steal() noexcept {
}
// failure: the item we just tried to steal was pop()'ed under our feet,
// simply discard it; nothing to do -- it's okay to try again.
// However, item might be corrupted, so it must be trivially destructible
static_assert(std::is_trivially_destructible_v<TYPE>);
}
}

View File

@@ -23,9 +23,6 @@
// 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>
@@ -249,53 +246,44 @@ inline bool JobSystem::hasActiveJobs() const noexcept {
}
inline bool JobSystem::hasJobCompleted(JobSystem::Job const* job) noexcept {
return job->runningJobCount.load(std::memory_order_acquire) <= 0;
return (job->runningJobCount.load(std::memory_order_acquire) & JOB_COUNT_MASK) == 0;
}
void JobSystem::wait(std::unique_lock<Mutex>& lock, Job* job) noexcept {
inline void JobSystem::wait(std::unique_lock<Mutex>& lock) noexcept {
HEAVY_SYSTRACE_CALL();
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.
size_t const id = std::distance(mThreadStates.data(), &getState());
auto activeJobs = mActiveJobs.load();
if (job) {
auto runningJobCount = job->runningJobCount.load();
FILAMENT_CHECK_POSTCONDITION(runningJobCount > 0)
<< "JobSystem(" << this << ", " << unsigned(id) << "): waiting while job "
<< job << " has completed and " << activeJobs << " jobs are active!";
}
FILAMENT_CHECK_POSTCONDITION(activeJobs <= 0)
<< "JobSystem(" << this << ", " << unsigned(id) << "): waiting while "
<< activeJobs << " jobs are active!";
} while (true);
}
mWaiterCondition.wait(lock);
}
inline uint32_t JobSystem::wait(std::unique_lock<Mutex>& lock, Job* const job) noexcept {
HEAVY_SYSTRACE_CALL();
// signal we are waiting
if (hasActiveJobs() || exitRequested()) {
return job->runningJobCount.load(std::memory_order_acquire);
}
uint32_t runningJobCount =
job->runningJobCount.fetch_add(1 << WAITER_COUNT_SHIFT, std::memory_order_relaxed);
if (runningJobCount & JOB_COUNT_MASK) {
mWaiterCondition.wait(lock);
}
runningJobCount =
job->runningJobCount.fetch_sub(1 << WAITER_COUNT_SHIFT, std::memory_order_acquire);
assert_invariant((runningJobCount >> WAITER_COUNT_SHIFT) >= 1);
return runningJobCount;
}
UTILS_NOINLINE
void JobSystem::wakeAll() noexcept {
// wakeAll() is called when a job finishes (to wake up any thread that might be waiting on it)
HEAVY_SYSTRACE_CALL();
SYSTRACE_CALL();
mWaiterLock.lock();
// this empty critical section is needed -- it guarantees that notify_all() happens
// after the condition's variables are set.
// either before the condition is checked, or after the condition variable sleeps.
mWaiterLock.unlock();
// notify_all() can be pretty slow, and it doesn't need to be inside the lock.
mWaiterCondition.notify_all();
@@ -306,7 +294,7 @@ void JobSystem::wakeOne() noexcept {
HEAVY_SYSTRACE_CALL();
mWaiterLock.lock();
// this empty critical section is needed -- it guarantees that notify_one() happens
// after the condition's variables are set.
// either before the condition is checked, or after the condition variable sleeps.
mWaiterLock.unlock();
// notify_one() can be pretty slow, and it doesn't need to be inside the lock.
mWaiterCondition.notify_one();
@@ -328,50 +316,37 @@ void JobSystem::put(WorkQueue& workQueue, Job* job) noexcept {
size_t const index = job - mJobStorageBase;
assert(index >= 0 && index < MAX_JOB_COUNT);
// put the job into the queue first
// put the job into the queue
workQueue.push(uint16_t(index + 1));
// then increase our active job count
int32_t const 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...
}
// increase our active job count (the order in which we're doing this must not matter
// because we're not using std::memory_order_seq_cst (here or in WorkQueue::push()).
mActiveJobs.fetch_add(1, std::memory_order_relaxed);
// Note: it's absolutely possible for mActiveJobs to be 0 here, because the job could have
// been handled by a zealous worker already. In that case we could avoid calling wakeOne(),
// but that is not the common case.
wakeOne();
}
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 const index = workQueue.pop();
assert(index <= MAX_JOB_COUNT);
Job* const 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) {
// no need to wake someone else up because, we will go into job-stealing mode
// immediately after this
mActiveJobs.fetch_add(1, std::memory_order_relaxed);
if (UTILS_LIKELY(job)) {
mActiveJobs.fetch_sub(1, std::memory_order_relaxed);
}
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 const index = workQueue.steal();
assert_invariant(index <= MAX_JOB_COUNT);
Job* const job = !index ? nullptr : &mJobStorageBase[index - 1];
if (!job) {
// If we failed taking a job, we need to correct mActiveJobs.
mActiveJobs.fetch_add(1, std::memory_order_relaxed);
if (UTILS_LIKELY(job)) {
mActiveJobs.fetch_sub(1, std::memory_order_relaxed);
}
return job;
}
@@ -402,7 +377,7 @@ JobSystem::Job* JobSystem::steal(JobSystem::ThreadState& state) noexcept {
Job* job = nullptr;
do {
ThreadState* const stateToStealFrom = getStateToStealFrom(state);
if (UTILS_LIKELY(stateToStealFrom)) {
if (stateToStealFrom) {
job = steal(stateToStealFrom->workQueue);
}
// nullptr -> nothing to steal in that queue either, if there are active jobs,
@@ -415,14 +390,18 @@ bool JobSystem::execute(JobSystem::ThreadState& state) noexcept {
HEAVY_SYSTRACE_CALL();
Job* job = pop(state.workQueue);
if (UTILS_UNLIKELY(job == nullptr)) {
// It is beneficial for some benchmarks to poll on steal() for a bit, because going back to
// sleep and waking up is pretty expensive. However, it is unclear it helps in practice with
// larger jobs or when parallel_for is used.
constexpr size_t const STEAL_TRY_COUNT = 1;
for (size_t i = 0; UTILS_UNLIKELY(!job && i < STEAL_TRY_COUNT); i++) {
// 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)) {
assert((job->runningJobCount.load(std::memory_order_relaxed) & JOB_COUNT_MASK) >= 1);
if (UTILS_LIKELY(job->function)) {
HEAVY_SYSTRACE_NAME("job->function");
job->id = std::distance(mThreadStates.data(), &state);
@@ -467,11 +446,16 @@ void JobSystem::finish(Job* job) noexcept {
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);
uint32_t const v = job->runningJobCount.fetch_sub(1, std::memory_order_acq_rel);
uint32_t const runningJobCount = v & JOB_COUNT_MASK;
assert(runningJobCount > 0);
if (runningJobCount == 1) {
// no more work, destroy this job and notify its parent
notify = true;
uint32_t const waiters = v >> WAITER_COUNT_SHIFT;
if (waiters) {
notify = true;
}
Job* const parent = job->parent == 0x7FFF ? nullptr : &storage[job->parent];
decRef(job);
job = parent;
@@ -482,7 +466,8 @@ void JobSystem::finish(Job* job) noexcept {
} while (job);
// wake-up all threads that could potentially be waiting on this job finishing
if (notify) {
if (UTILS_UNLIKELY(notify)) {
// but avoid calling notify_all() at all cost, because it's always expensive
wakeAll();
}
}
@@ -500,10 +485,11 @@ JobSystem::Job* JobSystem::create(JobSystem::Job* parent, JobFunc func) noexcept
// 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);
UTILS_UNUSED_IN_RELEASE auto const parentJobCount =
parent->runningJobCount.fetch_add(1, std::memory_order_relaxed);
// can't create a child job of a terminated parent
assert(parentJobCount > 0);
assert((parentJobCount & JOB_COUNT_MASK) > 0);
index = parent - mJobStorageBase;
assert(index < MAX_JOB_COUNT);
@@ -567,7 +553,7 @@ void JobSystem::waitAndRelease(Job*& job) noexcept {
ThreadState& state(getState());
do {
if (!execute(state)) {
if (UTILS_UNLIKELY(!execute(state))) {
// test if job has completed first, to possibly avoid taking the lock
if (hasJobCompleted(job)) {
break;
@@ -583,11 +569,23 @@ void JobSystem::waitAndRelease(Job*& job) noexcept {
// continue to handle more jobs, as they get added.
std::unique_lock<Mutex> lock(mWaiterLock);
if (!hasJobCompleted(job) && !hasActiveJobs() && !exitRequested()) {
wait(lock, job);
uint32_t const runningJobCount = wait(lock, job);
// we could be waking up because either:
// - the job we're waiting on has completed
// - more jobs where added to the JobSystem
// - we're asked to exit
if ((runningJobCount & JOB_COUNT_MASK) == 0 || exitRequested()) {
break;
}
// if we get here, it means that
// - the job we're waiting on is still running, and
// - we're not asked to exit, and
// - there were some active jobs
// So we try to handle one.
continue;
}
} while (!hasJobCompleted(job) && !exitRequested());
} while (UTILS_LIKELY(!hasJobCompleted(job) && !exitRequested()));
if (job == mRootJob) {
mRootJob = nullptr;

View File

@@ -627,6 +627,7 @@ class_<Renderer>("Renderer")
.function("getClearOptions", &Renderer::getClearOptions)
.function("setPresentationTime", &Renderer::setPresentationTime)
.function("setVsyncTime", &Renderer::setVsyncTime)
.function("skipFrame", &Renderer::skipFrame)
.function("beginFrame", EMBIND_LAMBDA(bool, (Renderer* self, SwapChain* swapChain), {
return self->beginFrame(swapChain);
}), allow_raw_pointers())
@@ -683,7 +684,8 @@ class_<View>("View")
.function("isStencilBufferEnabled", &View::isStencilBufferEnabled)
.function("setMaterialGlobal", &View::setMaterialGlobal)
.function("getMaterialGlobal", &View::getMaterialGlobal)
.function("getFogEntity", &View::getFogEntity);
.function("getFogEntity", &View::getFogEntity)
.function("clearFrameHistory", &View::clearFrameHistory);
/// Scene ::core class:: Flat container of renderables and lights.
/// See also the [Engine] methods `createScene` and `destroyScene`.