diff --git a/filament/backend/include/backend/Platform.h b/filament/backend/include/backend/Platform.h index 1777860f86c..a84a8ba01fe 100644 --- a/filament/backend/include/backend/Platform.h +++ b/filament/backend/include/backend/Platform.h @@ -46,6 +46,13 @@ class UTILS_PUBLIC Platform { * Driver clamps to valid values. */ size_t handleArenaSize = 0; + + /* + * this number of most-recently destroyed textures will be tracked for use-after-free. + * Throws an exception when a texture is freed but still bound to a SamplerGroup and used in + * a draw call. 0 disables completely. Currently only respected by the Metal backend. + */ + size_t textureUseAfterFreePoolSize = 0; }; Platform() noexcept; diff --git a/filament/backend/include/private/backend/Driver.h b/filament/backend/include/private/backend/Driver.h index a8c31ef4bf0..c0be64f6cd5 100644 --- a/filament/backend/include/private/backend/Driver.h +++ b/filament/backend/include/private/backend/Driver.h @@ -24,6 +24,7 @@ #include #include +#include #include #include diff --git a/filament/backend/include/private/backend/DriverAPI.inc b/filament/backend/include/private/backend/DriverAPI.inc index 37ddd4c6ba0..5e5e82641ba 100644 --- a/filament/backend/include/private/backend/DriverAPI.inc +++ b/filament/backend/include/private/backend/DriverAPI.inc @@ -219,7 +219,7 @@ DECL_DRIVER_API_R_N(backend::TextureHandle, importTexture, backend::TextureUsage, usage) DECL_DRIVER_API_R_N(backend::SamplerGroupHandle, createSamplerGroup, - uint32_t, size) + uint32_t, size, utils::FixedSizeString<32>, debugName) DECL_DRIVER_API_R_N(backend::RenderPrimitiveHandle, createRenderPrimitive, backend::VertexBufferHandle, vbh, diff --git a/filament/backend/src/metal/MetalContext.h b/filament/backend/src/metal/MetalContext.h index 7b118f95bc1..5a26e2600aa 100644 --- a/filament/backend/src/metal/MetalContext.h +++ b/filament/backend/src/metal/MetalContext.h @@ -24,6 +24,8 @@ #include #include +#include + #include #include #include @@ -53,6 +55,9 @@ struct MetalVertexBuffer; constexpr static uint8_t MAX_SAMPLE_COUNT = 8; // Metal devices support at most 8 MSAA samples struct MetalContext { + explicit MetalContext(size_t metalFreedTextureListSize) + : texturesToDestroy(metalFreedTextureListSize) {} + MetalDriver* driver; id device = nullptr; id commandQueue = nullptr; @@ -111,6 +116,14 @@ struct MetalContext { tsl::robin_set samplerGroups; tsl::robin_set textures; + // This circular buffer implements delayed destruction for Metal texture handles. It keeps a + // handle to a fixed number of the most recently destroyed texture handles. When we're asked to + // destroy a texture handle, we free its texture memory, but keep the MetalTexture object alive, + // marking it as "terminated". If we later are asked to use that texture, we can check its + // terminated status and throw an Objective-C error instead of crashing, which is helpful for + // debugging use-after-free issues in release builds. + utils::FixedCircularBuffer> texturesToDestroy; + MetalBufferPool* bufferPool; MetalSwapChain* currentDrawSwapChain = nil; diff --git a/filament/backend/src/metal/MetalDriver.mm b/filament/backend/src/metal/MetalDriver.mm index d14d9fb900c..8933430fc2a 100644 --- a/filament/backend/src/metal/MetalDriver.mm +++ b/filament/backend/src/metal/MetalDriver.mm @@ -50,7 +50,8 @@ Driver* MetalDriver::create(MetalPlatform* const platform, const Platform::DriverConfig& driverConfig) { assert_invariant(platform); size_t defaultSize = FILAMENT_METAL_HANDLE_ARENA_SIZE_IN_MB * 1024U * 1024U; - Platform::DriverConfig validConfig { .handleArenaSize = std::max(driverConfig.handleArenaSize, defaultSize) }; + Platform::DriverConfig validConfig {driverConfig}; + validConfig.handleArenaSize = std::max(driverConfig.handleArenaSize, defaultSize); return new MetalDriver(platform, validConfig); } @@ -60,7 +61,7 @@ MetalDriver::MetalDriver(MetalPlatform* platform, const Platform::DriverConfig& driverConfig) noexcept : mPlatform(*platform), - mContext(new MetalContext), + mContext(new MetalContext(driverConfig.textureUseAfterFreePoolSize)), mHandleAllocator("Handles", driverConfig.handleArenaSize) { mContext->driver = this; @@ -303,8 +304,9 @@ target, levels, format, samples, width, height, depth, usage, metalTexture)); } -void MetalDriver::createSamplerGroupR(Handle sbh, uint32_t size) { - mContext->samplerGroups.insert(construct_handle(sbh, size)); +void MetalDriver::createSamplerGroupR( + Handle sbh, uint32_t size, utils::FixedSizeString<32> debugName) { + mContext->samplerGroups.insert(construct_handle(sbh, size, debugName)); } void MetalDriver::createRenderPrimitiveR(Handle rph, @@ -530,8 +532,18 @@ return; } - mContext->textures.erase(handle_cast(th)); - destruct_handle(th); + auto* metalTexture = handle_cast(th); + mContext->textures.erase(metalTexture); + + // Free memory from the texture and mark it as freed. + metalTexture->terminate(); + + // Add this texture handle to our texturesToDestroy queue to be destroyed later. + if (auto handleToFree = mContext->texturesToDestroy.push(th)) { + // If texturesToDestroy is full, then .push evicts the oldest texture handle in the + // queue (or simply th, if use-after-free detection is disabled). + destruct_handle(handleToFree.value()); + } } void MetalDriver::destroyRenderTarget(Handle rth) { @@ -557,6 +569,12 @@ } void MetalDriver::terminate() { + // Terminate any outstanding MetalTextures. + while (!mContext->texturesToDestroy.empty()) { + Handle toDestroy = mContext->texturesToDestroy.pop(); + destruct_handle(toDestroy); + } + // finish() will flush the pending command buffer and will ensure all GPU work has finished. // This must be done before calling bufferPool->reset() to ensure no buffers are in flight. finish(); @@ -854,13 +872,17 @@ assert_invariant(sb->size == data.size / sizeof(SamplerDescriptor)); auto const* const samplers = (SamplerDescriptor const*) data.buffer; -#ifndef NDEBUG - // In debug builds, verify that all the textures in the sampler group are still alive. + // Verify that all the textures in the sampler group are still alive. // These bugs lead to memory corruption and can be difficult to track down. for (size_t s = 0; s < data.size / sizeof(SamplerDescriptor); s++) { if (!samplers[s].t) { continue; } + // The difference between this check and the one below is that in release, we do this for + // only a set number of recently freed textures, while the debug check is exhaustive. + auto* metalTexture = handle_cast(samplers[s].t); + metalTexture->checkUseAfterFree(sb->debugName.c_str(), s); +#ifndef NDEBUG auto iter = mContext->textures.find(handle_cast(samplers[s].t)); if (iter == mContext->textures.end()) { utils::slog.e << "updateSamplerGroup: texture #" @@ -868,8 +890,8 @@ << samplers[s].t << utils::io::endl; } assert_invariant(iter != mContext->textures.end()); - } #endif + } // Create a MTLArgumentEncoder for these textures. // Ideally, we would create this encoder at createSamplerGroup time, but we need to know the @@ -1357,23 +1379,27 @@ id cmdBuffer = getPendingCommandBuffer(mContext); -#ifndef NDEBUG - // In debug builds, verify that all the textures in the sampler group are still alive. + // Verify that all the textures in the sampler group are still alive. // These bugs lead to memory corruption and can be difficult to track down. const auto& handles = samplerGroup->getTextureHandles(); for (size_t s = 0; s < handles.size(); s++) { if (!handles[s]) { continue; } - auto iter = mContext->textures.find(handle_cast(handles[s])); + // The difference between this check and the one below is that in release, we do this for + // only a set number of recently freed textures, while the debug check is exhaustive. + auto* metalTexture = handle_cast(handles[s]); + metalTexture->checkUseAfterFree(samplerGroup->debugName.c_str(), s); +#ifndef NDEBUG + auto iter = mContext->textures.find(metalTexture); if (iter == mContext->textures.end()) { utils::slog.e << "finalizeSamplerGroup: texture #" << (int) s << " is dead, texture handle = " << handles[s] << utils::io::endl; } assert_invariant(iter != mContext->textures.end()); - } #endif + } utils::FixedCapacityVector> newTextures(samplerGroup->size, nil); for (size_t binding = 0; binding < samplerGroup->size; binding++) { diff --git a/filament/backend/src/metal/MetalHandles.h b/filament/backend/src/metal/MetalHandles.h index cd3a4e8c68d..b4db3131eec 100644 --- a/filament/backend/src/metal/MetalHandles.h +++ b/filament/backend/src/metal/MetalHandles.h @@ -32,6 +32,7 @@ #include "private/backend/SamplerGroup.h" #include +#include #include #include @@ -228,8 +229,8 @@ class MetalTexture : public HwTexture { // - using the texture as a render target attachment // - calling setMinMaxLevels // A texture's available mips are consistent throughout a render pass. - void setLodRange(uint32_t minLevel, uint32_t maxLevel); - void extendLodRangeTo(uint32_t level); + void setLodRange(uint16_t minLevel, uint16_t maxLevel); + void extendLodRangeTo(uint16_t level); static MTLPixelFormat decidePixelFormat(MetalContext* context, TextureFormat format); @@ -243,6 +244,26 @@ class MetalTexture : public HwTexture { MTLPixelFormat devicePixelFormat; + // Frees memory associated with this texture and marks it as "terminated". + // Used to track "use after free" scenario. + void terminate() noexcept; + bool isTerminated() const noexcept { return terminated; } + inline void checkUseAfterFree(const char* samplerGroupDebugName, size_t textureIndex) const { + if (UTILS_LIKELY(!isTerminated())) { + return; + } + NSString* reason = + [NSString stringWithFormat: + @"Filament Metal texture use after free, sampler group = " + @"%s, texture index = %zu", + samplerGroupDebugName, textureIndex]; + NSException* useAfterFreeException = + [NSException exceptionWithName:@"MetalTextureUseAfterFree" + reason:reason + userInfo:nil]; + [useAfterFreeException raise]; + } + private: void loadSlice(uint32_t level, MTLRegion region, uint32_t byteOffset, uint32_t slice, PixelBufferDescriptor const& data) noexcept; @@ -259,14 +280,17 @@ class MetalTexture : public HwTexture { id swizzledTextureView = nil; id lodTextureView = nil; - uint32_t minLod = UINT_MAX; - uint32_t maxLod = 0; + uint16_t minLod = std::numeric_limits::max(); + uint16_t maxLod = 0; + + bool terminated = false; }; class MetalSamplerGroup : public HwSamplerGroup { public: - explicit MetalSamplerGroup(size_t size) noexcept + explicit MetalSamplerGroup(size_t size, utils::FixedSizeString<32> name) noexcept : size(size), + debugName(name), textureHandles(size, Handle()), textures(size, nil), samplers(size, nil) {} @@ -276,12 +300,10 @@ class MetalSamplerGroup : public HwSamplerGroup { textureHandles[index] = th; } -#ifndef NDEBUG // This method is only used for debugging, to ensure all texture handles are alive. const auto& getTextureHandles() const { return textureHandles; } -#endif // Encode a MTLTexture into this SamplerGroup at the given index. inline void setFinalizedTexture(size_t index, id t) { @@ -327,6 +349,7 @@ class MetalSamplerGroup : public HwSamplerGroup { void useResources(id renderPassEncoder); size_t size; + utils::FixedSizeString<32> debugName; public: diff --git a/filament/backend/src/metal/MetalHandles.mm b/filament/backend/src/metal/MetalHandles.mm index 595ec6f9cb4..c43338c4487 100644 --- a/filament/backend/src/metal/MetalHandles.mm +++ b/filament/backend/src/metal/MetalHandles.mm @@ -535,6 +535,15 @@ void presentDrawable(bool presentFrame, void* user) { setLodRange(0, levels - 1); } +void MetalTexture::terminate() noexcept { + texture = nil; + swizzledTextureView = nil; + lodTextureView = nil; + msaaSidecar = nil; + externalImage.set(nullptr); + terminated = true; +} + MetalTexture::~MetalTexture() { externalImage.set(nullptr); } @@ -807,14 +816,14 @@ void presentDrawable(bool presentFrame, void* user) { context.blitter->blit(getPendingCommandBuffer(&context), args, "Texture upload blit"); } -void MetalTexture::extendLodRangeTo(uint32_t level) { +void MetalTexture::extendLodRangeTo(uint16_t level) { assert_invariant(!isInRenderPass(&context)); minLod = std::min(minLod, level); maxLod = std::max(maxLod, level); lodTextureView = nil; } -void MetalTexture::setLodRange(uint32_t min, uint32_t max) { +void MetalTexture::setLodRange(uint16_t min, uint16_t max) { assert_invariant(!isInRenderPass(&context)); assert_invariant(min <= max); minLod = min; diff --git a/filament/backend/src/opengl/OpenGLDriver.cpp b/filament/backend/src/opengl/OpenGLDriver.cpp index 2f0e6f618a7..3884427fbbe 100644 --- a/filament/backend/src/opengl/OpenGLDriver.cpp +++ b/filament/backend/src/opengl/OpenGLDriver.cpp @@ -147,8 +147,8 @@ Driver* OpenGLDriver::create(OpenGLPlatform* const platform, #endif size_t const defaultSize = FILAMENT_OPENGL_HANDLE_ARENA_SIZE_IN_MB * 1024U * 1024U; - Platform::DriverConfig const validConfig { - .handleArenaSize = std::max(driverConfig.handleArenaSize, defaultSize) }; + Platform::DriverConfig validConfig {driverConfig}; + validConfig.handleArenaSize = std::max(driverConfig.handleArenaSize, defaultSize); OpenGLDriver* const driver = new OpenGLDriver(ec, validConfig); return driver; } @@ -555,7 +555,8 @@ void OpenGLDriver::createProgramR(Handle ph, Program&& program) { CHECK_GL_ERROR(utils::slog.e) } -void OpenGLDriver::createSamplerGroupR(Handle sbh, uint32_t size) { +void OpenGLDriver::createSamplerGroupR(Handle sbh, uint32_t size, + utils::FixedSizeString<32> debugName) { DEBUG_MARKER() construct(sbh, size); diff --git a/filament/backend/src/vulkan/VulkanDriver.cpp b/filament/backend/src/vulkan/VulkanDriver.cpp index 217aab56905..4b8f4d5e4f7 100644 --- a/filament/backend/src/vulkan/VulkanDriver.cpp +++ b/filament/backend/src/vulkan/VulkanDriver.cpp @@ -214,8 +214,8 @@ Driver* VulkanDriver::create(VulkanPlatform* platform, VulkanContext const& cont Platform::DriverConfig const& driverConfig) noexcept { assert_invariant(platform); size_t defaultSize = FVK_HANDLE_ARENA_SIZE_IN_MB * 1024U * 1024U; - Platform::DriverConfig validConfig{ - .handleArenaSize = std::max(driverConfig.handleArenaSize, defaultSize)}; + Platform::DriverConfig validConfig {driverConfig}; + validConfig.handleArenaSize = std::max(driverConfig.handleArenaSize, defaultSize); return new VulkanDriver(platform, context, validConfig); } @@ -315,7 +315,8 @@ void VulkanDriver::finish(int dummy) { FVK_SYSTRACE_END(); } -void VulkanDriver::createSamplerGroupR(Handle sbh, uint32_t count) { +void VulkanDriver::createSamplerGroupR(Handle sbh, uint32_t count, + utils::FixedSizeString<32> debugName) { auto sg = mResourceAllocator.construct(sbh, count); mResourceManager.acquire(sg); } diff --git a/filament/backend/test/test_FeedbackLoops.cpp b/filament/backend/test/test_FeedbackLoops.cpp index 605328f100f..24dd7b202a3 100644 --- a/filament/backend/test/test_FeedbackLoops.cpp +++ b/filament/backend/test/test_FeedbackLoops.cpp @@ -193,7 +193,8 @@ TEST_F(BackendTest, FeedbackLoops) { sparams.filterMag = SamplerMagFilter::LINEAR; sparams.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST; samplers.setSampler(0, { texture, sparams }); - auto sgroup = api.createSamplerGroup(samplers.getSize()); + auto sgroup = + api.createSamplerGroup(samplers.getSize(), utils::FixedSizeString<32>("Test")); api.updateSamplerGroup(sgroup, samplers.toBufferDescriptor(api)); auto ubuffer = api.createBufferObject(sizeof(MaterialParams), BufferObjectBinding::UNIFORM, BufferUsage::STATIC); diff --git a/filament/backend/test/test_LoadImage.cpp b/filament/backend/test/test_LoadImage.cpp index ddfab69a8a8..9b9dbfdf12d 100644 --- a/filament/backend/test/test_LoadImage.cpp +++ b/filament/backend/test/test_LoadImage.cpp @@ -303,7 +303,7 @@ TEST_F(BackendTest, UpdateImage2D) { sparams.filterMag = SamplerMagFilter::LINEAR; sparams.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST; samplers.setSampler(0, { texture, sparams }); - auto sgroup = api.createSamplerGroup(samplers.getSize()); + auto sgroup = api.createSamplerGroup(samplers.getSize(), utils::FixedSizeString<32>("Test")); api.updateSamplerGroup(sgroup, samplers.toBufferDescriptor(api)); api.bindSamplers(0, sgroup); @@ -394,7 +394,7 @@ TEST_F(BackendTest, UpdateImageSRGB) { sparams.filterMag = SamplerMagFilter::LINEAR; sparams.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST; samplers.setSampler(0, { texture, sparams }); - auto sgroup = api.createSamplerGroup(samplers.getSize()); + auto sgroup = api.createSamplerGroup(samplers.getSize(), utils::FixedSizeString<32>("Test")); api.updateSamplerGroup(sgroup, samplers.toBufferDescriptor(api)); api.bindSamplers(0, sgroup); @@ -469,7 +469,7 @@ TEST_F(BackendTest, UpdateImageMipLevel) { sparams.filterMag = SamplerMagFilter::LINEAR; sparams.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST; samplers.setSampler(0, { texture, sparams }); - auto sgroup = api.createSamplerGroup(samplers.getSize()); + auto sgroup = api.createSamplerGroup(samplers.getSize(), utils::FixedSizeString<32>("Test")); api.updateSamplerGroup(sgroup, samplers.toBufferDescriptor(api)); api.bindSamplers(0, sgroup); @@ -556,7 +556,7 @@ TEST_F(BackendTest, UpdateImage3D) { sparams.filterMag = SamplerMagFilter::LINEAR; sparams.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST; samplers.setSampler(0, { texture, sparams}); - auto sgroup = api.createSamplerGroup(samplers.getSize()); + auto sgroup = api.createSamplerGroup(samplers.getSize(), utils::FixedSizeString<32>("Test")); api.updateSamplerGroup(sgroup, samplers.toBufferDescriptor(api)); api.bindSamplers(0, sgroup); diff --git a/filament/backend/test/test_MipLevels.cpp b/filament/backend/test/test_MipLevels.cpp index a794003ba3f..738e21fc812 100644 --- a/filament/backend/test/test_MipLevels.cpp +++ b/filament/backend/test/test_MipLevels.cpp @@ -194,7 +194,8 @@ TEST_F(BackendTest, SetMinMaxLevel) { samplerParams.filterMag = SamplerMagFilter::NEAREST; samplerParams.filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST; samplers.setSampler(0, { texture, samplerParams }); - backend::Handle samplerGroup = api.createSamplerGroup(1); + backend::Handle samplerGroup = + api.createSamplerGroup(1, utils::FixedSizeString<32>("Test")); api.updateSamplerGroup(samplerGroup, samplers.toBufferDescriptor(api)); api.bindSamplers(0, samplerGroup); @@ -242,4 +243,4 @@ TEST_F(BackendTest, SetMinMaxLevel) { getDriver().purge(); } -} // namespace test \ No newline at end of file +} // namespace test diff --git a/filament/backend/test/test_RenderExternalImage.cpp b/filament/backend/test/test_RenderExternalImage.cpp index 8101dd1467c..27519a0a255 100644 --- a/filament/backend/test/test_RenderExternalImage.cpp +++ b/filament/backend/test/test_RenderExternalImage.cpp @@ -113,7 +113,8 @@ TEST_F(BackendTest, RenderExternalImageWithoutSet) { SamplerGroup samplers(1); samplers.setSampler(0, { texture, {} }); - backend::Handle samplerGroup = getDriverApi().createSamplerGroup(1); + backend::Handle samplerGroup = + getDriverApi().createSamplerGroup(1, utils::FixedSizeString<32>("Test")); getDriverApi().updateSamplerGroup(samplerGroup, samplers.toBufferDescriptor(getDriverApi())); getDriverApi().bindSamplers(0, samplerGroup); @@ -234,7 +235,8 @@ TEST_F(BackendTest, RenderExternalImage) { SamplerGroup samplers(1); samplers.setSampler(0, { texture, {} }); - backend::Handle samplerGroup = getDriverApi().createSamplerGroup(1); + backend::Handle samplerGroup = + getDriverApi().createSamplerGroup(1, utils::FixedSizeString<32>("Test")); getDriverApi().updateSamplerGroup(samplerGroup, samplers.toBufferDescriptor(getDriverApi())); getDriverApi().bindSamplers(0, samplerGroup); diff --git a/filament/include/filament/Engine.h b/filament/include/filament/Engine.h index f4173f26144..30d01526fd6 100644 --- a/filament/include/filament/Engine.h +++ b/filament/include/filament/Engine.h @@ -267,6 +267,16 @@ class UTILS_PUBLIC Engine { * This value does not affect the application's memory usage. */ uint32_t perFrameCommandsSizeMB = FILAMENT_PER_FRAME_COMMANDS_SIZE_IN_MB; + + /* + * Number of most-recently destroyed textures to track for use-after-free. + * + * This will cause the backend to throw an exception when a texture is freed but still bound + * to a SamplerGroup and used in a draw call. 0 disables completely. + * + * Currently only respected by the Metal backend. + */ + size_t textureUseAfterFreePoolSize = 0; }; diff --git a/filament/src/PerViewUniforms.cpp b/filament/src/PerViewUniforms.cpp index c24c46fab62..a7b6513fbcb 100644 --- a/filament/src/PerViewUniforms.cpp +++ b/filament/src/PerViewUniforms.cpp @@ -43,7 +43,8 @@ PerViewUniforms::PerViewUniforms(FEngine& engine) noexcept : mSamplers(PerViewSib::SAMPLER_COUNT) { DriverApi& driver = engine.getDriverApi(); - mSamplerGroupHandle = driver.createSamplerGroup(mSamplers.getSize()); + mSamplerGroupHandle = driver.createSamplerGroup( + mSamplers.getSize(), utils::FixedSizeString<32>("Per-view samplers")); mUniformBufferHandle = driver.createBufferObject(mUniforms.getSize(), BufferObjectBinding::UNIFORM, BufferUsage::DYNAMIC); diff --git a/filament/src/details/Engine.cpp b/filament/src/details/Engine.cpp index 3e50fc71352..103c382b812 100644 --- a/filament/src/details/Engine.cpp +++ b/filament/src/details/Engine.cpp @@ -606,7 +606,10 @@ int FEngine::loop() { JobSystem::setThreadName("FEngine::loop"); JobSystem::setThreadPriority(JobSystem::Priority::DISPLAY); - DriverConfig const driverConfig { .handleArenaSize = getRequestedDriverHandleArenaSize() }; + DriverConfig const driverConfig { + .handleArenaSize = getRequestedDriverHandleArenaSize(), + .textureUseAfterFreePoolSize = mConfig.textureUseAfterFreePoolSize + }; mDriver = mPlatform->createDriver(mSharedGLContext, driverConfig); mDriverBarrier.latch(); diff --git a/filament/src/details/MaterialInstance.cpp b/filament/src/details/MaterialInstance.cpp index c23915c6d22..ad3ea314ce0 100644 --- a/filament/src/details/MaterialInstance.cpp +++ b/filament/src/details/MaterialInstance.cpp @@ -73,7 +73,8 @@ FMaterialInstance::FMaterialInstance(FEngine& engine, if (!material->getSamplerInterfaceBlock().isEmpty()) { mSamplers = other->getSamplerGroup(); - mSbHandle = driver.createSamplerGroup(mSamplers.getSize()); + mSbHandle = driver.createSamplerGroup( + mSamplers.getSize(), utils::FixedSizeString<32>(mMaterial->getName().c_str_safe())); } if (material->hasDoubleSidedCapability()) { @@ -115,7 +116,8 @@ void FMaterialInstance::initDefaultInstance(FEngine& engine, FMaterial const* ma if (!material->getSamplerInterfaceBlock().isEmpty()) { mSamplers = SamplerGroup(material->getSamplerInterfaceBlock().getSize()); - mSbHandle = driver.createSamplerGroup(mSamplers.getSize()); + mSbHandle = driver.createSamplerGroup( + mSamplers.getSize(), utils::FixedSizeString<32>("Default material")); } const RasterState& rasterState = material->getRasterState(); diff --git a/filament/src/details/MorphTargetBuffer.cpp b/filament/src/details/MorphTargetBuffer.cpp index 6102b3c1169..b615e6e7054 100644 --- a/filament/src/details/MorphTargetBuffer.cpp +++ b/filament/src/details/MorphTargetBuffer.cpp @@ -125,7 +125,8 @@ FMorphTargetBuffer::FMorphTargetBuffer(FEngine& engine, const Builder& builder) TextureUsage::DEFAULT); // create and update sampler group - mSbHandle = driver.createSamplerGroup(PerRenderPrimitiveMorphingSib::SAMPLER_COUNT); + mSbHandle = driver.createSamplerGroup(PerRenderPrimitiveMorphingSib::SAMPLER_COUNT, + utils::FixedSizeString<32>("Morph target samplers")); SamplerGroup samplerGroup(PerRenderPrimitiveMorphingSib::SAMPLER_COUNT); samplerGroup.setSampler(PerRenderPrimitiveMorphingSib::POSITIONS, { mPbHandle, {}}); samplerGroup.setSampler(PerRenderPrimitiveMorphingSib::TANGENTS, { mTbHandle, {}}); diff --git a/filament/src/details/SkinningBuffer.cpp b/filament/src/details/SkinningBuffer.cpp index b70e454af5e..74de88f713b 100644 --- a/filament/src/details/SkinningBuffer.cpp +++ b/filament/src/details/SkinningBuffer.cpp @@ -242,7 +242,8 @@ FSkinningBuffer::HandleIndicesAndWeights FSkinningBuffer::createIndicesAndWeight getSkinningBufferWidth(count), getSkinningBufferHeight(count), 1, TextureUsage::DEFAULT); - samplerHandle = driver.createSamplerGroup(PerRenderPrimitiveSkinningSib::SAMPLER_COUNT); + samplerHandle = driver.createSamplerGroup(PerRenderPrimitiveSkinningSib::SAMPLER_COUNT, + utils::FixedSizeString<32>("Skinning buffer samplers")); SamplerGroup samplerGroup(PerRenderPrimitiveSkinningSib::SAMPLER_COUNT); samplerGroup.setSampler(PerRenderPrimitiveSkinningSib::BONE_INDICES_AND_WEIGHTS, { textureHandle, {}}); diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt index 0e26b014e04..9f6b33bba79 100644 --- a/libs/utils/CMakeLists.txt +++ b/libs/utils/CMakeLists.txt @@ -148,6 +148,7 @@ set(TEST_SRCS test/test_CyclicBarrier.cpp test/test_Entity.cpp test/test_FixedCapacityVector.cpp + test/test_FixedCircularBuffer.cpp test/test_Hash.cpp test/test_JobSystem.cpp test/test_QuadTreeArray.cpp diff --git a/libs/utils/include/utils/CString.h b/libs/utils/include/utils/CString.h index 46a823b4c13..3e4c26db3c7 100644 --- a/libs/utils/include/utils/CString.h +++ b/libs/utils/include/utils/CString.h @@ -225,6 +225,28 @@ class UTILS_PUBLIC CString { template CString to_string(T value) noexcept; +// ------------------------------------------------------------------------------------------------ + +template +class UTILS_PUBLIC FixedSizeString { +public: + using value_type = char; + using pointer = value_type*; + using const_pointer = const value_type*; + static_assert(N > 0); + + FixedSizeString() noexcept = default; + explicit FixedSizeString(const char* str) noexcept { + strncpy(mData, str, N - 1); // leave room for the null terminator + } + + const_pointer c_str() const noexcept { return mData; } + pointer c_str() noexcept { return mData; } + +private: + value_type mData[N] = {0}; +}; + } // namespace utils #endif // TNT_UTILS_CSTRING_H diff --git a/libs/utils/include/utils/FixedCircularBuffer.h b/libs/utils/include/utils/FixedCircularBuffer.h new file mode 100644 index 00000000000..5252b526062 --- /dev/null +++ b/libs/utils/include/utils/FixedCircularBuffer.h @@ -0,0 +1,77 @@ +/* + * 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. + * + */ + +#ifndef TNT_UTILS_FIXEDCIRCULARBUFFER_H +#define TNT_UTILS_FIXEDCIRCULARBUFFER_H + +#include + +#include +#include +#include +#include + +namespace utils { + +template +class FixedCircularBuffer { +public: + explicit FixedCircularBuffer(size_t capacity) + : mData(std::make_unique(capacity)), mCapacity(capacity) {} + + size_t size() const noexcept { return mSize; } + size_t capacity() const noexcept { return mCapacity; } + bool full() const noexcept { return mCapacity > 0 && mSize == mCapacity; } + bool empty() const noexcept { return mSize == 0; } + + /** + * Push v into the buffer. If the buffer is already full, removes the oldest item and returns + * it. If this buffer has no capacity, simply returns v. + * @param v the new value to push into the buffer + * @return if the buffer was full, the oldest value which was displaced + */ + std::optional push(T v) noexcept { + if (mCapacity == 0) { + return v; + } + std::optional displaced = full() ? pop() : std::optional{}; + mData[mEnd] = v; + mEnd = (mEnd + 1) % mCapacity; + mSize++; + return displaced; + } + + T pop() noexcept { + assert_invariant(mSize > 0); + T result = mData[mBegin]; + mBegin = (mBegin + 1) % mCapacity; + mSize--; + return result; + } + +private: + std::unique_ptr mData; + + size_t mBegin = 0; + size_t mEnd = 0; + size_t mSize = 0; + size_t mCapacity; +}; + +} // namespace utils + +#endif // TNT_UTILS_FIXEDCIRCULARBUFFER_H diff --git a/libs/utils/test/test_CString.cpp b/libs/utils/test/test_CString.cpp index 759f30a4527..a19de9b7a45 100644 --- a/libs/utils/test/test_CString.cpp +++ b/libs/utils/test/test_CString.cpp @@ -93,3 +93,25 @@ TEST(CString, ReplacePastEndOfString) { EXPECT_STREQ("foo bar bat", str.c_str()); } } + +TEST(FixedSizeString, EmptyString) { + { + FixedSizeString<32> str; + EXPECT_STREQ("", str.c_str()); + } + { + FixedSizeString<32> str(""); + EXPECT_STREQ("", str.c_str()); + } +} + +TEST(FixedSizeString, Constructors) { + { + FixedSizeString<32> str("short string"); + EXPECT_STREQ("short string", str.c_str()); + } + { + FixedSizeString<16> str("a long string abcdefghijklmnopqrst"); + EXPECT_STREQ("a long string a", str.c_str()); + } +} diff --git a/libs/utils/test/test_FixedCircularBuffer.cpp b/libs/utils/test/test_FixedCircularBuffer.cpp new file mode 100644 index 00000000000..8749dfb89a1 --- /dev/null +++ b/libs/utils/test/test_FixedCircularBuffer.cpp @@ -0,0 +1,97 @@ +/* + * 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 + +#include + +using namespace utils; + +TEST(FixedCircularBufferTest, Simple) { + FixedCircularBuffer circularBuffer(4); + EXPECT_EQ(circularBuffer.size(), 0); + + EXPECT_FALSE(circularBuffer.push(1).has_value()); + EXPECT_FALSE(circularBuffer.push(2).has_value()); + EXPECT_FALSE(circularBuffer.push(3).has_value()); + EXPECT_EQ(circularBuffer.size(), 3); + EXPECT_EQ(circularBuffer.pop(), 1); + EXPECT_EQ(circularBuffer.pop(), 2); + EXPECT_EQ(circularBuffer.pop(), 3); + EXPECT_EQ(circularBuffer.size(), 0); + + EXPECT_FALSE(circularBuffer.push(4).has_value()); + EXPECT_FALSE(circularBuffer.push(5).has_value()); + EXPECT_FALSE(circularBuffer.push(6).has_value()); + EXPECT_FALSE(circularBuffer.push(7).has_value()); + EXPECT_EQ(circularBuffer.size(), 4); + EXPECT_TRUE(circularBuffer.full()); + EXPECT_EQ(circularBuffer.pop(), 4); + EXPECT_EQ(circularBuffer.pop(), 5); + EXPECT_EQ(circularBuffer.pop(), 6); + EXPECT_EQ(circularBuffer.pop(), 7); +} + +TEST(FixedCircularBufferTest, Displace) { + FixedCircularBuffer circularBuffer(4); + EXPECT_EQ(circularBuffer.size(), 0); + EXPECT_FALSE(circularBuffer.push(1).has_value()); + EXPECT_FALSE(circularBuffer.push(2).has_value()); + EXPECT_FALSE(circularBuffer.push(3).has_value()); + EXPECT_FALSE(circularBuffer.push(4).has_value()); + EXPECT_TRUE(circularBuffer.full()); + + { + auto v = circularBuffer.push(5); + EXPECT_EQ(v.value(), 1); + } + { + auto v = circularBuffer.push(6); + EXPECT_EQ(v.value(), 2); + } + EXPECT_TRUE(circularBuffer.full()); + + EXPECT_EQ(circularBuffer.pop(), 3); + EXPECT_EQ(circularBuffer.size(), 3); +} + +TEST(FixedCircularBufferTest, ZeroCapacity) { + FixedCircularBuffer circularBuffer(0); + EXPECT_EQ(circularBuffer.size(), 0); + EXPECT_EQ(circularBuffer.full(), false); + + auto v = circularBuffer.push(1); + EXPECT_EQ(v.value(), 1); + EXPECT_EQ(circularBuffer.size(), 0); + EXPECT_EQ(circularBuffer.full(), false); +} + +TEST(FixedCircularBufferTest, Exceptions) { +#if !defined(NDEBUG) && defined(GTEST_HAS_DEATH_TEST) + FixedCircularBuffer circularBuffer(4); + + EXPECT_DEATH({ + circularBuffer.pop(); // should assert + }, "failed assertion"); + + circularBuffer.push(1); + circularBuffer.push(2); + circularBuffer.push(3); + circularBuffer.push(4); + circularBuffer.push(5); // should not assert +#endif +}