From eda2b7955d64314aa526a550d1cf4df64c2fc9b2 Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Fri, 10 Jun 2022 09:27:07 -0700 Subject: [PATCH] Basic automatic instancing. --- filament/src/RenderPass.cpp | 144 +++++++++++++++++++++++++++++++++--- filament/src/RenderPass.h | 9 ++- 2 files changed, 142 insertions(+), 11 deletions(-) diff --git a/filament/src/RenderPass.cpp b/filament/src/RenderPass.cpp index c577bde701..fa35b9205a 100644 --- a/filament/src/RenderPass.cpp +++ b/filament/src/RenderPass.cpp @@ -21,8 +21,6 @@ #include "details/Material.h" #include "details/MaterialInstance.h" -// NOTE: We only need Renderer.h here because the definition of some FRenderer methods are here -#include "details/Renderer.h" #include "details/View.h" #include @@ -186,8 +184,128 @@ void RenderPass::sortCommands() noexcept { }); resize(uint32_t(last - mCommandBegin)); + + instanceify(); } +void RenderPass::instanceify() noexcept { + SYSTRACE_NAME("instanceify"); + + // instanceify works by scanning the **sorted** command stream, looking for repeat draw + // commands. When one is found, it is replaced by an instanced command. + // A "repeat" draw is one that ends-up using the same draw parameters and state. + // Currently, this relies somewhat on luck that "repeat draws" are found consecutively, + // we could improve this by including some or all of these "repeat" parameters in the + // sorting key (e.g. raster state, primitive handle, etc...), the key could even use a small + // hash of those parameters. + + UTILS_UNUSED_IN_RELEASE uint32_t drawCallsSavedCount = 0; + + Command* curr = mCommandBegin; + Command* const last = mCommandEnd; + + Command* firstSentinel = nullptr; + PerRenderableData const* uboData = nullptr; + PerRenderableData* stagingBuffer = nullptr; + uint32_t stagingBufferSize = 0; + uint32_t instancedPrimitiveOffset = 0; + + // TODO: for the case of instancing we could actually use 128 instead of 64 instances + constexpr size_t maxInstanceCount = sizeof(PerRenderableUib) / sizeof(PerRenderableData); + static_assert(maxInstanceCount == 64); // just to make sure we don't change by accident + + while (curr != last) { + + // we can't have nice things! No more than maxInstanceCount due to UBO size limits + Command const* const e = std::find_if_not(curr, std::min(last, last + maxInstanceCount), + [lhs = *curr](Command const& rhs) { + // primitives must be identical to be instanced. Currently, instancing doesn't support + // skinning/morphing. + return lhs.primitive.mi == rhs.primitive.mi && + lhs.primitive.primitiveHandle == rhs.primitive.primitiveHandle && + lhs.primitive.rasterState == rhs.primitive.rasterState && + lhs.primitive.skinningHandle == rhs.primitive.skinningHandle && + lhs.primitive.skinningOffset == rhs.primitive.skinningOffset && + lhs.primitive.morphWeightBuffer == rhs.primitive.morphWeightBuffer && + lhs.primitive.morphTargetBuffer == rhs.primitive.morphTargetBuffer; + }); + + uint32_t instanceCount = e - curr; + assert_invariant(instanceCount > 0); + + if (UTILS_UNLIKELY(instanceCount > 1)) { + drawCallsSavedCount += instanceCount - 1; + + // allocate our staging buffer only if needed + if (UTILS_UNLIKELY(!stagingBuffer)) { + // TODO: use stream inline buffer for small sizes + // TODO: use a pool for larger heap buffers + // buffer large enough for all instances data + stagingBufferSize = sizeof(PerRenderableData) * (last - curr); + stagingBuffer = (PerRenderableData*)::malloc(stagingBufferSize); + uboData = mRenderableSoa->data(); + } + + // copy the ubo data to a staging buffer + assert_invariant(instancedPrimitiveOffset + instanceCount + <= stagingBufferSize / sizeof(PerRenderableData)); + for (uint32_t i = 0; i < instanceCount; i++) { + stagingBuffer[instancedPrimitiveOffset + i] = uboData[curr[i].primitive.index]; + } + + // make the first command instanced + curr[0].primitive.instanceCount = instanceCount; + curr[0].primitive.index = instancedPrimitiveOffset; + instancedPrimitiveOffset += instanceCount; + + // cancel commands that are now instances + firstSentinel = !firstSentinel ? curr : firstSentinel; + for (uint32_t i = 1; i < instanceCount; i++) { + curr[i].key = uint64_t(Pass::SENTINEL); + } + } + + curr = const_cast(e); + } + + if (UTILS_UNLIKELY(firstSentinel)) { +#ifndef NDEBUG + // TODO: remove this eventually + slog.d << "auto-instancing, saving " << drawCallsSavedCount << " draw calls, out of " + << mCommandEnd - mCommandBegin << io::endl; +#endif + + // we have instanced primitives + DriverApi& driver = mEngine.getDriverApi(); + + // TODO: maybe use a pool? so we can reuse the buffer. + // create a ubo to hold the instanced primitive data + mInstancedUboHandle = driver.createBufferObject( + sizeof(PerRenderableData) * instancedPrimitiveOffset + sizeof(PerRenderableUib), + BufferObjectBinding::UNIFORM, backend::BufferUsage::STATIC); + + // copy our instanced ubo data + driver.updateBufferObjectUnsynchronized(mInstancedUboHandle, { + stagingBuffer, sizeof(PerRenderableData) * instancedPrimitiveOffset, + +[](void* buffer, size_t size, void* user) { + ::free(buffer); + } + }, 0); + + stagingBuffer = nullptr; + + // remove all the canceled commands + auto lastCommand = std::remove_if(firstSentinel, mCommandEnd, [](auto const& command) { + return command.key == uint64_t(Pass::SENTINEL); + }); + + resize(uint32_t(lastCommand - mCommandBegin)); + } + + assert_invariant(stagingBuffer == nullptr); +} + + /* static */ UTILS_ALWAYS_INLINE // this function exists only to make the code more readable. we want it inlined. inline // and we don't need it in the compilation unit @@ -601,7 +719,6 @@ void RenderPass::Executor::recordDriverCommands(FEngine& engine, backend::Driver Handle uboHandle = mUboHandle; FMaterialInstance const* UTILS_RESTRICT mi = nullptr; FMaterial const* UTILS_RESTRICT ma = nullptr; - uint32_t uboDataIndex = std::numeric_limits::max(); auto customCommands = mCustomCommands.data(); first--; @@ -639,12 +756,12 @@ void RenderPass::Executor::recordDriverCommands(FEngine& engine, backend::Driver pipeline.program = ma->getProgram(info.materialVariant); - if (uboDataIndex != info.index) { - uboDataIndex = info.index; - driver.bindUniformBufferRange(BindingPoints::PER_RENDERABLE, - uboHandle, uboDataIndex * sizeof(PerRenderableData), - sizeof(PerRenderableUib)); - } + // bind per-renderable uniform block. there is no need to attempt to skip this command + // because the backends already do this. + driver.bindUniformBufferRange(BindingPoints::PER_RENDERABLE, + (info.instanceCount > 1) ? mInstancedUboHandle : uboHandle, + info.index * sizeof(PerRenderableData), + sizeof(PerRenderableUib)); if (UTILS_UNLIKELY(info.skinningHandle)) { // note: we can't bind less than sizeof(PerRenderableBoneUib) due to glsl limitations @@ -669,13 +786,20 @@ void RenderPass::Executor::recordDriverCommands(FEngine& engine, backend::Driver driver.draw(pipeline, info.primitiveHandle, info.instanceCount); } } + + if (mInstancedUboHandle) { + driver.destroyBufferObject(mInstancedUboHandle); + } + } // ------------------------------------------------------------------------------------------------ RenderPass::Executor::Executor(RenderPass const* pass, Command const* b, Command const* e) noexcept : mEngine(pass->mEngine), mBegin(b), mEnd(e), - mCustomCommands(pass->mCustomCommands), mUboHandle(pass->mUboHandle), + mCustomCommands(pass->mCustomCommands), + mUboHandle(pass->mUboHandle), + mInstancedUboHandle(pass->mInstancedUboHandle), mPolygonOffset(pass->mPolygonOffset), mScissor(pass->mScissor), mPolygonOffsetOverride(pass->mPolygonOffsetOverride), diff --git a/filament/src/RenderPass.h b/filament/src/RenderPass.h index 03d14d4707..e03b66fdc5 100644 --- a/filament/src/RenderPass.h +++ b/filament/src/RenderPass.h @@ -54,6 +54,10 @@ public: * t = two-pass transparency ordering * 0 = reserved, must be zero * + * + * TODO: we need to add a "primitive id" in the low-bits of material-id, so that + * auto-instancing can work better + * * DEPTH command * | 6 | 2| 2|1| 3 | 2| 6 | 10 | 32 | * +------+--+--+-+---+--+------+----------+--------------------------------+ @@ -305,7 +309,7 @@ public: // the current camera, geometry and flags set. This can be called multiple times if needed. void appendCommands(CommandTypeFlags commandTypeFlags) noexcept; - // sorts commands, then trims sentinels + // sorts and instancify commands then trims sentinels void sortCommands() noexcept; // Helper to execute all the commands generated by this RenderPass @@ -329,6 +333,7 @@ public: Command const* mEnd; const CustomCommandVector mCustomCommands; const backend::Handle mUboHandle; + const backend::Handle mInstancedUboHandle; const backend::PolygonOffset mPolygonOffset; const backend::Viewport mScissor; const bool mPolygonOffsetOverride : 1; @@ -367,6 +372,7 @@ private: Command* append(size_t count) noexcept; void resize(size_t count) noexcept; + void instanceify() noexcept; // on 64-bits systems, we process batches of 256 (64 bytes) cache-lines, or 512 (32 bytes) commands // on 32-bits systems, we process batches of 512 (32 bytes) cache-lines, or 512 (32 bytes) commands @@ -415,6 +421,7 @@ private: // the UBO containing the data for the renderables backend::Handle mUboHandle; + backend::Handle mInstancedUboHandle; // info about the camera math::float3 mCameraPosition{};