Compare commits

...

11 Commits

Author SHA1 Message Date
bridgewaterrobbie
4a77d03d02 Try to handle non-transient attatchment supporting GPUs 2025-05-09 13:22:19 -04:00
Powei Feng
5cb96e5732 Clean up ndk version in build.sh (#8717)
Follow up to #8663
2025-05-09 15:51:30 +00:00
Matthew Hoffman
927aa57a4e Document ASAN (with leak detection) on MacOS (#8716)
* Revert "Optional CMake flag for enabling ASAN for backend and its tests. (#8696)"

This reverts commit 543b93939a.
There were other already existing ways to achieve this without the need for new flags.

* Add documentation on running with ASAN and leak detection on mac.

BUGS=[398198310]
2025-05-09 10:22:29 -05:00
Powei Feng
36e775902d renderdiff: script for updating golden images (#8709)
Adding a python script to enable updating new goldens into
a staging branch in the golden repo (filament-assets).

The same script can be used in github workflow to automatically
create a golden staging branch. This will be useful for users
without access to a mac (the only platform for generating
goldens as of now).
2025-05-08 22:07:17 +00:00
Powei Feng
53e28f3b33 github: fix release mac build (#8713) 2025-05-08 11:44:08 -07:00
Powei Feng
7ccbdb4633 Release Filament 1.59.5 2025-05-08 09:06:44 -07:00
Daisuke Kasuga
f10d226565 Add getter functions for settings to build ColorGrading object (#8699)
* add getter methods for ColorGrading builder settings
* add tone mapper impl clone funcs
* update the release notes
---------

Co-authored-by: Daisuke Kasuga <dkasuga@google.com>
2025-05-09 00:43:40 +09:00
Powei Feng
28ecf5c35d renderdiff: add golden repo support (#8689)
- Add GoldenManager to manage access to the repo containing the
  goldens
- Add tif comparison code
- Enable comparison by default for actual test
2025-05-07 21:20:22 +00:00
Matthew Hoffman
9683eb649c Backend test python script updated to display side by side images. (#8695) 2025-05-07 20:48:55 +00:00
Powei Feng
3d10ae3ee3 github: more build file refactoring (#8678)
- Move emscripten download into its own script
 - Refactor the common "CI choice" prompt into its own file.
 - Move the content of `build/common/ci-common.sh` to the
   "CI choice" script.
 - Mention the get-emscripten.sh script in BUILDING.md
2025-05-07 19:39:11 +00:00
rafadevai
44a75dd44b VK: Add a new platform method to check UMA (#8704)
* VK: Add a new context method to check UMA

This heuristic should work on almost all devices,
can be extended later as needed.

* Addressing PR comments

---------

Co-authored-by: Powei Feng <powei@google.com>
Co-authored-by: Serge Metral <sergemetral@google.com>
2025-05-07 12:08:34 -07:00
54 changed files with 1261 additions and 342 deletions

View File

@@ -3,5 +3,5 @@ runs:
using: "composite"
steps:
- name: Set up dependency versions
shell: bash
shell: bash
run: cat ./build/common/versions >> $GITHUB_ENV

16
.github/actions/web-prereq/action.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: 'Web Preqrequisites'
runs:
using: "composite"
steps:
- uses: ./.github/actions/dep-versions
- name: Cache EMSDK
id: emsdk-cache
uses: actions/cache@v4 # Use a specific version
with:
path: emsdk
key: ${{ runner.os }}-emsdk-${{ env.GITHUB_EMSDK_VERSION }}
- name: Install Web Prerequisites
shell: bash
run: |
bash ./build/common/get-emscripten.sh
echo "EMSDK=$PWD/emsdk" >> $GITHUB_ENV

View File

@@ -96,6 +96,7 @@ jobs:
with:
fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- uses: ./.github/actions/web-prereq
- name: Run build script
run: |
cd build/web && printf "y" | ./build.sh presubmit
@@ -123,13 +124,15 @@ jobs:
- uses: ./.github/actions/mac-prereq
- name: Cache Mesa and deps
id: mesa-cache
uses: actions/cache@v4 # Use a specific version
uses: actions/cache@v4
with:
path: mesa
key: ${{ runner.os }}-mesa-deps-2-${{ vars.MESA_VERSION }}
- name: Get Mesa
id: mesa-prereq
run: bash test/utils/get_mesa.sh
- name: Prerequisites
id: prereqs
run: |
bash test/utils/get_mesa.sh
pip install tifffile numpy
- name: Run Test
run: bash test/renderdiff/test.sh
- uses: actions/upload-artifact@v4
@@ -150,7 +153,7 @@ jobs:
- name: Run test
run: ./out/cmake-debug/libs/filamat/test_filamat --gtest_filter=MaterialCompiler.Wgsl*
code-correcteness:
code-correctness:
name: code-correctness
runs-on: 'macos-14-xlarge'
steps:

View File

@@ -65,13 +65,9 @@ jobs:
build-mac:
name: build-mac
runs-on: ${{ matrix.os }}
runs-on: macos-14-xlarge
if: github.event_name == 'release' || github.event.inputs.platform == 'desktop'
strategy:
matrix:
os: [macos-14-xlarge, ubuntu-22.04-32core]
steps:
- name: Decide Git ref
id: git_ref
@@ -118,6 +114,7 @@ jobs:
with:
ref: ${{ steps.git_ref.outputs.ref }}
- uses: ./.github/actions/linux-prereq
- uses: ./.github/actions/web-prereq
- name: Run build script
env:
TAG: ${{ steps.git_ref.outputs.tag }}

View File

@@ -17,6 +17,7 @@ jobs:
with:
fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- uses: ./.github/actions/web-prereq
- name: Run build script
run: |
cd build/web && printf "y" | ./build.sh continuous

View File

@@ -363,6 +363,8 @@ python ./emsdk.py activate latest
source ./emsdk_env.sh
```
Alternatively, you can try running the script `build/common/get-emscripten.sh`.
After this you can invoke the [easy build](#easy-build) script as follows:
```shell

View File

@@ -7,5 +7,3 @@ for next branch cut* header.
appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md).
## Release notes for next branch cut
- materials: remove dependence on per-view descset layout from filamat. [⚠️ **New Material Version**]

View File

@@ -31,7 +31,7 @@ repositories {
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.59.4'
implementation 'com.google.android.filament:filament-android:1.59.5'
}
```
@@ -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.59.4'
pod 'Filament', '~> 1.59.5'
```
## Documentation

View File

@@ -7,6 +7,11 @@ 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.60.0
- materials: remove dependence on per-view descset layout from filamat. [⚠️ **New Material Version**]
- `ColorGrading::Builder::toneMapper` now takes a `shared_ptr<ToneMapper>`
## v1.59.5

View File

@@ -18,6 +18,7 @@
#include <filament/ColorGrading.h>
#include <filament/ToneMapper.h>
#include <memory>
#include <math/vec3.h>
#include <math/vec4.h>
@@ -67,8 +68,9 @@ extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_ColorGrading_nBuilderToneMapper(JNIEnv*, jclass,
jlong nativeBuilder, jlong toneMapper_) {
ColorGrading::Builder* builder = (ColorGrading::Builder*) nativeBuilder;
const ToneMapper* toneMapper = (const ToneMapper*) toneMapper_;
builder->toneMapper(toneMapper);
ToneMapper* toneMapper = reinterpret_cast<ToneMapper*>(toneMapper_);
std::shared_ptr<ToneMapper> toneMapperCopy(toneMapper->clone());
builder->toneMapper(toneMapperCopy);
}
#pragma clang diagnostic push

View File

@@ -214,9 +214,9 @@ public class ColorGrading {
*
* The default tone mapping operator is {@link ToneMapper.ACESLegacy}.
*
* The specified tone mapper must have a lifecycle that exceeds the lifetime of
* this builder. Since the build(Engine&) method is synchronous, it is safe to
* delete the tone mapper object after that finishes executing.
* The copy of the specified tone mapper is set to this builder. It is safe to delete
* the original tone mapper object while the copied one is held by the built ColorGrading
* object.
*
* @param toneMapper The tone mapping operator to apply to the HDR color buffer
*

View File

@@ -1,5 +1,5 @@
GROUP=com.google.android.filament
VERSION_NAME=1.59.4
VERSION_NAME=1.59.5
POM_DESCRIPTION=Real-time physically based rendering engine for Android.

View File

@@ -151,7 +151,7 @@ function print_fgviewer_help {
}
# Unless explicitly specified, NDK version will be selected as highest available version within same major release chain
FILAMENT_NDK_VERSION=${FILAMENT_NDK_VERSION:-$(cat `dirname $0`/build/android/ndk.version | cut -f 1 -d ".")}
FILAMENT_NDK_VERSION=${FILAMENT_NDK_VERSION:-$(cat `dirname $0`/build/common/versions | grep GITHUB_NDK_VERSION | cut -f 1 -d ".")}
# Requirements
CMAKE_MAJOR=3

View File

@@ -1,28 +1,6 @@
#!/bin/bash
# Usage: the first argument selects the build type:
# - release, to build release only
# - debug, to build debug only
# - continuous, to build release and debug
# - presubmit, for presubmit builds
#
# The default is release
echo "This script is intended to run in a CI environment and may modify your current environment."
echo "Please refer to BUILDING.md for more information."
read -r -p "Do you wish to proceed (y/n)? " choice
case "${choice}" in
y|Y)
echo "Build will proceed..."
;;
n|N)
exit 0
;;
*)
exit 0
;;
esac
source `dirname $0`/../common/ci-check.sh
set -e
set -x
@@ -30,11 +8,6 @@ set -x
UNAME=`echo $(uname)`
LC_UNAME=`echo $UNAME | tr '[:upper:]' '[:lower:]'`
# build-common.sh will generate the following variables:
# $GENERATE_ARCHIVES
# $BUILD_DEBUG
# $BUILD_RELEASE
source `dirname $0`/../common/ci-common.sh
source `dirname $0`/../common/build-common.sh
if [[ "$GITHUB_WORKFLOW" ]]; then

View File

@@ -1,5 +1,20 @@
#!/bin/bash
# build-common.sh will generate the following variables:
# $GENERATE_ARCHIVES
# $BUILD_DEBUG
# $BUILD_RELEASE
# Typically a build script (build.sh) would source this script. For example,
# source `dirname $0`/../common/build-common.sh
# Usage: the first argument selects the build type:
# - release, to build release only
# - debug, to build debug only
# - continuous, to build release and debug
# - presubmit, for presubmit builds
#
# The default is release
if [[ ! "$TARGET" ]]; then
if [[ "$1" ]]; then
TARGET=$1

19
build/common/ci-check.sh Normal file
View File

@@ -0,0 +1,19 @@
echo "This script is intended to run in a CI environment and may modify your current environment."
echo "Please refer to BUILDING.md for more information."
read -r -p "Do you wish to proceed (y/n)? " choice
case "${choice}" in
y|Y)
echo "Build will proceed..."
;;
n|N)
exit 0
;;
*)
exit 0
;;
esac
if [[ "$GITHUB_WORKFLOW" ]]; then
echo "Running workflow $GITHUB_WORKFLOW (event: $GITHUB_EVENT_NAME, action: $GITHUB_ACTION)"
fi

View File

@@ -1,5 +0,0 @@
#!/bin/bash
if [[ "$GITHUB_WORKFLOW" ]]; then
echo "Running workflow $GITHUB_WORKFLOW (event: $GITHUB_EVENT_NAME, action: $GITHUB_ACTION)"
fi

22
build/common/get-emscripten.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
if [ -d "./emsdk" ]; then
echo "emsdk folder found. Assume emsdk has been installed."
cd emsdk
./emsdk activate latest
source ./emsdk_env.sh
export EMSDK="$PWD"
cd ..
exit 0
fi
# Install emscripten.
EMSDK_VERSION=${GITHUB_EMSDK_VERSION-3.1.60}
curl -L https://github.com/emscripten-core/emsdk/archive/refs/tags/${EMSDK_VERSION}.zip > emsdk.zip
unzip emsdk.zip ; mv emsdk-* emsdk ; cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
export EMSDK="$PWD"
cd ..

View File

@@ -3,4 +3,5 @@ GITHUB_CMAKE_VERSION=3.19.5
GITHUB_NINJA_VERSION=1.10.2
GITHUB_MESA_VERSION=24.2.1
GITHUB_LLVM_VERSION=16
GITHUB_NDK_VERSION=27.0.11718014
GITHUB_NDK_VERSION=27.0.11718014
GITHUB_EMSDK_VERSION=3.1.60

View File

@@ -1,35 +1,11 @@
#!/bin/bash
# Usage: the first argument selects the build type:
# - release, to build release only
# - debug, to build debug only
# - continuous, to build release and debug
# - presubmit, for presubmit builds
#
# The default is release
echo "This script is intended to run in a CI environment and may modify your current environment."
echo "Please refer to BUILDING.md for more information."
read -r -p "Do you wish to proceed (y/n)? " choice
case "${choice}" in
y|Y)
echo "Build will proceed..."
;;
n|N)
exit 0
;;
*)
exit 0
;;
esac
source `dirname $0`/../common/ci-check.sh
set -e
set -x
source `dirname $0`/../common/ci-common.sh
source `dirname $0`/../common/build-common.sh
pushd `dirname $0`/../.. > /dev/null
# If we're generating an archive for release or continuous builds, then we'll also build for the

