Compare commits

...

157 Commits

Author SHA1 Message Date
Sungun Park
c772b06572 Merge branch 'rc/1.70.2' into release 2026-04-01 20:08:21 -07:00
Powei Feng
02602c6a9c Bump version to 1.70.2 2026-03-24 10:00:31 -07:00
Powei Feng
ebda7353af Merge branch 'rc/1.70.1' into release 2026-03-24 10:00:30 -07:00
Powei Feng
9dfd3f73bc Release Filament 1.70.1 2026-03-24 10:00:22 -07:00
Powei Feng
84c9752493 Update MATERIAL_VERSION to 70 2026-03-23 15:22:20 -07:00
Powei Feng
a102daed53 Update MATERIAL_VERSION to 70 2026-03-23 15:20:26 -07:00
Powei Feng
cab8a89346 vk: make fbo eviction time configurable (#9808)
The previous eviction time is too large (40), then causes memory
to bloat over time. We set the default to 3 (as in triple
buffering), but make it a configurable option via VulkanPlatform.

Fixes #9786
2026-03-23 10:12:41 -07:00
Sungun Park
859d930f60 Fix race condition between program caching and backend's modifying (#9821)
The previous implementation allowed the backend thread to modify the
program state while it was still being cached on the compiler thread.
This could lead to potential data corruption or inconsistent cache
blobs.

To ensure data integrity, the program blob is now cached before the
signal is dispatched. This prevents the receiving thread from modifying
the program (e.g., via glUniformBlockBinding) until the caching process
is fully finalized.

BUGS=[491819760]
2026-03-23 16:12:39 +00:00
Powei Feng
5177b5c973 vk: make fbo eviction time configurable (#9808)
The previous eviction time is too large (40), then causes memory
to bloat over time. We set the default to 3 (as in triple
buffering), but make it a configurable option via VulkanPlatform.

Fixes #9786
2026-03-21 00:57:27 +00:00
Mathias Agopian
6416cf1eaa remove sampleDepth(), inline the code instead. (#9806)
This hides less what actually happens, and allows us to save
a few calls to saturate().
2026-03-20 16:56:55 -07:00
Powei Feng
ddb2de1649 github: support ARM linux in backend and renderdiff tests (#9817)
- Update get-mesa.sh to query dpkg --print-architecture to install
  the correct packages dynamically, enabling aarch64 support.
- Update get-vulkan-sdk.sh to install via apt-get on Linux
  (libvulkan-dev, vulkan-validationlayers, glslang-tools,
  spirv-tools) instead of downloading LunarG's x86_64 tarball.
- Add runner architecture to Mesa and Vulkan SDK GitHub Action cache
  keys to prevent collision.
- Update test.sh and preamble.sh to detect aarch64 and point to
  the appropriate aarch64-linux-gnu libraries.
- Fix typos and some renaming
2026-03-20 22:46:44 +00:00
Patrick Ribas
20aa91edc9 remove sunDisk in skybox at FL0 (#9675) 2026-03-20 10:54:43 -07:00
Powei Feng
d65589bf77 android: [render-validation] add more test result details (#9812)
Add the following information:
 - Android build fingerprint, version
 - GPU driver name, info, vendor name
 - Time elapsed for test
 - Rendered images (as oppose to diff image)

filament-utils:
 - Add DeviceUtils to hook into Platform methods for reading out
   strings about gpu vendor, driver.

Fix "tolerance" in test definition
2026-03-19 23:03:00 +00:00
Powei Feng
81961f6d49 github: [renderdiff] ensure right clang (#9816)
For renderdiff, we compile mesa using brew's llvm/clang, but
we compile filament with the system clang (AppleClang).

This is to prevent compiling filament with brew's clang (which
can cause unexpected compilation error).
2026-03-19 15:29:47 -07:00
Powei Feng
9fbf438baa github: [mesa] ensure right llvm is linked on macos (#9815) 2026-03-19 11:43:03 -07:00
Powei Feng
5007b4e428 android: allow modelviewer to reset (#9813)
- Add reset functionality to ModelViewer
 - We use this reset in render-validation to not have to reload
   the scene in runs that use the same model.
 - Fix a bug where the camera manipulator is disabling setting
   the camera via view config.
2026-03-19 18:11:46 +00:00
Sungun Park
0d2ecb5436 Relocate the read-only property to OpenGLContext (#9811)
This is a preparatory cleanup step to resolve a TSan warning ahead of a
planned refactor of the OpenGLContext class.

BUGS=[491522442]
2026-03-19 03:54:57 +00:00
Sungun Park
f24db4633e Fix crash in OpenGLDriver::destroyStream (#9801)
Fixes a SIGSEGV crash that occurs during
`ExternalStreamManagerAndroid::release()` when users provide a custom
Platform instance during Engine creation.

When a custom Platform is used, ExternalStreamManagerAndroid is
instantiated on the application's thread. Its constructor was caching a
C++ reference (`VirtualMachineEnv& mVm`) to the `thread_local`
VirtualMachineEnv instance belonging to the application thread.

Later, when the Filament backend thread executed `release()`, it
accessed this cached reference. This caused the backend thread to
retrieve the `JNIEnv*` from the application thread's `thread_local`
instance, and subsequently call `env->DeleteGlobalRef()`.  A thread must
use its own `JNIEnv` pointer, so using the application thread's pointer
from the backend thread resulted in a crash.

BUGS=[489814416]
2026-03-19 02:26:58 +00:00
Patrick Ribas
45d92cac14 Silently disable MSAA for FL0. (#9807) 2026-03-18 16:29:36 -07:00
Eliza
c5e915b96c engine: panic if prepareProgram() fails (#9809)
This change crashes Filament with a helpful error message if we ever try to
prepare a program and fail.
2026-03-18 16:28:41 -07:00
Mathias Agopian
e2b195706a fix overdraw visualization in gltf_viewer (#9805) 2026-03-18 09:01:18 -07:00
Powei Feng
3ebb2086ed github: use arm linux instead of x86 whenever possible (#9794)
- Simplify getting cmake, ninja for linux
 - Move linux, web, webgpu builds to use arm (more cores for less)
 - Android cannot be moved due to Android SDK not available on
   arm linux yet.
 - Add Arm Linux release artifacts

Fixes #8674
2026-03-18 07:19:38 +00:00
Mathias Agopian
052092f553 remove the remaining instances of std::string (#9802)
libfilament (including libutils and libmath) are 100% std::string
free.

std::string is pulled in the .so (on android) through libc++ for 
exception handling, even if we're not using them. There is not much
we can do here, but at least, it's not because of us!

utils::ostream still references it but only as an inline function,
so if the inline is not called, std::string won't be pulled in.

It's also referenced from Path.cpp, but that's not included in
libfilament.
2026-03-17 15:27:12 -07:00
Mathias Agopian
7163aaa627 don't output the full shader when matc fails (#9803)
This made it nearly impossible to find the actual error. Now, we
output only the error from the compiler + the material name, variant
and shader stage all in one line.
2026-03-17 13:37:14 -07:00
Mathias Agopian
0c7ff3fcf4 store shadow atlas resolution in frameUniforms (#9804) 2026-03-17 13:36:58 -07:00
Powei Feng
962756e283 gl: add getter for GL_VERSION (#9798)
On Android, GL_VERSION contains information about the driver
version number.  This can be useful when debugging or running
tests.
2026-03-17 17:56:51 +00:00
Sungun Park
48ee727c8d fix: Resolve a TSan warning for OpenGLTimerQuery (#9790)
The main thread invokes `getTimerQueryValue` synchronously, reading
`tq->state`. Meanwhile, the driver thread handles `createTimerQuery`
asynchronously and initializes `tq->state = std::make_shared<State>()`.

Eliminate this race by initializing the state in createTimerQueryS.

BUGS=[491522442]
2026-03-16 20:48:31 +00:00
Powei Feng
2d1e4b8ce2 github: fix broken header check (#9800)
- Move header check from postsubmit to presubmit
 - Install third_party/getop if needed
 - Modify check-headers test to use system getopt when it's available.
2026-03-16 10:26:59 -07:00
Powei Feng
749b03ed2a utils: refactor getopt into utils namespace (#9796)
On certain linux, macOS environment, there is already a system
getopt. This often creates conflict when compiling filament.
Here we alias utils::getopt to either the system getopt (if
present) or third_party/getopt.

Fixes #7551
2026-03-13 17:22:44 -07:00
Mathias Agopian
497a1cc42d fix android CMake so LTO can be turned on (#9797)
our custom build system didn't set CMAKE_ANDROID_NDK_VERSION which
is needed for CMake's check_ipo_supported.
2026-03-13 16:32:59 -07:00
Run Yu
256a494cd0 webgpu: implement the scissor backend function (#9783)
also enable ScissorViewportRegion backend test.
It was previously skipped, now it is passing.

BUGS=474393992
2026-03-13 21:37:54 +00:00
Powei Feng
b2531fff15 android: [sample-render-val] add difference/output viewer (#9781)
- Add viewer for closer examination
 - Add slider to enhance difference
 - Fixed ImageDiff jni bug to account for stride and
   premultiplication by alpha
2026-03-13 18:39:22 +00:00
Powei Feng
f18afe1d3e Try to fix verify-notes breakage (#9795)
- Update python3 version to 3.12.13
 - Use action/checkout instead of Bhacaz/checkout-files because action/checkout can
   use the action/files of the current PR, where as checkout-files is checking out
   a file on the main branch
 - action/checkout seems to run in less than a minute, so acceptable performance
   difference.

--------------------------------------------------------------------

Breakage is:

Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/jwt/algorithms.py", line 36, in <module>
    from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
ModuleNotFoundError: No module named 'cryptography'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/verify_release_notes.py", line 17, in <module>
    from github import Github
  File "/usr/local/lib/python3.10/site-packages/github/__init__.py", line 56, in <module>
    from github.MainClass import Github, GithubIntegration
  File "/usr/local/lib/python3.10/site-packages/github/MainClass.py", line 54, in <module>
    import jwt
  File "/usr/local/lib/python3.10/site-packages/jwt/__init__.py", line 1, in <module>
    from .api_jwk import PyJWK, PyJWKSet
  File "/usr/local/lib/python3.10/site-packages/jwt/api_jwk.py", line 8, in <module>
    from .algorithms import get_default_algorithms, has_crypto, requires_cryptography
  File "/usr/local/lib/python3.10/site-packages/jwt/algorithms.py", line 113, in <module>
    from typing_extensions import Never
ModuleNotFoundError: No module named 'typing_extensions'
2026-03-13 11:06:55 -07:00
Sungun Park
771eb4c1a4 fix: Resolve a TSan warning for ShaderCompilerService (#9792)
When the backend thread checks the availability of the token, it
directly accesses `gl.program`, which caused a TSan warning.

BUGS=[491819760]
2026-03-12 17:19:11 +00:00
Powei Feng
70e7cb2a27 github: fix bad conditional in presubmit (#9793)
The previous conditional used
github.event.head_commit.verification.verified, which is not a
real field. Hence, the attempt to reduce presubmit runs did not
actually succeed.  Here we replace it with a more correct
verification step.
2026-03-11 16:09:29 -07:00
Powei Feng
f95f127495 github: reduce presubmit runs (#9791)
We make two adjustments to make the presubmit workflows run less.
 - Only run on commit merge when the commit is not "verified".
   While not a 1-1 definition, "verified" commit typically implies
   that the commit came from a PR merge (which requires that
   presubmit passes). A commit could also be made by owners on
   main directly, in which case, the commit would not be verified.
 - Cancel any in progress presubmit runs are "overwritten" by an
   update to the PR.
2026-03-11 18:43:39 +00:00
Evan Mezeske
c0d63e826d Metal: fix crash from exceeding max texture dimensions for Bloom buffer (#9784)
I recently added a new fuzz test for my Filament-based app, which creates a window and then rapidly issues changes such as toggling View features, changing the window size, and other operations that the users of my app can do.

It quickly found a crash when Bloom is enabled and the window dimensions get resized to weird aspect ratios on a large monitor. Eventually I narrowed it down to the Bloom buffer's width exceeding the Metal max texture dimension (on my machine, 16K). Applying this patch fixes the crash.
2026-03-11 10:39:35 -07:00
haroonq
36583618f1 Allow child classes to configure the EGLDisplay. (#9779) 2026-03-10 23:42:17 +00:00
Powei Feng
9d322e7208 github: reduce action runner costs (#9789)
This is a speculative reduction of github runner costs by using
less costly machines. The usage time will be longer, but
hopefully the total cost will still be lower.

Note we disabled most of the large runners for postsubmit builds,
since we should have caught all breakages in presubmit.
2026-03-10 21:30:47 +00:00
Filament Bot
b897d22d20 [automated] Updating /docs due to commit e5fe3d4
Full commit hash is e5fe3d495e

DOCS_ALLOW_DIRECT_EDITS
2026-03-10 20:49:10 +00:00
Benjamin Doherty
90254338d6 Bump version to 1.70.1 2026-03-10 13:43:25 -07:00
Benjamin Doherty
06b7c1ffad Merge branch 'rc/1.70.0' into release 2026-03-10 13:43:24 -07:00
Benjamin Doherty
e5fe3d495e Release Filament 1.70.0 2026-03-10 13:43:15 -07:00
Benjamin Doherty
6eff9a9b00 Bump MATERIAL_VERSION to 70 2026-03-10 13:41:32 -07:00
Ben Doherty
2f885cb66d Add Sonatype publishing step to release workflow (#9764) 2026-03-10 13:40:06 -07:00
Mathias Agopian
35f501b3d5 Debug Shadow Cascades: remove dependency on frameUniforms (#9782)
The DebugShadowCasacade material was dependent on frameUniforms,
instead we use material parameters. We also pass all the informations
relative to cascades (it's not used yet, but will be in the future).
2026-03-09 17:14:07 -07:00
Powei Feng
5b799928c3 webgpu: fixes for deadlock, over-flushing (#9776) (#9767)
- In finish(), instead of block to wait for read pixels to finish,
   we poll the state of the counter to ensure that all the readbacks
   are complete.  This allows us to complete the readpixel callbacks
   by advancing webgpu's internal callback counter.
 - Remove all the redundant flushes that are no longer needed now
   that buffer upload is put on the command queue.
 - Add a convenience method for getting swapchain dimensions. Will
   be useful.
 - Add webgpu as a valid backend for running backend tests on linux
   (now that it no longer deadlocks).
2026-03-09 13:05:18 -07:00
Powei Feng
f7f586caff webgpu: fixes for deadlock, over-flushing (#9776)
- In finish(), instead of block to wait for read pixels to finish,
   we poll the state of the counter to ensure that all the readbacks
   are complete.  This allows us to complete the readpixel callbacks
   by advancing webgpu's internal callback counter.
 - Remove all the redundant flushes that are no longer needed now
   that buffer upload is put on the command queue.
 - Add a convenience method for getting swapchain dimensions. Will
   be useful.
 - Add webgpu as a valid backend for running backend tests on linux
   (now that it no longer deadlocks).
2026-03-09 18:14:26 +00:00
Mathias Agopian
5428812b93 remove an unnecessary assert in the froxelixer (#9785) 2026-03-09 10:41:43 -07:00
Patrick Ribas
e57f2f5c02 Add workaround for GLES2 mipmap requirements (#9770) 2026-03-06 16:36:03 -08:00
Mathias Agopian
fd9bcaa735 Fix heap buffer overflow in HDRDecoder RLE decoding (Issue #9748) (#9777)
This commit addresses a critical security vulnerability (OOB write) and 
several stability issues in the Radiance HDR parser.

Primary Fix:
* Fixed a heap buffer overflow in the RLE decoding loop (Issue #9748). 
  The decoder previously failed to verify if a run-length chunk exceeded 
  the remaining space in the scanline buffer. Added strict bounds checking 
  (`num_bytes + run_length > width`) before executing `memset` or 
  `mStream.read` to prevent arbitrary memory corruption.

Additional Security & Stability Improvements:
* Prevented an infinite loop (DoS) in header parsing. Replaced the 
  `do { ... } while(true);` loop with proper stream state checking 
  (`while (mStream.getline(...))`) to handle unexpected EOFs gracefully.
* Mitigated integer overflow and Out-Of-Memory (OOM) vulnerabilities by 
  enforcing maximum sane dimensions (`MAX_IMAGE_DIMENSION` and 
  `MAX_IMAGE_PIXELS`). This prevents catastrophic memory allocations 
  triggered by maliciously crafted width/height values.
* Initialized local variables and buffers (`buf`, `gamma`, `exposure`) to 
  prevent undefined behavior and parsing of stack garbage upon stream read 
  failures.

Fixes #9748
2026-03-06 16:35:24 -08:00
Powei Feng
9a14e54fc2 matdbg: prefer msl when running on metal (#9780)
Keeping ShaderLanguage::UNSPECIFIED will return metal library
as the preferred language on metal.  Here we just make it more
explicit that matdbg needs msl.

Fixes #9372
2026-03-06 21:34:21 +00:00
Powei Feng
d78bb294ed gltf-viewer: enable animation by default (#9778)
The toggle for animation was added recently to Settings and default 
to false. This differs from before where animation is always assumed
to be enabled.

For gltf_viewer's interactive mode, we enable animation. (Batch mode
will still have animation off).

Fixes #9775
2026-03-06 17:46:13 +00:00
haroonq
0b8dbe9b0a Small fix to allow PlatformEGL to work on non-Android targets. (#9724)
- Include gl_headers.h since it defines BACKEND_OPENGL_VERSION_GL(ES)
    depending on which headers are available.
- Don't assume glGetString(GL_EXTENSIONS) returns a non-null value as
    null is a possible return value for OpenGL (desktop).
2026-03-05 15:38:49 -08:00
Powei Feng
e595fd4b79 android-utils: ModelViewer camera manipulator can be null (#9766)
We add the ability for the maniuplator to be null. If it is null,
then the camera settings won't be determined by the manipulator.
This is useful for when we want to set the camera parameters
outside of the modelviewer, but still use it to do everything else.
2026-03-05 20:05:55 +00:00
Ben Doherty
c5d36cff7f Prepare Gradle plugin for publishing (#9773) 2026-03-05 10:50:15 -08:00
Powei Feng
f392e8be54 ppm: move stencil check right before driver calls (#9769)
The init block of addPass will be executed immediately, which means
that even for passes that are culled, we will trip on incorrect format
given to the pass.

We move the stencil check to the execute block of addPass so that
it'll only assert when the pass is actually present in the graph.

FIXES=489437881
2026-03-05 18:11:33 +00:00
Eliza
83653fb358 engine: encapsulate material cache handling (#9663)
* engine: encapsulate material cache handling

We will soon allow `MaterialInstance` to override the value of spec constants.
To avoid code duplication, we introduce a new class `MaterialPrograms` which
handles the chunky bits of managing the program cache and values of the spec
constants.

* MaterialPrograms: add explicit initialize method

* MaterialPrograms: backport fixes, address comments

* MaterialPrograms: set all constants at once

* MaterialPrograms: rename to LocalProgramCache
2026-03-05 06:42:05 +00:00
Eliza
8a3c48fef1 utils: add LRU cache to RefCountedMap (#9730)
* utils: add LRU cache to RefCountedMap

This change introduces a new data structure LruCache and uses it in
RefCountedMap to keep a fixed number of cache entries alive after their
reference count has dropped to zero in the main map.

* utils: address LRU cache comments
2026-03-05 06:04:16 +00:00
Powei Feng
7f6b9bb144 vk: reference rendertarget as part of fbo cache (#9771)
The FBO cache uses imageviews as its cache key. To ensure that these
views are valid for the lifetime of a key-value pair in the cache, we
need to also reference the rendertarget in the value part of the pair.
The RT contains attachments, which are wrappers around
which own the image views.

Fixes #9680
2026-03-04 09:59:05 -08:00
rafadevai
687c42583b VK: Only make a single copy of the descriptor with external samplers (#9765)
When more than one external sampler are present the
flow will clone the descriptor set twice and make a copy
of a binding that requires an immutable sampler. The copy
of this binding will cause a crash in some adreno GPUs.

This change will make sure the descriptor set is only cloned one
and after that only update binding operations are done for the
external sampled bindings.
2026-03-03 18:34:40 -08:00
Mathias Agopian
71e8cab08a Fix a race when garbage-collecting components (#9768)
Not all component managers are thread-safe for garbage-collection. In
particular, some need to access the DriverAPI which is never
thread-safe.

We refactor the garbage-collection code into FEngine, so it's not
duplicated in FRenderer. We make it more explicit that 
FRenderableManager::destroyComponent() needs the DriverAPI, and we
don't call its gc() from a job.

Besides the refactoring, the only change in this CL is that 
FRenderableManager::gc() is no longer called from a job.


FIXES=[489134910]
2026-03-03 13:21:06 -08:00
Ben Doherty
afae31a975 Add Sonatype publishing step to release workflow (#9764) 2026-03-03 12:41:55 -08:00
Powei Feng
5ac5dc4c95 ci: fetch PR ref explicitly to extract commit message for forks (#9756) 2026-03-03 17:24:00 +00:00
Filament Bot
e975572972 [automated] Updating /docs due to commit 29e91f0
Full commit hash is 29e91f0d3a

DOCS_ALLOW_DIRECT_EDITS
2026-03-03 16:21:50 +00:00
Sungun Park
13dcae6d5f Merge branch 'rc/1.69.5' into release 2026-03-03 08:15:44 -08:00
Sungun Park
c14b428acc Bump version to 1.70.0 2026-03-03 08:15:44 -08:00
Sungun Park
29e91f0d3a Release Filament 1.69.5 2026-03-03 08:15:31 -08:00
Mathias Agopian
6f0d47f275 EVSM improvements (#9758)
- refactoring/clecanup to make some changes easier
- VSM mipmap generation was mistakenly disable when blur radius was 0
- analytic variance was disabled because the math only worked for VSM. Fixed the math.
- better handling of large blurs when using fp32
- implement EVSM equivalent of receiver plane normal bias
- use correct EVSM clearing color
- mipmapping with point lights works much better (no seams)
- min variance is computed automatically
- custom high precision mipmaping shader for VSM
2026-03-02 16:46:07 -08:00
Powei Feng
cf66813f41 android: sample-render-validation with new UI and cli tool (#9751)
Android App:
- Refactored activity_main.xml to use modern Material 3 components.
- Grouped Export/Help buttons logically and moved ADB instructions
  to dedicated info icons.
- Added an in-app "Load Test" button to explicitly pick test bundles.
- Updated ValidationInputManager.kt to gracefully handle relative
  zip_path intent execution via ADB targeting app external storage.

Tooling & Documentation:
- Added a Python Textual TUI (validation_app.py) to automate device
  discovery, test execution, bundling, renaming, and
  downloading/uploading.
- Added README.md in test/render-validation documenting ADB intent
  parameter capabilities and TUI dashboard setup.
2026-03-02 23:51:07 +00:00
Powei Feng
5f89e8e711 vk: fix crash when resizing (#9762)
The problem is that we might flush when resizing happens. So
we need to ask for the command buffer "after" the resize/acquire
swapchain logic.

Fixes #9718
2026-02-27 18:14:59 +00:00
Anish Goyal
be9e9298e1 Add a default fence value to VulkanCommands (#9759)
This is useful for the case of certain devices, where a filament::Sync
may be created BEFORE any commands are submitted.
2026-02-27 06:03:30 +00:00
dependabot[bot]
9218b90c9c build(deps): bump the pip group across 1 directory with 2 updates (#9749)
Bumps the pip group with 2 updates in the /test/renderdiff/src directory: [flask](https://github.com/pallets/flask) and [werkzeug](https://github.com/pallets/werkzeug).


Updates `flask` from 3.1.2 to 3.1.3
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.1.2...3.1.3)

Updates `werkzeug` from 3.1.4 to 3.1.6
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.1.4...3.1.6)

---
updated-dependencies:
- dependency-name: flask
  dependency-version: 3.1.3
  dependency-type: direct:production
  dependency-group: pip
- dependency-name: werkzeug
  dependency-version: 3.1.6
  dependency-type: direct:production
  dependency-group: pip
...

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-02-27 05:37:29 +00:00
Mathias Agopian
070a07679d Simplify the API of MaterialInstanceManager (#9757)
It's no longer necessary to preallocate a "tag" to reuse a material
instance. Instead, we can simply pass a unique tag when getting the
instance from the pool.
2026-02-26 15:11:31 -08:00
Andrew Wilson
10b7bd71f9 Avoid backend test when building without testing (#9727) 2026-02-26 22:49:58 +00:00
Powei Feng
e4ae96a2a1 renderdiff: disable transmission + webgpu due to flake
RDIFF_ACCEPT_NEW_GOLDENS
2026-02-26 11:52:35 -08:00
Siyu
56ac08e353 Provide thread name when attaching to JVM on Android (#9755)
* Provide thread name when attaching to JVM on Android

When calling AttachCurrentThread on Android, pass a JavaVMAttachArgs structure. This allows providing the thread name, which is retrieved using pthread_getname_np, to the JVM.

* Fix Android build error: pthread_getname_np requires API 26+

---------

Co-authored-by: Mathias Agopian <mathias@google.com>
2026-02-26 10:44:24 -08:00
Siyu
52b0b553b4 Marshall the name size when setting thread name with pthread_setname_np (#9753)
* Marshall the name size when setting thread name with pthread_setname_np.

[pthread_setname_np](https://source.corp.google.com/piper///depot/google3/third_party/android/ndk/stable/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/pthread.h;l=330-341) requires the caller to keep the name within 16 bytes.

After this change, Filament threads like `OpenGLTimerQuer`, `CompilerThreadP`, `CompilerThreadP`, `Filament Choreo`, `FrameInfoGpuCom` would be displayed correctly in the trace.

* Use constexpr MAX_PTHREAD_NAME_LEN

---------

Co-authored-by: Powei Feng <powei@google.com>
Co-authored-by: Mathias Agopian <mathias@google.com>
2026-02-26 10:43:53 -08:00
Filament Bot
00f3c7175c [automated] Updating /docs due to commit e4fa86f
Full commit hash is e4fa86fb01

DOCS_ALLOW_DIRECT_EDITS
2026-02-26 18:05:45 +00:00
Powei Feng
e4fa86fb01 renderdiff: bump golden again
Transimssion.webgpu.TransmissionRoughnessTest seems to have small,
non-flaky differences.

RDIFF_ACCEPT_NEW_GOLDENS
2026-02-26 09:48:30 -08:00
Mathias Agopian
7da2a08df6 replace Variant::VSM with MNT and S2D (#9750)
The VSM variant bit was overloaded, it meant two different things
depending on the DEP bit (depth).

For standard variants (DEP = 0), it decides the type of the shadow
sampler used (PCF or 2D).

For depth variants (DEP = 1), it decides what is written during the
shadow pass (nothing, i.e. depth only, or EVSM depth moments).

We now clearly separate the two bits throughout the code.

This change should be purely source-cosmetic, there shouldn't be any
behavior changes.

Co-authored-by: Powei Feng <powei@google.com>
2026-02-25 16:16:07 -08:00
Powei Feng
82246d934d renderdiff: fix update_golden.py
And fix other python bugs

RDIFF_ACCEPT_NEW_GOLDENS
2026-02-25 15:35:02 -08:00
Filament Bot
c60969ef67 [automated] Updating /docs due to commit ce37f21
Full commit hash is ce37f216bc

DOCS_ALLOW_DIRECT_EDITS
2026-02-25 23:01:55 +00:00
Powei Feng
ce37f216bc Update renderdiff README
RDIFF_ACCEPT_NEW_GOLDENS
2026-02-25 14:58:35 -08:00
Mathias Agopian
81c71fbbb9 Improve shadow normal bias calculation (#9734)
The previous code used the max of the texel's width or height footprint
in world space to compute the offset; this could both overestimate or
underestimate the bias causing some peter panning or acne.

The new code replaces a sqrt with a dot, but is otherwise similar.
2026-02-25 12:16:13 -08:00
Mathias Agopian
07a7c6003a better computation of the jacobian of a projection (#9731)
The previous code was a bit clunky and not generic, it made assumptions
about the shape of the projection matrix. 
This just uses the generic, correct calculation.

In addition the previous code used the wrong matrix, it assumed that
Wp wasn't needed, but it was because translations do change the
jacobian value at a given point.

RDIFF_ACCEPT_NEW_GOLDENS
2026-02-25 12:04:55 -08:00
Sungun Park
804a74c205 Remove unused member in OpenGLContext (#9741) 2026-02-25 19:46:34 +00:00
Anish Goyal
dde49a410a Add inferred template to View.cpp (#9746)
In some cases, this missing template causes build issues when locally
building Impress prebuilts.
2026-02-25 09:19:19 -08:00
Filament Bot
770ce7f8ec [automated] Updating /docs due to commit 11714d3
Full commit hash is 11714d3adc

DOCS_ALLOW_DIRECT_EDITS
2026-02-25 03:44:07 +00:00
Powei Feng
847d657ad5 Merge branch 'rc/1.69.4' into release 2026-02-24 19:37:59 -08:00
Powei Feng
48592d7d22 Bump version to 1.69.5 2026-02-24 19:37:59 -08:00
Powei Feng
11714d3adc Release Filament 1.69.4 2026-02-24 19:37:50 -08:00
Filament Bot
6aac9071b3 [automated] Updating /docs due to commit da9173e
Full commit hash is da9173e9dc

DOCS_ALLOW_DIRECT_EDITS
2026-02-25 03:15:24 +00:00
Powei Feng
da9173e9dc ci: automate renderdiff golden image updates (#9740)
- Introduces the `RDIFF_ACCEPT_NEW_GOLDENS` commit message tag.
- Conditionally skip the `test-renderdiff` presubmit comparison
  if this tag is present.
- Extracts renderdiff generation into a reusable
  `.github/actions/renderdiff-generate` composite action.
- Modifies `postsubmit-main.yml` to automatically generate new
  goldens and push them to a temporary
  `accept-goldens-<short-hash>` branch before merging them into
  `main` when the tag is found.
2026-02-25 03:12:50 +00:00
Anish Goyal
cd64d50408 Fixes FBO destroys for inflight command buffers (#9744)
If a framebuffer is destroyed while a command buffer is in flight (e.g.
during a window resize), it's possible for a command buffer to try to
use a framebuffer that no longer exists. This encapsulates framebuffers
and render passes in resource_ptr, to ensure they are never reclaimed
prematurely.
2026-02-25 01:13:51 +00:00
dependabot[bot]
a3145cb96f Bump minimatch (#9747)
Bumps the npm_and_yarn group with 1 update in the /build/common/upload-release-assets directory: [minimatch](https://github.com/isaacs/minimatch).


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

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.3
  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>
2026-02-24 16:52:35 -08:00
Nick Fisher
cdfb92e14a gltfio: Allow compile-time override of GLTFIO_USE_FILESYSTEM (#9733) 2026-02-24 17:47:49 +00:00
Doris Wu
551add7519 call execute() under single threaded mode (#9738) 2026-02-23 14:22:56 -08:00
Doris Wu
55c16e6e7a call execute() under single threaded mode (#9738) 2026-02-23 22:20:03 +00:00
Powei Feng
65e3c3bfb9 backend: disable autoresolve test for gl+vk on CI (#9742)
BUGS=486954356
2026-02-23 21:57:12 +00:00
Ben Doherty
902f869721 Metal: recreate sidecar texture if sample count changes (#9430) 2026-02-23 09:54:21 -08:00
Eliza
b118ded3fa engine: fix VSM (#9737) 2026-02-22 16:12:31 -08:00
Powei Feng
b3d0416a65 gl: update record when detaching stream (#9712)
FIXES=483744050
2026-02-22 16:12:14 -08:00
Eliza
ad1bc6f360 engine: fix VSM (#9737) 2026-02-20 15:08:59 -08:00
Sungun Park
1d5f6cd6a9 Turn off UBO batching (#9736)
BUGS=[486200381]
2026-02-20 12:09:22 -08:00
Sungun Park
73c343635e Turn off UBO batching (#9736)
BUGS=[486200381]
2026-02-20 20:04:05 +00:00
Mathias Agopian
432e672022 Revert "Swap logic of how the EGL display is initialized. (#9634)" (#9729)
This reverts commit c35ae6571f.

BUGS=[481534922, 478925865]
2026-02-20 08:34:51 -08:00
Doris Wu
b56b04c5f8 Fix translucent objects are pickable when skybox is disabled (#9688) 2026-02-20 11:36:58 +08:00
Filament Bot
99816d67c2 [automated] Updating /docs due to commit d6d4f92
Full commit hash is d6d4f92922

DOCS_ALLOW_DIRECT_EDITS
2026-02-19 20:03:46 +00:00
Mathias Agopian
d6d4f92922 fix intensities (#9728)
DOCS_FORCE
2026-02-19 11:59:34 -08:00
Powei Feng
6a59a68622 gl: update record when detaching stream (#9712)
FIXES=483744050
2026-02-19 18:33:04 +00:00
Filament Bot
4580f57987 [automated] Updating /docs due to commit 38f7e57
Full commit hash is 38f7e579f1

DOCS_ALLOW_DIRECT_EDITS
2026-02-19 17:33:34 +00:00
Benjamin Doherty
5e4c24a7f6 Bump version to 1.69.4 2026-02-19 09:30:21 -08:00
Benjamin Doherty
3c27a83315 Merge branch 'rc/1.69.3' into release 2026-02-19 09:30:20 -08:00
Benjamin Doherty
38f7e579f1 Release Filament 1.69.3 2026-02-19 09:30:12 -08:00
Powei Feng
9b1c8a2bf5 backend: add R11G11B10 format as input to reshaper (#9722) 2026-02-19 00:06:23 -08:00
rafadevai
4504471021 Vulkan: Implement stencil state in pipeline cache (#9716)
Store and set the stencil state for a graphics pipeline.
2026-02-18 20:09:07 +00:00
Powei Feng
37c316fa03 backend: reduce size increase of recent datareshaper change (#9723)
The previous datareshaper change used too multiple parameters
in templates in an attempt to reduce conditionals in the hot-loop.
However, this dramatically increased the binary size.

This change keeps the original intention of not having conditional
in the inner loop, but isolate small sections that need to be
templated into separate components.
2026-02-18 18:12:06 +00:00
Serge Metral
14960f7118 Protected content fix vulkan 2 (#9721)
* Adding the begin frame message for later xtrace post processing.

* Add the flags.

* Cleanup

* Cleanup

---------

Co-authored-by: Powei Feng <powei@google.com>
2026-02-18 09:50:17 -08:00
Powei Feng
1deb657442 github: add sizeguard presubmit test (#9719)
- Add a presubmit check after the android build to check the
   built artifact sizes against previous build sizes.
 - If the size built is +20K over the previous, then the test
   fails.
 - This prevents dramatic size increases of our mobile build
2026-02-18 08:41:58 +00:00
Powei Feng
45c0d1b34f backend: enhance data reshaper (#9711)
- add half float as source type
 - gray-scaled output when src-channel=1 and dest-channel=3 or 4
 - Ensure that per-pixel work is as-constexpr-as-possible
2026-02-17 20:14:34 +00:00
chenriji
1ddd10f326 Fix: Morph Target animations (Blend Shapes) fail to render when loading GLB files via ResourceLoader (#9696)
* fixbug:"debug error, reason: The material 'Material_MR' has not been compiled to include the required GLSL or SPIR-V chunks for the vertex shader (variant=5, filtered=5)",Because the member variable mVariantFilter is not initialized, random values will appear on Windows 10 or others platform, which eventually causes some variants to be filtered out by this mVariantFilter.

* fix:Fix morph target loading for accessors without buffer_view

  Morph targets were not working because ResourceLoader skipped all
  accessors without buffer_view. For morph targets, the data can be
  accessed directly via cgltf_accessor_unpack_floats().

  This fix properly unpacks and uploads morph target vertex data to the
  GPU, enabling blendshapes and facial deformation to work correctly.

Steps to Reproduce
1、In Unity (2022.3.11): Create a Prefab with Blend Shapes (Morph Targets) and an Animator to control them (e.g., an animation clip that makes the eyes squint).
2、Export: Use the UnityGLTF tool to export the model as a .glb file (including the Animator and Morph Target tracks).
3、In Filament: Load and play the animation.
4、Result: The skeletal animation (bone-based) may play, but the Morph Target effect (squinting) is missing or static.
2026-02-13 23:07:17 +00:00
Powei Feng
308668a705 android: render-validation sample UI and additions (#9692)
- Update UI
 - Wait for resources to finish loading before testing
 - Refactor validation to input/output
 - Move assets to be generated from the repo
 - Fix AutomationEngine java binding to add missing fields
2026-02-13 21:53:20 +00:00
Powei Feng
1cd48619e3 github: add scripts to store size of android release artifact (#9705)
- Add a script to test/sizeguard to compute sizes of artifacts
   within a compressed file.
- Add a step to postsubmit-main.yml that will run the above
   script on every commit to main.

This will enable us to add a presubmit step to guard against
dramatic size increases in the Android build.
2026-02-13 20:31:23 +00:00
Sungun Park
89c3b3f40b tools: harden zbloat against command injection (#9715)
This fixes #9701 by replacing shell execution (`shell=True` and
`os.system`) with direct subprocess calls using argument lists in
`tools/zbloat/zbloat.py`

Previously, the script used f-strings to pass paths directly into a
shell command, which created an unnecessary risk: if an external archive
contained files with shell metacharacters, it could lead to accidental
or malicious command execution during analysis.

By passing arguments as lists, the subprocess module maps them directly
to the executable, bypassing the system shell and eliminating the
vulnerability.
2026-02-13 12:03:13 -08:00
Doris Wu
487f4d077d Prevent circular buffer overflow during UboManager reallocation (#9714)
When UboManager::reallocate() is triggered, a large number of material instances may be invalidated simultaneously. This leads to a massive spike in descriptor set updates and command generation, which can overflow the circular buffer.

To prevent this, we now flush commands in batches, we trigger a flush whenever the command buffer usage exceeds half of its capacity. (Like what RenderPass::Executor::execute does)

BUGS = [474264976, 479079631]
2026-02-14 00:48:47 +08:00
Doris Wu
e830ec28e4 Prevent circular buffer overflow during UboManager reallocation (#9714)
When UboManager::reallocate() is triggered, a large number of material instances may be invalidated simultaneously. This leads to a massive spike in descriptor set updates and command generation, which can overflow the circular buffer.

To prevent this, we now flush commands in batches, we trigger a flush whenever the command buffer usage exceeds half of its capacity. (Like what RenderPass::Executor::execute does)

BUGS = [474264976, 479079631]
2026-02-14 00:39:14 +08:00
Sungun Park
e00be09af6 Revert "Set multiview as the default for stereoscopic rendering (#9682)" (#9713)
This reverts commit f10a7d9bbc.
2026-02-12 16:47:48 -08:00
Sungun Park
b58ffb87e0 Revert "Set multiview as the default for stereoscopic rendering (#9682)" (#9713)
This reverts commit f10a7d9bbc.
2026-02-12 22:13:18 +00:00
rafadevai
385d8969cf VK: Fix AHB import validation error (#9710)
In some cases the external image to be imported is
flagged as using a sRGB dataspace but when the AHB
is imported its actual format is UNDEFINED and
requires an external sampler.

In these cases, its not valid to set the
VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT flag.

See VUID-VkImageCreateInfo-pNext-02396

FIXES=[483456747]
2026-02-12 10:19:01 -08:00
Ben Doherty
53bc372876 Log Filament command buffer statistics (#9709) 2026-02-12 12:01:22 -05:00
Andrew Wilson
58f6d77e78 Add build testing checks for all the tests (#9684) 2026-02-11 12:53:40 -08:00
Filament Bot
3769d0a9d3 [automated] Updating /docs due to commit 2bc7124
Full commit hash is 2bc71240cf

DOCS_ALLOW_DIRECT_EDITS
2026-02-11 19:19:32 +00:00
Ramon
2bc71240cf Fix incorrect table structure in README (#9707)
The README had linebreaks that broke the badges in the table.
2026-02-11 19:16:56 +00:00
rafadevai
e1fb3f7442 VK: Fix validation errors when creating a sampler (#9708)
When creating a sampler with a chroma conversion
anisotropic must be disabled and the addressModes
must be set to CLAMP_TO_EDGE.

FIXES = [483454760]
2026-02-11 18:56:22 +00:00
Sungun Park
e832805faf Add cube sample for Web using Feature Level 0 (#9699)
Introduce a new cube sample that utilizes the feature level 0 (FL0) on
the web. The `Engine.create` method accepts `config` as an argument,
allowing users to explicitly request a GLES 2.0 context.

In addition, this change helps a future implementation of asynchronous
support for web, allowing the asynchronous mode to be turned on via the
configuration setting.

BUGS=[476134614]
2026-02-11 09:59:25 -08:00
Filament Bot
2ce71d6d98 [automated] Updating /docs due to commit 26c51e0
Full commit hash is 26c51e0d9a

DOCS_ALLOW_DIRECT_EDITS
2026-02-11 00:10:00 +00:00
Romain Guy
26c51e0d9a Update README with new badge links and features (#9704)
- Replace broken Maven badge links with shields. links
- Updated missing rendering features
2026-02-11 00:07:31 +00:00
rafadevai
510ae15867 VK: Fix the descriptor set layout mismatch errors (#9703)
* VK: Fix the descriptor set layout mismatch errors

Depending on how the descriptor sets and external
sampled images are used is possible to get a lot of
errors about the pipeline layout and the descriptor
set layouts dont match with the validation layers.

This causes flicking artifacts and textures not being
displayed.

This is a partial rewrite of the implementation when
using external samplers through a MaterialInstance.

* Addressing review comments
2026-02-10 15:45:34 -08:00
Powei Feng
d6caa9dc0b diffimg: binary tool for comparing images (#9668)
This tool uses existing libraries: image, imageio, imageio-lite,
imagediff to perform difference comparison for on-disk images.

We refactor renderdiff to use this tool instead of using
python dependencies.


Co-authored-by: Ben Doherty <bendoherty@google.com>
2026-02-10 22:42:30 +00:00
Filament Bot
19209a00e6 [automated] Updating /docs due to commit 188113b
Full commit hash is 188113bad6

DOCS_ALLOW_DIRECT_EDITS
2026-02-10 21:34:43 +00:00
Benjamin Doherty
188113bad6 Update Maven release guide 2026-02-10 16:30:08 -05:00
Powei Feng
5916837318 metal: add readTexture implementation (#9678)
- Also added a test to verify the y-flipping mechanism of
   each backend produces the same rendering across backends.
2026-02-10 19:45:30 +00:00
Ben Doherty
27aa517c48 Add new Filament Gradle plugin (#9694) 2026-02-10 14:15:45 -05:00
Filament Bot
4622e88a6b [automated] Updating /docs due to commit 9bdb6ac
Full commit hash is 9bdb6acd63

DOCS_ALLOW_DIRECT_EDITS
2026-02-10 19:12:10 +00:00
Benjamin Doherty
9bdb6acd63 Update CocoaPods documentation on testing locally 2026-02-10 14:09:25 -05:00
Filament Bot
751d213145 [automated] Updating /docs due to commit 0c3ae45
Full commit hash is 0c3ae457a6

DOCS_ALLOW_DIRECT_EDITS
2026-02-10 18:27:57 +00:00
Sungun Park
7ea1f97b57 Bump version to 1.69.3 2026-02-10 10:22:33 -08:00
Sungun Park
0c3ae457a6 Release Filament 1.69.2 2026-02-10 10:22:22 -08:00
Anish Goyal
92d4be6923 Fix framebuffer cache memory leak (#9693)
Once a framebuffer is destroyed, it must be removed from the mapping,
and not just marked unused. Same for render passes.
2026-02-09 23:36:44 +00:00
Powei Feng
ad8c188f58 3p: update robin-map to 1.4.1 (#9698) 2026-02-09 14:01:48 -08:00
Ben Doherty
9716b3924b Changes necessary for GCC (#9672) 2026-02-09 13:36:17 -05:00
Anish Goyal
ae9b951b08 Fix a set of unmapped 3 channel formats (#9690)
These three formats would likely break even if used on a device where
they are supported, as we reshape all 3-channel buffers to 4 channel.
The data would be in the wrong format.

We will have to follow-up, as there are other formats that are affected,
but those formats likely have better driver support, as they are better
aligned formats (e.g. 16-bit, 32-bit). These three formats were 96-bit
formats, which we've remapped to 128-bit.
2026-02-09 17:52:22 +00:00
Mathias Agopian
78a0d8f4f6 use correct attachments flags for the current swapchain (#9689)
the current swapchain is associated to the default rendertarget, but
the later doesn't know which attachments the swapchain actually has.
it used to hardcode COLOR and DEPTH, but it's possible for the
swapchain to have a STENCIL attachment.

The proper way to do this is to use the swapchain's attachments when
the default rendertarget is used (which means the swapchain is used).

FIXES=[482120868]
2026-02-06 12:16:04 -08:00
Anish Goyal
675d8bc5be Prevent compilation of redundant pipelines (#9681)
If we've already destroyed a program, we shouldn't run prewarm for it,
as that could entail five pipeline compilations.
2026-02-05 23:39:15 +00:00
Patrick Ribas
a90019baa2 Remove nonlinear fog from FL0 (#9658)
* Remove fog and lighting uniforms from FL0

---------

Co-authored-by: Mathias Agopian <mathias@google.com>
2026-02-05 12:51:37 -08:00
Filament Bot
72997ee71e [automated] Updating /docs due to commit 5b63105
Full commit hash is 5b631056b1

DOCS_ALLOW_DIRECT_EDITS
2026-02-05 06:04:40 +00:00
364 changed files with 17314 additions and 8032 deletions

View File

@@ -27,17 +27,24 @@ runs:
echo "$HASH" > /tmp/commit_hash.txt
- name: Find commit message (PR)
shell: bash
id: checkout_code
if: github.event_name == 'pull_request'
run: |
BEFORE_HASH=$(git rev-parse HEAD)
echo "hash=$BEFORE_HASH" >> "$GITHUB_OUTPUT"
# Next we will checkout the actual head (not the merge commits) of the PR
AFTER_HASH="${{ github.event.pull_request.head.sha }}"
git checkout $AFTER_HASH
COMMIT_MESSAGE=$(git log -1 --no-merges)
echo "$COMMIT_MESSAGE" > /tmp/commit_msg.txt
echo "$AFTER_HASH" > /tmp/commit_hash.txt
PR_NUMBER="${{ github.event.pull_request.number }}"
# Fetch the head of the PR explicitly to handle forks. Depth 50 ensures we can traverse past recent merge commits.
git fetch --depth=50 origin "pull/$PR_NUMBER/head:pr-head"
COMMIT_HASH=$(git log -1 --no-merges pr-head --format=%H)
AUTHOR_NAME=$(git log -1 --no-merges pr-head --format=%an)
AUTHOR_EMAIL=$(git log -1 --no-merges pr-head --format=%ae)
TSTAMP=$(git log -1 --no-merges pr-head --format=%aI)
COMMIT_MESSAGE=$(git log -1 --no-merges pr-head --format=%B)
echo "commit $COMMIT_HASH" > /tmp/commit_msg.txt
echo "Author: ${AUTHOR_NAME}<${AUTHOR_EMAIL}>" >> /tmp/commit_msg.txt
echo "Date: ${TSTAMP}" >> /tmp/commit_msg.txt
echo "" >> /tmp/commit_msg.txt
echo "$COMMIT_MESSAGE" >> /tmp/commit_msg.txt
echo "$COMMIT_HASH" > /tmp/commit_hash.txt
- shell: bash
id: action_output
run: |
@@ -47,9 +54,4 @@ runs:
cat /tmp/commit_msg.txt >> "$GITHUB_OUTPUT"
echo "$DELIMITER" >> "$GITHUB_OUTPUT"
# Get the commit hash
echo "hash=$(cat /tmp/commit_hash.txt)" >> "$GITHUB_OUTPUT"
- name: Cleanup Find commit message (PR)
shell: bash
if: github.event_name == 'pull_request'
run: |
git checkout ${{ steps.checkout_code.outputs.hash }}
echo "hash=$(cat /tmp/commit_hash.txt)" >> "$GITHUB_OUTPUT"

View File

@@ -9,7 +9,7 @@ runs:
uses: actions/cache@v4
with:
path: mesa
key: ${{ runner.os }}-mesa-deps-${{ env.GITHUB_MESA_VERSION }}
key: ${{ runner.os }}-${{ runner.arch }}-mesa-deps-${{ env.GITHUB_MESA_VERSION }}
- name: Get Mesa
run: |
bash build/common/get-mesa.sh

View File

@@ -9,7 +9,7 @@ runs:
id: cache-vulkan-sdk
with:
path: ~/VulkanSDK
key: vulkansdk-${{ env.GITHUB_VULKANSDK_VERSION }}-2-${{ runner.os }}
key: vulkansdk-${{ env.GITHUB_VULKANSDK_VERSION }}-2-${{ runner.os }}-${{ runner.arch }}
- name: Download Vulkan SDK
if: steps.cache-vulkan-sdk.outputs.cache-hit != 'true'
run: |
@@ -23,6 +23,7 @@ runs:
unpack_vulkan_installer
shell: bash
- name: Run Vulkan SDK setup
if: runner.os != 'Linux'
run: |
pushd .
cd ~/VulkanSDK/${GITHUB_VULKANSDK_VERSION}

View File

@@ -11,24 +11,13 @@ runs:
echo "set man-db/auto-update false" | sudo debconf-communicate
sudo dpkg-reconfigure man-db
# Install ninja
source ./build/common/get-ninja.sh
# Install CMake
mkdir -p cmake
cd cmake
sudo wget https://github.com/Kitware/CMake/releases/download/v$GITHUB_CMAKE_VERSION/cmake-$GITHUB_CMAKE_VERSION-Linux-x86_64.sh
sudo chmod +x ./cmake-$GITHUB_CMAKE_VERSION-Linux-x86_64.sh
sudo ./cmake-$GITHUB_CMAKE_VERSION-Linux-x86_64.sh --skip-license > /dev/null
sudo update-alternatives --install /usr/bin/cmake cmake $(pwd)/bin/cmake 1000 --force
cd ..
sudo wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
sudo apt-get update
sudo apt-get install clang-$GITHUB_CLANG_VERSION libc++-$GITHUB_CLANG_VERSION-dev libc++abi-$GITHUB_CLANG_VERSION-dev
sudo apt-get install mesa-common-dev libxi-dev libxxf86vm-dev
sudo apt-get install cmake ninja-build
# For dawn
sudo apt-get install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libx11-xcb-dev

View File

@@ -0,0 +1,32 @@
name: 'Renderdiff Generate'
description: 'Sets up prerequisites and runs the generate script for renderdiff'
runs:
using: "composite"
steps:
- uses: ./.github/actions/mac-prereq
- uses: ./.github/actions/get-gltf-assets
- uses: ./.github/actions/get-mesa
- uses: ./.github/actions/get-vulkan-sdk
- name: Prerequisites
if: runner.os == 'macOS'
run: |
# Must have at least clang-16 for a webgpu/dawn build.
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
# this enforces the gltf_viewer build to use apple clang as oppose the brew clang used for get-mesa
echo "CC=/usr/bin/clang" >> $GITHUB_ENV
echo "CXX=/usr/bin/clang++" >> $GITHUB_ENV
echo "/usr/local/bin" >> $GITHUB_PATH
echo "/usr/bin" >> $GITHUB_PATH
shell: bash
- name: Generate images
run: |
TEST_DIR=test/renderdiff
source ${TEST_DIR}/src/preamble.sh
set -eux
bash ${TEST_DIR}/generate.sh
set +eux
shell: bash
- name: Build diffimg tool
run: |
./build.sh release diffimg
shell: bash

View File

@@ -1,4 +1,4 @@
FROM python:3.10.4
FROM python:3.12.13
RUN pip3 install pygithub==1.55

View File

@@ -1,4 +1,4 @@
name: 'Web Preqrequisites'
name: 'Web Prerequisites'
runs:
using: "composite"
steps:
@@ -8,7 +8,7 @@ runs:
uses: actions/cache@v4 # Use a specific version
with:
path: emsdk
key: ${{ runner.os }}-emsdk-${{ env.GITHUB_EMSDK_VERSION }}
key: ${{ runner.os }}-${{ runner.arch }}-emsdk-${{ env.GITHUB_EMSDK_VERSION }}
- name: Install Web Prerequisites
shell: bash
run: |

View File

@@ -6,18 +6,31 @@ on:
- main
jobs:
# Update the renderdiff goldens in filament-assets. This will add or merge the new goldens from
# a branch on filament-assets.
update-renderdiff-goldens:
name: update-renderdiff-goldens
runs-on: 'ubuntu-24.04-4core'
runs-on: 'macos-14'
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- name: Prerequisites
run: pip install tifffile numpy
- name: Check if accepting new goldens
id: check_accept
env:
COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }}
run: |
if echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py --mode=accept_new_goldens; then
echo "accept=true" >> "$GITHUB_OUTPUT"
else
echo "accept=false" >> "$GITHUB_OUTPUT"
fi
shell: bash
- name: Renderdiff Generate for new goldens
if: steps.check_accept.outputs.accept == 'true'
uses: ./.github/actions/renderdiff-generate
- name: Run update script
env:
GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }}
@@ -25,16 +38,31 @@ jobs:
run: |
GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py)
COMMIT_HASH="${{ steps.get_commit_msg.outputs.hash }}"
git config --global user.email "filament.bot@gmail.com"
git config --global user.name "Filament Bot"
git config --global credential.helper cache
if [[ "${{ steps.check_accept.outputs.accept }}" == "true" ]]; then
SHORT_HASH="${COMMIT_HASH:0:8}"
GOLDEN_BRANCH="accept-goldens-${SHORT_HASH}"
echo "Generating new goldens for branch ${GOLDEN_BRANCH}"
python3 test/renderdiff/src/update_golden.py \
--branch=${GOLDEN_BRANCH} \
--source=$(pwd)/out/renderdiff/renders \
--commit-msg="Auto-update goldens from ${COMMIT_HASH}" \
--push-to-remote \
--golden-repo-token=${GH_TOKEN}
fi
if [[ "${GOLDEN_BRANCH}" != "main" ]]; then
git config --global user.email "filament.bot@gmail.com"
git config --global user.name "Filament Bot"
git config --global credential.helper cache
echo "branch==${GOLDEN_BRANCH}"
echo "hash==${COMMIT_HASH}"
python3 test/renderdiff/src/update_golden.py --branch=${GOLDEN_BRANCH} \
--merge-to-main --filament-tag=${COMMIT_HASH} --golden-repo-token=${GH_TOKEN}
fi
# Update the /docs (offiicla github-hosted Filament site) if necessary
update-docs:
name: update-docs
runs-on: 'ubuntu-24.04-4core'
@@ -57,3 +85,40 @@ jobs:
git config --global user.name "Filament Bot"
git config --global credential.helper cache
bash docs_src/build/postsubmit.sh ${COMMIT_HASH} ${GH_TOKEN}
# Produce a json that describes the android artifact sizes, and will push that json to a folder in
# filament-assets
update-sizeguard:
name: update-sizeguard
runs-on: 'ubuntu-24.04-4core'
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- name: Build and generate size report
run: |
cd build/android && printf "y" | ./build.sh release all
cd ../..
COMMIT_HASH="${{ steps.get_commit_msg.outputs.hash }}"
python3 test/sizeguard/dump_artifact_size.py out/*.tgz out/*.aar > "${COMMIT_HASH}.json"
- name: Push to filament-assets
env:
GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }}
run: |
COMMIT_HASH="${{ steps.get_commit_msg.outputs.hash }}"
git config --global user.email "filament.bot@gmail.com"
git config --global user.name "Filament Bot"
git clone https://x-access-token:${GH_TOKEN}@github.com/google/filament-assets.git filament-assets
mkdir -p filament-assets/sizeguard
mv "${COMMIT_HASH}.json" filament-assets/sizeguard/
cd filament-assets
git add sizeguard/"${COMMIT_HASH}.json"
git commit -m "Update sizeguard for filament@${COMMIT_HASH}" || echo "No changes to commit"
git push https://x-access-token:${GH_TOKEN}@github.com/google/filament-assets.git main

View File

@@ -12,8 +12,7 @@ jobs:
name: build-android
# We intentially use a larger runner here to enable larger disk space
# (standard linux runner will fail on disk space and faster build time).
runs-on: 'ubuntu-24.04-16core'
runs-on: 'ubuntu-24.04-4core'
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -26,8 +25,7 @@ jobs:
build-ios:
name: build-ios
runs-on: macos-14-xlarge
runs-on: 'macos-14'
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -46,8 +44,7 @@ jobs:
build-linux:
name: build-linux
runs-on: 'ubuntu-24.04-16core'
runs-on: 'arm-ubuntu-24.04-16core'
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -58,13 +55,12 @@ jobs:
cd build/linux && printf "y" | ./build.sh continuous
- uses: actions/upload-artifact@v4
with:
name: filament-linux
path: out/filament-release-linux.tgz
name: filament-arm-linux
path: out/filament-release-arm-linux.tgz
build-mac:
name: build-mac
runs-on: macos-14-xlarge
runs-on: 'macos-14'
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -77,14 +73,10 @@ jobs:
with:
name: filament-mac
path: out/filament-release-darwin.tgz
- name: Check public headers
run: |
test/check-headers/test.sh out/release/filament/include
build-web:
name: build-web
runs-on: 'ubuntu-24.04-16core'
runs-on: 'arm-ubuntu-24.04-16core'
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -101,7 +93,7 @@ jobs:
build-windows:
name: build-windows
runs-on: windows-2022-32core
runs-on: 'windows-2022'
steps:
- uses: actions/checkout@v4.1.6

View File

@@ -8,10 +8,54 @@ on:
branches:
- main
# This will cancel in-flight runs when there is an update to a PR
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# The conditional 'if' on each job is meant to skip presubmit jobs when a commit is pushed to main
# and that commit is cryptographically "verified". Typically, a verified commit (like a GitHub UI
# squash-and-merge) has already passed through presubmit during the Pull Request phase.
# The conditional explicitly checks:
# 1. always() && !cancelled(): Ensures the job runs even if 'check-verification' is skipped, but
# aborts if the workflow was manually cancelled.
# 2. github.event_name == 'pull_request': Presubmits should always run normally on PRs.
# 3. github.event_name == 'push' && ...: If it's a push to main, it only runs if the
# 'check-verification' job confirmed the commit is NOT verified.
jobs:
check-verification:
if: github.event_name == 'push'
runs-on: ubuntu-latest
outputs:
verified: ${{ steps.check.outputs.verified }}
steps:
- name: Check commit verification
id: check
uses: actions/github-script@v7
with:
script: |
const { data } = await github.rest.repos.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha
});
core.setOutput('verified', data.commit.verification.verified);
build-desktop-mac:
name: build-mac
runs-on: macos-14-xlarge
runs-on: 'macos-14-xlarge'
needs: [check-verification]
if: >
always() && !cancelled() &&
(
github.event_name == 'pull_request' ||
(
github.event_name == 'push' &&
needs.check-verification.result == 'success' &&
needs.check-verification.outputs.verified == 'false'
)
)
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -20,13 +64,30 @@ jobs:
- name: Run build script
run: |
cd build/mac && printf "y" | ./build.sh presubmit-with-test
- name: Test material parser
- name: Test - material parser
run: |
out/cmake-release/filament/test/test_material_parser
- name: Test - public headers
run: |
# out/cmake-release should have the artifacts ready for installation. Here we install it
# to test the public headers
ninja -C out/cmake-release install
test/check-headers/test.sh out/release/filament/include
build-desktop-linux:
name: build-linux
runs-on: 'ubuntu-24.04-16core'
runs-on: 'arm-ubuntu-24.04-16core'
needs: [check-verification]
if: >
always() && !cancelled() &&
(
github.event_name == 'pull_request' ||
(
github.event_name == 'push' &&
needs.check-verification.result == 'success' &&
needs.check-verification.outputs.verified == 'false'
)
)
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -35,13 +96,24 @@ jobs:
- name: Run build script
run: |
cd build/linux && printf "y" | ./build.sh presubmit
- name: Test material parser
- name: Test - material parser
run: |
out/cmake-release/filament/test/test_material_parser
build-windows:
name: build-windows
runs-on: windows-2022-32core
runs-on: 'windows-2022'
needs: [check-verification]
if: >
always() && !cancelled() &&
(
github.event_name == 'pull_request' ||
(
github.event_name == 'push' &&
needs.check-verification.result == 'success' &&
needs.check-verification.outputs.verified == 'false'
)
)
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -53,7 +125,18 @@ jobs:
build-android:
name: build-android
runs-on: 'ubuntu-24.04-16core'
runs-on: 'ubuntu-24.04-4core'
needs: [check-verification]
if: >
always() && !cancelled() &&
(
github.event_name == 'pull_request' ||
(
github.event_name == 'push' &&
needs.check-verification.result == 'success' &&
needs.check-verification.outputs.verified == 'false'
)
)
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -67,11 +150,31 @@ jobs:
# Only build 1 64 bit target during presubmit to cut down build times during presubmit
# Continuous builds will build everything
run: |
cd build/android && printf "y" | ./build.sh presubmit arm64-v8a
pushd .
cd build/android && printf "y" | ./build.sh presubmit-with-archive arm64-v8a
popd
- name: Check artifact sizes
run: |
python3 test/sizeguard/dump_artifact_size.py out/*.aar > current_size.json
python3 test/sizeguard/check_size.py current_size.json \
--target-branch origin/main \
--threshold 20480 \
--artifacts filament-android-release.aar/jni/arm64-v8a/libfilament-jni.so
build-ios:
name: build-iOS
runs-on: macos-14-xlarge
runs-on: 'macos-14-xlarge'
needs: [check-verification]
if: >
always() && !cancelled() &&
(
github.event_name == 'pull_request' ||
(
github.event_name == 'push' &&
needs.check-verification.result == 'success' &&
needs.check-verification.outputs.verified == 'false'
)
)
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -86,7 +189,18 @@ jobs:
build-web:
name: build-web
runs-on: 'ubuntu-24.04-16core'
runs-on: 'arm-ubuntu-24.04-16core'
needs: [check-verification]
if: >
always() && !cancelled() &&
(
github.event_name == 'pull_request' ||
(
github.event_name == 'push' &&
needs.check-verification.result == 'success' &&
needs.check-verification.outputs.verified == 'false'
)
)
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -99,7 +213,18 @@ jobs:
validate-docs:
name: validate-docs
runs-on: 'ubuntu-24.04-4core'
runs-on: 'ubuntu-24.04'
needs: [check-verification]
if: >
always() && !cancelled() &&
(
github.event_name == 'pull_request' ||
(
github.event_name == 'push' &&
needs.check-verification.result == 'success' &&
needs.check-verification.outputs.verified == 'false'
)
)
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -112,24 +237,40 @@ jobs:
test-renderdiff:
name: test-renderdiff
runs-on: macos-14-xlarge
runs-on: 'macos-14-xlarge'
needs: [check-verification]
if: >
always() && !cancelled() &&
(
github.event_name == 'pull_request' ||
(
github.event_name == 'push' &&
needs.check-verification.result == 'success' &&
needs.check-verification.outputs.verified == 'false'
)
)
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/mac-prereq
- uses: ./.github/actions/get-gltf-assets
- uses: ./.github/actions/get-mesa
- uses: ./.github/actions/get-vulkan-sdk
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- name: Prerequisites
- name: Check if accepting new goldens
id: check_accept
env:
COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }}
run: |
pip install tifffile numpy
# Must have at least clang-16 for a webgpu/dawn build.
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
if echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py --mode=accept_new_goldens; then
echo "accept=true" >> "$GITHUB_OUTPUT"
else
echo "accept=false" >> "$GITHUB_OUTPUT"
fi
shell: bash
- name: Render and compare
- name: Renderdiff generate
if: steps.check_accept.outputs.accept != 'true'
uses: ./.github/actions/renderdiff-generate
- name: Compare rendered images
if: steps.check_accept.outputs.accept != 'true'
id: render_compare
env:
COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }}
@@ -138,7 +279,7 @@ jobs:
source ${TEST_DIR}/src/preamble.sh
set -eux
GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 ${TEST_DIR}/src/commit_msg.py)
bash ${TEST_DIR}/generate.sh
python3 ${TEST_DIR}/src/golden_manager.py \
--branch=${GOLDEN_BRANCH} \
--output=${GOLDEN_OUTPUT_DIR}
@@ -149,7 +290,9 @@ jobs:
python3 ${TEST_DIR}/src/compare.py \
--src=${GOLDEN_OUTPUT_DIR} \
--dest=${RENDER_OUTPUT_DIR} \
--out=${DIFF_OUTPUT_DIR} 2>&1 | tee compare_output.txt
--out=${DIFF_OUTPUT_DIR} \
--diffimg="$(pwd)/out/cmake-release/tools/diffimg/diffimg" \
--test="${TEST_DIR}/tests/presubmit.json" 2>&1 | tee compare_output.txt
if grep "Failed" compare_output.txt > /dev/null; then
DELIMITER="EOF_FILE_CONTENT_$(date +%s)" # Using timestamp to make it more unique
@@ -158,11 +301,14 @@ jobs:
echo "$DELIMITER" >> "$GITHUB_OUTPUT"
fi
shell: bash
- uses: actions/upload-artifact@v4
- name: Upload artifacts
uses: actions/upload-artifact@v4
if: steps.check_accept.outputs.accept != 'true'
with:
name: presubmit-renderdiff-result
path: ./out/renderdiff
- name: Compare result
- name: Check results
if: steps.check_accept.outputs.accept != 'true'
run: |
ERROR_STR="${{ steps.render_compare.outputs.err }}"
if [ -n "${ERROR_STR}" ]; then
@@ -170,9 +316,20 @@ jobs:
exit 1
fi
validate-wgsl-webgpu:
build-wgsl-webgpu:
name: validate-wgsl-webgpu
runs-on: 'ubuntu-24.04-16core'
runs-on: 'arm-ubuntu-24.04-16core'
needs: [check-verification]
if: >
always() && !cancelled() &&
(
github.event_name == 'pull_request' ||
(
github.event_name == 'push' &&
needs.check-verification.result == 'success' &&
needs.check-verification.outputs.verified == 'false'
)
)
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -186,6 +343,17 @@ jobs:
test-code-correctness:
name: test-code-correctness
runs-on: 'macos-14-xlarge'
needs: [check-verification]
if: >
always() && !cancelled() &&
(
github.event_name == 'pull_request' ||
(
github.event_name == 'push' &&
needs.check-verification.result == 'success' &&
needs.check-verification.outputs.verified == 'false'
)
)
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -206,7 +374,18 @@ jobs:
test-backend:
name: test-backend
runs-on: macos-14-xlarge
runs-on: 'macos-14-xlarge'
needs: [check-verification]
if: >
always() && !cancelled() &&
(
github.event_name == 'pull_request' ||
(
github.event_name == 'push' &&
needs.check-verification.result == 'success' &&
needs.check-verification.outputs.verified == 'false'
)
)
steps:
- uses: actions/checkout@v4.1.6
with:

View File

@@ -63,6 +63,39 @@ jobs:
const globber = await glob.create('out/*.tgz');
await upload({ github, context }, await globber.glob(), TAG);
build-arm-linux:
name: build-arm-linux
runs-on: 'arm-ubuntu-24.04-16core'
if: github.event_name == 'release' || github.event.inputs.platform == 'desktop'
steps:
- name: Decide Git ref
id: git_ref
run: |
REF=${RELEASE_TAG:-${GITHUB_REF}}
TAG=${REF##*/}
echo "ref=${REF}" >> $GITHUB_OUTPUT
echo "tag=${TAG}" >> $GITHUB_OUTPUT
- uses: actions/checkout@v4.1.6
with:
ref: ${{ steps.git_ref.outputs.ref }}
- uses: ./.github/actions/linux-prereq
- name: Run build script
env:
TAG: ${{ steps.git_ref.outputs.tag }}
run: |
cd build/linux && printf "y" | ./build.sh release
cd ../..
mv out/filament-release-linux.tgz out/filament-${TAG}-arm-linux.tgz
- uses: actions/github-script@v6
env:
TAG: ${{ steps.git_ref.outputs.tag }}
with:
script: |
const upload = require('./build/common/upload-release-assets');
const { TAG } = process.env;
const globber = await glob.create('out/*.tgz');
await upload({ github, context }, await globber.glob(), TAG);
build-mac:
name: build-mac
runs-on: macos-14-xlarge
@@ -99,7 +132,7 @@ jobs:
build-web:
name: build-web
runs-on: 'ubuntu-24.04-16core'
runs-on: 'arm-ubuntu-24.04-16core'
if: github.event_name == 'release' || github.event.inputs.platform == 'web'
steps:
@@ -188,6 +221,54 @@ jobs:
const globber = await glob.create(['out/*.aar', 'out/*.apk', 'out/*.tgz'].join('\n'));
await upload({ github, context }, await globber.glob(), TAG);
sonatype-publish:
name: sonatype-publish
runs-on: 'ubuntu-24.04-16core'
# Depends on the the Android build for the Android binaries.
# Depends on the Mac, Linux, and Windows builds for host tools.
needs: [build-mac, build-linux, build-windows, build-android]
if: github.event_name == 'release' || github.event.inputs.platform == 'android'
steps:
- name: Decide Git ref
id: git_ref
run: |
REF=${RELEASE_TAG:-${GITHUB_REF}}
TAG=${REF##*/}
echo "ref=${REF}" >> $GITHUB_OUTPUT
echo "tag=${TAG}" >> $GITHUB_OUTPUT
- uses: actions/checkout@v4.1.6
with:
ref: ${{ steps.git_ref.outputs.ref }}
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- uses: ./.github/actions/linux-prereq
- name: Download Android Release
run: |
gh release download ${TAG} \
--repo ${{ github.repository }} \
--pattern 'filament-*-android-native.tgz'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.git_ref.outputs.tag }}
- name: Unzip Android Release
run: |
mkdir -p out/android-release
tar -xzvf filament-${TAG}-android-native.tgz -C out/android-release/
env:
TAG: ${{ steps.git_ref.outputs.tag }}
- name: Publish To Sonatype
run: |
cd android
./gradlew publishToSonatype closeSonatypeStagingRepository
env:
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USERNAME }}
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }}
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.MAVEN_SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.MAVEN_SIGNING_PASSWORD }}
build-ios:
name: build-ios
runs-on: macos-14-xlarge

View File

@@ -11,14 +11,11 @@ jobs:
name: Verify Release Notes
runs-on: ubuntu-latest
steps:
# We *only* need to check out the .github/ directory.
# The verify RELEASE_NOTES script uses the GitHub API to verify that RELEASE_NOTES was
# modified.
- name: Check out action
uses: Bhacaz/checkout-files@73e17cfbe8d7e0c6b2672b20cb05a718e20d18d4
- uses: actions/checkout@v6
with:
files: .github
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
sparse-checkout: |
.github
- name: Verify release notes
uses: ./.github/actions/verify-release-notes
with:

View File

@@ -49,6 +49,8 @@ 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)
option(FILAMENT_ENABLE_FGVIEWER "Enable the frame graph viewer" OFF)
@@ -63,6 +65,8 @@ option(FILAMENT_SUPPORTS_WEBP_TEXTURES "Enable webp texture support for Filament
# On the regular filament build (where size is of less concern), we enable GTAO by default.
option(FILAMENT_DISABLE_GTAO "Disable GTAO" OFF)
option(FILAMENT_BUILD_TESTING "Build tests" ON)
set(FILAMENT_NDK_VERSION "" CACHE STRING
"Android NDK version or version prefix to be used when building for Android."
)
@@ -322,11 +326,13 @@ endif()
if (FILAMENT_ENABLE_LTO)
include(CheckIPOSupported)
check_ipo_supported(RESULT IPO_SUPPORT)
check_ipo_supported(RESULT IPO_SUPPORT OUTPUT IPO_ERROR)
if (IPO_SUPPORT)
message(STATUS "LTO support is enabled")
message(STATUS "IPO / LTO is enabled")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
else()
message(WARNING "IPO / LTO is not supported by this architecture: ${IPO_ERROR}")
endif()
endif()
@@ -605,6 +611,23 @@ else()
option(FILAMENT_DISABLE_MATOPT "Disable material optimizations" ON)
endif()
# This only affects the prebuilt shader files in gltfio and samples, not filament library.
# The value can be either "instanced", "multiview", or "none"
set(FILAMENT_SAMPLES_STEREO_TYPE "none" CACHE STRING
"Stereoscopic type that shader files in gltfio and samples are built for."
)
string(TOLOWER "${FILAMENT_SAMPLES_STEREO_TYPE}" FILAMENT_SAMPLES_STEREO_TYPE)
if (NOT FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "instanced"
AND NOT FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "multiview"
AND NOT FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "none")
message(FATAL_ERROR "Invalid stereo type: \"${FILAMENT_SAMPLES_STEREO_TYPE}\" choose either \"instanced\", \"multiview\", or \"none\" ")
endif ()
# Compiling samples for multiview implies enabling multiview feature as well.
if (FILAMENT_SAMPLES_STEREO_TYPE STREQUAL "multiview")
set(FILAMENT_ENABLE_MULTIVIEW ON)
endif ()
# Define backend flag for debug only
if (CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT FILAMENT_BACKEND_DEBUG_FLAG STREQUAL "")
add_definitions(-DFILAMENT_BACKEND_DEBUG_FLAG=${FILAMENT_BACKEND_DEBUG_FLAG})
@@ -846,6 +869,12 @@ endfunction()
# Sub-projects
# ==================================================================================================
include(CheckSymbolExists)
check_symbol_exists(getopt_long "getopt.h" HAS_SYSTEM_GETOPT)
if (NOT HAS_SYSTEM_GETOPT)
add_subdirectory(${EXTERNAL}/getopt)
endif()
# Common to all platforms
add_subdirectory(${EXTERNAL}/libgtest/tnt)
add_subdirectory(${LIBRARIES}/camutils)
@@ -881,7 +910,6 @@ add_subdirectory(${EXTERNAL}/cgltf/tnt)
add_subdirectory(${EXTERNAL}/draco/tnt)
add_subdirectory(${EXTERNAL}/jsmn/tnt)
add_subdirectory(${EXTERNAL}/stb/tnt)
add_subdirectory(${EXTERNAL}/getopt)
add_subdirectory(${EXTERNAL}/perfetto/tnt)
add_subdirectory(${EXTERNAL}/basisu/tnt)
@@ -955,6 +983,7 @@ if (IS_HOST_PLATFORM)
add_subdirectory(${TOOLS}/cmgen)
add_subdirectory(${TOOLS}/cso-lut)
add_subdirectory(${TOOLS}/diffimg)
add_subdirectory(${TOOLS}/filamesh)
add_subdirectory(${TOOLS}/glslminifier)
add_subdirectory(${TOOLS}/matc)

View File

@@ -31,7 +31,7 @@ repositories {
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.69.2'
implementation 'com.google.android.filament:filament-android:1.70.2'
}
```
@@ -39,19 +39,18 @@ Here are all the libraries available in the group `com.google.android.filament`:
| Artifact | Description |
| ------------- | ------------- |
| [![filament-android](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android/badge.svg?subject=filament-android)](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android) | The Filament rendering engine itself. |
| [![filament-android-debug](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android-debug/badge.svg?subject=filament-android-debug)](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android-debug) | Debug version of `filament-android`. |
| [![gltfio-android](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/gltfio-android/badge.svg?subject=gltfio-android)](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/gltfio-android) | A glTF 2.0 loader for Filament, depends on `filament-android`. |
| [![filament-utils-android](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-utils-android/badge.svg?subject=filament-utils-android)](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-utils-android) | KTX loading, Kotlin math, and camera utilities, depends on `gltfio-android`. |
| [![filamat-android](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android/badge.svg?subject=filamat-android)](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android) | A runtime material builder/compiler. This library is large but contains a full shader compiler/validator/optimizer and supports both OpenGL and Vulkan. |
| [![filamat-android-lite](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android-lite/badge.svg?subject=filamat-android-lite)](https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android-lite) | A much smaller alternative to `filamat-android` that can only generate OpenGL shaders. It does not provide validation or optimizations. |
| [![filament-android](https://img.shields.io/maven-central/v/com.google.android.filament/filament-android?label=filament-android&color=green)](https://mvnrepository.com/artifact/com.google.android.filament/filament-android) | The Filament rendering engine itself. |
| [![filament-android-debug](https://img.shields.io/maven-central/v/com.google.android.filament/filament-android-debug?label=filament-android-debug&color=green)](https://mvnrepository.com/artifact/com.google.android.filament/filament-android-debug) | Debug version of `filament-android`. |
| [![gltfio-android](https://img.shields.io/maven-central/v/com.google.android.filament/gltfio-android?label=gltfio-android&color=green)](https://mvnrepository.com/artifact/com.google.android.filament/gltfio-android) | A glTF 2.0 loader for Filament, depends on `filament-android`. |
| [![filament-utils-android](https://img.shields.io/maven-central/v/com.google.android.filament/filament-utils-android?label=filament-utils-android&color=green)](https://mvnrepository.com/artifact/com.google.android.filament/filament-utils-android) | KTX loading, Kotlin math, and camera utilities, depends on `gltfio-android`. |
| [![filamat-android](https://img.shields.io/maven-central/v/com.google.android.filament/filamat-android?label=filamat-android&color=green)](https://mvnrepository.com/artifact/com.google.android.filament/filamat-android) | A runtime material builder/compiler. This library is large but contains a full shader compiler/validator/optimizer and supports both OpenGL and Vulkan. |
### iOS
iOS projects can use CocoaPods to install the latest release:
```shell
pod 'Filament', '~> 1.69.2'
pod 'Filament', '~> 1.70.2'
```
## Documentation
@@ -89,7 +88,8 @@ pod 'Filament', '~> 1.69.2'
- OpenGL ES 3.0+ for Android and iOS
- Metal for macOS and iOS
- Vulkan 1.0 for Android, Linux, macOS, and Windows
- WebGL 2.0 for all platforms
- WebGPU for Android, Linux, macOS, and Windows
- WebGL 2.0 for all browsers supporting it
### Rendering
@@ -124,7 +124,7 @@ pod 'Filament', '~> 1.69.2'
- HDR bloom
- Depth of field bokeh
- Multiple tone mappers: generic (customizable), ACES, filmic, etc.
- Multiple tone mappers: PBR Neutral, AgX, generic (customizable), ACES, filmic, etc.
- Color and tone management: luminance scaling, gamut mapping
- Color grading: exposure, night adaptation, white balance, channel mixer,
shadows/mid-tones/highlights, ASC CDL, contrast, saturation, etc.
@@ -158,15 +158,16 @@ pod 'Filament', '~> 1.69.2'
- [x] KHR_draco_mesh_compression
- [x] KHR_lights_punctual
- [x] KHR_materials_clearcoat
- [x] KHR_materials_dispersion
- [x] KHR_materials_emissive_strength
- [x] KHR_materials_ior
- [x] KHR_materials_pbrSpecularGlossiness
- [x] KHR_materials_sheen
- [x] KHR_materials_specular
- [x] KHR_materials_transmission
- [x] KHR_materials_unlit
- [x] KHR_materials_variants
- [x] KHR_materials_volume
- [x] KHR_materials_specular
- [x] KHR_mesh_quantization
- [x] KHR_texture_basisu
- [x] KHR_texture_transform
@@ -331,7 +332,7 @@ and tools.
- `filamesh`: Mesh converter
- `glslminifier`: Minifies GLSL source code
- `matc`: Material compiler
- `filament-matp`: Material parser
- `matedit`: Material editor for compiled materials
- `matinfo` Displays information about materials compiled with `matc`
- `mipgen` Generates a series of miplevels from a source image
- `normal-blending`: Tool to blend normal maps

View File

@@ -7,6 +7,27 @@ 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.70.2
## v1.70.1
## v1.70.0
- engine: fix crash when using variance shadow maps
- materials: better shadow normal-bias calculations [⚠️ **New Material Version**]
## v1.69.5
- engine: fix crash when using variance shadow maps
## v1.69.4
## v1.69.3
## v1.69.2
- engine: fix shader compilation failure in TAA material

View File

@@ -40,15 +40,21 @@
// - Build and upload artifacts with ./gradlew publish
// - Close and release staging repo on Nexus with ./gradlew closeAndReleaseStagingRepository
//
// The following is needed in ~/gradle/gradle.properties:
// The following properties need to be set (either in ~/gradle/gradle.properties, on the command
// line, or as environment variables, e.g.: ORG_GRADLE_PROJECT_property=value):
//
// sonatypeUsername=nexus_user
// sonatypePassword=nexus_password
//
// To sign with a key ring file:
// signing.keyId=pgp_key_id
// signing.password=pgp_key_password
// signing.secretKeyRingFile=/Users/user/.gnupg/maven_signing.key
//
// To sign with in-memory keys (useful for CI):,
// signingKey=ASCII armored key (begins with -----BEGIN PGP PRIVATE KEY BLOCK-----)
// signingPassword=key password
//
buildscript {
def path = providers
@@ -194,7 +200,7 @@ subprojects {
google()
}
if (!name.startsWith("sample") && name != "filament-tools") {
if (!name.startsWith("sample") && name != "filament-tools" && name != "gradle-plugin") {
apply plugin: 'com.android.library'
android {

View File

@@ -1,16 +0,0 @@
plugins {
id 'groovy-gradle-plugin'
}
gradlePlugin {
plugins {
create("filament-tools-plugin") {
id = "filament-tools-plugin"
implementationClass = "FilamentToolsPlugin"
}
}
}
repositories {
mavenCentral()
}

View File

@@ -1,359 +0,0 @@
// This plugin accepts the following parameters:
//
// com.google.android.filament.tools-dir
// Path to the Filament distribution/install directory for desktop.
// This directory must contain bin/matc.
//
// com.google.android.filament.exclude-vulkan
// When set, support for Vulkan will be excluded.
//
// Example:
// ./gradlew -Pcom.google.android.filament.tools-dir=../../dist-release assembleDebug
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileType
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.logging.LogLevel
import org.gradle.api.logging.Logger
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.incremental.InputFileDetails
import org.gradle.api.model.ObjectFactory
import org.gradle.internal.os.OperatingSystem
import org.gradle.process.ExecOperations
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import java.nio.file.Paths
import javax.inject.Inject
abstract class TaskWithBinary extends DefaultTask {
private final String binaryName
private Property<String> binaryPath = null
TaskWithBinary(String name) {
binaryName = name
}
@Inject abstract ObjectFactory getObjects()
@Inject abstract ProviderFactory getProviders()
@Input
Property<String> getBinary() {
if (binaryPath == null) {
def tool = ["/bin/${binaryName}.exe", "/bin/${binaryName}"]
def fullPath = tool.collect { path ->
def filamentToolsPath = providers
.gradleProperty("com.google.android.filament.tools-dir")
.forUseAtConfigurationTime().get()
def directory = objects.fileProperty()
.fileValue(new File(filamentToolsPath)).getAsFile().get()
Paths.get(directory.absolutePath, path).toFile()
}
binaryPath = objects.property(String.class)
binaryPath.set(
(OperatingSystem.current().isWindows() ? fullPath[0] : fullPath[1]).toString())
}
return binaryPath
}
}
class LogOutputStream extends ByteArrayOutputStream {
private final Logger logger
private final LogLevel level
LogOutputStream(Logger logger, LogLevel level) {
this.logger = logger
this.level = level
}
@Override
void flush() {
logger.log(level, toString())
reset()
}
}
// Custom task to compile material files using matc
// This task handles incremental builds
abstract class MaterialCompiler extends TaskWithBinary {
@Incremental
@InputDirectory
abstract DirectoryProperty getInputDir()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@Inject abstract FileSystemOperations getFs()
@Inject abstract ExecOperations getExec()
@Inject abstract ObjectFactory getObjects()
@Inject abstract ProviderFactory getProviders()
MaterialCompiler() {
super("matc")
}
@TaskAction
void execute(InputChanges inputs) {
if (!inputs.incremental) {
fs.delete({
delete(objects.fileTree().from(outputDir).matching { include '*.filamat' })
})
}
inputs.getFileChanges(inputDir).each { InputFileDetails change ->
if (change.fileType == FileType.DIRECTORY) return
def file = change.file
if (change.changeType == ChangeType.REMOVED) {
getOutputFile(file).delete()
} else {
def out = new LogOutputStream(logger, LogLevel.LIFECYCLE)
def err = new LogOutputStream(logger, LogLevel.ERROR)
def header = ("Compiling material " + file + "\n").getBytes()
out.write(header)
out.flush()
if (!new File(binary.get()).exists()) {
throw new GradleException("Could not find ${binary.get()}." +
" Ensure Filament has been built/installed before building this app.")
}
def matcArgs = []
def exclude_vulkan = providers
.gradleProperty("com.google.android.filament.exclude-vulkan")
.forUseAtConfigurationTime().present
if (!exclude_vulkan) {
matcArgs += ['-a', 'vulkan']
}
def include_webgpu = providers
.gradleProperty("com.google.android.filament.include-webgpu")
.forUseAtConfigurationTime().present
if (include_webgpu) {
matcArgs += ['-a', 'webgpu', '--variant-filter=stereo']
}
def mat_no_opt = providers
.gradleProperty("com.google.android.filament.matnopt")
.forUseAtConfigurationTime().present
if (mat_no_opt) {
matcArgs += ['-g']
}
matcArgs += ['-a', 'opengl', '-p', 'mobile', '-o', getOutputFile(file), file]
exec.exec {
standardOutput out
errorOutput err
executable "${binary.get()}"
args matcArgs
}
}
}
}
File getOutputFile(final File file) {
return outputDir.file(file.name[0..file.name.lastIndexOf('.')] + 'filamat').get().asFile
}
}
// Custom task to process IBLs using cmgen
// This task handles incremental builds
abstract class IblGenerator extends TaskWithBinary {
@Input
@Optional
abstract Property<String> getCmgenArgs()
@Incremental
@InputFile
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@Inject abstract FileSystemOperations getFs()
@Inject abstract ExecOperations getExec()
@Inject abstract ObjectFactory getObjects()
IblGenerator() {
super("cmgen")
}
@TaskAction
void execute(InputChanges inputs) {
if (!inputs.incremental) {
fs.delete({
delete(objects.fileTree().from(outputDir).matching { include '*' })
})
}
inputs.getFileChanges(inputFile).each { InputFileDetails change ->
if (change.fileType == FileType.DIRECTORY) return
def file = change.file
if (change.changeType == ChangeType.REMOVED) {
getOutputFile(file).delete()
} else {
def out = new LogOutputStream(logger, LogLevel.LIFECYCLE)
def err = new LogOutputStream(logger, LogLevel.ERROR)
def header = ("Generating IBL " + file + "\n").getBytes()
out.write(header)
out.flush()
if (!new File(binary.get()).exists()) {
throw new GradleException("Could not find ${binary.get()}." +
" Ensure Filament has been built/installed before building this app.")
}
def outputPath = outputDir.get().asFile
def commandArgs = cmgenArgs.getOrNull()
if (commandArgs == null) {
commandArgs =
'-q -x ' + outputPath + ' --format=rgb32f ' +
'--extract-blur=0.08 --extract=' + outputPath.absolutePath
}
commandArgs = commandArgs + " " + file
exec.exec {
standardOutput out
errorOutput err
executable "${binary.get()}"
args(commandArgs.split())
}
}
}
}
File getOutputFile(final File file) {
return outputDir.file(file.name[0..file.name.lastIndexOf('.') - 1]).get().asFile
}
}
// Custom task to compile mesh files using filamesh
// This task handles incremental builds
abstract class MeshCompiler extends TaskWithBinary {
@Incremental
@InputFile
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@Inject abstract FileSystemOperations getFs()
@Inject abstract ExecOperations getExec()
MeshCompiler() {
super("filamesh")
}
@TaskAction
void execute(InputChanges inputs) {
if (!inputs.incremental) {
fs.delete({
delete(objects.fileTree().from(outputDir).matching { include '*.filamesh' })
})
}
inputs.getFileChanges(inputFile).each { InputFileDetails change ->
if (change.fileType == FileType.DIRECTORY) return
def file = change.file
if (change.changeType == ChangeType.REMOVED) {
getOutputFile(file).delete()
} else {
def out = new LogOutputStream(logger, LogLevel.LIFECYCLE)
def err = new LogOutputStream(logger, LogLevel.ERROR)
def header = ("Compiling mesh " + file + "\n").getBytes()
out.write(header)
out.flush()
if (!new File(binary.get()).exists()) {
throw new GradleException("Could not find ${binary.get()}." +
" Ensure Filament has been built/installed before building this app.")
}
exec.exec {
standardOutput out
errorOutput err
executable "${binary.get()}"
args(file, getOutputFile(file))
}
}
}
}
File getOutputFile(final File file) {
return outputDir.file(file.name[0..file.name.lastIndexOf('.')] + 'filamesh').get().asFile
}
}
class FilamentToolsPluginExtension {
DirectoryProperty materialInputDir
DirectoryProperty materialOutputDir
String cmgenArgs
RegularFileProperty iblInputFile
DirectoryProperty iblOutputDir
RegularFileProperty meshInputFile
DirectoryProperty meshOutputDir
}
class FilamentToolsPlugin implements Plugin<Project> {
void apply(Project project) {
def extension = project.extensions.create('filamentTools', FilamentToolsPluginExtension)
extension.materialInputDir = project.objects.directoryProperty()
extension.materialOutputDir = project.objects.directoryProperty()
extension.iblInputFile = project.objects.fileProperty()
extension.iblOutputDir = project.objects.directoryProperty()
extension.meshInputFile = project.objects.fileProperty()
extension.meshOutputDir = project.objects.directoryProperty()
project.tasks.register("filamentCompileMaterials", MaterialCompiler) {
enabled =
extension.materialInputDir.isPresent() &&
extension.materialOutputDir.isPresent()
inputDir.set(extension.materialInputDir.getOrNull())
outputDir.set(extension.materialOutputDir.getOrNull())
}
project.preBuild.dependsOn "filamentCompileMaterials"
project.tasks.register("filamentGenerateIbl", IblGenerator) {
enabled = extension.iblInputFile.isPresent() && extension.iblOutputDir.isPresent()
cmgenArgs = extension.cmgenArgs
inputFile = extension.iblInputFile.getOrNull()
outputDir = extension.iblOutputDir.getOrNull()
}
project.preBuild.dependsOn "filamentGenerateIbl"
project.tasks.register("filamentCompileMesh", MeshCompiler) {
enabled = extension.meshInputFile.isPresent() && extension.meshOutputDir.isPresent()
inputFile = extension.meshInputFile.getOrNull()
outputDir = extension.meshOutputDir.getOrNull()
}
project.preBuild.dependsOn "filamentCompileMesh"
}
}

View File

@@ -2121,7 +2121,7 @@ public class View {
*/
public boolean highPrecision = false;
/**
* VSM minimum variance scale, must be positive.
* @deprecated has no effect.
*/
public float minVarianceScale = 0.5f;
/**

View File

@@ -61,6 +61,7 @@ set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,max-page-size
add_library(filament-utils-jni SHARED
src/main/cpp/AutomationEngine.cpp
src/main/cpp/Bookmark.cpp
src/main/cpp/DeviceUtils.cpp
src/main/cpp/HDRLoader.cpp
src/main/cpp/IBLPrefilterContext.cpp
src/main/cpp/Utils.cpp

View File

@@ -71,8 +71,10 @@ Java_com_google_android_filament_utils_AutomationEngine_nStartBatchMode(JNIEnv*
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_utils_AutomationEngine_nTick(JNIEnv* env, jclass klass,
jlong nativeAutomation, jlong nativeEngine,
jlong view, jlongArray materials, jlong renderer, jfloat deltaTime) {
jlong view, jlongArray materials, jlong renderer, jlong nativeIbl, jint sunlightEntity,
jintArray assetLights, jlong nativeLm, jlong scene, jfloat deltaTime) {
using MaterialPointer = MaterialInstance*;
jsize materialCount = 0;
jlong* longMaterials = nullptr;
MaterialPointer* ptrMaterials = nullptr;
@@ -84,12 +86,28 @@ Java_com_google_android_filament_utils_AutomationEngine_nTick(JNIEnv* env, jclas
ptrMaterials[i] = (MaterialPointer) longMaterials[i];
}
}
jsize lightCount = 0;
jint* intLights = nullptr;
if (assetLights) {
lightCount = env->GetArrayLength(assetLights);
intLights = env->GetIntArrayElements(assetLights, nullptr);
}
static_assert(sizeof(jint) == sizeof(Entity));
AutomationEngine* automation = (AutomationEngine*) nativeAutomation;
AutomationEngine::ViewerContent content = {
.view = (View*) view,
.renderer = (Renderer*) renderer,
.materials = ptrMaterials,
.materialCount = (size_t) materialCount,
.lightManager = (LightManager*) nativeLm,
.scene = (Scene*) scene,
.indirectLight = (IndirectLight*) nativeIbl,
.sunlight = (Entity&) sunlightEntity,
.assetLights = (Entity*) intLights,
.assetLightCount = (size_t) lightCount,
};
Engine* engine = (Engine*)nativeEngine;
automation->tick(engine, content, deltaTime);
@@ -97,6 +115,9 @@ Java_com_google_android_filament_utils_AutomationEngine_nTick(JNIEnv* env, jclas
env->ReleaseLongArrayElements(materials, longMaterials, 0);
delete[] ptrMaterials;
}
if (intLights) {
env->ReleaseIntArrayElements(assetLights, intLights, 0);
}
}
extern "C" JNIEXPORT void JNICALL

View File

@@ -0,0 +1,85 @@
/*
* 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 <jni.h>
#include <filament/Engine.h>
#include <backend/Platform.h>
#include <utils/CString.h>
#include <algorithm>
#include <array>
using namespace filament;
namespace {
constexpr std::array<backend::Platform::DeviceInfoType, 3> VULKAN_INFO = {
backend::Platform::DeviceInfoType::VULKAN_DEVICE_NAME,
backend::Platform::DeviceInfoType::VULKAN_DRIVER_NAME,
backend::Platform::DeviceInfoType::VULKAN_DRIVER_INFO,
};
constexpr std::array<backend::Platform::DeviceInfoType, 3> GL_INFO = {
backend::Platform::DeviceInfoType::OPENGL_VENDOR,
backend::Platform::DeviceInfoType::OPENGL_RENDERER,
backend::Platform::DeviceInfoType::OPENGL_VERSION,
};
} // namespace
extern "C" JNIEXPORT jstring JNICALL
Java_com_google_android_filament_utils_DeviceUtils_nGetGpuDriverInfo(JNIEnv* env, jclass,
jlong nativeEngine) {
auto emptyStr = [env]() { return env->NewStringUTF(""); };
Engine* engine = (Engine*) nativeEngine;
if (!engine) {
return emptyStr();
}
backend::Platform* platform = engine->getPlatform();
if (!platform) {
return emptyStr();
}
std::array<backend::Platform::DeviceInfoType, 3> infoTypes;
switch (engine->getBackend()) {
case backend::Backend::VULKAN:
infoTypes = VULKAN_INFO;
break;
case backend::Backend::OPENGL:
infoTypes = GL_INFO;
break;
default:
return emptyStr();
}
backend::Driver* driver = const_cast<backend::Driver*>(engine->getDriver());
utils::CString fullInfo;
std::for_each(infoTypes.begin(), infoTypes.end(),
[&](backend::Platform::DeviceInfoType infoType) {
utils::CString const newInfo = platform->getDeviceInfo(infoType, driver);
if (!newInfo.empty()) {
if (!fullInfo.empty()) {
fullInfo += " | ";
}
fullInfo += newInfo.c_str();
}
});
return env->NewStringUTF(fullInfo.c_str());
}

View File

@@ -20,8 +20,6 @@
#include <imagediff/ImageDiff.h>
#include <utils/Log.h>
#include <vector>
using namespace imagediff;
using namespace utils;
@@ -102,30 +100,48 @@ jobject createResult(JNIEnv* env, ImageDiffResult const& result, bool generateDi
if (generateDiff && result.diffImage.getWidth() > 0) {
jclass bitmapClass = env->FindClass("android/graphics/Bitmap");
jmethodID createBitmap = env->GetStaticMethodID(bitmapClass, "createBitmap",
jmethodID createBitmap = env->GetStaticMethodID(bitmapClass, "createBitmap",
"(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
jclass configClass = env->FindClass("android/graphics/Bitmap$Config");
jfieldID argb8888 = env->GetStaticFieldID(configClass, "ARGB_8888", "Landroid/graphics/Bitmap$Config;");
jobject configObj = env->GetStaticObjectField(configClass, argb8888);
uint32_t width = result.diffImage.getWidth();
uint32_t height = result.diffImage.getHeight();
jobject diffBitmap = env->CallStaticObjectMethod(bitmapClass, createBitmap, (jint)width, (jint)height, configObj);
jobject diffBitmap = env->CallStaticObjectMethod(bitmapClass, createBitmap, (jint) width,
(jint) height, configObj);
if (diffBitmap) {
// We need to transport the bit differences accurately to the java side, so set
// premultiplied to false. From the java-side, if the bitmap is used to draw to a
// canvas, then client needs to set premultiplied to true again.
jmethodID setPremultiplied = env->GetMethodID(bitmapClass, "setPremultiplied", "(Z)V");
if (setPremultiplied) {
env->CallVoidMethod(diffBitmap, setPremultiplied, JNI_FALSE);
}
void* diffPixels;
if (AndroidBitmap_lockPixels(env, diffBitmap, &diffPixels) == 0) {
AndroidBitmapInfo info;
AndroidBitmap_getInfo(env, diffBitmap, &info);
float const* src = result.diffImage.getPixelRef();
uint8_t* dst = (uint8_t*) diffPixels;
uint32_t channels = result.diffImage.getChannels(); // usually 4
for (size_t i = 0; i < width * height; ++i) {
for (int c = 0; c < 4; ++c) {
float v = 0.0f;
if (c < channels) v = src[i * channels + c];
if (c == 3 && channels < 4) v = 1.0f; // Alpha 1.0 if missing
dst[i * 4 + c] = (uint8_t) std::min(255.0f, std::max(0.0f, v * 255.0f));
uint32_t const channels = result.diffImage.getChannels(); // usually 4
for (size_t y = 0; y < height; ++y) {
uint8_t* row = dst + y * info.stride;
for (size_t x = 0; x < width; ++x) {
size_t srcIdx = (y * width + x) * channels;
for (int c = 0; c < 4; ++c) {
float v = 0.0f;
if (c < channels) v = src[srcIdx + c];
if (c == 3 && channels < 4) v = 1.0f; // Alpha 1.0 if missing
row[x * 4 + c] = uint8_t(
std::min(255.0f, std::max(0.0f, std::round(v * 255.0f))));
}
}
}
AndroidBitmap_unlockPixels(env, diffBitmap);
@@ -133,7 +149,7 @@ jobject createResult(JNIEnv* env, ImageDiffResult const& result, bool generateDi
}
}
}
return resultObj;
}
@@ -147,7 +163,7 @@ Java_com_google_android_filament_utils_ImageDiff_nCompareBasic(JNIEnv* env, jcla
BitmapLock maskArg(env, maskBitmap);
if (!refArg.isValid() || !candArg.isValid()) {
ImageDiffResult emptyResult;
ImageDiffResult emptyResult;
emptyResult.status = ImageDiffResult::Status::SIZE_MISMATCH; // or ERROR
return createResult(env, emptyResult, false);
}
@@ -175,13 +191,13 @@ Java_com_google_android_filament_utils_ImageDiff_nCompareBasic(JNIEnv* env, jcla
extern "C" JNIEXPORT jobject JNICALL
Java_com_google_android_filament_utils_ImageDiff_nCompareJson(JNIEnv* env, jclass,
jobject refBitmap, jobject candBitmap, jstring jsonConfig, jobject maskBitmap) {
BitmapLock refArg(env, refBitmap);
BitmapLock candArg(env, candBitmap);
BitmapLock maskArg(env, maskBitmap);
if (!refArg.isValid() || !candArg.isValid()) {
ImageDiffResult emptyResult;
ImageDiffResult emptyResult;
emptyResult.status = ImageDiffResult::Status::SIZE_MISMATCH; // or ERROR
return createResult(env, emptyResult, false);
}
@@ -189,7 +205,7 @@ Java_com_google_android_filament_utils_ImageDiff_nCompareJson(JNIEnv* env, jclas
ImageDiffConfig config;
const char* nativeJson = env->GetStringUTFChars(jsonConfig, 0);
size_t length = env->GetStringUTFLength(jsonConfig);
bool parsed = parseConfig(nativeJson, length, &config);
env->ReleaseStringUTFChars(jsonConfig, nativeJson);
@@ -214,4 +230,3 @@ Java_com_google_android_filament_utils_ImageDiff_nCompareJson(JNIEnv* env, jclas
return createResult(env, result, generateDiff);
}

View File

@@ -178,7 +178,12 @@ public class AutomationEngine {
}
long nativeView = content.view.getNativeObject();
long nativeRenderer = content.renderer.getNativeObject();
nTick(mNativeObject, engine.getNativeObject(), nativeView, nativeMaterialInstances, nativeRenderer, deltaTime);
long nativeIbl = content.indirectLight == null ? 0 : content.indirectLight.getNativeObject();
long nativeLm = content.lightManager == null ? 0 : content.lightManager.getNativeObject();
long nativeScene = content.scene == null ? 0 : content.scene.getNativeObject();
nTick(mNativeObject, engine.getNativeObject(), nativeView, nativeMaterialInstances,
nativeRenderer, nativeIbl, content.sunlight, content.assetLights, nativeLm,
nativeScene, deltaTime);
}
/**
@@ -287,7 +292,8 @@ public class AutomationEngine {
private static native void nStartRunning(long nativeObject);
private static native void nStartBatchMode(long nativeObject);
private static native void nTick(long nativeObject, long nativeEngine,
long view, long[] materials, long renderer, float deltaTime);
long view, long[] materials, long renderer, long ibl, int sunlight, int[] assetLights,
long lightManager, long scene, float deltaTime);
private static native void nApplySettings(long nativeObject, long nativeEngine,
String jsonSettings, long view,
long[] materials, long ibl, int sunlight, int[] assetLights, long lightManager,

View File

@@ -0,0 +1,26 @@
/*
* 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.
*/
package com.google.android.filament.utils;
import com.google.android.filament.Engine;
public class DeviceUtils {
public static String getGpuDriverInfo(Engine engine) {
return nGetGpuDriverInfo(engine.getNativeObject());
}
private static native String nGetGpuDriverInfo(long nativeEngine);
}

View File

@@ -106,8 +106,8 @@ class ModelViewer(
var skyboxCubemap: Texture? = null
private lateinit var displayHelper: DisplayHelper
private lateinit var cameraManipulator: Manipulator
private lateinit var gestureDetector: GestureDetector
private var cameraManipulator: Manipulator? = null
private var gestureDetector: GestureDetector? = null
private var surfaceView: SurfaceView? = null
private var textureView: TextureView? = null
@@ -157,15 +157,13 @@ class ModelViewer(
surfaceView: SurfaceView,
engine: Engine = Engine.create(),
uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK),
manipulator: Manipulator? = null
manipulator: Manipulator? = defaultCameraManipulator(surfaceView.width, surfaceView.height)
) : this(engine, uiHelper) {
cameraManipulator = manipulator ?: Manipulator.Builder()
.targetPosition(kDefaultObjectPosition.x, kDefaultObjectPosition.y, kDefaultObjectPosition.z)
.viewport(surfaceView.width, surfaceView.height)
.build(Manipulator.Mode.ORBIT)
this.surfaceView = surfaceView
gestureDetector = GestureDetector(surfaceView, cameraManipulator)
cameraManipulator = manipulator
cameraManipulator?.let { c ->
gestureDetector = GestureDetector(surfaceView, c)
}
displayHelper = DisplayHelper(surfaceView.context)
uiHelper.renderCallback = SurfaceCallback()
uiHelper.attachTo(surfaceView)
@@ -177,15 +175,14 @@ class ModelViewer(
textureView: TextureView,
engine: Engine = Engine.create(),
uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK),
manipulator: Manipulator? = null
manipulator: Manipulator? = defaultCameraManipulator(textureView.width, textureView.height)
) : this(engine, uiHelper) {
cameraManipulator = manipulator ?: Manipulator.Builder()
.targetPosition(kDefaultObjectPosition.x, kDefaultObjectPosition.y, kDefaultObjectPosition.z)
.viewport(textureView.width, textureView.height)
.build(Manipulator.Mode.ORBIT)
cameraManipulator = manipulator
this.textureView = textureView
gestureDetector = GestureDetector(textureView, cameraManipulator)
cameraManipulator = manipulator
cameraManipulator?.let { c ->
gestureDetector = GestureDetector(textureView, c)
}
displayHelper = DisplayHelper(textureView.context)
uiHelper.renderCallback = SurfaceCallback()
uiHelper.attachTo(textureView)
@@ -269,6 +266,35 @@ class ModelViewer(
}
}
/**
* Resets the model's transform, animation, and camera state to defaults.
* Call this when reusing the same model across multiple tests.
*/
fun resetToDefaultState() {
// 1. Reset Camera parameters
cameraFocalLength = 28f
cameraNear = kNearPlane
cameraFar = kFarPlane
updateCameraProjection()
// 2. Reset the manipulator's look-at vectors to initial state
cameraManipulator?.let { cm ->
cm.jumpToBookmark(cm.homeBookmark)
}
// 3. Reset Animations
animator?.let {
if (it.animationCount > 0) {
it.applyAnimation(0, 0.0f)
}
it.updateBoneMatrices()
}
// 4. Re-apply the unit cube transform to clear custom scaling/translation
clearRootTransform()
transformToUnitCube()
}
/**
* Frees all entities associated with the most recently-loaded model.
*/
@@ -302,11 +328,13 @@ class ModelViewer(
asset?.let { populateScene(it) }
// Extract the camera basis from the helper and push it to the Filament camera.
cameraManipulator.getLookAt(eyePos, target, upward)
camera.lookAt(
cameraManipulator?.let { cm ->
cm.getLookAt(eyePos, target, upward)
camera.lookAt(
eyePos[0], eyePos[1], eyePos[2],
target[0], target[1], target[2],
upward[0], upward[1], upward[2])
}
// Render the scene, unless the renderer wants to skip the frame.
if (renderer.beginFrame(swapChain!!, frameTimeNanos)) {
@@ -398,7 +426,7 @@ class ModelViewer(
* Handles a [MotionEvent] to enable one-finger orbit, two-finger pan, and pinch-to-zoom.
*/
fun onTouchEvent(event: MotionEvent) {
gestureDetector.onTouchEvent(event)
gestureDetector?.onTouchEvent(event)
}
@SuppressWarnings("ClickableViewAccessibility")
@@ -451,7 +479,7 @@ class ModelViewer(
override fun onResized(width: Int, height: Int) {
view.viewport = Viewport(0, 0, width, height)
cameraManipulator.setViewport(width, height)
cameraManipulator?.setViewport(width, height)
updateCameraProjection()
synchronizePendingFrames(engine)
}
@@ -468,5 +496,11 @@ class ModelViewer(
companion object {
private val kDefaultObjectPosition = Float3(0.0f, 0.0f, -4.0f)
private fun defaultCameraManipulator(width: Int, height: Int) : Manipulator {
return Manipulator.Builder()
.targetPosition(kDefaultObjectPosition.x, kDefaultObjectPosition.y, kDefaultObjectPosition.z)
.viewport(width, height)
.build(Manipulator.Mode.ORBIT)
}
}
}

View File

@@ -0,0 +1,120 @@
# Filament Tools Gradle Plugin
## About
The **Filament Tools Gradle Plugin** helps integrate Filament into your Android project. It
automates the use of Filament's command-line tools (`matc`, `cmgen`, and `filamesh`).
This plugin handles:
- **Material Compilation**: Compiles `.mat` material definitions into `.filamat` binaries.
- **IBL Generation**: Generates Image-Based Lighting assets from HDR environment maps.
- **Mesh Compilation**: Converts models into Filament's efficient `.filamesh` binary format. *Note:
This tool is no longer recommended; instead, use glTF and Filament's `gltfio` library for model
loading.*
The plugin hooks directly into the Android build lifecycle (via `preBuild`) and supports incremental
builds, so assets are only recompiled when source files change.
## Usage
Apply the plugin in your module's `build.gradle` file and configure the `filament` block. You can
specify inputs and outputs for any combination of materials, IBLs, or meshes. If a path is not
configured, the corresponding task will be disabled.
### Example Configuration
```groovy
plugins {
id 'com.google.android.filament-tools'
}
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
iblInputFile = project.layout.projectDirectory.file("path/to/environment.hdr")
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")
meshInputFile = project.layout.projectDirectory.file("path/to/model.obj")
meshOutputDir = project.layout.projectDirectory.dir("src/main/assets/models")
}
```
### Configuration Details
- **materialInputDir**: The directory containing your source material definitions (`.mat` files).
- **materialOutputDir**: The directory where the compiled material files (`.filamat`) will be
generated.
- **iblInputFile**: The source high-dynamic-range image file (e.g., `.hdr` or `.exr`) used to
generate Image Based Lighting assets.
- **iblOutputDir**: The directory where the generated IBL assets (typically `.ktx` files) will be
placed.
- **meshInputFile**: The source mesh file (e.g., `.obj`) to be compiled.
- **meshOutputDir**: The directory where the compiled mesh file (`.filamesh`) will be generated.
Automatically adds tasks to your Android build to compile materials, generate an IBL, and compile a
mesh. The plugin hooks into `preBuild` to ensure assets are generated before the application is
built.
### Configuration Flags
You can control specific compilation options using Gradle properties (e.g., in `gradle.properties`
or via command line `-P`).
- **`com.google.android.filament.exclude-vulkan`** When set to `true`, the Vulkan backend is
excluded from the compiled materials. This can be useful to reduce the size of the generated
assets if your application does not target Vulkan. *Default: `false` (Vulkan is included)*
- **`com.google.android.filament.include-webgpu`** When set to `true`, the WebGPU backend is
included in the compiled materials. Use this if you intend to use the materials in a context
supporting WebGPU. *Default: `false`*
## Tools Configuration
The Filament Tools plugin requires some binary tools to be available: `matc`, `cmgen`, and
`filamesh`.
There are three ways to configure Filament tools:
1. **Point to a local path directly**
```groovy
filament {
matc {
path = "/path/to/matc"
}
cmgen {
path = "/path/to/cmgen"
}
filamesh {
path = "/path/to/filamesh"
}
}
```
2. **Point to a Maven artifact**
```groovy
filament {
matc {
// The minor version (the middle number) must match the Filament dependency's.
artifact = 'com.google.android.filament:matc:1.68.5'
}
cmgen {
artifact = 'com.google.android.filament:cmgen:1.68.5'
}
}
```
*Note that the `filamesh` artifact is not hosted on Maven Central, so it must be provided locally or
via another mechanism if needed.*
Gradle will automatically handle downloading the tool appropriate for your machine (MacOS/Linux/Windows) from Maven.
3. **Set the `com.google.android.filament.tools-dir` Gradle property**
This will override any other configuration. Gradle will attempt to locate the tools under
`<tools-dir>/bin` (e.g., `.../bin/matc`, `.../bin/cmgen`, `.../bin/filamesh`).

View File

@@ -0,0 +1,31 @@
plugins {
id 'groovy-gradle-plugin'
id 'com.gradle.plugin-publish' version '2.1.0'
}
group = "com.google.android.filament"
version = "0.1.0"
gradlePlugin {
website = "https://github.com/google/filament/tree/main/android/gradle-plugin"
vcsUrl = "https://github.com/google/filament/tree/main/android/gradle-plugin"
plugins {
create("filament-tools") {
id = "com.google.android.filament-tools"
displayName = "Filament Tools Gradle Plugin"
description = "A plugin that helps integrate Filament into Android projects"
tags.addAll('android', 'graphics', 'rendering', 'filament', '3d', 'gltf', 'native')
implementationClass = "com.google.android.filament.gradle.FilamentPlugin"
}
}
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation "com.google.gradle:osdetector-gradle-plugin:1.7.3"
}

View File

@@ -0,0 +1,57 @@
/*
* 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.
*/
package com.google.android.filament.gradle
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
class FilamentExtension {
final ToolsLocator tools
final DirectoryProperty materialInputDir
final DirectoryProperty materialOutputDir
final Property<String> cmgenArgs
final RegularFileProperty iblInputFile
final DirectoryProperty iblOutputDir
final RegularFileProperty meshInputFile
final DirectoryProperty meshOutputDir
FilamentExtension(Project project) {
this.tools = new ToolsLocator(project)
this.materialInputDir = project.objects.directoryProperty()
this.materialOutputDir = project.objects.directoryProperty()
this.cmgenArgs = project.objects.property(String)
this.iblInputFile = project.objects.fileProperty()
this.iblOutputDir = project.objects.directoryProperty()
this.meshInputFile = project.objects.fileProperty()
this.meshOutputDir = project.objects.directoryProperty()
}
void matc(Action<ToolsLocator.ToolConfig> action) {
action.execute(tools.matc)
}
void cmgen(Action<ToolsLocator.ToolConfig> action) {
action.execute(tools.cmgen)
}
void filamesh(Action<ToolsLocator.ToolConfig> action) {
action.execute(tools.filamesh)
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.
*/
package com.google.android.filament.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
class FilamentPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.pluginManager.apply("com.google.osdetector")
FilamentExtension extension = project.extensions.create("filament", FilamentExtension, project)
project.afterEvaluate {
extension.tools.resolve(project)
project.tasks.register("filamentCompileMaterials", MaterialCompileTask) {
enabled = extension.materialInputDir.isPresent() && extension.materialOutputDir.isPresent()
inputDir.set(extension.materialInputDir.getOrNull())
outputDir.set(extension.materialOutputDir.getOrNull())
matcTool.from(extension.tools.matcToolFiles)
}
project.tasks.register("filamentGenerateIbl", IblGenerateTask) {
enabled = extension.iblInputFile.isPresent() && extension.iblOutputDir.isPresent()
cmgenArgs = extension.cmgenArgs
inputFile.set(extension.iblInputFile.getOrNull())
outputDir.set(extension.iblOutputDir.getOrNull())
cmgenTool.from(extension.tools.cmgenToolFiles)
}
project.tasks.register("filamentCompileMesh", MeshCompileTask) {
enabled = extension.meshInputFile.isPresent() && extension.meshOutputDir.isPresent()
inputFile = extension.meshInputFile.getOrNull()
outputDir = extension.meshOutputDir.getOrNull()
filameshTool.from(extension.tools.filameshToolFiles)
}
project.preBuild.dependsOn "filamentCompileMaterials"
project.preBuild.dependsOn "filamentGenerateIbl"
project.preBuild.dependsOn "filamentCompileMesh"
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* 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.
*/
package com.google.android.filament.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileType
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import org.gradle.api.tasks.incremental.InputFileDetails
import javax.inject.Inject
abstract class IblGenerateTask extends DefaultTask {
@Input
@Optional
abstract Property<String> getCmgenArgs()
@Incremental
@InputFile
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@InputFiles
abstract ConfigurableFileCollection getCmgenTool()
@Inject
abstract FileSystemOperations getFileSystemOperations()
@Inject
abstract ExecOperations getExecOperations()
@Inject
abstract ObjectFactory getObjectFactory()
@TaskAction
void execute(InputChanges inputs) {
if (cmgenTool.empty) {
throw new IllegalStateException(
"cmgen executable not configured. Please configure the 'cmgen' block in the " +
"'filament' extension or set the 'com.google.android.filament.tools-dir' " +
"property."
)
}
File cmgen = getCmgenTool().singleFile
if (!cmgen.exists()) {
throw new IllegalStateException("cmgen executable does not exist: ${cmgen.absolutePath}")
}
if (!cmgen.canExecute()) {
cmgen.setExecutable(true)
}
if (!inputs.incremental) {
getFileSystemOperations().delete {
delete(getObjectFactory().fileTree().from(getOutputDir()).matching { include '*' })
}
}
inputs.getFileChanges(getInputFile()).each { InputFileDetails change ->
if (change.fileType == FileType.DIRECTORY) return
def file = change.file
if (change.changeType == ChangeType.REMOVED) {
computeOutputFile(file).delete()
} else {
println "Generating IBL: ${file.name}"
def outputPath = getOutputDir().get().asFile
def commandArgs = getCmgenArgs().getOrNull()
if (commandArgs == null) {
// Default args if not provided
commandArgs = '-q -x ' + outputPath + ' --format=rgb32f ' +
'--extract-blur=0.08 --extract=' + outputPath.absolutePath
}
def argsList = commandArgs.split(' ').toList()
argsList.add(file.absolutePath)
getExecOperations().exec { spec ->
spec.executable(cmgen)
spec.args(argsList)
}
}
}
}
File computeOutputFile(final File file) {
String name = file.name
int dotIndex = name.lastIndexOf('.')
String baseName = dotIndex > 0 ? name.substring(0, dotIndex) : name
return getOutputDir().file(baseName).get().asFile
}
}

View File

@@ -0,0 +1,147 @@
/*
* 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.
*/
package com.google.android.filament.gradle
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileType
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.SkipWhenEmpty
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import javax.inject.Inject
abstract class MaterialCompileTask extends DefaultTask {
@Incremental
@InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
abstract DirectoryProperty getInputDir()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@InputFiles
@PathSensitive(PathSensitivity.NONE)
abstract ConfigurableFileCollection getMatcTool()
@Inject
abstract ExecOperations getExecOperations()
@Inject
abstract FileSystemOperations getFileSystemOperations()
@Inject
abstract ObjectFactory getObjectFactory()
@Inject
abstract ProviderFactory getProviderFactory()
@TaskAction
void compile(InputChanges inputs) {
if (matcTool.empty) {
throw new IllegalStateException(
"matc executable not configured. Please configure the 'matc' block in the " +
"'filament' extension or set the 'com.google.android.filament.tools-dir' " +
"property."
)
}
File matc = matcTool.singleFile
if (!matc.exists()) {
throw new IllegalStateException("matc executable does not exist: ${matc.absolutePath}")
}
if (!matc.canExecute()) {
matc.setExecutable(true)
}
if (!inputs.incremental) {
getFileSystemOperations().delete {
delete(getObjectFactory().fileTree().from(getOutputDir()).matching {
include '*.filamat'
})
}
}
def pf = getProviderFactory()
def excludeVulkanProperty = pf.gradleProperty("com.google.android.filament.exclude-vulkan")
def includeWebGpuProperty = pf.gradleProperty("com.google.android.filament.include-webgpu")
def matNoOptProperty = pf.gradleProperty("com.google.android.filament.matnopt")
def excludeVulkan = excludeVulkanProperty.orNull == "true"
def includeWebGpu = includeWebGpuProperty.orNull == "true"
def matNoOpt = matNoOptProperty.orNull == "true"
inputs.getFileChanges(getInputDir()).each { change ->
if (change.fileType == FileType.DIRECTORY) return
File file = change.file
File outputFile = computeOutputFile(file)
if (change.changeType == ChangeType.REMOVED) {
outputFile.delete()
} else {
println "Compiling material: ${file.name}"
def args = []
if (!excludeVulkan) {
args += ['-a', 'vulkan']
}
if (includeWebGpu) {
args += ['-a', 'webgpu', '--variant-filter=stereo']
}
if (matNoOpt) {
args += ['-g']
}
args += [
'-a', 'opengl', '-p', 'mobile',
'-o', outputFile.absolutePath,
file.absolutePath
]
getExecOperations().exec { spec ->
spec.executable(matc)
spec.args(args)
}
}
}
}
File computeOutputFile(File inputFile) {
String baseName = inputFile.name
int dotIndex = baseName.lastIndexOf('.')
if (dotIndex > 0) {
baseName = baseName.substring(0, dotIndex)
}
return getOutputDir().file("${baseName}.filamat").get().asFile
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.
*/
package com.google.android.filament.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.FileType
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import javax.inject.Inject
abstract class MeshCompileTask extends DefaultTask {
@Incremental
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@InputFiles
@PathSensitive(PathSensitivity.NONE)
abstract ConfigurableFileCollection getFilameshTool()
@Inject
abstract ExecOperations getExecOperations()
@Inject
abstract FileSystemOperations getFileSystemOperations()
@Inject
abstract ObjectFactory getObjectFactory()
@TaskAction
void compile(InputChanges inputs) {
if (filameshTool.empty) {
throw new IllegalStateException(
"filamesh executable not configured. Please configure the 'filamesh' block in the " +
"'filament' extension or set the 'com.google.android.filament.tools-dir' " +
"property."
)
}
File filamesh = filameshTool.singleFile
if (!filamesh.exists()) {
throw new IllegalStateException("filamesh executable does not exist: ${filamesh.absolutePath}")
}
if (!filamesh.canExecute()) {
filamesh.setExecutable(true)
}
if (!inputs.incremental) {
getFileSystemOperations().delete {
delete(getObjectFactory().fileTree().from(getOutputDir()).matching {
include '*.filamesh'
})
}
}
inputs.getFileChanges(inputFile).each { change ->
if (change.fileType == FileType.DIRECTORY) return
File file = change.file
File outputFile = computeOutputFile(file)
if (change.changeType == ChangeType.REMOVED) {
outputFile.delete()
} else {
println "Compiling mesh: ${file.name}"
getExecOperations().exec { spec ->
spec.executable(filamesh)
spec.args(file.absolutePath, outputFile.absolutePath)
}
}
}
}
File computeOutputFile(File inputFile) {
String baseName = inputFile.name
int dotIndex = baseName.lastIndexOf('.')
if (dotIndex > 0) {
baseName = baseName.substring(0, dotIndex)
}
return getOutputDir().file("${baseName}.filamesh").get().asFile
}
}

View File

@@ -0,0 +1,114 @@
/*
* 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.
*/
package com.google.android.filament.gradle
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.file.FileCollection
import org.gradle.internal.os.OperatingSystem
import java.nio.file.Paths
class ToolsLocator {
static class ToolConfig {
String artifact
String path
FileCollection files
}
final ToolConfig matc = new ToolConfig()
final ToolConfig cmgen = new ToolConfig()
final ToolConfig filamesh = new ToolConfig()
private final Project project
ToolsLocator(Project project) {
this.project = project
}
void resolve(Project project) {
resolveTool(matc, "matc")
resolveTool(cmgen, "cmgen")
resolveTool(filamesh, "filamesh")
}
/**
* Resolves a specific tool by its name and sets the {@link ToolConfig#files} property of the
* provided {@link ToolConfig} object. It first attempts to locate the tool based on a Gradle
* property `com.google.android.filament.tools-dir` if present, otherwise it resolves the tool
* through a Gradle configuration.
*
* @param tool The {@link ToolConfig} object whose {@code files} property will be set.
* @param name The name of the tool (e.g., "matc", "cmgen").
*/
private void resolveTool(ToolConfig tool, String name) {
// Find the OS classifier, e.g. 'osx-aarch_64'.
def classifier =
project.extensions.getByType(com.google.gradle.osdetector.OsDetector).classifier
// If com.google.android.filament.tools-dir is set to a non-empty string, we'll use it as
// the tool's base path.
def toolsDirProp = project.providers.gradleProperty("com.google.android.filament.tools-dir")
if (toolsDirProp.isPresent() && !toolsDirProp.get().trim().isEmpty()) {
def toolsDir = toolsDirProp.get()
def path = OperatingSystem.current().isWindows() ?
"${toolsDir}/bin/${name}.exe" :
"${toolsDir}/bin/${name}"
tool.files = project.files(path)
return
}
// If an explicit path for the tool is provided in ToolConfig
// (e.g. matc { path = 'path/to/tool' }), use it directly.
if (tool.path) {
tool.files = project.files(tool.path)
return
}
// Otherwise, if an artifact is provided
// (e.g. matc { artifact = 'com.google.android.filament:matc:1.68.5' }), resolve it.
if (tool.artifact) {
String depString = tool.artifact
// In Gradle, a configuration is a named, manageable group of dependencies.
// Resolve the tool artifact using a detached configuration. A detached configuration
// is a temporary, isolated configuration that is not part of the project's regular
// configuration hierarchy.
Configuration config = project.configurations.detachedConfiguration()
config.setTransitive(false) // We only want the tool itself, not its dependencies
def dep = project.dependencies.create("${depString}:${classifier}@exe")
config.dependencies.add(dep)
// A Gradle Configuration implements FileCollection. When treated as a FileCollection,
// it represents the resolved files of its dependencies.
tool.files = config
}
}
FileCollection getMatcToolFiles() {
return matc.files ?: project.files()
}
FileCollection getCmgenToolFiles() {
return cmgen.files ?: project.files()
}
FileCollection getFilameshToolFiles() {
return filamesh.files ?: project.files()
}
}

View File

@@ -1,5 +1,5 @@
GROUP=com.google.android.filament
VERSION_NAME=1.69.2
VERSION_NAME=1.70.2
POM_DESCRIPTION=Real-time physically based rendering engine for Android.

View File

@@ -94,6 +94,13 @@ afterEvaluate { project ->
}
signing {
def signingKey = findProperty("signingKey")
def signingPassword = findProperty("signingPassword")
if (signingKey && signingPassword) {
println("Signing with in-memory keys")
useInMemoryPgpKeys(signingKey, signingPassword)
}
publishing.publications.all { publication ->
sign publication
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
cmgenArgs = "-q --format=ktx --size=256 --extract-blur=0.1 --deploy=src/main/assets/envs/default_env"
iblInputFile = project.layout.projectDirectory.file("../../../third_party/environments/lightroom_14b.hdr")
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
meshInputFile = project.layout.projectDirectory.file("../../../third_party/models/shader_ball/shader_ball.obj")
meshOutputDir = project.layout.projectDirectory.dir("src/main/assets/models")

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
iblInputFile = project.layout.projectDirectory.file("../../../third_party/environments/studio_small_02_2k.hdr")
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")

View File

@@ -1,6 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -9,6 +10,26 @@ kotlin {
jvmToolchain(versions.jdk)
}
filament {
cmgenArgs = "-q --format=ktx --size=256 --extract-blur=0.1 --deploy=src/main/assets/envs/default_env"
iblInputFile = project.layout.projectDirectory.file("../../../third_party/environments/lightroom_14b.hdr")
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")
}
// don't forget to update MainACtivity.kt when/if changing this.
tasks.register('copyDamagedHelmetGltf', Copy) {
from file("../../../third_party/models/DamagedHelmet/DamagedHelmet.glb")
into file("src/main/assets/models")
rename {String fileName -> "helmet.glb"}
}
preBuild.dependsOn copyDamagedHelmetGltf
clean.doFirst {
delete "src/main/assets/envs"
delete "src/main/assets/models"
}
android {
namespace 'com.google.android.filament.validation'
@@ -27,9 +48,10 @@ android {
dependencies {
implementation deps.kotlin
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.10.0'
implementation deps.coroutines.core
implementation project(':filament-android')
implementation project(':gltfio-android')
implementation project(':filament-utils-android')
implementation project(':filament-utils-android')
}

View File

@@ -12,10 +12,11 @@
android:supportsRtl="true"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:theme="@android:style/Theme.NoTitleBar">
android:theme="@style/Theme.Material3.DayNight.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:screenOrientation="fullSensor">
<intent-filter>

View File

@@ -0,0 +1,899 @@
{
"name": "Default Test",
"backends": [
"opengl"
],
"models": {
"DamagedHelmet": "helmet.glb"
},
"presets": [
{
"name": "base",
"models": [
"DamagedHelmet"
],
"rendering": {
"camera": {
"enabled": true,
"horizontalFov": 45.0,
"center": [
0,
0,
0
],
"lookAt": [
0,
0,
-1
]
},
"view": {
"postProcessingEnabled": true,
"dithering": "NONE"
}
},
"tolerance": {
"maxAbsDiff": 0.1,
"maxFailingPixelsFraction": 0.0
}
},
{
"name": "tilted",
"rendering": {
"camera": {
"enabled": true,
"horizontalFov": 45.0,
"center": [
-4,
-2,
-3
],
"lookAt": [
0,
0,
-4
]
}
}
}
],
"tests": [
{
"name": "basic",
"apply_presets": [
"base"
]
},
{
"name": "rotated",
"apply_presets": [
"base",
"tilted"
]
},
{
"name": "ssao",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true
}
},
{
"name": "msaa",
"apply_presets": [
"base"
],
"rendering": {
"view.msaa.enabled": true
}
},
{
"name": "bloom",
"apply_presets": [
"base",
"tilted"
],
"rendering": {
"view.bloom.enabled": true
}
},
{
"name": "aa_none",
"apply_presets": [
"base"
],
"rendering": {
"view.antiAliasing": "NONE"
}
},
{
"name": "aa_fxaa",
"apply_presets": [
"base"
],
"rendering": {
"view.antiAliasing": "FXAA"
}
},
{
"name": "dithering_none",
"apply_presets": [
"base"
],
"rendering": {
"view.dithering": "NONE"
}
},
{
"name": "msaa_8",
"apply_presets": [
"base"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.sampleCount": 8
}
},
{
"name": "taa_custom",
"apply_presets": [
"base"
],
"rendering": {
"view.taa.enabled": true,
"view.taa.feedback": 0.2,
"view.taa.jitterPattern": "HALTON_23_X16"
}
},
{
"name": "ssao_gtao",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.aoType": "GTAO"
}
},
{
"name": "ssao_sao",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.aoType": "SAO"
}
},
{
"name": "ssao_radius_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.radius": 1.0
}
},
{
"name": "ssao_radius_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.radius": 0.1
}
},
{
"name": "ssao_power_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.power": 2.0
}
},
{
"name": "ssao_power_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.power": 0.5
}
},
{
"name": "ssao_bias_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.bias": 0.05
}
},
{
"name": "ssao_resolution_half",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"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"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.bentNormals": true
}
},
{
"name": "ssao_quality_ultra",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.quality": "ULTRA"
}
},
{
"name": "ssao_quality_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.quality": "LOW"
}
},
{
"name": "ssao_lowPassFilter_ultra",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.lowPassFilter": "ULTRA"
}
},
{
"name": "ssao_upsampling_ultra",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.upsampling": "ULTRA"
}
},
{
"name": "ssr_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true
}
},
{
"name": "ssr_thickness_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.5
}
},
{
"name": "ssr_bias_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.bias": 0.1
}
},
{
"name": "ssr_maxDistance_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.maxDistance": 5.0
}
},
{
"name": "ssr_stride_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.stride": 4.0
}
},
{
"name": "ssr_stride_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.stride": 1.0
}
},
{
"name": "ssr_thickness_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.01
}
},
{
"name": "ssr_maxDistance_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.maxDistance": 1.0
}
},
{
"name": "ssr_bias_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.bias": 0.001
}
},
{
"name": "ssr_quality_combo",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.2,
"view.screenSpaceReflections.stride": 2.0
}
},
{
"name": "bloom_levels_high",
"apply_presets": [
"base"
],
"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"
}
},
{
"name": "bloom_lensflare",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true
}
},
{
"name": "bloom_lensflare_no_starburst",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.starburst": false
}
},
{
"name": "bloom_lensflare_chromatic",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.chromaticAberration": 0.05
}
},
{
"name": "bloom_lensflare_ghosts",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.ghostCount": 8
}
},
{
"name": "bloom_lensflare_ghostSpacing",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.ghostSpacing": 0.8
}
},
{
"name": "bloom_halo_thick",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.haloThickness": 0.2
}
},
{
"name": "bloom_halo_radius",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.haloRadius": 0.5
}
},
{
"name": "dof_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true
}
},
{
"name": "dof_cocScale_high",
"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
}
},
{
"name": "dof_filter_none",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.filter": "NONE"
}
},
{
"name": "dof_nativeResolution",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.nativeResolution": true
}
},
{
"name": "dof_rings_high",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.foregroundRingCount": 5,
"view.dof.backgroundRingCount": 5
}
},
{
"name": "dof_rings_low",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.foregroundRingCount": 3,
"view.dof.backgroundRingCount": 3
}
},
{
"name": "dof_max_coc",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.maxForegroundCOC": 16,
"view.dof.maxBackgroundCOC": 16
}
},
{
"name": "fog_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true
}
},
{
"name": "fog_distance",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.distance": 10.0
}
},
{
"name": "fog_cutOffDistance",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.cutOffDistance": 100.0
}
},
{
"name": "fog_maximumOpacity",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.maximumOpacity": 0.5
}
},
{
"name": "fog_height",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.height": 5.0
}
},
{
"name": "fog_heightFalloff",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.heightFalloff": 0.5
}
},
{
"name": "fog_density_high",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.density": 0.5
}
},
{
"name": "fog_inScatteringStart",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.inScatteringStart": 5.0
}
},
{
"name": "fog_inScatteringSize",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.inScatteringSize": 10.0
}
},
{
"name": "fog_fogColorFromIbl",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.fogColorFromIbl": true
}
},
{
"name": "vignette_basic",
"apply_presets": [
"base"
],
"rendering": {
"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": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "FILMIC"
}
},
{
"name": "cg_aces",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "ACES"
}
},
{
"name": "cg_agx",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "AGX"
}
},
{
"name": "cg_pbr_neutral",
"apply_presets": [
"base"
],
"rendering": {
"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": [
"base"
],
"rendering": {
"view.shadowType": "VSM"
}
},
{
"name": "shadow_vsm_anisotropy",
"apply_presets": [
"base"
],
"rendering": {
"view.shadowType": "VSM",
"view.vsmShadowOptions.anisotropy": 2
}
},
{
"name": "shadow_vsm_highPrecision",
"apply_presets": [
"base"
],
"rendering": {
"view.shadowType": "VSM",
"view.vsmShadowOptions.highPrecision": true
}
}
]
}

View File

@@ -17,16 +17,35 @@
package com.google.android.filament.validation
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.text.Html
import android.util.Log
import android.view.Choreographer
import android.view.SurfaceView
import android.view.View
import android.view.WindowManager
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import com.google.android.filament.utils.KTX1Loader
import com.google.android.filament.utils.ModelViewer
import com.google.android.filament.utils.Utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class MainActivity : Activity(), ValidationRunner.Callback {
@@ -42,8 +61,35 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private lateinit var choreographer: Choreographer
private lateinit var modelViewer: ModelViewer
private lateinit var statusTextView: TextView
private lateinit var testResultsHeader: TextView
private lateinit var resultsContainer: LinearLayout
private lateinit var inputManager: ValidationInputManager
private var currentInput: ValidationInputManager.ValidationInput? = null
private var currentAlphaDiffBitmap: Bitmap? = null
private var globalEnhancementFactor: Float = 1.0f
private data class TestImages(
val testName: String,
val golden: Bitmap?,
val rendered: Bitmap?,
val diff: Bitmap?,
val alphaDiff: Bitmap?
)
private val diffImageViews = mutableListOf<ImageView>()
// UI Elements
private lateinit var runButton: Button
private lateinit var loadButton: Button
private lateinit var optionsButton: Button
private lateinit var enhancementContainer: LinearLayout
private lateinit var enhancementLabel: TextView
private lateinit var enhancementSlider: android.widget.SeekBar
private var resultManager: ValidationResultManager? = null
private var validationRunner: ValidationRunner? = null
// Frame callback
private val frameScheduler = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
@@ -55,58 +101,302 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Simple layout
val layout = android.widget.FrameLayout(this)
surfaceView = SurfaceView(this)
layout.addView(surfaceView)
statusTextView = TextView(this)
statusTextView.setTextColor(0xFFFFFFFF.toInt())
statusTextView.textSize = 16f
statusTextView.setPadding(20, 20, 20, 20)
statusTextView.text = "Initializing..."
layout.addView(statusTextView)
setContentView(layout)
setContentView(R.layout.activity_main)
// SurfaceView container
surfaceView = findViewById(R.id.surface_view)
surfaceView.holder.setFixedSize(512, 512)
statusTextView = findViewById(R.id.status_text)
testResultsHeader = findViewById(R.id.test_results_header)
resultsContainer = findViewById(R.id.results_container)
runButton = findViewById(R.id.run_button)
loadButton = findViewById(R.id.load_button)
optionsButton = findViewById(R.id.options_button)
enhancementContainer = findViewById(R.id.enhancement_container)
enhancementLabel = findViewById(R.id.enhancement_label)
enhancementSlider = findViewById(R.id.enhancement_slider)
enhancementSlider.setOnSeekBarChangeListener(object : android.widget.SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: android.widget.SeekBar?, progress: Int, fromUser: Boolean) {
globalEnhancementFactor = 1.0f + (progress / 100f) * 49.0f
enhancementLabel.text = String.format(Locale.US, "Enhancement: %.1fx", globalEnhancementFactor)
applyGlobalEnhancement()
}
override fun onStartTrackingTouch(seekBar: android.widget.SeekBar?) {}
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 Load Button
loadButton.setOnClickListener {
showLoadDialog()
}
// Setup Options Menu Button
optionsButton.setOnClickListener { view ->
val popup = android.widget.PopupMenu(this, view)
popup.menu.add(0, 1, 0, "Generate Golden")
popup.menu.add(0, 2, 0, "Export Test")
popup.menu.add(0, 3, 0, "Export Result")
popup.menu.add(0, 4, 0, "Test ADB Info")
popup.menu.add(0, 5, 0, "Result ADB Info")
popup.menu.add(0, 6, 0, "Toggle Enhancement Slider")
popup.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
}
}
true
}
popup.show()
}
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
choreographer = Choreographer.getInstance()
modelViewer = ModelViewer(surfaceView)
// Check permissions?
// We assume 'adb install -g' or permissions granted.
// But for scoped storage we might not need PERMISSION if reading from app-specific dirs,
// but user mentioned /sdcard/ so we need MANAGE_EXTERNAL_STORAGE or READ_EXTERNAL_STORAGE.
// For waiting/simplicity, we just try.
modelViewer = ModelViewer(surfaceView=surfaceView, manipulator=null)
inputManager = ValidationInputManager(this)
// Initialize IBL
createIndirectLight()
handleIntent()
}
private fun handleIntent() {
val intent = intent
val testConfigPath = intent.getStringExtra("test_config")
if (testConfigPath != null) {
startValidation(testConfigPath)
private fun showLoadDialog() {
val exportDir = getExternalFilesDir(null) ?: filesDir
// Filter out result zips (starting with "results_") to only show test bundles
val zips = exportDir.listFiles { _, name ->
name.endsWith(".zip") && !name.startsWith("results_")
}?.sortedByDescending { it.lastModified() } ?: emptyList()
if (zips.isEmpty()) {
AlertDialog.Builder(this)
.setTitle("Load Test")
.setMessage("No test bundles found.")
.setPositiveButton("OK", null)
.show()
return
}
val builder = AlertDialog.Builder(this)
builder.setTitle("Select Test Bundle")
val items = zips.map { it.name }.toTypedArray()
builder.setItems(items) { dialog, which ->
val selectedFile = zips[which]
loadZipBundle(selectedFile)
dialog.dismiss()
}
builder.setNegativeButton("Cancel", null)
builder.show()
}
private fun showTestAdbInfo() {
val exportDir = getExternalFilesDir(null) ?: filesDir
val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder()
message.append("Storage Path: $path<br><br>")
message.append("<b>--- PULL FROM DEVICE ---</b><br>")
if (isInternal) {
message.append("<tt>adb shell \"run-as $packageName cat files/&lt;filename&gt;\" &gt; &lt;filename&gt;</tt><br><br>")
} else {
statusTextView.text = "No test_config provided via Intent.\nUse -e test_config <path>"
Log.w(TAG, "No test config provided")
message.append("<tt>adb pull $path/&lt;filename&gt; .</tt><br><br>")
}
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>")
} 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;.")
AlertDialog.Builder(this)
.setTitle("Test Bundle ADB Info")
.setMessage(Html.fromHtml(message.toString(), Html.FROM_HTML_MODE_LEGACY))
.setPositiveButton("OK", null)
.show()
}
private fun showResultAdbInfo() {
val exportDir = getExternalFilesDir(null) ?: filesDir
val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder()
message.append("<b>--- PULL RESULTS ---</b><br>")
if (isInternal) {
message.append("<tt>adb shell \"run-as $packageName cat files/&lt;filename&gt;\" &gt; &lt;filename&gt;</tt><br><br>")
} else {
message.append("<tt>adb pull $path/&lt;filename&gt; .</tt><br><br>")
}
message.append("<b>--- AVAILABLE RESULTS ---</b><br>")
val zips = exportDir.listFiles { _, name ->
name.endsWith(".zip") && name.startsWith("results_")
}?.sortedByDescending { it.lastModified() } ?: emptyList()
if (zips.isEmpty()) {
message.append("No result zips found.<br>")
} else {
zips.forEach { file ->
message.append("${file.name}<br>")
}
}
AlertDialog.Builder(this)
.setTitle("Result ADB Info")
.setMessage(Html.fromHtml(message.toString(), Html.FROM_HTML_MODE_LEGACY))
.setPositiveButton("OK", null)
.show()
}
private fun loadZipBundle(file: File) {
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}"
} catch (e: Exception) {
Log.e(TAG, "Failed to load zip", e)
statusTextView.text = "Error: ${e.message}"
}
}
}
private fun createIndirectLight() {
try {
val engine = modelViewer.engine
val scene = modelViewer.scene
val iblName = "default_env"
fun readAsset(path: String): ByteBuffer {
val input = assets.open(path)
val bytes = input.readBytes()
return ByteBuffer.wrap(bytes)
}
readAsset("envs/$iblName/${iblName}_ibl.ktx").let {
val bundle = KTX1Loader.createIndirectLight(engine, it)
scene.indirectLight = bundle.indirectLight
modelViewer.indirectLightCubemap = bundle.cubemap
scene.indirectLight!!.intensity = 30_000.0f
}
readAsset("envs/$iblName/${iblName}_skybox.ktx").let {
val bundle = KTX1Loader.createSkybox(engine, it)
scene.skybox = bundle.skybox
modelViewer.skyboxCubemap = bundle.cubemap
}
Log.i(TAG, "IBL loaded successfully")
} catch (e: Exception) {
Log.e(TAG, "Failed to load IBL", e)
statusTextView.text = "Warning: Failed to load IBL"
}
}
private fun startValidation(configPath: String) {
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}"
}
} catch (e: Exception) {
Log.e(TAG, "Failed to resolve config", e)
statusTextView.text = "Error: ${e.message}"
}
}
}
private fun createResultManager(outputDir: File): ValidationResultManager {
val gpuDriverInfo = com.google.android.filament.utils.DeviceUtils.getGpuDriverInfo(modelViewer.engine)
return ValidationResultManager(
outputDir = outputDir,
gpuDriverInfo = gpuDriverInfo,
deviceName = android.os.Build.MODEL,
deviceCodeName = android.os.Build.DEVICE,
androidVersion = android.os.Build.VERSION.RELEASE,
androidBuildNumber = android.os.Build.DISPLAY
)
}
private fun startValidation(input: ValidationInputManager.ValidationInput) {
try {
Log.i(TAG, "Parsing config from $configPath")
val config = ConfigParser.parseFromPath(configPath)
val outputDir = File(getExternalFilesDir(null), "validation_results")
Log.i(TAG, "Output dir: ${outputDir.absolutePath}")
validationRunner = ValidationRunner(this, modelViewer, config, outputDir)
resultsContainer.removeAllViews()
diffImageViews.clear()
enhancementSlider.isEnabled = false
Log.i(TAG, "Starting validation with config: ${input.config.name}")
Log.i(TAG, "Output dir: ${input.outputDir.absolutePath}")
testResultsHeader.text = "${input.config.name}"
resultManager = createResultManager(input.outputDir)
validationRunner = ValidationRunner(this, modelViewer, input.config, resultManager!!)
validationRunner?.callback = this
validationRunner?.generateGoldens = input.generateGoldens
validationRunner?.start()
} catch (e: Exception) {
Log.e(TAG, "Failed to start validation", e)
statusTextView.text = "Error: ${e.message}"
@@ -118,6 +408,12 @@ class MainActivity : Activity(), ValidationRunner.Callback {
choreographer.postFrameCallback(frameScheduler)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleIntent()
}
override fun onPause() {
super.onPause()
choreographer.removeFrameCallback(frameScheduler)
@@ -128,20 +424,154 @@ class MainActivity : Activity(), ValidationRunner.Callback {
choreographer.removeFrameCallback(frameScheduler)
}
override fun onTestFinished(result: ValidationRunner.TestResult) {
private var currentRenderedBitmap: Bitmap? = null
private var currentGoldenBitmap: Bitmap? = null
private var currentDiffBitmap: Bitmap? = null
override fun onTestFinished(result: ValidationResult) {
runOnUiThread {
val status = "Test ${result.name} finished: ${if(result.passed) "PASS" else "FAIL"}"
val status = "Test ${result.testName} finished: ${if(result.passed) "PASS" else "FAIL"}"
statusTextView.text = status
Log.i(TAG, status)
// Container for this result
val resultContainer = LinearLayout(this)
resultContainer.orientation = LinearLayout.VERTICAL
resultContainer.setPadding(0, 10, 0, 20)
// Header Layout
val headerRow = LinearLayout(this)
headerRow.orientation = LinearLayout.HORIZONTAL
headerRow.gravity = android.view.Gravity.CENTER_VERTICAL
// Status Icon + Name
val statusView = TextView(this)
val icon = if (result.passed) "" else ""
statusView.text = "$icon ${result.testName}"
statusView.setTextColor(
if (result.passed) Color.parseColor("#4CAF50") else Color.parseColor("#F44336")
)
statusView.textSize = 12f
statusView.setTypeface(null, android.graphics.Typeface.BOLD)
headerRow.addView(statusView)
// Diff Metric (only show if it's relevant/there's a diff or we just always show it like before)
val diffView = TextView(this)
diffView.text = " (Diff: ${result.diffMetric})"
diffView.textSize = 12f
diffView.setTextColor(Color.GRAY)
headerRow.addView(diffView)
resultContainer.addView(headerRow)
// Images Row
val imagesRow = LinearLayout(this)
imagesRow.orientation = LinearLayout.HORIZONTAL
val testImages = TestImages(
testName = result.testName,
golden = currentGoldenBitmap,
rendered = currentRenderedBitmap,
diff = currentDiffBitmap,
alphaDiff = currentAlphaDiffBitmap
)
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)
}
}
addImage("Rendered", currentRenderedBitmap, false)
addImage("Golden", currentGoldenBitmap, false)
if (!result.passed) {
addImage("Diff", currentDiffBitmap, true)
}
if (currentAlphaDiffBitmap != null) {
addImage("Alpha Diff", currentAlphaDiffBitmap, true)
}
resultContainer.addView(imagesRow)
resultsContainer.addView(resultContainer)
// Clear current images for next test
currentRenderedBitmap = null
currentGoldenBitmap = null
currentDiffBitmap = null
currentAlphaDiffBitmap = null
}
}
override fun onAllTestsFinished() {
runOnUiThread {
statusTextView.text = "All tests finished!"
Log.i(TAG, "All tests finished")
// Optional: Auto-close activity?
// finish()
enhancementSlider.isEnabled = true
Log.i(TAG, "All tests finished " + if (currentInput?.autoExport == true) "Exporting bundle" else "x")
if (currentInput?.autoExport == true) {
exportTestBundleAction()
}
if (currentInput?.autoExportResults == true) {
exportTestResultsAction()
}
}
}
private fun exportTestBundleAction() {
currentInput?.let { input ->
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: createResultManager(input.outputDir)
val zip = rm.exportTestBundle(input.config, timestamp)
if (zip != null) {
val msg = "Exported Bundle: ${zip.name}"
statusTextView.text = msg
Log.i(TAG, "Exported test bundle to ${zip.absolutePath}")
} else {
statusTextView.text = "Export Bundle failed"
Log.e(TAG, "Export Bundle failed")
}
}
}
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")
}
}
}
@@ -150,5 +580,179 @@ class MainActivity : Activity(), ValidationRunner.Callback {
statusTextView.text = status
}
}
}
override fun onImageResult(type: String, bitmap: Bitmap) {
runOnUiThread {
// Update the "live" views
when (type) {
"Rendered" -> {
currentRenderedBitmap = bitmap
}
"Golden" -> {
currentGoldenBitmap = bitmap
}
"Diff" -> {
currentDiffBitmap = bitmap
}
"Alpha Diff" -> {
currentAlphaDiffBitmap = bitmap
}
}
}
}
private fun applyEnhancementToView(iv: ImageView, factor: Float) {
val cm = android.graphics.ColorMatrix()
cm.setScale(factor, factor, factor, 1.0f)
iv.colorFilter = android.graphics.ColorMatrixColorFilter(cm)
}
private fun applyGlobalEnhancement() {
for (iv in diffImageViews) {
applyEnhancementToView(iv, globalEnhancementFactor)
}
}
private fun showImageDialog(images: TestImages, initialLabel: String) {
val dialogView = layoutInflater.inflate(R.layout.dialog_image_viewer, null)
val dialog = AlertDialog.Builder(this)
.setView(dialogView)
.create()
val titleView = dialogView.findViewById<TextView>(R.id.dialog_title)
val typeView = dialogView.findViewById<TextView>(R.id.dialog_image_type)
val imageView = dialogView.findViewById<ImageView>(R.id.dialog_image)
val btnClose = dialogView.findViewById<View>(R.id.btn_close)
val btnReset = dialogView.findViewById<View>(R.id.btn_reset)
val btnPrev = dialogView.findViewById<View>(R.id.btn_prev)
val btnNext = dialogView.findViewById<View>(R.id.btn_next)
val enhancementContainer = dialogView.findViewById<View>(R.id.dialog_enhancement_container)
val enhancementLabel = dialogView.findViewById<TextView>(R.id.dialog_enhancement_label)
val enhancementSlider = dialogView.findViewById<android.widget.SeekBar>(R.id.dialog_enhancement_slider)
titleView.text = images.testName
val availableImages = mutableListOf<Pair<String, Bitmap>>()
images.rendered?.let { availableImages.add(Pair("Rendered", it)) }
images.golden?.let { availableImages.add(Pair("Golden", it)) }
images.diff?.let { availableImages.add(Pair("Diff", it)) }
images.alphaDiff?.let { availableImages.add(Pair("Alpha Diff", it)) }
if (availableImages.isEmpty()) return
var currentIndex = availableImages.indexOfFirst { it.first == initialLabel }
if (currentIndex == -1) currentIndex = 0
var currentDialogEnhancement = globalEnhancementFactor
val matrix = android.graphics.Matrix()
// Save initial values for translation tracking
var lastTouchX = 0f
var lastTouchY = 0f
var isDragging = false
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)
imageView.imageMatrix = matrix
return true
}
})
imageView.setOnTouchListener { _, event ->
scaleDetector.onTouchEvent(event)
when (event.actionMasked) {
android.view.MotionEvent.ACTION_DOWN -> {
lastTouchX = event.x
lastTouchY = event.y
isDragging = true
}
android.view.MotionEvent.ACTION_MOVE -> {
if (isDragging && !scaleDetector.isInProgress) {
val dx = event.x - lastTouchX
val dy = event.y - lastTouchY
matrix.postTranslate(dx, dy)
imageView.imageMatrix = matrix
}
lastTouchX = event.x
lastTouchY = event.y
}
android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL -> {
isDragging = false
}
}
true
}
fun updateView() {
val (label, bitmap) = availableImages[currentIndex]
typeView.text = label
imageView.setImageBitmap(bitmap)
(imageView.drawable as? android.graphics.drawable.BitmapDrawable)?.setAntiAlias(false)
(imageView.drawable as? android.graphics.drawable.BitmapDrawable)?.setFilterBitmap(false)
imageView.imageMatrix = matrix
if (label == "Diff" || label == "Alpha Diff") {
enhancementContainer.visibility = View.VISIBLE
applyEnhancementToView(imageView, currentDialogEnhancement)
} else {
enhancementContainer.visibility = View.GONE
imageView.colorFilter = null
}
}
fun resetMatrix() {
val drawable = imageView.drawable ?: return
val width = imageView.width.toFloat()
val height = imageView.height.toFloat()
val dw = drawable.intrinsicWidth.toFloat()
val dh = drawable.intrinsicHeight.toFloat()
val scaleX = width / dw
val scaleY = height / dh
val scale = Math.min(scaleX, scaleY)
val dx = (width - dw * scale) / 2f
val dy = (height - dh * scale) / 2f
matrix.reset()
matrix.postScale(scale, scale)
matrix.postTranslate(dx, dy)
imageView.imageMatrix = matrix
}
btnClose.setOnClickListener { dialog.dismiss() }
btnReset.setOnClickListener { resetMatrix() }
btnPrev.setOnClickListener {
currentIndex = (currentIndex - 1 + availableImages.size) % availableImages.size
updateView()
}
btnNext.setOnClickListener {
currentIndex = (currentIndex + 1) % availableImages.size
updateView()
}
val defaultProgress = ((currentDialogEnhancement - 1.0f) / 49.0f * 100).toInt()
val safeProgress = Math.max(0, Math.min(100, defaultProgress))
enhancementSlider.progress = safeProgress
enhancementLabel.text = String.format(Locale.US, "Enhance: %.1fx", currentDialogEnhancement)
enhancementSlider.setOnSeekBarChangeListener(object : android.widget.SeekBar.OnSeekBarChangeListener {
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)
updateView()
}
override fun onStartTrackingTouch(seekBar: android.widget.SeekBar?) {}
override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) {}
})
imageView.post {
resetMatrix()
}
updateView()
dialog.show()
}
}

View File

@@ -70,6 +70,26 @@ class ConfigParser {
}
}
// Explicit models map override
val modelsJson = json.optJSONObject("models")
if (modelsJson != null) {
val keys = modelsJson.keys()
while (keys.hasNext()) {
val name = keys.next()
val path = modelsJson.getString(name)
// Resolve path relative to baseDir if not absolute
val file = File(path)
if (file.isAbsolute) {
models[name] = path
} else if (baseDir != null) {
models[name] = File(baseDir, path).absolutePath
} else {
models[name] = path
}
}
}
val presetsJson = json.optJSONArray("presets")
val presets = mutableMapOf<String, PresetConfig>()
if (presetsJson != null) {

View File

@@ -0,0 +1,264 @@
/*
* 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.
*/
package com.google.android.filament.validation
import android.content.Context
import android.content.Intent
import android.util.Log
import kotlinx.coroutines.Dispatchers
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 {
private const val TAG = "ValidationInputManager"
}
data class ValidationInput(
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
)
/**
* 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)
val outputPath = intent.getStringExtra("output_path")
Log.i(TAG, "Resolving config with outputPath: $outputPath")
val baseDir = context.getExternalFilesDir(null) ?: context.filesDir
Log.i(TAG, "Base directory for resolution: ${baseDir.absolutePath}")
val outputDir = if (!outputPath.isNullOrBlank()) {
val file = File(outputPath)
val resolved = if (file.isAbsolute) {
file
} 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")
File(baseDir, "validation_results")
} else {
resolved
}
} 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) {
val file = File(zipPath)
if (file.isAbsolute) {
file
} else {
File(baseDir, zipPath)
}
} else {
null
}
val config = when {
sourceZipFile != null && sourceZipFile.exists() -> loadFromZip(sourceZipFile)
urlConfig != null -> downloadConfig(urlConfig, urlModelsBase)
testConfigPath != null -> ConfigParser.parseFromPath(testConfigPath)
else -> extractDefaultAssets()
}
return@withContext ValidationInput(config, outputDir, generateGoldens, autoRun, autoExport, autoExportResults, sourceZipFile)
}
suspend fun loadFromZip(zipFile: File): RenderTestConfig {
Log.i(TAG, "Unzipping validation bundle: ${zipFile.absolutePath}")
// Use a unique cache dir based on timestamp or just overwrite a common one
// Overwriting is safer to avoid filling up disk, but we must ensure we don't conflict with current run
val baseCacheDir = context.externalCacheDir ?: context.cacheDir
val cacheDir = File(baseCacheDir, "unzipped_validation")
if (cacheDir.exists()) cacheDir.deleteRecursively()
cacheDir.mkdirs()
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)
}
}
}
}
}
// 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()
Log.i(TAG, "Parsed config from ${configFile.absolutePath}")
return ConfigParser.parseFromPath(configFile.absolutePath)
}
private suspend fun extractDefaultAssets(): RenderTestConfig {
Log.i(TAG, "Extracting default assets...")
val filesDir = context.getExternalFilesDir(null) ?: context.filesDir
val assetManager = context.assets
// Copy default_test.json
val configDir = File(filesDir, "config")
configDir.mkdirs()
val configOut = File(configDir, "default_test.json")
assetManager.open("default_test.json").use { input ->
FileOutputStream(configOut).use { output ->
input.copyTo(output)
}
}
// Copy DamagedHelmet.glb
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)
}
}
// 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())
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)
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)
}
}
}
}

View File

@@ -0,0 +1,307 @@
/*
* 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.
*/
package com.google.android.filament.validation
import android.graphics.Bitmap
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import org.json.JSONArray
import org.json.JSONObject
data class ValidationResult(
val testName: String,
val passed: Boolean,
val diffMetric: Float = 0f
)
class ValidationResultManager(
private val outputDir: File,
private val gpuDriverInfo: String,
private val deviceName: String,
private val deviceCodeName: String,
private val androidVersion: String,
private val androidBuildNumber: String
) {
companion object {
private const val TAG = "ValidationResultManager"
}
private val results = mutableListOf<ValidationResult>()
init {
if (!outputDir.exists()) {
outputDir.mkdirs()
}
}
fun addResult(result: ValidationResult) {
results.add(result)
}
fun saveImage(name: String, bitmap: Bitmap) {
val file = File(outputDir, "$name.png")
try {
FileOutputStream(file).use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to save image $name", e)
}
}
fun getOutputDir(): File {
return outputDir
}
fun finalizeResults(totalTimeMs: Long): File? {
// Write results JSON
writeResultsJson(totalTimeMs)
return null
}
/**
* Exports a zip containing:
* - results.json
* - input test bundle (as nested zip), if provided
* - diff images (if any failure)
*/
fun exportTestResults(sourceZip: File?, timestamp: String): File? {
// Safe parent dir resolution
val parentDir = outputDir.canonicalFile.parentFile ?: outputDir.parentFile
if (parentDir == null) return null
val resultZipName = "results_$timestamp"
val zipFile = File(parentDir, "$resultZipName.zip")
Log.i(TAG, "Exporting results to ${zipFile.absolutePath}")
try {
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
// 1. Add results.json
val resultsJson = File(outputDir, "results.json")
if (resultsJson.exists()) {
zos.putNextEntry(ZipEntry("results.json"))
resultsJson.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
// 2. Add source zip if exists
if (sourceZip != null && sourceZip.exists()) {
zos.putNextEntry(ZipEntry(sourceZip.name))
sourceZip.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
// 3. Add images (only rendered images, exclude diffs)
outputDir.listFiles { _, name -> name.endsWith(".png") && !name.endsWith("_diff.png") }?.forEach { imgFile ->
zos.putNextEntry(ZipEntry(imgFile.name))
imgFile.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
Log.i(TAG, "Exported results to ${zipFile.absolutePath}")
return zipFile
} catch (e: Exception) {
Log.e(TAG, "Failed to export results", e)
return null
}
}
/**
* Exports a zip bundle containing:
* - Modified config.json (with updated name and relative paths)
* - Models (in models/ subdirectory)
* - Golden images (in goldens/ subdirectory)
*
* Structure:
* test_name_TIMESTAMP/
* config.json
* models/
* model.glb
* goldens/
* test_result.png
*/
fun exportTestBundle(config: RenderTestConfig, timestamp: String): File? {
Log.i(TAG, "Starting exportTestBundle for ${config.name} at $timestamp")
Log.i(TAG, "OutputDir: ${outputDir.absolutePath}")
val parentDir = outputDir.canonicalFile.parentFile ?: outputDir.parentFile
if (parentDir == null) {
Log.e(TAG, "OutputDir parent is null: ${outputDir.absolutePath}")
return null
}
Log.i(TAG, "Using parentDir for export: ${parentDir.absolutePath}")
val testNameWithTimestamp = "${config.name}_$timestamp"
val exportNameNoSpaces = testNameWithTimestamp.replace(" ", "_")
val exportDir = File(parentDir, "export_temp_$timestamp")
Log.i(TAG, "Creating export temp dir: ${exportDir.absolutePath}")
if (exportDir.exists()) exportDir.deleteRecursively()
if (!exportDir.mkdirs()) {
Log.e(TAG, "Failed to create export dir: ${exportDir.absolutePath}")
return null
}
val rootDir = File(exportDir, exportNameNoSpaces)
rootDir.mkdirs()
val modelsDir = File(rootDir, "models")
modelsDir.mkdirs()
val goldensDir = File(rootDir, "goldens")
goldensDir.mkdirs()
try {
// 1. Copy Models and update config map
val newModelsMap = mutableMapOf<String, String>()
Log.i(TAG, "Copying models...")
for ((modelName, modelPath) in config.models) {
val sourceFile = File(modelPath)
if (sourceFile.exists()) {
val destFile = File(modelsDir, sourceFile.name)
Log.d(TAG, "Copying model $modelName: $modelPath -> ${destFile.name}")
sourceFile.copyTo(destFile, overwrite = true)
// Use relative path for the new config
newModelsMap[modelName] = "models/${sourceFile.name}"
} else {
Log.w(TAG, "Model file not found: $modelPath")
}
}
// 2. Copy Golden Images
// We assume goldens are in outputDir with .png extension
Log.i(TAG, "Copying goldens from ${outputDir.absolutePath}...")
outputDir.listFiles { _, name -> name.endsWith(".png") }?.forEach { file ->
Log.d(TAG, "Copying golden: ${file.name}")
file.copyTo(File(goldensDir, file.name), overwrite = true)
}
// 3. Create modified config JSON
Log.i(TAG, "Creating config.json...")
val newConfigJson = JSONObject()
newConfigJson.put("name", testNameWithTimestamp) // Keep spaces in JSON name
// Reconstruct backends
val backendsArray = JSONArray()
config.backends.forEach { backendsArray.put(it) }
newConfigJson.put("backends", backendsArray)
// Reconstruct models
val modelsJson = JSONObject()
for ((k, v) in newModelsMap) {
modelsJson.put(k, v)
}
newConfigJson.put("models", modelsJson)
// Reconstruct tests
val testsArray = JSONArray()
for (test in config.tests) {
val testJson = JSONObject()
testJson.put("name", test.name)
if (test.description != null) testJson.put("description", test.description)
// Models for this test (set of strings)
val testModelsArray = JSONArray()
test.models.forEach { testModelsArray.put(it) }
testJson.put("models", testModelsArray)
// Rendering settings
testJson.put("rendering", test.rendering)
// Tolerance
if (test.tolerance != null) {
testJson.put("tolerance", test.tolerance)
}
// Backends (optional override)
val testBackends = JSONArray()
test.backends.forEach { testBackends.put(it) }
testJson.put("backends", testBackends)
testsArray.put(testJson)
}
newConfigJson.put("tests", testsArray)
// Write config.json
File(rootDir, "config.json").writeText(newConfigJson.toString(4))
// 4. Zip it
val zipFile = File(parentDir, "$exportNameNoSpaces.zip")
Log.i(TAG, "Zipping to ${zipFile.absolutePath}...")
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
rootDir.walkTopDown().forEach { file ->
if (file.isFile) {
val entryName = file.relativeTo(exportDir).path
zos.putNextEntry(ZipEntry(entryName))
file.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
}
// Cleanup temp dir
exportDir.deleteRecursively()
Log.i(TAG, "Exported test bundle to ${zipFile.absolutePath}")
return zipFile
} catch (e: Exception) {
Log.e(TAG, "Failed to export test bundle", e)
exportDir.deleteRecursively()
return null
}
}
private fun writeResultsJson(totalTimeMs: Long) {
val rootObject = JSONObject()
val metadataObject = JSONObject()
metadataObject.put("gpu_driver_info", gpuDriverInfo ?: "")
metadataObject.put("total_time_ms", totalTimeMs)
metadataObject.put("device_name", deviceName ?: "")
metadataObject.put("device_code_name", deviceCodeName ?: "")
metadataObject.put("android_version", androidVersion ?: "")
metadataObject.put("android_build_number", androidBuildNumber ?: "")
rootObject.put("metadata", metadataObject)
val jsonArray = JSONArray()
for (result in results) {
val jsonObject = JSONObject()
jsonObject.put("test_name", result.testName)
jsonObject.put("passed", result.passed)
jsonObject.put("diff_metric", result.diffMetric)
jsonArray.put(jsonObject)
}
rootObject.put("results", jsonArray)
val jsonFile = File(outputDir, "results.json")
try {
FileOutputStream(jsonFile).use { out ->
out.write(rootObject.toString(4).toByteArray())
}
} catch (e: Exception) {
Log.e(TAG, "Failed to write results.json", e)
}
}
}

View File

@@ -19,25 +19,19 @@ package com.google.android.filament.validation
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import com.google.android.filament.Engine
import com.google.android.filament.Renderer
import com.google.android.filament.View
import com.google.android.filament.utils.AutomationEngine
import com.google.android.filament.utils.ImageDiff
import com.google.android.filament.utils.ModelViewer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
class ValidationRunner(
private val context: Context,
private val modelViewer: ModelViewer,
private val config: RenderTestConfig,
private val outputDir: File
private val resultManager: ValidationResultManager
) {
private var currentState = State.IDLE
@@ -46,31 +40,33 @@ class ValidationRunner(
private var currentEngine: AutomationEngine? = null
private var currentTestConfig: TestConfig? = null
private var currentModelName: String? = null
private var loadStartFence: com.google.android.filament.Fence? = null
private var loadStartTime = 0L
private var frameCounter = 0
private var suiteStartTime: Long = 0
enum class State {
IDLE,
LOADING_MODEL,
WAITING_FOR_FENCE,
RUNNING_TEST,
COMPARING
WAITING_FOR_RESOURCES,
WARMUP,
RUNNING_TEST
}
interface Callback {
fun onTestFinished(result: TestResult)
fun onTestFinished(result: ValidationResult)
fun onAllTestsFinished()
fun onStatusChanged(status: String)
fun onImageResult(type: String, bitmap: Bitmap)
}
var callback: Callback? = null
var generateGoldens: Boolean = false
fun start() {
if (config.tests.isEmpty()) {
callback?.onAllTestsFinished()
return
}
suiteStartTime = System.currentTimeMillis()
currentTestIndex = 0
currentModelIndex = 0
startTest(config.tests[0])
@@ -87,6 +83,17 @@ class ValidationRunner(
}
private fun startModel(modelName: String) {
if (currentModelName == modelName) {
Log.i("ValidationRunner", "Reusing model $modelName")
callback?.onStatusChanged("Reusing $modelName for ${currentTestConfig?.name}")
modelViewer.resetToDefaultState()
frameCounter = 0
currentState = State.WARMUP
return
}
currentModelName = modelName
val modelPath = config.models[modelName]
if (modelPath == null) {
@@ -94,12 +101,9 @@ class ValidationRunner(
nextModel()
return
}
currentState = State.LOADING_MODEL
callback?.onStatusChanged("Loading $modelName for ${currentTestConfig?.name}")
// Load model on main thread (required by ModelViewer)
// We assume this is called from main thread or we dispatch
loadModel(modelPath)
}
@@ -107,35 +111,37 @@ class ValidationRunner(
// Assume called on Main Thread
modelViewer.destroyModel()
try {
Log.i("ValidationRunner", "Reading model file: $path")
val bytes = File(path).readBytes()
Log.i("ValidationRunner", "Loading GLB buffer... (${bytes.size} bytes)")
val buffer = ByteBuffer.wrap(bytes)
modelViewer.loadModelGlb(buffer)
Log.i("ValidationRunner", "Model loaded.")
modelViewer.transformToUnitCube()
loadStartFence = modelViewer.engine.createFence()
loadStartTime = System.nanoTime()
currentState = State.WAITING_FOR_FENCE
currentState = State.WAITING_FOR_RESOURCES
frameCounter = 0
Log.i("ValidationRunner", "State set to WAITING_FOR_RESOURCES")
} catch (e: Exception) {
Log.e("ValidationRunner", "Failed to load $path", e)
nextModel()
Log.e("ValidationRunner", "Failed to load $path", e)
nextModel()
}
}
fun onFrame(frameTimeNanos: Long) {
when (currentState) {
State.IDLE -> {}
State.WAITING_FOR_FENCE -> {
loadStartFence?.let { fence ->
if (fence.wait(com.google.android.filament.Fence.Mode.FLUSH, 0) == com.google.android.filament.Fence.FenceStatus.CONDITION_SATISFIED) {
modelViewer.engine.destroyFence(fence)
loadStartFence = null
// Compile materials (simplified)
modelViewer.scene.forEach { entity ->
// ... existing material compilation logic ...
}
startAutomation()
}
State.WAITING_FOR_RESOURCES -> {
val progress = modelViewer.progress
if (progress >= 1.0f) {
Log.i("ValidationRunner", "Resources loaded. Starting warmup.")
frameCounter = 0
currentState = State.WARMUP
}
}
State.WARMUP -> {
frameCounter++
if (frameCounter > 5) { // 5 frames warmup
startAutomation()
}
}
State.RUNNING_TEST -> {
@@ -145,21 +151,20 @@ class ValidationRunner(
content.renderer = modelViewer.renderer
content.scene = modelViewer.scene
content.lightManager = modelViewer.engine.lightManager
// Tick
// Delta time?
val deltaTime = 1.0f / 60.0f // Fixed step for consistency?
val deltaTime = 1.0f / 60.0f
engine.tick(modelViewer.engine, content, deltaTime)
if (!engine.isRunning) {
frameCounter++
if (engine.shouldClose()) {
Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)")
// Test finished (for this spec)
currentState = State.COMPARING
currentState = State.IDLE
captureAndCompare()
}
}
}
State.COMPARING -> {} // Busy
State.LOADING_MODEL -> {}
}
}
@@ -175,104 +180,157 @@ class ValidationRunner(
options.sleepDuration = 0.0f // Minimal sleep, let frames drive it
options.minFrameCount = 5 // Ensure some frames pass
currentEngine?.setOptions(options)
currentEngine?.startRunning()
// Use batch mode to ensure shouldClose() works reliably
currentEngine?.startBatchMode()
currentEngine?.signalBatchMode() // Start immediately
frameCounter = 0
currentState = State.RUNNING_TEST
}
private fun captureAndCompare() {
callback?.onStatusChanged("Comparing ${currentTestConfig?.name}...")
val view = modelViewer.view
val renderer = modelViewer.renderer
val width = view.viewport.width
val height = view.viewport.height
val buffer = ByteBuffer.allocateDirect(width * height * 4)
val pbd = com.google.android.filament.Texture.PixelBufferDescriptor(
buffer,
com.google.android.filament.Texture.Format.RGBA,
com.google.android.filament.Texture.Type.UBYTE,
1, 0, 0, 0, 0, // alignment, left, top, stride (0=default)
null // handler (null = current thread? no, handler is for callback)
) {
// Callback when readPixels is done
// Dispatch to background thread for comparison to avoid blocking UI?
// "it" is undefined here? The callback interface is Runnable?
// Kotlin lambda for Runnable.
compareCapturedImage(buffer, width, height)
modelViewer.debugGetNextFrameCallback { bitmap ->
compareCapturedImage(bitmap)
}
renderer.readPixels(0, 0, width, height, pbd)
}
private fun compareCapturedImage(buffer: java.nio.Buffer, width: Int, height: Int) {
// This runs on... which thread? Filament driver thread possibly.
// We should use a helper to process.
private fun compareCapturedImage(bitmap: Bitmap) {
val testName = currentTestConfig!!.name
val modelName = currentModelName!!
val backend = "opengl" // Hardcoded for now, or get from View/Engine?
val backend = currentTestConfig?.backends?.firstOrNull() ?: "opengl"
val testFullName = "${testName}.${backend}.${modelName}"
// Golden path
// We expect a golden directory.
val goldenFile = File(config.models.get(modelName)!!).parentFile.parentFile.resolve("golden/${testFullName}.png")
// Strategy: models are in .../models/model.glb
// Goldens are in .../golden/
val modelFile = File(config.models.get(modelName)!!)
val modelParent = modelFile.parentFile!!
// Search for golden in:
// 1. ../golden/ (standard structure)
// 2. ../goldens/ (exported structure, sibling of models)
// 3. ./goldens/ (backup)
val searchPaths = mutableListOf<File>()
modelParent.parentFile?.let {
searchPaths.add(it.resolve("golden"))
searchPaths.add(it.resolve("goldens"))
}
searchPaths.add(modelParent.resolve("goldens"))
var goldenFile: File? = null
for (path in searchPaths) {
val f = path.resolve("${testFullName}.png")
if (f.exists()) {
goldenFile = f
break
}
}
if (goldenFile != null) {
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 {
try {
// Convert buffer to Bitmap
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(buffer)
// Flip Y? ReadPixels is typically bottom-up?
// Filament readPixels is bottom-left? YES.
// Bitmap is top-left.
// We need to flip.
val matrix = android.graphics.Matrix()
matrix.postScale(1f, -1f)
val flipped = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true)
val flipped = bitmap
callback?.onImageResult("Rendered", flipped)
var passed = false
if (goldenFile.exists()) {
val golden = android.graphics.BitmapFactory.decodeFile(goldenFile.absolutePath)
if (golden != null) {
// Populate tolerance from config
val tol = currentTestConfig?.tolerance ?: org.json.JSONObject()
val tolJson = tol.toString()
val result = ImageDiff.compare(golden, flipped, tolJson, null)
passed = (result.status == ImageDiff.Result.Status.PASSED)
// Save diff if failed?
if (!passed) {
val diffFile = File(outputDir, "${testFullName}_diff.png")
if (result.diffImage != null) {
FileOutputStream(diffFile).use { out ->
result.diffImage.compress(Bitmap.CompressFormat.PNG, 100, out)
}
}
}
} else {
Log.e("ValidationRunner", "Failed to load golden: ${goldenFile.absolutePath}")
var diffMetric = 0f
if (generateGoldens) {
val targetGolden = goldenFile ?: modelParent.parentFile?.resolve("golden/${testFullName}.png") ?: File(modelParent, "golden/${testFullName}.png")
targetGolden.parentFile?.mkdirs()
FileOutputStream(targetGolden).use { out ->
flipped.compress(Bitmap.CompressFormat.PNG, 100, out)
}
passed = true // Generating goldens always passes if successful
callback?.onStatusChanged("Golden generated")
} else {
Log.w("ValidationRunner", "Golden not found: ${goldenFile.absolutePath}")
}
// Save output
val outFile = File(outputDir, "${testFullName}.png")
FileOutputStream(outFile).use { out ->
flipped.compress(Bitmap.CompressFormat.PNG, 100, out)
if (goldenFile != null && goldenFile.exists()) {
val golden = android.graphics.BitmapFactory.decodeFile(goldenFile.absolutePath)
if (golden != null) {
callback?.onImageResult("Golden", golden)
val tol = currentTestConfig?.tolerance ?: org.json.JSONObject()
val tolJson = tol.toString()
val result = ImageDiff.compare(golden, flipped, tolJson, null)
passed = (result.status == ImageDiff.Result.Status.PASSED)
diffMetric = result.failingPixelCount.toFloat()
if (!passed) {
if (result.diffImage != null) {
val diffImg = result.diffImage!!
val width = diffImg.width
val height = diffImg.height
val pixels = IntArray(width * height)
diffImg.getPixels(pixels, 0, width, 0, 0, width, height)
var hasAlphaDiff = false
val alphaPixels = IntArray(width * height)
for (i in pixels.indices) {
val color = pixels[i]
val a = android.graphics.Color.alpha(color)
val r = android.graphics.Color.red(color)
val g = android.graphics.Color.green(color)
val b = android.graphics.Color.blue(color)
if (a > 0) {
hasAlphaDiff = true
}
// Map alpha diff to grayscale RGB
alphaPixels[i] = android.graphics.Color.argb(255, a, a, a)
// Force main diff image alpha to 255
pixels[i] = android.graphics.Color.argb(255, r, g, b)
}
// Apply updated pixels to diff image
diffImg.setPixels(pixels, 0, width, 0, 0, width, height)
// The C++ ImageDiff code sets isPremultiplied to false so Android
// doesn't erase RGB diff values when Alpha diff is 0. However, Android's
// Canvas will crash if we try to draw a non-premultiplied bitmap.
// Since we just forced all alpha values to 255 (fully opaque) in the
// loop above, we can safely mark it as premultiplied again here.
diffImg.isPremultiplied = true
callback?.onImageResult("Diff", diffImg)
resultManager.saveImage("${testFullName}_diff", diffImg)
if (hasAlphaDiff) {
val alphaDiffImg = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
alphaDiffImg.setPixels(alphaPixels, 0, width, 0, 0, width, height)
callback?.onImageResult("Alpha Diff", alphaDiffImg)
resultManager.saveImage("${testFullName}_alpha_diff", alphaDiffImg)
}
}
}
} else {
callback?.onStatusChanged("Failed to load golden")
}
} else {
Log.w("ValidationRunner", "Golden not found: ${goldenFile?.absolutePath}")
callback?.onStatusChanged("Golden not found")
}
}
callback?.onTestFinished(TestResult(testFullName, passed))
// Schedule next model on main thread
// Use Handler or View.post
modelViewer.view.viewport
// dispatch nextModel()
// Save output
resultManager.saveImage(testFullName, flipped)
val result = ValidationResult(testFullName, passed, diffMetric)
resultManager.addResult(result)
callback?.onTestFinished(result)
android.os.Handler(android.os.Looper.getMainLooper()).post {
nextModel()
}
@@ -300,28 +358,11 @@ class ValidationRunner(
startTest(config.tests[currentTestIndex])
} else {
currentState = State.IDLE
zipResults()
val totalTimeMs = System.currentTimeMillis() - suiteStartTime
resultManager.finalizeResults(totalTimeMs)
callback?.onAllTestsFinished()
}
}
private fun zipResults() {
callback?.onStatusChanged("Zipping results...")
val zipFile = File(outputDir, "results.zip")
try {
java.util.zip.ZipOutputStream(java.io.FileOutputStream(zipFile)).use { zos ->
outputDir.walkTopDown().filter { it.isFile && it.name != "results.zip" }.forEach { file ->
val entryName = file.relativeTo(outputDir).path
zos.putNextEntry(java.util.zip.ZipEntry(entryName))
file.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
Log.i("ValidationRunner", "Zipped results to ${zipFile.absolutePath}")
} catch (e: Exception) {
Log.e("ValidationRunner", "Failed to zip results", e)
}
}
data class TestResult(val name: String, val passed: Boolean)
}

View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/surface_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintWidth_percent="0.6"
android:layout_marginTop="32dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<LinearLayout
android:id="@+id/controls_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingTop="20dp"
android:paddingBottom="0dp"
app:layout_constraintTop_toBottomOf="@id/surface_container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Initializing..."
android:textSize="12sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/run_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/Widget.Material3.Button"
android:text="Run Test" />
<com.google.android.material.button.MaterialButton
android:id="@+id/load_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.TonalButton"
android:text="Load Test"
android:layout_marginStart="8dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/options_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.IconButton.Outlined"
app:icon="@android:drawable/ic_menu_more"
android:contentDescription="More Options"
android:layout_marginStart="8dp"/>
</LinearLayout>
<!-- Global Enhancement Controls (Hidden by default) -->
<LinearLayout
android:id="@+id/enhancement_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:visibility="gone"
android:paddingBottom="8dp">
<TextView
android:id="@+id/enhancement_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Enhancement: 1.0x"
android:textSize="12sp"
android:minWidth="120dp" />
<SeekBar
android:id="@+id/enhancement_slider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:max="100"
android:progress="0" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/test_results_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Test Results"
android:gravity="center"
android:textSize="14sp"
android:paddingTop="0dp"
android:paddingBottom="8dp" />
</LinearLayout>
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/controls_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingBottom="20dp">
<LinearLayout
android:id="@+id/results_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#FF202020"
android:padding="8dp">
<!-- Header with title and close button -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/dialog_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Test Result"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:textStyle="bold" />
<ImageButton
android:id="@+id/btn_close"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_close_clear_cancel" />
</LinearLayout>
<!-- Subtitle for Image Type -->
<TextView
android:id="@+id/dialog_image_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:text="Rendered"
android:textColor="#BBBBBB"
android:textSize="14sp"
android:paddingBottom="8dp" />
<!-- Image Area with Arrows -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintDimensionRatio="1:1">
<ImageView
android:id="@+id/dialog_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="matrix" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Navigation Arrows -->
<LinearLayout
android:id="@+id/dialog_arrow_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageButton
android:id="@+id/btn_prev"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_media_previous"
app:tint="#FFFFFF" />
<ImageButton
android:id="@+id/btn_reset"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginHorizontal="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_revert"
app:tint="#FFFFFF" />
<ImageButton
android:id="@+id/btn_next"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_media_next"
app:tint="#FFFFFF" />
</LinearLayout>
<!-- Enhancement Controls (only for diff images) -->
<LinearLayout
android:id="@+id/dialog_enhancement_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="12dp"
android:paddingBottom="8dp"
android:visibility="gone">
<TextView
android:id="@+id/dialog_enhancement_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Enhance: 1.0x"
android:textColor="#FFFFFF"
android:textSize="12sp"
android:minWidth="100dp" />
<SeekBar
android:id="@+id/dialog_enhancement_slider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:max="100"
android:progress="0" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="5dp">
<TextView
android:id="@+id/image_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Label" />
<ImageView
android:id="@+id/image_view"
android:layout_width="300px"
android:layout_height="300px"
android:scaleType="fitCenter"
android:background="#404040" />
</LinearLayout>

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")

View File

@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-tools-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -10,7 +10,7 @@ kotlin {
jvmToolchain(versions.jdk)
}
filamentTools {
filament {
materialInputDir = project.layout.projectDirectory.dir("src/main/materials")
materialOutputDir = project.layout.projectDirectory.dir("src/main/assets/materials")
}

View File

@@ -1,3 +1,8 @@
// Filament tools plugin
pluginManagement {
includeBuild 'gradle-plugin'
}
// Libraries
include ':filament-android'
include ':filamat-android'

View File

@@ -214,6 +214,8 @@ ENABLE_PERFETTO=""
BACKEND_DEBUG_FLAG_OPTION=""
STEREOSCOPIC_OPTION=""
OSMESA_OPTION=""
IOS_BUILD_SIMULATOR=false
@@ -314,6 +316,7 @@ function build_desktop_target {
${ASAN_UBSAN_OPTION} \
${COVERAGE_OPTION} \
${BACKEND_DEBUG_FLAG_OPTION} \
${STEREOSCOPIC_OPTION} \
${OSMESA_OPTION} \
${architectures} \
../..
@@ -452,6 +455,7 @@ function build_android_target {
${VULKAN_ANDROID_OPTION} \
${WEBGPU_OPTION} \
${BACKEND_DEBUG_FLAG_OPTION} \
${STEREOSCOPIC_OPTION} \
${ENABLE_PERFETTO} \
../..
ln -sf "out/cmake-android-${lc_target}-${arch}/compile_commands.json" \
@@ -693,6 +697,7 @@ function build_ios_target {
${WEBGPU_OPTION} \
${MATDBG_OPTION} \
${MATOPT_OPTION} \
${STEREOSCOPIC_OPTION} \
../..
ln -sf "out/cmake-ios-${lc_target}-${arch}/compile_commands.json" \
../../compile_commands.json
@@ -1006,6 +1011,20 @@ while getopts ":hacCfgimp:q:uvWslwedtk:bVx:S:X:Py:" opt; do
;;
x) BACKEND_DEBUG_FLAG_OPTION="-DFILAMENT_BACKEND_DEBUG_FLAG=${OPTARG}"
;;
S) case $(echo "${OPTARG}" | tr '[:upper:]' '[:lower:]') in
instanced)
STEREOSCOPIC_OPTION="-DFILAMENT_SAMPLES_STEREO_TYPE=instanced"
;;
multiview)
STEREOSCOPIC_OPTION="-DFILAMENT_SAMPLES_STEREO_TYPE=multiview"
;;
*)
echo "Unknown stereoscopic type ${OPTARG}"
echo "Type must be one of [instanced|multiview]"
echo ""
exit 1
esac
;;
X) OSMESA_OPTION="-DFILAMENT_OSMESA_PATH=${OPTARG}"
;;
y)

View File

@@ -39,6 +39,11 @@ if [[ "$TARGET" == "presubmit-with-test" ]]; then
RUN_TESTS=-u
fi
if [[ "$TARGET" == "presubmit-with-archive" ]]; then
BUILD_RELEASE=release
GENERATE_ARCHIVES=-a
fi
if [[ "$TARGET" == "debug" ]]; then
BUILD_DEBUG=debug
GENERATE_ARCHIVES=-a

View File

@@ -54,8 +54,13 @@ if [[ "$OS_NAME" == "Linux" ]]; then
# is constantly being updated and sometimes not compatible with the current
# linux platform.
# Note that we assume this platform is compatible with ubuntu-22.04 x86_64
DPKG_ARCH=$(dpkg --print-architecture)
EXTRA_PACKAGES=""
if [[ "$DPKG_ARCH" == "amd64" ]]; then
EXTRA_PACKAGES="lib32gcc-s1 lib32stdc++6 libc6-i386"
fi
sudo apt-get -y install \
autoconf automake autopoint autotools-dev bindgen bison build-essential bzip2 cpp cpp-11 debhelper debugedit dh-autoreconf dh-strip-nondeterminism diffstat directx-headers-dev dpkg-dev dwz flex g++ g++-11 gcc gcc-11 gcc-11-base:amd64 gettext glslang-tools icu-devtools intltool-debian lib32gcc-s1 lib32stdc++6 libarchive-zip-perl libasan6:amd64 libatomic1:amd64 libc-dev-bin libc6-dbg:amd64 libc6-dev:amd64 libc6-i386 libcc1-0:amd64 libclang-${GITHUB_CLANG_VERSION}-dev libclang-common-${GITHUB_CLANG_VERSION}-dev libclang-cpp${GITHUB_CLANG_VERSION} libclang-cpp${GITHUB_CLANG_VERSION}-dev libclang1-14 libclang1-${GITHUB_CLANG_VERSION} libclc-${GITHUB_CLANG_VERSION} libclc-${GITHUB_CLANG_VERSION}-dev libcrypt-dev:amd64 libdebhelper-perl libdpkg-perl libdrm-amdgpu1:amd64 libdrm-dev:amd64 libdrm-intel1:amd64 libdrm-nouveau2:amd64 libdrm-radeon1:amd64 libelf-dev:amd64 libexpat1-dev:amd64 libffi-dev:amd64 libfile-stripnondeterminism-perl libgc1:amd64 libgcc-11-dev:amd64 libgl1:amd64 libgl1-mesa-dri:amd64 libglapi-mesa:amd64 libglvnd-core-dev:amd64 libglvnd0:amd64 libglx-mesa0:amd64 libglx0:amd64 libgomp1:amd64 libicu-dev:amd64 libisl23:amd64 libitm1:amd64 libllvm14:amd64 libllvm${GITHUB_CLANG_VERSION}:amd64 libllvmspirvlib-${GITHUB_CLANG_VERSION}-dev:amd64 libllvmspirvlib${GITHUB_CLANG_VERSION}:amd64 liblsan0:amd64 libmpc3:amd64 libncurses-dev:amd64 libnsl-dev:amd64 libobjc-11-dev:amd64 libobjc4:amd64 libpciaccess-dev:amd64 libpciaccess0f:amd64 libpfm4:amd64 libpthread-stubs0-dev:amd64 libquadmath0:amd64 libsensors-config libsensors-dev:amd64 libsensors5:amd64 libset-scalar-perl libstd-rust-1.75:amd64 libstd-rust-dev:amd64 libstdc++-11-dev:amd64 libsub-override-perl libtinfo-dev:amd64 libtirpc-dev:amd64 libtool libtsan0:amd64 libubsan1:amd64 libva-dev:amd64 libva-drm2:amd64 libva-glx2:amd64 libva-wayland2:amd64 libva-x11-2:amd64 libva2:amd64 libvdpau-dev:amd64 libvdpau1:amd64 libvulkan-dev:amd64 libvulkan1:amd64 libwayland-bin libwayland-client0:amd64 libwayland-cursor0:amd64 libwayland-dev:amd64 libwayland-egl-backend-dev:amd64 libwayland-egl1:amd64 libwayland-server0:amd64 libx11-dev:amd64 libx11-xcb-dev:amd64 libx11-xcb1:amd64 libxau-dev:amd64 libxcb-dri2-0:amd64 libxcb-dri2-0-dev:amd64 libxcb-dri3-0:amd64 libxcb-dri3-dev:amd64 libxcb-glx0:amd64 libxcb-glx0-dev:amd64 libxcb-present-dev:amd64 libxcb-present0:amd64 libxcb-randr0:amd64 libxcb-randr0-dev:amd64 libxcb-render0:amd64 libxcb-render0-dev:amd64 libxcb-shape0:amd64 libxcb-shape0-dev:amd64 libxcb-shm0:amd64 libxcb-shm0-dev:amd64 libxcb-sync-dev:amd64 libxcb-sync1:amd64 libxcb-xfixes0:amd64 libxcb-xfixes0-dev:amd64 libxcb1-dev:amd64 libxdmcp-dev:amd64 libxext-dev:amd64 libxfixes-dev:amd64 libxfixes3:amd64 libxml2-dev:amd64 libxrandr-dev:amd64 libxrandr2:amd64 libxrender-dev:amd64 libxrender1:amd64 libxshmfence-dev:amd64 libxshmfence1:amd64 libxxf86vm-dev:amd64 libxxf86vm1:amd64 libz3-4:amd64 libz3-dev:amd64 libzstd-dev:amd64 linux-libc-dev:amd64 llvm-${LLVM_VERSION} llvm-${LLVM_VERSION}-dev llvm-${LLVM_VERSION}-linker-tools llvm-${LLVM_VERSION}-runtime llvm-${LLVM_VERSION}-tools llvm-spirv-${LLVM_VERSION} lto-disabled-list m4 make meson ninja-build pkg-config po-debconf python3-mako python3-ply python3-pygments quilt rpcsvc-proto rustc spirv-tools valgrind wayland-protocols x11proto-dev xorg-sgml-doctools xtrans-dev zlib1g-dev:amd64 \
autoconf automake autopoint autotools-dev bindgen bison build-essential bzip2 cpp cpp-11 debhelper debugedit dh-autoreconf dh-strip-nondeterminism diffstat directx-headers-dev dpkg-dev dwz flex g++ g++-11 gcc gcc-11 gcc-11-base:${DPKG_ARCH} gettext glslang-tools icu-devtools intltool-debian ${EXTRA_PACKAGES} libarchive-zip-perl libasan6:${DPKG_ARCH} libatomic1:${DPKG_ARCH} libc-dev-bin libc6-dbg:${DPKG_ARCH} libc6-dev:${DPKG_ARCH} libcc1-0:${DPKG_ARCH} libclang-${GITHUB_CLANG_VERSION}-dev libclang-common-${GITHUB_CLANG_VERSION}-dev libclang-cpp${GITHUB_CLANG_VERSION} libclang-cpp${GITHUB_CLANG_VERSION}-dev libclang1-14 libclang1-${GITHUB_CLANG_VERSION} libclc-${GITHUB_CLANG_VERSION} libclc-${GITHUB_CLANG_VERSION}-dev libcrypt-dev:${DPKG_ARCH} libdebhelper-perl libdpkg-perl libdrm-amdgpu1:${DPKG_ARCH} libdrm-dev:${DPKG_ARCH} libdrm-intel1:${DPKG_ARCH} libdrm-nouveau2:${DPKG_ARCH} libdrm-radeon1:${DPKG_ARCH} libelf-dev:${DPKG_ARCH} libexpat1-dev:${DPKG_ARCH} libffi-dev:${DPKG_ARCH} libfile-stripnondeterminism-perl libgc1:${DPKG_ARCH} libgcc-11-dev:${DPKG_ARCH} libgl1:${DPKG_ARCH} libgl1-mesa-dri:${DPKG_ARCH} libglapi-mesa:${DPKG_ARCH} libglvnd-core-dev:${DPKG_ARCH} libglvnd0:${DPKG_ARCH} libglx-mesa0:${DPKG_ARCH} libglx0:${DPKG_ARCH} libgomp1:${DPKG_ARCH} libicu-dev:${DPKG_ARCH} libisl23:${DPKG_ARCH} libitm1:${DPKG_ARCH} libllvm14:${DPKG_ARCH} libllvm${GITHUB_CLANG_VERSION}:${DPKG_ARCH} libllvmspirvlib-${GITHUB_CLANG_VERSION}-dev:${DPKG_ARCH} libllvmspirvlib${GITHUB_CLANG_VERSION}:${DPKG_ARCH} liblsan0:${DPKG_ARCH} libmpc3:${DPKG_ARCH} libncurses-dev:${DPKG_ARCH} libnsl-dev:${DPKG_ARCH} libobjc-11-dev:${DPKG_ARCH} libobjc4:${DPKG_ARCH} libpciaccess-dev:${DPKG_ARCH} libpciaccess0f:${DPKG_ARCH} libpfm4:${DPKG_ARCH} libpthread-stubs0-dev:${DPKG_ARCH} libquadmath0:${DPKG_ARCH} libsensors-config libsensors-dev:${DPKG_ARCH} libsensors5:${DPKG_ARCH} libset-scalar-perl libstd-rust-1.75:${DPKG_ARCH} libstd-rust-dev:${DPKG_ARCH} libstdc++-11-dev:${DPKG_ARCH} libsub-override-perl libtinfo-dev:${DPKG_ARCH} libtirpc-dev:${DPKG_ARCH} libtool libtsan0:${DPKG_ARCH} libubsan1:${DPKG_ARCH} libva-dev:${DPKG_ARCH} libva-drm2:${DPKG_ARCH} libva-glx2:${DPKG_ARCH} libva-wayland2:${DPKG_ARCH} libva-x11-2:${DPKG_ARCH} libva2:${DPKG_ARCH} libvdpau-dev:${DPKG_ARCH} libvdpau1:${DPKG_ARCH} libvulkan-dev:${DPKG_ARCH} libvulkan1:${DPKG_ARCH} libwayland-bin libwayland-client0:${DPKG_ARCH} libwayland-cursor0:${DPKG_ARCH} libwayland-dev:${DPKG_ARCH} libwayland-egl-backend-dev:${DPKG_ARCH} libwayland-egl1:${DPKG_ARCH} libwayland-server0:${DPKG_ARCH} libx11-dev:${DPKG_ARCH} libx11-xcb-dev:${DPKG_ARCH} libx11-xcb1:${DPKG_ARCH} libxau-dev:${DPKG_ARCH} libxcb-dri2-0:${DPKG_ARCH} libxcb-dri2-0-dev:${DPKG_ARCH} libxcb-dri3-0:${DPKG_ARCH} libxcb-dri3-dev:${DPKG_ARCH} libxcb-glx0:${DPKG_ARCH} libxcb-glx0-dev:${DPKG_ARCH} libxcb-present-dev:${DPKG_ARCH} libxcb-present0:${DPKG_ARCH} libxcb-randr0:${DPKG_ARCH} libxcb-randr0-dev:${DPKG_ARCH} libxcb-render0:${DPKG_ARCH} libxcb-render0-dev:${DPKG_ARCH} libxcb-shape0:${DPKG_ARCH} libxcb-shape0-dev:${DPKG_ARCH} libxcb-shm0:${DPKG_ARCH} libxcb-shm0-dev:${DPKG_ARCH} libxcb-sync-dev:${DPKG_ARCH} libxcb-sync1:${DPKG_ARCH} libxcb-xfixes0:${DPKG_ARCH} libxcb-xfixes0-dev:${DPKG_ARCH} libxcb1-dev:${DPKG_ARCH} libxdmcp-dev:${DPKG_ARCH} libxext-dev:${DPKG_ARCH} libxfixes-dev:${DPKG_ARCH} libxfixes3:${DPKG_ARCH} libxml2-dev:${DPKG_ARCH} libxrandr-dev:${DPKG_ARCH} libxrandr2:${DPKG_ARCH} libxrender-dev:${DPKG_ARCH} libxrender1:${DPKG_ARCH} libxshmfence-dev:${DPKG_ARCH} libxshmfence1:${DPKG_ARCH} libxxf86vm-dev:${DPKG_ARCH} libxxf86vm1:${DPKG_ARCH} libz3-4:${DPKG_ARCH} libz3-dev:${DPKG_ARCH} libzstd-dev:${DPKG_ARCH} linux-libc-dev:${DPKG_ARCH} llvm-${LLVM_VERSION} llvm-${LLVM_VERSION}-dev llvm-${LLVM_VERSION}-linker-tools llvm-${LLVM_VERSION}-runtime llvm-${LLVM_VERSION}-tools llvm-spirv-${LLVM_VERSION} lto-disabled-list m4 make meson ninja-build pkg-config po-debconf python3-mako python3-ply python3-pygments quilt rpcsvc-proto rustc spirv-tools valgrind wayland-protocols x11proto-dev xorg-sgml-doctools xtrans-dev zlib1g-dev:${DPKG_ARCH} \
clang-$GITHUB_CLANG_VERSION libc++-$GITHUB_CLANG_VERSION-dev libc++abi-$GITHUB_CLANG_VERSION-dev
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-${GITHUB_CLANG_VERSION} 100
@@ -80,6 +85,7 @@ elif [[ "$OS_NAME" == "Darwin" ]]; then
if command -v brew > /dev/null 2>&1; then
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=true brew install autoconf automake libx11 libxext libxrandr \
llvm@${LLVM_VERSION} ninja meson pkg-config libxshmfence
brew link --overwrite llvm@${LLVM_VERSION}
# For reasons unknown, this is necessary for pkg-config to find homebrew's packages
LOCAL_PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig:$PKG_CONFIG_PATH"
elif command -v port > /dev/null 2>&1; then

View File

@@ -63,6 +63,10 @@ function _preferred_os_filename() {
function download_vulkan_installer() {
local os=$(_get_os)
if [[ "$os" == "linux" ]]; then
echo "Linux uses apt to install vulkan dependencies, skipping tarball download." >&2
return 0
fi
local dl_filename=$(_os_filename ${VULKAN_SDK_VERSION})
local filename=$(_preferred_os_filename)
local url=https://sdk.lunarg.com/sdk/download/$VULKAN_SDK_VERSION/$os/$dl_filename?Human=true
@@ -79,15 +83,22 @@ function download_vulkan_installer() {
function unpack_vulkan_installer() {
local os=$(_get_os)
if [[ "$os" == "linux" ]]; then
install_${os}
return 0
fi
local filename=$(_preferred_os_filename $os)
test -f $filename
install_${os}
}
function install_linux() {
test -d $VULKAN_SDK_DIR && test -f vulkan_sdk.tar.gz
echo "extract just the SDK's prebuilt binaries ($VULKAN_SDK_VERSION/x86_64) from vulkan_sdk.tar.gz into $VULKAN_SDK" >&2
tar -C "$VULKAN_SDK_DIR" --strip-components 2 -xf vulkan_sdk.tar.gz $VULKAN_SDK_VERSION/x86_64
echo "Installing Vulkan dependencies via apt for Linux..." >&2
sudo apt-get update -y
sudo apt-get install -y libvulkan-dev vulkan-validationlayers glslang-tools spirv-tools
# Create a dummy SDK dir to satisfy scripts expecting a VulkanSDK folder structure
mkdir -p ~/VulkanSDK/${VULKAN_SDK_VERSION}
}
function install_mac() {

View File

@@ -235,9 +235,9 @@
"license": "MIT"
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -429,9 +429,9 @@
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"requires": {
"brace-expansion": "^1.1.7"
}

View File

@@ -46,6 +46,7 @@ list(SORT NDK_VERSIONS)
list(GET NDK_VERSIONS -1 NDK_VERSION)
get_filename_component(NDK_VERSION ${NDK_VERSION} NAME)
set(TOOLCHAIN ${ANDROID_HOME_UNIX}/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/${HOST_NAME_L}-x86_64)
set(CMAKE_ANDROID_NDK_VERSION ${NDK_VERSION})
# specify the cross compiler
set(COMPILER_SUFFIX)

View File

@@ -47,6 +47,7 @@ list(SORT NDK_VERSIONS)
list(GET NDK_VERSIONS -1 NDK_VERSION)
get_filename_component(NDK_VERSION ${NDK_VERSION} NAME)
set(TOOLCHAIN ${ANDROID_HOME_UNIX}/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/${HOST_NAME_L}-x86_64)
set(CMAKE_ANDROID_NDK_VERSION ${NDK_VERSION})
# specify the cross compiler
set(COMPILER_SUFFIX)

View File

@@ -46,6 +46,7 @@ list(SORT NDK_VERSIONS)
list(GET NDK_VERSIONS -1 NDK_VERSION)
get_filename_component(NDK_VERSION ${NDK_VERSION} NAME)
set(TOOLCHAIN ${ANDROID_HOME_UNIX}/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/${HOST_NAME_L}-x86_64)
set(CMAKE_ANDROID_NDK_VERSION ${NDK_VERSION})
# specify the cross compiler
set(COMPILER_SUFFIX)

View File

@@ -46,6 +46,7 @@ list(SORT NDK_VERSIONS)
list(GET NDK_VERSIONS -1 NDK_VERSION)
get_filename_component(NDK_VERSION ${NDK_VERSION} NAME)
set(TOOLCHAIN ${ANDROID_HOME_UNIX}/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/${HOST_NAME_L}-x86_64)
set(CMAKE_ANDROID_NDK_VERSION ${NDK_VERSION})
# specify the cross compiler
set(COMPILER_SUFFIX)

View File

@@ -181,22 +181,21 @@ important for <code>matc</code> (material compiler).</p>
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.69.0'
implementation 'com.google.android.filament:filament-android:1.70.0'
}
</code></pre>
<p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p>
<div class="table-wrapper"><table><thead><tr><th>Artifact</th><th>Description</th></tr></thead><tbody>
<tr><td><a href="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android"><img src="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android/badge.svg?subject=filament-android" alt="filament-android" /></a></td><td>The Filament rendering engine itself.</td></tr>
<tr><td><a href="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android-debug"><img src="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-android-debug/badge.svg?subject=filament-android-debug" alt="filament-android-debug" /></a></td><td>Debug version of <code>filament-android</code>.</td></tr>
<tr><td><a href="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/gltfio-android"><img src="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/gltfio-android/badge.svg?subject=gltfio-android" alt="gltfio-android" /></a></td><td>A glTF 2.0 loader for Filament, depends on <code>filament-android</code>.</td></tr>
<tr><td><a href="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-utils-android"><img src="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filament-utils-android/badge.svg?subject=filament-utils-android" alt="filament-utils-android" /></a></td><td>KTX loading, Kotlin math, and camera utilities, depends on <code>gltfio-android</code>.</td></tr>
<tr><td><a href="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android"><img src="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android/badge.svg?subject=filamat-android" alt="filamat-android" /></a></td><td>A runtime material builder/compiler. This library is large but contains a full shader compiler/validator/optimizer and supports both OpenGL and Vulkan.</td></tr>
<tr><td><a href="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android-lite"><img src="https://maven-badges.herokuapp.com/maven-central/com.google.android.filament/filamat-android-lite/badge.svg?subject=filamat-android-lite" alt="filamat-android-lite" /></a></td><td>A much smaller alternative to <code>filamat-android</code> that can only generate OpenGL shaders. It does not provide validation or optimizations.</td></tr>
<tr><td><a href="https://mvnrepository.com/artifact/com.google.android.filament/filament-android"><img src="https://img.shields.io/maven-central/v/com.google.android.filament/filament-android?label=filament-android&amp;color=green" alt="filament-android" /></a></td><td>The Filament rendering engine itself.</td></tr>
<tr><td><a href="https://mvnrepository.com/artifact/com.google.android.filament/filament-android-debug"><img src="https://img.shields.io/maven-central/v/com.google.android.filament/filament-android-debug?label=filament-android-debug&amp;color=green" alt="filament-android-debug" /></a></td><td>Debug version of <code>filament-android</code>.</td></tr>
<tr><td><a href="https://mvnrepository.com/artifact/com.google.android.filament/gltfio-android"><img src="https://img.shields.io/maven-central/v/com.google.android.filament/gltfio-android?label=gltfio-android&amp;color=green" alt="gltfio-android" /></a></td><td>A glTF 2.0 loader for Filament, depends on <code>filament-android</code>.</td></tr>
<tr><td><a href="https://mvnrepository.com/artifact/com.google.android.filament/filament-utils-android"><img src="https://img.shields.io/maven-central/v/com.google.android.filament/filament-utils-android?label=filament-utils-android&amp;color=green" alt="filament-utils-android" /></a></td><td>KTX loading, Kotlin math, and camera utilities, depends on <code>gltfio-android</code>.</td></tr>
<tr><td><a href="https://mvnrepository.com/artifact/com.google.android.filament/filamat-android"><img src="https://img.shields.io/maven-central/v/com.google.android.filament/filamat-android?label=filamat-android&amp;color=green" alt="filamat-android" /></a></td><td>A runtime material builder/compiler. This library is large but contains a full shader compiler/validator/optimizer and supports both OpenGL and Vulkan.</td></tr>
</tbody></table>
</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.69.0'
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.70.0'
</code></pre>
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
<ul>
@@ -230,7 +229,8 @@ sheet for the standard material model.</li>
<li>OpenGL ES 3.0+ for Android and iOS</li>
<li>Metal for macOS and iOS</li>
<li>Vulkan 1.0 for Android, Linux, macOS, and Windows</li>
<li>WebGL 2.0 for all platforms</li>
<li>WebGPU for Android, Linux, macOS, and Windows</li>
<li>WebGL 2.0 for all browsers supporting it</li>
</ul>
<h3 id="rendering"><a class="header" href="#rendering">Rendering</a></h3>
<ul>
@@ -265,7 +265,7 @@ sheet for the standard material model.</li>
<ul>
<li>HDR bloom</li>
<li>Depth of field bokeh</li>
<li>Multiple tone mappers: generic (customizable), ACES, filmic, etc.</li>
<li>Multiple tone mappers: PBR Neutral, AgX, generic (customizable), ACES, filmic, etc.</li>
<li>Color and tone management: luminance scaling, gamut mapping</li>
<li>Color grading: exposure, night adaptation, white balance, channel mixer,
shadows/mid-tones/highlights, ASC CDL, contrast, saturation, etc.</li>
@@ -332,6 +332,8 @@ KHR_lights_punctual</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_clearcoat</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_dispersion</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_emissive_strength</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_ior</li>
@@ -340,6 +342,8 @@ KHR_materials_pbrSpecularGlossiness</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_sheen</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_specular</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_transmission</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_unlit</li>
@@ -348,8 +352,6 @@ KHR_materials_variants</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_volume</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_materials_specular</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_mesh_quantization</li>
<li><input disabled="" type="checkbox" checked=""/>
KHR_texture_basisu</li>
@@ -501,7 +503,7 @@ and tools.</p>
<li><code>filamesh</code>: Mesh converter</li>
<li><code>glslminifier</code>: Minifies GLSL source code</li>
<li><code>matc</code>: Material compiler</li>
<li><code>filament-matp</code>: Material parser</li>
<li><code>matedit</code>: Material editor for compiled materials</li>
<li><code>matinfo</code> Displays information about materials compiled with <code>matc</code></li>
<li><code>mipgen</code> Generates a series of miplevels from a source image</li>
<li><code>normal-blending</code>: Tool to blend normal maps</li>

View File

@@ -225,9 +225,10 @@ into <strong>branch</strong> of <code>filament-assets</code>. This branch is pai
<code>filament</code> repo.</p>
<p>As an example, imagine I am working on a PR, and I've uploaded my change, which is in a
branch called <code>my-pr-branch</code>, to <code>filament</code>. This PR requires updating the golden. We would do
it in the following fashion</p>
it in the following fashion on a macOS machine:</p>
<h3 id="using-a-script-to-update-the-golden-repo"><a class="header" href="#using-a-script-to-update-the-golden-repo">Using a script to update the golden repo</a></h3>
<ul>
<li>Make sure you've completed the steps in 'Setting up python'</li>
<li>Run interactive mode in the <code>update_golden.py</code> script.
<pre><code>python3 test/renderdiff/src/update_golden.py
</code></pre>
@@ -263,6 +264,27 @@ branch of the golden repo (i.e. <code>my-pr-branch-golden</code>).</li>
<li>If the PR is merged, then there is another workflow that will merge <code>my-pr-branch-golden</code>
to the <code>main</code> branch of the golden repo.</li>
</ul>
<h3 id="automated-update-via-commit-message"><a class="header" href="#automated-update-via-commit-message">Automated update via commit message</a></h3>
<p>Alternatively, if you are confident in your changes and want the CI to handle the update
for you, you can use the following tag in your commit message:</p>
<ul>
<li>In the commit message of your working branch on <code>filament</code>, add the following line:
<pre><code>RDIFF_ACCEPT_NEW_GOLDENS
</code></pre>
</li>
</ul>
<p>This has the following effects:</p>
<ul>
<li>The presubmit test <code>test-renderdiff</code> will be bypassed (it will not perform rendering or
comparison).</li>
<li>When the PR is merged, the postsubmit CI will automatically:
<ol>
<li>Build Filament and generate the new images.</li>
<li>Upload them to a temporary branch in <code>filament-assets</code>.</li>
<li>Merge that branch into <code>main</code>.</li>
</ol>
</li>
</ul>
<h2 id="viewing-test-results"><a class="header" href="#viewing-test-results">Viewing test results</a></h2>
<p>We provide a viewer for looking at the result of a test run. The viewer is a webapp that can
be used by pointing your browser to a localhost port. If you input the viewer with a PR or a

View File

@@ -195,6 +195,30 @@ for inspection.</li>
<p><strong>Tip:</strong> To view all defined subspecs, grep the podspec file:
<code>grep "spec.subspec" ios/CocoaPods/Filament.podspec</code></p>
</blockquote>
<h2 id="testing-a-podspec-locally"><a class="header" href="#testing-a-podspec-locally">Testing a Podspec Locally</a></h2>
<p>Before pushing a new version to the CocoaPods trunk, you should verify the podspec in a sample project.</p>
<h3 id="using-the-sample-project"><a class="header" href="#using-the-sample-project">Using the Sample Project</a></h3>
<p>Filament includes a dedicated sample project located in <code>ios/samples/HelloCocoaPods</code>. To test your
local changes:</p>
<ol>
<li><strong>Modify the Podfile</strong>: Add a <code>:podspec</code> directive pointing to your local <code>.podspec</code> file.</li>
</ol>
<pre><code class="language-ruby">platform :ios, '13.0'
target 'HelloCocoaPods' do
# Use the local development podspec
pod 'Filament', :podspec =&gt; '../../CocoaPods/Filament.podspec'
end
</code></pre>
<ol start="2">
<li><strong>Sync Dependencies</strong>: Run the installation command from the sample directory:</li>
</ol>
<pre><code class="language-bash">pod install
</code></pre>
<ol start="3">
<li><strong>Verify the Build</strong>: Open the generated <code>HelloCocoaPods.xcworkspace</code> in Xcode. Build and run the
target to ensure the headers are found and the binaries link correctly.</li>
</ol>
</main>

View File

@@ -199,17 +199,21 @@ Sonatype's <a href="https://central.sonatype.org/">Central Publisher Portal</a>.
<p>The new Central Publisher Portal does not officially support Gradle. However, Sonatype provides a
staging API compatibility service, which works with Filament's Gradle setup.</p>
<hr />
<h3 id="1-upload-to-the-staging-api-compatibility-service"><a class="header" href="#1-upload-to-the-staging-api-compatibility-service">1. Upload to the Staging API Compatibility Service</a></h3>
<h3 id="1-upload-to-the-central-publisher-portal"><a class="header" href="#1-upload-to-the-central-publisher-portal">1. Upload to the Central Publisher Portal</a></h3>
<p>To upload the artifacts, it is important to run both of these Gradle tasks together in a single
command. This ensures the staging repository is created and closed automatically.</p>
<pre><code class="language-bash">cd android
./gradlew publishToSonatype
./gradlew publishToSonatype closeSonatypeStagingRepository
</code></pre>
<h3 id="2-move-the-repository-to-the-central-publisher-portal"><a class="header" href="#2-move-the-repository-to-the-central-publisher-portal">2. Move the Repository to the Central Publisher Portal</a></h3>
<p>We have a script to automate this. It reads the <code>sonatypeUsername</code> and <code>sonatypePassword</code> from your
<code>~/.gradle/gradle.properties</code> file.</p>
<h4 id="troubleshooting-manual-staging"><a class="header" href="#troubleshooting-manual-staging">Troubleshooting: Manual Staging</a></h4>
<p>If you ran <code>publishToSonatype</code> by itself, the repository will remain open and won't appear in the
portal correctly. You can fix this by running our automation script, which uses the
<code>sonatypeUsername</code> and <code>sonatypePassword</code> from your ~/.gradle/gradle.properties file:</p>
<pre><code class="language-bash">python3 build/common/close-sonatype-staging-repository.py
</code></pre>
<h3 id="3-publish-the-release-on-sonatype"><a class="header" href="#3-publish-the-release-on-sonatype">3. Publish the Release on Sonatype</a></h3>
<p>Navigate to <a href="https://central.sonatype.com/publishing/deployments">Maven Central Repository Deployments</a>.</p>
<h3 id="2-publish-the-release-on-sonatype"><a class="header" href="#2-publish-the-release-on-sonatype">2. Publish the Release on Sonatype</a></h3>
<p>Once the upload is successful, you must manually trigger the final release. Navigate to <a href="https://central.sonatype.com/publishing/deployments">Maven
Central Repository Deployments</a>.</p>
<p>Here, you should see a new deployment with a <strong>Validated</strong> status and all your artifacts listed. Click
the <strong>Publish</strong> button to publish the artifacts. It typically takes around 5 minutes after clicking
<strong>Publish</strong> for the artifacts to go live.</p>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -16,12 +16,13 @@ class SimulatedSkybox {
this.ozone = 0.0;
this.msFactors = [0.1, 0.5, 0.0];
this.contrast = 1.0;
this.nightColor = [0.0, 0.0003, 0.00075];
this.nightColor = [0.0, 3.0e-9, 7.5e-9];
this.shimmerControl = [0.0, 20.0, 0.1];
this.cloudControl = [0.0, 0.1, 8000.0, 0.0];
this.cloudControl2 = [0.0, 0.0, 0.0, 0.0];
this.waterControl = [50.0, 1.0, 1.0, 4.0]; // x=Strength, y=Speed, z=DerivativeTrick, w=Octaves
this.starControl = [0.001, 1.0, 350.0, 0.01]; // x=Density, y=Enabled, z=Frequency, w=PixelScale
this.starIntensity = 1.0;
this.focalLength = 24.0;
this.height = 1000.0;
this.planetRadius = 6360.0;
@@ -43,7 +44,7 @@ class SimulatedSkybox {
// Milky Way Parameters
// x=Intensity, y=Saturation, z=Unused
this.milkyWayControl = [1.0, 1.0, 0.07];
this.milkyWayControl = [1.0, 1.2, 0.05];
this.milkyWayEnabled = true;
this.milkyWayRotation = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Identity by default
this.initEntity();
@@ -453,6 +454,11 @@ class SimulatedSkybox {
this.updateCoefficients();
}
setStarIntensity(intensity) {
this.starIntensity = Math.max(0.0, intensity);
this.updateCoefficients();
}
setMoonPosition(direction) {
// normalize
const len = Math.hypot(direction[0], direction[1], direction[2]);
@@ -500,6 +506,13 @@ class SimulatedSkybox {
}
}
setExposure(exposure) {
this.exposure = exposure;
this.updateCoefficients();
}
updateCoefficients() {
if (!this.materialInstance) {
console.warn("updateCoefficients called before material loaded");
@@ -585,6 +598,7 @@ class SimulatedSkybox {
this.materialInstance.setFloat4Parameter('cloudControl2', new Float32Array(this.cloudControl2));
this.materialInstance.setFloat4Parameter('waterControl', new Float32Array(this.waterControl));
this.materialInstance.setFloat4Parameter('starControl', new Float32Array(this.starControl));
this.materialInstance.setFloatParameter('starIntensity', this.starIntensity);
this.materialInstance.setFloatParameter('sunIntensity', physicalSunIntensity);
@@ -615,12 +629,13 @@ class SimulatedSkybox {
this.materialInstance.setFloatParameter('sunIntensity2', finalMoonIntensity);
// Scale Milky Way by Sun Intensity (Pre-exposed) to match dynamic range
// Calibration: 1.0 User Intensity ~ 0.025 Lux Relative (approx 2.5% of Sun Pixel Value at Day)
// At Sunny 16 (Sun ~ 2.6), this gives ~0.065 pixel brightness, which is visible but dim.
// Scale Milky Way by Sun Intensity
// Calibration: 1.0 User Intensity = 1.5e-3 cd/m^2 (Nits) approx.
// Target: 1.5e-3 Nits. SunIntensity = 100,000.
// Scale = 1.5e-3 / 1.0e5 = 1.5e-8.
const mwIntensity = this.milkyWayEnabled ? this.milkyWayControl[0] : 0.0;
const mwUniform = [
mwIntensity * this.sunIntensity * 0.025,
mwIntensity * this.sunIntensity * 1.5e-8,
this.milkyWayControl[1],
this.milkyWayControl[2]
];
@@ -634,6 +649,7 @@ class SimulatedSkybox {
const moonHaloUpload = [...this.moonHalo];
moonHaloUpload[2] *= moonRadConv;
this.materialInstance.setFloat4Parameter('sunHalo2', new Float32Array(moonHaloUpload));
this.materialInstance.setFloatParameter('exposure', this.exposure !== undefined ? this.exposure : 1.0);
// Solar Eclipse (CPU Calculation)
const sunRadius = Math.acos(this.sunHalo[0]);

View File

@@ -218,6 +218,7 @@ class App {
const exposure = this.getExposure();
const preExposedIntensity = this.params.sunIntensity * exposure;
this.skybox.setSunIntensity(preExposedIntensity);
this.skybox.setExposure(exposure); // Update Skybox Exposure Uniform
// Moon Exposure
if (this.mParams) {
@@ -389,7 +390,7 @@ class App {
};
mwFolder.add(this.mwParams, 'enabled').name('Enabled').onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 5.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 100.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'saturation', 0.0, 2.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'blackPoint', 0.0, 0.5).name('Black Point').onChange(updateMW);
mwFolder.add(this.mwParams, 'siderealTime', 0.0, 24.0).name('Sidereal Time').listen().onChange(updateMW);
@@ -464,18 +465,23 @@ class App {
const starFolder = gui.addFolder('Stars');
this.sParams = {
enabled: true,
density: 0.001
density: 0.001,
intensity: 0.0 // 2^0 = 1.0
};
// Initialize defaults (Density 0.001, Enabled True)
sky.setStarControl(0.001, true);
sky.setStarIntensity(Math.pow(2.0, 0.0));
const updateStars = () => {
sky.setStarControl(this.sParams.density, this.sParams.enabled);
sky.setStarIntensity(Math.pow(2.0, this.sParams.intensity));
};
starFolder.add(this.sParams, 'enabled').name('Enabled').onChange(updateStars);
starFolder.add(this.sParams, 'density', 0.0, 0.01, 0.0001).name('Density').onChange(updateStars);
starFolder.add(this.sParams, 'intensity', 0.0, 24.0).name('Intensity (Exp)').onChange(updateStars);
starFolder.close();
this.updateStars = updateStars;
const artFolder = gui.addFolder('Artistic');
// Set Horizon Glow default to 0.0
@@ -489,7 +495,7 @@ class App {
artFolder.add(sky.msFactors, 2, 0.0, 1.0).name('Horizon Glow').onChange(v => sky.setHorizonGlow(v));
artFolder.add(sky, 'contrast', 0.1, 2.0).onChange(v => sky.setContrast(v));
artFolder.addColor(sky, 'nightColor').onChange(v => sky.setNightColor(v));
const shmFolder = artFolder.addFolder('Shimmer');
// Set Shimmer Strength default to 0.0
@@ -502,7 +508,7 @@ class App {
const camFolder = gui.addFolder('Camera');
camFolder.add(this.params, 'focalLength', 8.0, 300.0).name('Focal Length').onChange(() => this.updateCameraProjection());
camFolder.add(this.params, 'aperture', 1.4, 32.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'shutterSpeed', 1.0, 1000.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'shutterSpeed', 0.05, 1000.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'iso', 50.0, 3200.0).onChange(() => this.updateCameraExposure());
const bloomFolder = camFolder.addFolder('Bloom');
@@ -692,13 +698,15 @@ class App {
const s = this.sParams;
const b = this.bParams;
const m = this.mParams;
const mw = this.mwParams;
const sk = this.skybox;
return {
p: { a: p.aperture, ss: p.shutterSpeed, i: p.iso, st: p.sunTheta, sp: p.sunPhi, fl: p.focalLength, si: p.sunIntensity },
c: { v: c.volumetrics, co: c.coverage, d: c.density, h: c.height, s: c.speed, e: c.evolution },
w: { dt: w.derivativeTrick, st: w.strength, s: w.speed, o: w.octaves },
s: { e: s.enabled, d: s.density },
s: { e: s.enabled, d: s.density, i: s.intensity },
mw: { e: mw.enabled, i: mw.intensity, s: mw.saturation, bp: mw.blackPoint, st: mw.siderealTime, l: mw.latitude },
b: { e: b.enabled, lf: b.lensFlare },
m: { e: m.enabled, az: m.azimuth, h: m.height, r: m.radius, i: m.intensity },
cm: { t: this.camState.theta, p: this.camState.phi },
@@ -723,6 +731,7 @@ class App {
const c = state.c;
const w = state.w;
const s = state.s;
const mw = state.mw;
const b = state.b;
const m = state.m;
const k = state.k;
@@ -757,6 +766,18 @@ class App {
if (s) {
if (s.e !== undefined) this.sParams.enabled = s.e;
if (s.d !== undefined) this.sParams.density = s.d;
if (s.i !== undefined) this.sParams.intensity = s.i;
if (this.updateStars) this.updateStars();
}
if (mw) {
if (mw.e !== undefined) this.mwParams.enabled = mw.e;
if (mw.i !== undefined) this.mwParams.intensity = mw.i;
if (mw.s !== undefined) this.mwParams.saturation = mw.s;
if (mw.bp !== undefined) this.mwParams.blackPoint = mw.bp;
if (mw.st !== undefined) this.mwParams.siderealTime = mw.st;
if (mw.l !== undefined) this.mwParams.latitude = mw.l;
if (this.updateMW) this.updateMW();
}
if (b) {

View File

@@ -96,10 +96,20 @@ material {
name : starControl, // x=Density, y=Enabled, z=Frequency, w=PixelScale
precision : high
},
{
type : float,
name : starIntensity,
precision : high
},
{
type : sampler2d,
name : moonTexture
},
{
type : float,
name : exposure,
precision : high
},
{
type : sampler2d,
name : moonNormal
@@ -162,9 +172,9 @@ fragment {
// --- CONFIGURATION ---
// Stars
#define STAR_GLOBAL_INTENSITY 150.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.5 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 4.0 // Random brightness variance range [0.0 - 10.0]
#define STAR_GLOBAL_INTENSITY 100.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.01 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 15.0 // Random brightness variance range [0.0 - 10.0]
#define STAR_FADE_SUN_ELV_HIGH 0.10 // Sun elevation (sin) where stars are fully hidden [0.0 - 0.5]
#define STAR_FADE_SUN_ELV_LOW -0.20 // Sun elevation (sin) where stars are fully visible [-0.5 - 0.0]
#define STAR_CLOUD_OCCLUSION 0.1 // Visibility when covered by clouds [0.0 - 1.0]
@@ -961,7 +971,11 @@ fragment {
highp vec4 sunHalo, highp vec4 cloudControl, highp vec4 cloudControl2,
highp vec4 shimmerControl, highp vec4 waterControl,
highp vec3 L2, highp float sunIntensity2, highp vec4 sunHalo2,
sampler2D moonTex, sampler2D moonNormal) {
sampler2D moonTex, sampler2D moonNormal,
highp vec3 nightColor,
highp mat3 milkyWayRotation,
highp vec3 milkyWayControl,
sampler2D milkyWayTexture) {
// Project to plane y=0
highp float t = WATER_PLANE_HEIGHT / min(V.y, -0.0002); // Reduced clamp to minimize "wall" artifact
@@ -1057,7 +1071,7 @@ fragment {
highp float rHorizonMask = 1.0 - smoothstep(0.0, REFLECTION_HORIZON_FADE, R.y);
if (rHorizonMask > 0.0) {
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask;
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask * materialParams.exposure;
}
// Sun Disk Reflection
@@ -1088,6 +1102,29 @@ fragment {
// Apply clouds to reflection
reflection = mix(reflection, reflCloudLayer, reflCloudDensity);
// Add Milky Way to Reflection
// Calculate Fade based on Sun Elevation
highp float sunElvSin = L.y;
highp float mwFade = smoothstep(STAR_FADE_SUN_ELV_HIGH, STAR_FADE_SUN_ELV_LOW, sunElvSin);
if (mwFade > 0.0) {
highp vec3 mwColor = getMilkyWay(R, milkyWayRotation, milkyWayTexture, milkyWayControl);
// Apply Atmosphere Transmittance (approximate)
mwColor *= outTransmittance;
// Apply Fades
mwColor *= mwFade;
mwColor *= (1.0 - reflMoonOcclusion); // Occlude by Moon
mwColor *= (1.0 - reflCloudDensity); // Occlude by Clouds
reflection += mwColor;
}
// Add Night Color to Reflection
reflection += nightColor;
// Fresnel
highp float F0 = WATER_FRESNEL_F0; // Water
highp float cosTheta = clamp(dot(-V, N_water), 0.0, 1.0);
@@ -1187,7 +1224,7 @@ fragment {
// 7. Stars
// Add stars before clouds (clouds cover stars)
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl);
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl) * materialParams.exposure * materialParams.starIntensity;
// 7b. Milky Way
// Add Milky Way behind stars (conceptually) but handled similarly
@@ -1232,7 +1269,11 @@ fragment {
materialParams.shimmerControl,
materialParams.waterControl,
L2, materialParams.sunIntensity2, materialParams.sunHalo2,
materialParams_moonTexture, materialParams_moonNormal);
materialParams_moonTexture, materialParams_moonNormal,
materialParams.nightColor,
materialParams.milkyWayRotation,
materialParams.milkyWayControl,
materialParams_milkyWayTexture);
color = applyDynamicToneMapping(color, L, materialParams.contrast);
}

View File

@@ -64,25 +64,30 @@ staging API compatibility service, which works with Filament's Gradle setup.
-----
### 1\. Upload to the Staging API Compatibility Service
### 1\. Upload to the Central Publisher Portal
To upload the artifacts, it is important to run both of these Gradle tasks together in a single
command. This ensures the staging repository is created and closed automatically.
```bash
cd android
./gradlew publishToSonatype
./gradlew publishToSonatype closeSonatypeStagingRepository
```
### 2\. Move the Repository to the Central Publisher Portal
#### Troubleshooting: Manual Staging
We have a script to automate this. It reads the `sonatypeUsername` and `sonatypePassword` from your
`~/.gradle/gradle.properties` file.
If you ran `publishToSonatype` by itself, the repository will remain open and won't appear in the
portal correctly. You can fix this by running our automation script, which uses the
`sonatypeUsername` and `sonatypePassword` from your ~/.gradle/gradle.properties file:
```bash
python3 build/common/close-sonatype-staging-repository.py
```
### 3\. Publish the Release on Sonatype
### 2\. Publish the Release on Sonatype
Navigate to [Maven Central Repository Deployments](https://central.sonatype.com/publishing/deployments).
Once the upload is successful, you must manually trigger the final release. Navigate to [Maven
Central Repository Deployments](https://central.sonatype.com/publishing/deployments).
Here, you should see a new deployment with a **Validated** status and all your artifacts listed. Click
the **Publish** button to publish the artifacts. It typically takes around 5 minutes after clicking

View File

@@ -16,12 +16,13 @@ class SimulatedSkybox {
this.ozone = 0.0;
this.msFactors = [0.1, 0.5, 0.0];
this.contrast = 1.0;
this.nightColor = [0.0, 0.0003, 0.00075];
this.nightColor = [0.0, 3.0e-9, 7.5e-9];
this.shimmerControl = [0.0, 20.0, 0.1];
this.cloudControl = [0.0, 0.1, 8000.0, 0.0];
this.cloudControl2 = [0.0, 0.0, 0.0, 0.0];
this.waterControl = [50.0, 1.0, 1.0, 4.0]; // x=Strength, y=Speed, z=DerivativeTrick, w=Octaves
this.starControl = [0.001, 1.0, 350.0, 0.01]; // x=Density, y=Enabled, z=Frequency, w=PixelScale
this.starIntensity = 1.0;
this.focalLength = 24.0;
this.height = 1000.0;
this.planetRadius = 6360.0;
@@ -43,7 +44,7 @@ class SimulatedSkybox {
// Milky Way Parameters
// x=Intensity, y=Saturation, z=Unused
this.milkyWayControl = [1.0, 1.0, 0.07];
this.milkyWayControl = [1.0, 1.2, 0.05];
this.milkyWayEnabled = true;
this.milkyWayRotation = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Identity by default
this.initEntity();
@@ -453,6 +454,11 @@ class SimulatedSkybox {
this.updateCoefficients();
}
setStarIntensity(intensity) {
this.starIntensity = Math.max(0.0, intensity);
this.updateCoefficients();
}
setMoonPosition(direction) {
// normalize
const len = Math.hypot(direction[0], direction[1], direction[2]);
@@ -500,6 +506,13 @@ class SimulatedSkybox {
}
}
setExposure(exposure) {
this.exposure = exposure;
this.updateCoefficients();
}
updateCoefficients() {
if (!this.materialInstance) {
console.warn("updateCoefficients called before material loaded");
@@ -585,6 +598,7 @@ class SimulatedSkybox {
this.materialInstance.setFloat4Parameter('cloudControl2', new Float32Array(this.cloudControl2));
this.materialInstance.setFloat4Parameter('waterControl', new Float32Array(this.waterControl));
this.materialInstance.setFloat4Parameter('starControl', new Float32Array(this.starControl));
this.materialInstance.setFloatParameter('starIntensity', this.starIntensity);
this.materialInstance.setFloatParameter('sunIntensity', physicalSunIntensity);
@@ -615,12 +629,13 @@ class SimulatedSkybox {
this.materialInstance.setFloatParameter('sunIntensity2', finalMoonIntensity);
// Scale Milky Way by Sun Intensity (Pre-exposed) to match dynamic range
// Calibration: 1.0 User Intensity ~ 0.025 Lux Relative (approx 2.5% of Sun Pixel Value at Day)
// At Sunny 16 (Sun ~ 2.6), this gives ~0.065 pixel brightness, which is visible but dim.
// Scale Milky Way by Sun Intensity
// Calibration: 1.0 User Intensity = 1.5e-3 cd/m^2 (Nits) approx.
// Target: 1.5e-3 Nits. SunIntensity = 100,000.
// Scale = 1.5e-3 / 1.0e5 = 1.5e-8.
const mwIntensity = this.milkyWayEnabled ? this.milkyWayControl[0] : 0.0;
const mwUniform = [
mwIntensity * this.sunIntensity * 0.025,
mwIntensity * this.sunIntensity * 1.5e-8,
this.milkyWayControl[1],
this.milkyWayControl[2]
];
@@ -634,6 +649,7 @@ class SimulatedSkybox {
const moonHaloUpload = [...this.moonHalo];
moonHaloUpload[2] *= moonRadConv;
this.materialInstance.setFloat4Parameter('sunHalo2', new Float32Array(moonHaloUpload));
this.materialInstance.setFloatParameter('exposure', this.exposure !== undefined ? this.exposure : 1.0);
// Solar Eclipse (CPU Calculation)
const sunRadius = Math.acos(this.sunHalo[0]);

View File

@@ -218,6 +218,7 @@ class App {
const exposure = this.getExposure();
const preExposedIntensity = this.params.sunIntensity * exposure;
this.skybox.setSunIntensity(preExposedIntensity);
this.skybox.setExposure(exposure); // Update Skybox Exposure Uniform
// Moon Exposure
if (this.mParams) {
@@ -389,7 +390,7 @@ class App {
};
mwFolder.add(this.mwParams, 'enabled').name('Enabled').onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 5.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 100.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'saturation', 0.0, 2.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'blackPoint', 0.0, 0.5).name('Black Point').onChange(updateMW);
mwFolder.add(this.mwParams, 'siderealTime', 0.0, 24.0).name('Sidereal Time').listen().onChange(updateMW);
@@ -464,18 +465,23 @@ class App {
const starFolder = gui.addFolder('Stars');
this.sParams = {
enabled: true,
density: 0.001
density: 0.001,
intensity: 0.0 // 2^0 = 1.0
};
// Initialize defaults (Density 0.001, Enabled True)
sky.setStarControl(0.001, true);
sky.setStarIntensity(Math.pow(2.0, 0.0));
const updateStars = () => {
sky.setStarControl(this.sParams.density, this.sParams.enabled);
sky.setStarIntensity(Math.pow(2.0, this.sParams.intensity));
};
starFolder.add(this.sParams, 'enabled').name('Enabled').onChange(updateStars);
starFolder.add(this.sParams, 'density', 0.0, 0.01, 0.0001).name('Density').onChange(updateStars);
starFolder.add(this.sParams, 'intensity', 0.0, 24.0).name('Intensity (Exp)').onChange(updateStars);
starFolder.close();
this.updateStars = updateStars;
const artFolder = gui.addFolder('Artistic');
// Set Horizon Glow default to 0.0
@@ -489,7 +495,7 @@ class App {
artFolder.add(sky.msFactors, 2, 0.0, 1.0).name('Horizon Glow').onChange(v => sky.setHorizonGlow(v));
artFolder.add(sky, 'contrast', 0.1, 2.0).onChange(v => sky.setContrast(v));
artFolder.addColor(sky, 'nightColor').onChange(v => sky.setNightColor(v));
const shmFolder = artFolder.addFolder('Shimmer');
// Set Shimmer Strength default to 0.0
@@ -502,7 +508,7 @@ class App {
const camFolder = gui.addFolder('Camera');
camFolder.add(this.params, 'focalLength', 8.0, 300.0).name('Focal Length').onChange(() => this.updateCameraProjection());
camFolder.add(this.params, 'aperture', 1.4, 32.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'shutterSpeed', 1.0, 1000.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'shutterSpeed', 0.05, 1000.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'iso', 50.0, 3200.0).onChange(() => this.updateCameraExposure());
const bloomFolder = camFolder.addFolder('Bloom');
@@ -692,13 +698,15 @@ class App {
const s = this.sParams;
const b = this.bParams;
const m = this.mParams;
const mw = this.mwParams;
const sk = this.skybox;
return {
p: { a: p.aperture, ss: p.shutterSpeed, i: p.iso, st: p.sunTheta, sp: p.sunPhi, fl: p.focalLength, si: p.sunIntensity },
c: { v: c.volumetrics, co: c.coverage, d: c.density, h: c.height, s: c.speed, e: c.evolution },
w: { dt: w.derivativeTrick, st: w.strength, s: w.speed, o: w.octaves },
s: { e: s.enabled, d: s.density },
s: { e: s.enabled, d: s.density, i: s.intensity },
mw: { e: mw.enabled, i: mw.intensity, s: mw.saturation, bp: mw.blackPoint, st: mw.siderealTime, l: mw.latitude },
b: { e: b.enabled, lf: b.lensFlare },
m: { e: m.enabled, az: m.azimuth, h: m.height, r: m.radius, i: m.intensity },
cm: { t: this.camState.theta, p: this.camState.phi },
@@ -723,6 +731,7 @@ class App {
const c = state.c;
const w = state.w;
const s = state.s;
const mw = state.mw;
const b = state.b;
const m = state.m;
const k = state.k;
@@ -757,6 +766,18 @@ class App {
if (s) {
if (s.e !== undefined) this.sParams.enabled = s.e;
if (s.d !== undefined) this.sParams.density = s.d;
if (s.i !== undefined) this.sParams.intensity = s.i;
if (this.updateStars) this.updateStars();
}
if (mw) {
if (mw.e !== undefined) this.mwParams.enabled = mw.e;
if (mw.i !== undefined) this.mwParams.intensity = mw.i;
if (mw.s !== undefined) this.mwParams.saturation = mw.s;
if (mw.bp !== undefined) this.mwParams.blackPoint = mw.bp;
if (mw.st !== undefined) this.mwParams.siderealTime = mw.st;
if (mw.l !== undefined) this.mwParams.latitude = mw.l;
if (this.updateMW) this.updateMW();
}
if (b) {

View File

@@ -96,10 +96,20 @@ material {
name : starControl, // x=Density, y=Enabled, z=Frequency, w=PixelScale
precision : high
},
{
type : float,
name : starIntensity,
precision : high
},
{
type : sampler2d,
name : moonTexture
},
{
type : float,
name : exposure,
precision : high
},
{
type : sampler2d,
name : moonNormal
@@ -162,9 +172,9 @@ fragment {
// --- CONFIGURATION ---
// Stars
#define STAR_GLOBAL_INTENSITY 150.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.5 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 4.0 // Random brightness variance range [0.0 - 10.0]
#define STAR_GLOBAL_INTENSITY 100.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.01 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 15.0 // Random brightness variance range [0.0 - 10.0]
#define STAR_FADE_SUN_ELV_HIGH 0.10 // Sun elevation (sin) where stars are fully hidden [0.0 - 0.5]
#define STAR_FADE_SUN_ELV_LOW -0.20 // Sun elevation (sin) where stars are fully visible [-0.5 - 0.0]
#define STAR_CLOUD_OCCLUSION 0.1 // Visibility when covered by clouds [0.0 - 1.0]
@@ -961,7 +971,11 @@ fragment {
highp vec4 sunHalo, highp vec4 cloudControl, highp vec4 cloudControl2,
highp vec4 shimmerControl, highp vec4 waterControl,
highp vec3 L2, highp float sunIntensity2, highp vec4 sunHalo2,
sampler2D moonTex, sampler2D moonNormal) {
sampler2D moonTex, sampler2D moonNormal,
highp vec3 nightColor,
highp mat3 milkyWayRotation,
highp vec3 milkyWayControl,
sampler2D milkyWayTexture) {
// Project to plane y=0
highp float t = WATER_PLANE_HEIGHT / min(V.y, -0.0002); // Reduced clamp to minimize "wall" artifact
@@ -1057,7 +1071,7 @@ fragment {
highp float rHorizonMask = 1.0 - smoothstep(0.0, REFLECTION_HORIZON_FADE, R.y);
if (rHorizonMask > 0.0) {
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask;
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask * materialParams.exposure;
}
// Sun Disk Reflection
@@ -1088,6 +1102,29 @@ fragment {
// Apply clouds to reflection
reflection = mix(reflection, reflCloudLayer, reflCloudDensity);
// Add Milky Way to Reflection
// Calculate Fade based on Sun Elevation
highp float sunElvSin = L.y;
highp float mwFade = smoothstep(STAR_FADE_SUN_ELV_HIGH, STAR_FADE_SUN_ELV_LOW, sunElvSin);
if (mwFade > 0.0) {
highp vec3 mwColor = getMilkyWay(R, milkyWayRotation, milkyWayTexture, milkyWayControl);
// Apply Atmosphere Transmittance (approximate)
mwColor *= outTransmittance;
// Apply Fades
mwColor *= mwFade;
mwColor *= (1.0 - reflMoonOcclusion); // Occlude by Moon
mwColor *= (1.0 - reflCloudDensity); // Occlude by Clouds
reflection += mwColor;
}
// Add Night Color to Reflection
reflection += nightColor;
// Fresnel
highp float F0 = WATER_FRESNEL_F0; // Water
highp float cosTheta = clamp(dot(-V, N_water), 0.0, 1.0);
@@ -1187,7 +1224,7 @@ fragment {
// 7. Stars
// Add stars before clouds (clouds cover stars)
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl);
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl) * materialParams.exposure * materialParams.starIntensity;
// 7b. Milky Way
// Add Milky Way behind stars (conceptually) but handled similarly
@@ -1232,7 +1269,11 @@ fragment {
materialParams.shimmerControl,
materialParams.waterControl,
L2, materialParams.sunIntensity2, materialParams.sunHalo2,
materialParams_moonTexture, materialParams_moonNormal);
materialParams_moonTexture, materialParams_moonNormal,
materialParams.nightColor,
materialParams.milkyWayRotation,
materialParams.milkyWayControl,
materialParams_milkyWayTexture);
color = applyDynamicToneMapping(color, L, materialParams.contrast);
}

View File

@@ -93,6 +93,7 @@ set(SRCS
src/MorphTargetBuffer.cpp
src/PostProcessManager.cpp
src/ProgramSpecialization.cpp
src/LocalProgramCache.cpp
src/RenderPass.cpp
src/RenderPrimitive.cpp
src/RenderTarget.cpp
@@ -186,6 +187,7 @@ set(PRIVATE_HDRS
src/MaterialCache.h
src/MaterialDefinition.h
src/MaterialParser.h
src/LocalProgramCache.h
src/MaterialInstanceManager.h
src/PIDController.h
src/PostProcessManager.h
@@ -314,6 +316,12 @@ set(MATERIAL_FL0_SRCS
src/materials/skybox.mat
)
set(MATERIAL_MULTIVIEW_SRCS
src/materials/clearDepth.mat
src/materials/defaultMaterial.mat
src/materials/skybox.mat
)
# ==================================================================================================
# Configuration
# ==================================================================================================
@@ -338,6 +346,11 @@ 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)
endif()
# Whether to force the profiling mode.
if (FILAMENT_FORCE_PROFILING_MODE)
add_definitions(-DFILAMENT_FORCE_PROFILING_MODE)
@@ -428,6 +441,21 @@ foreach(mat_dir ${MATERIAL_DIRS})
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path_fl0})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path_fl0})
endif()
# --- Multiview variant ---
list(FIND MATERIAL_MULTIVIEW_SRCS ${mat_src} index)
if (${index} GREATER -1 AND FILAMENT_ENABLE_MULTIVIEW)
string(REGEX REPLACE "[.]filamat$" "_multiview.filamat" output_path_multiview ${output_path})
add_custom_command(
OUTPUT ${output_path_multiview}
COMMAND matc ${MATC_BASE_FLAGS} -PstereoscopicType=multiview -o ${output_path_multiview} ${fullname}
MAIN_DEPENDENCY ${fullname}
DEPENDS matc
COMMENT "Compiling material ${fullname} (Multiview)"
)
list(APPEND FILAMAT_FILES_FOR_GROUP ${output_path_multiview})
list(APPEND FILAMAT_TARGETS_FOR_GROUP ${output_path_multiview})
endif()
endforeach()
# Generate a single resource file for the whole group

View File

@@ -564,169 +564,169 @@ endif()
# ==================================================================================================
# Test
# ==================================================================================================
option(INSTALL_BACKEND_TEST "Install the backend test library so it can be consumed on iOS" OFF)
if (FILAMENT_BUILD_TESTING)
option(INSTALL_BACKEND_TEST "Install the backend test library so it can be consumed on iOS" OFF)
if (APPLE OR LINUX)
set(BACKEND_TEST_SRC
test/BackendTest.cpp
test/ShaderGenerator.cpp
test/TrianglePrimitive.cpp
test/Arguments.cpp
test/ImageExpectations.cpp
test/Lifetimes.cpp
test/PlatformRunner.cpp
test/Shader.cpp
test/SharedShaders.cpp
test/Skip.cpp
test/test_FeedbackLoops.cpp
test/test_Blit.cpp
test/test_MissingRequiredAttributes.cpp
test/test_ReadPixels.cpp
test/test_ReadTexture.cpp
test/test_BufferUpdates.cpp
test/test_Callbacks.cpp
test/test_JobQueue.cpp
test/test_MemoryMappedBuffer.cpp
test/test_MsaaSwapChain.cpp
test/test_MRT.cpp
test/test_PushConstants.cpp
test/test_LoadImage.cpp
test/test_StencilBuffer.cpp
test/test_Scissor.cpp
test/test_MipLevels.cpp
test/test_Handles.cpp
test/test_CircularBuffer.cpp
test/test_CommandBufferQueue.cpp
test/test_Template.cpp
)
if (FILAMENT_SUPPORTS_WEBGPU)
list(APPEND BACKEND_TEST_SRC
test/test_WebGPUAsyncTaskCounter.cpp
test/test_WebGPUComposeSwizzle.cpp)
endif()
if (APPLE)
# Metal-specific tests
list(APPEND BACKEND_TEST_SRC
test/test_MetalBlitter.mm
if (APPLE OR LINUX)
set(BACKEND_TEST_SRC
test/BackendTest.cpp
test/ShaderGenerator.cpp
test/TrianglePrimitive.cpp
test/Arguments.cpp
test/ImageExpectations.cpp
test/Lifetimes.cpp
test/PlatformRunner.cpp
test/Shader.cpp
test/SharedShaders.cpp
test/Skip.cpp
test/test_Autoresolve.cpp
test/test_FeedbackLoops.cpp
test/test_Blit.cpp
test/test_MissingRequiredAttributes.cpp
test/test_ReadPixels.cpp
test/test_ReadTexture.cpp
test/test_BufferUpdates.cpp
test/test_Callbacks.cpp
test/test_JobQueue.cpp
test/test_MemoryMappedBuffer.cpp
test/test_MsaaSwapChain.cpp
test/test_MRT.cpp
test/test_PushConstants.cpp
test/test_LoadImage.cpp
test/test_StencilBuffer.cpp
test/test_Scissor.cpp
test/test_MipLevels.cpp
test/test_Handles.cpp
test/test_CircularBuffer.cpp
test/test_CommandBufferQueue.cpp
test/test_Template.cpp
test/test_Platform.cpp
)
endif()
set(BACKEND_TEST_LIBS
absl::str_format
backend
getopt
gtest
imageio
filamat
SPIRV
spirv-cross-glsl)
# Create input/output directories for test result images.
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/actual_images)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/diff_images)
file(COPY test/expected_images DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/images)
endif()
# TODO: Disabling IOS test due to breakage wrt glslang update
if (APPLE AND NOT IOS)
# TODO: we should expand this test to Linux and other platforms.
list(APPEND BACKEND_TEST_SRC
test/test_RenderExternalImage.cpp)
add_library(backend_test STATIC ${BACKEND_TEST_SRC})
target_link_libraries(backend_test PUBLIC ${BACKEND_TEST_LIBS})
target_compile_options(backend_test PRIVATE "-fobjc-arc")
set(BACKEND_TEST_DEPS
OSDependent
SPIRV
SPIRV-Tools
SPIRV-Tools-opt
backend_test
getopt
if (FILAMENT_SUPPORTS_WEBGPU)
list(APPEND BACKEND_TEST_SRC
test/test_WebGPUAsyncTaskCounter.cpp
test/test_WebGPUComposeSwizzle.cpp)
endif()
if (APPLE)
# Metal-specific tests
list(APPEND BACKEND_TEST_SRC
test/test_MetalBlitter.mm
)
endif()
set(BACKEND_TEST_LIBS
absl::str_format
backend
gtest
glslang
spirv-cross-core
spirv-cross-glsl
spirv-cross-msl)
if (NOT IOS)
target_link_libraries(backend_test PRIVATE image imageio)
list(APPEND BACKEND_TEST_DEPS image)
endif()
if (FILAMENT_SUPPORTS_WEBGPU)
target_link_libraries(backend_test PRIVATE webgpu_dawn dawncpp_headers)
list(APPEND BACKEND_TEST_DEPS webgpu_dawn dawncpp_headers)
imageio
filamat
SPIRV
spirv-cross-glsl)
# Create input/output directories for test result images.
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/actual_images)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/diff_images)
file(COPY test/expected_images DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/images)
endif()
set(BACKEND_TEST_COMBINED_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libbackendtest_combined.a")
combine_static_libs(backend_test "${BACKEND_TEST_COMBINED_OUTPUT}" "${BACKEND_TEST_DEPS}")
# TODO: Disabling IOS test due to breakage wrt glslang update
if (APPLE AND NOT IOS)
# TODO: we should expand this test to Linux and other platforms.
list(APPEND BACKEND_TEST_SRC
test/test_RenderExternalImage.cpp)
add_library(backend_test STATIC ${BACKEND_TEST_SRC})
target_link_libraries(backend_test PUBLIC ${BACKEND_TEST_LIBS})
target_compile_options(backend_test PRIVATE "-fobjc-arc")
set(BACKEND_TEST_LIB_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}backend_test${CMAKE_STATIC_LIBRARY_SUFFIX})
set(BACKEND_TEST_DEPS
OSDependent
SPIRV
SPIRV-Tools
SPIRV-Tools-opt
backend_test
getopt
gtest
glslang
spirv-cross-core
spirv-cross-glsl
spirv-cross-msl)
if (INSTALL_BACKEND_TEST)
install(FILES "${BACKEND_TEST_COMBINED_OUTPUT}" DESTINATION lib/${DIST_DIR} RENAME ${BACKEND_TEST_LIB_NAME})
install(FILES test/PlatformRunner.h DESTINATION include/backend_test)
if (NOT IOS)
target_link_libraries(backend_test PRIVATE image imageio)
list(APPEND BACKEND_TEST_DEPS image)
endif()
if (FILAMENT_SUPPORTS_WEBGPU)
target_link_libraries(backend_test PRIVATE webgpu_dawn dawncpp_headers)
list(APPEND BACKEND_TEST_DEPS webgpu_dawn dawncpp_headers)
endif()
set(BACKEND_TEST_COMBINED_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libbackendtest_combined.a")
combine_static_libs(backend_test "${BACKEND_TEST_COMBINED_OUTPUT}" "${BACKEND_TEST_DEPS}")
set(BACKEND_TEST_LIB_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}backend_test${CMAKE_STATIC_LIBRARY_SUFFIX})
if (INSTALL_BACKEND_TEST)
install(FILES "${BACKEND_TEST_COMBINED_OUTPUT}" DESTINATION lib/${DIST_DIR} RENAME ${BACKEND_TEST_LIB_NAME})
install(FILES test/PlatformRunner.h DESTINATION include/backend_test)
endif()
set_target_properties(backend_test PROPERTIES FOLDER Tests)
if (APPLE AND NOT IOS)
add_executable(backend_test_mac test/mac_runner.mm)
target_link_libraries(backend_test_mac PRIVATE "-framework Metal -framework AppKit -framework QuartzCore")
# Because each test case is a separate file, the -force_load flag is necessary to prevent the
# linker from removing "unused" symbols.
target_link_libraries(backend_test_mac PRIVATE -force_load backend_test)
set_target_properties(backend_test_mac PROPERTIES FOLDER Tests)
# This is needed after XCode 15.3
set_target_properties(backend_test_mac PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE)
set_target_properties(backend_test_mac PROPERTIES INSTALL_RPATH /usr/local/lib)
endif()
endif()
set_target_properties(backend_test PROPERTIES FOLDER Tests)
if (LINUX)
add_executable(backend_test_linux test/linux_runner.cpp ${BACKEND_TEST_SRC})
target_link_libraries(backend_test_linux PRIVATE ${BACKEND_TEST_LIBS})
set_target_properties(backend_test_linux PROPERTIES FOLDER Tests)
if (FILAMENT_SUPPORTS_WEBGPU)
target_link_libraries(backend_test_linux PRIVATE webgpu_dawn dawncpp_headers)
endif()
endif()
# ==================================================================================================
# Compute tests
#
#if (NOT IOS AND NOT WEBGL)
#
#add_executable(compute_test
# test/ComputeTest.cpp
# test/Arguments.cpp
# test/test_ComputeBasic.cpp
# )
#
#target_link_libraries(compute_test PRIVATE
# backend
# getopt
# gtest
# )
#
#set_target_properties(compute_test PROPERTIES FOLDER Tests)
#
#endif()
# ==================================================================================================
# Metal utils tests
if (APPLE AND NOT IOS)
add_executable(backend_test_mac test/mac_runner.mm)
target_link_libraries(backend_test_mac PRIVATE "-framework Metal -framework AppKit -framework QuartzCore")
# Because each test case is a separate file, the -force_load flag is necessary to prevent the
# linker from removing "unused" symbols.
target_link_libraries(backend_test_mac PRIVATE -force_load backend_test)
set_target_properties(backend_test_mac PROPERTIES FOLDER Tests)
add_executable(metal_utils_test test/MetalTest.mm)
# This is needed after XCode 15.3
set_target_properties(backend_test_mac PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE)
set_target_properties(backend_test_mac PROPERTIES INSTALL_RPATH /usr/local/lib)
target_compile_options(metal_utils_test PRIVATE "-fobjc-arc")
target_link_libraries(metal_utils_test PRIVATE
backend
gtest
)
set_target_properties(metal_utils_test PROPERTIES FOLDER Tests)
endif()
endif()
if (LINUX)
add_executable(backend_test_linux test/linux_runner.cpp ${BACKEND_TEST_SRC})
target_link_libraries(backend_test_linux PRIVATE ${BACKEND_TEST_LIBS})
set_target_properties(backend_test_linux PROPERTIES FOLDER Tests)
if (FILAMENT_SUPPORTS_WEBGPU)
target_link_libraries(backend_test_linux PRIVATE webgpu_dawn dawncpp_headers)
endif()
endif()
# ==================================================================================================
# Compute tests
#
#if (NOT IOS AND NOT WEBGL)
#
#add_executable(compute_test
# test/ComputeTest.cpp
# test/Arguments.cpp
# test/test_ComputeBasic.cpp
# )
#
#target_link_libraries(compute_test PRIVATE
# backend
# getopt
# gtest
# )
#
#set_target_properties(compute_test PROPERTIES FOLDER Tests)
#
#endif()
# ==================================================================================================
# Metal utils tests
if (APPLE AND NOT IOS)
add_executable(metal_utils_test test/MetalTest.mm)
target_compile_options(metal_utils_test PRIVATE "-fobjc-arc")
target_link_libraries(metal_utils_test PRIVATE
backend
getopt
gtest
)
set_target_properties(metal_utils_test PROPERTIES FOLDER Tests)
endif()

View File

@@ -216,6 +216,18 @@ public:
MULTIVIEW,
};
/**
* Types of device/driver information that can be queried from the platform.
*/
enum class DeviceInfoType {
OPENGL_RENDERER, //!< glGetString(GL_RENDERER)
OPENGL_VENDOR, //!< glGetString(GL_VENDOR)
OPENGL_VERSION, //!< glGetString(GL_VERSION)
VULKAN_DEVICE_NAME, //!< VkPhysicalDeviceProperties::deviceName
VULKAN_DRIVER_NAME, //!< VkPhysicalDeviceDriverProperties::driverName
VULKAN_DRIVER_INFO, //!< VkPhysicalDeviceDriverProperties::driverInfo
};
/**
* This controls the priority level for GPU work scheduling, which helps prioritize the
* submitted GPU work and enables preemption.
@@ -316,7 +328,7 @@ public:
*/
StereoscopicType stereoscopicType = StereoscopicType::NONE;
/*
/**
* The number of eyes to render when stereoscopic rendering is enabled. Supported values are
* between 1 and Engine::getMaxStereoscopicEyes() (inclusive).
*/
@@ -377,6 +389,16 @@ public:
*/
virtual int getOSVersion() const noexcept = 0;
/**
* Queries device/driver information of the graphics API.
* @param infoType the type of information to query.
* @param driver a pointer to the current driver.
* @return a CString containing the requested information.
*/
virtual utils::CString getDeviceInfo(DeviceInfoType infoType,
Driver* UTILS_NULLABLE driver) const noexcept = 0;
/**
* Creates and initializes the low-level API (e.g. an OpenGL context or Vulkan instance),
* then creates the concrete Driver.
@@ -561,16 +583,18 @@ public:
/**
* Sets the callback function that the backend can use to update backend-specific statistics
* to aid with debugging. This callback is guaranteed to be called on the Filament driver
* thread.
* to aid with debugging. This callback can be called on either the Filament main thread or
* the Filament driver thread.
*
* The callback signature is (key, intValue, stringValue). Note that for any given call,
* only one of the value parameters (intValue or stringValue) will be meaningful, depending on
* the specific key.
*
* IMPORTANT_NOTE: because the callback is called on the driver thread, only quick, non-blocking
* work should be done inside it. Furthermore, no graphics API calls (such as GL calls) should
* be made, which could interfere with Filament's driver state.
* IMPORTANT_NOTE: because the callback can be called on the driver thread, only quick,
* non-blocking work should be done inside it. Furthermore, no graphics API calls (such as GL
* calls) should be made, which could interfere with Filament's driver state. Lastly, the
* callback implementation must be synchronized (thread-safe) since it can be called from
* either thread.
*
* @param debugUpdateStat an Invocable that updates debug statistics
*/
@@ -587,8 +611,7 @@ public:
* with a given key. It is possible for this function to be called multiple times with the
* same key, in which case newer values should overwrite older values.
*
* This function is guaranteed to be called only on a single thread, the Filament driver
* thread.
* This function can be called on either the Filament main thread or the Filament driver thread.
*
* @param key a null-terminated C-string with the key of the debug statistic
* @param intValue the updated integer value of key (the string value passed to the
@@ -602,8 +625,7 @@ public:
* with a given key. It is possible for this function to be called multiple times with the
* same key, in which case newer values should overwrite older values.
*
* This function is guaranteed to be called only on a single thread, the Filament driver
* thread.
* This function can be called on either the Filament main thread or the Filament driver thread.
*
* @param key a null-terminated C-string with the key of the debug statistic
* @param stringValue the updated string value of key (the integer value passed to the

View File

@@ -52,6 +52,9 @@ protected:
~OpenGLPlatform() noexcept override;
utils::CString getDeviceInfo(DeviceInfoType infoType,
Driver* UTILS_NULLABLE driver) const noexcept override;
public:
struct ExternalTexture {
unsigned int target; // GLenum target
@@ -70,6 +73,12 @@ public:
*/
static utils::CString getRendererString(Driver const* UTILS_NONNULL driver);
/**
* Return the OpenGL version string of the specified Driver instance.
* @return The GL_VERSION string
*/
static utils::CString getVersionString(Driver const* UTILS_NONNULL driver);
/**
* Called by the driver to destroy the OpenGL context. This should clean up any windows
* or buffers from initialization. This is for instance where `eglDestroyContext` would be

View File

@@ -159,6 +159,12 @@ protected:
EGLConfig getEglConfig() const noexcept { return mEGLConfig; }
EGLConfig getSuitableConfigForSwapChain(uint64_t flags, bool window, bool pbuffer) const;
// Sets the EGLDisplay to be used by this platform. This should only be called by derived
// classes before invoking createDriver. Calling it after that point will result in
// undefined behaviour. This class will take ownership of the display and call eglTerminate
// on it during shutdown.
void setEglDisplay(EGLDisplay display) noexcept;
// supported extensions detected at runtime
struct {
struct {

View File

@@ -185,7 +185,7 @@ private:
};
int mOSVersion;
ExternalStreamManagerAndroid& mExternalStreamManager;
ExternalStreamManagerAndroid* mExternalStreamManager = nullptr;
AndroidDetails& mAndroidDetails;
utils::PerformanceHintManager mPerformanceHintManager;
utils::PerformanceHintManager::Session mPerformanceHintSession;

View File

@@ -38,6 +38,7 @@ public:
Driver* createDriver(void* sharedContext, const Platform::DriverConfig& driverConfig) override;
int getOSVersion() const noexcept override { return 0; }
utils::CString getDeviceInfo(DeviceInfoType, Driver*) const noexcept override { return {}; }
/**
* Optionally initializes the Metal platform by acquiring resources necessary for rendering.

View File

@@ -31,7 +31,6 @@
#include <cstring>
#include <cstddef>
#include <functional>
#include <string>
#include <tuple>
#include <unordered_set>
@@ -69,7 +68,7 @@ public:
struct ExtensionHashFn {
std::size_t operator()(utils::CString const& s) const noexcept {
return std::hash<std::string>{}(s.data());
return std::hash<utils::CString>{}(s.data());
}
};
// Note: utils::CString::operator== has an edge case that breaks for the extension set.
@@ -143,6 +142,8 @@ public:
return 0;
}
utils::CString getDeviceInfo(DeviceInfoType infoType, Driver* driver) const noexcept override;
// ----------------------------------------------------
// ---------- Platform Customization options ----------
struct Customization {
@@ -175,6 +176,12 @@ public:
* presentation. Default is true.
*/
bool transitionSwapChainImageLayoutForPresent = true;
/**
* The number of frames before an unused framebuffer is evicted from the cache.
* Default is 3.
*/
uint32_t timeBeforeEvictionFbo = 3;
};
/**

View File

@@ -46,6 +46,9 @@ public:
~WebGPUPlatform() override = default;
[[nodiscard]] int getOSVersion() const noexcept final { return 0; }
[[nodiscard]] utils::CString getDeviceInfo(DeviceInfoType, Driver*) const noexcept override {
return {};
}
[[nodiscard]] wgpu::Instance& getInstance() noexcept { return mInstance; }

View File

@@ -32,7 +32,7 @@
#include <cstddef>
#include <functional>
#include <string>
#include <string_view>
#include <utility>
#ifdef __ANDROID__

View File

@@ -19,21 +19,169 @@
#include <backend/PixelBufferDescriptor.h>
#include <math/scalar.h>
#include <math/half.h>
#include <utils/debug.h>
#include <utils/Logger.h>
#include <cstdint>
#include <cstring>
#include <stddef.h>
#include <stdint.h>
#include <math/scalar.h>
#include <utils/debug.h>
namespace filament {
namespace backend {
namespace {
// Provides an alpha value when expanding 3-channel images to 4-channel.
// Also used as a normalization scale when converting between numeric types.
template<typename componentType> inline componentType getMaxValue();
template<> inline constexpr float getMaxValue() { return 1.0f; }
template<> inline constexpr int32_t getMaxValue() { return 0x7fffffff; }
template<> inline constexpr uint32_t getMaxValue() { return 0xffffffff; }
template<> inline constexpr uint16_t getMaxValue() { return 0x3c00; } // 0x3c00 is 1.0 in half-float.
template<> inline constexpr uint8_t getMaxValue() { return 0xff; }
template<> inline math::half getMaxValue() { return math::half(1.0f); }
// We use template below to reduce code duplication across the different input/output
// type/channle-count permutations. Morever, templates help us reduce the number of conditionals
// in the inner-loop of the reshape operation. However, this needs to be a carefully considered
// because too many templated params will cause a large binary size increase.
// Note that we intentionally do not want to expand the template params to include the channel count
// because of the size increase.
template<typename dstComponentType, bool hasAlpha>
void grayscaleFill(dstComponentType* dst, uint8_t, uint8_t) {
for (size_t channel = 1; channel < 3; ++channel) {
dst[channel] = dst[0];
}
if constexpr (hasAlpha) {
dst[3] = getMaxValue<dstComponentType>();
}
}
// Note that we intentionally do not want to expand the template params to include the channel count
// because of the size increase.
template<typename dstComponentType>
inline void maxValFill(dstComponentType* dst, uint8_t srcChannelCount, uint8_t dstChannelCount) {
dstComponentType dstMaxValue = getMaxValue<dstComponentType>();
for (size_t channel = srcChannelCount; channel < dstChannelCount; ++channel) {
dst[channel] = dstMaxValue;
}
}
// Converts a n-channel image of UBYTE, INT, UINT, HALF, or FLOAT to a different type.
template<typename dstComponentType, typename srcComponentType>
void reshapeImageImpl(uint8_t* UTILS_RESTRICT dest, const uint8_t* UTILS_RESTRICT src,
size_t srcBytesPerRow, size_t srcChannelCount, size_t dstRowOffset, size_t dstColumnOffset,
size_t dstBytesPerRow, size_t dstChannelCount, size_t width, size_t height, bool swizzle) {
static_assert(!std::is_same_v<dstComponentType, math::half>);
const size_t minChannelCount = math::min(srcChannelCount, dstChannelCount);
const dstComponentType dstMaxValue = getMaxValue<dstComponentType>();
const srcComponentType srcMaxValue = getMaxValue<srcComponentType>();
double const mFactor = dstMaxValue / ((double) srcMaxValue);
assert_invariant(minChannelCount <= 4);
UTILS_ASSUME(minChannelCount <= 4);
dest += (dstRowOffset * dstBytesPerRow);
void (*fill)(dstComponentType*, uint8_t, uint8_t);
if (srcChannelCount == 1 && dstChannelCount == 3) {
fill = grayscaleFill<dstComponentType, false>;
} else if (srcChannelCount == 1 && dstChannelCount == 4) {
fill = grayscaleFill<dstComponentType, true>;
} else {
fill = maxValFill<dstComponentType>;
}
const int inds[4] = { swizzle ? 2 : 0, 1, swizzle ? 0 : 2, 3 };
for (size_t row = 0; row < height; ++row) {
const srcComponentType* in = (const srcComponentType*) src;
dstComponentType* out = (dstComponentType*) dest + (dstColumnOffset * dstChannelCount);
for (size_t column = 0; column < width; ++column) {
for (uint8_t channel = 0; channel < minChannelCount; ++channel) {
if constexpr (std::is_same_v<dstComponentType, srcComponentType>) {
out[channel] = in[inds[channel]];
} else {
// convert to double then clamp and cast to dst type.
out[channel] = static_cast<dstComponentType>(std::clamp(
in[inds[channel]] * mFactor, 0.0,
static_cast<double>(std::numeric_limits<dstComponentType>::max())));
}
}
// This will fill in all the channels that are not copied.
fill(out, srcChannelCount, dstChannelCount);
in += srcChannelCount;
out += dstChannelCount;
}
src += srcBytesPerRow;
dest += dstBytesPerRow;
}
}
struct UnpackerR11G11B10 {
static void unpack(const uint8_t* src, float* out) {
uint32_t p;
std::memcpy(&p, src, 4);
using R11 = math::fp<0, 5, 6>;
using G11 = math::fp<0, 5, 6>;
using B10 = math::fp<0, 5, 5>;
out[0] = R11::tof(R11(uint16_t((p >> 21) & 0x7FF)));
out[1] = G11::tof(G11(uint16_t((p >> 10) & 0x7FF)));
out[2] = B10::tof(B10(uint16_t(p & 0x3FF)));
}
};
template<typename dstComponentType, typename Unpacker, bool Swizzle>
static void reshapeImagePacked(uint8_t* UTILS_RESTRICT dest, const uint8_t* UTILS_RESTRICT src,
size_t srcBytesPerRow, size_t srcChannelCount, size_t dstRowOffset, size_t dstColumnOffset,
size_t dstBytesPerRow, size_t dstChannelCount, size_t width, size_t height, bool /*swizzle*/) {
dest += (dstRowOffset * dstBytesPerRow);
const dstComponentType dstMaxValue = getMaxValue<dstComponentType>();
for (size_t row = 0; row < height; ++row) {
const uint8_t* inPtr = src;
dstComponentType* out = (dstComponentType*) dest + (dstColumnOffset * dstChannelCount);
for (size_t column = 0; column < width; ++column) {
float rgba[4] = {0.0f, 0.0f, 0.0f, 1.0f};
Unpacker::unpack(inPtr, rgba);
if constexpr (Swizzle) {
std::swap(rgba[0], rgba[2]);
}
for (size_t c = 0; c < dstChannelCount; ++c) {
if constexpr (std::is_same_v<dstComponentType, float>) {
out[c] = rgba[c];
} else if constexpr (std::is_same_v<dstComponentType, math::half>) {
out[c] = math::half(rgba[c]);
} else {
out[c] = static_cast<dstComponentType>(std::clamp(
static_cast<double>(rgba[c]) * static_cast<double>(dstMaxValue),
0.0,
static_cast<double>(std::numeric_limits<dstComponentType>::max())
));
}
}
inPtr += 4;
out += dstChannelCount;
}
src += srcBytesPerRow;
dest += dstBytesPerRow;
}
}
} // anonymous namespace
class DataReshaper {
public:
@@ -76,51 +224,9 @@ public:
}
}
// Converts a n-channel image of UBYTE, INT, UINT, or FLOAT to a different type.
template<typename dstComponentType, typename srcComponentType>
static void reshapeImage(uint8_t* UTILS_RESTRICT dest, const uint8_t* UTILS_RESTRICT src,
size_t srcBytesPerRow,
size_t srcChannelCount,
size_t dstRowOffset, size_t dstColumnOffset,
size_t dstBytesPerRow, size_t dstChannelCount,
size_t width, size_t height, bool swizzle) {
// TODO: there's a fast-path where memcpy will work but currently not being taken advantage
// of.
const dstComponentType dstMaxValue = getMaxValue<dstComponentType>();
const srcComponentType srcMaxValue = getMaxValue<srcComponentType>();
const size_t minChannelCount = math::min(srcChannelCount, dstChannelCount);
assert_invariant(minChannelCount <= 4);
UTILS_ASSUME(minChannelCount <= 4);
dest += (dstRowOffset * dstBytesPerRow);
const int inds[4] = { swizzle ? 2 : 0, 1, swizzle ? 0 : 2, 3 };
for (size_t row = 0; row < height; ++row) {
const srcComponentType* in = (const srcComponentType*) src;
dstComponentType* out = (dstComponentType*)dest + (dstColumnOffset * dstChannelCount);
for (size_t column = 0; column < width; ++column) {
for (size_t channel = 0; channel < minChannelCount; ++channel) {
if constexpr (std::is_same_v<dstComponentType, srcComponentType>) {
out[channel] = in[inds[channel]];
} else {
// FIXME: beware of overflows in the multiply
// FIXME: probably not correct for _INTEGER src/dst
out[channel] = in[inds[channel]] * dstMaxValue / srcMaxValue;
}
}
for (size_t channel = srcChannelCount; channel < dstChannelCount; ++channel) {
out[channel] = dstMaxValue;
}
in += srcChannelCount;
out += dstChannelCount;
}
src += srcBytesPerRow;
dest += dstBytesPerRow;
}
}
// Converts a n-channel image of UBYTE, INT, UINT, or FLOAT to a different type.
static bool reshapeImage(PixelBufferDescriptor* UTILS_RESTRICT dst, PixelDataType srcType,
uint32_t srcChannelCount, const uint8_t* UTILS_RESTRICT srcBytes, int srcBytesPerRow,
uint32_t srcChannelCount, const uint8_t* UTILS_RESTRICT srcBytes, int srcBytesPerRow,
int width, int height, bool swizzle) {
size_t dstChannelCount;
switch (dst->format) {
@@ -132,87 +238,123 @@ public:
case PixelDataFormat::RG: dstChannelCount = 2; break;
case PixelDataFormat::RGB: dstChannelCount = 3; break;
case PixelDataFormat::RGBA: dstChannelCount = 4; break;
default: return false;
default:
LOG(ERROR) << "DataReshaper: unsupported dst->format: " << (int) dst->format;
return false;
}
void (*reshaper)(uint8_t* dest, const uint8_t* src, size_t srcBytesPerRow,
size_t srcChannelCount,
size_t srcRowOffset, size_t srcColumnOffset,
size_t dstBytesPerRow, size_t dstChannelCount,
size_t width, size_t height, bool swizzle) = nullptr;
size_t srcChannelCount, size_t srcRowOffset, size_t srcColumnOffset,
size_t dstBytesPerRow, size_t dstChannelCount, size_t width, size_t height,
bool swizzle) = nullptr;
constexpr auto UBYTE = PixelDataType::UBYTE;
constexpr auto FLOAT = PixelDataType::FLOAT;
constexpr auto UINT = PixelDataType::UINT;
constexpr auto INT = PixelDataType::INT;
constexpr auto HALF = PixelDataType::HALF;
constexpr auto UINT_10F_11F_11F_REV = PixelDataType::UINT_10F_11F_11F_REV;
switch (dst->type) {
case UBYTE:
switch (srcType) {
case UBYTE:
reshaper = reshapeImage<uint8_t, uint8_t>;
reshaper = reshapeImageImpl<uint8_t, uint8_t>;
if (dst->format == PixelDataFormat::RGBA &&
dstChannelCount == srcChannelCount && !swizzle && dst->top == 0 &&
dst->left == 0) {
reshaper = copyImage;
}
break;
case FLOAT: reshaper = reshapeImage<uint8_t, float>; break;
case INT: reshaper = reshapeImage<uint8_t, int32_t>; break;
case UINT: reshaper = reshapeImage<uint8_t, uint32_t>; break;
default: return false;
case FLOAT: reshaper = reshapeImageImpl<uint8_t, float>; break;
case INT: reshaper = reshapeImageImpl<uint8_t, int32_t>; break;
case UINT: reshaper = reshapeImageImpl<uint8_t, uint32_t>; break;
case HALF: reshaper = reshapeImageImpl<uint8_t, math::half>; break;
case UINT_10F_11F_11F_REV:
if (swizzle) reshaper = reshapeImagePacked<uint8_t, UnpackerR11G11B10, true>;
else reshaper = reshapeImagePacked<uint8_t, UnpackerR11G11B10, false>;
break;
default:
LOG(ERROR) << "DataReshaper: UBYTE dst, unsupported srcType: "
<< (int) srcType;
return false;
}
break;
case FLOAT:
switch (srcType) {
case UBYTE: reshaper = reshapeImage<float, uint8_t>; break;
case FLOAT: reshaper = reshapeImage<float, float>; break;
case INT: reshaper = reshapeImage<float, int32_t>; break;
case UINT: reshaper = reshapeImage<float, uint32_t>; break;
default: return false;
case UBYTE: reshaper = reshapeImageImpl<float, uint8_t>; break;
case FLOAT: reshaper = reshapeImageImpl<float, float>; break;
case INT: reshaper = reshapeImageImpl<float, int32_t>; break;
case UINT: reshaper = reshapeImageImpl<float, uint32_t>; break;
case UINT_10F_11F_11F_REV:
if (swizzle) reshaper = reshapeImagePacked<float, UnpackerR11G11B10, true>;
else reshaper = reshapeImagePacked<float, UnpackerR11G11B10, false>;
break;
default:
LOG(ERROR) << "DataReshaper: FLOAT dst, unsupported srcType: "
<< (int) srcType;
return false;
}
break;
case INT:
switch (srcType) {
case UBYTE: reshaper = reshapeImage<int32_t, uint8_t>; break;
case FLOAT: reshaper = reshapeImage<int32_t, float>; break;
case INT: reshaper = reshapeImage<int32_t, int32_t>; break;
case UINT: reshaper = reshapeImage<int32_t, uint32_t>; break;
default: return false;
case UBYTE: reshaper = reshapeImageImpl<int32_t, uint8_t>; break;
case FLOAT: reshaper = reshapeImageImpl<int32_t, float>; break;
case INT: reshaper = reshapeImageImpl<int32_t, int32_t>; break;
case UINT: reshaper = reshapeImageImpl<int32_t, uint32_t>; break;
case UINT_10F_11F_11F_REV:
if (swizzle) reshaper = reshapeImagePacked<int32_t, UnpackerR11G11B10, true>;
else reshaper = reshapeImagePacked<int32_t, UnpackerR11G11B10, false>;
break;
default:
LOG(ERROR)
<< "DataReshaper: INT dst, unsupported srcType: " << (int) srcType;
return false;
}
break;
case UINT:
switch (srcType) {
case UBYTE: reshaper = reshapeImage<uint32_t, uint8_t>; break;
case FLOAT: reshaper = reshapeImage<uint32_t, float>; break;
case INT: reshaper = reshapeImage<uint32_t, int32_t>; break;
case UINT: reshaper = reshapeImage<uint32_t, uint32_t>; break;
default: return false;
case UBYTE: reshaper = reshapeImageImpl<uint32_t, uint8_t>; break;
case FLOAT: reshaper = reshapeImageImpl<uint32_t, float>; break;
case INT: reshaper = reshapeImageImpl<uint32_t, int32_t>; break;
case UINT: reshaper = reshapeImageImpl<uint32_t, uint32_t>; break;
case UINT_10F_11F_11F_REV:
if (swizzle) reshaper = reshapeImagePacked<uint32_t, UnpackerR11G11B10, true>;
else reshaper = reshapeImagePacked<uint32_t, UnpackerR11G11B10, false>;
break;
default:
LOG(ERROR)
<< "DataReshaper: UINT dst, unsupported srcType: " << (int) srcType;
return false;
}
break;
case HALF:
switch (srcType) {
case HALF: reshaper = copyImage; break;
default: return false;
case HALF:
reshaper = copyImage;
break;
case UINT_10F_11F_11F_REV:
if (swizzle) reshaper = reshapeImagePacked<math::half, UnpackerR11G11B10, true>;
else reshaper = reshapeImagePacked<math::half, UnpackerR11G11B10, false>;
break;
default:
LOG(ERROR)
<< "DataReshaper: HALF dst, unsupported srcType: " << (int) srcType;
return false;
}
break;
default:
LOG(ERROR) << "DataReshaper: unsupported dst->type: " << (int) dst->type;
return false;
}
uint8_t* dstBytes = (uint8_t*) dst->buffer;
const int dstBytesPerRow = PixelBufferDescriptor::computeDataSize(dst->format, dst->type,
dst->stride ? dst->stride : width, 1, dst->alignment);
reshaper(dstBytes, srcBytes, srcBytesPerRow, srcChannelCount,
dst->top, dst->left, dstBytesPerRow,
dstChannelCount, width, height, swizzle);
reshaper(dstBytes, srcBytes, srcBytesPerRow, srcChannelCount, dst->top, dst->left,
dstBytesPerRow, dstChannelCount, width, height, swizzle);
return true;
}
};
template<> inline float getMaxValue() { return 1.0f; }
template<> inline int32_t getMaxValue() { return 0x7fffffff; }
template<> inline uint32_t getMaxValue() { return 0xffffffff; }
template<> inline uint16_t getMaxValue() { return 0x3c00; } // 0x3c00 is 1.0 in half-float.
template<> inline uint8_t getMaxValue() { return 0xff; }
} // namespace backend
} // namespace filament

View File

@@ -25,6 +25,11 @@
#include <mutex>
#ifdef __ANDROID__
#include <pthread.h>
#endif
namespace filament {
using namespace utils;
@@ -105,8 +110,21 @@ JNIEnv* VirtualMachineEnv::getEnvironmentSlow() {
FILAMENT_CHECK_PRECONDITION(mVirtualMachine)
<< "JNI_OnLoad() has not been called";
#if defined(__ANDROID__)
jint const result = mVirtualMachine->AttachCurrentThread(&mJniEnv, nullptr);
#ifdef __ANDROID__
JavaVMAttachArgs args;
args.version = JNI_VERSION_1_6;
args.group = nullptr;
char threadName[16]; // pthread_getname_np returns at most 16 bytes
if (__builtin_available(android 26, *)) {
if (pthread_getname_np(pthread_self(), threadName, sizeof(threadName)) == 0) {
args.name = threadName;
} else {
args.name = nullptr;
}
} else {
args.name = nullptr;
}
jint const result = mVirtualMachine->AttachCurrentThread(&mJniEnv, &args);
#else
jint const result = mVirtualMachine->AttachCurrentThread(reinterpret_cast<void**>(&mJniEnv), nullptr);
#endif

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