diff --git a/.github/actions/ubuntu-apt-add-src/action.yml b/.github/actions/ubuntu-apt-add-src/action.yml new file mode 100644 index 00000000000..c104574a632 --- /dev/null +++ b/.github/actions/ubuntu-apt-add-src/action.yml @@ -0,0 +1,9 @@ +name: 'ubuntu apt add deb-src' +runs: + using: "composite" + steps: + - name: "ubuntu apt add deb-src" + run: | + echo "deb-src http://archive.ubuntu.com/ubuntu jammy main restricted universe" | sudo tee /etc/apt/sources.list.d/my.list + sudo apt-get update + shell: bash diff --git a/.github/workflows/presubmit.yml b/.github/workflows/presubmit.yml index 8e0d9ccc08f..151dd375657 100644 --- a/.github/workflows/presubmit.yml +++ b/.github/workflows/presubmit.yml @@ -76,3 +76,18 @@ jobs: - name: Run build script run: | cd build/web && printf "y" | ./build.sh presubmit + + test-renderdiff: + name: test-renderdiff + runs-on: ubuntu-22.04-32core + + steps: + - uses: actions/checkout@v4.1.6 + - uses: ./.github/actions/ubuntu-apt-add-src + - name: Run script + run: | + source ./build/linux/ci-common.sh && bash test/renderdiff_tests.sh + - uses: actions/upload-artifact@v4 + with: + name: presubmit-renderdiff-result + path: ./out/renderdiff_tests diff --git a/CMakeLists.txt b/CMakeLists.txt index 283778b327b..649577b396c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,8 @@ option(FILAMENT_ENABLE_FEATURE_LEVEL_0 "Enable Feature Level 0" ON) option(FILAMENT_ENABLE_MULTIVIEW "Enable multiview for Filament" OFF) +option(FILAMENT_SUPPORTS_OSMESA "Enable OSMesa (headless GL context) for Filament" OFF) + set(FILAMENT_NDK_VERSION "" CACHE STRING "Android NDK version or version prefix to be used when building for Android." ) @@ -73,6 +75,10 @@ set(FILAMENT_BACKEND_DEBUG_FLAG "" CACHE STRING "A debug flag meant for enabling/disabling backend debugging paths" ) +set(FILAMENT_OSMESA_PATH "" CACHE STRING + "Path to the OSMesa header and lib" +) + # Enable exceptions by default in spirv-cross. set(SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS OFF) @@ -132,12 +138,22 @@ else() endif() if (LINUX) + if (NOT FILAMENT_OSMESA_PATH STREQUAL "") + if (NOT EXISTS ${FILAMENT_OSMESA_PATH}/) + message(FATAL_ERROR "Cannot find specified OSMesa build directory: ${FILAMENT_OSMESA_PATH}") + endif() + set(FILAMENT_SUPPORTS_OSMESA TRUE) + endif() + if (FILAMENT_SUPPORTS_WAYLAND) add_definitions(-DFILAMENT_SUPPORTS_WAYLAND) set(FILAMENT_SUPPORTS_X11 FALSE) elseif (FILAMENT_SUPPORTS_EGL_ON_LINUX) add_definitions(-DFILAMENT_SUPPORTS_EGL_ON_LINUX) set(FILAMENT_SUPPORTS_X11 FALSE) + elseif (FILAMENT_SUPPORTS_OSMESA) + set(FILAMENT_SUPPORTS_X11 FALSE) + add_definitions(-DFILAMENT_SUPPORTS_OSMESA) else () if (FILAMENT_SUPPORTS_XCB) add_definitions(-DFILAMENT_SUPPORTS_XCB) diff --git a/README.md b/README.md index 1daff364f40..15741c613ff 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ repositories { } dependencies { - implementation 'com.google.android.filament:filament-android:1.55.0' + implementation 'com.google.android.filament:filament-android:1.55.1' } ``` @@ -51,7 +51,7 @@ Here are all the libraries available in the group `com.google.android.filament`: iOS projects can use CocoaPods to install the latest release: ```shell -pod 'Filament', '~> 1.55.0' +pod 'Filament', '~> 1.55.1' ``` ## Documentation diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e6727d791a7..b5522d40a21 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -7,6 +7,9 @@ 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.55.1 + + ## v1.55.0 - Add descriptor sets to describe shader resources. [⚠️ **New Material Version**] diff --git a/android/filament-android/src/main/cpp/Engine.cpp b/android/filament-android/src/main/cpp/Engine.cpp index f5714222559..7de25a58ba2 100644 --- a/android/filament-android/src/main/cpp/Engine.cpp +++ b/android/filament-android/src/main/cpp/Engine.cpp @@ -520,13 +520,12 @@ extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_Engine_nSetBu extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_Engine_nSetBuilderConfig(JNIEnv*, jclass, jlong nativeBuilder, jlong commandBufferSizeMB, jlong perRenderPassArenaSizeMB, jlong driverHandleArenaSizeMB, jlong minCommandBufferSizeMB, jlong perFrameCommandsSizeMB, - jlong jobSystemThreadCount, - jlong textureUseAfterFreePoolSize, jboolean disableParallelShaderCompile, + jlong jobSystemThreadCount, jboolean disableParallelShaderCompile, jint stereoscopicType, jlong stereoscopicEyeCount, jlong resourceAllocatorCacheSizeMB, jlong resourceAllocatorCacheMaxAge, jboolean disableHandleUseAfterFreeCheck, jint preferredShaderLanguage, - jboolean forceGLES2Context) { + jboolean forceGLES2Context, jboolean assertNativeWindowIsValid) { Engine::Builder* builder = (Engine::Builder*) nativeBuilder; Engine::Config config = { .commandBufferSizeMB = (uint32_t) commandBufferSizeMB, @@ -535,7 +534,6 @@ extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_Engine_nSetBu .minCommandBufferSizeMB = (uint32_t) minCommandBufferSizeMB, .perFrameCommandsSizeMB = (uint32_t) perFrameCommandsSizeMB, .jobSystemThreadCount = (uint32_t) jobSystemThreadCount, - .textureUseAfterFreePoolSize = (uint32_t) textureUseAfterFreePoolSize, .disableParallelShaderCompile = (bool) disableParallelShaderCompile, .stereoscopicType = (Engine::StereoscopicType) stereoscopicType, .stereoscopicEyeCount = (uint8_t) stereoscopicEyeCount, @@ -544,6 +542,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_Engine_nSetBu .disableHandleUseAfterFreeCheck = (bool) disableHandleUseAfterFreeCheck, .preferredShaderLanguage = (Engine::Config::ShaderLanguage) preferredShaderLanguage, .forceGLES2Context = (bool) forceGLES2Context, + .assertNativeWindowIsValid = (bool) assertNativeWindowIsValid, }; builder->config(&config); } diff --git a/android/filament-android/src/main/java/com/google/android/filament/Engine.java b/android/filament-android/src/main/java/com/google/android/filament/Engine.java index 6cadf7d5c7d..a433a55d8c0 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/Engine.java +++ b/android/filament-android/src/main/java/com/google/android/filament/Engine.java @@ -224,13 +224,12 @@ public Builder config(Config config) { nSetBuilderConfig(mNativeBuilder, config.commandBufferSizeMB, config.perRenderPassArenaSizeMB, config.driverHandleArenaSizeMB, config.minCommandBufferSizeMB, config.perFrameCommandsSizeMB, - config.jobSystemThreadCount, - config.textureUseAfterFreePoolSize, config.disableParallelShaderCompile, + config.jobSystemThreadCount, config.disableParallelShaderCompile, config.stereoscopicType.ordinal(), config.stereoscopicEyeCount, config.resourceAllocatorCacheSizeMB, config.resourceAllocatorCacheMaxAge, config.disableHandleUseAfterFreeCheck, config.preferredShaderLanguage.ordinal(), - config.forceGLES2Context); + config.forceGLES2Context, config.assertNativeWindowIsValid); return this; } @@ -419,12 +418,12 @@ public static class Config { */ public long stereoscopicEyeCount = 2; - /* + /** * @Deprecated This value is no longer used. */ public long resourceAllocatorCacheSizeMB = 64; - /* + /** * 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. @@ -432,12 +431,12 @@ public static class Config { */ public long resourceAllocatorCacheMaxAge = 1; - /* + /** * Disable backend handles use-after-free checks. */ public boolean disableHandleUseAfterFreeCheck = false; - /* + /** * Sets a preferred shader language for Filament to use. * * The Metal backend supports two shader languages: MSL (Metal Shading Language) and @@ -459,12 +458,19 @@ public enum ShaderLanguage { }; public ShaderLanguage preferredShaderLanguage = ShaderLanguage.DEFAULT; - /* + /** * When the OpenGL ES backend is used, setting this value to true will force a GLES2.0 * context if supported by the Platform, or if not, will have the backend pretend * it's a GLES2 context. Ignored on other backends. */ public boolean forceGLES2Context = false; + + /** + * Assert the native window associated to a SwapChain is valid when calling makeCurrent(). + * This is only supported for: + * - PlatformEGLAndroid + */ + public boolean assertNativeWindowIsValid = false; } private Engine(long nativeEngine, Config config) { @@ -1406,12 +1412,11 @@ private static void assertDestroy(boolean success) { private static native void nSetBuilderConfig(long nativeBuilder, long commandBufferSizeMB, long perRenderPassArenaSizeMB, long driverHandleArenaSizeMB, long minCommandBufferSizeMB, long perFrameCommandsSizeMB, long jobSystemThreadCount, - long textureUseAfterFreePoolSize, boolean disableParallelShaderCompile, - int stereoscopicType, long stereoscopicEyeCount, + boolean disableParallelShaderCompile, int stereoscopicType, long stereoscopicEyeCount, long resourceAllocatorCacheSizeMB, long resourceAllocatorCacheMaxAge, boolean disableHandleUseAfterFreeCheck, int preferredShaderLanguage, - boolean forceGLES2Context); + boolean forceGLES2Context, boolean assertNativeWindowIsValid); private static native void nSetBuilderFeatureLevel(long nativeBuilder, int ordinal); private static native void nSetBuilderSharedContext(long nativeBuilder, long sharedContext); private static native void nSetBuilderPaused(long nativeBuilder, boolean paused); diff --git a/android/gradle.properties b/android/gradle.properties index 19e07a87034..9b015765b8f 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.google.android.filament -VERSION_NAME=1.55.0 +VERSION_NAME=1.55.1 POM_DESCRIPTION=Real-time physically based rendering engine for Android. diff --git a/build.sh b/build.sh index ab6440f45de..8c5d94a96db 100755 --- a/build.sh +++ b/build.sh @@ -64,6 +64,9 @@ function print_help { echo " enabling debug paths in the backend from the build script. For example, make a" echo " systrace-enabled build without directly changing #defines. Remember to add -f when" echo " changing this option." + echo " -X osmesa_path" + echo " Indicates a path to a completed OSMesa build. OSMesa is used to create an offscreen GL" + echo " context for software rasterization" echo " -S type" echo " Enable stereoscopic rendering where type is one of [instanced|multiview]. This is only" echo " meant for building the samples." @@ -180,6 +183,8 @@ BACKEND_DEBUG_FLAG_OPTION="" STEREOSCOPIC_OPTION="" +OSMESA_OPTION="" + IOS_BUILD_SIMULATOR=false BUILD_UNIVERSAL_LIBRARIES=false @@ -240,6 +245,7 @@ function build_desktop_target { ${ASAN_UBSAN_OPTION} \ ${BACKEND_DEBUG_FLAG_OPTION} \ ${STEREOSCOPIC_OPTION} \ + ${OSMESA_OPTION} \ ${architectures} \ ../.. ln -sf "out/cmake-${lc_target}/compile_commands.json" \ @@ -796,7 +802,7 @@ function check_debug_release_build { pushd "$(dirname "$0")" > /dev/null -while getopts ":hacCfgijmp:q:uvslwedk:bx:S:" opt; do +while getopts ":hacCfgijmp:q:uvslwedk:bx:S:X:" opt; do case ${opt} in h) print_help @@ -950,6 +956,8 @@ while getopts ":hacCfgijmp:q:uvslwedk:bx:S:" opt; do exit 1 esac ;; + X) OSMESA_OPTION="-DFILAMENT_OSMESA_PATH=${OPTARG}" + ;; \?) echo "Invalid option: -${OPTARG}" >&2 echo "" diff --git a/filament/backend/CMakeLists.txt b/filament/backend/CMakeLists.txt index 9283f20f7a1..3878723f434 100644 --- a/filament/backend/CMakeLists.txt +++ b/filament/backend/CMakeLists.txt @@ -119,6 +119,8 @@ if (FILAMENT_SUPPORTS_OPENGL AND NOT FILAMENT_USE_EXTERNAL_GLES3) list(APPEND SRCS src/opengl/platforms/PlatformGLX.cpp) elseif (FILAMENT_SUPPORTS_EGL_ON_LINUX) list(APPEND SRCS src/opengl/platforms/PlatformEGLHeadless.cpp) + elseif (FILAMENT_SUPPORTS_OSMESA) + list(APPEND SRCS src/opengl/platforms/PlatformOSMesa.cpp) endif() elseif (WIN32) list(APPEND SRCS src/opengl/platforms/PlatformWGL.cpp) @@ -366,6 +368,15 @@ set(LINUX_LINKER_OPTIMIZATION_FLAGS -Wl,--exclude-libs,bluegl ) +if (LINUX AND FILAMENT_SUPPORTS_OSMESA) + set(OSMESA_COMPILE_FLAGS + -I${FILAMENT_OSMESA_PATH}/include/GL) + set(OSMESA_LINKER_FLAGS + -Wl,-L${FILAMENT_OSMESA_PATH}/lib/x86_64-linux-gnu/ + -lOSMesa + ) +endif() + if (MSVC) set(FILAMENT_WARNINGS /W3) else() @@ -386,6 +397,7 @@ endif() target_compile_options(${TARGET} PRIVATE ${FILAMENT_WARNINGS} + ${OSMESA_COMPILE_FLAGS} $<$:${OPTIMIZATION_FLAGS}> $<$,$>:${DARWIN_OPTIMIZATION_FLAGS}> ) @@ -395,6 +407,7 @@ if (FILAMENT_SUPPORTS_METAL) endif() target_link_libraries(${TARGET} PRIVATE + ${OSMESA_LINKER_FLAGS} $<$,$>:${LINUX_LINKER_OPTIMIZATION_FLAGS}> ) @@ -429,6 +442,7 @@ if (APPLE OR LINUX) test/test_StencilBuffer.cpp test/test_Scissor.cpp test/test_MipLevels.cpp + test/test_Handles.cpp ) set(BACKEND_TEST_LIBS backend diff --git a/filament/backend/include/backend/DriverEnums.h b/filament/backend/include/backend/DriverEnums.h index 51926ad2def..191e7ac2262 100644 --- a/filament/backend/include/backend/DriverEnums.h +++ b/filament/backend/include/backend/DriverEnums.h @@ -765,7 +765,8 @@ enum class TextureUsage : uint16_t { BLIT_SRC = 0x0040, //!< Texture can be used the source of a blit() BLIT_DST = 0x0080, //!< Texture can be used the destination of a blit() PROTECTED = 0x0100, //!< Texture can be used for protected content - DEFAULT = UPLOADABLE | SAMPLEABLE //!< Default texture usage + DEFAULT = UPLOADABLE | SAMPLEABLE, //!< Default texture usage + ALL_ATTACHMENTS = COLOR_ATTACHMENT | DEPTH_ATTACHMENT | STENCIL_ATTACHMENT | SUBPASS_INPUT, //!< Mask of all attachments }; inline const char* stringify(TextureUsage usage) { diff --git a/filament/backend/include/backend/Handle.h b/filament/backend/include/backend/Handle.h index 2cf52244149..d5e88206b8f 100644 --- a/filament/backend/include/backend/Handle.h +++ b/filament/backend/include/backend/Handle.h @@ -23,6 +23,7 @@ #include #include // FIXME: STL headers are not allowed in public headers +#include #include @@ -106,8 +107,18 @@ struct Handle : public HandleBase { Handle(Handle const& rhs) noexcept = default; Handle(Handle&& rhs) noexcept = default; - Handle& operator=(Handle const& rhs) noexcept = default; - Handle& operator=(Handle&& rhs) noexcept = default; + // Explicitly redefine copy/move assignment operators rather than just using default here. + // Because it doesn't make a call to the parent's method automatically during the std::move + // function call(https://en.cppreference.com/w/cpp/algorithm/move) in certain compilers like + // NDK 25.1.8937393 and below (see b/371980551) + Handle& operator=(Handle const& rhs) noexcept { + HandleBase::operator=(rhs); + return *this; + } + Handle& operator=(Handle&& rhs) noexcept { + HandleBase::operator=(std::move(rhs)); + return *this; + } explicit Handle(HandleId id) noexcept : HandleBase(id) { } diff --git a/filament/backend/include/backend/Platform.h b/filament/backend/include/backend/Platform.h index b8befb4cee6..f9adc51a623 100644 --- a/filament/backend/include/backend/Platform.h +++ b/filament/backend/include/backend/Platform.h @@ -68,13 +68,6 @@ class UTILS_PUBLIC Platform { */ 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; - size_t metalUploadBufferSizeBytes = 512 * 1024; /** @@ -98,6 +91,13 @@ class UTILS_PUBLIC Platform { * Sets the technique for stereoscopic rendering. */ StereoscopicType stereoscopicType = StereoscopicType::NONE; + + /** + * Assert the native window associated to a SwapChain is valid when calling makeCurrent(). + * This is only supported for: + * - PlatformEGLAndroid + */ + bool assertNativeWindowIsValid = false; }; Platform() noexcept; diff --git a/filament/backend/include/backend/platforms/PlatformEGLAndroid.h b/filament/backend/include/backend/platforms/PlatformEGLAndroid.h index c3cc7da89d8..bc7d0c1f2f2 100644 --- a/filament/backend/include/backend/platforms/PlatformEGLAndroid.h +++ b/filament/backend/include/backend/platforms/PlatformEGLAndroid.h @@ -89,6 +89,11 @@ class PlatformEGLAndroid : public PlatformEGL { */ AcquiredImage transformAcquiredImage(AcquiredImage source) noexcept override; +protected: + bool makeCurrent(ContextType type, + SwapChain* drawSwapChain, + SwapChain* readSwapChain) noexcept override; + private: struct InitializeJvmForPerformanceManagerIfNeeded { InitializeJvmForPerformanceManagerIfNeeded(); @@ -102,6 +107,10 @@ class PlatformEGLAndroid : public PlatformEGL { using clock = std::chrono::high_resolution_clock; clock::time_point mStartTimeOfActualWork; + + void* mNativeWindowLib = nullptr; + int32_t (*ANativeWindow_getBuffersDefaultDataSpace)(ANativeWindow* window) = nullptr; + bool mAssertNativeWindowIsValid = false; }; } // namespace filament::backend diff --git a/filament/backend/include/backend/platforms/PlatformOSMesa.h b/filament/backend/include/backend/platforms/PlatformOSMesa.h new file mode 100644 index 00000000000..bcb82195a4c --- /dev/null +++ b/filament/backend/include/backend/platforms/PlatformOSMesa.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 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_FILAMENT_BACKEND_OPENGL_OPENGL_PLATFORM_OSMESA_H +#define TNT_FILAMENT_BACKEND_OPENGL_OPENGL_PLATFORM_OSMESA_H + +#include + +#include "bluegl/BlueGL.h" + +#include "osmesa.h" + +#include +#include + +namespace filament::backend { + +/** + * A concrete implementation of OpenGLPlatform that uses OSMesa, which is an offscreen + * context that can be used in conjunction with Mesa for software rasterization. + * See https://docs.mesa3d.org/osmesa.html for more information. + */ +class PlatformOSMesa : public OpenGLPlatform { +protected: + // -------------------------------------------------------------------------------------------- + // Platform Interface + + Driver* createDriver(void* sharedGLContext, const DriverConfig& driverConfig) noexcept override; + + int getOSVersion() const noexcept final override { return 0; } + + // -------------------------------------------------------------------------------------------- + // OpenGLPlatform Interface + + void terminate() noexcept override; + + SwapChain* createSwapChain(void* nativewindow, uint64_t flags) noexcept override; + SwapChain* createSwapChain(uint32_t width, uint32_t height, uint64_t flags) noexcept override; + void destroySwapChain(SwapChain* swapChain) noexcept override; + bool makeCurrent(ContextType type, SwapChain* drawSwapChain, + SwapChain* readSwapChain) noexcept override; + void commit(SwapChain* swapChain) noexcept override; + +private: + OSMesaContext mContext; +}; + +} // namespace filament::backend + +#endif // TNT_FILAMENT_BACKEND_OPENGL_OPENGL_PLATFORM_OSMESA_H diff --git a/filament/backend/include/private/backend/DriverAPI.inc b/filament/backend/include/private/backend/DriverAPI.inc index 20297b61792..66556b0f41f 100644 --- a/filament/backend/include/private/backend/DriverAPI.inc +++ b/filament/backend/include/private/backend/DriverAPI.inc @@ -139,7 +139,8 @@ DECL_DRIVER_API_N(beginFrame, DECL_DRIVER_API_N(setFrameScheduledCallback, backend::SwapChainHandle, sch, backend::CallbackHandler*, handler, - backend::FrameScheduledCallback&&, callback) + backend::FrameScheduledCallback&&, callback, + uint64_t, flags) DECL_DRIVER_API_N(setFrameCompletedCallback, backend::SwapChainHandle, sch, diff --git a/filament/backend/include/private/backend/HandleAllocator.h b/filament/backend/include/private/backend/HandleAllocator.h index 8eaa9481d62..93333363022 100644 --- a/filament/backend/include/private/backend/HandleAllocator.h +++ b/filament/backend/include/private/backend/HandleAllocator.h @@ -169,7 +169,7 @@ class HandleAllocator { auto [p, tag] = handleToPointer(handle.getId()); if (isPoolHandle(handle.getId())) { - // check for use after free + // check for pool handle use-after-free if (UTILS_UNLIKELY(!mUseAfterFreeCheckDisabled)) { uint8_t const age = (tag & HANDLE_AGE_MASK) >> HANDLE_AGE_SHIFT; auto const pNode = static_cast(p); @@ -179,6 +179,22 @@ class HandleAllocator { << "use-after-free of Handle with id=" << handle.getId() << ", tag=" << getHandleTag(handle.getId()).c_str_safe(); } + } else { + // check for heap handle use-after-free + if (UTILS_UNLIKELY(!mUseAfterFreeCheckDisabled)) { + uint8_t const index = (handle.getId() & HANDLE_INDEX_MASK); + // if we've already handed out this handle index before, it's definitely a + // use-after-free, otherwise it's probably just a corrupted handle + if (index < mId) { + FILAMENT_CHECK_POSTCONDITION(p != nullptr) + << "use-after-free of heap Handle with id=" << handle.getId() + << ", tag=" << getHandleTag(handle.getId()).c_str_safe(); + } else { + FILAMENT_CHECK_POSTCONDITION(p != nullptr) + << "corrupted heap Handle with id=" << handle.getId() + << ", tag=" << getHandleTag(handle.getId()).c_str_safe(); + } + } } return static_cast(p); @@ -186,14 +202,18 @@ class HandleAllocator { template bool is_valid(Handle& handle) { - if (handle && isPoolHandle(handle.getId())) { - auto [p, tag] = handleToPointer(handle.getId()); + if (!handle) { + // null handles are invalid + return false; + } + auto [p, tag] = handleToPointer(handle.getId()); + if (isPoolHandle(handle.getId())) { uint8_t const age = (tag & HANDLE_AGE_MASK) >> HANDLE_AGE_SHIFT; auto const pNode = static_cast(p); uint8_t const expectedAge = pNode[-1].age; return expectedAge == age; } - return true; + return p != nullptr; } template diff --git a/filament/backend/src/PlatformFactory.cpp b/filament/backend/src/PlatformFactory.cpp index a97eec8f4f5..86be6d231e0 100644 --- a/filament/backend/src/PlatformFactory.cpp +++ b/filament/backend/src/PlatformFactory.cpp @@ -41,6 +41,10 @@ #if defined(FILAMENT_SUPPORTS_OPENGL) && !defined(FILAMENT_USE_EXTERNAL_GLES3) #include "backend/platforms/PlatformEGLHeadless.h" #endif + #elif defined(FILAMENT_SUPPORTS_OSMESA) + #if defined(FILAMENT_SUPPORTS_OPENGL) && !defined(FILAMENT_USE_EXTERNAL_GLES3) + #include "backend/platforms/PlatformOSMesa.h" + #endif #endif #elif defined(WIN32) #if defined(FILAMENT_SUPPORTS_OPENGL) && !defined(FILAMENT_USE_EXTERNAL_GLES3) @@ -124,6 +128,8 @@ Platform* PlatformFactory::create(Backend* backend) noexcept { return new PlatformGLX(); #elif defined(FILAMENT_SUPPORTS_EGL_ON_LINUX) return new PlatformEGLHeadless(); + #elif defined(FILAMENT_SUPPORTS_OSMESA) + return new PlatformOSMesa(); #else return nullptr; #endif diff --git a/filament/backend/src/metal/MetalBuffer.h b/filament/backend/src/metal/MetalBuffer.h index 4fc725bb046..f6fa90c1990 100644 --- a/filament/backend/src/metal/MetalBuffer.h +++ b/filament/backend/src/metal/MetalBuffer.h @@ -174,8 +174,10 @@ class MetalBuffer { * Update the buffer with data inside src. Potentially allocates a new buffer allocation to hold * the bytes which will be released when the current frame is finished. */ - void copyIntoBuffer(void* src, size_t size, size_t byteOffset); - void copyIntoBufferUnsynchronized(void* src, size_t size, size_t byteOffset); + using TagResolver = utils::Invocable; + void copyIntoBuffer(void* src, size_t size, size_t byteOffset, TagResolver&& getHandleTag); + void copyIntoBufferUnsynchronized( + void* src, size_t size, size_t byteOffset, TagResolver&& getHandleTag); /** * Denotes that this buffer is used for a draw call ensuring that its allocation remains valid @@ -214,8 +216,10 @@ class MetalBuffer { BUMP_ALLOCATOR, }; - void uploadWithPoolBuffer(void* src, size_t size, size_t byteOffset) const; - void uploadWithBumpAllocator(void* src, size_t size, size_t byteOffset) const; + void uploadWithPoolBuffer( + void* src, size_t size, size_t byteOffset, TagResolver&& getHandleTag) const; + void uploadWithBumpAllocator( + void* src, size_t size, size_t byteOffset, TagResolver&& getHandleTag) const; UploadStrategy mUploadStrategy; TrackedMetalBuffer mBuffer; diff --git a/filament/backend/src/metal/MetalBuffer.mm b/filament/backend/src/metal/MetalBuffer.mm index d6f68c33af0..6347ab70c6f 100644 --- a/filament/backend/src/metal/MetalBuffer.mm +++ b/filament/backend/src/metal/MetalBuffer.mm @@ -66,15 +66,20 @@ } } -void MetalBuffer::copyIntoBuffer(void* src, size_t size, size_t byteOffset) { +void MetalBuffer::copyIntoBuffer( + void* src, size_t size, size_t byteOffset, TagResolver&& getHandleTag) { if (size <= 0) { return; } + + FILAMENT_CHECK_PRECONDITION(src) + << "copyIntoBuffer called with a null src, tag=" << getHandleTag(); FILAMENT_CHECK_PRECONDITION(size + byteOffset <= mBufferSize) << "Attempting to copy " << size << " bytes into a buffer of size " << mBufferSize - << " at offset " << byteOffset; + << " at offset " << byteOffset << ", tag=" << getHandleTag(); // The copy blit requires that byteOffset be a multiple of 4. - FILAMENT_CHECK_PRECONDITION(!(byteOffset & 0x3)) << "byteOffset must be a multiple of 4"; + FILAMENT_CHECK_PRECONDITION(!(byteOffset & 0x3)) + << "byteOffset must be a multiple of 4, tag=" << getHandleTag(); // If we have a cpu buffer, we can directly copy into it. if (mCpuBuffer) { @@ -84,17 +89,18 @@ switch (mUploadStrategy) { case UploadStrategy::BUMP_ALLOCATOR: - uploadWithBumpAllocator(src, size, byteOffset); + uploadWithBumpAllocator(src, size, byteOffset, std::move(getHandleTag)); break; case UploadStrategy::POOL: - uploadWithPoolBuffer(src, size, byteOffset); + uploadWithPoolBuffer(src, size, byteOffset, std::move(getHandleTag)); break; } } -void MetalBuffer::copyIntoBufferUnsynchronized(void* src, size_t size, size_t byteOffset) { +void MetalBuffer::copyIntoBufferUnsynchronized( + void* src, size_t size, size_t byteOffset, TagResolver&& getHandleTag) { // TODO: implement the unsynchronized version - copyIntoBuffer(src, size, byteOffset); + copyIntoBuffer(src, size, byteOffset, std::move(getHandleTag)); } id MetalBuffer::getGpuBufferForDraw() noexcept { @@ -200,9 +206,13 @@ } } -void MetalBuffer::uploadWithPoolBuffer(void* src, size_t size, size_t byteOffset) const { +void MetalBuffer::uploadWithPoolBuffer( + void* src, size_t size, size_t byteOffset, TagResolver&& getHandleTag) const { MetalBufferPool* bufferPool = mContext.bufferPool; const MetalBufferPoolEntry* const staging = bufferPool->acquireBuffer(size); + FILAMENT_CHECK_POSTCONDITION(staging) + << "uploadWithPoolbuffer unable to acquire staging buffer of size " << size + << ", tag=" << getHandleTag(); memcpy(staging->buffer.get().contents, src, size); // Encode a blit from the staging buffer into the private GPU buffer. @@ -220,10 +230,18 @@ }]; } -void MetalBuffer::uploadWithBumpAllocator(void* src, size_t size, size_t byteOffset) const { +void MetalBuffer::uploadWithBumpAllocator( + void* src, size_t size, size_t byteOffset, TagResolver&& getHandleTag) const { MetalBumpAllocator& allocator = *mContext.bumpAllocator; auto [buffer, offset] = allocator.allocateStagingArea(size); - memcpy(static_cast(buffer.contents) + offset, src, size); + FILAMENT_CHECK_POSTCONDITION(buffer) + << "uploadWithBumpAllocator unable to acquire staging area of size " << size + << ", tag=" << getHandleTag(); + void* const contents = buffer.contents; + FILAMENT_CHECK_POSTCONDITION(contents) + << "uploadWithBumpAllocator unable to acquire pointer to staging area, size " << size + << ", tag=" << getHandleTag(); + memcpy(static_cast(contents) + offset, src, size); // Encode a blit from the staging buffer into the private GPU buffer. id cmdBuffer = getPendingCommandBuffer(&mContext); diff --git a/filament/backend/src/metal/MetalContext.h b/filament/backend/src/metal/MetalContext.h index 3f6cbaa1a95..ef1175fa688 100644 --- a/filament/backend/src/metal/MetalContext.h +++ b/filament/backend/src/metal/MetalContext.h @@ -118,9 +118,6 @@ class MetalDynamicOffsets { }; struct MetalContext { - explicit MetalContext(size_t metalFreedTextureListSize) - : texturesToDestroy(metalFreedTextureListSize) {} - MetalDriver* driver; id device = nullptr; id commandQueue = nullptr; @@ -187,14 +184,6 @@ struct MetalContext { // Keeps track of all alive textures. 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; MetalBumpAllocator* bumpAllocator; diff --git a/filament/backend/src/metal/MetalDriver.mm b/filament/backend/src/metal/MetalDriver.mm index 3c4b28d8596..fb17e99c76b 100644 --- a/filament/backend/src/metal/MetalDriver.mm +++ b/filament/backend/src/metal/MetalDriver.mm @@ -19,6 +19,8 @@ #include "CommandStreamDispatcher.h" #include "metal/MetalDriver.h" +#include + #include "MetalBlitter.h" #include "MetalBufferPool.h" #include "MetalContext.h" @@ -38,6 +40,7 @@ #include #include #include +#include #include @@ -105,13 +108,13 @@ return ConcreteDispatcher::make(); } -MetalDriver::MetalDriver(MetalPlatform* platform, const Platform::DriverConfig& driverConfig) noexcept - : mPlatform(*platform), - mContext(new MetalContext(driverConfig.textureUseAfterFreePoolSize)), - mHandleAllocator("Handles", - driverConfig.handleArenaSize, - driverConfig.disableHandleUseAfterFreeCheck), - mStereoscopicType(driverConfig.stereoscopicType) { +MetalDriver::MetalDriver( + MetalPlatform* platform, const Platform::DriverConfig& driverConfig) noexcept + : mPlatform(*platform), + mContext(new MetalContext), + mHandleAllocator( + "Handles", driverConfig.handleArenaSize, driverConfig.disableHandleUseAfterFreeCheck), + mStereoscopicType(driverConfig.stereoscopicType) { mContext->driver = this; TrackedMetalBuffer::setPlatform(platform); @@ -256,10 +259,14 @@ } } -void MetalDriver::setFrameScheduledCallback( - Handle sch, CallbackHandler* handler, FrameScheduledCallback&& callback) { +void MetalDriver::setFrameScheduledCallback(Handle sch, CallbackHandler* handler, + FrameScheduledCallback&& callback, uint64_t flags) { + // Turn off the CALLBACK_DEFAULT_USE_METAL_COMPLETION_HANDLER flag if a custom handler is provided. + if (handler) { + flags &= ~SwapChain::CALLBACK_DEFAULT_USE_METAL_COMPLETION_HANDLER; + } auto* swapChain = handle_cast(sch); - swapChain->setFrameScheduledCallback(handler, std::move(callback)); + swapChain->setFrameScheduledCallback(handler, std::move(callback), flags); } void MetalDriver::setFrameCompletedCallback( @@ -802,15 +809,7 @@ 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()); - } + destruct_handle(th); } void MetalDriver::destroyRenderTarget(Handle rth) { @@ -860,12 +859,6 @@ } 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(); @@ -1089,24 +1082,34 @@ void MetalDriver::updateIndexBuffer(Handle ibh, BufferDescriptor&& data, uint32_t byteOffset) { + FILAMENT_CHECK_PRECONDITION(data.buffer) + << "updateIndexBuffer called with a null buffer."; auto* ib = handle_cast(ibh); - ib->buffer.copyIntoBuffer(data.buffer, data.size, byteOffset); + ib->buffer.copyIntoBuffer(data.buffer, data.size, byteOffset, + [&]() { return mHandleAllocator.getHandleTag(ibh.getId()).c_str_safe(); }); scheduleDestroy(std::move(data)); } void MetalDriver::updateBufferObject(Handle boh, BufferDescriptor&& data, uint32_t byteOffset) { FILAMENT_CHECK_PRECONDITION(!isInRenderPass(mContext)) - << "updateBufferObject must be called outside of a render pass."; + << "updateBufferObject must be called outside of a render pass. tag=" + << mHandleAllocator.getHandleTag(boh.getId()).c_str_safe(); + FILAMENT_CHECK_PRECONDITION(data.buffer) + << "updateBufferObject called with a null buffer. tag=" + << mHandleAllocator.getHandleTag(boh.getId()).c_str_safe(); auto* bo = handle_cast(boh); - bo->updateBuffer(data.buffer, data.size, byteOffset); + + bo->updateBuffer(data.buffer, data.size, byteOffset, + [&]() { return mHandleAllocator.getHandleTag(boh.getId()).c_str_safe(); }); scheduleDestroy(std::move(data)); } void MetalDriver::updateBufferObjectUnsynchronized(Handle boh, BufferDescriptor&& data, uint32_t byteOffset) { auto* bo = handle_cast(boh); - bo->updateBufferUnsynchronized(data.buffer, data.size, byteOffset); + bo->updateBufferUnsynchronized(data.buffer, data.size, byteOffset, + [&]() { return mHandleAllocator.getHandleTag(boh.getId()).c_str_safe(); }); scheduleDestroy(std::move(data)); } diff --git a/filament/backend/src/metal/MetalHandles.h b/filament/backend/src/metal/MetalHandles.h index b7c61e4fa94..0c02b213edb 100644 --- a/filament/backend/src/metal/MetalHandles.h +++ b/filament/backend/src/metal/MetalHandles.h @@ -74,7 +74,8 @@ class MetalSwapChain : public HwSwapChain { void releaseDrawable(); - void setFrameScheduledCallback(CallbackHandler* handler, FrameScheduledCallback&& callback); + void setFrameScheduledCallback( + CallbackHandler* handler, FrameScheduledCallback&& callback, uint64_t flags); void setFrameCompletedCallback( CallbackHandler* handler, utils::Invocable&& callback); @@ -123,6 +124,7 @@ class MetalSwapChain : public HwSwapChain { struct { CallbackHandler* handler = nullptr; std::shared_ptr callback = nullptr; + uint64_t flags = 0; } frameScheduled; struct { @@ -133,11 +135,15 @@ class MetalSwapChain : public HwSwapChain { class MetalBufferObject : public HwBufferObject { public: + + using TagResolver = MetalBuffer::TagResolver; + MetalBufferObject(MetalContext& context, BufferObjectBinding bindingType, BufferUsage usage, uint32_t byteCount); - void updateBuffer(void* data, size_t size, uint32_t byteOffset); - void updateBufferUnsynchronized(void* data, size_t size, uint32_t byteOffset); + void updateBuffer(void* data, size_t size, uint32_t byteOffset, TagResolver&& getHandleTag); + void updateBufferUnsynchronized( + void* data, size_t size, uint32_t byteOffset, TagResolver&& getHandleTag); MetalBuffer* getBuffer() { return &buffer; } private: @@ -265,26 +271,6 @@ 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; @@ -301,8 +287,6 @@ class MetalTexture : public HwTexture { // Filament swizzling only affects texture reads, so this should not be used when the texture is // bound as a render target attachment. id swizzledTextureView = nil; - - bool terminated = false; }; class MetalRenderTarget : public HwRenderTarget { diff --git a/filament/backend/src/metal/MetalHandles.mm b/filament/backend/src/metal/MetalHandles.mm index 71be41842bd..e0d40f5cdc6 100644 --- a/filament/backend/src/metal/MetalHandles.mm +++ b/filament/backend/src/metal/MetalHandles.mm @@ -229,9 +229,10 @@ static inline MTLTextureUsage getMetalTextureUsage(TextureUsage usage) { } void MetalSwapChain::setFrameScheduledCallback( - CallbackHandler* handler, FrameScheduledCallback&& callback) { + CallbackHandler* handler, FrameScheduledCallback&& callback, uint64_t flags) { frameScheduled.handler = handler; frameScheduled.callback = std::make_shared(std::move(callback)); + frameScheduled.flags = flags; } void MetalSwapChain::setFrameCompletedCallback( @@ -260,10 +261,10 @@ static inline MTLTextureUsage getMetalTextureUsage(TextureUsage usage) { PresentDrawableData& operator=(const PresentDrawableData&) = delete; static PresentDrawableData* create(id drawable, - std::shared_ptr drawableMutex, MetalDriver* driver) { + std::shared_ptr drawableMutex, MetalDriver* driver, uint64_t flags) { assert_invariant(drawableMutex); assert_invariant(driver); - return new PresentDrawableData(drawable, drawableMutex, driver); + return new PresentDrawableData(drawable, drawableMutex, driver, flags); } static void maybePresentAndDestroyAsync(PresentDrawableData* that, bool shouldPresent) { @@ -271,16 +272,23 @@ static void maybePresentAndDestroyAsync(PresentDrawableData* that, bool shouldPr [that->mDrawable present]; } - // mDrawable is acquired on the driver thread. Typically, we would release this object on - // the same thread, but after receiving consistent crash reports from within - // [CAMetalDrawable dealloc], we suspect this object requires releasing on the main thread. - dispatch_async(dispatch_get_main_queue(), ^{ cleanupAndDestroy(that); }); + if (that->mFlags & SwapChain::CALLBACK_DEFAULT_USE_METAL_COMPLETION_HANDLER) { + cleanupAndDestroy(that); + } else { + // mDrawable is acquired on the driver thread. Typically, we would release this object + // on the same thread, but after receiving consistent crash reports from within + // [CAMetalDrawable dealloc], we suspect this object requires releasing on the main + // thread. + dispatch_async(dispatch_get_main_queue(), ^{ + cleanupAndDestroy(that); + }); + } } private: PresentDrawableData(id drawable, std::shared_ptr drawableMutex, - MetalDriver* driver) - : mDrawable(drawable), mDrawableMutex(drawableMutex), mDriver(driver) {} + MetalDriver* driver, uint64_t flags) + : mDrawable(drawable), mDrawableMutex(drawableMutex), mDriver(driver), mFlags(flags) {} static void cleanupAndDestroy(PresentDrawableData *that) { if (that->mDrawable) { @@ -295,6 +303,7 @@ static void cleanupAndDestroy(PresentDrawableData *that) { id mDrawable; std::shared_ptr mDrawableMutex; MetalDriver* mDriver = nullptr; + uint64_t mFlags = 0; }; void presentDrawable(bool presentFrame, void* user) { @@ -311,8 +320,8 @@ void presentDrawable(bool presentFrame, void* user) { struct Callback { Callback(std::shared_ptr callback, id drawable, - std::shared_ptr drawableMutex, MetalDriver* driver) - : f(callback), data(PresentDrawableData::create(drawable, drawableMutex, driver)) {} + std::shared_ptr drawableMutex, MetalDriver* driver, uint64_t flags) + : f(callback), data(PresentDrawableData::create(drawable, drawableMutex, driver, flags)) {} std::shared_ptr f; // PresentDrawableData* is destroyed by maybePresentAndDestroyAsync() later. std::unique_ptr data; @@ -327,14 +336,19 @@ static void func(void* user) { // This callback pointer will be captured by the block. Even if the scheduled handler is never // called, the unique_ptr will still ensure we don't leak memory. + uint64_t const flags = frameScheduled.flags; __block auto callback = std::make_unique( - frameScheduled.callback, drawable, layerDrawableMutex, context.driver); + frameScheduled.callback, drawable, layerDrawableMutex, context.driver, flags); backend::CallbackHandler* handler = frameScheduled.handler; MetalDriver* driver = context.driver; [getPendingCommandBuffer(&context) addScheduledHandler:^(id cb) { Callback* user = callback.release(); - driver->scheduleCallback(handler, user, &Callback::func); + if (flags & SwapChain::CALLBACK_DEFAULT_USE_METAL_COMPLETION_HANDLER) { + Callback::func(user); + } else { + driver->scheduleCallback(handler, user, &Callback::func); + } }]; } @@ -369,12 +383,14 @@ static void func(void* user) { BufferUsage usage, uint32_t byteCount) : HwBufferObject(byteCount), buffer(context, bindingType, usage, byteCount) {} -void MetalBufferObject::updateBuffer(void* data, size_t size, uint32_t byteOffset) { - buffer.copyIntoBuffer(data, size, byteOffset); +void MetalBufferObject::updateBuffer( + void* data, size_t size, uint32_t byteOffset, TagResolver&& getHandleTag) { + buffer.copyIntoBuffer(data, size, byteOffset, std::move(getHandleTag)); } -void MetalBufferObject::updateBufferUnsynchronized(void* data, size_t size, uint32_t byteOffset) { - buffer.copyIntoBufferUnsynchronized(data, size, byteOffset); +void MetalBufferObject::updateBufferUnsynchronized( + void* data, size_t size, uint32_t byteOffset, TagResolver&& getHandleTag) { + buffer.copyIntoBufferUnsynchronized(data, size, byteOffset, std::move(getHandleTag)); } MetalVertexBufferInfo::MetalVertexBufferInfo(MetalContext& context, uint8_t bufferCount, @@ -634,14 +650,6 @@ static void func(void* user) { texture = externalImage->getMtlTexture(); } -void MetalTexture::terminate() noexcept { - texture = nil; - swizzledTextureView = nil; - msaaSidecar = nil; - externalImage = nullptr; - terminated = true; -} - id MetalTexture::getMtlTextureForRead() const noexcept { return swizzledTextureView ? swizzledTextureView : texture; } diff --git a/filament/backend/src/noop/NoopDriver.cpp b/filament/backend/src/noop/NoopDriver.cpp index 8a718288f52..afc5ac808df 100644 --- a/filament/backend/src/noop/NoopDriver.cpp +++ b/filament/backend/src/noop/NoopDriver.cpp @@ -54,7 +54,7 @@ void NoopDriver::beginFrame(int64_t monotonic_clock_ns, } void NoopDriver::setFrameScheduledCallback(Handle sch, - CallbackHandler* handler, FrameScheduledCallback&& callback) { + CallbackHandler* handler, FrameScheduledCallback&& callback, uint64_t flags) { } diff --git a/filament/backend/src/opengl/OpenGLDriver.cpp b/filament/backend/src/opengl/OpenGLDriver.cpp index 35da0a05191..a606d090870 100644 --- a/filament/backend/src/opengl/OpenGLDriver.cpp +++ b/filament/backend/src/opengl/OpenGLDriver.cpp @@ -3467,8 +3467,8 @@ void OpenGLDriver::beginFrame( } } -void OpenGLDriver::setFrameScheduledCallback(Handle sch, - CallbackHandler* handler, FrameScheduledCallback&& callback) { +void OpenGLDriver::setFrameScheduledCallback(Handle sch, CallbackHandler* handler, + FrameScheduledCallback&& callback, uint64_t flags) { DEBUG_MARKER() } diff --git a/filament/backend/src/opengl/platforms/PlatformEGL.cpp b/filament/backend/src/opengl/platforms/PlatformEGL.cpp index 0a8f638a73e..38b8b9d5049 100644 --- a/filament/backend/src/opengl/platforms/PlatformEGL.cpp +++ b/filament/backend/src/opengl/platforms/PlatformEGL.cpp @@ -553,6 +553,8 @@ void PlatformEGL::destroySwapChain(Platform::SwapChain* swapChain) noexcept { if (swapChain) { SwapChainEGL const* const sc = static_cast(swapChain); if (sc->sur != EGL_NO_SURFACE) { + // - if EGL_KHR_surfaceless_context is supported, mEGLDummySurface is EGL_NO_SURFACE. + // - this is actually a bit too aggressive, but it is a rare operation. egl.makeCurrent(mEGLDummySurface, mEGLDummySurface); eglDestroySurface(mEGLDisplay, sc->sur); delete sc; diff --git a/filament/backend/src/opengl/platforms/PlatformEGLAndroid.cpp b/filament/backend/src/opengl/platforms/PlatformEGLAndroid.cpp index 577b66fb315..07e4b10d36d 100644 --- a/filament/backend/src/opengl/platforms/PlatformEGLAndroid.cpp +++ b/filament/backend/src/opengl/platforms/PlatformEGLAndroid.cpp @@ -25,12 +25,14 @@ #include "ExternalStreamManagerAndroid.h" #include +#include #include #include #include #include +#include #include #include @@ -42,7 +44,9 @@ #include #include +#include +#include #include #include @@ -80,8 +84,6 @@ UTILS_PRIVATE PFNEGLGETFRAMETIMESTAMPSANDROIDPROC eglGetFrameTimestampsANDROID = } using namespace glext; -using EGLStream = Platform::Stream; - // --------------------------------------------------------------------------------------------- PlatformEGLAndroid::InitializeJvmForPerformanceManagerIfNeeded::InitializeJvmForPerformanceManagerIfNeeded() { @@ -101,38 +103,73 @@ PlatformEGLAndroid::PlatformEGLAndroid() noexcept mExternalStreamManager(ExternalStreamManagerAndroid::create()), mInitializeJvmForPerformanceManagerIfNeeded(), mPerformanceHintManager() { - - char scratch[PROP_VALUE_MAX + 1]; - int length = __system_property_get("ro.build.version.release", scratch); - int const androidVersion = length >= 0 ? atoi(scratch) : 1; - if (!androidVersion) { - mOSVersion = 1000; // if androidVersion is 0, it means "future" - } else { - length = __system_property_get("ro.build.version.sdk", scratch); - mOSVersion = length >= 0 ? atoi(scratch) : 1; + mOSVersion = android_get_device_api_level(); + if (mOSVersion < 0) { + mOSVersion = __ANDROID_API_FUTURE__; } - // This disables an ANGLE optimization on ARM, which turns out to be more costly for us - // see b/229017581 - // We need to do this before we create the GL context. - // An alternative solution is use a system property: - // __system_property_set( - // "debug.angle.feature_overrides_disabled", - // "preferSubmitAtFBOBoundary"); - // but that would outlive this process, so the environment variable is better. - // We also make sure to not update the variable if it already exists. - // There is no harm setting this if we're not on ANGLE or ARM. - setenv("ANGLE_FEATURE_OVERRIDES_DISABLED", "preferSubmitAtFBOBoundary", false); + mNativeWindowLib = dlopen("libnativewindow.so", RTLD_LOCAL | RTLD_NOW); + if (mNativeWindowLib) { + ANativeWindow_getBuffersDefaultDataSpace = + (int32_t(*)(ANativeWindow*))dlsym(mNativeWindowLib, + "ANativeWindow_getBuffersDefaultDataSpace"); + } } -PlatformEGLAndroid::~PlatformEGLAndroid() noexcept = default; - +PlatformEGLAndroid::~PlatformEGLAndroid() noexcept { + if (mNativeWindowLib) { + dlclose(mNativeWindowLib); + } +} void PlatformEGLAndroid::terminate() noexcept { ExternalStreamManagerAndroid::destroy(&mExternalStreamManager); PlatformEGL::terminate(); } +static constexpr const std::string_view kNativeWindowInvalidMsg = + "ANativeWindow is invalid. It probably has been destroyed. EGL surface = "; + +bool PlatformEGLAndroid::makeCurrent(ContextType type, + SwapChain* drawSwapChain, + SwapChain* readSwapChain) noexcept { + + // fast & safe path + if (UTILS_LIKELY(!mAssertNativeWindowIsValid)) { + return PlatformEGL::makeCurrent(type, drawSwapChain, readSwapChain); + } + + SwapChainEGL const* const dsc = static_cast(drawSwapChain); + if (ANativeWindow_getBuffersDefaultDataSpace) { + // anw can be nullptr if we're using a pbuffer surface + if (UTILS_LIKELY(dsc->nativeWindow)) { + // this a proxy of is_valid() + auto result = ANativeWindow_getBuffersDefaultDataSpace(dsc->nativeWindow); + FILAMENT_CHECK_POSTCONDITION(result >= 0) << kNativeWindowInvalidMsg << dsc->sur; + } + } else { + // If we don't have ANativeWindow_getBuffersDefaultDataSpace, we revert to using the + // private query() call. + // Shadow version if the real ANativeWindow, so we can access the query() hook. Query + // has existed since forever, probably Android 1.0. + struct NativeWindow { + // is valid query enum value + enum { IS_VALID = 17 }; + uint64_t pad[18]; + int (* query)(ANativeWindow const*, int, int*); + } const* pWindow = reinterpret_cast(dsc->nativeWindow); + int isValid = 0; + if (UTILS_LIKELY(pWindow->query)) { // just in case it's nullptr + int const err = pWindow->query(dsc->nativeWindow, NativeWindow::IS_VALID, &isValid); + if (UTILS_LIKELY(err >= 0)) { // in case the IS_VALID enum is not recognized + // query call succeeded + FILAMENT_CHECK_POSTCONDITION(isValid) << kNativeWindowInvalidMsg << dsc->sur; + } + } + } + return PlatformEGL::makeCurrent(type, drawSwapChain, readSwapChain); +} + void PlatformEGLAndroid::beginFrame( int64_t monotonic_clock_ns, int64_t refreshIntervalNs, @@ -189,6 +226,8 @@ Driver* PlatformEGLAndroid::createDriver(void* sharedContext, "eglGetFrameTimestampsANDROID"); } + mAssertNativeWindowIsValid = driverConfig.assertNativeWindowIsValid; + return driver; } diff --git a/filament/backend/src/opengl/platforms/PlatformOSMesa.cpp b/filament/backend/src/opengl/platforms/PlatformOSMesa.cpp new file mode 100644 index 00000000000..9a34577dc1c --- /dev/null +++ b/filament/backend/src/opengl/platforms/PlatformOSMesa.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2024 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 +#include + +#include + +namespace filament::backend { + +using namespace backend; + +namespace { + +using BackingType = GLfloat; +#define BACKING_GL_TYPE GL_FLOAT + +struct OSMesaSwapchain { + OSMesaSwapchain(uint32_t width, uint32_t height) + : width(width), + height(height), + buffer(new uint8_t[width * height * 4 * sizeof(BackingType)]) {} + + uint32_t width = 0; + uint32_t height = 0; + std::unique_ptr buffer; +}; + +} // anonymous namespace + +Driver* PlatformOSMesa::createDriver(void* const sharedGLContext, + const DriverConfig& driverConfig) noexcept { + FILAMENT_CHECK_PRECONDITION(sharedGLContext == nullptr) + << "shared GL context is not supported with PlatformOSMesa"; + mContext = OSMesaCreateContext(GL_RGBA, NULL); + + // We need to do a no-op makecurrent here so that the context will be in a correct state before + // any GL calls. + auto chain = createSwapChain(1, 1, 0); + makeCurrent(ContextType::UNPROTECTED, chain, nullptr); + destroySwapChain(chain); + + int result = bluegl::bind(); + FILAMENT_CHECK_POSTCONDITION(!result) << "Unable to load OpenGL entry points."; + + return OpenGLPlatform::createDefaultDriver(this, sharedGLContext, driverConfig); +} + +void PlatformOSMesa::terminate() noexcept { + OSMesaDestroyContext(mContext); + bluegl::unbind(); +} + +Platform::SwapChain* PlatformOSMesa::createSwapChain(void* nativeWindow, uint64_t flags) noexcept { + FILAMENT_CHECK_POSTCONDITION(false) << "Cannot create non-headless swapchain"; + return (SwapChain*) nativeWindow; +} + +Platform::SwapChain* PlatformOSMesa::createSwapChain(uint32_t width, uint32_t height, + uint64_t flags) noexcept { + OSMesaSwapchain* swapchain = new OSMesaSwapchain(width, height); + return (SwapChain*) swapchain; +} + +void PlatformOSMesa::destroySwapChain(Platform::SwapChain* swapChain) noexcept { + OSMesaSwapchain* impl = (OSMesaSwapchain*) swapChain; + delete impl; +} + +bool PlatformOSMesa::makeCurrent(ContextType type, SwapChain* drawSwapChain, + SwapChain* readSwapChain) noexcept { + OSMesaSwapchain* impl = (OSMesaSwapchain*) drawSwapChain; + + auto result = OSMesaMakeCurrent(mContext, (BackingType*) impl->buffer.get(), BACKING_GL_TYPE, + impl->width, impl->height); + FILAMENT_CHECK_POSTCONDITION(result) << "OSMesaMakeCurrent failed!"; + + return true; +} + +void PlatformOSMesa::commit(Platform::SwapChain* swapChain) noexcept { + // No-op since we are not scanning out to a display. +} + +} // namespace filament::backend diff --git a/filament/backend/src/vulkan/VulkanBlitter.cpp b/filament/backend/src/vulkan/VulkanBlitter.cpp index 7100b026352..db68b94b58b 100644 --- a/filament/backend/src/vulkan/VulkanBlitter.cpp +++ b/filament/backend/src/vulkan/VulkanBlitter.cpp @@ -66,10 +66,10 @@ inline void blitFast(VulkanCommandBuffer* commands, VkImageAspectFlags aspect, V 1, blitRegions, filter); if (oldSrcLayout == VulkanLayout::UNDEFINED) { - oldSrcLayout = imgutil::getDefaultLayout(src.texture->usage); + oldSrcLayout = src.texture->getDefaultLayout(); } if (oldDstLayout == VulkanLayout::UNDEFINED) { - oldDstLayout = imgutil::getDefaultLayout(dst.texture->usage); + oldDstLayout = dst.texture->getDefaultLayout(); } src.texture->transitionLayout(commands, srcRange, oldSrcLayout); dst.texture->transitionLayout(commands, dstRange, oldDstLayout); @@ -109,10 +109,10 @@ inline void resolveFast(VulkanCommandBuffer* commands, VkImageAspectFlags aspect 1, resolveRegions); if (oldSrcLayout == VulkanLayout::UNDEFINED) { - oldSrcLayout = imgutil::getDefaultLayout(src.texture->usage); + oldSrcLayout = src.texture->getDefaultLayout(); } if (oldDstLayout == VulkanLayout::UNDEFINED) { - oldDstLayout = imgutil::getDefaultLayout(dst.texture->usage); + oldDstLayout = dst.texture->getDefaultLayout(); } src.texture->transitionLayout(commands, srcRange, oldSrcLayout); dst.texture->transitionLayout(commands, dstRange, oldDstLayout); diff --git a/filament/backend/src/vulkan/VulkanContext.h b/filament/backend/src/vulkan/VulkanContext.h index a820087d7a2..e7a18141962 100644 --- a/filament/backend/src/vulkan/VulkanContext.h +++ b/filament/backend/src/vulkan/VulkanContext.h @@ -144,6 +144,10 @@ struct VulkanContext { return mPhysicalDeviceFeatures.shaderClipDistance == VK_TRUE; } + inline bool isLazilyAllocatedMemorySupported() const noexcept { + return mLazilyAllocatedMemorySupported; + } + private: VkPhysicalDeviceMemoryProperties mMemoryProperties = {}; VkPhysicalDeviceProperties mPhysicalDeviceProperties = {}; @@ -151,6 +155,7 @@ struct VulkanContext { bool mDebugMarkersSupported = false; bool mDebugUtilsSupported = false; bool mMultiviewEnabled = false; + bool mLazilyAllocatedMemorySupported = false; VkFormatList mDepthStencilFormats; VkFormatList mBlittableDepthStencilFormats; diff --git a/filament/backend/src/vulkan/VulkanDriver.cpp b/filament/backend/src/vulkan/VulkanDriver.cpp index debb6213359..d71a91d014d 100644 --- a/filament/backend/src/vulkan/VulkanDriver.cpp +++ b/filament/backend/src/vulkan/VulkanDriver.cpp @@ -88,28 +88,6 @@ VmaAllocator createAllocator(VkInstance instance, VkPhysicalDevice physicalDevic return allocator; } -VulkanTexture* createEmptyTexture(VkDevice device, VkPhysicalDevice physicalDevice, - VulkanContext const& context, VmaAllocator allocator, VulkanCommands* commands, - VulkanResourceAllocator* handleAllocator, VulkanStagePool& stagePool) { - VulkanTexture* emptyTexture = new VulkanTexture(device, physicalDevice, context, allocator, - commands, handleAllocator, SamplerType::SAMPLER_2D, 1, TextureFormat::RGBA8, 1, 1, 1, 1, - TextureUsage::DEFAULT | TextureUsage::COLOR_ATTACHMENT | TextureUsage::SUBPASS_INPUT, - stagePool, true /* heap allocated */); - uint32_t black = 0; - PixelBufferDescriptor pbd(&black, 4, PixelDataFormat::RGBA, PixelDataType::UBYTE); - emptyTexture->updateImage(pbd, 1, 1, 1, 0, 0, 0, 0); - return emptyTexture; -} - -VulkanBufferObject* createEmptyBufferObject(VmaAllocator allocator, VulkanStagePool& stagePool, - VulkanCommands* commands) { - VulkanBufferObject* obj = - new VulkanBufferObject(allocator, stagePool, 1, BufferObjectBinding::UNIFORM); - uint8_t byte = 0; - obj->buffer.loadFromCpu(commands->get().buffer(), &byte, 0, 1); - return obj; -} - #if FVK_ENABLED(FVK_DEBUG_VALIDATION) VKAPI_ATTR VkBool32 VKAPI_CALL debugReportCallback(VkDebugReportFlagsEXT flags, VkDebugReportObjectTypeEXT objectType, uint64_t object, size_t location, @@ -255,13 +233,6 @@ VulkanDriver::VulkanDriver(VulkanPlatform* platform, VulkanContext const& contex #endif mTimestamps = std::make_unique(mPlatform->getDevice()); - - mEmptyTexture = createEmptyTexture(mPlatform->getDevice(), mPlatform->getPhysicalDevice(), - mContext, mAllocator, &mCommands, &mResourceAllocator, mStagePool); - mEmptyBufferObject = createEmptyBufferObject(mAllocator, mStagePool, &mCommands); - - mDescriptorSetManager.setPlaceHolders(mSamplerCache.getSampler({}), mEmptyTexture, - mEmptyBufferObject); } VulkanDriver::~VulkanDriver() noexcept = default; @@ -326,9 +297,6 @@ void VulkanDriver::terminate() { // to those commands are no longer referenced. finish(0); - delete mEmptyBufferObject; - delete mEmptyTexture; - // Command buffers should come first since it might have commands depending on resources that // are about to be destroyed. mCommands.terminate(); @@ -399,8 +367,8 @@ void VulkanDriver::beginFrame(int64_t monotonic_clock_ns, FVK_SYSTRACE_END(); } -void VulkanDriver::setFrameScheduledCallback(Handle sch, - CallbackHandler* handler, FrameScheduledCallback&& callback) { +void VulkanDriver::setFrameScheduledCallback(Handle sch, CallbackHandler* handler, + FrameScheduledCallback&& callback, uint64_t flags) { } void VulkanDriver::setFrameCompletedCallback(Handle sch, @@ -436,14 +404,6 @@ void VulkanDriver::updateDescriptorSetTexture( SamplerParams params) { VulkanDescriptorSet* set = mResourceAllocator.handle_cast(dsh); VulkanTexture* texture = mResourceAllocator.handle_cast(th); - - // We need to make sure the initial layout transition has been completed before we can write - // the sampler descriptor. We flush and wait until the transition has been completed. - if (!texture->transitionReady()) { - mCommands.flush(); - mCommands.wait(); - } - VkSampler const vksampler = mSamplerCache.getSampler(params); mDescriptorSetManager.updateSampler(set, binding, texture, vksampler); } @@ -1315,12 +1275,14 @@ void VulkanDriver::beginRenderPass(Handle rth, const RenderPassP // Create the VkRenderPass or fetch it from cache. VulkanFboCache::RenderPassKey rpkey = { - .initialDepthLayout = currentDepthLayout, .depthFormat = depth.getFormat(), .clear = clearVal, .discardStart = discardStart, .discardEnd = discardEndVal, + .initialDepthLayout = currentDepthLayout, .samples = rt->getSamples(), + .needsResolveMask = 0, + .usesLazilyAllocatedMemory = 0, .subpassMask = uint8_t(params.subpassMask), .viewCount = renderTargetLayerCount, }; @@ -1329,8 +1291,14 @@ void VulkanDriver::beginRenderPass(Handle rth, const RenderPassP if (info.texture) { assert_invariant(info.layerCount == renderTargetLayerCount); rpkey.colorFormat[i] = info.getFormat(); - if (rpkey.samples > 1 && info.texture->samples == 1) { - rpkey.needsResolveMask |= (1 << i); + if (rpkey.samples > 1) { + const VulkanTexture* sidecar = info.texture->getSidecar(); + if (sidecar && sidecar->isTransientAttachment()) { + rpkey.usesLazilyAllocatedMemory |= (1 << i); + } + if (info.texture->samples == 1) { + rpkey.needsResolveMask |= (1 << i); + } } } else { rpkey.colorFormat[i] = VK_FORMAT_UNDEFINED; @@ -1382,8 +1350,6 @@ void VulkanDriver::beginRenderPass(Handle rth, const RenderPassP VulkanTexture* texture = attachment.texture; if (texture->samples == 1) { - mRenderPassFboInfo.hasColorResolve = true; - auto const& range = attachment.getSubresourceRange(); attachment.texture->transitionLayout(&commands, range, VulkanLayout::COLOR_ATTACHMENT); diff --git a/filament/backend/src/vulkan/VulkanDriver.h b/filament/backend/src/vulkan/VulkanDriver.h index d811bfebb2a..7dd95209342 100644 --- a/filament/backend/src/vulkan/VulkanDriver.h +++ b/filament/backend/src/vulkan/VulkanDriver.h @@ -115,10 +115,6 @@ class VulkanDriver final : public DriverBase { VulkanPlatform* mPlatform = nullptr; std::unique_ptr mTimestamps; - // Placeholder resources - VulkanTexture* mEmptyTexture; - VulkanBufferObject* mEmptyBufferObject; - VulkanSwapChain* mCurrentSwapChain = nullptr; VulkanRenderTarget* mDefaultRenderTarget = nullptr; VulkanRenderPass mCurrentRenderPass = {}; @@ -156,7 +152,6 @@ class VulkanDriver final : public DriverBase { struct { using AttachmentArray = CappedArray; AttachmentArray attachments; - bool hasColorResolve = false; } mRenderPassFboInfo = {}; bool const mIsSRGBSwapChainSupported; diff --git a/filament/backend/src/vulkan/VulkanFboCache.cpp b/filament/backend/src/vulkan/VulkanFboCache.cpp index 927dd4bcedf..980131025e5 100644 --- a/filament/backend/src/vulkan/VulkanFboCache.cpp +++ b/filament/backend/src/vulkan/VulkanFboCache.cpp @@ -41,6 +41,7 @@ bool VulkanFboCache::RenderPassEq::operator()(const RenderPassKey& k1, if (k1.discardEnd != k2.discardEnd) return false; if (k1.samples != k2.samples) return false; if (k1.needsResolveMask != k2.needsResolveMask) return false; + if (k1.usesLazilyAllocatedMemory != k2.usesLazilyAllocatedMemory) return false; if (k1.subpassMask != k2.subpassMask) return false; if (k1.viewCount != k2.viewCount) return false; return true; @@ -254,7 +255,7 @@ VkRenderPass VulkanFboCache::getRenderPass(RenderPassKey config) noexcept { .format = config.colorFormat[i], .samples = (VkSampleCountFlagBits) config.samples, .loadOp = clear ? kClear : (discard ? kDontCare : kKeep), - .storeOp = kEnableStore, + .storeOp = (config.usesLazilyAllocatedMemory & (1 << i)) ? kDisableStore : kEnableStore, .stencilLoadOp = kDontCare, .stencilStoreOp = kDisableStore, .initialLayout = imgutil::getVkLayout(VulkanLayout::COLOR_ATTACHMENT), diff --git a/filament/backend/src/vulkan/VulkanFboCache.h b/filament/backend/src/vulkan/VulkanFboCache.h index 3335db7c1c9..2b186106f68 100644 --- a/filament/backend/src/vulkan/VulkanFboCache.h +++ b/filament/backend/src/vulkan/VulkanFboCache.h @@ -42,18 +42,19 @@ class VulkanFboCache { // RenderPassKey is a small POD representing the immutable state that is used to construct // a VkRenderPass. It is hashed and used as a lookup key. struct alignas(8) RenderPassKey { - VulkanLayout initialDepthLayout; - uint8_t padding[3] = {}; - VkFormat colorFormat[MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT]; // 32 bytes VkFormat depthFormat; // 4 bytes TargetBufferFlags clear; // 4 bytes TargetBufferFlags discardStart; // 4 bytes TargetBufferFlags discardEnd; // 4 bytes + + VulkanLayout initialDepthLayout; // 1 byte uint8_t samples; // 1 byte uint8_t needsResolveMask; // 1 byte + uint8_t usesLazilyAllocatedMemory; // 1 byte uint8_t subpassMask; // 1 byte uint8_t viewCount; // 1 byte + uint8_t padding[2]; }; struct RenderPassVal { VkRenderPass handle; diff --git a/filament/backend/src/vulkan/VulkanHandles.cpp b/filament/backend/src/vulkan/VulkanHandles.cpp index e2adea0f560..d7fc1d76ec7 100644 --- a/filament/backend/src/vulkan/VulkanHandles.cpp +++ b/filament/backend/src/vulkan/VulkanHandles.cpp @@ -115,7 +115,6 @@ VulkanDescriptorSetLayout::VulkanDescriptorSetLayout(DescriptorSetLayout const& void VulkanDescriptorSet::acquire(VulkanTexture* texture) { mResources.acquire(texture); - mTextures[mTextureCount++] = texture; } void VulkanDescriptorSet::acquire(VulkanBufferObject* bufferObject) { @@ -283,11 +282,16 @@ VulkanRenderTarget::VulkanRenderTarget(VkDevice device, VkPhysicalDevice physica if (texture && texture->samples == 1) { auto msTexture = texture->getSidecar(); if (UTILS_UNLIKELY(!msTexture)) { + // Clear all usage flags that are not related to attachments, so that we can + // use the transient usage flag. + const TextureUsage usage = texture->usage & TextureUsage::ALL_ATTACHMENTS; + assert_invariant(static_cast(usage) != 0U); + // TODO: This should be allocated with the ResourceAllocator. msTexture = new VulkanTexture(device, physicalDevice, context, allocator, commands, handleAllocator, texture->target, ((VulkanTexture const*) texture)->levels, texture->format, - samples, texture->width, texture->height, texture->depth, texture->usage, + samples, texture->width, texture->height, texture->depth, usage, stagePool, true /* heap allocated */); texture->setSidecar(msTexture); } diff --git a/filament/backend/src/vulkan/VulkanHandles.h b/filament/backend/src/vulkan/VulkanHandles.h index 16f9c888f2e..1c8fe9d786c 100644 --- a/filament/backend/src/vulkan/VulkanHandles.h +++ b/filament/backend/src/vulkan/VulkanHandles.h @@ -151,17 +151,9 @@ struct VulkanDescriptorSet : public VulkanResource, HwDescriptorSet { void acquire(VulkanBufferObject* texture); - bool hasTexture(VulkanTexture* texture) { - return std::any_of(mTextures.begin(), mTextures.end(), - [texture](auto t) { return t == texture; }); - } - - // TODO: maybe change to fixed size for performance. VkDescriptorSet const vkSet; private: - std::array mTextures = { nullptr }; - uint8_t mTextureCount = 0; VulkanAcquireOnlyResourceManager mResources; OnRecycle mOnRecycleFn; }; @@ -202,10 +194,6 @@ struct VulkanProgram : public HwProgram, VulkanResource { inline VkShaderModule getFragmentShader() const { return mInfo->shaders[1]; } - inline utils::FixedCapacityVector const& getBindingToSamplerIndex() const { - return mInfo->bindingToSamplerIndex; - } - // Get a list of the sampler binding indices so that we don't have to loop through all possible // samplers. inline BindingList const& getBindings() const { return mInfo->bindings; } @@ -236,8 +224,7 @@ struct VulkanProgram : public HwProgram, VulkanResource { private: struct PipelineInfo { explicit PipelineInfo(backend::Program const& program) noexcept - : bindingToSamplerIndex(MAX_SAMPLER_COUNT, 0xffff), - pushConstantDescription(program) + : pushConstantDescription(program) #if FVK_ENABLED_DEBUG_SAMPLER_NAME , bindingToName(MAX_SAMPLER_COUNT, "") #endif @@ -245,8 +232,6 @@ struct VulkanProgram : public HwProgram, VulkanResource { BindingList bindings; - // We store the samplerGroupIndex as the top 8-bit and the index within each group as the lower 8-bit. - utils::FixedCapacityVector bindingToSamplerIndex; VkShaderModule shaders[MAX_SHADER_MODULES] = { VK_NULL_HANDLE }; PushConstantDescription pushConstantDescription; diff --git a/filament/backend/src/vulkan/VulkanImageUtility.h b/filament/backend/src/vulkan/VulkanImageUtility.h index 82ff436c163..f8027d96975 100644 --- a/filament/backend/src/vulkan/VulkanImageUtility.h +++ b/filament/backend/src/vulkan/VulkanImageUtility.h @@ -76,36 +76,6 @@ inline VkImageViewType getViewType(SamplerType target) { } } -inline VulkanLayout getDefaultLayout(TextureUsage usage) { - if (any(usage & TextureUsage::DEPTH_ATTACHMENT)) { - if (any(usage & TextureUsage::SAMPLEABLE)) { - return VulkanLayout::DEPTH_SAMPLER; - } else { - return VulkanLayout::DEPTH_ATTACHMENT; - } - } - - if (any(usage & TextureUsage::COLOR_ATTACHMENT)) { - return VulkanLayout::COLOR_ATTACHMENT; - } - // Finally, the layout for an immutable texture is optimal read-only. - return VulkanLayout::READ_ONLY; -} - -inline VulkanLayout getDefaultLayout(VkImageUsageFlags vkusage) { - TextureUsage usage{}; - if (vkusage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) { - usage = usage | TextureUsage::DEPTH_ATTACHMENT; - } - if (vkusage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) { - usage = usage | TextureUsage::COLOR_ATTACHMENT; - } - if (vkusage & VK_IMAGE_USAGE_SAMPLED_BIT) { - usage = usage | TextureUsage::SAMPLEABLE; - } - return getDefaultLayout(usage); -} - constexpr inline VkImageLayout getVkLayout(VulkanLayout layout) { switch (layout) { case VulkanLayout::UNDEFINED: diff --git a/filament/backend/src/vulkan/VulkanReadPixels.cpp b/filament/backend/src/vulkan/VulkanReadPixels.cpp index bef08811c6d..418f6771961 100644 --- a/filament/backend/src/vulkan/VulkanReadPixels.cpp +++ b/filament/backend/src/vulkan/VulkanReadPixels.cpp @@ -232,7 +232,7 @@ void VulkanReadPixels::run(VulkanRenderTarget* srcTarget, uint32_t const x, uint VulkanAttachment const srcAttachment = srcTarget->getColor(0); VkImageSubresourceRange const srcRange = srcAttachment.getSubresourceRange(); - srcTexture->transitionLayout(cmdbuffer, {}, srcRange, VulkanLayout::TRANSFER_SRC); + srcTexture->transitionLayout(cmdbuffer, srcRange, VulkanLayout::TRANSFER_SRC); VkImageCopy const imageCopyRegion = { .srcSubresource = { @@ -268,7 +268,7 @@ void VulkanReadPixels::run(VulkanRenderTarget* srcTarget, uint32_t const x, uint imgutil::getVkLayout(VulkanLayout::TRANSFER_DST), 1, &imageCopyRegion); // Restore the source image layout. - srcTexture->transitionLayout(cmdbuffer, {}, srcRange, VulkanLayout::COLOR_ATTACHMENT); + srcTexture->transitionLayout(cmdbuffer, srcRange, srcTexture->getDefaultLayout()); vkEndCommandBuffer(cmdbuffer); diff --git a/filament/backend/src/vulkan/VulkanTexture.cpp b/filament/backend/src/vulkan/VulkanTexture.cpp index 2181ee59053..2358d6ea0de 100644 --- a/filament/backend/src/vulkan/VulkanTexture.cpp +++ b/filament/backend/src/vulkan/VulkanTexture.cpp @@ -113,24 +113,52 @@ VkComponentMapping composeSwizzle(VkComponentMapping const& prev, VkComponentMap }; } -} // anonymous namespace +inline VulkanLayout getDefaultLayoutImpl(TextureUsage usage) { + if (any(usage & TextureUsage::DEPTH_ATTACHMENT)) { + if (any(usage & TextureUsage::SAMPLEABLE)) { + return VulkanLayout::DEPTH_SAMPLER; + } else { + return VulkanLayout::DEPTH_ATTACHMENT; + } + } -VulkanTextureState::VulkanTextureState( - VkDevice device, VmaAllocator allocator, VulkanCommands* commands, - VulkanStagePool& stagePool, - VkFormat format, VkImageViewType viewType, uint8_t levels, uint8_t layerCount) - : VulkanResource(VulkanResourceType::HEAP_ALLOCATED), - mVkFormat(format), - mViewType(viewType), - mFullViewRange { - filament::backend::getImageAspect(format), 0, levels, 0, layerCount - }, - mStagePool(stagePool), - mDevice(device), - mAllocator(allocator), - mCommands(commands) { + if (any(usage & TextureUsage::COLOR_ATTACHMENT)) { + return VulkanLayout::COLOR_ATTACHMENT; + } + // Finally, the layout for an immutable texture is optimal read-only. + return VulkanLayout::READ_ONLY; +} + +inline VulkanLayout getDefaultLayoutImpl(VkImageUsageFlags vkusage) { + TextureUsage usage{}; + if (vkusage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) { + usage = usage | TextureUsage::DEPTH_ATTACHMENT; + } + if (vkusage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) { + usage = usage | TextureUsage::COLOR_ATTACHMENT; + } + if (vkusage & VK_IMAGE_USAGE_SAMPLED_BIT) { + usage = usage | TextureUsage::SAMPLEABLE; + } + return getDefaultLayoutImpl(usage); } +} // anonymous namespace + +VulkanTextureState::VulkanTextureState(VkDevice device, VmaAllocator allocator, + VulkanCommands* commands, VulkanStagePool& stagePool, VkFormat format, + VkImageViewType viewType, uint8_t levels, uint8_t layerCount, VulkanLayout defaultLayout) + : VulkanResource(VulkanResourceType::HEAP_ALLOCATED), + mVkFormat(format), + mViewType(viewType), + mFullViewRange{filament::backend::getImageAspect(format), 0, levels, 0, layerCount}, + mDefaultLayout(defaultLayout), + mStagePool(stagePool), + mDevice(device), + mAllocator(allocator), + mCommands(commands), + mIsTransientAttachment(false) {} + VulkanTextureState* VulkanTexture::getSharedState() { VulkanTextureState* state = mAllocator->handle_cast(mState); return state; @@ -141,6 +169,7 @@ VulkanTextureState const* VulkanTexture::getSharedState() const { return state; } +// Constructor for internally passed VkImage VulkanTexture::VulkanTexture( VkDevice device, VmaAllocator allocator, VulkanCommands* commands, VulkanResourceAllocator* handleAllocator, @@ -153,12 +182,14 @@ VulkanTexture::VulkanTexture( mAllocator(handleAllocator), mState(handleAllocator->initHandle( device, allocator, commands, stagePool, - format, imgutil::getViewType(SamplerType::SAMPLER_2D), 1, 1)) { + format, imgutil::getViewType(SamplerType::SAMPLER_2D), 1, 1, + getDefaultLayoutImpl(tusage))) { auto* const state = getSharedState(); state->mTextureImage = image; mPrimaryViewRange = state->mFullViewRange; } +// Constructor for user facing texture VulkanTexture::VulkanTexture(VkDevice device, VkPhysicalDevice physicalDevice, VulkanContext const& context, VmaAllocator allocator, VulkanCommands* commands, VulkanResourceAllocator* handleAllocator, SamplerType target, uint8_t levels, @@ -170,7 +201,7 @@ VulkanTexture::VulkanTexture(VkDevice device, VkPhysicalDevice physicalDevice, mAllocator(handleAllocator), mState(handleAllocator->initHandle(device, allocator, commands, stagePool, backend::getVkFormat(tformat), imgutil::getViewType(target), levels, - getLayerCount(target, depth))) { + getLayerCount(target, depth), VulkanLayout::UNDEFINED)) { auto* const state = getSharedState(); // Create an appropriately-sized device-only VkImage, but do not fill it yet. @@ -209,6 +240,19 @@ VulkanTexture::VulkanTexture(VkDevice device, VkPhysicalDevice physicalDevice, imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; } + // Determine if we can use the transient usage flag combined with lazily allocated memory. + const bool useTransientAttachment = + // Lazily allocated memory is available. + context.isLazilyAllocatedMemorySupported() && + // Usage consists of attachment flags only. + none(tusage & ~TextureUsage::ALL_ATTACHMENTS) && + // Usage contains at least one attachment flag. + any(tusage & TextureUsage::ALL_ATTACHMENTS); + state->mIsTransientAttachment = useTransientAttachment; + + const VkImageUsageFlags transientFlag = + useTransientAttachment ? VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT : 0U; + if (any(usage & TextureUsage::SAMPLEABLE)) { #if FVK_ENABLED(FVK_DEBUG_TEXTURE) @@ -224,19 +268,19 @@ VulkanTexture::VulkanTexture(VkDevice device, VkPhysicalDevice physicalDevice, imageInfo.usage |= VK_IMAGE_USAGE_SAMPLED_BIT; } if (any(usage & TextureUsage::COLOR_ATTACHMENT)) { - imageInfo.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + imageInfo.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | transientFlag; if (any(usage & TextureUsage::SUBPASS_INPUT)) { imageInfo.usage |= VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT; } } if (any(usage & TextureUsage::STENCIL_ATTACHMENT)) { - imageInfo.usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + imageInfo.usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | transientFlag; } if (any(usage & TextureUsage::UPLOADABLE)) { imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; } if (any(usage & TextureUsage::DEPTH_ATTACHMENT)) { - imageInfo.usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + imageInfo.usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | transientFlag; // Depth resolves uses a custom shader and therefore needs to be sampleable. if (samples > 1) { @@ -285,8 +329,11 @@ VulkanTexture::VulkanTexture(VkDevice device, VkPhysicalDevice physicalDevice, VkMemoryRequirements memReqs = {}; vkGetImageMemoryRequirements(state->mDevice, state->mTextureImage, &memReqs); + const VkFlags requiredMemoryFlags = + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | + (useTransientAttachment ? VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT : 0U); uint32_t memoryTypeIndex - = context.selectMemoryType(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + = context.selectMemoryType(memReqs.memoryTypeBits, requiredMemoryFlags); FILAMENT_CHECK_POSTCONDITION(memoryTypeIndex < VK_MAX_MEMORY_TYPES) << "VulkanTexture: unable to find a memory type that meets requirements."; @@ -309,18 +356,20 @@ VulkanTexture::VulkanTexture(VkDevice device, VkPhysicalDevice physicalDevice, VulkanCommandBuffer& commandsBuf = state->mCommands->get(); commandsBuf.acquire(this); - transitionLayout(&commandsBuf, mPrimaryViewRange, imgutil::getDefaultLayout(imageInfo.usage)); + + auto const defaultLayout = state->mDefaultLayout = getDefaultLayoutImpl(imageInfo.usage); + transitionLayout(&commandsBuf, mPrimaryViewRange, defaultLayout); } +// Constructor for creating a texture view VulkanTexture::VulkanTexture(VkDevice device, VkPhysicalDevice physicalDevice, VulkanContext const& context, VmaAllocator allocator, VulkanCommands* commands, - VulkanResourceAllocator* handleAllocator, - VulkanTexture const* src, uint8_t baseLevel, uint8_t levelCount) - : HwTexture(src->target, src->levels, src->samples, src->width, src->height, src->depth, - src->format, src->usage), - VulkanResource(VulkanResourceType::TEXTURE), - mAllocator(handleAllocator) -{ + VulkanResourceAllocator* handleAllocator, VulkanTexture const* src, uint8_t baseLevel, + uint8_t levelCount) + : HwTexture(src->target, src->levels, src->samples, src->width, src->height, src->depth, + src->format, src->usage), + VulkanResource(VulkanResourceType::TEXTURE), + mAllocator(handleAllocator) { mState = src->mState; auto* state = getSharedState(); @@ -330,14 +379,15 @@ VulkanTexture::VulkanTexture(VkDevice device, VkPhysicalDevice physicalDevice, mPrimaryViewRange.levelCount = levelCount; } +// Constructor for creating a texture view with swizzle VulkanTexture::VulkanTexture(VkDevice device, VkPhysicalDevice physicalDevice, VulkanContext const& context, VmaAllocator allocator, VulkanCommands* commands, - VulkanResourceAllocator* handleAllocator, - VulkanTexture const* src, VkComponentMapping swizzle) - : HwTexture(src->target, src->levels, src->samples, src->width, src->height, src->depth, - src->format, src->usage), - VulkanResource(VulkanResourceType::TEXTURE), - mAllocator(handleAllocator) { + VulkanResourceAllocator* handleAllocator, VulkanTexture const* src, + VkComponentMapping swizzle) + : HwTexture(src->target, src->levels, src->samples, src->width, src->height, src->depth, + src->format, src->usage), + VulkanResource(VulkanResourceType::TEXTURE), + mAllocator(handleAllocator) { mState = src->mState; auto* state = getSharedState(); state->refs++; @@ -440,7 +490,7 @@ void VulkanTexture::updateImage(const PixelBufferDescriptor& data, uint32_t widt VkImageLayout const newVkLayout = imgutil::getVkLayout(newLayout); if (nextLayout == VulkanLayout::UNDEFINED) { - nextLayout = imgutil::getDefaultLayout(this->usage); + nextLayout = getDefaultLayout(); } transitionLayout(&commands, transitionRange, newLayout); @@ -491,6 +541,11 @@ void VulkanTexture::updateImageWithBlit(const PixelBufferDescriptor& hostData, u transitionLayout(&commands, range, oldLayout); } +VulkanLayout VulkanTexture::getDefaultLayout() const { + auto* const state = getSharedState(); + return state->mDefaultLayout; +} + VkImageView VulkanTexture::getAttachmentView(VkImageSubresourceRange range) { range.levelCount = 1; range.layerCount = 1; @@ -536,13 +591,11 @@ VkImageAspectFlags VulkanTexture::getImageAspect() const { } bool VulkanTexture::transitionLayout(VulkanCommandBuffer* commands, - const VkImageSubresourceRange& range, VulkanLayout newLayout) { - return transitionLayout(commands->buffer(), commands->fence, range, newLayout); + VkImageSubresourceRange const& range, VulkanLayout newLayout) { + return transitionLayout(commands->buffer(), range, newLayout); } -bool VulkanTexture::transitionLayout( - VkCommandBuffer cmdbuf, std::shared_ptr fence, - const VkImageSubresourceRange& range, +bool VulkanTexture::transitionLayout(VkCommandBuffer cmdbuf, VkImageSubresourceRange const& range, VulkanLayout newLayout) { auto* const state = getSharedState(); VulkanLayout const oldLayout = getLayout(range.baseArrayLayer, range.baseMipLevel); @@ -602,8 +655,6 @@ bool VulkanTexture::transitionLayout( setLayout(range, newLayout); if (hasTransitions) { - state->mTransitionFence = fence; - #if FVK_ENABLED(FVK_DEBUG_LAYOUT_TRANSITION) FVK_LOGD << "transition texture=" << state->mTextureImage << " (" << range.baseArrayLayer << "," << range.baseMipLevel << ")" << " count=(" << range.layerCount << "," diff --git a/filament/backend/src/vulkan/VulkanTexture.h b/filament/backend/src/vulkan/VulkanTexture.h index e3d1babbbd0..acc58846709 100644 --- a/filament/backend/src/vulkan/VulkanTexture.h +++ b/filament/backend/src/vulkan/VulkanTexture.h @@ -34,8 +34,8 @@ class VulkanResourceAllocator; struct VulkanTextureState : public VulkanResource { VulkanTextureState(VkDevice device, VmaAllocator allocator, VulkanCommands* commands, - VulkanStagePool& stagePool, - VkFormat format, VkImageViewType viewType, uint8_t levels, uint8_t layerCount); + VulkanStagePool& stagePool, VkFormat format, VkImageViewType viewType, uint8_t levels, + uint8_t layerCount, VulkanLayout defaultLayout); struct ImageViewKey { VkImageSubresourceRange range; // 4 * 5 bytes @@ -67,8 +67,8 @@ struct VulkanTextureState : public VulkanResource { VkFormat const mVkFormat; VkImageViewType const mViewType; VkImageSubresourceRange const mFullViewRange; - VkImage mTextureImage = VK_NULL_HANDLE; + VulkanLayout mDefaultLayout; // Track the image layout of each subresource using a sparse range map. utils::RangeMap mSubresourceLayouts; @@ -78,7 +78,7 @@ struct VulkanTextureState : public VulkanResource { VkDevice mDevice; VmaAllocator mAllocator; VulkanCommands* mCommands; - std::shared_ptr mTransitionFence; + bool mIsTransientAttachment; }; @@ -134,6 +134,10 @@ struct VulkanTexture : public HwTexture, VulkanResource { return getLayout(mPrimaryViewRange.baseArrayLayer, mPrimaryViewRange.baseMipLevel); } + // Returns the layout for the intended use of this texture (and not the expected layout at the + // time of the call. + VulkanLayout getDefaultLayout() const; + // Gets or creates a cached VkImageView for a single subresource that can be used as a render // target attachment. Unlike the primary image view, this always has type VK_IMAGE_VIEW_TYPE_2D // and the identity swizzle. @@ -170,11 +174,16 @@ struct VulkanTexture : public HwTexture, VulkanResource { return state->mSidecarMSAA.get(); } - bool transitionLayout(VulkanCommandBuffer* commands, const VkImageSubresourceRange& range, + bool isTransientAttachment() const { + VulkanTextureState const* state = getSharedState(); + return state->mIsTransientAttachment; + } + + bool transitionLayout(VulkanCommandBuffer* commands, VkImageSubresourceRange const& range, VulkanLayout newLayout); - bool transitionLayout(VkCommandBuffer cmdbuf, std::shared_ptr fence, - VkImageSubresourceRange const& range, VulkanLayout newLayout); + bool transitionLayout(VkCommandBuffer cmdbuf, VkImageSubresourceRange const& range, + VulkanLayout newLayout); void attachmentToSamplerBarrier(VulkanCommandBuffer* commands, VkImageSubresourceRange const& range); @@ -190,13 +199,6 @@ struct VulkanTexture : public HwTexture, VulkanResource { // manually (outside of calls to transitionLayout). void setLayout(VkImageSubresourceRange const& range, VulkanLayout newLayout); - bool transitionReady() { - VulkanTextureState* state = getSharedState(); - auto res = !state->mTransitionFence || state->mTransitionFence->getStatus() == VK_SUCCESS; - state->mTransitionFence.reset(); - return res; - } - #if FVK_ENABLED(FVK_DEBUG_TEXTURE) void print() const; #endif diff --git a/filament/backend/src/vulkan/caching/VulkanDescriptorSetManager.cpp b/filament/backend/src/vulkan/caching/VulkanDescriptorSetManager.cpp index eb72d1be908..6ffaa8ae07d 100644 --- a/filament/backend/src/vulkan/caching/VulkanDescriptorSetManager.cpp +++ b/filament/backend/src/vulkan/caching/VulkanDescriptorSetManager.cpp @@ -193,64 +193,6 @@ class DescriptorPool { UnusedSetMap mUnused; }; -// This is an ever-expanding pool of sets where it -// 1. Keeps a list of smaller pools of different layout-dimensions. -// 2. Will add a pool if existing pool are not compatible with the requested layout o runs out. -class DescriptorInfinitePool { -private: - static constexpr uint16_t EXPECTED_SET_COUNT = 10; - static constexpr float SET_COUNT_GROWTH_FACTOR = 1.5; - -public: - DescriptorInfinitePool(VkDevice device) - : mDevice(device) {} - - VkDescriptorSet obtainSet(VulkanDescriptorSetLayout* layout) { - auto const vklayout = layout->getVkLayout(); - DescriptorPool* sameTypePool = nullptr; - for (auto& pool: mPools) { - if (!pool->canAllocate(layout->count)) { - continue; - } - if (auto set = pool->obtainSet(vklayout); set != VK_NULL_HANDLE) { - return set; - } - if (!sameTypePool || sameTypePool->capacity() < pool->capacity()) { - sameTypePool = pool.get(); - } - } - - uint16_t capacity = EXPECTED_SET_COUNT; - if (sameTypePool) { - // Exponentially increase the size of the pool to ensure we don't hit this too often. - capacity = std::ceil(sameTypePool->capacity() * SET_COUNT_GROWTH_FACTOR); - } - - // We need to increase the set of pools by one. - mPools.push_back(std::make_unique(mDevice, - DescriptorCount::fromLayoutBitmask(layout->bitmask), capacity)); - auto& pool = mPools.back(); - auto ret = pool->obtainSet(vklayout); - assert_invariant(ret != VK_NULL_HANDLE && "failed to obtain a set?"); - return ret; - } - - void recycle(DescriptorCount const& count, VkDescriptorSetLayout vklayout, - VkDescriptorSet vkSet) { - for (auto& pool: mPools) { - if (!pool->canAllocate(count)) { - continue; - } - pool->recycle(vklayout, vkSet); - break; - } - } - -private: - VkDevice mDevice; - std::vector> mPools; -}; - template struct Equal { bool operator()(Key const& k1, Key const& k2) const { @@ -318,7 +260,67 @@ inline VkDescriptorSetLayout createLayout(VkDevice device, BitmaskGroup const& b return layout; } -class DescriptorSetLayoutManager { +} // anonymous namespace + +// This is an ever-expanding pool of sets where it +// 1. Keeps a list of smaller pools of different layout-dimensions. +// 2. Will add a pool if existing pool are not compatible with the requested layout o runs out. +class VulkanDescriptorSetManager::DescriptorInfinitePool { +private: + static constexpr uint16_t EXPECTED_SET_COUNT = 10; + static constexpr float SET_COUNT_GROWTH_FACTOR = 1.5; + +public: + DescriptorInfinitePool(VkDevice device) + : mDevice(device) {} + + VkDescriptorSet obtainSet(VulkanDescriptorSetLayout* layout) { + auto const vklayout = layout->getVkLayout(); + DescriptorPool* sameTypePool = nullptr; + for (auto& pool: mPools) { + if (!pool->canAllocate(layout->count)) { + continue; + } + if (auto set = pool->obtainSet(vklayout); set != VK_NULL_HANDLE) { + return set; + } + if (!sameTypePool || sameTypePool->capacity() < pool->capacity()) { + sameTypePool = pool.get(); + } + } + + uint16_t capacity = EXPECTED_SET_COUNT; + if (sameTypePool) { + // Exponentially increase the size of the pool to ensure we don't hit this too often. + capacity = std::ceil(sameTypePool->capacity() * SET_COUNT_GROWTH_FACTOR); + } + + // We need to increase the set of pools by one. + mPools.push_back(std::make_unique(mDevice, + DescriptorCount::fromLayoutBitmask(layout->bitmask), capacity)); + auto& pool = mPools.back(); + auto ret = pool->obtainSet(vklayout); + assert_invariant(ret != VK_NULL_HANDLE && "failed to obtain a set?"); + return ret; + } + + void recycle(DescriptorCount const& count, VkDescriptorSetLayout vklayout, + VkDescriptorSet vkSet) { + for (auto& pool: mPools) { + if (!pool->canAllocate(count)) { + continue; + } + pool->recycle(vklayout, vkSet); + break; + } + } + +private: + VkDevice mDevice; + std::vector> mPools; +}; + +class VulkanDescriptorSetManager::DescriptorSetLayoutManager { public: DescriptorSetLayoutManager(VkDevice device) : mDevice(device) {} @@ -345,329 +347,226 @@ class DescriptorSetLayoutManager { mVkLayouts; }; -} // anonymous namespace - -class VulkanDescriptorSetManager::Impl { +class VulkanDescriptorSetManager::DescriptorSetHistory { private: - struct DescriptorSetHistory { - private: - using TextureBundle = std::pair; - public: - DescriptorSetHistory() - : dynamicUboCount(0), - mResources(nullptr) {} - - DescriptorSetHistory(UniformBufferBitmask const& dynamicUbo, uint8_t uniqueDynamicUboCount, - VulkanResourceAllocator* allocator, VulkanDescriptorSet* set) - : dynamicUboMask(dynamicUbo), - dynamicUboCount(uniqueDynamicUboCount), - mResources(allocator), - mSet(set), - mBound(false) { - assert_invariant(set); - // initial state is unbound. - mResources.acquire(mSet); - unbind(); - } - - ~DescriptorSetHistory() { - if (mSet) { - mResources.clear(); - } - } - - void setOffsets(backend::DescriptorSetOffsetArray&& offsets) noexcept { - mOffsets = std::move(offsets); - mBound = false; - } - - void write(uint8_t binding) noexcept { - mBound = false; - } - - // Ownership will be transfered to the commandbuffer. - void bind(VulkanCommandBuffer* commands, VkPipelineLayout pipelineLayout, uint8_t index) noexcept { - VkCommandBuffer const cmdbuffer = commands->buffer(); - vkCmdBindDescriptorSets(cmdbuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, - index, 1, &mSet->vkSet, dynamicUboCount, mOffsets.data()); - - commands->acquire(mSet); - mResources.clear(); - mBound = true; - } - - void unbind() noexcept { - mResources.acquire(mSet); - mBound = false; - } - - bool bound() const noexcept { return mBound; } - - UniformBufferBitmask const dynamicUboMask; - uint8_t const dynamicUboCount; - - private: - FixedSizeVulkanResourceManager<1> mResources; - VulkanDescriptorSet* mSet = nullptr; - - backend::DescriptorSetOffsetArray mOffsets; - bool mBound = false; - }; - - using DescriptorSetHistoryArray = - std::array; - - struct BoundInfo { - VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; - DescriptorSetMask setMask; - DescriptorSetHistoryArray boundSets; - - bool operator==(BoundInfo const& info) const { - if (pipelineLayout != info.pipelineLayout || setMask != info.setMask) { - return false; - } - bool equal = true; - setMask.forEachSetBit([&](size_t i) { - if (boundSets[i] != info.boundSets[i]) { - equal = false; - } - }); - return equal; - } - }; + using TextureBundle = std::pair; public: - Impl(VkDevice device, VulkanResourceAllocator* resourceAllocator) - : mDevice(device), - mResourceAllocator(resourceAllocator), - mLayoutManager(device), - mDescriptorPool(device) {} - - // bind() is not really binding the set but just stashing until we have all the info - // (pipelinelayout). - void bind(uint8_t setIndex, VulkanDescriptorSet* set, - backend::DescriptorSetOffsetArray&& offsets) { - auto& history = mHistory[set]; - history.setOffsets(std::move(offsets)); - - auto lastHistory = mStashedSets[setIndex]; - if (lastHistory) { - lastHistory->unbind(); - } - mStashedSets[setIndex] = &history; + DescriptorSetHistory() + : dynamicUboCount(0), + mResources(nullptr) {} + + DescriptorSetHistory(UniformBufferBitmask const& dynamicUbo, uint8_t uniqueDynamicUboCount, + VulkanResourceAllocator* allocator, VulkanDescriptorSet* set) + : dynamicUboMask(dynamicUbo), + dynamicUboCount(uniqueDynamicUboCount), + mResources(allocator), + mSet(set), + mBound(false) { + assert_invariant(set); + // initial state is unbound. + unbind(); } - void commit(VulkanCommandBuffer* commands, VkPipelineLayout pipelineLayout, - DescriptorSetMask const& setMask) { - DescriptorSetHistoryArray& updateSets = mStashedSets; - - // setMask indicates the set of descriptor sets the driver wants to bind, curMask is the - // actual set of sets that *needs* to be bound. - DescriptorSetMask curMask = setMask; - - setMask.forEachSetBit([&](size_t index) { - if (!updateSets[index] || updateSets[index]->bound()) { - curMask.unset(index); - } - }); - - BoundInfo nextInfo = { - pipelineLayout, - setMask, - updateSets, - }; - if (curMask.none() && mLastBoundInfo == nextInfo) { - return; + ~DescriptorSetHistory() { + if (mSet) { + mResources.clear(); } + } - curMask.forEachSetBit([&updateSets, commands, pipelineLayout](size_t index) { - updateSets[index]->bind(commands, pipelineLayout, index); - }); - mLastBoundInfo = nextInfo; + void setOffsets(backend::DescriptorSetOffsetArray&& offsets) noexcept { + mOffsets = std::move(offsets); + mBound = false; } - void updateBuffer(VulkanDescriptorSet* set, uint8_t binding, VulkanBufferObject* bufferObject, - VkDeviceSize offset, VkDeviceSize size) noexcept { - VkDescriptorBufferInfo const info = { - .buffer = bufferObject->buffer.getGpuBuffer(), - .offset = offset, - .range = size, - }; + void write(uint8_t binding) noexcept { mBound = false; } - VkDescriptorType type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - auto& history = mHistory[set]; + // Ownership will be transfered to the commandbuffer. + void bind(VulkanCommandBuffer* commands, VkPipelineLayout pipelineLayout, + uint8_t index) noexcept { + VkCommandBuffer const cmdbuffer = commands->buffer(); + vkCmdBindDescriptorSets(cmdbuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, index, + 1, &mSet->vkSet, dynamicUboCount, mOffsets.data()); - if (history.dynamicUboMask.test(binding)) { - type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; - } - VkWriteDescriptorSet const descriptorWrite = { - .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - .pNext = nullptr, - .dstSet = set->vkSet, - .dstBinding = binding, - .descriptorCount = 1, - .descriptorType = type, - .pBufferInfo = &info, - }; - vkUpdateDescriptorSets(mDevice, 1, &descriptorWrite, 0, nullptr); - set->acquire(bufferObject); - history.write(binding); + commands->acquire(mSet); + mResources.clear(); + mBound = true; } - void updateSampler(VulkanDescriptorSet* set, uint8_t binding, VulkanTexture* texture, - VkSampler sampler) noexcept { - VkDescriptorImageInfo info{ - .sampler = sampler, - }; - VkImageSubresourceRange const range = texture->getPrimaryViewRange(); - VkImageViewType const expectedType = texture->getViewType(); - if (any(texture->usage & TextureUsage::DEPTH_ATTACHMENT) && - expectedType == VK_IMAGE_VIEW_TYPE_2D) { - // If the sampler is part of a mipmapped depth texture, where one of the level *can* be - // an attachment, then the sampler for this texture has the same view properties as a - // view for an attachment. Therefore, we can use getAttachmentView to get a - // corresponding VkImageView. - info.imageView = texture->getAttachmentView(range); - } else { - info.imageView = texture->getViewForType(range, expectedType); - } - info.imageLayout = imgutil::getVkLayout(texture->getPrimaryImageLayout()); - VkWriteDescriptorSet const descriptorWrite = { - .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - .pNext = nullptr, - .dstSet = set->vkSet, - .dstBinding = binding, - .descriptorCount = 1, - .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - .pImageInfo = &info, - }; - vkUpdateDescriptorSets(mDevice, 1, &descriptorWrite, 0, nullptr); - set->acquire(texture); - mHistory[set].write(binding); + void unbind() noexcept { + mResources.acquire(mSet); + mBound = false; } - void updateInputAttachment(VulkanDescriptorSet* set, VulkanAttachment attachment) noexcept { - // TOOD: fill-in this region - } + bool bound() const noexcept { return mBound; } - void setPlaceHolders(VkSampler sampler, VulkanTexture* texture, - VulkanBufferObject* bufferObject) noexcept { - mPlaceHolderBufferInfo = { - .buffer = bufferObject->buffer.getGpuBuffer(), - .offset = 0, - .range = 1, - }; - mPlaceHolderImageInfo = { - .sampler = sampler, - .imageView = texture->getPrimaryImageView(), - .imageLayout = imgutil::getVkLayout(texture->getPrimaryImageLayout()), - }; - } + UniformBufferBitmask const dynamicUboMask; + uint8_t const dynamicUboCount; - void createSet(Handle handle, VulkanDescriptorSetLayout* layout) { - auto const vkSet = mDescriptorPool.obtainSet(layout); - auto const& count = layout->count; - auto const vklayout = layout->getVkLayout(); - VulkanDescriptorSet* set = - mResourceAllocator->construct(handle, mResourceAllocator, - vkSet, [vkSet, count, vklayout, this](VulkanDescriptorSet* set) { - eraseSetFromHistory(set); - mDescriptorPool.recycle(count, vklayout, vkSet); - }); - mHistory.emplace( - std::piecewise_construct, - std::forward_as_tuple(set), - std::forward_as_tuple(layout->bitmask.dynamicUbo, layout->count.dynamicUbo, - mResourceAllocator, set)); - } +private: + FixedSizeVulkanResourceManager<1> mResources; + VulkanDescriptorSet* mSet = nullptr; - void destroySet(Handle handle) { - VulkanDescriptorSet* set = mResourceAllocator->handle_cast(handle); - eraseSetFromHistory(set); - } + backend::DescriptorSetOffsetArray mOffsets; + bool mBound = false; +}; - void initVkLayout(VulkanDescriptorSetLayout* layout) { - layout->setVkLayout(mLayoutManager.getVkLayout(layout)); - } +VulkanDescriptorSetManager::VulkanDescriptorSetManager(VkDevice device, + VulkanResourceAllocator* resourceAllocator) + : mDevice(device), + mResourceAllocator(resourceAllocator), + mLayoutManager(std::make_unique(device)), + mDescriptorPool(std::make_unique(device)) {} -private: - inline void eraseSetFromHistory(VulkanDescriptorSet* set) { - DescriptorSetHistory* history = &mHistory[set]; - mHistory.erase(set); +VulkanDescriptorSetManager::~VulkanDescriptorSetManager() = default; - for (uint8_t i = 0; i < mStashedSets.size(); ++i) { - if (mStashedSets[i] == history) { - mStashedSets[i] = nullptr; - } - } +void VulkanDescriptorSetManager::terminate() noexcept{ + mLayoutManager.reset(); + mDescriptorPool.reset(); + mHistory.clear(); +} + +// bind() is not really binding the set but just stashing until we have all the info +// (pipelinelayout). +void VulkanDescriptorSetManager::bind(uint8_t setIndex, VulkanDescriptorSet* set, + backend::DescriptorSetOffsetArray&& offsets) { + auto history = mHistory[set].get(); + history->setOffsets(std::move(offsets)); + + auto lastHistory = mStashedSets[setIndex]; + if (lastHistory) { + lastHistory->unbind(); } + mStashedSets[setIndex] = history; +} - VkDevice mDevice; - VulkanResourceAllocator* mResourceAllocator; - DescriptorSetLayoutManager mLayoutManager; - DescriptorInfinitePool mDescriptorPool; - std::pair mInputAttachment; - std::unordered_map mHistory; - DescriptorSetHistoryArray mStashedSets = {}; +void VulkanDescriptorSetManager::commit(VulkanCommandBuffer* commands, + VkPipelineLayout pipelineLayout, DescriptorSetMask const& setMask) { + DescriptorSetHistoryArray& updateSets = mStashedSets; - BoundInfo mLastBoundInfo; + // setMask indicates the set of descriptor sets the driver wants to bind, curMask is the + // actual set of sets that *needs* to be bound. + DescriptorSetMask curMask = setMask; - VkDescriptorBufferInfo mPlaceHolderBufferInfo; - VkDescriptorImageInfo mPlaceHolderImageInfo; -}; + setMask.forEachSetBit([&](size_t index) { + if (!updateSets[index] || updateSets[index]->bound()) { + curMask.unset(index); + } + }); -VulkanDescriptorSetManager::VulkanDescriptorSetManager(VkDevice device, - VulkanResourceAllocator* resourceAllocator) - : mImpl(new Impl(device, resourceAllocator)) {} + BoundInfo nextInfo = { + pipelineLayout, + setMask, + updateSets, + }; + if (curMask.none() && mLastBoundInfo == nextInfo) { + return; + } -void VulkanDescriptorSetManager::terminate() noexcept { - assert_invariant(mImpl); - delete mImpl; - mImpl = nullptr; + curMask.forEachSetBit([&updateSets, commands, pipelineLayout](size_t index) { + updateSets[index]->bind(commands, pipelineLayout, index); + }); + mLastBoundInfo = nextInfo; } void VulkanDescriptorSetManager::updateBuffer(VulkanDescriptorSet* set, uint8_t binding, VulkanBufferObject* bufferObject, VkDeviceSize offset, VkDeviceSize size) noexcept { - mImpl->updateBuffer(set, binding, bufferObject, offset, size); + VkDescriptorBufferInfo const info = { + .buffer = bufferObject->buffer.getGpuBuffer(), + .offset = offset, + .range = size, + }; + + VkDescriptorType type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + auto& history = mHistory[set]; + + if (history->dynamicUboMask.test(binding)) { + type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; + } + VkWriteDescriptorSet const descriptorWrite = { + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .pNext = nullptr, + .dstSet = set->vkSet, + .dstBinding = binding, + .descriptorCount = 1, + .descriptorType = type, + .pBufferInfo = &info, + }; + vkUpdateDescriptorSets(mDevice, 1, &descriptorWrite, 0, nullptr); + set->acquire(bufferObject); + history->write(binding); } void VulkanDescriptorSetManager::updateSampler(VulkanDescriptorSet* set, uint8_t binding, VulkanTexture* texture, VkSampler sampler) noexcept { - mImpl->updateSampler(set, binding, texture, sampler); + VkDescriptorImageInfo info{ + .sampler = sampler, + }; + VkImageSubresourceRange const range = texture->getPrimaryViewRange(); + VkImageViewType const expectedType = texture->getViewType(); + if (any(texture->usage & TextureUsage::DEPTH_ATTACHMENT) && + expectedType == VK_IMAGE_VIEW_TYPE_2D) { + // If the sampler is part of a mipmapped depth texture, where one of the level *can* be + // an attachment, then the sampler for this texture has the same view properties as a + // view for an attachment. Therefore, we can use getAttachmentView to get a + // corresponding VkImageView. + info.imageView = texture->getAttachmentView(range); + } else { + info.imageView = texture->getViewForType(range, expectedType); + } + info.imageLayout = imgutil::getVkLayout(texture->getDefaultLayout()); + VkWriteDescriptorSet const descriptorWrite = { + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .pNext = nullptr, + .dstSet = set->vkSet, + .dstBinding = binding, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .pImageInfo = &info, + }; + vkUpdateDescriptorSets(mDevice, 1, &descriptorWrite, 0, nullptr); + set->acquire(texture); + mHistory[set]->write(binding); } void VulkanDescriptorSetManager::updateInputAttachment(VulkanDescriptorSet* set, VulkanAttachment attachment) noexcept { - mImpl->updateInputAttachment(set, attachment); -} - -void VulkanDescriptorSetManager::setPlaceHolders(VkSampler sampler, VulkanTexture* texture, - VulkanBufferObject* bufferObject) noexcept { - mImpl->setPlaceHolders(sampler, texture, bufferObject); -} - -void VulkanDescriptorSetManager::bind(uint8_t setIndex, VulkanDescriptorSet* set, - backend::DescriptorSetOffsetArray&& offsets) { - return mImpl->bind(setIndex, set, std::move(offsets)); -} - -void VulkanDescriptorSetManager::commit(VulkanCommandBuffer* commands, - VkPipelineLayout pipelineLayout, DescriptorSetMask const& setMask) { - mImpl->commit(commands, pipelineLayout, setMask); + // TOOD: fill-in this region } void VulkanDescriptorSetManager::createSet(Handle handle, VulkanDescriptorSetLayout* layout) { - mImpl->createSet(handle, layout); + auto const vkSet = mDescriptorPool->obtainSet(layout); + auto const& count = layout->count; + auto const vklayout = layout->getVkLayout(); + VulkanDescriptorSet* set = mResourceAllocator->construct(handle, + mResourceAllocator, vkSet, [vkSet, count, vklayout, this](VulkanDescriptorSet* set) { + eraseSetFromHistory(set); + mDescriptorPool->recycle(count, vklayout, vkSet); + }); + mHistory[set] = std::make_unique(layout->bitmask.dynamicUbo, + layout->count.dynamicUbo, mResourceAllocator, set); } void VulkanDescriptorSetManager::destroySet(Handle handle) { - mImpl->destroySet(handle); + VulkanDescriptorSet* set = mResourceAllocator->handle_cast(handle); + eraseSetFromHistory(set); } void VulkanDescriptorSetManager::initVkLayout(VulkanDescriptorSetLayout* layout) { - mImpl->initVkLayout(layout); + layout->setVkLayout(mLayoutManager->getVkLayout(layout)); +} + +void VulkanDescriptorSetManager::eraseSetFromHistory(VulkanDescriptorSet* set) { + DescriptorSetHistory* history = mHistory[set].get(); + mHistory.erase(set); + + for (uint8_t i = 0; i < mStashedSets.size(); ++i) { + if (mStashedSets[i] == history) { + mStashedSets[i] = nullptr; + } + } } -}// namespace filament::backend +} // namespace filament::backend diff --git a/filament/backend/src/vulkan/caching/VulkanDescriptorSetManager.h b/filament/backend/src/vulkan/caching/VulkanDescriptorSetManager.h index 6d78f31b244..a98cc84b454 100644 --- a/filament/backend/src/vulkan/caching/VulkanDescriptorSetManager.h +++ b/filament/backend/src/vulkan/caching/VulkanDescriptorSetManager.h @@ -30,6 +30,8 @@ #include #include +#include + namespace filament::backend { // [GDSR]: Great-Descriptor-Set-Refactor: As of 03/20/24, the Filament frontend is planning to @@ -45,6 +47,7 @@ class VulkanDescriptorSetManager { using DescriptorSetLayoutArray = VulkanDescriptorSetLayout::DescriptorSetLayoutArray; VulkanDescriptorSetManager(VkDevice device, VulkanResourceAllocator* resourceAllocator); + ~VulkanDescriptorSetManager(); void terminate() noexcept; @@ -71,8 +74,43 @@ class VulkanDescriptorSetManager { void initVkLayout(VulkanDescriptorSetLayout* layout); private: - class Impl; - Impl* mImpl; + class DescriptorSetHistory; + class DescriptorSetLayoutManager; + class DescriptorInfinitePool; + + void eraseSetFromHistory(VulkanDescriptorSet* set); + + using DescriptorSetHistoryArray = + std::array; + + struct BoundInfo { + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + DescriptorSetMask setMask; + DescriptorSetHistoryArray boundSets; + + bool operator==(BoundInfo const& info) const { + if (pipelineLayout != info.pipelineLayout || setMask != info.setMask) { + return false; + } + bool equal = true; + setMask.forEachSetBit([&](size_t i) { + if (boundSets[i] != info.boundSets[i]) { + equal = false; + } + }); + return equal; + } + }; + + VkDevice mDevice; + VulkanResourceAllocator* mResourceAllocator; + std::unique_ptr mLayoutManager; + std::unique_ptr mDescriptorPool; + std::pair mInputAttachment; + std::unordered_map> mHistory; + DescriptorSetHistoryArray mStashedSets = {}; + + BoundInfo mLastBoundInfo; }; }// namespace filament::backend diff --git a/filament/backend/src/vulkan/platform/VulkanPlatform.cpp b/filament/backend/src/vulkan/platform/VulkanPlatform.cpp index 2e88542080f..6ddfb94bd31 100644 --- a/filament/backend/src/vulkan/platform/VulkanPlatform.cpp +++ b/filament/backend/src/vulkan/platform/VulkanPlatform.cpp @@ -740,6 +740,20 @@ Driver* VulkanPlatform::createDriver(void* sharedContext, context.mDebugMarkersSupported = setContains(deviceExts, VK_EXT_DEBUG_MARKER_EXTENSION_NAME); context.mMultiviewEnabled = setContains(deviceExts, VK_KHR_MULTIVIEW_EXTENSION_NAME); + // Check the availability of lazily allocated memory + { + context.mLazilyAllocatedMemorySupported = false; + const uint32_t typeCount = context.mMemoryProperties.memoryTypeCount; + for(uint32_t i = 0; i < typeCount; ++i) { + const VkMemoryType type = context.mMemoryProperties.memoryTypes[i]; + if (type.propertyFlags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) { + context.mLazilyAllocatedMemorySupported = true; + assert_invariant(type.propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + break; + } + } + } + #ifdef NDEBUG // If we are in release build, we should not have turned on debug extensions FILAMENT_CHECK_POSTCONDITION(!context.mDebugUtilsSupported && !context.mDebugMarkersSupported) diff --git a/filament/backend/src/vulkan/platform/VulkanPlatformSwapChainImpl.cpp b/filament/backend/src/vulkan/platform/VulkanPlatformSwapChainImpl.cpp index 6c643f754d8..d8cace97a62 100644 --- a/filament/backend/src/vulkan/platform/VulkanPlatformSwapChainImpl.cpp +++ b/filament/backend/src/vulkan/platform/VulkanPlatformSwapChainImpl.cpp @@ -31,22 +31,22 @@ namespace { std::tuple createImageAndMemory(VulkanContext const& context, VkDevice device, VkExtent2D extent, VkFormat format) { bool const isDepth = isVkDepthFormat(format); - // Filament expects blit() to work with any texture, so we almost always set these usage flags. - // TODO: investigate performance implications of setting these flags. - VkImageUsageFlags const blittable - = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; - VkImageCreateInfo imageInfo{ - .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, - .imageType = VK_IMAGE_TYPE_2D, - .format = format, - .extent = {extent.width, extent.height, 1}, - .mipLevels = 1, - .arrayLayers = 1, - .samples = VK_SAMPLE_COUNT_1_BIT, - .tiling = VK_IMAGE_TILING_OPTIMAL, - .usage = blittable - | (isDepth ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT - : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT), + // Filament expects blit() to work with any texture, so we almost always set these usage flags + // (see copyFrame() and readPixels()). + VkImageUsageFlags const blittable = + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + + VkImageCreateInfo imageInfo { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .imageType = VK_IMAGE_TYPE_2D, + .format = format, + .extent = {extent.width, extent.height, 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = blittable | (isDepth ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT + : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT), }; VkImage image; VkResult result = vkCreateImage(device, &imageInfo, VKALLOC, &image); @@ -220,7 +220,7 @@ VkResult VulkanPlatformSurfaceSwapChain::create() { .imageArrayLayers = 1, .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT - | VK_IMAGE_USAGE_TRANSFER_DST_BIT // Allows use as a blit destination. + | VK_IMAGE_USAGE_TRANSFER_DST_BIT // Allows use as a blit destination (for copyFrame) | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,// Allows use as a blit source (for readPixels) // TODO: Setting the preTransform to IDENTITY means we are letting the Android @@ -239,7 +239,7 @@ VkResult VulkanPlatformSurfaceSwapChain::create() { }; VkResult result = vkCreateSwapchainKHR(mDevice, &createInfo, VKALLOC, &mSwapchain); FILAMENT_CHECK_POSTCONDITION(result == VK_SUCCESS) - << "vkGetPhysicalDeviceSurfaceFormatsKHR error: " << static_cast(result); + << "vkCreateSwapchainKHR error: " << static_cast(result); mSwapChainBundle.colors = enumerate(vkGetSwapchainImagesKHR, mDevice, mSwapchain); mSwapChainBundle.colorFormat = surfaceFormat.format; diff --git a/filament/backend/test/test_Callbacks.cpp b/filament/backend/test/test_Callbacks.cpp index 0f02f1b2141..78f6fedf033 100644 --- a/filament/backend/test/test_Callbacks.cpp +++ b/filament/backend/test/test_Callbacks.cpp @@ -35,7 +35,7 @@ TEST_F(BackendTest, FrameScheduledCallback) { api.setFrameScheduledCallback(swapChain, nullptr, [&callbackCountA](PresentCallable callable) { callable(); callbackCountA++; - }); + }, 0); // Render the first frame. api.makeCurrent(swapChain, swapChain); @@ -58,7 +58,7 @@ TEST_F(BackendTest, FrameScheduledCallback) { api.setFrameScheduledCallback(swapChain, nullptr, [&callbackCountB](PresentCallable callable) { callable(); callbackCountB++; - }); + }, 0); // Render one final frame. api.makeCurrent(swapChain, swapChain); diff --git a/filament/backend/test/test_Handles.cpp b/filament/backend/test/test_Handles.cpp new file mode 100644 index 00000000000..22390b7acb7 --- /dev/null +++ b/filament/backend/test/test_Handles.cpp @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024 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 +#include +#include +#include "utils/Panic.h" + +using namespace filament::backend; + +// FIXME: consider making this constant non-private so we can use it in tests. +static constexpr uint32_t HANDLE_HEAP_FLAG = 0x80000000u; +static constexpr size_t POOL_SIZE_BYTES = 8 * 1024U * 1024U; +// NOTE: actual count may be lower due to alignment requirements +constexpr size_t const POOL_HANDLE_COUNT = POOL_SIZE_BYTES / (32 + 96 + 136); // 31775 + +// This must match HandleAllocatorGL, so its implementation is present on all platforms. +#define HandleAllocatorTest HandleAllocator<32, 96, 136> // ~4520 / pool / MiB + +struct MyHandle { +}; + +struct Concrete : public MyHandle { + uint8_t data[32]; +}; + +#if GTEST_HAS_EXCEPTIONS +#define EXPECT_THROW_IF_ENABLED EXPECT_THROW +#else +#define EXPECT_THROW_IF_ENABLED(args...) +#endif + +TEST(HandlesTest, useAfterFreePool) { + HandleAllocatorTest allocator("Test Handles", POOL_SIZE_BYTES, false); + + Handle handleA = allocator.allocate(); + allocator.deallocate(handleA); + + Handle handleB = allocator.allocate(); + + EXPECT_THROW_IF_ENABLED({ + Concrete* actualHandleA = allocator.handle_cast(handleA); + }, utils::PostconditionPanic); +} + +TEST(HandlesTest, useAfterFreeHeap) { + HandleAllocatorTest allocator("Test Handles", POOL_SIZE_BYTES, false); + + // Use up all the non-heap handles. + for (int i = 0; i < POOL_HANDLE_COUNT; i++) { + Handle handle = allocator.allocate(); + } + + // This one is guaranteed to be a heap handle. + Handle handleA = allocator.allocate(); + EXPECT_TRUE(handleA.getId() & HANDLE_HEAP_FLAG); + allocator.deallocate(handleA); + + Handle handleB = allocator.allocate(); + + // This should assert: + EXPECT_THROW_IF_ENABLED({ + Concrete* actualHandleA = allocator.handle_cast(handleA); + }, utils::PostconditionPanic); + + // simulate a "corrupted" handle + Handle corruptedHandle { HANDLE_HEAP_FLAG | 0x123456 }; + + EXPECT_THROW_IF_ENABLED({ + Concrete* actualHandleA = allocator.handle_cast(corruptedHandle); + }, utils::PostconditionPanic); +} + +TEST(HandlesTest, isValid) { + HandleAllocatorTest allocator("Test Handles", POOL_SIZE_BYTES, false); + Handle poolHandle = allocator.allocate(); + EXPECT_TRUE((poolHandle.getId() & HANDLE_HEAP_FLAG) == 0u); + + // Use up all the non-heap handles. + for (int i = 0; i < POOL_HANDLE_COUNT; i++) { + Handle handle = allocator.allocate(); + } + + // This one is guaranteed to be a heap handle. + Handle heapHandle = allocator.allocate(); + EXPECT_TRUE(heapHandle.getId() & HANDLE_HEAP_FLAG); + + EXPECT_TRUE(allocator.is_valid(poolHandle)); + EXPECT_TRUE(allocator.is_valid(heapHandle)); + + { + // null handles are invalid + Handle handle; + EXPECT_FALSE(allocator.is_valid(handle)); + } + { + Handle handle { HANDLE_HEAP_FLAG | 0x123456 }; + EXPECT_FALSE(allocator.is_valid(handle)); + } +} diff --git a/filament/include/filament/Engine.h b/filament/include/filament/Engine.h index 641c9846eda..311cf2e5a0c 100644 --- a/filament/include/filament/Engine.h +++ b/filament/include/filament/Engine.h @@ -288,16 +288,6 @@ class UTILS_PUBLIC Engine { */ uint32_t jobSystemThreadCount = 0; - /* - * 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; - /** * When uploading vertex or index data, the Filament Metal backend copies data * into a shared staging area before transferring it to the GPU. This setting controls @@ -390,8 +380,14 @@ class UTILS_PUBLIC Engine { * it's a GLES2 context. Ignored on other backends. */ bool forceGLES2Context = false; - }; + /** + * Assert the native window associated to a SwapChain is valid when calling makeCurrent(). + * This is only supported for: + * - PlatformEGLAndroid + */ + bool assertNativeWindowIsValid = false; + }; #if UTILS_HAS_THREADING using CreateCallback = void(void* UTILS_NULLABLE user, void* UTILS_NONNULL token); diff --git a/filament/include/filament/Renderer.h b/filament/include/filament/Renderer.h index a4e919610db..7e66c898c45 100644 --- a/filament/include/filament/Renderer.h +++ b/filament/include/filament/Renderer.h @@ -508,7 +508,7 @@ class UTILS_PUBLIC Renderer : public FilamentAPI { * * Framebuffer as seen on User buffer (PixelBufferDescriptor&) * screen - * + * * +--------------------+ * | | .stride .alignment * | | ----------------------->--> @@ -538,6 +538,9 @@ class UTILS_PUBLIC Renderer : public FilamentAPI { * uploaded to it via setImage, the data returned from readPixels will be y-flipped with respect * to the setImage call. * + * Note: the texture that backs the COLOR attachment for `renderTarget` must have + * TextureUsage::BLIT_SRC as part of its usage. + * * @remark * readPixels() is intended for debugging and testing. It will impact performance significantly. * diff --git a/filament/include/filament/SwapChain.h b/filament/include/filament/SwapChain.h index 6917507a5d6..9b7e9cfa18c 100644 --- a/filament/include/filament/SwapChain.h +++ b/filament/include/filament/SwapChain.h @@ -254,6 +254,19 @@ class UTILS_PUBLIC SwapChain : public FilamentAPI { void* UTILS_NULLABLE getNativeWindow() const noexcept; + /** + * If this flag is passed to setFrameScheduledCallback, then the behavior of the default + * CallbackHandler (when nullptr is passed as the handler argument) is altered to call the + * callback on the Metal completion handler thread (as opposed to the main Filament thread). + * This flag also instructs the Metal backend to release the associated CAMetalDrawable on the + * completion handler thread. + * + * This flag has no effect if a custom CallbackHandler is passed. + * + * @see setFrameScheduledCallback + */ + static constexpr uint64_t CALLBACK_DEFAULT_USE_METAL_COMPLETION_HANDLER = 1; + /** * FrameScheduledCallback is a callback function that notifies an application when Filament has * completed processing a frame and that frame is ready to be scheduled for presentation. @@ -298,7 +311,7 @@ class UTILS_PUBLIC SwapChain : public FilamentAPI { * @see PresentCallable */ void setFrameScheduledCallback(backend::CallbackHandler* UTILS_NULLABLE handler = nullptr, - FrameScheduledCallback&& callback = {}); + FrameScheduledCallback&& callback = {}, uint64_t flags = 0); /** * Returns whether or not this SwapChain currently has a FrameScheduledCallback set. diff --git a/filament/src/PostProcessManager.cpp b/filament/src/PostProcessManager.cpp index 98150556f6b..66aa50a7ed6 100644 --- a/filament/src/PostProcessManager.cpp +++ b/filament/src/PostProcessManager.cpp @@ -643,7 +643,7 @@ FrameGraphId PostProcessManager::ssr(FrameGraph& fg, auto out = resources.getRenderPassInfo(); // Remove the HAS_SHADOWING RenderFlags, since it's irrelevant when rendering reflections - passBuilder.renderFlags(~RenderPass::HAS_SHADOWING, 0); + passBuilder.renderFlags(RenderPass::HAS_SHADOWING, 0); // use our special SSR variant, it can only be applied to object that have // the SCREEN_SPACE ReflectionMode. @@ -3410,6 +3410,7 @@ FrameGraphId PostProcessManager::debugCombineArrayTexture(Fra float(vp.width) / inputDesc.width, float(vp.height) / inputDesc.height }); + mi->commit(driver); mi->use(driver); auto pipeline = material.getPipelineState(mEngine); diff --git a/filament/src/RenderPass.cpp b/filament/src/RenderPass.cpp index bec8e313275..dd0dd262617 100644 --- a/filament/src/RenderPass.cpp +++ b/filament/src/RenderPass.cpp @@ -655,12 +655,18 @@ RenderPass::Command* RenderPass::generateCommandsImpl(RenderPass::CommandTypeFla bool const hasSkinning = soaVisibility[i].skinning; bool const hasSkinningOrMorphing = hasSkinning || hasMorphing; - // if we are already an SSR variant, the SRE bit is already set, - // there is no harm setting it again + // if we are already an SSR variant, the SRE bit is already set static_assert(Variant::SPECIAL_SSR & Variant::SRE); - Variant renderableVariant = variant; - renderableVariant.setShadowReceiver( - Variant::isSSRVariant(variant) || (soaVisibility[i].receiveShadows & hasShadowing)); + Variant renderableVariant{ variant }; + + // we can't have SSR and shadowing together by construction + bool const isSsrVariant = Variant::isSSRVariant(variant); + assert_invariant((isSsrVariant && !hasShadowing) || !isSsrVariant); + if (!isSsrVariant) { + // set the SRE variant, unless we're in SSR mode + renderableVariant.setShadowReceiver(soaVisibility[i].receiveShadows && hasShadowing); + } + renderableVariant.setSkinning(hasSkinningOrMorphing); FRenderableManager::SkinningBindingInfo const& skinning = soaSkinning[i]; diff --git a/filament/src/ShadowMapManager.cpp b/filament/src/ShadowMapManager.cpp index 51b29065063..763e026db33 100644 --- a/filament/src/ShadowMapManager.cpp +++ b/filament/src/ShadowMapManager.cpp @@ -261,10 +261,15 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG auto& passList = data.passList; // Directional, cascaded shadow maps - auto const directionalShadowCastersRange = view.getVisibleDirectionalShadowCasters(); - if (!directionalShadowCastersRange.empty()) { + // if the view has shadowing enabled, the SRE variant could be used, so we must + // generate the shadow maps + if (view.needsDirectionalShadowMaps()) { + auto const directionalShadowCastersRange = view.getVisibleDirectionalShadowCasters(); for (auto& shadowMap : getCascadedShadowMap()) { - // for the directional light, we already know if it has visible shadows. + // For the directional light, we already know if it has visible shadows. + // if hasVisibleShadows() is false, we're guaranteed the shader won't + // sample this shadow map (see PerViewUib::cascades and + // ShadowMapManager::updateCascadeShadowMaps) if (shadowMap.hasVisibleShadows()) { passList.push_back({ {}, &shadowMap, directionalShadowCastersRange, @@ -274,8 +279,10 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG } // Point lights and Spotlight shadow maps - auto const spotShadowCastersRange = view.getVisibleSpotShadowCasters(); - if (!spotShadowCastersRange.empty()) { + // if the view has shadowing enabled, the SRE variant could be used, so we must + // generate the shadow maps + if (view.needsPointShadowMaps()) { + auto const spotShadowCastersRange = view.getVisibleSpotShadowCasters(); for (auto& shadowMap : getSpotShadowMaps()) { assert_invariant(!shadowMap.isDirectionalShadow()); @@ -293,11 +300,11 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG break; } - if (shadowMap.hasVisibleShadows()) { - passList.push_back({ - {}, &shadowMap, spotShadowCastersRange, - VISIBLE_DYN_SHADOW_RENDERABLE }); - } + // For spot/point lights, shadowMap.hasVisibleShadows() doesn't guarantee + // the shader won't access the shadow map, so we must generate it. + passList.push_back({ + {}, &shadowMap, spotShadowCastersRange, + VISIBLE_DYN_SHADOW_RENDERABLE }); } } @@ -311,7 +318,6 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG scene, mainCameraInfo, userTime, passBuilder = passBuilder]( FrameGraphResources const&, auto const& data, DriverApi& driver) mutable { - // Note: we could almost parallel_for the loop below, the problem currently is // that updatePrimitivesLod() updates temporary global state. // prepareSpotShadowMap() also update the visibility of renderable. These two @@ -321,7 +327,6 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG // Generate a RenderPass for each shadow map for (auto const& entry : data.passList) { ShadowMap const& shadowMap = *entry.shadowMap; - assert_invariant(shadowMap.hasVisibleShadows()); // Note: this loop can generate a lot of commands that come out of the // "per frame command arena". The allocation persists until the @@ -339,14 +344,18 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG // we should never be here break; case ShadowType::SPOT: - ShadowMapManager::cullSpotShadowMap(shadowMap, engine, view, - scene->getRenderableData(), entry.range, - scene->getLightData()); + if (shadowMap.hasVisibleShadows()) { + ShadowMapManager::cullSpotShadowMap(shadowMap, engine, view, + scene->getRenderableData(), entry.range, + scene->getLightData()); + } break; case ShadowType::POINT: - ShadowMapManager::cullPointShadowMap(shadowMap, view, - scene->getRenderableData(), entry.range, - scene->getLightData()); + if (shadowMap.hasVisibleShadows()) { + ShadowMapManager::cullPointShadowMap(shadowMap, view, + scene->getRenderableData(), entry.range, + scene->getLightData()); + } break; } @@ -420,22 +429,21 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG auto const& passList = prepareShadowPass.getData().passList; for (auto const& entry: passList) { - assert_invariant(entry.shadowMap->hasVisibleShadows()); - const uint8_t layer = entry.shadowMap->getLayer(); const auto* options = entry.shadowMap->getShadowOptions(); const auto msaaSamples = textureRequirements.msaaSamples; + const bool blur = entry.shadowMap->hasVisibleShadows() && + view.hasVSM() && options->vsm.blurWidth > 0.0f; auto& shadowPass = fg.addPass("Shadow Pass", [&](FrameGraph::Builder& builder, auto& data) { - const bool blur = view.hasVSM() && options->vsm.blurWidth > 0.0f; FrameGraphRenderPass::Descriptor renderTargetDesc{}; data.output = builder.createSubresource(prepareShadowPass->shadows, "Shadowmap Layer", { .layer = layer }); - if (view.hasVSM()) { + if (UTILS_UNLIKELY(view.hasVSM())) { // Each shadow pass has its own sample count, but textures are created with // a default count of 1 because we're using "magic resolve" (sample count is // set on the render target). @@ -447,13 +455,6 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG .format = TextureFormat::DEPTH16, }); - // Temporary (resolved) texture used to render the shadowmap when blurring - // is needed -- it'll be used as the source of the blur. - data.tempBlurSrc = builder.createTexture("Temporary Shadowmap", { - .width = textureRequirements.size, .height = textureRequirements.size, - .format = textureRequirements.format - }); - depth = builder.write(depth, FrameGraphTexture::Usage::DEPTH_ATTACHMENT); @@ -468,7 +469,15 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG renderTargetDesc.clearColor = vsmClearColor; renderTargetDesc.samples = msaaSamples; - if (blur) { + if (UTILS_UNLIKELY(blur)) { + // Temporary (resolved) texture used to render the shadowmap when blurring + // is needed -- it'll be used as the source of the blur. + data.tempBlurSrc = builder.createTexture("Temporary Shadowmap", { + .width = textureRequirements.size, + .height = textureRequirements.size, + .format = textureRequirements.format + }); + data.tempBlurSrc = builder.write(data.tempBlurSrc, FrameGraphTexture::Usage::COLOR_ATTACHMENT); @@ -510,15 +519,20 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG auto rt = resources.getRenderPassInfo(data.rt); driver.beginRenderPass(rt.target, rt.params); - entry.shadowMap->bind(driver); - entry.executor.overrideScissor(entry.shadowMap->getScissor()); - entry.executor.execute(engine, "Shadow Pass"); + // if we know there are no visible shadows, we can skip rendering, but + // we need the render-pass to clear/initialize the shadow-map + // Note: this is always true for directional/cascade shadows. + if (entry.shadowMap->hasVisibleShadows()) { + entry.shadowMap->bind(driver); + entry.executor.overrideScissor(entry.shadowMap->getScissor()); + entry.executor.execute(engine, "Shadow Pass"); + } driver.endRenderPass(); }); // now emit the blurring passes if needed - if (view.hasVSM()) { + if (UTILS_UNLIKELY(blur)) { auto& ppm = engine.getPostProcessManager(); const float blurWidth = options->vsm.blurWidth; diff --git a/filament/src/SwapChain.cpp b/filament/src/SwapChain.cpp index dd7db7bd011..66502931e79 100644 --- a/filament/src/SwapChain.cpp +++ b/filament/src/SwapChain.cpp @@ -29,8 +29,8 @@ void* SwapChain::getNativeWindow() const noexcept { } void SwapChain::setFrameScheduledCallback( - backend::CallbackHandler* handler, FrameScheduledCallback&& callback) { - downcast(this)->setFrameScheduledCallback(handler, std::move(callback)); + backend::CallbackHandler* handler, FrameScheduledCallback&& callback, uint64_t flags) { + downcast(this)->setFrameScheduledCallback(handler, std::move(callback), flags); } bool SwapChain::isFrameScheduledCallbackSet() const noexcept { diff --git a/filament/src/details/Engine.cpp b/filament/src/details/Engine.cpp index c6598409538..e62a94d0307 100644 --- a/filament/src/details/Engine.cpp +++ b/filament/src/details/Engine.cpp @@ -103,12 +103,12 @@ Engine* FEngine::create(Engine::Builder const& builder) { } DriverConfig const driverConfig{ .handleArenaSize = instance->getRequestedDriverHandleArenaSize(), - .textureUseAfterFreePoolSize = instance->getConfig().textureUseAfterFreePoolSize, .metalUploadBufferSizeBytes = instance->getConfig().metalUploadBufferSizeBytes, .disableParallelShaderCompile = instance->getConfig().disableParallelShaderCompile, .disableHandleUseAfterFreeCheck = instance->getConfig().disableHandleUseAfterFreeCheck, .forceGLES2Context = instance->getConfig().forceGLES2Context, - .stereoscopicType = instance->getConfig().stereoscopicType, + .stereoscopicType = instance->getConfig().stereoscopicType, + .assertNativeWindowIsValid = instance->getConfig().assertNativeWindowIsValid, }; instance->mDriver = platform->createDriver(sharedContext, driverConfig); @@ -701,12 +701,12 @@ int FEngine::loop() { DriverConfig const driverConfig { .handleArenaSize = getRequestedDriverHandleArenaSize(), - .textureUseAfterFreePoolSize = mConfig.textureUseAfterFreePoolSize, .metalUploadBufferSizeBytes = mConfig.metalUploadBufferSizeBytes, .disableParallelShaderCompile = mConfig.disableParallelShaderCompile, .disableHandleUseAfterFreeCheck = mConfig.disableHandleUseAfterFreeCheck, .forceGLES2Context = mConfig.forceGLES2Context, .stereoscopicType = mConfig.stereoscopicType, + .assertNativeWindowIsValid = mConfig.assertNativeWindowIsValid, }; mDriver = mPlatform->createDriver(mSharedGLContext, driverConfig); diff --git a/filament/src/details/RenderTarget.cpp b/filament/src/details/RenderTarget.cpp index 6c232e43e5c..53697057b7d 100644 --- a/filament/src/details/RenderTarget.cpp +++ b/filament/src/details/RenderTarget.cpp @@ -156,8 +156,8 @@ RenderTarget* RenderTarget::Builder::build(Engine& engine) { // ------------------------------------------------------------------------------------------------ FRenderTarget::FRenderTarget(FEngine& engine, const RenderTarget::Builder& builder) - : mSupportedColorAttachmentsCount(engine.getDriverApi().getMaxDrawBuffers()) { - + : mSupportedColorAttachmentsCount(engine.getDriverApi().getMaxDrawBuffers()), + mSupportsReadPixels(false) { std::copy(std::begin(builder.mImpl->mAttachments), std::end(builder.mImpl->mAttachments), std::begin(mAttachments)); @@ -189,6 +189,16 @@ FRenderTarget::FRenderTarget(FEngine& engine, const RenderTarget::Builder& build (TextureUsage::SAMPLEABLE | Texture::Usage::SUBPASS_INPUT))) { mSampleableAttachmentsMask |= targetBufferBit; } + + // readPixels() only applies to the color attachment that binds at index 0. + if (i == 0 && any(attachment.texture->getUsage() & TextureUsage::COLOR_ATTACHMENT)) { + + // TODO: the following will be changed to + // mSupportsReadPixels = + // any(attachment.texture->getUsage() & TextureUsage::BLIT_SRC); + // in a later filament version when clients have properly added the right usage. + mSupportsReadPixels = attachment.texture->hasBlitSrcUsage(); + } } } diff --git a/filament/src/details/RenderTarget.h b/filament/src/details/RenderTarget.h index 1b57b5acc9b..9fba1f60ff1 100644 --- a/filament/src/details/RenderTarget.h +++ b/filament/src/details/RenderTarget.h @@ -68,6 +68,10 @@ class FRenderTarget : public RenderTarget { bool hasSampleableDepth() const noexcept; + bool supportsReadPixels() const noexcept { + return mSupportsReadPixels; + } + private: friend class RenderTarget; static constexpr size_t ATTACHMENT_COUNT = MAX_SUPPORTED_COLOR_ATTACHMENTS_COUNT + 1u; @@ -76,6 +80,7 @@ class FRenderTarget : public RenderTarget { backend::TargetBufferFlags mAttachmentMask = {}; backend::TargetBufferFlags mSampleableAttachmentsMask = {}; const uint8_t mSupportedColorAttachmentsCount; + bool mSupportsReadPixels = false; }; FILAMENT_DOWNCAST(RenderTarget) diff --git a/filament/src/details/Renderer.cpp b/filament/src/details/Renderer.cpp index f056976833a..494486e2a14 100644 --- a/filament/src/details/Renderer.cpp +++ b/filament/src/details/Renderer.cpp @@ -455,6 +455,15 @@ void FRenderer::readPixels(uint32_t xoffset, uint32_t yoffset, uint32_t width, u void FRenderer::readPixels(FRenderTarget* renderTarget, uint32_t xoffset, uint32_t yoffset, uint32_t width, uint32_t height, backend::PixelBufferDescriptor&& buffer) { + + // TODO: change the following to an assert when client call sites have addressed the issue. + if (!renderTarget->supportsReadPixels()) { + utils::slog.w << "readPixels() must be called with a renderTarget with COLOR0 created with " + "TextureUsage::BLIT_SRC. This precondition will be asserted in a later " + "release of Filament." + << utils::io::endl; + } + RendererUtils::readPixels(mEngine.getDriverApi(), renderTarget->getHwHandle(), xoffset, yoffset, width, height, std::move(buffer)); } @@ -802,7 +811,7 @@ void FRenderer::renderJob(RootArenaScope& rootArenaScope, FView& view) { passBuilder.renderFlags(renderFlags); Variant variant; - variant.setDirectionalLighting(view.hasDirectionalLight()); + variant.setDirectionalLighting(view.hasDirectionalLighting()); variant.setDynamicLighting(view.hasDynamicLighting()); variant.setFog(view.hasFog()); variant.setVsm(view.hasShadowing() && view.getShadowType() != ShadowType::PCF); @@ -963,15 +972,18 @@ void FRenderer::renderJob(RootArenaScope& rootArenaScope, FView& view) { }); const auto picking = picking_; - if (view.hasPicking()) { struct PickingResolvePassData { FrameGraphId picking; }; - fg.addPass("Picking Resolve Pass", + fg.addPass( + "Picking Resolve Pass", [&](FrameGraph::Builder& builder, auto& data) { - data.picking = builder.read(picking, - FrameGraphTexture::Usage::COLOR_ATTACHMENT); + // Note that BLIT_SRC is needed because this texture will be read later (via + // readPixels()). + data.picking = + builder.read(picking, FrameGraphTexture::Usage::COLOR_ATTACHMENT | + FrameGraphTexture::Usage::BLIT_SRC); builder.declareRenderPass("Picking Resolve Target", { .attachments = { .color = { data.picking }} }); @@ -1197,7 +1209,7 @@ void FRenderer::renderJob(RootArenaScope& rootArenaScope, FView& view) { // Debug: CSM visualisation if (UTILS_UNLIKELY(engine.debug.shadowmap.visualize_cascades && - view.hasShadowing() && view.hasDirectionalLight())) { + view.hasShadowing() && view.hasDirectionalLighting())) { input = ppm.debugShadowCascades(fg, input, depth); } diff --git a/filament/src/details/SwapChain.cpp b/filament/src/details/SwapChain.cpp index 20589c38f14..90d0920991b 100644 --- a/filament/src/details/SwapChain.cpp +++ b/filament/src/details/SwapChain.cpp @@ -70,9 +70,10 @@ void FSwapChain::terminate(FEngine& engine) noexcept { } void FSwapChain::setFrameScheduledCallback( - backend::CallbackHandler* handler, FrameScheduledCallback&& callback) { + backend::CallbackHandler* handler, FrameScheduledCallback&& callback, uint64_t flags) { mFrameScheduledCallbackIsSet = bool(callback); - mEngine.getDriverApi().setFrameScheduledCallback(mHwSwapChain, handler, std::move(callback)); + mEngine.getDriverApi().setFrameScheduledCallback( + mHwSwapChain, handler, std::move(callback), flags); } bool FSwapChain::isFrameScheduledCallbackSet() const noexcept { diff --git a/filament/src/details/SwapChain.h b/filament/src/details/SwapChain.h index efe7483563e..e31bf670a36 100644 --- a/filament/src/details/SwapChain.h +++ b/filament/src/details/SwapChain.h @@ -79,7 +79,7 @@ class FSwapChain : public SwapChain { } void setFrameScheduledCallback( - backend::CallbackHandler* handler, FrameScheduledCallback&& callback); + backend::CallbackHandler* handler, FrameScheduledCallback&& callback, uint64_t flags); bool isFrameScheduledCallbackSet() const noexcept; diff --git a/filament/src/details/Texture.cpp b/filament/src/details/Texture.cpp index 2f405fd2ab3..176907345b7 100644 --- a/filament/src/details/Texture.cpp +++ b/filament/src/details/Texture.cpp @@ -79,6 +79,7 @@ struct Texture::BuilderDetails { Sampler mTarget = Sampler::SAMPLER_2D; InternalFormat mFormat = InternalFormat::RGBA8; Usage mUsage = Usage::NONE; + bool mHasBlitSrc = false; bool mTextureIsSwizzled = false; std::array mSwizzle = { Swizzle::CHANNEL_0, Swizzle::CHANNEL_1, @@ -183,6 +184,15 @@ Texture* Texture::Builder::build(Engine& engine) { } } + // TODO: remove in a future filament release. + // Clients might not have known that textures that are read need to have BLIT_SRC as usages. For + // now, we workaround the issue by making sure any color attachment can be the source of a copy + // for readPixels(). + mImpl->mHasBlitSrc = any(mImpl->mUsage & TextureUsage::BLIT_SRC); + if (!mImpl->mHasBlitSrc && any(mImpl->mUsage & TextureUsage::COLOR_ATTACHMENT)) { + mImpl->mUsage |= TextureUsage::BLIT_SRC; + } + const bool sampleable = bool(mImpl->mUsage & TextureUsage::SAMPLEABLE); const bool swizzled = mImpl->mTextureIsSwizzled; const bool imported = mImpl->mImportedId; @@ -232,6 +242,7 @@ FTexture::FTexture(FEngine& engine, const Builder& builder) { mLevelCount = builder->mLevels; mSwizzle = builder->mSwizzle; mTextureIsSwizzled = builder->mTextureIsSwizzled; + mHasBlitSrc = builder->mHasBlitSrc; bool const isImported = builder->mImportedId != 0; if (mTarget == SamplerType::SAMPLER_EXTERNAL && !isImported) { diff --git a/filament/src/details/Texture.h b/filament/src/details/Texture.h index 7c0234bb535..6bf04dae4ac 100644 --- a/filament/src/details/Texture.h +++ b/filament/src/details/Texture.h @@ -123,6 +123,11 @@ class FTexture : public Texture { bool textureHandleCanMutate() const noexcept; void updateLodRange(uint8_t level) noexcept; + // TODO: remove in a future filament release. See below for description. + inline bool hasBlitSrcUsage() const noexcept { + return mHasBlitSrc; + } + private: friend class Texture; struct LodRange { @@ -163,7 +168,13 @@ class FTexture : public Texture { bool mTextureIsSwizzled; Usage mUsage = Usage::DEFAULT; - // there is 7 bytes of padding here + + // TODO: remove in a future filament release. + // Indicates whether the user has set the TextureUsage::BLIT_SRC usage. This will be used to + // temporarily validate whether this texture can be used for readPixels. + bool mHasBlitSrc = false; + // there is 5 bytes of padding here + FStream* mStream = nullptr; // only needed for streaming textures }; diff --git a/filament/src/details/View.cpp b/filament/src/details/View.cpp index 8d23837ccac..a50b05c8e43 100644 --- a/filament/src/details/View.cpp +++ b/filament/src/details/View.cpp @@ -584,7 +584,7 @@ void FView::prepare(FEngine& engine, DriverApi& driver, RootArenaScope& rootAren // we also know if we have a directional light FLightManager::Instance const directionalLight = lightData.elementAt(0); - mHasDirectionalLight = directionalLight.isValid(); + mHasDirectionalLighting = directionalLight.isValid(); // As soon as prepareVisibleLight finishes, we can kick-off the froxelization if (hasDynamicLighting()) { diff --git a/filament/src/details/View.h b/filament/src/details/View.h index 0aeacd21b54..4f548ded94a 100644 --- a/filament/src/details/View.h +++ b/filament/src/details/View.h @@ -175,9 +175,17 @@ class FView : public View { utils::JobSystem::Job* getFroxelizerSync() const noexcept { return mFroxelizerSync; } void setFroxelizerSync(utils::JobSystem::Job* sync) noexcept { mFroxelizerSync = sync; } - bool hasDirectionalLight() const noexcept { return mHasDirectionalLight; } + // ultimately decides to use the DIR variant + bool hasDirectionalLighting() const noexcept { return mHasDirectionalLighting; } + + // ultimately decides to use the DYN variant bool hasDynamicLighting() const noexcept { return mHasDynamicLighting; } + + // ultimately decides to use the SRE variant bool hasShadowing() const noexcept { return mHasShadowing; } + + bool needsDirectionalShadowMaps() const noexcept { return mHasShadowing && mHasDirectionalLighting; } + bool needsPointShadowMaps() const noexcept { return mHasShadowing && mHasDynamicLighting; } bool needsShadowMap() const noexcept { return mNeedsShadowMap; } bool hasFog() const noexcept { return mFogOptions.enabled && mFogOptions.density > 0.0f; } bool hasVSM() const noexcept { return mShadowType == ShadowType::VSM; } @@ -573,7 +581,7 @@ class FView : public View { Range mVisibleDirectionalShadowCasters; Range mSpotLightShadowCasters; uint32_t mRenderableUBOSize = 0; - mutable bool mHasDirectionalLight = false; + mutable bool mHasDirectionalLighting = false; mutable bool mHasDynamicLighting = false; mutable bool mHasShadowing = false; mutable bool mNeedsShadowMap = false; diff --git a/ios/CocoaPods/Filament.podspec b/ios/CocoaPods/Filament.podspec index cbddfbcb0c2..c9eab0abb15 100644 --- a/ios/CocoaPods/Filament.podspec +++ b/ios/CocoaPods/Filament.podspec @@ -1,12 +1,12 @@ Pod::Spec.new do |spec| spec.name = "Filament" - spec.version = "1.55.0" + spec.version = "1.55.1" spec.license = { :type => "Apache 2.0", :file => "LICENSE" } spec.homepage = "https://google.github.io/filament" spec.authors = "Google LLC." spec.summary = "Filament is a real-time physically based rendering engine for Android, iOS, Windows, Linux, macOS, and WASM/WebGL." spec.platform = :ios, "11.0" - spec.source = { :http => "https://github.com/google/filament/releases/download/v1.55.0/filament-v1.55.0-ios.tgz" } + spec.source = { :http => "https://github.com/google/filament/releases/download/v1.55.1/filament-v1.55.1-ios.tgz" } # Fix linking error with Xcode 12; we do not yet support the simulator on Apple silicon. spec.pod_target_xcconfig = { diff --git a/libs/camutils/src/FreeFlightManipulator.h b/libs/camutils/src/FreeFlightManipulator.h index c20b2578295..e700713ee98 100644 --- a/libs/camutils/src/FreeFlightManipulator.h +++ b/libs/camutils/src/FreeFlightManipulator.h @@ -226,6 +226,8 @@ class FreeFlightManipulator : public Manipulator { void jumpToBookmark(const Bookmark& bookmark) override { Base::mEye = bookmark.flight.position; + mTargetEuler.x = bookmark.flight.pitch; + mTargetEuler.y = bookmark.flight.yaw; updateTarget(bookmark.flight.pitch, bookmark.flight.yaw); } diff --git a/test/renderdiff/README.md b/test/renderdiff/README.md new file mode 100644 index 00000000000..f90f2970636 --- /dev/null +++ b/test/renderdiff/README.md @@ -0,0 +1,17 @@ +# Rendering Difference Test + +We created a few scripts to run `gltf_viewer` and produce headless renderings. + +This is mainly useful for continuous integration where GPUs are generally not available on cloud +machines. To perform software rasterization, these scripts are centered around [Mesa]'s software +rasterizers, but nothing bars us from using another rasterizer like [SwiftShader]. Additionally, +we should be able to use GPUs where available (though this is more of a future work). + +The script `run.py` contains the core logic for taking input parameters (such as the test +description file) and then running gltf_viewer to produce the results. + +In the `test` directory is a list of test descriptions that are specified in json. Please see +`sample.json` to parse the structure. + +[Mesa]: https://docs.mesa3d.org +[SwiftShader]: https://github.com/google/swiftshader \ No newline at end of file diff --git a/test/renderdiff/parse_test_json.py b/test/renderdiff/parse_test_json.py new file mode 100644 index 00000000000..2cc260ffec3 --- /dev/null +++ b/test/renderdiff/parse_test_json.py @@ -0,0 +1,142 @@ +# Copyright (C) 2024 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. + +from utils import execute, ArgParseImpl + +import glob +from itertools import chain +import json +import sys +import os +from os import path + +def _is_list_of_strings(field): + return isinstance(field, list) and\ + all(isinstance(item, str) for item in field) + +def _is_string(s): + return isinstance(s, str) + +def _is_dict(s): + return isinstance(s, dict) + +class RenderingConfig(): + def __init__(self, data): + assert 'name' in data + assert _is_string(data['name']) + self.name = data['name'] + + assert 'rendering' in data + assert _is_dict(data['rendering']) + self.rendering = data['rendering'] + +class PresetConfig(RenderingConfig): + def __init__(self, data, existing_models): + RenderingConfig.__init__(self, data) + models = data.get('models') + if models: + assert _is_list_of_strings(models) + assert all(m in existing_models for m in models) + self.models = models + +class TestConfig(RenderingConfig): + def __init__(self, data, existing_models, presets): + RenderingConfig.__init__(self, data) + description = data.get('description') + if description: + assert _is_string(description) + self.description = description + + apply_presets = data.get('apply_presets') + rendering = {} + preset_models = [] + if apply_presets: + given_presets = {p.name: p for p in presets} + assert all((name in given_presets) for name in apply_presets) + for preset in apply_presets: + rendering.update(given_presets[preset].rendering) + preset_models += given_presets[preset].models + + assert 'rendering' in data + rendering.update(data['rendering']) + self.rendering = rendering + + models = data.get('models') + self.models = preset_models + if models: + assert _is_list_of_strings(models) + assert all(m in existing_models for m in models) + self.models = set(models + self.models) + + def to_filament_format(self): + json_out = { + 'name': self.name, + 'base': self.rendering + } + return json.dumps(json_out) + +class RenderTestConfig(): + def __init__(self, data): + assert 'name' in data + name = data['name'] + assert _is_string(name) + self.name = name + + assert 'backends' in data + backends = data['backends'] + assert _is_list_of_strings(backends) + self.backends = backends + + assert 'model_search_paths' in data + model_search_paths = data.get('model_search_paths') + assert _is_list_of_strings(model_search_paths) + assert all(path.isdir(p) for p in model_search_paths) + + model_paths = list( + chain(*(glob.glob(f'{d}/**/*.glb', recursive=True) for d in model_search_paths))) + # This flatten the output for glob.glob + self.models = {path.splitext(path.basename(model))[0]: model for model in model_paths} + + preset_data = data.get('presets') + presets = [] + if preset_data: + presets = [PresetConfig(p, self.models) for p in preset_data] + + assert 'tests' in data + self.tests = [TestConfig(t, self.models, presets) for t in data['tests']] + test_names = list([t.name for t in self.tests]) + + # We cannot have duplicate test names + assert len(test_names) == len(set(test_names)) + +def _remove_comments_from_json_txt(json_txt): + res = [] + for line in json_txt.split('\n'): + if '//' in line: + line = line.split('//')[0] + res.append(line) + return '\n'.join(res) + +def parse_test_config_from_path(config_path): + with open(config_path, 'r') as f: + json_txt = json.loads(_remove_comments_from_json_txt(f.read())) + return RenderTestConfig(json_txt) + + +if __name__ == "__main__": + parser = ArgParseImpl() + parser.add_argument('--test', help='Configuration of the test', required=True) + + args, _ = parser.parse_known_args(sys.argv[1:]) + test = parse_test_config_from_path(args.test) diff --git a/test/renderdiff/run.py b/test/renderdiff/run.py new file mode 100644 index 00000000000..17a8a7acb47 --- /dev/null +++ b/test/renderdiff/run.py @@ -0,0 +1,54 @@ +# Copyright (C) 2024 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. + +from utils import execute, ArgParseImpl + +from parse_test_json import parse_test_config_from_path +import sys +import os + +def run_test(gltf_viewer, pixel_test, output_dir, opengl_lib=None, vk_icd=None): + assert os.path.isdir(output_dir) + assert os.access(gltf_viewer, os.X_OK) + + for test in pixel_test.tests: + test_json_path = f'{output_dir}/{test.name}_simplified.json' + + with open(test_json_path, 'w') as f: + f.write(f'[{test.to_filament_format()}]') + + for backend in pixel_test.backends: + env = None + if backend == 'opengl' and opengl_lib and os.path.isdir(opengl_lib): + env = {'LD_LIBRARY_PATH': opengl_lib} + + for model in test.models: + model_path = pixel_test.models[model] + out_name = f'{test.name}_{model}_{backend}' + execute(f'{gltf_viewer} -a {backend} --batch={test_json_path} -e {model_path} --headless', + env=env, capture_output=False) + execute(f'mv -f {test.name}0.ppm {output_dir}/{out_name}.ppm', capture_output=False) + execute(f'mv -f {test.name}0.json {output_dir}/{test.name}.json', capture_output=False) + +if __name__ == "__main__": + parser = ArgParseImpl() + parser.add_argument('--test', help='Configuration of the test', required=True) + parser.add_argument('--gltf_viewer', help='Path to the gltf_viewer', required=True) + parser.add_argument('--output_dir', help='Output Directory', required=True) + parser.add_argument('--opengl_lib', help='Path to the folder containing OpenGL driver lib (for LD_LIBRARY_PATH)') + parser.add_argument('--vk_icd', help='Path to VK ICD file') + + args, _ = parser.parse_known_args(sys.argv[1:]) + test = parse_test_config_from_path(args.test) + run_test(args.gltf_viewer, test, args.output_dir, opengl_lib=args.opengl_lib, vk_icd=args.vk_icd) diff --git a/test/renderdiff/tests/presubmit.json b/test/renderdiff/tests/presubmit.json new file mode 100644 index 00000000000..78224d419ce --- /dev/null +++ b/test/renderdiff/tests/presubmit.json @@ -0,0 +1,34 @@ +{ + "name": "PresubmitPixelTests", + "backends": ["opengl"], + "model_search_paths": ["third_party/models"], + "presets": [ + { + "name": "Standard", + "models": ["lucy", "DamagedHelmet"], + "rendering": { + "viewer.cameraFocusDistance": 0, + "view.postProcessingEnabled": true + } + } + ], + "tests": [ + { + "name": "BloomFlare", + "description": "Testing bloom and flare", + "apply_presets": ["Standard"], + "rendering": { + "view.bloom.enabled": true, + "view.bloom.lensFlare": true + } + }, + { + "name": "MSAA", + "description": "Testing Multi-sample Anti-aliasing", + "apply_presets": ["Standard"], + "rendering": { + "view.msaa.enabled": true + } + } + ] +} diff --git a/test/renderdiff/tests/sample.json b/test/renderdiff/tests/sample.json new file mode 100644 index 00000000000..eb689d8102d --- /dev/null +++ b/test/renderdiff/tests/sample.json @@ -0,0 +1,34 @@ +{ + "name": "SampleTest" , // [required] + "backends": ["opengl"], // [required] Specifies the backend that will be used to render + // this test + "model_search_paths": ["third_party/models"], // [optional] Base iirectory from which we will glob for + // .glb files and try to match against names in the 'models' + // field. + "presets": [ // [optional] A list of preset configurations that tests can + // inherit from. + { + "name": "Standard", // [required] + "models": ["lucy", "DamagedHelmet"], // [optional] Base name for the gltf file used in the test. For + // example, source files are lucy.glb and DamagedHelmet.gltf + "rendering": { // [required] Rendering settings used in the test. The json format + "viewer.cameraFocusDistance": 0, // is taken from AutomationSpec in libs/viewer + "view.postProcessingEnabled": true + } + } + ], + "tests": [ // [required] List of test configurations + { + "name": "BloomFlare", // [required] + "description": "Testing bloom and flare", // [optional] + "apply_presets": ["Standard"], // [optional] Applies the preset in order. Item must be in + // 'presets' field in the top-level struct. + "model": [], // [optional] List of models used in this test. The list is + // *appended* onto the lists provided by the presets. + "rendering": { // [required] See description in 'presets' + "view.bloom.enabled": true, + "view.bloom.lensFlare": true + } + } + ] +} diff --git a/test/renderdiff/utils.py b/test/renderdiff/utils.py new file mode 100644 index 00000000000..f708d7a6596 --- /dev/null +++ b/test/renderdiff/utils.py @@ -0,0 +1,68 @@ +# Copyright (C) 2024 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. + +import subprocess +import os +import argparse +import sys + +def execute(cmd, + cwd=None, + capture_output=True, + stdin=None, + env=None, + raise_errors=False): + in_env = os.environ + in_env.update(env if env else {}) + home = os.environ['HOME'] + if f'{home}/bin' not in in_env['PATH']: + in_env['PATH'] = in_env['PATH'] + f':{home}/bin' + + stdout = subprocess.PIPE if capture_output else sys.stdout + stderr = subprocess.PIPE if capture_output else sys.stdout + output = '' + err_output = '' + return_code = -1 + kwargs = { + 'cwd': cwd, + 'env': in_env, + 'stdout': stdout, + 'stderr': stderr, + 'stdin': stdin, + 'universal_newlines': True + } + if capture_output: + process = subprocess.Popen(cmd.split(' '), **kwargs) + output, err_output = process.communicate() + return_code = process.returncode + else: + return_code = subprocess.call(cmd.split(' '), **kwargs) + + if return_code: + # Error + if raise_errors: + raise subprocess.CalledProcessError(return_code, cmd) + if output: + if type(output) != str: + try: + output = output.decode('utf-8').strip() + except UnicodeDecodeError as e: + print('cannot decode ', output, file=sys.stderr) + return return_code, (output if return_code == 0 else err_output) + +class ArgParseImpl(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write('error: %s\n' % message) + self.print_help() + sys.exit(1) diff --git a/test/renderdiff_tests.sh b/test/renderdiff_tests.sh new file mode 100755 index 00000000000..fc5b14fab04 --- /dev/null +++ b/test/renderdiff_tests.sh @@ -0,0 +1,45 @@ +# Copyright (C) 2024 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. + +#!/usr/bin/bash + +OUTPUT_DIR="$(pwd)/out/renderdiff_tests" +RENDERDIFF_TEST_DIR="$(pwd)/test/renderdiff" +TEST_UTILS_DIR="$(pwd)/test/utils" +MESA_DIR="$(pwd)/mesa/out/" +MESA_LIB_DIR="${MESA_DIR}/lib/x86_64-linux-gnu" + +function prepare_mesa() { + if [ ! -d ${MESA_LIB_DIR} ]; then + rm -rf mesa + bash ${TEST_UTILS_DIR}/get_mesa.sh + fi +} + +# Following steps are taken: +# - Get and build mesa +# - Build gltf_viewer +# - Run the python script that runs the test +# - Zip up the result + +set -e && set -x && prepare_mesa && \ + mkdir -p ${OUTPUT_DIR} && \ + ./build.sh -X ${MESA_DIR} -p desktop debug gltf_viewer && \ + python3 ${RENDERDIFF_TEST_DIR}/run.py \ + --gltf_viewer="$(pwd)/out/cmake-debug/samples/gltf_viewer" \ + --test=${RENDERDIFF_TEST_DIR}/tests/presubmit.json \ + --output_dir=${OUTPUT_DIR} \ + --opengl_lib=${MESA_LIB_DIR} + +unset MESA_LIB_DIR diff --git a/test/utils/get_mesa.sh b/test/utils/get_mesa.sh new file mode 100755 index 00000000000..575182846d0 --- /dev/null +++ b/test/utils/get_mesa.sh @@ -0,0 +1,38 @@ +# Copyright (C) 2024 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. + +#!/usr/bin/bash + +set -e +set -x + +sudo apt-get -y build-dep mesa + +git clone https://gitlab.freedesktop.org/mesa/mesa.git + +pushd . + +cd mesa + +git checkout mesa-23.2.1 + +mkdir -p out + +# -Dosmesa=true => builds OSMesa, which is an offscreen GL context +# -Dgallium-drivers=swrast => builds GL software rasterizer +# -Dvulkan-drivers=swrast => builds VK software rasterizer +meson setup builddir/ -Dprefix="$(pwd)/out" -Dosmesa=true -Dglx=xlib -Dgallium-drivers=swrast -Dvulkan-drivers=swrast +meson install -C builddir/ + +popd diff --git a/third_party/libsdl2/include/SDL_config.h b/third_party/libsdl2/include/SDL_config.h index 4070905ea05..637f32353fc 100644 --- a/third_party/libsdl2/include/SDL_config.h +++ b/third_party/libsdl2/include/SDL_config.h @@ -46,6 +46,10 @@ #include "SDL_config_linux_wayland.h" #elif defined(FILAMENT_SUPPORTS_X11) #include "SDL_config_linux_x11.h" +#elif defined(FILAMENT_SUPPORTS_OSMESA) +// We still want the same defines and libs as x11 but we won't actually +// open any windows. +#include "SDL_config_linux_x11.h" #endif #else /* This is a minimal configuration just to get SDL running on new platforms */ diff --git a/third_party/libsdl2/tnt/CMakeLists.txt b/third_party/libsdl2/tnt/CMakeLists.txt index 73f56a5077e..5f6c35536a8 100755 --- a/third_party/libsdl2/tnt/CMakeLists.txt +++ b/third_party/libsdl2/tnt/CMakeLists.txt @@ -103,7 +103,7 @@ if (LINUX) if (FILAMENT_SUPPORTS_WAYLAND) file(GLOB SRCS_LINUX_WAYLAND ${SRC_DIR}/video/wayland/*.c) list(APPEND SRCS ${SRCS_LINUX_WAYLAND}) - elseif(FILAMENT_SUPPORTS_X11) + elseif(FILAMENT_SUPPORTS_X11 OR FILAMENT_SUPPORTS_OSMESA) file(GLOB SRCS_LINUX_X11 ${SRC_DIR}/video/x11/*.c) list(APPEND SRCS ${SRCS_LINUX_X11}) endif() diff --git a/web/filament-js/package.json b/web/filament-js/package.json index 58c8ffdb202..97cfa9933a2 100644 --- a/web/filament-js/package.json +++ b/web/filament-js/package.json @@ -1,6 +1,6 @@ { "name": "filament", - "version": "1.55.0", + "version": "1.55.1", "description": "Real-time physically based rendering engine", "main": "filament.js", "module": "filament.js",