Compare commits

..
Author SHA1 Message Date
Powei Feng f0a3b10681 docs: document 2D coordinate spaces
And remove the pngs that documented the same thing.
2026-04-13 11:35:17 -07:00
1554 changed files with 43010 additions and 135164 deletions
+1 -8
View File
@@ -150,20 +150,13 @@ jobs:
pushd . pushd .
cd build/android && printf "y" | ./build.sh presubmit-with-archive arm64-v8a cd build/android && printf "y" | ./build.sh presubmit-with-archive arm64-v8a
popd popd
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- name: Check artifact sizes - name: Check artifact sizes
run: | run: |
python3 test/sizeguard/dump_artifact_size.py out/*.aar > current_size.json python3 test/sizeguard/dump_artifact_size.py out/*.aar > current_size.json
BYPASS_ARG=""
if python3 test/sizeguard/check_bypass.py ${{ steps.get_commit_msg.outputs.hash }}; then
BYPASS_ARG="--bypass"
fi
python3 test/sizeguard/check_size.py current_size.json \ python3 test/sizeguard/check_size.py current_size.json \
--target-branch origin/main \ --target-branch origin/main \
--threshold 20480 \ --threshold 20480 \
--artifacts filament-android-release.aar/jni/arm64-v8a/libfilament-jni.so \ --artifacts filament-android-release.aar/jni/arm64-v8a/libfilament-jni.so
$BYPASS_ARG
build-ios: build-ios:
name: build-iOS name: build-iOS
+1 -1
View File
@@ -5,7 +5,7 @@
To build Filament, you must first install the following tools: To build Filament, you must first install the following tools:
- CMake 3.22.1 (or more recent) - CMake 3.22.1 (or more recent)
- clang 16.0 (or more recent) (Required for Linux and macOS; see [Windows](#windows) section for MSVC support) - clang 16.0 (or more recent)
- [ninja 1.10](https://github.com/ninja-build/ninja/wiki/Pre-built-Ninja-packages) (or more recent) - [ninja 1.10](https://github.com/ninja-build/ninja/wiki/Pre-built-Ninja-packages) (or more recent)
Additional dependencies may be required for your operating system. Please refer to the appropriate Additional dependencies may be required for your operating system. Please refer to the appropriate
+3 -1
View File
@@ -13,7 +13,7 @@ cmake_minimum_required(VERSION 3.22.1)
# On iOS, the deployment target is set inside third_party/clang/ios.cmake # On iOS, the deployment target is set inside third_party/clang/ios.cmake
if (APPLE AND NOT IOS) if (APPLE AND NOT IOS)
# This must be set before project() is called # This must be set before project() is called
set(CMAKE_OSX_DEPLOYMENT_TARGET 11.0 CACHE STRING "") set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15 CACHE STRING "")
endif() endif()
# ================================================================================================== # ==================================================================================================
@@ -47,6 +47,8 @@ option(FILAMENT_ENABLE_TSAN "Enable Thread Sanitizer" OFF)
option(FILAMENT_ENABLE_COVERAGE "Enable LLVM code coverage" OFF) option(FILAMENT_ENABLE_COVERAGE "Enable LLVM code coverage" OFF)
option(FILAMENT_ENABLE_FEATURE_LEVEL_0 "Enable Feature Level 0" ON)
option(FILAMENT_ENABLE_MULTIVIEW "Enable multiview for Filament" OFF) option(FILAMENT_ENABLE_MULTIVIEW "Enable multiview for Filament" OFF)
option(FILAMENT_SUPPORTS_OSMESA "Enable OSMesa (headless GL context) for Filament" OFF) option(FILAMENT_SUPPORTS_OSMESA "Enable OSMesa (headless GL context) for Filament" OFF)
+1 -1
View File
@@ -7,4 +7,4 @@ appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md).
## Release notes for next branch cut ## Release notes for next branch cut
- engine: add `MaterialInstance::setConstant()` and `MaterialInstance::getConstant()` methods. These allow for per-material instance specialization constant overrides. - matc: better material compression using a Multi-Base Variable-Length Interleaved Token Stream [⚠️ **New Material Version**]
+2 -2
View File
@@ -31,7 +31,7 @@ repositories {
} }
dependencies { dependencies {
implementation 'com.google.android.filament:filament-android:1.71.2' implementation 'com.google.android.filament:filament-android:1.70.1'
} }
``` ```
@@ -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.71.2' pod 'Filament', '~> 1.70.1'
``` ```
## Documentation ## Documentation
-10
View File
@@ -7,16 +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.71.2
## v1.71.1
## v1.71.0
- matc: better material compression using a Multi-Base Variable-Length Interleaved Token Stream [⚠️ **New Material Version**]
## v1.70.2 ## v1.70.2
@@ -220,23 +220,3 @@ Java_com_google_android_filament_ColorGrading_nBuilderCurves(JNIEnv* env, jclass
env->ReleaseFloatArrayElements(midPoint_, midPoint, JNI_ABORT); env->ReleaseFloatArrayElements(midPoint_, midPoint, JNI_ABORT);
env->ReleaseFloatArrayElements(scale_, scale, JNI_ABORT); env->ReleaseFloatArrayElements(scale_, scale, JNI_ABORT);
} }
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_ColorGrading_nBuilderCustomLut(JNIEnv *env, jclass,
jlong nativeBuilder, jobject buffer, jint dimension) {
ColorGrading::Builder* builder = (ColorGrading::Builder*) nativeBuilder;
if (dimension == 0) {
return;
}
float3* data = (float3*) env->GetDirectBufferAddress(buffer);
size_t count = size_t(dimension) * dimension * dimension;
utils::FixedCapacityVector<math::float3> lut =
utils::FixedCapacityVector<math::float3>::with_capacity(count);
for (size_t i = 0; i < count; ++i) {
lut.push_back(data[i]);
}
builder->customLut(std::move(lut), (uint8_t)dimension);
}
@@ -22,12 +22,6 @@
#include <utils/Entity.h> #include <utils/Entity.h>
#include <utils/EntityManager.h> #include <utils/EntityManager.h>
#include <utils/tribool.h>
#include <filament/Material.h>
#include <filament/View.h>
#include "common/CallbackUtils.h"
using namespace filament; using namespace filament;
using namespace utils; using namespace utils;
@@ -435,24 +429,6 @@ Java_com_google_android_filament_Engine_nIsPaused(JNIEnv*, jclass,
return (jboolean)engine->isPaused(); return (jboolean)engine->isPaused();
} }
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_Engine_nCompile(JNIEnv* env, jclass,
jlong nativeEngine, jint priority, jlong nativeMaterial, jlong nativeView,
jint shadowReceiver, jint skinning, jobject handler, jobject runnable) {
Engine* engine = (Engine*) nativeEngine;
Material* material = (Material*) nativeMaterial;
View* view = (View*) nativeView;
JniCallback* jniCallback = JniCallback::make(env, handler, runnable);
engine->compile(
(backend::CompilerPriorityQueue) priority,
material, view,
(utils::tribool::value_t) shadowReceiver,
(utils::tribool::value_t) skinning,
jniCallback->getHandler(), [jniCallback](Material*){
JniCallback::postToJavaAndDestroy(jniCallback);
});
}
extern "C" JNIEXPORT void JNICALL extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_Engine_nSetPaused(JNIEnv*, jclass, Java_com_google_android_filament_Engine_nSetPaused(JNIEnv*, jclass,
jlong nativeEngine, jboolean paused) { jlong nativeEngine, jboolean paused) {
@@ -20,8 +20,6 @@
#include <filament/Texture.h> #include <filament/Texture.h>
#include <filament/TextureSampler.h> #include <filament/TextureSampler.h>
#include "common/CallbackUtils.h"
#include <math/mat3.h> #include <math/mat3.h>
#include <math/mat4.h> #include <math/mat4.h>
#include <math/vec2.h> #include <math/vec2.h>
@@ -248,69 +246,6 @@ Java_com_google_android_filament_MaterialInstance_nSetFloatParameterArray(JNIEnv
env->ReleaseStringUTFChars(name_, name); env->ReleaseStringUTFChars(name_, name);
} }
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_google_android_filament_MaterialInstance_nGetConstantBool(JNIEnv *env, jclass,
jlong nativeMaterialInstance, jstring name_) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
const char *name = env->GetStringUTFChars(name_, 0);
jboolean result = instance->getConstant<bool>(name);
env->ReleaseStringUTFChars(name_, name);
return result;
}
extern "C"
JNIEXPORT jfloat JNICALL
Java_com_google_android_filament_MaterialInstance_nGetConstantFloat(JNIEnv *env, jclass,
jlong nativeMaterialInstance, jstring name_) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
const char *name = env->GetStringUTFChars(name_, 0);
jfloat result = instance->getConstant<float>(name);
env->ReleaseStringUTFChars(name_, name);
return result;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_google_android_filament_MaterialInstance_nGetConstantInt(JNIEnv *env, jclass,
jlong nativeMaterialInstance, jstring name_) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
const char *name = env->GetStringUTFChars(name_, 0);
jint result = instance->getConstant<int32_t>(name);
env->ReleaseStringUTFChars(name_, name);
return result;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_MaterialInstance_nSetConstantBool(JNIEnv *env, jclass,
jlong nativeMaterialInstance, jstring name_, jboolean x) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
const char *name = env->GetStringUTFChars(name_, 0);
instance->setConstant<bool>(name, x);
env->ReleaseStringUTFChars(name_, name);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_MaterialInstance_nSetConstantFloat(JNIEnv *env, jclass,
jlong nativeMaterialInstance, jstring name_, jfloat x) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
const char *name = env->GetStringUTFChars(name_, 0);
instance->setConstant<float>(name, x);
env->ReleaseStringUTFChars(name_, name);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_MaterialInstance_nSetConstantInt(JNIEnv *env, jclass,
jlong nativeMaterialInstance, jstring name_, jint x) {
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
const char *name = env->GetStringUTFChars(name_, 0);
instance->setConstant<int32_t>(name, x);
env->ReleaseStringUTFChars(name_, name);
}
// defined in TextureSampler.cpp // defined in TextureSampler.cpp
namespace filament::JniUtils { namespace filament::JniUtils {
TextureSampler from_long(jlong params) noexcept; TextureSampler from_long(jlong params) noexcept;
@@ -645,17 +580,3 @@ Java_com_google_android_filament_MaterialInstance_nGetTransparencyMode(JNIEnv*,
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance; MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
return (jint) instance->getTransparencyMode(); return (jint) instance->getTransparencyMode();
} }
extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_MaterialInstance_nCompile(JNIEnv *env, jclass clazz,
jlong nativeMaterialInstance, jint priority, jint variants, jobject handler, jobject runnable) {
MaterialInstance* materialInstance = (MaterialInstance*) nativeMaterialInstance;
JniCallback* jniCallback = JniCallback::make(env, handler, runnable);
materialInstance->compile(
(MaterialInstance::CompilerPriorityQueue) priority,
(UserVariantFilterBit) variants,
jniCallback->getHandler(), [jniCallback](MaterialInstance*){
JniCallback::postToJavaAndDestroy(jniCallback);
});
}
@@ -556,22 +556,6 @@ public class ColorGrading {
return this; return this;
} }
/**
* Specifies a custom 3D color grading LUT to map the final sRGB color.
* The LUT is applied after post-processing and in LDR (sRGB space).
* The data must be a 3D array of float3 (RGB) values.
* The data must remain valid until build() is called.
*
* @param buffer Direct ByteBuffer containing the custom LUT data (3D array of float3).
* @param dimension Dimension of the custom LUT (e.g., 16, 32, 64).
*
* @return This Builder, for chaining calls
*/
public Builder customLut(@NonNull java.nio.Buffer buffer, int dimension) {
nBuilderCustomLut(mNativeBuilder, buffer, dimension);
return this;
}
/** /**
* Creates the IndirectLight object and returns a pointer to it. * Creates the IndirectLight object and returns a pointer to it.
* *
@@ -636,7 +620,6 @@ public class ColorGrading {
private static native void nBuilderVibrance(long nativeBuilder, float vibrance); private static native void nBuilderVibrance(long nativeBuilder, float vibrance);
private static native void nBuilderSaturation(long nativeBuilder, float saturation); private static native void nBuilderSaturation(long nativeBuilder, float saturation);
private static native void nBuilderCurves(long nativeBuilder, float[] gamma, float[] midPoint, float[] scale); private static native void nBuilderCurves(long nativeBuilder, float[] gamma, float[] midPoint, float[] scale);
private static native void nBuilderCustomLut(long nativeBuilder, java.nio.Buffer buffer, int dim);
private static native long nBuilderBuild(long nativeBuilder, long nativeEngine); private static native long nBuilderBuild(long nativeBuilder, long nativeEngine);
} }
@@ -205,15 +205,6 @@ public class Engine {
REALTIME, REALTIME,
}; };
/**
* Three-state feature state.
*/
public enum FeatureState {
FALSE,
TRUE,
INDETERMINATE
}
/** /**
* Constructs <code>Engine</code> objects using a builder pattern. * Constructs <code>Engine</code> objects using a builder pattern.
*/ */
@@ -627,35 +618,6 @@ public class Engine {
return mNativeObject != 0; return mNativeObject != 0;
} }
/**
* Asynchronously ensures that the variants of the specified Material required to render it
* in the provided View are compiled. This determines the necessary permutations of
* feature flags based on the supplied View, and compiles the corresponding shader variants.
*
* See {@link Material#compile(Material.CompilerPriorityQueue, int, Object, Runnable)} for
* important details about the compilation process, callback scheduling, priorities, and flushing the engine.
*
* @param priority Which priority queue to use, LOW or HIGH.
* @param material The Material whose variants will be compiled.
* @param view The View in which the material will be rendered.
* @param shadowReceiver Indicates whether to compile variants where the material receives shadows.
* Use FeatureState.INDETERMINATE to compile both permutations.
* @param skinning Indicates whether to compile variants with skinning.
* Use FeatureState.INDETERMINATE to compile both permutations.
* @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
* by backend.
*/
public void compile(@NonNull Material.CompilerPriorityQueue priority,
@NonNull Material material,
@NonNull View view,
@NonNull FeatureState shadowReceiver,
@NonNull FeatureState skinning,
@Nullable Object handler,
@Nullable Runnable callback) {
nCompile(getNativeObject(), priority.ordinal(), material.getNativeObject(), view.getNativeObject(), shadowReceiver.ordinal(), skinning.ordinal(), handler, callback);
}
/** /**
* Destroy the <code>Engine</code> instance and all associated resources. * Destroy the <code>Engine</code> instance and all associated resources.
* <p> * <p>
@@ -1569,7 +1531,6 @@ public class Engine {
private static native void nDestroyEntity(long nativeEngine, int entity); private static native void nDestroyEntity(long nativeEngine, int entity);
private static native boolean nFlushAndWait(long nativeEngine, long timeout); private static native boolean nFlushAndWait(long nativeEngine, long timeout);
private static native void nFlush(long nativeEngine); private static native void nFlush(long nativeEngine);
private static native void nCompile(long nativeEngine, int priority, long nativeMaterial, long nativeView, int shadowReceiver, int skinning, Object handler, Runnable callback);
private static native boolean nIsPaused(long nativeEngine); private static native boolean nIsPaused(long nativeEngine);
private static native void nSetPaused(long nativeEngine, boolean paused); private static native void nSetPaused(long nativeEngine, boolean paused);
private static native void nUnprotected(long nativeEngine); private static native void nUnprotected(long nativeEngine);
@@ -522,19 +522,11 @@ public class Material {
* for stereoscopic rendering. If an application is not planning to render in stereo, this bit * for stereoscopic rendering. If an application is not planning to render in stereo, this bit
* should be turned off to avoid unnecessary material compilations. * should be turned off to avoid unnecessary material compilations.
*</p> *</p>
*<p>
* Note that it is possible to override specialization constants on a per-MaterialInstance basis
* (see {@link MaterialInstance#setConstant}). In that case, the programs compiled by a call to
* Material::compile() may not be reusable by that MaterialInstance. It's better to call
* MaterialInstance::compile() in cases where you intend to override specialization constants.
*</p>
* @param priority Which priority queue to use, LOW or HIGH. * @param priority Which priority queue to use, LOW or HIGH.
* @param variants Variants to include to the compile command. * @param variants Variants to include to the compile command.
* @param handler An {@link java.util.concurrent.Executor Executor}. On Android this can also be a {@link android.os.Handler Handler}. * @param 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 * @param callback callback called on the main thread when the compilation is done on
* by backend. * by backend.
*
* @see MaterialInstance#compile
*/ */
public void compile(@NonNull CompilerPriorityQueue priority, public void compile(@NonNull CompilerPriorityQueue priority,
int variants, int variants,
@@ -18,7 +18,6 @@ package com.google.android.filament;
import androidx.annotation.IntRange; import androidx.annotation.IntRange;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Size; import androidx.annotation.Size;
import com.google.android.filament.proguard.UsedByNative; import com.google.android.filament.proguard.UsedByNative;
@@ -143,28 +142,6 @@ public class MaterialInstance {
return mMaterial; return mMaterial;
} }
/**
* Asynchronously ensures that a subset of this MaterialInstance's variants are compiled.
*
* <p>This function behaves identically to {@link Material#compile}, but takes into account
* the specific constants overridden by {@link #setConstant}.</p>
*
* @param priority Priority of the compile command.
* @param variants Variants to include to the compile command.
* @param handler An {@link java.util.concurrent.Executor Executor}. On Android this can also be a {@link android.os.Handler Handler}.
* @param callback callback called on the main thread when the compilation is done on
* by backend.
*
* @see Material#compile
* @see #setConstant
*/
public void compile(@NonNull Material.CompilerPriorityQueue priority,
int variants,
@Nullable Object handler,
@Nullable Runnable callback) {
nCompile(getNativeObject(), priority.ordinal(), variants, handler, callback);
}
/** @return the name associated with this instance */ /** @return the name associated with this instance */
@NonNull @NonNull
public String getName() { public String getName() {
@@ -425,69 +402,6 @@ public class MaterialInstance {
nSetParameterFloat4(getNativeObject(), name, color[0], color[1], color[2], color[3]); nSetParameterFloat4(getNativeObject(), name, color[0], color[1], color[2], color[3]);
} }
/**
* Overrides a specialization constant of this material instance.
*
* @param name The name of the constant as defined in the material.
* @param value The value of the constant.
* @see Material.Builder#constant
*/
public void setConstant(@NonNull String name, boolean value) {
nSetConstantBool(getNativeObject(), name, value);
}
/**
* Overrides a specialization constant of this material instance.
*
* @param name The name of the constant as defined in the material.
* @param value The value of the constant.
* @see Material.Builder#constant
*/
public void setConstant(@NonNull String name, float value) {
nSetConstantFloat(getNativeObject(), name, value);
}
/**
* Overrides a specialization constant of this material instance.
*
* @param name The name of the constant as defined in the material.
* @param value The value of the constant.
* @see Material.Builder#constant
*/
public void setConstant(@NonNull String name, int value) {
nSetConstantInt(getNativeObject(), name, value);
}
/**
* Gets the value of a specialization constant by name.
*
* @param name The name of the constant as defined in the material.
* @return The value of the constant.
*/
public boolean getConstantBoolean(@NonNull String name) {
return nGetConstantBool(getNativeObject(), name);
}
/**
* Gets the value of a specialization constant by name.
*
* @param name The name of the constant as defined in the material.
* @return The value of the constant.
*/
public float getConstantFloat(@NonNull String name) {
return nGetConstantFloat(getNativeObject(), name);
}
/**
* Gets the value of a specialization constant by name.
*
* @param name The name of the constant as defined in the material.
* @return The value of the constant.
*/
public int getConstantInt(@NonNull String name) {
return nGetConstantInt(getNativeObject(), name);
}
/** /**
* Set-up a custom scissor rectangle; by default it is disabled. * Set-up a custom scissor rectangle; by default it is disabled.
* *
@@ -1023,17 +937,6 @@ public class MaterialInstance {
@NonNull String name, int element, @NonNull @Size(min = 1) float[] v, @NonNull String name, int element, @NonNull @Size(min = 1) float[] v,
@IntRange(from = 0) int offset, @IntRange(from = 1) int count); @IntRange(from = 0) int offset, @IntRange(from = 1) int count);
private static native boolean nGetConstantBool(long nativeMaterialInstance, @NonNull String name);
private static native float nGetConstantFloat(long nativeMaterialInstance, @NonNull String name);
private static native int nGetConstantInt(long nativeMaterialInstance, @NonNull String name);
private static native void nSetConstantBool(long nativeMaterialInstance,
@NonNull String name, boolean x);
private static native void nSetConstantFloat(long nativeMaterialInstance,
@NonNull String name, float x);
private static native void nSetConstantInt(long nativeMaterialInstance,
@NonNull String name, int x);
private static native void nSetParameterTexture(long nativeMaterialInstance, private static native void nSetParameterTexture(long nativeMaterialInstance,
@NonNull String name, long nativeTexture, long sampler); @NonNull String name, long nativeTexture, long sampler);
@@ -1097,5 +1000,4 @@ public class MaterialInstance {
private static native int nGetDepthFunc(long nativeMaterialInstance); private static native int nGetDepthFunc(long nativeMaterialInstance);
private static native void nSetTransparencyMode(long nativeMaterialInstance, int mode); private static native void nSetTransparencyMode(long nativeMaterialInstance, int mode);
private static native int nGetTransparencyMode(long nativeMaterialInstance); private static native int nGetTransparencyMode(long nativeMaterialInstance);
private static native void nCompile(long nativeMaterialInstance, int priority, int variants, Object handler, Runnable callback);
} }
@@ -2115,10 +2115,9 @@ public class View {
public int msaaSamples = 1; public int msaaSamples = 1;
/** /**
* Whether to use a 32-bits or 16-bits texture format for VSM shadow maps. 32-bits * Whether to use a 32-bits or 16-bits texture format for VSM shadow maps. 32-bits
* precision is rarely needed, but it does reduce light leaks as well as "fading" * precision is rarely needed, but it does reduces light leaks as well as "fading"
* of the shadows in some situations. Setting highPrecision to true for a single * of the shadows in some situations. Setting highPrecision to true for a single
* shadow map will double the memory usage of all shadow maps. * shadow map will double the memory usage of all shadow maps.
* This may not be supported on all mobile devices.
*/ */
public boolean highPrecision = false; public boolean highPrecision = false;
/** /**
@@ -30,8 +30,8 @@ using namespace filament;
namespace { namespace {
constexpr std::array<backend::Platform::DeviceInfoType, 3> VULKAN_INFO = { constexpr std::array<backend::Platform::DeviceInfoType, 3> VULKAN_INFO = {
backend::Platform::DeviceInfoType::VULKAN_DRIVER_NAME,
backend::Platform::DeviceInfoType::VULKAN_DEVICE_NAME, backend::Platform::DeviceInfoType::VULKAN_DEVICE_NAME,
backend::Platform::DeviceInfoType::VULKAN_DRIVER_NAME,
backend::Platform::DeviceInfoType::VULKAN_DRIVER_INFO, backend::Platform::DeviceInfoType::VULKAN_DRIVER_INFO,
}; };
@@ -143,7 +143,7 @@ class ModelViewer(
light = EntityManager.get().create() light = EntityManager.get().create()
val (r, g, b) = Colors.cct(6_500.0f) val (r, g, b) = Colors.cct(6_500.0f)
LightManager.Builder(LightManager.Type.SUN) LightManager.Builder(LightManager.Type.DIRECTIONAL)
.color(r, g, b) .color(r, g, b)
.intensity(100_000.0f) .intensity(100_000.0f)
.direction(0.0f, -1.0f, 0.0f) .direction(0.0f, -1.0f, 0.0f)
@@ -316,9 +316,9 @@ class ModelViewer(
* @param frameTimeNanos time in nanoseconds when the frame started being rendered, * @param frameTimeNanos time in nanoseconds when the frame started being rendered,
* typically comes from {@link android.view.Choreographer.FrameCallback} * typically comes from {@link android.view.Choreographer.FrameCallback}
*/ */
fun render(frameTimeNanos: Long): Boolean { fun render(frameTimeNanos: Long) {
if (!uiHelper.isReadyToRender) { if (!uiHelper.isReadyToRender) {
return false return
} }
// Allow the resource loader to finalize textures that have become ready. // Allow the resource loader to finalize textures that have become ready.
@@ -337,9 +337,7 @@ class ModelViewer(
} }
// Render the scene, unless the renderer wants to skip the frame. // Render the scene, unless the renderer wants to skip the frame.
var rendered = false
if (renderer.beginFrame(swapChain!!, frameTimeNanos)) { if (renderer.beginFrame(swapChain!!, frameTimeNanos)) {
rendered = true
renderer.render(view) renderer.render(view)
debugFrameCallback?.let { debugFrameCallback?.let {
@@ -362,7 +360,6 @@ class ModelViewer(
renderer.endFrame() renderer.endFrame()
} }
return rendered
} }
/* /*
@@ -393,45 +390,38 @@ class ModelViewer(
view.addOnAttachStateChangeListener(object : android.view.View.OnAttachStateChangeListener { view.addOnAttachStateChangeListener(object : android.view.View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: android.view.View) {} override fun onViewAttachedToWindow(v: android.view.View) {}
override fun onViewDetachedFromWindow(v: android.view.View) { override fun onViewDetachedFromWindow(v: android.view.View) {
destroy() uiHelper.detach()
destroyModel()
assetLoader.destroy()
materialProvider.destroyMaterials()
materialProvider.destroy()
resourceLoader.destroy()
if (indirectLightCubemap != null) {
engine.destroyTexture(indirectLightCubemap!!)
indirectLightCubemap = null
}
if (skyboxCubemap != null) {
engine.destroyTexture(skyboxCubemap!!)
skyboxCubemap = null
}
engine.destroyEntity(light)
engine.destroyRenderer(renderer)
engine.destroyView(this@ModelViewer.view)
engine.destroyScene(scene)
engine.destroyCameraComponent(camera.entity)
EntityManager.get().destroy(camera.entity)
EntityManager.get().destroy(light)
engine.destroy()
} }
}) })
} }
/**
* Explicitly destroys the ModelViewer and its underlying Filament engine and resources.
*/
fun destroy() {
uiHelper.detach()
destroyModel()
assetLoader.destroy()
materialProvider.destroyMaterials()
materialProvider.destroy()
resourceLoader.destroy()
if (indirectLightCubemap != null) {
engine.destroyTexture(indirectLightCubemap!!)
indirectLightCubemap = null
}
if (skyboxCubemap != null) {
engine.destroyTexture(skyboxCubemap!!)
skyboxCubemap = null
}
engine.destroyEntity(light)
engine.destroyRenderer(renderer)
engine.destroyView(this@ModelViewer.view)
engine.destroyScene(scene)
engine.destroyCameraComponent(camera.entity)
EntityManager.get().destroy(camera.entity)
EntityManager.get().destroy(light)
engine.destroy()
}
/** /**
* 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.
*/ */
+1 -1
View File
@@ -1,5 +1,5 @@
GROUP=com.google.android.filament GROUP=com.google.android.filament
VERSION_NAME=1.71.2 VERSION_NAME=1.70.1
POM_DESCRIPTION=Real-time physically based rendering engine for Android. POM_DESCRIPTION=Real-time physically based rendering engine for Android.
@@ -16,15 +16,14 @@ filament {
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs") iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")
} }
// don't forget to update MainActivity.kt when/if changing this. // don't forget to update MainACtivity.kt when/if changing this.
tasks.register('copyModels', Copy) { tasks.register('copyDamagedHelmetGltf', Copy) {
from file("../../../third_party/models/DamagedHelmet/DamagedHelmet.glb") from file("../../../third_party/models/DamagedHelmet/DamagedHelmet.glb")
from file("../../../third_party/models/Fox/Fox.glb")
from file("../../../third_party/models/TransmissionRoughnessTest/TransmissionRoughnessTest.glb")
into file("src/main/assets/models") into file("src/main/assets/models")
rename {String fileName -> "helmet.glb"}
} }
preBuild.dependsOn copyModels preBuild.dependsOn copyDamagedHelmetGltf
clean.doFirst { clean.doFirst {
delete "src/main/assets/envs" delete "src/main/assets/envs"
@@ -1,13 +1,10 @@
{ {
"name": "Default Test", "name": "Default Test",
"backends": [ "backends": [
"opengl", "opengl"
"vulkan"
], ],
"models": { "models": {
"DamagedHelmet": "DamagedHelmet.glb", "DamagedHelmet": "helmet.glb"
"Fox": "Fox.glb",
"TransmissionRoughnessTest": "TransmissionRoughnessTest.glb"
}, },
"presets": [ "presets": [
{ {
@@ -22,7 +19,7 @@
"center": [ "center": [
0, 0,
0, 0,
-0.8 0
], ],
"lookAt": [ "lookAt": [
0, 0,
@@ -36,24 +33,8 @@
} }
}, },
"tolerance": { "tolerance": {
"mode": "OR", "maxAbsDiff": 0.1,
"maxFailingPixelsFraction": 0.0005, "maxFailingPixelsFraction": 0.0
"children": [
// Scenario: Geometry rasterization snapping differences between different GPU architectures (e.g., Adreno versions).
// Method: Allow a 1-pixel positional shift but demand a very strict color match.
{
"maxAbsDiff": 0.02,
"shiftRadius": 1,
"blurRadius": 0
},
// Scenario: Floating-point math precision differences in shaders across architectures.
// Method: Demand exact pixel position but allow a slightly looser color precision variance.
{
"maxAbsDiff": 0.05,
"shiftRadius": 0,
"blurRadius": 0
}
]
} }
}, },
{ {
@@ -74,43 +55,6 @@
] ]
} }
} }
},
{
"name": "relaxed_tolerance",
// Scenario: Screen Space Reflections (SSR) stochastic raymarching noise.
// Method: Apply a 7x7 low-pass filter (blurRadius: 3) to smooth out high-frequency noise,
// allow slight reflection warping (shiftRadius: 2), and strictly check the smoothed underlying color.
"tolerance": {
"maxAbsDiff": 0.09,
"shiftRadius": 2,
"blurRadius": 4,
"maxFailingPixelsFraction": 0.0005
},
"rendering": {}
},
{
"name": "noisy_tolerance",
"rendering": {},
"tolerance": {
"mode": "OR",
"maxFailingPixelsFraction": 0.0005,
"children": [
// Scenario: Bloom & MSAA edge jitter caused by slightly different sub-pixel sampling patterns.
// Method: Allow a 1-pixel positional shift with a moderate color variance.
{
"maxAbsDiff": 0.05,
"shiftRadius": 1,
"blurRadius": 0
},
// Scenario: High-frequency noise generated by noisy post-processing effects.
// Method: Apply a 5x5 low-pass filter (blurRadius: 2) to smooth the noise, demanding exact position and strict color match.
{
"maxAbsDiff": 0.05,
"shiftRadius": 0,
"blurRadius": 2
}
]
}
} }
], ],
"tests": [ "tests": [
@@ -128,32 +72,64 @@
] ]
}, },
{ {
"name": "msaa_custom", "name": "ssao",
"apply_presets": [ "apply_presets": [
"base", "base"
"noisy_tolerance"
], ],
"rendering": { "rendering": {
"view.msaa.enabled": true, "view.ssao.enabled": true
"view.msaa.customResolve": true
} }
}, },
{ {
"name": "msaa_standard", "name": "msaa",
"apply_presets": [ "apply_presets": [
"base", "base"
"noisy_tolerance"
], ],
"rendering": { "rendering": {
"view.msaa.enabled": true, "view.msaa.enabled": true
"view.msaa.customResolve": false }
},
{
"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", "name": "msaa_8",
"apply_presets": [ "apply_presets": [
"base", "base"
"noisy_tolerance"
], ],
"rendering": { "rendering": {
"view.msaa.enabled": true, "view.msaa.enabled": true,
@@ -161,13 +137,14 @@
} }
}, },
{ {
"name": "fxaa", "name": "taa_custom",
"apply_presets": [ "apply_presets": [
"base", "base"
"noisy_tolerance"
], ],
"rendering": { "rendering": {
"view.antiAliasing": "FXAA" "view.taa.enabled": true,
"view.taa.feedback": 0.2,
"view.taa.jitterPattern": "HALTON_23_X16"
} }
}, },
{ {
@@ -191,7 +168,57 @@
} }
}, },
{ {
"name": "ssao_half_res", "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": [ "apply_presets": [
"base" "base"
], ],
@@ -200,11 +227,30 @@
"view.ssao.resolution": 0.5 "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", "name": "ssao_bent_normals",
"apply_presets": [ "apply_presets": [
"base", "base"
"relaxed_tolerance"
], ],
"rendering": { "rendering": {
"view.ssao.enabled": true, "view.ssao.enabled": true,
@@ -252,20 +298,98 @@
} }
}, },
{ {
"name": "ssr", "name": "ssr_basic",
"apply_presets": [ "apply_presets": [
"base", "base"
"relaxed_tolerance"
], ],
"rendering": { "rendering": {
"view.screenSpaceReflections.enabled": true "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", "name": "ssr_quality_combo",
"apply_presets": [ "apply_presets": [
"base", "base"
"relaxed_tolerance"
], ],
"rendering": { "rendering": {
"view.screenSpaceReflections.enabled": true, "view.screenSpaceReflections.enabled": true,
@@ -274,20 +398,89 @@
} }
}, },
{ {
"name": "bloom", "name": "bloom_levels_high",
"apply_presets": [ "apply_presets": [
"base", "base"
"noisy_tolerance"
], ],
"rendering": { "rendering": {
"view.bloom.enabled": true "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", "name": "bloom_lensflare",
"apply_presets": [ "apply_presets": [
"base", "base"
"noisy_tolerance"
], ],
"rendering": { "rendering": {
"view.bloom.enabled": true, "view.bloom.enabled": true,
@@ -297,8 +490,7 @@
{ {
"name": "bloom_lensflare_no_starburst", "name": "bloom_lensflare_no_starburst",
"apply_presets": [ "apply_presets": [
"base", "base"
"noisy_tolerance"
], ],
"rendering": { "rendering": {
"view.bloom.enabled": true, "view.bloom.enabled": true,
@@ -309,8 +501,7 @@
{ {
"name": "bloom_lensflare_chromatic", "name": "bloom_lensflare_chromatic",
"apply_presets": [ "apply_presets": [
"base", "base"
"noisy_tolerance"
], ],
"rendering": { "rendering": {
"view.bloom.enabled": true, "view.bloom.enabled": true,
@@ -319,29 +510,51 @@
} }
}, },
{ {
"name": "bloom_quality_high", "name": "bloom_lensflare_ghosts",
"apply_presets": [ "apply_presets": [
"base", "base"
"noisy_tolerance"
], ],
"rendering": { "rendering": {
"view.bloom.enabled": true, "view.bloom.enabled": true,
"view.bloom.quality": "HIGH" "view.bloom.lensFlare": true,
"view.bloom.ghostCount": 8
} }
}, },
{ {
"name": "bloom_blendMode_interpolate", "name": "bloom_lensflare_ghostSpacing",
"apply_presets": [ "apply_presets": [
"base", "base"
"noisy_tolerance"
], ],
"rendering": { "rendering": {
"view.bloom.enabled": true, "view.bloom.enabled": true,
"view.bloom.blendMode": "INTERPOLATE" "view.bloom.lensFlare": true,
"view.bloom.ghostSpacing": 0.8
} }
}, },
{ {
"name": "dof", "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": [ "apply_presets": [
"base" "base"
], ],
@@ -350,13 +563,43 @@
} }
}, },
{ {
"name": "dof_native_res", "name": "dof_cocScale_high",
"apply_presets": [ "apply_presets": [
"base" "base"
], ],
"rendering": { "rendering": {
"view.dof.enabled": true, "view.dof.enabled": true,
"view.dof.nativeResolution": 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
} }
}, },
{ {
@@ -370,7 +613,50 @@
} }
}, },
{ {
"name": "fog", "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": [ "apply_presets": [
"base" "base"
], ],
@@ -379,7 +665,97 @@
} }
}, },
{ {
"name": "vignette", "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": [ "apply_presets": [
"base" "base"
], ],
@@ -387,6 +763,46 @@
"view.vignette.enabled": true "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", "name": "cg_filmic",
"apply_presets": [ "apply_presets": [
@@ -423,6 +839,33 @@
"view.colorGrading.toneMapping": "PBR_NEUTRAL" "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", "name": "shadow_vsm",
"apply_presets": [ "apply_presets": [
@@ -445,169 +888,12 @@
{ {
"name": "shadow_vsm_highPrecision", "name": "shadow_vsm_highPrecision",
"apply_presets": [ "apply_presets": [
"base", "base"
"relaxed_tolerance"
], ],
"rendering": { "rendering": {
"view.shadowType": "VSM", "view.shadowType": "VSM",
"view.vsmShadowOptions.highPrecision": true "view.vsmShadowOptions.highPrecision": true
} }
},
{
"name": "msaa_custom_bloom",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.customResolve": true,
"view.bloom.enabled": true
}
},
{
"name": "msaa_standard_bloom",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.customResolve": false,
"view.bloom.enabled": true
}
},
{
"name": "msaa_custom_dof",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.customResolve": true,
"view.dof.enabled": true
}
},
{
"name": "msaa_standard_dof",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.customResolve": false,
"view.dof.enabled": true
}
},
{
"name": "dof_bloom",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.dof.enabled": true,
"view.bloom.enabled": true
}
},
{
"name": "ssao_ssr",
"apply_presets": [
"base",
"relaxed_tolerance"
],
"rendering": {
"view.ssao.enabled": true,
"view.screenSpaceReflections.enabled": true
}
},
{
"name": "bloom_ssr",
"apply_presets": [
"base",
"noisy_tolerance",
"relaxed_tolerance"
],
"rendering": {
"view.bloom.enabled": true,
"view.screenSpaceReflections.enabled": true
}
},
{
"name": "dof_ssr",
"apply_presets": [
"base",
"relaxed_tolerance"
],
"rendering": {
"view.dof.enabled": true,
"view.screenSpaceReflections.enabled": true
}
},
{
"name": "fog_bloom",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.fog.enabled": true,
"view.bloom.enabled": true
}
},
{
"name": "fxaa_bloom",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.antiAliasing": "FXAA",
"view.bloom.enabled": true
}
},
{
"name": "fxaa_dof",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.antiAliasing": "FXAA",
"view.dof.enabled": true
}
},
{
"name": "fox_animation",
"models": [
"Fox"
],
"apply_presets": [
"base"
],
"rendering": {
"animation": {
"enabled": true,
"time": 0.5
}
}
},
{
"name": "transmission",
"models": [
"TransmissionRoughnessTest"
],
"apply_presets": [
"base"
],
"rendering": {
"animation": {
"enabled": true,
"time": 0.5
}
}
} }
] ]
} }
@@ -22,7 +22,6 @@ import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.os.FileObserver
import android.text.Html import android.text.Html
import android.util.Log import android.util.Log
import android.view.Choreographer import android.view.Choreographer
@@ -31,10 +30,8 @@ import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Button import android.widget.Button
import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.Spinner import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import com.google.android.filament.utils.KTX1Loader import com.google.android.filament.utils.KTX1Loader
@@ -62,29 +59,22 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private lateinit var surfaceView: SurfaceView private lateinit var surfaceView: SurfaceView
private lateinit var choreographer: Choreographer private lateinit var choreographer: Choreographer
private lateinit var modelViewer: ModelViewer
private lateinit var statusTextView: TextView private lateinit var statusTextView: TextView
private lateinit var testResultsHeader: TextView private lateinit var testResultsHeader: TextView
private lateinit var progressContainer: FrameLayout
private lateinit var testProgress: com.google.android.filament.validation.TestProgressBar
private lateinit var progressTriangle: ImageView
private lateinit var scrollView: ScrollView
private lateinit var testSummaryText: TextView
private lateinit var deviceInfoText: 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 var fileObserver: FileObserver? = null
private var currentAlphaDiffBitmap: Bitmap? = null private var currentAlphaDiffBitmap: Bitmap? = null
private var globalEnhancementFactor: Float = 1.0f private var globalEnhancementFactor: Float = 1.0f
private var modelViewer: ModelViewer? = null
private data class TestImages( private data class TestImages(
val testName: String, val testName: String,
val golden: File?, val golden: Bitmap?,
val rendered: File?, val rendered: Bitmap?,
val diff: File?, val diff: Bitmap?,
val alphaDiff: File? val alphaDiff: Bitmap?
) )
private val diffImageViews = mutableListOf<ImageView>() private val diffImageViews = mutableListOf<ImageView>()
@@ -94,8 +84,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private lateinit var loadButton: Button private lateinit var loadButton: Button
private lateinit var optionsButton: Button private lateinit var optionsButton: Button
private lateinit var enhancementContainer: LinearLayout private lateinit var enhancementContainer: LinearLayout
private lateinit var backendFilterContainer: LinearLayout
private lateinit var backendRadioGroup: android.widget.RadioGroup
private lateinit var enhancementLabel: TextView private lateinit var enhancementLabel: TextView
private lateinit var enhancementSlider: android.widget.SeekBar private lateinit var enhancementSlider: android.widget.SeekBar
@@ -106,10 +94,8 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private val frameScheduler = object : Choreographer.FrameCallback { private val frameScheduler = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) { override fun doFrame(frameTimeNanos: Long) {
choreographer.postFrameCallback(this) choreographer.postFrameCallback(this)
val rendered = modelViewer?.render(frameTimeNanos) ?: false modelViewer.render(frameTimeNanos)
if (rendered) { validationRunner?.onFrame(frameTimeNanos)
validationRunner?.onFrame(frameTimeNanos)
}
} }
} }
@@ -123,28 +109,12 @@ class MainActivity : Activity(), ValidationRunner.Callback {
statusTextView = findViewById(R.id.status_text) statusTextView = findViewById(R.id.status_text)
testResultsHeader = findViewById(R.id.test_results_header) testResultsHeader = findViewById(R.id.test_results_header)
progressContainer = findViewById(R.id.progress_container)
testProgress = findViewById(R.id.test_progress)
progressTriangle = findViewById(R.id.progress_triangle)
scrollView = findViewById(R.id.scroll_view)
testSummaryText = findViewById(R.id.test_summary_text)
deviceInfoText = findViewById(R.id.device_info_text)
deviceInfoText.text = "Running on: ${android.os.Build.MODEL}"
resultsContainer = findViewById(R.id.results_container) resultsContainer = findViewById(R.id.results_container)
scrollView.viewTreeObserver.addOnScrollChangedListener {
updateTrianglePosition()
}
scrollView.viewTreeObserver.addOnGlobalLayoutListener {
updateTrianglePosition()
}
runButton = findViewById(R.id.run_button) runButton = findViewById(R.id.run_button)
loadButton = findViewById(R.id.load_button) loadButton = findViewById(R.id.load_button)
optionsButton = findViewById(R.id.options_button) optionsButton = findViewById(R.id.options_button)
enhancementContainer = findViewById(R.id.enhancement_container) enhancementContainer = findViewById(R.id.enhancement_container)
backendFilterContainer = findViewById(R.id.backend_filter_container)
backendRadioGroup = findViewById(R.id.backend_radio_group)
enhancementLabel = findViewById(R.id.enhancement_label) enhancementLabel = findViewById(R.id.enhancement_label)
enhancementSlider = findViewById(R.id.enhancement_slider) enhancementSlider = findViewById(R.id.enhancement_slider)
@@ -158,7 +128,13 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) {} override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) {}
}) })
// Setup Run Button (Listener is set in handleIntent based on state) // Setup Run Button
runButton.setOnClickListener {
currentInput?.let { input ->
// Always use the generateGoldens flag from the intent/input
startValidation(input)
}
}
// Setup Load Button // Setup Load Button
loadButton.setOnClickListener { loadButton.setOnClickListener {
@@ -168,22 +144,27 @@ class MainActivity : Activity(), ValidationRunner.Callback {
// Setup Options Menu Button // Setup Options Menu Button
optionsButton.setOnClickListener { view -> optionsButton.setOnClickListener { view ->
val popup = android.widget.PopupMenu(this, 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, 4, 0, "Test ADB Info")
popup.menu.add(0, 5, 0, "Result ADB Info") popup.menu.add(0, 5, 0, "Result ADB Info")
popup.menu.add(0, 6, 0, "Toggle Enhancement Slider") popup.menu.add(0, 6, 0, "Toggle Enhancement Slider")
popup.menu.add(0, 7, 0, "Toggle Backend Filter")
popup.setOnMenuItemClickListener { item -> popup.setOnMenuItemClickListener { item ->
when (item.itemId) { when (item.itemId) {
1 -> {
currentInput?.let { input ->
val goldenInput = input.copy(generateGoldens = true)
startValidation(goldenInput)
}
}
2 -> exportTestBundleAction()
3 -> exportTestResultsAction()
4 -> showTestAdbInfo() 4 -> showTestAdbInfo()
5 -> showResultAdbInfo() 5 -> showResultAdbInfo()
6 -> { 6 -> {
enhancementContainer.visibility = enhancementContainer.visibility = if (enhancementContainer.visibility == View.VISIBLE) View.GONE else View.VISIBLE
if (enhancementContainer.visibility == View.VISIBLE) View.GONE else View.VISIBLE
}
7 -> {
backendFilterContainer.visibility =
if (backendFilterContainer.visibility == View.VISIBLE) View.GONE else View.VISIBLE
} }
} }
true true
@@ -194,14 +175,17 @@ class MainActivity : Activity(), ValidationRunner.Callback {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
choreographer = Choreographer.getInstance() choreographer = Choreographer.getInstance()
modelViewer = ModelViewer(surfaceView=surfaceView, manipulator=null)
inputManager = ValidationInputManager(this) inputManager = ValidationInputManager(this)
setupFileObserver() // Initialize IBL
createIndirectLight()
handleIntent() handleIntent()
} }
private fun showLoadDialog() { private fun showLoadDialog() {
val exportDir = inputManager.getBaseDir() val exportDir = getExternalFilesDir(null) ?: filesDir
// Filter out result zips (starting with "results_") to only show test bundles // Filter out result zips (starting with "results_") to only show test bundles
val zips = exportDir.listFiles { _, name -> val zips = exportDir.listFiles { _, name ->
name.endsWith(".zip") && !name.startsWith("results_") name.endsWith(".zip") && !name.startsWith("results_")
@@ -232,7 +216,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
} }
private fun showTestAdbInfo() { private fun showTestAdbInfo() {
val exportDir = inputManager.getBaseDir() val exportDir = getExternalFilesDir(null) ?: filesDir
val path = exportDir.absolutePath val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath) val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder() val message = StringBuilder()
@@ -248,11 +232,12 @@ class MainActivity : Activity(), ValidationRunner.Callback {
message.append("<b>--- PUSH TO DEVICE ---</b><br>") message.append("<b>--- PUSH TO DEVICE ---</b><br>")
if (isInternal) { if (isInternal) {
message.append("<tt>cat &lt;filename&gt; | adb shell \"run-as $packageName sh -c 'cat &gt; files/&lt;filename&gt;'\"</tt><br>") 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 { } else {
message.append("<tt>adb push &lt;filename&gt; $path/</tt><br>") message.append("<tt>adb push &lt;filename&gt; $path/</tt><br>")
} }
message.append("<br>Note: Use underscores instead of spaces in &lt;filename&gt;. The default bundle is <tt>default_test.zip</tt>.") message.append("<br>Note: Use underscores instead of spaces in &lt;filename&gt;.")
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle("Test Bundle ADB Info") .setTitle("Test Bundle ADB Info")
@@ -262,7 +247,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
} }
private fun showResultAdbInfo() { private fun showResultAdbInfo() {
val exportDir = inputManager.getBaseDir() val exportDir = getExternalFilesDir(null) ?: filesDir
val path = exportDir.absolutePath val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath) val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder() val message = StringBuilder()
@@ -298,14 +283,29 @@ class MainActivity : Activity(), ValidationRunner.Callback {
statusTextView.text = "Loading ${file.name}..." statusTextView.text = "Loading ${file.name}..."
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
try { 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 // Clear existing results UI and state
resultsContainer.removeAllViews() resultsContainer.removeAllViews()
diffImageViews.clear() diffImageViews.clear()
resultManager = null resultManager = null
val input = inputManager.loadValidationInputFromZip(file) val newInput = ValidationInputManager.ValidationInput(
onInputUpdated(input) config = config,
Log.i(TAG, "Setting header to: Test Results: ${input!!.config!!.name}") 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) { } catch (e: Exception) {
Log.e(TAG, "Failed to load zip", e) Log.e(TAG, "Failed to load zip", e)
statusTextView.text = "Error: ${e.message}" statusTextView.text = "Error: ${e.message}"
@@ -315,112 +315,52 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private fun createIndirectLight() { private fun createIndirectLight() {
try { try {
modelViewer?.let { mv -> val engine = modelViewer.engine
val engine = mv.engine val scene = modelViewer.scene
val scene = mv.scene val iblName = "default_env"
val iblName = "default_env"
fun readAsset(path: String): ByteBuffer { fun readAsset(path: String): ByteBuffer {
val input = assets.open(path) val input = assets.open(path)
val bytes = input.readBytes() val bytes = input.readBytes()
return ByteBuffer.wrap(bytes) return ByteBuffer.wrap(bytes)
}
readAsset("envs/$iblName/${iblName}_ibl.ktx").let {
val bundle = KTX1Loader.createIndirectLight(engine, it)
scene.indirectLight = bundle.indirectLight
mv.indirectLightCubemap = bundle.cubemap
scene.indirectLight!!.intensity = 30_000.0f
}
readAsset("envs/$iblName/${iblName}_skybox.ktx").let {
val bundle = KTX1Loader.createSkybox(engine, it)
scene.skybox = bundle.skybox
mv.skyboxCubemap = bundle.cubemap
}
Log.i(TAG, "IBL loaded successfully")
} }
readAsset("envs/$iblName/${iblName}_ibl.ktx").let {
val bundle = KTX1Loader.createIndirectLight(engine, it)
scene.indirectLight = bundle.indirectLight
modelViewer.indirectLightCubemap = bundle.cubemap
scene.indirectLight!!.intensity = 30_000.0f
}
readAsset("envs/$iblName/${iblName}_skybox.ktx").let {
val bundle = KTX1Loader.createSkybox(engine, it)
scene.skybox = bundle.skybox
modelViewer.skyboxCubemap = bundle.cubemap
}
Log.i(TAG, "IBL loaded successfully")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to load IBL", e) Log.e(TAG, "Failed to load IBL", e)
statusTextView.text = "Warning: Failed to load IBL" statusTextView.text = "Warning: Failed to load IBL"
} }
} }
private fun onInputUpdated(input: ValidationInputManager.ValidationInput) {
loadButton.isEnabled = inputManager.hasAnyTest();
if (input.config == null && !inputManager.hasDefaultTest()) {
statusTextView.text = "No test loaded. Tap Generate Goldens to create a default test."
testResultsHeader.text = "No Test Loaded"
runButton.text = "Generate Goldens"
runButton.isEnabled = true
runButton.setOnClickListener {
statusTextView.text = "Generating default goldens..."
CoroutineScope(Dispatchers.Main).launch {
try {
// Pressing this button is as if we received an intent to generate goldens
val emptyIntent = Intent()
emptyIntent.putExtra("generate_goldens", true)
onInputUpdated(inputManager.resolveConfig(emptyIntent))
} catch (e: Exception) {
statusTextView.text = "Failed to generate goldens: ${e.message}"
Log.e(TAG, "Failed to generate goldens", e)
}
}
}
return;
}
if (input.config == null && inputManager.hasAnyTest()) {
// User can choose to load at least one test
statusTextView.text = "No test loaded. Please select a test to run."
runButton.text = "No Test Loaded"
runButton.isEnabled = false
runButton.setOnClickListener(null)
return;
}
// Here the input has a configuration, meaning we can run it, so just copy it but account
// for the existance of the default test.
currentInput = input.copy(
generateGoldens = input.generateGoldens && !inputManager.hasDefaultTest()
)
currentInput?.let {
runButton.text = "Run Test"
runButton.isEnabled = true
// Update header
val name = it.config!!.name
Log.i(TAG, "handleIntent: Setting header to: Test Results: ${name}")
testResultsHeader.text = "${name}"
if (it.autoRun) {
startValidation(it)
return
}
// Test is not running, but it is ready
runButton.setOnClickListener {
currentInput?.let { startValidation(it) }
}
// Clear existing results UI and state
clearUIState();
validationRunner?.let {
it.cleanup()
modelViewer = null
}
validationRunner = null
// Just show status
statusTextView.text = "Ready: ${name}"
}
}
private fun handleIntent() { private fun handleIntent() {
statusTextView.text = "Resolving configuration..." statusTextView.text = "Resolving configuration..."
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
try { try {
onInputUpdated(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
if (input.autoRun) {
startValidation(input)
} else {
// Just show status
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}"
@@ -428,95 +368,33 @@ class MainActivity : Activity(), ValidationRunner.Callback {
} }
} }
private fun setupFileObserver() {
val baseDir = inputManager.getBaseDir()
fileObserver = object : FileObserver(baseDir.absolutePath) {
override fun onEvent(event: Int, path: String?) {
if (path == null) return
val isRelevant = (event and (FileObserver.CLOSE_WRITE or FileObserver.MOVED_TO)) != 0
if (isRelevant && path.endsWith(".zip")) {
Log.i(TAG, "FileObserver: Detected new zip file: $path")
if (currentInput?.config == null) {
CoroutineScope(Dispatchers.Main).launch {
try {
// Pressing this button is as if we received an intent to generate goldens
val emptyIntent = Intent()
onInputUpdated(inputManager.resolveConfig(emptyIntent))
} catch (e: Exception) {
statusTextView.text = "Failed to update input on file change: ${e.message}"
Log.e(TAG, "Failed to update input on file change", e)
}
}
}
}
}
}
fileObserver?.startWatching()
}
private fun createResultManager(outputDir: File): ValidationResultManager { private fun createResultManager(outputDir: File): ValidationResultManager {
val gpuDriverInfo = com.google.android.filament.utils.DeviceUtils.getGpuDriverInfo(modelViewer.engine)
return ValidationResultManager( return ValidationResultManager(
outputDir = outputDir, outputDir = outputDir,
gpuDriverInfo = gpuDriverInfo,
deviceName = android.os.Build.MODEL, deviceName = android.os.Build.MODEL,
deviceHardware = android.os.Build.HARDWARE,
deviceCodeName = android.os.Build.DEVICE, deviceCodeName = android.os.Build.DEVICE,
androidVersion = android.os.Build.VERSION.RELEASE, androidVersion = android.os.Build.VERSION.RELEASE,
androidBuildNumber = android.os.Build.DISPLAY androidBuildNumber = android.os.Build.DISPLAY
) )
} }
private fun clearUIState() {
resultsContainer.removeAllViews()
diffImageViews.clear()
resultManager = null
}
private fun startValidation(input: ValidationInputManager.ValidationInput) { private fun startValidation(input: ValidationInputManager.ValidationInput) {
try { try {
val config = input.config ?: return resultsContainer.removeAllViews()
diffImageViews.clear()
clearUIState();
// Disable UI while running
enhancementSlider.isEnabled = false enhancementSlider.isEnabled = false
runButton.isEnabled = false Log.i(TAG, "Starting validation with config: ${input.config.name}")
loadButton.isEnabled = false
optionsButton.isEnabled = false
backendRadioGroup.isEnabled = false
for (i in 0 until backendRadioGroup.childCount) {
backendRadioGroup.getChildAt(i).isEnabled = false
}
Log.i(TAG, "Starting validation with config: ${config.name}")
Log.i(TAG, "Output dir: ${input.outputDir.absolutePath}") Log.i(TAG, "Output dir: ${input.outputDir.absolutePath}")
testProgress.visibility = View.VISIBLE testResultsHeader.text = "${input.config.name}"
progressContainer.visibility = View.VISIBLE
testSummaryText.visibility = View.GONE
deviceInfoText.visibility = View.VISIBLE
totalPassed = 0
totalFailed = 0
testProgress.reset(1)
resultManager = createResultManager(input.outputDir) resultManager = createResultManager(input.outputDir)
val backendFilter = when (backendRadioGroup.checkedRadioButtonId) { validationRunner = ValidationRunner(this, modelViewer, input.config, resultManager!!)
R.id.radio_gles -> "gles"
R.id.radio_vulkan -> "vulkan"
else -> "both"
}
// If we are starting another run, we need to clean up the previous runner's
// resources before we can proceed.
validationRunner?.let {
it.cleanup()
modelViewer = null
}
validationRunner = ValidationRunner(this, surfaceView, config, resultManager!!, backendFilter)
validationRunner?.callback = this validationRunner?.callback = this
validationRunner?.generateGoldens = input.generateGoldens validationRunner?.generateGoldens = input.generateGoldens
validationRunner?.testFilter = input.testFilter
validationRunner?.start() validationRunner?.start()
} catch (e: Exception) { } catch (e: Exception) {
@@ -525,14 +403,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
} }
} }
override fun onModelViewerRecreated(modelViewer: ModelViewer?) {
runOnUiThread {
this.modelViewer = modelViewer
// Re-apply IBL to the new engine/scene
createIndirectLight()
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
choreographer.postFrameCallback(frameScheduler) choreographer.postFrameCallback(frameScheduler)
@@ -552,54 +422,14 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
choreographer.removeFrameCallback(frameScheduler) choreographer.removeFrameCallback(frameScheduler)
fileObserver?.stopWatching()
validationRunner?.let {
it.cleanup()
modelViewer = null
}
} }
private var currentRenderedBitmap: Bitmap? = null private var currentRenderedBitmap: Bitmap? = null
private var currentGoldenBitmap: Bitmap? = null private var currentGoldenBitmap: Bitmap? = null
private var currentDiffBitmap: Bitmap? = null private var currentDiffBitmap: Bitmap? = null
private var totalPassed = 0
private var totalFailed = 0
private var totalTestsCount = 1
private fun updateTrianglePosition() {
if (progressContainer.visibility != View.VISIBLE || resultsContainer.childCount == 0) return
val scrollY = scrollView.scrollY
val visibleHeight = scrollView.height
val centerY = scrollY + visibleHeight / 2f
var bestIndex = -1
var minDistance = Float.MAX_VALUE
for (i in 0 until resultsContainer.childCount) {
val child = resultsContainer.getChildAt(i)
val childCenterY = child.top + child.height / 2f
val dist = Math.abs(childCenterY - centerY)
if (dist < minDistance) {
minDistance = dist
bestIndex = i
}
}
if (bestIndex >= 0) {
val progressWidth = testProgress.width
if (progressWidth > 0 && totalTestsCount > 0) {
val segmentWidth = progressWidth.toFloat() / totalTestsCount
val targetX = (bestIndex * segmentWidth) + (segmentWidth / 2f)
progressTriangle.translationX = targetX - progressTriangle.width / 2f
}
}
}
override fun onTestFinished(result: ValidationResult) { override fun onTestFinished(result: ValidationResult) {
runOnUiThread { runOnUiThread {
if (result.passed) totalPassed++ else totalFailed++
testProgress.addResult(result.passed)
val status = "Test ${result.testName} finished: ${if(result.passed) "PASS" else "FAIL"}" val status = "Test ${result.testName} finished: ${if(result.passed) "PASS" else "FAIL"}"
statusTextView.text = status statusTextView.text = status
Log.i(TAG, status) Log.i(TAG, status)
@@ -638,49 +468,44 @@ class MainActivity : Activity(), ValidationRunner.Callback {
val imagesRow = LinearLayout(this) val imagesRow = LinearLayout(this)
imagesRow.orientation = LinearLayout.HORIZONTAL imagesRow.orientation = LinearLayout.HORIZONTAL
val outDir = resultManager!!.getOutputDir()
val renderedFile = File(outDir, "${result.testName}.png")
val diffFile = File(outDir, "${result.testName}_diff.png")
val alphaDiffFile = File(outDir, "${result.testName}_alpha_diff.png")
val goldenFile = result.goldenPath?.let { File(it) }
val testImages = TestImages( val testImages = TestImages(
testName = result.testName, testName = result.testName,
golden = goldenFile?.takeIf { currentGoldenBitmap != null }, golden = currentGoldenBitmap,
rendered = renderedFile.takeIf { currentRenderedBitmap != null }, rendered = currentRenderedBitmap,
diff = diffFile.takeIf { currentDiffBitmap != null }, diff = currentDiffBitmap,
alphaDiff = alphaDiffFile.takeIf { currentAlphaDiffBitmap != null } alphaDiff = currentAlphaDiffBitmap
) )
fun addImage(label: String, bitmap: Bitmap?, isDiff: Boolean) { fun addImage(label: String, bitmap: Bitmap?, isDiff: Boolean) {
if (bitmap == null) { if (bitmap != null) {
return; val container = LinearLayout(this)
container.orientation = LinearLayout.VERTICAL
container.setPadding(0, 0, 10, 0)
val labelView = TextView(this)
labelView.text = label
labelView.textSize = 9f
container.addView(labelView)
val iv = ImageView(this)
iv.setImageBitmap(bitmap) // Use the same bitmap (or copy if needed, but same is usually fine for UI)
iv.layoutParams = LinearLayout.LayoutParams(250, 250) // Smaller thumbnails
iv.scaleType = ImageView.ScaleType.FIT_CENTER
iv.setBackgroundColor(0xFF404040.toInt())
if (isDiff) {
diffImageViews.add(iv)
applyEnhancementToView(iv, globalEnhancementFactor)
}
iv.setOnClickListener {
showImageDialog(testImages, label)
}
container.addView(iv)
imagesRow.addView(container)
} }
val container = LinearLayout(this)
container.orientation = LinearLayout.VERTICAL
container.setPadding(0, 0, 10, 0)
val labelView = TextView(this)
labelView.text = label
labelView.textSize = 9f
container.addView(labelView)
val iv = ImageView(this)
iv.setImageBitmap(bitmap!!) // Use the same bitmap (or copy if needed, but same is usually fine for UI)
iv.layoutParams = LinearLayout.LayoutParams(250, 250) // Smaller thumbnails
iv.scaleType = ImageView.ScaleType.FIT_CENTER
iv.setBackgroundColor(0xFF404040.toInt())
if (isDiff) {
diffImageViews.add(iv)
applyEnhancementToView(iv, globalEnhancementFactor)
}
iv.setOnClickListener {
showImageDialog(testImages, label)
}
container.addView(iv)
imagesRow.addView(container)
} }
addImage("Rendered", currentRenderedBitmap, false) addImage("Rendered", currentRenderedBitmap, false)
@@ -694,7 +519,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
resultContainer.addView(imagesRow) resultContainer.addView(imagesRow)
resultsContainer.addView(resultContainer) resultsContainer.addView(resultContainer)
resultsContainer.post { updateTrianglePosition() }
// Clear current images for next test // Clear current images for next test
currentRenderedBitmap = null currentRenderedBitmap = null
@@ -704,96 +528,50 @@ class MainActivity : Activity(), ValidationRunner.Callback {
} }
} }
override fun onTestProgress(current: Int, total: Int) {
runOnUiThread {
totalTestsCount = Math.max(1, total)
testProgress.setMax(total)
updateTrianglePosition()
}
}
override fun onAllTestsFinished() { override fun onAllTestsFinished() {
runOnUiThread { runOnUiThread {
val total = totalPassed + totalFailed
val colorPassed = if (totalPassed > 0) "#4CAF50" else "#000000"
val colorFailed = if (totalFailed > 0) "#F44336" else "#000000"
val html = "Passed: <font color='$colorPassed'><b>$totalPassed</b></font> / $total &nbsp;&nbsp;&nbsp; Failed: <font color='$colorFailed'><b>$totalFailed</b></font>"
testSummaryText.text = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY)
testSummaryText.visibility = View.VISIBLE
deviceInfoText.visibility = View.GONE
statusTextView.text = "All tests finished!" statusTextView.text = "All tests finished!"
// Re-enable UI
enhancementSlider.isEnabled = true enhancementSlider.isEnabled = true
runButton.isEnabled = true Log.i(TAG, "All tests finished " + if (currentInput?.autoExport == true) "Exporting bundle" else "x")
loadButton.isEnabled = true
optionsButton.isEnabled = true if (currentInput?.autoExport == true) {
backendRadioGroup.isEnabled = true exportTestBundleAction()
for (i in 0 until backendRadioGroup.childCount) {
backendRadioGroup.getChildAt(i).isEnabled = true
} }
if (currentInput?.autoExportResults == true) {
Log.i(TAG, "All tests finished") exportTestResultsAction()
currentInput?.let {
if (it.generateGoldens) {
exportTestBundleAction(it)
} else {
exportTestResultsAction(it)
}
} }
} }
} }
private fun exportTestBundleAction(input: ValidationInputManager.ValidationInput) { private fun exportTestBundleAction() {
val config = input.config ?: return currentInput?.let { input ->
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: createResultManager(input.outputDir) val rm = resultManager ?: createResultManager(input.outputDir)
val bundleNameOverride = if (input.sourceZip == null) "default_test" else null val zip = rm.exportTestBundle(input.config, timestamp)
val zip = rm.exportTestBundle(config, timestamp, bundleNameOverride) if (zip != null) {
if (zip != null) {
if (input.sourceZip == null) {
val msg = "Exported Default Bundle: ${zip.name}."
statusTextView.text = msg
Log.i(TAG, "Exported test bundle to ${zip.absolutePath}.")
CoroutineScope(Dispatchers.Main).launch {
try {
// here we don't want to call onInputUpdated(newInput) since we want to keep
// the state after the test ran
currentInput = inputManager.loadValidationInputFromZip(zip)
Log.i(TAG, "Auto-loaded default bundle.")
} catch (e: Exception) {
statusTextView.text = "Failed to auto-load default bundle: ${e.message}"
Log.e(TAG, "Failed to load default bundle", e)
}
}
} else {
val msg = "Exported Bundle: ${zip.name}" val msg = "Exported Bundle: ${zip.name}"
statusTextView.text = msg statusTextView.text = msg
Log.i(TAG, "Exported test bundle to ${zip.absolutePath}") Log.i(TAG, "Exported test bundle to ${zip.absolutePath}")
} } else {
} else { statusTextView.text = "Export Bundle failed"
statusTextView.text = "Export Bundle failed" Log.e(TAG, "Export Bundle failed")
Log.e(TAG, "Export Bundle failed") }
} }
} }
private fun exportTestResultsAction(input: ValidationInputManager.ValidationInput) { private fun exportTestResultsAction() {
if (input.config == null) { currentInput?.let { input ->
return val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
} val rm = resultManager ?: createResultManager(input.outputDir)
val config = input.config!! val zip = rm.exportTestResults(input.sourceZip, timestamp)
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) if (zip != null) {
val rm = resultManager ?: createResultManager(input.outputDir) val msg = "Exported Results: ${zip.name}"
val zip = rm.exportTestResults(input.sourceZip, config, timestamp) statusTextView.text = msg
if (zip != null) { Log.i(TAG, "Exported results to ${zip.absolutePath}")
val msg = "Exported Results: ${zip.name}" } else {
statusTextView.text = msg statusTextView.text = "Export Results failed"
Log.i(TAG, "Exported results to ${zip.absolutePath}") Log.e(TAG, "Export Results failed")
} else { }
statusTextView.text = "Export Results failed"
Log.e(TAG, "Export Results failed")
} }
} }
@@ -804,24 +582,20 @@ class MainActivity : Activity(), ValidationRunner.Callback {
} }
override fun onImageResult(type: String, bitmap: Bitmap) { override fun onImageResult(type: String, bitmap: Bitmap) {
// Create a scaled-down thumbnail (e.g. 128x128) to save memory in the UI scroll view.
// We use true for filter to smooth the scaling.
val scaledBitmap = Bitmap.createScaledBitmap(bitmap, 128, 128, true)
runOnUiThread { runOnUiThread {
// Update the "live" views // Update the "live" views
when (type) { when (type) {
"Rendered" -> { "Rendered" -> {
currentRenderedBitmap = scaledBitmap currentRenderedBitmap = bitmap
} }
"Golden" -> { "Golden" -> {
currentGoldenBitmap = scaledBitmap currentGoldenBitmap = bitmap
} }
"Diff" -> { "Diff" -> {
currentDiffBitmap = scaledBitmap currentDiffBitmap = bitmap
} }
"Alpha Diff" -> { "Alpha Diff" -> {
currentAlphaDiffBitmap = scaledBitmap currentAlphaDiffBitmap = bitmap
} }
} }
} }
@@ -859,18 +633,17 @@ class MainActivity : Activity(), ValidationRunner.Callback {
titleView.text = images.testName titleView.text = images.testName
val availableFiles = mutableListOf<Pair<String, File>>() val availableImages = mutableListOf<Pair<String, Bitmap>>()
images.rendered?.takeIf { it.exists() }?.let { availableFiles.add(Pair("Rendered", it)) } images.rendered?.let { availableImages.add(Pair("Rendered", it)) }
images.golden?.takeIf { it.exists() }?.let { availableFiles.add(Pair("Golden", it)) } images.golden?.let { availableImages.add(Pair("Golden", it)) }
images.diff?.takeIf { it.exists() }?.let { availableFiles.add(Pair("Diff", it)) } images.diff?.let { availableImages.add(Pair("Diff", it)) }
images.alphaDiff?.takeIf { it.exists() }?.let { availableFiles.add(Pair("Alpha Diff", it)) } images.alphaDiff?.let { availableImages.add(Pair("Alpha Diff", it)) }
if (availableFiles.isEmpty()) return if (availableImages.isEmpty()) return
var currentIndex = availableFiles.indexOfFirst { it.first == initialLabel } var currentIndex = availableImages.indexOfFirst { it.first == initialLabel }
if (currentIndex == -1) currentIndex = 0 if (currentIndex == -1) currentIndex = 0
var currentDialogBitmap: Bitmap? = null
var currentDialogEnhancement = globalEnhancementFactor var currentDialogEnhancement = globalEnhancementFactor
val matrix = android.graphics.Matrix() val matrix = android.graphics.Matrix()
@@ -879,11 +652,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
var lastTouchY = 0f var lastTouchY = 0f
var isDragging = false var isDragging = false
// this is to prevent resetting the view matrix when flipping through goloden, rendered,
// diff images. These actions will trigger the OnLayoutChangeListener, but in these cases,
// We want to keep the same view matrix.
var matrixResettable = true
val scaleDetector = android.view.ScaleGestureDetector(this, object : android.view.ScaleGestureDetector.SimpleOnScaleGestureListener() { val scaleDetector = android.view.ScaleGestureDetector(this, object : android.view.ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: android.view.ScaleGestureDetector): Boolean { override fun onScale(detector: android.view.ScaleGestureDetector): Boolean {
matrix.postScale(detector.scaleFactor, detector.scaleFactor, detector.focusX, detector.focusY) matrix.postScale(detector.scaleFactor, detector.scaleFactor, detector.focusX, detector.focusY)
@@ -918,13 +686,9 @@ class MainActivity : Activity(), ValidationRunner.Callback {
} }
fun updateView() { fun updateView() {
val (label, file) = availableFiles[currentIndex] val (label, bitmap) = availableImages[currentIndex]
typeView.text = label typeView.text = label
imageView.setImageBitmap(bitmap)
val oldBitmap = currentDialogBitmap
currentDialogBitmap = android.graphics.BitmapFactory.decodeFile(file.absolutePath)
imageView.setImageBitmap(currentDialogBitmap)
(imageView.drawable as? android.graphics.drawable.BitmapDrawable)?.setAntiAlias(false) (imageView.drawable as? android.graphics.drawable.BitmapDrawable)?.setAntiAlias(false)
(imageView.drawable as? android.graphics.drawable.BitmapDrawable)?.setFilterBitmap(false) (imageView.drawable as? android.graphics.drawable.BitmapDrawable)?.setFilterBitmap(false)
imageView.imageMatrix = matrix imageView.imageMatrix = matrix
@@ -936,18 +700,14 @@ class MainActivity : Activity(), ValidationRunner.Callback {
enhancementContainer.visibility = View.GONE enhancementContainer.visibility = View.GONE
imageView.colorFilter = null imageView.colorFilter = null
} }
oldBitmap?.recycle()
} }
fun resetMatrix() { fun resetMatrix() {
val drawable = imageView.drawable ?: return val drawable = imageView.drawable ?: return
val width = imageView.width.toFloat() val width = imageView.width.toFloat()
val height = imageView.height.toFloat() val height = imageView.height.toFloat()
if (width <= 0f || height <= 0f) return
val dw = drawable.intrinsicWidth.toFloat() val dw = drawable.intrinsicWidth.toFloat()
val dh = drawable.intrinsicHeight.toFloat() val dh = drawable.intrinsicHeight.toFloat()
if (dw <= 0f || dh <= 0f) return
val scaleX = width / dw val scaleX = width / dw
val scaleY = height / dh val scaleY = height / dh
@@ -962,33 +722,14 @@ class MainActivity : Activity(), ValidationRunner.Callback {
imageView.imageMatrix = matrix imageView.imageMatrix = matrix
} }
imageView.addOnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
if (matrixResettable &&
(left != oldLeft || right != oldRight || top != oldTop || bottom != oldBottom)) {
resetMatrix()
}
}
dialog.setOnDismissListener {
matrixResettable = true;
currentDialogBitmap?.recycle()
currentDialogBitmap = null
}
btnClose.setOnClickListener { dialog.dismiss() } btnClose.setOnClickListener { dialog.dismiss() }
btnReset.setOnClickListener { btnReset.setOnClickListener { resetMatrix() }
matrixResettable = true
resetMatrix()
}
btnPrev.setOnClickListener { btnPrev.setOnClickListener {
currentIndex = (currentIndex - 1 + availableFiles.size) % availableFiles.size currentIndex = (currentIndex - 1 + availableImages.size) % availableImages.size
matrixResettable = false
updateView() updateView()
} }
btnNext.setOnClickListener { btnNext.setOnClickListener {
currentIndex = (currentIndex + 1) % availableFiles.size currentIndex = (currentIndex + 1) % availableImages.size
matrixResettable = false
updateView() updateView()
} }
@@ -1001,13 +742,16 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onProgressChanged(seekBar: android.widget.SeekBar?, progress: Int, fromUser: Boolean) { override fun onProgressChanged(seekBar: android.widget.SeekBar?, progress: Int, fromUser: Boolean) {
currentDialogEnhancement = 1.0f + (progress / 100f) * 49.0f currentDialogEnhancement = 1.0f + (progress / 100f) * 49.0f
enhancementLabel.text = String.format(Locale.US, "Enhance: %.1fx", currentDialogEnhancement) enhancementLabel.text = String.format(Locale.US, "Enhance: %.1fx", currentDialogEnhancement)
matrixResettable = false
updateView() updateView()
} }
override fun onStartTrackingTouch(seekBar: android.widget.SeekBar?) {} override fun onStartTrackingTouch(seekBar: android.widget.SeekBar?) {}
override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) {} override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) {}
}) })
imageView.post {
resetMatrix()
}
updateView() updateView()
dialog.show() dialog.show()
} }
@@ -31,7 +31,7 @@ data class RenderTestConfig(
data class TestConfig( data class TestConfig(
val name: String, val name: String,
val description: String?, val description: String?,
val backend: String, val backends: List<String>,
val models: Set<String>, val models: Set<String>,
val rendering: JSONObject, val rendering: JSONObject,
val tolerance: JSONObject? val tolerance: JSONObject?
@@ -102,7 +102,7 @@ class ConfigParser {
val testsJson = json.getJSONArray("tests") val testsJson = json.getJSONArray("tests")
val tests = mutableListOf<TestConfig>() val tests = mutableListOf<TestConfig>()
for (i in 0 until testsJson.length()) { for (i in 0 until testsJson.length()) {
tests.addAll(parseTestConfig(testsJson.getJSONObject(i), models.keys, presets, backends)) tests.add(parseTestConfig(testsJson.getJSONObject(i), models.keys, presets, backends))
} }
return RenderTestConfig(name, backends, models, tests) return RenderTestConfig(name, backends, models, tests)
@@ -125,7 +125,7 @@ class ConfigParser {
existingModels: Set<String>, existingModels: Set<String>,
presets: Map<String, PresetConfig>, presets: Map<String, PresetConfig>,
defaultBackends: List<String> defaultBackends: List<String>
): List<TestConfig> { ): TestConfig {
val name = json.getString("name") val name = json.getString("name")
val description = json.optString("description") val description = json.optString("description")
val backends = json.optJSONArray("backends")?.toList<String>() ?: defaultBackends val backends = json.optJSONArray("backends")?.toList<String>() ?: defaultBackends
@@ -165,9 +165,7 @@ class ConfigParser {
val tolerance = json.optJSONObject("tolerance") ?: lastTolerance val tolerance = json.optJSONObject("tolerance") ?: lastTolerance
return backends.map { backend -> return TestConfig(name, description, backends, combinedModels, rendering, tolerance)
TestConfig(name, description, backend, combinedModels, rendering, tolerance)
}
} }
} }
} }
@@ -1,55 +0,0 @@
package com.google.android.filament.validation
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
class TestProgressBar(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {
private var maxTests = 1
private val results = mutableListOf<Boolean>()
private val paintPass = Paint().apply { color = Color.parseColor("#4CAF50") } // Green
private val paintFail = Paint().apply { color = Color.parseColor("#F44336") } // Red
private val paintBg = Paint().apply { color = Color.parseColor("#E0E0E0") } // Gray
fun reset(max: Int) {
maxTests = Math.max(1, max)
results.clear()
invalidate()
}
fun addResult(passed: Boolean) {
results.add(passed)
invalidate()
}
// Kept for partial compatibility with previous ProgressBar
fun setMax(max: Int) {
maxTests = Math.max(1, max)
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val w = width.toFloat()
val h = height.toFloat()
// Draw background
canvas.drawRect(0f, 0f, w, h, paintBg)
if (results.isEmpty() || maxTests <= 0) return
val segmentWidth = w / maxTests
for (i in results.indices) {
val paint = if (results[i]) paintPass else paintFail
val left = i * segmentWidth
val right = (i + 1) * segmentWidth
canvas.drawRect(left, 0f, right, h, paint)
}
}
}
@@ -24,8 +24,16 @@ import kotlinx.coroutines.withContext
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.zip.ZipFile import java.util.zip.ZipFile
/**
* Handles the retrieval and preparation of test configuration and assets.
* Supports loading from:
* 1. Intent extras (local path or URL)
* 2. Default embedded assets (fallback)
*/
class ValidationInputManager(private val context: Context) { class ValidationInputManager(private val context: Context) {
companion object { companion object {
@@ -33,54 +41,35 @@ 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 autoRun: Boolean = false,
val sourceZip: File? = null, val autoExport: Boolean = false,
val testFilter: String? = null val autoExportResults: Boolean = false,
val sourceZip: File? = null
) )
public fun getBaseDir() : File {
// Note that this is the app's internal directory. Not visible unless you're root or you're
// this app.
return context.filesDir
}
public fun hasDefaultTest(): Boolean {
val defaultTestZip = File(getBaseDir(), "default_test.zip")
return defaultTestZip.exists()
}
public fun hasAnyTest(): Boolean {
val dir = getBaseDir()
val matchedFiles = dir.listFiles {_, name ->
name.endsWith("_test.zip")
}
return matchedFiles != null && matchedFiles.isNotEmpty()
}
/** /**
* Resolves the test configuration based on the provided intent extras. * Resolves the test configuration based on the provided intent extras.
* This may involve extracting assets or downloading files. * This may involve extracting assets or downloading files.
*/ */
suspend fun resolveConfig(intent: Intent): ValidationInput = withContext(Dispatchers.IO) { suspend fun resolveConfig(intent: Intent): ValidationInput = withContext(Dispatchers.IO) {
val testConfigPath = intent.getStringExtra("test_config")
val urlConfig = intent.getStringExtra("url_config")
val urlModelsBase = intent.getStringExtra("url_models_base")
val zipPath = intent.getStringExtra("zip_path") val 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)
// If we get a generateGoldens signal, then it should trigger a run val autoExport = intent.getBooleanExtra("auto_export", false)
val autoRun = intent.getBooleanExtra("auto_run", false) || val autoExportResults = intent.getBooleanExtra("auto_export_results", false)
intent.getBooleanExtra("generate_goldens", false)
val testFilter = if (autoRun) intent.getStringExtra("test_filter") else null
val outputPath = intent.getStringExtra("output_path") val outputPath = intent.getStringExtra("output_path")
Log.i(TAG, "Resolving config with outputPath: $outputPath") Log.i(TAG, "Resolving config with outputPath: $outputPath")
val baseDir = getBaseDir() val baseDir = context.getExternalFilesDir(null) ?: context.filesDir
Log.i(TAG, "Base directory for resolution: ${baseDir.absolutePath}") Log.i(TAG, "Base directory for resolution: ${baseDir.absolutePath}")
val outputDir = if (!outputPath.isNullOrBlank()) { val outputDir = if (!outputPath.isNullOrBlank()) {
val file = File(outputPath) val file = File(outputPath)
val resolved = if (file.isAbsolute) { val resolved = if (file.isAbsolute) {
@@ -88,7 +77,7 @@ class ValidationInputManager(private val context: Context) {
} else { } else {
File(baseDir, outputPath) File(baseDir, outputPath)
} }
// Critical check: if resolved is root or very short, it's likely wrong // Critical check: if resolved is root or very short, it's likely wrong
if (resolved.absolutePath == "/" || resolved.parent == null) { if (resolved.absolutePath == "/" || resolved.parent == null) {
Log.w(TAG, "Resolved outputDir is root ($resolved), defaulting to app-specific dir") Log.w(TAG, "Resolved outputDir is root ($resolved), defaulting to app-specific dir")
@@ -99,115 +88,76 @@ class ValidationInputManager(private val context: Context) {
} else { } else {
File(baseDir, "validation_results") File(baseDir, "validation_results")
} }
if (!outputDir.exists() && !outputDir.mkdirs()) { if (!outputDir.exists() && !outputDir.mkdirs()) {
Log.e(TAG, "Failed to create outputDir: ${outputDir.absolutePath}") Log.e(TAG, "Failed to create outputDir: ${outputDir.absolutePath}")
} }
Log.i(TAG, "Final outputDir: ${outputDir.absolutePath}") Log.i(TAG, "Final outputDir: ${outputDir.absolutePath}")
var sourceZipFile = if (zipPath != null) { val sourceZipFile = if (zipPath != null) {
val file = File(zipPath) val file = File(zipPath)
val resolvedFile = if (file.isAbsolute) { if (file.isAbsolute) {
file file
} else { } else {
File(baseDir, zipPath) File(baseDir, zipPath)
} }
Log.i(TAG, "Resolved zipPath '$zipPath' to ${resolvedFile.absolutePath} (exists: ${resolvedFile.exists()})")
resolvedFile
} else { } else {
null null
} }
// Auto-load logic: if no zipPath provided, and default_test.zip is the ONLY zip, auto-load it
if (sourceZipFile == null) {
val exportDir = baseDir
val availableZips = exportDir.listFiles { _, name ->
name.endsWith(".zip") && !name.startsWith("results_")
}?.toList() ?: emptyList()
if (availableZips.size == 1 && availableZips[0].name == "default_test.zip") {
sourceZipFile = availableZips[0]
Log.i(TAG, "Auto-loaded default_test.zip since it's the only test bundle available.")
}
}
val config = when { val config = when {
sourceZipFile != null && sourceZipFile.exists() -> loadFromZip(sourceZipFile) sourceZipFile != null && sourceZipFile.exists() -> loadFromZip(sourceZipFile)
generateGoldens -> extractDefaultAssets() urlConfig != null -> downloadConfig(urlConfig, urlModelsBase)
else -> null testConfigPath != null -> ConfigParser.parseFromPath(testConfigPath)
else -> extractDefaultAssets()
} }
return@withContext ValidationInput(config, outputDir, generateGoldens, autoRun, sourceZipFile, testFilter) return@withContext ValidationInput(config, outputDir, generateGoldens, autoRun, autoExport, autoExportResults, sourceZipFile)
} }
private var lastUnzippedFile: String? = null suspend fun loadFromZip(zipFile: File): RenderTestConfig {
private var lastUnzippedTime: Long = 0 Log.i(TAG, "Unzipping validation bundle: ${zipFile.absolutePath}")
// Use a unique cache dir based on timestamp or just overwrite a common one
suspend fun loadValidationInputFromZip(file: File) : ValidationInput { // Overwriting is safer to avoid filling up disk, but we must ensure we don't conflict with current run
val config = loadFromZip(file)
val baseDir = getBaseDir()
val outputDir = File(baseDir, "validation_results").apply { mkdirs() }
val newInput = ValidationInputManager.ValidationInput(
config = config,
outputDir = outputDir,
generateGoldens = false,
autoRun = false,
sourceZip = file,
testFilter = null
)
return newInput
}
private suspend fun loadFromZip(zipFile: File): RenderTestConfig {
val baseCacheDir = context.externalCacheDir ?: context.cacheDir val baseCacheDir = context.externalCacheDir ?: context.cacheDir
val cacheDir = File(baseCacheDir, "unzipped_validation") val cacheDir = File(baseCacheDir, "unzipped_validation")
if (cacheDir.exists()) cacheDir.deleteRecursively()
cacheDir.mkdirs()
if (lastUnzippedFile == zipFile.absolutePath && lastUnzippedTime == zipFile.lastModified() && Log.i(TAG, "Using cacheDir: ${cacheDir.absolutePath}")
cacheDir.exists()) {
Log.i(TAG, "Zip file unmodified, skipping unzip to preserve generated goldens.")
} else {
Log.i(TAG, "Unzipping validation bundle: ${zipFile.absolutePath}")
if (cacheDir.exists()) cacheDir.deleteRecursively()
cacheDir.mkdirs()
Log.i(TAG, "Using cacheDir: ${cacheDir.absolutePath}") ZipFile(zipFile).use { zip ->
val entries = zip.entries()
ZipFile(zipFile).use { zip -> while (entries.hasMoreElements()) {
val entries = zip.entries() val entry = entries.nextElement()
while (entries.hasMoreElements()) { val entryFile = File(cacheDir, entry.name)
val entry = entries.nextElement() // specific check to avoid zip slip vulnerability (though low risk here)
val entryFile = File(cacheDir, entry.name) if (!entryFile.canonicalPath.startsWith(cacheDir.canonicalPath)) {
// specific check to avoid zip slip vulnerability (though low risk here) throw SecurityException("Zip entry is outside of the target dir: ${entry.name}")
if (!entryFile.canonicalPath.startsWith(cacheDir.canonicalPath)) { }
throw SecurityException("Zip entry is outside of the target dir: ${entry.name}")
} if (entry.isDirectory) {
entryFile.mkdirs()
if (entry.isDirectory) { } else {
entryFile.mkdirs() entryFile.parentFile?.mkdirs()
} else { zip.getInputStream(entry).use { input ->
entryFile.parentFile?.mkdirs() FileOutputStream(entryFile).use { output ->
zip.getInputStream(entry).use { input -> input.copyTo(output)
FileOutputStream(entryFile).use { output -> }
input.copyTo(output)
}
}
} }
} }
} }
lastUnzippedFile = zipFile.absolutePath
lastUnzippedTime = zipFile.lastModified()
} }
// Find config.json // Find config.json
// We look for a file ending in .json within the unzipped structure // We look for a file ending in .json within the unzipped structure
// Exclude results.json if it happened to be there // Exclude results.json if it happened to be there
val jsonFiles = cacheDir.walkTopDown() val jsonFiles = cacheDir.walkTopDown()
.filter { it.isFile && it.extension == "json" && it.name != "results.json" } .filter { it.isFile && it.extension == "json" && it.name != "results.json" }
.toList() .toList()
if (jsonFiles.isEmpty()) throw IllegalStateException("No config.json found in zip") if (jsonFiles.isEmpty()) throw IllegalStateException("No config.json found in zip")
// Prefer one named config.json or take the first one // Prefer one named config.json or take the first one
val configFile = jsonFiles.find { it.name == "config.json" } ?: jsonFiles.first() val configFile = jsonFiles.find { it.name == "config.json" } ?: jsonFiles.first()
@@ -215,7 +165,7 @@ class ValidationInputManager(private val context: Context) {
return ConfigParser.parseFromPath(configFile.absolutePath) return ConfigParser.parseFromPath(configFile.absolutePath)
} }
suspend fun extractDefaultAssets(): RenderTestConfig { private suspend fun extractDefaultAssets(): RenderTestConfig {
Log.i(TAG, "Extracting default assets...") Log.i(TAG, "Extracting default assets...")
val filesDir = context.getExternalFilesDir(null) ?: context.filesDir val filesDir = context.getExternalFilesDir(null) ?: context.filesDir
val assetManager = context.assets val assetManager = context.assets
@@ -231,39 +181,84 @@ class ValidationInputManager(private val context: Context) {
} }
} }
// Copy models // Copy DamagedHelmet.glb
val modelsDir = File(filesDir, "models") val modelsDir = File(filesDir, "models")
modelsDir.mkdirs() modelsDir.mkdirs()
assetManager.list("models")?.forEach { modelFileName -> val modelOut = File(modelsDir, "helmet.glb")
val modelOut = File(modelsDir, modelFileName)
assetManager.open("models/$modelFileName").use { input -> assetManager.open("models/helmet.glb").use { input ->
FileOutputStream(modelOut).use { output -> FileOutputStream(modelOut).use { output ->
input.copyTo(output) input.copyTo(output)
}
} }
} }
// Update config to point to relative path (standardizing on relative for portability where possible) // Update config to point to relative path (standardizing on relative for portability where possible)
// or absolute. Here we use relative as per previous logic. // or absolute. Here we use relative as per previous logic.
val configJson = JSONObject(configOut.readText()) val configJson = JSONObject(configOut.readText())
// Dynamically name the test bundle based on device model
configJson.put("name", "${android.os.Build.MODEL} Test")
val models = configJson.getJSONObject("models") val models = configJson.getJSONObject("models")
// Update all model paths to point to the extracted files in the models directory // Ensure the default model points to the extracted file
val keys = models.keys() // We can use absolute path to be safe since we know where it is now.
val newModels = JSONObject() models.put("DamagedHelmet", modelOut.absolutePath)
while (keys.hasNext()) {
val key = keys.next()
val fileName = models.getString(key)
newModels.put(key, java.io.File(modelsDir, fileName).absolutePath)
}
configJson.put("models", newModels)
configOut.writeText(configJson.toString(2)) configOut.writeText(configJson.toString(2))
return ConfigParser.parseFromPath(configOut.absolutePath) return ConfigParser.parseFromPath(configOut.absolutePath)
} }
private suspend fun downloadConfig(urlConfig: String, urlModelsBase: String?): RenderTestConfig {
Log.i(TAG, "Downloading config from $urlConfig")
val filesDir = context.getExternalFilesDir(null) ?: context.filesDir
val configDir = File(filesDir, "config")
configDir.mkdirs()
val modelsDir = File(filesDir, "models")
modelsDir.mkdirs()
val configName = "downloaded_config.json"
val configFile = File(configDir, configName)
downloadFile(urlConfig, configFile)
if (urlModelsBase != null) {
val configJson = JSONObject(configFile.readText())
val models = configJson.optJSONObject("models")
if (models != null) {
val keys = models.keys()
while (keys.hasNext()) {
val key = keys.next()
val modelPath = models.getString(key)
val fileName = File(modelPath).name
val modelFile = File(modelsDir, fileName)
val modelUrl = "$urlModelsBase/$fileName"
Log.i(TAG, "Downloading model: $fileName from $modelUrl")
downloadFile(modelUrl, modelFile)
// Update config to point to absolute path
models.put(key, modelFile.absolutePath)
}
configFile.writeText(configJson.toString())
}
}
return ConfigParser.parseFromPath(configFile.absolutePath)
}
private fun downloadFile(urlStr: String, destFile: File) {
val url = URL(urlStr)
val connection = url.openConnection() as HttpURLConnection
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} for $urlStr")
}
destFile.parentFile?.mkdirs()
connection.inputStream.use { input ->
FileOutputStream(destFile).use { output ->
input.copyTo(output)
}
}
}
} }
@@ -28,14 +28,13 @@ import org.json.JSONObject
data class ValidationResult( data class ValidationResult(
val testName: String, val testName: String,
val passed: Boolean, val passed: Boolean,
val diffMetric: Float = 0f, val diffMetric: Float = 0f
val goldenPath: String? = null
) )
class ValidationResultManager( class ValidationResultManager(
private val outputDir: File, private val outputDir: File,
private val gpuDriverInfo: String,
private val deviceName: String, private val deviceName: String,
private val deviceHardware: String,
private val deviceCodeName: String, private val deviceCodeName: String,
private val androidVersion: String, private val androidVersion: String,
private val androidBuildNumber: String private val androidBuildNumber: String
@@ -45,8 +44,7 @@ class ValidationResultManager(
private const val TAG = "ValidationResultManager" private const val TAG = "ValidationResultManager"
} }
private val results = java.util.concurrent.CopyOnWriteArrayList<ValidationResult>() private val results = mutableListOf<ValidationResult>()
private val gpuDriverInfos = JSONObject()
init { init {
if (!outputDir.exists()) { if (!outputDir.exists()) {
@@ -54,10 +52,6 @@ class ValidationResultManager(
} }
} }
fun addGpuDriverInfo(backend: String, info: String) {
gpuDriverInfos.put(backend, info)
}
fun addResult(result: ValidationResult) { fun addResult(result: ValidationResult) {
results.add(result) results.add(result)
} }
@@ -89,19 +83,18 @@ class ValidationResultManager(
* - input test bundle (as nested zip), if provided * - input test bundle (as nested zip), if provided
* - diff images (if any failure) * - diff images (if any failure)
*/ */
fun exportTestResults(sourceZip: File?, config: RenderTestConfig, timestamp: String): File? { fun exportTestResults(sourceZip: File?, timestamp: String): File? {
// Safe parent dir resolution // Safe parent dir resolution
val parentDir = outputDir.canonicalFile.parentFile ?: outputDir.parentFile val parentDir = outputDir.canonicalFile.parentFile ?: outputDir.parentFile
if (parentDir == null) return null if (parentDir == null) return null
val resultZipName = "results_$timestamp" val resultZipName = "results_$timestamp"
val tempZipFile = File(parentDir, "$resultZipName.zip.tmp") val zipFile = File(parentDir, "$resultZipName.zip")
val finalZipFile = File(parentDir, "$resultZipName.zip")
Log.i(TAG, "Exporting results to ${tempZipFile.absolutePath}") Log.i(TAG, "Exporting results to ${zipFile.absolutePath}")
try { try {
ZipOutputStream(FileOutputStream(tempZipFile)).use { zos -> ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
// 1. Add results.json // 1. Add results.json
val resultsJson = File(outputDir, "results.json") val resultsJson = File(outputDir, "results.json")
if (resultsJson.exists()) { if (resultsJson.exists()) {
@@ -110,28 +103,24 @@ class ValidationResultManager(
zos.closeEntry() zos.closeEntry()
} }
// 2. Add test bundle zip (fallback to sourceZip) // 2. Add source zip if exists
if (sourceZip != null && sourceZip.exists()) { if (sourceZip != null && sourceZip.exists()) {
zos.putNextEntry(ZipEntry("bundle.zip")) zos.putNextEntry(ZipEntry(sourceZip.name))
sourceZip.inputStream().use { it.copyTo(zos) } sourceZip.inputStream().use { it.copyTo(zos) }
zos.closeEntry() zos.closeEntry()
} }
// 3. Add images (exclude diffs, frontend computes them) // 3. Add images (only rendered images, exclude diffs)
outputDir.listFiles { _, name -> name.endsWith(".png") && !name.endsWith("_diff.png") }?.forEach { imgFile -> outputDir.listFiles { _, name -> name.endsWith(".png") && !name.endsWith("_diff.png") }?.forEach { imgFile ->
zos.putNextEntry(ZipEntry(imgFile.name)) zos.putNextEntry(ZipEntry(imgFile.name))
imgFile.inputStream().use { it.copyTo(zos) } imgFile.inputStream().use { it.copyTo(zos) }
zos.closeEntry() zos.closeEntry()
} }
} }
Log.i(TAG, "Exported results to ${zipFile.absolutePath}")
tempZipFile.renameTo(finalZipFile) return zipFile
Log.i(TAG, "Exported results to ${finalZipFile.absolutePath}")
return finalZipFile
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to export results", e) Log.e(TAG, "Failed to export results", e)
tempZipFile.delete()
return null return null
} }
} }
@@ -150,7 +139,7 @@ class ValidationResultManager(
* goldens/ * goldens/
* test_result.png * test_result.png
*/ */
fun exportTestBundle(config: RenderTestConfig, timestamp: String, bundleNameOverride: String? = null): File? { fun exportTestBundle(config: RenderTestConfig, timestamp: String): File? {
Log.i(TAG, "Starting exportTestBundle for ${config.name} at $timestamp") Log.i(TAG, "Starting exportTestBundle for ${config.name} at $timestamp")
Log.i(TAG, "OutputDir: ${outputDir.absolutePath}") Log.i(TAG, "OutputDir: ${outputDir.absolutePath}")
@@ -161,7 +150,8 @@ class ValidationResultManager(
} }
Log.i(TAG, "Using parentDir for export: ${parentDir.absolutePath}") Log.i(TAG, "Using parentDir for export: ${parentDir.absolutePath}")
val exportNameNoSpaces = bundleNameOverride ?: config.name.lowercase(java.util.Locale.US).replace(" ", "_") val testNameWithTimestamp = "${config.name}_$timestamp"
val exportNameNoSpaces = testNameWithTimestamp.replace(" ", "_")
val exportDir = File(parentDir, "export_temp_$timestamp") val exportDir = File(parentDir, "export_temp_$timestamp")
@@ -209,7 +199,7 @@ class ValidationResultManager(
// 3. Create modified config JSON // 3. Create modified config JSON
Log.i(TAG, "Creating config.json...") Log.i(TAG, "Creating config.json...")
val newConfigJson = JSONObject() val newConfigJson = JSONObject()
newConfigJson.put("name", config.name) // Keep spaces in JSON name newConfigJson.put("name", testNameWithTimestamp) // Keep spaces in JSON name
// Reconstruct backends // Reconstruct backends
val backendsArray = JSONArray() val backendsArray = JSONArray()
@@ -245,7 +235,7 @@ class ValidationResultManager(
// Backends (optional override) // Backends (optional override)
val testBackends = JSONArray() val testBackends = JSONArray()
testBackends.put(test.backend) test.backends.forEach { testBackends.put(it) }
testJson.put("backends", testBackends) testJson.put("backends", testBackends)
testsArray.put(testJson) testsArray.put(testJson)
@@ -256,11 +246,10 @@ class ValidationResultManager(
File(rootDir, "config.json").writeText(newConfigJson.toString(4)) File(rootDir, "config.json").writeText(newConfigJson.toString(4))
// 4. Zip it // 4. Zip it
val tempZipFile = File(parentDir, "$exportNameNoSpaces.zip.tmp") val zipFile = File(parentDir, "$exportNameNoSpaces.zip")
val finalZipFile = File(parentDir, "$exportNameNoSpaces.zip") Log.i(TAG, "Zipping to ${zipFile.absolutePath}...")
Log.i(TAG, "Zipping to ${tempZipFile.absolutePath}...")
ZipOutputStream(FileOutputStream(tempZipFile)).use { zos -> ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
rootDir.walkTopDown().forEach { file -> rootDir.walkTopDown().forEach { file ->
if (file.isFile) { if (file.isFile) {
val entryName = file.relativeTo(exportDir).path val entryName = file.relativeTo(exportDir).path
@@ -271,18 +260,15 @@ class ValidationResultManager(
} }
} }
tempZipFile.renameTo(finalZipFile)
// Cleanup temp dir // Cleanup temp dir
exportDir.deleteRecursively() exportDir.deleteRecursively()
Log.i(TAG, "Exported test bundle to ${finalZipFile.absolutePath}") Log.i(TAG, "Exported test bundle to ${zipFile.absolutePath}")
return finalZipFile return zipFile
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to export test bundle", e) Log.e(TAG, "Failed to export test bundle", e)
exportDir.deleteRecursively() exportDir.deleteRecursively()
File(parentDir, "$exportNameNoSpaces.zip.tmp").delete()
return null return null
} }
} }
@@ -291,10 +277,9 @@ class ValidationResultManager(
val rootObject = JSONObject() val rootObject = JSONObject()
val metadataObject = JSONObject() val metadataObject = JSONObject()
metadataObject.put("gpu_driver_info", gpuDriverInfos) metadataObject.put("gpu_driver_info", gpuDriverInfo ?: "")
metadataObject.put("total_time_ms", totalTimeMs) metadataObject.put("total_time_ms", totalTimeMs)
metadataObject.put("device_name", deviceName ?: "") metadataObject.put("device_name", deviceName ?: "")
metadataObject.put("device_hardware", deviceHardware ?: "")
metadataObject.put("device_code_name", deviceCodeName ?: "") metadataObject.put("device_code_name", deviceCodeName ?: "")
metadataObject.put("android_version", androidVersion ?: "") metadataObject.put("android_version", androidVersion ?: "")
metadataObject.put("android_build_number", androidBuildNumber ?: "") metadataObject.put("android_build_number", androidBuildNumber ?: "")
@@ -29,10 +29,9 @@ import java.nio.ByteBuffer
class ValidationRunner( class ValidationRunner(
private val context: Context, private val context: Context,
private val surfaceView: android.view.SurfaceView, private val modelViewer: ModelViewer,
private val config: RenderTestConfig, private val config: RenderTestConfig,
private val resultManager: ValidationResultManager, private val resultManager: ValidationResultManager
private val backendFilter: String = "both" // "gles", "vulkan", or "both"
) { ) {
private var currentState = State.IDLE private var currentState = State.IDLE
@@ -41,47 +40,9 @@ class ValidationRunner(
private var currentEngine: AutomationEngine? = null private var currentEngine: AutomationEngine? = null
private var currentTestConfig: TestConfig? = null private var currentTestConfig: TestConfig? = null
private var currentModelName: String? = null private var currentModelName: String? = null
private var currentBackend: String? = null
private var frameCounter = 0 private var frameCounter = 0
private var suiteStartTime: Long = 0 private var suiteStartTime: Long = 0
private var modelViewer: ModelViewer? = null
private val sortedTests: List<Pair<TestConfig, String>> by lazy {
val filtered = config.tests.filter {
when (backendFilter) {
"gles" -> it.backend == "opengl"
"vulkan" -> it.backend == "vulkan"
else -> true
}
}
val expanded = mutableListOf<Pair<TestConfig, String>>()
filtered.forEach { test ->
test.models.forEach { model ->
expanded.add(test to model)
}
}
// Sort by backend then model. This tries to minimize the number of times we need to
// recreate the Engine, and then the ModelViewer.
val sorted = expanded.sortedWith(compareBy({ it.first.backend }, { it.second }))
val filter = testFilter
if (filter != null && filter.count { it == '.' } == 2) {
val parts = filter.split(".")
val nameRegex = ("^" + parts[0].replace("*", ".*") + "$").toRegex()
val backendRegex = ("^" + parts[1].replace("*", ".*") + "$").toRegex()
val modelRegex = ("^" + parts[2].replace("*", ".*") + "$").toRegex()
sorted.filter { (test, model) ->
val testBackend = test.backend ?: "opengl"
test.name.matches(nameRegex) && testBackend.matches(backendRegex) && model.matches(modelRegex)
}
} else {
sorted
}
}
enum class State { enum class State {
IDLE, IDLE,
@@ -91,80 +52,42 @@ class ValidationRunner(
} }
interface Callback { interface Callback {
fun onTestProgress(current: Int, total: Int)
fun onTestFinished(result: ValidationResult) fun onTestFinished(result: ValidationResult)
fun onAllTestsFinished() fun onAllTestsFinished()
fun onStatusChanged(status: String) fun onStatusChanged(status: String)
fun onImageResult(type: String, bitmap: Bitmap) fun onImageResult(type: String, bitmap: Bitmap)
fun onModelViewerRecreated(modelViewer: ModelViewer?)
} }
var callback: Callback? = null var callback: Callback? = null
var generateGoldens: Boolean = false var generateGoldens: Boolean = false
var testFilter: String? = null
fun start() { fun start() {
if (sortedTests.isEmpty()) { if (config.tests.isEmpty()) {
callback?.onAllTestsFinished() callback?.onAllTestsFinished()
return return
} }
suiteStartTime = System.currentTimeMillis() suiteStartTime = System.currentTimeMillis()
currentBackend = null
currentTestIndex = 0 currentTestIndex = 0
runTest(sortedTests[0]) currentModelIndex = 0
startTest(config.tests[0])
} }
private fun runTest(testPair: Pair<TestConfig, String>) { private fun startTest(test: TestConfig) {
callback?.onTestProgress(currentTestIndex, sortedTests.size)
val (test, modelName) = testPair
currentTestConfig = test currentTestConfig = test
if (test.models.isEmpty()) {
if (currentBackend != test.backend) { nextTest()
currentBackend = test.backend return
callback?.onStatusChanged("Switching to backend: ${test.backend}")
recreateEngine(test.backend)
// After recreation, we need to reload the model
currentModelName = null
} }
currentModelIndex = 0
startModel(modelName) startModel(test.models.elementAt(0))
}
public fun cleanup() {
// When the runner ends, we need to destroy the resources to ensure that they are free for
// another run.
modelViewer?.destroy()
}
private fun recreateEngine(backendName: String) {
val backend = when (backendName.lowercase()) {
"vulkan" -> com.google.android.filament.Engine.Backend.VULKAN
else -> com.google.android.filament.Engine.Backend.OPENGL
}
// ModelViewer's detach listener will handle engine destruction when surface is detached,
// but here we are still using the same surface. We need a way to swap the engine.
// The easiest way is to recreate ModelViewer with a new Engine.
modelViewer?.destroy()
val engine = com.google.android.filament.Engine.create(backend)
resultManager.addGpuDriverInfo(backendName, com.google.android.filament.utils.DeviceUtils.getGpuDriverInfo(engine))
// Setting the camera manipulator to null enables changing the camera outside of ModelViewer
val newModelViewer = ModelViewer(surfaceView, engine, manipulator=null)
// Update the reference in MainActivity (via callback)
callback?.onModelViewerRecreated(newModelViewer)
modelViewer = newModelViewer
} }
private fun startModel(modelName: String) { private fun startModel(modelName: String) {
if (currentModelName == modelName) { if (currentModelName == modelName) {
Log.i("ValidationRunner", "Reusing model $modelName") Log.i("ValidationRunner", "Reusing model $modelName")
callback?.onStatusChanged("Reusing $modelName for ${currentTestConfig?.name} (${currentTestConfig?.backend})") callback?.onStatusChanged("Reusing $modelName for ${currentTestConfig?.name}")
modelViewer?.resetToDefaultState() modelViewer.resetToDefaultState()
frameCounter = 0 frameCounter = 0
currentState = State.WARMUP currentState = State.WARMUP
@@ -175,10 +98,10 @@ class ValidationRunner(
val modelPath = config.models[modelName] val modelPath = config.models[modelName]
if (modelPath == null) { if (modelPath == null) {
Log.e("ValidationRunner", "Model $modelName not found") Log.e("ValidationRunner", "Model $modelName not found")
nextTest() nextModel()
return return
} }
callback?.onStatusChanged("Loading $modelName for ${currentTestConfig?.name} (${currentTestConfig?.backend})") callback?.onStatusChanged("Loading $modelName for ${currentTestConfig?.name}")
// Load model on main thread (required by ModelViewer) // Load model on main thread (required by ModelViewer)
loadModel(modelPath) loadModel(modelPath)
@@ -186,21 +109,21 @@ class ValidationRunner(
private fun loadModel(path: String) { private fun loadModel(path: String) {
// Assume called on Main Thread // Assume called on Main Thread
modelViewer?.destroyModel() modelViewer.destroyModel()
try { try {
Log.i("ValidationRunner", "Reading model file: $path") Log.i("ValidationRunner", "Reading model file: $path")
val bytes = File(path).readBytes() val bytes = File(path).readBytes()
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.")
modelViewer?.transformToUnitCube() modelViewer.transformToUnitCube()
currentState = State.WAITING_FOR_RESOURCES currentState = State.WAITING_FOR_RESOURCES
frameCounter = 0 frameCounter = 0
Log.i("ValidationRunner", "State set to WAITING_FOR_RESOURCES") Log.i("ValidationRunner", "State set to WAITING_FOR_RESOURCES")
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ValidationRunner", "Failed to load $path", e) Log.e("ValidationRunner", "Failed to load $path", e)
nextTest() nextModel()
} }
} }
@@ -208,72 +131,43 @@ class ValidationRunner(
when (currentState) { when (currentState) {
State.IDLE -> {} State.IDLE -> {}
State.WAITING_FOR_RESOURCES -> { State.WAITING_FOR_RESOURCES -> {
modelViewer?.let { mv -> val progress = modelViewer.progress
val progress = mv.progress if (progress >= 1.0f) {
if (progress >= 1.0f) { Log.i("ValidationRunner", "Resources loaded. Starting warmup.")
Log.i("ValidationRunner", "Resources loaded. Starting warmup.") frameCounter = 0
frameCounter = 0 currentState = State.WARMUP
currentState = State.WARMUP }
}
}
} }
State.WARMUP -> { State.WARMUP -> {
frameCounter++ frameCounter++
if (frameCounter > 1) { // 1 frames warmup if (frameCounter > 5) { // 5 frames warmup
startAutomation() startAutomation()
} }
} }
State.RUNNING_TEST -> { State.RUNNING_TEST -> {
currentEngine?.let { engine -> currentEngine?.let { engine ->
modelViewer?.let { mv -> val content = AutomationEngine.ViewerContent()
val content = AutomationEngine.ViewerContent() content.view = modelViewer.view
content.view = mv.view content.renderer = modelViewer.renderer
content.renderer = mv.renderer content.scene = modelViewer.scene
content.scene = mv.scene content.lightManager = modelViewer.engine.lightManager
content.lightManager = mv.engine.lightManager
// Tick
val deltaTime = 1.0f / 60.0f
engine.tick(mv.engine, content, deltaTime)
applyAnimation(mv, currentTestConfig!!.rendering) // Tick
val deltaTime = 1.0f / 60.0f
engine.tick(modelViewer.engine, content, deltaTime)
frameCounter++ frameCounter++
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.IDLE
captureAndCompare() captureAndCompare()
}
} }
} }
} }
} }
} }
private fun applyAnimation(mv: com.google.android.filament.utils.ModelViewer, rendering: JSONObject) {
val animator = mv.animator ?: return
if (animator.animationCount == 0) return
var enabled = false
var time = 0.0f
if (rendering.has("animation")) {
val anim = rendering.optJSONObject("animation")
if (anim != null) {
enabled = anim.optBoolean("enabled", false)
time = anim.optDouble("time", 0.0).toFloat()
}
} else {
enabled = rendering.optBoolean("animation.enabled", false)
time = rendering.optDouble("animation.time", 0.0).toFloat()
}
if (enabled) {
animator.applyAnimation(0, time)
animator.updateBoneMatrices()
}
}
private fun startAutomation() { private fun startAutomation() {
val test = currentTestConfig!! val test = currentTestConfig!!
val specJson = JSONObject() val specJson = JSONObject()
@@ -284,27 +178,21 @@ class ValidationRunner(
currentEngine = AutomationEngine(fullSpec) currentEngine = AutomationEngine(fullSpec)
val options = AutomationEngine.Options() val options = AutomationEngine.Options()
options.sleepDuration = 0.0f // Minimal sleep, let frames drive it options.sleepDuration = 0.0f // Minimal sleep, let frames drive it
options.minFrameCount = 15 // Ensure enough frames pass for temporal accumulation options.minFrameCount = 5 // Ensure some frames pass
currentEngine?.setOptions(options) currentEngine?.setOptions(options)
// Use batch mode to ensure shouldClose() works reliably // Use batch mode to ensure shouldClose() works reliably
currentEngine?.startBatchMode() currentEngine?.startBatchMode()
currentEngine?.signalBatchMode() // Start immediately currentEngine?.signalBatchMode() // Start immediately
// Clear the frame history so that temporal effects (like SSR or TAA) don't bleed over
// or ghost from the visual output of the previous test.
modelViewer?.let { mv ->
mv.view.clearFrameHistory(mv.engine)
}
frameCounter = 0 frameCounter = 0
currentState = State.RUNNING_TEST currentState = State.RUNNING_TEST
} }
private fun captureAndCompare() { private fun captureAndCompare() {
callback?.onStatusChanged("Comparing ${currentTestConfig?.name} (${currentTestConfig?.backend})...") callback?.onStatusChanged("Comparing ${currentTestConfig?.name}...")
modelViewer?.debugGetNextFrameCallback { bitmap -> modelViewer.debugGetNextFrameCallback { bitmap ->
compareCapturedImage(bitmap) compareCapturedImage(bitmap)
} }
} }
@@ -312,7 +200,7 @@ class ValidationRunner(
private fun compareCapturedImage(bitmap: Bitmap) { private fun compareCapturedImage(bitmap: Bitmap) {
val testName = currentTestConfig!!.name val testName = currentTestConfig!!.name
val modelName = currentModelName!! val modelName = currentModelName!!
val backend = currentTestConfig?.backend ?: "opengl" val backend = currentTestConfig?.backends?.firstOrNull() ?: "opengl"
val testFullName = "${testName}.${backend}.${modelName}" val testFullName = "${testName}.${backend}.${modelName}"
// Golden path // Golden path
@@ -344,6 +232,8 @@ class ValidationRunner(
Log.i("ValidationRunner", "Found golden at ${goldenFile.absolutePath}") Log.i("ValidationRunner", "Found golden at ${goldenFile.absolutePath}")
} else { } else {
Log.w("ValidationRunner", "Golden not found for $testFullName. Searched in: ${searchPaths.joinToString { it.absolutePath }}") 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 {
@@ -422,12 +312,9 @@ class ValidationRunner(
alphaDiffImg.setPixels(alphaPixels, 0, width, 0, 0, width, height) alphaDiffImg.setPixels(alphaPixels, 0, width, 0, 0, width, height)
callback?.onImageResult("Alpha Diff", alphaDiffImg) callback?.onImageResult("Alpha Diff", alphaDiffImg)
resultManager.saveImage("${testFullName}_alpha_diff", alphaDiffImg) resultManager.saveImage("${testFullName}_alpha_diff", alphaDiffImg)
alphaDiffImg.recycle()
} }
diffImg.recycle()
} }
} }
golden.recycle()
} else { } else {
callback?.onStatusChanged("Failed to load golden") callback?.onStatusChanged("Failed to load golden")
} }
@@ -440,27 +327,35 @@ class ValidationRunner(
// Save output // Save output
resultManager.saveImage(testFullName, flipped) resultManager.saveImage(testFullName, flipped)
val result = ValidationResult(testFullName, passed, diffMetric, goldenFile?.absolutePath) val result = ValidationResult(testFullName, passed, diffMetric)
resultManager.addResult(result) resultManager.addResult(result)
callback?.onTestFinished(result) callback?.onTestFinished(result)
android.os.Handler(android.os.Looper.getMainLooper()).post { android.os.Handler(android.os.Looper.getMainLooper()).post {
nextTest() nextModel()
} }
} catch (e: Throwable) { } catch (e: Exception) {
Log.e("ValidationRunner", "Comparison failed", e) Log.e("ValidationRunner", "Comparison failed", e)
// Recycle even on error android.os.Handler(android.os.Looper.getMainLooper()).post { nextModel() }
bitmap.recycle()
android.os.Handler(android.os.Looper.getMainLooper()).post { nextTest() }
} }
}.start() }.start()
} }
private fun nextModel() {
currentModelIndex++
if (currentTestConfig != null && currentModelIndex < currentTestConfig!!.models.size) {
startModel(currentTestConfig!!.models.elementAt(currentModelIndex))
} else {
nextTest()
}
}
private fun nextTest() { private fun nextTest() {
currentTestIndex++ currentTestIndex++
if (currentTestIndex < sortedTests.size) { if (currentTestIndex < config.tests.size) {
runTest(sortedTests[currentTestIndex]) startTest(config.tests[currentTestIndex])
} else { } else {
currentState = State.IDLE currentState = State.IDLE
@@ -27,8 +27,6 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center_horizontal"
android:clipChildren="false"
android:paddingStart="20dp" android:paddingStart="20dp"
android:paddingEnd="20dp" android:paddingEnd="20dp"
android:paddingTop="20dp" android:paddingTop="20dp"
@@ -84,53 +82,6 @@
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>
</LinearLayout> </LinearLayout>
<!-- Backend Filter (Hidden by default) -->
<LinearLayout
android:id="@+id/backend_filter_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:visibility="gone"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Backend:"
android:textSize="12sp"
android:minWidth="120dp" />
<RadioGroup
android:id="@+id/backend_radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/radio_both"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Both"
android:textSize="12sp"
android:checked="true" />
<RadioButton
android:id="@+id/radio_gles"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="GLES"
android:textSize="12sp" />
<RadioButton
android:id="@+id/radio_vulkan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Vulkan"
android:textSize="12sp" />
</RadioGroup>
</LinearLayout>
<!-- Global Enhancement Controls (Hidden by default) --> <!-- Global Enhancement Controls (Hidden by default) -->
<LinearLayout <LinearLayout
android:id="@+id/enhancement_container" android:id="@+id/enhancement_container"
@@ -163,57 +114,15 @@
android:id="@+id/test_results_header" android:id="@+id/test_results_header"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="Test Results" android:text="Test Results"
android:gravity="center"
android:textSize="14sp" android:textSize="14sp"
android:paddingTop="0dp" android:paddingTop="0dp"
android:paddingBottom="12dp" /> android:paddingBottom="8dp" />
<FrameLayout
android:id="@+id/progress_container"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:clipChildren="false"
android:visibility="gone">
<com.google.android.filament.validation.TestProgressBar
android:id="@+id/test_progress"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_marginBottom="8dp" />
<ImageView
android:id="@+id/progress_triangle"
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_gravity="bottom"
android:layout_marginTop="4dp"
android:scaleType="fitXY"
android:src="@drawable/ic_triangle_up" />
</FrameLayout>
<TextView
android:id="@+id/test_summary_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:layout_marginBottom="12dp"
android:visibility="gone" />
<TextView
android:id="@+id/device_info_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:layout_marginBottom="12dp"
android:visibility="gone" />
</LinearLayout> </LinearLayout>
<ScrollView <ScrollView
android:id="@+id/scroll_view"
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/controls_container"
@@ -4,6 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:background="#FF202020"
android:padding="8dp"> android:padding="8dp">
<!-- Header with title and close button --> <!-- Header with title and close button -->
@@ -19,14 +20,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="Test Result" android:text="Test Result"
android:textAppearance="?attr/textAppearanceTitleLarge" android:textColor="#FFFFFF"
android:textSize="16sp"
android:textStyle="bold" /> android:textStyle="bold" />
<ImageButton <ImageButton
android:id="@+id/btn_close" android:id="@+id/btn_close"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
app:tint="?attr/colorOnSurface"
android:src="@android:drawable/ic_menu_close_clear_cancel" /> android:src="@android:drawable/ic_menu_close_clear_cancel" />
</LinearLayout> </LinearLayout>
@@ -37,8 +38,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="start" android:gravity="start"
android:text="Rendered" android:text="Rendered"
android:textAppearance="?attr/textAppearanceBodyMedium" android:textColor="#BBBBBB"
android:textColor="?android:attr/textColorSecondary" android:textSize="14sp"
android:paddingBottom="8dp" /> android:paddingBottom="8dp" />
<!-- Image Area with Arrows --> <!-- Image Area with Arrows -->
@@ -77,7 +78,7 @@
android:layout_height="64dp" android:layout_height="64dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_media_previous" android:src="@android:drawable/ic_media_previous"
app:tint="?attr/colorOnSurface" /> app:tint="#FFFFFF" />
<ImageButton <ImageButton
android:id="@+id/btn_reset" android:id="@+id/btn_reset"
@@ -86,7 +87,7 @@
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_revert" android:src="@android:drawable/ic_menu_revert"
app:tint="?attr/colorOnSurface" /> app:tint="#FFFFFF" />
<ImageButton <ImageButton
android:id="@+id/btn_next" android:id="@+id/btn_next"
@@ -94,7 +95,7 @@
android:layout_height="64dp" android:layout_height="64dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_media_next" android:src="@android:drawable/ic_media_next"
app:tint="?attr/colorOnSurface" /> app:tint="#FFFFFF" />
</LinearLayout> </LinearLayout>
<!-- Enhancement Controls (only for diff images) --> <!-- Enhancement Controls (only for diff images) -->
@@ -113,7 +114,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Enhance: 1.0x" android:text="Enhance: 1.0x"
android:textAppearance="?attr/textAppearanceBodySmall" android:textColor="#FFFFFF"
android:textSize="12sp"
android:minWidth="100dp" /> android:minWidth="100dp" />
<SeekBar <SeekBar
Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

+13 -12
View File
@@ -204,9 +204,10 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.13", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -234,9 +235,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.5", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@@ -409,9 +410,9 @@
"integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==" "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.13", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -428,9 +429,9 @@
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==" "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="
}, },
"minimatch": { "minimatch": {
"version": "3.1.5", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
-18
View File
@@ -81,8 +81,6 @@ in table [standardProperties].
**roughness** | Perceived smoothness (1.0) or roughness (0.0) of a surface. Smooth surfaces exhibit sharp reflections **roughness** | Perceived smoothness (1.0) or roughness (0.0) of a surface. Smooth surfaces exhibit sharp reflections
**metallic** | Whether a surface appears to be dielectric (0.0) or conductor (1.0). Often used as a binary value (0 or 1) **metallic** | Whether a surface appears to be dielectric (0.0) or conductor (1.0). Often used as a binary value (0 or 1)
**reflectance** | Fresnel reflectance at normal incidence for dielectric surfaces. This directly controls the strength of the reflections **reflectance** | Fresnel reflectance at normal incidence for dielectric surfaces. This directly controls the strength of the reflections
**specularFactor** | Scales the amount of specular reflection for non-metallic surfaces
**specularColorFactor** | Color of the specular reflection for non-metallic surfaces
**ambientOcclusion** | Defines how much of the ambient light is accessible to a surface point. It is a per-pixel shadowing factor between 0.0 and 1.0 **ambientOcclusion** | Defines how much of the ambient light is accessible to a surface point. It is a per-pixel shadowing factor between 0.0 and 1.0
**clearCoat** | Strength of the clear coat layer **clearCoat** | Strength of the clear coat layer
**clearCoatRoughness** | Perceived smoothness or roughness of the clear coat layer **clearCoatRoughness** | Perceived smoothness or roughness of the clear coat layer
@@ -112,8 +110,6 @@ The type and range of each property is described in table [standardPropertiesTyp
**metallic** | float | [0..1] | Should be 0 or 1 **metallic** | float | [0..1] | Should be 0 or 1
**roughness** | float | [0..1] | **roughness** | float | [0..1] |
**reflectance** | float | [0..1] | Prefer values > 0.35 **reflectance** | float | [0..1] | Prefer values > 0.35
**specularFactor** | float | [0..1] | Optional, defaults to 1.0
**specularColorFactor** | float3 | [0..1] | Linear RGB, defaults to 1.0
**sheenColor** | float3 | [0..1] | Linear RGB **sheenColor** | float3 | [0..1] | Linear RGB
**sheenRoughness** | float | [0..1] | **sheenRoughness** | float | [0..1] |
**clearCoat** | float | [0..1] | Should be 0 or 1 **clearCoat** | float | [0..1] | Should be 0 or 1
@@ -308,20 +304,6 @@ it easy to define from a texture.
See section [Index of refraction] for more information about the `ior` property and refractive See section [Index of refraction] for more information about the `ior` property and refractive
indices. indices.
### Specular factor
The `specularFactor` property scales the amount of specular reflection for non-metallic surfaces.
This property is often used to implement the `KHR_materials_specular` glTF extension. This value
scales the Fresnel reflectance at normal incidence (F0), which is also computed from the
`reflectance` property. It also sets the Fresnel reflectance at grazing angles (F90).
### Specular color factor
The `specularColorFactor` property defines the color of the specular reflection for non-metallic
surfaces. This property is often used to implement the `KHR_materials_specular` glTF extension. This
value scales the Fresnel reflectance at normal incidence (F0), which is also computed from the
`reflectance` property.
### Sheen color ### Sheen color
The sheen color controls the color appearance and strength of an optional sheen layer on top of the The sheen color controls the color appearance and strength of an optional sheen layer on top of the
+1 -1
View File
@@ -163,7 +163,7 @@
<p>To build Filament, you must first install the following tools:</p> <p>To build Filament, you must first install the following tools:</p>
<ul> <ul>
<li>CMake 3.22.1 (or more recent)</li> <li>CMake 3.22.1 (or more recent)</li>
<li>clang 16.0 (or more recent) (Required for Linux and macOS; see <a href="#windows">Windows</a> section for MSVC support)</li> <li>clang 16.0 (or more recent)</li>
<li><a href="https://github.com/ninja-build/ninja/wiki/Pre-built-Ninja-packages">ninja 1.10</a> (or more recent)</li> <li><a href="https://github.com/ninja-build/ninja/wiki/Pre-built-Ninja-packages">ninja 1.10</a> (or more recent)</li>
</ul> </ul>
<p>Additional dependencies may be required for your operating system. Please refer to the appropriate <p>Additional dependencies may be required for your operating system. Please refer to the appropriate
+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.71.0' implementation 'com.google.android.filament:filament-android:1.70.1'
} }
</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.71.0' <pre><code class="language-shell">pod 'Filament', '~&gt; 1.70.1'
</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>
+2 -2
View File
@@ -319,7 +319,7 @@ the run number is <code>18023632663</code>.</p>
<i class="fa fa-angle-left"></i> <i class="fa fa-angle-left"></i>
</a> </a>
<a rel="next prefetch" href="../dup/test_ci_sizeguard.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right"> <a rel="next prefetch" href="../notes/libs.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
</a> </a>
@@ -333,7 +333,7 @@ the run number is <code>18023632663</code>.</p>
<i class="fa fa-angle-left"></i> <i class="fa fa-angle-left"></i>
</a> </a>
<a rel="next prefetch" href="../dup/test_ci_sizeguard.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right"> <a rel="next prefetch" href="../notes/libs.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
</a> </a>
</nav> </nav>
-240
View File
@@ -1,240 +0,0 @@
<!DOCTYPE HTML>
<html lang="en" class="light sidebar-visible" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>CI: sizeguard - Filament</title>
<!-- Custom HTML head -->
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
<!-- MathJax -->
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<!-- Provide site root to javascript -->
<script>
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "light" : "light";
</script>
<!-- Start loading toc.js asap -->
<script src="../toc.js"></script>
</head>
<body>
<div id="body-container">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
const html = document.documentElement;
html.classList.remove('light')
html.classList.add(theme);
html.classList.add("js");
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div style="display:flex;align-items:center;justify-content:center">
<img class="flogo" src="../images/filament_logo_small.png"></img>
</div>
<!-- populated by js -->
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
<noscript>
<iframe class="sidebar-iframe-outer" src="../toc.html"></iframe>
</noscript>
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<!-- Filament: disable themes because the markdeep part does not look good for dark themes -->
<!--
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
-->
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Filament</h1>
<div class="right-buttons">
<a href="https://github.com/google/filament" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="sizeguard"><a class="header" href="#sizeguard">Sizeguard</a></h1>
<p>This directory contains scripts used to monitor and gate the size of Filament artifacts.</p>
<h2 id="scripts"><a class="header" href="#scripts">Scripts</a></h2>
<h3 id="dump_artifact_sizepy"><a class="header" href="#dump_artifact_sizepy"><code>dump_artifact_size.py</code></a></h3>
<p>Computes the sizes of build artifacts (e.g., <code>.aar</code>, <code>.tgz</code>) and their internal contents. It outputs a JSON representation of these sizes.</p>
<p><strong>Usage:</strong></p>
<pre><code class="language-bash">python3 dump_artifact_size.py out/*.aar &gt; current_size.json
</code></pre>
<h3 id="check_sizepy"><a class="header" href="#check_sizepy"><code>check_size.py</code></a></h3>
<p>Compares a current size JSON (generated by <code>dump_artifact_size.py</code>) against historical data stored in the <code>filament-assets</code> repository. It fails if any artifact's size increase exceeds a specified threshold.</p>
<p><strong>Key Arguments:</strong></p>
<ul>
<li><code>current_json</code>: Path to the local JSON file.</li>
<li><code>--threshold</code>: Size increase threshold in bytes (default: 20KB).</li>
<li><code>--bypass</code>: If provided, the script will print the comparison but exit successfully even if thresholds are exceeded.</li>
</ul>
<h3 id="check_bypasspy"><a class="header" href="#check_bypasspy"><code>check_bypass.py</code></a></h3>
<p>A utility script that checks the commit message for a specific tag to determine if the sizeguard check should be bypassed.</p>
<p><strong>Usage:</strong></p>
<ul>
<li>Returns exit code <code>0</code> if the tag <code>SIZEGUARD_BYPASS</code> is found in the commit message.</li>
<li>Returns exit code <code>1</code> otherwise.</li>
</ul>
<h2 id="continuous-integration"><a class="header" href="#continuous-integration">Continuous Integration</a></h2>
<p>These scripts are integrated into the GitHub Actions workflows (e.g., <code>.github/workflows/presubmit.yml</code>).</p>
<p>To bypass a failing sizeguard check in a PR, add the following tag on a new line in your commit message:</p>
<pre><code>SIZEGUARD_BYPASS
</code></pre>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../dup/test_ci_renderdiff.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../notes/libs.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../dup/test_ci_renderdiff.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../notes/libs.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script>
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js"></script>
<script src="../mark.min.js"></script>
<script src="../searcher.js"></script>
<script src="../clipboard.min.js"></script>
<script src="../highlight.js"></script>
<script src="../book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>
+3 -3
View File
@@ -173,10 +173,10 @@
<p>The root object contains the following categories:</p> <p>The root object contains the following categories:</p>
<div class="table-wrapper"><table><thead><tr><th style="text-align: left">Key</th><th style="text-align: left">Type</th><th style="text-align: left">Description</th></tr></thead><tbody> <div class="table-wrapper"><table><thead><tr><th style="text-align: left">Key</th><th style="text-align: left">Type</th><th style="text-align: left">Description</th></tr></thead><tbody>
<tr><td style="text-align: left"><code>view</code></td><td style="text-align: left">Object</td><td style="text-align: left">Post-processing and rendering quality settings.</td></tr> <tr><td style="text-align: left"><code>view</code></td><td style="text-align: left">Object</td><td style="text-align: left">Post-processing and rendering quality settings.</td></tr>
<tr><td style="text-align: left"><code>camera</code></td><td style="text-align: left">Object</td><td style="text-align: left">Explicit camera control (pose, projection, exposure).</td></tr> <tr><td style="text-align: left"><code>camera</code></td><td style="text-align: left">Object</td><td style="text-align: left"><strong>[NEW]</strong> Explicit camera control (pose, projection, exposure).</td></tr>
<tr><td style="text-align: left"><code>lighting</code></td><td style="text-align: left">Object</td><td style="text-align: left">Environment and dynamic light settings.</td></tr> <tr><td style="text-align: left"><code>lighting</code></td><td style="text-align: left">Object</td><td style="text-align: left"><strong>[NEW]</strong> Environment and dynamic light settings.</td></tr>
<tr><td style="text-align: left"><code>viewer</code></td><td style="text-align: left">Object</td><td style="text-align: left">Global viewer options (skybox, background, scaling).</td></tr> <tr><td style="text-align: left"><code>viewer</code></td><td style="text-align: left">Object</td><td style="text-align: left">Global viewer options (skybox, background, scaling).</td></tr>
<tr><td style="text-align: left"><code>animation</code></td><td style="text-align: left">Object</td><td style="text-align: left">Animation playback control.</td></tr> <tr><td style="text-align: left"><code>animation</code></td><td style="text-align: left">Object</td><td style="text-align: left"><strong>[NEW]</strong> Animation playback control.</td></tr>
<tr><td style="text-align: left"><code>material</code></td><td style="text-align: left">Object</td><td style="text-align: left">Material overrides.</td></tr> <tr><td style="text-align: left"><code>material</code></td><td style="text-align: left">Object</td><td style="text-align: left">Material overrides.</td></tr>
</tbody></table> </tbody></table>
</div> </div>
+35 -55
View File
@@ -198,25 +198,23 @@ counter-increment: h6;margin-right:10px}
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/metals" class="level3"><span class="tocNumber">3.1.5&nbsp; </span>Metals</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/metals" class="level3"><span class="tocNumber">3.1.5&nbsp; </span>Metals</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/refraction" class="level3"><span class="tocNumber">3.1.6&nbsp; </span>Refraction</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/refraction" class="level3"><span class="tocNumber">3.1.6&nbsp; </span>Refraction</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/reflectance" class="level3"><span class="tocNumber">3.1.7&nbsp; </span>Reflectance</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/reflectance" class="level3"><span class="tocNumber">3.1.7&nbsp; </span>Reflectance</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/specularfactor" class="level3"><span class="tocNumber">3.1.8&nbsp; </span>Specular factor</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/sheencolor" class="level3"><span class="tocNumber">3.1.8&nbsp; </span>Sheen color</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/specularcolorfactor" class="level3"><span class="tocNumber">3.1.9&nbsp; </span>Specular color factor</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/sheenroughness" class="level3"><span class="tocNumber">3.1.9&nbsp; </span>Sheen roughness</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/sheencolor" class="level3"><span class="tocNumber">3.1.10&nbsp; </span>Sheen color</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/clearcoat" class="level3"><span class="tocNumber">3.1.10&nbsp; </span>Clear coat</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/sheenroughness" class="level3"><span class="tocNumber">3.1.11&nbsp; </span>Sheen roughness</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/clearcoatroughness" class="level3"><span class="tocNumber">3.1.11&nbsp; </span>Clear coat roughness</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/clearcoat" class="level3"><span class="tocNumber">3.1.12&nbsp; </span>Clear coat</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/anisotropy" class="level3"><span class="tocNumber">3.1.12&nbsp; </span>Anisotropy</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/clearcoatroughness" class="level3"><span class="tocNumber">3.1.13&nbsp; </span>Clear coat roughness</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/anisotropydirection" class="level3"><span class="tocNumber">3.1.13&nbsp; </span>Anisotropy direction</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/anisotropy" class="level3"><span class="tocNumber">3.1.14&nbsp; </span>Anisotropy</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/ambientocclusion" class="level3"><span class="tocNumber">3.1.14&nbsp; </span>Ambient occlusion</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/anisotropydirection" class="level3"><span class="tocNumber">3.1.15&nbsp; </span>Anisotropy direction</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/normal" class="level3"><span class="tocNumber">3.1.15&nbsp; </span>Normal</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/ambientocclusion" class="level3"><span class="tocNumber">3.1.16&nbsp; </span>Ambient occlusion</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/bentnormal" class="level3"><span class="tocNumber">3.1.16&nbsp; </span>Bent normal</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/normal" class="level3"><span class="tocNumber">3.1.17&nbsp; </span>Normal</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/clearcoatnormal" class="level3"><span class="tocNumber">3.1.17&nbsp; </span>Clear coat normal</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/bentnormal" class="level3"><span class="tocNumber">3.1.18&nbsp; </span>Bent normal</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/emissive" class="level3"><span class="tocNumber">3.1.18&nbsp; </span>Emissive</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/clearcoatnormal" class="level3"><span class="tocNumber">3.1.19&nbsp; </span>Clear coat normal</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/post-lightingcolor" class="level3"><span class="tocNumber">3.1.19&nbsp; </span>Post-lighting color</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/emissive" class="level3"><span class="tocNumber">3.1.20&nbsp; </span>Emissive</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/indexofrefraction" class="level3"><span class="tocNumber">3.1.20&nbsp; </span>Index of refraction</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/post-lightingcolor" class="level3"><span class="tocNumber">3.1.21&nbsp; </span>Post-lighting color</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/transmission" class="level3"><span class="tocNumber">3.1.21&nbsp; </span>Transmission</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/indexofrefraction" class="level3"><span class="tocNumber">3.1.22&nbsp; </span>Index of refraction</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/absorption" class="level3"><span class="tocNumber">3.1.22&nbsp; </span>Absorption</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/transmission" class="level3"><span class="tocNumber">3.1.23&nbsp; </span>Transmission</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/micro-thicknessandthickness" class="level3"><span class="tocNumber">3.1.23&nbsp; </span>Micro-thickness and thickness</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/absorption" class="level3"><span class="tocNumber">3.1.24&nbsp; </span>Absorption</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/dispersion" class="level3"><span class="tocNumber">3.1.24&nbsp; </span>Dispersion</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/micro-thicknessandthickness" class="level3"><span class="tocNumber">3.1.25&nbsp; </span>Micro-thickness and thickness</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/litmodel/dispersion" class="level3"><span class="tocNumber">3.1.26&nbsp; </span>Dispersion</a><br/>
&nbsp;&nbsp;<a href="#materialmodels/subsurfacemodel" class="level2"><span class="tocNumber">3.2&nbsp; </span>Subsurface model</a><br/> &nbsp;&nbsp;<a href="#materialmodels/subsurfacemodel" class="level2"><span class="tocNumber">3.2&nbsp; </span>Subsurface model</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/subsurfacemodel/thickness" class="level3"><span class="tocNumber">3.2.1&nbsp; </span>Thickness</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/subsurfacemodel/thickness" class="level3"><span class="tocNumber">3.2.1&nbsp; </span>Thickness</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/subsurfacemodel/subsurfacecolor" class="level3"><span class="tocNumber">3.2.2&nbsp; </span>Subsurface color</a><br/> &nbsp;&nbsp;&nbsp;&nbsp;<a href="#materialmodels/subsurfacemodel/subsurfacecolor" class="level3"><span class="tocNumber">3.2.2&nbsp; </span>Subsurface color</a><br/>
@@ -379,8 +377,6 @@ in <a href="#table_standardproperties">table&nbsp;1</a>.
<tr><td style="text-align:right"> <strong class="asterisk">roughness</strong> </td><td style="text-align:left"> Perceived smoothness (1.0) or roughness (0.0) of a surface. Smooth surfaces exhibit sharp reflections </td></tr> <tr><td style="text-align:right"> <strong class="asterisk">roughness</strong> </td><td style="text-align:left"> Perceived smoothness (1.0) or roughness (0.0) of a surface. Smooth surfaces exhibit sharp reflections </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">metallic</strong> </td><td style="text-align:left"> Whether a surface appears to be dielectric (0.0) or conductor (1.0). Often used as a binary value (0 or 1) </td></tr> <tr><td style="text-align:right"> <strong class="asterisk">metallic</strong> </td><td style="text-align:left"> Whether a surface appears to be dielectric (0.0) or conductor (1.0). Often used as a binary value (0 or 1) </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">reflectance</strong> </td><td style="text-align:left"> Fresnel reflectance at normal incidence for dielectric surfaces. This directly controls the strength of the reflections </td></tr> <tr><td style="text-align:right"> <strong class="asterisk">reflectance</strong> </td><td style="text-align:left"> Fresnel reflectance at normal incidence for dielectric surfaces. This directly controls the strength of the reflections </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">specularFactor</strong> </td><td style="text-align:left"> Scales the amount of specular reflection for non-metallic surfaces </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">specularColorFactor</strong> </td><td style="text-align:left"> Color of the specular reflection for non-metallic surfaces </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">ambientOcclusion</strong> </td><td style="text-align:left"> Defines how much of the ambient light is accessible to a surface point. It is a per-pixel shadowing factor between 0.0 and 1.0 </td></tr> <tr><td style="text-align:right"> <strong class="asterisk">ambientOcclusion</strong> </td><td style="text-align:left"> Defines how much of the ambient light is accessible to a surface point. It is a per-pixel shadowing factor between 0.0 and 1.0 </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">clearCoat</strong> </td><td style="text-align:left"> Strength of the clear coat layer </td></tr> <tr><td style="text-align:right"> <strong class="asterisk">clearCoat</strong> </td><td style="text-align:left"> Strength of the clear coat layer </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">clearCoatRoughness</strong> </td><td style="text-align:left"> Perceived smoothness or roughness of the clear coat layer </td></tr> <tr><td style="text-align:right"> <strong class="asterisk">clearCoatRoughness</strong> </td><td style="text-align:left"> Perceived smoothness or roughness of the clear coat layer </td></tr>
@@ -409,8 +405,6 @@ The type and range of each property is described in <a href="#table_standardprop
<tr><td style="text-align:right"> <strong class="asterisk">metallic</strong> </td><td style="text-align:center"> float </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> Should be 0 or 1 </td></tr> <tr><td style="text-align:right"> <strong class="asterisk">metallic</strong> </td><td style="text-align:center"> float </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> Should be 0 or 1 </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">roughness</strong> </td><td style="text-align:center"> float </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> &nbsp; </td></tr> <tr><td style="text-align:right"> <strong class="asterisk">roughness</strong> </td><td style="text-align:center"> float </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> &nbsp; </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">reflectance</strong> </td><td style="text-align:center"> float </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> Prefer values > 0.35 </td></tr> <tr><td style="text-align:right"> <strong class="asterisk">reflectance</strong> </td><td style="text-align:center"> float </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> Prefer values > 0.35 </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">specularFactor</strong> </td><td style="text-align:center"> float </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> Optional, defaults to 1.0 </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">specularColorFactor</strong> </td><td style="text-align:center"> float3 </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> Linear RGB, defaults to 1.0 </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">sheenColor</strong> </td><td style="text-align:center"> float3 </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> Linear RGB </td></tr> <tr><td style="text-align:right"> <strong class="asterisk">sheenColor</strong> </td><td style="text-align:center"> float3 </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> Linear RGB </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">sheenRoughness</strong> </td><td style="text-align:center"> float </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> &nbsp; </td></tr> <tr><td style="text-align:right"> <strong class="asterisk">sheenRoughness</strong> </td><td style="text-align:center"> float </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> &nbsp; </td></tr>
<tr><td style="text-align:right"> <strong class="asterisk">clearCoat</strong> </td><td style="text-align:center"> float </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> Should be 0 or 1 </td></tr> <tr><td style="text-align:right"> <strong class="asterisk">clearCoat</strong> </td><td style="text-align:center"> float </td><td style="text-align:center"> [0..1] </td><td style="text-align:left"> Should be 0 or 1 </td></tr>
@@ -598,24 +592,10 @@ impossible materials, however, this might be desirable for artistic reasons.
The <code>reflectance</code> property is designed as a normalized property in the range 0..1 which makes The <code>reflectance</code> property is designed as a normalized property in the range 0..1 which makes
it easy to define from a texture. it easy to define from a texture.
</p><p> </p><p>
See section <a href="#toc3.1.22">3.1.22</a> for more information about the <code>ior</code> property and refractive See section <a href="#toc3.1.20">3.1.20</a> for more information about the <code>ior</code> property and refractive
indices. indices.
</p> </p>
<a class="target" name="specularfactor">&nbsp;</a><a class="target" name="materialmodels/litmodel/specularfactor">&nbsp;</a><a class="target" name="toc3.1.8">&nbsp;</a><h3 id="specular-factor"><a class="header" href="#specular-factor">Specular factor</a></h3> <a class="target" name="sheencolor">&nbsp;</a><a class="target" name="materialmodels/litmodel/sheencolor">&nbsp;</a><a class="target" name="toc3.1.8">&nbsp;</a><h3 id="sheen-color"><a class="header" href="#sheen-color">Sheen color</a></h3>
<p>
<p>The <code>specularFactor</code> property scales the amount of specular reflection for non-metallic surfaces.
This property is often used to implement the <code>KHR_materials_specular</code> glTF extension. This value
scales the Fresnel reflectance at normal incidence (F0), which is also computed from the
<code>reflectance</code> property. It also sets the Fresnel reflectance at grazing angles (F90).</p>
</p>
<a class="target" name="specularcolorfactor">&nbsp;</a><a class="target" name="materialmodels/litmodel/specularcolorfactor">&nbsp;</a><a class="target" name="toc3.1.9">&nbsp;</a><h3 id="specular-color-factor"><a class="header" href="#specular-color-factor">Specular color factor</a></h3>
<p>
<p>The <code>specularColorFactor</code> property defines the color of the specular reflection for non-metallic
surfaces. This property is often used to implement the <code>KHR_materials_specular</code> glTF extension. This
value scales the Fresnel reflectance at normal incidence (F0), which is also computed from the
<code>reflectance</code> property.</p>
</p>
<a class="target" name="sheencolor">&nbsp;</a><a class="target" name="materialmodels/litmodel/sheencolor">&nbsp;</a><a class="target" name="toc3.1.10">&nbsp;</a><h3 id="sheen-color"><a class="header" href="#sheen-color">Sheen color</a></h3>
<p> <p>
<p>The sheen color controls the color appearance and strength of an optional sheen layer on top of the <p>The sheen color controls the color appearance and strength of an optional sheen layer on top of the
base layer described by the properties above. The sheen layer always sits below the clear coat layer base layer described by the properties above. The sheen layer always sits below the clear coat layer
@@ -633,7 +613,7 @@ The effect of <code>sheenColor</code> is shown in <a href="#figure_materialsheen
create a cloth-like or fabric-like appearance, it is more efficient to use the dedicated cloth create a cloth-like or fabric-like appearance, it is more efficient to use the dedicated cloth
model described in section <a href="#toc3.3">3.3</a>.</div> model described in section <a href="#toc3.3">3.3</a>.</div>
</p> </p>
<a class="target" name="sheenroughness">&nbsp;</a><a class="target" name="materialmodels/litmodel/sheenroughness">&nbsp;</a><a class="target" name="toc3.1.11">&nbsp;</a><h3 id="sheen-roughness"><a class="header" href="#sheen-roughness">Sheen roughness</a></h3> <a class="target" name="sheenroughness">&nbsp;</a><a class="target" name="materialmodels/litmodel/sheenroughness">&nbsp;</a><a class="target" name="toc3.1.9">&nbsp;</a><h3 id="sheen-roughness"><a class="header" href="#sheen-roughness">Sheen roughness</a></h3>
<p> <p>
<p>The <code>sheenRoughness</code> property is similar to the <code>roughness</code> property but applies only to the <p>The <code>sheenRoughness</code> property is similar to the <code>roughness</code> property but applies only to the
sheen layer.</p> sheen layer.</p>
@@ -645,7 +625,7 @@ The effect of <code>sheenRoughness</code> on a rough metal is shown in <a href="
<center><div class="image" style=""><a href="../images/materials/sheen_roughness.png" target="_blank"><img class="markdeep" src="../images/materials/sheen_roughness.png" /></a><center><span class="imagecaption"><a class="target" name="figure_sheenroughnessproperty">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;8:</b> <code>sheenRoughness</code> varying from 0.0 <center><div class="image" style=""><a href="../images/materials/sheen_roughness.png" target="_blank"><img class="markdeep" src="../images/materials/sheen_roughness.png" /></a><center><span class="imagecaption"><a class="target" name="figure_sheenroughnessproperty">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;8:</b> <code>sheenRoughness</code> varying from 0.0
(left) to 1.0 (right)</span></center></div></center> (left) to 1.0 (right)</span></center></div></center>
</p> </p>
<a class="target" name="clearcoat">&nbsp;</a><a class="target" name="materialmodels/litmodel/clearcoat">&nbsp;</a><a class="target" name="toc3.1.12">&nbsp;</a><h3 id="clear-coat"><a class="header" href="#clear-coat">Clear coat</a></h3> <a class="target" name="clearcoat">&nbsp;</a><a class="target" name="materialmodels/litmodel/clearcoat">&nbsp;</a><a class="target" name="toc3.1.10">&nbsp;</a><h3 id="clear-coat"><a class="header" href="#clear-coat">Clear coat</a></h3>
<p> <p>
<p>Multi-layer materials are fairly common, particularly materials with a thin translucent <p>Multi-layer materials are fairly common, particularly materials with a thin translucent
layer over a base layer. Real world examples of such materials include car paints, soda cans, layer over a base layer. Real world examples of such materials include car paints, soda cans,
@@ -672,7 +652,7 @@ The effect of <code>clearCoat</code> on a rough metal is shown in <a href="#figu
</p><p> </p><p>
<div class="admonition note">The clear coat layer is added on top of the sheen layer if present.</div> <div class="admonition note">The clear coat layer is added on top of the sheen layer if present.</div>
</p> </p>
<a class="target" name="clearcoatroughness">&nbsp;</a><a class="target" name="materialmodels/litmodel/clearcoatroughness">&nbsp;</a><a class="target" name="toc3.1.13">&nbsp;</a><h3 id="clear-coat-roughness"><a class="header" href="#clear-coat-roughness">Clear coat roughness</a></h3> <a class="target" name="clearcoatroughness">&nbsp;</a><a class="target" name="materialmodels/litmodel/clearcoatroughness">&nbsp;</a><a class="target" name="toc3.1.11">&nbsp;</a><h3 id="clear-coat-roughness"><a class="header" href="#clear-coat-roughness">Clear coat roughness</a></h3>
<p> <p>
<p>The <code>clearCoatRoughness</code> property is similar to the <code>roughness</code> property but applies only to the <p>The <code>clearCoatRoughness</code> property is similar to the <code>roughness</code> property but applies only to the
clear coat layer.</p> clear coat layer.</p>
@@ -683,7 +663,7 @@ The effect of <code>clearCoatRoughness</code> on a rough metal is shown in <a hr
<center><div class="image" style=""><a href="../images/materials/clear_coat_roughness.png" target="_blank"><img class="markdeep" src="../images/materials/clear_coat_roughness.png" /></a><center><span class="imagecaption"><a class="target" name="figure_clearcoatroughnessproperty">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;11:</b> <code>clearCoatRoughness</code> varying from 0.0 <center><div class="image" style=""><a href="../images/materials/clear_coat_roughness.png" target="_blank"><img class="markdeep" src="../images/materials/clear_coat_roughness.png" /></a><center><span class="imagecaption"><a class="target" name="figure_clearcoatroughnessproperty">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;11:</b> <code>clearCoatRoughness</code> varying from 0.0
(left) to 1.0 (right)</span></center></div></center> (left) to 1.0 (right)</span></center></div></center>
</p> </p>
<a class="target" name="anisotropy">&nbsp;</a><a class="target" name="materialmodels/litmodel/anisotropy">&nbsp;</a><a class="target" name="toc3.1.14">&nbsp;</a><h3 id="anisotropy"><a class="header" href="#anisotropy">Anisotropy</a></h3> <a class="target" name="anisotropy">&nbsp;</a><a class="target" name="materialmodels/litmodel/anisotropy">&nbsp;</a><a class="target" name="toc3.1.12">&nbsp;</a><h3 id="anisotropy"><a class="header" href="#anisotropy">Anisotropy</a></h3>
<p> <p>
<p>Many real-world materials, such as brushed metal, can only be replicated using an anisotropic <p>Many real-world materials, such as brushed metal, can only be replicated using an anisotropic
reflectance model. A material can be changed from the default isotropic model to an anisotropic reflectance model. A material can be changed from the default isotropic model to an anisotropic
@@ -708,7 +688,7 @@ tangent direction and negative values in the bitangent direction.
<div class="admonition tip">The anisotropic material model is slightly more expensive than the standard material model. Do <div class="admonition tip">The anisotropic material model is slightly more expensive than the standard material model. Do
not assign a value (even 0.0) to the <code>anisotropy</code> property if you don't need anisotropy.</div> not assign a value (even 0.0) to the <code>anisotropy</code> property if you don't need anisotropy.</div>
</p> </p>
<a class="target" name="anisotropydirection">&nbsp;</a><a class="target" name="materialmodels/litmodel/anisotropydirection">&nbsp;</a><a class="target" name="toc3.1.15">&nbsp;</a><h3 id="anisotropy-direction"><a class="header" href="#anisotropy-direction">Anisotropy direction</a></h3> <a class="target" name="anisotropydirection">&nbsp;</a><a class="target" name="materialmodels/litmodel/anisotropydirection">&nbsp;</a><a class="target" name="toc3.1.13">&nbsp;</a><h3 id="anisotropy-direction"><a class="header" href="#anisotropy-direction">Anisotropy direction</a></h3>
<p> <p>
<p>The <code>anisotropyDirection</code> property defines the direction of the surface at a given point and thus <p>The <code>anisotropyDirection</code> property defines the direction of the surface at a given point and thus
control the shape of the specular highlights. It is specified as vector of 3 values that usually control the shape of the specular highlights. It is specified as vector of 3 values that usually
@@ -726,7 +706,7 @@ in <a href="#figure_anisotropydirectionproperty">figure&nbsp;16</a>.
</p><p> </p><p>
<center><div class="image" style=""><a href="../images/screenshot_anisotropy_map.jpg" target="_blank"><img class="markdeep" src="../images/screenshot_anisotropy_map.jpg" /></a><center><span class="imagecaption"><a class="target" name="figure_anisotropydirectionproperty">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;16:</b> Example of Lighting: specularAmbientOcclusiona direction map</span></center></div></center> <center><div class="image" style=""><a href="../images/screenshot_anisotropy_map.jpg" target="_blank"><img class="markdeep" src="../images/screenshot_anisotropy_map.jpg" /></a><center><span class="imagecaption"><a class="target" name="figure_anisotropydirectionproperty">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;16:</b> Example of Lighting: specularAmbientOcclusiona direction map</span></center></div></center>
</p> </p>
<a class="target" name="ambientocclusion">&nbsp;</a><a class="target" name="materialmodels/litmodel/ambientocclusion">&nbsp;</a><a class="target" name="toc3.1.16">&nbsp;</a><h3 id="ambient-occlusion"><a class="header" href="#ambient-occlusion">Ambient occlusion</a></h3> <a class="target" name="ambientocclusion">&nbsp;</a><a class="target" name="materialmodels/litmodel/ambientocclusion">&nbsp;</a><a class="target" name="toc3.1.14">&nbsp;</a><h3 id="ambient-occlusion"><a class="header" href="#ambient-occlusion">Ambient occlusion</a></h3>
<p> <p>
<p>The <code>ambientOcclusion</code> property defines how much of the ambient light is accessible to a surface <p>The <code>ambientOcclusion</code> property defines how much of the ambient light is accessible to a surface
point. It is a per-pixel shadowing factor between 0.0 (fully shadowed) and 1.0 (fully lit). This point. It is a per-pixel shadowing factor between 0.0 (fully shadowed) and 1.0 (fully lit). This
@@ -736,7 +716,7 @@ directional, point and spot lights, nor specular lighting.</p>
<center><div class="image" style=""><a href="../images/screenshot_ao.jpg" target="_blank"><img class="markdeep" src="../images/screenshot_ao.jpg" /></a><center><span class="imagecaption"><a class="target" name="figure_aoexample">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;17:</b> Comparison of materials without diffuse ambient occlusion <center><div class="image" style=""><a href="../images/screenshot_ao.jpg" target="_blank"><img class="markdeep" src="../images/screenshot_ao.jpg" /></a><center><span class="imagecaption"><a class="target" name="figure_aoexample">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;17:</b> Comparison of materials without diffuse ambient occlusion
(left) and with (right)</span></center></div></center> (left) and with (right)</span></center></div></center>
</p> </p>
<a class="target" name="normal">&nbsp;</a><a class="target" name="materialmodels/litmodel/normal">&nbsp;</a><a class="target" name="toc3.1.17">&nbsp;</a><h3 id="normal"><a class="header" href="#normal">Normal</a></h3> <a class="target" name="normal">&nbsp;</a><a class="target" name="materialmodels/litmodel/normal">&nbsp;</a><a class="target" name="toc3.1.15">&nbsp;</a><h3 id="normal"><a class="header" href="#normal">Normal</a></h3>
<p> <p>
<p>The <code>normal</code> property defines the normal of the surface at a given point. It usually comes from a <p>The <code>normal</code> property defines the normal of the surface at a given point. It usually comes from a
<em class="underscore">normal map</em> texture, which allows to vary the property per-pixel. The normal is supplied in tangent <em class="underscore">normal map</em> texture, which allows to vary the property per-pixel. The normal is supplied in tangent
@@ -754,7 +734,7 @@ and with (right)</span></center></div></center>
</p><p> </p><p>
<div class="admonition warning">Using a normal map increases the runtime cost of the material model.</div> <div class="admonition warning">Using a normal map increases the runtime cost of the material model.</div>
</p> </p>
<a class="target" name="bentnormal">&nbsp;</a><a class="target" name="materialmodels/litmodel/bentnormal">&nbsp;</a><a class="target" name="toc3.1.18">&nbsp;</a><h3 id="bent-normal"><a class="header" href="#bent-normal">Bent normal</a></h3> <a class="target" name="bentnormal">&nbsp;</a><a class="target" name="materialmodels/litmodel/bentnormal">&nbsp;</a><a class="target" name="toc3.1.16">&nbsp;</a><h3 id="bent-normal"><a class="header" href="#bent-normal">Bent normal</a></h3>
<p> <p>
<p>The <code>bentNormal</code> property defines the average unoccluded direction at a point on the surface. It is <p>The <code>bentNormal</code> property defines the average unoccluded direction at a point on the surface. It is
used to improve the accuracy of indirect lighting. Bent normals can also improve the quality of used to improve the accuracy of indirect lighting. Bent normals can also improve the quality of
@@ -768,7 +748,7 @@ instance.
<center><div class="image" style=""><a href="../images/material_bent_normal.gif" target="_blank"><img class="markdeep" src="../images/material_bent_normal.gif" /></a><center><span class="imagecaption"><a class="target" name="figure_bentnormalmapped">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;19:</b> Example of a model rendered with and without a bent normal map. Both <center><div class="image" style=""><a href="../images/material_bent_normal.gif" target="_blank"><img class="markdeep" src="../images/material_bent_normal.gif" /></a><center><span class="imagecaption"><a class="target" name="figure_bentnormalmapped">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;19:</b> Example of a model rendered with and without a bent normal map. Both
versions use the same ambient occlusion map.</span></center></div></center> versions use the same ambient occlusion map.</span></center></div></center>
</p> </p>
<a class="target" name="clearcoatnormal">&nbsp;</a><a class="target" name="materialmodels/litmodel/clearcoatnormal">&nbsp;</a><a class="target" name="toc3.1.19">&nbsp;</a><h3 id="clear-coat-normal"><a class="header" href="#clear-coat-normal">Clear coat normal</a></h3> <a class="target" name="clearcoatnormal">&nbsp;</a><a class="target" name="materialmodels/litmodel/clearcoatnormal">&nbsp;</a><a class="target" name="toc3.1.17">&nbsp;</a><h3 id="clear-coat-normal"><a class="header" href="#clear-coat-normal">Clear coat normal</a></h3>
<p> <p>
<p>The <code>clearCoatNormal</code> property defines the normal of the clear coat layer at a given point. It <p>The <code>clearCoatNormal</code> property defines the normal of the clear coat layer at a given point. It
behaves otherwise like the <code>normal</code> property.</p> behaves otherwise like the <code>normal</code> property.</p>
@@ -778,7 +758,7 @@ map and a surface normal map</span></center></div></center>
</p><p> </p><p>
<div class="admonition warning">Using a clear coat normal map increases the runtime cost of the material model.</div> <div class="admonition warning">Using a clear coat normal map increases the runtime cost of the material model.</div>
</p> </p>
<a class="target" name="emissive">&nbsp;</a><a class="target" name="materialmodels/litmodel/emissive">&nbsp;</a><a class="target" name="toc3.1.20">&nbsp;</a><h3 id="emissive"><a class="header" href="#emissive">Emissive</a></h3> <a class="target" name="emissive">&nbsp;</a><a class="target" name="materialmodels/litmodel/emissive">&nbsp;</a><a class="target" name="toc3.1.18">&nbsp;</a><h3 id="emissive"><a class="header" href="#emissive">Emissive</a></h3>
<p> <p>
<p>The <code>emissive</code> property can be used to simulate additional light emitted by the surface. It is <p>The <code>emissive</code> property can be used to simulate additional light emitted by the surface. It is
defined as a <code>float4</code> value that contains an RGB intensity in nits as well as an exposure defined as a <code>float4</code> value that contains an RGB intensity in nits as well as an exposure
@@ -797,7 +777,7 @@ force an emissive surface to bloom. When the exposure weight is set to 0, the em
not affected by the camera exposure. When the weight is set to 1, the intensity is multiplied by not affected by the camera exposure. When the weight is set to 1, the intensity is multiplied by
the camera exposure like with any regular light. the camera exposure like with any regular light.
</p> </p>
<a class="target" name="post-lightingcolor">&nbsp;</a><a class="target" name="materialmodels/litmodel/post-lightingcolor">&nbsp;</a><a class="target" name="toc3.1.21">&nbsp;</a><h3 id="post-lighting-color"><a class="header" href="#post-lighting-color">Post-lighting color</a></h3> <a class="target" name="post-lightingcolor">&nbsp;</a><a class="target" name="materialmodels/litmodel/post-lightingcolor">&nbsp;</a><a class="target" name="toc3.1.19">&nbsp;</a><h3 id="post-lighting-color"><a class="header" href="#post-lighting-color">Post-lighting color</a></h3>
<p> <p>
<p>The <code>postLightingColor</code> can be used to modify the surface color after lighting computations. This <p>The <code>postLightingColor</code> can be used to modify the surface color after lighting computations. This
property has no physical meaning and only exists to implement specific effects or to help with property has no physical meaning and only exists to implement specific effects or to help with
@@ -811,7 +791,7 @@ this option for more information.
<div class="admonition tip"><code>postLightingColor</code> can be used as a simpler <code>emissive</code> property by setting <div class="admonition tip"><code>postLightingColor</code> can be used as a simpler <code>emissive</code> property by setting
<code>postLightingBlending</code> to <code>add</code> and by providing an RGB color with alpha set to <code>0.0</code>.</div> <code>postLightingBlending</code> to <code>add</code> and by providing an RGB color with alpha set to <code>0.0</code>.</div>
</p> </p>
<a class="target" name="indexofrefraction">&nbsp;</a><a class="target" name="materialmodels/litmodel/indexofrefraction">&nbsp;</a><a class="target" name="toc3.1.22">&nbsp;</a><h3 id="index-of-refraction"><a class="header" href="#index-of-refraction">Index of refraction</a></h3> <a class="target" name="indexofrefraction">&nbsp;</a><a class="target" name="materialmodels/litmodel/indexofrefraction">&nbsp;</a><a class="target" name="toc3.1.20">&nbsp;</a><h3 id="index-of-refraction"><a class="header" href="#index-of-refraction">Index of refraction</a></h3>
<p> <p>
<p>The <code>ior</code> property only affects non-metallic surfaces. This property can be used to control the <p>The <code>ior</code> property only affects non-metallic surfaces. This property can be used to control the
index of refraction and the specular intensity of materials. The <code>ior</code> property is intended to index of refraction and the specular intensity of materials. The <code>ior</code> property is intended to
@@ -864,7 +844,7 @@ See the <a href="#reflectance">Reflectance</a> section for more information on t
light, creating a diffusion effect useful to recreate &ldquo;blurry&rdquo; appearances such as frosted light, creating a diffusion effect useful to recreate &ldquo;blurry&rdquo; appearances such as frosted
glass, certain plastics, etc.</div> glass, certain plastics, etc.</div>
</p> </p>
<a class="target" name="transmission">&nbsp;</a><a class="target" name="materialmodels/litmodel/transmission">&nbsp;</a><a class="target" name="toc3.1.23">&nbsp;</a><h3 id="transmission"><a class="header" href="#transmission">Transmission</a></h3> <a class="target" name="transmission">&nbsp;</a><a class="target" name="materialmodels/litmodel/transmission">&nbsp;</a><a class="target" name="toc3.1.21">&nbsp;</a><h3 id="transmission"><a class="header" href="#transmission">Transmission</a></h3>
<p> <p>
<p>The <code>transmission</code> property defines what ratio of diffuse light is transmitted through a refractive <p>The <code>transmission</code> property defines what ratio of diffuse light is transmitted through a refractive
material. This property only affects materials with a <code>refractionMode</code> set to <code>cubemap</code> or material. This property only affects materials with a <code>refractionMode</code> set to <code>cubemap</code> or
@@ -884,7 +864,7 @@ The effect of <code>transmission</code> on a glossy dielectric (<code>ior</code>
<div class="admonition tip">The <code>transmission</code> property is useful to create decals, paint, etc. at the surface of refractive <div class="admonition tip">The <code>transmission</code> property is useful to create decals, paint, etc. at the surface of refractive
materials.</div> materials.</div>
</p> </p>
<a class="target" name="absorption">&nbsp;</a><a class="target" name="materialmodels/litmodel/absorption">&nbsp;</a><a class="target" name="toc3.1.24">&nbsp;</a><h3 id="absorption"><a class="header" href="#absorption">Absorption</a></h3> <a class="target" name="absorption">&nbsp;</a><a class="target" name="materialmodels/litmodel/absorption">&nbsp;</a><a class="target" name="toc3.1.22">&nbsp;</a><h3 id="absorption"><a class="header" href="#absorption">Absorption</a></h3>
<p> <p>
<p>The <code>absorption</code> property defines the absorption coefficients of light transmitted through the <p>The <code>absorption</code> property defines the absorption coefficients of light transmitted through the
material. <a href="#figure_absorptionexample">Figure 24</a> shows the effect of <code>absorption</code> on a refracting object with material. <a href="#figure_absorptionexample">Figure 24</a> shows the effect of <code>absorption</code> on a refracting object with
@@ -921,7 +901,7 @@ $$absorption = -\frac{ln(transmittanceColor)}{atDistance}$$
While this computation can be done in the material itself we recommend doing it offline whenever While this computation can be done in the material itself we recommend doing it offline whenever
possible. Filament provides an API for this purpose, <code>Color::absorptionAtDistance()</code>. possible. Filament provides an API for this purpose, <code>Color::absorptionAtDistance()</code>.
</p> </p>
<a class="target" name="micro-thicknessandthickness">&nbsp;</a><a class="target" name="materialmodels/litmodel/micro-thicknessandthickness">&nbsp;</a><a class="target" name="toc3.1.25">&nbsp;</a><h3 id="micro-thickness-and-thickness"><a class="header" href="#micro-thickness-and-thickness">Micro-thickness and thickness</a></h3> <a class="target" name="micro-thicknessandthickness">&nbsp;</a><a class="target" name="materialmodels/litmodel/micro-thicknessandthickness">&nbsp;</a><a class="target" name="toc3.1.23">&nbsp;</a><h3 id="micro-thickness-and-thickness"><a class="header" href="#micro-thickness-and-thickness">Micro-thickness and thickness</a></h3>
<p> <p>
<p>The <code>microThickness</code> and <code>thickness</code> properties define the optical depth of the material of a <p>The <code>microThickness</code> and <code>thickness</code> properties define the optical depth of the material of a
refracting object. <code>microThickness</code> is used when <code>refractionType</code> is set to <code>thin</code>, and <code>thickness</code> refracting object. <code>microThickness</code> is used when <code>refractionType</code> is set to <code>thin</code>, and <code>thickness</code>
@@ -954,7 +934,7 @@ the <code>refractionType</code> is set to <code>solid</code> and <code>absorptio
<center><div class="image" style=""><a href="../images/material_thickness.png" target="_blank"><img class="markdeep" src="../images/material_thickness.png" /></a><center><span class="imagecaption"><a class="target" name="figure_varyingthickness">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;27:</b> <code>thickness</code> varying from 0.0 at the top of the prism to 3.0 at the <center><div class="image" style=""><a href="../images/material_thickness.png" target="_blank"><img class="markdeep" src="../images/material_thickness.png" /></a><center><span class="imagecaption"><a class="target" name="figure_varyingthickness">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;27:</b> <code>thickness</code> varying from 0.0 at the top of the prism to 3.0 at the
bottom of the prism</span></center></div></center> bottom of the prism</span></center></div></center>
</p> </p>
<a class="target" name="dispersion">&nbsp;</a><a class="target" name="materialmodels/litmodel/dispersion">&nbsp;</a><a class="target" name="toc3.1.26">&nbsp;</a><h3 id="dispersion"><a class="header" href="#dispersion">Dispersion</a></h3> <a class="target" name="dispersion">&nbsp;</a><a class="target" name="materialmodels/litmodel/dispersion">&nbsp;</a><a class="target" name="toc3.1.24">&nbsp;</a><h3 id="dispersion"><a class="header" href="#dispersion">Dispersion</a></h3>
<p> <p>
<p>The dispersion property controls the angular separation of colors transmitting through a relatively <p>The dispersion property controls the angular separation of colors transmitting through a relatively
clear volume. It can only be used when <code>refractionType</code> is set to <code>volume</code>. clear volume. It can only be used when <code>refractionType</code> is set to <code>volume</code>.
+2 -2
View File
@@ -165,7 +165,7 @@
<nav class="nav-wrapper" aria-label="Page navigation"> <nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons --> <!-- Mobile navigation buttons -->
<a rel="prev" href="../dup/test_ci_sizeguard.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left"> <a rel="prev" href="../dup/test_ci_renderdiff.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i> <i class="fa fa-angle-left"></i>
</a> </a>
@@ -179,7 +179,7 @@
</div> </div>
<nav class="nav-wide-wrapper" aria-label="Page navigation"> <nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../dup/test_ci_sizeguard.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left"> <a rel="prev" href="../dup/test_ci_renderdiff.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i> <i class="fa fa-angle-left"></i>
</a> </a>
+2 -2
View File
@@ -174,7 +174,7 @@
<script src="../../web/lib/filament.js"></script> <script src="../../web/lib/filament.js"></script>
<script src="../../web/lib/gl-matrix-min.js"></script> <script src="../../web/lib/gl-matrix-min.js"></script>
<script> <script>
Filament.init(['../../web/assets/cube_fl0/nonlit.filamat'], () => { Filament.init(['../../web/assets/cube_fl0/nonlit_fl0.filamat'], () => {
window.VertexAttribute = Filament.VertexAttribute; window.VertexAttribute = Filament.VertexAttribute;
window.AttributeType = Filament.VertexBuffer$AttributeType; window.AttributeType = Filament.VertexBuffer$AttributeType;
window.Projection = Filament.Camera$Projection; window.Projection = Filament.Camera$Projection;
@@ -257,7 +257,7 @@
.bufferType(Filament.IndexBuffer$IndexType.USHORT) .bufferType(Filament.IndexBuffer$IndexType.USHORT)
.build(engine); .build(engine);
this.ib.setBuffer(engine, CUBE_INDICES); this.ib.setBuffer(engine, CUBE_INDICES);
const mat = engine.createMaterial('../../web/assets/cube_fl0/nonlit.filamat'); const mat = engine.createMaterial('../../web/assets/cube_fl0/nonlit_fl0.filamat');
const matinst = mat.getDefaultInstance(); const matinst = mat.getDefaultInstance();
Filament.RenderableManager.Builder(1) Filament.RenderableManager.Builder(1)
.boundingBox({ center: [-1, -1, -1], halfExtent: [1, 1, 1] }) .boundingBox({ center: [-1, -1, -1], halfExtent: [1, 1, 1] })
+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
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
+4
View File
@@ -3,3 +3,7 @@ src_mdbook/book
src_mdbook/src/dup/*.md src_mdbook/src/dup/*.md
src_mdbook/src/main/filament.md src_mdbook/src/main/filament.md
src_mdbook/src/main/materials.md src_mdbook/src/main/materials.md
src_mdbook/src/samples/web/*.md
src_mdbook/src/web
src_mdbook/src/release/cocoapods.md
src_mdbook/src/remote
-3
View File
@@ -100,9 +100,6 @@
"test/backend/README.md": { "test/backend/README.md": {
"dest": "dup/test_ci_backend.md" "dest": "dup/test_ci_backend.md"
}, },
"test/sizeguard/README.md": {
"dest": "dup/test_ci_sizeguard.md"
},
"filament/backend/test/README.md": { "filament/backend/test/README.md": {
"dest": "dup/backend_test.md" "dest": "dup/backend_test.md"
}, },
-18
View File
@@ -81,8 +81,6 @@ in table [standardProperties].
**roughness** | Perceived smoothness (1.0) or roughness (0.0) of a surface. Smooth surfaces exhibit sharp reflections **roughness** | Perceived smoothness (1.0) or roughness (0.0) of a surface. Smooth surfaces exhibit sharp reflections
**metallic** | Whether a surface appears to be dielectric (0.0) or conductor (1.0). Often used as a binary value (0 or 1) **metallic** | Whether a surface appears to be dielectric (0.0) or conductor (1.0). Often used as a binary value (0 or 1)
**reflectance** | Fresnel reflectance at normal incidence for dielectric surfaces. This directly controls the strength of the reflections **reflectance** | Fresnel reflectance at normal incidence for dielectric surfaces. This directly controls the strength of the reflections
**specularFactor** | Scales the amount of specular reflection for non-metallic surfaces
**specularColorFactor** | Color of the specular reflection for non-metallic surfaces
**ambientOcclusion** | Defines how much of the ambient light is accessible to a surface point. It is a per-pixel shadowing factor between 0.0 and 1.0 **ambientOcclusion** | Defines how much of the ambient light is accessible to a surface point. It is a per-pixel shadowing factor between 0.0 and 1.0
**clearCoat** | Strength of the clear coat layer **clearCoat** | Strength of the clear coat layer
**clearCoatRoughness** | Perceived smoothness or roughness of the clear coat layer **clearCoatRoughness** | Perceived smoothness or roughness of the clear coat layer
@@ -112,8 +110,6 @@ The type and range of each property is described in table [standardPropertiesTyp
**metallic** | float | [0..1] | Should be 0 or 1 **metallic** | float | [0..1] | Should be 0 or 1
**roughness** | float | [0..1] | **roughness** | float | [0..1] |
**reflectance** | float | [0..1] | Prefer values > 0.35 **reflectance** | float | [0..1] | Prefer values > 0.35
**specularFactor** | float | [0..1] | Optional, defaults to 1.0
**specularColorFactor** | float3 | [0..1] | Linear RGB, defaults to 1.0
**sheenColor** | float3 | [0..1] | Linear RGB **sheenColor** | float3 | [0..1] | Linear RGB
**sheenRoughness** | float | [0..1] | **sheenRoughness** | float | [0..1] |
**clearCoat** | float | [0..1] | Should be 0 or 1 **clearCoat** | float | [0..1] | Should be 0 or 1
@@ -308,20 +304,6 @@ it easy to define from a texture.
See section [Index of refraction] for more information about the `ior` property and refractive See section [Index of refraction] for more information about the `ior` property and refractive
indices. indices.
### Specular factor
The `specularFactor` property scales the amount of specular reflection for non-metallic surfaces.
This property is often used to implement the `KHR_materials_specular` glTF extension. This value
scales the Fresnel reflectance at normal incidence (F0), which is also computed from the
`reflectance` property. It also sets the Fresnel reflectance at grazing angles (F90).
### Specular color factor
The `specularColorFactor` property defines the color of the specular reflection for non-metallic
surfaces. This property is often used to implement the `KHR_materials_specular` glTF extension. This
value scales the Fresnel reflectance at normal incidence (F0), which is also computed from the
`reflectance` property.
### Sheen color ### Sheen color
The sheen color controls the color appearance and strength of an optional sheen layer on top of the The sheen color controls the color appearance and strength of an optional sheen layer on top of the
+13 -13
View File
@@ -23,28 +23,22 @@
- [skinning](./samples/web/skinning.md) - [skinning](./samples/web/skinning.md)
- [sky](./samples/web/sky.md) - [sky](./samples/web/sky.md)
- [Technical Notes](./notes/README.md) - [Technical Notes](./notes/README.md)
- [Material Properties](./notes/material_properties.md)
- [Release](./release/README.md)
- [CocoaPods](./release/cocoapods.md)
- [Maven](./release/maven.md)
- [Github](./release/guide.md)
- [Versioning](./release/versioning.md)
- [Branching](./release/branching.md)
- [Documentation](./dup/docs.md)
- [Debugging](./notes/debugging.md) - [Debugging](./notes/debugging.md)
- [Metal](./notes/metal_debugging.md) - [Metal](./notes/metal_debugging.md)
- [Vulkan](./notes/vulkan_debugging.md) - [Vulkan](./notes/vulkan_debugging.md)
- [SPIR-V](./notes/spirv_debugging.md) - [SPIR-V](./notes/spirv_debugging.md)
- [Running with ASAN and UBSAN](./notes/asan_ubsan.md) - [ASAN and UBSAN](./notes/asan_ubsan.md)
- [Using Instruments on macOS](./notes/instruments.md) - [Instruments on macOS](./notes/instruments.md)
- [Code coverage analysis](./notes/coverage.md) - [Code coverage](./notes/coverage.md)
- [Performance analysis](./notes/performance_analysis.md) - [Performance](./notes/performance_analysis.md)
- [Documentation](./dup/docs.md)
- [Material Properties](./notes/material_properties.md)
- [Framegraph](./notes/framegraph.md) - [Framegraph](./notes/framegraph.md)
- [2D Coordinate Spaces](./notes/2d_coordinate_spaces.md)
- [Tests](./notes/tests.md) - [Tests](./notes/tests.md)
- [backend](./dup/backend_test.md) - [backend](./dup/backend_test.md)
- [CI: backend](./dup/test_ci_backend.md) - [CI: backend](./dup/test_ci_backend.md)
- [CI: renderdiff](./dup/test_ci_renderdiff.md) - [CI: renderdiff](./dup/test_ci_renderdiff.md)
- [CI: sizeguard](./dup/test_ci_sizeguard.md)
- [Libraries](./notes/libs.md) - [Libraries](./notes/libs.md)
- [bluegl](./dup/bluegl.md) - [bluegl](./dup/bluegl.md)
- [bluevk](./dup/bluevk.md) - [bluevk](./dup/bluevk.md)
@@ -67,3 +61,9 @@
- [specgen](./dup/specgen.md) - [specgen](./dup/specgen.md)
- [specular-color](./dup/specular_color.md) - [specular-color](./dup/specular_color.md)
- [zbloat](./dup/zbloat.md) - [zbloat](./dup/zbloat.md)
- [Release](./release/README.md)
- [CocoaPods](./release/cocoapods.md)
- [Maven](./release/maven.md)
- [Github](./release/guide.md)
- [Versioning](./release/versioning.md)
- [Branching](./release/branching.md)
@@ -0,0 +1,221 @@
# 2D Coordinate Spaces
This page describes Filament's interpretation of 2D coordinate spaces. We address the differences
between graphics APIs and provide explanations for custom logic and client APIs.
## 1. Texture coordinate space
To begin our discussion, we imagine looking at a rectangular image, like a photograph. To be able to
describe to another person where things are on the photograph (say, a **tree**), we need to first
define a consistent coordinate system that both parties understand. A natural choice is to pick one
of the corners as the origin of the coordinate system (that is, corners are easy to identify on a
rectangle, and do not require tools - like a ruler - for positioning). We can further refine this
coordinate system by assigning 2D numbers to the four corners so that we can accurately
describe the positions on the photograph.
So for example, we assign the corners in clockwise order, starting from the top-left corner:
\\((0, 0)\\), \\((1, 0)\\), \\((1, 1)\\), \\((0, 1)\\).
Assuming we are sampling from a texture in a shader, we sample by using a 2D coordinate (UV)
to identify a point on the rectangular space of the texture. We can make a statement such as, "the
**tree** is near the point of \\(((0.65, 0.35)\\))", to communicate the position of a tree in
the picture to another person.
Given the description above, we conclude this section with the following statements
- Programmers communicate with computers using coordinate systems like the above to draw 2D images
onto a display.
- In 3D graphics, similar coordinate systems are used to texturize 3D models with 2D images. The
2D images are called *textures*.
- Filament defines its coordinate system using the *top-left* of the image as \\((0, 0)\\) and
the *bottom-right* is \\((1, 1)\\).
### 1.1. OpenGL (The Bottom-Left API)
OpenGL has its texture coordinate origin \\((0, 0)\\) at the **bottom-left**, and
\\((1, 1)\\) is at the top-right. Note that this differs from Filament's assumed texture
coordinate system.
The GL function for uploading texture is `glTexImage2D`. Even though GL interprets \\((0, 0)\\) as
the bottom-left of a 2D image, `glTexImage2D` uploads the lower-bits as the *bottom* of the image
as opposed to the *top*. (This behavior is opposite of the other backends' texture upload
APIs). The two differences cancel each other, meaning there is no difference between the different
backends in upload and sampling of textures.
We can restate the above as
| |
|---|
| *Filament's internal texture coordinate space defines the **top-left** corner of the image as the origin.* |
### 1.2. Readback of rendertarget
While we've established that the APIs are not any different in the *uploading* and *sampling* of
textures for Filament's backends, there are, unfortunately, cases where special handling of
texture coordinate is necessary.
Reading the result of rendering into CPU memory can be very useful; for example, one might want to
take snapshots of the rendering and output to image formats such as PNG or JPEG. Another use is to
perform image analysis or processing over a rendered image. In Filament, the readback path
corresponds to the `readPixels`. In the OpenGL backend, this is implemented using the `glReadPixels`
function. GL's interpretation of texture coordinate space holds true for `glReadPixels`; this means
that it treats the bottom-left of the image as the origin and the readback output *starts* from the
origin. Hence, the first row of the image in CPU memory is the bottom-most row of the image (when
displayed).
This creates two related inconsistencies:
- First, (not a real use case), if one were to upload a texture, attach it as an attachment, and
read the attachment, the GL backend would place the bottom of the image as the first row of
pixels in CPU memory.
- Second, the other backends consider the top-left corner as the origin, which means their
readbacks would place the top of the image as the first row of the image in CPU memory.
To account for these inconsistencies, *the GL backend vertically flips the rows of the readback
image*.
### 1.3. `flipUV` in Materials
In order to work with textures and associated primitives from different sources, Filament provides
a simple, material-level client-set flag to flip the v-direction.
For example, if a 3D modeler
exported a mesh with textures such that the bottom-left corner of the texture is \\(u=0, v=0\\) in
texture space, then this is not consistent with Filament's assumption. Luckily, modern modelers will
only choose either the top-left corner or the bottom-right corner as the origin. To address this
inconsistency, Filament provides `flipUV` in the material definition. Setting this flag to true will
flip the UV coordinate in the vertex attributes so that \\(v' = 1 - v\\), and the texture
coordinate interpretation is then consistent with Filament's internal coordinates.
## 2. Clip-space
The clip-space is the bounded 3D space that a primitive vertex is transformed into at the end of a
vertex shader. Abstractly, the XY-plane of the clip-space is parallel to the display, and its
bounds correspond to the bounds of the window, surface, or buffer that we are rendering into. (This
API-specific bounded space is also known as Normalized Device Coordinates or NDC).
In different graphics API, the bounds and orientation of the NDC is also inconsistent. For all of
the backends, the ranges of the XY-plane \\([-1, 1]\\) and the origin (\\((0, 0)\\)) are the
same. However, the depth range and the Y-axis directions are not the same.
| Graphics API | Depth Range (Z) | Y-Axis Direction | Frame Buffer Origin |
| -------------- | --------------- | ---------------- | ------------- |
| OpenGL / WebGL | \\([−1, 1]\\) | Up | bottom-left |
| Metal | \\([0, 1]\\) | Up | top-left |
| WebGPU | \\([0, 1]\\) | Up | top-left |
| Vulkan | \\([0, 1]\\) | Down | top-left |
(We will defer the discussion of GL's depth range difference to section 2.3.)
### 2.1. Vulkan y-axis correction
The XY ranges for all backends are the same, but Vulkan interprets the Y-Axis direction as
"Down", meaning that \\(y=1\\) is at the bottom of the image. To account for this difference,
Filament flips the projected \\(y\\) coordinate in the vertex shader for Vulkan
```c++
#if defined(TARGET_VULKAN_ENVIRONMENT)
// In Vulkan, clip space is Y-down. In OpenGL and Metal, clip space is Y-up.
position.y = -position.y;
#endif
```
### 2.2 Fix Frame buffer origin difference with `uvToRenderTargetUV`
After the workaround in the above section, we can rewrite the table thus
| Graphics API | Y-Axis Direction | Frame Buffer Origin |
| -------------- | ---------------- | ------------------- |
| OpenGL / WebGL | Up | bottom-left |
| Metal | Up | top-left |
| WebGPU | Up | top-left |
| Vulkan | Up (corrected) | top-left |
We can rephrase the Y-Axis Direction column to say that
| |
|---|
| *Filament's internal clip-space defines the Y-axis direction as **UP** (i.e. the physical world up direction pointing to \\(+1\\)).* |
Now consider the "Frame Buffer Origin" column. This column identifies the origin of the rendered
image in the context of sampling by subsequent passes.
Assume
- We have two passes \\(A\\) and \\(B\\)
- \\(A\\) renders to a render target that attaches texture \\(T\\)
- \\(B\\) renders a full-screen quad
- In \\(B\\), top-left and bottom-right corners of the quad are assigned texturing UV coordinates
\\((0, 0)\\) and \\((1, 1)\\), respectively.
- The quad in \\(B\\) samples texture \\(T\\).
For GL, having the *bottom-left* corner as the origin means that rendering a pixel to clip-space
\\(x=-1, y=-1\\) in \\(A\\) will output to pixel at \\(u=0, v=0\\) of \\(T\\). Note that GL
clip-space's y-axis direction is consistent with the texture space v-axis direction. That is, in
pass \\(B\\), sampling from \\(v=0\\) to \\(v=1\\) (moving in the positive direction) maps to going
from \\(y=-1\\) to \\(y=1\\) in clip-space (also moving in positive direction).
This behavior is opposite for all other backends; sampling from \\(v=0\\) to \\(v=1\\) will map to
clip-space movement from \\(y=1\\) to \\(y=-1\\) (going in the negative direction).
To account for this, Filament provides the following shader method that clients can call to
correctly sample from a texture that was the rendertarget of a previous pass.
```c++
highp vec2 uvToRenderTargetUV(const highp vec2 uv) {
#if defined(TARGET_METAL_ENVIRONMENT) || defined(TARGET_VULKAN_ENVIRONMENT) || defined(TARGET_WEBGPU_ENVIRONMENT)
return vec2(uv.x, 1.0 - uv.y);
#else
return uv;
#endif
}
```
It's meant to be used like so
```c++
vec2 uv0 = uvToRenderTargetUV(getUV0());
material.baseColor = texture(materialParams_baseColor, uv0);
```
### 2.3. Clip-space depth adjustments for OpenGL
While the clip-space depth (or Z) dimension does not pertain to the 2D coordinate spaces that we have
been discussing, the concept in this section parallels the space adjustment logic in the other
sections. Hence, we have included it here.
Consider again this table
| Graphics API | Depth Range (Z) |
| -------------- | --------------- |
| OpenGL / WebGL | \\([−1, 1]\\) |
| Metal | \\([0, 1]\\) |
| WebGPU | \\([0, 1]\\) |
| Vulkan | \\([0, 1]\\) |
For OpenGL, the NDC depth dimension ranges from \\(-1\\) to \\(1\\). Filament implements two
paths to adjust this so that the internal clip-space definition remains consistent:
1. Use the `EXT_clip_control` extension to change the depth range to \\([0, 1]\\).
```c++
#if !defined(__EMSCRIPTEN__)
if (ext.EXT_clip_control) {
#if defined(BACKEND_OPENGL_VERSION_GL)
glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE);
#elif defined(GL_EXT_clip_control)
glClipControlEXT(GL_LOWER_LEFT_EXT, GL_ZERO_TO_ONE_EXT);
#endif
}
#endif
```
2. For cases where the extension is not available, the following shader code will convert the
internal (or noted as virtual in the code) coordinates to GL's expected depth coordinates.
```c++
#if !defined(TARGET_VULKAN_ENVIRONMENT) && !defined(TARGET_METAL_ENVIRONMENT) && !defined(TARGET_WEBGPU_ENVIRONMENT)
// This is not needed in Vulkan, Metal or WebGPU because clipControl is always (1, 0)
position.z = position.z * frameUniforms.clipControl.x
+ position.w * frameUniforms.clipControl.y;
#endif
```
This code uses a vector in the uniform buffer to scale and translate the resulting depth. This
vector is written in runtime (so that we'd know whether the extension is present or not).
+44 -60
View File
@@ -56,7 +56,6 @@ set(SRCS
src/materials/bloom/bloom.cpp src/materials/bloom/bloom.cpp
src/materials/colorGrading/colorGrading.cpp src/materials/colorGrading/colorGrading.cpp
src/materials/dof/dof.cpp src/materials/dof/dof.cpp
src/materials/evsm/evsm.cpp
src/materials/flare/flare.cpp src/materials/flare/flare.cpp
src/materials/fog/fog.cpp src/materials/fog/fog.cpp
src/materials/fsr/fsr.cpp src/materials/fsr/fsr.cpp
@@ -286,8 +285,6 @@ set(MATERIAL_SRCS
src/materials/dof/dofMipmap.mat src/materials/dof/dofMipmap.mat
src/materials/dof/dofTiles.mat src/materials/dof/dofTiles.mat
src/materials/dof/dofTilesSwizzle.mat src/materials/dof/dofTilesSwizzle.mat
src/materials/evsm/gaussian.mat
src/materials/evsm/vsmMipmap.mat
src/materials/flare/flare.mat src/materials/flare/flare.mat
src/materials/fog/fog.mat src/materials/fog/fog.mat
src/materials/fsr/fsr_easu.mat src/materials/fsr/fsr_easu.mat
@@ -304,6 +301,7 @@ set(MATERIAL_SRCS
src/materials/ssao/mipmapDepth.mat src/materials/ssao/mipmapDepth.mat
src/materials/ssao/sao.mat src/materials/ssao/sao.mat
src/materials/ssao/saoBentNormals.mat src/materials/ssao/saoBentNormals.mat
src/materials/vsmMipmap.mat
) )
if (NOT FILAMENT_DISABLE_GTAO) if (NOT FILAMENT_DISABLE_GTAO)
@@ -313,16 +311,15 @@ if (NOT FILAMENT_DISABLE_GTAO)
) )
endif() endif()
set(MATERIAL_MULTIVIEW_SRCS set(MATERIAL_FL0_SRCS
src/materials/clearDepth.mat
src/materials/defaultMaterial.mat src/materials/defaultMaterial.mat
src/materials/skybox.mat src/materials/skybox.mat
) )
# This set of materials should only be compiled for vulkan or metal because they contain operations set(MATERIAL_MULTIVIEW_SRCS
# not allowed on other platforms. (e.g. sampling from a multi-sampled texture). src/materials/clearDepth.mat
set(MATERIAL_VULKAN_METAL_SRCS src/materials/defaultMaterial.mat
src/materials/resolveDepth.mat src/materials/skybox.mat
) )
# ================================================================================================== # ==================================================================================================
@@ -344,6 +341,11 @@ if (NOT DFG_LUT_SIZE)
endif() endif()
message(STATUS "DFG LUT size set to ${DFG_LUT_SIZE}x${DFG_LUT_SIZE}") message(STATUS "DFG LUT size set to ${DFG_LUT_SIZE}x${DFG_LUT_SIZE}")
# Whether to include FL0 materials.
if (FILAMENT_ENABLE_FEATURE_LEVEL_0)
add_definitions(-DFILAMENT_ENABLE_FEATURE_LEVEL_0)
endif()
# Whether to include MULTIVIEW materials. # Whether to include MULTIVIEW materials.
if (FILAMENT_ENABLE_MULTIVIEW) if (FILAMENT_ENABLE_MULTIVIEW)
add_definitions(-DFILAMENT_ENABLE_MULTIVIEW) add_definitions(-DFILAMENT_ENABLE_MULTIVIEW)
@@ -393,27 +395,6 @@ foreach(mat_src ${MATERIAL_SRCS})
endforeach() endforeach()
list(REMOVE_DUPLICATES MATERIAL_DIRS) list(REMOVE_DUPLICATES MATERIAL_DIRS)
function(check_in_list list_name item out_var)
list(FIND ${list_name} "${item}" index)
if (index GREATER -1)
set(${out_var} TRUE PARENT_SCOPE)
else()
set(${out_var} FALSE PARENT_SCOPE)
endif()
endfunction()
macro(compile_material output_path fullname flags_var comment_str)
add_custom_command(
OUTPUT ${output_path}
COMMAND matc ${${flags_var}} -o ${output_path} ${fullname}
MAIN_DEPENDENCY ${fullname}
DEPENDS matc
COMMENT "${comment_str}"
)
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path})
endmacro()
# For each directory, generate a single resource package # For each directory, generate a single resource package
foreach(mat_dir ${MATERIAL_DIRS}) foreach(mat_dir ${MATERIAL_DIRS})
get_filename_component(package_name ${mat_dir} NAME) get_filename_component(package_name ${mat_dir} NAME)
@@ -436,35 +417,44 @@ foreach(mat_dir ${MATERIAL_DIRS})
# --- Base material --- # --- Base material ---
set(output_path "${MATERIAL_DIR}/${localname}.filamat") set(output_path "${MATERIAL_DIR}/${localname}.filamat")
set(MATC_CUSTOM_FLAGS "${MATC_BASE_FLAGS}") add_custom_command(
set(COMMENT_STR "Compiling material ${fullname}") OUTPUT ${output_path}
COMMAND matc ${MATC_BASE_FLAGS} -o ${output_path} ${fullname}
MAIN_DEPENDENCY ${fullname}
DEPENDS matc
COMMENT "Compiling material ${fullname}"
)
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path})
check_in_list(MATERIAL_VULKAN_METAL_SRCS ${mat_src} is_vk_metal) # --- Feature Level 0 variant ---
if (is_vk_metal) list(FIND MATERIAL_FL0_SRCS ${mat_src} index)
# Remove webgpu and related parameters if (${index} GREATER -1 AND FILAMENT_ENABLE_FEATURE_LEVEL_0)
# The variant-filter=stereo is added in the root folder's CMakeLists.txt for webgpu string(REGEX REPLACE "[.]filamat$" "_fl0.filamat" output_path_fl0 ${output_path})
# Context is that webgpu does not support the stereo variant add_custom_command(
string(REPLACE "-a;webgpu;--variant-filter=stereo" "" MATC_CUSTOM_FLAGS "${MATC_CUSTOM_FLAGS}") OUTPUT ${output_path_fl0}
COMMAND matc ${MATC_BASE_FLAGS} -PfeatureLevel=0 -o ${output_path_fl0} ${fullname}
# Remove gl MAIN_DEPENDENCY ${fullname}
string(REPLACE "-a;opengl" "" MATC_CUSTOM_FLAGS "${MATC_CUSTOM_FLAGS}") DEPENDS matc
COMMENT "Compiling material ${fullname} (FL0)"
# Remove any empty args )
string(REPLACE ";;" ";" MATC_CUSTOM_FLAGS "${MATC_CUSTOM_FLAGS}") list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path_fl0})
string(REGEX REPLACE "^;|;$" "" MATC_CUSTOM_FLAGS "${MATC_CUSTOM_FLAGS}") list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path_fl0})
set(COMMENT_STR "Compiling material ${fullname} (Vulkan/Metal)")
endif() endif()
compile_material(${output_path} ${fullname} MATC_CUSTOM_FLAGS "${COMMENT_STR}")
# --- Multiview variant --- # --- Multiview variant ---
check_in_list(MATERIAL_MULTIVIEW_SRCS ${mat_src} is_multiview) list(FIND MATERIAL_MULTIVIEW_SRCS ${mat_src} index)
if (is_multiview AND FILAMENT_ENABLE_MULTIVIEW) if (${index} GREATER -1 AND FILAMENT_ENABLE_MULTIVIEW)
string(REGEX REPLACE "[.]filamat$" "_multiview.filamat" output_path_multiview ${output_path}) string(REGEX REPLACE "[.]filamat$" "_multiview.filamat" output_path_multiview ${output_path})
set(MATC_MULTIVIEW_FLAGS "${MATC_BASE_FLAGS}" "-PstereoscopicType=multiview") add_custom_command(
set(COMMENT_STR "Compiling material ${fullname} (Multiview)") OUTPUT ${output_path_multiview}
compile_material(${output_path_multiview} ${fullname} MATC_MULTIVIEW_FLAGS "${COMMENT_STR}") COMMAND matc ${MATC_BASE_FLAGS} -PstereoscopicType=multiview -o ${output_path_multiview} ${fullname}
MAIN_DEPENDENCY ${fullname}
DEPENDS matc
COMMENT "Compiling material ${fullname} (Multiview)"
)
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path_multiview})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path_multiview})
endif() endif()
endforeach() endforeach()
@@ -649,12 +639,6 @@ add_custom_command(
APPEND APPEND
) )
add_custom_command(
OUTPUT "${MATERIAL_DIR}/gaussian.filamat"
DEPENDS src/materials/evsm/gaussian.fs
APPEND
)
file(MAKE_DIRECTORY "${GENERATION_ROOT}/generated/data/") file(MAKE_DIRECTORY "${GENERATION_ROOT}/generated/data/")
set(output_path_dfg_bin "${GENERATION_ROOT}/generated/data/dfg.bin") set(output_path_dfg_bin "${GENERATION_ROOT}/generated/data/dfg.bin")
-2
View File
@@ -86,8 +86,6 @@ if (FILAMENT_SUPPORTS_OPENGL AND NOT FILAMENT_USE_EXTERNAL_GLES3)
src/opengl/OpenGLBlobCache.h src/opengl/OpenGLBlobCache.h
src/opengl/OpenGLContext.cpp src/opengl/OpenGLContext.cpp
src/opengl/OpenGLContext.h src/opengl/OpenGLContext.h
src/opengl/OpenGLState.cpp
src/opengl/OpenGLState.h
src/opengl/OpenGLDriver.cpp src/opengl/OpenGLDriver.cpp
src/opengl/OpenGLDriver.h src/opengl/OpenGLDriver.h
src/opengl/OpenGLDriverFactory.h src/opengl/OpenGLDriverFactory.h
+1 -14
View File
@@ -48,7 +48,7 @@ class ostream;
/** /**
* Types and enums used by filament's driver. * Types and enums used by filament's driver.
* *
* Effectively these types are public but should not be used directly. Instead, use public classes * Effectively these types are public but should not be used directly. Instead use public classes
* internal redeclaration of these types. * internal redeclaration of these types.
* For e.g. Use Texture::Sampler instead of filament::SamplerType. * For e.g. Use Texture::Sampler instead of filament::SamplerType.
*/ */
@@ -1147,19 +1147,6 @@ constexpr bool isDepthFormat(TextureFormat format) noexcept {
} }
} }
//! returns whether this format a 32-bit float format
constexpr bool isFp32ColorFormat(TextureFormat format) noexcept {
switch (format) {
case TextureFormat::R32F:
case TextureFormat::RG32F:
case TextureFormat::RGB32F:
case TextureFormat::RGBA32F:
return true;
default:
return false;
}
}
constexpr bool isStencilFormat(TextureFormat format) noexcept { constexpr bool isStencilFormat(TextureFormat format) noexcept {
switch (format) { switch (format) {
case TextureFormat::STENCIL8: case TextureFormat::STENCIL8:
@@ -398,7 +398,6 @@ DECL_DRIVER_API_SYNCHRONOUS_N(void, getPlatformSync, backend::SyncHandle, sh,
DECL_DRIVER_API_SYNCHRONOUS_N(bool, isTextureFormatSupported, backend::TextureFormat, format) DECL_DRIVER_API_SYNCHRONOUS_N(bool, isTextureFormatSupported, backend::TextureFormat, format)
DECL_DRIVER_API_SYNCHRONOUS_0(bool, isTextureSwizzleSupported) DECL_DRIVER_API_SYNCHRONOUS_0(bool, isTextureSwizzleSupported)
DECL_DRIVER_API_SYNCHRONOUS_N(bool, isTextureFormatMipmappable, backend::TextureFormat, format) DECL_DRIVER_API_SYNCHRONOUS_N(bool, isTextureFormatMipmappable, backend::TextureFormat, format)
DECL_DRIVER_API_SYNCHRONOUS_N(bool, isTextureFormatFilterable, backend::TextureFormat, format)
DECL_DRIVER_API_SYNCHRONOUS_N(bool, isRenderTargetFormatSupported, backend::TextureFormat, format) DECL_DRIVER_API_SYNCHRONOUS_N(bool, isRenderTargetFormatSupported, backend::TextureFormat, format)
DECL_DRIVER_API_SYNCHRONOUS_0(bool, isFrameBufferFetchSupported) DECL_DRIVER_API_SYNCHRONOUS_0(bool, isFrameBufferFetchSupported)
DECL_DRIVER_API_SYNCHRONOUS_0(bool, isFrameBufferFetchMultiSampleSupported) DECL_DRIVER_API_SYNCHRONOUS_0(bool, isFrameBufferFetchMultiSampleSupported)
-22
View File
@@ -214,25 +214,6 @@ public:
void scheduleCallback(CallbackHandler* handler, void* user, CallbackHandler::Callback callback) final; void scheduleCallback(CallbackHandler* handler, void* user, CallbackHandler::Callback callback) final;
template<typename Predicate>
bool waitForFence(Predicate predicate, std::chrono::steady_clock::time_point until) {
std::unique_lock lock(mFenceMutex);
return mFenceCondition.wait_until(lock, until, predicate);
}
template<typename Predicate>
void waitForFence(Predicate predicate) {
std::unique_lock lock(mFenceMutex);
mFenceCondition.wait(lock, predicate);
}
template<typename Action>
void signalFence(Action action) {
std::lock_guard lock(mFenceMutex);
action();
mFenceCondition.notify_all();
}
// -------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------
// Privates // Privates
// -------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------
@@ -272,9 +253,6 @@ private:
std::condition_variable mServiceThreadCondition; std::condition_variable mServiceThreadCondition;
std::vector<std::tuple<CallbackHandler*, CallbackHandler::Callback, void*>> mServiceThreadCallbackQueue; std::vector<std::tuple<CallbackHandler*, CallbackHandler::Callback, void*>> mServiceThreadCallbackQueue;
bool mExitRequested = false; bool mExitRequested = false;
std::condition_variable mFenceCondition;
std::mutex mFenceMutex;
}; };
+9 -5
View File
@@ -114,7 +114,9 @@ void logMTLCommandBufferError(MTLCommandBufferError error) {
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorOutOfMemory) MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorOutOfMemory)
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorInvalidResource) MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorInvalidResource)
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorMemoryless) if (@available(macOS 11.0, *)) {
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorMemoryless)
}
if (@available(iOS 15.0, macOS 12.0, *)) { if (@available(iOS 15.0, macOS 12.0, *)) {
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorStackOverflow) MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorStackOverflow)
@@ -141,10 +143,12 @@ id<MTLCommandBuffer> getPendingCommandBuffer(MetalContext* context) {
context->latestCompletedCommandBufferId = thisCommandBufferId; context->latestCompletedCommandBufferId = thisCommandBufferId;
auto errorCode = (MTLCommandBufferError)buffer.error.code; auto errorCode = (MTLCommandBufferError)buffer.error.code;
if (errorCode == MTLCommandBufferErrorMemoryless) { if (@available(macOS 11.0, *)) {
LOG(WARNING) << "Metal: memoryless geometry limit reached. Continuing with private " if (errorCode == MTLCommandBufferErrorMemoryless) {
"storage mode."; LOG(WARNING) << "Metal: memoryless geometry limit reached. Continuing with private "
context->memorylessLimitsReached = true; "storage mode.";
context->memorylessLimitsReached = true;
}
} }
if (UTILS_UNLIKELY(errorCode != MTLCommandBufferErrorNone)) { if (UTILS_UNLIKELY(errorCode != MTLCommandBufferErrorNone)) {
+11 -25
View File
@@ -1259,21 +1259,6 @@ bool MetalDriver::isTextureFormatMipmappable(TextureFormat format) {
} }
} }
bool MetalDriver::isTextureFormatFilterable(TextureFormat format) {
if (isFp32ColorFormat(format)) {
if (@available(macOS 11.0, iOS 14.0, *)) {
return mContext->device.supports32BitFloatFiltering;
}
return mContext->highestSupportedGpuFamily.apple >= 7 ||
mContext->highestSupportedGpuFamily.mac >= 1;
}
if (isUnsignedIntFormat(format) || isSignedIntFormat(format) ||
isDepthFormat(format) || isStencilFormat(format)) {
return false;
}
return true;
}
bool MetalDriver::isRenderTargetFormatSupported(TextureFormat format) { bool MetalDriver::isRenderTargetFormatSupported(TextureFormat format) {
MTLPixelFormat mtlFormat = getMetalFormat(mContext, format); MTLPixelFormat mtlFormat = getMetalFormat(mContext, format);
// RGB9E5 isn't supported on Mac as a color render target. // RGB9E5 isn't supported on Mac as a color render target.
@@ -1284,8 +1269,11 @@ bool MetalDriver::isFrameBufferFetchSupported() {
// FrameBuffer fetch is achievable via "programmable blending" in Metal, and only supported on // FrameBuffer fetch is achievable via "programmable blending" in Metal, and only supported on
// Apple GPUs with readWriteTextureSupport. // Apple GPUs with readWriteTextureSupport.
// On macOS, framebuffer fetch requires MSL 2.3, which is only available with macOS 11.0. // On macOS, framebuffer fetch requires MSL 2.3, which is only available with macOS 11.0.
return mContext->highestSupportedGpuFamily.apple >= 1 && if (@available(macOS 11.0, *)) {
mContext->device.readWriteTextureSupport; return mContext->highestSupportedGpuFamily.apple >= 1 &&
mContext->device.readWriteTextureSupport;
}
return false;
} }
bool MetalDriver::isFrameBufferFetchMultiSampleSupported() { bool MetalDriver::isFrameBufferFetchMultiSampleSupported() {
@@ -2295,14 +2283,12 @@ void MetalDriver::draw2(uint32_t indexOffset, uint32_t indexCount, uint32_t inst
// Bind the offset data. // Bind the offset data.
if (mContext->dynamicOffsets.isDirty()) { if (mContext->dynamicOffsets.isDirty()) {
const auto [size, data] = mContext->dynamicOffsets.getOffsets(); const auto [size, data] = mContext->dynamicOffsets.getOffsets();
if (size > 0) { [mContext->currentRenderPassEncoder setFragmentBytes:data
[mContext->currentRenderPassEncoder setFragmentBytes:data length:size * sizeof(uint32_t)
length:size * sizeof(uint32_t) atIndex:DYNAMIC_OFFSET_BINDING];
atIndex:DYNAMIC_OFFSET_BINDING]; [mContext->currentRenderPassEncoder setVertexBytes:data
[mContext->currentRenderPassEncoder setVertexBytes:data length:size * sizeof(uint32_t)
length:size * sizeof(uint32_t) atIndex:DYNAMIC_OFFSET_BINDING];
atIndex:DYNAMIC_OFFSET_BINDING];
}
mContext->dynamicOffsets.setDirty(false); mContext->dynamicOffsets.setDirty(false);
} }
+30 -26
View File
@@ -148,7 +148,9 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
inline MTLPixelFormat getMetalFormat(PixelDataFormat format, PixelDataType type) { inline MTLPixelFormat getMetalFormat(PixelDataFormat format, PixelDataType type) {
if (type == PixelDataType::UINT_2_10_10_10_REV) return MTLPixelFormatRGB10A2Unorm; if (type == PixelDataType::UINT_2_10_10_10_REV) return MTLPixelFormatRGB10A2Unorm;
if (type == PixelDataType::UINT_10F_11F_11F_REV) return MTLPixelFormatRG11B10Float; if (type == PixelDataType::UINT_10F_11F_11F_REV) return MTLPixelFormatRG11B10Float;
if (type == PixelDataType::USHORT_565) return MTLPixelFormatB5G6R5Unorm; if (@available(macOS 11, *)) {
if (type == PixelDataType::USHORT_565) return MTLPixelFormatB5G6R5Unorm;
}
#define CONVERT(FORMAT, TYPE, MTL) \ #define CONVERT(FORMAT, TYPE, MTL) \
if (PixelDataFormat::FORMAT == format && PixelDataType::TYPE == type) return MTLPixelFormat ## MTL; if (PixelDataFormat::FORMAT == format && PixelDataType::TYPE == type) return MTLPixelFormat ## MTL;
@@ -203,31 +205,33 @@ inline MTLPixelFormat getMetalFormatLinear(MTLPixelFormat format) {
#endif #endif
default: break; default: break;
} }
switch (format) { if (@available(macOS 11, *)) {
case MTLPixelFormatASTC_4x4_sRGB: return MTLPixelFormatASTC_4x4_LDR; switch (format) {
case MTLPixelFormatASTC_5x4_sRGB: return MTLPixelFormatASTC_5x4_LDR; case MTLPixelFormatASTC_4x4_sRGB: return MTLPixelFormatASTC_4x4_LDR;
case MTLPixelFormatASTC_5x5_sRGB: return MTLPixelFormatASTC_5x5_LDR; case MTLPixelFormatASTC_5x4_sRGB: return MTLPixelFormatASTC_5x4_LDR;
case MTLPixelFormatASTC_6x5_sRGB: return MTLPixelFormatASTC_6x5_LDR; case MTLPixelFormatASTC_5x5_sRGB: return MTLPixelFormatASTC_5x5_LDR;
case MTLPixelFormatASTC_6x6_sRGB: return MTLPixelFormatASTC_6x6_LDR; case MTLPixelFormatASTC_6x5_sRGB: return MTLPixelFormatASTC_6x5_LDR;
case MTLPixelFormatASTC_8x5_sRGB: return MTLPixelFormatASTC_8x5_LDR; case MTLPixelFormatASTC_6x6_sRGB: return MTLPixelFormatASTC_6x6_LDR;
case MTLPixelFormatASTC_8x6_sRGB: return MTLPixelFormatASTC_8x6_LDR; case MTLPixelFormatASTC_8x5_sRGB: return MTLPixelFormatASTC_8x5_LDR;
case MTLPixelFormatASTC_8x8_sRGB: return MTLPixelFormatASTC_8x8_LDR; case MTLPixelFormatASTC_8x6_sRGB: return MTLPixelFormatASTC_8x6_LDR;
case MTLPixelFormatASTC_10x5_sRGB: return MTLPixelFormatASTC_10x5_LDR; case MTLPixelFormatASTC_8x8_sRGB: return MTLPixelFormatASTC_8x8_LDR;
case MTLPixelFormatASTC_10x6_sRGB: return MTLPixelFormatASTC_10x6_LDR; case MTLPixelFormatASTC_10x5_sRGB: return MTLPixelFormatASTC_10x5_LDR;
case MTLPixelFormatASTC_10x8_sRGB: return MTLPixelFormatASTC_10x8_LDR; case MTLPixelFormatASTC_10x6_sRGB: return MTLPixelFormatASTC_10x6_LDR;
case MTLPixelFormatASTC_10x10_sRGB: return MTLPixelFormatASTC_10x10_LDR; case MTLPixelFormatASTC_10x8_sRGB: return MTLPixelFormatASTC_10x8_LDR;
case MTLPixelFormatASTC_12x10_sRGB: return MTLPixelFormatASTC_12x10_LDR; case MTLPixelFormatASTC_10x10_sRGB: return MTLPixelFormatASTC_10x10_LDR;
case MTLPixelFormatASTC_12x12_sRGB: return MTLPixelFormatASTC_12x12_LDR; case MTLPixelFormatASTC_12x10_sRGB: return MTLPixelFormatASTC_12x10_LDR;
case MTLPixelFormatPVRTC_RGB_2BPP_sRGB: return MTLPixelFormatPVRTC_RGB_2BPP; case MTLPixelFormatASTC_12x12_sRGB: return MTLPixelFormatASTC_12x12_LDR;
case MTLPixelFormatPVRTC_RGB_4BPP_sRGB: return MTLPixelFormatPVRTC_RGB_4BPP; case MTLPixelFormatPVRTC_RGB_2BPP_sRGB: return MTLPixelFormatPVRTC_RGB_2BPP;
case MTLPixelFormatPVRTC_RGBA_2BPP_sRGB: return MTLPixelFormatPVRTC_RGBA_2BPP; case MTLPixelFormatPVRTC_RGB_4BPP_sRGB: return MTLPixelFormatPVRTC_RGB_4BPP;
case MTLPixelFormatPVRTC_RGBA_4BPP_sRGB: return MTLPixelFormatPVRTC_RGBA_4BPP; case MTLPixelFormatPVRTC_RGBA_2BPP_sRGB: return MTLPixelFormatPVRTC_RGBA_2BPP;
case MTLPixelFormatEAC_RGBA8_sRGB: return MTLPixelFormatEAC_RGBA8; case MTLPixelFormatPVRTC_RGBA_4BPP_sRGB: return MTLPixelFormatPVRTC_RGBA_4BPP;
case MTLPixelFormatETC2_RGB8_sRGB: return MTLPixelFormatETC2_RGB8; case MTLPixelFormatEAC_RGBA8_sRGB: return MTLPixelFormatEAC_RGBA8;
case MTLPixelFormatETC2_RGB8A1_sRGB: return MTLPixelFormatETC2_RGB8A1; case MTLPixelFormatETC2_RGB8_sRGB: return MTLPixelFormatETC2_RGB8;
case MTLPixelFormatBGR10_XR_sRGB: return MTLPixelFormatBGR10_XR; case MTLPixelFormatETC2_RGB8A1_sRGB: return MTLPixelFormatETC2_RGB8A1;
case MTLPixelFormatBGRA10_XR_sRGB: return MTLPixelFormatBGRA10_XR; case MTLPixelFormatBGR10_XR_sRGB: return MTLPixelFormatBGR10_XR;
default: break; case MTLPixelFormatBGRA10_XR_sRGB: return MTLPixelFormatBGRA10_XR;
default: break;
}
} }
return format; return format;
} }
+8 -2
View File
@@ -98,12 +98,14 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
// Packed 16 bit formats are only available on Apple GPUs. // Packed 16 bit formats are only available on Apple GPUs.
if (context->highestSupportedGpuFamily.apple >= 1) { if (context->highestSupportedGpuFamily.apple >= 1) {
if (@available(macOS 11.0, *)) {
switch (format) { switch (format) {
case TextureFormat::RGB565: return MTLPixelFormatB5G6R5Unorm; case TextureFormat::RGB565: return MTLPixelFormatB5G6R5Unorm;
case TextureFormat::RGB5_A1: return MTLPixelFormatA1BGR5Unorm; case TextureFormat::RGB5_A1: return MTLPixelFormatA1BGR5Unorm;
case TextureFormat::RGBA4: return MTLPixelFormatABGR4Unorm; case TextureFormat::RGBA4: return MTLPixelFormatABGR4Unorm;
default: break; default: break;
} }
}
} }
if (@available(iOS 13.0, *)) { if (@available(iOS 13.0, *)) {
@@ -113,16 +115,18 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
#if TARGET_OS_OSX #if TARGET_OS_OSX
if (context->highestSupportedGpuFamily.mac >= 1 && if (context->highestSupportedGpuFamily.mac >= 1 &&
context->device.depth24Stencil8PixelFormatSupported) { context->device.depth24Stencil8PixelFormatSupported) {
if (@available(macOS 11.0, *)) {
if (format == TextureFormat::DEPTH24_STENCIL8) { if (format == TextureFormat::DEPTH24_STENCIL8) {
return MTLPixelFormatDepth24Unorm_Stencil8; return MTLPixelFormatDepth24Unorm_Stencil8;
} }
}
} }
#endif #endif
// Only iOS 13.0 and Apple Silicon support the ASTC HDR profile. Older OS versions fallback to // Only iOS 13.0 and Apple Silicon support the ASTC HDR profile. Older OS versions fallback to
// LDR. The HDR profile is a superset of the LDR profile. // LDR. The HDR profile is a superset of the LDR profile.
if (context->highestSupportedGpuFamily.apple >= 2) { if (context->highestSupportedGpuFamily.apple >= 2) {
if (@available(iOS 13, *)) { if (@available(iOS 13, macOS 11.0, *)) {
switch (format) { switch (format) {
case TextureFormat::RGBA_ASTC_4x4: return MTLPixelFormatASTC_4x4_HDR; case TextureFormat::RGBA_ASTC_4x4: return MTLPixelFormatASTC_4x4_HDR;
case TextureFormat::RGBA_ASTC_5x4: return MTLPixelFormatASTC_5x4_HDR; case TextureFormat::RGBA_ASTC_5x4: return MTLPixelFormatASTC_5x4_HDR;
@@ -140,7 +144,7 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
case TextureFormat::RGBA_ASTC_12x12: return MTLPixelFormatASTC_12x12_HDR; case TextureFormat::RGBA_ASTC_12x12: return MTLPixelFormatASTC_12x12_HDR;
default: break; default: break;
} }
} else { } else if (@available(macOS 11.0, *)) {
switch (format) { switch (format) {
case TextureFormat::RGBA_ASTC_4x4: return MTLPixelFormatASTC_4x4_LDR; case TextureFormat::RGBA_ASTC_4x4: return MTLPixelFormatASTC_4x4_LDR;
case TextureFormat::RGBA_ASTC_5x4: return MTLPixelFormatASTC_5x4_LDR; case TextureFormat::RGBA_ASTC_5x4: return MTLPixelFormatASTC_5x4_LDR;
@@ -163,6 +167,7 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
// EAC / ETC2 formats are only available on Apple GPUs. // EAC / ETC2 formats are only available on Apple GPUs.
if (context->highestSupportedGpuFamily.apple >= 1) { if (context->highestSupportedGpuFamily.apple >= 1) {
if (@available(macOS 11.0, *)) {
switch (format) { switch (format) {
case TextureFormat::EAC_R11: return MTLPixelFormatEAC_R11Unorm; case TextureFormat::EAC_R11: return MTLPixelFormatEAC_R11Unorm;
case TextureFormat::EAC_R11_SIGNED: return MTLPixelFormatEAC_R11Snorm; case TextureFormat::EAC_R11_SIGNED: return MTLPixelFormatEAC_R11Snorm;
@@ -176,6 +181,7 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
case TextureFormat::ETC2_EAC_SRGBA8: return MTLPixelFormatEAC_RGBA8_sRGB; case TextureFormat::ETC2_EAC_SRGBA8: return MTLPixelFormatEAC_RGBA8_sRGB;
default: break; default: break;
} }
}
} }
// DXT (BC) formats are only available on macOS desktop. // DXT (BC) formats are only available on macOS desktop.
@@ -478,6 +478,8 @@ private:
struct State { struct State {
FenceStatus status { FenceStatus::TIMEOUT_EXPIRED }; FenceStatus status { FenceStatus::TIMEOUT_EXPIRED };
std::condition_variable cv;
std::mutex mutex;
}; };
std::shared_ptr<State> state { std::make_shared<State>() }; std::shared_ptr<State> state { std::make_shared<State>() };
+20 -23
View File
@@ -20,7 +20,6 @@
#include "MetalEnums.h" #include "MetalEnums.h"
#include "MetalUtils.h" #include "MetalUtils.h"
#include "MetalBufferPool.h" #include "MetalBufferPool.h"
#include "MetalDriver.h"
#include <filament/SwapChain.h> #include <filament/SwapChain.h>
@@ -184,7 +183,9 @@ id<MTLTexture> MetalSwapChain::createMultisampledTexture(MetalContext const& con
descriptor.resourceOptions = MTLResourceStorageModePrivate; descriptor.resourceOptions = MTLResourceStorageModePrivate;
if (context.supportsMemorylessRenderTargets) { if (context.supportsMemorylessRenderTargets) {
descriptor.resourceOptions = MTLResourceStorageModeMemoryless; if (@available(macOS 11.0, *)) {
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
}
} }
return [context.device newTextureWithDescriptor:descriptor]; return [context.device newTextureWithDescriptor:descriptor];
@@ -1374,7 +1375,9 @@ id<MTLTexture> MetalRenderTarget::createMultisampledTexture(MTLPixelFormat forma
descriptor.resourceOptions = MTLResourceStorageModePrivate; descriptor.resourceOptions = MTLResourceStorageModePrivate;
if (context->supportsMemorylessRenderTargets) { if (context->supportsMemorylessRenderTargets) {
descriptor.resourceOptions = MTLResourceStorageModeMemoryless; if (@available(macOS 11.0, *)) {
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
}
} }
return [context->device newTextureWithDescriptor:descriptor]; return [context->device newTextureWithDescriptor:descriptor];
@@ -1401,13 +1404,12 @@ void MetalFence::encode() {
// Using a weak_ptr here because the Fence could be deleted before the block executes. // Using a weak_ptr here because the Fence could be deleted before the block executes.
std::weak_ptr<State> weakState = state; std::weak_ptr<State> weakState = state;
MetalDriver* driver = context.driver;
[event notifyListener:context.eventListener atValue:value block:^(id <MTLSharedEvent> o, [event notifyListener:context.eventListener atValue:value block:^(id <MTLSharedEvent> o,
uint64_t value) { uint64_t value) {
if (auto s = weakState.lock()) { if (auto s = weakState.lock()) {
driver->signalFence([&] { std::lock_guard<std::mutex> guard(s->mutex);
s->status = FenceStatus::CONDITION_SATISFIED; s->status = FenceStatus::CONDITION_SATISFIED;
}); s->cv.notify_all();
} }
}]; }];
} }
@@ -1420,29 +1422,24 @@ void MetalFence::onSignal(MetalFenceSignalBlock block) {
FenceStatus MetalFence::wait(uint64_t timeoutNs) { FenceStatus MetalFence::wait(uint64_t timeoutNs) {
if (@available(iOS 12, *)) { if (@available(iOS 12, *)) {
using ns = std::chrono::nanoseconds; using ns = std::chrono::nanoseconds;
FenceStatus result = FenceStatus::TIMEOUT_EXPIRED; std::unique_lock<std::mutex> guard(state->mutex);
auto predicate = [&] { while (state->status == FenceStatus::TIMEOUT_EXPIRED) {
if (state->status != FenceStatus::TIMEOUT_EXPIRED) { if (timeoutNs == FENCE_WAIT_FOR_EVER) {
result = state->status; state->cv.wait(guard);
return true; } else if (timeoutNs == 0 ||
state->cv.wait_for(guard, ns(timeoutNs)) == std::cv_status::timeout) {
return state->status;
} }
return false;
};
if (timeoutNs == FENCE_WAIT_FOR_EVER) {
context.driver->waitForFence(predicate);
} else {
auto const until = std::chrono::steady_clock::now() + ns(timeoutNs);
context.driver->waitForFence(predicate, until);
} }
return result; return state->status;
} }
return FenceStatus::ERROR; return FenceStatus::ERROR;
} }
void MetalFence::cancel() { void MetalFence::cancel() {
context.driver->signalFence([&] { std::unique_lock guard(state->mutex);
state->status = FenceStatus::ERROR; state->status = FenceStatus::ERROR;
}); state->cv.notify_all();
} }
MetalDescriptorSetLayout::MetalDescriptorSetLayout(DescriptorSetLayout&& l) noexcept MetalDescriptorSetLayout::MetalDescriptorSetLayout(DescriptorSetLayout&& l) noexcept
-4
View File
@@ -194,10 +194,6 @@ bool NoopDriver::isTextureFormatMipmappable(TextureFormat format) {
return true; return true;
} }
bool NoopDriver::isTextureFormatFilterable(TextureFormat format) {
return true;
}
bool NoopDriver::isRenderTargetFormatSupported(TextureFormat format) { bool NoopDriver::isRenderTargetFormatSupported(TextureFormat format) {
return true; return true;
} }
@@ -21,8 +21,8 @@
#include "GLTexture.h" #include "GLTexture.h"
#include "GLUtils.h" #include "GLUtils.h"
#include "OpenGLDriver.h" #include "OpenGLDriver.h"
#include "OpenGLContext.h"
#include "OpenGLProgram.h" #include "OpenGLProgram.h"
#include "OpenGLState.h"
#include "gl_headers.h" #include "gl_headers.h"
@@ -49,7 +49,7 @@
namespace filament::backend { namespace filament::backend {
GLDescriptorSet::GLDescriptorSet(OpenGLState& gl, DescriptorSetLayoutHandle dslh, GLDescriptorSet::GLDescriptorSet(OpenGLContext& gl, DescriptorSetLayoutHandle dslh,
GLDescriptorSetLayout const* layout) GLDescriptorSetLayout const* layout)
: descriptors(layout->maxDescriptorBinding + 1), : descriptors(layout->maxDescriptorBinding + 1),
dslh(std::move(dslh)) { dslh(std::move(dslh)) {
@@ -142,7 +142,7 @@ GLDescriptorSet::GLDescriptorSet(OpenGLState& gl, DescriptorSetLayoutHandle dslh
} }
} }
void GLDescriptorSet::update(OpenGLState&, void GLDescriptorSet::update(OpenGLContext&,
descriptor_binding_t binding, GLBufferObject* bo, size_t offset, size_t size) noexcept { descriptor_binding_t binding, GLBufferObject* bo, size_t offset, size_t size) noexcept {
assert_invariant(binding < descriptors.size()); assert_invariant(binding < descriptors.size());
std::visit([=](auto&& arg) { std::visit([=](auto&& arg) {
@@ -164,7 +164,7 @@ void GLDescriptorSet::update(OpenGLState&,
}, descriptors[binding].desc); }, descriptors[binding].desc);
} }
void GLDescriptorSet::update(OpenGLState& gl, HandleAllocatorGL& handleAllocator, void GLDescriptorSet::update(OpenGLContext& gl, HandleAllocatorGL& handleAllocator,
descriptor_binding_t binding, TextureHandle th, SamplerParams params) noexcept { descriptor_binding_t binding, TextureHandle th, SamplerParams params) noexcept {
GLTexture* t = th ? handleAllocator.handle_cast<GLTexture*>(th) : nullptr; GLTexture* t = th ? handleAllocator.handle_cast<GLTexture*>(th) : nullptr;
@@ -183,18 +183,9 @@ void GLDescriptorSet::update(OpenGLState& gl, HandleAllocatorGL& handleAllocator
params.wrapT = SamplerWrapMode::CLAMP_TO_EDGE; params.wrapT = SamplerWrapMode::CLAMP_TO_EDGE;
params.wrapR = SamplerWrapMode::CLAMP_TO_EDGE; params.wrapR = SamplerWrapMode::CLAMP_TO_EDGE;
} }
// GLES3.x specification forbids depth textures to be filtered. // GLES3.x specification forbids depth textures to be filtered.
bool const isDepthFmt = t && isDepthFormat(t->format); if (t && isDepthFormat(t->format)
bool const depthTextureNoPcf = isDepthFmt && && params.compareMode == SamplerCompareMode::NONE) {
params.compareMode == SamplerCompareMode::NONE;
// GLES3.x may not support filtering of fp32 textures
bool const isFp32Fmt = t && isFp32ColorFormat(t->format);
bool const fp32TextureNotFilterable = isFp32Fmt &&
!gl.context().ext.OES_texture_float_linear;
if (t && (depthTextureNoPcf || fp32TextureNotFilterable)) {
params.filterMag = SamplerMagFilter::NEAREST; params.filterMag = SamplerMagFilter::NEAREST;
switch (params.filterMin) { switch (params.filterMin) {
case SamplerMinFilter::LINEAR: case SamplerMinFilter::LINEAR:
@@ -232,7 +223,7 @@ void GLDescriptorSet::update(OpenGLState& gl, HandleAllocatorGL& handleAllocator
}, descriptors[binding].desc); }, descriptors[binding].desc);
} }
void GLDescriptorSet::updateTextureView(OpenGLState& gl, void GLDescriptorSet::updateTextureView(OpenGLContext& gl,
HandleAllocatorGL& handleAllocator, GLuint unit, GLTexture const* t) noexcept { HandleAllocatorGL& handleAllocator, GLuint unit, GLTexture const* t) noexcept {
// The common case is that we don't have a ref handle (we only have one if // The common case is that we don't have a ref handle (we only have one if
// the texture ever had a View on it). // the texture ever had a View on it).
@@ -269,7 +260,7 @@ void GLDescriptorSet::updateTextureView(OpenGLState& gl,
} }
void GLDescriptorSet::bind( void GLDescriptorSet::bind(
OpenGLState& gl, OpenGLContext& gl,
HandleAllocatorGL& handleAllocator, HandleAllocatorGL& handleAllocator,
OpenGLProgram const& p, OpenGLProgram const& p,
descriptor_set_t set, uint32_t const* offsets, bool offsetsOnly) const noexcept { descriptor_set_t set, uint32_t const* offsets, bool offsetsOnly) const noexcept {
@@ -387,7 +378,7 @@ void GLDescriptorSet::bind(
} }
} }
#endif #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,
@@ -44,27 +44,27 @@ struct GLTexture;
struct GLTextureRef; struct GLTextureRef;
struct GLDescriptorSetLayout; struct GLDescriptorSetLayout;
class OpenGLProgram; class OpenGLProgram;
class OpenGLContext;
class OpenGLDriver; class OpenGLDriver;
class OpenGLState;
struct GLDescriptorSet : public HwDescriptorSet { struct GLDescriptorSet : public HwDescriptorSet {
using HwDescriptorSet::HwDescriptorSet; using HwDescriptorSet::HwDescriptorSet;
GLDescriptorSet(OpenGLState& gl, DescriptorSetLayoutHandle dslh, GLDescriptorSet(OpenGLContext& gl, DescriptorSetLayoutHandle dslh,
GLDescriptorSetLayout const* layout); GLDescriptorSetLayout const* layout);
// update a buffer descriptor in the set // update a buffer descriptor in the set
void update(OpenGLState& gl, void update(OpenGLContext& gl,
descriptor_binding_t binding, GLBufferObject* bo, size_t offset, size_t size) noexcept; descriptor_binding_t binding, GLBufferObject* bo, size_t offset, size_t size) noexcept;
// update a sampler descriptor in the set // update a sampler descriptor in the set
void update(OpenGLState& gl, HandleAllocatorGL& handleAllocator, void update(OpenGLContext& gl, HandleAllocatorGL& handleAllocator,
descriptor_binding_t binding, TextureHandle th, SamplerParams params) noexcept; descriptor_binding_t binding, TextureHandle th, SamplerParams params) noexcept;
// conceptually bind the set to the command buffer // conceptually bind the set to the command buffer
void bind( void bind(
OpenGLState& gl, OpenGLContext& gl,
HandleAllocatorGL& handleAllocator, HandleAllocatorGL& handleAllocator,
OpenGLProgram const& p, OpenGLProgram const& p,
descriptor_set_t set, uint32_t const* offsets, bool offsetsOnly) const noexcept; descriptor_set_t set, uint32_t const* offsets, bool offsetsOnly) const noexcept;
@@ -138,7 +138,7 @@ private:
}; };
static_assert(sizeof(Descriptor) <= 32); static_assert(sizeof(Descriptor) <= 32);
static void updateTextureView(OpenGLState& gl, static void updateTextureView(OpenGLContext& gl,
HandleAllocatorGL& handleAllocator, GLuint unit, GLTexture const* t) noexcept; HandleAllocatorGL& handleAllocator, GLuint unit, GLTexture const* t) noexcept;
utils::FixedCapacityVector<Descriptor> descriptors; // 16 utils::FixedCapacityVector<Descriptor> descriptors; // 16
@@ -18,8 +18,8 @@
#include "GLBufferObject.h" #include "GLBufferObject.h"
#include "GLUtils.h" #include "GLUtils.h"
#include "OpenGLContext.h"
#include "OpenGLDriver.h" #include "OpenGLDriver.h"
#include "OpenGLState.h"
#include "gl_headers.h" #include "gl_headers.h"
@@ -45,7 +45,7 @@ GLMemoryMappedBuffer::GLMemoryMappedBuffer() = default;
GLMemoryMappedBuffer::~GLMemoryMappedBuffer() = default; GLMemoryMappedBuffer::~GLMemoryMappedBuffer() = default;
GLMemoryMappedBuffer::GLMemoryMappedBuffer(OpenGLState& gls, HandleAllocatorGL& handleAllocator, GLMemoryMappedBuffer::GLMemoryMappedBuffer(OpenGLContext& glc, HandleAllocatorGL& handleAllocator,
BufferObjectHandle boh, BufferObjectHandle boh,
size_t const offset, size_t const size, MapBufferAccessFlags const access) size_t const offset, size_t const size, MapBufferAccessFlags const access)
: boh(boh), access(access) { : boh(boh), access(access) {
@@ -60,7 +60,7 @@ GLMemoryMappedBuffer::GLMemoryMappedBuffer(OpenGLState& gls, HandleAllocatorGL&
assert_invariant(any(bo->usage & BufferUsage::SHARED_WRITE_BIT)); assert_invariant(any(bo->usage & BufferUsage::SHARED_WRITE_BIT));
} }
if (UTILS_UNLIKELY(gls.isES2())) { if (UTILS_UNLIKELY(glc.isES2())) {
gl.vaddr = static_cast<char*>(bo->gl.buffer) + offset; gl.vaddr = static_cast<char*>(bo->gl.buffer) + offset;
gl.size = size; gl.size = size;
gl.offset = offset; gl.offset = offset;
@@ -85,7 +85,7 @@ GLMemoryMappedBuffer::GLMemoryMappedBuffer(OpenGLState& gls, HandleAllocatorGL&
gl_access |= GL_MAP_INVALIDATE_RANGE_BIT; gl_access |= GL_MAP_INVALIDATE_RANGE_BIT;
} }
gls.bindBuffer(bo->gl.binding, bo->gl.id); glc.bindBuffer(bo->gl.binding, bo->gl.id);
addr = glMapBufferRange(bo->gl.binding, GLsizeiptr(offset), GLsizeiptr(size), gl_access); addr = glMapBufferRange(bo->gl.binding, GLsizeiptr(offset), GLsizeiptr(size), gl_access);
@@ -103,20 +103,20 @@ GLMemoryMappedBuffer::GLMemoryMappedBuffer(OpenGLState& gls, HandleAllocatorGL&
#endif #endif
} }
void GLMemoryMappedBuffer::unmap(OpenGLState& gls, HandleAllocatorGL& handleAllocator) const { void GLMemoryMappedBuffer::unmap(OpenGLContext& glc, HandleAllocatorGL& handleAllocator) const {
GLBufferObject* const bo = handleAllocator.handle_cast<GLBufferObject*>(boh); GLBufferObject* const bo = handleAllocator.handle_cast<GLBufferObject*>(boh);
assert_invariant(bo); assert_invariant(bo);
assert_invariant(bo->mappingCount > 0); assert_invariant(bo->mappingCount > 0);
bo->mappingCount--; bo->mappingCount--;
if (UTILS_UNLIKELY(gls.isES2())) { if (UTILS_UNLIKELY(glc.isES2())) {
// nothing to do // nothing to do
return; return;
} }
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2 #ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
gls.bindBuffer(gl.binding, gl.id); glc.bindBuffer(gl.binding, gl.id);
# if !defined(__EMSCRIPTEN__) # if !defined(__EMSCRIPTEN__)
// don't unmap if we don't have a mapping or it didn't work // don't unmap if we don't have a mapping or it didn't work
if (UTILS_LIKELY(gl.vaddr)) { if (UTILS_LIKELY(gl.vaddr)) {
@@ -131,7 +131,7 @@ void GLMemoryMappedBuffer::unmap(OpenGLState& gls, HandleAllocatorGL& handleAllo
#endif #endif
} }
void GLMemoryMappedBuffer::copy(OpenGLState& gls, OpenGLDriver& gld, void GLMemoryMappedBuffer::copy(OpenGLContext& glc, OpenGLDriver& gld,
size_t const offset, BufferDescriptor&& data) const { size_t const offset, BufferDescriptor&& data) const {
assert_invariant(any(access & MapBufferAccessFlags::WRITE_BIT)); assert_invariant(any(access & MapBufferAccessFlags::WRITE_BIT));
assert_invariant(offset + data.size <= gl.size); assert_invariant(offset + data.size <= gl.size);
@@ -139,11 +139,11 @@ void GLMemoryMappedBuffer::copy(OpenGLState& gls, OpenGLDriver& gld,
if (UTILS_LIKELY(gl.vaddr)) { if (UTILS_LIKELY(gl.vaddr)) {
memcpy(static_cast<char *>(gl.vaddr) + offset, data.buffer, data.size); memcpy(static_cast<char *>(gl.vaddr) + offset, data.buffer, data.size);
} else { } else {
assert_invariant(!gls.isES2()); assert_invariant(!glc.isES2());
// we couldn't map (WebGL or error), revert to glBufferSubData. In the future we can // we couldn't map (WebGL or error), revert to glBufferSubData. In the future we can
// improve this by keeping around the BufferDescriptor and coalescing the calls // improve this by keeping around the BufferDescriptor and coalescing the calls
// to glBufferSubData. // to glBufferSubData.
gls.bindBuffer(gl.binding, gl.id); glc.bindBuffer(gl.binding, gl.id);
glBufferSubData(gl.binding, GLintptr(gl.offset + offset), GLsizeiptr(data.size), data.buffer); glBufferSubData(gl.binding, GLintptr(gl.offset + offset), GLsizeiptr(data.size), data.buffer);
CHECK_GL_ERROR(); CHECK_GL_ERROR();
} }
@@ -31,7 +31,7 @@
namespace filament::backend { namespace filament::backend {
class OpenGLState; class OpenGLContext;
class OpenGLDriver; class OpenGLDriver;
class BufferDescriptor; class BufferDescriptor;
@@ -50,14 +50,14 @@ struct GLMemoryMappedBuffer : public HwMemoryMappedBuffer {
GLMemoryMappedBuffer(); GLMemoryMappedBuffer();
GLMemoryMappedBuffer(OpenGLState& gls, HandleAllocatorGL& handleAllocator, GLMemoryMappedBuffer(OpenGLContext& glc, HandleAllocatorGL& handleAllocator,
BufferObjectHandle boh, size_t offset, size_t size, MapBufferAccessFlags access); BufferObjectHandle boh, size_t offset, size_t size, MapBufferAccessFlags access);
~GLMemoryMappedBuffer(); ~GLMemoryMappedBuffer();
void unmap(OpenGLState& gls, HandleAllocatorGL& handleAllocator) const; void unmap(OpenGLContext& gl, HandleAllocatorGL& handleAllocator) const;
void copy(OpenGLState& gls, OpenGLDriver& gld, void copy(OpenGLContext& glc, OpenGLDriver& gld,
size_t offset, BufferDescriptor&& data) const; size_t offset, BufferDescriptor&& data) const;
}; };
+544 -34
View File
@@ -15,7 +15,6 @@
*/ */
#include "OpenGLContext.h" #include "OpenGLContext.h"
#include "OpenGLState.h"
#include "GLUtils.h" #include "GLUtils.h"
#include "OpenGLTimerQuery.h" #include "OpenGLTimerQuery.h"
@@ -30,7 +29,9 @@
#include <utils/ostream.h> #include <utils/ostream.h>
#include <algorithm> #include <algorithm>
#include <functional>
#include <string_view> #include <string_view>
#include <utility>
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
@@ -67,37 +68,40 @@ bool OpenGLContext::queryOpenGLVersion(GLint* major, GLint* minor) noexcept {
OpenGLContext::OpenGLContext(OpenGLPlatform& platform, OpenGLContext::OpenGLContext(OpenGLPlatform& platform,
Platform::DriverConfig const& driverConfig) noexcept Platform::DriverConfig const& driverConfig) noexcept
: mPlatform(platform), : mPlatform(platform),
mSamplerMap(32),
mDriverConfig(driverConfig) { mDriverConfig(driverConfig) {
// These queries work with all GL/GLES versions! state.vao.p = &mDefaultVAO;
vendor = (char const*)glGetString(GL_VENDOR);
renderer = (char const*)glGetString(GL_RENDERER);
version = (char const*)glGetString(GL_VERSION);
shader = (char const*)glGetString(GL_SHADING_LANGUAGE_VERSION);
LOG(INFO) << "[" << vendor << "], [" << renderer << "], " // These queries work with all GL/GLES versions!
"[" << version << "], [" << shader << "]"; state.vendor = (char const*)glGetString(GL_VENDOR);
state.renderer = (char const*)glGetString(GL_RENDERER);
state.version = (char const*)glGetString(GL_VERSION);
state.shader = (char const*)glGetString(GL_SHADING_LANGUAGE_VERSION);
LOG(INFO) << "[" << state.vendor << "], [" << state.renderer << "], "
"[" << state.version << "], [" << state.shader << "]";
/* /*
* Figure out GL / GLES version, extensions and capabilities we need to * Figure out GL / GLES version, extensions and capabilities we need to
* determine the feature level * determine the feature level
*/ */
queryOpenGLVersion(&major, &minor); queryOpenGLVersion(&state.major, &state.minor);
#if defined(BACKEND_OPENGL_VERSION_GLES) #if defined(BACKEND_OPENGL_VERSION_GLES)
if (UTILS_UNLIKELY(driverConfig.forceGLES2Context)) { if (UTILS_UNLIKELY(driverConfig.forceGLES2Context)) {
major = 2; state.major = 2;
minor = 0; state.minor = 0;
} }
#endif #endif
initExtensions(&ext, major, minor); initExtensions(&ext, state.major, state.minor);
initProcs(&procs, ext, major, minor); initProcs(&procs, ext, state.major, state.minor);
initBugs(&bugs, ext, major, minor, initBugs(&bugs, ext, state.major, state.minor,
vendor, renderer, version, shader); state.vendor, state.renderer, state.version, state.shader);
initWorkarounds(bugs, &ext); initWorkarounds(bugs, &ext);
@@ -109,7 +113,7 @@ OpenGLContext::OpenGLContext(OpenGLPlatform& platform,
glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE, &gets.max_3d_texture_size); glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE, &gets.max_3d_texture_size);
glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &gets.max_array_texture_layers); glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &gets.max_array_texture_layers);
mFeatureLevel = resolveFeatureLevel(major, minor, ext, gets, bugs); mFeatureLevel = resolveFeatureLevel(state.major, state.minor, ext, gets, bugs);
#ifdef BACKEND_OPENGL_VERSION_GLES #ifdef BACKEND_OPENGL_VERSION_GLES
mShaderModel = ShaderModel::MOBILE; mShaderModel = ShaderModel::MOBILE;
@@ -197,6 +201,8 @@ OpenGLContext::OpenGLContext(OpenGLPlatform& platform,
assert_invariant(mFeatureLevel == FeatureLevel::FEATURE_LEVEL_0 || gets.max_draw_buffers >= 4); // minspec assert_invariant(mFeatureLevel == FeatureLevel::FEATURE_LEVEL_0 || gets.max_draw_buffers >= 4); // minspec
#endif #endif
setDefaultState();
#ifdef GL_EXT_texture_filter_anisotropic #ifdef GL_EXT_texture_filter_anisotropic
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2 #ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
if (mFeatureLevel >= FeatureLevel::FEATURE_LEVEL_1 && ext.EXT_texture_filter_anisotropic) { if (mFeatureLevel >= FeatureLevel::FEATURE_LEVEL_1 && ext.EXT_texture_filter_anisotropic) {
@@ -257,20 +263,120 @@ OpenGLContext::OpenGLContext(OpenGLPlatform& platform,
glDebugMessageCallback(cb, nullptr); glDebugMessageCallback(cb, nullptr);
} }
#endif #endif
mTimerQueryFactory = TimerQueryFactory::init(platform, *this);
} }
OpenGLContext::~OpenGLContext() noexcept = default; OpenGLContext::~OpenGLContext() noexcept {
// note: this is called from the main thread. Can't do any GL calls.
OpenGLState* OpenGLContext::createState() noexcept { delete mTimerQueryFactory;
auto* state = new(std::nothrow) OpenGLState(*this);
assert_invariant(state);
return state;
} }
void OpenGLContext::destroyState(OpenGLState* state) noexcept { void OpenGLContext::terminate() noexcept {
if (state) { // note: this is called from the backend thread
state->terminate(); #ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
delete state; if (!isES2()) {
for (auto& item: mSamplerMap) {
unbindSampler(item.second);
glDeleteSamplers(1, &item.second);
}
mSamplerMap.clear();
}
#endif
}
void OpenGLContext::destroyWithContext(
size_t index, std::function<void(OpenGLContext&)> const& closure) {
if (index == 0) {
// Note: we only need to delay the destruction of objects on the unprotected context
// (index 0) because the protected context is always immediately destroyed and all its
// active objects and bindings are then automatically destroyed.
// TODO: this is only guaranteed for EGLPlatform, but that's the only one we care about.
mDestroyWithNormalContext.push_back(closure);
}
}
void OpenGLContext::unbindEverything() noexcept {
// TODO: we're supposed to unbind everything here so that resources don't get
// stuck in this context (contextIndex) when destroyed in the other context.
// However, because EGLPlatform always immediately destroys the protected context (1),
// the bindings will automatically be severed when we switch back to the default context.
// Since bindings now only exist in one context, we don't have a ref-counting issue to
// worry about.
}
void OpenGLContext::synchronizeStateAndCache(size_t index) {
// if we're just switching back to context 0, run all the pending destructors
if (index == 0) {
auto list = std::move(mDestroyWithNormalContext);
for (auto&& fn: list) {
fn(*this);
}
}
// the default FBO could be invalid
mDefaultFbo[index].reset();
contextIndex = index;
resetState();
}
void OpenGLContext::setDefaultState() noexcept {
// We need to make sure our internal state matches the GL state when we start.
// (some of these calls may be unneeded as they might be the gl defaults)
GLenum const caps[] = {
GL_BLEND,
GL_CULL_FACE,
GL_SCISSOR_TEST,
GL_DEPTH_TEST,
GL_STENCIL_TEST,
GL_DITHER,
GL_SAMPLE_ALPHA_TO_COVERAGE,
GL_SAMPLE_COVERAGE,
GL_POLYGON_OFFSET_FILL,
};
UTILS_NOUNROLL
for (auto const capi : caps) {
size_t const capIndex = getIndexForCap(capi);
if (state.enables.caps[capIndex]) {
glEnable(capi);
} else {
glDisable(capi);
}
}
// Point sprite size and seamless cubemap filtering are disabled by default in desktop GL.
// In OpenGL ES, these flags do not exist because they are always on.
#ifdef BACKEND_OPENGL_VERSION_GL
glEnable(GL_PROGRAM_POINT_SIZE);
enable(GL_PROGRAM_POINT_SIZE);
#endif
#ifdef GL_ARB_seamless_cube_map
glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
enable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
#endif
#ifdef GL_FRAGMENT_SHADER_DERIVATIVE_HINT
glHint(GL_FRAGMENT_SHADER_DERIVATIVE_HINT, GL_NICEST);
#endif
#if !defined(__EMSCRIPTEN__)
if (ext.EXT_clip_control) {
#if defined(BACKEND_OPENGL_VERSION_GL)
glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE);
#elif defined(GL_EXT_clip_control)
glClipControlEXT(GL_LOWER_LEFT_EXT, GL_ZERO_TO_ONE_EXT);
#endif
}
#endif
if (ext.EXT_clip_cull_distance
&& mDriverConfig.stereoscopicType == StereoscopicType::INSTANCED) {
glEnable(GL_CLIP_DISTANCE0);
glEnable(GL_CLIP_DISTANCE1);
} }
} }
@@ -675,8 +781,6 @@ void OpenGLContext::initExtensionsGLES(Extensions* ext, GLint major, GLint minor
ext->OES_EGL_image_external_essl3 = exts.has("GL_OES_EGL_image_external_essl3"sv); ext->OES_EGL_image_external_essl3 = exts.has("GL_OES_EGL_image_external_essl3"sv);
ext->OES_rgb8_rgba8 = exts.has("GL_OES_rgb8_rgba8"sv); ext->OES_rgb8_rgba8 = exts.has("GL_OES_rgb8_rgba8"sv);
ext->OES_standard_derivatives = exts.has("GL_OES_standard_derivatives"sv); ext->OES_standard_derivatives = exts.has("GL_OES_standard_derivatives"sv);
ext->OES_texture_float_linear = exts.has("GL_OES_texture_float_linear"sv);
ext->OES_texture_half_float_linear = exts.has("GL_OES_texture_half_float_linear"sv);
ext->OES_texture_npot = exts.has("GL_OES_texture_npot"sv); ext->OES_texture_npot = exts.has("GL_OES_texture_npot"sv);
ext->OES_vertex_array_object = exts.has("GL_OES_vertex_array_object"sv); ext->OES_vertex_array_object = exts.has("GL_OES_vertex_array_object"sv);
ext->OVR_multiview2 = exts.has("GL_OVR_multiview2"sv); ext->OVR_multiview2 = exts.has("GL_OVR_multiview2"sv);
@@ -684,15 +788,14 @@ void OpenGLContext::initExtensionsGLES(Extensions* ext, GLint major, GLint minor
ext->WEBGL_compressed_texture_s3tc = exts.has("WEBGL_compressed_texture_s3tc"sv); ext->WEBGL_compressed_texture_s3tc = exts.has("WEBGL_compressed_texture_s3tc"sv);
ext->WEBGL_compressed_texture_s3tc_srgb = exts.has("WEBGL_compressed_texture_s3tc_srgb"sv); ext->WEBGL_compressed_texture_s3tc_srgb = exts.has("WEBGL_compressed_texture_s3tc_srgb"sv);
// ES 3.2 implies EXT_color_buffer_float (but not necessarily filterable) // ES 3.2 implies EXT_color_buffer_float
if (major > 3 || (major == 3 && minor >= 2)) { if (major > 3 || (major == 3 && minor >= 2)) {
ext->EXT_color_buffer_float = true; ext->EXT_color_buffer_float = true;
} }
// ES 3.x implies EXT_discard_framebuffer, OES_vertex_array_object and OES_texture_half_float_linear // ES 3.x implies EXT_discard_framebuffer and OES_vertex_array_object
if (major >= 3) { if (major >= 3) {
ext->EXT_discard_framebuffer = true; ext->EXT_discard_framebuffer = true;
ext->OES_vertex_array_object = true; ext->OES_vertex_array_object = true;
ext->OES_texture_half_float_linear = true;
} }
} }
@@ -743,8 +846,6 @@ void OpenGLContext::initExtensionsGL(Extensions* ext, GLint major, GLint minor)
ext->OES_EGL_image_external_essl3 = false; ext->OES_EGL_image_external_essl3 = false;
ext->OES_rgb8_rgba8 = true; ext->OES_rgb8_rgba8 = true;
ext->OES_standard_derivatives = true; ext->OES_standard_derivatives = true;
ext->OES_texture_float_linear = true;
ext->OES_texture_half_float_linear = true;
ext->OES_texture_npot = true; ext->OES_texture_npot = true;
ext->OES_vertex_array_object = true; ext->OES_vertex_array_object = true;
ext->OVR_multiview2 = exts.has("GL_OVR_multiview2"sv); ext->OVR_multiview2 = exts.has("GL_OVR_multiview2"sv);
@@ -769,4 +870,413 @@ void OpenGLContext::initExtensionsGL(Extensions* ext, GLint major, GLint minor)
#endif // BACKEND_OPENGL_VERSION_GL #endif // BACKEND_OPENGL_VERSION_GL
} // namespace filament::backend
GLuint OpenGLContext::bindFramebuffer(GLenum target, GLuint buffer) noexcept {
if (UTILS_UNLIKELY(buffer == 0)) {
// we're binding the default frame buffer, resolve its actual name
auto& defaultFboForThisContext = mDefaultFbo[contextIndex];
if (UTILS_UNLIKELY(!defaultFboForThisContext.has_value())) {
defaultFboForThisContext = GLuint(mPlatform.getDefaultFramebufferObject());
}
// by construction, defaultFboForThisContext has a value. value_or() avoids a throwing call.
buffer = defaultFboForThisContext.value_or(0);
}
bindFramebufferResolved(target, buffer);
return buffer;
}
void OpenGLContext::unbindFramebuffer(GLenum target) noexcept {
bindFramebufferResolved(target, 0);
}
void OpenGLContext::bindFramebufferResolved(GLenum target, GLuint buffer) noexcept {
switch (target) {
case GL_FRAMEBUFFER:
if (state.draw_fbo != buffer || state.read_fbo != buffer) {
state.draw_fbo = state.read_fbo = buffer;
glBindFramebuffer(target, buffer);
}
break;
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
case GL_DRAW_FRAMEBUFFER:
if (state.draw_fbo != buffer) {
state.draw_fbo = buffer;
glBindFramebuffer(target, buffer);
}
break;
case GL_READ_FRAMEBUFFER:
if (state.read_fbo != buffer) {
state.read_fbo = buffer;
glBindFramebuffer(target, buffer);
}
break;
#endif
default:
break;
}
}
void OpenGLContext::bindBuffer(GLenum target, GLuint buffer) noexcept {
if (target == GL_ELEMENT_ARRAY_BUFFER) {
constexpr size_t targetIndex = getIndexForBufferTarget(GL_ELEMENT_ARRAY_BUFFER);
// GL_ELEMENT_ARRAY_BUFFER is a special case, where the currently bound VAO remembers
// the index buffer, unless there are no VAO bound (see: bindVertexArray)
assert_invariant(state.vao.p);
if (state.buffers.genericBinding[targetIndex] != buffer
|| ((state.vao.p != &mDefaultVAO) && (state.vao.p->elementArray != buffer))) {
state.buffers.genericBinding[targetIndex] = buffer;
if (state.vao.p != &mDefaultVAO) {
state.vao.p->elementArray = buffer;
}
glBindBuffer(target, buffer);
}
} else {
size_t const targetIndex = getIndexForBufferTarget(target);
update_state(state.buffers.genericBinding[targetIndex], buffer, [&]() {
glBindBuffer(target, buffer);
});
}
}
void OpenGLContext::pixelStore(GLenum pname, GLint param) noexcept {
GLint* pcur;
// Note: GL_UNPACK_SKIP_PIXELS, GL_UNPACK_SKIP_ROWS and
// GL_PACK_SKIP_PIXELS, GL_PACK_SKIP_ROWS
// are actually provided as conveniences to the programmer; they provide no functionality
// that cannot be duplicated at the call site (e.g. glTexImage2D or glReadPixels)
switch (pname) {
case GL_PACK_ALIGNMENT:
pcur = &state.pack.alignment;
break;
case GL_UNPACK_ALIGNMENT:
pcur = &state.unpack.alignment;
break;
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
case GL_UNPACK_ROW_LENGTH:
assert_invariant(state.major > 2);
pcur = &state.unpack.row_length;
break;
#endif
default:
goto default_case;
}
if (UTILS_UNLIKELY(*pcur != param)) {
*pcur = param;
default_case:
glPixelStorei(pname, param);
}
}
void OpenGLContext::unbindTexture(
UTILS_UNUSED_IN_RELEASE GLenum target, GLuint texture_id) noexcept {
// unbind this texture from all the units it might be bound to
// no need unbind the texture from FBOs because we're not tracking that state (and there is
// no need to).
// Never attempt to unbind texture 0. This could happen with external textures w/ streaming if
// never populated.
if (texture_id) {
UTILS_NOUNROLL
for (GLuint unit = 0; unit < MAX_TEXTURE_UNIT_COUNT; unit++) {
if (state.textures.units[unit].id == texture_id) {
// if this texture is bound, it should be at the same target
assert_invariant(state.textures.units[unit].target == target);
unbindTextureUnit(unit);
}
}
}
}
void OpenGLContext::unbindTextureUnit(GLuint unit) noexcept {
update_state(state.textures.units[unit].id, 0u, [&]() {
activeTexture(unit);
glBindTexture(state.textures.units[unit].target, 0u);
});
}
void OpenGLContext::unbindSampler(GLuint sampler) noexcept {
// unbind this sampler from all the units it might be bound to
UTILS_NOUNROLL // clang generates >800B of code!!!
for (GLuint unit = 0; unit < MAX_TEXTURE_UNIT_COUNT; unit++) {
if (state.textures.units[unit].sampler == sampler) {
bindSampler(unit, 0);
}
}
}
void OpenGLContext::deleteBuffer(GLuint buffer, GLenum target) noexcept {
glDeleteBuffers(1, &buffer);
// bindings of bound buffers are reset to 0
size_t const targetIndex = getIndexForBufferTarget(target);
auto& genericBinding = state.buffers.genericBinding[targetIndex];
if (genericBinding == buffer) {
genericBinding = 0;
}
if (UTILS_UNLIKELY(bugs.rebind_buffer_after_deletion)) {
if (genericBinding) {
glBindBuffer(target, genericBinding);
}
}
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
assert_invariant(mFeatureLevel >= FeatureLevel::FEATURE_LEVEL_1 ||
(target != GL_UNIFORM_BUFFER && target != GL_TRANSFORM_FEEDBACK_BUFFER));
if (target == GL_UNIFORM_BUFFER || target == GL_TRANSFORM_FEEDBACK_BUFFER) {
auto& indexedBinding = state.buffers.targets[targetIndex];
UTILS_NOUNROLL
for (auto& entry: indexedBinding.buffers) {
if (entry.name == buffer) {
entry.name = 0;
entry.offset = 0;
entry.size = 0;
}
}
}
#endif
}
void OpenGLContext::deleteVertexArray(GLuint vao) noexcept {
if (UTILS_LIKELY(vao)) {
procs.deleteVertexArrays(1, &vao);
// if the destroyed VAO is bound, clear the binding.
if (state.vao.p->vao[contextIndex] == vao) {
bindVertexArray(nullptr);
}
}
}
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
GLuint OpenGLContext::getSamplerSlow(SamplerParams params) const noexcept {
assert_invariant(mSamplerMap.find(params) == mSamplerMap.end());
using namespace GLUtils;
GLuint s;
glGenSamplers(1, &s);
glSamplerParameteri(s, GL_TEXTURE_MIN_FILTER, (GLint)getTextureFilter(params.filterMin));
glSamplerParameteri(s, GL_TEXTURE_MAG_FILTER, (GLint)getTextureFilter(params.filterMag));
glSamplerParameteri(s, GL_TEXTURE_WRAP_S, (GLint)getWrapMode(params.wrapS));
glSamplerParameteri(s, GL_TEXTURE_WRAP_T, (GLint)getWrapMode(params.wrapT));
glSamplerParameteri(s, GL_TEXTURE_WRAP_R, (GLint)getWrapMode(params.wrapR));
glSamplerParameteri(s, GL_TEXTURE_COMPARE_MODE, (GLint)getTextureCompareMode(params.compareMode));
glSamplerParameteri(s, GL_TEXTURE_COMPARE_FUNC, (GLint)getTextureCompareFunc(params.compareFunc));
#if defined(GL_EXT_texture_filter_anisotropic)
if (ext.EXT_texture_filter_anisotropic &&
!bugs.texture_filter_anisotropic_broken_on_sampler) {
GLfloat const anisotropy = float(1u << params.anisotropyLog2);
glSamplerParameterf(s, GL_TEXTURE_MAX_ANISOTROPY_EXT,
std::min(gets.max_anisotropy, anisotropy));
}
#endif
CHECK_GL_ERROR()
mSamplerMap[params] = s;
return s;
}
#endif
void OpenGLContext::resetState() noexcept {
// Force GL state to match the Filament state
// increase the state version so other parts of the state know to reset
state.age++;
if (state.major > 2) {
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, state.draw_fbo);
glBindFramebuffer(GL_READ_FRAMEBUFFER, state.read_fbo);
#endif
} else {
assert_invariant(state.read_fbo == state.draw_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, state.draw_fbo);
state.read_fbo = state.draw_fbo;
}
// state.program
glUseProgram(state.program.use);
// state.vao
state.vao.p = nullptr;
bindVertexArray(nullptr);
// state.raster
glFrontFace(state.raster.frontFace);
glCullFace(state.raster.cullFace);
glBlendEquationSeparate(state.raster.blendEquationRGB, state.raster.blendEquationA);
glBlendFuncSeparate(
state.raster.blendFunctionSrcRGB,
state.raster.blendFunctionDstRGB,
state.raster.blendFunctionSrcA,
state.raster.blendFunctionDstA
);
glColorMask(
state.raster.colorMask,
state.raster.colorMask,
state.raster.colorMask,
state.raster.colorMask
);
glDepthMask(state.raster.depthMask);
glDepthFunc(state.raster.depthFunc);
// state.stencil
glStencilFuncSeparate(
GL_FRONT,
state.stencil.front.func.func,
state.stencil.front.func.ref,
state.stencil.front.func.mask
);
glStencilFuncSeparate(
GL_BACK,
state.stencil.back.func.func,
state.stencil.back.func.ref,
state.stencil.back.func.mask
);
glStencilOpSeparate(
GL_FRONT,
state.stencil.front.op.sfail,
state.stencil.front.op.dpfail,
state.stencil.front.op.dppass
);
glStencilOpSeparate(
GL_BACK,
state.stencil.back.op.sfail,
state.stencil.back.op.dpfail,
state.stencil.back.op.dppass
);
glStencilMaskSeparate(GL_FRONT, state.stencil.front.stencilMask);
glStencilMaskSeparate(GL_BACK, state.stencil.back.stencilMask);
// state.polygonOffset
glPolygonOffset(state.polygonOffset.factor, state.polygonOffset.units);
// state.enables
setDefaultState();
// state.buffers
// Reset state.buffers to its default state to avoid the complexity and error-prone
// nature of resetting the GL state to its existing state
state.buffers = {};
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
if (state.major > 2) {
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
for (auto const target: {
GL_UNIFORM_BUFFER,
GL_TRANSFORM_FEEDBACK_BUFFER,
#if defined(BACKEND_OPENGL_LEVEL_GLES31)
GL_SHADER_STORAGE_BUFFER,
#endif
GL_PIXEL_PACK_BUFFER,
GL_PIXEL_UNPACK_BUFFER,
}) {
glBindBuffer(target, 0);
}
for (size_t bufferIndex = 0; bufferIndex < MAX_BUFFER_BINDINGS; ++bufferIndex) {
if (bufferIndex < (size_t)gets.max_uniform_buffer_bindings) {
glBindBufferBase(GL_UNIFORM_BUFFER, bufferIndex, 0);
}
if (bufferIndex < (size_t)gets.max_transform_feedback_separate_attribs) {
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, bufferIndex, 0);
}
}
#endif
}
// state.textures
// Reset state.textures to its default state to avoid the complexity and error-prone
// nature of resetting the GL state to its existing state
state.textures = {};
const std::pair<GLuint, bool> textureTargets[] = {
{ GL_TEXTURE_2D, true },
{ GL_TEXTURE_2D_ARRAY, true },
{ GL_TEXTURE_CUBE_MAP, true },
{ GL_TEXTURE_3D, true },
#if defined(BACKEND_OPENGL_LEVEL_GLES31)
{ GL_TEXTURE_2D_MULTISAMPLE, true },
#endif
#if !defined(__EMSCRIPTEN__)
#if defined(GL_OES_EGL_image_external)
{ GL_TEXTURE_EXTERNAL_OES, ext.OES_EGL_image_external_essl3 },
#endif
#if defined(BACKEND_OPENGL_VERSION_GL) || defined(GL_EXT_texture_cube_map_array)
{ GL_TEXTURE_CUBE_MAP_ARRAY, ext.EXT_texture_cube_map_array },
#endif
#endif
};
for (GLint unit = 0; unit < gets.max_combined_texture_image_units; ++unit) {
glActiveTexture(GL_TEXTURE0 + unit);
if (state.major > 2) {
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
glBindSampler(unit, 0);
#endif
}
for (auto [target, available] : textureTargets) {
if (available) {
glBindTexture(target, 0);
}
}
}
glActiveTexture(GL_TEXTURE0 + state.textures.active);
// state.unpack
glPixelStorei(GL_UNPACK_ALIGNMENT, state.unpack.alignment);
if (state.major > 2) {
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
glPixelStorei(GL_UNPACK_ROW_LENGTH, state.unpack.row_length);
#endif
}
// state.pack
glPixelStorei(GL_PACK_ALIGNMENT, state.pack.alignment);
if (state.major > 2) {
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
glPixelStorei(GL_PACK_ROW_LENGTH, 0); // we rely on GL_PACK_ROW_LENGTH being zero
#endif
}
// state.window
glScissor(
state.window.scissor.x,
state.window.scissor.y,
state.window.scissor.z,
state.window.scissor.w
);
glViewport(
state.window.viewport.x,
state.window.viewport.y,
state.window.viewport.z,
state.window.viewport.w
);
glDepthRangef(state.window.depthRange.x, state.window.depthRange.y);
}
void OpenGLContext::createTimerQuery(GLTimerQuery* query) {
mTimerQueryFactory->createTimerQuery(query);
}
void OpenGLContext::destroyTimerQuery(GLTimerQuery* query) {
mTimerQueryFactory->destroyTimerQuery(query);
}
void OpenGLContext::beginTimeElapsedQuery(GLTimerQuery* query) {
mTimerQueryFactory->beginTimeElapsedQuery(query);
}
void OpenGLContext::endTimeElapsedQuery(OpenGLDriver& driver, GLTimerQuery* query) {
mTimerQueryFactory->endTimeElapsedQuery(driver, query);
}
} // namesapce filament
+545 -25
View File
@@ -17,6 +17,9 @@
#ifndef TNT_FILAMENT_BACKEND_OPENGLCONTEXT_H #ifndef TNT_FILAMENT_BACKEND_OPENGLCONTEXT_H
#define TNT_FILAMENT_BACKEND_OPENGLCONTEXT_H #define TNT_FILAMENT_BACKEND_OPENGLCONTEXT_H
#include "OpenGLTimerQuery.h"
#include <backend/platforms/OpenGLPlatform.h> #include <backend/platforms/OpenGLPlatform.h>
#include <backend/DriverEnums.h> #include <backend/DriverEnums.h>
@@ -31,8 +34,13 @@
#include <math/vec2.h> #include <math/vec2.h>
#include <math/vec4.h> #include <math/vec4.h>
#include <tsl/robin_map.h>
#include <array> #include <array>
#include <functional>
#include <optional>
#include <tuple> #include <tuple>
#include <vector>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
@@ -40,9 +48,8 @@
namespace filament::backend { namespace filament::backend {
class OpenGLPlatform; class OpenGLPlatform;
class OpenGLState;
class OpenGLContext final { class OpenGLContext final : public TimerQueryFactoryInterface {
public: public:
static constexpr const size_t MAX_TEXTURE_UNIT_COUNT = MAX_SAMPLER_COUNT; static constexpr const size_t MAX_TEXTURE_UNIT_COUNT = MAX_SAMPLER_COUNT;
static constexpr const size_t DUMMY_TEXTURE_BINDING = 7; // highest binding guaranteed to work with ES2 static constexpr const size_t DUMMY_TEXTURE_BINDING = 7; // highest binding guaranteed to work with ES2
@@ -70,12 +77,12 @@ public:
// be updated (see OpenGLDriver::updateVertexArrayObject()) // be updated (see OpenGLDriver::updateVertexArrayObject())
uint8_t vertexBufferVersion = 0; // 1 uint8_t vertexBufferVersion = 0; // 1
// if this differs from OpenGLState::state.age, this VAO needs to // if this differs from OpenGLContext::state.age, this VAO needs to
// be updated (see OpenGLDriver::updateVertexArrayObject()) // be updated (see OpenGLDriver::updateVertexArrayObject())
uint8_t stateVersion = 0; // 1 uint8_t stateVersion = 0; // 1
// If this differs from OpenGLState::state.age, this VAO's name needs to be updated. // If this differs from OpenGLContext::state.age, this VAO's name needs to be updated.
// See OpenGLState::bindVertexArray() // See OpenGLContext::bindVertexArray()
uint8_t nameVersion = 0; // 1 uint8_t nameVersion = 0; // 1
// Size in bytes of indices in the index buffer (1 or 2) // Size in bytes of indices in the index buffer (1 or 2)
@@ -91,18 +98,24 @@ public:
explicit OpenGLContext(OpenGLPlatform& platform, explicit OpenGLContext(OpenGLPlatform& platform,
Platform::DriverConfig const& driverConfig) noexcept; Platform::DriverConfig const& driverConfig) noexcept;
~OpenGLContext() noexcept; ~OpenGLContext() noexcept final;
// State lifecycle - allocate/deallocate per-thread OpenGLState instances void terminate() noexcept;
OpenGLState* createState() noexcept;
void destroyState(OpenGLState* state) noexcept; // TimerQueryInterface ------------------------------------------------------------------------
// note: OpenGLContext being final ensures (clang) these are not called through the vtable
void createTimerQuery(GLTimerQuery* query) override;
void destroyTimerQuery(GLTimerQuery* query) override;
void beginTimeElapsedQuery(GLTimerQuery* query) override;
void endTimeElapsedQuery(OpenGLDriver& driver, GLTimerQuery* query) override;
// -------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------
template<int MAJOR, int MINOR> template<int MAJOR, int MINOR>
inline bool isAtLeastGL() const noexcept { inline bool isAtLeastGL() const noexcept {
#ifdef BACKEND_OPENGL_VERSION_GL #ifdef BACKEND_OPENGL_VERSION_GL
return major > MAJOR || (major == MAJOR && minor >= MINOR); return state.major > MAJOR || (state.major == MAJOR && state.minor >= MINOR);
#else #else
return false; return false;
#endif #endif
@@ -111,7 +124,7 @@ public:
template<int MAJOR, int MINOR> template<int MAJOR, int MINOR>
inline bool isAtLeastGLES() const noexcept { inline bool isAtLeastGLES() const noexcept {
#ifdef BACKEND_OPENGL_VERSION_GLES #ifdef BACKEND_OPENGL_VERSION_GLES
return major > MAJOR || (major == MAJOR && minor >= MINOR); return state.major > MAJOR || (state.major == MAJOR && state.minor >= MINOR);
#else #else
return false; return false;
#endif #endif
@@ -141,13 +154,60 @@ public:
#endif #endif
} }
constexpr inline size_t getIndexForCap(GLenum cap) noexcept;
constexpr static inline size_t getIndexForBufferTarget(GLenum target) noexcept;
ShaderModel getShaderModel() const noexcept { return mShaderModel; } ShaderModel getShaderModel() const noexcept { return mShaderModel; }
FeatureLevel getFeatureLevel() const noexcept { return mFeatureLevel; }
OpenGLPlatform& getPlatform() const noexcept { return mPlatform; } void resetState() noexcept;
Platform::DriverConfig const& getDriverConfig() const noexcept { return mDriverConfig; }
bool isGpuTimeSupported() const noexcept { return mGpuTimeSupported; } bool isGpuTimeSupported() const noexcept { return mGpuTimeSupported; }
inline void useProgram(GLuint program) noexcept;
void pixelStore(GLenum, GLint) noexcept;
inline void activeTexture(GLuint unit) noexcept;
inline void bindTexture(GLuint unit, GLuint target, GLuint texId, bool external) noexcept;
void unbindTexture(GLenum target, GLuint id) noexcept;
void unbindTextureUnit(GLuint unit) noexcept;
inline void bindVertexArray(RenderPrimitive const* p) noexcept;
inline void bindSampler(GLuint unit, GLuint sampler) noexcept;
void unbindSampler(GLuint sampler) noexcept;
void bindBuffer(GLenum target, GLuint buffer) noexcept;
inline void bindBufferRange(GLenum target, GLuint index, GLuint buffer,
GLintptr offset, GLsizeiptr size) noexcept;
GLuint bindFramebuffer(GLenum target, GLuint buffer) noexcept;
void unbindFramebuffer(GLenum target) noexcept;
inline void enableVertexAttribArray(RenderPrimitive const* rp, GLuint index) noexcept;
inline void disableVertexAttribArray(RenderPrimitive const* rp, GLuint index) noexcept;
inline void enable(GLenum cap) noexcept;
inline void disable(GLenum cap) noexcept;
inline void frontFace(GLenum mode) noexcept;
inline void cullFace(GLenum mode) noexcept;
inline void blendEquation(GLenum modeRGB, GLenum modeA) noexcept;
inline void blendFunction(GLenum srcRGB, GLenum srcA, GLenum dstRGB, GLenum dstA) noexcept;
inline void colorMask(GLboolean flag) noexcept;
inline void depthMask(GLboolean flag) noexcept;
inline void depthFunc(GLenum func) noexcept;
inline void stencilFuncSeparate(GLenum funcFront, GLint refFront, GLuint maskFront,
GLenum funcBack, GLint refBack, GLuint maskBack) noexcept;
inline void stencilOpSeparate(GLenum sfailFront, GLenum dpfailFront, GLenum dppassFront,
GLenum sfailBack, GLenum dpfailBack, GLenum dppassBack) noexcept;
inline void stencilMaskSeparate(GLuint maskFront, GLuint maskBack) noexcept;
inline void polygonOffset(GLfloat factor, GLfloat units) noexcept;
inline void setScissor(GLint left, GLint bottom, GLsizei width, GLsizei height) noexcept;
inline void viewport(GLint left, GLint bottom, GLsizei width, GLsizei height) noexcept;
inline void depthRange(GLclampf near, GLclampf far) noexcept;
void deleteBuffer(GLuint buffer, GLenum target) noexcept;
void deleteVertexArray(GLuint vao) noexcept;
void destroyWithContext(size_t index, std::function<void(OpenGLContext&)> const& closure);
// glGet*() values // glGet*() values
struct Gets { struct Gets {
@@ -208,8 +268,6 @@ public:
bool OES_packed_depth_stencil; bool OES_packed_depth_stencil;
bool OES_rgb8_rgba8; bool OES_rgb8_rgba8;
bool OES_standard_derivatives; bool OES_standard_derivatives;
bool OES_texture_float_linear;
bool OES_texture_half_float_linear;
bool OES_texture_npot; bool OES_texture_npot;
bool OES_vertex_array_object; bool OES_vertex_array_object;
bool OVR_multiview2; bool OVR_multiview2;
@@ -299,6 +357,143 @@ public:
} bugs = {}; } bugs = {};
// state getters -- as needed.
vec4gli const& getViewport() const { return state.window.viewport; }
// function to handle state changes we don't control
void updateTexImage(GLenum target, GLuint id) noexcept {
assert_invariant(target == GL_TEXTURE_EXTERNAL_OES);
// if another target is bound to this texture unit, unbind that texture
if (UTILS_UNLIKELY(state.textures.units[state.textures.active].target != target)) {
glBindTexture(state.textures.units[state.textures.active].target, 0);
state.textures.units[state.textures.active].target = GL_TEXTURE_EXTERNAL_OES;
}
// the texture is already bound to `target`, we just update our internal state
state.textures.units[state.textures.active].id = id;
}
void resetProgram() noexcept { state.program.use = 0; }
FeatureLevel getFeatureLevel() const noexcept { return mFeatureLevel; }
// This is the index of the context in use. Must be either 0 (Unprotected) or 1 (Protected).
// This is used to manage the OpenGL name of ContainerObjects within each context.
uint32_t contextIndex = 0;
// Try to keep the State structure sorted by data-access patterns
struct State {
State() noexcept = default;
// make sure we don't copy this state by accident
State(State const& rhs) = delete;
State(State&& rhs) noexcept = delete;
State& operator=(State const& rhs) = delete;
State& operator=(State&& rhs) noexcept = delete;
GLint major = 0;
GLint minor = 0;
char const* vendor = nullptr;
char const* renderer = nullptr;
char const* version = nullptr;
char const* shader = nullptr;
GLuint draw_fbo = 0;
GLuint read_fbo = 0;
struct {
GLuint use = 0;
} program;
struct {
RenderPrimitive* p = nullptr;
} vao;
struct {
GLenum frontFace = GL_CCW;
GLenum cullFace = GL_BACK;
GLenum blendEquationRGB = GL_FUNC_ADD;
GLenum blendEquationA = GL_FUNC_ADD;
GLenum blendFunctionSrcRGB = GL_ONE;
GLenum blendFunctionSrcA = GL_ONE;
GLenum blendFunctionDstRGB = GL_ZERO;
GLenum blendFunctionDstA = GL_ZERO;
GLboolean colorMask = GL_TRUE;
GLboolean depthMask = GL_TRUE;
GLenum depthFunc = GL_LESS;
} raster;
struct {
struct StencilFunc {
GLenum func = GL_ALWAYS;
GLint ref = 0;
GLuint mask = ~GLuint(0);
bool operator != (StencilFunc const& rhs) const noexcept {
return func != rhs.func || ref != rhs.ref || mask != rhs.mask;
}
};
struct StencilOp {
GLenum sfail = GL_KEEP;
GLenum dpfail = GL_KEEP;
GLenum dppass = GL_KEEP;
bool operator != (StencilOp const& rhs) const noexcept {
return sfail != rhs.sfail || dpfail != rhs.dpfail || dppass != rhs.dppass;
}
};
struct {
StencilFunc func;
StencilOp op;
GLuint stencilMask = ~GLuint(0);
} front, back;
} stencil;
struct PolygonOffset {
GLfloat factor = 0;
GLfloat units = 0;
bool operator != (PolygonOffset const& rhs) const noexcept {
return factor != rhs.factor || units != rhs.units;
}
} polygonOffset;
struct {
utils::bitset32 caps;
} enables;
struct {
struct {
struct {
GLuint name = 0;
GLintptr offset = 0;
GLsizeiptr size = 0;
} buffers[MAX_BUFFER_BINDINGS];
} targets[3]; // there are only 3 indexed buffer targets
GLuint genericBinding[7] = {};
} buffers;
struct {
GLuint active = 0; // zero-based
struct {
GLuint sampler = 0;
GLuint target = 0;
GLuint id = 0;
} units[MAX_TEXTURE_UNIT_COUNT];
} textures;
struct {
GLint row_length = 0;
GLint alignment = 4;
} unpack;
struct {
GLint alignment = 4;
} pack;
struct {
vec4gli scissor { 0 };
vec4gli viewport { 0 };
vec2glf depthRange { 0.0f, 1.0f };
} window;
uint8_t age = 0;
} state;
struct Procs { struct Procs {
void (* bindVertexArray)(GLuint array); void (* bindVertexArray)(GLuint array);
void (* deleteVertexArrays)(GLsizei n, const GLuint* arrays); void (* deleteVertexArrays)(GLsizei n, const GLuint* arrays);
@@ -316,25 +511,43 @@ public:
void (* maxShaderCompilerThreadsKHR)(GLuint count); void (* maxShaderCompilerThreadsKHR)(GLuint count);
} procs{}; } procs{};
// GL version info — immutable after construction void unbindEverything() noexcept;
GLint major = 0; void synchronizeStateAndCache(size_t index);
GLint minor = 0;
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
GLuint getSamplerSlow(SamplerParams sp) const noexcept;
inline GLuint getSampler(SamplerParams sp) const noexcept {
assert_invariant(!sp.padding0);
assert_invariant(!sp.padding1);
assert_invariant(!sp.padding2);
auto& samplerMap = mSamplerMap;
auto pos = samplerMap.find(sp);
if (UTILS_UNLIKELY(pos == samplerMap.end())) {
return getSamplerSlow(sp);
}
return pos->second;
}
#endif
// GL info strings — immutable after construction
char const* vendor = nullptr;
char const* renderer = nullptr;
char const* version = nullptr;
char const* shader = nullptr;
private: private:
OpenGLPlatform& mPlatform; OpenGLPlatform& mPlatform;
ShaderModel mShaderModel = ShaderModel::MOBILE; ShaderModel mShaderModel = ShaderModel::MOBILE;
FeatureLevel mFeatureLevel = FeatureLevel::FEATURE_LEVEL_1; FeatureLevel mFeatureLevel = FeatureLevel::FEATURE_LEVEL_1;
TimerQueryFactoryInterface* mTimerQueryFactory = nullptr;
std::vector<std::function<void(OpenGLContext&)>> mDestroyWithNormalContext;
RenderPrimitive mDefaultVAO;
std::optional<GLuint> mDefaultFbo[2]; // 0:Unprotected, 1:Protected
mutable tsl::robin_map<SamplerParams, GLuint,
SamplerParams::Hasher, SamplerParams::EqualTo> mSamplerMap;
Platform::DriverConfig const mDriverConfig; Platform::DriverConfig const mDriverConfig;
bool mGpuTimeSupported = false; bool mGpuTimeSupported = false;
void bindFramebufferResolved(GLenum target, GLuint buffer) noexcept;
const std::array<std::tuple<bool const&, char const*, char const*>, sizeof(bugs)> mBugDatabase{{ const std::array<std::tuple<bool const&, char const*, char const*>, sizeof(bugs)> mBugDatabase{{
{ bugs.disable_glFlush, { bugs.disable_glFlush,
"disable_glFlush", "disable_glFlush",
@@ -426,8 +639,315 @@ private:
Extensions const& exts, Extensions const& exts,
Gets const& gets, Gets const& gets,
Bugs const& bugs) noexcept; Bugs const& bugs) noexcept;
template <typename T, typename F>
static inline void update_state(T& state, T const& expected, F functor, bool force = false) noexcept {
if (UTILS_UNLIKELY(force || state != expected)) {
state = expected;
functor();
}
}
void setDefaultState() noexcept;
}; };
} // namespace filament::backend // ------------------------------------------------------------------------------------------------
constexpr size_t OpenGLContext::getIndexForCap(GLenum cap) noexcept { //NOLINT
size_t index = 0;
switch (cap) {
case GL_BLEND: index = 0; break;
case GL_CULL_FACE: index = 1; break;
case GL_SCISSOR_TEST: index = 2; break;
case GL_DEPTH_TEST: index = 3; break;
case GL_STENCIL_TEST: index = 4; break;
case GL_DITHER: index = 5; break;
case GL_SAMPLE_ALPHA_TO_COVERAGE: index = 6; break;
case GL_SAMPLE_COVERAGE: index = 7; break;
case GL_POLYGON_OFFSET_FILL: index = 8; break;
#ifdef GL_ARB_seamless_cube_map
case GL_TEXTURE_CUBE_MAP_SEAMLESS: index = 9; break;
#endif
#ifdef BACKEND_OPENGL_VERSION_GL
case GL_PROGRAM_POINT_SIZE: index = 10; break;
#endif
case GL_DEPTH_CLAMP: index = 11; break;
default: break;
}
assert_invariant(index < state.enables.caps.size());
return index;
}
constexpr size_t OpenGLContext::getIndexForBufferTarget(GLenum target) noexcept {
size_t index = 0;
switch (target) {
// The indexed buffers MUST be first in this list (those usable with bindBufferRange)
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
case GL_UNIFORM_BUFFER: index = 0; break;
case GL_TRANSFORM_FEEDBACK_BUFFER: index = 1; break;
#if defined(BACKEND_OPENGL_LEVEL_GLES31)
case GL_SHADER_STORAGE_BUFFER: index = 2; break;
#endif
#endif
case GL_ARRAY_BUFFER: index = 3; break;
case GL_ELEMENT_ARRAY_BUFFER: index = 4; break;
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
case GL_PIXEL_PACK_BUFFER: index = 5; break;
case GL_PIXEL_UNPACK_BUFFER: index = 6; break;
#endif
default: break;
}
assert_invariant(index < sizeof(state.buffers.genericBinding)/sizeof(state.buffers.genericBinding[0])); // NOLINT(misc-redundant-expression)
return index;
}
// ------------------------------------------------------------------------------------------------
void OpenGLContext::activeTexture(GLuint unit) noexcept {
assert_invariant(unit < MAX_TEXTURE_UNIT_COUNT);
update_state(state.textures.active, unit, [&]() {
glActiveTexture(GL_TEXTURE0 + unit);
});
}
void OpenGLContext::bindSampler(GLuint unit, GLuint sampler) noexcept {
assert_invariant(unit < MAX_TEXTURE_UNIT_COUNT);
assert_invariant(mFeatureLevel >= FeatureLevel::FEATURE_LEVEL_1);
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
update_state(state.textures.units[unit].sampler, sampler, [&]() {
glBindSampler(unit, sampler);
});
#endif
}
void OpenGLContext::setScissor(GLint left, GLint bottom, GLsizei width, GLsizei height) noexcept {
vec4gli const scissor(left, bottom, width, height);
update_state(state.window.scissor, scissor, [&]() {
glScissor(left, bottom, width, height);
});
}
void OpenGLContext::viewport(GLint left, GLint bottom, GLsizei width, GLsizei height) noexcept {
vec4gli const viewport(left, bottom, width, height);
update_state(state.window.viewport, viewport, [&]() {
glViewport(left, bottom, width, height);
});
}
void OpenGLContext::depthRange(GLclampf near, GLclampf far) noexcept {
vec2glf const depthRange(near, far);
update_state(state.window.depthRange, depthRange, [&]() {
glDepthRangef(near, far);
});
}
void OpenGLContext::bindVertexArray(RenderPrimitive const* p) noexcept {
RenderPrimitive* vao = p ? const_cast<RenderPrimitive *>(p) : &mDefaultVAO;
update_state(state.vao.p, vao, [&]() {
// See if we need to create a name for this VAO on the fly, this would happen if:
// - we're not the default VAO, because its name is always 0
// - our name is 0, this could happen if this VAO was created in the "other" context
// - the nameVersion is out of date *and* we're on the protected context, in this case:
// - the name must be stale from a previous use of this context because we always
// destroy the protected context when we're done with it.
bool const recreateVaoName = vao != &mDefaultVAO &&
((vao->vao[contextIndex] == 0) ||
(vao->nameVersion != state.age && contextIndex == 1));
if (UTILS_UNLIKELY(recreateVaoName)) {
vao->nameVersion = state.age;
procs.genVertexArrays(1, &vao->vao[contextIndex]);
}
procs.bindVertexArray(vao->vao[contextIndex]);
// update GL_ELEMENT_ARRAY_BUFFER, which is updated by glBindVertexArray
size_t const targetIndex = getIndexForBufferTarget(GL_ELEMENT_ARRAY_BUFFER);
state.buffers.genericBinding[targetIndex] = vao->elementArray;
if (UTILS_UNLIKELY(bugs.vao_doesnt_store_element_array_buffer_binding || recreateVaoName)) {
// This shouldn't be needed, but it looks like some drivers don't do the implicit
// glBindBuffer().
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vao->elementArray);
}
});
}
void OpenGLContext::bindBufferRange(GLenum target, GLuint index, GLuint buffer,
GLintptr offset, GLsizeiptr size) noexcept {
assert_invariant(mFeatureLevel >= FeatureLevel::FEATURE_LEVEL_1);
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
# ifdef BACKEND_OPENGL_LEVEL_GLES31
assert_invariant(false
|| target == GL_UNIFORM_BUFFER
|| target == GL_TRANSFORM_FEEDBACK_BUFFER
|| target == GL_SHADER_STORAGE_BUFFER);
# else
assert_invariant(false
|| target == GL_UNIFORM_BUFFER
|| target == GL_TRANSFORM_FEEDBACK_BUFFER);
# endif
size_t const targetIndex = getIndexForBufferTarget(target);
// this ALSO sets the generic binding
assert_invariant(targetIndex < sizeof(state.buffers.targets) / sizeof(*state.buffers.targets));
if ( state.buffers.targets[targetIndex].buffers[index].name != buffer
|| state.buffers.targets[targetIndex].buffers[index].offset != offset
|| state.buffers.targets[targetIndex].buffers[index].size != size) {
state.buffers.targets[targetIndex].buffers[index].name = buffer;
state.buffers.targets[targetIndex].buffers[index].offset = offset;
state.buffers.targets[targetIndex].buffers[index].size = size;
state.buffers.genericBinding[targetIndex] = buffer;
glBindBufferRange(target, index, buffer, offset, size);
}
#endif
}
void OpenGLContext::bindTexture(GLuint unit, GLuint target, GLuint texId, bool external) noexcept {
// another texture is bound to the same unit with a different target,
// unbind the texture from the current target
update_state(state.textures.units[unit].target, target, [&]() {
activeTexture(unit);
glBindTexture(state.textures.units[unit].target, 0);
});
update_state(state.textures.units[unit].id, texId, [&]() {
activeTexture(unit);
glBindTexture(target, texId);
}, external);
}
void OpenGLContext::useProgram(GLuint program) noexcept {
update_state(state.program.use, program, [&]() {
glUseProgram(program);
});
}
void OpenGLContext::enableVertexAttribArray(RenderPrimitive const* rp, GLuint index) noexcept {
assert_invariant(rp);
assert_invariant(index < rp->vertexAttribArray.size());
bool const force = rp->stateVersion != state.age;
if (UTILS_UNLIKELY(force || !rp->vertexAttribArray[index])) {
rp->vertexAttribArray.set(index);
glEnableVertexAttribArray(index);
}
}
void OpenGLContext::disableVertexAttribArray(RenderPrimitive const* rp, GLuint index) noexcept {
assert_invariant(rp);
assert_invariant(index < rp->vertexAttribArray.size());
bool const force = rp->stateVersion != state.age;
if (UTILS_UNLIKELY(force || rp->vertexAttribArray[index])) {
rp->vertexAttribArray.unset(index);
glDisableVertexAttribArray(index);
}
}
void OpenGLContext::enable(GLenum cap) noexcept {
size_t const index = getIndexForCap(cap);
if (UTILS_UNLIKELY(!state.enables.caps[index])) {
state.enables.caps.set(index);
glEnable(cap);
}
}
void OpenGLContext::disable(GLenum cap) noexcept {
size_t const index = getIndexForCap(cap);
if (UTILS_UNLIKELY(state.enables.caps[index])) {
state.enables.caps.unset(index);
glDisable(cap);
}
}
void OpenGLContext::frontFace(GLenum mode) noexcept {
update_state(state.raster.frontFace, mode, [&]() {
glFrontFace(mode);
});
}
void OpenGLContext::cullFace(GLenum mode) noexcept {
update_state(state.raster.cullFace, mode, [&]() {
glCullFace(mode);
});
}
void OpenGLContext::blendEquation(GLenum modeRGB, GLenum modeA) noexcept {
if (UTILS_UNLIKELY(
state.raster.blendEquationRGB != modeRGB || state.raster.blendEquationA != modeA)) {
state.raster.blendEquationRGB = modeRGB;
state.raster.blendEquationA = modeA;
glBlendEquationSeparate(modeRGB, modeA);
}
}
void OpenGLContext::blendFunction(GLenum srcRGB, GLenum srcA, GLenum dstRGB, GLenum dstA) noexcept {
if (UTILS_UNLIKELY(
state.raster.blendFunctionSrcRGB != srcRGB ||
state.raster.blendFunctionSrcA != srcA ||
state.raster.blendFunctionDstRGB != dstRGB ||
state.raster.blendFunctionDstA != dstA)) {
state.raster.blendFunctionSrcRGB = srcRGB;
state.raster.blendFunctionSrcA = srcA;
state.raster.blendFunctionDstRGB = dstRGB;
state.raster.blendFunctionDstA = dstA;
glBlendFuncSeparate(srcRGB, dstRGB, srcA, dstA);
}
}
void OpenGLContext::colorMask(GLboolean flag) noexcept {
update_state(state.raster.colorMask, flag, [&]() {
glColorMask(flag, flag, flag, flag);
});
}
void OpenGLContext::depthMask(GLboolean flag) noexcept {
update_state(state.raster.depthMask, flag, [&]() {
glDepthMask(flag);
});
}
void OpenGLContext::depthFunc(GLenum func) noexcept {
update_state(state.raster.depthFunc, func, [&]() {
glDepthFunc(func);
});
}
void OpenGLContext::stencilFuncSeparate(GLenum funcFront, GLint refFront, GLuint maskFront,
GLenum funcBack, GLint refBack, GLuint maskBack) noexcept {
update_state(state.stencil.front.func, {funcFront, refFront, maskFront}, [&]() {
glStencilFuncSeparate(GL_FRONT, funcFront, refFront, maskFront);
});
update_state(state.stencil.back.func, {funcBack, refBack, maskBack}, [&]() {
glStencilFuncSeparate(GL_BACK, funcBack, refBack, maskBack);
});
}
void OpenGLContext::stencilOpSeparate(GLenum sfailFront, GLenum dpfailFront, GLenum dppassFront,
GLenum sfailBack, GLenum dpfailBack, GLenum dppassBack) noexcept {
update_state(state.stencil.front.op, {sfailFront, dpfailFront, dppassFront}, [&]() {
glStencilOpSeparate(GL_FRONT, sfailFront, dpfailFront, dppassFront);
});
update_state(state.stencil.back.op, {sfailBack, dpfailBack, dppassBack}, [&]() {
glStencilOpSeparate(GL_BACK, sfailBack, dpfailBack, dppassBack);
});
}
void OpenGLContext::stencilMaskSeparate(GLuint maskFront, GLuint maskBack) noexcept {
update_state(state.stencil.front.stencilMask, maskFront, [&]() {
glStencilMaskSeparate(GL_FRONT, maskFront);
});
update_state(state.stencil.back.stencilMask, maskBack, [&]() {
glStencilMaskSeparate(GL_BACK, maskBack);
});
}
void OpenGLContext::polygonOffset(GLfloat factor, GLfloat units) noexcept {
update_state(state.polygonOffset, { factor, units }, [&]() {
if (factor != 0 || units != 0) {
glPolygonOffset(factor, units);
enable(GL_POLYGON_OFFSET_FILL);
} else {
disable(GL_POLYGON_OFFSET_FILL);
}
});
}
} // namespace filament
#endif //TNT_FILAMENT_BACKEND_OPENGLCONTEXT_H #endif //TNT_FILAMENT_BACKEND_OPENGLCONTEXT_H
File diff suppressed because it is too large Load Diff
+18 -30
View File
@@ -74,7 +74,6 @@
namespace filament::backend { namespace filament::backend {
class OpenGLPlatform; class OpenGLPlatform;
class OpenGLState;
class PixelBufferDescriptor; class PixelBufferDescriptor;
struct TargetBufferInfo; struct TargetBufferInfo;
class OpenGLProgram; class OpenGLProgram;
@@ -197,6 +196,8 @@ public:
struct GLFence : public HwFence { struct GLFence : public HwFence {
using HwFence::HwFence; using HwFence::HwFence;
struct State { struct State {
utils::Mutex lock; // NOLINT(*-include-cleaner)
utils::Condition cond; // NOLINT(*-include-cleaner)
FenceStatus status{ FenceStatus::TIMEOUT_EXPIRED }; FenceStatus status{ FenceStatus::TIMEOUT_EXPIRED };
}; };
std::shared_ptr<State> state{ std::make_shared<State>() }; std::shared_ptr<State> state{ std::make_shared<State>() };
@@ -223,24 +224,11 @@ public:
private: private:
OpenGLPlatform& mPlatform; OpenGLPlatform& mPlatform;
OpenGLContext mContext; OpenGLContext mContext;
OpenGLState* mBackendState = nullptr; // owned by backend thread
OpenGLState* mWorkerState = nullptr; // owned by worker thread (initialized for async mode)
ShaderCompilerService mShaderCompilerService; ShaderCompilerService mShaderCompilerService;
friend class TimerQueryFactory; friend class TimerQueryFactory;
friend class TimerQueryNativeFactory; friend class TimerQueryNativeFactory;
OpenGLContext& getContext() noexcept { return mContext; } OpenGLContext& getContext() noexcept { return mContext; }
OpenGLState& getBackendState() const noexcept {
// mBackendState should always be available.
assert_invariant(mBackendState);
return *mBackendState;
}
OpenGLState& getWorkerState() const noexcept {
// mWorkerState should be available only when the asynchronous mode is enabled.
// Ideally, this method should be called within asynchronous jobs.
assert_invariant(mWorkerState);
return *mWorkerState;
}
ShaderCompilerService& getShaderCompilerService() noexcept { ShaderCompilerService& getShaderCompilerService() noexcept {
return mShaderCompilerService; return mShaderCompilerService;
@@ -255,15 +243,15 @@ private:
*/ */
utils::CString getVendorString() const noexcept override { utils::CString getVendorString() const noexcept override {
return utils::CString{ mContext.vendor }; return utils::CString{ mContext.state.vendor };
} }
utils::CString getRendererString() const noexcept override { utils::CString getRendererString() const noexcept override {
return utils::CString{ mContext.renderer }; return utils::CString{ mContext.state.renderer };
} }
utils::CString getVersionString() const noexcept override { utils::CString getVersionString() const noexcept override {
return utils::CString{ mContext.version }; return utils::CString{ mContext.state.version };
} }
JobQueue* getJobQueue() const noexcept { return mJobQueue.get(); } JobQueue* getJobQueue() const noexcept { return mJobQueue.get(); }
@@ -347,13 +335,13 @@ private:
void setStencilState(StencilState ss) noexcept; void setStencilState(StencilState ss) noexcept;
void setTextureData(OpenGLState& gl, GLTexture const* t, void setTextureData(GLTexture const* t,
uint32_t level, uint32_t level,
uint32_t xoffset, uint32_t yoffset, uint32_t zoffset, uint32_t xoffset, uint32_t yoffset, uint32_t zoffset,
uint32_t width, uint32_t height, uint32_t depth, uint32_t width, uint32_t height, uint32_t depth,
PixelBufferDescriptor&& p); PixelBufferDescriptor&& p);
void setCompressedTextureData(OpenGLState& gl, GLTexture const* t, void setCompressedTextureData(GLTexture const* t,
uint32_t level, uint32_t level,
uint32_t xoffset, uint32_t yoffset, uint32_t zoffset, uint32_t xoffset, uint32_t yoffset, uint32_t zoffset,
uint32_t width, uint32_t height, uint32_t depth, uint32_t width, uint32_t height, uint32_t depth,
@@ -362,7 +350,7 @@ private:
void renderBufferStorage(GLuint rbo, GLenum internalformat, uint32_t width, void renderBufferStorage(GLuint rbo, GLenum internalformat, uint32_t width,
uint32_t height, uint8_t samples) const noexcept; uint32_t height, uint8_t samples) const noexcept;
void textureStorage(OpenGLState& gl, GLTexture* t, uint32_t width, uint32_t height, void textureStorage(GLTexture* t, uint32_t width, uint32_t height,
uint32_t depth, bool useProtectedMemory) noexcept; uint32_t depth, bool useProtectedMemory) noexcept;
/* State tracking GL wrappers... */ /* State tracking GL wrappers... */
@@ -382,32 +370,32 @@ private:
// Common methods // Common methods
void readPixelsFromBoundFramebuffer(uint32_t x, uint32_t y, uint32_t width, uint32_t height, void readPixelsFromBoundFramebuffer(uint32_t x, uint32_t y, uint32_t width, uint32_t height,
PixelBufferDescriptor&& p); PixelBufferDescriptor&& p);
void createTextureCommon(OpenGLState& gl, Handle<HwTexture> th, SamplerType target, uint8_t levels, void createTextureCommon(Handle<HwTexture> th, SamplerType target, uint8_t levels,
TextureFormat format, uint8_t samples, uint32_t width, uint32_t height, uint32_t depth, TextureFormat format, uint8_t samples, uint32_t width, uint32_t height, uint32_t depth,
TextureUsage usage, utils::ImmutableCString&& tag); TextureUsage usage, utils::ImmutableCString&& tag);
void update3DImageCommon(OpenGLState& gl, Handle<HwTexture> th, void update3DImageCommon(Handle<HwTexture> th,
uint32_t level, uint32_t xoffset, uint32_t yoffset, uint32_t zoffset, uint32_t level, uint32_t xoffset, uint32_t yoffset, uint32_t zoffset,
uint32_t width, uint32_t height, uint32_t depth, uint32_t width, uint32_t height, uint32_t depth,
PixelBufferDescriptor&& data); PixelBufferDescriptor&& data);
void createTextureViewSwizzleCommon(Handle<HwTexture> th, Handle<HwTexture> srch, void createTextureViewSwizzleCommon(Handle<HwTexture> th, Handle<HwTexture> srch,
TextureSwizzle r, TextureSwizzle g, TextureSwizzle b, TextureSwizzle r, TextureSwizzle g, TextureSwizzle b,
TextureSwizzle a, utils::ImmutableCString&& tag); TextureSwizzle a, utils::ImmutableCString&& tag);
void importTextureCommon(OpenGLState& gl, Handle<HwTexture> th, intptr_t id, SamplerType target, uint8_t levels, void importTextureCommon(Handle<HwTexture> th, intptr_t id, SamplerType target, uint8_t levels,
TextureFormat format, uint8_t samples, uint32_t width, uint32_t height, uint32_t depth, TextureFormat format, uint8_t samples, uint32_t width, uint32_t height, uint32_t depth,
TextureUsage usage, utils::ImmutableCString&& tag); TextureUsage usage, utils::ImmutableCString&& tag);
void createBufferObjectCommon(OpenGLState& gl, Handle<HwBufferObject> boh, uint32_t byteCount, void createBufferObjectCommon(Handle<HwBufferObject> boh, uint32_t byteCount,
BufferObjectBinding bindingType, BufferUsage usage, utils::ImmutableCString&& tag); BufferObjectBinding bindingType, BufferUsage usage, utils::ImmutableCString&& tag);
void setVertexBufferObjectCommon(Handle<HwVertexBuffer> vbh, uint32_t index, void setVertexBufferObjectCommon(Handle<HwVertexBuffer> vbh, uint32_t index,
Handle<HwBufferObject> boh); Handle<HwBufferObject> boh);
void updateBufferObjectCommon(OpenGLState& gl, Handle<HwBufferObject> boh, BufferDescriptor&& bd, void updateBufferObjectCommon(Handle<HwBufferObject> boh, BufferDescriptor&& bd,
uint32_t byteOffset); uint32_t byteOffset);
void createIndexBufferCommon(OpenGLState& gl, Handle<HwIndexBuffer> ibh, ElementType const elementType, void createIndexBufferCommon(Handle<HwIndexBuffer> ibh, ElementType const elementType,
uint32_t indexCount, BufferUsage const usage, utils::ImmutableCString&& tag); uint32_t indexCount, BufferUsage const usage, utils::ImmutableCString&& tag);
void updateIndexBufferCommon(OpenGLState& gl, Handle<HwIndexBuffer> ibh, BufferDescriptor&& p, void updateIndexBufferCommon(Handle<HwIndexBuffer> ibh, BufferDescriptor&& p,
uint32_t const byteOffset); uint32_t const byteOffset);
void destroyTextureCommon(OpenGLState& gl, Handle<HwTexture> th); void destroyTextureCommon(Handle<HwTexture> th);
void destroyBufferObjectCommon(OpenGLState& gl, Handle<HwBufferObject> boh); void destroyBufferObjectCommon(Handle<HwBufferObject> boh);
void destroyIndexBufferCommon(OpenGLState& gl, Handle<HwIndexBuffer> ibh); void destroyIndexBufferCommon(Handle<HwIndexBuffer> ibh);
// state required to represent the current render pass // state required to represent the current render pass
Handle<HwRenderTarget> mRenderPassTarget; Handle<HwRenderTarget> mRenderPassTarget;
@@ -112,7 +112,7 @@ void OpenGLProgram::initialize(OpenGLDriver& gld) {
assert_invariant(mToken == nullptr); assert_invariant(mToken == nullptr);
if (gl.program) { if (gl.program) {
assert_invariant(lazyInitializationData); assert_invariant(lazyInitializationData);
initializeProgramState(gld.getBackendState(), gl.program, *lazyInitializationData); initializeProgramState(gld.getContext(), gl.program, *lazyInitializationData);
delete lazyInitializationData; delete lazyInitializationData;
} }
} }
@@ -121,7 +121,7 @@ void OpenGLProgram::initialize(OpenGLDriver& gld) {
* Initializes our internal state from a valid program. This must only be called after * Initializes our internal state from a valid program. This must only be called after
* checkProgramStatus() has been successfully called. * checkProgramStatus() has been successfully called.
*/ */
void OpenGLProgram::initializeProgramState(OpenGLState& gls, GLuint program, void OpenGLProgram::initializeProgramState(OpenGLContext& context, GLuint program,
LazyInitializationData& lazyInitializationData) { LazyInitializationData& lazyInitializationData) {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT); FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
@@ -139,7 +139,7 @@ void OpenGLProgram::initializeProgramState(OpenGLState& gls, GLuint program,
GLuint binding = 0; GLuint binding = 0;
// needed for samplers // needed for samplers
gls.useProgram(program); context.useProgram(program);
UTILS_NOUNROLL UTILS_NOUNROLL
for (descriptor_set_t set = 0; set < MAX_DESCRIPTOR_SET_COUNT; set++) { for (descriptor_set_t set = 0; set < MAX_DESCRIPTOR_SET_COUNT; set++) {
@@ -149,7 +149,7 @@ void OpenGLProgram::initializeProgramState(OpenGLState& gls, GLuint program,
case DescriptorType::SHADER_STORAGE_BUFFER: { case DescriptorType::SHADER_STORAGE_BUFFER: {
if (!entry.name.empty()) { if (!entry.name.empty()) {
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2 #ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
if (UTILS_LIKELY(!gls.isES2())) { if (UTILS_LIKELY(!context.isES2())) {
GLuint const index = glGetUniformBlockIndex(program, GLuint const index = glGetUniformBlockIndex(program,
entry.name.c_str()); entry.name.c_str());
if (index != GL_INVALID_INDEX) { if (index != GL_INVALID_INDEX) {
@@ -219,7 +219,7 @@ void OpenGLProgram::initializeProgramState(OpenGLState& gls, GLuint program,
CHECK_GL_ERROR() CHECK_GL_ERROR()
} }
if (gls.isES2()) { if (context.isES2()) {
// ES2 initialization of (fake) UBOs // ES2 initialization of (fake) UBOs
UniformsRecord* const uniformsRecords = new(std::nothrow) UniformsRecord[Program::UNIFORM_BINDING_COUNT]; UniformsRecord* const uniformsRecords = new(std::nothrow) UniformsRecord[Program::UNIFORM_BINDING_COUNT];
UTILS_NOUNROLL UTILS_NOUNROLL
+4 -4
View File
@@ -20,7 +20,7 @@
#include "DriverBase.h" #include "DriverBase.h"
#include "BindingMap.h" #include "BindingMap.h"
#include "OpenGLState.h" #include "OpenGLContext.h"
#include "ShaderCompilerService.h" #include "ShaderCompilerService.h"
#include <private/backend/Driver.h> #include <private/backend/Driver.h>
@@ -56,7 +56,7 @@ public:
bool isValid() const noexcept { return mToken || gl.program != 0; } bool isValid() const noexcept { return mToken || gl.program != 0; }
bool use(OpenGLDriver* const gld, OpenGLState& gls) noexcept { bool use(OpenGLDriver* const gld, OpenGLContext& context) noexcept {
// both non-null is impossible by construction // both non-null is impossible by construction
assert_invariant(!mToken || !gl.program); assert_invariant(!mToken || !gl.program);
@@ -71,7 +71,7 @@ public:
return false; return false;
} }
gls.useProgram(gl.program); context.useProgram(gl.program);
return true; return true;
} }
@@ -105,7 +105,7 @@ private:
void initialize(OpenGLDriver& gld); void initialize(OpenGLDriver& gld);
void initializeProgramState(OpenGLState& gls, GLuint program, void initializeProgramState(OpenGLContext& context, GLuint program,
LazyInitializationData& lazyInitializationData); LazyInitializationData& lazyInitializationData);
BindingMap mBindingMap; // 8 bytes + out-of-line 256 bytes BindingMap mBindingMap; // 8 bytes + out-of-line 256 bytes
-559
View File
@@ -1,559 +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 "OpenGLState.h"
#include "GLUtils.h"
#include <backend/platforms/OpenGLPlatform.h>
#include <utility>
using namespace utils;
namespace filament::backend {
OpenGLState::OpenGLState(OpenGLContext& context) noexcept
: ext(context.ext),
bugs(context.bugs),
gets(context.gets),
procs(context.procs),
features(context.features),
mContext(context),
mTimerQueryFactory(TimerQueryFactory::init(mContext.getPlatform(), context)),
mSamplerMap(32) {
state.vao.p = &mDefaultVAO;
// Push GL defaults to the new context
setDefaultState();
}
OpenGLState::~OpenGLState() noexcept {
delete mTimerQueryFactory;
}
void OpenGLState::terminate() noexcept {
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
if (!isES2()) {
for (auto& item: mSamplerMap) {
unbindSampler(item.second);
glDeleteSamplers(1, &item.second);
}
mSamplerMap.clear();
}
#endif
}
void OpenGLState::destroyWithContext(
size_t index, std::function<void(OpenGLState&)> const& closure) {
if (index == 0) {
// Note: we only need to delay the destruction of objects on the unprotected context
// (index 0) because the protected context is always immediately destroyed and all its
// active objects and bindings are then automatically destroyed.
// TODO: this is only guaranteed for EGLPlatform, but that's the only one we care about.
mDestroyWithNormalContext.push_back(closure);
}
}
void OpenGLState::unbindEverything() noexcept {
// TODO: we're supposed to unbind everything here so that resources don't get
// stuck in this context (contextIndex) when destroyed in the other context.
// However, because EGLPlatform always immediately destroys the protected context (1),
// the bindings will automatically be severed when we switch back to the default context.
// Since bindings now only exist in one context, we don't have a ref-counting issue to
// worry about.
}
void OpenGLState::synchronizeStateAndCache(size_t index) {
// if we're just switching back to context 0, run all the pending destructors
if (index == 0) {
auto list = std::move(mDestroyWithNormalContext);
for (auto&& fn: list) {
fn(*this);
}
}
// the default FBO could be invalid
mDefaultFbo[index].reset();
contextIndex = index;
syncState();
}
void OpenGLState::setDefaultState() noexcept {
// We need to make sure our internal state matches the GL state when we start.
// (some of these calls may be unneeded as they might be the gl defaults)
constexpr GLenum caps[] = {
GL_BLEND,
GL_CULL_FACE,
GL_SCISSOR_TEST,
GL_DEPTH_TEST,
GL_STENCIL_TEST,
GL_DITHER,
GL_SAMPLE_ALPHA_TO_COVERAGE,
GL_SAMPLE_COVERAGE,
GL_POLYGON_OFFSET_FILL,
};
UTILS_NOUNROLL
for (auto const capi : caps) {
size_t const capIndex = getIndexForCap(capi);
if (state.enables.caps[capIndex]) {
glEnable(capi);
} else {
glDisable(capi);
}
}
// Point sprite size and seamless cubemap filtering are disabled by default in desktop GL.
// In OpenGL ES, these flags do not exist because they are always on.
#ifdef BACKEND_OPENGL_VERSION_GL
glEnable(GL_PROGRAM_POINT_SIZE);
enable(GL_PROGRAM_POINT_SIZE);
#endif
#ifdef GL_ARB_seamless_cube_map
glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
enable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
#endif
#ifdef GL_FRAGMENT_SHADER_DERIVATIVE_HINT
glHint(GL_FRAGMENT_SHADER_DERIVATIVE_HINT, GL_NICEST);
#endif
#if !defined(__EMSCRIPTEN__)
if (ext.EXT_clip_control) {
#if defined(BACKEND_OPENGL_VERSION_GL)
glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE);
#elif defined(GL_EXT_clip_control)
glClipControlEXT(GL_LOWER_LEFT_EXT, GL_ZERO_TO_ONE_EXT);
#endif
}
#endif
if (ext.EXT_clip_cull_distance
&& mContext.getDriverConfig().stereoscopicType == StereoscopicType::INSTANCED) {
glEnable(GL_CLIP_DISTANCE0);
glEnable(GL_CLIP_DISTANCE1);
}
}
GLuint OpenGLState::bindFramebuffer(GLenum target, GLuint buffer) noexcept {
if (UTILS_UNLIKELY(buffer == 0)) {
// we're binding the default frame buffer, resolve its actual name
auto& defaultFboForThisContext = mDefaultFbo[contextIndex];
if (UTILS_UNLIKELY(!defaultFboForThisContext.has_value())) {
defaultFboForThisContext = GLuint(mContext.getPlatform().getDefaultFramebufferObject());
}
// by construction, defaultFboForThisContext has a value. value_or() avoids a throwing call.
buffer = defaultFboForThisContext.value_or(0);
}
bindFramebufferResolved(target, buffer);
return buffer;
}
void OpenGLState::unbindFramebuffer(GLenum target) noexcept {
bindFramebufferResolved(target, 0);
}
void OpenGLState::bindFramebufferResolved(GLenum target, GLuint buffer) noexcept {
switch (target) {
case GL_FRAMEBUFFER:
if (state.draw_fbo != buffer || state.read_fbo != buffer) {
state.draw_fbo = state.read_fbo = buffer;
glBindFramebuffer(target, buffer);
}
break;
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
case GL_DRAW_FRAMEBUFFER:
if (state.draw_fbo != buffer) {
state.draw_fbo = buffer;
glBindFramebuffer(target, buffer);
}
break;
case GL_READ_FRAMEBUFFER:
if (state.read_fbo != buffer) {
state.read_fbo = buffer;
glBindFramebuffer(target, buffer);
}
break;
#endif
default:
break;
}
}
void OpenGLState::bindBuffer(GLenum target, GLuint buffer) noexcept {
if (target == GL_ELEMENT_ARRAY_BUFFER) {
constexpr size_t targetIndex = getIndexForBufferTarget(GL_ELEMENT_ARRAY_BUFFER);
// GL_ELEMENT_ARRAY_BUFFER is a special case, where the currently bound VAO remembers
// the index buffer, unless there are no VAO bound (see: bindVertexArray)
assert_invariant(state.vao.p);
if (state.buffers.genericBinding[targetIndex] != buffer
|| ((state.vao.p != &mDefaultVAO) && (state.vao.p->elementArray != buffer))) {
state.buffers.genericBinding[targetIndex] = buffer;
if (state.vao.p != &mDefaultVAO) {
state.vao.p->elementArray = buffer;
}
glBindBuffer(target, buffer);
}
} else {
size_t const targetIndex = getIndexForBufferTarget(target);
update_state(state.buffers.genericBinding[targetIndex], buffer, [&]() {
glBindBuffer(target, buffer);
});
}
}
void OpenGLState::pixelStore(GLenum pname, GLint param) noexcept {
GLint* pcur;
// Note: GL_UNPACK_SKIP_PIXELS, GL_UNPACK_SKIP_ROWS and
// GL_PACK_SKIP_PIXELS, GL_PACK_SKIP_ROWS
// are actually provided as conveniences to the programmer; they provide no functionality
// that cannot be duplicated at the call site (e.g. glTexImage2D or glReadPixels)
switch (pname) {
case GL_PACK_ALIGNMENT:
pcur = &state.pack.alignment;
break;
case GL_UNPACK_ALIGNMENT:
pcur = &state.unpack.alignment;
break;
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
case GL_UNPACK_ROW_LENGTH:
assert_invariant(mContext.major > 2);
pcur = &state.unpack.row_length;
break;
#endif
default:
goto default_case;
}
if (UTILS_UNLIKELY(*pcur != param)) {
*pcur = param;
default_case:
glPixelStorei(pname, param);
}
}
void OpenGLState::unbindTexture(
UTILS_UNUSED_IN_RELEASE GLenum target, GLuint texture_id) noexcept {
// unbind this texture from all the units it might be bound to
// no need unbind the texture from FBOs because we're not tracking that state (and there is
// no need to).
// Never attempt to unbind texture 0. This could happen with external textures w/ streaming if
// never populated.
if (texture_id) {
UTILS_NOUNROLL
for (GLuint unit = 0; unit < MAX_TEXTURE_UNIT_COUNT; unit++) {
if (state.textures.units[unit].id == texture_id) {
assert_invariant(state.textures.units[unit].target == target);
unbindTextureUnit(unit);
}
}
}
}
void OpenGLState::unbindTextureUnit(GLuint unit) noexcept {
update_state(state.textures.units[unit].id, 0u, [&]() {
activeTexture(unit);
glBindTexture(state.textures.units[unit].target, 0u);
});
}
void OpenGLState::unbindSampler(GLuint sampler) noexcept {
// unbind this sampler from all the units it might be bound to
UTILS_NOUNROLL
for (GLuint unit = 0; unit < MAX_TEXTURE_UNIT_COUNT; unit++) {
if (state.textures.units[unit].sampler == sampler) {
bindSampler(unit, 0);
}
}
}
void OpenGLState::deleteBuffer(GLuint buffer, GLenum target) noexcept {
glDeleteBuffers(1, &buffer);
// bindings of bound buffers are reset to 0
size_t const targetIndex = getIndexForBufferTarget(target);
auto& genericBinding = state.buffers.genericBinding[targetIndex];
if (genericBinding == buffer) {
genericBinding = 0;
}
if (UTILS_UNLIKELY(bugs.rebind_buffer_after_deletion)) {
if (genericBinding) {
glBindBuffer(target, genericBinding);
}
}
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
assert_invariant(mContext.getFeatureLevel() >= FeatureLevel::FEATURE_LEVEL_1 ||
(target != GL_UNIFORM_BUFFER && target != GL_TRANSFORM_FEEDBACK_BUFFER));
if (target == GL_UNIFORM_BUFFER || target == GL_TRANSFORM_FEEDBACK_BUFFER) {
auto& indexedBinding = state.buffers.targets[targetIndex];
UTILS_NOUNROLL
for (auto& entry: indexedBinding.buffers) {
if (entry.name == buffer) {
entry.name = 0;
entry.offset = 0;
entry.size = 0;
}
}
}
#endif
}
void OpenGLState::deleteVertexArray(GLuint vao) noexcept {
if (UTILS_LIKELY(vao)) {
procs.deleteVertexArrays(1, &vao);
// if the destroyed VAO is bound, clear the binding.
if (state.vao.p->vao[contextIndex] == vao) {
bindVertexArray(nullptr);
}
}
}
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
GLuint OpenGLState::getSamplerSlow(SamplerParams params) const noexcept {
assert_invariant(mSamplerMap.find(params) == mSamplerMap.end());
using namespace GLUtils;
GLuint s;
glGenSamplers(1, &s);
glSamplerParameteri(s, GL_TEXTURE_MIN_FILTER, (GLint)getTextureFilter(params.filterMin));
glSamplerParameteri(s, GL_TEXTURE_MAG_FILTER, (GLint)getTextureFilter(params.filterMag));
glSamplerParameteri(s, GL_TEXTURE_WRAP_S, (GLint)getWrapMode(params.wrapS));
glSamplerParameteri(s, GL_TEXTURE_WRAP_T, (GLint)getWrapMode(params.wrapT));
glSamplerParameteri(s, GL_TEXTURE_WRAP_R, (GLint)getWrapMode(params.wrapR));
glSamplerParameteri(s, GL_TEXTURE_COMPARE_MODE, (GLint)getTextureCompareMode(params.compareMode));
glSamplerParameteri(s, GL_TEXTURE_COMPARE_FUNC, (GLint)getTextureCompareFunc(params.compareFunc));
#if defined(GL_EXT_texture_filter_anisotropic)
if (ext.EXT_texture_filter_anisotropic &&
!bugs.texture_filter_anisotropic_broken_on_sampler) {
GLfloat const anisotropy = float(1u << params.anisotropyLog2);
glSamplerParameterf(s, GL_TEXTURE_MAX_ANISOTROPY_EXT,
std::min(gets.max_anisotropy, anisotropy));
}
#endif
CHECK_GL_ERROR()
mSamplerMap[params] = s;
return s;
}
#endif
void OpenGLState::syncState() noexcept {
// Force GL state to match the Filament state
// increase the state version so other parts of the state know to reset
state.age++;
if (mContext.major > 2) {
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, state.draw_fbo);
glBindFramebuffer(GL_READ_FRAMEBUFFER, state.read_fbo);
#endif
} else {
assert_invariant(state.read_fbo == state.draw_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, state.draw_fbo);
state.read_fbo = state.draw_fbo;
}
// state.program
glUseProgram(state.program.use);
// state.vao
state.vao.p = nullptr;
bindVertexArray(nullptr);
// state.raster
glFrontFace(state.raster.frontFace);
glCullFace(state.raster.cullFace);
glBlendEquationSeparate(state.raster.blendEquationRGB, state.raster.blendEquationA);
glBlendFuncSeparate(
state.raster.blendFunctionSrcRGB,
state.raster.blendFunctionDstRGB,
state.raster.blendFunctionSrcA,
state.raster.blendFunctionDstA
);
glColorMask(
state.raster.colorMask,
state.raster.colorMask,
state.raster.colorMask,
state.raster.colorMask
);
glDepthMask(state.raster.depthMask);
glDepthFunc(state.raster.depthFunc);
// state.stencil
glStencilFuncSeparate(
GL_FRONT,
state.stencil.front.func.func,
state.stencil.front.func.ref,
state.stencil.front.func.mask
);
glStencilFuncSeparate(
GL_BACK,
state.stencil.back.func.func,
state.stencil.back.func.ref,
state.stencil.back.func.mask
);
glStencilOpSeparate(
GL_FRONT,
state.stencil.front.op.sfail,
state.stencil.front.op.dpfail,
state.stencil.front.op.dppass
);
glStencilOpSeparate(
GL_BACK,
state.stencil.back.op.sfail,
state.stencil.back.op.dpfail,
state.stencil.back.op.dppass
);
glStencilMaskSeparate(GL_FRONT, state.stencil.front.stencilMask);
glStencilMaskSeparate(GL_BACK, state.stencil.back.stencilMask);
// state.polygonOffset
glPolygonOffset(state.polygonOffset.factor, state.polygonOffset.units);
// state.enables
setDefaultState();
// state.buffers
// Reset state.buffers to its default state to avoid the complexity and error-prone
// nature of resetting the GL state to its existing state
state.buffers = {};
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
if (mContext.major > 2) {
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
for (auto const target: {
GL_UNIFORM_BUFFER,
GL_TRANSFORM_FEEDBACK_BUFFER,
#if defined(BACKEND_OPENGL_LEVEL_GLES31)
GL_SHADER_STORAGE_BUFFER,
#endif
GL_PIXEL_PACK_BUFFER,
GL_PIXEL_UNPACK_BUFFER,
}) {
glBindBuffer(target, 0);
}
for (size_t bufferIndex = 0; bufferIndex < MAX_BUFFER_BINDINGS; ++bufferIndex) {
if (bufferIndex < (size_t)gets.max_uniform_buffer_bindings) {
glBindBufferBase(GL_UNIFORM_BUFFER, bufferIndex, 0);
}
if (bufferIndex < (size_t)gets.max_transform_feedback_separate_attribs) {
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, bufferIndex, 0);
}
}
#endif
}
// state.textures
// Reset state.textures to its default state to avoid the complexity and error-prone
// nature of resetting the GL state to its existing state
state.textures = {};
const std::pair<GLuint, bool> textureTargets[] = {
{ GL_TEXTURE_2D, true },
{ GL_TEXTURE_2D_ARRAY, true },
{ GL_TEXTURE_CUBE_MAP, true },
{ GL_TEXTURE_3D, true },
#if defined(BACKEND_OPENGL_LEVEL_GLES31)
{ GL_TEXTURE_2D_MULTISAMPLE, true },
#endif
#if !defined(__EMSCRIPTEN__)
#if defined(GL_OES_EGL_image_external)
{ GL_TEXTURE_EXTERNAL_OES, ext.OES_EGL_image_external_essl3 },
#endif
#if defined(BACKEND_OPENGL_VERSION_GL) || defined(GL_EXT_texture_cube_map_array)
{ GL_TEXTURE_CUBE_MAP_ARRAY, ext.EXT_texture_cube_map_array },
#endif
#endif
};
for (GLint unit = 0; unit < gets.max_combined_texture_image_units; ++unit) {
glActiveTexture(GL_TEXTURE0 + unit);
if (mContext.major > 2) {
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
glBindSampler(unit, 0);
#endif
}
for (auto [target, available] : textureTargets) {
if (available) {
glBindTexture(target, 0);
}
}
}
glActiveTexture(GL_TEXTURE0 + state.textures.active);
// state.unpack
glPixelStorei(GL_UNPACK_ALIGNMENT, state.unpack.alignment);
if (mContext.major > 2) {
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
glPixelStorei(GL_UNPACK_ROW_LENGTH, state.unpack.row_length);
#endif
}
// state.pack
glPixelStorei(GL_PACK_ALIGNMENT, state.pack.alignment);
if (mContext.major > 2) {
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
#endif
}
// state.window
glScissor(
state.window.scissor.x,
state.window.scissor.y,
state.window.scissor.z,
state.window.scissor.w
);
glViewport(
state.window.viewport.x,
state.window.viewport.y,
state.window.viewport.z,
state.window.viewport.w
);
glDepthRangef(state.window.depthRange.x, state.window.depthRange.y);
}
void OpenGLState::createTimerQuery(GLTimerQuery* query) {
mTimerQueryFactory->createTimerQuery(query);
}
void OpenGLState::destroyTimerQuery(GLTimerQuery* query) {
mTimerQueryFactory->destroyTimerQuery(query);
}
void OpenGLState::beginTimeElapsedQuery(GLTimerQuery* query) {
mTimerQueryFactory->beginTimeElapsedQuery(query);
}
void OpenGLState::endTimeElapsedQuery(OpenGLDriver& driver, GLTimerQuery* query) {
mTimerQueryFactory->endTimeElapsedQuery(driver, query);
}
} // namespace filament::backend
-615
View File
@@ -1,615 +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.
*/
#ifndef TNT_FILAMENT_BACKEND_OPENGL_OPENGLSTATE_H
#define TNT_FILAMENT_BACKEND_OPENGL_OPENGLSTATE_H
#include "OpenGLContext.h"
#include "OpenGLTimerQuery.h"
#include <backend/DriverEnums.h>
#include "gl_headers.h"
#include <utils/compiler.h>
#include <utils/bitset.h>
#include <utils/debug.h>
#include <math/vec2.h>
#include <math/vec4.h>
#include <tsl/robin_map.h>
#include <functional>
#include <optional>
#include <vector>
#include <stddef.h>
#include <stdint.h>
namespace filament::backend {
class OpenGLDriver;
class OpenGLState final : public TimerQueryFactoryInterface {
public:
using RenderPrimitive = OpenGLContext::RenderPrimitive;
using Extensions = OpenGLContext::Extensions;
using Bugs = OpenGLContext::Bugs;
using Gets = OpenGLContext::Gets;
using Procs = OpenGLContext::Procs;
using vec4gli = OpenGLContext::vec4gli;
using vec2glf = OpenGLContext::vec2glf;
static constexpr size_t MAX_TEXTURE_UNIT_COUNT = OpenGLContext::MAX_TEXTURE_UNIT_COUNT;
static constexpr size_t DUMMY_TEXTURE_BINDING = OpenGLContext::DUMMY_TEXTURE_BINDING;
static constexpr size_t MAX_BUFFER_BINDINGS = OpenGLContext::MAX_BUFFER_BINDINGS;
explicit OpenGLState(OpenGLContext& context) noexcept;
~OpenGLState() noexcept override;
// TimerQueryInterface ------------------------------------------------------------------------
// note: OpenGLState being final ensures (clang) these are not called through the vtable
void createTimerQuery(GLTimerQuery* query) override;
void destroyTimerQuery(GLTimerQuery* query) override;
void beginTimeElapsedQuery(GLTimerQuery* query) override;
void endTimeElapsedQuery(OpenGLDriver& driver, GLTimerQuery* query) override;
// --------------------------------------------------------------------------------------------
// Immutable config forwarding (enables gl.ext.XXX, gl.bugs.XXX, etc)
Extensions const& ext;
Bugs const& bugs;
Gets const& gets;
Procs const& procs;
decltype(OpenGLContext::features) const& features;
// Read-only query forwarding
bool isES2() const noexcept { return mContext.isES2(); }
bool hasFences() const noexcept { return mContext.hasFences(); }
FeatureLevel getFeatureLevel() const noexcept { return mContext.getFeatureLevel(); }
ShaderModel getShaderModel() const noexcept { return mContext.getShaderModel(); }
template<int MAJOR, int MINOR>
bool isAtLeastGL() const noexcept { return mContext.isAtLeastGL<MAJOR, MINOR>(); }
template<int MAJOR, int MINOR>
bool isAtLeastGLES() const noexcept { return mContext.isAtLeastGLES<MAJOR, MINOR>(); }
OpenGLContext const& context() const noexcept { return mContext; }
// --------------------------------------------------------------------------------------------
constexpr inline size_t getIndexForCap(GLenum cap) noexcept;
constexpr static inline size_t getIndexForBufferTarget(GLenum target) noexcept;
void terminate() noexcept;
void syncState() noexcept;
inline void useProgram(GLuint program) noexcept;
void pixelStore(GLenum, GLint) noexcept;
inline void activeTexture(GLuint unit) noexcept;
inline void bindTexture(GLuint unit, GLuint target, GLuint texId, bool external) noexcept;
void unbindTexture(GLenum target, GLuint id) noexcept;
void unbindTextureUnit(GLuint unit) noexcept;
inline void bindVertexArray(RenderPrimitive const* p) noexcept;
inline void bindSampler(GLuint unit, GLuint sampler) noexcept;
void unbindSampler(GLuint sampler) noexcept;
void bindBuffer(GLenum target, GLuint buffer) noexcept;
inline void bindBufferRange(GLenum target, GLuint index, GLuint buffer,
GLintptr offset, GLsizeiptr size) noexcept;
GLuint bindFramebuffer(GLenum target, GLuint buffer) noexcept;
void unbindFramebuffer(GLenum target) noexcept;
inline void enableVertexAttribArray(RenderPrimitive const* rp, GLuint index) noexcept;
inline void disableVertexAttribArray(RenderPrimitive const* rp, GLuint index) noexcept;
inline void enable(GLenum cap) noexcept;
inline void disable(GLenum cap) noexcept;
inline void frontFace(GLenum mode) noexcept;
inline void cullFace(GLenum mode) noexcept;
inline void blendEquation(GLenum modeRGB, GLenum modeA) noexcept;
inline void blendFunction(GLenum srcRGB, GLenum srcA, GLenum dstRGB, GLenum dstA) noexcept;
inline void colorMask(GLboolean flag) noexcept;
inline void depthMask(GLboolean flag) noexcept;
inline void depthFunc(GLenum func) noexcept;
inline void stencilFuncSeparate(GLenum funcFront, GLint refFront, GLuint maskFront,
GLenum funcBack, GLint refBack, GLuint maskBack) noexcept;
inline void stencilOpSeparate(GLenum sfailFront, GLenum dpfailFront, GLenum dppassFront,
GLenum sfailBack, GLenum dpfailBack, GLenum dppassBack) noexcept;
inline void stencilMaskSeparate(GLuint maskFront, GLuint maskBack) noexcept;
inline void polygonOffset(GLfloat factor, GLfloat units) noexcept;
inline void setScissor(GLint left, GLint bottom, GLsizei width, GLsizei height) noexcept;
inline void viewport(GLint left, GLint bottom, GLsizei width, GLsizei height) noexcept;
inline void depthRange(GLclampf near, GLclampf far) noexcept;
void deleteBuffer(GLuint buffer, GLenum target) noexcept;
void deleteVertexArray(GLuint vao) noexcept;
void destroyWithContext(size_t index, std::function<void(OpenGLState&)> const& closure);
// state getters
vec4gli const& getViewport() const { return state.window.viewport; }
// function to handle state changes we don't control
void updateTexImage(GLenum target, GLuint id) noexcept {
assert_invariant(target == GL_TEXTURE_EXTERNAL_OES);
// if another target is bound to this texture unit, unbind that texture
if (UTILS_UNLIKELY(state.textures.units[state.textures.active].target != target)) {
glBindTexture(state.textures.units[state.textures.active].target, 0);
state.textures.units[state.textures.active].target = GL_TEXTURE_EXTERNAL_OES;
}
// the texture is already bound to `target`, we just update our internal state
state.textures.units[state.textures.active].id = id;
}
void resetProgram() noexcept { state.program.use = 0; }
void unbindEverything() noexcept;
void synchronizeStateAndCache(size_t index);
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
GLuint getSamplerSlow(SamplerParams sp) const noexcept;
inline GLuint getSampler(SamplerParams sp) const noexcept {
assert_invariant(!sp.padding0);
assert_invariant(!sp.padding1);
assert_invariant(!sp.padding2);
auto& samplerMap = mSamplerMap;
auto pos = samplerMap.find(sp);
if (UTILS_UNLIKELY(pos == samplerMap.end())) {
return getSamplerSlow(sp);
}
return pos->second;
}
#endif
// This is the index of the context in use. Must be either 0 (Unprotected) or 1 (Protected).
// This is used to manage the OpenGL name of ContainerObjects within each context.
uint32_t contextIndex = 0;
// Try to keep the State structure sorted by data-access patterns
struct State {
State() noexcept = default;
// make sure we don't copy this state by accident
State(State const& rhs) = delete;
State(State&& rhs) noexcept = delete;
State& operator=(State const& rhs) = delete;
State& operator=(State&& rhs) noexcept = delete;
GLuint draw_fbo = 0;
GLuint read_fbo = 0;
struct {
GLuint use = 0;
} program;
struct {
RenderPrimitive* p = nullptr;
} vao;
struct {
GLenum frontFace = GL_CCW;
GLenum cullFace = GL_BACK;
GLenum blendEquationRGB = GL_FUNC_ADD;
GLenum blendEquationA = GL_FUNC_ADD;
GLenum blendFunctionSrcRGB = GL_ONE;
GLenum blendFunctionSrcA = GL_ONE;
GLenum blendFunctionDstRGB = GL_ZERO;
GLenum blendFunctionDstA = GL_ZERO;
GLboolean colorMask = GL_TRUE;
GLboolean depthMask = GL_TRUE;
GLenum depthFunc = GL_LESS;
} raster;
struct {
struct StencilFunc {
GLenum func = GL_ALWAYS;
GLint ref = 0;
GLuint mask = ~GLuint(0);
bool operator != (StencilFunc const& rhs) const noexcept {
return func != rhs.func || ref != rhs.ref || mask != rhs.mask;
}
};
struct StencilOp {
GLenum sfail = GL_KEEP;
GLenum dpfail = GL_KEEP;
GLenum dppass = GL_KEEP;
bool operator != (StencilOp const& rhs) const noexcept {
return sfail != rhs.sfail || dpfail != rhs.dpfail || dppass != rhs.dppass;
}
};
struct {
StencilFunc func;
StencilOp op;
GLuint stencilMask = ~GLuint(0);
} front, back;
} stencil;
struct PolygonOffset {
GLfloat factor = 0;
GLfloat units = 0;
bool operator != (PolygonOffset const& rhs) const noexcept {
return factor != rhs.factor || units != rhs.units;
}
} polygonOffset;
struct {
utils::bitset32 caps;
} enables;
struct {
struct {
struct {
GLuint name = 0;
GLintptr offset = 0;
GLsizeiptr size = 0;
} buffers[MAX_BUFFER_BINDINGS];
} targets[3]; // there are only 3 indexed buffer targets
GLuint genericBinding[7] = {};
} buffers;
struct {
GLuint active = 0; // zero-based
struct {
GLuint sampler = 0;
GLuint target = 0;
GLuint id = 0;
} units[MAX_TEXTURE_UNIT_COUNT];
} textures;
struct {
GLint row_length = 0;
GLint alignment = 4;
} unpack;
struct {
GLint alignment = 4;
} pack;
struct {
vec4gli scissor { 0 };
vec4gli viewport { 0 };
vec2glf depthRange { 0.0f, 1.0f };
} window;
uint8_t age = 0;
} state;
private:
OpenGLContext const& mContext;
TimerQueryFactoryInterface* mTimerQueryFactory = nullptr;
std::vector<std::function<void(OpenGLState&)>> mDestroyWithNormalContext;
RenderPrimitive mDefaultVAO;
std::optional<GLuint> mDefaultFbo[2]; // 0:Unprotected, 1:Protected
mutable tsl::robin_map<SamplerParams, GLuint,
SamplerParams::Hasher, SamplerParams::EqualTo> mSamplerMap;
void bindFramebufferResolved(GLenum target, GLuint buffer) noexcept;
void setDefaultState() noexcept;
template <typename T, typename F>
static inline void update_state(T& state, T const& expected, F functor, bool force = false) noexcept {
if (UTILS_UNLIKELY(force || state != expected)) {
state = expected;
functor();
}
}
};
// ------------------------------------------------------------------------------------------------
constexpr size_t OpenGLState::getIndexForCap(GLenum cap) noexcept { //NOLINT
size_t index = 0;
switch (cap) {
case GL_BLEND: index = 0; break;
case GL_CULL_FACE: index = 1; break;
case GL_SCISSOR_TEST: index = 2; break;
case GL_DEPTH_TEST: index = 3; break;
case GL_STENCIL_TEST: index = 4; break;
case GL_DITHER: index = 5; break;
case GL_SAMPLE_ALPHA_TO_COVERAGE: index = 6; break;
case GL_SAMPLE_COVERAGE: index = 7; break;
case GL_POLYGON_OFFSET_FILL: index = 8; break;
#ifdef GL_ARB_seamless_cube_map
case GL_TEXTURE_CUBE_MAP_SEAMLESS: index = 9; break;
#endif
#ifdef BACKEND_OPENGL_VERSION_GL
case GL_PROGRAM_POINT_SIZE: index = 10; break;
#endif
case GL_DEPTH_CLAMP: index = 11; break;
default: break;
}
assert_invariant(index < state.enables.caps.size());
return index;
}
constexpr size_t OpenGLState::getIndexForBufferTarget(GLenum target) noexcept {
size_t index = 0;
switch (target) {
// The indexed buffers MUST be first in this list (those usable with bindBufferRange)
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
case GL_UNIFORM_BUFFER: index = 0; break;
case GL_TRANSFORM_FEEDBACK_BUFFER: index = 1; break;
#if defined(BACKEND_OPENGL_LEVEL_GLES31)
case GL_SHADER_STORAGE_BUFFER: index = 2; break;
#endif
#endif
case GL_ARRAY_BUFFER: index = 3; break;
case GL_ELEMENT_ARRAY_BUFFER: index = 4; break;
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
case GL_PIXEL_PACK_BUFFER: index = 5; break;
case GL_PIXEL_UNPACK_BUFFER: index = 6; break;
#endif
default: break;
}
assert_invariant(index < sizeof(state.buffers.genericBinding)/sizeof(state.buffers.genericBinding[0])); // NOLINT(misc-redundant-expression)
return index;
}
// ------------------------------------------------------------------------------------------------
void OpenGLState::activeTexture(GLuint unit) noexcept {
assert_invariant(unit < MAX_TEXTURE_UNIT_COUNT);
update_state(state.textures.active, unit, [&]() {
glActiveTexture(GL_TEXTURE0 + unit);
});
}
void OpenGLState::bindSampler(GLuint unit, GLuint sampler) noexcept {
assert_invariant(unit < MAX_TEXTURE_UNIT_COUNT);
assert_invariant(mContext.getFeatureLevel() >= FeatureLevel::FEATURE_LEVEL_1);
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
update_state(state.textures.units[unit].sampler, sampler, [&]() {
glBindSampler(unit, sampler);
});
#endif
}
void OpenGLState::setScissor(GLint left, GLint bottom, GLsizei width, GLsizei height) noexcept {
vec4gli const scissor(left, bottom, width, height);
update_state(state.window.scissor, scissor, [&]() {
glScissor(left, bottom, width, height);
});
}
void OpenGLState::viewport(GLint left, GLint bottom, GLsizei width, GLsizei height) noexcept {
vec4gli const viewport(left, bottom, width, height);
update_state(state.window.viewport, viewport, [&]() {
glViewport(left, bottom, width, height);
});
}
void OpenGLState::depthRange(GLclampf near, GLclampf far) noexcept {
vec2glf const depthRange(near, far);
update_state(state.window.depthRange, depthRange, [&]() {
glDepthRangef(near, far);
});
}
void OpenGLState::bindVertexArray(RenderPrimitive const* p) noexcept {
RenderPrimitive* vao = p ? const_cast<RenderPrimitive *>(p) : &mDefaultVAO;
update_state(state.vao.p, vao, [&]() {
// See if we need to create a name for this VAO on the fly, this would happen if:
// - we're not the default VAO, because its name is always 0
// - our name is 0, this could happen if this VAO was created in the "other" context
// - the nameVersion is out of date *and* we're on the protected context, in this case:
// - the name must be stale from a previous use of this context because we always
// destroy the protected context when we're done with it.
bool const recreateVaoName = vao != &mDefaultVAO &&
((vao->vao[contextIndex] == 0) ||
(vao->nameVersion != state.age && contextIndex == 1));
if (UTILS_UNLIKELY(recreateVaoName)) {
vao->nameVersion = state.age;
procs.genVertexArrays(1, &vao->vao[contextIndex]);
}
procs.bindVertexArray(vao->vao[contextIndex]);
// update GL_ELEMENT_ARRAY_BUFFER, which is updated by glBindVertexArray
size_t const targetIndex = getIndexForBufferTarget(GL_ELEMENT_ARRAY_BUFFER);
state.buffers.genericBinding[targetIndex] = vao->elementArray;
if (UTILS_UNLIKELY(bugs.vao_doesnt_store_element_array_buffer_binding || recreateVaoName)) {
// This shouldn't be needed, but it looks like some drivers don't do the implicit
// glBindBuffer().
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vao->elementArray);
}
});
}
void OpenGLState::bindBufferRange(GLenum target, GLuint index, GLuint buffer,
GLintptr offset, GLsizeiptr size) noexcept {
assert_invariant(mContext.getFeatureLevel() >= FeatureLevel::FEATURE_LEVEL_1);
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
# ifdef BACKEND_OPENGL_LEVEL_GLES31
assert_invariant(false
|| target == GL_UNIFORM_BUFFER
|| target == GL_TRANSFORM_FEEDBACK_BUFFER
|| target == GL_SHADER_STORAGE_BUFFER);
# else
assert_invariant(false
|| target == GL_UNIFORM_BUFFER
|| target == GL_TRANSFORM_FEEDBACK_BUFFER);
# endif
size_t const targetIndex = getIndexForBufferTarget(target);
// this ALSO sets the generic binding
assert_invariant(targetIndex < sizeof(state.buffers.targets) / sizeof(*state.buffers.targets));
if ( state.buffers.targets[targetIndex].buffers[index].name != buffer
|| state.buffers.targets[targetIndex].buffers[index].offset != offset
|| state.buffers.targets[targetIndex].buffers[index].size != size) {
state.buffers.targets[targetIndex].buffers[index].name = buffer;
state.buffers.targets[targetIndex].buffers[index].offset = offset;
state.buffers.targets[targetIndex].buffers[index].size = size;
state.buffers.genericBinding[targetIndex] = buffer;
glBindBufferRange(target, index, buffer, offset, size);
}
#endif
}
void OpenGLState::bindTexture(GLuint unit, GLuint target, GLuint texId, bool external) noexcept {
// another texture is bound to the same unit with a different target,
// unbind the texture from the current target
update_state(state.textures.units[unit].target, target, [&]() {
activeTexture(unit);
glBindTexture(state.textures.units[unit].target, 0);
});
update_state(state.textures.units[unit].id, texId, [&]() {
activeTexture(unit);
glBindTexture(target, texId);
}, external);
}
void OpenGLState::useProgram(GLuint program) noexcept {
update_state(state.program.use, program, [&]() {
glUseProgram(program);
});
}
void OpenGLState::enableVertexAttribArray(RenderPrimitive const* rp, GLuint index) noexcept {
assert_invariant(rp);
assert_invariant(index < rp->vertexAttribArray.size());
bool const force = rp->stateVersion != state.age;
if (UTILS_UNLIKELY(force || !rp->vertexAttribArray[index])) {
rp->vertexAttribArray.set(index);
glEnableVertexAttribArray(index);
}
}
void OpenGLState::disableVertexAttribArray(RenderPrimitive const* rp, GLuint index) noexcept {
assert_invariant(rp);
assert_invariant(index < rp->vertexAttribArray.size());
bool const force = rp->stateVersion != state.age;
if (UTILS_UNLIKELY(force || rp->vertexAttribArray[index])) {
rp->vertexAttribArray.unset(index);
glDisableVertexAttribArray(index);
}
}
void OpenGLState::enable(GLenum cap) noexcept {
size_t const index = getIndexForCap(cap);
if (UTILS_UNLIKELY(!state.enables.caps[index])) {
state.enables.caps.set(index);
glEnable(cap);
}
}
void OpenGLState::disable(GLenum cap) noexcept {
size_t const index = getIndexForCap(cap);
if (UTILS_UNLIKELY(state.enables.caps[index])) {
state.enables.caps.unset(index);
glDisable(cap);
}
}
void OpenGLState::frontFace(GLenum mode) noexcept {
update_state(state.raster.frontFace, mode, [&]() {
glFrontFace(mode);
});
}
void OpenGLState::cullFace(GLenum mode) noexcept {
update_state(state.raster.cullFace, mode, [&]() {
glCullFace(mode);
});
}
void OpenGLState::blendEquation(GLenum modeRGB, GLenum modeA) noexcept {
if (UTILS_UNLIKELY(
state.raster.blendEquationRGB != modeRGB || state.raster.blendEquationA != modeA)) {
state.raster.blendEquationRGB = modeRGB;
state.raster.blendEquationA = modeA;
glBlendEquationSeparate(modeRGB, modeA);
}
}
void OpenGLState::blendFunction(GLenum srcRGB, GLenum srcA, GLenum dstRGB, GLenum dstA) noexcept {
if (UTILS_UNLIKELY(
state.raster.blendFunctionSrcRGB != srcRGB ||
state.raster.blendFunctionSrcA != srcA ||
state.raster.blendFunctionDstRGB != dstRGB ||
state.raster.blendFunctionDstA != dstA)) {
state.raster.blendFunctionSrcRGB = srcRGB;
state.raster.blendFunctionSrcA = srcA;
state.raster.blendFunctionDstRGB = dstRGB;
state.raster.blendFunctionDstA = dstA;
glBlendFuncSeparate(srcRGB, dstRGB, srcA, dstA);
}
}
void OpenGLState::colorMask(GLboolean flag) noexcept {
update_state(state.raster.colorMask, flag, [&]() {
glColorMask(flag, flag, flag, flag);
});
}
void OpenGLState::depthMask(GLboolean flag) noexcept {
update_state(state.raster.depthMask, flag, [&]() {
glDepthMask(flag);
});
}
void OpenGLState::depthFunc(GLenum func) noexcept {
update_state(state.raster.depthFunc, func, [&]() {
glDepthFunc(func);
});
}
void OpenGLState::stencilFuncSeparate(GLenum funcFront, GLint refFront, GLuint maskFront,
GLenum funcBack, GLint refBack, GLuint maskBack) noexcept {
update_state(state.stencil.front.func, {funcFront, refFront, maskFront}, [&]() {
glStencilFuncSeparate(GL_FRONT, funcFront, refFront, maskFront);
});
update_state(state.stencil.back.func, {funcBack, refBack, maskBack}, [&]() {
glStencilFuncSeparate(GL_BACK, funcBack, refBack, maskBack);
});
}
void OpenGLState::stencilOpSeparate(GLenum sfailFront, GLenum dpfailFront, GLenum dppassFront,
GLenum sfailBack, GLenum dpfailBack, GLenum dppassBack) noexcept {
update_state(state.stencil.front.op, {sfailFront, dpfailFront, dppassFront}, [&]() {
glStencilOpSeparate(GL_FRONT, sfailFront, dpfailFront, dppassFront);
});
update_state(state.stencil.back.op, {sfailBack, dpfailBack, dppassBack}, [&]() {
glStencilOpSeparate(GL_BACK, sfailBack, dpfailBack, dppassBack);
});
}
void OpenGLState::stencilMaskSeparate(GLuint maskFront, GLuint maskBack) noexcept {
update_state(state.stencil.front.stencilMask, maskFront, [&]() {
glStencilMaskSeparate(GL_FRONT, maskFront);
});
update_state(state.stencil.back.stencilMask, maskBack, [&]() {
glStencilMaskSeparate(GL_BACK, maskBack);
});
}
void OpenGLState::polygonOffset(GLfloat factor, GLfloat units) noexcept {
update_state(state.polygonOffset, { factor, units }, [&]() {
if (factor != 0 || units != 0) {
glPolygonOffset(factor, units);
enable(GL_POLYGON_OFFSET_FILL);
} else {
disable(GL_POLYGON_OFFSET_FILL);
}
});
}
} // namespace filament::backend
#endif // TNT_FILAMENT_BACKEND_OPENGL_OPENGLSTATE_H
@@ -237,7 +237,7 @@ void ShaderCompilerService::init() noexcept {
uint32_t poolSize = 1; uint32_t poolSize = 1;
JobSystem::Priority priority = JobSystem::Priority::DISPLAY; JobSystem::Priority priority = JobSystem::Priority::DISPLAY;
auto const& renderer = mDriver.getContext().renderer; auto const& renderer = mDriver.getContext().state.renderer;
// Some drivers support parallel shader compilation well, so we use N // Some drivers support parallel shader compilation well, so we use N
// threads, we can use lower priority threads here because urgent compilations // threads, we can use lower priority threads here because urgent compilations
// will most likely happen on the main gl thread. Using too many thread can // will most likely happen on the main gl thread. Using too many thread can
@@ -16,10 +16,6 @@
#include "VulkanAsyncHandles.h" #include "VulkanAsyncHandles.h"
#include "VulkanConstants.h"
#include "vulkan/utils/Spirv.h"
#include <backend/DriverEnums.h> #include <backend/DriverEnums.h>
#include <utils/debug.h> #include <utils/debug.h>
@@ -33,154 +29,6 @@ using namespace bluevk;
namespace filament::backend { namespace filament::backend {
namespace {
inline VkShaderStageFlags getVkStage(backend::ShaderStage stage) {
switch(stage) {
case backend::ShaderStage::VERTEX:
return VK_SHADER_STAGE_VERTEX_BIT;
case backend::ShaderStage::FRAGMENT:
return VK_SHADER_STAGE_FRAGMENT_BIT;
case backend::ShaderStage::COMPUTE:
PANIC_POSTCONDITION("Unsupported stage");
}
}
} // namespace
PushConstantDescription::PushConstantDescription(backend::Program const& program) {
mRangeCount = 0;
uint32_t offset = 0;
// The range is laid out so that the vertex constants are defined as the first set of bytes,
// followed by fragment and compute. This means we need to keep track of the offset for each
// stage. We do the bookeeping in mDescriptions.
for (auto stage: { ShaderStage::VERTEX, ShaderStage::FRAGMENT, ShaderStage::COMPUTE }) {
auto const& constants = program.getPushConstants(stage);
if (constants.empty()) {
continue;
}
auto& description = mDescriptions[(uint8_t) stage];
// We store the type of the constant for type-checking when writing.
description.types.reserve(constants.size());
std::for_each(constants.cbegin(), constants.cend(),
[&description](Program::PushConstant t) { description.types.push_back(t.type); });
uint32_t const constantsSize = (uint32_t) constants.size() * ENTRY_SIZE;
mRanges[mRangeCount++] = {
.stageFlags = getVkStage(stage),
.offset = offset,
.size = constantsSize,
};
description.offset = offset;
offset += constantsSize;
}
}
void PushConstantDescription::write(VkCommandBuffer cmdbuf, VkPipelineLayout layout,
backend::ShaderStage stage, uint8_t index, backend::PushConstantVariant const& value) {
uint32_t binaryValue = 0;
auto const& description = mDescriptions[(uint8_t) stage];
UTILS_UNUSED_IN_RELEASE auto const& types = description.types;
uint32_t const offset = description.offset;
if (std::holds_alternative<bool>(value)) {
assert_invariant(types[index] == ConstantType::BOOL);
bool const bval = std::get<bool>(value);
binaryValue = static_cast<uint32_t const>(bval ? VK_TRUE : VK_FALSE);
} else if (std::holds_alternative<float>(value)) {
assert_invariant(types[index] == ConstantType::FLOAT);
float const fval = std::get<float>(value);
binaryValue = *reinterpret_cast<uint32_t const*>(&fval);
} else {
assert_invariant(types[index] == ConstantType::INT);
int const ival = std::get<int>(value);
binaryValue = *reinterpret_cast<uint32_t const*>(&ival);
}
vkCmdPushConstants(cmdbuf, layout, getVkStage(stage), offset + index * ENTRY_SIZE, ENTRY_SIZE,
&binaryValue);
}
VulkanProgram::VulkanProgram(VkDevice device, Program const& builder) noexcept
: HwProgram(builder.getName()),
mInfo(new(std::nothrow) PipelineInfo(builder)),
mDevice(device) {
Program::ShaderSource const& blobs = builder.getShadersSource();
auto& modules = mInfo->shaders;
auto const& specializationConstants = builder.getSpecializationConstants();
std::vector<uint32_t> shader;
static_assert(static_cast<ShaderStage>(0) == ShaderStage::VERTEX &&
static_cast<ShaderStage>(1) == ShaderStage::FRAGMENT &&
MAX_SHADER_MODULES == 2);
for (size_t i = 0; i < MAX_SHADER_MODULES; i++) {
Program::ShaderBlob const& blob = blobs[i];
uint32_t* data = (uint32_t*) blob.data();
size_t dataSize = blob.size();
if (!specializationConstants.empty()) {
fvkutils::workaroundSpecConstant(blob, specializationConstants, shader);
data = (uint32_t*) shader.data();
dataSize = shader.size() * 4;
}
VkShaderModule& module = modules[i];
VkShaderModuleCreateInfo moduleInfo = {
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = dataSize,
.pCode = data,
};
VkResult result = vkCreateShaderModule(mDevice, &moduleInfo, VKALLOC, &module);
FILAMENT_CHECK_POSTCONDITION(result == VK_SUCCESS)
<< "Unable to create shader module."
<< " error=" << static_cast<int32_t>(result);
#if FVK_ENABLED(FVK_DEBUG_DEBUG_UTILS)
utils::CString name{ builder.getName().c_str(), builder.getName().size() };
switch (static_cast<ShaderStage>(i)) {
case ShaderStage::VERTEX:
name += "_vs";
break;
case ShaderStage::FRAGMENT:
name += "_fs";
break;
default:
PANIC_POSTCONDITION("Unexpected stage");
break;
}
VulkanDriver::DebugUtils::setName(VK_OBJECT_TYPE_SHADER_MODULE,
reinterpret_cast<uint64_t>(module), name.c_str());
#endif
}
#if FVK_ENABLED(FVK_DEBUG_SHADER_MODULE)
FVK_LOGD << "Created VulkanProgram " << builder << ", shaders = (" << modules[0]
<< ", " << modules[1] << ")";
#endif
}
VulkanProgram::~VulkanProgram() {
for (auto shader: mInfo->shaders) {
vkDestroyShaderModule(mDevice, shader, VKALLOC);
}
delete mInfo;
}
void VulkanProgram::flushPushConstants(VkPipelineLayout layout) {
// At this point, we really ought to have a VkPipelineLayout.
assert_invariant(layout != VK_NULL_HANDLE);
for (const auto& c : mQueuedPushConstants) {
mInfo->pushConstantDescription.write(c.cmdbuf, layout, c.stage, c.index, c.value);
}
mQueuedPushConstants.clear();
}
std::shared_ptr<VulkanCmdFence> VulkanCmdFence::completed() noexcept { std::shared_ptr<VulkanCmdFence> VulkanCmdFence::completed() noexcept {
auto cmdFence = std::make_shared<VulkanCmdFence>(VK_NULL_HANDLE); auto cmdFence = std::make_shared<VulkanCmdFence>(VK_NULL_HANDLE);
cmdFence->mStatus = VK_SUCCESS; cmdFence->mStatus = VK_SUCCESS;
+19 -121
View File
@@ -24,9 +24,6 @@
#include "backend/Platform.h" #include "backend/Platform.h"
#include "vulkan/memory/Resource.h" #include "vulkan/memory/Resource.h"
#include "vulkan/utils/StaticVector.h"
#include <backend/Program.h>
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
@@ -38,123 +35,6 @@
namespace filament::backend { namespace filament::backend {
using PushConstantNameArray = utils::FixedCapacityVector<char const*>;
using PushConstantNameByStage = std::array<PushConstantNameArray, Program::SHADER_TYPE_COUNT>;
struct PushConstantDescription {
explicit PushConstantDescription(backend::Program const& program);
VkPushConstantRange const* getVkRanges() const noexcept { return mRanges; }
uint32_t getVkRangeCount() const noexcept { return mRangeCount; }
void write(VkCommandBuffer cmdbuf, VkPipelineLayout layout, backend::ShaderStage stage,
uint8_t index, backend::PushConstantVariant const& value);
private:
static constexpr uint32_t ENTRY_SIZE = sizeof(uint32_t);
struct ConstantDescription {
utils::FixedCapacityVector<backend::ConstantType> types;
uint32_t offset = 0;
};
// Describes the constants in each shader stage.
ConstantDescription mDescriptions[Program::SHADER_TYPE_COUNT];
VkPushConstantRange mRanges[Program::SHADER_TYPE_COUNT];
uint32_t mRangeCount;
};
// Encapsulates a VkShaderModule. Note: this is a ThreadSafeResource
// because we may use parallel compilation, in which case ref counts may
// be modified on multiple backend threads. This is simply to avoid
// destroying a VkShaderModule mid-compilation of a pipeline. It is
// still assumed that all calls will originate from the backend.
struct VulkanProgram : public HwProgram, fvkmemory::ThreadSafeResource {
using BindingList = fvkutils::StaticVector<uint16_t, MAX_SAMPLER_COUNT>;
VulkanProgram(VkDevice device, Program const& builder) noexcept;
~VulkanProgram();
/**
* Cancels any parallel compilation jobs that have not yet run for this
* program.
*/
inline void cancelParallelCompilation() {
mParallelCompilationCanceled.store(true, std::memory_order_release);
}
/**
* Writes out any queued push constants using the provided VkPipelineLayout.
*
* @param layout The layout that is to be used along with these push constants,
* in the next draw call.
*/
void flushPushConstants(VkPipelineLayout layout);
inline VkShaderModule getVertexShader() const {
return mInfo->shaders[0];
}
inline VkShaderModule getFragmentShader() const { return mInfo->shaders[1]; }
inline uint32_t getPushConstantRangeCount() const {
return mInfo->pushConstantDescription.getVkRangeCount();
}
inline VkPushConstantRange const* getPushConstantRanges() const {
return mInfo->pushConstantDescription.getVkRanges();
}
/**
* Returns true if parallel compilation is canceled, false if not. Parallel
* compilation will be canceled if this program is destroyed before relevant
* pipelines are created.
*
* @return true if parallel compilation should run for this program, false if not
*/
inline bool isParallelCompilationCanceled() const {
return mParallelCompilationCanceled.load(std::memory_order_acquire);
}
inline void writePushConstant(VkCommandBuffer cmdbuf, VkPipelineLayout layout,
backend::ShaderStage stage, uint8_t index, backend::PushConstantVariant const& value) {
// It's possible that we don't have the layout yet. When external samplers are used, bindPipeline()
// in VulkanDriver returns early, without binding a layout. If that happens, the layout is not
// set until draw time. Any push constants that are written during that time should be saved for
// later, and flushed when the layout is set.
if (layout != VK_NULL_HANDLE) {
mInfo->pushConstantDescription.write(cmdbuf, layout, stage, index, value);
} else {
mQueuedPushConstants.push_back({cmdbuf, stage, index, value});
}
}
// TODO: handle compute shaders.
// The expected order of shaders - from frontend to backend - is vertex, fragment, compute.
static constexpr uint8_t const MAX_SHADER_MODULES = 2;
private:
struct PipelineInfo {
explicit PipelineInfo(backend::Program const& program) noexcept
: pushConstantDescription(program)
{}
VkShaderModule shaders[MAX_SHADER_MODULES] = { VK_NULL_HANDLE };
PushConstantDescription pushConstantDescription;
};
struct PushConstantInfo {
VkCommandBuffer cmdbuf;
backend::ShaderStage stage;
uint8_t index;
backend::PushConstantVariant value;
};
PipelineInfo* mInfo;
VkDevice mDevice = VK_NULL_HANDLE;
std::atomic<bool> mParallelCompilationCanceled { false };
std::vector<PushConstantInfo> mQueuedPushConstants;
};
// Wrapper to enable use of shared_ptr for implementing shared ownership of low-level Vulkan fences. // Wrapper to enable use of shared_ptr for implementing shared ownership of low-level Vulkan fences.
struct VulkanCmdFence { struct VulkanCmdFence {
explicit VulkanCmdFence(VkFence fence) : mFence(fence) { } explicit VulkanCmdFence(VkFence fence) : mFence(fence) { }
@@ -204,21 +84,39 @@ struct VulkanFence : public HwFence, fvkmemory::ThreadSafeResource {
VulkanFence() {} VulkanFence() {}
void setFence(std::shared_ptr<VulkanCmdFence> fence) { void setFence(std::shared_ptr<VulkanCmdFence> fence) {
std::lock_guard const l(lock);
sharedFence = std::move(fence); sharedFence = std::move(fence);
cond.notify_all();
} }
std::pair<std::shared_ptr<VulkanCmdFence>, bool> getStatus() const { std::shared_ptr<VulkanCmdFence>& getSharedFence() {
std::lock_guard const l(lock);
return sharedFence;
}
std::pair<std::shared_ptr<VulkanCmdFence>, bool>
wait(std::chrono::steady_clock::time_point const until) {
// hold a reference so that our state doesn't disappear while we wait
std::unique_lock l(lock);
cond.wait_until(l, until, [this] {
return bool(sharedFence) || canceled;
});
// here mSharedFence will be null if we timed out
return { sharedFence, canceled }; return { sharedFence, canceled };
} }
void cancel() const { void cancel() const {
std::lock_guard const l(lock);
if (sharedFence) { if (sharedFence) {
sharedFence->cancel(); sharedFence->cancel();
} }
canceled = true; canceled = true;
cond.notify_all();
} }
private: private:
mutable std::mutex lock;
mutable std::condition_variable cond;
mutable bool canceled = false; mutable bool canceled = false;
std::shared_ptr<VulkanCmdFence> sharedFence; std::shared_ptr<VulkanCmdFence> sharedFence;
}; };
+3 -11
View File
@@ -34,11 +34,10 @@
#include <utils/Mutex.h> #include <utils/Mutex.h>
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <list> #include <list>
#include <type_traits>
#include <utility> #include <utility>
#include <variant>
namespace filament::backend { namespace filament::backend {
@@ -73,11 +72,7 @@ struct VulkanCommandBuffer {
~VulkanCommandBuffer(); ~VulkanCommandBuffer();
template <typename T, inline void acquire(fvkmemory::resource_ptr<fvkmemory::Resource> resource) {
typename = std::enable_if_t<
std::is_base_of_v<fvkmemory::Resource, T> ||
std::is_base_of_v<fvkmemory::ThreadSafeResource, T>>>
inline void acquire(fvkmemory::resource_ptr<T> resource) {
mResources.push_back(resource); mResources.push_back(resource);
} }
@@ -120,9 +115,6 @@ struct VulkanCommandBuffer {
} }
private: private:
using HeldResource = std::variant<fvkmemory::resource_ptr<Resource>,
fvkmemory::resource_ptr<ThreadSafeResource>>;
static uint32_t sAgeCounter; static uint32_t sAgeCounter;
VulkanContext const& mContext; VulkanContext const& mContext;
@@ -137,7 +129,7 @@ private:
fvkmemory::resource_ptr<VulkanSemaphore> mSubmission; fvkmemory::resource_ptr<VulkanSemaphore> mSubmission;
VkFence mFence; VkFence mFence;
std::shared_ptr<VulkanCmdFence> mFenceStatus; std::shared_ptr<VulkanCmdFence> mFenceStatus;
std::vector<HeldResource> mResources; std::vector<fvkmemory::resource_ptr<Resource>> mResources;
uint32_t mAge; uint32_t mAge;
}; };
+3 -28
View File
@@ -1008,9 +1008,7 @@ void VulkanDriver::createFenceR(Handle<HwFence> fh, utils::ImmutableCString&& ta
// it with appropriate VulkanCmdFence, which is associated with the current, recording command // it with appropriate VulkanCmdFence, which is associated with the current, recording command
// buffer. // buffer.
auto fence = resource_ptr<VulkanFence>::cast(&mResourceManager, fh); auto fence = resource_ptr<VulkanFence>::cast(&mResourceManager, fh);
signalFence([&] { fence->setFence(cmdbuf->getFenceStatus());
fence->setFence(cmdbuf->getFenceStatus());
});
mResourceManager.associateHandle(fh.getId(), std::move(tag)); mResourceManager.associateHandle(fh.getId(), std::move(tag));
} }
@@ -1461,9 +1459,7 @@ void VulkanDriver::fenceCancel(FenceHandle const fh) {
// Even though this is a synchronous call, the fence handle must be (and stay) valid // Even though this is a synchronous call, the fence handle must be (and stay) valid
assert_invariant(fh); assert_invariant(fh);
auto fence = resource_ptr<VulkanFence>::cast(&mResourceManager, fh); auto fence = resource_ptr<VulkanFence>::cast(&mResourceManager, fh);
signalFence([&] { fence->cancel();
fence->cancel();
});
} }
FenceStatus VulkanDriver::getFenceStatus(Handle<HwFence> const fh) { FenceStatus VulkanDriver::getFenceStatus(Handle<HwFence> const fh) {
@@ -1490,18 +1486,7 @@ FenceStatus VulkanDriver::fenceWait(FenceHandle const fh, uint64_t const timeout
until = now + nanoseconds(timeout); until = now + nanoseconds(timeout);
} }
std::shared_ptr<VulkanCmdFence> cmdfence; auto const [cmdfence, canceled] = fence->wait(until);
bool canceled = false;
waitForFence([&] {
auto status = fence->getStatus();
if (bool(status.first) || status.second) {
cmdfence = status.first;
canceled = status.second;
return true;
}
return false;
}, until);
if (!cmdfence || canceled) { if (!cmdfence || canceled) {
return canceled ? FenceStatus::ERROR : FenceStatus::TIMEOUT_EXPIRED; return canceled ? FenceStatus::ERROR : FenceStatus::TIMEOUT_EXPIRED;
} }
@@ -1563,16 +1548,6 @@ bool VulkanDriver::isTextureFormatMipmappable(TextureFormat format) {
} }
} }
bool VulkanDriver::isTextureFormatFilterable(TextureFormat format) {
VkFormat vkformat = fvkutils::getVkFormat(format);
if (vkformat == VK_FORMAT_UNDEFINED) {
return false;
}
VkFormatProperties info;
vkGetPhysicalDeviceFormatProperties(mPlatform->getPhysicalDevice(), vkformat, &info);
return (info.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0;
}
bool VulkanDriver::isRenderTargetFormatSupported(TextureFormat format) { bool VulkanDriver::isRenderTargetFormatSupported(TextureFormat format) {
VkFormat vkformat = fvkutils::getVkFormat(format); VkFormat vkformat = fvkutils::getVkFormat(format);
if (vkformat == VK_FORMAT_UNDEFINED) { if (vkformat == VK_FORMAT_UNDEFINED) {
@@ -16,6 +16,8 @@
#include "VulkanHandles.h" #include "VulkanHandles.h"
#include "VulkanConstants.h"
// TODO: remove this by moving DebugUtils out of VulkanDriver // TODO: remove this by moving DebugUtils out of VulkanDriver
#include "VulkanDriver.h" #include "VulkanDriver.h"
@@ -24,6 +26,7 @@
#include "vulkan/utils/Conversion.h" #include "vulkan/utils/Conversion.h"
#include "vulkan/utils/Definitions.h" #include "vulkan/utils/Definitions.h"
#include "vulkan/utils/Image.h" #include "vulkan/utils/Image.h"
#include "vulkan/utils/Spirv.h"
#include <backend/platforms/VulkanPlatform.h> #include <backend/platforms/VulkanPlatform.h>
@@ -83,6 +86,17 @@ inline void fromStageFlags(backend::ShaderStageFlags stage, descriptor_binding_t
} }
} }
inline VkShaderStageFlags getVkStage(backend::ShaderStage stage) {
switch(stage) {
case backend::ShaderStage::VERTEX:
return VK_SHADER_STAGE_VERTEX_BIT;
case backend::ShaderStage::FRAGMENT:
return VK_SHADER_STAGE_FRAGMENT_BIT;
case backend::ShaderStage::COMPUTE:
PANIC_POSTCONDITION("Unsupported stage");
}
}
using BitmaskGroup = VulkanDescriptorSetLayout::Bitmask; using BitmaskGroup = VulkanDescriptorSetLayout::Bitmask;
BitmaskGroup fromBackendLayout(DescriptorSetLayout const& layout) { BitmaskGroup fromBackendLayout(DescriptorSetLayout const& layout) {
BitmaskGroup mask; BitmaskGroup mask;
@@ -250,6 +264,139 @@ void VulkanDescriptorSet::addNewSet(VkDescriptorSet vkSet, OnRecycle&& onRecycle
mSets.push_back({ vkSet, std::move(onRecycleFn) }); mSets.push_back({ vkSet, std::move(onRecycleFn) });
} }
PushConstantDescription::PushConstantDescription(backend::Program const& program) {
mRangeCount = 0;
uint32_t offset = 0;
// The range is laid out so that the vertex constants are defined as the first set of bytes,
// followed by fragment and compute. This means we need to keep track of the offset for each
// stage. We do the bookeeping in mDescriptions.
for (auto stage: { ShaderStage::VERTEX, ShaderStage::FRAGMENT, ShaderStage::COMPUTE }) {
auto const& constants = program.getPushConstants(stage);
if (constants.empty()) {
continue;
}
auto& description = mDescriptions[(uint8_t) stage];
// We store the type of the constant for type-checking when writing.
description.types.reserve(constants.size());
std::for_each(constants.cbegin(), constants.cend(),
[&description](Program::PushConstant t) { description.types.push_back(t.type); });
uint32_t const constantsSize = (uint32_t) constants.size() * ENTRY_SIZE;
mRanges[mRangeCount++] = {
.stageFlags = getVkStage(stage),
.offset = offset,
.size = constantsSize,
};
description.offset = offset;
offset += constantsSize;
}
}
void PushConstantDescription::write(VkCommandBuffer cmdbuf, VkPipelineLayout layout,
backend::ShaderStage stage, uint8_t index, backend::PushConstantVariant const& value) {
uint32_t binaryValue = 0;
auto const& description = mDescriptions[(uint8_t) stage];
UTILS_UNUSED_IN_RELEASE auto const& types = description.types;
uint32_t const offset = description.offset;
if (std::holds_alternative<bool>(value)) {
assert_invariant(types[index] == ConstantType::BOOL);
bool const bval = std::get<bool>(value);
binaryValue = static_cast<uint32_t const>(bval ? VK_TRUE : VK_FALSE);
} else if (std::holds_alternative<float>(value)) {
assert_invariant(types[index] == ConstantType::FLOAT);
float const fval = std::get<float>(value);
binaryValue = *reinterpret_cast<uint32_t const*>(&fval);
} else {
assert_invariant(types[index] == ConstantType::INT);
int const ival = std::get<int>(value);
binaryValue = *reinterpret_cast<uint32_t const*>(&ival);
}
vkCmdPushConstants(cmdbuf, layout, getVkStage(stage), offset + index * ENTRY_SIZE, ENTRY_SIZE,
&binaryValue);
}
VulkanProgram::VulkanProgram(VkDevice device, Program const& builder) noexcept
: HwProgram(builder.getName()),
mInfo(new(std::nothrow) PipelineInfo(builder)),
mDevice(device) {
Program::ShaderSource const& blobs = builder.getShadersSource();
auto& modules = mInfo->shaders;
auto const& specializationConstants = builder.getSpecializationConstants();
std::vector<uint32_t> shader;
static_assert(static_cast<ShaderStage>(0) == ShaderStage::VERTEX &&
static_cast<ShaderStage>(1) == ShaderStage::FRAGMENT &&
MAX_SHADER_MODULES == 2);
for (size_t i = 0; i < MAX_SHADER_MODULES; i++) {
Program::ShaderBlob const& blob = blobs[i];
uint32_t* data = (uint32_t*) blob.data();
size_t dataSize = blob.size();
if (!specializationConstants.empty()) {
fvkutils::workaroundSpecConstant(blob, specializationConstants, shader);
data = (uint32_t*) shader.data();
dataSize = shader.size() * 4;
}
VkShaderModule& module = modules[i];
VkShaderModuleCreateInfo moduleInfo = {
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = dataSize,
.pCode = data,
};
VkResult result = vkCreateShaderModule(mDevice, &moduleInfo, VKALLOC, &module);
FILAMENT_CHECK_POSTCONDITION(result == VK_SUCCESS)
<< "Unable to create shader module."
<< " error=" << static_cast<int32_t>(result);
#if FVK_ENABLED(FVK_DEBUG_DEBUG_UTILS)
utils::CString name{ builder.getName().c_str(), builder.getName().size() };
switch (static_cast<ShaderStage>(i)) {
case ShaderStage::VERTEX:
name += "_vs";
break;
case ShaderStage::FRAGMENT:
name += "_fs";
break;
default:
PANIC_POSTCONDITION("Unexpected stage");
break;
}
VulkanDriver::DebugUtils::setName(VK_OBJECT_TYPE_SHADER_MODULE,
reinterpret_cast<uint64_t>(module), name.c_str());
#endif
}
#if FVK_ENABLED(FVK_DEBUG_SHADER_MODULE)
FVK_LOGD << "Created VulkanProgram " << builder << ", shaders = (" << modules[0]
<< ", " << modules[1] << ")";
#endif
}
VulkanProgram::~VulkanProgram() {
for (auto shader: mInfo->shaders) {
vkDestroyShaderModule(mDevice, shader, VKALLOC);
}
delete mInfo;
}
void VulkanProgram::flushPushConstants(VkPipelineLayout layout) {
// At this point, we really ought to have a VkPipelineLayout.
assert_invariant(layout != VK_NULL_HANDLE);
for (const auto& c : mQueuedPushConstants) {
mInfo->pushConstantDescription.write(c.cmdbuf, layout, c.stage, c.index, c.value);
}
mQueuedPushConstants.clear();
}
// Creates a special "default" render target (i.e. associated with the swap chain) // Creates a special "default" render target (i.e. associated with the swap chain)
VulkanRenderTarget::VulkanRenderTarget() VulkanRenderTarget::VulkanRenderTarget()
: HwRenderTarget(0, 0), : HwRenderTarget(0, 0),
+115
View File
@@ -30,6 +30,9 @@
#include "vulkan/memory/Resource.h" #include "vulkan/memory/Resource.h"
#include "vulkan/memory/ResourcePointer.h" #include "vulkan/memory/ResourcePointer.h"
#include "vulkan/utils/Definitions.h" #include "vulkan/utils/Definitions.h"
#include "vulkan/utils/StaticVector.h"
#include <backend/Program.h>
#include <utils/FixedCapacityVector.h> #include <utils/FixedCapacityVector.h>
#include <utils/Mutex.h> #include <utils/Mutex.h>
@@ -238,6 +241,118 @@ struct VulkanMemoryMappedBuffer : public HwMemoryMappedBuffer, Resource {
uint32_t offset = 0; uint32_t offset = 0;
}; };
using PushConstantNameArray = utils::FixedCapacityVector<char const*>;
using PushConstantNameByStage = std::array<PushConstantNameArray, Program::SHADER_TYPE_COUNT>;
struct PushConstantDescription {
explicit PushConstantDescription(backend::Program const& program);
VkPushConstantRange const* getVkRanges() const noexcept { return mRanges; }
uint32_t getVkRangeCount() const noexcept { return mRangeCount; }
void write(VkCommandBuffer cmdbuf, VkPipelineLayout layout, backend::ShaderStage stage,
uint8_t index, backend::PushConstantVariant const& value);
private:
static constexpr uint32_t ENTRY_SIZE = sizeof(uint32_t);
struct ConstantDescription {
utils::FixedCapacityVector<backend::ConstantType> types;
uint32_t offset = 0;
};
// Describes the constants in each shader stage.
ConstantDescription mDescriptions[Program::SHADER_TYPE_COUNT];
VkPushConstantRange mRanges[Program::SHADER_TYPE_COUNT];
uint32_t mRangeCount;
};
struct VulkanProgram : public HwProgram, fvkmemory::Resource {
using BindingList = fvkutils::StaticVector<uint16_t, MAX_SAMPLER_COUNT>;
VulkanProgram(VkDevice device, Program const& builder) noexcept;
~VulkanProgram();
/**
* Cancels any parallel compilation jobs that have not yet run for this
* program.
*/
inline void cancelParallelCompilation() {
mParallelCompilationCanceled.store(true, std::memory_order_release);
}
/**
* Writes out any queued push constants using the provided VkPipelineLayout.
*
* @param layout The layout that is to be used along with these push constants,
* in the next draw call.
*/
void flushPushConstants(VkPipelineLayout layout);
inline VkShaderModule getVertexShader() const {
return mInfo->shaders[0];
}
inline VkShaderModule getFragmentShader() const { return mInfo->shaders[1]; }
inline uint32_t getPushConstantRangeCount() const {
return mInfo->pushConstantDescription.getVkRangeCount();
}
inline VkPushConstantRange const* getPushConstantRanges() const {
return mInfo->pushConstantDescription.getVkRanges();
}
/**
* Returns true if parallel compilation is canceled, false if not. Parallel
* compilation will be canceled if this program is destroyed before relevant
* pipelines are created.
*
* @return true if parallel compilation should run for this program, false if not
*/
inline bool isParallelCompilationCanceled() const {
return mParallelCompilationCanceled.load(std::memory_order_acquire);
}
inline void writePushConstant(VkCommandBuffer cmdbuf, VkPipelineLayout layout,
backend::ShaderStage stage, uint8_t index, backend::PushConstantVariant const& value) {
// It's possible that we don't have the layout yet. When external samplers are used, bindPipeline()
// in VulkanDriver returns early, without binding a layout. If that happens, the layout is not
// set until draw time. Any push constants that are written during that time should be saved for
// later, and flushed when the layout is set.
if (layout != VK_NULL_HANDLE) {
mInfo->pushConstantDescription.write(cmdbuf, layout, stage, index, value);
} else {
mQueuedPushConstants.push_back({cmdbuf, stage, index, value});
}
}
// TODO: handle compute shaders.
// The expected order of shaders - from frontend to backend - is vertex, fragment, compute.
static constexpr uint8_t const MAX_SHADER_MODULES = 2;
private:
struct PipelineInfo {
explicit PipelineInfo(backend::Program const& program) noexcept
: pushConstantDescription(program)
{}
VkShaderModule shaders[MAX_SHADER_MODULES] = { VK_NULL_HANDLE };
PushConstantDescription pushConstantDescription;
};
struct PushConstantInfo {
VkCommandBuffer cmdbuf;
backend::ShaderStage stage;
uint8_t index;
backend::PushConstantVariant value;
};
PipelineInfo* mInfo;
VkDevice mDevice = VK_NULL_HANDLE;
std::atomic<bool> mParallelCompilationCanceled { false };
std::vector<PushConstantInfo> mQueuedPushConstants;
};
// The render target bundles together a set of attachments, each of which can have one of the // The render target bundles together a set of attachments, each of which can have one of the
// following ownership semantics: // following ownership semantics:
// //
@@ -223,8 +223,6 @@ void VulkanReadPixels::run(fvkmemory::resource_ptr<VulkanTexture> srcTexture, ui
bpp *= componentCount; bpp *= componentCount;
} }
uint32_t const samples = srcTexture->samples > 1 ? srcTexture->samples : 1;
// Use a VkBuffer as the staging area for readback. // Use a VkBuffer as the staging area for readback.
// Using a buffer instead of a linearly tiled VkImage unifies the readback path and avoids // Using a buffer instead of a linearly tiled VkImage unifies the readback path and avoids
// driver/validation layer issues since some implementations strictly prohibit linear tiling // driver/validation layer issues since some implementations strictly prohibit linear tiling
@@ -232,10 +230,9 @@ void VulkanReadPixels::run(fvkmemory::resource_ptr<VulkanTexture> srcTexture, ui
VkBuffer stagingBuffer = VK_NULL_HANDLE; VkBuffer stagingBuffer = VK_NULL_HANDLE;
VkMemoryRequirements memReqs; VkMemoryRequirements memReqs;
uint32_t const stagingSize = width * height * bpp * samples;
VkBufferCreateInfo bufferInfo = { VkBufferCreateInfo bufferInfo = {
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size = stagingSize, .size = width * height * bpp,
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
}; };
// TODO: we could use a staging buffer pool, but this is on another thread. We'd need to // TODO: we could use a staging buffer pool, but this is on another thread. We'd need to
@@ -374,9 +371,6 @@ void VulkanReadPixels::run(fvkmemory::resource_ptr<VulkanTexture> srcTexture, ui
uint8_t const* srcPixels; uint8_t const* srcPixels;
vkMapMemory(device, stagingMemory, 0, VK_WHOLE_SIZE, 0, (void**) &srcPixels); vkMapMemory(device, stagingMemory, 0, VK_WHOLE_SIZE, 0, (void**) &srcPixels);
// If MSAA, MoltenVK returns samples in planar layout (Sample 0 is the first width * height
// pixels). So we can simply ask DataReshaper to read width * height elements with standard
// row pitch!
int const rowPitch = width * bpp; int const rowPitch = width * bpp;
if (!DataReshaper::reshapeImage(&p, componentType, componentCount, srcPixels, rowPitch, if (!DataReshaper::reshapeImage(&p, componentType, componentCount, srcPixels, rowPitch,
static_cast<int>(width), static_cast<int>(height), swizzle)) { static_cast<int>(width), static_cast<int>(height), swizzle)) {
@@ -67,10 +67,7 @@ ResourceType getTypeEnum() noexcept;
std::string_view getTypeStr(ResourceType type); std::string_view getTypeStr(ResourceType type);
inline bool isThreadSafeType(ResourceType type) { inline bool isThreadSafeType(ResourceType type) {
return type == ResourceType::PROGRAM || return type == ResourceType::FENCE || type == ResourceType::TIMER_QUERY;
type == ResourceType::FENCE ||
type == ResourceType::TIMER_QUERY ||
type == ResourceType::SYNC;
} }
struct Resource { struct Resource {
@@ -49,11 +49,8 @@ private:
using AllocatorImpl = HandleAllocatorVK; using AllocatorImpl = HandleAllocatorVK;
template<typename D> template<typename D>
using requires_thread_safety = typename std::disjunction< using requires_thread_safety = typename std::disjunction<std::is_same<D, VulkanFence>,
std::is_same<D, VulkanProgram>, std::is_same<D, VulkanTimerQuery>>;
std::is_same<D, VulkanFence>,
std::is_same<D, VulkanTimerQuery>,
std::is_same<D, VulkanSync>>;
template<typename D, typename B, typename... ARGS> template<typename D, typename B, typename... ARGS>
inline D* construct(Handle<B> const& handle, ARGS&&... args) noexcept { inline D* construct(Handle<B> const& handle, ARGS&&... args) noexcept {
@@ -770,7 +770,6 @@ PixelDataType getComponentType(VkFormat format) {
case VK_FORMAT_D16_UNORM: return PixelDataType::USHORT; case VK_FORMAT_D16_UNORM: return PixelDataType::USHORT;
case VK_FORMAT_D32_SFLOAT: return PixelDataType::FLOAT; case VK_FORMAT_D32_SFLOAT: return PixelDataType::FLOAT;
case VK_FORMAT_X8_D24_UNORM_PACK32: return PixelDataType::UINT; case VK_FORMAT_X8_D24_UNORM_PACK32: return PixelDataType::UINT;
case VK_FORMAT_B10G11R11_UFLOAT_PACK32: return PixelDataType::UINT_10F_11F_11F_REV;
// For combined depth/stencil formats, we return the primary (depth) aspect type. // For combined depth/stencil formats, we return the primary (depth) aspect type.
// Stencil aspect overrides are handled dynamically during readback. // Stencil aspect overrides are handled dynamically during readback.
case VK_FORMAT_D24_UNORM_S8_UINT: return PixelDataType::UINT; case VK_FORMAT_D24_UNORM_S8_UINT: return PixelDataType::UINT;
@@ -849,8 +848,6 @@ uint32_t getComponentCount(VkFormat format) {
case VK_FORMAT_R32G32B32_UINT: case VK_FORMAT_R32G32B32_UINT:
case VK_FORMAT_R32G32B32_SINT: case VK_FORMAT_R32G32B32_SINT:
case VK_FORMAT_R32G32B32_SFLOAT: case VK_FORMAT_R32G32B32_SFLOAT:
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
return 3; return 3;
case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8G8B8A8_UNORM:
+7 -47
View File
@@ -702,9 +702,7 @@ void WebGPUDriver::createRenderTargetR(Handle<HwRenderTarget> renderTargetHandle
void WebGPUDriver::createFenceR(Handle<HwFence> fenceHandle, utils::ImmutableCString&& tag) { void WebGPUDriver::createFenceR(Handle<HwFence> fenceHandle, utils::ImmutableCString&& tag) {
// The handle is constructed synchronously in createFenceS. // The handle is constructed synchronously in createFenceS.
const auto fence = handleCast<WebGPUFence>(fenceHandle); const auto fence = handleCast<WebGPUFence>(fenceHandle);
signalFence([&] { fence->setSubmissionState(mQueueManager.getLatestSubmissionState());
fence->setSubmissionState(mQueueManager.getLatestSubmissionState());
});
setDebugTag(fenceHandle.getId(), std::move(tag)); setDebugTag(fenceHandle.getId(), std::move(tag));
} }
@@ -774,7 +772,11 @@ void WebGPUDriver::fenceCancel(FenceHandle fh) {
} }
FenceStatus WebGPUDriver::getFenceStatus(Handle<HwFence> fenceHandle) { FenceStatus WebGPUDriver::getFenceStatus(Handle<HwFence> fenceHandle) {
return fenceWait(fenceHandle, 0); const auto fence = handleCast<WebGPUFence>(fenceHandle);
if (!fence) {
return FenceStatus::ERROR;
}
return fence->getStatus();
} }
FenceStatus WebGPUDriver::fenceWait(FenceHandle fenceHandle, uint64_t const timeout) { FenceStatus WebGPUDriver::fenceWait(FenceHandle fenceHandle, uint64_t const timeout) {
@@ -782,38 +784,7 @@ FenceStatus WebGPUDriver::fenceWait(FenceHandle fenceHandle, uint64_t const time
if (!fence) { if (!fence) {
return FenceStatus::ERROR; return FenceStatus::ERROR;
} }
return fence->wait(timeout);
using namespace std::chrono;
auto now = steady_clock::now();
steady_clock::time_point until = steady_clock::time_point::max();
using TimeoutType = decltype(timeout);
constexpr TimeoutType maxTimeout = std::numeric_limits<TimeoutType>::max();
constexpr nanoseconds maxNano = nanoseconds::max();
if (timeout < maxNano.count() && timeout < maxTimeout && // Need to account for overflow
now <= steady_clock::time_point::max() - nanoseconds(timeout)) {
until = now + nanoseconds(timeout);
}
std::shared_ptr<WebGPUSubmissionState> state;
bool const success = waitForFence([&] {
state = fence->getState();
return bool(state);
}, until);
if (!success) {
return FenceStatus::TIMEOUT_EXPIRED;
}
auto duration_ns = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(steady_clock::now() - now)
.count());
// In the unlikely event that duration_ns is too close to timeout, we just assume the leftover
// timeout is 0.
if (timeout == 0 || duration_ns > timeout) {
duration_ns = timeout;
}
return state->waitForCompletion(timeout - duration_ns);
} }
void WebGPUDriver::destroySync(Handle<HwSync> syncHandle) { void WebGPUDriver::destroySync(Handle<HwSync> syncHandle) {
@@ -851,17 +822,6 @@ bool WebGPUDriver::isTextureFormatMipmappable(const TextureFormat format) {
return WebGPUTexture::supportsMultipleMipLevelsViaStorageBinding(webGpuFormat); return WebGPUTexture::supportsMultipleMipLevelsViaStorageBinding(webGpuFormat);
} }
bool WebGPUDriver::isTextureFormatFilterable(TextureFormat format) {
if (isFp32ColorFormat(format)) {
return mDevice.HasFeature(wgpu::FeatureName::Float32Filterable);
}
if (isUnsignedIntFormat(format) || isSignedIntFormat(format) ||
isDepthFormat(format) || isStencilFormat(format)) {
return false;
}
return true;
}
bool WebGPUDriver::isRenderTargetFormatSupported(const TextureFormat format) { bool WebGPUDriver::isRenderTargetFormatSupported(const TextureFormat format) {
//todo //todo
return true; return true;
@@ -23,6 +23,49 @@ namespace filament::backend {
WebGPUFence::WebGPUFence() = default; WebGPUFence::WebGPUFence() = default;
WebGPUFence::~WebGPUFence() = default; WebGPUFence::~WebGPUFence() = default;
void WebGPUFence::setSubmissionState(std::shared_ptr<WebGPUSubmissionState> state) {
std::lock_guard<std::mutex> const lock(mLock);
mState = state;
mCond.notify_one();
}
FenceStatus WebGPUFence::getStatus() {
return wait(0);
}
FenceStatus WebGPUFence::wait(uint64_t timeout) {
// we have to take into account that the STL's wait_for() actually works with
// time_points relative to steady_clock::now() internally.
using namespace std::chrono;
auto now = steady_clock::now();
steady_clock::time_point until = steady_clock::time_point::max();
using TimeoutType = decltype(timeout);
constexpr TimeoutType maxTimeout = std::numeric_limits<TimeoutType>::max();
constexpr nanoseconds maxNano = nanoseconds::max();
if (timeout < maxNano.count() && timeout < maxTimeout && // Need to account for overflow
now <= steady_clock::time_point::max() - nanoseconds(timeout)) {
until = now + nanoseconds(timeout);
}
{
std::unique_lock<std::mutex> lock(mLock);
bool const success = mCond.wait_until(lock, until, [this] {
return bool(mState);
});
if (!success) {
return FenceStatus::TIMEOUT_EXPIRED;
}
}
auto duration_ns = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(steady_clock::now() - now)
.count());
// In the unlikely event that duration_ns is too close to timeout, we just assume the leftover
// timeout is 0.
if (timeout == 0 || duration_ns > timeout) {
duration_ns = timeout;
}
return mState->waitForCompletion(timeout - duration_ns);
}
} // namespace filament::backend } // namespace filament::backend
+6 -6
View File
@@ -33,15 +33,15 @@ public:
~WebGPUFence(); ~WebGPUFence();
// Initialize the fence with a submission state from the queue manager. // Initialize the fence with a submission state from the queue manager.
void setSubmissionState(std::shared_ptr<WebGPUSubmissionState> state) { void setSubmissionState(std::shared_ptr<WebGPUSubmissionState> state);
mState = state;
}
std::shared_ptr<WebGPUSubmissionState> getState() const { // Note that getStatus cannot be const since it's implemented with wait();
return mState; FenceStatus getStatus();
} FenceStatus wait(uint64_t timeout);
private: private:
std::mutex mLock;
std::condition_variable mCond;
std::shared_ptr<WebGPUSubmissionState> mState; std::shared_ptr<WebGPUSubmissionState> mState;
}; };
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

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