Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69fc1f302c | ||
|
|
e7c96cd124 | ||
|
|
730bc99025 | ||
|
|
7441e878bb | ||
|
|
ef4c4a76fc | ||
|
|
be4f287b07 | ||
|
|
d3a35de386 | ||
|
|
9057c47a56 | ||
|
|
4a5081388d | ||
|
|
a60fe41681 | ||
|
|
a8ace2891d | ||
|
|
669ffc85c0 | ||
|
|
d957dd8082 | ||
|
|
8eda62c957 | ||
|
|
90f079ac2c | ||
|
|
3728f06603 | ||
|
|
39cfce641c | ||
|
|
3fbf3f7111 | ||
|
|
ef9bbece2a | ||
|
|
eb8fa9ecdf | ||
|
|
e1bfe7ca81 | ||
|
|
ca4c7ac739 | ||
|
|
9676bc94e8 | ||
|
|
f7a5111106 | ||
|
|
56ac7ab902 | ||
|
|
44b4de904e | ||
|
|
a2fe9f745c |
@@ -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);
|
||||
|
||||
+12
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, ®ion);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -40,6 +40,7 @@ class CallbackHandler;
|
||||
|
||||
class Camera;
|
||||
class ColorGrading;
|
||||
class Engine;
|
||||
class MaterialInstance;
|
||||
class RenderTarget;
|
||||
class Scene;
|
||||
@@ -878,6 +879,17 @@ public:
|
||||
*/
|
||||
utils::Entity getFogEntity() const noexcept;
|
||||
|
||||
|
||||
/**
|
||||
* When certain temporal features are used (e.g.: TAA or Screen-space reflections), the view
|
||||
* keeps a history of previous frame renders associated with the Renderer the view was last
|
||||
* used with. When switching Renderer, it may be necessary to clear that history by calling
|
||||
* this method. Similarly, if the whole content of the screen change, like when a cut-scene
|
||||
* starts, clearing the history might be needed to avoid artifacts due to the previous frame
|
||||
* being very different.
|
||||
*/
|
||||
void clearFrameHistory(Engine& engine) noexcept;
|
||||
|
||||
/**
|
||||
* List of available ambient occlusion techniques
|
||||
* @deprecated use AmbientOcclusionOptions::enabled instead
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
#include <fg/FrameGraphTexture.h>
|
||||
|
||||
#include <math/mat4.h>
|
||||
#include <math/vec2.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace filament {
|
||||
|
||||
|
||||
+132
-45
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <filament/Viewport.h>
|
||||
|
||||
#include <utils/BinaryTreeArray.h>
|
||||
#include <utils/Log.h>
|
||||
#include <utils/Systrace.h>
|
||||
#include <utils/debug.h>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -172,7 +172,7 @@ FrameGraphId<FrameGraphTexture> RendererUtils::colorPass(
|
||||
|
||||
data.color = builder.write(data.color, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
|
||||
data.depth = builder.write(data.depth, FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
|
||||
if (engine.getConfig().stereoscopicType == StereoscopicType::MULTIVIEW) {
|
||||
if (view.hasStereo() && engine.getConfig().stereoscopicType == StereoscopicType::MULTIVIEW) {
|
||||
layerCount = engine.getConfig().stereoscopicEyeCount;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,15 +27,18 @@
|
||||
|
||||
#include "private/backend/DriverApi.h"
|
||||
|
||||
#include <utils/algorithm.h>
|
||||
#include <utils/bitset.h>
|
||||
#include <utils/compiler.h>
|
||||
#include <utils/debug.h>
|
||||
#include <utils/FixedCapacityVector.h>
|
||||
#include <utils/Log.h>
|
||||
#include <utils/ostream.h>
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include <stddef.h>
|
||||
@@ -89,8 +92,15 @@ void ResourceAllocator::AssociativeContainer<K, V, H>::emplace(ARGS&& ... args)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
ResourceAllocatorInterface::~ResourceAllocatorInterface() = default;
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
ResourceAllocatorDisposerInterface::~ResourceAllocatorDisposerInterface() = default;
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
size_t ResourceAllocator::TextureKey::getSize() const noexcept {
|
||||
size_t const pixelCount = width * height * depth;
|
||||
size_t size = pixelCount * FTexture::getFormatSize(format);
|
||||
@@ -110,16 +120,22 @@ size_t ResourceAllocator::TextureKey::getSize() const noexcept {
|
||||
|
||||
ResourceAllocator::ResourceAllocator(Engine::Config const& config, DriverApi& driverApi) noexcept
|
||||
: mCacheMaxAge(config.resourceAllocatorCacheMaxAge),
|
||||
mBackend(driverApi) {
|
||||
mBackend(driverApi),
|
||||
mDisposer(std::make_shared<ResourceAllocatorDisposer>(driverApi)) {
|
||||
}
|
||||
|
||||
ResourceAllocator::ResourceAllocator(std::shared_ptr<ResourceAllocatorDisposer> disposer,
|
||||
Engine::Config const& config, DriverApi& driverApi) noexcept
|
||||
: mCacheMaxAge(config.resourceAllocatorCacheMaxAge),
|
||||
mBackend(driverApi),
|
||||
mDisposer(std::move(disposer)) {
|
||||
}
|
||||
|
||||
ResourceAllocator::~ResourceAllocator() noexcept {
|
||||
assert_invariant(!mTextureCache.size());
|
||||
assert_invariant(!mInUseTextures.size());
|
||||
assert_invariant(mTextureCache.empty());
|
||||
}
|
||||
|
||||
void ResourceAllocator::terminate() noexcept {
|
||||
assert_invariant(!mInUseTextures.size());
|
||||
auto& textureCache = mTextureCache;
|
||||
for (auto it = textureCache.begin(); it != textureCache.end();) {
|
||||
mBackend.destroyTexture(it->second.handle);
|
||||
@@ -174,7 +190,7 @@ backend::TextureHandle ResourceAllocator::createTexture(const char* name,
|
||||
swizzle[0], swizzle[1], swizzle[2], swizzle[3]);
|
||||
}
|
||||
}
|
||||
mInUseTextures.emplace(handle, key);
|
||||
mDisposer->checkout(handle, key);
|
||||
} else {
|
||||
if (swizzle == defaultSwizzle) {
|
||||
handle = mBackend.createTexture(
|
||||
@@ -190,58 +206,99 @@ backend::TextureHandle ResourceAllocator::createTexture(const char* name,
|
||||
|
||||
void ResourceAllocator::destroyTexture(TextureHandle h) noexcept {
|
||||
if constexpr (mEnabled) {
|
||||
// find the texture in the in-use list (it must be there!)
|
||||
auto it = mInUseTextures.find(h);
|
||||
assert_invariant(it != mInUseTextures.end());
|
||||
|
||||
// move it to the cache
|
||||
const TextureKey key = it->second;
|
||||
uint32_t const size = key.getSize();
|
||||
|
||||
mTextureCache.emplace(key, TextureCachePayload{ h, mAge, size });
|
||||
mCacheSize += size;
|
||||
|
||||
// remove it from the in-use list
|
||||
mInUseTextures.erase(it);
|
||||
auto const key = mDisposer->checkin(h);
|
||||
if (UTILS_LIKELY(key.has_value())) {
|
||||
uint32_t const size = key.value().getSize();
|
||||
mTextureCache.emplace(key.value(), TextureCachePayload{ h, mAge, size });
|
||||
mCacheSize += size;
|
||||
mCacheSizeHiWaterMark = std::max(mCacheSizeHiWaterMark, mCacheSize);
|
||||
}
|
||||
} else {
|
||||
mBackend.destroyTexture(h);
|
||||
}
|
||||
}
|
||||
|
||||
void ResourceAllocator::gc() noexcept {
|
||||
// this is called regularly -- usually once per frame of each Renderer
|
||||
ResourceAllocatorDisposerInterface& ResourceAllocator::getDisposer() noexcept {
|
||||
return *mDisposer;
|
||||
}
|
||||
|
||||
// increase our age
|
||||
const size_t age = mAge++;
|
||||
void ResourceAllocator::gc(bool skippedFrame) noexcept {
|
||||
// this is called regularly -- usually once per frame
|
||||
|
||||
// increase our age at each (non-skipped) frame
|
||||
const size_t age = mAge;
|
||||
if (!skippedFrame) {
|
||||
mAge++;
|
||||
}
|
||||
|
||||
// Purging strategy:
|
||||
// - remove entries that are older than a certain age
|
||||
// - remove only one entry per gc(),
|
||||
// - remove all entries older than MAX_AGE_SKIPPED_FRAME when skipping a frame
|
||||
// - remove entries older than mCacheMaxAgeSoft
|
||||
// - remove only MAX_EVICTION_COUNT entry per gc(),
|
||||
// - look for the number of unique resource ages present in the cache (this basically gives
|
||||
// us how many buckets of resources we have corresponding to previous frames.
|
||||
// - remove all resources that have an age older than the MAX_UNIQUE_AGE_COUNT'th bucket
|
||||
|
||||
auto& textureCache = mTextureCache;
|
||||
|
||||
// when skipping a frame, the maximum age to keep in the cache
|
||||
constexpr size_t MAX_AGE_SKIPPED_FRAME = 1;
|
||||
|
||||
// maximum entry count to evict per GC, under the mCacheMaxAgeSoft limit
|
||||
constexpr size_t MAX_EVICTION_COUNT = 1;
|
||||
|
||||
// maximum number of unique ages in the cache
|
||||
constexpr size_t MAX_UNIQUE_AGE_COUNT = 3;
|
||||
|
||||
utils::bitset32 ages;
|
||||
uint32_t evictedCount = 0;
|
||||
for (auto it = textureCache.begin(); it != textureCache.end();) {
|
||||
const size_t ageDiff = age - it->second.age;
|
||||
if (ageDiff >= mCacheMaxAge) {
|
||||
size_t const ageDiff = age - it->second.age;
|
||||
if ((ageDiff >= MAX_AGE_SKIPPED_FRAME && skippedFrame) ||
|
||||
(ageDiff >= mCacheMaxAge && evictedCount < MAX_EVICTION_COUNT)) {
|
||||
evictedCount++;
|
||||
purge(it);
|
||||
// only purge a single entry per gc
|
||||
break;
|
||||
} else {
|
||||
// build the set of ages present in the cache after eviction
|
||||
ages.set(std::min(size_t(31), ageDiff));
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// if we have MAX_UNIQUE_AGE_COUNT ages or more, we evict all the resources that
|
||||
// are older than the MAX_UNIQUE_AGE_COUNT'th age.
|
||||
if (!skippedFrame && ages.count() >= MAX_UNIQUE_AGE_COUNT) {
|
||||
uint32_t bits = ages.getValue();
|
||||
// remove from the set the ages we keep
|
||||
for (size_t i = 0; i < MAX_UNIQUE_AGE_COUNT - 1; i++) {
|
||||
bits &= ~(1 << utils::ctz(bits));
|
||||
}
|
||||
size_t const maxAge = utils::ctz(bits);
|
||||
for (auto it = textureCache.begin(); it != textureCache.end();) {
|
||||
const size_t ageDiff = age - it->second.age;
|
||||
if (ageDiff >= maxAge) {
|
||||
purge(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UTILS_NOINLINE
|
||||
void ResourceAllocator::dump(bool brief) const noexcept {
|
||||
slog.d << "# entries=" << mTextureCache.size() << ", sz=" << mCacheSize / float(1u << 20u)
|
||||
<< " MiB" << io::endl;
|
||||
constexpr float MiB = 1.0f / float(1u << 20u);
|
||||
slog.d << "# entries=" << mTextureCache.size()
|
||||
<< ", sz=" << (float)mCacheSize * MiB << " MiB"
|
||||
<< ", max=" << (float)mCacheSizeHiWaterMark * MiB << " MiB"
|
||||
<< io::endl;
|
||||
if (!brief) {
|
||||
for (auto const& it : mTextureCache) {
|
||||
auto w = it.first.width;
|
||||
auto h = it.first.height;
|
||||
auto f = FTexture::getFormatSize(it.first.format);
|
||||
slog.d << it.first.name << ": w=" << w << ", h=" << h << ", f=" << f << ", sz="
|
||||
<< it.second.size / float(1u << 20u) << io::endl;
|
||||
<< (float)it.second.size * MiB << io::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,4 +311,46 @@ void ResourceAllocator::purge(
|
||||
mTextureCache.erase(pos);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
ResourceAllocatorDisposer::ResourceAllocatorDisposer(DriverApi& driverApi) noexcept
|
||||
: mBackend(driverApi) {
|
||||
}
|
||||
|
||||
ResourceAllocatorDisposer::~ResourceAllocatorDisposer() noexcept {
|
||||
assert_invariant(mInUseTextures.empty());
|
||||
}
|
||||
|
||||
void ResourceAllocatorDisposer::terminate() noexcept {
|
||||
assert_invariant(mInUseTextures.empty());
|
||||
}
|
||||
|
||||
void ResourceAllocatorDisposer::destroy(backend::TextureHandle handle) noexcept {
|
||||
if (handle) {
|
||||
auto r = checkin(handle);
|
||||
if (r.has_value()) {
|
||||
mBackend.destroyTexture(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ResourceAllocatorDisposer::checkout(backend::TextureHandle handle,
|
||||
ResourceAllocator::TextureKey key) {
|
||||
mInUseTextures.emplace(handle, key);
|
||||
}
|
||||
|
||||
std::optional<ResourceAllocator::TextureKey> ResourceAllocatorDisposer::checkin(
|
||||
backend::TextureHandle handle) {
|
||||
// find the texture in the in-use list (it must be there!)
|
||||
auto it = mInUseTextures.find(handle);
|
||||
assert_invariant(it != mInUseTextures.end());
|
||||
if (it == mInUseTextures.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
TextureKey const key = it->second;
|
||||
// remove it from the in-use list
|
||||
mInUseTextures.erase(it);
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace filament
|
||||
|
||||
@@ -28,16 +28,29 @@
|
||||
#include <utils/Hash.h>
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <cstddef>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace filament {
|
||||
|
||||
class ResourceAllocatorDisposer;
|
||||
|
||||
// The only reason we use an interface here is for unit-tests, so we can mock this allocator.
|
||||
// This is not too time-critical, so that's okay.
|
||||
|
||||
class ResourceAllocatorDisposerInterface {
|
||||
public:
|
||||
virtual void destroy(backend::TextureHandle handle) noexcept = 0;
|
||||
protected:
|
||||
virtual ~ResourceAllocatorDisposerInterface();
|
||||
};
|
||||
|
||||
class ResourceAllocatorInterface {
|
||||
public:
|
||||
virtual backend::RenderTargetHandle createRenderTarget(const char* name,
|
||||
@@ -60,15 +73,20 @@ public:
|
||||
|
||||
virtual void destroyTexture(backend::TextureHandle h) noexcept = 0;
|
||||
|
||||
virtual ResourceAllocatorDisposerInterface& getDisposer() noexcept = 0;
|
||||
|
||||
protected:
|
||||
virtual ~ResourceAllocatorInterface();
|
||||
};
|
||||
|
||||
|
||||
class ResourceAllocator final : public ResourceAllocatorInterface {
|
||||
public:
|
||||
explicit ResourceAllocator(std::shared_ptr<ResourceAllocatorDisposer> disposer,
|
||||
Engine::Config const& config, backend::DriverApi& driverApi) noexcept;
|
||||
|
||||
explicit ResourceAllocator(
|
||||
Engine::Config const& config, backend::DriverApi& driverApi) noexcept;
|
||||
|
||||
~ResourceAllocator() noexcept override;
|
||||
|
||||
void terminate() noexcept;
|
||||
@@ -93,7 +111,9 @@ public:
|
||||
|
||||
void destroyTexture(backend::TextureHandle h) noexcept override;
|
||||
|
||||
void gc() noexcept;
|
||||
ResourceAllocatorDisposerInterface& getDisposer() noexcept override;
|
||||
|
||||
void gc(bool skippedFrame = false) noexcept;
|
||||
|
||||
private:
|
||||
size_t const mCacheMaxAge;
|
||||
@@ -181,6 +201,7 @@ private:
|
||||
using value_type = typename Container::value_type::second_type;
|
||||
|
||||
size_t size() const { return mContainer.size(); }
|
||||
bool empty() const { return size() == 0; }
|
||||
iterator begin() { return mContainer.begin(); }
|
||||
const_iterator begin() const { return mContainer.begin(); }
|
||||
iterator end() { return mContainer.end(); }
|
||||
@@ -193,16 +214,36 @@ private:
|
||||
};
|
||||
|
||||
using CacheContainer = AssociativeContainer<TextureKey, TextureCachePayload>;
|
||||
using InUseContainer = AssociativeContainer<backend::TextureHandle, TextureKey>;
|
||||
|
||||
void purge(ResourceAllocator::CacheContainer::iterator const& pos);
|
||||
|
||||
backend::DriverApi& mBackend;
|
||||
std::shared_ptr<ResourceAllocatorDisposer> mDisposer;
|
||||
CacheContainer mTextureCache;
|
||||
InUseContainer mInUseTextures;
|
||||
size_t mAge = 0;
|
||||
uint32_t mCacheSize = 0;
|
||||
uint32_t mCacheSizeHiWaterMark = 0;
|
||||
static constexpr bool mEnabled = true;
|
||||
|
||||
friend class ResourceAllocatorDisposer;
|
||||
};
|
||||
|
||||
class ResourceAllocatorDisposer final : public ResourceAllocatorDisposerInterface {
|
||||
using TextureKey = ResourceAllocator::TextureKey;
|
||||
public:
|
||||
explicit ResourceAllocatorDisposer(backend::DriverApi& driverApi) noexcept;
|
||||
~ResourceAllocatorDisposer() noexcept override;
|
||||
void terminate() noexcept;
|
||||
void destroy(backend::TextureHandle handle) noexcept override;
|
||||
|
||||
private:
|
||||
friend class ResourceAllocator;
|
||||
void checkout(backend::TextureHandle handle, TextureKey key);
|
||||
std::optional<TextureKey> checkin(backend::TextureHandle handle);
|
||||
|
||||
using InUseContainer = ResourceAllocator::AssociativeContainer<backend::TextureHandle, TextureKey>;
|
||||
backend::DriverApi& mBackend;
|
||||
InUseContainer mInUseTextures;
|
||||
};
|
||||
|
||||
} // namespace filament
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
|
||||
#include "details/View.h"
|
||||
#include "filament/View.h"
|
||||
|
||||
|
||||
namespace filament {
|
||||
|
||||
@@ -312,4 +314,8 @@ utils::Entity View::getFogEntity() const noexcept {
|
||||
return downcast(this)->getFogEntity();
|
||||
}
|
||||
|
||||
void View::clearFrameHistory(Engine& engine) noexcept {
|
||||
downcast(this)->clearFrameHistory(downcast(engine));
|
||||
}
|
||||
|
||||
} // namespace filament
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -163,8 +163,7 @@ FEngine* FEngine::getEngine(void* token) {
|
||||
FILAMENT_CHECK_PRECONDITION(ThreadUtils::isThisThread(instance->mMainThreadId))
|
||||
<< "Engine::createAsync() and Engine::getEngine() must be called on the same thread.";
|
||||
|
||||
// we use mResourceAllocator as a proxy for "am I already initialized"
|
||||
if (!instance->mResourceAllocator) {
|
||||
if (!instance->mInitialized) {
|
||||
if (UTILS_UNLIKELY(!instance->mDriver)) {
|
||||
// something went horribly wrong during driver initialization
|
||||
instance->mDriverThread.join();
|
||||
@@ -262,7 +261,7 @@ void FEngine::init() {
|
||||
slog.i << "FEngine feature level: " << int(mActiveFeatureLevel) << io::endl;
|
||||
|
||||
|
||||
mResourceAllocator = new ResourceAllocator(mConfig, driverApi);
|
||||
mResourceAllocatorDisposer = std::make_shared<ResourceAllocatorDisposer>(driverApi);
|
||||
|
||||
mFullScreenTriangleVb = downcast(VertexBuffer::Builder()
|
||||
.vertexCount(3)
|
||||
@@ -425,11 +424,13 @@ void FEngine::init() {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mInitialized = true;
|
||||
}
|
||||
|
||||
FEngine::~FEngine() noexcept {
|
||||
SYSTRACE_CALL();
|
||||
delete mResourceAllocator;
|
||||
assert_invariant(!mResourceAllocatorDisposer);
|
||||
delete mDriver;
|
||||
if (mOwnPlatform) {
|
||||
PlatformFactory::destroy(&mPlatform);
|
||||
@@ -440,7 +441,7 @@ void FEngine::shutdown() {
|
||||
SYSTRACE_CALL();
|
||||
|
||||
// by construction this should never be nullptr
|
||||
assert_invariant(mResourceAllocator);
|
||||
assert_invariant(mResourceAllocatorDisposer);
|
||||
|
||||
FILAMENT_CHECK_PRECONDITION(ThreadUtils::isThisThread(mMainThreadId))
|
||||
<< "Engine::shutdown() called from the wrong thread!";
|
||||
@@ -460,7 +461,8 @@ void FEngine::shutdown() {
|
||||
*/
|
||||
|
||||
mPostProcessManager.terminate(driver); // free-up post-process manager resources
|
||||
mResourceAllocator->terminate();
|
||||
mResourceAllocatorDisposer->terminate();
|
||||
mResourceAllocatorDisposer.reset();
|
||||
mDFG.terminate(*this); // free-up the DFG
|
||||
mRenderableManager.terminate(); // free-up all renderables
|
||||
mLightManager.terminate(); // free-up all lights
|
||||
|
||||
@@ -86,6 +86,7 @@ namespace filament {
|
||||
|
||||
class Renderer;
|
||||
class MaterialParser;
|
||||
class ResourceAllocatorDisposer;
|
||||
|
||||
namespace backend {
|
||||
class Driver;
|
||||
@@ -254,9 +255,13 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
ResourceAllocator& getResourceAllocator() noexcept {
|
||||
assert_invariant(mResourceAllocator);
|
||||
return *mResourceAllocator;
|
||||
ResourceAllocatorDisposer& getResourceAllocatorDisposer() noexcept {
|
||||
assert_invariant(mResourceAllocatorDisposer);
|
||||
return *mResourceAllocatorDisposer;
|
||||
}
|
||||
|
||||
std::shared_ptr<ResourceAllocatorDisposer> const& getSharedResourceAllocatorDisposer() noexcept {
|
||||
return mResourceAllocatorDisposer;
|
||||
}
|
||||
|
||||
void* streamAlloc(size_t size, size_t alignment) noexcept;
|
||||
@@ -490,7 +495,7 @@ private:
|
||||
FTransformManager mTransformManager;
|
||||
FLightManager mLightManager;
|
||||
FCameraManager mCameraManager;
|
||||
ResourceAllocator* mResourceAllocator = nullptr;
|
||||
std::shared_ptr<ResourceAllocatorDisposer> mResourceAllocatorDisposer;
|
||||
HwVertexBufferInfoFactory mHwVertexBufferInfoFactory;
|
||||
|
||||
ResourceList<FBufferObject> mBufferObjects{ "BufferObject" };
|
||||
@@ -562,6 +567,8 @@ private:
|
||||
|
||||
std::thread::id mMainThreadId{};
|
||||
|
||||
bool mInitialized = false;
|
||||
|
||||
// Creation parameters
|
||||
Config mConfig;
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include <stddef.h>
|
||||
@@ -88,7 +89,11 @@ FRenderer::FRenderer(FEngine& engine) :
|
||||
mHdrQualityMedium(TextureFormat::R11F_G11F_B10F),
|
||||
mHdrQualityHigh(TextureFormat::RGB16F),
|
||||
mIsRGB8Supported(false),
|
||||
mUserEpoch(engine.getEngineEpoch())
|
||||
mUserEpoch(engine.getEngineEpoch()),
|
||||
mResourceAllocator(std::make_unique<ResourceAllocator>(
|
||||
engine.getSharedResourceAllocatorDisposer(),
|
||||
engine.getConfig(),
|
||||
engine.getDriverApi()))
|
||||
{
|
||||
FDebugRegistry& debugRegistry = engine.getDebugRegistry();
|
||||
debugRegistry.registerProperty("d.renderer.doFrameCapture",
|
||||
@@ -176,6 +181,7 @@ void FRenderer::terminate(FEngine& engine) {
|
||||
}
|
||||
mFrameInfoManager.terminate(driver);
|
||||
mFrameSkipper.terminate(driver);
|
||||
mResourceAllocator->terminate();
|
||||
}
|
||||
|
||||
void FRenderer::resetUserTime() {
|
||||
@@ -236,6 +242,39 @@ void FRenderer::setVsyncTime(uint64_t steadyClockTimeNano) noexcept {
|
||||
mVsyncSteadyClockTimeNano = steadyClockTimeNano;
|
||||
}
|
||||
|
||||
void FRenderer::skipFrame(uint64_t vsyncSteadyClockTimeNano) {
|
||||
SYSTRACE_CALL();
|
||||
|
||||
FILAMENT_CHECK_PRECONDITION(!mSwapChain) <<
|
||||
"skipFrame() can't be called between beginFrame() and endFrame()";
|
||||
|
||||
if (!vsyncSteadyClockTimeNano) {
|
||||
vsyncSteadyClockTimeNano = mVsyncSteadyClockTimeNano;
|
||||
mVsyncSteadyClockTimeNano = 0;
|
||||
}
|
||||
|
||||
FEngine& engine = mEngine;
|
||||
FEngine::DriverApi& driver = engine.getDriverApi();
|
||||
|
||||
// Gives the backend a chance to execute periodic tasks. This must be called before
|
||||
// the frame skipper.
|
||||
driver.tick();
|
||||
|
||||
// do this before engine.flush()
|
||||
mResourceAllocator->gc(true);
|
||||
|
||||
// Run the component managers' GC in parallel
|
||||
// WARNING: while doing this we can't access any component manager
|
||||
auto& js = engine.getJobSystem();
|
||||
|
||||
auto *job = js.runAndRetain(jobs::createJob(js, nullptr, &FEngine::gc, &engine)); // gc all managers
|
||||
|
||||
engine.flush(); // flush command stream
|
||||
|
||||
// make sure we're done with the gcs
|
||||
js.waitAndRelease(job);
|
||||
}
|
||||
|
||||
bool FRenderer::beginFrame(FSwapChain* swapChain, uint64_t vsyncSteadyClockTimeNano) {
|
||||
assert_invariant(swapChain);
|
||||
|
||||
@@ -380,7 +419,7 @@ void FRenderer::endFrame() {
|
||||
}
|
||||
|
||||
// do this before engine.flush()
|
||||
engine.getResourceAllocator().gc();
|
||||
mResourceAllocator->gc();
|
||||
|
||||
// Run the component managers' GC in parallel
|
||||
// WARNING: while doing this we can't access any component manager
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -248,7 +248,7 @@ void FScene::prepare(utils::JobSystem& js,
|
||||
|
||||
auto* renderableJob = jobs::parallel_for(js, rootJob,
|
||||
renderableInstances.data(), renderableInstances.size(),
|
||||
std::cref(renderableWork), jobs::CountSplitter<128, 5>());
|
||||
std::cref(renderableWork), jobs::CountSplitter<64>());
|
||||
|
||||
auto* lightJob = jobs::parallel_for(js, rootJob,
|
||||
lightInstances.data(), lightInstances.size(),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "details/View.h"
|
||||
|
||||
#include "Culler.h"
|
||||
#include "FrameHistory.h"
|
||||
#include "Froxelizer.h"
|
||||
#include "RenderPrimitive.h"
|
||||
#include "ResourceAllocator.h"
|
||||
@@ -118,7 +119,7 @@ void FView::terminate(FEngine& engine) {
|
||||
DriverApi& driver = engine.getDriverApi();
|
||||
driver.destroyBufferObject(mLightUbh);
|
||||
driver.destroyBufferObject(mRenderableUbh);
|
||||
drainFrameHistory(engine);
|
||||
clearFrameHistory(engine);
|
||||
|
||||
ShadowMapManager::terminate(engine, mShadowMapManager);
|
||||
mPerViewUniforms.terminate(driver);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "FrameGraphPass.h"
|
||||
#include "FrameGraphRenderPass.h"
|
||||
#include "FrameGraphTexture.h"
|
||||
#include "ResourceAllocator.h"
|
||||
|
||||
#include "details/Engine.h"
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ if (ANDROID)
|
||||
target_link_libraries(test_compiler PRIVATE utils)
|
||||
target_link_libraries(test_compiler PRIVATE EGL)
|
||||
target_link_libraries(test_compiler PRIVATE GLESv3)
|
||||
target_link_libraries(test_compiler PRIVATE bluevk)
|
||||
endif()
|
||||
|
||||
add_executable(test_material_parser
|
||||
|
||||
+67
-166
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include "CubemapUtilsImpl.h"
|
||||
|
||||
#include <utils/Log.h>
|
||||
#include <utils/JobSystem.h>
|
||||
|
||||
#include <math/mat4.h>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -627,6 +627,7 @@ class_<Renderer>("Renderer")
|
||||
.function("getClearOptions", &Renderer::getClearOptions)
|
||||
.function("setPresentationTime", &Renderer::setPresentationTime)
|
||||
.function("setVsyncTime", &Renderer::setVsyncTime)
|
||||
.function("skipFrame", &Renderer::skipFrame)
|
||||
.function("beginFrame", EMBIND_LAMBDA(bool, (Renderer* self, SwapChain* swapChain), {
|
||||
return self->beginFrame(swapChain);
|
||||
}), allow_raw_pointers())
|
||||
@@ -683,7 +684,8 @@ class_<View>("View")
|
||||
.function("isStencilBufferEnabled", &View::isStencilBufferEnabled)
|
||||
.function("setMaterialGlobal", &View::setMaterialGlobal)
|
||||
.function("getMaterialGlobal", &View::getMaterialGlobal)
|
||||
.function("getFogEntity", &View::getFogEntity);
|
||||
.function("getFogEntity", &View::getFogEntity)
|
||||
.function("clearFrameHistory", &View::clearFrameHistory);
|
||||
|
||||
/// Scene ::core class:: Flat container of renderables and lights.
|
||||
/// See also the [Engine] methods `createScene` and `destroyScene`.
|
||||
|
||||
Reference in New Issue
Block a user