View File

@@ -1,38 +1,11 @@
#!/bin/bash
# Usage: the first argument selects the build type:
# - release, to build release only
# - debug, to build debug only
# - continuous, to build release and debug
# - presubmit, for presubmit builds
#
# The default is release
echo "This script is intended to run in a CI environment and may modify your current environment."
echo "Please refer to BUILDING.md for more information."
read -r -p "Do you wish to proceed (y/n)? " choice
case "${choice}" in
y|Y)
echo "Build will proceed..."
;;
n|N)
exit 0
;;
*)
exit 0
;;
esac
source `dirname $0`/../common/ci-check.sh
set -e
set -x
# build-common.sh will generate the following variables:
# $GENERATE_ARCHIVES
# $BUILD_DEBUG
# $BUILD_RELEASE
source `dirname $0`/../common/ci-common.sh
source `dirname $0`/../common/build-common.sh
pushd `dirname $0`/../.. > /dev/null
./build.sh -c $RUN_TESTS $GENERATE_ARCHIVES $BUILD_DEBUG $BUILD_RELEASE

View File

@@ -1,34 +1,11 @@
#!/bin/bash
# Usage: the first argument selects the build type:
# - release, to build release only
# - debug, to build debug only
# - continuous, to build release and debug
# - presubmit, for presubmit builds
#
# The default is release
echo "This script is intended to run in a CI environment and may modify your current environment."
echo "Please refer to BUILDING.md for more information."
read -r -p "Do you wish to proceed (y/n)? " choice
case "${choice}" in
y|Y)
echo "Build will proceed..."
;;
n|N)
exit 0
;;
*)
exit 0
;;
esac
source `dirname $0`/../common/ci-check.sh
set -e
set -x
source `dirname $0`/../common/ci-common.sh
source `dirname $0`/../common/build-common.sh
pushd `dirname $0`/../.. > /dev/null
./build.sh -c $RUN_TESTS $GENERATE_ARCHIVES $BUILD_DEBUG $BUILD_RELEASE

View File

@@ -1,34 +1,10 @@
#!/bin/bash
# Usage: the first argument selects the build type:
# - release, to build release only
# - debug, to build debug only
# - continuous, to build release and debug
# - presubmit, for presubmit builds
#
# The default is release
echo "This script is intended to run in a CI environment and may modify your current environment."
echo "Please refer to BUILDING.md for more information."
read -r -p "Do you wish to proceed (y/n)? " choice
case "${choice}" in
y|Y)
echo "Build will proceed..."
;;
n|N)
exit 0
;;
*)
exit 0
;;
esac
source `dirname $0`/../common/ci-check.sh
set -e
set -x
source `dirname $0`/../common/ci-common.sh
source `dirname $0`/ci-common.sh
source `dirname $0`/../common/build-common.sh
pushd `dirname $0`/../.. > /dev/null

View File

@@ -1,11 +0,0 @@
#!/bin/bash
# Install emscripten.
curl -L https://github.com/emscripten-core/emsdk/archive/refs/tags/3.1.60.zip > emsdk.zip
unzip emsdk.zip ; mv emsdk-* emsdk ; cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
export EMSDK="$PWD"
cd ..

View File

@@ -18,6 +18,7 @@
- [Metal](./notes/metal_debugging.md)
- [Vulkan](./notes/vulkan_debugging.md)
- [SPIR-V](./notes/spirv_debugging.md)
- [Running with ASAN and UBSAN](./notes/asan_ubsan.md)
- [Libraries](./notes/libs.md)
- [bluegl](./dup/bluegl.md)
- [bluevk](./dup/bluevk.md)

View File

