Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d869223c82 | ||
|
|
2a561211aa | ||
|
|
533294de9a | ||
|
|
4787e341d6 | ||
|
|
afeae22423 | ||
|
|
a2547731c0 | ||
|
|
bfbb651ecd | ||
|
|
c041970c76 | ||
|
|
3a78c78e7c | ||
|
|
8bfdc60d68 | ||
|
|
f8f2280ee3 | ||
|
|
e6e7c0325e | ||
|
|
8d16ad8882 | ||
|
|
a943b57c86 | ||
|
|
ffa2886493 | ||
|
|
415ba57272 | ||
|
|
d6369f1007 | ||
|
|
0203291b6a | ||
|
|
e85c021dec | ||
|
|
eb3d592d18 | ||
|
|
3b5776a232 | ||
|
|
afb0b39903 | ||
|
|
54f3490414 | ||
|
|
e0b2133333 | ||
|
|
9887eab8c0 | ||
|
|
562b66c715 | ||
|
|
8ec732dadc | ||
|
|
62ac2950c9 | ||
|
|
defba15173 | ||
|
|
4b4074450a | ||
|
|
84dc1b1c43 | ||
|
|
115c2cf8fe | ||
|
|
5fed291284 | ||
|
|
7212317a3b | ||
|
|
e8ae717c28 | ||
|
|
b6cb8096e6 |
+1
-1
@@ -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)
|
||||
- clang 16.0 (or more recent) (Required for Linux and macOS; see [Windows](#windows) section for MSVC support)
|
||||
- [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
|
||||
|
||||
+1
-3
@@ -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 10.15 CACHE STRING "")
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 11.0 CACHE STRING "")
|
||||
endif()
|
||||
|
||||
# ==================================================================================================
|
||||
@@ -47,8 +47,6 @@ 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)
|
||||
|
||||
@@ -6,5 +6,3 @@
|
||||
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.
|
||||
|
||||
@@ -31,7 +31,7 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.android.filament:filament-android:1.71.1'
|
||||
implementation 'com.google.android.filament:filament-android:1.71.0'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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.1'
|
||||
pod 'Filament', '~> 1.71.0'
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -220,3 +220,23 @@ 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);
|
||||
}
|
||||
|
||||
@@ -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,6 +556,22 @@ 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.
|
||||
*
|
||||
@@ -620,6 +636,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
-98
@@ -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);
|
||||
}
|
||||
|
||||
+5
-2
@@ -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) {
|
||||
fun render(frameTimeNanos: Long): Boolean {
|
||||
if (!uiHelper.isReadyToRender) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
// Allow the resource loader to finalize textures that have become ready.
|
||||
@@ -337,7 +337,9 @@ 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 {
|
||||
@@ -360,6 +362,7 @@ class ModelViewer(
|
||||
|
||||
renderer.endFrame()
|
||||
}
|
||||
return rendered
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
GROUP=com.google.android.filament
|
||||
VERSION_NAME=1.71.1
|
||||
VERSION_NAME=1.71.0
|
||||
|
||||
POM_DESCRIPTION=Real-time physically based rendering engine for Android.
|
||||
|
||||
|
||||
@@ -16,14 +16,15 @@ filament {
|
||||
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")
|
||||
}
|
||||
|
||||
// don't forget to update MainACtivity.kt when/if changing this.
|
||||
tasks.register('copyDamagedHelmetGltf', Copy) {
|
||||
// don't forget to update MainActivity.kt when/if changing this.
|
||||
tasks.register('copyModels', 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 copyDamagedHelmetGltf
|
||||
preBuild.dependsOn copyModels
|
||||
|
||||
clean.doFirst {
|
||||
delete "src/main/assets/envs"
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
"vulkan"
|
||||
],
|
||||
"models": {
|
||||
"DamagedHelmet": "helmet.glb"
|
||||
"DamagedHelmet": "DamagedHelmet.glb",
|
||||
"Fox": "Fox.glb",
|
||||
"TransmissionRoughnessTest": "TransmissionRoughnessTest.glb"
|
||||
},
|
||||
"presets": [
|
||||
{
|
||||
@@ -20,7 +22,7 @@
|
||||
"center": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
-0.8
|
||||
],
|
||||
"lookAt": [
|
||||
0,
|
||||
@@ -34,8 +36,24 @@
|
||||
}
|
||||
},
|
||||
"tolerance": {
|
||||
"maxAbsDiff": 0.1,
|
||||
"maxFailingPixelsFraction": 0.0
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -56,6 +74,43 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
@@ -73,64 +128,32 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ssao",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.ssao.enabled": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "msaa",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.msaa.enabled": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bloom",
|
||||
"name": "msaa_custom",
|
||||
"apply_presets": [
|
||||
"base",
|
||||
"tilted"
|
||||
"noisy_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true
|
||||
"view.msaa.enabled": true,
|
||||
"view.msaa.customResolve": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "aa_none",
|
||||
"name": "msaa_standard",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
"base",
|
||||
"noisy_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.antiAliasing": "NONE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "aa_fxaa",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.antiAliasing": "FXAA"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dithering_none",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.dithering": "NONE"
|
||||
"view.msaa.enabled": true,
|
||||
"view.msaa.customResolve": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "msaa_8",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
"base",
|
||||
"noisy_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.msaa.enabled": true,
|
||||
@@ -138,14 +161,13 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "taa_custom",
|
||||
"name": "fxaa",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
"base",
|
||||
"noisy_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.taa.enabled": true,
|
||||
"view.taa.feedback": 0.2,
|
||||
"view.taa.jitterPattern": "HALTON_23_X16"
|
||||
"view.antiAliasing": "FXAA"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -169,57 +191,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"name": "ssao_half_res",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
@@ -228,30 +200,11 @@
|
||||
"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"
|
||||
"base",
|
||||
"relaxed_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.ssao.enabled": true,
|
||||
@@ -299,98 +252,20 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ssr_basic",
|
||||
"name": "ssr",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
"base",
|
||||
"relaxed_tolerance"
|
||||
],
|
||||
"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"
|
||||
"base",
|
||||
"relaxed_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.screenSpaceReflections.enabled": true,
|
||||
@@ -399,89 +274,20 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bloom_levels_high",
|
||||
"name": "bloom",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
"base",
|
||||
"noisy_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true,
|
||||
"view.bloom.levels": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bloom_levels_low",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true,
|
||||
"view.bloom.levels": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bloom_resolution_high",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true,
|
||||
"view.bloom.resolution": 1024
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bloom_strength_high",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true,
|
||||
"view.bloom.strength": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bloom_strength_low",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true,
|
||||
"view.bloom.strength": 0.01
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bloom_blendMode_interpolate",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true,
|
||||
"view.bloom.blendMode": "INTERPOLATE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bloom_no_threshold",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true,
|
||||
"view.bloom.threshold": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bloom_quality_high",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true,
|
||||
"view.bloom.quality": "HIGH"
|
||||
"view.bloom.enabled": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bloom_lensflare",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
"base",
|
||||
"noisy_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true,
|
||||
@@ -491,7 +297,8 @@
|
||||
{
|
||||
"name": "bloom_lensflare_no_starburst",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
"base",
|
||||
"noisy_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true,
|
||||
@@ -502,7 +309,8 @@
|
||||
{
|
||||
"name": "bloom_lensflare_chromatic",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
"base",
|
||||
"noisy_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true,
|
||||
@@ -511,51 +319,29 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bloom_lensflare_ghosts",
|
||||
"name": "bloom_quality_high",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
"base",
|
||||
"noisy_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true,
|
||||
"view.bloom.lensFlare": true,
|
||||
"view.bloom.ghostCount": 8
|
||||
"view.bloom.quality": "HIGH"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bloom_lensflare_ghostSpacing",
|
||||
"name": "bloom_blendMode_interpolate",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
"base",
|
||||
"noisy_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.bloom.enabled": true,
|
||||
"view.bloom.lensFlare": true,
|
||||
"view.bloom.ghostSpacing": 0.8
|
||||
"view.bloom.blendMode": "INTERPOLATE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"name": "dof",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
@@ -564,43 +350,13 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dof_cocScale_high",
|
||||
"name": "dof_native_res",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.dof.enabled": true,
|
||||
"view.dof.cocScale": 2.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dof_cocScale_low",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.dof.enabled": true,
|
||||
"view.dof.cocScale": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dof_cocAspectRatio",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.dof.enabled": true,
|
||||
"view.dof.cocAspectRatio": 2.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dof_maxApertureDiameter",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"view.dof.enabled": true,
|
||||
"view.dof.maxApertureDiameter": 0.05
|
||||
"view.dof.nativeResolution": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -614,50 +370,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"name": "fog",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
@@ -666,97 +379,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"name": "vignette",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
@@ -764,46 +387,6 @@
|
||||
"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": [
|
||||
@@ -840,33 +423,6 @@
|
||||
"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": [
|
||||
@@ -889,12 +445,169 @@
|
||||
{
|
||||
"name": "shadow_vsm_highPrecision",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
"base",
|
||||
"relaxed_tolerance"
|
||||
],
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+329
-134
@@ -22,6 +22,7 @@ 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
|
||||
@@ -30,8 +31,10 @@ 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
|
||||
@@ -61,10 +64,16 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
private lateinit var choreographer: Choreographer
|
||||
private lateinit var statusTextView: TextView
|
||||
private lateinit var testResultsHeader: TextView
|
||||
private lateinit var testProgress: android.widget.ProgressBar
|
||||
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
|
||||
@@ -97,8 +106,10 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
private val frameScheduler = object : Choreographer.FrameCallback {
|
||||
override fun doFrame(frameTimeNanos: Long) {
|
||||
choreographer.postFrameCallback(this)
|
||||
modelViewer?.render(frameTimeNanos)
|
||||
validationRunner?.onFrame(frameTimeNanos)
|
||||
val rendered = modelViewer?.render(frameTimeNanos) ?: false
|
||||
if (rendered) {
|
||||
validationRunner?.onFrame(frameTimeNanos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,9 +123,22 @@ 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)
|
||||
@@ -134,13 +158,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) {}
|
||||
})
|
||||
|
||||
// Setup Run Button
|
||||
runButton.setOnClickListener {
|
||||
currentInput?.let { input ->
|
||||
// Always use the generateGoldens flag from the intent/input
|
||||
startValidation(input)
|
||||
}
|
||||
}
|
||||
// Setup Run Button (Listener is set in handleIntent based on state)
|
||||
|
||||
// Setup Load Button
|
||||
loadButton.setOnClickListener {
|
||||
@@ -150,9 +168,6 @@ 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")
|
||||
@@ -160,21 +175,15 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
|
||||
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
|
||||
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
|
||||
backendFilterContainer.visibility =
|
||||
if (backendFilterContainer.visibility == View.VISIBLE) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
||||
true
|
||||
@@ -187,11 +196,12 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
choreographer = Choreographer.getInstance()
|
||||
inputManager = ValidationInputManager(this)
|
||||
|
||||
setupFileObserver()
|
||||
handleIntent()
|
||||
}
|
||||
|
||||
private fun showLoadDialog() {
|
||||
val exportDir = getExternalFilesDir(null) ?: filesDir
|
||||
val exportDir = inputManager.getBaseDir()
|
||||
// Filter out result zips (starting with "results_") to only show test bundles
|
||||
val zips = exportDir.listFiles { _, name ->
|
||||
name.endsWith(".zip") && !name.startsWith("results_")
|
||||
@@ -222,7 +232,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
}
|
||||
|
||||
private fun showTestAdbInfo() {
|
||||
val exportDir = getExternalFilesDir(null) ?: filesDir
|
||||
val exportDir = inputManager.getBaseDir()
|
||||
val path = exportDir.absolutePath
|
||||
val isInternal = path.startsWith(filesDir.absolutePath)
|
||||
val message = StringBuilder()
|
||||
@@ -238,12 +248,11 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
|
||||
message.append("<b>--- PUSH TO DEVICE ---</b><br>")
|
||||
if (isInternal) {
|
||||
message.append("1. <tt>adb push <filename> /sdcard/Download/</tt><br>")
|
||||
message.append("2. <tt>adb shell \"run-as $packageName cp /sdcard/Download/<filename> files/\"</tt><br>")
|
||||
message.append("<tt>cat <filename> | adb shell \"run-as $packageName sh -c 'cat > files/<filename>'\"</tt><br>")
|
||||
} else {
|
||||
message.append("<tt>adb push <filename> $path/</tt><br>")
|
||||
}
|
||||
message.append("<br>Note: Use underscores instead of spaces in <filename>.")
|
||||
message.append("<br>Note: Use underscores instead of spaces in <filename>. The default bundle is <tt>default_test.zip</tt>.")
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle("Test Bundle ADB Info")
|
||||
@@ -253,7 +262,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
}
|
||||
|
||||
private fun showResultAdbInfo() {
|
||||
val exportDir = getExternalFilesDir(null) ?: filesDir
|
||||
val exportDir = inputManager.getBaseDir()
|
||||
val path = exportDir.absolutePath
|
||||
val isInternal = path.startsWith(filesDir.absolutePath)
|
||||
val message = StringBuilder()
|
||||
@@ -289,29 +298,14 @@ 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 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}"
|
||||
val input = inputManager.loadValidationInputFromZip(file)
|
||||
onInputUpdated(input)
|
||||
Log.i(TAG, "Setting header to: Test Results: ${input!!.config!!.name}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load zip", e)
|
||||
statusTextView.text = "Error: ${e.message}"
|
||||
@@ -352,23 +346,81 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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}"
|
||||
}
|
||||
onInputUpdated(inputManager.resolveConfig(intent))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to resolve config", e)
|
||||
statusTextView.text = "Error: ${e.message}"
|
||||
@@ -376,27 +428,75 @@ 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 {
|
||||
return ValidationResultManager(
|
||||
outputDir = outputDir,
|
||||
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 {
|
||||
resultsContainer.removeAllViews()
|
||||
diffImageViews.clear()
|
||||
val config = input.config ?: return
|
||||
|
||||
clearUIState();
|
||||
|
||||
// Disable UI while running
|
||||
enhancementSlider.isEnabled = false
|
||||
Log.i(TAG, "Starting validation with config: ${input.config.name}")
|
||||
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, "Output dir: ${input.outputDir.absolutePath}")
|
||||
|
||||
testResultsHeader.text = "${input.config.name}"
|
||||
testProgress.visibility = View.VISIBLE
|
||||
testProgress.progress = 0
|
||||
progressContainer.visibility = View.VISIBLE
|
||||
testSummaryText.visibility = View.GONE
|
||||
deviceInfoText.visibility = View.VISIBLE
|
||||
totalPassed = 0
|
||||
totalFailed = 0
|
||||
testProgress.reset(1)
|
||||
|
||||
resultManager = createResultManager(input.outputDir)
|
||||
|
||||
@@ -410,11 +510,13 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
// resources before we can proceed.
|
||||
validationRunner?.let {
|
||||
it.cleanup()
|
||||
modelViewer = null
|
||||
}
|
||||
|
||||
validationRunner = ValidationRunner(this, surfaceView, input.config, resultManager!!, backendFilter)
|
||||
validationRunner = ValidationRunner(this, surfaceView, config, resultManager!!, backendFilter)
|
||||
validationRunner?.callback = this
|
||||
validationRunner?.generateGoldens = input.generateGoldens
|
||||
validationRunner?.testFilter = input.testFilter
|
||||
validationRunner?.start()
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -450,14 +552,54 @@ 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)
|
||||
@@ -511,35 +653,34 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
)
|
||||
|
||||
fun addImage(label: String, bitmap: Bitmap?, isDiff: Boolean) {
|
||||
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)
|
||||
if (bitmap == null) {
|
||||
return;
|
||||
}
|
||||
val container = LinearLayout(this)
|
||||
container.orientation = LinearLayout.VERTICAL
|
||||
container.setPadding(0, 0, 10, 0)
|
||||
|
||||
val labelView = TextView(this)
|
||||
labelView.text = label
|
||||
labelView.textSize = 9f
|
||||
container.addView(labelView)
|
||||
|
||||
val iv = ImageView(this)
|
||||
iv.setImageBitmap(bitmap!!) // Use the same bitmap (or copy if needed, but same is usually fine for UI)
|
||||
iv.layoutParams = LinearLayout.LayoutParams(250, 250) // Smaller thumbnails
|
||||
iv.scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
iv.setBackgroundColor(0xFF404040.toInt())
|
||||
|
||||
if (isDiff) {
|
||||
diffImageViews.add(iv)
|
||||
applyEnhancementToView(iv, globalEnhancementFactor)
|
||||
}
|
||||
|
||||
iv.setOnClickListener {
|
||||
showImageDialog(testImages, label)
|
||||
}
|
||||
container.addView(iv)
|
||||
imagesRow.addView(container)
|
||||
}
|
||||
|
||||
addImage("Rendered", currentRenderedBitmap, false)
|
||||
@@ -553,6 +694,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
|
||||
resultContainer.addView(imagesRow)
|
||||
resultsContainer.addView(resultContainer)
|
||||
resultsContainer.post { updateTrianglePosition() }
|
||||
|
||||
// Clear current images for next test
|
||||
currentRenderedBitmap = null
|
||||
@@ -564,56 +706,94 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
|
||||
override fun onTestProgress(current: Int, total: Int) {
|
||||
runOnUiThread {
|
||||
testProgress.max = total
|
||||
testProgress.progress = current
|
||||
totalTestsCount = Math.max(1, total)
|
||||
testProgress.setMax(total)
|
||||
updateTrianglePosition()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAllTestsFinished() {
|
||||
runOnUiThread {
|
||||
testProgress.visibility = View.GONE
|
||||
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 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!"
|
||||
enhancementSlider.isEnabled = true
|
||||
Log.i(TAG, "All tests finished " + if (currentInput?.autoExport == true) "Exporting bundle" else "x")
|
||||
|
||||
if (currentInput?.autoExport == true) {
|
||||
exportTestBundleAction()
|
||||
// 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
|
||||
}
|
||||
if (currentInput?.autoExportResults == true) {
|
||||
exportTestResultsAction()
|
||||
|
||||
Log.i(TAG, "All tests finished")
|
||||
|
||||
currentInput?.let {
|
||||
if (it.generateGoldens) {
|
||||
exportTestBundleAction(it)
|
||||
} else {
|
||||
exportTestResultsAction(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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 {
|
||||
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() {
|
||||
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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,11 +871,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
if (currentIndex == -1) currentIndex = 0
|
||||
|
||||
var currentDialogBitmap: Bitmap? = null
|
||||
dialog.setOnDismissListener {
|
||||
currentDialogBitmap?.recycle()
|
||||
currentDialogBitmap = null
|
||||
}
|
||||
|
||||
var currentDialogEnhancement = globalEnhancementFactor
|
||||
|
||||
val matrix = android.graphics.Matrix()
|
||||
@@ -704,6 +879,11 @@ 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)
|
||||
@@ -741,7 +921,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
val (label, file) = availableFiles[currentIndex]
|
||||
typeView.text = label
|
||||
|
||||
currentDialogBitmap?.recycle()
|
||||
val oldBitmap = currentDialogBitmap
|
||||
currentDialogBitmap = android.graphics.BitmapFactory.decodeFile(file.absolutePath)
|
||||
|
||||
imageView.setImageBitmap(currentDialogBitmap)
|
||||
@@ -756,6 +936,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
enhancementContainer.visibility = View.GONE
|
||||
imageView.colorFilter = null
|
||||
}
|
||||
oldBitmap?.recycle()
|
||||
}
|
||||
|
||||
fun resetMatrix() {
|
||||
@@ -782,19 +963,32 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
}
|
||||
|
||||
imageView.addOnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
||||
if (left != oldLeft || top != oldTop || right != oldRight || bottom != 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 { resetMatrix() }
|
||||
btnReset.setOnClickListener {
|
||||
matrixResettable = true
|
||||
resetMatrix()
|
||||
}
|
||||
btnPrev.setOnClickListener {
|
||||
currentIndex = (currentIndex - 1 + availableFiles.size) % availableFiles.size
|
||||
matrixResettable = false
|
||||
updateView()
|
||||
}
|
||||
btnNext.setOnClickListener {
|
||||
currentIndex = (currentIndex + 1) % availableFiles.size
|
||||
matrixResettable = false
|
||||
updateView()
|
||||
}
|
||||
|
||||
@@ -807,6 +1001,7 @@ 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?) {}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+129
-124
@@ -24,16 +24,8 @@ 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 {
|
||||
@@ -41,35 +33,54 @@ 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 autoExport: Boolean = false,
|
||||
val autoExportResults: Boolean = false,
|
||||
val sourceZip: File? = null
|
||||
val sourceZip: File? = null,
|
||||
val testFilter: String? = 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)
|
||||
val autoRun = intent.getBooleanExtra("auto_run", false)
|
||||
val autoExport = intent.getBooleanExtra("auto_export", false)
|
||||
val autoExportResults = intent.getBooleanExtra("auto_export_results", 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 outputPath = intent.getStringExtra("output_path")
|
||||
|
||||
Log.i(TAG, "Resolving config with outputPath: $outputPath")
|
||||
|
||||
val baseDir = context.getExternalFilesDir(null) ?: context.filesDir
|
||||
val baseDir = getBaseDir()
|
||||
Log.i(TAG, "Base directory for resolution: ${baseDir.absolutePath}")
|
||||
|
||||
|
||||
val outputDir = if (!outputPath.isNullOrBlank()) {
|
||||
val file = File(outputPath)
|
||||
val resolved = if (file.isAbsolute) {
|
||||
@@ -77,7 +88,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")
|
||||
@@ -88,76 +99,115 @@ 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}")
|
||||
|
||||
val sourceZipFile = if (zipPath != null) {
|
||||
var sourceZipFile = if (zipPath != null) {
|
||||
val file = File(zipPath)
|
||||
if (file.isAbsolute) {
|
||||
val resolvedFile = if (file.isAbsolute) {
|
||||
file
|
||||
} else {
|
||||
File(baseDir, zipPath)
|
||||
}
|
||||
Log.i(TAG, "Resolved zipPath '$zipPath' to ${resolvedFile.absolutePath} (exists: ${resolvedFile.exists()})")
|
||||
resolvedFile
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val config = when {
|
||||
sourceZipFile != null && sourceZipFile.exists() -> loadFromZip(sourceZipFile)
|
||||
urlConfig != null -> downloadConfig(urlConfig, urlModelsBase)
|
||||
testConfigPath != null -> ConfigParser.parseFromPath(testConfigPath)
|
||||
else -> extractDefaultAssets()
|
||||
// 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.")
|
||||
}
|
||||
}
|
||||
|
||||
return@withContext ValidationInput(config, outputDir, generateGoldens, autoRun, autoExport, autoExportResults, sourceZipFile)
|
||||
val config = when {
|
||||
sourceZipFile != null && sourceZipFile.exists() -> loadFromZip(sourceZipFile)
|
||||
generateGoldens -> extractDefaultAssets()
|
||||
else -> null
|
||||
}
|
||||
|
||||
return@withContext ValidationInput(config, outputDir, generateGoldens, autoRun, sourceZipFile, testFilter)
|
||||
}
|
||||
|
||||
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
|
||||
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 {
|
||||
val baseCacheDir = context.externalCacheDir ?: context.cacheDir
|
||||
val cacheDir = File(baseCacheDir, "unzipped_validation")
|
||||
if (cacheDir.exists()) cacheDir.deleteRecursively()
|
||||
cacheDir.mkdirs()
|
||||
|
||||
Log.i(TAG, "Using cacheDir: ${cacheDir.absolutePath}")
|
||||
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()
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
|
||||
@@ -165,7 +215,7 @@ class ValidationInputManager(private val context: Context) {
|
||||
return ConfigParser.parseFromPath(configFile.absolutePath)
|
||||
}
|
||||
|
||||
private suspend fun extractDefaultAssets(): RenderTestConfig {
|
||||
suspend fun extractDefaultAssets(): RenderTestConfig {
|
||||
Log.i(TAG, "Extracting default assets...")
|
||||
val filesDir = context.getExternalFilesDir(null) ?: context.filesDir
|
||||
val assetManager = context.assets
|
||||
@@ -181,84 +231,39 @@ class ValidationInputManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Copy DamagedHelmet.glb
|
||||
// Copy models
|
||||
val modelsDir = File(filesDir, "models")
|
||||
modelsDir.mkdirs()
|
||||
val modelOut = File(modelsDir, "helmet.glb")
|
||||
|
||||
assetManager.open("models/helmet.glb").use { input ->
|
||||
FileOutputStream(modelOut).use { output ->
|
||||
input.copyTo(output)
|
||||
assetManager.list("models")?.forEach { modelFileName ->
|
||||
val modelOut = File(modelsDir, modelFileName)
|
||||
assetManager.open("models/$modelFileName").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")
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-19
@@ -35,6 +35,7 @@ data class ValidationResult(
|
||||
class ValidationResultManager(
|
||||
private val outputDir: File,
|
||||
private val deviceName: String,
|
||||
private val deviceHardware: String,
|
||||
private val deviceCodeName: String,
|
||||
private val androidVersion: String,
|
||||
private val androidBuildNumber: String
|
||||
@@ -44,7 +45,7 @@ class ValidationResultManager(
|
||||
private const val TAG = "ValidationResultManager"
|
||||
}
|
||||
|
||||
private val results = mutableListOf<ValidationResult>()
|
||||
private val results = java.util.concurrent.CopyOnWriteArrayList<ValidationResult>()
|
||||
private val gpuDriverInfos = JSONObject()
|
||||
|
||||
init {
|
||||
@@ -88,18 +89,19 @@ class ValidationResultManager(
|
||||
* - input test bundle (as nested zip), if provided
|
||||
* - diff images (if any failure)
|
||||
*/
|
||||
fun exportTestResults(sourceZip: File?, timestamp: String): File? {
|
||||
fun exportTestResults(sourceZip: File?, config: RenderTestConfig, timestamp: String): File? {
|
||||
// Safe parent dir resolution
|
||||
val parentDir = outputDir.canonicalFile.parentFile ?: outputDir.parentFile
|
||||
if (parentDir == null) return null
|
||||
|
||||
val resultZipName = "results_$timestamp"
|
||||
val zipFile = File(parentDir, "$resultZipName.zip")
|
||||
val tempZipFile = File(parentDir, "$resultZipName.zip.tmp")
|
||||
val finalZipFile = File(parentDir, "$resultZipName.zip")
|
||||
|
||||
Log.i(TAG, "Exporting results to ${zipFile.absolutePath}")
|
||||
Log.i(TAG, "Exporting results to ${tempZipFile.absolutePath}")
|
||||
|
||||
try {
|
||||
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
|
||||
ZipOutputStream(FileOutputStream(tempZipFile)).use { zos ->
|
||||
// 1. Add results.json
|
||||
val resultsJson = File(outputDir, "results.json")
|
||||
if (resultsJson.exists()) {
|
||||
@@ -108,24 +110,28 @@ class ValidationResultManager(
|
||||
zos.closeEntry()
|
||||
}
|
||||
|
||||
// 2. Add source zip if exists
|
||||
// 2. Add test bundle zip (fallback to sourceZip)
|
||||
if (sourceZip != null && sourceZip.exists()) {
|
||||
zos.putNextEntry(ZipEntry(sourceZip.name))
|
||||
zos.putNextEntry(ZipEntry("bundle.zip"))
|
||||
sourceZip.inputStream().use { it.copyTo(zos) }
|
||||
zos.closeEntry()
|
||||
}
|
||||
|
||||
// 3. Add images (only rendered images, exclude diffs)
|
||||
// 3. Add images (exclude diffs, frontend computes them)
|
||||
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()
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "Exported results to ${zipFile.absolutePath}")
|
||||
return zipFile
|
||||
|
||||
tempZipFile.renameTo(finalZipFile)
|
||||
|
||||
Log.i(TAG, "Exported results to ${finalZipFile.absolutePath}")
|
||||
return finalZipFile
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to export results", e)
|
||||
tempZipFile.delete()
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -144,7 +150,7 @@ class ValidationResultManager(
|
||||
* goldens/
|
||||
* test_result.png
|
||||
*/
|
||||
fun exportTestBundle(config: RenderTestConfig, timestamp: String): File? {
|
||||
fun exportTestBundle(config: RenderTestConfig, timestamp: String, bundleNameOverride: String? = null): File? {
|
||||
Log.i(TAG, "Starting exportTestBundle for ${config.name} at $timestamp")
|
||||
Log.i(TAG, "OutputDir: ${outputDir.absolutePath}")
|
||||
|
||||
@@ -155,8 +161,7 @@ class ValidationResultManager(
|
||||
}
|
||||
Log.i(TAG, "Using parentDir for export: ${parentDir.absolutePath}")
|
||||
|
||||
val testNameWithTimestamp = "${config.name}_$timestamp"
|
||||
val exportNameNoSpaces = testNameWithTimestamp.replace(" ", "_")
|
||||
val exportNameNoSpaces = bundleNameOverride ?: config.name.lowercase(java.util.Locale.US).replace(" ", "_")
|
||||
|
||||
val exportDir = File(parentDir, "export_temp_$timestamp")
|
||||
|
||||
@@ -204,7 +209,7 @@ class ValidationResultManager(
|
||||
// 3. Create modified config JSON
|
||||
Log.i(TAG, "Creating config.json...")
|
||||
val newConfigJson = JSONObject()
|
||||
newConfigJson.put("name", testNameWithTimestamp) // Keep spaces in JSON name
|
||||
newConfigJson.put("name", config.name) // Keep spaces in JSON name
|
||||
|
||||
// Reconstruct backends
|
||||
val backendsArray = JSONArray()
|
||||
@@ -251,10 +256,11 @@ class ValidationResultManager(
|
||||
File(rootDir, "config.json").writeText(newConfigJson.toString(4))
|
||||
|
||||
// 4. Zip it
|
||||
val zipFile = File(parentDir, "$exportNameNoSpaces.zip")
|
||||
Log.i(TAG, "Zipping to ${zipFile.absolutePath}...")
|
||||
val tempZipFile = File(parentDir, "$exportNameNoSpaces.zip.tmp")
|
||||
val finalZipFile = File(parentDir, "$exportNameNoSpaces.zip")
|
||||
Log.i(TAG, "Zipping to ${tempZipFile.absolutePath}...")
|
||||
|
||||
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
|
||||
ZipOutputStream(FileOutputStream(tempZipFile)).use { zos ->
|
||||
rootDir.walkTopDown().forEach { file ->
|
||||
if (file.isFile) {
|
||||
val entryName = file.relativeTo(exportDir).path
|
||||
@@ -265,15 +271,18 @@ class ValidationResultManager(
|
||||
}
|
||||
}
|
||||
|
||||
tempZipFile.renameTo(finalZipFile)
|
||||
|
||||
// Cleanup temp dir
|
||||
exportDir.deleteRecursively()
|
||||
|
||||
Log.i(TAG, "Exported test bundle to ${zipFile.absolutePath}")
|
||||
return zipFile
|
||||
Log.i(TAG, "Exported test bundle to ${finalZipFile.absolutePath}")
|
||||
return finalZipFile
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to export test bundle", e)
|
||||
exportDir.deleteRecursively()
|
||||
File(parentDir, "$exportNameNoSpaces.zip.tmp").delete()
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -285,6 +294,7 @@ class ValidationResultManager(
|
||||
metadataObject.put("gpu_driver_info", gpuDriverInfos)
|
||||
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 ?: "")
|
||||
|
||||
+50
-5
@@ -65,7 +65,22 @@ class ValidationRunner(
|
||||
|
||||
// Sort by backend then model. This tries to minimize the number of times we need to
|
||||
// recreate the Engine, and then the ModelViewer.
|
||||
expanded.sortedWith(compareBy({ it.first.backend }, { it.second }))
|
||||
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 {
|
||||
@@ -86,6 +101,7 @@ class ValidationRunner(
|
||||
|
||||
var callback: Callback? = null
|
||||
var generateGoldens: Boolean = false
|
||||
var testFilter: String? = null
|
||||
|
||||
fun start() {
|
||||
if (sortedTests.isEmpty()) {
|
||||
@@ -215,11 +231,12 @@ class ValidationRunner(
|
||||
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)
|
||||
|
||||
applyAnimation(mv, currentTestConfig!!.rendering)
|
||||
|
||||
frameCounter++
|
||||
if (engine.shouldClose()) {
|
||||
Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)")
|
||||
@@ -233,6 +250,30 @@ class ValidationRunner(
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -243,13 +284,19 @@ class ValidationRunner(
|
||||
currentEngine = AutomationEngine(fullSpec)
|
||||
val options = AutomationEngine.Options()
|
||||
options.sleepDuration = 0.0f // Minimal sleep, let frames drive it
|
||||
options.minFrameCount = 1 // Ensure some frames pass
|
||||
options.minFrameCount = 15 // Ensure enough frames pass for temporal accumulation
|
||||
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
|
||||
}
|
||||
@@ -297,8 +344,6 @@ 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 {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:clipChildren="false"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:paddingTop="20dp"
|
||||
@@ -162,24 +163,57 @@
|
||||
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:textSize="14sp"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="12dp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/test_progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:gravity="center"
|
||||
<FrameLayout
|
||||
android:id="@+id/progress_container"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:clipChildren="false"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.filament.validation.TestProgressBar
|
||||
android:id="@+id/test_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/progress_triangle"
|
||||
android:layout_width="8dp"
|
||||
android:layout_height="8dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginTop="4dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_triangle_up" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/test_summary_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="10sp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/device_info_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="10sp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroll_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/controls_container"
|
||||
|
||||
@@ -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)</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><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
@@ -181,7 +181,7 @@ important for <code>matc</code> (material compiler).</p>
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.android.filament:filament-android:1.70.2'
|
||||
implementation 'com.google.android.filament:filament-android:1.71.0'
|
||||
}
|
||||
</code></pre>
|
||||
<p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p>
|
||||
@@ -195,7 +195,7 @@ dependencies {
|
||||
</div>
|
||||
<h3 id="ios"><a class="header" href="#ios">iOS</a></h3>
|
||||
<p>iOS projects can use CocoaPods to install the latest release:</p>
|
||||
<pre><code class="language-shell">pod 'Filament', '~> 1.70.2'
|
||||
<pre><code class="language-shell">pod 'Filament', '~> 1.71.0'
|
||||
</code></pre>
|
||||
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
|
||||
<ul>
|
||||
|
||||
@@ -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_fl0.filamat'], () => {
|
||||
Filament.init(['../../web/assets/cube_fl0/nonlit.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_fl0.filamat');
|
||||
const mat = engine.createMaterial('../../web/assets/cube_fl0/nonlit.filamat');
|
||||
const matinst = mat.getDefaultInstance();
|
||||
Filament.RenderableManager.Builder(1)
|
||||
.boundingBox({ center: [-1, -1, -1], halfExtent: [1, 1, 1] })
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
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
File diff suppressed because one or more lines are too long
Binary file not shown.
+51
-43
@@ -313,17 +313,18 @@ if (NOT FILAMENT_DISABLE_GTAO)
|
||||
)
|
||||
endif()
|
||||
|
||||
set(MATERIAL_FL0_SRCS
|
||||
src/materials/defaultMaterial.mat
|
||||
src/materials/skybox.mat
|
||||
)
|
||||
|
||||
set(MATERIAL_MULTIVIEW_SRCS
|
||||
src/materials/clearDepth.mat
|
||||
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
|
||||
)
|
||||
|
||||
# ==================================================================================================
|
||||
# Configuration
|
||||
# ==================================================================================================
|
||||
@@ -343,11 +344,6 @@ 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)
|
||||
@@ -397,6 +393,27 @@ 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)
|
||||
@@ -419,44 +436,35 @@ foreach(mat_dir ${MATERIAL_DIRS})
|
||||
|
||||
# --- Base material ---
|
||||
set(output_path "${MATERIAL_DIR}/${localname}.filamat")
|
||||
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})
|
||||
set(MATC_CUSTOM_FLAGS "${MATC_BASE_FLAGS}")
|
||||
set(COMMENT_STR "Compiling material ${fullname}")
|
||||
|
||||
# --- 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})
|
||||
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)")
|
||||
endif()
|
||||
|
||||
compile_material(${output_path} ${fullname} MATC_CUSTOM_FLAGS "${COMMENT_STR}")
|
||||
|
||||
# --- Multiview variant ---
|
||||
list(FIND MATERIAL_MULTIVIEW_SRCS ${mat_src} index)
|
||||
if (${index} GREATER -1 AND FILAMENT_ENABLE_MULTIVIEW)
|
||||
check_in_list(MATERIAL_MULTIVIEW_SRCS ${mat_src} is_multiview)
|
||||
if (is_multiview AND FILAMENT_ENABLE_MULTIVIEW)
|
||||
string(REGEX REPLACE "[.]filamat$" "_multiview.filamat" output_path_multiview ${output_path})
|
||||
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})
|
||||
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}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
|
||||
@@ -214,6 +214,25 @@ 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
|
||||
// --------------------------------------------------------------------------------------------
|
||||
@@ -253,6 +272,9 @@ private:
|
||||
std::condition_variable mServiceThreadCondition;
|
||||
std::vector<std::tuple<CallbackHandler*, CallbackHandler::Callback, void*>> mServiceThreadCallbackQueue;
|
||||
bool mExitRequested = false;
|
||||
|
||||
std::condition_variable mFenceCondition;
|
||||
std::mutex mFenceMutex;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -114,9 +114,7 @@ void logMTLCommandBufferError(MTLCommandBufferError error) {
|
||||
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorOutOfMemory)
|
||||
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorInvalidResource)
|
||||
|
||||
if (@available(macOS 11.0, *)) {
|
||||
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorMemoryless)
|
||||
}
|
||||
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorMemoryless)
|
||||
|
||||
if (@available(iOS 15.0, macOS 12.0, *)) {
|
||||
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorStackOverflow)
|
||||
@@ -143,12 +141,10 @@ id<MTLCommandBuffer> getPendingCommandBuffer(MetalContext* context) {
|
||||
context->latestCompletedCommandBufferId = thisCommandBufferId;
|
||||
|
||||
auto errorCode = (MTLCommandBufferError)buffer.error.code;
|
||||
if (@available(macOS 11.0, *)) {
|
||||
if (errorCode == MTLCommandBufferErrorMemoryless) {
|
||||
LOG(WARNING) << "Metal: memoryless geometry limit reached. Continuing with private "
|
||||
"storage mode.";
|
||||
context->memorylessLimitsReached = true;
|
||||
}
|
||||
if (errorCode == MTLCommandBufferErrorMemoryless) {
|
||||
LOG(WARNING) << "Metal: memoryless geometry limit reached. Continuing with private "
|
||||
"storage mode.";
|
||||
context->memorylessLimitsReached = true;
|
||||
}
|
||||
|
||||
if (UTILS_UNLIKELY(errorCode != MTLCommandBufferErrorNone)) {
|
||||
|
||||
@@ -1284,11 +1284,8 @@ 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.
|
||||
if (@available(macOS 11.0, *)) {
|
||||
return mContext->highestSupportedGpuFamily.apple >= 1 &&
|
||||
mContext->device.readWriteTextureSupport;
|
||||
}
|
||||
return false;
|
||||
return mContext->highestSupportedGpuFamily.apple >= 1 &&
|
||||
mContext->device.readWriteTextureSupport;
|
||||
}
|
||||
|
||||
bool MetalDriver::isFrameBufferFetchMultiSampleSupported() {
|
||||
|
||||
@@ -148,9 +148,7 @@ 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 (@available(macOS 11, *)) {
|
||||
if (type == PixelDataType::USHORT_565) return MTLPixelFormatB5G6R5Unorm;
|
||||
}
|
||||
if (type == PixelDataType::USHORT_565) return MTLPixelFormatB5G6R5Unorm;
|
||||
|
||||
#define CONVERT(FORMAT, TYPE, MTL) \
|
||||
if (PixelDataFormat::FORMAT == format && PixelDataType::TYPE == type) return MTLPixelFormat ## MTL;
|
||||
@@ -205,33 +203,31 @@ inline MTLPixelFormat getMetalFormatLinear(MTLPixelFormat format) {
|
||||
#endif
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -98,14 +98,12 @@ 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, *)) {
|
||||
@@ -115,18 +113,16 @@ 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, macOS 11.0, *)) {
|
||||
if (@available(iOS 13, *)) {
|
||||
switch (format) {
|
||||
case TextureFormat::RGBA_ASTC_4x4: return MTLPixelFormatASTC_4x4_HDR;
|
||||
case TextureFormat::RGBA_ASTC_5x4: return MTLPixelFormatASTC_5x4_HDR;
|
||||
@@ -144,7 +140,7 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
|
||||
case TextureFormat::RGBA_ASTC_12x12: return MTLPixelFormatASTC_12x12_HDR;
|
||||
default: break;
|
||||
}
|
||||
} else if (@available(macOS 11.0, *)) {
|
||||
} else {
|
||||
switch (format) {
|
||||
case TextureFormat::RGBA_ASTC_4x4: return MTLPixelFormatASTC_4x4_LDR;
|
||||
case TextureFormat::RGBA_ASTC_5x4: return MTLPixelFormatASTC_5x4_LDR;
|
||||
@@ -167,7 +163,6 @@ 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;
|
||||
@@ -181,7 +176,6 @@ 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,8 +478,6 @@ 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,6 +20,7 @@
|
||||
#include "MetalEnums.h"
|
||||
#include "MetalUtils.h"
|
||||
#include "MetalBufferPool.h"
|
||||
#include "MetalDriver.h"
|
||||
|
||||
#include <filament/SwapChain.h>
|
||||
|
||||
@@ -183,9 +184,7 @@ id<MTLTexture> MetalSwapChain::createMultisampledTexture(MetalContext const& con
|
||||
descriptor.resourceOptions = MTLResourceStorageModePrivate;
|
||||
|
||||
if (context.supportsMemorylessRenderTargets) {
|
||||
if (@available(macOS 11.0, *)) {
|
||||
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
|
||||
}
|
||||
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
|
||||
}
|
||||
|
||||
return [context.device newTextureWithDescriptor:descriptor];
|
||||
@@ -1375,9 +1374,7 @@ id<MTLTexture> MetalRenderTarget::createMultisampledTexture(MTLPixelFormat forma
|
||||
descriptor.resourceOptions = MTLResourceStorageModePrivate;
|
||||
|
||||
if (context->supportsMemorylessRenderTargets) {
|
||||
if (@available(macOS 11.0, *)) {
|
||||
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
|
||||
}
|
||||
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
|
||||
}
|
||||
|
||||
return [context->device newTextureWithDescriptor:descriptor];
|
||||
@@ -1404,12 +1401,13 @@ 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()) {
|
||||
std::lock_guard<std::mutex> guard(s->mutex);
|
||||
s->status = FenceStatus::CONDITION_SATISFIED;
|
||||
s->cv.notify_all();
|
||||
driver->signalFence([&] {
|
||||
s->status = FenceStatus::CONDITION_SATISFIED;
|
||||
});
|
||||
}
|
||||
}];
|
||||
}
|
||||
@@ -1422,24 +1420,29 @@ void MetalFence::onSignal(MetalFenceSignalBlock block) {
|
||||
FenceStatus MetalFence::wait(uint64_t timeoutNs) {
|
||||
if (@available(iOS 12, *)) {
|
||||
using ns = std::chrono::nanoseconds;
|
||||
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;
|
||||
FenceStatus result = FenceStatus::TIMEOUT_EXPIRED;
|
||||
auto predicate = [&] {
|
||||
if (state->status != FenceStatus::TIMEOUT_EXPIRED) {
|
||||
result = state->status;
|
||||
return true;
|
||||
}
|
||||
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 state->status;
|
||||
return result;
|
||||
}
|
||||
return FenceStatus::ERROR;
|
||||
}
|
||||
|
||||
void MetalFence::cancel() {
|
||||
std::unique_lock guard(state->mutex);
|
||||
state->status = FenceStatus::ERROR;
|
||||
state->cv.notify_all();
|
||||
context.driver->signalFence([&] {
|
||||
state->status = FenceStatus::ERROR;
|
||||
});
|
||||
}
|
||||
|
||||
MetalDescriptorSetLayout::MetalDescriptorSetLayout(DescriptorSetLayout&& l) noexcept
|
||||
|
||||
@@ -2074,9 +2074,9 @@ void OpenGLDriver::createFenceR(Handle<HwFence> fh, ImmutableCString&& tag) {
|
||||
assert_invariant(f->state);
|
||||
|
||||
if (mPlatform.canCreateFence()) {
|
||||
std::lock_guard const lock(f->state->lock);
|
||||
f->fence = mPlatform.createFence();
|
||||
f->state->cond.notify_all();
|
||||
signalFence([&] {
|
||||
f->fence = mPlatform.createFence();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2090,11 +2090,11 @@ void OpenGLDriver::createFenceR(Handle<HwFence> fh, ImmutableCString&& tag) {
|
||||
// This is the case where we need to use OpenGL fences, as soon as we return, the user
|
||||
// is allowed to destroy the fence, so we need to keep a reference to the internal state.
|
||||
std::weak_ptr<GLFence::State> const weak = f->state;
|
||||
whenGpuCommandsComplete([weak] {
|
||||
whenGpuCommandsComplete([weak, this] {
|
||||
if (auto const state = weak.lock()) {
|
||||
std::lock_guard const lock(state->lock);
|
||||
state->status = FenceStatus::CONDITION_SATISFIED;
|
||||
state->cond.notify_all();
|
||||
signalFence([&] {
|
||||
state->status = FenceStatus::CONDITION_SATISFIED;
|
||||
});
|
||||
}
|
||||
});
|
||||
#else
|
||||
@@ -2633,9 +2633,9 @@ void OpenGLDriver::fenceCancel(Handle<HwFence> fh) {
|
||||
GLFence const* const f = handle_cast<GLFence*>(fh);
|
||||
assert_invariant(f->state);
|
||||
|
||||
std::lock_guard const lock(f->state->lock);
|
||||
f->state->status = FenceStatus::ERROR;
|
||||
f->state->cond.notify_all();
|
||||
signalFence([&] {
|
||||
f->state->status = FenceStatus::ERROR;
|
||||
});
|
||||
}
|
||||
|
||||
FenceStatus OpenGLDriver::getFenceStatus(Handle<HwFence> fh) {
|
||||
@@ -2661,22 +2661,18 @@ FenceStatus OpenGLDriver::fenceWait(FenceHandle fh, uint64_t const timeout) {
|
||||
// `f` is not supposed to become invalid while we wait.
|
||||
|
||||
if (mPlatform.canCreateFence()) {
|
||||
std::unique_lock lock(f->state->lock);
|
||||
// we've been called before the fence was created asynchronously,
|
||||
// so we need to wait for that, before using the real fence.
|
||||
// By construction, "f" can't be destroyed while we wait, because its
|
||||
// construction call is in the queue and a destroy call will have to come later.
|
||||
waitForFence([f] {
|
||||
return f->fence != nullptr;
|
||||
}, until);
|
||||
if (f->fence == nullptr) {
|
||||
// we've been called before the fence was created asynchronously,
|
||||
// so we need to wait for that, before using the real fence.
|
||||
// By construction, "f" can't be destroyed while we wait, because its
|
||||
// construction call is in the queue and a destroy call will have to come later.
|
||||
f->state->cond.wait_until(lock, until, [f] {
|
||||
return f->fence != nullptr;
|
||||
});
|
||||
if (f->fence == nullptr) {
|
||||
// the only possible choice here is that we timed out
|
||||
assert_invariant(f->state->status == FenceStatus::TIMEOUT_EXPIRED);
|
||||
return FenceStatus::TIMEOUT_EXPIRED;
|
||||
}
|
||||
// the only possible choice here is that we timed out
|
||||
assert_invariant(f->state->status == FenceStatus::TIMEOUT_EXPIRED);
|
||||
return FenceStatus::TIMEOUT_EXPIRED;
|
||||
}
|
||||
lock.unlock();
|
||||
// here we know that we have the platform fence
|
||||
assert_invariant(f->fence);
|
||||
return mPlatform.waitFence(f->fence, timeout);
|
||||
@@ -2689,11 +2685,15 @@ FenceStatus OpenGLDriver::fenceWait(FenceHandle fh, uint64_t const timeout) {
|
||||
|
||||
// This is the case where we need to use OpenGL fences
|
||||
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
|
||||
std::unique_lock lock(f->state->lock);
|
||||
f->state->cond.wait_until(lock, until, [f] {
|
||||
return f->state->status != FenceStatus::TIMEOUT_EXPIRED;
|
||||
});
|
||||
return f->state->status;
|
||||
FenceStatus result = FenceStatus::TIMEOUT_EXPIRED;
|
||||
waitForFence([&] {
|
||||
if (f->state->status != FenceStatus::TIMEOUT_EXPIRED) {
|
||||
result = f->state->status;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, until);
|
||||
return result;
|
||||
#else
|
||||
return FenceStatus::ERROR;
|
||||
#endif
|
||||
|
||||
@@ -197,8 +197,6 @@ 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>() };
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
#include "VulkanAsyncHandles.h"
|
||||
|
||||
#include "VulkanConstants.h"
|
||||
|
||||
#include "vulkan/utils/Spirv.h"
|
||||
|
||||
#include <backend/DriverEnums.h>
|
||||
|
||||
#include <utils/debug.h>
|
||||
@@ -29,6 +33,154 @@ 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;
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
#include "backend/Platform.h"
|
||||
|
||||
#include "vulkan/memory/Resource.h"
|
||||
#include "vulkan/utils/StaticVector.h"
|
||||
|
||||
#include <backend/Program.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
@@ -35,6 +38,123 @@
|
||||
|
||||
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) { }
|
||||
@@ -84,39 +204,21 @@ 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::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
|
||||
std::pair<std::shared_ptr<VulkanCmdFence>, bool> getStatus() const {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -34,10 +34,11 @@
|
||||
#include <utils/Mutex.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include <chrono>
|
||||
#include <list>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
namespace filament::backend {
|
||||
|
||||
@@ -72,7 +73,11 @@ struct VulkanCommandBuffer {
|
||||
|
||||
~VulkanCommandBuffer();
|
||||
|
||||
inline void acquire(fvkmemory::resource_ptr<fvkmemory::Resource> resource) {
|
||||
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) {
|
||||
mResources.push_back(resource);
|
||||
}
|
||||
|
||||
@@ -115,6 +120,9 @@ struct VulkanCommandBuffer {
|
||||
}
|
||||
|
||||
private:
|
||||
using HeldResource = std::variant<fvkmemory::resource_ptr<Resource>,
|
||||
fvkmemory::resource_ptr<ThreadSafeResource>>;
|
||||
|
||||
static uint32_t sAgeCounter;
|
||||
|
||||
VulkanContext const& mContext;
|
||||
@@ -129,7 +137,7 @@ private:
|
||||
fvkmemory::resource_ptr<VulkanSemaphore> mSubmission;
|
||||
VkFence mFence;
|
||||
std::shared_ptr<VulkanCmdFence> mFenceStatus;
|
||||
std::vector<fvkmemory::resource_ptr<Resource>> mResources;
|
||||
std::vector<HeldResource> mResources;
|
||||
uint32_t mAge;
|
||||
};
|
||||
|
||||
|
||||
@@ -1008,7 +1008,9 @@ 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);
|
||||
fence->setFence(cmdbuf->getFenceStatus());
|
||||
signalFence([&] {
|
||||
fence->setFence(cmdbuf->getFenceStatus());
|
||||
});
|
||||
mResourceManager.associateHandle(fh.getId(), std::move(tag));
|
||||
}
|
||||
|
||||
@@ -1459,7 +1461,9 @@ 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);
|
||||
fence->cancel();
|
||||
signalFence([&] {
|
||||
fence->cancel();
|
||||
});
|
||||
}
|
||||
|
||||
FenceStatus VulkanDriver::getFenceStatus(Handle<HwFence> const fh) {
|
||||
@@ -1486,7 +1490,18 @@ FenceStatus VulkanDriver::fenceWait(FenceHandle const fh, uint64_t const timeout
|
||||
until = now + nanoseconds(timeout);
|
||||
}
|
||||
|
||||
auto const [cmdfence, canceled] = fence->wait(until);
|
||||
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);
|
||||
|
||||
if (!cmdfence || canceled) {
|
||||
return canceled ? FenceStatus::ERROR : FenceStatus::TIMEOUT_EXPIRED;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
#include "VulkanHandles.h"
|
||||
|
||||
#include "VulkanConstants.h"
|
||||
|
||||
// TODO: remove this by moving DebugUtils out of VulkanDriver
|
||||
#include "VulkanDriver.h"
|
||||
|
||||
@@ -26,7 +24,6 @@
|
||||
#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>
|
||||
|
||||
@@ -86,17 +83,6 @@ 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;
|
||||
@@ -264,139 +250,6 @@ 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),
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
#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>
|
||||
@@ -241,118 +238,6 @@ 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,6 +223,8 @@ 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
|
||||
@@ -230,9 +232,10 @@ 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 = width * height * bpp,
|
||||
.size = stagingSize,
|
||||
.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
|
||||
@@ -371,6 +374,9 @@ 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,7 +67,10 @@ ResourceType getTypeEnum() noexcept;
|
||||
std::string_view getTypeStr(ResourceType type);
|
||||
|
||||
inline bool isThreadSafeType(ResourceType type) {
|
||||
return type == ResourceType::FENCE || type == ResourceType::TIMER_QUERY;
|
||||
return type == ResourceType::PROGRAM ||
|
||||
type == ResourceType::FENCE ||
|
||||
type == ResourceType::TIMER_QUERY ||
|
||||
type == ResourceType::SYNC;
|
||||
}
|
||||
|
||||
struct Resource {
|
||||
|
||||
@@ -49,8 +49,11 @@ private:
|
||||
using AllocatorImpl = HandleAllocatorVK;
|
||||
|
||||
template<typename D>
|
||||
using requires_thread_safety = typename std::disjunction<std::is_same<D, VulkanFence>,
|
||||
std::is_same<D, VulkanTimerQuery>>;
|
||||
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>>;
|
||||
|
||||
template<typename D, typename B, typename... ARGS>
|
||||
inline D* construct(Handle<B> const& handle, ARGS&&... args) noexcept {
|
||||
|
||||
@@ -770,6 +770,7 @@ 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;
|
||||
@@ -848,6 +849,8 @@ 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:
|
||||
|
||||
@@ -702,7 +702,9 @@ 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);
|
||||
fence->setSubmissionState(mQueueManager.getLatestSubmissionState());
|
||||
signalFence([&] {
|
||||
fence->setSubmissionState(mQueueManager.getLatestSubmissionState());
|
||||
});
|
||||
setDebugTag(fenceHandle.getId(), std::move(tag));
|
||||
}
|
||||
|
||||
@@ -772,11 +774,7 @@ void WebGPUDriver::fenceCancel(FenceHandle fh) {
|
||||
}
|
||||
|
||||
FenceStatus WebGPUDriver::getFenceStatus(Handle<HwFence> fenceHandle) {
|
||||
const auto fence = handleCast<WebGPUFence>(fenceHandle);
|
||||
if (!fence) {
|
||||
return FenceStatus::ERROR;
|
||||
}
|
||||
return fence->getStatus();
|
||||
return fenceWait(fenceHandle, 0);
|
||||
}
|
||||
|
||||
FenceStatus WebGPUDriver::fenceWait(FenceHandle fenceHandle, uint64_t const timeout) {
|
||||
@@ -784,7 +782,38 @@ FenceStatus WebGPUDriver::fenceWait(FenceHandle fenceHandle, uint64_t const time
|
||||
if (!fence) {
|
||||
return FenceStatus::ERROR;
|
||||
}
|
||||
return fence->wait(timeout);
|
||||
|
||||
using namespace std::chrono;
|
||||
auto now = steady_clock::now();
|
||||
steady_clock::time_point until = steady_clock::time_point::max();
|
||||
|
||||
using TimeoutType = decltype(timeout);
|
||||
constexpr TimeoutType maxTimeout = std::numeric_limits<TimeoutType>::max();
|
||||
constexpr nanoseconds maxNano = nanoseconds::max();
|
||||
if (timeout < maxNano.count() && timeout < maxTimeout && // Need to account for overflow
|
||||
now <= steady_clock::time_point::max() - nanoseconds(timeout)) {
|
||||
until = now + nanoseconds(timeout);
|
||||
}
|
||||
|
||||
std::shared_ptr<WebGPUSubmissionState> state;
|
||||
bool const success = waitForFence([&] {
|
||||
state = fence->getState();
|
||||
return bool(state);
|
||||
}, until);
|
||||
|
||||
if (!success) {
|
||||
return FenceStatus::TIMEOUT_EXPIRED;
|
||||
}
|
||||
|
||||
auto duration_ns = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(steady_clock::now() - now)
|
||||
.count());
|
||||
// In the unlikely event that duration_ns is too close to timeout, we just assume the leftover
|
||||
// timeout is 0.
|
||||
if (timeout == 0 || duration_ns > timeout) {
|
||||
duration_ns = timeout;
|
||||
}
|
||||
return state->waitForCompletion(timeout - duration_ns);
|
||||
}
|
||||
|
||||
void WebGPUDriver::destroySync(Handle<HwSync> syncHandle) {
|
||||
|
||||
@@ -23,49 +23,6 @@ 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
|
||||
|
||||
@@ -33,15 +33,15 @@ public:
|
||||
~WebGPUFence();
|
||||
|
||||
// Initialize the fence with a submission state from the queue manager.
|
||||
void setSubmissionState(std::shared_ptr<WebGPUSubmissionState> state);
|
||||
void setSubmissionState(std::shared_ptr<WebGPUSubmissionState> state) {
|
||||
mState = state;
|
||||
}
|
||||
|
||||
// Note that getStatus cannot be const since it's implemented with wait();
|
||||
FenceStatus getStatus();
|
||||
FenceStatus wait(uint64_t timeout);
|
||||
std::shared_ptr<WebGPUSubmissionState> getState() const {
|
||||
return mState;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mLock;
|
||||
std::condition_variable mCond;
|
||||
std::shared_ptr<WebGPUSubmissionState> mState;
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <filament/ToneMapper.h>
|
||||
|
||||
#include <utils/compiler.h>
|
||||
#include <utils/FixedCapacityVector.h>
|
||||
|
||||
#include <math/mathfwd.h>
|
||||
|
||||
@@ -461,6 +462,22 @@ public:
|
||||
*/
|
||||
Builder& curves(math::float3 shadowGamma, math::float3 midPoint, math::float3 highlightScale) noexcept;
|
||||
|
||||
/**
|
||||
* 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 dimension does not need to be a power of two, but must be non-zero.
|
||||
* The values are always interpolated (trilinear) because the input color from previous steps is continuous.
|
||||
* The dimension doesn't need to match dimensions().
|
||||
* If the dimension is 0 or the data is empty, the custom LUT is skipped (ignored).
|
||||
*
|
||||
* @param data FixedCapacityVector containing the custom LUT data (3D array of float3).
|
||||
* @param dimension Dimension of the custom LUT.
|
||||
*
|
||||
* @return This Builder, for chaining calls
|
||||
*/
|
||||
Builder& customLut(utils::FixedCapacityVector<math::float3> data, uint8_t dimension) noexcept;
|
||||
|
||||
/**
|
||||
* Sets the output color space for this ColorGrading object. After all color grading steps
|
||||
* have been applied, the final color will be converted in the desired color space.
|
||||
|
||||
@@ -139,8 +139,10 @@ public:
|
||||
Builder& package(const void* UTILS_NONNULL payload, size_t size);
|
||||
|
||||
template<typename T>
|
||||
using is_supported_constant_parameter_t =
|
||||
MaterialInstance::is_supported_constant_parameter_t<T>;
|
||||
using is_supported_constant_parameter_t = std::enable_if_t<
|
||||
std::is_same_v<int32_t, T> ||
|
||||
std::is_same_v<float, T> ||
|
||||
std::is_same_v<bool, T>>;
|
||||
|
||||
/**
|
||||
* Specialize a constant parameter specified in the material definition with a concrete
|
||||
@@ -241,18 +243,11 @@ public:
|
||||
* for stereoscopic rendering. If an application is not planning to render in stereo, this bit
|
||||
* should be turned off to avoid unnecessary material compilations.
|
||||
*
|
||||
* Note that it is possible to override specialization constants on a per-MaterialInstance basis
|
||||
* (@see 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.
|
||||
*
|
||||
* @param priority Which priority queue to use, LOW or HIGH.
|
||||
* @param variants Variants to include to the compile command.
|
||||
* @param handler Handler to dispatch the callback or nullptr for the default handler
|
||||
* @param callback callback called on the main thread when the compilation is done on
|
||||
* by backend.
|
||||
*
|
||||
* @see Material::compile
|
||||
*/
|
||||
void compile(CompilerPriorityQueue priority,
|
||||
UserVariantFilterMask variants,
|
||||
|
||||
@@ -94,12 +94,6 @@ public:
|
||||
std::is_same_v<math::mat3f, T>
|
||||
>;
|
||||
|
||||
template<typename T>
|
||||
using is_supported_constant_parameter_t = std::enable_if_t<
|
||||
std::is_same_v<int32_t, T> ||
|
||||
std::is_same_v<float, T> ||
|
||||
std::is_same_v<bool, T>>;
|
||||
|
||||
/**
|
||||
* Creates a new MaterialInstance using another MaterialInstance as a template for initialization.
|
||||
* The new MaterialInstance is an instance of the same Material of the template instance and
|
||||
@@ -282,91 +276,6 @@ public:
|
||||
return getParameter<T>(name, strlen(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides a specialization constant of this material instance.
|
||||
*
|
||||
* @tparam T The type of the constant. Must be int32_t, float, or bool.
|
||||
* @param name The name of the constant as defined in the material. Cannot be nullptr.
|
||||
* @param nameLength Length in `char` of the name parameter.
|
||||
* @param value The value of the constant.
|
||||
*
|
||||
* @see Material::Builder::constant
|
||||
*/
|
||||
template<typename T, typename = is_supported_constant_parameter_t<T>>
|
||||
void setConstant(const char* UTILS_NONNULL name, size_t nameLength, T value);
|
||||
|
||||
/** inline helper to provide the name as a null-terminated string literal */
|
||||
template<typename T, typename = is_supported_constant_parameter_t<T>>
|
||||
void setConstant(StringLiteral const name, T value) {
|
||||
setConstant<T>(name.data, name.size, value);
|
||||
}
|
||||
|
||||
/** inline helper to provide the name as a null-terminated C string */
|
||||
template<typename T, typename = is_supported_constant_parameter_t<T>>
|
||||
void setConstant(const char* UTILS_NONNULL name, T value) {
|
||||
setConstant<T>(name, strlen(name), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of a specialization constant by name.
|
||||
*
|
||||
* @tparam T The type of the constant. Must be int32_t, float, or bool.
|
||||
* @param name The name of the constant as defined in the material. Cannot be nullptr.
|
||||
* @param nameLength Length in `char` of the name parameter.
|
||||
* @return The value of the constant.
|
||||
*/
|
||||
template<typename T, typename = is_supported_constant_parameter_t<T>>
|
||||
T getConstant(const char* UTILS_NONNULL name, size_t nameLength) const;
|
||||
|
||||
/** inline helper to provide the name as a null-terminated C string */
|
||||
template<typename T, typename = is_supported_constant_parameter_t<T>>
|
||||
T getConstant(StringLiteral const name) const {
|
||||
return getConstant<T>(name.data, name.size);
|
||||
}
|
||||
|
||||
/** inline helper to provide the name as a null-terminated C string */
|
||||
template<typename T, typename = is_supported_constant_parameter_t<T>>
|
||||
T getConstant(const char* UTILS_NONNULL name) const {
|
||||
return getConstant<T>(name, strlen(name));
|
||||
}
|
||||
|
||||
using CompilerPriorityQueue = backend::CompilerPriorityQueue;
|
||||
|
||||
/**
|
||||
* Asynchronously ensures that a subset of this MaterialInstance's variants are compiled.
|
||||
*
|
||||
* This function behaves identically to Material::compile(), but takes into account the
|
||||
* specific constants overridden by setConstant().
|
||||
*
|
||||
* @param priority Which priority queue to use, LOW or HIGH.
|
||||
* @param variants Variants to include to the compile command.
|
||||
* @param handler Handler to dispatch the callback or nullptr for the default handler
|
||||
* @param callback callback called on the main thread when the compilation is done on
|
||||
* by backend.
|
||||
*
|
||||
* @see Material::compile
|
||||
* @see setConstant
|
||||
*/
|
||||
void compile(CompilerPriorityQueue priority,
|
||||
UserVariantFilterMask variants,
|
||||
backend::CallbackHandler* UTILS_NULLABLE handler = nullptr,
|
||||
utils::Invocable<void(MaterialInstance* UTILS_NONNULL)>&& callback = {}) noexcept;
|
||||
|
||||
inline void compile(CompilerPriorityQueue priority,
|
||||
UserVariantFilterBit variants,
|
||||
backend::CallbackHandler* UTILS_NULLABLE handler = nullptr,
|
||||
utils::Invocable<void(MaterialInstance* UTILS_NONNULL)>&& callback = {}) noexcept {
|
||||
compile(priority, UserVariantFilterMask(variants), handler,
|
||||
std::forward<utils::Invocable<void(MaterialInstance* UTILS_NONNULL)>>(callback));
|
||||
}
|
||||
|
||||
inline void compile(CompilerPriorityQueue priority,
|
||||
backend::CallbackHandler* UTILS_NULLABLE handler = nullptr,
|
||||
utils::Invocable<void(MaterialInstance* UTILS_NONNULL)>&& callback = {}) noexcept {
|
||||
compile(priority, UserVariantFilterBit::ALL, handler,
|
||||
std::forward<utils::Invocable<void(MaterialInstance* UTILS_NONNULL)>>(callback));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set-up a custom scissor rectangle; by default it is disabled.
|
||||
*
|
||||
|
||||
@@ -182,11 +182,15 @@ Program::SpecializationConstant LocalProgramCache::getConstantImpl(
|
||||
std::string_view name) const noexcept {
|
||||
assert_invariant(mMaterial != nullptr);
|
||||
|
||||
auto const& constants = mMaterial->getDefinition().specializationConstantsNameToIndex;
|
||||
auto it = constants.find(name);
|
||||
FILAMENT_CHECK_PRECONDITION(it != constants.end()) << "Constant " << name << " does not exist";
|
||||
MaterialDefinition const& definition = mMaterial->getDefinition();
|
||||
auto it = definition.specializationConstantsNameToIndex.find(name);
|
||||
if (it != definition.specializationConstantsNameToIndex.cend()) {
|
||||
return getConstantImpl(it->second + CONFIG_MAX_RESERVED_SPEC_CONSTANTS);
|
||||
}
|
||||
|
||||
return getConstantImpl(it->second + CONFIG_MAX_RESERVED_SPEC_CONSTANTS);
|
||||
CString name_cstring(name);
|
||||
PANIC_PRECONDITION("No such constant exists: %s", name_cstring.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
void LocalProgramCache::setConstants(
|
||||
@@ -236,13 +240,6 @@ void LocalProgramCache::setConstants(
|
||||
}
|
||||
}
|
||||
|
||||
void LocalProgramCache::setConstants(
|
||||
FixedCapacityVector<Program::SpecializationConstant> constants) noexcept {
|
||||
assert_invariant(mMaterial != nullptr);
|
||||
|
||||
setConstantsImpl(std::move(constants));
|
||||
}
|
||||
|
||||
void LocalProgramCache::setConstantsImpl(
|
||||
FixedCapacityVector<Program::SpecializationConstant> constants) noexcept {
|
||||
FEngine& engine = mMaterial->getEngine();
|
||||
|
||||
@@ -119,14 +119,7 @@ public:
|
||||
std::pair<std::string_view, backend::Program::SpecializationConstant>>
|
||||
constants) noexcept;
|
||||
|
||||
// Set constants list directly.
|
||||
void setConstants(utils::FixedCapacityVector<backend::Program::SpecializationConstant>
|
||||
constants) noexcept;
|
||||
|
||||
private:
|
||||
// Apply any pending specialization constants. Invalidates programs as necessary.
|
||||
void flushConstants() const;
|
||||
|
||||
backend::Handle<backend::HwProgram> prepareProgramSlow(backend::DriverApi& driver,
|
||||
Variant const variant,
|
||||
backend::CompilerPriorityQueue const priorityQueue) const noexcept;
|
||||
|
||||
@@ -233,26 +233,6 @@ template UTILS_PUBLIC mat3f MaterialInstance::getParameter<mat3f> (const ch
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
template<typename T, typename>
|
||||
void MaterialInstance::setConstant(const char* name, size_t nameLength, T value) {
|
||||
downcast(this)->setConstantImpl(std::string_view{name, nameLength}, value);
|
||||
}
|
||||
|
||||
template UTILS_PUBLIC void MaterialInstance::setConstant<int32_t>(const char* name, size_t nameLength, int32_t value);
|
||||
template UTILS_PUBLIC void MaterialInstance::setConstant<float>(const char* name, size_t nameLength, float value);
|
||||
template UTILS_PUBLIC void MaterialInstance::setConstant<bool>(const char* name, size_t nameLength, bool value);
|
||||
|
||||
template<typename T, typename>
|
||||
T MaterialInstance::getConstant(const char* name, size_t nameLength) const {
|
||||
return downcast(this)->getConstantImpl<T>(std::string_view{name, nameLength});
|
||||
}
|
||||
|
||||
template UTILS_PUBLIC int32_t MaterialInstance::getConstant<int32_t>(const char* name, size_t nameLength) const;
|
||||
template UTILS_PUBLIC float MaterialInstance::getConstant<float>(const char* name, size_t nameLength) const;
|
||||
template UTILS_PUBLIC bool MaterialInstance::getConstant<bool>(const char* name, size_t nameLength) const;
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
Material const* MaterialInstance::getMaterial() const noexcept {
|
||||
return downcast(this)->getMaterial();
|
||||
}
|
||||
@@ -423,10 +403,4 @@ void MaterialInstance::commit(Engine& engine) const {
|
||||
downcast(this)->commit(downcast(engine));
|
||||
}
|
||||
|
||||
void MaterialInstance::compile(CompilerPriorityQueue const priority,
|
||||
UserVariantFilterMask const variants, CallbackHandler* handler,
|
||||
utils::Invocable<void(MaterialInstance*)>&& callback) noexcept {
|
||||
downcast(this)->compile(priority, variants, handler, std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace filament
|
||||
|
||||
@@ -186,10 +186,12 @@ void PostProcessManager::PostProcessMaterial::loadMaterial(FEngine& engine) cons
|
||||
}
|
||||
|
||||
UTILS_NOINLINE
|
||||
FMaterial* PostProcessManager::PostProcessMaterial::getMaterial(FEngine& engine) const noexcept {
|
||||
FMaterial* PostProcessManager::PostProcessMaterial::getMaterial(FEngine& engine,
|
||||
DriverApi& driver, Variant::type_t const variant) const noexcept {
|
||||
if (UTILS_UNLIKELY(mSize)) {
|
||||
loadMaterial(engine);
|
||||
}
|
||||
mMaterial->prepareProgram(driver, Variant{ variant }, CompilerPriorityQueue::CRITICAL);
|
||||
return mMaterial;
|
||||
}
|
||||
|
||||
@@ -247,20 +249,6 @@ void PostProcessManager::bindPerRenderableDescriptorSet(DriverApi& driver) const
|
||||
{ { 0, 0 }, driver });
|
||||
}
|
||||
|
||||
FMaterialInstance* PostProcessManager::getMaterialInstance(backend::DriverApi& driver,
|
||||
FMaterial const* ma, Variant::type_t variant) const {
|
||||
FMaterialInstance* mi = mMaterialInstanceManager.getMaterialInstance(ma);
|
||||
mi->prepareProgram(driver, Variant{ variant }, backend::CompilerPriorityQueue::CRITICAL);
|
||||
return mi;
|
||||
}
|
||||
|
||||
FMaterialInstance* PostProcessManager::getMaterialInstanceWithTag(backend::DriverApi& driver,
|
||||
FMaterial const* ma, uint32_t tag, Variant::type_t variant) const {
|
||||
FMaterialInstance* mi = mMaterialInstanceManager.getMaterialInstance(ma, tag);
|
||||
mi->prepareProgram(driver, Variant { variant }, backend::CompilerPriorityQueue::CRITICAL);
|
||||
return mi;
|
||||
}
|
||||
|
||||
UboManager* PostProcessManager::getUboManager() const noexcept {
|
||||
return mEngine.getUboManager();
|
||||
}
|
||||
@@ -480,10 +468,9 @@ void PostProcessManager::unbindAllDescriptorSets(DriverApi& driver) noexcept {
|
||||
|
||||
UTILS_NOINLINE
|
||||
PipelineState PostProcessManager::getPipelineState(
|
||||
FMaterialInstance const* const mi, Variant::type_t const variant) const noexcept {
|
||||
FMaterial const* const ma = mi->getMaterial();
|
||||
FMaterial const* const ma, Variant::type_t const variant) const noexcept {
|
||||
return {
|
||||
.program = mi->getProgram(Variant{ variant }),
|
||||
.program = ma->getProgram(Variant{ variant }),
|
||||
.vertexBufferInfo = mFullScreenQuadVbih,
|
||||
.pipelineLayout = {
|
||||
.setLayout = {
|
||||
@@ -491,7 +478,7 @@ PipelineState PostProcessManager::getPipelineState(
|
||||
mPerRenderableDslh,
|
||||
ma->getDescriptorSetLayout().getHandle()
|
||||
}},
|
||||
.rasterState = mi->getRasterState()
|
||||
.rasterState = ma->getRasterState()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -531,7 +518,8 @@ void PostProcessManager::commitAndRenderFullScreenQuad(DriverApi& driver,
|
||||
PostProcessVariant const variant) const noexcept {
|
||||
mi->commit(driver, getUboManager());
|
||||
mi->use(driver);
|
||||
PipelineState const pipeline = getPipelineState(mi, variant);
|
||||
FMaterial const* const ma = mi->getMaterial();
|
||||
PipelineState const pipeline = getPipelineState(ma, variant);
|
||||
|
||||
assert_invariant(
|
||||
((out.params.readOnlyDepthStencil & RenderPassParams::READONLY_DEPTH)
|
||||
@@ -648,12 +636,12 @@ PostProcessManager::StructurePassOutput PostProcessManager::structure(FrameGraph
|
||||
|
||||
auto in = resources.getTexture(data.depth);
|
||||
auto& material = getPostProcessMaterial("mipmapDepth");
|
||||
FMaterial const* const ma = material.getMaterial(mEngine);
|
||||
FMaterialInstance* const mi = getMaterialInstance(driver, ma);
|
||||
FMaterial const* const ma = material.getMaterial(mEngine, driver);
|
||||
FMaterialInstance* const mi = getMaterialInstance(ma);
|
||||
// Only the depth texture is changing in the material instance (no UBO updates),
|
||||
// we do not move getMaterialInstance() inside the loop.
|
||||
|
||||
auto pipeline = getPipelineState(mi);
|
||||
auto pipeline = getPipelineState(ma);
|
||||
|
||||
// The first mip already exists, so we process n-1 lods
|
||||
for (size_t level = 0; level < levelCount - 1; level++) {
|
||||
@@ -1013,13 +1001,14 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::screenSpaceAmbientOcclusion(
|
||||
#endif
|
||||
auto& material = getPostProcessMaterial(materialName);
|
||||
|
||||
FMaterial* ma = material.getMaterial(mEngine);
|
||||
FMaterial* ma = material.getMaterial(mEngine, driver);
|
||||
ma->getPrograms().setConstants({
|
||||
{ "useVisibilityBitmasks", options.gtao.useVisibilityBitmasks },
|
||||
{ "linearThickness", options.gtao.linearThickness },
|
||||
});
|
||||
|
||||
FMaterialInstance* const mi = getMaterialInstance(driver, ma);
|
||||
ma = material.getMaterial(mEngine, driver);
|
||||
FMaterialInstance* const mi = getMaterialInstance(ma);
|
||||
|
||||
// Set AO type specific material parameters
|
||||
switch (aoType) {
|
||||
@@ -1095,7 +1084,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::screenSpaceAmbientOcclusion(
|
||||
mi->commit(driver, getUboManager());
|
||||
mi->use(driver);
|
||||
|
||||
auto pipeline = getPipelineState(mi);
|
||||
auto pipeline = getPipelineState(ma);
|
||||
pipeline.rasterState.depthFunc = RasterState::DepthFunc::L;
|
||||
assert_invariant(ssao.params.readOnlyDepthStencil & RenderPassParams::READONLY_DEPTH);
|
||||
renderFullScreenQuad(ssao, pipeline, driver);
|
||||
@@ -1205,8 +1194,8 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::bilateralBlurPass(FrameGraph
|
||||
auto& material = config.bentNormals ?
|
||||
getPostProcessMaterial("bilateralBlurBentNormals") :
|
||||
getPostProcessMaterial("bilateralBlur");
|
||||
FMaterial const* const ma = material.getMaterial(mEngine);
|
||||
FMaterialInstance* const mi = getMaterialInstance(driver, ma);
|
||||
FMaterial const* const ma = material.getMaterial(mEngine, driver);
|
||||
FMaterialInstance* const mi = getMaterialInstance(ma);
|
||||
mi->setParameter("ssao", ssao, { /* only reads level 0 */ });
|
||||
mi->setParameter("axis", axis / float2{desc.width, desc.height});
|
||||
mi->setParameter("kernel", kGaussianSamples, kGaussianCount);
|
||||
@@ -1216,7 +1205,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::bilateralBlurPass(FrameGraph
|
||||
mi->commit(driver, getUboManager());
|
||||
mi->use(driver);
|
||||
|
||||
auto pipeline = getPipelineState(mi);
|
||||
auto pipeline = getPipelineState(ma);
|
||||
pipeline.rasterState.depthFunc = RasterState::DepthFunc::L;
|
||||
renderFullScreenQuad(blurred, pipeline, driver);
|
||||
unbindAllDescriptorSets(driver);
|
||||
@@ -1375,7 +1364,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::gaussianBlurPass(FrameGraph&
|
||||
"separableGaussianBlur4L"sv : "separableGaussianBlur4"sv; break;
|
||||
}
|
||||
auto const& separableGaussianBlur = getPostProcessMaterial(materialName);
|
||||
auto ma = separableGaussianBlur.getMaterial(mEngine);
|
||||
auto ma = separableGaussianBlur.getMaterial(mEngine, driver);
|
||||
|
||||
const size_t kernelStorageSize = ma->reflect("kernel")->size;
|
||||
float2 kernel[64];
|
||||
@@ -1898,10 +1887,9 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::dof(FrameGraph& fg,
|
||||
auto inOutCoc = resources.getTexture(data.inOutCoc);
|
||||
|
||||
auto const& material = getPostProcessMaterial("dofMipmap");
|
||||
FMaterial const* const ma = material.getMaterial(mEngine);
|
||||
FMaterialInstance* const mi = getMaterialInstance(driver, ma);
|
||||
FMaterial const* const ma = material.getMaterial(mEngine, driver);
|
||||
|
||||
auto const pipeline = getPipelineState(mi, variant);
|
||||
auto const pipeline = getPipelineState(ma, variant);
|
||||
|
||||
for (size_t level = 0 ; level < mipmapCount - 1u ; level++) {
|
||||
const float w = FTexture::valueForLevel(level, desc.width);
|
||||
@@ -1909,8 +1897,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::dof(FrameGraph& fg,
|
||||
auto const& out = resources.getRenderPassInfo(data.rp[level]);
|
||||
auto inColor = driver.createTextureView(inOutColor, level, 1);
|
||||
auto inCoc = driver.createTextureView(inOutCoc, level, 1);
|
||||
// FIXME: is this necessary?
|
||||
FMaterialInstance* const mi = getMaterialInstance(driver, ma);
|
||||
FMaterialInstance* const mi = getMaterialInstance(ma);
|
||||
|
||||
mi->setParameter("color", inColor, SamplerParams{
|
||||
.filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST });
|
||||
@@ -2439,9 +2426,9 @@ PostProcessManager::BloomPassOutput PostProcessManager::bloom(FrameGraph& fg,
|
||||
auto const& outDesc = resources.getDescriptor(data.out);
|
||||
|
||||
auto const& material = getPostProcessMaterial("bloomUpsample");
|
||||
FMaterial const* const ma = material.getMaterial(mEngine);
|
||||
FMaterial const* const ma = material.getMaterial(mEngine, driver);
|
||||
|
||||
auto pipeline = getPipelineState(getMaterialInstance(driver, ma));
|
||||
auto pipeline = getPipelineState(ma);
|
||||
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
|
||||
pipeline.rasterState.blendFunctionDstRGB = BlendFunction::ONE;
|
||||
|
||||
@@ -2449,7 +2436,7 @@ PostProcessManager::BloomPassOutput PostProcessManager::bloom(FrameGraph& fg,
|
||||
// Note that we wouldn't want to use the same instance for each pass since that
|
||||
// would imply using the same UBOs, which implies synchronization across the
|
||||
// passes.
|
||||
FMaterialInstance* mi = getMaterialInstance(driver, ma);
|
||||
FMaterialInstance* mi = getMaterialInstance(ma);
|
||||
auto hwDstRT = resources.getRenderPassInfo(data.outRT[i - 1]);
|
||||
hwDstRT.params.flags.discardStart = TargetBufferFlags::NONE; // b/c we'll blend
|
||||
hwDstRT.params.flags.discardEnd = TargetBufferFlags::NONE;
|
||||
@@ -2582,12 +2569,11 @@ void PostProcessManager::colorGradingSubpass(DriverApi& driver,
|
||||
PostProcessVariant::TRANSLUCENT : PostProcessVariant::OPAQUE;
|
||||
|
||||
auto const& material = getPostProcessMaterial("colorGradingAsSubpass");
|
||||
FMaterial const* const ma = material.getMaterial(mEngine);
|
||||
FMaterial const* const ma = material.getMaterial(mEngine, driver, variant);
|
||||
// the UBO has been set and committed in colorGradingPrepareSubpass()
|
||||
FMaterialInstance const* mi =
|
||||
getMaterialInstanceWithTag(driver, ma, colorGradingConfig.translucent, variant);
|
||||
FMaterialInstance const* mi = mMaterialInstanceManager.getMaterialInstance(ma, colorGradingConfig.translucent);
|
||||
mi->use(driver);
|
||||
auto const pipeline = getPipelineState(mi, variant);
|
||||
auto const pipeline = getPipelineState(ma, variant);
|
||||
driver.nextSubpass();
|
||||
driver.scissor(mi->getScissor());
|
||||
driver.draw(pipeline, mFullScreenQuadRph, 0, 3, 1);
|
||||
@@ -2595,8 +2581,8 @@ void PostProcessManager::colorGradingSubpass(DriverApi& driver,
|
||||
|
||||
void PostProcessManager::customResolvePrepareSubpass(DriverApi& driver, CustomResolveOp const op) noexcept {
|
||||
auto const& material = getPostProcessMaterial("customResolveAsSubpass");
|
||||
auto const ma = material.getMaterial(mEngine);
|
||||
auto* const mi = getMaterialInstance(driver, ma, 0);
|
||||
auto const ma = material.getMaterial(mEngine, driver, PostProcessVariant::OPAQUE);
|
||||
auto* const mi = mMaterialInstanceManager.getMaterialInstance(ma, 0);
|
||||
mi->setParameter("direction", op == CustomResolveOp::COMPRESS ? 1.0f : -1.0f),
|
||||
mi->commit(driver, getUboManager());
|
||||
}
|
||||
@@ -2606,12 +2592,12 @@ void PostProcessManager::customResolveSubpass(DriverApi& driver) noexcept {
|
||||
bindPerRenderableDescriptorSet(driver);
|
||||
|
||||
auto const& material = getPostProcessMaterial("customResolveAsSubpass");
|
||||
FMaterial const* const ma = material.getMaterial(mEngine);
|
||||
FMaterial const* const ma = material.getMaterial(mEngine, driver);
|
||||
// the UBO has been set and committed in customResolvePrepareSubpass()
|
||||
FMaterialInstance const* mi = getMaterialInstance(driver, ma, 0);
|
||||
FMaterialInstance const* mi = mMaterialInstanceManager.getMaterialInstance(ma, 0);
|
||||
mi->use(driver);
|
||||
|
||||
auto const pipeline = getPipelineState(mi);
|
||||
auto const pipeline = getPipelineState(ma);
|
||||
driver.nextSubpass();
|
||||
driver.scissor(mi->getScissor());
|
||||
driver.draw(pipeline, mFullScreenQuadRph, 0, 3, 1);
|
||||
@@ -2647,8 +2633,8 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::customResolveUncompressPass(
|
||||
void PostProcessManager::clearAncillaryBuffersPrepare(DriverApi& driver,
|
||||
Variant::type_t variant) noexcept {
|
||||
auto const& material = getPostProcessMaterial("clearDepth");
|
||||
auto const ma = material.getMaterial(mEngine);
|
||||
auto const mi = getMaterialInstanceWithTag(driver, ma, 0, variant);
|
||||
auto const ma = material.getMaterial(mEngine, driver, variant);
|
||||
auto const mi = mMaterialInstanceManager.getMaterialInstance(ma, 0);
|
||||
mi->commit(driver, getUboManager());
|
||||
}
|
||||
|
||||
@@ -2663,13 +2649,13 @@ void PostProcessManager::clearAncillaryBuffers(DriverApi& driver,
|
||||
bindPerRenderableDescriptorSet(driver);
|
||||
|
||||
auto const& material = getPostProcessMaterial("clearDepth");
|
||||
FMaterial const* const ma = material.getMaterial(mEngine);
|
||||
FMaterial const* const ma = material.getMaterial(mEngine, driver, variant);
|
||||
|
||||
// the UBO has been set and committed in clearAncillaryBuffersPrepare()
|
||||
FMaterialInstance const* const mi = getMaterialInstanceWithTag(driver, ma, 0, variant);
|
||||
FMaterialInstance const* const mi = mMaterialInstanceManager.getMaterialInstance(ma, 0);
|
||||
mi->use(driver);
|
||||
|
||||
auto pipeline = getPipelineState(mi, variant);
|
||||
auto pipeline = getPipelineState(ma, variant);
|
||||
pipeline.rasterState.depthFunc = RasterState::DepthFunc::A;
|
||||
|
||||
driver.scissor(mi->getScissor());
|
||||
@@ -2679,7 +2665,7 @@ void PostProcessManager::clearAncillaryBuffers(DriverApi& driver,
|
||||
void PostProcessManager::fogPrepare(DriverApi& driver) noexcept {
|
||||
// ensures the material is loaded and material instance created
|
||||
auto const& material = getPostProcessMaterial("fog");
|
||||
FMaterial const* const ma = material.getMaterial(mEngine);
|
||||
FMaterial const* const ma = material.getMaterial(mEngine, driver, PostProcessVariant::OPAQUE);
|
||||
FMaterialInstance const* mi = ma->getDefaultInstance();
|
||||
mi->commit(driver, getUboManager());
|
||||
}
|
||||
@@ -2690,11 +2676,11 @@ void PostProcessManager::fog(DriverApi& driver) noexcept {
|
||||
bindPerRenderableDescriptorSet(driver);
|
||||
|
||||
auto const& material = getPostProcessMaterial("fog");
|
||||
FMaterial const* const ma = material.getMaterial(mEngine);
|
||||
FMaterial const* const ma = material.getMaterial(mEngine, driver);
|
||||
FMaterialInstance const* mi = ma->getDefaultInstance();
|
||||
mi->use(driver);
|
||||
|
||||
auto pipeline = getPipelineState(mi, Variant::NO_VARIANT);
|
||||
auto pipeline = getPipelineState(ma, Variant::NO_VARIANT);
|
||||
driver.scissor(mi->getScissor());
|
||||
driver.draw(pipeline, mFullScreenQuadRph, 0, 3, 1);
|
||||
}
|
||||
@@ -2951,7 +2937,7 @@ void PostProcessManager::TaaJitterCamera(
|
||||
void PostProcessManager::configureTemporalAntiAliasingMaterial(backend::DriverApi& driver,
|
||||
TemporalAntiAliasingOptions const& taaOptions) noexcept {
|
||||
|
||||
FMaterial* const ma = getPostProcessMaterial("taa").getMaterial(mEngine);
|
||||
FMaterial* const ma = getPostProcessMaterial("taa").getMaterial(mEngine, driver);
|
||||
ma->getPrograms().setConstants({
|
||||
{ "upscaling", taaOptions.upscaling > 1.0f },
|
||||
{ "historyReprojection", taaOptions.historyReprojection },
|
||||
@@ -2970,7 +2956,7 @@ FMaterialInstance* PostProcessManager::configureColorGradingMaterial(backend::Dr
|
||||
PostProcessMaterial const& material, FColorGrading const* colorGrading,
|
||||
ColorGradingConfig const& colorGradingConfig, VignetteOptions const& vignetteOptions,
|
||||
uint32_t const width, uint32_t const height) noexcept {
|
||||
FMaterial* ma = material.getMaterial(mEngine);
|
||||
FMaterial* ma = material.getMaterial(mEngine, driver);
|
||||
ma->getPrograms().setConstants({
|
||||
{ "isOneDimensional", colorGrading->isOneDimensional() },
|
||||
{ "isLDR", colorGrading->isLDR() },
|
||||
@@ -2979,9 +2965,8 @@ FMaterialInstance* PostProcessManager::configureColorGradingMaterial(backend::Dr
|
||||
PostProcessVariant const variant = colorGradingConfig.translucent
|
||||
? PostProcessVariant::TRANSLUCENT
|
||||
: PostProcessVariant::OPAQUE;
|
||||
ma = material.getMaterial(mEngine);
|
||||
FMaterialInstance* mi =
|
||||
getMaterialInstanceWithTag(driver, ma, colorGradingConfig.translucent, variant);
|
||||
ma = material.getMaterial(mEngine, driver, variant);
|
||||
FMaterialInstance* mi = mMaterialInstanceManager.getMaterialInstance(ma, colorGradingConfig.translucent);
|
||||
|
||||
const SamplerParams params = SamplerParams{
|
||||
.filterMag = SamplerMagFilter::LINEAR,
|
||||
@@ -3093,9 +3078,9 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::taa(FrameGraph& fg,
|
||||
PostProcessVariant const variant = colorGradingConfig.translucent ?
|
||||
PostProcessVariant::TRANSLUCENT : PostProcessVariant::OPAQUE;
|
||||
|
||||
FMaterial const* const ma = material.getMaterial(mEngine);
|
||||
FMaterial const* const ma = material.getMaterial(mEngine, driver, variant);
|
||||
|
||||
FMaterialInstance* mi = getMaterialInstance(driver, ma);
|
||||
FMaterialInstance* mi = getMaterialInstance(ma);
|
||||
mi->setParameter("color", color, SamplerParams{}); // nearest
|
||||
mi->setParameter("depth", depth, SamplerParams{}); // nearest
|
||||
mi->setParameter("history", history, SamplerParams{
|
||||
@@ -3135,7 +3120,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::taa(FrameGraph& fg,
|
||||
if (colorGradingConfig.asSubpass) {
|
||||
out.params.subpassMask = 1;
|
||||
}
|
||||
auto const pipeline = getPipelineState(mi, variant);
|
||||
auto const pipeline = getPipelineState(ma, variant);
|
||||
|
||||
driver.beginRenderPass(out.target, out.params);
|
||||
driver.draw(pipeline, mFullScreenQuadRph, 0, 3, 1);
|
||||
@@ -3217,7 +3202,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::rcas(
|
||||
mi->commit(driver, getUboManager());
|
||||
mi->use(driver);
|
||||
|
||||
auto pipeline = getPipelineState(mi, variant);
|
||||
auto pipeline = getPipelineState(material.getMaterial(mEngine, driver), variant);
|
||||
if (mode == RcasMode::BLENDED) {
|
||||
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
|
||||
pipeline.rasterState.blendFunctionSrcAlpha = BlendFunction::ONE;
|
||||
@@ -3310,7 +3295,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::upscaleBilinear(FrameGraph&
|
||||
|
||||
auto out = resources.getRenderPassInfo();
|
||||
|
||||
auto pipeline = getPipelineState(mi);
|
||||
auto pipeline = getPipelineState(material.getMaterial(mEngine, driver));
|
||||
if (blended) {
|
||||
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
|
||||
pipeline.rasterState.blendFunctionSrcAlpha = BlendFunction::ONE;
|
||||
@@ -3524,15 +3509,15 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::upscaleFSR1(FrameGraph& fg,
|
||||
auto out = resources.getRenderPassInfo();
|
||||
|
||||
if (UTILS_UNLIKELY(twoPassesEASU)) {
|
||||
auto pipeline0 = getPipelineState(getMaterialInstance(mEngine, driver, *splitEasuMaterial));
|
||||
auto pipeline1 = getPipelineState(getMaterialInstance(mEngine, driver, *easuMaterial));
|
||||
auto pipeline0 = getPipelineState(splitEasuMaterial->getMaterial(mEngine, driver));
|
||||
auto pipeline1 = getPipelineState(easuMaterial->getMaterial(mEngine, driver));
|
||||
pipeline1.rasterState.depthFunc = SamplerCompareFunc::NE;
|
||||
driver.beginRenderPass(out.target, out.params);
|
||||
driver.draw(pipeline0, mFullScreenQuadRph, 0, 3, 1);
|
||||
driver.draw(pipeline1, mFullScreenQuadRph, 0, 3, 1);
|
||||
driver.endRenderPass();
|
||||
} else {
|
||||
auto pipeline = getPipelineState(getMaterialInstance(mEngine, driver, *easuMaterial));
|
||||
auto pipeline = getPipelineState(easuMaterial->getMaterial(mEngine, driver));
|
||||
renderFullScreenQuad(out, pipeline, driver);
|
||||
}
|
||||
unbindAllDescriptorSets(driver);
|
||||
@@ -3584,8 +3569,8 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::blit(FrameGraph& fg, bool co
|
||||
|
||||
PostProcessMaterial const& material =
|
||||
getPostProcessMaterial(layer ? "blitArray" : "blitLow");
|
||||
FMaterial const* const ma = material.getMaterial(mEngine);
|
||||
auto* mi = getMaterialInstance(driver, ma);
|
||||
FMaterial const* const ma = material.getMaterial(mEngine, driver);
|
||||
auto* mi = getMaterialInstance(ma);
|
||||
mi->setParameter("color", color, SamplerParams{
|
||||
.filterMag = filterMag,
|
||||
.filterMin = filterMin
|
||||
@@ -3603,7 +3588,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::blit(FrameGraph& fg, bool co
|
||||
mi->commit(driver, getUboManager());
|
||||
mi->use(driver);
|
||||
|
||||
auto pipeline = getPipelineState(mi);
|
||||
auto pipeline = getPipelineState(ma);
|
||||
if (translucent) {
|
||||
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
|
||||
pipeline.rasterState.blendFunctionSrcAlpha = BlendFunction::ONE;
|
||||
@@ -3898,10 +3883,10 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::debugCombineArrayTexture(Fra
|
||||
// set uniforms
|
||||
|
||||
PostProcessMaterial const& material = getPostProcessMaterial("blitArray");
|
||||
FMaterial const* const ma = material.getMaterial(mEngine);
|
||||
FMaterial const* const ma = material.getMaterial(mEngine, driver);
|
||||
// It should be ok to not move this getMaterialInstance to inside the loop, since
|
||||
// this is a pass meant for debug.
|
||||
auto* mi = getMaterialInstance(driver, ma);
|
||||
auto* mi = getMaterialInstance(ma);
|
||||
mi->setParameter("color", color, SamplerParams{
|
||||
.filterMag = filterMag,
|
||||
.filterMin = filterMin
|
||||
@@ -3915,7 +3900,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::debugCombineArrayTexture(Fra
|
||||
mi->commit(driver, getUboManager());
|
||||
mi->use(driver);
|
||||
|
||||
auto pipeline = getPipelineState(mi);
|
||||
auto pipeline = getPipelineState(ma);
|
||||
if (translucent) {
|
||||
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
|
||||
pipeline.rasterState.blendFunctionSrcAlpha = BlendFunction::ONE;
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
#include <array>
|
||||
#include <random>
|
||||
#include <string_view>
|
||||
#include <optional>
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
@@ -356,7 +357,13 @@ public:
|
||||
|
||||
void terminate(FEngine& engine) noexcept;
|
||||
|
||||
FMaterial* getMaterial(FEngine& engine) const noexcept;
|
||||
FMaterial* getMaterial(FEngine& engine, backend::DriverApi& driver,
|
||||
Variant::type_t variant) const noexcept;
|
||||
|
||||
FMaterial* getMaterial(FEngine& engine, backend::DriverApi& driver,
|
||||
PostProcessVariant variant = PostProcessVariant::OPAQUE) const noexcept {
|
||||
return getMaterial(engine, driver, Variant::type_t(variant));
|
||||
}
|
||||
|
||||
private:
|
||||
void loadMaterial(FEngine& engine) const noexcept;
|
||||
@@ -383,12 +390,11 @@ public:
|
||||
|
||||
void bindPerRenderableDescriptorSet(backend::DriverApi& driver) const noexcept;
|
||||
|
||||
backend::PipelineState getPipelineState(FMaterialInstance const* mi,
|
||||
Variant::type_t variant) const noexcept;
|
||||
backend::PipelineState getPipelineState(FMaterial const* ma, Variant::type_t variant) const noexcept;
|
||||
|
||||
backend::PipelineState getPipelineState(FMaterialInstance const* mi,
|
||||
PostProcessVariant variant = PostProcessVariant::OPAQUE) const noexcept {
|
||||
return getPipelineState(mi, Variant::type_t(variant));
|
||||
backend::PipelineState getPipelineState(FMaterial const* ma,
|
||||
PostProcessVariant variant = PostProcessVariant::OPAQUE) const noexcept {
|
||||
return getPipelineState(ma, Variant::type_t(variant));
|
||||
}
|
||||
|
||||
void renderFullScreenQuad(FrameGraphResources::RenderPassInfo const& out,
|
||||
@@ -418,47 +424,27 @@ public:
|
||||
|
||||
void resetForRender();
|
||||
|
||||
|
||||
MaterialInstanceManager& getMaterialInstanceManager() noexcept {
|
||||
return mMaterialInstanceManager;
|
||||
}
|
||||
|
||||
// Helper to get a MaterialInstance from a FMaterial
|
||||
// This currently just call FMaterial::getDefaultInstance().
|
||||
FMaterialInstance* getMaterialInstance(FMaterial const* ma) {
|
||||
return mMaterialInstanceManager.getMaterialInstance(ma);
|
||||
}
|
||||
|
||||
// Helper to get a MaterialInstance from a PostProcessMaterial.
|
||||
FMaterialInstance* getMaterialInstance(FEngine& engine, backend::DriverApi& driver,
|
||||
PostProcessMaterial const& material, PostProcessVariant variant = PostProcessVariant::OPAQUE) {
|
||||
FMaterial const* ma = material.getMaterial(engine, driver, variant);
|
||||
return getMaterialInstance(ma);
|
||||
}
|
||||
|
||||
static void unbindAllDescriptorSets(backend::DriverApi& driver) noexcept;
|
||||
|
||||
private:
|
||||
|
||||
// Helpers to get MaterialInstances.
|
||||
//
|
||||
// These funcions additionally ensure that the necessary shader programs are compiled via
|
||||
// prepareProgram().
|
||||
FMaterialInstance* getMaterialInstance(backend::DriverApi& driver, FMaterial const* ma,
|
||||
Variant::type_t variant) const;
|
||||
|
||||
FMaterialInstance* getMaterialInstance(backend::DriverApi& driver, FMaterial const* ma,
|
||||
PostProcessVariant variant = PostProcessVariant::OPAQUE) const {
|
||||
return getMaterialInstance(driver, ma, Variant::type_t(variant));
|
||||
}
|
||||
|
||||
FMaterialInstance* getMaterialInstance(FEngine& engine, backend::DriverApi& driver,
|
||||
PostProcessMaterial const& material,
|
||||
PostProcessVariant variant = PostProcessVariant::OPAQUE) const {
|
||||
return getMaterialInstance(driver, material.getMaterial(engine), Variant::type_t(variant));
|
||||
}
|
||||
|
||||
FMaterialInstance* getMaterialInstanceWithTag(backend::DriverApi& driver, FMaterial const* ma,
|
||||
uint32_t tag, Variant::type_t variant) const;
|
||||
|
||||
FMaterialInstance* getMaterialInstanceWithTag(backend::DriverApi& driver, FMaterial const* ma,
|
||||
uint32_t tag, PostProcessVariant variant = PostProcessVariant::OPAQUE) const {
|
||||
return getMaterialInstanceWithTag(driver, ma, tag, Variant::type_t(variant));
|
||||
}
|
||||
|
||||
FMaterialInstance* getMaterialInstanceWithTag(FEngine& engine, backend::DriverApi& driver,
|
||||
PostProcessMaterial const& material, uint32_t tag,
|
||||
PostProcessVariant variant = PostProcessVariant::OPAQUE) const {
|
||||
return getMaterialInstanceWithTag(driver, material.getMaterial(engine), tag,
|
||||
Variant::type_t(variant));
|
||||
}
|
||||
|
||||
UboManager* getUboManager() const noexcept;
|
||||
|
||||
backend::RenderPrimitiveHandle mFullScreenQuadRph;
|
||||
|
||||
@@ -243,8 +243,8 @@ void RenderPass::appendCommands(FEngine const& engine, backend::DriverApi& drive
|
||||
// This must be done from the main thread.
|
||||
for (Command const* first = curr, *last = curr + commandCount ; first != last ; ++first) {
|
||||
if (UTILS_LIKELY((first->key & CUSTOM_MASK) == uint64_t(CustomCommand::PASS))) {
|
||||
first->info.mi->prepareProgram(driver, first->info.materialVariant,
|
||||
CompilerPriorityQueue::CRITICAL);
|
||||
auto ma = first->info.mi->getMaterial();
|
||||
ma->prepareProgram(driver, first->info.materialVariant, CompilerPriorityQueue::CRITICAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,18 +426,16 @@ void RenderPass::setupColorCommand(Command& cmdDraw, Variant variant,
|
||||
bool const isBlendingCommand = !hasScreenSpaceRefraction &&
|
||||
(blendingMode != BlendingMode::OPAQUE && blendingMode != BlendingMode::MASKED);
|
||||
|
||||
RasterState rasterState = mi->getRasterState();
|
||||
|
||||
uint64_t keyDraw = cmdDraw.key;
|
||||
keyDraw &= ~(PASS_MASK | BLENDING_MASK | MATERIAL_MASK);
|
||||
keyDraw |= uint64_t(hasScreenSpaceRefraction ? Pass::REFRACT : Pass::COLOR);
|
||||
keyDraw |= uint64_t(CustomCommand::PASS);
|
||||
keyDraw |= mi->getSortingKey(); // already all set-up for direct or'ing
|
||||
keyDraw |= makeField(variant.key, MATERIAL_VARIANT_KEY_MASK, MATERIAL_VARIANT_KEY_SHIFT);
|
||||
keyDraw |= makeField(rasterState.alphaToCoverage, BLENDING_MASK, BLENDING_SHIFT);
|
||||
keyDraw |= makeField(ma->getRasterState().alphaToCoverage, BLENDING_MASK, BLENDING_SHIFT);
|
||||
|
||||
cmdDraw.key = isBlendingCommand ? keyBlending : keyDraw;
|
||||
cmdDraw.info.rasterState = rasterState;
|
||||
cmdDraw.info.rasterState = ma->getRasterState();
|
||||
|
||||
// for SSR pass, the blending mode of opaques (including MASKED) must be off
|
||||
// see Material.cpp.
|
||||
@@ -448,6 +446,10 @@ void RenderPass::setupColorCommand(Command& cmdDraw, Variant variant,
|
||||
BlendFunction::ZERO : cmdDraw.info.rasterState.blendFunctionDstAlpha;
|
||||
|
||||
cmdDraw.info.rasterState.inverseFrontFaces = inverseFrontFaces;
|
||||
cmdDraw.info.rasterState.culling = mi->getCullingMode();
|
||||
cmdDraw.info.rasterState.colorWrite = mi->isColorWriteEnabled();
|
||||
cmdDraw.info.rasterState.depthWrite = mi->isDepthWriteEnabled();
|
||||
cmdDraw.info.rasterState.depthFunc = mi->getDepthFunc();
|
||||
cmdDraw.info.rasterState.depthClamp = hasDepthClamp;
|
||||
cmdDraw.info.materialVariant = variant;
|
||||
// we keep "RasterState::colorWrite" to the value set by material (could be disabled)
|
||||
@@ -1088,7 +1090,8 @@ void RenderPass::Executor::execute(FEngine const& engine, DriverApi& driver,
|
||||
mi->use(driver, info.materialVariant);
|
||||
}
|
||||
|
||||
pipeline.program = mi->getProgram(info.materialVariant);
|
||||
assert_invariant(ma);
|
||||
pipeline.program = ma->getProgram(info.materialVariant);
|
||||
|
||||
if (UTILS_UNLIKELY(memcmp(&pipeline, ¤tPipeline, sizeof(PipelineState)) != 0)) {
|
||||
currentPipeline = pipeline;
|
||||
|
||||
@@ -636,7 +636,7 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::gaussianBlurSeparatedPass(
|
||||
|
||||
// get the material
|
||||
auto const& separableGaussianBlur = ppm.getPostProcessMaterial("gaussian");
|
||||
auto const ma = separableGaussianBlur.getMaterial(engine);
|
||||
auto const ma = separableGaussianBlur.getMaterial(engine, driver);
|
||||
|
||||
// Generates half of a Gaussian kernel (center + one side)
|
||||
auto generateGaussianWeights = [](std::array<float, 32>& weights,
|
||||
@@ -685,7 +685,7 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::gaussianBlurSeparatedPass(
|
||||
for (auto const mi : miList) {
|
||||
mi->use(driver);
|
||||
driver.scissor(mi->getScissor());
|
||||
driver.draw(ppm.getPipelineState(mi), engine.getFullScreenRenderPrimitive(), 0, 3, 1);
|
||||
driver.draw(ppm.getPipelineState(ma), engine.getFullScreenRenderPrimitive(), 0, 3, 1);
|
||||
}
|
||||
driver.endRenderPass();
|
||||
|
||||
@@ -733,12 +733,12 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::vsmMipmapPass(
|
||||
ppm.bindPerRenderableDescriptorSet(driver);
|
||||
|
||||
auto& material = ppm.getPostProcessMaterial("vsmMipmap");
|
||||
FMaterial const* const ma = material.getMaterial(engine);
|
||||
auto const mi = ppm.getMaterialInstanceManager().getMaterialInstance(ma);
|
||||
FMaterial const* const ma = material.getMaterial(engine, driver);
|
||||
|
||||
auto const pipeline = ppm.getPipelineState(mi);
|
||||
auto const pipeline = ppm.getPipelineState(ma);
|
||||
backend::Viewport const scissor = { 0, 0, dim, dim };
|
||||
|
||||
auto const mi = ppm.getMaterialInstanceManager().getMaterialInstance(ma);
|
||||
mi->setParameter("color", in, SamplerParams{
|
||||
.filterMag = SamplerMagFilter::NEAREST,
|
||||
.filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
#include <utils/JobSystem.h>
|
||||
#include <utils/Mutex.h>
|
||||
#include <utils/Panic.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
@@ -101,6 +102,10 @@ struct ColorGrading::BuilderDetails {
|
||||
// Output color space
|
||||
ColorSpace outputColorSpace = Rec709-sRGB-D65;
|
||||
|
||||
// Custom LUT
|
||||
utils::FixedCapacityVector<math::float3> customLutData;
|
||||
uint8_t customLutDimension = 0;
|
||||
|
||||
bool operator!=(const BuilderDetails &rhs) const {
|
||||
return !(rhs == *this);
|
||||
}
|
||||
@@ -130,7 +135,9 @@ struct ColorGrading::BuilderDetails {
|
||||
shadowGamma == rhs.shadowGamma &&
|
||||
midPoint == rhs.midPoint &&
|
||||
highlightScale == rhs.highlightScale &&
|
||||
outputColorSpace == rhs.outputColorSpace;
|
||||
outputColorSpace == rhs.outputColorSpace &&
|
||||
customLutData == rhs.customLutData &&
|
||||
customLutDimension == rhs.customLutDimension;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -272,11 +279,27 @@ ColorGrading::Builder& ColorGrading::Builder::outputColorSpace(
|
||||
return *this;
|
||||
}
|
||||
|
||||
ColorGrading::Builder& ColorGrading::Builder::customLut(
|
||||
utils::FixedCapacityVector<math::float3> data, uint8_t dimension) noexcept {
|
||||
mImpl->customLutData = std::move(data);
|
||||
mImpl->customLutDimension = dimension;
|
||||
return *this;
|
||||
}
|
||||
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
#endif
|
||||
ColorGrading* ColorGrading::Builder::build(Engine& engine) {
|
||||
if (mImpl->customLutDimension == 0 || mImpl->customLutData.empty()) {
|
||||
mImpl->customLutData.clear();
|
||||
mImpl->customLutDimension = 0;
|
||||
} else {
|
||||
FILAMENT_CHECK_PRECONDITION(mImpl->customLutData.size() ==
|
||||
size_t(mImpl->customLutDimension) * mImpl->customLutDimension * mImpl->customLutDimension)
|
||||
<< "Custom LUT data size does not match dimension^3";
|
||||
}
|
||||
|
||||
// We want to see if any of the default adjustment values have been modified
|
||||
// We skip the tonemapping operator on purpose since we always want to apply it
|
||||
BuilderDetails defaults;
|
||||
@@ -544,6 +567,39 @@ inline float3 curves(float3 v, float3 shadowGamma, float3 midPoint, float3 highl
|
||||
};
|
||||
}
|
||||
|
||||
static float3 applyCustomLut(float3 v, const math::float3* lut, uint8_t dim) noexcept {
|
||||
float3 pos = v * float(dim - 1);
|
||||
float3 pos_floor = floor(pos);
|
||||
float3 pos_ceil = min(pos_floor + 1.0f, float(dim - 1));
|
||||
float3 d = pos - pos_floor;
|
||||
|
||||
int3 i0 = int3(pos_floor);
|
||||
int3 i1 = int3(pos_ceil);
|
||||
|
||||
auto fetch = [&](int r, int g, int b) {
|
||||
return lut[r + g * dim + b * dim * dim];
|
||||
};
|
||||
|
||||
float3 c000 = fetch(i0.x, i0.y, i0.z);
|
||||
float3 c100 = fetch(i1.x, i0.y, i0.z);
|
||||
float3 c010 = fetch(i0.x, i1.y, i0.z);
|
||||
float3 c110 = fetch(i1.x, i1.y, i0.z);
|
||||
float3 c001 = fetch(i0.x, i0.y, i1.z);
|
||||
float3 c101 = fetch(i1.x, i0.y, i1.z);
|
||||
float3 c011 = fetch(i0.x, i1.y, i1.z);
|
||||
float3 c111 = fetch(i1.x, i1.y, i1.z);
|
||||
|
||||
float3 c00 = c000 * (1.0f - d.x) + c100 * d.x;
|
||||
float3 c10 = c010 * (1.0f - d.x) + c110 * d.x;
|
||||
float3 c01 = c001 * (1.0f - d.x) + c101 * d.x;
|
||||
float3 c11 = c011 * (1.0f - d.x) + c111 * d.x;
|
||||
|
||||
float3 c0 = c00 * (1.0f - d.y) + c10 * d.y;
|
||||
float3 c1 = c01 * (1.0f - d.y) + c11 * d.y;
|
||||
|
||||
return c0 * (1.0f - d.z) + c1 * d.z;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Luminance scaling
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -665,6 +721,7 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
|
||||
// spaces are the same, but we currently don't check that. We must revise these conditions if we
|
||||
// ever handle this case.
|
||||
mIsOneDimensional = !builder->hasAdjustments && !builder->luminanceScaling
|
||||
&& builder->customLutData.empty()
|
||||
&& builder->toneMapper->isOneDimensional()
|
||||
&& engine.features.engine.color_grading.use_1d_lut;
|
||||
mIsLDR = mIsOneDimensional && builder->toneMapper->isLDR();
|
||||
@@ -795,6 +852,11 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
|
||||
// Apply OETF
|
||||
v = config.oetf(v);
|
||||
|
||||
// Apply custom LUT if provided
|
||||
if (!builder->customLutData.empty()) {
|
||||
v = applyCustomLut(v, builder->customLutData.data(), builder->customLutDimension);
|
||||
}
|
||||
|
||||
return v;
|
||||
};
|
||||
|
||||
|
||||
@@ -350,10 +350,6 @@ void FEngine::init() {
|
||||
|
||||
mActiveFeatureLevel = std::min(mActiveFeatureLevel, driverApi.getFeatureLevel());
|
||||
|
||||
#ifndef FILAMENT_ENABLE_FEATURE_LEVEL_0
|
||||
assert_invariant(mActiveFeatureLevel > FeatureLevel::FEATURE_LEVEL_0);
|
||||
#endif
|
||||
|
||||
LOG(INFO) << "Backend feature level: " << int(driverApi.getFeatureLevel());
|
||||
LOG(INFO) << "FEngine feature level: " << int(mActiveFeatureLevel);
|
||||
|
||||
@@ -461,34 +457,23 @@ void FEngine::init() {
|
||||
driverApi,
|
||||
descriptor_sets::getPerRenderableLayout() };
|
||||
|
||||
#ifdef FILAMENT_ENABLE_FEATURE_LEVEL_0
|
||||
if (UTILS_UNLIKELY(mActiveFeatureLevel == FeatureLevel::FEATURE_LEVEL_0)) {
|
||||
FMaterial::DefaultMaterialBuilder defaultMaterialBuilder;
|
||||
defaultMaterialBuilder.package(
|
||||
MATERIALS_DEFAULTMATERIAL_FL0_DATA, MATERIALS_DEFAULTMATERIAL_FL0_SIZE);
|
||||
mDefaultMaterial = downcast(defaultMaterialBuilder.build(*const_cast<FEngine*>(this)));
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
FMaterial::DefaultMaterialBuilder defaultMaterialBuilder;
|
||||
switch (mConfig.stereoscopicType) {
|
||||
case StereoscopicType::NONE:
|
||||
case StereoscopicType::INSTANCED:
|
||||
defaultMaterialBuilder.package(
|
||||
MATERIALS_DEFAULTMATERIAL_DATA, MATERIALS_DEFAULTMATERIAL_SIZE);
|
||||
break;
|
||||
case StereoscopicType::MULTIVIEW:
|
||||
FMaterial::DefaultMaterialBuilder defaultMaterialBuilder;
|
||||
switch (mConfig.stereoscopicType) {
|
||||
case StereoscopicType::NONE:
|
||||
case StereoscopicType::INSTANCED:
|
||||
defaultMaterialBuilder.package(
|
||||
MATERIALS_DEFAULTMATERIAL_DATA, MATERIALS_DEFAULTMATERIAL_SIZE);
|
||||
break;
|
||||
case StereoscopicType::MULTIVIEW:
|
||||
#ifdef FILAMENT_ENABLE_MULTIVIEW
|
||||
defaultMaterialBuilder.package(
|
||||
MATERIALS_DEFAULTMATERIAL_MULTIVIEW_DATA,
|
||||
MATERIALS_DEFAULTMATERIAL_MULTIVIEW_SIZE);
|
||||
defaultMaterialBuilder.package(
|
||||
MATERIALS_DEFAULTMATERIAL_MULTIVIEW_DATA, MATERIALS_DEFAULTMATERIAL_MULTIVIEW_SIZE);
|
||||
#else
|
||||
assert_invariant(false);
|
||||
assert_invariant(false);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
mDefaultMaterial = downcast(defaultMaterialBuilder.build(*this));
|
||||
break;
|
||||
}
|
||||
mDefaultMaterial = downcast(defaultMaterialBuilder.build(*this));
|
||||
|
||||
// We must commit the default material instance here. It may not be used in a scene, but its
|
||||
// descriptor set may still be used for shared variants.
|
||||
@@ -1756,7 +1741,7 @@ void FEngine::compile(
|
||||
CallbackHandler* handler,
|
||||
Invocable<void(Material*)>&& callback) {
|
||||
auto const variants = getMaterialCompileVariants(view, shadowReceiver, skinning);
|
||||
const_cast<FMaterial*>(material)->compile(priority, variants, handler, std::move(callback));
|
||||
material->compile(priority, variants, handler, std::move(callback));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -170,6 +170,8 @@ FMaterial::FMaterial(FEngine& engine, const Builder& builder, MaterialDefinition
|
||||
|
||||
DriverApi& driver = engine.getDriverApi();
|
||||
|
||||
mIsStereoSupported = driver.isStereoSupported();
|
||||
mIsParallelShaderCompileSupported = driver.isParallelShaderCompileSupported();
|
||||
mDepthPrecacheDisabled =
|
||||
driver.isWorkaroundNeeded(Workaround::DISABLE_DEPTH_PRECACHE_FOR_DEFAULT_MATERIAL);
|
||||
mDefaultMaterial = engine.getDefaultMaterial();
|
||||
@@ -238,33 +240,44 @@ filament::DescriptorSetLayout const& FMaterial::getPerViewDescriptorSetLayout(
|
||||
void FMaterial::compile(CompilerPriorityQueue const priority,
|
||||
UserVariantFilterMask variantSpec,
|
||||
CallbackHandler* handler,
|
||||
Invocable<void(Material*)>&& callback) noexcept {
|
||||
FMaterialInstance* mi = getDefaultInstance();
|
||||
if (callback) {
|
||||
mi->compile(priority, variantSpec, handler,
|
||||
[this, callback = std::move(callback)](MaterialInstance*) {
|
||||
callback(this);
|
||||
});
|
||||
} else {
|
||||
mi->compile(priority, variantSpec, handler, {});
|
||||
Invocable<void(Material*)>&& callback) const noexcept {
|
||||
DriverApi& driver = mEngine.getDriverApi();
|
||||
|
||||
// Turn off the STE variant if stereo is not supported.
|
||||
if (!mIsStereoSupported) {
|
||||
variantSpec &= ~UserVariantFilterMask(UserVariantFilterBit::STE);
|
||||
}
|
||||
|
||||
UserVariantFilterMask const variantFilter = ~variantSpec & UserVariantFilterMask(UserVariantFilterBit::ALL);
|
||||
ShaderModel const shaderModel = mEngine.getShaderModel();
|
||||
bool const isStereoSupported = mEngine.getDriverApi().isStereoSupported();
|
||||
|
||||
if (UTILS_LIKELY(mIsParallelShaderCompileSupported)) {
|
||||
for (auto const variant: mDefinition.getVariants()) {
|
||||
if (!variantFilter || variant == Variant::filterUserVariant(variant, variantFilter)) {
|
||||
if (mDefinition.hasVariant(variant, shaderModel, isStereoSupported)) {
|
||||
prepareProgram(driver, variant, priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileAllPrograms(priority, handler, std::move(callback));
|
||||
}
|
||||
|
||||
void FMaterial::compile(CompilerPriorityQueue const priority,
|
||||
FixedCapacityVector<Variant> const& variants,
|
||||
CallbackHandler* handler,
|
||||
Invocable<void(Material*)>&& callback) noexcept {
|
||||
Invocable<void(Material*)>&& callback) const noexcept {
|
||||
DriverApi& driver = mEngine.getDriverApi();
|
||||
FMaterialInstance* mi = getDefaultInstance();
|
||||
|
||||
ShaderModel const shaderModel = mEngine.getShaderModel();
|
||||
bool const isStereoSupported = driver.isStereoSupported();
|
||||
bool const isParallelShaderCompileSupported = driver.isParallelShaderCompileSupported();
|
||||
|
||||
if (UTILS_LIKELY(isParallelShaderCompileSupported)) {
|
||||
if (UTILS_LIKELY(mIsParallelShaderCompileSupported)) {
|
||||
for (auto const variant : variants) {
|
||||
if (mDefinition.hasVariant(variant, shaderModel, isStereoSupported)) {
|
||||
mi->prepareProgram(driver, variant, priority);
|
||||
prepareProgram(driver, variant, priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,7 +287,7 @@ void FMaterial::compile(CompilerPriorityQueue const priority,
|
||||
|
||||
void FMaterial::compileAllPrograms(CompilerPriorityQueue const priority,
|
||||
CallbackHandler* handler,
|
||||
Invocable<void(Material*)>&& callback) noexcept {
|
||||
Invocable<void(Material*)>&& callback) const noexcept {
|
||||
DriverApi& driver = mEngine.getDriverApi();
|
||||
|
||||
if (callback) {
|
||||
|
||||
@@ -106,12 +106,12 @@ public:
|
||||
void compile(CompilerPriorityQueue priority,
|
||||
UserVariantFilterMask variantSpec,
|
||||
backend::CallbackHandler* handler,
|
||||
utils::Invocable<void(Material*)>&& callback) noexcept;
|
||||
utils::Invocable<void(Material*)>&& callback) const noexcept;
|
||||
|
||||
void compile(CompilerPriorityQueue priority,
|
||||
utils::FixedCapacityVector<Variant> const& variants,
|
||||
backend::CallbackHandler* handler,
|
||||
utils::Invocable<void(Material*)>&& callback) noexcept;
|
||||
utils::Invocable<void(Material*)>&& callback) const noexcept;
|
||||
|
||||
// Creates an instance of this material, specifying the batching mode.
|
||||
FMaterialInstance* createInstance(const char* name) const noexcept;
|
||||
@@ -130,6 +130,40 @@ public:
|
||||
|
||||
FEngine& getEngine() const noexcept { return mEngine; }
|
||||
|
||||
// prepareProgram creates the program for the material's given variant at the backend level.
|
||||
// Must be called outside of backend render pass.
|
||||
// Must be called before getProgram() below.
|
||||
backend::Handle<backend::HwProgram> prepareProgram(backend::DriverApi& driver,
|
||||
Variant const variant,
|
||||
backend::CompilerPriorityQueue const priorityQueue) const noexcept {
|
||||
return mPrograms.prepareProgram(driver, variant, priorityQueue);
|
||||
}
|
||||
|
||||
// getProgram returns the backend program for the material's given variant.
|
||||
// Must be called after prepareProgram().
|
||||
[[nodiscard]]
|
||||
backend::Handle<backend::HwProgram> getProgram(Variant variant) const noexcept {
|
||||
|
||||
if (UTILS_UNLIKELY(mEngine.features.material.enable_fog_as_postprocess)) {
|
||||
// if the fog as post-process feature is enabled, we need to proceed "as-if" the material
|
||||
// didn't have the FOG variant bit.
|
||||
if (getMaterialDomain() == MaterialDomain::SURFACE) {
|
||||
BlendingMode const blendingMode = getBlendingMode();
|
||||
bool const hasScreenSpaceRefraction = getRefractionMode() == RefractionMode::SCREEN_SPACE;
|
||||
bool const isBlendingCommand = !hasScreenSpaceRefraction &&
|
||||
(blendingMode != BlendingMode::OPAQUE && blendingMode != BlendingMode::MASKED);
|
||||
if (!isBlendingCommand) {
|
||||
variant.setFog(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if FILAMENT_ENABLE_MATDBG
|
||||
updateActiveProgramsForMatdbg(variant);
|
||||
#endif
|
||||
return mPrograms.getProgram(variant);
|
||||
}
|
||||
|
||||
// MaterialInstance::use() binds descriptor sets before drawing. For shared variants,
|
||||
// however, the material instance will call useShared() to bind the default material's sets
|
||||
// instead.
|
||||
@@ -276,9 +310,6 @@ public:
|
||||
}
|
||||
|
||||
/** @}*/
|
||||
|
||||
// Called by getProgram() to update active program list for matdbg UI.
|
||||
void updateActiveProgramsForMatdbg(Variant const variant) const noexcept;
|
||||
#endif
|
||||
|
||||
private:
|
||||
@@ -287,13 +318,15 @@ private:
|
||||
|
||||
void compileAllPrograms(CompilerPriorityQueue priority,
|
||||
backend::CallbackHandler* handler,
|
||||
utils::Invocable<void(Material*)>&& callback) noexcept;
|
||||
utils::Invocable<void(Material*)>&& callback) const noexcept;
|
||||
|
||||
MaterialDefinition const& mDefinition;
|
||||
|
||||
bool mIsDefaultMaterial = false;
|
||||
|
||||
bool mUseUboBatching = false;
|
||||
bool mIsStereoSupported = false;
|
||||
bool mIsParallelShaderCompileSupported = false;
|
||||
bool mDepthPrecacheDisabled = false;
|
||||
|
||||
FMaterial const* mDefaultMaterial = nullptr;
|
||||
@@ -308,6 +341,8 @@ private:
|
||||
mutable utils::Mutex mPendingEditsLock;
|
||||
std::unique_ptr<MaterialParser> mPendingEdits;
|
||||
std::unique_ptr<MaterialParser> mEditedMaterialParser;
|
||||
// Called by getProgram() to update active program list for matdbg UI.
|
||||
void updateActiveProgramsForMatdbg(Variant const variant) const noexcept;
|
||||
void setPendingEdits(std::unique_ptr<MaterialParser> pendingEdits) noexcept;
|
||||
bool hasPendingEdits() const noexcept;
|
||||
void latchPendingEdits() noexcept;
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
#include <filament/MaterialInstance.h>
|
||||
|
||||
#include "RenderPass.h"
|
||||
#include "MaterialParser.h"
|
||||
|
||||
#include "ds/DescriptorSetLayout.h"
|
||||
|
||||
@@ -132,7 +131,6 @@ FMaterialInstance::FMaterialInstance(FEngine& engine,
|
||||
mTextureParameters(other->mTextureParameters),
|
||||
mDescriptorSet(other->mDescriptorSet.duplicate(
|
||||
"MaterialInstance", mMaterial->getDescriptorSetLayout())),
|
||||
mPrograms(other->mPrograms),
|
||||
mPolygonOffset(other->mPolygonOffset),
|
||||
mStencilState(other->mStencilState),
|
||||
mMaskThreshold(other->mMaskThreshold),
|
||||
@@ -209,10 +207,6 @@ void FMaterialInstance::terminate(FEngine& engine) {
|
||||
if (ubHandle){
|
||||
driver.destroyBufferObject(*ubHandle);
|
||||
}
|
||||
|
||||
if (mPrograms.isInitialized()) {
|
||||
mPrograms.terminate(engine);
|
||||
}
|
||||
}
|
||||
|
||||
void FMaterialInstance::commit(FEngine& engine) const {
|
||||
@@ -265,39 +259,6 @@ void FMaterialInstance::commit(FEngine::DriverApi& driver, UboManager* uboManage
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
template<typename T>
|
||||
void FMaterialInstance::setConstantImpl(std::string_view name, T value) {
|
||||
auto const& constants = mMaterial->getDefinition().specializationConstantsNameToIndex;
|
||||
auto it = constants.find(name);
|
||||
FILAMENT_CHECK_PRECONDITION(it != constants.end()) << "Constant " << name << " does not exist";
|
||||
|
||||
if (UTILS_UNLIKELY(mPendingSpecializationConstants.empty())) {
|
||||
mPendingSpecializationConstants =
|
||||
FixedCapacityVector<backend::Program::SpecializationConstant>(
|
||||
getPrograms().getSpecializationConstants());
|
||||
}
|
||||
|
||||
uint32_t id = it->second + CONFIG_MAX_RESERVED_SPEC_CONSTANTS;
|
||||
mPendingSpecializationConstants[id] = value;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T FMaterialInstance::getConstantImpl(std::string_view name) const {
|
||||
auto const& constants = mMaterial->getDefinition().specializationConstantsNameToIndex;
|
||||
auto it = constants.find(name);
|
||||
FILAMENT_CHECK_PRECONDITION(it != constants.end()) << "Constant " << name << " does not exist";
|
||||
|
||||
uint32_t id = it->second + CONFIG_MAX_RESERVED_SPEC_CONSTANTS;
|
||||
|
||||
if (UTILS_UNLIKELY(!mPendingSpecializationConstants.empty())) {
|
||||
return std::get<T>(mPendingSpecializationConstants[id]);
|
||||
}
|
||||
|
||||
return getPrograms().getConstant<T>(id);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
void FMaterialInstance::setParameter(std::string_view const name,
|
||||
Handle<HwTexture> texture, SamplerParams const params) {
|
||||
auto const binding = mMaterial->getSamplerBinding(name);
|
||||
@@ -417,15 +378,6 @@ void FMaterialInstance::setTransparencyMode(TransparencyMode const mode) noexcep
|
||||
mTransparencyMode = mode;
|
||||
}
|
||||
|
||||
RasterState FMaterialInstance::getRasterState() const noexcept {
|
||||
RasterState rs = mMaterial->getRasterState();
|
||||
rs.culling = mCulling;
|
||||
rs.depthWrite = mDepthWrite;
|
||||
rs.depthFunc = mDepthFunc;
|
||||
rs.colorWrite = mColorWrite;
|
||||
return rs;
|
||||
}
|
||||
|
||||
void FMaterialInstance::setDepthCulling(bool const enable) noexcept {
|
||||
mDepthFunc = enable ? RasterState::DepthFunc::GE : RasterState::DepthFunc::A;
|
||||
}
|
||||
@@ -446,51 +398,6 @@ const char* FMaterialInstance::getName() const noexcept {
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
void FMaterialInstance::compile(CompilerPriorityQueue const priority,
|
||||
UserVariantFilterMask variantSpec, CallbackHandler* handler,
|
||||
Invocable<void(MaterialInstance*)>&& callback) noexcept {
|
||||
FEngine& engine = mMaterial->getEngine();
|
||||
DriverApi& driver = engine.getDriverApi();
|
||||
MaterialDefinition const& definition = mMaterial->getDefinition();
|
||||
|
||||
bool const isStereoSupported = driver.isStereoSupported();
|
||||
|
||||
// Turn off the STE variant if stereo is not supported.
|
||||
if (UTILS_LIKELY(!isStereoSupported)) {
|
||||
variantSpec &= ~UserVariantFilterMask(UserVariantFilterBit::STE);
|
||||
}
|
||||
|
||||
UserVariantFilterMask const variantFilter =
|
||||
~variantSpec & UserVariantFilterMask(UserVariantFilterBit::ALL);
|
||||
ShaderModel const shaderModel = engine.getShaderModel();
|
||||
|
||||
if (UTILS_LIKELY(driver.isParallelShaderCompileSupported())) {
|
||||
for (auto const variant: definition.getVariants()) {
|
||||
if (!variantFilter || variant == Variant::filterUserVariant(variant, variantFilter)) {
|
||||
if (definition.hasVariant(variant, shaderModel, isStereoSupported)) {
|
||||
prepareProgram(driver, variant, priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
struct Callback {
|
||||
Invocable<void(MaterialInstance*)> f;
|
||||
MaterialInstance* m;
|
||||
static void func(void* user) {
|
||||
auto* const c = static_cast<Callback*>(user);
|
||||
c->f(c->m);
|
||||
delete c;
|
||||
}
|
||||
};
|
||||
auto* const user = new (std::nothrow) Callback{ std::move(callback), this };
|
||||
driver.compilePrograms(priority, handler, &Callback::func, user);
|
||||
} else {
|
||||
driver.compilePrograms(priority, nullptr, nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void FMaterialInstance::use(FEngine::DriverApi& driver, Variant variant) const {
|
||||
assert_invariant(mDescriptorSet.getHandle());
|
||||
assert_invariant(!isUsingUboBatching() || BufferAllocator::isValid(getAllocationId()));
|
||||
@@ -595,36 +502,4 @@ void FMaterialInstance::fixMissingSamplers() const {
|
||||
}
|
||||
}
|
||||
|
||||
LocalProgramCache const& FMaterialInstance::getPrograms() const noexcept {
|
||||
return mPrograms.isInitialized() ? mPrograms : mMaterial->getPrograms();
|
||||
}
|
||||
|
||||
void FMaterialInstance::flushSpecializationConstants() const noexcept {
|
||||
if (mPendingSpecializationConstants.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mPrograms.isInitialized()) {
|
||||
mPrograms.initializeForMaterialInstance(mMaterial->getEngine(), *mMaterial);
|
||||
}
|
||||
mPrograms.setConstants(std::move(mPendingSpecializationConstants));
|
||||
mPendingSpecializationConstants.clear();
|
||||
}
|
||||
|
||||
#if FILAMENT_ENABLE_MATDBG
|
||||
|
||||
void FMaterialInstance::updateActiveProgramsForMatdbg(Variant const variant) const noexcept {
|
||||
mMaterial->updateActiveProgramsForMatdbg(variant);
|
||||
}
|
||||
|
||||
#endif // FILAMENT_ENABLE_MATDBG
|
||||
|
||||
template void FMaterialInstance::setConstantImpl<int32_t>(std::string_view name, int32_t value);
|
||||
template void FMaterialInstance::setConstantImpl<float>(std::string_view name, float value);
|
||||
template void FMaterialInstance::setConstantImpl<bool>(std::string_view name, bool value);
|
||||
|
||||
template int32_t FMaterialInstance::getConstantImpl<int32_t>(std::string_view name) const;
|
||||
template float FMaterialInstance::getConstantImpl<float>(std::string_view name) const;
|
||||
template bool FMaterialInstance::getConstantImpl<bool>(std::string_view name) const;
|
||||
|
||||
} // namespace filament
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#define TNT_FILAMENT_DETAILS_MATERIALINSTANCE_H
|
||||
|
||||
#include "downcast.h"
|
||||
#include "LocalProgramCache.h"
|
||||
|
||||
#include "UniformBuffer.h"
|
||||
|
||||
#include "ds/DescriptorSet.h"
|
||||
@@ -26,6 +26,8 @@
|
||||
#include "details/BufferAllocator.h"
|
||||
#include "details/Engine.h"
|
||||
|
||||
#include "private/backend/DriverApi.h"
|
||||
|
||||
#include <filament/MaterialInstance.h>
|
||||
|
||||
#include <private/filament/Variant.h>
|
||||
@@ -65,7 +67,7 @@ public:
|
||||
~FMaterialInstance() noexcept;
|
||||
|
||||
void terminate(FEngine& engine);
|
||||
|
||||
|
||||
void commit(FEngine& engine) const;
|
||||
|
||||
void commit(FEngine::DriverApi& driver, UboManager* uboManager) const;
|
||||
@@ -84,32 +86,6 @@ public:
|
||||
|
||||
UniformBuffer const& getUniformBuffer() const noexcept { return mUniforms; }
|
||||
|
||||
void compile(backend::CompilerPriorityQueue priority, UserVariantFilterMask variantSpec,
|
||||
backend::CallbackHandler* handler,
|
||||
utils::Invocable<void(MaterialInstance*)>&& callback) noexcept;
|
||||
|
||||
// prepareProgram creates the program for the material's given variant at the backend level.
|
||||
// Must be called outside of backend render pass.
|
||||
// Must be called before getProgram() below.
|
||||
backend::Handle<backend::HwProgram> prepareProgram(backend::DriverApi& driver,
|
||||
Variant const variant,
|
||||
backend::CompilerPriorityQueue const priorityQueue) const noexcept {
|
||||
flushSpecializationConstants();
|
||||
return getPrograms().prepareProgram(driver, variant, priorityQueue);
|
||||
}
|
||||
|
||||
// getProgram returns the backend program for the material's given variant.
|
||||
// Must be called after prepareProgram().
|
||||
//
|
||||
// See also Material::getProgram().
|
||||
[[nodiscard]]
|
||||
backend::Handle<backend::HwProgram> getProgram(Variant const variant) const noexcept {
|
||||
#if FILAMENT_ENABLE_MATDBG
|
||||
updateActiveProgramsForMatdbg(variant);
|
||||
#endif
|
||||
return getPrograms().getProgram(variant);
|
||||
}
|
||||
|
||||
void setScissor(uint32_t const left, uint32_t const bottom, uint32_t const width, uint32_t const height) noexcept {
|
||||
constexpr uint32_t maxvalu = std::numeric_limits<int32_t>::max();
|
||||
mScissorRect = { int32_t(left), int32_t(bottom),
|
||||
@@ -131,8 +107,6 @@ public:
|
||||
|
||||
bool hasScissor() const noexcept { return mHasScissor; }
|
||||
|
||||
backend::RasterState getRasterState() const noexcept;
|
||||
|
||||
backend::CullingMode getCullingMode() const noexcept { return mCulling; }
|
||||
|
||||
backend::CullingMode getShadowCullingMode() const noexcept { return mShadowCulling; }
|
||||
@@ -280,15 +254,11 @@ public:
|
||||
backend::Handle<backend::HwTexture> texture, backend::SamplerParams params);
|
||||
|
||||
using MaterialInstance::setParameter;
|
||||
using MaterialInstance::setConstant;
|
||||
|
||||
private:
|
||||
friend class FMaterial;
|
||||
friend class MaterialInstance;
|
||||
|
||||
// Cannot inline since it inspects the FMaterial class.
|
||||
LocalProgramCache const& getPrograms() const noexcept;
|
||||
|
||||
template<size_t Size>
|
||||
void setParameterUntypedImpl(std::string_view name, const void* value);
|
||||
|
||||
@@ -307,19 +277,6 @@ private:
|
||||
template<typename T>
|
||||
T getParameterImpl(std::string_view name) const;
|
||||
|
||||
template<typename T>
|
||||
void setConstantImpl(std::string_view name, T value);
|
||||
|
||||
template<typename T>
|
||||
T getConstantImpl(std::string_view name) const;
|
||||
|
||||
void flushSpecializationConstants() const noexcept;
|
||||
|
||||
#if FILAMENT_ENABLE_MATDBG
|
||||
// Called by getProgram() to update active program list for matdbg UI.
|
||||
void updateActiveProgramsForMatdbg(Variant const variant) const noexcept;
|
||||
#endif
|
||||
|
||||
// keep these grouped, they're accessed together in the render-loop
|
||||
FMaterial const* mMaterial = nullptr;
|
||||
|
||||
@@ -334,11 +291,6 @@ private:
|
||||
mutable DescriptorSet mDescriptorSet;
|
||||
UniformBuffer mUniforms;
|
||||
|
||||
// HACK: Mutable so that prepareProgram() can update specialization constants.
|
||||
mutable LocalProgramCache mPrograms;
|
||||
mutable utils::FixedCapacityVector<backend::Program::SpecializationConstant>
|
||||
mPendingSpecializationConstants;
|
||||
|
||||
backend::PolygonOffset mPolygonOffset{};
|
||||
backend::StencilState mStencilState{};
|
||||
|
||||
|
||||
@@ -132,26 +132,19 @@ FSkybox::FSkybox(FEngine& engine, const Builder& builder) noexcept
|
||||
|
||||
FMaterial const* FSkybox::createMaterial(FEngine& engine) {
|
||||
Material::Builder builder;
|
||||
#ifdef FILAMENT_ENABLE_FEATURE_LEVEL_0
|
||||
if (UTILS_UNLIKELY(engine.getActiveFeatureLevel() == Engine::FeatureLevel::FEATURE_LEVEL_0)) {
|
||||
builder.package(MATERIALS_SKYBOX_FL0_DATA, MATERIALS_SKYBOX_FL0_SIZE);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
switch (engine.getConfig().stereoscopicType) {
|
||||
case Engine::StereoscopicType::NONE:
|
||||
case Engine::StereoscopicType::INSTANCED:
|
||||
builder.package(MATERIALS_SKYBOX_DATA, MATERIALS_SKYBOX_SIZE);
|
||||
break;
|
||||
case Engine::StereoscopicType::MULTIVIEW:
|
||||
switch (engine.getConfig().stereoscopicType) {
|
||||
case Engine::StereoscopicType::NONE:
|
||||
case Engine::StereoscopicType::INSTANCED:
|
||||
builder.package(MATERIALS_SKYBOX_DATA, MATERIALS_SKYBOX_SIZE);
|
||||
break;
|
||||
case Engine::StereoscopicType::MULTIVIEW:
|
||||
#ifdef FILAMENT_ENABLE_MULTIVIEW
|
||||
builder.package(MATERIALS_SKYBOX_MULTIVIEW_DATA, MATERIALS_SKYBOX_MULTIVIEW_SIZE);
|
||||
builder.package(MATERIALS_SKYBOX_MULTIVIEW_DATA, MATERIALS_SKYBOX_MULTIVIEW_SIZE);
|
||||
#else
|
||||
PANIC_POSTCONDITION("Multiview is enabled in the Engine, but this build has not "
|
||||
"been compiled for multiview.");
|
||||
PANIC_POSTCONDITION("Multiview is enabled in the Engine, but this build has not "
|
||||
"been compiled for multiview.");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
auto material = builder.build(engine);
|
||||
return downcast(material);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
material {
|
||||
name : "Filament Default Material",
|
||||
shadingModel : unlit
|
||||
shadingModel : unlit,
|
||||
featureLevel: 0
|
||||
}
|
||||
|
||||
fragment {
|
||||
|
||||
@@ -26,7 +26,8 @@ material {
|
||||
depthWrite : false,
|
||||
shadingModel : unlit,
|
||||
variantFilter : [ skinning, shadowReceiver, vsm ],
|
||||
culling: none
|
||||
culling: none,
|
||||
featureLevel: 0
|
||||
}
|
||||
|
||||
fragment {
|
||||
|
||||
@@ -73,13 +73,14 @@ void computeAmbientOcclusionSAO(inout float occlusion, inout vec3 bentNormal,
|
||||
|
||||
// now we have the sample, compute AO
|
||||
highp vec3 v = p - origin; // sample vector
|
||||
float vv = dot(v, v); // squared distance
|
||||
float vn = dot(v, normal); // distance * cos(v, normal)
|
||||
highp float vv = dot(v, v); // squared distance
|
||||
highp float vn = dot(v, normal); // distance * cos(v, normal)
|
||||
|
||||
// discard samples that are outside of the radius, preventing distant geometry to
|
||||
// cast shadows -- there are many functions that work and choosing one is an artistic
|
||||
// decision.
|
||||
float w = sq(max(0.0, 1.0 - vv * materialParams.invRadiusSquared));
|
||||
// Need to be highp to avoid imprecision on certain hardware (e.g. PowerVR)
|
||||
highp float w = sq(max(0.0, 1.0 - vv * materialParams.invRadiusSquared));
|
||||
|
||||
// discard samples that are too close to the horizon to reduce shadows cast by geometry
|
||||
// not sufficently tessellated. The goal is to discard samples that form an angle 'beta'
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
#include <filament/Engine.h>
|
||||
#include <filament/Material.h>
|
||||
#include <filament/MaterialInstance.h>
|
||||
|
||||
#include "filament_test_resources.h"
|
||||
|
||||
@@ -168,52 +167,3 @@ TEST(Material, MaterialSettingInvalidApiLevelReturnsAnInvalidPackage) {
|
||||
|
||||
Engine::destroy(engine);
|
||||
}
|
||||
|
||||
TEST(MaterialInstanceTest, SetConstant) {
|
||||
Engine* engine = Engine::create(Engine::Backend::NOOP);
|
||||
|
||||
std::string shaderCode(R"(
|
||||
void material(inout MaterialInputs material) {
|
||||
prepareMaterial(material);
|
||||
material.baseColor = vec4(1.0);
|
||||
}
|
||||
)");
|
||||
|
||||
filamat::MaterialBuilder builder;
|
||||
builder.init();
|
||||
builder.name("MaterialInstanceTest");
|
||||
builder.material(shaderCode.c_str());
|
||||
builder.constant("myFloat", filamat::MaterialBuilder::ConstantType::FLOAT, 1.0f);
|
||||
builder.constant("myInt", filamat::MaterialBuilder::ConstantType::INT, 2);
|
||||
builder.constant("myBool", filamat::MaterialBuilder::ConstantType::BOOL, false);
|
||||
|
||||
filamat::Package result = builder.build(engine->getJobSystem());
|
||||
ASSERT_TRUE(result.isValid());
|
||||
|
||||
Material* material = Material::Builder()
|
||||
.package(result.getData(), result.getSize())
|
||||
.build(*engine);
|
||||
ASSERT_NE(material, nullptr);
|
||||
|
||||
MaterialInstance* instance = material->createInstance();
|
||||
ASSERT_NE(instance, nullptr);
|
||||
|
||||
// Verify default values
|
||||
EXPECT_EQ(instance->getConstant<float>("myFloat"), 1.0f);
|
||||
EXPECT_EQ(instance->getConstant<int32_t>("myInt"), 2);
|
||||
EXPECT_EQ(instance->getConstant<bool>("myBool"), false);
|
||||
|
||||
// Set new values
|
||||
instance->setConstant("myFloat", 3.0f);
|
||||
instance->setConstant("myInt", 4);
|
||||
instance->setConstant("myBool", true);
|
||||
|
||||
// Verify new values
|
||||
EXPECT_EQ(instance->getConstant<float>("myFloat"), 3.0f);
|
||||
EXPECT_EQ(instance->getConstant<int32_t>("myInt"), 4);
|
||||
EXPECT_EQ(instance->getConstant<bool>("myBool"), true);
|
||||
|
||||
engine->destroy(instance);
|
||||
engine->destroy(material);
|
||||
Engine::destroy(engine);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "Filament"
|
||||
spec.version = "1.71.1"
|
||||
spec.version = "1.71.0"
|
||||
spec.license = { :type => "Apache 2.0", :file => "LICENSE" }
|
||||
spec.homepage = "https://google.github.io/filament"
|
||||
spec.authors = "Google LLC."
|
||||
spec.summary = "Filament is a real-time physically based rendering engine for Android, iOS, Windows, Linux, macOS, and WASM/WebGL."
|
||||
spec.platform = :ios, "11.0"
|
||||
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.71.1/filament-v1.71.1-ios.tgz" }
|
||||
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.71.0/filament-v1.71.0-ios.tgz" }
|
||||
|
||||
spec.libraries = 'c++'
|
||||
|
||||
|
||||
@@ -186,6 +186,9 @@ bool MaterialChunk::getTextShader(Unflattener unflattener,
|
||||
if (!unflattener.read(&shaderSize)){
|
||||
return false;
|
||||
}
|
||||
if (shaderSize == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read how many lines there are.
|
||||
uint32_t lineCount = 0;
|
||||
|
||||
@@ -941,8 +941,28 @@ bool MaterialBuilder::generateShaders(JobSystem& jobSystem, const std::vector<Va
|
||||
if (params.featureLevel == FeatureLevel::FEATURE_LEVEL_0) {
|
||||
assert_invariant(params.shaderModel == ShaderModel::MOBILE);
|
||||
assert_invariant(params.targetApi == TargetApi::OPENGL);
|
||||
// skip all variants that can't be used with ESSL1
|
||||
if (filament::Variant::isValidStandardVariant(v.variant)) {
|
||||
if (v.variant.hasDirectionalLighting()) {
|
||||
continue;
|
||||
}
|
||||
if (v.variant.hasDynamicLighting()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (filament::Variant::isShadowReceiverVariant(v.variant)) {
|
||||
continue;
|
||||
}
|
||||
if (filament::Variant::isStereoVariant(v.variant)) {
|
||||
// the stereo variant can never be used at feature level 0
|
||||
continue;
|
||||
}
|
||||
if (filament::Variant::isDepthMomentsVariant(v.variant)) {
|
||||
continue;
|
||||
}
|
||||
if (filament::Variant::isShadowSampler2DVariant(v.variant)) {
|
||||
continue;
|
||||
}
|
||||
if (filament::Variant::isSSRVariant(v.variant)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1010,8 +1030,9 @@ bool MaterialBuilder::generateShaders(JobSystem& jobSystem, const std::vector<Va
|
||||
return "unknown";
|
||||
};
|
||||
char filename[256];
|
||||
snprintf(filename, sizeof(filename), "%s_0x%02x.%s", mMaterialName.c_str_safe(),
|
||||
variantKey, getExtension(v.stage));
|
||||
snprintf(filename, sizeof(filename), "%s_0x%02x_fl%d.%s",
|
||||
mMaterialName.c_str_safe(), variantKey, (int) featureLevel,
|
||||
getExtension(v.stage));
|
||||
printf("Writing variant 0x%02x to %s\n", variantKey, filename);
|
||||
std::ofstream file(filename);
|
||||
if (file.is_open()) {
|
||||
@@ -1359,16 +1380,6 @@ error:
|
||||
goto error;
|
||||
}
|
||||
|
||||
// adjust variant-filter for feature level *before* we start writing into the container
|
||||
if (mFeatureLevel == FeatureLevel::FEATURE_LEVEL_0) {
|
||||
// at feature level 0, many variants are not supported
|
||||
mVariantFilter |= uint32_t(UserVariantFilterBit::DIRECTIONAL_LIGHTING);
|
||||
mVariantFilter |= uint32_t(UserVariantFilterBit::DYNAMIC_LIGHTING);
|
||||
mVariantFilter |= uint32_t(UserVariantFilterBit::SHADOW_RECEIVER);
|
||||
mVariantFilter |= uint32_t(UserVariantFilterBit::VSM);
|
||||
mVariantFilter |= uint32_t(UserVariantFilterBit::SSR);
|
||||
}
|
||||
|
||||
// Create chunk tree.
|
||||
ChunkContainer container;
|
||||
writeCommonChunks(container, info);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "ShaderMinifier.h"
|
||||
|
||||
#include <utils/Log.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace filamat {
|
||||
|
||||
|
||||
@@ -67,7 +67,11 @@ void ArchiveCache::load(const void* archiveData, uint64_t archiveByteCount) {
|
||||
uint64_t* basePointer = (uint64_t*) utils::aligned_alloc(decompSize, 8);
|
||||
ZSTD_decompress(basePointer, decompSize, archiveData, archiveByteCount);
|
||||
mArchive = (ReadableArchive*) basePointer;
|
||||
convertOffsetsToPointers(mArchive);
|
||||
if (!convertOffsetsToPointers(mArchive, decompSize)) {
|
||||
utils::aligned_free(basePointer);
|
||||
mArchive = nullptr;
|
||||
return;
|
||||
}
|
||||
mMaterials = FixedCapacityVector<Material*>(mArchive->specsCount, nullptr);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "Utility.h"
|
||||
#include "extended/ResourceLoaderExtended.h"
|
||||
|
||||
#include <limits>
|
||||
#include <filament/BufferObject.h>
|
||||
#include <filament/Engine.h>
|
||||
#include <filament/IndexBuffer.h>
|
||||
@@ -163,9 +164,40 @@ inline void normalizeSkinningWeights(cgltf_data const* gltf) {
|
||||
LOG(WARNING) << "Cannot normalize weights, unsupported attribute type.";
|
||||
return;
|
||||
}
|
||||
if (!data->buffer_view || !data->buffer_view->buffer ||
|
||||
!data->buffer_view->buffer->data) {
|
||||
LOG(WARNING) << "Cannot normalize weights, missing buffer data.";
|
||||
return;
|
||||
}
|
||||
const cgltf_size bufferSize = data->buffer_view->buffer->size;
|
||||
const cgltf_size viewOffset = data->buffer_view->offset;
|
||||
const cgltf_size accessorOffset = data->offset;
|
||||
const cgltf_size totalOffset = viewOffset + accessorOffset;
|
||||
|
||||
if (totalOffset >= bufferSize) {
|
||||
LOG(WARNING) << "Cannot normalize weights, accessor offset exceeds buffer size.";
|
||||
return;
|
||||
}
|
||||
|
||||
const cgltf_size availableBytes = bufferSize - totalOffset;
|
||||
const cgltf_size stride = data->stride;
|
||||
|
||||
cgltf_size maxCount = 0;
|
||||
if (stride > 0 && availableBytes >= sizeof(float4)) {
|
||||
maxCount = 1 + (availableBytes - sizeof(float4)) / stride;
|
||||
}
|
||||
|
||||
cgltf_size safeCount = data->count;
|
||||
if (safeCount > maxCount) {
|
||||
LOG(WARNING) << "Skinning weights accessor count (" << safeCount
|
||||
<< ") exceeds buffer capacity (" << maxCount
|
||||
<< "), clamping to prevent out-of-bounds access.";
|
||||
safeCount = maxCount;
|
||||
}
|
||||
|
||||
uint8_t* bytes = (uint8_t*) data->buffer_view->buffer->data;
|
||||
bytes += data->offset + data->buffer_view->offset;
|
||||
for (cgltf_size i = 0, n = data->count; i < n; ++i, bytes += data->stride) {
|
||||
bytes += totalOffset;
|
||||
for (cgltf_size i = 0, n = safeCount; i < n; ++i, bytes += stride) {
|
||||
float4* weights = (float4*) bytes;
|
||||
const float sum = weights->x + weights->y + weights->z + weights->w;
|
||||
*weights /= sum;
|
||||
@@ -243,8 +275,20 @@ inline void uploadBuffers(FFilamentAsset* asset, Engine& engine,
|
||||
|
||||
// For morph targets without buffer_view, use cgltf_accessor_unpack_floats to unpack data directly
|
||||
if (!accessor->buffer_view && isMorphTarget) {
|
||||
const size_t floatsCount = accessor->count * cgltf_num_components(accessor->type);
|
||||
const size_t components = cgltf_num_components(accessor->type);
|
||||
if (components > 0 && accessor->count > std::numeric_limits<size_t>::max() / components) {
|
||||
continue;
|
||||
}
|
||||
const size_t floatsCount = accessor->count * components;
|
||||
if (floatsCount == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (floatsCount > std::numeric_limits<size_t>::max() / sizeof(float)) {
|
||||
continue;
|
||||
}
|
||||
const size_t floatsByteCount = sizeof(float) * floatsCount;
|
||||
|
||||
float* floatsData = (float*)malloc(floatsByteCount);
|
||||
cgltf_accessor_unpack_floats(accessor, floatsData, floatsCount);
|
||||
|
||||
@@ -273,10 +317,48 @@ inline void uploadBuffers(FFilamentAsset* asset, Engine& engine,
|
||||
}
|
||||
assert_invariant(bufferData);
|
||||
const uint32_t size = utility::computeBindingSize(accessor);
|
||||
if (size == 0) {
|
||||
// Skip buffer upload for this accessor as requested by user
|
||||
continue;
|
||||
}
|
||||
if (slot.vertexBuffer) {
|
||||
if (utility::requiresConversion(accessor)) {
|
||||
const size_t floatsCount = accessor->count * cgltf_num_components(accessor->type);
|
||||
const cgltf_size bufferSize = accessor->buffer_view->buffer->size;
|
||||
const cgltf_size totalOffset = accessor->buffer_view->offset + accessor->offset;
|
||||
|
||||
if (totalOffset >= bufferSize) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cgltf_size availableBytes = bufferSize - totalOffset;
|
||||
const cgltf_size stride = accessor->stride;
|
||||
const cgltf_size elementSize = cgltf_calc_size(accessor->type, accessor->component_type);
|
||||
|
||||
cgltf_size maxCount = 0;
|
||||
if (stride > 0 && availableBytes >= elementSize) {
|
||||
maxCount = 1 + (availableBytes - elementSize) / stride;
|
||||
}
|
||||
|
||||
cgltf_size safeCount = accessor->count;
|
||||
if (safeCount > maxCount) {
|
||||
LOG(WARNING) << "Accessor count exceeds buffer capacity, clamping.";
|
||||
safeCount = maxCount;
|
||||
}
|
||||
|
||||
const size_t components = cgltf_num_components(accessor->type);
|
||||
if (components > 0 && safeCount > std::numeric_limits<size_t>::max() / components) {
|
||||
continue;
|
||||
}
|
||||
const size_t floatsCount = safeCount * components;
|
||||
if (floatsCount == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (floatsCount > std::numeric_limits<size_t>::max() / sizeof(float)) {
|
||||
continue;
|
||||
}
|
||||
const size_t floatsByteCount = sizeof(float) * floatsCount;
|
||||
|
||||
float* floatsData = (float*) malloc(floatsByteCount);
|
||||
cgltf_accessor_unpack_floats(accessor, floatsData, floatsCount);
|
||||
BufferObject* bo = BufferObject::Builder().size(floatsByteCount).build(engine);
|
||||
@@ -313,8 +395,42 @@ inline void uploadBuffers(FFilamentAsset* asset, Engine& engine,
|
||||
assert(slot.morphTargetBuffer);
|
||||
|
||||
if (utility::requiresPacking(accessor)) {
|
||||
const size_t floatsCount = accessor->count * cgltf_num_components(accessor->type);
|
||||
const cgltf_size bufferSize = accessor->buffer_view->buffer->size;
|
||||
const cgltf_size totalOffset = accessor->buffer_view->offset + accessor->offset;
|
||||
|
||||
if (totalOffset >= bufferSize) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cgltf_size availableBytes = bufferSize - totalOffset;
|
||||
const cgltf_size stride = accessor->stride;
|
||||
const cgltf_size elementSize = cgltf_calc_size(accessor->type, accessor->component_type);
|
||||
|
||||
cgltf_size maxCount = 0;
|
||||
if (stride > 0 && availableBytes >= elementSize) {
|
||||
maxCount = 1 + (availableBytes - elementSize) / stride;
|
||||
}
|
||||
|
||||
cgltf_size safeCount = accessor->count;
|
||||
if (safeCount > maxCount) {
|
||||
LOG(WARNING) << "Accessor count exceeds buffer capacity, clamping.";
|
||||
safeCount = maxCount;
|
||||
}
|
||||
|
||||
const size_t components = cgltf_num_components(accessor->type);
|
||||
if (components > 0 && safeCount > std::numeric_limits<size_t>::max() / components) {
|
||||
continue;
|
||||
}
|
||||
const size_t floatsCount = safeCount * components;
|
||||
if (floatsCount == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (floatsCount > std::numeric_limits<size_t>::max() / sizeof(float)) {
|
||||
continue;
|
||||
}
|
||||
const size_t floatsByteCount = sizeof(float) * floatsCount;
|
||||
|
||||
float* floatsData = (float*) malloc(floatsByteCount);
|
||||
cgltf_accessor_unpack_floats(accessor, floatsData, floatsCount);
|
||||
if (accessor->type == cgltf_type_vec3) {
|
||||
@@ -467,7 +583,9 @@ bool ResourceLoader::loadResources(FFilamentAsset* asset, bool async) {
|
||||
}
|
||||
utility::decodeDracoMeshes(gltf, prim, dracoCache);
|
||||
}
|
||||
utility::decodeMeshoptCompression((cgltf_data*) gltf);
|
||||
if (!utility::decodeMeshoptCompression((cgltf_data*) gltf)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uploadBuffers(asset, *pImpl->mEngine, pImpl->mUriDataCache);
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ void decodeDracoMeshes(cgltf_data const* gltf, cgltf_primitive const* prim,
|
||||
}
|
||||
}
|
||||
|
||||
void decodeMeshoptCompression(cgltf_data* data) {
|
||||
bool decodeMeshoptCompression(cgltf_data* data) {
|
||||
for (size_t i = 0; i < data->buffer_views_count; ++i) {
|
||||
if (!data->buffer_views[i].has_meshopt_compression) {
|
||||
continue;
|
||||
@@ -97,6 +97,11 @@ void decodeMeshoptCompression(cgltf_data* data) {
|
||||
assert_invariant(source);
|
||||
source += compression->offset;
|
||||
|
||||
if (compression->stride == 0) {
|
||||
slog.e << "gltfio: meshopt decompression failed, stride is 0." << io::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t const theoreticalMaxCount = std::numeric_limits<size_t>::max() / compression->stride;
|
||||
FILAMENT_CHECK_PRECONDITION(compression->count <= theoreticalMaxCount)
|
||||
<< "gltfio: meshopt decompression exceeds maximum count of " << theoreticalMaxCount
|
||||
@@ -107,7 +112,7 @@ void decodeMeshoptCompression(cgltf_data* data) {
|
||||
void* destination = malloc(compression->count * compression->stride);
|
||||
assert_invariant(destination);
|
||||
|
||||
UTILS_UNUSED_IN_RELEASE int error = 0;
|
||||
int error = 0;
|
||||
switch (compression->mode) {
|
||||
case cgltf_meshopt_compression_mode_invalid:
|
||||
break;
|
||||
@@ -127,7 +132,12 @@ void decodeMeshoptCompression(cgltf_data* data) {
|
||||
assert_invariant(false);
|
||||
break;
|
||||
}
|
||||
assert_invariant(!error);
|
||||
|
||||
if (error != 0) {
|
||||
slog.e << "gltfio: meshopt decompression failed with error " << error << io::endl;
|
||||
free(destination);
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (compression->filter) {
|
||||
case cgltf_meshopt_compression_filter_none:
|
||||
@@ -148,6 +158,7 @@ void decodeMeshoptCompression(cgltf_data* data) {
|
||||
|
||||
data->buffer_views[i].data = destination;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool primitiveHasVertexColor(cgltf_primitive* inPrim) {
|
||||
@@ -167,6 +178,9 @@ bool primitiveHasVertexColor(cgltf_primitive* inPrim) {
|
||||
// exist in the glTF we need to compute it manually. This is a bit of a cheat, cgltf_calc_size is
|
||||
// private but its implementation file is available in this cpp file.
|
||||
uint32_t computeBindingSize(cgltf_accessor const* accessor) {
|
||||
if (accessor->count == 0) {
|
||||
return 0;
|
||||
}
|
||||
cgltf_size element_size = cgltf_calc_size(accessor->type, accessor->component_type);
|
||||
return uint32_t(accessor->stride * (accessor->count - 1) + element_size);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace filament::gltfio::utility {
|
||||
|
||||
// Functions that are shared between the original implementation and the extended implementation.
|
||||
void decodeDracoMeshes(cgltf_data const* gltf, cgltf_primitive const* prim, DracoCache* dracoCache);
|
||||
void decodeMeshoptCompression(cgltf_data* data);
|
||||
bool decodeMeshoptCompression(cgltf_data* data);
|
||||
bool primitiveHasVertexColor(cgltf_primitive* inPrim);
|
||||
uint32_t computeBindingSize(cgltf_accessor const* accessor);
|
||||
void convertBytesToShorts(uint16_t* dst, uint8_t const* src, size_t count);
|
||||
|
||||
@@ -16,11 +16,18 @@
|
||||
|
||||
#include <image/Ktx1Bundle.h>
|
||||
|
||||
#include <utils/Logger.h>
|
||||
#include <utils/Panic.h>
|
||||
#include <utils/string.h>
|
||||
|
||||
#include <math/vec3.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <new>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -39,7 +46,7 @@ struct SerializationHeader {
|
||||
static_assert(sizeof(SerializationHeader) == 16 * 4, "Unexpected header size.");
|
||||
|
||||
// We flatten the three-dimensional blob index using the ordering defined by the KTX spec.
|
||||
inline size_t flatten(const image::Ktx1Bundle* bundle, image::KtxBlobIndex index) {
|
||||
size_t flatten(const image::Ktx1Bundle* bundle, image::KtxBlobIndex index) {
|
||||
const uint32_t nfaces = bundle->isCubemap() ? 6 : 1;
|
||||
const uint32_t nlayers = bundle->getArrayLength();
|
||||
return index.cubeFace + index.arrayIndex * nfaces + index.mipLevel * nfaces * nlayers;
|
||||
@@ -83,7 +90,7 @@ struct KtxBlobList {
|
||||
postSize += sizes[i];
|
||||
}
|
||||
}
|
||||
uint32_t oldSize = sizes[blobIndex];
|
||||
uint32_t const oldSize = sizes[blobIndex];
|
||||
std::vector<uint8_t> newBlobs(blobs.size() + newSize - oldSize);
|
||||
uint8_t const* src = blobs.data();
|
||||
uint8_t* dst = newBlobs.data();
|
||||
@@ -102,19 +109,27 @@ struct KtxBlobList {
|
||||
Ktx1Bundle::~Ktx1Bundle() = default;
|
||||
|
||||
Ktx1Bundle::Ktx1Bundle(uint32_t numMipLevels, uint32_t arrayLength, bool isCubemap) :
|
||||
mBlobs(new KtxBlobList), mMetadata(new KtxMetadata) {
|
||||
mBlobs(new(std::nothrow) KtxBlobList), mMetadata(new(std::nothrow) KtxMetadata) {
|
||||
uint64_t const totalBlobs = uint64_t(numMipLevels) * arrayLength * (isCubemap ? 6 : 1);
|
||||
if (totalBlobs > uint64_t(std::numeric_limits<uint32_t>::max())) {
|
||||
LOG(WARNING) << "KTX dimensions overflow";
|
||||
mNumMipLevels = 0;
|
||||
mArrayLength = 0;
|
||||
mNumCubeFaces = 0;
|
||||
return;
|
||||
}
|
||||
mNumMipLevels = numMipLevels;
|
||||
mArrayLength = arrayLength;
|
||||
mNumCubeFaces = isCubemap ? 6 : 1;
|
||||
mBlobs->sizes.resize(numMipLevels * arrayLength * mNumCubeFaces);
|
||||
mBlobs->sizes.resize(uint32_t(totalBlobs));
|
||||
}
|
||||
|
||||
Ktx1Bundle::Ktx1Bundle(uint8_t const* bytes, uint32_t nbytes) :
|
||||
mBlobs(new KtxBlobList), mMetadata(new KtxMetadata) {
|
||||
mBlobs(new(std::nothrow) KtxBlobList), mMetadata(new(std::nothrow) KtxMetadata) {
|
||||
FILAMENT_CHECK_POSTCONDITION(sizeof(SerializationHeader) <= nbytes) << "KTX buffer is too small";
|
||||
|
||||
// First, "parse" the header by casting it to a struct.
|
||||
SerializationHeader const* header = (SerializationHeader const*) bytes;
|
||||
SerializationHeader const* header = reinterpret_cast<SerializationHeader const*>(bytes);
|
||||
FILAMENT_CHECK_POSTCONDITION(memcmp(header->magic, MAGIC, 12) == 0)
|
||||
<< "KTX has unexpected identifier";
|
||||
mInfo = header->info;
|
||||
@@ -149,12 +164,12 @@ Ktx1Bundle::Ktx1Bundle(uint8_t const* bytes, uint32_t nbytes) :
|
||||
size_t const keyLength = keyEnd - keyStart;
|
||||
FILAMENT_CHECK_POSTCONDITION(keyLength < keyAndValueByteSize) << "KTX metadata key is not null terminated";
|
||||
|
||||
std::string key(keyStart, keyLength);
|
||||
std::string const key(keyStart, keyLength);
|
||||
uint8_t const* pval = pdata + keyLength + 1;
|
||||
size_t const valLength = keyAndValueByteSize - (keyLength + 1);
|
||||
|
||||
pdata += keyAndValueByteSize;
|
||||
std::string val((const char*) pval, valLength);
|
||||
std::string const val(reinterpret_cast<const char*>(pval), valLength);
|
||||
mMetadata->keyvals.insert({key, val});
|
||||
|
||||
const uint32_t paddingSize = 3 - ((keyAndValueByteSize + 3) % 4);
|
||||
@@ -211,7 +226,7 @@ Ktx1Bundle::Ktx1Bundle(uint8_t const* bytes, uint32_t nbytes) :
|
||||
}
|
||||
|
||||
bool Ktx1Bundle::serialize(uint8_t* destination, uint32_t numBytes) const {
|
||||
uint32_t requiredLength = getSerializedLength();
|
||||
uint32_t const requiredLength = getSerializedLength();
|
||||
if (numBytes < requiredLength) {
|
||||
return false;
|
||||
}
|
||||
@@ -264,15 +279,15 @@ bool Ktx1Bundle::serialize(uint8_t* destination, uint32_t numBytes) const {
|
||||
for (uint32_t mipmap = 0; mipmap < mNumMipLevels; ++mipmap) {
|
||||
|
||||
// Every blob in a given miplevel has the same size, and each miplevel has at least one
|
||||
// blob. Therefore we can safely determine each of the so-called "imageSize" fields in KTX
|
||||
// blob. Therefore, we can safely determine each of the so-called "imageSize" fields in KTX
|
||||
// by simply looking at the first blob in the LOD.
|
||||
uint32_t faceSize;
|
||||
uint8_t* blobData;
|
||||
if (!getBlob({mipmap, 0, 0}, &blobData, &faceSize)) {
|
||||
return false;
|
||||
}
|
||||
uint32_t imageSize = isNonArrayCube ? faceSize : (faceSize * facesPerMip);
|
||||
*((uint32_t*) pdata) = imageSize;
|
||||
uint32_t const imageSize = isNonArrayCube ? faceSize : (faceSize * facesPerMip);
|
||||
*reinterpret_cast<uint32_t*>(pdata) = imageSize;
|
||||
pdata += sizeof(imageSize);
|
||||
|
||||
// Copy out all layer and face blobs for this mipmap at once, since they are contiguous
|
||||
@@ -300,15 +315,15 @@ uint32_t Ktx1Bundle::getSerializedLength() const {
|
||||
FILAMENT_CHECK_POSTCONDITION(mBlobs->sizes[startIndex + i] == blobSize)
|
||||
<< "Inconsistent blob sizes within LOD";
|
||||
}
|
||||
total += (uint64_t)blobSize * numElements;
|
||||
total += uint64_t(blobSize) * numElements;
|
||||
}
|
||||
FILAMENT_CHECK_POSTCONDITION(total <= (uint64_t)std::numeric_limits<uint32_t>::max())
|
||||
FILAMENT_CHECK_POSTCONDITION(total <= uint64_t(std::numeric_limits<uint32_t>::max()))
|
||||
<< "KTX serialization size overflow";
|
||||
return (uint32_t)total;
|
||||
}
|
||||
|
||||
const char* Ktx1Bundle::getMetadata(const char* key, size_t* valueSize) const {
|
||||
auto iter = mMetadata->keyvals.find(key);
|
||||
auto const iter = mMetadata->keyvals.find(key);
|
||||
if (iter == mMetadata->keyvals.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -346,8 +361,8 @@ bool Ktx1Bundle::getBlob(KtxBlobIndex index, uint8_t** data, uint32_t* size) con
|
||||
index.cubeFace >= mNumCubeFaces) {
|
||||
return false;
|
||||
}
|
||||
uint32_t flatIndex = flatten(this, index);
|
||||
auto blobSize = mBlobs->sizes[flatIndex];
|
||||
uint32_t const flatIndex = flatten(this, index);
|
||||
auto const blobSize = mBlobs->sizes[flatIndex];
|
||||
if (blobSize == 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -361,8 +376,8 @@ bool Ktx1Bundle::setBlob(KtxBlobIndex index, uint8_t const* data, uint32_t size)
|
||||
index.cubeFace >= mNumCubeFaces) {
|
||||
return false;
|
||||
}
|
||||
uint32_t flatIndex = flatten(this, index);
|
||||
uint32_t blobSize = mBlobs->sizes[flatIndex];
|
||||
uint32_t const flatIndex = flatten(this, index);
|
||||
uint32_t const blobSize = mBlobs->sizes[flatIndex];
|
||||
if (blobSize != size) {
|
||||
mBlobs->resize(flatIndex, size);
|
||||
}
|
||||
@@ -375,7 +390,7 @@ bool Ktx1Bundle::allocateBlob(KtxBlobIndex index, uint32_t size) {
|
||||
index.cubeFace >= mNumCubeFaces) {
|
||||
return false;
|
||||
}
|
||||
uint32_t flatIndex = flatten(this, index);
|
||||
uint32_t const flatIndex = flatten(this, index);
|
||||
mBlobs->resize(flatIndex, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
# include <arpa/inet.h>
|
||||
#endif
|
||||
|
||||
#include <utils/Log.h>
|
||||
#include <math/vec3.h>
|
||||
#include <math/vec4.h>
|
||||
|
||||
@@ -373,6 +374,12 @@ LinearImage PSDDecoder::decode() {
|
||||
uint32_t width = ntohl(h.width);
|
||||
uint32_t height = ntohl(h.height);
|
||||
|
||||
// Limit dimensions to 30,000 as per PSD specification to prevent integer overflows
|
||||
// during allocation in LinearImage.
|
||||
if (width == 0 || height == 0 || width > 30000 || height > 30000) {
|
||||
throw std::runtime_error("PSD dimensions exceed maximum allowed size (30,000)");
|
||||
}
|
||||
|
||||
uint32_t length;
|
||||
|
||||
// color mode data section
|
||||
@@ -394,6 +401,8 @@ LinearImage PSDDecoder::decode() {
|
||||
throw std::runtime_error("compressed images are not supported");
|
||||
}
|
||||
|
||||
// The multiplication width * height * 3 cannot overflow uint32_t because
|
||||
// width and height are at most 30,000. (30000 * 30000 * 3 = 2.7e9 < 4.29e9)
|
||||
LinearImage image(width, height, 3);
|
||||
|
||||
if (depth == 32) {
|
||||
@@ -403,6 +412,9 @@ LinearImage PSDDecoder::decode() {
|
||||
filament::math::float3& pixel =
|
||||
*reinterpret_cast< filament::math::float3*>(image.getPixelRef(x, y));
|
||||
pixel[i] = read32(mStream);
|
||||
if (!mStream.good()) {
|
||||
throw std::runtime_error("Truncated PSD file");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,6 +425,9 @@ LinearImage PSDDecoder::decode() {
|
||||
filament::math::float3& pixel =
|
||||
*reinterpret_cast< filament::math::float3*>(image.getPixelRef(x, y));
|
||||
pixel[i] = read16(mStream);
|
||||
if (!mStream.good()) {
|
||||
throw std::runtime_error("Truncated PSD file");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,12 +134,19 @@ static Result transcodeImageLevel(ktx2_transcoder& transcoder,
|
||||
|
||||
if (formatInfo.isCompressed) {
|
||||
const uint32_t qwordsPerBlock = basisu::get_qwords_per_block(destFormat);
|
||||
const size_t byteCount = sizeof(uint64_t) * qwordsPerBlock * levelInfo.m_total_blocks;
|
||||
const size_t byteCount = (size_t)sizeof(uint64_t) * (size_t)qwordsPerBlock * (size_t)levelInfo.m_total_blocks;
|
||||
|
||||
if (qwordsPerBlock != 0 && levelInfo.m_total_blocks != 0 &&
|
||||
byteCount / qwordsPerBlock / sizeof(uint64_t) != levelInfo.m_total_blocks) {
|
||||
return Result::COMPRESSED_TRANSCODE_FAILURE;
|
||||
}
|
||||
|
||||
uint64_t* const blocks = (uint64_t*) malloc(byteCount);
|
||||
if (!transcoder.transcode_image_level(levelIndex, layerIndex, faceIndex, blocks,
|
||||
levelInfo.m_total_blocks, formatInfo.basisFormat, decodeFlags,
|
||||
outputRowPitch, outputRowCount, channel0,
|
||||
channel1, &transcoderState)) {
|
||||
free(blocks);
|
||||
return Result::COMPRESSED_TRANSCODE_FAILURE;
|
||||
}
|
||||
*pbd = new Texture::PixelBufferDescriptor(blocks,
|
||||
@@ -149,11 +156,23 @@ static Result transcodeImageLevel(ktx2_transcoder& transcoder,
|
||||
|
||||
const uint32_t rowCount = levelInfo.m_orig_height;
|
||||
const uint32_t bytesPerPix = basis_get_bytes_per_block_or_pixel(formatInfo.basisFormat);
|
||||
const size_t byteCount = bytesPerPix * levelInfo.m_orig_width * rowCount;
|
||||
|
||||
if (bytesPerPix == 0) {
|
||||
return Result::UNCOMPRESSED_TRANSCODE_FAILURE;
|
||||
}
|
||||
|
||||
const size_t byteCount = (size_t)bytesPerPix * (size_t)levelInfo.m_orig_width * (size_t)rowCount;
|
||||
|
||||
if (levelInfo.m_orig_width != 0 &&
|
||||
byteCount / bytesPerPix / levelInfo.m_orig_width != rowCount) {
|
||||
return Result::UNCOMPRESSED_TRANSCODE_FAILURE;
|
||||
}
|
||||
|
||||
uint64_t* const rows = (uint64_t*) malloc(byteCount);
|
||||
if (!transcoder.transcode_image_level(levelIndex, layerIndex, faceIndex, rows,
|
||||
byteCount / bytesPerPix, formatInfo.basisFormat, decodeFlags,
|
||||
outputRowPitch, outputRowCount, channel0, channel1, &transcoderState)) {
|
||||
free(rows);
|
||||
return Result::UNCOMPRESSED_TRANSCODE_FAILURE;
|
||||
}
|
||||
*pbd = new Texture::PixelBufferDescriptor(rows, byteCount,
|
||||
@@ -259,6 +278,7 @@ Texture* Ktx2Reader::load(const void* data, size_t size, TransferFunction transf
|
||||
return nullptr;
|
||||
}
|
||||
texture->setImage(mEngine, levelIndex, std::move(*pbd));
|
||||
delete pbd;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
@@ -45,3 +45,12 @@ endif()
|
||||
# ==================================================================================================
|
||||
install(TARGETS ${TARGET} ARCHIVE DESTINATION lib/${DIST_DIR})
|
||||
install(DIRECTORY ${PUBLIC_HDR_DIR}/uberz DESTINATION include)
|
||||
|
||||
# ==================================================================================================
|
||||
# Tests
|
||||
# ==================================================================================================
|
||||
if (FILAMENT_BUILD_TESTING)
|
||||
add_executable(test_${TARGET} tests/test_ReadableArchive.cpp)
|
||||
target_link_libraries(test_${TARGET} PRIVATE ${TARGET} gtest)
|
||||
set_target_properties(test_${TARGET} PROPERTIES FOLDER Tests)
|
||||
endif()
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace filament::uberz {
|
||||
// ArchiveSpec is a parse-free binary format. The client simply casts a word-aligned content blob
|
||||
// into a ReadableArchive struct pointer, then calls the following function to convert all the
|
||||
// offset fields into pointers.
|
||||
void convertOffsetsToPointers(struct ReadableArchive* archive);
|
||||
bool convertOffsetsToPointers(struct ReadableArchive* archive, size_t archiveSize);
|
||||
|
||||
UTILS_WARNING_PUSH
|
||||
UTILS_WARNING_ENABLE_PADDED
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
#include <uberz/ReadableArchive.h>
|
||||
|
||||
#include <utils/debug.h>
|
||||
#include <utils/Logger.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
using namespace filament;
|
||||
using namespace utils;
|
||||
@@ -27,21 +30,110 @@ static_assert(sizeof(ReadableArchive) == 4 + 4 + 8 + 8);
|
||||
static_assert(sizeof(ArchiveSpec) == 1 + 1 + 2 + 4 + 8 + 8);
|
||||
static_assert(sizeof(ArchiveFlag) == 8 + 8);
|
||||
|
||||
void convertOffsetsToPointers(ReadableArchive* archive) {
|
||||
constexpr size_t wordSize = sizeof(uint64_t);
|
||||
assert_invariant(archive->specsOffset % wordSize == 0);
|
||||
uint64_t* basePointer = (uint64_t*) archive;
|
||||
archive->specs = (ArchiveSpec*) (basePointer + archive->specsOffset / wordSize);
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t READABLE_ARCHIVE_MAGIC = 'UBER';
|
||||
constexpr uint32_t READABLE_ARCHIVE_VERSION = 0;
|
||||
constexpr size_t WORD_SIZE = sizeof(uint64_t);
|
||||
|
||||
bool checkedMultiply(uint64_t count, size_t itemSize, size_t& result) {
|
||||
if (count > std::numeric_limits<size_t>::max() / itemSize) {
|
||||
return false;
|
||||
}
|
||||
result = size_t(count) * itemSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t* checkedOffset(ReadableArchive* archive, size_t archiveSize,
|
||||
uint64_t offset, size_t length) {
|
||||
if (offset > archiveSize || length > archiveSize - size_t(offset)) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<uint8_t*>(archive) + size_t(offset);
|
||||
}
|
||||
|
||||
const char* checkedCString(ReadableArchive* archive, size_t archiveSize,
|
||||
uint64_t offset) {
|
||||
if (offset >= archiveSize) {
|
||||
return nullptr;
|
||||
}
|
||||
const char* string = reinterpret_cast<const char*>(archive) + size_t(offset);
|
||||
if (memchr(string, 0, archiveSize - size_t(offset)) == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool convertOffsetsToPointers(ReadableArchive* archive, size_t archiveSize) {
|
||||
if (archiveSize < sizeof(ReadableArchive)) {
|
||||
LOG(ERROR) << "Uberz archive header is truncated";
|
||||
return false;
|
||||
}
|
||||
if (archive->magic != READABLE_ARCHIVE_MAGIC) {
|
||||
LOG(ERROR) << "Uberz archive has invalid magic";
|
||||
return false;
|
||||
}
|
||||
if (archive->version != READABLE_ARCHIVE_VERSION) {
|
||||
LOG(ERROR) << "Uberz archive has unsupported version";
|
||||
return false;
|
||||
}
|
||||
if (archive->specsOffset % WORD_SIZE != 0) {
|
||||
LOG(ERROR) << "Uberz specs offset is misaligned";
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t specsSize;
|
||||
if (!checkedMultiply(archive->specsCount, sizeof(ArchiveSpec), specsSize)) {
|
||||
LOG(ERROR) << "Uberz specs array size overflows";
|
||||
return false;
|
||||
}
|
||||
|
||||
archive->specs = reinterpret_cast<ArchiveSpec*>(checkedOffset(archive, archiveSize,
|
||||
archive->specsOffset, specsSize));
|
||||
if (!archive->specs) {
|
||||
LOG(ERROR) << "Uberz specs array exceeds buffer";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint64_t i = 0; i < archive->specsCount; ++i) {
|
||||
ArchiveSpec& spec = archive->specs[i];
|
||||
assert_invariant(spec.flagsOffset % wordSize == 0);
|
||||
spec.flags = (ArchiveFlag*) (basePointer + (spec.flagsOffset / wordSize));
|
||||
spec.package = ((uint8_t*) basePointer) + spec.packageOffset;
|
||||
if (spec.flagsOffset % WORD_SIZE != 0) {
|
||||
LOG(ERROR) << "Uberz flags offset is misaligned";
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t flagsSize;
|
||||
if (!checkedMultiply(spec.flagsCount, sizeof(ArchiveFlag), flagsSize)) {
|
||||
LOG(ERROR) << "Uberz flags array size overflows";
|
||||
return false;
|
||||
}
|
||||
|
||||
spec.flags = reinterpret_cast<ArchiveFlag*>(checkedOffset(archive, archiveSize,
|
||||
spec.flagsOffset, flagsSize));
|
||||
if (!spec.flags) {
|
||||
LOG(ERROR) << "Uberz flags array exceeds buffer";
|
||||
return false;
|
||||
}
|
||||
|
||||
spec.package = checkedOffset(archive, archiveSize, spec.packageOffset,
|
||||
spec.packageByteCount);
|
||||
if (!spec.package) {
|
||||
LOG(ERROR) << "Uberz package exceeds buffer";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint64_t j = 0; j < spec.flagsCount; ++j) {
|
||||
ArchiveFlag& flag = spec.flags[j];
|
||||
flag.name = ((const char*) basePointer) + flag.nameOffset;
|
||||
flag.name = checkedCString(archive, archiveSize, flag.nameOffset);
|
||||
if (!flag.name) {
|
||||
LOG(ERROR) << "Uberz flag name exceeds buffer or is not NUL-terminated";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace filament::uberz
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 <uberz/ReadableArchive.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
using filament::uberz::ArchiveFeature;
|
||||
using filament::uberz::ArchiveFlag;
|
||||
using filament::uberz::ArchiveSpec;
|
||||
using filament::uberz::ReadableArchive;
|
||||
using filament::uberz::convertOffsetsToPointers;
|
||||
|
||||
namespace {
|
||||
|
||||
TEST(ReadableArchiveTest, RejectsSpecsOffsetOutsideBuffer) {
|
||||
alignas(8) std::array<uint8_t, 64> storage {};
|
||||
auto* archive = reinterpret_cast<ReadableArchive*>(storage.data());
|
||||
archive->magic = 'UBER';
|
||||
archive->version = 0;
|
||||
archive->specsCount = 1;
|
||||
archive->specsOffset = storage.size();
|
||||
|
||||
EXPECT_FALSE(convertOffsetsToPointers(archive, storage.size()));
|
||||
}
|
||||
|
||||
TEST(ReadableArchiveTest, RejectsFlagNamesOutsideBuffer) {
|
||||
alignas(8) std::array<uint8_t, 96> storage {};
|
||||
auto* archive = reinterpret_cast<ReadableArchive*>(storage.data());
|
||||
archive->magic = 'UBER';
|
||||
archive->version = 0;
|
||||
|
||||
archive->specsCount = 1;
|
||||
archive->specsOffset = sizeof(ReadableArchive);
|
||||
|
||||
auto* spec = reinterpret_cast<ArchiveSpec*>(storage.data() + archive->specsOffset);
|
||||
*spec = {};
|
||||
spec->flagsCount = 1;
|
||||
spec->flagsOffset = archive->specsOffset + sizeof(ArchiveSpec);
|
||||
spec->packageOffset = storage.size() - 1;
|
||||
|
||||
auto* flag = reinterpret_cast<ArchiveFlag*>(storage.data() + spec->flagsOffset);
|
||||
flag->nameOffset = storage.size();
|
||||
flag->value = ArchiveFeature::OPTIONAL;
|
||||
|
||||
EXPECT_FALSE(convertOffsetsToPointers(archive, storage.size()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -78,7 +78,7 @@ public:
|
||||
|
||||
// Return whether the given Entity has been destroyed (false) or not (true).
|
||||
// Thread safe.
|
||||
bool isAlive(Entity e) const noexcept {
|
||||
bool isAlive(Entity const e) const noexcept {
|
||||
assert(getIndex(e) < RAW_INDEX_COUNT);
|
||||
return (!e.isNull()) && (getGeneration(e) == mGens[getIndex(e)]);
|
||||
}
|
||||
@@ -94,7 +94,7 @@ public:
|
||||
/* no user serviceable parts below */
|
||||
|
||||
// current generation of the given index. Use for debugging and testing.
|
||||
uint8_t getGenerationForIndex(size_t index) const noexcept {
|
||||
uint8_t getGenerationForIndex(size_t const index) const noexcept {
|
||||
return mGens[index];
|
||||
}
|
||||
|
||||
@@ -107,6 +107,11 @@ public:
|
||||
void dumpActiveEntities(utils::io::ostream& out) const;
|
||||
#endif
|
||||
|
||||
// use carefully, several entities can have the same index.
|
||||
static Entity::Type getIndex(Entity const e) noexcept {
|
||||
return e.getId() & INDEX_MASK;
|
||||
}
|
||||
|
||||
private:
|
||||
friend class EntityManagerImpl;
|
||||
EntityManager();
|
||||
@@ -114,22 +119,20 @@ private:
|
||||
|
||||
// GENERATION_SHIFT determines how many simultaneous Entities are available, the
|
||||
// minimum memory requirement is 2^GENERATION_SHIFT bytes.
|
||||
static constexpr const int GENERATION_SHIFT = 17;
|
||||
static constexpr const size_t RAW_INDEX_COUNT = (1 << GENERATION_SHIFT);
|
||||
static constexpr const Entity::Type INDEX_MASK = (1 << GENERATION_SHIFT) - 1u;
|
||||
static constexpr int GENERATION_SHIFT = 17;
|
||||
static constexpr size_t RAW_INDEX_COUNT = (1 << GENERATION_SHIFT);
|
||||
static constexpr Entity::Type INDEX_MASK = (1 << GENERATION_SHIFT) - 1u;
|
||||
|
||||
static inline Entity::Type getGeneration(Entity e) noexcept {
|
||||
static Entity::Type getGeneration(Entity const e) noexcept {
|
||||
return e.getId() >> GENERATION_SHIFT;
|
||||
}
|
||||
static inline Entity::Type getIndex(Entity e) noexcept {
|
||||
return e.getId() & INDEX_MASK;
|
||||
}
|
||||
static inline Entity::Type makeIdentity(Entity::Type g, Entity::Type i) noexcept {
|
||||
|
||||
static Entity::Type makeIdentity(Entity::Type const g, Entity::Type const i) noexcept {
|
||||
return (g << GENERATION_SHIFT) | (i & INDEX_MASK);
|
||||
}
|
||||
|
||||
// stores the generation of each index.
|
||||
uint8_t * const mGens;
|
||||
uint8_t* const mGens;
|
||||
};
|
||||
|
||||
} // namespace utils
|
||||
|
||||
@@ -146,6 +146,14 @@ struct AgxToneMapperSettings {
|
||||
bool operator==(const AgxToneMapperSettings& rhs) const;
|
||||
};
|
||||
|
||||
enum class CustomLut : uint8_t {
|
||||
NONE = 0,
|
||||
NEGATIVE = 1,
|
||||
GRAYSCALE = 2,
|
||||
SEPIA = 3,
|
||||
TEAL_AND_ORANGE = 4,
|
||||
};
|
||||
|
||||
struct ColorGradingSettings {
|
||||
// fields are ordered to avoid padding
|
||||
bool enabled = true;
|
||||
@@ -154,7 +162,7 @@ struct ColorGradingSettings {
|
||||
bool gamutMapping = false;
|
||||
filament::ColorGrading::QualityLevel quality = filament::ColorGrading::QualityLevel::MEDIUM;
|
||||
ToneMapping toneMapping = ToneMapping::ACES_LEGACY;
|
||||
bool padding0{};
|
||||
CustomLut customLut = CustomLut::NONE;
|
||||
AgxToneMapperSettings agxToneMapper;
|
||||
color::ColorSpace colorspace = Rec709-sRGB-D65;
|
||||
GenericToneMapperSettings genericToneMapper;
|
||||
|
||||
@@ -98,6 +98,18 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ToneMapp
|
||||
return i + 1;
|
||||
}
|
||||
|
||||
static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, CustomLut* out) {
|
||||
if (0 == compare(tokens[i], jsonChunk, "NONE")) { *out = CustomLut::NONE; }
|
||||
else if (0 == compare(tokens[i], jsonChunk, "NEGATIVE")) { *out = CustomLut::NEGATIVE; }
|
||||
else if (0 == compare(tokens[i], jsonChunk, "GRAYSCALE")) { *out = CustomLut::GRAYSCALE; }
|
||||
else if (0 == compare(tokens[i], jsonChunk, "SEPIA")) { *out = CustomLut::SEPIA; }
|
||||
else if (0 == compare(tokens[i], jsonChunk, "TEAL_AND_ORANGE")) { *out = CustomLut::TEAL_AND_ORANGE; }
|
||||
else {
|
||||
slog.w << "Invalid CustomLut: '" << STR(tokens[i], jsonChunk) << "'" << io::endl;
|
||||
}
|
||||
return i + 1;
|
||||
}
|
||||
|
||||
static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, color::ColorSpace* out) {
|
||||
using namespace filament::color;
|
||||
if (0 == compare(tokens[i], jsonChunk, "Rec709-Linear-D65")) { *out = Rec709-Linear-D65; }
|
||||
@@ -207,6 +219,8 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ColorGra
|
||||
i = parse(tokens, i + 1, jsonChunk, &out->quality);
|
||||
} else if (compare(tok, jsonChunk, "toneMapping") == 0) {
|
||||
i = parse(tokens, i + 1, jsonChunk, &out->toneMapping);
|
||||
} else if (compare(tok, jsonChunk, "customLut") == 0) {
|
||||
i = parse(tokens, i + 1, jsonChunk, &out->customLut);
|
||||
} else if (compare(tok, jsonChunk, "genericToneMapper") == 0) {
|
||||
i = parse(tokens, i + 1, jsonChunk, &out->genericToneMapper);
|
||||
} else if (compare(tok, jsonChunk, "agxToneMapper") == 0) {
|
||||
@@ -984,10 +998,57 @@ constexpr ToneMapper* createToneMapper(const ColorGradingSettings& settings) noe
|
||||
}
|
||||
}
|
||||
|
||||
static utils::FixedCapacityVector<math::float3> generateCustomLut(CustomLut type, uint8_t dim) {
|
||||
using namespace filament::math;
|
||||
size_t count = size_t(dim) * dim * dim;
|
||||
auto lut = utils::FixedCapacityVector<float3>::with_capacity(count);
|
||||
|
||||
for (size_t b = 0; b < dim; ++b) {
|
||||
for (size_t g = 0; g < dim; ++g) {
|
||||
for (size_t r = 0; r < dim; ++r) {
|
||||
float3 v = float3{r, g, b} * (1.0f / (dim - 1));
|
||||
switch (type) {
|
||||
case CustomLut::NONE:
|
||||
break;
|
||||
case CustomLut::NEGATIVE:
|
||||
v = 1.0f - v;
|
||||
break;
|
||||
case CustomLut::GRAYSCALE:
|
||||
{
|
||||
float luma = dot(v, float3{0.2126f, 0.7152f, 0.0722f});
|
||||
v = float3{luma};
|
||||
}
|
||||
break;
|
||||
case CustomLut::SEPIA:
|
||||
{
|
||||
float luma = dot(v, float3{0.299f, 0.587f, 0.114f});
|
||||
v = float3{
|
||||
clamp(luma * 1.2f, 0.0f, 1.0f),
|
||||
clamp(luma * 1.0f, 0.0f, 1.0f),
|
||||
clamp(luma * 0.8f, 0.0f, 1.0f)
|
||||
};
|
||||
}
|
||||
break;
|
||||
case CustomLut::TEAL_AND_ORANGE:
|
||||
{
|
||||
float luma = dot(v, float3{0.2126f, 0.7152f, 0.0722f});
|
||||
float3 teal{0.0f, 0.5f, 0.5f};
|
||||
float3 orange{1.0f, 0.5f, 0.0f};
|
||||
v = teal * (1.0f - luma) + orange * luma;
|
||||
}
|
||||
break;
|
||||
}
|
||||
lut.push_back(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
return lut;
|
||||
}
|
||||
|
||||
ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* engine) {
|
||||
ToneMapper* toneMapper = createToneMapper(settings);
|
||||
ColorGrading *colorGrading = ColorGrading::Builder()
|
||||
.quality(settings.quality)
|
||||
ColorGrading::Builder builder;
|
||||
builder.quality(settings.quality)
|
||||
.exposure(settings.exposure)
|
||||
.nightAdaptation(settings.nightAdaptation)
|
||||
.whiteBalance(settings.temperature, settings.tint)
|
||||
@@ -1006,8 +1067,14 @@ ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* e
|
||||
.toneMapper(toneMapper)
|
||||
.luminanceScaling(settings.luminanceScaling)
|
||||
.gamutMapping(settings.gamutMapping)
|
||||
.outputColorSpace(settings.colorspace)
|
||||
.build(*engine);
|
||||
.outputColorSpace(settings.colorspace);
|
||||
|
||||
if (settings.customLut != CustomLut::NONE) {
|
||||
uint8_t dim = 16;
|
||||
builder.customLut(generateCustomLut(settings.customLut, dim), dim);
|
||||
}
|
||||
|
||||
ColorGrading *colorGrading = builder.build(*engine);
|
||||
delete toneMapper;
|
||||
return colorGrading;
|
||||
}
|
||||
@@ -1080,12 +1147,24 @@ static std::ostream& operator<<(std::ostream& out, const AgxToneMapperSettings&
|
||||
<< "}";
|
||||
}
|
||||
|
||||
static std::ostream& operator<<(std::ostream& out, CustomLut in) {
|
||||
switch (in) {
|
||||
case CustomLut::NONE: return out << "\"NONE\"";
|
||||
case CustomLut::NEGATIVE: return out << "\"NEGATIVE\"";
|
||||
case CustomLut::GRAYSCALE: return out << "\"GRAYSCALE\"";
|
||||
case CustomLut::SEPIA: return out << "\"SEPIA\"";
|
||||
case CustomLut::TEAL_AND_ORANGE: return out << "\"TEAL_AND_ORANGE\"";
|
||||
}
|
||||
return out << "\"INVALID\"";
|
||||
}
|
||||
|
||||
static std::ostream& operator<<(std::ostream& out, const ColorGradingSettings& in) {
|
||||
return out << "{\n"
|
||||
<< "\"enabled\": " << to_string(in.enabled) << ",\n"
|
||||
<< "\"colorspace\": " << to_string(in.colorspace) << ",\n"
|
||||
<< "\"quality\": " << (in.quality) << ",\n"
|
||||
<< "\"toneMapping\": " << (in.toneMapping) << ",\n"
|
||||
<< "\"customLut\": " << (in.customLut) << ",\n"
|
||||
<< "\"genericToneMapper\": " << (in.genericToneMapper) << ",\n"
|
||||
<< "\"agxToneMapper\": " << (in.agxToneMapper) << ",\n"
|
||||
<< "\"luminanceScaling\": " << to_string(in.luminanceScaling) << ",\n"
|
||||
@@ -1346,6 +1425,7 @@ bool ColorGradingSettings::operator==(const ColorGradingSettings& rhs) const {
|
||||
colorspace == rhs.colorspace &&
|
||||
quality == rhs.quality &&
|
||||
toneMapping == rhs.toneMapping &&
|
||||
customLut == rhs.customLut &&
|
||||
genericToneMapper == rhs.genericToneMapper &&
|
||||
agxToneMapper == rhs.agxToneMapper &&
|
||||
luminanceScaling == rhs.luminanceScaling &&
|
||||
|
||||
@@ -226,6 +226,11 @@ static void colorGradingUI(Settings& settings, float* rangePlot, float* curvePlo
|
||||
ImGui::Combo("Quality##colorGradingQuality", &quality, "Low\0Medium\0High\0Ultra\0\0");
|
||||
colorGrading.quality = (decltype(colorGrading.quality)) quality;
|
||||
|
||||
int customLut = (int) colorGrading.customLut;
|
||||
if (ImGui::Combo("Custom LUT##colorGradingCustomLut", &customLut, "None\0Negative\0Grayscale\0Sepia\0Teal and Orange\0\0")) {
|
||||
colorGrading.customLut = (CustomLut) customLut;
|
||||
}
|
||||
|
||||
int colorspace = (colorGrading.colorspace == Rec709-Linear-D65) ? 0 : 1;
|
||||
ImGui::Combo("Output color space", &colorspace, "Rec709-Linear-D65\0Rec709-sRGB-D65\0\0");
|
||||
colorGrading.colorspace = (colorspace == 0) ? Rec709-Linear-D65 : Rec709-sRGB-D65;
|
||||
|
||||
@@ -57,29 +57,47 @@ Depending on the Android version and device storage policy, the app's file locat
|
||||
|
||||
---
|
||||
|
||||
## Python Terminal UI (TUI) Dashboard
|
||||
## Render Validation Results Viewer
|
||||
|
||||
To make managing tests and results easier, a Python-based Textual TUI (`validation_app.py`) is provided in the `validation_tui` directory. It automatically polls the connected device using ADB, acts as a GUI for the intent commands above, and handles downloading/uploading `.zip` bundles to circumvent Android's scoped storage limits.
|
||||
The project includes a static web viewer to visualize and compare test results across different devices. The viewer supports high-resolution image comparison with zoom/pan controls and dynamic diffing.
|
||||
|
||||
### Setup
|
||||
1. Ensure you have Python 3 and `adb` installed and in your PATH.
|
||||
2. Navigate to the TUI directory: `cd test/render-validation`
|
||||
3. Create a virtual environment: `python3 -m venv venv`
|
||||
4. Activate it: `source venv/bin/activate` (Mac/Linux) or `venv\Scripts\activate` (Windows)
|
||||
5. Install requirements: `pip install -r requirements.txt` (Installs the `textual` framework)
|
||||
### Setup & Requirements
|
||||
The results processor requires `numpy` and `Pillow`. These are not included in the main `requirements.txt` to keep the TUI dependencies minimal.
|
||||
|
||||
1. Install processing dependencies:
|
||||
```bash
|
||||
pip install numpy Pillow
|
||||
```
|
||||
|
||||
### 1. Process Result Bundles
|
||||
The `process_results.py` script takes a directory of `.zip` result files (exported from the Android app) and generates a static web folder.
|
||||
|
||||
### Usage
|
||||
Start the dashboard by running:
|
||||
```bash
|
||||
python validation_app.py
|
||||
# Usage: python process_results.py <input_zip_dir> <output_web_dir>
|
||||
python process_results.py ./my_results ./web_output
|
||||
```
|
||||
|
||||
### TUI Features
|
||||
- **Auto-Polling Mechanism**: Syncs the file lists with your device every 2 seconds.
|
||||
- **Generate Test/Result Buttons**: One-click execution of the `am start` intents.
|
||||
- **Upload Local Test Bundle**: Automatically pushes a local `.zip` file from your PC to the correct directory on the Android device.
|
||||
- **Per-File Actions**:
|
||||
- `▶` (Load): Restarts the app with `--es zip_path <filename>` to set it as the active test on device.
|
||||
- `↓` (Download): Pulls the `.zip` to your PC's current working directory.
|
||||
- `✎` (Rename): Quickly renames the file directly on the Android file system.
|
||||
- `✗` (Delete): Quickly removes the file from the Android device to free up storage.
|
||||
This script:
|
||||
- Extracts images and metadata from the result zips.
|
||||
- Generates thumbnails for efficient browser performance.
|
||||
- Packages the exact tolerance configurations for the web viewer.
|
||||
|
||||
### 2. View Results
|
||||
Because the viewer uses ES modules and fetches data, it must be served via a web server.
|
||||
|
||||
```bash
|
||||
cd ./web_output
|
||||
python3 -m http.server 1234
|
||||
```
|
||||
|
||||
Navigate to `http://localhost:1234` in your desktop browser.
|
||||
|
||||
### Web Viewer Features
|
||||
- **Tabular Overview**: Compare results across multiple devices and test runs in a single grid.
|
||||
- **High-Res Viewer**: Click any thumbnail to open a full-size modal.
|
||||
- **Zoom & Pan**: Use the mouse wheel to zoom and left-click-drag to pan around the render.
|
||||
- **Comparison Modes**: Cycle between "Rendered", "Golden", and "Diff" views.
|
||||
- **Dynamic JS Diffing**: The `imagediff` algorithm (including `shiftRadius`, `blurRadius`, and complex tolerance trees) is implemented in JavaScript and computed on-the-fly.
|
||||
- **Fail Highlighting**: Toggle "Highlight Failing Pixels" in the Diff view to see exactly which pixels exceeded the tolerance threshold in pure red.
|
||||
- **Contrast Control**: Use the contrast slider to amplify subtle rendering differences.
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
def get_test_tolerance(config, full_test_name):
|
||||
# test name is e.g. "basic.opengl.DamagedHelmet"
|
||||
parts = full_test_name.split('.')
|
||||
if not parts: return None
|
||||
test_id = parts[0]
|
||||
|
||||
for test in config.get("tests", []):
|
||||
if test.get("name") == test_id:
|
||||
return test.get("tolerance")
|
||||
return None
|
||||
|
||||
def process_zip(zip_path, output_dir, device_name):
|
||||
"""Extract and process a single zip file from the results folder."""
|
||||
extract_dir = os.path.join(output_dir, "tmp", device_name)
|
||||
os.makedirs(extract_dir, exist_ok=True)
|
||||
|
||||
with zipfile.ZipFile(zip_path, 'r') as z:
|
||||
z.extractall(extract_dir)
|
||||
|
||||
results_json_path = os.path.join(extract_dir, "results.json")
|
||||
if not os.path.exists(results_json_path):
|
||||
return {}, []
|
||||
|
||||
with open(results_json_path, 'r') as f:
|
||||
try:
|
||||
device_results = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
return {}, []
|
||||
|
||||
metadata = device_results['metadata']
|
||||
|
||||
bundle_zip_path = os.path.join(extract_dir, "bundle.zip")
|
||||
bundle_dir = os.path.join(extract_dir, "bundle")
|
||||
|
||||
if os.path.exists(bundle_zip_path):
|
||||
os.makedirs(bundle_dir, exist_ok=True)
|
||||
with zipfile.ZipFile(bundle_zip_path, 'r') as z:
|
||||
z.extractall(bundle_dir)
|
||||
|
||||
config_path = os.path.join(bundle_dir, "default_test", "config.json")
|
||||
config = {}
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
run_results = []
|
||||
|
||||
for test_result in device_results.get("results", []):
|
||||
test_name = test_result.get("test_name")
|
||||
passed = test_result.get("passed", False)
|
||||
|
||||
rendered_path = os.path.join(extract_dir, f"{test_name}.png")
|
||||
golden_path = os.path.join(bundle_dir, "default_test", "goldens", f"{test_name}.png")
|
||||
|
||||
if not os.path.exists(rendered_path):
|
||||
continue
|
||||
|
||||
rendered_img = Image.open(rendered_path).convert("RGBA")
|
||||
|
||||
# Output paths for web
|
||||
rel_test_dir = f"{device_name}/{test_name}"
|
||||
out_test_dir = os.path.join(output_dir, "assets", rel_test_dir)
|
||||
os.makedirs(out_test_dir, exist_ok=True)
|
||||
|
||||
out_golden = os.path.join(out_test_dir, "golden.png")
|
||||
out_rendered = os.path.join(out_test_dir, "rendered.png")
|
||||
out_thumb = os.path.join(out_test_dir, "thumb.png")
|
||||
|
||||
rendered_img.save(out_rendered)
|
||||
|
||||
# Generate thumbnail
|
||||
thumb_size = (128, 128)
|
||||
thumb_img = rendered_img.copy()
|
||||
thumb_img.thumbnail(thumb_size)
|
||||
thumb_img.save(out_thumb)
|
||||
|
||||
has_golden = False
|
||||
if os.path.exists(golden_path):
|
||||
has_golden = True
|
||||
shutil.copy2(golden_path, out_golden)
|
||||
else:
|
||||
Image.new("RGBA", rendered_img.size, (0,0,0,0)).save(out_golden)
|
||||
|
||||
tolerance_config = get_test_tolerance(config, test_name)
|
||||
|
||||
run_results.append({
|
||||
"testName": test_name,
|
||||
"passed": passed,
|
||||
"golden": f"assets/{rel_test_dir}/golden.png",
|
||||
"rendered": f"assets/{rel_test_dir}/rendered.png",
|
||||
"thumb": f"assets/{rel_test_dir}/thumb.png",
|
||||
"hasGolden": has_golden,
|
||||
"config": tolerance_config
|
||||
})
|
||||
|
||||
return [metadata, run_results]
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Process render validation zip results.")
|
||||
parser.add_argument("input_dir", help="Directory containing .zip result files")
|
||||
parser.add_argument("output_dir", help="Directory to output the static web viewer")
|
||||
args = parser.parse_args()
|
||||
|
||||
input_dir = Path(args.input_dir)
|
||||
output_dir = Path(args.output_dir)
|
||||
|
||||
if not input_dir.exists():
|
||||
print(f"Error: Input directory {input_dir} does not exist.")
|
||||
return
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
os.makedirs(output_dir / "assets", exist_ok=True)
|
||||
|
||||
all_results = []
|
||||
|
||||
for zip_file in input_dir.glob("*.zip"):
|
||||
device_name = zip_file.stem
|
||||
print(f"Processing {zip_file.name} for device {device_name}...")
|
||||
|
||||
metadata, device_results = process_zip(zip_file, output_dir, device_name)
|
||||
if device_results:
|
||||
all_results.append({
|
||||
"metadata": metadata,
|
||||
"device": device_name,
|
||||
"runs": device_results
|
||||
})
|
||||
|
||||
# Cleanup tmp
|
||||
tmp_dir = output_dir / "tmp"
|
||||
if tmp_dir.exists():
|
||||
shutil.rmtree(tmp_dir)
|
||||
|
||||
# Write data.json
|
||||
with open(output_dir / "data.json", "w") as f:
|
||||
json.dump(all_results, f, indent=2)
|
||||
|
||||
# Copy web viewer files
|
||||
viewer_src = Path(__file__).parent / "result-viewer"
|
||||
if viewer_src.exists():
|
||||
for item in viewer_src.iterdir():
|
||||
if item.is_file():
|
||||
shutil.copy2(item, output_dir)
|
||||
elif item.is_dir():
|
||||
dest_dir = output_dir / item.name
|
||||
if dest_dir.exists():
|
||||
shutil.rmtree(dest_dir)
|
||||
shutil.copytree(item, dest_dir)
|
||||
|
||||
print("Done. To view results, run a static server in the output directory:")
|
||||
print(f"cd {output_dir} && python3 -m http.server 1234")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1 +1,3 @@
|
||||
textual>=0.52.0
|
||||
Pillow
|
||||
numpy
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import './components/result-table.js';
|
||||
import './components/image-viewer.js';
|
||||
|
||||
async function init() {
|
||||
const container = document.getElementById('container');
|
||||
|
||||
try {
|
||||
const response = await fetch('./data.json');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
container.innerHTML = `
|
||||
<result-table></result-table>
|
||||
<image-viewer></image-viewer>
|
||||
`;
|
||||
|
||||
const resultTable = container.querySelector('result-table');
|
||||
const imageViewer = container.querySelector('image-viewer');
|
||||
|
||||
resultTable.results = data;
|
||||
|
||||
// Listen for view events from the table
|
||||
container.addEventListener('view-result', (e) => {
|
||||
imageViewer.open(e.detail.device, e.detail.run);
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
container.innerHTML = `<div style="color: red; padding: 20px;">Error loading results: ${e.message}.<br>Make sure you are running a local server.</div>`;
|
||||
console.error("Failed to load data:", e);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
@@ -0,0 +1,699 @@
|
||||
import { html, css, LitElement } from 'lit';
|
||||
import './tiff-viewer.js';
|
||||
|
||||
const RES_EQUAL = "equal";
|
||||
const RES_MISMATCHED_DIMENSIONS = "mismatched dimensions";
|
||||
const RES_DIFFERENT_PIXELS = "different pixels";
|
||||
const RES_NOT_READY = "not ready";
|
||||
|
||||
export class ImageViewer extends LitElement {
|
||||
static properties = {
|
||||
runData: { type: Object },
|
||||
deviceName: { type: String },
|
||||
isOpen: { type: Boolean },
|
||||
|
||||
diffResult: { type: Object },
|
||||
magnifierEnabled: { type: Boolean },
|
||||
highlightFailing: { type: Boolean },
|
||||
currentDiffImageData: { type: Object },
|
||||
|
||||
viewMode: { type: String },
|
||||
toggleState: { type: String },
|
||||
autoAlternate: { type: Boolean }
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: black;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.header {
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #ddd;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
z-index: 10;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
.title {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn:hover { background: #eee; }
|
||||
.close-btn {
|
||||
background: #e74c3c;
|
||||
border-color: #c0392b;
|
||||
color: white;
|
||||
}
|
||||
.close-btn:hover { background: #c0392b; color: white; }
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.viewer-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.control-panel {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.viewer-wrap {
|
||||
flex: 1;
|
||||
max-width: 33%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.viewer-wrap.full-width {
|
||||
max-width: 512px;
|
||||
}
|
||||
.viewer-label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
tiff-viewer {
|
||||
width: 100%;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.isOpen = false;
|
||||
this.magnifierEnabled = true;
|
||||
this.highlightFailing = false;
|
||||
this.originalDiffImageData = null;
|
||||
this.currentDiffImageData = null;
|
||||
this.diffResult = null;
|
||||
|
||||
this.leftImageLoaded = false;
|
||||
this.rightImageLoaded = false;
|
||||
|
||||
this.viewMode = 'side-by-side';
|
||||
this.toggleState = 'rendered';
|
||||
this.autoAlternate = false;
|
||||
this._alternateInterval = null;
|
||||
|
||||
this.addEventListener(
|
||||
'image-loaded',
|
||||
(ev) => {
|
||||
if (!this.runData) return;
|
||||
if (ev.detail.url === this.runData.golden) {
|
||||
this.leftImageLoaded = true;
|
||||
}
|
||||
if (ev.detail.url === this.runData.rendered) {
|
||||
this.rightImageLoaded = true;
|
||||
}
|
||||
if (this.leftImageLoaded && this.rightImageLoaded) {
|
||||
this._triggerDiff();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
open(deviceName, runData) {
|
||||
this.deviceName = deviceName;
|
||||
this.runData = runData;
|
||||
this.isOpen = true;
|
||||
|
||||
this.diffResult = null;
|
||||
this.currentDiffImageData = null;
|
||||
this.originalDiffImageData = null;
|
||||
this.leftImageLoaded = false;
|
||||
this.rightImageLoaded = false;
|
||||
this.highlightFailing = false;
|
||||
|
||||
this.viewMode = 'side-by-side';
|
||||
this.toggleState = 'rendered';
|
||||
this.autoAlternate = false;
|
||||
if (this._alternateInterval) {
|
||||
clearInterval(this._alternateInterval);
|
||||
this._alternateInterval = null;
|
||||
}
|
||||
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
document.body.style.overflow = '';
|
||||
if (this._alternateInterval) {
|
||||
clearInterval(this._alternateInterval);
|
||||
this._alternateInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
_computeDiff() {
|
||||
const tiffViewerLeft = this.shadowRoot.querySelector('#viewer-left');
|
||||
const tiffViewerRight = this.shadowRoot.querySelector('#viewer-right');
|
||||
|
||||
if (!tiffViewerLeft || !tiffViewerRight) {
|
||||
return { "result": RES_NOT_READY };
|
||||
}
|
||||
|
||||
const canvasLeft = tiffViewerLeft.shadowRoot.querySelector('canvas');
|
||||
const canvasRight = tiffViewerRight.shadowRoot.querySelector('canvas');
|
||||
|
||||
if (!canvasLeft || !canvasRight) {
|
||||
return { "result": RES_NOT_READY };
|
||||
}
|
||||
|
||||
const imgLeft = tiffViewerLeft.imgdata;
|
||||
const imgRight = tiffViewerRight.imgdata;
|
||||
|
||||
if (!imgLeft || !imgRight) {
|
||||
return { "result": RES_NOT_READY };
|
||||
}
|
||||
|
||||
if (imgLeft.width !== imgRight.width || imgLeft.height !== imgRight.height) {
|
||||
console.error("Images have different dimensions");
|
||||
return {
|
||||
"result": RES_MISMATCHED_DIMENSIONS,
|
||||
"explanation": "Images have different dimensions " +
|
||||
"left=(" + imgLeft.width + ", " + imgLeft.height + ") " +
|
||||
"right=(" + imgRight.width + ", " + imgRight.height + ")",
|
||||
};
|
||||
}
|
||||
|
||||
const width = imgLeft.width;
|
||||
const height = imgLeft.height;
|
||||
const goldenData = imgLeft.data;
|
||||
const renderedData = imgRight.data;
|
||||
const imgDiff = new Uint8ClampedArray(width * height * 4);
|
||||
const maxDiff = [0, 0, 0, 0];
|
||||
|
||||
const config = this.runData?.config;
|
||||
const highlight = this.highlightFailing;
|
||||
|
||||
const blurCacheG = new Map();
|
||||
const blurCacheR = new Map();
|
||||
if (highlight && config) {
|
||||
blurCacheG.set(0, goldenData);
|
||||
blurCacheR.set(0, renderedData);
|
||||
|
||||
const applyBlur = (data, radius) => {
|
||||
if (radius === 0) return data;
|
||||
const out = new Uint8ClampedArray(width * height * 4);
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
let r=0, g=0, b=0, a=0, count=0;
|
||||
for (let dy = -radius; dy <= radius; dy++) {
|
||||
for (let dx = -radius; dx <= radius; dx++) {
|
||||
let nx = Math.max(0, Math.min(width - 1, x + dx));
|
||||
let ny = Math.max(0, Math.min(height - 1, y + dy));
|
||||
let idx = (ny * width + nx) * 4;
|
||||
r += data[idx];
|
||||
g += data[idx+1];
|
||||
b += data[idx+2];
|
||||
a += data[idx+3];
|
||||
count++;
|
||||
}
|
||||
}
|
||||
let oidx = (y * width + x) * 4;
|
||||
out[oidx] = r/count;
|
||||
out[oidx+1] = g/count;
|
||||
out[oidx+2] = b/count;
|
||||
out[oidx+3] = 255;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const getUniqueBlurs = (cfg, out = new Set()) => {
|
||||
if (!cfg) return out;
|
||||
if (cfg.mode === "LEAF" || !cfg.mode) {
|
||||
out.add(cfg.blurRadius || 0);
|
||||
} else if (cfg.children) {
|
||||
for (let c of cfg.children) getUniqueBlurs(c, out);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const blurs = getUniqueBlurs(config);
|
||||
for (let rad of blurs) {
|
||||
if (rad > 0 && !blurCacheG.has(rad)) {
|
||||
blurCacheG.set(rad, applyBlur(goldenData, rad));
|
||||
blurCacheR.set(rad, applyBlur(renderedData, rad));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkPixel = (x, y, cfg) => {
|
||||
const mode = cfg.mode || "LEAF";
|
||||
if (mode === "LEAF") {
|
||||
const shiftRad = cfg.shiftRadius || 0;
|
||||
const blurRad = cfg.blurRadius || 0;
|
||||
const maxAllowedDiff = (cfg.maxAbsDiff || 0.0) * 255.0;
|
||||
const channelMask = cfg.channelMask !== undefined ? cfg.channelMask : 15;
|
||||
|
||||
const gData = blurCacheG.get(blurRad);
|
||||
const rData = blurCacheR.get(blurRad);
|
||||
|
||||
const activeCh = [];
|
||||
if (channelMask & 1) activeCh.push(0);
|
||||
if (channelMask & 2) activeCh.push(1);
|
||||
if (channelMask & 4) activeCh.push(2);
|
||||
if (channelMask & 8) activeCh.push(3);
|
||||
|
||||
let rIdx = (y * width + x) * 4;
|
||||
let rc = [rData[rIdx], rData[rIdx+1], rData[rIdx+2], rData[rIdx+3]];
|
||||
|
||||
for (let dy = -shiftRad; dy <= shiftRad; dy++) {
|
||||
for (let dx = -shiftRad; dx <= shiftRad; dx++) {
|
||||
let nx = Math.max(0, Math.min(width - 1, x + dx));
|
||||
let ny = Math.max(0, Math.min(height - 1, y + dy));
|
||||
let idx = (ny * width + nx) * 4;
|
||||
|
||||
let match = true;
|
||||
for (let c of activeCh) {
|
||||
if (Math.abs(gData[idx+c] - rc[c]) > maxAllowedDiff) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (mode === "AND") {
|
||||
for (let child of (cfg.children || [])) {
|
||||
if (!checkPixel(x, y, child)) return false;
|
||||
}
|
||||
return true;
|
||||
} else if (mode === "OR") {
|
||||
for (let child of (cfg.children || [])) {
|
||||
if (checkPixel(x, y, child)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
const idx = i * 4;
|
||||
const x = i % width;
|
||||
const y = Math.floor(i / width);
|
||||
|
||||
let rDiff = Math.abs(goldenData[idx] - renderedData[idx]);
|
||||
let gDiff = Math.abs(goldenData[idx+1] - renderedData[idx+1]);
|
||||
let bDiff = Math.abs(goldenData[idx+2] - renderedData[idx+2]);
|
||||
let aDiff = Math.abs(goldenData[idx+3] - renderedData[idx+3]);
|
||||
|
||||
maxDiff[0] = Math.max(maxDiff[0], rDiff);
|
||||
maxDiff[1] = Math.max(maxDiff[1], gDiff);
|
||||
maxDiff[2] = Math.max(maxDiff[2], bDiff);
|
||||
maxDiff[3] = Math.max(maxDiff[3], aDiff);
|
||||
|
||||
if (highlight) {
|
||||
let pass = true;
|
||||
if (config) {
|
||||
pass = checkPixel(x, y, config);
|
||||
} else {
|
||||
pass = (rDiff === 0 && gDiff === 0 && bDiff === 0 && aDiff === 0);
|
||||
}
|
||||
|
||||
if (!pass) {
|
||||
imgDiff[idx] = 255;
|
||||
imgDiff[idx+1] = 0;
|
||||
imgDiff[idx+2] = 0;
|
||||
imgDiff[idx+3] = 255;
|
||||
} else {
|
||||
imgDiff[idx] = rDiff;
|
||||
imgDiff[idx+1] = gDiff;
|
||||
imgDiff[idx+2] = bDiff;
|
||||
imgDiff[idx+3] = 255;
|
||||
}
|
||||
} else {
|
||||
imgDiff[idx] = rDiff;
|
||||
imgDiff[idx+1] = gDiff;
|
||||
imgDiff[idx+2] = bDiff;
|
||||
imgDiff[idx+3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxDiff[0] == 0 && maxDiff[1] == 0 && maxDiff[2] == 0 && maxDiff[3] == 0) {
|
||||
return {
|
||||
"result": RES_EQUAL,
|
||||
"explanation": "Equal",
|
||||
"dim": {"width": width, "height": height },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"result": RES_DIFFERENT_PIXELS,
|
||||
"explanation": "Images are different",
|
||||
"dim": {"width": width, "height": height },
|
||||
"maxDiff": maxDiff,
|
||||
"diffImg": imgDiff,
|
||||
};
|
||||
}
|
||||
|
||||
_triggerDiff() {
|
||||
const diff = this._computeDiff();
|
||||
if (diff.result == RES_DIFFERENT_PIXELS) {
|
||||
this.diffResult = diff;
|
||||
this.originalDiffImageData = null;
|
||||
const multDiv = this.shadowRoot.querySelector('#diffMultiplier');
|
||||
if (multDiv) {
|
||||
this._updateDiffCanvas(this.diffResult, multDiv.value);
|
||||
} else {
|
||||
this._updateDiffCanvas(this.diffResult, 1);
|
||||
}
|
||||
} else {
|
||||
this.diffResult = null;
|
||||
this.currentDiffImageData = null;
|
||||
}
|
||||
}
|
||||
|
||||
_updateDiffCanvas(diffResult, mult) {
|
||||
const diffImgCopy = diffResult.diffImg.slice();
|
||||
for (let i = 0; i < diffImgCopy.length; i += 4) {
|
||||
for (let j = 0; j < 3; j++) {
|
||||
diffImgCopy[i + j] = Math.min(255, mult * diffImgCopy[i + j]);
|
||||
}
|
||||
diffImgCopy[i + 3] = 255;
|
||||
}
|
||||
|
||||
const imgData = new ImageData(diffImgCopy, diffResult.dim.width, diffResult.dim.height);
|
||||
|
||||
if (!this.originalDiffImageData) {
|
||||
this.originalDiffImageData = new ImageData(diffResult.diffImg.slice(), diffResult.dim.width, diffResult.dim.height);
|
||||
}
|
||||
this.currentDiffImageData = imgData;
|
||||
}
|
||||
|
||||
_onGlobalMouseLeave(event) {
|
||||
for (const name of ['left', 'right', 'diff']) {
|
||||
const viewer = this.shadowRoot.querySelector('#viewer-' + name);
|
||||
if (viewer) {
|
||||
const mag = viewer.shadowRoot?.getElementById('magnifier');
|
||||
if (mag) {
|
||||
mag.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onGlobalMouseMove(event) {
|
||||
if (!this.magnifierEnabled) return;
|
||||
for (const name of ['left', 'right', 'diff']) {
|
||||
const viewer = this.shadowRoot.querySelector('#viewer-' + name);
|
||||
if (!viewer) continue;
|
||||
|
||||
const canvas = viewer.shadowRoot?.querySelector('canvas');
|
||||
if (!canvas) continue;
|
||||
|
||||
const imageData = viewer.imgdata;
|
||||
if (!imageData) continue;
|
||||
|
||||
const magnifier = viewer.shadowRoot?.getElementById('magnifier');
|
||||
|
||||
if (!this._isMouseOverAnyView(event)) {
|
||||
magnifier.hide();
|
||||
continue;
|
||||
}
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const { imageX, imageY, mouseX, mouseY } = this._calculateEquivalentPosition(event, rect, imageData);
|
||||
|
||||
this._lastImageX = imageX;
|
||||
this._lastImageY = imageY;
|
||||
|
||||
const origData = name == 'diff' ? this.originalDiffImageData : null;
|
||||
viewer.updateMagnifier(imageX, imageY, origData);
|
||||
}
|
||||
}
|
||||
|
||||
_isMouseOverElement(event, rect) {
|
||||
return event.clientX >= rect.left &&
|
||||
event.clientX <= rect.right &&
|
||||
event.clientY >= rect.top &&
|
||||
event.clientY <= rect.bottom;
|
||||
}
|
||||
|
||||
_isMouseOverAnyView(event) {
|
||||
const checkView = (id) => {
|
||||
const viewer = this.shadowRoot.querySelector(id);
|
||||
if (viewer) {
|
||||
const canvas = viewer.shadowRoot?.querySelector('canvas');
|
||||
if (canvas && this._isMouseOverElement(event, canvas.getBoundingClientRect())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return checkView('#viewer-left') || checkView('#viewer-right') || checkView('#viewer-diff');
|
||||
}
|
||||
|
||||
_calculateEquivalentPosition(event, targetRect, targetImageData) {
|
||||
let sourceRect = null;
|
||||
let sourceImageData = null;
|
||||
|
||||
const getSource = (id) => {
|
||||
const viewer = this.shadowRoot.querySelector(id);
|
||||
if (viewer) {
|
||||
const canvas = viewer.shadowRoot?.querySelector('canvas');
|
||||
if (canvas && this._isMouseOverElement(event, canvas.getBoundingClientRect())) {
|
||||
return { rect: canvas.getBoundingClientRect(), img: viewer.imgdata };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
let src = getSource('#viewer-left') || getSource('#viewer-right') || (this.diffResult ? getSource('#viewer-diff') : null);
|
||||
|
||||
if (src) {
|
||||
sourceRect = src.rect;
|
||||
sourceImageData = src.img;
|
||||
}
|
||||
|
||||
if (!sourceRect || !sourceImageData) {
|
||||
sourceRect = targetRect;
|
||||
sourceImageData = targetImageData;
|
||||
}
|
||||
|
||||
const sourceMouseX = event.clientX - sourceRect.left;
|
||||
const sourceMouseY = event.clientY - sourceRect.top;
|
||||
const sourceScaleX = sourceImageData.width / sourceRect.width;
|
||||
const sourceScaleY = sourceImageData.height / sourceRect.height;
|
||||
const sourceImageX = Math.floor(sourceMouseX * sourceScaleX);
|
||||
const sourceImageY = Math.floor(sourceMouseY * sourceScaleY);
|
||||
|
||||
const targetScaleX = targetImageData.width / targetRect.width;
|
||||
const targetScaleY = targetImageData.height / targetRect.height;
|
||||
const targetMouseX = sourceImageX / targetScaleX;
|
||||
const targetMouseY = sourceImageY / targetScaleY;
|
||||
|
||||
return {
|
||||
imageX: sourceImageX,
|
||||
imageY: sourceImageY,
|
||||
mouseX: targetMouseX,
|
||||
mouseY: targetMouseY
|
||||
};
|
||||
}
|
||||
|
||||
_updateMagnifiersForToggle() {
|
||||
if (!this.magnifierEnabled || this._lastImageX === undefined) return;
|
||||
const activeViewerId = this.toggleState === 'rendered' ? '#viewer-right' : '#viewer-left';
|
||||
const viewer = this.shadowRoot.querySelector(activeViewerId);
|
||||
if (!viewer || !viewer.imgdata) return;
|
||||
const origData = null; // No origData needed for golden/rendered
|
||||
viewer.updateMagnifier(this._lastImageX, this._lastImageY, origData);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isOpen || !this.runData) return html``;
|
||||
|
||||
const showDiff = !!this.diffResult;
|
||||
|
||||
const onMultiplierChange = (ev) => {
|
||||
const multiplierValue = this.shadowRoot.querySelector('#multiplierValue');
|
||||
multiplierValue.textContent = ev.target.value;
|
||||
this._updateDiffCanvas(this.diffResult, ev.target.value);
|
||||
};
|
||||
|
||||
const onMagnifierToggle = (ev) => {
|
||||
this.magnifierEnabled = ev.target.checked;
|
||||
};
|
||||
|
||||
const onHighlightToggle = (ev) => {
|
||||
this.highlightFailing = ev.target.checked;
|
||||
this._triggerDiff();
|
||||
};
|
||||
|
||||
const toggleViewMode = async () => {
|
||||
this.viewMode = this.viewMode === 'side-by-side' ? 'toggle' : 'side-by-side';
|
||||
if (this.viewMode === 'toggle') {
|
||||
this.autoAlternate = true;
|
||||
if (!this._alternateInterval) {
|
||||
this._alternateInterval = setInterval(async () => {
|
||||
this.toggleState = this.toggleState === 'rendered' ? 'golden' : 'rendered';
|
||||
await this.updateComplete;
|
||||
this._updateMagnifiersForToggle();
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
this.autoAlternate = false;
|
||||
if (this._alternateInterval) {
|
||||
clearInterval(this._alternateInterval);
|
||||
this._alternateInterval = null;
|
||||
}
|
||||
}
|
||||
await this.updateComplete;
|
||||
if (this.viewMode === 'toggle') {
|
||||
this._updateMagnifiersForToggle();
|
||||
}
|
||||
};
|
||||
|
||||
const switchToggleState = async () => {
|
||||
if (this.autoAlternate) return;
|
||||
this.toggleState = this.toggleState === 'rendered' ? 'golden' : 'rendered';
|
||||
await this.updateComplete;
|
||||
this._updateMagnifiersForToggle();
|
||||
};
|
||||
|
||||
const onAutoAlternateChange = (ev) => {
|
||||
this.autoAlternate = ev.target.checked;
|
||||
if (this.autoAlternate) {
|
||||
this._alternateInterval = setInterval(async () => {
|
||||
this.toggleState = this.toggleState === 'rendered' ? 'golden' : 'rendered';
|
||||
await this.updateComplete;
|
||||
this._updateMagnifiersForToggle();
|
||||
}, 2000);
|
||||
} else {
|
||||
if (this._alternateInterval) {
|
||||
clearInterval(this._alternateInterval);
|
||||
this._alternateInterval = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="modal-overlay">
|
||||
<div class="header">
|
||||
<div class="title">${this.deviceName} - ${this.runData.testName}</div>
|
||||
<div class="controls">
|
||||
<button class="btn" @click="${toggleViewMode}">
|
||||
${this.viewMode === 'side-by-side' ? 'Switch to Toggle Mode' : 'Switch to Side-by-Side'}
|
||||
</button>
|
||||
<button class="btn close-btn" @click="${this.close}">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
<div class="viewer-container ${this.viewMode === 'toggle' ? 'toggle-mode' : ''}" @mousemove="${this._onGlobalMouseMove}" @mouseleave="${this._onGlobalMouseLeave}">
|
||||
<div class="viewer-wrap ${this.viewMode === 'toggle' ? 'full-width' : ''}"
|
||||
style="${this.viewMode === 'toggle' && this.toggleState !== 'golden' ? 'display: none;' : ''}">
|
||||
<div class="viewer-label">Golden</div>
|
||||
<tiff-viewer id="viewer-left" class="viewer"
|
||||
fileurl="${this.runData.golden}"
|
||||
?magnifier-enabled="${this.magnifierEnabled}"
|
||||
disable-mouse-handlers></tiff-viewer>
|
||||
</div>
|
||||
|
||||
<div class="viewer-wrap" style="${(!showDiff || this.viewMode === 'toggle') ? 'display: none;' : ''}">
|
||||
<div class="viewer-label">Diff</div>
|
||||
<tiff-viewer id="viewer-diff" class="viewer"
|
||||
name="diff"
|
||||
.srcdata="${this.currentDiffImageData}"
|
||||
?magnifier-enabled="${this.magnifierEnabled}"
|
||||
disable-mouse-handlers></tiff-viewer>
|
||||
</div>
|
||||
|
||||
<div class="viewer-wrap ${this.viewMode === 'toggle' ? 'full-width' : ''}"
|
||||
style="${this.viewMode === 'toggle' && this.toggleState !== 'rendered' ? 'display: none;' : ''}">
|
||||
<div class="viewer-label">Rendered</div>
|
||||
<tiff-viewer id="viewer-right" class="viewer"
|
||||
fileurl="${this.runData.rendered}"
|
||||
?magnifier-enabled="${this.magnifierEnabled}"
|
||||
disable-mouse-handlers></tiff-viewer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-panel" style="${(this.viewMode === 'toggle' || !showDiff) ? 'display: none;' : ''}">
|
||||
<div>
|
||||
<strong>Difference Multiplier:</strong> <span id="multiplierValue">1</span>x
|
||||
</div>
|
||||
<input type="range" min="1" max="100" value="1" id="diffMultiplier" @input=${onMultiplierChange} style="width: 200px;">
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-top: 10px;">
|
||||
<label style="cursor: pointer; display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" .checked="${this.magnifierEnabled}" @change=${onMagnifierToggle}>
|
||||
Enable Magnifier
|
||||
</label>
|
||||
<label style="cursor: pointer; display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" .checked="${this.highlightFailing}" @change=${onHighlightToggle}>
|
||||
Highlight Failing Pixels (Red)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-panel" style="${this.viewMode === 'side-by-side' ? 'display: none;' : ''}">
|
||||
<div style="display: flex; gap: 20px; align-items: center;">
|
||||
<button class="btn" @click="${switchToggleState}" ?disabled="${this.autoAlternate}">
|
||||
Switch to ${this.toggleState === 'rendered' ? 'Golden' : 'Rendered'}
|
||||
</button>
|
||||
<label style="cursor: pointer; display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" .checked="${this.autoAlternate}" @change=${onAutoAlternateChange}>
|
||||
Auto-Alternate (2s)
|
||||
</label>
|
||||
<label style="cursor: pointer; display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" .checked="${this.magnifierEnabled}" @change=${onMagnifierToggle}>
|
||||
Enable Magnifier
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('image-viewer', ImageViewer);
|
||||
@@ -0,0 +1,333 @@
|
||||
import { html, css, LitElement } from 'lit';
|
||||
import './test-filter.js';
|
||||
|
||||
export class ResultTable extends LitElement {
|
||||
static properties = {
|
||||
results: { type: Array },
|
||||
sortBy: { type: String },
|
||||
backendFilter: { type: String },
|
||||
nameFilters: { type: Array },
|
||||
showOnlyFailures: { type: Boolean },
|
||||
gpuFilters: { type: Array }
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.sortBy = 'name-gpu-os';
|
||||
this.backendFilter = 'all';
|
||||
this.nameFilters = [];
|
||||
this.showOnlyFailures = false;
|
||||
this.gpuFilters = [];
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
overflow: auto;
|
||||
max-height: 100vh;
|
||||
max-width: 100vw;
|
||||
font-size: 11px;
|
||||
}
|
||||
.filter-bar {
|
||||
padding: 5px 10px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
margin-left: 200px;
|
||||
margin-right: -200px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
th, td {
|
||||
padding: 4px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
white-space: nowrap;
|
||||
position: sticky;
|
||||
top: 35px; /* Offset by filter-bar height approx */
|
||||
z-index: 10;
|
||||
}
|
||||
.device-col {
|
||||
color: #34495e;
|
||||
border-right: 1px solid #eee;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background-color: #fff;
|
||||
z-index: 5;
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
max-width: 200px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
th.device-col {
|
||||
z-index: 15;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.result-cell {
|
||||
text-align: center;
|
||||
}
|
||||
.result-container {
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.result-container:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.pass {
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.fail {
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.thumb {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.test-name {
|
||||
font-size: 0.85em;
|
||||
color: #555;
|
||||
}
|
||||
.test-name-container {
|
||||
vertical-align: top;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
if (!this.results || this.results.length === 0) {
|
||||
return html`<p style="padding: 20px;">No results found.</p>`;
|
||||
}
|
||||
|
||||
// Extract all unique test names to build columns
|
||||
const allTests = new Set();
|
||||
this.results.forEach(device => {
|
||||
device.runs.forEach(run => allTests.add(run.testName));
|
||||
});
|
||||
|
||||
let testNames = Array.from(allTests).sort(
|
||||
(a, b) => {
|
||||
let [atest, abackend, amodel] = a.split('.');
|
||||
let [btest, bbackend, bmodel] = b.split('.');
|
||||
if (abackend === bbackend) {
|
||||
return atest < btest ? -1 : 1;
|
||||
}
|
||||
return abackend < bbackend ? -1 : 1;
|
||||
}
|
||||
);
|
||||
|
||||
// Apply backend filter
|
||||
if (this.backendFilter !== 'all') {
|
||||
testNames = testNames.filter(name => {
|
||||
const [, backend] = name.split('.');
|
||||
return backend === this.backendFilter;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply name filters (OR logic)
|
||||
if (this.nameFilters && this.nameFilters.length > 0) {
|
||||
testNames = testNames.filter(name => {
|
||||
return this.nameFilters.some(filterStr => name.includes(filterStr));
|
||||
});
|
||||
}
|
||||
|
||||
// Apply failure filter
|
||||
if (this.showOnlyFailures) {
|
||||
testNames = testNames.filter(name => {
|
||||
return this.results.some(device => {
|
||||
const run = device.runs.find(r => r.testName === name);
|
||||
return run && !run.passed;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const nameToDiv = (name) => {
|
||||
const [testName, backend, modelName] = name.split('.');
|
||||
const sname = testName.split('_').map((n,i) => {
|
||||
const style = (i > 0) ? 'font-size:8px' : '';
|
||||
return html`<span style="${style}">${n}</span>`
|
||||
});
|
||||
const border = "border:1px solid black;border-radius:5px;padding:3px;";
|
||||
const buttonStyle = border + "font-size:9px;";
|
||||
const buttonColor = backend == 'opengl' ?
|
||||
"background-color:#e0e3c0" :
|
||||
"background-color:#b3b0f0";
|
||||
return html`
|
||||
<th class="test-name-container">
|
||||
<div style="margin-bottom:5px;display:inline-flex;flex-direction:column;${border}">
|
||||
${sname}
|
||||
</div>
|
||||
<span style="display:flex">
|
||||
<div style="${buttonStyle};${buttonColor}">
|
||||
${backend}
|
||||
</div>
|
||||
</span>
|
||||
</th>
|
||||
`;
|
||||
};
|
||||
const testRow = testNames.map(nameToDiv);
|
||||
|
||||
const getShortGPUName = (device) => {
|
||||
const gpuStr = device.metadata?.gpu_driver_info?.opengl || '';
|
||||
if (gpuStr.includes('PowerVR')) return 'PowerVR';
|
||||
if (gpuStr.includes('Mali')) return 'Mali';
|
||||
if (gpuStr.includes('Adreno')) return 'Adreno';
|
||||
if (gpuStr.includes('Xclipse')) return 'Xclipse';
|
||||
return gpuStr;
|
||||
};
|
||||
|
||||
let filteredDevices = this.results;
|
||||
if (this.gpuFilters && this.gpuFilters.length > 0) {
|
||||
filteredDevices = filteredDevices.filter(device => {
|
||||
const gpuStr = device.metadata?.gpu_driver_info?.opengl || '';
|
||||
return this.gpuFilters.some(filterStr => gpuStr.toLowerCase().includes(filterStr.toLowerCase()));
|
||||
});
|
||||
}
|
||||
|
||||
const sortedResults = [...filteredDevices].sort((a, b) => {
|
||||
const nameA = a.metadata.device_name || '';
|
||||
const nameB = b.metadata.device_name || '';
|
||||
|
||||
const gpuA = getShortGPUName(a);
|
||||
const gpuB = getShortGPUName(b);
|
||||
|
||||
const osA = parseInt(a.metadata.android_version, 10) || 0;
|
||||
const osB = parseInt(b.metadata.android_version, 10) || 0;
|
||||
|
||||
const cmpName = nameA.localeCompare(nameB);
|
||||
const cmpGpu = gpuA.localeCompare(gpuB);
|
||||
const cmpOs = osA - osB;
|
||||
|
||||
if (this.sortBy === 'name-gpu-os') {
|
||||
if (cmpName !== 0) return cmpName;
|
||||
if (cmpGpu !== 0) return cmpGpu;
|
||||
return cmpOs;
|
||||
} else if (this.sortBy === 'gpu-os-name') {
|
||||
if (cmpGpu !== 0) return cmpGpu;
|
||||
if (cmpOs !== 0) return cmpOs;
|
||||
return cmpName;
|
||||
} else if (this.sortBy === 'os-gpu-name') {
|
||||
if (cmpOs !== 0) return cmpOs;
|
||||
if (cmpGpu !== 0) return cmpGpu;
|
||||
return cmpName;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return html`
|
||||
<div style="position:absolute;width:200px;height:50px;background:white;z-index:10;"> </div>
|
||||
<div class="filter-bar">
|
||||
<span style="font-weight: 600;">Backend Filter:</span>
|
||||
<select style="font-size: 11px; padding: 4px;" @change="${(e) => this.backendFilter = e.target.value}">
|
||||
<option value="all" ?selected="${this.backendFilter === 'all'}">opengl / vulkan</option>
|
||||
<option value="opengl" ?selected="${this.backendFilter === 'opengl'}">opengl</option>
|
||||
<option value="vulkan" ?selected="${this.backendFilter === 'vulkan'}">vulkan</option>
|
||||
</select>
|
||||
<div style="width: 1px; height: 20px; background: #ccc; margin: 0 10px;"></div>
|
||||
<test-filter @filters-changed="${(e) => this.nameFilters = e.detail.filters}"></test-filter>
|
||||
<div style="width: 1px; height: 20px; background: #ccc; margin: 0 10px;"></div>
|
||||
<label style="display: flex; align-items: center; gap: 4px; font-weight: 600; font-size: 11px; cursor: pointer;">
|
||||
<input type="checkbox" .checked="${this.showOnlyFailures}" @change="${(e) => this.showOnlyFailures = e.target.checked}">
|
||||
Only columns with failed tests
|
||||
</label>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="device-col" style="font-size:15px; vertical-align: top; padding-top: 8px;">
|
||||
<div>Device</div>
|
||||
<select style="font-size: 11px; margin-top: 6px; width: 100%; padding: 2px;" @change="${(e) => this.sortBy = e.target.value}">
|
||||
<option value="name-gpu-os" ?selected="${this.sortBy === 'name-gpu-os'}">Device -> GPU -> Android Ver</option>
|
||||
<option value="gpu-os-name" ?selected="${this.sortBy === 'gpu-os-name'}">GPU -> Android Ver -> Device</option>
|
||||
<option value="os-gpu-name" ?selected="${this.sortBy === 'os-gpu-name'}">Android Ver -> GPU -> Device</option>
|
||||
</select>
|
||||
<div style="margin-top: 8px;">
|
||||
<test-filter label="GPU:" placeholder="Filter GPU..." @filters-changed="${(e) => this.gpuFilters = e.detail.filters}"></test-filter>
|
||||
</div>
|
||||
</th>
|
||||
${testRow}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${
|
||||
sortedResults.map(device => {
|
||||
const processBuildNumber = (rawBuild) => {
|
||||
if (rawBuild.indexOf(' ') >=0 && rawBuild.indexOf('dev-keys') >= 0) {
|
||||
return rawBuild.split(' ')[2].split('.')[0];
|
||||
}
|
||||
return rawBuild.split('.')[0];
|
||||
};
|
||||
const androidVersion = device.metadata.android_version + " " +
|
||||
processBuildNumber(device.metadata.android_build_number);
|
||||
const glGPU = device.metadata.gpu_driver_info.opengl.split(' | ');
|
||||
const driverInfo = device.metadata.gpu_driver_info.vulkan.split(' | ')[2];
|
||||
const truncatedStyle = "display:block;width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
|
||||
const marginLow = "margin-bottom:4px;"
|
||||
const androidVerStyle = marginLow + (this.sortBy.startsWith('os') ? "color:#f09090;" : '');
|
||||
const gpuStyle = marginLow + (this.sortBy.startsWith('gpu') ? "color:#f09090;" : '');
|
||||
const hardwareStyle = marginLow + "color:#bbb;";
|
||||
const deviceNameStyle = marginLow + "font-size:15px;font-weight:bold;";
|
||||
return html`
|
||||
<tr>
|
||||
<td class="device-col">
|
||||
<div style="${deviceNameStyle}">${device.metadata.device_name} </div>
|
||||
<div style="${hardwareStyle}">${device.metadata.device_hardware} </div>
|
||||
<div style="${androidVerStyle}">Android ${androidVersion}</div>
|
||||
<div style="${gpuStyle}">${glGPU[0]} ${glGPU[1]}</div>
|
||||
<div style="${truncatedStyle}">${driverInfo}</div>
|
||||
</td>
|
||||
${
|
||||
testNames.map(testName => {
|
||||
const run = device.runs.find(r => r.testName === testName);
|
||||
if (!run) return html`<td>-</td>`;
|
||||
|
||||
return html`
|
||||
<td class="result-cell">
|
||||
<div class="result-container ${run.passed ? 'pass' : 'fail'}"
|
||||
@click="${() => this._handleThumbnailClick(device.device, run)}">
|
||||
<img class="thumb" src="${run.thumb}" loading="lazy" alt="Thumbnail" />
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</tr>
|
||||
`})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
_handleThumbnailClick(device, run) {
|
||||
this.dispatchEvent(new CustomEvent('view-result', {
|
||||
detail: { device, run },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('result-table', ResultTable);
|
||||
@@ -0,0 +1,100 @@
|
||||
import { html, css, LitElement } from 'lit';
|
||||
|
||||
export class TestFilter extends LitElement {
|
||||
static properties = {
|
||||
filters: { type: Array },
|
||||
label: { type: String },
|
||||
placeholder: { type: String }
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.filters = [];
|
||||
this.label = 'Test Name:';
|
||||
this.placeholder = 'Filter tests...';
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
input {
|
||||
padding: 4px;
|
||||
font-size: 11px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
max-width: 120px;
|
||||
}
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: #e0e0e0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.remove-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.remove-btn:hover {
|
||||
color: #000;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this.label ? html`<span style="font-weight: 600; font-size: 11px;">${this.label}</span>` : ''}
|
||||
<input
|
||||
type="text"
|
||||
.placeholder="${this.placeholder}"
|
||||
@keydown="${this._handleKeydown}"
|
||||
>
|
||||
${this.filters.map((filter, index) => html`
|
||||
<div class="tag">
|
||||
${filter}
|
||||
<button class="remove-btn" @click="${() => this._removeFilter(index)}">×</button>
|
||||
</div>
|
||||
`)}
|
||||
`;
|
||||
}
|
||||
|
||||
_handleKeydown(e) {
|
||||
if (e.key === 'Enter') {
|
||||
const val = e.target.value.trim();
|
||||
if (val && !this.filters.includes(val)) {
|
||||
this.filters = [...this.filters, val];
|
||||
this._dispatchChange();
|
||||
}
|
||||
e.target.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
_removeFilter(index) {
|
||||
this.filters = this.filters.filter((_, i) => i !== index);
|
||||
this._dispatchChange();
|
||||
}
|
||||
|
||||
_dispatchChange() {
|
||||
this.dispatchEvent(new CustomEvent('filters-changed', {
|
||||
detail: { filters: this.filters },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('test-filter', TestFilter);
|
||||
@@ -0,0 +1,330 @@
|
||||
// Copyright (C) 2025 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { LitElement, html, css } from "https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js";
|
||||
|
||||
class ImageMagnifier extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([visible]) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.magnifier {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: 2px solid #000;
|
||||
border-radius: 75px;
|
||||
background: white;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.magnifier-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 73px;
|
||||
}
|
||||
|
||||
.pixel-info {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 15px;
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 9px;
|
||||
white-space: pre-line;
|
||||
max-width: 300px;
|
||||
}
|
||||
`;
|
||||
|
||||
static properties = {
|
||||
visible: {type: Boolean, reflect: true},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="magnifier">
|
||||
<canvas class="magnifier-canvas" id="magnifierCanvas"></canvas>
|
||||
<div class="pixel-info" id="pixelInfo"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
updateMagnifier(imageData, parentRect, imageX, imageY, originalImageData = null) {
|
||||
const zoomFactor = 8;
|
||||
if (!imageData) return;
|
||||
|
||||
if (imageX < 0 || imageX >= imageData.width || imageY < 0 || imageY >= imageData.height) {
|
||||
this.visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const pixelIndex = (imageY * imageData.width + imageX) * 4;
|
||||
const r = imageData.data[pixelIndex];
|
||||
const g = imageData.data[pixelIndex + 1];
|
||||
const b = imageData.data[pixelIndex + 2];
|
||||
const a = imageData.data[pixelIndex + 3];
|
||||
|
||||
let pixelInfoText = `(${r}, ${g}, ${b}, ${a})\n@ (${imageX}, ${imageY})`;
|
||||
|
||||
// If original image data is provided, show the unmultiplied values too
|
||||
if (originalImageData) {
|
||||
const origR = originalImageData.data[pixelIndex];
|
||||
const origG = originalImageData.data[pixelIndex + 1];
|
||||
const origB = originalImageData.data[pixelIndex + 2];
|
||||
const origA = originalImageData.data[pixelIndex + 3];
|
||||
pixelInfoText = `Orig: (${origR}, ${origG}, ${origB}, ${origA})\nMult: (${r}, ${g}, ${b}, ${a})\n@ (${imageX}, ${imageY})`;
|
||||
}
|
||||
|
||||
|
||||
const magnifierSize = 150;
|
||||
const sourceSize = magnifierSize / zoomFactor;
|
||||
const halfSource = sourceSize / 2;
|
||||
|
||||
const sourceX = Math.max(0, Math.min(imageData.width - sourceSize, imageX - halfSource));
|
||||
const sourceY = Math.max(0, Math.min(imageData.height - sourceSize, imageY - halfSource));
|
||||
|
||||
const magnifierCanvas = this.shadowRoot.getElementById('magnifierCanvas');
|
||||
const pixelInfo = this.shadowRoot.getElementById('pixelInfo');
|
||||
|
||||
magnifierCanvas.width = magnifierSize;
|
||||
magnifierCanvas.height = magnifierSize;
|
||||
const magnifierCtx = magnifierCanvas.getContext('2d');
|
||||
|
||||
magnifierCtx.imageSmoothingEnabled = false;
|
||||
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = imageData.width;
|
||||
tempCanvas.height = imageData.height;
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
tempCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
magnifierCtx.drawImage(
|
||||
tempCanvas,
|
||||
sourceX, sourceY, sourceSize, sourceSize,
|
||||
0, 0, magnifierSize, magnifierSize
|
||||
);
|
||||
|
||||
const centerX = magnifierSize / 2;
|
||||
const centerY = magnifierSize / 2;
|
||||
const lineWidth = 1;
|
||||
const boxWidth = zoomFactor + lineWidth;
|
||||
magnifierCtx.strokeStyle = 'red';
|
||||
magnifierCtx.lineWidth = lineWidth;
|
||||
magnifierCtx.beginPath();
|
||||
magnifierCtx.moveTo(centerX, centerY);
|
||||
magnifierCtx.lineTo(centerX + boxWidth, centerY);
|
||||
magnifierCtx.lineTo(centerX + boxWidth, centerY + boxWidth);
|
||||
magnifierCtx.lineTo(centerX, centerY + boxWidth);
|
||||
magnifierCtx.lineTo(centerX, centerY);
|
||||
magnifierCtx.stroke();
|
||||
|
||||
// Position relative to the TiffViewer container
|
||||
this.style.left = Math.round(-centerX +
|
||||
(imageX / imageData.width) * parentRect.width -
|
||||
boxWidth) + 'px';
|
||||
this.style.top = Math.round(-centerY +
|
||||
(imageY / imageData.height) * parentRect.height -
|
||||
boxWidth) + 'px';
|
||||
pixelInfo.textContent = pixelInfoText;
|
||||
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('image-magnifier', ImageMagnifier);
|
||||
|
||||
// Generated by Gemini with some modifications
|
||||
export class TiffViewer extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
canvas {
|
||||
border: 1px solid #ccc;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
static properties = {
|
||||
fileurl: {type: String, attribute: 'fileurl'},
|
||||
failedToFetch: {type: Boolean },
|
||||
magnifierEnabled: {type: Boolean, attribute: 'magnifier-enabled'},
|
||||
disableMouseHandlers: {type: Boolean, attribute: 'disable-mouse-handlers'},
|
||||
srcdata: {type: Object, attribute: 'srcdata'},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.fileurl = null;
|
||||
this.failedToFetch = false;
|
||||
this.magnifierEnabled = false;
|
||||
this.disableMouseHandlers = false;
|
||||
this.imgdata = null;
|
||||
this.srcdata = null;
|
||||
this.canvasRect = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.failedToFetch) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<canvas id="tiffCanvas"
|
||||
@mousemove="${this._onMouseMove}"
|
||||
@mouseenter="${this._onMouseEnter}"
|
||||
@mouseleave="${this._onMouseLeave}">
|
||||
</canvas>
|
||||
<image-magnifier id="magnifier"></image-magnifier>
|
||||
`;
|
||||
}
|
||||
|
||||
updated(props) {
|
||||
if (props.has('fileurl') && this.fileurl) {
|
||||
this._updateImage(this.fileurl);
|
||||
return;
|
||||
}
|
||||
if (props.has('srcdata') && this.srcdata) {
|
||||
this._drawImage(this.srcdata);
|
||||
}
|
||||
}
|
||||
|
||||
_drawImage(imageData) {
|
||||
const canvas = this.shadowRoot.getElementById('tiffCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
this.imgdata = imageData;
|
||||
}
|
||||
|
||||
async _updateImage(fileurl) {
|
||||
this.failedToFetch = false;
|
||||
const img = new Image();
|
||||
img.crossOrigin = "Anonymous";
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = this.shadowRoot.getElementById('tiffCanvas');
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||
|
||||
// The default mode would set alpha to 1 so that RGB differences would be displayed as non-transparent
|
||||
for (let i = 3; i < imageData.data.length; i += 4) {
|
||||
imageData.data[i] = 255;
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
this.imgdata = imageData;
|
||||
|
||||
this.dispatchEvent(new CustomEvent('url-hit', {
|
||||
detail: { value: fileurl },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
|
||||
this.dispatchEvent(new CustomEvent('image-loaded', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
url: this.fileurl,
|
||||
img: imageData,
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
this.failedToFetch = true;
|
||||
this.dispatchEvent(new CustomEvent('url-miss', {
|
||||
detail: { value: fileurl },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
this._clearCanvas();
|
||||
};
|
||||
|
||||
img.src = fileurl;
|
||||
}
|
||||
|
||||
_clearCanvas() {
|
||||
const canvas = this.shadowRoot.getElementById('tiffCanvas');
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
_onMouseEnter(event) {
|
||||
if (this.disableMouseHandlers || !this.magnifierEnabled || !this.imgdata) return;
|
||||
this.canvasRect = event.target.getBoundingClientRect();
|
||||
}
|
||||
|
||||
_onMouseLeave(event) {
|
||||
if (this.disableMouseHandlers || !this.magnifierEnabled) return;
|
||||
const magnifier = this.shadowRoot.getElementById('magnifier');
|
||||
magnifier.hide();
|
||||
}
|
||||
|
||||
_onMouseMove(event) {
|
||||
if (this.disableMouseHandlers || !this.canvasRect) return;
|
||||
const rect = this.canvasRect;
|
||||
|
||||
const scaleX = this.imgdata.width / rect.width;
|
||||
const scaleY = this.imgdata.height / rect.height;
|
||||
|
||||
const mouseX = event.clientX - rect.left;
|
||||
const mouseY = event.clientY - rect.top;
|
||||
|
||||
const imageX = Math.floor(mouseX * scaleX);
|
||||
const imageY = Math.floor(mouseY * scaleY);
|
||||
|
||||
this.updateMagnifier(imageX, imageY);
|
||||
}
|
||||
|
||||
updateMagnifier(imageX, imageY, origData = null) {
|
||||
let rect = null;
|
||||
const canvas = this.shadowRoot.getElementById('tiffCanvas');
|
||||
if (canvas) {
|
||||
rect = canvas.getBoundingClientRect();
|
||||
}
|
||||
if (!this.magnifierEnabled || !this.imgdata || !rect) return;
|
||||
const magnifier = this.shadowRoot.getElementById('magnifier');
|
||||
magnifier.updateMagnifier(this.imgdata, rect, imageX, imageY, origData);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tiff-viewer', TiffViewer);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user