From 485ae1b3244ac0836e1b3ac7f887430163838158 Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Mon, 13 Nov 2023 10:43:52 -0800 Subject: [PATCH] add Material.compile and Engine.flush java bindings (#7348) * Add Material.compile() Java binding. * Add Engine.flush() java binding * Add Scene.forEach java binding update the Android gltf-viewer sample to precompile all variants of all materials in the scene, similarly to the desktop sample. --- .../filament-android/src/main/cpp/Engine.cpp | 7 ++ .../src/main/cpp/Material.cpp | 15 ++++ .../filament-android/src/main/cpp/Scene.cpp | 19 +++++ .../com/google/android/filament/Engine.java | 13 ++++ .../com/google/android/filament/Material.java | 76 +++++++++++++++++++ .../com/google/android/filament/Scene.java | 48 ++++++++++++ .../android/filament/gltf/MainActivity.kt | 31 ++++++++ .../filament/hellotriangle/MainActivity.kt | 10 +++ 8 files changed, 219 insertions(+) diff --git a/android/filament-android/src/main/cpp/Engine.cpp b/android/filament-android/src/main/cpp/Engine.cpp index 72b499e618..b5fd507aa1 100644 --- a/android/filament-android/src/main/cpp/Engine.cpp +++ b/android/filament-android/src/main/cpp/Engine.cpp @@ -384,6 +384,13 @@ Java_com_google_android_filament_Engine_nFlushAndWait(JNIEnv*, jclass, engine->flushAndWait(); } +extern "C" JNIEXPORT void JNICALL +Java_com_google_android_filament_Engine_nFlush(JNIEnv*, jclass, + jlong nativeEngine) { + Engine* engine = (Engine*) nativeEngine; + engine->flush(); +} + // Managers... extern "C" JNIEXPORT jlong JNICALL diff --git a/android/filament-android/src/main/cpp/Material.cpp b/android/filament-android/src/main/cpp/Material.cpp index b6865cfaaf..7ef3b0b5be 100644 --- a/android/filament-android/src/main/cpp/Material.cpp +++ b/android/filament-android/src/main/cpp/Material.cpp @@ -19,6 +19,7 @@ #include #include "common/NioUtils.h" +#include "common/CallbackUtils.h" using namespace filament; @@ -271,3 +272,17 @@ Java_com_google_android_filament_Material_nHasParameter(JNIEnv* env, jclass, env->ReleaseStringUTFChars(name_, name); return (jboolean) hasParameter; } + +extern "C" +JNIEXPORT void JNICALL +Java_com_google_android_filament_Material_nCompile(JNIEnv *env, jclass clazz, + jlong nativeMaterial, jint priority, jint variants, jobject handler, jobject runnable) { + Material* material = (Material*) nativeMaterial; + JniCallback* jniCallback = JniCallback::make(env, handler, runnable); + material->compile( + (Material::CompilerPriorityQueue) priority, + (UserVariantFilterBit) variants, + jniCallback->getHandler(), [jniCallback](Material*){ + JniCallback::postToJavaAndDestroy(jniCallback); + }); +} diff --git a/android/filament-android/src/main/cpp/Scene.cpp b/android/filament-android/src/main/cpp/Scene.cpp index b1d3d00348..14b0b49a94 100644 --- a/android/filament-android/src/main/cpp/Scene.cpp +++ b/android/filament-android/src/main/cpp/Scene.cpp @@ -98,3 +98,22 @@ Java_com_google_android_filament_Scene_nHasEntity(JNIEnv *env, jclass type, jlon Entity entity = Entity::import(entityId); return (jboolean) scene->hasEntity(entity); } + +extern "C" +JNIEXPORT jboolean JNICALL +Java_com_google_android_filament_Scene_nGetEntities(JNIEnv *env, jclass , + jlong nativeScene, jintArray outArray, jint length) { + Scene const* const scene = (Scene*) nativeScene; + if (length < scene->getEntityCount()) { + // should not happen because we already checked on the java side + return JNI_FALSE; + } + jint *out = (jint *) env->GetIntArrayElements(outArray, nullptr); + scene->forEach([out, length, i = 0](Entity entity)mutable { + if (i < length) { // this is just paranoia here + out[i++] = (jint) entity.getId(); + } + }); + env->ReleaseIntArrayElements(outArray, (jint*) out, JNI_ABORT); + return JNI_TRUE; +} diff --git a/android/filament-android/src/main/java/com/google/android/filament/Engine.java b/android/filament-android/src/main/java/com/google/android/filament/Engine.java index 82fc77fc3d..26fd9e81c3 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/Engine.java +++ b/android/filament-android/src/main/java/com/google/android/filament/Engine.java @@ -1079,6 +1079,18 @@ public class Engine { nFlushAndWait(getNativeObject()); } + /** + * Kicks the hardware thread (e.g. the OpenGL, Vulkan or Metal thread) but does not wait + * for commands to be either executed or the hardware finished. + * + *

This is typically used after creating a lot of objects to start draining the command + * queue which has a limited size.

+ */ + public void flush() { + nFlush(getNativeObject()); + } + + @UsedByReflection("TextureHelper.java") public long getNativeObject() { if (mNativeObject == 0) { @@ -1151,6 +1163,7 @@ public class Engine { private static native boolean nIsValidSwapChain(long nativeEngine, long nativeSwapChain); private static native void nDestroyEntity(long nativeEngine, int entity); private static native void nFlushAndWait(long nativeEngine); + private static native void nFlush(long nativeEngine); private static native long nGetTransformManager(long nativeEngine); private static native long nGetLightManager(long nativeEngine); private static native long nGetRenderableManager(long nativeEngine); diff --git a/android/filament-android/src/main/java/com/google/android/filament/Material.java b/android/filament-android/src/main/java/com/google/android/filament/Material.java index 8f0d68a55c..80b143ee80 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/Material.java +++ b/android/filament-android/src/main/java/com/google/android/filament/Material.java @@ -18,6 +18,7 @@ package com.google.android.filament; import androidx.annotation.IntRange; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.Size; import com.google.android.filament.proguard.UsedByNative; @@ -238,6 +239,31 @@ public class Material { FRONT_AND_BACK } + public enum CompilerPriorityQueue { + HIGH, + LOW + } + + public static class UserVariantFilterBit { + /** Directional lighting */ + public static int DIRECTIONAL_LIGHTING = 0x01; + /** Dynamic lighting */ + public static int DYNAMIC_LIGHTING = 0x02; + /** Shadow receiver */ + public static int SHADOW_RECEIVER = 0x04; + /** Skinning */ + public static int SKINNING = 0x08; + /** Fog */ + public static int FOG = 0x10; + /** Variance shadow maps */ + public static int VSM = 0x20; + /** Screen-space reflections */ + public static int SSR = 0x40; + /** Instanced stereo rendering */ + public static int STE = 0x80; + public static int ALL = 0xFF; + } + @UsedByNative("Material.cpp") public static class Parameter { private static final Type[] sTypeValues = Type.values(); @@ -352,6 +378,55 @@ public class Material { } } + + /** + * Asynchronously ensures that a subset of this Material's variants are compiled. After issuing + * several compile() calls in a row, it is recommended to call {@link Engine#flush} + * such that the backend can start the compilation work as soon as possible. + * The provided callback is guaranteed to be called on the main thread after all specified + * variants of the material are compiled. This can take hundreds of milliseconds. + *

+ * If all the material's variants are already compiled, the callback will be scheduled as + * soon as possible, but this might take a few dozen millisecond, corresponding to how + * many previous frames are enqueued in the backend. This also varies by backend. Therefore, + * it is recommended to only call this method once per material shortly after creation. + *

+ *

+ * If the same variant is scheduled for compilation multiple times, the first scheduling + * takes precedence; later scheduling are ignored. + *

+ *

+ * caveat: A consequence is that if a variant is scheduled on the low priority queue and later + * scheduled again on the high priority queue, the later scheduling is ignored. + * Therefore, the second callback could be called before the variant is compiled. + * However, the first callback, if specified, will trigger as expected. + *

+ *

+ * The callback is guaranteed to be called. If the engine is destroyed while some material + * variants are still compiling or in the queue, these will be discarded and the corresponding + * callback will be called. In that case however the Material pointer passed to the callback + * is guaranteed to be invalid (either because it's been destroyed by the user already, or, + * because it's been cleaned-up by the Engine). + *

+ *

+ * {@link UserVariantFilterBit#ALL} should be used with caution. Only variants that an application + * needs should be included in the variants argument. For example, the STE variant is only used + * for stereoscopic rendering. If an application is not planning to render in stereo, this bit + * should be turned off to avoid unnecessary material compilations. + *

+ * @param priority Which priority queue to use, LOW or HIGH. + * @param variants Variants to include to the compile command. + * @param handler An {@link java.util.concurrent.Executor Executor}. On Android this can also be a {@link android.os.Handler Handler}. + * @param callback callback called on the main thread when the compilation is done on + * by backend. + */ + public void compile(@NonNull CompilerPriorityQueue priority, + int variants, + @Nullable Object handler, + @Nullable Runnable callback) { + nCompile(getNativeObject(), priority.ordinal(), variants, handler, callback); + } + /** * Creates a new instance of this material. Material instances should be freed using * {@link Engine#destroyMaterialInstance(MaterialInstance)}. @@ -953,6 +1028,7 @@ public class Material { private static native long nCreateInstanceWithName(long nativeMaterial, @NonNull String name); private static native long nGetDefaultInstance(long nativeMaterial); + private static native void nCompile(long nativeMaterial, int priority, int variants, Object handler, Runnable runnable); private static native String nGetName(long nativeMaterial); private static native int nGetShading(long nativeMaterial); private static native int nGetInterpolation(long nativeMaterial); diff --git a/android/filament-android/src/main/java/com/google/android/filament/Scene.java b/android/filament-android/src/main/java/com/google/android/filament/Scene.java index fd425fbeaa..9a41a4f64e 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/Scene.java +++ b/android/filament-android/src/main/java/com/google/android/filament/Scene.java @@ -16,6 +16,7 @@ package com.google.android.filament; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; /** @@ -190,6 +191,52 @@ public class Scene { return mNativeObject; } + /** + * Returns the list of all entities in the Scene. If outArray is provided and large enough, + * it is used to store the list and returned, otherwise a new array is allocated and returned. + * @param outArray an array to store the list of entities in the scene. + * @return outArray if it was used or a newly allocated array. + * @see #getEntityCount + */ + public int[] getEntities(@Nullable int[] outArray) { + int c = getEntityCount(); + if (outArray == null || outArray.length < c) { + outArray = new int[c]; + } + boolean success = nGetEntities(getNativeObject(), outArray, outArray.length); + if (!success) { + throw new IllegalStateException("Error retriving Scene's entities"); + } + return outArray; + } + + /** + * Returns the list of all entities in the Scene in a newly allocated array. + * @return an array containing the list of all entities in the scene. + * @see #getEntityCount + */ + public int[] getEntities() { + return getEntities(null); + } + + public interface EntityProcessor { + void process(@Entity int entity); + } + + /** + * Invokes user functor on each entity in the scene. + * + * It is not allowed to add or remove an entity from the scene within the functor. + * + * @param entityProcessor User provided functor called for each entity in the scene + */ + public void forEach(@NonNull EntityProcessor entityProcessor) { + int[] entities = getEntities(null); + for (int entity : entities) { + entityProcessor.process(entity); + } + } + void clearNativeObject() { mNativeObject = 0; } @@ -204,4 +251,5 @@ public class Scene { private static native int nGetRenderableCount(long nativeScene); private static native int nGetLightCount(long nativeScene); private static native boolean nHasEntity(long nativeScene, int entity); + private static native boolean nGetEntities(long nativeScene, int[] outArray, int length); } diff --git a/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt b/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt index 372cba5700..102c4944e6 100644 --- a/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt +++ b/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt @@ -26,6 +26,7 @@ import android.widget.TextView import android.widget.Toast import com.google.android.filament.Fence import com.google.android.filament.IndirectLight +import com.google.android.filament.Material import com.google.android.filament.Skybox import com.google.android.filament.View import com.google.android.filament.View.OnPickCallback @@ -392,6 +393,36 @@ class MainActivity : Activity() { Log.i(TAG, "The Filament backend took $total ms to load the model geometry.") modelViewer.engine.destroyFence(it) loadStartFence = null + + val materials = mutableSetOf() + val rcm = modelViewer.engine.renderableManager + modelViewer.scene.forEach { + val entity = it + if (rcm.hasComponent(entity)) { + val ri = rcm.getInstance(entity) + val c = rcm.getPrimitiveCount(ri) + for (i in 0 until c) { + val mi = rcm.getMaterialInstanceAt(ri, i) + val ma = mi.material + materials.add(ma) + } + } + } + materials.forEach { + it.compile( + Material.CompilerPriorityQueue.HIGH, + Material.UserVariantFilterBit.DIRECTIONAL_LIGHTING or + Material.UserVariantFilterBit.DYNAMIC_LIGHTING or + Material.UserVariantFilterBit.SHADOW_RECEIVER, + null, null) + it.compile( + Material.CompilerPriorityQueue.LOW, + Material.UserVariantFilterBit.FOG or + Material.UserVariantFilterBit.SKINNING or + Material.UserVariantFilterBit.SSR or + Material.UserVariantFilterBit.VSM, + null, null) + } } } diff --git a/android/samples/sample-hello-triangle/src/main/java/com/google/android/filament/hellotriangle/MainActivity.kt b/android/samples/sample-hello-triangle/src/main/java/com/google/android/filament/hellotriangle/MainActivity.kt index 8571ee4d60..f4b1e86817 100644 --- a/android/samples/sample-hello-triangle/src/main/java/com/google/android/filament/hellotriangle/MainActivity.kt +++ b/android/samples/sample-hello-triangle/src/main/java/com/google/android/filament/hellotriangle/MainActivity.kt @@ -20,6 +20,8 @@ import android.animation.ValueAnimator import android.app.Activity import android.opengl.Matrix import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.Choreographer import android.view.Surface import android.view.SurfaceView @@ -157,6 +159,14 @@ class MainActivity : Activity() { private fun loadMaterial() { readUncompressedAsset("materials/baked_color.filamat").let { material = Material.Builder().payload(it, it.remaining()).build(engine) + material.compile( + Material.CompilerPriorityQueue.HIGH, + Material.UserVariantFilterBit.ALL, + Handler(Looper.getMainLooper())) { + android.util.Log.i("hellotriangle", + "Material " + material.name + " compiled.") + } + engine.flush() } }