@@ -0,0 +1,41 @@
# Running with ASAN/UBSAN
## Enabling
When building though build.sh, pass the `-b` flag. This sets the cmake variable
`FILAMENT_ENABLE_ASAN_UBSAN=ON` which eventually passes `"-fsanitize=address -fsanitize=undefined"`
to all compile and link operations.
If building through CMake directly, or an IDE like CLion that doesn't use build.sh, instead pass
`-DFILAMENT_ENABLE_ASAN_UBSAN=ON` to cmake in order to get the same result.
## Getting memory leak detection on Mac
Memory leak detection isn't enabled by default on MacOS. There are two issues to address, first is
using a version of clang that supports memory leak detection and second is enabling it at runtime.
The version of clang distributed by Apple (with a version like "Apple clang version 16.0.0") doesn't
currently support leak detection at all. Instead you will need to get or build a different LLVM,
such as the one distributed through homebrew and get CMake to use that instead.
Then during runtime you'll need to have the environment variable `ASAN_OPTIONS` include the option
`detect_leaks=1`. Multiple `ASAN_OPTIONS` values are concatenated with `:`.
## Getting memory leak output in CLion
### Setting variables
Under `Settings | Build, Execution, Deployment | Dynamic Analysis Tools | Sanitizers` there is an
ASAN Settings field that overrides whatever other `ASAN_OPTIONS` you might set elsewhere, so you
must use that instead of setting it through your Run/Debug Configuration.
To pass `-DFILAMENT_ENABLE_ASAN_UBSAN=ON` to CMake you'll want to create a new CMake Profile and
pass it as a CMake argument.
### Avoiding losing output
CMake will consume ASAN output and display it through a separate "Sanitizers" tab. Unfortunately
certain leak detection errors that interrupt the executable seem to not show up in this tab, but are
still removed from the user-visible console output. If this is happening and you need to see the
unfiltered console output you'll need to go to `Settings | Build, Execution, Deployment | Dynamic
Analysis Tools | Sanitizers` and uncheck "Use visual representation for Sanitizer's output".

View File

@@ -5,18 +5,6 @@ set(TARGET backend)
set(PUBLIC_HDR_DIR include)
set(GENERATION_ROOT ${CMAKE_CURRENT_BINARY_DIR})
# ==================================================================================================
# Compilation options
# ==================================================================================================
#
set(BACKEND_SANITIZATION "" CACHE STRING "Sanitization option")
set_property(CACHE BACKEND_SANITIZATION PROPERTY STRINGS ";ASAN")
set(BACKEND_SANITIZERS)
if (BACKEND_SANITIZATION STREQUAL "ASAN")
set(BACKEND_SANITIZERS -fsanitize=address)
endif()
# ==================================================================================================
# Sources and headers
# ==================================================================================================
@@ -484,7 +472,6 @@ target_compile_options(${TARGET} PRIVATE
${OSMESA_COMPILE_FLAGS}
$<$<CONFIG:Release>:${OPTIMIZATION_FLAGS}>
$<$<AND:$<PLATFORM_ID:Darwin>,$<CONFIG:Release>>:${DARWIN_OPTIMIZATION_FLAGS}>
${BACKEND_SANITIZERS}
)
if (FILAMENT_SUPPORTS_METAL)
@@ -495,8 +482,6 @@ if (FILAMENT_SUPPORTS_WEBGPU)
target_compile_definitions(${TARGET} PRIVATE $<$<BOOL:${FILAMENT_WEBGPU_IMMEDIATE_ERROR_HANDLING}>:FILAMENT_WEBGPU_IMMEDIATE_ERROR_HANDLING>)
endif()
target_link_options(${TARGET} PRIVATE ${BACKEND_SANITIZERS})
target_link_libraries(${TARGET} PRIVATE
${OSMESA_LINKER_FLAGS}
$<$<AND:$<PLATFORM_ID:Linux>,$<CONFIG:Release>>:${LINUX_LINKER_OPTIMIZATION_FLAGS}>
@@ -566,8 +551,6 @@ if (APPLE AND NOT IOS)
test/test_RenderExternalImage.cpp)
add_library(backend_test STATIC ${BACKEND_TEST_SRC})
target_link_libraries(backend_test PUBLIC ${BACKEND_TEST_LIBS})
target_compile_options(backend_test PRIVATE ${BACKEND_SANITIZERS})
target_link_options(backend_test PRIVATE ${BACKEND_SANITIZERS})
set(BACKEND_TEST_DEPS
OSDependent
@@ -606,7 +589,6 @@ if (APPLE AND NOT IOS)
# linker from removing "unused" symbols.
target_link_libraries(backend_test_mac PRIVATE -force_load backend_test)
set_target_properties(backend_test_mac PROPERTIES FOLDER Tests)
target_link_options(backend_test_mac PRIVATE ${BACKEND_SANITIZERS})
# This is needed after XCode 15.3
set_target_properties(backend_test_mac PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE)
@@ -616,8 +598,6 @@ endif()
if (LINUX)
add_executable(backend_test_linux test/linux_runner.cpp ${BACKEND_TEST_SRC})
target_compile_options(backend_test_linux PRIVATE ${BACKEND_SANITIZERS})
target_link_options(backend_test_linux PRIVATE ${BACKEND_SANITIZERS})
target_link_libraries(backend_test_linux PRIVATE ${BACKEND_TEST_LIBS})
set_target_properties(backend_test_linux PROPERTIES FOLDER Tests)
endif()

View File

@@ -142,6 +142,10 @@ public:
return mPortabilitySubsetFeatures.imageView2DOn3DImage == VK_TRUE;
}
inline bool isUnifiedMemoryArchitecture() const noexcept {
return mIsUnifiedMemoryArchitecture;
}
private:
VkPhysicalDeviceMemoryProperties mMemoryProperties = {};
VkPhysicalDeviceProperties2 mPhysicalDeviceProperties = {
@@ -164,6 +168,7 @@ private:
bool mDebugUtilsSupported = false;
bool mLazilyAllocatedMemorySupported = false;
bool mProtectedMemorySupported = false;
bool mIsUnifiedMemoryArchitecture = false;
fvkutils::VkFormatList mDepthStencilFormats;
fvkutils::VkFormatList mBlittableDepthStencilFormats;

View File

@@ -631,6 +631,21 @@ fvkutils::VkFormatList findBlittableDepthStencilFormats(VkPhysicalDevice device)
return ret;
}
/**
* Check if the GPU has a unified memory architecture.
*/
bool hasUnifiedMemoryArchitecture(VkPhysicalDeviceMemoryProperties memoryProperties) noexcept {
// Try to identify if the platform is running on a Unified Memory Architecture by inspecting the
// memory heap flags, if they are all VK_MEMORY_HEAP_DEVICE_LOCAL_BIT it's UMA, otherwise not
// enough information to make a decision, so default to false.
for (uint32_t i = 0; i < memoryProperties.memoryHeapCount; ++i) {
if ((memoryProperties.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) {
return false;
}
}
return true;
}
}// anonymous namespace
using SwapChainPtr = VulkanPlatform::SwapChainPtr;
@@ -864,6 +879,8 @@ Driver* VulkanPlatform::createDriver(void* sharedContext,
}
}
context.mIsUnifiedMemoryArchitecture = hasUnifiedMemoryArchitecture(context.mMemoryProperties);
#ifdef NDEBUG
// If we are in release build, we should not have turned on debug extensions
FILAMENT_CHECK_POSTCONDITION(!context.mDebugUtilsSupported && !context.mDebugMarkersSupported)

View File

@@ -514,7 +514,7 @@ WGPUTexture::WGPUTexture(SamplerType target, uint8_t levels, TextureFormat forma
"the spec. See https://www.w3.org/TR/webgpu/#texture-creation or "
"https://gpuweb.github.io/gpuweb/#multisample-state");
// First, the texture aspect, starting with the defaults/basic configuration
mUsage = fToWGPUTextureUsage(usage);
mUsage = fToWGPUTextureUsage(usage, device.HasFeature(wgpu::FeatureName::TransientAttachments));
mFormat = fToWGPUTextureFormat(format);
wgpu::TextureDescriptor textureDescriptor{
.label = getUserTextureLabel(target),
@@ -566,7 +566,8 @@ WGPUTexture::WGPUTexture(WGPUTexture* src, uint8_t baseLevel, uint8_t levelCount
mTexView = makeTextureView(baseLevel, levelCount, target);
}
wgpu::TextureUsage WGPUTexture::fToWGPUTextureUsage(const TextureUsage& fUsage) {
wgpu::TextureUsage WGPUTexture::fToWGPUTextureUsage(const TextureUsage& fUsage,
const bool supportsTransientAttachment) {
wgpu::TextureUsage retUsage = wgpu::TextureUsage::None;
// Basing this mapping off of VulkanTexture.cpp's getUsage func and suggestions from Gemini
@@ -592,6 +593,7 @@ wgpu::TextureUsage WGPUTexture::fToWGPUTextureUsage(const TextureUsage& fUsage)
// This is from Vulkan logic- if there are any issues try disabling this first, allows perf
// benefit though
const bool useTransientAttachment =
supportsTransientAttachment &&
// Usage consists of attachment flags only.
none(fUsage & ~TextureUsage::ALL_ATTACHMENTS) &&
// Usage contains at least one attachment flag.

View File

@@ -194,7 +194,8 @@ private:
wgpu::TextureFormat mFormat = wgpu::TextureFormat::Undefined;
uint32_t mArrayLayerCount = 1;
wgpu::TextureView mTexView = nullptr;
wgpu::TextureUsage fToWGPUTextureUsage(const filament::backend::TextureUsage& fUsage);
wgpu::TextureUsage fToWGPUTextureUsage(const filament::backend::TextureUsage& fUsage,
const bool supportsTransientAttachment);
};
struct WGPURenderPrimitive : public HwRenderPrimitive {

View File

@@ -100,9 +100,11 @@ wgpu::Adapter WebGPUPlatform::requestAdapter(wgpu::Surface const& surface) {
wgpu::Device WebGPUPlatform::requestDevice(wgpu::Adapter const& adapter) {
// TODO consider passing limits
constexpr std::array optionalFeatures = { wgpu::FeatureName::DepthClipControl,
wgpu::FeatureName::Depth32FloatStencil8, wgpu::FeatureName::CoreFeaturesAndLimits };
wgpu::FeatureName::Depth32FloatStencil8, wgpu::FeatureName::CoreFeaturesAndLimits,
wgpu::FeatureName::TransientAttachments };
constexpr std::array requiredFeatures = { wgpu::FeatureName::TransientAttachments };
// Currently no required features, but logic to check them is available
constexpr std::array<wgpu::FeatureName, 0> requiredFeatures = {};
wgpu::SupportedFeatures supportedFeatures;
adapter.GetFeatures(&supportedFeatures);

View File

@@ -45,6 +45,7 @@ namespace test {
Backend BackendTest::sBackend = Backend::NOOP;
OperatingSystem BackendTest::sOperatingSystem = OperatingSystem::OTHER;
bool BackendTest::sIsMobilePlatform = false;
std::vector<std::string> BackendTest::sFailedImages;
void BackendTest::init(Backend backend, OperatingSystem operatingSystem, bool isMobilePlatform) {
sBackend = backend;
@@ -63,11 +64,12 @@ BackendTest::~BackendTest() {
flushAndWait();
mImageExpectations->evaluate();
// Note: Don't terminate the driver for OpenGL, as it wipes away the context and removes the buffer from the screen.
if (sBackend == Backend::OPENGL) {
return;
if (sBackend != Backend::OPENGL) {
driver->terminate();
delete driver;
}
driver->terminate();
delete driver;
recordFailedImages();
}
void BackendTest::initializeDriver() {
@@ -167,8 +169,24 @@ bool BackendTest::matchesEnvironment(OperatingSystem operatingSystem) {
return sOperatingSystem == operatingSystem;
}
bool BackendTest::matchesEnvironment(OperatingSystem operatingSystem, Backend backend) {
return matchesEnvironment(operatingSystem) && matchesEnvironment(backend);
void BackendTest::markImageAsFailure(std::string failedImageName) {
sFailedImages.emplace_back(std::move(failedImageName));
}
void BackendTest::recordFailedImages() {
if (!sFailedImages.empty()) {
std::string failedImages;
for (auto& failedTestImageName: sFailedImages) {
if (failedImages.empty()) {
failedImages = failedTestImageName;
} else {
failedImages.append(",");
failedImages.append(failedTestImageName);
}
}
RecordProperty("FailedImages", failedImages);
}
sFailedImages.clear();
}
class Environment : public ::testing::Environment {

View File

@@ -38,6 +38,9 @@ public:
static OperatingSystem sOperatingSystem;
static bool sIsMobilePlatform;
// Takes the name of the image that wasn't correct, without the .png suffix
static void markImageAsFailure(std::string failedImageName);
protected:
BackendTest();
@@ -73,8 +76,13 @@ protected:
static bool matchesEnvironment(Backend backend);
static bool matchesEnvironment(OperatingSystem operatingSystem);
static bool matchesEnvironment(OperatingSystem operatingSystem, Backend backend);
private:
// Adds all the images that failed an ImageExpectation to the XML metadata for the current tests
// case. Add --gtest_output=xml as a command line argument to generate a test_detail.xml file in
// the directory where the tests are run.
static void recordFailedImages();
static std::vector<std::string> sFailedImages;
filament::backend::Driver* driver = nullptr;
filament::backend::CommandBufferQueue commandBufferQueue;

View File

@@ -21,6 +21,7 @@
#include "utils/Hash.h"
#include <fstream>
#include "BackendTest.h"
#include "backend/PixelBufferDescriptor.h"
#include "private/backend/DriverApi.h"
@@ -32,6 +33,8 @@
#endif
namespace test {
ScreenshotParams::ScreenshotParams(int width, int height, std::string fileName,
uint32_t expectedHash, bool isSrgb)
: mWidth(width),
@@ -80,6 +83,10 @@ std::string ScreenshotParams::expectedFilePath() const {
return absl::StrFormat("%s/%s", expectedDirectoryPath(), expectedFileName());
}
const std::string ScreenshotParams::filePrefix() const {
return mFileName;
}
ImageExpectation::ImageExpectation(const char* fileName, int lineNumber,
filament::backend::DriverApi& api, ScreenshotParams params,
filament::backend::RenderTargetHandle renderTarget)
@@ -113,7 +120,11 @@ void ImageExpectation::compareImage() const {
#ifndef FILAMENT_IOS
LoadedPng loadedImage(mParams.expectedFilePath());
uint32_t loadedImageHash = loadedImage.hash();
EXPECT_THAT(actualHash, testing::Eq(loadedImageHash)) << mParams.expectedFileName();
auto compareToImageMatcher = testing::Eq(loadedImageHash);
if (!testing::Matches(compareToImageMatcher)(actualHash)) {
BackendTest::markImageAsFailure(mParams.filePrefix());
}
EXPECT_THAT(actualHash, compareToImageMatcher) << mParams.expectedFileName();
#endif
// For builds that can't load PNGs (currently iOS only) use the expected hash.
EXPECT_THAT(actualHash, testing::Eq(mParams.expectedHash())) << mParams.expectedFileName();
@@ -237,3 +248,5 @@ uint32_t LoadedPng::hash() const {
const std::vector<unsigned char>& LoadedPng::bytes() const {
return mBytes;
}
} // namespace test

View File

@@ -35,6 +35,8 @@ do { \
screenshotParams); \
} while (0)
namespace test {
/**
* Stores user-provided configuration values for an image expectation
*/
@@ -54,6 +56,7 @@ public:
static std::string expectedDirectoryPath();
std::string expectedFileName() const;
std::string expectedFilePath() const;
const std::string filePrefix() const;
private:
int mWidth;
@@ -153,4 +156,6 @@ private:
std::vector<std::unique_ptr<ImageExpectation>> mExpectations;
};
} // namespace test
#endif //TNT_IMAGE_EXPECTATIONS_H

View File

@@ -1,62 +1,143 @@
import os, shutil, argparse, typing
def match_sufffix(file_name: str, suffix: str, accepted_prefixes: typing.List[str]) -> str:
"""
Check if the file name is one of the searched for ones with the given suffix and if so return it.
:param accepted_prefixes: If None accepts any prefix
:return: file_name with the suffix removed or "" if it doesn't match. This does mean a string that
is just the suffix is considered to not match as it will return the empty string.
"""
if file_name.endswith(suffix):
prefix = file_name.removesuffix(suffix)
if accepted_prefixes is None or prefix in accepted_prefixes:
return prefix
return ""
import os, shutil, argparse, typing, xml.etree.ElementTree, subprocess, platform
def replace_file_names(path: str, removed: str, replacement: str = "", output_path: str = "",
prefixes: typing.List[str] = None):
if not output_path:
output_path = path
for file_name in os.listdir(path=path):
prefix = match_sufffix(file_name, removed, prefixes)
if prefix:
# Remove the prefix from the list so that prefixes is the list of intended but not yet found
# files.
if prefixes is not None:
prefixes.remove(prefix)
new_file_name = prefix + replacement
new_file_path = os.path.join(output_path, new_file_name)
old_file_path = os.path.join(path, file_name)
print(f'{old_file_path} to {new_file_path}')
shutil.move(old_file_path, new_file_path)
if prefixes is not None:
for unfound_prefix in prefixes:
print(f'Failed to find {unfound_prefix}_actual.png')
class TestResults(object):
ACTUAL_SUFFIX = '_actual.png'
EXPECTED_SUFFIX = '.png'
def __init__(self, results_directory: str, source_expected_directory: str):
self.results_directory = results_directory
self.actual_directory = os.path.join(self.results_directory, 'images', 'actual_images')
self.expected_directory = os.path.join(self.results_directory, 'images', 'expected_images')
self.source_expected_directory = source_expected_directory
def get_latest_failed_images(self) -> typing.List[str]:
failed_images = []
xml_tree = xml.etree.ElementTree.parse(
os.path.join(self.results_directory, 'test_detail.xml'))
testsuites = xml_tree.getroot()
for testsuite in testsuites.findall('testsuite'):
for testcase in testsuite.findall('testcase'):
for properties in testcase.findall('properties'):
for property in properties.findall('property'):
if property.get('name') == 'FailedImages':
failed_images.extend(property.get('value').split(','))
return failed_images
def handle_failed_image(self, failed_image):
self.show_images(failed_image)
print(f'Update {failed_image}\'s expected image? y/n')
while True:
user_input = input()
if user_input == 'y':
self.move_actual_to_source([failed_image])
break
elif user_input == 'n':
break
def handle_all_failed_images(self):
for failed_image in self.get_latest_failed_images():
self.handle_failed_image(failed_image)
def show_images(self, failed_image):
# TODO: Test more on non-mac systems
open_command: str
os_name = platform.system().lower()
if 'windows' in os_name:
open_command = 'start'
elif 'osx' in os_name or 'darwin' in os_name:
open_command = 'open'
else:
open_command = 'xdg-open'
subprocess.run(
[open_command,
os.path.join(self.actual_directory, failed_image + TestResults.ACTUAL_SUFFIX)])
subprocess.run(
[open_command,
os.path.join(self.expected_directory, failed_image + TestResults.EXPECTED_SUFFIX)])
def move_actual_to_source(self, file_prefixes: typing.List[str]):
replace_file_names(path=self.actual_directory, removed=TestResults.ACTUAL_SUFFIX,
replacement=TestResults.EXPECTED_SUFFIX,
output_path=self.source_expected_directory, prefixes=file_prefixes)
def batch_move(self, prefixes: typing.Optional[typing.List[str]] = None):
replace_file_names(path=self.actual_directory, removed=TestResults.ACTUAL_SUFFIX,
replacement=TestResults.EXPECTED_SUFFIX,
output_path=self.source_expected_directory, prefixes=prefixes)
def match_suffix(file_name: str, suffix: str, accepted_prefixes: typing.List[str]) -> str:
"""
Check if the file name is one of the searched for ones with the given suffix and if so return
it.
:param accepted_prefixes: If None accepts any prefix
:return: file_name with the suffix removed or "" if it doesn't match. This does mean a string
that is just the suffix is considered to not match as it will return the empty string.
"""
if file_name.endswith(suffix):
prefix = file_name.removesuffix(suffix)
if accepted_prefixes is None or prefix in accepted_prefixes:
return prefix
return ''
def replace_file_names(path: str, removed: str, replacement: str = '', output_path: str = '',
prefixes: typing.Optional[typing.List[str]] = None):
if not output_path:
output_path = path
for file_name in os.listdir(path=path):
prefix = match_suffix(file_name, removed, prefixes)
if prefix:
# Remove the prefix from the list so that prefixes is the list of intended but not yet
# found files.
if prefixes is not None:
prefixes.remove(prefix)
new_file_name = prefix + replacement
new_file_path = os.path.join(output_path, new_file_name)
old_file_path = os.path.join(path, file_name)
print(f'{old_file_path} to {new_file_path}')
shutil.copyfile(old_file_path, new_file_path)
if prefixes is not None:
for unfound_prefix in prefixes:
print(f'Failed to find {unfound_prefix}_actual.png')
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog='Backend Test File Renamer',
description='Moves actual generated test images to the expected '
'images directory, to update the test requirements. '
'test_cases accepts multiple arguments that should '
'be the name of the expected image file without the '
'.png suffix. Also --all can be passed to copy all '
'images.\n'
'Remember to sync CMake after running this to move '
'the new expected images to the binary directory.')
parser.add_argument('-i', '--input_path')
parser.add_argument('-o', '--output_path', default="./expected_images")
parser.add_argument('-t', '--test_cases', action='extend', nargs='*')
parser.add_argument('-a', '--all', action='store_true')
parser = argparse.ArgumentParser(prog='Backend Test File Renamer',
description='Moves actual generated test images to the '
'expected images directory, to update the test '
'requirements. test_cases accepts multiple '
'arguments that should be the name of the '
'expected image file without the .png suffix. '
'Also --all can be passed to copy all images.\n'
'Remember to sync CMake after running this to '
'move the new expected images to the binary '
'directory.')
parser.add_argument('-r', '--results_path')
parser.add_argument('-s', '--source_expected_path', default="./expected_images")
# The mutually exclusive options for how to process the actual images
parser.add_argument('-b', '--batch', action='extend', nargs='*')
parser.add_argument('-a', '--all', action='store_true')
parser.add_argument('-t', '--tests', action='store_true')
parser.add_argument('-c', '--compare', action='extend', nargs='*')
args = parser.parse_args()
input_path = "."
if args.input_path:
input_path = args.input_path
args = parser.parse_args()
if not args.results_path:
raise AssertionError("No result path provided")
results_path = args.results_path
prefixes = args.test_cases
if args.all:
prefixes = None
results = TestResults(results_directory=results_path,
source_expected_directory=args.source_expected_path)
replace_file_names(path=input_path, output_path=args.output_path, removed="_actual.png",
replacement=".png", prefixes=prefixes)
if args.all:
results.batch_move()
elif args.tests:
results.handle_all_failed_images()
elif args.compare:
for file_prefix in args.compare:
results.show_images(file_prefix)
else:
results.batch_move(args.batch)

View File

@@ -21,13 +21,17 @@
#include <filament/FilamentAPI.h>
#include <filament/ToneMapper.h>
#include <filament/ColorSpace.h>
#include <utils/compiler.h>
#include <math/mathfwd.h>
#include <math/vec3.h>
#include <math/vec4.h>
#include <stdint.h>
#include <stddef.h>
#include <memory>
namespace filament {
@@ -201,15 +205,14 @@ public:
*
* The default tone mapping operator is ACESLegacyToneMapper.
*
* The specified tone mapper must have a lifecycle that exceeds the lifetime of
* this builder. Since the build(Engine&) method is synchronous, it is safe to
* delete the tone mapper object after that finishes executing.
* The ownership of the specified tone mapper is shared with the builder and built
* ColorGrading object.
*
* @param toneMapper The tone mapping operator to apply to the HDR color buffer
*
* @return This Builder, for chaining calls
*/
Builder& toneMapper(ToneMapper const* UTILS_NULLABLE toneMapper) noexcept;
Builder& toneMapper(std::shared_ptr<ToneMapper> toneMapper) noexcept;
/**
* Selects the tone mapping operator to apply to the HDR color buffer as the last
@@ -487,7 +490,118 @@ public:
friend class FColorGrading;
};
protected:
/** Returns the quality level used to create this ColorGrading object. */
QualityLevel getQuality() const noexcept;
/** Returns the LUT format used to create this ColorGrading object. */
LutFormat getLutFormat() const noexcept;
/** Returns the LUT dimensions used to create this ColorGrading object. */
uint8_t getLutDimensions() const noexcept;
/** Returns the tone mapper used to create this ColorGrading object. */
const ToneMapper& getToneMapper() const noexcept;
/** Returns whether luminance scaling was enabled during creation. */
bool isLuminanceScalingEnabled() const noexcept;
/** Returns whether gamut mapping was enabled during creation. */
bool isGamutMappingEnabled() const noexcept;
/** Returns the exposure value used to create this ColorGrading object. */
float getExposure() const noexcept;
/** Returns the night adaptation value used to create this ColorGrading object. */
float getNightAdaptation() const noexcept;
/** Returns the white balance temperature used to create this ColorGrading object. */
float getWhiteBalanceTemperature() const noexcept;
/** Returns the white balance tint used to create this ColorGrading object. */
float getWhiteBalanceTint() const noexcept;
/** Returns the channel mixer output for the red channel. */
math::float3 getChannelMixerOutRed() const noexcept;
/** Returns the channel mixer output for the green channel. */
math::float3 getChannelMixerOutGreen() const noexcept;
/** Returns the channel mixer output for the blue channel. */
math::float3 getChannelMixerOutBlue() const noexcept;
/** Returns the shadows adjustment used to create this ColorGrading object. */
math::float3 getShadows() const noexcept;
/** Returns the midtones adjustment used to create this ColorGrading object. */
math::float3 getMidtones() const noexcept;
/** Returns the highlights adjustment used to create this ColorGrading object. */
math::float3 getHighlights() const noexcept;
/** Returns the shadow/midtones/highlights ranges used to create this ColorGrading object. */
math::float4 getShadowMidtonesHighlightsRanges() const noexcept;
/** Returns the slope adjustment used to create this ColorGrading object. */
math::float3 getSlope() const noexcept;
/** Returns the offset adjustment used to create this ColorGrading object. */
math::float3 getOffset() const noexcept;
/** Returns the power adjustment used to create this ColorGrading object. */
math::float3 getPower() const noexcept;
/** Returns the contrast value used to create this ColorGrading object. */
float getContrast() const noexcept;
/** Returns the vibrance value used to create this ColorGrading object. */
float getVibrance() const noexcept;
/** Returns the saturation value used to create this ColorGrading object. */
float getSaturation() const noexcept;
/** Returns the shadow gamma curve adjustment used to create this ColorGrading object. */
math::float3 getCurvesShadowGamma() const noexcept;
/** Returns the mid-point curve adjustment used to create this ColorGrading object. */
math::float3 getCurvesMidPoint() const noexcept;
/** Returns the highlight scale curve adjustment used to create this ColorGrading object. */
math::float3 getCurvesHighlightScale() const noexcept;
/** Returns the output color space used to create this ColorGrading object. */
const color::ColorSpace& getOutputColorSpace() const noexcept;
protected :
struct Settings {
LutFormat lutFormat = LutFormat::INTEGER;
uint8_t lutDimensions = 32;
std::shared_ptr<ToneMapper> toneMapper = std::make_shared<ACESLegacyToneMapper>();
bool luminanceScaling = false;
bool gummapMapping = false;
float exposure = 0.0f;
float nightAdaptation = 0.0f;
float whiteBalanceTemperature = 0.0f;
float whiteBalanceTint = 0.0f;
math::float3 channelMixerOutRed{1.0f, 0.0f, 0.0f};
math::float3 channelMixerOutGreen{0.0f, 1.0f, 0.0f};
math::float3 channelMixerOutBlue{0.0f, 0.0f, 1.0f};
math::float3 shadows{1.0f, 1.0f, 1.0f};
math::float3 midtones{1.0f, 1.0f, 1.0f};
math::float3 highlights{1.0f, 1.0f, 1.0f};
math::float4 ShadowMidtonesHighlightsRanges{0.0f, 0.333f, 0.55f, 1.0f};
math::float3 slope{1.0f};
math::float3 offset{0.0f};
math::float3 power{1.0f};
float contrast = 1.0f;
float vibrance = 1.0f;
float saturation = 1.0f;
math::float3 curvesShadowGamma{1.0f, 1.0f, 1.0f};
math::float3 curvesMidPoint{1.0f, 1.0f, 1.0f};
math::float3 curvesHighlightScale{1.0f, 1.0f, 1.0f};
color::ColorSpace colorSpace = color::Rec709 - color::sRGB - color::D65;
};
Settings mSettings;
// prevent heap allocation
~ColorGrading() = default;
};

View File

@@ -54,6 +54,8 @@ namespace filament {
struct UTILS_PUBLIC ToneMapper {
ToneMapper() noexcept;
virtual ~ToneMapper() noexcept;
ToneMapper(ToneMapper const&) noexcept;
ToneMapper& operator=(ToneMapper const&) noexcept = default;
/**
* Maps an open domain (or "scene referred" values) color value to display
@@ -69,6 +71,13 @@ struct UTILS_PUBLIC ToneMapper {
*/
virtual math::float3 operator()(math::float3 c) const noexcept = 0;
/**
* Creates a copy of this tone mapper instance.
*
* @return A pointer to a new ToneMapper instance that is a copy of this instance.
*/
virtual ToneMapper* clone() const noexcept = 0;
/**
* If true, then this function holds that f(x) = vec3(f(x.r), f(x.g), f(x.b))
*
@@ -92,8 +101,10 @@ struct UTILS_PUBLIC ToneMapper {
struct UTILS_PUBLIC LinearToneMapper final : public ToneMapper {
LinearToneMapper() noexcept;
~LinearToneMapper() noexcept final;
LinearToneMapper(LinearToneMapper const&) noexcept;
math::float3 operator()(math::float3 c) const noexcept override;
LinearToneMapper* clone() const noexcept override;
bool isOneDimensional() const noexcept override { return true; }
bool isLDR() const noexcept override { return true; }
};
@@ -106,8 +117,10 @@ struct UTILS_PUBLIC LinearToneMapper final : public ToneMapper {
struct UTILS_PUBLIC ACESToneMapper final : public ToneMapper {
ACESToneMapper() noexcept;
~ACESToneMapper() noexcept final;
ACESToneMapper(ACESToneMapper const&) noexcept;
math::float3 operator()(math::float3 c) const noexcept override;
ACESToneMapper* clone() const noexcept override;
bool isOneDimensional() const noexcept override { return false; }
bool isLDR() const noexcept override { return false; }
};
@@ -121,8 +134,10 @@ struct UTILS_PUBLIC ACESToneMapper final : public ToneMapper {
struct UTILS_PUBLIC ACESLegacyToneMapper final : public ToneMapper {
ACESLegacyToneMapper() noexcept;
~ACESLegacyToneMapper() noexcept final;
ACESLegacyToneMapper(ACESLegacyToneMapper const&) noexcept;
math::float3 operator()(math::float3 c) const noexcept override;
ACESLegacyToneMapper* clone() const noexcept override;
bool isOneDimensional() const noexcept override { return false; }
bool isLDR() const noexcept override { return false; }
};
@@ -136,8 +151,10 @@ struct UTILS_PUBLIC ACESLegacyToneMapper final : public ToneMapper {
struct UTILS_PUBLIC FilmicToneMapper final : public ToneMapper {
FilmicToneMapper() noexcept;
~FilmicToneMapper() noexcept final;
FilmicToneMapper(FilmicToneMapper const&) noexcept;
math::float3 operator()(math::float3 x) const noexcept override;
FilmicToneMapper* clone() const noexcept override;
bool isOneDimensional() const noexcept override { return true; }
bool isLDR() const noexcept override { return false; }
};
@@ -150,8 +167,10 @@ struct UTILS_PUBLIC FilmicToneMapper final : public ToneMapper {
struct UTILS_PUBLIC PBRNeutralToneMapper final : public ToneMapper {
PBRNeutralToneMapper() noexcept;
~PBRNeutralToneMapper() noexcept final;
PBRNeutralToneMapper(PBRNeutralToneMapper const&) noexcept;
math::float3 operator()(math::float3 x) const noexcept override;
virtual PBRNeutralToneMapper* clone() const noexcept override;
bool isOneDimensional() const noexcept override { return false; }
bool isLDR() const noexcept override { return false; }
};
@@ -173,8 +192,10 @@ struct UTILS_PUBLIC AgxToneMapper final : public ToneMapper {
*/
explicit AgxToneMapper(AgxLook look = AgxLook::NONE) noexcept;
~AgxToneMapper() noexcept final;
AgxToneMapper(AgxToneMapper const&) noexcept;
math::float3 operator()(math::float3 x) const noexcept override;
virtual AgxToneMapper* clone() const noexcept override;
bool isOneDimensional() const noexcept override { return false; }
bool isLDR() const noexcept override { return false; }
@@ -215,12 +236,13 @@ struct UTILS_PUBLIC GenericToneMapper final : public ToneMapper {
) noexcept;
~GenericToneMapper() noexcept final;
GenericToneMapper(GenericToneMapper const&) = delete;
GenericToneMapper& operator=(GenericToneMapper const&) = delete;
GenericToneMapper(GenericToneMapper const& rhs) noexcept;
GenericToneMapper& operator=(GenericToneMapper const& rhs) noexcept;
GenericToneMapper(GenericToneMapper&& rhs) noexcept;
GenericToneMapper& operator=(GenericToneMapper&& rhs) noexcept;
math::float3 operator()(math::float3 x) const noexcept override;
GenericToneMapper* clone() const noexcept override;
bool isOneDimensional() const noexcept override { return true; }
bool isLDR() const noexcept override { return false; }
@@ -283,8 +305,10 @@ private:
struct UTILS_PUBLIC DisplayRangeToneMapper final : public ToneMapper {
DisplayRangeToneMapper() noexcept;
~DisplayRangeToneMapper() noexcept override;
DisplayRangeToneMapper(DisplayRangeToneMapper const&) noexcept;
math::float3 operator()(math::float3 c) const noexcept override;
DisplayRangeToneMapper* clone() const noexcept override;
bool isOneDimensional() const noexcept override { return false; }
bool isLDR() const noexcept override { return false; }
};

View File

@@ -188,7 +188,8 @@ float3 ACES(float3 color, float brightness) noexcept {
#define DEFAULT_CONSTRUCTORS(A) \
A::A() noexcept = default; \
A::~A() noexcept = default;
A::~A() noexcept = default; \
A::A(A const&) noexcept = default;
DEFAULT_CONSTRUCTORS(ToneMapper)
@@ -202,6 +203,10 @@ float3 LinearToneMapper::operator()(float3 const v) const noexcept {
return saturate(v);
}
LinearToneMapper* LinearToneMapper::clone() const noexcept {
return new LinearToneMapper(*this);
}
//------------------------------------------------------------------------------
// ACES tone mappers
//------------------------------------------------------------------------------
@@ -212,12 +217,20 @@ float3 ACESToneMapper::operator()(float3 const c) const noexcept {
return aces::ACES(c, 1.0f);
}
ACESToneMapper* ACESToneMapper::clone() const noexcept {
return new ACESToneMapper(*this);
}
DEFAULT_CONSTRUCTORS(ACESLegacyToneMapper)
float3 ACESLegacyToneMapper::operator()(float3 const c) const noexcept {
return aces::ACES(c, 1.0f / 0.6f);
}
ACESLegacyToneMapper* ACESLegacyToneMapper::clone() const noexcept {
return new ACESLegacyToneMapper(*this);
}
DEFAULT_CONSTRUCTORS(FilmicToneMapper)
float3 FilmicToneMapper::operator()(float3 const x) const noexcept {
@@ -230,6 +243,10 @@ float3 FilmicToneMapper::operator()(float3 const x) const noexcept {
return (x * (a * x + b)) / (x * (c * x + d) + e);
}
FilmicToneMapper* FilmicToneMapper::clone() const noexcept {
return new FilmicToneMapper(*this);
}
//------------------------------------------------------------------------------
// PBR Neutral tone mapper
//------------------------------------------------------------------------------
@@ -256,12 +273,17 @@ float3 PBRNeutralToneMapper::operator()(float3 color) const noexcept {
return mix(color, float3(newPeak), g);
}
PBRNeutralToneMapper* PBRNeutralToneMapper::clone() const noexcept {
return new PBRNeutralToneMapper(*this);
}
//------------------------------------------------------------------------------
// AgX tone mapper
//------------------------------------------------------------------------------
AgxToneMapper::AgxToneMapper(AgxLook const look) noexcept : look(look) {}
AgxToneMapper::~AgxToneMapper() noexcept = default;
AgxToneMapper::AgxToneMapper(AgxToneMapper const& rhs) noexcept = default;
// These matrices taken from Blender's implementation of AgX, which works with Rec.2020 primaries.
// https://github.com/EaryChow/AgX_LUT_Gen/blob/main/AgXBaseRec2020.py
@@ -356,6 +378,10 @@ float3 AgxToneMapper::operator()(float3 v) const noexcept {
return v;
}
AgxToneMapper* AgxToneMapper::clone() const noexcept {
return new AgxToneMapper(*this);
}
//------------------------------------------------------------------------------
// Display range tone mapper
//------------------------------------------------------------------------------
@@ -394,6 +420,10 @@ float3 DisplayRangeToneMapper::operator()(float3 const c) const noexcept {
return mix(debugColors[index], debugColors[index + 1], saturate(v - float(index)));
}
DisplayRangeToneMapper* DisplayRangeToneMapper::clone() const noexcept {
return new DisplayRangeToneMapper(*this);
}
//------------------------------------------------------------------------------
// Generic tone mapper
//------------------------------------------------------------------------------
@@ -454,7 +484,19 @@ GenericToneMapper::~GenericToneMapper() noexcept {
delete mOptions;
}
GenericToneMapper::GenericToneMapper(GenericToneMapper&& rhs) noexcept : mOptions(rhs.mOptions) {
GenericToneMapper::GenericToneMapper(GenericToneMapper const& rhs) noexcept {
mOptions = new Options(*rhs.mOptions);
}
GenericToneMapper& GenericToneMapper::operator=(GenericToneMapper const& rhs) noexcept {
if (this != &rhs) {
delete mOptions;
mOptions = new Options(*rhs.mOptions);
}
return *this;
}
GenericToneMapper::GenericToneMapper(GenericToneMapper&& rhs) noexcept : mOptions(rhs.mOptions) {
rhs.mOptions = nullptr;
}
@@ -469,6 +511,10 @@ float3 GenericToneMapper::operator()(float3 x) const noexcept {
return mOptions->outputScale * x / (x + mOptions->inputScale);
}
GenericToneMapper* GenericToneMapper::clone() const noexcept {
return new GenericToneMapper(*this);
}
float GenericToneMapper::getContrast() const noexcept { return mOptions->contrast; }
float GenericToneMapper::getMidGrayIn() const noexcept { return mOptions->midGrayIn; }
float GenericToneMapper::getMidGrayOut() const noexcept { return mOptions->midGrayOut; }

View File

@@ -49,7 +49,7 @@ using namespace backend;
//------------------------------------------------------------------------------
struct ColorGrading::BuilderDetails {
const ToneMapper* toneMapper = nullptr;
std::shared_ptr<ToneMapper> toneMapper = nullptr;
#if defined(__clang__)
#pragma clang diagnostic push
@@ -173,7 +173,7 @@ ColorGrading::Builder& ColorGrading::Builder::dimensions(uint8_t const dim) noex
return *this;
}
ColorGrading::Builder& ColorGrading::Builder::toneMapper(const ToneMapper* toneMapper) noexcept {
ColorGrading::Builder& ColorGrading::Builder::toneMapper(std::shared_ptr<ToneMapper> toneMapper) noexcept {
mImpl->toneMapper = toneMapper;
return *this;
}
@@ -271,6 +271,110 @@ ColorGrading::Builder& ColorGrading::Builder::outputColorSpace(
return *this;
}
ColorGrading::LutFormat ColorGrading::getLutFormat() const noexcept {
return mSettings.lutFormat;
}
uint8_t ColorGrading::getLutDimensions() const noexcept {
return mSettings.lutDimensions;
}
const ToneMapper& ColorGrading::getToneMapper() const noexcept {
return *mSettings.toneMapper;
}
bool ColorGrading::isLuminanceScalingEnabled() const noexcept {
return mSettings.luminanceScaling;
}
bool ColorGrading::isGamutMappingEnabled() const noexcept {
return mSettings.gummapMapping;
}
float ColorGrading::getExposure() const noexcept {
return mSettings.exposure;
}
float ColorGrading::getNightAdaptation() const noexcept {
return mSettings.nightAdaptation;
}
float ColorGrading::getWhiteBalanceTemperature() const noexcept {
return mSettings.whiteBalanceTemperature;
}
float ColorGrading::getWhiteBalanceTint() const noexcept {
return mSettings.whiteBalanceTint;
}
math::float3 ColorGrading::getChannelMixerOutRed() const noexcept {
return mSettings.channelMixerOutRed;
}
math::float3 ColorGrading::getChannelMixerOutGreen() const noexcept {
return mSettings.channelMixerOutGreen;
}
math::float3 ColorGrading::getChannelMixerOutBlue() const noexcept {
return mSettings.channelMixerOutBlue;
}
math::float3 ColorGrading::getShadows() const noexcept {
return mSettings.shadows;
}
math::float3 ColorGrading::getMidtones() const noexcept {
return mSettings.midtones;
}
math::float3 ColorGrading::getHighlights() const noexcept {
return mSettings.highlights;
}
math::float4 ColorGrading::getShadowMidtonesHighlightsRanges() const noexcept {
return mSettings.ShadowMidtonesHighlightsRanges;
}
math::float3 ColorGrading::getSlope() const noexcept {
return mSettings.slope;
}
math::float3 ColorGrading::getOffset() const noexcept {
return mSettings.offset;
}
math::float3 ColorGrading::getPower() const noexcept {
return mSettings.power;
}
float ColorGrading::getContrast() const noexcept {
return mSettings.contrast;
}
float ColorGrading::getVibrance() const noexcept {
return mSettings.vibrance;
}
float ColorGrading::getSaturation() const noexcept {
return mSettings.saturation;
}
math::float3 ColorGrading::getCurvesShadowGamma() const noexcept {
return mSettings.curvesShadowGamma;
}
math::float3 ColorGrading::getCurvesMidPoint() const noexcept {
return mSettings.curvesMidPoint;
}
math::float3 ColorGrading::getCurvesHighlightScale() const noexcept {
return mSettings.curvesHighlightScale;
}
const color::ColorSpace& ColorGrading::getOutputColorSpace() const noexcept {
return mSettings.colorSpace;
}
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
@@ -287,30 +391,25 @@ ColorGrading* ColorGrading::Builder::build(Engine& engine) {
if (needToneMapper) {
switch (mImpl->toneMapping) {
case ToneMapping::LINEAR:
mImpl->toneMapper = new LinearToneMapper();
mImpl->toneMapper = std::make_shared<LinearToneMapper>();
break;
case ToneMapping::ACES_LEGACY:
mImpl->toneMapper = new ACESLegacyToneMapper();
mImpl->toneMapper = std::make_shared<ACESLegacyToneMapper>();
break;
case ToneMapping::ACES:
mImpl->toneMapper = new ACESToneMapper();
mImpl->toneMapper = std::make_shared<ACESToneMapper>();
break;
case ToneMapping::FILMIC:
mImpl->toneMapper = new FilmicToneMapper();
mImpl->toneMapper = std::make_shared<FilmicToneMapper>();
break;
case ToneMapping::DISPLAY_RANGE:
mImpl->toneMapper = new DisplayRangeToneMapper();
mImpl->toneMapper = std::make_shared<DisplayRangeToneMapper>();
break;
}
}
FColorGrading* colorGrading = downcast(engine).createColorGrading(*this);
if (needToneMapper) {
delete mImpl->toneMapper;
mImpl->toneMapper = nullptr;
}
return colorGrading;
}
@@ -886,6 +985,9 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
data, lutElementCount * elementSize, format, type,
[](void* buffer, size_t, void*) { free(buffer); }
});
// Initialize settings from builder
initializeSettings(builder, mSettings);
}
FColorGrading::~FColorGrading() noexcept = default;
@@ -895,4 +997,33 @@ void FColorGrading::terminate(FEngine& engine) {
driver.destroyTexture(mLutHandle);
}
void FColorGrading::initializeSettings(const Builder& builder, Settings& settings) noexcept {
settings.lutFormat = builder->format;
settings.lutDimensions = builder->dimension;
settings.toneMapper = builder->toneMapper;
settings.luminanceScaling = builder->luminanceScaling;
settings.gummapMapping = builder->gamutMapping;
settings.exposure = builder->exposure;
settings.nightAdaptation = builder->nightAdaptation;
settings.whiteBalanceTemperature = builder->whiteBalance.x;
settings.whiteBalanceTint = builder->whiteBalance.y;
settings.channelMixerOutRed = builder->outRed;
settings.channelMixerOutGreen = builder->outGreen;
settings.channelMixerOutBlue = builder->outBlue;
settings.shadows = builder->shadows;
settings.midtones = builder->midtones;
settings.highlights = builder->highlights;
settings.ShadowMidtonesHighlightsRanges = builder->tonalRanges;
settings.slope = builder->slope;
settings.offset = builder->offset;
settings.power = builder->power;
settings.contrast = builder->contrast;
settings.vibrance = builder->vibrance;
settings.saturation = builder->saturation;
settings.curvesShadowGamma = builder->shadowGamma;
settings.curvesMidPoint = builder->midPoint;
settings.curvesHighlightScale = builder->highlightScale;
settings.colorSpace = builder->outputColorSpace;
}
} //namespace filament

View File

@@ -51,6 +51,8 @@ private:
uint32_t mDimension;
bool mIsOneDimensional;
bool mIsLDR;
static void initializeSettings(const Builder& builder, Settings& settings) noexcept;
};
FILAMENT_DOWNCAST(ColorGrading)

View File

@@ -1,12 +1,12 @@
Pod::Spec.new do |spec|
spec.name = "Filament"
spec.version = "1.59.4"
spec.version = "1.59.5"
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.59.4/filament-v1.59.4-ios.tgz" }
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.59.5/filament-v1.59.5-ios.tgz" }
# Fix linking error with Xcode 12; we do not yet support the simulator on Apple silicon.
spec.pod_target_xcconfig = {

View File

@@ -102,7 +102,8 @@ project in Xcode to see changes take effect.
## Building iOS Samples with ASan / UBSan
1. Turn on ASan / UBSan in Filament's top-level CMakeLists.txt by uncommenting the following line:
1. Turn on ASan / UBSan in Filament's top-level CMakeLists.txt by passing
`-DFILAMENT_ENABLE_ASAN_UBSAN=1` to trigger the following line:
```
set(EXTRA_SANITIZE_OPTIONS "-fsanitize=undefined -fsanitize=address")

