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 .
cd build/android && printf "y" | ./build.sh presubmit-with-archive arm64-v8a
popd
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- name: Check artifact sizes
run: |
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 \
--target-branch origin/main \
--threshold 20480 \
--artifacts filament-android-release.aar/jni/arm64-v8a/libfilament-jni.so \
$BYPASS_ARG
--artifacts filament-android-release.aar/jni/arm64-v8a/libfilament-jni.so
build-ios:
name: build-iOS
+1 -1
View File
@@ -5,7 +5,7 @@
To build Filament, you must first install the following tools:
- 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)
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
if (APPLE AND NOT IOS)
# 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()
# ==================================================================================================
@@ -47,6 +47,8 @@ option(FILAMENT_ENABLE_TSAN "Enable Thread Sanitizer" 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_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
- 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 {
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:
```shell
pod 'Filament', '~> 1.71.2'
pod 'Filament', '~> 1.70.1'
```
## 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
[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
@@ -220,23 +220,3 @@ Java_com_google_android_filament_ColorGrading_nBuilderCurves(JNIEnv* env, jclass
env->ReleaseFloatArrayElements(midPoint_, midPoint, 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/EntityManager.h>
#include <utils/tribool.h>
#include <filament/Material.h>
#include <filament/View.h>
#include "common/CallbackUtils.h"
using namespace filament;
using namespace utils;
@@ -435,24 +429,6 @@ Java_com_google_android_filament_Engine_nIsPaused(JNIEnv*, jclass,
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
Java_com_google_android_filament_Engine_nSetPaused(JNIEnv*, jclass,
jlong nativeEngine, jboolean paused) {
@@ -20,8 +20,6 @@
#include <filament/Texture.h>
#include <filament/TextureSampler.h>
#include "common/CallbackUtils.h"
#include <math/mat3.h>
#include <math/mat4.h>
#include <math/vec2.h>
@@ -248,69 +246,6 @@ Java_com_google_android_filament_MaterialInstance_nSetFloatParameterArray(JNIEnv
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
namespace filament::JniUtils {
TextureSampler from_long(jlong params) noexcept;
@@ -645,17 +580,3 @@ Java_com_google_android_filament_MaterialInstance_nGetTransparencyMode(JNIEnv*,
MaterialInstance* instance = (MaterialInstance*) nativeMaterialInstance;
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;
}
/**
* 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.
*
@@ -636,7 +620,6 @@ public class ColorGrading {
private static native void nBuilderVibrance(long nativeBuilder, float vibrance);
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 nBuilderCustomLut(long nativeBuilder, java.nio.Buffer buffer, int dim);
private static native long nBuilderBuild(long nativeBuilder, long nativeEngine);
}
@@ -205,15 +205,6 @@ public class Engine {
REALTIME,
};
/**
* Three-state feature state.
*/
public enum FeatureState {
FALSE,
TRUE,
INDETERMINATE
}
/**
* Constructs <code>Engine</code> objects using a builder pattern.
*/
@@ -627,35 +618,6 @@ public class Engine {
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.
* <p>
@@ -1569,7 +1531,6 @@ public class Engine {
private static native void nDestroyEntity(long nativeEngine, int entity);
private static native boolean nFlushAndWait(long nativeEngine, long timeout);
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 void nSetPaused(long nativeEngine, boolean paused);
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
* should be turned off to avoid unnecessary material compilations.
*</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 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 MaterialInstance#compile
*/
public void compile(@NonNull CompilerPriorityQueue priority,
int variants,
@@ -18,7 +18,6 @@ package com.google.android.filament;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Size;
import com.google.android.filament.proguard.UsedByNative;
@@ -143,28 +142,6 @@ public class MaterialInstance {
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 */
@NonNull
public String getName() {
@@ -425,69 +402,6 @@ public class MaterialInstance {
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.
*
@@ -1023,17 +937,6 @@ public class MaterialInstance {
@NonNull String name, int element, @NonNull @Size(min = 1) float[] v,
@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,
@NonNull String name, long nativeTexture, long sampler);
@@ -1097,5 +1000,4 @@ public class MaterialInstance {
private static native int nGetDepthFunc(long nativeMaterialInstance);
private static native void nSetTransparencyMode(long nativeMaterialInstance, int mode);
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;
/**
* 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
* shadow map will double the memory usage of all shadow maps.
* This may not be supported on all mobile devices.
*/
public boolean highPrecision = false;
/**
@@ -30,8 +30,8 @@ using namespace filament;
namespace {
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_DRIVER_NAME,
backend::Platform::DeviceInfoType::VULKAN_DRIVER_INFO,
};
@@ -143,7 +143,7 @@ class ModelViewer(
light = EntityManager.get().create()
val (r, g, b) = Colors.cct(6_500.0f)
LightManager.Builder(LightManager.Type.SUN)
LightManager.Builder(LightManager.Type.DIRECTIONAL)
.color(r, g, b)
.intensity(100_000.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,
* typically comes from {@link android.view.Choreographer.FrameCallback}
*/
fun render(frameTimeNanos: Long): Boolean {
fun render(frameTimeNanos: Long) {
if (!uiHelper.isReadyToRender) {
return false
return
}
// 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.
var rendered = false
if (renderer.beginFrame(swapChain!!, frameTimeNanos)) {
rendered = true
renderer.render(view)
debugFrameCallback?.let {
@@ -362,7 +360,6 @@ class ModelViewer(
renderer.endFrame()
}
return rendered
}
/*
@@ -393,45 +390,38 @@ class ModelViewer(
view.addOnAttachStateChangeListener(object : android.view.View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(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.
*/
+1 -1
View File
@@ -1,5 +1,5 @@
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.
@@ -16,15 +16,14 @@ filament {
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")
}
// don't forget to update MainActivity.kt when/if changing this.
tasks.register('copyModels', Copy) {
// don't forget to update MainACtivity.kt when/if changing this.
tasks.register('copyDamagedHelmetGltf', Copy) {
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")
rename {String fileName -> "helmet.glb"}
}
preBuild.dependsOn copyModels
preBuild.dependsOn copyDamagedHelmetGltf
clean.doFirst {
delete "src/main/assets/envs"
@@ -1,13 +1,10 @@
{
"name": "Default Test",
"backends": [
"opengl",
"vulkan"
"opengl"
],
"models": {
"DamagedHelmet": "DamagedHelmet.glb",
"Fox": "Fox.glb",
"TransmissionRoughnessTest": "TransmissionRoughnessTest.glb"
"DamagedHelmet": "helmet.glb"
},
"presets": [
{
@@ -22,7 +19,7 @@
"center": [
0,
0,
-0.8
0
],
"lookAt": [
0,
@@ -36,24 +33,8 @@
}
},
"tolerance": {
"mode": "OR",
"maxFailingPixelsFraction": 0.0005,
"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
}
]
"maxAbsDiff": 0.1,
"maxFailingPixelsFraction": 0.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": [
@@ -128,32 +72,64 @@
]
},
{
"name": "msaa_custom",
"name": "ssao",
"apply_presets": [
"base",
"noisy_tolerance"
"base"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.customResolve": true
"view.ssao.enabled": true
}
},
{
"name": "msaa_standard",
"name": "msaa",
"apply_presets": [
"base",
"noisy_tolerance"
"base"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.customResolve": false
"view.msaa.enabled": true
}
},
{
"name": "bloom",
"apply_presets": [
"base",
"tilted"
],
"rendering": {
"view.bloom.enabled": true
}
},
{
"name": "aa_none",
"apply_presets": [
"base"
],
"rendering": {
"view.antiAliasing": "NONE"
}
},
{
"name": "aa_fxaa",
"apply_presets": [
"base"
],
"rendering": {
"view.antiAliasing": "FXAA"
}
},
{
"name": "dithering_none",
"apply_presets": [
"base"
],
"rendering": {
"view.dithering": "NONE"
}
},
{
"name": "msaa_8",
"apply_presets": [
"base",
"noisy_tolerance"
"base"
],
"rendering": {
"view.msaa.enabled": true,
@@ -161,13 +137,14 @@
}
},
{
"name": "fxaa",
"name": "taa_custom",
"apply_presets": [
"base",
"noisy_tolerance"
"base"
],
"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": [
"base"
],
@@ -200,11 +227,30 @@
"view.ssao.resolution": 0.5
}
},
{
"name": "ssao_resolution_full",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.resolution": 1.0
}
},
{
"name": "ssao_intensity_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.intensity": 2.0
}
},
{
"name": "ssao_bent_normals",
"apply_presets": [
"base",
"relaxed_tolerance"
"base"
],
"rendering": {
"view.ssao.enabled": true,
@@ -252,20 +298,98 @@
}
},
{
"name": "ssr",
"name": "ssr_basic",
"apply_presets": [
"base",
"relaxed_tolerance"
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true
}
},
{
"name": "ssr_thickness_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.5
}
},
{
"name": "ssr_bias_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.bias": 0.1
}
},
{
"name": "ssr_maxDistance_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.maxDistance": 5.0
}
},
{
"name": "ssr_stride_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.stride": 4.0
}
},
{
"name": "ssr_stride_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.stride": 1.0
}
},
{
"name": "ssr_thickness_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.01
}
},
{
"name": "ssr_maxDistance_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.maxDistance": 1.0
}
},
{
"name": "ssr_bias_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.bias": 0.001
}
},
{
"name": "ssr_quality_combo",
"apply_presets": [
"base",
"relaxed_tolerance"
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
@@ -274,20 +398,89 @@
}
},
{
"name": "bloom",
"name": "bloom_levels_high",
"apply_presets": [
"base",
"noisy_tolerance"
"base"
],
"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",
"apply_presets": [
"base",
"noisy_tolerance"
"base"
],
"rendering": {
"view.bloom.enabled": true,
@@ -297,8 +490,7 @@
{
"name": "bloom_lensflare_no_starburst",
"apply_presets": [
"base",
"noisy_tolerance"
"base"
],
"rendering": {
"view.bloom.enabled": true,
@@ -309,8 +501,7 @@
{
"name": "bloom_lensflare_chromatic",
"apply_presets": [
"base",
"noisy_tolerance"
"base"
],
"rendering": {
"view.bloom.enabled": true,
@@ -319,29 +510,51 @@
}
},
{
"name": "bloom_quality_high",
"name": "bloom_lensflare_ghosts",
"apply_presets": [
"base",
"noisy_tolerance"
"base"
],
"rendering": {
"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": [
"base",
"noisy_tolerance"
"base"
],
"rendering": {
"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": [
"base"
],
@@ -350,13 +563,43 @@
}
},
{
"name": "dof_native_res",
"name": "dof_cocScale_high",
"apply_presets": [
"base"
],
"rendering": {
"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": [
"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": [
"base"
],
@@ -387,6 +763,46 @@
"view.vignette.enabled": true
}
},
{
"name": "vignette_midPoint",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.midPoint": 0.8
}
},
{
"name": "vignette_roundness_circle",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.roundness": 1.0
}
},
{
"name": "vignette_roundness_rect",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.roundness": 0.0
}
},
{
"name": "vignette_feather_sharp",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.feather": 0.1
}
},
{
"name": "cg_filmic",
"apply_presets": [
@@ -423,6 +839,33 @@
"view.colorGrading.toneMapping": "PBR_NEUTRAL"
}
},
{
"name": "cg_contrast_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.contrast": 1.5
}
},
{
"name": "cg_saturation_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.saturation": 1.5
}
},
{
"name": "cg_exposure_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.exposure": 1.0
}
},
{
"name": "shadow_vsm",
"apply_presets": [
@@ -445,169 +888,12 @@
{
"name": "shadow_vsm_highPrecision",
"apply_presets": [
"base",
"relaxed_tolerance"
"base"
],
"rendering": {
"view.shadowType": "VSM",
"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.Color
import android.os.Bundle
import android.os.FileObserver
import android.text.Html
import android.util.Log
import android.view.Choreographer
@@ -31,10 +30,8 @@ import android.view.View
import android.view.WindowManager
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.Spinner
import android.widget.TextView
import com.google.android.filament.utils.KTX1Loader
@@ -62,29 +59,22 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private lateinit var surfaceView: SurfaceView
private lateinit var choreographer: Choreographer
private lateinit var modelViewer: ModelViewer
private lateinit var statusTextView: 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 inputManager: ValidationInputManager
private var currentInput: ValidationInputManager.ValidationInput? = null
private var fileObserver: FileObserver? = null
private var currentAlphaDiffBitmap: Bitmap? = null
private var globalEnhancementFactor: Float = 1.0f
private var modelViewer: ModelViewer? = null
private data class TestImages(
val testName: String,
val golden: File?,
val rendered: File?,
val diff: File?,
val alphaDiff: File?
val golden: Bitmap?,
val rendered: Bitmap?,
val diff: Bitmap?,
val alphaDiff: Bitmap?
)
private val diffImageViews = mutableListOf<ImageView>()
@@ -94,8 +84,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private lateinit var loadButton: Button
private lateinit var optionsButton: Button
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 enhancementSlider: android.widget.SeekBar
@@ -106,10 +94,8 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private val frameScheduler = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
choreographer.postFrameCallback(this)
val rendered = modelViewer?.render(frameTimeNanos) ?: false
if (rendered) {
validationRunner?.onFrame(frameTimeNanos)
}
modelViewer.render(frameTimeNanos)
validationRunner?.onFrame(frameTimeNanos)
}
}
@@ -123,28 +109,12 @@ class MainActivity : Activity(), ValidationRunner.Callback {
statusTextView = findViewById(R.id.status_text)
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)
scrollView.viewTreeObserver.addOnScrollChangedListener {
updateTrianglePosition()
}
scrollView.viewTreeObserver.addOnGlobalLayoutListener {
updateTrianglePosition()
}
runButton = findViewById(R.id.run_button)
loadButton = findViewById(R.id.load_button)
optionsButton = findViewById(R.id.options_button)
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)
enhancementSlider = findViewById(R.id.enhancement_slider)
@@ -158,7 +128,13 @@ class MainActivity : Activity(), ValidationRunner.Callback {
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
loadButton.setOnClickListener {
@@ -168,22 +144,27 @@ class MainActivity : Activity(), ValidationRunner.Callback {
// Setup Options Menu Button
optionsButton.setOnClickListener { view ->
val popup = android.widget.PopupMenu(this, view)
popup.menu.add(0, 1, 0, "Generate Golden")
popup.menu.add(0, 2, 0, "Export Test")
popup.menu.add(0, 3, 0, "Export Result")
popup.menu.add(0, 4, 0, "Test ADB Info")
popup.menu.add(0, 5, 0, "Result ADB Info")
popup.menu.add(0, 6, 0, "Toggle Enhancement Slider")
popup.menu.add(0, 7, 0, "Toggle Backend Filter")
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
1 -> {
currentInput?.let { input ->
val goldenInput = input.copy(generateGoldens = true)
startValidation(goldenInput)
}
}
2 -> exportTestBundleAction()
3 -> exportTestResultsAction()
4 -> showTestAdbInfo()
5 -> showResultAdbInfo()
6 -> {
enhancementContainer.visibility =
if (enhancementContainer.visibility == View.VISIBLE) View.GONE else View.VISIBLE
}
7 -> {
backendFilterContainer.visibility =
if (backendFilterContainer.visibility == View.VISIBLE) View.GONE else View.VISIBLE
enhancementContainer.visibility = if (enhancementContainer.visibility == View.VISIBLE) View.GONE else View.VISIBLE
}
}
true
@@ -194,14 +175,17 @@ class MainActivity : Activity(), ValidationRunner.Callback {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
choreographer = Choreographer.getInstance()
modelViewer = ModelViewer(surfaceView=surfaceView, manipulator=null)
inputManager = ValidationInputManager(this)
setupFileObserver()
// Initialize IBL
createIndirectLight()
handleIntent()
}
private fun showLoadDialog() {
val exportDir = inputManager.getBaseDir()
val exportDir = getExternalFilesDir(null) ?: filesDir
// Filter out result zips (starting with "results_") to only show test bundles
val zips = exportDir.listFiles { _, name ->
name.endsWith(".zip") && !name.startsWith("results_")
@@ -232,7 +216,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
}
private fun showTestAdbInfo() {
val exportDir = inputManager.getBaseDir()
val exportDir = getExternalFilesDir(null) ?: filesDir
val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder()
@@ -248,11 +232,12 @@ class MainActivity : Activity(), ValidationRunner.Callback {
message.append("<b>--- PUSH TO DEVICE ---</b><br>")
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 {
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)
.setTitle("Test Bundle ADB Info")
@@ -262,7 +247,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
}
private fun showResultAdbInfo() {
val exportDir = inputManager.getBaseDir()
val exportDir = getExternalFilesDir(null) ?: filesDir
val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder()
@@ -298,14 +283,29 @@ class MainActivity : Activity(), ValidationRunner.Callback {
statusTextView.text = "Loading ${file.name}..."
CoroutineScope(Dispatchers.Main).launch {
try {
val config = inputManager.loadFromZip(file)
val baseDir = getExternalFilesDir(null) ?: filesDir
val outputDir = File(baseDir, "validation_results").apply { mkdirs() }
// Clear existing results UI and state
resultsContainer.removeAllViews()
diffImageViews.clear()
resultManager = null
val input = inputManager.loadValidationInputFromZip(file)
onInputUpdated(input)
Log.i(TAG, "Setting header to: Test Results: ${input!!.config!!.name}")
val newInput = ValidationInputManager.ValidationInput(
config = config,
outputDir = outputDir,
generateGoldens = false,
autoRun = false,
autoExport = false,
autoExportResults = false,
sourceZip = file
)
currentInput = newInput
statusTextView.text = "Loaded ${config.name}"
Log.i(TAG, "Setting header to: Test Results: ${config.name}")
testResultsHeader.text = "${config.name}"
} catch (e: Exception) {
Log.e(TAG, "Failed to load zip", e)
statusTextView.text = "Error: ${e.message}"
@@ -315,112 +315,52 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private fun createIndirectLight() {
try {
modelViewer?.let { mv ->
val engine = mv.engine
val scene = mv.scene
val iblName = "default_env"
val engine = modelViewer.engine
val scene = modelViewer.scene
val iblName = "default_env"
fun readAsset(path: String): ByteBuffer {
val input = assets.open(path)
val bytes = input.readBytes()
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")
fun readAsset(path: String): ByteBuffer {
val input = assets.open(path)
val bytes = input.readBytes()
return ByteBuffer.wrap(bytes)
}
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) {
Log.e(TAG, "Failed to load IBL", e)
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() {
statusTextView.text = "Resolving configuration..."
CoroutineScope(Dispatchers.Main).launch {
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) {
Log.e(TAG, "Failed to resolve config", e)
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 {
val gpuDriverInfo = com.google.android.filament.utils.DeviceUtils.getGpuDriverInfo(modelViewer.engine)
return ValidationResultManager(
outputDir = outputDir,
gpuDriverInfo = gpuDriverInfo,
deviceName = android.os.Build.MODEL,
deviceHardware = android.os.Build.HARDWARE,
deviceCodeName = android.os.Build.DEVICE,
androidVersion = android.os.Build.VERSION.RELEASE,
androidBuildNumber = android.os.Build.DISPLAY
)
}
private fun clearUIState() {
resultsContainer.removeAllViews()
diffImageViews.clear()
resultManager = null
}
private fun startValidation(input: ValidationInputManager.ValidationInput) {
try {
val config = input.config ?: return
clearUIState();
// Disable UI while running
resultsContainer.removeAllViews()
diffImageViews.clear()
enhancementSlider.isEnabled = false
runButton.isEnabled = false
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, "Starting validation with config: ${input.config.name}")
Log.i(TAG, "Output dir: ${input.outputDir.absolutePath}")
testProgress.visibility = View.VISIBLE
progressContainer.visibility = View.VISIBLE
testSummaryText.visibility = View.GONE
deviceInfoText.visibility = View.VISIBLE
totalPassed = 0
totalFailed = 0
testProgress.reset(1)
testResultsHeader.text = "${input.config.name}"
resultManager = createResultManager(input.outputDir)
val backendFilter = when (backendRadioGroup.checkedRadioButtonId) {
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 = ValidationRunner(this, modelViewer, input.config, resultManager!!)
validationRunner?.callback = this
validationRunner?.generateGoldens = input.generateGoldens
validationRunner?.testFilter = input.testFilter
validationRunner?.start()
} 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() {
super.onResume()
choreographer.postFrameCallback(frameScheduler)
@@ -552,54 +422,14 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onDestroy() {
super.onDestroy()
choreographer.removeFrameCallback(frameScheduler)
fileObserver?.stopWatching()
validationRunner?.let {
it.cleanup()
modelViewer = null
}
}
private var currentRenderedBitmap: Bitmap? = null
private var currentGoldenBitmap: 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) {
runOnUiThread {
if (result.passed) totalPassed++ else totalFailed++
testProgress.addResult(result.passed)
val status = "Test ${result.testName} finished: ${if(result.passed) "PASS" else "FAIL"}"
statusTextView.text = status
Log.i(TAG, status)
@@ -638,49 +468,44 @@ class MainActivity : Activity(), ValidationRunner.Callback {
val imagesRow = LinearLayout(this)
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(
testName = result.testName,
golden = goldenFile?.takeIf { currentGoldenBitmap != null },
rendered = renderedFile.takeIf { currentRenderedBitmap != null },
diff = diffFile.takeIf { currentDiffBitmap != null },
alphaDiff = alphaDiffFile.takeIf { currentAlphaDiffBitmap != null }
golden = currentGoldenBitmap,
rendered = currentRenderedBitmap,
diff = currentDiffBitmap,
alphaDiff = currentAlphaDiffBitmap
)
fun addImage(label: String, bitmap: Bitmap?, isDiff: Boolean) {
if (bitmap == null) {
return;
if (bitmap != null) {
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)
@@ -694,7 +519,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
resultContainer.addView(imagesRow)
resultsContainer.addView(resultContainer)
resultsContainer.post { updateTrianglePosition() }
// Clear current images for next test
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() {
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!"
// Re-enable UI
enhancementSlider.isEnabled = true
runButton.isEnabled = true
loadButton.isEnabled = true
optionsButton.isEnabled = true
backendRadioGroup.isEnabled = true
for (i in 0 until backendRadioGroup.childCount) {
backendRadioGroup.getChildAt(i).isEnabled = true
Log.i(TAG, "All tests finished " + if (currentInput?.autoExport == true) "Exporting bundle" else "x")
if (currentInput?.autoExport == true) {
exportTestBundleAction()
}
Log.i(TAG, "All tests finished")
currentInput?.let {
if (it.generateGoldens) {
exportTestBundleAction(it)
} else {
exportTestResultsAction(it)
}
if (currentInput?.autoExportResults == true) {
exportTestResultsAction()
}
}
}
private fun exportTestBundleAction(input: ValidationInputManager.ValidationInput) {
val config = input.config ?: return
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: createResultManager(input.outputDir)
val bundleNameOverride = if (input.sourceZip == null) "default_test" else null
val zip = rm.exportTestBundle(config, timestamp, bundleNameOverride)
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 {
private fun exportTestBundleAction() {
currentInput?.let { input ->
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: createResultManager(input.outputDir)
val zip = rm.exportTestBundle(input.config, timestamp)
if (zip != null) {
val msg = "Exported Bundle: ${zip.name}"
statusTextView.text = msg
Log.i(TAG, "Exported test bundle to ${zip.absolutePath}")
}
} else {
statusTextView.text = "Export Bundle failed"
Log.e(TAG, "Export Bundle failed")
} else {
statusTextView.text = "Export Bundle failed"
Log.e(TAG, "Export Bundle failed")
}
}
}
private fun exportTestResultsAction(input: ValidationInputManager.ValidationInput) {
if (input.config == null) {
return
}
val config = input.config!!
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: createResultManager(input.outputDir)
val zip = rm.exportTestResults(input.sourceZip, config, timestamp)
if (zip != null) {
val msg = "Exported Results: ${zip.name}"
statusTextView.text = msg
Log.i(TAG, "Exported results to ${zip.absolutePath}")
} else {
statusTextView.text = "Export Results failed"
Log.e(TAG, "Export Results failed")
private fun exportTestResultsAction() {
currentInput?.let { input ->
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: createResultManager(input.outputDir)
val zip = rm.exportTestResults(input.sourceZip, timestamp)
if (zip != null) {
val msg = "Exported Results: ${zip.name}"
statusTextView.text = msg
Log.i(TAG, "Exported results to ${zip.absolutePath}")
} else {
statusTextView.text = "Export Results failed"
Log.e(TAG, "Export Results failed")
}
}
}
@@ -804,24 +582,20 @@ class MainActivity : Activity(), ValidationRunner.Callback {
}
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 {
// Update the "live" views
when (type) {
"Rendered" -> {
currentRenderedBitmap = scaledBitmap
currentRenderedBitmap = bitmap
}
"Golden" -> {
currentGoldenBitmap = scaledBitmap
currentGoldenBitmap = bitmap
}
"Diff" -> {
currentDiffBitmap = scaledBitmap
currentDiffBitmap = bitmap
}
"Alpha Diff" -> {
currentAlphaDiffBitmap = scaledBitmap
currentAlphaDiffBitmap = bitmap
}
}
}
@@ -859,18 +633,17 @@ class MainActivity : Activity(), ValidationRunner.Callback {
titleView.text = images.testName
val availableFiles = mutableListOf<Pair<String, File>>()
images.rendered?.takeIf { it.exists() }?.let { availableFiles.add(Pair("Rendered", it)) }
images.golden?.takeIf { it.exists() }?.let { availableFiles.add(Pair("Golden", it)) }
images.diff?.takeIf { it.exists() }?.let { availableFiles.add(Pair("Diff", it)) }
images.alphaDiff?.takeIf { it.exists() }?.let { availableFiles.add(Pair("Alpha Diff", it)) }
val availableImages = mutableListOf<Pair<String, Bitmap>>()
images.rendered?.let { availableImages.add(Pair("Rendered", it)) }
images.golden?.let { availableImages.add(Pair("Golden", it)) }
images.diff?.let { availableImages.add(Pair("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
var currentDialogBitmap: Bitmap? = null
var currentDialogEnhancement = globalEnhancementFactor
val matrix = android.graphics.Matrix()
@@ -879,11 +652,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
var lastTouchY = 0f
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() {
override fun onScale(detector: android.view.ScaleGestureDetector): Boolean {
matrix.postScale(detector.scaleFactor, detector.scaleFactor, detector.focusX, detector.focusY)
@@ -918,13 +686,9 @@ class MainActivity : Activity(), ValidationRunner.Callback {
}
fun updateView() {
val (label, file) = availableFiles[currentIndex]
val (label, bitmap) = availableImages[currentIndex]
typeView.text = label
val oldBitmap = currentDialogBitmap
currentDialogBitmap = android.graphics.BitmapFactory.decodeFile(file.absolutePath)
imageView.setImageBitmap(currentDialogBitmap)
imageView.setImageBitmap(bitmap)
(imageView.drawable as? android.graphics.drawable.BitmapDrawable)?.setAntiAlias(false)
(imageView.drawable as? android.graphics.drawable.BitmapDrawable)?.setFilterBitmap(false)
imageView.imageMatrix = matrix
@@ -936,18 +700,14 @@ class MainActivity : Activity(), ValidationRunner.Callback {
enhancementContainer.visibility = View.GONE
imageView.colorFilter = null
}
oldBitmap?.recycle()
}
fun resetMatrix() {
val drawable = imageView.drawable ?: return
val width = imageView.width.toFloat()
val height = imageView.height.toFloat()
if (width <= 0f || height <= 0f) return
val dw = drawable.intrinsicWidth.toFloat()
val dh = drawable.intrinsicHeight.toFloat()
if (dw <= 0f || dh <= 0f) return
val scaleX = width / dw
val scaleY = height / dh
@@ -962,33 +722,14 @@ class MainActivity : Activity(), ValidationRunner.Callback {
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() }
btnReset.setOnClickListener {
matrixResettable = true
resetMatrix()
}
btnReset.setOnClickListener { resetMatrix() }
btnPrev.setOnClickListener {
currentIndex = (currentIndex - 1 + availableFiles.size) % availableFiles.size
matrixResettable = false
currentIndex = (currentIndex - 1 + availableImages.size) % availableImages.size
updateView()
}
btnNext.setOnClickListener {
currentIndex = (currentIndex + 1) % availableFiles.size
matrixResettable = false
currentIndex = (currentIndex + 1) % availableImages.size
updateView()
}
@@ -1001,13 +742,16 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onProgressChanged(seekBar: android.widget.SeekBar?, progress: Int, fromUser: Boolean) {
currentDialogEnhancement = 1.0f + (progress / 100f) * 49.0f
enhancementLabel.text = String.format(Locale.US, "Enhance: %.1fx", currentDialogEnhancement)
matrixResettable = false
updateView()
}
override fun onStartTrackingTouch(seekBar: android.widget.SeekBar?) {}
override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) {}
})
imageView.post {
resetMatrix()
}
updateView()
dialog.show()
}
@@ -31,7 +31,7 @@ data class RenderTestConfig(
data class TestConfig(
val name: String,
val description: String?,
val backend: String,
val backends: List<String>,
val models: Set<String>,
val rendering: JSONObject,
val tolerance: JSONObject?
@@ -102,7 +102,7 @@ class ConfigParser {
val testsJson = json.getJSONArray("tests")
val tests = mutableListOf<TestConfig>()
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)
@@ -125,7 +125,7 @@ class ConfigParser {
existingModels: Set<String>,
presets: Map<String, PresetConfig>,
defaultBackends: List<String>
): List<TestConfig> {
): TestConfig {
val name = json.getString("name")
val description = json.optString("description")
val backends = json.optJSONArray("backends")?.toList<String>() ?: defaultBackends
@@ -165,9 +165,7 @@ class ConfigParser {
val tolerance = json.optJSONObject("tolerance") ?: lastTolerance
return backends.map { backend ->
TestConfig(name, description, backend, combinedModels, rendering, tolerance)
}
return TestConfig(name, description, backends, 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 java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.zip.ZipFile
/**
* Handles the retrieval and preparation of test configuration and assets.
* Supports loading from:
* 1. Intent extras (local path or URL)
* 2. Default embedded assets (fallback)
*/
class ValidationInputManager(private val context: Context) {
companion object {
@@ -33,54 +41,35 @@ class ValidationInputManager(private val context: Context) {
}
data class ValidationInput(
val config: RenderTestConfig?,
val config: RenderTestConfig,
val outputDir: File,
val generateGoldens: Boolean,
val autoRun: Boolean = false,
val sourceZip: File? = null,
val testFilter: String? = null
val autoExport: Boolean = false,
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.
* This may involve extracting assets or downloading files.
*/
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 generateGoldens = intent.getBooleanExtra("generate_goldens", false)
// If we get a generateGoldens signal, then it should trigger a run
val autoRun = intent.getBooleanExtra("auto_run", false) ||
intent.getBooleanExtra("generate_goldens", false)
val testFilter = if (autoRun) intent.getStringExtra("test_filter") else null
val autoRun = intent.getBooleanExtra("auto_run", false)
val autoExport = intent.getBooleanExtra("auto_export", false)
val autoExportResults = intent.getBooleanExtra("auto_export_results", false)
val outputPath = intent.getStringExtra("output_path")
Log.i(TAG, "Resolving config with outputPath: $outputPath")
val baseDir = getBaseDir()
val baseDir = context.getExternalFilesDir(null) ?: context.filesDir
Log.i(TAG, "Base directory for resolution: ${baseDir.absolutePath}")
val outputDir = if (!outputPath.isNullOrBlank()) {
val file = File(outputPath)
val resolved = if (file.isAbsolute) {
@@ -88,7 +77,7 @@ class ValidationInputManager(private val context: Context) {
} else {
File(baseDir, outputPath)
}
// Critical check: if resolved is root or very short, it's likely wrong
if (resolved.absolutePath == "/" || resolved.parent == null) {
Log.w(TAG, "Resolved outputDir is root ($resolved), defaulting to app-specific dir")
@@ -99,115 +88,76 @@ class ValidationInputManager(private val context: Context) {
} else {
File(baseDir, "validation_results")
}
if (!outputDir.exists() && !outputDir.mkdirs()) {
Log.e(TAG, "Failed to create outputDir: ${outputDir.absolutePath}")
}
Log.i(TAG, "Final outputDir: ${outputDir.absolutePath}")
var sourceZipFile = if (zipPath != null) {
val sourceZipFile = if (zipPath != null) {
val file = File(zipPath)
val resolvedFile = if (file.isAbsolute) {
if (file.isAbsolute) {
file
} else {
File(baseDir, zipPath)
}
Log.i(TAG, "Resolved zipPath '$zipPath' to ${resolvedFile.absolutePath} (exists: ${resolvedFile.exists()})")
resolvedFile
} else {
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 {
sourceZipFile != null && sourceZipFile.exists() -> loadFromZip(sourceZipFile)
generateGoldens -> extractDefaultAssets()
else -> null
urlConfig != null -> downloadConfig(urlConfig, urlModelsBase)
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
private var lastUnzippedTime: Long = 0
suspend fun loadValidationInputFromZip(file: File) : ValidationInput {
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 {
suspend fun loadFromZip(zipFile: File): RenderTestConfig {
Log.i(TAG, "Unzipping validation bundle: ${zipFile.absolutePath}")
// Use a unique cache dir based on timestamp or just overwrite a common one
// Overwriting is safer to avoid filling up disk, but we must ensure we don't conflict with current run
val baseCacheDir = context.externalCacheDir ?: context.cacheDir
val cacheDir = File(baseCacheDir, "unzipped_validation")
if (cacheDir.exists()) cacheDir.deleteRecursively()
cacheDir.mkdirs()
if (lastUnzippedFile == zipFile.absolutePath && lastUnzippedTime == zipFile.lastModified() &&
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}")
Log.i(TAG, "Using cacheDir: ${cacheDir.absolutePath}")
ZipFile(zipFile).use { zip ->
val entries = zip.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
val entryFile = File(cacheDir, entry.name)
// specific check to avoid zip slip vulnerability (though low risk here)
if (!entryFile.canonicalPath.startsWith(cacheDir.canonicalPath)) {
throw SecurityException("Zip entry is outside of the target dir: ${entry.name}")
}
if (entry.isDirectory) {
entryFile.mkdirs()
} else {
entryFile.parentFile?.mkdirs()
zip.getInputStream(entry).use { input ->
FileOutputStream(entryFile).use { output ->
input.copyTo(output)
}
}
ZipFile(zipFile).use { zip ->
val entries = zip.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
val entryFile = File(cacheDir, entry.name)
// specific check to avoid zip slip vulnerability (though low risk here)
if (!entryFile.canonicalPath.startsWith(cacheDir.canonicalPath)) {
throw SecurityException("Zip entry is outside of the target dir: ${entry.name}")
}
if (entry.isDirectory) {
entryFile.mkdirs()
} else {
entryFile.parentFile?.mkdirs()
zip.getInputStream(entry).use { input ->
FileOutputStream(entryFile).use { output ->
input.copyTo(output)
}
}
}
}
lastUnzippedFile = zipFile.absolutePath
lastUnzippedTime = zipFile.lastModified()
}
// Find config.json
// We look for a file ending in .json within the unzipped structure
// Exclude results.json if it happened to be there
val jsonFiles = cacheDir.walkTopDown()
.filter { it.isFile && it.extension == "json" && it.name != "results.json" }
.toList()
if (jsonFiles.isEmpty()) throw IllegalStateException("No config.json found in zip")
// Prefer one named config.json or take the first one
val configFile = jsonFiles.find { it.name == "config.json" } ?: jsonFiles.first()
@@ -215,7 +165,7 @@ class ValidationInputManager(private val context: Context) {
return ConfigParser.parseFromPath(configFile.absolutePath)
}
suspend fun extractDefaultAssets(): RenderTestConfig {
private suspend fun extractDefaultAssets(): RenderTestConfig {
Log.i(TAG, "Extracting default assets...")
val filesDir = context.getExternalFilesDir(null) ?: context.filesDir
val assetManager = context.assets
@@ -231,39 +181,84 @@ class ValidationInputManager(private val context: Context) {
}
}
// Copy models
// Copy DamagedHelmet.glb
val modelsDir = File(filesDir, "models")
modelsDir.mkdirs()
assetManager.list("models")?.forEach { modelFileName ->
val modelOut = File(modelsDir, modelFileName)
assetManager.open("models/$modelFileName").use { input ->
FileOutputStream(modelOut).use { output ->
input.copyTo(output)
}
val modelOut = File(modelsDir, "helmet.glb")
assetManager.open("models/helmet.glb").use { input ->
FileOutputStream(modelOut).use { output ->
input.copyTo(output)
}
}
// Update config to point to relative path (standardizing on relative for portability where possible)
// or absolute. Here we use relative as per previous logic.
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")
// Update all model paths to point to the extracted files in the models directory
val keys = models.keys()
val newModels = JSONObject()
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)
// Ensure the default model points to the extracted file
// We can use absolute path to be safe since we know where it is now.
models.put("DamagedHelmet", modelOut.absolutePath)
configOut.writeText(configJson.toString(2))
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(
val testName: String,
val passed: Boolean,
val diffMetric: Float = 0f,
val goldenPath: String? = null
val diffMetric: Float = 0f
)
class ValidationResultManager(
private val outputDir: File,
private val gpuDriverInfo: String,
private val deviceName: String,
private val deviceHardware: String,
private val deviceCodeName: String,
private val androidVersion: String,
private val androidBuildNumber: String
@@ -45,8 +44,7 @@ class ValidationResultManager(
private const val TAG = "ValidationResultManager"
}
private val results = java.util.concurrent.CopyOnWriteArrayList<ValidationResult>()
private val gpuDriverInfos = JSONObject()
private val results = mutableListOf<ValidationResult>()
init {
if (!outputDir.exists()) {
@@ -54,10 +52,6 @@ class ValidationResultManager(
}
}
fun addGpuDriverInfo(backend: String, info: String) {
gpuDriverInfos.put(backend, info)
}
fun addResult(result: ValidationResult) {
results.add(result)
}
@@ -89,19 +83,18 @@ class ValidationResultManager(
* - input test bundle (as nested zip), if provided
* - 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
val parentDir = outputDir.canonicalFile.parentFile ?: outputDir.parentFile
if (parentDir == null) return null
val resultZipName = "results_$timestamp"
val tempZipFile = File(parentDir, "$resultZipName.zip.tmp")
val finalZipFile = File(parentDir, "$resultZipName.zip")
val zipFile = File(parentDir, "$resultZipName.zip")
Log.i(TAG, "Exporting results to ${tempZipFile.absolutePath}")
Log.i(TAG, "Exporting results to ${zipFile.absolutePath}")
try {
ZipOutputStream(FileOutputStream(tempZipFile)).use { zos ->
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
// 1. Add results.json
val resultsJson = File(outputDir, "results.json")
if (resultsJson.exists()) {
@@ -110,28 +103,24 @@ class ValidationResultManager(
zos.closeEntry()
}
// 2. Add test bundle zip (fallback to sourceZip)
// 2. Add source zip if exists
if (sourceZip != null && sourceZip.exists()) {
zos.putNextEntry(ZipEntry("bundle.zip"))
zos.putNextEntry(ZipEntry(sourceZip.name))
sourceZip.inputStream().use { it.copyTo(zos) }
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 ->
zos.putNextEntry(ZipEntry(imgFile.name))
imgFile.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
tempZipFile.renameTo(finalZipFile)
Log.i(TAG, "Exported results to ${finalZipFile.absolutePath}")
return finalZipFile
Log.i(TAG, "Exported results to ${zipFile.absolutePath}")
return zipFile
} catch (e: Exception) {
Log.e(TAG, "Failed to export results", e)
tempZipFile.delete()
return null
}
}
@@ -150,7 +139,7 @@ class ValidationResultManager(
* goldens/
* 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, "OutputDir: ${outputDir.absolutePath}")
@@ -161,7 +150,8 @@ class ValidationResultManager(
}
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")
@@ -209,7 +199,7 @@ class ValidationResultManager(
// 3. Create modified config JSON
Log.i(TAG, "Creating config.json...")
val newConfigJson = JSONObject()
newConfigJson.put("name", config.name) // Keep spaces in JSON name
newConfigJson.put("name", testNameWithTimestamp) // Keep spaces in JSON name
// Reconstruct backends
val backendsArray = JSONArray()
@@ -245,7 +235,7 @@ class ValidationResultManager(
// Backends (optional override)
val testBackends = JSONArray()
testBackends.put(test.backend)
test.backends.forEach { testBackends.put(it) }
testJson.put("backends", testBackends)
testsArray.put(testJson)
@@ -256,11 +246,10 @@ class ValidationResultManager(
File(rootDir, "config.json").writeText(newConfigJson.toString(4))
// 4. Zip it
val tempZipFile = File(parentDir, "$exportNameNoSpaces.zip.tmp")
val finalZipFile = File(parentDir, "$exportNameNoSpaces.zip")
Log.i(TAG, "Zipping to ${tempZipFile.absolutePath}...")
val zipFile = File(parentDir, "$exportNameNoSpaces.zip")
Log.i(TAG, "Zipping to ${zipFile.absolutePath}...")
ZipOutputStream(FileOutputStream(tempZipFile)).use { zos ->
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
rootDir.walkTopDown().forEach { file ->
if (file.isFile) {
val entryName = file.relativeTo(exportDir).path
@@ -271,18 +260,15 @@ class ValidationResultManager(
}
}
tempZipFile.renameTo(finalZipFile)
// Cleanup temp dir
exportDir.deleteRecursively()
Log.i(TAG, "Exported test bundle to ${finalZipFile.absolutePath}")
return finalZipFile
Log.i(TAG, "Exported test bundle to ${zipFile.absolutePath}")
return zipFile
} catch (e: Exception) {
Log.e(TAG, "Failed to export test bundle", e)
exportDir.deleteRecursively()
File(parentDir, "$exportNameNoSpaces.zip.tmp").delete()
return null
}
}
@@ -291,10 +277,9 @@ class ValidationResultManager(
val rootObject = JSONObject()
val metadataObject = JSONObject()
metadataObject.put("gpu_driver_info", gpuDriverInfos)
metadataObject.put("gpu_driver_info", gpuDriverInfo ?: "")
metadataObject.put("total_time_ms", totalTimeMs)
metadataObject.put("device_name", deviceName ?: "")
metadataObject.put("device_hardware", deviceHardware ?: "")
metadataObject.put("device_code_name", deviceCodeName ?: "")
metadataObject.put("android_version", androidVersion ?: "")
metadataObject.put("android_build_number", androidBuildNumber ?: "")
@@ -29,10 +29,9 @@ import java.nio.ByteBuffer
class ValidationRunner(
private val context: Context,
private val surfaceView: android.view.SurfaceView,
private val modelViewer: ModelViewer,
private val config: RenderTestConfig,
private val resultManager: ValidationResultManager,
private val backendFilter: String = "both" // "gles", "vulkan", or "both"
private val resultManager: ValidationResultManager
) {
private var currentState = State.IDLE
@@ -41,47 +40,9 @@ class ValidationRunner(
private var currentEngine: AutomationEngine? = null
private var currentTestConfig: TestConfig? = null
private var currentModelName: String? = null
private var currentBackend: String? = null
private var frameCounter = 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 {
IDLE,
@@ -91,80 +52,42 @@ class ValidationRunner(
}
interface Callback {
fun onTestProgress(current: Int, total: Int)
fun onTestFinished(result: ValidationResult)
fun onAllTestsFinished()
fun onStatusChanged(status: String)
fun onImageResult(type: String, bitmap: Bitmap)
fun onModelViewerRecreated(modelViewer: ModelViewer?)
}
var callback: Callback? = null
var generateGoldens: Boolean = false
var testFilter: String? = null
fun start() {
if (sortedTests.isEmpty()) {
if (config.tests.isEmpty()) {
callback?.onAllTestsFinished()
return
}
suiteStartTime = System.currentTimeMillis()
currentBackend = null
currentTestIndex = 0
runTest(sortedTests[0])
currentModelIndex = 0
startTest(config.tests[0])
}
private fun runTest(testPair: Pair<TestConfig, String>) {
callback?.onTestProgress(currentTestIndex, sortedTests.size)
val (test, modelName) = testPair
private fun startTest(test: TestConfig) {
currentTestConfig = test
if (currentBackend != test.backend) {
currentBackend = test.backend
callback?.onStatusChanged("Switching to backend: ${test.backend}")
recreateEngine(test.backend)
// After recreation, we need to reload the model
currentModelName = null
if (test.models.isEmpty()) {
nextTest()
return
}
startModel(modelName)
}
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
currentModelIndex = 0
startModel(test.models.elementAt(0))
}
private fun startModel(modelName: String) {
if (currentModelName == 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
currentState = State.WARMUP
@@ -175,10 +98,10 @@ class ValidationRunner(
val modelPath = config.models[modelName]
if (modelPath == null) {
Log.e("ValidationRunner", "Model $modelName not found")
nextTest()
nextModel()
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)
loadModel(modelPath)
@@ -186,21 +109,21 @@ class ValidationRunner(
private fun loadModel(path: String) {
// Assume called on Main Thread
modelViewer?.destroyModel()
modelViewer.destroyModel()
try {
Log.i("ValidationRunner", "Reading model file: $path")
val bytes = File(path).readBytes()
Log.i("ValidationRunner", "Loading GLB buffer... (${bytes.size} bytes)")
val buffer = ByteBuffer.wrap(bytes)
modelViewer?.loadModelGlb(buffer)
modelViewer.loadModelGlb(buffer)
Log.i("ValidationRunner", "Model loaded.")
modelViewer?.transformToUnitCube()
modelViewer.transformToUnitCube()
currentState = State.WAITING_FOR_RESOURCES
frameCounter = 0
Log.i("ValidationRunner", "State set to WAITING_FOR_RESOURCES")
} catch (e: Exception) {
Log.e("ValidationRunner", "Failed to load $path", e)
nextTest()
nextModel()
}
}
@@ -208,72 +131,43 @@ class ValidationRunner(
when (currentState) {
State.IDLE -> {}
State.WAITING_FOR_RESOURCES -> {
modelViewer?.let { mv ->
val progress = mv.progress
if (progress >= 1.0f) {
Log.i("ValidationRunner", "Resources loaded. Starting warmup.")
frameCounter = 0
currentState = State.WARMUP
}
}
val progress = modelViewer.progress
if (progress >= 1.0f) {
Log.i("ValidationRunner", "Resources loaded. Starting warmup.")
frameCounter = 0
currentState = State.WARMUP
}
}
State.WARMUP -> {
frameCounter++
if (frameCounter > 1) { // 1 frames warmup
if (frameCounter > 5) { // 5 frames warmup
startAutomation()
}
}
State.RUNNING_TEST -> {
currentEngine?.let { engine ->
modelViewer?.let { mv ->
val content = AutomationEngine.ViewerContent()
content.view = mv.view
content.renderer = mv.renderer
content.scene = mv.scene
content.lightManager = mv.engine.lightManager
// Tick
val deltaTime = 1.0f / 60.0f
engine.tick(mv.engine, content, deltaTime)
val content = AutomationEngine.ViewerContent()
content.view = modelViewer.view
content.renderer = modelViewer.renderer
content.scene = modelViewer.scene
content.lightManager = modelViewer.engine.lightManager
applyAnimation(mv, currentTestConfig!!.rendering)
// Tick
val deltaTime = 1.0f / 60.0f
engine.tick(modelViewer.engine, content, deltaTime)
frameCounter++
if (engine.shouldClose()) {
Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)")
// Test finished (for this spec)
currentState = State.IDLE
captureAndCompare()
}
frameCounter++
if (engine.shouldClose()) {
Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)")
// Test finished (for this spec)
currentState = State.IDLE
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() {
val test = currentTestConfig!!
val specJson = JSONObject()
@@ -284,27 +178,21 @@ class ValidationRunner(
currentEngine = AutomationEngine(fullSpec)
val options = AutomationEngine.Options()
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)
// Use batch mode to ensure shouldClose() works reliably
currentEngine?.startBatchMode()
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
currentState = State.RUNNING_TEST
}
private fun captureAndCompare() {
callback?.onStatusChanged("Comparing ${currentTestConfig?.name} (${currentTestConfig?.backend})...")
modelViewer?.debugGetNextFrameCallback { bitmap ->
callback?.onStatusChanged("Comparing ${currentTestConfig?.name}...")
modelViewer.debugGetNextFrameCallback { bitmap ->
compareCapturedImage(bitmap)
}
}
@@ -312,7 +200,7 @@ class ValidationRunner(
private fun compareCapturedImage(bitmap: Bitmap) {
val testName = currentTestConfig!!.name
val modelName = currentModelName!!
val backend = currentTestConfig?.backend ?: "opengl"
val backend = currentTestConfig?.backends?.firstOrNull() ?: "opengl"
val testFullName = "${testName}.${backend}.${modelName}"
// Golden path
@@ -344,6 +232,8 @@ class ValidationRunner(
Log.i("ValidationRunner", "Found golden at ${goldenFile.absolutePath}")
} else {
Log.w("ValidationRunner", "Golden not found for $testFullName. Searched in: ${searchPaths.joinToString { it.absolutePath }}")
// Fallback to old behavior for reference if everything else fails
goldenFile = modelParent.parentFile?.resolve("golden/${testFullName}.png") ?: File("nonexistent")
}
Thread {
@@ -422,12 +312,9 @@ class ValidationRunner(
alphaDiffImg.setPixels(alphaPixels, 0, width, 0, 0, width, height)
callback?.onImageResult("Alpha Diff", alphaDiffImg)
resultManager.saveImage("${testFullName}_alpha_diff", alphaDiffImg)
alphaDiffImg.recycle()
}
diffImg.recycle()
}
}
golden.recycle()
} else {
callback?.onStatusChanged("Failed to load golden")
}
@@ -440,27 +327,35 @@ class ValidationRunner(
// Save output
resultManager.saveImage(testFullName, flipped)
val result = ValidationResult(testFullName, passed, diffMetric, goldenFile?.absolutePath)
val result = ValidationResult(testFullName, passed, diffMetric)
resultManager.addResult(result)
callback?.onTestFinished(result)
android.os.Handler(android.os.Looper.getMainLooper()).post {
nextTest()
nextModel()
}
} catch (e: Throwable) {
} catch (e: Exception) {
Log.e("ValidationRunner", "Comparison failed", e)
// Recycle even on error
bitmap.recycle()
android.os.Handler(android.os.Looper.getMainLooper()).post { nextTest() }
android.os.Handler(android.os.Looper.getMainLooper()).post { nextModel() }
}
}.start()
}
private fun nextModel() {
currentModelIndex++
if (currentTestConfig != null && currentModelIndex < currentTestConfig!!.models.size) {
startModel(currentTestConfig!!.models.elementAt(currentModelIndex))
} else {
nextTest()
}
}
private fun nextTest() {
currentTestIndex++
if (currentTestIndex < sortedTests.size) {
runTest(sortedTests[currentTestIndex])
if (currentTestIndex < config.tests.size) {
startTest(config.tests[currentTestIndex])
} else {
currentState = State.IDLE
@@ -27,8 +27,6 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:clipChildren="false"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingTop="20dp"
@@ -84,53 +82,6 @@
android:layout_marginStart="8dp"/>
</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) -->
<LinearLayout
android:id="@+id/enhancement_container"
@@ -163,57 +114,15 @@
android:id="@+id/test_results_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="Test Results"
android:gravity="center"
android:textSize="14sp"
android:paddingTop="0dp"
android:paddingBottom="12dp" />
<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" />
android:paddingBottom="8dp" />
</LinearLayout>
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/controls_container"
@@ -4,6 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#FF202020"
android:padding="8dp">
<!-- Header with title and close button -->
@@ -19,14 +20,14 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Test Result"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:textStyle="bold" />
<ImageButton
android:id="@+id/btn_close"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="?attr/colorOnSurface"
android:src="@android:drawable/ic_menu_close_clear_cancel" />
</LinearLayout>
@@ -37,8 +38,8 @@
android:layout_height="wrap_content"
android:gravity="start"
android:text="Rendered"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:attr/textColorSecondary"
android:textColor="#BBBBBB"
android:textSize="14sp"
android:paddingBottom="8dp" />
<!-- Image Area with Arrows -->
@@ -77,7 +78,7 @@
android:layout_height="64dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_media_previous"
app:tint="?attr/colorOnSurface" />
app:tint="#FFFFFF" />
<ImageButton
android:id="@+id/btn_reset"
@@ -86,7 +87,7 @@
android:layout_marginHorizontal="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_revert"
app:tint="?attr/colorOnSurface" />
app:tint="#FFFFFF" />
<ImageButton
android:id="@+id/btn_next"
@@ -94,7 +95,7 @@
android:layout_height="64dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_media_next"
app:tint="?attr/colorOnSurface" />
app:tint="#FFFFFF" />
</LinearLayout>
<!-- Enhancement Controls (only for diff images) -->
@@ -113,7 +114,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Enhance: 1.0x"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="#FFFFFF"
android:textSize="12sp"
android:minWidth="100dp" />
<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"
},
"node_modules/brace-expansion": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -234,9 +235,9 @@
"license": "MIT"
},
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -409,9 +410,9 @@
"integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="
},
"brace-expansion": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -428,9 +429,9 @@
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="
},
"minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"requires": {
"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
**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
**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
**clearCoat** | Strength 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
**roughness** | float | [0..1] |
**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
**sheenRoughness** | float | [0..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
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
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>
<ul>
<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>
</ul>
<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 {
implementation 'com.google.android.filament:filament-android:1.71.0'
implementation 'com.google.android.filament:filament-android:1.70.1'
}
</code></pre>
<p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p>
@@ -195,7 +195,7 @@ dependencies {
</div>
<h3 id="ios"><a class="header" href="#ios">iOS</a></h3>
<p>iOS projects can use CocoaPods to install the latest release:</p>
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.71.0'
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.70.1'
</code></pre>
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
<ul>
+2 -2
View File
@@ -319,7 +319,7 @@ the run number is <code>18023632663</code>.</p>
<i class="fa fa-angle-left"></i>
</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>
</a>
@@ -333,7 +333,7 @@ the run number is <code>18023632663</code>.</p>
<i class="fa fa-angle-left"></i>
</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>
</a>
</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>
<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>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>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>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"><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>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>
</tbody></table>
</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/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/specularfactor" class="level3"><span class="tocNumber">3.1.8&nbsp; </span>Specular factor</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/sheencolor" class="level3"><span class="tocNumber">3.1.10&nbsp; </span>Sheen color</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/clearcoat" class="level3"><span class="tocNumber">3.1.12&nbsp; </span>Clear coat</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/anisotropy" class="level3"><span class="tocNumber">3.1.14&nbsp; </span>Anisotropy</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/ambientocclusion" class="level3"><span class="tocNumber">3.1.16&nbsp; </span>Ambient occlusion</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/bentnormal" class="level3"><span class="tocNumber">3.1.18&nbsp; </span>Bent normal</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/emissive" class="level3"><span class="tocNumber">3.1.20&nbsp; </span>Emissive</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/indexofrefraction" class="level3"><span class="tocNumber">3.1.22&nbsp; </span>Index of refraction</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/absorption" class="level3"><span class="tocNumber">3.1.24&nbsp; </span>Absorption</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;&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/sheenroughness" class="level3"><span class="tocNumber">3.1.9&nbsp; </span>Sheen roughness</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/clearcoatroughness" class="level3"><span class="tocNumber">3.1.11&nbsp; </span>Clear coat roughness</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/anisotropydirection" class="level3"><span class="tocNumber">3.1.13&nbsp; </span>Anisotropy direction</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/normal" class="level3"><span class="tocNumber">3.1.15&nbsp; </span>Normal</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/clearcoatnormal" class="level3"><span class="tocNumber">3.1.17&nbsp; </span>Clear coat 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/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/indexofrefraction" class="level3"><span class="tocNumber">3.1.20&nbsp; </span>Index of refraction</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/absorption" class="level3"><span class="tocNumber">3.1.22&nbsp; </span>Absorption</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/dispersion" class="level3"><span class="tocNumber">3.1.24&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;&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/>
@@ -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">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">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">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>
@@ -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">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">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">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>
@@ -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
it easy to define from a texture.
</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.
</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>
<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>
<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 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
@@ -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
model described in section <a href="#toc3.3">3.3</a>.</div>
</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>The <code>sheenRoughness</code> property is similar to the <code>roughness</code> property but applies only to the
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
(left) to 1.0 (right)</span></center></div></center>
</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>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,
@@ -672,7 +652,7 @@ The effect of <code>clearCoat</code> on a rough metal is shown in <a href="#figu
</p><p>
<div class="admonition note">The clear coat layer is added on top of the sheen layer if present.</div>
</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>The <code>clearCoatRoughness</code> property is similar to the <code>roughness</code> property but applies only to the
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
(left) to 1.0 (right)</span></center></div></center>
</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>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
@@ -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
not assign a value (even 0.0) to the <code>anisotropy</code> property if you don't need anisotropy.</div>
</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>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
@@ -726,7 +706,7 @@ in <a href="#figure_anisotropydirectionproperty">figure&nbsp;16</a>.
</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>
</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>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
@@ -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
(left) and with (right)</span></center></div></center>
</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>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
@@ -754,7 +734,7 @@ and with (right)</span></center></div></center>
</p><p>
<div class="admonition warning">Using a normal map increases the runtime cost of the material model.</div>
</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>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
@@ -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
versions use the same ambient occlusion map.</span></center></div></center>
</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>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>
@@ -778,7 +758,7 @@ map and a surface normal map</span></center></div></center>
</p><p>
<div class="admonition warning">Using a clear coat normal map increases the runtime cost of the material model.</div>
</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>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
@@ -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
the camera exposure like with any regular light.
</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>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
@@ -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
<code>postLightingBlending</code> to <code>add</code> and by providing an RGB color with alpha set to <code>0.0</code>.</div>
</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>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
@@ -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
glass, certain plastics, etc.</div>
</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>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
@@ -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
materials.</div>
</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>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
@@ -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
possible. Filament provides an API for this purpose, <code>Color::absorptionAtDistance()</code>.
</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>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>
@@ -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
bottom of the prism</span></center></div></center>
</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>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>.
+2 -2
View File
@@ -165,7 +165,7 @@
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- 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>
</a>
@@ -179,7 +179,7 @@
</div>
<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>
</a>
+2 -2
View File
@@ -174,7 +174,7 @@
<script src="../../web/lib/filament.js"></script>
<script src="../../web/lib/gl-matrix-min.js"></script>
<script>
Filament.init(['../../web/assets/cube_fl0/nonlit.filamat'], () => {
Filament.init(['../../web/assets/cube_fl0/nonlit_fl0.filamat'], () => {
window.VertexAttribute = Filament.VertexAttribute;
window.AttributeType = Filament.VertexBuffer$AttributeType;
window.Projection = Filament.Camera$Projection;
@@ -257,7 +257,7 @@
.bufferType(Filament.IndexBuffer$IndexType.USHORT)
.build(engine);
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();
Filament.RenderableManager.Builder(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/main/filament.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": {
"dest": "dup/test_ci_backend.md"
},
"test/sizeguard/README.md": {
"dest": "dup/test_ci_sizeguard.md"
},
"filament/backend/test/README.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
**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
**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
**clearCoat** | Strength 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
**roughness** | float | [0..1] |
**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
**sheenRoughness** | float | [0..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
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
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)
- [sky](./samples/web/sky.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)
- [Metal](./notes/metal_debugging.md)
- [Vulkan](./notes/vulkan_debugging.md)
- [SPIR-V](./notes/spirv_debugging.md)
- [Running with ASAN and UBSAN](./notes/asan_ubsan.md)
- [Using Instruments on macOS](./notes/instruments.md)
- [Code coverage analysis](./notes/coverage.md)
- [Performance analysis](./notes/performance_analysis.md)
- [ASAN and UBSAN](./notes/asan_ubsan.md)
- [Instruments on macOS](./notes/instruments.md)
- [Code coverage](./notes/coverage.md)
- [Performance](./notes/performance_analysis.md)
- [Documentation](./dup/docs.md)
- [Material Properties](./notes/material_properties.md)
- [Framegraph](./notes/framegraph.md)
- [2D Coordinate Spaces](./notes/2d_coordinate_spaces.md)
- [Tests](./notes/tests.md)
- [backend](./dup/backend_test.md)
- [CI: backend](./dup/test_ci_backend.md)
- [CI: renderdiff](./dup/test_ci_renderdiff.md)
- [CI: sizeguard](./dup/test_ci_sizeguard.md)
- [Libraries](./notes/libs.md)
- [bluegl](./dup/bluegl.md)
- [bluevk](./dup/bluevk.md)
@@ -67,3 +61,9 @@
- [specgen](./dup/specgen.md)
- [specular-color](./dup/specular_color.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/colorGrading/colorGrading.cpp
src/materials/dof/dof.cpp
src/materials/evsm/evsm.cpp
src/materials/flare/flare.cpp
src/materials/fog/fog.cpp
src/materials/fsr/fsr.cpp
@@ -286,8 +285,6 @@ set(MATERIAL_SRCS
src/materials/dof/dofMipmap.mat
src/materials/dof/dofTiles.mat
src/materials/dof/dofTilesSwizzle.mat
src/materials/evsm/gaussian.mat
src/materials/evsm/vsmMipmap.mat
src/materials/flare/flare.mat
src/materials/fog/fog.mat
src/materials/fsr/fsr_easu.mat
@@ -304,6 +301,7 @@ set(MATERIAL_SRCS
src/materials/ssao/mipmapDepth.mat
src/materials/ssao/sao.mat
src/materials/ssao/saoBentNormals.mat
src/materials/vsmMipmap.mat
)
if (NOT FILAMENT_DISABLE_GTAO)
@@ -313,16 +311,15 @@ if (NOT FILAMENT_DISABLE_GTAO)
)
endif()
set(MATERIAL_MULTIVIEW_SRCS
src/materials/clearDepth.mat
set(MATERIAL_FL0_SRCS
src/materials/defaultMaterial.mat
src/materials/skybox.mat
)
# This set of materials should only be compiled for vulkan or metal because they contain operations
# not allowed on other platforms. (e.g. sampling from a multi-sampled texture).
set(MATERIAL_VULKAN_METAL_SRCS
src/materials/resolveDepth.mat
set(MATERIAL_MULTIVIEW_SRCS
src/materials/clearDepth.mat
src/materials/defaultMaterial.mat
src/materials/skybox.mat
)
# ==================================================================================================
@@ -344,6 +341,11 @@ if (NOT DFG_LUT_SIZE)
endif()
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.
if (FILAMENT_ENABLE_MULTIVIEW)
add_definitions(-DFILAMENT_ENABLE_MULTIVIEW)
@@ -393,27 +395,6 @@ foreach(mat_src ${MATERIAL_SRCS})
endforeach()
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
foreach(mat_dir ${MATERIAL_DIRS})
get_filename_component(package_name ${mat_dir} NAME)
@@ -436,35 +417,44 @@ foreach(mat_dir ${MATERIAL_DIRS})
# --- Base material ---
set(output_path "${MATERIAL_DIR}/${localname}.filamat")
set(MATC_CUSTOM_FLAGS "${MATC_BASE_FLAGS}")
set(COMMENT_STR "Compiling material ${fullname}")
add_custom_command(
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)
if (is_vk_metal)
# Remove webgpu and related parameters
# The variant-filter=stereo is added in the root folder's CMakeLists.txt for webgpu
# Context is that webgpu does not support the stereo variant
string(REPLACE "-a;webgpu;--variant-filter=stereo" "" MATC_CUSTOM_FLAGS "${MATC_CUSTOM_FLAGS}")
# Remove gl
string(REPLACE "-a;opengl" "" MATC_CUSTOM_FLAGS "${MATC_CUSTOM_FLAGS}")
# Remove any empty args
string(REPLACE ";;" ";" MATC_CUSTOM_FLAGS "${MATC_CUSTOM_FLAGS}")
string(REGEX REPLACE "^;|;$" "" MATC_CUSTOM_FLAGS "${MATC_CUSTOM_FLAGS}")
set(COMMENT_STR "Compiling material ${fullname} (Vulkan/Metal)")
# --- Feature Level 0 variant ---
list(FIND MATERIAL_FL0_SRCS ${mat_src} index)
if (${index} GREATER -1 AND FILAMENT_ENABLE_FEATURE_LEVEL_0)
string(REGEX REPLACE "[.]filamat$" "_fl0.filamat" output_path_fl0 ${output_path})
add_custom_command(
OUTPUT ${output_path_fl0}
COMMAND matc ${MATC_BASE_FLAGS} -PfeatureLevel=0 -o ${output_path_fl0} ${fullname}
MAIN_DEPENDENCY ${fullname}
DEPENDS matc
COMMENT "Compiling material ${fullname} (FL0)"
)
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path_fl0})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path_fl0})
endif()
compile_material(${output_path} ${fullname} MATC_CUSTOM_FLAGS "${COMMENT_STR}")
# --- Multiview variant ---
check_in_list(MATERIAL_MULTIVIEW_SRCS ${mat_src} is_multiview)
if (is_multiview AND FILAMENT_ENABLE_MULTIVIEW)
list(FIND MATERIAL_MULTIVIEW_SRCS ${mat_src} index)
if (${index} GREATER -1 AND FILAMENT_ENABLE_MULTIVIEW)
string(REGEX REPLACE "[.]filamat$" "_multiview.filamat" output_path_multiview ${output_path})
set(MATC_MULTIVIEW_FLAGS "${MATC_BASE_FLAGS}" "-PstereoscopicType=multiview")
set(COMMENT_STR "Compiling material ${fullname} (Multiview)")
compile_material(${output_path_multiview} ${fullname} MATC_MULTIVIEW_FLAGS "${COMMENT_STR}")
add_custom_command(
OUTPUT ${output_path_multiview}
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()
endforeach()
@@ -649,12 +639,6 @@ add_custom_command(
APPEND
)
add_custom_command(
OUTPUT "${MATERIAL_DIR}/gaussian.filamat"
DEPENDS src/materials/evsm/gaussian.fs
APPEND
)
file(MAKE_DIRECTORY "${GENERATION_ROOT}/generated/data/")
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/OpenGLContext.cpp
src/opengl/OpenGLContext.h
src/opengl/OpenGLState.cpp
src/opengl/OpenGLState.h
src/opengl/OpenGLDriver.cpp
src/opengl/OpenGLDriver.h
src/opengl/OpenGLDriverFactory.h
+1 -14
View File
@@ -48,7 +48,7 @@ class ostream;
/**
* 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.
* 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 {
switch (format) {
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_0(bool, isTextureSwizzleSupported)
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_0(bool, isFrameBufferFetchSupported)
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;
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
// --------------------------------------------------------------------------------------------
@@ -272,9 +253,6 @@ private:
std::condition_variable mServiceThreadCondition;
std::vector<std::tuple<CallbackHandler*, CallbackHandler::Callback, void*>> mServiceThreadCallbackQueue;
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(MTLCommandBufferErrorInvalidResource)
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorMemoryless)
if (@available(macOS 11.0, *)) {
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorMemoryless)
}
if (@available(iOS 15.0, macOS 12.0, *)) {
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorStackOverflow)
@@ -141,10 +143,12 @@ id<MTLCommandBuffer> getPendingCommandBuffer(MetalContext* context) {
context->latestCompletedCommandBufferId = thisCommandBufferId;
auto errorCode = (MTLCommandBufferError)buffer.error.code;
if (errorCode == MTLCommandBufferErrorMemoryless) {
LOG(WARNING) << "Metal: memoryless geometry limit reached. Continuing with private "
"storage mode.";
context->memorylessLimitsReached = true;
if (@available(macOS 11.0, *)) {
if (errorCode == MTLCommandBufferErrorMemoryless) {
LOG(WARNING) << "Metal: memoryless geometry limit reached. Continuing with private "
"storage mode.";
context->memorylessLimitsReached = true;
}
}
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) {
MTLPixelFormat mtlFormat = getMetalFormat(mContext, format);
// 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
// Apple GPUs with readWriteTextureSupport.
// On macOS, framebuffer fetch requires MSL 2.3, which is only available with macOS 11.0.
return mContext->highestSupportedGpuFamily.apple >= 1 &&
mContext->device.readWriteTextureSupport;
if (@available(macOS 11.0, *)) {
return mContext->highestSupportedGpuFamily.apple >= 1 &&
mContext->device.readWriteTextureSupport;
}
return false;
}
bool MetalDriver::isFrameBufferFetchMultiSampleSupported() {
@@ -2295,14 +2283,12 @@ void MetalDriver::draw2(uint32_t indexOffset, uint32_t indexCount, uint32_t inst
// Bind the offset data.
if (mContext->dynamicOffsets.isDirty()) {
const auto [size, data] = mContext->dynamicOffsets.getOffsets();
if (size > 0) {
[mContext->currentRenderPassEncoder setFragmentBytes:data
length:size * sizeof(uint32_t)
atIndex:DYNAMIC_OFFSET_BINDING];
[mContext->currentRenderPassEncoder setVertexBytes:data
length:size * sizeof(uint32_t)
atIndex:DYNAMIC_OFFSET_BINDING];
}
[mContext->currentRenderPassEncoder setFragmentBytes:data
length:size * sizeof(uint32_t)
atIndex:DYNAMIC_OFFSET_BINDING];
[mContext->currentRenderPassEncoder setVertexBytes:data
length:size * sizeof(uint32_t)
atIndex:DYNAMIC_OFFSET_BINDING];
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) {
if (type == PixelDataType::UINT_2_10_10_10_REV) return MTLPixelFormatRGB10A2Unorm;
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) \
if (PixelDataFormat::FORMAT == format && PixelDataType::TYPE == type) return MTLPixelFormat ## MTL;
@@ -203,31 +205,33 @@ inline MTLPixelFormat getMetalFormatLinear(MTLPixelFormat format) {
#endif
default: break;
}
switch (format) {
case MTLPixelFormatASTC_4x4_sRGB: return MTLPixelFormatASTC_4x4_LDR;
case MTLPixelFormatASTC_5x4_sRGB: return MTLPixelFormatASTC_5x4_LDR;
case MTLPixelFormatASTC_5x5_sRGB: return MTLPixelFormatASTC_5x5_LDR;
case MTLPixelFormatASTC_6x5_sRGB: return MTLPixelFormatASTC_6x5_LDR;
case MTLPixelFormatASTC_6x6_sRGB: return MTLPixelFormatASTC_6x6_LDR;
case MTLPixelFormatASTC_8x5_sRGB: return MTLPixelFormatASTC_8x5_LDR;
case MTLPixelFormatASTC_8x6_sRGB: return MTLPixelFormatASTC_8x6_LDR;
case MTLPixelFormatASTC_8x8_sRGB: return MTLPixelFormatASTC_8x8_LDR;
case MTLPixelFormatASTC_10x5_sRGB: return MTLPixelFormatASTC_10x5_LDR;
case MTLPixelFormatASTC_10x6_sRGB: return MTLPixelFormatASTC_10x6_LDR;
case MTLPixelFormatASTC_10x8_sRGB: return MTLPixelFormatASTC_10x8_LDR;
case MTLPixelFormatASTC_10x10_sRGB: return MTLPixelFormatASTC_10x10_LDR;
case MTLPixelFormatASTC_12x10_sRGB: return MTLPixelFormatASTC_12x10_LDR;
case MTLPixelFormatASTC_12x12_sRGB: return MTLPixelFormatASTC_12x12_LDR;
case MTLPixelFormatPVRTC_RGB_2BPP_sRGB: return MTLPixelFormatPVRTC_RGB_2BPP;
case MTLPixelFormatPVRTC_RGB_4BPP_sRGB: return MTLPixelFormatPVRTC_RGB_4BPP;
case MTLPixelFormatPVRTC_RGBA_2BPP_sRGB: return MTLPixelFormatPVRTC_RGBA_2BPP;
case MTLPixelFormatPVRTC_RGBA_4BPP_sRGB: return MTLPixelFormatPVRTC_RGBA_4BPP;
case MTLPixelFormatEAC_RGBA8_sRGB: return MTLPixelFormatEAC_RGBA8;
case MTLPixelFormatETC2_RGB8_sRGB: return MTLPixelFormatETC2_RGB8;
case MTLPixelFormatETC2_RGB8A1_sRGB: return MTLPixelFormatETC2_RGB8A1;
case MTLPixelFormatBGR10_XR_sRGB: return MTLPixelFormatBGR10_XR;
case MTLPixelFormatBGRA10_XR_sRGB: return MTLPixelFormatBGRA10_XR;
default: break;
if (@available(macOS 11, *)) {
switch (format) {
case MTLPixelFormatASTC_4x4_sRGB: return MTLPixelFormatASTC_4x4_LDR;
case MTLPixelFormatASTC_5x4_sRGB: return MTLPixelFormatASTC_5x4_LDR;
case MTLPixelFormatASTC_5x5_sRGB: return MTLPixelFormatASTC_5x5_LDR;
case MTLPixelFormatASTC_6x5_sRGB: return MTLPixelFormatASTC_6x5_LDR;
case MTLPixelFormatASTC_6x6_sRGB: return MTLPixelFormatASTC_6x6_LDR;
case MTLPixelFormatASTC_8x5_sRGB: return MTLPixelFormatASTC_8x5_LDR;
case MTLPixelFormatASTC_8x6_sRGB: return MTLPixelFormatASTC_8x6_LDR;
case MTLPixelFormatASTC_8x8_sRGB: return MTLPixelFormatASTC_8x8_LDR;
case MTLPixelFormatASTC_10x5_sRGB: return MTLPixelFormatASTC_10x5_LDR;
case MTLPixelFormatASTC_10x6_sRGB: return MTLPixelFormatASTC_10x6_LDR;
case MTLPixelFormatASTC_10x8_sRGB: return MTLPixelFormatASTC_10x8_LDR;
case MTLPixelFormatASTC_10x10_sRGB: return MTLPixelFormatASTC_10x10_LDR;
case MTLPixelFormatASTC_12x10_sRGB: return MTLPixelFormatASTC_12x10_LDR;
case MTLPixelFormatASTC_12x12_sRGB: return MTLPixelFormatASTC_12x12_LDR;
case MTLPixelFormatPVRTC_RGB_2BPP_sRGB: return MTLPixelFormatPVRTC_RGB_2BPP;
case MTLPixelFormatPVRTC_RGB_4BPP_sRGB: return MTLPixelFormatPVRTC_RGB_4BPP;
case MTLPixelFormatPVRTC_RGBA_2BPP_sRGB: return MTLPixelFormatPVRTC_RGBA_2BPP;
case MTLPixelFormatPVRTC_RGBA_4BPP_sRGB: return MTLPixelFormatPVRTC_RGBA_4BPP;
case MTLPixelFormatEAC_RGBA8_sRGB: return MTLPixelFormatEAC_RGBA8;
case MTLPixelFormatETC2_RGB8_sRGB: return MTLPixelFormatETC2_RGB8;
case MTLPixelFormatETC2_RGB8A1_sRGB: return MTLPixelFormatETC2_RGB8A1;
case MTLPixelFormatBGR10_XR_sRGB: return MTLPixelFormatBGR10_XR;
case MTLPixelFormatBGRA10_XR_sRGB: return MTLPixelFormatBGRA10_XR;
default: break;
}
}
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.
if (context->highestSupportedGpuFamily.apple >= 1) {
if (@available(macOS 11.0, *)) {
switch (format) {
case TextureFormat::RGB565: return MTLPixelFormatB5G6R5Unorm;
case TextureFormat::RGB5_A1: return MTLPixelFormatA1BGR5Unorm;
case TextureFormat::RGBA4: return MTLPixelFormatABGR4Unorm;
default: break;
}
}
}
if (@available(iOS 13.0, *)) {
@@ -113,16 +115,18 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
#if TARGET_OS_OSX
if (context->highestSupportedGpuFamily.mac >= 1 &&
context->device.depth24Stencil8PixelFormatSupported) {
if (@available(macOS 11.0, *)) {
if (format == TextureFormat::DEPTH24_STENCIL8) {
return MTLPixelFormatDepth24Unorm_Stencil8;
}
}
}
#endif
// 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.
if (context->highestSupportedGpuFamily.apple >= 2) {
if (@available(iOS 13, *)) {
if (@available(iOS 13, macOS 11.0, *)) {
switch (format) {
case TextureFormat::RGBA_ASTC_4x4: return MTLPixelFormatASTC_4x4_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;
default: break;
}
} else {
} else if (@available(macOS 11.0, *)) {
switch (format) {
case TextureFormat::RGBA_ASTC_4x4: return MTLPixelFormatASTC_4x4_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.
if (context->highestSupportedGpuFamily.apple >= 1) {
if (@available(macOS 11.0, *)) {
switch (format) {
case TextureFormat::EAC_R11: return MTLPixelFormatEAC_R11Unorm;
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;
default: break;
}
}
}
// DXT (BC) formats are only available on macOS desktop.
@@ -478,6 +478,8 @@ private:
struct State {
FenceStatus status { FenceStatus::TIMEOUT_EXPIRED };
std::condition_variable cv;
std::mutex mutex;
};
std::shared_ptr<State> state { std::make_shared<State>() };
+20 -23
View File
@@ -20,7 +20,6 @@
#include "MetalEnums.h"
#include "MetalUtils.h"
#include "MetalBufferPool.h"
#include "MetalDriver.h"
#include <filament/SwapChain.h>
@@ -184,7 +183,9 @@ id<MTLTexture> MetalSwapChain::createMultisampledTexture(MetalContext const& con
descriptor.resourceOptions = MTLResourceStorageModePrivate;
if (context.supportsMemorylessRenderTargets) {
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
if (@available(macOS 11.0, *)) {
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
}
}
return [context.device newTextureWithDescriptor:descriptor];
@@ -1374,7 +1375,9 @@ id<MTLTexture> MetalRenderTarget::createMultisampledTexture(MTLPixelFormat forma
descriptor.resourceOptions = MTLResourceStorageModePrivate;
if (context->supportsMemorylessRenderTargets) {
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
if (@available(macOS 11.0, *)) {
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
}
}
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.
std::weak_ptr<State> weakState = state;
MetalDriver* driver = context.driver;
[event notifyListener:context.eventListener atValue:value block:^(id <MTLSharedEvent> o,
uint64_t value) {
if (auto s = weakState.lock()) {
driver->signalFence([&] {
s->status = FenceStatus::CONDITION_SATISFIED;
});
std::lock_guard<std::mutex> guard(s->mutex);
s->status = FenceStatus::CONDITION_SATISFIED;
s->cv.notify_all();
}
}];
}
@@ -1420,29 +1422,24 @@ void MetalFence::onSignal(MetalFenceSignalBlock block) {
FenceStatus MetalFence::wait(uint64_t timeoutNs) {
if (@available(iOS 12, *)) {
using ns = std::chrono::nanoseconds;
FenceStatus result = FenceStatus::TIMEOUT_EXPIRED;
auto predicate = [&] {
if (state->status != FenceStatus::TIMEOUT_EXPIRED) {
result = state->status;
return true;
std::unique_lock<std::mutex> guard(state->mutex);
while (state->status == FenceStatus::TIMEOUT_EXPIRED) {
if (timeoutNs == FENCE_WAIT_FOR_EVER) {
state->cv.wait(guard);
} 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;
}
void MetalFence::cancel() {
context.driver->signalFence([&] {
state->status = FenceStatus::ERROR;
});
std::unique_lock guard(state->mutex);
state->status = FenceStatus::ERROR;
state->cv.notify_all();
}
MetalDescriptorSetLayout::MetalDescriptorSetLayout(DescriptorSetLayout&& l) noexcept
-4
View File
@@ -194,10 +194,6 @@ bool NoopDriver::isTextureFormatMipmappable(TextureFormat format) {
return true;
}
bool NoopDriver::isTextureFormatFilterable(TextureFormat format) {
return true;
}
bool NoopDriver::isRenderTargetFormatSupported(TextureFormat format) {
return true;
}
@@ -21,8 +21,8 @@
#include "GLTexture.h"
#include "GLUtils.h"
#include "OpenGLDriver.h"
#include "OpenGLContext.h"
#include "OpenGLProgram.h"
#include "OpenGLState.h"
#include "gl_headers.h"
@@ -49,7 +49,7 @@
namespace filament::backend {
GLDescriptorSet::GLDescriptorSet(OpenGLState& gl, DescriptorSetLayoutHandle dslh,
GLDescriptorSet::GLDescriptorSet(OpenGLContext& gl, DescriptorSetLayoutHandle dslh,
GLDescriptorSetLayout const* layout)
: descriptors(layout->maxDescriptorBinding + 1),
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 {
assert_invariant(binding < descriptors.size());
std::visit([=](auto&& arg) {
@@ -164,7 +164,7 @@ void GLDescriptorSet::update(OpenGLState&,
}, 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 {
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.wrapR = SamplerWrapMode::CLAMP_TO_EDGE;
}
// GLES3.x specification forbids depth textures to be filtered.
bool const isDepthFmt = t && isDepthFormat(t->format);
bool const depthTextureNoPcf = isDepthFmt &&
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)) {
if (t && isDepthFormat(t->format)
&& params.compareMode == SamplerCompareMode::NONE) {
params.filterMag = SamplerMagFilter::NEAREST;
switch (params.filterMin) {
case SamplerMinFilter::LINEAR:
@@ -232,7 +223,7 @@ void GLDescriptorSet::update(OpenGLState& gl, HandleAllocatorGL& handleAllocator
}, descriptors[binding].desc);
}
void GLDescriptorSet::updateTextureView(OpenGLState& gl,
void GLDescriptorSet::updateTextureView(OpenGLContext& gl,
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 texture ever had a View on it).
@@ -269,7 +260,7 @@ void GLDescriptorSet::updateTextureView(OpenGLState& gl,
}
void GLDescriptorSet::bind(
OpenGLState& gl,
OpenGLContext& gl,
HandleAllocatorGL& handleAllocator,
OpenGLProgram const& p,
descriptor_set_t set, uint32_t const* offsets, bool offsetsOnly) const noexcept {
@@ -387,7 +378,7 @@ void GLDescriptorSet::bind(
}
}
#endif
glTexParameteri(t->gl.target, GL_TEXTURE_MIN_FILTER,
(GLint)GLUtils::getTextureFilter(params.filterMin));
glTexParameteri(t->gl.target, GL_TEXTURE_MAG_FILTER,
@@ -44,27 +44,27 @@ struct GLTexture;
struct GLTextureRef;
struct GLDescriptorSetLayout;
class OpenGLProgram;
class OpenGLContext;
class OpenGLDriver;
class OpenGLState;
struct GLDescriptorSet : public HwDescriptorSet {
using HwDescriptorSet::HwDescriptorSet;
GLDescriptorSet(OpenGLState& gl, DescriptorSetLayoutHandle dslh,
GLDescriptorSet(OpenGLContext& gl, DescriptorSetLayoutHandle dslh,
GLDescriptorSetLayout const* layout);
// 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;
// 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;
// conceptually bind the set to the command buffer
void bind(
OpenGLState& gl,
OpenGLContext& gl,
HandleAllocatorGL& handleAllocator,
OpenGLProgram const& p,
descriptor_set_t set, uint32_t const* offsets, bool offsetsOnly) const noexcept;
@@ -138,7 +138,7 @@ private:
};
static_assert(sizeof(Descriptor) <= 32);
static void updateTextureView(OpenGLState& gl,
static void updateTextureView(OpenGLContext& gl,
HandleAllocatorGL& handleAllocator, GLuint unit, GLTexture const* t) noexcept;
utils::FixedCapacityVector<Descriptor> descriptors; // 16
@@ -18,8 +18,8 @@
#include "GLBufferObject.h"
#include "GLUtils.h"
#include "OpenGLContext.h"
#include "OpenGLDriver.h"
#include "OpenGLState.h"
#include "gl_headers.h"
@@ -45,7 +45,7 @@ GLMemoryMappedBuffer::GLMemoryMappedBuffer() = default;
GLMemoryMappedBuffer::~GLMemoryMappedBuffer() = default;
GLMemoryMappedBuffer::GLMemoryMappedBuffer(OpenGLState& gls, HandleAllocatorGL& handleAllocator,
GLMemoryMappedBuffer::GLMemoryMappedBuffer(OpenGLContext& glc, HandleAllocatorGL& handleAllocator,
BufferObjectHandle boh,
size_t const offset, size_t const size, MapBufferAccessFlags const access)
: boh(boh), access(access) {
@@ -60,7 +60,7 @@ GLMemoryMappedBuffer::GLMemoryMappedBuffer(OpenGLState& gls, HandleAllocatorGL&
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.size = size;
gl.offset = offset;
@@ -85,7 +85,7 @@ GLMemoryMappedBuffer::GLMemoryMappedBuffer(OpenGLState& gls, HandleAllocatorGL&
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);
@@ -103,20 +103,20 @@ GLMemoryMappedBuffer::GLMemoryMappedBuffer(OpenGLState& gls, HandleAllocatorGL&
#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);
assert_invariant(bo);
assert_invariant(bo->mappingCount > 0);
bo->mappingCount--;
if (UTILS_UNLIKELY(gls.isES2())) {
if (UTILS_UNLIKELY(glc.isES2())) {
// nothing to do
return;
}
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
gls.bindBuffer(gl.binding, gl.id);
glc.bindBuffer(gl.binding, gl.id);
# if !defined(__EMSCRIPTEN__)
// don't unmap if we don't have a mapping or it didn't work
if (UTILS_LIKELY(gl.vaddr)) {
@@ -131,7 +131,7 @@ void GLMemoryMappedBuffer::unmap(OpenGLState& gls, HandleAllocatorGL& handleAllo
#endif
}
void GLMemoryMappedBuffer::copy(OpenGLState& gls, OpenGLDriver& gld,
void GLMemoryMappedBuffer::copy(OpenGLContext& glc, OpenGLDriver& gld,
size_t const offset, BufferDescriptor&& data) const {
assert_invariant(any(access & MapBufferAccessFlags::WRITE_BIT));
assert_invariant(offset + data.size <= gl.size);
@@ -139,11 +139,11 @@ void GLMemoryMappedBuffer::copy(OpenGLState& gls, OpenGLDriver& gld,
if (UTILS_LIKELY(gl.vaddr)) {
memcpy(static_cast<char *>(gl.vaddr) + offset, data.buffer, data.size);
} else {
assert_invariant(!gls.isES2());
assert_invariant(!glc.isES2());
// 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
// 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);
CHECK_GL_ERROR();
}
@@ -31,7 +31,7 @@
namespace filament::backend {
class OpenGLState;
class OpenGLContext;
class OpenGLDriver;
class BufferDescriptor;
@@ -50,14 +50,14 @@ struct GLMemoryMappedBuffer : public HwMemoryMappedBuffer {
GLMemoryMappedBuffer();
GLMemoryMappedBuffer(OpenGLState& gls, HandleAllocatorGL& handleAllocator,
GLMemoryMappedBuffer(OpenGLContext& glc, HandleAllocatorGL& handleAllocator,
BufferObjectHandle boh, size_t offset, size_t size, MapBufferAccessFlags access);
~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;
};
+544 -34
View File
@@ -15,7 +15,6 @@
*/
#include "OpenGLContext.h"
#include "OpenGLState.h"
#include "GLUtils.h"
#include "OpenGLTimerQuery.h"
@@ -30,7 +29,9 @@
#include <utils/ostream.h>
#include <algorithm>
#include <functional>
#include <string_view>
#include <utility>
#include <stddef.h>
#include <stdio.h>
@@ -67,37 +68,40 @@ bool OpenGLContext::queryOpenGLVersion(GLint* major, GLint* minor) noexcept {
OpenGLContext::OpenGLContext(OpenGLPlatform& platform,
Platform::DriverConfig const& driverConfig) noexcept
: mPlatform(platform),
mSamplerMap(32),
mDriverConfig(driverConfig) {
// These queries work with all GL/GLES versions!
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);
state.vao.p = &mDefaultVAO;
LOG(INFO) << "[" << vendor << "], [" << renderer << "], "
"[" << version << "], [" << shader << "]";
// These queries work with all GL/GLES versions!
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
* determine the feature level
*/
queryOpenGLVersion(&major, &minor);
queryOpenGLVersion(&state.major, &state.minor);
#if defined(BACKEND_OPENGL_VERSION_GLES)
if (UTILS_UNLIKELY(driverConfig.forceGLES2Context)) {
major = 2;
minor = 0;
state.major = 2;
state.minor = 0;
}
#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,
vendor, renderer, version, shader);
initBugs(&bugs, ext, state.major, state.minor,
state.vendor, state.renderer, state.version, state.shader);
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_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
mShaderModel = ShaderModel::MOBILE;
@@ -197,6 +201,8 @@ OpenGLContext::OpenGLContext(OpenGLPlatform& platform,
assert_invariant(mFeatureLevel == FeatureLevel::FEATURE_LEVEL_0 || gets.max_draw_buffers >= 4); // minspec
#endif
setDefaultState();
#ifdef GL_EXT_texture_filter_anisotropic
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
if (mFeatureLevel >= FeatureLevel::FEATURE_LEVEL_1 && ext.EXT_texture_filter_anisotropic) {
@@ -257,20 +263,120 @@ OpenGLContext::OpenGLContext(OpenGLPlatform& platform,
glDebugMessageCallback(cb, nullptr);
}
#endif
mTimerQueryFactory = TimerQueryFactory::init(platform, *this);
}
OpenGLContext::~OpenGLContext() noexcept = default;
OpenGLState* OpenGLContext::createState() noexcept {
auto* state = new(std::nothrow) OpenGLState(*this);
assert_invariant(state);
return state;
OpenGLContext::~OpenGLContext() noexcept {
// note: this is called from the main thread. Can't do any GL calls.
delete mTimerQueryFactory;
}
void OpenGLContext::destroyState(OpenGLState* state) noexcept {
if (state) {
state->terminate();
delete state;
void OpenGLContext::terminate() noexcept {
// note: this is called from the backend thread
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
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_rgb8_rgba8 = exts.has("GL_OES_rgb8_rgba8"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_vertex_array_object = exts.has("GL_OES_vertex_array_object"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_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)) {
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) {
ext->EXT_discard_framebuffer = 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_rgb8_rgba8 = 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_vertex_array_object = true;
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
} // 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
#define TNT_FILAMENT_BACKEND_OPENGLCONTEXT_H
#include "OpenGLTimerQuery.h"
#include <backend/platforms/OpenGLPlatform.h>
#include <backend/DriverEnums.h>
@@ -31,8 +34,13 @@
#include <math/vec2.h>
#include <math/vec4.h>
#include <tsl/robin_map.h>
#include <array>
#include <functional>
#include <optional>
#include <tuple>
#include <vector>
#include <stddef.h>
#include <stdint.h>
@@ -40,9 +48,8 @@
namespace filament::backend {
class OpenGLPlatform;
class OpenGLState;
class OpenGLContext final {
class OpenGLContext final : public TimerQueryFactoryInterface {
public:
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
@@ -70,12 +77,12 @@ public:
// be updated (see OpenGLDriver::updateVertexArrayObject())
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())
uint8_t stateVersion = 0; // 1
// If this differs from OpenGLState::state.age, this VAO's name needs to be updated.
// See OpenGLState::bindVertexArray()
// If this differs from OpenGLContext::state.age, this VAO's name needs to be updated.
// See OpenGLContext::bindVertexArray()
uint8_t nameVersion = 0; // 1
// Size in bytes of indices in the index buffer (1 or 2)
@@ -91,18 +98,24 @@ public:
explicit OpenGLContext(OpenGLPlatform& platform,
Platform::DriverConfig const& driverConfig) noexcept;
~OpenGLContext() noexcept;
~OpenGLContext() noexcept final;
// State lifecycle - allocate/deallocate per-thread OpenGLState instances
OpenGLState* createState() noexcept;
void destroyState(OpenGLState* state) noexcept;
void terminate() 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>
inline bool isAtLeastGL() const noexcept {
#ifdef BACKEND_OPENGL_VERSION_GL
return major > MAJOR || (major == MAJOR && minor >= MINOR);
return state.major > MAJOR || (state.major == MAJOR && state.minor >= MINOR);
#else
return false;
#endif
@@ -111,7 +124,7 @@ public:
template<int MAJOR, int MINOR>
inline bool isAtLeastGLES() const noexcept {
#ifdef BACKEND_OPENGL_VERSION_GLES
return major > MAJOR || (major == MAJOR && minor >= MINOR);
return state.major > MAJOR || (state.major == MAJOR && state.minor >= MINOR);
#else
return false;
#endif
@@ -141,13 +154,60 @@ public:
#endif
}
constexpr inline size_t getIndexForCap(GLenum cap) noexcept;
constexpr static inline size_t getIndexForBufferTarget(GLenum target) noexcept;
ShaderModel getShaderModel() const noexcept { return mShaderModel; }
FeatureLevel getFeatureLevel() const noexcept { return mFeatureLevel; }
OpenGLPlatform& getPlatform() const noexcept { return mPlatform; }
Platform::DriverConfig const& getDriverConfig() const noexcept { return mDriverConfig; }
void resetState() noexcept;
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
struct Gets {
@@ -208,8 +268,6 @@ public:
bool OES_packed_depth_stencil;
bool OES_rgb8_rgba8;
bool OES_standard_derivatives;
bool OES_texture_float_linear;
bool OES_texture_half_float_linear;
bool OES_texture_npot;
bool OES_vertex_array_object;
bool OVR_multiview2;
@@ -299,6 +357,143 @@ public:
} 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 {
void (* bindVertexArray)(GLuint array);
void (* deleteVertexArrays)(GLsizei n, const GLuint* arrays);
@@ -316,25 +511,43 @@ public:
void (* maxShaderCompilerThreadsKHR)(GLuint count);
} procs{};
// GL version info — immutable after construction
GLint major = 0;
GLint minor = 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
// GL info strings — immutable after construction
char const* vendor = nullptr;
char const* renderer = nullptr;
char const* version = nullptr;
char const* shader = nullptr;
private:
OpenGLPlatform& mPlatform;
ShaderModel mShaderModel = ShaderModel::MOBILE;
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;
bool mGpuTimeSupported = false;
void bindFramebufferResolved(GLenum target, GLuint buffer) noexcept;
const std::array<std::tuple<bool const&, char const*, char const*>, sizeof(bugs)> mBugDatabase{{
{ bugs.disable_glFlush,
"disable_glFlush",
@@ -426,8 +639,315 @@ private:
Extensions const& exts,
Gets const& gets,
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
File diff suppressed because it is too large Load Diff
+18 -30
View File
@@ -74,7 +74,6 @@
namespace filament::backend {
class OpenGLPlatform;
class OpenGLState;
class PixelBufferDescriptor;
struct TargetBufferInfo;
class OpenGLProgram;
@@ -197,6 +196,8 @@ public:
struct GLFence : public HwFence {
using HwFence::HwFence;
struct State {
utils::Mutex lock; // NOLINT(*-include-cleaner)
utils::Condition cond; // NOLINT(*-include-cleaner)
FenceStatus status{ FenceStatus::TIMEOUT_EXPIRED };
};
std::shared_ptr<State> state{ std::make_shared<State>() };
@@ -223,24 +224,11 @@ public:
private:
OpenGLPlatform& mPlatform;
OpenGLContext mContext;
OpenGLState* mBackendState = nullptr; // owned by backend thread
OpenGLState* mWorkerState = nullptr; // owned by worker thread (initialized for async mode)
ShaderCompilerService mShaderCompilerService;
friend class TimerQueryFactory;
friend class TimerQueryNativeFactory;
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 {
return mShaderCompilerService;
@@ -255,15 +243,15 @@ private:
*/
utils::CString getVendorString() const noexcept override {
return utils::CString{ mContext.vendor };
return utils::CString{ mContext.state.vendor };
}
utils::CString getRendererString() const noexcept override {
return utils::CString{ mContext.renderer };
return utils::CString{ mContext.state.renderer };
}
utils::CString getVersionString() const noexcept override {
return utils::CString{ mContext.version };
return utils::CString{ mContext.state.version };
}
JobQueue* getJobQueue() const noexcept { return mJobQueue.get(); }
@@ -347,13 +335,13 @@ private:
void setStencilState(StencilState ss) noexcept;
void setTextureData(OpenGLState& gl, GLTexture const* t,
void setTextureData(GLTexture const* t,
uint32_t level,
uint32_t xoffset, uint32_t yoffset, uint32_t zoffset,
uint32_t width, uint32_t height, uint32_t depth,
PixelBufferDescriptor&& p);
void setCompressedTextureData(OpenGLState& gl, GLTexture const* t,
void setCompressedTextureData(GLTexture const* t,
uint32_t level,
uint32_t xoffset, uint32_t yoffset, uint32_t zoffset,
uint32_t width, uint32_t height, uint32_t depth,
@@ -362,7 +350,7 @@ private:
void renderBufferStorage(GLuint rbo, GLenum internalformat, uint32_t width,
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;
/* State tracking GL wrappers... */
@@ -382,32 +370,32 @@ private:
// Common methods
void readPixelsFromBoundFramebuffer(uint32_t x, uint32_t y, uint32_t width, uint32_t height,
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,
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 width, uint32_t height, uint32_t depth,
PixelBufferDescriptor&& data);
void createTextureViewSwizzleCommon(Handle<HwTexture> th, Handle<HwTexture> srch,
TextureSwizzle r, TextureSwizzle g, TextureSwizzle b,
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,
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);
void setVertexBufferObjectCommon(Handle<HwVertexBuffer> vbh, uint32_t index,
Handle<HwBufferObject> boh);
void updateBufferObjectCommon(OpenGLState& gl, Handle<HwBufferObject> boh, BufferDescriptor&& bd,
void updateBufferObjectCommon(Handle<HwBufferObject> boh, BufferDescriptor&& bd,
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);
void updateIndexBufferCommon(OpenGLState& gl, Handle<HwIndexBuffer> ibh, BufferDescriptor&& p,
void updateIndexBufferCommon(Handle<HwIndexBuffer> ibh, BufferDescriptor&& p,
uint32_t const byteOffset);
void destroyTextureCommon(OpenGLState& gl, Handle<HwTexture> th);
void destroyBufferObjectCommon(OpenGLState& gl, Handle<HwBufferObject> boh);
void destroyIndexBufferCommon(OpenGLState& gl, Handle<HwIndexBuffer> ibh);
void destroyTextureCommon(Handle<HwTexture> th);
void destroyBufferObjectCommon(Handle<HwBufferObject> boh);
void destroyIndexBufferCommon(Handle<HwIndexBuffer> ibh);
// state required to represent the current render pass
Handle<HwRenderTarget> mRenderPassTarget;
@@ -112,7 +112,7 @@ void OpenGLProgram::initialize(OpenGLDriver& gld) {
assert_invariant(mToken == nullptr);
if (gl.program) {
assert_invariant(lazyInitializationData);
initializeProgramState(gld.getBackendState(), gl.program, *lazyInitializationData);
initializeProgramState(gld.getContext(), gl.program, *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
* checkProgramStatus() has been successfully called.
*/
void OpenGLProgram::initializeProgramState(OpenGLState& gls, GLuint program,
void OpenGLProgram::initializeProgramState(OpenGLContext& context, GLuint program,
LazyInitializationData& lazyInitializationData) {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
@@ -139,7 +139,7 @@ void OpenGLProgram::initializeProgramState(OpenGLState& gls, GLuint program,
GLuint binding = 0;
// needed for samplers
gls.useProgram(program);
context.useProgram(program);
UTILS_NOUNROLL
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: {
if (!entry.name.empty()) {
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
if (UTILS_LIKELY(!gls.isES2())) {
if (UTILS_LIKELY(!context.isES2())) {
GLuint const index = glGetUniformBlockIndex(program,
entry.name.c_str());
if (index != GL_INVALID_INDEX) {
@@ -219,7 +219,7 @@ void OpenGLProgram::initializeProgramState(OpenGLState& gls, GLuint program,
CHECK_GL_ERROR()
}
if (gls.isES2()) {
if (context.isES2()) {
// ES2 initialization of (fake) UBOs
UniformsRecord* const uniformsRecords = new(std::nothrow) UniformsRecord[Program::UNIFORM_BINDING_COUNT];
UTILS_NOUNROLL
+4 -4
View File
@@ -20,7 +20,7 @@
#include "DriverBase.h"
#include "BindingMap.h"
#include "OpenGLState.h"
#include "OpenGLContext.h"
#include "ShaderCompilerService.h"
#include <private/backend/Driver.h>
@@ -56,7 +56,7 @@ public:
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
assert_invariant(!mToken || !gl.program);
@@ -71,7 +71,7 @@ public:
return false;
}
gls.useProgram(gl.program);
context.useProgram(gl.program);
return true;
}
@@ -105,7 +105,7 @@ private:
void initialize(OpenGLDriver& gld);
void initializeProgramState(OpenGLState& gls, GLuint program,
void initializeProgramState(OpenGLContext& context, GLuint program,
LazyInitializationData& lazyInitializationData);
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;
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
// 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
@@ -16,10 +16,6 @@
#include "VulkanAsyncHandles.h"
#include "VulkanConstants.h"
#include "vulkan/utils/Spirv.h"
#include <backend/DriverEnums.h>
#include <utils/debug.h>
@@ -33,154 +29,6 @@ using namespace bluevk;
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 {
auto cmdFence = std::make_shared<VulkanCmdFence>(VK_NULL_HANDLE);
cmdFence->mStatus = VK_SUCCESS;
+19 -121
View File
@@ -24,9 +24,6 @@
#include "backend/Platform.h"
#include "vulkan/memory/Resource.h"
#include "vulkan/utils/StaticVector.h"
#include <backend/Program.h>
#include <chrono>
#include <cstdint>
@@ -38,123 +35,6 @@
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.
struct VulkanCmdFence {
explicit VulkanCmdFence(VkFence fence) : mFence(fence) { }
@@ -204,21 +84,39 @@ struct VulkanFence : public HwFence, fvkmemory::ThreadSafeResource {
VulkanFence() {}
void setFence(std::shared_ptr<VulkanCmdFence> fence) {
std::lock_guard const l(lock);
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 };
}
void cancel() const {
std::lock_guard const l(lock);
if (sharedFence) {
sharedFence->cancel();
}
canceled = true;
cond.notify_all();
}
private:
mutable std::mutex lock;
mutable std::condition_variable cond;
mutable bool canceled = false;
std::shared_ptr<VulkanCmdFence> sharedFence;
};
+3 -11
View File
@@ -34,11 +34,10 @@
#include <utils/Mutex.h>
#include <atomic>
#include <chrono>
#include <list>
#include <type_traits>
#include <utility>
#include <variant>
namespace filament::backend {
@@ -73,11 +72,7 @@ struct VulkanCommandBuffer {
~VulkanCommandBuffer();
template <typename T,
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) {
inline void acquire(fvkmemory::resource_ptr<fvkmemory::Resource> resource) {
mResources.push_back(resource);
}
@@ -120,9 +115,6 @@ struct VulkanCommandBuffer {
}
private:
using HeldResource = std::variant<fvkmemory::resource_ptr<Resource>,
fvkmemory::resource_ptr<ThreadSafeResource>>;
static uint32_t sAgeCounter;
VulkanContext const& mContext;
@@ -137,7 +129,7 @@ private:
fvkmemory::resource_ptr<VulkanSemaphore> mSubmission;
VkFence mFence;
std::shared_ptr<VulkanCmdFence> mFenceStatus;
std::vector<HeldResource> mResources;
std::vector<fvkmemory::resource_ptr<Resource>> mResources;
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
// buffer.
auto fence = resource_ptr<VulkanFence>::cast(&mResourceManager, fh);
signalFence([&] {
fence->setFence(cmdbuf->getFenceStatus());
});
fence->setFence(cmdbuf->getFenceStatus());
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
assert_invariant(fh);
auto fence = resource_ptr<VulkanFence>::cast(&mResourceManager, fh);
signalFence([&] {
fence->cancel();
});
fence->cancel();
}
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);
}
std::shared_ptr<VulkanCmdFence> cmdfence;
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);
auto const [cmdfence, canceled] = fence->wait(until);
if (!cmdfence || canceled) {
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) {
VkFormat vkformat = fvkutils::getVkFormat(format);
if (vkformat == VK_FORMAT_UNDEFINED) {
@@ -16,6 +16,8 @@
#include "VulkanHandles.h"
#include "VulkanConstants.h"
// TODO: remove this by moving DebugUtils out of VulkanDriver
#include "VulkanDriver.h"
@@ -24,6 +26,7 @@
#include "vulkan/utils/Conversion.h"
#include "vulkan/utils/Definitions.h"
#include "vulkan/utils/Image.h"
#include "vulkan/utils/Spirv.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;
BitmaskGroup fromBackendLayout(DescriptorSetLayout const& layout) {
BitmaskGroup mask;
@@ -250,6 +264,139 @@ void VulkanDescriptorSet::addNewSet(VkDescriptorSet vkSet, OnRecycle&& onRecycle
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)
VulkanRenderTarget::VulkanRenderTarget()
: HwRenderTarget(0, 0),
+115
View File
@@ -30,6 +30,9 @@
#include "vulkan/memory/Resource.h"
#include "vulkan/memory/ResourcePointer.h"
#include "vulkan/utils/Definitions.h"
#include "vulkan/utils/StaticVector.h"
#include <backend/Program.h>
#include <utils/FixedCapacityVector.h>
#include <utils/Mutex.h>
@@ -238,6 +241,118 @@ struct VulkanMemoryMappedBuffer : public HwMemoryMappedBuffer, Resource {
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
// following ownership semantics:
//
@@ -223,8 +223,6 @@ void VulkanReadPixels::run(fvkmemory::resource_ptr<VulkanTexture> srcTexture, ui
bpp *= componentCount;
}
uint32_t const samples = srcTexture->samples > 1 ? srcTexture->samples : 1;
// Use a VkBuffer as the staging area for readback.
// 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
@@ -232,10 +230,9 @@ void VulkanReadPixels::run(fvkmemory::resource_ptr<VulkanTexture> srcTexture, ui
VkBuffer stagingBuffer = VK_NULL_HANDLE;
VkMemoryRequirements memReqs;
uint32_t const stagingSize = width * height * bpp * samples;
VkBufferCreateInfo bufferInfo = {
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size = stagingSize,
.size = width * height * bpp,
.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
@@ -374,9 +371,6 @@ void VulkanReadPixels::run(fvkmemory::resource_ptr<VulkanTexture> srcTexture, ui
uint8_t const* 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;
if (!DataReshaper::reshapeImage(&p, componentType, componentCount, srcPixels, rowPitch,
static_cast<int>(width), static_cast<int>(height), swizzle)) {
@@ -67,10 +67,7 @@ ResourceType getTypeEnum() noexcept;
std::string_view getTypeStr(ResourceType type);
inline bool isThreadSafeType(ResourceType type) {
return type == ResourceType::PROGRAM ||
type == ResourceType::FENCE ||
type == ResourceType::TIMER_QUERY ||
type == ResourceType::SYNC;
return type == ResourceType::FENCE || type == ResourceType::TIMER_QUERY;
}
struct Resource {
@@ -49,11 +49,8 @@ private:
using AllocatorImpl = HandleAllocatorVK;
template<typename D>
using requires_thread_safety = typename std::disjunction<
std::is_same<D, VulkanProgram>,
std::is_same<D, VulkanFence>,
std::is_same<D, VulkanTimerQuery>,
std::is_same<D, VulkanSync>>;
using requires_thread_safety = typename std::disjunction<std::is_same<D, VulkanFence>,
std::is_same<D, VulkanTimerQuery>>;
template<typename D, typename B, typename... ARGS>
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_D32_SFLOAT: return PixelDataType::FLOAT;
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.
// Stencil aspect overrides are handled dynamically during readback.
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_SINT:
case VK_FORMAT_R32G32B32_SFLOAT:
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
return 3;
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) {
// The handle is constructed synchronously in createFenceS.
const auto fence = handleCast<WebGPUFence>(fenceHandle);
signalFence([&] {
fence->setSubmissionState(mQueueManager.getLatestSubmissionState());
});
fence->setSubmissionState(mQueueManager.getLatestSubmissionState());
setDebugTag(fenceHandle.getId(), std::move(tag));
}
@@ -774,7 +772,11 @@ void WebGPUDriver::fenceCancel(FenceHandle fh) {
}
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) {
@@ -782,38 +784,7 @@ FenceStatus WebGPUDriver::fenceWait(FenceHandle fenceHandle, uint64_t const time
if (!fence) {
return FenceStatus::ERROR;
}
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);
return fence->wait(timeout);
}
void WebGPUDriver::destroySync(Handle<HwSync> syncHandle) {
@@ -851,17 +822,6 @@ bool WebGPUDriver::isTextureFormatMipmappable(const TextureFormat format) {
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) {
//todo
return true;
@@ -23,6 +23,49 @@ namespace filament::backend {
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
+6 -6
View File
@@ -33,15 +33,15 @@ public:
~WebGPUFence();
// Initialize the fence with a submission state from the queue manager.
void setSubmissionState(std::shared_ptr<WebGPUSubmissionState> state) {
mState = state;
}
void setSubmissionState(std::shared_ptr<WebGPUSubmissionState> state);
std::shared_ptr<WebGPUSubmissionState> getState() const {
return mState;
}
// Note that getStatus cannot be const since it's implemented with wait();
FenceStatus getStatus();
FenceStatus wait(uint64_t timeout);
private:
std::mutex mLock;
std::condition_variable mCond;
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