Compare commits
1 Commits
release
...
android-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d869223c82 |
9
.github/workflows/presubmit.yml
vendored
9
.github/workflows/presubmit.yml
vendored
@@ -150,20 +150,13 @@ jobs:
|
||||
pushd .
|
||||
cd build/android && printf "y" | ./build.sh presubmit-with-archive arm64-v8a
|
||||
popd
|
||||
- id: get_commit_msg
|
||||
uses: ./.github/actions/get-commit-msg
|
||||
- name: Check artifact sizes
|
||||
run: |
|
||||
python3 test/sizeguard/dump_artifact_size.py out/*.aar > current_size.json
|
||||
BYPASS_ARG=""
|
||||
if python3 test/sizeguard/check_bypass.py ${{ steps.get_commit_msg.outputs.hash }}; then
|
||||
BYPASS_ARG="--bypass"
|
||||
fi
|
||||
python3 test/sizeguard/check_size.py current_size.json \
|
||||
--target-branch origin/main \
|
||||
--threshold 20480 \
|
||||
--artifacts filament-android-release.aar/jni/arm64-v8a/libfilament-jni.so \
|
||||
$BYPASS_ARG
|
||||
--artifacts filament-android-release.aar/jni/arm64-v8a/libfilament-jni.so
|
||||
|
||||
build-ios:
|
||||
name: build-iOS
|
||||
|
||||
@@ -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.2'
|
||||
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.2'
|
||||
pod 'Filament', '~> 1.71.0'
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -7,9 +7,6 @@ A new header is inserted each time a *tag* is created.
|
||||
Instead, if you are authoring a PR for the main branch, add your release note to
|
||||
[NEW_RELEASE_NOTES.md](./NEW_RELEASE_NOTES.md).
|
||||
|
||||
## v1.71.2
|
||||
|
||||
|
||||
## v1.71.1
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -522,19 +522,11 @@ public class Material {
|
||||
* for stereoscopic rendering. If an application is not planning to render in stereo, this bit
|
||||
* should be turned off to avoid unnecessary material compilations.
|
||||
*</p>
|
||||
*<p>
|
||||
* Note that it is possible to override specialization constants on a per-MaterialInstance basis
|
||||
* (see {@link MaterialInstance#setConstant}). In that case, the programs compiled by a call to
|
||||
* Material::compile() may not be reusable by that MaterialInstance. It's better to call
|
||||
* MaterialInstance::compile() in cases where you intend to override specialization constants.
|
||||
*</p>
|
||||
* @param priority Which priority queue to use, LOW or HIGH.
|
||||
* @param variants Variants to include to the compile command.
|
||||
* @param handler An {@link java.util.concurrent.Executor Executor}. On Android this can also be a {@link android.os.Handler Handler}.
|
||||
* @param callback callback called on the main thread when the compilation is done on
|
||||
* by backend.
|
||||
*
|
||||
* @see MaterialInstance#compile
|
||||
*/
|
||||
public void compile(@NonNull CompilerPriorityQueue priority,
|
||||
int variants,
|
||||
|
||||
@@ -18,7 +18,6 @@ package com.google.android.filament;
|
||||
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Size;
|
||||
|
||||
import com.google.android.filament.proguard.UsedByNative;
|
||||
@@ -143,28 +142,6 @@ public class MaterialInstance {
|
||||
return mMaterial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously ensures that a subset of this MaterialInstance's variants are compiled.
|
||||
*
|
||||
* <p>This function behaves identically to {@link Material#compile}, but takes into account
|
||||
* the specific constants overridden by {@link #setConstant}.</p>
|
||||
*
|
||||
* @param priority Priority of the compile command.
|
||||
* @param variants Variants to include to the compile command.
|
||||
* @param handler An {@link java.util.concurrent.Executor Executor}. On Android this can also be a {@link android.os.Handler Handler}.
|
||||
* @param callback callback called on the main thread when the compilation is done on
|
||||
* by backend.
|
||||
*
|
||||
* @see Material#compile
|
||||
* @see #setConstant
|
||||
*/
|
||||
public void compile(@NonNull Material.CompilerPriorityQueue priority,
|
||||
int variants,
|
||||
@Nullable Object handler,
|
||||
@Nullable Runnable callback) {
|
||||
nCompile(getNativeObject(), priority.ordinal(), variants, handler, callback);
|
||||
}
|
||||
|
||||
/** @return the name associated with this instance */
|
||||
@NonNull
|
||||
public String getName() {
|
||||
@@ -425,69 +402,6 @@ public class MaterialInstance {
|
||||
nSetParameterFloat4(getNativeObject(), name, color[0], color[1], color[2], color[3]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides a specialization constant of this material instance.
|
||||
*
|
||||
* @param name The name of the constant as defined in the material.
|
||||
* @param value The value of the constant.
|
||||
* @see Material.Builder#constant
|
||||
*/
|
||||
public void setConstant(@NonNull String name, boolean value) {
|
||||
nSetConstantBool(getNativeObject(), name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides a specialization constant of this material instance.
|
||||
*
|
||||
* @param name The name of the constant as defined in the material.
|
||||
* @param value The value of the constant.
|
||||
* @see Material.Builder#constant
|
||||
*/
|
||||
public void setConstant(@NonNull String name, float value) {
|
||||
nSetConstantFloat(getNativeObject(), name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides a specialization constant of this material instance.
|
||||
*
|
||||
* @param name The name of the constant as defined in the material.
|
||||
* @param value The value of the constant.
|
||||
* @see Material.Builder#constant
|
||||
*/
|
||||
public void setConstant(@NonNull String name, int value) {
|
||||
nSetConstantInt(getNativeObject(), name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of a specialization constant by name.
|
||||
*
|
||||
* @param name The name of the constant as defined in the material.
|
||||
* @return The value of the constant.
|
||||
*/
|
||||
public boolean getConstantBoolean(@NonNull String name) {
|
||||
return nGetConstantBool(getNativeObject(), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of a specialization constant by name.
|
||||
*
|
||||
* @param name The name of the constant as defined in the material.
|
||||
* @return The value of the constant.
|
||||
*/
|
||||
public float getConstantFloat(@NonNull String name) {
|
||||
return nGetConstantFloat(getNativeObject(), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of a specialization constant by name.
|
||||
*
|
||||
* @param name The name of the constant as defined in the material.
|
||||
* @return The value of the constant.
|
||||
*/
|
||||
public int getConstantInt(@NonNull String name) {
|
||||
return nGetConstantInt(getNativeObject(), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set-up a custom scissor rectangle; by default it is disabled.
|
||||
*
|
||||
@@ -1023,17 +937,6 @@ public class MaterialInstance {
|
||||
@NonNull String name, int element, @NonNull @Size(min = 1) float[] v,
|
||||
@IntRange(from = 0) int offset, @IntRange(from = 1) int count);
|
||||
|
||||
private static native boolean nGetConstantBool(long nativeMaterialInstance, @NonNull String name);
|
||||
private static native float nGetConstantFloat(long nativeMaterialInstance, @NonNull String name);
|
||||
private static native int nGetConstantInt(long nativeMaterialInstance, @NonNull String name);
|
||||
|
||||
private static native void nSetConstantBool(long nativeMaterialInstance,
|
||||
@NonNull String name, boolean x);
|
||||
private static native void nSetConstantFloat(long nativeMaterialInstance,
|
||||
@NonNull String name, float x);
|
||||
private static native void nSetConstantInt(long nativeMaterialInstance,
|
||||
@NonNull String name, int x);
|
||||
|
||||
private static native void nSetParameterTexture(long nativeMaterialInstance,
|
||||
@NonNull String name, long nativeTexture, long sampler);
|
||||
|
||||
@@ -1097,5 +1000,4 @@ public class MaterialInstance {
|
||||
private static native int nGetDepthFunc(long nativeMaterialInstance);
|
||||
private static native void nSetTransparencyMode(long nativeMaterialInstance, int mode);
|
||||
private static native int nGetTransparencyMode(long nativeMaterialInstance);
|
||||
private static native void nCompile(long nativeMaterialInstance, int priority, int variants, Object handler, Runnable callback);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
GROUP=com.google.android.filament
|
||||
VERSION_NAME=1.71.2
|
||||
VERSION_NAME=1.71.0
|
||||
|
||||
POM_DESCRIPTION=Real-time physically based rendering engine for Android.
|
||||
|
||||
|
||||
@@ -319,7 +319,7 @@ the run number is <code>18023632663</code>.</p>
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../dup/test_ci_sizeguard.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<a rel="next prefetch" href="../notes/libs.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
|
||||
@@ -333,7 +333,7 @@ the run number is <code>18023632663</code>.</p>
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../dup/test_ci_sizeguard.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<a rel="next prefetch" href="../notes/libs.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en" class="light sidebar-visible" dir="ltr">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>CI: sizeguard - Filament</title>
|
||||
|
||||
|
||||
<!-- Custom HTML head -->
|
||||
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<link rel="shortcut icon" href="../favicon.png">
|
||||
<link rel="stylesheet" href="../css/variables.css">
|
||||
<link rel="stylesheet" href="../css/general.css">
|
||||
<link rel="stylesheet" href="../css/chrome.css">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
|
||||
<link rel="stylesheet" href="../fonts/fonts.css">
|
||||
|
||||
<!-- Highlight.js Stylesheets -->
|
||||
<link rel="stylesheet" href="../highlight.css">
|
||||
<link rel="stylesheet" href="../tomorrow-night.css">
|
||||
<link rel="stylesheet" href="../ayu-highlight.css">
|
||||
|
||||
<!-- Custom theme stylesheets -->
|
||||
|
||||
<!-- MathJax -->
|
||||
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
|
||||
<!-- Provide site root to javascript -->
|
||||
<script>
|
||||
var path_to_root = "../";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "light" : "light";
|
||||
</script>
|
||||
<!-- Start loading toc.js asap -->
|
||||
<script src="../toc.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="body-container">
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
<script>
|
||||
try {
|
||||
var theme = localStorage.getItem('mdbook-theme');
|
||||
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||
|
||||
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||
}
|
||||
|
||||
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||
}
|
||||
} catch (e) { }
|
||||
</script>
|
||||
|
||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||
<script>
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||
const html = document.documentElement;
|
||||
html.classList.remove('light')
|
||||
html.classList.add(theme);
|
||||
html.classList.add("js");
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
var sidebar = null;
|
||||
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||||
if (document.body.clientWidth >= 1080) {
|
||||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||
sidebar = sidebar || 'visible';
|
||||
} else {
|
||||
sidebar = 'hidden';
|
||||
}
|
||||
sidebar_toggle.checked = sidebar === 'visible';
|
||||
html.classList.remove('sidebar-visible');
|
||||
html.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<div style="display:flex;align-items:center;justify-content:center">
|
||||
<img class="flogo" src="../images/filament_logo_small.png"></img>
|
||||
</div>
|
||||
<!-- populated by js -->
|
||||
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
|
||||
<noscript>
|
||||
<iframe class="sidebar-iframe-outer" src="../toc.html"></iframe>
|
||||
</noscript>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
|
||||
<div class="page">
|
||||
<div id="menu-bar-hover-placeholder"></div>
|
||||
<div id="menu-bar" class="menu-bar sticky">
|
||||
<div class="left-buttons">
|
||||
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||
<i class="fa fa-bars"></i>
|
||||
</label>
|
||||
<!-- Filament: disable themes because the markdeep part does not look good for dark themes -->
|
||||
<!--
|
||||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||
<i class="fa fa-paint-brush"></i>
|
||||
</button>
|
||||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||||
</ul>
|
||||
-->
|
||||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h1 class="menu-title">Filament</h1>
|
||||
|
||||
<div class="right-buttons">
|
||||
<a href="https://github.com/google/filament" title="Git repository" aria-label="Git repository">
|
||||
<i id="git-repository-button" class="fa fa-github"></i>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="search-wrapper" class="hidden">
|
||||
<form id="searchbar-outer" class="searchbar-outer">
|
||||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||
</form>
|
||||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||
<div id="searchresults-header" class="searchresults-header"></div>
|
||||
<ul id="searchresults">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||
<script>
|
||||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="content" class="content">
|
||||
<main>
|
||||
<h1 id="sizeguard"><a class="header" href="#sizeguard">Sizeguard</a></h1>
|
||||
<p>This directory contains scripts used to monitor and gate the size of Filament artifacts.</p>
|
||||
<h2 id="scripts"><a class="header" href="#scripts">Scripts</a></h2>
|
||||
<h3 id="dump_artifact_sizepy"><a class="header" href="#dump_artifact_sizepy"><code>dump_artifact_size.py</code></a></h3>
|
||||
<p>Computes the sizes of build artifacts (e.g., <code>.aar</code>, <code>.tgz</code>) and their internal contents. It outputs a JSON representation of these sizes.</p>
|
||||
<p><strong>Usage:</strong></p>
|
||||
<pre><code class="language-bash">python3 dump_artifact_size.py out/*.aar > current_size.json
|
||||
</code></pre>
|
||||
<h3 id="check_sizepy"><a class="header" href="#check_sizepy"><code>check_size.py</code></a></h3>
|
||||
<p>Compares a current size JSON (generated by <code>dump_artifact_size.py</code>) against historical data stored in the <code>filament-assets</code> repository. It fails if any artifact's size increase exceeds a specified threshold.</p>
|
||||
<p><strong>Key Arguments:</strong></p>
|
||||
<ul>
|
||||
<li><code>current_json</code>: Path to the local JSON file.</li>
|
||||
<li><code>--threshold</code>: Size increase threshold in bytes (default: 20KB).</li>
|
||||
<li><code>--bypass</code>: If provided, the script will print the comparison but exit successfully even if thresholds are exceeded.</li>
|
||||
</ul>
|
||||
<h3 id="check_bypasspy"><a class="header" href="#check_bypasspy"><code>check_bypass.py</code></a></h3>
|
||||
<p>A utility script that checks the commit message for a specific tag to determine if the sizeguard check should be bypassed.</p>
|
||||
<p><strong>Usage:</strong></p>
|
||||
<ul>
|
||||
<li>Returns exit code <code>0</code> if the tag <code>SIZEGUARD_BYPASS</code> is found in the commit message.</li>
|
||||
<li>Returns exit code <code>1</code> otherwise.</li>
|
||||
</ul>
|
||||
<h2 id="continuous-integration"><a class="header" href="#continuous-integration">Continuous Integration</a></h2>
|
||||
<p>These scripts are integrated into the GitHub Actions workflows (e.g., <code>.github/workflows/presubmit.yml</code>).</p>
|
||||
<p>To bypass a failing sizeguard check in a PR, add the following tag on a new line in your commit message:</p>
|
||||
<pre><code>SIZEGUARD_BYPASS
|
||||
</code></pre>
|
||||
|
||||
</main>
|
||||
|
||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||
<!-- Mobile navigation buttons -->
|
||||
<a rel="prev" href="../dup/test_ci_renderdiff.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../notes/libs.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
|
||||
<div style="clear: both"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
<a rel="prev" href="../dup/test_ci_renderdiff.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
<a rel="next prefetch" href="../notes/libs.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
window.playground_copyable = true;
|
||||
</script>
|
||||
|
||||
|
||||
<script src="../elasticlunr.min.js"></script>
|
||||
<script src="../mark.min.js"></script>
|
||||
<script src="../searcher.js"></script>
|
||||
|
||||
<script src="../clipboard.min.js"></script>
|
||||
<script src="../highlight.js"></script>
|
||||
<script src="../book.js"></script>
|
||||
|
||||
<!-- Custom JS scripts -->
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -165,7 +165,7 @@
|
||||
|
||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||
<!-- Mobile navigation buttons -->
|
||||
<a rel="prev" href="../dup/test_ci_sizeguard.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<a rel="prev" href="../dup/test_ci_renderdiff.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
</div>
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
<a rel="prev" href="../dup/test_ci_sizeguard.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<a rel="prev" href="../dup/test_ci_renderdiff.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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.
@@ -100,9 +100,6 @@
|
||||
"test/backend/README.md": {
|
||||
"dest": "dup/test_ci_backend.md"
|
||||
},
|
||||
"test/sizeguard/README.md": {
|
||||
"dest": "dup/test_ci_sizeguard.md"
|
||||
},
|
||||
"filament/backend/test/README.md": {
|
||||
"dest": "dup/backend_test.md"
|
||||
},
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
- [backend](./dup/backend_test.md)
|
||||
- [CI: backend](./dup/test_ci_backend.md)
|
||||
- [CI: renderdiff](./dup/test_ci_renderdiff.md)
|
||||
- [CI: sizeguard](./dup/test_ci_sizeguard.md)
|
||||
- [Libraries](./notes/libs.md)
|
||||
- [bluegl](./dup/bluegl.md)
|
||||
- [bluevk](./dup/bluevk.md)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -357,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;
|
||||
@@ -384,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,
|
||||
@@ -419,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
|
||||
|
||||
@@ -1741,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{};
|
||||
|
||||
|
||||
@@ -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.2"
|
||||
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.2/filament-v1.71.2-ios.tgz" }
|
||||
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.71.0/filament-v1.71.0-ios.tgz" }
|
||||
|
||||
spec.libraries = 'c++'
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ struct MaterialInputs {
|
||||
#endif
|
||||
|
||||
#if defined(FRAG_OUTPUT0)
|
||||
FRAG_OUTPUT_PRECISION0 FRAG_OUTPUT_MATERIAL_TYPE0 FRAG_OUTPUT0;
|
||||
FRAG_OUTPUT_MATERIAL_TYPE0 FRAG_OUTPUT0;
|
||||
#endif
|
||||
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
161
test/render-validation/process_results.py
Normal file
161
test/render-validation/process_results.py
Normal file
@@ -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
|
||||
|
||||
35
test/render-validation/result-viewer/app.js
Normal file
35
test/render-validation/result-viewer/app.js
Normal file
@@ -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);
|
||||
699
test/render-validation/result-viewer/components/image-viewer.js
Normal file
699
test/render-validation/result-viewer/components/image-viewer.js
Normal file
@@ -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);
|
||||
333
test/render-validation/result-viewer/components/result-table.js
Normal file
333
test/render-validation/result-viewer/components/result-table.js
Normal file
@@ -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);
|
||||
100
test/render-validation/result-viewer/components/test-filter.js
Normal file
100
test/render-validation/result-viewer/components/test-filter.js
Normal file
@@ -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);
|
||||
330
test/render-validation/result-viewer/components/tiff-viewer.js
Normal file
330
test/render-validation/result-viewer/components/tiff-viewer.js
Normal file
@@ -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);
|
||||
225
test/render-validation/result-viewer/components/tools.js
Normal file
225
test/render-validation/result-viewer/components/tools.js
Normal file
@@ -0,0 +1,225 @@
|
||||
// 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";
|
||||
|
||||
// Generated by Gemini
|
||||
export class RadioButtonGroup extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.radio-group-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
label:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
input[type="radio"] {
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
/* Custom radio button appearance */
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 50%;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
input[type="radio"]:checked {
|
||||
border-color: #656565;
|
||||
background-color: #656565; /* Optional: fill color when checked */
|
||||
}
|
||||
input[type="radio"]:checked::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin: 3px; /* Adjust to center the dot */
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
input[type="radio"]:focus {
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
.label-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
`;
|
||||
static properties = {
|
||||
/**
|
||||
* An array of strings representing the choices for the radio buttons.
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
choices: { type: Array },
|
||||
|
||||
/**
|
||||
* The name for the radio button group. This is important for accessibility
|
||||
* and ensuring only one radio button in the group can be selected.
|
||||
* @type {string}
|
||||
*/
|
||||
name: { type: String },
|
||||
|
||||
/**
|
||||
* The currently selected value.
|
||||
* @type {string}
|
||||
*/
|
||||
value: { type: String, reflect: true },
|
||||
|
||||
/**
|
||||
* The label or title for the radio group.
|
||||
* @type {string}
|
||||
*/
|
||||
groupLabel: { type: String }
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.choices = [];
|
||||
this.name = 'radio-group'; // Default name
|
||||
this.value = '';
|
||||
this.groupLabel = '';
|
||||
}
|
||||
|
||||
_handleChange(event) {
|
||||
const selectedValue = event.target.value;
|
||||
if (this.value !== selectedValue) {
|
||||
this.value = selectedValue;
|
||||
// Dispatch a custom event with the new value
|
||||
this.dispatchEvent(new CustomEvent('radio-change', {
|
||||
detail: { value: this.value, radioId: this.id },
|
||||
bubbles: true, // Allows the event to bubble up through the DOM
|
||||
composed: true // Allows the event to cross shadow DOM boundaries
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="radio-group-container" role="radiogroup" aria-labelledby="group-label">
|
||||
${this.groupLabel ? html`<span id="group-label" class="group-label">${this.groupLabel}</span>` : ''}
|
||||
${this.choices.map(choice => html`
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name=${this.name}
|
||||
.value=${choice}
|
||||
.checked=${choice === this.value}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
<span class="label-text">${choice}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('radio-button-group', RadioButtonGroup);
|
||||
|
||||
// Generated by Gemini with some modifications
|
||||
class ModalDialog extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: none; /* Hidden by default */
|
||||
}
|
||||
|
||||
:host([open]) {
|
||||
display: block; /* Show when open attribute is present */
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000; /* Ensure it's on top */
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
min-width: 300px; /* Or your desired width */
|
||||
z-index: 1001; /* Above the backdrop */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
open: { type: Boolean, reflect: true },
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
_handleBackdropClick(event) {
|
||||
// Close only if the click is directly on the backdrop, not on the dialog itself
|
||||
if (event.target === this.shadowRoot.querySelector('.backdrop')) {
|
||||
this.open = false;
|
||||
this.dispatchEvent(new CustomEvent('dialog-closed', { bubbles: true, composed: true }));
|
||||
}
|
||||
}
|
||||
|
||||
_handleDialogClick(event) {
|
||||
// Prevent clicks inside the dialog from bubbling up to the backdrop
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.open) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="backdrop" @click="${this._handleBackdropClick}">
|
||||
<div class="dialog" @click="${this._handleDialogClick}">
|
||||
<slot name="header"><h2>Default Header</h2></slot>
|
||||
<slot>
|
||||
<p>This is the default content of the modal.</p>
|
||||
</slot>
|
||||
<slot name="footer">
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define('modal-dialog', ModalDialog);
|
||||
54
test/render-validation/result-viewer/index.html
Normal file
54
test/render-validation/result-viewer/index.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Render Validation Results</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0px;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
height: 100%;
|
||||
}
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #3498db;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
#app {
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
color: #7f8c8d;
|
||||
margin-top: 50px;
|
||||
}
|
||||
#container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<!-- Use ES modules to load Lit -->
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"lit": "https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="container">
|
||||
<div class="loading">Loading data...</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -135,11 +135,8 @@ class FileItem(Static):
|
||||
with Horizontal(id="file_row", classes="file-item-row"):
|
||||
yield Label(self.filename, id="lbl_filename", classes="file-name")
|
||||
|
||||
# Only show Load button for test configurations, not results
|
||||
if not self.filename.startswith("results_"):
|
||||
yield Button("▶", id="btn_load", variant="success", classes="compact-btn", tooltip="Load this test on device")
|
||||
else:
|
||||
yield Button("🌐", id="btn_serve", variant="success", classes="compact-btn", tooltip="Serve and view results locally")
|
||||
|
||||
yield Button("↓", id="btn_download", variant="primary", classes="compact-btn", tooltip="Download to PC")
|
||||
yield Button("✎", id="btn_start_rename", variant="warning", classes="compact-btn", tooltip="Rename on device")
|
||||
@@ -148,11 +145,9 @@ class FileItem(Static):
|
||||
yield Input(value=self.filename, id="inp_rename", classes="rename-input")
|
||||
yield Button("Save", id="btn_save_rename", variant="success", classes="compact-btn")
|
||||
yield Button("Cancel", id="btn_cancel_rename", classes="compact-btn")
|
||||
yield Label("", id="lbl_server_url", classes="server-url")
|
||||
|
||||
def on_mount(self):
|
||||
self.query_one("#rename_row").display = False
|
||||
self.query_one("#lbl_server_url").display = False
|
||||
|
||||
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
btn_id = event.button.id
|
||||
@@ -210,81 +205,8 @@ class FileItem(Static):
|
||||
self.app.notify(f"Loading {self.filename} on device...", title="Load Test")
|
||||
self.run_worker(self.load_on_device(), exclusive=True)
|
||||
|
||||
elif btn_id == "btn_serve":
|
||||
if hasattr(self, "server_proc") and self.server_proc:
|
||||
self.stop_server(event.button)
|
||||
else:
|
||||
event.button.disabled = True
|
||||
self.run_worker(self.start_server(event.button), exclusive=True)
|
||||
|
||||
async def start_server(self, button: Button) -> None:
|
||||
try:
|
||||
# Create a tmp directory for the results
|
||||
tmp_dir = os.path.join(os.getcwd(), "tmp")
|
||||
os.makedirs(tmp_dir, exist_ok=True)
|
||||
dest = os.path.join(tmp_dir, self.filename)
|
||||
|
||||
# Download file to tmp
|
||||
if not os.path.exists(dest):
|
||||
self.app.notify(f"Downloading {self.filename} for viewer...", title="Preparing Server")
|
||||
if self.is_internal:
|
||||
cmd = f"adb -s {self.serial} shell \"run-as {PACKAGE} cat {self.filepath}\" > \"{dest}\""
|
||||
proc = await asyncio.create_subprocess_shell(cmd)
|
||||
await proc.communicate()
|
||||
else:
|
||||
await run_adb_cmd("-s", self.serial, "pull", self.filepath, dest)
|
||||
|
||||
# Find unoccupied port
|
||||
import socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.bind(("", 0))
|
||||
port = s.getsockname()[1]
|
||||
s.close()
|
||||
|
||||
# Start server
|
||||
server_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "result-viewer", "server.py")
|
||||
self.server_proc = await asyncio.create_subprocess_exec(
|
||||
"python3", server_script, dest, "--port", str(port)
|
||||
)
|
||||
|
||||
button.label = "🛑"
|
||||
button.variant = "error"
|
||||
button.tooltip = "Stop Server"
|
||||
|
||||
lbl_url = self.query_one("#lbl_server_url", Label)
|
||||
lbl_url.update(f" ↳ Server: http://localhost:{port}")
|
||||
lbl_url.display = True
|
||||
|
||||
self.app.notify(f"Result viewer started on http://localhost:{port}", title="Server Started")
|
||||
except Exception as e:
|
||||
self.app.notify(f"Failed to start server: {e}", title="Server Error", severity="error")
|
||||
button.label = "🌐"
|
||||
button.variant = "success"
|
||||
button.tooltip = "Serve and view results locally"
|
||||
self.query_one("#lbl_server_url", Label).display = False
|
||||
self.server_proc = None
|
||||
finally:
|
||||
button.disabled = False
|
||||
|
||||
def stop_server(self, button: Button = None) -> None:
|
||||
if hasattr(self, "server_proc") and self.server_proc:
|
||||
try:
|
||||
self.server_proc.terminate()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
self.server_proc = None
|
||||
if button:
|
||||
button.label = "🌐"
|
||||
button.variant = "success"
|
||||
button.tooltip = "Serve and view results locally"
|
||||
try:
|
||||
self.query_one("#lbl_server_url", Label).display = False
|
||||
except Exception:
|
||||
pass
|
||||
self.app.notify("Server stopped", title="Server Stopped")
|
||||
|
||||
def on_unmount(self) -> None:
|
||||
self.stop_server()
|
||||
pass
|
||||
|
||||
async def load_on_device(self) -> None:
|
||||
cmd_args = [
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# Sizeguard
|
||||
|
||||
This directory contains scripts used to monitor and gate the size of Filament artifacts.
|
||||
|
||||
## Scripts
|
||||
|
||||
### `dump_artifact_size.py`
|
||||
Computes the sizes of build artifacts (e.g., `.aar`, `.tgz`) and their internal contents. It outputs a JSON representation of these sizes.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python3 dump_artifact_size.py out/*.aar > current_size.json
|
||||
```
|
||||
|
||||
### `check_size.py`
|
||||
Compares a current size JSON (generated by `dump_artifact_size.py`) against historical data stored in the `filament-assets` repository. It fails if any artifact's size increase exceeds a specified threshold.
|
||||
|
||||
**Key Arguments:**
|
||||
- `current_json`: Path to the local JSON file.
|
||||
- `--threshold`: Size increase threshold in bytes (default: 20KB).
|
||||
- `--bypass`: If provided, the script will print the comparison but exit successfully even if thresholds are exceeded.
|
||||
|
||||
### `check_bypass.py`
|
||||
A utility script that checks the commit message for a specific tag to determine if the sizeguard check should be bypassed.
|
||||
|
||||
**Usage:**
|
||||
- Returns exit code `0` if the tag `SIZEGUARD_BYPASS` is found in the commit message.
|
||||
- Returns exit code `1` otherwise.
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
These scripts are integrated into the GitHub Actions workflows (e.g., `.github/workflows/presubmit.yml`).
|
||||
|
||||
To bypass a failing sizeguard check in a PR, add the following tag on a new line in your commit message:
|
||||
```
|
||||
SIZEGUARD_BYPASS
|
||||
```
|
||||
@@ -1,47 +0,0 @@
|
||||
# Copyright (C) 2026 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
def commit_msg_has_tag(commit_hash, tag):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'log', '-n1', '--pretty=%B', commit_hash],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=True,
|
||||
text=True
|
||||
)
|
||||
for line in result.stdout.split('\n'):
|
||||
if tag == line.strip():
|
||||
return True
|
||||
return False
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error reading commit message: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: check_bypass.py <commit_hash>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
commit_hash = sys.argv[1]
|
||||
|
||||
if commit_msg_has_tag(commit_hash, "SIZEGUARD_BYPASS"):
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
@@ -103,10 +103,6 @@ def main():
|
||||
"--artifacts", nargs="+",
|
||||
help="List of artifact paths to check (e.g. 'foo.aar' or 'foo.aar/lib/arm64/bar.so')."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bypass", action="store_true",
|
||||
help="Bypass the size threshold check and exit successfully."
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -183,10 +179,7 @@ def main():
|
||||
|
||||
print("-" * 110)
|
||||
|
||||
if args.bypass:
|
||||
print("SUCCESS: Size guard test has been bypassed via commit message tag.")
|
||||
sys.exit(0)
|
||||
elif failures:
|
||||
if failures:
|
||||
print(f"FAILURE: {len(failures)} artifacts exceeded threshold of {args.threshold} bytes.")
|
||||
sys.exit(1)
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "filament",
|
||||
"version": "1.71.2",
|
||||
"version": "1.71.0",
|
||||
"description": "Real-time physically based rendering engine",
|
||||
"main": "filament.js",
|
||||
"module": "filament.js",
|
||||
|
||||
Reference in New Issue
Block a user