View File

@@ -700,7 +700,8 @@ constexpr ToneMapper* createToneMapper(const ColorGradingSettings& settings) noe
}
ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* engine) {
ToneMapper* toneMapper = createToneMapper(settings);
ToneMapper* toneMapperRaw = createToneMapper(settings);
std::shared_ptr<ToneMapper> toneMapper(toneMapperRaw);
ColorGrading *colorGrading = ColorGrading::Builder()
.quality(settings.quality)
.exposure(settings.exposure)
@@ -723,7 +724,6 @@ ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* e
.gamutMapping(settings.gamutMapping)
.outputColorSpace(settings.colorspace)
.build(*engine);
delete toneMapper;
return colorGrading;
}

View File

@@ -0,0 +1,143 @@
# Copyright (C) 2025 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 os
import shutil
import re
from utils import execute, ArgParseImpl, mkdir_p
GOLDENS_DIR = 'renderdiff'
ACCESS_TYPE_TOKEN = 'token'
ACCESS_TYPE_SSH = 'ssh'
ACCESS_TYPE_READ_ONLY = 'read-only'
def _read_git_config(curdir):
with open(os.path.join(curdir, './.git/config'), 'r') as f:
return f.read()
def _write_git_config(curdir, config_str):
with open(os.path.join(curdir, './.git/config'), 'w') as f:
return f.write(config_str)
class GoldenManager:
def __init__(self, working_dir, access_type=ACCESS_TYPE_READ_ONLY, access_token=None):
self.working_dir_ = working_dir
self.access_token_ = access_token
self.access_type_ = access_type
assert os.path.isdir(self.working_dir_),\
f"working directory {self.working_dir_} does not exist"
self._prepare()
def _assets_dir(self):
return os.path.join(self.working_dir_, "filament-assets")
# Returns the directory containing the goldens
def directory(self):
return os.path.join(self._assets_dir(), GOLDENS_DIR)
def _get_repo_url(self):
protocol = ''
protocol_separator = ''
if self.access_type_ == ACCESS_TYPE_SSH:
protocol = 'git@'
protocol_separator = ':'
else:
protocol = 'https://' + \
(f'x-access-token:{self.access_token_}@' if self.access_token_ else '')
protocol_separator = '/'
return f'{protocol}github.com{protocol_separator}google/filament-assets.git'
def _prepare(self):
assets_dir = self._assets_dir()
if not os.path.exists(assets_dir):
execute(
f'git clone --depth=1 {self._get_repo_url()}',
cwd=self.working_dir_,
capture_output=False
)
else:
if self.access_type_ == ACCESS_TYPE_SSH:
config = _read_git_config(self._assets_dir())
https_url = r'https://github\.com\/google\/filament\.git'
config = re.sub(https_url, self._get_repo_url(), config)
_write_git_config(self._assets_dir(), config)
self.update()
def update(self):
self._git_exec('fetch')
self._git_exec('checkout main')
self._git_exec('rebase')
def _git_exec(self, cmd):
execute(f'git {cmd}', cwd=self._assets_dir(), capture_output=False)
def merge_to_main(self, branch, push_to_remote=False):
self.update()
assets_dir = self._assets_dir()
self._git_exec(f'checkout main')
self._git_exec(f'merge --no-ff {branch}')
if push_to_remote and \
(self.access_token_ or self.access_type_ == ACCESS_TYPE_SSH):
self._git_exec(f'push origin main')
self.update()
def source_from(self, src_dir, commit_msg, branch,
updates=[], deletes=[], push_to_remote=False):
assets_dir = self._assets_dir()
self._git_exec(f'checkout main')
# Force create the branch (note will overwrite the old branch)
self._git_exec(f'switch -C {branch}')
rdiff_dir = os.path.join(assets_dir, GOLDENS_DIR)
if len(updates) == 0 and len(deletes) == 0:
shutil.rmtree(rdiff_dir, ignore_errors=True)
mkdir_p(rdiff_dir)
shutil.copytree(src_dir, rdiff_dir, dirs_exist_ok=True)
self._git_exec(f'add {GOLDENS_DIR}')
else:
for f in deletes:
self._git_exec(f'remove {os.path.join(GOLDENS_DIR, f)}')
for f in updates:
shutil.copy2(
os.path.join(src_dir, f),
os.path.join(rdiff_dir, f))
self._git_exec(f'add {os.path.join(GOLDENS_DIR, f)}')
TMP_GOLDEN_COMMIT_FILE = '/tmp/golden_commit.txt'
with open(TMP_GOLDEN_COMMIT_FILE, 'w') as f:
f.write(commit_msg)
self._git_exec(f'commit -a -F {TMP_GOLDEN_COMMIT_FILE}')
if push_to_remote and \
(self.access_token_ or self.access_type_ == ACCESS_TYPE_SSH):
self._git_exec(f'push -f origin {branch}')
self.update()
def download_to(self, dest_dir, branch='main'):
self._git_exec(f'checkout {branch}')
assets_dir = self._assets_dir()
mkdir_p(dest_dir)
rdiff_dir = os.path.join(assets_dir, GOLDENS_DIR)
shutil.copytree(rdiff_dir, dest_dir, dirs_exist_ok=True)
# For testing only
if __name__ == "__main__":
golden_manager = GoldenManager(os.getcwd())
# golden_manager.source_from_and_commit(
# os.path.join(os.getcwd(), 'out/renderdiff_tests'),
# 'First commit (local)',
# branch='branch-test')
# golden_manager.merge_to_main('branch-test', push_to_remote=True)
# golden_manager.download_to(os.path.join(os.getcwd(), 'tmp/goldens'))

