Compare 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 CommodoreandPowei Feng 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
Sungun Park 9057c47a56 Release Filament 1.53.2 2024-07-23 01:23:57 +00:00
Mathias Agopian 4a5081388d RenderPass batch size was too small
The batch size was calculated incorrectly, which in theory could lead
to command buffer overflows (albeit unlikely).
2024-07-22 13:53:05 -07:00
Mathias Agopian a60fe41681 performance improvements to JobSystem
- reduce the number of calls to notify_one() and notify_all().
  notify_one() is not only called when running a new job, and
  notify_all() only when a job finishes.

- don't hold the condition lock while calling notify_*(), as it is not
  strictly needed, and because notify_*() can be very slow, there can
  be a lot of contention on this lock as a result; blocking the whole
  jobsystem thread pool.

- add a new version of run() that takes an opaque thread id that can
  be retrieved from a job's execute function; this is especially
  intended to be used by parallel_for(); it's just a more efficient
  version of run() that avoids a hashmap lookup.


Overall these change yield a significant performance boost:
- running + waiting a job: +200%
- running many jobs: +150%
- running many jobs in parallel: +50%
2024-07-22 11:12:51 -07:00
Mathias Agopian a8ace2891d improve frame timing info
- we use a circular buffer for the frame history so that 
  we don't have to copy the data when insert a new entry.
  This also allows us to keep a reference to an entry, which
  doesn't get invalidated when an entry is added/removed.

- we now store the gpu frame time in the correct slot (instead of
  always the latest). It didn't matter before because the API wasn't
  public and we only needed some recent frame time.

- a new public API now returns the frame history, which now contains 
  more data; in particular the main and backend thread's begin/end
  frame time.


