Compare commits
7 Commits
v1.53.3
...
pf/test-ro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69fc1f302c | ||
|
|
e7c96cd124 | ||
|
|
730bc99025 | ||
|
|
7441e878bb | ||
|
|
ef4c4a76fc | ||
|
|
be4f287b07 | ||
|
|
d3a35de386 |
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
#include <fg/FrameGraphTexture.h>
|
||||
|
||||
#include <math/mat4.h>
|
||||
#include <math/vec2.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace filament {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "FrameGraphPass.h"
|
||||
#include "FrameGraphRenderPass.h"
|
||||
#include "FrameGraphTexture.h"
|
||||
#include "ResourceAllocator.h"
|
||||
|
||||
#include "details/Engine.h"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<>
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user