Compare commits

..
169 changed files with 1706 additions and 5281 deletions
+15 -17
View File
@@ -27,24 +27,17 @@ runs:
echo "$HASH" > /tmp/commit_hash.txt echo "$HASH" > /tmp/commit_hash.txt
- name: Find commit message (PR) - name: Find commit message (PR)
shell: bash shell: bash
id: checkout_code
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
run: | run: |
PR_NUMBER="${{ github.event.pull_request.number }}" BEFORE_HASH=$(git rev-parse HEAD)
# Fetch the head of the PR explicitly to handle forks. Depth 50 ensures we can traverse past recent merge commits. echo "hash=$BEFORE_HASH" >> "$GITHUB_OUTPUT"
git fetch --depth=50 origin "pull/$PR_NUMBER/head:pr-head" # Next we will checkout the actual head (not the merge commits) of the PR
AFTER_HASH="${{ github.event.pull_request.head.sha }}"
COMMIT_HASH=$(git log -1 --no-merges pr-head --format=%H) git checkout $AFTER_HASH
AUTHOR_NAME=$(git log -1 --no-merges pr-head --format=%an) COMMIT_MESSAGE=$(git log -1 --no-merges)
AUTHOR_EMAIL=$(git log -1 --no-merges pr-head --format=%ae) echo "$COMMIT_MESSAGE" > /tmp/commit_msg.txt
TSTAMP=$(git log -1 --no-merges pr-head --format=%aI) echo "$AFTER_HASH" > /tmp/commit_hash.txt
COMMIT_MESSAGE=$(git log -1 --no-merges pr-head --format=%B)
echo "commit $COMMIT_HASH" > /tmp/commit_msg.txt
echo "Author: ${AUTHOR_NAME}<${AUTHOR_EMAIL}>" >> /tmp/commit_msg.txt
echo "Date: ${TSTAMP}" >> /tmp/commit_msg.txt
echo "" >> /tmp/commit_msg.txt
echo "$COMMIT_MESSAGE" >> /tmp/commit_msg.txt
echo "$COMMIT_HASH" > /tmp/commit_hash.txt
- shell: bash - shell: bash
id: action_output id: action_output
run: | run: |
@@ -54,4 +47,9 @@ runs:
cat /tmp/commit_msg.txt >> "$GITHUB_OUTPUT" cat /tmp/commit_msg.txt >> "$GITHUB_OUTPUT"
echo "$DELIMITER" >> "$GITHUB_OUTPUT" echo "$DELIMITER" >> "$GITHUB_OUTPUT"
# Get the commit hash # Get the commit hash
echo "hash=$(cat /tmp/commit_hash.txt)" >> "$GITHUB_OUTPUT" echo "hash=$(cat /tmp/commit_hash.txt)" >> "$GITHUB_OUTPUT"
- name: Cleanup Find commit message (PR)
shell: bash
if: github.event_name == 'pull_request'
run: |
git checkout ${{ steps.checkout_code.outputs.hash }}
@@ -1,26 +0,0 @@
name: 'Renderdiff Generate'
description: 'Sets up prerequisites and runs the generate script for renderdiff'
runs:
using: "composite"
steps:
- uses: ./.github/actions/mac-prereq
- uses: ./.github/actions/get-gltf-assets
- uses: ./.github/actions/get-mesa
- uses: ./.github/actions/get-vulkan-sdk
- name: Prerequisites
run: |
# Must have at least clang-16 for a webgpu/dawn build.
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
shell: bash
- name: Generate images
run: |
TEST_DIR=test/renderdiff
source ${TEST_DIR}/src/preamble.sh
set -eux
bash ${TEST_DIR}/generate.sh
set +eux
shell: bash
- name: Build diffimg tool
run: |
./build.sh release diffimg
shell: bash
+7 -32
View File
@@ -10,27 +10,16 @@ jobs:
# a branch on filament-assets. # a branch on filament-assets.
update-renderdiff-goldens: update-renderdiff-goldens:
name: update-renderdiff-goldens name: update-renderdiff-goldens
runs-on: 'macos-14-xlarge' runs-on: 'ubuntu-24.04-4core'
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- id: get_commit_msg - id: get_commit_msg
uses: ./.github/actions/get-commit-msg uses: ./.github/actions/get-commit-msg
- name: Check if accepting new goldens - name: Build diffimg
id: check_accept run: ./build.sh release diffimg
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: Run update script - name: Run update script
env: env:
GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }} GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }}
@@ -38,24 +27,10 @@ jobs:
run: | run: |
GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py) GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py)
COMMIT_HASH="${{ steps.get_commit_msg.outputs.hash }}" 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 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 "branch==${GOLDEN_BRANCH}"
echo "hash==${COMMIT_HASH}" echo "hash==${COMMIT_HASH}"
python3 test/renderdiff/src/update_golden.py --branch=${GOLDEN_BRANCH} \ python3 test/renderdiff/src/update_golden.py --branch=${GOLDEN_BRANCH} \
+10 -15
View File
@@ -126,24 +126,18 @@ jobs:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
fetch-depth: 0 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 - id: get_commit_msg
uses: ./.github/actions/get-commit-msg uses: ./.github/actions/get-commit-msg
- name: Check if accepting new goldens - name: Prerequisites
id: check_accept
env:
COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }}
run: | run: |
if echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py --mode=accept_new_goldens; then # Must have at least clang-16 for a webgpu/dawn build.
echo "accept=true" >> "$GITHUB_OUTPUT" sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
else
echo "accept=false" >> "$GITHUB_OUTPUT"
fi
shell: bash shell: bash
- name: Renderdiff Generate
if: steps.check_accept.outputs.accept != 'true'
uses: ./.github/actions/renderdiff-generate
- name: Render and compare - name: Render and compare
if: steps.check_accept.outputs.accept != 'true'
id: render_compare id: render_compare
env: env:
COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }} COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }}
@@ -152,6 +146,9 @@ jobs:
source ${TEST_DIR}/src/preamble.sh source ${TEST_DIR}/src/preamble.sh
set -eux set -eux
GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 ${TEST_DIR}/src/commit_msg.py) 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 \ python3 ${TEST_DIR}/src/golden_manager.py \
--branch=${GOLDEN_BRANCH} \ --branch=${GOLDEN_BRANCH} \
@@ -175,12 +172,10 @@ jobs:
fi fi
shell: bash shell: bash
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: steps.check_accept.outputs.accept != 'true'
with: with:
name: presubmit-renderdiff-result name: presubmit-renderdiff-result
path: ./out/renderdiff path: ./out/renderdiff
- name: Compare result - name: Compare result
if: steps.check_accept.outputs.accept != 'true'
run: | run: |
ERROR_STR="${{ steps.render_compare.outputs.err }}" ERROR_STR="${{ steps.render_compare.outputs.err }}"
if [ -n "${ERROR_STR}" ]; then if [ -n "${ERROR_STR}" ]; then
-48
View File
@@ -188,54 +188,6 @@ jobs:
const globber = await glob.create(['out/*.aar', 'out/*.apk', 'out/*.tgz'].join('\n')); const globber = await glob.create(['out/*.aar', 'out/*.apk', 'out/*.tgz'].join('\n'));
await upload({ github, context }, await globber.glob(), TAG); await upload({ github, context }, await globber.glob(), TAG);
sonatype-publish:
name: sonatype-publish
runs-on: 'ubuntu-24.04-16core'
# Depends on the the Android build for the Android binaries.
# Depends on the Mac, Linux, and Windows builds for host tools.
needs: [build-mac, build-linux, build-windows, build-android]
if: github.event_name == 'release' || github.event.inputs.platform == 'android'
steps:
- name: Decide Git ref
id: git_ref
run: |
REF=${RELEASE_TAG:-${GITHUB_REF}}
TAG=${REF##*/}
echo "ref=${REF}" >> $GITHUB_OUTPUT
echo "tag=${TAG}" >> $GITHUB_OUTPUT
- uses: actions/checkout@v4.1.6
with:
ref: ${{ steps.git_ref.outputs.ref }}
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- uses: ./.github/actions/linux-prereq
- name: Download Android Release
run: |
gh release download ${TAG} \
--repo ${{ github.repository }} \
--pattern 'filament-*-android-native.tgz'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.git_ref.outputs.tag }}
- name: Unzip Android Release
run: |
mkdir -p out/android-release
tar -xzvf filament-${TAG}-android-native.tgz -C out/android-release/
env:
TAG: ${{ steps.git_ref.outputs.tag }}
- name: Publish To Sonatype
run: |
cd android
./gradlew publishToSonatype closeSonatypeStagingRepository
env:
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USERNAME }}
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }}
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.MAVEN_SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.MAVEN_SIGNING_PASSWORD }}
build-ios: build-ios:
name: build-ios name: build-ios
runs-on: macos-14-xlarge runs-on: macos-14-xlarge
+2
View File
@@ -6,3 +6,5 @@
appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md). appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md).
## Release notes for next branch cut ## Release notes for next branch cut
- engine: fix crash when using variance shadow maps
+2 -2
View File
@@ -31,7 +31,7 @@ repositories {
} }
dependencies { dependencies {
implementation 'com.google.android.filament:filament-android:1.70.1' implementation 'com.google.android.filament:filament-android:1.69.3'
} }
``` ```
@@ -50,7 +50,7 @@ Here are all the libraries available in the group `com.google.android.filament`:
iOS projects can use CocoaPods to install the latest release: iOS projects can use CocoaPods to install the latest release:
```shell ```shell
pod 'Filament', '~> 1.70.1' pod 'Filament', '~> 1.69.3'
``` ```
## Documentation ## Documentation
-12
View File
@@ -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 Instead, if you are authoring a PR for the main branch, add your release note to
[NEW_RELEASE_NOTES.md](./NEW_RELEASE_NOTES.md). [NEW_RELEASE_NOTES.md](./NEW_RELEASE_NOTES.md).
## v1.70.1
## 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.4
+2 -8
View File
@@ -40,21 +40,15 @@
// - Build and upload artifacts with ./gradlew publish // - Build and upload artifacts with ./gradlew publish
// - Close and release staging repo on Nexus with ./gradlew closeAndReleaseStagingRepository // - Close and release staging repo on Nexus with ./gradlew closeAndReleaseStagingRepository
// //
// The following properties need to be set (either in ~/gradle/gradle.properties, on the command // The following is needed in ~/gradle/gradle.properties:
// line, or as environment variables, e.g.: ORG_GRADLE_PROJECT_property=value):
// //
// sonatypeUsername=nexus_user // sonatypeUsername=nexus_user
// sonatypePassword=nexus_password // sonatypePassword=nexus_password
// //
// To sign with a key ring file:
// signing.keyId=pgp_key_id // signing.keyId=pgp_key_id
// signing.password=pgp_key_password // signing.password=pgp_key_password
// signing.secretKeyRingFile=/Users/user/.gnupg/maven_signing.key // signing.secretKeyRingFile=/Users/user/.gnupg/maven_signing.key
// //
// To sign with in-memory keys (useful for CI):,
// signingKey=ASCII armored key (begins with -----BEGIN PGP PRIVATE KEY BLOCK-----)
// signingPassword=key password
//
buildscript { buildscript {
def path = providers def path = providers
@@ -200,7 +194,7 @@ subprojects {
google() google()
} }
if (!name.startsWith("sample") && name != "filament-tools" && name != "gradle-plugin") { if (!name.startsWith("sample") && name != "filament-tools") {
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
@@ -25,7 +25,7 @@ configured, the corresponding task will be disabled.
```groovy ```groovy
plugins { plugins {
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
filament { filament {
+21
View File
@@ -0,0 +1,21 @@
plugins {
id 'groovy-gradle-plugin'
}
gradlePlugin {
plugins {
create("filament-plugin") {
id = "filament-plugin"
implementationClass = "com.google.android.filament.gradle.FilamentPlugin"
}
}
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation "com.google.gradle:osdetector-gradle-plugin:1.7.3"
}
@@ -60,10 +60,9 @@ class ToolsLocator {
def classifier = def classifier =
project.extensions.getByType(com.google.gradle.osdetector.OsDetector).classifier project.extensions.getByType(com.google.gradle.osdetector.OsDetector).classifier
// If com.google.android.filament.tools-dir is set to a non-empty string, we'll use it as // If com.google.android.filament.tools-dir is set, we'll use it as the tool's base path.
// the tool's base path.
def toolsDirProp = project.providers.gradleProperty("com.google.android.filament.tools-dir") def toolsDirProp = project.providers.gradleProperty("com.google.android.filament.tools-dir")
if (toolsDirProp.isPresent() && !toolsDirProp.get().trim().isEmpty()) { if (toolsDirProp.isPresent()) {
def toolsDir = toolsDirProp.get() def toolsDir = toolsDirProp.get()
def path = OperatingSystem.current().isWindows() ? def path = OperatingSystem.current().isWindows() ?
"${toolsDir}/bin/${name}.exe" : "${toolsDir}/bin/${name}.exe" :
@@ -2121,7 +2121,7 @@ public class View {
*/ */
public boolean highPrecision = false; public boolean highPrecision = false;
/** /**
* @deprecated has no effect. * VSM minimum variance scale, must be positive.
*/ */
public float minVarianceScale = 0.5f; public float minVarianceScale = 0.5f;
/** /**
@@ -106,8 +106,8 @@ class ModelViewer(
var skyboxCubemap: Texture? = null var skyboxCubemap: Texture? = null
private lateinit var displayHelper: DisplayHelper private lateinit var displayHelper: DisplayHelper
private var cameraManipulator: Manipulator? = null private lateinit var cameraManipulator: Manipulator
private var gestureDetector: GestureDetector? = null private lateinit var gestureDetector: GestureDetector
private var surfaceView: SurfaceView? = null private var surfaceView: SurfaceView? = null
private var textureView: TextureView? = null private var textureView: TextureView? = null
@@ -157,13 +157,15 @@ class ModelViewer(
surfaceView: SurfaceView, surfaceView: SurfaceView,
engine: Engine = Engine.create(), engine: Engine = Engine.create(),
uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK), uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK),
manipulator: Manipulator? = defaultCameraManipulator(surfaceView.width, surfaceView.height) manipulator: Manipulator? = null
) : this(engine, uiHelper) { ) : this(engine, uiHelper) {
cameraManipulator = manipulator ?: Manipulator.Builder()
.targetPosition(kDefaultObjectPosition.x, kDefaultObjectPosition.y, kDefaultObjectPosition.z)
.viewport(surfaceView.width, surfaceView.height)
.build(Manipulator.Mode.ORBIT)
this.surfaceView = surfaceView this.surfaceView = surfaceView
cameraManipulator = manipulator gestureDetector = GestureDetector(surfaceView, cameraManipulator)
cameraManipulator?.let { c ->
gestureDetector = GestureDetector(surfaceView, c)
}
displayHelper = DisplayHelper(surfaceView.context) displayHelper = DisplayHelper(surfaceView.context)
uiHelper.renderCallback = SurfaceCallback() uiHelper.renderCallback = SurfaceCallback()
uiHelper.attachTo(surfaceView) uiHelper.attachTo(surfaceView)
@@ -175,14 +177,15 @@ class ModelViewer(
textureView: TextureView, textureView: TextureView,
engine: Engine = Engine.create(), engine: Engine = Engine.create(),
uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK), uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK),
manipulator: Manipulator? = defaultCameraManipulator(textureView.width, textureView.height) manipulator: Manipulator? = null
) : this(engine, uiHelper) { ) : this(engine, uiHelper) {
cameraManipulator = manipulator cameraManipulator = manipulator ?: Manipulator.Builder()
.targetPosition(kDefaultObjectPosition.x, kDefaultObjectPosition.y, kDefaultObjectPosition.z)
.viewport(textureView.width, textureView.height)
.build(Manipulator.Mode.ORBIT)
this.textureView = textureView this.textureView = textureView
cameraManipulator = manipulator gestureDetector = GestureDetector(textureView, cameraManipulator)
cameraManipulator?.let { c ->
gestureDetector = GestureDetector(textureView, c)
}
displayHelper = DisplayHelper(textureView.context) displayHelper = DisplayHelper(textureView.context)
uiHelper.renderCallback = SurfaceCallback() uiHelper.renderCallback = SurfaceCallback()
uiHelper.attachTo(textureView) uiHelper.attachTo(textureView)
@@ -299,13 +302,11 @@ class ModelViewer(
asset?.let { populateScene(it) } asset?.let { populateScene(it) }
// Extract the camera basis from the helper and push it to the Filament camera. // Extract the camera basis from the helper and push it to the Filament camera.
cameraManipulator?.let { cm -> cameraManipulator.getLookAt(eyePos, target, upward)
cm.getLookAt(eyePos, target, upward) camera.lookAt(
camera.lookAt(
eyePos[0], eyePos[1], eyePos[2], eyePos[0], eyePos[1], eyePos[2],
target[0], target[1], target[2], target[0], target[1], target[2],
upward[0], upward[1], upward[2]) upward[0], upward[1], upward[2])
}
// Render the scene, unless the renderer wants to skip the frame. // Render the scene, unless the renderer wants to skip the frame.
if (renderer.beginFrame(swapChain!!, frameTimeNanos)) { if (renderer.beginFrame(swapChain!!, frameTimeNanos)) {
@@ -397,7 +398,7 @@ class ModelViewer(
* Handles a [MotionEvent] to enable one-finger orbit, two-finger pan, and pinch-to-zoom. * Handles a [MotionEvent] to enable one-finger orbit, two-finger pan, and pinch-to-zoom.
*/ */
fun onTouchEvent(event: MotionEvent) { fun onTouchEvent(event: MotionEvent) {
gestureDetector?.onTouchEvent(event) gestureDetector.onTouchEvent(event)
} }
@SuppressWarnings("ClickableViewAccessibility") @SuppressWarnings("ClickableViewAccessibility")
@@ -450,7 +451,7 @@ class ModelViewer(
override fun onResized(width: Int, height: Int) { override fun onResized(width: Int, height: Int) {
view.viewport = Viewport(0, 0, width, height) view.viewport = Viewport(0, 0, width, height)
cameraManipulator?.setViewport(width, height) cameraManipulator.setViewport(width, height)
updateCameraProjection() updateCameraProjection()
synchronizePendingFrames(engine) synchronizePendingFrames(engine)
} }
@@ -467,11 +468,5 @@ class ModelViewer(
companion object { companion object {
private val kDefaultObjectPosition = Float3(0.0f, 0.0f, -4.0f) private val kDefaultObjectPosition = Float3(0.0f, 0.0f, -4.0f)
private fun defaultCameraManipulator(width: Int, height: Int) : Manipulator {
return Manipulator.Builder()
.targetPosition(kDefaultObjectPosition.x, kDefaultObjectPosition.y, kDefaultObjectPosition.z)
.viewport(width, height)
.build(Manipulator.Mode.ORBIT)
}
} }
} }
-31
View File
@@ -1,31 +0,0 @@
plugins {
id 'groovy-gradle-plugin'
id 'com.gradle.plugin-publish' version '2.1.0'
}
group = "com.google.android.filament"
version = "0.1.0"
gradlePlugin {
website = "https://github.com/google/filament/tree/main/android/gradle-plugin"
vcsUrl = "https://github.com/google/filament/tree/main/android/gradle-plugin"
plugins {
create("filament-tools") {
id = "com.google.android.filament-tools"
displayName = "Filament Tools Gradle Plugin"
description = "A plugin that helps integrate Filament into Android projects"
tags.addAll('android', 'graphics', 'rendering', 'filament', '3d', 'gltf', 'native')
implementationClass = "com.google.android.filament.gradle.FilamentPlugin"
}
}
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation "com.google.gradle:osdetector-gradle-plugin:1.7.3"
}
+1 -1
View File
@@ -1,5 +1,5 @@
GROUP=com.google.android.filament GROUP=com.google.android.filament
VERSION_NAME=1.70.1 VERSION_NAME=1.69.3
POM_DESCRIPTION=Real-time physically based rendering engine for Android. POM_DESCRIPTION=Real-time physically based rendering engine for Android.
-7
View File
@@ -94,13 +94,6 @@ afterEvaluate { project ->
} }
signing { signing {
def signingKey = findProperty("signingKey")
def signingPassword = findProperty("signingPassword")
if (signingKey && signingPassword) {
println("Signing with in-memory keys")
useInMemoryPgpKeys(signingKey, signingPassword)
}
publishing.publications.all { publication -> publishing.publications.all { publication ->
sign publication sign publication
} }
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
+1 -1
View File
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -26,8 +26,7 @@ tasks.register('copyDamagedHelmetGltf', Copy) {
preBuild.dependsOn copyDamagedHelmetGltf preBuild.dependsOn copyDamagedHelmetGltf
clean.doFirst { clean.doFirst {
delete "src/main/assets/envs" delete "src/main/assets"
delete "src/main/assets/models"
} }
android { android {
@@ -49,7 +48,6 @@ android {
dependencies { dependencies {
implementation deps.kotlin implementation deps.kotlin
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.10.0'
implementation deps.coroutines.core implementation deps.coroutines.core
implementation project(':filament-android') implementation project(':filament-android')
implementation project(':gltfio-android') implementation project(':gltfio-android')
@@ -12,11 +12,10 @@
android:supportsRtl="true" android:supportsRtl="true"
android:largeHeap="true" android:largeHeap="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:theme="@style/Theme.Material3.DayNight.NoActionBar"> android:theme="@android:style/Theme.NoTitleBar">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:screenOrientation="fullSensor"> android:screenOrientation="fullSensor">
<intent-filter> <intent-filter>
@@ -1,899 +0,0 @@
{
"name": "Default Test",
"backends": [
"opengl"
],
"models": {
"DamagedHelmet": "helmet.glb"
},
"presets": [
{
"name": "base",
"models": [
"DamagedHelmet"
],
"rendering": {
"camera": {
"enabled": true,
"horizontalFov": 45.0,
"center": [
0,
0,
0
],
"lookAt": [
0,
0,
-1
]
},
"view": {
"postProcessingEnabled": true,
"dithering": "NONE"
},
"tolerance": {
"maxAbsDiff": 0.1,
"maxFailingPixelsFraction": 0.0
}
}
},
{
"name": "tilted",
"rendering": {
"camera": {
"enabled": true,
"horizontalFov": 45.0,
"center": [
-4,
-2,
-3
],
"lookAt": [
0,
0,
-4
]
}
}
}
],
"tests": [
{
"name": "basic",
"apply_presets": [
"base"
]
},
{
"name": "rotated",
"apply_presets": [
"base",
"tilted"
]
},
{
"name": "ssao",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true
}
},
{
"name": "msaa",
"apply_presets": [
"base"
],
"rendering": {
"view.msaa.enabled": true
}
},
{
"name": "bloom",
"apply_presets": [
"base",
"tilted"
],
"rendering": {
"view.bloom.enabled": true
}
},
{
"name": "aa_none",
"apply_presets": [
"base"
],
"rendering": {
"view.antiAliasing": "NONE"
}
},
{
"name": "aa_fxaa",
"apply_presets": [
"base"
],
"rendering": {
"view.antiAliasing": "FXAA"
}
},
{
"name": "dithering_none",
"apply_presets": [
"base"
],
"rendering": {
"view.dithering": "NONE"
}
},
{
"name": "msaa_8",
"apply_presets": [
"base"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.sampleCount": 8
}
},
{
"name": "taa_custom",
"apply_presets": [
"base"
],
"rendering": {
"view.taa.enabled": true,
"view.taa.feedback": 0.2,
"view.taa.jitterPattern": "HALTON_23_X16"
}
},
{
"name": "ssao_gtao",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.aoType": "GTAO"
}
},
{
"name": "ssao_sao",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.aoType": "SAO"
}
},
{
"name": "ssao_radius_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.radius": 1.0
}
},
{
"name": "ssao_radius_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.radius": 0.1
}
},
{
"name": "ssao_power_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.power": 2.0
}
},
{
"name": "ssao_power_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.power": 0.5
}
},
{
"name": "ssao_bias_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.bias": 0.05
}
},
{
"name": "ssao_resolution_half",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.resolution": 0.5
}
},
{
"name": "ssao_resolution_full",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.resolution": 1.0
}
},
{
"name": "ssao_intensity_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.intensity": 2.0
}
},
{
"name": "ssao_bent_normals",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.bentNormals": true
}
},
{
"name": "ssao_quality_ultra",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.quality": "ULTRA"
}
},
{
"name": "ssao_quality_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.quality": "LOW"
}
},
{
"name": "ssao_lowPassFilter_ultra",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.lowPassFilter": "ULTRA"
}
},
{
"name": "ssao_upsampling_ultra",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.upsampling": "ULTRA"
}
},
{
"name": "ssr_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true
}
},
{
"name": "ssr_thickness_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.5
}
},
{
"name": "ssr_bias_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.bias": 0.1
}
},
{
"name": "ssr_maxDistance_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.maxDistance": 5.0
}
},
{
"name": "ssr_stride_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.stride": 4.0
}
},
{
"name": "ssr_stride_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.stride": 1.0
}
},
{
"name": "ssr_thickness_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.01
}
},
{
"name": "ssr_maxDistance_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.maxDistance": 1.0
}
},
{
"name": "ssr_bias_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.bias": 0.001
}
},
{
"name": "ssr_quality_combo",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.2,
"view.screenSpaceReflections.stride": 2.0
}
},
{
"name": "bloom_levels_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.levels": 8
}
},
{
"name": "bloom_levels_low",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.levels": 4
}
},
{
"name": "bloom_resolution_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.resolution": 1024
}
},
{
"name": "bloom_strength_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.strength": 0.5
}
},
{
"name": "bloom_strength_low",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.strength": 0.01
}
},
{
"name": "bloom_blendMode_interpolate",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.blendMode": "INTERPOLATE"
}
},
{
"name": "bloom_no_threshold",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.threshold": false
}
},
{
"name": "bloom_quality_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.quality": "HIGH"
}
},
{
"name": "bloom_lensflare",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true
}
},
{
"name": "bloom_lensflare_no_starburst",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.starburst": false
}
},
{
"name": "bloom_lensflare_chromatic",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.chromaticAberration": 0.05
}
},
{
"name": "bloom_lensflare_ghosts",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.ghostCount": 8
}
},
{
"name": "bloom_lensflare_ghostSpacing",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.ghostSpacing": 0.8
}
},
{
"name": "bloom_halo_thick",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.haloThickness": 0.2
}
},
{
"name": "bloom_halo_radius",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.haloRadius": 0.5
}
},
{
"name": "dof_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true
}
},
{
"name": "dof_cocScale_high",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.cocScale": 2.0
}
},
{
"name": "dof_cocScale_low",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.cocScale": 0.5
}
},
{
"name": "dof_cocAspectRatio",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.cocAspectRatio": 2.0
}
},
{
"name": "dof_maxApertureDiameter",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.maxApertureDiameter": 0.05
}
},
{
"name": "dof_filter_none",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.filter": "NONE"
}
},
{
"name": "dof_nativeResolution",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.nativeResolution": true
}
},
{
"name": "dof_rings_high",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.foregroundRingCount": 5,
"view.dof.backgroundRingCount": 5
}
},
{
"name": "dof_rings_low",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.foregroundRingCount": 3,
"view.dof.backgroundRingCount": 3
}
},
{
"name": "dof_max_coc",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.maxForegroundCOC": 16,
"view.dof.maxBackgroundCOC": 16
}
},
{
"name": "fog_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true
}
},
{
"name": "fog_distance",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.distance": 10.0
}
},
{
"name": "fog_cutOffDistance",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.cutOffDistance": 100.0
}
},
{
"name": "fog_maximumOpacity",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.maximumOpacity": 0.5
}
},
{
"name": "fog_height",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.height": 5.0
}
},
{
"name": "fog_heightFalloff",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.heightFalloff": 0.5
}
},
{
"name": "fog_density_high",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.density": 0.5
}
},
{
"name": "fog_inScatteringStart",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.inScatteringStart": 5.0
}
},
{
"name": "fog_inScatteringSize",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.inScatteringSize": 10.0
}
},
{
"name": "fog_fogColorFromIbl",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.fogColorFromIbl": true
}
},
{
"name": "vignette_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true
}
},
{
"name": "vignette_midPoint",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.midPoint": 0.8
}
},
{
"name": "vignette_roundness_circle",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.roundness": 1.0
}
},
{
"name": "vignette_roundness_rect",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.roundness": 0.0
}
},
{
"name": "vignette_feather_sharp",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.feather": 0.1
}
},
{
"name": "cg_filmic",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "FILMIC"
}
},
{
"name": "cg_aces",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "ACES"
}
},
{
"name": "cg_agx",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "AGX"
}
},
{
"name": "cg_pbr_neutral",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "PBR_NEUTRAL"
}
},
{
"name": "cg_contrast_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.contrast": 1.5
}
},
{
"name": "cg_saturation_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.saturation": 1.5
}
},
{
"name": "cg_exposure_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.exposure": 1.0
}
},
{
"name": "shadow_vsm",
"apply_presets": [
"base"
],
"rendering": {
"view.shadowType": "VSM"
}
},
{
"name": "shadow_vsm_anisotropy",
"apply_presets": [
"base"
],
"rendering": {
"view.shadowType": "VSM",
"view.vsmShadowOptions.anisotropy": 2
}
},
{
"name": "shadow_vsm_highPrecision",
"apply_presets": [
"base"
],
"rendering": {
"view.shadowType": "VSM",
"view.vsmShadowOptions.highPrecision": true
}
}
]
}
@@ -17,35 +17,36 @@
package com.google.android.filament.validation package com.google.android.filament.validation
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.Html
import android.util.Log import android.util.Log
import android.view.Choreographer import android.view.Choreographer
import android.view.SurfaceView import android.view.SurfaceView
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Spinner import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import com.google.android.filament.utils.KTX1Loader
import com.google.android.filament.utils.ModelViewer import com.google.android.filament.utils.ModelViewer
import com.google.android.filament.utils.Utils import com.google.android.filament.utils.Utils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.text.SimpleDateFormat import android.widget.ArrayAdapter
import java.util.Date import android.widget.Button
import java.util.Locale import android.widget.Spinner
import android.widget.AdapterView
class MainActivity : Activity(), ValidationRunner.Callback { class MainActivity : Activity(), ValidationRunner.Callback {
@@ -61,17 +62,13 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private lateinit var choreographer: Choreographer private lateinit var choreographer: Choreographer
private lateinit var modelViewer: ModelViewer private lateinit var modelViewer: ModelViewer
private lateinit var statusTextView: TextView private lateinit var statusTextView: TextView
private lateinit var testResultsHeader: TextView
private lateinit var resultsContainer: LinearLayout private lateinit var resultsContainer: LinearLayout
private lateinit var inputManager: ValidationInputManager private lateinit var inputManager: ValidationInputManager
private var currentInput: ValidationInputManager.ValidationInput? = null private var currentInput: ValidationInputManager.ValidationInput? = null
private lateinit var modeSpinner: Spinner
// UI Elements
private lateinit var runButton: Button private lateinit var runButton: Button
private lateinit var loadButton: Button
private lateinit var optionsButton: Button
private var resultManager: ValidationResultManager? = null private var resultManager: ValidationResultManager? = null
private var validationRunner: ValidationRunner? = null private var validationRunner: ValidationRunner? = null
// Frame callback // Frame callback
@@ -92,53 +89,25 @@ class MainActivity : Activity(), ValidationRunner.Callback {
surfaceView.holder.setFixedSize(512, 512) surfaceView.holder.setFixedSize(512, 512)
statusTextView = findViewById(R.id.status_text) 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) resultsContainer = findViewById(R.id.results_container)
runButton = findViewById(R.id.run_button) // Setup Spinner
loadButton = findViewById(R.id.load_button) val modes = arrayOf("Run Validation", "Generate Goldens")
optionsButton = findViewById(R.id.options_button) 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 // Setup Run Button
runButton.setOnClickListener { runButton.setOnClickListener {
currentInput?.let { input -> currentInput?.let { input ->
// Always use the generateGoldens flag from the intent/input val generateGoldens = modeSpinner.selectedItemPosition == 1
startValidation(input) 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) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
choreographer = Choreographer.getInstance() choreographer = Choreographer.getInstance()
@@ -151,134 +120,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
handleIntent() handleIntent()
} }
private fun showLoadDialog() {
val exportDir = getExternalFilesDir(null) ?: filesDir
// Filter out result zips (starting with "results_") to only show test bundles
val zips = exportDir.listFiles { _, name ->
name.endsWith(".zip") && !name.startsWith("results_")
}?.sortedByDescending { it.lastModified() } ?: emptyList()
if (zips.isEmpty()) {
AlertDialog.Builder(this)
.setTitle("Load Test")
.setMessage("No test bundles found.")
.setPositiveButton("OK", null)
.show()
return
}
val builder = AlertDialog.Builder(this)
builder.setTitle("Select Test Bundle")
val items = zips.map { it.name }.toTypedArray()
builder.setItems(items) { dialog, which ->
val selectedFile = zips[which]
loadZipBundle(selectedFile)
dialog.dismiss()
}
builder.setNegativeButton("Cancel", null)
builder.show()
}
private fun showTestAdbInfo() {
val exportDir = getExternalFilesDir(null) ?: filesDir
val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder()
message.append("Storage Path: $path<br><br>")
message.append("<b>--- PULL FROM DEVICE ---</b><br>")
if (isInternal) {
message.append("<tt>adb shell \"run-as $packageName cat files/&lt;filename&gt;\" &gt; &lt;filename&gt;</tt><br><br>")
} else {
message.append("<tt>adb pull $path/&lt;filename&gt; .</tt><br><br>")
}
message.append("<b>--- PUSH TO DEVICE ---</b><br>")
if (isInternal) {
message.append("1. <tt>adb push &lt;filename&gt; /sdcard/Download/</tt><br>")
message.append("2. <tt>adb shell \"run-as $packageName cp /sdcard/Download/&lt;filename&gt; files/\"</tt><br>")
} else {
message.append("<tt>adb push &lt;filename&gt; $path/</tt><br>")
}
message.append("<br>Note: Use underscores instead of spaces in &lt;filename&gt;.")
AlertDialog.Builder(this)
.setTitle("Test Bundle ADB Info")
.setMessage(Html.fromHtml(message.toString(), Html.FROM_HTML_MODE_LEGACY))
.setPositiveButton("OK", null)
.show()
}
private fun showResultAdbInfo() {
val exportDir = getExternalFilesDir(null) ?: filesDir
val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder()
message.append("<b>--- PULL RESULTS ---</b><br>")
if (isInternal) {
message.append("<tt>adb shell \"run-as $packageName cat files/&lt;filename&gt;\" &gt; &lt;filename&gt;</tt><br><br>")
} else {
message.append("<tt>adb pull $path/&lt;filename&gt; .</tt><br><br>")
}
message.append("<b>--- AVAILABLE RESULTS ---</b><br>")
val zips = exportDir.listFiles { _, name ->
name.endsWith(".zip") && name.startsWith("results_")
}?.sortedByDescending { it.lastModified() } ?: emptyList()
if (zips.isEmpty()) {
message.append("No result zips found.<br>")
} else {
zips.forEach { file ->
message.append("${file.name}<br>")
}
}
AlertDialog.Builder(this)
.setTitle("Result ADB Info")
.setMessage(Html.fromHtml(message.toString(), Html.FROM_HTML_MODE_LEGACY))
.setPositiveButton("OK", null)
.show()
}
private fun loadZipBundle(file: File) {
statusTextView.text = "Loading ${file.name}..."
CoroutineScope(Dispatchers.Main).launch {
try {
val config = inputManager.loadFromZip(file)
val baseDir = getExternalFilesDir(null) ?: filesDir
val outputDir = File(baseDir, "validation_results").apply { mkdirs() }
// Clear existing results UI and state
resultsContainer.removeAllViews()
resultManager = null
val newInput = ValidationInputManager.ValidationInput(
config = config,
outputDir = outputDir,
generateGoldens = false,
autoRun = false,
autoExport = false,
autoExportResults = false,
sourceZip = file
)
currentInput = newInput
statusTextView.text = "Loaded ${config.name}"
Log.i(TAG, "Setting header to: Test Results: ${config.name}")
testResultsHeader.text = "${config.name}"
} catch (e: Exception) {
Log.e(TAG, "Failed to load zip", e)
statusTextView.text = "Error: ${e.message}"
}
}
}
private fun createIndirectLight() { private fun createIndirectLight() {
try { try {
val engine = modelViewer.engine val engine = modelViewer.engine
@@ -315,18 +156,12 @@ class MainActivity : Activity(), ValidationRunner.Callback {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
try { try {
val input = inputManager.resolveConfig(intent) 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 currentInput = input
if (input.autoRun) { // Sync spinner with intent
startValidation(input) modeSpinner.setSelection(if (input.generateGoldens) 1 else 0)
} else {
// Just show status startValidation(input)
statusTextView.text = "Ready: ${input.config.name}"
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to resolve config", e) Log.e(TAG, "Failed to resolve config", e)
statusTextView.text = "Error: ${e.message}" 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, "Starting validation with config: ${input.config.name}")
Log.i(TAG, "Output dir: ${input.outputDir.absolutePath}") Log.i(TAG, "Output dir: ${input.outputDir.absolutePath}")
testResultsHeader.text = "${input.config.name}"
resultManager = ValidationResultManager(input.outputDir) resultManager = ValidationResultManager(input.outputDir)
validationRunner = ValidationRunner(this, modelViewer, input.config, resultManager!!) validationRunner = ValidationRunner(this, modelViewer, input.config, resultManager!!)
@@ -349,6 +182,9 @@ class MainActivity : Activity(), ValidationRunner.Callback {
validationRunner?.generateGoldens = input.generateGoldens validationRunner?.generateGoldens = input.generateGoldens
validationRunner?.start() validationRunner?.start()
// Sync spinner in case it was called programmatically or changed implicitly
modeSpinner.setSelection(if (input.generateGoldens) 1 else 0)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to start validation", e) Log.e(TAG, "Failed to start validation", e)
statusTextView.text = "Error: ${e.message}" statusTextView.text = "Error: ${e.message}"
@@ -360,12 +196,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
choreographer.postFrameCallback(frameScheduler) choreographer.postFrameCallback(frameScheduler)
} }
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleIntent()
}
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
choreographer.removeFrameCallback(frameScheduler) choreographer.removeFrameCallback(frameScheduler)
@@ -391,30 +221,13 @@ class MainActivity : Activity(), ValidationRunner.Callback {
resultContainer.orientation = LinearLayout.VERTICAL resultContainer.orientation = LinearLayout.VERTICAL
resultContainer.setPadding(0, 10, 0, 20) resultContainer.setPadding(0, 10, 0, 20)
// Header Layout // Header
val headerRow = LinearLayout(this) val resultView = TextView(this)
headerRow.orientation = LinearLayout.HORIZONTAL resultView.text = "${result.testName}: ${if(result.passed) "PASS" else "FAIL"} (Diff: ${result.diffMetric})"
headerRow.gravity = android.view.Gravity.CENTER_VERTICAL resultView.setTextColor(if(result.passed) Color.GREEN else Color.RED)
resultView.textSize = 16f
// Status Icon + Name resultView.setTypeface(null, android.graphics.Typeface.BOLD)
val statusView = TextView(this) resultContainer.addView(resultView)
val icon = if (result.passed) "✔" else "✖"
statusView.text = "$icon ${result.testName}"
statusView.setTextColor(
if (result.passed) Color.parseColor("#4CAF50") else Color.parseColor("#F44336")
)
statusView.textSize = 12f
statusView.setTypeface(null, android.graphics.Typeface.BOLD)
headerRow.addView(statusView)
// Diff Metric (only show if it's relevant/there's a diff or we just always show it like before)
val diffView = TextView(this)
diffView.text = " (Diff: ${result.diffMetric})"
diffView.textSize = 12f
diffView.setTextColor(Color.GRAY)
headerRow.addView(diffView)
resultContainer.addView(headerRow)
// Images Row // Images Row
val imagesRow = LinearLayout(this) val imagesRow = LinearLayout(this)
@@ -428,7 +241,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
val labelView = TextView(this) val labelView = TextView(this)
labelView.text = label labelView.text = label
labelView.textSize = 9f labelView.textSize = 12f
container.addView(labelView) container.addView(labelView)
val iv = ImageView(this) val iv = ImageView(this)
@@ -461,46 +274,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onAllTestsFinished() { override fun onAllTestsFinished() {
runOnUiThread { runOnUiThread {
statusTextView.text = "All tests finished!" statusTextView.text = "All tests finished!"
Log.i(TAG, "All tests finished " + if (currentInput?.autoExport == true) "Exporting bundle" else "x") Log.i(TAG, "All tests finished")
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")
}
} }
} }
@@ -527,3 +301,50 @@ class MainActivity : Activity(), ValidationRunner.Callback {
} }
} }
} }
/*
* Scripts for reference:
*
* generate_goldens.sh:
* --------------------
* #!/bin/bash
* set -e
*
* # Config path (on device)
* CONFIG_PATH=$1
* if [ -z "$CONFIG_PATH" ]; then
* echo "Usage: $0 <device_config_path>"
* echo "Example: $0 /sdcard/Android/data/com.google.android.filament.validation/files/default_test.json"
* exit 1
* fi
*
* echo "Starting Golden Generation for $CONFIG_PATH..."
* adb shell am force-stop com.google.android.filament.validation
* adb shell am start -n com.google.android.filament.validation/.MainActivity \
* -e test_config "$CONFIG_PATH" \
* --ez generate_goldens true
*
* echo "Check device or logcat for progress."
* echo "adb logcat -s FilamentValidation:I ValidationRunner:I"
* echo "To pull results: ./samples/sample-render-validation/pull_goldens.sh"
*
* pull_goldens.sh:
* ----------------
* #!/bin/bash
* set -e
*
* # Default destination is local golden directory relative to script
* SCRIPT_DIR=$(cd $(dirname $0); pwd)
* DEST_DIR=${1:-"$SCRIPT_DIR/golden"}
*
* echo "Pulling goldens to $DEST_DIR..."
* mkdir -p "$DEST_DIR"
*
* # Path on device
* DEVICE_GOLDEN_DIR="/storage/emulated/0/Android/data/com.google.android.filament.validation/files/golden/."
*
* adb pull "$DEVICE_GOLDEN_DIR" "$DEST_DIR"
*
* echo "Done."
* ls -l "$DEST_DIR"
*/
@@ -26,7 +26,6 @@ import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.zip.ZipFile
/** /**
* Handles the retrieval and preparation of test configuration and assets. * Handles the retrieval and preparation of test configuration and assets.
@@ -43,11 +42,7 @@ class ValidationInputManager(private val context: Context) {
data class ValidationInput( data class ValidationInput(
val config: RenderTestConfig, val config: RenderTestConfig,
val outputDir: File, val outputDir: File,
val generateGoldens: Boolean, val generateGoldens: Boolean
val autoRun: Boolean = false,
val autoExport: Boolean = false,
val autoExportResults: Boolean = false,
val sourceZip: File? = null
) )
/** /**
@@ -58,111 +53,22 @@ class ValidationInputManager(private val context: Context) {
val testConfigPath = intent.getStringExtra("test_config") val testConfigPath = intent.getStringExtra("test_config")
val urlConfig = intent.getStringExtra("url_config") val urlConfig = intent.getStringExtra("url_config")
val urlModelsBase = intent.getStringExtra("url_models_base") val urlModelsBase = intent.getStringExtra("url_models_base")
val zipPath = intent.getStringExtra("zip_path")
val generateGoldens = intent.getBooleanExtra("generate_goldens", false) val generateGoldens = intent.getBooleanExtra("generate_goldens", false)
val autoRun = intent.getBooleanExtra("auto_run", false)
val autoExport = intent.getBooleanExtra("auto_export", false)
val autoExportResults = intent.getBooleanExtra("auto_export_results", false)
val outputPath = intent.getStringExtra("output_path") val outputPath = intent.getStringExtra("output_path")
Log.i(TAG, "Resolving config with outputPath: $outputPath") val outputDir = if (outputPath != null) {
File(outputPath).apply { mkdirs() }
val baseDir = context.getExternalFilesDir(null) ?: context.filesDir
Log.i(TAG, "Base directory for resolution: ${baseDir.absolutePath}")
val outputDir = if (!outputPath.isNullOrBlank()) {
val file = File(outputPath)
val resolved = if (file.isAbsolute) {
file
} else {
File(baseDir, outputPath)
}
// Critical check: if resolved is root or very short, it's likely wrong
if (resolved.absolutePath == "/" || resolved.parent == null) {
Log.w(TAG, "Resolved outputDir is root ($resolved), defaulting to app-specific dir")
File(baseDir, "validation_results")
} else {
resolved
}
} else { } else {
File(baseDir, "validation_results") File(context.getExternalFilesDir(null), "validation_results").apply { mkdirs() }
}
if (!outputDir.exists() && !outputDir.mkdirs()) {
Log.e(TAG, "Failed to create outputDir: ${outputDir.absolutePath}")
}
Log.i(TAG, "Final outputDir: ${outputDir.absolutePath}")
val sourceZipFile = if (zipPath != null) {
val file = File(zipPath)
if (file.isAbsolute) {
file
} else {
File(baseDir, zipPath)
}
} else {
null
} }
val config = when { val config = when {
sourceZipFile != null && sourceZipFile.exists() -> loadFromZip(sourceZipFile)
urlConfig != null -> downloadConfig(urlConfig, urlModelsBase) urlConfig != null -> downloadConfig(urlConfig, urlModelsBase)
testConfigPath != null -> ConfigParser.parseFromPath(testConfigPath) testConfigPath != null -> ConfigParser.parseFromPath(testConfigPath)
else -> extractDefaultAssets() else -> extractDefaultAssets()
} }
return@withContext ValidationInput(config, outputDir, generateGoldens, autoRun, autoExport, autoExportResults, sourceZipFile) return@withContext ValidationInput(config, outputDir, generateGoldens)
}
suspend fun loadFromZip(zipFile: File): RenderTestConfig {
Log.i(TAG, "Unzipping validation bundle: ${zipFile.absolutePath}")
// Use a unique cache dir based on timestamp or just overwrite a common one
// Overwriting is safer to avoid filling up disk, but we must ensure we don't conflict with current run
val baseCacheDir = context.externalCacheDir ?: context.cacheDir
val cacheDir = File(baseCacheDir, "unzipped_validation")
if (cacheDir.exists()) cacheDir.deleteRecursively()
cacheDir.mkdirs()
Log.i(TAG, "Using cacheDir: ${cacheDir.absolutePath}")
ZipFile(zipFile).use { zip ->
val entries = zip.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
val entryFile = File(cacheDir, entry.name)
// specific check to avoid zip slip vulnerability (though low risk here)
if (!entryFile.canonicalPath.startsWith(cacheDir.canonicalPath)) {
throw SecurityException("Zip entry is outside of the target dir: ${entry.name}")
}
if (entry.isDirectory) {
entryFile.mkdirs()
} else {
entryFile.parentFile?.mkdirs()
zip.getInputStream(entry).use { input ->
FileOutputStream(entryFile).use { output ->
input.copyTo(output)
}
}
}
}
}
// Find config.json
// We look for a file ending in .json within the unzipped structure
// Exclude results.json if it happened to be there
val jsonFiles = cacheDir.walkTopDown()
.filter { it.isFile && it.extension == "json" && it.name != "results.json" }
.toList()
if (jsonFiles.isEmpty()) throw IllegalStateException("No config.json found in zip")
// Prefer one named config.json or take the first one
val configFile = jsonFiles.find { it.name == "config.json" } ?: jsonFiles.first()
Log.i(TAG, "Parsed config from ${configFile.absolutePath}")
return ConfigParser.parseFromPath(configFile.absolutePath)
} }
private suspend fun extractDefaultAssets(): RenderTestConfig { private suspend fun extractDefaultAssets(): RenderTestConfig {
@@ -184,9 +90,9 @@ class ValidationInputManager(private val context: Context) {
// Copy DamagedHelmet.glb // Copy DamagedHelmet.glb
val modelsDir = File(filesDir, "models") val modelsDir = File(filesDir, "models")
modelsDir.mkdirs() 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 -> FileOutputStream(modelOut).use { output ->
input.copyTo(output) input.copyTo(output)
} }
@@ -67,201 +67,22 @@ class ValidationResultManager(private val outputDir: File) {
fun finalizeResults(): File? { fun finalizeResults(): File? {
// Write results JSON // Write results JSON
writeResultsJson() 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 { try {
ZipOutputStream(FileOutputStream(zipFile)).use { zos -> ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
// 1. Add results.json outputDir.walkTopDown().filter { it.isFile && it.name != "results.zip" }.forEach { file ->
val resultsJson = File(outputDir, "results.json") val entryName = file.relativeTo(outputDir).path
if (resultsJson.exists()) { zos.putNextEntry(ZipEntry(entryName))
zos.putNextEntry(ZipEntry("results.json")) file.inputStream().use { it.copyTo(zos) }
resultsJson.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
// 2. Add source zip if exists
if (sourceZip != null && sourceZip.exists()) {
zos.putNextEntry(ZipEntry(sourceZip.name))
sourceZip.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
// 3. Add diff images (any file ending in _diff.png in outputDir)
outputDir.listFiles { _, name -> name.endsWith("_diff.png") }?.forEach { diffFile ->
zos.putNextEntry(ZipEntry(diffFile.name))
diffFile.inputStream().use { it.copyTo(zos) }
zos.closeEntry() zos.closeEntry()
} }
} }
Log.i(TAG, "Exported results to ${zipFile.absolutePath}") Log.i(TAG, "Zipped results to ${zipFile.absolutePath}")
return zipFile return zipFile
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to export results", e) Log.e(TAG, "Failed to zip results", e)
return null
}
}
/**
* Exports a zip bundle containing:
* - Modified config.json (with updated name and relative paths)
* - Models (in models/ subdirectory)
* - Golden images (in goldens/ subdirectory)
*
* Structure:
* test_name_TIMESTAMP/
* config.json
* models/
* model.glb
* goldens/
* test_result.png
*/
fun exportTestBundle(config: RenderTestConfig, timestamp: String): File? {
Log.i(TAG, "Starting exportTestBundle for ${config.name} at $timestamp")
Log.i(TAG, "OutputDir: ${outputDir.absolutePath}")
val parentDir = outputDir.canonicalFile.parentFile ?: outputDir.parentFile
if (parentDir == null) {
Log.e(TAG, "OutputDir parent is null: ${outputDir.absolutePath}")
return null
}
Log.i(TAG, "Using parentDir for export: ${parentDir.absolutePath}")
val testNameWithTimestamp = "${config.name}_$timestamp"
val exportNameNoSpaces = testNameWithTimestamp.replace(" ", "_")
val exportDir = File(parentDir, "export_temp_$timestamp")
Log.i(TAG, "Creating export temp dir: ${exportDir.absolutePath}")
if (exportDir.exists()) exportDir.deleteRecursively()
if (!exportDir.mkdirs()) {
Log.e(TAG, "Failed to create export dir: ${exportDir.absolutePath}")
return null
}
val rootDir = File(exportDir, exportNameNoSpaces)
rootDir.mkdirs()
val modelsDir = File(rootDir, "models")
modelsDir.mkdirs()
val goldensDir = File(rootDir, "goldens")
goldensDir.mkdirs()
try {
// 1. Copy Models and update config map
val newModelsMap = mutableMapOf<String, String>()
Log.i(TAG, "Copying models...")
for ((modelName, modelPath) in config.models) {
val sourceFile = File(modelPath)
if (sourceFile.exists()) {
val destFile = File(modelsDir, sourceFile.name)
Log.d(TAG, "Copying model $modelName: $modelPath -> ${destFile.name}")
sourceFile.copyTo(destFile, overwrite = true)
// Use relative path for the new config
newModelsMap[modelName] = "models/${sourceFile.name}"
} else {
Log.w(TAG, "Model file not found: $modelPath")
}
}
// 2. Copy Golden Images
// We assume goldens are in outputDir with .png extension
Log.i(TAG, "Copying goldens from ${outputDir.absolutePath}...")
outputDir.listFiles { _, name -> name.endsWith(".png") }?.forEach { file ->
Log.d(TAG, "Copying golden: ${file.name}")
file.copyTo(File(goldensDir, file.name), overwrite = true)
}
// 3. Create modified config JSON
Log.i(TAG, "Creating config.json...")
val newConfigJson = JSONObject()
newConfigJson.put("name", testNameWithTimestamp) // Keep spaces in JSON name
// Reconstruct backends
val backendsArray = JSONArray()
config.backends.forEach { backendsArray.put(it) }
newConfigJson.put("backends", backendsArray)
// Reconstruct models
val modelsJson = JSONObject()
for ((k, v) in newModelsMap) {
modelsJson.put(k, v)
}
newConfigJson.put("models", modelsJson)
// Reconstruct tests
val testsArray = JSONArray()
for (test in config.tests) {
val testJson = JSONObject()
testJson.put("name", test.name)
if (test.description != null) testJson.put("description", test.description)
// Models for this test (set of strings)
val testModelsArray = JSONArray()
test.models.forEach { testModelsArray.put(it) }
testJson.put("models", testModelsArray)
// Rendering settings
testJson.put("rendering", test.rendering)
// Tolerance
if (test.tolerance != null) {
testJson.put("tolerance", test.tolerance)
}
// Backends (optional override)
val testBackends = JSONArray()
test.backends.forEach { testBackends.put(it) }
testJson.put("backends", testBackends)
testsArray.put(testJson)
}
newConfigJson.put("tests", testsArray)
// Write config.json
File(rootDir, "config.json").writeText(newConfigJson.toString(4))
// 4. Zip it
val zipFile = File(parentDir, "$exportNameNoSpaces.zip")
Log.i(TAG, "Zipping to ${zipFile.absolutePath}...")
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
rootDir.walkTopDown().forEach { file ->
if (file.isFile) {
val entryName = file.relativeTo(exportDir).path
zos.putNextEntry(ZipEntry(entryName))
file.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
}
// Cleanup temp dir
exportDir.deleteRecursively()
Log.i(TAG, "Exported test bundle to ${zipFile.absolutePath}")
return zipFile
} catch (e: Exception) {
Log.e(TAG, "Failed to export test bundle", e)
exportDir.deleteRecursively()
return null return null
} }
} }
@@ -41,13 +41,18 @@ class ValidationRunner(
private var currentTestConfig: TestConfig? = null private var currentTestConfig: TestConfig? = null
private var currentModelName: String? = null private var currentModelName: String? = null
private var loadStartFence: com.google.android.filament.Fence? = null
private var loadStartTime = 0L
private var frameCounter = 0 private var frameCounter = 0
enum class State { enum class State {
IDLE, IDLE,
LOADING_MODEL,
WAITING_FOR_FENCE,
WAITING_FOR_RESOURCES, WAITING_FOR_RESOURCES,
WARMUP, WARMUP,
RUNNING_TEST RUNNING_TEST,
COMPARING
} }
interface Callback { interface Callback {
@@ -88,6 +93,8 @@ class ValidationRunner(
nextModel() nextModel()
return return
} }
currentState = State.LOADING_MODEL
callback?.onStatusChanged("Loading $modelName for ${currentTestConfig?.name}") callback?.onStatusChanged("Loading $modelName for ${currentTestConfig?.name}")
// Load model on main thread (required by ModelViewer) // Load model on main thread (required by ModelViewer)
@@ -103,24 +110,51 @@ class ValidationRunner(
Log.i("ValidationRunner", "Loading GLB buffer... (${bytes.size} bytes)") Log.i("ValidationRunner", "Loading GLB buffer... (${bytes.size} bytes)")
val buffer = ByteBuffer.wrap(bytes) val buffer = ByteBuffer.wrap(bytes)
modelViewer.loadModelGlb(buffer) modelViewer.loadModelGlb(buffer)
Log.i("ValidationRunner", "Model loaded.") Log.i("ValidationRunner", "Model loaded. initializing fence.")
modelViewer.transformToUnitCube() modelViewer.transformToUnitCube()
currentState = State.WAITING_FOR_RESOURCES loadStartFence = modelViewer.engine.createFence()
frameCounter = 0 loadStartTime = System.nanoTime()
Log.i("ValidationRunner", "State set to WAITING_FOR_RESOURCES") currentState = State.WAITING_FOR_FENCE
frameCounter = 0 // Reset for fence timeout tracking
Log.i("ValidationRunner", "State set to WAITING_FOR_FENCE")
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ValidationRunner", "Failed to load $path", e) Log.e("ValidationRunner", "Failed to load $path", e)
nextModel() nextModel()
} }
} }
fun onFrame(frameTimeNanos: Long) { fun onFrame(frameTimeNanos: Long) {
if (frameCounter % 60 == 0) { if (frameCounter % 60 == 0) {
Log.i("ValidationRunner", "onFrame: $currentState (frame: $frameCounter)") Log.i("ValidationRunner", "onFrame: $currentState (frame: $frameCounter)")
} }
when (currentState) { when (currentState) {
State.IDLE -> {} 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 -> { State.WAITING_FOR_RESOURCES -> {
val progress = modelViewer.progress val progress = modelViewer.progress
if (progress >= 1.0f) { if (progress >= 1.0f) {
@@ -132,12 +166,12 @@ class ValidationRunner(
State.WARMUP -> { State.WARMUP -> {
frameCounter++ frameCounter++
if (frameCounter > 5) { // 5 frames warmup if (frameCounter > 5) { // 5 frames warmup
startAutomation() startAutomation()
} }
} }
State.RUNNING_TEST -> { State.RUNNING_TEST -> {
// Log.i("ValidationRunner", "Running test...") // Log.i("ValidationRunner", "Running test...")
currentEngine?.let { engine -> currentEngine?.let { engine ->
val content = AutomationEngine.ViewerContent() val content = AutomationEngine.ViewerContent()
content.view = modelViewer.view content.view = modelViewer.view
content.renderer = modelViewer.renderer content.renderer = modelViewer.renderer
@@ -152,11 +186,13 @@ class ValidationRunner(
if (engine.shouldClose()) { if (engine.shouldClose()) {
Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)") Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)")
// Test finished (for this spec) // Test finished (for this spec)
currentState = State.IDLE currentState = State.COMPARING
captureAndCompare() captureAndCompare()
} }
} }
} }
State.COMPARING -> {} // Busy
State.LOADING_MODEL -> {}
} }
} }
@@ -197,36 +233,7 @@ class ValidationRunner(
// Golden path // Golden path
val modelFile = File(config.models.get(modelName)!!) val modelFile = File(config.models.get(modelName)!!)
val modelParent = modelFile.parentFile!! val goldenFile = modelFile.parentFile!!.parentFile!!.resolve("golden/${testFullName}.png")
// Search for golden in:
// 1. ../golden/ (standard structure)
// 2. ../goldens/ (exported structure, sibling of models)
// 3. ./goldens/ (backup)
val searchPaths = mutableListOf<File>()
modelParent.parentFile?.let {
searchPaths.add(it.resolve("golden"))
searchPaths.add(it.resolve("goldens"))
}
searchPaths.add(modelParent.resolve("goldens"))
var goldenFile: File? = null
for (path in searchPaths) {
val f = path.resolve("${testFullName}.png")
if (f.exists()) {
goldenFile = f
break
}
}
if (goldenFile != null) {
Log.i("ValidationRunner", "Found golden at ${goldenFile.absolutePath}")
} else {
Log.w("ValidationRunner", "Golden not found for $testFullName. Searched in: ${searchPaths.joinToString { it.absolutePath }}")
// Fallback to old behavior for reference if everything else fails
goldenFile = modelParent.parentFile?.resolve("golden/${testFullName}.png") ?: File("nonexistent")
}
Thread { Thread {
try { try {
@@ -238,15 +245,14 @@ class ValidationRunner(
var diffMetric = 0f var diffMetric = 0f
if (generateGoldens) { if (generateGoldens) {
val targetGolden = goldenFile ?: modelParent.parentFile?.resolve("golden/${testFullName}.png") ?: File(modelParent, "golden/${testFullName}.png") goldenFile.parentFile?.mkdirs()
targetGolden.parentFile?.mkdirs() FileOutputStream(goldenFile).use { out ->
FileOutputStream(targetGolden).use { out ->
flipped.compress(Bitmap.CompressFormat.PNG, 100, out) flipped.compress(Bitmap.CompressFormat.PNG, 100, out)
} }
passed = true // Generating goldens always passes if successful passed = true // Generating goldens always passes if successful
callback?.onStatusChanged("Golden generated") callback?.onStatusChanged("Golden generated")
} else { } else {
if (goldenFile != null && goldenFile.exists()) { if (goldenFile.exists()) {
val golden = android.graphics.BitmapFactory.decodeFile(goldenFile.absolutePath) val golden = android.graphics.BitmapFactory.decodeFile(goldenFile.absolutePath)
if (golden != null) { if (golden != null) {
callback?.onImageResult("Golden", golden) callback?.onImageResult("Golden", golden)
@@ -267,7 +273,7 @@ class ValidationRunner(
callback?.onStatusChanged("Failed to load golden") callback?.onStatusChanged("Failed to load golden")
} }
} else { } else {
Log.w("ValidationRunner", "Golden not found: ${goldenFile?.absolutePath}") Log.w("ValidationRunner", "Golden not found: ${goldenFile.absolutePath}")
callback?.onStatusChanged("Golden not found") callback?.onStatusChanged("Golden not found")
} }
} }
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -10,8 +10,6 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1" app:layout_constraintDimensionRatio="1:1"
app:layout_constraintWidth_percent="0.6"
android:layout_marginTop="32dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
@@ -22,83 +20,10 @@
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</FrameLayout> </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 <ScrollView
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="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_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
@@ -107,9 +32,41 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:paddingStart="20dp" android:padding="20dp">
android:paddingEnd="20dp"
android:paddingBottom="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 <LinearLayout
android:id="@+id/results_container" android:id="@+id/results_container"
@@ -117,6 +74,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" /> android:orientation="vertical" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'com.google.android.filament-tools' id 'filament-plugin'
} }
project.ext.isSample = true project.ext.isSample = true
-5
View File
@@ -1,8 +1,3 @@
// Filament tools plugin
pluginManagement {
includeBuild 'gradle-plugin'
}
// Libraries // Libraries
include ':filament-android' include ':filament-android'
include ':filamat-android' include ':filamat-android'
+6 -6
View File
@@ -235,9 +235,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.3", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@@ -429,9 +429,9 @@
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==" "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="
}, },
"minimatch": { "minimatch": {
"version": "3.1.3", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
+2 -2
View File
@@ -181,7 +181,7 @@ important for <code>matc</code> (material compiler).</p>
} }
dependencies { dependencies {
implementation 'com.google.android.filament:filament-android:1.69.5' implementation 'com.google.android.filament:filament-android:1.69.3'
} }
</code></pre> </code></pre>
<p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p> <p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p>
@@ -195,7 +195,7 @@ dependencies {
</div> </div>
<h3 id="ios"><a class="header" href="#ios">iOS</a></h3> <h3 id="ios"><a class="header" href="#ios">iOS</a></h3>
<p>iOS projects can use CocoaPods to install the latest release:</p> <p>iOS projects can use CocoaPods to install the latest release:</p>
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.69.5' <pre><code class="language-shell">pod 'Filament', '~&gt; 1.69.3'
</code></pre> </code></pre>
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2> <h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
<ul> <ul>
+1 -23
View File
@@ -225,10 +225,9 @@ into <strong>branch</strong> of <code>filament-assets</code>. This branch is pai
<code>filament</code> repo.</p> <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 <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 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> <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> <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. <li>Run interactive mode in the <code>update_golden.py</code> script.
<pre><code>python3 test/renderdiff/src/update_golden.py <pre><code>python3 test/renderdiff/src/update_golden.py
</code></pre> </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> <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> to the <code>main</code> branch of the golden repo.</li>
</ul> </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> <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 <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 be used by pointing your browser to a localhost port. If you input the viewer with a PR or a
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-2
View File
@@ -93,7 +93,6 @@ set(SRCS
src/MorphTargetBuffer.cpp src/MorphTargetBuffer.cpp
src/PostProcessManager.cpp src/PostProcessManager.cpp
src/ProgramSpecialization.cpp src/ProgramSpecialization.cpp
src/LocalProgramCache.cpp
src/RenderPass.cpp src/RenderPass.cpp
src/RenderPrimitive.cpp src/RenderPrimitive.cpp
src/RenderTarget.cpp src/RenderTarget.cpp
@@ -187,7 +186,6 @@ set(PRIVATE_HDRS
src/MaterialCache.h src/MaterialCache.h
src/MaterialDefinition.h src/MaterialDefinition.h
src/MaterialParser.h src/MaterialParser.h
src/LocalProgramCache.h
src/MaterialInstanceManager.h src/MaterialInstanceManager.h
src/PIDController.h src/PIDController.h
src/PostProcessManager.h src/PostProcessManager.h
+152 -153
View File
@@ -564,171 +564,170 @@ endif()
# ================================================================================================== # ==================================================================================================
# Test # 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) if (APPLE OR LINUX)
set(BACKEND_TEST_SRC set(BACKEND_TEST_SRC
test/BackendTest.cpp test/BackendTest.cpp
test/ShaderGenerator.cpp test/ShaderGenerator.cpp
test/TrianglePrimitive.cpp test/TrianglePrimitive.cpp
test/Arguments.cpp test/Arguments.cpp
test/ImageExpectations.cpp test/ImageExpectations.cpp
test/Lifetimes.cpp test/Lifetimes.cpp
test/PlatformRunner.cpp test/PlatformRunner.cpp
test/Shader.cpp test/Shader.cpp
test/SharedShaders.cpp test/SharedShaders.cpp
test/Skip.cpp test/Skip.cpp
test/test_Autoresolve.cpp test/test_Autoresolve.cpp
test/test_FeedbackLoops.cpp test/test_FeedbackLoops.cpp
test/test_Blit.cpp test/test_Blit.cpp
test/test_MissingRequiredAttributes.cpp test/test_MissingRequiredAttributes.cpp
test/test_ReadPixels.cpp test/test_ReadPixels.cpp
test/test_ReadTexture.cpp test/test_ReadTexture.cpp
test/test_BufferUpdates.cpp test/test_BufferUpdates.cpp
test/test_Callbacks.cpp test/test_Callbacks.cpp
test/test_JobQueue.cpp test/test_JobQueue.cpp
test/test_MemoryMappedBuffer.cpp test/test_MemoryMappedBuffer.cpp
test/test_MsaaSwapChain.cpp test/test_MsaaSwapChain.cpp
test/test_MRT.cpp test/test_MRT.cpp
test/test_PushConstants.cpp test/test_PushConstants.cpp
test/test_LoadImage.cpp test/test_LoadImage.cpp
test/test_StencilBuffer.cpp test/test_StencilBuffer.cpp
test/test_Scissor.cpp test/test_Scissor.cpp
test/test_MipLevels.cpp test/test_MipLevels.cpp
test/test_Handles.cpp test/test_Handles.cpp
test/test_CircularBuffer.cpp test/test_CircularBuffer.cpp
test/test_CommandBufferQueue.cpp test/test_CommandBufferQueue.cpp
test/test_Template.cpp test/test_Template.cpp
test/test_Platform.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) endif()
list(APPEND BACKEND_TEST_SRC set(BACKEND_TEST_LIBS
test/test_WebGPUAsyncTaskCounter.cpp absl::str_format
test/test_WebGPUComposeSwizzle.cpp) backend
endif() getopt
if (APPLE) gtest
# Metal-specific tests imageio
list(APPEND BACKEND_TEST_SRC filamat
test/test_MetalBlitter.mm SPIRV
) spirv-cross-glsl)
endif() # Create input/output directories for test result images.
set(BACKEND_TEST_LIBS file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/actual_images)
absl::str_format file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/diff_images)
backend 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 getopt
gtest gtest
imageio glslang
filamat spirv-cross-core
SPIRV spirv-cross-glsl
spirv-cross-glsl) spirv-cross-msl)
# Create input/output directories for test result images.
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/actual_images) if (NOT IOS)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/diff_images) target_link_libraries(backend_test PRIVATE image imageio)
file(COPY test/expected_images DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/images) 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() endif()
# TODO: Disabling IOS test due to breakage wrt glslang update set(BACKEND_TEST_COMBINED_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libbackendtest_combined.a")
if (APPLE AND NOT IOS) combine_static_libs(backend_test "${BACKEND_TEST_COMBINED_OUTPUT}" "${BACKEND_TEST_DEPS}")
# 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 set(BACKEND_TEST_LIB_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}backend_test${CMAKE_STATIC_LIBRARY_SUFFIX})
OSDependent
SPIRV
SPIRV-Tools
SPIRV-Tools-opt
backend_test
getopt
gtest
glslang
spirv-cross-core
spirv-cross-glsl
spirv-cross-msl)
if (NOT IOS) if (INSTALL_BACKEND_TEST)
target_link_libraries(backend_test PRIVATE image imageio) install(FILES "${BACKEND_TEST_COMBINED_OUTPUT}" DESTINATION lib/${DIST_DIR} RENAME ${BACKEND_TEST_LIB_NAME})
list(APPEND BACKEND_TEST_DEPS image) install(FILES test/PlatformRunner.h DESTINATION include/backend_test)
endif()
if (FILAMENT_SUPPORTS_WEBGPU)
target_link_libraries(backend_test PRIVATE webgpu_dawn dawncpp_headers)
list(APPEND BACKEND_TEST_DEPS webgpu_dawn dawncpp_headers)
endif()
set(BACKEND_TEST_COMBINED_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libbackendtest_combined.a")
combine_static_libs(backend_test "${BACKEND_TEST_COMBINED_OUTPUT}" "${BACKEND_TEST_DEPS}")
set(BACKEND_TEST_LIB_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}backend_test${CMAKE_STATIC_LIBRARY_SUFFIX})
if (INSTALL_BACKEND_TEST)
install(FILES "${BACKEND_TEST_COMBINED_OUTPUT}" DESTINATION lib/${DIST_DIR} RENAME ${BACKEND_TEST_LIB_NAME})
install(FILES test/PlatformRunner.h DESTINATION include/backend_test)
endif()
set_target_properties(backend_test PROPERTIES FOLDER Tests)
if (APPLE AND NOT IOS)
add_executable(backend_test_mac test/mac_runner.mm)
target_link_libraries(backend_test_mac PRIVATE "-framework Metal -framework AppKit -framework QuartzCore")
# Because each test case is a separate file, the -force_load flag is necessary to prevent the
# linker from removing "unused" symbols.
target_link_libraries(backend_test_mac PRIVATE -force_load backend_test)
set_target_properties(backend_test_mac PROPERTIES FOLDER Tests)
# This is needed after XCode 15.3
set_target_properties(backend_test_mac PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE)
set_target_properties(backend_test_mac PROPERTIES INSTALL_RPATH /usr/local/lib)
endif()
endif() endif()
if (LINUX) set_target_properties(backend_test PROPERTIES FOLDER Tests)
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) 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") # This is needed after XCode 15.3
set_target_properties(backend_test_mac PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE)
target_link_libraries(metal_utils_test PRIVATE set_target_properties(backend_test_mac PROPERTIES INSTALL_RPATH /usr/local/lib)
backend
getopt
gtest
)
set_target_properties(metal_utils_test PROPERTIES FOLDER Tests)
endif() 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()
+1 -22
View File
@@ -216,17 +216,6 @@ public:
MULTIVIEW, MULTIVIEW,
}; };
/**
* Types of device/driver information that can be queried from the platform.
*/
enum class DeviceInfoType {
OPENGL_RENDERER, //!< glGetString(GL_RENDERER)
OPENGL_VENDOR, //!< glGetString(GL_VENDOR)
VULKAN_DEVICE_NAME, //!< VkPhysicalDeviceProperties::deviceName
VULKAN_DRIVER_NAME, //!< VkPhysicalDeviceDriverProperties::driverName
VULKAN_DRIVER_INFO, //!< VkPhysicalDeviceDriverProperties::driverInfo
};
/** /**
* This controls the priority level for GPU work scheduling, which helps prioritize the * This controls the priority level for GPU work scheduling, which helps prioritize the
* submitted GPU work and enables preemption. * submitted GPU work and enables preemption.
@@ -327,7 +316,7 @@ public:
*/ */
StereoscopicType stereoscopicType = StereoscopicType::NONE; StereoscopicType stereoscopicType = StereoscopicType::NONE;
/** /*
* The number of eyes to render when stereoscopic rendering is enabled. Supported values are * The number of eyes to render when stereoscopic rendering is enabled. Supported values are
* between 1 and Engine::getMaxStereoscopicEyes() (inclusive). * between 1 and Engine::getMaxStereoscopicEyes() (inclusive).
*/ */
@@ -388,16 +377,6 @@ public:
*/ */
virtual int getOSVersion() const noexcept = 0; virtual int getOSVersion() const noexcept = 0;
/**
* Queries device/driver information of the graphics API.
* @param infoType the type of information to query.
* @param driver a pointer to the current driver.
* @return a CString containing the requested information.
*/
virtual utils::CString getDeviceInfo(DeviceInfoType infoType,
Driver* UTILS_NULLABLE driver) const noexcept = 0;
/** /**
* Creates and initializes the low-level API (e.g. an OpenGL context or Vulkan instance), * Creates and initializes the low-level API (e.g. an OpenGL context or Vulkan instance),
* then creates the concrete Driver. * then creates the concrete Driver.
@@ -52,9 +52,6 @@ protected:
~OpenGLPlatform() noexcept override; ~OpenGLPlatform() noexcept override;
utils::CString getDeviceInfo(DeviceInfoType infoType,
Driver* UTILS_NULLABLE driver) const noexcept override;
public: public:
struct ExternalTexture { struct ExternalTexture {
unsigned int target; // GLenum target unsigned int target; // GLenum target
@@ -38,7 +38,6 @@ public:
Driver* createDriver(void* sharedContext, const Platform::DriverConfig& driverConfig) override; Driver* createDriver(void* sharedContext, const Platform::DriverConfig& driverConfig) override;
int getOSVersion() const noexcept override { return 0; } int getOSVersion() const noexcept override { return 0; }
utils::CString getDeviceInfo(DeviceInfoType, Driver*) const noexcept override { return {}; }
/** /**
* Optionally initializes the Metal platform by acquiring resources necessary for rendering. * Optionally initializes the Metal platform by acquiring resources necessary for rendering.
@@ -143,8 +143,6 @@ public:
return 0; return 0;
} }
utils::CString getDeviceInfo(DeviceInfoType infoType, Driver* driver) const noexcept override;
// ---------------------------------------------------- // ----------------------------------------------------
// ---------- Platform Customization options ---------- // ---------- Platform Customization options ----------
struct Customization { struct Customization {
@@ -177,12 +175,6 @@ public:
* presentation. Default is true. * presentation. Default is true.
*/ */
bool transitionSwapChainImageLayoutForPresent = true; bool transitionSwapChainImageLayoutForPresent = true;
/**
* The number of frames before an unused framebuffer is evicted from the cache.
* Default is 3.
*/
uint32_t timeBeforeEvictionFbo = 3;
}; };
/** /**
@@ -46,9 +46,6 @@ public:
~WebGPUPlatform() override = default; ~WebGPUPlatform() override = default;
[[nodiscard]] int getOSVersion() const noexcept final { return 0; } [[nodiscard]] int getOSVersion() const noexcept final { return 0; }
[[nodiscard]] utils::CString getDeviceInfo(DeviceInfoType, Driver*) const noexcept override {
return {};
}
[[nodiscard]] wgpu::Instance& getInstance() noexcept { return mInstance; } [[nodiscard]] wgpu::Instance& getInstance() noexcept { return mInstance; }
@@ -27,6 +27,8 @@
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <functional>
namespace filament::backend { namespace filament::backend {
/* /*
@@ -64,7 +66,7 @@ public:
// all commands buffers (Slices) written to this point are returned by waitForCommand(). This // all commands buffers (Slices) written to this point are returned by waitForCommand(). This
// call blocks until the CircularBuffer has at least mRequiredSize bytes available. // call blocks until the CircularBuffer has at least mRequiredSize bytes available.
void flush(); void flush(std::function<void(void*, void*)> const& debugPrintHistogram = nullptr);
// returns from waitForCommands() immediately. // returns from waitForCommands() immediately.
void requestExit(); void requestExit();
@@ -56,6 +56,7 @@ namespace filament::backend {
class CommandBase { class CommandBase {
static constexpr size_t FILAMENT_OBJECT_ALIGNMENT = alignof(std::max_align_t); static constexpr size_t FILAMENT_OBJECT_ALIGNMENT = alignof(std::max_align_t);
friend class CommandStream;
protected: protected:
using Execute = Dispatcher::Execute; using Execute = Dispatcher::Execute;
@@ -168,8 +169,8 @@ struct CommandType<void (Driver::*)(ARGS...)> {
class CustomCommand : public CommandBase { class CustomCommand : public CommandBase {
std::function<void()> mCommand; std::function<void()> mCommand;
static void execute(Driver&, CommandBase* base, intptr_t* next);
public: public:
static void execute(Driver&, CommandBase* base, intptr_t* next);
CustomCommand(CustomCommand&& rhs) = default; CustomCommand(CustomCommand&& rhs) = default;
explicit CustomCommand(std::function<void()> cmd) explicit CustomCommand(std::function<void()> cmd)
@@ -179,11 +180,12 @@ public:
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
class NoopCommand : public CommandBase { class NoopCommand : public CommandBase {
public:
intptr_t mNext; intptr_t mNext;
static void execute(Driver&, CommandBase* self, intptr_t* next) noexcept { static void execute(Driver&, CommandBase* self, intptr_t* next) noexcept {
*next = static_cast<NoopCommand*>(self)->mNext; *next = static_cast<NoopCommand*>(self)->mNext;
} }
public:
constexpr explicit NoopCommand(void* next) noexcept constexpr explicit NoopCommand(void* next) noexcept
: CommandBase(execute), mNext(intptr_t((char *)next - (char *)this)) { } : CommandBase(execute), mNext(intptr_t((char *)next - (char *)this)) { }
}; };
@@ -219,6 +221,36 @@ public:
CircularBuffer const& getCircularBuffer() const noexcept { return mCurrentBuffer; } CircularBuffer const& getCircularBuffer() const noexcept { return mCurrentBuffer; }
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
using Execute = Dispatcher::Execute;
struct CommandInfo {
size_t size;
const char* name;
int index;
};
std::unordered_map<Execute, CommandInfo> mCommands;
void initializeLookup() {
int currentIndex = 0;
#define DECL_DRIVER_API_SYNCHRONOUS(RetType, methodName, paramsDecl, params)
#define DECL_DRIVER_API(methodName, paramsDecl, params) \
mCommands[mDispatcher.methodName##_] = { CommandBase::align(sizeof(COMMAND_TYPE(methodName))), \
#methodName, currentIndex++ };
#define DECL_DRIVER_API_RETURN(RetType, methodName, paramsDecl, params) \
mCommands[mDispatcher.methodName##_] = { \
CommandBase::align(sizeof(COMMAND_TYPE(methodName##R))), #methodName, currentIndex++ \
};
#include "private/backend/DriverAPI.inc"
mCommands[CustomCommand::execute] = { CommandBase::align(sizeof(CustomCommand)),
"CustomCommand", currentIndex++ };
// NoopCommands have variable size. We will handle them specially using their mNext pointer.
mCommands[NoopCommand::execute] = { 0, "NoopCommand", currentIndex++ };
}
#endif
public: public:
#define DECL_DRIVER_API(methodName, paramsDecl, params) \ #define DECL_DRIVER_API(methodName, paramsDecl, params) \
inline void methodName(paramsDecl) noexcept { \ inline void methodName(paramsDecl) noexcept { \
@@ -263,6 +295,13 @@ public:
void execute(void* buffer); void execute(void* buffer);
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
void debugIterateCommands(void* head, void* tail,
std::function<void(CommandInfo const& info)> const& callback);
void debugPrintHistogram(void* head, void* tail);
#endif
/* /*
* queueCommand() allows to queue a lambda function as a command. * queueCommand() allows to queue a lambda function as a command.
* This is much less efficient than using the Driver* API. * This is much less efficient than using the Driver* API.
@@ -48,6 +48,11 @@
#define FILAMENT_DEBUG_COMMANDS FILAMENT_DEBUG_COMMANDS_NONE #define FILAMENT_DEBUG_COMMANDS FILAMENT_DEBUG_COMMANDS_NONE
// Upon command stream overflow, print a histogram of commands
#ifndef FILAMENT_DEBUG_COMMANDS_HISTOGRAM
#define FILAMENT_DEBUG_COMMANDS_HISTOGRAM 0
#endif
namespace filament::backend { namespace filament::backend {
class BufferDescriptor; class BufferDescriptor;
+8 -1
View File
@@ -79,7 +79,7 @@ bool CommandBufferQueue::isExitRequested() const {
} }
void CommandBufferQueue::flush() { void CommandBufferQueue::flush(std::function<void(void*, void*)> const& debugPrintHistogram) {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT); FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
CircularBuffer& circularBuffer = mCircularBuffer; CircularBuffer& circularBuffer = mCircularBuffer;
@@ -106,6 +106,13 @@ void CommandBufferQueue::flush() {
std::unique_lock lock(mLock); std::unique_lock lock(mLock);
// circular buffer is too small, we corrupted the stream // circular buffer is too small, we corrupted the stream
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
if (UTILS_VERY_UNLIKELY(used > mFreeSpace)) {
if (debugPrintHistogram) {
debugPrintHistogram(begin, end);
}
}
#endif
FILAMENT_CHECK_POSTCONDITION(used <= mFreeSpace) << FILAMENT_CHECK_POSTCONDITION(used <= mFreeSpace) <<
"Backend CommandStream overflow. Commands are corrupted and unrecoverable.\n" "Backend CommandStream overflow. Commands are corrupted and unrecoverable.\n"
"Please increase minCommandBufferSizeMB inside the Config passed to Engine::create.\n" "Please increase minCommandBufferSizeMB inside the Config passed to Engine::create.\n"
+74
View File
@@ -30,6 +30,11 @@
#include <utils/ostream.h> #include <utils/ostream.h>
#include <utils/sstream.h> #include <utils/sstream.h>
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
#include <algorithm>
#include <vector>
#endif
#include <cstddef> #include <cstddef>
#include <functional> #include <functional>
#include <string> #include <string>
@@ -83,6 +88,10 @@ CommandStream::CommandStream(Driver& driver, CircularBuffer& buffer) noexcept
__system_property_get("debug.filament.perfcounters", property); __system_property_get("debug.filament.perfcounters", property);
mUsePerformanceCounter = bool(atoi(property)); mUsePerformanceCounter = bool(atoi(property));
#endif #endif
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
initializeLookup();
#endif
} }
void CommandStream::execute(void* buffer) { void CommandStream::execute(void* buffer) {
@@ -126,6 +135,71 @@ void CommandStream::execute(void* buffer) {
} }
} }
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
void CommandStream::debugIterateCommands(void* head, void* tail,
std::function<void(CommandInfo const& info)> const& callback) {
CommandBase* UTILS_RESTRICT base = static_cast<CommandBase*>(head);
auto p = base;
while (UTILS_LIKELY(p)) {
if (p >= tail) {
break;
}
Execute e = p->mExecute;
if (e == NoopCommand::execute) {
NoopCommand* noop = static_cast<NoopCommand*>(p);
size_t size = noop->mNext;
int noopIndex = mCommands[NoopCommand::execute].index;
callback({ size, "NoopCommand", noopIndex });
p = reinterpret_cast<CommandBase*>(reinterpret_cast<char*>(p) + size);
continue;
}
if (auto it = mCommands.find(e); it != mCommands.end()) {
size_t size = it->second.size;
callback(it->second);
p = reinterpret_cast<CommandBase*>(reinterpret_cast<char*>(p) + size);
} else {
LOG(ERROR) << "Cannot find command in lookup table";
return;
}
}
}
void CommandStream::debugPrintHistogram(void* head, void* tail) {
std::unordered_map<std::string_view, int> histogram;
std::unordered_map<int, int> index_histogram;
debugIterateCommands(head, tail, [&](CommandInfo const& info) {
histogram[std::string_view(info.name)]++;
index_histogram[info.index]++;
});
std::vector<std::pair<std::string_view, int>> sorted_histogram(histogram.begin(),
histogram.end());
std::sort(sorted_histogram.begin(), sorted_histogram.end(),
[](auto const& a, auto const& b) { return a.second > b.second; });
LOG(INFO) << "Command stream histogram:";
for (auto const& [name, count]: sorted_histogram) {
LOG(INFO) << name << ": " << count;
}
std::vector<std::pair<int, int>> sorted_index_histogram(index_histogram.begin(),
index_histogram.end());
std::sort(sorted_index_histogram.begin(), sorted_index_histogram.end(),
[](auto const& a, auto const& b) { return a.second > b.second; });
std::string short_histogram = "";
for (size_t i = 0, n = sorted_index_histogram.size(); i < n; ++i) {
short_histogram += std::to_string(sorted_index_histogram[i].first) + ":" +
std::to_string(sorted_index_histogram[i].second);
short_histogram += (i < n - 1) ? ";" : ".";
}
LOG(INFO) << "CS hist: " << short_histogram;
LOG(INFO) << "";
}
#endif
void CommandStream::queueCommand(std::function<void()> command) { void CommandStream::queueCommand(std::function<void()> command) {
new(allocateCommand(CustomCommand::align(sizeof(CustomCommand)))) CustomCommand(std::move(command)); new(allocateCommand(CustomCommand::align(sizeof(CustomCommand)))) CustomCommand(std::move(command));
} }
+2 -20
View File
@@ -25,11 +25,6 @@
#include <mutex> #include <mutex>
#ifdef __ANDROID__
#include <pthread.h>
#endif
namespace filament { namespace filament {
using namespace utils; using namespace utils;
@@ -110,21 +105,8 @@ JNIEnv* VirtualMachineEnv::getEnvironmentSlow() {
FILAMENT_CHECK_PRECONDITION(mVirtualMachine) FILAMENT_CHECK_PRECONDITION(mVirtualMachine)
<< "JNI_OnLoad() has not been called"; << "JNI_OnLoad() has not been called";
#ifdef __ANDROID__ #if defined(__ANDROID__)
JavaVMAttachArgs args; jint const result = mVirtualMachine->AttachCurrentThread(&mJniEnv, nullptr);
args.version = JNI_VERSION_1_6;
args.group = nullptr;
char threadName[16]; // pthread_getname_np returns at most 16 bytes
if (__builtin_available(android 26, *)) {
if (pthread_getname_np(pthread_self(), threadName, sizeof(threadName)) == 0) {
args.name = threadName;
} else {
args.name = nullptr;
}
} else {
args.name = nullptr;
}
jint const result = mVirtualMachine->AttachCurrentThread(&mJniEnv, &args);
#else #else
jint const result = mVirtualMachine->AttachCurrentThread(reinterpret_cast<void**>(&mJniEnv), nullptr); jint const result = mVirtualMachine->AttachCurrentThread(reinterpret_cast<void**>(&mJniEnv), nullptr);
#endif #endif
-1
View File
@@ -26,7 +26,6 @@ class PlatformNoop final : public Platform {
public: public:
int getOSVersion() const noexcept final { return 0; } int getOSVersion() const noexcept final { return 0; }
utils::CString getDeviceInfo(DeviceInfoType, Driver*) const noexcept override { return {}; }
~PlatformNoop() noexcept override = default; ~PlatformNoop() noexcept override = default;
@@ -341,44 +341,7 @@ void GLDescriptorSet::bind(
if (arg.handle) { if (arg.handle) {
GLTexture const* const t = handleAllocator.handle_cast<GLTexture*>(arg.handle); GLTexture const* const t = handleAllocator.handle_cast<GLTexture*>(arg.handle);
gl.bindTexture(unit, t->gl.target, t->gl.id, t->gl.external); gl.bindTexture(unit, t->gl.target, t->gl.id, t->gl.external);
SamplerParams params = arg.params; SamplerParams const params = arg.params;
#if defined(__EMSCRIPTEN__)
// From https://registry.khronos.org/OpenGL-Refpages/es2.0/
// GLES 2.0 will draw mipmapped samplers as black if the following conditions are not met:
//
// "...if the width or height of a texture image are not powers of two and either the
// GL_TEXTURE_MIN_FILTER is set to one of the functions that requires mipmaps or the
// GL_TEXTURE_WRAP_S or GL_TEXTURE_WRAP_T is not set to GL_CLAMP_TO_EDGE, then the
// texture image unit will return (R, G, B, A) = (0, 0, 0, 1)."
//
// "If the texture has dimensions w × h , there are [floor(log2(max(w, h))) + 1] mipmap levels.
// Level 0 is the original texture; level [floor(log2(max(w, h)))] is the final 1 × 1 mipmap."
// "Suppose that a texture is accessed from a fragment shader or vertex shader and has set GL_TEXTURE_MIN_FILTER
// to one of the functions that requires mipmaps. If either the dimensions of the texture images currently
// defined (with previous calls to glTexImage2D, glCompressedTexImage2D, or glCopyTexImage2D) do not follow
// the proper sequence for mipmaps (described above), or there are fewer texture images defined than are needed,
// or the set of texture images were defined with different formats or types, then the texture image unit
// will return (R, G, B, A) = (0, 0, 0, 1)."
//
// So we force the sampler to a valid state in those cases.
auto isPowerOfTwo = [](uint32_t x) -> bool { return (x & (x - 1)) == 0 && x > 0; };
bool textureIsPowerOfTwo = isPowerOfTwo(t->width) && isPowerOfTwo(t->height);
uint32_t requiredMipLevels = (std::floor(std::log2(std::max(t->width, std::max(t->height, t->depth)))) + 1);
bool textureHasRequiredMipLevels = t->levels == requiredMipLevels;
bool samplerCanBeInvalid = params.filterMin > SamplerMinFilter::LINEAR;
if (samplerCanBeInvalid && (!textureIsPowerOfTwo || !textureHasRequiredMipLevels)) {
if (params.filterMin == SamplerMinFilter::NEAREST_MIPMAP_NEAREST
|| params.filterMin == SamplerMinFilter::LINEAR_MIPMAP_NEAREST) {
params.filterMin = SamplerMinFilter::NEAREST;
} else {
params.filterMin = SamplerMinFilter::LINEAR;
}
}
#endif
glTexParameteri(t->gl.target, GL_TEXTURE_MIN_FILTER, glTexParameteri(t->gl.target, GL_TEXTURE_MIN_FILTER,
(GLint)GLUtils::getTextureFilter(params.filterMin)); (GLint)GLUtils::getTextureFilter(params.filterMin));
glTexParameteri(t->gl.target, GL_TEXTURE_MAG_FILTER, glTexParameteri(t->gl.target, GL_TEXTURE_MAG_FILTER,
+1 -1
View File
@@ -91,7 +91,7 @@ public:
GLenum getIndicesType() const noexcept { GLenum getIndicesType() const noexcept {
return indicesType; return indicesType;
} }
}; } gl;
static bool queryOpenGLVersion(GLint* major, GLint* minor) noexcept; static bool queryOpenGLVersion(GLint* major, GLint* minor) noexcept;
@@ -45,19 +45,6 @@ Driver* OpenGLPlatform::createDefaultDriver(OpenGLPlatform* platform,
OpenGLPlatform::~OpenGLPlatform() noexcept = default; OpenGLPlatform::~OpenGLPlatform() noexcept = default;
utils::CString OpenGLPlatform::getDeviceInfo(DeviceInfoType infoType,
Driver* driver) const noexcept {
switch (infoType) {
case DeviceInfoType::OPENGL_RENDERER:
return getRendererString(driver);
case DeviceInfoType::OPENGL_VENDOR:
return getVendorString(driver);
default:
FILAMENT_CHECK_POSTCONDITION(false) << "Unsupported DeviceInfoType for OpenGLPlatform";
return {};
}
}
utils::CString OpenGLPlatform::getVendorString(Driver const* driver) { utils::CString OpenGLPlatform::getVendorString(Driver const* driver) {
auto const p = static_cast<OpenGLDriverBase const*>(driver); auto const p = static_cast<OpenGLDriverBase const*>(driver);
#if UTILS_HAS_RTTI #if UTILS_HAS_RTTI
@@ -706,11 +706,10 @@ bool PlatformEGL::setExternalImage(ExternalImageHandleRef externalImage,
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------
void PlatformEGL::initializeGlExtensions() noexcept { void PlatformEGL::initializeGlExtensions() noexcept {
// We're guaranteed to be on an ES platform, since we're using EGL
const char* const extensions = (const char*)glGetString(GL_EXTENSIONS); const char* const extensions = (const char*)glGetString(GL_EXTENSIONS);
if (extensions) { GLUtils::unordered_string_set const glExtensions = GLUtils::split(extensions);
GLUtils::unordered_string_set const glExtensions = GLUtils::split(extensions); ext.gl.OES_EGL_image_external_essl3 = glExtensions.has("GL_OES_EGL_image_external_essl3");
ext.gl.OES_EGL_image_external_essl3 = glExtensions.has("GL_OES_EGL_image_external_essl3");
}
} }
EGLContext PlatformEGL::getContextForType(ContextType const type) const noexcept { EGLContext PlatformEGL::getContextForType(ContextType const type) const noexcept {
@@ -29,12 +29,6 @@ using namespace bluevk;
namespace filament::backend { 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, FenceStatus VulkanCmdFence::wait(VkDevice device, uint64_t const timeout,
std::chrono::steady_clock::time_point const until) { std::chrono::steady_clock::time_point const until) {
@@ -40,13 +40,6 @@ struct VulkanCmdFence {
explicit VulkanCmdFence(VkFence fence) : mFence(fence) { } explicit VulkanCmdFence(VkFence fence) : mFence(fence) { }
~VulkanCmdFence() = default; ~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) { void setStatus(VkResult const value) {
std::lock_guard const l(mLock); std::lock_guard const l(mLock);
mStatus = value; mStatus = value;
+1 -5
View File
@@ -277,11 +277,7 @@ private:
fvkmemory::resource_ptr<VulkanSemaphore> mLastSubmit; fvkmemory::resource_ptr<VulkanSemaphore> mLastSubmit;
VkFence mLastFence = VK_NULL_HANDLE; VkFence mLastFence = VK_NULL_HANDLE;
// Start out with a completed fence, because if no commands have std::shared_ptr<VulkanCmdFence> mLastFenceStatus;
// been queued or submited, then by definition, all pending work
// is complete.
std::shared_ptr<VulkanCmdFence> mLastFenceStatus =
VulkanCmdFence::completed();
VkPipelineStageFlags mInjectedDependencyWaitStage = 0; VkPipelineStageFlags mInjectedDependencyWaitStage = 0;
}; };
+2 -17
View File
@@ -36,7 +36,6 @@ VK_DEFINE_HANDLE(VmaPool)
namespace filament::backend { namespace filament::backend {
struct VulkanCommandBuffer; struct VulkanCommandBuffer;
struct VulkanRenderPass;
struct VulkanRenderTarget; struct VulkanRenderTarget;
struct VulkanSwapChain; struct VulkanSwapChain;
struct VulkanTexture; struct VulkanTexture;
@@ -59,11 +58,11 @@ struct VulkanAttachment {
VkImageSubresourceRange getSubresourceRange() const; VkImageSubresourceRange getSubresourceRange() const;
}; };
struct VulkanRenderPassContext { struct VulkanRenderPass {
// Between the begin and end command render pass we cache the command buffer // Between the begin and end command render pass we cache the command buffer
VulkanCommandBuffer* commandBuffer; VulkanCommandBuffer* commandBuffer;
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget; fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget;
fvkmemory::resource_ptr<VulkanRenderPass> renderPass; VkRenderPass renderPass;
RenderPassParams params; RenderPassParams params;
int currentSubpass; int currentSubpass;
}; };
@@ -137,14 +136,6 @@ public:
return mFenceExportFlags; return mFenceExportFlags;
} }
inline const char* getPhysicalDeviceName() const noexcept {
return mPhysicalDeviceProperties.properties.deviceName;
}
inline const char* getDriverName() const noexcept { return mDriverProperties.driverName; }
inline const char* getDriverInfo() const noexcept { return mDriverProperties.driverInfo; }
inline bool isImageCubeArraySupported() const noexcept { inline bool isImageCubeArraySupported() const noexcept {
return mPhysicalDeviceFeatures.features.imageCubeArray == VK_TRUE; return mPhysicalDeviceFeatures.features.imageCubeArray == VK_TRUE;
} }
@@ -220,16 +211,11 @@ public:
return mGlobalPrioritySupported; return mGlobalPrioritySupported;
} }
inline bool isDriverPropertiesSupported() const noexcept { return mDriverPropertiesSupported; }
private: private:
VkPhysicalDeviceMemoryProperties mMemoryProperties = {}; VkPhysicalDeviceMemoryProperties mMemoryProperties = {};
VkPhysicalDeviceProperties2 mPhysicalDeviceProperties = { VkPhysicalDeviceProperties2 mPhysicalDeviceProperties = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
}; };
VkPhysicalDeviceDriverProperties mDriverProperties = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES,
};
VkPhysicalDeviceVulkan11Features mPhysicalDeviceVk11Features = { VkPhysicalDeviceVulkan11Features mPhysicalDeviceVk11Features = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES,
}; };
@@ -259,7 +245,6 @@ private:
bool mProtectedMemorySupported = false; bool mProtectedMemorySupported = false;
bool mVertexInputDynamicStateSupported = false; bool mVertexInputDynamicStateSupported = false;
bool mGlobalPrioritySupported = false; bool mGlobalPrioritySupported = false;
bool mDriverPropertiesSupported = false;
// These are options that can be enabled or disabled at an application level. // These are options that can be enabled or disabled at an application level.
bool mAsyncPipelineCachePrewarmingEnabled = false; bool mAsyncPipelineCachePrewarmingEnabled = false;
@@ -355,8 +355,35 @@ void VulkanDescriptorSetCache::updateBuffer(fvkmemory::resource_ptr<VulkanDescri
} }
void VulkanDescriptorSetCache::updateSampler(fvkmemory::resource_ptr<VulkanDescriptorSet> set, void VulkanDescriptorSetCache::updateSampler(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
uint8_t binding, fvkmemory::resource_ptr<VulkanTexture> texture, uint8_t binding, fvkmemory::resource_ptr<VulkanTexture> texture, VkSampler sampler,
VkSampler sampler, VkDescriptorSetLayout externalSamplerLayout) noexcept { VkDescriptorSetLayout externalSamplerLayout) noexcept {
// We have to update a bound set for two use cases
// - streaming API (a changing feed of AHardwareBuffer)
// - external samplers - potential changing of dataspace per-frame
// TODO: Fix the stream flow case base on the above comment!!
if (set->isAnExternalSamplerBound) {
auto layout = set->getLayout();
// Build a new descriptor set from the new layout
VkDescriptorSetLayout const genLayout = set->boundLayout;
VkDescriptorSet const newSet = getVkSet(layout->count, genLayout);
Bitmask const ubo = layout->bitmask.ubo | layout->bitmask.dynamicUbo;
Bitmask samplers = layout->bitmask.sampler;
samplers.unset(binding);
// Each bitmask denotes a binding index, and separated into two stages - vertex and buffer
// We fold the two stages into just the lower half of the bits to denote a combined set of
// bindings.
Bitmask const copyBindings = foldBitsInHalf(ubo | samplers);
VkDescriptorSet const srcSet = set->getVkSet();
copySet(srcSet, newSet, copyBindings);
set->addNewSet(newSet,
[this, layoutCount = layout->count, genLayout, newSet](VulkanDescriptorSet*) {
this->manualRecycle(layoutCount, genLayout, newSet);
});
}
VkDescriptorSet const vkset = set->getVkSet(); VkDescriptorSet const vkset = set->getVkSet();
VkImageSubresourceRange range = texture->getPrimaryViewRange(); VkImageSubresourceRange range = texture->getPrimaryViewRange();
VkImageViewType const expectedType = texture->getViewType(); VkImageViewType const expectedType = texture->getViewType();
@@ -405,28 +432,6 @@ fvkmemory::resource_ptr<VulkanDescriptorSet> VulkanDescriptorSetCache::createSet
return set; return set;
} }
void VulkanDescriptorSetCache::cloneSet(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
fvkutils::SamplerBitmask samplerMask) noexcept {
auto const& layout = set->getLayout();
// Build a new descriptor set from layout
VkDescriptorSetLayout const genLayout = set->boundLayout;
VkDescriptorSet const newSet = getVkSet(layout->count, genLayout);
// Each bitmask denotes a binding index, and separated into two stages - vertex and buffer
// We fold the two stages into just the lower half of the bits to denote a combined set of
// bindings.
Bitmask const ubo = layout->bitmask.ubo | layout->bitmask.dynamicUbo;
// Don't copy the samplers in the mask
Bitmask samplers = layout->bitmask.sampler ^ samplerMask;
Bitmask const copyBindings = foldBitsInHalf(ubo | samplers);
VkDescriptorSet const srcSet = set->getVkSet();
copySet(srcSet, newSet, copyBindings);
set->addNewSet(newSet,
[this, layoutCount = layout->count, genLayout, newSet](
VulkanDescriptorSet*) { this->manualRecycle(layoutCount, genLayout, newSet); });
}
VkDescriptorSet VulkanDescriptorSetCache::getVkSet(DescriptorCount const& count, VkDescriptorSet VulkanDescriptorSetCache::getVkSet(DescriptorCount const& count,
VkDescriptorSetLayout vklayout) { VkDescriptorSetLayout vklayout) {
return mDescriptorPool->obtainSet(count, vklayout); return mDescriptorPool->obtainSet(count, vklayout);
@@ -76,11 +76,6 @@ public:
fvkmemory::resource_ptr<VulkanDescriptorSet> createSet(Handle<HwDescriptorSet> handle, fvkmemory::resource_ptr<VulkanDescriptorSet> createSet(Handle<HwDescriptorSet> handle,
fvkmemory::resource_ptr<VulkanDescriptorSetLayout> layout); fvkmemory::resource_ptr<VulkanDescriptorSetLayout> layout);
// Create and set as current a new VkDescriptorSet using the `set` currently bound layout and
// copy all the bindings and ignoring the samplers bindings in the `samplerMask`.
void cloneSet(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
fvkutils::SamplerBitmask samplerMask) noexcept;
// This method is meant to be used with external samplers // This method is meant to be used with external samplers
VkDescriptorSet getVkSet(DescriptorCount const& count, VkDescriptorSetLayout vklayout); VkDescriptorSet getVkSet(DescriptorCount const& count, VkDescriptorSetLayout vklayout);
+15 -21
View File
@@ -260,8 +260,7 @@ VulkanDriver::VulkanDriver(VulkanPlatform* platform, VulkanContext& context,
mPipelineCache(*this, mPlatform->getDevice(), mContext), mPipelineCache(*this, mPlatform->getDevice(), mContext),
mStagePool(mAllocator, &mResourceManager, &mCommands, &mContext.getPhysicalDeviceLimits()), mStagePool(mAllocator, &mResourceManager, &mCommands, &mContext.getPhysicalDeviceLimits()),
mBufferCache(mContext, mResourceManager, mAllocator), mBufferCache(mContext, mResourceManager, mAllocator),
mFramebufferCache(mPlatform->getDevice(), mFramebufferCache(mPlatform->getDevice()),
mPlatform->getCustomization().timeBeforeEvictionFbo),
mYcbcrConversionCache(mPlatform->getDevice()), mYcbcrConversionCache(mPlatform->getDevice()),
mSamplerCache(mPlatform->getDevice()), mSamplerCache(mPlatform->getDevice()),
mBlitter(mPlatform->getPhysicalDevice(), &mCommands), mBlitter(mPlatform->getPhysicalDevice(), &mCommands),
@@ -1914,6 +1913,9 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
auto rt = resource_ptr<VulkanRenderTarget>::cast(&mResourceManager, rth); 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 // 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 // first render pass. Note however that its contents are often preserved on subsequent render
// passes, due to multiple views. // passes, due to multiple views.
@@ -1928,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; // Note that retrieving the extent must come after the acquireNextSwapchainImage() above;
// otherwise it might be 0. // otherwise it might be 0.
VkExtent2D const extent = rt->getExtent(); VkExtent2D const extent = rt->getExtent();
@@ -1978,19 +1975,17 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
rpkey.initialDepthLayout = currentDepthLayout; rpkey.initialDepthLayout = currentDepthLayout;
rpkey.subpassMask = uint8_t(params.subpassMask); rpkey.subpassMask = uint8_t(params.subpassMask);
fvkmemory::resource_ptr<VulkanRenderPass> renderPass = VkRenderPass renderPass = mFramebufferCache.getRenderPass(rpkey);
mFramebufferCache.getRenderPass(rpkey, &mResourceManager);
mPipelineCache.bindRenderPass(renderPass, 0); mPipelineCache.bindRenderPass(renderPass, 0);
// Create the VkFramebuffer or fetch it from cache. // Create the VkFramebuffer or fetch it from cache.
VulkanFboCache::FboKey fbkey = rt->getFboKey(); VulkanFboCache::FboKey fbkey = rt->getFboKey();
fbkey.renderPass = renderPass->getVkRenderPass(); fbkey.renderPass = renderPass;
fbkey.layers = 1; fbkey.layers = 1;
rt->emitBarriersBeginRenderPass(*commandBuffer); rt->emitBarriersBeginRenderPass(*commandBuffer);
fvkmemory::resource_ptr<VulkanFramebuffer> vkfb = VkFramebuffer vkfb = mFramebufferCache.getFramebuffer(fbkey);
mFramebufferCache.getFramebuffer(fbkey, &mResourceManager, rt);
// Assign a label to the framebuffer for debugging purposes. // Assign a label to the framebuffer for debugging purposes.
#if FVK_ENABLED(FVK_DEBUG_GROUP_MARKERS | FVK_DEBUG_DEBUG_UTILS) #if FVK_ENABLED(FVK_DEBUG_GROUP_MARKERS | FVK_DEBUG_DEBUG_UTILS)
@@ -2003,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. // The current command buffer now has references to the render target and its attachments.
commandBuffer->acquire(rt); commandBuffer->acquire(rt);
commandBuffer->acquire(renderPass);
commandBuffer->acquire(vkfb);
// Populate the structures required for vkCmdBeginRenderPass. // Populate the structures required for vkCmdBeginRenderPass.
VkRenderPassBeginInfo renderPassInfo { VkRenderPassBeginInfo renderPassInfo {
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
.renderPass = renderPass->getVkRenderPass(), .renderPass = renderPass,
.framebuffer = vkfb->getVkFramebuffer(), .framebuffer = vkfb,
// The renderArea field constrains the LoadOp, but scissoring does not. // 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(). // Therefore, we do not set the scissor rect here, we only need it in draw().
@@ -2065,7 +2058,7 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
mCurrentRenderPass = { mCurrentRenderPass = {
.commandBuffer = commandBuffer, .commandBuffer = commandBuffer,
.renderTarget = rt, .renderTarget = rt,
.renderPass = renderPass, .renderPass = renderPassInfo.renderPass,
.params = params, .params = params,
.currentSubpass = 0, .currentSubpass = 0,
}; };
@@ -2085,7 +2078,8 @@ void VulkanDriver::endRenderPass(int) {
rt->emitBarriersEndRenderPass(*mCurrentRenderPass.commandBuffer); rt->emitBarriersEndRenderPass(*mCurrentRenderPass.commandBuffer);
mCurrentRenderPass.renderTarget = {}; mCurrentRenderPass.renderTarget = {};
mCurrentRenderPass.renderPass = {}; mCurrentRenderPass.renderPass = VK_NULL_HANDLE;
mCurrentRenderPass.commandBuffer = nullptr; mCurrentRenderPass.commandBuffer = nullptr;
} }
@@ -2250,7 +2244,7 @@ void VulkanDriver::resolve(
Handle<HwTexture> src, uint8_t dstLevel, uint8_t dstLayer) { Handle<HwTexture> src, uint8_t dstLevel, uint8_t dstLayer) {
FVK_SYSTRACE_SCOPE(); FVK_SYSTRACE_SCOPE();
FILAMENT_CHECK_PRECONDITION(!mCurrentRenderPass.renderPass) FILAMENT_CHECK_PRECONDITION(mCurrentRenderPass.renderPass == VK_NULL_HANDLE)
<< "resolve() cannot be invoked inside a render pass."; << "resolve() cannot be invoked inside a render pass.";
auto srcTexture = resource_ptr<VulkanTexture>::cast(&mResourceManager, src); auto srcTexture = resource_ptr<VulkanTexture>::cast(&mResourceManager, src);
@@ -2293,7 +2287,7 @@ void VulkanDriver::blit(
math::uint2 size) { math::uint2 size) {
FVK_SYSTRACE_SCOPE(); FVK_SYSTRACE_SCOPE();
FILAMENT_CHECK_PRECONDITION(!mCurrentRenderPass.renderPass) FILAMENT_CHECK_PRECONDITION(mCurrentRenderPass.renderPass == VK_NULL_HANDLE)
<< "blit() cannot be invoked inside a render pass."; << "blit() cannot be invoked inside a render pass.";
auto srcTexture = resource_ptr<VulkanTexture>::cast(&mResourceManager, src); auto srcTexture = resource_ptr<VulkanTexture>::cast(&mResourceManager, src);
@@ -2335,7 +2329,7 @@ void VulkanDriver::blitDEPRECATED(TargetBufferFlags buffers,
// Note: blitDEPRECATED is only used for Renderer::copyFrame() // 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."; << "blitDEPRECATED() cannot be invoked inside a render pass.";
FILAMENT_CHECK_PRECONDITION(buffers == TargetBufferFlags::COLOR0) FILAMENT_CHECK_PRECONDITION(buffers == TargetBufferFlags::COLOR0)
+1 -1
View File
@@ -139,7 +139,7 @@ private:
resource_ptr<VulkanSwapChain> mCurrentSwapChain; resource_ptr<VulkanSwapChain> mCurrentSwapChain;
resource_ptr<VulkanRenderTarget> mDefaultRenderTarget; resource_ptr<VulkanRenderTarget> mDefaultRenderTarget;
VulkanRenderPassContext mCurrentRenderPass = {}; VulkanRenderPass mCurrentRenderPass = {};
VmaAllocator mAllocator = VK_NULL_HANDLE; VmaAllocator mAllocator = VK_NULL_HANDLE;
VkDebugReportCallbackEXT mDebugCallback = VK_NULL_HANDLE; VkDebugReportCallbackEXT mDebugCallback = VK_NULL_HANDLE;
@@ -94,9 +94,6 @@ void VulkanExternalImageManager::updateSetAndLayout(
fvkmemory::resource_ptr<VulkanDescriptorSetLayout> const& layout = set->getLayout(); fvkmemory::resource_ptr<VulkanDescriptorSetLayout> const& layout = set->getLayout();
set->boundLayout = mDescriptorSetLayoutCache->getVkLayout(layout->bitmask, set->boundLayout = mDescriptorSetLayoutCache->getVkLayout(layout->bitmask,
actualExternalSamplers, outSamplers); actualExternalSamplers, outSamplers);
mDescriptorSetCache->cloneSet(set, actualExternalSamplers);
// Update the external samplers in the set // Update the external samplers in the set
for (auto& [binding, sampler, image]: samplerAndBindings) { for (auto& [binding, sampler, image]: samplerAndBindings) {
// We cannot call updateSamplerForExternalSamplerSet because some samplers are non NULL // We cannot call updateSamplerForExternalSamplerSet because some samplers are non NULL
+39 -46
View File
@@ -17,10 +17,8 @@
#include "VulkanFboCache.h" #include "VulkanFboCache.h"
#include "VulkanConstants.h" #include "VulkanConstants.h"
#include "VulkanHandles.h"
#include "vulkan/utils/Image.h" #include "vulkan/utils/Image.h"
#include <utils/compiler.h>
#include <utils/Panic.h> #include <utils/Panic.h>
// If any VkRenderPass or VkFramebuffer is unused for more than TIME_BEFORE_EVICTION frames, it // If any VkRenderPass or VkFramebuffer is unused for more than TIME_BEFORE_EVICTION frames, it
@@ -63,20 +61,17 @@ bool VulkanFboCache::FboKeyEqualFn::operator()(const FboKey& k1, const FboKey& k
return true; return true;
} }
VulkanFboCache::VulkanFboCache(VkDevice device, uint32_t timeBeforeEvictionFbo) VulkanFboCache::VulkanFboCache(VkDevice device)
: mDevice(device), : mDevice(device) {}
mTimeBeforeEvictionFbo(timeBeforeEvictionFbo) {}
VulkanFboCache::~VulkanFboCache() { VulkanFboCache::~VulkanFboCache() {
FILAMENT_CHECK_POSTCONDITION(mFramebufferCache.empty() && mRenderPassCache.empty()) FILAMENT_CHECK_POSTCONDITION(mFramebufferCache.empty() && mRenderPassCache.empty())
<< "Please explicitly call terminate() while the VkDevice is still alive."; << "Please explicitly call terminate() while the VkDevice is still alive.";
} }
fvkmemory::resource_ptr<VulkanFramebuffer> VulkanFboCache::getFramebuffer(FboKey const& config, VkFramebuffer VulkanFboCache::getFramebuffer(FboKey const& config) noexcept {
fvkmemory::ResourceManager* resManager,
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget) noexcept {
FboMap::iterator iter = mFramebufferCache.find(config); 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; iter.value().timestamp = mCurrentTime;
return iter->second.handle; return iter->second.handle;
} }
@@ -122,17 +117,13 @@ fvkmemory::resource_ptr<VulkanFramebuffer> VulkanFboCache::getFramebuffer(FboKey
VkResult error = vkCreateFramebuffer(mDevice, &info, VKALLOC, &framebuffer); VkResult error = vkCreateFramebuffer(mDevice, &info, VKALLOC, &framebuffer);
FILAMENT_CHECK_POSTCONDITION(error == VK_SUCCESS) << "Unable to create framebuffer." FILAMENT_CHECK_POSTCONDITION(error == VK_SUCCESS) << "Unable to create framebuffer."
<< " error=" << static_cast<int32_t>(error); << " error=" << static_cast<int32_t>(error);
fvkmemory::resource_ptr<VulkanFramebuffer> fbh = mFramebufferCache[config] = {framebuffer, mCurrentTime};
fvkmemory::resource_ptr<VulkanFramebuffer>::construct(resManager, mDevice, framebuffer, return framebuffer;
renderTarget);
mFramebufferCache[config] = { fbh, mCurrentTime };
return fbh;
} }
fvkmemory::resource_ptr<VulkanRenderPass> VulkanFboCache::getRenderPass( VkRenderPass VulkanFboCache::getRenderPass(RenderPassKey const& config) noexcept {
RenderPassKey const& config, fvkmemory::ResourceManager* resManager) noexcept {
auto iter = mRenderPassCache.find(config); 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; iter.value().timestamp = mCurrentTime;
return iter->second.handle; return iter->second.handle;
} }
@@ -335,9 +326,7 @@ fvkmemory::resource_ptr<VulkanRenderPass> VulkanFboCache::getRenderPass(
VkResult error = vkCreateRenderPass(mDevice, &renderPassInfo, VKALLOC, &renderPass); VkResult error = vkCreateRenderPass(mDevice, &renderPassInfo, VKALLOC, &renderPass);
FILAMENT_CHECK_POSTCONDITION(error == VK_SUCCESS) << "Unable to create render pass." FILAMENT_CHECK_POSTCONDITION(error == VK_SUCCESS) << "Unable to create render pass."
<< " error=" << error; << " error=" << error;
fvkmemory::resource_ptr<VulkanRenderPass> rph = mRenderPassCache[config] = {renderPass, mCurrentTime};
fvkmemory::resource_ptr<VulkanRenderPass>::construct(resManager, mDevice, renderPass);
mRenderPassCache[config] = {rph, mCurrentTime};
#if FVK_ENABLED(FVK_DEBUG_FBO_CACHE) #if FVK_ENABLED(FVK_DEBUG_FBO_CACHE)
FVK_LOGD << "Created render pass " << renderPass << " with "; FVK_LOGD << "Created render pass " << renderPass << " with ";
@@ -354,12 +343,13 @@ fvkmemory::resource_ptr<VulkanRenderPass> VulkanFboCache::getRenderPass(
<< "colorAttachmentCount[0] = " << subpasses[0].colorAttachmentCount; << "colorAttachmentCount[0] = " << subpasses[0].colorAttachmentCount;
#endif #endif
return rph; return renderPass;
} }
void VulkanFboCache::resetFramebuffers() noexcept { void VulkanFboCache::resetFramebuffers() noexcept {
for (const auto& pair: mFramebufferCache) { for (const auto& pair: mFramebufferCache) {
mRenderPassRefCount[pair.first.renderPass]--; mRenderPassRefCount[pair.first.renderPass]--;
vkDestroyFramebuffer(mDevice, pair.second.handle, VKALLOC);
} }
mFramebufferCache.clear(); mFramebufferCache.clear();
} }
@@ -367,6 +357,9 @@ void VulkanFboCache::resetFramebuffers() noexcept {
void VulkanFboCache::terminate() noexcept { void VulkanFboCache::terminate() noexcept {
resetFramebuffers(); resetFramebuffers();
for (const auto& pair: mRenderPassCache) {
vkDestroyRenderPass(mDevice, pair.second.handle, VKALLOC);
}
mRenderPassRefCount.clear(); mRenderPassRefCount.clear();
mRenderPassCache.clear(); mRenderPassCache.clear();
} }
@@ -378,36 +371,36 @@ void VulkanFboCache::gc() noexcept {
FVK_SYSTRACE_START("fbocache::gc"); FVK_SYSTRACE_START("fbocache::gc");
// If this is one of the first few frames, return early to avoid wrapping unsigned integers. // If this is one of the first few frames, return early to avoid wrapping unsigned integers.
++mCurrentTime; if (++mCurrentTime <= TIME_BEFORE_EVICTION) {
return;
}
const uint32_t evictTime = mCurrentTime - TIME_BEFORE_EVICTION;
if (UTILS_UNLIKELY(mCurrentTime > mTimeBeforeEvictionFbo)) { for (FboMap::iterator iter = mFramebufferCache.begin(); iter != mFramebufferCache.end(); ) {
const uint32_t evictTimeFbo = mCurrentTime - mTimeBeforeEvictionFbo; const FboVal fbo = iter->second;
for (FboMap::iterator iter = mFramebufferCache.begin(); iter != mFramebufferCache.end();) { if (fbo.timestamp < evictTime && fbo.handle) {
const FboVal fbo = iter->second; mRenderPassRefCount[iter->first.renderPass]--;
if (fbo.timestamp < evictTimeFbo && fbo.handle) { vkDestroyFramebuffer(mDevice, fbo.handle, VKALLOC);
mRenderPassRefCount[iter->first.renderPass]--; iter.value().handle = VK_NULL_HANDLE;
// erase(iterator) returns the iterator to the next element. // erase(iterator) returns the iterator to the next element.
iter = mFramebufferCache.erase(iter); iter = mFramebufferCache.erase(iter);
} else { } else {
++iter; ++iter;
}
} }
} }
if (UTILS_UNLIKELY(mCurrentTime > TIME_BEFORE_EVICTION)) { for (RenderPassMap::iterator iter = mRenderPassCache.begin(); iter != mRenderPassCache.end(); ) {
const uint32_t evictTimeRp = mCurrentTime - TIME_BEFORE_EVICTION; const VkRenderPass handle = iter->second.handle;
for (RenderPassMap::iterator iter = mRenderPassCache.begin(); if (iter->second.timestamp < evictTime && handle && mRenderPassRefCount[handle] == 0) {
iter != mRenderPassCache.end();) { vkDestroyRenderPass(mDevice, handle, VKALLOC);
const VkRenderPass handle = iter->second.handle->getVkRenderPass(); iter.value().handle = VK_NULL_HANDLE;
if (iter->second.timestamp < evictTimeRp && handle &&
mRenderPassRefCount[handle] == 0) { // erase(iterator) returns the iterator to the next element.
// erase(iterator) returns the iterator to the next element. iter = mRenderPassCache.erase(iter);
iter = mRenderPassCache.erase(iter); mRenderPassRefCount.erase(handle);
mRenderPassRefCount.erase(handle); } else {
} else { ++iter;
++iter;
}
} }
} }
+5 -15
View File
@@ -18,9 +18,6 @@
#define TNT_FILAMENT_BACKEND_VULKANFBOCACHE_H #define TNT_FILAMENT_BACKEND_VULKANFBOCACHE_H
#include "VulkanContext.h" #include "VulkanContext.h"
#include "vulkan/memory/Resource.h"
#include "vulkan/memory/ResourceManager.h"
#include "vulkan/memory/ResourcePointer.h"
#include <utils/Hash.h> #include <utils/Hash.h>
@@ -30,9 +27,6 @@
namespace filament::backend { namespace filament::backend {
struct VulkanFramebuffer;
struct VulkanRenderPass;
// Simple manager for VkFramebuffer and VkRenderPass objects. // 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, // 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]; uint8_t padding[2];
}; };
struct RenderPassVal { struct RenderPassVal {
fvkmemory::resource_ptr<VulkanRenderPass> handle; VkRenderPass handle;
uint32_t timestamp; uint32_t timestamp;
}; };
static_assert(0 == MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT % 8); static_assert(0 == MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT % 8);
@@ -89,7 +83,7 @@ public:
VkImageView depth; // 8 bytes VkImageView depth; // 8 bytes
}; };
struct FboVal { struct FboVal {
fvkmemory::resource_ptr<VulkanFramebuffer> handle; VkFramebuffer handle;
uint32_t timestamp; uint32_t timestamp;
}; };
static_assert(sizeof(VkRenderPass) == 8, "VkRenderPass has unexpected size."); static_assert(sizeof(VkRenderPass) == 8, "VkRenderPass has unexpected size.");
@@ -100,17 +94,14 @@ public:
bool operator()(const FboKey& k1, const FboKey& k2) const; bool operator()(const FboKey& k1, const FboKey& k2) const;
}; };
explicit VulkanFboCache(VkDevice device, uint32_t timeBeforeEvictionFbo); explicit VulkanFboCache(VkDevice device);
~VulkanFboCache(); ~VulkanFboCache();
// Retrieves or creates a VkFramebuffer handle. // Retrieves or creates a VkFramebuffer handle.
fvkmemory::resource_ptr<VulkanFramebuffer> getFramebuffer(FboKey const& config, VkFramebuffer getFramebuffer(FboKey const& config) noexcept;
fvkmemory::ResourceManager* resManager,
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget) noexcept;
// Retrieves or creates a VkRenderPass handle. // Retrieves or creates a VkRenderPass handle.
fvkmemory::resource_ptr<VulkanRenderPass> getRenderPass( VkRenderPass getRenderPass(RenderPassKey const& config) noexcept;
RenderPassKey const& config, fvkmemory::ResourceManager* resManager) noexcept;
// Evicts old unused Vulkan objects. Call this once per frame. // Evicts old unused Vulkan objects. Call this once per frame.
void gc() noexcept; void gc() noexcept;
@@ -130,7 +121,6 @@ private:
RenderPassMap mRenderPassCache; RenderPassMap mRenderPassCache;
tsl::robin_map<VkRenderPass, uint32_t> mRenderPassRefCount; tsl::robin_map<VkRenderPass, uint32_t> mRenderPassRefCount;
uint32_t mCurrentTime = 0; uint32_t mCurrentTime = 0;
uint32_t mTimeBeforeEvictionFbo;
}; };
} // namespace filament::backend } // namespace filament::backend
+1 -18
View File
@@ -565,7 +565,7 @@ void VulkanRenderTarget::transformViewportToPlatform(VkViewport* bounds) const {
flipVertically(bounds, getExtent().height); flipVertically(bounds, getExtent().height);
} }
uint8_t VulkanRenderTarget::getColorTargetCount(const VulkanRenderPassContext& pass) const { uint8_t VulkanRenderTarget::getColorTargetCount(const VulkanRenderPass& pass) const {
if (!mOffscreen) { if (!mOffscreen) {
return 1; return 1;
} }
@@ -715,21 +715,4 @@ VulkanRenderPrimitive::VulkanRenderPrimitive(PrimitiveType pt,
vertexBuffer(vb), vertexBuffer(vb),
indexBuffer(ib) {} indexBuffer(ib) {}
VulkanFramebuffer::VulkanFramebuffer(VkDevice device, VkFramebuffer framebuffer,
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget)
: mDevice(device),
mFramebuffer(framebuffer),
mRenderTarget(renderTarget) {}
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 } // namespace filament::backend
+3 -36
View File
@@ -195,7 +195,7 @@ public:
} }
// The current layout used by the descriptor set. This one will match the bindings, including // The current layout used by the descriptor set. This one will match the bindings, including
// external samplers data. // external samplers data.
// This will not necessarilly be the same as `mLayout`. // This will not necessarilly be the same as `mLayout`.
VkDescriptorSetLayout boundLayout = VK_NULL_HANDLE; VkDescriptorSetLayout boundLayout = VK_NULL_HANDLE;
@@ -203,7 +203,7 @@ public:
fvkutils::UniformBufferBitmask const& dynamicUboMask; fvkutils::UniformBufferBitmask const& dynamicUboMask;
uint8_t const uniqueDynamicUboCount; uint8_t const uniqueDynamicUboCount;
// Flag to indicate if the current layout needs to be recreated or not. // Flag to indicate if the current layout needs to be recreated or not.
// This should only set to `true` when a external sampler image is bound to the descriptor set. // This should only set to `true` when a external sampler image is bound to the descriptor set.
bool isLayoutDirty = false; bool isLayoutDirty = false;
@@ -419,7 +419,7 @@ struct VulkanRenderTarget : private HwRenderTarget, fvkmemory::Resource {
return mInfo->fbkey.samples; 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; } inline bool hasDepth() const { return mInfo->depthIndex != Auxiliary::UNDEFINED_INDEX; }
@@ -585,39 +585,6 @@ struct VulkanRenderPrimitive : public HwRenderPrimitive, fvkmemory::Resource {
fvkmemory::resource_ptr<VulkanIndexBuffer> indexBuffer; fvkmemory::resource_ptr<VulkanIndexBuffer> indexBuffer;
}; };
struct VulkanFramebuffer : public fvkmemory::Resource {
VulkanFramebuffer(VkDevice device,
VkFramebuffer framebuffer,
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget);
~VulkanFramebuffer();
inline VkFramebuffer getVkFramebuffer() const noexcept {
return mFramebuffer;
}
private:
VkDevice mDevice;
VkFramebuffer mFramebuffer;
// We need to keep a reference to the renderTarget because the key of the framebuffer in the
// cache has references to the image views that are derived from the textures of the render
// target.
fvkmemory::resource_ptr<VulkanRenderTarget> mRenderTarget;
};
struct VulkanRenderPass : public fvkmemory::Resource {
VulkanRenderPass(VkDevice device, VkRenderPass renderPass);
~VulkanRenderPass();
inline VkRenderPass getVkRenderPass() const noexcept {
return mRenderPass;
}
private:
VkDevice mDevice;
VkRenderPass mRenderPass;
};
} // namespace filament::backend } // namespace filament::backend
#endif // TNT_FILAMENT_BACKEND_VULKANHANDLES_H #endif // TNT_FILAMENT_BACKEND_VULKANHANDLES_H
@@ -459,10 +459,8 @@ void VulkanPipelineCache::bindStencilState(StencilState const& stencilState) noe
mPipelineRequirements.stencilState = stencilState; mPipelineRequirements.stencilState = stencilState;
} }
void VulkanPipelineCache::bindRenderPass( void VulkanPipelineCache::bindRenderPass(VkRenderPass renderPass, int subpassIndex) noexcept {
fvkmemory::resource_ptr<VulkanRenderPass> renderPass, mPipelineRequirements.renderPass = renderPass;
int subpassIndex) noexcept {
mPipelineRequirements.renderPass = renderPass->getVkRenderPass();
mPipelineRequirements.subpassIndex = subpassIndex; mPipelineRequirements.subpassIndex = subpassIndex;
} }
@@ -123,8 +123,7 @@ public:
void bindProgram(fvkmemory::resource_ptr<VulkanProgram> program) noexcept; void bindProgram(fvkmemory::resource_ptr<VulkanProgram> program) noexcept;
void bindRasterState(RasterState const& rasterState) noexcept; void bindRasterState(RasterState const& rasterState) noexcept;
void bindStencilState(StencilState const& stencilState) noexcept; void bindStencilState(StencilState const& stencilState) noexcept;
void bindRenderPass(fvkmemory::resource_ptr<VulkanRenderPass> renderPass, void bindRenderPass(VkRenderPass renderPass, int subpassIndex) noexcept;
int subpassIndex) noexcept;
void bindPrimitiveTopology(VkPrimitiveTopology topology) noexcept; void bindPrimitiveTopology(VkPrimitiveTopology topology) noexcept;
void bindVertexArray(VkVertexInputAttributeDescription const* attribDesc, void bindVertexArray(VkVertexInputAttributeDescription const* attribDesc,
VkVertexInputBindingDescription const* bufferDesc, uint8_t count); VkVertexInputBindingDescription const* bufferDesc, uint8_t count);
@@ -42,8 +42,6 @@ template ResourceType getTypeEnum<VulkanSync>() noexcept;
template ResourceType getTypeEnum<VulkanMemoryMappedBuffer>() noexcept; template ResourceType getTypeEnum<VulkanMemoryMappedBuffer>() noexcept;
template ResourceType getTypeEnum<VulkanSemaphore>() noexcept; template ResourceType getTypeEnum<VulkanSemaphore>() noexcept;
template ResourceType getTypeEnum<VulkanStream>() noexcept; template ResourceType getTypeEnum<VulkanStream>() noexcept;
template ResourceType getTypeEnum<VulkanFramebuffer>() noexcept;
template ResourceType getTypeEnum<VulkanRenderPass>() noexcept;
template<typename D> template<typename D>
ResourceType getTypeEnum() noexcept { ResourceType getTypeEnum() noexcept {
@@ -110,12 +108,6 @@ ResourceType getTypeEnum() noexcept {
if constexpr (std::is_same_v<D, VulkanStream>) { if constexpr (std::is_same_v<D, VulkanStream>) {
return ResourceType::STREAM; 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; return ResourceType::UNDEFINED_TYPE;
} }
@@ -163,10 +155,6 @@ std::string_view getTypeStr(ResourceType type) {
return "Semaphore"; return "Semaphore";
case ResourceType::STREAM: case ResourceType::STREAM:
return "VulkanStream"; return "VulkanStream";
case ResourceType::FRAMEBUFFER:
return "Framebuffer";
case ResourceType::RENDER_PASS:
return "RenderPass";
case ResourceType::UNDEFINED_TYPE: case ResourceType::UNDEFINED_TYPE:
return ""; return "";
} }
@@ -56,9 +56,7 @@ enum class ResourceType : uint8_t {
MEMORY_MAPPED_BUFFER = 18, MEMORY_MAPPED_BUFFER = 18,
SEMAPHORE = 19, SEMAPHORE = 19,
STREAM = 20, STREAM = 20,
FRAMEBUFFER = 21, UNDEFINED_TYPE = 21, // Must be the last enum because we use it for iterating over the enums.
RENDER_PASS = 22,
UNDEFINED_TYPE = 23, // Must be the last enum because we use it for iterating over the enums.
}; };
template<typename D> template<typename D>
@@ -126,12 +126,6 @@ void ResourceManager::destroyWithType(ResourceType type, HandleId id) {
case ResourceType::STREAM: case ResourceType::STREAM:
destruct<VulkanStream>(Handle<VulkanStream>(id)); destruct<VulkanStream>(Handle<VulkanStream>(id));
break; break;
case ResourceType::FRAMEBUFFER:
destruct<VulkanFramebuffer>(Handle<VulkanFramebuffer>(id));
break;
case ResourceType::RENDER_PASS:
destruct<VulkanRenderPass>(Handle<VulkanRenderPass>(id));
break;
case ResourceType::UNDEFINED_TYPE: case ResourceType::UNDEFINED_TYPE:
break; break;
} }
@@ -704,30 +704,6 @@ VulkanPlatform::VulkanPlatform() = default;
VulkanPlatform::~VulkanPlatform() = default; VulkanPlatform::~VulkanPlatform() = default;
utils::CString VulkanPlatform::getDeviceInfo(DeviceInfoType infoType,
Driver* driver) const noexcept {
if (mImpl->mPhysicalDevice == VK_NULL_HANDLE) {
return {};
}
auto& context = mImpl->mContext;
switch (infoType) {
case DeviceInfoType::VULKAN_DEVICE_NAME: {
return utils::CString(context.getPhysicalDeviceName());
}
case DeviceInfoType::VULKAN_DRIVER_NAME: {
return context.isDriverPropertiesSupported() ? utils::CString(context.getDriverName())
: utils::CString();
}
case DeviceInfoType::VULKAN_DRIVER_INFO: {
return context.isDriverPropertiesSupported() ? utils::CString(context.getDriverInfo())
: utils::CString();
}
default:
FILAMENT_CHECK_POSTCONDITION(false) << "Unsupported DeviceInfoType for VulkanPlatform";
return {};
}
}
VulkanPlatform::SwapChainBundle VulkanPlatform::getSwapChainBundle(SwapChainPtr handle) { VulkanPlatform::SwapChainBundle VulkanPlatform::getSwapChainBundle(SwapChainPtr handle) {
return static_cast<VulkanPlatformSwapChainBase*>(handle)->getSwapChainBundle(); return static_cast<VulkanPlatformSwapChainBase*>(handle)->getSwapChainBundle();
} }
@@ -976,11 +952,6 @@ void VulkanPlatform::queryAndSetDeviceFeatures(Platform::DriverConfig const& dri
chainStruct(&context.mPhysicalDeviceFeatures, &globalPriorityFeatures); chainStruct(&context.mPhysicalDeviceFeatures, &globalPriorityFeatures);
} }
if (vkGetPhysicalDeviceProperties2) {
chainStruct(&context.mPhysicalDeviceProperties, &context.mDriverProperties);
context.mDriverPropertiesSupported = true;
}
// Initialize the following fields: physicalDeviceProperties, memoryProperties, // Initialize the following fields: physicalDeviceProperties, memoryProperties,
// physicalDeviceFeatures. // physicalDeviceFeatures.
vkGetPhysicalDeviceProperties2(mImpl->mPhysicalDevice, &context.mPhysicalDeviceProperties); vkGetPhysicalDeviceProperties2(mImpl->mPhysicalDevice, &context.mPhysicalDeviceProperties);
+31 -15
View File
@@ -192,15 +192,8 @@ void WebGPUDriver::flush(int) {
} }
void WebGPUDriver::finish(int /* dummy */) { void WebGPUDriver::finish(int /* dummy */) {
mReadPixelMapsCounter.waitForAllToFinish();
mQueueManager.finish(); mQueueManager.finish();
// We use polling to advance webgpu's callback counter until all the read backs have been
// processed. Note that blocking with mReadPixelMapsCounter.waitForAllToFinish will only
// deadlock since we could not advance the counter.
while (!mReadPixelMapsCounter.isIdle()) {
mAdapter.GetInstance().ProcessEvents();
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
} }
void WebGPUDriver::destroyRenderPrimitive(Handle<HwRenderPrimitive> rph) { void WebGPUDriver::destroyRenderPrimitive(Handle<HwRenderPrimitive> rph) {
@@ -947,6 +940,10 @@ size_t WebGPUDriver::getUniformBufferOffsetAlignment(){
void WebGPUDriver::updateIndexBuffer(Handle<HwIndexBuffer> indexBufferHandle, void WebGPUDriver::updateIndexBuffer(Handle<HwIndexBuffer> indexBufferHandle,
BufferDescriptor&& bufferDescriptor, const uint32_t byteOffset) { BufferDescriptor&& bufferDescriptor, const uint32_t byteOffset) {
// make sure command elements (draws, etc.) prior to the buffer update are processed before the
// update on the GPU, otherwise the expected data may not be available at the time certain
// draw calls are made.
flush();
handleCast<WebGPUIndexBuffer>(indexBufferHandle) handleCast<WebGPUIndexBuffer>(indexBufferHandle)
->updateGPUBuffer(bufferDescriptor, byteOffset, mDevice, &mQueueManager, &mStagePool); ->updateGPUBuffer(bufferDescriptor, byteOffset, mDevice, &mQueueManager, &mStagePool);
scheduleDestroy(std::move(bufferDescriptor)); scheduleDestroy(std::move(bufferDescriptor));
@@ -961,6 +958,10 @@ void WebGPUDriver::updateIndexBufferAsyncR(AsyncCallId jobId,
void WebGPUDriver::updateBufferObject(Handle<HwBufferObject> bufferObjectHandle, void WebGPUDriver::updateBufferObject(Handle<HwBufferObject> bufferObjectHandle,
BufferDescriptor&& bufferDescriptor, const uint32_t byteOffset) { BufferDescriptor&& bufferDescriptor, const uint32_t byteOffset) {
// make sure command elements (draws, etc.) prior to the buffer update are processed before the
// update on the GPU, otherwise the expected data may not be available at the time certain
// draw calls are made.
flush();
handleCast<WebGPUBufferObject>(bufferObjectHandle) handleCast<WebGPUBufferObject>(bufferObjectHandle)
->updateGPUBuffer(bufferDescriptor, byteOffset, mDevice, &mQueueManager, &mStagePool); ->updateGPUBuffer(bufferDescriptor, byteOffset, mDevice, &mQueueManager, &mStagePool);
scheduleDestroy(std::move(bufferDescriptor)); scheduleDestroy(std::move(bufferDescriptor));
@@ -1007,6 +1008,10 @@ void WebGPUDriver::update3DImage(Handle<HwTexture> textureHandle, const uint32_t
const uint32_t xoffset, const uint32_t yoffset, const uint32_t zoffset, const uint32_t xoffset, const uint32_t yoffset, const uint32_t zoffset,
const uint32_t width, const uint32_t height, const uint32_t depth, const uint32_t width, const uint32_t height, const uint32_t depth,
PixelBufferDescriptor&& pixelBufferDescriptor) { PixelBufferDescriptor&& pixelBufferDescriptor) {
// one way or another this function writes texture(s), thus any commands (draw calls etc.) should
// get submitted prior to these updates so that subsequent commands/draws run with the
// image/texture(s) updated as expected.
flush();
PixelBufferDescriptor* inputData{ &pixelBufferDescriptor }; PixelBufferDescriptor* inputData{ &pixelBufferDescriptor };
PixelBufferDescriptor reshapedData; PixelBufferDescriptor reshapedData;
if (reshape(pixelBufferDescriptor, reshapedData)) { if (reshape(pixelBufferDescriptor, reshapedData)) {
@@ -1178,6 +1183,11 @@ void WebGPUDriver::generateMipmaps(Handle<HwTexture> textureHandle) {
return; // nothing to do return; // nothing to do
} }
// make sure command elements (draws, etc.) prior to the texture update are processed before the
// update on the GPU.
// this ensures subsequent draw calls are run after the mipmaps have been generated as expected
flush();
const auto usage = wgpuTexture.GetUsage(); const auto usage = wgpuTexture.GetUsage();
FILAMENT_CHECK_PRECONDITION(usage & wgpu::TextureUsage::TextureBinding) FILAMENT_CHECK_PRECONDITION(usage & wgpu::TextureUsage::TextureBinding)
<< "Texture for mipmap generation must have TextureBinding usage."; << "Texture for mipmap generation must have TextureBinding usage.";
@@ -1366,9 +1376,6 @@ void WebGPUDriver::beginRenderPass(Handle<HwRenderTarget> renderTargetHandle,
customDepthStencilMsaaSidecarTextureView); customDepthStencilMsaaSidecarTextureView);
mRenderPassEncoder = commandEncoder.BeginRenderPass(&renderPassDescriptor); mRenderPassEncoder = commandEncoder.BeginRenderPass(&renderPassDescriptor);
// TODO: there's a bug here because the webgpu viewport has the origin at top-left, whereas
// filament expects it to be bottom left.
mRenderPassEncoder.SetViewport( mRenderPassEncoder.SetViewport(
static_cast<float>(params.viewport.left), static_cast<float>(params.viewport.left),
static_cast<float>(params.viewport.bottom), static_cast<float>(params.viewport.bottom),
@@ -1456,6 +1463,7 @@ void WebGPUDriver::stopCapture(int /* dummy */) {
void WebGPUDriver::readPixels(Handle<HwRenderTarget> sourceRenderTargetHandle, const uint32_t x, void WebGPUDriver::readPixels(Handle<HwRenderTarget> sourceRenderTargetHandle, const uint32_t x,
const uint32_t y, const uint32_t width, const uint32_t height, const uint32_t y, const uint32_t width, const uint32_t height,
PixelBufferDescriptor&& pixelBufferDescriptor) { PixelBufferDescriptor&& pixelBufferDescriptor) {
flush();
const auto srcTarget{ handleCast<WebGPURenderTarget>(sourceRenderTargetHandle) }; const auto srcTarget{ handleCast<WebGPURenderTarget>(sourceRenderTargetHandle) };
assert_invariant(srcTarget); assert_invariant(srcTarget);
@@ -1513,7 +1521,10 @@ void WebGPUDriver::readTextureToBuffer(wgpu::Texture srcTexture, uint32_t level,
return; return;
} }
auto commandEncoder = mQueueManager.getCommandEncoder(); const wgpu::CommandEncoderDescriptor commandEncoderDescriptor{
.label = "read_texture_to_buffer_command",
};
auto commandEncoder = mDevice.CreateCommandEncoder(&commandEncoderDescriptor);
FILAMENT_CHECK_POSTCONDITION(commandEncoder) FILAMENT_CHECK_POSTCONDITION(commandEncoder)
<< "Failed to create command encoder for readTextureToBuffer?"; << "Failed to create command encoder for readTextureToBuffer?";
@@ -1635,6 +1646,9 @@ void WebGPUDriver::readTextureToBuffer(wgpu::Texture srcTexture, uint32_t level,
.depthOrArrayLayers = 1, .depthOrArrayLayers = 1,
}; };
commandEncoder.CopyTextureToBuffer(&source, &destination, &copySize); commandEncoder.CopyTextureToBuffer(&source, &destination, &copySize);
wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
assert_invariant(commandBuffer);
mDevice.GetQueue().Submit(1, &commandBuffer);
// Map the buffer to read the data // Map the buffer to read the data
struct UserData final { struct UserData final {
@@ -1655,9 +1669,6 @@ void WebGPUDriver::readTextureToBuffer(wgpu::Texture srcTexture, uint32_t level,
}); });
mReadPixelMapsCounter.startTask(); mReadPixelMapsCounter.startTask();
// We need to flush here before we can readback from the staging buffer since the copy op is
// pending to be submitted.
mQueueManager.flush();
userData->buffer.MapAsync( userData->buffer.MapAsync(
wgpu::MapMode::Read, 0, bufferSize, wgpu::CallbackMode::AllowSpontaneous, wgpu::MapMode::Read, 0, bufferSize, wgpu::CallbackMode::AllowSpontaneous,
[](wgpu::MapAsyncStatus status, const char* message, UserData* userdata) { [](wgpu::MapAsyncStatus status, const char* message, UserData* userdata) {
@@ -1750,6 +1761,7 @@ void WebGPUDriver::blitDEPRECATED(TargetBufferFlags buffers,
const wgpu::Extent2D destinationSize{ static_cast<uint32_t>(destinationViewport.width), const wgpu::Extent2D destinationSize{ static_cast<uint32_t>(destinationViewport.width),
static_cast<uint32_t>(destinationViewport.height) }; static_cast<uint32_t>(destinationViewport.height) };
mQueueManager.flush();
wgpu::CommandEncoder commandEncoder = mQueueManager.getCommandEncoder(); wgpu::CommandEncoder commandEncoder = mQueueManager.getCommandEncoder();
const WebGPUBlitter::BlitArgs blitArgs{ const WebGPUBlitter::BlitArgs blitArgs{
.source = { .texture = sourceTexture->getTexture(), .source = { .texture = sourceTexture->getTexture(),
@@ -1769,6 +1781,8 @@ void WebGPUDriver::blitDEPRECATED(TargetBufferFlags buffers,
.filter = filter, .filter = filter,
}; };
mBlitter.blit(mDevice.GetQueue(), commandEncoder, blitArgs); mBlitter.blit(mDevice.GetQueue(), commandEncoder, blitArgs);
mQueueManager.flush();
} }
void WebGPUDriver::resolve(Handle<HwTexture> destinationTextureHandle, const uint8_t sourceLevel, void WebGPUDriver::resolve(Handle<HwTexture> destinationTextureHandle, const uint8_t sourceLevel,
@@ -1808,6 +1822,7 @@ void WebGPUDriver::blit(Handle<HwTexture> destinationTextureHandle, const uint8_
Handle<HwTexture> sourceTextureHandle, const uint8_t destinationLevel, Handle<HwTexture> sourceTextureHandle, const uint8_t destinationLevel,
const uint8_t destinationLayer, const math::uint2 sourceOrigin, const math::uint2 size) { const uint8_t destinationLayer, const math::uint2 sourceOrigin, const math::uint2 size) {
FWGPU_SYSTRACE_SCOPE(); FWGPU_SYSTRACE_SCOPE();
mQueueManager.flush();
wgpu::CommandEncoder commandEncoder = mQueueManager.getCommandEncoder(); wgpu::CommandEncoder commandEncoder = mQueueManager.getCommandEncoder();
const auto sourceTexture{ handleCast<WebGPUTexture>(sourceTextureHandle) }; const auto sourceTexture{ handleCast<WebGPUTexture>(sourceTextureHandle) };
const auto destinationTexture{ handleCast<WebGPUTexture>(destinationTextureHandle) }; const auto destinationTexture{ handleCast<WebGPUTexture>(destinationTextureHandle) };
@@ -1831,6 +1846,7 @@ void WebGPUDriver::blit(Handle<HwTexture> destinationTextureHandle, const uint8_
.filter = SamplerMagFilter::NEAREST, .filter = SamplerMagFilter::NEAREST,
}; };
mBlitter.blit(mDevice.GetQueue(), commandEncoder, blitArgs); mBlitter.blit(mDevice.GetQueue(), commandEncoder, blitArgs);
mQueueManager.flush();
} }
void WebGPUDriver::bindPipeline(PipelineState const& pipelineState) { void WebGPUDriver::bindPipeline(PipelineState const& pipelineState) {
@@ -50,9 +50,6 @@ public:
[[nodiscard]] bool isHeadless() const { return mType == SwapChainType::HEADLESS; } [[nodiscard]] bool isHeadless() const { return mType == SwapChainType::HEADLESS; }
[[nodiscard]] uint32_t getWidth() const { return mConfig.width; }
[[nodiscard]] uint32_t getHeight() const { return mConfig.height; }
void present(DriverBase& driver); void present(DriverBase& driver);
void setFrameScheduledCallback(CallbackHandler* handler, FrameScheduledCallback&& callback) { void setFrameScheduledCallback(CallbackHandler* handler, FrameScheduledCallback&& callback) {
@@ -41,10 +41,5 @@ void AsyncTaskCounter::waitForAllToFinish() {
mFinishedCondition.wait(lock, [this] { return mTasksInProgress == 0; }); mFinishedCondition.wait(lock, [this] { return mTasksInProgress == 0; });
} }
bool AsyncTaskCounter::isIdle() {
std::lock_guard<std::mutex> lock{ mMutex };
return mTasksInProgress == 0;
}
} // namespace filament::backend::webgpuutils } // namespace filament::backend::webgpuutils
@@ -51,11 +51,6 @@ public:
*/ */
void waitForAllToFinish(); void waitForAllToFinish();
/**
* Check if all tasks have finished (work counter is 0) (thread-safe)
*/
bool isIdle();
private: private:
std::mutex mMutex; std::mutex mMutex;
std::condition_variable mFinishedCondition; std::condition_variable mFinishedCondition;
+3 -4
View File
@@ -35,10 +35,9 @@ test::NativeView getNativeView() {
namespace { namespace {
std::array<test::Backend, 3> const VALID_BACKENDS{ std::array<test::Backend, 2> const VALID_BACKENDS{
test::Backend::OPENGL, test::Backend::OPENGL,
test::Backend::VULKAN, test::Backend::VULKAN,
test::Backend::WEBGPU
}; };
}// namespace }// namespace
@@ -18,7 +18,6 @@
#include "Shader.h" #include "Shader.h"
#include "SharedShaders.h" #include "SharedShaders.h"
#include "Skip.h"
#include "TrianglePrimitive.h" #include "TrianglePrimitive.h"
#include <backend/PixelBufferDescriptor.h> #include <backend/PixelBufferDescriptor.h>
@@ -32,9 +31,6 @@ using namespace filament::backend;
using namespace filament::math; using namespace filament::math;
TEST_F(BackendTest, AutoresolveDifferingSampleCounts) { 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(); auto& api = getDriverApi();
constexpr int kRenderTargetSize = 512; constexpr int kRenderTargetSize = 512;
-50
View File
@@ -1,50 +0,0 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <backend/Platform.h>
#include <gtest/gtest.h>
#include <private/backend/PlatformFactory.h>
namespace filament::backend {
TEST(PlatformTest, GetDeviceInfo) {
Backend backend = Backend::DEFAULT;
Platform* platform = PlatformFactory::create(&backend);
ASSERT_NE(platform, nullptr);
// Test valid queries for the current platform (will be either GL, Vulkan, or Metal depending on
// host)
if (backend == Backend::OPENGL) {
platform->getDeviceInfo(Platform::DeviceInfoType::OPENGL_RENDERER, nullptr);
platform->getDeviceInfo(Platform::DeviceInfoType::OPENGL_VENDOR, nullptr);
// Death tests for Vulkan info on OpenGL platform
EXPECT_DEATH(platform->getDeviceInfo(Platform::DeviceInfoType::VULKAN_DEVICE_NAME, nullptr),
"Unsupported DeviceInfoType");
} else if (backend == Backend::VULKAN) {
platform->getDeviceInfo(Platform::DeviceInfoType::VULKAN_DEVICE_NAME, nullptr);
platform->getDeviceInfo(Platform::DeviceInfoType::VULKAN_DRIVER_NAME, nullptr);
platform->getDeviceInfo(Platform::DeviceInfoType::VULKAN_DRIVER_INFO, nullptr);
// Death tests for OpenGL info on Vulkan platform
EXPECT_DEATH(platform->getDeviceInfo(Platform::DeviceInfoType::OPENGL_RENDERER, nullptr),
"Unsupported DeviceInfoType");
}
PlatformFactory::destroy(&platform);
}
} // namespace filament::backend
+1 -1
View File
@@ -372,7 +372,7 @@ public:
} vsm; } 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). * enabled. (2cm by default).
*/ */
float shadowBulbRadius = 0.02f; float shadowBulbRadius = 0.02f;
+1 -1
View File
@@ -735,7 +735,7 @@ struct VsmShadowOptions {
bool highPrecision = false; bool highPrecision = false;
/** /**
* @deprecated has no effect. * VSM minimum variance scale, must be positive.
*/ */
float minVarianceScale = 0.5f; float minVarianceScale = 0.5f;
+1
View File
@@ -227,6 +227,7 @@ bool Froxelizer::prepare(
const mat4f& projection, float const projectionNear, float const projectionFar, const mat4f& projection, float const projectionNear, float const projectionFar,
float4 const& clipTransform) noexcept { float4 const& clipTransform) noexcept {
assert_invariant(projectionFar > projectionNear); assert_invariant(projectionFar > projectionNear);
assert_invariant(projectionNear > 0);
setViewport(viewport); setViewport(viewport);
setProjection(projection, projectionNear, projectionFar); setProjection(projection, projectionNear, projectionFar);
-265
View File
@@ -1,265 +0,0 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "LocalProgramCache.h"
#include "MaterialParser.h"
#include "details/Engine.h"
#include "details/Material.h"
#include <backend/DriverApiForward.h>
namespace filament {
using namespace backend;
using namespace utils;
LocalProgramCache::LocalProgramCache(LocalProgramCache const& other)
: mMaterial(other.mMaterial),
mCachedPrograms(other.mCachedPrograms.size()),
mSpecializationConstants((other.mMaterial != nullptr)
? other.mMaterial->getEngine()
.getMaterialCache()
.getSpecializationConstantsInternPool()
.acquire(other.mSpecializationConstants)
: SpecializationConstants()) {}
LocalProgramCache& LocalProgramCache::operator=(LocalProgramCache const& other) {
assert_invariant(mMaterial == nullptr);
assert_invariant(mCachedPrograms.empty());
assert_invariant(mSpecializationConstants.empty());
mMaterial = other.mMaterial;
if (mMaterial != nullptr) {
mCachedPrograms = FixedCapacityVector<Handle<HwProgram>>(other.mCachedPrograms.size());
mSpecializationConstants = other.mMaterial->getEngine()
.getMaterialCache()
.getSpecializationConstantsInternPool()
.acquire(other.mSpecializationConstants);
}
return *this;
}
void LocalProgramCache::initializeForMaterial(FEngine& engine, FMaterial const& material,
utils::FixedCapacityVector<backend::Program::SpecializationConstant>
specializationConstants) {
assert_invariant(mMaterial == nullptr);
assert_invariant(mCachedPrograms.empty());
assert_invariant(mSpecializationConstants.empty());
mMaterial = &material;
mSpecializationConstants =
engine.getMaterialCache().getSpecializationConstantsInternPool().acquire(
std::move(specializationConstants));
size_t cachedProgramsSize;
switch (material.getMaterialDomain()) {
case filament::MaterialDomain::SURFACE:
cachedProgramsSize = 1 << VARIANT_BITS;
break;
case filament::MaterialDomain::POST_PROCESS:
cachedProgramsSize = 1 << POST_PROCESS_VARIANT_BITS;
break;
case filament::MaterialDomain::COMPUTE:
cachedProgramsSize = 1;
break;
}
mCachedPrograms = FixedCapacityVector<Handle<HwProgram>>(cachedProgramsSize);
material.getDefinition().acquirePrograms(engine, mCachedPrograms.as_slice(),
material.getMaterialParser(), mSpecializationConstants, material.isDefaultMaterial());
}
void LocalProgramCache::initializeForMaterialInstance(FEngine& engine, FMaterial const& material) {
assert_invariant(mMaterial == nullptr);
assert_invariant(mCachedPrograms.empty());
assert_invariant(mSpecializationConstants.empty());
mMaterial = &material;
LocalProgramCache const& programs = material.getPrograms();
mSpecializationConstants =
engine.getMaterialCache().getSpecializationConstantsInternPool().acquire(
programs.getSpecializationConstants());
mCachedPrograms =
FixedCapacityVector<Handle<HwProgram>>(material.getPrograms().mCachedPrograms.size());
}
Handle<HwProgram> LocalProgramCache::prepareProgramSlow(DriverApi& driver, Variant const variant,
CompilerPriorityQueue const priorityQueue) const noexcept {
assert_invariant(mMaterial != nullptr);
FEngine& engine = mMaterial->getEngine();
if (mMaterial->isSharedVariant(variant)) {
FMaterial const* defaultMaterial = engine.getDefaultMaterial();
assert_invariant(defaultMaterial);
LocalProgramCache const& defaultPrograms = defaultMaterial->getPrograms();
Handle<HwProgram> program = defaultPrograms.mCachedPrograms[variant.key];
if (program) {
return mCachedPrograms[variant.key] = program;
}
return mCachedPrograms[variant.key] =
defaultPrograms.prepareProgram(driver, variant, priorityQueue);
}
return mCachedPrograms[variant.key] = mMaterial->getDefinition().prepareProgram(engine, driver,
mMaterial->getMaterialParser(), getProgramSpecialization(variant), priorityQueue);
}
ProgramSpecialization LocalProgramCache::getProgramSpecialization(Variant variant) const noexcept {
assert_invariant(mMaterial != nullptr);
return ProgramSpecialization {
.materialCrc32 = mMaterial->getMaterialParser().getCrc32(),
.variant = variant,
.specializationConstants = mSpecializationConstants,
};
}
void LocalProgramCache::terminate(FEngine& engine) {
assert_invariant(mMaterial != nullptr);
mMaterial->getDefinition().releasePrograms(engine, mCachedPrograms.as_slice(),
mMaterial->getMaterialParser(), mSpecializationConstants,
mMaterial->isDefaultMaterial());
engine.getMaterialCache().releaseMaterial(engine, mMaterial->getDefinition());
engine.getMaterialCache().getSpecializationConstantsInternPool().release(
mSpecializationConstants);
}
void LocalProgramCache::clear(FEngine& engine) {
assert_invariant(mMaterial != nullptr);
mMaterial->getDefinition().releasePrograms(engine, mCachedPrograms.as_slice(),
mMaterial->getMaterialParser(), mSpecializationConstants,
mMaterial->isDefaultMaterial());
}
Variant LocalProgramCache::filterVariantForGetProgram(Variant variant) const noexcept {
if (UTILS_UNLIKELY(mMaterial->getEngine().features.material.enable_fog_as_postprocess)) {
// if the fog as post-process feature is enabled, we need to proceed "as-if" the material
// didn't have the FOG variant bit.
if (mMaterial->getMaterialDomain() == MaterialDomain::SURFACE) {
BlendingMode const blendingMode = mMaterial->getBlendingMode();
bool const hasScreenSpaceRefraction =
mMaterial->getRefractionMode() == RefractionMode::SCREEN_SPACE;
bool const isBlendingCommand = !hasScreenSpaceRefraction &&
(blendingMode != BlendingMode::OPAQUE && blendingMode != BlendingMode::MASKED);
if (!isBlendingCommand) {
variant.setFog(false);
}
}
}
return variant;
}
Program::SpecializationConstant LocalProgramCache::getConstantImpl(uint32_t id) const noexcept {
return mSpecializationConstants[id];
}
Program::SpecializationConstant LocalProgramCache::getConstantImpl(
std::string_view name) const noexcept {
assert_invariant(mMaterial != nullptr);
MaterialDefinition const& definition = mMaterial->getDefinition();
auto it = definition.specializationConstantsNameToIndex.find(name);
if (it != definition.specializationConstantsNameToIndex.cend()) {
return getConstantImpl(it->second + CONFIG_MAX_RESERVED_SPEC_CONSTANTS);
}
std::string name_cstring(name);
PANIC_PRECONDITION("No such constant exists: %s", name_cstring.c_str());
return {};
}
void LocalProgramCache::setConstants(
std::initializer_list<std::pair<uint32_t, Program::SpecializationConstant>>
constants) noexcept {
assert_invariant(mMaterial != nullptr);
auto newSpecializationConstants =
FixedCapacityVector<Program::SpecializationConstant>(mSpecializationConstants);
bool hasChanged = false;
for (const auto& [id, value] : constants) {
if (newSpecializationConstants[id] != value) {
newSpecializationConstants[id] = value;
hasChanged = true;
}
}
if (hasChanged) {
setConstantsImpl(std::move(newSpecializationConstants));
}
}
void LocalProgramCache::setConstants(
std::initializer_list<std::pair<std::string_view, Program::SpecializationConstant>>
constants) noexcept {
assert_invariant(mMaterial != nullptr);
auto newSpecializationConstants =
FixedCapacityVector<Program::SpecializationConstant>(mSpecializationConstants);
bool hasChanged = false;
for (const auto& [name, value] : constants) {
MaterialDefinition const& definition = mMaterial->getDefinition();
auto it = definition.specializationConstantsNameToIndex.find(name);
if (it != definition.specializationConstantsNameToIndex.cend()) {
uint32_t id = it->second + CONFIG_MAX_RESERVED_SPEC_CONSTANTS;
if (newSpecializationConstants[id] != value) {
newSpecializationConstants[id] = value;
hasChanged = true;
}
}
}
if (hasChanged) {
setConstantsImpl(std::move(newSpecializationConstants));
}
}
void LocalProgramCache::setConstantsImpl(
FixedCapacityVector<Program::SpecializationConstant> constants) noexcept {
FEngine& engine = mMaterial->getEngine();
auto& internPool = engine.getMaterialCache().getSpecializationConstantsInternPool();
MaterialParser const& materialParser = mMaterial->getMaterialParser();
MaterialDefinition const& definition = mMaterial->getDefinition();
const bool isDefaultMaterial = mMaterial->isDefaultMaterial();
// Release old resources...
definition.releasePrograms(engine, mCachedPrograms.as_slice(), materialParser,
mSpecializationConstants, isDefaultMaterial);
internPool.release(mSpecializationConstants);
// Then acquire new ones.
mSpecializationConstants = internPool.acquire(std::move(constants));
definition.acquirePrograms(engine, mCachedPrograms.as_slice(), materialParser,
mSpecializationConstants, isDefaultMaterial);
}
template int32_t LocalProgramCache::getConstant<int32_t>(uint32_t id) const noexcept;
template float LocalProgramCache::getConstant<float>(uint32_t id) const noexcept;
template bool LocalProgramCache::getConstant<bool>(uint32_t id) const noexcept;
template int32_t LocalProgramCache::getConstant<int32_t>(std::string_view name) const noexcept;
template float LocalProgramCache::getConstant<float>(std::string_view name) const noexcept;
template bool LocalProgramCache::getConstant<bool>(std::string_view name) const noexcept;
} // namespace filament

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