BUGS=[321110544]
2024-07-18 21:14:44 -07:00
mdagoisandPowei Feng 669ffc85c0 Minor typo fix and slight optimization (#7978)
Co-authored-by: Powei Feng <powei@google.com>
2024-07-19 03:07:54 +00:00
mdagoisandPowei Feng d957dd8082 Fix to barriers in VulkanBuffer (#7979)
Co-authored-by: Powei Feng <powei@google.com>
2024-07-19 10:45:10 +08:00
Ben Doherty 8eda62c957 Update imgui to v1.90.9 (#7977) 2024-07-18 16:38:25 -07:00
Sungun Park 90f079ac2c Add an extra check for renderStandaloneView (#7975)
renderStandaloneView must be called outside of beginFrame() /
endFrame(). This extra check ensures this behavior.
2024-07-18 06:31:02 -07:00
Balaji M 3728f06603 utility::loadCgltfBuffers is done once instead of doing for each primitive (#7969) 2024-07-12 23:22:25 +00:00
Adam Wychowaniec 39cfce641c Fix precondition check when creating static geometry. 2024-07-08 11:53:24 -07:00
Mathias Agopian 3fbf3f7111 fix a couple small bugs when mapping the circular buffer
- in case of failure we were munmap'ing the wrong size for the
  guard page (in practice this never happened)
- the post-condition check was incorrect; it checked for nullptr instead
  of MAP_FAILED. this also never happened in practice.

Also made a couple of small improvements:

- in case the special circular buffer mapping fails, log a message
  as warning instead of debug.

- immediately memset (i.e. populate) the pages for the circular buffer
  since they will all be accessed rather quickly.
2024-07-08 10:48:44 -07:00
Reinar ef9bbece2a Fix feature level detection for VulkanDriver 2024-07-02 15:09:25 -07:00
Mathias Agopian eb8fa9ecdf shader compilation could fail on ES 3.0 devices
the failure could happen if the shader didn't have any #extension
strings, which was likely to happen on release builds (e.g. on 
emulator).
2024-07-01 11:25:12 -07:00
Ben Doherty e1bfe7ca81 Metal: add a more detailed CHECK_POSTCONDITION when creating a texture (#7943) 2024-06-28 13:11:00 -07:00
Ben Doherty ca4c7ac739 Update cgltf to 1.14 (#7945) 2024-06-28 13:10:41 -07:00
Ben Doherty 9676bc94e8 Fix MetalBumpAllocator (#7951) 2024-06-28 13:09:01 -07:00
Mathias Agopian f7a5111106 switch to new morphing API
- remove deprecated morphing APIs
- repair gltfio, samples and tests

The new API doesn't allow a MorphTargetBuffer per RenderPrimitive,
instead the MorphTargetBuffer is specified per Renderable.

gltfio separates RenderPrimitives from Renderables, in particular all
RenderPrimitives are created before their Renderable; this was
problematic for this change because all primitives must share
a single MorphTargetBuffer living in the Renderable.

To fix this, we're no longer initializing the morphing paramters
at RenderPrimitive creation, instead we store a reference to the
BufferSlot in the Primtive structure, so that later, when the Renderable
is created we can finally retrieve the BufferSlot and initialize its
morphing paramters, which are not available. The "morphing parameters"
are now expanded to contain the MorphTargetBuffer as before (except now
it's always the same for all the primitives of a Rendrable), as well
as the offset within the buffer and the vertex count.
2024-06-28 12:14:57 -07:00
Sungun Park 56ac7ab902 IBL drag & drop support for desktop gltf_viewer (#7947)
Users now can drag and drop IBL files for desktop gltf_viewer.

It attempts to load gltf files first then tries to load IBL files.
2024-06-27 23:14:13 +00:00
Ben Doherty 44b4de904e Add saveRawVariants flag to matc (#7949) 2024-06-27 14:36:03 -07:00
Tibor Hencz a2fe9f745c Add ability to disable panning in the OrbitManipulator (#7928) 2024-06-27 00:05:14 +00:00
62 changed files with 1290 additions and 541 deletions
+4
View File
@@ -7,6 +7,10 @@ A new header is inserted each time a *tag* is created.
Instead, if you are authoring a PR for the main branch, add your release note to
[NEW_RELEASE_NOTES.md](./NEW_RELEASE_NOTES.md).
## v1.53.3
- Add drag and drop support for IBL files for desktop gltf_viewer.
## v1.53.2
@@ -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.
@@ -125,6 +125,11 @@ extern "C" JNIEXPORT void Java_com_google_android_filament_utils_Manipulator_nBu
builder->groundPlane(a, b, c, d);
}
extern "C" JNIEXPORT void Java_com_google_android_filament_utils_Manipulator_nBuilderPanning(JNIEnv*, jclass, jlong nativeBuilder, jboolean enabled) {
Builder* builder = (Builder*) nativeBuilder;
builder->panning(enabled);
}
extern "C" JNIEXPORT long Java_com_google_android_filament_utils_Manipulator_nBuilderBuild(JNIEnv*, jclass, jlong nativeBuilder, jint mode) {
Builder* builder = (Builder*) nativeBuilder;
return (jlong) builder->build((Mode) mode);
@@ -274,6 +274,17 @@ public class Manipulator {
return this;
}
/**
* Sets whether panning is enabled in the manipulator.
*
* @return this <code>Builder</code> object for chaining calls
*/
@NonNull
public Builder panning(Boolean enabled) {
nBuilderPanning(mNativeBuilder, enabled);
return this;
}
/**
* Creates and returns the <code>Manipulator</code> object.
*
@@ -483,6 +494,7 @@ public class Manipulator {
private static native void nBuilderFlightPanSpeed(long nativeBuilder, float x, float y);
private static native void nBuilderFlightMoveDamping(long nativeBuilder, float damping);
private static native void nBuilderGroundPlane(long nativeBuilder, float a, float b, float c, float d);
private static native void nBuilderPanning(long nativeBuilder, Boolean enabled);
private static native long nBuilderBuild(long nativeBuilder, int mode);
private static native void nDestroyManipulator(long nativeManip);
+9 -6
View File
@@ -32,10 +32,11 @@
# define HAS_MMAP 0
#endif
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
using namespace utils;
@@ -81,6 +82,9 @@ void* CircularBuffer::alloc(size_t size) noexcept {
// map the circular buffer once...
vaddr = mmap(reserve_vaddr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (vaddr != MAP_FAILED) {
// populate the address space with pages (because this is a circular buffer,
// all the pages will be allocated eventually, might as well do it now)
memset(vaddr, 0, size);
// and map the circular buffer again, behind the previous copy...
vaddr_shadow = mmap((char*)vaddr + size, size,
PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
@@ -101,7 +105,7 @@ void* CircularBuffer::alloc(size_t size) noexcept {
if (UTILS_UNLIKELY(mAshmemFd < 0)) {
// ashmem failed
if (vaddr_guard != MAP_FAILED) {
munmap(vaddr_guard, size);
munmap(vaddr_guard, BLOCK_SIZE);
}
if (vaddr_shadow != MAP_FAILED) {
@@ -119,12 +123,11 @@ void* CircularBuffer::alloc(size_t size) noexcept {
data = mmap(nullptr, size * 2 + BLOCK_SIZE,
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
FILAMENT_CHECK_POSTCONDITION(data) <<
FILAMENT_CHECK_POSTCONDITION(data != MAP_FAILED) <<
"couldn't allocate " << (size * 2 / 1024) <<
" KiB of virtual address space for the command buffer";
slog.d << "WARNING: Using soft CircularBuffer (" << (size * 2 / 1024) << " KiB)"
<< io::endl;
slog.w << "Using 'soft' CircularBuffer (" << (size * 2 / 1024) << " KiB)" << io::endl;
// guard page at the end
void* guard = (void*)(uintptr_t(data) + size * 2);
+6 -6
View File
@@ -556,8 +556,6 @@ MetalTexture::MetalTexture(MetalContext& context, SamplerType target, uint8_t le
descriptor.usage = getMetalTextureUsage(usage);
descriptor.storageMode = MTLStorageModePrivate;
texture = [context.device newTextureWithDescriptor:descriptor];
FILAMENT_CHECK_POSTCONDITION(texture != nil)
<< "Could not create Metal texture. Out of memory?";
break;
case SamplerType::SAMPLER_CUBEMAP:
case SamplerType::SAMPLER_CUBEMAP_ARRAY:
@@ -572,8 +570,6 @@ MetalTexture::MetalTexture(MetalContext& context, SamplerType target, uint8_t le
descriptor.usage = getMetalTextureUsage(usage);
descriptor.storageMode = MTLStorageModePrivate;
texture = [context.device newTextureWithDescriptor:descriptor];
FILAMENT_CHECK_POSTCONDITION(texture != nil)
<< "Could not create Metal texture. Out of memory?";
break;
case SamplerType::SAMPLER_3D:
descriptor = [MTLTextureDescriptor new];
@@ -586,8 +582,6 @@ MetalTexture::MetalTexture(MetalContext& context, SamplerType target, uint8_t le
descriptor.usage = getMetalTextureUsage(usage);
descriptor.storageMode = MTLStorageModePrivate;
texture = [context.device newTextureWithDescriptor:descriptor];
FILAMENT_CHECK_POSTCONDITION(texture != nil)
<< "Could not create Metal texture. Out of memory?";
break;
case SamplerType::SAMPLER_EXTERNAL:
// If we're using external textures (CVPixelBufferRefs), we don't need to make any
@@ -596,6 +590,12 @@ MetalTexture::MetalTexture(MetalContext& context, SamplerType target, uint8_t le
break;
}
FILAMENT_CHECK_POSTCONDITION(target == SamplerType::SAMPLER_EXTERNAL || texture != nil)
<< "Could not create Metal texture (SamplerType = " << int(target)
<< ", levels = " << int(levels) << ", MTLPixelFormat = " << int(devicePixelFormat)
<< ", width = " << width << ", height = " << height << ", depth = " << depth
<< "). Out of memory?";
// If swizzling is set, set up a swizzled texture view that we'll use when sampling this texture.
const bool isDefaultSwizzle =
r == TextureSwizzle::CHANNEL_0 &&
@@ -28,6 +28,7 @@ VulkanBuffer::VulkanBuffer(VmaAllocator allocator, VulkanStagePool& stagePool,
: mAllocator(allocator),
mStagePool(stagePool),
mUsage(usage),
mUpdatedOffset(0),
mUpdatedBytes(0) {
// for now make sure that only 1 bit is set in usage
// (because loadFromCpu() assumes that somewhat)
@@ -80,6 +81,7 @@ void VulkanBuffer::loadFromCpu(VkCommandBuffer cmdbuf, const void* cpuData, uint
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.buffer = mGpuBuffer,
.offset = mUpdatedOffset,
.size = mUpdatedBytes,
};
vkCmdPipelineBarrier(cmdbuf, srcStage, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1,
@@ -93,6 +95,7 @@ void VulkanBuffer::loadFromCpu(VkCommandBuffer cmdbuf, const void* cpuData, uint
};
vkCmdCopyBuffer(cmdbuf, stage->buffer, mGpuBuffer, 1, &region);
mUpdatedOffset = byteOffset;
mUpdatedBytes = numBytes;
// Firstly, ensure that the copy finishes before the next draw call.
@@ -42,6 +42,7 @@ private:
VmaAllocation mGpuMemory = VK_NULL_HANDLE;
VkBuffer mGpuBuffer = VK_NULL_HANDLE;
VkBufferUsageFlags mUsage = {};
uint32_t mUpdatedOffset = 0;
uint32_t mUpdatedBytes = 0;
};
+4 -4
View File
@@ -986,15 +986,15 @@ FeatureLevel VulkanDriver::getFeatureLevel() {
// If the max sampler counts do not meet FL2 standards, then this is an FL1 device.
const auto& fl2 = FEATURE_LEVEL_CAPS[+FeatureLevel::FEATURE_LEVEL_2];
if (fl2.MAX_VERTEX_SAMPLER_COUNT < limits.maxPerStageDescriptorSamplers ||
fl2.MAX_FRAGMENT_SAMPLER_COUNT < limits.maxPerStageDescriptorSamplers) {
if (limits.maxPerStageDescriptorSamplers < fl2.MAX_VERTEX_SAMPLER_COUNT ||
limits.maxPerStageDescriptorSamplers < fl2.MAX_FRAGMENT_SAMPLER_COUNT) {
return FeatureLevel::FEATURE_LEVEL_1;
}
// If the max sampler counts do not meet FL3 standards, then this is an FL2 device.
const auto& fl3 = FEATURE_LEVEL_CAPS[+FeatureLevel::FEATURE_LEVEL_3];
if (fl3.MAX_VERTEX_SAMPLER_COUNT < limits.maxPerStageDescriptorSamplers ||
fl3.MAX_FRAGMENT_SAMPLER_COUNT < limits.maxPerStageDescriptorSamplers) {
if (limits.maxPerStageDescriptorSamplers < fl3.MAX_VERTEX_SAMPLER_COUNT||
limits.maxPerStageDescriptorSamplers < fl3.MAX_FRAGMENT_SAMPLER_COUNT) {
return FeatureLevel::FEATURE_LEVEL_2;
}
@@ -33,11 +33,11 @@ VkPipelineLayout VulkanPipelineLayoutCache::getLayout(
// build the push constant layout key
uint32_t pushConstantRangeCount = program->getPushConstantRangeCount();
auto const& pushCostantRanges = program->getPushConstantRanges();
auto const& pushConstantRanges = program->getPushConstantRanges();
if (pushConstantRangeCount > 0) {
assert_invariant(pushConstantRangeCount <= Program::SHADER_TYPE_COUNT);
for (uint8_t i = 0; i < pushConstantRangeCount; ++i) {
auto const& range = pushCostantRanges[i];
auto const& range = pushConstantRanges[i];
auto& pushConstant = key.pushConstant[i];
if (range.stageFlags & VK_SHADER_STAGE_VERTEX_BIT) {
pushConstant.stage = static_cast<uint8_t>(ShaderStage::VERTEX);
@@ -52,9 +52,8 @@ VkPipelineLayout VulkanPipelineLayoutCache::getLayout(
}
}
auto iter = mPipelineLayouts.find(key);
if (iter != mPipelineLayouts.end()) {
PipelineLayoutCacheEntry& entry = mPipelineLayouts[key];
if (PipelineLayoutMap::iterator iter = mPipelineLayouts.find(key); iter != mPipelineLayouts.end()) {
PipelineLayoutCacheEntry& entry = iter.value();
entry.lastUsed = mTimestamp++;
return entry.handle;
}
@@ -65,7 +64,7 @@ VkPipelineLayout VulkanPipelineLayoutCache::getLayout(
.setLayoutCount = (uint32_t) descSetLayoutCount,
.pSetLayouts = key.descSetLayouts.data(),
.pushConstantRangeCount = pushConstantRangeCount,
.pPushConstantRanges = pushCostantRanges,
.pPushConstantRanges = pushConstantRanges,
};
VkPipelineLayout layout;
+5 -4
View File
@@ -64,14 +64,15 @@ First determine the Filament material and variant that causes the problem.
What we want is the "raw" GLSL version of the shader, before any optimizations / cross-compilation
happens.
The easiest way I've found to do this is by adding a `std::cout` statement inside `MaterialBuilder`,
right after `sg.createFragmentProgram` or `sg.createVertexProgram` is called. For example,
We can use the `--save-raw-variants` debug flag in matc to export each GLSL
shader to a file. For example:
```
if (v.variant.key == 8u) { std::cout << shader << std::endl; }
matc --save-raw-variants --optimize-size --variant-filter fog,ssr,vsm,stereo \
-a all -p all -o mymaterial.filamat mymaterial.mat
```
Then, I invoke `matc` and redirect stdout to a .frag or .vert file.
Files will be named like `mymaterial_0x05.frag` or `mymaterial_0x05.vert`.
Note that gltfio material "templates" first go through a build step. After building gltfio, the
gltfio Filament materials are output to:
+5 -2
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.
+41
View File
@@ -22,9 +22,11 @@
#include <filament/FilamentAPI.h>
#include <utils/compiler.h>
#include <utils/FixedCapacityVector.h>
#include <math/vec4.h>
#include <stddef.h>
#include <stdint.h>
namespace filament {
@@ -81,6 +83,37 @@ public:
UTILS_DEPRECATED uint64_t vsyncOffsetNanos = 0;
};
/**
* Timing information about a frame
* @see getFrameInfoHistory()
*/
struct FrameInfo {
using time_point_ns = int64_t;
using duration_ns = int64_t;
uint32_t frameId; //!< monotonically increasing frame identifier
duration_ns frameTime; //!< frame duration on the GPU in nanosecond [ns]
duration_ns denoisedFrameTime; //!< denoised frame duration on the GPU in [ns]
time_point_ns beginFrame; //!< Renderer::beginFrame() time since epoch [ns]
time_point_ns endFrame; //!< Renderer::endFrame() time since epoch [ns]
time_point_ns backendBeginFrame; //!< Backend thread time of frame start since epoch [ns]
time_point_ns backendEndFrame; //!< Backend thread time of frame end since epoch [ns]
};
/**
* Retrieve an historic of frame timing information. The maximum frame history size is
* given by getMaxFrameHistorySize().
* @param historySize requested history size. The returned vector could be smaller.
* @return A vector of FrameInfo.
*/
utils::FixedCapacityVector<Renderer::FrameInfo> getFrameInfoHistory(
size_t historySize = 1) const noexcept;
/**
* @return the maximum supported frame history size.
* @see getFrameInfoHistory()
*/
size_t getMaxFrameHistorySize() const noexcept;
/**
* Use FrameRateOptions to set the desired frame rate and control how quickly the system
* reacts to GPU load changes.
@@ -238,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.
*
+12
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
+3
View File
@@ -21,6 +21,9 @@
#include <fg/FrameGraphTexture.h>
#include <math/mat4.h>
#include <math/vec2.h>
#include <stdint.h>
namespace filament {
+132 -45
View File
@@ -16,30 +16,33 @@
#include "FrameInfo.h"
#include <filament/Renderer.h>
#include <backend/DriverEnums.h>
#include <utils/compiler.h>
#include <utils/debug.h>
#include <utils/FixedCapacityVector.h>
#include <utils/Log.h>
#include <utils/Systrace.h>
#include <utils/ostream.h>
#include <math/scalar.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <memory>
#include <ratio>
#include <cmath>
#include <stdint.h>
#include <stddef.h>
namespace filament {
using namespace utils;
using namespace backend;
// this is to avoid a call to memmove
template<class InputIterator, class OutputIterator>
static inline
void move_backward(InputIterator first, InputIterator last, OutputIterator result) {
while (first != last) {
*--result = *--last;
}
}
FrameInfoManager::FrameInfoManager(DriverApi& driver) noexcept {
for (auto& query : mQueries) {
query = driver.createTimerQuery();
query.handle = driver.createTimerQuery();
}
}
@@ -47,14 +50,36 @@ FrameInfoManager::~FrameInfoManager() noexcept = default;
void FrameInfoManager::terminate(DriverApi& driver) noexcept {
for (auto& query : mQueries) {
driver.destroyTimerQuery(query);
driver.destroyTimerQuery(query.handle);
}
}
void FrameInfoManager::beginFrame(DriverApi& driver,Config const& config, uint32_t) noexcept {
driver.beginTimerQuery(mQueries[mIndex]);
void FrameInfoManager::beginFrame(DriverApi& driver, Config const& config, uint32_t frameId) noexcept {
auto& history = mFrameTimeHistory;
// don't exceed the capacity, drop the oldest entry
if (UTILS_LIKELY(history.size() == history.capacity())) {
history.pop_back();
}
// create a new entry
auto& front = history.emplace_front(frameId);
// store the current time
front.beginFrame = std::chrono::steady_clock::now();
// references are not invalidated by CircularQueue<>, so we can associate a reference to
// the slot we created to the timer query used to find the frame time.
mQueries[mIndex].pInfo = std::addressof(front);
// issue the timer query
driver.beginTimerQuery(mQueries[mIndex].handle);
// issue the custom backend command to get the backend time
driver.queueCommand([&front](){
front.backendBeginFrame = std::chrono::steady_clock::now();
});
// now is a good time to check the oldest active query
uint64_t elapsed = 0;
TimerQueryResult const result = driver.getTimerQueryValue(mQueries[mLast], &elapsed);
TimerQueryResult const result = driver.getTimerQueryValue(mQueries[mLast].handle, &elapsed);
switch (result) {
case TimerQueryResult::NOT_READY:
// nothing to do
@@ -63,49 +88,111 @@ void FrameInfoManager::beginFrame(DriverApi& driver,Config const& config, uint32
mLast = (mLast + 1) % POOL_COUNT;
break;
case TimerQueryResult::AVAILABLE:
mLast = (mLast + 1) % POOL_COUNT;
// conversion to our duration happens here
mFrameTime = std::chrono::duration<uint64_t, std::nano>(elapsed);
pFront = mQueries[mLast].pInfo;
pFront->frameTime = std::chrono::duration<uint64_t, std::nano>(elapsed);
mLast = (mLast + 1) % POOL_COUNT;
denoiseFrameTime(config);
break;
}
update(config, mFrameTime);
// keep this just for debugging
if constexpr (false) {
using namespace utils;
auto h = getFrameInfoHistory(1);
if (!h.empty()) {
slog.d << frameId << ": "
<< h[0].frameId << " (" << frameId - h[0].frameId << ")"
<< ", Dm=" << h[0].endFrame - h[0].beginFrame
<< ", L =" << h[0].backendBeginFrame - h[0].beginFrame
<< ", Db=" << h[0].backendEndFrame - h[0].backendBeginFrame
<< ", T =" << h[0].frameTime
<< io::endl;
}
}
}
void FrameInfoManager::endFrame(DriverApi& driver) noexcept {
driver.endTimerQuery(mQueries[mIndex]);
auto& front = mFrameTimeHistory.front();
// close the timer query
driver.endTimerQuery(mQueries[mIndex].handle);
// queue custom backend command to query the current time
driver.queueCommand([&front](){
// backend frame end-time
front.backendEndFrame = std::chrono::steady_clock::now();
// signal that the data is available
front.ready.store(true, std::memory_order_release);
});
// and finally acquire the time on the main thread
front.endFrame = std::chrono::steady_clock::now();
mIndex = (mIndex + 1) % POOL_COUNT;
}
void FrameInfoManager::update(Config const& config,
FrameInfoManager::duration lastFrameTime) noexcept {
// keep an history of frame times
void FrameInfoManager::denoiseFrameTime(Config const& config) noexcept {
auto& history = mFrameTimeHistory;
assert_invariant(!history.empty());
// this is like doing { pop_back(); push_front(); }
filament::move_backward(history.begin(), history.end() - 1, history.end());
history[0].frameTime = lastFrameTime;
mFrameTimeHistorySize = std::min(++mFrameTimeHistorySize, uint32_t(MAX_FRAMETIME_HISTORY));
if (UTILS_UNLIKELY(mFrameTimeHistorySize < 3)) {
// not enough history to do anything useful
history[0].valid = false;
return;
// find the first slot that has a valid frame duration
size_t first = history.size();
for (size_t i = 0, c = history.size(); i < c; ++i) {
if (history[i].frameTime != duration(0)) {
assert_invariant(std::addressof(history[i]) == pFront);
first = i;
break;
}
}
assert_invariant(first != history.size());
// apply a median filter to get a good representation of the frame time of the last
// N frames.
std::array<duration, MAX_FRAMETIME_HISTORY> median; // NOLINT -- it's initialized below
size_t const size = std::min(mFrameTimeHistorySize,
std::min(config.historySize, (uint32_t)median.size()));
for (size_t i = 0; i < size; ++i) {
median[i] = history[i].frameTime;
}
std::sort(median.begin(), median.begin() + size);
duration const denoisedFrameTime = median[size / 2];
// we need at least 3 valid frame time to calculate the median
if (history.size() >= first + 3) {
// apply a median filter to get a good representation of the frame time of the last
// N frames.
std::array<duration, MAX_FRAMETIME_HISTORY> median; // NOLINT -- it's initialized below
size_t const size = std::min({
history.size() - first,
median.size(),
size_t(config.historySize) });
history[0].denoisedFrameTime = denoisedFrameTime;
history[0].valid = true;
for (size_t i = 0; i < size; ++i) {
median[i] = history[first + i].frameTime;
}
std::sort(median.begin(), median.begin() + size);
duration const denoisedFrameTime = median[size / 2];
history[first].denoisedFrameTime = denoisedFrameTime;
history[first].valid = true;
}
}
utils::FixedCapacityVector<Renderer::FrameInfo> FrameInfoManager::getFrameInfoHistory(
size_t historySize) const noexcept {
auto result = utils::FixedCapacityVector<Renderer::FrameInfo>::with_capacity(MAX_FRAMETIME_HISTORY);
auto const& history = mFrameTimeHistory;
size_t i = 0;
size_t const c = history.size();
for (; i < c; ++i) {
auto const& entry = history[i];
if (entry.ready.load(std::memory_order_acquire) && entry.valid) {
// once we found an entry ready,
// we know by construction that all following ones are too
break;
}
}
for (; i < c && historySize; ++i, --historySize) {
auto const& entry = history[i];
using namespace std::chrono;
result.push_back({
entry.frameId,
duration_cast<nanoseconds>(entry.frameTime).count(),
duration_cast<nanoseconds>(entry.denoisedFrameTime).count(),
duration_cast<nanoseconds>(entry.beginFrame.time_since_epoch()).count(),
duration_cast<nanoseconds>(entry.endFrame.time_since_epoch()).count(),
duration_cast<nanoseconds>(entry.backendBeginFrame.time_since_epoch()).count(),
duration_cast<nanoseconds>(entry.backendEndFrame.time_since_epoch()).count()
});
}
return result;
}
} // namespace filament
+124 -17
View File
@@ -17,30 +17,136 @@
#ifndef TNT_FILAMENT_FRAMEINFO_H
#define TNT_FILAMENT_FRAMEINFO_H
#include "backend/Handle.h"
#include <filament/Renderer.h>
#include <backend/Handle.h>
#include <private/backend/DriverApi.h>
#include <utils/compiler.h>
#include <utils/debug.h>
#include <utils/FixedCapacityVector.h>
#include <array>
#include <atomic>
#include <chrono>
#include <ratio>
#include <type_traits>
#include <stdint.h>
#include <stddef.h>
namespace filament {
class FEngine;
namespace details {
struct FrameInfo {
using duration = std::chrono::duration<float, std::milli>;
duration frameTime{}; // frame period
duration denoisedFrameTime{}; // frame period (median filter)
bool valid = false;
bool valid = false; // true if the data of the structure is valid
};
} // namespace details
struct FrameInfoImpl : public details::FrameInfo {
using clock = std::chrono::steady_clock;
using time_point = clock::time_point;
uint32_t const frameId;
time_point beginFrame; // main thread beginFrame time
time_point endFrame; // main thread endFrame time
time_point backendBeginFrame; // backend thread beginFrame time (makeCurrent time)
time_point backendEndFrame; // backend thread endFrame time (present time)
std::atomic_bool ready{}; // true once backend thread has populated its data
explicit FrameInfoImpl(uint32_t frameId) noexcept
: frameId(frameId) {
}
};
template<typename T, size_t CAPACITY>
class CircularQueue {
public:
using value_type = T;
using reference = value_type&;
using const_reference = value_type const&;
size_t capacity() const {
return CAPACITY;
}
size_t size() const {
return mSize;
}
bool empty() const noexcept {
return !size();
}
void pop_back() noexcept {
assert_invariant(!empty());
--mSize;
std::destroy_at(&mStorage[(mFront - mSize) % CAPACITY]);
}
void push_front(T const& v) noexcept {
assert_invariant(size() < CAPACITY);
mFront = advance(mFront);
new(&mStorage[mFront]) T(v);
++mSize;
}
void push_front(T&& v) noexcept {
assert_invariant(size() < CAPACITY);
mFront = advance(mFront);
new(&mStorage[mFront]) T(std::move(v));
++mSize;
}
template<typename ...Args>
T& emplace_front(Args&&... args) noexcept {
assert_invariant(size() < CAPACITY);
mFront = advance(mFront);
new(&mStorage[mFront]) T(std::forward<Args>(args)...);
++mSize;
return front();
}
T& operator[](size_t pos) noexcept {
assert_invariant(pos < size());
size_t const index = (mFront + CAPACITY - pos) % CAPACITY;
return *std::launder(reinterpret_cast<T*>(&mStorage[index]));
}
T const& operator[](size_t pos) const noexcept {
return const_cast<CircularQueue&>(*this)[pos];
}
T const& front() const noexcept {
assert_invariant(!empty());
return operator[](0);
}
T& front() noexcept {
assert_invariant(!empty());
return operator[](0);
}
private:
using Storage = std::aligned_storage_t<sizeof(T), alignof(T)>;
Storage mStorage[CAPACITY];
uint32_t mFront = 0; // always index 0
uint32_t mSize = 0;
[[nodiscard]] inline uint32_t advance(uint32_t v) noexcept {
return (v + 1) % CAPACITY;
}
};
class FrameInfoManager {
static constexpr size_t POOL_COUNT = 4;
static constexpr size_t MAX_FRAMETIME_HISTORY = 31u;
static constexpr size_t MAX_FRAMETIME_HISTORY = 16u;
public:
using duration = FrameInfo::duration;
using duration = FrameInfoImpl::duration;
using clock = FrameInfoImpl::clock;
struct Config {
uint32_t historySize;
@@ -57,23 +163,24 @@ public:
// call this immediately before "swap buffers"
void endFrame(backend::DriverApi& driver) noexcept;
FrameInfo const& getLastFrameInfo() const noexcept {
return mFrameTimeHistory[0];
details::FrameInfo const& getLastFrameInfo() const noexcept {
// if pFront is not set yet, return front() but in this case front().valid will be false
return pFront ? *pFront : mFrameTimeHistory.front();
}
duration getLastFrameTime() const noexcept {
return getLastFrameInfo().frameTime;
}
utils::FixedCapacityVector<Renderer::FrameInfo> getFrameInfoHistory(size_t historySize) const noexcept;
private:
void update(Config const& config, duration lastFrameTime) noexcept;
backend::Handle<backend::HwTimerQuery> mQueries[POOL_COUNT];
duration mFrameTime{};
uint32_t mIndex = 0;
uint32_t mLast = 0;
std::array<FrameInfo, MAX_FRAMETIME_HISTORY> mFrameTimeHistory;
uint32_t mFrameTimeHistorySize = 0;
void denoiseFrameTime(Config const& config) noexcept;
struct Query {
backend::Handle<backend::HwTimerQuery> handle{};
FrameInfoImpl* pInfo = nullptr;
};
std::array<Query, POOL_COUNT> mQueries;
uint32_t mIndex = 0; // index of current query
uint32_t mLast = 0; // index of oldest query still active
FrameInfoImpl* pFront = nullptr; // the most recent slot with a valid frame time
CircularQueue<FrameInfoImpl, MAX_FRAMETIME_HISTORY> mFrameTimeHistory;
};
+1
View File
@@ -26,6 +26,7 @@
#include <filament/Viewport.h>
#include <utils/BinaryTreeArray.h>
#include <utils/Log.h>
#include <utils/Systrace.h>
#include <utils/debug.h>
+4 -2
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) {
+25 -8
View File
@@ -38,6 +38,7 @@
#include <backend/PipelineState.h>
#include "private/backend/CircularBuffer.h"
#include "private/backend/CommandStream.h"
#include <utils/compiler.h>
#include <utils/debug.h>
@@ -153,7 +154,7 @@ RenderPass::RenderPass(FEngine& engine, RenderPassBuilder const& builder) noexce
sortCommands(builder.mArena);
if (engine.isAutomaticInstancingEnabled()) {
uint32_t stereoscopicEyeCount = 1;
int32_t stereoscopicEyeCount = 1;
if (builder.mFlags & IS_INSTANCED_STEREOSCOPIC) {
stereoscopicEyeCount *= engine.getConfig().stereoscopicEyeCount;
}
@@ -162,8 +163,7 @@ RenderPass::RenderPass(FEngine& engine, RenderPassBuilder const& builder) noexce
}
// this destructor is actually heavy because it inlines ~vector<>
RenderPass::~RenderPass() noexcept {
}
RenderPass::~RenderPass() noexcept = default;
void RenderPass::resize(Arena& arena, size_t count) noexcept {
if (mCommandBegin) {
@@ -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);
}
@@ -885,14 +885,31 @@ void RenderPass::Executor::execute(FEngine& engine,
// Maximum space occupied in the CircularBuffer by a single `Command`. This must be
// reevaluated when the inner loop below adds DriverApi commands or when we change the
// CommandStream protocol. Currently, the maximum is 240 bytes, and we use 256 to be on
// the safer side.
size_t const maxCommandSizeInBytes = 256;
// CommandStream protocol. Currently, the maximum is 320 bytes.
// The batch size is calculated by adding the size of all commands that can possibly be
// emitted per draw call:
constexpr size_t const maxCommandSizeInBytes =
sizeof(CustomCommand) +
sizeof(COMMAND_TYPE(scissor)) +
sizeof(COMMAND_TYPE(bindUniformBuffer)) +
sizeof(COMMAND_TYPE(bindSamplers)) +
sizeof(COMMAND_TYPE(bindBufferRange)) +
sizeof(COMMAND_TYPE(bindBufferRange)) +
sizeof(COMMAND_TYPE(bindSamplers)) +
sizeof(COMMAND_TYPE(bindSamplers)) +
sizeof(COMMAND_TYPE(bindUniformBuffer)) +
sizeof(COMMAND_TYPE(bindSamplers)) +
sizeof(COMMAND_TYPE(bindSamplers)) +
sizeof(COMMAND_TYPE(bindPipeline)) +
sizeof(COMMAND_TYPE(setPushConstant)) +
sizeof(COMMAND_TYPE(bindRenderPrimitive)) +
sizeof(COMMAND_TYPE(draw2));
// Number of Commands that can be issued and guaranteed to fit in the current
// CircularBuffer allocation. In practice, we'll have tons of headroom especially if
// skinning and morphing aren't used. With a 2 MiB buffer (the default) a batch is
// 8192 commands (i.e. draw calls).
// 6553 commands (i.e. draw calls).
size_t const batchCommandCount = capacity / maxCommandSizeInBytes;
while(first != last) {
Command const* const batchLast = std::min(first + batchCommandCount, last);
+1 -2
View File
@@ -417,8 +417,7 @@ private:
void instanceify(FEngine& engine, Arena& arena, int32_t eyeCount) noexcept;
// We choose the command count per job to minimize JobSystem overhead.
// On a Pixel 4, 2048 commands is about half a millisecond of processing.
static constexpr size_t JOBS_PARALLEL_FOR_COMMANDS_COUNT = 2048;
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;
+21
View File
@@ -14,11 +14,20 @@
* limitations under the License.
*/
#include <filament/Renderer.h>
#include "details/Renderer.h"
#include "details/Engine.h"
#include "details/View.h"
#include <utils/FixedCapacityVector.h>
#include <utility>
#include <stddef.h>
#include <stdint.h>
namespace filament {
using namespace math;
@@ -36,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);
}
@@ -93,4 +106,12 @@ void Renderer::setVsyncTime(uint64_t steadyClockTimeNano) noexcept {
downcast(this)->setVsyncTime(steadyClockTimeNano);
}
utils::FixedCapacityVector<Renderer::FrameInfo> Renderer::getFrameInfoHistory(size_t historySize) const noexcept {
return downcast(this)->getFrameInfoHistory(historySize);
}
size_t Renderer::getMaxFrameHistorySize() const noexcept {
return downcast(this)->getMaxFrameHistorySize();
}
} // namespace filament
+1 -1
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;
}
+131 -32
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
+46 -5
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
+6
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
+5 -4
View File
@@ -20,12 +20,13 @@
#include "details/Engine.h"
#include <math/fast.h>
#include <math/scalar.h>
#include <utils/debug.h>
#include <filament/LightManager.h>
#include <utils/debug.h>
#include <utils/Log.h>
#include <math/fast.h>
#include <math/scalar.h>
using namespace filament::math;
using namespace utils;
@@ -443,10 +443,10 @@ RenderableManager::Builder::Result RenderableManager::Builder::build(Engine& eng
<< ") instances when supplying transforms via an InstanceBuffer.";
if (mImpl->mGeometryType == GeometryType::STATIC) {
FILAMENT_CHECK_PRECONDITION(mImpl->mSkinningBoneCount > 0)
FILAMENT_CHECK_PRECONDITION(mImpl->mSkinningBoneCount == 0)
<< "Skinning can't be used with STATIC geometry";
FILAMENT_CHECK_PRECONDITION(mImpl->mMorphTargetCount > 0)
FILAMENT_CHECK_PRECONDITION(mImpl->mMorphTargetCount == 0)
<< "Morphing can't be used with STATIC geometry";
}
+8 -6
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
+11 -4
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;
+45 -3
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
@@ -468,6 +507,9 @@ void FRenderer::renderStandaloneView(FView const* view) {
FILAMENT_CHECK_PRECONDITION(view->getRenderTarget())
<< "View \"" << view->getName() << "\" must have a RenderTarget associated";
FILAMENT_CHECK_PRECONDITION(!mSwapChain)
<< "renderStandaloneView() must be called outside of beginFrame() / endFrame()";
if (UTILS_LIKELY(view->getScene())) {
mPreviousRenderTargets.clear();
mFrameId++;
@@ -747,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();
+16
View File
@@ -37,6 +37,7 @@
#include <utils/compiler.h>
#include <utils/Allocator.h>
#include <utils/FixedCapacityVector.h>
#include <math/vec4.h>
@@ -45,6 +46,7 @@
#include <algorithm>
#include <chrono>
#include <functional>
#include <memory>
#include <utility>
#include <stddef.h>
@@ -52,6 +54,8 @@
namespace filament {
class ResourceAllocator;
namespace backend {
class Driver;
} // namespace backend
@@ -87,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);
@@ -140,6 +147,14 @@ public:
return mClearOptions;
}
utils::FixedCapacityVector<Renderer::FrameInfo> getFrameInfoHistory(size_t historySize) const noexcept {
return mFrameInfoManager.getFrameInfoHistory(historySize);
}
size_t getMaxFrameHistorySize() const noexcept {
return MAX_FRAMETIME_HISTORY;
}
private:
friend class Renderer;
using Command = RenderPass::Command;
@@ -197,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)
+1 -1
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(),
+17 -7
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);
@@ -162,7 +163,7 @@ void FView::setDynamicLightingOptions(float zLightNear, float zLightFar) noexcep
}
float2 FView::updateScale(FEngine& engine,
FrameInfo const& info,
filament::details::FrameInfo const& info,
Renderer::FrameRateOptions const& frameRateOptions,
Renderer::DisplayInfo const& displayInfo) noexcept {
@@ -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();
}
}
+5 -5
View File
@@ -283,7 +283,7 @@ public:
}
math::float2 updateScale(FEngine& engine,
FrameInfo const& info,
details::FrameInfo const& info,
Renderer::FrameRateOptions const& frameRateOptions,
Renderer::DisplayInfo const& displayInfo) noexcept;
@@ -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(
+1
View File
@@ -24,6 +24,7 @@
#include "FrameGraphPass.h"
#include "FrameGraphRenderPass.h"
#include "FrameGraphTexture.h"
#include "ResourceAllocator.h"
#include "details/Engine.h"
+1
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
+67 -166
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;
}
@@ -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 {
@@ -109,6 +109,7 @@ public:
vec4 groundPlane;
RayCallback raycastCallback;
void* raycastUserdata;
bool panning = true;
};
struct Builder {
@@ -143,6 +144,8 @@ public:
Builder& groundPlane(FLOAT a, FLOAT b, FLOAT c, FLOAT d); //! Plane equation used as a raycast fallback
Builder& raycastCallback(RayCallback cb, void* userdata); //! Raycast function for accurate grab-and-pan
Builder& panning(bool enabled); //! Sets whether panning is enabled
/**
* Creates a new camera manipulator, either ORBIT, MAP, or FREE_FLIGHT.
*
+8
View File
@@ -144,6 +144,14 @@ Manipulator<FLOAT>::Builder& Manipulator<FLOAT>::Builder::raycastCallback(RayCal
return *this;
}
template <typename FLOAT> typename
Manipulator<FLOAT>::Builder& Manipulator<FLOAT>::Builder::panning(bool enabled) {
details.panning = enabled;
return *this;
}
template <typename FLOAT>
Manipulator<FLOAT>* Manipulator<FLOAT>::Builder::build(Mode mode) {
switch (mode) {
+1 -1
View File
@@ -69,7 +69,7 @@ public:
}
void grabBegin(int x, int y, bool strafe) override {
mGrabState = strafe ? PANNING : ORBITING;
mGrabState = strafe && Base::mProps.panning ? PANNING : ORBITING;
mGrabPivot = mPivot;
mGrabEye = Base::mEye;
mGrabTarget = Base::mTarget;
@@ -134,6 +134,7 @@ protected:
TargetApi mTargetApi = (TargetApi) 0;
Optimization mOptimization = Optimization::PERFORMANCE;
bool mPrintShaders = false;
bool mSaveRawVariants = false;
bool mGenerateDebugInfo = false;
bool mIncludeEssl1 = true;
utils::bitset32 mShaderModels;
@@ -586,6 +587,14 @@ public:
//! If true, will output the generated GLSL shader code to stdout.
MaterialBuilder& printShaders(bool printShaders) noexcept;
/**
* If true, this will write the raw generated GLSL for each variant to a text file in the
* current directory. The file will be named after the material name and the variant name. Its
* extension will be derived from the shader stage. For example, mymaterial_0x0e.frag,
* mymaterial_0x18.vert, etc.
*/
MaterialBuilder& saveRawVariants(bool saveVariants) noexcept;
//! If true, will include debugging information in generated SPIRV.
MaterialBuilder& generateDebugInfo(bool generateDebugInfo) noexcept;
+30 -1
View File
@@ -57,7 +57,7 @@
#include <atomic>
#include <utility>
#include <vector>
#include <fstream>
namespace filamat {
@@ -547,6 +547,11 @@ MaterialBuilder& MaterialBuilder::printShaders(bool printShaders) noexcept {
return *this;
}
MaterialBuilder& MaterialBuilder::saveRawVariants(bool saveRawVariants) noexcept {
mSaveRawVariants = saveRawVariants;
return *this;
}
MaterialBuilder& MaterialBuilder::generateDebugInfo(bool generateDebugInfo) noexcept {
mGenerateDebugInfo = generateDebugInfo;
return *this;
@@ -918,6 +923,30 @@ bool MaterialBuilder::generateShaders(JobSystem& jobSystem, const std::vector<Va
shaderModel, targetApi, targetLanguage, featureLevel, info);
}
// Write the variant to a file.
if (mSaveRawVariants) {
int variantKey = v.variant.key;
auto getExtension = [](backend::ShaderStage stage) {
switch (stage) {
case backend::ShaderStage::VERTEX:
return "vert";
case backend::ShaderStage::FRAGMENT:
return "frag";
case backend::ShaderStage::COMPUTE:
return "comp";
}
};
char filename[256];
snprintf(filename, sizeof(filename), "%s_0x%02x.%s", mMaterialName.c_str_safe(),
variantKey, getExtension(v.stage));
printf("Writing variant 0x%02x to %s\n", variantKey, filename);
std::ofstream file(filename);
if (file.is_open()) {
file << shader;
file.close();
}
}
std::string* pGlsl = nullptr;
if (targetApiNeedsGlsl) {
pGlsl = &shader;
@@ -104,6 +104,8 @@ public:
size_t getSkippedFrameCount() const { return mSkippedFrames; }
void loadIBL(std::string_view path);
FilamentApp(const FilamentApp& rhs) = delete;
FilamentApp(FilamentApp&& rhs) = delete;
FilamentApp& operator=(const FilamentApp& rhs) = delete;
+30 -25
View File
@@ -174,11 +174,6 @@ void FilamentApp::run(const Config& config, SetupCallback setupCallback,
loadDirt(config);
loadIBL(config);
if (mIBL != nullptr) {
mIBL->getSkybox()->setLayerMask(0x7, 0x4);
mScene->setSkybox(mIBL->getSkybox());
mScene->setIndirectLight(mIBL->getIndirectLight());
}
for (auto& view : window->mViews) {
if (view.get() != window->mUiView) {
@@ -519,31 +514,41 @@ const utils::Path& FilamentApp::getRootAssetsPath() {
return root;
}
void FilamentApp::loadIBL(const Config& config) {
if (!config.iblDirectory.empty()) {
Path iblPath(config.iblDirectory);
void FilamentApp::loadIBL(std::string_view path) {
Path iblPath(path);
if (!iblPath.exists()) {
std::cerr << "The specified IBL path does not exist: " << iblPath << std::endl;
return;
}
if (!iblPath.exists()) {
std::cerr << "The specified IBL path does not exist: " << iblPath << std::endl;
mIBL = std::make_unique<IBL>(*mEngine);
if (!iblPath.isDirectory()) {
if (!mIBL->loadFromEquirect(iblPath)) {
std::cerr << "Could not load the specified IBL: " << iblPath << std::endl;
mIBL.reset(nullptr);
return;
}
mIBL = std::make_unique<IBL>(*mEngine);
if (!iblPath.isDirectory()) {
if (!mIBL->loadFromEquirect(iblPath)) {
std::cerr << "Could not load the specified IBL: " << iblPath << std::endl;
mIBL.reset(nullptr);
return;
}
} else {
if (!mIBL->loadFromDirectory(iblPath)) {
std::cerr << "Could not load the specified IBL: " << iblPath << std::endl;
mIBL.reset(nullptr);
return;
}
} else {
if (!mIBL->loadFromDirectory(iblPath)) {
std::cerr << "Could not load the specified IBL: " << iblPath << std::endl;
mIBL.reset(nullptr);
return;
}
}
if (mIBL != nullptr) {
mIBL->getSkybox()->setLayerMask(0x7, 0x4);
mScene->setSkybox(mIBL->getSkybox());
mScene->setIndirectLight(mIBL->getIndirectLight());
}
}
void FilamentApp::loadIBL(const Config& config) {
if (config.iblDirectory.empty()) {
return;
}
loadIBL(config.iblDirectory);
}
void FilamentApp::loadDirt(const Config& config) {
+57 -8
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);
@@ -525,8 +525,9 @@ bool AssetLoaderExtended::createPrimitive(Input* input, Output* out,
slotCount++};
}
if (!utility::loadCgltfBuffers(gltf, mGltfPath.c_str(), mUriDataCache)) {
return false;
if (!mCgltfBuffersLoaded) {
mCgltfBuffersLoaded = utility::loadCgltfBuffers(gltf, mGltfPath.c_str(), mUriDataCache);
if (!mCgltfBuffersLoaded) return false;
}
utility::decodeDracoMeshes(gltf, prim, input->dracoCache);
@@ -83,6 +83,7 @@ private:
std::string mGltfPath;
MaterialProvider& mMaterials;
UriDataCacheHandle mUriDataCache;
bool mCgltfBuffersLoaded;
};
} // namespace filament::gltfio
+1
View File
@@ -22,6 +22,7 @@
#include "CubemapUtilsImpl.h"
#include <utils/Log.h>
#include <utils/JobSystem.h>
#include <math/mat4.h>
+103 -26
View File
@@ -21,7 +21,7 @@
#include <utils/architecture.h>
#include <utils/compiler.h>
#include <utils/Condition.h>
#include <utils/Log.h>
#include <utils/debug.h>
#include <utils/memalign.h>
#include <utils/Mutex.h>
#include <utils/Slice.h>
@@ -44,15 +44,23 @@
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;
using Condition = utils::Condition;
public:
class Job;
using ThreadId = uint8_t;
using JobFunc = void(*)(void*, JobSystem&, Job*);
static constexpr ThreadId invalidThreadId = 0xff;
class alignas(CACHELINE_SIZE) Job {
public:
Job() noexcept {} /* = default; */ /* clang bug */ // NOLINT(modernize-use-equals-default,cppcoreguidelines-pro-type-member-init)
@@ -75,12 +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
// 6 | 2 (padding)
mutable ThreadId id = invalidThreadId; // 1 | 1
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();
@@ -199,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 {
@@ -214,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.
@@ -244,7 +288,7 @@ public:
}
/*
* Add job to this thread's execution queue. It's reference will drop automatically.
* Add job to this thread's execution queue. Its reference will drop automatically.
* The current thread must be owned by JobSystem's thread pool. See adopt().
*
* The job can't be used after this call.
@@ -255,7 +299,19 @@ public:
run(p);
}
void signal() noexcept;
/*
* Add job to this thread's execution queue. Its reference will drop automatically.
* The current thread must be owned by JobSystem's thread pool. See adopt().
* id must be the current thread id obtained with JobSystem::getThreadId(Job*). This
* API is more efficient than the methods above.
*
* The job can't be used after this call.
*/
void run(Job*& job, ThreadId id) noexcept;
void run(Job*&& job, ThreadId id) noexcept { // allows run(createJob(...));
Job* p = job;
run(p, id);
}
/*
* Add job to this thread's execution queue and keep a reference to it.
@@ -312,12 +368,29 @@ public:
size_t getThreadCount() const { return mThreadCount; }
// returns the current ThreadId, which can be used with run(). This method can only be
// called from a job's function.
static ThreadId getThreadId(Job const* job) noexcept {
assert_invariant(job->id != invalidThreadId);
return job->id;
}
private:
// this is just to avoid using std::default_random_engine, since we're in a public header.
class default_random_engine {
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) {
}
@@ -362,13 +435,15 @@ private:
Job* pop(WorkQueue& workQueue) noexcept;
Job* steal(WorkQueue& workQueue) noexcept;
void wait(std::unique_lock<Mutex>& lock, Job* job = nullptr) noexcept;
void wake(size_t hint) 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;
// these have thread contention, keep them together
utils::Mutex mWaiterLock;
utils::Condition mWaiterCondition;
Mutex mWaiterLock;
Condition mWaiterCondition;
std::atomic<int32_t> mActiveJobs = { 0 };
utils::Arena<utils::ThreadSafeObjectPoolAllocator<Job>, LockingPolicy::NoLock> mJobPool;
@@ -391,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;
};
@@ -409,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,
@@ -425,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)...));
}
@@ -459,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;
@@ -468,7 +545,7 @@ right_side:
// start the left side before attempting the right side, so we parallelize in case
// of job creation failure -- rare, but still.
js.run(l);
js.run(l, JobSystem::getThreadId(parent));
// don't spawn a job for the right side, just reuse us -- spawning jobs is more
// costly than we'd like.
@@ -500,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
@@ -512,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<>
@@ -524,7 +601,7 @@ JobSystem::Job* parallel_for(JobSystem& js, JobSystem::Job* parent,
}
template <size_t COUNT, size_t MAX_SPLITS = 12>
template<size_t COUNT, size_t MAX_SPLITS = 12>
class CountSplitter {
public:
bool split(size_t splits, size_t count) const noexcept {
+14 -6
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>);
}
}
+120 -115
View File
@@ -23,20 +23,18 @@
// when SYSTRACE_TAG_JOBSYSTEM is used, enables even heavier systraces
#define HEAVY_SYSTRACE 0
// enable for catching hangs waiting on a job to finish
static constexpr bool DEBUG_FINISH_HANGS = false;
#include <utils/JobSystem.h>
#include <utils/compiler.h>
#include <utils/debug.h>
#include <utils/Log.h>
#include <utils/memalign.h>
#include <utils/ostream.h>
#include <utils/Panic.h>
#include <utils/Systrace.h>
#include <algorithm>
#include <atomic>
#include <cerrno>
#include <chrono>
#include <cmath>
#include <condition_variable>
@@ -248,68 +246,62 @@ 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 {
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);
}
}
void JobSystem::wakeAll() noexcept {
inline void JobSystem::wait(std::unique_lock<Mutex>& lock) noexcept {
HEAVY_SYSTRACE_CALL();
std::lock_guard<Mutex> const lock(mWaiterLock);
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)
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();
}
void JobSystem::wake(size_t hint) noexcept {
void JobSystem::wakeOne() noexcept {
// wakeOne() is called when a new job is added to a queue
HEAVY_SYSTRACE_CALL();
std::lock_guard<Mutex> const lock(mWaiterLock);
// this empty critical section is needed -- it guarantees that notify_all() happens
// after the condition's variables are set.
if (hint == 1) {
mWaiterCondition.notify_one();
} else {
mWaiterCondition.notify_all();
}
mWaiterLock.lock();
// this empty critical section is needed -- it guarantees that notify_one() happens
// 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();
}
inline JobSystem::ThreadState& JobSystem::getState() noexcept {
std::lock_guard<utils::Mutex> const lock(mThreadMapLock);
std::lock_guard<Mutex> const lock(mThreadMapLock);
auto iter = mThreadMap.find(std::this_thread::get_id());
FILAMENT_CHECK_PRECONDITION(iter != mThreadMap.end()) << "This thread has not been adopted.";
return *iter->second;
@@ -324,58 +316,36 @@ 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) {
wake(oldActiveJobs + 1); // 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) {
int32_t const oldActiveJobs = mActiveJobs.fetch_add(1, std::memory_order_relaxed);
if (oldActiveJobs >= 0) {
// And if there are some active jobs, then we need to wake someone up. We know it
// can't be us, because we failed taking a job and we know another thread can't
// have added one in our queue.
wake(oldActiveJobs + 1); // wake-up a thread if needed...
}
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(index <= MAX_JOB_COUNT);
assert_invariant(index <= MAX_JOB_COUNT);
Job* const job = !index ? nullptr : &mJobStorageBase[index - 1];
// If we failed taking a job, we need to correct mActiveJobs.
if (!job) {
int32_t const oldActiveJobs = mActiveJobs.fetch_add(1, std::memory_order_relaxed);
if (oldActiveJobs >= 0) {
// And if there are some active jobs, then we need to wake someone up. We know it
// can't be us, because we failed taking a job and we know another thread can't
// have added one in our queue.
wake(oldActiveJobs + 1); // wake-up a thread if needed...
}
if (UTILS_LIKELY(job)) {
mActiveJobs.fetch_sub(1, std::memory_order_relaxed);
}
return job;
}
@@ -407,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,
@@ -420,17 +390,23 @@ 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);
job->function(job->storage, *this, job);
job->id = invalidThreadId;
}
finish(job);
}
@@ -442,9 +418,10 @@ void JobSystem::loop(ThreadState* state) noexcept {
setThreadPriority(Priority::DISPLAY);
// record our work queue
mThreadMapLock.lock();
std::unique_lock<Mutex> lock(mThreadMapLock);
bool const inserted = mThreadMap.emplace(std::this_thread::get_id(), state).second;
mThreadMapLock.unlock();
lock.unlock();
FILAMENT_CHECK_PRECONDITION(inserted) << "This thread is already in a loop.";
// run our main loop...
@@ -469,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;
@@ -484,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();
}
}
@@ -502,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);
@@ -532,10 +516,6 @@ void JobSystem::release(JobSystem::Job*& job) noexcept {
job = nullptr;
}
void JobSystem::signal() noexcept {
wakeAll();
}
void JobSystem::run(Job*& job) noexcept {
HEAVY_SYSTRACE_CALL();
@@ -547,6 +527,18 @@ void JobSystem::run(Job*& job) noexcept {
job = nullptr;
}
void JobSystem::run(Job*& job, uint8_t id) noexcept {
HEAVY_SYSTRACE_CALL();
ThreadState& state = mThreadStates[id];
assert_invariant(&state == &getState());
put(state.workQueue, job);
// after run() returns, the job is virtually invalid (it'll die on its own)
job = nullptr;
}
JobSystem::Job* JobSystem::runAndRetain(Job* job) noexcept {
JobSystem::Job* retained = retain(job);
run(job);
@@ -561,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;
@@ -577,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;
@@ -591,6 +595,7 @@ void JobSystem::waitAndRelease(Job*& job) noexcept {
}
void JobSystem::runAndWait(JobSystem::Job*& job) noexcept {
SYSTRACE_CALL();
runAndRetain(job);
waitAndRelease(job);
}
@@ -598,7 +603,7 @@ void JobSystem::runAndWait(JobSystem::Job*& job) noexcept {
void JobSystem::adopt() {
const auto tid = std::this_thread::get_id();
std::unique_lock<utils::Mutex> lock(mThreadMapLock);
std::unique_lock<Mutex> lock(mThreadMapLock);
auto iter = mThreadMap.find(tid);
ThreadState* const state = iter == mThreadMap.end() ? nullptr : iter->second;
lock.unlock();
@@ -631,7 +636,7 @@ void JobSystem::adopt() {
void JobSystem::emancipate() {
const auto tid = std::this_thread::get_id();
std::lock_guard<utils::Mutex> const lock(mThreadMapLock);
std::unique_lock<Mutex> const lock(mThreadMapLock);
auto iter = mThreadMap.find(tid);
ThreadState* const state = iter == mThreadMap.end() ? nullptr : iter->second;
FILAMENT_CHECK_PRECONDITION(state) << "this thread is not an adopted thread";
+48 -14
View File
@@ -58,6 +58,7 @@
#include <cgltf.h>
#include <algorithm>
#include <array>
#include <fstream>
#include <iostream>
@@ -508,30 +509,55 @@ static void onClick(App& app, View* view, ImVec2 pos) {
});
}
static utils::Path getPathForAsset(std::string_view string) {
static utils::Path getPathForIBLAsset(std::string_view string) {
auto isIBL = [] (utils::Path file) -> bool {
return file.getExtension() == "ktx" || file.getExtension() == "hdr";
};
utils::Path filename{ string };
if (!filename.exists()) {
std::cerr << "file " << filename << " not found!" << std::endl;
return {};
}
if (filename.isDirectory()) {
auto files = filename.listContents();
for (const auto& file: files) {
if (file.getExtension() == "gltf" || file.getExtension() == "glb") {
filename = file;
break;
}
}
if (filename.isDirectory()) {
std::cerr << "no glTF file found in " << filename << std::endl;
std::vector<Path> files = filename.listContents();
if (std::none_of(files.cbegin(), files.cend(), isIBL)) {
return {};
}
} else if (!isIBL(filename)) {
return {};
}
return filename;
}
static utils::Path getPathForGLTFAsset(std::string_view string) {
auto isGLTF = [] (utils::Path file) -> bool {
return file.getExtension() == "gltf" || file.getExtension() == "glb";
};
static bool checkAsset(const utils::Path& filename) {
utils::Path filename{ string };
if (!filename.exists()) {
std::cerr << "file " << filename << " not found!" << std::endl;
return {};
}
if (filename.isDirectory()) {
std::vector<Path> files = filename.listContents();
auto it = std::find_if(files.cbegin(), files.cend(), isGLTF);
if (it == files.end()) {
return {};
}
filename = *it;
} else if (!isGLTF(filename)) {
return {};
}
return filename;
}
static bool checkGLTFAsset(const utils::Path& filename) {
// Peek at the file size to allow pre-allocation.
long const contentSize = static_cast<long>(getFileSize(filename.c_str()));
if (contentSize <= 0) {
@@ -571,8 +597,9 @@ int main(int argc, char** argv) {
utils::Path filename;
int const num_args = argc - optionIndex;
if (num_args >= 1) {
filename = getPathForAsset(argv[optionIndex]);
filename = getPathForGLTFAsset(argv[optionIndex]);
if (filename.isEmpty()) {
std::cerr << "no glTF file found in " << filename << std::endl;
return 1;
}
}
@@ -1125,9 +1152,9 @@ int main(int argc, char** argv) {
filamentApp.resize(resize);
filamentApp.setDropHandler([&](std::string_view path) {
utils::Path const filename = getPathForAsset(path);
utils::Path filename = getPathForGLTFAsset(path);
if (!filename.isEmpty()) {
if (checkAsset(filename)) {
if (checkGLTFAsset(filename)) {
app.resourceLoader->asyncCancelLoad();
app.resourceLoader->evictResourceData();
app.viewer->removeAsset();
@@ -1136,6 +1163,13 @@ int main(int argc, char** argv) {
loadResources(filename);
app.viewer->setAsset(app.asset, app.instance);
}
return;
}
filename = getPathForIBLAsset(path);
if (!filename.isEmpty()) {
FilamentApp::get().loadIBL(path);
return;
}
});
+6 -1
View File
@@ -103,6 +103,8 @@ static void usage(char* name) {
" Skip validation of number of sampler used\n\n"
" --print, -t\n"
" Print generated shaders for debugging\n\n"
" --save-raw-variants, -R\n"
" Write the raw generated GLSL for each variant to a text file in the current directory.\n\n"
);
const std::string from("MATC");
for (size_t pos = usage.find(from); pos != std::string::npos; pos = usage.find(from, pos)) {
@@ -179,7 +181,7 @@ static void parseDefine(std::string defineString, Config::StringReplacementMap&
}
bool CommandlineConfig::parse() {
static constexpr const char* OPTSTR = "hLxo:f:dm:a:l:p:D:T:P:OSEr:vV:gtwF1";
static constexpr const char* OPTSTR = "hLxo:f:dm:a:l:p:D:T:P:OSEr:vV:gtwF1R";
static const struct option OPTIONS[] = {
{ "help", no_argument, nullptr, 'h' },
{ "license", no_argument, nullptr, 'L' },
@@ -204,6 +206,7 @@ bool CommandlineConfig::parse() {
{ "version", no_argument, nullptr, 'v' },
{ "raw", no_argument, nullptr, 'w' },
{ "no-sampler-validation", no_argument, nullptr, 'F' },
{ "save-raw-variants", no_argument, nullptr, 'R' },
{ nullptr, 0, nullptr, 0 } // termination of the option list
};
@@ -326,6 +329,8 @@ bool CommandlineConfig::parse() {
break;
case 'F':
mNoSamplerValidation = true;
case 'R':
mSaveRawVariants = true;
break;
}
}
+5
View File
@@ -111,6 +111,10 @@ public:
return mPrintShaders;
}
bool saveRawVariants() const noexcept {
return mSaveRawVariants;
}
bool rawShaderMode() const noexcept {
return mRawShaderMode;
}
@@ -149,6 +153,7 @@ protected:
bool mPrintShaders = false;
bool mRawShaderMode = false;
bool mNoSamplerValidation = false;
bool mSaveRawVariants = false;
Optimization mOptimizationLevel = Optimization::PERFORMANCE;
Metadata mReflectionTarget = Metadata::NONE;
Platform mPlatform = Platform::ALL;
+2
View File
@@ -24,6 +24,7 @@
#include <filamat/Enums.h>
#include <utils/Log.h>
#include <utils/JobSystem.h>
#include "DirIncluder.h"
@@ -408,6 +409,7 @@ bool MaterialCompiler::run(const Config& config) {
.targetApi(config.getTargetApi())
.optimization(config.getOptimizationLevel())
.printShaders(config.printShaders())
.saveRawVariants(config.saveRawVariants())
.generateDebugInfo(config.isDebug())
.variantFilter(config.getVariantFilter() | builder.getVariantFilter());
+3 -1
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`.