Compare commits

..
35 Commits
Author SHA1 Message Date
Eliza Velasquez 6b0dc7babe wip 2026-03-09 16:34:29 +09:00
Eliza Velasquez c4ea6cb1bf android: fix build errors 2026-03-06 16:39:27 +09:00
Eliza Velasquez 87ed485b04 java: add bindings for MaterialInstance::compile() 2026-03-06 16:11:45 +09:00
Eliza Velasquez 68ae023108 engine: add MaterialInstance::compile() 2026-03-06 15:31:46 +09:00
Eliza Velasquez 474fe0c96c engine: override spec constants on instance-basis
This commit adds `getConstant()` and `setConstant()` methods to
`MaterialInstance`.

When a material instance is created, it doesn't create or manage any spec
constants on its own, but instead relies on `Material`. The client can change
constants at any time, after which the necessary resources are copied and
maintained by `MaterialInstance`.

As part of this change, most internal rendering methods which depended on
`Material` have been refactored to instead depend on `MaterialInstance`.
2026-03-05 15:51:55 +09:00
Eliza 83653fb358 engine: encapsulate material cache handling (#9663)
* engine: encapsulate material cache handling

We will soon allow `MaterialInstance` to override the value of spec constants.
To avoid code duplication, we introduce a new class `MaterialPrograms` which
handles the chunky bits of managing the program cache and values of the spec
constants.

* MaterialPrograms: add explicit initialize method

* MaterialPrograms: backport fixes, address comments

* MaterialPrograms: set all constants at once

* MaterialPrograms: rename to LocalProgramCache
2026-03-05 06:42:05 +00:00
Eliza 8a3c48fef1 utils: add LRU cache to RefCountedMap (#9730)
* utils: add LRU cache to RefCountedMap

This change introduces a new data structure LruCache and uses it in
RefCountedMap to keep a fixed number of cache entries alive after their
reference count has dropped to zero in the main map.

* utils: address LRU cache comments
2026-03-05 06:04:16 +00:00
Powei Feng 7f6b9bb144 vk: reference rendertarget as part of fbo cache (#9771)
The FBO cache uses imageviews as its cache key. To ensure that these
views are valid for the lifetime of a key-value pair in the cache, we
need to also reference the rendertarget in the value part of the pair.
The RT contains attachments, which are wrappers around
which own the image views.

Fixes #9680
2026-03-04 09:59:05 -08:00
rafadevai 687c42583b VK: Only make a single copy of the descriptor with external samplers (#9765)
When more than one external sampler are present the
flow will clone the descriptor set twice and make a copy
of a binding that requires an immutable sampler. The copy
of this binding will cause a crash in some adreno GPUs.

This change will make sure the descriptor set is only cloned one
and after that only update binding operations are done for the
external sampled bindings.
2026-03-03 18:34:40 -08:00
Mathias Agopian 71e8cab08a Fix a race when garbage-collecting components (#9768)
Not all component managers are thread-safe for garbage-collection. In
particular, some need to access the DriverAPI which is never
thread-safe.

We refactor the garbage-collection code into FEngine, so it's not
duplicated in FRenderer. We make it more explicit that 
FRenderableManager::destroyComponent() needs the DriverAPI, and we
don't call its gc() from a job.

Besides the refactoring, the only change in this CL is that 
FRenderableManager::gc() is no longer called from a job.


FIXES=[489134910]
2026-03-03 13:21:06 -08:00
Ben Doherty afae31a975 Add Sonatype publishing step to release workflow (#9764) 2026-03-03 12:41:55 -08:00
Powei Feng 5ac5dc4c95 ci: fetch PR ref explicitly to extract commit message for forks (#9756) 2026-03-03 17:24:00 +00:00
Filament Bot e975572972 [automated] Updating /docs due to commit 29e91f0
Full commit hash is 29e91f0d3a

DOCS_ALLOW_DIRECT_EDITS
2026-03-03 16:21:50 +00:00
Sungun Park 29e91f0d3a Release Filament 1.69.5 2026-03-03 08:15:31 -08:00
Mathias Agopian 6f0d47f275 EVSM improvements (#9758)
- refactoring/clecanup to make some changes easier
- VSM mipmap generation was mistakenly disable when blur radius was 0
- analytic variance was disabled because the math only worked for VSM. Fixed the math.
- better handling of large blurs when using fp32
- implement EVSM equivalent of receiver plane normal bias
- use correct EVSM clearing color
- mipmapping with point lights works much better (no seams)
- min variance is computed automatically
- custom high precision mipmaping shader for VSM
2026-03-02 16:46:07 -08:00
Powei Feng cf66813f41 android: sample-render-validation with new UI and cli tool (#9751)
Android App:
- Refactored activity_main.xml to use modern Material 3 components.
- Grouped Export/Help buttons logically and moved ADB instructions
  to dedicated info icons.
- Added an in-app "Load Test" button to explicitly pick test bundles.
- Updated ValidationInputManager.kt to gracefully handle relative
  zip_path intent execution via ADB targeting app external storage.

Tooling & Documentation:
- Added a Python Textual TUI (validation_app.py) to automate device
  discovery, test execution, bundling, renaming, and
  downloading/uploading.
- Added README.md in test/render-validation documenting ADB intent
  parameter capabilities and TUI dashboard setup.
2026-03-02 23:51:07 +00:00
Powei Feng 5f89e8e711 vk: fix crash when resizing (#9762)
The problem is that we might flush when resizing happens. So
we need to ask for the command buffer "after" the resize/acquire
swapchain logic.

Fixes #9718
2026-02-27 18:14:59 +00:00
Anish Goyal be9e9298e1 Add a default fence value to VulkanCommands (#9759)
This is useful for the case of certain devices, where a filament::Sync
may be created BEFORE any commands are submitted.
2026-02-27 06:03:30 +00:00
dependabot[bot]andPowei Feng 9218b90c9c build(deps): bump the pip group across 1 directory with 2 updates (#9749)
Bumps the pip group with 2 updates in the /test/renderdiff/src directory: [flask](https://github.com/pallets/flask) and [werkzeug](https://github.com/pallets/werkzeug).


Updates `flask` from 3.1.2 to 3.1.3
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.1.2...3.1.3)

Updates `werkzeug` from 3.1.4 to 3.1.6
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.1.4...3.1.6)

---
updated-dependencies:
- dependency-name: flask
  dependency-version: 3.1.3
  dependency-type: direct:production
  dependency-group: pip
- dependency-name: werkzeug
  dependency-version: 3.1.6
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Powei Feng <powei@google.com>
2026-02-27 05:37:29 +00:00
Mathias Agopian 070a07679d Simplify the API of MaterialInstanceManager (#9757)
It's no longer necessary to preallocate a "tag" to reuse a material
instance. Instead, we can simply pass a unique tag when getting the
instance from the pool.
2026-02-26 15:11:31 -08:00
Andrew Wilson 10b7bd71f9 Avoid backend test when building without testing (#9727) 2026-02-26 22:49:58 +00:00
Powei Feng e4ae96a2a1 renderdiff: disable transmission + webgpu due to flake
RDIFF_ACCEPT_NEW_GOLDENS
2026-02-26 11:52:35 -08:00
SiyuandMathias Agopian 56ac08e353 Provide thread name when attaching to JVM on Android (#9755)
* Provide thread name when attaching to JVM on Android

When calling AttachCurrentThread on Android, pass a JavaVMAttachArgs structure. This allows providing the thread name, which is retrieved using pthread_getname_np, to the JVM.

* Fix Android build error: pthread_getname_np requires API 26+

---------

Co-authored-by: Mathias Agopian <mathias@google.com>
2026-02-26 10:44:24 -08:00
52b0b553b4 Marshall the name size when setting thread name with pthread_setname_np (#9753)
* Marshall the name size when setting thread name with pthread_setname_np.

[pthread_setname_np](https://source.corp.google.com/piper///depot/google3/third_party/android/ndk/stable/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/pthread.h;l=330-341) requires the caller to keep the name within 16 bytes.

After this change, Filament threads like `OpenGLTimerQuer`, `CompilerThreadP`, `CompilerThreadP`, `Filament Choreo`, `FrameInfoGpuCom` would be displayed correctly in the trace.

* Use constexpr MAX_PTHREAD_NAME_LEN

---------

Co-authored-by: Powei Feng <powei@google.com>
Co-authored-by: Mathias Agopian <mathias@google.com>
2026-02-26 10:43:53 -08:00
Filament Bot 00f3c7175c [automated] Updating /docs due to commit e4fa86f
Full commit hash is e4fa86fb01

DOCS_ALLOW_DIRECT_EDITS
2026-02-26 18:05:45 +00:00
Powei Feng e4fa86fb01 renderdiff: bump golden again
Transimssion.webgpu.TransmissionRoughnessTest seems to have small,
non-flaky differences.

RDIFF_ACCEPT_NEW_GOLDENS
2026-02-26 09:48:30 -08:00
Mathias AgopianandPowei Feng 7da2a08df6 replace Variant::VSM with MNT and S2D (#9750)
The VSM variant bit was overloaded, it meant two different things
depending on the DEP bit (depth).

For standard variants (DEP = 0), it decides the type of the shadow
sampler used (PCF or 2D).

For depth variants (DEP = 1), it decides what is written during the
shadow pass (nothing, i.e. depth only, or EVSM depth moments).

We now clearly separate the two bits throughout the code.

This change should be purely source-cosmetic, there shouldn't be any
behavior changes.

Co-authored-by: Powei Feng <powei@google.com>
2026-02-25 16:16:07 -08:00
Powei Feng 82246d934d renderdiff: fix update_golden.py
And fix other python bugs

RDIFF_ACCEPT_NEW_GOLDENS
2026-02-25 15:35:02 -08:00
Filament Bot c60969ef67 [automated] Updating /docs due to commit ce37f21
Full commit hash is ce37f216bc

DOCS_ALLOW_DIRECT_EDITS
2026-02-25 23:01:55 +00:00
Powei Feng ce37f216bc Update renderdiff README
RDIFF_ACCEPT_NEW_GOLDENS
2026-02-25 14:58:35 -08:00
Mathias Agopian 81c71fbbb9 Improve shadow normal bias calculation (#9734)
The previous code used the max of the texel's width or height footprint
in world space to compute the offset; this could both overestimate or
underestimate the bias causing some peter panning or acne.

The new code replaces a sqrt with a dot, but is otherwise similar.
2026-02-25 12:16:13 -08:00
Mathias Agopian 07a7c6003a better computation of the jacobian of a projection (#9731)
The previous code was a bit clunky and not generic, it made assumptions
about the shape of the projection matrix. 
This just uses the generic, correct calculation.

In addition the previous code used the wrong matrix, it assumed that
Wp wasn't needed, but it was because translations do change the
jacobian value at a given point.

RDIFF_ACCEPT_NEW_GOLDENS
2026-02-25 12:04:55 -08:00
Sungun Park 804a74c205 Remove unused member in OpenGLContext (#9741) 2026-02-25 19:46:34 +00:00
Anish Goyal dde49a410a Add inferred template to View.cpp (#9746)
In some cases, this missing template causes build issues when locally
building Impress prebuilts.
2026-02-25 09:19:19 -08:00
Filament Bot 770ce7f8ec [automated] Updating /docs due to commit 11714d3
Full commit hash is 11714d3adc

DOCS_ALLOW_DIRECT_EDITS
2026-02-25 03:44:07 +00:00
103 changed files with 5196 additions and 1501 deletions
+17 -15
View File
@@ -27,17 +27,24 @@ runs:
echo "$HASH" > /tmp/commit_hash.txt
- name: Find commit message (PR)
shell: bash
id: checkout_code
if: github.event_name == 'pull_request'
run: |
BEFORE_HASH=$(git rev-parse HEAD)
echo "hash=$BEFORE_HASH" >> "$GITHUB_OUTPUT"
# Next we will checkout the actual head (not the merge commits) of the PR
AFTER_HASH="${{ github.event.pull_request.head.sha }}"
git checkout $AFTER_HASH
COMMIT_MESSAGE=$(git log -1 --no-merges)
echo "$COMMIT_MESSAGE" > /tmp/commit_msg.txt
echo "$AFTER_HASH" > /tmp/commit_hash.txt
PR_NUMBER="${{ github.event.pull_request.number }}"
# Fetch the head of the PR explicitly to handle forks. Depth 50 ensures we can traverse past recent merge commits.
git fetch --depth=50 origin "pull/$PR_NUMBER/head:pr-head"
COMMIT_HASH=$(git log -1 --no-merges pr-head --format=%H)
AUTHOR_NAME=$(git log -1 --no-merges pr-head --format=%an)
AUTHOR_EMAIL=$(git log -1 --no-merges pr-head --format=%ae)
TSTAMP=$(git log -1 --no-merges pr-head --format=%aI)
COMMIT_MESSAGE=$(git log -1 --no-merges pr-head --format=%B)
echo "commit $COMMIT_HASH" > /tmp/commit_msg.txt
echo "Author: ${AUTHOR_NAME}<${AUTHOR_EMAIL}>" >> /tmp/commit_msg.txt
echo "Date: ${TSTAMP}" >> /tmp/commit_msg.txt
echo "" >> /tmp/commit_msg.txt
echo "$COMMIT_MESSAGE" >> /tmp/commit_msg.txt
echo "$COMMIT_HASH" > /tmp/commit_hash.txt
- shell: bash
id: action_output
run: |
@@ -47,9 +54,4 @@ runs:
cat /tmp/commit_msg.txt >> "$GITHUB_OUTPUT"
echo "$DELIMITER" >> "$GITHUB_OUTPUT"
# Get the commit hash
echo "hash=$(cat /tmp/commit_hash.txt)" >> "$GITHUB_OUTPUT"
- name: Cleanup Find commit message (PR)
shell: bash
if: github.event_name == 'pull_request'
run: |
git checkout ${{ steps.checkout_code.outputs.hash }}
echo "hash=$(cat /tmp/commit_hash.txt)" >> "$GITHUB_OUTPUT"
+48
View File
@@ -188,6 +188,54 @@ jobs:
const globber = await glob.create(['out/*.aar', 'out/*.apk', 'out/*.tgz'].join('\n'));
await upload({ github, context }, await globber.glob(), TAG);
sonatype-publish:
name: sonatype-publish
runs-on: 'ubuntu-24.04-16core'
# Depends on the the Android build for the Android binaries.
# Depends on the Mac, Linux, and Windows builds for host tools.
needs: [build-mac, build-linux, build-windows, build-android]
if: github.event_name == 'release' || github.event.inputs.platform == 'android'
steps:
- name: Decide Git ref
id: git_ref
run: |
REF=${RELEASE_TAG:-${GITHUB_REF}}
TAG=${REF##*/}
echo "ref=${REF}" >> $GITHUB_OUTPUT
echo "tag=${TAG}" >> $GITHUB_OUTPUT
- uses: actions/checkout@v4.1.6
with:
ref: ${{ steps.git_ref.outputs.ref }}
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- uses: ./.github/actions/linux-prereq
- name: Download Android Release
run: |
gh release download ${TAG} \
--repo ${{ github.repository }} \
--pattern 'filament-*-android-native.tgz'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.git_ref.outputs.tag }}
- name: Unzip Android Release
run: |
mkdir -p out/android-release
tar -xzvf filament-${TAG}-android-native.tgz -C out/android-release/
env:
TAG: ${{ steps.git_ref.outputs.tag }}
- name: Publish To Sonatype
run: |
cd android
./gradlew publishToSonatype closeSonatypeStagingRepository
env:
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USERNAME }}
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }}
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.MAVEN_SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.MAVEN_SIGNING_PASSWORD }}
build-ios:
name: build-ios
runs-on: macos-14-xlarge
+2
View File
@@ -6,3 +6,5 @@
appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md).
## Release notes for next branch cut
- engine: add `MaterialInstance::setConstant()` and `MaterialInstance::getConstant()` methods. These allow for per-material instance specialization constant overrides.
+5
View File
@@ -7,6 +7,11 @@ A new header is inserted each time a *tag* is created.
Instead, if you are authoring a PR for the main branch, add your release note to
[NEW_RELEASE_NOTES.md](./NEW_RELEASE_NOTES.md).
## v1.70.0
- engine: fix crash when using variance shadow maps
- materials: better shadow normal-bias calculations [⚠️ **New Material Version**]
## v1.69.5
- engine: fix crash when using variance shadow maps
+7 -1
View File
@@ -40,15 +40,21 @@
// - Build and upload artifacts with ./gradlew publish
// - Close and release staging repo on Nexus with ./gradlew closeAndReleaseStagingRepository
//
// The following is needed in ~/gradle/gradle.properties:
// The following properties need to be set (either in ~/gradle/gradle.properties, on the command
// line, or as environment variables, e.g.: ORG_GRADLE_PROJECT_property=value):
//
// sonatypeUsername=nexus_user
// sonatypePassword=nexus_password
//
// To sign with a key ring file:
// signing.keyId=pgp_key_id
// signing.password=pgp_key_password
// signing.secretKeyRingFile=/Users/user/.gnupg/maven_signing.key
//
// To sign with in-memory keys (useful for CI):,
// signingKey=ASCII armored key (begins with -----BEGIN PGP PRIVATE KEY BLOCK-----)
// signingPassword=key password
//
buildscript {
def path = providers
@@ -20,6 +20,8 @@
#include <filament/Texture.h>
#include <filament/TextureSampler.h>
#include "common/CallbackUtils.h"
#include <math/mat3.h>
#include <math/mat4.h>
#include <math/vec2.h>
@@ -246,6 +248,69 @@ Java_com_google_android_filament_MaterialInstance_nSetFloatParameterArray(JNIEnv
env->ReleaseStringUTFChars(name_, name);
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_google_android_filament_MaterialInstance_nGetConstantBool(JNIEnv *env, jclass,
jlong nativeMaterialInstance, jstring name_) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
const char *name = env->GetStringUTFChars(name_, 0);
jboolean result = instance->getConstant<bool>(name);
env->ReleaseStringUTFChars(name_, name);
return result;
}
extern "C"
JNIEXPORT jfloat JNICALL
Java_com_google_android_filament_MaterialInstance_nGetConstantFloat(JNIEnv *env, jclass,
jlong nativeMaterialInstance, jstring name_) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
const char *name = env->GetStringUTFChars(name_, 0);
jfloat result = instance->getConstant<float>(name);
env->ReleaseStringUTFChars(name_, name);
return result;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_google_android_filament_MaterialInstance_nGetConstantInt(JNIEnv *env, jclass,
jlong nativeMaterialInstance, jstring name_) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
const char *name = env->GetStringUTFChars(name_, 0);
jint result = instance->getConstant<int32_t>(name);
env->ReleaseStringUTFChars(name_, name);
return result;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_MaterialInstance_nSetConstantBool(JNIEnv *env, jclass,
jlong nativeMaterialInstance, jstring name_, jboolean x) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
const char *name = env->GetStringUTFChars(name_, 0);
instance->setConstant<bool>(name, x);
env->ReleaseStringUTFChars(name_, name);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_MaterialInstance_nSetConstantFloat(JNIEnv *env, jclass,
jlong nativeMaterialInstance, jstring name_, jfloat x) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
const char *name = env->GetStringUTFChars(name_, 0);
instance->setConstant<float>(name, x);
env->ReleaseStringUTFChars(name_, name);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_MaterialInstance_nSetConstantInt(JNIEnv *env, jclass,
jlong nativeMaterialInstance, jstring name_, jint x) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
const char *name = env->GetStringUTFChars(name_, 0);
instance->setConstant<int32_t>(name, x);
env->ReleaseStringUTFChars(name_, name);
}
// defined in TextureSampler.cpp
namespace filament::JniUtils {
TextureSampler from_long(jlong params) noexcept;
@@ -580,3 +645,17 @@ Java_com_google_android_filament_MaterialInstance_nGetTransparencyMode(JNIEnv*,
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
return (jint) instance->getTransparencyMode();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_MaterialInstance_nCompile(JNIEnv *env, jclass clazz,
jlong nativeMaterialInstance, jint priority, jint variants, jobject handler, jobject runnable) {
MaterialInstance* materialInstance = (MaterialInstance*) nativeMaterialInstance;
JniCallback* jniCallback = JniCallback::make(env, handler, runnable);
materialInstance->compile(
(MaterialInstance::CompilerPriorityQueue) priority,
(UserVariantFilterBit) variants,
jniCallback->getHandler(), [jniCallback](MaterialInstance*){
JniCallback::postToJavaAndDestroy(jniCallback);
});
}
@@ -18,6 +18,7 @@ package com.google.android.filament;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Size;
import com.google.android.filament.proguard.UsedByNative;
@@ -142,6 +143,28 @@ public class MaterialInstance {
return mMaterial;
}
/**
* Asynchronously ensures that a subset of this MaterialInstance's variants are compiled.
*
* <p>This function behaves identically to {@link Material#compile}, but takes into account
* the specific constants overridden by {@link #setConstant}.</p>
*
* @param priority Priority of the compile command.
* @param variants Variants to include to the compile command.
* @param handler An {@link java.util.concurrent.Executor Executor}. On Android this can also be a {@link android.os.Handler Handler}.
* @param callback callback called on the main thread when the compilation is done on
* by backend.
*
* @see Material#compile
* @see #setConstant
*/
public void compile(@NonNull Material.CompilerPriorityQueue priority,
int variants,
@Nullable Object handler,
@Nullable Runnable callback) {
nCompile(getNativeObject(), priority.ordinal(), variants, handler, callback);
}
/** @return the name associated with this instance */
@NonNull
public String getName() {
@@ -402,6 +425,69 @@ public class MaterialInstance {
nSetParameterFloat4(getNativeObject(), name, color[0], color[1], color[2], color[3]);
}
/**
* Overrides a specialization constant of this material instance.
*
* @param name The name of the constant as defined in the material.
* @param value The value of the constant.
* @see Material.Builder#constant
*/
public void setConstant(@NonNull String name, boolean value) {
nSetConstantBool(getNativeObject(), name, value);
}
/**
* Overrides a specialization constant of this material instance.
*
* @param name The name of the constant as defined in the material.
* @param value The value of the constant.
* @see Material.Builder#constant
*/
public void setConstant(@NonNull String name, float value) {
nSetConstantFloat(getNativeObject(), name, value);
}
/**
* Overrides a specialization constant of this material instance.
*
* @param name The name of the constant as defined in the material.
* @param value The value of the constant.
* @see Material.Builder#constant
*/
public void setConstant(@NonNull String name, int value) {
nSetConstantInt(getNativeObject(), name, value);
}
/**
* Gets the value of a specialization constant by name.
*
* @param name The name of the constant as defined in the material.
* @return The value of the constant.
*/
public boolean getConstantBoolean(@NonNull String name) {
return nGetConstantBool(getNativeObject(), name);
}
/**
* Gets the value of a specialization constant by name.
*
* @param name The name of the constant as defined in the material.
* @return The value of the constant.
*/
public float getConstantFloat(@NonNull String name) {
return nGetConstantFloat(getNativeObject(), name);
}
/**
* Gets the value of a specialization constant by name.
*
* @param name The name of the constant as defined in the material.
* @return The value of the constant.
*/
public int getConstantInt(@NonNull String name) {
return nGetConstantInt(getNativeObject(), name);
}
/**
* Set-up a custom scissor rectangle; by default it is disabled.
*
@@ -937,6 +1023,17 @@ public class MaterialInstance {
@NonNull String name, int element, @NonNull @Size(min = 1) float[] v,
@IntRange(from = 0) int offset, @IntRange(from = 1) int count);
private static native boolean nGetConstantBool(long nativeMaterialInstance, @NonNull String name);
private static native float nGetConstantFloat(long nativeMaterialInstance, @NonNull String name);
private static native int nGetConstantInt(long nativeMaterialInstance, @NonNull String name);
private static native void nSetConstantBool(long nativeMaterialInstance,
@NonNull String name, boolean x);
private static native void nSetConstantFloat(long nativeMaterialInstance,
@NonNull String name, float x);
private static native void nSetConstantInt(long nativeMaterialInstance,
@NonNull String name, int x);
private static native void nSetParameterTexture(long nativeMaterialInstance,
@NonNull String name, long nativeTexture, long sampler);
@@ -1000,4 +1097,5 @@ public class MaterialInstance {
private static native int nGetDepthFunc(long nativeMaterialInstance);
private static native void nSetTransparencyMode(long nativeMaterialInstance, int mode);
private static native int nGetTransparencyMode(long nativeMaterialInstance);
private static native void nCompile(long nativeMaterialInstance, int priority, int variants, Object handler, Runnable callback);
}
@@ -2121,7 +2121,7 @@ public class View {
*/
public boolean highPrecision = false;
/**
* VSM minimum variance scale, must be positive.
* @deprecated has no effect.
*/
public float minVarianceScale = 0.5f;
/**
+7
View File
@@ -94,6 +94,13 @@ afterEvaluate { project ->
}
signing {
def signingKey = findProperty("signingKey")
def signingPassword = findProperty("signingPassword")
if (signingKey && signingPassword) {
println("Signing with in-memory keys")
useInMemoryPgpKeys(signingKey, signingPassword)
}
publishing.publications.all { publication ->
sign publication
}
@@ -26,7 +26,8 @@ tasks.register('copyDamagedHelmetGltf', Copy) {
preBuild.dependsOn copyDamagedHelmetGltf
clean.doFirst {
delete "src/main/assets"
delete "src/main/assets/envs"
delete "src/main/assets/models"
}
android {
@@ -48,6 +49,7 @@ android {
dependencies {
implementation deps.kotlin
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.10.0'
implementation deps.coroutines.core
implementation project(':filament-android')
implementation project(':gltfio-android')
@@ -12,10 +12,11 @@
android:supportsRtl="true"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:theme="@android:style/Theme.NoTitleBar">
android:theme="@style/Theme.Material3.DayNight.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:screenOrientation="fullSensor">
<intent-filter>
@@ -0,0 +1,899 @@
{
"name": "Default Test",
"backends": [
"opengl"
],
"models": {
"DamagedHelmet": "helmet.glb"
},
"presets": [
{
"name": "base",
"models": [
"DamagedHelmet"
],
"rendering": {
"camera": {
"enabled": true,
"horizontalFov": 45.0,
"center": [
0,
0,
0
],
"lookAt": [
0,
0,
-1
]
},
"view": {
"postProcessingEnabled": true,
"dithering": "NONE"
},
"tolerance": {
"maxAbsDiff": 0.1,
"maxFailingPixelsFraction": 0.0
}
}
},
{
"name": "tilted",
"rendering": {
"camera": {
"enabled": true,
"horizontalFov": 45.0,
"center": [
-4,
-2,
-3
],
"lookAt": [
0,
0,
-4
]
}
}
}
],
"tests": [
{
"name": "basic",
"apply_presets": [
"base"
]
},
{
"name": "rotated",
"apply_presets": [
"base",
"tilted"
]
},
{
"name": "ssao",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true
}
},
{
"name": "msaa",
"apply_presets": [
"base"
],
"rendering": {
"view.msaa.enabled": true
}
},
{
"name": "bloom",
"apply_presets": [
"base",
"tilted"
],
"rendering": {
"view.bloom.enabled": true
}
},
{
"name": "aa_none",
"apply_presets": [
"base"
],
"rendering": {
"view.antiAliasing": "NONE"
}
},
{
"name": "aa_fxaa",
"apply_presets": [
"base"
],
"rendering": {
"view.antiAliasing": "FXAA"
}
},
{
"name": "dithering_none",
"apply_presets": [
"base"
],
"rendering": {
"view.dithering": "NONE"
}
},
{
"name": "msaa_8",
"apply_presets": [
"base"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.sampleCount": 8
}
},
{
"name": "taa_custom",
"apply_presets": [
"base"
],
"rendering": {
"view.taa.enabled": true,
"view.taa.feedback": 0.2,
"view.taa.jitterPattern": "HALTON_23_X16"
}
},
{
"name": "ssao_gtao",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.aoType": "GTAO"
}
},
{
"name": "ssao_sao",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.aoType": "SAO"
}
},
{
"name": "ssao_radius_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.radius": 1.0
}
},
{
"name": "ssao_radius_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.radius": 0.1
}
},
{
"name": "ssao_power_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.power": 2.0
}
},
{
"name": "ssao_power_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.power": 0.5
}
},
{
"name": "ssao_bias_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.bias": 0.05
}
},
{
"name": "ssao_resolution_half",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.resolution": 0.5
}
},
{
"name": "ssao_resolution_full",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.resolution": 1.0
}
},
{
"name": "ssao_intensity_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.intensity": 2.0
}
},
{
"name": "ssao_bent_normals",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.bentNormals": true
}
},
{
"name": "ssao_quality_ultra",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.quality": "ULTRA"
}
},
{
"name": "ssao_quality_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.quality": "LOW"
}
},
{
"name": "ssao_lowPassFilter_ultra",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.lowPassFilter": "ULTRA"
}
},
{
"name": "ssao_upsampling_ultra",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.upsampling": "ULTRA"
}
},
{
"name": "ssr_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true
}
},
{
"name": "ssr_thickness_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.5
}
},
{
"name": "ssr_bias_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.bias": 0.1
}
},
{
"name": "ssr_maxDistance_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.maxDistance": 5.0
}
},
{
"name": "ssr_stride_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.stride": 4.0
}
},
{
"name": "ssr_stride_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.stride": 1.0
}
},
{
"name": "ssr_thickness_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.01
}
},
{
"name": "ssr_maxDistance_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.maxDistance": 1.0
}
},
{
"name": "ssr_bias_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.bias": 0.001
}
},
{
"name": "ssr_quality_combo",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.2,
"view.screenSpaceReflections.stride": 2.0
}
},
{
"name": "bloom_levels_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.levels": 8
}
},
{
"name": "bloom_levels_low",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.levels": 4
}
},
{
"name": "bloom_resolution_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.resolution": 1024
}
},
{
"name": "bloom_strength_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.strength": 0.5
}
},
{
"name": "bloom_strength_low",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.strength": 0.01
}
},
{
"name": "bloom_blendMode_interpolate",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.blendMode": "INTERPOLATE"
}
},
{
"name": "bloom_no_threshold",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.threshold": false
}
},
{
"name": "bloom_quality_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.quality": "HIGH"
}
},
{
"name": "bloom_lensflare",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true
}
},
{
"name": "bloom_lensflare_no_starburst",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.starburst": false
}
},
{
"name": "bloom_lensflare_chromatic",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.chromaticAberration": 0.05
}
},
{
"name": "bloom_lensflare_ghosts",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.ghostCount": 8
}
},
{
"name": "bloom_lensflare_ghostSpacing",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.ghostSpacing": 0.8
}
},
{
"name": "bloom_halo_thick",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.haloThickness": 0.2
}
},
{
"name": "bloom_halo_radius",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.haloRadius": 0.5
}
},
{
"name": "dof_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true
}
},
{
"name": "dof_cocScale_high",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.cocScale": 2.0
}
},
{
"name": "dof_cocScale_low",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.cocScale": 0.5
}
},
{
"name": "dof_cocAspectRatio",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.cocAspectRatio": 2.0
}
},
{
"name": "dof_maxApertureDiameter",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.maxApertureDiameter": 0.05
}
},
{
"name": "dof_filter_none",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.filter": "NONE"
}
},
{
"name": "dof_nativeResolution",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.nativeResolution": true
}
},
{
"name": "dof_rings_high",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.foregroundRingCount": 5,
"view.dof.backgroundRingCount": 5
}
},
{
"name": "dof_rings_low",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.foregroundRingCount": 3,
"view.dof.backgroundRingCount": 3
}
},
{
"name": "dof_max_coc",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.maxForegroundCOC": 16,
"view.dof.maxBackgroundCOC": 16
}
},
{
"name": "fog_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true
}
},
{
"name": "fog_distance",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.distance": 10.0
}
},
{
"name": "fog_cutOffDistance",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.cutOffDistance": 100.0
}
},
{
"name": "fog_maximumOpacity",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.maximumOpacity": 0.5
}
},
{
"name": "fog_height",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.height": 5.0
}
},
{
"name": "fog_heightFalloff",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.heightFalloff": 0.5
}
},
{
"name": "fog_density_high",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.density": 0.5
}
},
{
"name": "fog_inScatteringStart",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.inScatteringStart": 5.0
}
},
{
"name": "fog_inScatteringSize",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.inScatteringSize": 10.0
}
},
{
"name": "fog_fogColorFromIbl",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.fogColorFromIbl": true
}
},
{
"name": "vignette_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true
}
},
{
"name": "vignette_midPoint",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.midPoint": 0.8
}
},
{
"name": "vignette_roundness_circle",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.roundness": 1.0
}
},
{
"name": "vignette_roundness_rect",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.roundness": 0.0
}
},
{
"name": "vignette_feather_sharp",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.feather": 0.1
}
},
{
"name": "cg_filmic",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "FILMIC"
}
},
{
"name": "cg_aces",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "ACES"
}
},
{
"name": "cg_agx",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "AGX"
}
},
{
"name": "cg_pbr_neutral",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "PBR_NEUTRAL"
}
},
{
"name": "cg_contrast_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.contrast": 1.5
}
},
{
"name": "cg_saturation_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.saturation": 1.5
}
},
{
"name": "cg_exposure_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.exposure": 1.0
}
},
{
"name": "shadow_vsm",
"apply_presets": [
"base"
],
"rendering": {
"view.shadowType": "VSM"
}
},
{
"name": "shadow_vsm_anisotropy",
"apply_presets": [
"base"
],
"rendering": {
"view.shadowType": "VSM",
"view.vsmShadowOptions.anisotropy": 2
}
},
{
"name": "shadow_vsm_highPrecision",
"apply_presets": [
"base"
],
"rendering": {
"view.shadowType": "VSM",
"view.vsmShadowOptions.highPrecision": true
}
}
]
}
@@ -17,36 +17,35 @@
package com.google.android.filament.validation
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.text.Html
import android.util.Log
import android.view.Choreographer
import android.view.SurfaceView
import android.view.View
import android.view.WindowManager
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.Spinner
import android.widget.TextView
import com.google.android.filament.utils.KTX1Loader
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
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class MainActivity : Activity(), ValidationRunner.Callback {
@@ -62,13 +61,17 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private lateinit var choreographer: Choreographer
private lateinit var modelViewer: ModelViewer
private lateinit var statusTextView: TextView
private lateinit var testResultsHeader: 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
// UI Elements
private lateinit var runButton: Button
private lateinit var loadButton: Button
private lateinit var optionsButton: Button
private var resultManager: ValidationResultManager? = null
private var validationRunner: ValidationRunner? = null
// Frame callback
@@ -89,25 +92,53 @@ class MainActivity : Activity(), ValidationRunner.Callback {
surfaceView.holder.setFixedSize(512, 512)
statusTextView = findViewById(R.id.status_text)
modeSpinner = findViewById(R.id.mode_spinner)
runButton = findViewById(R.id.run_button)
testResultsHeader = findViewById(R.id.test_results_header)
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
runButton = findViewById(R.id.run_button)
loadButton = findViewById(R.id.load_button)
optionsButton = findViewById(R.id.options_button)
// Setup Run Button
runButton.setOnClickListener {
currentInput?.let { input ->
val generateGoldens = modeSpinner.selectedItemPosition == 1
val newInput = input.copy(generateGoldens = generateGoldens)
startValidation(newInput)
// Always use the generateGoldens flag from the intent/input
startValidation(input)
}
}
// Setup Load Button
loadButton.setOnClickListener {
showLoadDialog()
}
// Setup Options Menu Button
optionsButton.setOnClickListener { view ->
val popup = android.widget.PopupMenu(this, view)
popup.menu.add(0, 1, 0, "Generate Golden")
popup.menu.add(0, 2, 0, "Export Test")
popup.menu.add(0, 3, 0, "Export Result")
popup.menu.add(0, 4, 0, "Test ADB Info")
popup.menu.add(0, 5, 0, "Result ADB Info")
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
1 -> {
currentInput?.let { input ->
val goldenInput = input.copy(generateGoldens = true)
startValidation(goldenInput)
}
}
2 -> exportTestBundleAction()
3 -> exportTestResultsAction()
4 -> showTestAdbInfo()
5 -> showResultAdbInfo()
}
true
}
popup.show()
}
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
choreographer = Choreographer.getInstance()
@@ -120,6 +151,134 @@ class MainActivity : Activity(), ValidationRunner.Callback {
handleIntent()
}
private fun showLoadDialog() {
val exportDir = getExternalFilesDir(null) ?: filesDir
// Filter out result zips (starting with "results_") to only show test bundles
val zips = exportDir.listFiles { _, name ->
name.endsWith(".zip") && !name.startsWith("results_")
}?.sortedByDescending { it.lastModified() } ?: emptyList()
if (zips.isEmpty()) {
AlertDialog.Builder(this)
.setTitle("Load Test")
.setMessage("No test bundles found.")
.setPositiveButton("OK", null)
.show()
return
}
val builder = AlertDialog.Builder(this)
builder.setTitle("Select Test Bundle")
val items = zips.map { it.name }.toTypedArray()
builder.setItems(items) { dialog, which ->
val selectedFile = zips[which]
loadZipBundle(selectedFile)
dialog.dismiss()
}
builder.setNegativeButton("Cancel", null)
builder.show()
}
private fun showTestAdbInfo() {
val exportDir = getExternalFilesDir(null) ?: filesDir
val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder()
message.append("Storage Path: $path<br><br>")
message.append("<b>--- PULL FROM DEVICE ---</b><br>")
if (isInternal) {
message.append("<tt>adb shell \"run-as $packageName cat files/&lt;filename&gt;\" &gt; &lt;filename&gt;</tt><br><br>")
} else {
message.append("<tt>adb pull $path/&lt;filename&gt; .</tt><br><br>")
}
message.append("<b>--- PUSH TO DEVICE ---</b><br>")
if (isInternal) {
message.append("1. <tt>adb push &lt;filename&gt; /sdcard/Download/</tt><br>")
message.append("2. <tt>adb shell \"run-as $packageName cp /sdcard/Download/&lt;filename&gt; files/\"</tt><br>")
} else {
message.append("<tt>adb push &lt;filename&gt; $path/</tt><br>")
}
message.append("<br>Note: Use underscores instead of spaces in &lt;filename&gt;.")
AlertDialog.Builder(this)
.setTitle("Test Bundle ADB Info")
.setMessage(Html.fromHtml(message.toString(), Html.FROM_HTML_MODE_LEGACY))
.setPositiveButton("OK", null)
.show()
}
private fun showResultAdbInfo() {
val exportDir = getExternalFilesDir(null) ?: filesDir
val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder()
message.append("<b>--- PULL RESULTS ---</b><br>")
if (isInternal) {
message.append("<tt>adb shell \"run-as $packageName cat files/&lt;filename&gt;\" &gt; &lt;filename&gt;</tt><br><br>")
} else {
message.append("<tt>adb pull $path/&lt;filename&gt; .</tt><br><br>")
}
message.append("<b>--- AVAILABLE RESULTS ---</b><br>")
val zips = exportDir.listFiles { _, name ->
name.endsWith(".zip") && name.startsWith("results_")
}?.sortedByDescending { it.lastModified() } ?: emptyList()
if (zips.isEmpty()) {
message.append("No result zips found.<br>")
} else {
zips.forEach { file ->
message.append("${file.name}<br>")
}
}
AlertDialog.Builder(this)
.setTitle("Result ADB Info")
.setMessage(Html.fromHtml(message.toString(), Html.FROM_HTML_MODE_LEGACY))
.setPositiveButton("OK", null)
.show()
}
private fun loadZipBundle(file: File) {
statusTextView.text = "Loading ${file.name}..."
CoroutineScope(Dispatchers.Main).launch {
try {
val config = inputManager.loadFromZip(file)
val baseDir = getExternalFilesDir(null) ?: filesDir
val outputDir = File(baseDir, "validation_results").apply { mkdirs() }
// Clear existing results UI and state
resultsContainer.removeAllViews()
resultManager = null
val newInput = ValidationInputManager.ValidationInput(
config = config,
outputDir = outputDir,
generateGoldens = false,
autoRun = false,
autoExport = false,
autoExportResults = false,
sourceZip = file
)
currentInput = newInput
statusTextView.text = "Loaded ${config.name}"
Log.i(TAG, "Setting header to: Test Results: ${config.name}")
testResultsHeader.text = "${config.name}"
} catch (e: Exception) {
Log.e(TAG, "Failed to load zip", e)
statusTextView.text = "Error: ${e.message}"
}
}
}
private fun createIndirectLight() {
try {
val engine = modelViewer.engine
@@ -156,12 +315,18 @@ class MainActivity : Activity(), ValidationRunner.Callback {
CoroutineScope(Dispatchers.Main).launch {
try {
val input = inputManager.resolveConfig(intent)
// Update header
Log.i(TAG, "handleIntent: Setting header to: Test Results: ${input.config.name}")
testResultsHeader.text = "${input.config.name}"
currentInput = input
// Sync spinner with intent
modeSpinner.setSelection(if (input.generateGoldens) 1 else 0)
startValidation(input)
if (input.autoRun) {
startValidation(input)
} else {
// Just show status
statusTextView.text = "Ready: ${input.config.name}"
}
} catch (e: Exception) {
Log.e(TAG, "Failed to resolve config", e)
statusTextView.text = "Error: ${e.message}"
@@ -175,6 +340,8 @@ class MainActivity : Activity(), ValidationRunner.Callback {
Log.i(TAG, "Starting validation with config: ${input.config.name}")
Log.i(TAG, "Output dir: ${input.outputDir.absolutePath}")
testResultsHeader.text = "${input.config.name}"
resultManager = ValidationResultManager(input.outputDir)
validationRunner = ValidationRunner(this, modelViewer, input.config, resultManager!!)
@@ -182,9 +349,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
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}"
@@ -196,6 +360,12 @@ class MainActivity : Activity(), ValidationRunner.Callback {
choreographer.postFrameCallback(frameScheduler)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleIntent()
}
override fun onPause() {
super.onPause()
choreographer.removeFrameCallback(frameScheduler)
@@ -221,13 +391,30 @@ class MainActivity : Activity(), ValidationRunner.Callback {
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)
// Header Layout
val headerRow = LinearLayout(this)
headerRow.orientation = LinearLayout.HORIZONTAL
headerRow.gravity = android.view.Gravity.CENTER_VERTICAL
// Status Icon + Name
val statusView = TextView(this)
val icon = if (result.passed) "✔" else "✖"
statusView.text = "$icon ${result.testName}"
statusView.setTextColor(
if (result.passed) Color.parseColor("#4CAF50") else Color.parseColor("#F44336")
)
statusView.textSize = 12f
statusView.setTypeface(null, android.graphics.Typeface.BOLD)
headerRow.addView(statusView)
// Diff Metric (only show if it's relevant/there's a diff or we just always show it like before)
val diffView = TextView(this)
diffView.text = " (Diff: ${result.diffMetric})"
diffView.textSize = 12f
diffView.setTextColor(Color.GRAY)
headerRow.addView(diffView)
resultContainer.addView(headerRow)
// Images Row
val imagesRow = LinearLayout(this)
@@ -241,7 +428,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
val labelView = TextView(this)
labelView.text = label
labelView.textSize = 12f
labelView.textSize = 9f
container.addView(labelView)
val iv = ImageView(this)
@@ -274,7 +461,46 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onAllTestsFinished() {
runOnUiThread {
statusTextView.text = "All tests finished!"
Log.i(TAG, "All tests finished")
Log.i(TAG, "All tests finished " + if (currentInput?.autoExport == true) "Exporting bundle" else "x")
if (currentInput?.autoExport == true) {
exportTestBundleAction()
}
if (currentInput?.autoExportResults == true) {
exportTestResultsAction()
}
}
}
private fun exportTestBundleAction() {
currentInput?.let { input ->
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: ValidationResultManager(input.outputDir)
val zip = rm.exportTestBundle(input.config, timestamp)
if (zip != null) {
val msg = "Exported Bundle: ${zip.name}"
statusTextView.text = msg
Log.i(TAG, "Exported test bundle to ${zip.absolutePath}")
} else {
statusTextView.text = "Export Bundle failed"
Log.e(TAG, "Export Bundle failed")
}
}
}
private fun exportTestResultsAction() {
currentInput?.let { input ->
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: ValidationResultManager(input.outputDir)
val zip = rm.exportTestResults(input.sourceZip, timestamp)
if (zip != null) {
val msg = "Exported Results: ${zip.name}"
statusTextView.text = msg
Log.i(TAG, "Exported results to ${zip.absolutePath}")
} else {
statusTextView.text = "Export Results failed"
Log.e(TAG, "Export Results failed")
}
}
}
@@ -301,50 +527,3 @@ class MainActivity : Activity(), ValidationRunner.Callback {
}
}
}
/*
* 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"
*/
@@ -26,6 +26,7 @@ import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.zip.ZipFile
/**
* Handles the retrieval and preparation of test configuration and assets.
@@ -42,7 +43,11 @@ class ValidationInputManager(private val context: Context) {
data class ValidationInput(
val config: RenderTestConfig,
val outputDir: File,
val generateGoldens: Boolean
val generateGoldens: Boolean,
val autoRun: Boolean = false,
val autoExport: Boolean = false,
val autoExportResults: Boolean = false,
val sourceZip: File? = null
)
/**
@@ -53,22 +58,111 @@ class ValidationInputManager(private val context: Context) {
val testConfigPath = intent.getStringExtra("test_config")
val urlConfig = intent.getStringExtra("url_config")
val urlModelsBase = intent.getStringExtra("url_models_base")
val zipPath = intent.getStringExtra("zip_path")
val generateGoldens = intent.getBooleanExtra("generate_goldens", false)
val autoRun = intent.getBooleanExtra("auto_run", false)
val autoExport = intent.getBooleanExtra("auto_export", false)
val autoExportResults = intent.getBooleanExtra("auto_export_results", false)
val outputPath = intent.getStringExtra("output_path")
val outputDir = if (outputPath != null) {
File(outputPath).apply { mkdirs() }
Log.i(TAG, "Resolving config with outputPath: $outputPath")
val baseDir = context.getExternalFilesDir(null) ?: context.filesDir
Log.i(TAG, "Base directory for resolution: ${baseDir.absolutePath}")
val outputDir = if (!outputPath.isNullOrBlank()) {
val file = File(outputPath)
val resolved = if (file.isAbsolute) {
file
} else {
File(baseDir, outputPath)
}
// Critical check: if resolved is root or very short, it's likely wrong
if (resolved.absolutePath == "/" || resolved.parent == null) {
Log.w(TAG, "Resolved outputDir is root ($resolved), defaulting to app-specific dir")
File(baseDir, "validation_results")
} else {
resolved
}
} else {
File(context.getExternalFilesDir(null), "validation_results").apply { mkdirs() }
File(baseDir, "validation_results")
}
if (!outputDir.exists() && !outputDir.mkdirs()) {
Log.e(TAG, "Failed to create outputDir: ${outputDir.absolutePath}")
}
Log.i(TAG, "Final outputDir: ${outputDir.absolutePath}")
val sourceZipFile = if (zipPath != null) {
val file = File(zipPath)
if (file.isAbsolute) {
file
} else {
File(baseDir, zipPath)
}
} else {
null
}
val config = when {
sourceZipFile != null && sourceZipFile.exists() -> loadFromZip(sourceZipFile)
urlConfig != null -> downloadConfig(urlConfig, urlModelsBase)
testConfigPath != null -> ConfigParser.parseFromPath(testConfigPath)
else -> extractDefaultAssets()
}
return@withContext ValidationInput(config, outputDir, generateGoldens)
return@withContext ValidationInput(config, outputDir, generateGoldens, autoRun, autoExport, autoExportResults, sourceZipFile)
}
suspend fun loadFromZip(zipFile: File): RenderTestConfig {
Log.i(TAG, "Unzipping validation bundle: ${zipFile.absolutePath}")
// Use a unique cache dir based on timestamp or just overwrite a common one
// Overwriting is safer to avoid filling up disk, but we must ensure we don't conflict with current run
val baseCacheDir = context.externalCacheDir ?: context.cacheDir
val cacheDir = File(baseCacheDir, "unzipped_validation")
if (cacheDir.exists()) cacheDir.deleteRecursively()
cacheDir.mkdirs()
Log.i(TAG, "Using cacheDir: ${cacheDir.absolutePath}")
ZipFile(zipFile).use { zip ->
val entries = zip.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
val entryFile = File(cacheDir, entry.name)
// specific check to avoid zip slip vulnerability (though low risk here)
if (!entryFile.canonicalPath.startsWith(cacheDir.canonicalPath)) {
throw SecurityException("Zip entry is outside of the target dir: ${entry.name}")
}
if (entry.isDirectory) {
entryFile.mkdirs()
} else {
entryFile.parentFile?.mkdirs()
zip.getInputStream(entry).use { input ->
FileOutputStream(entryFile).use { output ->
input.copyTo(output)
}
}
}
}
}
// Find config.json
// We look for a file ending in .json within the unzipped structure
// Exclude results.json if it happened to be there
val jsonFiles = cacheDir.walkTopDown()
.filter { it.isFile && it.extension == "json" && it.name != "results.json" }
.toList()
if (jsonFiles.isEmpty()) throw IllegalStateException("No config.json found in zip")
// Prefer one named config.json or take the first one
val configFile = jsonFiles.find { it.name == "config.json" } ?: jsonFiles.first()
Log.i(TAG, "Parsed config from ${configFile.absolutePath}")
return ConfigParser.parseFromPath(configFile.absolutePath)
}
private suspend fun extractDefaultAssets(): RenderTestConfig {
@@ -90,9 +184,9 @@ class ValidationInputManager(private val context: Context) {
// Copy DamagedHelmet.glb
val modelsDir = File(filesDir, "models")
modelsDir.mkdirs()
val modelOut = File(modelsDir, "DamagedHelmet.glb")
val modelOut = File(modelsDir, "helmet.glb")
assetManager.open("DamagedHelmet.glb").use { input ->
assetManager.open("models/helmet.glb").use { input ->
FileOutputStream(modelOut).use { output ->
input.copyTo(output)
}
@@ -67,22 +67,201 @@ class ValidationResultManager(private val outputDir: File) {
fun finalizeResults(): File? {
// Write results JSON
writeResultsJson()
return null
}
/**
* Exports a zip containing:
* - results.json
* - input test bundle (as nested zip), if provided
* - diff images (if any failure)
*/
fun exportTestResults(sourceZip: File?, timestamp: String): File? {
// Safe parent dir resolution
val parentDir = outputDir.canonicalFile.parentFile ?: outputDir.parentFile
if (parentDir == null) return null
val resultZipName = "results_$timestamp"
val zipFile = File(parentDir, "$resultZipName.zip")
Log.i(TAG, "Exporting results to ${zipFile.absolutePath}")
// 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) }
// 1. Add results.json
val resultsJson = File(outputDir, "results.json")
if (resultsJson.exists()) {
zos.putNextEntry(ZipEntry("results.json"))
resultsJson.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
// 2. Add source zip if exists
if (sourceZip != null && sourceZip.exists()) {
zos.putNextEntry(ZipEntry(sourceZip.name))
sourceZip.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
// 3. Add diff images (any file ending in _diff.png in outputDir)
outputDir.listFiles { _, name -> name.endsWith("_diff.png") }?.forEach { diffFile ->
zos.putNextEntry(ZipEntry(diffFile.name))
diffFile.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
Log.i(TAG, "Zipped results to ${zipFile.absolutePath}")
Log.i(TAG, "Exported results to ${zipFile.absolutePath}")
return zipFile
} catch (e: Exception) {
Log.e(TAG, "Failed to zip results", e)
Log.e(TAG, "Failed to export results", e)
return null
}
}
/**
* Exports a zip bundle containing:
* - Modified config.json (with updated name and relative paths)
* - Models (in models/ subdirectory)
* - Golden images (in goldens/ subdirectory)
*
* Structure:
* test_name_TIMESTAMP/
* config.json
* models/
* model.glb
* goldens/
* test_result.png
*/
fun exportTestBundle(config: RenderTestConfig, timestamp: String): File? {
Log.i(TAG, "Starting exportTestBundle for ${config.name} at $timestamp")
Log.i(TAG, "OutputDir: ${outputDir.absolutePath}")
val parentDir = outputDir.canonicalFile.parentFile ?: outputDir.parentFile
if (parentDir == null) {
Log.e(TAG, "OutputDir parent is null: ${outputDir.absolutePath}")
return null
}
Log.i(TAG, "Using parentDir for export: ${parentDir.absolutePath}")
val testNameWithTimestamp = "${config.name}_$timestamp"
val exportNameNoSpaces = testNameWithTimestamp.replace(" ", "_")
val exportDir = File(parentDir, "export_temp_$timestamp")
Log.i(TAG, "Creating export temp dir: ${exportDir.absolutePath}")
if (exportDir.exists()) exportDir.deleteRecursively()
if (!exportDir.mkdirs()) {
Log.e(TAG, "Failed to create export dir: ${exportDir.absolutePath}")
return null
}
val rootDir = File(exportDir, exportNameNoSpaces)
rootDir.mkdirs()
val modelsDir = File(rootDir, "models")
modelsDir.mkdirs()
val goldensDir = File(rootDir, "goldens")
goldensDir.mkdirs()
try {
// 1. Copy Models and update config map
val newModelsMap = mutableMapOf<String, String>()
Log.i(TAG, "Copying models...")
for ((modelName, modelPath) in config.models) {
val sourceFile = File(modelPath)
if (sourceFile.exists()) {
val destFile = File(modelsDir, sourceFile.name)
Log.d(TAG, "Copying model $modelName: $modelPath -> ${destFile.name}")
sourceFile.copyTo(destFile, overwrite = true)
// Use relative path for the new config
newModelsMap[modelName] = "models/${sourceFile.name}"
} else {
Log.w(TAG, "Model file not found: $modelPath")
}
}
// 2. Copy Golden Images
// We assume goldens are in outputDir with .png extension
Log.i(TAG, "Copying goldens from ${outputDir.absolutePath}...")
outputDir.listFiles { _, name -> name.endsWith(".png") }?.forEach { file ->
Log.d(TAG, "Copying golden: ${file.name}")
file.copyTo(File(goldensDir, file.name), overwrite = true)
}
// 3. Create modified config JSON
Log.i(TAG, "Creating config.json...")
val newConfigJson = JSONObject()
newConfigJson.put("name", testNameWithTimestamp) // Keep spaces in JSON name
// Reconstruct backends
val backendsArray = JSONArray()
config.backends.forEach { backendsArray.put(it) }
newConfigJson.put("backends", backendsArray)
// Reconstruct models
val modelsJson = JSONObject()
for ((k, v) in newModelsMap) {
modelsJson.put(k, v)
}
newConfigJson.put("models", modelsJson)
// Reconstruct tests
val testsArray = JSONArray()
for (test in config.tests) {
val testJson = JSONObject()
testJson.put("name", test.name)
if (test.description != null) testJson.put("description", test.description)
// Models for this test (set of strings)
val testModelsArray = JSONArray()
test.models.forEach { testModelsArray.put(it) }
testJson.put("models", testModelsArray)
// Rendering settings
testJson.put("rendering", test.rendering)
// Tolerance
if (test.tolerance != null) {
testJson.put("tolerance", test.tolerance)
}
// Backends (optional override)
val testBackends = JSONArray()
test.backends.forEach { testBackends.put(it) }
testJson.put("backends", testBackends)
testsArray.put(testJson)
}
newConfigJson.put("tests", testsArray)
// Write config.json
File(rootDir, "config.json").writeText(newConfigJson.toString(4))
// 4. Zip it
val zipFile = File(parentDir, "$exportNameNoSpaces.zip")
Log.i(TAG, "Zipping to ${zipFile.absolutePath}...")
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
rootDir.walkTopDown().forEach { file ->
if (file.isFile) {
val entryName = file.relativeTo(exportDir).path
zos.putNextEntry(ZipEntry(entryName))
file.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
}
// Cleanup temp dir
exportDir.deleteRecursively()
Log.i(TAG, "Exported test bundle to ${zipFile.absolutePath}")
return zipFile
} catch (e: Exception) {
Log.e(TAG, "Failed to export test bundle", e)
exportDir.deleteRecursively()
return null
}
}
@@ -41,18 +41,13 @@ class ValidationRunner(
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
RUNNING_TEST
}
interface Callback {
@@ -93,8 +88,6 @@ class ValidationRunner(
nextModel()
return
}
currentState = State.LOADING_MODEL
callback?.onStatusChanged("Loading $modelName for ${currentTestConfig?.name}")
// Load model on main thread (required by ModelViewer)
@@ -110,51 +103,24 @@ class ValidationRunner(
Log.i("ValidationRunner", "Loading GLB buffer... (${bytes.size} bytes)")
val buffer = ByteBuffer.wrap(bytes)
modelViewer.loadModelGlb(buffer)
Log.i("ValidationRunner", "Model loaded. initializing fence.")
Log.i("ValidationRunner", "Model loaded.")
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")
currentState = State.WAITING_FOR_RESOURCES
frameCounter = 0
Log.i("ValidationRunner", "State set to WAITING_FOR_RESOURCES")
} catch (e: Exception) {
Log.e("ValidationRunner", "Failed to load $path", e)
nextModel()
Log.e("ValidationRunner", "Failed to load $path", e)
nextModel()
}
}
fun onFrame(frameTimeNanos: Long) {
if (frameCounter % 60 == 0) {
Log.i("ValidationRunner", "onFrame: $currentState (frame: $frameCounter)")
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) {
modelViewer.engine.destroyFence(fence)
loadStartFence = null
// Compile materials (simplified)
modelViewer.scene.forEach { entity ->
// ... existing material compilation logic ...
}
currentState = State.WAITING_FOR_RESOURCES
}
}
}
State.WAITING_FOR_RESOURCES -> {
val progress = modelViewer.progress
if (progress >= 1.0f) {
@@ -166,12 +132,12 @@ class ValidationRunner(
State.WARMUP -> {
frameCounter++
if (frameCounter > 5) { // 5 frames warmup
startAutomation()
startAutomation()
}
}
State.RUNNING_TEST -> {
// Log.i("ValidationRunner", "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
@@ -186,13 +152,11 @@ class ValidationRunner(
if (engine.shouldClose()) {
Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)")
// Test finished (for this spec)
currentState = State.COMPARING
currentState = State.IDLE
captureAndCompare()
}
}
}
State.COMPARING -> {} // Busy
State.LOADING_MODEL -> {}
}
}
@@ -233,7 +197,36 @@ class ValidationRunner(
// Golden path
val modelFile = File(config.models.get(modelName)!!)
val goldenFile = modelFile.parentFile!!.parentFile!!.resolve("golden/${testFullName}.png")
val modelParent = modelFile.parentFile!!
// Search for golden in:
// 1. ../golden/ (standard structure)
// 2. ../goldens/ (exported structure, sibling of models)
// 3. ./goldens/ (backup)
val searchPaths = mutableListOf<File>()
modelParent.parentFile?.let {
searchPaths.add(it.resolve("golden"))
searchPaths.add(it.resolve("goldens"))
}
searchPaths.add(modelParent.resolve("goldens"))
var goldenFile: File? = null
for (path in searchPaths) {
val f = path.resolve("${testFullName}.png")
if (f.exists()) {
goldenFile = f
break
}
}
if (goldenFile != null) {
Log.i("ValidationRunner", "Found golden at ${goldenFile.absolutePath}")
} else {
Log.w("ValidationRunner", "Golden not found for $testFullName. Searched in: ${searchPaths.joinToString { it.absolutePath }}")
// Fallback to old behavior for reference if everything else fails
goldenFile = modelParent.parentFile?.resolve("golden/${testFullName}.png") ?: File("nonexistent")
}
Thread {
try {
@@ -245,14 +238,15 @@ class ValidationRunner(
var diffMetric = 0f
if (generateGoldens) {
goldenFile.parentFile?.mkdirs()
FileOutputStream(goldenFile).use { out ->
val targetGolden = goldenFile ?: modelParent.parentFile?.resolve("golden/${testFullName}.png") ?: File(modelParent, "golden/${testFullName}.png")
targetGolden.parentFile?.mkdirs()
FileOutputStream(targetGolden).use { out ->
flipped.compress(Bitmap.CompressFormat.PNG, 100, out)
}
passed = true // Generating goldens always passes if successful
callback?.onStatusChanged("Golden generated")
} else {
if (goldenFile.exists()) {
if (goldenFile != null && goldenFile.exists()) {
val golden = android.graphics.BitmapFactory.decodeFile(goldenFile.absolutePath)
if (golden != null) {
callback?.onImageResult("Golden", golden)
@@ -273,7 +267,7 @@ class ValidationRunner(
callback?.onStatusChanged("Failed to load golden")
}
} else {
Log.w("ValidationRunner", "Golden not found: ${goldenFile.absolutePath}")
Log.w("ValidationRunner", "Golden not found: ${goldenFile?.absolutePath}")
callback?.onStatusChanged("Golden not found")
}
}
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
<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"
@@ -10,6 +10,8 @@
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintWidth_percent="0.6"
android:layout_marginTop="32dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
@@ -20,10 +22,83 @@
android:layout_height="match_parent" />
</FrameLayout>
<LinearLayout
android:id="@+id/controls_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingTop="20dp"
android:paddingBottom="0dp"
app:layout_constraintTop_toBottomOf="@id/surface_container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Initializing..."
android:textSize="12sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/run_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/Widget.Material3.Button"
android:text="Run Test" />
<com.google.android.material.button.MaterialButton
android:id="@+id/load_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.TonalButton"
android:text="Load Test"
android:layout_marginStart="8dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/options_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.IconButton.Outlined"
app:icon="@android:drawable/ic_menu_more"
android:contentDescription="More Options"
android:layout_marginStart="8dp"/>
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/test_results_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Test Results"
android:gravity="center"
android:textSize="14sp"
android:paddingTop="0dp"
android:paddingBottom="8dp" />
</LinearLayout>
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/surface_container"
app:layout_constraintTop_toBottomOf="@id/controls_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
@@ -32,41 +107,9 @@
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" />
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingBottom="20dp">
<LinearLayout
android:id="@+id/results_container"
@@ -74,7 +117,6 @@
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</ScrollView>
+2 -2
View File
@@ -181,7 +181,7 @@ important for <code>matc</code> (material compiler).</p>
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.69.3'
implementation 'com.google.android.filament:filament-android:1.69.5'
}
</code></pre>
<p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p>
@@ -195,7 +195,7 @@ dependencies {
</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.3'
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.69.5'
</code></pre>
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
<ul>
+2 -1
View File
@@ -225,9 +225,10 @@ into <strong>branch</strong> of <code>filament-assets</code>. This branch is pai
<code>filament</code> repo.</p>
<p>As an example, imagine I am working on a PR, and I've uploaded my change, which is in a
branch called <code>my-pr-branch</code>, to <code>filament</code>. This PR requires updating the golden. We would do
it in the following fashion</p>
it in the following fashion on a macOS machine:</p>
<h3 id="using-a-script-to-update-the-golden-repo"><a class="header" href="#using-a-script-to-update-the-golden-repo">Using a script to update the golden repo</a></h3>
<ul>
<li>Make sure you've completed the steps in 'Setting up python'</li>
<li>Run interactive mode in the <code>update_golden.py</code> script.
<pre><code>python3 test/renderdiff/src/update_golden.py
</code></pre>
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -93,6 +93,7 @@ set(SRCS
src/MorphTargetBuffer.cpp
src/PostProcessManager.cpp
src/ProgramSpecialization.cpp
src/LocalProgramCache.cpp
src/RenderPass.cpp
src/RenderPrimitive.cpp
src/RenderTarget.cpp
@@ -186,6 +187,7 @@ set(PRIVATE_HDRS
src/MaterialCache.h
src/MaterialDefinition.h
src/MaterialParser.h
src/LocalProgramCache.h
src/MaterialInstanceManager.h
src/PIDController.h
src/PostProcessManager.h
+153 -153
View File
@@ -564,170 +564,170 @@ endif()
# ==================================================================================================
# Test
# ==================================================================================================
option(INSTALL_BACKEND_TEST "Install the backend test library so it can be consumed on iOS" OFF)
if (FILAMENT_BUILD_TESTING)
option(INSTALL_BACKEND_TEST "Install the backend test library so it can be consumed on iOS" OFF)
if (APPLE OR LINUX)
set(BACKEND_TEST_SRC
test/BackendTest.cpp
test/ShaderGenerator.cpp
test/TrianglePrimitive.cpp
test/Arguments.cpp
test/ImageExpectations.cpp
test/Lifetimes.cpp
test/PlatformRunner.cpp
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
test/test_ReadPixels.cpp
test/test_ReadTexture.cpp
test/test_BufferUpdates.cpp
test/test_Callbacks.cpp
test/test_JobQueue.cpp
test/test_MemoryMappedBuffer.cpp
test/test_MsaaSwapChain.cpp
test/test_MRT.cpp
test/test_PushConstants.cpp
test/test_LoadImage.cpp
test/test_StencilBuffer.cpp
test/test_Scissor.cpp
test/test_MipLevels.cpp
test/test_Handles.cpp
test/test_CircularBuffer.cpp
test/test_CommandBufferQueue.cpp
test/test_Template.cpp
)
if (FILAMENT_SUPPORTS_WEBGPU)
list(APPEND BACKEND_TEST_SRC
test/test_WebGPUAsyncTaskCounter.cpp
test/test_WebGPUComposeSwizzle.cpp)
endif()
if (APPLE)
# Metal-specific tests
list(APPEND BACKEND_TEST_SRC
test/test_MetalBlitter.mm
if (APPLE OR LINUX)
set(BACKEND_TEST_SRC
test/BackendTest.cpp
test/ShaderGenerator.cpp
test/TrianglePrimitive.cpp
test/Arguments.cpp
test/ImageExpectations.cpp
test/Lifetimes.cpp
test/PlatformRunner.cpp
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
test/test_ReadPixels.cpp
test/test_ReadTexture.cpp
test/test_BufferUpdates.cpp
test/test_Callbacks.cpp
test/test_JobQueue.cpp
test/test_MemoryMappedBuffer.cpp
test/test_MsaaSwapChain.cpp
test/test_MRT.cpp
test/test_PushConstants.cpp
test/test_LoadImage.cpp
test/test_StencilBuffer.cpp
test/test_Scissor.cpp
test/test_MipLevels.cpp
test/test_Handles.cpp
test/test_CircularBuffer.cpp
test/test_CommandBufferQueue.cpp
test/test_Template.cpp
)
endif()
set(BACKEND_TEST_LIBS
absl::str_format
backend
getopt
gtest
imageio
filamat
SPIRV
spirv-cross-glsl)
# Create input/output directories for test result images.
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/actual_images)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/diff_images)
file(COPY test/expected_images DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/images)
endif()
# TODO: Disabling IOS test due to breakage wrt glslang update
if (APPLE AND NOT IOS)
# TODO: we should expand this test to Linux and other platforms.
list(APPEND BACKEND_TEST_SRC
test/test_RenderExternalImage.cpp)
add_library(backend_test STATIC ${BACKEND_TEST_SRC})
target_link_libraries(backend_test PUBLIC ${BACKEND_TEST_LIBS})
target_compile_options(backend_test PRIVATE "-fobjc-arc")
set(BACKEND_TEST_DEPS
OSDependent
SPIRV
SPIRV-Tools
SPIRV-Tools-opt
backend_test
if (FILAMENT_SUPPORTS_WEBGPU)
list(APPEND BACKEND_TEST_SRC
test/test_WebGPUAsyncTaskCounter.cpp
test/test_WebGPUComposeSwizzle.cpp)
endif()
if (APPLE)
# Metal-specific tests
list(APPEND BACKEND_TEST_SRC
test/test_MetalBlitter.mm
)
endif()
set(BACKEND_TEST_LIBS
absl::str_format
backend
getopt
gtest
glslang
spirv-cross-core
spirv-cross-glsl
spirv-cross-msl)
if (NOT IOS)
target_link_libraries(backend_test PRIVATE image imageio)
list(APPEND BACKEND_TEST_DEPS image)
endif()
if (FILAMENT_SUPPORTS_WEBGPU)
target_link_libraries(backend_test PRIVATE webgpu_dawn dawncpp_headers)
list(APPEND BACKEND_TEST_DEPS webgpu_dawn dawncpp_headers)
imageio
filamat
SPIRV
spirv-cross-glsl)
# Create input/output directories for test result images.
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/actual_images)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/diff_images)
file(COPY test/expected_images DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/images)
endif()
set(BACKEND_TEST_COMBINED_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libbackendtest_combined.a")
combine_static_libs(backend_test "${BACKEND_TEST_COMBINED_OUTPUT}" "${BACKEND_TEST_DEPS}")
# TODO: Disabling IOS test due to breakage wrt glslang update
if (APPLE AND NOT IOS)
# TODO: we should expand this test to Linux and other platforms.
list(APPEND BACKEND_TEST_SRC
test/test_RenderExternalImage.cpp)
add_library(backend_test STATIC ${BACKEND_TEST_SRC})
target_link_libraries(backend_test PUBLIC ${BACKEND_TEST_LIBS})
target_compile_options(backend_test PRIVATE "-fobjc-arc")
set(BACKEND_TEST_LIB_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}backend_test${CMAKE_STATIC_LIBRARY_SUFFIX})
set(BACKEND_TEST_DEPS
OSDependent
SPIRV
SPIRV-Tools
SPIRV-Tools-opt
backend_test
getopt
gtest
glslang
spirv-cross-core
spirv-cross-glsl
spirv-cross-msl)
if (INSTALL_BACKEND_TEST)
install(FILES "${BACKEND_TEST_COMBINED_OUTPUT}" DESTINATION lib/${DIST_DIR} RENAME ${BACKEND_TEST_LIB_NAME})
install(FILES test/PlatformRunner.h DESTINATION include/backend_test)
if (NOT IOS)
target_link_libraries(backend_test PRIVATE image imageio)
list(APPEND BACKEND_TEST_DEPS image)
endif()
if (FILAMENT_SUPPORTS_WEBGPU)
target_link_libraries(backend_test PRIVATE webgpu_dawn dawncpp_headers)
list(APPEND BACKEND_TEST_DEPS webgpu_dawn dawncpp_headers)
endif()
set(BACKEND_TEST_COMBINED_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libbackendtest_combined.a")
combine_static_libs(backend_test "${BACKEND_TEST_COMBINED_OUTPUT}" "${BACKEND_TEST_DEPS}")
set(BACKEND_TEST_LIB_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}backend_test${CMAKE_STATIC_LIBRARY_SUFFIX})
if (INSTALL_BACKEND_TEST)
install(FILES "${BACKEND_TEST_COMBINED_OUTPUT}" DESTINATION lib/${DIST_DIR} RENAME ${BACKEND_TEST_LIB_NAME})
install(FILES test/PlatformRunner.h DESTINATION include/backend_test)
endif()
set_target_properties(backend_test PROPERTIES FOLDER Tests)
if (APPLE AND NOT IOS)
add_executable(backend_test_mac test/mac_runner.mm)
target_link_libraries(backend_test_mac PRIVATE "-framework Metal -framework AppKit -framework QuartzCore")
# Because each test case is a separate file, the -force_load flag is necessary to prevent the
# linker from removing "unused" symbols.
target_link_libraries(backend_test_mac PRIVATE -force_load backend_test)
set_target_properties(backend_test_mac PROPERTIES FOLDER Tests)
# This is needed after XCode 15.3
set_target_properties(backend_test_mac PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE)
set_target_properties(backend_test_mac PROPERTIES INSTALL_RPATH /usr/local/lib)
endif()
endif()
set_target_properties(backend_test PROPERTIES FOLDER Tests)
if (LINUX)
add_executable(backend_test_linux test/linux_runner.cpp ${BACKEND_TEST_SRC})
target_link_libraries(backend_test_linux PRIVATE ${BACKEND_TEST_LIBS})
set_target_properties(backend_test_linux PROPERTIES FOLDER Tests)
if (FILAMENT_SUPPORTS_WEBGPU)
target_link_libraries(backend_test_linux PRIVATE webgpu_dawn dawncpp_headers)
endif()
endif()
# ==================================================================================================
# Compute tests
#
#if (NOT IOS AND NOT WEBGL)
#
#add_executable(compute_test
# test/ComputeTest.cpp
# test/Arguments.cpp
# test/test_ComputeBasic.cpp
# )
#
#target_link_libraries(compute_test PRIVATE
# backend
# getopt
# gtest
# )
#
#set_target_properties(compute_test PROPERTIES FOLDER Tests)
#
#endif()
# ==================================================================================================
# Metal utils tests
if (APPLE AND NOT IOS)
add_executable(backend_test_mac test/mac_runner.mm)
target_link_libraries(backend_test_mac PRIVATE "-framework Metal -framework AppKit -framework QuartzCore")
# Because each test case is a separate file, the -force_load flag is necessary to prevent the
# linker from removing "unused" symbols.
target_link_libraries(backend_test_mac PRIVATE -force_load backend_test)
set_target_properties(backend_test_mac PROPERTIES FOLDER Tests)
add_executable(metal_utils_test test/MetalTest.mm)
# This is needed after XCode 15.3
set_target_properties(backend_test_mac PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE)
set_target_properties(backend_test_mac PROPERTIES INSTALL_RPATH /usr/local/lib)
target_compile_options(metal_utils_test PRIVATE "-fobjc-arc")
target_link_libraries(metal_utils_test PRIVATE
backend
getopt
gtest
)
set_target_properties(metal_utils_test PROPERTIES FOLDER Tests)
endif()
endif()
if (LINUX)
add_executable(backend_test_linux test/linux_runner.cpp ${BACKEND_TEST_SRC})
target_link_libraries(backend_test_linux PRIVATE ${BACKEND_TEST_LIBS})
set_target_properties(backend_test_linux PROPERTIES FOLDER Tests)
if (FILAMENT_SUPPORTS_WEBGPU)
target_link_libraries(backend_test_linux PRIVATE webgpu_dawn dawncpp_headers)
endif()
endif()
# ==================================================================================================
# Compute tests
#
#if (NOT IOS AND NOT WEBGL)
#
#add_executable(compute_test
# test/ComputeTest.cpp
# test/Arguments.cpp
# test/test_ComputeBasic.cpp
# )
#
#target_link_libraries(compute_test PRIVATE
# backend
# getopt
# gtest
# )
#
#set_target_properties(compute_test PROPERTIES FOLDER Tests)
#
#endif()
# ==================================================================================================
# Metal utils tests
if (APPLE AND NOT IOS)
add_executable(metal_utils_test test/MetalTest.mm)
target_compile_options(metal_utils_test PRIVATE "-fobjc-arc")
target_link_libraries(metal_utils_test PRIVATE
backend
getopt
gtest
)
set_target_properties(metal_utils_test PROPERTIES FOLDER Tests)
endif()
endif()
+20 -2
View File
@@ -25,6 +25,11 @@
#include <mutex>
#ifdef __ANDROID__
#include <pthread.h>
#endif
namespace filament {
using namespace utils;
@@ -105,8 +110,21 @@ JNIEnv* VirtualMachineEnv::getEnvironmentSlow() {
FILAMENT_CHECK_PRECONDITION(mVirtualMachine)
<< "JNI_OnLoad() has not been called";
#if defined(__ANDROID__)
jint const result = mVirtualMachine->AttachCurrentThread(&mJniEnv, nullptr);
#ifdef __ANDROID__
JavaVMAttachArgs args;
args.version = JNI_VERSION_1_6;
args.group = nullptr;
char threadName[16]; // pthread_getname_np returns at most 16 bytes
if (__builtin_available(android 26, *)) {
if (pthread_getname_np(pthread_self(), threadName, sizeof(threadName)) == 0) {
args.name = threadName;
} else {
args.name = nullptr;
}
} else {
args.name = nullptr;
}
jint const result = mVirtualMachine->AttachCurrentThread(&mJniEnv, &args);
#else
jint const result = mVirtualMachine->AttachCurrentThread(reinterpret_cast<void**>(&mJniEnv), nullptr);
#endif
+1 -1
View File
@@ -91,7 +91,7 @@ public:
GLenum getIndicesType() const noexcept {
return indicesType;
}
} gl;
};
static bool queryOpenGLVersion(GLint* major, GLint* minor) noexcept;
@@ -29,6 +29,12 @@ using namespace bluevk;
namespace filament::backend {
std::shared_ptr<VulkanCmdFence> VulkanCmdFence::completed() noexcept {
auto cmdFence = std::make_shared<VulkanCmdFence>(VK_NULL_HANDLE);
cmdFence->mStatus = VK_SUCCESS;
return cmdFence;
}
FenceStatus VulkanCmdFence::wait(VkDevice device, uint64_t const timeout,
std::chrono::steady_clock::time_point const until) {
@@ -40,6 +40,13 @@ struct VulkanCmdFence {
explicit VulkanCmdFence(VkFence fence) : mFence(fence) { }
~VulkanCmdFence() = default;
// Creates a VulkanCmdFence with its status set to VK_SUCCESS. It holds
// a null handle; it is assumed that any user of this object will avoid
// using the fence handle directly if getStatus() returns VK_SUCCESS, as
// in that case, it's likely the fence is being reused for other passes,
// and is not in the expected state anyway.
static std::shared_ptr<VulkanCmdFence> completed() noexcept;
void setStatus(VkResult const value) {
std::lock_guard const l(mLock);
mStatus = value;
+5 -1
View File
@@ -277,7 +277,11 @@ private:
fvkmemory::resource_ptr<VulkanSemaphore> mLastSubmit;
VkFence mLastFence = VK_NULL_HANDLE;
std::shared_ptr<VulkanCmdFence> mLastFenceStatus;
// Start out with a completed fence, because if no commands have
// been queued or submited, then by definition, all pending work
// is complete.
std::shared_ptr<VulkanCmdFence> mLastFenceStatus =
VulkanCmdFence::completed();
VkPipelineStageFlags mInjectedDependencyWaitStage = 0;
};
@@ -355,35 +355,8 @@ 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 {
// 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
// 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
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;
samplers.unset(binding);
// Each bitmask denotes a binding index, and separated into two stages - vertex and buffer
// We fold the two stages into just the lower half of the bits to denote a combined set of
// bindings.
Bitmask const copyBindings = foldBitsInHalf(ubo | samplers);
VkDescriptorSet const srcSet = set->getVkSet();
copySet(srcSet, newSet, copyBindings);
set->addNewSet(newSet,
[this, layoutCount = layout->count, genLayout, newSet](VulkanDescriptorSet*) {
this->manualRecycle(layoutCount, genLayout, newSet);
});
}
uint8_t binding, fvkmemory::resource_ptr<VulkanTexture> texture,
VkSampler sampler, VkDescriptorSetLayout externalSamplerLayout) noexcept {
VkDescriptorSet const vkset = set->getVkSet();
VkImageSubresourceRange range = texture->getPrimaryViewRange();
VkImageViewType const expectedType = texture->getViewType();
@@ -432,6 +405,28 @@ fvkmemory::resource_ptr<VulkanDescriptorSet> VulkanDescriptorSetCache::createSet
return set;
}
void VulkanDescriptorSetCache::cloneSet(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
fvkutils::SamplerBitmask samplerMask) noexcept {
auto const& layout = set->getLayout();
// Build a new descriptor set from layout
VkDescriptorSetLayout const genLayout = set->boundLayout;
VkDescriptorSet const newSet = getVkSet(layout->count, genLayout);
// Each bitmask denotes a binding index, and separated into two stages - vertex and buffer
// We fold the two stages into just the lower half of the bits to denote a combined set of
// bindings.
Bitmask const ubo = layout->bitmask.ubo | layout->bitmask.dynamicUbo;
// Don't copy the samplers in the mask
Bitmask samplers = layout->bitmask.sampler ^ samplerMask;
Bitmask const copyBindings = foldBitsInHalf(ubo | samplers);
VkDescriptorSet const srcSet = set->getVkSet();
copySet(srcSet, newSet, copyBindings);
set->addNewSet(newSet,
[this, layoutCount = layout->count, genLayout, newSet](
VulkanDescriptorSet*) { this->manualRecycle(layoutCount, genLayout, newSet); });
}
VkDescriptorSet VulkanDescriptorSetCache::getVkSet(DescriptorCount const& count,
VkDescriptorSetLayout vklayout) {
return mDescriptorPool->obtainSet(count, vklayout);
@@ -76,6 +76,11 @@ public:
fvkmemory::resource_ptr<VulkanDescriptorSet> createSet(Handle<HwDescriptorSet> handle,
fvkmemory::resource_ptr<VulkanDescriptorSetLayout> layout);
// Create and set as current a new VkDescriptorSet using the `set` currently bound layout and
// copy all the bindings and ignoring the samplers bindings in the `samplerMask`.
void cloneSet(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
fvkutils::SamplerBitmask samplerMask) noexcept;
// This method is meant to be used with external samplers
VkDescriptorSet getVkSet(DescriptorCount const& count, VkDescriptorSetLayout vklayout);
+6 -4
View File
@@ -1913,9 +1913,6 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
auto rt = resource_ptr<VulkanRenderTarget>::cast(&mResourceManager, rth);
VulkanCommandBuffer* commandBuffer = rt->isProtected() ?
&mCommands.getProtected() : &mCommands.get();
// Filament has the expectation that the contents of the swap chain are not preserved on the
// first render pass. Note however that its contents are often preserved on subsequent render
// passes, due to multiple views.
@@ -1930,6 +1927,11 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
}
}
// Note that this needs to come after the acquireNextswapchainImage() above because that path
// might flush the current command buffer.
VulkanCommandBuffer* commandBuffer =
rt->isProtected() ? &mCommands.getProtected() : &mCommands.get();
// Note that retrieving the extent must come after the acquireNextSwapchainImage() above;
// otherwise it might be 0.
VkExtent2D const extent = rt->getExtent();
@@ -1987,7 +1989,7 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
rt->emitBarriersBeginRenderPass(*commandBuffer);
fvkmemory::resource_ptr<VulkanFramebuffer> vkfb =
mFramebufferCache.getFramebuffer(fbkey, &mResourceManager);
mFramebufferCache.getFramebuffer(fbkey, &mResourceManager, rt);
// Assign a label to the framebuffer for debugging purposes.
#if FVK_ENABLED(FVK_DEBUG_GROUP_MARKERS | FVK_DEBUG_DEBUG_UTILS)
@@ -94,6 +94,9 @@ void VulkanExternalImageManager::updateSetAndLayout(
fvkmemory::resource_ptr<VulkanDescriptorSetLayout> const& layout = set->getLayout();
set->boundLayout = mDescriptorSetLayoutCache->getVkLayout(layout->bitmask,
actualExternalSamplers, outSamplers);
mDescriptorSetCache->cloneSet(set, actualExternalSamplers);
// Update the external samplers in the set
for (auto& [binding, sampler, image]: samplerAndBindings) {
// We cannot call updateSamplerForExternalSamplerSet because some samplers are non NULL
@@ -70,8 +70,9 @@ VulkanFboCache::~VulkanFboCache() {
<< "Please explicitly call terminate() while the VkDevice is still alive.";
}
fvkmemory::resource_ptr<VulkanFramebuffer> VulkanFboCache::getFramebuffer(
FboKey const& config, fvkmemory::ResourceManager* resManager) noexcept {
fvkmemory::resource_ptr<VulkanFramebuffer> VulkanFboCache::getFramebuffer(FboKey const& config,
fvkmemory::ResourceManager* resManager,
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget) noexcept {
FboMap::iterator iter = mFramebufferCache.find(config);
if (UTILS_LIKELY(iter != mFramebufferCache.end())) {
iter.value().timestamp = mCurrentTime;
@@ -120,8 +121,9 @@ fvkmemory::resource_ptr<VulkanFramebuffer> VulkanFboCache::getFramebuffer(
FILAMENT_CHECK_POSTCONDITION(error == VK_SUCCESS) << "Unable to create framebuffer."
<< " error=" << static_cast<int32_t>(error);
fvkmemory::resource_ptr<VulkanFramebuffer> fbh =
fvkmemory::resource_ptr<VulkanFramebuffer>::construct(resManager, mDevice, framebuffer);
mFramebufferCache[config] = {fbh, mCurrentTime};
fvkmemory::resource_ptr<VulkanFramebuffer>::construct(resManager, mDevice, framebuffer,
renderTarget);
mFramebufferCache[config] = { fbh, mCurrentTime };
return fbh;
}
+3 -2
View File
@@ -104,8 +104,9 @@ public:
~VulkanFboCache();
// Retrieves or creates a VkFramebuffer handle.
fvkmemory::resource_ptr<VulkanFramebuffer> getFramebuffer(
FboKey const& config, fvkmemory::ResourceManager* resManager) noexcept;
fvkmemory::resource_ptr<VulkanFramebuffer> getFramebuffer(FboKey const& config,
fvkmemory::ResourceManager* resManager,
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget) noexcept;
// Retrieves or creates a VkRenderPass handle.
fvkmemory::resource_ptr<VulkanRenderPass> getRenderPass(
@@ -715,8 +715,11 @@ VulkanRenderPrimitive::VulkanRenderPrimitive(PrimitiveType pt,
vertexBuffer(vb),
indexBuffer(ib) {}
VulkanFramebuffer::VulkanFramebuffer(VkDevice device, VkFramebuffer framebuffer)
: mDevice(device), mFramebuffer(framebuffer) {}
VulkanFramebuffer::VulkanFramebuffer(VkDevice device, VkFramebuffer framebuffer,
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget)
: mDevice(device),
mFramebuffer(framebuffer),
mRenderTarget(renderTarget) {}
VulkanFramebuffer::~VulkanFramebuffer() {
vkDestroyFramebuffer(mDevice, mFramebuffer, VKALLOC);
+10 -3
View File
@@ -195,7 +195,7 @@ public:
}
// The current layout used by the descriptor set. This one will match the bindings, including
// external samplers data.
// external samplers data.
// This will not necessarilly be the same as `mLayout`.
VkDescriptorSetLayout boundLayout = VK_NULL_HANDLE;
@@ -203,7 +203,7 @@ public:
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;
@@ -586,7 +586,9 @@ struct VulkanRenderPrimitive : public HwRenderPrimitive, fvkmemory::Resource {
};
struct VulkanFramebuffer : public fvkmemory::Resource {
VulkanFramebuffer(VkDevice device, VkFramebuffer framebuffer);
VulkanFramebuffer(VkDevice device,
VkFramebuffer framebuffer,
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget);
~VulkanFramebuffer();
inline VkFramebuffer getVkFramebuffer() const noexcept {
@@ -596,6 +598,11 @@ struct VulkanFramebuffer : public fvkmemory::Resource {
private:
VkDevice mDevice;
VkFramebuffer mFramebuffer;
// We need to keep a reference to the renderTarget because the key of the framebuffer in the
// cache has references to the image views that are derived from the textures of the render
// target.
fvkmemory::resource_ptr<VulkanRenderTarget> mRenderTarget;
};
struct VulkanRenderPass : public fvkmemory::Resource {
+1 -1
View File
@@ -372,7 +372,7 @@ public:
} vsm;
/**
* Light bulb radius used for soft shadows. Currently this is only used when DPCF or PCSS is
* Light bulb radius used for soft shadows. Currently, this is only used when DPCF or PCSS is
* enabled. (2cm by default).
*/
float shadowBulbRadius = 0.02f;
@@ -94,6 +94,12 @@ public:
std::is_same_v<math::mat3f, T>
>;
template<typename T>
using is_supported_constant_parameter_t = std::enable_if_t<
std::is_same_v<int32_t, T> ||
std::is_same_v<float, T> ||
std::is_same_v<bool, T>>;
/**
* Creates a new MaterialInstance using another MaterialInstance as a template for initialization.
* The new MaterialInstance is an instance of the same Material of the template instance and
@@ -276,6 +282,91 @@ public:
return getParameter<T>(name, strlen(name));
}
/**
* Overrides a specialization constant of this material instance.
*
* @tparam T The type of the constant. Must be int32_t, float, or bool.
* @param name The name of the constant as defined in the material. Cannot be nullptr.
* @param nameLength Length in `char` of the name parameter.
* @param value The value of the constant.
*
* @see Material::Builder::constant
*/
template<typename T, typename = is_supported_constant_parameter_t<T>>
void setConstant(const char* UTILS_NONNULL name, size_t nameLength, T value);
/** inline helper to provide the name as a null-terminated string literal */
template<typename T, typename = is_supported_constant_parameter_t<T>>
void setConstant(StringLiteral const name, T value) {
setConstant<T>(name.data, name.size, value);
}
/** inline helper to provide the name as a null-terminated C string */
template<typename T, typename = is_supported_constant_parameter_t<T>>
void setConstant(const char* UTILS_NONNULL name, T value) {
setConstant<T>(name, strlen(name), value);
}
/**
* Gets the value of a specialization constant by name.
*
* @tparam T The type of the constant. Must be int32_t, float, or bool.
* @param name The name of the constant as defined in the material. Cannot be nullptr.
* @param nameLength Length in `char` of the name parameter.
* @return The value of the constant.
*/
template<typename T, typename = is_supported_constant_parameter_t<T>>
T getConstant(const char* UTILS_NONNULL name, size_t nameLength) const;
/** inline helper to provide the name as a null-terminated C string */
template<typename T, typename = is_supported_constant_parameter_t<T>>
T getConstant(StringLiteral const name) const {
return getConstant<T>(name.data, name.size);
}
/** inline helper to provide the name as a null-terminated C string */
template<typename T, typename = is_supported_constant_parameter_t<T>>
T getConstant(const char* UTILS_NONNULL name) const {
return getConstant<T>(name, strlen(name));
}
using CompilerPriorityQueue = backend::CompilerPriorityQueue;
/**
* Asynchronously ensures that a subset of this MaterialInstance's variants are compiled.
*
* This function behaves identically to Material::compile(), but takes into account the
* specific constants overridden by setConstant().
*
* @param priority Which priority queue to use, LOW or HIGH.
* @param variants Variants to include to the compile command.
* @param handler Handler to dispatch the callback or nullptr for the default handler
* @param callback callback called on the main thread when the compilation is done on
* by backend.
*
* @see Material::compile
* @see setConstant
*/
void compile(CompilerPriorityQueue priority,
UserVariantFilterMask variants,
backend::CallbackHandler* UTILS_NULLABLE handler = nullptr,
utils::Invocable<void(MaterialInstance* UTILS_NONNULL)>&& callback = {}) noexcept;
inline void compile(CompilerPriorityQueue priority,
UserVariantFilterBit variants,
backend::CallbackHandler* UTILS_NULLABLE handler = nullptr,
utils::Invocable<void(MaterialInstance* UTILS_NONNULL)>&& callback = {}) noexcept {
compile(priority, UserVariantFilterMask(variants), handler,
std::forward<utils::Invocable<void(MaterialInstance* UTILS_NONNULL)>>(callback));
}
inline void compile(CompilerPriorityQueue priority,
backend::CallbackHandler* UTILS_NULLABLE handler = nullptr,
utils::Invocable<void(MaterialInstance* UTILS_NONNULL)>&& callback = {}) noexcept {
compile(priority, UserVariantFilterBit::ALL, handler,
std::forward<utils::Invocable<void(MaterialInstance* UTILS_NONNULL)>>(callback));
}
/**
* Set-up a custom scissor rectangle; by default it is disabled.
*
+1 -1
View File
@@ -735,7 +735,7 @@ struct VsmShadowOptions {
bool highPrecision = false;
/**
* VSM minimum variance scale, must be positive.
* @deprecated has no effect.
*/
float minVarianceScale = 0.5f;
+268
View File
@@ -0,0 +1,268 @@
/*
* 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.
*/
#include "LocalProgramCache.h"
#include "MaterialParser.h"
#include "details/Engine.h"
#include "details/Material.h"
#include <backend/DriverApiForward.h>
namespace filament {
using namespace backend;
using namespace utils;
LocalProgramCache::LocalProgramCache(LocalProgramCache const& other)
: mMaterial(other.mMaterial),
mCachedPrograms(other.mCachedPrograms.size()),
mSpecializationConstants((other.mMaterial != nullptr)
? other.mMaterial->getEngine()
.getMaterialCache()
.getSpecializationConstantsInternPool()
.acquire(other.mSpecializationConstants)
: SpecializationConstants()) {}
LocalProgramCache& LocalProgramCache::operator=(LocalProgramCache const& other) {
assert_invariant(mMaterial == nullptr);
assert_invariant(mCachedPrograms.empty());
assert_invariant(mSpecializationConstants.empty());
mMaterial = other.mMaterial;
if (mMaterial != nullptr) {
mCachedPrograms = FixedCapacityVector<Handle<HwProgram>>(other.mCachedPrograms.size());
mSpecializationConstants = other.mMaterial->getEngine()
.getMaterialCache()
.getSpecializationConstantsInternPool()
.acquire(other.mSpecializationConstants);
}
return *this;
}
void LocalProgramCache::initializeForMaterial(FEngine& engine, FMaterial const& material,
utils::FixedCapacityVector<backend::Program::SpecializationConstant>
specializationConstants) {
assert_invariant(mMaterial == nullptr);
assert_invariant(mCachedPrograms.empty());
assert_invariant(mSpecializationConstants.empty());
mMaterial = &material;
mSpecializationConstants =
engine.getMaterialCache().getSpecializationConstantsInternPool().acquire(
std::move(specializationConstants));
size_t cachedProgramsSize;
switch (material.getMaterialDomain()) {
case filament::MaterialDomain::SURFACE:
cachedProgramsSize = 1 << VARIANT_BITS;
break;
case filament::MaterialDomain::POST_PROCESS:
cachedProgramsSize = 1 << POST_PROCESS_VARIANT_BITS;
break;
case filament::MaterialDomain::COMPUTE:
cachedProgramsSize = 1;
break;
}
mCachedPrograms = FixedCapacityVector<Handle<HwProgram>>(cachedProgramsSize);
material.getDefinition().acquirePrograms(engine, mCachedPrograms.as_slice(),
material.getMaterialParser(), mSpecializationConstants, material.isDefaultMaterial());
}
void LocalProgramCache::initializeForMaterialInstance(FEngine& engine, FMaterial const& material) {
assert_invariant(mMaterial == nullptr);
assert_invariant(mCachedPrograms.empty());
assert_invariant(mSpecializationConstants.empty());
mMaterial = &material;
LocalProgramCache const& programs = material.getPrograms();
mSpecializationConstants =
engine.getMaterialCache().getSpecializationConstantsInternPool().acquire(
programs.getSpecializationConstants());
mCachedPrograms =
FixedCapacityVector<Handle<HwProgram>>(material.getPrograms().mCachedPrograms.size());
}
Handle<HwProgram> LocalProgramCache::prepareProgramSlow(DriverApi& driver, Variant const variant,
CompilerPriorityQueue const priorityQueue) const noexcept {
assert_invariant(mMaterial != nullptr);
FEngine& engine = mMaterial->getEngine();
if (mMaterial->isSharedVariant(variant)) {
FMaterial const* defaultMaterial = engine.getDefaultMaterial();
assert_invariant(defaultMaterial);
LocalProgramCache const& defaultPrograms = defaultMaterial->getPrograms();
Handle<HwProgram> program = defaultPrograms.mCachedPrograms[variant.key];
if (program) {
return mCachedPrograms[variant.key] = program;
}
return mCachedPrograms[variant.key] =
defaultPrograms.prepareProgram(driver, variant, priorityQueue);
}
return mCachedPrograms[variant.key] = mMaterial->getDefinition().prepareProgram(engine, driver,
mMaterial->getMaterialParser(), getProgramSpecialization(variant), priorityQueue);
}
ProgramSpecialization LocalProgramCache::getProgramSpecialization(Variant variant) const noexcept {
assert_invariant(mMaterial != nullptr);
return ProgramSpecialization {
.materialCrc32 = mMaterial->getMaterialParser().getCrc32(),
.variant = variant,
.specializationConstants = mSpecializationConstants,
};
}
void LocalProgramCache::terminate(FEngine& engine) {
assert_invariant(mMaterial != nullptr);
mMaterial->getDefinition().releasePrograms(engine, mCachedPrograms.as_slice(),
mMaterial->getMaterialParser(), mSpecializationConstants,
mMaterial->isDefaultMaterial());
engine.getMaterialCache().releaseMaterial(engine, mMaterial->getDefinition());
engine.getMaterialCache().getSpecializationConstantsInternPool().release(
mSpecializationConstants);
}
void LocalProgramCache::clear(FEngine& engine) {
assert_invariant(mMaterial != nullptr);
mMaterial->getDefinition().releasePrograms(engine, mCachedPrograms.as_slice(),
mMaterial->getMaterialParser(), mSpecializationConstants,
mMaterial->isDefaultMaterial());
}
Variant LocalProgramCache::filterVariantForGetProgram(Variant variant) const noexcept {
if (UTILS_UNLIKELY(mMaterial->getEngine().features.material.enable_fog_as_postprocess)) {
// if the fog as post-process feature is enabled, we need to proceed "as-if" the material
// didn't have the FOG variant bit.
if (mMaterial->getMaterialDomain() == MaterialDomain::SURFACE) {
BlendingMode const blendingMode = mMaterial->getBlendingMode();
bool const hasScreenSpaceRefraction =
mMaterial->getRefractionMode() == RefractionMode::SCREEN_SPACE;
bool const isBlendingCommand = !hasScreenSpaceRefraction &&
(blendingMode != BlendingMode::OPAQUE && blendingMode != BlendingMode::MASKED);
if (!isBlendingCommand) {
variant.setFog(false);
}
}
}
return variant;
}
Program::SpecializationConstant LocalProgramCache::getConstantImpl(uint32_t id) const noexcept {
return mSpecializationConstants[id];
}
Program::SpecializationConstant LocalProgramCache::getConstantImpl(
std::string_view name) const noexcept {
assert_invariant(mMaterial != nullptr);
auto const& constants = mMaterial->getDefinition().specializationConstantsNameToIndex;
auto it = constants.find(name);
FILAMENT_CHECK_PRECONDITION(it != constants.end()) << "Constant " << name << " does not exist";
return getConstantImpl(it->second + CONFIG_MAX_RESERVED_SPEC_CONSTANTS);
}
void LocalProgramCache::setConstants(
std::initializer_list<std::pair<uint32_t, Program::SpecializationConstant>>
constants) noexcept {
assert_invariant(mMaterial != nullptr);
auto newSpecializationConstants =
FixedCapacityVector<Program::SpecializationConstant>(mSpecializationConstants);
bool hasChanged = false;
for (const auto& [id, value] : constants) {
if (newSpecializationConstants[id] != value) {
newSpecializationConstants[id] = value;
hasChanged = true;
}
}
if (hasChanged) {
setConstantsImpl(std::move(newSpecializationConstants));
}
}
void LocalProgramCache::setConstants(
std::initializer_list<std::pair<std::string_view, Program::SpecializationConstant>>
constants) noexcept {
assert_invariant(mMaterial != nullptr);
auto newSpecializationConstants =
FixedCapacityVector<Program::SpecializationConstant>(mSpecializationConstants);
bool hasChanged = false;
for (const auto& [name, value] : constants) {
MaterialDefinition const& definition = mMaterial->getDefinition();
auto it = definition.specializationConstantsNameToIndex.find(name);
if (it != definition.specializationConstantsNameToIndex.cend()) {
uint32_t id = it->second + CONFIG_MAX_RESERVED_SPEC_CONSTANTS;
if (newSpecializationConstants[id] != value) {
newSpecializationConstants[id] = value;
hasChanged = true;
}
}
}
if (hasChanged) {
setConstantsImpl(std::move(newSpecializationConstants));
}
}
void LocalProgramCache::setConstants(
FixedCapacityVector<Program::SpecializationConstant> constants) noexcept {
assert_invariant(mMaterial != nullptr);
setConstantsImpl(std::move(constants));
}
void LocalProgramCache::setConstantsImpl(
FixedCapacityVector<Program::SpecializationConstant> constants) noexcept {
FEngine& engine = mMaterial->getEngine();
auto& internPool = engine.getMaterialCache().getSpecializationConstantsInternPool();
MaterialParser const& materialParser = mMaterial->getMaterialParser();
MaterialDefinition const& definition = mMaterial->getDefinition();
const bool isDefaultMaterial = mMaterial->isDefaultMaterial();
// Release old resources...
definition.releasePrograms(engine, mCachedPrograms.as_slice(), materialParser,
mSpecializationConstants, isDefaultMaterial);
internPool.release(mSpecializationConstants);
// Then acquire new ones.
mSpecializationConstants = internPool.acquire(std::move(constants));
definition.acquirePrograms(engine, mCachedPrograms.as_slice(), materialParser,
mSpecializationConstants, isDefaultMaterial);
}
template int32_t LocalProgramCache::getConstant<int32_t>(uint32_t id) const noexcept;
template float LocalProgramCache::getConstant<float>(uint32_t id) const noexcept;
template bool LocalProgramCache::getConstant<bool>(uint32_t id) const noexcept;
template int32_t LocalProgramCache::getConstant<int32_t>(std::string_view name) const noexcept;
template float LocalProgramCache::getConstant<float>(std::string_view name) const noexcept;
template bool LocalProgramCache::getConstant<bool>(std::string_view name) const noexcept;
} // namespace filament
+151
View File
@@ -0,0 +1,151 @@
/*
* 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.
*/
#ifndef TNT_FILAMENT_LOCALPROGRAMCACHE_H
#define TNT_FILAMENT_LOCALPROGRAMCACHE_H
#include "MaterialDefinition.h"
#include <backend/Handle.h>
#include <private/filament/Variant.h>
#include <backend/DriverEnums.h>
#include <backend/DriverApiForward.h>
#include <backend/Program.h>
#include <utility>
namespace filament {
class FMaterial;
// L0 cache for material programs. Manages recompiling them on-demand; owned by Material and
// MaterialInstance.
class LocalProgramCache {
template<typename T>
using is_supported_constant_parameter_t =
std::enable_if_t<std::is_same_v<int32_t, T> || std::is_same_v<float, T> ||
std::is_same_v<bool, T>>;
public:
using Programs = utils::Slice<const backend::Handle<backend::HwProgram>>;
using SpecializationConstants = utils::Slice<const backend::Program::SpecializationConstant>;
LocalProgramCache() = default;
LocalProgramCache(LocalProgramCache const& other);
LocalProgramCache& operator=(LocalProgramCache const& other);
// Initialize for use in a Material.
void initializeForMaterial(FEngine& engine, FMaterial const& material,
utils::FixedCapacityVector<backend::Program::SpecializationConstant>
specializationConstants);
// Initialize for use in a MaterialInstance. Copies the set of spec constants currently in use
// from its Material.
void initializeForMaterialInstance(FEngine& engine, FMaterial const& material);
bool isInitialized() const noexcept { return mMaterial != nullptr; }
// prepareProgram creates the program for the material's given variant at the backend level.
// Must be called outside of backend render pass.
// Must be called before getProgram() below.
backend::Handle<backend::HwProgram> prepareProgram(backend::DriverApi& driver,
Variant const variant,
backend::CompilerPriorityQueue const priorityQueue) const noexcept {
backend::Handle<backend::HwProgram> program = mCachedPrograms[variant.key];
if (UTILS_LIKELY(program)) {
return program;
}
return prepareProgramSlow(driver, variant, priorityQueue);
}
// getProgram returns the backend program for the material's given variant.
// Must be called after prepareProgram().
[[nodiscard]]
backend::Handle<backend::HwProgram> getProgram(Variant variant) const noexcept {
variant = filterVariantForGetProgram(variant);
backend::Handle<backend::HwProgram> program = mCachedPrograms[variant.key];
assert_invariant(program);
return program;
}
SpecializationConstants getSpecializationConstants() const noexcept {
return mSpecializationConstants;
}
Programs getPrograms() const noexcept { return mCachedPrograms.as_slice(); }
// Free all engine resources associated with this instance.
void terminate(FEngine& engine);
// Clear all cached programs. Used primarily by matdbg to "sever" a Material's connection to the
// global material cache.
void clear(FEngine& engine);
// Get constant by ID.
template<typename T, typename = is_supported_constant_parameter_t<T>>
T getConstant(uint32_t id) const noexcept {
return std::get<T>(getConstantImpl(id));
}
// Get constant by name.
template<typename T, typename = is_supported_constant_parameter_t<T>>
T getConstant(std::string_view name) const noexcept {
return std::get<T>(getConstantImpl(name));
}
// Set constants by ID.
void setConstants(
std::initializer_list<std::pair<uint32_t, backend::Program::SpecializationConstant>>
constants) noexcept;
// Set constants by name.
void setConstants(std::initializer_list<
std::pair<std::string_view, backend::Program::SpecializationConstant>>
constants) noexcept;
// Set constants list directly.
void setConstants(utils::FixedCapacityVector<backend::Program::SpecializationConstant>
constants) noexcept;
private:
// Apply any pending specialization constants. Invalidates programs as necessary.
void flushConstants() const;
backend::Handle<backend::HwProgram> prepareProgramSlow(backend::DriverApi& driver,
Variant const variant,
backend::CompilerPriorityQueue const priorityQueue) const noexcept;
ProgramSpecialization getProgramSpecialization(Variant variant) const noexcept;
Variant filterVariantForGetProgram(Variant const variant) const noexcept;
void setConstantsImpl(utils::FixedCapacityVector<backend::Program::SpecializationConstant>
constants) noexcept;
backend::Program::SpecializationConstant getConstantImpl(uint32_t id) const noexcept;
backend::Program::SpecializationConstant getConstantImpl(std::string_view name) const noexcept;
FMaterial const* mMaterial = nullptr;
mutable utils::FixedCapacityVector<backend::Handle<backend::HwProgram>> mCachedPrograms;
SpecializationConstants mSpecializationConstants;
};
} // namespace filament
#endif // TNT_FILAMENT_LOCALPROGRAMCACHE_H
+13
View File
@@ -39,12 +39,25 @@ bool MaterialCache::MaterialKey::operator==(MaterialKey const& rhs) const noexce
return parser == rhs.parser;
}
MaterialCache::MaterialCache()
: mDefinitions("MaterialCache::mDefinitions", 0),
mPrograms("MaterialCache::mPrograms", 0) {}
MaterialCache::~MaterialCache() {
assert_invariant(mDefinitions.empty());
assert_invariant(mPrograms.empty());
assert_invariant(mSpecializationConstantsInternPool.empty());
}
void MaterialCache::terminate(FEngine& engine) {
mPrograms.clearLruCache([&engine](backend::Handle<backend::HwProgram>& program) {
engine.getDriverApi().destroyProgram(program);
});
mDefinitions.clearLruCache([&engine](MaterialDefinition& definition) {
definition.terminate(engine);
});
}
MaterialDefinition* UTILS_NULLABLE MaterialCache::acquireMaterial(FEngine& engine,
const void* UTILS_NONNULL data, size_t size) noexcept {
std::unique_ptr<MaterialParser> parser = MaterialDefinition::createParser(engine.getBackend(),
+5
View File
@@ -56,8 +56,13 @@ public:
using ProgramCache =
utils::RefCountedMap<ProgramSpecialization, backend::Handle<backend::HwProgram>>;
MaterialCache();
~MaterialCache();
// All reference-counted resources should be freed by the time MaterialCache is destructed, but
// the LRU cache needs to be explicitly freed in addition.
void terminate(FEngine& engine);
SpecializationConstantInternPool& getSpecializationConstantsInternPool() {
return mSpecializationConstantsInternPool;
}
+16 -16
View File
@@ -271,8 +271,8 @@ std::unique_ptr<MaterialDefinition> MaterialDefinition::create(FEngine& engine,
void MaterialDefinition::terminate(FEngine& engine) {
DriverApi& driver = engine.getDriverApi();
perViewDescriptorSetLayout.terminate(engine.getDescriptorSetLayoutFactory(), driver);
perViewDescriptorSetLayoutVsm.terminate(engine.getDescriptorSetLayoutFactory(), driver);
perViewDescriptorSetLayoutPcf.terminate(engine.getDescriptorSetLayoutFactory(), driver);
perViewDescriptorSetLayoutS2d.terminate(engine.getDescriptorSetLayoutFactory(), driver);
descriptorSetLayout.terminate(engine.getDescriptorSetLayoutFactory(), driver);
}
@@ -607,23 +607,23 @@ void MaterialDefinition::processDescriptorSets(FEngine& engine) {
refractionMode == RefractionMode::SCREEN_SPACE;
bool const hasFog = !(variantFilterMask & UserVariantFilterMask(UserVariantFilterBit::FOG));
this->perViewDescriptorSetLayoutDescription = descriptor_sets::getPerViewDescriptorSetLayout(
this->perViewDescriptorSetLayoutPcfDescription = descriptor_sets::getPerViewDescriptorSetLayout(
materialDomain, isLit, isSSR, hasFog, false);
this->perViewDescriptorSetLayoutVsmDescription = descriptor_sets::getPerViewDescriptorSetLayout(
this->perViewDescriptorSetLayoutS2dDescription = descriptor_sets::getPerViewDescriptorSetLayout(
materialDomain, isLit, isSSR, hasFog, true);
// set the labels
this->descriptorSetLayoutDescription.label = CString{ name }.append("_perMat");
this->perViewDescriptorSetLayoutDescription.label = CString{ name }.append("_perView");
this->perViewDescriptorSetLayoutVsmDescription.label = CString{ name }.append("_perViewVsm");
this->perViewDescriptorSetLayoutPcfDescription.label = CString{ name }.append("_perView");
this->perViewDescriptorSetLayoutS2dDescription.label = CString{ name }.append("_perViewVsm");
// get the PER_RENDERABLE and PER_VIEW descriptor binding info
for (auto&& [bindingPoint, dsl] : {
std::pair{ DescriptorSetBindingPoints::PER_RENDERABLE,
descriptor_sets::getPerRenderableLayout() },
std::pair{ DescriptorSetBindingPoints::PER_VIEW,
this->perViewDescriptorSetLayoutDescription }}) {
this->perViewDescriptorSetLayoutPcfDescription }}) {
Program::DescriptorBindingsInfo& descriptors = programDescriptorBindings[+bindingPoint];
descriptors.reserve(dsl.descriptors.size());
for (auto const& entry: dsl.descriptors) {
@@ -636,17 +636,17 @@ void MaterialDefinition::processDescriptorSets(FEngine& engine) {
descriptorSetLayoutFactory, driver,
this->descriptorSetLayoutDescription };
this->perViewDescriptorSetLayout = {
this->perViewDescriptorSetLayoutPcf = {
descriptorSetLayoutFactory, driver,
this->perViewDescriptorSetLayoutDescription };
this->perViewDescriptorSetLayoutPcfDescription };
this->perViewDescriptorSetLayoutVsm = {
this->perViewDescriptorSetLayoutS2d = {
descriptorSetLayoutFactory, driver,
this->perViewDescriptorSetLayoutVsmDescription };
this->perViewDescriptorSetLayoutS2dDescription };
}
backend::DescriptorSetLayout const& MaterialDefinition::getPerViewDescriptorSetLayoutDescription(
Variant const variant, bool const useVsmDescriptorSetLayout) const noexcept {
Variant const variant, bool const useS2dDescriptorSetLayout) const noexcept {
if (materialDomain == MaterialDomain::SURFACE) {
if (Variant::isValidDepthVariant(variant)) {
// Use the layout description used to create the per view depth variant layout.
@@ -657,10 +657,10 @@ backend::DescriptorSetLayout const& MaterialDefinition::getPerViewDescriptorSetL
return descriptor_sets::getSsrVariantLayout();
}
}
if (useVsmDescriptorSetLayout) {
return perViewDescriptorSetLayoutVsmDescription;
if (useS2dDescriptorSetLayout) {
return perViewDescriptorSetLayoutS2dDescription;
}
return perViewDescriptorSetLayoutDescription;
return perViewDescriptorSetLayoutPcfDescription;
}
Handle<HwProgram> MaterialDefinition::compileProgram(
@@ -689,7 +689,7 @@ Handle<HwProgram> MaterialDefinition::compileProgram(
pb.descriptorLayout(+DescriptorSetBindingPoints::PER_VIEW,
getPerViewDescriptorSetLayoutDescription(
specialization.variant,
Variant::isVSMVariant(specialization.variant)));
Variant::isShadowSampler2DVariant(specialization.variant)));
pb.descriptorLayout(+DescriptorSetBindingPoints::PER_RENDERABLE,
descriptor_sets::getPerRenderableLayout());
pb.descriptorLayout(
+5 -5
View File
@@ -94,17 +94,17 @@ struct MaterialDefinition {
backend::ShaderModel const sm, bool isStereoSupported) const noexcept;
backend::DescriptorSetLayout const& getPerViewDescriptorSetLayoutDescription(
Variant const variant, bool useVsmDescriptorSetLayout) const noexcept;
Variant const variant, bool useS2dDescriptorSetLayout) const noexcept;
// Keep track of the definitions of the descriptor set layouts, as these
// may be used by some backends in parallel compilation of programs.
backend::DescriptorSetLayout perViewDescriptorSetLayoutDescription;
backend::DescriptorSetLayout perViewDescriptorSetLayoutVsmDescription;
backend::DescriptorSetLayout perViewDescriptorSetLayoutPcfDescription;
backend::DescriptorSetLayout perViewDescriptorSetLayoutS2dDescription;
backend::DescriptorSetLayout descriptorSetLayoutDescription;
// try to order by frequency of use
filament::DescriptorSetLayout perViewDescriptorSetLayout;
filament::DescriptorSetLayout perViewDescriptorSetLayoutVsm;
filament::DescriptorSetLayout perViewDescriptorSetLayoutPcf;
filament::DescriptorSetLayout perViewDescriptorSetLayoutS2d;
filament::DescriptorSetLayout descriptorSetLayout;
backend::Program::DescriptorSetInfo programDescriptorBindings;
+26
View File
@@ -233,6 +233,26 @@ template UTILS_PUBLIC mat3f MaterialInstance::getParameter<mat3f> (const ch
// ------------------------------------------------------------------------------------------------
template<typename T, typename>
void MaterialInstance::setConstant(const char* name, size_t nameLength, T value) {
downcast(this)->setConstantImpl(std::string_view{name, nameLength}, value);
}
template UTILS_PUBLIC void MaterialInstance::setConstant<int32_t>(const char* name, size_t nameLength, int32_t value);
template UTILS_PUBLIC void MaterialInstance::setConstant<float>(const char* name, size_t nameLength, float value);
template UTILS_PUBLIC void MaterialInstance::setConstant<bool>(const char* name, size_t nameLength, bool value);
template<typename T, typename>
T MaterialInstance::getConstant(const char* name, size_t nameLength) const {
return downcast(this)->getConstantImpl<T>(std::string_view{name, nameLength});
}
template UTILS_PUBLIC int32_t MaterialInstance::getConstant<int32_t>(const char* name, size_t nameLength) const;
template UTILS_PUBLIC float MaterialInstance::getConstant<float>(const char* name, size_t nameLength) const;
template UTILS_PUBLIC bool MaterialInstance::getConstant<bool>(const char* name, size_t nameLength) const;
// ------------------------------------------------------------------------------------------------
Material const* MaterialInstance::getMaterial() const noexcept {
return downcast(this)->getMaterial();
}
@@ -403,4 +423,10 @@ void MaterialInstance::commit(Engine& engine) const {
downcast(this)->commit(downcast(engine));
}
void MaterialInstance::compile(CompilerPriorityQueue const priority,
UserVariantFilterMask const variants, CallbackHandler* handler,
utils::Invocable<void(MaterialInstance*)>&& callback) noexcept {
downcast(this)->compile(priority, variants, handler, std::move(callback));
}
} // namespace filament
+38 -67
View File
@@ -19,90 +19,61 @@
#include "details/Engine.h"
#include "details/Material.h"
#include "details/MaterialInstance.h"
#include <utils/debug.h>
#include <iterator>
#include <utility>
#include <stdint.h>
#include <cstdint>
namespace filament {
using Record = MaterialInstanceManager::Record;
using namespace utils;
std::pair<FMaterialInstance*, int32_t> Record::getInstance() {
if (mAvailable < mInstances.size()) {
auto index = mAvailable++;
return { mInstances[index], index };
}
assert_invariant(mAvailable == mInstances.size());
auto& name = mMaterial->getName();
FMaterialInstance* inst = mMaterial->createInstance(name.c_str_safe());
mInstances.push_back(inst);
return { inst, mAvailable++ };
}
MaterialInstanceManager::MaterialInstanceManager() noexcept = default;
FMaterialInstance* Record::getInstance(int32_t const fixedInstanceindex) const {
assert_invariant(fixedInstanceindex >= 0 && fixedInstanceindex < int32_t(mInstances.size()));
return mInstances[fixedInstanceindex];
}
// Defined in cpp to avoid inlining
Record::Record(Record const& rhs) noexcept = default;
Record& Record::operator=(Record const& rhs) noexcept = default;
Record::Record(Record&& rhs) noexcept = default;
Record& Record::operator=(Record&& rhs) noexcept = default;
void Record::terminate(FEngine& engine) {
std::for_each(mInstances.begin(), mInstances.end(),
[&engine](auto instance) { engine.destroy(instance); });
}
MaterialInstanceManager::MaterialInstanceManager() noexcept {}
MaterialInstanceManager::MaterialInstanceManager(
MaterialInstanceManager const& rhs) noexcept = default;
MaterialInstanceManager::MaterialInstanceManager(MaterialInstanceManager&& rhs) noexcept = default;
MaterialInstanceManager& MaterialInstanceManager::operator=(
MaterialInstanceManager const& rhs) noexcept = default;
MaterialInstanceManager& MaterialInstanceManager::operator=(
MaterialInstanceManager&& rhs) noexcept = default;
MaterialInstanceManager& MaterialInstanceManager::operator=(MaterialInstanceManager&& rhs) noexcept = default;
MaterialInstanceManager::~MaterialInstanceManager() = default;
void MaterialInstanceManager::terminate(FEngine& engine) {
std::for_each(mMaterials.begin(), mMaterials.end(), [&engine](auto& record) {
record.terminate(engine);
});
for (auto const& [key, instance] : mMaterialInstances) {
engine.destroy(instance);
}
mMaterialInstances.clear();
for (auto const& [material, pool] : mAnonymousMaterialInstances) {
for (auto instance : pool.instances) {
engine.destroy(instance);
}
}
mAnonymousMaterialInstances.clear();
}
Record& MaterialInstanceManager::getRecord(FMaterial const* const ma) const {
auto itr = std::find_if(mMaterials.begin(), mMaterials.end(), [ma](auto& record) {
return ma == record.mMaterial;
});
if (itr == mMaterials.end()) {
mMaterials.emplace_back(ma);
itr = std::prev(mMaterials.end());
void MaterialInstanceManager::reset() {
for (auto& [material, pool] : mAnonymousMaterialInstances) {
pool.highWaterMark = 0;
}
return *itr;
}
FMaterialInstance* MaterialInstanceManager::getMaterialInstance(FMaterial const* ma, uint32_t tag) const {
Key const key{ma, tag};
auto it = mMaterialInstances.find(key);
if (it != mMaterialInstances.end()) {
return it->second;
}
FMaterialInstance* const instance = ma->createInstance(ma->getName().c_str_safe());
mMaterialInstances.emplace(key, instance);
return instance;
}
FMaterialInstance* MaterialInstanceManager::getMaterialInstance(FMaterial const* ma) const {
auto [inst, index] = getRecord(ma).getInstance();
return inst;
}
AnonymousPool& pool = mAnonymousMaterialInstances[ma];
if (pool.highWaterMark < pool.instances.size()) {
return pool.instances[pool.highWaterMark++];
}
FMaterialInstance* MaterialInstanceManager::getMaterialInstance(FMaterial const* ma,
int32_t const fixedIndex) const {
return getRecord(ma).getInstance(fixedIndex);
FMaterialInstance* const instance = ma->createInstance(ma->getName().c_str_safe());
pool.instances.push_back(instance);
pool.highWaterMark++;
return instance;
}
std::pair<FMaterialInstance*, int32_t> MaterialInstanceManager::getFixedMaterialInstance(
FMaterial const* ma) {
return getRecord(ma).getInstance();
}
} // namespace filament
+56 -60
View File
@@ -16,10 +16,13 @@
#pragma once
#include <utils/bitset.h>
#include <cstddef>
#include <cstdint>
#include <unordered_map>
#include <vector>
#include <utils/Hash.h>
namespace filament {
class FMaterial;
@@ -30,37 +33,10 @@ class FEngine;
// re-use instances across frames.
class MaterialInstanceManager {
public:
class Record {
public:
Record(FMaterial const* material)
: mMaterial(material),
mAvailable(0) {}
~Record() = default;
Record(Record const& rhs) noexcept;
Record& operator=(Record const& rhs) noexcept;
Record(Record&& rhs) noexcept;
Record& operator=(Record&& rhs) noexcept;
void terminate(FEngine& engine);
void reset() { mAvailable = 0; }
std::pair<FMaterialInstance*, int32_t> getInstance();
FMaterialInstance* getInstance(int32_t fixedInstanceindex) const;
private:
FMaterial const* mMaterial = nullptr;
std::vector<FMaterialInstance*> mInstances;
uint32_t mAvailable;
friend class MaterialInstanceManager;
};
constexpr static int32_t INVALID_FIXED_INDEX = -1;
MaterialInstanceManager() noexcept;
MaterialInstanceManager(MaterialInstanceManager const& rhs) noexcept;
MaterialInstanceManager(MaterialInstanceManager const& rhs) = delete;
MaterialInstanceManager(MaterialInstanceManager&& rhs) noexcept;
MaterialInstanceManager& operator=(MaterialInstanceManager const& rhs) noexcept;
MaterialInstanceManager& operator=(MaterialInstanceManager const& rhs) = delete;
MaterialInstanceManager& operator=(MaterialInstanceManager&& rhs) noexcept;
~MaterialInstanceManager();
@@ -71,40 +47,60 @@ public:
*/
void terminate(FEngine& engine);
/*
* This returns a material instance given a material. The implementation will try to find an
* available instance in the cache. If one is not found, then a new instance will be created and
* added to the cache.
/**
* Resets the anonymous material instances cache.
*/
void reset();
/**
* This returns a material instance given a material and a tag.
*
* If the material instance doesn't exist in the cache, it is created and cached.
*
* @param ma FMaterial to get a MaterialInstance for
* @param tag A unique tag identifying the MaterialInstance
* @return A FMaterialInstance pointer
*/
FMaterialInstance* getMaterialInstance(FMaterial const* ma, uint32_t tag) const;
/**
* This returns a material instance given a material from a cache.
*
* If the material instance doesn't exist in the cache, it is created and cached.
*
* It is permissible to call the method several times, in which case a different MaterialInstance will be returned.
* It is guaranteed to be different from MaterialInstances returned with a tag.
*
* @param ma FMaterial to get a MaterialInstance for
* @return A FMaterialInstance pointer
*/
FMaterialInstance* getMaterialInstance(FMaterial const* ma) const;
/*
* This returns a material instance given a material and an index. The `fixedIndex` should be
* a value returned by getiFixedMaterialInstance.
*/
FMaterialInstance* getMaterialInstance(FMaterial const* ma, int32_t const fixedIndex) const;
/*
* This returns a material instance and an index given a material. This is needed for the
* case when two framegraph passes need to refer to the same material instance.
* The returned index can be used with `getFixedMaterialInstance` to get a specific instance
* of a material (and not a random entry in the record cache).
*/
std::pair<FMaterialInstance*, int32_t> getFixedMaterialInstance(FMaterial const* ma);
/*
* Marks all of the material instances as unused. Typically, you'd call this at the beginning of
* a frame.
*/
void reset() {
std::for_each(mMaterials.begin(), mMaterials.end(), [](auto& record) { record.reset(); });
}
private:
Record& getRecord(FMaterial const* material) const;
struct Key {
FMaterial const* material;
uint32_t tag;
bool operator==(Key const& rhs) const noexcept {
return material == rhs.material && tag == rhs.tag;
}
};
mutable std::vector<Record> mMaterials;
struct Hasher {
std::size_t operator()(Key const& key) const noexcept {
std::size_t seed = 0;
utils::hash::combine(seed, key.material);
utils::hash::combine(seed, key.tag);
return seed;
}
};
struct AnonymousPool {
std::vector<FMaterialInstance*> instances;
uint32_t highWaterMark = 0;
};
mutable std::unordered_map<Key, FMaterialInstance*, Hasher> mMaterialInstances;
mutable std::unordered_map<FMaterial const*, AnonymousPool> mAnonymousMaterialInstances;
};
} // namespace filament
+100 -109
View File
@@ -184,12 +184,10 @@ void PostProcessManager::PostProcessMaterial::loadMaterial(FEngine& engine) cons
}
UTILS_NOINLINE
FMaterial* PostProcessManager::PostProcessMaterial::getMaterial(FEngine& engine,
DriverApi& driver, Variant::type_t const variant) const noexcept {
FMaterial* PostProcessManager::PostProcessMaterial::getMaterial(FEngine& engine) const noexcept {
if (UTILS_UNLIKELY(mSize)) {
loadMaterial(engine);
}
mMaterial->prepareProgram(driver, Variant{ variant }, CompilerPriorityQueue::CRITICAL);
return mMaterial;
}
@@ -225,7 +223,6 @@ const PostProcessManager::JitterSequence<32>
PostProcessManager::PostProcessManager(FEngine& engine) noexcept
: mEngine(engine),
mFixedMaterialInstanceIndex {},
mWorkaroundSplitEasu(false),
mWorkaroundAllowReadOnlyAncillaryFeedbackLoop(false) {
// don't use Engine here, it's not fully initialized yet
@@ -248,6 +245,20 @@ void PostProcessManager::bindPerRenderableDescriptorSet(DriverApi& driver) const
{ { 0, 0 }, driver });
}
FMaterialInstance* PostProcessManager::getMaterialInstance(backend::DriverApi& driver,
FMaterial const* ma, Variant::type_t variant) const {
FMaterialInstance* mi = mMaterialInstanceManager.getMaterialInstance(ma);
mi->prepareProgram(driver, Variant{ variant }, backend::CompilerPriorityQueue::CRITICAL);
return mi;
}
FMaterialInstance* PostProcessManager::getMaterialInstanceWithTag(backend::DriverApi& driver,
FMaterial const* ma, uint32_t tag, Variant::type_t variant) const {
FMaterialInstance* mi = mMaterialInstanceManager.getMaterialInstance(ma, tag);
mi->prepareProgram(driver, Variant { variant }, backend::CompilerPriorityQueue::CRITICAL);
return mi;
}
UboManager* PostProcessManager::getUboManager() const noexcept {
return mEngine.getUboManager();
}
@@ -455,7 +466,6 @@ Handle<HwTexture> PostProcessManager::getZeroTextureArray() const {
void PostProcessManager::resetForRender() {
mMaterialInstanceManager.reset();
mFixedMaterialInstanceIndex = {};
}
void PostProcessManager::unbindAllDescriptorSets(DriverApi& driver) noexcept {
@@ -466,9 +476,10 @@ void PostProcessManager::unbindAllDescriptorSets(DriverApi& driver) noexcept {
UTILS_NOINLINE
PipelineState PostProcessManager::getPipelineState(
FMaterial const* const ma, Variant::type_t const variant) const noexcept {
FMaterialInstance const* const mi, Variant::type_t const variant) const noexcept {
FMaterial const* const ma = mi->getMaterial();
return {
.program = ma->getProgram(Variant{ variant }),
.program = mi->getProgram(Variant{ variant }),
.vertexBufferInfo = mFullScreenQuadVbih,
.pipelineLayout = {
.setLayout = {
@@ -476,7 +487,7 @@ PipelineState PostProcessManager::getPipelineState(
mPerRenderableDslh,
ma->getDescriptorSetLayout().getHandle()
}},
.rasterState = ma->getRasterState()
.rasterState = mi->getRasterState()
};
}
@@ -516,8 +527,7 @@ void PostProcessManager::commitAndRenderFullScreenQuad(DriverApi& driver,
PostProcessVariant const variant) const noexcept {
mi->commit(driver, getUboManager());
mi->use(driver);
FMaterial const* const ma = mi->getMaterial();
PipelineState const pipeline = getPipelineState(ma, variant);
PipelineState const pipeline = getPipelineState(mi, variant);
assert_invariant(
((out.params.readOnlyDepthStencil & RenderPassParams::READONLY_DEPTH)
@@ -634,12 +644,12 @@ PostProcessManager::StructurePassOutput PostProcessManager::structure(FrameGraph
auto in = resources.getTexture(data.depth);
auto& material = getPostProcessMaterial("mipmapDepth");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
FMaterialInstance* const mi = getMaterialInstance(ma);
FMaterial const* const ma = material.getMaterial(mEngine);
FMaterialInstance* const mi = getMaterialInstance(driver, ma);
// Only the depth texture is changing in the material instance (no UBO updates),
// we do not move getMaterialInstance() inside the loop.
auto pipeline = getPipelineState(ma);
auto pipeline = getPipelineState(mi);
// The first mip already exists, so we process n-1 lods
for (size_t level = 0; level < levelCount - 1; level++) {
@@ -802,7 +812,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::ssr(FrameGraph& fg,
// use our special SSR variant, it can only be applied to object that have
// the SCREEN_SPACE ReflectionMode.
passBuilder.variant(Variant{ Variant::SPECIAL_SSR });
passBuilder.variant(Variant{ Variant::SPECIAL_SSR_VARIANT });
// generate all our drawing commands, except blended objects.
passBuilder.commandTypeFlags(RenderPass::CommandTypeFlags::SCREEN_SPACE_REFLECTIONS);
@@ -999,17 +1009,13 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::screenSpaceAmbientOcclusion(
#endif
auto& material = getPostProcessMaterial(materialName);
FMaterial* ma = material.getMaterial(mEngine, driver);
{
FMaterial::SpecializationConstantsBuilder maConstants =
ma->getSpecializationConstantsBuilder();
maConstants.set("useVisibilityBitmasks", options.gtao.useVisibilityBitmasks);
maConstants.set("linearThickness", options.gtao.linearThickness);
ma->setSpecializationConstants(std::move(maConstants));
}
FMaterial* ma = material.getMaterial(mEngine);
ma->getPrograms().setConstants({
{ "useVisibilityBitmasks", options.gtao.useVisibilityBitmasks },
{ "linearThickness", options.gtao.linearThickness },
});
ma = material.getMaterial(mEngine, driver);
FMaterialInstance* const mi = getMaterialInstance(ma);
FMaterialInstance* const mi = getMaterialInstance(driver, ma);
// Set AO type specific material parameters
switch (aoType) {
@@ -1085,7 +1091,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::screenSpaceAmbientOcclusion(
mi->commit(driver, getUboManager());
mi->use(driver);
auto pipeline = getPipelineState(ma);
auto pipeline = getPipelineState(mi);
pipeline.rasterState.depthFunc = RasterState::DepthFunc::L;
assert_invariant(ssao.params.readOnlyDepthStencil & RenderPassParams::READONLY_DEPTH);
renderFullScreenQuad(ssao, pipeline, driver);
@@ -1195,8 +1201,8 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::bilateralBlurPass(FrameGraph
auto& material = config.bentNormals ?
getPostProcessMaterial("bilateralBlurBentNormals") :
getPostProcessMaterial("bilateralBlur");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
FMaterialInstance* const mi = getMaterialInstance(ma);
FMaterial const* const ma = material.getMaterial(mEngine);
FMaterialInstance* const mi = getMaterialInstance(driver, ma);
mi->setParameter("ssao", ssao, { /* only reads level 0 */ });
mi->setParameter("axis", axis / float2{desc.width, desc.height});
mi->setParameter("kernel", kGaussianSamples, kGaussianCount);
@@ -1206,7 +1212,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::bilateralBlurPass(FrameGraph
mi->commit(driver, getUboManager());
mi->use(driver);
auto pipeline = getPipelineState(ma);
auto pipeline = getPipelineState(mi);
pipeline.rasterState.depthFunc = RasterState::DepthFunc::L;
renderFullScreenQuad(blurred, pipeline, driver);
unbindAllDescriptorSets(driver);
@@ -1364,7 +1370,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::gaussianBlurPass(FrameGraph&
"separableGaussianBlur4L"sv : "separableGaussianBlur4"sv; break;
}
auto const& separableGaussianBlur = getPostProcessMaterial(materialName);
auto ma = separableGaussianBlur.getMaterial(mEngine, driver);
auto ma = separableGaussianBlur.getMaterial(mEngine);
const size_t kernelStorageSize = ma->reflect("kernel")->size;
float2 kernel[64];
@@ -1880,9 +1886,10 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::dof(FrameGraph& fg,
auto inOutCoc = resources.getTexture(data.inOutCoc);
auto const& material = getPostProcessMaterial("dofMipmap");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
FMaterial const* const ma = material.getMaterial(mEngine);
FMaterialInstance* const mi = getMaterialInstance(driver, ma);
auto const pipeline = getPipelineState(ma, variant);
auto const pipeline = getPipelineState(mi, variant);
for (size_t level = 0 ; level < mipmapCount - 1u ; level++) {
const float w = FTexture::valueForLevel(level, desc.width);
@@ -1890,7 +1897,8 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::dof(FrameGraph& fg,
auto const& out = resources.getRenderPassInfo(data.rp[level]);
auto inColor = driver.createTextureView(inOutColor, level, 1);
auto inCoc = driver.createTextureView(inOutCoc, level, 1);
FMaterialInstance* const mi = getMaterialInstance(ma);
// FIXME: is this necessary?
FMaterialInstance* const mi = getMaterialInstance(driver, ma);
mi->setParameter("color", inColor, SamplerParams{
.filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST });
@@ -2407,9 +2415,9 @@ PostProcessManager::BloomPassOutput PostProcessManager::bloom(FrameGraph& fg,
auto const& outDesc = resources.getDescriptor(data.out);
auto const& material = getPostProcessMaterial("bloomUpsample");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
FMaterial const* const ma = material.getMaterial(mEngine);
auto pipeline = getPipelineState(ma);
auto pipeline = getPipelineState(getMaterialInstance(driver, ma));
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
pipeline.rasterState.blendFunctionDstRGB = BlendFunction::ONE;
@@ -2417,7 +2425,7 @@ PostProcessManager::BloomPassOutput PostProcessManager::bloom(FrameGraph& fg,
// Note that we wouldn't want to use the same instance for each pass since that
// would imply using the same UBOs, which implies synchronization across the
// passes.
FMaterialInstance* mi = getMaterialInstance(ma);
FMaterialInstance* mi = getMaterialInstance(driver, ma);
auto hwDstRT = resources.getRenderPassInfo(data.outRT[i - 1]);
hwDstRT.params.flags.discardStart = TargetBufferFlags::NONE; // b/c we'll blend
hwDstRT.params.flags.discardEnd = TargetBufferFlags::NONE;
@@ -2550,15 +2558,12 @@ void PostProcessManager::colorGradingSubpass(DriverApi& driver,
PostProcessVariant::TRANSLUCENT : PostProcessVariant::OPAQUE;
auto const& material = getPostProcessMaterial("colorGradingAsSubpass");
FMaterial const* const ma = material.getMaterial(mEngine, driver, variant);
FMaterial const* const ma = material.getMaterial(mEngine);
// the UBO has been set and committed in colorGradingPrepareSubpass()
int32_t const fixedIndex = colorGradingConfig.translucent
? mFixedMaterialInstanceIndex.colorGradingTranslucent
: mFixedMaterialInstanceIndex.colorGradingOpaque;
FMaterialInstance const* mi = mMaterialInstanceManager.getMaterialInstance(ma, fixedIndex);
FMaterialInstance const* mi =
getMaterialInstanceWithTag(driver, ma, colorGradingConfig.translucent, variant);
mi->use(driver);
auto const pipeline = getPipelineState(ma, variant);
auto const pipeline = getPipelineState(mi, variant);
driver.nextSubpass();
driver.scissor(mi->getScissor());
driver.draw(pipeline, mFullScreenQuadRph, 0, 3, 1);
@@ -2566,9 +2571,8 @@ void PostProcessManager::colorGradingSubpass(DriverApi& driver,
void PostProcessManager::customResolvePrepareSubpass(DriverApi& driver, CustomResolveOp const op) noexcept {
auto const& material = getPostProcessMaterial("customResolveAsSubpass");
auto const ma = material.getMaterial(mEngine, driver, PostProcessVariant::OPAQUE);
auto [mi, fixedIndex] = mMaterialInstanceManager.getFixedMaterialInstance(ma);
mFixedMaterialInstanceIndex.customResolve = fixedIndex;
auto const ma = material.getMaterial(mEngine);
auto* const mi = getMaterialInstance(driver, ma, 0);
mi->setParameter("direction", op == CustomResolveOp::COMPRESS ? 1.0f : -1.0f),
mi->commit(driver, getUboManager());
}
@@ -2578,13 +2582,12 @@ void PostProcessManager::customResolveSubpass(DriverApi& driver) noexcept {
bindPerRenderableDescriptorSet(driver);
auto const& material = getPostProcessMaterial("customResolveAsSubpass");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
FMaterial const* const ma = material.getMaterial(mEngine);
// the UBO has been set and committed in customResolvePrepareSubpass()
FMaterialInstance const* mi = mMaterialInstanceManager.getMaterialInstance(ma,
mFixedMaterialInstanceIndex.customResolve);
FMaterialInstance const* mi = getMaterialInstance(driver, ma, 0);
mi->use(driver);
auto const pipeline = getPipelineState(ma);
auto const pipeline = getPipelineState(mi);
driver.nextSubpass();
driver.scissor(mi->getScissor());
driver.draw(pipeline, mFullScreenQuadRph, 0, 3, 1);
@@ -2620,9 +2623,8 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::customResolveUncompressPass(
void PostProcessManager::clearAncillaryBuffersPrepare(DriverApi& driver,
Variant::type_t variant) noexcept {
auto const& material = getPostProcessMaterial("clearDepth");
auto ma = material.getMaterial(mEngine, driver, variant);
auto [mi, fixedIndex] = mMaterialInstanceManager.getFixedMaterialInstance(ma);
mFixedMaterialInstanceIndex.clearDepth = fixedIndex;
auto const ma = material.getMaterial(mEngine);
auto const mi = getMaterialInstanceWithTag(driver, ma, 0, variant);
mi->commit(driver, getUboManager());
}
@@ -2637,14 +2639,13 @@ void PostProcessManager::clearAncillaryBuffers(DriverApi& driver,
bindPerRenderableDescriptorSet(driver);
auto const& material = getPostProcessMaterial("clearDepth");
FMaterial const* const ma = material.getMaterial(mEngine, driver, variant);
FMaterial const* const ma = material.getMaterial(mEngine);
// the UBO has been set and committed in clearAncillaryBuffersPrepare()
FMaterialInstance const* const mi = mMaterialInstanceManager.getMaterialInstance(ma,
mFixedMaterialInstanceIndex.clearDepth);
FMaterialInstance const* const mi = getMaterialInstanceWithTag(driver, ma, 0, variant);
mi->use(driver);
auto pipeline = getPipelineState(ma, variant);
auto pipeline = getPipelineState(mi, variant);
pipeline.rasterState.depthFunc = RasterState::DepthFunc::A;
driver.scissor(mi->getScissor());
@@ -2654,7 +2655,7 @@ void PostProcessManager::clearAncillaryBuffers(DriverApi& driver,
void PostProcessManager::fogPrepare(DriverApi& driver) noexcept {
// ensures the material is loaded and material instance created
auto const& material = getPostProcessMaterial("fog");
FMaterial const* const ma = material.getMaterial(mEngine, driver, PostProcessVariant::OPAQUE);
FMaterial const* const ma = material.getMaterial(mEngine);
FMaterialInstance const* mi = ma->getDefaultInstance();
mi->commit(driver, getUboManager());
}
@@ -2665,11 +2666,11 @@ void PostProcessManager::fog(DriverApi& driver) noexcept {
bindPerRenderableDescriptorSet(driver);
auto const& material = getPostProcessMaterial("fog");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
FMaterial const* const ma = material.getMaterial(mEngine);
FMaterialInstance const* mi = ma->getDefaultInstance();
mi->use(driver);
auto pipeline = getPipelineState(ma, Variant::NO_VARIANT);
auto pipeline = getPipelineState(mi, Variant::NO_VARIANT);
driver.scissor(mi->getScissor());
driver.draw(pipeline, mFullScreenQuadRph, 0, 3, 1);
}
@@ -2926,45 +2927,37 @@ void PostProcessManager::TaaJitterCamera(
void PostProcessManager::configureTemporalAntiAliasingMaterial(backend::DriverApi& driver,
TemporalAntiAliasingOptions const& taaOptions) noexcept {
FMaterial* const ma = getPostProcessMaterial("taa").getMaterial(mEngine, driver);
FMaterial::SpecializationConstantsBuilder maConstants = ma->getSpecializationConstantsBuilder();
maConstants.set("upscaling", taaOptions.upscaling > 1.0f);
maConstants.set("historyReprojection", taaOptions.historyReprojection);
maConstants.set("filterHistory", taaOptions.filterHistory);
maConstants.set("filterInput", taaOptions.filterInput);
maConstants.set("useYCoCg", taaOptions.useYCoCg);
maConstants.set("hdr", taaOptions.hdr);
maConstants.set("preventFlickering", taaOptions.preventFlickering);
maConstants.set("boxType", int32_t(taaOptions.boxType));
maConstants.set("boxClipping", int32_t(taaOptions.boxClipping));
maConstants.set("varianceGamma", taaOptions.varianceGamma);
ma->setSpecializationConstants(std::move(maConstants));
FMaterial* const ma = getPostProcessMaterial("taa").getMaterial(mEngine);
ma->getPrograms().setConstants({
{ "upscaling", taaOptions.upscaling > 1.0f },
{ "historyReprojection", taaOptions.historyReprojection },
{ "filterHistory", taaOptions.filterHistory },
{ "filterInput", taaOptions.filterInput },
{ "useYCoCg", taaOptions.useYCoCg },
{ "hdr", taaOptions.hdr },
{ "preventFlickering", taaOptions.preventFlickering },
{ "boxType", int32_t(taaOptions.boxType) },
{ "boxClipping", int32_t(taaOptions.boxClipping) },
{ "varianceGamma", taaOptions.varianceGamma },
});
}
FMaterialInstance* PostProcessManager::configureColorGradingMaterial(backend::DriverApi& driver,
PostProcessMaterial const& material, FColorGrading const* colorGrading,
ColorGradingConfig const& colorGradingConfig, VignetteOptions const& vignetteOptions,
uint32_t const width, uint32_t const height) noexcept {
FMaterial* ma = material.getMaterial(mEngine, driver);
{
FMaterial::SpecializationConstantsBuilder maConstants =
ma->getSpecializationConstantsBuilder();
maConstants.set("isOneDimensional", colorGrading->isOneDimensional());
maConstants.set("isLDR", colorGrading->isLDR());
ma->setSpecializationConstants(std::move(maConstants));
}
FMaterial* ma = material.getMaterial(mEngine);
ma->getPrograms().setConstants({
{ "isOneDimensional", colorGrading->isOneDimensional() },
{ "isLDR", colorGrading->isLDR() },
});
PostProcessVariant const variant = colorGradingConfig.translucent
? PostProcessVariant::TRANSLUCENT
: PostProcessVariant::OPAQUE;
ma = material.getMaterial(mEngine, driver, variant);
FMaterialInstance* mi = nullptr;
int32_t& fixedIndex = colorGradingConfig.translucent
? mFixedMaterialInstanceIndex.colorGradingTranslucent
: mFixedMaterialInstanceIndex.colorGradingOpaque;
std::tie(mi, fixedIndex) = mMaterialInstanceManager.getFixedMaterialInstance(ma);
ma = material.getMaterial(mEngine);
FMaterialInstance* mi =
getMaterialInstanceWithTag(driver, ma, colorGradingConfig.translucent, variant);
const SamplerParams params = SamplerParams{
.filterMag = SamplerMagFilter::LINEAR,
@@ -3076,9 +3069,9 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::taa(FrameGraph& fg,
PostProcessVariant const variant = colorGradingConfig.translucent ?
PostProcessVariant::TRANSLUCENT : PostProcessVariant::OPAQUE;
FMaterial const* const ma = material.getMaterial(mEngine, driver, variant);
FMaterial const* const ma = material.getMaterial(mEngine);
FMaterialInstance* mi = getMaterialInstance(ma);
FMaterialInstance* mi = getMaterialInstance(driver, ma);
mi->setParameter("color", color, SamplerParams{}); // nearest
mi->setParameter("depth", depth, SamplerParams{}); // nearest
mi->setParameter("history", history, SamplerParams{
@@ -3118,7 +3111,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::taa(FrameGraph& fg,
if (colorGradingConfig.asSubpass) {
out.params.subpassMask = 1;
}
auto const pipeline = getPipelineState(ma, variant);
auto const pipeline = getPipelineState(mi, variant);
driver.beginRenderPass(out.target, out.params);
driver.draw(pipeline, mFullScreenQuadRph, 0, 3, 1);
@@ -3200,7 +3193,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::rcas(
mi->commit(driver, getUboManager());
mi->use(driver);
auto pipeline = getPipelineState(material.getMaterial(mEngine, driver), variant);
auto pipeline = getPipelineState(mi, variant);
if (mode == RcasMode::BLENDED) {
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
pipeline.rasterState.blendFunctionSrcAlpha = BlendFunction::ONE;
@@ -3293,7 +3286,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::upscaleBilinear(FrameGraph&
auto out = resources.getRenderPassInfo();
auto pipeline = getPipelineState(material.getMaterial(mEngine, driver));
auto pipeline = getPipelineState(mi);
if (blended) {
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
pipeline.rasterState.blendFunctionSrcAlpha = BlendFunction::ONE;
@@ -3507,15 +3500,15 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::upscaleFSR1(FrameGraph& fg,
auto out = resources.getRenderPassInfo();
if (UTILS_UNLIKELY(twoPassesEASU)) {
auto pipeline0 = getPipelineState(splitEasuMaterial->getMaterial(mEngine, driver));
auto pipeline1 = getPipelineState(easuMaterial->getMaterial(mEngine, driver));
auto pipeline0 = getPipelineState(getMaterialInstance(mEngine, driver, *splitEasuMaterial));
auto pipeline1 = getPipelineState(getMaterialInstance(mEngine, driver, *easuMaterial));
pipeline1.rasterState.depthFunc = SamplerCompareFunc::NE;
driver.beginRenderPass(out.target, out.params);
driver.draw(pipeline0, mFullScreenQuadRph, 0, 3, 1);
driver.draw(pipeline1, mFullScreenQuadRph, 0, 3, 1);
driver.endRenderPass();
} else {
auto pipeline = getPipelineState(easuMaterial->getMaterial(mEngine, driver));
auto pipeline = getPipelineState(getMaterialInstance(mEngine, driver, *easuMaterial));
renderFullScreenQuad(out, pipeline, driver);
}
unbindAllDescriptorSets(driver);
@@ -3567,8 +3560,8 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::blit(FrameGraph& fg, bool co
PostProcessMaterial const& material =
getPostProcessMaterial(layer ? "blitArray" : "blitLow");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
auto* mi = getMaterialInstance(ma);
FMaterial const* const ma = material.getMaterial(mEngine);
auto* mi = getMaterialInstance(driver, ma);
mi->setParameter("color", color, SamplerParams{
.filterMag = filterMag,
.filterMin = filterMin
@@ -3586,7 +3579,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::blit(FrameGraph& fg, bool co
mi->commit(driver, getUboManager());
mi->use(driver);
auto pipeline = getPipelineState(ma);
auto pipeline = getPipelineState(mi);
if (translucent) {
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
pipeline.rasterState.blendFunctionSrcAlpha = BlendFunction::ONE;
@@ -3823,17 +3816,15 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::vsmMipmapPass(FrameGraph& fg
auto const& inDesc = resources.getDescriptor(data.in);
auto width = inDesc.width;
assert_invariant(width == inDesc.height);
int const dim = width >> (level + 1);
uint32_t const dim = std::max(1u, width >> (level + 1));
auto& material = getPostProcessMaterial("vsmMipmap");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
FMaterial const* const ma = material.getMaterial(mEngine);
FMaterialInstance* const mi = getMaterialInstance(driver, ma);
// When generating shadow map mip levels, we want to preserve the 1 texel border.
// (note clearing never respects the scissor in Filament)
auto const pipeline = getPipelineState(ma);
backend::Viewport const scissor = { 1u, 1u, dim - 2u, dim - 2u };
auto const pipeline = getPipelineState(mi);
backend::Viewport const scissor = { 0, 0, dim, dim };
FMaterialInstance* const mi = getMaterialInstance(ma);
mi->setParameter("color", in, SamplerParams{
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
@@ -3928,10 +3919,10 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::debugCombineArrayTexture(Fra
// set uniforms
PostProcessMaterial const& material = getPostProcessMaterial("blitArray");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
FMaterial const* const ma = material.getMaterial(mEngine);
// It should be ok to not move this getMaterialInstance to inside the loop, since
// this is a pass meant for debug.
auto* mi = getMaterialInstance(ma);
auto* mi = getMaterialInstance(driver, ma);
mi->setParameter("color", color, SamplerParams{
.filterMag = filterMag,
.filterMin = filterMin
@@ -3945,7 +3936,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::debugCombineArrayTexture(Fra
mi->commit(driver, getUboManager());
mi->use(driver);
auto pipeline = getPipelineState(ma);
auto pipeline = getPipelineState(mi);
if (translucent) {
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
pipeline.rasterState.blendFunctionSrcAlpha = BlendFunction::ONE;
+35 -27
View File
@@ -357,13 +357,7 @@ public:
void terminate(FEngine& engine) noexcept;
FMaterial* getMaterial(FEngine& engine, backend::DriverApi& driver,
Variant::type_t variant) const noexcept;
FMaterial* getMaterial(FEngine& engine, backend::DriverApi& driver,
PostProcessVariant variant = PostProcessVariant::OPAQUE) const noexcept {
return getMaterial(engine, driver, Variant::type_t(variant));
}
FMaterial* getMaterial(FEngine& engine) const noexcept;
private:
void loadMaterial(FEngine& engine) const noexcept;
@@ -388,11 +382,12 @@ public:
void bindPostProcessDescriptorSet(backend::DriverApi& driver) const noexcept;
backend::PipelineState getPipelineState(FMaterial const* ma, Variant::type_t variant) const noexcept;
backend::PipelineState getPipelineState(FMaterialInstance const* mi,
Variant::type_t variant) const noexcept;
backend::PipelineState getPipelineState(FMaterial const* ma,
PostProcessVariant variant = PostProcessVariant::OPAQUE) const noexcept {
return getPipelineState(ma, Variant::type_t(variant));
backend::PipelineState getPipelineState(FMaterialInstance const* mi,
PostProcessVariant variant = PostProcessVariant::OPAQUE) const noexcept {
return getPipelineState(mi, Variant::type_t(variant));
}
void renderFullScreenQuad(FrameGraphResources::RenderPassInfo const& out,
@@ -427,17 +422,37 @@ private:
void bindPerRenderableDescriptorSet(backend::DriverApi& driver) const noexcept;
// Helper to get a MaterialInstance from a FMaterial
// This currently just call FMaterial::getDefaultInstance().
FMaterialInstance* getMaterialInstance(FMaterial const* ma) {
return mMaterialInstanceManager.getMaterialInstance(ma);
// Helpers to get MaterialInstances.
//
// These funcions additionally ensure that the necessary shader programs are compiled via
// prepareProgram().
FMaterialInstance* getMaterialInstance(backend::DriverApi& driver, FMaterial const* ma,
Variant::type_t variant) const;
FMaterialInstance* getMaterialInstance(backend::DriverApi& driver, FMaterial const* ma,
PostProcessVariant variant = PostProcessVariant::OPAQUE) const {
return getMaterialInstance(driver, ma, Variant::type_t(variant));
}
// Helper to get a MaterialInstance from a PostProcessMaterial.
FMaterialInstance* getMaterialInstance(FEngine& engine, backend::DriverApi& driver, PostProcessMaterial const& material,
PostProcessVariant variant = PostProcessVariant::OPAQUE) {
FMaterial const* ma = material.getMaterial(engine, driver, variant);
return getMaterialInstance(ma);
FMaterialInstance* getMaterialInstance(FEngine& engine, backend::DriverApi& driver,
PostProcessMaterial const& material,
PostProcessVariant variant = PostProcessVariant::OPAQUE) const {
return getMaterialInstance(driver, material.getMaterial(engine), Variant::type_t(variant));
}
FMaterialInstance* getMaterialInstanceWithTag(backend::DriverApi& driver, FMaterial const* ma,
uint32_t tag, Variant::type_t variant) const;
FMaterialInstance* getMaterialInstanceWithTag(backend::DriverApi& driver, FMaterial const* ma,
uint32_t tag, PostProcessVariant variant = PostProcessVariant::OPAQUE) const {
return getMaterialInstanceWithTag(driver, ma, tag, Variant::type_t(variant));
}
FMaterialInstance* getMaterialInstanceWithTag(FEngine& engine, backend::DriverApi& driver,
PostProcessMaterial const& material, uint32_t tag,
PostProcessVariant variant = PostProcessVariant::OPAQUE) const {
return getMaterialInstanceWithTag(driver, material.getMaterial(engine), tag,
Variant::type_t(variant));
}
UboManager* getUboManager() const noexcept;
@@ -486,13 +501,6 @@ private:
MaterialInstanceManager mMaterialInstanceManager;
struct {
int32_t colorGradingTranslucent = MaterialInstanceManager::INVALID_FIXED_INDEX;
int32_t colorGradingOpaque = MaterialInstanceManager::INVALID_FIXED_INDEX;
int32_t customResolve = MaterialInstanceManager::INVALID_FIXED_INDEX;
int32_t clearDepth = MaterialInstanceManager::INVALID_FIXED_INDEX;
} mFixedMaterialInstanceIndex;
backend::Handle<backend::HwTexture> mStarburstTexture;
std::uniform_real_distribution<float> mUniformDistribution{0.0f, 1.0f};
+9 -12
View File
@@ -243,8 +243,8 @@ void RenderPass::appendCommands(FEngine const& engine, backend::DriverApi& drive
// This must be done from the main thread.
for (Command const* first = curr, *last = curr + commandCount ; first != last ; ++first) {
if (UTILS_LIKELY((first->key & CUSTOM_MASK) == uint64_t(CustomCommand::PASS))) {
auto ma = first->info.mi->getMaterial();
ma->prepareProgram(driver, first->info.materialVariant, CompilerPriorityQueue::CRITICAL);
first->info.mi->prepareProgram(driver, first->info.materialVariant,
CompilerPriorityQueue::CRITICAL);
}
}
}
@@ -426,16 +426,18 @@ void RenderPass::setupColorCommand(Command& cmdDraw, Variant variant,
bool const isBlendingCommand = !hasScreenSpaceRefraction &&
(blendingMode != BlendingMode::OPAQUE && blendingMode != BlendingMode::MASKED);
RasterState rasterState = mi->getRasterState();
uint64_t keyDraw = cmdDraw.key;
keyDraw &= ~(PASS_MASK | BLENDING_MASK | MATERIAL_MASK);
keyDraw |= uint64_t(hasScreenSpaceRefraction ? Pass::REFRACT : Pass::COLOR);
keyDraw |= uint64_t(CustomCommand::PASS);
keyDraw |= mi->getSortingKey(); // already all set-up for direct or'ing
keyDraw |= makeField(variant.key, MATERIAL_VARIANT_KEY_MASK, MATERIAL_VARIANT_KEY_SHIFT);
keyDraw |= makeField(ma->getRasterState().alphaToCoverage, BLENDING_MASK, BLENDING_SHIFT);
keyDraw |= makeField(rasterState.alphaToCoverage, BLENDING_MASK, BLENDING_SHIFT);
cmdDraw.key = isBlendingCommand ? keyBlending : keyDraw;
cmdDraw.info.rasterState = ma->getRasterState();
cmdDraw.info.rasterState = rasterState;
// for SSR pass, the blending mode of opaques (including MASKED) must be off
// see Material.cpp.
@@ -446,10 +448,6 @@ void RenderPass::setupColorCommand(Command& cmdDraw, Variant variant,
BlendFunction::ZERO : cmdDraw.info.rasterState.blendFunctionDstAlpha;
cmdDraw.info.rasterState.inverseFrontFaces = inverseFrontFaces;
cmdDraw.info.rasterState.culling = mi->getCullingMode();
cmdDraw.info.rasterState.colorWrite = mi->isColorWriteEnabled();
cmdDraw.info.rasterState.depthWrite = mi->isDepthWriteEnabled();
cmdDraw.info.rasterState.depthFunc = mi->getDepthFunc();
cmdDraw.info.rasterState.depthClamp = hasDepthClamp;
cmdDraw.info.materialVariant = variant;
// we keep "RasterState::colorWrite" to the value set by material (could be disabled)
@@ -568,7 +566,7 @@ RenderPass::Command* RenderPass::generateCommandsImpl(CommandTypeFlags extraFlag
if constexpr (isDepthPass) {
cmd.info.materialVariant = variant;
cmd.info.rasterState = {};
cmd.info.rasterState.colorWrite = Variant::isPickingVariant(variant) || Variant::isVSMVariant(variant);
cmd.info.rasterState.colorWrite = Variant::isPickingVariant(variant) || Variant::isDepthMomentsVariant(variant);
cmd.info.rasterState.depthWrite = true;
cmd.info.rasterState.depthFunc = RasterState::DepthFunc::GE;
cmd.info.rasterState.alphaToCoverage = false;
@@ -616,7 +614,7 @@ RenderPass::Command* RenderPass::generateCommandsImpl(CommandTypeFlags extraFlag
bool const hasSkinningOrMorphing = hasSkinning || hasMorphing;
// if we are already an SSR variant, the SRE bit is already set
static_assert(Variant::SPECIAL_SSR & Variant::SRE);
static_assert(Variant::SPECIAL_SSR_VARIANT & Variant::SRE);
Variant renderableVariant{ variant };
// we can't have SSR and shadowing together by construction
@@ -1090,8 +1088,7 @@ void RenderPass::Executor::execute(FEngine const& engine, DriverApi& driver,
mi->use(driver, info.materialVariant);
}
assert_invariant(ma);
pipeline.program = ma->getProgram(info.materialVariant);
pipeline.program = mi->getProgram(info.materialVariant);
if (UTILS_UNLIKELY(memcmp(&pipeline, &currentPipeline, sizeof(PipelineState)) != 0)) {
currentPipeline = pipeline;
+1 -1
View File
@@ -54,7 +54,7 @@ RenderableManager::getInstance(Entity const e) const noexcept {
}
void RenderableManager::destroy(Entity const e) noexcept {
return downcast(this)->destroy(e);
return downcast(this)->clientDestroy(e);
}
void RenderableManager::setAxisAlignedBoundingBox(Instance const instance, const Box& aabb) {
+136 -141
View File
@@ -62,6 +62,8 @@ ShadowMap::ShadowMap(FEngine& engine) noexcept
: mPerShadowMapUniforms(engine),
mShadowType(ShadowType::DIRECTIONAL),
mHasVisibleShadows(false),
mVsm(false),
mReservedBit(false),
mFace(0) {
Entity entities[2];
engine.getEntityManager().create(2, entities);
@@ -85,7 +87,7 @@ void ShadowMap::terminate(FEngine& engine) {
ShadowMap::~ShadowMap() = default;
void ShadowMap::initialize(size_t const lightIndex, ShadowType const shadowType,
void ShadowMap::initialize(size_t const lightIndex, ShadowType const shadowType, bool const vsm,
uint16_t const shadowIndex, uint8_t const face,
LightManager::ShadowOptions const* options) {
mLightIndex = lightIndex;
@@ -93,6 +95,7 @@ void ShadowMap::initialize(size_t const lightIndex, ShadowType const shadowType,
mOptions = options;
mShadowType = shadowType;
mFace = face;
mVsm = vsm;
}
mat4f ShadowMap::getDirectionalLightViewMatrix(float3 direction, float3 up,
@@ -213,12 +216,14 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine,
* Final shadow map transform
*/
// Final shadow transform (focused warped light-space)
// Final shadow transform (focused warped light-space), world space to clip space
const mat4f S = F * (W * LMpMv);
// Computes St the transform to use in the shader to access the shadow map texture
// i.e. it transforms a world-space vertex to a texture coordinate in the shadowmap
const auto [Mt, Mn] = getTextureCoordsMapping(shadowMapInfo, getViewport());
// world space to texture atlas space
const mat4f St = highPrecisionMultiply(Mt, S);
ShaderParameters shaderParameters;
@@ -226,14 +231,12 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine,
// note: in texelSizeWorldSpace() below, we can use Mb * Mt * F * W because
// L * Mp * Mv is a rigid transform for directional lights, and doesn't matter.
// if Wp[3][1] is 0, then LISPSM was cancelled.
// if Wp[3][1] is 0, then LiSPSM was canceled.
if (useLispsm && Wp[3][1] != 0.0f) {
shaderParameters.texelSizeAtOneMeterWs =
texelSizeWorldSpace(Wp, mat4f(Mt * F), shadowMapInfo.shadowDimension);
shaderParameters.texelSizeAtOneMeterWs = texelSizeWorldSpace(S, shadowMapInfo.shadowDimension);
} else {
// We know we're using an ortho projection
shaderParameters.texelSizeAtOneMeterWs =
texelSizeWorldSpace(St.upperLeft(), shadowMapInfo.shadowDimension);
shaderParameters.texelSizeAtOneMeterWs = texelSizeWorldSpace(S.upperLeft(), shadowMapInfo.shadowDimension);
}
if (!shadowMapInfo.vsm) {
shaderParameters.lightSpace = St;
@@ -1117,90 +1120,72 @@ bool ShadowMap::intersectSegmentWithPlanarQuad(float3& UTILS_RESTRICT p,
return hit;
}
float ShadowMap::texelSizeWorldSpace(const mat3f& worldToShadowTexture,
uint16_t shadowDimension) noexcept {
float2 ShadowMap::texelSizeWorldSpace(const mat3f& clipFromWorld, uint16_t shadowDimension) noexcept {
// The Jacobian of the transformation from texture-to-world is the matrix itself for
// orthographic projections. We just need to inverse worldToShadowTexture,
// orthographic projections. We just need to inverse shadowMapFromWorld,
// which is guaranteed to be orthographic.
// The two first columns give us how a texel maps in world-space.
const float ures = 1.0f / float(shadowDimension);
const float vres = 1.0f / float(shadowDimension);
const mat3f shadowTextureToWorld(inverse(worldToShadowTexture));
const float3 Jx = shadowTextureToWorld[0];
const float3 Jy = shadowTextureToWorld[1];
const float s = std::max(length(Jx) * ures, length(Jy) * vres);
float const oneTexel = 2.0f / float(shadowDimension);
mat3f const worldFromClip(inverse(clipFromWorld));
float3 const Jx = worldFromClip[0];
float3 const Jy = worldFromClip[1];
float2 const s = float2{ length(Jx), length(Jy) } * oneTexel;
return s;
}
float ShadowMap::texelSizeWorldSpace(const mat4f& Wp, const mat4f& MbMtF,
uint16_t shadowDimension) noexcept {
// Here we compute the Jacobian of inverse(MbMtF * Wp).
// The expression below has been computed with Mathematica. However, it's not very hard,
// albeit error-prone, to do it by hand because MbMtF is a linear transform.
// So we really only need to calculate the Jacobian of inverse(Wp) at inverse(MbMtF).
//
// Because we're only interested in the length of the columns of the Jacobian, we can use
// Mb * Mt * F * Wp instead of the full expression Mb * Mt * F * Wp * Wv * L * Mp * Mv,
// because Wv * L * Mp * Mv is a rigid transform, which doesn't affect the length of
// the Jacobian's column vectors.
/**
* Calculates the Jacobian matrix J = ∂(u,v,d)/∂(x,y,z) for a 4x4 perspective projection.
*
* Given a view-space point P = [x, y, z, 1]^T and a projection matrix M,
* the projected homogeneous coordinates are [X, Y, Z, W]^T = M * P.
* The resulting NDC coordinates are u = X/W, v = Y/W, and depth d = Z/W.
*
* To find the Jacobian on the CPU, we apply the quotient rule to each component:
* ∂(X/W) / ∂xi = ( (∂X/∂xi) * W - X * (∂W/∂xi) ) / W^2
*
* In matrix form, this can be expressed as:
* J = (1/W) * [ M_sub - (1/W) * (T ⊗ w_grad) ]
*
* Where:
* - W: The homogeneous w-component after projection (usually -z for standard mats).
* - M_sub: The top-left 3x3 submatrix of M.
* - T: The column vector [X, Y, Z]^T (pre-perspective divide).
* - w_grad: The row vector [m30, m31, m32] (the first three elements of M's last row).
* - ⊗: The outer product, resulting in a 3x3 matrix.
*
* This Jacobian describes the local "stretch" of the projection. For LiSPSM or
* shadow mapping, the inverse Jacobian J^-1 provides the world-space footprint
* of a shadow texel, which is essential for calculating an accurate,
* non-constant depth bias to eliminate shadow acne.
*
* @param M The 4x4 projection matrix.
* @param p The 3D point in view-space where the Jacobian is evaluated.
* @return A 3x3 matrix representing the partial derivatives of (u,v,d) w.r.t (x,y,z).
*/
static mat3f jacobian(mat4f const& M, float3 const& p) noexcept {
float4 const T = M * p;
mat3f const M_sub = M.upperLeft();
float3 const w_grad = { M[0].w, M[1].w, M[2].w };
mat3f const t_cross_w{
w_grad.x * T.xyz,
w_grad.y * T.xyz,
w_grad.z * T.xyz
};
return (M_sub - t_cross_w / T.w) / T.w;
}
float2 ShadowMap::texelSizeWorldSpace(mat4f const& S, uint16_t const shadowDimension) noexcept {
// The Jacobian is not constant, so we evaluate it in the center of the shadow-map texture.
// It might be better to do this computation in the vertex shader.
float3 const p = { 0.5f, 0.5f, 0.0f };
const float ures = 1.0f / float(shadowDimension);
const float vres = 1.0f / float(shadowDimension);
const float dres = 1.0f / 65536.0f;
constexpr bool JACOBIAN_ESTIMATE = false;
if constexpr (JACOBIAN_ESTIMATE) {
// This estimates the Jacobian -- this is a lot heavier. This is mostly for reference
// and testing.
const mat4f Si(inverse(MbMtF * Wp));
const float3 p0 = mat4f::project(Si, p);
const float3 p1 = mat4f::project(Si, p + float3{ 1, 0, 0 } * ures);
const float3 p2 = mat4f::project(Si, p + float3{ 0, 1, 0 } * vres);
const float3 p3 = mat4f::project(Si, p + float3{ 0, 0, 1 } * dres);
const float3 Jx = p1 - p0;
const float3 Jy = p2 - p0;
const float3 UTILS_UNUSED Jz = p3 - p0;
const float s = std::max(length(Jx), length(Jy));
return s;
}
const float n = Wp[0][0];
const float A = Wp[1][1];
const float B = Wp[3][1];
const float sx = MbMtF[0][0];
const float sy = MbMtF[1][1];
const float sz = MbMtF[2][2];
const float ox = MbMtF[3][0];
const float oy = MbMtF[3][1];
const float oz = MbMtF[3][2];
const float X = p.x - ox;
const float Y = p.y - oy;
const float Z = p.z - oz;
const float dz = A * sy - Y;
const float nsxsz = n * sx * sz;
const float j = -(B * sy) / (nsxsz * dz * dz);
const mat3f J(mat3f::row_major_init{
j * dz * sz, j * X * sz, 0.0f,
0.0f, j * nsxsz, 0.0f,
0.0f, j * Z * sx, j * dz * sx
});
float3 const Jx = J[0] * ures;
float3 const Jy = J[1] * vres;
UTILS_UNUSED float3 const Jz = J[2] * dres;
const float s = std::max(length(Jx), length(Jy));
float3 const p = { 0.0f, 0.0f, 0.0f }; // clip-space
float const oneTexel = 2.0f / float(shadowDimension);
mat3f const J = jacobian(inverse(S), p);
float2 const s = float2{ length(J[0]), length(J[1]) } * oneTexel;
return s;
}
template<typename Casters, typename Receivers>
void ShadowMap::visitScene(const FScene& scene, uint32_t const visibleLayers,
Casters casters, Receivers receivers) noexcept {
template<typename Visitor>
void ShadowMap::visitScene(const FScene& scene, uint32_t const visibleLayers, Visitor visitor) noexcept {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
using State = FRenderableManager::Visibility;
@@ -1213,14 +1198,11 @@ void ShadowMap::visitScene(const FScene& scene, uint32_t const visibleLayers,
size_t const c = soa.size();
for (size_t i = 0; i < c; i++) {
if (layers[i] & visibleLayers) {
const Aabb aabb{ worldAABBCenter[i] - worldAABBExtent[i],
worldAABBCenter[i] + worldAABBExtent[i] };
if (visibility[i].castShadows) {
casters(aabb, visibleMasks[i]);
}
if (visibility[i].receiveShadows) {
receivers(aabb, visibleMasks[i]);
}
Aabb const aabb{
worldAABBCenter[i] - worldAABBExtent[i],
worldAABBCenter[i] + worldAABBExtent[i]
};
visitor(aabb, visibleMasks[i], visibility[i]);
}
}
}
@@ -1239,13 +1221,15 @@ ShadowMap::SceneInfo::SceneInfo(
wsShadowCastersVolume = {};
wsShadowReceiversVolume = {};
visitScene(scene, visibleLayers,
[&](Aabb caster, Culler::result_type) {
wsShadowCastersVolume.min = min(wsShadowCastersVolume.min, caster.min);
wsShadowCastersVolume.max = max(wsShadowCastersVolume.max, caster.max);
},
[&](Aabb receiver, Culler::result_type) {
wsShadowReceiversVolume.min = min(wsShadowReceiversVolume.min, receiver.min);
wsShadowReceiversVolume.max = max(wsShadowReceiversVolume.max, receiver.max);
[this](Aabb const& aabb, Culler::result_type, FRenderableManager::Visibility const visibility) {
if (visibility.castShadows) {
wsShadowCastersVolume.min = min(wsShadowCastersVolume.min, aabb.min);
wsShadowCastersVolume.max = max(wsShadowCastersVolume.max, aabb.max);
}
if (visibility.receiveShadows) {
wsShadowReceiversVolume.min = min(wsShadowReceiversVolume.min, aabb.min);
wsShadowReceiversVolume.max = max(wsShadowReceiversVolume.max, aabb.max);
}
}
);
}
@@ -1259,18 +1243,20 @@ void ShadowMap::updateSceneInfoDirectional(mat4f const& Mv, FScene const& scene,
sceneInfo.lsCastersNearFar = { std::numeric_limits<float>::lowest(), std::numeric_limits<float>::max() };
sceneInfo.lsReceiversNearFar = { std::numeric_limits<float>::lowest(), std::numeric_limits<float>::max() };
visitScene(scene, sceneInfo.visibleLayers,
[&](Aabb caster, Culler::result_type) {
auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, caster);
sceneInfo.lsCastersNearFar.x = max(sceneInfo.lsCastersNearFar.x, r.max.z);
sceneInfo.lsCastersNearFar.y = min(sceneInfo.lsCastersNearFar.y, r.min.z);
},
[&](Aabb receiver, Culler::result_type const vis) {
// account only for objects that are visible by the camera
auto mask = 1u << VISIBLE_RENDERABLE_BIT;
if ((vis & mask) == mask) {
auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, receiver);
sceneInfo.lsReceiversNearFar.x = max(sceneInfo.lsReceiversNearFar.x, r.max.z);
sceneInfo.lsReceiversNearFar.y = min(sceneInfo.lsReceiversNearFar.y, r.min.z);
[&](Aabb const& aabb, Culler::result_type const vis, FRenderableManager::Visibility const visibility) {
if (visibility.castShadows) {
auto const r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, aabb);
sceneInfo.lsCastersNearFar.x = max(sceneInfo.lsCastersNearFar.x, r.max.z);
sceneInfo.lsCastersNearFar.y = min(sceneInfo.lsCastersNearFar.y, r.min.z);
}
if (visibility.castShadows) {
// account only for objects that are visible by the camera
constexpr auto mask = 1u << VISIBLE_RENDERABLE_BIT;
if ((vis & mask) == mask) {
auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, aabb);
sceneInfo.lsReceiversNearFar.x = max(sceneInfo.lsReceiversNearFar.x, r.max.z);
sceneInfo.lsReceiversNearFar.y = min(sceneInfo.lsReceiversNearFar.y, r.min.z);
}
}
}
);
@@ -1285,15 +1271,15 @@ void ShadowMap::updateSceneInfoSpot(mat4f const& Mv, FScene const& scene,
sceneInfo.lsCastersNearFar = { std::numeric_limits<float>::lowest(), std::numeric_limits<float>::max() };
// account only for objects that are visible by both the camera and the light
visitScene(scene, sceneInfo.visibleLayers,
[&](Aabb caster, Culler::result_type const vis) {
auto mask = VISIBLE_DYN_SHADOW_RENDERABLE;
if ((vis & mask) == mask) {
auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, caster);
sceneInfo.lsCastersNearFar.x = std::max(sceneInfo.lsCastersNearFar.x, r.max.z); // near
sceneInfo.lsCastersNearFar.y = std::min(sceneInfo.lsCastersNearFar.y, r.min.z); // far
[&](Aabb const& aabb, Culler::result_type const vis, FRenderableManager::Visibility const visibility) {
if (visibility.castShadows) {
constexpr auto mask = VISIBLE_DYN_SHADOW_RENDERABLE;
if ((vis & mask) == mask) {
auto const r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, aabb);
sceneInfo.lsCastersNearFar.x = std::max(sceneInfo.lsCastersNearFar.x, r.max.z); // near
sceneInfo.lsCastersNearFar.y = std::min(sceneInfo.lsCastersNearFar.y, r.min.z); // far
}
}
},
[&](Aabb receiver, Culler::result_type) {
}
);
}
@@ -1303,47 +1289,56 @@ void ShadowMap::setAllocation(uint8_t const layer, backend::Viewport viewport) n
}
backend::Viewport ShadowMap::getViewport() const noexcept {
// We set a viewport with a 1-texel border for when we index outside the texture.
// This happens only for directional lights when "focus shadow casters" is used,
// or when shadowFar is smaller than the camera far.
// For spot- and point-lights we also use a 1-texel border, so that bilinear filtering
// can work properly if the shadowmap is in an atlas (and we can't rely on h/w clamp).
// This is used for calculating the light space; in particular, if the shadowmap is in a 2D atlas, this
// returns the valid area of the shadowmap. By definition all values must be integer.
// We set a viewport with a 1-texel border for when we index outside the texture, which should only happen for
// directional lights when "focus shadow casters" is used, or when shadowFar is smaller than the camera far.
// For directional lights, the border is filed with "fully lit".
//
// For point-lights, we also use a 1-texel border, but for a different reason; the border is filled with data
// so that bilinear (PCF) filtering works properly.
//
// Spot-light a treated like point lights (the border will be filled with "fully-lit" anyways)
const uint32_t dim = mOptions->mapSize;
const uint16_t border = 1u;
return { mOffset.x + border, mOffset.y + border, dim - 2u * border, dim - 2u * border };
}
backend::Viewport ShadowMap::getScissor() const noexcept {
// We set a viewport with a 1-texel border for when we index outside the texture.
// This happens only for directional lights when "focus shadow casters" is used,
// or when shadowFar is smaller than the camera far.
// For spot- and point-lights we also use a 1-texel border, so that bilinear filtering
// can work properly if the shadowmap is in an atlas (and we can't rely on h/w clamp), so we
// don't scissor the border, so it gets filled with correct neighboring texels.
// This is used while rendering the shadowmap.
const uint32_t dim = mOptions->mapSize;
const uint16_t border = 1u;
switch (mShadowType) {
case ShadowType::DIRECTIONAL:
return { mOffset.x + border, mOffset.y + border, dim - 2u * border, dim - 2u * border };
switch (mShadowType) { // NOLINT(*-multiway-paths-covered)
case ShadowType::DIRECTIONAL: {
// Don't render anything into the border; it's already filled with "fully lit".
return {mOffset.x + border, mOffset.y + border, dim - 2u * border, dim - 2u * border};
}
case ShadowType::SPOT:
case ShadowType::POINT:
return { mOffset.x, mOffset.y, dim, dim };
case ShadowType::POINT: {
// Render into the border (which will get the adjacent faces texels)
return {mOffset.x, mOffset.y, dim, dim};
}
}
return {};
}
float4 ShadowMap::getClampToEdgeCoords(ShadowMapInfo const& shadowMapInfo) const noexcept {
float border; // shadowmap border in texels
switch (mShadowType) {
// This is used to clamp the texture coordinates when sampling the shadowmap
float border = 0; // shadowmap border in texels
switch (mShadowType) { // NOLINT(*-multiway-paths-covered)
case ShadowType::DIRECTIONAL:
// For directional lights, we need to allow the sampling to reach the border, it
// happens when "focus shadow casters" is used for instance.
// For directional lights, we need to allow the sampling to reach the center of border texels,
// but no further, so we don't read into the adjacent texture in the 2D atlas (taking bilinear filtering
// into account).
border = 0.5f;
break;
case ShadowType::SPOT:
case ShadowType::POINT:
// For spot and point light, this is equal to the viewport. i.e. the valid
// texels are inside the viewport (w/ 1-texel border), the border will be used
// for bilinear filtering.
// For point-light, the border is only needed for bilinear filtering (of the other faces)
border = 1.0f;
break;
}
@@ -1372,8 +1367,8 @@ void ShadowMap::prepareCamera(Transaction const& transaction,
}
void ShadowMap::prepareViewport(Transaction const& transaction,
backend::Viewport const& viewport) noexcept {
ShadowMapDescriptorSet::prepareViewport(transaction, viewport);
backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept {
ShadowMapDescriptorSet::prepareViewport(transaction, physicalViewport, logicalViewport);
}
void ShadowMap::prepareTime(Transaction const& transaction,
@@ -1387,8 +1382,8 @@ void ShadowMap::prepareMaterialGlobals(Transaction const& transaction,
}
void ShadowMap::prepareShadowMapping(Transaction const& transaction,
bool const highPrecision) noexcept {
ShadowMapDescriptorSet::prepareShadowMapping(transaction, highPrecision);
float const vsmExponent, float const vsmMaxMoment) noexcept {
ShadowMapDescriptorSet::prepareShadowMapping(transaction, vsmExponent, vsmMaxMoment);
}
ShadowMapDescriptorSet::Transaction ShadowMap::open(DriverApi& driver) noexcept {
+15 -14
View File
@@ -128,14 +128,14 @@ public:
static math::mat4f getPointLightViewMatrix(backend::TextureCubemapFace face,
math::float3 position) noexcept;
void initialize(size_t lightIndex, ShadowType shadowType, uint16_t shadowIndex, uint8_t face,
void initialize(size_t lightIndex, ShadowType shadowType, bool vsm, uint16_t shadowIndex, uint8_t face,
LightManager::ShadowOptions const* options);
struct ShaderParameters {
math::mat4f lightSpace{};
math::float4 lightFromWorldZ{};
math::float4 scissorNormalized{};
float texelSizeAtOneMeterWs{};
math::float2 texelSizeAtOneMeterWs{};
};
// Call once per frame if the light, scene (or visible layers) or camera changes.
@@ -173,7 +173,10 @@ public:
static void updateSceneInfoSpot(const math::mat4f& Mv, FScene const& scene,
SceneInfo& sceneInfo);
LightManager::ShadowOptions const* getShadowOptions() const noexcept { return mOptions; }
LightManager::ShadowOptions const& getShadowOptions() const noexcept {
assert_invariant(mOptions);
return *mOptions;
}
size_t getLightIndex() const { return mLightIndex; }
uint16_t getShadowIndex() const { return mShadowIndex; }
void setAllocation(uint8_t layer, backend::Viewport viewport) noexcept;
@@ -193,13 +196,13 @@ public:
static void prepareCamera(Transaction const& transaction,
FEngine const& engine, const CameraInfo& cameraInfo) noexcept;
static void prepareViewport(Transaction const& transaction,
backend::Viewport const& viewport) noexcept;
backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept;
static void prepareTime(Transaction const& transaction,
FEngine const& engine, math::float4 const& userTime) noexcept;
static void prepareMaterialGlobals(Transaction const& transaction,
std::array<math::float4, 4> const& materialGlobals) noexcept;
static void prepareShadowMapping(Transaction const& transaction,
bool highPrecision) noexcept;
float vsmExponent, float vsmMaxMoment) noexcept;
static ShadowMapDescriptorSet::Transaction open(backend::DriverApi& driver) noexcept;
void commit(Transaction& transaction, FEngine& engine, backend::DriverApi& driver) const noexcept;
void bind(backend::DriverApi& driver) const noexcept;
@@ -273,9 +276,8 @@ private:
static inline math::float4 computeBoundingSphere(
math::float3 const* vertices, size_t count) noexcept;
template<typename Casters, typename Receivers>
static void visitScene(FScene const& scene, uint32_t visibleLayers,
Casters casters, Receivers receivers) noexcept;
template<typename Visitor>
static void visitScene(FScene const& scene, uint32_t visibleLayers, Visitor visitor) noexcept;
static inline Aabb compute2DBounds(const math::mat4f& lightView,
math::float3 const* wsVertices, size_t count) noexcept;
@@ -319,11 +321,8 @@ private:
math::float4 getClampToEdgeCoords(ShadowMapInfo const& shadowMapInfo) const noexcept;
static float texelSizeWorldSpace(const math::mat3f& worldToShadowTexture,
uint16_t shadowDimension) noexcept;
static float texelSizeWorldSpace(const math::mat4f& W, const math::mat4f& MbMtF,
uint16_t shadowDimension) noexcept;
static math::float2 texelSizeWorldSpace(const math::mat3f& clipFromWorld, uint16_t shadowDimension) noexcept;
static math::float2 texelSizeWorldSpace(const math::mat4f& clipFromWorld, uint16_t shadowDimension) noexcept;
static constexpr Segment sBoxSegments[12] = {
{ 0, 1 }, { 1, 3 }, { 3, 2 }, { 2, 0 },
@@ -352,9 +351,11 @@ private:
uint8_t mLayer = 0; // our layer in the shadowMap texture // 1
ShadowType mShadowType : 2; // :2
bool mHasVisibleShadows : 1; // :1
bool mVsm : 1; // :1
UTILS_UNUSED bool mReservedBit : 1; // :1
uint8_t mFace : 3; // :3
math::ushort2 mOffset{}; // 4
UTILS_UNUSED uint8_t reserved[4]; // 4
UTILS_UNUSED uint8_t reserved[4] = {}; // 4
};
} // namespace filament
+73 -71
View File
@@ -151,12 +151,14 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::update(
mDirectionalShadowMapCount = builder.mDirectionalShadowMapCount;
mSpotShadowMapCount = builder.mSpotShadowMapCount;
const bool vsm = view.hasVSM();
for (auto const& entry : builder.mShadowMaps) {
auto& shadowMap = getShadowMap(entry.shadowIndex);
shadowMap.initialize(
entry.lightIndex,
entry.shadowType,
vsm,
entry.shadowIndex,
entry.face,
entry.options);
@@ -230,15 +232,12 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
FView& view, CameraInfo const& mainCameraInfo,
float4 const& userTime) noexcept {
const float moment2 = std::numeric_limits<half>::max();
const float moment1 = std::sqrt(moment2);
const float4 vsmClearColor{ moment1, moment2, -moment1, moment2 };
FScene* scene = view.getScene();
assert_invariant(scene);
// make a copy here, because it's a very small structure
const TextureAtlasRequirements textureRequirements = mTextureAtlasRequirements;
VsmShadowOptions const& vsmShadowOptions = view.getVsmShadowOptions();
TextureAtlasRequirements const textureRequirements = mTextureAtlasRequirements;
assert_invariant(textureRequirements.layers <= CONFIG_MAX_SHADOW_LAYERS);
// -------------------------------------------------------------------------------------------
@@ -258,8 +257,6 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
utils::FixedCapacityVector<ShadowPass> passList;
};
VsmShadowOptions const& vsmShadowOptions = view.getVsmShadowOptions();
auto& prepareShadowPass = fg.addPass<PrepareShadowPassData>("Prepare Shadow Pass",
[&](FrameGraph::Builder& builder, auto& data) {
data.passList.reserve(getMaxShadowMapCount());
@@ -392,16 +389,18 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
auto transaction = ShadowMap::open(driver);
ShadowMap::prepareCamera(transaction, engine, cameraInfo);
ShadowMap::prepareViewport(transaction, shadowMap.getViewport());
ShadowMap::prepareViewport(transaction,
{ 0, 0, textureRequirements.size, textureRequirements.size },
shadowMap.getViewport());
ShadowMap::prepareTime(transaction, engine, userTime);
ShadowMap::prepareMaterialGlobals(transaction, view.getMaterialGlobals());
ShadowMap::prepareShadowMapping(transaction,
vsmShadowOptions.highPrecision);
getWrapExponentEVSM(vsmShadowOptions, shadowMap.getShadowOptions()),
getMaxMomentEVSM(vsmShadowOptions));
shadowMap.commit(transaction, engine, driver);
// updatePrimitivesLod must be run before RenderPass::appendCommands.
FView::updatePrimitivesLod(scene->getRenderableData(),
engine, cameraInfo, entry.range);
FView::updatePrimitivesLod(scene->getRenderableData(), engine, cameraInfo, entry.range);
// generate and sort the commands for rendering the shadow map
@@ -430,10 +429,10 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
entry.executor = pass.getExecutor();
if (!view.hasVSM()) {
auto const* options = shadowMap.getShadowOptions();
auto const& options = shadowMap.getShadowOptions();
PolygonOffset const polygonOffset = { // handle reversed Z
.slope = -options->polygonOffsetSlope,
.constant = -options->polygonOffsetConstant
.slope = -options.polygonOffsetSlope,
.constant = -options.polygonOffsetConstant
};
entry.executor.overridePolygonOffset(&polygonOffset);
}
@@ -465,10 +464,10 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
auto const& entry = *first;
const uint8_t layer = entry.shadowMap->getLayer();
const auto* options = entry.shadowMap->getShadowOptions();
const auto& options = entry.shadowMap->getShadowOptions();
const auto msaaSamples = textureRequirements.msaaSamples;
const bool blur = entry.shadowMap->hasVisibleShadows() &&
view.hasVSM() && options->vsm.blurWidth > 0.0f;
view.hasVSM() && options.vsm.blurWidth > 0.0f;
auto last = first;
// loop over each shadow pass to find its layer range
@@ -508,10 +507,9 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
renderTargetDesc.attachments.color[0] = data.output;
renderTargetDesc.attachments.depth = depth;
renderTargetDesc.clearFlags =
TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH;
renderTargetDesc.clearFlags = TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH;
// we need to clear the shadow map with the max EVSM moments
renderTargetDesc.clearColor = vsmClearColor;
renderTargetDesc.clearColor = textureRequirements.clearColor;
renderTargetDesc.samples = msaaSamples;
if (UTILS_UNLIKELY(blur)) {
@@ -530,16 +528,14 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
.attachments = {
.color = { data.tempBlurSrc },
.depth = depth },
.clearColor = vsmClearColor,
.clearColor = textureRequirements.clearColor,
.samples = msaaSamples,
.clearFlags = TargetBufferFlags::COLOR
| TargetBufferFlags::DEPTH
.clearFlags = TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH
});
}
} else {
// the shadowmap layer
data.output = builder.write(data.output,
FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
data.output = builder.write(data.output, FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
renderTargetDesc.attachments.depth = data.output;
renderTargetDesc.clearFlags = TargetBufferFlags::DEPTH;
}
@@ -594,27 +590,27 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
// the whole layer. Blurring should happen per shadowmap, not for the whole
// layer.
const float blurWidth = options->vsm.blurWidth;
// FIXME: this Gaussian blur is not precise enough for EVSM
const float blurWidth = options.vsm.blurWidth;
if (blurWidth > 0.0f) {
const float sigma = (blurWidth + 1.0f) / 6.0f;
size_t kernelWidth = std::ceil((blurWidth - 5.0f) / 4.0f);
size_t kernelWidth = size_t(std::ceil((blurWidth - 5.0f) / 4.0f));
kernelWidth = kernelWidth * 4 + 5;
ppm.gaussianBlurPass(fg,
shadowPass->tempBlurSrc,
shadowPass->output,
false, kernelWidth, sigma);
}
}
// FIXME: mipmapping here is broken because it'll access texels from adjacent
// shadow maps.
// If the shadow texture has more than one level, mipmapping was requested, either directly
// or indirectly via anisotropic filtering.
// So generate the mipmaps for each layer
if (textureRequirements.levels > 1) {
for (size_t level = 0; level < textureRequirements.levels - 1; level++) {
ppm.vsmMipmapPass(fg, prepareShadowPass->shadows, layer, level, vsmClearColor);
}
// If the shadow texture has more than one level, mipmapping was requested, either directly
// or indirectly via anisotropic filtering.
// So generate the mipmaps for each layer
if (UTILS_UNLIKELY(textureRequirements.levels > 1)) {
auto& ppm = engine.getPostProcessManager();
for (size_t level = 0; level < textureRequirements.levels - 1; level++) {
ppm.vsmMipmapPass(fg, prepareShadowPass->shadows, layer, level, textureRequirements.clearColor);
}
}
}
@@ -626,7 +622,8 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng
FView& view, CameraInfo cameraInfo, FScene::RenderableSoa& renderableData,
FScene::LightSoa const& lightData, ShadowMap::SceneInfo sceneInfo) noexcept {
FScene* scene = view.getScene();
FScene const* const scene = view.getScene();
auto const& vsmShadowOptions = view.getVsmShadowOptions();
auto& lcm = engine.getLightManager();
FLightManager::Instance const directionalLight = lightData.elementAt<FScene::LIGHT_INSTANCE>(0);
@@ -650,7 +647,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng
};
bool hasVisibleShadows = false;
utils::Slice<ShadowMap> cascadedShadowMaps = getCascadedShadowMap();
utils::Slice<ShadowMap> const cascadedShadowMaps = getCascadedShadowMap();
if (!cascadedShadowMaps.empty()) {
// Even if we have more than one cascade, we cull directional shadow casters against the
// entire camera frustum, as if we only had a single cascade.
@@ -742,17 +739,17 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng
// Texel size is constant for directional light (although that's not true when LISPSM
// is used, but in that case we're pretending it is).
const float wsTexelSize = shaderParameters.texelSizeAtOneMeterWs;
float2 const wsTexelSize = shaderParameters.texelSizeAtOneMeterWs;
auto& s = mShadowUb.edit();
s.shadows[shadowIndex].layer = shadowMap.getLayer();
s.shadows[shadowIndex].lightFromWorldMatrix = shaderParameters.lightSpace;
s.shadows[shadowIndex].scissorNormalized = shaderParameters.scissorNormalized;
s.shadows[shadowIndex].normalBias = normalBias * wsTexelSize;
s.shadows[shadowIndex].texelSizeAtOneMeter = wsTexelSize;
s.shadows[shadowIndex].normalBias = wsTexelSize * normalBias;
s.shadows[shadowIndex].elvsm = options.vsm.elvsm;
s.shadows[shadowIndex].vsmExponent = getWrapExponentEVSM(vsmShadowOptions, options);
s.shadows[shadowIndex].bulbRadiusLs =
mSoftShadowOptions.penumbraScale * options.shadowBulbRadius / wsTexelSize;
mSoftShadowOptions.penumbraScale * options.shadowBulbRadius / length(wsTexelSize);
shadowTechnique |= ShadowTechnique::SHADOW_MAP;
cascadeHasVisibleShadows |= 0x1u << i;
@@ -802,10 +799,8 @@ void ShadowMapManager::updateSpotVisibilityMasks(
const bool visSpotShadowRenderable = v.castShadows && inVisibleLayer &&
(!v.culling || (mask & VISIBLE_DYN_SHADOW_RENDERABLE));
using Type = Culler::result_type;
visibleMask[i] &= ~Type(VISIBLE_DYN_SHADOW_RENDERABLE);
visibleMask[i] |= Type(visSpotShadowRenderable << VISIBLE_DYN_SHADOW_RENDERABLE_BIT);
visibleMask[i] &= ~Culler::result_type(VISIBLE_DYN_SHADOW_RENDERABLE);
visibleMask[i] |= Culler::result_type(visSpotShadowRenderable << VISIBLE_DYN_SHADOW_RENDERABLE_BIT);
}
}
@@ -814,13 +809,14 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin
FScene::LightSoa const& lightData, ShadowMap::SceneInfo const& sceneInfo) noexcept {
const size_t lightIndex = shadowMap.getLightIndex();
FLightManager::ShadowOptions const* const options = shadowMap.getShadowOptions();
FLightManager::ShadowOptions const& options = shadowMap.getShadowOptions();
auto const& vsmShadowOptions = view.getVsmShadowOptions();
// update the shadow map frustum/camera
const ShadowMap::ShadowMapInfo shadowMapInfo{
.atlasDimension = mTextureAtlasRequirements.size,
.textureDimension = uint16_t(options->mapSize),
.shadowDimension = uint16_t(options->mapSize - 2u),
.textureDimension = uint16_t(options.mapSize),
.shadowDimension = uint16_t(options.mapSize - 2u),
.textureSpaceFlipped = engine.getBackend() == Backend::METAL ||
engine.getBackend() == Backend::VULKAN ||
engine.getBackend() == Backend::WEBGPU,
@@ -833,9 +829,9 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin
// and if we need to generate it, update all the UBO data
if (shadowMap.hasVisibleShadows()) {
const size_t shadowIndex = shadowMap.getShadowIndex();
const float wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs;
const float2 wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs;
// note: normalBias is set to zero for VSM
const float normalBias = shadowMapInfo.vsm ? 0.0f : options->normalBias;
const float normalBias = shadowMapInfo.vsm ? 0.0f : options.normalBias;
auto& s = mShadowUb.edit();
const double n = shadowMap.getCamera().getNear();
@@ -845,12 +841,12 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin
s.shadows[shadowIndex].scissorNormalized = shaderParameters.scissorNormalized;
s.shadows[shadowIndex].normalBias = normalBias * wsTexelSizeAtOneMeter;
s.shadows[shadowIndex].lightFromWorldZ = shaderParameters.lightFromWorldZ;
s.shadows[shadowIndex].texelSizeAtOneMeter = wsTexelSizeAtOneMeter;
s.shadows[shadowIndex].nearOverFarMinusNear = float(n / (f - n));
s.shadows[shadowIndex].elvsm = options->vsm.elvsm;
s.shadows[shadowIndex].elvsm = options.vsm.elvsm;
s.shadows[shadowIndex].vsmExponent = getWrapExponentEVSM(vsmShadowOptions, options);
s.shadows[shadowIndex].bulbRadiusLs =
mSoftShadowOptions.penumbraScale * options->shadowBulbRadius
/ wsTexelSizeAtOneMeter;
mSoftShadowOptions.penumbraScale * options.shadowBulbRadius
/ length(wsTexelSizeAtOneMeter);
}
}
@@ -907,13 +903,14 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap,
const uint8_t face = shadowMap.getFace();
const size_t lightIndex = shadowMap.getLightIndex();
FLightManager::ShadowOptions const* const options = shadowMap.getShadowOptions();
FLightManager::ShadowOptions const& options = shadowMap.getShadowOptions();
auto const& vsmShadowOptions = view.getVsmShadowOptions();
// update the shadow map frustum/camera
const ShadowMap::ShadowMapInfo shadowMapInfo{
.atlasDimension = mTextureAtlasRequirements.size,
.textureDimension = uint16_t(options->mapSize),
.shadowDimension = uint16_t(options->mapSize), // point-lights don't have a border
.textureDimension = uint16_t(options.mapSize),
.shadowDimension = uint16_t(options.mapSize), // point-lights don't have a border
.textureSpaceFlipped = engine.getBackend() == Backend::METAL ||
engine.getBackend() == Backend::VULKAN ||
engine.getBackend() == Backend::WEBGPU,
@@ -926,9 +923,9 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap,
// and if we need to generate it, update all the UBO data
if (shadowMap.hasVisibleShadows()) {
const size_t shadowIndex = shadowMap.getShadowIndex();
const float wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs;
const float2 wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs;
// note: normalBias is set to zero for VSM
const float normalBias = shadowMapInfo.vsm ? 0.0f : options->normalBias;
const float normalBias = shadowMapInfo.vsm ? 0.0f : options.normalBias;
auto& s = mShadowUb.edit();
const double n = shadowMap.getCamera().getNear();
@@ -938,12 +935,12 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap,
s.shadows[shadowIndex].scissorNormalized = shaderParameters.scissorNormalized;
s.shadows[shadowIndex].normalBias = normalBias * wsTexelSizeAtOneMeter;
s.shadows[shadowIndex].lightFromWorldZ = shaderParameters.lightFromWorldZ;
s.shadows[shadowIndex].texelSizeAtOneMeter = wsTexelSizeAtOneMeter;
s.shadows[shadowIndex].nearOverFarMinusNear = float(n / (f - n));
s.shadows[shadowIndex].elvsm = options->vsm.elvsm;
s.shadows[shadowIndex].elvsm = options.vsm.elvsm;
s.shadows[shadowIndex].vsmExponent = getWrapExponentEVSM(vsmShadowOptions, options);
s.shadows[shadowIndex].bulbRadiusLs =
mSoftShadowOptions.penumbraScale * options->shadowBulbRadius
/ wsTexelSizeAtOneMeter;
mSoftShadowOptions.penumbraScale * options.shadowBulbRadius
/ length(wsTexelSizeAtOneMeter);
}
}
@@ -998,7 +995,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(FEngine
lightData.data<FScene::SHADOW_INFO>());
ShadowTechnique shadowTechnique{};
utils::Slice<const ShadowMap> spotShadowMaps = getSpotShadowMaps();
utils::Slice<const ShadowMap> const spotShadowMaps = getSpotShadowMaps();
if (!spotShadowMaps.empty()) {
shadowTechnique |= ShadowTechnique::SHADOW_MAP;
for (ShadowMap const& shadowMap : spotShadowMaps) {
@@ -1038,14 +1035,14 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
for (ShadowMap const& shadowMap : getCascadedShadowMap()) {
// Shadow map size should be the same for all cascades.
auto const& options = shadowMap.getShadowOptions();
maxDimension = std::max(maxDimension, options->mapSize);
elvsm = elvsm || options->vsm.elvsm;
maxDimension = std::max(maxDimension, options.mapSize);
elvsm = elvsm || options.vsm.elvsm;
}
for (ShadowMap const& shadowMap : getSpotShadowMaps()) {
auto const& options = shadowMap.getShadowOptions();
maxDimension = std::max(maxDimension, options->mapSize);
elvsm = elvsm || options->vsm.elvsm;
maxDimension = std::max(maxDimension, options.mapSize);
elvsm = elvsm || options.vsm.elvsm;
}
uint8_t layersNeeded = 0;
@@ -1055,7 +1052,7 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
ShadowMap* pShadowMap) mutable {
// Allocate shadowmap from our Atlas Allocator
auto const& options = pShadowMap->getShadowOptions();
auto allocation = allocator.allocate(options->mapSize);
auto allocation = allocator.allocate(options.mapSize);
assert_invariant(allocation.isValid());
assert_invariant(!allocation.viewport.empty());
pShadowMap->setAllocation(allocation.layer, allocation.viewport);
@@ -1091,6 +1088,7 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
msaaSamples = 1;
}
float4 clearColor{};
TextureFormat format = TextureFormat::DEPTH16;
if (view.hasVSM()) {
if (vsmShadowOptions.highPrecision) {
@@ -1098,6 +1096,9 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
} else {
format = elvsm ? TextureFormat::RGBA16F : TextureFormat::RG16F;
}
const float maxMoment2 = getMaxMomentEVSM(vsmShadowOptions);
const float maxMoment1 = std::sqrt(maxMoment2);
clearColor = { maxMoment1, maxMoment2, 0, 0 };
}
mSoftShadowOptions = view.getSoftShadowOptions();
@@ -1119,7 +1120,8 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
layersNeeded,
mipLevels,
msaaSamples,
format
format,
clearColor
};
}
+26
View File
@@ -47,9 +47,12 @@
#include <utils/Slice.h>
#include <math/mat4.h>
#include <math/half.h>
#include <math/vec4.h>
#include <algorithm>
#include <array>
#include <limits>
#include <memory>
#include <new>
#include <type_traits>
@@ -145,6 +148,28 @@ public:
// for debugging only
utils::FixedCapacityVector<Camera const*> getDirectionalShadowCameras() const noexcept;
static float getMaxMomentEVSM(VsmShadowOptions const& vsmShadowOptions) noexcept {
return vsmShadowOptions.highPrecision ?
std::numeric_limits<float>::max() :
float(std::numeric_limits<math::half>::max());
}
static float getMaxWrapExponentEVSM(VsmShadowOptions const& vsmShadowOptions) noexcept {
constexpr float low = 5.2f; // ~ std::log(std::numeric_limits<math::half>::max()) * 0.5f;
constexpr float high = 40.0f; // ~ std::log(std::numeric_limits<float>::max()) * 0.5f;
return vsmShadowOptions.highPrecision ? high : low;
}
static float getWrapExponentEVSM(
VsmShadowOptions const& vsmShadowOptions,
LightManager::ShadowOptions const& options) noexcept {
constexpr float ABSOLUTE_FILTER_LIMIT = 42.0f;
float const targetExponent = getMaxWrapExponentEVSM(vsmShadowOptions);
float const effectiveFilterRadius = std::max(1.0f, options.vsm.blurWidth);
float const filterCeiling = ABSOLUTE_FILTER_LIMIT / effectiveFilterRadius;
return std::min(targetExponent, filterCeiling);
}
private:
explicit ShadowMapManager(FEngine& engine);
@@ -216,6 +241,7 @@ private:
uint8_t levels = 0;
uint8_t msaaSamples = 1;
backend::TextureFormat format = backend::TextureFormat::DEPTH16;
math::float4 clearColor{};
} mTextureAtlasRequirements;
SoftShadowOptions mSoftShadowOptions;
@@ -85,7 +85,6 @@ void FCameraManager::destroy(FEngine& engine, Entity const e) noexcept {
{ // scope for camera -- it's invalid after this scope.
FCamera* const camera = manager.elementAt<CAMERA>(i);
assert_invariant(camera);
camera->terminate(engine);
engine.getHeapAllocator().destroy(camera);
// Remove the camera component
+14 -13
View File
@@ -577,7 +577,7 @@ void FRenderableManager::create(
FEngine::DriverApi& driver = engine.getDriverApi();
if (UTILS_UNLIKELY(manager.hasComponent(entity))) {
destroy(entity);
destroy(entity, driver);
}
Instance const ci = manager.addComponent(entity);
assert_invariant(ci);
@@ -733,41 +733,42 @@ void FRenderableManager::create(
}
// this destroys a single component from an entity
void FRenderableManager::destroy(Entity const e) noexcept {
void FRenderableManager::destroy(Entity const e, DriverApi& driver) noexcept {
Instance const ci = getInstance(e);
if (ci) {
destroyComponent(ci);
destroyComponent(ci, driver);
mManager.removeComponent(e);
}
}
void FRenderableManager::clientDestroy(utils::Entity e) noexcept {
destroy(e, mEngine.getDriverApi());
}
// this destroys all components in this manager
void FRenderableManager::terminate() noexcept {
void FRenderableManager::terminate(DriverApi& driver) noexcept {
auto& manager = mManager;
if (!manager.empty()) {
DLOG(INFO) << "cleaning up " << manager.getComponentCount()
<< " leaked Renderable components";
while (!manager.empty()) {
Instance const ci = manager.end() - 1;
destroyComponent(ci);
destroyComponent(ci, driver);
manager.removeComponent(manager.getEntity(ci));
}
}
mHwRenderPrimitiveFactory.terminate(mEngine.getDriverApi());
mHwRenderPrimitiveFactory.terminate(driver);
}
void FRenderableManager::gc(EntityManager& em) noexcept {
mManager.gc(em, [this](Entity const e) {
destroy(e);
void FRenderableManager::gc(EntityManager& em, DriverApi& driver) noexcept {
mManager.gc(em, [this, &driver](Entity const e) {
destroy(e, driver);
});
}
// This is basically a Renderable's destructor.
void FRenderableManager::destroyComponent(Instance const ci) noexcept {
void FRenderableManager::destroyComponent(Instance const ci, DriverApi& driver) noexcept {
auto& manager = mManager;
FEngine& engine = mEngine;
FEngine::DriverApi& driver = engine.getDriverApi();
// See create(RenderableManager::Builder&, Entity)
destroyComponentPrimitives(mHwRenderPrimitiveFactory, driver, manager[ci].primitives);
+8 -4
View File
@@ -91,9 +91,9 @@ public:
~FRenderableManager();
// free-up all resources
void terminate() noexcept;
void terminate(backend::DriverApi& driver) noexcept;
void gc(utils::EntityManager& em) noexcept;
void gc(utils::EntityManager& em, backend::DriverApi& driver) noexcept;
/*
* Component Manager APIs
@@ -125,7 +125,11 @@ public:
void create(const Builder& builder, utils::Entity entity);
void destroy(utils::Entity e) noexcept;
void destroy(utils::Entity e, backend::DriverApi& driver) noexcept;
// The client API should have taken an Engine&, but it doesn't, so that's a workaround
// so we can keep the API as it is.
void clientDestroy(utils::Entity e) noexcept;
inline void setAxisAlignedBoundingBox(Instance instance, const Box& aabb);
@@ -239,7 +243,7 @@ public:
};
private:
void destroyComponent(Instance ci) noexcept;
void destroyComponent(Instance ci, backend::DriverApi& driver) noexcept;
static void destroyComponentPrimitives(
HwRenderPrimitiveFactory& factory, backend::DriverApi& driver,
utils::Slice<FRenderPrimitive> primitives) noexcept;
-3
View File
@@ -46,9 +46,6 @@ public:
FCamera(FEngine& engine, utils::Entity e);
void terminate(FEngine&) noexcept { }
// Sets the projection matrices (viewing and culling). The viewing matrice has infinite far.
void setProjection(Projection projection,
double left, double right, double bottom, double top,
+43 -21
View File
@@ -20,6 +20,11 @@
#include "TextureCache.h"
#include "RenderPrimitive.h"
#include "components/CameraManager.h"
#include "components/LightManager.h"
#include "components/RenderableManager.h"
#include "components/TransformManager.h"
#include "details/BufferObject.h"
#include "details/Camera.h"
#include "details/Fence.h"
@@ -58,6 +63,7 @@
#include <utils/CallStack.h>
#include <utils/CString.h>
#include <utils/Invocable.h>
#include <utils/JobSystem.h>
#include <utils/Logger.h>
#include <utils/Panic.h>
#include <utils/PrivateImplementation-impl.h>
@@ -538,12 +544,10 @@ void FEngine::init() {
&debug.shadowmap.debug_directional_shadowmap, [this] {
mMaterials.forEach([this](FMaterial* material) {
if (material->getMaterialDomain() == MaterialDomain::SURFACE) {
FMaterial::SpecializationConstantsBuilder constants =
material->getSpecializationConstantsBuilder();
constants.set(+ReservedSpecializationConstants::
CONFIG_DEBUG_DIRECTIONAL_SHADOWMAP,
debug.shadowmap.debug_directional_shadowmap);
material->setSpecializationConstants(std::move(constants));
material->getPrograms().setConstants({
{ +ReservedSpecializationConstants::CONFIG_DEBUG_DIRECTIONAL_SHADOWMAP,
debug.shadowmap.debug_directional_shadowmap },
});
}
});
});
@@ -552,12 +556,10 @@ void FEngine::init() {
&debug.lighting.debug_froxel_visualization, [this] {
mMaterials.forEach([this](FMaterial* material) {
if (material->getMaterialDomain() == MaterialDomain::SURFACE) {
FMaterial::SpecializationConstantsBuilder constants =
material->getSpecializationConstantsBuilder();
constants.set(+ReservedSpecializationConstants::
CONFIG_DEBUG_FROXEL_VISUALIZATION,
debug.lighting.debug_froxel_visualization);
material->setSpecializationConstants(std::move(constants));
material->getPrograms().setConstants({
{ +ReservedSpecializationConstants::CONFIG_DEBUG_FROXEL_VISUALIZATION,
debug.lighting.debug_froxel_visualization },
});
}
});
});
@@ -590,6 +592,9 @@ void FEngine::shutdown() {
DLOG(INFO) << "CircularBuffer: High watermark " << wm / 1024 << " KiB (" << wmpct << "%)";
#endif
/* Destroy any leftover items in the cache. */
mMaterialCache.terminate(*this);
DriverApi& driver = getDriverApi();
/*
@@ -600,7 +605,7 @@ void FEngine::shutdown() {
mResourceAllocatorDisposer->terminate();
mResourceAllocatorDisposer.reset();
mDFG.terminate(*this); // free-up the DFG
mRenderableManager.terminate(); // free-up all renderables
mRenderableManager.terminate(driver); // free-up all renderables
mLightManager.terminate(); // free-up all lights
mCameraManager.terminate(*this); // free-up all cameras
@@ -764,13 +769,30 @@ void FEngine::prepare(DriverApi& driver) {
});
}
void FEngine::gcManagers() {
// Note: this runs in a Job
auto& em = mEntityManager;
mRenderableManager.gc(em);
mLightManager.gc(em);
mTransformManager.gc(em);
mCameraManager.gc(*this, em);
void FEngine::gc() {
JobSystem& js = getJobSystem();
auto *rootJob = js.createJob();
js.run(
jobs::createJob(js, rootJob, [this] {
// These are safe to GC *sequentially* from a worker thread.
// The JobSystem acts as a release/acquire operation.
mLightManager.gc(mEntityManager);
mTransformManager.gc(mEntityManager);
mCameraManager.gc(*this, mEntityManager);
}));
if (isAsynchronousModeEnabled()) {
// gcDeferredAsyncObjectDestruction() is thread-safe in this context because
// 1. JobSystem provides the release/acquire operation
// 2. it's only used from the main thread, which we are blocking during the gc
js.run(jobs::createJob(js, rootJob, &FEngine::gcDeferredAsyncObjectDestruction, this));
}
// RenderableManager cannot be gc'ed from a different thread, because it needs the DriverAPI
mRenderableManager.gc(mEntityManager, getDriverApi());
js.runAndWait(rootJob);
}
void FEngine::gcDeferredAsyncObjectDestruction() {
@@ -1399,7 +1421,7 @@ bool FEngine::destroy(const FMaterialInstance* p) {
UTILS_NOINLINE
void FEngine::destroy(Entity const e) {
mRenderableManager.destroy(e);
mRenderableManager.destroy(e, getDriverApi());
mLightManager.destroy(e);
mTransformManager.destroy(e);
mCameraManager.destroy(*this, e);
+3 -2
View File
@@ -456,8 +456,7 @@ public:
}
void prepare(backend::DriverApi& driver);
void gcManagers();
void gcDeferredAsyncObjectDestruction();
void gc();
void submitFrame();
using ShaderContent = utils::FixedCapacityVector<uint8_t>;
@@ -583,6 +582,8 @@ private:
int loop();
void flushCommandBuffer(backend::CommandBufferQueue& commandBufferQueue) const;
void gcDeferredAsyncObjectDestruction();
template<typename T>
bool isValid(const T* ptr, ResourceList<T> const& list) const;
+13 -124
View File
@@ -170,34 +170,11 @@ FMaterial::FMaterial(FEngine& engine, const Builder& builder, MaterialDefinition
DriverApi& driver = engine.getDriverApi();
mIsStereoSupported = driver.isStereoSupported();
mIsParallelShaderCompileSupported = driver.isParallelShaderCompileSupported();
mDepthPrecacheDisabled =
driver.isWorkaroundNeeded(Workaround::DISABLE_DEPTH_PRECACHE_FOR_DEFAULT_MATERIAL);
mDefaultMaterial = engine.getDefaultMaterial();
FixedCapacityVector<Program::SpecializationConstant> specializationConstants =
processSpecializationConstants(builder);
mSpecializationConstants =
engine.getMaterialCache().getSpecializationConstantsInternPool().acquire(
std::move(specializationConstants));
size_t cachedProgramsSize;
switch (mDefinition.materialDomain) {
case filament::MaterialDomain::SURFACE:
cachedProgramsSize = 1 << VARIANT_BITS;
break;
case filament::MaterialDomain::POST_PROCESS:
cachedProgramsSize = 1 << POST_PROCESS_VARIANT_BITS;
break;
case filament::MaterialDomain::COMPUTE:
cachedProgramsSize = 1;
break;
}
mCachedPrograms = FixedCapacityVector<backend::Handle<backend::HwProgram>>(cachedProgramsSize);
mDefinition.acquirePrograms(engine, mCachedPrograms.as_slice(), getMaterialParser(),
mSpecializationConstants, mIsDefaultMaterial);
mPrograms.initializeForMaterial(engine, *this, processSpecializationConstants(builder));
#if FILAMENT_ENABLE_MATDBG
// Register the material with matdbg.
@@ -237,11 +214,7 @@ void FMaterial::terminate(FEngine& engine) {
}
#endif
mDefinition.releasePrograms(engine, mCachedPrograms.as_slice(),
getMaterialParser(), mSpecializationConstants, mIsDefaultMaterial);
engine.getMaterialCache().releaseMaterial(engine, mDefinition);
engine.getMaterialCache().getSpecializationConstantsInternPool().release(
mSpecializationConstants);
mPrograms.terminate(engine);
}
filament::DescriptorSetLayout const& FMaterial::getPerViewDescriptorSetLayout(
@@ -257,71 +230,24 @@ filament::DescriptorSetLayout const& FMaterial::getPerViewDescriptorSetLayout(
}
// mDefinition.perViewDescriptorSetLayout{Vsm} is already resolved for MaterialDomain
if (useVsmDescriptorSetLayout) {
return mDefinition.perViewDescriptorSetLayoutVsm;
return mDefinition.perViewDescriptorSetLayoutS2d;
}
return mDefinition.perViewDescriptorSetLayout;
return mDefinition.perViewDescriptorSetLayoutPcf;
}
void FMaterial::compile(CompilerPriorityQueue const priority,
UserVariantFilterMask variantSpec,
CallbackHandler* handler,
Invocable<void(Material*)>&& callback) noexcept {
DriverApi& driver = mEngine.getDriverApi();
// Turn off the STE variant if stereo is not supported.
if (!mIsStereoSupported) {
variantSpec &= ~UserVariantFilterMask(UserVariantFilterBit::STE);
}
UserVariantFilterMask const variantFilter =
~variantSpec & UserVariantFilterMask(UserVariantFilterBit::ALL);
ShaderModel const shaderModel = mEngine.getShaderModel();
bool const isStereoSupported = mEngine.getDriverApi().isStereoSupported();
if (UTILS_LIKELY(mIsParallelShaderCompileSupported)) {
for (auto const variant: mDefinition.getVariants()) {
if (!variantFilter || variant == Variant::filterUserVariant(variant, variantFilter)) {
if (mDefinition.hasVariant(variant, shaderModel, isStereoSupported)) {
prepareProgram(driver, variant, priority);
}
}
}
}
void FMaterial::compile(CompilerPriorityQueue const priority, UserVariantFilterMask variantSpec,
CallbackHandler* handler, Invocable<void(Material*)>&& callback) noexcept {
FMaterialInstance* mi = getDefaultInstance();
if (callback) {
struct Callback {
Invocable<void(Material*)> f;
Material* m;
static void func(void* user) {
auto* const c = static_cast<Callback*>(user);
c->f(c->m);
delete c;
}
};
auto* const user = new(std::nothrow) Callback{ std::move(callback), this };
driver.compilePrograms(priority, handler, &Callback::func, user);
mi->compile(priority, variantSpec, handler,
[this, callback = std::move(callback)](MaterialInstance*) {
callback(this);
});
} else {
driver.compilePrograms(priority, nullptr, nullptr, nullptr);
mi->compile(priority, variantSpec, handler, {});
}
}
Handle<HwProgram> FMaterial::prepareProgramSlow(DriverApi& driver, Variant const variant,
CompilerPriorityQueue const priorityQueue) const noexcept {
if (isSharedVariant(variant)) {
FMaterial const* defaultMaterial = mEngine.getDefaultMaterial();
FILAMENT_CHECK_PRECONDITION(defaultMaterial);
Handle<HwProgram> program = defaultMaterial->mCachedPrograms[variant.key];
if (program) {
return mCachedPrograms[variant.key] = program;
}
return mCachedPrograms[variant.key] =
defaultMaterial->prepareProgram(driver, variant, priorityQueue);
}
return mCachedPrograms[variant.key] = mDefinition.prepareProgram(mEngine, driver,
getMaterialParser(), getProgramSpecialization(variant), priorityQueue);
}
FMaterialInstance* FMaterial::createInstance(const char* name) const noexcept {
if (mDefaultMaterialInstance) {
// if we have a default instance, use it to create a new one
@@ -356,14 +282,6 @@ BufferInterfaceBlock::FieldInfo const* FMaterial::reflect(
return mDefinition.uniformInterfaceBlock.getFieldInfo(name);
}
ProgramSpecialization FMaterial::getProgramSpecialization(Variant const variant) const noexcept {
return ProgramSpecialization {
.materialCrc32 = mDefinition.getMaterialParser().getCrc32(),
.variant = variant,
.specializationConstants = mSpecializationConstants,
};
}
size_t FMaterial::getParameters(ParameterInfo* parameters, size_t count) const noexcept {
count = std::min(count, getParameterCount());
@@ -430,8 +348,7 @@ void FMaterial::updateActiveProgramsForMatdbg(Variant const variant) const noexc
void FMaterial::applyPendingEdits() noexcept {
const char* name = mDefinition.name.c_str();
DLOG(INFO) << "Applying edits to " << (name ? name : "(untitled)");
mDefinition.releasePrograms(mEngine, mCachedPrograms.as_slice(), getMaterialParser(),
mSpecializationConstants, mIsDefaultMaterial);
mPrograms.clear(mEngine);
latchPendingEdits();
}
@@ -480,26 +397,6 @@ void FMaterial::onQueryCallback(void* userdata, VariantList* pActiveVariants) {
#endif // FILAMENT_ENABLE_MATDBG
void FMaterial::setSpecializationConstants(SpecializationConstantsBuilder&& builder) noexcept {
if (builder.mConstants.empty()) {
// Nothing was changed.
return;
}
auto& internPool = mEngine.getMaterialCache().getSpecializationConstantsInternPool();
MaterialParser const& materialParser = getMaterialParser();
// Release old resources...
mDefinition.releasePrograms(mEngine, mCachedPrograms.as_slice(), materialParser,
mSpecializationConstants, mIsDefaultMaterial);
internPool.release(mSpecializationConstants);
// Then acquire new ones.
mSpecializationConstants = internPool.acquire(std::move(builder.mConstants));
mDefinition.acquirePrograms(mEngine, mCachedPrograms.as_slice(), materialParser,
mSpecializationConstants, mIsDefaultMaterial);
}
FixedCapacityVector<Program::SpecializationConstant> FMaterial::processSpecializationConstants(
Builder const& builder) {
FixedCapacityVector<Program::SpecializationConstant> specializationConstants =
@@ -560,12 +457,4 @@ const char* FMaterial::getParameterTransformName(std::string_view samplerName) c
return info->transformName.c_str();
}
template FMaterial::SpecializationConstantsBuilder& FMaterial::SpecializationConstantsBuilder::set<int32_t>(uint32_t id, int32_t value) noexcept;
template FMaterial::SpecializationConstantsBuilder& FMaterial::SpecializationConstantsBuilder::set<float>(uint32_t id, float value) noexcept;
template FMaterial::SpecializationConstantsBuilder& FMaterial::SpecializationConstantsBuilder::set<bool>(uint32_t id, bool value) noexcept;
template FMaterial::SpecializationConstantsBuilder& FMaterial::SpecializationConstantsBuilder::set<int32_t>(std::string_view name, int32_t value) noexcept;
template FMaterial::SpecializationConstantsBuilder& FMaterial::SpecializationConstantsBuilder::set<float>(std::string_view name, float value) noexcept;
template FMaterial::SpecializationConstantsBuilder& FMaterial::SpecializationConstantsBuilder::set<bool>(std::string_view name, bool value) noexcept;
} // namespace filament
+34 -144
View File
@@ -18,6 +18,7 @@
#define TNT_FILAMENT_DETAILS_MATERIAL_H
#include "downcast.h"
#include "LocalProgramCache.h"
#include "details/MaterialInstance.h"
@@ -45,7 +46,6 @@
#include <utils/Invocable.h>
#include <utils/Mutex.h>
#include <optional>
#include <string_view>
#include <stddef.h>
@@ -72,62 +72,6 @@ public:
DefaultMaterialBuilder();
};
// Used to change specialization constants at runtime internally by Filament.
//
// Call Material::getSpecializationConstantsBuilder() to return an instance of
// SpecializationConstantsBuilder. The lifetime of this object must not exceed the lifetime of
// the corresponding Material. After spec constants are changed with calls to set(), apply the
// changes to the original Material via setSpecializationConstants().
//
// It would be nice to reuse Builder for this, but Builder allocates an entire map and asserts
// if specified constants don't exist. In constrast, SpecializationConstantsBuilder is
// copy-on-write and allows non-existent specialization constants to be named.
class SpecializationConstantsBuilder {
friend class FMaterial;
public:
SpecializationConstantsBuilder(SpecializationConstantsBuilder const& rhs) = delete;
SpecializationConstantsBuilder& operator=(
SpecializationConstantsBuilder const& rhs) = delete;
SpecializationConstantsBuilder(SpecializationConstantsBuilder&& rhs) = default;
SpecializationConstantsBuilder& operator=(SpecializationConstantsBuilder&& rhs) = default;
template<typename T, typename = Builder::is_supported_constant_parameter_t<T>>
SpecializationConstantsBuilder& set(uint32_t id, T value) noexcept {
// Don't allocate if we can help it.
if (mConstants.empty()) {
if (std::get<T>(mDefaultConstants[id]) == value) {
return *this;
}
mConstants = utils::FixedCapacityVector<backend::Program::SpecializationConstant>(
mDefaultConstants);
}
mConstants[id] = value;
return *this;
}
template<typename T, typename = Builder::is_supported_constant_parameter_t<T>>
SpecializationConstantsBuilder& set(std::string_view name, T value) noexcept {
auto it = mDefinition->specializationConstantsNameToIndex.find(name);
if (it != mDefinition->specializationConstantsNameToIndex.cend()) {
set(it->second + CONFIG_MAX_RESERVED_SPEC_CONSTANTS, value);
}
return *this;
}
private:
SpecializationConstantsBuilder(MaterialDefinition const& definition,
utils::Slice<const backend::Program::SpecializationConstant> defaultConstants)
: mDefinition(&definition),
mDefaultConstants(defaultConstants) {}
MaterialDefinition const* mDefinition;
utils::Slice<const backend::Program::SpecializationConstant> mDefaultConstants;
// Copy-on-write vector.
utils::FixedCapacityVector<backend::Program::SpecializationConstant> mConstants;
};
void terminate(FEngine& engine);
// return the uniform interface block for this material
@@ -139,7 +83,7 @@ public:
// This is mostly intended to be used for post-process materials; but it's also useful for
// Surface material that behave like post-process material (i.e. that don't really have variants or for
// which VSM is nonsensical, like for unlit materials)
return mDefinition.perViewDescriptorSetLayout;
return mDefinition.perViewDescriptorSetLayoutPcf;
}
DescriptorSetLayout const& getPerViewDescriptorSetLayout(
@@ -181,46 +125,6 @@ public:
FEngine& getEngine() const noexcept { return mEngine; }
// prepareProgram creates the program for the material's given variant at the backend level.
// Must be called outside of backend render pass.
// Must be called before getProgram() below.
backend::Handle<backend::HwProgram> prepareProgram(backend::DriverApi& driver,
Variant const variant,
backend::CompilerPriorityQueue const priorityQueue) const noexcept {
backend::Handle<backend::HwProgram> program = mCachedPrograms[variant.key];
if (UTILS_LIKELY(program)) {
return program;
}
return prepareProgramSlow(driver, variant, priorityQueue);
}
// getProgram returns the backend program for the material's given variant.
// Must be called after prepareProgram().
[[nodiscard]]
backend::Handle<backend::HwProgram> getProgram(Variant variant) const noexcept {
if (UTILS_UNLIKELY(mEngine.features.material.enable_fog_as_postprocess)) {
// if the fog as post-process feature is enabled, we need to proceed "as-if" the material
// didn't have the FOG variant bit.
if (getMaterialDomain() == MaterialDomain::SURFACE) {
BlendingMode const blendingMode = getBlendingMode();
bool const hasScreenSpaceRefraction = getRefractionMode() == RefractionMode::SCREEN_SPACE;
bool const isBlendingCommand = !hasScreenSpaceRefraction &&
(blendingMode != BlendingMode::OPAQUE && blendingMode != BlendingMode::MASKED);
if (!isBlendingCommand) {
variant.setFog(false);
}
}
}
#if FILAMENT_ENABLE_MATDBG
updateActiveProgramsForMatdbg(variant);
#endif
backend::Handle<backend::HwProgram> program = mCachedPrograms[variant.key];
assert_invariant(program);
return program;
}
// MaterialInstance::use() binds descriptor sets before drawing. For shared variants,
// however, the material instance will call useShared() to bind the default material's sets
// instead.
@@ -275,6 +179,8 @@ public:
float getSpecularAntiAliasingVariance() const noexcept { return mDefinition.specularAntiAliasingVariance; }
float getSpecularAntiAliasingThreshold() const noexcept { return mDefinition.specularAntiAliasingThreshold; }
bool isDefaultMaterial() const noexcept { return mIsDefaultMaterial; }
backend::descriptor_binding_t getSamplerBinding(
std::string_view const& name) const;
@@ -297,12 +203,7 @@ public:
uint32_t generateMaterialInstanceId() const noexcept { return mMaterialInstanceId++; }
// Update specialization constants.
SpecializationConstantsBuilder getSpecializationConstantsBuilder() const noexcept {
return SpecializationConstantsBuilder(mDefinition, mSpecializationConstants);
}
void setSpecializationConstants(SpecializationConstantsBuilder&& builder) noexcept;
LocalProgramCache& getPrograms() noexcept { return mPrograms; }
uint8_t getPerViewLayoutIndex() const noexcept {
return mDefinition.perViewLayoutIndex;
@@ -316,6 +217,30 @@ public:
return mDefinition.source.c_str_safe();
}
inline bool isSharedVariant(Variant const variant) const {
// HACK: The default material "should" have MNT | 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::MNT | Variant::DEP;
return mDefinition.materialDomain == MaterialDomain::SURFACE && !mIsDefaultMaterial &&
!mDefinition.hasCustomDepthShader && Variant::isValidDepthVariant(variant) &&
(variant.key & vsmAndDep) != vsmAndDep;
}
MaterialParser const& getMaterialParser() const noexcept {
#if FILAMENT_ENABLE_MATDBG
if (mEditedMaterialParser) {
return *mEditedMaterialParser;
}
#endif
return mDefinition.getMaterialParser();
}
MaterialDefinition const& getDefinition() const noexcept { return mDefinition; }
LocalProgramCache const& getPrograms() const noexcept { return mPrograms; }
#if FILAMENT_ENABLE_MATDBG
void applyPendingEdits() noexcept;
@@ -346,24 +271,12 @@ public:
}
/** @}*/
// Called by getProgram() to update active program list for matdbg UI.
void updateActiveProgramsForMatdbg(Variant const variant) const noexcept;
#endif
private:
MaterialParser const& getMaterialParser() const noexcept {
#if FILAMENT_ENABLE_MATDBG
if (mEditedMaterialParser) {
return *mEditedMaterialParser;
}
#endif
return mDefinition.getMaterialParser();
}
ProgramSpecialization getProgramSpecialization(Variant const variant) const noexcept;
backend::Handle<backend::HwProgram> prepareProgramSlow(backend::DriverApi& driver,
Variant const variant,
backend::CompilerPriorityQueue const priorityQueue) const noexcept;
utils::FixedCapacityVector<backend::Program::SpecializationConstant>
processSpecializationConstants(Builder const& builder);
@@ -371,31 +284,11 @@ private:
void createAndCacheProgram(backend::DriverApi& driver, backend::Program&& p, Variant variant) const noexcept;
backend::DescriptorSetLayout const& getPerViewDescriptorSetLayoutDescription(
Variant variant, bool useVsmDescriptorSetLayout) const noexcept;
backend::DescriptorSetLayout const& getDescriptorSetLayoutDescription(
Variant variant = {}) const noexcept;
bool isSharedVariant(Variant const variant) const {
// 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;
MaterialDefinition const& mDefinition;
bool mIsDefaultMaterial = false;
bool mUseUboBatching = false;
bool mIsStereoSupported = false;
bool mIsParallelShaderCompileSupported = false;
bool mDepthPrecacheDisabled = false;
FMaterial const* mDefaultMaterial = nullptr;
@@ -403,9 +296,6 @@ private:
// reserve some space to construct the default material instance
mutable FMaterialInstance* mDefaultMaterialInstance = nullptr;
// current specialization constants for the HwProgram
utils::Slice<const backend::Program::SpecializationConstant> mSpecializationConstants;
#if FILAMENT_ENABLE_MATDBG
matdbg::MaterialKey mDebuggerId;
mutable utils::Mutex mActiveProgramsLock;
@@ -413,8 +303,6 @@ private:
mutable utils::Mutex mPendingEditsLock;
std::unique_ptr<MaterialParser> mPendingEdits;
std::unique_ptr<MaterialParser> mEditedMaterialParser;
// Called by getProgram() to update active program list for matdbg UI.
void updateActiveProgramsForMatdbg(Variant const variant) const noexcept;
void setPendingEdits(std::unique_ptr<MaterialParser> pendingEdits) noexcept;
bool hasPendingEdits() const noexcept;
void latchPendingEdits() noexcept;
@@ -423,6 +311,8 @@ private:
FEngine& mEngine;
const uint32_t mMaterialId;
mutable uint32_t mMaterialInstanceId = 0;
LocalProgramCache mPrograms;
};
+123
View File
@@ -17,6 +17,7 @@
#include <filament/MaterialInstance.h>
#include "RenderPass.h"
#include "MaterialParser.h"
#include "ds/DescriptorSetLayout.h"
@@ -131,6 +132,7 @@ FMaterialInstance::FMaterialInstance(FEngine& engine,
mTextureParameters(other->mTextureParameters),
mDescriptorSet(other->mDescriptorSet.duplicate(
"MaterialInstance", mMaterial->getDescriptorSetLayout())),
mPrograms(other->mPrograms),
mPolygonOffset(other->mPolygonOffset),
mStencilState(other->mStencilState),
mMaskThreshold(other->mMaskThreshold),
@@ -207,6 +209,10 @@ void FMaterialInstance::terminate(FEngine& engine) {
if (ubHandle){
driver.destroyBufferObject(*ubHandle);
}
if (mPrograms.isInitialized()) {
mPrograms.terminate(engine);
}
}
void FMaterialInstance::commit(FEngine& engine) const {
@@ -259,6 +265,39 @@ void FMaterialInstance::commit(FEngine::DriverApi& driver, UboManager* uboManage
// ------------------------------------------------------------------------------------------------
template<typename T>
void FMaterialInstance::setConstantImpl(std::string_view name, T value) {
auto const& constants = mMaterial->getDefinition().specializationConstantsNameToIndex;
auto it = constants.find(name);
FILAMENT_CHECK_PRECONDITION(it != constants.end()) << "Constant " << name << " does not exist";
if (UTILS_UNLIKELY(mPendingSpecializationConstants.empty())) {
mPendingSpecializationConstants =
FixedCapacityVector<backend::Program::SpecializationConstant>(
getPrograms().getSpecializationConstants());
}
uint32_t id = it->second + CONFIG_MAX_RESERVED_SPEC_CONSTANTS;
mPendingSpecializationConstants[id] = value;
}
template<typename T>
T FMaterialInstance::getConstantImpl(std::string_view name) const {
auto const& constants = mMaterial->getDefinition().specializationConstantsNameToIndex;
auto it = constants.find(name);
FILAMENT_CHECK_PRECONDITION(it != constants.end()) << "Constant " << name << " does not exist";
uint32_t id = it->second + CONFIG_MAX_RESERVED_SPEC_CONSTANTS;
if (UTILS_UNLIKELY(!mPendingSpecializationConstants.empty())) {
return std::get<T>(mPendingSpecializationConstants[id]);
}
return getPrograms().getConstant<T>(id);
}
// ------------------------------------------------------------------------------------------------
void FMaterialInstance::setParameter(std::string_view const name,
Handle<HwTexture> texture, SamplerParams const params) {
auto const binding = mMaterial->getSamplerBinding(name);
@@ -378,6 +417,15 @@ void FMaterialInstance::setTransparencyMode(TransparencyMode const mode) noexcep
mTransparencyMode = mode;
}
RasterState FMaterialInstance::getRasterState() const noexcept {
RasterState rs = mMaterial->getRasterState();
rs.culling = mCulling;
rs.depthWrite = mDepthWrite;
rs.depthFunc = mDepthFunc;
rs.colorWrite = mColorWrite;
return rs;
}
void FMaterialInstance::setDepthCulling(bool const enable) noexcept {
mDepthFunc = enable ? RasterState::DepthFunc::GE : RasterState::DepthFunc::A;
}
@@ -398,6 +446,51 @@ const char* FMaterialInstance::getName() const noexcept {
// ------------------------------------------------------------------------------------------------
void FMaterialInstance::compile(CompilerPriorityQueue const priority,
UserVariantFilterMask variantSpec, CallbackHandler* handler,
Invocable<void(MaterialInstance*)>&& callback) noexcept {
FEngine& engine = mMaterial->getEngine();
DriverApi& driver = engine.getDriverApi();
MaterialDefinition const& definition = mMaterial->getDefinition();
bool const isStereoSupported = driver.isStereoSupported();
// Turn off the STE variant if stereo is not supported.
if (UTILS_LIKELY(!isStereoSupported)) {
variantSpec &= ~UserVariantFilterMask(UserVariantFilterBit::STE);
}
UserVariantFilterMask const variantFilter =
~variantSpec & UserVariantFilterMask(UserVariantFilterBit::ALL);
ShaderModel const shaderModel = engine.getShaderModel();
if (UTILS_LIKELY(driver.isParallelShaderCompileSupported())) {
for (auto const variant: definition.getVariants()) {
if (!variantFilter || variant == Variant::filterUserVariant(variant, variantFilter)) {
if (definition.hasVariant(variant, shaderModel, isStereoSupported)) {
prepareProgram(driver, variant, priority);
}
}
}
}
if (callback) {
struct Callback {
Invocable<void(MaterialInstance*)> f;
MaterialInstance* m;
static void func(void* user) {
auto* const c = static_cast<Callback*>(user);
c->f(c->m);
delete c;
}
};
auto* const user = new (std::nothrow) Callback{ std::move(callback), this };
driver.compilePrograms(priority, handler, &Callback::func, user);
} else {
driver.compilePrograms(priority, nullptr, nullptr, nullptr);
}
}
void FMaterialInstance::use(FEngine::DriverApi& driver, Variant variant) const {
assert_invariant(mDescriptorSet.getHandle());
assert_invariant(!isUsingUboBatching() || BufferAllocator::isValid(getAllocationId()));
@@ -502,4 +595,34 @@ void FMaterialInstance::fixMissingSamplers() const {
}
}
LocalProgramCache const& FMaterialInstance::getPrograms() const noexcept {
return mPrograms.isInitialized() ? mPrograms : mMaterial->getPrograms();
}
void FMaterialInstance::flushSpecializationConstants() const noexcept {
assert_invariant(!mPendingSpecializationConstants.empty());
if (!mPrograms.isInitialized()) {
mPrograms.initializeForMaterialInstance(mMaterial->getEngine(), *mMaterial);
}
mPrograms.setConstants(std::move(mPendingSpecializationConstants));
mPendingSpecializationConstants.clear();
}
#if FILAMENT_ENABLE_MATDBG
void FMaterialInstance::updateActiveProgramsForMatdbg(Variant const variant) const noexcept {
mMaterial->updateActiveProgramsForMatdbg(variant);
}
#endif // FILAMENT_ENABLE_MATDBG
template void FMaterialInstance::setConstantImpl<int32_t>(std::string_view name, int32_t value);
template void FMaterialInstance::setConstantImpl<float>(std::string_view name, float value);
template void FMaterialInstance::setConstantImpl<bool>(std::string_view name, bool value);
template int32_t FMaterialInstance::getConstantImpl<int32_t>(std::string_view name) const;
template float FMaterialInstance::getConstantImpl<float>(std::string_view name) const;
template bool FMaterialInstance::getConstantImpl<bool>(std::string_view name) const;
} // namespace filament
+58 -4
View File
@@ -18,7 +18,7 @@
#define TNT_FILAMENT_DETAILS_MATERIALINSTANCE_H
#include "downcast.h"
#include "LocalProgramCache.h"
#include "UniformBuffer.h"
#include "ds/DescriptorSet.h"
@@ -26,8 +26,6 @@
#include "details/BufferAllocator.h"
#include "details/Engine.h"
#include "private/backend/DriverApi.h"
#include <filament/MaterialInstance.h>
#include <private/filament/Variant.h>
@@ -67,7 +65,7 @@ public:
~FMaterialInstance() noexcept;
void terminate(FEngine& engine);
void commit(FEngine& engine) const;
void commit(FEngine::DriverApi& driver, UboManager* uboManager) const;
@@ -86,6 +84,34 @@ public:
UniformBuffer const& getUniformBuffer() const noexcept { return mUniforms; }
void compile(backend::CompilerPriorityQueue priority, UserVariantFilterMask variantSpec,
backend::CallbackHandler* handler,
utils::Invocable<void(MaterialInstance*)>&& callback) noexcept;
// prepareProgram creates the program for the material's given variant at the backend level.
// Must be called outside of backend render pass.
// Must be called before getProgram() below.
backend::Handle<backend::HwProgram> prepareProgram(backend::DriverApi& driver,
Variant const variant,
backend::CompilerPriorityQueue const priorityQueue) const noexcept {
if (UTILS_UNLIKELY(!mPendingSpecializationConstants.empty())) {
flushSpecializationConstants();
}
return getPrograms().prepareProgram(driver, variant, priorityQueue);
}
// getProgram returns the backend program for the material's given variant.
// Must be called after prepareProgram().
//
// See also Material::getProgram().
[[nodiscard]]
backend::Handle<backend::HwProgram> getProgram(Variant const variant) const noexcept {
#if FILAMENT_ENABLE_MATDBG
updateActiveProgramsForMatdbg(variant);
#endif
return getPrograms().getProgram(variant);
}
void setScissor(uint32_t const left, uint32_t const bottom, uint32_t const width, uint32_t const height) noexcept {
constexpr uint32_t maxvalu = std::numeric_limits<int32_t>::max();
mScissorRect = { int32_t(left), int32_t(bottom),
@@ -93,6 +119,10 @@ public:
mHasScissor = true;
}
void setScissor(backend::Viewport const& viewport) noexcept {
setScissor(viewport.left, viewport.bottom, viewport.width, viewport.height);
}
void unsetScissor() noexcept {
constexpr uint32_t maxvalu = std::numeric_limits<int32_t>::max();
mScissorRect = { 0, 0, maxvalu, maxvalu };
@@ -103,6 +133,8 @@ public:
bool hasScissor() const noexcept { return mHasScissor; }
backend::RasterState getRasterState() const noexcept;
backend::CullingMode getCullingMode() const noexcept { return mCulling; }
backend::CullingMode getShadowCullingMode() const noexcept { return mShadowCulling; }
@@ -250,11 +282,15 @@ public:
backend::Handle<backend::HwTexture> texture, backend::SamplerParams params);
using MaterialInstance::setParameter;
using MaterialInstance::setConstant;
private:
friend class FMaterial;
friend class MaterialInstance;
// Cannot inline since it inspects the FMaterial class.
LocalProgramCache const& getPrograms() const noexcept;
template<size_t Size>
void setParameterUntypedImpl(std::string_view name, const void* value);
@@ -273,6 +309,19 @@ private:
template<typename T>
T getParameterImpl(std::string_view name) const;
template<typename T>
void setConstantImpl(std::string_view name, T value);
template<typename T>
T getConstantImpl(std::string_view name) const;
void flushSpecializationConstants() const noexcept;
#if FILAMENT_ENABLE_MATDBG
// Called by getProgram() to update active program list for matdbg UI.
void updateActiveProgramsForMatdbg(Variant const variant) const noexcept;
#endif
// keep these grouped, they're accessed together in the render-loop
FMaterial const* mMaterial = nullptr;
@@ -287,6 +336,11 @@ private:
mutable DescriptorSet mDescriptorSet;
UniformBuffer mUniforms;
// HACK: Mutable so that prepareProgram() can update specialization constants.
mutable LocalProgramCache mPrograms;
mutable utils::FixedCapacityVector<backend::Program::SpecializationConstant>
mPendingSpecializationConstants;
backend::PolygonOffset mPolygonOffset{};
backend::StencilState mStencilState{};
+4 -22
View File
@@ -310,13 +310,7 @@ void FRenderer::skipFrame(uint64_t vsyncSteadyClockTimeNano) {
engine.flush(); // flush command stream
// Run GC
JobSystem& js = engine.getJobSystem();
auto *rootJob = js.createJob();
js.run(jobs::createJob(js, rootJob, &FEngine::gcManagers, &engine)); // gc all component managers
if (engine.isAsynchronousModeEnabled()) {
js.run(jobs::createJob(js, rootJob, &FEngine::gcDeferredAsyncObjectDestruction, &engine));
}
js.runAndWait(rootJob);
engine.gc();
mFrameSkipper.frameSkipped();
}
@@ -518,13 +512,7 @@ void FRenderer::endFrame() {
engine.flush(); // flush command stream
// Run GC
JobSystem& js = engine.getJobSystem();
auto *rootJob = js.createJob();
js.run(jobs::createJob(js, rootJob, &FEngine::gcManagers, &engine)); // gc all component managers
if (engine.isAsynchronousModeEnabled()) {
js.run(jobs::createJob(js, rootJob, &FEngine::gcDeferredAsyncObjectDestruction, &engine));
}
js.runAndWait(rootJob);
engine.gc();
}
void FRenderer::readPixels(
@@ -969,10 +957,7 @@ void FRenderer::renderJob(DriverApi& driver, RootArenaScope& rootArenaScope, FVi
variant.setDirectionalLighting(view.hasDirectionalLighting());
variant.setDynamicLighting(view.hasDynamicLighting());
variant.setFog(view.hasFog());
// The VSM bit has a different meaning for STANDARD_VARIANT (as opposed to DEPTH_VARIANT),
// In the STANDARD_VARIANT case, we are *using* the shadow-map, and the VSM only decides which
// type of sampler is used (samplerShadow or sampler2D).
variant.setVsm(view.hasShadowing() && view.getShadowType() != ShadowType::PCF);
variant.setShadowSampler2D(view.hasShadowing() && view.getShadowType() != ShadowType::PCF);
variant.setStereo(view.hasStereo());
/*
@@ -981,10 +966,7 @@ void FRenderer::renderJob(DriverApi& driver, RootArenaScope& rootArenaScope, FVi
if (view.needsShadowMap()) {
Variant shadowVariant(Variant::DEPTH_VARIANT);
// The VSM bit has a different meaning for DEPTH_VARIANT (as opposed to STANDARD_VARIANT),
// In the DEPTH_VARIANT case, we are *generating* the shadow-map, and some computations
// are handled differently. In addition, the color buffer is used.
shadowVariant.setVsm(view.getShadowType() == ShadowType::VSM);
shadowVariant.setDepthMoments(view.getShadowType() == ShadowType::VSM);
auto shadows = view.renderShadowMaps(engine, fg, cameraInfo, mShaderUserTime,
RenderPassBuilder{ commandArena }
+4 -5
View File
@@ -76,6 +76,7 @@
#include <cmath>
#include <chrono>
#include <functional>
#include <limits>
#include <memory>
#include <new>
#include <ratio>
@@ -143,7 +144,7 @@ FView::FView(FEngine& engine)
#ifndef NDEBUG
// This can fail if another view has already registered this data source
mDebugState->owner = debugRegistry.registerDataSource("d.view.frame_info",
[weak = std::weak_ptr(mDebugState)]() -> DebugRegistry::DataSource {
[weak = std::weak_ptr<DebugState>(mDebugState)]() -> DebugRegistry::DataSource {
// the View could have been destroyed by the time we do this
auto const state = weak.lock();
if (!state) {
@@ -1131,8 +1132,6 @@ void FView::prepareShadowMapping() const noexcept {
uniforms = mShadowMapManager->getShadowMappingUniforms();
}
constexpr float low = 5.54f; // ~ std::log(std::numeric_limits<math::half>::max()) * 0.5f;
constexpr float high = 42.0f; // ~ std::log(std::numeric_limits<float>::max()) * 0.5f;
constexpr uint32_t SHADOW_SAMPLING_RUNTIME_PCF = 0u;
constexpr uint32_t SHADOW_SAMPLING_RUNTIME_EVSM = 1u;
constexpr uint32_t SHADOW_SAMPLING_RUNTIME_DPCF = 2u;
@@ -1148,8 +1147,8 @@ void FView::prepareShadowMapping() const noexcept {
break;
case ShadowType::VSM:
s.shadowSamplingType = SHADOW_SAMPLING_RUNTIME_EVSM;
s.vsmExponent = mVsmShadowOptions.highPrecision ? high : low;
s.vsmDepthScale = mVsmShadowOptions.minVarianceScale * 0.01f * s.vsmExponent;
s.vsmExponent = 0; // this is only used when rendering the shadowmap, not when using it
s.vsmMaxMoment = ShadowMapManager::getMaxMomentEVSM(mVsmShadowOptions);
s.vsmLightBleedReduction = mVsmShadowOptions.lightBleedReduction;
break;
case ShadowType::DPCF:
+7 -6
View File
@@ -28,9 +28,11 @@
#include <utils/debug.h>
#include <math/half.h>
#include <math/vec4.h>
#include <array>
#include <limits>
namespace filament {
@@ -74,8 +76,8 @@ void ShadowMapDescriptorSet::prepareLodBias(Transaction const& transaction, floa
}
void ShadowMapDescriptorSet::prepareViewport(Transaction const& transaction,
backend::Viewport const& viewport) noexcept {
PerViewDescriptorSetUtils::prepareViewport(edit(transaction), viewport, viewport);
backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept {
PerViewDescriptorSetUtils::prepareViewport(edit(transaction), physicalViewport, logicalViewport);
// TODO: offset calculation is now different
}
@@ -90,11 +92,10 @@ void ShadowMapDescriptorSet::prepareMaterialGlobals(Transaction const& transacti
}
void ShadowMapDescriptorSet::prepareShadowMapping(Transaction const& transaction,
bool const highPrecision) noexcept {
float const vsmExponent, float const vsmMaxMoment) noexcept {
auto& s = edit(transaction);
constexpr float low = 5.54f; // ~ std::log(std::numeric_limits<math::half>::max()) * 0.5f;
constexpr float high = 42.0f; // ~ std::log(std::numeric_limits<float>::max()) * 0.5f;
s.vsmExponent = highPrecision ? high : low;
s.vsmExponent = vsmExponent;
s.vsmMaxMoment = vsmMaxMoment;
}
ShadowMapDescriptorSet::Transaction ShadowMapDescriptorSet::open(DriverApi& driver) noexcept {
+2 -2
View File
@@ -66,7 +66,7 @@ public:
float bias) noexcept;
static void prepareViewport(Transaction const& transaction,
backend::Viewport const& viewport) noexcept;
backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept;
static void prepareTime(Transaction const& transaction,
FEngine const& engine, math::float4 const& userTime) noexcept;
@@ -75,7 +75,7 @@ public:
std::array<math::float4, 4> const& materialGlobals) noexcept;
static void prepareShadowMapping(Transaction const& transaction,
bool highPrecision) noexcept;
float vsmExponent, float vsmMaxMoment) noexcept;
static Transaction open(backend::DriverApi& driver) noexcept;
+26 -4
View File
@@ -19,7 +19,8 @@ material {
{
name : color,
target : color,
type : float4
type : float4,
precision : high
}
],
domain : postprocess,
@@ -30,8 +31,29 @@ material {
fragment {
void postProcess(inout PostProcessInputs postProcess) {
highp vec2 uv = gl_FragCoord.xy * materialParams.uvscale;
postProcess.color = textureLod(materialParams_color,
vec3(uv, materialParams.layer), 0.0);
// For EVSM mipmapping, we use a custom box filter (instead of the GPU's bilinear filter), because GPUs often
// use a lower precision for texture interpolation and EVSM moments are very sensitive to that.
highp ivec2 destCoord = ivec2(gl_FragCoord.xy);
highp ivec3 srcCoord = ivec3(destCoord * 2, materialParams.layer);
#if defined(TARGET_WEBGPU_ENVIRONMENT)
// it looks like WebGPU doesn't support texelFetchOffset
highp vec4 m0 = texelFetch(materialParams_color, ivec3(srcCoord.xy + ivec2(0, 0), srcCoord.z), 0);
highp vec4 m1 = texelFetch(materialParams_color, ivec3(srcCoord.xy + ivec2(1, 0), srcCoord.z), 0);
highp vec4 m2 = texelFetch(materialParams_color, ivec3(srcCoord.xy + ivec2(0, 1), srcCoord.z), 0);
highp vec4 m3 = texelFetch(materialParams_color, ivec3(srcCoord.xy + ivec2(1, 1), srcCoord.z), 0);
#else
highp vec4 m0 = texelFetchOffset(materialParams_color, srcCoord, 0, ivec2(0, 0));
highp vec4 m1 = texelFetchOffset(materialParams_color, srcCoord, 0, ivec2(1, 0));
highp vec4 m2 = texelFetchOffset(materialParams_color, srcCoord, 0, ivec2(0, 1));
highp vec4 m3 = texelFetchOffset(materialParams_color, srcCoord, 0, ivec2(1, 1));
#endif
// Make sure to not overflow the moments when averaging them
postProcess.color = (m0 * 0.25) +
(m1 * 0.25) +
(m2 * 0.25) +
(m3 * 0.25);
}
}
+50
View File
@@ -19,6 +19,7 @@
#include <filament/Engine.h>
#include <filament/Material.h>
#include <filament/MaterialInstance.h>
#include "filament_test_resources.h"
@@ -167,3 +168,52 @@ TEST(Material, MaterialSettingInvalidApiLevelReturnsAnInvalidPackage) {
Engine::destroy(engine);
}
TEST(MaterialInstanceTest, SetConstant) {
Engine* engine = Engine::create(Engine::Backend::NOOP);
std::string shaderCode(R"(
void material(inout MaterialInputs material) {
prepareMaterial(material);
material.baseColor = vec4(1.0);
}
)");
filamat::MaterialBuilder builder;
builder.init();
builder.name("MaterialInstanceTest");
builder.material(shaderCode.c_str());
builder.constant("myFloat", filamat::MaterialBuilder::ConstantType::FLOAT, 1.0f);
builder.constant("myInt", filamat::MaterialBuilder::ConstantType::INT, 2);
builder.constant("myBool", filamat::MaterialBuilder::ConstantType::BOOL, false);
filamat::Package result = builder.build(engine->getJobSystem());
ASSERT_TRUE(result.isValid());
Material* material = Material::Builder()
.package(result.getData(), result.getSize())
.build(*engine);
ASSERT_NE(material, nullptr);
MaterialInstance* instance = material->createInstance();
ASSERT_NE(instance, nullptr);
// Verify default values
EXPECT_EQ(instance->getConstant<float>("myFloat"), 1.0f);
EXPECT_EQ(instance->getConstant<int32_t>("myInt"), 2);
EXPECT_EQ(instance->getConstant<bool>("myBool"), false);
// Set new values
instance->setConstant("myFloat", 3.0f);
instance->setConstant("myInt", 4);
instance->setConstant("myBool", true);
// Verify new values
EXPECT_EQ(instance->getConstant<float>("myFloat"), 3.0f);
EXPECT_EQ(instance->getConstant<int32_t>("myInt"), 4);
EXPECT_EQ(instance->getConstant<bool>("myBool"), true);
engine->destroy(instance);
engine->destroy(material);
Engine::destroy(engine);
}
@@ -36,7 +36,7 @@ backend::DescriptorSetLayout const& getPerRenderableLayout() noexcept;
backend::DescriptorSetLayout getPerViewDescriptorSetLayout(
MaterialDomain domain,
bool isLit, bool isSSR, bool hasFog,
bool isVSM) noexcept;
bool isShadowSampler2D) noexcept;
backend::DescriptorSetLayout getPerViewDescriptorSetLayoutWithVariant(
Variant variant,
@@ -166,7 +166,7 @@ struct PerViewUib { // NOLINT(cppcoreguidelines-pro-type-member-init)
// VSM shadows [variant: VSM]
// --------------------------------------------------------------------------------------------
float vsmExponent;
float vsmDepthScale;
float vsmMaxMoment;
float vsmLightBleedReduction;
uint32_t shadowSamplingType; // 0: vsm, 1: dpcf
@@ -299,13 +299,12 @@ struct ShadowUib { // NOLINT(cppcoreguidelines-pro-type-member-init)
math::mat4f lightFromWorldMatrix; // 64
math::float4 lightFromWorldZ; // 16
math::float4 scissorNormalized; // 16
float texelSizeAtOneMeter; // 4
float bulbRadiusLs; // 4
float nearOverFarMinusNear; // 4
float normalBias; // 4
bool elvsm; // 4
uint32_t layer; // 4
uint32_t reserved1; // 4
math::float2 normalBias; // 4
bool elvsm; // 4 // could be 1 bit
uint32_t layer; // 4 // could be 8 bits
float vsmExponent; // 4 // could be fp16
uint32_t reserved2; // 4
};
ShadowData shadows[CONFIG_MAX_SHADOWMAPS];
@@ -48,20 +48,21 @@ struct Variant {
// SRE: Shadow Receiver
// SKN: Skinning
// DEP: Depth only
// FOG: Fog
// PCK: Picking (depth variant only)
// VSM: Variance shadow maps (depth) / sampler type (standard)
// FOG: Fog (standard)
// PCK: Picking (depth)
// MNT: Output depth moments (depth)
// S2D: Sampler type for shadows (0: samplerShadowArray, 1: sampler2DArray) (standard)
// STE: Instanced stereo rendering
//
// X: either 1 or 0
// +-----+-----+-----+-----+-----+-----+-----+-----+
// Variant | STE | VSM | FOG | DEP | SKN | SRE | DYN | DIR | 256
// Variant | STE | S2D | FOG | DEP | SKN | SRE | DYN | DIR | 256
// +-----+-----+-----+-----+-----+-----+-----+-----+
// PCK
// MNT PCK
//
// Standard variants:
// +-----+-----+-----+-----+-----+-----+-----+-----+
// | STE | VSM | FOG | 0 | SKN | SRE | DYN | DIR | 128 - 44 = 84
// | STE | S2D | FOG | 0 | SKN | SRE | DYN | DIR | 128 - 44 = 84
// +-----+-----+-----+-----+-----+-----+-----+-----+
// Vertex shader X 0 0 0 X X X X
// Fragment shader 0 X X 0 0 X X X
@@ -72,7 +73,7 @@ struct Variant {
//
// Depth variants:
// +-----+-----+-----+-----+-----+-----+-----+-----+
// | STE | VSM | PCK | 1 | SKN | 0 | 0 | 0 | 16 - 4 = 12
// | STE | MNT | PCK | 1 | SKN | 0 | 0 | 0 | 16 - 4 = 12
// +-----+-----+-----+-----+-----+-----+-----+-----+
// Vertex depth X X 0 1 X 0 0 0
// Fragment depth 0 0 X 1 0 0 0 0
@@ -96,13 +97,15 @@ struct Variant {
static constexpr type_t DEP = 0x10; // depth only variants
static constexpr type_t FOG = 0x20; // fog (standard)
static constexpr type_t PCK = 0x20; // picking (depth)
static constexpr type_t VSM = 0x40; // variance shadow maps / sampler type
static constexpr type_t S2D = 0x40; // sampler type
static constexpr type_t MNT = 0x40; // variance shadow maps
static constexpr type_t STE = 0x80; // instanced stereo
static constexpr type_t NO_VARIANT = 0u;
// special variants (variants that use the reserved space)
static constexpr type_t SPECIAL_SSR = VSM | SRE; // screen-space reflections variant
static constexpr type_t SPECIAL_SSR_VARIANT= S2D | SRE ;
static constexpr type_t SPECIAL_SSR_MASK = STE | S2D | DEP | SRE | DYN | DIR;
static constexpr type_t STANDARD_MASK = DEP;
static constexpr type_t STANDARD_VARIANT = 0u;
@@ -127,29 +130,30 @@ struct Variant {
void setSkinning(bool v) noexcept { set(v, SKN); }
void setFog(bool v) noexcept { set(v, FOG); }
void setPicking(bool v) noexcept { set(v, PCK); }
void setVsm(bool v) noexcept { set(v, VSM); }
void setShadowSampler2D(bool v) noexcept { set(v, S2D); }
void setDepthMoments(bool v) noexcept { set(v, MNT); }
void setStereo(bool v) noexcept { set(v, STE); }
static constexpr bool isValidDepthVariant(Variant variant) noexcept {
// Can't have VSM and PICKING together with DEPTH variants
constexpr type_t RESERVED_MASK = VSM | PCK | DEP | SRE | DYN | DIR;
constexpr type_t RESERVED_VALUE = VSM | PCK | DEP;
constexpr type_t RESERVED_MASK = MNT | PCK | DEP | SRE | DYN | DIR;
constexpr type_t RESERVED_VALUE = MNT | PCK | DEP;
return ((variant.key & DEPTH_MASK) == DEPTH_VARIANT) &&
((variant.key & RESERVED_MASK) != RESERVED_VALUE);
}
static constexpr bool isValidStandardVariant(Variant variant) noexcept {
// can't have shadow receiver if we don't have any lighting
constexpr type_t RESERVED0_MASK = VSM | FOG | SRE | DYN | DIR;
constexpr type_t RESERVED0_VALUE = VSM | FOG | SRE;
constexpr type_t RESERVED0_MASK = S2D | FOG | SRE | DYN | DIR;
constexpr type_t RESERVED0_VALUE = S2D | FOG | SRE;
// can't have shadow receiver if we don't have any lighting
constexpr type_t RESERVED1_MASK = VSM | SRE | DYN | DIR;
constexpr type_t RESERVED1_MASK = S2D | SRE | DYN | DIR;
constexpr type_t RESERVED1_VALUE = SRE;
// can't have VSM without shadow receiver
constexpr type_t RESERVED2_MASK = VSM | SRE;
constexpr type_t RESERVED2_VALUE = VSM;
constexpr type_t RESERVED2_MASK = S2D | SRE;
constexpr type_t RESERVED2_VALUE = S2D;
return ((variant.key & STANDARD_MASK) == STANDARD_VARIANT) &&
((variant.key & RESERVED0_MASK) != RESERVED0_VALUE) &&
@@ -174,11 +178,15 @@ struct Variant {
}
static constexpr bool isSSRVariant(Variant variant) noexcept {
return (variant.key & (STE | VSM | DEP | SRE | DYN | DIR)) == (VSM | SRE);
return (variant.key & SPECIAL_SSR_MASK) == SPECIAL_SSR_VARIANT;
}
static constexpr bool isVSMVariant(Variant variant) noexcept {
return !isSSRVariant(variant) && ((variant.key & VSM) == VSM);
static constexpr bool isShadowSampler2DVariant(Variant variant) noexcept {
return !isSSRVariant(variant) && ((variant.key & (S2D | DEP)) == S2D);
}
static constexpr bool isDepthMomentsVariant(Variant variant) noexcept {
return !isSSRVariant(variant) && ((variant.key & (MNT | DEP)) == (MNT | DEP));
}
static constexpr bool isShadowReceiverVariant(Variant variant) noexcept {
@@ -202,13 +210,13 @@ struct Variant {
// vertex shader.
if ((variant.key & STANDARD_MASK) == STANDARD_VARIANT) {
if (isSSRVariant(variant)) {
variant.key &= ~(VSM | SRE);
variant.key &= ~SPECIAL_SSR_VARIANT;
}
return variant & (STE | SKN | SRE | DYN | DIR);
}
if ((variant.key & DEPTH_MASK) == DEPTH_VARIANT) {
// Only VSM, skinning, and stereo affect the vertex shader's DEPTH variant
return variant & (STE | VSM | SKN | DEP);
// Only MNT, skinning, and stereo affect the vertex shader's DEPTH variant
return variant & (STE | MNT | SKN | DEP);
}
return {};
}
@@ -217,11 +225,11 @@ struct Variant {
// filter out fragment variants that are not needed. For e.g. skinning doesn't
// affect the fragment shader.
if ((variant.key & STANDARD_MASK) == STANDARD_VARIANT) {
return variant & (VSM | FOG | SRE | DYN | DIR);
return variant & (S2D | FOG | SRE | DYN | DIR);
}
if ((variant.key & DEPTH_MASK) == DEPTH_VARIANT) {
// Only VSM & PICKING affects the fragment shader's DEPTH variant
return variant & (VSM | PCK | DEP);
return variant & (MNT | PCK | DEP);
}
return {};
}
@@ -230,8 +238,8 @@ struct Variant {
// special case for depth variant
if (isValidDepthVariant(variant)) {
if (!isLit) {
// if we're unlit, we never need the VSM variant
return variant & ~VSM;
// if we're unlit, we never need the MNT variant
return variant & ~MNT;
}
return variant;
}
@@ -242,9 +250,9 @@ struct Variant {
// when the shading mode is unlit, remove all the lighting variants
return variant & UNLIT_MASK;
}
// if shadow receiver is disabled, turn off VSM
// if shadow receiver is disabled, we pick the shadow sampler
if (!(variant.key & SRE)) {
return variant & ~VSM;
return variant & ~S2D;
}
return variant;
}
+3 -4
View File
@@ -216,7 +216,7 @@ utils::CString getDescriptorName(DescriptorSetBindingPoints const set,
DescriptorSetLayout getPerViewDescriptorSetLayout(
MaterialDomain const domain,
bool const isLit, bool const isSSR, bool const hasFog,
bool const isVSM) noexcept {
bool const isShadowSampler2D) noexcept {
switch (domain) {
case MaterialDomain::SURFACE: {
@@ -255,7 +255,7 @@ DescriptorSetLayout getPerViewDescriptorSetLayout(
}
// change the SHADOW_MAP descriptor type for VSM
if (isVSM) {
if (isShadowSampler2D) {
auto const pos = std::find_if(layout.descriptors.begin(), layout.descriptors.end(),
[](auto const& v) {
return v.binding == PerViewBindingPoints::SHADOW_MAP;
@@ -285,8 +285,7 @@ DescriptorSetLayout getPerViewDescriptorSetLayoutWithVariant(
return ssrVariantDescriptorSetLayout;
}
// We need to filter out all the descriptors not included in the "resolved" layout below
return getPerViewDescriptorSetLayout(domain, isLit, isSSR, hasFog,
Variant::isVSMVariant(variant));
return getPerViewDescriptorSetLayout(domain, isLit, isSSR, hasFog, Variant::isShadowSampler2DVariant(variant));
}
DescriptorType getDescriptorType(SamplerType const type, SamplerFormat const format) {
+21 -16
View File
@@ -32,41 +32,46 @@ namespace filament {
Variant Variant::filterUserVariant(
Variant variant, UserVariantFilterMask filterMask) noexcept {
// these are easy to filter by just removing the corresponding bit
if (filterMask & (uint32_t)UserVariantFilterBit::DIRECTIONAL_LIGHTING) {
if (filterMask & uint32_t(UserVariantFilterBit::DIRECTIONAL_LIGHTING)) {
variant.key &= ~DIR;
}
if (filterMask & (uint32_t)UserVariantFilterBit::DYNAMIC_LIGHTING) {
if (filterMask & uint32_t(UserVariantFilterBit::DYNAMIC_LIGHTING)) {
variant.key &= ~DYN;
}
if (filterMask & (uint32_t)UserVariantFilterBit::SKINNING) {
if (filterMask & uint32_t(UserVariantFilterBit::SKINNING)) {
variant.key &= ~SKN;
}
if (filterMask & (uint32_t)UserVariantFilterBit::STE) {
if (filterMask & uint32_t(UserVariantFilterBit::STE)) {
variant.key &= ~(filterMask & STE);
}
if (!isValidDepthVariant(variant)) {
// we can't remove FOG from depth variants, this would, in fact, remove picking
if (filterMask & (uint32_t)UserVariantFilterBit::FOG) {
variant.key &= ~FOG;
if (isValidDepthVariant(variant)) {
// depth variants can have their MNT bit filtered
if (filterMask & uint32_t(UserVariantFilterBit::VSM)) {
variant.key &= ~MNT;
}
} else {
// depth variants can have their VSM bit filtered
if (filterMask & (uint32_t)UserVariantFilterBit::VSM) {
variant.key &= ~VSM;
// we can't remove FOG from depth variants, this would, in fact, remove picking
if (filterMask & uint32_t(UserVariantFilterBit::FOG)) {
variant.key &= ~FOG;
}
}
if (!isSSRVariant(variant)) {
// SSR variant needs to be handled separately
if (filterMask & (uint32_t)UserVariantFilterBit::SHADOW_RECEIVER) {
if (filterMask & uint32_t(UserVariantFilterBit::SHADOW_RECEIVER)) {
variant.key &= ~SRE;
}
if (filterMask & (uint32_t)UserVariantFilterBit::VSM) {
variant.key &= ~VSM;
if (filterMask & uint32_t(UserVariantFilterBit::VSM)) {
variant.key &= ~S2D;
}
} else {
// see if we need to filter out the SSR variants
if (filterMask & (uint32_t)UserVariantFilterBit::SSR) {
variant.key &= ~SPECIAL_SSR;
if (filterMask & uint32_t(UserVariantFilterBit::SSR)) {
variant.key &= ~SPECIAL_SSR_VARIANT;
}
}
return variant;
+2 -1
View File
@@ -60,7 +60,8 @@ void ShaderGenerator::generateSurfaceMaterialVariantDefines(io::sstream& out,
CodeGenerator::generateDefine(out, "VARIANT_HAS_SHADOWING",
litVariants && filament::Variant::isShadowReceiverVariant(variant));
CodeGenerator::generateDefine(out, "VARIANT_HAS_VSM",
filament::Variant::isVSMVariant(variant));
filament::Variant::isShadowSampler2DVariant(variant) ||
filament::Variant::isDepthMomentsVariant(variant));
CodeGenerator::generateDefine(out, "VARIANT_HAS_STEREO",
hasStereo(variant, featureLevel));
CodeGenerator::generateDefine(out, "VARIANT_DEPTH",
+5 -5
View File
@@ -49,7 +49,7 @@ SamplerInterfaceBlock const& SibGenerator::getPerViewSib(Variant variant) noexce
// reason we name them "unused*" to ensure we're not using them by mistake (type/format don't
// matter).
static SamplerInterfaceBlock const sibPcf{ SamplerInterfaceBlock::Builder()
static SamplerInterfaceBlock const sibShadowSamplerPcf{ SamplerInterfaceBlock::Builder()
.name("sampler0")
.stageFlags(backend::ShaderStageFlags::FRAGMENT)
.add( {{ "shadowMap", +PerViewBindingPoints::SHADOW_MAP, Type::SAMPLER_2D_ARRAY, Format::SHADOW, Precision::MEDIUM, FILTERABLE, !MULTISAMPLE, ALL_STAGES },
@@ -62,7 +62,7 @@ SamplerInterfaceBlock const& SibGenerator::getPerViewSib(Variant variant) noexce
)
.build() };
static SamplerInterfaceBlock const sibVsm{ SamplerInterfaceBlock::Builder()
static SamplerInterfaceBlock const sibShadowSampler2D{ SamplerInterfaceBlock::Builder()
.name("sampler0")
.stageFlags(backend::ShaderStageFlags::FRAGMENT)
.add( {{ "shadowMap", +PerViewBindingPoints::SHADOW_MAP, Type::SAMPLER_2D_ARRAY, Format::FLOAT, Precision::HIGH, FILTERABLE, !MULTISAMPLE, ALL_STAGES },
@@ -85,10 +85,10 @@ SamplerInterfaceBlock const& SibGenerator::getPerViewSib(Variant variant) noexce
if (Variant::isSSRVariant(variant)) {
return sibSsr;
} else if (Variant::isVSMVariant(variant)) {
return sibVsm;
} else if (Variant::isShadowSampler2DVariant(variant)) {
return sibShadowSampler2D;
} else {
return sibPcf;
return sibShadowSamplerPcf;
}
}
+1 -1
View File
@@ -175,7 +175,7 @@ BufferInterfaceBlock const& UibGenerator::getPerViewUib() noexcept {
// VSM shadows [variant: VSM]
// ------------------------------------------------------------------------------------
{ "vsmExponent", 0, Type::FLOAT },
{ "vsmDepthScale", 0, Type::FLOAT },
{ "vsmMaxMoment", 0, Type::FLOAT, Precision::HIGH },
{ "vsmLightBleedReduction", 0, Type::FLOAT },
{ "shadowSamplingType", 0, Type::UINT },
+2 -1
View File
@@ -48,10 +48,11 @@ std::string formatVariantString(Variant variant, MaterialDomain domain) noexcept
if (variant.key & Variant::DEP) variantString += "DEP|";
if (variant.key & Variant::DEP) {
if (variant.key & Variant::PCK) variantString += "PCK|";
if (variant.key & Variant::MNT) variantString += "MNT|";
} else {
if (variant.key & Variant::FOG) variantString += "FOG|";
if (variant.key & Variant::S2D) variantString += "S2D|";
}
if (variant.key & Variant::VSM) variantString += "VSM|";
if (variant.key & Variant::STE) variantString += "STE|";
variantString = variantString.substr(0, variantString.length() - 1);
}
+2
View File
@@ -31,6 +31,7 @@ set(DIST_HDRS
${PUBLIC_HDR_DIR}/${TARGET}/Invocable.h
${PUBLIC_HDR_DIR}/${TARGET}/Log.h
${PUBLIC_HDR_DIR}/${TARGET}/Logger.h
${PUBLIC_HDR_DIR}/${TARGET}/LruCache.h
${PUBLIC_HDR_DIR}/${TARGET}/memalign.h
${PUBLIC_HDR_DIR}/${TARGET}/MonotonicRingMap.h
${PUBLIC_HDR_DIR}/${TARGET}/Mutex.h
@@ -181,6 +182,7 @@ if (FILAMENT_BUILD_TESTING)
test/test_FixedCircularBuffer.cpp
test/test_Hash.cpp
test/test_InternPool.cpp
test/test_LruCache.cpp
test/test_JobSystem.cpp
test/test_MonotonicRingMap.cpp
test/test_QuadTreeArray.cpp
+322
View File
@@ -0,0 +1,322 @@
/*
* 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.
*/
#ifndef TNT_UTILS_LRUCACHE_H
#define TNT_UTILS_LRUCACHE_H
#include <utils/Allocator.h>
#include <utils/compiler.h>
#include <tsl/robin_map.h>
#include <cassert>
#include <cstddef>
#include <functional>
#include <optional>
#include <utility>
namespace utils {
/**
* A fixed-capacity Least Recently Used (LRU) cache.
*
* This container allows O(1) access, insertion, and eviction. It uses an Arena with an
* ObjectPoolAllocator to store the nodes of a doubly-linked list representing the LRU order,
* and a robin_map for fast key lookups.
*
* This class is not thread-safe. Additionally, the pop() and put() methods have special concerns
* with regards to object lifetimes.
*
* @tparam Key The type of the keys.
* @tparam T The type of the values.
* @tparam Hash The hasher for the keys.
*/
template<typename Key,
typename T,
typename Hash = std::hash<Key>,
typename KeyEqual = std::equal_to<Key>>
class LruCache {
// If Key is bigger than a pointer, robin map keys are references to Node::key. Otherwise,
// they're full copies.
using MapKey = std::conditional_t<(sizeof(Key) > sizeof(void*)),
std::reference_wrapper<const Key>, Key>;
public:
using size_type = uint32_t;
/**
* Creates an LRU cache with the specified capacity.
*
* @param name Name to identify the backing Arena allocator.
* @param capacity The maximum number of elements the cache can hold.
*/
LruCache(const char* UTILS_NONNULL name, size_type capacity)
: mCapacity(capacity),
// HACK: FreeList cannot handle a capacity of 0.
mArena(name, (capacity ? capacity : 1) * sizeof(Node)),
mHead(nullptr),
mTail(nullptr) {
mMap.reserve(capacity);
}
~LruCache() {
clear();
}
LruCache(const LruCache&) = delete;
LruCache& operator=(const LruCache&) = delete;
LruCache(LruCache&&) = default;
LruCache& operator=(LruCache&&) = default;
/**
* Returns a reference to the value associated with the key.
*
* Additionally moves the key/value pair to the front of the most recently used (MRU) list.
*
* @param key The key to look up.
* @param hash The precomputed hash of the key.
* @return Pointer to the value, if exists; otherwise, nullptr.
*/
T* UTILS_NULLABLE get(Key const& key, size_t hash) {
auto it = mMap.find(key, hash);
if (UTILS_UNLIKELY(it == mMap.end())) {
return nullptr;
}
Node* node = it->second;
assert(node != nullptr);
moveToFront(node);
return &node->value;
}
inline T* UTILS_NULLABLE get(Key const& key) {
return get(key, Hash{}(key));
}
/**
* Moves the value out of the cache, if it exists.
*
* Because this moves the actual value, any previous references to this object returned by a
* call to get() or put() is invalidated by this call.
*
* @param key The key to look up.
* @param hash The precomputed hash of the key.
*/
std::optional<T> pop(Key const& key, size_t hash) {
auto it = mMap.find(key, hash);
if (UTILS_UNLIKELY(it == mMap.end())) {
return std::nullopt;
}
Node* node = it->second;
assert(node != nullptr);
if (node == mHead && node == mTail) {
mHead = nullptr;
mTail = nullptr;
} else if (node == mHead) {
assert(node->next != nullptr);
mHead = node->next;
mHead->prev = nullptr;
} else if (node == mTail) {
assert(node->prev != nullptr);
mTail = node->prev;
mTail->next = nullptr;
} else {
assert(node->prev != nullptr);
assert(node->next != nullptr);
node->prev->next = node->next;
node->next->prev = node->prev;
}
T r = std::move(node->value);
mMap.erase(it);
mArena.destroy(node);
return r;
}
inline std::optional<T> pop(Key const& key) {
return pop(key, Hash{}(key));
}
/**
* Inserts a new entry or updates an existing one.
*
* Prepends it to the front of the most recently used (MRU) list. Potentially evicts the
* least-recently used (LRU) key/value pair by calling the releaser function.
*
* Due to the evicting nature of this function, any pointers to any objects within the LRU cache
* that had been previously returned by a call to get() or another call to put() can be
* considered INVALID and any access to them will result in undefined behavior.
*
* An evicted node is removed from the map before the releaser callback is called. Calls to
* get() or pop() in the body of the releaser for the item in question will fail.
*
* @tparam F Callable type accepting T&&.
* @param key The key to insert or update.
* @param t The value to insert or update.
* @param hash The precomputed hash of the key.
* @param releaser Function called with the evicted value (T&&) if eviction occurs.
* @return Reference to the inserted or updated value.
*/
template<typename F>
T& put(Key key, T value, size_t hash, F releaser) {
// Assert that we have capacity to store at least one item
assert(mCapacity > 0);
auto it = mMap.find(key, hash);
if (it != mMap.end()) {
Node* node = it->second;
assert(node != nullptr);
moveToFront(node);
node->value = std::move(value);
return node->value;
}
if (UTILS_LIKELY(mMap.size() >= mCapacity)) {
evict(releaser);
}
// Create new node at front of list.
Node* node = mArena.template make<Node>(std::move(key), std::move(value), mHead);
assert(node != nullptr);
attach(node);
mMap.emplace(node->key, node);
return node->value;
}
template<typename F>
inline T& put(Key key, T value, F releaser) {
return put(std::move(key), std::move(value), Hash{}(key), std::move(releaser));
}
/**
* Clear the cache, calling releaser on each item evicted.
*
* @param releaser Function called with the evicted value (T&&)
*/
template<typename F>
void clear(F releaser) {
// Clear map first, just in case it tries to dereference any keys.
mMap.clear();
// Destroy everything in the arena.
Node* node = mHead;
while (UTILS_LIKELY(node)) {
Node* next = node->next;
releaser(std::move(node->value));
mArena.destroy(node);
node = next;
}
mHead = nullptr;
mTail = nullptr;
}
void clear() {
clear([](T&&){});
}
/** Returns the number of elements in the cache. */
size_type size() const noexcept { return mMap.size(); }
/** Returns the capacity of the cache. */
size_type capacity() const noexcept { return mCapacity; }
private:
struct Node {
Key key;
T value;
Node* UTILS_NULLABLE prev;
Node* UTILS_NULLABLE next;
Node(Key&& k, T&& v, Node* UTILS_NULLABLE next)
: key(std::move(k)),
value(std::move(v)),
prev(nullptr),
next(next) {}
};
void moveToFront(Node* UTILS_NONNULL node) {
if (node == mHead) {
return;
}
// First, detach from list.
if (UTILS_UNLIKELY(node == mTail)) {
assert(node->prev != nullptr);
mTail = node->prev;
mTail->next = nullptr;
} else {
assert(node->prev != nullptr);
assert(node->next != nullptr);
node->prev->next = node->next;
node->next->prev = node->prev;
}
// Then attach to head.
node->prev = nullptr;
node->next = mHead;
attach(node);
}
// Attach node to front.
void attach(Node* UTILS_NONNULL node) {
if (UTILS_LIKELY(mHead != nullptr)) {
mHead->prev = node;
} else {
assert(mTail == nullptr);
mTail = node;
}
mHead = node;
}
// Evict the least recently used node.
template<typename F>
void evict(F releaser) {
Node* node = mTail;
assert(node != nullptr);
if (UTILS_UNLIKELY(node == mHead)) {
mTail = nullptr;
mHead = nullptr;
} else {
// Move new tail node to mTail.
assert(node->prev != nullptr);
mTail = node->prev;
mTail->next = nullptr;
}
// Finally free the node.
mMap.erase(node->key);
releaser(std::move(node->value));
mArena.destroy(node);
}
size_type mCapacity;
utils::Arena<utils::ObjectPoolAllocator<Node>, utils::LockingPolicy::NoLock> mArena;
tsl::robin_map<MapKey, Node* UTILS_NONNULL, Hash, KeyEqual> mMap;
Node* UTILS_NULLABLE mHead;
Node* UTILS_NULLABLE mTail;
};
} // namespace utils
#endif // TNT_UTILS_LRUCACHE_H
+49 -9
View File
@@ -19,6 +19,7 @@
#include <utils/Panic.h>
#include <utils/compiler.h>
#include <utils/debug.h>
#include <utils/LruCache.h>
#include <tsl/robin_map.h>
@@ -75,8 +76,6 @@ class RefCountedMap {
T value;
};
using Map = tsl::robin_map<Key, Entry, Hash>;
static constexpr TValue& deref(T& a) {
if constexpr (refcountedmap::IsPointer<T>) {
return *a;
@@ -99,6 +98,9 @@ class RefCountedMap {
"Attempted to get missing value";
public:
explicit RefCountedMap(const char* UTILS_NONNULL name = "RefCountedMap", size_t lruCapacity = 0)
: mLruCache(name, lruCapacity) {}
/** Acquire and return a value by key, initializing it with F if it doesn't exist.
*
* If F returns NullValue{}(), this indicates a failure to create the object. If T is a value
@@ -111,12 +113,16 @@ public:
it.value().referenceCount++;
return &deref(it.value().value);
}
if (std::optional<T> lruValue = mLruCache.pop(key, hash)) {
return &insert(key, std::move(*lruValue));
}
T r = factory();
if (r == NullValue{}()) {
return nullptr;
}
// TODO: how to use above computed hash here?
return &deref(mMap.insert({ key, Entry{ 1, std::move(r) } }).first.value().value);
return &insert(key, std::move(r));
}
template<typename F>
@@ -138,6 +144,11 @@ public:
it.value().referenceCount++;
return &deref(it.value().value);
}
if (std::optional<T> lruValue = mLruCache.pop(key, hash)) {
return &insert(key, std::move(*lruValue));
}
// TODO: how to use above computed hash here?
mMap.insert({ key, Entry{ 1, NullValue{}() } });
return nullptr;
@@ -156,8 +167,14 @@ public:
auto it = mMap.find(key, hash);
FILAMENT_CHECK_PRECONDITION(it != mMap.end()) << MISSING_ENTRY_ERROR_STRING;
if (--it.value().referenceCount == 0) {
if (it.value().value != NullValue{}()){
releaser(deref(it.value().value));
if (it.value().value != NullValue{}()) {
if (mLruCache.capacity() > 0) {
mLruCache.put(key, std::move(it.value().value), hash, [&releaser](T&& v) {
releaser(deref(v));
});
} else {
releaser(deref(it.value().value));
}
}
// TODO: change to erase_fast
mMap.erase(it);
@@ -177,6 +194,9 @@ public:
auto it = mMap.find(key, hash);
FILAMENT_CHECK_PRECONDITION(it != mMap.end()) << MISSING_ENTRY_ERROR_STRING;
if (--it.value().referenceCount == 0) {
if (mLruCache.capacity() > 0) {
mLruCache.put(key, std::move(it.value().value), hash, [](T&&){});
}
// TODO: change to erase_fast
mMap.erase(it);
}
@@ -217,7 +237,7 @@ public:
* Panics if no entry found in map.
*/
TValue& get(KeyRef key, size_t hash) {
auto it = mMap.find(key);
auto it = mMap.find(key, hash);
FILAMENT_CHECK_PRECONDITION(it != mMap.end()) << MISSING_ENTRY_ERROR_STRING;
FILAMENT_CHECK_PRECONDITION(it.value().value != NullValue{}())
<< MISSING_VALUE_ERROR_STRING;
@@ -236,13 +256,33 @@ public:
inline TValue const& get(KeyRef key) const noexcept { return get(key, Hash{}(key)); }
/** Clear the LRU cache, calling releaser on each item evicted. */
template<typename F>
void clearLruCache(F releaser) {
mLruCache.clear([&releaser](T&& v) {
releaser(deref(v));
});
}
/** Clear the LRU cache without calling any releasers. */
void clearLruCache() {
mLruCache.clear();
}
/** Returns true if the map is empty. */
inline bool empty() const noexcept { return mMap.empty(); }
private:
Map mMap;
tsl::robin_map<Key, Entry, Hash> mMap;
utils::LruCache<Key, T, Hash> mLruCache;
TValue& insert(KeyRef key, T value) {
// TODO: how to use computed hash here?
auto it = mMap.insert({ key, Entry{ 1, std::move(value) } });
return deref(it.first.value().value);
}
};
}
} // utils
#endif // TNT_UTILS_REFCOUNTEDMAP_H
+2 -2
View File
@@ -87,7 +87,7 @@ FreeList::Node* FreeList::init(void* begin, void* end,
void* const p = pointermath::align(begin, alignment, extra);
void* const n = pointermath::align(pointermath::add(p, elementSize), alignment, extra);
assert_invariant(p >= begin && p < end);
assert_invariant(n >= begin && n < end && n > p);
assert_invariant(n >= begin && n <= end && n > p);
const size_t d = uintptr_t(n) - uintptr_t(p);
const size_t num = (uintptr_t(end) - uintptr_t(p)) / d;
@@ -128,7 +128,7 @@ AtomicFreeList::AtomicFreeList(void* begin, void* end,
void* const p = pointermath::align(begin, alignment, extra);
void* const n = pointermath::align(pointermath::add(p, elementSize), alignment, extra);
assert_invariant(p >= begin && p < end);
assert_invariant(n >= begin && n < end && n > p);
assert_invariant(n >= begin && n <= end && n > p);
const size_t d = uintptr_t(n) - uintptr_t(p);
const size_t num = (uintptr_t(end) - uintptr_t(p)) / d;
+6 -1
View File
@@ -17,6 +17,7 @@
// TODO: Clean-up. We shouldn't need this #ifndef here, but a client has requested that perfetto be
// disabled due to size increase. In their case, this flag would be defined across targets. Hence
// we guard below with an #ifndef.
#include <cstring>
#ifndef FILAMENT_TRACING_ENABLED
// Note: The overhead of TRACING is not negligible especially with parallel_for().
#define FILAMENT_TRACING_ENABLED false
@@ -98,7 +99,11 @@ namespace utils {
void JobSystem::setThreadName(const char* name) noexcept {
#if defined(__linux__)
pthread_setname_np(pthread_self(), name);
constexpr size_t MAX_PTHREAD_NAME_LEN = 16;
char buf[MAX_PTHREAD_NAME_LEN];
strncpy(buf, name, MAX_PTHREAD_NAME_LEN - 1);
buf[MAX_PTHREAD_NAME_LEN - 1] = '\0';
pthread_setname_np(pthread_self(), buf);
#elif defined(__APPLE__)
pthread_setname_np(name);
#elif defined(WIN32)
+268
View File
@@ -0,0 +1,268 @@
/*
* 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.
*/
#include <utils/LruCache.h>
#include <gtest/gtest.h>
#include <string>
#include <vector>
using namespace utils;
TEST(LruCacheTest, BasicPutGet) {
LruCache<int, std::string> cache("LruCacheTest", 3);
cache.put(1, "one", [](std::string&&) {});
cache.put(2, "two", [](std::string&&) {});
auto* v1 = cache.get(1);
ASSERT_NE(v1, nullptr);
EXPECT_EQ(*v1, "one");
auto* v2 = cache.get(2);
ASSERT_NE(v2, nullptr);
EXPECT_EQ(*v2, "two");
EXPECT_EQ(cache.size(), 2);
}
TEST(LruCacheTest, MissingKey) {
LruCache<int, int> cache("LruCacheTest", 3);
EXPECT_EQ(cache.get(999), nullptr);
}
TEST(LruCacheTest, Eviction) {
LruCache<int, int> cache("LruCacheTest", 3);
std::vector<int> evicted;
auto releaser = [&](int&& v) { evicted.push_back(v); };
cache.put(1, 10, releaser);
cache.put(2, 20, releaser);
cache.put(3, 30, releaser);
// Cache: 3(MRU), 2, 1(LRU)
EXPECT_EQ(cache.size(), 3);
EXPECT_TRUE(evicted.empty());
cache.put(4, 40, releaser);
// Evicted 1. Cache: 4(MRU), 3, 2
EXPECT_EQ(cache.size(), 3);
ASSERT_EQ(evicted.size(), 1);
EXPECT_EQ(evicted[0], 10);
// Verify contents
EXPECT_NE(cache.get(2), nullptr);
EXPECT_NE(cache.get(3), nullptr);
EXPECT_NE(cache.get(4), nullptr);
EXPECT_EQ(cache.get(1), nullptr);
// Accessing 2 makes it MRU. Cache: 2(MRU), 4, 3(LRU)
cache.get(2);
cache.put(5, 50, releaser);
// Should evict 3.
ASSERT_EQ(evicted.size(), 2);
EXPECT_EQ(evicted[1], 30);
}
TEST(LruCacheTest, UpdatePromotesToMRU) {
LruCache<int, int> cache("LruCacheTest", 3);
auto releaser = [](int&&) {};
cache.put(1, 10, releaser);
cache.put(2, 20, releaser);
cache.put(3, 30, releaser);
// Order: 3, 2, 1
cache.put(1, 11, releaser);
// Order: 1, 3, 2
cache.put(4, 40, releaser);
// Order: 4, 1, 3, 2 (evicted)
// Check if 1 exists
auto* v1 = cache.get(1);
ASSERT_NE(v1, nullptr);
EXPECT_EQ(*v1, 11);
}
TEST(LruCacheTest, GetPromotesToMRU) {
LruCache<int, int> cache("LruCacheTest", 3);
std::vector<int> evicted;
auto releaser = [&](int&& v) { evicted.push_back(v); };
cache.put(1, 10, releaser);
cache.put(2, 20, releaser);
cache.put(3, 30, releaser);
// 3, 2, 1
cache.get(1);
// 1, 3, 2
cache.put(4, 40, releaser);
// Evicts 2.
ASSERT_EQ(evicted.size(), 1);
EXPECT_EQ(evicted[0], 20);
}
TEST(LruCacheTest, MoveToFrontFromTail) {
// This specific test targets the potential bug where moving from tail doesn't update tail.
LruCache<int, int> cache("LruCacheTest", 3);
std::vector<int> evicted;
auto releaser = [&](int&& v) { evicted.push_back(v); };
cache.put(1, 10, releaser);
cache.put(2, 20, releaser);
cache.put(3, 30, releaser);
// State: 3 (MRU) -> 2 -> 1 (LRU, Tail)
// Move 1 to front.
cache.get(1);
// Expected State: 1 (MRU) -> 3 -> 2 (LRU, Tail)
// Add 4. Should evict 2.
cache.put(4, 40, releaser);
ASSERT_EQ(evicted.size(), 1);
EXPECT_EQ(evicted[0], 20);
}
TEST(LruCacheTest, MoveToFrontFromMiddle) {
LruCache<int, int> cache("LruCacheTest", 3);
std::vector<int> evicted;
auto releaser = [&](int&& v) { evicted.push_back(v); };
cache.put(1, 10, releaser);
cache.put(2, 20, releaser);
cache.put(3, 30, releaser);
// 3, 2, 1
cache.get(2);
// 2, 3, 1
cache.put(4, 40, releaser);
// Evicts 1.
ASSERT_EQ(evicted.size(), 1);
EXPECT_EQ(evicted[0], 10);
}
TEST(LruCacheTest, CapacityOne) {
LruCache<int, int> cache("LruCacheTest", 1);
std::vector<int> evicted;
auto releaser = [&](int&& v) { evicted.push_back(v); };
cache.put(1, 10, releaser);
EXPECT_NE(cache.get(1), nullptr);
EXPECT_EQ(*cache.get(1), 10);
cache.put(2, 20, releaser);
ASSERT_EQ(evicted.size(), 1);
EXPECT_EQ(evicted[0], 10);
EXPECT_NE(cache.get(2), nullptr);
EXPECT_EQ(*cache.get(2), 20);
}
#ifdef GTEST_HAS_DEATH_TEST
TEST(LruCacheTest, CapacityZero) {
LruCache<int, int> cache("LruCacheTest", 0);
EXPECT_EQ(cache.capacity(), 0);
EXPECT_EQ(cache.size(), 0);
EXPECT_DEATH(cache.put(1, 1, [](int&&){}), "");
}
#endif
TEST(LruCacheTest, Clear) {
LruCache<int, int> cache("LruCacheTest", 3);
std::vector<int> evicted;
auto releaser = [&](int&& v) { evicted.push_back(v); };
cache.put(1, 10, releaser);
cache.put(2, 20, releaser);
EXPECT_EQ(cache.size(), 2);
cache.clear(releaser);
EXPECT_EQ(cache.size(), 0);
ASSERT_EQ(evicted.size(), 2);
// Implementation traverses mHead -> mTail (MRU to LRU).
EXPECT_EQ(evicted[0], 20); // MRU
EXPECT_EQ(evicted[1], 10); // LRU
// We should now be able to refill the cache.
cache.put(3, 30, releaser);
cache.put(4, 40, releaser);
EXPECT_EQ(cache.size(), 2);
EXPECT_NE(cache.get(3), nullptr);
EXPECT_NE(cache.get(4), nullptr);
}
TEST(LruCacheTest, Pop) {
LruCache<int, int> cache("LruCacheTest", 3);
auto releaser = [](int&&) {};
cache.put(1, 10, releaser);
cache.put(2, 20, releaser);
std::optional<int> val1 = cache.pop(1);
EXPECT_TRUE(val1.has_value());
EXPECT_EQ(*val1, 10);
EXPECT_EQ(cache.size(), 1);
EXPECT_EQ(cache.get(1), nullptr);
EXPECT_NE(cache.get(2), nullptr);
std::optional<int> val2 = cache.pop(2);
EXPECT_TRUE(val2.has_value());
EXPECT_EQ(*val2, 20);
EXPECT_EQ(cache.size(), 0);
}
/* Large key tests. */
struct LargeKey {
long long a, b;
bool operator==(LargeKey const& other) const {
return a == other.a && b == other.b;
}
};
struct LargeKeyHash {
size_t operator()(LargeKey const& k) const {
return std::hash<long long>{}(k.a) ^ std::hash<long long>{}(k.b);
}
};
TEST(LruCacheTest, LargeKey) {
static_assert(sizeof(LargeKey) > sizeof(void*), "LargeKey must be larger than pointer");
LruCache<LargeKey, int, LargeKeyHash> cache("LruCacheTest", 2);
LargeKey k1{1, 1};
LargeKey k2{2, 2};
LargeKey k3{3, 3};
cache.put(k1, 100, [](int&&){});
cache.put(k2, 200, [](int&&){});
EXPECT_NE(cache.get(k1), nullptr);
EXPECT_EQ(*cache.get(k1), 100);
// Evict k2 (LRU because k1 was accessed)
cache.put(k3, 300, [](int&&){});
EXPECT_NE(cache.get(k3), nullptr); // k3 present
}
+59
View File
@@ -261,3 +261,62 @@ TEST(RefCountedMapTest, SmartPointerType_PanicsIfGetsNullValue) {
ASSERT_DEATH(map.get(1), "");
}
#endif // GTEST_HAS_DEATH_TEST
TEST(RefCountedMapTest, LruRecycling) {
RefCountedMap<KeyType, SmartPointerType> map("RefCountedMapTest", 1);
bool factoryCalled = false;
auto factory = [&]() {
factoryCalled = true;
return std::make_unique<size_t>(100);
};
// 1. Acquire K1. Ref=1.
ValueType* v1 = map.acquire(1, factory);
ASSERT_NE(v1, nullptr);
EXPECT_EQ(*v1, 100);
EXPECT_TRUE(factoryCalled);
// 2. Release K1. Ref=0. Should move to LRU.
map.release(1);
// map.empty() checks mMap. mMap should be empty.
EXPECT_TRUE(map.empty());
// 3. Acquire K1 again. Should come from LRU.
factoryCalled = false;
ValueType* v2 = map.acquire(1, factory);
ASSERT_NE(v2, nullptr);
EXPECT_EQ(*v2, 100);
// Factory should NOT be called.
EXPECT_FALSE(factoryCalled);
// The underlying pointer (ValueType*) should be the same.
EXPECT_EQ(v1, v2);
// 4. Release K1.
map.release(1);
}
TEST(RefCountedMapTest, ClearLruCache) {
RefCountedMap<KeyType, ValueType> map("RefCountedMapTest", 2);
int destroyed = 0;
auto releaser = [&](ValueType& v) { destroyed++; };
// Acquire and release two items to fill LRU
map.acquire(1, []{ return 10; });
map.release(1, releaser);
map.acquire(2, []{ return 20; });
map.release(2, releaser);
// LRU size 2. Destoyed 0.
EXPECT_EQ(destroyed, 0);
map.clearLruCache(releaser);
EXPECT_EQ(destroyed, 2);
// Check they are gone (revival fails)
bool factoryCalled = false;
map.acquire(1, [&]{ factoryCalled = true; return 11; });
EXPECT_TRUE(factoryCalled);
map.release(1, releaser);
}
+22 -16
View File
@@ -20,7 +20,7 @@ layout(location = 0) out highp uvec2 outPicking;
// note: VARIANT_HAS_VSM and VARIANT_HAS_PICKING are mutually exclusive
//------------------------------------------------------------------------------
highp vec2 computeDepthMomentsVSM(const highp float depth);
highp vec4 computeDepthMomentsVSM(const highp float depth);
void main() {
filament_lodBias = frameUniforms.lodBias;
@@ -53,9 +53,7 @@ void main() {
// we always compute the "negative" side of ELVSM because the cost is small, and this allows
// EVSM/ELVSM choice to be done on the CPU side more easily.
highp float depth = vertex_worldPosition.w;
depth = exp(frameUniforms.vsmExponent * depth);
fragColor.xy = computeDepthMomentsVSM(depth);
fragColor.zw = computeDepthMomentsVSM(-1.0 / depth); // requires at least RGBA16F
fragColor = computeDepthMomentsVSM(depth);
#elif defined(VARIANT_HAS_PICKING)
#if FILAMENT_EFFECTIVE_VERSION == 100
outPicking.a = mod(float(object_uniforms_objectId / 65536), 256.0) / 255.0;
@@ -74,22 +72,30 @@ void main() {
#endif
}
highp vec2 computeDepthMomentsVSM(const highp float depth) {
// computes the moments
#if MATERIAL_FEATURE_LEVEL > 0
highp vec4 computeDepthMomentsVSM(const highp float depth) {
// See GPU Gems 3
// https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-8-summed-area-variance-shadow-maps
highp vec2 moments;
// computes the first two moments
float c = frameUniforms.vsmExponent;
highp float MAX_MOMENT = frameUniforms.vsmMaxMoment;
// the first moment is just the depth (average)
moments.x = depth;
// wrap depth for EVSM
highp float z = exp(c * depth);
// compute the 2nd moment over the pixel extents.
moments.y = depth * depth;
// compute EVSM moments
highp vec2 m1 = vec2(z, -1.0 / z);
highp vec2 m2 = m1 * m1;
// the local linear approximation is not correct with a warped depth
//highp float dx = dFdx(depth);
//highp float dy = dFdy(depth);
//moments.y += 0.25 * (dx * dx + dy * dy);
// compute analytic variance (2nd moment), taking into account the change in depth accross the texel
highp float dzdx = dFdx(depth);
highp float dzdy = dFdy(depth);
highp float linearVariance = 0.25 * (dzdx * dzdx + dzdy * dzdy);
highp vec2 analyticVariance = c * c * m2 * linearVariance;
m2 = min(m2 + analyticVariance, MAX_MOMENT);
return moments;
return vec4(m1.x, m2.x, m1.y, m2.y);
}
#endif
+1 -1
View File
@@ -111,7 +111,7 @@ highp vec4 getSpotLightSpacePosition(int index, highp vec3 dir, highp float zLig
highp mat4 lightFromWorldMatrix = shadowUniforms.shadows[index].lightFromWorldMatrix;
// for spotlights, the bias depends on z
float bias = shadowUniforms.shadows[index].normalBias * zLight;
highp vec2 bias = shadowUniforms.shadows[index].normalBias * zLight;
return computeLightSpacePosition(getWorldPosition(), getWorldGeometricNormalVector(),
dir, bias, lightFromWorldMatrix);
+92 -69
View File
@@ -335,6 +335,91 @@ float ShadowSample_PCSS(const bool DIRECTIONAL,
return 1.0 - percentageOccluded;
}
//------------------------------------------------------------------------------
// VSM
//------------------------------------------------------------------------------
float chebyshevUpperBound(const highp vec2 moments, const highp float depth,
const highp float minVariance, const highp float lbrAmount) {
// Fast path: if the receiver is fully in front of the caster
if (depth <= moments.x) {
return 1.0;
}
// Calculate variance with our dynamically injected floor
highp float variance = max(moments.y - (moments.x * moments.x), minVariance);
// Standard Chebyshev inequality
highp float d = depth - moments.x;
highp float p_max = variance / (variance + d * d);
// Apply Light Bleeding Reduction (LBR)
return saturate((p_max - lbrAmount) / (1.0 - lbrAmount));
}
float evaluateEVSM(const bool ELVSM, float c,
const highp vec4 moments, const highp float zReceiver,
const highp vec2 dzduv, const highp vec2 texelSize) {
const highp float EPSILON_MULTIPLIER = 0.002; // could be 0.00001 in fp32
float lbrAmount = frameUniforms.vsmLightBleedReduction;
// Scale the UV-space gradient down to a single shadow map texel footprint.
highp vec2 texel_dzduv = dzduv * texelSize;
// squared magnitude of the linear depth gradient across a single shadow map texel footprint
highp float dz2 = dot(texel_dzduv, texel_dzduv);
// remap depth to [-1, 1]
highp float depth = zReceiver * 2.0 - 1.0;
// positive wrap
highp float pw = exp(c * depth);
highp float epsilon = EPSILON_MULTIPLIER * (pw * pw);
// Dynamic variance for the positive side (derivative of wraped depth w.r.t. light-space depth via Chain Rule)
highp float dpwdz = 2.0 * c * pw;
highp float pMinVariance = epsilon + 0.25 * (dpwdz * dpwdz) * dz2;
float p = chebyshevUpperBound(moments.xy, pw, pMinVariance, lbrAmount);
// negative wrap
if (ELVSM) {
highp float nw = -1.0 / pw;
highp float epsilon = EPSILON_MULTIPLIER * (nw * nw);
// Dynamic variance for the negative side (derivative of wraped depth w.r.t. light-space depth via Chain Rule)
highp float dnwdz = 2.0 * c * nw;
highp float nMinVariance = epsilon + 0.25 * (dnwdz * dnwdz) * dz2;
float n = chebyshevUpperBound(moments.zw, nw, nMinVariance, lbrAmount);
p = min(p, n);
}
return p;
}
float ShadowSample_VSM(const bool DIRECTIONAL, const highp sampler2DArray shadowMap,
const highp vec4 scissorNormalized,
const uint layer, const int index,
const highp vec4 shadowPosition, const highp float zLight) {
bool ELVSM = shadowUniforms.shadows[index].elvsm;
float c = shadowUniforms.shadows[index].vsmExponent;
highp vec2 texelSize = vec2(1.0) / vec2(textureSize(shadowMap, 0)); // TODO: put this in a uniform
// note: shadowPosition.z is in linear light-space normalized to [0, 1]
// see: ShadowMap::computeVsmLightSpaceMatrix() in ShadowMap.cpp
// see: computeLightSpacePosition() in common_shadowing.fs
highp vec3 position = vec3(shadowPosition.xy * (1.0 / shadowPosition.w), shadowPosition.z);
// plane receiver bias to reduce shadow acnee on the received plane (before clamp)
highp vec2 dzduv = computeReceiverPlaneDepthBias(position);
// clamp uv to border
position.xy = clamp(position.xy, scissorNormalized.xy, scissorNormalized.zw);
// Read the shadow map with all available filtering
highp vec4 moments = texture(shadowMap, vec3(position.xy, layer));
return evaluateEVSM(ELVSM, c, moments, position.z, dzduv, texelSize);
}
//------------------------------------------------------------------------------
// Screen-space Contact Shadows
//------------------------------------------------------------------------------
@@ -409,70 +494,6 @@ float screenSpaceContactShadow(vec3 lightDirection) {
return occlusion;
}
//------------------------------------------------------------------------------
// VSM
//------------------------------------------------------------------------------
float linstep(const float min, const float max, const float v) {
// we could use smoothstep() too
return clamp((v - min) / (max - min), 0.0, 1.0);
}
float reduceLightBleed(const float pMax, const float amount) {
// Remove the [0, amount] tail and linearly rescale (amount, 1].
return linstep(amount, 1.0, pMax);
}
float chebyshevUpperBound(const highp vec2 moments, const highp float mean,
const highp float minVariance, const float lightBleedReduction) {
// Donnelly and Lauritzen 2006, "Variance Shadow Maps"
highp float variance = moments.y - (moments.x * moments.x);
variance = max(variance, minVariance);
highp float d = mean - moments.x;
float pMax = variance / (variance + d * d);
pMax = reduceLightBleed(pMax, lightBleedReduction);
return mean <= moments.x ? 1.0 : pMax;
}
float evaluateShadowVSM(const highp vec2 moments, const highp float depth) {
highp float depthScale = frameUniforms.vsmDepthScale * depth;
highp float minVariance = depthScale * depthScale;
return chebyshevUpperBound(moments, depth, minVariance, frameUniforms.vsmLightBleedReduction);
}
float ShadowSample_VSM(const bool ELVSM, const highp sampler2DArray shadowMap,
const highp vec4 scissorNormalized,
const uint layer, const highp vec4 shadowPosition) {
// note: shadowPosition.z is in linear light-space normalized to [0, 1]
// see: ShadowMap::computeVsmLightSpaceMatrix() in ShadowMap.cpp
// see: computeLightSpacePosition() in common_shadowing.fs
highp vec3 position = vec3(shadowPosition.xy * (1.0 / shadowPosition.w), shadowPosition.z);
// Note: we don't need to clamp to `scissorNormalized` in the VSM case because this is only
// needed when the shadow casters and receivers are different, which is never the case with VSM
// (see ShadowMap.cpp).
// Read the shadow map with all available filtering
highp vec4 moments = texture(shadowMap, vec3(position.xy, layer));
highp float depth = position.z;
// EVSM depth warping
depth = depth * 2.0 - 1.0;
depth = frameUniforms.vsmExponent * depth;
depth = exp(depth);
float p = evaluateShadowVSM(moments.xy, depth);
if (ELVSM) {
p = min(p, evaluateShadowVSM(moments.zw, -1.0 / depth));
}
return p;
}
//------------------------------------------------------------------------------
// Shadow sampling dispatch
//------------------------------------------------------------------------------
@@ -529,6 +550,9 @@ float shadow(const bool DIRECTIONAL,
} else if (CONFIG_SHADOW_SAMPLING_METHOD == SHADOW_SAMPLING_PCF_LOW) {
return ShadowSample_PCF_Low(shadowMap, scissorNormalized, layer, shadowPosition);
}
// should not happen
return 0.0;
}
// Shadow requiring a sampler2D sampler (VSM, DPCF and PCSS)
@@ -539,9 +563,8 @@ float shadow(const bool DIRECTIONAL,
uint layer = shadowUniforms.shadows[index].layer;
// This conditional is resolved at compile time
if (frameUniforms.shadowSamplingType == SHADOW_SAMPLING_RUNTIME_EVSM) {
bool elvsm = shadowUniforms.shadows[index].elvsm;
return ShadowSample_VSM(elvsm, shadowMap, scissorNormalized, layer,
shadowPosition);
return ShadowSample_VSM(DIRECTIONAL, shadowMap, scissorNormalized, layer, index,
shadowPosition, zLight);
}
if (frameUniforms.shadowSamplingType == SHADOW_SAMPLING_RUNTIME_DPCF) {
@@ -558,8 +581,8 @@ float shadow(const bool DIRECTIONAL,
// This is here mostly for debugging at this point.
// Note: In this codepath, the normal bias is not applied because we're in the VSM variant.
// (see: get{Cascade|Spot}LightSpacePosition)
return ShadowSample_PCF(shadowMap, scissorNormalized,
layer, shadowPosition);
return ShadowSample_PCF(shadowMap, scissorNormalized, layer,
shadowPosition);
}
// should not happen
+43 -4
View File
@@ -12,12 +12,51 @@
*/
highp vec4 computeLightSpacePosition(highp vec3 p, const highp vec3 n,
const highp vec3 dir, const float b, highp_mat4 lightFromWorldMatrix) {
const highp vec3 dir, const highp vec2 b, highp_mat4 lightFromWorldMatrix) {
#if !defined(VARIANT_HAS_VSM)
highp float cosTheta = saturate(dot(n, dir));
highp float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
p += n * (sinTheta * b);
// --------------------------------------------------------------------------------------
// Anisotropic Normal Bias for Shadow Mapping
// --------------------------------------------------------------------------------------
// To prevent shadow acne, we must push the geometry along its normal to clear the
// quantization steps of the shadow map's discrete depth grid. The exact physical depth
// error we must clear is proportional to the shadow texel's world-space dimensions.
//
// This implementation computes the exact geometric projection of the rectangular
// shadow map texel onto the surface normal.
//
// 1. Coordinate Space Transition:
// We project the world-space normal onto the light's X and Y basis vectors (L_right,
// L_up). This gives us the lateral components of the normal in Light Space (n_Lx, n_Ly).
//
// 2. The Implicit sin(theta) Slope Scale:
// Because the normal is a unit vector, the magnitude of its lateral components in
// light space inherently equals sin(theta), where theta is the angle of incidence.
// This perfectly and automatically scales the bias from 0.0 (top-down, flat surface)
// to maximum (grazing angle).
//
// 3. Exact Anisotropic Footprint (The L1 Norm):
// Shadow texels are rarely perfectly square due to Cascaded Shadow Maps (CSM) or
// Light Space Perspective Shadow Maps (LiSPSM). Jx and Jy are the physical world-space
// dimensions of the texel.
// By evaluating `abs(n_Lx * Jx) + abs(n_Ly * Jy)`, we compute the exact scalar
// projection of the rectangular texel footprint.
// - It is superior to `max(Jx, Jy)` which assumes a massive square and causes Peter Panning.
// - It is superior to `length()` which assumes an ellipse and under-biases the corners.
// --------------------------------------------------------------------------------------
// Extract the first row (Light's Right vector in World Space)
highp vec3 L_right = vec3(lightFromWorldMatrix[0][0], lightFromWorldMatrix[1][0], lightFromWorldMatrix[2][0]);
// Extract the second row (Light's Up vector in World Space)
highp vec3 L_up = vec3(lightFromWorldMatrix[0][1], lightFromWorldMatrix[1][1], lightFromWorldMatrix[2][1]);
// Project the world normal onto the shadow map's 2D grid
highp float n_Lx = dot(n, L_right);
highp float n_Ly = dot(n, L_up);
// Apply the anisotropic normal bias
p += n * (abs(n_Lx * b.x) + abs(n_Ly * b.y));
#endif
return mulMat4x4Float3(lightFromWorldMatrix, p);
+2 -3
View File
@@ -5,13 +5,12 @@ struct ShadowData {
highp mat4 lightFromWorldMatrix;
highp vec4 lightFromWorldZ;
highp vec4 scissorNormalized;
mediump float texelSizeAtOneMeter;
mediump float bulbRadiusLs;
mediump float nearOverFarMinusNear;
mediump float normalBias;
highp vec2 normalBias;
bool elvsm;
mediump uint layer;
mediump uint reserved1;
mediump float vsmExponent;
mediump uint reserved2;
};
#endif
+85
View File
@@ -0,0 +1,85 @@
# Render Validation Sample & TUI
This project is an Android application for validating Filament render behavior on-device, primarily using bundled tests and goldens. It operates via `adb` intents to automate the generation, exporting, and running of test bundles.
## Automated Execution via ADB Intents
You can fully control the validation app directly from your host machine without touching the device screen. The app listens for specific intent extras when launched.
**General Command Structure:**
```bash
adb shell am start -n com.google.android.filament.validation/.MainActivity <extras>
```
### Supported Intent Extras (Booleans)
- `--ez auto_run true`: Immediately runs the loaded or default test upon startup.
- `--ez generate_goldens true`: Forces the app to generate new golden reference images for the current test instead of comparing against existing ones.
- `--ez auto_export true`: Automatically packages the generated tests/goldens into a `.zip` archive (`Default_Test_<timestamp>.zip`) to the app's files directory when finished.
- `--ez auto_export_results true`: Automatically packages the comparison results and diff images into a `.zip` archive (`results_<timestamp>.zip`) to the app's files directory when finished.
### Supported Intent Extras (Strings)
- `--es zip_path <filename.zip>`: Loads a specific test `.zip` bundle.
- If you pass an absolute path, it loads from there.
- If you pass just a filename (e.g. `Default_Test_123.zip`), it intelligently searches the app's external files directory (`/sdcard/Android/data/.../files`) to find it.
### Example ADB Workflows
**Generate new goldens and export them as a test bundle:**
```bash
adb shell am start -n com.google.android.filament.validation/.MainActivity \
--ez auto_run true \
--ez generate_goldens true \
--ez auto_export true
```
**Run an existing specific test bundle and export the results:**
```bash
adb shell am start -n com.google.android.filament.validation/.MainActivity \
--es zip_path "Default_Test_123.zip" \
--ez auto_run true \
--ez auto_export_results true
```
### Manual File Management via ADB
Depending on the Android version and device storage policy, the app's file location resides either on standard External Storage or strictly within inside its Internal Sandbox.
**App's External Storage (Default for most devices):**
- **Push a test bundle to device:** `adb push <filename.zip> /sdcard/Android/data/com.google.android.filament.validation/files/`
- **Pull a test bundle from device:** `adb pull /sdcard/Android/data/com.google.android.filament.validation/files/<filename.zip> .`
- **Pull a result bundle from device:** `adb pull /sdcard/Android/data/com.google.android.filament.validation/files/results_<timestamp>.zip .`
**App's Internal Storage (If external is unavailable):**
*Note: Due to security restrictions, you must use `run-as` to pipe data into/out of the application's secure sandbox.*
- **Push a test bundle to device:**
1. `adb push <filename.zip> /sdcard/Download/`
2. `adb shell "run-as com.google.android.filament.validation cp /sdcard/Download/<filename.zip> files/"`
- **Pull a test or result bundle from device:**
`adb shell "run-as com.google.android.filament.validation cat files/<filename.zip>" > <filename.zip>`
---
## Python Terminal UI (TUI) Dashboard
To make managing tests and results easier, a Python-based Textual TUI (`validation_app.py`) is provided in the `validation_tui` directory. It automatically polls the connected device using ADB, acts as a GUI for the intent commands above, and handles downloading/uploading `.zip` bundles to circumvent Android's scoped storage limits.
### Setup
1. Ensure you have Python 3 and `adb` installed and in your PATH.
2. Navigate to the TUI directory: `cd test/render-validation`
3. Create a virtual environment: `python3 -m venv venv`
4. Activate it: `source venv/bin/activate` (Mac/Linux) or `venv\Scripts\activate` (Windows)
5. Install requirements: `pip install -r requirements.txt` (Installs the `textual` framework)
### Usage
Start the dashboard by running:
```bash
python validation_app.py
```
### TUI Features
- **Auto-Polling Mechanism**: Syncs the file lists with your device every 2 seconds.
- **Generate Test/Result Buttons**: One-click execution of the `am start` intents.
- **Upload Local Test Bundle**: Automatically pushes a local `.zip` file from your PC to the correct directory on the Android device.
- **Per-File Actions**:
- `▶` (Load): Restarts the app with `--es zip_path <filename>` to set it as the active test on device.
- `↓` (Download): Pulls the `.zip` to your PC's current working directory.
- `✎` (Rename): Quickly renames the file directly on the Android file system.
- `✗` (Delete): Quickly removes the file from the Android device to free up storage.
+1
View File
@@ -0,0 +1 @@
textual>=0.52.0
+559
View File
@@ -0,0 +1,559 @@
import os
import sys
import shutil
import asyncio
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical, ScrollableContainer
from textual.widgets import Header, Footer, Button, Label, Static, ListItem, ListView, Input, DirectoryTree
from textual.screen import Screen, ModalScreen
from textual.reactive import reactive
from textual.message import Message
PACKAGE = "com.google.android.filament.validation"
EXTERNAL_DIR = f"/sdcard/Android/data/{PACKAGE}/files"
INTERNAL_DIR = "files"
if not shutil.which("adb"):
print("Error: 'adb' not found in PATH. Please install Android Platform Tools and ensure it's in your PATH.", file=sys.stderr)
sys.exit(1)
async def run_adb_cmd(*args):
"""Run an adb command asynchronously and return code, stdout, stderr."""
proc = await asyncio.create_subprocess_exec(
"adb", *args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
return proc.returncode, stdout.decode().strip(), stderr.decode().strip()
class DeviceSelectScreen(Screen):
def compose(self) -> ComposeResult:
yield Label("Multiple Android devices detected. Please select one:", id="title")
yield ListView(id="device_list")
async def on_mount(self) -> None:
code, out, err = await run_adb_cmd("devices")
lv = self.query_one("#device_list", ListView)
for line in out.splitlines()[1:]:
if line.strip() and "device" in line:
serial = line.split()[0]
await lv.append(ListItem(Label(serial), id=f"dev_{serial}"))
def on_list_view_selected(self, event: ListView.Selected) -> None:
serial = event.item.id.replace("dev_", "")
self.dismiss(serial)
class FileSelectScreen(ModalScreen[str]):
CSS = """
FileSelectScreen {
align: center middle;
}
#dialog {
width: 80%;
height: 80%;
padding: 1;
border: thick $background 80%;
background: $surface;
}
"""
def compose(self) -> ComposeResult:
with Vertical(id="dialog"):
yield Label("Select a local .zip test bundle to upload:")
yield DirectoryTree(os.getcwd())
with Horizontal():
yield Button("Cancel", id="btn_cancel_upload", variant="error")
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "btn_cancel_upload":
self.dismiss(None)
def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected) -> None:
if str(event.path).endswith(".zip"):
self.dismiss(str(event.path))
else:
self.app.notify("Please select a .zip file!", severity="warning")
class FileItem(Static):
class FileChanged(Message):
"""Emitted when a file is renamed or deleted."""
pass
class TestLoaded(Message):
"""Emitted when a test is loaded on device."""
def __init__(self, filename: str) -> None:
self.filename = filename
super().__init__()
def __init__(self, filename: str, filepath: str, is_internal: bool, serial: str, **kwargs):
super().__init__(**kwargs)
self.filename = filename
self.filepath = filepath
self.is_internal = is_internal
self.serial = serial
self.renaming = False
def compose(self) -> ComposeResult:
with Vertical(classes="file-item-container"):
with Horizontal(id="file_row", classes="file-item-row"):
yield Label(self.filename, id="lbl_filename", classes="file-name")
# Only show Load button for test configurations, not results
if not self.filename.startswith("results_"):
yield Button("▶", id="btn_load", variant="success", classes="compact-btn", tooltip="Load this test on device")
yield Button("↓", id="btn_download", variant="primary", classes="compact-btn", tooltip="Download to PC")
yield Button("✎", id="btn_start_rename", variant="warning", classes="compact-btn", tooltip="Rename on device")
yield Button("✗", id="btn_delete", variant="error", classes="compact-btn", tooltip="Delete on device")
with Horizontal(id="rename_row", classes="rename-row"):
yield Input(value=self.filename, id="inp_rename", classes="rename-input")
yield Button("Save", id="btn_save_rename", variant="success", classes="compact-btn")
yield Button("Cancel", id="btn_cancel_rename", classes="compact-btn")
def on_mount(self):
self.query_one("#rename_row").display = False
async def on_button_pressed(self, event: Button.Pressed) -> None:
btn_id = event.button.id
if btn_id == "btn_start_rename":
self.query_one("#file_row").display = False
self.query_one("#rename_row").display = True
inp = self.query_one("#inp_rename", Input)
inp.value = self.filename
inp.focus()
elif btn_id == "btn_cancel_rename":
self.query_one("#rename_row").display = False
self.query_one("#file_row").display = True
elif btn_id == "btn_save_rename":
new_name = self.query_one("#inp_rename", Input).value.strip()
if new_name and new_name != self.filename:
# Need to run ADB mv
base_dir = self.filepath.rsplit('/', 1)[0]
new_path = f"{base_dir}/{new_name}"
if self.is_internal:
cmd = f"run-as {PACKAGE} mv {self.filepath} {new_path}"
await run_adb_cmd("-s", self.serial, "shell", cmd)
else:
cmd = f"mv {self.filepath} {new_path}"
await run_adb_cmd("-s", self.serial, "shell", cmd)
self.app.notify(f"Renamed {self.filename} to {new_name}")
self.post_message(self.FileChanged())
else:
self.query_one("#rename_row").display = False
self.query_one("#file_row").display = True
elif btn_id == "btn_delete":
event.button.disabled = True
if self.is_internal:
cmd = f"run-as {PACKAGE} rm {self.filepath}"
await run_adb_cmd("-s", self.serial, "shell", cmd)
else:
cmd = f"rm {self.filepath}"
await run_adb_cmd("-s", self.serial, "shell", cmd)
self.app.notify(f"Deleted {self.filename}")
self.post_message(self.FileChanged())
elif btn_id == "btn_download":
event.button.disabled = True
event.button.label = "..."
self.app.notify(f"Downloading {self.filename} to {os.getcwd()}...", title="Download Started")
self.run_worker(self.download_file(event.button), exclusive=True)
elif btn_id == "btn_load":
self.app.notify(f"Loading {self.filename} on device...", title="Load Test")
self.run_worker(self.load_on_device(), exclusive=True)
async def load_on_device(self) -> None:
cmd_args = [
"-s", self.serial, "shell", "am", "start",
"-n", f"{PACKAGE}/.MainActivity",
"--es", "zip_path", self.filename
]
await run_adb_cmd(*cmd_args)
self.post_message(self.TestLoaded(self.filename))
self.app.notify(f"Requested device to load {self.filename}", title="Load Complete")
async def download_file(self, button: Button) -> None:
dest = os.path.join(os.getcwd(), self.filename)
if self.is_internal:
# internal requires run-as
cmd = f"adb -s {self.serial} shell \"run-as {PACKAGE} cat {self.filepath}\" > \"{dest}\""
proc = await asyncio.create_subprocess_shell(cmd)
await proc.communicate()
else:
# external can be a direct pull
await run_adb_cmd("-s", self.serial, "pull", self.filepath, dest)
button.label = "Downloaded ✓"
button.variant = "success"
self.app.notify(f"Saved: {dest}", title="Complete")
await asyncio.sleep(2)
button.label = "Download"
button.disabled = False
button.variant = "primary"
class MainScreen(Screen):
device_serial = reactive("")
is_connected = reactive(True)
current_test = reactive("")
is_foreground = reactive(False)
def __init__(self, serial=None, **kwargs):
super().__init__(**kwargs)
self.device_serial = serial or ""
self.tests_seen = set()
self.results_seen = set()
def compose(self) -> ComposeResult:
yield Header()
yield Label(id="banner", classes="banner")
with Vertical(id="launch_container"):
yield Label("App is not running in the foreground.", id="lbl_launch_msg")
yield Button("Launch App", id="btn_launch_app", variant="success", classes="main-action-btn launch-btn")
with Horizontal(id="main_container"):
with Vertical(classes="column"):
yield Label("🧪 Tests", classes="col-title")
yield Button("Upload Local Test Bundle", id="btn_upload_test", variant="primary", classes="main-action-btn")
yield Button("Generate Goldens", id="btn_gen_goldens", variant="warning", classes="main-action-btn")
yield Button("Generate Test Bundle", id="btn_gen_test", variant="success", classes="main-action-btn")
yield ScrollableContainer(id="test_list")
with Vertical(classes="column"):
yield Label("📊 Results", classes="col-title")
yield Label("Current: Default", id="lbl_current_test", classes="col-subtitle")
yield Button("Run test", id="btn_gen_res", variant="success", classes="main-action-btn")
yield ScrollableContainer(id="result_list")
yield Footer()
def on_file_item_test_loaded(self, event: FileItem.TestLoaded) -> None:
self.current_test = event.filename
def watch_current_test(self, new_test: str) -> None:
from textual.css.query import NoMatches
try:
if new_test:
self.query_one("#lbl_current_test", Label).update(f"Current: {new_test}")
except NoMatches:
pass
def watch_is_foreground(self, foreground: bool) -> None:
from textual.css.query import NoMatches
try:
lc = self.query_one("#launch_container")
mc = self.query_one("#main_container")
if foreground:
lc.display = False
mc.display = True
else:
lc.display = True
mc.display = False
except NoMatches:
pass
def on_file_item_file_changed(self, event: FileItem.FileChanged) -> None:
"""Called when a child file is deleted or renamed to force a full refresh."""
self.query_one("#test_list").remove_children()
self.query_one("#result_list").remove_children()
self.tests_seen.clear()
self.results_seen.clear()
def watch_device_serial(self, serial: str) -> None:
from textual.css.query import NoMatches
try:
self.query_one(Header).title = "Filament Validation Runner"
self.query_one(Header).sub_title = f"Device: {serial}"
except NoMatches:
pass
def watch_is_connected(self, connected: bool) -> None:
from textual.css.query import NoMatches
try:
banner = self.query_one("#banner", Label)
if connected:
banner.update("Status: Device Connected ✅")
banner.remove_class("disconnected")
else:
banner.update("Status: Disconnected - Retrying... ❌")
banner.add_class("disconnected")
except NoMatches:
pass
def on_mount(self) -> None:
# Check ADB constantly every 2 seconds
self.set_interval(2.0, self.poll_adb)
# Hide conditionally
self.query_one("#launch_container").display = False
self.query_one("#main_container").display = False
async def get_files(self, directory: str, is_internal: bool):
if is_internal:
code, out, err = await run_adb_cmd("-s", self.device_serial, "shell", f"run-as {PACKAGE} ls -1 {directory}")
else:
code, out, err = await run_adb_cmd("-s", self.device_serial, "shell", f"ls -1 {directory}")
if code == 0 and "Permission denied" not in err and "No such file or directory" not in err:
return [f.strip() for f in out.splitlines() if f.strip() and f.strip().endswith(".zip")]
return []
async def poll_adb(self) -> None:
code, out, err = await run_adb_cmd("-s", self.device_serial, "shell", "echo ok")
if code != 0 or "ok" not in out:
self.is_connected = False
self.is_foreground = False
return
self.is_connected = True
# Check foreground
proc = await asyncio.create_subprocess_shell(
f"adb -s {self.device_serial} shell \"dumpsys window | grep mCurrentFocus\"",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, _ = await proc.communicate()
out = stdout.decode('utf-8', errors='ignore')
self.is_foreground = PACKAGE in out
# We only really need to pull files if we are in the foreground
if not self.is_foreground:
return
current_tests = set()
current_results = set()
files_dict = {}
# 1. External
ext_files = await self.get_files(EXTERNAL_DIR, False)
for f in ext_files:
files_dict[f] = (f"{EXTERNAL_DIR}/{f}", False)
# 2. Internal
int_files = await self.get_files(INTERNAL_DIR, True)
for f in int_files:
if f not in files_dict:
files_dict[f] = (f"{INTERNAL_DIR}/{f}", True)
for f, (path, is_int) in files_dict.items():
if f.startswith("results_"):
current_results.add((f, path, is_int))
else:
# Any other .zip file is considered a test
current_tests.add((f, path, is_int))
# Check if any files were removed (e.g. wiped data)
removed_tests = self.tests_seen - current_tests
removed_results = self.results_seen - current_results
if removed_tests or removed_results:
self.query_one("#test_list").remove_children()
self.query_one("#result_list").remove_children()
self.tests_seen.clear()
self.results_seen.clear()
# Update lists safely if they are new
new_tests = current_tests - self.tests_seen
test_list = self.query_one("#test_list", ScrollableContainer)
for f, path, is_int in new_tests:
await test_list.mount(FileItem(f, path, is_int, self.device_serial))
test_list.scroll_end(animate=False)
self.tests_seen.update(new_tests)
new_results = current_results - self.results_seen
result_list = self.query_one("#result_list", ScrollableContainer)
for f, path, is_int in new_results:
await result_list.mount(FileItem(f, path, is_int, self.device_serial))
result_list.scroll_end(animate=False)
self.results_seen.update(new_results)
async def on_button_pressed(self, event: Button.Pressed) -> None:
btn_id = event.button.id
if btn_id == "btn_gen_goldens":
event.button.disabled = True
event.button.label = "Generating Goldens..."
self.run_worker(self.generate_and_auto_export(event.button, "golden"), exclusive=True)
elif btn_id == "btn_gen_test":
event.button.disabled = True
event.button.label = "Generating & Exporting..."
self.run_worker(self.generate_and_auto_export(event.button, "test"), exclusive=True)
elif btn_id == "btn_gen_res":
event.button.disabled = True
event.button.label = "Generating & Exporting..."
self.run_worker(self.generate_and_auto_export(event.button, "result"), exclusive=True)
elif btn_id == "btn_upload_test":
self.app.push_screen(FileSelectScreen(), self.handle_upload)
elif btn_id == "btn_launch_app":
self.run_worker(self.launch_app(), exclusive=True)
async def launch_app(self) -> None:
self.app.notify("Commanding device to start the app...", title="Launching")
await run_adb_cmd("-s", self.device_serial, "shell", "am", "start", "-n", f"{PACKAGE}/.MainActivity")
def handle_upload(self, file_path: str | None) -> None:
if file_path:
self.run_worker(self.upload_file(file_path), exclusive=True)
async def upload_file(self, file_path: str):
filename = os.path.basename(file_path)
self.app.notify(f"Uploading {filename}...", title="Upload Started")
# We push to EXTERNAL_DIR because `adb push` works seamlessly there
dest = f"{EXTERNAL_DIR}/{filename}"
code, out, err = await run_adb_cmd("-s", self.device_serial, "push", file_path, dest)
if code == 0:
self.app.notify(f"Successfully uploaded to device.", title="Upload Finished")
# Force a refresh
self.tests_seen.clear()
self.query_one("#test_list").remove_children()
else:
self.app.notify(f"Upload failed: {err}", title="Error", severity="error")
async def generate_and_auto_export(self, button: Button, mode: str):
self.app.notify("Commanding device...", title="Working")
# Notice we removed '-S' so it doesn't force stop the activity first
cmd_args = [
"-s", self.device_serial, "shell", "am", "start",
"-n", f"{PACKAGE}/.MainActivity"
]
if mode == "golden":
cmd_args.extend(["--ez", "generate_goldens", "true", "--ez", "auto_run", "true"])
elif mode == "test":
cmd_args.extend(["--ez", "auto_run", "true", "--ez", "auto_export", "true"])
elif mode == "result":
cmd_args.extend(["--ez", "auto_run", "true", "--ez", "auto_export_results", "true"])
if self.current_test:
cmd_args.extend(["--es", "zip_path", self.current_test])
await run_adb_cmd(*cmd_args)
# Allow time to run and let auto-polling grab the new file (if exporting)
await asyncio.sleep(6)
button.disabled = False
if mode == "golden":
button.label = "Generate Goldens"
elif mode == "test":
button.label = "Generate & Download New Test"
elif mode == "result":
button.label = "Generate & Download Results"
self.app.notify("Action Finished")
class ValidationApp(App):
CSS = """
Screen {
layout: vertical;
}
.banner {
width: 100%;
content-align: center middle;
background: $success;
color: $text;
padding: 1;
text-style: bold;
}
.banner.disconnected {
background: $error;
}
#launch_container {
align: center middle;
height: 1fr;
}
#lbl_launch_msg {
text-align: center;
margin-bottom: 2;
}
.launch-btn {
width: 30;
}
#main_container {
height: 1fr;
layout: horizontal;
}
.column {
width: 50%;
height: 100%;
border: solid $accent;
padding: 1;
}
.col-title {
text-align: center;
text-style: bold;
width: 100%;
margin-bottom: 1;
}
.col-subtitle {
text-align: center;
width: 100%;
margin-bottom: 1;
color: $secondary;
text-style: italic;
}
.main-action-btn {
width: 100%;
margin-bottom: 1;
}
.file-item-container {
height: auto;
padding-bottom: 1;
border-bottom: solid $surface;
}
.file-item-row {
height: 3;
width: 100%;
align: left middle;
}
.file-name {
width: 1fr;
content-align: left middle;
}
.compact-btn {
min-width: 4;
width: auto;
margin-left: 1;
}
.rename-row {
height: 3;
width: 100%;
}
.rename-input {
width: 1fr;
}
"""
BINDINGS = [
("q", "quit", "Quit application")
]
async def on_mount(self) -> None:
code, out, err = await run_adb_cmd("devices")
devices = []
for line in out.splitlines()[1:]:
if line.strip() and "device" in line:
devices.append(line.split()[0])
if len(devices) > 1:
self.push_screen(DeviceSelectScreen(), self.start_main)
elif len(devices) == 1:
self.start_main(devices[0])
else:
self.notify("No devices connected via ADB!", severity="error")
self.start_main("")
def start_main(self, serial: str) -> None:
self.push_screen(MainScreen(serial=serial))
if __name__ == "__main__":
app = ValidationApp()
app.run()
+2 -1
View File
@@ -76,10 +76,11 @@ into **branch** of `filament-assets`. This branch is paired with a PR or commit
As an example, imagine I am working on a PR, and I've uploaded my change, which is in a
branch called `my-pr-branch`, to `filament`. This PR requires updating the golden. We would do
it in the following fashion
it in the following fashion on a macOS machine:
### Using a script to update the golden repo
- Make sure you've completed the steps in 'Setting up python'
- Run interactive mode in the `update_golden.py` script.
```
python3 test/renderdiff/src/update_golden.py
+9 -7
View File
@@ -16,6 +16,7 @@ import os
import shutil
import re
import sys
import tempfile
from utils import execute, ArgParseImpl, mkdir_p
@@ -99,10 +100,11 @@ class GoldenManager:
code, old_commit = execute(f'git log --format=%B -n 1', cwd=assets_dir)
if tag and len(tag) > 0:
old_commit += f'\nFILAMENT={tag}'
COMMIT_FILE = '/tmp/golden_commit.txt'
with open(COMMIT_FILE, 'w') as f:
with tempfile.NamedTemporaryFile('w', delete=False) as f:
f.write(old_commit)
self._git_exec(f'commit --amend -F {COMMIT_FILE}')
commit_file = f.name
self._git_exec(f'commit --amend -F {commit_file}')
os.remove(commit_file)
# Do the actual merge
self._git_exec(f'checkout main')
@@ -139,11 +141,11 @@ class GoldenManager:
os.path.join(rdiff_dir, f))
self._git_exec(f'add {os.path.join(GOLDENS_DIR, f)}')
TMP_GOLDEN_COMMIT_FILE = '/tmp/golden_commit.txt'
with open(TMP_GOLDEN_COMMIT_FILE, 'w') as f:
with tempfile.NamedTemporaryFile('w', delete=False) as f:
f.write(commit_msg)
self._git_exec(f'commit -a -F {TMP_GOLDEN_COMMIT_FILE}')
tmp_golden_commit_file = f.name
self._git_exec(f'commit -a -F {tmp_golden_commit_file}')
os.remove(tmp_golden_commit_file)
if push_to_remote and \
(self.access_token_ or self.access_type_ == ACCESS_TYPE_SSH):
self._git_exec(f'push -f origin {branch}')
+19 -19
View File
@@ -64,19 +64,19 @@ def _do_update(golden_manager, config):
def _same_image_diffimg(diffimg_path, img1, img2):
cmd = [diffimg_path, img1, img2]
try:
result = subprocess.run(cmd, capture_output=True, text=True)
output = result.stdout.strip()
if not output:
return False
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"diffimg failed with return code {result.returncode}:\n{result.stderr}")
try:
res_json = json.loads(output)
return res_json.get('passed', False)
except json.JSONDecodeError:
return False
except Exception:
return False
output = result.stdout.strip()
if not output:
raise RuntimeError("diffimg produced no output")
try:
res_json = json.loads(output)
return res_json.get('passed', False)
except json.JSONDecodeError as e:
raise RuntimeError(f"Failed to parse diffimg output: {e}\nOutput: {output}")
def _get_deletes_updates(update_dir, golden_dir, diffimg_path):
ret_delete = []
@@ -93,13 +93,13 @@ def _get_deletes_updates(update_dir, golden_dir, diffimg_path):
# However, update_golden typically only updates/adds based on the new render set.
# But strict sync might imply deleting missing ones.
# The original logic was: delete = list(base - new).
delete = list(base - new)
delete_files = list(base_files - new_files)
# Files in new but not in base are definitely updates (additions)
update = list(new - base)
update_files = list(new_files - base_files)
# Files in both need comparison
for fpath in base.intersection(new_files):
for fpath in base_files.intersection(new_files):
base_fpath = os.path.join(golden_dir, fpath)
new_fpath = os.path.join(update_dir, fpath)
@@ -110,10 +110,10 @@ def _get_deletes_updates(update_dir, golden_dir, diffimg_path):
is_different = _file_as_str(new_fpath) != _file_as_str(base_fpath)
if is_different:
update.append(fpath)
ret_update.append(fpath)
ret_update += update
ret_delete += delete
ret_update += update_files
ret_delete += delete_files
return ret_delete, ret_update
@@ -179,7 +179,7 @@ if __name__ == "__main__":
parser = ArgParseImpl()
parser.add_argument('--branch', help='Branch of the golden repo to write to')
parser.add_argument('--golden-repo-token', help='Access token for the golden repo')
parser.add_argument('--push-to-remote', action="store_true", help='Access token for the golden repo')
parser.add_argument('--push-to-remote', action="store_true", help='Push the golden repo changes to remote')
parser.add_argument('--diffimg', help='Path to the diffimg tool',
default='./out/cmake-release/tools/diffimg/diffimg')
+4 -3
View File
@@ -18,6 +18,7 @@ import shutil
import argparse
import sys
import pathlib
import shlex
def execute(cmd,
cwd=None,
@@ -25,7 +26,7 @@ def execute(cmd,
stdin=None,
env=None,
raise_errors=False):
in_env = os.environ
in_env = os.environ.copy()
in_env.update(env if env else {})
home = os.environ['HOME']
if f'{home}/bin' not in in_env['PATH']:
@@ -45,11 +46,11 @@ def execute(cmd,
'universal_newlines': True
}
if capture_output:
process = subprocess.Popen(cmd.split(' '), **kwargs)
process = subprocess.Popen(shlex.split(cmd), **kwargs)
output, err_output = process.communicate()
return_code = process.returncode
else:
return_code = subprocess.call(cmd.split(' '), **kwargs)
return_code = subprocess.call(shlex.split(cmd), **kwargs)
if return_code:
# Error

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