Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
277669e5fe |
@@ -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
|
||||
@@ -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} \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,7 +31,7 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.android.filament:filament-android:1.70.0'
|
||||
implementation 'com.google.android.filament:filament-android:1.69.2'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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.70.0'
|
||||
pod 'Filament', '~> 1.69.2'
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -7,18 +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
|
||||
|
||||
|
||||
## v1.69.3
|
||||
|
||||
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
GROUP=com.google.android.filament
|
||||
VERSION_NAME=1.70.0
|
||||
VERSION_NAME=1.69.2
|
||||
|
||||
POM_DESCRIPTION=Real-time physically based rendering engine for Android.
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+87
-266
@@ -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/<filename>\" > <filename></tt><br><br>")
|
||||
} else {
|
||||
message.append("<tt>adb pull $path/<filename> .</tt><br><br>")
|
||||
}
|
||||
|
||||
message.append("<b>--- PUSH TO DEVICE ---</b><br>")
|
||||
if (isInternal) {
|
||||
message.append("1. <tt>adb push <filename> /sdcard/Download/</tt><br>")
|
||||
message.append("2. <tt>adb shell \"run-as $packageName cp /sdcard/Download/<filename> files/\"</tt><br>")
|
||||
} else {
|
||||
message.append("<tt>adb push <filename> $path/</tt><br>")
|
||||
}
|
||||
message.append("<br>Note: Use underscores instead of spaces in <filename>.")
|
||||
|
||||
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/<filename>\" > <filename></tt><br><br>")
|
||||
} else {
|
||||
message.append("<tt>adb pull $path/<filename> .</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"
|
||||
*/
|
||||
|
||||
+7
-101
@@ -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)
|
||||
}
|
||||
|
||||
+8
-187
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+53
-47
@@ -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
@@ -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
@@ -181,7 +181,7 @@ important for <code>matc</code> (material compiler).</p>
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.android.filament:filament-android:1.69.4'
|
||||
implementation 'com.google.android.filament:filament-android:1.69.2'
|
||||
}
|
||||
</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', '~> 1.69.4'
|
||||
<pre><code class="language-shell">pod 'Filament', '~> 1.69.2'
|
||||
</code></pre>
|
||||
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
|
||||
<ul>
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -16,13 +16,12 @@ class SimulatedSkybox {
|
||||
this.ozone = 0.0;
|
||||
this.msFactors = [0.1, 0.5, 0.0];
|
||||
this.contrast = 1.0;
|
||||
this.nightColor = [0.0, 3.0e-9, 7.5e-9];
|
||||
this.nightColor = [0.0, 0.0003, 0.00075];
|
||||
this.shimmerControl = [0.0, 20.0, 0.1];
|
||||
this.cloudControl = [0.0, 0.1, 8000.0, 0.0];
|
||||
this.cloudControl2 = [0.0, 0.0, 0.0, 0.0];
|
||||
this.waterControl = [50.0, 1.0, 1.0, 4.0]; // x=Strength, y=Speed, z=DerivativeTrick, w=Octaves
|
||||
this.starControl = [0.001, 1.0, 350.0, 0.01]; // x=Density, y=Enabled, z=Frequency, w=PixelScale
|
||||
this.starIntensity = 1.0;
|
||||
this.focalLength = 24.0;
|
||||
this.height = 1000.0;
|
||||
this.planetRadius = 6360.0;
|
||||
@@ -44,7 +43,7 @@ class SimulatedSkybox {
|
||||
|
||||
// Milky Way Parameters
|
||||
// x=Intensity, y=Saturation, z=Unused
|
||||
this.milkyWayControl = [1.0, 1.2, 0.05];
|
||||
this.milkyWayControl = [1.0, 1.0, 0.07];
|
||||
this.milkyWayEnabled = true;
|
||||
this.milkyWayRotation = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Identity by default
|
||||
this.initEntity();
|
||||
@@ -454,11 +453,6 @@ class SimulatedSkybox {
|
||||
this.updateCoefficients();
|
||||
}
|
||||
|
||||
setStarIntensity(intensity) {
|
||||
this.starIntensity = Math.max(0.0, intensity);
|
||||
this.updateCoefficients();
|
||||
}
|
||||
|
||||
setMoonPosition(direction) {
|
||||
// normalize
|
||||
const len = Math.hypot(direction[0], direction[1], direction[2]);
|
||||
@@ -506,13 +500,6 @@ class SimulatedSkybox {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
setExposure(exposure) {
|
||||
this.exposure = exposure;
|
||||
this.updateCoefficients();
|
||||
}
|
||||
|
||||
updateCoefficients() {
|
||||
if (!this.materialInstance) {
|
||||
console.warn("updateCoefficients called before material loaded");
|
||||
@@ -598,7 +585,6 @@ class SimulatedSkybox {
|
||||
this.materialInstance.setFloat4Parameter('cloudControl2', new Float32Array(this.cloudControl2));
|
||||
this.materialInstance.setFloat4Parameter('waterControl', new Float32Array(this.waterControl));
|
||||
this.materialInstance.setFloat4Parameter('starControl', new Float32Array(this.starControl));
|
||||
this.materialInstance.setFloatParameter('starIntensity', this.starIntensity);
|
||||
|
||||
this.materialInstance.setFloatParameter('sunIntensity', physicalSunIntensity);
|
||||
|
||||
@@ -629,13 +615,12 @@ class SimulatedSkybox {
|
||||
|
||||
this.materialInstance.setFloatParameter('sunIntensity2', finalMoonIntensity);
|
||||
|
||||
// Scale Milky Way by Sun Intensity
|
||||
// Calibration: 1.0 User Intensity = 1.5e-3 cd/m^2 (Nits) approx.
|
||||
// Target: 1.5e-3 Nits. SunIntensity = 100,000.
|
||||
// Scale = 1.5e-3 / 1.0e5 = 1.5e-8.
|
||||
// Scale Milky Way by Sun Intensity (Pre-exposed) to match dynamic range
|
||||
// Calibration: 1.0 User Intensity ~ 0.025 Lux Relative (approx 2.5% of Sun Pixel Value at Day)
|
||||
// At Sunny 16 (Sun ~ 2.6), this gives ~0.065 pixel brightness, which is visible but dim.
|
||||
const mwIntensity = this.milkyWayEnabled ? this.milkyWayControl[0] : 0.0;
|
||||
const mwUniform = [
|
||||
mwIntensity * this.sunIntensity * 1.5e-8,
|
||||
mwIntensity * this.sunIntensity * 0.025,
|
||||
this.milkyWayControl[1],
|
||||
this.milkyWayControl[2]
|
||||
];
|
||||
@@ -649,7 +634,6 @@ class SimulatedSkybox {
|
||||
const moonHaloUpload = [...this.moonHalo];
|
||||
moonHaloUpload[2] *= moonRadConv;
|
||||
this.materialInstance.setFloat4Parameter('sunHalo2', new Float32Array(moonHaloUpload));
|
||||
this.materialInstance.setFloatParameter('exposure', this.exposure !== undefined ? this.exposure : 1.0);
|
||||
|
||||
// Solar Eclipse (CPU Calculation)
|
||||
const sunRadius = Math.acos(this.sunHalo[0]);
|
||||
|
||||
Binary file not shown.
+5
-26
@@ -218,7 +218,6 @@ class App {
|
||||
const exposure = this.getExposure();
|
||||
const preExposedIntensity = this.params.sunIntensity * exposure;
|
||||
this.skybox.setSunIntensity(preExposedIntensity);
|
||||
this.skybox.setExposure(exposure); // Update Skybox Exposure Uniform
|
||||
|
||||
// Moon Exposure
|
||||
if (this.mParams) {
|
||||
@@ -390,7 +389,7 @@ class App {
|
||||
};
|
||||
|
||||
mwFolder.add(this.mwParams, 'enabled').name('Enabled').onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'intensity', 0.0, 100.0).onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'intensity', 0.0, 5.0).onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'saturation', 0.0, 2.0).onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'blackPoint', 0.0, 0.5).name('Black Point').onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'siderealTime', 0.0, 24.0).name('Sidereal Time').listen().onChange(updateMW);
|
||||
@@ -465,23 +464,18 @@ class App {
|
||||
const starFolder = gui.addFolder('Stars');
|
||||
this.sParams = {
|
||||
enabled: true,
|
||||
density: 0.001,
|
||||
intensity: 0.0 // 2^0 = 1.0
|
||||
density: 0.001
|
||||
};
|
||||
// Initialize defaults (Density 0.001, Enabled True)
|
||||
sky.setStarControl(0.001, true);
|
||||
sky.setStarIntensity(Math.pow(2.0, 0.0));
|
||||
|
||||
const updateStars = () => {
|
||||
sky.setStarControl(this.sParams.density, this.sParams.enabled);
|
||||
sky.setStarIntensity(Math.pow(2.0, this.sParams.intensity));
|
||||
};
|
||||
|
||||
starFolder.add(this.sParams, 'enabled').name('Enabled').onChange(updateStars);
|
||||
starFolder.add(this.sParams, 'density', 0.0, 0.01, 0.0001).name('Density').onChange(updateStars);
|
||||
starFolder.add(this.sParams, 'intensity', 0.0, 24.0).name('Intensity (Exp)').onChange(updateStars);
|
||||
starFolder.close();
|
||||
this.updateStars = updateStars;
|
||||
|
||||
const artFolder = gui.addFolder('Artistic');
|
||||
// Set Horizon Glow default to 0.0
|
||||
@@ -495,7 +489,7 @@ class App {
|
||||
artFolder.add(sky.msFactors, 2, 0.0, 1.0).name('Horizon Glow').onChange(v => sky.setHorizonGlow(v));
|
||||
artFolder.add(sky, 'contrast', 0.1, 2.0).onChange(v => sky.setContrast(v));
|
||||
|
||||
|
||||
artFolder.addColor(sky, 'nightColor').onChange(v => sky.setNightColor(v));
|
||||
|
||||
const shmFolder = artFolder.addFolder('Shimmer');
|
||||
// Set Shimmer Strength default to 0.0
|
||||
@@ -508,7 +502,7 @@ class App {
|
||||
const camFolder = gui.addFolder('Camera');
|
||||
camFolder.add(this.params, 'focalLength', 8.0, 300.0).name('Focal Length').onChange(() => this.updateCameraProjection());
|
||||
camFolder.add(this.params, 'aperture', 1.4, 32.0).onChange(() => this.updateCameraExposure());
|
||||
camFolder.add(this.params, 'shutterSpeed', 0.05, 1000.0).onChange(() => this.updateCameraExposure());
|
||||
camFolder.add(this.params, 'shutterSpeed', 1.0, 1000.0).onChange(() => this.updateCameraExposure());
|
||||
camFolder.add(this.params, 'iso', 50.0, 3200.0).onChange(() => this.updateCameraExposure());
|
||||
|
||||
const bloomFolder = camFolder.addFolder('Bloom');
|
||||
@@ -698,15 +692,13 @@ class App {
|
||||
const s = this.sParams;
|
||||
const b = this.bParams;
|
||||
const m = this.mParams;
|
||||
const mw = this.mwParams;
|
||||
const sk = this.skybox;
|
||||
|
||||
return {
|
||||
p: { a: p.aperture, ss: p.shutterSpeed, i: p.iso, st: p.sunTheta, sp: p.sunPhi, fl: p.focalLength, si: p.sunIntensity },
|
||||
c: { v: c.volumetrics, co: c.coverage, d: c.density, h: c.height, s: c.speed, e: c.evolution },
|
||||
w: { dt: w.derivativeTrick, st: w.strength, s: w.speed, o: w.octaves },
|
||||
s: { e: s.enabled, d: s.density, i: s.intensity },
|
||||
mw: { e: mw.enabled, i: mw.intensity, s: mw.saturation, bp: mw.blackPoint, st: mw.siderealTime, l: mw.latitude },
|
||||
s: { e: s.enabled, d: s.density },
|
||||
b: { e: b.enabled, lf: b.lensFlare },
|
||||
m: { e: m.enabled, az: m.azimuth, h: m.height, r: m.radius, i: m.intensity },
|
||||
cm: { t: this.camState.theta, p: this.camState.phi },
|
||||
@@ -731,7 +723,6 @@ class App {
|
||||
const c = state.c;
|
||||
const w = state.w;
|
||||
const s = state.s;
|
||||
const mw = state.mw;
|
||||
const b = state.b;
|
||||
const m = state.m;
|
||||
const k = state.k;
|
||||
@@ -766,18 +757,6 @@ class App {
|
||||
if (s) {
|
||||
if (s.e !== undefined) this.sParams.enabled = s.e;
|
||||
if (s.d !== undefined) this.sParams.density = s.d;
|
||||
if (s.i !== undefined) this.sParams.intensity = s.i;
|
||||
if (this.updateStars) this.updateStars();
|
||||
}
|
||||
|
||||
if (mw) {
|
||||
if (mw.e !== undefined) this.mwParams.enabled = mw.e;
|
||||
if (mw.i !== undefined) this.mwParams.intensity = mw.i;
|
||||
if (mw.s !== undefined) this.mwParams.saturation = mw.s;
|
||||
if (mw.bp !== undefined) this.mwParams.blackPoint = mw.bp;
|
||||
if (mw.st !== undefined) this.mwParams.siderealTime = mw.st;
|
||||
if (mw.l !== undefined) this.mwParams.latitude = mw.l;
|
||||
if (this.updateMW) this.updateMW();
|
||||
}
|
||||
|
||||
if (b) {
|
||||
|
||||
@@ -96,20 +96,10 @@ material {
|
||||
name : starControl, // x=Density, y=Enabled, z=Frequency, w=PixelScale
|
||||
precision : high
|
||||
},
|
||||
{
|
||||
type : float,
|
||||
name : starIntensity,
|
||||
precision : high
|
||||
},
|
||||
{
|
||||
type : sampler2d,
|
||||
name : moonTexture
|
||||
},
|
||||
{
|
||||
type : float,
|
||||
name : exposure,
|
||||
precision : high
|
||||
},
|
||||
{
|
||||
type : sampler2d,
|
||||
name : moonNormal
|
||||
@@ -172,9 +162,9 @@ fragment {
|
||||
// --- CONFIGURATION ---
|
||||
|
||||
// Stars
|
||||
#define STAR_GLOBAL_INTENSITY 100.0 // Master brightness multiplier [0.0 - 500.0]
|
||||
#define STAR_BRIGHTNESS_BASE 0.01 // Minimum random brightness [0.0 - 1.0]
|
||||
#define STAR_BRIGHTNESS_VAR 15.0 // Random brightness variance range [0.0 - 10.0]
|
||||
#define STAR_GLOBAL_INTENSITY 150.0 // Master brightness multiplier [0.0 - 500.0]
|
||||
#define STAR_BRIGHTNESS_BASE 0.5 // Minimum random brightness [0.0 - 1.0]
|
||||
#define STAR_BRIGHTNESS_VAR 4.0 // Random brightness variance range [0.0 - 10.0]
|
||||
#define STAR_FADE_SUN_ELV_HIGH 0.10 // Sun elevation (sin) where stars are fully hidden [0.0 - 0.5]
|
||||
#define STAR_FADE_SUN_ELV_LOW -0.20 // Sun elevation (sin) where stars are fully visible [-0.5 - 0.0]
|
||||
#define STAR_CLOUD_OCCLUSION 0.1 // Visibility when covered by clouds [0.0 - 1.0]
|
||||
@@ -971,11 +961,7 @@ fragment {
|
||||
highp vec4 sunHalo, highp vec4 cloudControl, highp vec4 cloudControl2,
|
||||
highp vec4 shimmerControl, highp vec4 waterControl,
|
||||
highp vec3 L2, highp float sunIntensity2, highp vec4 sunHalo2,
|
||||
sampler2D moonTex, sampler2D moonNormal,
|
||||
highp vec3 nightColor,
|
||||
highp mat3 milkyWayRotation,
|
||||
highp vec3 milkyWayControl,
|
||||
sampler2D milkyWayTexture) {
|
||||
sampler2D moonTex, sampler2D moonNormal) {
|
||||
|
||||
// Project to plane y=0
|
||||
highp float t = WATER_PLANE_HEIGHT / min(V.y, -0.0002); // Reduced clamp to minimize "wall" artifact
|
||||
@@ -1071,7 +1057,7 @@ fragment {
|
||||
highp float rHorizonMask = 1.0 - smoothstep(0.0, REFLECTION_HORIZON_FADE, R.y);
|
||||
|
||||
if (rHorizonMask > 0.0) {
|
||||
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask * materialParams.exposure;
|
||||
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask;
|
||||
}
|
||||
|
||||
// Sun Disk Reflection
|
||||
@@ -1102,29 +1088,6 @@ fragment {
|
||||
// Apply clouds to reflection
|
||||
reflection = mix(reflection, reflCloudLayer, reflCloudDensity);
|
||||
|
||||
// Add Milky Way to Reflection
|
||||
// Calculate Fade based on Sun Elevation
|
||||
highp float sunElvSin = L.y;
|
||||
highp float mwFade = smoothstep(STAR_FADE_SUN_ELV_HIGH, STAR_FADE_SUN_ELV_LOW, sunElvSin);
|
||||
|
||||
if (mwFade > 0.0) {
|
||||
highp vec3 mwColor = getMilkyWay(R, milkyWayRotation, milkyWayTexture, milkyWayControl);
|
||||
|
||||
// Apply Atmosphere Transmittance (approximate)
|
||||
mwColor *= outTransmittance;
|
||||
|
||||
// Apply Fades
|
||||
mwColor *= mwFade;
|
||||
mwColor *= (1.0 - reflMoonOcclusion); // Occlude by Moon
|
||||
mwColor *= (1.0 - reflCloudDensity); // Occlude by Clouds
|
||||
|
||||
reflection += mwColor;
|
||||
}
|
||||
|
||||
|
||||
// Add Night Color to Reflection
|
||||
reflection += nightColor;
|
||||
|
||||
// Fresnel
|
||||
highp float F0 = WATER_FRESNEL_F0; // Water
|
||||
highp float cosTheta = clamp(dot(-V, N_water), 0.0, 1.0);
|
||||
@@ -1224,7 +1187,7 @@ fragment {
|
||||
|
||||
// 7. Stars
|
||||
// Add stars before clouds (clouds cover stars)
|
||||
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl) * materialParams.exposure * materialParams.starIntensity;
|
||||
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl);
|
||||
|
||||
// 7b. Milky Way
|
||||
// Add Milky Way behind stars (conceptually) but handled similarly
|
||||
@@ -1269,11 +1232,7 @@ fragment {
|
||||
materialParams.shimmerControl,
|
||||
materialParams.waterControl,
|
||||
L2, materialParams.sunIntensity2, materialParams.sunHalo2,
|
||||
materialParams_moonTexture, materialParams_moonNormal,
|
||||
materialParams.nightColor,
|
||||
materialParams.milkyWayRotation,
|
||||
materialParams.milkyWayControl,
|
||||
materialParams_milkyWayTexture);
|
||||
materialParams_moonTexture, materialParams_moonNormal);
|
||||
color = applyDynamicToneMapping(color, L, materialParams.contrast);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,12 @@ class SimulatedSkybox {
|
||||
this.ozone = 0.0;
|
||||
this.msFactors = [0.1, 0.5, 0.0];
|
||||
this.contrast = 1.0;
|
||||
this.nightColor = [0.0, 3.0e-9, 7.5e-9];
|
||||
this.nightColor = [0.0, 0.0003, 0.00075];
|
||||
this.shimmerControl = [0.0, 20.0, 0.1];
|
||||
this.cloudControl = [0.0, 0.1, 8000.0, 0.0];
|
||||
this.cloudControl2 = [0.0, 0.0, 0.0, 0.0];
|
||||
this.waterControl = [50.0, 1.0, 1.0, 4.0]; // x=Strength, y=Speed, z=DerivativeTrick, w=Octaves
|
||||
this.starControl = [0.001, 1.0, 350.0, 0.01]; // x=Density, y=Enabled, z=Frequency, w=PixelScale
|
||||
this.starIntensity = 1.0;
|
||||
this.focalLength = 24.0;
|
||||
this.height = 1000.0;
|
||||
this.planetRadius = 6360.0;
|
||||
@@ -44,7 +43,7 @@ class SimulatedSkybox {
|
||||
|
||||
// Milky Way Parameters
|
||||
// x=Intensity, y=Saturation, z=Unused
|
||||
this.milkyWayControl = [1.0, 1.2, 0.05];
|
||||
this.milkyWayControl = [1.0, 1.0, 0.07];
|
||||
this.milkyWayEnabled = true;
|
||||
this.milkyWayRotation = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Identity by default
|
||||
this.initEntity();
|
||||
@@ -454,11 +453,6 @@ class SimulatedSkybox {
|
||||
this.updateCoefficients();
|
||||
}
|
||||
|
||||
setStarIntensity(intensity) {
|
||||
this.starIntensity = Math.max(0.0, intensity);
|
||||
this.updateCoefficients();
|
||||
}
|
||||
|
||||
setMoonPosition(direction) {
|
||||
// normalize
|
||||
const len = Math.hypot(direction[0], direction[1], direction[2]);
|
||||
@@ -506,13 +500,6 @@ class SimulatedSkybox {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
setExposure(exposure) {
|
||||
this.exposure = exposure;
|
||||
this.updateCoefficients();
|
||||
}
|
||||
|
||||
updateCoefficients() {
|
||||
if (!this.materialInstance) {
|
||||
console.warn("updateCoefficients called before material loaded");
|
||||
@@ -598,7 +585,6 @@ class SimulatedSkybox {
|
||||
this.materialInstance.setFloat4Parameter('cloudControl2', new Float32Array(this.cloudControl2));
|
||||
this.materialInstance.setFloat4Parameter('waterControl', new Float32Array(this.waterControl));
|
||||
this.materialInstance.setFloat4Parameter('starControl', new Float32Array(this.starControl));
|
||||
this.materialInstance.setFloatParameter('starIntensity', this.starIntensity);
|
||||
|
||||
this.materialInstance.setFloatParameter('sunIntensity', physicalSunIntensity);
|
||||
|
||||
@@ -629,13 +615,12 @@ class SimulatedSkybox {
|
||||
|
||||
this.materialInstance.setFloatParameter('sunIntensity2', finalMoonIntensity);
|
||||
|
||||
// Scale Milky Way by Sun Intensity
|
||||
// Calibration: 1.0 User Intensity = 1.5e-3 cd/m^2 (Nits) approx.
|
||||
// Target: 1.5e-3 Nits. SunIntensity = 100,000.
|
||||
// Scale = 1.5e-3 / 1.0e5 = 1.5e-8.
|
||||
// Scale Milky Way by Sun Intensity (Pre-exposed) to match dynamic range
|
||||
// Calibration: 1.0 User Intensity ~ 0.025 Lux Relative (approx 2.5% of Sun Pixel Value at Day)
|
||||
// At Sunny 16 (Sun ~ 2.6), this gives ~0.065 pixel brightness, which is visible but dim.
|
||||
const mwIntensity = this.milkyWayEnabled ? this.milkyWayControl[0] : 0.0;
|
||||
const mwUniform = [
|
||||
mwIntensity * this.sunIntensity * 1.5e-8,
|
||||
mwIntensity * this.sunIntensity * 0.025,
|
||||
this.milkyWayControl[1],
|
||||
this.milkyWayControl[2]
|
||||
];
|
||||
@@ -649,7 +634,6 @@ class SimulatedSkybox {
|
||||
const moonHaloUpload = [...this.moonHalo];
|
||||
moonHaloUpload[2] *= moonRadConv;
|
||||
this.materialInstance.setFloat4Parameter('sunHalo2', new Float32Array(moonHaloUpload));
|
||||
this.materialInstance.setFloatParameter('exposure', this.exposure !== undefined ? this.exposure : 1.0);
|
||||
|
||||
// Solar Eclipse (CPU Calculation)
|
||||
const sunRadius = Math.acos(this.sunHalo[0]);
|
||||
|
||||
Binary file not shown.
@@ -218,7 +218,6 @@ class App {
|
||||
const exposure = this.getExposure();
|
||||
const preExposedIntensity = this.params.sunIntensity * exposure;
|
||||
this.skybox.setSunIntensity(preExposedIntensity);
|
||||
this.skybox.setExposure(exposure); // Update Skybox Exposure Uniform
|
||||
|
||||
// Moon Exposure
|
||||
if (this.mParams) {
|
||||
@@ -390,7 +389,7 @@ class App {
|
||||
};
|
||||
|
||||
mwFolder.add(this.mwParams, 'enabled').name('Enabled').onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'intensity', 0.0, 100.0).onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'intensity', 0.0, 5.0).onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'saturation', 0.0, 2.0).onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'blackPoint', 0.0, 0.5).name('Black Point').onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'siderealTime', 0.0, 24.0).name('Sidereal Time').listen().onChange(updateMW);
|
||||
@@ -465,23 +464,18 @@ class App {
|
||||
const starFolder = gui.addFolder('Stars');
|
||||
this.sParams = {
|
||||
enabled: true,
|
||||
density: 0.001,
|
||||
intensity: 0.0 // 2^0 = 1.0
|
||||
density: 0.001
|
||||
};
|
||||
// Initialize defaults (Density 0.001, Enabled True)
|
||||
sky.setStarControl(0.001, true);
|
||||
sky.setStarIntensity(Math.pow(2.0, 0.0));
|
||||
|
||||
const updateStars = () => {
|
||||
sky.setStarControl(this.sParams.density, this.sParams.enabled);
|
||||
sky.setStarIntensity(Math.pow(2.0, this.sParams.intensity));
|
||||
};
|
||||
|
||||
starFolder.add(this.sParams, 'enabled').name('Enabled').onChange(updateStars);
|
||||
starFolder.add(this.sParams, 'density', 0.0, 0.01, 0.0001).name('Density').onChange(updateStars);
|
||||
starFolder.add(this.sParams, 'intensity', 0.0, 24.0).name('Intensity (Exp)').onChange(updateStars);
|
||||
starFolder.close();
|
||||
this.updateStars = updateStars;
|
||||
|
||||
const artFolder = gui.addFolder('Artistic');
|
||||
// Set Horizon Glow default to 0.0
|
||||
@@ -495,7 +489,7 @@ class App {
|
||||
artFolder.add(sky.msFactors, 2, 0.0, 1.0).name('Horizon Glow').onChange(v => sky.setHorizonGlow(v));
|
||||
artFolder.add(sky, 'contrast', 0.1, 2.0).onChange(v => sky.setContrast(v));
|
||||
|
||||
|
||||
artFolder.addColor(sky, 'nightColor').onChange(v => sky.setNightColor(v));
|
||||
|
||||
const shmFolder = artFolder.addFolder('Shimmer');
|
||||
// Set Shimmer Strength default to 0.0
|
||||
@@ -508,7 +502,7 @@ class App {
|
||||
const camFolder = gui.addFolder('Camera');
|
||||
camFolder.add(this.params, 'focalLength', 8.0, 300.0).name('Focal Length').onChange(() => this.updateCameraProjection());
|
||||
camFolder.add(this.params, 'aperture', 1.4, 32.0).onChange(() => this.updateCameraExposure());
|
||||
camFolder.add(this.params, 'shutterSpeed', 0.05, 1000.0).onChange(() => this.updateCameraExposure());
|
||||
camFolder.add(this.params, 'shutterSpeed', 1.0, 1000.0).onChange(() => this.updateCameraExposure());
|
||||
camFolder.add(this.params, 'iso', 50.0, 3200.0).onChange(() => this.updateCameraExposure());
|
||||
|
||||
const bloomFolder = camFolder.addFolder('Bloom');
|
||||
@@ -698,15 +692,13 @@ class App {
|
||||
const s = this.sParams;
|
||||
const b = this.bParams;
|
||||
const m = this.mParams;
|
||||
const mw = this.mwParams;
|
||||
const sk = this.skybox;
|
||||
|
||||
return {
|
||||
p: { a: p.aperture, ss: p.shutterSpeed, i: p.iso, st: p.sunTheta, sp: p.sunPhi, fl: p.focalLength, si: p.sunIntensity },
|
||||
c: { v: c.volumetrics, co: c.coverage, d: c.density, h: c.height, s: c.speed, e: c.evolution },
|
||||
w: { dt: w.derivativeTrick, st: w.strength, s: w.speed, o: w.octaves },
|
||||
s: { e: s.enabled, d: s.density, i: s.intensity },
|
||||
mw: { e: mw.enabled, i: mw.intensity, s: mw.saturation, bp: mw.blackPoint, st: mw.siderealTime, l: mw.latitude },
|
||||
s: { e: s.enabled, d: s.density },
|
||||
b: { e: b.enabled, lf: b.lensFlare },
|
||||
m: { e: m.enabled, az: m.azimuth, h: m.height, r: m.radius, i: m.intensity },
|
||||
cm: { t: this.camState.theta, p: this.camState.phi },
|
||||
@@ -731,7 +723,6 @@ class App {
|
||||
const c = state.c;
|
||||
const w = state.w;
|
||||
const s = state.s;
|
||||
const mw = state.mw;
|
||||
const b = state.b;
|
||||
const m = state.m;
|
||||
const k = state.k;
|
||||
@@ -766,18 +757,6 @@ class App {
|
||||
if (s) {
|
||||
if (s.e !== undefined) this.sParams.enabled = s.e;
|
||||
if (s.d !== undefined) this.sParams.density = s.d;
|
||||
if (s.i !== undefined) this.sParams.intensity = s.i;
|
||||
if (this.updateStars) this.updateStars();
|
||||
}
|
||||
|
||||
if (mw) {
|
||||
if (mw.e !== undefined) this.mwParams.enabled = mw.e;
|
||||
if (mw.i !== undefined) this.mwParams.intensity = mw.i;
|
||||
if (mw.s !== undefined) this.mwParams.saturation = mw.s;
|
||||
if (mw.bp !== undefined) this.mwParams.blackPoint = mw.bp;
|
||||
if (mw.st !== undefined) this.mwParams.siderealTime = mw.st;
|
||||
if (mw.l !== undefined) this.mwParams.latitude = mw.l;
|
||||
if (this.updateMW) this.updateMW();
|
||||
}
|
||||
|
||||
if (b) {
|
||||
|
||||
@@ -96,20 +96,10 @@ material {
|
||||
name : starControl, // x=Density, y=Enabled, z=Frequency, w=PixelScale
|
||||
precision : high
|
||||
},
|
||||
{
|
||||
type : float,
|
||||
name : starIntensity,
|
||||
precision : high
|
||||
},
|
||||
{
|
||||
type : sampler2d,
|
||||
name : moonTexture
|
||||
},
|
||||
{
|
||||
type : float,
|
||||
name : exposure,
|
||||
precision : high
|
||||
},
|
||||
{
|
||||
type : sampler2d,
|
||||
name : moonNormal
|
||||
@@ -172,9 +162,9 @@ fragment {
|
||||
// --- CONFIGURATION ---
|
||||
|
||||
// Stars
|
||||
#define STAR_GLOBAL_INTENSITY 100.0 // Master brightness multiplier [0.0 - 500.0]
|
||||
#define STAR_BRIGHTNESS_BASE 0.01 // Minimum random brightness [0.0 - 1.0]
|
||||
#define STAR_BRIGHTNESS_VAR 15.0 // Random brightness variance range [0.0 - 10.0]
|
||||
#define STAR_GLOBAL_INTENSITY 150.0 // Master brightness multiplier [0.0 - 500.0]
|
||||
#define STAR_BRIGHTNESS_BASE 0.5 // Minimum random brightness [0.0 - 1.0]
|
||||
#define STAR_BRIGHTNESS_VAR 4.0 // Random brightness variance range [0.0 - 10.0]
|
||||
#define STAR_FADE_SUN_ELV_HIGH 0.10 // Sun elevation (sin) where stars are fully hidden [0.0 - 0.5]
|
||||
#define STAR_FADE_SUN_ELV_LOW -0.20 // Sun elevation (sin) where stars are fully visible [-0.5 - 0.0]
|
||||
#define STAR_CLOUD_OCCLUSION 0.1 // Visibility when covered by clouds [0.0 - 1.0]
|
||||
@@ -971,11 +961,7 @@ fragment {
|
||||
highp vec4 sunHalo, highp vec4 cloudControl, highp vec4 cloudControl2,
|
||||
highp vec4 shimmerControl, highp vec4 waterControl,
|
||||
highp vec3 L2, highp float sunIntensity2, highp vec4 sunHalo2,
|
||||
sampler2D moonTex, sampler2D moonNormal,
|
||||
highp vec3 nightColor,
|
||||
highp mat3 milkyWayRotation,
|
||||
highp vec3 milkyWayControl,
|
||||
sampler2D milkyWayTexture) {
|
||||
sampler2D moonTex, sampler2D moonNormal) {
|
||||
|
||||
// Project to plane y=0
|
||||
highp float t = WATER_PLANE_HEIGHT / min(V.y, -0.0002); // Reduced clamp to minimize "wall" artifact
|
||||
@@ -1071,7 +1057,7 @@ fragment {
|
||||
highp float rHorizonMask = 1.0 - smoothstep(0.0, REFLECTION_HORIZON_FADE, R.y);
|
||||
|
||||
if (rHorizonMask > 0.0) {
|
||||
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask * materialParams.exposure;
|
||||
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask;
|
||||
}
|
||||
|
||||
// Sun Disk Reflection
|
||||
@@ -1102,29 +1088,6 @@ fragment {
|
||||
// Apply clouds to reflection
|
||||
reflection = mix(reflection, reflCloudLayer, reflCloudDensity);
|
||||
|
||||
// Add Milky Way to Reflection
|
||||
// Calculate Fade based on Sun Elevation
|
||||
highp float sunElvSin = L.y;
|
||||
highp float mwFade = smoothstep(STAR_FADE_SUN_ELV_HIGH, STAR_FADE_SUN_ELV_LOW, sunElvSin);
|
||||
|
||||
if (mwFade > 0.0) {
|
||||
highp vec3 mwColor = getMilkyWay(R, milkyWayRotation, milkyWayTexture, milkyWayControl);
|
||||
|
||||
// Apply Atmosphere Transmittance (approximate)
|
||||
mwColor *= outTransmittance;
|
||||
|
||||
// Apply Fades
|
||||
mwColor *= mwFade;
|
||||
mwColor *= (1.0 - reflMoonOcclusion); // Occlude by Moon
|
||||
mwColor *= (1.0 - reflCloudDensity); // Occlude by Clouds
|
||||
|
||||
reflection += mwColor;
|
||||
}
|
||||
|
||||
|
||||
// Add Night Color to Reflection
|
||||
reflection += nightColor;
|
||||
|
||||
// Fresnel
|
||||
highp float F0 = WATER_FRESNEL_F0; // Water
|
||||
highp float cosTheta = clamp(dot(-V, N_water), 0.0, 1.0);
|
||||
@@ -1224,7 +1187,7 @@ fragment {
|
||||
|
||||
// 7. Stars
|
||||
// Add stars before clouds (clouds cover stars)
|
||||
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl) * materialParams.exposure * materialParams.starIntensity;
|
||||
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl);
|
||||
|
||||
// 7b. Milky Way
|
||||
// Add Milky Way behind stars (conceptually) but handled similarly
|
||||
@@ -1269,11 +1232,7 @@ fragment {
|
||||
materialParams.shimmerControl,
|
||||
materialParams.waterControl,
|
||||
L2, materialParams.sunIntensity2, materialParams.sunHalo2,
|
||||
materialParams_moonTexture, materialParams_moonNormal,
|
||||
materialParams.nightColor,
|
||||
materialParams.milkyWayRotation,
|
||||
materialParams.milkyWayControl,
|
||||
materialParams_milkyWayTexture);
|
||||
materialParams_moonTexture, materialParams_moonNormal);
|
||||
color = applyDynamicToneMapping(color, L, materialParams.contrast);
|
||||
}
|
||||
|
||||
|
||||
+152
-153
@@ -564,170 +564,169 @@ 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_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()
|
||||
|
||||
@@ -83,6 +83,22 @@ protected:
|
||||
*/
|
||||
Driver* createDriver(void* sharedContext, const DriverConfig& driverConfig) override;
|
||||
|
||||
/**
|
||||
* The implementation of *createDriver*.
|
||||
* @param sharedContext an optional shared context. This is not meaningful with all graphic
|
||||
* APIs and platforms.
|
||||
* For EGL platforms, this is an EGLContext.
|
||||
*
|
||||
* @param driverConfig specifies driver initialization parameters
|
||||
*
|
||||
* @param initFirstbyquery determines the order of initialization. If true, then we'd query by
|
||||
* eglQueryDevicesEXT first instead of using the default display. Useful
|
||||
* for headless egl initialization.
|
||||
* @return nullptr on failure, or a pointer to the newly created driver.
|
||||
*/
|
||||
Driver* createDriverBase(void* sharedContext, const DriverConfig& driverConfig,
|
||||
bool initFirstByQuery);
|
||||
|
||||
/**
|
||||
* This returns zero. This method can be overridden to return something more useful.
|
||||
* @return zero
|
||||
|
||||
@@ -123,63 +123,6 @@ void reshapeImageImpl(uint8_t* UTILS_RESTRICT dest, const uint8_t* UTILS_RESTRIC
|
||||
}
|
||||
}
|
||||
|
||||
struct UnpackerR11G11B10 {
|
||||
static void unpack(const uint8_t* src, float* out) {
|
||||
uint32_t p;
|
||||
std::memcpy(&p, src, 4);
|
||||
|
||||
using R11 = math::fp<0, 5, 6>;
|
||||
using G11 = math::fp<0, 5, 6>;
|
||||
using B10 = math::fp<0, 5, 5>;
|
||||
|
||||
out[0] = R11::tof(R11(uint16_t((p >> 21) & 0x7FF)));
|
||||
out[1] = G11::tof(G11(uint16_t((p >> 10) & 0x7FF)));
|
||||
out[2] = B10::tof(B10(uint16_t(p & 0x3FF)));
|
||||
}
|
||||
};
|
||||
|
||||
template<typename dstComponentType, typename Unpacker, bool Swizzle>
|
||||
static void reshapeImagePacked(uint8_t* UTILS_RESTRICT dest, const uint8_t* UTILS_RESTRICT src,
|
||||
size_t srcBytesPerRow, size_t srcChannelCount, size_t dstRowOffset, size_t dstColumnOffset,
|
||||
size_t dstBytesPerRow, size_t dstChannelCount, size_t width, size_t height, bool /*swizzle*/) {
|
||||
|
||||
dest += (dstRowOffset * dstBytesPerRow);
|
||||
const dstComponentType dstMaxValue = getMaxValue<dstComponentType>();
|
||||
|
||||
for (size_t row = 0; row < height; ++row) {
|
||||
const uint8_t* inPtr = src;
|
||||
dstComponentType* out = (dstComponentType*) dest + (dstColumnOffset * dstChannelCount);
|
||||
|
||||
for (size_t column = 0; column < width; ++column) {
|
||||
float rgba[4] = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
Unpacker::unpack(inPtr, rgba);
|
||||
|
||||
if constexpr (Swizzle) {
|
||||
std::swap(rgba[0], rgba[2]);
|
||||
}
|
||||
|
||||
for (size_t c = 0; c < dstChannelCount; ++c) {
|
||||
if constexpr (std::is_same_v<dstComponentType, float>) {
|
||||
out[c] = rgba[c];
|
||||
} else if constexpr (std::is_same_v<dstComponentType, math::half>) {
|
||||
out[c] = math::half(rgba[c]);
|
||||
} else {
|
||||
out[c] = static_cast<dstComponentType>(std::clamp(
|
||||
static_cast<double>(rgba[c]) * static_cast<double>(dstMaxValue),
|
||||
0.0,
|
||||
static_cast<double>(std::numeric_limits<dstComponentType>::max())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
inPtr += 4;
|
||||
out += dstChannelCount;
|
||||
}
|
||||
src += srcBytesPerRow;
|
||||
dest += dstBytesPerRow;
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
class DataReshaper {
|
||||
@@ -251,8 +194,6 @@ public:
|
||||
constexpr auto UINT = PixelDataType::UINT;
|
||||
constexpr auto INT = PixelDataType::INT;
|
||||
constexpr auto HALF = PixelDataType::HALF;
|
||||
constexpr auto UINT_10F_11F_11F_REV = PixelDataType::UINT_10F_11F_11F_REV;
|
||||
|
||||
switch (dst->type) {
|
||||
case UBYTE:
|
||||
switch (srcType) {
|
||||
@@ -268,10 +209,6 @@ public:
|
||||
case INT: reshaper = reshapeImageImpl<uint8_t, int32_t>; break;
|
||||
case UINT: reshaper = reshapeImageImpl<uint8_t, uint32_t>; break;
|
||||
case HALF: reshaper = reshapeImageImpl<uint8_t, math::half>; break;
|
||||
case UINT_10F_11F_11F_REV:
|
||||
if (swizzle) reshaper = reshapeImagePacked<uint8_t, UnpackerR11G11B10, true>;
|
||||
else reshaper = reshapeImagePacked<uint8_t, UnpackerR11G11B10, false>;
|
||||
break;
|
||||
default:
|
||||
LOG(ERROR) << "DataReshaper: UBYTE dst, unsupported srcType: "
|
||||
<< (int) srcType;
|
||||
@@ -284,10 +221,6 @@ public:
|
||||
case FLOAT: reshaper = reshapeImageImpl<float, float>; break;
|
||||
case INT: reshaper = reshapeImageImpl<float, int32_t>; break;
|
||||
case UINT: reshaper = reshapeImageImpl<float, uint32_t>; break;
|
||||
case UINT_10F_11F_11F_REV:
|
||||
if (swizzle) reshaper = reshapeImagePacked<float, UnpackerR11G11B10, true>;
|
||||
else reshaper = reshapeImagePacked<float, UnpackerR11G11B10, false>;
|
||||
break;
|
||||
default:
|
||||
LOG(ERROR) << "DataReshaper: FLOAT dst, unsupported srcType: "
|
||||
<< (int) srcType;
|
||||
@@ -300,10 +233,6 @@ public:
|
||||
case FLOAT: reshaper = reshapeImageImpl<int32_t, float>; break;
|
||||
case INT: reshaper = reshapeImageImpl<int32_t, int32_t>; break;
|
||||
case UINT: reshaper = reshapeImageImpl<int32_t, uint32_t>; break;
|
||||
case UINT_10F_11F_11F_REV:
|
||||
if (swizzle) reshaper = reshapeImagePacked<int32_t, UnpackerR11G11B10, true>;
|
||||
else reshaper = reshapeImagePacked<int32_t, UnpackerR11G11B10, false>;
|
||||
break;
|
||||
default:
|
||||
LOG(ERROR)
|
||||
<< "DataReshaper: INT dst, unsupported srcType: " << (int) srcType;
|
||||
@@ -316,10 +245,6 @@ public:
|
||||
case FLOAT: reshaper = reshapeImageImpl<uint32_t, float>; break;
|
||||
case INT: reshaper = reshapeImageImpl<uint32_t, int32_t>; break;
|
||||
case UINT: reshaper = reshapeImageImpl<uint32_t, uint32_t>; break;
|
||||
case UINT_10F_11F_11F_REV:
|
||||
if (swizzle) reshaper = reshapeImagePacked<uint32_t, UnpackerR11G11B10, true>;
|
||||
else reshaper = reshapeImagePacked<uint32_t, UnpackerR11G11B10, false>;
|
||||
break;
|
||||
default:
|
||||
LOG(ERROR)
|
||||
<< "DataReshaper: UINT dst, unsupported srcType: " << (int) srcType;
|
||||
@@ -331,10 +256,6 @@ public:
|
||||
case HALF:
|
||||
reshaper = copyImage;
|
||||
break;
|
||||
case UINT_10F_11F_11F_REV:
|
||||
if (swizzle) reshaper = reshapeImagePacked<math::half, UnpackerR11G11B10, true>;
|
||||
else reshaper = reshapeImagePacked<math::half, UnpackerR11G11B10, false>;
|
||||
break;
|
||||
default:
|
||||
LOG(ERROR)
|
||||
<< "DataReshaper: HALF dst, unsupported srcType: " << (int) srcType;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1092,7 +1092,7 @@ MetalRenderTarget::MetalRenderTarget(MetalContext* context, uint32_t width, uint
|
||||
// a multisampled sidecar texture and do a resolve automatically.
|
||||
if (samples > 1 && texture->samples == 1) {
|
||||
auto& sidecar = texture->msaaSidecar;
|
||||
if (!sidecar || sidecar.sampleCount != samples) {
|
||||
if (!sidecar) {
|
||||
sidecar = createMultisampledTexture(mtlTexture.pixelFormat, texture->width,
|
||||
texture->height, samples);
|
||||
}
|
||||
@@ -1123,7 +1123,7 @@ MetalRenderTarget::MetalRenderTarget(MetalContext* context, uint32_t width, uint
|
||||
// a multisampled sidecar texture and do a resolve automatically.
|
||||
if (samples > 1 && texture->samples == 1) {
|
||||
auto& sidecar = texture->msaaSidecar;
|
||||
if (!sidecar || sidecar.sampleCount != samples) {
|
||||
if (!sidecar) {
|
||||
sidecar = createMultisampledTexture(mtlTexture.pixelFormat, texture->width,
|
||||
texture->height, samples);
|
||||
}
|
||||
@@ -1155,7 +1155,7 @@ MetalRenderTarget::MetalRenderTarget(MetalContext* context, uint32_t width, uint
|
||||
// a multisampled sidecar texture and do a resolve automatically.
|
||||
if (samples > 1 && texture->samples == 1) {
|
||||
auto& sidecar = texture->msaaSidecar;
|
||||
if (!sidecar || sidecar.sampleCount != samples) {
|
||||
if (!sidecar) {
|
||||
sidecar = createMultisampledTexture(mtlTexture.pixelFormat, texture->width,
|
||||
texture->height, samples);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ public:
|
||||
GLenum getIndicesType() const noexcept {
|
||||
return indicesType;
|
||||
}
|
||||
};
|
||||
} gl;
|
||||
|
||||
static bool queryOpenGLVersion(GLint* major, GLint* minor) noexcept;
|
||||
|
||||
|
||||
@@ -3623,8 +3623,6 @@ void OpenGLDriver::detachStream(GLTexture* t) noexcept {
|
||||
case StreamType::NATIVE:
|
||||
mPlatform.detach(t->hwStream->stream);
|
||||
// ^ this deletes the texture id
|
||||
// We still need to call unbind to update the bookkeeping.
|
||||
gl.unbindTexture(t->gl.target, t->gl.id);
|
||||
break;
|
||||
case StreamType::ACQUIRED:
|
||||
gl.unbindTexture(t->gl.target, t->gl.id);
|
||||
|
||||
@@ -120,25 +120,57 @@ bool PlatformEGL::isOpenGL() const noexcept {
|
||||
PlatformEGL::ExternalImageEGL::~ExternalImageEGL() = default;
|
||||
|
||||
Driver* PlatformEGL::createDriver(void* sharedContext, const DriverConfig& driverConfig) {
|
||||
mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
assert_invariant(mEGLDisplay != EGL_NO_DISPLAY);
|
||||
return createDriverBase(sharedContext, driverConfig, false /* initFirstByQuery */);
|
||||
}
|
||||
|
||||
Driver* PlatformEGL::createDriverBase(void* sharedContext, const DriverConfig& driverConfig,
|
||||
bool initFirstByQuery) {
|
||||
static constexpr int kMaxNumEGLDevices = 32;
|
||||
|
||||
EGLint major, minor;
|
||||
EGLBoolean initialized = eglInitialize(mEGLDisplay, &major, &minor);
|
||||
EGLBoolean initialized = false;
|
||||
|
||||
if (!initialized) {
|
||||
EGLDeviceEXT eglDevice;
|
||||
EGLint numDevices;
|
||||
using InitFunc = std::function<void()>;
|
||||
|
||||
InitFunc queryInit = [&]() {
|
||||
PFNEGLQUERYDEVICESEXTPROC const eglQueryDevicesEXT =
|
||||
PFNEGLQUERYDEVICESEXTPROC(eglGetProcAddress("eglQueryDevicesEXT"));
|
||||
if (eglQueryDevicesEXT != nullptr) {
|
||||
eglQueryDevicesEXT(1, &eglDevice, &numDevices);
|
||||
if(auto* getPlatformDisplay = reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(
|
||||
eglGetProcAddress("eglGetPlatformDisplay"))) {
|
||||
mEGLDisplay = getPlatformDisplay(EGL_PLATFORM_DEVICE_EXT, eglDevice, nullptr);
|
||||
PFNEGLGETPLATFORMDISPLAYEXTPROC const getPlatformDisplay =
|
||||
PFNEGLGETPLATFORMDISPLAYEXTPROC(eglGetProcAddress("eglGetPlatformDisplay"));
|
||||
|
||||
if (!eglQueryDevicesEXT || !getPlatformDisplay) {
|
||||
return;
|
||||
}
|
||||
|
||||
EGLint numDevices = 0;
|
||||
EGLDeviceEXT eglDevices[kMaxNumEGLDevices];
|
||||
if (eglQueryDevicesEXT(kMaxNumEGLDevices, eglDevices, &numDevices)) {
|
||||
for (int i = 0; i < numDevices && !initialized; ++i) {
|
||||
mEGLDisplay = getPlatformDisplay(EGL_PLATFORM_DEVICE_EXT, eglDevices[i], nullptr);
|
||||
initialized = eglInitialize(mEGLDisplay, &major, &minor);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
InitFunc defaultInit = [&]() {
|
||||
mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
initialized = eglInitialize(mEGLDisplay, &major, &minor);
|
||||
};
|
||||
|
||||
// The order by which we check for display/device does matter because certain platforms have
|
||||
// multiple displays/devices. We either return the first queried (and successfully init'd
|
||||
// display) or just use the default display. Deciding which init path should go first is
|
||||
// determined by the bool *initFirstByQuery*..
|
||||
std::array<InitFunc, 2> initFuncs{ defaultInit, queryInit };
|
||||
if (initFirstByQuery) {
|
||||
std::swap(initFuncs[0], initFuncs[1]);
|
||||
}
|
||||
|
||||
for (auto& initFunc: initFuncs) {
|
||||
if (initialized) {
|
||||
break;
|
||||
}
|
||||
initFunc();
|
||||
}
|
||||
|
||||
if (UTILS_UNLIKELY(!initialized)) {
|
||||
|
||||
@@ -66,7 +66,7 @@ backend::Driver* PlatformEGLHeadless::createDriver(void* sharedContext,
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return PlatformEGL::createDriver(sharedContext, driverConfig);
|
||||
return PlatformEGL::createDriverBase(sharedContext, driverConfig, true /* initFirstByQuery */);
|
||||
}
|
||||
|
||||
} // namespace filament
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -277,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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -1913,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.
|
||||
@@ -1927,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();
|
||||
@@ -1977,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)
|
||||
@@ -2002,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().
|
||||
@@ -2064,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,
|
||||
};
|
||||
@@ -2084,7 +2078,8 @@ void VulkanDriver::endRenderPass(int) {
|
||||
rt->emitBarriersEndRenderPass(*mCurrentRenderPass.commandBuffer);
|
||||
|
||||
mCurrentRenderPass.renderTarget = {};
|
||||
mCurrentRenderPass.renderPass = {};
|
||||
mCurrentRenderPass.renderPass = VK_NULL_HANDLE;
|
||||
|
||||
mCurrentRenderPass.commandBuffer = nullptr;
|
||||
}
|
||||
|
||||
@@ -2249,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);
|
||||
@@ -2292,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);
|
||||
@@ -2334,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)
|
||||
@@ -2486,7 +2481,6 @@ void VulkanDriver::bindPipelineImpl(PipelineState const& pipelineState,
|
||||
// Push state changes to the VulkanPipelineCache instance. This is fast and does not make VK calls.
|
||||
mPipelineCache.bindProgram(program);
|
||||
mPipelineCache.bindRasterState(vulkanRasterState);
|
||||
mPipelineCache.bindStencilState(pipelineState.stencilState);
|
||||
mPipelineCache.bindPrimitiveTopology(topology);
|
||||
mPipelineCache.bindVertexArray(attribDesc, bufferDesc, vbi->getAttributeCount());
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -166,7 +166,6 @@ void VulkanPipelineCache::asyncPrewarmCache(
|
||||
.depthBiasConstantFactor = 0.f,
|
||||
.depthBiasSlopeFactor = 0.f,
|
||||
},
|
||||
.stencilState = {},
|
||||
.layout = layout,
|
||||
};
|
||||
PipelineDynamicOptions dynamicOptions {
|
||||
@@ -298,45 +297,24 @@ VkPipeline VulkanPipelineCache::createPipeline(
|
||||
bool const enableDepthTest =
|
||||
raster.depthCompareOp != SamplerCompareFunc::A ||
|
||||
raster.depthWriteEnable;
|
||||
// Stencil must be enabled if we're testing OR writing to the stencil buffer.
|
||||
auto const& stencil = key.stencilState;
|
||||
bool const enableStencilTest =
|
||||
stencil.front.stencilFunc != StencilState::StencilFunction::A ||
|
||||
stencil.back.stencilFunc != StencilState::StencilFunction::A ||
|
||||
stencil.front.stencilOpDepthFail != StencilOperation::KEEP ||
|
||||
stencil.back.stencilOpDepthFail != StencilOperation::KEEP ||
|
||||
stencil.front.stencilOpStencilFail != StencilOperation::KEEP ||
|
||||
stencil.back.stencilOpStencilFail != StencilOperation::KEEP ||
|
||||
stencil.front.stencilOpDepthStencilPass != StencilOperation::KEEP ||
|
||||
stencil.back.stencilOpDepthStencilPass != StencilOperation::KEEP ||
|
||||
stencil.stencilWrite;
|
||||
VkPipelineDepthStencilStateCreateInfo vkDs = {
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO,
|
||||
.depthTestEnable = enableDepthTest ? VK_TRUE : VK_FALSE,
|
||||
.depthWriteEnable = raster.depthWriteEnable,
|
||||
.depthCompareOp = fvkutils::getCompareOp(raster.depthCompareOp),
|
||||
.depthBoundsTestEnable = VK_FALSE,
|
||||
.stencilTestEnable = enableStencilTest ? VK_TRUE : VK_FALSE,
|
||||
.stencilTestEnable = VK_FALSE,
|
||||
.minDepthBounds = 0.0f,
|
||||
.maxDepthBounds = 0.0f,
|
||||
};
|
||||
vkDs.front = {
|
||||
.failOp = fvkutils::getStencilOp(stencil.front.stencilOpStencilFail),
|
||||
.passOp = fvkutils::getStencilOp(stencil.front.stencilOpDepthStencilPass),
|
||||
.depthFailOp = fvkutils::getStencilOp(stencil.front.stencilOpDepthFail),
|
||||
.compareOp = fvkutils::getCompareOp(stencil.front.stencilFunc),
|
||||
.compareMask = stencil.front.readMask,
|
||||
.writeMask = (uint32_t) (stencil.stencilWrite ? stencil.front.writeMask : 0u),
|
||||
.reference = (uint32_t) stencil.front.ref,
|
||||
};
|
||||
vkDs.back = {
|
||||
.failOp = fvkutils::getStencilOp(stencil.back.stencilOpStencilFail),
|
||||
.passOp = fvkutils::getStencilOp(stencil.back.stencilOpDepthStencilPass),
|
||||
.depthFailOp = fvkutils::getStencilOp(stencil.back.stencilOpDepthFail),
|
||||
.compareOp = fvkutils::getCompareOp(stencil.back.stencilFunc),
|
||||
.compareMask = stencil.back.readMask,
|
||||
.writeMask = (uint32_t) (stencil.stencilWrite ? stencil.back.writeMask : 0u),
|
||||
.reference = (uint32_t) stencil.back.ref,
|
||||
vkDs.front = vkDs.back = {
|
||||
.failOp = VK_STENCIL_OP_KEEP,
|
||||
.passOp = VK_STENCIL_OP_KEEP,
|
||||
.depthFailOp = VK_STENCIL_OP_KEEP,
|
||||
.compareOp = VK_COMPARE_OP_ALWAYS,
|
||||
.compareMask = 0u,
|
||||
.writeMask = 0u,
|
||||
.reference = 0u,
|
||||
};
|
||||
|
||||
VkGraphicsPipelineCreateInfo pipelineCreateInfo = {
|
||||
@@ -455,14 +433,8 @@ void VulkanPipelineCache::bindRasterState(RasterState const& rasterState) noexce
|
||||
mPipelineRequirements.rasterState = rasterState;
|
||||
}
|
||||
|
||||
void VulkanPipelineCache::bindStencilState(StencilState const& stencilState) noexcept {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -122,9 +122,7 @@ public:
|
||||
void bindLayout(VkPipelineLayout layout) noexcept;
|
||||
void bindProgram(fvkmemory::resource_ptr<VulkanProgram> program) noexcept;
|
||||
void bindRasterState(RasterState const& rasterState) noexcept;
|
||||
void bindStencilState(StencilState const& stencilState) noexcept;
|
||||
void bindRenderPass(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);
|
||||
@@ -188,8 +186,8 @@ private:
|
||||
VertexInputAttributeDescription vertexAttributes[VERTEX_ATTRIBUTE_COUNT]; // 128 : 28
|
||||
VertexInputBindingDescription vertexBuffers[VERTEX_ATTRIBUTE_COUNT]; // 128 : 156
|
||||
RasterState rasterState; // 16 : 284
|
||||
StencilState stencilState; // 12 : 300
|
||||
VkPipelineLayout layout; // 8 : 312
|
||||
uint32_t padding; // 4 : 300
|
||||
VkPipelineLayout layout; // 8 : 304
|
||||
};
|
||||
|
||||
// Provides information about any dynamic state that should be used in creation of the
|
||||
@@ -205,7 +203,7 @@ private:
|
||||
uint8_t stereoscopicViewCount = 2;
|
||||
};
|
||||
|
||||
static_assert(sizeof(PipelineKey) == 320, "PipelineKey must not have implicit padding.");
|
||||
static_assert(sizeof(PipelineKey) == 312, "PipelineKey must not have implicit padding.");
|
||||
|
||||
using PipelineHashFn = utils::hash::MurmurHashFn<PipelineKey>;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -633,19 +633,6 @@ VkCompareOp getCompareOp(SamplerCompareFunc func) {
|
||||
}
|
||||
}
|
||||
|
||||
VkStencilOp getStencilOp(StencilOperation op) {
|
||||
switch (op) {
|
||||
case StencilOperation::KEEP: return VK_STENCIL_OP_KEEP;
|
||||
case StencilOperation::ZERO: return VK_STENCIL_OP_ZERO;
|
||||
case StencilOperation::REPLACE: return VK_STENCIL_OP_REPLACE;
|
||||
case StencilOperation::INCR: return VK_STENCIL_OP_INCREMENT_AND_CLAMP;
|
||||
case StencilOperation::INCR_WRAP: return VK_STENCIL_OP_INCREMENT_AND_WRAP;
|
||||
case StencilOperation::DECR: return VK_STENCIL_OP_DECREMENT_AND_CLAMP;
|
||||
case StencilOperation::DECR_WRAP: return VK_STENCIL_OP_DECREMENT_AND_WRAP;
|
||||
case StencilOperation::INVERT: return VK_STENCIL_OP_INVERT;
|
||||
}
|
||||
}
|
||||
|
||||
VkBlendFactor getBlendFactor(BlendFunction mode) {
|
||||
switch (mode) {
|
||||
case BlendFunction::ZERO: return VK_BLEND_FACTOR_ZERO;
|
||||
|
||||
@@ -53,7 +53,6 @@ uint32_t getBytesPerPixel(TextureFormat format);
|
||||
uint8_t getTexelBlockSize(VkFormat format);
|
||||
|
||||
VkCompareOp getCompareOp(SamplerCompareFunc func);
|
||||
VkStencilOp getStencilOp(StencilOperation op);
|
||||
VkBlendFactor getBlendFactor(BlendFunction mode);
|
||||
VkCullModeFlags getCullMode(CullingMode mode);
|
||||
VkFrontFace getFrontFace(bool inverseFrontFaces);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "BackendTest.h"
|
||||
|
||||
#include "Shader.h"
|
||||
#include "SharedShaders.h"
|
||||
#include "Skip.h"
|
||||
#include "TrianglePrimitive.h"
|
||||
|
||||
#include <backend/PixelBufferDescriptor.h>
|
||||
|
||||
#include <utils/FixedCapacityVector.h>
|
||||
|
||||
namespace test {
|
||||
|
||||
using namespace filament;
|
||||
using namespace filament::backend;
|
||||
using namespace filament::math;
|
||||
|
||||
TEST_F(BackendTest, AutoresolveDifferingSampleCounts) {
|
||||
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;
|
||||
|
||||
auto swapChain = addCleanup(createSwapChain());
|
||||
api.makeCurrent(swapChain, swapChain);
|
||||
|
||||
Shader shader = SharedShaders::makeShader(api, *mCleanup,
|
||||
{
|
||||
.mVertexType = VertexShaderType::Simple,
|
||||
.mFragmentType = FragmentShaderType::SolidColored,
|
||||
.mUniformType = ShaderUniformType::Simple,
|
||||
});
|
||||
|
||||
TrianglePrimitive triangle(api);
|
||||
PipelineState ps = getColorWritePipelineState();
|
||||
shader.addProgramToPipelineState(ps);
|
||||
|
||||
auto ubuffer = addCleanup(api.createBufferObject(sizeof(SimpleMaterialParams),
|
||||
BufferObjectBinding::UNIFORM, BufferUsage::STATIC));
|
||||
shader.bindUniform<SimpleMaterialParams>(api, ubuffer);
|
||||
|
||||
// Create a texture with sample count = 1.
|
||||
Handle<HwTexture> texture = addCleanup(api.createTexture(SamplerType::SAMPLER_2D, 1,
|
||||
TextureFormat::RGBA8, 1, kRenderTargetSize, kRenderTargetSize, 1,
|
||||
TextureUsage::COLOR_ATTACHMENT | TextureUsage::SAMPLEABLE));
|
||||
|
||||
// First render pass: render a red triangle to a RenderTarget with 8 samples.
|
||||
{
|
||||
Handle<HwRenderTarget> renderTarget =
|
||||
addCleanup(api.createRenderTarget(TargetBufferFlags::COLOR, kRenderTargetSize,
|
||||
kRenderTargetSize, 8, 1, { { texture } }, {}, {}));
|
||||
|
||||
shader.uploadUniform(api, ubuffer,
|
||||
SimpleMaterialParams{
|
||||
.color = float4(1, 0, 0, 1),
|
||||
});
|
||||
|
||||
RenderPassParams params = getClearColorRenderPass(float4(0));
|
||||
params.viewport = { 0, 0, kRenderTargetSize, kRenderTargetSize };
|
||||
|
||||
RenderFrame frame(api);
|
||||
api.beginRenderPass(renderTarget, params);
|
||||
ps.primitiveType = PrimitiveType::TRIANGLES;
|
||||
ps.vertexBufferInfo = triangle.getVertexBufferInfo();
|
||||
api.bindPipeline(ps);
|
||||
api.bindRenderPrimitive(triangle.getRenderPrimitive());
|
||||
api.draw2(0, 3, 1);
|
||||
api.endRenderPass();
|
||||
api.commit(swapChain);
|
||||
}
|
||||
|
||||
// Second render pass: render a green triangle to a RenderTarget with 4 samples, attached to
|
||||
// the same texture.
|
||||
{
|
||||
Handle<HwRenderTarget> renderTarget =
|
||||
addCleanup(api.createRenderTarget(TargetBufferFlags::COLOR, kRenderTargetSize,
|
||||
kRenderTargetSize, 4, 1, { { texture } }, {}, {}));
|
||||
|
||||
shader.uploadUniform(api, ubuffer,
|
||||
SimpleMaterialParams{
|
||||
.color = float4(0, 1, 0, 1),
|
||||
});
|
||||
|
||||
RenderPassParams params = getClearColorRenderPass(float4(0));
|
||||
params.viewport = { 0, 0, kRenderTargetSize, kRenderTargetSize };
|
||||
|
||||
RenderFrame frame(api);
|
||||
api.beginRenderPass(renderTarget, params);
|
||||
ps.primitiveType = PrimitiveType::TRIANGLES;
|
||||
ps.vertexBufferInfo = triangle.getVertexBufferInfo();
|
||||
api.bindPipeline(ps);
|
||||
api.bindRenderPrimitive(triangle.getRenderPrimitive());
|
||||
api.draw2(0, 3, 1);
|
||||
api.endRenderPass();
|
||||
|
||||
EXPECT_IMAGE(renderTarget, ScreenshotParams(kRenderTargetSize, kRenderTargetSize,
|
||||
"AutoresolveDifferingSampleCounts", 1048576));
|
||||
|
||||
api.commit(swapChain);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
@@ -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;
|
||||
|
||||
@@ -735,7 +735,7 @@ struct VsmShadowOptions {
|
||||
bool highPrecision = false;
|
||||
|
||||
/**
|
||||
* @deprecated has no effect.
|
||||
* VSM minimum variance scale, must be positive.
|
||||
*/
|
||||
float minVarianceScale = 0.5f;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
@@ -798,19 +798,16 @@ RenderPass::Command* RenderPass::generateCommandsImpl(CommandTypeFlags extraFlag
|
||||
cmd.info.rasterState.culling = cullingMode;
|
||||
|
||||
// FIXME: should writeDepthForShadowCasters take precedence over mi->getDepthWrite()?
|
||||
cmd.info.rasterState.depthWrite =
|
||||
(1 // only keep bit 0
|
||||
& (mi->isDepthWriteEnabled() |
|
||||
(mode == TransparencyMode::TWO_PASSES_ONE_SIDE) |
|
||||
isPickingVariant) &
|
||||
!(depthFilterAlphaMaskedObjects & rs.alphaToCoverage)) |
|
||||
writeDepthForShadowCasters;
|
||||
cmd.info.rasterState.depthWrite = (1 // only keep bit 0
|
||||
& (mi->isDepthWriteEnabled() | (mode == TransparencyMode::TWO_PASSES_ONE_SIDE)
|
||||
| isPickingVariant)
|
||||
& !(filterTranslucentObjects & translucent)
|
||||
& !(depthFilterAlphaMaskedObjects & rs.alphaToCoverage))
|
||||
| writeDepthForShadowCasters;
|
||||
|
||||
*curr = cmd;
|
||||
// cancel command if both front and back faces are culled
|
||||
curr->key |= select(cullingMode == CullingMode::FRONT_AND_BACK);
|
||||
// cancel command if asked to filter translucent objects
|
||||
curr->key |= select(filterTranslucentObjects & translucent);
|
||||
}
|
||||
|
||||
++curr;
|
||||
|
||||
+141
-136
@@ -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;
|
||||
}
|
||||
|
||||
float 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);
|
||||
const mat3f worldFromClip(inverse(clipFromWorld));
|
||||
const float3 Jx = worldFromClip[0];
|
||||
const float3 Jy = worldFromClip[1];
|
||||
const float s = std::max(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.
|
||||
|
||||
float 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);
|
||||
const float s = std::max(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 {
|
||||
|
||||
+13
-14
@@ -128,7 +128,7 @@ 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 {
|
||||
@@ -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 float texelSizeWorldSpace(const math::mat3f& clipFromWorld, uint16_t shadowDimension) noexcept;
|
||||
static float 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
|
||||
|
||||
@@ -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.
|
||||
@@ -748,7 +751,6 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng
|
||||
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 / wsTexelSize;
|
||||
|
||||
@@ -800,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -810,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,
|
||||
@@ -832,7 +835,7 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin
|
||||
const size_t shadowIndex = shadowMap.getShadowIndex();
|
||||
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();
|
||||
@@ -844,10 +847,9 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin
|
||||
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
|
||||
mSoftShadowOptions.penumbraScale * options->shadowBulbRadius
|
||||
/ wsTexelSizeAtOneMeter;
|
||||
|
||||
}
|
||||
@@ -905,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,
|
||||
@@ -927,7 +928,7 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap,
|
||||
const size_t shadowIndex = shadowMap.getShadowIndex();
|
||||
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();
|
||||
@@ -939,10 +940,9 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap,
|
||||
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
|
||||
mSoftShadowOptions.penumbraScale * options->shadowBulbRadius
|
||||
/ wsTexelSizeAtOneMeter;
|
||||
}
|
||||
}
|
||||
@@ -998,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) {
|
||||
@@ -1038,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;
|
||||
@@ -1055,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);
|
||||
@@ -1091,7 +1091,6 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
|
||||
msaaSamples = 1;
|
||||
}
|
||||
|
||||
float4 clearColor{};
|
||||
TextureFormat format = TextureFormat::DEPTH16;
|
||||
if (view.hasVSM()) {
|
||||
if (vsmShadowOptions.highPrecision) {
|
||||
@@ -1099,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();
|
||||
@@ -1123,8 +1119,7 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
|
||||
layersNeeded,
|
||||
mipLevels,
|
||||
msaaSamples,
|
||||
format,
|
||||
clearColor
|
||||
format
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,14 +378,8 @@ 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
|
||||
// lit material, which would increase binary size. Perhaps we could specially compile it
|
||||
// with this variant, but with the shader program cache in active development, the days of
|
||||
// the default material are numbered anyway.
|
||||
constexpr Variant::type_t vsmAndDep = Variant::MNT | Variant::DEP;
|
||||
return mDefinition.materialDomain == MaterialDomain::SURFACE && !mIsDefaultMaterial &&
|
||||
!mDefinition.hasCustomDepthShader && Variant::isValidDepthVariant(variant) &&
|
||||
(variant.key & vsmAndDep) != vsmAndDep;
|
||||
return (mDefinition.materialDomain == MaterialDomain::SURFACE) && !mIsDefaultMaterial &&
|
||||
!mDefinition.hasCustomDepthShader && Variant::isValidDepthVariant(variant);
|
||||
}
|
||||
|
||||
mutable utils::FixedCapacityVector<backend::Handle<backend::HwProgram>> mCachedPrograms;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "Filament"
|
||||
spec.version = "1.70.0"
|
||||
spec.version = "1.69.2"
|
||||
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.70.0/filament-v1.70.0-ios.tgz" }
|
||||
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.69.2/filament-v1.69.2-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
|
||||
|
||||
@@ -305,7 +305,7 @@ struct ShadowUib { // NOLINT(cppcoreguidelines-pro-type-member-init)
|
||||
float normalBias; // 4
|
||||
bool elvsm; // 4
|
||||
uint32_t layer; // 4
|
||||
float vsmExponent; // 4 // could be fp16
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ public:
|
||||
} backend;
|
||||
struct {
|
||||
bool check_crc32_after_loading = false;
|
||||
bool enable_material_instance_uniform_batching = false;
|
||||
bool enable_material_instance_uniform_batching = true;
|
||||
bool enable_fog_as_postprocess = false;
|
||||
} material;
|
||||
} features;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ struct ShadowData {
|
||||
mediump float normalBias;
|
||||
bool elvsm;
|
||||
mediump uint layer;
|
||||
mediump float vsmExponent;
|
||||
mediump uint reserved1;
|
||||
mediump uint reserved2;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
# Render Validation Sample & TUI
|
||||
|
||||
This project is an Android application for validating Filament render behavior on-device, primarily using bundled tests and goldens. It operates via `adb` intents to automate the generation, exporting, and running of test bundles.
|
||||
|
||||
## Automated Execution via ADB Intents
|
||||
|
||||
You can fully control the validation app directly from your host machine without touching the device screen. The app listens for specific intent extras when launched.
|
||||
|
||||
**General Command Structure:**
|
||||
```bash
|
||||
adb shell am start -n com.google.android.filament.validation/.MainActivity <extras>
|
||||
```
|
||||
|
||||
### Supported Intent Extras (Booleans)
|
||||
- `--ez auto_run true`: Immediately runs the loaded or default test upon startup.
|
||||
- `--ez generate_goldens true`: Forces the app to generate new golden reference images for the current test instead of comparing against existing ones.
|
||||
- `--ez auto_export true`: Automatically packages the generated tests/goldens into a `.zip` archive (`Default_Test_<timestamp>.zip`) to the app's files directory when finished.
|
||||
- `--ez auto_export_results true`: Automatically packages the comparison results and diff images into a `.zip` archive (`results_<timestamp>.zip`) to the app's files directory when finished.
|
||||
|
||||
### Supported Intent Extras (Strings)
|
||||
- `--es zip_path <filename.zip>`: Loads a specific test `.zip` bundle.
|
||||
- If you pass an absolute path, it loads from there.
|
||||
- If you pass just a filename (e.g. `Default_Test_123.zip`), it intelligently searches the app's external files directory (`/sdcard/Android/data/.../files`) to find it.
|
||||
|
||||
### Example ADB Workflows
|
||||
**Generate new goldens and export them as a test bundle:**
|
||||
```bash
|
||||
adb shell am start -n com.google.android.filament.validation/.MainActivity \
|
||||
--ez auto_run true \
|
||||
--ez generate_goldens true \
|
||||
--ez auto_export true
|
||||
```
|
||||
|
||||
**Run an existing specific test bundle and export the results:**
|
||||
```bash
|
||||
adb shell am start -n com.google.android.filament.validation/.MainActivity \
|
||||
--es zip_path "Default_Test_123.zip" \
|
||||
--ez auto_run true \
|
||||
--ez auto_export_results true
|
||||
```
|
||||
|
||||
### Manual File Management via ADB
|
||||
Depending on the Android version and device storage policy, the app's file location resides either on standard External Storage or strictly within inside its Internal Sandbox.
|
||||
|
||||
**App's External Storage (Default for most devices):**
|
||||
- **Push a test bundle to device:** `adb push <filename.zip> /sdcard/Android/data/com.google.android.filament.validation/files/`
|
||||
- **Pull a test bundle from device:** `adb pull /sdcard/Android/data/com.google.android.filament.validation/files/<filename.zip> .`
|
||||
- **Pull a result bundle from device:** `adb pull /sdcard/Android/data/com.google.android.filament.validation/files/results_<timestamp>.zip .`
|
||||
|
||||
**App's Internal Storage (If external is unavailable):**
|
||||
*Note: Due to security restrictions, you must use `run-as` to pipe data into/out of the application's secure sandbox.*
|
||||
- **Push a test bundle to device:**
|
||||
1. `adb push <filename.zip> /sdcard/Download/`
|
||||
2. `adb shell "run-as com.google.android.filament.validation cp /sdcard/Download/<filename.zip> files/"`
|
||||
- **Pull a test or result bundle from device:**
|
||||
`adb shell "run-as com.google.android.filament.validation cat files/<filename.zip>" > <filename.zip>`
|
||||
|
||||
---
|
||||
|
||||
## Python Terminal UI (TUI) Dashboard
|
||||
|
||||
To make managing tests and results easier, a Python-based Textual TUI (`validation_app.py`) is provided in the `validation_tui` directory. It automatically polls the connected device using ADB, acts as a GUI for the intent commands above, and handles downloading/uploading `.zip` bundles to circumvent Android's scoped storage limits.
|
||||
|
||||
### Setup
|
||||
1. Ensure you have Python 3 and `adb` installed and in your PATH.
|
||||
2. Navigate to the TUI directory: `cd test/render-validation`
|
||||
3. Create a virtual environment: `python3 -m venv venv`
|
||||
4. Activate it: `source venv/bin/activate` (Mac/Linux) or `venv\Scripts\activate` (Windows)
|
||||
5. Install requirements: `pip install -r requirements.txt` (Installs the `textual` framework)
|
||||
|
||||
### Usage
|
||||
Start the dashboard by running:
|
||||
```bash
|
||||
python validation_app.py
|
||||
```
|
||||
|
||||
### TUI Features
|
||||
- **Auto-Polling Mechanism**: Syncs the file lists with your device every 2 seconds.
|
||||
- **Generate Test/Result Buttons**: One-click execution of the `am start` intents.
|
||||
- **Upload Local Test Bundle**: Automatically pushes a local `.zip` file from your PC to the correct directory on the Android device.
|
||||
- **Per-File Actions**:
|
||||
- `▶` (Load): Restarts the app with `--es zip_path <filename>` to set it as the active test on device.
|
||||
- `↓` (Download): Pulls the `.zip` to your PC's current working directory.
|
||||
- `✎` (Rename): Quickly renames the file directly on the Android file system.
|
||||
- `✗` (Delete): Quickly removes the file from the Android device to free up storage.
|
||||
@@ -1 +0,0 @@
|
||||
textual>=0.52.0
|
||||
@@ -1,559 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import asyncio
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Horizontal, Vertical, ScrollableContainer
|
||||
from textual.widgets import Header, Footer, Button, Label, Static, ListItem, ListView, Input, DirectoryTree
|
||||
from textual.screen import Screen, ModalScreen
|
||||
from textual.reactive import reactive
|
||||
from textual.message import Message
|
||||
|
||||
PACKAGE = "com.google.android.filament.validation"
|
||||
EXTERNAL_DIR = f"/sdcard/Android/data/{PACKAGE}/files"
|
||||
INTERNAL_DIR = "files"
|
||||
|
||||
if not shutil.which("adb"):
|
||||
print("Error: 'adb' not found in PATH. Please install Android Platform Tools and ensure it's in your PATH.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
async def run_adb_cmd(*args):
|
||||
"""Run an adb command asynchronously and return code, stdout, stderr."""
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"adb", *args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
return proc.returncode, stdout.decode().strip(), stderr.decode().strip()
|
||||
|
||||
class DeviceSelectScreen(Screen):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label("Multiple Android devices detected. Please select one:", id="title")
|
||||
yield ListView(id="device_list")
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
code, out, err = await run_adb_cmd("devices")
|
||||
lv = self.query_one("#device_list", ListView)
|
||||
for line in out.splitlines()[1:]:
|
||||
if line.strip() and "device" in line:
|
||||
serial = line.split()[0]
|
||||
await lv.append(ListItem(Label(serial), id=f"dev_{serial}"))
|
||||
|
||||
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
||||
serial = event.item.id.replace("dev_", "")
|
||||
self.dismiss(serial)
|
||||
|
||||
class FileSelectScreen(ModalScreen[str]):
|
||||
CSS = """
|
||||
FileSelectScreen {
|
||||
align: center middle;
|
||||
}
|
||||
#dialog {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
padding: 1;
|
||||
border: thick $background 80%;
|
||||
background: $surface;
|
||||
}
|
||||
"""
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="dialog"):
|
||||
yield Label("Select a local .zip test bundle to upload:")
|
||||
yield DirectoryTree(os.getcwd())
|
||||
with Horizontal():
|
||||
yield Button("Cancel", id="btn_cancel_upload", variant="error")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "btn_cancel_upload":
|
||||
self.dismiss(None)
|
||||
|
||||
def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected) -> None:
|
||||
if str(event.path).endswith(".zip"):
|
||||
self.dismiss(str(event.path))
|
||||
else:
|
||||
self.app.notify("Please select a .zip file!", severity="warning")
|
||||
|
||||
class FileItem(Static):
|
||||
class FileChanged(Message):
|
||||
"""Emitted when a file is renamed or deleted."""
|
||||
pass
|
||||
class TestLoaded(Message):
|
||||
"""Emitted when a test is loaded on device."""
|
||||
def __init__(self, filename: str) -> None:
|
||||
self.filename = filename
|
||||
super().__init__()
|
||||
|
||||
def __init__(self, filename: str, filepath: str, is_internal: bool, serial: str, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.filename = filename
|
||||
self.filepath = filepath
|
||||
self.is_internal = is_internal
|
||||
self.serial = serial
|
||||
self.renaming = False
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(classes="file-item-container"):
|
||||
with Horizontal(id="file_row", classes="file-item-row"):
|
||||
yield Label(self.filename, id="lbl_filename", classes="file-name")
|
||||
|
||||
# Only show Load button for test configurations, not results
|
||||
if not self.filename.startswith("results_"):
|
||||
yield Button("▶", id="btn_load", variant="success", classes="compact-btn", tooltip="Load this test on device")
|
||||
|
||||
yield Button("↓", id="btn_download", variant="primary", classes="compact-btn", tooltip="Download to PC")
|
||||
yield Button("✎", id="btn_start_rename", variant="warning", classes="compact-btn", tooltip="Rename on device")
|
||||
yield Button("✗", id="btn_delete", variant="error", classes="compact-btn", tooltip="Delete on device")
|
||||
with Horizontal(id="rename_row", classes="rename-row"):
|
||||
yield Input(value=self.filename, id="inp_rename", classes="rename-input")
|
||||
yield Button("Save", id="btn_save_rename", variant="success", classes="compact-btn")
|
||||
yield Button("Cancel", id="btn_cancel_rename", classes="compact-btn")
|
||||
|
||||
def on_mount(self):
|
||||
self.query_one("#rename_row").display = False
|
||||
|
||||
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
btn_id = event.button.id
|
||||
|
||||
if btn_id == "btn_start_rename":
|
||||
self.query_one("#file_row").display = False
|
||||
self.query_one("#rename_row").display = True
|
||||
inp = self.query_one("#inp_rename", Input)
|
||||
inp.value = self.filename
|
||||
inp.focus()
|
||||
|
||||
elif btn_id == "btn_cancel_rename":
|
||||
self.query_one("#rename_row").display = False
|
||||
self.query_one("#file_row").display = True
|
||||
|
||||
elif btn_id == "btn_save_rename":
|
||||
new_name = self.query_one("#inp_rename", Input).value.strip()
|
||||
if new_name and new_name != self.filename:
|
||||
# Need to run ADB mv
|
||||
base_dir = self.filepath.rsplit('/', 1)[0]
|
||||
new_path = f"{base_dir}/{new_name}"
|
||||
|
||||
if self.is_internal:
|
||||
cmd = f"run-as {PACKAGE} mv {self.filepath} {new_path}"
|
||||
await run_adb_cmd("-s", self.serial, "shell", cmd)
|
||||
else:
|
||||
cmd = f"mv {self.filepath} {new_path}"
|
||||
await run_adb_cmd("-s", self.serial, "shell", cmd)
|
||||
|
||||
self.app.notify(f"Renamed {self.filename} to {new_name}")
|
||||
self.post_message(self.FileChanged())
|
||||
else:
|
||||
self.query_one("#rename_row").display = False
|
||||
self.query_one("#file_row").display = True
|
||||
|
||||
elif btn_id == "btn_delete":
|
||||
event.button.disabled = True
|
||||
if self.is_internal:
|
||||
cmd = f"run-as {PACKAGE} rm {self.filepath}"
|
||||
await run_adb_cmd("-s", self.serial, "shell", cmd)
|
||||
else:
|
||||
cmd = f"rm {self.filepath}"
|
||||
await run_adb_cmd("-s", self.serial, "shell", cmd)
|
||||
|
||||
self.app.notify(f"Deleted {self.filename}")
|
||||
self.post_message(self.FileChanged())
|
||||
|
||||
elif btn_id == "btn_download":
|
||||
event.button.disabled = True
|
||||
event.button.label = "..."
|
||||
self.app.notify(f"Downloading {self.filename} to {os.getcwd()}...", title="Download Started")
|
||||
self.run_worker(self.download_file(event.button), exclusive=True)
|
||||
|
||||
elif btn_id == "btn_load":
|
||||
self.app.notify(f"Loading {self.filename} on device...", title="Load Test")
|
||||
self.run_worker(self.load_on_device(), exclusive=True)
|
||||
|
||||
async def load_on_device(self) -> None:
|
||||
cmd_args = [
|
||||
"-s", self.serial, "shell", "am", "start",
|
||||
"-n", f"{PACKAGE}/.MainActivity",
|
||||
"--es", "zip_path", self.filename
|
||||
]
|
||||
await run_adb_cmd(*cmd_args)
|
||||
self.post_message(self.TestLoaded(self.filename))
|
||||
self.app.notify(f"Requested device to load {self.filename}", title="Load Complete")
|
||||
|
||||
async def download_file(self, button: Button) -> None:
|
||||
dest = os.path.join(os.getcwd(), self.filename)
|
||||
|
||||
if self.is_internal:
|
||||
# internal requires run-as
|
||||
cmd = f"adb -s {self.serial} shell \"run-as {PACKAGE} cat {self.filepath}\" > \"{dest}\""
|
||||
proc = await asyncio.create_subprocess_shell(cmd)
|
||||
await proc.communicate()
|
||||
else:
|
||||
# external can be a direct pull
|
||||
await run_adb_cmd("-s", self.serial, "pull", self.filepath, dest)
|
||||
|
||||
button.label = "Downloaded ✓"
|
||||
button.variant = "success"
|
||||
self.app.notify(f"Saved: {dest}", title="Complete")
|
||||
await asyncio.sleep(2)
|
||||
button.label = "Download"
|
||||
button.disabled = False
|
||||
button.variant = "primary"
|
||||
|
||||
class MainScreen(Screen):
|
||||
device_serial = reactive("")
|
||||
is_connected = reactive(True)
|
||||
current_test = reactive("")
|
||||
is_foreground = reactive(False)
|
||||
|
||||
def __init__(self, serial=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.device_serial = serial or ""
|
||||
self.tests_seen = set()
|
||||
self.results_seen = set()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Label(id="banner", classes="banner")
|
||||
|
||||
with Vertical(id="launch_container"):
|
||||
yield Label("App is not running in the foreground.", id="lbl_launch_msg")
|
||||
yield Button("Launch App", id="btn_launch_app", variant="success", classes="main-action-btn launch-btn")
|
||||
|
||||
with Horizontal(id="main_container"):
|
||||
with Vertical(classes="column"):
|
||||
yield Label("🧪 Tests", classes="col-title")
|
||||
yield Button("Upload Local Test Bundle", id="btn_upload_test", variant="primary", classes="main-action-btn")
|
||||
yield Button("Generate Goldens", id="btn_gen_goldens", variant="warning", classes="main-action-btn")
|
||||
yield Button("Generate Test Bundle", id="btn_gen_test", variant="success", classes="main-action-btn")
|
||||
yield ScrollableContainer(id="test_list")
|
||||
with Vertical(classes="column"):
|
||||
yield Label("📊 Results", classes="col-title")
|
||||
yield Label("Current: Default", id="lbl_current_test", classes="col-subtitle")
|
||||
yield Button("Run test", id="btn_gen_res", variant="success", classes="main-action-btn")
|
||||
yield ScrollableContainer(id="result_list")
|
||||
yield Footer()
|
||||
|
||||
def on_file_item_test_loaded(self, event: FileItem.TestLoaded) -> None:
|
||||
self.current_test = event.filename
|
||||
|
||||
def watch_current_test(self, new_test: str) -> None:
|
||||
from textual.css.query import NoMatches
|
||||
try:
|
||||
if new_test:
|
||||
self.query_one("#lbl_current_test", Label).update(f"Current: {new_test}")
|
||||
except NoMatches:
|
||||
pass
|
||||
|
||||
def watch_is_foreground(self, foreground: bool) -> None:
|
||||
from textual.css.query import NoMatches
|
||||
try:
|
||||
lc = self.query_one("#launch_container")
|
||||
mc = self.query_one("#main_container")
|
||||
if foreground:
|
||||
lc.display = False
|
||||
mc.display = True
|
||||
else:
|
||||
lc.display = True
|
||||
mc.display = False
|
||||
except NoMatches:
|
||||
pass
|
||||
|
||||
def on_file_item_file_changed(self, event: FileItem.FileChanged) -> None:
|
||||
"""Called when a child file is deleted or renamed to force a full refresh."""
|
||||
self.query_one("#test_list").remove_children()
|
||||
self.query_one("#result_list").remove_children()
|
||||
self.tests_seen.clear()
|
||||
self.results_seen.clear()
|
||||
|
||||
def watch_device_serial(self, serial: str) -> None:
|
||||
from textual.css.query import NoMatches
|
||||
try:
|
||||
self.query_one(Header).title = "Filament Validation Runner"
|
||||
self.query_one(Header).sub_title = f"Device: {serial}"
|
||||
except NoMatches:
|
||||
pass
|
||||
|
||||
def watch_is_connected(self, connected: bool) -> None:
|
||||
from textual.css.query import NoMatches
|
||||
try:
|
||||
banner = self.query_one("#banner", Label)
|
||||
if connected:
|
||||
banner.update("Status: Device Connected ✅")
|
||||
banner.remove_class("disconnected")
|
||||
else:
|
||||
banner.update("Status: Disconnected - Retrying... ❌")
|
||||
banner.add_class("disconnected")
|
||||
except NoMatches:
|
||||
pass
|
||||
|
||||
def on_mount(self) -> None:
|
||||
# Check ADB constantly every 2 seconds
|
||||
self.set_interval(2.0, self.poll_adb)
|
||||
|
||||
# Hide conditionally
|
||||
self.query_one("#launch_container").display = False
|
||||
self.query_one("#main_container").display = False
|
||||
|
||||
async def get_files(self, directory: str, is_internal: bool):
|
||||
if is_internal:
|
||||
code, out, err = await run_adb_cmd("-s", self.device_serial, "shell", f"run-as {PACKAGE} ls -1 {directory}")
|
||||
else:
|
||||
code, out, err = await run_adb_cmd("-s", self.device_serial, "shell", f"ls -1 {directory}")
|
||||
|
||||
if code == 0 and "Permission denied" not in err and "No such file or directory" not in err:
|
||||
return [f.strip() for f in out.splitlines() if f.strip() and f.strip().endswith(".zip")]
|
||||
return []
|
||||
|
||||
async def poll_adb(self) -> None:
|
||||
code, out, err = await run_adb_cmd("-s", self.device_serial, "shell", "echo ok")
|
||||
if code != 0 or "ok" not in out:
|
||||
self.is_connected = False
|
||||
self.is_foreground = False
|
||||
return
|
||||
|
||||
self.is_connected = True
|
||||
|
||||
# Check foreground
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
f"adb -s {self.device_serial} shell \"dumpsys window | grep mCurrentFocus\"",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, _ = await proc.communicate()
|
||||
out = stdout.decode('utf-8', errors='ignore')
|
||||
|
||||
self.is_foreground = PACKAGE in out
|
||||
|
||||
# We only really need to pull files if we are in the foreground
|
||||
if not self.is_foreground:
|
||||
return
|
||||
|
||||
current_tests = set()
|
||||
current_results = set()
|
||||
files_dict = {}
|
||||
|
||||
# 1. External
|
||||
ext_files = await self.get_files(EXTERNAL_DIR, False)
|
||||
for f in ext_files:
|
||||
files_dict[f] = (f"{EXTERNAL_DIR}/{f}", False)
|
||||
|
||||
# 2. Internal
|
||||
int_files = await self.get_files(INTERNAL_DIR, True)
|
||||
for f in int_files:
|
||||
if f not in files_dict:
|
||||
files_dict[f] = (f"{INTERNAL_DIR}/{f}", True)
|
||||
|
||||
for f, (path, is_int) in files_dict.items():
|
||||
if f.startswith("results_"):
|
||||
current_results.add((f, path, is_int))
|
||||
else:
|
||||
# Any other .zip file is considered a test
|
||||
current_tests.add((f, path, is_int))
|
||||
|
||||
# Check if any files were removed (e.g. wiped data)
|
||||
removed_tests = self.tests_seen - current_tests
|
||||
removed_results = self.results_seen - current_results
|
||||
|
||||
if removed_tests or removed_results:
|
||||
self.query_one("#test_list").remove_children()
|
||||
self.query_one("#result_list").remove_children()
|
||||
self.tests_seen.clear()
|
||||
self.results_seen.clear()
|
||||
|
||||
# Update lists safely if they are new
|
||||
new_tests = current_tests - self.tests_seen
|
||||
test_list = self.query_one("#test_list", ScrollableContainer)
|
||||
for f, path, is_int in new_tests:
|
||||
await test_list.mount(FileItem(f, path, is_int, self.device_serial))
|
||||
test_list.scroll_end(animate=False)
|
||||
self.tests_seen.update(new_tests)
|
||||
|
||||
new_results = current_results - self.results_seen
|
||||
result_list = self.query_one("#result_list", ScrollableContainer)
|
||||
for f, path, is_int in new_results:
|
||||
await result_list.mount(FileItem(f, path, is_int, self.device_serial))
|
||||
result_list.scroll_end(animate=False)
|
||||
self.results_seen.update(new_results)
|
||||
|
||||
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
btn_id = event.button.id
|
||||
if btn_id == "btn_gen_goldens":
|
||||
event.button.disabled = True
|
||||
event.button.label = "Generating Goldens..."
|
||||
self.run_worker(self.generate_and_auto_export(event.button, "golden"), exclusive=True)
|
||||
elif btn_id == "btn_gen_test":
|
||||
event.button.disabled = True
|
||||
event.button.label = "Generating & Exporting..."
|
||||
self.run_worker(self.generate_and_auto_export(event.button, "test"), exclusive=True)
|
||||
elif btn_id == "btn_gen_res":
|
||||
event.button.disabled = True
|
||||
event.button.label = "Generating & Exporting..."
|
||||
self.run_worker(self.generate_and_auto_export(event.button, "result"), exclusive=True)
|
||||
elif btn_id == "btn_upload_test":
|
||||
self.app.push_screen(FileSelectScreen(), self.handle_upload)
|
||||
elif btn_id == "btn_launch_app":
|
||||
self.run_worker(self.launch_app(), exclusive=True)
|
||||
|
||||
async def launch_app(self) -> None:
|
||||
self.app.notify("Commanding device to start the app...", title="Launching")
|
||||
await run_adb_cmd("-s", self.device_serial, "shell", "am", "start", "-n", f"{PACKAGE}/.MainActivity")
|
||||
|
||||
def handle_upload(self, file_path: str | None) -> None:
|
||||
if file_path:
|
||||
self.run_worker(self.upload_file(file_path), exclusive=True)
|
||||
|
||||
async def upload_file(self, file_path: str):
|
||||
filename = os.path.basename(file_path)
|
||||
self.app.notify(f"Uploading {filename}...", title="Upload Started")
|
||||
|
||||
# We push to EXTERNAL_DIR because `adb push` works seamlessly there
|
||||
dest = f"{EXTERNAL_DIR}/{filename}"
|
||||
code, out, err = await run_adb_cmd("-s", self.device_serial, "push", file_path, dest)
|
||||
|
||||
if code == 0:
|
||||
self.app.notify(f"Successfully uploaded to device.", title="Upload Finished")
|
||||
# Force a refresh
|
||||
self.tests_seen.clear()
|
||||
self.query_one("#test_list").remove_children()
|
||||
else:
|
||||
self.app.notify(f"Upload failed: {err}", title="Error", severity="error")
|
||||
|
||||
async def generate_and_auto_export(self, button: Button, mode: str):
|
||||
self.app.notify("Commanding device...", title="Working")
|
||||
|
||||
# Notice we removed '-S' so it doesn't force stop the activity first
|
||||
cmd_args = [
|
||||
"-s", self.device_serial, "shell", "am", "start",
|
||||
"-n", f"{PACKAGE}/.MainActivity"
|
||||
]
|
||||
|
||||
if mode == "golden":
|
||||
cmd_args.extend(["--ez", "generate_goldens", "true", "--ez", "auto_run", "true"])
|
||||
elif mode == "test":
|
||||
cmd_args.extend(["--ez", "auto_run", "true", "--ez", "auto_export", "true"])
|
||||
elif mode == "result":
|
||||
cmd_args.extend(["--ez", "auto_run", "true", "--ez", "auto_export_results", "true"])
|
||||
if self.current_test:
|
||||
cmd_args.extend(["--es", "zip_path", self.current_test])
|
||||
|
||||
await run_adb_cmd(*cmd_args)
|
||||
|
||||
# Allow time to run and let auto-polling grab the new file (if exporting)
|
||||
await asyncio.sleep(6)
|
||||
|
||||
button.disabled = False
|
||||
if mode == "golden":
|
||||
button.label = "Generate Goldens"
|
||||
elif mode == "test":
|
||||
button.label = "Generate & Download New Test"
|
||||
elif mode == "result":
|
||||
button.label = "Generate & Download Results"
|
||||
|
||||
self.app.notify("Action Finished")
|
||||
|
||||
class ValidationApp(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: vertical;
|
||||
}
|
||||
.banner {
|
||||
width: 100%;
|
||||
content-align: center middle;
|
||||
background: $success;
|
||||
color: $text;
|
||||
padding: 1;
|
||||
text-style: bold;
|
||||
}
|
||||
.banner.disconnected {
|
||||
background: $error;
|
||||
}
|
||||
#launch_container {
|
||||
align: center middle;
|
||||
height: 1fr;
|
||||
}
|
||||
#lbl_launch_msg {
|
||||
text-align: center;
|
||||
margin-bottom: 2;
|
||||
}
|
||||
.launch-btn {
|
||||
width: 30;
|
||||
}
|
||||
#main_container {
|
||||
height: 1fr;
|
||||
layout: horizontal;
|
||||
}
|
||||
.column {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
border: solid $accent;
|
||||
padding: 1;
|
||||
}
|
||||
.col-title {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
width: 100%;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
.col-subtitle {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin-bottom: 1;
|
||||
color: $secondary;
|
||||
text-style: italic;
|
||||
}
|
||||
.main-action-btn {
|
||||
width: 100%;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
.file-item-container {
|
||||
height: auto;
|
||||
padding-bottom: 1;
|
||||
border-bottom: solid $surface;
|
||||
}
|
||||
.file-item-row {
|
||||
height: 3;
|
||||
width: 100%;
|
||||
align: left middle;
|
||||
}
|
||||
.file-name {
|
||||
width: 1fr;
|
||||
content-align: left middle;
|
||||
}
|
||||
.compact-btn {
|
||||
min-width: 4;
|
||||
width: auto;
|
||||
margin-left: 1;
|
||||
}
|
||||
.rename-row {
|
||||
height: 3;
|
||||
width: 100%;
|
||||
}
|
||||
.rename-input {
|
||||
width: 1fr;
|
||||
}
|
||||
"""
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quit application")
|
||||
]
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
code, out, err = await run_adb_cmd("devices")
|
||||
devices = []
|
||||
for line in out.splitlines()[1:]:
|
||||
if line.strip() and "device" in line:
|
||||
devices.append(line.split()[0])
|
||||
|
||||
if len(devices) > 1:
|
||||
self.push_screen(DeviceSelectScreen(), self.start_main)
|
||||
elif len(devices) == 1:
|
||||
self.start_main(devices[0])
|
||||
else:
|
||||
self.notify("No devices connected via ADB!", severity="error")
|
||||
self.start_main("")
|
||||
|
||||
def start_main(self, serial: str) -> None:
|
||||
self.push_screen(MainScreen(serial=serial))
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = ValidationApp()
|
||||
app.run()
|
||||
@@ -76,11 +76,10 @@ into **branch** of `filament-assets`. This branch is paired with a PR or commit
|
||||
|
||||
As an example, imagine I am working on a PR, and I've uploaded my change, which is in a
|
||||
branch called `my-pr-branch`, to `filament`. This PR requires updating the golden. We would do
|
||||
it in the following fashion on a macOS machine:
|
||||
it in the following fashion
|
||||
|
||||
### Using a script to update the golden repo
|
||||
|
||||
- Make sure you've completed the steps in 'Setting up python'
|
||||
- Run interactive mode in the `update_golden.py` script.
|
||||
```
|
||||
python3 test/renderdiff/src/update_golden.py
|
||||
@@ -115,24 +114,6 @@ Doing the above has multiple effects:
|
||||
- If the PR is merged, then there is another workflow that will merge `my-pr-branch-golden`
|
||||
to the `main` branch of the golden repo.
|
||||
|
||||
### Automated update via commit message
|
||||
|
||||
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:
|
||||
|
||||
- In the commit message of your working branch on `filament`, add the following line:
|
||||
```
|
||||
RDIFF_ACCEPT_NEW_GOLDENS
|
||||
```
|
||||
|
||||
This has the following effects:
|
||||
- The presubmit test `test-renderdiff` will be bypassed (it will not perform rendering or
|
||||
comparison).
|
||||
- When the PR is merged, the postsubmit CI will automatically:
|
||||
1. Build Filament and generate the new images.
|
||||
2. Upload them to a temporary branch in `filament-assets`.
|
||||
3. Merge that branch into `main`.
|
||||
|
||||
## Viewing test results
|
||||
|
||||
We provide a viewer for looking at the result of a test run. The viewer is a webapp that can
|
||||
|
||||
@@ -18,7 +18,6 @@ import re
|
||||
from utils import execute, ArgParseImpl
|
||||
|
||||
RDIFF_UPDATE_GOLDEN_STR = 'RDIFF_BRANCH'
|
||||
RDIFF_ACCEPT_NEW_GOLDENS_STR = 'RDIFF_ACCEPT_NEW_GOLDENS'
|
||||
|
||||
def _parse_commit(commit_str):
|
||||
lines = commit_str.split('\n')
|
||||
@@ -43,11 +42,9 @@ def _parse_commit(commit_str):
|
||||
|
||||
if __name__ == "__main__":
|
||||
RE_STR = rf"{RDIFF_UPDATE_GOLDEN_STR}(?:\s)?\=(?:\s)?([a-zA-Z0-9\s\-\/]+)"
|
||||
RE_ACCEPT = rf"^{RDIFF_ACCEPT_NEW_GOLDENS_STR}$"
|
||||
|
||||
parser = ArgParseImpl()
|
||||
parser.add_argument('--file', help='A file containing the commit message')
|
||||
parser.add_argument('--mode', choices=['branch', 'accept_new_goldens'], default='branch', help='Mode of operation')
|
||||
args, _ = parser.parse_known_args(sys.argv[1:])
|
||||
|
||||
if not args.file:
|
||||
@@ -56,21 +53,14 @@ if __name__ == "__main__":
|
||||
with open(args.file, 'r') as f:
|
||||
msg = f.read()
|
||||
|
||||
to_update = []
|
||||
commit, title, description = _parse_commit(msg)
|
||||
|
||||
if args.mode == 'accept_new_goldens':
|
||||
for line in description:
|
||||
if re.match(RE_ACCEPT, line):
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
|
||||
# args.mode == 'branch'
|
||||
for line in description:
|
||||
m = re.match(RE_STR, line)
|
||||
if not m:
|
||||
continue
|
||||
print(m.group(1))
|
||||
sys.exit(0)
|
||||
exit(0)
|
||||
|
||||
# Always default to the main branch
|
||||
print('main')
|
||||
|
||||
@@ -16,7 +16,6 @@ import os
|
||||
import shutil
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from utils import execute, ArgParseImpl, mkdir_p
|
||||
|
||||
@@ -100,11 +99,10 @@ class GoldenManager:
|
||||
code, old_commit = execute(f'git log --format=%B -n 1', cwd=assets_dir)
|
||||
if tag and len(tag) > 0:
|
||||
old_commit += f'\nFILAMENT={tag}'
|
||||
with tempfile.NamedTemporaryFile('w', delete=False) as f:
|
||||
COMMIT_FILE = '/tmp/golden_commit.txt'
|
||||
with open(COMMIT_FILE, 'w') as f:
|
||||
f.write(old_commit)
|
||||
commit_file = f.name
|
||||
self._git_exec(f'commit --amend -F {commit_file}')
|
||||
os.remove(commit_file)
|
||||
self._git_exec(f'commit --amend -F {COMMIT_FILE}')
|
||||
|
||||
# Do the actual merge
|
||||
self._git_exec(f'checkout main')
|
||||
@@ -141,11 +139,11 @@ class GoldenManager:
|
||||
os.path.join(rdiff_dir, f))
|
||||
self._git_exec(f'add {os.path.join(GOLDENS_DIR, f)}')
|
||||
|
||||
with tempfile.NamedTemporaryFile('w', delete=False) as f:
|
||||
TMP_GOLDEN_COMMIT_FILE = '/tmp/golden_commit.txt'
|
||||
|
||||
with open(TMP_GOLDEN_COMMIT_FILE, 'w') as f:
|
||||
f.write(commit_msg)
|
||||
tmp_golden_commit_file = f.name
|
||||
self._git_exec(f'commit -a -F {tmp_golden_commit_file}')
|
||||
os.remove(tmp_golden_commit_file)
|
||||
self._git_exec(f'commit -a -F {TMP_GOLDEN_COMMIT_FILE}')
|
||||
if push_to_remote and \
|
||||
(self.access_token_ or self.access_type_ == ACCESS_TYPE_SSH):
|
||||
self._git_exec(f'push -f origin {branch}')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user