Compare commits

..

52 Commits

Author SHA1 Message Date
Benjamin Doherty
2730fbc31b Add FILAMENT_DEBUG_COMMANDS_HISTOGRAM compile flag 2026-02-24 12:41:24 -08:00
Benjamin Doherty
7d3b8eb7b9 Print a compressed histogram 2026-02-24 12:24:54 -08:00
Benjamin Doherty
557387375f Print command stream histogram before crashing 2026-02-24 10:02:12 -08:00
Ben Doherty
902f869721 Metal: recreate sidecar texture if sample count changes (#9430) 2026-02-23 09:54:21 -08:00
Eliza
ad1bc6f360 engine: fix VSM (#9737) 2026-02-20 15:08:59 -08:00
Sungun Park
73c343635e Turn off UBO batching (#9736)
BUGS=[486200381]
2026-02-20 20:04:05 +00:00
Mathias Agopian
432e672022 Revert "Swap logic of how the EGL display is initialized. (#9634)" (#9729)
This reverts commit c35ae6571f.

BUGS=[481534922, 478925865]
2026-02-20 08:34:51 -08:00
Doris Wu
b56b04c5f8 Fix translucent objects are pickable when skybox is disabled (#9688) 2026-02-20 11:36:58 +08:00
Filament Bot
99816d67c2 [automated] Updating /docs due to commit d6d4f92
Full commit hash is d6d4f92922

DOCS_ALLOW_DIRECT_EDITS
2026-02-19 20:03:46 +00:00
Mathias Agopian
d6d4f92922 fix intensities (#9728)
DOCS_FORCE
2026-02-19 11:59:34 -08:00
Powei Feng
6a59a68622 gl: update record when detaching stream (#9712)
FIXES=483744050
2026-02-19 18:33:04 +00:00
Filament Bot
4580f57987 [automated] Updating /docs due to commit 38f7e57
Full commit hash is 38f7e579f1

DOCS_ALLOW_DIRECT_EDITS
2026-02-19 17:33:34 +00:00
Benjamin Doherty
38f7e579f1 Release Filament 1.69.3 2026-02-19 09:30:12 -08:00
Powei Feng
9b1c8a2bf5 backend: add R11G11B10 format as input to reshaper (#9722) 2026-02-19 00:06:23 -08:00
rafadevai
4504471021 Vulkan: Implement stencil state in pipeline cache (#9716)
Store and set the stencil state for a graphics pipeline.
2026-02-18 20:09:07 +00:00
Powei Feng
37c316fa03 backend: reduce size increase of recent datareshaper change (#9723)
The previous datareshaper change used too multiple parameters
in templates in an attempt to reduce conditionals in the hot-loop.
However, this dramatically increased the binary size.

This change keeps the original intention of not having conditional
in the inner loop, but isolate small sections that need to be
templated into separate components.
2026-02-18 18:12:06 +00:00
Serge Metral
14960f7118 Protected content fix vulkan 2 (#9721)
* Adding the begin frame message for later xtrace post processing.

* Add the flags.

* Cleanup

* Cleanup

---------

Co-authored-by: Powei Feng <powei@google.com>
2026-02-18 09:50:17 -08:00
Powei Feng
1deb657442 github: add sizeguard presubmit test (#9719)
- Add a presubmit check after the android build to check the
   built artifact sizes against previous build sizes.
 - If the size built is +20K over the previous, then the test
   fails.
 - This prevents dramatic size increases of our mobile build
2026-02-18 08:41:58 +00:00
Powei Feng
45c0d1b34f backend: enhance data reshaper (#9711)
- add half float as source type
 - gray-scaled output when src-channel=1 and dest-channel=3 or 4
 - Ensure that per-pixel work is as-constexpr-as-possible
2026-02-17 20:14:34 +00:00
chenriji
1ddd10f326 Fix: Morph Target animations (Blend Shapes) fail to render when loading GLB files via ResourceLoader (#9696)
* fixbug:"debug error, reason: The material 'Material_MR' has not been compiled to include the required GLSL or SPIR-V chunks for the vertex shader (variant=5, filtered=5)",Because the member variable mVariantFilter is not initialized, random values will appear on Windows 10 or others platform, which eventually causes some variants to be filtered out by this mVariantFilter.

* fix:Fix morph target loading for accessors without buffer_view

  Morph targets were not working because ResourceLoader skipped all
  accessors without buffer_view. For morph targets, the data can be
  accessed directly via cgltf_accessor_unpack_floats().

  This fix properly unpacks and uploads morph target vertex data to the
  GPU, enabling blendshapes and facial deformation to work correctly.

Steps to Reproduce
1、In Unity (2022.3.11): Create a Prefab with Blend Shapes (Morph Targets) and an Animator to control them (e.g., an animation clip that makes the eyes squint).
2、Export: Use the UnityGLTF tool to export the model as a .glb file (including the Animator and Morph Target tracks).
3、In Filament: Load and play the animation.
4、Result: The skeletal animation (bone-based) may play, but the Morph Target effect (squinting) is missing or static.
2026-02-13 23:07:17 +00:00
Powei Feng
308668a705 android: render-validation sample UI and additions (#9692)
- Update UI
 - Wait for resources to finish loading before testing
 - Refactor validation to input/output
 - Move assets to be generated from the repo
 - Fix AutomationEngine java binding to add missing fields
2026-02-13 21:53:20 +00:00
Powei Feng
1cd48619e3 github: add scripts to store size of android release artifact (#9705)
- Add a script to test/sizeguard to compute sizes of artifacts
   within a compressed file.
- Add a step to postsubmit-main.yml that will run the above
   script on every commit to main.

This will enable us to add a presubmit step to guard against
dramatic size increases in the Android build.
2026-02-13 20:31:23 +00:00
Sungun Park
89c3b3f40b tools: harden zbloat against command injection (#9715)
This fixes #9701 by replacing shell execution (`shell=True` and
`os.system`) with direct subprocess calls using argument lists in
`tools/zbloat/zbloat.py`

Previously, the script used f-strings to pass paths directly into a
shell command, which created an unnecessary risk: if an external archive
contained files with shell metacharacters, it could lead to accidental
or malicious command execution during analysis.

By passing arguments as lists, the subprocess module maps them directly
to the executable, bypassing the system shell and eliminating the
vulnerability.
2026-02-13 12:03:13 -08:00
Doris Wu
e830ec28e4 Prevent circular buffer overflow during UboManager reallocation (#9714)
When UboManager::reallocate() is triggered, a large number of material instances may be invalidated simultaneously. This leads to a massive spike in descriptor set updates and command generation, which can overflow the circular buffer.

To prevent this, we now flush commands in batches, we trigger a flush whenever the command buffer usage exceeds half of its capacity. (Like what RenderPass::Executor::execute does)

BUGS = [474264976, 479079631]
2026-02-14 00:39:14 +08:00
Sungun Park
b58ffb87e0 Revert "Set multiview as the default for stereoscopic rendering (#9682)" (#9713)
This reverts commit f10a7d9bbc.
2026-02-12 22:13:18 +00:00
rafadevai
385d8969cf VK: Fix AHB import validation error (#9710)
In some cases the external image to be imported is
flagged as using a sRGB dataspace but when the AHB
is imported its actual format is UNDEFINED and
requires an external sampler.

In these cases, its not valid to set the
VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT flag.

See VUID-VkImageCreateInfo-pNext-02396

FIXES=[483456747]
2026-02-12 10:19:01 -08:00
Ben Doherty
53bc372876 Log Filament command buffer statistics (#9709) 2026-02-12 12:01:22 -05:00
Andrew Wilson
58f6d77e78 Add build testing checks for all the tests (#9684) 2026-02-11 12:53:40 -08:00
Filament Bot
3769d0a9d3 [automated] Updating /docs due to commit 2bc7124
Full commit hash is 2bc71240cf

DOCS_ALLOW_DIRECT_EDITS
2026-02-11 19:19:32 +00:00
Ramon
2bc71240cf Fix incorrect table structure in README (#9707)
The README had linebreaks that broke the badges in the table.
2026-02-11 19:16:56 +00:00
rafadevai
e1fb3f7442 VK: Fix validation errors when creating a sampler (#9708)
When creating a sampler with a chroma conversion
anisotropic must be disabled and the addressModes
must be set to CLAMP_TO_EDGE.

FIXES = [483454760]
2026-02-11 18:56:22 +00:00
Sungun Park
e832805faf Add cube sample for Web using Feature Level 0 (#9699)
Introduce a new cube sample that utilizes the feature level 0 (FL0) on
the web. The `Engine.create` method accepts `config` as an argument,
allowing users to explicitly request a GLES 2.0 context.

In addition, this change helps a future implementation of asynchronous
support for web, allowing the asynchronous mode to be turned on via the
configuration setting.

BUGS=[476134614]
2026-02-11 09:59:25 -08:00
Filament Bot
2ce71d6d98 [automated] Updating /docs due to commit 26c51e0
Full commit hash is 26c51e0d9a

DOCS_ALLOW_DIRECT_EDITS
2026-02-11 00:10:00 +00:00
Romain Guy
26c51e0d9a Update README with new badge links and features (#9704)
- Replace broken Maven badge links with shields. links
- Updated missing rendering features
2026-02-11 00:07:31 +00:00
rafadevai
510ae15867 VK: Fix the descriptor set layout mismatch errors (#9703)
* VK: Fix the descriptor set layout mismatch errors

Depending on how the descriptor sets and external
sampled images are used is possible to get a lot of
errors about the pipeline layout and the descriptor
set layouts dont match with the validation layers.

This causes flicking artifacts and textures not being
displayed.

This is a partial rewrite of the implementation when
using external samplers through a MaterialInstance.

* Addressing review comments
2026-02-10 15:45:34 -08:00
Powei Feng
d6caa9dc0b diffimg: binary tool for comparing images (#9668)
This tool uses existing libraries: image, imageio, imageio-lite,
imagediff to perform difference comparison for on-disk images.

We refactor renderdiff to use this tool instead of using
python dependencies.


Co-authored-by: Ben Doherty <bendoherty@google.com>
2026-02-10 22:42:30 +00:00
Filament Bot
19209a00e6 [automated] Updating /docs due to commit 188113b
Full commit hash is 188113bad6

DOCS_ALLOW_DIRECT_EDITS
2026-02-10 21:34:43 +00:00
Benjamin Doherty
188113bad6 Update Maven release guide 2026-02-10 16:30:08 -05:00
Powei Feng
5916837318 metal: add readTexture implementation (#9678)
- Also added a test to verify the y-flipping mechanism of
   each backend produces the same rendering across backends.
2026-02-10 19:45:30 +00:00
Ben Doherty
27aa517c48 Add new Filament Gradle plugin (#9694) 2026-02-10 14:15:45 -05:00
Filament Bot
4622e88a6b [automated] Updating /docs due to commit 9bdb6ac
Full commit hash is 9bdb6acd63

DOCS_ALLOW_DIRECT_EDITS
2026-02-10 19:12:10 +00:00
Benjamin Doherty
9bdb6acd63 Update CocoaPods documentation on testing locally 2026-02-10 14:09:25 -05:00
Filament Bot
751d213145 [automated] Updating /docs due to commit 0c3ae45
Full commit hash is 0c3ae457a6

DOCS_ALLOW_DIRECT_EDITS
2026-02-10 18:27:57 +00:00
Sungun Park
0c3ae457a6 Release Filament 1.69.2 2026-02-10 10:22:22 -08:00
Anish Goyal
92d4be6923 Fix framebuffer cache memory leak (#9693)
Once a framebuffer is destroyed, it must be removed from the mapping,
and not just marked unused. Same for render passes.
2026-02-09 23:36:44 +00:00
Powei Feng
ad8c188f58 3p: update robin-map to 1.4.1 (#9698) 2026-02-09 14:01:48 -08:00
Ben Doherty
9716b3924b Changes necessary for GCC (#9672) 2026-02-09 13:36:17 -05:00
Anish Goyal
ae9b951b08 Fix a set of unmapped 3 channel formats (#9690)
These three formats would likely break even if used on a device where
they are supported, as we reshape all 3-channel buffers to 4 channel.
The data would be in the wrong format.

We will have to follow-up, as there are other formats that are affected,
but those formats likely have better driver support, as they are better
aligned formats (e.g. 16-bit, 32-bit). These three formats were 96-bit
formats, which we've remapped to 128-bit.
2026-02-09 17:52:22 +00:00
Mathias Agopian
78a0d8f4f6 use correct attachments flags for the current swapchain (#9689)
the current swapchain is associated to the default rendertarget, but
the later doesn't know which attachments the swapchain actually has.
it used to hardcode COLOR and DEPTH, but it's possible for the
swapchain to have a STENCIL attachment.

The proper way to do this is to use the swapchain's attachments when
the default rendertarget is used (which means the swapchain is used).

FIXES=[482120868]
2026-02-06 12:16:04 -08:00
Anish Goyal
675d8bc5be Prevent compilation of redundant pipelines (#9681)
If we've already destroyed a program, we shouldn't run prewarm for it,
as that could entail five pipeline compilations.
2026-02-05 23:39:15 +00:00
Patrick Ribas
a90019baa2 Remove nonlinear fog from FL0 (#9658)
* Remove fog and lighting uniforms from FL0

---------

Co-authored-by: Mathias Agopian <mathias@google.com>
2026-02-05 12:51:37 -08:00
Filament Bot
72997ee71e [automated] Updating /docs due to commit 5b63105
Full commit hash is 5b631056b1

DOCS_ALLOW_DIRECT_EDITS
2026-02-05 06:04:40 +00:00
166 changed files with 10180 additions and 5520 deletions

View File

@@ -6,6 +6,8 @@ on:
- main
jobs:
# Update the renderdiff goldens in filament-assets. This will add or merge the new goldens from
# a branch on filament-assets.
update-renderdiff-goldens:
name: update-renderdiff-goldens
runs-on: 'ubuntu-24.04-4core'
@@ -16,8 +18,8 @@ jobs:
- uses: ./.github/actions/linux-prereq
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- name: Prerequisites
run: pip install tifffile numpy
- name: Build diffimg
run: ./build.sh release diffimg
- name: Run update script
env:
GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }}
@@ -35,6 +37,7 @@ jobs:
--merge-to-main --filament-tag=${COMMIT_HASH} --golden-repo-token=${GH_TOKEN}
fi
# Update the /docs (offiicla github-hosted Filament site) if necessary
update-docs:
name: update-docs
runs-on: 'ubuntu-24.04-4core'
@@ -57,3 +60,40 @@ jobs:
git config --global user.name "Filament Bot"
git config --global credential.helper cache
bash docs_src/build/postsubmit.sh ${COMMIT_HASH} ${GH_TOKEN}
# Produce a json that describes the android artifact sizes, and will push that json to a folder in
# filament-assets
update-sizeguard:
name: update-sizeguard
runs-on: 'ubuntu-24.04-16core'
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- name: Build and generate size report
run: |
cd build/android && printf "y" | ./build.sh release all
cd ../..
COMMIT_HASH="${{ steps.get_commit_msg.outputs.hash }}"
python3 test/sizeguard/dump_artifact_size.py out/*.tgz out/*.aar > "${COMMIT_HASH}.json"
- name: Push to filament-assets
env:
GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }}
run: |
COMMIT_HASH="${{ steps.get_commit_msg.outputs.hash }}"
git config --global user.email "filament.bot@gmail.com"
git config --global user.name "Filament Bot"
git clone https://x-access-token:${GH_TOKEN}@github.com/google/filament-assets.git filament-assets
mkdir -p filament-assets/sizeguard
mv "${COMMIT_HASH}.json" filament-assets/sizeguard/
cd filament-assets
git add sizeguard/"${COMMIT_HASH}.json"
git commit -m "Update sizeguard for filament@${COMMIT_HASH}" || echo "No changes to commit"
git push https://x-access-token:${GH_TOKEN}@github.com/google/filament-assets.git main

View File

@@ -67,7 +67,16 @@ jobs:
# Only build 1 64 bit target during presubmit to cut down build times during presubmit
# Continuous builds will build everything
run: |
cd build/android && printf "y" | ./build.sh presubmit arm64-v8a
pushd .
cd build/android && printf "y" | ./build.sh presubmit-with-archive arm64-v8a
popd
- name: Check artifact sizes
run: |
python3 test/sizeguard/dump_artifact_size.py out/*.aar > current_size.json
python3 test/sizeguard/check_size.py current_size.json \
--target-branch origin/main \
--threshold 20480 \
--artifacts filament-android-release.aar/jni/arm64-v8a/libfilament-jni.so
build-ios:
name: build-iOS
@@ -125,7 +134,6 @@ jobs:
uses: ./.github/actions/get-commit-msg
- name: Prerequisites
run: |
pip install tifffile numpy
# Must have at least clang-16 for a webgpu/dawn build.
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
shell: bash
@@ -139,6 +147,9 @@ jobs:
set -eux
GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 ${TEST_DIR}/src/commit_msg.py)
bash ${TEST_DIR}/generate.sh
# Build diffimg tool
./build.sh release diffimg
python3 ${TEST_DIR}/src/golden_manager.py \
--branch=${GOLDEN_BRANCH} \
--output=${GOLDEN_OUTPUT_DIR}
@@ -149,7 +160,9 @@ jobs:
python3 ${TEST_DIR}/src/compare.py \
--src=${GOLDEN_OUTPUT_DIR} \
--dest=${RENDER_OUTPUT_DIR} \
--out=${DIFF_OUTPUT_DIR} 2>&1 | tee compare_output.txt
--out=${DIFF_OUTPUT_DIR} \
--diffimg="$(pwd)/out/cmake-release/tools/diffimg/diffimg" \
--test="${TEST_DIR}/tests/presubmit.json" 2>&1 | tee compare_output.txt
if grep "Failed" compare_output.txt > /dev/null; then
DELIMITER="EOF_FILE_CONTENT_$(date +%s)" # Using timestamp to make it more unique

View File

@@ -49,6 +49,8 @@ option(FILAMENT_ENABLE_COVERAGE "Enable LLVM code coverage" OFF)
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)
option(FILAMENT_ENABLE_FGVIEWER "Enable the frame graph viewer" OFF)
@@ -63,6 +65,8 @@ option(FILAMENT_SUPPORTS_WEBP_TEXTURES "Enable webp texture support for Filament
# On the regular filament build (where size is of less concern), we enable GTAO by default.
option(FILAMENT_DISABLE_GTAO "Disable GTAO" OFF)
option(FILAMENT_BUILD_TESTING "Build tests" ON)
set(FILAMENT_NDK_VERSION "" CACHE STRING
"Android NDK version or version prefix to be used when building for Android."
)
@@ -605,6 +609,23 @@ else()
option(FILAMENT_DISABLE_MATOPT "Disable material optimizations" ON)
endif()
# This only affects the prebuilt shader files in gltfio and samples, not filament library.
# The value can be either "instanced", "multiview", or "none"
set(FILAMENT_SAMPLES_STEREO_TYPE "none" CACHE STRING
"Stereoscopic type that shader files in gltfio and samples are built for."
)
string(TOLOWER "${FILAMENT_SAMPLES_STEREO_TYPE}" FILAMENT_SAMPLES_STEREO_TYPE)
if (NOT FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "instanced"
AND NOT FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "multiview"
AND NOT FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "none")
message(FATAL_ERROR "Invalid stereo type: \"${FILAMENT_SAMPLES_STEREO_TYPE}\" choose either \"instanced\", \"multiview\", or \"none\" ")
endif ()
# Compiling samples for multiview implies enabling multiview feature as well.
if (FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "multiview")
set(FILAMENT_ENABLE_MULTIVIEW ON)
endif ()
# Define backend flag for debug only
if (CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT FILAMENT_BACKEND_DEBUG_FLAG STREQUAL "")
add_definitions(-DFILAMENT_BACKEND_DEBUG_FLAG=${FILAMENT_BACKEND_DEBUG_FLAG})
@@ -955,6 +976,7 @@ if (IS_HOST_PLATFORM)
add_subdirectory(${TOOLS}/cmgen)
add_subdirectory(${TOOLS}/cso-lut)
add_subdirectory(${TOOLS}/diffimg)
add_subdirectory(${TOOLS}/filamesh)
add_subdirectory(${TOOLS}/glslminifier)
add_subdirectory(${TOOLS}/matc)

View File

@@ -6,3 +6,5 @@
appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md).
## Release notes for next branch cut
- engine: fix crash when using variance shadow maps

View File

@@ -31,7 +31,7 @@ repositories {
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.69.2'
implementation 'com.google.android.filament:filament-android:1.69.3'
}
```
@@ -39,19 +39,18 @@ Here are all the libraries available in the group `com.google.android.filament`:
| Artifact | Description |
| ------------- | ------------- |
| [![filament-android](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android/badge.svg?subject=filament-android)](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android) | The Filament rendering engine itself. |
| [![filament-android-debug](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android-debug/badge.svg?subject=filament-android-debug)](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android-debug) | Debug version of `filament-android`. |
| [![gltfio-android](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/gltfio-android/badge.svg?subject=gltfio-android)](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/gltfio-android) | A glTF 2.0 loader for Filament, depends on `filament-android`. |
| [![filament-utils-android](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-utils-android/badge.svg?subject=filament-utils-android)](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-utils-android) | KTX loading, Kotlin math, and camera utilities, depends on `gltfio-android`. |
| [![filamat-android](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android/badge.svg?subject=filamat-android)](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android) | A runtime material builder/compiler. This library is large but contains a full shader compiler/validator/optimizer and supports both OpenGL and Vulkan. |
| [![filamat-android-lite](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android-lite/badge.svg?subject=filamat-android-lite)](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android-lite) | A much smaller alternative to `filamat-android` that can only generate OpenGL shaders. It does not provide validation or optimizations. |
| [![filament-android](https://img.shields.io/maven-central/v/com.google.android.filament/filament-android?label=filament-android&color=green)](https://mvnrepository.com/artifact/com.google.android.filament/filament-android) | The Filament rendering engine itself. |
| [![filament-android-debug](https://img.shields.io/maven-central/v/com.google.android.filament/filament-android-debug?label=filament-android-debug&color=green)](https://mvnrepository.com/artifact/com.google.android.filament/filament-android-debug) | Debug version of `filament-android`. |
| [![gltfio-android](https://img.shields.io/maven-central/v/com.google.android.filament/gltfio-android?label=gltfio-android&color=green)](https://mvnrepository.com/artifact/com.google.android.filament/gltfio-android) | A glTF 2.0 loader for Filament, depends on `filament-android`. |
| [![filament-utils-android](https://img.shields.io/maven-central/v/com.google.android.filament/filament-utils-android?label=filament-utils-android&color=green)](https://mvnrepository.com/artifact/com.google.android.filament/filament-utils-android) | KTX loading, Kotlin math, and camera utilities, depends on `gltfio-android`. |
| [![filamat-android](https://img.shields.io/maven-central/v/com.google.android.filament/filamat-android?label=filamat-android&color=green)](https://mvnrepository.com/artifact/com.google.android.filament/filamat-android) | A runtime material builder/compiler. This library is large but contains a full shader compiler/validator/optimizer and supports both OpenGL and Vulkan. |
### iOS
iOS projects can use CocoaPods to install the latest release:
```shell
pod 'Filament', '~> 1.69.2'
pod 'Filament', '~> 1.69.3'
```
## Documentation
@@ -89,7 +88,8 @@ pod 'Filament', '~> 1.69.2'
- OpenGL ES 3.0+ for Android and iOS
- Metal for macOS and iOS
- Vulkan 1.0 for Android, Linux, macOS, and Windows
- WebGL 2.0 for all platforms
- WebGPU for Android, Linux, macOS, and Windows
- WebGL 2.0 for all browsers supporting it
### Rendering
@@ -124,7 +124,7 @@ pod 'Filament', '~> 1.69.2'
- HDR bloom
- Depth of field bokeh
- Multiple tone mappers: generic (customizable), ACES, filmic, etc.
- Multiple tone mappers: PBR Neutral, AgX, generic (customizable), ACES, filmic, etc.
- Color and tone management: luminance scaling, gamut mapping
- Color grading: exposure, night adaptation, white balance, channel mixer,
shadows/mid-tones/highlights, ASC CDL, contrast, saturation, etc.
@@ -158,15 +158,16 @@ pod 'Filament', '~> 1.69.2'
- [x] KHR_draco_mesh_compression
- [x] KHR_lights_punctual
- [x] KHR_materials_clearcoat
- [x] KHR_materials_dispersion
- [x] KHR_materials_emissive_strength
- [x] KHR_materials_ior
- [x] KHR_materials_pbrSpecularGlossiness
- [x] KHR_materials_sheen
- [x] KHR_materials_specular
- [x] KHR_materials_transmission
- [x] KHR_materials_unlit
- [x] KHR_materials_variants
- [x] KHR_materials_volume
- [x] KHR_materials_specular
- [x] KHR_mesh_quantization
- [x] KHR_texture_basisu
- [x] KHR_texture_transform
@@ -331,7 +332,7 @@ and tools.
- `filamesh`: Mesh converter
- `glslminifier`: Minifies GLSL source code
- `matc`: Material compiler
- `filament-matp`: Material parser
- `matedit`: Material editor for compiled materials
- `matinfo` Displays information about materials compiled with `matc`
- `mipgen` Generates a series of miplevels from a source image
- `normal-blending`: Tool to blend normal maps

View File

@@ -7,6 +7,12 @@ 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.69.4
## v1.69.3
## v1.69.2
- engine: fix shader compilation failure in TAA material

120
android/buildSrc/README.md Normal file
View File

@@ -0,0 +1,120 @@
# Filament Tools Gradle Plugin
## About
The **Filament Tools Gradle Plugin** helps integrate Filament into your Android project. It
automates the use of Filament's command-line tools (`matc`, `cmgen`, and `filamesh`).
This plugin handles:
- **Material Compilation**: Compiles `.mat` material definitions into `.filamat` binaries.
- **IBL Generation**: Generates Image-Based Lighting assets from HDR environment maps.
- **Mesh Compilation**: Converts models into Filament's efficient `.filamesh` binary format. *Note:
This tool is no longer recommended; instead, use glTF and Filament's `gltfio` library for model
loading.*
The plugin hooks directly into the Android build lifecycle (via `preBuild`) and supports incremental
builds, so assets are only recompiled when source files change.
## Usage
Apply the plugin in your module's `build.gradle` file and configure the `filament` block. You can
specify inputs and outputs for any combination of materials, IBLs, or meshes. If a path is not
configured, the corresponding task will be disabled.
### Example Configuration
```groovy
plugins {
id 'filament-plugin'
}
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
iblInputFile = project.layout.projectDirectory.file("path/to/environment.hdr")
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")
meshInputFile = project.layout.projectDirectory.file("path/to/model.obj")
meshOutputDir = project.layout.projectDirectory.dir("src/main/assets/models")
}
```
### Configuration Details
- **materialInputDir**: The directory containing your source material definitions (`.mat` files).
- **materialOutputDir**: The directory where the compiled material files (`.filamat`) will be
generated.
- **iblInputFile**: The source high-dynamic-range image file (e.g., `.hdr` or `.exr`) used to
generate Image Based Lighting assets.
- **iblOutputDir**: The directory where the generated IBL assets (typically `.ktx` files) will be
placed.
- **meshInputFile**: The source mesh file (e.g., `.obj`) to be compiled.
- **meshOutputDir**: The directory where the compiled mesh file (`.filamesh`) will be generated.
Automatically adds tasks to your Android build to compile materials, generate an IBL, and compile a
mesh. The plugin hooks into `preBuild` to ensure assets are generated before the application is
built.
### Configuration Flags
You can control specific compilation options using Gradle properties (e.g., in `gradle.properties`
or via command line `-P`).
- **`com.google.android.filament.exclude-vulkan`** When set to `true`, the Vulkan backend is
excluded from the compiled materials. This can be useful to reduce the size of the generated
assets if your application does not target Vulkan. *Default: `false` (Vulkan is included)*
- **`com.google.android.filament.include-webgpu`** When set to `true`, the WebGPU backend is
included in the compiled materials. Use this if you intend to use the materials in a context
supporting WebGPU. *Default: `false`*
## Tools Configuration
The Filament Tools plugin requires some binary tools to be available: `matc`, `cmgen`, and
`filamesh`.
There are three ways to configure Filament tools:
1. **Point to a local path directly**
```groovy
filament {
matc {
path = "/path/to/matc"
}
cmgen {
path = "/path/to/cmgen"
}
filamesh {
path = "/path/to/filamesh"
}
}
```
2. **Point to a Maven artifact**
```groovy
filament {
matc {
// The minor version (the middle number) must match the Filament dependency's.
artifact = 'com.google.android.filament:matc:1.68.5'
}
cmgen {
artifact = 'com.google.android.filament:cmgen:1.68.5'
}
}
```
*Note that the `filamesh` artifact is not hosted on Maven Central, so it must be provided locally or
via another mechanism if needed.*
Gradle will automatically handle downloading the tool appropriate for your machine (MacOS/Linux/Windows) from Maven.
3. **Set the `com.google.android.filament.tools-dir` Gradle property**
This will override any other configuration. Gradle will attempt to locate the tools under
`<tools-dir>/bin` (e.g., `.../bin/matc`, `.../bin/cmgen`, `.../bin/filamesh`).

View File

@@ -4,13 +4,18 @@ plugins {
gradlePlugin {
plugins {
create("filament-tools-plugin") {
id = "filament-tools-plugin"
implementationClass = "FilamentToolsPlugin"
create("filament-plugin") {
id = "filament-plugin"
implementationClass = "com.google.android.filament.gradle.FilamentPlugin"
}
}
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation "com.google.gradle:osdetector-gradle-plugin:1.7.3"
}

View File

@@ -1,359 +0,0 @@
// This plugin accepts the following parameters:
//
// com.google.android.filament.tools-dir
// Path to the Filament distribution/install directory for desktop.
// This directory must contain bin/matc.
//
// com.google.android.filament.exclude-vulkan
// When set, support for Vulkan will be excluded.
//
// Example:
// ./gradlew -Pcom.google.android.filament.tools-dir=../../dist-release assembleDebug
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileType
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.logging.LogLevel
import org.gradle.api.logging.Logger
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.incremental.InputFileDetails
import org.gradle.api.model.ObjectFactory
import org.gradle.internal.os.OperatingSystem
import org.gradle.process.ExecOperations
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import java.nio.file.Paths
import javax.inject.Inject
abstract class TaskWithBinary extends DefaultTask {
private final String binaryName
private Property<String> binaryPath = null
TaskWithBinary(String name) {
binaryName = name
}
@Inject abstract ObjectFactory getObjects()
@Inject abstract ProviderFactory getProviders()
@Input
Property<String> getBinary() {
if (binaryPath == null) {
def tool = ["/bin/${binaryName}.exe", "/bin/${binaryName}"]
def fullPath = tool.collect { path ->
def filamentToolsPath = providers
.gradleProperty("com.google.android.filament.tools-dir")
.forUseAtConfigurationTime().get()
def directory = objects.fileProperty()
.fileValue(new File(filamentToolsPath)).getAsFile().get()
Paths.get(directory.absolutePath, path).toFile()
}
binaryPath = objects.property(String.class)
binaryPath.set(
(OperatingSystem.current().isWindows() ? fullPath[0] : fullPath[1]).toString())
}
return binaryPath
}
}
class LogOutputStream extends ByteArrayOutputStream {
private final Logger logger
private final LogLevel level
LogOutputStream(Logger logger, LogLevel level) {
this.logger = logger
this.level = level
}
@Override
void flush() {
logger.log(level, toString())
reset()
}
}
// Custom task to compile material files using matc
// This task handles incremental builds
abstract class MaterialCompiler extends TaskWithBinary {
@Incremental
@InputDirectory
abstract DirectoryProperty getInputDir()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@Inject abstract FileSystemOperations getFs()
@Inject abstract ExecOperations getExec()
@Inject abstract ObjectFactory getObjects()
@Inject abstract ProviderFactory getProviders()
MaterialCompiler() {
super("matc")
}
@TaskAction
void execute(InputChanges inputs) {
if (!inputs.incremental) {
fs.delete({
delete(objects.fileTree().from(outputDir).matching { include '*.filamat' })
})
}
inputs.getFileChanges(inputDir).each { InputFileDetails change ->
if (change.fileType == FileType.DIRECTORY) return
def file = change.file
if (change.changeType == ChangeType.REMOVED) {
getOutputFile(file).delete()
} else {
def out = new LogOutputStream(logger, LogLevel.LIFECYCLE)
def err = new LogOutputStream(logger, LogLevel.ERROR)
def header = ("Compiling material " + file + "\n").getBytes()
out.write(header)
out.flush()
if (!new File(binary.get()).exists()) {
throw new GradleException("Could not find ${binary.get()}." +
" Ensure Filament has been built/installed before building this app.")
}
def matcArgs = []
def exclude_vulkan = providers
.gradleProperty("com.google.android.filament.exclude-vulkan")
.forUseAtConfigurationTime().present
if (!exclude_vulkan) {
matcArgs += ['-a', 'vulkan']
}
def include_webgpu = providers
.gradleProperty("com.google.android.filament.include-webgpu")
.forUseAtConfigurationTime().present
if (include_webgpu) {
matcArgs += ['-a', 'webgpu', '--variant-filter=stereo']
}
def mat_no_opt = providers
.gradleProperty("com.google.android.filament.matnopt")
.forUseAtConfigurationTime().present
if (mat_no_opt) {
matcArgs += ['-g']
}
matcArgs += ['-a', 'opengl', '-p', 'mobile', '-o', getOutputFile(file), file]
exec.exec {
standardOutput out
errorOutput err
executable "${binary.get()}"
args matcArgs
}
}
}
}
File getOutputFile(final File file) {
return outputDir.file(file.name[0..file.name.lastIndexOf('.')] + 'filamat').get().asFile
}
}
// Custom task to process IBLs using cmgen
// This task handles incremental builds
abstract class IblGenerator extends TaskWithBinary {
@Input
@Optional
abstract Property<String> getCmgenArgs()
@Incremental
@InputFile
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@Inject abstract FileSystemOperations getFs()
@Inject abstract ExecOperations getExec()
@Inject abstract ObjectFactory getObjects()
IblGenerator() {
super("cmgen")
}
@TaskAction
void execute(InputChanges inputs) {
if (!inputs.incremental) {
fs.delete({
delete(objects.fileTree().from(outputDir).matching { include '*' })
})
}
inputs.getFileChanges(inputFile).each { InputFileDetails change ->
if (change.fileType == FileType.DIRECTORY) return
def file = change.file
if (change.changeType == ChangeType.REMOVED) {
getOutputFile(file).delete()
} else {
def out = new LogOutputStream(logger, LogLevel.LIFECYCLE)
def err = new LogOutputStream(logger, LogLevel.ERROR)
def header = ("Generating IBL " + file + "\n").getBytes()
out.write(header)
out.flush()
if (!new File(binary.get()).exists()) {
throw new GradleException("Could not find ${binary.get()}." +
" Ensure Filament has been built/installed before building this app.")
}
def outputPath = outputDir.get().asFile
def commandArgs = cmgenArgs.getOrNull()
if (commandArgs == null) {
commandArgs =
'-q -x ' + outputPath + ' --format=rgb32f ' +
'--extract-blur=0.08 --extract=' + outputPath.absolutePath
}
commandArgs = commandArgs + " " + file
exec.exec {
standardOutput out
errorOutput err
executable "${binary.get()}"
args(commandArgs.split())
}
}
}
}
File getOutputFile(final File file) {
return outputDir.file(file.name[0..file.name.lastIndexOf('.') - 1]).get().asFile
}
}
// Custom task to compile mesh files using filamesh
// This task handles incremental builds
abstract class MeshCompiler extends TaskWithBinary {
@Incremental
@InputFile
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@Inject abstract FileSystemOperations getFs()
@Inject abstract ExecOperations getExec()
MeshCompiler() {
super("filamesh")
}
@TaskAction
void execute(InputChanges inputs) {
if (!inputs.incremental) {
fs.delete({
delete(objects.fileTree().from(outputDir).matching { include '*.filamesh' })
})
}
inputs.getFileChanges(inputFile).each { InputFileDetails change ->
if (change.fileType == FileType.DIRECTORY) return
def file = change.file
if (change.changeType == ChangeType.REMOVED) {
getOutputFile(file).delete()
} else {
def out = new LogOutputStream(logger, LogLevel.LIFECYCLE)
def err = new LogOutputStream(logger, LogLevel.ERROR)
def header = ("Compiling mesh " + file + "\n").getBytes()
out.write(header)
out.flush()
if (!new File(binary.get()).exists()) {
throw new GradleException("Could not find ${binary.get()}." +
" Ensure Filament has been built/installed before building this app.")
}
exec.exec {
standardOutput out
errorOutput err
executable "${binary.get()}"
args(file, getOutputFile(file))
}
}
}
}
File getOutputFile(final File file) {
return outputDir.file(file.name[0..file.name.lastIndexOf('.')] + 'filamesh').get().asFile
}
}
class FilamentToolsPluginExtension {
DirectoryProperty materialInputDir
DirectoryProperty materialOutputDir
String cmgenArgs
RegularFileProperty iblInputFile
DirectoryProperty iblOutputDir
RegularFileProperty meshInputFile
DirectoryProperty meshOutputDir
}
class FilamentToolsPlugin implements Plugin<Project> {
void apply(Project project) {
def extension = project.extensions.create('filamentTools', FilamentToolsPluginExtension)
extension.materialInputDir = project.objects.directoryProperty()
extension.materialOutputDir = project.objects.directoryProperty()
extension.iblInputFile = project.objects.fileProperty()
extension.iblOutputDir = project.objects.directoryProperty()
extension.meshInputFile = project.objects.fileProperty()
extension.meshOutputDir = project.objects.directoryProperty()
project.tasks.register("filamentCompileMaterials", MaterialCompiler) {
enabled =
extension.materialInputDir.isPresent() &&
extension.materialOutputDir.isPresent()
inputDir.set(extension.materialInputDir.getOrNull())
outputDir.set(extension.materialOutputDir.getOrNull())
}
project.preBuild.dependsOn "filamentCompileMaterials"
project.tasks.register("filamentGenerateIbl", IblGenerator) {
enabled = extension.iblInputFile.isPresent() && extension.iblOutputDir.isPresent()
cmgenArgs = extension.cmgenArgs
inputFile = extension.iblInputFile.getOrNull()
outputDir = extension.iblOutputDir.getOrNull()
}
project.preBuild.dependsOn "filamentGenerateIbl"
project.tasks.register("filamentCompileMesh", MeshCompiler) {
enabled = extension.meshInputFile.isPresent() && extension.meshOutputDir.isPresent()
inputFile = extension.meshInputFile.getOrNull()
outputDir = extension.meshOutputDir.getOrNull()
}
project.preBuild.dependsOn "filamentCompileMesh"
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2026 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.
*/
package com.google.android.filament.gradle
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
class FilamentExtension {
final ToolsLocator tools
final DirectoryProperty materialInputDir
final DirectoryProperty materialOutputDir
final Property<String> cmgenArgs
final RegularFileProperty iblInputFile
final DirectoryProperty iblOutputDir
final RegularFileProperty meshInputFile
final DirectoryProperty meshOutputDir
FilamentExtension(Project project) {
this.tools = new ToolsLocator(project)
this.materialInputDir = project.objects.directoryProperty()
this.materialOutputDir = project.objects.directoryProperty()
this.cmgenArgs = project.objects.property(String)
this.iblInputFile = project.objects.fileProperty()
this.iblOutputDir = project.objects.directoryProperty()
this.meshInputFile = project.objects.fileProperty()
this.meshOutputDir = project.objects.directoryProperty()
}
void matc(Action<ToolsLocator.ToolConfig> action) {
action.execute(tools.matc)
}
void cmgen(Action<ToolsLocator.ToolConfig> action) {
action.execute(tools.cmgen)
}
void filamesh(Action<ToolsLocator.ToolConfig> action) {
action.execute(tools.filamesh)
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2026 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.
*/
package com.google.android.filament.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
class FilamentPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.pluginManager.apply("com.google.osdetector")
FilamentExtension extension = project.extensions.create("filament", FilamentExtension, project)
project.afterEvaluate {
extension.tools.resolve(project)
project.tasks.register("filamentCompileMaterials", MaterialCompileTask) {
enabled = extension.materialInputDir.isPresent() && extension.materialOutputDir.isPresent()
inputDir.set(extension.materialInputDir.getOrNull())
outputDir.set(extension.materialOutputDir.getOrNull())
matcTool.from(extension.tools.matcToolFiles)
}
project.tasks.register("filamentGenerateIbl", IblGenerateTask) {
enabled = extension.iblInputFile.isPresent() && extension.iblOutputDir.isPresent()
cmgenArgs = extension.cmgenArgs
inputFile.set(extension.iblInputFile.getOrNull())
outputDir.set(extension.iblOutputDir.getOrNull())
cmgenTool.from(extension.tools.cmgenToolFiles)
}
project.tasks.register("filamentCompileMesh", MeshCompileTask) {
enabled = extension.meshInputFile.isPresent() && extension.meshOutputDir.isPresent()
inputFile = extension.meshInputFile.getOrNull()
outputDir = extension.meshOutputDir.getOrNull()
filameshTool.from(extension.tools.filameshToolFiles)
}
project.preBuild.dependsOn "filamentCompileMaterials"
project.preBuild.dependsOn "filamentGenerateIbl"
project.preBuild.dependsOn "filamentCompileMesh"
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (C) 2026 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.
*/
package com.google.android.filament.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileType
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import org.gradle.api.tasks.incremental.InputFileDetails
import javax.inject.Inject
abstract class IblGenerateTask extends DefaultTask {
@Input
@Optional
abstract Property<String> getCmgenArgs()
@Incremental
@InputFile
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@InputFiles
abstract ConfigurableFileCollection getCmgenTool()
@Inject
abstract FileSystemOperations getFileSystemOperations()
@Inject
abstract ExecOperations getExecOperations()
@Inject
abstract ObjectFactory getObjectFactory()
@TaskAction
void execute(InputChanges inputs) {
if (cmgenTool.empty) {
throw new IllegalStateException(
"cmgen executable not configured. Please configure the 'cmgen' block in the " +
"'filament' extension or set the 'com.google.android.filament.tools-dir' " +
"property."
)
}
File cmgen = getCmgenTool().singleFile
if (!cmgen.exists()) {
throw new IllegalStateException("cmgen executable does not exist: ${cmgen.absolutePath}")
}
if (!cmgen.canExecute()) {
cmgen.setExecutable(true)
}
if (!inputs.incremental) {
getFileSystemOperations().delete {
delete(getObjectFactory().fileTree().from(getOutputDir()).matching { include '*' })
}
}
inputs.getFileChanges(getInputFile()).each { InputFileDetails change ->
if (change.fileType == FileType.DIRECTORY) return
def file = change.file
if (change.changeType == ChangeType.REMOVED) {
computeOutputFile(file).delete()
} else {
println "Generating IBL: ${file.name}"
def outputPath = getOutputDir().get().asFile
def commandArgs = getCmgenArgs().getOrNull()
if (commandArgs == null) {
// Default args if not provided
commandArgs = '-q -x ' + outputPath + ' --format=rgb32f ' +
'--extract-blur=0.08 --extract=' + outputPath.absolutePath
}
def argsList = commandArgs.split(' ').toList()
argsList.add(file.absolutePath)
getExecOperations().exec { spec ->
spec.executable(cmgen)
spec.args(argsList)
}
}
}
}
File computeOutputFile(final File file) {
String name = file.name
int dotIndex = name.lastIndexOf('.')
String baseName = dotIndex > 0 ? name.substring(0, dotIndex) : name
return getOutputDir().file(baseName).get().asFile
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2026 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.
*/
package com.google.android.filament.gradle
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileType
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.SkipWhenEmpty
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import javax.inject.Inject
abstract class MaterialCompileTask extends DefaultTask {
@Incremental
@InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
abstract DirectoryProperty getInputDir()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@InputFiles
@PathSensitive(PathSensitivity.NONE)
abstract ConfigurableFileCollection getMatcTool()
@Inject
abstract ExecOperations getExecOperations()
@Inject
abstract FileSystemOperations getFileSystemOperations()
@Inject
abstract ObjectFactory getObjectFactory()
@Inject
abstract ProviderFactory getProviderFactory()
@TaskAction
void compile(InputChanges inputs) {
if (matcTool.empty) {
throw new IllegalStateException(
"matc executable not configured. Please configure the 'matc' block in the " +
"'filament' extension or set the 'com.google.android.filament.tools-dir' " +
"property."
)
}
File matc = matcTool.singleFile
if (!matc.exists()) {
throw new IllegalStateException("matc executable does not exist: ${matc.absolutePath}")
}
if (!matc.canExecute()) {
matc.setExecutable(true)
}
if (!inputs.incremental) {
getFileSystemOperations().delete {
delete(getObjectFactory().fileTree().from(getOutputDir()).matching {
include '*.filamat'
})
}
}
def pf = getProviderFactory()
def excludeVulkanProperty = pf.gradleProperty("com.google.android.filament.exclude-vulkan")
def includeWebGpuProperty = pf.gradleProperty("com.google.android.filament.include-webgpu")
def matNoOptProperty = pf.gradleProperty("com.google.android.filament.matnopt")
def excludeVulkan = excludeVulkanProperty.orNull == "true"
def includeWebGpu = includeWebGpuProperty.orNull == "true"
def matNoOpt = matNoOptProperty.orNull == "true"
inputs.getFileChanges(getInputDir()).each { change ->
if (change.fileType == FileType.DIRECTORY) return
File file = change.file
File outputFile = computeOutputFile(file)
if (change.changeType == ChangeType.REMOVED) {
outputFile.delete()
} else {
println "Compiling material: ${file.name}"
def args = []
if (!excludeVulkan) {
args += ['-a', 'vulkan']
}
if (includeWebGpu) {
args += ['-a', 'webgpu', '--variant-filter=stereo']
}
if (matNoOpt) {
args += ['-g']
}
args += [
'-a', 'opengl', '-p', 'mobile',
'-o', outputFile.absolutePath,
file.absolutePath
]
getExecOperations().exec { spec ->
spec.executable(matc)
spec.args(args)
}
}
}
}
File computeOutputFile(File inputFile) {
String baseName = inputFile.name
int dotIndex = baseName.lastIndexOf('.')
if (dotIndex > 0) {
baseName = baseName.substring(0, dotIndex)
}
return getOutputDir().file("${baseName}.filamat").get().asFile
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2026 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.
*/
package com.google.android.filament.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileType
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import javax.inject.Inject
abstract class MeshCompileTask extends DefaultTask {
@Incremental
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@InputFiles
@PathSensitive(PathSensitivity.NONE)
abstract ConfigurableFileCollection getFilameshTool()
@Inject
abstract ExecOperations getExecOperations()
@Inject
abstract FileSystemOperations getFileSystemOperations()
@Inject
abstract ObjectFactory getObjectFactory()
@TaskAction
void compile(InputChanges inputs) {
if (filameshTool.empty) {
throw new IllegalStateException(
"filamesh executable not configured. Please configure the 'filamesh' block in the " +
"'filament' extension or set the 'com.google.android.filament.tools-dir' " +
"property."
)
}
File filamesh = filameshTool.singleFile
if (!filamesh.exists()) {
throw new IllegalStateException("filamesh executable does not exist: ${filamesh.absolutePath}")
}
if (!filamesh.canExecute()) {
filamesh.setExecutable(true)
}
if (!inputs.incremental) {
getFileSystemOperations().delete {
delete(getObjectFactory().fileTree().from(getOutputDir()).matching {
include '*.filamesh'
})
}
}
inputs.getFileChanges(inputFile).each { change ->
if (change.fileType == FileType.DIRECTORY) return
File file = change.file
File outputFile = computeOutputFile(file)
if (change.changeType == ChangeType.REMOVED) {
outputFile.delete()
} else {
println "Compiling mesh: ${file.name}"
getExecOperations().exec { spec ->
spec.executable(filamesh)
spec.args(file.absolutePath, outputFile.absolutePath)
}
}
}
}
File computeOutputFile(File inputFile) {
String baseName = inputFile.name
int dotIndex = baseName.lastIndexOf('.')
if (dotIndex > 0) {
baseName = baseName.substring(0, dotIndex)
}
return getOutputDir().file("${baseName}.filamesh").get().asFile
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright (C) 2026 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.
*/
package com.google.android.filament.gradle
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.file.FileCollection
import org.gradle.internal.os.OperatingSystem
import java.nio.file.Paths
class ToolsLocator {
static class ToolConfig {
String artifact
String path
FileCollection files
}
final ToolConfig matc = new ToolConfig()
final ToolConfig cmgen = new ToolConfig()
final ToolConfig filamesh = new ToolConfig()
private final Project project
ToolsLocator(Project project) {
this.project = project
}
void resolve(Project project) {
resolveTool(matc, "matc")
resolveTool(cmgen, "cmgen")
resolveTool(filamesh, "filamesh")
}
/**
* Resolves a specific tool by its name and sets the {@link ToolConfig#files} property of the
* provided {@link ToolConfig} object. It first attempts to locate the tool based on a Gradle
* property `com.google.android.filament.tools-dir` if present, otherwise it resolves the tool
* through a Gradle configuration.
*
* @param tool The {@link ToolConfig} object whose {@code files} property will be set.
* @param name The name of the tool (e.g., "matc", "cmgen").
*/
private void resolveTool(ToolConfig tool, String name) {
// Find the OS classifier, e.g. 'osx-aarch_64'.
def classifier =
project.extensions.getByType(com.google.gradle.osdetector.OsDetector).classifier
// If com.google.android.filament.tools-dir is set, we'll use it as the tool's base path.
def toolsDirProp = project.providers.gradleProperty("com.google.android.filament.tools-dir")
if (toolsDirProp.isPresent()) {
def toolsDir = toolsDirProp.get()
def path = OperatingSystem.current().isWindows() ?
"${toolsDir}/bin/${name}.exe" :
"${toolsDir}/bin/${name}"
tool.files = project.files(path)
return
}
// If an explicit path for the tool is provided in ToolConfig
// (e.g. matc { path = 'path/to/tool' }), use it directly.
if (tool.path) {
tool.files = project.files(tool.path)
return
}
// Otherwise, if an artifact is provided
// (e.g. matc { artifact = 'com.google.android.filament:matc:1.68.5' }), resolve it.
if (tool.artifact) {
String depString = tool.artifact
// In Gradle, a configuration is a named, manageable group of dependencies.
// Resolve the tool artifact using a detached configuration. A detached configuration
// is a temporary, isolated configuration that is not part of the project's regular
// configuration hierarchy.
Configuration config = project.configurations.detachedConfiguration()
config.setTransitive(false) // We only want the tool itself, not its dependencies
def dep = project.dependencies.create("${depString}:${classifier}@exe")
config.dependencies.add(dep)
// A Gradle Configuration implements FileCollection. When treated as a FileCollection,
// it represents the resolved files of its dependencies.
tool.files = config
}
}
FileCollection getMatcToolFiles() {
return matc.files ?: project.files()
}
FileCollection getCmgenToolFiles() {
return cmgen.files ?: project.files()
}
FileCollection getFilameshToolFiles() {
return filamesh.files ?: project.files()
}
}

View File

@@ -71,8 +71,10 @@ Java_com_google_android_filament_utils_AutomationEngine_nStartBatchMode(JNIEnv*
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_utils_AutomationEngine_nTick(JNIEnv* env, jclass klass,
jlong nativeAutomation, jlong nativeEngine,
jlong view, jlongArray materials, jlong renderer, jfloat deltaTime) {
jlong view, jlongArray materials, jlong renderer, jlong nativeIbl, jint sunlightEntity,
jintArray assetLights, jlong nativeLm, jlong scene, jfloat deltaTime) {
using MaterialPointer = MaterialInstance*;
jsize materialCount = 0;
jlong* longMaterials = nullptr;
MaterialPointer* ptrMaterials = nullptr;
@@ -84,12 +86,28 @@ Java_com_google_android_filament_utils_AutomationEngine_nTick(JNIEnv* env, jclas
ptrMaterials[i] = (MaterialPointer) longMaterials[i];
}
}
jsize lightCount = 0;
jint* intLights = nullptr;
if (assetLights) {
lightCount = env->GetArrayLength(assetLights);
intLights = env->GetIntArrayElements(assetLights, nullptr);
}
static_assert(sizeof(jint) == sizeof(Entity));
AutomationEngine* automation = (AutomationEngine*) nativeAutomation;
AutomationEngine::ViewerContent content = {
.view = (View*) view,
.renderer = (Renderer*) renderer,
.materials = ptrMaterials,
.materialCount = (size_t) materialCount,
.lightManager = (LightManager*) nativeLm,
.scene = (Scene*) scene,
.indirectLight = (IndirectLight*) nativeIbl,
.sunlight = (Entity&) sunlightEntity,
.assetLights = (Entity*) intLights,
.assetLightCount = (size_t) lightCount,
};
Engine* engine = (Engine*)nativeEngine;
automation->tick(engine, content, deltaTime);
@@ -97,6 +115,9 @@ Java_com_google_android_filament_utils_AutomationEngine_nTick(JNIEnv* env, jclas
env->ReleaseLongArrayElements(materials, longMaterials, 0);
delete[] ptrMaterials;
}
if (intLights) {
env->ReleaseIntArrayElements(assetLights, intLights, 0);
}
}
extern "C" JNIEXPORT void JNICALL

View File

@@ -178,7 +178,12 @@ public class AutomationEngine {
}
long nativeView = content.view.getNativeObject();
long nativeRenderer = content.renderer.getNativeObject();
nTick(mNativeObject, engine.getNativeObject(), nativeView, nativeMaterialInstances, nativeRenderer, deltaTime);
long nativeIbl = content.indirectLight == null ? 0 : content.indirectLight.getNativeObject();
long nativeLm = content.lightManager == null ? 0 : content.lightManager.getNativeObject();
long nativeScene = content.scene == null ? 0 : content.scene.getNativeObject();
nTick(mNativeObject, engine.getNativeObject(), nativeView, nativeMaterialInstances,
nativeRenderer, nativeIbl, content.sunlight, content.assetLights, nativeLm,
nativeScene, deltaTime);
}
/**
@@ -287,7 +292,8 @@ public class AutomationEngine {
private static native void nStartRunning(long nativeObject);
private static native void nStartBatchMode(long nativeObject);
private static native void nTick(long nativeObject, long nativeEngine,
long view, long[] materials, long renderer, float deltaTime);
long view, long[] materials, long renderer, long ibl, int sunlight, int[] assetLights,
long lightManager, long scene, float deltaTime);
private static native void nApplySettings(long nativeObject, long nativeEngine,
String jsonSettings, long view,
long[] materials, long ibl, int sunlight, int[] assetLights, long lightManager,

View File

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

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
cmgenArgs = "-q --format=ktx --size=256 --extract-blur=0.1 --deploy=src/main/assets/envs/default_env"
iblInputFile = project.layout.projectDirectory.file("../../../third_party/environments/lightroom_14b.hdr")
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
meshInputFile = project.layout.projectDirectory.file("../../../third_party/models/shader_ball/shader_ball.obj")
meshOutputDir = project.layout.projectDirectory.dir("src/main/assets/models")

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
iblInputFile = project.layout.projectDirectory.file("../../../third_party/environments/studio_small_02_2k.hdr")
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")

View File

@@ -1,6 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -9,6 +10,25 @@ kotlin {
jvmToolchain(versions.jdk)
}
filament {
cmgenArgs = "-q --format=ktx --size=256 --extract-blur=0.1 --deploy=src/main/assets/envs/default_env"
iblInputFile = project.layout.projectDirectory.file("../../../third_party/environments/lightroom_14b.hdr")
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")
}
// don't forget to update MainACtivity.kt when/if changing this.
tasks.register('copyDamagedHelmetGltf', Copy) {
from file("../../../third_party/models/DamagedHelmet/DamagedHelmet.glb")
into file("src/main/assets/models")
rename {String fileName -> "helmet.glb"}
}
preBuild.dependsOn copyDamagedHelmetGltf
clean.doFirst {
delete "src/main/assets"
}
android {
namespace 'com.google.android.filament.validation'
@@ -27,9 +47,9 @@ android {
dependencies {
implementation deps.kotlin
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation deps.coroutines.core
implementation project(':filament-android')
implementation project(':gltfio-android')
implementation project(':filament-utils-android')
implementation project(':filament-utils-android')
}

View File

@@ -17,16 +17,36 @@
package com.google.android.filament.validation
import android.app.Activity
import android.graphics.Bitmap
import android.os.Bundle
import android.util.Log
import android.view.Choreographer
import android.view.SurfaceView
import android.view.View
import android.view.WindowManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import com.google.android.filament.utils.ModelViewer
import com.google.android.filament.utils.Utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.google.android.filament.utils.KTX1Loader
import com.google.android.filament.IndirectLight
import com.google.android.filament.Skybox
import android.graphics.Color
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.nio.ByteBuffer
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.Spinner
import android.widget.AdapterView
class MainActivity : Activity(), ValidationRunner.Callback {
@@ -42,8 +62,15 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private lateinit var choreographer: Choreographer
private lateinit var modelViewer: ModelViewer
private lateinit var statusTextView: TextView
private lateinit var resultsContainer: LinearLayout
private lateinit var inputManager: ValidationInputManager
private var currentInput: ValidationInputManager.ValidationInput? = null
private lateinit var modeSpinner: Spinner
private lateinit var runButton: Button
private var resultManager: ValidationResultManager? = null
private var validationRunner: ValidationRunner? = null
// Frame callback
private val frameScheduler = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
@@ -55,58 +82,109 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Simple layout
val layout = android.widget.FrameLayout(this)
surfaceView = SurfaceView(this)
layout.addView(surfaceView)
statusTextView = TextView(this)
statusTextView.setTextColor(0xFFFFFFFF.toInt())
statusTextView.textSize = 16f
statusTextView.setPadding(20, 20, 20, 20)
statusTextView.text = "Initializing..."
layout.addView(statusTextView)
setContentView(layout)
setContentView(R.layout.activity_main)
// SurfaceView container
surfaceView = findViewById(R.id.surface_view)
surfaceView.holder.setFixedSize(512, 512)
statusTextView = findViewById(R.id.status_text)
modeSpinner = findViewById(R.id.mode_spinner)
runButton = findViewById(R.id.run_button)
resultsContainer = findViewById(R.id.results_container)
// Setup Spinner
val modes = arrayOf("Run Validation", "Generate Goldens")
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, modes)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
modeSpinner.adapter = adapter
// Setup Run Button
runButton.setOnClickListener {
currentInput?.let { input ->
val generateGoldens = modeSpinner.selectedItemPosition == 1
val newInput = input.copy(generateGoldens = generateGoldens)
startValidation(newInput)
}
}
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
choreographer = Choreographer.getInstance()
modelViewer = ModelViewer(surfaceView)
// Check permissions?
// We assume 'adb install -g' or permissions granted.
// But for scoped storage we might not need PERMISSION if reading from app-specific dirs,
// but user mentioned /sdcard/ so we need MANAGE_EXTERNAL_STORAGE or READ_EXTERNAL_STORAGE.
// For waiting/simplicity, we just try.
inputManager = ValidationInputManager(this)
// Initialize IBL
createIndirectLight()
handleIntent()
}
private fun handleIntent() {
val intent = intent
val testConfigPath = intent.getStringExtra("test_config")
if (testConfigPath != null) {
startValidation(testConfigPath)
} else {
statusTextView.text = "No test_config provided via Intent.\nUse -e test_config <path>"
Log.w(TAG, "No test config provided")
private fun createIndirectLight() {
try {
val engine = modelViewer.engine
val scene = modelViewer.scene
val iblName = "default_env"
fun readAsset(path: String): ByteBuffer {
val input = assets.open(path)
val bytes = input.readBytes()
return ByteBuffer.wrap(bytes)
}
readAsset("envs/$iblName/${iblName}_ibl.ktx").let {
val bundle = KTX1Loader.createIndirectLight(engine, it)
scene.indirectLight = bundle.indirectLight
modelViewer.indirectLightCubemap = bundle.cubemap
scene.indirectLight!!.intensity = 30_000.0f
}
readAsset("envs/$iblName/${iblName}_skybox.ktx").let {
val bundle = KTX1Loader.createSkybox(engine, it)
scene.skybox = bundle.skybox
modelViewer.skyboxCubemap = bundle.cubemap
}
Log.i(TAG, "IBL loaded successfully")
} catch (e: Exception) {
Log.e(TAG, "Failed to load IBL", e)
statusTextView.text = "Warning: Failed to load IBL"
}
}
private fun startValidation(configPath: String) {
private fun handleIntent() {
statusTextView.text = "Resolving configuration..."
CoroutineScope(Dispatchers.Main).launch {
try {
val input = inputManager.resolveConfig(intent)
currentInput = input
// Sync spinner with intent
modeSpinner.setSelection(if (input.generateGoldens) 1 else 0)
startValidation(input)
} catch (e: Exception) {
Log.e(TAG, "Failed to resolve config", e)
statusTextView.text = "Error: ${e.message}"
}
}
}
private fun startValidation(input: ValidationInputManager.ValidationInput) {
try {
Log.i(TAG, "Parsing config from $configPath")
val config = ConfigParser.parseFromPath(configPath)
val outputDir = File(getExternalFilesDir(null), "validation_results")
Log.i(TAG, "Output dir: ${outputDir.absolutePath}")
validationRunner = ValidationRunner(this, modelViewer, config, outputDir)
resultsContainer.removeAllViews()
Log.i(TAG, "Starting validation with config: ${input.config.name}")
Log.i(TAG, "Output dir: ${input.outputDir.absolutePath}")
resultManager = ValidationResultManager(input.outputDir)
validationRunner = ValidationRunner(this, modelViewer, input.config, resultManager!!)
validationRunner?.callback = this
validationRunner?.generateGoldens = input.generateGoldens
validationRunner?.start()
// Sync spinner in case it was called programmatically or changed implicitly
modeSpinner.setSelection(if (input.generateGoldens) 1 else 0)
} catch (e: Exception) {
Log.e(TAG, "Failed to start validation", e)
statusTextView.text = "Error: ${e.message}"
@@ -128,11 +206,68 @@ class MainActivity : Activity(), ValidationRunner.Callback {
choreographer.removeFrameCallback(frameScheduler)
}
override fun onTestFinished(result: ValidationRunner.TestResult) {
private var currentRenderedBitmap: Bitmap? = null
private var currentGoldenBitmap: Bitmap? = null
private var currentDiffBitmap: Bitmap? = null
override fun onTestFinished(result: ValidationResult) {
runOnUiThread {
val status = "Test ${result.name} finished: ${if(result.passed) "PASS" else "FAIL"}"
val status = "Test ${result.testName} finished: ${if(result.passed) "PASS" else "FAIL"}"
statusTextView.text = status
Log.i(TAG, status)
// Container for this result
val resultContainer = LinearLayout(this)
resultContainer.orientation = LinearLayout.VERTICAL
resultContainer.setPadding(0, 10, 0, 20)
// Header
val resultView = TextView(this)
resultView.text = "${result.testName}: ${if(result.passed) "PASS" else "FAIL"} (Diff: ${result.diffMetric})"
resultView.setTextColor(if(result.passed) Color.GREEN else Color.RED)
resultView.textSize = 16f
resultView.setTypeface(null, android.graphics.Typeface.BOLD)
resultContainer.addView(resultView)
// Images Row
val imagesRow = LinearLayout(this)
imagesRow.orientation = LinearLayout.HORIZONTAL
fun addImage(label: String, bitmap: Bitmap?) {
if (bitmap != null) {
val container = LinearLayout(this)
container.orientation = LinearLayout.VERTICAL
container.setPadding(0, 0, 10, 0)
val labelView = TextView(this)
labelView.text = label
labelView.textSize = 12f
container.addView(labelView)
val iv = ImageView(this)
iv.setImageBitmap(bitmap) // Use the same bitmap (or copy if needed, but same is usually fine for UI)
iv.layoutParams = LinearLayout.LayoutParams(250, 250) // Smaller thumbnails
iv.scaleType = ImageView.ScaleType.FIT_CENTER
iv.setBackgroundColor(0xFF404040.toInt())
container.addView(iv)
imagesRow.addView(container)
}
}
addImage("Rendered", currentRenderedBitmap)
addImage("Golden", currentGoldenBitmap)
if (!result.passed) {
addImage("Diff", currentDiffBitmap)
}
resultContainer.addView(imagesRow)
resultsContainer.addView(resultContainer)
// Clear current images for next test
currentRenderedBitmap = null
currentGoldenBitmap = null
currentDiffBitmap = null
}
}
@@ -140,8 +275,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
runOnUiThread {
statusTextView.text = "All tests finished!"
Log.i(TAG, "All tests finished")
// Optional: Auto-close activity?
// finish()
}
}
@@ -150,5 +283,68 @@ class MainActivity : Activity(), ValidationRunner.Callback {
statusTextView.text = status
}
}
override fun onImageResult(type: String, bitmap: Bitmap) {
runOnUiThread {
// Update the "live" views
when (type) {
"Rendered" -> {
currentRenderedBitmap = bitmap
}
"Golden" -> {
currentGoldenBitmap = bitmap
}
"Diff" -> {
currentDiffBitmap = bitmap
}
}
}
}
}
/*
* Scripts for reference:
*
* generate_goldens.sh:
* --------------------
* #!/bin/bash
* set -e
*
* # Config path (on device)
* CONFIG_PATH=$1
* if [ -z "$CONFIG_PATH" ]; then
* echo "Usage: $0 <device_config_path>"
* echo "Example: $0 /sdcard/Android/data/com.google.android.filament.validation/files/default_test.json"
* exit 1
* fi
*
* echo "Starting Golden Generation for $CONFIG_PATH..."
* adb shell am force-stop com.google.android.filament.validation
* adb shell am start -n com.google.android.filament.validation/.MainActivity \
* -e test_config "$CONFIG_PATH" \
* --ez generate_goldens true
*
* echo "Check device or logcat for progress."
* echo "adb logcat -s FilamentValidation:I ValidationRunner:I"
* echo "To pull results: ./samples/sample-render-validation/pull_goldens.sh"
*
* pull_goldens.sh:
* ----------------
* #!/bin/bash
* set -e
*
* # Default destination is local golden directory relative to script
* SCRIPT_DIR=$(cd $(dirname $0); pwd)
* DEST_DIR=${1:-"$SCRIPT_DIR/golden"}
*
* echo "Pulling goldens to $DEST_DIR..."
* mkdir -p "$DEST_DIR"
*
* # Path on device
* DEVICE_GOLDEN_DIR="/storage/emulated/0/Android/data/com.google.android.filament.validation/files/golden/."
*
* adb pull "$DEVICE_GOLDEN_DIR" "$DEST_DIR"
*
* echo "Done."
* ls -l "$DEST_DIR"
*/

View File

@@ -70,6 +70,26 @@ class ConfigParser {
}
}
// Explicit models map override
val modelsJson = json.optJSONObject("models")
if (modelsJson != null) {
val keys = modelsJson.keys()
while (keys.hasNext()) {
val name = keys.next()
val path = modelsJson.getString(name)
// Resolve path relative to baseDir if not absolute
val file = File(path)
if (file.isAbsolute) {
models[name] = path
} else if (baseDir != null) {
models[name] = File(baseDir, path).absolutePath
} else {
models[name] = path
}
}
}
val presetsJson = json.optJSONArray("presets")
val presets = mutableMapOf<String, PresetConfig>()
if (presetsJson != null) {

View File

@@ -0,0 +1,170 @@
/*
* Copyright (C) 2026 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.
*/
package com.google.android.filament.validation
import android.content.Context
import android.content.Intent
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
/**
* Handles the retrieval and preparation of test configuration and assets.
* Supports loading from:
* 1. Intent extras (local path or URL)
* 2. Default embedded assets (fallback)
*/
class ValidationInputManager(private val context: Context) {
companion object {
private const val TAG = "ValidationInputManager"
}
data class ValidationInput(
val config: RenderTestConfig,
val outputDir: File,
val generateGoldens: Boolean
)
/**
* Resolves the test configuration based on the provided intent extras.
* This may involve extracting assets or downloading files.
*/
suspend fun resolveConfig(intent: Intent): ValidationInput = withContext(Dispatchers.IO) {
val testConfigPath = intent.getStringExtra("test_config")
val urlConfig = intent.getStringExtra("url_config")
val urlModelsBase = intent.getStringExtra("url_models_base")
val generateGoldens = intent.getBooleanExtra("generate_goldens", false)
val outputPath = intent.getStringExtra("output_path")
val outputDir = if (outputPath != null) {
File(outputPath).apply { mkdirs() }
} else {
File(context.getExternalFilesDir(null), "validation_results").apply { mkdirs() }
}
val config = when {
urlConfig != null -> downloadConfig(urlConfig, urlModelsBase)
testConfigPath != null -> ConfigParser.parseFromPath(testConfigPath)
else -> extractDefaultAssets()
}
return@withContext ValidationInput(config, outputDir, generateGoldens)
}
private suspend fun extractDefaultAssets(): RenderTestConfig {
Log.i(TAG, "Extracting default assets...")
val filesDir = context.getExternalFilesDir(null) ?: context.filesDir
val assetManager = context.assets
// Copy default_test.json
val configDir = File(filesDir, "config")
configDir.mkdirs()
val configOut = File(configDir, "default_test.json")
assetManager.open("default_test.json").use { input ->
FileOutputStream(configOut).use { output ->
input.copyTo(output)
}
}
// Copy DamagedHelmet.glb
val modelsDir = File(filesDir, "models")
modelsDir.mkdirs()
val modelOut = File(modelsDir, "DamagedHelmet.glb")
assetManager.open("DamagedHelmet.glb").use { input ->
FileOutputStream(modelOut).use { output ->
input.copyTo(output)
}
}
// Update config to point to relative path (standardizing on relative for portability where possible)
// or absolute. Here we use relative as per previous logic.
val configJson = JSONObject(configOut.readText())
val models = configJson.getJSONObject("models")
// Ensure the default model points to the extracted file
// We can use absolute path to be safe since we know where it is now.
models.put("DamagedHelmet", modelOut.absolutePath)
configOut.writeText(configJson.toString(2))
return ConfigParser.parseFromPath(configOut.absolutePath)
}
private suspend fun downloadConfig(urlConfig: String, urlModelsBase: String?): RenderTestConfig {
Log.i(TAG, "Downloading config from $urlConfig")
val filesDir = context.getExternalFilesDir(null) ?: context.filesDir
val configDir = File(filesDir, "config")
configDir.mkdirs()
val modelsDir = File(filesDir, "models")
modelsDir.mkdirs()
val configName = "downloaded_config.json"
val configFile = File(configDir, configName)
downloadFile(urlConfig, configFile)
if (urlModelsBase != null) {
val configJson = JSONObject(configFile.readText())
val models = configJson.optJSONObject("models")
if (models != null) {
val keys = models.keys()
while (keys.hasNext()) {
val key = keys.next()
val modelPath = models.getString(key)
val fileName = File(modelPath).name
val modelFile = File(modelsDir, fileName)
val modelUrl = "$urlModelsBase/$fileName"
Log.i(TAG, "Downloading model: $fileName from $modelUrl")
downloadFile(modelUrl, modelFile)
// Update config to point to absolute path
models.put(key, modelFile.absolutePath)
}
configFile.writeText(configJson.toString())
}
}
return ConfigParser.parseFromPath(configFile.absolutePath)
}
private fun downloadFile(urlStr: String, destFile: File) {
val url = URL(urlStr)
val connection = url.openConnection() as HttpURLConnection
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} for $urlStr")
}
destFile.parentFile?.mkdirs()
connection.inputStream.use { input ->
FileOutputStream(destFile).use { output ->
input.copyTo(output)
}
}
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2026 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.
*/
package com.google.android.filament.validation
import android.graphics.Bitmap
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import org.json.JSONArray
import org.json.JSONObject
data class ValidationResult(
val testName: String,
val passed: Boolean,
val diffMetric: Float = 0f
)
class ValidationResultManager(private val outputDir: File) {
companion object {
private const val TAG = "ValidationResultManager"
}
private val results = mutableListOf<ValidationResult>()
init {
if (!outputDir.exists()) {
outputDir.mkdirs()
}
}
fun addResult(result: ValidationResult) {
results.add(result)
}
fun saveImage(name: String, bitmap: Bitmap) {
val file = File(outputDir, "$name.png")
try {
FileOutputStream(file).use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to save image $name", e)
}
}
fun getOutputDir(): File {
return outputDir
}
fun finalizeResults(): File? {
// Write results JSON
writeResultsJson()
// Zip results
val zipFile = File(outputDir, "results.zip")
try {
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
outputDir.walkTopDown().filter { it.isFile && it.name != "results.zip" }.forEach { file ->
val entryName = file.relativeTo(outputDir).path
zos.putNextEntry(ZipEntry(entryName))
file.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
Log.i(TAG, "Zipped results to ${zipFile.absolutePath}")
return zipFile
} catch (e: Exception) {
Log.e(TAG, "Failed to zip results", e)
return null
}
}
private fun writeResultsJson() {
val jsonArray = JSONArray()
for (result in results) {
val jsonObject = JSONObject()
jsonObject.put("test_name", result.testName)
jsonObject.put("passed", result.passed)
jsonObject.put("diff_metric", result.diffMetric)
jsonArray.put(jsonObject)
}
val jsonFile = File(outputDir, "results.json")
try {
FileOutputStream(jsonFile).use { out ->
out.write(jsonArray.toString(4).toByteArray())
}
} catch (e: Exception) {
Log.e(TAG, "Failed to write results.json", e)
}
}
}

View File

@@ -19,25 +19,19 @@ package com.google.android.filament.validation
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import com.google.android.filament.Engine
import com.google.android.filament.Renderer
import com.google.android.filament.View
import com.google.android.filament.utils.AutomationEngine
import com.google.android.filament.utils.ImageDiff
import com.google.android.filament.utils.ModelViewer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
class ValidationRunner(
private val context: Context,
private val modelViewer: ModelViewer,
private val config: RenderTestConfig,
private val outputDir: File
private val resultManager: ValidationResultManager
) {
private var currentState = State.IDLE
@@ -46,25 +40,30 @@ class ValidationRunner(
private var currentEngine: AutomationEngine? = null
private var currentTestConfig: TestConfig? = null
private var currentModelName: String? = null
private var loadStartFence: com.google.android.filament.Fence? = null
private var loadStartTime = 0L
private var frameCounter = 0
enum class State {
IDLE,
LOADING_MODEL,
WAITING_FOR_FENCE,
WAITING_FOR_RESOURCES,
WARMUP,
RUNNING_TEST,
COMPARING
}
interface Callback {
fun onTestFinished(result: TestResult)
fun onTestFinished(result: ValidationResult)
fun onAllTestsFinished()
fun onStatusChanged(status: String)
fun onImageResult(type: String, bitmap: Bitmap)
}
var callback: Callback? = null
var generateGoldens: Boolean = false
fun start() {
if (config.tests.isEmpty()) {
@@ -94,12 +93,11 @@ class ValidationRunner(
nextModel()
return
}
currentState = State.LOADING_MODEL
callback?.onStatusChanged("Loading $modelName for ${currentTestConfig?.name}")
// Load model on main thread (required by ModelViewer)
// We assume this is called from main thread or we dispatch
loadModel(modelPath)
}
@@ -107,13 +105,18 @@ class ValidationRunner(
// Assume called on Main Thread
modelViewer.destroyModel()
try {
Log.i("ValidationRunner", "Reading model file: $path")
val bytes = File(path).readBytes()
Log.i("ValidationRunner", "Loading GLB buffer... (${bytes.size} bytes)")
val buffer = ByteBuffer.wrap(bytes)
modelViewer.loadModelGlb(buffer)
Log.i("ValidationRunner", "Model loaded. initializing fence.")
modelViewer.transformToUnitCube()
loadStartFence = modelViewer.engine.createFence()
loadStartTime = System.nanoTime()
currentState = State.WAITING_FOR_FENCE
frameCounter = 0 // Reset for fence timeout tracking
Log.i("ValidationRunner", "State set to WAITING_FOR_FENCE")
} catch (e: Exception) {
Log.e("ValidationRunner", "Failed to load $path", e)
nextModel()
@@ -121,37 +124,67 @@ class ValidationRunner(
}
fun onFrame(frameTimeNanos: Long) {
if (frameCounter % 60 == 0) {
Log.i("ValidationRunner", "onFrame: $currentState (frame: $frameCounter)")
}
when (currentState) {
State.IDLE -> {}
State.WAITING_FOR_FENCE -> {
frameCounter++
if (frameCounter > 600) {
Log.w("ValidationRunner", "Fence timed out after 600 frames! Forcing proceed.")
modelViewer.engine.destroyFence(loadStartFence!!)
loadStartFence = null
currentState = State.WAITING_FOR_RESOURCES
return
}
loadStartFence?.let { fence ->
if (fence.wait(com.google.android.filament.Fence.Mode.FLUSH, 0) == com.google.android.filament.Fence.FenceStatus.CONDITION_SATISFIED) {
if (fence.wait(com.google.android.filament.Fence.Mode.FLUSH, 0) ==
com.google.android.filament.Fence.FenceStatus.CONDITION_SATISFIED) {
modelViewer.engine.destroyFence(fence)
loadStartFence = null
// Compile materials (simplified)
modelViewer.scene.forEach { entity ->
// ... existing material compilation logic ...
}
startAutomation()
currentState = State.WAITING_FOR_RESOURCES
}
}
}
State.WAITING_FOR_RESOURCES -> {
val progress = modelViewer.progress
if (progress >= 1.0f) {
Log.i("ValidationRunner", "Resources loaded. Starting warmup.")
frameCounter = 0
currentState = State.WARMUP
}
}
State.WARMUP -> {
frameCounter++
if (frameCounter > 5) { // 5 frames warmup
startAutomation()
}
}
State.RUNNING_TEST -> {
currentEngine?.let { engine ->
// Log.i("ValidationRunner", "Running test...")
currentEngine?.let { engine ->
val content = AutomationEngine.ViewerContent()
content.view = modelViewer.view
content.renderer = modelViewer.renderer
content.scene = modelViewer.scene
content.lightManager = modelViewer.engine.lightManager
// Tick
// Delta time?
val deltaTime = 1.0f / 60.0f // Fixed step for consistency?
val deltaTime = 1.0f / 60.0f
engine.tick(modelViewer.engine, content, deltaTime)
if (!engine.isRunning) {
frameCounter++
if (engine.shouldClose()) {
Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)")
// Test finished (for this spec)
currentState = State.COMPARING
captureAndCompare()
@@ -175,104 +208,83 @@ class ValidationRunner(
options.sleepDuration = 0.0f // Minimal sleep, let frames drive it
options.minFrameCount = 5 // Ensure some frames pass
currentEngine?.setOptions(options)
currentEngine?.startRunning()
// Use batch mode to ensure shouldClose() works reliably
currentEngine?.startBatchMode()
currentEngine?.signalBatchMode() // Start immediately
frameCounter = 0
currentState = State.RUNNING_TEST
}
private fun captureAndCompare() {
callback?.onStatusChanged("Comparing ${currentTestConfig?.name}...")
val view = modelViewer.view
val renderer = modelViewer.renderer
val width = view.viewport.width
val height = view.viewport.height
val buffer = ByteBuffer.allocateDirect(width * height * 4)
val pbd = com.google.android.filament.Texture.PixelBufferDescriptor(
buffer,
com.google.android.filament.Texture.Format.RGBA,
com.google.android.filament.Texture.Type.UBYTE,
1, 0, 0, 0, 0, // alignment, left, top, stride (0=default)
null // handler (null = current thread? no, handler is for callback)
) {
// Callback when readPixels is done
// Dispatch to background thread for comparison to avoid blocking UI?
// "it" is undefined here? The callback interface is Runnable?
// Kotlin lambda for Runnable.
compareCapturedImage(buffer, width, height)
modelViewer.debugGetNextFrameCallback { bitmap ->
compareCapturedImage(bitmap)
}
renderer.readPixels(0, 0, width, height, pbd)
}
private fun compareCapturedImage(buffer: java.nio.Buffer, width: Int, height: Int) {
// This runs on... which thread? Filament driver thread possibly.
// We should use a helper to process.
private fun compareCapturedImage(bitmap: Bitmap) {
val testName = currentTestConfig!!.name
val modelName = currentModelName!!
val backend = "opengl" // Hardcoded for now, or get from View/Engine?
val backend = currentTestConfig?.backends?.firstOrNull() ?: "opengl"
val testFullName = "${testName}.${backend}.${modelName}"
// Golden path
// We expect a golden directory.
val goldenFile = File(config.models.get(modelName)!!).parentFile.parentFile.resolve("golden/${testFullName}.png")
// Strategy: models are in .../models/model.glb
// Goldens are in .../golden/
val modelFile = File(config.models.get(modelName)!!)
val goldenFile = modelFile.parentFile!!.parentFile!!.resolve("golden/${testFullName}.png")
Thread {
try {
// Convert buffer to Bitmap
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(buffer)
// Flip Y? ReadPixels is typically bottom-up?
// Filament readPixels is bottom-left? YES.
// Bitmap is top-left.
// We need to flip.
val matrix = android.graphics.Matrix()
matrix.postScale(1f, -1f)
val flipped = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true)
val flipped = bitmap
callback?.onImageResult("Rendered", flipped)
var passed = false
if (goldenFile.exists()) {
val golden = android.graphics.BitmapFactory.decodeFile(goldenFile.absolutePath)
if (golden != null) {
// Populate tolerance from config
val tol = currentTestConfig?.tolerance ?: org.json.JSONObject()
val tolJson = tol.toString()
val result = ImageDiff.compare(golden, flipped, tolJson, null)
passed = (result.status == ImageDiff.Result.Status.PASSED)
// Save diff if failed?
if (!passed) {
val diffFile = File(outputDir, "${testFullName}_diff.png")
if (result.diffImage != null) {
FileOutputStream(diffFile).use { out ->
result.diffImage.compress(Bitmap.CompressFormat.PNG, 100, out)
}
}
}
} else {
Log.e("ValidationRunner", "Failed to load golden: ${goldenFile.absolutePath}")
var diffMetric = 0f
if (generateGoldens) {
goldenFile.parentFile?.mkdirs()
FileOutputStream(goldenFile).use { out ->
flipped.compress(Bitmap.CompressFormat.PNG, 100, out)
}
passed = true // Generating goldens always passes if successful
callback?.onStatusChanged("Golden generated")
} else {
Log.w("ValidationRunner", "Golden not found: ${goldenFile.absolutePath}")
}
// Save output
val outFile = File(outputDir, "${testFullName}.png")
FileOutputStream(outFile).use { out ->
flipped.compress(Bitmap.CompressFormat.PNG, 100, out)
if (goldenFile.exists()) {
val golden = android.graphics.BitmapFactory.decodeFile(goldenFile.absolutePath)
if (golden != null) {
callback?.onImageResult("Golden", golden)
val tol = currentTestConfig?.tolerance ?: org.json.JSONObject()
val tolJson = tol.toString()
val result = ImageDiff.compare(golden, flipped, tolJson, null)
passed = (result.status == ImageDiff.Result.Status.PASSED)
diffMetric = result.failingPixelCount.toFloat()
if (!passed) {
if (result.diffImage != null) {
callback?.onImageResult("Diff", result.diffImage!!)
resultManager.saveImage("${testFullName}_diff", result.diffImage!!)
}
}
} else {
callback?.onStatusChanged("Failed to load golden")
}
} else {
Log.w("ValidationRunner", "Golden not found: ${goldenFile.absolutePath}")
callback?.onStatusChanged("Golden not found")
}
}
callback?.onTestFinished(TestResult(testFullName, passed))
// Schedule next model on main thread
// Use Handler or View.post
modelViewer.view.viewport
// dispatch nextModel()
// Save output
resultManager.saveImage(testFullName, flipped)
val result = ValidationResult(testFullName, passed, diffMetric)
resultManager.addResult(result)
callback?.onTestFinished(result)
android.os.Handler(android.os.Looper.getMainLooper()).post {
nextModel()
}
@@ -300,28 +312,8 @@ class ValidationRunner(
startTest(config.tests[currentTestIndex])
} else {
currentState = State.IDLE
zipResults()
resultManager.finalizeResults()
callback?.onAllTestsFinished()
}
}
private fun zipResults() {
callback?.onStatusChanged("Zipping results...")
val zipFile = File(outputDir, "results.zip")
try {
java.util.zip.ZipOutputStream(java.io.FileOutputStream(zipFile)).use { zos ->
outputDir.walkTopDown().filter { it.isFile && it.name != "results.zip" }.forEach { file ->
val entryName = file.relativeTo(outputDir).path
zos.putNextEntry(java.util.zip.ZipEntry(entryName))
file.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
Log.i("ValidationRunner", "Zipped results to ${zipFile.absolutePath}")
} catch (e: Exception) {
Log.e("ValidationRunner", "Failed to zip results", e)
}
}
data class TestResult(val name: String, val passed: Boolean)
}

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/surface_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/surface_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Initializing..."
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<Spinner
android:id="@+id/mode_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/run_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Run" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Test Results"
android:textSize="18sp"
android:paddingTop="20dp"
android:paddingBottom="10dp" />
<LinearLayout
android:id="@+id/results_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="5dp">
<TextView
android:id="@+id/image_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Label" />
<ImageView
android:id="@+id/image_view"
android:layout_width="300px"
android:layout_height="300px"
android:scaleType="fitCenter"
android:background="#404040" />
</LinearLayout>

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'filament-plugin'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -214,6 +214,8 @@ ENABLE_PERFETTO=""
BACKEND_DEBUG_FLAG_OPTION=""
STEREOSCOPIC_OPTION=""
OSMESA_OPTION=""
IOS_BUILD_SIMULATOR=false
@@ -314,6 +316,7 @@ function build_desktop_target {
${ASAN_UBSAN_OPTION} \
${COVERAGE_OPTION} \
${BACKEND_DEBUG_FLAG_OPTION} \
${STEREOSCOPIC_OPTION} \
${OSMESA_OPTION} \
${architectures} \
../..
@@ -452,6 +455,7 @@ function build_android_target {
${VULKAN_ANDROID_OPTION} \
${WEBGPU_OPTION} \
${BACKEND_DEBUG_FLAG_OPTION} \
${STEREOSCOPIC_OPTION} \
${ENABLE_PERFETTO} \
../..
ln -sf "out/cmake-android-${lc_target}-${arch}/compile_commands.json" \
@@ -693,6 +697,7 @@ function build_ios_target {
${WEBGPU_OPTION} \
${MATDBG_OPTION} \
${MATOPT_OPTION} \
${STEREOSCOPIC_OPTION} \
../..
ln -sf "out/cmake-ios-${lc_target}-${arch}/compile_commands.json" \
../../compile_commands.json
@@ -1006,6 +1011,20 @@ while getopts ":hacCfgimp:q:uvWslwedtk:bVx:S:X:Py:" opt; do
;;
x) BACKEND_DEBUG_FLAG_OPTION="-DFILAMENT_BACKEND_DEBUG_FLAG=${OPTARG}"
;;
S) case $(echo "${OPTARG}" | tr '[:upper:]' '[:lower:]') in
instanced)
STEREOSCOPIC_OPTION="-DFILAMENT_SAMPLES_STEREO_TYPE=instanced"
;;
multiview)
STEREOSCOPIC_OPTION="-DFILAMENT_SAMPLES_STEREO_TYPE=multiview"
;;
*)
echo "Unknown stereoscopic type ${OPTARG}"
echo "Type must be one of [instanced|multiview]"
echo ""
exit 1
esac
;;
X) OSMESA_OPTION="-DFILAMENT_OSMESA_PATH=${OPTARG}"
;;
y)

View File

@@ -39,6 +39,11 @@ if [[ "$TARGET" == "presubmit-with-test" ]]; then
RUN_TESTS=-u
fi
if [[ "$TARGET" == "presubmit-with-archive" ]]; then
BUILD_RELEASE=release
GENERATE_ARCHIVES=-a
fi
if [[ "$TARGET" == "debug" ]]; then
BUILD_DEBUG=debug
GENERATE_ARCHIVES=-a

View File

@@ -181,22 +181,21 @@ important for <code>matc</code> (material compiler).</p>
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.69.0'
implementation 'com.google.android.filament:filament-android:1.69.3'
}
</code></pre>
<p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p>
<div class="table-wrapper"><table><thead><tr><th>Artifact</th><th>Description</th></tr></thead><tbody>
<tr><td><a href="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android"><img src="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android/badge.svg?subject=filament-android" alt="filament-android" /></a></td><td>The Filament rendering engine itself.</td></tr>
<tr><td><a href="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android-debug"><img src="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android-debug/badge.svg?subject=filament-android-debug" alt="filament-android-debug" /></a></td><td>Debug version of <code>filament-android</code>.</td></tr>
<tr><td><a href="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/gltfio-android"><img src="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/gltfio-android/badge.svg?subject=gltfio-android" alt="gltfio-android" /></a></td><td>A glTF 2.0 loader for Filament, depends on <code>filament-android</code>.</td></tr>
<tr><td><a href="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-utils-android"><img src="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-utils-android/badge.svg?subject=filament-utils-android" alt="filament-utils-android" /></a></td><td>KTX loading, Kotlin math, and camera utilities, depends on <code>gltfio-android</code>.</td></tr>
<tr><td><a href="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android"><img src="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android/badge.svg?subject=filamat-android" alt="filamat-android" /></a></td><td>A runtime material builder/compiler. This library is large but contains a full shader compiler/validator/optimizer and supports both OpenGL and Vulkan.</td></tr>
<tr><td><a href="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android-lite"><img src="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android-lite/badge.svg?subject=filamat-android-lite" alt="filamat-android-lite" /></a></td><td>A much smaller alternative to <code>filamat-android</code> that can only generate OpenGL shaders. It does not provide validation or optimizations.</td></tr>
<tr><td><a href="https://mvnrepository.com/artifact/com.google.android.filament/filament-android"><img src="https://img.shields.io/maven-central/v/com.google.android.filament/filament-android?label=filament-android&amp;color=green" alt="filament-android" /></a></td><td>The Filament rendering engine itself.</td></tr>
<tr><td><a href="https://mvnrepository.com/artifact/com.google.android.filament/filament-android-debug"><img src="https://img.shields.io/maven-central/v/com.google.android.filament/filament-android-debug?label=filament-android-debug&amp;color=green" alt="filament-android-debug" /></a></td><td>Debug version of <code>filament-android</code>.</td></tr>
<tr><td><a href="https://mvnrepository.com/artifact/com.google.android.filament/gltfio-android"><img src="https://img.shields.io/maven-central/v/com.google.android.filament/gltfio-android?label=gltfio-android&amp;color=green" alt="gltfio-android" /></a></td><td>A glTF 2.0 loader for Filament, depends on <code>filament-android</code>.</td></tr>
<tr><td><a href="https://mvnrepository.com/artifact/com.google.android.filament/filament-utils-android"><img src="https://img.shields.io/maven-central/v/com.google.android.filament/filament-utils-android?label=filament-utils-android&amp;color=green" alt="filament-utils-android" /></a></td><td>KTX loading, Kotlin math, and camera utilities, depends on <code>gltfio-android</code>.</td></tr>
<tr><td><a href="https://mvnrepository.com/artifact/com.google.android.filament/filamat-android"><img src="https://img.shields.io/maven-central/v/com.google.android.filament/filamat-android?label=filamat-android&amp;color=green" alt="filamat-android" /></a></td><td>A runtime material builder/compiler. This library is large but contains a full shader compiler/validator/optimizer and supports both OpenGL and Vulkan.</td></tr>
</tbody></table>
</div>
<h3 id="ios"><a class="header" href="#ios">iOS</a></h3>
<p>iOS projects can use CocoaPods to install the latest release:</p>
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.69.0'
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.69.3'
</code></pre>
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
<ul>
@@ -230,7 +229,8 @@ sheet for the standard material model.</li>
<li>OpenGL ES 3.0+ for Android and iOS</li>
<li>Metal for macOS and iOS</li>
<li>Vulkan 1.0 for Android, Linux, macOS, and Windows</li>
<li>WebGL 2.0 for all platforms</li>
<li>WebGPU for Android, Linux, macOS, and Windows</li>
<li>WebGL 2.0 for all browsers supporting it</li>
</ul>
<h3 id="rendering"><a class="header" href="#rendering">Rendering</a></h3>
<ul>
@@ -265,7 +265,7 @@ sheet for the standard material model.</li>
<ul>
<li>HDR bloom</li>
<li>Depth of field bokeh</li>
<li>Multiple tone mappers: generic (customizable), ACES, filmic, etc.</li>
<li>Multiple tone mappers: PBR Neutral, AgX, generic (customizable), ACES, filmic, etc.</li>
<li>Color and tone management: luminance scaling, gamut mapping</li>
<li>Color grading: exposure, night adaptation, white balance, channel mixer,
shadows/mid-tones/highlights, ASC CDL, contrast, saturation, etc.</li>
@@ -332,6 +332,8 @@ KHR_lights_punctual</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_clearcoat</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_dispersion</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_emissive_strength</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_ior</li>
@@ -340,6 +342,8 @@ KHR_materials_pbrSpecularGlossiness</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_sheen</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_specular</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_transmission</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_unlit</li>
@@ -348,8 +352,6 @@ KHR_materials_variants</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_volume</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_specular</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_mesh_quantization</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_texture_basisu</li>
@@ -501,7 +503,7 @@ and tools.</p>
<li><code>filamesh</code>: Mesh converter</li>
<li><code>glslminifier</code>: Minifies GLSL source code</li>
<li><code>matc</code>: Material compiler</li>
<li><code>filament-matp</code>: Material parser</li>
<li><code>matedit</code>: Material editor for compiled materials</li>
<li><code>matinfo</code> Displays information about materials compiled with <code>matc</code></li>
<li><code>mipgen</code> Generates a series of miplevels from a source image</li>
<li><code>normal-blending</code>: Tool to blend normal maps</li>

View File

@@ -195,6 +195,30 @@ for inspection.</li>
<p><strong>Tip:</strong> To view all defined subspecs, grep the podspec file:
<code>grep "spec.subspec" ios/CocoaPods/Filament.podspec</code></p>
</blockquote>
<h2 id="testing-a-podspec-locally"><a class="header" href="#testing-a-podspec-locally">Testing a Podspec Locally</a></h2>
<p>Before pushing a new version to the CocoaPods trunk, you should verify the podspec in a sample project.</p>
<h3 id="using-the-sample-project"><a class="header" href="#using-the-sample-project">Using the Sample Project</a></h3>
<p>Filament includes a dedicated sample project located in <code>ios/samples/HelloCocoaPods</code>. To test your
local changes:</p>
<ol>
<li><strong>Modify the Podfile</strong>: Add a <code>:podspec</code> directive pointing to your local <code>.podspec</code> file.</li>
</ol>
<pre><code class="language-ruby">platform :ios, '13.0'
target 'HelloCocoaPods' do
# Use the local development podspec
pod 'Filament', :podspec =&gt; '../../CocoaPods/Filament.podspec'
end
</code></pre>
<ol start="2">
<li><strong>Sync Dependencies</strong>: Run the installation command from the sample directory:</li>
</ol>
<pre><code class="language-bash">pod install
</code></pre>
<ol start="3">
<li><strong>Verify the Build</strong>: Open the generated <code>HelloCocoaPods.xcworkspace</code> in Xcode. Build and run the
target to ensure the headers are found and the binaries link correctly.</li>
</ol>
</main>

View File

@@ -199,17 +199,21 @@ Sonatype's <a href="https://central.sonatype.org/">Central Publisher Portal</a>.
<p>The new Central Publisher Portal does not officially support Gradle. However, Sonatype provides a
staging API compatibility service, which works with Filament's Gradle setup.</p>
<hr />
<h3 id="1-upload-to-the-staging-api-compatibility-service"><a class="header" href="#1-upload-to-the-staging-api-compatibility-service">1. Upload to the Staging API Compatibility Service</a></h3>
<h3 id="1-upload-to-the-central-publisher-portal"><a class="header" href="#1-upload-to-the-central-publisher-portal">1. Upload to the Central Publisher Portal</a></h3>
<p>To upload the artifacts, it is important to run both of these Gradle tasks together in a single
command. This ensures the staging repository is created and closed automatically.</p>
<pre><code class="language-bash">cd android
./gradlew publishToSonatype
./gradlew publishToSonatype closeSonatypeStagingRepository
</code></pre>
<h3 id="2-move-the-repository-to-the-central-publisher-portal"><a class="header" href="#2-move-the-repository-to-the-central-publisher-portal">2. Move the Repository to the Central Publisher Portal</a></h3>
<p>We have a script to automate this. It reads the <code>sonatypeUsername</code> and <code>sonatypePassword</code> from your
<code>~/.gradle/gradle.properties</code> file.</p>
<h4 id="troubleshooting-manual-staging"><a class="header" href="#troubleshooting-manual-staging">Troubleshooting: Manual Staging</a></h4>
<p>If you ran <code>publishToSonatype</code> by itself, the repository will remain open and won't appear in the
portal correctly. You can fix this by running our automation script, which uses the
<code>sonatypeUsername</code> and <code>sonatypePassword</code> from your ~/.gradle/gradle.properties file:</p>
<pre><code class="language-bash">python3 build/common/close-sonatype-staging-repository.py
</code></pre>
<h3 id="3-publish-the-release-on-sonatype"><a class="header" href="#3-publish-the-release-on-sonatype">3. Publish the Release on Sonatype</a></h3>
<p>Navigate to <a href="https://central.sonatype.com/publishing/deployments">Maven Central Repository Deployments</a>.</p>
<h3 id="2-publish-the-release-on-sonatype"><a class="header" href="#2-publish-the-release-on-sonatype">2. Publish the Release on Sonatype</a></h3>
<p>Once the upload is successful, you must manually trigger the final release. Navigate to <a href="https://central.sonatype.com/publishing/deployments">Maven
Central Repository Deployments</a>.</p>
<p>Here, you should see a new deployment with a <strong>Validated</strong> status and all your artifacts listed. Click
the <strong>Publish</strong> button to publish the artifacts. It typically takes around 5 minutes after clicking
<strong>Publish</strong> for the artifacts to go live.</p>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -16,12 +16,13 @@ class SimulatedSkybox {
this.ozone = 0.0;
this.msFactors = [0.1, 0.5, 0.0];
this.contrast = 1.0;
this.nightColor = [0.0, 0.0003, 0.00075];
this.nightColor = [0.0, 3.0e-9, 7.5e-9];
this.shimmerControl = [0.0, 20.0, 0.1];
this.cloudControl = [0.0, 0.1, 8000.0, 0.0];
this.cloudControl2 = [0.0, 0.0, 0.0, 0.0];
this.waterControl = [50.0, 1.0, 1.0, 4.0]; // x=Strength, y=Speed, z=DerivativeTrick, w=Octaves
this.starControl = [0.001, 1.0, 350.0, 0.01]; // x=Density, y=Enabled, z=Frequency, w=PixelScale
this.starIntensity = 1.0;
this.focalLength = 24.0;
this.height = 1000.0;
this.planetRadius = 6360.0;
@@ -43,7 +44,7 @@ class SimulatedSkybox {
// Milky Way Parameters
// x=Intensity, y=Saturation, z=Unused
this.milkyWayControl = [1.0, 1.0, 0.07];
this.milkyWayControl = [1.0, 1.2, 0.05];
this.milkyWayEnabled = true;
this.milkyWayRotation = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Identity by default
this.initEntity();
@@ -453,6 +454,11 @@ class SimulatedSkybox {
this.updateCoefficients();
}
setStarIntensity(intensity) {
this.starIntensity = Math.max(0.0, intensity);
this.updateCoefficients();
}
setMoonPosition(direction) {
// normalize
const len = Math.hypot(direction[0], direction[1], direction[2]);
@@ -500,6 +506,13 @@ class SimulatedSkybox {
}
}
setExposure(exposure) {
this.exposure = exposure;
this.updateCoefficients();
}
updateCoefficients() {
if (!this.materialInstance) {
console.warn("updateCoefficients called before material loaded");
@@ -585,6 +598,7 @@ class SimulatedSkybox {
this.materialInstance.setFloat4Parameter('cloudControl2', new Float32Array(this.cloudControl2));
this.materialInstance.setFloat4Parameter('waterControl', new Float32Array(this.waterControl));
this.materialInstance.setFloat4Parameter('starControl', new Float32Array(this.starControl));
this.materialInstance.setFloatParameter('starIntensity', this.starIntensity);
this.materialInstance.setFloatParameter('sunIntensity', physicalSunIntensity);
@@ -615,12 +629,13 @@ class SimulatedSkybox {
this.materialInstance.setFloatParameter('sunIntensity2', finalMoonIntensity);
// Scale Milky Way by Sun Intensity (Pre-exposed) to match dynamic range
// Calibration: 1.0 User Intensity ~ 0.025 Lux Relative (approx 2.5% of Sun Pixel Value at Day)
// At Sunny 16 (Sun ~ 2.6), this gives ~0.065 pixel brightness, which is visible but dim.
// Scale Milky Way by Sun Intensity
// Calibration: 1.0 User Intensity = 1.5e-3 cd/m^2 (Nits) approx.
// Target: 1.5e-3 Nits. SunIntensity = 100,000.
// Scale = 1.5e-3 / 1.0e5 = 1.5e-8.
const mwIntensity = this.milkyWayEnabled ? this.milkyWayControl[0] : 0.0;
const mwUniform = [
mwIntensity * this.sunIntensity * 0.025,
mwIntensity * this.sunIntensity * 1.5e-8,
this.milkyWayControl[1],
this.milkyWayControl[2]
];
@@ -634,6 +649,7 @@ class SimulatedSkybox {
const moonHaloUpload = [...this.moonHalo];
moonHaloUpload[2] *= moonRadConv;
this.materialInstance.setFloat4Parameter('sunHalo2', new Float32Array(moonHaloUpload));
this.materialInstance.setFloatParameter('exposure', this.exposure !== undefined ? this.exposure : 1.0);
// Solar Eclipse (CPU Calculation)
const sunRadius = Math.acos(this.sunHalo[0]);

View File

@@ -218,6 +218,7 @@ class App {
const exposure = this.getExposure();
const preExposedIntensity = this.params.sunIntensity * exposure;
this.skybox.setSunIntensity(preExposedIntensity);
this.skybox.setExposure(exposure); // Update Skybox Exposure Uniform
// Moon Exposure
if (this.mParams) {
@@ -389,7 +390,7 @@ class App {
};
mwFolder.add(this.mwParams, 'enabled').name('Enabled').onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 5.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 100.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'saturation', 0.0, 2.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'blackPoint', 0.0, 0.5).name('Black Point').onChange(updateMW);
mwFolder.add(this.mwParams, 'siderealTime', 0.0, 24.0).name('Sidereal Time').listen().onChange(updateMW);
@@ -464,18 +465,23 @@ class App {
const starFolder = gui.addFolder('Stars');
this.sParams = {
enabled: true,
density: 0.001
density: 0.001,
intensity: 0.0 // 2^0 = 1.0
};
// Initialize defaults (Density 0.001, Enabled True)
sky.setStarControl(0.001, true);
sky.setStarIntensity(Math.pow(2.0, 0.0));
const updateStars = () => {
sky.setStarControl(this.sParams.density, this.sParams.enabled);
sky.setStarIntensity(Math.pow(2.0, this.sParams.intensity));
};
starFolder.add(this.sParams, 'enabled').name('Enabled').onChange(updateStars);
starFolder.add(this.sParams, 'density', 0.0, 0.01, 0.0001).name('Density').onChange(updateStars);
starFolder.add(this.sParams, 'intensity', 0.0, 24.0).name('Intensity (Exp)').onChange(updateStars);
starFolder.close();
this.updateStars = updateStars;
const artFolder = gui.addFolder('Artistic');
// Set Horizon Glow default to 0.0
@@ -489,7 +495,7 @@ class App {
artFolder.add(sky.msFactors, 2, 0.0, 1.0).name('Horizon Glow').onChange(v => sky.setHorizonGlow(v));
artFolder.add(sky, 'contrast', 0.1, 2.0).onChange(v => sky.setContrast(v));
artFolder.addColor(sky, 'nightColor').onChange(v => sky.setNightColor(v));
const shmFolder = artFolder.addFolder('Shimmer');
// Set Shimmer Strength default to 0.0
@@ -502,7 +508,7 @@ class App {
const camFolder = gui.addFolder('Camera');
camFolder.add(this.params, 'focalLength', 8.0, 300.0).name('Focal Length').onChange(() => this.updateCameraProjection());
camFolder.add(this.params, 'aperture', 1.4, 32.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'shutterSpeed', 1.0, 1000.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'shutterSpeed', 0.05, 1000.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'iso', 50.0, 3200.0).onChange(() => this.updateCameraExposure());
const bloomFolder = camFolder.addFolder('Bloom');
@@ -692,13 +698,15 @@ class App {
const s = this.sParams;
const b = this.bParams;
const m = this.mParams;
const mw = this.mwParams;
const sk = this.skybox;
return {
p: { a: p.aperture, ss: p.shutterSpeed, i: p.iso, st: p.sunTheta, sp: p.sunPhi, fl: p.focalLength, si: p.sunIntensity },
c: { v: c.volumetrics, co: c.coverage, d: c.density, h: c.height, s: c.speed, e: c.evolution },
w: { dt: w.derivativeTrick, st: w.strength, s: w.speed, o: w.octaves },
s: { e: s.enabled, d: s.density },
s: { e: s.enabled, d: s.density, i: s.intensity },
mw: { e: mw.enabled, i: mw.intensity, s: mw.saturation, bp: mw.blackPoint, st: mw.siderealTime, l: mw.latitude },
b: { e: b.enabled, lf: b.lensFlare },
m: { e: m.enabled, az: m.azimuth, h: m.height, r: m.radius, i: m.intensity },
cm: { t: this.camState.theta, p: this.camState.phi },
@@ -723,6 +731,7 @@ class App {
const c = state.c;
const w = state.w;
const s = state.s;
const mw = state.mw;
const b = state.b;
const m = state.m;
const k = state.k;
@@ -757,6 +766,18 @@ class App {
if (s) {
if (s.e !== undefined) this.sParams.enabled = s.e;
if (s.d !== undefined) this.sParams.density = s.d;
if (s.i !== undefined) this.sParams.intensity = s.i;
if (this.updateStars) this.updateStars();
}
if (mw) {
if (mw.e !== undefined) this.mwParams.enabled = mw.e;
if (mw.i !== undefined) this.mwParams.intensity = mw.i;
if (mw.s !== undefined) this.mwParams.saturation = mw.s;
if (mw.bp !== undefined) this.mwParams.blackPoint = mw.bp;
if (mw.st !== undefined) this.mwParams.siderealTime = mw.st;
if (mw.l !== undefined) this.mwParams.latitude = mw.l;
if (this.updateMW) this.updateMW();
}
if (b) {

View File

@@ -96,10 +96,20 @@ material {
name : starControl, // x=Density, y=Enabled, z=Frequency, w=PixelScale
precision : high
},
{
type : float,
name : starIntensity,
precision : high
},
{
type : sampler2d,
name : moonTexture
},
{
type : float,
name : exposure,
precision : high
},
{
type : sampler2d,
name : moonNormal
@@ -162,9 +172,9 @@ fragment {
// --- CONFIGURATION ---
// Stars
#define STAR_GLOBAL_INTENSITY 150.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.5 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 4.0 // Random brightness variance range [0.0 - 10.0]
#define STAR_GLOBAL_INTENSITY 100.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.01 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 15.0 // Random brightness variance range [0.0 - 10.0]
#define STAR_FADE_SUN_ELV_HIGH 0.10 // Sun elevation (sin) where stars are fully hidden [0.0 - 0.5]
#define STAR_FADE_SUN_ELV_LOW -0.20 // Sun elevation (sin) where stars are fully visible [-0.5 - 0.0]
#define STAR_CLOUD_OCCLUSION 0.1 // Visibility when covered by clouds [0.0 - 1.0]
@@ -961,7 +971,11 @@ fragment {
highp vec4 sunHalo, highp vec4 cloudControl, highp vec4 cloudControl2,
highp vec4 shimmerControl, highp vec4 waterControl,
highp vec3 L2, highp float sunIntensity2, highp vec4 sunHalo2,
sampler2D moonTex, sampler2D moonNormal) {
sampler2D moonTex, sampler2D moonNormal,
highp vec3 nightColor,
highp mat3 milkyWayRotation,
highp vec3 milkyWayControl,
sampler2D milkyWayTexture) {
// Project to plane y=0
highp float t = WATER_PLANE_HEIGHT / min(V.y, -0.0002); // Reduced clamp to minimize "wall" artifact
@@ -1057,7 +1071,7 @@ fragment {
highp float rHorizonMask = 1.0 - smoothstep(0.0, REFLECTION_HORIZON_FADE, R.y);
if (rHorizonMask > 0.0) {
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask;
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask * materialParams.exposure;
}
// Sun Disk Reflection
@@ -1088,6 +1102,29 @@ fragment {
// Apply clouds to reflection
reflection = mix(reflection, reflCloudLayer, reflCloudDensity);
// Add Milky Way to Reflection
// Calculate Fade based on Sun Elevation
highp float sunElvSin = L.y;
highp float mwFade = smoothstep(STAR_FADE_SUN_ELV_HIGH, STAR_FADE_SUN_ELV_LOW, sunElvSin);
if (mwFade > 0.0) {
highp vec3 mwColor = getMilkyWay(R, milkyWayRotation, milkyWayTexture, milkyWayControl);
// Apply Atmosphere Transmittance (approximate)
mwColor *= outTransmittance;
// Apply Fades
mwColor *= mwFade;
mwColor *= (1.0 - reflMoonOcclusion); // Occlude by Moon
mwColor *= (1.0 - reflCloudDensity); // Occlude by Clouds
reflection += mwColor;
}
// Add Night Color to Reflection
reflection += nightColor;
// Fresnel
highp float F0 = WATER_FRESNEL_F0; // Water
highp float cosTheta = clamp(dot(-V, N_water), 0.0, 1.0);
@@ -1187,7 +1224,7 @@ fragment {
// 7. Stars
// Add stars before clouds (clouds cover stars)
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl);
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl) * materialParams.exposure * materialParams.starIntensity;
// 7b. Milky Way
// Add Milky Way behind stars (conceptually) but handled similarly
@@ -1232,7 +1269,11 @@ fragment {
materialParams.shimmerControl,
materialParams.waterControl,
L2, materialParams.sunIntensity2, materialParams.sunHalo2,
materialParams_moonTexture, materialParams_moonNormal);
materialParams_moonTexture, materialParams_moonNormal,
materialParams.nightColor,
materialParams.milkyWayRotation,
materialParams.milkyWayControl,
materialParams_milkyWayTexture);
color = applyDynamicToneMapping(color, L, materialParams.contrast);
}

View File

@@ -64,25 +64,30 @@ staging API compatibility service, which works with Filament's Gradle setup.
-----
### 1\. Upload to the Staging API Compatibility Service
### 1\. Upload to the Central Publisher Portal
To upload the artifacts, it is important to run both of these Gradle tasks together in a single
command. This ensures the staging repository is created and closed automatically.
```bash
cd android
./gradlew publishToSonatype
./gradlew publishToSonatype closeSonatypeStagingRepository
```
### 2\. Move the Repository to the Central Publisher Portal
#### Troubleshooting: Manual Staging
We have a script to automate this. It reads the `sonatypeUsername` and `sonatypePassword` from your
`~/.gradle/gradle.properties` file.
If you ran `publishToSonatype` by itself, the repository will remain open and won't appear in the
portal correctly. You can fix this by running our automation script, which uses the
`sonatypeUsername` and `sonatypePassword` from your ~/.gradle/gradle.properties file:
```bash
python3 build/common/close-sonatype-staging-repository.py
```
### 3\. Publish the Release on Sonatype
### 2\. Publish the Release on Sonatype
Navigate to [Maven Central Repository Deployments](https://central.sonatype.com/publishing/deployments).
Once the upload is successful, you must manually trigger the final release. Navigate to [Maven
Central Repository Deployments](https://central.sonatype.com/publishing/deployments).
Here, you should see a new deployment with a **Validated** status and all your artifacts listed. Click
the **Publish** button to publish the artifacts. It typically takes around 5 minutes after clicking

View File

@@ -16,12 +16,13 @@ class SimulatedSkybox {
this.ozone = 0.0;
this.msFactors = [0.1, 0.5, 0.0];
this.contrast = 1.0;
this.nightColor = [0.0, 0.0003, 0.00075];
this.nightColor = [0.0, 3.0e-9, 7.5e-9];
this.shimmerControl = [0.0, 20.0, 0.1];
this.cloudControl = [0.0, 0.1, 8000.0, 0.0];
this.cloudControl2 = [0.0, 0.0, 0.0, 0.0];
this.waterControl = [50.0, 1.0, 1.0, 4.0]; // x=Strength, y=Speed, z=DerivativeTrick, w=Octaves
this.starControl = [0.001, 1.0, 350.0, 0.01]; // x=Density, y=Enabled, z=Frequency, w=PixelScale
this.starIntensity = 1.0;
this.focalLength = 24.0;
this.height = 1000.0;
this.planetRadius = 6360.0;
@@ -43,7 +44,7 @@ class SimulatedSkybox {
// Milky Way Parameters
// x=Intensity, y=Saturation, z=Unused
this.milkyWayControl = [1.0, 1.0, 0.07];
this.milkyWayControl = [1.0, 1.2, 0.05];
this.milkyWayEnabled = true;
this.milkyWayRotation = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Identity by default
this.initEntity();
@@ -453,6 +454,11 @@ class SimulatedSkybox {
this.updateCoefficients();
}
setStarIntensity(intensity) {
this.starIntensity = Math.max(0.0, intensity);
this.updateCoefficients();
}
setMoonPosition(direction) {
// normalize
const len = Math.hypot(direction[0], direction[1], direction[2]);
@@ -500,6 +506,13 @@ class SimulatedSkybox {
}
}
setExposure(exposure) {
this.exposure = exposure;
this.updateCoefficients();
}
updateCoefficients() {
if (!this.materialInstance) {
console.warn("updateCoefficients called before material loaded");
@@ -585,6 +598,7 @@ class SimulatedSkybox {
this.materialInstance.setFloat4Parameter('cloudControl2', new Float32Array(this.cloudControl2));
this.materialInstance.setFloat4Parameter('waterControl', new Float32Array(this.waterControl));
this.materialInstance.setFloat4Parameter('starControl', new Float32Array(this.starControl));
this.materialInstance.setFloatParameter('starIntensity', this.starIntensity);
this.materialInstance.setFloatParameter('sunIntensity', physicalSunIntensity);
@@ -615,12 +629,13 @@ class SimulatedSkybox {
this.materialInstance.setFloatParameter('sunIntensity2', finalMoonIntensity);
// Scale Milky Way by Sun Intensity (Pre-exposed) to match dynamic range
// Calibration: 1.0 User Intensity ~ 0.025 Lux Relative (approx 2.5% of Sun Pixel Value at Day)
// At Sunny 16 (Sun ~ 2.6), this gives ~0.065 pixel brightness, which is visible but dim.
// Scale Milky Way by Sun Intensity
// Calibration: 1.0 User Intensity = 1.5e-3 cd/m^2 (Nits) approx.
// Target: 1.5e-3 Nits. SunIntensity = 100,000.
// Scale = 1.5e-3 / 1.0e5 = 1.5e-8.
const mwIntensity = this.milkyWayEnabled ? this.milkyWayControl[0] : 0.0;
const mwUniform = [
mwIntensity * this.sunIntensity * 0.025,
mwIntensity * this.sunIntensity * 1.5e-8,
this.milkyWayControl[1],
this.milkyWayControl[2]
];
@@ -634,6 +649,7 @@ class SimulatedSkybox {
const moonHaloUpload = [...this.moonHalo];
moonHaloUpload[2] *= moonRadConv;
this.materialInstance.setFloat4Parameter('sunHalo2', new Float32Array(moonHaloUpload));
this.materialInstance.setFloatParameter('exposure', this.exposure !== undefined ? this.exposure : 1.0);
// Solar Eclipse (CPU Calculation)
const sunRadius = Math.acos(this.sunHalo[0]);

View File

@@ -218,6 +218,7 @@ class App {
const exposure = this.getExposure();
const preExposedIntensity = this.params.sunIntensity * exposure;
this.skybox.setSunIntensity(preExposedIntensity);
this.skybox.setExposure(exposure); // Update Skybox Exposure Uniform
// Moon Exposure
if (this.mParams) {
@@ -389,7 +390,7 @@ class App {
};
mwFolder.add(this.mwParams, 'enabled').name('Enabled').onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 5.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 100.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'saturation', 0.0, 2.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'blackPoint', 0.0, 0.5).name('Black Point').onChange(updateMW);
mwFolder.add(this.mwParams, 'siderealTime', 0.0, 24.0).name('Sidereal Time').listen().onChange(updateMW);
@@ -464,18 +465,23 @@ class App {
const starFolder = gui.addFolder('Stars');
this.sParams = {
enabled: true,
density: 0.001
density: 0.001,
intensity: 0.0 // 2^0 = 1.0
};
// Initialize defaults (Density 0.001, Enabled True)
sky.setStarControl(0.001, true);
sky.setStarIntensity(Math.pow(2.0, 0.0));
const updateStars = () => {
sky.setStarControl(this.sParams.density, this.sParams.enabled);
sky.setStarIntensity(Math.pow(2.0, this.sParams.intensity));
};
starFolder.add(this.sParams, 'enabled').name('Enabled').onChange(updateStars);
starFolder.add(this.sParams, 'density', 0.0, 0.01, 0.0001).name('Density').onChange(updateStars);
starFolder.add(this.sParams, 'intensity', 0.0, 24.0).name('Intensity (Exp)').onChange(updateStars);
starFolder.close();
this.updateStars = updateStars;
const artFolder = gui.addFolder('Artistic');
// Set Horizon Glow default to 0.0
@@ -489,7 +495,7 @@ class App {
artFolder.add(sky.msFactors, 2, 0.0, 1.0).name('Horizon Glow').onChange(v => sky.setHorizonGlow(v));
artFolder.add(sky, 'contrast', 0.1, 2.0).onChange(v => sky.setContrast(v));
artFolder.addColor(sky, 'nightColor').onChange(v => sky.setNightColor(v));
const shmFolder = artFolder.addFolder('Shimmer');
// Set Shimmer Strength default to 0.0
@@ -502,7 +508,7 @@ class App {
const camFolder = gui.addFolder('Camera');
camFolder.add(this.params, 'focalLength', 8.0, 300.0).name('Focal Length').onChange(() => this.updateCameraProjection());
camFolder.add(this.params, 'aperture', 1.4, 32.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'shutterSpeed', 1.0, 1000.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'shutterSpeed', 0.05, 1000.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'iso', 50.0, 3200.0).onChange(() => this.updateCameraExposure());
const bloomFolder = camFolder.addFolder('Bloom');
@@ -692,13 +698,15 @@ class App {
const s = this.sParams;
const b = this.bParams;
const m = this.mParams;
const mw = this.mwParams;
const sk = this.skybox;
return {
p: { a: p.aperture, ss: p.shutterSpeed, i: p.iso, st: p.sunTheta, sp: p.sunPhi, fl: p.focalLength, si: p.sunIntensity },
c: { v: c.volumetrics, co: c.coverage, d: c.density, h: c.height, s: c.speed, e: c.evolution },
w: { dt: w.derivativeTrick, st: w.strength, s: w.speed, o: w.octaves },
s: { e: s.enabled, d: s.density },
s: { e: s.enabled, d: s.density, i: s.intensity },
mw: { e: mw.enabled, i: mw.intensity, s: mw.saturation, bp: mw.blackPoint, st: mw.siderealTime, l: mw.latitude },
b: { e: b.enabled, lf: b.lensFlare },
m: { e: m.enabled, az: m.azimuth, h: m.height, r: m.radius, i: m.intensity },
cm: { t: this.camState.theta, p: this.camState.phi },
@@ -723,6 +731,7 @@ class App {
const c = state.c;
const w = state.w;
const s = state.s;
const mw = state.mw;
const b = state.b;
const m = state.m;
const k = state.k;
@@ -757,6 +766,18 @@ class App {
if (s) {
if (s.e !== undefined) this.sParams.enabled = s.e;
if (s.d !== undefined) this.sParams.density = s.d;
if (s.i !== undefined) this.sParams.intensity = s.i;
if (this.updateStars) this.updateStars();
}
if (mw) {
if (mw.e !== undefined) this.mwParams.enabled = mw.e;
if (mw.i !== undefined) this.mwParams.intensity = mw.i;
if (mw.s !== undefined) this.mwParams.saturation = mw.s;
if (mw.bp !== undefined) this.mwParams.blackPoint = mw.bp;
if (mw.st !== undefined) this.mwParams.siderealTime = mw.st;
if (mw.l !== undefined) this.mwParams.latitude = mw.l;
if (this.updateMW) this.updateMW();
}
if (b) {

View File

@@ -96,10 +96,20 @@ material {
name : starControl, // x=Density, y=Enabled, z=Frequency, w=PixelScale
precision : high
},
{
type : float,
name : starIntensity,
precision : high
},
{
type : sampler2d,
name : moonTexture
},
{
type : float,
name : exposure,
precision : high
},
{
type : sampler2d,
name : moonNormal
@@ -162,9 +172,9 @@ fragment {
// --- CONFIGURATION ---
// Stars
#define STAR_GLOBAL_INTENSITY 150.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.5 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 4.0 // Random brightness variance range [0.0 - 10.0]
#define STAR_GLOBAL_INTENSITY 100.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.01 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 15.0 // Random brightness variance range [0.0 - 10.0]
#define STAR_FADE_SUN_ELV_HIGH 0.10 // Sun elevation (sin) where stars are fully hidden [0.0 - 0.5]
#define STAR_FADE_SUN_ELV_LOW -0.20 // Sun elevation (sin) where stars are fully visible [-0.5 - 0.0]
#define STAR_CLOUD_OCCLUSION 0.1 // Visibility when covered by clouds [0.0 - 1.0]
@@ -961,7 +971,11 @@ fragment {
highp vec4 sunHalo, highp vec4 cloudControl, highp vec4 cloudControl2,
highp vec4 shimmerControl, highp vec4 waterControl,
highp vec3 L2, highp float sunIntensity2, highp vec4 sunHalo2,
sampler2D moonTex, sampler2D moonNormal) {
sampler2D moonTex, sampler2D moonNormal,
highp vec3 nightColor,
highp mat3 milkyWayRotation,
highp vec3 milkyWayControl,
sampler2D milkyWayTexture) {
// Project to plane y=0
highp float t = WATER_PLANE_HEIGHT / min(V.y, -0.0002); // Reduced clamp to minimize "wall" artifact
@@ -1057,7 +1071,7 @@ fragment {
highp float rHorizonMask = 1.0 - smoothstep(0.0, REFLECTION_HORIZON_FADE, R.y);
if (rHorizonMask > 0.0) {
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask;
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask * materialParams.exposure;
}
// Sun Disk Reflection
@@ -1088,6 +1102,29 @@ fragment {
// Apply clouds to reflection
reflection = mix(reflection, reflCloudLayer, reflCloudDensity);
// Add Milky Way to Reflection
// Calculate Fade based on Sun Elevation
highp float sunElvSin = L.y;
highp float mwFade = smoothstep(STAR_FADE_SUN_ELV_HIGH, STAR_FADE_SUN_ELV_LOW, sunElvSin);
if (mwFade > 0.0) {
highp vec3 mwColor = getMilkyWay(R, milkyWayRotation, milkyWayTexture, milkyWayControl);
// Apply Atmosphere Transmittance (approximate)
mwColor *= outTransmittance;
// Apply Fades
mwColor *= mwFade;
mwColor *= (1.0 - reflMoonOcclusion); // Occlude by Moon
mwColor *= (1.0 - reflCloudDensity); // Occlude by Clouds
reflection += mwColor;
}
// Add Night Color to Reflection
reflection += nightColor;
// Fresnel
highp float F0 = WATER_FRESNEL_F0; // Water
highp float cosTheta = clamp(dot(-V, N_water), 0.0, 1.0);
@@ -1187,7 +1224,7 @@ fragment {
// 7. Stars
// Add stars before clouds (clouds cover stars)
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl);
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl) * materialParams.exposure * materialParams.starIntensity;
// 7b. Milky Way
// Add Milky Way behind stars (conceptually) but handled similarly
@@ -1232,7 +1269,11 @@ fragment {
materialParams.shimmerControl,
materialParams.waterControl,
L2, materialParams.sunIntensity2, materialParams.sunHalo2,
materialParams_moonTexture, materialParams_moonNormal);
materialParams_moonTexture, materialParams_moonNormal,
materialParams.nightColor,
materialParams.milkyWayRotation,
materialParams.milkyWayControl,
materialParams_milkyWayTexture);
color = applyDynamicToneMapping(color, L, materialParams.contrast);
}

View File

@@ -314,6 +314,12 @@ set(MATERIAL_FL0_SRCS
src/materials/skybox.mat
)
set(MATERIAL_MULTIVIEW_SRCS
src/materials/clearDepth.mat
src/materials/defaultMaterial.mat
src/materials/skybox.mat
)
# ==================================================================================================
# Configuration
# ==================================================================================================
@@ -338,6 +344,11 @@ if (FILAMENT_ENABLE_FEATURE_LEVEL_0)
add_definitions(-DFILAMENT_ENABLE_FEATURE_LEVEL_0)
endif()
# Whether to include MULTIVIEW materials.
if (FILAMENT_ENABLE_MULTIVIEW)
add_definitions(-DFILAMENT_ENABLE_MULTIVIEW)
endif()
# Whether to force the profiling mode.
if (FILAMENT_FORCE_PROFILING_MODE)
add_definitions(-DFILAMENT_FORCE_PROFILING_MODE)
@@ -428,6 +439,21 @@ foreach(mat_dir ${MATERIAL_DIRS})
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path_fl0})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path_fl0})
endif()
# --- Multiview variant ---
list(FIND MATERIAL_MULTIVIEW_SRCS ${mat_src} index)
if (${index} GREATER -1 AND FILAMENT_ENABLE_MULTIVIEW)
string(REGEX REPLACE "[.]filamat$" "_multiview.filamat" output_path_multiview ${output_path})
add_custom_command(
OUTPUT ${output_path_multiview}
COMMAND matc ${MATC_BASE_FLAGS} -PstereoscopicType=multiview -o ${output_path_multiview} ${fullname}
MAIN_DEPENDENCY ${fullname}
DEPENDS matc
COMMENT "Compiling material ${fullname} (Multiview)"
)
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path_multiview})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path_multiview})
endif()
endforeach()
# Generate a single resource file for the whole group

View File

@@ -578,6 +578,7 @@ if (APPLE OR LINUX)
test/Shader.cpp
test/SharedShaders.cpp
test/Skip.cpp
test/test_Autoresolve.cpp
test/test_FeedbackLoops.cpp
test/test_Blit.cpp
test/test_MissingRequiredAttributes.cpp

View File

@@ -561,16 +561,18 @@ public:
/**
* Sets the callback function that the backend can use to update backend-specific statistics
* to aid with debugging. This callback is guaranteed to be called on the Filament driver
* thread.
* to aid with debugging. This callback can be called on either the Filament main thread or
* the Filament driver thread.
*
* The callback signature is (key, intValue, stringValue). Note that for any given call,
* only one of the value parameters (intValue or stringValue) will be meaningful, depending on
* the specific key.
*
* IMPORTANT_NOTE: because the callback is called on the driver thread, only quick, non-blocking
* work should be done inside it. Furthermore, no graphics API calls (such as GL calls) should
* be made, which could interfere with Filament's driver state.
* IMPORTANT_NOTE: because the callback can be called on the driver thread, only quick,
* non-blocking work should be done inside it. Furthermore, no graphics API calls (such as GL
* calls) should be made, which could interfere with Filament's driver state. Lastly, the
* callback implementation must be synchronized (thread-safe) since it can be called from
* either thread.
*
* @param debugUpdateStat an Invocable that updates debug statistics
*/
@@ -587,8 +589,7 @@ public:
* with a given key. It is possible for this function to be called multiple times with the
* same key, in which case newer values should overwrite older values.
*
* This function is guaranteed to be called only on a single thread, the Filament driver
* thread.
* This function can be called on either the Filament main thread or the Filament driver thread.
*
* @param key a null-terminated C-string with the key of the debug statistic
* @param intValue the updated integer value of key (the string value passed to the
@@ -602,8 +603,7 @@ public:
* with a given key. It is possible for this function to be called multiple times with the
* same key, in which case newer values should overwrite older values.
*
* This function is guaranteed to be called only on a single thread, the Filament driver
* thread.
* This function can be called on either the Filament main thread or the Filament driver thread.
*
* @param key a null-terminated C-string with the key of the debug statistic
* @param stringValue the updated string value of key (the integer value passed to the

View File

@@ -27,6 +27,8 @@
#include <stddef.h>
#include <stdint.h>
#include <functional>
namespace filament::backend {
/*
@@ -64,7 +66,7 @@ public:
// all commands buffers (Slices) written to this point are returned by waitForCommand(). This
// call blocks until the CircularBuffer has at least mRequiredSize bytes available.
void flush();
void flush(std::function<void(void*, void*)> const& debugPrintHistogram = nullptr);
// returns from waitForCommands() immediately.
void requestExit();

View File

@@ -56,6 +56,7 @@ namespace filament::backend {
class CommandBase {
static constexpr size_t FILAMENT_OBJECT_ALIGNMENT = alignof(std::max_align_t);
friend class CommandStream;
protected:
using Execute = Dispatcher::Execute;
@@ -168,8 +169,8 @@ struct CommandType<void (Driver::*)(ARGS...)> {
class CustomCommand : public CommandBase {
std::function<void()> mCommand;
static void execute(Driver&, CommandBase* base, intptr_t* next);
public:
static void execute(Driver&, CommandBase* base, intptr_t* next);
CustomCommand(CustomCommand&& rhs) = default;
explicit CustomCommand(std::function<void()> cmd)
@@ -179,11 +180,12 @@ public:
// ------------------------------------------------------------------------------------------------
class NoopCommand : public CommandBase {
public:
intptr_t mNext;
static void execute(Driver&, CommandBase* self, intptr_t* next) noexcept {
*next = static_cast<NoopCommand*>(self)->mNext;
}
public:
constexpr explicit NoopCommand(void* next) noexcept
: CommandBase(execute), mNext(intptr_t((char *)next - (char *)this)) { }
};
@@ -219,6 +221,36 @@ public:
CircularBuffer const& getCircularBuffer() const noexcept { return mCurrentBuffer; }
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
using Execute = Dispatcher::Execute;
struct CommandInfo {
size_t size;
const char* name;
int index;
};
std::unordered_map<Execute, CommandInfo> mCommands;
void initializeLookup() {
int currentIndex = 0;
#define DECL_DRIVER_API_SYNCHRONOUS(RetType, methodName, paramsDecl, params)
#define DECL_DRIVER_API(methodName, paramsDecl, params) \
mCommands[mDispatcher.methodName##_] = { CommandBase::align(sizeof(COMMAND_TYPE(methodName))), \
#methodName, currentIndex++ };
#define DECL_DRIVER_API_RETURN(RetType, methodName, paramsDecl, params) \
mCommands[mDispatcher.methodName##_] = { \
CommandBase::align(sizeof(COMMAND_TYPE(methodName##R))), #methodName, currentIndex++ \
};
#include "private/backend/DriverAPI.inc"
mCommands[CustomCommand::execute] = { CommandBase::align(sizeof(CustomCommand)),
"CustomCommand", currentIndex++ };
// NoopCommands have variable size. We will handle them specially using their mNext pointer.
mCommands[NoopCommand::execute] = { 0, "NoopCommand", currentIndex++ };
}
#endif
public:
#define DECL_DRIVER_API(methodName, paramsDecl, params) \
inline void methodName(paramsDecl) noexcept { \
@@ -263,6 +295,13 @@ public:
void execute(void* buffer);
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
void debugIterateCommands(void* head, void* tail,
std::function<void(CommandInfo const& info)> const& callback);
void debugPrintHistogram(void* head, void* tail);
#endif
/*
* queueCommand() allows to queue a lambda function as a command.
* This is much less efficient than using the Driver* API.

View File

@@ -48,6 +48,11 @@
#define FILAMENT_DEBUG_COMMANDS FILAMENT_DEBUG_COMMANDS_NONE
// Upon command stream overflow, print a histogram of commands
#ifndef FILAMENT_DEBUG_COMMANDS_HISTOGRAM
#define FILAMENT_DEBUG_COMMANDS_HISTOGRAM 0
#endif
namespace filament::backend {
class BufferDescriptor;

View File

@@ -79,7 +79,7 @@ bool CommandBufferQueue::isExitRequested() const {
}
void CommandBufferQueue::flush() {
void CommandBufferQueue::flush(std::function<void(void*, void*)> const& debugPrintHistogram) {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
CircularBuffer& circularBuffer = mCircularBuffer;
@@ -106,6 +106,13 @@ void CommandBufferQueue::flush() {
std::unique_lock lock(mLock);
// circular buffer is too small, we corrupted the stream
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
if (UTILS_VERY_UNLIKELY(used > mFreeSpace)) {
if (debugPrintHistogram) {
debugPrintHistogram(begin, end);
}
}
#endif
FILAMENT_CHECK_POSTCONDITION(used <= mFreeSpace) <<
"Backend CommandStream overflow. Commands are corrupted and unrecoverable.\n"
"Please increase minCommandBufferSizeMB inside the Config passed to Engine::create.\n"

View File

@@ -30,6 +30,11 @@
#include <utils/ostream.h>
#include <utils/sstream.h>
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
#include <algorithm>
#include <vector>
#endif
#include <cstddef>
#include <functional>
#include <string>
@@ -83,6 +88,10 @@ CommandStream::CommandStream(Driver& driver, CircularBuffer& buffer) noexcept
__system_property_get("debug.filament.perfcounters", property);
mUsePerformanceCounter = bool(atoi(property));
#endif
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
initializeLookup();
#endif
}
void CommandStream::execute(void* buffer) {
@@ -126,6 +135,71 @@ void CommandStream::execute(void* buffer) {
}
}
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
void CommandStream::debugIterateCommands(void* head, void* tail,
std::function<void(CommandInfo const& info)> const& callback) {
CommandBase* UTILS_RESTRICT base = static_cast<CommandBase*>(head);
auto p = base;
while (UTILS_LIKELY(p)) {
if (p >= tail) {
break;
}
Execute e = p->mExecute;
if (e == NoopCommand::execute) {
NoopCommand* noop = static_cast<NoopCommand*>(p);
size_t size = noop->mNext;
int noopIndex = mCommands[NoopCommand::execute].index;
callback({ size, "NoopCommand", noopIndex });
p = reinterpret_cast<CommandBase*>(reinterpret_cast<char*>(p) + size);
continue;
}
if (auto it = mCommands.find(e); it != mCommands.end()) {
size_t size = it->second.size;
callback(it->second);
p = reinterpret_cast<CommandBase*>(reinterpret_cast<char*>(p) + size);
} else {
LOG(ERROR) << "Cannot find command in lookup table";
return;
}
}
}
void CommandStream::debugPrintHistogram(void* head, void* tail) {
std::unordered_map<std::string_view, int> histogram;
std::unordered_map<int, int> index_histogram;
debugIterateCommands(head, tail, [&](CommandInfo const& info) {
histogram[std::string_view(info.name)]++;
index_histogram[info.index]++;
});
std::vector<std::pair<std::string_view, int>> sorted_histogram(histogram.begin(),
histogram.end());
std::sort(sorted_histogram.begin(), sorted_histogram.end(),
[](auto const& a, auto const& b) { return a.second > b.second; });
LOG(INFO) << "Command stream histogram:";
for (auto const& [name, count]: sorted_histogram) {
LOG(INFO) << name << ": " << count;
}
std::vector<std::pair<int, int>> sorted_index_histogram(index_histogram.begin(),
index_histogram.end());
std::sort(sorted_index_histogram.begin(), sorted_index_histogram.end(),
[](auto const& a, auto const& b) { return a.second > b.second; });
std::string short_histogram = "";
for (size_t i = 0, n = sorted_index_histogram.size(); i < n; ++i) {
short_histogram += std::to_string(sorted_index_histogram[i].first) + ":" +
std::to_string(sorted_index_histogram[i].second);
short_histogram += (i < n - 1) ? ";" : ".";
}
LOG(INFO) << "CS hist: " << short_histogram;
LOG(INFO) << "";
}
#endif
void CommandStream::queueCommand(std::function<void()> command) {
new(allocateCommand(CustomCommand::align(sizeof(CustomCommand)))) CustomCommand(std::move(command));
}

View File

@@ -19,21 +19,169 @@
#include <backend/PixelBufferDescriptor.h>
#include <math/scalar.h>
#include <math/half.h>
#include <utils/debug.h>
#include <utils/Logger.h>
#include <cstdint>
#include <cstring>
#include <stddef.h>
#include <stdint.h>
#include <math/scalar.h>
#include <utils/debug.h>
namespace filament {
namespace backend {
namespace {
// Provides an alpha value when expanding 3-channel images to 4-channel.
// Also used as a normalization scale when converting between numeric types.
template<typename componentType> inline componentType getMaxValue();
template<> inline constexpr float getMaxValue() { return 1.0f; }
template<> inline constexpr int32_t getMaxValue() { return 0x7fffffff; }
template<> inline constexpr uint32_t getMaxValue() { return 0xffffffff; }
template<> inline constexpr uint16_t getMaxValue() { return 0x3c00; } // 0x3c00 is 1.0 in half-float.
template<> inline constexpr uint8_t getMaxValue() { return 0xff; }
template<> inline math::half getMaxValue() { return math::half(1.0f); }
// We use template below to reduce code duplication across the different input/output
// type/channle-count permutations. Morever, templates help us reduce the number of conditionals
// in the inner-loop of the reshape operation. However, this needs to be a carefully considered
// because too many templated params will cause a large binary size increase.
// Note that we intentionally do not want to expand the template params to include the channel count
// because of the size increase.
template<typename dstComponentType, bool hasAlpha>
void grayscaleFill(dstComponentType* dst, uint8_t, uint8_t) {
for (size_t channel = 1; channel < 3; ++channel) {
dst[channel] = dst[0];
}
if constexpr (hasAlpha) {
dst[3] = getMaxValue<dstComponentType>();
}
}
// Note that we intentionally do not want to expand the template params to include the channel count
// because of the size increase.
template<typename dstComponentType>
inline void maxValFill(dstComponentType* dst, uint8_t srcChannelCount, uint8_t dstChannelCount) {
dstComponentType dstMaxValue = getMaxValue<dstComponentType>();
for (size_t channel = srcChannelCount; channel < dstChannelCount; ++channel) {
dst[channel] = dstMaxValue;
}
}
// Converts a n-channel image of UBYTE, INT, UINT, HALF, or FLOAT to a different type.
template<typename dstComponentType, typename srcComponentType>
void reshapeImageImpl(uint8_t* UTILS_RESTRICT dest, const uint8_t* UTILS_RESTRICT src,
size_t srcBytesPerRow, size_t srcChannelCount, size_t dstRowOffset, size_t dstColumnOffset,
size_t dstBytesPerRow, size_t dstChannelCount, size_t width, size_t height, bool swizzle) {
static_assert(!std::is_same_v<dstComponentType, math::half>);
const size_t minChannelCount = math::min(srcChannelCount, dstChannelCount);
const dstComponentType dstMaxValue = getMaxValue<dstComponentType>();
const srcComponentType srcMaxValue = getMaxValue<srcComponentType>();
double const mFactor = dstMaxValue / ((double) srcMaxValue);
assert_invariant(minChannelCount <= 4);
UTILS_ASSUME(minChannelCount <= 4);
dest += (dstRowOffset * dstBytesPerRow);
void (*fill)(dstComponentType*, uint8_t, uint8_t);
if (srcChannelCount == 1 && dstChannelCount == 3) {
fill = grayscaleFill<dstComponentType, false>;
} else if (srcChannelCount == 1 && dstChannelCount == 4) {
fill = grayscaleFill<dstComponentType, true>;
} else {
fill = maxValFill<dstComponentType>;
}
const int inds[4] = { swizzle ? 2 : 0, 1, swizzle ? 0 : 2, 3 };
for (size_t row = 0; row < height; ++row) {
const srcComponentType* in = (const srcComponentType*) src;
dstComponentType* out = (dstComponentType*) dest + (dstColumnOffset * dstChannelCount);
for (size_t column = 0; column < width; ++column) {
for (uint8_t channel = 0; channel < minChannelCount; ++channel) {
if constexpr (std::is_same_v<dstComponentType, srcComponentType>) {
out[channel] = in[inds[channel]];
} else {
// convert to double then clamp and cast to dst type.
out[channel] = static_cast<dstComponentType>(std::clamp(
in[inds[channel]] * mFactor, 0.0,
static_cast<double>(std::numeric_limits<dstComponentType>::max())));
}
}
// This will fill in all the channels that are not copied.
fill(out, srcChannelCount, dstChannelCount);
in += srcChannelCount;
out += dstChannelCount;
}
src += srcBytesPerRow;
dest += dstBytesPerRow;
}
}
struct UnpackerR11G11B10 {
static void unpack(const uint8_t* src, float* out) {
uint32_t p;
std::memcpy(&p, src, 4);
using R11 = math::fp<0, 5, 6>;
using G11 = math::fp<0, 5, 6>;
using B10 = math::fp<0, 5, 5>;
out[0] = R11::tof(R11(uint16_t((p >> 21) & 0x7FF)));
out[1] = G11::tof(G11(uint16_t((p >> 10) & 0x7FF)));
out[2] = B10::tof(B10(uint16_t(p & 0x3FF)));
}
};
template<typename dstComponentType, typename Unpacker, bool Swizzle>
static void reshapeImagePacked(uint8_t* UTILS_RESTRICT dest, const uint8_t* UTILS_RESTRICT src,
size_t srcBytesPerRow, size_t srcChannelCount, size_t dstRowOffset, size_t dstColumnOffset,
size_t dstBytesPerRow, size_t dstChannelCount, size_t width, size_t height, bool /*swizzle*/) {
dest += (dstRowOffset * dstBytesPerRow);
const dstComponentType dstMaxValue = getMaxValue<dstComponentType>();
for (size_t row = 0; row < height; ++row) {
const uint8_t* inPtr = src;
dstComponentType* out = (dstComponentType*) dest + (dstColumnOffset * dstChannelCount);
for (size_t column = 0; column < width; ++column) {
float rgba[4] = {0.0f, 0.0f, 0.0f, 1.0f};
Unpacker::unpack(inPtr, rgba);
if constexpr (Swizzle) {
std::swap(rgba[0], rgba[2]);
}
for (size_t c = 0; c < dstChannelCount; ++c) {
if constexpr (std::is_same_v<dstComponentType, float>) {
out[c] = rgba[c];
} else if constexpr (std::is_same_v<dstComponentType, math::half>) {
out[c] = math::half(rgba[c]);
} else {
out[c] = static_cast<dstComponentType>(std::clamp(
static_cast<double>(rgba[c]) * static_cast<double>(dstMaxValue),
0.0,
static_cast<double>(std::numeric_limits<dstComponentType>::max())
));
}
}
inPtr += 4;
out += dstChannelCount;
}
src += srcBytesPerRow;
dest += dstBytesPerRow;
}
}
} // anonymous namespace
class DataReshaper {
public:
@@ -76,51 +224,9 @@ public:
}
}
// Converts a n-channel image of UBYTE, INT, UINT, or FLOAT to a different type.
template<typename dstComponentType, typename srcComponentType>
static void reshapeImage(uint8_t* UTILS_RESTRICT dest, const uint8_t* UTILS_RESTRICT src,
size_t srcBytesPerRow,
size_t srcChannelCount,
size_t dstRowOffset, size_t dstColumnOffset,
size_t dstBytesPerRow, size_t dstChannelCount,
size_t width, size_t height, bool swizzle) {
// TODO: there's a fast-path where memcpy will work but currently not being taken advantage
// of.
const dstComponentType dstMaxValue = getMaxValue<dstComponentType>();
const srcComponentType srcMaxValue = getMaxValue<srcComponentType>();
const size_t minChannelCount = math::min(srcChannelCount, dstChannelCount);
assert_invariant(minChannelCount <= 4);
UTILS_ASSUME(minChannelCount <= 4);
dest += (dstRowOffset * dstBytesPerRow);
const int inds[4] = { swizzle ? 2 : 0, 1, swizzle ? 0 : 2, 3 };
for (size_t row = 0; row < height; ++row) {
const srcComponentType* in = (const srcComponentType*) src;
dstComponentType* out = (dstComponentType*)dest + (dstColumnOffset * dstChannelCount);
for (size_t column = 0; column < width; ++column) {
for (size_t channel = 0; channel < minChannelCount; ++channel) {
if constexpr (std::is_same_v<dstComponentType, srcComponentType>) {
out[channel] = in[inds[channel]];
} else {
// FIXME: beware of overflows in the multiply
// FIXME: probably not correct for _INTEGER src/dst
out[channel] = in[inds[channel]] * dstMaxValue / srcMaxValue;
}
}
for (size_t channel = srcChannelCount; channel < dstChannelCount; ++channel) {
out[channel] = dstMaxValue;
}
in += srcChannelCount;
out += dstChannelCount;
}
src += srcBytesPerRow;
dest += dstBytesPerRow;
}
}
// Converts a n-channel image of UBYTE, INT, UINT, or FLOAT to a different type.
static bool reshapeImage(PixelBufferDescriptor* UTILS_RESTRICT dst, PixelDataType srcType,
uint32_t srcChannelCount, const uint8_t* UTILS_RESTRICT srcBytes, int srcBytesPerRow,
uint32_t srcChannelCount, const uint8_t* UTILS_RESTRICT srcBytes, int srcBytesPerRow,
int width, int height, bool swizzle) {
size_t dstChannelCount;
switch (dst->format) {
@@ -132,87 +238,123 @@ public:
case PixelDataFormat::RG: dstChannelCount = 2; break;
case PixelDataFormat::RGB: dstChannelCount = 3; break;
case PixelDataFormat::RGBA: dstChannelCount = 4; break;
default: return false;
default:
LOG(ERROR) << "DataReshaper: unsupported dst->format: " << (int) dst->format;
return false;
}
void (*reshaper)(uint8_t* dest, const uint8_t* src, size_t srcBytesPerRow,
size_t srcChannelCount,
size_t srcRowOffset, size_t srcColumnOffset,
size_t dstBytesPerRow, size_t dstChannelCount,
size_t width, size_t height, bool swizzle) = nullptr;
size_t srcChannelCount, size_t srcRowOffset, size_t srcColumnOffset,
size_t dstBytesPerRow, size_t dstChannelCount, size_t width, size_t height,
bool swizzle) = nullptr;
constexpr auto UBYTE = PixelDataType::UBYTE;
constexpr auto FLOAT = PixelDataType::FLOAT;
constexpr auto UINT = PixelDataType::UINT;
constexpr auto INT = PixelDataType::INT;
constexpr auto HALF = PixelDataType::HALF;
constexpr auto UINT_10F_11F_11F_REV = PixelDataType::UINT_10F_11F_11F_REV;
switch (dst->type) {
case UBYTE:
switch (srcType) {
case UBYTE:
reshaper = reshapeImage<uint8_t, uint8_t>;
reshaper = reshapeImageImpl<uint8_t, uint8_t>;
if (dst->format == PixelDataFormat::RGBA &&
dstChannelCount == srcChannelCount && !swizzle && dst->top == 0 &&
dst->left == 0) {
reshaper = copyImage;
}
break;
case FLOAT: reshaper = reshapeImage<uint8_t, float>; break;
case INT: reshaper = reshapeImage<uint8_t, int32_t>; break;
case UINT: reshaper = reshapeImage<uint8_t, uint32_t>; break;
default: return false;
case FLOAT: reshaper = reshapeImageImpl<uint8_t, float>; break;
case INT: reshaper = reshapeImageImpl<uint8_t, int32_t>; break;
case UINT: reshaper = reshapeImageImpl<uint8_t, uint32_t>; break;
case HALF: reshaper = reshapeImageImpl<uint8_t, math::half>; break;
case UINT_10F_11F_11F_REV:
if (swizzle) reshaper = reshapeImagePacked<uint8_t, UnpackerR11G11B10, true>;
else reshaper = reshapeImagePacked<uint8_t, UnpackerR11G11B10, false>;
break;
default:
LOG(ERROR) << "DataReshaper: UBYTE dst, unsupported srcType: "
<< (int) srcType;
return false;
}
break;
case FLOAT:
switch (srcType) {
case UBYTE: reshaper = reshapeImage<float, uint8_t>; break;
case FLOAT: reshaper = reshapeImage<float, float>; break;
case INT: reshaper = reshapeImage<float, int32_t>; break;
case UINT: reshaper = reshapeImage<float, uint32_t>; break;
default: return false;
case UBYTE: reshaper = reshapeImageImpl<float, uint8_t>; break;
case FLOAT: reshaper = reshapeImageImpl<float, float>; break;
case INT: reshaper = reshapeImageImpl<float, int32_t>; break;
case UINT: reshaper = reshapeImageImpl<float, uint32_t>; break;
case UINT_10F_11F_11F_REV:
if (swizzle) reshaper = reshapeImagePacked<float, UnpackerR11G11B10, true>;
else reshaper = reshapeImagePacked<float, UnpackerR11G11B10, false>;
break;
default:
LOG(ERROR) << "DataReshaper: FLOAT dst, unsupported srcType: "
<< (int) srcType;
return false;
}
break;
case INT:
switch (srcType) {
case UBYTE: reshaper = reshapeImage<int32_t, uint8_t>; break;
case FLOAT: reshaper = reshapeImage<int32_t, float>; break;
case INT: reshaper = reshapeImage<int32_t, int32_t>; break;
case UINT: reshaper = reshapeImage<int32_t, uint32_t>; break;
default: return false;
case UBYTE: reshaper = reshapeImageImpl<int32_t, uint8_t>; break;
case FLOAT: reshaper = reshapeImageImpl<int32_t, float>; break;
case INT: reshaper = reshapeImageImpl<int32_t, int32_t>; break;
case UINT: reshaper = reshapeImageImpl<int32_t, uint32_t>; break;
case UINT_10F_11F_11F_REV:
if (swizzle) reshaper = reshapeImagePacked<int32_t, UnpackerR11G11B10, true>;
else reshaper = reshapeImagePacked<int32_t, UnpackerR11G11B10, false>;
break;
default:
LOG(ERROR)
<< "DataReshaper: INT dst, unsupported srcType: " << (int) srcType;
return false;
}
break;
case UINT:
switch (srcType) {
case UBYTE: reshaper = reshapeImage<uint32_t, uint8_t>; break;
case FLOAT: reshaper = reshapeImage<uint32_t, float>; break;
case INT: reshaper = reshapeImage<uint32_t, int32_t>; break;
case UINT: reshaper = reshapeImage<uint32_t, uint32_t>; break;
default: return false;
case UBYTE: reshaper = reshapeImageImpl<uint32_t, uint8_t>; break;
case FLOAT: reshaper = reshapeImageImpl<uint32_t, float>; break;
case INT: reshaper = reshapeImageImpl<uint32_t, int32_t>; break;
case UINT: reshaper = reshapeImageImpl<uint32_t, uint32_t>; break;
case UINT_10F_11F_11F_REV:
if (swizzle) reshaper = reshapeImagePacked<uint32_t, UnpackerR11G11B10, true>;
else reshaper = reshapeImagePacked<uint32_t, UnpackerR11G11B10, false>;
break;
default:
LOG(ERROR)
<< "DataReshaper: UINT dst, unsupported srcType: " << (int) srcType;
return false;
}
break;
case HALF:
switch (srcType) {
case HALF: reshaper = copyImage; break;
default: return false;
case HALF:
reshaper = copyImage;
break;
case UINT_10F_11F_11F_REV:
if (swizzle) reshaper = reshapeImagePacked<math::half, UnpackerR11G11B10, true>;
else reshaper = reshapeImagePacked<math::half, UnpackerR11G11B10, false>;
break;
default:
LOG(ERROR)
<< "DataReshaper: HALF dst, unsupported srcType: " << (int) srcType;
return false;
}
break;
default:
LOG(ERROR) << "DataReshaper: unsupported dst->type: " << (int) dst->type;
return false;
}
uint8_t* dstBytes = (uint8_t*) dst->buffer;
const int dstBytesPerRow = PixelBufferDescriptor::computeDataSize(dst->format, dst->type,
dst->stride ? dst->stride : width, 1, dst->alignment);
reshaper(dstBytes, srcBytes, srcBytesPerRow, srcChannelCount,
dst->top, dst->left, dstBytesPerRow,
dstChannelCount, width, height, swizzle);
reshaper(dstBytes, srcBytes, srcBytesPerRow, srcChannelCount, dst->top, dst->left,
dstBytesPerRow, dstChannelCount, width, height, swizzle);
return true;
}
};
template<> inline float getMaxValue() { return 1.0f; }
template<> inline int32_t getMaxValue() { return 0x7fffffff; }
template<> inline uint32_t getMaxValue() { return 0xffffffff; }
template<> inline uint16_t getMaxValue() { return 0x3c00; } // 0x3c00 is 1.0 in half-float.
template<> inline uint8_t getMaxValue() { return 0xff; }
} // namespace backend
} // namespace filament

View File

@@ -35,6 +35,8 @@
#include <vector>
#include <deque>
@protocol MTLTexture;
namespace filament {
namespace backend {
@@ -160,6 +162,9 @@ private:
void enumerateBoundBuffers(BufferObjectBinding bindingType,
const std::function<void(const BufferState&, MetalBuffer*, uint32_t)>& f);
void readTextureCommon(id<MTLTexture> srcTexture, uint8_t level, uint16_t layer, uint32_t x,
uint32_t y, uint32_t width, uint32_t height, PixelBufferDescriptor&& data);
backend::StereoscopicType const mStereoscopicType;
backend::AsynchronousMode const mAsynchronousMode;
};

View File

@@ -1751,12 +1751,34 @@ void MetalDriver::readPixels(Handle<HwRenderTarget> src, uint32_t x, uint32_t y,
MetalAttachment color = srcTarget->getDrawColorAttachment(0);
id<MTLTexture> srcTexture = color.getTexture();
size_t miplevel = color.getLevel();
size_t layer = color.getLayer();
// Clamp height and width to actual texture's height and width
MTLSize srcTextureSize = MTLSizeMake(srcTexture.width >> miplevel, srcTexture.height >> miplevel, 1);
height = std::min(static_cast<uint32_t>(srcTextureSize.height), height);
width = std::min(static_cast<uint32_t>(srcTextureSize.width), width);
readTextureCommon(srcTexture, miplevel, layer, x, y, width, height, std::move(data));
}
void MetalDriver::readTexture(Handle<HwTexture> src, uint8_t level, uint16_t layer, uint32_t x,
uint32_t y, uint32_t width, uint32_t height, PixelBufferDescriptor&& p) {
FILAMENT_CHECK_PRECONDITION(!isInRenderPass(mContext))
<< "readTexture must be called outside of a render pass.";
auto srcTexture = handle_cast<MetalTexture>(src);
id<MTLTexture> texture = srcTexture->getMtlTextureForWrite();
// Clamp height and width to actual texture's height and width
MTLSize srcTextureSize = MTLSizeMake(texture.width >> level, texture.height >> level, 1);
height = std::min(static_cast<uint32_t>(srcTextureSize.height), height);
width = std::min(static_cast<uint32_t>(srcTextureSize.width), width);
readTextureCommon(texture, level, layer, x, y, width, height, std::move(p));
}
void MetalDriver::readTextureCommon(id<MTLTexture> srcTexture, uint8_t level, uint16_t layer,
uint32_t x, uint32_t y, uint32_t width, uint32_t height, PixelBufferDescriptor&& data) {
const MTLPixelFormat format = getMetalFormat(data.format, data.type);
FILAMENT_CHECK_PRECONDITION(format != MTLPixelFormatInvalid)
<< "The chosen combination of PixelDataFormat (" << (int)data.format
@@ -1764,10 +1786,14 @@ void MetalDriver::readPixels(Handle<HwRenderTarget> src, uint32_t x, uint32_t y,
<< ") is not supported for "
"readPixels.";
// Clamp height and width to actual texture's height and width
MTLSize levelSize = MTLSizeMake(std::max(1lu, srcTexture.width >> level),
std::max(1lu, srcTexture.height >> level), 1);
MTLTextureDescriptor* textureDescriptor =
[MTLTextureDescriptor texture2DDescriptorWithPixelFormat:format
width:srcTextureSize.width
height:srcTextureSize.height
width:levelSize.width
height:levelSize.height
mipmapped:NO];
#if defined(FILAMENT_IOS)
textureDescriptor.storageMode = MTLStorageModeShared;
@@ -1779,14 +1805,16 @@ void MetalDriver::readPixels(Handle<HwRenderTarget> src, uint32_t x, uint32_t y,
MetalBlitter::BlitArgs args{};
args.filter = SamplerMagFilter::NEAREST;
args.source.level = miplevel;
args.source.region = MTLRegionMake2D(0, 0, srcTexture.width >> miplevel, srcTexture.height >> miplevel);
args.source.level = level;
args.source.slice = layer;
args.source.region = MTLRegionMake2D(0, 0, levelSize.width, levelSize.height);
args.source.texture = srcTexture;
args.destination.level = 0;
args.destination.slice = 0;
args.destination.region = MTLRegionMake2D(0, 0, readPixelsTexture.width, readPixelsTexture.height);
args.destination.texture = readPixelsTexture;
mContext->blitter->blit(getPendingCommandBuffer(mContext), args, "readPixels blit");
mContext->blitter->blit(getPendingCommandBuffer(mContext), args, "readTexture blit");
#if !defined(FILAMENT_IOS)
// Managed textures on macOS require explicit synchronization between GPU / CPU.
@@ -1814,12 +1842,6 @@ void MetalDriver::readPixels(Handle<HwRenderTarget> src, uint32_t x, uint32_t y,
}];
}
void MetalDriver::readTexture(Handle<HwTexture> src, uint8_t level, uint16_t layer, uint32_t x,
uint32_t y, uint32_t width, uint32_t height, PixelBufferDescriptor&& p) {
// TODO: implement readTexture
scheduleDestroy(std::move(p));
}
void MetalDriver::readBufferSubData(backend::BufferObjectHandle boh,
uint32_t offset, uint32_t size, backend::BufferDescriptor&& p) {
// TODO: implement readBufferSubData

View File

@@ -1092,7 +1092,7 @@ MetalRenderTarget::MetalRenderTarget(MetalContext* context, uint32_t width, uint
// a multisampled sidecar texture and do a resolve automatically.
if (samples > 1 && texture->samples == 1) {
auto& sidecar = texture->msaaSidecar;
if (!sidecar) {
if (!sidecar || sidecar.sampleCount != samples) {
sidecar = createMultisampledTexture(mtlTexture.pixelFormat, texture->width,
texture->height, samples);
}
@@ -1123,7 +1123,7 @@ MetalRenderTarget::MetalRenderTarget(MetalContext* context, uint32_t width, uint
// a multisampled sidecar texture and do a resolve automatically.
if (samples > 1 && texture->samples == 1) {
auto& sidecar = texture->msaaSidecar;
if (!sidecar) {
if (!sidecar || sidecar.sampleCount != samples) {
sidecar = createMultisampledTexture(mtlTexture.pixelFormat, texture->width,
texture->height, samples);
}
@@ -1155,7 +1155,7 @@ MetalRenderTarget::MetalRenderTarget(MetalContext* context, uint32_t width, uint
// a multisampled sidecar texture and do a resolve automatically.
if (samples > 1 && texture->samples == 1) {
auto& sidecar = texture->msaaSidecar;
if (!sidecar) {
if (!sidecar || sidecar.sampleCount != samples) {
sidecar = createMultisampledTexture(mtlTexture.pixelFormat, texture->width,
texture->height, samples);
}

View File

@@ -1912,7 +1912,9 @@ void OpenGLDriver::createDefaultRenderTargetR(
rt->gl.isDefault = true;
rt->gl.fbo = 0; // the actual id is resolved at binding time
rt->gl.samples = 1;
// FIXME: these flags should reflect the actual attachments present
// for the default render target, the attachments (i.e. targets) are unknown until the swapChain is bound
// (via OpenGLPlatform::makeCurrent()). Here we initialize the field with some reasonable defaults, but
// these will be ignored in begin/endRenderPass()
rt->targets = TargetBufferFlags::COLOR0 | TargetBufferFlags::DEPTH;
mHandleAllocator.associateTagToHandle(rth.getId(), std::move(tag));
}
@@ -2104,6 +2106,13 @@ void OpenGLDriver::createSwapChainR(Handle<HwSwapChain> sch, void* nativeWindow,
GLSwapChain* sc = handle_cast<GLSwapChain*>(sch);
sc->swapChain = mPlatform.createSwapChain(nativeWindow, flags);
// TODO: This is a bit fragile, instead we should ask the SwapChain for its actual attachments.
// But this requires an API change in the platform. So we can do that later if needed.
sc->attachments = TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH;
if (flags & SWAP_CHAIN_CONFIG_HAS_STENCIL_BUFFER) {
sc->attachments |= TargetBufferFlags::STENCIL;
}
#if !defined(__EMSCRIPTEN__)
// note: in practice this should never happen on Android
FILAMENT_CHECK_POSTCONDITION(sc->swapChain) << "createSwapChain(" << nativeWindow << ", "
@@ -2126,6 +2135,13 @@ void OpenGLDriver::createSwapChainHeadlessR(Handle<HwSwapChain> sch,
GLSwapChain* sc = handle_cast<GLSwapChain*>(sch);
sc->swapChain = mPlatform.createSwapChain(width, height, flags);
// TODO: This is a bit fragile, instead we should ask the SwapChain for its actual attachments.
// But this requires an API change in the platform. So we can do that later if needed.
sc->attachments = TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH;
if (flags & SWAP_CHAIN_CONFIG_HAS_STENCIL_BUFFER) {
sc->attachments |= TargetBufferFlags::STENCIL;
}
#if !defined(__EMSCRIPTEN__)
// note: in practice this should never happen on Android
FILAMENT_CHECK_POSTCONDITION(sc->swapChain)
@@ -3607,6 +3623,8 @@ void OpenGLDriver::detachStream(GLTexture* t) noexcept {
case StreamType::NATIVE:
mPlatform.detach(t->hwStream->stream);
// ^ this deletes the texture id
// We still need to call unbind to update the bookkeeping.
gl.unbindTexture(t->gl.target, t->gl.id);
break;
case StreamType::ACQUIRED:
gl.unbindTexture(t->gl.target, t->gl.id);
@@ -3702,8 +3720,10 @@ void OpenGLDriver::beginRenderPass(Handle<HwRenderTarget> rth,
assert_invariant(!rt->gl.isDefault || mCurrentDrawSwapChain);
mRec709OutputColorspace = rt->gl.isDefault ? mCurrentDrawSwapChain->rec709 : false;
const TargetBufferFlags clearFlags = params.flags.clear & rt->targets;
TargetBufferFlags discardFlags = params.flags.discardStart & rt->targets;
// for the default renderTarget the attachments come from the current swapChain
TargetBufferFlags const rtAttachments = rt->gl.isDefault ? mCurrentDrawSwapChain->attachments : rt->targets;
TargetBufferFlags const clearFlags = params.flags.clear & rtAttachments;
TargetBufferFlags discardFlags = params.flags.discardStart & rtAttachments;
GLuint const fbo = gl.bindFramebuffer(GL_FRAMEBUFFER, rt->gl.fbo);
CHECK_GL_FRAMEBUFFER_STATUS(GL_FRAMEBUFFER)
@@ -3768,7 +3788,8 @@ void OpenGLDriver::endRenderPass(int) {
GLRenderTarget const* const rt = handle_cast<GLRenderTarget*>(mRenderPassTarget);
TargetBufferFlags discardFlags = mRenderPassParams.flags.discardEnd & rt->targets;
TargetBufferFlags const rtAttachments = rt->gl.isDefault ? mCurrentDrawSwapChain->attachments : rt->targets;
TargetBufferFlags discardFlags = mRenderPassParams.flags.discardEnd & rtAttachments;
if (rt->gl.fbo_read) {
resolvePass(ResolveAction::STORE, rt, discardFlags);
}

View File

@@ -101,6 +101,7 @@ public:
struct GLSwapChain : public HwSwapChain {
using HwSwapChain::HwSwapChain;
TargetBufferFlags attachments{};
bool rec709 = false;
struct {
CallbackHandler* handler = nullptr;

View File

@@ -120,33 +120,27 @@ bool PlatformEGL::isOpenGL() const noexcept {
PlatformEGL::ExternalImageEGL::~ExternalImageEGL() = default;
Driver* PlatformEGL::createDriver(void* sharedContext, const DriverConfig& driverConfig) {
static constexpr int kMaxNumEGLDevices = 32;
mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
assert_invariant(mEGLDisplay != EGL_NO_DISPLAY);
EGLint major, minor;
EGLBoolean initialized = false;
EGLBoolean initialized = eglInitialize(mEGLDisplay, &major, &minor);
PFNEGLQUERYDEVICESEXTPROC const eglQueryDevicesEXT =
PFNEGLQUERYDEVICESEXTPROC(eglGetProcAddress("eglQueryDevicesEXT"));
PFNEGLGETPLATFORMDISPLAYEXTPROC const getPlatformDisplay =
PFNEGLGETPLATFORMDISPLAYEXTPROC(eglGetProcAddress("eglGetPlatformDisplay"));
if (eglQueryDevicesEXT != nullptr && getPlatformDisplay != nullptr) {
EGLint numDevices = 0;
EGLDeviceEXT eglDevices[kMaxNumEGLDevices];
if (eglQueryDevicesEXT(kMaxNumEGLDevices, eglDevices, &numDevices)) {
for (int i = 0; i < numDevices && !initialized; ++i) {
mEGLDisplay = getPlatformDisplay(EGL_PLATFORM_DEVICE_EXT, eglDevices[i], nullptr);
if (!initialized) {
EGLDeviceEXT eglDevice;
EGLint numDevices;
PFNEGLQUERYDEVICESEXTPROC const eglQueryDevicesEXT =
PFNEGLQUERYDEVICESEXTPROC(eglGetProcAddress("eglQueryDevicesEXT"));
if (eglQueryDevicesEXT != nullptr) {
eglQueryDevicesEXT(1, &eglDevice, &numDevices);
if(auto* getPlatformDisplay = reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(
eglGetProcAddress("eglGetPlatformDisplay"))) {
mEGLDisplay = getPlatformDisplay(EGL_PLATFORM_DEVICE_EXT, eglDevice, nullptr);
initialized = eglInitialize(mEGLDisplay, &major, &minor);
}
}
}
if (!initialized) {
mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
assert_invariant(mEGLDisplay != EGL_NO_DISPLAY);
initialized = eglInitialize(mEGLDisplay, &major, &minor);
}
if (UTILS_UNLIKELY(!initialized)) {
LOG(ERROR) << "eglInitialize failed";
return nullptr;

View File

@@ -289,8 +289,7 @@ void VulkanDescriptorSetCache::unbind(uint8_t setIndex) {
}
void VulkanDescriptorSetCache::commit(VulkanCommandBuffer* commands,
VkPipelineLayout pipelineLayout, fvkutils::DescriptorSetMask const& useExternalSamplers,
fvkutils::DescriptorSetMask const& setMask) {
VkPipelineLayout pipelineLayout, fvkutils::DescriptorSetMask const& setMask) {
// setMask indicates the set of descriptor sets the driver wants to bind, curMask is the
// actual set of sets that *needs* to be bound.
fvkutils::DescriptorSetMask curMask = setMask;
@@ -306,8 +305,7 @@ void VulkanDescriptorSetCache::commit(VulkanCommandBuffer* commands,
auto& lastBoundSets = mLastBoundInfo.boundSets;
curMask.forEachSetBit([&](size_t index) {
auto& set = updateSets[index];
if (set == lastBoundSets[index] && !useExternalSamplers[index] &&
set->uniqueDynamicUboCount == 0) {
if (set == lastBoundSets[index] && set->uniqueDynamicUboCount == 0) {
curMask.unset(index);
}
});
@@ -357,17 +355,18 @@ void VulkanDescriptorSetCache::updateBuffer(fvkmemory::resource_ptr<VulkanDescri
}
void VulkanDescriptorSetCache::updateSampler(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
uint8_t binding, fvkmemory::resource_ptr<VulkanTexture> texture,
VkSampler sampler, VkDescriptorSetLayout externalSamplerLayout) noexcept {
uint8_t binding, fvkmemory::resource_ptr<VulkanTexture> texture, VkSampler sampler,
VkDescriptorSetLayout externalSamplerLayout) noexcept {
// We have to update a bound set for two use cases
// - streaming API (a changing feed of AHardwareBuffer)
// - external samplers - potential changing of dataspace per-frame
if (UTILS_UNLIKELY(set->isBound())) {
// TODO: Fix the stream flow case base on the above comment!!
if (set->isAnExternalSamplerBound) {
auto layout = set->getLayout();
// Build a new descriptor set from the new layout
auto genLayout = externalSamplerLayout != VK_NULL_HANDLE ?
externalSamplerLayout : layout->getVkLayout();
VkDescriptorSetLayout const genLayout = set->boundLayout;
VkDescriptorSet const newSet = getVkSet(layout->count, genLayout);
Bitmask const ubo = layout->bitmask.ubo | layout->bitmask.dynamicUbo;
Bitmask samplers = layout->bitmask.sampler;

View File

@@ -71,7 +71,6 @@ public:
void unbind(uint8_t setIndex);
void commit(VulkanCommandBuffer* commands, VkPipelineLayout pipelineLayout,
fvkutils::DescriptorSetMask const& useExternalSamplerMask,
fvkutils::DescriptorSetMask const& setMask);
fvkmemory::resource_ptr<VulkanDescriptorSet> createSet(Handle<HwDescriptorSet> handle,

View File

@@ -268,7 +268,7 @@ VulkanDriver::VulkanDriver(VulkanPlatform* platform, VulkanContext& context,
mDescriptorSetLayoutCache(mPlatform->getDevice(), &mResourceManager),
mDescriptorSetCache(mPlatform->getDevice(), &mResourceManager),
mQueryManager(mPlatform->getDevice()),
mExternalImageManager(platform, &mSamplerCache, &mYcbcrConversionCache, &mDescriptorSetCache,
mExternalImageManager(&mSamplerCache, &mYcbcrConversionCache, &mDescriptorSetCache,
&mDescriptorSetLayoutCache),
mStreamedImageManager(&mExternalImageManager),
mIsSRGBSwapChainSupported(mPlatform->getCustomization().isSRGBSwapChainSupported),
@@ -465,10 +465,6 @@ void VulkanDriver::beginFrame(int64_t monotonic_clock_ns,
//
// This will let us check if any VulkanBuffer is currently in flight or not.
mCommands.gc();
if (mAppState.hasExternalSamplers()) {
mExternalImageManager.onBeginFrame();
}
}
void VulkanDriver::setFrameScheduledCallback(Handle<HwSwapChain> sch, CallbackHandler* handler,
@@ -514,8 +510,12 @@ void VulkanDriver::updateDescriptorSetTexture(
if (UTILS_UNLIKELY(mExternalImageManager.isExternallySampledTexture(texture))) {
mExternalImageManager.bindExternallySampledTexture(set, binding, texture, params);
mAppState.hasBoundExternalImages = true;
set->isAnExternalSamplerBound = true;
set->isLayoutDirty = true;
} else if (bool(texture->getStream())) {
mStreamedImageManager.bindStreamedTexture(set, binding, texture, params);
// TODO: Fix the stream flow!! In this case the binded image doesnt have to be one
// with an external sampler.
mAppState.hasBoundExternalImages = true;
} else {
VulkanSamplerCache::Params cacheParams = {
@@ -768,7 +768,7 @@ void VulkanDriver::createTextureExternalImage2R(Handle<HwTexture> th, backend::S
texture->transitionLayout(&commands, texture->getPrimaryViewRange(), VulkanLayout::FRAG_READ);
if (imgData.external.valid()) {
mExternalImageManager.addExternallySampledTexture(texture, externalImage);
mExternalImageManager.addExternallySampledTexture(texture, conversion);
}
texture.inc();
@@ -902,6 +902,7 @@ void VulkanDriver::destroyProgram(Handle<HwProgram> ph) {
return;
}
auto vprogram = resource_ptr<VulkanProgram>::cast(&mResourceManager, ph);
vprogram->cancelParallelCompilation();
vprogram.dec();
}
@@ -1333,7 +1334,7 @@ void VulkanDriver::destroyDescriptorSet(Handle<HwDescriptorSet> dsh) {
auto set = resource_ptr<VulkanDescriptorSet>::cast(&mResourceManager, dsh);
set.dec();
if (mAppState.hasExternalSamplers()) {
if (set->isAnExternalSamplerBound) {
mExternalImageManager.removeDescriptorSet(set);
}
}
@@ -1429,7 +1430,7 @@ void VulkanDriver::updateStreams(CommandStream* driver) {
if (imgData.external.valid()) {
mExternalImageManager.addExternallySampledTexture(newTexture,
externalImage);
conversion);
// Cache the AHB backed image. Acquires the image here.
s->pushImage(image, newTexture);
}
@@ -2480,6 +2481,7 @@ void VulkanDriver::bindPipelineImpl(PipelineState const& pipelineState,
// Push state changes to the VulkanPipelineCache instance. This is fast and does not make VK calls.
mPipelineCache.bindProgram(program);
mPipelineCache.bindRasterState(vulkanRasterState);
mPipelineCache.bindStencilState(pipelineState.stencilState);
mPipelineCache.bindPrimitiveTopology(topology);
mPipelineCache.bindVertexArray(attribDesc, bufferDesc, vbi->getAttributeCount());
@@ -2523,16 +2525,24 @@ void VulkanDriver::bindDescriptorSet(
backend::DescriptorSetOffsetArray&& offsets) {
if (dsh) {
auto set = resource_ptr<VulkanDescriptorSet>::cast(&mResourceManager, dsh);
// If the set has binded texture that requires an immutable sampler,
// a new DescriptorSetLayout must be created otherwise use the default layout.
if (set->isLayoutDirty) {
mExternalImageManager.updateSetAndLayout(set);
set->isLayoutDirty = false;
}
mDescriptorSetCache.bind(setIndex, set, std::move(offsets));
if (mAppState.hasExternalSamplers()) {
if (set->isAnExternalSamplerBound) {
auto const& bindInDrawBundle = mPipelineState.bindInDraw.second;
// The set index being bound has already been bound or will be bound. If it's already
// been bound and this set has external samplers, we do the doBindindraw block in
// draw2() again. Because this set might potentially cause a new pipelineLayout
// (therefore pipeline) to be bound.
if (bindInDrawBundle.descriptorSetMask[setIndex] &&
mExternalImageManager.hasExternalSampler(set)) {
if (bindInDrawBundle.descriptorSetMask[setIndex]) {
// TODO: Only do bindInDraw if the previous bounded layout at `setIndex` is the
// different.
mPipelineState.bindInDraw.first = true;
}
}
@@ -2548,21 +2558,20 @@ void VulkanDriver::draw2(uint32_t indexOffset, uint32_t indexCount, uint32_t ins
fvkutils::DescriptorSetMask setsWithExternalSamplers = {};
if (doBindInDraw) {
auto& layoutHandles = bundle.dsLayoutHandles;
setsWithExternalSamplers = mExternalImageManager.prepareBindSets(layoutHandles,
mDescriptorSetCache.getBoundSets());
// Create the new pipeline layout from the current bounded descriptor sets.
// The layout of the descriptor sets at this point should have the final one taking into account
// the external samplers.
//
// The final layouts of the descriptor sets are regenerated as needed at `bindDescriptorSet`.
VulkanDescriptorSetCache::DescriptorSetArray const& boundSets = mDescriptorSetCache.getBoundSets();
VulkanDescriptorSetLayout::DescriptorSetLayoutArray vklayouts;
for (size_t i = 0; i < layoutHandles.size(); i++) {
if (!layoutHandles[i]) {
for (size_t i = 0; i < boundSets.size(); i++) {
if (!boundSets[i]) {
vklayouts[i] = VK_NULL_HANDLE;
continue;
}
if (setsWithExternalSamplers[i]) {
vklayouts[i] = layoutHandles[i]->getExternalSamplerVkLayout();
} else {
vklayouts[i] = layoutHandles[i]->getVkLayout();
}
vklayouts[i] = boundSets[i]->boundLayout;
}
auto program =
resource_ptr<VulkanProgram>::cast(&mResourceManager, bundle.pipelineState.program);
@@ -2578,7 +2587,7 @@ void VulkanDriver::draw2(uint32_t indexOffset, uint32_t indexCount, uint32_t ins
mPipelineState.bindInDraw.first = false;
}
mDescriptorSetCache.commit(mCurrentRenderPass.commandBuffer, mPipelineState.pipelineLayout,
setsWithExternalSamplers, mPipelineState.descriptorSetMask);
mPipelineState.descriptorSetMask);
// Finally, make the actual draw call. TODO: support subranges
uint32_t const firstIndex = indexOffset;

View File

@@ -49,15 +49,13 @@ ImageData& findImage(std::vector<ImageData>& images,
}// namespace
VulkanExternalImageManager::VulkanExternalImageManager(VulkanPlatform* platform,
VulkanSamplerCache* samplerCache, VulkanYcbcrConversionCache* ycbcrConversionCache,
VulkanDescriptorSetCache* setCache, VulkanDescriptorSetLayoutCache* layoutCache)
: mPlatform(platform),
mSamplerCache(samplerCache),
mYcbcrConversionCache(ycbcrConversionCache),
mDescriptorSetCache(setCache),
mDescriptorSetLayoutCache(layoutCache) {
}
VulkanExternalImageManager::VulkanExternalImageManager(VulkanSamplerCache* samplerCache,
VulkanYcbcrConversionCache* ycbcrConversionCache, VulkanDescriptorSetCache* setCache,
VulkanDescriptorSetLayoutCache* layoutCache)
: mSamplerCache(samplerCache),
mYcbcrConversionCache(ycbcrConversionCache),
mDescriptorSetCache(setCache),
mDescriptorSetLayoutCache(layoutCache) {}
VulkanExternalImageManager::~VulkanExternalImageManager() = default;
@@ -66,75 +64,21 @@ void VulkanExternalImageManager::terminate() {
mImages.clear();
}
void VulkanExternalImageManager::onBeginFrame() {
std::for_each(mImages.begin(), mImages.end(), [](ImageData& image) {
image.hasBeenValidated = false;
});
std::for_each(mSetBindings.begin(), mSetBindings.end(), [](SetBindingInfo& info) {
info.bound = false;
});
}
fvkutils::DescriptorSetMask VulkanExternalImageManager::prepareBindSets(LayoutArray const& layouts,
SetArray const& sets) {
fvkutils::DescriptorSetMask shouldUseExternalSampler{};
for (uint8_t i = 0; i < sets.size(); i++) {
auto set = sets[i];
auto layout = layouts[i];
if (!set || !layout) {
continue;
}
if (hasExternalSampler(set)) {
updateSetAndLayout(set, layout);
shouldUseExternalSampler.set(i);
}
}
return shouldUseExternalSampler;
}
bool VulkanExternalImageManager::hasExternalSampler(
fvkmemory::resource_ptr<VulkanDescriptorSet> set) const {
auto itr = std::find_if(mSetBindings.begin(), mSetBindings.end(),
[&](SetBindingInfo const& info) { return info.set == set; });
return itr != mSetBindings.end();
}
void VulkanExternalImageManager::updateSetAndLayout(
fvkmemory::resource_ptr<VulkanDescriptorSet> set,
fvkmemory::resource_ptr<VulkanDescriptorSetLayout> layout) {
fvkmemory::resource_ptr<VulkanDescriptorSet> set) {
utils::FixedCapacityVector<
std::tuple<uint8_t, VkSampler, fvkmemory::resource_ptr<VulkanTexture>>>
samplerAndBindings;
samplerAndBindings.reserve(MAX_SAMPLER_COUNT);
fvkutils::SamplerBitmask actualExternalSamplers;
for (auto& bindingInfo : mSetBindings) {
if (bindingInfo.set != set || bindingInfo.bound) {
for (auto& bindingInfo: mSetBindings) {
if (bindingInfo.set != set) {
continue;
}
auto& imageData = findImage(mImages, bindingInfo.image);
// For non YUV images (some ext images are NOT ext FMT)
// getVkSamplerYcbcrConversion(metadata) will return NULL, and image->conversion will be
// null
updateImage(&imageData);
auto samplerParams = bindingInfo.samplerParams;
// according to spec, these must match chromaFilter
// https://registry.khronos.org/vulkan/specs/latest/man/html/VkSamplerCreateInfo.html#VUID-VkSamplerCreateInfo-minFilter-01645
samplerParams.filterMag = SamplerMagFilter::NEAREST;
samplerParams.filterMin = SamplerMinFilter::NEAREST;
auto sampler = mSamplerCache->getSampler({
.sampler = samplerParams,
.conversion = imageData.conversion,
});
actualExternalSamplers.set(bindingInfo.binding);
samplerAndBindings.push_back({ bindingInfo.binding, sampler, bindingInfo.image });
bindingInfo.bound = true;
}
if (samplerAndBindings.empty()) {
return;
samplerAndBindings.push_back(
{ bindingInfo.binding, bindingInfo.sampler, bindingInfo.image });
}
// Sort by binding number
@@ -147,14 +91,14 @@ void VulkanExternalImageManager::updateSetAndLayout(
std::for_each(samplerAndBindings.begin(), samplerAndBindings.end(),
[&](auto const& b) { outSamplers.push_back({ static_cast<uint64_t>(std::get<0>(b)), std::get<1>(b) }); });
VkDescriptorSetLayout const newLayout = mDescriptorSetLayoutCache->getVkLayout(layout->bitmask,
fvkmemory::resource_ptr<VulkanDescriptorSetLayout> const& layout = set->getLayout();
set->boundLayout = mDescriptorSetLayoutCache->getVkLayout(layout->bitmask,
actualExternalSamplers, outSamplers);
layout->setExternalSamplerVkLayout(newLayout);
// Update the external samplers in the set
for (auto& [binding, sampler, image]: samplerAndBindings) {
// We cannot call updateSamplerForExternalSamplerSet because some samplers are non NULL
// (RGB) and we cannot do a combined update with a NULL sampler.
mDescriptorSetCache->updateSampler(set, binding, image, sampler, newLayout);
mDescriptorSetCache->updateSampler(set, binding, image, sampler, set->boundLayout);
}
}
@@ -184,23 +128,6 @@ VkSamplerYcbcrConversion VulkanExternalImageManager::getVkSamplerYcbcrConversion
return mYcbcrConversionCache->getConversion(ycbcrParams);
}
void VulkanExternalImageManager::updateImage(ImageData* image) {
if (image->hasBeenValidated) {
return;
}
image->hasBeenValidated = true;
auto metadata = mPlatform->extractExternalImageMetadata(image->platformHandle);
auto vkYcbcr = getVkSamplerYcbcrConversion(metadata);
if (vkYcbcr == image->conversion) {
return;
}
image->image->setYcbcrConversion(vkYcbcr);
image->conversion = vkYcbcr;
return;
}
void VulkanExternalImageManager::removeDescriptorSet(
fvkmemory::resource_ptr<VulkanDescriptorSet> inSet) {
erasep<SetBindingInfo>(mSetBindings,
@@ -212,25 +139,32 @@ void VulkanExternalImageManager::bindExternallySampledTexture(
fvkmemory::resource_ptr<VulkanTexture> image, SamplerParams samplerParams) {
// Should we do duplicate validation here?
auto& imageData = findImage(mImages, image);
auto itr = std::find_if(mSetBindings.begin(), mSetBindings.end(),
[&](SetBindingInfo const& binding) {
return (binding.set == set && binding.binding == bindingPoint);
});
if (itr == mSetBindings.end()) {
mSetBindings.push_back({ bindingPoint, imageData.image, set, samplerParams });
} else {
// override the image data in the binding point
itr->image = image;
itr->samplerParams = samplerParams;
}
// according to spec, these must match chromaFilter
// https://registry.khronos.org/vulkan/specs/latest/man/html/VkSamplerCreateInfo.html#VUID-VkSamplerCreateInfo-minFilter-01645
samplerParams.filterMag = SamplerMagFilter::NEAREST;
samplerParams.filterMin = SamplerMinFilter::NEAREST;
// If the sampler has a ycbcrConversion then anisotropic must be disabled and addressModeU,
// addressModeV and addressModeW must be VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE.
// https://docs.vulkan.org/spec/latest/chapters/samplers.html#VUID-VkSamplerCreateInfo-addressModeU-01646
samplerParams.wrapS = SamplerWrapMode::CLAMP_TO_EDGE;
samplerParams.wrapT = SamplerWrapMode::CLAMP_TO_EDGE;
samplerParams.wrapR = SamplerWrapMode::CLAMP_TO_EDGE;
samplerParams.anisotropyLog2 = 0;
VkSampler const sampler = mSamplerCache->getSampler({
.sampler = samplerParams,
.conversion = imageData.conversion,
});
mSetBindings.push_back({ bindingPoint, imageData.image, set, sampler });
}
void VulkanExternalImageManager::addExternallySampledTexture(
fvkmemory::resource_ptr<VulkanTexture> image,
Platform::ExternalImageHandleRef platformHandleRef) {
// By passing VK_NULL_HANDLE which is already there by default.
// We make it clear that all default images do NOT have a chroma conversion.
mImages.push_back({ image, platformHandleRef, false, VK_NULL_HANDLE });
fvkmemory::resource_ptr<VulkanTexture> image, VkSamplerYcbcrConversion const conversion) {
mImages.push_back({
.image = image,
.conversion = conversion,
});
}
void VulkanExternalImageManager::removeExternallySampledTexture(

View File

@@ -36,7 +36,6 @@ class VulkanExternalImageManager {
public:
VulkanExternalImageManager(
VulkanPlatform* platform,
VulkanSamplerCache* samplerCache,
VulkanYcbcrConversionCache* ycbcrConversionCache,
VulkanDescriptorSetCache* setCache,
@@ -46,8 +45,6 @@ public:
void terminate();
void onBeginFrame();
using SetArray = std::array<fvkmemory::resource_ptr<VulkanDescriptorSet>,
VulkanDescriptorSetLayout::UNIQUE_DESCRIPTOR_SET_COUNT>;
@@ -56,10 +53,6 @@ public:
using VkLayoutArray = VulkanDescriptorSetLayout::DescriptorSetLayoutArray;
// Returns bitmask to indicate whether or not to use the external sampler version of each
// descriptor set.
fvkutils::DescriptorSetMask prepareBindSets(LayoutArray const& layouts, SetArray const& sets);
void removeDescriptorSet(fvkmemory::resource_ptr<VulkanDescriptorSet> set);
void bindExternallySampledTexture(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
@@ -70,7 +63,7 @@ public:
uint8_t bindingPoint);
void addExternallySampledTexture(fvkmemory::resource_ptr<VulkanTexture> external,
Platform::ExternalImageHandleRef platformHandleRef);
VkSamplerYcbcrConversion const conversion);
void removeExternallySampledTexture(fvkmemory::resource_ptr<VulkanTexture> image);
@@ -81,20 +74,19 @@ public:
struct ImageData {
fvkmemory::resource_ptr<VulkanTexture> image;
Platform::ExternalImageHandle platformHandle;
bool hasBeenValidated = false; // indicates whether the image has been validated *this frame*
VkSamplerYcbcrConversion conversion = VK_NULL_HANDLE;
};
bool hasExternalSampler(fvkmemory::resource_ptr<VulkanDescriptorSet> set) const;
// - Update the descriptor set layout with the external samplers. This layout will be assign the
// respective immutable samplers to the layout.
// - Switch from binding the base layout to the new one.
// - Create a new descriptor set based on the new layout and copy the old descriptor set
// bindings that don't use external samplers.
// - Update the bindings that use external samplers.
void updateSetAndLayout(fvkmemory::resource_ptr<VulkanDescriptorSet> set);
private:
void updateSetAndLayout(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
fvkmemory::resource_ptr<VulkanDescriptorSetLayout> layout);
void updateImage(ImageData* imageData);
VulkanPlatform* mPlatform;
VulkanSamplerCache* mSamplerCache;
VulkanYcbcrConversionCache* mYcbcrConversionCache;
VulkanDescriptorSetCache* mDescriptorSetCache;
@@ -107,8 +99,7 @@ private:
uint8_t binding = 0;
fvkmemory::resource_ptr<VulkanTexture> image;
fvkmemory::resource_ptr<VulkanDescriptorSet> set;
SamplerParams samplerParams;
bool bound = false;
VkSampler sampler = VK_NULL_HANDLE;
};
// Use vectors instead of hash maps because we only expect small number of entries.

View File

@@ -376,21 +376,34 @@ void VulkanFboCache::gc() noexcept {
}
const uint32_t evictTime = mCurrentTime - TIME_BEFORE_EVICTION;
for (FboMap::iterator iter = mFramebufferCache.begin(); iter != mFramebufferCache.end(); ++iter) {
for (FboMap::iterator iter = mFramebufferCache.begin(); iter != mFramebufferCache.end(); ) {
const FboVal fbo = iter->second;
if (fbo.timestamp < evictTime && fbo.handle) {
mRenderPassRefCount[iter->first.renderPass]--;
vkDestroyFramebuffer(mDevice, fbo.handle, VKALLOC);
iter.value().handle = VK_NULL_HANDLE;
// erase(iterator) returns the iterator to the next element.
iter = mFramebufferCache.erase(iter);
} else {
++iter;
}
}
for (auto iter = mRenderPassCache.begin(); iter != mRenderPassCache.end(); ++iter) {
for (RenderPassMap::iterator iter = mRenderPassCache.begin(); iter != mRenderPassCache.end(); ) {
const VkRenderPass handle = iter->second.handle;
if (iter->second.timestamp < evictTime && handle && mRenderPassRefCount[handle] == 0) {
vkDestroyRenderPass(mDevice, handle, VKALLOC);
iter.value().handle = VK_NULL_HANDLE;
// erase(iterator) returns the iterator to the next element.
iter = mRenderPassCache.erase(iter);
mRenderPassRefCount.erase(handle);
} else {
++iter;
}
}
FVK_SYSTRACE_END();
}

View File

@@ -117,7 +117,8 @@ private:
using FboMap = tsl::robin_map<FboKey, FboVal, FboKeyHashFn, FboKeyEqualFn>;
FboMap mFramebufferCache;
tsl::robin_map<RenderPassKey, RenderPassVal, RenderPassHash, RenderPassEq> mRenderPassCache;
using RenderPassMap = tsl::robin_map<RenderPassKey, RenderPassVal, RenderPassHash, RenderPassEq>;
RenderPassMap mRenderPassCache;
tsl::robin_map<VkRenderPass, uint32_t> mRenderPassRefCount;
uint32_t mCurrentTime = 0;
};

View File

@@ -207,7 +207,8 @@ VulkanDescriptorSet::~VulkanDescriptorSet() {
VulkanDescriptorSet::VulkanDescriptorSet(fvkmemory::resource_ptr<VulkanDescriptorSetLayout> layout,
OnRecycle&& onRecycleFn, VkDescriptorSet vkSet)
: dynamicUboMask(layout->bitmask.dynamicUbo),
: boundLayout(layout->getVkLayout()),
dynamicUboMask(layout->bitmask.dynamicUbo),
uniqueDynamicUboCount(layout->count.dynamicUbo),
mLayout(layout),
mCurrentSetIndex(0) {

View File

@@ -194,10 +194,20 @@ public:
return bool(mSets[mCurrentSetIndex].fenceStatus);
}
// The current layout used by the descriptor set. This one will match the bindings, including
// external samplers data.
// This will not necessarilly be the same as `mLayout`.
VkDescriptorSetLayout boundLayout = VK_NULL_HANDLE;
fvkmemory::resource_ptr<VulkanDescriptorSetLayout> getLayout() const { return mLayout; }
fvkutils::UniformBufferBitmask const& dynamicUboMask;
uint8_t const uniqueDynamicUboCount;
// Flag to indicate if the current layout needs to be recreated or not.
// This should only set to `true` when a external sampler image is bound to the descriptor set.
bool isLayoutDirty = false;
bool isAnExternalSamplerBound = false;
private:
friend class VulkanDescriptorSetCache;
@@ -262,6 +272,14 @@ struct VulkanProgram : public HwProgram, fvkmemory::Resource {
VulkanProgram(VkDevice device, Program const& builder) noexcept;
~VulkanProgram();
/**
* Cancels any parallel compilation jobs that have not yet run for this
* program.
*/
inline void cancelParallelCompilation() {
mParallelCompilationCanceled.store(true, std::memory_order_release);
}
/**
* Writes out any queued push constants using the provided VkPipelineLayout.
*
@@ -284,6 +302,17 @@ struct VulkanProgram : public HwProgram, fvkmemory::Resource {
return mInfo->pushConstantDescription.getVkRanges();
}
/**
* Returns true if parallel compilation is canceled, false if not. Parallel
* compilation will be canceled if this program is destroyed before relevant
* pipelines are created.
*
* @return true if parallel compilation should run for this program, false if not
*/
inline bool isParallelCompilationCanceled() const {
return mParallelCompilationCanceled.load(std::memory_order_acquire);
}
inline void writePushConstant(VkCommandBuffer cmdbuf, VkPipelineLayout layout,
backend::ShaderStage stage, uint8_t index, backend::PushConstantVariant const& value) {
// It's possible that we don't have the layout yet. When external samplers are used, bindPipeline()
@@ -320,6 +349,7 @@ private:
PipelineInfo* mInfo;
VkDevice mDevice = VK_NULL_HANDLE;
std::atomic<bool> mParallelCompilationCanceled { false };
std::vector<PushConstantInfo> mQueuedPushConstants;
};

View File

@@ -166,6 +166,7 @@ void VulkanPipelineCache::asyncPrewarmCache(
.depthBiasConstantFactor = 0.f,
.depthBiasSlopeFactor = 0.f,
},
.stencilState = {},
.layout = layout,
};
PipelineDynamicOptions dynamicOptions {
@@ -178,9 +179,14 @@ void VulkanPipelineCache::asyncPrewarmCache(
CallbackManager::Handle cmh = mCallbackManager.get();
auto token = std::make_shared<ProgramToken>();
// Note: we keep a ref to vprogram in this lambda so that the shader modules don't get
// destroyed before we call createPipeline(). This is rare enough that we are ok with
// occasionally creating pipelines for destroyed materials.
// destroyed before we call createPipeline(). We can catch this in some cases, to avoid
// compiling too many unnecessary already-destroyed-materials.
mCompilerThreadPool.queue(priority, token, [this, vprogram, key, dynamicOptions, cmh]() mutable {
if (vprogram->isParallelCompilationCanceled()) {
FVK_LOGD << "Skipping prewarm for a program that has been destroyed already.";
return;
}
VkPipeline pipeline = createPipeline(key, dynamicOptions);
mCallbackManager.put(cmh);
// We don't actually need this pipeline, we just wanted to force the driver to cache
@@ -292,24 +298,45 @@ VkPipeline VulkanPipelineCache::createPipeline(
bool const enableDepthTest =
raster.depthCompareOp != SamplerCompareFunc::A ||
raster.depthWriteEnable;
// Stencil must be enabled if we're testing OR writing to the stencil buffer.
auto const& stencil = key.stencilState;
bool const enableStencilTest =
stencil.front.stencilFunc != StencilState::StencilFunction::A ||
stencil.back.stencilFunc != StencilState::StencilFunction::A ||
stencil.front.stencilOpDepthFail != StencilOperation::KEEP ||
stencil.back.stencilOpDepthFail != StencilOperation::KEEP ||
stencil.front.stencilOpStencilFail != StencilOperation::KEEP ||
stencil.back.stencilOpStencilFail != StencilOperation::KEEP ||
stencil.front.stencilOpDepthStencilPass != StencilOperation::KEEP ||
stencil.back.stencilOpDepthStencilPass != StencilOperation::KEEP ||
stencil.stencilWrite;
VkPipelineDepthStencilStateCreateInfo vkDs = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO,
.depthTestEnable = enableDepthTest ? VK_TRUE : VK_FALSE,
.depthWriteEnable = raster.depthWriteEnable,
.depthCompareOp = fvkutils::getCompareOp(raster.depthCompareOp),
.depthBoundsTestEnable = VK_FALSE,
.stencilTestEnable = VK_FALSE,
.stencilTestEnable = enableStencilTest ? VK_TRUE : VK_FALSE,
.minDepthBounds = 0.0f,
.maxDepthBounds = 0.0f,
};
vkDs.front = vkDs.back = {
.failOp = VK_STENCIL_OP_KEEP,
.passOp = VK_STENCIL_OP_KEEP,
.depthFailOp = VK_STENCIL_OP_KEEP,
.compareOp = VK_COMPARE_OP_ALWAYS,
.compareMask = 0u,
.writeMask = 0u,
.reference = 0u,
vkDs.front = {
.failOp = fvkutils::getStencilOp(stencil.front.stencilOpStencilFail),
.passOp = fvkutils::getStencilOp(stencil.front.stencilOpDepthStencilPass),
.depthFailOp = fvkutils::getStencilOp(stencil.front.stencilOpDepthFail),
.compareOp = fvkutils::getCompareOp(stencil.front.stencilFunc),
.compareMask = stencil.front.readMask,
.writeMask = (uint32_t) (stencil.stencilWrite ? stencil.front.writeMask : 0u),
.reference = (uint32_t) stencil.front.ref,
};
vkDs.back = {
.failOp = fvkutils::getStencilOp(stencil.back.stencilOpStencilFail),
.passOp = fvkutils::getStencilOp(stencil.back.stencilOpDepthStencilPass),
.depthFailOp = fvkutils::getStencilOp(stencil.back.stencilOpDepthFail),
.compareOp = fvkutils::getCompareOp(stencil.back.stencilFunc),
.compareMask = stencil.back.readMask,
.writeMask = (uint32_t) (stencil.stencilWrite ? stencil.back.writeMask : 0u),
.reference = (uint32_t) stencil.back.ref,
};
VkGraphicsPipelineCreateInfo pipelineCreateInfo = {
@@ -428,6 +455,10 @@ void VulkanPipelineCache::bindRasterState(RasterState const& rasterState) noexce
mPipelineRequirements.rasterState = rasterState;
}
void VulkanPipelineCache::bindStencilState(StencilState const& stencilState) noexcept {
mPipelineRequirements.stencilState = stencilState;
}
void VulkanPipelineCache::bindRenderPass(VkRenderPass renderPass, int subpassIndex) noexcept {
mPipelineRequirements.renderPass = renderPass;
mPipelineRequirements.subpassIndex = subpassIndex;

View File

@@ -122,6 +122,7 @@ public:
void bindLayout(VkPipelineLayout layout) noexcept;
void bindProgram(fvkmemory::resource_ptr<VulkanProgram> program) noexcept;
void bindRasterState(RasterState const& rasterState) noexcept;
void bindStencilState(StencilState const& stencilState) noexcept;
void bindRenderPass(VkRenderPass renderPass, int subpassIndex) noexcept;
void bindPrimitiveTopology(VkPrimitiveTopology topology) noexcept;
void bindVertexArray(VkVertexInputAttributeDescription const* attribDesc,
@@ -186,8 +187,8 @@ private:
VertexInputAttributeDescription vertexAttributes[VERTEX_ATTRIBUTE_COUNT]; // 128 : 28
VertexInputBindingDescription vertexBuffers[VERTEX_ATTRIBUTE_COUNT]; // 128 : 156
RasterState rasterState; // 16 : 284
uint32_t padding; // 4 : 300
VkPipelineLayout layout; // 8 : 304
StencilState stencilState; // 12 : 300
VkPipelineLayout layout; // 8 : 312
};
// Provides information about any dynamic state that should be used in creation of the
@@ -203,7 +204,7 @@ private:
uint8_t stereoscopicViewCount = 2;
};
static_assert(sizeof(PipelineKey) == 312, "PipelineKey must not have implicit padding.");
static_assert(sizeof(PipelineKey) == 320, "PipelineKey must not have implicit padding.");
using PipelineHashFn = utils::hash::MurmurHashFn<PipelineKey>;

View File

@@ -368,6 +368,7 @@ VulkanTexture::VulkanTexture(VkDevice device, VkPhysicalDevice physicalDevice,
bool const isProtected = any(tusage & TextureUsage::PROTECTED);
VkImageCreateInfo imageInfo{
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
.flags = isProtected ? VK_IMAGE_CREATE_PROTECTED_BIT : 0u,
.imageType = target == SamplerType::SAMPLER_3D ? VK_IMAGE_TYPE_3D : VK_IMAGE_TYPE_2D,
.format = vkFormat,
.extent = {w, h, depth},

View File

@@ -72,6 +72,10 @@ VkFormat transformVkFormat(VkFormat format, bool sRGB) {
return format;
}
bool isFormatSrgb(VkFormat format) {
return format == VK_FORMAT_R8G8B8A8_SRGB || format == VK_FORMAT_R8G8B8_SRGB;
}
bool isProtectedFromUsage(uint64_t usage) {
return usage & AHARDWAREBUFFER_USAGE_PROTECTED_CONTENT;
}
@@ -367,17 +371,22 @@ VulkanPlatform::ImageData VulkanPlatformAndroid::createVkImageFromExternal(
.pViewFormats = formats,
};
if (fvkExternalImage->sRGB) {
if (isFormatSrgb(metadata.format)) {
formats[0] = metadata.format;
formats[1] = transformVkFormat(metadata.format, /*sRGB=*/false);
imageFormatListInfo.pNext = externalCreateInfo.pNext;
externalCreateInfo.pNext = &imageFormatListInfo;
}
VkImageCreateFlags imageFlags =
(isFormatSrgb(metadata.format) ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u) |
(any(metadata.filamentUsage & TextureUsage::PROTECTED)
? VK_IMAGE_CREATE_PROTECTED_BIT
: 0u);
VkImageCreateInfo const imageInfo = {
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
.pNext = &externalCreateInfo,
.flags = fvkExternalImage->sRGB ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u,
.flags = imageFlags,
.imageType = VK_IMAGE_TYPE_2D,
// For non external images, use the same format as the AHB, which isn't in SRGB
// Fix VUID-VkMemoryAllocateInfo-pNext-02387

View File

@@ -633,6 +633,19 @@ VkCompareOp getCompareOp(SamplerCompareFunc func) {
}
}
VkStencilOp getStencilOp(StencilOperation op) {
switch (op) {
case StencilOperation::KEEP: return VK_STENCIL_OP_KEEP;
case StencilOperation::ZERO: return VK_STENCIL_OP_ZERO;
case StencilOperation::REPLACE: return VK_STENCIL_OP_REPLACE;
case StencilOperation::INCR: return VK_STENCIL_OP_INCREMENT_AND_CLAMP;
case StencilOperation::INCR_WRAP: return VK_STENCIL_OP_INCREMENT_AND_WRAP;
case StencilOperation::DECR: return VK_STENCIL_OP_DECREMENT_AND_CLAMP;
case StencilOperation::DECR_WRAP: return VK_STENCIL_OP_DECREMENT_AND_WRAP;
case StencilOperation::INVERT: return VK_STENCIL_OP_INVERT;
}
}
VkBlendFactor getBlendFactor(BlendFunction mode) {
switch (mode) {
case BlendFunction::ZERO: return VK_BLEND_FACTOR_ZERO;

View File

@@ -53,6 +53,7 @@ uint32_t getBytesPerPixel(TextureFormat format);
uint8_t getTexelBlockSize(VkFormat format);
VkCompareOp getCompareOp(SamplerCompareFunc func);
VkStencilOp getStencilOp(StencilOperation op);
VkBlendFactor getBlendFactor(BlendFunction mode);
VkCullModeFlags getCullMode(CullingMode mode);
VkFrontFace getFrontFace(bool inverseFrontFaces);

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,116 @@
/*
* 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.
*/
#include "BackendTest.h"
#include "Shader.h"
#include "SharedShaders.h"
#include "TrianglePrimitive.h"
#include <backend/PixelBufferDescriptor.h>
#include <utils/FixedCapacityVector.h>
namespace test {
using namespace filament;
using namespace filament::backend;
using namespace filament::math;
TEST_F(BackendTest, AutoresolveDifferingSampleCounts) {
auto& api = getDriverApi();
constexpr int kRenderTargetSize = 512;
auto swapChain = addCleanup(createSwapChain());
api.makeCurrent(swapChain, swapChain);
Shader shader = SharedShaders::makeShader(api, *mCleanup,
{
.mVertexType = VertexShaderType::Simple,
.mFragmentType = FragmentShaderType::SolidColored,
.mUniformType = ShaderUniformType::Simple,
});
TrianglePrimitive triangle(api);
PipelineState ps = getColorWritePipelineState();
shader.addProgramToPipelineState(ps);
auto ubuffer = addCleanup(api.createBufferObject(sizeof(SimpleMaterialParams),
BufferObjectBinding::UNIFORM, BufferUsage::STATIC));
shader.bindUniform<SimpleMaterialParams>(api, ubuffer);
// Create a texture with sample count = 1.
Handle<HwTexture> texture = addCleanup(api.createTexture(SamplerType::SAMPLER_2D, 1,
TextureFormat::RGBA8, 1, kRenderTargetSize, kRenderTargetSize, 1,
TextureUsage::COLOR_ATTACHMENT | TextureUsage::SAMPLEABLE));
// First render pass: render a red triangle to a RenderTarget with 8 samples.
{
Handle<HwRenderTarget> renderTarget =
addCleanup(api.createRenderTarget(TargetBufferFlags::COLOR, kRenderTargetSize,
kRenderTargetSize, 8, 1, { { texture } }, {}, {}));
shader.uploadUniform(api, ubuffer,
SimpleMaterialParams{
.color = float4(1, 0, 0, 1),
});
RenderPassParams params = getClearColorRenderPass(float4(0));
params.viewport = { 0, 0, kRenderTargetSize, kRenderTargetSize };
RenderFrame frame(api);
api.beginRenderPass(renderTarget, params);
ps.primitiveType = PrimitiveType::TRIANGLES;
ps.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(ps);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
api.commit(swapChain);
}
// Second render pass: render a green triangle to a RenderTarget with 4 samples, attached to
// the same texture.
{
Handle<HwRenderTarget> renderTarget =
addCleanup(api.createRenderTarget(TargetBufferFlags::COLOR, kRenderTargetSize,
kRenderTargetSize, 4, 1, { { texture } }, {}, {}));
shader.uploadUniform(api, ubuffer,
SimpleMaterialParams{
.color = float4(0, 1, 0, 1),
});
RenderPassParams params = getClearColorRenderPass(float4(0));
params.viewport = { 0, 0, kRenderTargetSize, kRenderTargetSize };
RenderFrame frame(api);
api.beginRenderPass(renderTarget, params);
ps.primitiveType = PrimitiveType::TRIANGLES;
ps.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(ps);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
EXPECT_IMAGE(renderTarget, ScreenshotParams(kRenderTargetSize, kRenderTargetSize,
"AutoresolveDifferingSampleCounts", 1048576));
api.commit(swapChain);
}
}
} // namespace test

View File

@@ -41,6 +41,17 @@ void main() {
}
)");
std::string fragmentCoord(R"(#version 450 core
layout(location = 0) out vec4 fragColor;
void main() {
// gl_FragCoord.xy is in pixels. (0.5, 0.5) is the center of the bottom-left pixel.
// We want to map this to a color to verify orientation.
// Red varies with X, Green varies with Y.
// We assume a 64x64 texture for this test.
vec2 coord = gl_FragCoord.xy / 64.0;
fragColor = vec4(coord.x, coord.y, 0.0, 1.0);
}
)");
}
namespace test {
@@ -51,8 +62,6 @@ public:
};
TEST_F(ReadTextureTest, ReadTexture2D) {
SKIP_IF(Backend::METAL, "readTexture not implemented for Metal");
DriverApi& api = getDriverApi();
const size_t textureSize = 64;
@@ -146,8 +155,6 @@ TEST_F(ReadTextureTest, ReadTexture2D) {
}
TEST_F(ReadTextureTest, ReadTextureArray) {
SKIP_IF(Backend::METAL, "readTexture not implemented for Metal");
DriverApi& api = getDriverApi();
const size_t textureSize = 64;
const uint16_t layers = 2;
@@ -214,4 +221,108 @@ TEST_F(ReadTextureTest, ReadTextureArray) {
}
}
TEST_F(ReadTextureTest, ReadTextureXCoordinates) {
SKIP_IF(Backend::OPENGL, "readTexture not implemented for OpenGL");
DriverApi& api = getDriverApi();
const size_t textureSize = 64;
Handle<HwSwapChain> swapChain =
addCleanup(api.createSwapChainHeadless(textureSize, textureSize, 0));
api.makeCurrent(swapChain, swapChain);
auto usage = TextureUsage::COLOR_ATTACHMENT | TextureUsage::SAMPLEABLE | TextureUsage::BLIT_SRC;
Handle<HwTexture> const texture = addCleanup(api.createTexture(SamplerType::SAMPLER_2D, 1,
TextureFormat::RGBA8, 1, textureSize, textureSize, 1, usage));
Handle<HwRenderTarget> renderTarget = addCleanup(api.createRenderTarget(
TargetBufferFlags::COLOR, textureSize, textureSize, 1, 0, { { texture, 0 } }, {}, {}));
// Draw a full-screen quad (using a large triangle)
TrianglePrimitive triangle(api);
const math::float2 fsVertices[3] = { { -1.0f, -1.0f }, { 3.0f, -1.0f }, { -1.0f, 3.0f } };
triangle.updateVertices(fsVertices);
std::string vertexShader =
SharedShaders::getVertexShaderText(VertexShaderType::Noop, ShaderUniformType::None);
Shader shader(api, *mCleanup,
ShaderConfig{
.vertexShader = vertexShader,
.fragmentShader = fragmentCoord,
.uniforms = {},
});
RenderPassParams params = getClearColorRenderPass(math::float4(0));
params.viewport.width = textureSize;
params.viewport.height = textureSize;
api.beginFrame(0, 0, 0);
api.beginRenderPass(renderTarget, params);
PipelineState state = getColorWritePipelineState();
shader.addProgramToPipelineState(state);
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(state);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
// Read texture back
size_t bufferSize = textureSize * textureSize * 4;
void* buffer = calloc(1, bufferSize);
struct UserData {
bool finished = false;
} userData;
PixelBufferDescriptor descriptor(
buffer, bufferSize, PixelDataFormat::RGBA, PixelDataType::UBYTE, 1, 0, 0, textureSize,
[](void* buffer, size_t size, void* user) {
UserData* data = (UserData*) user;
data->finished = true;
uint8_t* pixels = (uint8_t*) buffer;
const size_t width = 64;
const size_t height = 64;
// Check Bottom-Left (0, 0) -> Should be black (0, 0, 0)
// In buffer, this is at index 0
// Y=0, X=0
EXPECT_NEAR(pixels[0], 0, 5);
EXPECT_NEAR(pixels[1], 0, 5);
// Check Bottom-Right (1, 0) -> Should be Red (255, 0, 0)
// Y=0, X=63
size_t br_idx = (63) * 4;
EXPECT_NEAR(pixels[br_idx], 255, 5);
EXPECT_NEAR(pixels[br_idx + 1], 0, 5);
// Check Top-Left (0, 1) -> Should be Green (0, 255, 0)
// Y=63, X=0
size_t tl_idx = (63 * width) * 4;
EXPECT_NEAR(pixels[tl_idx], 0, 5);
EXPECT_NEAR(pixels[tl_idx + 1], 255, 5);
// Check Top-Right (1, 1) -> Should be Yellow (255, 255, 0)
// Y=63, X=63
size_t tr_idx = (63 * width + 63) * 4;
EXPECT_NEAR(pixels[tr_idx], 255, 5);
EXPECT_NEAR(pixels[tr_idx + 1], 255, 5);
free(buffer);
},
&userData);
api.readTexture(texture, 0, 0, 0, 0, textureSize, textureSize, std::move(descriptor));
api.commit(swapChain);
api.endFrame(0);
flushAndWait();
EXPECT_TRUE(userData.finished);
}
} // namespace test

View File

@@ -798,16 +798,19 @@ RenderPass::Command* RenderPass::generateCommandsImpl(CommandTypeFlags extraFlag
cmd.info.rasterState.culling = cullingMode;
// FIXME: should writeDepthForShadowCasters take precedence over mi->getDepthWrite()?
cmd.info.rasterState.depthWrite = (1 // only keep bit 0
& (mi->isDepthWriteEnabled() | (mode == TransparencyMode::TWO_PASSES_ONE_SIDE)
| isPickingVariant)
& !(filterTranslucentObjects & translucent)
& !(depthFilterAlphaMaskedObjects & rs.alphaToCoverage))
| writeDepthForShadowCasters;
cmd.info.rasterState.depthWrite =
(1 // only keep bit 0
& (mi->isDepthWriteEnabled() |
(mode == TransparencyMode::TWO_PASSES_ONE_SIDE) |
isPickingVariant) &
!(depthFilterAlphaMaskedObjects & rs.alphaToCoverage)) |
writeDepthForShadowCasters;
*curr = cmd;
// cancel command if both front and back faces are culled
curr->key |= select(cullingMode == CullingMode::FRONT_AND_BACK);
// cancel command if asked to filter translucent objects
curr->key |= select(filterTranslucentObjects & translucent);
}
++curr;
@@ -901,6 +904,12 @@ void RenderPass::Executor::execute(FEngine const& engine, DriverApi& driver,
size_t const capacity = engine.getMinCommandBufferSize();
CircularBuffer const& circularBuffer = driver.getCircularBuffer();
// b/479079631: Log the number of commands in this render pass.
size_t const commandCount = last - first;
if (Platform* platform = engine.getPlatform(); platform->hasDebugUpdateStatFunc()) {
platform->debugUpdateStat("filament.renderer.render_pass.command_count", commandCount);
}
if (first != last) {
FILAMENT_TRACING_VALUE(FILAMENT_TRACING_CATEGORY_FILAMENT, "commandCount", last - first);

View File

@@ -258,7 +258,21 @@ RendererUtils::ColorPassOutput RendererUtils::colorPass(
}
driver.beginRenderPass(out.target, out.params);
Platform* platform = engine.getPlatform();
CircularBuffer const& circularBuffer = driver.getCircularBuffer();
// b/479079631: Log the current command buffer size before and after executing the
// render pass.
if (platform->hasDebugUpdateStatFunc()) {
platform->debugUpdateStat(
"filament.renderer.color_pass.command_buffer_used_start",
circularBuffer.getUsed());
}
passExecutor.execute(engine, driver);
if (platform->hasDebugUpdateStatFunc()) {
platform->debugUpdateStat(
"filament.renderer.color_pass.command_buffer_used_end",
circularBuffer.getUsed());
}
driver.endRenderPass();
// unbind all descriptor sets to avoid false dependencies with the next pass

View File

@@ -462,8 +462,22 @@ void FEngine::init() {
#endif
{
FMaterial::DefaultMaterialBuilder defaultMaterialBuilder;
defaultMaterialBuilder.package(
MATERIALS_DEFAULTMATERIAL_DATA, MATERIALS_DEFAULTMATERIAL_SIZE);
switch (mConfig.stereoscopicType) {
case StereoscopicType::NONE:
case StereoscopicType::INSTANCED:
defaultMaterialBuilder.package(
MATERIALS_DEFAULTMATERIAL_DATA, MATERIALS_DEFAULTMATERIAL_SIZE);
break;
case StereoscopicType::MULTIVIEW:
#ifdef FILAMENT_ENABLE_MULTIVIEW
defaultMaterialBuilder.package(
MATERIALS_DEFAULTMATERIAL_MULTIVIEW_DATA,
MATERIALS_DEFAULTMATERIAL_MULTIVIEW_SIZE);
#else
assert_invariant(false);
#endif
break;
}
mDefaultMaterial = downcast(defaultMaterialBuilder.build(*this));
}
@@ -721,14 +735,21 @@ void FEngine::prepare(DriverApi& driver) {
}
UboManager* uboManager = mUboManager;
size_t const capacity = getMinCommandBufferSize();
for (auto& materialInstanceList: mMaterialInstances) {
materialInstanceList.second.forEach([&driver, uboManager](FMaterialInstance const* item) {
// post-process materials instances must be commited explicitly because their
// parameters are typically not set at this point in time.
if (item->getMaterial()->getMaterialDomain() == MaterialDomain::SURFACE) {
item->commit(driver, uboManager);
}
});
materialInstanceList.second.forEach(
[this, &driver, uboManager, capacity](FMaterialInstance const* item) {
// post-process materials instances must be commited explicitly because their
// parameters are typically not set at this point in time.
if (item->getMaterial()->getMaterialDomain() == MaterialDomain::SURFACE) {
// If the remaining space is less than half the capacity, we flush right
// away to allow some headroom for commands that might come later.
if (UTILS_UNLIKELY(driver.getCircularBuffer().getUsed() > capacity / 2)) {
flush();
}
item->commit(driver, uboManager);
}
});
}
if (useUboBatching) {
@@ -899,7 +920,12 @@ int FEngine::loop() {
void FEngine::flushCommandBuffer(CommandBufferQueue& commandBufferQueue) const {
getDriver().purge();
commandBufferQueue.flush();
commandBufferQueue.flush([this](void* begin, void* end) {
UTILS_UNUSED FEngine* engine = const_cast<FEngine*>(this);
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
engine->getDriverApi().debugPrintHistogram(begin, end);
#endif
});
}
const FMaterial* FEngine::getSkyboxMaterial() const noexcept {

View File

@@ -378,8 +378,14 @@ private:
Variant variant = {}) const noexcept;
bool isSharedVariant(Variant const variant) const {
return (mDefinition.materialDomain == MaterialDomain::SURFACE) && !mIsDefaultMaterial &&
!mDefinition.hasCustomDepthShader && Variant::isValidDepthVariant(variant);
// HACK: The default material "should" have VSM | DEP, but then we'd have to compile it as a
// lit material, which would increase binary size. Perhaps we could specially compile it
// with this variant, but with the shader program cache in active development, the days of
// the default material are numbered anyway.
constexpr Variant::type_t vsmAndDep = Variant::VSM | Variant::DEP;
return mDefinition.materialDomain == MaterialDomain::SURFACE && !mIsDefaultMaterial &&
!mDefinition.hasCustomDepthShader && Variant::isValidDepthVariant(variant) &&
(variant.key & vsmAndDep) != vsmAndDep;
}
mutable utils::FixedCapacityVector<backend::Handle<backend::HwProgram>> mCachedPrograms;

View File

@@ -138,7 +138,20 @@ FMaterial const* FSkybox::createMaterial(FEngine& engine) {
} else
#endif
{
builder.package(MATERIALS_SKYBOX_DATA, MATERIALS_SKYBOX_SIZE);
switch (engine.getConfig().stereoscopicType) {
case Engine::StereoscopicType::NONE:
case Engine::StereoscopicType::INSTANCED:
builder.package(MATERIALS_SKYBOX_DATA, MATERIALS_SKYBOX_SIZE);
break;
case Engine::StereoscopicType::MULTIVIEW:
#ifdef FILAMENT_ENABLE_MULTIVIEW
builder.package(MATERIALS_SKYBOX_MULTIVIEW_DATA, MATERIALS_SKYBOX_MULTIVIEW_SIZE);
#else
PANIC_POSTCONDITION("Multiview is enabled in the Engine, but this build has not "
"been compiled for multiview.");
#endif
break;
}
}
auto material = builder.build(engine);
return downcast(material);

View File

@@ -54,45 +54,47 @@ list(APPEND RESGEN_SOURCE ${DUMMY_SRC})
# Unit tests
# ==================================================================================================
# The following tests rely on private APIs that are stripped
# away in Release builds
if (TNT_DEV)
add_executable(test_${TARGET}
MockDriver.h
filament_AtlasAllocator_test.cpp
test_BufferAllocator.cpp
test_BufferAllocatorStress.cpp
test_CircularQueue.cpp
test_compact.cpp
test_FenceManager.cpp
test_UboManager.cpp
filament_test_exposure.cpp
filament_rendering_test.cpp
filament_bimap_test.cpp
filament_framegraph_test.cpp
filament_test.cpp
filament_test_material.cpp
${RESGEN_SOURCE})
if (FILAMENT_BUILD_TESTING)
# The following tests rely on private APIs that are stripped
# away in Release builds
if (TNT_DEV)
add_executable(test_${TARGET}
MockDriver.h
filament_AtlasAllocator_test.cpp
test_BufferAllocator.cpp
test_BufferAllocatorStress.cpp
test_CircularQueue.cpp
test_compact.cpp
test_FenceManager.cpp
test_UboManager.cpp
filament_test_exposure.cpp
filament_rendering_test.cpp
filament_bimap_test.cpp
filament_framegraph_test.cpp
filament_test.cpp
filament_test_material.cpp
${RESGEN_SOURCE})
target_link_libraries(test_${TARGET} PRIVATE filamat filament gtest)
target_compile_options(test_${TARGET} PRIVATE ${COMPILER_FLAGS})
target_include_directories(test_${TARGET} PRIVATE ${RESOURCE_DIR})
set_target_properties(test_${TARGET} PROPERTIES FOLDER Tests)
target_link_libraries(test_${TARGET} PRIVATE filamat filament gtest)
target_compile_options(test_${TARGET} PRIVATE ${COMPILER_FLAGS})
target_include_directories(test_${TARGET} PRIVATE ${RESOURCE_DIR})
set_target_properties(test_${TARGET} PROPERTIES FOLDER Tests)
add_executable(test_depth depth_test.cpp)
target_link_libraries(test_depth PRIVATE utils)
endif()
add_executable(test_material_parser
filament_test_material_parser.cpp
${RESGEN_SOURCE}
)
target_link_libraries(test_material_parser PRIVATE filamat filament gtest)
target_compile_options(test_material_parser PRIVATE ${COMPILER_FLAGS})
target_include_directories(test_material_parser PRIVATE ${RESOURCE_DIR})
set_target_properties(test_material_parser PROPERTIES FOLDER Tests)
if (ANDROID)
add_executable(test_compiler compiler_test.cpp)
target_link_libraries(test_compiler PRIVATE gtest utils EGL GLESv3)
add_executable(test_depth depth_test.cpp)
target_link_libraries(test_depth PRIVATE utils)
endif()
add_executable(test_material_parser
filament_test_material_parser.cpp
${RESGEN_SOURCE}
)
target_link_libraries(test_material_parser PRIVATE filamat filament gtest)
target_compile_options(test_material_parser PRIVATE ${COMPILER_FLAGS})
target_include_directories(test_material_parser PRIVATE ${RESOURCE_DIR})
set_target_properties(test_material_parser PROPERTIES FOLDER Tests)
if (ANDROID)
add_executable(test_compiler compiler_test.cpp)
target_link_libraries(test_compiler PRIVATE gtest utils EGL GLESv3)
endif()
endif()

View File

@@ -1,12 +1,12 @@
Pod::Spec.new do |spec|
spec.name = "Filament"
spec.version = "1.69.2"
spec.version = "1.69.3"
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.69.2/filament-v1.69.2-ios.tgz" }
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.69.3/filament-v1.69.3-ios.tgz" }
spec.libraries = 'c++'

View File

@@ -52,3 +52,32 @@ pod spec lint --no-clean --verbose --subspec=Filament/filament 2>&1 | tee lint_o
> **Tip:** To view all defined subspecs, grep the podspec file:
> `grep "spec.subspec" ios/CocoaPods/Filament.podspec`
## Testing a Podspec Locally
Before pushing a new version to the CocoaPods trunk, you should verify the podspec in a sample project.
### Using the Sample Project
Filament includes a dedicated sample project located in `ios/samples/HelloCocoaPods`. To test your
local changes:
1. **Modify the Podfile**: Add a `:podspec` directive pointing to your local `.podspec` file.
```ruby
platform :ios, '13.0'
target 'HelloCocoaPods' do
# Use the local development podspec
pod 'Filament', :podspec => '../../CocoaPods/Filament.podspec'
end
```
2. **Sync Dependencies**: Run the installation command from the sample directory:
```bash
pod install
```
3. **Verify the Build**: Open the generated `HelloCocoaPods.xcworkspace` in Xcode. Build and run the
target to ensure the headers are found and the binaries link correctly.

View File

@@ -74,17 +74,19 @@ endif()
install(TARGETS ${TARGET} ARCHIVE DESTINATION lib/${DIST_DIR})
# Build the tests...
add_executable(test_${TARGET}
tests/OpenGLSupport.cpp
tests/OpenGLSupport.hpp
tests/test_bluegl.cpp)
if (FILAMENT_BUILD_TESTING)
# Build the tests...
add_executable(test_${TARGET}
tests/OpenGLSupport.cpp
tests/OpenGLSupport.hpp
tests/test_bluegl.cpp)
if (LINUX)
target_link_libraries(test_${TARGET} PUBLIC dl)
if (LINUX)
target_link_libraries(test_${TARGET} PUBLIC dl)
endif()
# and we're linking against the libraries below, importing their public headers
target_link_libraries(test_${TARGET} LINK_PUBLIC ${TARGET})
target_link_libraries(test_${TARGET} LINK_PUBLIC gtest)
set_target_properties(test_${TARGET} PROPERTIES FOLDER Tests)
endif()
# and we're linking against the libraries below, importing their public headers
target_link_libraries(test_${TARGET} LINK_PUBLIC ${TARGET})
target_link_libraries(test_${TARGET} LINK_PUBLIC gtest)
set_target_properties(test_${TARGET} PROPERTIES FOLDER Tests)

View File

@@ -50,7 +50,7 @@ install(DIRECTORY ${PUBLIC_HDR_DIR}/camutils DESTINATION include)
# ==================================================================================================
# Tests
# ==================================================================================================
if (NOT ANDROID AND NOT WEBGL AND NOT IOS)
if (FILAMENT_BUILD_TESTING AND NOT ANDROID AND NOT WEBGL AND NOT IOS)
add_executable(test_${TARGET} tests/test_camutils.cpp)
target_link_libraries(test_${TARGET} PRIVATE camutils gtest)
set_target_properties(test_${TARGET} PROPERTIES FOLDER Tests)

View File

@@ -40,13 +40,20 @@ endif()
file(MAKE_DIRECTORY ${MATERIAL_DIR})
set (MATC_FLAGS ${MATC_BASE_FLAGS})
if (FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "instanced")
set (MATC_FLAGS ${MATC_FLAGS} -PstereoscopicType=instanced)
elseif (FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "multiview")
set (MATC_FLAGS ${MATC_FLAGS} -PstereoscopicType=multiview)
endif ()
foreach (mat_src ${MATERIAL_SRCS})
get_filename_component(localname "${mat_src}" NAME_WE)
get_filename_component(fullname "${mat_src}" ABSOLUTE)
set(output_path "${MATERIAL_DIR}/${localname}.filamat")
add_custom_command(
OUTPUT ${output_path}
COMMAND matc ${MATC_BASE_FLAGS} -o ${output_path} ${fullname}
COMMAND matc ${MATC_FLAGS} -o ${output_path} ${fullname}
DEPENDS ${mat_src} matc
COMMENT "Compiling material ${mat_src} to ${output_path}"
)

Some files were not shown because too many files have changed in this diff Show More