View File

@@ -0,0 +1,43 @@
# Copyright (C) 2025 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 tifffile
import numpy
def same_image(tiff_file_a, tiff_file_b):
try:
img1_data = tifffile.imread(tiff_file_a)
img2_data = tifffile.imread(tiff_file_b)
# If the dimensions (height, width, number of channels, number of pages/frames)
# are different, the images are not the same.
if img1_data.shape != img2_data.shape:
print(f"Images have different shapes: {img1_data.shape} vs {img2_data.shape}")
return False
# numpy.array_equal() checks if two arrays have the same shape and elements.
if numpy.array_equal(img1_data, img2_data):
return True
else:
return False
except FileNotFoundError:
print(f"Error: One or both files not found ('{file_path1}', '{file_path2}').")
return False
except tifffile.TiffFileError as e:
print(f"Error: One or both files are not valid TIFF files or could not be read. Details: {e}")
return False
except Exception as e:
print(f"An unexpected error occurred: {e}")
return False

View File

@@ -14,9 +14,14 @@
import sys
import os
import json
import glob
import shutil
from utils import execute, ArgParseImpl
from utils import execute, ArgParseImpl, mkdir_p, mv_f
from parse_test_json import parse_test_config_from_path
from golden_manager import GoldenManager
from image_diff import same_image
def important_print(msg):
lines = msg.split('\n')
@@ -28,13 +33,21 @@ def important_print(msg):
print(information)
print('-' * (max_len + 8))
def render_test(gltf_viewer, test_config, output_dir,
opengl_lib=None, vk_icd=None):
RESULT_OK = 'ok'
RESULT_FAILED_TO_RENDER = 'failed-to-render'
RESULT_FAILED_IMAGE_DIFF = 'failed-image-diff'
RESULT_FAILED_NO_GOLDEN = 'failed-no-golden'
def run_test(gltf_viewer,
test_config,
output_dir,
opengl_lib=None,
vk_icd=None):
assert os.path.isdir(output_dir), f"output directory {output_dir} does not exist"
assert os.access(gltf_viewer, os.X_OK)
named_output_dir = os.path.join(output_dir, test_config.name)
execute(f'mkdir -p {named_output_dir}')
mkdir_p(named_output_dir)
results = []
for test in test_config.tests:
@@ -60,43 +73,45 @@ def render_test(gltf_viewer, test_config, output_dir,
important_print(f'Rendering {test_desc}')
res, _ = execute(f'{gltf_viewer} -a {backend} --batch={test_json_path} -e {model_path} --headless',
env=env, capture_output=False)
out_code, _ = execute(
f'{gltf_viewer} -a {backend} --batch={test_json_path} -e {model_path} --headless',
env=env, capture_output=False
)
if res == 0:
execute(f'mv -f {test.name}0.tif {named_output_dir}/{out_name}.tif', capture_output=False)
execute(f'mv -f {test.name}0.json {named_output_dir}/{test.name}.json', capture_output=False)
result = ''
if out_code == 0:
result = RESULT_OK
out_tif_basename = f'{out_name}.tif'
out_tif_name = f'{named_output_dir}/{out_tif_basename}'
mv_f(f'{test.name}0.tif', out_tif_name)
mv_f(f'{test.name}0.json', f'{named_output_dir}/{test.name}.json')
else:
important_print(f'{test_desc} failed with error={res}')
print('')
result = RESULT_FAILED_TO_RENDER
important_print(f'{test_desc} rendering failed with error={out_code}')
results.append((out_name, res))
return results
results.append({
'name': out_name,
'result': result,
'result_code': out_code,
})
return named_output_dir, results
GOLDENS_DIR = 'renderdiff_goldens'
def compare_goldens(render_results, output_dir, goldens):
for result in render_results:
if result['result'] != RESULT_OK:
continue
# We pull the goldens from the filament-assets repo
def pull_goldens(output_dir):
assert os.path.isdir(output_dir), f"output directory {output_dir} does not exist"
golden_dir = os.path.join(output_dir, "golden")
assets_dir = os.path.join(output_dir, "filament-assets")
out_tif_basename = f"{result['name']}.tif"
out_tif_name = f'{output_dir}/{out_tif_basename}'
golden_path = goldens.get(out_tif_basename)
if not golden_path:
result['result'] = RESULT_FAILED_NO_GOLDEN
result['result_code'] = 1
elif not same_image(golden_path, out_tif_name):
result['result'] = RESULT_FAILED_IMAGE_DIFF
result['result_code'] = 1
if not os.path.exists(assets_dir):
execute('git clone --depth 1 git@github.com:google/filament-assets.git', cwd=output_dir)
else:
execute('git fetch', cwd=assets_dir)
execute('git checkout main ', cwd=assets_dir)
execute('git rebase', cwd=assets_dir)
if os.path.exists(golden_dir):
execute('rm -f goldens/*', cwd=output_dir)
execute(f'cp filament-assets/{GOLDENS_DIR}/* goldens', cwd=output_dir)
def push_goldens(output_dir, test_name, filter_func=lambda a:True):
for test in test_config.tests:
for backend in test_config.backends:
for model in test.models:
pass
return render_results
if __name__ == "__main__":
parser = ArgParseImpl()
@@ -105,12 +120,43 @@ if __name__ == "__main__":
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')
parser.add_argument('--golden_branch', help='Branch of the golden repo to compare against')
args, _ = parser.parse_known_args(sys.argv[1:])
test = parse_test_config_from_path(args.test)
render_result = render_test(args.gltf_viewer, test, args.output_dir, opengl_lib=args.opengl_lib, vk_icd=args.vk_icd)
failed = [f' {tname}' for tname, res in render_result if res != 0]
success_count = len(render_result) - len(failed )
important_print(f'Successfully rendered {success_count} / {len(render_result)}' +
output_dir, results = \
run_test(args.gltf_viewer,
test,
args.output_dir,
opengl_lib=args.opengl_lib,
vk_icd=args.vk_icd)
do_compare = False
# The presence of this argument indicates comparison against a set of goldens.
if args.golden_branch:
# prepare goldens working directory
tmp_golden_dir = '/tmp/renderdiff-goldens'
mkdir_p(tmp_golden_dir)
# Download the golden repo into the current working directory
golden_manager = GoldenManager(os.getcwd())
golden_manager.download_to(tmp_golden_dir, branch=args.golden_branch)
goldens = {
os.path.basename(fpath) : fpath for fpath in \
glob.glob(f'{os.path.join(tmp_golden_dir, test.name)}/**/*.tif', recursive=True)
}
results = compare_goldens(results, output_dir, goldens)
do_compare = True
with open(f'{output_dir}/results.json', 'w') as f:
f.write(json.dumps(results))
shutil.copy2(args.test, f'{output_dir}/test.json')
failed = [f" {k['name']}" for k in results if k['result'] != RESULT_OK]
success_count = len(results) - len(failed)
op = 'tested' if do_compare else 'rendered'
important_print(f'Successfully {op} {success_count} / {len(results)}' +
('\nFailed:\n' + ('\n'.join(failed)) if len(failed) > 0 else ''))

View File

@@ -0,0 +1,171 @@
# Copyright (C) 2025 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 sys
import os
import glob
import time
from golden_manager import GoldenManager, ACCESS_TYPE_SSH, ACCESS_TYPE_TOKEN
from image_diff import same_image
from utils import execute, ArgParseImpl
from utils import prompt_helper, PROMPT_YES, PROMPT_NO
def line_prompt(prompt, validator=lambda a:True):
while True:
res = input(f'{prompt} => ').strip()
if validator(res):
return res
return None
CONFIG_NEW_SRC_DIR = 'goldens_dir'
CONFIG_GOLDENS_BRANCH = 'goldens_branch'
CONFIG_GOLDENS_UPDATES = 'goldens_updates'
CONFIG_GOLDENS_DELETES = 'goldens_deletes'
CONFIG_AUTO_COMMIT = 'auto-commit'
CONFIG_COMMIT_MSG = 'commit_msg'
def _get_current_branch():
code, res = execute('git branch --show-current')
return res.strip()
def _file_as_str(fpath):
with open(fpath, 'r') as f:
return f.read()
def _do_update(golden_manager, config):
deletes = config[CONFIG_GOLDENS_DELETES]
updates = config[CONFIG_GOLDENS_UPDATES]
if len(deletes) == 0 and len(updates) == 0:
print('Nothing to update. Exiting...')
exit(0)
branch = config[CONFIG_GOLDENS_BRANCH]
src_dir = config[CONFIG_NEW_SRC_DIR]
auto_commit = config[CONFIG_AUTO_COMMIT]
commit_msg = config[CONFIG_COMMIT_MSG]
golden_manager.source_from(src_dir, commit_msg, branch,
updates=updates,
deletes=deletes,
push_to_remote=auto_commit)
def _get_deletes_updates(update_dir, golden_dir):
ret_delete = []
ret_update = []
for ext in ['tif', 'json']:
base = set(glob.glob(f'./**/*.{ext}', root_dir=golden_dir, recursive=True))
new = set(glob.glob(f'./**/*.{ext}', root_dir=update_dir, recursive=True))
delete = list(base - new)
update = list(new - base)
for fpath in base.intersection(new):
base_fpath = os.path.join(golden_dir, fpath)
new_fpath = os.path.join(update_dir, fpath)
if (ext == 'tif' and not same_image(new_fpath, base_fpath)) or \
(ext == 'json' and _file_as_str(new_fpath) != _file_as_str(base_fpath)):
update.append(fpath)
ret_update += update
ret_delete += delete
return ret_delete, ret_update
# Ask a bunch of questions to gather the configuration for the update
def _interactive_mode(base_golden_dir):
config = {}
cur_branch = _get_current_branch()
if prompt_helper(
f'Generate the new goldens from your local ' \
f'Filament branch? (branch={cur_branch})') == PROMPT_YES:
code, res = execute('bash ./test/renderdiff/test.sh generate',
capture_output=False)
if code != 0:
print('Failed to generate new goldens')
exit(1)
config[CONFIG_NEW_SRC_DIR] = os.path.join(os.getcwd(), './out/renderdiff_tests/')
else:
def validator(src_dir):
if not os.path.exists(src_dir):
print(f'Cannot find directory {src_dir}. Please try again.')
return False
return True
config[CONFIG_NEW_SRC_DIR] = line_prompt(
'Please provide path of directory containing new goldens',
validator)
if prompt_helper(f'Update new goldens to branch={cur_branch}? '
'(Note that this refers to a branch in the goldens repo, not the Filament repo.)'
) == PROMPT_YES:
config[CONFIG_GOLDENS_BRANCH] = cur_branch
else:
config[CONFIG_GOLDENS_BRANCH] = line_prompt('Please provide new branch name for update')
if prompt_helper(f'Provide a commit message?') == PROMPT_YES:
config[CONFIG_COMMIT_MSG] = line_prompt('Message:')
else:
config[CONFIG_COMMIT_MSG] = f'Update {time.time()} from filament ({cur_branch})'
new_golden_dir = config[CONFIG_NEW_SRC_DIR]
deletes, updates = _get_deletes_updates(new_golden_dir, base_golden_dir)
if len(deletes) + len(updates) != 0:
prompt = 'The following files will be changed:\n' + \
'\n'.join([f' {fname} [delete]' for fname in deletes]) + \
'\n'.join([f' {fname} [update]' for fname in updates]) + \
'\nIs that ok?'
if prompt_helper(prompt) == PROMPT_YES:
config[CONFIG_GOLDENS_DELETES] = deletes
config[CONFIG_GOLDENS_UPDATES] = updates
else:
# We cannot proceed if user answered no.
exit(1)
else:
config[CONFIG_GOLDENS_DELETES] = []
config[CONFIG_GOLDENS_UPDATES] = []
config[CONFIG_AUTO_COMMIT] = \
prompt_helper(f'Commit golden repo changes to remote?') == PROMPT_YES
return config
if __name__ == "__main__":
parser = ArgParseImpl()
parser.add_argument('--branch', help='Branch of the golden repo to write to')
parser.add_argument('--source', help='Directory containing the new goldens')
parser.add_argument('--commit-msg', help='Message for the commit to the golden repo')
parser.add_argument('--golden-repo-token', help='Access token for the golden repo')
args, _ = parser.parse_known_args(sys.argv[1:])
config = {}
golden_manager = GoldenManager(
os.getcwd(),
access_type=ACCESS_TYPE_SSH if not args.golden_repo_token else ACCESS_TYPE_TOKEN,
access_token=args.golden_repo_token
)
base_golden_dir = golden_manager.directory()
if args.branch and args.source and args.commit_msg:
assert os.path.exists(args.source), f'{args.source} (--source) directory not found'
deletes, updates = _get_deletes_updates(args.source, base_golden_dir)
config = {
CONFIG_AUTO_COMMIT: True,
CONFIG_GOLDENS_BRANCH: args.branch,
CONFIG_NEW_SRC_DIR: args.source,
CONFIG_GOLDENS_UPDATES: updates,
CONFIG_GOLDENS_DELETES: deletes,
CONFIG_COMMIT_MSG: args.commit_msg,
}
else:
config = _interactive_mode(base_golden_dir)
_do_update(golden_manager, config)

View File

@@ -16,6 +16,7 @@ import subprocess
import os
import argparse
import sys
import pathlib
def execute(cmd,
cwd=None,
@@ -66,3 +67,42 @@ class ArgParseImpl(argparse.ArgumentParser):
sys.stderr.write('error: %s\n' % message)
self.print_help()
sys.exit(1)
PROMPT_YES = 'y'
PROMPT_NO = 'n'
PROMPT_YES_NO = f'{PROMPT_YES}{PROMPT_NO}'
class GetCh:
def __init__(self):
pass
def __call__(self):
import sys, tty, termios
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
getch = GetCh()
def prompt_helper(prompt_str, keys=PROMPT_YES_NO):
while True:
print(f'{prompt_str}: [' + ', '.join(keys) + '] => ', end='', flush=True)
val = getch()
print(val)
if val in keys or ord(val) == 3: # If user pressed Ctrl+c
if ord(val) == 3:
exit(1)
return val
def mkdir_p(path_str):
pathlib.Path(path_str).mkdir(parents=True, exist_ok=True)
def mv_f(src_str, dst_str):
src = pathlib.Path(src_str)
src.replace(dst_str)

View File

@@ -17,18 +17,29 @@ from utils import execute
def get_last_commit():
res, o = execute('git log -1')
commit, author, date, _, title, *desc = o.split('\n')
commit = commit.split(' ')[1]
title = title.strip()
desc = [l.strip() for l in desc[1:]]
if len(desc) > 0 and len(desc[0]) == 0:
while len(desc) > 0 and len(desc[0]) == 0:
desc = desc[1:]
return (
commit.split(' ')[1],
title.strip(),
desc)
commit,
title,
desc
)
def sanitized_split(line, split_atom='\n'):
return list(filter(lambda x: len(x) > 0, map(lambda x: x.strip(), line.split(split_atom))))
return list(
filter(
lambda x: len(x) > 0,
map(
lambda x: x.strip(),
line.split(split_atom)
)
)
)
RDIFF_UPDATE_GOLDEN_STR = 'RDIFF_UPDATE_GOLDEN'

View File

@@ -18,6 +18,7 @@ OUTPUT_DIR="$(pwd)/out/renderdiff_tests"
RENDERDIFF_TEST_DIR="$(pwd)/test/renderdiff"
TEST_UTILS_DIR="$(pwd)/test/utils"
MESA_DIR="$(pwd)/mesa/out/"
VENV_DIR="$(pwd)/venv"
os_name=$(uname -s)
if [[ "$os_name" == "Linux" ]]; then
@@ -29,9 +30,33 @@ else
exit 1
fi
function prepare_mesa() {
if [ ! -d ${MESA_LIB_DIR} ]; then
bash ${TEST_UTILS_DIR}/get_mesa.sh
function start_() {
if [[ "$GITHUB_WORKFLOW" ]]; then
set -ex
else
if [ ! -d ${MESA_LIB_DIR} ]; then
bash ${TEST_UTILS_DIR}/get_mesa.sh
fi
# Install python deps
python3 -m venv ${VENV_DIR}
source ${VENV_DIR}/bin/activate
NEEDED_PYTHON_DEPS=("numpy" "tifffile")
for cmd in "${NEEDED_PYTHON_DEPS[@]}"; do
if ! python3 -m pip show -q "${cmd}"; then
python3 -m pip install ${cmd}
fi
done
fi
}
function end_() {
if [[ "$GITHUB_WORKFLOW" ]]; then
set +ex
else
deactivate # End python virtual env
fi
}
@@ -41,11 +66,19 @@ function prepare_mesa() {
# - Run the python script that runs the test
# - Zip up the result
set -ex && prepare_mesa && \
GOLDEN_BRANCH_PARAM='--golden_branch=main'
if [ "$1" == "generate" ]; then
GOLDEN_BRANCH_PARAM=''
fi
start_ && \
mkdir -p ${OUTPUT_DIR} && \
CXX=`which clang++` CC=`which clang` ./build.sh -X ${MESA_DIR} -p desktop debug gltf_viewer && \
python3 ${RENDERDIFF_TEST_DIR}/src/run.py \
CXX=`which clang++` CC=`which clang` ./build.sh -f -X ${MESA_DIR} -p desktop debug gltf_viewer && \
python3 ${RENDERDIFF_TEST_DIR}/src/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}
--opengl_lib=${MESA_LIB_DIR} \
${GOLDEN_BRANCH_PARAM} && \
end_

View File

@@ -14,9 +14,9 @@
#!/usr/bin/bash
set -x
set -e
if [[ "$GITHUB_WORKFLOW" ]]; then
set -e
set -x
fi
OS_NAME=$(uname -s)
@@ -35,7 +35,7 @@ source ${ORIG_DIR}/venv/bin/activate
NEEDED_PYTHON_DEPS=("mako" "setuptools" "pyyaml")
for cmd in "${NEEDED_PYTHON_DEPS[@]}"; do
if ! python3 -m pip show "${cmd}" >/dev/null 2>&1; then
if ! python3 -m pip show -q "${cmd}" >/dev/null 2>&1; then
python3 -m pip install ${cmd}
fi
done
@@ -145,6 +145,6 @@ deactivate
popd
if [[ "$GITHUB_WORKFLOW" ]]; then
set +e
set +x
fi
set +x
set +e

View File

@@ -1,6 +1,6 @@
{
"name": "filament",
"version": "1.59.4",
"version": "1.59.5",
"description": "Real-time physically based rendering engine",
"main": "filament.js",
"module": "filament.js",