Compare commits

..
Author SHA1 Message Date
Powei Feng d869223c82 render-val: add page generation for results 2026-04-28 23:07:30 -07:00
Powei Feng 2a561211aa ppm: compile resolveDepth only for vk/metal (#9904)
Sampling from a multi-sampled texture is not allowed for webgpu.
From the comments in the material, we only need this for vk and
metal.
2026-04-20 18:36:25 +00:00
Ben Doherty 533294de9a Add support for custom outputs for unlit surface materials (#9912) 2026-04-20 11:46:21 -04:00
Ben Doherty 4787e341d6 Add feature level when outputting raw shaders (#9911) 2026-04-17 13:32:45 -04:00
Powei Feng afeae22423 android: [render-validation] collection of adjustments (#9898)
- A triangle tracker under the progress bar to track the test
   currently in view within the scroll view.
 - Text label to indicate the current device under test when a
   test is running.
 -  add filter intent arg
 - Add Fox (animation, skinning) and TransmissionRoughnessTest
   (transmission) to the test.
 - Add applyAnimation to the ValidationRunner
 - Make camera zoom in for default test and adjust tolerance
   acoordingly
 - Fix non-determinism of SSR by
     - waiting for more frames
     - account for false returned from beginFrame
     - Clear frame history on new test entry
 - Include Build.HARDWARE information into result
 - Make results array more thread-safe
 - Remove unused paths
 - Adjust tolerance for two tests
2026-04-16 19:28:48 +00:00
Mathias AgopianandBen Doherty a2547731c0 Backend: Centralize fence synchronization and fix potential data races. (#9899)
* Backend: Centralize fence synchronization and fix potential data races.

Transitioned fence synchronization from per-fence condition variables to a
centralized shared condition variable in DriverBase, enabling future global
error propagation and interruption.

Encapsulated synchronization by hiding the mutex and condition
variable within DriverBase and exposing `waitForFence` and `signalFence`.

Fixed a critical bug in Metal backend where spurious wakeups would reset the
relative timeout duration, potentially leading to much longer waits than
intended. Resolved by using absolute time points (wait_until).

Fixed potential data races in Metal, OpenGL, and WebGPU backends by ensuring
that shared state (status or state pointers) is always accessed under the
protection of the mutex, specifically by capturing it inside the predicate.

Simplified Vulkan and WebGPU fences to be purely passive data holders.

Trade-off: Spurious wakeups are increased due to the shared condition variable,
but are handled correctly by mandatory predicates in all backends.

* Update filament/backend/src/metal/MetalHandles.mm

Co-authored-by: Ben Doherty <bendoherty@google.com>

* Update filament/backend/src/metal/MetalHandles.mm

Co-authored-by: Ben Doherty <bendoherty@google.com>

---------

Co-authored-by: Ben Doherty <bendoherty@google.com>
2026-04-16 11:30:41 -07:00
Ben Doherty bfbb651ecd Metal: fix potential crash in when dynamic offsets are empty (#9908) 2026-04-16 14:05:10 -04:00
Anish Goyal c041970c76 Make VulkanProgram a ThreadSafeResource (#9788)
With the introduction of cache prewarming, handles for vulkan programs
will be managed on a separate thread in the backend along with the main
backend thread. This requires an atomic refcount to ensure accuracy and
prevent race conditions.
2026-04-16 17:15:53 +00:00
Mathias Agopian 3a78c78e7c gltfio: Continue if floatsCount is 0 in ResourceLoader.cpp
Address feedback from PR #9896 to avoid zero-sized allocations and potential
out-of-bounds reads.
2026-04-16 09:10:08 -07:00
Mathias Agopian 8bfdc60d68 imageio: Fix vulnerabilities in PSDDecoder from PR #9881
- PSDDecoder: Add dimension limits (max 30,000) to prevent integer
  overflows during LinearImage allocation.
- Add checks for stream failure during pixel decoding to handle truncated
  files safely.
- Follow existing pattern in this file by using std::runtime_error for
  these checks, which are caught and handled by resetting the stream and
  returning an empty image.
- Add comment that LinearImage allocation cannot overflow with the new
  limits.
2026-04-16 09:10:08 -07:00
Mathias Agopian f8f2280ee3 filaflat, image: Fix vulnerabilities from PR #9879 without aborts
- MaterialChunk: Return false in getTextShader when shaderSize is 0 to
  prevent out-of-bounds write of null terminator.
- Ktx1Bundle: In 3-argument constructor, check if total blobs calculation
  overflows uint32_t.
- Instead of aborting with FILAMENT_CHECK_POSTCONDITION, log a warning and
  set the object to a safe empty state (dimensions set to 0).
2026-04-16 09:10:08 -07:00
Mathias Agopian e6e7c0325e uberz: Fix vulnerabilities in convertOffsetsToPointers without aborts
- Implement fixes inspired by PR #9853 to make convertOffsetsToPointers
  size-aware and prevent OOB reads and writes.
- Change function signature to return bool instead of void, allowing graceful
  error propagation instead of runtime aborts.
- Replace FILAMENT_CHECK_PRECONDITION with explicit checks that log errors
  and return false.
- Update ArchiveCache in gltfio and main in tools/uberz to handle failure.
- Add unit tests to verify rejection of invalid offsets.
2026-04-16 09:10:08 -07:00
Mathias Agopian 8d16ad8882 gltfio: Fix vulnerabilities in ResourceLoader.cpp
- Implement PR #9878 to add bounds checking in normalizeSkinningWeights,
  preventing heap OOB write by clamping the count to available buffer size.
- Fix 3 similar vulnerabilities in uploadBuffers where accessor->count was
  trusted for malloc allocation before cgltf_accessor_unpack_floats.
- Add overflow checks for allocation sizes in uploadBuffers.
- Clamp accessor count based on available buffer bytes in uploadBuffers to
  prevent OOB read.
2026-04-16 09:10:08 -07:00
Mathias Agopian a943b57c86 ktxreader: Fix vulnerabilities and memory leaks in Ktx2Reader.cpp
- Implement PR #9842 to fix integer overflows in transcodeImageLevel.
- Fix potential division by zero in transcodeImageLevel if bytesPerPix is 0.
- Fix memory leaks in transcodeImageLevel by freeing allocated buffers on
  failure.
- Fix memory leak in Ktx2Reader::load by deleting PixelBufferDescriptor after
  use.
2026-04-16 09:10:08 -07:00
Mathias Agopian ffa2886493 gltfio: Fix security and memory corruption issues in Utility.cpp
- Fix potential division by zero in decodeMeshoptCompression when stride is 0.
- Fix ignored errors in release builds for meshopt decoding by returning bool
  and checking the result in ResourceLoader.cpp.
- Fix potential integer underflow in computeBindingSize when count is 0 by
  returning 0.
- Update ResourceLoader.cpp to skip buffer upload when computeBindingSize
  returns 0, preventing potential OOM or OOB reads.
2026-04-16 09:10:08 -07:00
Mathias Agopian 415ba57272 Correctly filter out ESSL1 variants
This fixes a recent breakage of the SSR variant while reducing the
size of featureLevel 0 materials.
2026-04-16 09:08:44 -07:00
Ben Doherty d6369f1007 Bump macOS minimum supported version to 11.0 (#9902) 2026-04-15 16:17:25 -07:00
Filament Bot 0203291b6a [automated] Updating /docs due to commit e85c021
Full commit hash is e85c021dec

DOCS_ALLOW_DIRECT_EDITS
2026-04-15 16:45:49 +00:00
Mathias Agopian e85c021dec remove the now unneeded FL0 materials
Filament needed to compile FL0 materials for both FL0 and FL1, but this
is now done automatically when a material is a FL0 material.
2026-04-15 09:20:26 -07:00
Mathias Agopian eb3d592d18 expose EntityManager::getIndex() publicly
This can be needed for interactions with an outside entity system.
Use with caution.
2026-04-15 09:19:00 -07:00
Powei Feng 3b5776a232 vk: improve readpixels (#9883)
- Account for multi-sampling in readback
- Add more types for Conversion::getComponentType()
2026-04-14 05:28:00 +00:00
Filament Bot afb0b39903 [automated] Updating /docs due to commit 562b66c
Full commit hash is 562b66c715

DOCS_ALLOW_DIRECT_EDITS
2026-04-14 04:46:10 +00:00
Powei Feng 54f3490414 Fix ssao precision issue around silhouettes
In the ssao pass, for silhouette pixels,

```
highp vec3 v = p - origin;
```

can be a "long" vector where the length cannot be captured with
mediump on mobile. This means that

```
float vv = dot(v, v);
float vn = dot(v, normal);
```
and other variables can overflow. This caused the silhouettes to
be dark when ssao is turned on a PowerVR gpu.

The fix is use highp for the relevant variables.
2026-04-13 21:43:48 -07:00
Kai e0b2133333 Fix direct lighting computation 2026-04-13 21:43:13 -07:00
Kai 9887eab8c0 update clearcoat ibl doc 2026-04-13 21:43:13 -07:00
Sungun Park 562b66c715 doc: Clarify MSVC support for windows
See issue #9894 for more context
2026-04-13 21:18:25 -07:00
Mathias Agopian 8ec732dadc FL0 materials are always compile via spirv-cross 2026-04-13 20:53:35 -07:00
Filament Bot 62ac2950c9 [automated] Updating /docs due to commit defba15
Full commit hash is defba15173

DOCS_ALLOW_DIRECT_EDITS
2026-04-13 21:27:53 +00:00
StreetwareGames defba15173 update the cached image too 2026-04-13 13:58:32 -07:00
StreetwareGames 4b4074450a fix milkyway image url 2026-04-13 13:58:32 -07:00
Mathias Agopian 84dc1b1c43 Add custom color grading LUT support and test UI in gltf_viewer.
Implemented the custom 3D LUT feature in the ColorGrading API, allowing
users to specify a custom LUT for final color mapping.

To test this feature, added a "Custom LUT" option to the color grading
settings in the gltf_viewer sample. This includes several procedurally
generated LUTs for testing purposes:
- Negative
- Grayscale
- Sepia
- Teal and Orange

Updated settings serialization (Settings.cpp) and the viewer UI
(ViewerGui.cpp) to support these options.

FIXES=[362596936]
2026-04-13 13:40:23 -07:00
Jocelin Cailloux 115c2cf8fe Add missing #include <cstring> and <optional> (#9887) 2026-04-10 18:54:05 +00:00
Powei Feng 5fed291284 3p: add models Fox and TransmissionRoughnessTest (#9889) 2026-04-10 05:10:33 +00:00
Powei Feng 7212317a3b android: [render-validation] UI update and enhanced tolerance definition (#9876)
- Update default_test.json with targeted test variants and documented
  tolerance presets.
- Refactor test suite to cover distinct code paths while maintaining
  multi-backend compatibility (OpenGL/Vulkan).
- Integrate TestProgressBar and UI summary text for better test progress
  visualization.
- Enhance validation managers with dynamic naming, zip extraction/export,
  and bundle creation.
- Update validation_app.py terminal UI to support multiple devices,
  device switching, and local result serving.
2026-04-09 15:39:20 -07:00
Ben Doherty e8ae717c28 Fix null texture pointer crash in GLDescriptorSet (#9884) 2026-04-08 13:00:41 -07:00
Filament Bot b6cb8096e6 [automated] Updating /docs due to commit be32398
Full commit hash is be3239810a

DOCS_ALLOW_DIRECT_EDITS
2026-04-08 02:09:32 +00:00
Benjamin Doherty be3239810a Release Filament 1.71.0 2026-04-07 18:41:31 -07:00
Mathias Agopian e8b268bce4 replace AutoresolveDifferingSampleCounts test
It's not clear what this test was trying to test and its golden was
wrong. 

Removed this test, and replaced it with a simple "autoresolve" test.
2026-04-03 17:02:18 -07:00
Mathias Agopian 88ef82a611 [backend/test] Fix MSAA resolves of depth/stencil attachments
- the first problem was that we don't need (or can) resolve RenderBuffer
  attachments, it's meaningless.

- the bigger problem was that we were marking all attachments of a FBO
  for potential resolve (at FBO creation).  Later, during endRenderPass, 
  the resolve pass itself would be triggered if the RT had a sidecar
  RT. But that's wrong because attachments that match the RT sample
  count don't need to be resolved.
  In turn, this was causing these attachments to be *discarded* 
  because, in beginRenderPass, we were detecting a case where it 
  looked like a MS attachment was used with non a MS rendertarget
  (and filament disallows that, the attachment must be discarded).
  We fix this by making sure attachments that don't need to be
  resolved are not marked as such.


FIXES=[453757787]
2026-04-03 17:02:18 -07:00
Mathias Agopian b4256c1c15 [backend/test] Fix LoadImageTest.UpdateImageSRGB precision mismatch on OpenGL
Relaxes exact pixel matching for `UpdateImageSRGB` test on software/MESA OpenGL drivers. Linear filtering on sRGB textures produces slightly different fractional weights for edge pixels compared to hardware texturing units.

FIXES=[453756688]
2026-04-03 17:02:18 -07:00
Mathias Agopian b49b61e131 [backend/test] Fix BlitTest scaling failures on MESA OpenGL
Relaxes exact pixel matching for `ColorMagnify` and `ColorMinify` tests on software/MESA OpenGL drivers. Software blit interpolation (glBlitFramebuffer) produces slightly different fractional weights for edge pixels compared to discrete hardware texturing units. 

FIXES=[453757697, 453758045, 453776623]
2026-04-03 17:02:18 -07:00
Mathias Agopian 92dc063a8a filaflat: Fix missing bounds checks in MaterialChunk::getTextShader (#9873)
- Added a bounds check in the DICTIONARY_NUMERIC_FLAG case to prevent a 
  heap buffer overflow when writing decoded numeric literals.

- Tightened the existing bounds check for dictionary strings to use `>=`
  instead of `>`, preventing a potential off-by-one overflow when 
  writing the null terminator.

- Fixed the `MaterialChunkHeapOverflow` and `MaterialChunkOOBReadText` 
  unit tests in `test_filaflat.cpp` by adding missing header fields to 
  the mock payloads so they correctly pass initialization.

Fixes #9871
2026-04-03 16:02:56 -07:00
Powei Feng e8fe85ee89 filamat: make DryRunner in Flattener not static (#9872)
This enables multiple thread to use MaterialBuilder/Flattener at
the cost of allocating, deallocating a flattener.
2026-04-03 18:41:50 +00:00
Doris Wu 0b63969aab Make the LruCache size of material/program cache configurable (#9865) 2026-04-03 03:10:14 +00:00
dependabot[bot]andPowei Feng af71d773ae build(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#9870)
Bumps the npm_and_yarn group with 2 updates in the /build/common/upload-release-assets directory: [brace-expansion](https://github.com/juliangruber/brace-expansion) and [minimatch](https://github.com/isaacs/minimatch).


Updates `brace-expansion` from 1.1.12 to 1.1.13
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

Updates `minimatch` from 3.1.3 to 3.1.5
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.3...v3.1.5)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Powei Feng <powei@google.com>
2026-04-02 17:06:54 +00:00
Mathias Agopian 57faae4645 postfx materials now support sRGB emulation in FL0 (#9866)
FIXES=[498317018]
2026-04-02 09:08:50 -07:00
Filament Bot e6cf5fa815 [automated] Updating /docs due to commit 426db0a
Full commit hash is 426db0a01d

DOCS_ALLOW_DIRECT_EDITS
2026-04-02 03:37:21 +00:00
127 changed files with 4659 additions and 1799 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
To build Filament, you must first install the following tools:
- CMake 3.22.1 (or more recent)
- clang 16.0 (or more recent)
- clang 16.0 (or more recent) (Required for Linux and macOS; see [Windows](#windows) section for MSVC support)
- [ninja 1.10](https://github.com/ninja-build/ninja/wiki/Pre-built-Ninja-packages) (or more recent)
Additional dependencies may be required for your operating system. Please refer to the appropriate
+1 -3
View File
@@ -13,7 +13,7 @@ cmake_minimum_required(VERSION 3.22.1)
# On iOS, the deployment target is set inside third_party/clang/ios.cmake
if (APPLE AND NOT IOS)
# This must be set before project() is called
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15 CACHE STRING "")
set(CMAKE_OSX_DEPLOYMENT_TARGET 11.0 CACHE STRING "")
endif()
# ==================================================================================================
@@ -47,8 +47,6 @@ option(FILAMENT_ENABLE_TSAN "Enable Thread Sanitizer" OFF)
option(FILAMENT_ENABLE_COVERAGE "Enable LLVM code coverage" OFF)
option(FILAMENT_ENABLE_FEATURE_LEVEL_0 "Enable Feature Level 0" ON)
option(FILAMENT_ENABLE_MULTIVIEW "Enable multiview for Filament" OFF)
option(FILAMENT_SUPPORTS_OSMESA "Enable OSMesa (headless GL context) for Filament" OFF)
+2 -2
View File
@@ -31,7 +31,7 @@ repositories {
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.70.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.70.2'
pod 'Filament', '~> 1.71.0'
```
## Documentation
+3
View File
@@ -7,6 +7,9 @@ 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.1
## v1.71.0
- matc: better material compression using a Multi-Base Variable-Length Interleaved Token Stream [⚠️ **New Material Version**]
@@ -220,3 +220,23 @@ Java_com_google_android_filament_ColorGrading_nBuilderCurves(JNIEnv* env, jclass
env->ReleaseFloatArrayElements(midPoint_, midPoint, JNI_ABORT);
env->ReleaseFloatArrayElements(scale_, scale, JNI_ABORT);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_ColorGrading_nBuilderCustomLut(JNIEnv *env, jclass,
jlong nativeBuilder, jobject buffer, jint dimension) {
ColorGrading::Builder* builder = (ColorGrading::Builder*) nativeBuilder;
if (dimension == 0) {
return;
}
float3* data = (float3*) env->GetDirectBufferAddress(buffer);
size_t count = size_t(dimension) * dimension * dimension;
utils::FixedCapacityVector<math::float3> lut =
utils::FixedCapacityVector<math::float3>::with_capacity(count);
for (size_t i = 0; i < count; ++i) {
lut.push_back(data[i]);
}
builder->customLut(std::move(lut), (uint8_t)dimension);
}
@@ -556,6 +556,22 @@ public class ColorGrading {
return this;
}
/**
* Specifies a custom 3D color grading LUT to map the final sRGB color.
* The LUT is applied after post-processing and in LDR (sRGB space).
* The data must be a 3D array of float3 (RGB) values.
* The data must remain valid until build() is called.
*
* @param buffer Direct ByteBuffer containing the custom LUT data (3D array of float3).
* @param dimension Dimension of the custom LUT (e.g., 16, 32, 64).
*
* @return This Builder, for chaining calls
*/
public Builder customLut(@NonNull java.nio.Buffer buffer, int dimension) {
nBuilderCustomLut(mNativeBuilder, buffer, dimension);
return this;
}
/**
* Creates the IndirectLight object and returns a pointer to it.
*
@@ -620,6 +636,7 @@ public class ColorGrading {
private static native void nBuilderVibrance(long nativeBuilder, float vibrance);
private static native void nBuilderSaturation(long nativeBuilder, float saturation);
private static native void nBuilderCurves(long nativeBuilder, float[] gamma, float[] midPoint, float[] scale);
private static native void nBuilderCustomLut(long nativeBuilder, java.nio.Buffer buffer, int dim);
private static native long nBuilderBuild(long nativeBuilder, long nativeEngine);
}
@@ -316,9 +316,9 @@ class ModelViewer(
* @param frameTimeNanos time in nanoseconds when the frame started being rendered,
* typically comes from {@link android.view.Choreographer.FrameCallback}
*/
fun render(frameTimeNanos: Long) {
fun render(frameTimeNanos: Long): Boolean {
if (!uiHelper.isReadyToRender) {
return
return false
}
// Allow the resource loader to finalize textures that have become ready.
@@ -337,7 +337,9 @@ class ModelViewer(
}
// Render the scene, unless the renderer wants to skip the frame.
var rendered = false
if (renderer.beginFrame(swapChain!!, frameTimeNanos)) {
rendered = true
renderer.render(view)
debugFrameCallback?.let {
@@ -360,6 +362,7 @@ class ModelViewer(
renderer.endFrame()
}
return rendered
}
/*
+1 -1
View File
@@ -1,5 +1,5 @@
GROUP=com.google.android.filament
VERSION_NAME=1.70.2
VERSION_NAME=1.71.0
POM_DESCRIPTION=Real-time physically based rendering engine for Android.
@@ -16,14 +16,15 @@ filament {
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")
}
// don't forget to update MainACtivity.kt when/if changing this.
tasks.register('copyDamagedHelmetGltf', Copy) {
// don't forget to update MainActivity.kt when/if changing this.
tasks.register('copyModels', Copy) {
from file("../../../third_party/models/DamagedHelmet/DamagedHelmet.glb")
from file("../../../third_party/models/Fox/Fox.glb")
from file("../../../third_party/models/TransmissionRoughnessTest/TransmissionRoughnessTest.glb")
into file("src/main/assets/models")
rename {String fileName -> "helmet.glb"}
}
preBuild.dependsOn copyDamagedHelmetGltf
preBuild.dependsOn copyModels
clean.doFirst {
delete "src/main/assets/envs"
@@ -5,7 +5,9 @@
"vulkan"
],
"models": {
"DamagedHelmet": "helmet.glb"
"DamagedHelmet": "DamagedHelmet.glb",
"Fox": "Fox.glb",
"TransmissionRoughnessTest": "TransmissionRoughnessTest.glb"
},
"presets": [
{
@@ -20,7 +22,7 @@
"center": [
0,
0,
0
-0.8
],
"lookAt": [
0,
@@ -34,8 +36,24 @@
}
},
"tolerance": {
"maxAbsDiff": 0.1,
"maxFailingPixelsFraction": 0.0
"mode": "OR",
"maxFailingPixelsFraction": 0.0005,
"children": [
// Scenario: Geometry rasterization snapping differences between different GPU architectures (e.g., Adreno versions).
// Method: Allow a 1-pixel positional shift but demand a very strict color match.
{
"maxAbsDiff": 0.02,
"shiftRadius": 1,
"blurRadius": 0
},
// Scenario: Floating-point math precision differences in shaders across architectures.
// Method: Demand exact pixel position but allow a slightly looser color precision variance.
{
"maxAbsDiff": 0.05,
"shiftRadius": 0,
"blurRadius": 0
}
]
}
},
{
@@ -56,6 +74,43 @@
]
}
}
},
{
"name": "relaxed_tolerance",
// Scenario: Screen Space Reflections (SSR) stochastic raymarching noise.
// Method: Apply a 7x7 low-pass filter (blurRadius: 3) to smooth out high-frequency noise,
// allow slight reflection warping (shiftRadius: 2), and strictly check the smoothed underlying color.
"tolerance": {
"maxAbsDiff": 0.09,
"shiftRadius": 2,
"blurRadius": 4,
"maxFailingPixelsFraction": 0.0005
},
"rendering": {}
},
{
"name": "noisy_tolerance",
"rendering": {},
"tolerance": {
"mode": "OR",
"maxFailingPixelsFraction": 0.0005,
"children": [
// Scenario: Bloom & MSAA edge jitter caused by slightly different sub-pixel sampling patterns.
// Method: Allow a 1-pixel positional shift with a moderate color variance.
{
"maxAbsDiff": 0.05,
"shiftRadius": 1,
"blurRadius": 0
},
// Scenario: High-frequency noise generated by noisy post-processing effects.
// Method: Apply a 5x5 low-pass filter (blurRadius: 2) to smooth the noise, demanding exact position and strict color match.
{
"maxAbsDiff": 0.05,
"shiftRadius": 0,
"blurRadius": 2
}
]
}
}
],
"tests": [
@@ -73,64 +128,32 @@
]
},
{
"name": "ssao",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true
}
},
{
"name": "msaa",
"apply_presets": [
"base"
],
"rendering": {
"view.msaa.enabled": true
}
},
{
"name": "bloom",
"name": "msaa_custom",
"apply_presets": [
"base",
"tilted"
"noisy_tolerance"
],
"rendering": {
"view.bloom.enabled": true
"view.msaa.enabled": true,
"view.msaa.customResolve": true
}
},
{
"name": "aa_none",
"name": "msaa_standard",
"apply_presets": [
"base"
"base",
"noisy_tolerance"
],
"rendering": {
"view.antiAliasing": "NONE"
}
},
{
"name": "aa_fxaa",
"apply_presets": [
"base"
],
"rendering": {
"view.antiAliasing": "FXAA"
}
},
{
"name": "dithering_none",
"apply_presets": [
"base"
],
"rendering": {
"view.dithering": "NONE"
"view.msaa.enabled": true,
"view.msaa.customResolve": false
}
},
{
"name": "msaa_8",
"apply_presets": [
"base"
"base",
"noisy_tolerance"
],
"rendering": {
"view.msaa.enabled": true,
@@ -138,14 +161,13 @@
}
},
{
"name": "taa_custom",
"name": "fxaa",
"apply_presets": [
"base"
"base",
"noisy_tolerance"
],
"rendering": {
"view.taa.enabled": true,
"view.taa.feedback": 0.2,
"view.taa.jitterPattern": "HALTON_23_X16"
"view.antiAliasing": "FXAA"
}
},
{
@@ -169,57 +191,7 @@
}
},
{
"name": "ssao_radius_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.radius": 1.0
}
},
{
"name": "ssao_radius_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.radius": 0.1
}
},
{
"name": "ssao_power_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.power": 2.0
}
},
{
"name": "ssao_power_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.power": 0.5
}
},
{
"name": "ssao_bias_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.bias": 0.05
}
},
{
"name": "ssao_resolution_half",
"name": "ssao_half_res",
"apply_presets": [
"base"
],
@@ -228,30 +200,11 @@
"view.ssao.resolution": 0.5
}
},
{
"name": "ssao_resolution_full",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.resolution": 1.0
}
},
{
"name": "ssao_intensity_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.intensity": 2.0
}
},
{
"name": "ssao_bent_normals",
"apply_presets": [
"base"
"base",
"relaxed_tolerance"
],
"rendering": {
"view.ssao.enabled": true,
@@ -299,98 +252,20 @@
}
},
{
"name": "ssr_basic",
"name": "ssr",
"apply_presets": [
"base"
"base",
"relaxed_tolerance"
],
"rendering": {
"view.screenSpaceReflections.enabled": true
}
},
{
"name": "ssr_thickness_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.5
}
},
{
"name": "ssr_bias_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.bias": 0.1
}
},
{
"name": "ssr_maxDistance_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.maxDistance": 5.0
}
},
{
"name": "ssr_stride_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.stride": 4.0
}
},
{
"name": "ssr_stride_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.stride": 1.0
}
},
{
"name": "ssr_thickness_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.01
}
},
{
"name": "ssr_maxDistance_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.maxDistance": 1.0
}
},
{
"name": "ssr_bias_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.bias": 0.001
}
},
{
"name": "ssr_quality_combo",
"apply_presets": [
"base"
"base",
"relaxed_tolerance"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
@@ -399,89 +274,20 @@
}
},
{
"name": "bloom_levels_high",
"name": "bloom",
"apply_presets": [
"base"
"base",
"noisy_tolerance"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.levels": 8
}
},
{
"name": "bloom_levels_low",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.levels": 4
}
},
{
"name": "bloom_resolution_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.resolution": 1024
}
},
{
"name": "bloom_strength_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.strength": 0.5
}
},
{
"name": "bloom_strength_low",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.strength": 0.01
}
},
{
"name": "bloom_blendMode_interpolate",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.blendMode": "INTERPOLATE"
}
},
{
"name": "bloom_no_threshold",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.threshold": false
}
},
{
"name": "bloom_quality_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.quality": "HIGH"
"view.bloom.enabled": true
}
},
{
"name": "bloom_lensflare",
"apply_presets": [
"base"
"base",
"noisy_tolerance"
],
"rendering": {
"view.bloom.enabled": true,
@@ -491,7 +297,8 @@
{
"name": "bloom_lensflare_no_starburst",
"apply_presets": [
"base"
"base",
"noisy_tolerance"
],
"rendering": {
"view.bloom.enabled": true,
@@ -502,7 +309,8 @@
{
"name": "bloom_lensflare_chromatic",
"apply_presets": [
"base"
"base",
"noisy_tolerance"
],
"rendering": {
"view.bloom.enabled": true,
@@ -511,51 +319,29 @@
}
},
{
"name": "bloom_lensflare_ghosts",
"name": "bloom_quality_high",
"apply_presets": [
"base"
"base",
"noisy_tolerance"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.ghostCount": 8
"view.bloom.quality": "HIGH"
}
},
{
"name": "bloom_lensflare_ghostSpacing",
"name": "bloom_blendMode_interpolate",
"apply_presets": [
"base"
"base",
"noisy_tolerance"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.ghostSpacing": 0.8
"view.bloom.blendMode": "INTERPOLATE"
}
},
{
"name": "bloom_halo_thick",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.haloThickness": 0.2
}
},
{
"name": "bloom_halo_radius",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.haloRadius": 0.5
}
},
{
"name": "dof_basic",
"name": "dof",
"apply_presets": [
"base"
],
@@ -564,43 +350,13 @@
}
},
{
"name": "dof_cocScale_high",
"name": "dof_native_res",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.cocScale": 2.0
}
},
{
"name": "dof_cocScale_low",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.cocScale": 0.5
}
},
{
"name": "dof_cocAspectRatio",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.cocAspectRatio": 2.0
}
},
{
"name": "dof_maxApertureDiameter",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.maxApertureDiameter": 0.05
"view.dof.nativeResolution": true
}
},
{
@@ -614,50 +370,7 @@
}
},
{
"name": "dof_nativeResolution",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.nativeResolution": true
}
},
{
"name": "dof_rings_high",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.foregroundRingCount": 5,
"view.dof.backgroundRingCount": 5
}
},
{
"name": "dof_rings_low",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.foregroundRingCount": 3,
"view.dof.backgroundRingCount": 3
}
},
{
"name": "dof_max_coc",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.maxForegroundCOC": 16,
"view.dof.maxBackgroundCOC": 16
}
},
{
"name": "fog_basic",
"name": "fog",
"apply_presets": [
"base"
],
@@ -666,97 +379,7 @@
}
},
{
"name": "fog_distance",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.distance": 10.0
}
},
{
"name": "fog_cutOffDistance",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.cutOffDistance": 100.0
}
},
{
"name": "fog_maximumOpacity",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.maximumOpacity": 0.5
}
},
{
"name": "fog_height",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.height": 5.0
}
},
{
"name": "fog_heightFalloff",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.heightFalloff": 0.5
}
},
{
"name": "fog_density_high",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.density": 0.5
}
},
{
"name": "fog_inScatteringStart",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.inScatteringStart": 5.0
}
},
{
"name": "fog_inScatteringSize",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.inScatteringSize": 10.0
}
},
{
"name": "fog_fogColorFromIbl",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.fogColorFromIbl": true
}
},
{
"name": "vignette_basic",
"name": "vignette",
"apply_presets": [
"base"
],
@@ -764,46 +387,6 @@
"view.vignette.enabled": true
}
},
{
"name": "vignette_midPoint",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.midPoint": 0.8
}
},
{
"name": "vignette_roundness_circle",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.roundness": 1.0
}
},
{
"name": "vignette_roundness_rect",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.roundness": 0.0
}
},
{
"name": "vignette_feather_sharp",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.feather": 0.1
}
},
{
"name": "cg_filmic",
"apply_presets": [
@@ -840,33 +423,6 @@
"view.colorGrading.toneMapping": "PBR_NEUTRAL"
}
},
{
"name": "cg_contrast_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.contrast": 1.5
}
},
{
"name": "cg_saturation_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.saturation": 1.5
}
},
{
"name": "cg_exposure_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.exposure": 1.0
}
},
{
"name": "shadow_vsm",
"apply_presets": [
@@ -889,12 +445,169 @@
{
"name": "shadow_vsm_highPrecision",
"apply_presets": [
"base"
"base",
"relaxed_tolerance"
],
"rendering": {
"view.shadowType": "VSM",
"view.vsmShadowOptions.highPrecision": true
}
},
{
"name": "msaa_custom_bloom",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.customResolve": true,
"view.bloom.enabled": true
}
},
{
"name": "msaa_standard_bloom",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.customResolve": false,
"view.bloom.enabled": true
}
},
{
"name": "msaa_custom_dof",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.customResolve": true,
"view.dof.enabled": true
}
},
{
"name": "msaa_standard_dof",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.customResolve": false,
"view.dof.enabled": true
}
},
{
"name": "dof_bloom",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.dof.enabled": true,
"view.bloom.enabled": true
}
},
{
"name": "ssao_ssr",
"apply_presets": [
"base",
"relaxed_tolerance"
],
"rendering": {
"view.ssao.enabled": true,
"view.screenSpaceReflections.enabled": true
}
},
{
"name": "bloom_ssr",
"apply_presets": [
"base",
"noisy_tolerance",
"relaxed_tolerance"
],
"rendering": {
"view.bloom.enabled": true,
"view.screenSpaceReflections.enabled": true
}
},
{
"name": "dof_ssr",
"apply_presets": [
"base",
"relaxed_tolerance"
],
"rendering": {
"view.dof.enabled": true,
"view.screenSpaceReflections.enabled": true
}
},
{
"name": "fog_bloom",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.fog.enabled": true,
"view.bloom.enabled": true
}
},
{
"name": "fxaa_bloom",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.antiAliasing": "FXAA",
"view.bloom.enabled": true
}
},
{
"name": "fxaa_dof",
"apply_presets": [
"base",
"noisy_tolerance"
],
"rendering": {
"view.antiAliasing": "FXAA",
"view.dof.enabled": true
}
},
{
"name": "fox_animation",
"models": [
"Fox"
],
"apply_presets": [
"base"
],
"rendering": {
"animation": {
"enabled": true,
"time": 0.5
}
}
},
{
"name": "transmission",
"models": [
"TransmissionRoughnessTest"
],
"apply_presets": [
"base"
],
"rendering": {
"animation": {
"enabled": true,
"time": 0.5
}
}
}
]
}
@@ -22,6 +22,7 @@ import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.os.FileObserver
import android.text.Html
import android.util.Log
import android.view.Choreographer
@@ -30,8 +31,10 @@ import android.view.View
import android.view.WindowManager
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.Spinner
import android.widget.TextView
import com.google.android.filament.utils.KTX1Loader
@@ -61,10 +64,16 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private lateinit var choreographer: Choreographer
private lateinit var statusTextView: TextView
private lateinit var testResultsHeader: TextView
private lateinit var testProgress: android.widget.ProgressBar
private lateinit var progressContainer: FrameLayout
private lateinit var testProgress: com.google.android.filament.validation.TestProgressBar
private lateinit var progressTriangle: ImageView
private lateinit var scrollView: ScrollView
private lateinit var testSummaryText: TextView
private lateinit var deviceInfoText: TextView
private lateinit var resultsContainer: LinearLayout
private lateinit var inputManager: ValidationInputManager
private var currentInput: ValidationInputManager.ValidationInput? = null
private var fileObserver: FileObserver? = null
private var currentAlphaDiffBitmap: Bitmap? = null
private var globalEnhancementFactor: Float = 1.0f
@@ -97,8 +106,10 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private val frameScheduler = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
choreographer.postFrameCallback(this)
modelViewer?.render(frameTimeNanos)
validationRunner?.onFrame(frameTimeNanos)
val rendered = modelViewer?.render(frameTimeNanos) ?: false
if (rendered) {
validationRunner?.onFrame(frameTimeNanos)
}
}
}
@@ -112,9 +123,22 @@ class MainActivity : Activity(), ValidationRunner.Callback {
statusTextView = findViewById(R.id.status_text)
testResultsHeader = findViewById(R.id.test_results_header)
progressContainer = findViewById(R.id.progress_container)
testProgress = findViewById(R.id.test_progress)
progressTriangle = findViewById(R.id.progress_triangle)
scrollView = findViewById(R.id.scroll_view)
testSummaryText = findViewById(R.id.test_summary_text)
deviceInfoText = findViewById(R.id.device_info_text)
deviceInfoText.text = "Running on: ${android.os.Build.MODEL}"
resultsContainer = findViewById(R.id.results_container)
scrollView.viewTreeObserver.addOnScrollChangedListener {
updateTrianglePosition()
}
scrollView.viewTreeObserver.addOnGlobalLayoutListener {
updateTrianglePosition()
}
runButton = findViewById(R.id.run_button)
loadButton = findViewById(R.id.load_button)
optionsButton = findViewById(R.id.options_button)
@@ -134,13 +158,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) {}
})
// Setup Run Button
runButton.setOnClickListener {
currentInput?.let { input ->
// Always use the generateGoldens flag from the intent/input
startValidation(input)
}
}
// Setup Run Button (Listener is set in handleIntent based on state)
// Setup Load Button
loadButton.setOnClickListener {
@@ -150,9 +168,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
// Setup Options Menu Button
optionsButton.setOnClickListener { view ->
val popup = android.widget.PopupMenu(this, view)
popup.menu.add(0, 1, 0, "Generate Golden")
popup.menu.add(0, 2, 0, "Export Test")
popup.menu.add(0, 3, 0, "Export Result")
popup.menu.add(0, 4, 0, "Test ADB Info")
popup.menu.add(0, 5, 0, "Result ADB Info")
popup.menu.add(0, 6, 0, "Toggle Enhancement Slider")
@@ -160,21 +175,15 @@ class MainActivity : Activity(), ValidationRunner.Callback {
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
1 -> {
currentInput?.let { input ->
val goldenInput = input.copy(generateGoldens = true)
startValidation(goldenInput)
}
}
2 -> exportTestBundleAction()
3 -> exportTestResultsAction()
4 -> showTestAdbInfo()
5 -> showResultAdbInfo()
6 -> {
enhancementContainer.visibility = if (enhancementContainer.visibility == View.VISIBLE) View.GONE else View.VISIBLE
enhancementContainer.visibility =
if (enhancementContainer.visibility == View.VISIBLE) View.GONE else View.VISIBLE
}
7 -> {
backendFilterContainer.visibility = if (backendFilterContainer.visibility == View.VISIBLE) View.GONE else View.VISIBLE
backendFilterContainer.visibility =
if (backendFilterContainer.visibility == View.VISIBLE) View.GONE else View.VISIBLE
}
}
true
@@ -187,11 +196,12 @@ class MainActivity : Activity(), ValidationRunner.Callback {
choreographer = Choreographer.getInstance()
inputManager = ValidationInputManager(this)
setupFileObserver()
handleIntent()
}
private fun showLoadDialog() {
val exportDir = getExternalFilesDir(null) ?: filesDir
val exportDir = inputManager.getBaseDir()
// Filter out result zips (starting with "results_") to only show test bundles
val zips = exportDir.listFiles { _, name ->
name.endsWith(".zip") && !name.startsWith("results_")
@@ -222,7 +232,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
}
private fun showTestAdbInfo() {
val exportDir = getExternalFilesDir(null) ?: filesDir
val exportDir = inputManager.getBaseDir()
val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder()
@@ -238,12 +248,11 @@ class MainActivity : Activity(), ValidationRunner.Callback {
message.append("<b>--- PUSH TO DEVICE ---</b><br>")
if (isInternal) {
message.append("1. <tt>adb push &lt;filename&gt; /sdcard/Download/</tt><br>")
message.append("2. <tt>adb shell \"run-as $packageName cp /sdcard/Download/&lt;filename&gt; files/\"</tt><br>")
message.append("<tt>cat &lt;filename&gt; | adb shell \"run-as $packageName sh -c 'cat &gt; files/&lt;filename&gt;'\"</tt><br>")
} else {
message.append("<tt>adb push &lt;filename&gt; $path/</tt><br>")
}
message.append("<br>Note: Use underscores instead of spaces in &lt;filename&gt;.")
message.append("<br>Note: Use underscores instead of spaces in &lt;filename&gt;. The default bundle is <tt>default_test.zip</tt>.")
AlertDialog.Builder(this)
.setTitle("Test Bundle ADB Info")
@@ -253,7 +262,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
}
private fun showResultAdbInfo() {
val exportDir = getExternalFilesDir(null) ?: filesDir
val exportDir = inputManager.getBaseDir()
val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder()
@@ -289,29 +298,14 @@ class MainActivity : Activity(), ValidationRunner.Callback {
statusTextView.text = "Loading ${file.name}..."
CoroutineScope(Dispatchers.Main).launch {
try {
val config = inputManager.loadFromZip(file)
val baseDir = getExternalFilesDir(null) ?: filesDir
val outputDir = File(baseDir, "validation_results").apply { mkdirs() }
// Clear existing results UI and state
resultsContainer.removeAllViews()
diffImageViews.clear()
resultManager = null
val newInput = ValidationInputManager.ValidationInput(
config = config,
outputDir = outputDir,
generateGoldens = false,
autoRun = false,
autoExport = false,
autoExportResults = false,
sourceZip = file
)
currentInput = newInput
statusTextView.text = "Loaded ${config.name}"
Log.i(TAG, "Setting header to: Test Results: ${config.name}")
testResultsHeader.text = "${config.name}"
val input = inputManager.loadValidationInputFromZip(file)
onInputUpdated(input)
Log.i(TAG, "Setting header to: Test Results: ${input!!.config!!.name}")
} catch (e: Exception) {
Log.e(TAG, "Failed to load zip", e)
statusTextView.text = "Error: ${e.message}"
@@ -352,23 +346,81 @@ class MainActivity : Activity(), ValidationRunner.Callback {
}
}
private fun onInputUpdated(input: ValidationInputManager.ValidationInput) {
loadButton.isEnabled = inputManager.hasAnyTest();
if (input.config == null && !inputManager.hasDefaultTest()) {
statusTextView.text = "No test loaded. Tap Generate Goldens to create a default test."
testResultsHeader.text = "No Test Loaded"
runButton.text = "Generate Goldens"
runButton.isEnabled = true
runButton.setOnClickListener {
statusTextView.text = "Generating default goldens..."
CoroutineScope(Dispatchers.Main).launch {
try {
// Pressing this button is as if we received an intent to generate goldens
val emptyIntent = Intent()
emptyIntent.putExtra("generate_goldens", true)
onInputUpdated(inputManager.resolveConfig(emptyIntent))
} catch (e: Exception) {
statusTextView.text = "Failed to generate goldens: ${e.message}"
Log.e(TAG, "Failed to generate goldens", e)
}
}
}
return;
}
if (input.config == null && inputManager.hasAnyTest()) {
// User can choose to load at least one test
statusTextView.text = "No test loaded. Please select a test to run."
runButton.text = "No Test Loaded"
runButton.isEnabled = false
runButton.setOnClickListener(null)
return;
}
// Here the input has a configuration, meaning we can run it, so just copy it but account
// for the existance of the default test.
currentInput = input.copy(
generateGoldens = input.generateGoldens && !inputManager.hasDefaultTest()
)
currentInput?.let {
runButton.text = "Run Test"
runButton.isEnabled = true
// Update header
val name = it.config!!.name
Log.i(TAG, "handleIntent: Setting header to: Test Results: ${name}")
testResultsHeader.text = "${name}"
if (it.autoRun) {
startValidation(it)
return
}
// Test is not running, but it is ready
runButton.setOnClickListener {
currentInput?.let { startValidation(it) }
}
// Clear existing results UI and state
clearUIState();
validationRunner?.let {
it.cleanup()
modelViewer = null
}
validationRunner = null
// Just show status
statusTextView.text = "Ready: ${name}"
}
}
private fun handleIntent() {
statusTextView.text = "Resolving configuration..."
CoroutineScope(Dispatchers.Main).launch {
try {
val input = inputManager.resolveConfig(intent)
// Update header
Log.i(TAG, "handleIntent: Setting header to: Test Results: ${input.config.name}")
testResultsHeader.text = "${input.config.name}"
currentInput = input
if (input.autoRun) {
startValidation(input)
} else {
// Just show status
statusTextView.text = "Ready: ${input.config.name}"
}
onInputUpdated(inputManager.resolveConfig(intent))
} catch (e: Exception) {
Log.e(TAG, "Failed to resolve config", e)
statusTextView.text = "Error: ${e.message}"
@@ -376,27 +428,75 @@ class MainActivity : Activity(), ValidationRunner.Callback {
}
}
private fun setupFileObserver() {
val baseDir = inputManager.getBaseDir()
fileObserver = object : FileObserver(baseDir.absolutePath) {
override fun onEvent(event: Int, path: String?) {
if (path == null) return
val isRelevant = (event and (FileObserver.CLOSE_WRITE or FileObserver.MOVED_TO)) != 0
if (isRelevant && path.endsWith(".zip")) {
Log.i(TAG, "FileObserver: Detected new zip file: $path")
if (currentInput?.config == null) {
CoroutineScope(Dispatchers.Main).launch {
try {
// Pressing this button is as if we received an intent to generate goldens
val emptyIntent = Intent()
onInputUpdated(inputManager.resolveConfig(emptyIntent))
} catch (e: Exception) {
statusTextView.text = "Failed to update input on file change: ${e.message}"
Log.e(TAG, "Failed to update input on file change", e)
}
}
}
}
}
}
fileObserver?.startWatching()
}
private fun createResultManager(outputDir: File): ValidationResultManager {
return ValidationResultManager(
outputDir = outputDir,
deviceName = android.os.Build.MODEL,
deviceHardware = android.os.Build.HARDWARE,
deviceCodeName = android.os.Build.DEVICE,
androidVersion = android.os.Build.VERSION.RELEASE,
androidBuildNumber = android.os.Build.DISPLAY
)
}
private fun clearUIState() {
resultsContainer.removeAllViews()
diffImageViews.clear()
resultManager = null
}
private fun startValidation(input: ValidationInputManager.ValidationInput) {
try {
resultsContainer.removeAllViews()
diffImageViews.clear()
val config = input.config ?: return
clearUIState();
// Disable UI while running
enhancementSlider.isEnabled = false
Log.i(TAG, "Starting validation with config: ${input.config.name}")
runButton.isEnabled = false
loadButton.isEnabled = false
optionsButton.isEnabled = false
backendRadioGroup.isEnabled = false
for (i in 0 until backendRadioGroup.childCount) {
backendRadioGroup.getChildAt(i).isEnabled = false
}
Log.i(TAG, "Starting validation with config: ${config.name}")
Log.i(TAG, "Output dir: ${input.outputDir.absolutePath}")
testResultsHeader.text = "${input.config.name}"
testProgress.visibility = View.VISIBLE
testProgress.progress = 0
progressContainer.visibility = View.VISIBLE
testSummaryText.visibility = View.GONE
deviceInfoText.visibility = View.VISIBLE
totalPassed = 0
totalFailed = 0
testProgress.reset(1)
resultManager = createResultManager(input.outputDir)
@@ -410,11 +510,13 @@ class MainActivity : Activity(), ValidationRunner.Callback {
// resources before we can proceed.
validationRunner?.let {
it.cleanup()
modelViewer = null
}
validationRunner = ValidationRunner(this, surfaceView, input.config, resultManager!!, backendFilter)
validationRunner = ValidationRunner(this, surfaceView, config, resultManager!!, backendFilter)
validationRunner?.callback = this
validationRunner?.generateGoldens = input.generateGoldens
validationRunner?.testFilter = input.testFilter
validationRunner?.start()
} catch (e: Exception) {
@@ -450,14 +552,54 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onDestroy() {
super.onDestroy()
choreographer.removeFrameCallback(frameScheduler)
fileObserver?.stopWatching()
validationRunner?.let {
it.cleanup()
modelViewer = null
}
}
private var currentRenderedBitmap: Bitmap? = null
private var currentGoldenBitmap: Bitmap? = null
private var currentDiffBitmap: Bitmap? = null
private var totalPassed = 0
private var totalFailed = 0
private var totalTestsCount = 1
private fun updateTrianglePosition() {
if (progressContainer.visibility != View.VISIBLE || resultsContainer.childCount == 0) return
val scrollY = scrollView.scrollY
val visibleHeight = scrollView.height
val centerY = scrollY + visibleHeight / 2f
var bestIndex = -1
var minDistance = Float.MAX_VALUE
for (i in 0 until resultsContainer.childCount) {
val child = resultsContainer.getChildAt(i)
val childCenterY = child.top + child.height / 2f
val dist = Math.abs(childCenterY - centerY)
if (dist < minDistance) {
minDistance = dist
bestIndex = i
}
}
if (bestIndex >= 0) {
val progressWidth = testProgress.width
if (progressWidth > 0 && totalTestsCount > 0) {
val segmentWidth = progressWidth.toFloat() / totalTestsCount
val targetX = (bestIndex * segmentWidth) + (segmentWidth / 2f)
progressTriangle.translationX = targetX - progressTriangle.width / 2f
}
}
}
override fun onTestFinished(result: ValidationResult) {
runOnUiThread {
if (result.passed) totalPassed++ else totalFailed++
testProgress.addResult(result.passed)
val status = "Test ${result.testName} finished: ${if(result.passed) "PASS" else "FAIL"}"
statusTextView.text = status
Log.i(TAG, status)
@@ -511,35 +653,34 @@ class MainActivity : Activity(), ValidationRunner.Callback {
)
fun addImage(label: String, bitmap: Bitmap?, isDiff: Boolean) {
if (bitmap != null) {
val container = LinearLayout(this)
container.orientation = LinearLayout.VERTICAL
container.setPadding(0, 0, 10, 0)
val labelView = TextView(this)
labelView.text = label
labelView.textSize = 9f
container.addView(labelView)
val iv = ImageView(this)
iv.setImageBitmap(bitmap) // Use the same bitmap (or copy if needed, but same is usually fine for UI)
iv.layoutParams = LinearLayout.LayoutParams(250, 250) // Smaller thumbnails
iv.scaleType = ImageView.ScaleType.FIT_CENTER
iv.setBackgroundColor(0xFF404040.toInt())
if (isDiff) {
diffImageViews.add(iv)
applyEnhancementToView(iv, globalEnhancementFactor)
}
iv.setOnClickListener {
showImageDialog(testImages, label)
}
container.addView(iv)
imagesRow.addView(container)
if (bitmap == null) {
return;
}
val container = LinearLayout(this)
container.orientation = LinearLayout.VERTICAL
container.setPadding(0, 0, 10, 0)
val labelView = TextView(this)
labelView.text = label
labelView.textSize = 9f
container.addView(labelView)
val iv = ImageView(this)
iv.setImageBitmap(bitmap!!) // Use the same bitmap (or copy if needed, but same is usually fine for UI)
iv.layoutParams = LinearLayout.LayoutParams(250, 250) // Smaller thumbnails
iv.scaleType = ImageView.ScaleType.FIT_CENTER
iv.setBackgroundColor(0xFF404040.toInt())
if (isDiff) {
diffImageViews.add(iv)
applyEnhancementToView(iv, globalEnhancementFactor)
}
iv.setOnClickListener {
showImageDialog(testImages, label)
}
container.addView(iv)
imagesRow.addView(container)
}
addImage("Rendered", currentRenderedBitmap, false)
@@ -553,6 +694,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
resultContainer.addView(imagesRow)
resultsContainer.addView(resultContainer)
resultsContainer.post { updateTrianglePosition() }
// Clear current images for next test
currentRenderedBitmap = null
@@ -564,56 +706,94 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onTestProgress(current: Int, total: Int) {
runOnUiThread {
testProgress.max = total
testProgress.progress = current
totalTestsCount = Math.max(1, total)
testProgress.setMax(total)
updateTrianglePosition()
}
}
override fun onAllTestsFinished() {
runOnUiThread {
testProgress.visibility = View.GONE
val total = totalPassed + totalFailed
val colorPassed = if (totalPassed > 0) "#4CAF50" else "#000000"
val colorFailed = if (totalFailed > 0) "#F44336" else "#000000"
val html = "Passed: <font color='$colorPassed'><b>$totalPassed</b></font> / $total &nbsp;&nbsp;&nbsp; Failed: <font color='$colorFailed'><b>$totalFailed</b></font>"
testSummaryText.text = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY)
testSummaryText.visibility = View.VISIBLE
deviceInfoText.visibility = View.GONE
statusTextView.text = "All tests finished!"
enhancementSlider.isEnabled = true
Log.i(TAG, "All tests finished " + if (currentInput?.autoExport == true) "Exporting bundle" else "x")
if (currentInput?.autoExport == true) {
exportTestBundleAction()
// Re-enable UI
enhancementSlider.isEnabled = true
runButton.isEnabled = true
loadButton.isEnabled = true
optionsButton.isEnabled = true
backendRadioGroup.isEnabled = true
for (i in 0 until backendRadioGroup.childCount) {
backendRadioGroup.getChildAt(i).isEnabled = true
}
if (currentInput?.autoExportResults == true) {
exportTestResultsAction()
Log.i(TAG, "All tests finished")
currentInput?.let {
if (it.generateGoldens) {
exportTestBundleAction(it)
} else {
exportTestResultsAction(it)
}
}
}
}
private fun exportTestBundleAction() {
currentInput?.let { input ->
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: createResultManager(input.outputDir)
val zip = rm.exportTestBundle(input.config, timestamp)
if (zip != null) {
private fun exportTestBundleAction(input: ValidationInputManager.ValidationInput) {
val config = input.config ?: return
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: createResultManager(input.outputDir)
val bundleNameOverride = if (input.sourceZip == null) "default_test" else null
val zip = rm.exportTestBundle(config, timestamp, bundleNameOverride)
if (zip != null) {
if (input.sourceZip == null) {
val msg = "Exported Default Bundle: ${zip.name}."
statusTextView.text = msg
Log.i(TAG, "Exported test bundle to ${zip.absolutePath}.")
CoroutineScope(Dispatchers.Main).launch {
try {
// here we don't want to call onInputUpdated(newInput) since we want to keep
// the state after the test ran
currentInput = inputManager.loadValidationInputFromZip(zip)
Log.i(TAG, "Auto-loaded default bundle.")
} catch (e: Exception) {
statusTextView.text = "Failed to auto-load default bundle: ${e.message}"
Log.e(TAG, "Failed to load default bundle", e)
}
}
} else {
val msg = "Exported Bundle: ${zip.name}"
statusTextView.text = msg
Log.i(TAG, "Exported test bundle to ${zip.absolutePath}")
} else {
statusTextView.text = "Export Bundle failed"
Log.e(TAG, "Export Bundle failed")
}
}
} else {
statusTextView.text = "Export Bundle failed"
Log.e(TAG, "Export Bundle failed")
}
}
private fun exportTestResultsAction() {
currentInput?.let { input ->
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: createResultManager(input.outputDir)
val zip = rm.exportTestResults(input.sourceZip, timestamp)
if (zip != null) {
val msg = "Exported Results: ${zip.name}"
statusTextView.text = msg
Log.i(TAG, "Exported results to ${zip.absolutePath}")
} else {
statusTextView.text = "Export Results failed"
Log.e(TAG, "Export Results failed")
}
private fun exportTestResultsAction(input: ValidationInputManager.ValidationInput) {
if (input.config == null) {
return
}
val config = input.config!!
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: createResultManager(input.outputDir)
val zip = rm.exportTestResults(input.sourceZip, config, timestamp)
if (zip != null) {
val msg = "Exported Results: ${zip.name}"
statusTextView.text = msg
Log.i(TAG, "Exported results to ${zip.absolutePath}")
} else {
statusTextView.text = "Export Results failed"
Log.e(TAG, "Export Results failed")
}
}
@@ -691,11 +871,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
if (currentIndex == -1) currentIndex = 0
var currentDialogBitmap: Bitmap? = null
dialog.setOnDismissListener {
currentDialogBitmap?.recycle()
currentDialogBitmap = null
}
var currentDialogEnhancement = globalEnhancementFactor
val matrix = android.graphics.Matrix()
@@ -704,6 +879,11 @@ class MainActivity : Activity(), ValidationRunner.Callback {
var lastTouchY = 0f
var isDragging = false
// this is to prevent resetting the view matrix when flipping through goloden, rendered,
// diff images. These actions will trigger the OnLayoutChangeListener, but in these cases,
// We want to keep the same view matrix.
var matrixResettable = true
val scaleDetector = android.view.ScaleGestureDetector(this, object : android.view.ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: android.view.ScaleGestureDetector): Boolean {
matrix.postScale(detector.scaleFactor, detector.scaleFactor, detector.focusX, detector.focusY)
@@ -741,7 +921,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
val (label, file) = availableFiles[currentIndex]
typeView.text = label
currentDialogBitmap?.recycle()
val oldBitmap = currentDialogBitmap
currentDialogBitmap = android.graphics.BitmapFactory.decodeFile(file.absolutePath)
imageView.setImageBitmap(currentDialogBitmap)
@@ -756,6 +936,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
enhancementContainer.visibility = View.GONE
imageView.colorFilter = null
}
oldBitmap?.recycle()
}
fun resetMatrix() {
@@ -782,19 +963,32 @@ class MainActivity : Activity(), ValidationRunner.Callback {
}
imageView.addOnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
if (matrixResettable &&
(left != oldLeft || right != oldRight || top != oldTop || bottom != oldBottom)) {
resetMatrix()
}
}
dialog.setOnDismissListener {
matrixResettable = true;
currentDialogBitmap?.recycle()
currentDialogBitmap = null
}
btnClose.setOnClickListener { dialog.dismiss() }
btnReset.setOnClickListener { resetMatrix() }
btnReset.setOnClickListener {
matrixResettable = true
resetMatrix()
}
btnPrev.setOnClickListener {
currentIndex = (currentIndex - 1 + availableFiles.size) % availableFiles.size
matrixResettable = false
updateView()
}
btnNext.setOnClickListener {
currentIndex = (currentIndex + 1) % availableFiles.size
matrixResettable = false
updateView()
}
@@ -807,6 +1001,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onProgressChanged(seekBar: android.widget.SeekBar?, progress: Int, fromUser: Boolean) {
currentDialogEnhancement = 1.0f + (progress / 100f) * 49.0f
enhancementLabel.text = String.format(Locale.US, "Enhance: %.1fx", currentDialogEnhancement)
matrixResettable = false
updateView()
}
override fun onStartTrackingTouch(seekBar: android.widget.SeekBar?) {}
@@ -0,0 +1,55 @@
package com.google.android.filament.validation
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
class TestProgressBar(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {
private var maxTests = 1
private val results = mutableListOf<Boolean>()
private val paintPass = Paint().apply { color = Color.parseColor("#4CAF50") } // Green
private val paintFail = Paint().apply { color = Color.parseColor("#F44336") } // Red
private val paintBg = Paint().apply { color = Color.parseColor("#E0E0E0") } // Gray
fun reset(max: Int) {
maxTests = Math.max(1, max)
results.clear()
invalidate()
}
fun addResult(passed: Boolean) {
results.add(passed)
invalidate()
}
// Kept for partial compatibility with previous ProgressBar
fun setMax(max: Int) {
maxTests = Math.max(1, max)
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val w = width.toFloat()
val h = height.toFloat()
// Draw background
canvas.drawRect(0f, 0f, w, h, paintBg)
if (results.isEmpty() || maxTests <= 0) return
val segmentWidth = w / maxTests
for (i in results.indices) {
val paint = if (results[i]) paintPass else paintFail
val left = i * segmentWidth
val right = (i + 1) * segmentWidth
canvas.drawRect(left, 0f, right, h, paint)
}
}
}
@@ -24,16 +24,8 @@ import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.zip.ZipFile
/**
* Handles the retrieval and preparation of test configuration and assets.
* Supports loading from:
* 1. Intent extras (local path or URL)
* 2. Default embedded assets (fallback)
*/
class ValidationInputManager(private val context: Context) {
companion object {
@@ -41,35 +33,54 @@ class ValidationInputManager(private val context: Context) {
}
data class ValidationInput(
val config: RenderTestConfig,
val config: RenderTestConfig?,
val outputDir: File,
val generateGoldens: Boolean,
val autoRun: Boolean = false,
val autoExport: Boolean = false,
val autoExportResults: Boolean = false,
val sourceZip: File? = null
val sourceZip: File? = null,
val testFilter: String? = null
)
public fun getBaseDir() : File {
// Note that this is the app's internal directory. Not visible unless you're root or you're
// this app.
return context.filesDir
}
public fun hasDefaultTest(): Boolean {
val defaultTestZip = File(getBaseDir(), "default_test.zip")
return defaultTestZip.exists()
}
public fun hasAnyTest(): Boolean {
val dir = getBaseDir()
val matchedFiles = dir.listFiles {_, name ->
name.endsWith("_test.zip")
}
return matchedFiles != null && matchedFiles.isNotEmpty()
}
/**
* Resolves the test configuration based on the provided intent extras.
* This may involve extracting assets or downloading files.
*/
suspend fun resolveConfig(intent: Intent): ValidationInput = withContext(Dispatchers.IO) {
val testConfigPath = intent.getStringExtra("test_config")
val urlConfig = intent.getStringExtra("url_config")
val urlModelsBase = intent.getStringExtra("url_models_base")
val zipPath = intent.getStringExtra("zip_path")
val generateGoldens = intent.getBooleanExtra("generate_goldens", false)
val autoRun = intent.getBooleanExtra("auto_run", false)
val autoExport = intent.getBooleanExtra("auto_export", false)
val autoExportResults = intent.getBooleanExtra("auto_export_results", false)
// If we get a generateGoldens signal, then it should trigger a run
val autoRun = intent.getBooleanExtra("auto_run", false) ||
intent.getBooleanExtra("generate_goldens", false)
val testFilter = if (autoRun) intent.getStringExtra("test_filter") else null
val outputPath = intent.getStringExtra("output_path")
Log.i(TAG, "Resolving config with outputPath: $outputPath")
val baseDir = context.getExternalFilesDir(null) ?: context.filesDir
val baseDir = getBaseDir()
Log.i(TAG, "Base directory for resolution: ${baseDir.absolutePath}")
val outputDir = if (!outputPath.isNullOrBlank()) {
val file = File(outputPath)
val resolved = if (file.isAbsolute) {
@@ -77,7 +88,7 @@ class ValidationInputManager(private val context: Context) {
} else {
File(baseDir, outputPath)
}
// Critical check: if resolved is root or very short, it's likely wrong
if (resolved.absolutePath == "/" || resolved.parent == null) {
Log.w(TAG, "Resolved outputDir is root ($resolved), defaulting to app-specific dir")
@@ -88,76 +99,115 @@ class ValidationInputManager(private val context: Context) {
} else {
File(baseDir, "validation_results")
}
if (!outputDir.exists() && !outputDir.mkdirs()) {
Log.e(TAG, "Failed to create outputDir: ${outputDir.absolutePath}")
}
Log.i(TAG, "Final outputDir: ${outputDir.absolutePath}")
val sourceZipFile = if (zipPath != null) {
var sourceZipFile = if (zipPath != null) {
val file = File(zipPath)
if (file.isAbsolute) {
val resolvedFile = if (file.isAbsolute) {
file
} else {
File(baseDir, zipPath)
}
Log.i(TAG, "Resolved zipPath '$zipPath' to ${resolvedFile.absolutePath} (exists: ${resolvedFile.exists()})")
resolvedFile
} else {
null
}
val config = when {
sourceZipFile != null && sourceZipFile.exists() -> loadFromZip(sourceZipFile)
urlConfig != null -> downloadConfig(urlConfig, urlModelsBase)
testConfigPath != null -> ConfigParser.parseFromPath(testConfigPath)
else -> extractDefaultAssets()
// Auto-load logic: if no zipPath provided, and default_test.zip is the ONLY zip, auto-load it
if (sourceZipFile == null) {
val exportDir = baseDir
val availableZips = exportDir.listFiles { _, name ->
name.endsWith(".zip") && !name.startsWith("results_")
}?.toList() ?: emptyList()
if (availableZips.size == 1 && availableZips[0].name == "default_test.zip") {
sourceZipFile = availableZips[0]
Log.i(TAG, "Auto-loaded default_test.zip since it's the only test bundle available.")
}
}
return@withContext ValidationInput(config, outputDir, generateGoldens, autoRun, autoExport, autoExportResults, sourceZipFile)
val config = when {
sourceZipFile != null && sourceZipFile.exists() -> loadFromZip(sourceZipFile)
generateGoldens -> extractDefaultAssets()
else -> null
}
return@withContext ValidationInput(config, outputDir, generateGoldens, autoRun, sourceZipFile, testFilter)
}
suspend fun loadFromZip(zipFile: File): RenderTestConfig {
Log.i(TAG, "Unzipping validation bundle: ${zipFile.absolutePath}")
// Use a unique cache dir based on timestamp or just overwrite a common one
// Overwriting is safer to avoid filling up disk, but we must ensure we don't conflict with current run
private var lastUnzippedFile: String? = null
private var lastUnzippedTime: Long = 0
suspend fun loadValidationInputFromZip(file: File) : ValidationInput {
val config = loadFromZip(file)
val baseDir = getBaseDir()
val outputDir = File(baseDir, "validation_results").apply { mkdirs() }
val newInput = ValidationInputManager.ValidationInput(
config = config,
outputDir = outputDir,
generateGoldens = false,
autoRun = false,
sourceZip = file,
testFilter = null
)
return newInput
}
private suspend fun loadFromZip(zipFile: File): RenderTestConfig {
val baseCacheDir = context.externalCacheDir ?: context.cacheDir
val cacheDir = File(baseCacheDir, "unzipped_validation")
if (cacheDir.exists()) cacheDir.deleteRecursively()
cacheDir.mkdirs()
Log.i(TAG, "Using cacheDir: ${cacheDir.absolutePath}")
if (lastUnzippedFile == zipFile.absolutePath && lastUnzippedTime == zipFile.lastModified() &&
cacheDir.exists()) {
Log.i(TAG, "Zip file unmodified, skipping unzip to preserve generated goldens.")
} else {
Log.i(TAG, "Unzipping validation bundle: ${zipFile.absolutePath}")
if (cacheDir.exists()) cacheDir.deleteRecursively()
cacheDir.mkdirs()
ZipFile(zipFile).use { zip ->
val entries = zip.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
val entryFile = File(cacheDir, entry.name)
// specific check to avoid zip slip vulnerability (though low risk here)
if (!entryFile.canonicalPath.startsWith(cacheDir.canonicalPath)) {
throw SecurityException("Zip entry is outside of the target dir: ${entry.name}")
}
if (entry.isDirectory) {
entryFile.mkdirs()
} else {
entryFile.parentFile?.mkdirs()
zip.getInputStream(entry).use { input ->
FileOutputStream(entryFile).use { output ->
input.copyTo(output)
}
Log.i(TAG, "Using cacheDir: ${cacheDir.absolutePath}")
ZipFile(zipFile).use { zip ->
val entries = zip.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
val entryFile = File(cacheDir, entry.name)
// specific check to avoid zip slip vulnerability (though low risk here)
if (!entryFile.canonicalPath.startsWith(cacheDir.canonicalPath)) {
throw SecurityException("Zip entry is outside of the target dir: ${entry.name}")
}
if (entry.isDirectory) {
entryFile.mkdirs()
} else {
entryFile.parentFile?.mkdirs()
zip.getInputStream(entry).use { input ->
FileOutputStream(entryFile).use { output ->
input.copyTo(output)
}
}
}
}
}
lastUnzippedFile = zipFile.absolutePath
lastUnzippedTime = zipFile.lastModified()
}
// Find config.json
// We look for a file ending in .json within the unzipped structure
// Exclude results.json if it happened to be there
val jsonFiles = cacheDir.walkTopDown()
.filter { it.isFile && it.extension == "json" && it.name != "results.json" }
.toList()
if (jsonFiles.isEmpty()) throw IllegalStateException("No config.json found in zip")
// Prefer one named config.json or take the first one
val configFile = jsonFiles.find { it.name == "config.json" } ?: jsonFiles.first()
@@ -165,7 +215,7 @@ class ValidationInputManager(private val context: Context) {
return ConfigParser.parseFromPath(configFile.absolutePath)
}
private suspend fun extractDefaultAssets(): RenderTestConfig {
suspend fun extractDefaultAssets(): RenderTestConfig {
Log.i(TAG, "Extracting default assets...")
val filesDir = context.getExternalFilesDir(null) ?: context.filesDir
val assetManager = context.assets
@@ -181,84 +231,39 @@ class ValidationInputManager(private val context: Context) {
}
}
// Copy DamagedHelmet.glb
// Copy models
val modelsDir = File(filesDir, "models")
modelsDir.mkdirs()
val modelOut = File(modelsDir, "helmet.glb")
assetManager.open("models/helmet.glb").use { input ->
FileOutputStream(modelOut).use { output ->
input.copyTo(output)
assetManager.list("models")?.forEach { modelFileName ->
val modelOut = File(modelsDir, modelFileName)
assetManager.open("models/$modelFileName").use { input ->
FileOutputStream(modelOut).use { output ->
input.copyTo(output)
}
}
}
// Update config to point to relative path (standardizing on relative for portability where possible)
// or absolute. Here we use relative as per previous logic.
val configJson = JSONObject(configOut.readText())
// Dynamically name the test bundle based on device model
configJson.put("name", "${android.os.Build.MODEL} Test")
val models = configJson.getJSONObject("models")
// Ensure the default model points to the extracted file
// We can use absolute path to be safe since we know where it is now.
models.put("DamagedHelmet", modelOut.absolutePath)
// Update all model paths to point to the extracted files in the models directory
val keys = models.keys()
val newModels = JSONObject()
while (keys.hasNext()) {
val key = keys.next()
val fileName = models.getString(key)
newModels.put(key, java.io.File(modelsDir, fileName).absolutePath)
}
configJson.put("models", newModels)
configOut.writeText(configJson.toString(2))
return ConfigParser.parseFromPath(configOut.absolutePath)
}
private suspend fun downloadConfig(urlConfig: String, urlModelsBase: String?): RenderTestConfig {
Log.i(TAG, "Downloading config from $urlConfig")
val filesDir = context.getExternalFilesDir(null) ?: context.filesDir
val configDir = File(filesDir, "config")
configDir.mkdirs()
val modelsDir = File(filesDir, "models")
modelsDir.mkdirs()
val configName = "downloaded_config.json"
val configFile = File(configDir, configName)
downloadFile(urlConfig, configFile)
if (urlModelsBase != null) {
val configJson = JSONObject(configFile.readText())
val models = configJson.optJSONObject("models")
if (models != null) {
val keys = models.keys()
while (keys.hasNext()) {
val key = keys.next()
val modelPath = models.getString(key)
val fileName = File(modelPath).name
val modelFile = File(modelsDir, fileName)
val modelUrl = "$urlModelsBase/$fileName"
Log.i(TAG, "Downloading model: $fileName from $modelUrl")
downloadFile(modelUrl, modelFile)
// Update config to point to absolute path
models.put(key, modelFile.absolutePath)
}
configFile.writeText(configJson.toString())
}
}
return ConfigParser.parseFromPath(configFile.absolutePath)
}
private fun downloadFile(urlStr: String, destFile: File) {
val url = URL(urlStr)
val connection = url.openConnection() as HttpURLConnection
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} for $urlStr")
}
destFile.parentFile?.mkdirs()
connection.inputStream.use { input ->
FileOutputStream(destFile).use { output ->
input.copyTo(output)
}
}
}
}
@@ -35,6 +35,7 @@ data class ValidationResult(
class ValidationResultManager(
private val outputDir: File,
private val deviceName: String,
private val deviceHardware: String,
private val deviceCodeName: String,
private val androidVersion: String,
private val androidBuildNumber: String
@@ -44,7 +45,7 @@ class ValidationResultManager(
private const val TAG = "ValidationResultManager"
}
private val results = mutableListOf<ValidationResult>()
private val results = java.util.concurrent.CopyOnWriteArrayList<ValidationResult>()
private val gpuDriverInfos = JSONObject()
init {
@@ -88,18 +89,19 @@ class ValidationResultManager(
* - input test bundle (as nested zip), if provided
* - diff images (if any failure)
*/
fun exportTestResults(sourceZip: File?, timestamp: String): File? {
fun exportTestResults(sourceZip: File?, config: RenderTestConfig, timestamp: String): File? {
// Safe parent dir resolution
val parentDir = outputDir.canonicalFile.parentFile ?: outputDir.parentFile
if (parentDir == null) return null
val resultZipName = "results_$timestamp"
val zipFile = File(parentDir, "$resultZipName.zip")
val tempZipFile = File(parentDir, "$resultZipName.zip.tmp")
val finalZipFile = File(parentDir, "$resultZipName.zip")
Log.i(TAG, "Exporting results to ${zipFile.absolutePath}")
Log.i(TAG, "Exporting results to ${tempZipFile.absolutePath}")
try {
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
ZipOutputStream(FileOutputStream(tempZipFile)).use { zos ->
// 1. Add results.json
val resultsJson = File(outputDir, "results.json")
if (resultsJson.exists()) {
@@ -108,24 +110,28 @@ class ValidationResultManager(
zos.closeEntry()
}
// 2. Add source zip if exists
// 2. Add test bundle zip (fallback to sourceZip)
if (sourceZip != null && sourceZip.exists()) {
zos.putNextEntry(ZipEntry(sourceZip.name))
zos.putNextEntry(ZipEntry("bundle.zip"))
sourceZip.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
// 3. Add images (only rendered images, exclude diffs)
// 3. Add images (exclude diffs, frontend computes them)
outputDir.listFiles { _, name -> name.endsWith(".png") && !name.endsWith("_diff.png") }?.forEach { imgFile ->
zos.putNextEntry(ZipEntry(imgFile.name))
imgFile.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
Log.i(TAG, "Exported results to ${zipFile.absolutePath}")
return zipFile
tempZipFile.renameTo(finalZipFile)
Log.i(TAG, "Exported results to ${finalZipFile.absolutePath}")
return finalZipFile
} catch (e: Exception) {
Log.e(TAG, "Failed to export results", e)
tempZipFile.delete()
return null
}
}
@@ -144,7 +150,7 @@ class ValidationResultManager(
* goldens/
* test_result.png
*/
fun exportTestBundle(config: RenderTestConfig, timestamp: String): File? {
fun exportTestBundle(config: RenderTestConfig, timestamp: String, bundleNameOverride: String? = null): File? {
Log.i(TAG, "Starting exportTestBundle for ${config.name} at $timestamp")
Log.i(TAG, "OutputDir: ${outputDir.absolutePath}")
@@ -155,8 +161,7 @@ class ValidationResultManager(
}
Log.i(TAG, "Using parentDir for export: ${parentDir.absolutePath}")
val testNameWithTimestamp = "${config.name}_$timestamp"
val exportNameNoSpaces = testNameWithTimestamp.replace(" ", "_")
val exportNameNoSpaces = bundleNameOverride ?: config.name.lowercase(java.util.Locale.US).replace(" ", "_")
val exportDir = File(parentDir, "export_temp_$timestamp")
@@ -204,7 +209,7 @@ class ValidationResultManager(
// 3. Create modified config JSON
Log.i(TAG, "Creating config.json...")
val newConfigJson = JSONObject()
newConfigJson.put("name", testNameWithTimestamp) // Keep spaces in JSON name
newConfigJson.put("name", config.name) // Keep spaces in JSON name
// Reconstruct backends
val backendsArray = JSONArray()
@@ -251,10 +256,11 @@ class ValidationResultManager(
File(rootDir, "config.json").writeText(newConfigJson.toString(4))
// 4. Zip it
val zipFile = File(parentDir, "$exportNameNoSpaces.zip")
Log.i(TAG, "Zipping to ${zipFile.absolutePath}...")
val tempZipFile = File(parentDir, "$exportNameNoSpaces.zip.tmp")
val finalZipFile = File(parentDir, "$exportNameNoSpaces.zip")
Log.i(TAG, "Zipping to ${tempZipFile.absolutePath}...")
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
ZipOutputStream(FileOutputStream(tempZipFile)).use { zos ->
rootDir.walkTopDown().forEach { file ->
if (file.isFile) {
val entryName = file.relativeTo(exportDir).path
@@ -265,15 +271,18 @@ class ValidationResultManager(
}
}
tempZipFile.renameTo(finalZipFile)
// Cleanup temp dir
exportDir.deleteRecursively()
Log.i(TAG, "Exported test bundle to ${zipFile.absolutePath}")
return zipFile
Log.i(TAG, "Exported test bundle to ${finalZipFile.absolutePath}")
return finalZipFile
} catch (e: Exception) {
Log.e(TAG, "Failed to export test bundle", e)
exportDir.deleteRecursively()
File(parentDir, "$exportNameNoSpaces.zip.tmp").delete()
return null
}
}
@@ -285,6 +294,7 @@ class ValidationResultManager(
metadataObject.put("gpu_driver_info", gpuDriverInfos)
metadataObject.put("total_time_ms", totalTimeMs)
metadataObject.put("device_name", deviceName ?: "")
metadataObject.put("device_hardware", deviceHardware ?: "")
metadataObject.put("device_code_name", deviceCodeName ?: "")
metadataObject.put("android_version", androidVersion ?: "")
metadataObject.put("android_build_number", androidBuildNumber ?: "")
@@ -65,7 +65,22 @@ class ValidationRunner(
// Sort by backend then model. This tries to minimize the number of times we need to
// recreate the Engine, and then the ModelViewer.
expanded.sortedWith(compareBy({ it.first.backend }, { it.second }))
val sorted = expanded.sortedWith(compareBy({ it.first.backend }, { it.second }))
val filter = testFilter
if (filter != null && filter.count { it == '.' } == 2) {
val parts = filter.split(".")
val nameRegex = ("^" + parts[0].replace("*", ".*") + "$").toRegex()
val backendRegex = ("^" + parts[1].replace("*", ".*") + "$").toRegex()
val modelRegex = ("^" + parts[2].replace("*", ".*") + "$").toRegex()
sorted.filter { (test, model) ->
val testBackend = test.backend ?: "opengl"
test.name.matches(nameRegex) && testBackend.matches(backendRegex) && model.matches(modelRegex)
}
} else {
sorted
}
}
enum class State {
@@ -86,6 +101,7 @@ class ValidationRunner(
var callback: Callback? = null
var generateGoldens: Boolean = false
var testFilter: String? = null
fun start() {
if (sortedTests.isEmpty()) {
@@ -215,11 +231,12 @@ class ValidationRunner(
content.renderer = mv.renderer
content.scene = mv.scene
content.lightManager = mv.engine.lightManager
// Tick
val deltaTime = 1.0f / 60.0f
engine.tick(mv.engine, content, deltaTime)
applyAnimation(mv, currentTestConfig!!.rendering)
frameCounter++
if (engine.shouldClose()) {
Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)")
@@ -233,6 +250,30 @@ class ValidationRunner(
}
}
private fun applyAnimation(mv: com.google.android.filament.utils.ModelViewer, rendering: JSONObject) {
val animator = mv.animator ?: return
if (animator.animationCount == 0) return
var enabled = false
var time = 0.0f
if (rendering.has("animation")) {
val anim = rendering.optJSONObject("animation")
if (anim != null) {
enabled = anim.optBoolean("enabled", false)
time = anim.optDouble("time", 0.0).toFloat()
}
} else {
enabled = rendering.optBoolean("animation.enabled", false)
time = rendering.optDouble("animation.time", 0.0).toFloat()
}
if (enabled) {
animator.applyAnimation(0, time)
animator.updateBoneMatrices()
}
}
private fun startAutomation() {
val test = currentTestConfig!!
val specJson = JSONObject()
@@ -243,13 +284,19 @@ class ValidationRunner(
currentEngine = AutomationEngine(fullSpec)
val options = AutomationEngine.Options()
options.sleepDuration = 0.0f // Minimal sleep, let frames drive it
options.minFrameCount = 1 // Ensure some frames pass
options.minFrameCount = 15 // Ensure enough frames pass for temporal accumulation
currentEngine?.setOptions(options)
// Use batch mode to ensure shouldClose() works reliably
currentEngine?.startBatchMode()
currentEngine?.signalBatchMode() // Start immediately
// Clear the frame history so that temporal effects (like SSR or TAA) don't bleed over
// or ghost from the visual output of the previous test.
modelViewer?.let { mv ->
mv.view.clearFrameHistory(mv.engine)
}
frameCounter = 0
currentState = State.RUNNING_TEST
}
@@ -297,8 +344,6 @@ class ValidationRunner(
Log.i("ValidationRunner", "Found golden at ${goldenFile.absolutePath}")
} else {
Log.w("ValidationRunner", "Golden not found for $testFullName. Searched in: ${searchPaths.joinToString { it.absolutePath }}")
// Fallback to old behavior for reference if everything else fails
goldenFile = modelParent.parentFile?.resolve("golden/${testFullName}.png") ?: File("nonexistent")
}
Thread {
@@ -28,6 +28,7 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:clipChildren="false"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingTop="20dp"
@@ -162,24 +163,57 @@
android:id="@+id/test_results_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="Test Results"
android:textSize="14sp"
android:paddingTop="0dp"
android:paddingBottom="12dp" />
<ProgressBar
android:id="@+id/test_progress"
style="?android:attr/progressBarStyleHorizontal"
android:gravity="center"
<FrameLayout
android:id="@+id/progress_container"
android:layout_width="200dp"
android:layout_height="4dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:clipChildren="false"
android:visibility="gone">
<com.google.android.filament.validation.TestProgressBar
android:id="@+id/test_progress"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_marginBottom="8dp" />
<ImageView
android:id="@+id/progress_triangle"
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_gravity="bottom"
android:layout_marginTop="4dp"
android:scaleType="fitXY"
android:src="@drawable/ic_triangle_up" />
</FrameLayout>
<TextView
android:id="@+id/test_summary_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:layout_marginBottom="12dp"
android:visibility="gone" />
<TextView
android:id="@+id/device_info_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:layout_marginBottom="12dp"
android:visibility="gone" />
</LinearLayout>
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/controls_container"
+12 -13
View File
@@ -204,10 +204,9 @@
"license": "Apache-2.0"
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -235,9 +234,9 @@
"license": "MIT"
},
"node_modules/minimatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -410,9 +409,9 @@
"integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="
},
"brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -429,9 +428,9 @@
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="
},
"minimatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"requires": {
"brace-expansion": "^1.1.7"
}
+1 -1
View File
@@ -163,7 +163,7 @@
<p>To build Filament, you must first install the following tools:</p>
<ul>
<li>CMake 3.22.1 (or more recent)</li>
<li>clang 16.0 (or more recent)</li>
<li>clang 16.0 (or more recent) (Required for Linux and macOS; see <a href="#windows">Windows</a> section for MSVC support)</li>
<li><a href="https://github.com/ninja-build/ninja/wiki/Pre-built-Ninja-packages">ninja 1.10</a> (or more recent)</li>
</ul>
<p>Additional dependencies may be required for your operating system. Please refer to the appropriate
+2 -2
View File
@@ -181,7 +181,7 @@ important for <code>matc</code> (material compiler).</p>
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.70.1'
implementation 'com.google.android.filament:filament-android:1.71.0'
}
</code></pre>
<p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p>
@@ -195,7 +195,7 @@ dependencies {
</div>
<h3 id="ios"><a class="header" href="#ios">iOS</a></h3>
<p>iOS projects can use CocoaPods to install the latest release:</p>
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.70.1'
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.71.0'
</code></pre>
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
<ul>
+2 -2
View File
@@ -174,7 +174,7 @@
<script src="../../web/lib/filament.js"></script>
<script src="../../web/lib/gl-matrix-min.js"></script>
<script>
Filament.init(['../../web/assets/cube_fl0/nonlit_fl0.filamat'], () => {
Filament.init(['../../web/assets/cube_fl0/nonlit.filamat'], () => {
window.VertexAttribute = Filament.VertexAttribute;
window.AttributeType = Filament.VertexBuffer$AttributeType;
window.Projection = Filament.Camera$Projection;
@@ -257,7 +257,7 @@
.bufferType(Filament.IndexBuffer$IndexType.USHORT)
.build(engine);
this.ib.setBuffer(engine, CUBE_INDICES);
const mat = engine.createMaterial('../../web/assets/cube_fl0/nonlit_fl0.filamat');
const mat = engine.createMaterial('../../web/assets/cube_fl0/nonlit.filamat');
const matinst = mat.getDefaultInstance();
Filament.RenderableManager.Builder(1)
.boundingBox({ center: [-1, -1, -1], halfExtent: [1, 1, 1] })
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
+51 -43
View File
@@ -313,17 +313,18 @@ if (NOT FILAMENT_DISABLE_GTAO)
)
endif()
set(MATERIAL_FL0_SRCS
src/materials/defaultMaterial.mat
src/materials/skybox.mat
)
set(MATERIAL_MULTIVIEW_SRCS
src/materials/clearDepth.mat
src/materials/defaultMaterial.mat
src/materials/skybox.mat
)
# This set of materials should only be compiled for vulkan or metal because they contain operations
# not allowed on other platforms. (e.g. sampling from a multi-sampled texture).
set(MATERIAL_VULKAN_METAL_SRCS
src/materials/resolveDepth.mat
)
# ==================================================================================================
# Configuration
# ==================================================================================================
@@ -343,11 +344,6 @@ if (NOT DFG_LUT_SIZE)
endif()
message(STATUS "DFG LUT size set to ${DFG_LUT_SIZE}x${DFG_LUT_SIZE}")
# Whether to include FL0 materials.
if (FILAMENT_ENABLE_FEATURE_LEVEL_0)
add_definitions(-DFILAMENT_ENABLE_FEATURE_LEVEL_0)
endif()
# Whether to include MULTIVIEW materials.
if (FILAMENT_ENABLE_MULTIVIEW)
add_definitions(-DFILAMENT_ENABLE_MULTIVIEW)
@@ -397,6 +393,27 @@ foreach(mat_src ${MATERIAL_SRCS})
endforeach()
list(REMOVE_DUPLICATES MATERIAL_DIRS)
function(check_in_list list_name item out_var)
list(FIND ${list_name} "${item}" index)
if (index GREATER -1)
set(${out_var} TRUE PARENT_SCOPE)
else()
set(${out_var} FALSE PARENT_SCOPE)
endif()
endfunction()
macro(compile_material output_path fullname flags_var comment_str)
add_custom_command(
OUTPUT ${output_path}
COMMAND matc ${${flags_var}} -o ${output_path} ${fullname}
MAIN_DEPENDENCY ${fullname}
DEPENDS matc
COMMENT "${comment_str}"
)
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path})
endmacro()
# For each directory, generate a single resource package
foreach(mat_dir ${MATERIAL_DIRS})
get_filename_component(package_name ${mat_dir} NAME)
@@ -419,44 +436,35 @@ foreach(mat_dir ${MATERIAL_DIRS})
# --- Base material ---
set(output_path "${MATERIAL_DIR}/${localname}.filamat")
add_custom_command(
OUTPUT ${output_path}
COMMAND matc ${MATC_BASE_FLAGS} -o ${output_path} ${fullname}
MAIN_DEPENDENCY ${fullname}
DEPENDS matc
COMMENT "Compiling material ${fullname}"
)
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path})
set(MATC_CUSTOM_FLAGS "${MATC_BASE_FLAGS}")
set(COMMENT_STR "Compiling material ${fullname}")
# --- Feature Level 0 variant ---
list(FIND MATERIAL_FL0_SRCS ${mat_src} index)
if (${index} GREATER -1 AND FILAMENT_ENABLE_FEATURE_LEVEL_0)
string(REGEX REPLACE "[.]filamat$" "_fl0.filamat" output_path_fl0 ${output_path})
add_custom_command(
OUTPUT ${output_path_fl0}
COMMAND matc ${MATC_BASE_FLAGS} -PfeatureLevel=0 -o ${output_path_fl0} ${fullname}
MAIN_DEPENDENCY ${fullname}
DEPENDS matc
COMMENT "Compiling material ${fullname} (FL0)"
)
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path_fl0})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path_fl0})
check_in_list(MATERIAL_VULKAN_METAL_SRCS ${mat_src} is_vk_metal)
if (is_vk_metal)
# Remove webgpu and related parameters
# The variant-filter=stereo is added in the root folder's CMakeLists.txt for webgpu
# Context is that webgpu does not support the stereo variant
string(REPLACE "-a;webgpu;--variant-filter=stereo" "" MATC_CUSTOM_FLAGS "${MATC_CUSTOM_FLAGS}")
# Remove gl
string(REPLACE "-a;opengl" "" MATC_CUSTOM_FLAGS "${MATC_CUSTOM_FLAGS}")
# Remove any empty args
string(REPLACE ";;" ";" MATC_CUSTOM_FLAGS "${MATC_CUSTOM_FLAGS}")
string(REGEX REPLACE "^;|;$" "" MATC_CUSTOM_FLAGS "${MATC_CUSTOM_FLAGS}")
set(COMMENT_STR "Compiling material ${fullname} (Vulkan/Metal)")
endif()
compile_material(${output_path} ${fullname} MATC_CUSTOM_FLAGS "${COMMENT_STR}")
# --- Multiview variant ---
list(FIND MATERIAL_MULTIVIEW_SRCS ${mat_src} index)
if (${index} GREATER -1 AND FILAMENT_ENABLE_MULTIVIEW)
check_in_list(MATERIAL_MULTIVIEW_SRCS ${mat_src} is_multiview)
if (is_multiview AND FILAMENT_ENABLE_MULTIVIEW)
string(REGEX REPLACE "[.]filamat$" "_multiview.filamat" output_path_multiview ${output_path})
add_custom_command(
OUTPUT ${output_path_multiview}
COMMAND matc ${MATC_BASE_FLAGS} -PstereoscopicType=multiview -o ${output_path_multiview} ${fullname}
MAIN_DEPENDENCY ${fullname}
DEPENDS matc
COMMENT "Compiling material ${fullname} (Multiview)"
)
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path_multiview})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path_multiview})
set(MATC_MULTIVIEW_FLAGS "${MATC_BASE_FLAGS}" "-PstereoscopicType=multiview")
set(COMMENT_STR "Compiling material ${fullname} (Multiview)")
compile_material(${output_path_multiview} ${fullname} MATC_MULTIVIEW_FLAGS "${COMMENT_STR}")
endif()
endforeach()
+22
View File
@@ -214,6 +214,25 @@ public:
void scheduleCallback(CallbackHandler* handler, void* user, CallbackHandler::Callback callback) final;
template<typename Predicate>
bool waitForFence(Predicate predicate, std::chrono::steady_clock::time_point until) {
std::unique_lock lock(mFenceMutex);
return mFenceCondition.wait_until(lock, until, predicate);
}
template<typename Predicate>
void waitForFence(Predicate predicate) {
std::unique_lock lock(mFenceMutex);
mFenceCondition.wait(lock, predicate);
}
template<typename Action>
void signalFence(Action action) {
std::lock_guard lock(mFenceMutex);
action();
mFenceCondition.notify_all();
}
// --------------------------------------------------------------------------------------------
// Privates
// --------------------------------------------------------------------------------------------
@@ -253,6 +272,9 @@ private:
std::condition_variable mServiceThreadCondition;
std::vector<std::tuple<CallbackHandler*, CallbackHandler::Callback, void*>> mServiceThreadCallbackQueue;
bool mExitRequested = false;
std::condition_variable mFenceCondition;
std::mutex mFenceMutex;
};
+5 -9
View File
@@ -114,9 +114,7 @@ void logMTLCommandBufferError(MTLCommandBufferError error) {
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorOutOfMemory)
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorInvalidResource)
if (@available(macOS 11.0, *)) {
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorMemoryless)
}
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorMemoryless)
if (@available(iOS 15.0, macOS 12.0, *)) {
MTL_COMMAND_ERROR_CASE(MTLCommandBufferErrorStackOverflow)
@@ -143,12 +141,10 @@ id<MTLCommandBuffer> getPendingCommandBuffer(MetalContext* context) {
context->latestCompletedCommandBufferId = thisCommandBufferId;
auto errorCode = (MTLCommandBufferError)buffer.error.code;
if (@available(macOS 11.0, *)) {
if (errorCode == MTLCommandBufferErrorMemoryless) {
LOG(WARNING) << "Metal: memoryless geometry limit reached. Continuing with private "
"storage mode.";
context->memorylessLimitsReached = true;
}
if (errorCode == MTLCommandBufferErrorMemoryless) {
LOG(WARNING) << "Metal: memoryless geometry limit reached. Continuing with private "
"storage mode.";
context->memorylessLimitsReached = true;
}
if (UTILS_UNLIKELY(errorCode != MTLCommandBufferErrorNone)) {
+10 -11
View File
@@ -1284,11 +1284,8 @@ bool MetalDriver::isFrameBufferFetchSupported() {
// FrameBuffer fetch is achievable via "programmable blending" in Metal, and only supported on
// Apple GPUs with readWriteTextureSupport.
// On macOS, framebuffer fetch requires MSL 2.3, which is only available with macOS 11.0.
if (@available(macOS 11.0, *)) {
return mContext->highestSupportedGpuFamily.apple >= 1 &&
mContext->device.readWriteTextureSupport;
}
return false;
return mContext->highestSupportedGpuFamily.apple >= 1 &&
mContext->device.readWriteTextureSupport;
}
bool MetalDriver::isFrameBufferFetchMultiSampleSupported() {
@@ -2298,12 +2295,14 @@ void MetalDriver::draw2(uint32_t indexOffset, uint32_t indexCount, uint32_t inst
// Bind the offset data.
if (mContext->dynamicOffsets.isDirty()) {
const auto [size, data] = mContext->dynamicOffsets.getOffsets();
[mContext->currentRenderPassEncoder setFragmentBytes:data
length:size * sizeof(uint32_t)
atIndex:DYNAMIC_OFFSET_BINDING];
[mContext->currentRenderPassEncoder setVertexBytes:data
length:size * sizeof(uint32_t)
atIndex:DYNAMIC_OFFSET_BINDING];
if (size > 0) {
[mContext->currentRenderPassEncoder setFragmentBytes:data
length:size * sizeof(uint32_t)
atIndex:DYNAMIC_OFFSET_BINDING];
[mContext->currentRenderPassEncoder setVertexBytes:data
length:size * sizeof(uint32_t)
atIndex:DYNAMIC_OFFSET_BINDING];
}
mContext->dynamicOffsets.setDirty(false);
}
+26 -30
View File
@@ -148,9 +148,7 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
inline MTLPixelFormat getMetalFormat(PixelDataFormat format, PixelDataType type) {
if (type == PixelDataType::UINT_2_10_10_10_REV) return MTLPixelFormatRGB10A2Unorm;
if (type == PixelDataType::UINT_10F_11F_11F_REV) return MTLPixelFormatRG11B10Float;
if (@available(macOS 11, *)) {
if (type == PixelDataType::USHORT_565) return MTLPixelFormatB5G6R5Unorm;
}
if (type == PixelDataType::USHORT_565) return MTLPixelFormatB5G6R5Unorm;
#define CONVERT(FORMAT, TYPE, MTL) \
if (PixelDataFormat::FORMAT == format && PixelDataType::TYPE == type) return MTLPixelFormat ## MTL;
@@ -205,33 +203,31 @@ inline MTLPixelFormat getMetalFormatLinear(MTLPixelFormat format) {
#endif
default: break;
}
if (@available(macOS 11, *)) {
switch (format) {
case MTLPixelFormatASTC_4x4_sRGB: return MTLPixelFormatASTC_4x4_LDR;
case MTLPixelFormatASTC_5x4_sRGB: return MTLPixelFormatASTC_5x4_LDR;
case MTLPixelFormatASTC_5x5_sRGB: return MTLPixelFormatASTC_5x5_LDR;
case MTLPixelFormatASTC_6x5_sRGB: return MTLPixelFormatASTC_6x5_LDR;
case MTLPixelFormatASTC_6x6_sRGB: return MTLPixelFormatASTC_6x6_LDR;
case MTLPixelFormatASTC_8x5_sRGB: return MTLPixelFormatASTC_8x5_LDR;
case MTLPixelFormatASTC_8x6_sRGB: return MTLPixelFormatASTC_8x6_LDR;
case MTLPixelFormatASTC_8x8_sRGB: return MTLPixelFormatASTC_8x8_LDR;
case MTLPixelFormatASTC_10x5_sRGB: return MTLPixelFormatASTC_10x5_LDR;
case MTLPixelFormatASTC_10x6_sRGB: return MTLPixelFormatASTC_10x6_LDR;
case MTLPixelFormatASTC_10x8_sRGB: return MTLPixelFormatASTC_10x8_LDR;
case MTLPixelFormatASTC_10x10_sRGB: return MTLPixelFormatASTC_10x10_LDR;
case MTLPixelFormatASTC_12x10_sRGB: return MTLPixelFormatASTC_12x10_LDR;
case MTLPixelFormatASTC_12x12_sRGB: return MTLPixelFormatASTC_12x12_LDR;
case MTLPixelFormatPVRTC_RGB_2BPP_sRGB: return MTLPixelFormatPVRTC_RGB_2BPP;
case MTLPixelFormatPVRTC_RGB_4BPP_sRGB: return MTLPixelFormatPVRTC_RGB_4BPP;
case MTLPixelFormatPVRTC_RGBA_2BPP_sRGB: return MTLPixelFormatPVRTC_RGBA_2BPP;
case MTLPixelFormatPVRTC_RGBA_4BPP_sRGB: return MTLPixelFormatPVRTC_RGBA_4BPP;
case MTLPixelFormatEAC_RGBA8_sRGB: return MTLPixelFormatEAC_RGBA8;
case MTLPixelFormatETC2_RGB8_sRGB: return MTLPixelFormatETC2_RGB8;
case MTLPixelFormatETC2_RGB8A1_sRGB: return MTLPixelFormatETC2_RGB8A1;
case MTLPixelFormatBGR10_XR_sRGB: return MTLPixelFormatBGR10_XR;
case MTLPixelFormatBGRA10_XR_sRGB: return MTLPixelFormatBGRA10_XR;
default: break;
}
switch (format) {
case MTLPixelFormatASTC_4x4_sRGB: return MTLPixelFormatASTC_4x4_LDR;
case MTLPixelFormatASTC_5x4_sRGB: return MTLPixelFormatASTC_5x4_LDR;
case MTLPixelFormatASTC_5x5_sRGB: return MTLPixelFormatASTC_5x5_LDR;
case MTLPixelFormatASTC_6x5_sRGB: return MTLPixelFormatASTC_6x5_LDR;
case MTLPixelFormatASTC_6x6_sRGB: return MTLPixelFormatASTC_6x6_LDR;
case MTLPixelFormatASTC_8x5_sRGB: return MTLPixelFormatASTC_8x5_LDR;
case MTLPixelFormatASTC_8x6_sRGB: return MTLPixelFormatASTC_8x6_LDR;
case MTLPixelFormatASTC_8x8_sRGB: return MTLPixelFormatASTC_8x8_LDR;
case MTLPixelFormatASTC_10x5_sRGB: return MTLPixelFormatASTC_10x5_LDR;
case MTLPixelFormatASTC_10x6_sRGB: return MTLPixelFormatASTC_10x6_LDR;
case MTLPixelFormatASTC_10x8_sRGB: return MTLPixelFormatASTC_10x8_LDR;
case MTLPixelFormatASTC_10x10_sRGB: return MTLPixelFormatASTC_10x10_LDR;
case MTLPixelFormatASTC_12x10_sRGB: return MTLPixelFormatASTC_12x10_LDR;
case MTLPixelFormatASTC_12x12_sRGB: return MTLPixelFormatASTC_12x12_LDR;
case MTLPixelFormatPVRTC_RGB_2BPP_sRGB: return MTLPixelFormatPVRTC_RGB_2BPP;
case MTLPixelFormatPVRTC_RGB_4BPP_sRGB: return MTLPixelFormatPVRTC_RGB_4BPP;
case MTLPixelFormatPVRTC_RGBA_2BPP_sRGB: return MTLPixelFormatPVRTC_RGBA_2BPP;
case MTLPixelFormatPVRTC_RGBA_4BPP_sRGB: return MTLPixelFormatPVRTC_RGBA_4BPP;
case MTLPixelFormatEAC_RGBA8_sRGB: return MTLPixelFormatEAC_RGBA8;
case MTLPixelFormatETC2_RGB8_sRGB: return MTLPixelFormatETC2_RGB8;
case MTLPixelFormatETC2_RGB8A1_sRGB: return MTLPixelFormatETC2_RGB8A1;
case MTLPixelFormatBGR10_XR_sRGB: return MTLPixelFormatBGR10_XR;
case MTLPixelFormatBGRA10_XR_sRGB: return MTLPixelFormatBGRA10_XR;
default: break;
}
return format;
}
+2 -8
View File
@@ -98,14 +98,12 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
// Packed 16 bit formats are only available on Apple GPUs.
if (context->highestSupportedGpuFamily.apple >= 1) {
if (@available(macOS 11.0, *)) {
switch (format) {
case TextureFormat::RGB565: return MTLPixelFormatB5G6R5Unorm;
case TextureFormat::RGB5_A1: return MTLPixelFormatA1BGR5Unorm;
case TextureFormat::RGBA4: return MTLPixelFormatABGR4Unorm;
default: break;
}
}
}
if (@available(iOS 13.0, *)) {
@@ -115,18 +113,16 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
#if TARGET_OS_OSX
if (context->highestSupportedGpuFamily.mac >= 1 &&
context->device.depth24Stencil8PixelFormatSupported) {
if (@available(macOS 11.0, *)) {
if (format == TextureFormat::DEPTH24_STENCIL8) {
return MTLPixelFormatDepth24Unorm_Stencil8;
}
}
}
#endif
// Only iOS 13.0 and Apple Silicon support the ASTC HDR profile. Older OS versions fallback to
// LDR. The HDR profile is a superset of the LDR profile.
if (context->highestSupportedGpuFamily.apple >= 2) {
if (@available(iOS 13, macOS 11.0, *)) {
if (@available(iOS 13, *)) {
switch (format) {
case TextureFormat::RGBA_ASTC_4x4: return MTLPixelFormatASTC_4x4_HDR;
case TextureFormat::RGBA_ASTC_5x4: return MTLPixelFormatASTC_5x4_HDR;
@@ -144,7 +140,7 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
case TextureFormat::RGBA_ASTC_12x12: return MTLPixelFormatASTC_12x12_HDR;
default: break;
}
} else if (@available(macOS 11.0, *)) {
} else {
switch (format) {
case TextureFormat::RGBA_ASTC_4x4: return MTLPixelFormatASTC_4x4_LDR;
case TextureFormat::RGBA_ASTC_5x4: return MTLPixelFormatASTC_5x4_LDR;
@@ -167,7 +163,6 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
// EAC / ETC2 formats are only available on Apple GPUs.
if (context->highestSupportedGpuFamily.apple >= 1) {
if (@available(macOS 11.0, *)) {
switch (format) {
case TextureFormat::EAC_R11: return MTLPixelFormatEAC_R11Unorm;
case TextureFormat::EAC_R11_SIGNED: return MTLPixelFormatEAC_R11Snorm;
@@ -181,7 +176,6 @@ MTLPixelFormat getMetalFormat(MetalContext* context, TextureFormat format) noexc
case TextureFormat::ETC2_EAC_SRGBA8: return MTLPixelFormatEAC_RGBA8_sRGB;
default: break;
}
}
}
// DXT (BC) formats are only available on macOS desktop.
@@ -478,8 +478,6 @@ private:
struct State {
FenceStatus status { FenceStatus::TIMEOUT_EXPIRED };
std::condition_variable cv;
std::mutex mutex;
};
std::shared_ptr<State> state { std::make_shared<State>() };
+23 -20
View File
@@ -20,6 +20,7 @@
#include "MetalEnums.h"
#include "MetalUtils.h"
#include "MetalBufferPool.h"
#include "MetalDriver.h"
#include <filament/SwapChain.h>
@@ -183,9 +184,7 @@ id<MTLTexture> MetalSwapChain::createMultisampledTexture(MetalContext const& con
descriptor.resourceOptions = MTLResourceStorageModePrivate;
if (context.supportsMemorylessRenderTargets) {
if (@available(macOS 11.0, *)) {
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
}
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
}
return [context.device newTextureWithDescriptor:descriptor];
@@ -1375,9 +1374,7 @@ id<MTLTexture> MetalRenderTarget::createMultisampledTexture(MTLPixelFormat forma
descriptor.resourceOptions = MTLResourceStorageModePrivate;
if (context->supportsMemorylessRenderTargets) {
if (@available(macOS 11.0, *)) {
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
}
descriptor.resourceOptions = MTLResourceStorageModeMemoryless;
}
return [context->device newTextureWithDescriptor:descriptor];
@@ -1404,12 +1401,13 @@ void MetalFence::encode() {
// Using a weak_ptr here because the Fence could be deleted before the block executes.
std::weak_ptr<State> weakState = state;
MetalDriver* driver = context.driver;
[event notifyListener:context.eventListener atValue:value block:^(id <MTLSharedEvent> o,
uint64_t value) {
if (auto s = weakState.lock()) {
std::lock_guard<std::mutex> guard(s->mutex);
s->status = FenceStatus::CONDITION_SATISFIED;
s->cv.notify_all();
driver->signalFence([&] {
s->status = FenceStatus::CONDITION_SATISFIED;
});
}
}];
}
@@ -1422,24 +1420,29 @@ void MetalFence::onSignal(MetalFenceSignalBlock block) {
FenceStatus MetalFence::wait(uint64_t timeoutNs) {
if (@available(iOS 12, *)) {
using ns = std::chrono::nanoseconds;
std::unique_lock<std::mutex> guard(state->mutex);
while (state->status == FenceStatus::TIMEOUT_EXPIRED) {
if (timeoutNs == FENCE_WAIT_FOR_EVER) {
state->cv.wait(guard);
} else if (timeoutNs == 0 ||
state->cv.wait_for(guard, ns(timeoutNs)) == std::cv_status::timeout) {
return state->status;
FenceStatus result = FenceStatus::TIMEOUT_EXPIRED;
auto predicate = [&] {
if (state->status != FenceStatus::TIMEOUT_EXPIRED) {
result = state->status;
return true;
}
return false;
};
if (timeoutNs == FENCE_WAIT_FOR_EVER) {
context.driver->waitForFence(predicate);
} else {
auto const until = std::chrono::steady_clock::now() + ns(timeoutNs);
context.driver->waitForFence(predicate, until);
}
return state->status;
return result;
}
return FenceStatus::ERROR;
}
void MetalFence::cancel() {
std::unique_lock guard(state->mutex);
state->status = FenceStatus::ERROR;
state->cv.notify_all();
context.driver->signalFence([&] {
state->status = FenceStatus::ERROR;
});
}
MetalDescriptorSetLayout::MetalDescriptorSetLayout(DescriptorSetLayout&& l) noexcept
@@ -185,11 +185,13 @@ void GLDescriptorSet::update(OpenGLState& gl, HandleAllocatorGL& handleAllocator
}
// GLES3.x specification forbids depth textures to be filtered.
bool const depthTextureNoPcf = isDepthFormat(t->format) &&
bool const isDepthFmt = t && isDepthFormat(t->format);
bool const depthTextureNoPcf = isDepthFmt &&
params.compareMode == SamplerCompareMode::NONE;
// GLES3.x may not support filtering of fp32 textures
bool const fp32TextureNotFilterable = isFp32ColorFormat(t->format) &&
bool const isFp32Fmt = t && isFp32ColorFormat(t->format);
bool const fp32TextureNotFilterable = isFp32Fmt &&
!gl.context().ext.OES_texture_float_linear;
if (t && (depthTextureNoPcf || fp32TextureNotFilterable)) {
+58 -47
View File
@@ -1740,6 +1740,10 @@ void OpenGLDriver::framebufferTexture(TargetBufferInfo const& binfo,
assert_invariant(target == GL_TEXTURE_2D || target == GL_TEXTURE_EXTERNAL_OES);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment,
GL_RENDERBUFFER, t->gl.id);
// Clear the resolve bit for this particular attachment. Note that other attachment(s)
// might be sampleable, so this does not necessarily prevent the resolve from occurring.
resolveFlags = TargetBufferFlags::NONE;
}
break;
case GL_TEXTURE_3D:
@@ -1879,6 +1883,11 @@ void OpenGLDriver::framebufferTexture(TargetBufferInfo const& binfo,
CHECK_GL_ERROR()
}
// We don't need to resolve attachments that match the rendertarget's sample count.
if (rt->gl.samples == t->samples) {
resolveFlags = TargetBufferFlags::NONE;
}
rt->gl.resolve |= resolveFlags;
CHECK_GL_ERROR()
@@ -2065,9 +2074,9 @@ void OpenGLDriver::createFenceR(Handle<HwFence> fh, ImmutableCString&& tag) {
assert_invariant(f->state);
if (mPlatform.canCreateFence()) {
std::lock_guard const lock(f->state->lock);
f->fence = mPlatform.createFence();
f->state->cond.notify_all();
signalFence([&] {
f->fence = mPlatform.createFence();
});
return;
}
@@ -2081,11 +2090,11 @@ void OpenGLDriver::createFenceR(Handle<HwFence> fh, ImmutableCString&& tag) {
// This is the case where we need to use OpenGL fences, as soon as we return, the user
// is allowed to destroy the fence, so we need to keep a reference to the internal state.
std::weak_ptr<GLFence::State> const weak = f->state;
whenGpuCommandsComplete([weak] {
whenGpuCommandsComplete([weak, this] {
if (auto const state = weak.lock()) {
std::lock_guard const lock(state->lock);
state->status = FenceStatus::CONDITION_SATISFIED;
state->cond.notify_all();
signalFence([&] {
state->status = FenceStatus::CONDITION_SATISFIED;
});
}
});
#else
@@ -2624,9 +2633,9 @@ void OpenGLDriver::fenceCancel(Handle<HwFence> fh) {
GLFence const* const f = handle_cast<GLFence*>(fh);
assert_invariant(f->state);
std::lock_guard const lock(f->state->lock);
f->state->status = FenceStatus::ERROR;
f->state->cond.notify_all();
signalFence([&] {
f->state->status = FenceStatus::ERROR;
});
}
FenceStatus OpenGLDriver::getFenceStatus(Handle<HwFence> fh) {
@@ -2652,22 +2661,18 @@ FenceStatus OpenGLDriver::fenceWait(FenceHandle fh, uint64_t const timeout) {
// `f` is not supposed to become invalid while we wait.
if (mPlatform.canCreateFence()) {
std::unique_lock lock(f->state->lock);
// we've been called before the fence was created asynchronously,
// so we need to wait for that, before using the real fence.
// By construction, "f" can't be destroyed while we wait, because its
// construction call is in the queue and a destroy call will have to come later.
waitForFence([f] {
return f->fence != nullptr;
}, until);
if (f->fence == nullptr) {
// we've been called before the fence was created asynchronously,
// so we need to wait for that, before using the real fence.
// By construction, "f" can't be destroyed while we wait, because its
// construction call is in the queue and a destroy call will have to come later.
f->state->cond.wait_until(lock, until, [f] {
return f->fence != nullptr;
});
if (f->fence == nullptr) {
// the only possible choice here is that we timed out
assert_invariant(f->state->status == FenceStatus::TIMEOUT_EXPIRED);
return FenceStatus::TIMEOUT_EXPIRED;
}
// the only possible choice here is that we timed out
assert_invariant(f->state->status == FenceStatus::TIMEOUT_EXPIRED);
return FenceStatus::TIMEOUT_EXPIRED;
}
lock.unlock();
// here we know that we have the platform fence
assert_invariant(f->fence);
return mPlatform.waitFence(f->fence, timeout);
@@ -2680,11 +2685,15 @@ FenceStatus OpenGLDriver::fenceWait(FenceHandle fh, uint64_t const timeout) {
// This is the case where we need to use OpenGL fences
#ifndef FILAMENT_SILENCE_NOT_SUPPORTED_BY_ES2
std::unique_lock lock(f->state->lock);
f->state->cond.wait_until(lock, until, [f] {
return f->state->status != FenceStatus::TIMEOUT_EXPIRED;
});
return f->state->status;
FenceStatus result = FenceStatus::TIMEOUT_EXPIRED;
waitForFence([&] {
if (f->state->status != FenceStatus::TIMEOUT_EXPIRED) {
result = f->state->status;
return true;
}
return false;
}, until);
return result;
#else
return FenceStatus::ERROR;
#endif
@@ -3768,20 +3777,6 @@ void OpenGLDriver::beginRenderPass(Handle<HwRenderTarget> rth,
// each render-pass starts with a disabled scissor
gl.disable(GL_SCISSOR_TEST);
if (gl.ext.EXT_discard_framebuffer
&& !gl.bugs.disable_invalidate_framebuffer) {
AttachmentArray attachments; // NOLINT
if (GLsizei const attachmentCount = getAttachments(attachments, discardFlags, !fbo)) {
gl.procs.invalidateFramebuffer(GL_FRAMEBUFFER, attachmentCount, attachments.data());
}
CHECK_GL_ERROR()
} else {
// It's important to clear the framebuffer before drawing, as it resets
// the fb to a known state (resets fb compression and possibly other things).
// So we use glClear instead of glInvalidateFramebuffer
clearWithRasterPipe(discardFlags & ~clearFlags, { 0.0f }, 0.0f, 0);
}
if (rt->gl.fbo_read) {
// we have a multi-sample RenderTarget with non multi-sample attachments (i.e. this is the
// EXT_multisampled_render_to_texture emulation).
@@ -3793,9 +3788,26 @@ void OpenGLDriver::beginRenderPass(Handle<HwRenderTarget> rth,
discardFlags |= rt->gl.resolve;
}
// we don't discard attachments that are cleared (because clear is an implicit discard)
TargetBufferFlags const discardOnlyFlags = discardFlags & ~clearFlags;
if (any(discardOnlyFlags)) {
if (gl.ext.EXT_discard_framebuffer && !gl.bugs.disable_invalidate_framebuffer) {
AttachmentArray attachments; // NOLINT
if (GLsizei const attachmentCount = getAttachments(attachments, discardOnlyFlags, !fbo)) {
gl.procs.invalidateFramebuffer(GL_FRAMEBUFFER, attachmentCount, attachments.data());
}
CHECK_GL_ERROR()
} else {
// It's important to clear the framebuffer before drawing, as it resets
// the fb to a known state (resets fb compression and possibly other things).
// So we use glClear instead of glInvalidateFramebuffer
clearWithRasterPipe(discardOnlyFlags, { 0.0f }, 0.0f, 0);
}
}
if (any(clearFlags)) {
clearWithRasterPipe(clearFlags,
params.clearColor, GLfloat(params.clearDepth), GLint(params.clearStencil));
clearWithRasterPipe(clearFlags, params.clearColor, GLfloat(params.clearDepth), GLint(params.clearStencil));
}
// we need to reset those after we call clearWithRasterPipe()
@@ -3812,8 +3824,7 @@ void OpenGLDriver::beginRenderPass(Handle<HwRenderTarget> rth,
#ifndef NDEBUG
// clear the discarded (but not the cleared ones) buffers in debug builds
clearWithRasterPipe(discardFlags & ~clearFlags,
{ 1, 0, 0, 1 }, 1.0, 0);
clearWithRasterPipe(discardOnlyFlags, { 1, 0, 0, 1 }, 1.0, 0);
#endif
}
@@ -197,8 +197,6 @@ public:
struct GLFence : public HwFence {
using HwFence::HwFence;
struct State {
utils::Mutex lock; // NOLINT(*-include-cleaner)
utils::Condition cond; // NOLINT(*-include-cleaner)
FenceStatus status{ FenceStatus::TIMEOUT_EXPIRED };
};
std::shared_ptr<State> state{ std::make_shared<State>() };
@@ -16,6 +16,10 @@
#include "VulkanAsyncHandles.h"
#include "VulkanConstants.h"
#include "vulkan/utils/Spirv.h"
#include <backend/DriverEnums.h>
#include <utils/debug.h>
@@ -29,6 +33,154 @@ using namespace bluevk;
namespace filament::backend {
namespace {
inline VkShaderStageFlags getVkStage(backend::ShaderStage stage) {
switch(stage) {
case backend::ShaderStage::VERTEX:
return VK_SHADER_STAGE_VERTEX_BIT;
case backend::ShaderStage::FRAGMENT:
return VK_SHADER_STAGE_FRAGMENT_BIT;
case backend::ShaderStage::COMPUTE:
PANIC_POSTCONDITION("Unsupported stage");
}
}
} // namespace
PushConstantDescription::PushConstantDescription(backend::Program const& program) {
mRangeCount = 0;
uint32_t offset = 0;
// The range is laid out so that the vertex constants are defined as the first set of bytes,
// followed by fragment and compute. This means we need to keep track of the offset for each
// stage. We do the bookeeping in mDescriptions.
for (auto stage: { ShaderStage::VERTEX, ShaderStage::FRAGMENT, ShaderStage::COMPUTE }) {
auto const& constants = program.getPushConstants(stage);
if (constants.empty()) {
continue;
}
auto& description = mDescriptions[(uint8_t) stage];
// We store the type of the constant for type-checking when writing.
description.types.reserve(constants.size());
std::for_each(constants.cbegin(), constants.cend(),
[&description](Program::PushConstant t) { description.types.push_back(t.type); });
uint32_t const constantsSize = (uint32_t) constants.size() * ENTRY_SIZE;
mRanges[mRangeCount++] = {
.stageFlags = getVkStage(stage),
.offset = offset,
.size = constantsSize,
};
description.offset = offset;
offset += constantsSize;
}
}
void PushConstantDescription::write(VkCommandBuffer cmdbuf, VkPipelineLayout layout,
backend::ShaderStage stage, uint8_t index, backend::PushConstantVariant const& value) {
uint32_t binaryValue = 0;
auto const& description = mDescriptions[(uint8_t) stage];
UTILS_UNUSED_IN_RELEASE auto const& types = description.types;
uint32_t const offset = description.offset;
if (std::holds_alternative<bool>(value)) {
assert_invariant(types[index] == ConstantType::BOOL);
bool const bval = std::get<bool>(value);
binaryValue = static_cast<uint32_t const>(bval ? VK_TRUE : VK_FALSE);
} else if (std::holds_alternative<float>(value)) {
assert_invariant(types[index] == ConstantType::FLOAT);
float const fval = std::get<float>(value);
binaryValue = *reinterpret_cast<uint32_t const*>(&fval);
} else {
assert_invariant(types[index] == ConstantType::INT);
int const ival = std::get<int>(value);
binaryValue = *reinterpret_cast<uint32_t const*>(&ival);
}
vkCmdPushConstants(cmdbuf, layout, getVkStage(stage), offset + index * ENTRY_SIZE, ENTRY_SIZE,
&binaryValue);
}
VulkanProgram::VulkanProgram(VkDevice device, Program const& builder) noexcept
: HwProgram(builder.getName()),
mInfo(new(std::nothrow) PipelineInfo(builder)),
mDevice(device) {
Program::ShaderSource const& blobs = builder.getShadersSource();
auto& modules = mInfo->shaders;
auto const& specializationConstants = builder.getSpecializationConstants();
std::vector<uint32_t> shader;
static_assert(static_cast<ShaderStage>(0) == ShaderStage::VERTEX &&
static_cast<ShaderStage>(1) == ShaderStage::FRAGMENT &&
MAX_SHADER_MODULES == 2);
for (size_t i = 0; i < MAX_SHADER_MODULES; i++) {
Program::ShaderBlob const& blob = blobs[i];
uint32_t* data = (uint32_t*) blob.data();
size_t dataSize = blob.size();
if (!specializationConstants.empty()) {
fvkutils::workaroundSpecConstant(blob, specializationConstants, shader);
data = (uint32_t*) shader.data();
dataSize = shader.size() * 4;
}
VkShaderModule& module = modules[i];
VkShaderModuleCreateInfo moduleInfo = {
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = dataSize,
.pCode = data,
};
VkResult result = vkCreateShaderModule(mDevice, &moduleInfo, VKALLOC, &module);
FILAMENT_CHECK_POSTCONDITION(result == VK_SUCCESS)
<< "Unable to create shader module."
<< " error=" << static_cast<int32_t>(result);
#if FVK_ENABLED(FVK_DEBUG_DEBUG_UTILS)
utils::CString name{ builder.getName().c_str(), builder.getName().size() };
switch (static_cast<ShaderStage>(i)) {
case ShaderStage::VERTEX:
name += "_vs";
break;
case ShaderStage::FRAGMENT:
name += "_fs";
break;
default:
PANIC_POSTCONDITION("Unexpected stage");
break;
}
VulkanDriver::DebugUtils::setName(VK_OBJECT_TYPE_SHADER_MODULE,
reinterpret_cast<uint64_t>(module), name.c_str());
#endif
}
#if FVK_ENABLED(FVK_DEBUG_SHADER_MODULE)
FVK_LOGD << "Created VulkanProgram " << builder << ", shaders = (" << modules[0]
<< ", " << modules[1] << ")";
#endif
}
VulkanProgram::~VulkanProgram() {
for (auto shader: mInfo->shaders) {
vkDestroyShaderModule(mDevice, shader, VKALLOC);
}
delete mInfo;
}
void VulkanProgram::flushPushConstants(VkPipelineLayout layout) {
// At this point, we really ought to have a VkPipelineLayout.
assert_invariant(layout != VK_NULL_HANDLE);
for (const auto& c : mQueuedPushConstants) {
mInfo->pushConstantDescription.write(c.cmdbuf, layout, c.stage, c.index, c.value);
}
mQueuedPushConstants.clear();
}
std::shared_ptr<VulkanCmdFence> VulkanCmdFence::completed() noexcept {
auto cmdFence = std::make_shared<VulkanCmdFence>(VK_NULL_HANDLE);
cmdFence->mStatus = VK_SUCCESS;
+121 -19
View File
@@ -24,6 +24,9 @@
#include "backend/Platform.h"
#include "vulkan/memory/Resource.h"
#include "vulkan/utils/StaticVector.h"
#include <backend/Program.h>
#include <chrono>
#include <cstdint>
@@ -35,6 +38,123 @@
namespace filament::backend {
using PushConstantNameArray = utils::FixedCapacityVector<char const*>;
using PushConstantNameByStage = std::array<PushConstantNameArray, Program::SHADER_TYPE_COUNT>;
struct PushConstantDescription {
explicit PushConstantDescription(backend::Program const& program);
VkPushConstantRange const* getVkRanges() const noexcept { return mRanges; }
uint32_t getVkRangeCount() const noexcept { return mRangeCount; }
void write(VkCommandBuffer cmdbuf, VkPipelineLayout layout, backend::ShaderStage stage,
uint8_t index, backend::PushConstantVariant const& value);
private:
static constexpr uint32_t ENTRY_SIZE = sizeof(uint32_t);
struct ConstantDescription {
utils::FixedCapacityVector<backend::ConstantType> types;
uint32_t offset = 0;
};
// Describes the constants in each shader stage.
ConstantDescription mDescriptions[Program::SHADER_TYPE_COUNT];
VkPushConstantRange mRanges[Program::SHADER_TYPE_COUNT];
uint32_t mRangeCount;
};
// Encapsulates a VkShaderModule. Note: this is a ThreadSafeResource
// because we may use parallel compilation, in which case ref counts may
// be modified on multiple backend threads. This is simply to avoid
// destroying a VkShaderModule mid-compilation of a pipeline. It is
// still assumed that all calls will originate from the backend.
struct VulkanProgram : public HwProgram, fvkmemory::ThreadSafeResource {
using BindingList = fvkutils::StaticVector<uint16_t, MAX_SAMPLER_COUNT>;
VulkanProgram(VkDevice device, Program const& builder) noexcept;
~VulkanProgram();
/**
* Cancels any parallel compilation jobs that have not yet run for this
* program.
*/
inline void cancelParallelCompilation() {
mParallelCompilationCanceled.store(true, std::memory_order_release);
}
/**
* Writes out any queued push constants using the provided VkPipelineLayout.
*
* @param layout The layout that is to be used along with these push constants,
* in the next draw call.
*/
void flushPushConstants(VkPipelineLayout layout);
inline VkShaderModule getVertexShader() const {
return mInfo->shaders[0];
}
inline VkShaderModule getFragmentShader() const { return mInfo->shaders[1]; }
inline uint32_t getPushConstantRangeCount() const {
return mInfo->pushConstantDescription.getVkRangeCount();
}
inline VkPushConstantRange const* getPushConstantRanges() const {
return mInfo->pushConstantDescription.getVkRanges();
}
/**
* Returns true if parallel compilation is canceled, false if not. Parallel
* compilation will be canceled if this program is destroyed before relevant
* pipelines are created.
*
* @return true if parallel compilation should run for this program, false if not
*/
inline bool isParallelCompilationCanceled() const {
return mParallelCompilationCanceled.load(std::memory_order_acquire);
}
inline void writePushConstant(VkCommandBuffer cmdbuf, VkPipelineLayout layout,
backend::ShaderStage stage, uint8_t index, backend::PushConstantVariant const& value) {
// It's possible that we don't have the layout yet. When external samplers are used, bindPipeline()
// in VulkanDriver returns early, without binding a layout. If that happens, the layout is not
// set until draw time. Any push constants that are written during that time should be saved for
// later, and flushed when the layout is set.
if (layout != VK_NULL_HANDLE) {
mInfo->pushConstantDescription.write(cmdbuf, layout, stage, index, value);
} else {
mQueuedPushConstants.push_back({cmdbuf, stage, index, value});
}
}
// TODO: handle compute shaders.
// The expected order of shaders - from frontend to backend - is vertex, fragment, compute.
static constexpr uint8_t const MAX_SHADER_MODULES = 2;
private:
struct PipelineInfo {
explicit PipelineInfo(backend::Program const& program) noexcept
: pushConstantDescription(program)
{}
VkShaderModule shaders[MAX_SHADER_MODULES] = { VK_NULL_HANDLE };
PushConstantDescription pushConstantDescription;
};
struct PushConstantInfo {
VkCommandBuffer cmdbuf;
backend::ShaderStage stage;
uint8_t index;
backend::PushConstantVariant value;
};
PipelineInfo* mInfo;
VkDevice mDevice = VK_NULL_HANDLE;
std::atomic<bool> mParallelCompilationCanceled { false };
std::vector<PushConstantInfo> mQueuedPushConstants;
};
// Wrapper to enable use of shared_ptr for implementing shared ownership of low-level Vulkan fences.
struct VulkanCmdFence {
explicit VulkanCmdFence(VkFence fence) : mFence(fence) { }
@@ -84,39 +204,21 @@ struct VulkanFence : public HwFence, fvkmemory::ThreadSafeResource {
VulkanFence() {}
void setFence(std::shared_ptr<VulkanCmdFence> fence) {
std::lock_guard const l(lock);
sharedFence = std::move(fence);
cond.notify_all();
}
std::shared_ptr<VulkanCmdFence>& getSharedFence() {
std::lock_guard const l(lock);
return sharedFence;
}
std::pair<std::shared_ptr<VulkanCmdFence>, bool>
wait(std::chrono::steady_clock::time_point const until) {
// hold a reference so that our state doesn't disappear while we wait
std::unique_lock l(lock);
cond.wait_until(l, until, [this] {
return bool(sharedFence) || canceled;
});
// here mSharedFence will be null if we timed out
std::pair<std::shared_ptr<VulkanCmdFence>, bool> getStatus() const {
return { sharedFence, canceled };
}
void cancel() const {
std::lock_guard const l(lock);
if (sharedFence) {
sharedFence->cancel();
}
canceled = true;
cond.notify_all();
}
private:
mutable std::mutex lock;
mutable std::condition_variable cond;
mutable bool canceled = false;
std::shared_ptr<VulkanCmdFence> sharedFence;
};
+11 -3
View File
@@ -34,10 +34,11 @@
#include <utils/Mutex.h>
#include <atomic>
#include <chrono>
#include <list>
#include <type_traits>
#include <utility>
#include <variant>
namespace filament::backend {
@@ -72,7 +73,11 @@ struct VulkanCommandBuffer {
~VulkanCommandBuffer();
inline void acquire(fvkmemory::resource_ptr<fvkmemory::Resource> resource) {
template <typename T,
typename = std::enable_if_t<
std::is_base_of_v<fvkmemory::Resource, T> ||
std::is_base_of_v<fvkmemory::ThreadSafeResource, T>>>
inline void acquire(fvkmemory::resource_ptr<T> resource) {
mResources.push_back(resource);
}
@@ -115,6 +120,9 @@ struct VulkanCommandBuffer {
}
private:
using HeldResource = std::variant<fvkmemory::resource_ptr<Resource>,
fvkmemory::resource_ptr<ThreadSafeResource>>;
static uint32_t sAgeCounter;
VulkanContext const& mContext;
@@ -129,7 +137,7 @@ private:
fvkmemory::resource_ptr<VulkanSemaphore> mSubmission;
VkFence mFence;
std::shared_ptr<VulkanCmdFence> mFenceStatus;
std::vector<fvkmemory::resource_ptr<Resource>> mResources;
std::vector<HeldResource> mResources;
uint32_t mAge;
};
+18 -3
View File
@@ -1008,7 +1008,9 @@ void VulkanDriver::createFenceR(Handle<HwFence> fh, utils::ImmutableCString&& ta
// it with appropriate VulkanCmdFence, which is associated with the current, recording command
// buffer.
auto fence = resource_ptr<VulkanFence>::cast(&mResourceManager, fh);
fence->setFence(cmdbuf->getFenceStatus());
signalFence([&] {
fence->setFence(cmdbuf->getFenceStatus());
});
mResourceManager.associateHandle(fh.getId(), std::move(tag));
}
@@ -1459,7 +1461,9 @@ void VulkanDriver::fenceCancel(FenceHandle const fh) {
// Even though this is a synchronous call, the fence handle must be (and stay) valid
assert_invariant(fh);
auto fence = resource_ptr<VulkanFence>::cast(&mResourceManager, fh);
fence->cancel();
signalFence([&] {
fence->cancel();
});
}
FenceStatus VulkanDriver::getFenceStatus(Handle<HwFence> const fh) {
@@ -1486,7 +1490,18 @@ FenceStatus VulkanDriver::fenceWait(FenceHandle const fh, uint64_t const timeout
until = now + nanoseconds(timeout);
}
auto const [cmdfence, canceled] = fence->wait(until);
std::shared_ptr<VulkanCmdFence> cmdfence;
bool canceled = false;
waitForFence([&] {
auto status = fence->getStatus();
if (bool(status.first) || status.second) {
cmdfence = status.first;
canceled = status.second;
return true;
}
return false;
}, until);
if (!cmdfence || canceled) {
return canceled ? FenceStatus::ERROR : FenceStatus::TIMEOUT_EXPIRED;
}
@@ -16,8 +16,6 @@
#include "VulkanHandles.h"
#include "VulkanConstants.h"
// TODO: remove this by moving DebugUtils out of VulkanDriver
#include "VulkanDriver.h"
@@ -26,7 +24,6 @@
#include "vulkan/utils/Conversion.h"
#include "vulkan/utils/Definitions.h"
#include "vulkan/utils/Image.h"
#include "vulkan/utils/Spirv.h"
#include <backend/platforms/VulkanPlatform.h>
@@ -86,17 +83,6 @@ inline void fromStageFlags(backend::ShaderStageFlags stage, descriptor_binding_t
}
}
inline VkShaderStageFlags getVkStage(backend::ShaderStage stage) {
switch(stage) {
case backend::ShaderStage::VERTEX:
return VK_SHADER_STAGE_VERTEX_BIT;
case backend::ShaderStage::FRAGMENT:
return VK_SHADER_STAGE_FRAGMENT_BIT;
case backend::ShaderStage::COMPUTE:
PANIC_POSTCONDITION("Unsupported stage");
}
}
using BitmaskGroup = VulkanDescriptorSetLayout::Bitmask;
BitmaskGroup fromBackendLayout(DescriptorSetLayout const& layout) {
BitmaskGroup mask;
@@ -264,139 +250,6 @@ void VulkanDescriptorSet::addNewSet(VkDescriptorSet vkSet, OnRecycle&& onRecycle
mSets.push_back({ vkSet, std::move(onRecycleFn) });
}
PushConstantDescription::PushConstantDescription(backend::Program const& program) {
mRangeCount = 0;
uint32_t offset = 0;
// The range is laid out so that the vertex constants are defined as the first set of bytes,
// followed by fragment and compute. This means we need to keep track of the offset for each
// stage. We do the bookeeping in mDescriptions.
for (auto stage: { ShaderStage::VERTEX, ShaderStage::FRAGMENT, ShaderStage::COMPUTE }) {
auto const& constants = program.getPushConstants(stage);
if (constants.empty()) {
continue;
}
auto& description = mDescriptions[(uint8_t) stage];
// We store the type of the constant for type-checking when writing.
description.types.reserve(constants.size());
std::for_each(constants.cbegin(), constants.cend(),
[&description](Program::PushConstant t) { description.types.push_back(t.type); });
uint32_t const constantsSize = (uint32_t) constants.size() * ENTRY_SIZE;
mRanges[mRangeCount++] = {
.stageFlags = getVkStage(stage),
.offset = offset,
.size = constantsSize,
};
description.offset = offset;
offset += constantsSize;
}
}
void PushConstantDescription::write(VkCommandBuffer cmdbuf, VkPipelineLayout layout,
backend::ShaderStage stage, uint8_t index, backend::PushConstantVariant const& value) {
uint32_t binaryValue = 0;
auto const& description = mDescriptions[(uint8_t) stage];
UTILS_UNUSED_IN_RELEASE auto const& types = description.types;
uint32_t const offset = description.offset;
if (std::holds_alternative<bool>(value)) {
assert_invariant(types[index] == ConstantType::BOOL);
bool const bval = std::get<bool>(value);
binaryValue = static_cast<uint32_t const>(bval ? VK_TRUE : VK_FALSE);
} else if (std::holds_alternative<float>(value)) {
assert_invariant(types[index] == ConstantType::FLOAT);
float const fval = std::get<float>(value);
binaryValue = *reinterpret_cast<uint32_t const*>(&fval);
} else {
assert_invariant(types[index] == ConstantType::INT);
int const ival = std::get<int>(value);
binaryValue = *reinterpret_cast<uint32_t const*>(&ival);
}
vkCmdPushConstants(cmdbuf, layout, getVkStage(stage), offset + index * ENTRY_SIZE, ENTRY_SIZE,
&binaryValue);
}
VulkanProgram::VulkanProgram(VkDevice device, Program const& builder) noexcept
: HwProgram(builder.getName()),
mInfo(new(std::nothrow) PipelineInfo(builder)),
mDevice(device) {
Program::ShaderSource const& blobs = builder.getShadersSource();
auto& modules = mInfo->shaders;
auto const& specializationConstants = builder.getSpecializationConstants();
std::vector<uint32_t> shader;
static_assert(static_cast<ShaderStage>(0) == ShaderStage::VERTEX &&
static_cast<ShaderStage>(1) == ShaderStage::FRAGMENT &&
MAX_SHADER_MODULES == 2);
for (size_t i = 0; i < MAX_SHADER_MODULES; i++) {
Program::ShaderBlob const& blob = blobs[i];
uint32_t* data = (uint32_t*) blob.data();
size_t dataSize = blob.size();
if (!specializationConstants.empty()) {
fvkutils::workaroundSpecConstant(blob, specializationConstants, shader);
data = (uint32_t*) shader.data();
dataSize = shader.size() * 4;
}
VkShaderModule& module = modules[i];
VkShaderModuleCreateInfo moduleInfo = {
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = dataSize,
.pCode = data,
};
VkResult result = vkCreateShaderModule(mDevice, &moduleInfo, VKALLOC, &module);
FILAMENT_CHECK_POSTCONDITION(result == VK_SUCCESS)
<< "Unable to create shader module."
<< " error=" << static_cast<int32_t>(result);
#if FVK_ENABLED(FVK_DEBUG_DEBUG_UTILS)
utils::CString name{ builder.getName().c_str(), builder.getName().size() };
switch (static_cast<ShaderStage>(i)) {
case ShaderStage::VERTEX:
name += "_vs";
break;
case ShaderStage::FRAGMENT:
name += "_fs";
break;
default:
PANIC_POSTCONDITION("Unexpected stage");
break;
}
VulkanDriver::DebugUtils::setName(VK_OBJECT_TYPE_SHADER_MODULE,
reinterpret_cast<uint64_t>(module), name.c_str());
#endif
}
#if FVK_ENABLED(FVK_DEBUG_SHADER_MODULE)
FVK_LOGD << "Created VulkanProgram " << builder << ", shaders = (" << modules[0]
<< ", " << modules[1] << ")";
#endif
}
VulkanProgram::~VulkanProgram() {
for (auto shader: mInfo->shaders) {
vkDestroyShaderModule(mDevice, shader, VKALLOC);
}
delete mInfo;
}
void VulkanProgram::flushPushConstants(VkPipelineLayout layout) {
// At this point, we really ought to have a VkPipelineLayout.
assert_invariant(layout != VK_NULL_HANDLE);
for (const auto& c : mQueuedPushConstants) {
mInfo->pushConstantDescription.write(c.cmdbuf, layout, c.stage, c.index, c.value);
}
mQueuedPushConstants.clear();
}
// Creates a special "default" render target (i.e. associated with the swap chain)
VulkanRenderTarget::VulkanRenderTarget()
: HwRenderTarget(0, 0),
-115
View File
@@ -30,9 +30,6 @@
#include "vulkan/memory/Resource.h"
#include "vulkan/memory/ResourcePointer.h"
#include "vulkan/utils/Definitions.h"
#include "vulkan/utils/StaticVector.h"
#include <backend/Program.h>
#include <utils/FixedCapacityVector.h>
#include <utils/Mutex.h>
@@ -241,118 +238,6 @@ struct VulkanMemoryMappedBuffer : public HwMemoryMappedBuffer, Resource {
uint32_t offset = 0;
};
using PushConstantNameArray = utils::FixedCapacityVector<char const*>;
using PushConstantNameByStage = std::array<PushConstantNameArray, Program::SHADER_TYPE_COUNT>;
struct PushConstantDescription {
explicit PushConstantDescription(backend::Program const& program);
VkPushConstantRange const* getVkRanges() const noexcept { return mRanges; }
uint32_t getVkRangeCount() const noexcept { return mRangeCount; }
void write(VkCommandBuffer cmdbuf, VkPipelineLayout layout, backend::ShaderStage stage,
uint8_t index, backend::PushConstantVariant const& value);
private:
static constexpr uint32_t ENTRY_SIZE = sizeof(uint32_t);
struct ConstantDescription {
utils::FixedCapacityVector<backend::ConstantType> types;
uint32_t offset = 0;
};
// Describes the constants in each shader stage.
ConstantDescription mDescriptions[Program::SHADER_TYPE_COUNT];
VkPushConstantRange mRanges[Program::SHADER_TYPE_COUNT];
uint32_t mRangeCount;
};
struct VulkanProgram : public HwProgram, fvkmemory::Resource {
using BindingList = fvkutils::StaticVector<uint16_t, MAX_SAMPLER_COUNT>;
VulkanProgram(VkDevice device, Program const& builder) noexcept;
~VulkanProgram();
/**
* Cancels any parallel compilation jobs that have not yet run for this
* program.
*/
inline void cancelParallelCompilation() {
mParallelCompilationCanceled.store(true, std::memory_order_release);
}
/**
* Writes out any queued push constants using the provided VkPipelineLayout.
*
* @param layout The layout that is to be used along with these push constants,
* in the next draw call.
*/
void flushPushConstants(VkPipelineLayout layout);
inline VkShaderModule getVertexShader() const {
return mInfo->shaders[0];
}
inline VkShaderModule getFragmentShader() const { return mInfo->shaders[1]; }
inline uint32_t getPushConstantRangeCount() const {
return mInfo->pushConstantDescription.getVkRangeCount();
}
inline VkPushConstantRange const* getPushConstantRanges() const {
return mInfo->pushConstantDescription.getVkRanges();
}
/**
* Returns true if parallel compilation is canceled, false if not. Parallel
* compilation will be canceled if this program is destroyed before relevant
* pipelines are created.
*
* @return true if parallel compilation should run for this program, false if not
*/
inline bool isParallelCompilationCanceled() const {
return mParallelCompilationCanceled.load(std::memory_order_acquire);
}
inline void writePushConstant(VkCommandBuffer cmdbuf, VkPipelineLayout layout,
backend::ShaderStage stage, uint8_t index, backend::PushConstantVariant const& value) {
// It's possible that we don't have the layout yet. When external samplers are used, bindPipeline()
// in VulkanDriver returns early, without binding a layout. If that happens, the layout is not
// set until draw time. Any push constants that are written during that time should be saved for
// later, and flushed when the layout is set.
if (layout != VK_NULL_HANDLE) {
mInfo->pushConstantDescription.write(cmdbuf, layout, stage, index, value);
} else {
mQueuedPushConstants.push_back({cmdbuf, stage, index, value});
}
}
// TODO: handle compute shaders.
// The expected order of shaders - from frontend to backend - is vertex, fragment, compute.
static constexpr uint8_t const MAX_SHADER_MODULES = 2;
private:
struct PipelineInfo {
explicit PipelineInfo(backend::Program const& program) noexcept
: pushConstantDescription(program)
{}
VkShaderModule shaders[MAX_SHADER_MODULES] = { VK_NULL_HANDLE };
PushConstantDescription pushConstantDescription;
};
struct PushConstantInfo {
VkCommandBuffer cmdbuf;
backend::ShaderStage stage;
uint8_t index;
backend::PushConstantVariant value;
};
PipelineInfo* mInfo;
VkDevice mDevice = VK_NULL_HANDLE;
std::atomic<bool> mParallelCompilationCanceled { false };
std::vector<PushConstantInfo> mQueuedPushConstants;
};
// The render target bundles together a set of attachments, each of which can have one of the
// following ownership semantics:
//
@@ -223,6 +223,8 @@ void VulkanReadPixels::run(fvkmemory::resource_ptr<VulkanTexture> srcTexture, ui
bpp *= componentCount;
}
uint32_t const samples = srcTexture->samples > 1 ? srcTexture->samples : 1;
// Use a VkBuffer as the staging area for readback.
// Using a buffer instead of a linearly tiled VkImage unifies the readback path and avoids
// driver/validation layer issues since some implementations strictly prohibit linear tiling
@@ -230,9 +232,10 @@ void VulkanReadPixels::run(fvkmemory::resource_ptr<VulkanTexture> srcTexture, ui
VkBuffer stagingBuffer = VK_NULL_HANDLE;
VkMemoryRequirements memReqs;
uint32_t const stagingSize = width * height * bpp * samples;
VkBufferCreateInfo bufferInfo = {
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size = width * height * bpp,
.size = stagingSize,
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
};
// TODO: we could use a staging buffer pool, but this is on another thread. We'd need to
@@ -371,6 +374,9 @@ void VulkanReadPixels::run(fvkmemory::resource_ptr<VulkanTexture> srcTexture, ui
uint8_t const* srcPixels;
vkMapMemory(device, stagingMemory, 0, VK_WHOLE_SIZE, 0, (void**) &srcPixels);
// If MSAA, MoltenVK returns samples in planar layout (Sample 0 is the first width * height
// pixels). So we can simply ask DataReshaper to read width * height elements with standard
// row pitch!
int const rowPitch = width * bpp;
if (!DataReshaper::reshapeImage(&p, componentType, componentCount, srcPixels, rowPitch,
static_cast<int>(width), static_cast<int>(height), swizzle)) {
@@ -67,7 +67,10 @@ ResourceType getTypeEnum() noexcept;
std::string_view getTypeStr(ResourceType type);
inline bool isThreadSafeType(ResourceType type) {
return type == ResourceType::FENCE || type == ResourceType::TIMER_QUERY;
return type == ResourceType::PROGRAM ||
type == ResourceType::FENCE ||
type == ResourceType::TIMER_QUERY ||
type == ResourceType::SYNC;
}
struct Resource {
@@ -49,8 +49,11 @@ private:
using AllocatorImpl = HandleAllocatorVK;
template<typename D>
using requires_thread_safety = typename std::disjunction<std::is_same<D, VulkanFence>,
std::is_same<D, VulkanTimerQuery>>;
using requires_thread_safety = typename std::disjunction<
std::is_same<D, VulkanProgram>,
std::is_same<D, VulkanFence>,
std::is_same<D, VulkanTimerQuery>,
std::is_same<D, VulkanSync>>;
template<typename D, typename B, typename... ARGS>
inline D* construct(Handle<B> const& handle, ARGS&&... args) noexcept {
@@ -770,6 +770,7 @@ PixelDataType getComponentType(VkFormat format) {
case VK_FORMAT_D16_UNORM: return PixelDataType::USHORT;
case VK_FORMAT_D32_SFLOAT: return PixelDataType::FLOAT;
case VK_FORMAT_X8_D24_UNORM_PACK32: return PixelDataType::UINT;
case VK_FORMAT_B10G11R11_UFLOAT_PACK32: return PixelDataType::UINT_10F_11F_11F_REV;
// For combined depth/stencil formats, we return the primary (depth) aspect type.
// Stencil aspect overrides are handled dynamically during readback.
case VK_FORMAT_D24_UNORM_S8_UINT: return PixelDataType::UINT;
@@ -848,6 +849,8 @@ uint32_t getComponentCount(VkFormat format) {
case VK_FORMAT_R32G32B32_UINT:
case VK_FORMAT_R32G32B32_SINT:
case VK_FORMAT_R32G32B32_SFLOAT:
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
return 3;
case VK_FORMAT_R8G8B8A8_UNORM:
+36 -7
View File
@@ -702,7 +702,9 @@ void WebGPUDriver::createRenderTargetR(Handle<HwRenderTarget> renderTargetHandle
void WebGPUDriver::createFenceR(Handle<HwFence> fenceHandle, utils::ImmutableCString&& tag) {
// The handle is constructed synchronously in createFenceS.
const auto fence = handleCast<WebGPUFence>(fenceHandle);
fence->setSubmissionState(mQueueManager.getLatestSubmissionState());
signalFence([&] {
fence->setSubmissionState(mQueueManager.getLatestSubmissionState());
});
setDebugTag(fenceHandle.getId(), std::move(tag));
}
@@ -772,11 +774,7 @@ void WebGPUDriver::fenceCancel(FenceHandle fh) {
}
FenceStatus WebGPUDriver::getFenceStatus(Handle<HwFence> fenceHandle) {
const auto fence = handleCast<WebGPUFence>(fenceHandle);
if (!fence) {
return FenceStatus::ERROR;
}
return fence->getStatus();
return fenceWait(fenceHandle, 0);
}
FenceStatus WebGPUDriver::fenceWait(FenceHandle fenceHandle, uint64_t const timeout) {
@@ -784,7 +782,38 @@ FenceStatus WebGPUDriver::fenceWait(FenceHandle fenceHandle, uint64_t const time
if (!fence) {
return FenceStatus::ERROR;
}
return fence->wait(timeout);
using namespace std::chrono;
auto now = steady_clock::now();
steady_clock::time_point until = steady_clock::time_point::max();
using TimeoutType = decltype(timeout);
constexpr TimeoutType maxTimeout = std::numeric_limits<TimeoutType>::max();
constexpr nanoseconds maxNano = nanoseconds::max();
if (timeout < maxNano.count() && timeout < maxTimeout && // Need to account for overflow
now <= steady_clock::time_point::max() - nanoseconds(timeout)) {
until = now + nanoseconds(timeout);
}
std::shared_ptr<WebGPUSubmissionState> state;
bool const success = waitForFence([&] {
state = fence->getState();
return bool(state);
}, until);
if (!success) {
return FenceStatus::TIMEOUT_EXPIRED;
}
auto duration_ns = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(steady_clock::now() - now)
.count());
// In the unlikely event that duration_ns is too close to timeout, we just assume the leftover
// timeout is 0.
if (timeout == 0 || duration_ns > timeout) {
duration_ns = timeout;
}
return state->waitForCompletion(timeout - duration_ns);
}
void WebGPUDriver::destroySync(Handle<HwSync> syncHandle) {
@@ -23,49 +23,6 @@ namespace filament::backend {
WebGPUFence::WebGPUFence() = default;
WebGPUFence::~WebGPUFence() = default;
void WebGPUFence::setSubmissionState(std::shared_ptr<WebGPUSubmissionState> state) {
std::lock_guard<std::mutex> const lock(mLock);
mState = state;
mCond.notify_one();
}
FenceStatus WebGPUFence::getStatus() {
return wait(0);
}
FenceStatus WebGPUFence::wait(uint64_t timeout) {
// we have to take into account that the STL's wait_for() actually works with
// time_points relative to steady_clock::now() internally.
using namespace std::chrono;
auto now = steady_clock::now();
steady_clock::time_point until = steady_clock::time_point::max();
using TimeoutType = decltype(timeout);
constexpr TimeoutType maxTimeout = std::numeric_limits<TimeoutType>::max();
constexpr nanoseconds maxNano = nanoseconds::max();
if (timeout < maxNano.count() && timeout < maxTimeout && // Need to account for overflow
now <= steady_clock::time_point::max() - nanoseconds(timeout)) {
until = now + nanoseconds(timeout);
}
{
std::unique_lock<std::mutex> lock(mLock);
bool const success = mCond.wait_until(lock, until, [this] {
return bool(mState);
});
if (!success) {
return FenceStatus::TIMEOUT_EXPIRED;
}
}
auto duration_ns = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(steady_clock::now() - now)
.count());
// In the unlikely event that duration_ns is too close to timeout, we just assume the leftover
// timeout is 0.
if (timeout == 0 || duration_ns > timeout) {
duration_ns = timeout;
}
return mState->waitForCompletion(timeout - duration_ns);
}
} // namespace filament::backend
+6 -6
View File
@@ -33,15 +33,15 @@ public:
~WebGPUFence();
// Initialize the fence with a submission state from the queue manager.
void setSubmissionState(std::shared_ptr<WebGPUSubmissionState> state);
void setSubmissionState(std::shared_ptr<WebGPUSubmissionState> state) {
mState = state;
}
// Note that getStatus cannot be const since it's implemented with wait();
FenceStatus getStatus();
FenceStatus wait(uint64_t timeout);
std::shared_ptr<WebGPUSubmissionState> getState() const {
return mState;
}
private:
std::mutex mLock;
std::condition_variable mCond;
std::shared_ptr<WebGPUSubmissionState> mState;
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

+6 -36
View File
@@ -31,10 +31,7 @@ using namespace filament;
using namespace filament::backend;
using namespace filament::math;
TEST_F(BackendTest, AutoresolveDifferingSampleCounts) {
SKIP_IF(SkipEnvironment(OperatingSystem::CI, Backend::OPENGL), "see b/486954356");
SKIP_IF(SkipEnvironment(OperatingSystem::CI, Backend::VULKAN), "see b/486954356");
TEST_F(BackendTest, Autoresolve) {
auto& api = getDriverApi();
constexpr int kRenderTargetSize = 512;
@@ -61,33 +58,7 @@ TEST_F(BackendTest, AutoresolveDifferingSampleCounts) {
TextureFormat::RGBA8, 1, kRenderTargetSize, kRenderTargetSize, 1,
TextureUsage::COLOR_ATTACHMENT | TextureUsage::SAMPLEABLE));
// First render pass: render a red triangle to a RenderTarget with 8 samples.
{
Handle<HwRenderTarget> renderTarget =
addCleanup(api.createRenderTarget(TargetBufferFlags::COLOR, kRenderTargetSize,
kRenderTargetSize, 8, 1, { { texture } }, {}, {}));
shader.uploadUniform(api, ubuffer,
SimpleMaterialParams{
.color = float4(1, 0, 0, 1),
});
RenderPassParams params = getClearColorRenderPass(float4(0));
params.viewport = { 0, 0, kRenderTargetSize, kRenderTargetSize };
RenderFrame frame(api);
api.beginRenderPass(renderTarget, params);
ps.primitiveType = PrimitiveType::TRIANGLES;
ps.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(ps);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
api.commit(swapChain);
}
// Second render pass: render a green triangle to a RenderTarget with 4 samples, attached to
// the same texture.
// render a red triangle to a RenderTarget with 4 samples.
{
Handle<HwRenderTarget> renderTarget =
addCleanup(api.createRenderTarget(TargetBufferFlags::COLOR, kRenderTargetSize,
@@ -95,10 +66,10 @@ TEST_F(BackendTest, AutoresolveDifferingSampleCounts) {
shader.uploadUniform(api, ubuffer,
SimpleMaterialParams{
.color = float4(0, 1, 0, 1),
.color = float4(1, 0, 0, 1),
});
RenderPassParams params = getClearColorRenderPass(float4(0));
RenderPassParams params = getClearColorRenderPass(float4(1));
params.viewport = { 0, 0, kRenderTargetSize, kRenderTargetSize };
RenderFrame frame(api);
@@ -109,11 +80,10 @@ TEST_F(BackendTest, AutoresolveDifferingSampleCounts) {
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
api.commit(swapChain);
EXPECT_IMAGE(renderTarget, ScreenshotParams(kRenderTargetSize, kRenderTargetSize,
"AutoresolveDifferingSampleCounts", 1048576));
api.commit(swapChain);
"Autoresolve", 545665143));
}
}
+4 -6
View File
@@ -145,7 +145,7 @@ static void createFaces(DriverApi& dapi, Handle<HwTexture> texture, int baseWidt
}
TEST_F(BlitTest, ColorMagnify) {
SKIP_IF(SkipEnvironment(OperatingSystem::CI, Backend::OPENGL), "b/453757697");
// b/453757697: MESA glBlitFramebuffer linear interpolation fallback precision slightly mismatches.
SKIP_IF(SkipEnvironment(OperatingSystem::CI, Backend::VULKAN), "b/453777397");
SKIP_IF(Backend::WEBGPU, "test cases fail in WebGPU, see b/424157731");
@@ -206,15 +206,13 @@ TEST_F(BlitTest, ColorMagnify) {
{
RenderFrame frame(api);
EXPECT_IMAGE(dstRenderTargets[0],
ScreenshotParams(kDstTexWidth, kDstTexHeight, "ColorMagnify", 0x410bdd31));
ScreenshotParams(kDstTexWidth, kDstTexHeight, "ColorMagnify", 0x410bdd31, false, 8000, 5));
api.commit(swapChain);
}
}
TEST_F(BlitTest, ColorMinify) {
SKIP_IF(Backend::WEBGPU, "test cases fail in WebGPU, see b/424157731");
SKIP_IF(SkipEnvironment(OperatingSystem::CI, Backend::OPENGL), "b/453758045");
SKIP_IF(SkipEnvironment(OperatingSystem::CI, Backend::VULKAN), "b/453776623");
auto& api = getDriverApi();
mCleanup.addPostCall([&]() { executeCommands(); });
@@ -268,7 +266,7 @@ TEST_F(BlitTest, ColorMinify) {
SamplerMagFilter::LINEAR);
EXPECT_IMAGE(dstRenderTargets[0],
ScreenshotParams(kDstTexWidth, kDstTexHeight, "ColorMinify", 0xf3d9c53f));
ScreenshotParams(kDstTexWidth, kDstTexHeight, "ColorMinify", 0xf3d9c53f, false, 8000, 5));
}
TEST_F(BlitTest, ColorResolve) {
@@ -355,7 +353,7 @@ TEST_F(BlitTest, ColorResolve) {
SamplerMagFilter::NEAREST);
EXPECT_IMAGE(dstRenderTarget,
ScreenshotParams(kDstTexWidth, kDstTexHeight, "ColorResolve", 531759687));
ScreenshotParams(kDstTexWidth, kDstTexHeight, "ColorResolve", 531759687, false, 8000, 5));
}
TEST_F(BlitTest, Blit2DTextureArray) {
+1 -2
View File
@@ -391,7 +391,6 @@ TEST_F(LoadImageTest, UpdateImage2D) {
}
TEST_F(LoadImageTest, UpdateImageSRGB) {
SKIP_IF(SkipEnvironment(OperatingSystem::CI, Backend::OPENGL), "see b/453756688");
SKIP_IF(Backend::VULKAN, "b/454040142");
auto& api = getDriverApi();
@@ -468,7 +467,7 @@ TEST_F(LoadImageTest, UpdateImageSRGB) {
api.endRenderPass();
EXPECT_IMAGE(defaultRenderTarget,
ScreenshotParams(kTexSize, kTexSize, "UpdateImageSRGB", 3300305265));
ScreenshotParams(kTexSize, kTexSize, "UpdateImageSRGB", 3300305265, false, 8000, 5));
api.commit(swapChain);
api.endFrame(0);
+1 -3
View File
@@ -177,9 +177,7 @@ TEST_F(BasicStencilBufferTest, DepthAndStencilBuffer) {
TEST_F(BasicStencilBufferTest, StencilBufferMSAA) {
SKIP_IF(Backend::WEBGPU, "test cases fail in WebGPU, see b/424157731");
SKIP_IF(SkipEnvironment(OperatingSystem::APPLE, Backend::OPENGL), "Stencil isn't applied");
SKIP_IF(SkipEnvironment(OperatingSystem::CI, Backend::OPENGL), "see b/453757787");
SKIP_IF(Backend::VULKAN, "b/453777217");
SKIP_IF(SkipEnvironment(OperatingSystem::APPLE, Backend::VULKAN), "Stencil not preserved");
auto& api = getDriverApi();
+17
View File
@@ -23,6 +23,7 @@
#include <filament/ToneMapper.h>
#include <utils/compiler.h>
#include <utils/FixedCapacityVector.h>
#include <math/mathfwd.h>
@@ -461,6 +462,22 @@ public:
*/
Builder& curves(math::float3 shadowGamma, math::float3 midPoint, math::float3 highlightScale) noexcept;
/**
* Specifies a custom 3D color grading LUT to map the final sRGB color.
* The LUT is applied after post-processing and in LDR (sRGB space).
* The data must be a 3D array of float3 (RGB) values.
* The dimension does not need to be a power of two, but must be non-zero.
* The values are always interpolated (trilinear) because the input color from previous steps is continuous.
* The dimension doesn't need to match dimensions().
* If the dimension is 0 or the data is empty, the custom LUT is skipped (ignored).
*
* @param data FixedCapacityVector containing the custom LUT data (3D array of float3).
* @param dimension Dimension of the custom LUT.
*
* @return This Builder, for chaining calls
*/
Builder& customLut(utils::FixedCapacityVector<math::float3> data, uint8_t dimension) noexcept;
/**
* Sets the output color space for this ColorGrading object. After all color grading steps
* have been applied, the final color will be converted in the desired color space.
+18
View File
@@ -442,6 +442,24 @@ public:
* invoking asynchronous methods.
*/
AsynchronousMode asynchronousMode = AsynchronousMode::NONE;
/**
* Capacity of the LRU cache for material definitions.
*
* A value of 0 indicates that definitions will be destroyed immediately when they are no
* longer referenced by any material instances or scenes. A value greater than 0 defines
* the maximum number of unreferenced definitions to keep alive to avoid re-compilation.
*/
uint32_t materialCacheCapacity = 0;
/**
* Capacity of the LRU cache for program specializations.
*
* Similar to materialCacheCapacity, but applies to the underlying shader programs generated
* for materials. A value of 0 means immediate destruction of unreferenced programs. A
* positive value caches up to that number of programs.
*/
uint32_t programCacheCapacity = 0;
};
+3 -3
View File
@@ -39,9 +39,9 @@ bool MaterialCache::MaterialKey::operator==(MaterialKey const& rhs) const noexce
return parser == rhs.parser;
}
MaterialCache::MaterialCache()
: mDefinitions("MaterialCache::mDefinitions", 0),
mPrograms("MaterialCache::mPrograms", 0) {}
MaterialCache::MaterialCache(uint32_t materialCapacity, uint32_t programCapacity)
: mDefinitions("MaterialCache::mDefinitions", materialCapacity),
mPrograms("MaterialCache::mPrograms", programCapacity) {}
MaterialCache::~MaterialCache() {
assert_invariant(mDefinitions.empty());
+1 -1
View File
@@ -56,7 +56,7 @@ public:
using ProgramCache =
utils::RefCountedMap<ProgramSpecialization, backend::Handle<backend::HwProgram>>;
MaterialCache();
MaterialCache(uint32_t materialCapacity, uint32_t programCapacity);
~MaterialCache();
// All reference-counted resources should be freed by the time MaterialCache is destructed, but
+1
View File
@@ -53,6 +53,7 @@
#include <array>
#include <random>
#include <string_view>
#include <optional>
#include <stddef.h>
#include <stdint.h>
+63 -1
View File
@@ -33,6 +33,7 @@
#include <utils/JobSystem.h>
#include <utils/Mutex.h>
#include <utils/Panic.h>
#include <cmath>
#include <cstdlib>
@@ -101,6 +102,10 @@ struct ColorGrading::BuilderDetails {
// Output color space
ColorSpace outputColorSpace = Rec709-sRGB-D65;
// Custom LUT
utils::FixedCapacityVector<math::float3> customLutData;
uint8_t customLutDimension = 0;
bool operator!=(const BuilderDetails &rhs) const {
return !(rhs == *this);
}
@@ -130,7 +135,9 @@ struct ColorGrading::BuilderDetails {
shadowGamma == rhs.shadowGamma &&
midPoint == rhs.midPoint &&
highlightScale == rhs.highlightScale &&
outputColorSpace == rhs.outputColorSpace;
outputColorSpace == rhs.outputColorSpace &&
customLutData == rhs.customLutData &&
customLutDimension == rhs.customLutDimension;
}
};
@@ -272,11 +279,27 @@ ColorGrading::Builder& ColorGrading::Builder::outputColorSpace(
return *this;
}
ColorGrading::Builder& ColorGrading::Builder::customLut(
utils::FixedCapacityVector<math::float3> data, uint8_t dimension) noexcept {
mImpl->customLutData = std::move(data);
mImpl->customLutDimension = dimension;
return *this;
}
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
ColorGrading* ColorGrading::Builder::build(Engine& engine) {
if (mImpl->customLutDimension == 0 || mImpl->customLutData.empty()) {
mImpl->customLutData.clear();
mImpl->customLutDimension = 0;
} else {
FILAMENT_CHECK_PRECONDITION(mImpl->customLutData.size() ==
size_t(mImpl->customLutDimension) * mImpl->customLutDimension * mImpl->customLutDimension)
<< "Custom LUT data size does not match dimension^3";
}
// We want to see if any of the default adjustment values have been modified
// We skip the tonemapping operator on purpose since we always want to apply it
BuilderDetails defaults;
@@ -544,6 +567,39 @@ inline float3 curves(float3 v, float3 shadowGamma, float3 midPoint, float3 highl
};
}
static float3 applyCustomLut(float3 v, const math::float3* lut, uint8_t dim) noexcept {
float3 pos = v * float(dim - 1);
float3 pos_floor = floor(pos);
float3 pos_ceil = min(pos_floor + 1.0f, float(dim - 1));
float3 d = pos - pos_floor;
int3 i0 = int3(pos_floor);
int3 i1 = int3(pos_ceil);
auto fetch = [&](int r, int g, int b) {
return lut[r + g * dim + b * dim * dim];
};
float3 c000 = fetch(i0.x, i0.y, i0.z);
float3 c100 = fetch(i1.x, i0.y, i0.z);
float3 c010 = fetch(i0.x, i1.y, i0.z);
float3 c110 = fetch(i1.x, i1.y, i0.z);
float3 c001 = fetch(i0.x, i0.y, i1.z);
float3 c101 = fetch(i1.x, i0.y, i1.z);
float3 c011 = fetch(i0.x, i1.y, i1.z);
float3 c111 = fetch(i1.x, i1.y, i1.z);
float3 c00 = c000 * (1.0f - d.x) + c100 * d.x;
float3 c10 = c010 * (1.0f - d.x) + c110 * d.x;
float3 c01 = c001 * (1.0f - d.x) + c101 * d.x;
float3 c11 = c011 * (1.0f - d.x) + c111 * d.x;
float3 c0 = c00 * (1.0f - d.y) + c10 * d.y;
float3 c1 = c01 * (1.0f - d.y) + c11 * d.y;
return c0 * (1.0f - d.z) + c1 * d.z;
}
//------------------------------------------------------------------------------
// Luminance scaling
//------------------------------------------------------------------------------
@@ -665,6 +721,7 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
// spaces are the same, but we currently don't check that. We must revise these conditions if we
// ever handle this case.
mIsOneDimensional = !builder->hasAdjustments && !builder->luminanceScaling
&& builder->customLutData.empty()
&& builder->toneMapper->isOneDimensional()
&& engine.features.engine.color_grading.use_1d_lut;
mIsLDR = mIsOneDimensional && builder->toneMapper->isLDR();
@@ -795,6 +852,11 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
// Apply OETF
v = config.oetf(v);
// Apply custom LUT if provided
if (!builder->customLutData.empty()) {
v = applyCustomLut(v, builder->customLutData.data(), builder->customLutDimension);
}
return v;
};
+14 -28
View File
@@ -271,6 +271,7 @@ FEngine::FEngine(Builder const& builder) :
mRenderableManager(*this),
mLightManager(*this),
mCameraManager(*this),
mMaterialCache(builder->mConfig.materialCacheCapacity, builder->mConfig.programCacheCapacity),
mCommandBufferQueue(
builder->mConfig.minCommandBufferSizeMB * MiB,
builder->mConfig.commandBufferSizeMB * MiB,
@@ -349,10 +350,6 @@ void FEngine::init() {
mActiveFeatureLevel = std::min(mActiveFeatureLevel, driverApi.getFeatureLevel());
#ifndef FILAMENT_ENABLE_FEATURE_LEVEL_0
assert_invariant(mActiveFeatureLevel > FeatureLevel::FEATURE_LEVEL_0);
#endif
LOG(INFO) << "Backend feature level: " << int(driverApi.getFeatureLevel());
LOG(INFO) << "FEngine feature level: " << int(mActiveFeatureLevel);
@@ -460,34 +457,23 @@ void FEngine::init() {
driverApi,
descriptor_sets::getPerRenderableLayout() };
#ifdef FILAMENT_ENABLE_FEATURE_LEVEL_0
if (UTILS_UNLIKELY(mActiveFeatureLevel == FeatureLevel::FEATURE_LEVEL_0)) {
FMaterial::DefaultMaterialBuilder defaultMaterialBuilder;
defaultMaterialBuilder.package(
MATERIALS_DEFAULTMATERIAL_FL0_DATA, MATERIALS_DEFAULTMATERIAL_FL0_SIZE);
mDefaultMaterial = downcast(defaultMaterialBuilder.build(*const_cast<FEngine*>(this)));
} else
#endif
{
FMaterial::DefaultMaterialBuilder defaultMaterialBuilder;
switch (mConfig.stereoscopicType) {
case StereoscopicType::NONE:
case StereoscopicType::INSTANCED:
defaultMaterialBuilder.package(
MATERIALS_DEFAULTMATERIAL_DATA, MATERIALS_DEFAULTMATERIAL_SIZE);
break;
case StereoscopicType::MULTIVIEW:
FMaterial::DefaultMaterialBuilder defaultMaterialBuilder;
switch (mConfig.stereoscopicType) {
case StereoscopicType::NONE:
case StereoscopicType::INSTANCED:
defaultMaterialBuilder.package(
MATERIALS_DEFAULTMATERIAL_DATA, MATERIALS_DEFAULTMATERIAL_SIZE);
break;
case StereoscopicType::MULTIVIEW:
#ifdef FILAMENT_ENABLE_MULTIVIEW
defaultMaterialBuilder.package(
MATERIALS_DEFAULTMATERIAL_MULTIVIEW_DATA,
MATERIALS_DEFAULTMATERIAL_MULTIVIEW_SIZE);
defaultMaterialBuilder.package(
MATERIALS_DEFAULTMATERIAL_MULTIVIEW_DATA, MATERIALS_DEFAULTMATERIAL_MULTIVIEW_SIZE);
#else
assert_invariant(false);
assert_invariant(false);
#endif
break;
}
mDefaultMaterial = downcast(defaultMaterialBuilder.build(*this));
break;
}
mDefaultMaterial = downcast(defaultMaterialBuilder.build(*this));
// We must commit the default material instance here. It may not be used in a scene, but its
// descriptor set may still be used for shared variants.
+10 -17
View File
@@ -132,26 +132,19 @@ FSkybox::FSkybox(FEngine& engine, const Builder& builder) noexcept
FMaterial const* FSkybox::createMaterial(FEngine& engine) {
Material::Builder builder;
#ifdef FILAMENT_ENABLE_FEATURE_LEVEL_0
if (UTILS_UNLIKELY(engine.getActiveFeatureLevel() == Engine::FeatureLevel::FEATURE_LEVEL_0)) {
builder.package(MATERIALS_SKYBOX_FL0_DATA, MATERIALS_SKYBOX_FL0_SIZE);
} else
#endif
{
switch (engine.getConfig().stereoscopicType) {
case Engine::StereoscopicType::NONE:
case Engine::StereoscopicType::INSTANCED:
builder.package(MATERIALS_SKYBOX_DATA, MATERIALS_SKYBOX_SIZE);
break;
case Engine::StereoscopicType::MULTIVIEW:
switch (engine.getConfig().stereoscopicType) {
case Engine::StereoscopicType::NONE:
case Engine::StereoscopicType::INSTANCED:
builder.package(MATERIALS_SKYBOX_DATA, MATERIALS_SKYBOX_SIZE);
break;
case Engine::StereoscopicType::MULTIVIEW:
#ifdef FILAMENT_ENABLE_MULTIVIEW
builder.package(MATERIALS_SKYBOX_MULTIVIEW_DATA, MATERIALS_SKYBOX_MULTIVIEW_SIZE);
builder.package(MATERIALS_SKYBOX_MULTIVIEW_DATA, MATERIALS_SKYBOX_MULTIVIEW_SIZE);
#else
PANIC_POSTCONDITION("Multiview is enabled in the Engine, but this build has not "
"been compiled for multiview.");
PANIC_POSTCONDITION("Multiview is enabled in the Engine, but this build has not "
"been compiled for multiview.");
#endif
break;
}
break;
}
auto material = builder.build(engine);
return downcast(material);
+2 -2
View File
@@ -34,8 +34,8 @@ vertex {
fragment {
void postProcess(inout PostProcessInputs postProcess) {
#if FILAMENT_EFFECTIVE_VERSION == 100
postProcess.color = texture2D(materialParams_color, variable_vertex.xy);
#if MATERIAL_FEATURE_LEVEL == 0
postProcess.color = texture(materialParams_color, variable_vertex.xy);
#else
postProcess.color = textureLod(materialParams_color, variable_vertex.xy, materialParams.levelOfDetail);
#endif
+2 -1
View File
@@ -1,6 +1,7 @@
material {
name : "Filament Default Material",
shadingModel : unlit
shadingModel : unlit,
featureLevel: 0
}
fragment {
+3 -2
View File
@@ -26,7 +26,8 @@ material {
depthWrite : false,
shadingModel : unlit,
variantFilter : [ skinning, shadowReceiver, vsm ],
culling: none
culling: none,
featureLevel: 0
}
fragment {
@@ -37,7 +38,7 @@ fragment {
sky = materialParams.color;
} else {
#if MATERIAL_FEATURE_LEVEL == 0
sky = vec4(textureCube(materialParams_skybox, variable_eyeDirection.xyz).rgb, 1.0);
sky = vec4(texture(materialParams_skybox, variable_eyeDirection.xyz).rgb, 1.0);
#else
sky = vec4(textureLod(materialParams_skybox, variable_eyeDirection.xyz, 0.0).rgb, 1.0);
sky.rgb *= frameUniforms.iblLuminance;
+4 -3
View File
@@ -73,13 +73,14 @@ void computeAmbientOcclusionSAO(inout float occlusion, inout vec3 bentNormal,
// now we have the sample, compute AO
highp vec3 v = p - origin; // sample vector
float vv = dot(v, v); // squared distance
float vn = dot(v, normal); // distance * cos(v, normal)
highp float vv = dot(v, v); // squared distance
highp float vn = dot(v, normal); // distance * cos(v, normal)
// discard samples that are outside of the radius, preventing distant geometry to
// cast shadows -- there are many functions that work and choosing one is an artistic
// decision.
float w = sq(max(0.0, 1.0 - vv * materialParams.invRadiusSquared));
// Need to be highp to avoid imprecision on certain hardware (e.g. PowerVR)
highp float w = sq(max(0.0, 1.0 - vv * materialParams.invRadiusSquared));
// discard samples that are too close to the horizon to reduce shadows cast by geometry
// not sufficently tessellated. The goal is to discard samples that form an angle 'beta'
+2 -2
View File
@@ -1,12 +1,12 @@
Pod::Spec.new do |spec|
spec.name = "Filament"
spec.version = "1.70.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.70.2/filament-v1.70.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++'
+7 -1
View File
@@ -186,6 +186,9 @@ bool MaterialChunk::getTextShader(Unflattener unflattener,
if (!unflattener.read(&shaderSize)){
return false;
}
if (shaderSize == 0) {
return false;
}
// Read how many lines there are.
uint32_t lineCount = 0;
@@ -244,6 +247,9 @@ bool MaterialChunk::getTextShader(Unflattener unflattener,
buf[0] = '_';
auto const [ptr, ec] = std::to_chars(buf + 1, buf + 16, numericLiteral);
size_t const len = ptr - buf;
if (UTILS_UNLIKELY(cursor + len >= shaderSize)) {
return false;
}
memcpy(&shaderContent[cursor], buf, len);
cursor += len;
continue;
@@ -263,7 +269,7 @@ bool MaterialChunk::getTextShader(Unflattener unflattener,
const auto& content = dictionary[globalIndex];
// Ensure string is correctly formed and doesn't exceed reserved shader space.
if (UTILS_UNLIKELY(content.size() == 0 || cursor + content.size() - 1 > shaderSize)) {
if (UTILS_UNLIKELY(content.size() == 0 || cursor + content.size() - 1 >= shaderSize)) {
return false;
}
+18 -4
View File
@@ -90,16 +90,23 @@ TEST_F(FilaflatSecurityTest, DictionaryTextOOBRead) {
// 2. Heap Buffer Overflow writing dictionary arrays to undersized output buffers
TEST_F(FilaflatSecurityTest, MaterialChunkHeapOverflow) {
std::vector<uint8_t> payload;
write16(payload, 0); // mSharedStrings
write16(payload, 0); // mVertexStrings
write16(payload, 0); // mFragmentStrings
write16(payload, 0); // mComputeStrings
write64(payload, 1); // numShaders
payload.push_back(1); // model
payload.push_back(0); // variant
payload.push_back(0); // stage
write32(payload, 15); // offset (8 + 1 + 1 + 1 + 4 = 15)
write32(payload, 23); // offset (4*2 + 8 + 1 + 1 + 1 + 4 = 23)
// Shader content layout
write32(payload, 4); // shaderSize (vulnerable tiny size)
write32(payload, 1); // lineCount = 1
write16(payload, 0); // lineIndex = 0
write32(payload, 0); // extLength
write32(payload, 0); // baseLength
write32(payload, 0); // numericLength
payload.push_back(0); // lineIndex = 0
std::vector<uint8_t> fileData;
write64(fileData, (uint64_t)filamat::ChunkType::MaterialGlsl);
@@ -132,14 +139,21 @@ TEST_F(FilaflatSecurityTest, MaterialChunkHeapOverflow) {
// 3. Out of Bounds Read via index mapping evasion (Text mode)
TEST_F(FilaflatSecurityTest, MaterialChunkOOBReadText) {
std::vector<uint8_t> payload;
write16(payload, 0); // mSharedStrings
write16(payload, 0); // mVertexStrings
write16(payload, 0); // mFragmentStrings
write16(payload, 0); // mComputeStrings
write64(payload, 1); // numShaders
payload.push_back(1); // model
payload.push_back(0); // variant
payload.push_back(0); // stage
write32(payload, 15); // offset (8 + 1 + 1 + 1 + 4 = 15)
write32(payload, 23); // offset
write32(payload, 100); // shaderSize
write32(payload, 1); // lineCount = 1
write16(payload, 9999); // lineIndex = 9999 (VULNERABLE ACCESS)
write32(payload, 0); // extLength
write32(payload, 0); // baseLength
write32(payload, 0); // numericLength
payload.push_back(1); // lineIndex = 1 (OOB for empty dict)
std::vector<uint8_t> fileData;
write64(fileData, (uint64_t)filamat::ChunkType::MaterialGlsl);
+1 -1
View File
@@ -22,7 +22,7 @@ fragment {
prepareMaterial(material);
vec2 uv = getUV0();
uv.y = 1.0 - uv.y;
vec4 albedo = texture2D(materialParams_albedo, uv);
vec4 albedo = texture(materialParams_albedo, uv);
material.baseColor = getColor() * albedo;
material.baseColor.rgb *= material.baseColor.a;
}
@@ -22,7 +22,7 @@ fragment {
prepareMaterial(material);
vec2 uv = getUV0();
uv.y = 1.0 - uv.y;
vec4 albedo = texture2D(materialParams_albedo, uv);
vec4 albedo = texture(materialParams_albedo, uv);
material.baseColor = getColor() * albedo;
material.baseColor.rgb *= material.baseColor.a;
}
@@ -278,7 +278,15 @@ public:
FLOAT,
FLOAT2,
FLOAT3,
FLOAT4
FLOAT4,
INT,
INT2,
INT3,
INT4,
UINT,
UINT2,
UINT3,
UINT4
};
struct PreprocessorDefine {
+8
View File
@@ -148,6 +148,14 @@ std::unordered_map<std::string_view, OutputType> Enums::mStringToOutputType = {
{ "float2", OutputType::FLOAT2 },
{ "float3", OutputType::FLOAT3 },
{ "float4", OutputType::FLOAT4 },
{ "int", OutputType::INT },
{ "int2", OutputType::INT2 },
{ "int3", OutputType::INT3 },
{ "int4", OutputType::INT4 },
{ "uint", OutputType::UINT },
{ "uint2", OutputType::UINT2 },
{ "uint3", OutputType::UINT3 },
{ "uint4", OutputType::UINT4 }
};
template <>
+32
View File
@@ -950,6 +950,38 @@ bool GLSLPostProcessor::fullOptimization(const TShader& tShader,
if (found != std::string::npos) {
str.replace(found, clipDistanceDefinition.length(), "");
}
// Validate the transpiled ESSL1 shader dynamically before considering it successful.
// This proactively catches unsupported SPIR-V -> ESSL1 translation quirks (like textureLod)
// at compile-time since we can't easily test all variants on physical GLES 2.0 devices.
if (config.featureLevel == 0) {
// preampitively forbid spirv-cross from cheating and polyfilling disabled features
auto const& exts = glslCompiler.get_required_extensions();
for (auto const& ext : exts) {
if (ext != "GL_OES_standard_derivatives" &&
ext != "GL_OES_EGL_image_external" &&
ext != "GL_EXT_shader_framebuffer_fetch" &&
ext != "GL_EXT_shader_framebuffer_fetch_non_coherent") {
slog.e << "ERROR: Feature Level 0 shaders cannot require: " << ext << ". "
<< "spirv-cross attempted to unilaterally inject it." << io::endl;
return false;
}
}
TShader validateShader(internalConfig.shLang);
// The cleaner must be declared after the TShader to manage the glslang memory pool
// teardown order correctly and safely destroy the AST.
GLSLangCleaner const validateCleaner;
const char* shaderCString = str.c_str();
validateShader.setStrings(&shaderCString, 1);
bool const validateOk = validateShader.parse(&DefaultTBuiltInResource, glslOptions.version, false, EShMsgDefault);
if (!validateOk) {
slog.e << "ESSL1 Validation failed:\n" << validateShader.getInfoLog() << io::endl;
return false;
}
}
}
return true;
}
+54 -16
View File
@@ -131,7 +131,7 @@ void MaterialBuilderBase::prepare(bool const vulkanSemantics,
// OpenGL is a special case. If we're doing any optimization, then we need to go to Spir-V.
TargetLanguage glTargetLanguage = mOptimization > Optimization::PREPROCESSOR ?
TargetLanguage::SPIRV : TargetLanguage::GLSL;
if (vulkanSemantics) {
if (vulkanSemantics || featureLevel == backend::FeatureLevel::FEATURE_LEVEL_0) {
// Currently GLSLPostProcessor.cpp is incapable of compiling SPIRV to GLSL without
// running the optimizer. For now we just activate the optimizer in that case.
mOptimization = Optimization::PERFORMANCE;
@@ -167,7 +167,7 @@ void MaterialBuilderBase::prepare(bool const vulkanSemantics,
&& featureLevel == backend::FeatureLevel::FEATURE_LEVEL_0
&& shaderModel == ShaderModel::MOBILE) {
mCodeGenPermutations.push_back({
shaderModel,
ShaderModel::MOBILE,
TargetApi::OPENGL,
glTargetLanguage,
backend::FeatureLevel::FEATURE_LEVEL_0
@@ -938,6 +938,35 @@ bool MaterialBuilder::generateShaders(JobSystem& jobSystem, const std::vector<Va
JobSystem::Job* parent = jobSystem.createJob();
for (const auto& v : variants) {
if (params.featureLevel == FeatureLevel::FEATURE_LEVEL_0) {
assert_invariant(params.shaderModel == ShaderModel::MOBILE);
assert_invariant(params.targetApi == TargetApi::OPENGL);
// skip all variants that can't be used with ESSL1
if (filament::Variant::isValidStandardVariant(v.variant)) {
if (v.variant.hasDirectionalLighting()) {
continue;
}
if (v.variant.hasDynamicLighting()) {
continue;
}
}
if (filament::Variant::isShadowReceiverVariant(v.variant)) {
continue;
}
if (filament::Variant::isStereoVariant(v.variant)) {
continue;
}
if (filament::Variant::isDepthMomentsVariant(v.variant)) {
continue;
}
if (filament::Variant::isShadowSampler2DVariant(v.variant)) {
continue;
}
if (filament::Variant::isSSRVariant(v.variant)) {
continue;
}
}
JobSystem::Job* job = jobs::createJob(jobSystem, parent, [&]() {
if (cancelJobs.load()) {
return;
@@ -1001,8 +1030,9 @@ bool MaterialBuilder::generateShaders(JobSystem& jobSystem, const std::vector<Va
return "unknown";
};
char filename[256];
snprintf(filename, sizeof(filename), "%s_0x%02x.%s", mMaterialName.c_str_safe(),
variantKey, getExtension(v.stage));
snprintf(filename, sizeof(filename), "%s_0x%02x_fl%d.%s",
mMaterialName.c_str_safe(), variantKey, (int) featureLevel,
getExtension(v.stage));
printf("Writing variant 0x%02x to %s\n", variantKey, filename);
std::ofstream file(filename);
if (file.is_open()) {
@@ -1273,6 +1303,25 @@ error:
mShading = Shading::UNLIT;
}
if (mMaterialDomain == MaterialDomain::SURFACE && !mOutputs.empty()) {
if (mFeatureLevel == FeatureLevel::FEATURE_LEVEL_0) {
LOG(ERROR) << "Error: feature level 0 does not support custom outputs.";
goto error;
}
if (mOutputs.size() > 1) {
LOG(ERROR) << "Error: surface materials only support a maximum of 1 custom output.";
goto error;
}
if (mShading != Shading::UNLIT) {
LOG(ERROR) << "Error: custom outputs are only supported for unlit surface materials.";
goto error;
}
if (mBlendingMode != BlendingMode::OPAQUE) {
LOG(ERROR) << "Error: surface materials with custom outputs must use opaque blending.";
goto error;
}
}
// Add a default color output.
if (mMaterialDomain == MaterialDomain::POST_PROCESS && mOutputs.empty()) {
output(VariableQualifier::OUT,
@@ -1319,8 +1368,7 @@ error:
CodeGenParams const semanticCodeGenParams = {
.shaderModel = ShaderModel::MOBILE,
.targetApi = TargetApi::OPENGL,
.targetLanguage = (info.featureLevel == FeatureLevel::FEATURE_LEVEL_0) ?
TargetLanguage::GLSL : TargetLanguage::SPIRV,
.targetLanguage = TargetLanguage::SPIRV,
.featureLevel = info.featureLevel,
};
@@ -1332,16 +1380,6 @@ error:
goto error;
}
// adjust variant-filter for feature level *before* we start writing into the container
if (mFeatureLevel == FeatureLevel::FEATURE_LEVEL_0) {
// at feature level 0, many variants are not supported
mVariantFilter |= uint32_t(UserVariantFilterBit::DIRECTIONAL_LIGHTING);
mVariantFilter |= uint32_t(UserVariantFilterBit::DYNAMIC_LIGHTING);
mVariantFilter |= uint32_t(UserVariantFilterBit::SHADOW_RECEIVER);
mVariantFilter |= uint32_t(UserVariantFilterBit::VSM);
mVariantFilter |= uint32_t(UserVariantFilterBit::SSR);
}
// Create chunk tree.
ChunkContainer container;
writeCommonChunks(container, info);
+1
View File
@@ -18,6 +18,7 @@
#include "ShaderMinifier.h"
#include <utils/Log.h>
#include <cstring>
namespace filamat {
+2 -1
View File
@@ -24,7 +24,8 @@ namespace filamat {
// when the container is about to be flattened.
size_t ChunkContainer::getSize() const {
return flatten(Flattener::getDryRunner());
auto flattener = Flattener::getDryRunner();
return flatten(flattener);
}
size_t ChunkContainer::flatten(Flattener& f) const {
+2 -8
View File
@@ -48,14 +48,8 @@ public:
// flattening. If we set mStart = nullptr and mEnd=nullptr, we would hit an error about
// offsetting on null when ubsan is enabled. Instead we point mStart to a fake address, and
// mCursor is offset from that.
static Flattener& getDryRunner() {
static Flattener dryRunner = Flattener(nullptr);
dryRunner.mStart = (uint8_t*) FAKE_DRY_RUNNER_START_ADDR;
dryRunner.mCursor = (uint8_t*) FAKE_DRY_RUNNER_START_ADDR;
dryRunner.mOffsetPlaceholders.clear();
dryRunner.mSizePlaceholders.clear();
dryRunner.mValuePlaceholders.clear();
return dryRunner;
static Flattener getDryRunner() {
return Flattener((uint8_t*) FAKE_DRY_RUNNER_START_ADDR);
}
bool isDryRunner() {
+13 -66
View File
@@ -202,36 +202,6 @@ utils::io::sstream& CodeGenerator::generateCommonProlog(utils::io::sstream& out,
out << "#define FILAMENT_HAS_FEATURE_INSTANCING\n";
}
// During compilation and optimization, __VERSION__ reflects the shader language version of the
// intermediate code, not the version of the final code. spirv-cross automatically adapts
// certain language features (e.g. fragment output) but leaves others untouched (e.g. sampler
// functions, bit shift operations). Client code may have to make decisions based on this
// information, so define a FILAMENT_EFFECTIVE_VERSION constant.
const char *effective_version;
if (mTargetLanguage == TargetLanguage::GLSL) {
effective_version = "__VERSION__";
} else {
switch (mShaderModel) {
case ShaderModel::MOBILE:
if (mFeatureLevel >= FeatureLevel::FEATURE_LEVEL_1) {
effective_version = "300";
} else {
effective_version = "100";
}
break;
case ShaderModel::DESKTOP:
if (mFeatureLevel >= FeatureLevel::FEATURE_LEVEL_2) {
effective_version = "450";
} else {
effective_version = "410";
}
break;
default:
assert(false);
}
}
generateDefine(out, "FILAMENT_EFFECTIVE_VERSION", effective_version);
switch (material.stereoscopicType) {
case StereoscopicType::INSTANCED:
generateDefine(out, "FILAMENT_STEREO_INSTANCED", true);
@@ -253,17 +223,11 @@ utils::io::sstream& CodeGenerator::generateCommonProlog(utils::io::sstream& out,
generateDefine(out, "MATERIAL_HAS_CUSTOM_DEPTH", material.userMaterialHasCustomDepth);
}
if (mTargetLanguage == TargetLanguage::SPIRV ||
mFeatureLevel >= FeatureLevel::FEATURE_LEVEL_1) {
if (stage == ShaderStage::VERTEX) {
generateDefine(out, "VARYING", "out");
generateDefine(out, "ATTRIBUTE", "in");
} else if (stage == ShaderStage::FRAGMENT) {
generateDefine(out, "VARYING", "in");
}
} else {
generateDefine(out, "VARYING", "varying");
generateDefine(out, "ATTRIBUTE", "attribute");
if (stage == ShaderStage::VERTEX) {
generateDefine(out, "VARYING", "out");
generateDefine(out, "ATTRIBUTE", "in");
} else if (stage == ShaderStage::FRAGMENT) {
generateDefine(out, "VARYING", "in");
}
auto getShadingDefine = [](Shading shading) -> const char* {
@@ -361,31 +325,6 @@ utils::io::sstream& CodeGenerator::generateCommonProlog(utils::io::sstream& out,
out << '\n';
out << SHADERS_COMMON_DEFINES_GLSL_DATA;
if (material.featureLevel == FeatureLevel::FEATURE_LEVEL_0 &&
(mFeatureLevel > FeatureLevel::FEATURE_LEVEL_0
|| mTargetLanguage == TargetLanguage::SPIRV)) {
// Insert compatibility definitions for ESSL 1.0 functions which were removed in ESSL 3.0.
// This is the minimum required value according to the OpenGL ES Shading Language Version
// 1.00 document. glslang forbids defining symbols beginning with gl_ as const, hence the
// #define.
generateDefine(out, "gl_MaxVaryingVectors", "8");
generateDefine(out, "texture2D", "texture");
generateDefine(out, "texture2DProj", "textureProj");
generateDefine(out, "texture3D", "texture");
generateDefine(out, "texture3DProj", "textureProj");
generateDefine(out, "textureCube", "texture");
if (stage == ShaderStage::VERTEX) {
generateDefine(out, "texture2DLod", "textureLod");
generateDefine(out, "texture2DProjLod", "textureProjLod");
generateDefine(out, "texture3DLod", "textureLod");
generateDefine(out, "texture3DProjLod", "textureProjLod");
generateDefine(out, "textureCubeLod", "textureLod");
}
}
// Api level enforcement.
generateDefine(out, "CLIENT_MATERIAL_API_LEVEL", apiLevel);
generateDefine(out, "UNSTABLE_MATERIAL_API_LEVEL", filament::UNSTABLE_MATERIAL_API_LEVEL);
@@ -1303,6 +1242,14 @@ char const* CodeGenerator::getOutputTypeName(MaterialBuilder::OutputType type) n
case MaterialBuilder::OutputType::FLOAT2: return "vec2";
case MaterialBuilder::OutputType::FLOAT3: return "vec3";
case MaterialBuilder::OutputType::FLOAT4: return "vec4";
case MaterialBuilder::OutputType::INT: return "int";
case MaterialBuilder::OutputType::INT2: return "ivec2";
case MaterialBuilder::OutputType::INT3: return "ivec3";
case MaterialBuilder::OutputType::INT4: return "ivec4";
case MaterialBuilder::OutputType::UINT: return "uint";
case MaterialBuilder::OutputType::UINT2: return "uvec2";
case MaterialBuilder::OutputType::UINT3: return "uvec3";
case MaterialBuilder::OutputType::UINT4: return "uvec4";
}
}
+31 -11
View File
@@ -250,7 +250,9 @@ void ShaderGenerator::generateVertexDomainDefines(io::sstream& out, VertexDomain
}
void ShaderGenerator::generatePostProcessMaterialVariantDefines(io::sstream& out,
PostProcessVariant const variant) noexcept {
ShaderStage const, MaterialBuilder::FeatureLevel const featureLevel,
MaterialInfo const&, PostProcessVariant const variant) noexcept {
CodeGenerator::generateDefine(out, "MATERIAL_FEATURE_LEVEL", uint32_t(featureLevel));
switch (variant) {
case PostProcessVariant::OPAQUE:
CodeGenerator::generateDefine(out, "POST_PROCESS_OPAQUE", 1u);
@@ -605,6 +607,16 @@ std::string ShaderGenerator::createSurfaceFragmentProgram(ShaderModel const shad
+PerMaterialBindingPoints::MATERIAL_PARAMS,
material.uib);
if (!filament::Variant::isValidDepthVariant(variant)) {
if (!mOutputs.empty()) {
CodeGenerator::generateDefine(fs, "HAS_CUSTOM_OUTPUT", 1u);
for (const auto& output: mOutputs) {
cg.generateOutput(fs, ShaderStage::FRAGMENT, output.name, output.location,
output.qualifier, output.precision, output.type);
}
}
}
CodeGenerator::generateSeparator(fs);
if (featureLevel >= FeatureLevel::FEATURE_LEVEL_1) {
@@ -687,6 +699,8 @@ std::string ShaderGenerator::createSurfaceComputeProgram(ShaderModel const shade
generateUserSpecConstants(cg, s, mConstants);
CodeGenerator::generateDefine(s, "MATERIAL_FEATURE_LEVEL", uint32_t(featureLevel));
CodeGenerator::generateSurfaceTypes(s, ShaderStage::COMPUTE);
cg.generateUniforms(s, ShaderStage::COMPUTE,
@@ -737,7 +751,8 @@ std::string ShaderGenerator::createPostProcessVertexProgram(ShaderModel const sm
}
CodeGenerator::generatePostProcessInputs(vs, ShaderStage::VERTEX);
generatePostProcessMaterialVariantDefines(vs, PostProcessVariant(variantKey));
generatePostProcessMaterialVariantDefines(vs, ShaderStage::VERTEX,
featureLevel, material, PostProcessVariant(variantKey));
cg.generateUniforms(vs, ShaderStage::VERTEX,
DescriptorSetBindingPoints::PER_VIEW,
@@ -765,14 +780,16 @@ std::string ShaderGenerator::createPostProcessVertexProgram(ShaderModel const sm
std::string ShaderGenerator::createPostProcessFragmentProgram(ShaderModel const sm,
MaterialBuilder::TargetApi const targetApi, MaterialBuilder::TargetLanguage const targetLanguage,
MaterialBuilder::FeatureLevel const featureLevel,
MaterialInfo const& material, uint8_t variant, uint32_t apiLevel) const noexcept {
MaterialInfo const& material, filament::Variant::type_t variantKey,
uint32_t apiLevel) const noexcept {
const CodeGenerator cg(sm, targetApi, targetLanguage, featureLevel);
io::sstream fs;
cg.generateCommonProlog(fs, ShaderStage::FRAGMENT, material, {}, apiLevel);
generateUserSpecConstants(cg, fs, mConstants);
generatePostProcessMaterialVariantDefines(fs, PostProcessVariant(variant));
generatePostProcessMaterialVariantDefines(fs, ShaderStage::FRAGMENT,
featureLevel, material, PostProcessVariant(variantKey));
// custom material variables
size_t variableIndex = 0;
@@ -801,13 +818,16 @@ std::string ShaderGenerator::createPostProcessFragmentProgram(ShaderModel const
CodeGenerator::generatePostProcessGetters(fs, ShaderStage::FRAGMENT);
// Generate post-process outputs.
for (const auto& output : mOutputs) {
if (output.target == MaterialBuilder::OutputTarget::COLOR) {
cg.generateOutput(fs, ShaderStage::FRAGMENT, output.name, output.location,
output.qualifier, output.precision, output.type);
}
if (output.target == MaterialBuilder::OutputTarget::DEPTH) {
CodeGenerator::generateDefine(fs, "FRAG_OUTPUT_DEPTH", 1u);
if (!mOutputs.empty()) {
CodeGenerator::generateDefine(fs, "HAS_CUSTOM_OUTPUT", 1u);
for (const auto& output: mOutputs) {
if (output.target == MaterialBuilder::OutputTarget::COLOR) {
cg.generateOutput(fs, ShaderStage::FRAGMENT, output.name, output.location,
output.qualifier, output.precision, output.type);
}
if (output.target == MaterialBuilder::OutputTarget::DEPTH) {
CodeGenerator::generateDefine(fs, "FRAG_OUTPUT_DEPTH", 1u);
}
}
}
+5 -2
View File
@@ -102,7 +102,9 @@ private:
MaterialInfo const& material, filament::Variant variant) noexcept;
static void generatePostProcessMaterialVariantDefines(utils::io::sstream& out,
filament::PostProcessVariant variant) noexcept;
filament::backend::ShaderStage stage,
MaterialBuilder::FeatureLevel featureLevel,
MaterialInfo const& material, filament::PostProcessVariant variant) noexcept;
static void generateUserSpecConstants(
const CodeGenerator& cg, utils::io::sstream& fs,
@@ -117,7 +119,8 @@ private:
std::string createPostProcessFragmentProgram(filament::backend::ShaderModel sm,
MaterialBuilder::TargetApi targetApi, MaterialBuilder::TargetLanguage targetLanguage,
MaterialBuilder::FeatureLevel featureLevel,
MaterialInfo const& material, uint8_t variant, uint32_t apiLevel) const noexcept;
MaterialInfo const& material, filament::Variant::type_t variantKey,
uint32_t apiLevel) const noexcept;
static void appendShader(utils::io::sstream& ss,
const utils::CString& shader, size_t lineOffset) noexcept;
+82 -3
View File
@@ -923,7 +923,7 @@ TEST_F(MaterialCompiler, FeatureLevel0Sampler2D) {
std::string shaderCode(R"(
void material(inout MaterialInputs material) {
prepareMaterial(material);
material.baseColor = texture2D(materialParams_sampler, vec2(0.0, 0.0));
material.baseColor = texture(materialParams_sampler, vec2(0.0, 0.0));
}
)");
filamat::MaterialBuilder builder;
@@ -941,7 +941,7 @@ TEST_F(MaterialCompiler, SamplerTransformName) {
void material(inout MaterialInputs material) {
prepareMaterial(material);
vec3 uvw = materialParams.sampler_transform * vec3(0.0, 0.0, 0.0);
material.baseColor = texture2D(materialParams_sampler, uvw.xy);
material.baseColor = texture(materialParams_sampler, uvw.xy);
}
)");
filamat::MaterialBuilder builder;
@@ -960,7 +960,7 @@ TEST_F(MaterialCompiler, SamplerMissingTransformName) {
void material(inout MaterialInputs material) {
prepareMaterial(material);
vec3 uvw = materialParams.sampler_transform * vec3(0.0, 0.0, 0.0);
material.baseColor = texture2D(materialParams_sampler, uvw.xy);
material.baseColor = texture(materialParams_sampler, uvw.xy);
}
)");
filamat::MaterialBuilder builder;
@@ -1061,6 +1061,85 @@ TEST_F(MaterialCompiler, ClientApiLevelReleasedUseOfUnstableApiFails) {
EXPECT_FALSE(result.isValid());
}
TEST_F(MaterialCompiler, CustomOutputSuccess) {
std::string shaderCode(R"(
void material(inout MaterialInputs material) {
prepareMaterial(material);
material.customOutput = uvec4(1, 2, 3, 4);
}
)");
filamat::MaterialBuilder builder;
builder.material(shaderCode.c_str());
builder.shading(filament::Shading::UNLIT);
builder.blending(filament::BlendingMode::OPAQUE);
builder.featureLevel(FeatureLevel::FEATURE_LEVEL_1);
builder.output(filamat::MaterialBuilder::VariableQualifier::OUT,
filamat::MaterialBuilder::OutputTarget::COLOR,
filamat::MaterialBuilder::Precision::DEFAULT,
filamat::MaterialBuilder::OutputType::UINT4,
"customOutput");
filamat::Package result = builder.build(*jobSystem);
EXPECT_TRUE(result.isValid());
}
TEST_F(MaterialCompiler, CustomOutputFeatureLevel0Fails) {
filamat::MaterialBuilder builder;
builder.featureLevel(FeatureLevel::FEATURE_LEVEL_0);
builder.output(filamat::MaterialBuilder::VariableQualifier::OUT,
filamat::MaterialBuilder::OutputTarget::COLOR,
filamat::MaterialBuilder::Precision::DEFAULT,
filamat::MaterialBuilder::OutputType::UINT4,
"customOutput");
filamat::Package result = builder.build(*jobSystem);
EXPECT_FALSE(result.isValid());
}
TEST_F(MaterialCompiler, CustomOutputMoreThanOneFails) {
filamat::MaterialBuilder builder;
builder.featureLevel(FeatureLevel::FEATURE_LEVEL_1);
builder.output(filamat::MaterialBuilder::VariableQualifier::OUT,
filamat::MaterialBuilder::OutputTarget::COLOR,
filamat::MaterialBuilder::Precision::DEFAULT,
filamat::MaterialBuilder::OutputType::UINT4,
"customOutput");
builder.output(filamat::MaterialBuilder::VariableQualifier::OUT,
filamat::MaterialBuilder::OutputTarget::COLOR,
filamat::MaterialBuilder::Precision::DEFAULT,
filamat::MaterialBuilder::OutputType::UINT4,
"customOutput1");
filamat::Package result = builder.build(*jobSystem);
EXPECT_FALSE(result.isValid());
}
TEST_F(MaterialCompiler, CustomOutputLitFails) {
filamat::MaterialBuilder builder;
builder.featureLevel(FeatureLevel::FEATURE_LEVEL_1);
builder.shading(filament::Shading::LIT);
builder.output(filamat::MaterialBuilder::VariableQualifier::OUT,
filamat::MaterialBuilder::OutputTarget::COLOR,
filamat::MaterialBuilder::Precision::DEFAULT,
filamat::MaterialBuilder::OutputType::UINT4,
"customOutput");
filamat::Package result = builder.build(*jobSystem);
EXPECT_FALSE(result.isValid());
}
TEST_F(MaterialCompiler, CustomOutputTransparentFails) {
filamat::MaterialBuilder builder;
builder.featureLevel(FeatureLevel::FEATURE_LEVEL_1);
builder.shading(filament::Shading::UNLIT);
builder.blending(filament::BlendingMode::TRANSPARENT);
builder.output(filamat::MaterialBuilder::VariableQualifier::OUT,
filamat::MaterialBuilder::OutputTarget::COLOR,
filamat::MaterialBuilder::Precision::DEFAULT,
filamat::MaterialBuilder::OutputType::UINT4,
"customOutput");
filamat::Package result = builder.build(*jobSystem);
EXPECT_FALSE(result.isValid());
}
#if FILAMENT_SUPPORTS_WEBGPU
TEST_F(MaterialCompiler, WgslConversionBakedColor) {
std::string bakedColorCodeFrag(R"(
+5 -1
View File
@@ -67,7 +67,11 @@ void ArchiveCache::load(const void* archiveData, uint64_t archiveByteCount) {
uint64_t* basePointer = (uint64_t*) utils::aligned_alloc(decompSize, 8);
ZSTD_decompress(basePointer, decompSize, archiveData, archiveByteCount);
mArchive = (ReadableArchive*) basePointer;
convertOffsetsToPointers(mArchive);
if (!convertOffsetsToPointers(mArchive, decompSize)) {
utils::aligned_free(basePointer);
mArchive = nullptr;
return;
}
mMaterials = FixedCapacityVector<Material*>(mArchive->specsCount, nullptr);
}
+124 -6
View File
@@ -24,6 +24,7 @@
#include "Utility.h"
#include "extended/ResourceLoaderExtended.h"
#include <limits>
#include <filament/BufferObject.h>
#include <filament/Engine.h>
#include <filament/IndexBuffer.h>
@@ -163,9 +164,40 @@ inline void normalizeSkinningWeights(cgltf_data const* gltf) {
LOG(WARNING) << "Cannot normalize weights, unsupported attribute type.";
return;
}
if (!data->buffer_view || !data->buffer_view->buffer ||
!data->buffer_view->buffer->data) {
LOG(WARNING) << "Cannot normalize weights, missing buffer data.";
return;
}
const cgltf_size bufferSize = data->buffer_view->buffer->size;
const cgltf_size viewOffset = data->buffer_view->offset;
const cgltf_size accessorOffset = data->offset;
const cgltf_size totalOffset = viewOffset + accessorOffset;
if (totalOffset >= bufferSize) {
LOG(WARNING) << "Cannot normalize weights, accessor offset exceeds buffer size.";
return;
}
const cgltf_size availableBytes = bufferSize - totalOffset;
const cgltf_size stride = data->stride;
cgltf_size maxCount = 0;
if (stride > 0 && availableBytes >= sizeof(float4)) {
maxCount = 1 + (availableBytes - sizeof(float4)) / stride;
}
cgltf_size safeCount = data->count;
if (safeCount > maxCount) {
LOG(WARNING) << "Skinning weights accessor count (" << safeCount
<< ") exceeds buffer capacity (" << maxCount
<< "), clamping to prevent out-of-bounds access.";
safeCount = maxCount;
}
uint8_t* bytes = (uint8_t*) data->buffer_view->buffer->data;
bytes += data->offset + data->buffer_view->offset;
for (cgltf_size i = 0, n = data->count; i < n; ++i, bytes += data->stride) {
bytes += totalOffset;
for (cgltf_size i = 0, n = safeCount; i < n; ++i, bytes += stride) {
float4* weights = (float4*) bytes;
const float sum = weights->x + weights->y + weights->z + weights->w;
*weights /= sum;
@@ -243,8 +275,20 @@ inline void uploadBuffers(FFilamentAsset* asset, Engine& engine,
// For morph targets without buffer_view, use cgltf_accessor_unpack_floats to unpack data directly
if (!accessor->buffer_view && isMorphTarget) {
const size_t floatsCount = accessor->count * cgltf_num_components(accessor->type);
const size_t components = cgltf_num_components(accessor->type);
if (components > 0 && accessor->count > std::numeric_limits<size_t>::max() / components) {
continue;
}
const size_t floatsCount = accessor->count * components;
if (floatsCount == 0) {
continue;
}
if (floatsCount > std::numeric_limits<size_t>::max() / sizeof(float)) {
continue;
}
const size_t floatsByteCount = sizeof(float) * floatsCount;
float* floatsData = (float*)malloc(floatsByteCount);
cgltf_accessor_unpack_floats(accessor, floatsData, floatsCount);
@@ -273,10 +317,48 @@ inline void uploadBuffers(FFilamentAsset* asset, Engine& engine,
}
assert_invariant(bufferData);
const uint32_t size = utility::computeBindingSize(accessor);
if (size == 0) {
// Skip buffer upload for this accessor as requested by user
continue;
}
if (slot.vertexBuffer) {
if (utility::requiresConversion(accessor)) {
const size_t floatsCount = accessor->count * cgltf_num_components(accessor->type);
const cgltf_size bufferSize = accessor->buffer_view->buffer->size;
const cgltf_size totalOffset = accessor->buffer_view->offset + accessor->offset;
if (totalOffset >= bufferSize) {
continue;
}
const cgltf_size availableBytes = bufferSize - totalOffset;
const cgltf_size stride = accessor->stride;
const cgltf_size elementSize = cgltf_calc_size(accessor->type, accessor->component_type);
cgltf_size maxCount = 0;
if (stride > 0 && availableBytes >= elementSize) {
maxCount = 1 + (availableBytes - elementSize) / stride;
}
cgltf_size safeCount = accessor->count;
if (safeCount > maxCount) {
LOG(WARNING) << "Accessor count exceeds buffer capacity, clamping.";
safeCount = maxCount;
}
const size_t components = cgltf_num_components(accessor->type);
if (components > 0 && safeCount > std::numeric_limits<size_t>::max() / components) {
continue;
}
const size_t floatsCount = safeCount * components;
if (floatsCount == 0) {
continue;
}
if (floatsCount > std::numeric_limits<size_t>::max() / sizeof(float)) {
continue;
}
const size_t floatsByteCount = sizeof(float) * floatsCount;
float* floatsData = (float*) malloc(floatsByteCount);
cgltf_accessor_unpack_floats(accessor, floatsData, floatsCount);
BufferObject* bo = BufferObject::Builder().size(floatsByteCount).build(engine);
@@ -313,8 +395,42 @@ inline void uploadBuffers(FFilamentAsset* asset, Engine& engine,
assert(slot.morphTargetBuffer);
if (utility::requiresPacking(accessor)) {
const size_t floatsCount = accessor->count * cgltf_num_components(accessor->type);
const cgltf_size bufferSize = accessor->buffer_view->buffer->size;
const cgltf_size totalOffset = accessor->buffer_view->offset + accessor->offset;
if (totalOffset >= bufferSize) {
continue;
}
const cgltf_size availableBytes = bufferSize - totalOffset;
const cgltf_size stride = accessor->stride;
const cgltf_size elementSize = cgltf_calc_size(accessor->type, accessor->component_type);
cgltf_size maxCount = 0;
if (stride > 0 && availableBytes >= elementSize) {
maxCount = 1 + (availableBytes - elementSize) / stride;
}
cgltf_size safeCount = accessor->count;
if (safeCount > maxCount) {
LOG(WARNING) << "Accessor count exceeds buffer capacity, clamping.";
safeCount = maxCount;
}
const size_t components = cgltf_num_components(accessor->type);
if (components > 0 && safeCount > std::numeric_limits<size_t>::max() / components) {
continue;
}
const size_t floatsCount = safeCount * components;
if (floatsCount == 0) {
continue;
}
if (floatsCount > std::numeric_limits<size_t>::max() / sizeof(float)) {
continue;
}
const size_t floatsByteCount = sizeof(float) * floatsCount;
float* floatsData = (float*) malloc(floatsByteCount);
cgltf_accessor_unpack_floats(accessor, floatsData, floatsCount);
if (accessor->type == cgltf_type_vec3) {
@@ -467,7 +583,9 @@ bool ResourceLoader::loadResources(FFilamentAsset* asset, bool async) {
}
utility::decodeDracoMeshes(gltf, prim, dracoCache);
}
utility::decodeMeshoptCompression((cgltf_data*) gltf);
if (!utility::decodeMeshoptCompression((cgltf_data*) gltf)) {
return false;
}
uploadBuffers(asset, *pImpl->mEngine, pImpl->mUriDataCache);
+17 -3
View File
@@ -87,7 +87,7 @@ void decodeDracoMeshes(cgltf_data const* gltf, cgltf_primitive const* prim,
}
}
void decodeMeshoptCompression(cgltf_data* data) {
bool decodeMeshoptCompression(cgltf_data* data) {
for (size_t i = 0; i < data->buffer_views_count; ++i) {
if (!data->buffer_views[i].has_meshopt_compression) {
continue;
@@ -97,6 +97,11 @@ void decodeMeshoptCompression(cgltf_data* data) {
assert_invariant(source);
source += compression->offset;
if (compression->stride == 0) {
slog.e << "gltfio: meshopt decompression failed, stride is 0." << io::endl;
return false;
}
size_t const theoreticalMaxCount = std::numeric_limits<size_t>::max() / compression->stride;
FILAMENT_CHECK_PRECONDITION(compression->count <= theoreticalMaxCount)
<< "gltfio: meshopt decompression exceeds maximum count of " << theoreticalMaxCount
@@ -107,7 +112,7 @@ void decodeMeshoptCompression(cgltf_data* data) {
void* destination = malloc(compression->count * compression->stride);
assert_invariant(destination);
UTILS_UNUSED_IN_RELEASE int error = 0;
int error = 0;
switch (compression->mode) {
case cgltf_meshopt_compression_mode_invalid:
break;
@@ -127,7 +132,12 @@ void decodeMeshoptCompression(cgltf_data* data) {
assert_invariant(false);
break;
}
assert_invariant(!error);
if (error != 0) {
slog.e << "gltfio: meshopt decompression failed with error " << error << io::endl;
free(destination);
return false;
}
switch (compression->filter) {
case cgltf_meshopt_compression_filter_none:
@@ -148,6 +158,7 @@ void decodeMeshoptCompression(cgltf_data* data) {
data->buffer_views[i].data = destination;
}
return true;
}
bool primitiveHasVertexColor(cgltf_primitive* inPrim) {
@@ -167,6 +178,9 @@ bool primitiveHasVertexColor(cgltf_primitive* inPrim) {
// exist in the glTF we need to compute it manually. This is a bit of a cheat, cgltf_calc_size is
// private but its implementation file is available in this cpp file.
uint32_t computeBindingSize(cgltf_accessor const* accessor) {
if (accessor->count == 0) {
return 0;
}
cgltf_size element_size = cgltf_calc_size(accessor->type, accessor->component_type);
return uint32_t(accessor->stride * (accessor->count - 1) + element_size);
}
+1 -1
View File
@@ -44,7 +44,7 @@ namespace filament::gltfio::utility {
// Functions that are shared between the original implementation and the extended implementation.
void decodeDracoMeshes(cgltf_data const* gltf, cgltf_primitive const* prim, DracoCache* dracoCache);
void decodeMeshoptCompression(cgltf_data* data);
bool decodeMeshoptCompression(cgltf_data* data);
bool primitiveHasVertexColor(cgltf_primitive* inPrim);
uint32_t computeBindingSize(cgltf_accessor const* accessor);
void convertBytesToShorts(uint16_t* dst, uint8_t const* src, size_t count);
+35 -20
View File
@@ -16,11 +16,18 @@
#include <image/Ktx1Bundle.h>
#include <utils/Logger.h>
#include <utils/Panic.h>
#include <utils/string.h>
#include <math/vec3.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <limits>
#include <new>
#include <string>
#include <unordered_map>
#include <vector>
@@ -39,7 +46,7 @@ struct SerializationHeader {
static_assert(sizeof(SerializationHeader) == 16 * 4, "Unexpected header size.");
// We flatten the three-dimensional blob index using the ordering defined by the KTX spec.
inline size_t flatten(const image::Ktx1Bundle* bundle, image::KtxBlobIndex index) {
size_t flatten(const image::Ktx1Bundle* bundle, image::KtxBlobIndex index) {
const uint32_t nfaces = bundle->isCubemap() ? 6 : 1;
const uint32_t nlayers = bundle->getArrayLength();
return index.cubeFace + index.arrayIndex * nfaces + index.mipLevel * nfaces * nlayers;
@@ -83,7 +90,7 @@ struct KtxBlobList {
postSize += sizes[i];
}
}
uint32_t oldSize = sizes[blobIndex];
uint32_t const oldSize = sizes[blobIndex];
std::vector<uint8_t> newBlobs(blobs.size() + newSize - oldSize);
uint8_t const* src = blobs.data();
uint8_t* dst = newBlobs.data();
@@ -102,19 +109,27 @@ struct KtxBlobList {
Ktx1Bundle::~Ktx1Bundle() = default;
Ktx1Bundle::Ktx1Bundle(uint32_t numMipLevels, uint32_t arrayLength, bool isCubemap) :
mBlobs(new KtxBlobList), mMetadata(new KtxMetadata) {
mBlobs(new(std::nothrow) KtxBlobList), mMetadata(new(std::nothrow) KtxMetadata) {
uint64_t const totalBlobs = uint64_t(numMipLevels) * arrayLength * (isCubemap ? 6 : 1);
if (totalBlobs > uint64_t(std::numeric_limits<uint32_t>::max())) {
LOG(WARNING) << "KTX dimensions overflow";
mNumMipLevels = 0;
mArrayLength = 0;
mNumCubeFaces = 0;
return;
}
mNumMipLevels = numMipLevels;
mArrayLength = arrayLength;
mNumCubeFaces = isCubemap ? 6 : 1;
mBlobs->sizes.resize(numMipLevels * arrayLength * mNumCubeFaces);
mBlobs->sizes.resize(uint32_t(totalBlobs));
}
Ktx1Bundle::Ktx1Bundle(uint8_t const* bytes, uint32_t nbytes) :
mBlobs(new KtxBlobList), mMetadata(new KtxMetadata) {
mBlobs(new(std::nothrow) KtxBlobList), mMetadata(new(std::nothrow) KtxMetadata) {
FILAMENT_CHECK_POSTCONDITION(sizeof(SerializationHeader) <= nbytes) << "KTX buffer is too small";
// First, "parse" the header by casting it to a struct.
SerializationHeader const* header = (SerializationHeader const*) bytes;
SerializationHeader const* header = reinterpret_cast<SerializationHeader const*>(bytes);
FILAMENT_CHECK_POSTCONDITION(memcmp(header->magic, MAGIC, 12) == 0)
<< "KTX has unexpected identifier";
mInfo = header->info;
@@ -149,12 +164,12 @@ Ktx1Bundle::Ktx1Bundle(uint8_t const* bytes, uint32_t nbytes) :
size_t const keyLength = keyEnd - keyStart;
FILAMENT_CHECK_POSTCONDITION(keyLength < keyAndValueByteSize) << "KTX metadata key is not null terminated";
std::string key(keyStart, keyLength);
std::string const key(keyStart, keyLength);
uint8_t const* pval = pdata + keyLength + 1;
size_t const valLength = keyAndValueByteSize - (keyLength + 1);
pdata += keyAndValueByteSize;
std::string val((const char*) pval, valLength);
std::string const val(reinterpret_cast<const char*>(pval), valLength);
mMetadata->keyvals.insert({key, val});
const uint32_t paddingSize = 3 - ((keyAndValueByteSize + 3) % 4);
@@ -211,7 +226,7 @@ Ktx1Bundle::Ktx1Bundle(uint8_t const* bytes, uint32_t nbytes) :
}
bool Ktx1Bundle::serialize(uint8_t* destination, uint32_t numBytes) const {
uint32_t requiredLength = getSerializedLength();
uint32_t const requiredLength = getSerializedLength();
if (numBytes < requiredLength) {
return false;
}
@@ -264,15 +279,15 @@ bool Ktx1Bundle::serialize(uint8_t* destination, uint32_t numBytes) const {
for (uint32_t mipmap = 0; mipmap < mNumMipLevels; ++mipmap) {
// Every blob in a given miplevel has the same size, and each miplevel has at least one
// blob. Therefore we can safely determine each of the so-called "imageSize" fields in KTX
// blob. Therefore, we can safely determine each of the so-called "imageSize" fields in KTX
// by simply looking at the first blob in the LOD.
uint32_t faceSize;
uint8_t* blobData;
if (!getBlob({mipmap, 0, 0}, &blobData, &faceSize)) {
return false;
}
uint32_t imageSize = isNonArrayCube ? faceSize : (faceSize * facesPerMip);
*((uint32_t*) pdata) = imageSize;
uint32_t const imageSize = isNonArrayCube ? faceSize : (faceSize * facesPerMip);
*reinterpret_cast<uint32_t*>(pdata) = imageSize;
pdata += sizeof(imageSize);
// Copy out all layer and face blobs for this mipmap at once, since they are contiguous
@@ -300,15 +315,15 @@ uint32_t Ktx1Bundle::getSerializedLength() const {
FILAMENT_CHECK_POSTCONDITION(mBlobs->sizes[startIndex + i] == blobSize)
<< "Inconsistent blob sizes within LOD";
}
total += (uint64_t)blobSize * numElements;
total += uint64_t(blobSize) * numElements;
}
FILAMENT_CHECK_POSTCONDITION(total <= (uint64_t)std::numeric_limits<uint32_t>::max())
FILAMENT_CHECK_POSTCONDITION(total <= uint64_t(std::numeric_limits<uint32_t>::max()))
<< "KTX serialization size overflow";
return (uint32_t)total;
}
const char* Ktx1Bundle::getMetadata(const char* key, size_t* valueSize) const {
auto iter = mMetadata->keyvals.find(key);
auto const iter = mMetadata->keyvals.find(key);
if (iter == mMetadata->keyvals.end()) {
return nullptr;
}
@@ -346,8 +361,8 @@ bool Ktx1Bundle::getBlob(KtxBlobIndex index, uint8_t** data, uint32_t* size) con
index.cubeFace >= mNumCubeFaces) {
return false;
}
uint32_t flatIndex = flatten(this, index);
auto blobSize = mBlobs->sizes[flatIndex];
uint32_t const flatIndex = flatten(this, index);
auto const blobSize = mBlobs->sizes[flatIndex];
if (blobSize == 0) {
return false;
}
@@ -361,8 +376,8 @@ bool Ktx1Bundle::setBlob(KtxBlobIndex index, uint8_t const* data, uint32_t size)
index.cubeFace >= mNumCubeFaces) {
return false;
}
uint32_t flatIndex = flatten(this, index);
uint32_t blobSize = mBlobs->sizes[flatIndex];
uint32_t const flatIndex = flatten(this, index);
uint32_t const blobSize = mBlobs->sizes[flatIndex];
if (blobSize != size) {
mBlobs->resize(flatIndex, size);
}
@@ -375,7 +390,7 @@ bool Ktx1Bundle::allocateBlob(KtxBlobIndex index, uint32_t size) {
index.cubeFace >= mNumCubeFaces) {
return false;
}
uint32_t flatIndex = flatten(this, index);
uint32_t const flatIndex = flatten(this, index);
mBlobs->resize(flatIndex, size);
return true;
}
+15
View File
@@ -35,6 +35,7 @@
# include <arpa/inet.h>
#endif
#include <utils/Log.h>
#include <math/vec3.h>
#include <math/vec4.h>
@@ -373,6 +374,12 @@ LinearImage PSDDecoder::decode() {
uint32_t width = ntohl(h.width);
uint32_t height = ntohl(h.height);
// Limit dimensions to 30,000 as per PSD specification to prevent integer overflows
// during allocation in LinearImage.
if (width == 0 || height == 0 || width > 30000 || height > 30000) {
throw std::runtime_error("PSD dimensions exceed maximum allowed size (30,000)");
}
uint32_t length;
// color mode data section
@@ -394,6 +401,8 @@ LinearImage PSDDecoder::decode() {
throw std::runtime_error("compressed images are not supported");
}
// The multiplication width * height * 3 cannot overflow uint32_t because
// width and height are at most 30,000. (30000 * 30000 * 3 = 2.7e9 < 4.29e9)
LinearImage image(width, height, 3);
if (depth == 32) {
@@ -403,6 +412,9 @@ LinearImage PSDDecoder::decode() {
filament::math::float3& pixel =
*reinterpret_cast< filament::math::float3*>(image.getPixelRef(x, y));
pixel[i] = read32(mStream);
if (!mStream.good()) {
throw std::runtime_error("Truncated PSD file");
}
}
}
}
@@ -413,6 +425,9 @@ LinearImage PSDDecoder::decode() {
filament::math::float3& pixel =
*reinterpret_cast< filament::math::float3*>(image.getPixelRef(x, y));
pixel[i] = read16(mStream);
if (!mStream.good()) {
throw std::runtime_error("Truncated PSD file");
}
}
}
}
+22 -2
View File
@@ -134,12 +134,19 @@ static Result transcodeImageLevel(ktx2_transcoder& transcoder,
if (formatInfo.isCompressed) {
const uint32_t qwordsPerBlock = basisu::get_qwords_per_block(destFormat);
const size_t byteCount = sizeof(uint64_t) * qwordsPerBlock * levelInfo.m_total_blocks;
const size_t byteCount = (size_t)sizeof(uint64_t) * (size_t)qwordsPerBlock * (size_t)levelInfo.m_total_blocks;
if (qwordsPerBlock != 0 && levelInfo.m_total_blocks != 0 &&
byteCount / qwordsPerBlock / sizeof(uint64_t) != levelInfo.m_total_blocks) {
return Result::COMPRESSED_TRANSCODE_FAILURE;
}
uint64_t* const blocks = (uint64_t*) malloc(byteCount);
if (!transcoder.transcode_image_level(levelIndex, layerIndex, faceIndex, blocks,
levelInfo.m_total_blocks, formatInfo.basisFormat, decodeFlags,
outputRowPitch, outputRowCount, channel0,
channel1, &transcoderState)) {
free(blocks);
return Result::COMPRESSED_TRANSCODE_FAILURE;
}
*pbd = new Texture::PixelBufferDescriptor(blocks,
@@ -149,11 +156,23 @@ static Result transcodeImageLevel(ktx2_transcoder& transcoder,
const uint32_t rowCount = levelInfo.m_orig_height;
const uint32_t bytesPerPix = basis_get_bytes_per_block_or_pixel(formatInfo.basisFormat);
const size_t byteCount = bytesPerPix * levelInfo.m_orig_width * rowCount;
if (bytesPerPix == 0) {
return Result::UNCOMPRESSED_TRANSCODE_FAILURE;
}
const size_t byteCount = (size_t)bytesPerPix * (size_t)levelInfo.m_orig_width * (size_t)rowCount;
if (levelInfo.m_orig_width != 0 &&
byteCount / bytesPerPix / levelInfo.m_orig_width != rowCount) {
return Result::UNCOMPRESSED_TRANSCODE_FAILURE;
}
uint64_t* const rows = (uint64_t*) malloc(byteCount);
if (!transcoder.transcode_image_level(levelIndex, layerIndex, faceIndex, rows,
byteCount / bytesPerPix, formatInfo.basisFormat, decodeFlags,
outputRowPitch, outputRowCount, channel0, channel1, &transcoderState)) {
free(rows);
return Result::UNCOMPRESSED_TRANSCODE_FAILURE;
}
*pbd = new Texture::PixelBufferDescriptor(rows, byteCount,
@@ -259,6 +278,7 @@ Texture* Ktx2Reader::load(const void* data, size_t size, TransferFunction transf
return nullptr;
}
texture->setImage(mEngine, levelIndex, std::move(*pbd));
delete pbd;
}
return texture;
}
+9
View File
@@ -45,3 +45,12 @@ endif()
# ==================================================================================================
install(TARGETS ${TARGET} ARCHIVE DESTINATION lib/${DIST_DIR})
install(DIRECTORY ${PUBLIC_HDR_DIR}/uberz DESTINATION include)
# ==================================================================================================
# Tests
# ==================================================================================================
if (FILAMENT_BUILD_TESTING)
add_executable(test_${TARGET} tests/test_ReadableArchive.cpp)
target_link_libraries(test_${TARGET} PRIVATE ${TARGET} gtest)
set_target_properties(test_${TARGET} PROPERTIES FOLDER Tests)
endif()
+1 -1
View File
@@ -28,7 +28,7 @@ namespace filament::uberz {
// ArchiveSpec is a parse-free binary format. The client simply casts a word-aligned content blob
// into a ReadableArchive struct pointer, then calls the following function to convert all the
// offset fields into pointers.
void convertOffsetsToPointers(struct ReadableArchive* archive);
bool convertOffsetsToPointers(struct ReadableArchive* archive, size_t archiveSize);
UTILS_WARNING_PUSH
UTILS_WARNING_ENABLE_PADDED
+102 -10
View File
@@ -16,7 +16,10 @@
#include <uberz/ReadableArchive.h>
#include <utils/debug.h>
#include <utils/Logger.h>
#include <cstring>
#include <limits>
using namespace filament;
using namespace utils;
@@ -27,21 +30,110 @@ static_assert(sizeof(ReadableArchive) == 4 + 4 + 8 + 8);
static_assert(sizeof(ArchiveSpec) == 1 + 1 + 2 + 4 + 8 + 8);
static_assert(sizeof(ArchiveFlag) == 8 + 8);
void convertOffsetsToPointers(ReadableArchive* archive) {
constexpr size_t wordSize = sizeof(uint64_t);
assert_invariant(archive->specsOffset % wordSize == 0);
uint64_t* basePointer = (uint64_t*) archive;
archive->specs = (ArchiveSpec*) (basePointer + archive->specsOffset / wordSize);
namespace {
constexpr uint32_t READABLE_ARCHIVE_MAGIC = 'UBER';
constexpr uint32_t READABLE_ARCHIVE_VERSION = 0;
constexpr size_t WORD_SIZE = sizeof(uint64_t);
bool checkedMultiply(uint64_t count, size_t itemSize, size_t& result) {
if (count > std::numeric_limits<size_t>::max() / itemSize) {
return false;
}
result = size_t(count) * itemSize;
return true;
}
uint8_t* checkedOffset(ReadableArchive* archive, size_t archiveSize,
uint64_t offset, size_t length) {
if (offset > archiveSize || length > archiveSize - size_t(offset)) {
return nullptr;
}
return reinterpret_cast<uint8_t*>(archive) + size_t(offset);
}
const char* checkedCString(ReadableArchive* archive, size_t archiveSize,
uint64_t offset) {
if (offset >= archiveSize) {
return nullptr;
}
const char* string = reinterpret_cast<const char*>(archive) + size_t(offset);
if (memchr(string, 0, archiveSize - size_t(offset)) == nullptr) {
return nullptr;
}
return string;
}
} // namespace
bool convertOffsetsToPointers(ReadableArchive* archive, size_t archiveSize) {
if (archiveSize < sizeof(ReadableArchive)) {
LOG(ERROR) << "Uberz archive header is truncated";
return false;
}
if (archive->magic != READABLE_ARCHIVE_MAGIC) {
LOG(ERROR) << "Uberz archive has invalid magic";
return false;
}
if (archive->version != READABLE_ARCHIVE_VERSION) {
LOG(ERROR) << "Uberz archive has unsupported version";
return false;
}
if (archive->specsOffset % WORD_SIZE != 0) {
LOG(ERROR) << "Uberz specs offset is misaligned";
return false;
}
size_t specsSize;
if (!checkedMultiply(archive->specsCount, sizeof(ArchiveSpec), specsSize)) {
LOG(ERROR) << "Uberz specs array size overflows";
return false;
}
archive->specs = reinterpret_cast<ArchiveSpec*>(checkedOffset(archive, archiveSize,
archive->specsOffset, specsSize));
if (!archive->specs) {
LOG(ERROR) << "Uberz specs array exceeds buffer";
return false;
}
for (uint64_t i = 0; i < archive->specsCount; ++i) {
ArchiveSpec& spec = archive->specs[i];
assert_invariant(spec.flagsOffset % wordSize == 0);
spec.flags = (ArchiveFlag*) (basePointer + (spec.flagsOffset / wordSize));
spec.package = ((uint8_t*) basePointer) + spec.packageOffset;
if (spec.flagsOffset % WORD_SIZE != 0) {
LOG(ERROR) << "Uberz flags offset is misaligned";
return false;
}
size_t flagsSize;
if (!checkedMultiply(spec.flagsCount, sizeof(ArchiveFlag), flagsSize)) {
LOG(ERROR) << "Uberz flags array size overflows";
return false;
}
spec.flags = reinterpret_cast<ArchiveFlag*>(checkedOffset(archive, archiveSize,
spec.flagsOffset, flagsSize));
if (!spec.flags) {
LOG(ERROR) << "Uberz flags array exceeds buffer";
return false;
}
spec.package = checkedOffset(archive, archiveSize, spec.packageOffset,
spec.packageByteCount);
if (!spec.package) {
LOG(ERROR) << "Uberz package exceeds buffer";
return false;
}
for (uint64_t j = 0; j < spec.flagsCount; ++j) {
ArchiveFlag& flag = spec.flags[j];
flag.name = ((const char*) basePointer) + flag.nameOffset;
flag.name = checkedCString(archive, archiveSize, flag.nameOffset);
if (!flag.name) {
LOG(ERROR) << "Uberz flag name exceeds buffer or is not NUL-terminated";
return false;
}
}
}
return true;
}
} // namespace filament::uberz
+70
View File
@@ -0,0 +1,70 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <uberz/ReadableArchive.h>
#include <gtest/gtest.h>
#include <array>
#include <string>
using filament::uberz::ArchiveFeature;
using filament::uberz::ArchiveFlag;
using filament::uberz::ArchiveSpec;
using filament::uberz::ReadableArchive;
using filament::uberz::convertOffsetsToPointers;
namespace {
TEST(ReadableArchiveTest, RejectsSpecsOffsetOutsideBuffer) {
alignas(8) std::array<uint8_t, 64> storage {};
auto* archive = reinterpret_cast<ReadableArchive*>(storage.data());
archive->magic = 'UBER';
archive->version = 0;
archive->specsCount = 1;
archive->specsOffset = storage.size();
EXPECT_FALSE(convertOffsetsToPointers(archive, storage.size()));
}
TEST(ReadableArchiveTest, RejectsFlagNamesOutsideBuffer) {
alignas(8) std::array<uint8_t, 96> storage {};
auto* archive = reinterpret_cast<ReadableArchive*>(storage.data());
archive->magic = 'UBER';
archive->version = 0;
archive->specsCount = 1;
archive->specsOffset = sizeof(ReadableArchive);
auto* spec = reinterpret_cast<ArchiveSpec*>(storage.data() + archive->specsOffset);
*spec = {};
spec->flagsCount = 1;
spec->flagsOffset = archive->specsOffset + sizeof(ArchiveSpec);
spec->packageOffset = storage.size() - 1;
auto* flag = reinterpret_cast<ArchiveFlag*>(storage.data() + spec->flagsOffset);
flag->nameOffset = storage.size();
flag->value = ArchiveFeature::OPTIONAL;
EXPECT_FALSE(convertOffsetsToPointers(archive, storage.size()));
}
} // namespace
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+14 -11
View File
@@ -78,7 +78,7 @@ public:
// Return whether the given Entity has been destroyed (false) or not (true).
// Thread safe.
bool isAlive(Entity e) const noexcept {
bool isAlive(Entity const e) const noexcept {
assert(getIndex(e) < RAW_INDEX_COUNT);
return (!e.isNull()) && (getGeneration(e) == mGens[getIndex(e)]);
}
@@ -94,7 +94,7 @@ public:
/* no user serviceable parts below */
// current generation of the given index. Use for debugging and testing.
uint8_t getGenerationForIndex(size_t index) const noexcept {
uint8_t getGenerationForIndex(size_t const index) const noexcept {
return mGens[index];
}
@@ -107,6 +107,11 @@ public:
void dumpActiveEntities(utils::io::ostream& out) const;
#endif
// use carefully, several entities can have the same index.
static Entity::Type getIndex(Entity const e) noexcept {
return e.getId() & INDEX_MASK;
}
private:
friend class EntityManagerImpl;
EntityManager();
@@ -114,22 +119,20 @@ private:
// GENERATION_SHIFT determines how many simultaneous Entities are available, the
// minimum memory requirement is 2^GENERATION_SHIFT bytes.
static constexpr const int GENERATION_SHIFT = 17;
static constexpr const size_t RAW_INDEX_COUNT = (1 << GENERATION_SHIFT);
static constexpr const Entity::Type INDEX_MASK = (1 << GENERATION_SHIFT) - 1u;
static constexpr int GENERATION_SHIFT = 17;
static constexpr size_t RAW_INDEX_COUNT = (1 << GENERATION_SHIFT);
static constexpr Entity::Type INDEX_MASK = (1 << GENERATION_SHIFT) - 1u;
static inline Entity::Type getGeneration(Entity e) noexcept {
static Entity::Type getGeneration(Entity const e) noexcept {
return e.getId() >> GENERATION_SHIFT;
}
static inline Entity::Type getIndex(Entity e) noexcept {
return e.getId() & INDEX_MASK;
}
static inline Entity::Type makeIdentity(Entity::Type g, Entity::Type i) noexcept {
static Entity::Type makeIdentity(Entity::Type const g, Entity::Type const i) noexcept {
return (g << GENERATION_SHIFT) | (i & INDEX_MASK);
}
// stores the generation of each index.
uint8_t * const mGens;
uint8_t* const mGens;
};
} // namespace utils
+9 -1
View File
@@ -146,6 +146,14 @@ struct AgxToneMapperSettings {
bool operator==(const AgxToneMapperSettings& rhs) const;
};
enum class CustomLut : uint8_t {
NONE = 0,
NEGATIVE = 1,
GRAYSCALE = 2,
SEPIA = 3,
TEAL_AND_ORANGE = 4,
};
struct ColorGradingSettings {
// fields are ordered to avoid padding
bool enabled = true;
@@ -154,7 +162,7 @@ struct ColorGradingSettings {
bool gamutMapping = false;
filament::ColorGrading::QualityLevel quality = filament::ColorGrading::QualityLevel::MEDIUM;
ToneMapping toneMapping = ToneMapping::ACES_LEGACY;
bool padding0{};
CustomLut customLut = CustomLut::NONE;
AgxToneMapperSettings agxToneMapper;
color::ColorSpace colorspace = Rec709-sRGB-D65;
GenericToneMapperSettings genericToneMapper;
+84 -4
View File
@@ -98,6 +98,18 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ToneMapp
return i + 1;
}
static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, CustomLut* out) {
if (0 == compare(tokens[i], jsonChunk, "NONE")) { *out = CustomLut::NONE; }
else if (0 == compare(tokens[i], jsonChunk, "NEGATIVE")) { *out = CustomLut::NEGATIVE; }
else if (0 == compare(tokens[i], jsonChunk, "GRAYSCALE")) { *out = CustomLut::GRAYSCALE; }
else if (0 == compare(tokens[i], jsonChunk, "SEPIA")) { *out = CustomLut::SEPIA; }
else if (0 == compare(tokens[i], jsonChunk, "TEAL_AND_ORANGE")) { *out = CustomLut::TEAL_AND_ORANGE; }
else {
slog.w << "Invalid CustomLut: '" << STR(tokens[i], jsonChunk) << "'" << io::endl;
}
return i + 1;
}
static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, color::ColorSpace* out) {
using namespace filament::color;
if (0 == compare(tokens[i], jsonChunk, "Rec709-Linear-D65")) { *out = Rec709-Linear-D65; }
@@ -207,6 +219,8 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ColorGra
i = parse(tokens, i + 1, jsonChunk, &out->quality);
} else if (compare(tok, jsonChunk, "toneMapping") == 0) {
i = parse(tokens, i + 1, jsonChunk, &out->toneMapping);
} else if (compare(tok, jsonChunk, "customLut") == 0) {
i = parse(tokens, i + 1, jsonChunk, &out->customLut);
} else if (compare(tok, jsonChunk, "genericToneMapper") == 0) {
i = parse(tokens, i + 1, jsonChunk, &out->genericToneMapper);
} else if (compare(tok, jsonChunk, "agxToneMapper") == 0) {
@@ -984,10 +998,57 @@ constexpr ToneMapper* createToneMapper(const ColorGradingSettings& settings) noe
}
}
static utils::FixedCapacityVector<math::float3> generateCustomLut(CustomLut type, uint8_t dim) {
using namespace filament::math;
size_t count = size_t(dim) * dim * dim;
auto lut = utils::FixedCapacityVector<float3>::with_capacity(count);
for (size_t b = 0; b < dim; ++b) {
for (size_t g = 0; g < dim; ++g) {
for (size_t r = 0; r < dim; ++r) {
float3 v = float3{r, g, b} * (1.0f / (dim - 1));
switch (type) {
case CustomLut::NONE:
break;
case CustomLut::NEGATIVE:
v = 1.0f - v;
break;
case CustomLut::GRAYSCALE:
{
float luma = dot(v, float3{0.2126f, 0.7152f, 0.0722f});
v = float3{luma};
}
break;
case CustomLut::SEPIA:
{
float luma = dot(v, float3{0.299f, 0.587f, 0.114f});
v = float3{
clamp(luma * 1.2f, 0.0f, 1.0f),
clamp(luma * 1.0f, 0.0f, 1.0f),
clamp(luma * 0.8f, 0.0f, 1.0f)
};
}
break;
case CustomLut::TEAL_AND_ORANGE:
{
float luma = dot(v, float3{0.2126f, 0.7152f, 0.0722f});
float3 teal{0.0f, 0.5f, 0.5f};
float3 orange{1.0f, 0.5f, 0.0f};
v = teal * (1.0f - luma) + orange * luma;
}
break;
}
lut.push_back(v);
}
}
}
return lut;
}
ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* engine) {
ToneMapper* toneMapper = createToneMapper(settings);
ColorGrading *colorGrading = ColorGrading::Builder()
.quality(settings.quality)
ColorGrading::Builder builder;
builder.quality(settings.quality)
.exposure(settings.exposure)
.nightAdaptation(settings.nightAdaptation)
.whiteBalance(settings.temperature, settings.tint)
@@ -1006,8 +1067,14 @@ ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* e
.toneMapper(toneMapper)
.luminanceScaling(settings.luminanceScaling)
.gamutMapping(settings.gamutMapping)
.outputColorSpace(settings.colorspace)
.build(*engine);
.outputColorSpace(settings.colorspace);
if (settings.customLut != CustomLut::NONE) {
uint8_t dim = 16;
builder.customLut(generateCustomLut(settings.customLut, dim), dim);
}
ColorGrading *colorGrading = builder.build(*engine);
delete toneMapper;
return colorGrading;
}
@@ -1080,12 +1147,24 @@ static std::ostream& operator<<(std::ostream& out, const AgxToneMapperSettings&
<< "}";
}
static std::ostream& operator<<(std::ostream& out, CustomLut in) {
switch (in) {
case CustomLut::NONE: return out << "\"NONE\"";
case CustomLut::NEGATIVE: return out << "\"NEGATIVE\"";
case CustomLut::GRAYSCALE: return out << "\"GRAYSCALE\"";
case CustomLut::SEPIA: return out << "\"SEPIA\"";
case CustomLut::TEAL_AND_ORANGE: return out << "\"TEAL_AND_ORANGE\"";
}
return out << "\"INVALID\"";
}
static std::ostream& operator<<(std::ostream& out, const ColorGradingSettings& in) {
return out << "{\n"
<< "\"enabled\": " << to_string(in.enabled) << ",\n"
<< "\"colorspace\": " << to_string(in.colorspace) << ",\n"
<< "\"quality\": " << (in.quality) << ",\n"
<< "\"toneMapping\": " << (in.toneMapping) << ",\n"
<< "\"customLut\": " << (in.customLut) << ",\n"
<< "\"genericToneMapper\": " << (in.genericToneMapper) << ",\n"
<< "\"agxToneMapper\": " << (in.agxToneMapper) << ",\n"
<< "\"luminanceScaling\": " << to_string(in.luminanceScaling) << ",\n"
@@ -1346,6 +1425,7 @@ bool ColorGradingSettings::operator==(const ColorGradingSettings& rhs) const {
colorspace == rhs.colorspace &&
quality == rhs.quality &&
toneMapping == rhs.toneMapping &&
customLut == rhs.customLut &&
genericToneMapper == rhs.genericToneMapper &&
agxToneMapper == rhs.agxToneMapper &&
luminanceScaling == rhs.luminanceScaling &&
+5
View File
@@ -226,6 +226,11 @@ static void colorGradingUI(Settings& settings, float* rangePlot, float* curvePlo
ImGui::Combo("Quality##colorGradingQuality", &quality, "Low\0Medium\0High\0Ultra\0\0");
colorGrading.quality = (decltype(colorGrading.quality)) quality;
int customLut = (int) colorGrading.customLut;
if (ImGui::Combo("Custom LUT##colorGradingCustomLut", &customLut, "None\0Negative\0Grayscale\0Sepia\0Teal and Orange\0\0")) {
colorGrading.customLut = (CustomLut) customLut;
}
int colorspace = (colorGrading.colorspace == Rec709-Linear-D65) ? 0 : 1;
ImGui::Combo("Output color space", &colorspace, "Rec709-Linear-D65\0Rec709-sRGB-D65\0\0");
colorGrading.colorspace = (colorspace == 0) ? Rec709-Linear-D65 : Rec709-sRGB-D65;

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