Compare commits

..
112 changed files with 1222 additions and 3603 deletions
+15 -17
View File
@@ -27,24 +27,17 @@ runs:
echo "$HASH" > /tmp/commit_hash.txt
- name: Find commit message (PR)
shell: bash
id: checkout_code
if: github.event_name == 'pull_request'
run: |
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
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
- shell: bash
id: action_output
run: |
@@ -54,4 +47,9 @@ 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"
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 }}
@@ -1,26 +0,0 @@
name: 'Renderdiff Generate'
description: 'Sets up prerequisites and runs the generate script for renderdiff'
runs:
using: "composite"
steps:
- uses: ./.github/actions/mac-prereq
- uses: ./.github/actions/get-gltf-assets
- uses: ./.github/actions/get-mesa
- uses: ./.github/actions/get-vulkan-sdk
- name: Prerequisites
run: |
# Must have at least clang-16 for a webgpu/dawn build.
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
shell: bash
- name: Generate images
run: |
TEST_DIR=test/renderdiff
source ${TEST_DIR}/src/preamble.sh
set -eux
bash ${TEST_DIR}/generate.sh
set +eux
shell: bash
- name: Build diffimg tool
run: |
./build.sh release diffimg
shell: bash
+7 -32
View File
@@ -10,27 +10,16 @@ jobs:
# a branch on filament-assets.
update-renderdiff-goldens:
name: update-renderdiff-goldens
runs-on: 'macos-14-xlarge'
runs-on: 'ubuntu-24.04-4core'
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- name: Check if accepting new goldens
id: check_accept
env:
COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }}
run: |
if echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py --mode=accept_new_goldens; then
echo "accept=true" >> "$GITHUB_OUTPUT"
else
echo "accept=false" >> "$GITHUB_OUTPUT"
fi
shell: bash
- name: Renderdiff Generate for new goldens
if: steps.check_accept.outputs.accept == 'true'
uses: ./.github/actions/renderdiff-generate
- name: Build diffimg
run: ./build.sh release diffimg
- name: Run update script
env:
GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }}
@@ -38,24 +27,10 @@ jobs:
run: |
GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py)
COMMIT_HASH="${{ steps.get_commit_msg.outputs.hash }}"
git config --global user.email "filament.bot@gmail.com"
git config --global user.name "Filament Bot"
git config --global credential.helper cache
if [[ "${{ steps.check_accept.outputs.accept }}" == "true" ]]; then
SHORT_HASH="${COMMIT_HASH:0:8}"
GOLDEN_BRANCH="accept-goldens-${SHORT_HASH}"
echo "Generating new goldens for branch ${GOLDEN_BRANCH}"
python3 test/renderdiff/src/update_golden.py \
--branch=${GOLDEN_BRANCH} \
--source=$(pwd)/out/renderdiff/renders \
--commit-msg="Auto-update goldens from ${COMMIT_HASH}" \
--push-to-remote \
--golden-repo-token=${GH_TOKEN}
fi
if [[ "${GOLDEN_BRANCH}" != "main" ]]; then
git config --global user.email "filament.bot@gmail.com"
git config --global user.name "Filament Bot"
git config --global credential.helper cache
echo "branch==${GOLDEN_BRANCH}"
echo "hash==${COMMIT_HASH}"
python3 test/renderdiff/src/update_golden.py --branch=${GOLDEN_BRANCH} \
+10 -15
View File
@@ -126,24 +126,18 @@ jobs:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/mac-prereq
- uses: ./.github/actions/get-gltf-assets
- uses: ./.github/actions/get-mesa
- uses: ./.github/actions/get-vulkan-sdk
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- name: Check if accepting new goldens
id: check_accept
env:
COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }}
- name: Prerequisites
run: |
if echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py --mode=accept_new_goldens; then
echo "accept=true" >> "$GITHUB_OUTPUT"
else
echo "accept=false" >> "$GITHUB_OUTPUT"
fi
# Must have at least clang-16 for a webgpu/dawn build.
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
shell: bash
- name: Renderdiff Generate
if: steps.check_accept.outputs.accept != 'true'
uses: ./.github/actions/renderdiff-generate
- name: Render and compare
if: steps.check_accept.outputs.accept != 'true'
id: render_compare
env:
COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }}
@@ -152,6 +146,9 @@ jobs:
source ${TEST_DIR}/src/preamble.sh
set -eux
GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 ${TEST_DIR}/src/commit_msg.py)
bash ${TEST_DIR}/generate.sh
# Build diffimg tool
./build.sh release diffimg
python3 ${TEST_DIR}/src/golden_manager.py \
--branch=${GOLDEN_BRANCH} \
@@ -175,12 +172,10 @@ jobs:
fi
shell: bash
- uses: actions/upload-artifact@v4
if: steps.check_accept.outputs.accept != 'true'
with:
name: presubmit-renderdiff-result
path: ./out/renderdiff
- name: Compare result
if: steps.check_accept.outputs.accept != 'true'
run: |
ERROR_STR="${{ steps.render_compare.outputs.err }}"
if [ -n "${ERROR_STR}" ]; then
-48
View File
@@ -188,54 +188,6 @@ 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: fix crash when using variance shadow maps
+2 -2
View File
@@ -31,7 +31,7 @@ repositories {
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.69.5'
implementation 'com.google.android.filament:filament-android:1.69.3'
}
```
@@ -50,7 +50,7 @@ Here are all the libraries available in the group `com.google.android.filament`:
iOS projects can use CocoaPods to install the latest release:
```shell
pod 'Filament', '~> 1.69.5'
pod 'Filament', '~> 1.69.3'
```
## Documentation
-9
View File
@@ -7,15 +7,6 @@ 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
## v1.69.4
+1 -7
View File
@@ -40,21 +40,15 @@
// - Build and upload artifacts with ./gradlew publish
// - Close and release staging repo on Nexus with ./gradlew closeAndReleaseStagingRepository
//
// 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):
// The following is needed in ~/gradle/gradle.properties:
//
// 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
@@ -2121,7 +2121,7 @@ public class View {
*/
public boolean highPrecision = false;
/**
* @deprecated has no effect.
* VSM minimum variance scale, must be positive.
*/
public float minVarianceScale = 0.5f;
/**
+1 -1
View File
@@ -1,5 +1,5 @@
GROUP=com.google.android.filament
VERSION_NAME=1.69.5
VERSION_NAME=1.69.3
POM_DESCRIPTION=Real-time physically based rendering engine for Android.
-7
View File
@@ -94,13 +94,6 @@ 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,8 +26,7 @@ tasks.register('copyDamagedHelmetGltf', Copy) {
preBuild.dependsOn copyDamagedHelmetGltf
clean.doFirst {
delete "src/main/assets/envs"
delete "src/main/assets/models"
delete "src/main/assets"
}
android {
@@ -49,7 +48,6 @@ 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,11 +12,10 @@
android:supportsRtl="true"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/Theme.Material3.DayNight.NoActionBar">
android:theme="@android:style/Theme.NoTitleBar">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:screenOrientation="fullSensor">
<intent-filter>
@@ -1,899 +0,0 @@
{
"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,35 +17,36 @@
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.Spinner
import android.widget.ScrollView
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 java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.Spinner
import android.widget.AdapterView
class MainActivity : Activity(), ValidationRunner.Callback {
@@ -61,17 +62,13 @@ 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
// UI Elements
private lateinit var modeSpinner: Spinner
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
@@ -92,53 +89,25 @@ class MainActivity : Activity(), ValidationRunner.Callback {
surfaceView.holder.setFixedSize(512, 512)
statusTextView = findViewById(R.id.status_text)
testResultsHeader = findViewById(R.id.test_results_header)
modeSpinner = findViewById(R.id.mode_spinner)
runButton = findViewById(R.id.run_button)
resultsContainer = findViewById(R.id.results_container)
runButton = findViewById(R.id.run_button)
loadButton = findViewById(R.id.load_button)
optionsButton = findViewById(R.id.options_button)
// Setup Spinner
val modes = arrayOf("Run Validation", "Generate Goldens")
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, modes)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
modeSpinner.adapter = adapter
// Setup Run Button
runButton.setOnClickListener {
currentInput?.let { input ->
// Always use the generateGoldens flag from the intent/input
startValidation(input)
val generateGoldens = modeSpinner.selectedItemPosition == 1
val newInput = input.copy(generateGoldens = generateGoldens)
startValidation(newInput)
}
}
// 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()
@@ -151,134 +120,6 @@ 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
@@ -315,18 +156,12 @@ 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
if (input.autoRun) {
startValidation(input)
} else {
// Just show status
statusTextView.text = "Ready: ${input.config.name}"
}
// Sync spinner with intent
modeSpinner.setSelection(if (input.generateGoldens) 1 else 0)
startValidation(input)
} catch (e: Exception) {
Log.e(TAG, "Failed to resolve config", e)
statusTextView.text = "Error: ${e.message}"
@@ -340,8 +175,6 @@ 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!!)
@@ -349,6 +182,9 @@ 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}"
@@ -360,12 +196,6 @@ 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)
@@ -391,30 +221,13 @@ class MainActivity : Activity(), ValidationRunner.Callback {
resultContainer.orientation = LinearLayout.VERTICAL
resultContainer.setPadding(0, 10, 0, 20)
// 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)
// Header
val resultView = TextView(this)
resultView.text = "${result.testName}: ${if(result.passed) "PASS" else "FAIL"} (Diff: ${result.diffMetric})"
resultView.setTextColor(if(result.passed) Color.GREEN else Color.RED)
resultView.textSize = 16f
resultView.setTypeface(null, android.graphics.Typeface.BOLD)
resultContainer.addView(resultView)
// Images Row
val imagesRow = LinearLayout(this)
@@ -428,7 +241,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
val labelView = TextView(this)
labelView.text = label
labelView.textSize = 9f
labelView.textSize = 12f
container.addView(labelView)
val iv = ImageView(this)
@@ -461,46 +274,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onAllTestsFinished() {
runOnUiThread {
statusTextView.text = "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")
}
Log.i(TAG, "All tests finished")
}
}
@@ -527,3 +301,50 @@ 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,7 +26,6 @@ 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.
@@ -43,11 +42,7 @@ class ValidationInputManager(private val context: Context) {
data class ValidationInput(
val config: RenderTestConfig,
val outputDir: File,
val generateGoldens: Boolean,
val autoRun: Boolean = false,
val autoExport: Boolean = false,
val autoExportResults: Boolean = false,
val sourceZip: File? = null
val generateGoldens: Boolean
)
/**
@@ -58,111 +53,22 @@ 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")
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
}
val outputDir = if (outputPath != null) {
File(outputPath).apply { mkdirs() }
} else {
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
File(context.getExternalFilesDir(null), "validation_results").apply { mkdirs() }
}
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, 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)
return@withContext ValidationInput(config, outputDir, generateGoldens)
}
private suspend fun extractDefaultAssets(): RenderTestConfig {
@@ -184,9 +90,9 @@ class ValidationInputManager(private val context: Context) {
// Copy DamagedHelmet.glb
val modelsDir = File(filesDir, "models")
modelsDir.mkdirs()
val modelOut = File(modelsDir, "helmet.glb")
val modelOut = File(modelsDir, "DamagedHelmet.glb")
assetManager.open("models/helmet.glb").use { input ->
assetManager.open("DamagedHelmet.glb").use { input ->
FileOutputStream(modelOut).use { output ->
input.copyTo(output)
}
@@ -67,201 +67,22 @@ 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 ->
// 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) }
outputDir.walkTopDown().filter { it.isFile && it.name != "results.zip" }.forEach { file ->
val entryName = file.relativeTo(outputDir).path
zos.putNextEntry(ZipEntry(entryName))
file.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
Log.i(TAG, "Exported results to ${zipFile.absolutePath}")
Log.i(TAG, "Zipped results to ${zipFile.absolutePath}")
return zipFile
} catch (e: Exception) {
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()
Log.e(TAG, "Failed to zip results", e)
return null
}
}
@@ -41,13 +41,18 @@ 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
RUNNING_TEST,
COMPARING
}
interface Callback {
@@ -88,6 +93,8 @@ class ValidationRunner(
nextModel()
return
}
currentState = State.LOADING_MODEL
callback?.onStatusChanged("Loading $modelName for ${currentTestConfig?.name}")
// Load model on main thread (required by ModelViewer)
@@ -103,24 +110,51 @@ class ValidationRunner(
Log.i("ValidationRunner", "Loading GLB buffer... (${bytes.size} bytes)")
val buffer = ByteBuffer.wrap(bytes)
modelViewer.loadModelGlb(buffer)
Log.i("ValidationRunner", "Model loaded.")
Log.i("ValidationRunner", "Model loaded. initializing fence.")
modelViewer.transformToUnitCube()
currentState = State.WAITING_FOR_RESOURCES
frameCounter = 0
Log.i("ValidationRunner", "State set to WAITING_FOR_RESOURCES")
loadStartFence = modelViewer.engine.createFence()
loadStartTime = System.nanoTime()
currentState = State.WAITING_FOR_FENCE
frameCounter = 0 // Reset for fence timeout tracking
Log.i("ValidationRunner", "State set to WAITING_FOR_FENCE")
} catch (e: Exception) {
Log.e("ValidationRunner", "Failed to load $path", e)
nextModel()
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) {
@@ -132,12 +166,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
@@ -152,11 +186,13 @@ class ValidationRunner(
if (engine.shouldClose()) {
Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)")
// Test finished (for this spec)
currentState = State.IDLE
currentState = State.COMPARING
captureAndCompare()
}
}
}
State.COMPARING -> {} // Busy
State.LOADING_MODEL -> {}
}
}
@@ -197,36 +233,7 @@ class ValidationRunner(
// Golden path
val modelFile = File(config.models.get(modelName)!!)
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")
}
val goldenFile = modelFile.parentFile!!.parentFile!!.resolve("golden/${testFullName}.png")
Thread {
try {
@@ -238,15 +245,14 @@ class ValidationRunner(
var diffMetric = 0f
if (generateGoldens) {
val targetGolden = goldenFile ?: modelParent.parentFile?.resolve("golden/${testFullName}.png") ?: File(modelParent, "golden/${testFullName}.png")
targetGolden.parentFile?.mkdirs()
FileOutputStream(targetGolden).use { out ->
goldenFile.parentFile?.mkdirs()
FileOutputStream(goldenFile).use { out ->
flipped.compress(Bitmap.CompressFormat.PNG, 100, out)
}
passed = true // Generating goldens always passes if successful
callback?.onStatusChanged("Golden generated")
} else {
if (goldenFile != null && goldenFile.exists()) {
if (goldenFile.exists()) {
val golden = android.graphics.BitmapFactory.decodeFile(goldenFile.absolutePath)
if (golden != null) {
callback?.onImageResult("Golden", golden)
@@ -267,7 +273,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,8 +10,6 @@
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">
@@ -22,83 +20,10 @@
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/controls_container"
app:layout_constraintTop_toBottomOf="@id/surface_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
@@ -107,9 +32,41 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingBottom="20dp">
android:padding="20dp">
<TextView
android:id="@+id/status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Initializing..."
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<Spinner
android:id="@+id/mode_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/run_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Run" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Test Results"
android:textSize="18sp"
android:paddingTop="20dp"
android:paddingBottom="10dp" />
<LinearLayout
android:id="@+id/results_container"
@@ -117,6 +74,7 @@
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</ScrollView>
+6 -6
View File
@@ -235,9 +235,9 @@
"license": "MIT"
},
"node_modules/minimatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -429,9 +429,9 @@
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="
},
"minimatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
+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.5'
implementation 'com.google.android.filament:filament-android:1.69.3'
}
</code></pre>
<p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p>
@@ -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.5'
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.69.3'
</code></pre>
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
<ul>
+1 -23
View File
@@ -225,10 +225,9 @@ 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 on a macOS machine:</p>
it in the following fashion</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>
@@ -264,27 +263,6 @@ branch of the golden repo (i.e. <code>my-pr-branch-golden</code>).</li>
<li>If the PR is merged, then there is another workflow that will merge <code>my-pr-branch-golden</code>
to the <code>main</code> branch of the golden repo.</li>
</ul>
<h3 id="automated-update-via-commit-message"><a class="header" href="#automated-update-via-commit-message">Automated update via commit message</a></h3>
<p>Alternatively, if you are confident in your changes and want the CI to handle the update
for you, you can use the following tag in your commit message:</p>
<ul>
<li>In the commit message of your working branch on <code>filament</code>, add the following line:
<pre><code>RDIFF_ACCEPT_NEW_GOLDENS
</code></pre>
</li>
</ul>
<p>This has the following effects:</p>
<ul>
<li>The presubmit test <code>test-renderdiff</code> will be bypassed (it will not perform rendering or
comparison).</li>
<li>When the PR is merged, the postsubmit CI will automatically:
<ol>
<li>Build Filament and generate the new images.</li>
<li>Upload them to a temporary branch in <code>filament-assets</code>.</li>
<li>Merge that branch into <code>main</code>.</li>
</ol>
</li>
</ul>
<h2 id="viewing-test-results"><a class="header" href="#viewing-test-results">Viewing test results</a></h2>
<p>We provide a viewer for looking at the result of a test run. The viewer is a webapp that can
be used by pointing your browser to a localhost port. If you input the viewer with a PR or a
+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
+153 -153
View File
@@ -564,170 +564,170 @@ endif()
# ==================================================================================================
# Test
# ==================================================================================================
if (FILAMENT_BUILD_TESTING)
option(INSTALL_BACKEND_TEST "Install the backend test library so it can be consumed on iOS" OFF)
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 (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 (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
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
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)
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)
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_COMBINED_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libbackendtest_combined.a")
combine_static_libs(backend_test "${BACKEND_TEST_COMBINED_OUTPUT}" "${BACKEND_TEST_DEPS}")
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)
set(BACKEND_TEST_LIB_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}backend_test${CMAKE_STATIC_LIBRARY_SUFFIX})
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()
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()
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
set_target_properties(backend_test PROPERTIES FOLDER Tests)
if (APPLE AND NOT IOS)
add_executable(metal_utils_test test/MetalTest.mm)
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)
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)
# 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()
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()
@@ -27,6 +27,8 @@
#include <stddef.h>
#include <stdint.h>
#include <functional>
namespace filament::backend {
/*
@@ -64,7 +66,7 @@ public:
// all commands buffers (Slices) written to this point are returned by waitForCommand(). This
// call blocks until the CircularBuffer has at least mRequiredSize bytes available.
void flush();
void flush(std::function<void(void*, void*)> const& debugPrintHistogram = nullptr);
// returns from waitForCommands() immediately.
void requestExit();
@@ -56,6 +56,7 @@ namespace filament::backend {
class CommandBase {
static constexpr size_t FILAMENT_OBJECT_ALIGNMENT = alignof(std::max_align_t);
friend class CommandStream;
protected:
using Execute = Dispatcher::Execute;
@@ -168,8 +169,8 @@ struct CommandType<void (Driver::*)(ARGS...)> {
class CustomCommand : public CommandBase {
std::function<void()> mCommand;
static void execute(Driver&, CommandBase* base, intptr_t* next);
public:
static void execute(Driver&, CommandBase* base, intptr_t* next);
CustomCommand(CustomCommand&& rhs) = default;
explicit CustomCommand(std::function<void()> cmd)
@@ -179,11 +180,12 @@ public:
// ------------------------------------------------------------------------------------------------
class NoopCommand : public CommandBase {
public:
intptr_t mNext;
static void execute(Driver&, CommandBase* self, intptr_t* next) noexcept {
*next = static_cast<NoopCommand*>(self)->mNext;
}
public:
constexpr explicit NoopCommand(void* next) noexcept
: CommandBase(execute), mNext(intptr_t((char *)next - (char *)this)) { }
};
@@ -219,6 +221,36 @@ public:
CircularBuffer const& getCircularBuffer() const noexcept { return mCurrentBuffer; }
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
using Execute = Dispatcher::Execute;
struct CommandInfo {
size_t size;
const char* name;
int index;
};
std::unordered_map<Execute, CommandInfo> mCommands;
void initializeLookup() {
int currentIndex = 0;
#define DECL_DRIVER_API_SYNCHRONOUS(RetType, methodName, paramsDecl, params)
#define DECL_DRIVER_API(methodName, paramsDecl, params) \
mCommands[mDispatcher.methodName##_] = { CommandBase::align(sizeof(COMMAND_TYPE(methodName))), \
#methodName, currentIndex++ };
#define DECL_DRIVER_API_RETURN(RetType, methodName, paramsDecl, params) \
mCommands[mDispatcher.methodName##_] = { \
CommandBase::align(sizeof(COMMAND_TYPE(methodName##R))), #methodName, currentIndex++ \
};
#include "private/backend/DriverAPI.inc"
mCommands[CustomCommand::execute] = { CommandBase::align(sizeof(CustomCommand)),
"CustomCommand", currentIndex++ };
// NoopCommands have variable size. We will handle them specially using their mNext pointer.
mCommands[NoopCommand::execute] = { 0, "NoopCommand", currentIndex++ };
}
#endif
public:
#define DECL_DRIVER_API(methodName, paramsDecl, params) \
inline void methodName(paramsDecl) noexcept { \
@@ -263,6 +295,13 @@ public:
void execute(void* buffer);
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
void debugIterateCommands(void* head, void* tail,
std::function<void(CommandInfo const& info)> const& callback);
void debugPrintHistogram(void* head, void* tail);
#endif
/*
* queueCommand() allows to queue a lambda function as a command.
* This is much less efficient than using the Driver* API.
@@ -48,6 +48,11 @@
#define FILAMENT_DEBUG_COMMANDS FILAMENT_DEBUG_COMMANDS_NONE
// Upon command stream overflow, print a histogram of commands
#ifndef FILAMENT_DEBUG_COMMANDS_HISTOGRAM
#define FILAMENT_DEBUG_COMMANDS_HISTOGRAM 0
#endif
namespace filament::backend {
class BufferDescriptor;
+8 -1
View File
@@ -79,7 +79,7 @@ bool CommandBufferQueue::isExitRequested() const {
}
void CommandBufferQueue::flush() {
void CommandBufferQueue::flush(std::function<void(void*, void*)> const& debugPrintHistogram) {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
CircularBuffer& circularBuffer = mCircularBuffer;
@@ -106,6 +106,13 @@ void CommandBufferQueue::flush() {
std::unique_lock lock(mLock);
// circular buffer is too small, we corrupted the stream
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
if (UTILS_VERY_UNLIKELY(used > mFreeSpace)) {
if (debugPrintHistogram) {
debugPrintHistogram(begin, end);
}
}
#endif
FILAMENT_CHECK_POSTCONDITION(used <= mFreeSpace) <<
"Backend CommandStream overflow. Commands are corrupted and unrecoverable.\n"
"Please increase minCommandBufferSizeMB inside the Config passed to Engine::create.\n"
+74
View File
@@ -30,6 +30,11 @@
#include <utils/ostream.h>
#include <utils/sstream.h>
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
#include <algorithm>
#include <vector>
#endif
#include <cstddef>
#include <functional>
#include <string>
@@ -83,6 +88,10 @@ CommandStream::CommandStream(Driver& driver, CircularBuffer& buffer) noexcept
__system_property_get("debug.filament.perfcounters", property);
mUsePerformanceCounter = bool(atoi(property));
#endif
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
initializeLookup();
#endif
}
void CommandStream::execute(void* buffer) {
@@ -126,6 +135,71 @@ void CommandStream::execute(void* buffer) {
}
}
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
void CommandStream::debugIterateCommands(void* head, void* tail,
std::function<void(CommandInfo const& info)> const& callback) {
CommandBase* UTILS_RESTRICT base = static_cast<CommandBase*>(head);
auto p = base;
while (UTILS_LIKELY(p)) {
if (p >= tail) {
break;
}
Execute e = p->mExecute;
if (e == NoopCommand::execute) {
NoopCommand* noop = static_cast<NoopCommand*>(p);
size_t size = noop->mNext;
int noopIndex = mCommands[NoopCommand::execute].index;
callback({ size, "NoopCommand", noopIndex });
p = reinterpret_cast<CommandBase*>(reinterpret_cast<char*>(p) + size);
continue;
}
if (auto it = mCommands.find(e); it != mCommands.end()) {
size_t size = it->second.size;
callback(it->second);
p = reinterpret_cast<CommandBase*>(reinterpret_cast<char*>(p) + size);
} else {
LOG(ERROR) << "Cannot find command in lookup table";
return;
}
}
}
void CommandStream::debugPrintHistogram(void* head, void* tail) {
std::unordered_map<std::string_view, int> histogram;
std::unordered_map<int, int> index_histogram;
debugIterateCommands(head, tail, [&](CommandInfo const& info) {
histogram[std::string_view(info.name)]++;
index_histogram[info.index]++;
});
std::vector<std::pair<std::string_view, int>> sorted_histogram(histogram.begin(),
histogram.end());
std::sort(sorted_histogram.begin(), sorted_histogram.end(),
[](auto const& a, auto const& b) { return a.second > b.second; });
LOG(INFO) << "Command stream histogram:";
for (auto const& [name, count]: sorted_histogram) {
LOG(INFO) << name << ": " << count;
}
std::vector<std::pair<int, int>> sorted_index_histogram(index_histogram.begin(),
index_histogram.end());
std::sort(sorted_index_histogram.begin(), sorted_index_histogram.end(),
[](auto const& a, auto const& b) { return a.second > b.second; });
std::string short_histogram = "";
for (size_t i = 0, n = sorted_index_histogram.size(); i < n; ++i) {
short_histogram += std::to_string(sorted_index_histogram[i].first) + ":" +
std::to_string(sorted_index_histogram[i].second);
short_histogram += (i < n - 1) ? ";" : ".";
}
LOG(INFO) << "CS hist: " << short_histogram;
LOG(INFO) << "";
}
#endif
void CommandStream::queueCommand(std::function<void()> command) {
new(allocateCommand(CustomCommand::align(sizeof(CustomCommand)))) CustomCommand(std::move(command));
}
+2 -20
View File
@@ -25,11 +25,6 @@
#include <mutex>
#ifdef __ANDROID__
#include <pthread.h>
#endif
namespace filament {
using namespace utils;
@@ -110,21 +105,8 @@ JNIEnv* VirtualMachineEnv::getEnvironmentSlow() {
FILAMENT_CHECK_PRECONDITION(mVirtualMachine)
<< "JNI_OnLoad() has not been called";
#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);
#if defined(__ANDROID__)
jint const result = mVirtualMachine->AttachCurrentThread(&mJniEnv, nullptr);
#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,12 +29,6 @@ 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,13 +40,6 @@ 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;
@@ -118,14 +118,6 @@ void VulkanBufferCache::terminate() noexcept {
}
}
size_t VulkanBufferCache::getSize() const noexcept {
size_t size = 0;
for (int i = 0; i < MAX_POOL_COUNT; i++) {
size += mGpuBufferPools[i].size();
}
return size;
}
void VulkanBufferCache::release(VulkanGpuBuffer const* gpuBuffer) noexcept {
assert_invariant(gpuBuffer != nullptr);
@@ -48,8 +48,6 @@ public:
// This should be called while the context's VkDevice is still alive.
void terminate() noexcept;
size_t getSize() const noexcept;
private:
struct UnusedGpuBuffer {
uint64_t lastAccessed;
+1 -18
View File
@@ -158,8 +158,6 @@ struct CommandBufferPool {
inline bool isRecording() const { return mRecording != INVALID; }
size_t getSize() const noexcept { return mBuffers.size(); }
private:
static constexpr int CAPACITY = FVK_MAX_COMMAND_BUFFERS;
// int8 only goes up to 127, therefore capacity must be less than that.
@@ -257,17 +255,6 @@ public:
// Updates the atomic "status" variable in every extant fence.
void updateFences();
struct SizeInfo {
size_t regular;
size_t protectedPool;
};
SizeInfo getSize() const noexcept {
return {
mPool ? mPool->getSize() : 0,
mProtectedPool ? mProtectedPool->getSize() : 0
};
}
#if FVK_ENABLED(FVK_DEBUG_GROUP_MARKERS)
void pushGroupMarker(char const* str, VulkanGroupMarkers::Timestamp timestamp = {});
void popGroupMarker();
@@ -290,11 +277,7 @@ private:
fvkmemory::resource_ptr<VulkanSemaphore> mLastSubmit;
VkFence mLastFence = VK_NULL_HANDLE;
// 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();
std::shared_ptr<VulkanCmdFence> mLastFenceStatus;
VkPipelineStageFlags mInjectedDependencyWaitStage = 0;
};
+2 -3
View File
@@ -36,7 +36,6 @@ VK_DEFINE_HANDLE(VmaPool)
namespace filament::backend {
struct VulkanCommandBuffer;
struct VulkanRenderPass;
struct VulkanRenderTarget;
struct VulkanSwapChain;
struct VulkanTexture;
@@ -59,11 +58,11 @@ struct VulkanAttachment {
VkImageSubresourceRange getSubresourceRange() const;
};
struct VulkanRenderPassContext {
struct VulkanRenderPass {
// Between the begin and end command render pass we cache the command buffer
VulkanCommandBuffer* commandBuffer;
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget;
fvkmemory::resource_ptr<VulkanRenderPass> renderPass;
VkRenderPass renderPass;
RenderPassParams params;
int currentSubpass;
};
@@ -119,9 +119,6 @@ public:
return mCapacity;
}
uint16_t size() const { return mSize; }
uint16_t unusedCount() const { return mUnusedCount; }
// A convenience method for checking if this pool can allocate sets for a given layout.
inline bool canAllocate(DescriptorCount const& count) {
return count == mCount;
@@ -258,16 +255,6 @@ public:
}
}
VulkanDescriptorSetCache::SizeInfo getSize() const {
VulkanDescriptorSetCache::SizeInfo info = {};
info.poolCount = mPools.size();
for (auto& pool : mPools) {
info.totalSize += pool->size();
info.totalUnusedCount += pool->unusedCount();
}
return info;
}
private:
VkDevice mDevice;
std::vector<std::unique_ptr<DescriptorPool>> mPools;
@@ -461,10 +448,6 @@ void VulkanDescriptorSetCache::manualRecycle(VulkanDescriptorSetLayout::Count co
void VulkanDescriptorSetCache::gc() { mStashedSets = {}; }
VulkanDescriptorSetCache::SizeInfo VulkanDescriptorSetCache::getSize() const noexcept {
return mDescriptorPool->getSize();
}
void VulkanDescriptorSetCache::copySet(VkDescriptorSet srcSet, VkDescriptorSet dstSet,
fvkutils::SamplerBitmask bindings) const {
// TODO: fix the size for better memory management
@@ -89,13 +89,6 @@ public:
void resetCachedState() noexcept { mLastBoundInfo = {}; }
struct SizeInfo {
size_t poolCount;
size_t totalSize;
size_t totalUnusedCount;
};
SizeInfo getSize() const noexcept;
private:
void copySet(VkDescriptorSet srcSet, VkDescriptorSet destSet,
fvkutils::SamplerBitmask copyBindings) const;
@@ -49,8 +49,6 @@ public:
fvkutils::SamplerBitmask externalSamplers,
utils::FixedCapacityVector<std::pair<uint64_t, VkSampler>> immutableSamplers = {});
size_t getSize() const noexcept { return mVkLayouts.size(); }
private:
VkDevice mDevice;
fvkmemory::ResourceManager* mResourceManager;
+14 -43
View File
@@ -49,7 +49,6 @@
#include <chrono>
#include <mutex>
#include <stdio.h>
using namespace bluevk;
@@ -446,29 +445,6 @@ void VulkanDriver::collectGarbage() {
mResourceManager.gc();
auto dsSize = mDescriptorSetCache.getSize();
auto stageSize = mStagePool.getSize();
auto externalSize = mExternalImageManager.getSize();
auto commandsSize = mCommands.getSize();
fprintf(stderr, "Vulkan Cache Sizes:\n");
fprintf(stderr, " Pipelines: %zu\n", mPipelineCache.getSize());
fprintf(stderr, " Pipeline Layouts: %zu\n", mPipelineLayoutCache.getSize());
fprintf(stderr, " Descriptor Set Layouts: %zu\n", mDescriptorSetLayoutCache.getSize());
fprintf(stderr, " Descriptor Sets: %zu (pools: %zu, unused: %zu)\n", dsSize.totalSize, dsSize.poolCount, dsSize.totalUnusedCount);
fprintf(stderr, " FBOs: %zu\n", mFramebufferCache.getFboCacheSize());
fprintf(stderr, " Render Passes: %zu (refcount: %zu)\n",
mFramebufferCache.getRenderPassCacheSize(), mFramebufferCache.getRenderPassRefCountSize());
fprintf(stderr, " Samplers: %zu\n", mSamplerCache.getSize());
fprintf(stderr, " Buffers: %zu\n", mBufferCache.getSize());
fprintf(stderr, " Stages: %zu, Free Images: %zu\n", stageSize.stages, stageSize.freeImages);
fprintf(stderr, " YCbCr Conversions: %zu\n", mYcbcrConversionCache.getSize());
fprintf(stderr, " External Images: %zu, Set Bindings: %zu\n", externalSize.images, externalSize.setBindings);
fprintf(stderr, " Streamed Bindings: %zu\n", mStreamedImageManager.getSize());
fprintf(stderr, " Command Buffers: %zu (regular: %zu, protected: %zu)\n",
commandsSize.regular + commandsSize.protectedPool, commandsSize.regular, commandsSize.protectedPool);
fprintf(stderr, " Semaphores: %zu\n", mSemaphoreManager.getSize());
#if FVK_ENABLED(FVK_DEBUG_RESOURCE_LEAK)
mResourceManager.print();
#endif
@@ -1937,6 +1913,9 @@ 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.
@@ -1951,11 +1930,6 @@ 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();
@@ -2001,19 +1975,17 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
rpkey.initialDepthLayout = currentDepthLayout;
rpkey.subpassMask = uint8_t(params.subpassMask);
fvkmemory::resource_ptr<VulkanRenderPass> renderPass =
mFramebufferCache.getRenderPass(rpkey, &mResourceManager);
VkRenderPass renderPass = mFramebufferCache.getRenderPass(rpkey);
mPipelineCache.bindRenderPass(renderPass, 0);
// Create the VkFramebuffer or fetch it from cache.
VulkanFboCache::FboKey fbkey = rt->getFboKey();
fbkey.renderPass = renderPass->getVkRenderPass();
fbkey.renderPass = renderPass;
fbkey.layers = 1;
rt->emitBarriersBeginRenderPass(*commandBuffer);
fvkmemory::resource_ptr<VulkanFramebuffer> vkfb =
mFramebufferCache.getFramebuffer(fbkey, &mResourceManager);
VkFramebuffer vkfb = mFramebufferCache.getFramebuffer(fbkey);
// Assign a label to the framebuffer for debugging purposes.
#if FVK_ENABLED(FVK_DEBUG_GROUP_MARKERS | FVK_DEBUG_DEBUG_UTILS)
@@ -2026,14 +1998,12 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
// The current command buffer now has references to the render target and its attachments.
commandBuffer->acquire(rt);
commandBuffer->acquire(renderPass);
commandBuffer->acquire(vkfb);
// Populate the structures required for vkCmdBeginRenderPass.
VkRenderPassBeginInfo renderPassInfo {
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
.renderPass = renderPass->getVkRenderPass(),
.framebuffer = vkfb->getVkFramebuffer(),
.renderPass = renderPass,
.framebuffer = vkfb,
// The renderArea field constrains the LoadOp, but scissoring does not.
// Therefore, we do not set the scissor rect here, we only need it in draw().
@@ -2088,7 +2058,7 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
mCurrentRenderPass = {
.commandBuffer = commandBuffer,
.renderTarget = rt,
.renderPass = renderPass,
.renderPass = renderPassInfo.renderPass,
.params = params,
.currentSubpass = 0,
};
@@ -2108,7 +2078,8 @@ void VulkanDriver::endRenderPass(int) {
rt->emitBarriersEndRenderPass(*mCurrentRenderPass.commandBuffer);
mCurrentRenderPass.renderTarget = {};
mCurrentRenderPass.renderPass = {};
mCurrentRenderPass.renderPass = VK_NULL_HANDLE;
mCurrentRenderPass.commandBuffer = nullptr;
}
@@ -2273,7 +2244,7 @@ void VulkanDriver::resolve(
Handle<HwTexture> src, uint8_t dstLevel, uint8_t dstLayer) {
FVK_SYSTRACE_SCOPE();
FILAMENT_CHECK_PRECONDITION(!mCurrentRenderPass.renderPass)
FILAMENT_CHECK_PRECONDITION(mCurrentRenderPass.renderPass == VK_NULL_HANDLE)
<< "resolve() cannot be invoked inside a render pass.";
auto srcTexture = resource_ptr<VulkanTexture>::cast(&mResourceManager, src);
@@ -2316,7 +2287,7 @@ void VulkanDriver::blit(
math::uint2 size) {
FVK_SYSTRACE_SCOPE();
FILAMENT_CHECK_PRECONDITION(!mCurrentRenderPass.renderPass)
FILAMENT_CHECK_PRECONDITION(mCurrentRenderPass.renderPass == VK_NULL_HANDLE)
<< "blit() cannot be invoked inside a render pass.";
auto srcTexture = resource_ptr<VulkanTexture>::cast(&mResourceManager, src);
@@ -2358,7 +2329,7 @@ void VulkanDriver::blitDEPRECATED(TargetBufferFlags buffers,
// Note: blitDEPRECATED is only used for Renderer::copyFrame()
FILAMENT_CHECK_PRECONDITION(!mCurrentRenderPass.renderPass)
FILAMENT_CHECK_PRECONDITION(mCurrentRenderPass.renderPass == VK_NULL_HANDLE)
<< "blitDEPRECATED() cannot be invoked inside a render pass.";
FILAMENT_CHECK_PRECONDITION(buffers == TargetBufferFlags::COLOR0)
+1 -1
View File
@@ -139,7 +139,7 @@ private:
resource_ptr<VulkanSwapChain> mCurrentSwapChain;
resource_ptr<VulkanRenderTarget> mDefaultRenderTarget;
VulkanRenderPassContext mCurrentRenderPass = {};
VulkanRenderPass mCurrentRenderPass = {};
VmaAllocator mAllocator = VK_NULL_HANDLE;
VkDebugReportCallbackEXT mDebugCallback = VK_NULL_HANDLE;
@@ -85,12 +85,6 @@ public:
// - Update the bindings that use external samplers.
void updateSetAndLayout(fvkmemory::resource_ptr<VulkanDescriptorSet> set);
struct SizeInfo {
size_t setBindings;
size_t images;
};
SizeInfo getSize() const noexcept { return { mSetBindings.size(), mImages.size() }; }
private:
VulkanSamplerCache* mSamplerCache;
+18 -16
View File
@@ -17,7 +17,6 @@
#include "VulkanFboCache.h"
#include "VulkanConstants.h"
#include "VulkanHandles.h"
#include "vulkan/utils/Image.h"
#include <utils/Panic.h>
@@ -70,10 +69,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 {
VkFramebuffer VulkanFboCache::getFramebuffer(FboKey const& config) noexcept {
FboMap::iterator iter = mFramebufferCache.find(config);
if (UTILS_LIKELY(iter != mFramebufferCache.end())) {
if (UTILS_LIKELY(iter != mFramebufferCache.end() && iter->second.handle != VK_NULL_HANDLE)) {
iter.value().timestamp = mCurrentTime;
return iter->second.handle;
}
@@ -119,16 +117,13 @@ fvkmemory::resource_ptr<VulkanFramebuffer> VulkanFboCache::getFramebuffer(
VkResult error = vkCreateFramebuffer(mDevice, &info, VKALLOC, &framebuffer);
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};
return fbh;
mFramebufferCache[config] = {framebuffer, mCurrentTime};
return framebuffer;
}
fvkmemory::resource_ptr<VulkanRenderPass> VulkanFboCache::getRenderPass(
RenderPassKey const& config, fvkmemory::ResourceManager* resManager) noexcept {
VkRenderPass VulkanFboCache::getRenderPass(RenderPassKey const& config) noexcept {
auto iter = mRenderPassCache.find(config);
if (UTILS_LIKELY(iter != mRenderPassCache.end())) {
if (UTILS_LIKELY(iter != mRenderPassCache.end() && iter->second.handle != VK_NULL_HANDLE)) {
iter.value().timestamp = mCurrentTime;
return iter->second.handle;
}
@@ -331,9 +326,7 @@ fvkmemory::resource_ptr<VulkanRenderPass> VulkanFboCache::getRenderPass(
VkResult error = vkCreateRenderPass(mDevice, &renderPassInfo, VKALLOC, &renderPass);
FILAMENT_CHECK_POSTCONDITION(error == VK_SUCCESS) << "Unable to create render pass."
<< " error=" << error;
fvkmemory::resource_ptr<VulkanRenderPass> rph =
fvkmemory::resource_ptr<VulkanRenderPass>::construct(resManager, mDevice, renderPass);
mRenderPassCache[config] = {rph, mCurrentTime};
mRenderPassCache[config] = {renderPass, mCurrentTime};
#if FVK_ENABLED(FVK_DEBUG_FBO_CACHE)
FVK_LOGD << "Created render pass " << renderPass << " with ";
@@ -350,12 +343,13 @@ fvkmemory::resource_ptr<VulkanRenderPass> VulkanFboCache::getRenderPass(
<< "colorAttachmentCount[0] = " << subpasses[0].colorAttachmentCount;
#endif
return rph;
return renderPass;
}
void VulkanFboCache::resetFramebuffers() noexcept {
for (const auto& pair: mFramebufferCache) {
mRenderPassRefCount[pair.first.renderPass]--;
vkDestroyFramebuffer(mDevice, pair.second.handle, VKALLOC);
}
mFramebufferCache.clear();
}
@@ -363,6 +357,9 @@ void VulkanFboCache::resetFramebuffers() noexcept {
void VulkanFboCache::terminate() noexcept {
resetFramebuffers();
for (const auto& pair: mRenderPassCache) {
vkDestroyRenderPass(mDevice, pair.second.handle, VKALLOC);
}
mRenderPassRefCount.clear();
mRenderPassCache.clear();
}
@@ -383,6 +380,8 @@ void VulkanFboCache::gc() noexcept {
const FboVal fbo = iter->second;
if (fbo.timestamp < evictTime && fbo.handle) {
mRenderPassRefCount[iter->first.renderPass]--;
vkDestroyFramebuffer(mDevice, fbo.handle, VKALLOC);
iter.value().handle = VK_NULL_HANDLE;
// erase(iterator) returns the iterator to the next element.
iter = mFramebufferCache.erase(iter);
@@ -392,8 +391,11 @@ void VulkanFboCache::gc() noexcept {
}
for (RenderPassMap::iterator iter = mRenderPassCache.begin(); iter != mRenderPassCache.end(); ) {
const VkRenderPass handle = iter->second.handle->getVkRenderPass();
const VkRenderPass handle = iter->second.handle;
if (iter->second.timestamp < evictTime && handle && mRenderPassRefCount[handle] == 0) {
vkDestroyRenderPass(mDevice, handle, VKALLOC);
iter.value().handle = VK_NULL_HANDLE;
// erase(iterator) returns the iterator to the next element.
iter = mRenderPassCache.erase(iter);
mRenderPassRefCount.erase(handle);
+4 -16
View File
@@ -18,9 +18,6 @@
#define TNT_FILAMENT_BACKEND_VULKANFBOCACHE_H
#include "VulkanContext.h"
#include "vulkan/memory/Resource.h"
#include "vulkan/memory/ResourceManager.h"
#include "vulkan/memory/ResourcePointer.h"
#include <utils/Hash.h>
@@ -30,9 +27,6 @@
namespace filament::backend {
struct VulkanFramebuffer;
struct VulkanRenderPass;
// Simple manager for VkFramebuffer and VkRenderPass objects.
//
// Note that a VkFramebuffer is just a binding between a render pass and a set of image views. So,
@@ -63,7 +57,7 @@ public:
uint8_t padding[2];
};
struct RenderPassVal {
fvkmemory::resource_ptr<VulkanRenderPass> handle;
VkRenderPass handle;
uint32_t timestamp;
};
static_assert(0 == MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT % 8);
@@ -89,7 +83,7 @@ public:
VkImageView depth; // 8 bytes
};
struct FboVal {
fvkmemory::resource_ptr<VulkanFramebuffer> handle;
VkFramebuffer handle;
uint32_t timestamp;
};
static_assert(sizeof(VkRenderPass) == 8, "VkRenderPass has unexpected size.");
@@ -104,12 +98,10 @@ public:
~VulkanFboCache();
// Retrieves or creates a VkFramebuffer handle.
fvkmemory::resource_ptr<VulkanFramebuffer> getFramebuffer(
FboKey const& config, fvkmemory::ResourceManager* resManager) noexcept;
VkFramebuffer getFramebuffer(FboKey const& config) noexcept;
// Retrieves or creates a VkRenderPass handle.
fvkmemory::resource_ptr<VulkanRenderPass> getRenderPass(
RenderPassKey const& config, fvkmemory::ResourceManager* resManager) noexcept;
VkRenderPass getRenderPass(RenderPassKey const& config) noexcept;
// Evicts old unused Vulkan objects. Call this once per frame.
void gc() noexcept;
@@ -120,10 +112,6 @@ public:
// Frees all Vulkan objects. Call this during shutdown before the device is destroyed.
void terminate() noexcept;
size_t getFboCacheSize() const noexcept { return mFramebufferCache.size(); }
size_t getRenderPassCacheSize() const noexcept { return mRenderPassCache.size(); }
size_t getRenderPassRefCountSize() const noexcept { return mRenderPassRefCount.size(); }
private:
VkDevice mDevice;
using FboMap = tsl::robin_map<FboKey, FboVal, FboKeyHashFn, FboKeyEqualFn>;
+1 -15
View File
@@ -565,7 +565,7 @@ void VulkanRenderTarget::transformViewportToPlatform(VkViewport* bounds) const {
flipVertically(bounds, getExtent().height);
}
uint8_t VulkanRenderTarget::getColorTargetCount(const VulkanRenderPassContext& pass) const {
uint8_t VulkanRenderTarget::getColorTargetCount(const VulkanRenderPass& pass) const {
if (!mOffscreen) {
return 1;
}
@@ -715,18 +715,4 @@ VulkanRenderPrimitive::VulkanRenderPrimitive(PrimitiveType pt,
vertexBuffer(vb),
indexBuffer(ib) {}
VulkanFramebuffer::VulkanFramebuffer(VkDevice device, VkFramebuffer framebuffer)
: mDevice(device), mFramebuffer(framebuffer) {}
VulkanFramebuffer::~VulkanFramebuffer() {
vkDestroyFramebuffer(mDevice, mFramebuffer, VKALLOC);
}
VulkanRenderPass::VulkanRenderPass(VkDevice device, VkRenderPass renderPass)
: mDevice(device), mRenderPass(renderPass) {}
VulkanRenderPass::~VulkanRenderPass() {
vkDestroyRenderPass(mDevice, mRenderPass, VKALLOC);
}
} // namespace filament::backend
+1 -27
View File
@@ -419,7 +419,7 @@ struct VulkanRenderTarget : private HwRenderTarget, fvkmemory::Resource {
return mInfo->fbkey.samples;
}
uint8_t getColorTargetCount(VulkanRenderPassContext const& pass) const;
uint8_t getColorTargetCount(VulkanRenderPass const& pass) const;
inline bool hasDepth() const { return mInfo->depthIndex != Auxiliary::UNDEFINED_INDEX; }
@@ -585,32 +585,6 @@ struct VulkanRenderPrimitive : public HwRenderPrimitive, fvkmemory::Resource {
fvkmemory::resource_ptr<VulkanIndexBuffer> indexBuffer;
};
struct VulkanFramebuffer : public fvkmemory::Resource {
VulkanFramebuffer(VkDevice device, VkFramebuffer framebuffer);
~VulkanFramebuffer();
inline VkFramebuffer getVkFramebuffer() const noexcept {
return mFramebuffer;
}
private:
VkDevice mDevice;
VkFramebuffer mFramebuffer;
};
struct VulkanRenderPass : public fvkmemory::Resource {
VulkanRenderPass(VkDevice device, VkRenderPass renderPass);
~VulkanRenderPass();
inline VkRenderPass getVkRenderPass() const noexcept {
return mRenderPass;
}
private:
VkDevice mDevice;
VkRenderPass mRenderPass;
};
} // namespace filament::backend
#endif // TNT_FILAMENT_BACKEND_VULKANHANDLES_H
@@ -459,10 +459,8 @@ void VulkanPipelineCache::bindStencilState(StencilState const& stencilState) noe
mPipelineRequirements.stencilState = stencilState;
}
void VulkanPipelineCache::bindRenderPass(
fvkmemory::resource_ptr<VulkanRenderPass> renderPass,
int subpassIndex) noexcept {
mPipelineRequirements.renderPass = renderPass->getVkRenderPass();
void VulkanPipelineCache::bindRenderPass(VkRenderPass renderPass, int subpassIndex) noexcept {
mPipelineRequirements.renderPass = renderPass;
mPipelineRequirements.subpassIndex = subpassIndex;
}
@@ -123,8 +123,7 @@ public:
void bindProgram(fvkmemory::resource_ptr<VulkanProgram> program) noexcept;
void bindRasterState(RasterState const& rasterState) noexcept;
void bindStencilState(StencilState const& stencilState) noexcept;
void bindRenderPass(fvkmemory::resource_ptr<VulkanRenderPass> renderPass,
int subpassIndex) noexcept;
void bindRenderPass(VkRenderPass renderPass, int subpassIndex) noexcept;
void bindPrimitiveTopology(VkPrimitiveTopology topology) noexcept;
void bindVertexArray(VkVertexInputAttributeDescription const* attribDesc,
VkVertexInputBindingDescription const* bufferDesc, uint8_t count);
@@ -136,8 +135,6 @@ public:
void terminate() noexcept;
void gc() noexcept;
size_t getSize() const noexcept { return mPipelines.size(); }
private:
// PIPELINE CACHE KEY
// ------------------
@@ -59,8 +59,6 @@ public:
VkPipelineLayout getLayout(DescriptorSetLayoutArray const& descriptorSetLayouts,
fvkmemory::resource_ptr<VulkanProgram> program);
size_t getSize() const noexcept { return mPipelineLayouts.size(); }
private:
using Timestamp = uint64_t;
struct PipelineLayoutCacheEntry {
@@ -40,8 +40,6 @@ public:
explicit VulkanSamplerCache(VkDevice device);
VkSampler getSampler(Params params);
void terminate() noexcept;
size_t getSize() const noexcept { return mCache.size(); }
private:
VkDevice mDevice;
@@ -37,8 +37,6 @@ public:
void terminate();
Semaphore acquire();
size_t getSize() const noexcept { return mPool.size(); }
private:
friend struct VulkanSemaphore;
void recycle(VkSemaphore semaphore);
@@ -223,12 +223,6 @@ public:
// resource_ptrs, as this would lead to undefined behavior.
void terminate() noexcept;
struct SizeInfo {
size_t stages;
size_t freeImages;
};
SizeInfo getSize() const noexcept { return { mStages.size(), mFreeImages.size() }; }
private:
VmaAllocator mAllocator;
fvkmemory::ResourceManager* mResManager;
@@ -46,8 +46,6 @@ public:
void onStreamAcquireImage(fvkmemory::resource_ptr<VulkanTexture> image,
fvkmemory::resource_ptr<VulkanStream> stream);
size_t getSize() const noexcept { return mStreamedTexturesBindings.size(); }
private:
struct StreamedTextureBinding {
uint8_t binding = 0;
@@ -42,8 +42,6 @@ public:
VkSamplerYcbcrConversion getConversion(Params params);
void terminate() noexcept;
size_t getSize() const noexcept { return mCache.size(); }
private:
VkDevice mDevice;
@@ -42,8 +42,6 @@ template ResourceType getTypeEnum<VulkanSync>() noexcept;
template ResourceType getTypeEnum<VulkanMemoryMappedBuffer>() noexcept;
template ResourceType getTypeEnum<VulkanSemaphore>() noexcept;
template ResourceType getTypeEnum<VulkanStream>() noexcept;
template ResourceType getTypeEnum<VulkanFramebuffer>() noexcept;
template ResourceType getTypeEnum<VulkanRenderPass>() noexcept;
template<typename D>
ResourceType getTypeEnum() noexcept {
@@ -110,12 +108,6 @@ ResourceType getTypeEnum() noexcept {
if constexpr (std::is_same_v<D, VulkanStream>) {
return ResourceType::STREAM;
}
if constexpr (std::is_same_v<D, VulkanFramebuffer>) {
return ResourceType::FRAMEBUFFER;
}
if constexpr (std::is_same_v<D, VulkanRenderPass>) {
return ResourceType::RENDER_PASS;
}
return ResourceType::UNDEFINED_TYPE;
}
@@ -163,10 +155,6 @@ std::string_view getTypeStr(ResourceType type) {
return "Semaphore";
case ResourceType::STREAM:
return "VulkanStream";
case ResourceType::FRAMEBUFFER:
return "Framebuffer";
case ResourceType::RENDER_PASS:
return "RenderPass";
case ResourceType::UNDEFINED_TYPE:
return "";
}
@@ -56,9 +56,7 @@ enum class ResourceType : uint8_t {
MEMORY_MAPPED_BUFFER = 18,
SEMAPHORE = 19,
STREAM = 20,
FRAMEBUFFER = 21,
RENDER_PASS = 22,
UNDEFINED_TYPE = 23, // Must be the last enum because we use it for iterating over the enums.
UNDEFINED_TYPE = 21, // Must be the last enum because we use it for iterating over the enums.
};
template<typename D>
@@ -126,12 +126,6 @@ void ResourceManager::destroyWithType(ResourceType type, HandleId id) {
case ResourceType::STREAM:
destruct<VulkanStream>(Handle<VulkanStream>(id));
break;
case ResourceType::FRAMEBUFFER:
destruct<VulkanFramebuffer>(Handle<VulkanFramebuffer>(id));
break;
case ResourceType::RENDER_PASS:
destruct<VulkanRenderPass>(Handle<VulkanRenderPass>(id));
break;
case ResourceType::UNDEFINED_TYPE:
break;
}
@@ -18,7 +18,6 @@
#include "Shader.h"
#include "SharedShaders.h"
#include "Skip.h"
#include "TrianglePrimitive.h"
#include <backend/PixelBufferDescriptor.h>
@@ -32,9 +31,6 @@ using namespace filament::backend;
using namespace filament::math;
TEST_F(BackendTest, AutoresolveDifferingSampleCounts) {
SKIP_IF(SkipEnvironment(OperatingSystem::CI, Backend::OPENGL), "see b/486954356");
SKIP_IF(SkipEnvironment(OperatingSystem::CI, Backend::VULKAN), "see b/486954356");
auto& api = getDriverApi();
constexpr int kRenderTargetSize = 512;
+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;
+1 -1
View File
@@ -735,7 +735,7 @@ struct VsmShadowOptions {
bool highPrecision = false;
/**
* @deprecated has no effect.
* VSM minimum variance scale, must be positive.
*/
float minVarianceScale = 0.5f;
+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();
perViewDescriptorSetLayoutPcf.terminate(engine.getDescriptorSetLayoutFactory(), driver);
perViewDescriptorSetLayoutS2d.terminate(engine.getDescriptorSetLayoutFactory(), driver);
perViewDescriptorSetLayout.terminate(engine.getDescriptorSetLayoutFactory(), driver);
perViewDescriptorSetLayoutVsm.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->perViewDescriptorSetLayoutPcfDescription = descriptor_sets::getPerViewDescriptorSetLayout(
this->perViewDescriptorSetLayoutDescription = descriptor_sets::getPerViewDescriptorSetLayout(
materialDomain, isLit, isSSR, hasFog, false);
this->perViewDescriptorSetLayoutS2dDescription = descriptor_sets::getPerViewDescriptorSetLayout(
this->perViewDescriptorSetLayoutVsmDescription = descriptor_sets::getPerViewDescriptorSetLayout(
materialDomain, isLit, isSSR, hasFog, true);
// set the labels
this->descriptorSetLayoutDescription.label = CString{ name }.append("_perMat");
this->perViewDescriptorSetLayoutPcfDescription.label = CString{ name }.append("_perView");
this->perViewDescriptorSetLayoutS2dDescription.label = CString{ name }.append("_perViewVsm");
this->perViewDescriptorSetLayoutDescription.label = CString{ name }.append("_perView");
this->perViewDescriptorSetLayoutVsmDescription.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->perViewDescriptorSetLayoutPcfDescription }}) {
this->perViewDescriptorSetLayoutDescription }}) {
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->perViewDescriptorSetLayoutPcf = {
this->perViewDescriptorSetLayout = {
descriptorSetLayoutFactory, driver,
this->perViewDescriptorSetLayoutPcfDescription };
this->perViewDescriptorSetLayoutDescription };
this->perViewDescriptorSetLayoutS2d = {
this->perViewDescriptorSetLayoutVsm = {
descriptorSetLayoutFactory, driver,
this->perViewDescriptorSetLayoutS2dDescription };
this->perViewDescriptorSetLayoutVsmDescription };
}
backend::DescriptorSetLayout const& MaterialDefinition::getPerViewDescriptorSetLayoutDescription(
Variant const variant, bool const useS2dDescriptorSetLayout) const noexcept {
Variant const variant, bool const useVsmDescriptorSetLayout) 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 (useS2dDescriptorSetLayout) {
return perViewDescriptorSetLayoutS2dDescription;
if (useVsmDescriptorSetLayout) {
return perViewDescriptorSetLayoutVsmDescription;
}
return perViewDescriptorSetLayoutPcfDescription;
return perViewDescriptorSetLayoutDescription;
}
Handle<HwProgram> MaterialDefinition::compileProgram(
@@ -689,7 +689,7 @@ Handle<HwProgram> MaterialDefinition::compileProgram(
pb.descriptorLayout(+DescriptorSetBindingPoints::PER_VIEW,
getPerViewDescriptorSetLayoutDescription(
specialization.variant,
Variant::isShadowSampler2DVariant(specialization.variant)));
Variant::isVSMVariant(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 useS2dDescriptorSetLayout) const noexcept;
Variant const variant, bool useVsmDescriptorSetLayout) 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 perViewDescriptorSetLayoutPcfDescription;
backend::DescriptorSetLayout perViewDescriptorSetLayoutS2dDescription;
backend::DescriptorSetLayout perViewDescriptorSetLayoutDescription;
backend::DescriptorSetLayout perViewDescriptorSetLayoutVsmDescription;
backend::DescriptorSetLayout descriptorSetLayoutDescription;
// try to order by frequency of use
filament::DescriptorSetLayout perViewDescriptorSetLayoutPcf;
filament::DescriptorSetLayout perViewDescriptorSetLayoutS2d;
filament::DescriptorSetLayout perViewDescriptorSetLayout;
filament::DescriptorSetLayout perViewDescriptorSetLayoutVsm;
filament::DescriptorSetLayout descriptorSetLayout;
backend::Program::DescriptorSetInfo programDescriptorBindings;
+68 -39
View File
@@ -19,61 +19,90 @@
#include "details/Engine.h"
#include "details/Material.h"
#include "details/MaterialInstance.h"
#include <cstdint>
#include <utils/debug.h>
#include <iterator>
#include <utility>
#include <stdint.h>
namespace filament {
using namespace utils;
using Record = MaterialInstanceManager::Record;
MaterialInstanceManager::MaterialInstanceManager() noexcept = default;
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++ };
}
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&& rhs) noexcept = default;
MaterialInstanceManager& MaterialInstanceManager::operator=(
MaterialInstanceManager const& rhs) noexcept = default;
MaterialInstanceManager& MaterialInstanceManager::operator=(
MaterialInstanceManager&& rhs) noexcept = default;
MaterialInstanceManager::~MaterialInstanceManager() = default;
void MaterialInstanceManager::terminate(FEngine& 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();
std::for_each(mMaterials.begin(), mMaterials.end(), [&engine](auto& record) {
record.terminate(engine);
});
}
void MaterialInstanceManager::reset() {
for (auto& [material, pool] : mAnonymousMaterialInstances) {
pool.highWaterMark = 0;
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());
}
}
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;
return *itr;
}
FMaterialInstance* MaterialInstanceManager::getMaterialInstance(FMaterial const* ma) const {
AnonymousPool& pool = mAnonymousMaterialInstances[ma];
if (pool.highWaterMark < pool.instances.size()) {
return pool.instances[pool.highWaterMark++];
}
FMaterialInstance* const instance = ma->createInstance(ma->getName().c_str_safe());
pool.instances.push_back(instance);
pool.highWaterMark++;
return instance;
auto [inst, index] = getRecord(ma).getInstance();
return inst;
}
FMaterialInstance* MaterialInstanceManager::getMaterialInstance(FMaterial const* ma,
int32_t const fixedIndex) const {
return getRecord(ma).getInstance(fixedIndex);
}
std::pair<FMaterialInstance*, int32_t> MaterialInstanceManager::getFixedMaterialInstance(
FMaterial const* ma) {
return getRecord(ma).getInstance();
}
} // namespace filament
+60 -56
View File
@@ -16,12 +16,9 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <unordered_map>
#include <vector>
#include <utils/bitset.h>
#include <utils/Hash.h>
#include <vector>
namespace filament {
@@ -33,10 +30,37 @@ 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) = delete;
MaterialInstanceManager(MaterialInstanceManager const& rhs) noexcept;
MaterialInstanceManager(MaterialInstanceManager&& rhs) noexcept;
MaterialInstanceManager& operator=(MaterialInstanceManager const& rhs) = delete;
MaterialInstanceManager& operator=(MaterialInstanceManager const& rhs) noexcept;
MaterialInstanceManager& operator=(MaterialInstanceManager&& rhs) noexcept;
~MaterialInstanceManager();
@@ -47,60 +71,40 @@ public:
*/
void terminate(FEngine& engine);
/**
* 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
/*
* 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.
*/
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:
struct Key {
FMaterial const* material;
uint32_t tag;
bool operator==(Key const& rhs) const noexcept {
return material == rhs.material && tag == rhs.tag;
}
};
Record& getRecord(FMaterial const* material) const;
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;
mutable std::vector<Record> mMaterials;
};
} // namespace filament
+26 -10
View File
@@ -225,6 +225,7 @@ 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
@@ -454,6 +455,7 @@ Handle<HwTexture> PostProcessManager::getZeroTextureArray() const {
void PostProcessManager::resetForRender() {
mMaterialInstanceManager.reset();
mFixedMaterialInstanceIndex = {};
}
void PostProcessManager::unbindAllDescriptorSets(DriverApi& driver) noexcept {
@@ -800,7 +802,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_VARIANT });
passBuilder.variant(Variant{ Variant::SPECIAL_SSR });
// generate all our drawing commands, except blended objects.
passBuilder.commandTypeFlags(RenderPass::CommandTypeFlags::SCREEN_SPACE_REFLECTIONS);
@@ -2550,7 +2552,11 @@ void PostProcessManager::colorGradingSubpass(DriverApi& driver,
auto const& material = getPostProcessMaterial("colorGradingAsSubpass");
FMaterial const* const ma = material.getMaterial(mEngine, driver, variant);
// the UBO has been set and committed in colorGradingPrepareSubpass()
FMaterialInstance const* mi = mMaterialInstanceManager.getMaterialInstance(ma, colorGradingConfig.translucent);
int32_t const fixedIndex = colorGradingConfig.translucent
? mFixedMaterialInstanceIndex.colorGradingTranslucent
: mFixedMaterialInstanceIndex.colorGradingOpaque;
FMaterialInstance const* mi = mMaterialInstanceManager.getMaterialInstance(ma, fixedIndex);
mi->use(driver);
auto const pipeline = getPipelineState(ma, variant);
driver.nextSubpass();
@@ -2561,7 +2567,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* const mi = mMaterialInstanceManager.getMaterialInstance(ma, 0);
auto [mi, fixedIndex] = mMaterialInstanceManager.getFixedMaterialInstance(ma);
mFixedMaterialInstanceIndex.customResolve = fixedIndex;
mi->setParameter("direction", op == CustomResolveOp::COMPRESS ? 1.0f : -1.0f),
mi->commit(driver, getUboManager());
}
@@ -2573,7 +2580,8 @@ void PostProcessManager::customResolveSubpass(DriverApi& driver) noexcept {
auto const& material = getPostProcessMaterial("customResolveAsSubpass");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
// the UBO has been set and committed in customResolvePrepareSubpass()
FMaterialInstance const* mi = mMaterialInstanceManager.getMaterialInstance(ma, 0);
FMaterialInstance const* mi = mMaterialInstanceManager.getMaterialInstance(ma,
mFixedMaterialInstanceIndex.customResolve);
mi->use(driver);
auto const pipeline = getPipelineState(ma);
@@ -2612,8 +2620,9 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::customResolveUncompressPass(
void PostProcessManager::clearAncillaryBuffersPrepare(DriverApi& driver,
Variant::type_t variant) noexcept {
auto const& material = getPostProcessMaterial("clearDepth");
auto const ma = material.getMaterial(mEngine, driver, variant);
auto const mi = mMaterialInstanceManager.getMaterialInstance(ma, 0);
auto ma = material.getMaterial(mEngine, driver, variant);
auto [mi, fixedIndex] = mMaterialInstanceManager.getFixedMaterialInstance(ma);
mFixedMaterialInstanceIndex.clearDepth = fixedIndex;
mi->commit(driver, getUboManager());
}
@@ -2631,7 +2640,8 @@ void PostProcessManager::clearAncillaryBuffers(DriverApi& driver,
FMaterial const* const ma = material.getMaterial(mEngine, driver, variant);
// the UBO has been set and committed in clearAncillaryBuffersPrepare()
FMaterialInstance const* const mi = mMaterialInstanceManager.getMaterialInstance(ma, 0);
FMaterialInstance const* const mi = mMaterialInstanceManager.getMaterialInstance(ma,
mFixedMaterialInstanceIndex.clearDepth);
mi->use(driver);
auto pipeline = getPipelineState(ma, variant);
@@ -2950,7 +2960,11 @@ FMaterialInstance* PostProcessManager::configureColorGradingMaterial(backend::Dr
? PostProcessVariant::TRANSLUCENT
: PostProcessVariant::OPAQUE;
ma = material.getMaterial(mEngine, driver, variant);
FMaterialInstance* mi = mMaterialInstanceManager.getMaterialInstance(ma, colorGradingConfig.translucent);
FMaterialInstance* mi = nullptr;
int32_t& fixedIndex = colorGradingConfig.translucent
? mFixedMaterialInstanceIndex.colorGradingTranslucent
: mFixedMaterialInstanceIndex.colorGradingOpaque;
std::tie(mi, fixedIndex) = mMaterialInstanceManager.getFixedMaterialInstance(ma);
const SamplerParams params = SamplerParams{
.filterMag = SamplerMagFilter::LINEAR,
@@ -3809,13 +3823,15 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::vsmMipmapPass(FrameGraph& fg
auto const& inDesc = resources.getDescriptor(data.in);
auto width = inDesc.width;
assert_invariant(width == inDesc.height);
uint32_t const dim = std::max(1u, width >> (level + 1));
int const dim = width >> (level + 1);
auto& material = getPostProcessMaterial("vsmMipmap");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
// 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 = { 0, 0, dim, dim };
backend::Viewport const scissor = { 1u, 1u, dim - 2u, dim - 2u };
FMaterialInstance* const mi = getMaterialInstance(ma);
mi->setParameter("color", in, SamplerParams{
+7
View File
@@ -486,6 +486,13 @@ 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};
+2 -2
View File
@@ -568,7 +568,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::isDepthMomentsVariant(variant);
cmd.info.rasterState.colorWrite = Variant::isPickingVariant(variant) || Variant::isVSMVariant(variant);
cmd.info.rasterState.depthWrite = true;
cmd.info.rasterState.depthFunc = RasterState::DepthFunc::GE;
cmd.info.rasterState.alphaToCoverage = false;
@@ -616,7 +616,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 & Variant::SRE);
static_assert(Variant::SPECIAL_SSR & Variant::SRE);
Variant renderableVariant{ variant };
// we can't have SSR and shadowing together by construction
+141 -136
View File
@@ -62,8 +62,6 @@ 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);
@@ -87,7 +85,7 @@ void ShadowMap::terminate(FEngine& engine) {
ShadowMap::~ShadowMap() = default;
void ShadowMap::initialize(size_t const lightIndex, ShadowType const shadowType, bool const vsm,
void ShadowMap::initialize(size_t const lightIndex, ShadowType const shadowType,
uint16_t const shadowIndex, uint8_t const face,
LightManager::ShadowOptions const* options) {
mLightIndex = lightIndex;
@@ -95,7 +93,6 @@ 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,
@@ -216,14 +213,12 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine,
* Final shadow map transform
*/
// Final shadow transform (focused warped light-space), world space to clip space
// Final shadow transform (focused warped light-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;
@@ -231,12 +226,14 @@ 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 canceled.
// if Wp[3][1] is 0, then LISPSM was cancelled.
if (useLispsm && Wp[3][1] != 0.0f) {
shaderParameters.texelSizeAtOneMeterWs = texelSizeWorldSpace(S, shadowMapInfo.shadowDimension);
shaderParameters.texelSizeAtOneMeterWs =
texelSizeWorldSpace(Wp, mat4f(Mt * F), shadowMapInfo.shadowDimension);
} else {
// We know we're using an ortho projection
shaderParameters.texelSizeAtOneMeterWs = texelSizeWorldSpace(S.upperLeft(), shadowMapInfo.shadowDimension);
shaderParameters.texelSizeAtOneMeterWs =
texelSizeWorldSpace(St.upperLeft(), shadowMapInfo.shadowDimension);
}
if (!shadowMapInfo.vsm) {
shaderParameters.lightSpace = St;
@@ -1120,72 +1117,90 @@ bool ShadowMap::intersectSegmentWithPlanarQuad(float3& UTILS_RESTRICT p,
return hit;
}
float2 ShadowMap::texelSizeWorldSpace(const mat3f& clipFromWorld, uint16_t shadowDimension) noexcept {
float ShadowMap::texelSizeWorldSpace(const mat3f& worldToShadowTexture,
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 shadowMapFromWorld,
// orthographic projections. We just need to inverse worldToShadowTexture,
// which is guaranteed to be orthographic.
// The two first columns give us how a texel maps in world-space.
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;
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);
return s;
}
/**
* 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;
}
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.
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.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;
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));
return s;
}
template<typename Visitor>
void ShadowMap::visitScene(const FScene& scene, uint32_t const visibleLayers, Visitor visitor) noexcept {
template<typename Casters, typename Receivers>
void ShadowMap::visitScene(const FScene& scene, uint32_t const visibleLayers,
Casters casters, Receivers receivers) noexcept {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
using State = FRenderableManager::Visibility;
@@ -1198,11 +1213,14 @@ void ShadowMap::visitScene(const FScene& scene, uint32_t const visibleLayers, Vi
size_t const c = soa.size();
for (size_t i = 0; i < c; i++) {
if (layers[i] & visibleLayers) {
Aabb const aabb{
worldAABBCenter[i] - worldAABBExtent[i],
worldAABBCenter[i] + worldAABBExtent[i]
};
visitor(aabb, visibleMasks[i], visibility[i]);
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]);
}
}
}
}
@@ -1221,15 +1239,13 @@ ShadowMap::SceneInfo::SceneInfo(
wsShadowCastersVolume = {};
wsShadowReceiversVolume = {};
visitScene(scene, visibleLayers,
[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);
}
[&](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);
}
);
}
@@ -1243,20 +1259,18 @@ 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 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);
}
[&](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);
}
}
);
@@ -1271,15 +1285,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 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 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 receiver, Culler::result_type) {
}
);
}
@@ -1289,56 +1303,47 @@ void ShadowMap::setAllocation(uint8_t const layer, backend::Viewport viewport) n
}
backend::Viewport ShadowMap::getViewport() const noexcept {
// 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)
// 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).
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 {
// This is used while rendering the shadowmap.
// 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.
const uint32_t dim = mOptions->mapSize;
const uint16_t border = 1u;
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};
}
switch (mShadowType) {
case ShadowType::DIRECTIONAL:
return { mOffset.x + border, mOffset.y + border, dim - 2u * border, dim - 2u * border };
case ShadowType::SPOT:
case ShadowType::POINT: {
// Render into the border (which will get the adjacent faces texels)
return {mOffset.x, mOffset.y, dim, dim};
}
case ShadowType::POINT:
return { mOffset.x, mOffset.y, dim, dim };
}
return {};
}
float4 ShadowMap::getClampToEdgeCoords(ShadowMapInfo const& shadowMapInfo) const noexcept {
// 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)
float border; // shadowmap border in texels
switch (mShadowType) {
case ShadowType::DIRECTIONAL:
// 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).
// For directional lights, we need to allow the sampling to reach the border, it
// happens when "focus shadow casters" is used for instance.
border = 0.5f;
break;
case ShadowType::SPOT:
case ShadowType::POINT:
// For point-light, the border is only needed for bilinear filtering (of the other faces)
// 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.
border = 1.0f;
break;
}
@@ -1367,8 +1372,8 @@ void ShadowMap::prepareCamera(Transaction const& transaction,
}
void ShadowMap::prepareViewport(Transaction const& transaction,
backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept {
ShadowMapDescriptorSet::prepareViewport(transaction, physicalViewport, logicalViewport);
backend::Viewport const& viewport) noexcept {
ShadowMapDescriptorSet::prepareViewport(transaction, viewport);
}
void ShadowMap::prepareTime(Transaction const& transaction,
@@ -1382,8 +1387,8 @@ void ShadowMap::prepareMaterialGlobals(Transaction const& transaction,
}
void ShadowMap::prepareShadowMapping(Transaction const& transaction,
float const vsmExponent, float const vsmMaxMoment) noexcept {
ShadowMapDescriptorSet::prepareShadowMapping(transaction, vsmExponent, vsmMaxMoment);
bool const highPrecision) noexcept {
ShadowMapDescriptorSet::prepareShadowMapping(transaction, highPrecision);
}
ShadowMapDescriptorSet::Transaction ShadowMap::open(DriverApi& driver) noexcept {
+14 -15
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, bool vsm, uint16_t shadowIndex, uint8_t face,
void initialize(size_t lightIndex, ShadowType shadowType, uint16_t shadowIndex, uint8_t face,
LightManager::ShadowOptions const* options);
struct ShaderParameters {
math::mat4f lightSpace{};
math::float4 lightFromWorldZ{};
math::float4 scissorNormalized{};
math::float2 texelSizeAtOneMeterWs{};
float texelSizeAtOneMeterWs{};
};
// Call once per frame if the light, scene (or visible layers) or camera changes.
@@ -173,10 +173,7 @@ public:
static void updateSceneInfoSpot(const math::mat4f& Mv, FScene const& scene,
SceneInfo& sceneInfo);
LightManager::ShadowOptions const& getShadowOptions() const noexcept {
assert_invariant(mOptions);
return *mOptions;
}
LightManager::ShadowOptions const* getShadowOptions() const noexcept { return mOptions; }
size_t getLightIndex() const { return mLightIndex; }
uint16_t getShadowIndex() const { return mShadowIndex; }
void setAllocation(uint8_t layer, backend::Viewport viewport) noexcept;
@@ -196,13 +193,13 @@ public:
static void prepareCamera(Transaction const& transaction,
FEngine const& engine, const CameraInfo& cameraInfo) noexcept;
static void prepareViewport(Transaction const& transaction,
backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept;
backend::Viewport const& viewport) 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,
float vsmExponent, float vsmMaxMoment) noexcept;
bool highPrecision) 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;
@@ -276,8 +273,9 @@ private:
static inline math::float4 computeBoundingSphere(
math::float3 const* vertices, size_t count) noexcept;
template<typename Visitor>
static void visitScene(FScene const& scene, uint32_t visibleLayers, Visitor visitor) noexcept;
template<typename Casters, typename Receivers>
static void visitScene(FScene const& scene, uint32_t visibleLayers,
Casters casters, Receivers receivers) noexcept;
static inline Aabb compute2DBounds(const math::mat4f& lightView,
math::float3 const* wsVertices, size_t count) noexcept;
@@ -321,8 +319,11 @@ private:
math::float4 getClampToEdgeCoords(ShadowMapInfo const& shadowMapInfo) const 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 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 constexpr Segment sBoxSegments[12] = {
{ 0, 1 }, { 1, 3 }, { 3, 2 }, { 2, 0 },
@@ -351,11 +352,9 @@ 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
+71 -73
View File
@@ -151,14 +151,12 @@ 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);
@@ -232,12 +230,15 @@ 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
VsmShadowOptions const& vsmShadowOptions = view.getVsmShadowOptions();
TextureAtlasRequirements const textureRequirements = mTextureAtlasRequirements;
const TextureAtlasRequirements textureRequirements = mTextureAtlasRequirements;
assert_invariant(textureRequirements.layers <= CONFIG_MAX_SHADOW_LAYERS);
// -------------------------------------------------------------------------------------------
@@ -257,6 +258,8 @@ 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());
@@ -389,18 +392,16 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
auto transaction = ShadowMap::open(driver);
ShadowMap::prepareCamera(transaction, engine, cameraInfo);
ShadowMap::prepareViewport(transaction,
{ 0, 0, textureRequirements.size, textureRequirements.size },
shadowMap.getViewport());
ShadowMap::prepareViewport(transaction, shadowMap.getViewport());
ShadowMap::prepareTime(transaction, engine, userTime);
ShadowMap::prepareMaterialGlobals(transaction, view.getMaterialGlobals());
ShadowMap::prepareShadowMapping(transaction,
getWrapExponentEVSM(vsmShadowOptions, shadowMap.getShadowOptions()),
getMaxMomentEVSM(vsmShadowOptions));
vsmShadowOptions.highPrecision);
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
@@ -429,10 +430,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);
}
@@ -464,10 +465,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
@@ -507,9 +508,10 @@ 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 = textureRequirements.clearColor;
renderTargetDesc.clearColor = vsmClearColor;
renderTargetDesc.samples = msaaSamples;
if (UTILS_UNLIKELY(blur)) {
@@ -528,14 +530,16 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
.attachments = {
.color = { data.tempBlurSrc },
.depth = depth },
.clearColor = textureRequirements.clearColor,
.clearColor = vsmClearColor,
.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;
}
@@ -590,27 +594,27 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
// the whole layer. Blurring should happen per shadowmap, not for the whole
// layer.
// FIXME: this Gaussian blur is not precise enough for EVSM
const float blurWidth = options.vsm.blurWidth;
const float blurWidth = options->vsm.blurWidth;
if (blurWidth > 0.0f) {
const float sigma = (blurWidth + 1.0f) / 6.0f;
size_t kernelWidth = size_t(std::ceil((blurWidth - 5.0f) / 4.0f));
size_t kernelWidth = std::ceil((blurWidth - 5.0f) / 4.0f);
kernelWidth = kernelWidth * 4 + 5;
ppm.gaussianBlurPass(fg,
shadowPass->tempBlurSrc,
shadowPass->output,
false, kernelWidth, sigma);
}
}
// 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);
// 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);
}
}
}
}
@@ -622,8 +626,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng
FView& view, CameraInfo cameraInfo, FScene::RenderableSoa& renderableData,
FScene::LightSoa const& lightData, ShadowMap::SceneInfo sceneInfo) noexcept {
FScene const* const scene = view.getScene();
auto const& vsmShadowOptions = view.getVsmShadowOptions();
FScene* scene = view.getScene();
auto& lcm = engine.getLightManager();
FLightManager::Instance const directionalLight = lightData.elementAt<FScene::LIGHT_INSTANCE>(0);
@@ -647,7 +650,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng
};
bool hasVisibleShadows = false;
utils::Slice<ShadowMap> const cascadedShadowMaps = getCascadedShadowMap();
utils::Slice<ShadowMap> 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.
@@ -739,17 +742,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).
float2 const wsTexelSize = shaderParameters.texelSizeAtOneMeterWs;
const float 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 = wsTexelSize * normalBias;
s.shadows[shadowIndex].normalBias = normalBias * wsTexelSize;
s.shadows[shadowIndex].texelSizeAtOneMeter = wsTexelSize;
s.shadows[shadowIndex].elvsm = options.vsm.elvsm;
s.shadows[shadowIndex].vsmExponent = getWrapExponentEVSM(vsmShadowOptions, options);
s.shadows[shadowIndex].bulbRadiusLs =
mSoftShadowOptions.penumbraScale * options.shadowBulbRadius / length(wsTexelSize);
mSoftShadowOptions.penumbraScale * options.shadowBulbRadius / wsTexelSize;
shadowTechnique |= ShadowTechnique::SHADOW_MAP;
cascadeHasVisibleShadows |= 0x1u << i;
@@ -799,8 +802,10 @@ void ShadowMapManager::updateSpotVisibilityMasks(
const bool visSpotShadowRenderable = v.castShadows && inVisibleLayer &&
(!v.culling || (mask & VISIBLE_DYN_SHADOW_RENDERABLE));
visibleMask[i] &= ~Culler::result_type(VISIBLE_DYN_SHADOW_RENDERABLE);
visibleMask[i] |= Culler::result_type(visSpotShadowRenderable << VISIBLE_DYN_SHADOW_RENDERABLE_BIT);
using Type = Culler::result_type;
visibleMask[i] &= ~Type(VISIBLE_DYN_SHADOW_RENDERABLE);
visibleMask[i] |= Type(visSpotShadowRenderable << VISIBLE_DYN_SHADOW_RENDERABLE_BIT);
}
}
@@ -809,14 +814,13 @@ 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& options = shadowMap.getShadowOptions();
auto const& vsmShadowOptions = view.getVsmShadowOptions();
FLightManager::ShadowOptions const* const options = shadowMap.getShadowOptions();
// 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,
@@ -829,9 +833,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 float2 wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs;
const float 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();
@@ -841,12 +845,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].vsmExponent = getWrapExponentEVSM(vsmShadowOptions, options);
s.shadows[shadowIndex].elvsm = options->vsm.elvsm;
s.shadows[shadowIndex].bulbRadiusLs =
mSoftShadowOptions.penumbraScale * options.shadowBulbRadius
/ length(wsTexelSizeAtOneMeter);
mSoftShadowOptions.penumbraScale * options->shadowBulbRadius
/ wsTexelSizeAtOneMeter;
}
}
@@ -903,14 +907,13 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap,
const uint8_t face = shadowMap.getFace();
const size_t lightIndex = shadowMap.getLightIndex();
FLightManager::ShadowOptions const& options = shadowMap.getShadowOptions();
auto const& vsmShadowOptions = view.getVsmShadowOptions();
FLightManager::ShadowOptions const* const options = shadowMap.getShadowOptions();
// 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,
@@ -923,9 +926,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 float2 wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs;
const float 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();
@@ -935,12 +938,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].vsmExponent = getWrapExponentEVSM(vsmShadowOptions, options);
s.shadows[shadowIndex].elvsm = options->vsm.elvsm;
s.shadows[shadowIndex].bulbRadiusLs =
mSoftShadowOptions.penumbraScale * options.shadowBulbRadius
/ length(wsTexelSizeAtOneMeter);
mSoftShadowOptions.penumbraScale * options->shadowBulbRadius
/ wsTexelSizeAtOneMeter;
}
}
@@ -995,7 +998,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(FEngine
lightData.data<FScene::SHADOW_INFO>());
ShadowTechnique shadowTechnique{};
utils::Slice<const ShadowMap> const spotShadowMaps = getSpotShadowMaps();
utils::Slice<const ShadowMap> spotShadowMaps = getSpotShadowMaps();
if (!spotShadowMaps.empty()) {
shadowTechnique |= ShadowTechnique::SHADOW_MAP;
for (ShadowMap const& shadowMap : spotShadowMaps) {
@@ -1035,14 +1038,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;
@@ -1052,7 +1055,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);
@@ -1088,7 +1091,6 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
msaaSamples = 1;
}
float4 clearColor{};
TextureFormat format = TextureFormat::DEPTH16;
if (view.hasVSM()) {
if (vsmShadowOptions.highPrecision) {
@@ -1096,9 +1098,6 @@ 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();
@@ -1120,8 +1119,7 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
layersNeeded,
mipLevels,
msaaSamples,
format,
clearColor
format
};
}
-26
View File
@@ -47,12 +47,9 @@
#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>
@@ -148,28 +145,6 @@ 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);
@@ -241,7 +216,6 @@ private:
uint8_t levels = 0;
uint8_t msaaSamples = 1;
backend::TextureFormat format = backend::TextureFormat::DEPTH16;
math::float4 clearColor{};
} mTextureAtlasRequirements;
SoftShadowOptions mSoftShadowOptions;
+6 -7
View File
@@ -798,12 +798,6 @@ void FEngine::submitFrame() {
void FEngine::flush() {
// flush the command buffer
flushCommandBuffer(mCommandBufferQueue);
// In single-threaded mode, we have to call execute() to drain the command
// buffer to really free up space
if constexpr (!UTILS_HAS_THREADING) {
execute();
}
}
void FEngine::flushAndWait() {
@@ -926,7 +920,12 @@ int FEngine::loop() {
void FEngine::flushCommandBuffer(CommandBufferQueue& commandBufferQueue) const {
getDriver().purge();
commandBufferQueue.flush();
commandBufferQueue.flush([this](void* begin, void* end) {
UTILS_UNUSED FEngine* engine = const_cast<FEngine*>(this);
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
engine->getDriverApi().debugPrintHistogram(begin, end);
#endif
});
}
const FMaterial* FEngine::getSkyboxMaterial() const noexcept {
+2 -2
View File
@@ -257,9 +257,9 @@ filament::DescriptorSetLayout const& FMaterial::getPerViewDescriptorSetLayout(
}
// mDefinition.perViewDescriptorSetLayout{Vsm} is already resolved for MaterialDomain
if (useVsmDescriptorSetLayout) {
return mDefinition.perViewDescriptorSetLayoutS2d;
return mDefinition.perViewDescriptorSetLayoutVsm;
}
return mDefinition.perViewDescriptorSetLayoutPcf;
return mDefinition.perViewDescriptorSetLayout;
}
void FMaterial::compile(CompilerPriorityQueue const priority,
+3 -3
View File
@@ -139,7 +139,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.perViewDescriptorSetLayoutPcf;
return mDefinition.perViewDescriptorSetLayout;
}
DescriptorSetLayout const& getPerViewDescriptorSetLayout(
@@ -378,11 +378,11 @@ private:
Variant variant = {}) const noexcept;
bool isSharedVariant(Variant const variant) const {
// HACK: The default material "should" have MNT | DEP, but then we'd have to compile it as a
// 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::MNT | Variant::DEP;
constexpr Variant::type_t vsmAndDep = Variant::VSM | Variant::DEP;
return mDefinition.materialDomain == MaterialDomain::SURFACE && !mIsDefaultMaterial &&
!mDefinition.hasCustomDepthShader && Variant::isValidDepthVariant(variant) &&
(variant.key & vsmAndDep) != vsmAndDep;
-4
View File
@@ -93,10 +93,6 @@ 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 };
+8 -2
View File
@@ -969,7 +969,10 @@ void FRenderer::renderJob(DriverApi& driver, RootArenaScope& rootArenaScope, FVi
variant.setDirectionalLighting(view.hasDirectionalLighting());
variant.setDynamicLighting(view.hasDynamicLighting());
variant.setFog(view.hasFog());
variant.setShadowSampler2D(view.hasShadowing() && view.getShadowType() != ShadowType::PCF);
// 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.setStereo(view.hasStereo());
/*
@@ -978,7 +981,10 @@ void FRenderer::renderJob(DriverApi& driver, RootArenaScope& rootArenaScope, FVi
if (view.needsShadowMap()) {
Variant shadowVariant(Variant::DEPTH_VARIANT);
shadowVariant.setDepthMoments(view.getShadowType() == ShadowType::VSM);
// 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);
auto shadows = view.renderShadowMaps(engine, fg, cameraInfo, mShaderUserTime,
RenderPassBuilder{ commandArena }
+5 -4
View File
@@ -76,7 +76,6 @@
#include <cmath>
#include <chrono>
#include <functional>
#include <limits>
#include <memory>
#include <new>
#include <ratio>
@@ -144,7 +143,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<DebugState>(mDebugState)]() -> DebugRegistry::DataSource {
[weak = std::weak_ptr(mDebugState)]() -> DebugRegistry::DataSource {
// the View could have been destroyed by the time we do this
auto const state = weak.lock();
if (!state) {
@@ -1132,6 +1131,8 @@ 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;
@@ -1147,8 +1148,8 @@ void FView::prepareShadowMapping() const noexcept {
break;
case ShadowType::VSM:
s.shadowSamplingType = SHADOW_SAMPLING_RUNTIME_EVSM;
s.vsmExponent = 0; // this is only used when rendering the shadowmap, not when using it
s.vsmMaxMoment = ShadowMapManager::getMaxMomentEVSM(mVsmShadowOptions);
s.vsmExponent = mVsmShadowOptions.highPrecision ? high : low;
s.vsmDepthScale = mVsmShadowOptions.minVarianceScale * 0.01f * s.vsmExponent;
s.vsmLightBleedReduction = mVsmShadowOptions.lightBleedReduction;
break;
case ShadowType::DPCF:
+6 -7
View File
@@ -28,11 +28,9 @@
#include <utils/debug.h>
#include <math/half.h>
#include <math/vec4.h>
#include <array>
#include <limits>
namespace filament {
@@ -76,8 +74,8 @@ void ShadowMapDescriptorSet::prepareLodBias(Transaction const& transaction, floa
}
void ShadowMapDescriptorSet::prepareViewport(Transaction const& transaction,
backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept {
PerViewDescriptorSetUtils::prepareViewport(edit(transaction), physicalViewport, logicalViewport);
backend::Viewport const& viewport) noexcept {
PerViewDescriptorSetUtils::prepareViewport(edit(transaction), viewport, viewport);
// TODO: offset calculation is now different
}
@@ -92,10 +90,11 @@ void ShadowMapDescriptorSet::prepareMaterialGlobals(Transaction const& transacti
}
void ShadowMapDescriptorSet::prepareShadowMapping(Transaction const& transaction,
float const vsmExponent, float const vsmMaxMoment) noexcept {
bool const highPrecision) noexcept {
auto& s = edit(transaction);
s.vsmExponent = vsmExponent;
s.vsmMaxMoment = vsmMaxMoment;
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;
}
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& physicalViewport, backend::Viewport const& logicalViewport) noexcept;
backend::Viewport const& viewport) 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,
float vsmExponent, float vsmMaxMoment) noexcept;
bool highPrecision) noexcept;
static Transaction open(backend::DriverApi& driver) noexcept;
+4 -26
View File
@@ -19,8 +19,7 @@ material {
{
name : color,
target : color,
type : float4,
precision : high
type : float4
}
],
domain : postprocess,
@@ -31,29 +30,8 @@ material {
fragment {
void postProcess(inout PostProcessInputs postProcess) {
// 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);
highp vec2 uv = gl_FragCoord.xy * materialParams.uvscale;
postProcess.color = textureLod(materialParams_color,
vec3(uv, materialParams.layer), 0.0);
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
Pod::Spec.new do |spec|
spec.name = "Filament"
spec.version = "1.69.5"
spec.version = "1.69.3"
spec.license = { :type => "Apache 2.0", :file => "LICENSE" }
spec.homepage = "https://google.github.io/filament"
spec.authors = "Google LLC."
spec.summary = "Filament is a real-time physically based rendering engine for Android, iOS, Windows, Linux, macOS, and WASM/WebGL."
spec.platform = :ios, "11.0"
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.69.5/filament-v1.69.5-ios.tgz" }
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.69.3/filament-v1.69.3-ios.tgz" }
spec.libraries = 'c++'
@@ -36,7 +36,7 @@ backend::DescriptorSetLayout const& getPerRenderableLayout() noexcept;
backend::DescriptorSetLayout getPerViewDescriptorSetLayout(
MaterialDomain domain,
bool isLit, bool isSSR, bool hasFog,
bool isShadowSampler2D) noexcept;
bool isVSM) 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 vsmMaxMoment;
float vsmDepthScale;
float vsmLightBleedReduction;
uint32_t shadowSamplingType; // 0: vsm, 1: dpcf
@@ -299,12 +299,13 @@ 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
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
float normalBias; // 4
bool elvsm; // 4
uint32_t layer; // 4
uint32_t reserved1; // 4
uint32_t reserved2; // 4
};
ShadowData shadows[CONFIG_MAX_SHADOWMAPS];
@@ -48,21 +48,20 @@ struct Variant {
// SRE: Shadow Receiver
// SKN: Skinning
// DEP: Depth only
// FOG: Fog (standard)
// PCK: Picking (depth)
// MNT: Output depth moments (depth)
// S2D: Sampler type for shadows (0: samplerShadowArray, 1: sampler2DArray) (standard)
// FOG: Fog
// PCK: Picking (depth variant only)
// VSM: Variance shadow maps (depth) / sampler type (standard)
// STE: Instanced stereo rendering
//
// X: either 1 or 0
// +-----+-----+-----+-----+-----+-----+-----+-----+
// Variant | STE | S2D | FOG | DEP | SKN | SRE | DYN | DIR | 256
// Variant | STE | VSM | FOG | DEP | SKN | SRE | DYN | DIR | 256
// +-----+-----+-----+-----+-----+-----+-----+-----+
// MNT PCK
// PCK
//
// Standard variants:
// +-----+-----+-----+-----+-----+-----+-----+-----+
// | STE | S2D | FOG | 0 | SKN | SRE | DYN | DIR | 128 - 44 = 84
// | STE | VSM | 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
@@ -73,7 +72,7 @@ struct Variant {
//
// Depth variants:
// +-----+-----+-----+-----+-----+-----+-----+-----+
// | STE | MNT | PCK | 1 | SKN | 0 | 0 | 0 | 16 - 4 = 12
// | STE | VSM | 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
@@ -97,15 +96,13 @@ 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 S2D = 0x40; // sampler type
static constexpr type_t MNT = 0x40; // variance shadow maps
static constexpr type_t VSM = 0x40; // variance shadow maps / sampler type
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_VARIANT= S2D | SRE ;
static constexpr type_t SPECIAL_SSR_MASK = STE | S2D | DEP | SRE | DYN | DIR;
static constexpr type_t SPECIAL_SSR = VSM | SRE; // screen-space reflections variant
static constexpr type_t STANDARD_MASK = DEP;
static constexpr type_t STANDARD_VARIANT = 0u;
@@ -130,30 +127,29 @@ 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 setShadowSampler2D(bool v) noexcept { set(v, S2D); }
void setDepthMoments(bool v) noexcept { set(v, MNT); }
void setVsm(bool v) noexcept { set(v, VSM); }
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 = MNT | PCK | DEP | SRE | DYN | DIR;
constexpr type_t RESERVED_VALUE = MNT | PCK | DEP;
constexpr type_t RESERVED_MASK = VSM | PCK | DEP | SRE | DYN | DIR;
constexpr type_t RESERVED_VALUE = VSM | 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 = S2D | FOG | SRE | DYN | DIR;
constexpr type_t RESERVED0_VALUE = S2D | FOG | SRE;
constexpr type_t RESERVED0_MASK = VSM | FOG | SRE | DYN | DIR;
constexpr type_t RESERVED0_VALUE = VSM | FOG | SRE;
// can't have shadow receiver if we don't have any lighting
constexpr type_t RESERVED1_MASK = S2D | SRE | DYN | DIR;
constexpr type_t RESERVED1_MASK = VSM | SRE | DYN | DIR;
constexpr type_t RESERVED1_VALUE = SRE;
// can't have VSM without shadow receiver
constexpr type_t RESERVED2_MASK = S2D | SRE;
constexpr type_t RESERVED2_VALUE = S2D;
constexpr type_t RESERVED2_MASK = VSM | SRE;
constexpr type_t RESERVED2_VALUE = VSM;
return ((variant.key & STANDARD_MASK) == STANDARD_VARIANT) &&
((variant.key & RESERVED0_MASK) != RESERVED0_VALUE) &&
@@ -178,15 +174,11 @@ struct Variant {
}
static constexpr bool isSSRVariant(Variant variant) noexcept {
return (variant.key & SPECIAL_SSR_MASK) == SPECIAL_SSR_VARIANT;
return (variant.key & (STE | VSM | DEP | SRE | DYN | DIR)) == (VSM | SRE);
}
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 isVSMVariant(Variant variant) noexcept {
return !isSSRVariant(variant) && ((variant.key & VSM) == VSM);
}
static constexpr bool isShadowReceiverVariant(Variant variant) noexcept {
@@ -210,13 +202,13 @@ struct Variant {
// vertex shader.
if ((variant.key & STANDARD_MASK) == STANDARD_VARIANT) {
if (isSSRVariant(variant)) {
variant.key &= ~SPECIAL_SSR_VARIANT;
variant.key &= ~(VSM | SRE);
}
return variant & (STE | SKN | SRE | DYN | DIR);
}
if ((variant.key & DEPTH_MASK) == DEPTH_VARIANT) {
// Only MNT, skinning, and stereo affect the vertex shader's DEPTH variant
return variant & (STE | MNT | SKN | DEP);
// Only VSM, skinning, and stereo affect the vertex shader's DEPTH variant
return variant & (STE | VSM | SKN | DEP);
}
return {};
}
@@ -225,11 +217,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 & (S2D | FOG | SRE | DYN | DIR);
return variant & (VSM | FOG | SRE | DYN | DIR);
}
if ((variant.key & DEPTH_MASK) == DEPTH_VARIANT) {
// Only VSM & PICKING affects the fragment shader's DEPTH variant
return variant & (MNT | PCK | DEP);
return variant & (VSM | PCK | DEP);
}
return {};
}
@@ -238,8 +230,8 @@ struct Variant {
// special case for depth variant
if (isValidDepthVariant(variant)) {
if (!isLit) {
// if we're unlit, we never need the MNT variant
return variant & ~MNT;
// if we're unlit, we never need the VSM variant
return variant & ~VSM;
}
return variant;
}
@@ -250,9 +242,9 @@ struct Variant {
// when the shading mode is unlit, remove all the lighting variants
return variant & UNLIT_MASK;
}
// if shadow receiver is disabled, we pick the shadow sampler
// if shadow receiver is disabled, turn off VSM
if (!(variant.key & SRE)) {
return variant & ~S2D;
return variant & ~VSM;
}
return variant;
}
+4 -3
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 isShadowSampler2D) noexcept {
bool const isVSM) noexcept {
switch (domain) {
case MaterialDomain::SURFACE: {
@@ -255,7 +255,7 @@ DescriptorSetLayout getPerViewDescriptorSetLayout(
}
// change the SHADOW_MAP descriptor type for VSM
if (isShadowSampler2D) {
if (isVSM) {
auto const pos = std::find_if(layout.descriptors.begin(), layout.descriptors.end(),
[](auto const& v) {
return v.binding == PerViewBindingPoints::SHADOW_MAP;
@@ -285,7 +285,8 @@ 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::isShadowSampler2DVariant(variant));
return getPerViewDescriptorSetLayout(domain, isLit, isSSR, hasFog,
Variant::isVSMVariant(variant));
}
DescriptorType getDescriptorType(SamplerType const type, SamplerFormat const format) {
+16 -21
View File
@@ -32,46 +32,41 @@ 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)) {
// depth variants can have their MNT bit filtered
if (filterMask & uint32_t(UserVariantFilterBit::VSM)) {
variant.key &= ~MNT;
}
} else {
if (!isValidDepthVariant(variant)) {
// we can't remove FOG from depth variants, this would, in fact, remove picking
if (filterMask & uint32_t(UserVariantFilterBit::FOG)) {
if (filterMask & (uint32_t)UserVariantFilterBit::FOG) {
variant.key &= ~FOG;
}
} else {
// depth variants can have their VSM bit filtered
if (filterMask & (uint32_t)UserVariantFilterBit::VSM) {
variant.key &= ~VSM;
}
}
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 &= ~S2D;
if (filterMask & (uint32_t)UserVariantFilterBit::VSM) {
variant.key &= ~VSM;
}
} else {
// see if we need to filter out the SSR variants
if (filterMask & uint32_t(UserVariantFilterBit::SSR)) {
variant.key &= ~SPECIAL_SSR_VARIANT;
if (filterMask & (uint32_t)UserVariantFilterBit::SSR) {
variant.key &= ~SPECIAL_SSR;
}
}
return variant;
+1 -2
View File
@@ -60,8 +60,7 @@ 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::isShadowSampler2DVariant(variant) ||
filament::Variant::isDepthMomentsVariant(variant));
filament::Variant::isVSMVariant(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 sibShadowSamplerPcf{ SamplerInterfaceBlock::Builder()
static SamplerInterfaceBlock const sibPcf{ 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 sibShadowSampler2D{ SamplerInterfaceBlock::Builder()
static SamplerInterfaceBlock const sibVsm{ 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::isShadowSampler2DVariant(variant)) {
return sibShadowSampler2D;
} else if (Variant::isVSMVariant(variant)) {
return sibVsm;
} else {
return sibShadowSamplerPcf;
return sibPcf;
}
}
+1 -1
View File
@@ -175,7 +175,7 @@ BufferInterfaceBlock const& UibGenerator::getPerViewUib() noexcept {
// VSM shadows [variant: VSM]
// ------------------------------------------------------------------------------------
{ "vsmExponent", 0, Type::FLOAT },
{ "vsmMaxMoment", 0, Type::FLOAT, Precision::HIGH },
{ "vsmDepthScale", 0, Type::FLOAT },
{ "vsmLightBleedReduction", 0, Type::FLOAT },
{ "shadowSamplingType", 0, Type::UINT },
-2
View File
@@ -60,13 +60,11 @@
#define GLTFIO_WARN(msg) slog.w << msg << io::endl
#endif
#ifndef GLTFIO_USE_FILESYSTEM
#if defined(__EMSCRIPTEN__) || defined(__ANDROID__) || defined(FILAMENT_IOS)
#define GLTFIO_USE_FILESYSTEM 0
#else
#define GLTFIO_USE_FILESYSTEM 1
#endif
#endif
namespace utils {
class NameComponentManager;
+1 -2
View File
@@ -48,11 +48,10 @@ 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);
}
+1 -6
View File
@@ -17,7 +17,6 @@
// 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
@@ -99,11 +98,7 @@ namespace utils {
void JobSystem::setThreadName(const char* name) noexcept {
#if defined(__linux__)
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);
pthread_setname_np(pthread_self(), name);
#elif defined(__APPLE__)
pthread_setname_np(name);
#elif defined(WIN32)
+16 -22
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 vec4 computeDepthMomentsVSM(const highp float depth);
highp vec2 computeDepthMomentsVSM(const highp float depth);
void main() {
filament_lodBias = frameUniforms.lodBias;
@@ -53,7 +53,9 @@ 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;
fragColor = computeDepthMomentsVSM(depth);
depth = exp(frameUniforms.vsmExponent * depth);
fragColor.xy = computeDepthMomentsVSM(depth);
fragColor.zw = computeDepthMomentsVSM(-1.0 / depth); // requires at least RGBA16F
#elif defined(VARIANT_HAS_PICKING)
#if FILAMENT_EFFECTIVE_VERSION == 100
outPicking.a = mod(float(object_uniforms_objectId / 65536), 256.0) / 255.0;
@@ -72,30 +74,22 @@ void main() {
#endif
}
#if MATERIAL_FEATURE_LEVEL > 0
highp vec4 computeDepthMomentsVSM(const highp float depth) {
highp vec2 computeDepthMomentsVSM(const highp float depth) {
// computes the moments
// See GPU Gems 3
// https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-8-summed-area-variance-shadow-maps
// computes the first two moments
float c = frameUniforms.vsmExponent;
highp float MAX_MOMENT = frameUniforms.vsmMaxMoment;
highp vec2 moments;
// wrap depth for EVSM
highp float z = exp(c * depth);
// the first moment is just the depth (average)
moments.x = depth;
// compute EVSM moments
highp vec2 m1 = vec2(z, -1.0 / z);
highp vec2 m2 = m1 * m1;
// compute the 2nd moment over the pixel extents.
moments.y = depth * depth;
// 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);
// 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);
return vec4(m1.x, m2.x, m1.y, m2.y);
return moments;
}
#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
highp vec2 bias = shadowUniforms.shadows[index].normalBias * zLight;
float bias = shadowUniforms.shadows[index].normalBias * zLight;
return computeLightSpacePosition(getWorldPosition(), getWorldGeometricNormalVector(),
dir, bias, lightFromWorldMatrix);
+69 -92
View File
@@ -335,91 +335,6 @@ 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
//------------------------------------------------------------------------------
@@ -494,6 +409,70 @@ 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
//------------------------------------------------------------------------------
@@ -550,9 +529,6 @@ 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)
@@ -563,8 +539,9 @@ 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) {
return ShadowSample_VSM(DIRECTIONAL, shadowMap, scissorNormalized, layer, index,
shadowPosition, zLight);
bool elvsm = shadowUniforms.shadows[index].elvsm;
return ShadowSample_VSM(elvsm, shadowMap, scissorNormalized, layer,
shadowPosition);
}
if (frameUniforms.shadowSamplingType == SHADOW_SAMPLING_RUNTIME_DPCF) {
@@ -581,8 +558,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
+4 -43
View File
@@ -12,51 +12,12 @@
*/
highp vec4 computeLightSpacePosition(highp vec3 p, const highp vec3 n,
const highp vec3 dir, const highp vec2 b, highp_mat4 lightFromWorldMatrix) {
const highp vec3 dir, const float b, highp_mat4 lightFromWorldMatrix) {
#if !defined(VARIANT_HAS_VSM)
// --------------------------------------------------------------------------------------
// 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));
highp float cosTheta = saturate(dot(n, dir));
highp float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
p += n * (sinTheta * b);
#endif
return mulMat4x4Float3(lightFromWorldMatrix, p);
+3 -2
View File
@@ -5,12 +5,13 @@ struct ShadowData {
highp mat4 lightFromWorldMatrix;
highp vec4 lightFromWorldZ;
highp vec4 scissorNormalized;
mediump float texelSizeAtOneMeter;
mediump float bulbRadiusLs;
mediump float nearOverFarMinusNear;
highp vec2 normalBias;
mediump float normalBias;
bool elvsm;
mediump uint layer;
mediump float vsmExponent;
mediump uint reserved1;
mediump uint reserved2;
};
#endif

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