Compare commits

..
Author SHA1 Message Date
Powei Feng c5462621eb vk: adding logs just for debugging [DO NOT COMMIT] 2026-03-18 10:15:56 -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 e5fe3d495e Release Filament 1.70.0 2026-03-10 13:43:15 -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 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]andPowei Feng 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
SiyuandMathias Agopian 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
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 AgopianandPowei Feng 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 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 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
268 changed files with 7245 additions and 2857 deletions
+17 -15
View File
@@ -27,17 +27,24 @@ runs:
echo "$HASH" > /tmp/commit_hash.txt echo "$HASH" > /tmp/commit_hash.txt
- name: Find commit message (PR) - name: Find commit message (PR)
shell: bash shell: bash
id: checkout_code
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
run: | run: |
BEFORE_HASH=$(git rev-parse HEAD) PR_NUMBER="${{ github.event.pull_request.number }}"
echo "hash=$BEFORE_HASH" >> "$GITHUB_OUTPUT" # Fetch the head of the PR explicitly to handle forks. Depth 50 ensures we can traverse past recent merge commits.
# Next we will checkout the actual head (not the merge commits) of the PR git fetch --depth=50 origin "pull/$PR_NUMBER/head:pr-head"
AFTER_HASH="${{ github.event.pull_request.head.sha }}"
git checkout $AFTER_HASH COMMIT_HASH=$(git log -1 --no-merges pr-head --format=%H)
COMMIT_MESSAGE=$(git log -1 --no-merges) AUTHOR_NAME=$(git log -1 --no-merges pr-head --format=%an)
echo "$COMMIT_MESSAGE" > /tmp/commit_msg.txt AUTHOR_EMAIL=$(git log -1 --no-merges pr-head --format=%ae)
echo "$AFTER_HASH" > /tmp/commit_hash.txt 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 - shell: bash
id: action_output id: action_output
run: | run: |
@@ -47,9 +54,4 @@ runs:
cat /tmp/commit_msg.txt >> "$GITHUB_OUTPUT" cat /tmp/commit_msg.txt >> "$GITHUB_OUTPUT"
echo "$DELIMITER" >> "$GITHUB_OUTPUT" echo "$DELIMITER" >> "$GITHUB_OUTPUT"
# Get the commit hash # Get the commit hash
echo "hash=$(cat /tmp/commit_hash.txt)" >> "$GITHUB_OUTPUT" 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 }}
+1 -12
View File
@@ -11,24 +11,13 @@ runs:
echo "set man-db/auto-update false" | sudo debconf-communicate echo "set man-db/auto-update false" | sudo debconf-communicate
sudo dpkg-reconfigure man-db 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 .. cd ..
sudo wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - sudo wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
sudo apt-get update 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 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 mesa-common-dev libxi-dev libxxf86vm-dev
sudo apt-get install cmake ninja-build
# For dawn # For dawn
sudo apt-get install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libx11-xcb-dev sudo apt-get install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libx11-xcb-dev
@@ -0,0 +1,26 @@
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
run: |
# Must have at least clang-16 for a webgpu/dawn build.
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
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
@@ -1,4 +1,4 @@
FROM python:3.10.4 FROM python:3.12.13
RUN pip3 install pygithub==1.55 RUN pip3 install pygithub==1.55
+1 -1
View File
@@ -8,7 +8,7 @@ runs:
uses: actions/cache@v4 # Use a specific version uses: actions/cache@v4 # Use a specific version
with: with:
path: emsdk path: emsdk
key: ${{ runner.os }}-emsdk-${{ env.GITHUB_EMSDK_VERSION }} key: ${{ runner.os }}-${{ runner.arch }}-emsdk-${{ env.GITHUB_EMSDK_VERSION }}
- name: Install Web Prerequisites - name: Install Web Prerequisites
shell: bash shell: bash
run: | run: |
+33 -8
View File
@@ -10,16 +10,27 @@ jobs:
# a branch on filament-assets. # a branch on filament-assets.
update-renderdiff-goldens: update-renderdiff-goldens:
name: update-renderdiff-goldens name: update-renderdiff-goldens
runs-on: 'ubuntu-24.04-4core' runs-on: 'macos-14'
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- id: get_commit_msg - id: get_commit_msg
uses: ./.github/actions/get-commit-msg uses: ./.github/actions/get-commit-msg
- name: Build diffimg - name: Check if accepting new goldens
run: ./build.sh release diffimg 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 - name: Run update script
env: env:
GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }} GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }}
@@ -27,10 +38,24 @@ jobs:
run: | run: |
GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py) GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py)
COMMIT_HASH="${{ steps.get_commit_msg.outputs.hash }}" 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 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 "branch==${GOLDEN_BRANCH}"
echo "hash==${COMMIT_HASH}" echo "hash==${COMMIT_HASH}"
python3 test/renderdiff/src/update_golden.py --branch=${GOLDEN_BRANCH} \ python3 test/renderdiff/src/update_golden.py --branch=${GOLDEN_BRANCH} \
@@ -65,7 +90,7 @@ jobs:
# filament-assets # filament-assets
update-sizeguard: update-sizeguard:
name: update-sizeguard name: update-sizeguard
runs-on: 'ubuntu-24.04-16core' runs-on: 'ubuntu-24.04-4core'
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
+8 -16
View File
@@ -12,8 +12,7 @@ jobs:
name: build-android name: build-android
# We intentially use a larger runner here to enable larger disk space # We intentially use a larger runner here to enable larger disk space
# (standard linux runner will fail on disk space and faster build time). # (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: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -26,8 +25,7 @@ jobs:
build-ios: build-ios:
name: build-ios name: build-ios
runs-on: macos-14-xlarge runs-on: 'macos-14'
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -46,8 +44,7 @@ jobs:
build-linux: build-linux:
name: build-linux name: build-linux
runs-on: 'ubuntu-24.04-16core' runs-on: 'arm-ubuntu-24.04-16core'
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -58,13 +55,12 @@ jobs:
cd build/linux && printf "y" | ./build.sh continuous cd build/linux && printf "y" | ./build.sh continuous
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: filament-linux name: filament-arm-linux
path: out/filament-release-linux.tgz path: out/filament-release-arm-linux.tgz
build-mac: build-mac:
name: build-mac name: build-mac
runs-on: macos-14-xlarge runs-on: 'macos-14'
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -77,14 +73,10 @@ jobs:
with: with:
name: filament-mac name: filament-mac
path: out/filament-release-darwin.tgz path: out/filament-release-darwin.tgz
- name: Check public headers
run: |
test/check-headers/test.sh out/release/filament/include
build-web: build-web:
name: build-web name: build-web
runs-on: 'ubuntu-24.04-16core' runs-on: 'arm-ubuntu-24.04-16core'
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -101,7 +93,7 @@ jobs:
build-windows: build-windows:
name: build-windows name: build-windows
runs-on: windows-2022-32core runs-on: 'windows-2022'
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
+186 -21
View File
@@ -8,10 +8,54 @@ on:
branches: branches:
- main - 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: 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: build-desktop-mac:
name: build-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: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -20,13 +64,30 @@ jobs:
- name: Run build script - name: Run build script
run: | run: |
cd build/mac && printf "y" | ./build.sh presubmit-with-test cd build/mac && printf "y" | ./build.sh presubmit-with-test
- name: Test material parser - name: Test - material parser
run: | run: |
out/cmake-release/filament/test/test_material_parser 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: build-desktop-linux:
name: build-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: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -41,7 +102,18 @@ jobs:
build-windows: build-windows:
name: 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: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -53,7 +125,18 @@ jobs:
build-android: build-android:
name: 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: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -80,7 +163,18 @@ jobs:
build-ios: build-ios:
name: 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: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -95,7 +189,18 @@ jobs:
build-web: build-web:
name: 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: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -108,7 +213,18 @@ jobs:
validate-docs: validate-docs:
name: 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: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -121,23 +237,40 @@ jobs:
test-renderdiff: test-renderdiff:
name: 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: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
fetch-depth: 0 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 - id: get_commit_msg
uses: ./.github/actions/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: | run: |
# Must have at least clang-16 for a webgpu/dawn build. if echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py --mode=accept_new_goldens; then
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer echo "accept=true" >> "$GITHUB_OUTPUT"
else
echo "accept=false" >> "$GITHUB_OUTPUT"
fi
shell: bash shell: bash
- name: Renderdiff Generate
if: steps.check_accept.outputs.accept != 'true'
uses: ./.github/actions/renderdiff-generate
- name: Render and compare - name: Render and compare
if: steps.check_accept.outputs.accept != 'true'
id: render_compare id: render_compare
env: env:
COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }} COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }}
@@ -146,9 +279,6 @@ jobs:
source ${TEST_DIR}/src/preamble.sh source ${TEST_DIR}/src/preamble.sh
set -eux set -eux
GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 ${TEST_DIR}/src/commit_msg.py) GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 ${TEST_DIR}/src/commit_msg.py)
bash ${TEST_DIR}/generate.sh
# Build diffimg tool
./build.sh release diffimg
python3 ${TEST_DIR}/src/golden_manager.py \ python3 ${TEST_DIR}/src/golden_manager.py \
--branch=${GOLDEN_BRANCH} \ --branch=${GOLDEN_BRANCH} \
@@ -172,10 +302,12 @@ jobs:
fi fi
shell: bash shell: bash
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: steps.check_accept.outputs.accept != 'true'
with: with:
name: presubmit-renderdiff-result name: presubmit-renderdiff-result
path: ./out/renderdiff path: ./out/renderdiff
- name: Compare result - name: Compare result
if: steps.check_accept.outputs.accept != 'true'
run: | run: |
ERROR_STR="${{ steps.render_compare.outputs.err }}" ERROR_STR="${{ steps.render_compare.outputs.err }}"
if [ -n "${ERROR_STR}" ]; then if [ -n "${ERROR_STR}" ]; then
@@ -185,7 +317,18 @@ jobs:
validate-wgsl-webgpu: validate-wgsl-webgpu:
name: validate-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: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -199,6 +342,17 @@ jobs:
test-code-correctness: test-code-correctness:
name: test-code-correctness name: test-code-correctness
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: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
@@ -219,7 +373,18 @@ jobs:
test-backend: test-backend:
name: 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: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
with: with:
+82 -1
View File
@@ -63,6 +63,39 @@ jobs:
const globber = await glob.create('out/*.tgz'); const globber = await glob.create('out/*.tgz');
await upload({ github, context }, await globber.glob(), TAG); 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: build-mac:
name: build-mac name: build-mac
runs-on: macos-14-xlarge runs-on: macos-14-xlarge
@@ -99,7 +132,7 @@ jobs:
build-web: build-web:
name: 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' if: github.event_name == 'release' || github.event.inputs.platform == 'web'
steps: steps:
@@ -188,6 +221,54 @@ jobs:
const globber = await glob.create(['out/*.aar', 'out/*.apk', 'out/*.tgz'].join('\n')); const globber = await glob.create(['out/*.aar', 'out/*.apk', 'out/*.tgz'].join('\n'));
await upload({ github, context }, await globber.glob(), TAG); 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: build-ios:
name: build-ios name: build-ios
runs-on: macos-14-xlarge runs-on: macos-14-xlarge
+4 -7
View File
@@ -11,14 +11,11 @@ jobs:
name: Verify Release Notes name: Verify Release Notes
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# We *only* need to check out the .github/ directory. - uses: actions/checkout@v6
# 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
with: with:
files: .github fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }} sparse-checkout: |
.github
- name: Verify release notes - name: Verify release notes
uses: ./.github/actions/verify-release-notes uses: ./.github/actions/verify-release-notes
with: with:
+10 -3
View File
@@ -326,11 +326,13 @@ endif()
if (FILAMENT_ENABLE_LTO) if (FILAMENT_ENABLE_LTO)
include(CheckIPOSupported) include(CheckIPOSupported)
check_ipo_supported(RESULT IPO_SUPPORT) check_ipo_supported(RESULT IPO_SUPPORT OUTPUT IPO_ERROR)
if (IPO_SUPPORT) if (IPO_SUPPORT)
message(STATUS "LTO support is enabled") message(STATUS "IPO / LTO is enabled")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
else()
message(WARNING "IPO / LTO is not supported by this architecture: ${IPO_ERROR}")
endif() endif()
endif() endif()
@@ -867,6 +869,12 @@ endfunction()
# Sub-projects # 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 # Common to all platforms
add_subdirectory(${EXTERNAL}/libgtest/tnt) add_subdirectory(${EXTERNAL}/libgtest/tnt)
add_subdirectory(${LIBRARIES}/camutils) add_subdirectory(${LIBRARIES}/camutils)
@@ -902,7 +910,6 @@ add_subdirectory(${EXTERNAL}/cgltf/tnt)
add_subdirectory(${EXTERNAL}/draco/tnt) add_subdirectory(${EXTERNAL}/draco/tnt)
add_subdirectory(${EXTERNAL}/jsmn/tnt) add_subdirectory(${EXTERNAL}/jsmn/tnt)
add_subdirectory(${EXTERNAL}/stb/tnt) add_subdirectory(${EXTERNAL}/stb/tnt)
add_subdirectory(${EXTERNAL}/getopt)
add_subdirectory(${EXTERNAL}/perfetto/tnt) add_subdirectory(${EXTERNAL}/perfetto/tnt)
add_subdirectory(${EXTERNAL}/basisu/tnt) add_subdirectory(${EXTERNAL}/basisu/tnt)
-2
View File
@@ -6,5 +6,3 @@
appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md). appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md).
## Release notes for next branch cut ## Release notes for next branch cut
- engine: fix crash when using variance shadow maps
+2 -2
View File
@@ -31,7 +31,7 @@ repositories {
} }
dependencies { dependencies {
implementation 'com.google.android.filament:filament-android:1.69.3' implementation 'com.google.android.filament:filament-android:1.70.0'
} }
``` ```
@@ -50,7 +50,7 @@ Here are all the libraries available in the group `com.google.android.filament`:
iOS projects can use CocoaPods to install the latest release: iOS projects can use CocoaPods to install the latest release:
```shell ```shell
pod 'Filament', '~> 1.69.3' pod 'Filament', '~> 1.70.0'
``` ```
## Documentation ## Documentation
+12
View File
@@ -7,6 +7,18 @@ 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 Instead, if you are authoring a PR for the main branch, add your release note to
[NEW_RELEASE_NOTES.md](./NEW_RELEASE_NOTES.md). [NEW_RELEASE_NOTES.md](./NEW_RELEASE_NOTES.md).
## 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.4
+8 -2
View File
@@ -40,15 +40,21 @@
// - Build and upload artifacts with ./gradlew publish // - Build and upload artifacts with ./gradlew publish
// - Close and release staging repo on Nexus with ./gradlew closeAndReleaseStagingRepository // - 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 // sonatypeUsername=nexus_user
// sonatypePassword=nexus_password // sonatypePassword=nexus_password
// //
// To sign with a key ring file:
// signing.keyId=pgp_key_id // signing.keyId=pgp_key_id
// signing.password=pgp_key_password // signing.password=pgp_key_password
// signing.secretKeyRingFile=/Users/user/.gnupg/maven_signing.key // 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 { buildscript {
def path = providers def path = providers
@@ -194,7 +200,7 @@ subprojects {
google() google()
} }
if (!name.startsWith("sample") && name != "filament-tools") { if (!name.startsWith("sample") && name != "filament-tools" && name != "gradle-plugin") {
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
-21
View File
@@ -1,21 +0,0 @@
plugins {
id 'groovy-gradle-plugin'
}
gradlePlugin {
plugins {
create("filament-plugin") {
id = "filament-plugin"
implementationClass = "com.google.android.filament.gradle.FilamentPlugin"
}
}
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation "com.google.gradle:osdetector-gradle-plugin:1.7.3"
}
@@ -2121,7 +2121,7 @@ public class View {
*/ */
public boolean highPrecision = false; public boolean highPrecision = false;
/** /**
* VSM minimum variance scale, must be positive. * @deprecated has no effect.
*/ */
public float minVarianceScale = 0.5f; public float minVarianceScale = 0.5f;
/** /**
@@ -20,8 +20,6 @@
#include <imagediff/ImageDiff.h> #include <imagediff/ImageDiff.h>
#include <utils/Log.h> #include <utils/Log.h>
#include <vector>
using namespace imagediff; using namespace imagediff;
using namespace utils; using namespace utils;
@@ -102,30 +100,48 @@ jobject createResult(JNIEnv* env, ImageDiffResult const& result, bool generateDi
if (generateDiff && result.diffImage.getWidth() > 0) { if (generateDiff && result.diffImage.getWidth() > 0) {
jclass bitmapClass = env->FindClass("android/graphics/Bitmap"); 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;"); "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
jclass configClass = env->FindClass("android/graphics/Bitmap$Config"); jclass configClass = env->FindClass("android/graphics/Bitmap$Config");
jfieldID argb8888 = env->GetStaticFieldID(configClass, "ARGB_8888", "Landroid/graphics/Bitmap$Config;"); jfieldID argb8888 = env->GetStaticFieldID(configClass, "ARGB_8888", "Landroid/graphics/Bitmap$Config;");
jobject configObj = env->GetStaticObjectField(configClass, argb8888); jobject configObj = env->GetStaticObjectField(configClass, argb8888);
uint32_t width = result.diffImage.getWidth(); uint32_t width = result.diffImage.getWidth();
uint32_t height = result.diffImage.getHeight(); 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) { 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; void* diffPixels;
if (AndroidBitmap_lockPixels(env, diffBitmap, &diffPixels) == 0) { if (AndroidBitmap_lockPixels(env, diffBitmap, &diffPixels) == 0) {
AndroidBitmapInfo info;
AndroidBitmap_getInfo(env, diffBitmap, &info);
float const* src = result.diffImage.getPixelRef(); float const* src = result.diffImage.getPixelRef();
uint8_t* dst = (uint8_t*) diffPixels; uint8_t* dst = (uint8_t*) diffPixels;
uint32_t channels = result.diffImage.getChannels(); // usually 4 uint32_t const channels = result.diffImage.getChannels(); // usually 4
for (size_t i = 0; i < width * height; ++i) { for (size_t y = 0; y < height; ++y) {
for (int c = 0; c < 4; ++c) { uint8_t* row = dst + y * info.stride;
float v = 0.0f; for (size_t x = 0; x < width; ++x) {
if (c < channels) v = src[i * channels + c]; size_t srcIdx = (y * width + x) * channels;
if (c == 3 && channels < 4) v = 1.0f; // Alpha 1.0 if missing for (int c = 0; c < 4; ++c) {
dst[i * 4 + c] = (uint8_t) std::min(255.0f, std::max(0.0f, v * 255.0f)); 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); AndroidBitmap_unlockPixels(env, diffBitmap);
@@ -133,7 +149,7 @@ jobject createResult(JNIEnv* env, ImageDiffResult const& result, bool generateDi
} }
} }
} }
return resultObj; return resultObj;
} }
@@ -147,7 +163,7 @@ Java_com_google_android_filament_utils_ImageDiff_nCompareBasic(JNIEnv* env, jcla
BitmapLock maskArg(env, maskBitmap); BitmapLock maskArg(env, maskBitmap);
if (!refArg.isValid() || !candArg.isValid()) { if (!refArg.isValid() || !candArg.isValid()) {
ImageDiffResult emptyResult; ImageDiffResult emptyResult;
emptyResult.status = ImageDiffResult::Status::SIZE_MISMATCH; // or ERROR emptyResult.status = ImageDiffResult::Status::SIZE_MISMATCH; // or ERROR
return createResult(env, emptyResult, false); 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 extern "C" JNIEXPORT jobject JNICALL
Java_com_google_android_filament_utils_ImageDiff_nCompareJson(JNIEnv* env, jclass, Java_com_google_android_filament_utils_ImageDiff_nCompareJson(JNIEnv* env, jclass,
jobject refBitmap, jobject candBitmap, jstring jsonConfig, jobject maskBitmap) { jobject refBitmap, jobject candBitmap, jstring jsonConfig, jobject maskBitmap) {
BitmapLock refArg(env, refBitmap); BitmapLock refArg(env, refBitmap);
BitmapLock candArg(env, candBitmap); BitmapLock candArg(env, candBitmap);
BitmapLock maskArg(env, maskBitmap); BitmapLock maskArg(env, maskBitmap);
if (!refArg.isValid() || !candArg.isValid()) { if (!refArg.isValid() || !candArg.isValid()) {
ImageDiffResult emptyResult; ImageDiffResult emptyResult;
emptyResult.status = ImageDiffResult::Status::SIZE_MISMATCH; // or ERROR emptyResult.status = ImageDiffResult::Status::SIZE_MISMATCH; // or ERROR
return createResult(env, emptyResult, false); return createResult(env, emptyResult, false);
} }
@@ -189,7 +205,7 @@ Java_com_google_android_filament_utils_ImageDiff_nCompareJson(JNIEnv* env, jclas
ImageDiffConfig config; ImageDiffConfig config;
const char* nativeJson = env->GetStringUTFChars(jsonConfig, 0); const char* nativeJson = env->GetStringUTFChars(jsonConfig, 0);
size_t length = env->GetStringUTFLength(jsonConfig); size_t length = env->GetStringUTFLength(jsonConfig);
bool parsed = parseConfig(nativeJson, length, &config); bool parsed = parseConfig(nativeJson, length, &config);
env->ReleaseStringUTFChars(jsonConfig, nativeJson); env->ReleaseStringUTFChars(jsonConfig, nativeJson);
@@ -214,4 +230,3 @@ Java_com_google_android_filament_utils_ImageDiff_nCompareJson(JNIEnv* env, jclas
return createResult(env, result, generateDiff); return createResult(env, result, generateDiff);
} }
@@ -106,8 +106,8 @@ class ModelViewer(
var skyboxCubemap: Texture? = null var skyboxCubemap: Texture? = null
private lateinit var displayHelper: DisplayHelper private lateinit var displayHelper: DisplayHelper
private lateinit var cameraManipulator: Manipulator private var cameraManipulator: Manipulator? = null
private lateinit var gestureDetector: GestureDetector private var gestureDetector: GestureDetector? = null
private var surfaceView: SurfaceView? = null private var surfaceView: SurfaceView? = null
private var textureView: TextureView? = null private var textureView: TextureView? = null
@@ -157,15 +157,13 @@ class ModelViewer(
surfaceView: SurfaceView, surfaceView: SurfaceView,
engine: Engine = Engine.create(), engine: Engine = Engine.create(),
uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK), uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK),
manipulator: Manipulator? = null manipulator: Manipulator? = defaultCameraManipulator(surfaceView.width, surfaceView.height)
) : this(engine, uiHelper) { ) : 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 this.surfaceView = surfaceView
gestureDetector = GestureDetector(surfaceView, cameraManipulator) cameraManipulator = manipulator
cameraManipulator?.let { c ->
gestureDetector = GestureDetector(surfaceView, c)
}
displayHelper = DisplayHelper(surfaceView.context) displayHelper = DisplayHelper(surfaceView.context)
uiHelper.renderCallback = SurfaceCallback() uiHelper.renderCallback = SurfaceCallback()
uiHelper.attachTo(surfaceView) uiHelper.attachTo(surfaceView)
@@ -177,15 +175,14 @@ class ModelViewer(
textureView: TextureView, textureView: TextureView,
engine: Engine = Engine.create(), engine: Engine = Engine.create(),
uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK), uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK),
manipulator: Manipulator? = null manipulator: Manipulator? = defaultCameraManipulator(textureView.width, textureView.height)
) : this(engine, uiHelper) { ) : this(engine, uiHelper) {
cameraManipulator = manipulator ?: Manipulator.Builder() cameraManipulator = manipulator
.targetPosition(kDefaultObjectPosition.x, kDefaultObjectPosition.y, kDefaultObjectPosition.z)
.viewport(textureView.width, textureView.height)
.build(Manipulator.Mode.ORBIT)
this.textureView = textureView this.textureView = textureView
gestureDetector = GestureDetector(textureView, cameraManipulator) cameraManipulator = manipulator
cameraManipulator?.let { c ->
gestureDetector = GestureDetector(textureView, c)
}
displayHelper = DisplayHelper(textureView.context) displayHelper = DisplayHelper(textureView.context)
uiHelper.renderCallback = SurfaceCallback() uiHelper.renderCallback = SurfaceCallback()
uiHelper.attachTo(textureView) uiHelper.attachTo(textureView)
@@ -302,11 +299,13 @@ class ModelViewer(
asset?.let { populateScene(it) } asset?.let { populateScene(it) }
// Extract the camera basis from the helper and push it to the Filament camera. // Extract the camera basis from the helper and push it to the Filament camera.
cameraManipulator.getLookAt(eyePos, target, upward) cameraManipulator?.let { cm ->
camera.lookAt( cm.getLookAt(eyePos, target, upward)
camera.lookAt(
eyePos[0], eyePos[1], eyePos[2], eyePos[0], eyePos[1], eyePos[2],
target[0], target[1], target[2], target[0], target[1], target[2],
upward[0], upward[1], upward[2]) upward[0], upward[1], upward[2])
}
// Render the scene, unless the renderer wants to skip the frame. // Render the scene, unless the renderer wants to skip the frame.
if (renderer.beginFrame(swapChain!!, frameTimeNanos)) { if (renderer.beginFrame(swapChain!!, frameTimeNanos)) {
@@ -398,7 +397,7 @@ class ModelViewer(
* Handles a [MotionEvent] to enable one-finger orbit, two-finger pan, and pinch-to-zoom. * Handles a [MotionEvent] to enable one-finger orbit, two-finger pan, and pinch-to-zoom.
*/ */
fun onTouchEvent(event: MotionEvent) { fun onTouchEvent(event: MotionEvent) {
gestureDetector.onTouchEvent(event) gestureDetector?.onTouchEvent(event)
} }
@SuppressWarnings("ClickableViewAccessibility") @SuppressWarnings("ClickableViewAccessibility")
@@ -451,7 +450,7 @@ class ModelViewer(
override fun onResized(width: Int, height: Int) { override fun onResized(width: Int, height: Int) {
view.viewport = Viewport(0, 0, width, height) view.viewport = Viewport(0, 0, width, height)
cameraManipulator.setViewport(width, height) cameraManipulator?.setViewport(width, height)
updateCameraProjection() updateCameraProjection()
synchronizePendingFrames(engine) synchronizePendingFrames(engine)
} }
@@ -468,5 +467,11 @@ class ModelViewer(
companion object { companion object {
private val kDefaultObjectPosition = Float3(0.0f, 0.0f, -4.0f) 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)
}
} }
} }
@@ -25,7 +25,7 @@ configured, the corresponding task will be disabled.
```groovy ```groovy
plugins { plugins {
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
filament { filament {
+31
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"
}
@@ -60,9 +60,10 @@ class ToolsLocator {
def classifier = def classifier =
project.extensions.getByType(com.google.gradle.osdetector.OsDetector).classifier project.extensions.getByType(com.google.gradle.osdetector.OsDetector).classifier
// If com.google.android.filament.tools-dir is set, we'll use it as the tool's base path. // 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") def toolsDirProp = project.providers.gradleProperty("com.google.android.filament.tools-dir")
if (toolsDirProp.isPresent()) { if (toolsDirProp.isPresent() && !toolsDirProp.get().trim().isEmpty()) {
def toolsDir = toolsDirProp.get() def toolsDir = toolsDirProp.get()
def path = OperatingSystem.current().isWindows() ? def path = OperatingSystem.current().isWindows() ?
"${toolsDir}/bin/${name}.exe" : "${toolsDir}/bin/${name}.exe" :
+1 -1
View File
@@ -1,5 +1,5 @@
GROUP=com.google.android.filament GROUP=com.google.android.filament
VERSION_NAME=1.69.3 VERSION_NAME=1.70.0
POM_DESCRIPTION=Real-time physically based rendering engine for Android. POM_DESCRIPTION=Real-time physically based rendering engine for Android.
+7
View File
@@ -94,6 +94,13 @@ afterEvaluate { project ->
} }
signing { 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 -> publishing.publications.all { publication ->
sign publication sign publication
} }
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
+1 -1
View File
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -26,7 +26,8 @@ tasks.register('copyDamagedHelmetGltf', Copy) {
preBuild.dependsOn copyDamagedHelmetGltf preBuild.dependsOn copyDamagedHelmetGltf
clean.doFirst { clean.doFirst {
delete "src/main/assets" delete "src/main/assets/envs"
delete "src/main/assets/models"
} }
android { android {
@@ -48,6 +49,7 @@ android {
dependencies { dependencies {
implementation deps.kotlin implementation deps.kotlin
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.10.0'
implementation deps.coroutines.core implementation deps.coroutines.core
implementation project(':filament-android') implementation project(':filament-android')
implementation project(':gltfio-android') implementation project(':gltfio-android')
@@ -12,10 +12,11 @@
android:supportsRtl="true" android:supportsRtl="true"
android:largeHeap="true" android:largeHeap="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:theme="@android:style/Theme.NoTitleBar"> android:theme="@style/Theme.Material3.DayNight.NoActionBar">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:screenOrientation="fullSensor"> android:screenOrientation="fullSensor">
<intent-filter> <intent-filter>
@@ -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
}
}
]
}
@@ -17,36 +17,35 @@
package com.google.android.filament.validation package com.google.android.filament.validation
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.Html
import android.util.Log import android.util.Log
import android.view.Choreographer import android.view.Choreographer
import android.view.SurfaceView import android.view.SurfaceView
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ScrollView import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import com.google.android.filament.utils.KTX1Loader
import com.google.android.filament.utils.ModelViewer import com.google.android.filament.utils.ModelViewer
import com.google.android.filament.utils.Utils import com.google.android.filament.utils.Utils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import com.google.android.filament.utils.KTX1Loader
import com.google.android.filament.IndirectLight
import com.google.android.filament.Skybox
import android.graphics.Color
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.nio.ByteBuffer import java.nio.ByteBuffer
import android.widget.ArrayAdapter import java.text.SimpleDateFormat
import android.widget.Button import java.util.Date
import android.widget.Spinner import java.util.Locale
import android.widget.AdapterView
class MainActivity : Activity(), ValidationRunner.Callback { class MainActivity : Activity(), ValidationRunner.Callback {
@@ -62,13 +61,33 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private lateinit var choreographer: Choreographer private lateinit var choreographer: Choreographer
private lateinit var modelViewer: ModelViewer private lateinit var modelViewer: ModelViewer
private lateinit var statusTextView: TextView private lateinit var statusTextView: TextView
private lateinit var testResultsHeader: TextView
private lateinit var resultsContainer: LinearLayout private lateinit var resultsContainer: LinearLayout
private lateinit var inputManager: ValidationInputManager private lateinit var inputManager: ValidationInputManager
private var currentInput: ValidationInputManager.ValidationInput? = null private var currentInput: ValidationInputManager.ValidationInput? = null
private lateinit var modeSpinner: Spinner
private lateinit var runButton: Button
private var resultManager: ValidationResultManager? = 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 private var validationRunner: ValidationRunner? = null
// Frame callback // Frame callback
@@ -89,25 +108,70 @@ class MainActivity : Activity(), ValidationRunner.Callback {
surfaceView.holder.setFixedSize(512, 512) surfaceView.holder.setFixedSize(512, 512)
statusTextView = findViewById(R.id.status_text) statusTextView = findViewById(R.id.status_text)
modeSpinner = findViewById(R.id.mode_spinner) testResultsHeader = findViewById(R.id.test_results_header)
runButton = findViewById(R.id.run_button)
resultsContainer = findViewById(R.id.results_container) resultsContainer = findViewById(R.id.results_container)
// Setup Spinner runButton = findViewById(R.id.run_button)
val modes = arrayOf("Run Validation", "Generate Goldens") loadButton = findViewById(R.id.load_button)
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, modes) optionsButton = findViewById(R.id.options_button)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) enhancementContainer = findViewById(R.id.enhancement_container)
modeSpinner.adapter = adapter 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 // Setup Run Button
runButton.setOnClickListener { runButton.setOnClickListener {
currentInput?.let { input -> currentInput?.let { input ->
val generateGoldens = modeSpinner.selectedItemPosition == 1 // Always use the generateGoldens flag from the intent/input
val newInput = input.copy(generateGoldens = generateGoldens) startValidation(input)
startValidation(newInput)
} }
} }
// 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) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
choreographer = Choreographer.getInstance() choreographer = Choreographer.getInstance()
@@ -120,6 +184,135 @@ class MainActivity : Activity(), ValidationRunner.Callback {
handleIntent() handleIntent()
} }
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 {
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() { private fun createIndirectLight() {
try { try {
val engine = modelViewer.engine val engine = modelViewer.engine
@@ -156,12 +349,18 @@ class MainActivity : Activity(), ValidationRunner.Callback {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
try { try {
val input = inputManager.resolveConfig(intent) val input = inputManager.resolveConfig(intent)
// Update header
Log.i(TAG, "handleIntent: Setting header to: Test Results: ${input.config.name}")
testResultsHeader.text = "${input.config.name}"
currentInput = input currentInput = input
// Sync spinner with intent if (input.autoRun) {
modeSpinner.setSelection(if (input.generateGoldens) 1 else 0) startValidation(input)
} else {
startValidation(input) // Just show status
statusTextView.text = "Ready: ${input.config.name}"
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to resolve config", e) Log.e(TAG, "Failed to resolve config", e)
statusTextView.text = "Error: ${e.message}" statusTextView.text = "Error: ${e.message}"
@@ -172,9 +371,13 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private fun startValidation(input: ValidationInputManager.ValidationInput) { private fun startValidation(input: ValidationInputManager.ValidationInput) {
try { try {
resultsContainer.removeAllViews() resultsContainer.removeAllViews()
diffImageViews.clear()
enhancementSlider.isEnabled = false
Log.i(TAG, "Starting validation with config: ${input.config.name}") Log.i(TAG, "Starting validation with config: ${input.config.name}")
Log.i(TAG, "Output dir: ${input.outputDir.absolutePath}") Log.i(TAG, "Output dir: ${input.outputDir.absolutePath}")
testResultsHeader.text = "${input.config.name}"
resultManager = ValidationResultManager(input.outputDir) resultManager = ValidationResultManager(input.outputDir)
validationRunner = ValidationRunner(this, modelViewer, input.config, resultManager!!) validationRunner = ValidationRunner(this, modelViewer, input.config, resultManager!!)
@@ -182,9 +385,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
validationRunner?.generateGoldens = input.generateGoldens validationRunner?.generateGoldens = input.generateGoldens
validationRunner?.start() validationRunner?.start()
// Sync spinner in case it was called programmatically or changed implicitly
modeSpinner.setSelection(if (input.generateGoldens) 1 else 0)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to start validation", e) Log.e(TAG, "Failed to start validation", e)
statusTextView.text = "Error: ${e.message}" statusTextView.text = "Error: ${e.message}"
@@ -196,6 +396,12 @@ class MainActivity : Activity(), ValidationRunner.Callback {
choreographer.postFrameCallback(frameScheduler) choreographer.postFrameCallback(frameScheduler)
} }
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleIntent()
}
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
choreographer.removeFrameCallback(frameScheduler) choreographer.removeFrameCallback(frameScheduler)
@@ -221,19 +427,44 @@ class MainActivity : Activity(), ValidationRunner.Callback {
resultContainer.orientation = LinearLayout.VERTICAL resultContainer.orientation = LinearLayout.VERTICAL
resultContainer.setPadding(0, 10, 0, 20) resultContainer.setPadding(0, 10, 0, 20)
// Header // Header Layout
val resultView = TextView(this) val headerRow = LinearLayout(this)
resultView.text = "${result.testName}: ${if(result.passed) "PASS" else "FAIL"} (Diff: ${result.diffMetric})" headerRow.orientation = LinearLayout.HORIZONTAL
resultView.setTextColor(if(result.passed) Color.GREEN else Color.RED) headerRow.gravity = android.view.Gravity.CENTER_VERTICAL
resultView.textSize = 16f
resultView.setTypeface(null, android.graphics.Typeface.BOLD) // Status Icon + Name
resultContainer.addView(resultView) 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 // Images Row
val imagesRow = LinearLayout(this) val imagesRow = LinearLayout(this)
imagesRow.orientation = LinearLayout.HORIZONTAL imagesRow.orientation = LinearLayout.HORIZONTAL
fun addImage(label: String, bitmap: Bitmap?) { 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) { if (bitmap != null) {
val container = LinearLayout(this) val container = LinearLayout(this)
container.orientation = LinearLayout.VERTICAL container.orientation = LinearLayout.VERTICAL
@@ -241,7 +472,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
val labelView = TextView(this) val labelView = TextView(this)
labelView.text = label labelView.text = label
labelView.textSize = 12f labelView.textSize = 9f
container.addView(labelView) container.addView(labelView)
val iv = ImageView(this) val iv = ImageView(this)
@@ -249,16 +480,29 @@ class MainActivity : Activity(), ValidationRunner.Callback {
iv.layoutParams = LinearLayout.LayoutParams(250, 250) // Smaller thumbnails iv.layoutParams = LinearLayout.LayoutParams(250, 250) // Smaller thumbnails
iv.scaleType = ImageView.ScaleType.FIT_CENTER iv.scaleType = ImageView.ScaleType.FIT_CENTER
iv.setBackgroundColor(0xFF404040.toInt()) iv.setBackgroundColor(0xFF404040.toInt())
if (isDiff) {
diffImageViews.add(iv)
applyEnhancementToView(iv, globalEnhancementFactor)
}
iv.setOnClickListener {
showImageDialog(testImages, label)
}
container.addView(iv) container.addView(iv)
imagesRow.addView(container) imagesRow.addView(container)
} }
} }
addImage("Rendered", currentRenderedBitmap) addImage("Rendered", currentRenderedBitmap, false)
addImage("Golden", currentGoldenBitmap) addImage("Golden", currentGoldenBitmap, false)
if (!result.passed) { if (!result.passed) {
addImage("Diff", currentDiffBitmap) addImage("Diff", currentDiffBitmap, true)
}
if (currentAlphaDiffBitmap != null) {
addImage("Alpha Diff", currentAlphaDiffBitmap, true)
} }
resultContainer.addView(imagesRow) resultContainer.addView(imagesRow)
@@ -268,13 +512,54 @@ class MainActivity : Activity(), ValidationRunner.Callback {
currentRenderedBitmap = null currentRenderedBitmap = null
currentGoldenBitmap = null currentGoldenBitmap = null
currentDiffBitmap = null currentDiffBitmap = null
currentAlphaDiffBitmap = null
} }
} }
override fun onAllTestsFinished() { override fun onAllTestsFinished() {
runOnUiThread { runOnUiThread {
statusTextView.text = "All tests finished!" statusTextView.text = "All tests finished!"
Log.i(TAG, "All tests finished") enhancementSlider.isEnabled = true
Log.i(TAG, "All tests finished " + if (currentInput?.autoExport == true) "Exporting bundle" else "x")
if (currentInput?.autoExport == true) {
exportTestBundleAction()
}
if (currentInput?.autoExportResults == true) {
exportTestResultsAction()
}
}
}
private fun exportTestBundleAction() {
currentInput?.let { input ->
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: ValidationResultManager(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 ?: ValidationResultManager(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")
}
} }
} }
@@ -297,54 +582,165 @@ class MainActivity : Activity(), ValidationRunner.Callback {
"Diff" -> { "Diff" -> {
currentDiffBitmap = bitmap currentDiffBitmap = bitmap
} }
"Alpha Diff" -> {
currentAlphaDiffBitmap = bitmap
}
} }
} }
} }
}
/* private fun applyEnhancementToView(iv: ImageView, factor: Float) {
* Scripts for reference: val cm = android.graphics.ColorMatrix()
* cm.setScale(factor, factor, factor, 1.0f)
* generate_goldens.sh: iv.colorFilter = android.graphics.ColorMatrixColorFilter(cm)
* -------------------- }
* #!/bin/bash
* set -e private fun applyGlobalEnhancement() {
* for (iv in diffImageViews) {
* # Config path (on device) applyEnhancementToView(iv, globalEnhancementFactor)
* CONFIG_PATH=$1 }
* if [ -z "$CONFIG_PATH" ]; then }
* echo "Usage: $0 <device_config_path>"
* echo "Example: $0 /sdcard/Android/data/com.google.android.filament.validation/files/default_test.json" private fun showImageDialog(images: TestImages, initialLabel: String) {
* exit 1 val dialogView = layoutInflater.inflate(R.layout.dialog_image_viewer, null)
* fi val dialog = AlertDialog.Builder(this)
* .setView(dialogView)
* echo "Starting Golden Generation for $CONFIG_PATH..." .create()
* adb shell am force-stop com.google.android.filament.validation
* adb shell am start -n com.google.android.filament.validation/.MainActivity \ val titleView = dialogView.findViewById<TextView>(R.id.dialog_title)
* -e test_config "$CONFIG_PATH" \ val typeView = dialogView.findViewById<TextView>(R.id.dialog_image_type)
* --ez generate_goldens true val imageView = dialogView.findViewById<ImageView>(R.id.dialog_image)
* val btnClose = dialogView.findViewById<View>(R.id.btn_close)
* echo "Check device or logcat for progress." val btnReset = dialogView.findViewById<View>(R.id.btn_reset)
* echo "adb logcat -s FilamentValidation:I ValidationRunner:I" val btnPrev = dialogView.findViewById<View>(R.id.btn_prev)
* echo "To pull results: ./samples/sample-render-validation/pull_goldens.sh" val btnNext = dialogView.findViewById<View>(R.id.btn_next)
*
* pull_goldens.sh: val enhancementContainer = dialogView.findViewById<View>(R.id.dialog_enhancement_container)
* ---------------- val enhancementLabel = dialogView.findViewById<TextView>(R.id.dialog_enhancement_label)
* #!/bin/bash val enhancementSlider = dialogView.findViewById<android.widget.SeekBar>(R.id.dialog_enhancement_slider)
* set -e
* titleView.text = images.testName
* # Default destination is local golden directory relative to script
* SCRIPT_DIR=$(cd $(dirname $0); pwd) val availableImages = mutableListOf<Pair<String, Bitmap>>()
* DEST_DIR=${1:-"$SCRIPT_DIR/golden"} images.rendered?.let { availableImages.add(Pair("Rendered", it)) }
* images.golden?.let { availableImages.add(Pair("Golden", it)) }
* echo "Pulling goldens to $DEST_DIR..." images.diff?.let { availableImages.add(Pair("Diff", it)) }
* mkdir -p "$DEST_DIR" images.alphaDiff?.let { availableImages.add(Pair("Alpha Diff", it)) }
*
* # Path on device if (availableImages.isEmpty()) return
* DEVICE_GOLDEN_DIR="/storage/emulated/0/Android/data/com.google.android.filament.validation/files/golden/."
* var currentIndex = availableImages.indexOfFirst { it.first == initialLabel }
* adb pull "$DEVICE_GOLDEN_DIR" "$DEST_DIR" if (currentIndex == -1) currentIndex = 0
*
* echo "Done." var currentDialogEnhancement = globalEnhancementFactor
* ls -l "$DEST_DIR"
*/ 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()
}
}
@@ -26,6 +26,7 @@ import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.zip.ZipFile
/** /**
* Handles the retrieval and preparation of test configuration and assets. * Handles the retrieval and preparation of test configuration and assets.
@@ -42,7 +43,11 @@ class ValidationInputManager(private val context: Context) {
data class ValidationInput( data class ValidationInput(
val config: RenderTestConfig, val config: RenderTestConfig,
val outputDir: File, val outputDir: File,
val generateGoldens: Boolean val generateGoldens: Boolean,
val autoRun: Boolean = false,
val autoExport: Boolean = false,
val autoExportResults: Boolean = false,
val sourceZip: File? = null
) )
/** /**
@@ -53,22 +58,111 @@ class ValidationInputManager(private val context: Context) {
val testConfigPath = intent.getStringExtra("test_config") val testConfigPath = intent.getStringExtra("test_config")
val urlConfig = intent.getStringExtra("url_config") val urlConfig = intent.getStringExtra("url_config")
val urlModelsBase = intent.getStringExtra("url_models_base") val urlModelsBase = intent.getStringExtra("url_models_base")
val zipPath = intent.getStringExtra("zip_path")
val generateGoldens = intent.getBooleanExtra("generate_goldens", false) 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") val outputPath = intent.getStringExtra("output_path")
val outputDir = if (outputPath != null) { Log.i(TAG, "Resolving config with outputPath: $outputPath")
File(outputPath).apply { mkdirs() }
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 { } else {
File(context.getExternalFilesDir(null), "validation_results").apply { mkdirs() } 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 { val config = when {
sourceZipFile != null && sourceZipFile.exists() -> loadFromZip(sourceZipFile)
urlConfig != null -> downloadConfig(urlConfig, urlModelsBase) urlConfig != null -> downloadConfig(urlConfig, urlModelsBase)
testConfigPath != null -> ConfigParser.parseFromPath(testConfigPath) testConfigPath != null -> ConfigParser.parseFromPath(testConfigPath)
else -> extractDefaultAssets() else -> extractDefaultAssets()
} }
return@withContext ValidationInput(config, outputDir, generateGoldens) 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 { private suspend fun extractDefaultAssets(): RenderTestConfig {
@@ -90,9 +184,9 @@ class ValidationInputManager(private val context: Context) {
// Copy DamagedHelmet.glb // Copy DamagedHelmet.glb
val modelsDir = File(filesDir, "models") val modelsDir = File(filesDir, "models")
modelsDir.mkdirs() modelsDir.mkdirs()
val modelOut = File(modelsDir, "DamagedHelmet.glb") val modelOut = File(modelsDir, "helmet.glb")
assetManager.open("DamagedHelmet.glb").use { input -> assetManager.open("models/helmet.glb").use { input ->
FileOutputStream(modelOut).use { output -> FileOutputStream(modelOut).use { output ->
input.copyTo(output) input.copyTo(output)
} }
@@ -67,22 +67,201 @@ class ValidationResultManager(private val outputDir: File) {
fun finalizeResults(): File? { fun finalizeResults(): File? {
// Write results JSON // Write results JSON
writeResultsJson() writeResultsJson()
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}")
// Zip results
val zipFile = File(outputDir, "results.zip")
try { try {
ZipOutputStream(FileOutputStream(zipFile)).use { zos -> ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
outputDir.walkTopDown().filter { it.isFile && it.name != "results.zip" }.forEach { file -> // 1. Add results.json
val entryName = file.relativeTo(outputDir).path val resultsJson = File(outputDir, "results.json")
zos.putNextEntry(ZipEntry(entryName)) if (resultsJson.exists()) {
file.inputStream().use { it.copyTo(zos) } 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 diff images (any file ending in _diff.png in outputDir)
outputDir.listFiles { _, name -> name.endsWith("_diff.png") }?.forEach { diffFile ->
zos.putNextEntry(ZipEntry(diffFile.name))
diffFile.inputStream().use { it.copyTo(zos) }
zos.closeEntry() zos.closeEntry()
} }
} }
Log.i(TAG, "Zipped results to ${zipFile.absolutePath}") Log.i(TAG, "Exported results to ${zipFile.absolutePath}")
return zipFile return zipFile
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to zip results", e) 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 return null
} }
} }
@@ -41,18 +41,13 @@ class ValidationRunner(
private var currentTestConfig: TestConfig? = null private var currentTestConfig: TestConfig? = null
private var currentModelName: String? = 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 frameCounter = 0
enum class State { enum class State {
IDLE, IDLE,
LOADING_MODEL,
WAITING_FOR_FENCE,
WAITING_FOR_RESOURCES, WAITING_FOR_RESOURCES,
WARMUP, WARMUP,
RUNNING_TEST, RUNNING_TEST
COMPARING
} }
interface Callback { interface Callback {
@@ -93,8 +88,6 @@ class ValidationRunner(
nextModel() nextModel()
return return
} }
currentState = State.LOADING_MODEL
callback?.onStatusChanged("Loading $modelName for ${currentTestConfig?.name}") callback?.onStatusChanged("Loading $modelName for ${currentTestConfig?.name}")
// Load model on main thread (required by ModelViewer) // Load model on main thread (required by ModelViewer)
@@ -110,51 +103,20 @@ class ValidationRunner(
Log.i("ValidationRunner", "Loading GLB buffer... (${bytes.size} bytes)") Log.i("ValidationRunner", "Loading GLB buffer... (${bytes.size} bytes)")
val buffer = ByteBuffer.wrap(bytes) val buffer = ByteBuffer.wrap(bytes)
modelViewer.loadModelGlb(buffer) modelViewer.loadModelGlb(buffer)
Log.i("ValidationRunner", "Model loaded. initializing fence.") Log.i("ValidationRunner", "Model loaded.")
modelViewer.transformToUnitCube() modelViewer.transformToUnitCube()
loadStartFence = modelViewer.engine.createFence() currentState = State.WAITING_FOR_RESOURCES
loadStartTime = System.nanoTime() frameCounter = 0
currentState = State.WAITING_FOR_FENCE Log.i("ValidationRunner", "State set to WAITING_FOR_RESOURCES")
frameCounter = 0 // Reset for fence timeout tracking
Log.i("ValidationRunner", "State set to WAITING_FOR_FENCE")
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ValidationRunner", "Failed to load $path", e) Log.e("ValidationRunner", "Failed to load $path", e)
nextModel() nextModel()
} }
} }
fun onFrame(frameTimeNanos: Long) { fun onFrame(frameTimeNanos: Long) {
if (frameCounter % 60 == 0) {
Log.i("ValidationRunner", "onFrame: $currentState (frame: $frameCounter)")
}
when (currentState) { when (currentState) {
State.IDLE -> {} State.IDLE -> {}
State.WAITING_FOR_FENCE -> {
frameCounter++
if (frameCounter > 600) {
Log.w("ValidationRunner", "Fence timed out after 600 frames! Forcing proceed.")
modelViewer.engine.destroyFence(loadStartFence!!)
loadStartFence = null
currentState = State.WAITING_FOR_RESOURCES
return
}
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 ...
}
currentState = State.WAITING_FOR_RESOURCES
}
}
}
State.WAITING_FOR_RESOURCES -> { State.WAITING_FOR_RESOURCES -> {
val progress = modelViewer.progress val progress = modelViewer.progress
if (progress >= 1.0f) { if (progress >= 1.0f) {
@@ -166,12 +128,11 @@ class ValidationRunner(
State.WARMUP -> { State.WARMUP -> {
frameCounter++ frameCounter++
if (frameCounter > 5) { // 5 frames warmup if (frameCounter > 5) { // 5 frames warmup
startAutomation() startAutomation()
} }
} }
State.RUNNING_TEST -> { State.RUNNING_TEST -> {
// Log.i("ValidationRunner", "Running test...") currentEngine?.let { engine ->
currentEngine?.let { engine ->
val content = AutomationEngine.ViewerContent() val content = AutomationEngine.ViewerContent()
content.view = modelViewer.view content.view = modelViewer.view
content.renderer = modelViewer.renderer content.renderer = modelViewer.renderer
@@ -186,13 +147,11 @@ class ValidationRunner(
if (engine.shouldClose()) { if (engine.shouldClose()) {
Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)") Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)")
// Test finished (for this spec) // Test finished (for this spec)
currentState = State.COMPARING currentState = State.IDLE
captureAndCompare() captureAndCompare()
} }
} }
} }
State.COMPARING -> {} // Busy
State.LOADING_MODEL -> {}
} }
} }
@@ -233,7 +192,36 @@ class ValidationRunner(
// Golden path // Golden path
val modelFile = File(config.models.get(modelName)!!) val modelFile = File(config.models.get(modelName)!!)
val goldenFile = modelFile.parentFile!!.parentFile!!.resolve("golden/${testFullName}.png") 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 { Thread {
try { try {
@@ -245,14 +233,15 @@ class ValidationRunner(
var diffMetric = 0f var diffMetric = 0f
if (generateGoldens) { if (generateGoldens) {
goldenFile.parentFile?.mkdirs() val targetGolden = goldenFile ?: modelParent.parentFile?.resolve("golden/${testFullName}.png") ?: File(modelParent, "golden/${testFullName}.png")
FileOutputStream(goldenFile).use { out -> targetGolden.parentFile?.mkdirs()
FileOutputStream(targetGolden).use { out ->
flipped.compress(Bitmap.CompressFormat.PNG, 100, out) flipped.compress(Bitmap.CompressFormat.PNG, 100, out)
} }
passed = true // Generating goldens always passes if successful passed = true // Generating goldens always passes if successful
callback?.onStatusChanged("Golden generated") callback?.onStatusChanged("Golden generated")
} else { } else {
if (goldenFile.exists()) { if (goldenFile != null && goldenFile.exists()) {
val golden = android.graphics.BitmapFactory.decodeFile(goldenFile.absolutePath) val golden = android.graphics.BitmapFactory.decodeFile(goldenFile.absolutePath)
if (golden != null) { if (golden != null) {
callback?.onImageResult("Golden", golden) callback?.onImageResult("Golden", golden)
@@ -263,17 +252,61 @@ class ValidationRunner(
passed = (result.status == ImageDiff.Result.Status.PASSED) passed = (result.status == ImageDiff.Result.Status.PASSED)
diffMetric = result.failingPixelCount.toFloat() diffMetric = result.failingPixelCount.toFloat()
if (!passed) { if (!passed) {
if (result.diffImage != null) { if (result.diffImage != null) {
callback?.onImageResult("Diff", result.diffImage!!) val diffImg = result.diffImage!!
resultManager.saveImage("${testFullName}_diff", 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 { } else {
callback?.onStatusChanged("Failed to load golden") callback?.onStatusChanged("Failed to load golden")
} }
} else { } else {
Log.w("ValidationRunner", "Golden not found: ${goldenFile.absolutePath}") Log.w("ValidationRunner", "Golden not found: ${goldenFile?.absolutePath}")
callback?.onStatusChanged("Golden not found") callback?.onStatusChanged("Golden not found")
} }
} }
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -10,6 +10,8 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1" app:layout_constraintDimensionRatio="1:1"
app:layout_constraintWidth_percent="0.6"
android:layout_marginTop="32dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
@@ -20,10 +22,110 @@
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</FrameLayout> </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 <ScrollView
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/surface_container" app:layout_constraintTop_toBottomOf="@id/controls_container"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
@@ -32,41 +134,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="20dp"> android:paddingStart="20dp"
android:paddingEnd="20dp"
<TextView android:paddingBottom="20dp">
android:id="@+id/status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Initializing..."
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<Spinner
android:id="@+id/mode_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/run_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Run" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Test Results"
android:textSize="18sp"
android:paddingTop="20dp"
android:paddingBottom="10dp" />
<LinearLayout <LinearLayout
android:id="@+id/results_container" android:id="@+id/results_container"
@@ -74,7 +144,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" /> android:orientation="vertical" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
@@ -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>
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'filament-plugin' id 'com.google.android.filament-tools'
} }
project.ext.isSample = true project.ext.isSample = true
+5
View File
@@ -1,3 +1,8 @@
// Filament tools plugin
pluginManagement {
includeBuild 'gradle-plugin'
}
// Libraries // Libraries
include ':filament-android' include ':filament-android'
include ':filamat-android' include ':filamat-android'
+6 -6
View File
@@ -235,9 +235,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@@ -429,9 +429,9 @@
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==" "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="
}, },
"minimatch": { "minimatch": {
"version": "3.1.2", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@@ -46,6 +46,7 @@ list(SORT NDK_VERSIONS)
list(GET NDK_VERSIONS -1 NDK_VERSION) list(GET NDK_VERSIONS -1 NDK_VERSION)
get_filename_component(NDK_VERSION ${NDK_VERSION} NAME) 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(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 # specify the cross compiler
set(COMPILER_SUFFIX) set(COMPILER_SUFFIX)
+1
View File
@@ -47,6 +47,7 @@ list(SORT NDK_VERSIONS)
list(GET NDK_VERSIONS -1 NDK_VERSION) list(GET NDK_VERSIONS -1 NDK_VERSION)
get_filename_component(NDK_VERSION ${NDK_VERSION} NAME) 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(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 # specify the cross compiler
set(COMPILER_SUFFIX) set(COMPILER_SUFFIX)
+1
View File
@@ -46,6 +46,7 @@ list(SORT NDK_VERSIONS)
list(GET NDK_VERSIONS -1 NDK_VERSION) list(GET NDK_VERSIONS -1 NDK_VERSION)
get_filename_component(NDK_VERSION ${NDK_VERSION} NAME) 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(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 # specify the cross compiler
set(COMPILER_SUFFIX) set(COMPILER_SUFFIX)
@@ -46,6 +46,7 @@ list(SORT NDK_VERSIONS)
list(GET NDK_VERSIONS -1 NDK_VERSION) list(GET NDK_VERSIONS -1 NDK_VERSION)
get_filename_component(NDK_VERSION ${NDK_VERSION} NAME) 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(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 # specify the cross compiler
set(COMPILER_SUFFIX) set(COMPILER_SUFFIX)
+2 -2
View File
@@ -181,7 +181,7 @@ important for <code>matc</code> (material compiler).</p>
} }
dependencies { dependencies {
implementation 'com.google.android.filament:filament-android:1.69.3' implementation 'com.google.android.filament:filament-android:1.70.0'
} }
</code></pre> </code></pre>
<p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p> <p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p>
@@ -195,7 +195,7 @@ dependencies {
</div> </div>
<h3 id="ios"><a class="header" href="#ios">iOS</a></h3> <h3 id="ios"><a class="header" href="#ios">iOS</a></h3>
<p>iOS projects can use CocoaPods to install the latest release:</p> <p>iOS projects can use CocoaPods to install the latest release:</p>
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.69.3' <pre><code class="language-shell">pod 'Filament', '~&gt; 1.70.0'
</code></pre> </code></pre>
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2> <h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
<ul> <ul>
+23 -1
View File
@@ -225,9 +225,10 @@ into <strong>branch</strong> of <code>filament-assets</code>. This branch is pai
<code>filament</code> repo.</p> <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 <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 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> <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> <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. <li>Run interactive mode in the <code>update_golden.py</code> script.
<pre><code>python3 test/renderdiff/src/update_golden.py <pre><code>python3 test/renderdiff/src/update_golden.py
</code></pre> </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> <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> to the <code>main</code> branch of the golden repo.</li>
</ul> </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> <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 <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 be used by pointing your browser to a localhost port. If you input the viewer with a PR or a
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -93,6 +93,7 @@ set(SRCS
src/MorphTargetBuffer.cpp src/MorphTargetBuffer.cpp
src/PostProcessManager.cpp src/PostProcessManager.cpp
src/ProgramSpecialization.cpp src/ProgramSpecialization.cpp
src/LocalProgramCache.cpp
src/RenderPass.cpp src/RenderPass.cpp
src/RenderPrimitive.cpp src/RenderPrimitive.cpp
src/RenderTarget.cpp src/RenderTarget.cpp
@@ -186,6 +187,7 @@ set(PRIVATE_HDRS
src/MaterialCache.h src/MaterialCache.h
src/MaterialDefinition.h src/MaterialDefinition.h
src/MaterialParser.h src/MaterialParser.h
src/LocalProgramCache.h
src/MaterialInstanceManager.h src/MaterialInstanceManager.h
src/PIDController.h src/PIDController.h
src/PostProcessManager.h src/PostProcessManager.h
+152 -153
View File
@@ -564,170 +564,169 @@ endif()
# ================================================================================================== # ==================================================================================================
# Test # 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) if (APPLE OR LINUX)
set(BACKEND_TEST_SRC set(BACKEND_TEST_SRC
test/BackendTest.cpp test/BackendTest.cpp
test/ShaderGenerator.cpp test/ShaderGenerator.cpp
test/TrianglePrimitive.cpp test/TrianglePrimitive.cpp
test/Arguments.cpp test/Arguments.cpp
test/ImageExpectations.cpp test/ImageExpectations.cpp
test/Lifetimes.cpp test/Lifetimes.cpp
test/PlatformRunner.cpp test/PlatformRunner.cpp
test/Shader.cpp test/Shader.cpp
test/SharedShaders.cpp test/SharedShaders.cpp
test/Skip.cpp test/Skip.cpp
test/test_Autoresolve.cpp test/test_Autoresolve.cpp
test/test_FeedbackLoops.cpp test/test_FeedbackLoops.cpp
test/test_Blit.cpp test/test_Blit.cpp
test/test_MissingRequiredAttributes.cpp test/test_MissingRequiredAttributes.cpp
test/test_ReadPixels.cpp test/test_ReadPixels.cpp
test/test_ReadTexture.cpp test/test_ReadTexture.cpp
test/test_BufferUpdates.cpp test/test_BufferUpdates.cpp
test/test_Callbacks.cpp test/test_Callbacks.cpp
test/test_JobQueue.cpp test/test_JobQueue.cpp
test/test_MemoryMappedBuffer.cpp test/test_MemoryMappedBuffer.cpp
test/test_MsaaSwapChain.cpp test/test_MsaaSwapChain.cpp
test/test_MRT.cpp test/test_MRT.cpp
test/test_PushConstants.cpp test/test_PushConstants.cpp
test/test_LoadImage.cpp test/test_LoadImage.cpp
test/test_StencilBuffer.cpp test/test_StencilBuffer.cpp
test/test_Scissor.cpp test/test_Scissor.cpp
test/test_MipLevels.cpp test/test_MipLevels.cpp
test/test_Handles.cpp test/test_Handles.cpp
test/test_CircularBuffer.cpp test/test_CircularBuffer.cpp
test/test_CommandBufferQueue.cpp test/test_CommandBufferQueue.cpp
test/test_Template.cpp test/test_Template.cpp
) test/test_Platform.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
) )
endif() if (FILAMENT_SUPPORTS_WEBGPU)
set(BACKEND_TEST_LIBS list(APPEND BACKEND_TEST_SRC
absl::str_format test/test_WebGPUAsyncTaskCounter.cpp
backend test/test_WebGPUComposeSwizzle.cpp)
getopt endif()
gtest if (APPLE)
imageio # Metal-specific tests
filamat list(APPEND BACKEND_TEST_SRC
SPIRV test/test_MetalBlitter.mm
spirv-cross-glsl) )
# Create input/output directories for test result images. endif()
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/actual_images) set(BACKEND_TEST_LIBS
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/diff_images) absl::str_format
file(COPY test/expected_images DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/images) backend
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
gtest gtest
glslang imageio
spirv-cross-core filamat
spirv-cross-glsl SPIRV
spirv-cross-msl) spirv-cross-glsl)
# Create input/output directories for test result images.
if (NOT IOS) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/actual_images)
target_link_libraries(backend_test PRIVATE image imageio) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/diff_images)
list(APPEND BACKEND_TEST_DEPS image) file(COPY test/expected_images DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/images)
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() endif()
set(BACKEND_TEST_COMBINED_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libbackendtest_combined.a") # TODO: Disabling IOS test due to breakage wrt glslang update
combine_static_libs(backend_test "${BACKEND_TEST_COMBINED_OUTPUT}" "${BACKEND_TEST_DEPS}") 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) if (NOT IOS)
install(FILES "${BACKEND_TEST_COMBINED_OUTPUT}" DESTINATION lib/${DIST_DIR} RENAME ${BACKEND_TEST_LIB_NAME}) target_link_libraries(backend_test PRIVATE image imageio)
install(FILES test/PlatformRunner.h DESTINATION include/backend_test) 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() 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) if (APPLE AND NOT IOS)
add_executable(backend_test_mac test/mac_runner.mm) add_executable(metal_utils_test test/MetalTest.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 target_compile_options(metal_utils_test PRIVATE "-fobjc-arc")
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_link_libraries(metal_utils_test PRIVATE
backend
gtest
)
set_target_properties(metal_utils_test PROPERTIES FOLDER Tests)
endif() endif()
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()
+23 -1
View File
@@ -216,6 +216,18 @@ public:
MULTIVIEW, 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 * This controls the priority level for GPU work scheduling, which helps prioritize the
* submitted GPU work and enables preemption. * submitted GPU work and enables preemption.
@@ -316,7 +328,7 @@ public:
*/ */
StereoscopicType stereoscopicType = StereoscopicType::NONE; StereoscopicType stereoscopicType = StereoscopicType::NONE;
/* /**
* The number of eyes to render when stereoscopic rendering is enabled. Supported values are * The number of eyes to render when stereoscopic rendering is enabled. Supported values are
* between 1 and Engine::getMaxStereoscopicEyes() (inclusive). * between 1 and Engine::getMaxStereoscopicEyes() (inclusive).
*/ */
@@ -377,6 +389,16 @@ public:
*/ */
virtual int getOSVersion() const noexcept = 0; 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), * Creates and initializes the low-level API (e.g. an OpenGL context or Vulkan instance),
* then creates the concrete Driver. * then creates the concrete Driver.
@@ -52,6 +52,9 @@ protected:
~OpenGLPlatform() noexcept override; ~OpenGLPlatform() noexcept override;
utils::CString getDeviceInfo(DeviceInfoType infoType,
Driver* UTILS_NULLABLE driver) const noexcept override;
public: public:
struct ExternalTexture { struct ExternalTexture {
unsigned int target; // GLenum target unsigned int target; // GLenum target
@@ -70,6 +73,12 @@ public:
*/ */
static utils::CString getRendererString(Driver const* UTILS_NONNULL driver); 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 * 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 * or buffers from initialization. This is for instance where `eglDestroyContext` would be
@@ -159,6 +159,12 @@ protected:
EGLConfig getEglConfig() const noexcept { return mEGLConfig; } EGLConfig getEglConfig() const noexcept { return mEGLConfig; }
EGLConfig getSuitableConfigForSwapChain(uint64_t flags, bool window, bool pbuffer) const; 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 // supported extensions detected at runtime
struct { struct {
struct { struct {
@@ -38,6 +38,7 @@ public:
Driver* createDriver(void* sharedContext, const Platform::DriverConfig& driverConfig) override; Driver* createDriver(void* sharedContext, const Platform::DriverConfig& driverConfig) override;
int getOSVersion() const noexcept override { return 0; } 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. * Optionally initializes the Metal platform by acquiring resources necessary for rendering.
@@ -31,7 +31,6 @@
#include <cstring> #include <cstring>
#include <cstddef> #include <cstddef>
#include <functional> #include <functional>
#include <string>
#include <tuple> #include <tuple>
#include <unordered_set> #include <unordered_set>
@@ -69,7 +68,7 @@ public:
struct ExtensionHashFn { struct ExtensionHashFn {
std::size_t operator()(utils::CString const& s) const noexcept { 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. // Note: utils::CString::operator== has an edge case that breaks for the extension set.
@@ -143,6 +142,8 @@ public:
return 0; return 0;
} }
utils::CString getDeviceInfo(DeviceInfoType infoType, Driver* driver) const noexcept override;
// ---------------------------------------------------- // ----------------------------------------------------
// ---------- Platform Customization options ---------- // ---------- Platform Customization options ----------
struct Customization { struct Customization {
@@ -46,6 +46,9 @@ public:
~WebGPUPlatform() override = default; ~WebGPUPlatform() override = default;
[[nodiscard]] int getOSVersion() const noexcept final { return 0; } [[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; } [[nodiscard]] wgpu::Instance& getInstance() noexcept { return mInstance; }
@@ -27,8 +27,6 @@
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <functional>
namespace filament::backend { namespace filament::backend {
/* /*
@@ -66,7 +64,7 @@ public:
// all commands buffers (Slices) written to this point are returned by waitForCommand(). This // all commands buffers (Slices) written to this point are returned by waitForCommand(). This
// call blocks until the CircularBuffer has at least mRequiredSize bytes available. // call blocks until the CircularBuffer has at least mRequiredSize bytes available.
void flush(std::function<void(void*, void*)> const& debugPrintHistogram = nullptr); void flush();
// returns from waitForCommands() immediately. // returns from waitForCommands() immediately.
void requestExit(); void requestExit();
@@ -56,7 +56,6 @@ namespace filament::backend {
class CommandBase { class CommandBase {
static constexpr size_t FILAMENT_OBJECT_ALIGNMENT = alignof(std::max_align_t); static constexpr size_t FILAMENT_OBJECT_ALIGNMENT = alignof(std::max_align_t);
friend class CommandStream;
protected: protected:
using Execute = Dispatcher::Execute; using Execute = Dispatcher::Execute;
@@ -169,8 +168,8 @@ struct CommandType<void (Driver::*)(ARGS...)> {
class CustomCommand : public CommandBase { class CustomCommand : public CommandBase {
std::function<void()> mCommand; std::function<void()> mCommand;
public:
static void execute(Driver&, CommandBase* base, intptr_t* next); static void execute(Driver&, CommandBase* base, intptr_t* next);
public:
CustomCommand(CustomCommand&& rhs) = default; CustomCommand(CustomCommand&& rhs) = default;
explicit CustomCommand(std::function<void()> cmd) explicit CustomCommand(std::function<void()> cmd)
@@ -180,12 +179,11 @@ public:
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
class NoopCommand : public CommandBase { class NoopCommand : public CommandBase {
public:
intptr_t mNext; intptr_t mNext;
static void execute(Driver&, CommandBase* self, intptr_t* next) noexcept { static void execute(Driver&, CommandBase* self, intptr_t* next) noexcept {
*next = static_cast<NoopCommand*>(self)->mNext; *next = static_cast<NoopCommand*>(self)->mNext;
} }
public:
constexpr explicit NoopCommand(void* next) noexcept constexpr explicit NoopCommand(void* next) noexcept
: CommandBase(execute), mNext(intptr_t((char *)next - (char *)this)) { } : CommandBase(execute), mNext(intptr_t((char *)next - (char *)this)) { }
}; };
@@ -221,36 +219,6 @@ public:
CircularBuffer const& getCircularBuffer() const noexcept { return mCurrentBuffer; } CircularBuffer const& getCircularBuffer() const noexcept { return mCurrentBuffer; }
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
using Execute = Dispatcher::Execute;
struct CommandInfo {
size_t size;
const char* name;
int index;
};
std::unordered_map<Execute, CommandInfo> mCommands;
void initializeLookup() {
int currentIndex = 0;
#define DECL_DRIVER_API_SYNCHRONOUS(RetType, methodName, paramsDecl, params)
#define DECL_DRIVER_API(methodName, paramsDecl, params) \
mCommands[mDispatcher.methodName##_] = { CommandBase::align(sizeof(COMMAND_TYPE(methodName))), \
#methodName, currentIndex++ };
#define DECL_DRIVER_API_RETURN(RetType, methodName, paramsDecl, params) \
mCommands[mDispatcher.methodName##_] = { \
CommandBase::align(sizeof(COMMAND_TYPE(methodName##R))), #methodName, currentIndex++ \
};
#include "private/backend/DriverAPI.inc"
mCommands[CustomCommand::execute] = { CommandBase::align(sizeof(CustomCommand)),
"CustomCommand", currentIndex++ };
// NoopCommands have variable size. We will handle them specially using their mNext pointer.
mCommands[NoopCommand::execute] = { 0, "NoopCommand", currentIndex++ };
}
#endif
public: public:
#define DECL_DRIVER_API(methodName, paramsDecl, params) \ #define DECL_DRIVER_API(methodName, paramsDecl, params) \
inline void methodName(paramsDecl) noexcept { \ inline void methodName(paramsDecl) noexcept { \
@@ -295,13 +263,6 @@ public:
void execute(void* buffer); void execute(void* buffer);
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
void debugIterateCommands(void* head, void* tail,
std::function<void(CommandInfo const& info)> const& callback);
void debugPrintHistogram(void* head, void* tail);
#endif
/* /*
* queueCommand() allows to queue a lambda function as a command. * queueCommand() allows to queue a lambda function as a command.
* This is much less efficient than using the Driver* API. * This is much less efficient than using the Driver* API.
@@ -48,11 +48,6 @@
#define FILAMENT_DEBUG_COMMANDS FILAMENT_DEBUG_COMMANDS_NONE #define FILAMENT_DEBUG_COMMANDS FILAMENT_DEBUG_COMMANDS_NONE
// Upon command stream overflow, print a histogram of commands
#ifndef FILAMENT_DEBUG_COMMANDS_HISTOGRAM
#define FILAMENT_DEBUG_COMMANDS_HISTOGRAM 0
#endif
namespace filament::backend { namespace filament::backend {
class BufferDescriptor; class BufferDescriptor;
+1 -8
View File
@@ -79,7 +79,7 @@ bool CommandBufferQueue::isExitRequested() const {
} }
void CommandBufferQueue::flush(std::function<void(void*, void*)> const& debugPrintHistogram) { void CommandBufferQueue::flush() {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT); FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
CircularBuffer& circularBuffer = mCircularBuffer; CircularBuffer& circularBuffer = mCircularBuffer;
@@ -106,13 +106,6 @@ void CommandBufferQueue::flush(std::function<void(void*, void*)> const& debugPri
std::unique_lock lock(mLock); std::unique_lock lock(mLock);
// circular buffer is too small, we corrupted the stream // circular buffer is too small, we corrupted the stream
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
if (UTILS_VERY_UNLIKELY(used > mFreeSpace)) {
if (debugPrintHistogram) {
debugPrintHistogram(begin, end);
}
}
#endif
FILAMENT_CHECK_POSTCONDITION(used <= mFreeSpace) << FILAMENT_CHECK_POSTCONDITION(used <= mFreeSpace) <<
"Backend CommandStream overflow. Commands are corrupted and unrecoverable.\n" "Backend CommandStream overflow. Commands are corrupted and unrecoverable.\n"
"Please increase minCommandBufferSizeMB inside the Config passed to Engine::create.\n" "Please increase minCommandBufferSizeMB inside the Config passed to Engine::create.\n"
+1 -75
View File
@@ -30,14 +30,9 @@
#include <utils/ostream.h> #include <utils/ostream.h>
#include <utils/sstream.h> #include <utils/sstream.h>
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
#include <algorithm>
#include <vector>
#endif
#include <cstddef> #include <cstddef>
#include <functional> #include <functional>
#include <string> #include <string_view>
#include <utility> #include <utility>
#ifdef __ANDROID__ #ifdef __ANDROID__
@@ -88,10 +83,6 @@ CommandStream::CommandStream(Driver& driver, CircularBuffer& buffer) noexcept
__system_property_get("debug.filament.perfcounters", property); __system_property_get("debug.filament.perfcounters", property);
mUsePerformanceCounter = bool(atoi(property)); mUsePerformanceCounter = bool(atoi(property));
#endif #endif
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
initializeLookup();
#endif
} }
void CommandStream::execute(void* buffer) { void CommandStream::execute(void* buffer) {
@@ -135,71 +126,6 @@ void CommandStream::execute(void* buffer) {
} }
} }
#if FILAMENT_DEBUG_COMMANDS_HISTOGRAM
void CommandStream::debugIterateCommands(void* head, void* tail,
std::function<void(CommandInfo const& info)> const& callback) {
CommandBase* UTILS_RESTRICT base = static_cast<CommandBase*>(head);
auto p = base;
while (UTILS_LIKELY(p)) {
if (p >= tail) {
break;
}
Execute e = p->mExecute;
if (e == NoopCommand::execute) {
NoopCommand* noop = static_cast<NoopCommand*>(p);
size_t size = noop->mNext;
int noopIndex = mCommands[NoopCommand::execute].index;
callback({ size, "NoopCommand", noopIndex });
p = reinterpret_cast<CommandBase*>(reinterpret_cast<char*>(p) + size);
continue;
}
if (auto it = mCommands.find(e); it != mCommands.end()) {
size_t size = it->second.size;
callback(it->second);
p = reinterpret_cast<CommandBase*>(reinterpret_cast<char*>(p) + size);
} else {
LOG(ERROR) << "Cannot find command in lookup table";
return;
}
}
}
void CommandStream::debugPrintHistogram(void* head, void* tail) {
std::unordered_map<std::string_view, int> histogram;
std::unordered_map<int, int> index_histogram;
debugIterateCommands(head, tail, [&](CommandInfo const& info) {
histogram[std::string_view(info.name)]++;
index_histogram[info.index]++;
});
std::vector<std::pair<std::string_view, int>> sorted_histogram(histogram.begin(),
histogram.end());
std::sort(sorted_histogram.begin(), sorted_histogram.end(),
[](auto const& a, auto const& b) { return a.second > b.second; });
LOG(INFO) << "Command stream histogram:";
for (auto const& [name, count]: sorted_histogram) {
LOG(INFO) << name << ": " << count;
}
std::vector<std::pair<int, int>> sorted_index_histogram(index_histogram.begin(),
index_histogram.end());
std::sort(sorted_index_histogram.begin(), sorted_index_histogram.end(),
[](auto const& a, auto const& b) { return a.second > b.second; });
std::string short_histogram = "";
for (size_t i = 0, n = sorted_index_histogram.size(); i < n; ++i) {
short_histogram += std::to_string(sorted_index_histogram[i].first) + ":" +
std::to_string(sorted_index_histogram[i].second);
short_histogram += (i < n - 1) ? ";" : ".";
}
LOG(INFO) << "CS hist: " << short_histogram;
LOG(INFO) << "";
}
#endif
void CommandStream::queueCommand(std::function<void()> command) { void CommandStream::queueCommand(std::function<void()> command) {
new(allocateCommand(CustomCommand::align(sizeof(CustomCommand)))) CustomCommand(std::move(command)); new(allocateCommand(CustomCommand::align(sizeof(CustomCommand)))) CustomCommand(std::move(command));
} }
+20 -2
View File
@@ -25,6 +25,11 @@
#include <mutex> #include <mutex>
#ifdef __ANDROID__
#include <pthread.h>
#endif
namespace filament { namespace filament {
using namespace utils; using namespace utils;
@@ -105,8 +110,21 @@ JNIEnv* VirtualMachineEnv::getEnvironmentSlow() {
FILAMENT_CHECK_PRECONDITION(mVirtualMachine) FILAMENT_CHECK_PRECONDITION(mVirtualMachine)
<< "JNI_OnLoad() has not been called"; << "JNI_OnLoad() has not been called";
#if defined(__ANDROID__) #ifdef __ANDROID__
jint const result = mVirtualMachine->AttachCurrentThread(&mJniEnv, nullptr); 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 #else
jint const result = mVirtualMachine->AttachCurrentThread(reinterpret_cast<void**>(&mJniEnv), nullptr); jint const result = mVirtualMachine->AttachCurrentThread(reinterpret_cast<void**>(&mJniEnv), nullptr);
#endif #endif
+1
View File
@@ -26,6 +26,7 @@ class PlatformNoop final : public Platform {
public: public:
int getOSVersion() const noexcept final { return 0; } int getOSVersion() const noexcept final { return 0; }
utils::CString getDeviceInfo(DeviceInfoType, Driver*) const noexcept override { return {}; }
~PlatformNoop() noexcept override = default; ~PlatformNoop() noexcept override = default;
@@ -341,7 +341,44 @@ void GLDescriptorSet::bind(
if (arg.handle) { if (arg.handle) {
GLTexture const* const t = handleAllocator.handle_cast<GLTexture*>(arg.handle); GLTexture const* const t = handleAllocator.handle_cast<GLTexture*>(arg.handle);
gl.bindTexture(unit, t->gl.target, t->gl.id, t->gl.external); gl.bindTexture(unit, t->gl.target, t->gl.id, t->gl.external);
SamplerParams const params = arg.params; SamplerParams params = arg.params;
#if defined(__EMSCRIPTEN__)
// From https://registry.khronos.org/OpenGL-Refpages/es2.0/
// GLES 2.0 will draw mipmapped samplers as black if the following conditions are not met:
//
// "...if the width or height of a texture image are not powers of two and either the
// GL_TEXTURE_MIN_FILTER is set to one of the functions that requires mipmaps or the
// GL_TEXTURE_WRAP_S or GL_TEXTURE_WRAP_T is not set to GL_CLAMP_TO_EDGE, then the
// texture image unit will return (R, G, B, A) = (0, 0, 0, 1)."
//
// "If the texture has dimensions w × h , there are [floor(log2(max(w, h))) + 1] mipmap levels.
// Level 0 is the original texture; level [floor(log2(max(w, h)))] is the final 1 × 1 mipmap."
// "Suppose that a texture is accessed from a fragment shader or vertex shader and has set GL_TEXTURE_MIN_FILTER
// to one of the functions that requires mipmaps. If either the dimensions of the texture images currently
// defined (with previous calls to glTexImage2D, glCompressedTexImage2D, or glCopyTexImage2D) do not follow
// the proper sequence for mipmaps (described above), or there are fewer texture images defined than are needed,
// or the set of texture images were defined with different formats or types, then the texture image unit
// will return (R, G, B, A) = (0, 0, 0, 1)."
//
// So we force the sampler to a valid state in those cases.
auto isPowerOfTwo = [](uint32_t x) -> bool { return (x & (x - 1)) == 0 && x > 0; };
bool textureIsPowerOfTwo = isPowerOfTwo(t->width) && isPowerOfTwo(t->height);
uint32_t requiredMipLevels = (std::floor(std::log2(std::max(t->width, std::max(t->height, t->depth)))) + 1);
bool textureHasRequiredMipLevels = t->levels == requiredMipLevels;
bool samplerCanBeInvalid = params.filterMin > SamplerMinFilter::LINEAR;
if (samplerCanBeInvalid && (!textureIsPowerOfTwo || !textureHasRequiredMipLevels)) {
if (params.filterMin == SamplerMinFilter::NEAREST_MIPMAP_NEAREST
|| params.filterMin == SamplerMinFilter::LINEAR_MIPMAP_NEAREST) {
params.filterMin = SamplerMinFilter::NEAREST;
} else {
params.filterMin = SamplerMinFilter::LINEAR;
}
}
#endif
glTexParameteri(t->gl.target, GL_TEXTURE_MIN_FILTER, glTexParameteri(t->gl.target, GL_TEXTURE_MIN_FILTER,
(GLint)GLUtils::getTextureFilter(params.filterMin)); (GLint)GLUtils::getTextureFilter(params.filterMin));
glTexParameteri(t->gl.target, GL_TEXTURE_MAG_FILTER, glTexParameteri(t->gl.target, GL_TEXTURE_MAG_FILTER,
+1 -1
View File
@@ -91,7 +91,7 @@ public:
GLenum getIndicesType() const noexcept { GLenum getIndicesType() const noexcept {
return indicesType; return indicesType;
} }
} gl; };
static bool queryOpenGLVersion(GLint* major, GLint* minor) noexcept; static bool queryOpenGLVersion(GLint* major, GLint* minor) noexcept;
+5 -1
View File
@@ -716,7 +716,11 @@ Handle<HwSwapChain> OpenGLDriver::createSwapChainHeadlessS() noexcept {
} }
Handle<HwTimerQuery> OpenGLDriver::createTimerQueryS() noexcept { Handle<HwTimerQuery> OpenGLDriver::createTimerQueryS() noexcept {
return initHandle<GLTimerQuery>(); Handle<HwTimerQuery> tqh = initHandle<GLTimerQuery>();
// The state must be constructed here, as a synchronous call to getTimerQueryValue might happen
// before createTimerQueryR is executed on the backend thread.
handle_cast<GLTimerQuery*>(tqh)->state = std::make_shared<GLTimerQuery::State>();
return tqh;
} }
Handle<HwDescriptorSetLayout> OpenGLDriver::createDescriptorSetLayoutS() noexcept { Handle<HwDescriptorSetLayout> OpenGLDriver::createDescriptorSetLayoutS() noexcept {
@@ -250,6 +250,10 @@ private:
return utils::CString{ mContext.state.renderer }; return utils::CString{ mContext.state.renderer };
} }
utils::CString getVersionString() const noexcept override {
return utils::CString{ mContext.state.version };
}
JobQueue* getJobQueue() const noexcept { return mJobQueue.get(); } JobQueue* getJobQueue() const noexcept { return mJobQueue.get(); }
JobWorker* getJobWorker() const noexcept { return mJobWorker.get(); } JobWorker* getJobWorker() const noexcept { return mJobWorker.get(); }
@@ -34,6 +34,7 @@ protected:
public: public:
virtual utils::CString getVendorString() const noexcept = 0; virtual utils::CString getVendorString() const noexcept = 0;
virtual utils::CString getRendererString() const noexcept = 0; virtual utils::CString getRendererString() const noexcept = 0;
virtual utils::CString getVersionString() const noexcept = 0;
}; };
} // filament::backend } // filament::backend
@@ -45,6 +45,21 @@ Driver* OpenGLPlatform::createDefaultDriver(OpenGLPlatform* platform,
OpenGLPlatform::~OpenGLPlatform() noexcept = default; OpenGLPlatform::~OpenGLPlatform() noexcept = default;
utils::CString OpenGLPlatform::getDeviceInfo(DeviceInfoType infoType,
Driver* driver) const noexcept {
switch (infoType) {
case DeviceInfoType::OPENGL_RENDERER:
return getRendererString(driver);
case DeviceInfoType::OPENGL_VENDOR:
return getVendorString(driver);
case DeviceInfoType::OPENGL_VERSION:
return getVersionString(driver);
default:
FILAMENT_CHECK_POSTCONDITION(false) << "Unsupported DeviceInfoType for OpenGLPlatform";
return {};
}
}
utils::CString OpenGLPlatform::getVendorString(Driver const* driver) { utils::CString OpenGLPlatform::getVendorString(Driver const* driver) {
auto const p = static_cast<OpenGLDriverBase const*>(driver); auto const p = static_cast<OpenGLDriverBase const*>(driver);
#if UTILS_HAS_RTTI #if UTILS_HAS_RTTI
@@ -63,6 +78,15 @@ utils::CString OpenGLPlatform::getRendererString(Driver const* driver) {
return p->getRendererString(); return p->getRendererString();
} }
utils::CString OpenGLPlatform::getVersionString(Driver const* driver) {
auto const p = static_cast<OpenGLDriverBase const*>(driver);
#if UTILS_HAS_RTTI
FILAMENT_CHECK_POSTCONDITION(dynamic_cast<OpenGLDriverBase const*>(driver))
<< "Driver* has not been allocated with OpenGLPlatform";
#endif
return p->getVersionString();
}
void OpenGLPlatform::makeCurrent(SwapChain* drawSwapChain, SwapChain* readSwapChain, void OpenGLPlatform::makeCurrent(SwapChain* drawSwapChain, SwapChain* readSwapChain,
utils::Invocable<void()>, utils::Invocable<void(size_t)>) { utils::Invocable<void()>, utils::Invocable<void(size_t)>) {
makeCurrent(getCurrentContextType(), drawSwapChain, readSwapChain); makeCurrent(getCurrentContextType(), drawSwapChain, readSwapChain);
@@ -88,15 +88,14 @@ TimerQueryFactoryInterface::~TimerQueryFactoryInterface() = default;
// This is a backend synchronous call // This is a backend synchronous call
TimerQueryResult TimerQueryFactoryInterface::getTimerQueryValue( TimerQueryResult TimerQueryFactoryInterface::getTimerQueryValue(
GLTimerQuery* tq, uint64_t* elapsedTime) noexcept { GLTimerQuery* tq, uint64_t* elapsedTime) noexcept {
if (UTILS_LIKELY(tq->state)) { assert_invariant(tq->state);
int64_t const elapsed = tq->state->elapsed.load(std::memory_order_relaxed);
if (elapsed > 0) { int64_t const elapsed = tq->state->elapsed.load(std::memory_order_relaxed);
*elapsedTime = elapsed; if (elapsed > 0) {
return TimerQueryResult::AVAILABLE; *elapsedTime = elapsed;
} return TimerQueryResult::AVAILABLE;
return TimerQueryResult(elapsed);
} }
return TimerQueryResult::ERROR; return TimerQueryResult(elapsed);
} }
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
@@ -110,9 +109,8 @@ TimerQueryNativeFactory::TimerQueryNativeFactory(OpenGLContext& context)
TimerQueryNativeFactory::~TimerQueryNativeFactory() = default; TimerQueryNativeFactory::~TimerQueryNativeFactory() = default;
void TimerQueryNativeFactory::createTimerQuery(GLTimerQuery* tq) { void TimerQueryNativeFactory::createTimerQuery(GLTimerQuery* tq) {
assert_invariant(!tq->state); assert_invariant(tq->state);
tq->state = std::make_shared<GLTimerQuery::State>();
mContext.procs.genQueries(1u, &tq->state->gl.query); mContext.procs.genQueries(1u, &tq->state->gl.query);
CHECK_GL_ERROR() CHECK_GL_ERROR()
} }
@@ -181,8 +179,7 @@ TimerQueryFenceFactory::~TimerQueryFenceFactory() {
} }
void TimerQueryFenceFactory::createTimerQuery(GLTimerQuery* tq) { void TimerQueryFenceFactory::createTimerQuery(GLTimerQuery* tq) {
assert_invariant(!tq->state); assert_invariant(tq->state);
tq->state = std::make_shared<GLTimerQuery::State>();
} }
void TimerQueryFenceFactory::destroyTimerQuery(GLTimerQuery* tq) { void TimerQueryFenceFactory::destroyTimerQuery(GLTimerQuery* tq) {
@@ -238,8 +235,7 @@ TimerQueryFallbackFactory::TimerQueryFallbackFactory() = default;
TimerQueryFallbackFactory::~TimerQueryFallbackFactory() = default; TimerQueryFallbackFactory::~TimerQueryFallbackFactory() = default;
void TimerQueryFallbackFactory::createTimerQuery(GLTimerQuery* tq) { void TimerQueryFallbackFactory::createTimerQuery(GLTimerQuery* tq) {
assert_invariant(!tq->state); assert_invariant(tq->state);
tq->state = std::make_shared<GLTimerQuery::State>();
} }
void TimerQueryFallbackFactory::destroyTimerQuery(GLTimerQuery* tq) { void TimerQueryFallbackFactory::destroyTimerQuery(GLTimerQuery* tq) {
@@ -117,6 +117,13 @@ struct ShaderCompilerService::OpenGLProgramToken : ProgramToken {
cond.wait(l, [this] { return signaled; }); cond.wait(l, [this] { return signaled; });
} }
// Used in THREAD_POOL mode. Returns true if the token is signaled, meaning it's ready to be
// used.
bool isReady() const noexcept {
std::unique_lock const l(lock);
return signaled;
}
// This is invoked upon token completion, which occurs after a successful `gl.program` // This is invoked upon token completion, which occurs after a successful `gl.program`
// population or upon cancellation. In either scenario, the callback handle must be submitted // population or upon cancellation. In either scenario, the callback handle must be submitted
// to notify the caller that resource loading has concluded. // to notify the caller that resource loading has concluded.
@@ -524,8 +531,15 @@ GLuint ShaderCompilerService::initialize(program_token_t& token) {
} }
void ShaderCompilerService::ensureTokenIsReady(program_token_t const& token) { void ShaderCompilerService::ensureTokenIsReady(program_token_t const& token) {
if (token->gl.program) { if (mMode == Mode::THREAD_POOL) {
return;// It's ready. // Check if `token->gl.program` is populated
if (token->isReady()) {
return;
}
} else {
if (token->gl.program) {
return;
}
} }
switch (mMode) { switch (mMode) {
@@ -935,6 +949,7 @@ void ShaderCompilerService::cancelPendingSynchronousProgram(program_token_t cons
return false; return false;
} }
token->retrievedFromBlobCache = true; token->retrievedFromBlobCache = true;
token->signal(); // notify that `token->gl.program` is ready to use
return true; return true;
} }
@@ -34,6 +34,7 @@
#include <utils/Invocable.h> #include <utils/Invocable.h>
#include <utils/Logger.h> #include <utils/Logger.h>
#include <utils/Panic.h>
#include <utils/debug.h> #include <utils/debug.h>
#include <utils/ostream.h> #include <utils/ostream.h>
@@ -119,31 +120,41 @@ bool PlatformEGL::isOpenGL() const noexcept {
PlatformEGL::ExternalImageEGL::~ExternalImageEGL() = default; PlatformEGL::ExternalImageEGL::~ExternalImageEGL() = default;
void PlatformEGL::setEglDisplay(EGLDisplay display) noexcept {
FILAMENT_CHECK_PRECONDITION(mEGLDisplay == EGL_NO_DISPLAY)
<< "EGL Display has already been set.";
FILAMENT_CHECK_PRECONDITION(display != EGL_NO_DISPLAY)
<< "Must specify a valid EGL Display.";
mEGLDisplay = display;
}
Driver* PlatformEGL::createDriver(void* sharedContext, const DriverConfig& driverConfig) { Driver* PlatformEGL::createDriver(void* sharedContext, const DriverConfig& driverConfig) {
mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); if (mEGLDisplay == EGL_NO_DISPLAY) {
assert_invariant(mEGLDisplay != EGL_NO_DISPLAY); mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
assert_invariant(mEGLDisplay != EGL_NO_DISPLAY);
EGLint major, minor; EGLint major, minor;
EGLBoolean initialized = eglInitialize(mEGLDisplay, &major, &minor); EGLBoolean initialized = eglInitialize(mEGLDisplay, &major, &minor);
if (!initialized) { if (!initialized) {
EGLDeviceEXT eglDevice; EGLDeviceEXT eglDevice;
EGLint numDevices; EGLint numDevices;
PFNEGLQUERYDEVICESEXTPROC const eglQueryDevicesEXT = PFNEGLQUERYDEVICESEXTPROC const eglQueryDevicesEXT =
PFNEGLQUERYDEVICESEXTPROC(eglGetProcAddress("eglQueryDevicesEXT")); PFNEGLQUERYDEVICESEXTPROC(eglGetProcAddress("eglQueryDevicesEXT"));
if (eglQueryDevicesEXT != nullptr) { if (eglQueryDevicesEXT != nullptr) {
eglQueryDevicesEXT(1, &eglDevice, &numDevices); eglQueryDevicesEXT(1, &eglDevice, &numDevices);
if(auto* getPlatformDisplay = reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>( if(auto* getPlatformDisplay = reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(
eglGetProcAddress("eglGetPlatformDisplay"))) { eglGetProcAddress("eglGetPlatformDisplay"))) {
mEGLDisplay = getPlatformDisplay(EGL_PLATFORM_DEVICE_EXT, eglDevice, nullptr); mEGLDisplay = getPlatformDisplay(EGL_PLATFORM_DEVICE_EXT, eglDevice, nullptr);
initialized = eglInitialize(mEGLDisplay, &major, &minor); initialized = eglInitialize(mEGLDisplay, &major, &minor);
}
} }
} }
}
if (UTILS_UNLIKELY(!initialized)) { if (UTILS_UNLIKELY(!initialized)) {
LOG(ERROR) << "eglInitialize failed"; LOG(ERROR) << "eglInitialize failed";
return nullptr; return nullptr;
}
} }
#if defined(FILAMENT_IMPORT_ENTRY_POINTS) #if defined(FILAMENT_IMPORT_ENTRY_POINTS)
@@ -706,10 +717,11 @@ bool PlatformEGL::setExternalImage(ExternalImageHandleRef externalImage,
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------
void PlatformEGL::initializeGlExtensions() noexcept { void PlatformEGL::initializeGlExtensions() noexcept {
// We're guaranteed to be on an ES platform, since we're using EGL
const char* const extensions = (const char*)glGetString(GL_EXTENSIONS); const char* const extensions = (const char*)glGetString(GL_EXTENSIONS);
GLUtils::unordered_string_set const glExtensions = GLUtils::split(extensions); if (extensions) {
ext.gl.OES_EGL_image_external_essl3 = glExtensions.has("GL_OES_EGL_image_external_essl3"); GLUtils::unordered_string_set const glExtensions = GLUtils::split(extensions);
ext.gl.OES_EGL_image_external_essl3 = glExtensions.has("GL_OES_EGL_image_external_essl3");
}
} }
EGLContext PlatformEGL::getContextForType(ContextType const type) const noexcept { EGLContext PlatformEGL::getContextForType(ContextType const type) const noexcept {
@@ -29,6 +29,12 @@ using namespace bluevk;
namespace filament::backend { namespace filament::backend {
std::shared_ptr<VulkanCmdFence> VulkanCmdFence::completed() noexcept {
auto cmdFence = std::make_shared<VulkanCmdFence>(VK_NULL_HANDLE);
cmdFence->mStatus = VK_SUCCESS;
return cmdFence;
}
FenceStatus VulkanCmdFence::wait(VkDevice device, uint64_t const timeout, FenceStatus VulkanCmdFence::wait(VkDevice device, uint64_t const timeout,
std::chrono::steady_clock::time_point const until) { std::chrono::steady_clock::time_point const until) {
@@ -40,6 +40,13 @@ struct VulkanCmdFence {
explicit VulkanCmdFence(VkFence fence) : mFence(fence) { } explicit VulkanCmdFence(VkFence fence) : mFence(fence) { }
~VulkanCmdFence() = default; ~VulkanCmdFence() = default;
// Creates a VulkanCmdFence with its status set to VK_SUCCESS. It holds
// a null handle; it is assumed that any user of this object will avoid
// using the fence handle directly if getStatus() returns VK_SUCCESS, as
// in that case, it's likely the fence is being reused for other passes,
// and is not in the expected state anyway.
static std::shared_ptr<VulkanCmdFence> completed() noexcept;
void setStatus(VkResult const value) { void setStatus(VkResult const value) {
std::lock_guard const l(mLock); std::lock_guard const l(mLock);
mStatus = value; mStatus = value;
@@ -106,6 +106,12 @@ void VulkanBufferCache::gc() noexcept {
} }
} }
size_t totalBuffers = 0;
for (auto& bufferPool : mGpuBufferPools) {
totalBuffers += bufferPool.size();
}
FVK_LOGD << "VulkanBufferCache sizes: buffers=" << totalBuffers;
FVK_SYSTRACE_END(); FVK_SYSTRACE_END();
} }
@@ -157,6 +163,10 @@ VulkanGpuBuffer const* VulkanBufferCache::allocate(VulkanBufferBinding binding,
UTILS_UNUSED_IN_RELEASE VkResult result = vmaCreateBuffer(mAllocator, &bufferInfo, &allocInfo, UTILS_UNUSED_IN_RELEASE VkResult result = vmaCreateBuffer(mAllocator, &bufferInfo, &allocInfo,
&gpuBuffer->vkbuffer, &gpuBuffer->vmaAllocation, &gpuBuffer->allocationInfo); &gpuBuffer->vkbuffer, &gpuBuffer->vmaAllocation, &gpuBuffer->allocationInfo);
FVK_LOGD << "VulkanBufferCache - allocated a vkBuffer " << gpuBuffer->vkbuffer
<< " of size " << numBytes << " and binding = " << static_cast<int>(binding)
<< " result=" << (int)result;
#if FVK_ENABLED(FVK_DEBUG_VULKAN_BUFFER_CACHE) #if FVK_ENABLED(FVK_DEBUG_VULKAN_BUFFER_CACHE)
if (result != VK_SUCCESS) { if (result != VK_SUCCESS) {
FVK_LOGE << "VulkanBufferCache - failed to allocate a new vkBuffer of size " << numBytes FVK_LOGE << "VulkanBufferCache - failed to allocate a new vkBuffer of size " << numBytes
@@ -173,6 +183,7 @@ VulkanGpuBuffer const* VulkanBufferCache::allocate(VulkanBufferBinding binding,
} }
void VulkanBufferCache::destroy(VulkanGpuBuffer const* gpuBuffer) noexcept { void VulkanBufferCache::destroy(VulkanGpuBuffer const* gpuBuffer) noexcept {
FVK_LOGD << "VulkanBufferCache - Destroying vkBuffer " << gpuBuffer->vkbuffer;
vmaDestroyBuffer(mAllocator, gpuBuffer->vkbuffer, gpuBuffer->vmaAllocation); vmaDestroyBuffer(mAllocator, gpuBuffer->vkbuffer, gpuBuffer->vmaAllocation);
delete gpuBuffer; delete gpuBuffer;
gpuBuffer = nullptr; gpuBuffer = nullptr;
@@ -194,6 +194,7 @@ void VulkanCommandBuffer::begin() noexcept {
} }
fvkmemory::resource_ptr<VulkanSemaphore> VulkanCommandBuffer::submit() { fvkmemory::resource_ptr<VulkanSemaphore> VulkanCommandBuffer::submit() {
FVK_LOGD << "Submitting VulkanCommandBuffer: " << mBuffer << " (age=" << age() << ")";
while (mMarkerCount > 0) { while (mMarkerCount > 0) {
popMarker(); popMarker();
} }
+5 -1
View File
@@ -277,7 +277,11 @@ private:
fvkmemory::resource_ptr<VulkanSemaphore> mLastSubmit; fvkmemory::resource_ptr<VulkanSemaphore> mLastSubmit;
VkFence mLastFence = VK_NULL_HANDLE; VkFence mLastFence = VK_NULL_HANDLE;
std::shared_ptr<VulkanCmdFence> mLastFenceStatus; // Start out with a completed fence, because if no commands have
// been queued or submited, then by definition, all pending work
// is complete.
std::shared_ptr<VulkanCmdFence> mLastFenceStatus =
VulkanCmdFence::completed();
VkPipelineStageFlags mInjectedDependencyWaitStage = 0; VkPipelineStageFlags mInjectedDependencyWaitStage = 0;
}; };
+17 -2
View File
@@ -36,6 +36,7 @@ VK_DEFINE_HANDLE(VmaPool)
namespace filament::backend { namespace filament::backend {
struct VulkanCommandBuffer; struct VulkanCommandBuffer;
struct VulkanRenderPass;
struct VulkanRenderTarget; struct VulkanRenderTarget;
struct VulkanSwapChain; struct VulkanSwapChain;
struct VulkanTexture; struct VulkanTexture;
@@ -58,11 +59,11 @@ struct VulkanAttachment {
VkImageSubresourceRange getSubresourceRange() const; VkImageSubresourceRange getSubresourceRange() const;
}; };
struct VulkanRenderPass { struct VulkanRenderPassContext {
// Between the begin and end command render pass we cache the command buffer // Between the begin and end command render pass we cache the command buffer
VulkanCommandBuffer* commandBuffer; VulkanCommandBuffer* commandBuffer;
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget; fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget;
VkRenderPass renderPass; fvkmemory::resource_ptr<VulkanRenderPass> renderPass;
RenderPassParams params; RenderPassParams params;
int currentSubpass; int currentSubpass;
}; };
@@ -136,6 +137,14 @@ public:
return mFenceExportFlags; return mFenceExportFlags;
} }
inline const char* getPhysicalDeviceName() const noexcept {
return mPhysicalDeviceProperties.properties.deviceName;
}
inline const char* getDriverName() const noexcept { return mDriverProperties.driverName; }
inline const char* getDriverInfo() const noexcept { return mDriverProperties.driverInfo; }
inline bool isImageCubeArraySupported() const noexcept { inline bool isImageCubeArraySupported() const noexcept {
return mPhysicalDeviceFeatures.features.imageCubeArray == VK_TRUE; return mPhysicalDeviceFeatures.features.imageCubeArray == VK_TRUE;
} }
@@ -211,11 +220,16 @@ public:
return mGlobalPrioritySupported; return mGlobalPrioritySupported;
} }
inline bool isDriverPropertiesSupported() const noexcept { return mDriverPropertiesSupported; }
private: private:
VkPhysicalDeviceMemoryProperties mMemoryProperties = {}; VkPhysicalDeviceMemoryProperties mMemoryProperties = {};
VkPhysicalDeviceProperties2 mPhysicalDeviceProperties = { VkPhysicalDeviceProperties2 mPhysicalDeviceProperties = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
}; };
VkPhysicalDeviceDriverProperties mDriverProperties = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES,
};
VkPhysicalDeviceVulkan11Features mPhysicalDeviceVk11Features = { VkPhysicalDeviceVulkan11Features mPhysicalDeviceVk11Features = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES,
}; };
@@ -245,6 +259,7 @@ private:
bool mProtectedMemorySupported = false; bool mProtectedMemorySupported = false;
bool mVertexInputDynamicStateSupported = false; bool mVertexInputDynamicStateSupported = false;
bool mGlobalPrioritySupported = false; bool mGlobalPrioritySupported = false;
bool mDriverPropertiesSupported = false;
// These are options that can be enabled or disabled at an application level. // These are options that can be enabled or disabled at an application level.
bool mAsyncPipelineCachePrewarmingEnabled = false; bool mAsyncPipelineCachePrewarmingEnabled = false;
@@ -355,35 +355,8 @@ void VulkanDescriptorSetCache::updateBuffer(fvkmemory::resource_ptr<VulkanDescri
} }
void VulkanDescriptorSetCache::updateSampler(fvkmemory::resource_ptr<VulkanDescriptorSet> set, void VulkanDescriptorSetCache::updateSampler(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
uint8_t binding, fvkmemory::resource_ptr<VulkanTexture> texture, VkSampler sampler, uint8_t binding, fvkmemory::resource_ptr<VulkanTexture> texture,
VkDescriptorSetLayout externalSamplerLayout) noexcept { VkSampler sampler, VkDescriptorSetLayout externalSamplerLayout) noexcept {
// We have to update a bound set for two use cases
// - streaming API (a changing feed of AHardwareBuffer)
// - external samplers - potential changing of dataspace per-frame
// TODO: Fix the stream flow case base on the above comment!!
if (set->isAnExternalSamplerBound) {
auto layout = set->getLayout();
// Build a new descriptor set from the new layout
VkDescriptorSetLayout const genLayout = set->boundLayout;
VkDescriptorSet const newSet = getVkSet(layout->count, genLayout);
Bitmask const ubo = layout->bitmask.ubo | layout->bitmask.dynamicUbo;
Bitmask samplers = layout->bitmask.sampler;
samplers.unset(binding);
// Each bitmask denotes a binding index, and separated into two stages - vertex and buffer
// We fold the two stages into just the lower half of the bits to denote a combined set of
// bindings.
Bitmask const copyBindings = foldBitsInHalf(ubo | samplers);
VkDescriptorSet const srcSet = set->getVkSet();
copySet(srcSet, newSet, copyBindings);
set->addNewSet(newSet,
[this, layoutCount = layout->count, genLayout, newSet](VulkanDescriptorSet*) {
this->manualRecycle(layoutCount, genLayout, newSet);
});
}
VkDescriptorSet const vkset = set->getVkSet(); VkDescriptorSet const vkset = set->getVkSet();
VkImageSubresourceRange range = texture->getPrimaryViewRange(); VkImageSubresourceRange range = texture->getPrimaryViewRange();
VkImageViewType const expectedType = texture->getViewType(); VkImageViewType const expectedType = texture->getViewType();
@@ -432,6 +405,28 @@ fvkmemory::resource_ptr<VulkanDescriptorSet> VulkanDescriptorSetCache::createSet
return set; return set;
} }
void VulkanDescriptorSetCache::cloneSet(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
fvkutils::SamplerBitmask samplerMask) noexcept {
auto const& layout = set->getLayout();
// Build a new descriptor set from layout
VkDescriptorSetLayout const genLayout = set->boundLayout;
VkDescriptorSet const newSet = getVkSet(layout->count, genLayout);
// Each bitmask denotes a binding index, and separated into two stages - vertex and buffer
// We fold the two stages into just the lower half of the bits to denote a combined set of
// bindings.
Bitmask const ubo = layout->bitmask.ubo | layout->bitmask.dynamicUbo;
// Don't copy the samplers in the mask
Bitmask samplers = layout->bitmask.sampler ^ samplerMask;
Bitmask const copyBindings = foldBitsInHalf(ubo | samplers);
VkDescriptorSet const srcSet = set->getVkSet();
copySet(srcSet, newSet, copyBindings);
set->addNewSet(newSet,
[this, layoutCount = layout->count, genLayout, newSet](
VulkanDescriptorSet*) { this->manualRecycle(layoutCount, genLayout, newSet); });
}
VkDescriptorSet VulkanDescriptorSetCache::getVkSet(DescriptorCount const& count, VkDescriptorSet VulkanDescriptorSetCache::getVkSet(DescriptorCount const& count,
VkDescriptorSetLayout vklayout) { VkDescriptorSetLayout vklayout) {
return mDescriptorPool->obtainSet(count, vklayout); return mDescriptorPool->obtainSet(count, vklayout);
@@ -76,6 +76,11 @@ public:
fvkmemory::resource_ptr<VulkanDescriptorSet> createSet(Handle<HwDescriptorSet> handle, fvkmemory::resource_ptr<VulkanDescriptorSet> createSet(Handle<HwDescriptorSet> handle,
fvkmemory::resource_ptr<VulkanDescriptorSetLayout> layout); fvkmemory::resource_ptr<VulkanDescriptorSetLayout> layout);
// Create and set as current a new VkDescriptorSet using the `set` currently bound layout and
// copy all the bindings and ignoring the samplers bindings in the `samplerMask`.
void cloneSet(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
fvkutils::SamplerBitmask samplerMask) noexcept;
// This method is meant to be used with external samplers // This method is meant to be used with external samplers
VkDescriptorSet getVkSet(DescriptorCount const& count, VkDescriptorSetLayout vklayout); VkDescriptorSet getVkSet(DescriptorCount const& count, VkDescriptorSetLayout vklayout);
+19 -14
View File
@@ -1913,9 +1913,6 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
auto rt = resource_ptr<VulkanRenderTarget>::cast(&mResourceManager, rth); auto rt = resource_ptr<VulkanRenderTarget>::cast(&mResourceManager, rth);
VulkanCommandBuffer* commandBuffer = rt->isProtected() ?
&mCommands.getProtected() : &mCommands.get();
// Filament has the expectation that the contents of the swap chain are not preserved on the // Filament has the expectation that the contents of the swap chain are not preserved on the
// first render pass. Note however that its contents are often preserved on subsequent render // first render pass. Note however that its contents are often preserved on subsequent render
// passes, due to multiple views. // passes, due to multiple views.
@@ -1930,6 +1927,11 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
} }
} }
// Note that this needs to come after the acquireNextswapchainImage() above because that path
// might flush the current command buffer.
VulkanCommandBuffer* commandBuffer =
rt->isProtected() ? &mCommands.getProtected() : &mCommands.get();
// Note that retrieving the extent must come after the acquireNextSwapchainImage() above; // Note that retrieving the extent must come after the acquireNextSwapchainImage() above;
// otherwise it might be 0. // otherwise it might be 0.
VkExtent2D const extent = rt->getExtent(); VkExtent2D const extent = rt->getExtent();
@@ -1975,17 +1977,19 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
rpkey.initialDepthLayout = currentDepthLayout; rpkey.initialDepthLayout = currentDepthLayout;
rpkey.subpassMask = uint8_t(params.subpassMask); rpkey.subpassMask = uint8_t(params.subpassMask);
VkRenderPass renderPass = mFramebufferCache.getRenderPass(rpkey); fvkmemory::resource_ptr<VulkanRenderPass> renderPass =
mFramebufferCache.getRenderPass(rpkey, &mResourceManager);
mPipelineCache.bindRenderPass(renderPass, 0); mPipelineCache.bindRenderPass(renderPass, 0);
// Create the VkFramebuffer or fetch it from cache. // Create the VkFramebuffer or fetch it from cache.
VulkanFboCache::FboKey fbkey = rt->getFboKey(); VulkanFboCache::FboKey fbkey = rt->getFboKey();
fbkey.renderPass = renderPass; fbkey.renderPass = renderPass->getVkRenderPass();
fbkey.layers = 1; fbkey.layers = 1;
rt->emitBarriersBeginRenderPass(*commandBuffer); rt->emitBarriersBeginRenderPass(*commandBuffer);
VkFramebuffer vkfb = mFramebufferCache.getFramebuffer(fbkey); fvkmemory::resource_ptr<VulkanFramebuffer> vkfb =
mFramebufferCache.getFramebuffer(fbkey, &mResourceManager, rt);
// Assign a label to the framebuffer for debugging purposes. // Assign a label to the framebuffer for debugging purposes.
#if FVK_ENABLED(FVK_DEBUG_GROUP_MARKERS | FVK_DEBUG_DEBUG_UTILS) #if FVK_ENABLED(FVK_DEBUG_GROUP_MARKERS | FVK_DEBUG_DEBUG_UTILS)
@@ -1998,12 +2002,14 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
// The current command buffer now has references to the render target and its attachments. // The current command buffer now has references to the render target and its attachments.
commandBuffer->acquire(rt); commandBuffer->acquire(rt);
commandBuffer->acquire(renderPass);
commandBuffer->acquire(vkfb);
// Populate the structures required for vkCmdBeginRenderPass. // Populate the structures required for vkCmdBeginRenderPass.
VkRenderPassBeginInfo renderPassInfo { VkRenderPassBeginInfo renderPassInfo {
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
.renderPass = renderPass, .renderPass = renderPass->getVkRenderPass(),
.framebuffer = vkfb, .framebuffer = vkfb->getVkFramebuffer(),
// The renderArea field constrains the LoadOp, but scissoring does not. // The renderArea field constrains the LoadOp, but scissoring does not.
// Therefore, we do not set the scissor rect here, we only need it in draw(). // Therefore, we do not set the scissor rect here, we only need it in draw().
@@ -2058,7 +2064,7 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
mCurrentRenderPass = { mCurrentRenderPass = {
.commandBuffer = commandBuffer, .commandBuffer = commandBuffer,
.renderTarget = rt, .renderTarget = rt,
.renderPass = renderPassInfo.renderPass, .renderPass = renderPass,
.params = params, .params = params,
.currentSubpass = 0, .currentSubpass = 0,
}; };
@@ -2078,8 +2084,7 @@ void VulkanDriver::endRenderPass(int) {
rt->emitBarriersEndRenderPass(*mCurrentRenderPass.commandBuffer); rt->emitBarriersEndRenderPass(*mCurrentRenderPass.commandBuffer);
mCurrentRenderPass.renderTarget = {}; mCurrentRenderPass.renderTarget = {};
mCurrentRenderPass.renderPass = VK_NULL_HANDLE; mCurrentRenderPass.renderPass = {};
mCurrentRenderPass.commandBuffer = nullptr; mCurrentRenderPass.commandBuffer = nullptr;
} }
@@ -2244,7 +2249,7 @@ void VulkanDriver::resolve(
Handle<HwTexture> src, uint8_t dstLevel, uint8_t dstLayer) { Handle<HwTexture> src, uint8_t dstLevel, uint8_t dstLayer) {
FVK_SYSTRACE_SCOPE(); FVK_SYSTRACE_SCOPE();
FILAMENT_CHECK_PRECONDITION(mCurrentRenderPass.renderPass == VK_NULL_HANDLE) FILAMENT_CHECK_PRECONDITION(!mCurrentRenderPass.renderPass)
<< "resolve() cannot be invoked inside a render pass."; << "resolve() cannot be invoked inside a render pass.";
auto srcTexture = resource_ptr<VulkanTexture>::cast(&mResourceManager, src); auto srcTexture = resource_ptr<VulkanTexture>::cast(&mResourceManager, src);
@@ -2287,7 +2292,7 @@ void VulkanDriver::blit(
math::uint2 size) { math::uint2 size) {
FVK_SYSTRACE_SCOPE(); FVK_SYSTRACE_SCOPE();
FILAMENT_CHECK_PRECONDITION(mCurrentRenderPass.renderPass == VK_NULL_HANDLE) FILAMENT_CHECK_PRECONDITION(!mCurrentRenderPass.renderPass)
<< "blit() cannot be invoked inside a render pass."; << "blit() cannot be invoked inside a render pass.";
auto srcTexture = resource_ptr<VulkanTexture>::cast(&mResourceManager, src); auto srcTexture = resource_ptr<VulkanTexture>::cast(&mResourceManager, src);
@@ -2329,7 +2334,7 @@ void VulkanDriver::blitDEPRECATED(TargetBufferFlags buffers,
// Note: blitDEPRECATED is only used for Renderer::copyFrame() // Note: blitDEPRECATED is only used for Renderer::copyFrame()
FILAMENT_CHECK_PRECONDITION(mCurrentRenderPass.renderPass == VK_NULL_HANDLE) FILAMENT_CHECK_PRECONDITION(!mCurrentRenderPass.renderPass)
<< "blitDEPRECATED() cannot be invoked inside a render pass."; << "blitDEPRECATED() cannot be invoked inside a render pass.";
FILAMENT_CHECK_PRECONDITION(buffers == TargetBufferFlags::COLOR0) FILAMENT_CHECK_PRECONDITION(buffers == TargetBufferFlags::COLOR0)
+1 -1
View File
@@ -139,7 +139,7 @@ private:
resource_ptr<VulkanSwapChain> mCurrentSwapChain; resource_ptr<VulkanSwapChain> mCurrentSwapChain;
resource_ptr<VulkanRenderTarget> mDefaultRenderTarget; resource_ptr<VulkanRenderTarget> mDefaultRenderTarget;
VulkanRenderPass mCurrentRenderPass = {}; VulkanRenderPassContext mCurrentRenderPass = {};
VmaAllocator mAllocator = VK_NULL_HANDLE; VmaAllocator mAllocator = VK_NULL_HANDLE;
VkDebugReportCallbackEXT mDebugCallback = VK_NULL_HANDLE; VkDebugReportCallbackEXT mDebugCallback = VK_NULL_HANDLE;
@@ -94,6 +94,9 @@ void VulkanExternalImageManager::updateSetAndLayout(
fvkmemory::resource_ptr<VulkanDescriptorSetLayout> const& layout = set->getLayout(); fvkmemory::resource_ptr<VulkanDescriptorSetLayout> const& layout = set->getLayout();
set->boundLayout = mDescriptorSetLayoutCache->getVkLayout(layout->bitmask, set->boundLayout = mDescriptorSetLayoutCache->getVkLayout(layout->bitmask,
actualExternalSamplers, outSamplers); actualExternalSamplers, outSamplers);
mDescriptorSetCache->cloneSet(set, actualExternalSamplers);
// Update the external samplers in the set // Update the external samplers in the set
for (auto& [binding, sampler, image]: samplerAndBindings) { for (auto& [binding, sampler, image]: samplerAndBindings) {
// We cannot call updateSamplerForExternalSamplerSet because some samplers are non NULL // We cannot call updateSamplerForExternalSamplerSet because some samplers are non NULL
+23 -19
View File
@@ -17,13 +17,14 @@
#include "VulkanFboCache.h" #include "VulkanFboCache.h"
#include "VulkanConstants.h" #include "VulkanConstants.h"
#include "VulkanHandles.h"
#include "vulkan/utils/Image.h" #include "vulkan/utils/Image.h"
#include <utils/Panic.h> #include <utils/Panic.h>
// If any VkRenderPass or VkFramebuffer is unused for more than TIME_BEFORE_EVICTION frames, it // If any VkRenderPass or VkFramebuffer is unused for more than TIME_BEFORE_EVICTION frames, it
// is evicted from the cache. // is evicted from the cache.
static constexpr uint32_t TIME_BEFORE_EVICTION = FVK_MAX_COMMAND_BUFFERS; static constexpr uint32_t TIME_BEFORE_EVICTION = 3;
using namespace bluevk; using namespace bluevk;
@@ -69,9 +70,11 @@ VulkanFboCache::~VulkanFboCache() {
<< "Please explicitly call terminate() while the VkDevice is still alive."; << "Please explicitly call terminate() while the VkDevice is still alive.";
} }
VkFramebuffer VulkanFboCache::getFramebuffer(FboKey const& config) noexcept { fvkmemory::resource_ptr<VulkanFramebuffer> VulkanFboCache::getFramebuffer(FboKey const& config,
fvkmemory::ResourceManager* resManager,
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget) noexcept {
FboMap::iterator iter = mFramebufferCache.find(config); FboMap::iterator iter = mFramebufferCache.find(config);
if (UTILS_LIKELY(iter != mFramebufferCache.end() && iter->second.handle != VK_NULL_HANDLE)) { if (UTILS_LIKELY(iter != mFramebufferCache.end())) {
iter.value().timestamp = mCurrentTime; iter.value().timestamp = mCurrentTime;
return iter->second.handle; return iter->second.handle;
} }
@@ -117,13 +120,19 @@ VkFramebuffer VulkanFboCache::getFramebuffer(FboKey const& config) noexcept {
VkResult error = vkCreateFramebuffer(mDevice, &info, VKALLOC, &framebuffer); VkResult error = vkCreateFramebuffer(mDevice, &info, VKALLOC, &framebuffer);
FILAMENT_CHECK_POSTCONDITION(error == VK_SUCCESS) << "Unable to create framebuffer." FILAMENT_CHECK_POSTCONDITION(error == VK_SUCCESS) << "Unable to create framebuffer."
<< " error=" << static_cast<int32_t>(error); << " error=" << static_cast<int32_t>(error);
mFramebufferCache[config] = {framebuffer, mCurrentTime}; FVK_LOGD << "VulkanFboCache - Created VkFramebuffer " << framebuffer << " for renderTarget " << renderTarget.get()
return framebuffer; << " size=" << config.width << "x" << config.height;
fvkmemory::resource_ptr<VulkanFramebuffer> fbh =
fvkmemory::resource_ptr<VulkanFramebuffer>::construct(resManager, mDevice, framebuffer,
renderTarget);
mFramebufferCache[config] = { fbh, mCurrentTime };
return fbh;
} }
VkRenderPass VulkanFboCache::getRenderPass(RenderPassKey const& config) noexcept { fvkmemory::resource_ptr<VulkanRenderPass> VulkanFboCache::getRenderPass(
RenderPassKey const& config, fvkmemory::ResourceManager* resManager) noexcept {
auto iter = mRenderPassCache.find(config); auto iter = mRenderPassCache.find(config);
if (UTILS_LIKELY(iter != mRenderPassCache.end() && iter->second.handle != VK_NULL_HANDLE)) { if (UTILS_LIKELY(iter != mRenderPassCache.end())) {
iter.value().timestamp = mCurrentTime; iter.value().timestamp = mCurrentTime;
return iter->second.handle; return iter->second.handle;
} }
@@ -326,7 +335,9 @@ VkRenderPass VulkanFboCache::getRenderPass(RenderPassKey const& config) noexcept
VkResult error = vkCreateRenderPass(mDevice, &renderPassInfo, VKALLOC, &renderPass); VkResult error = vkCreateRenderPass(mDevice, &renderPassInfo, VKALLOC, &renderPass);
FILAMENT_CHECK_POSTCONDITION(error == VK_SUCCESS) << "Unable to create render pass." FILAMENT_CHECK_POSTCONDITION(error == VK_SUCCESS) << "Unable to create render pass."
<< " error=" << error; << " error=" << error;
mRenderPassCache[config] = {renderPass, mCurrentTime}; fvkmemory::resource_ptr<VulkanRenderPass> rph =
fvkmemory::resource_ptr<VulkanRenderPass>::construct(resManager, mDevice, renderPass);
mRenderPassCache[config] = {rph, mCurrentTime};
#if FVK_ENABLED(FVK_DEBUG_FBO_CACHE) #if FVK_ENABLED(FVK_DEBUG_FBO_CACHE)
FVK_LOGD << "Created render pass " << renderPass << " with "; FVK_LOGD << "Created render pass " << renderPass << " with ";
@@ -343,13 +354,12 @@ VkRenderPass VulkanFboCache::getRenderPass(RenderPassKey const& config) noexcept
<< "colorAttachmentCount[0] = " << subpasses[0].colorAttachmentCount; << "colorAttachmentCount[0] = " << subpasses[0].colorAttachmentCount;
#endif #endif
return renderPass; return rph;
} }
void VulkanFboCache::resetFramebuffers() noexcept { void VulkanFboCache::resetFramebuffers() noexcept {
for (const auto& pair: mFramebufferCache) { for (const auto& pair: mFramebufferCache) {
mRenderPassRefCount[pair.first.renderPass]--; mRenderPassRefCount[pair.first.renderPass]--;
vkDestroyFramebuffer(mDevice, pair.second.handle, VKALLOC);
} }
mFramebufferCache.clear(); mFramebufferCache.clear();
} }
@@ -357,9 +367,6 @@ void VulkanFboCache::resetFramebuffers() noexcept {
void VulkanFboCache::terminate() noexcept { void VulkanFboCache::terminate() noexcept {
resetFramebuffers(); resetFramebuffers();
for (const auto& pair: mRenderPassCache) {
vkDestroyRenderPass(mDevice, pair.second.handle, VKALLOC);
}
mRenderPassRefCount.clear(); mRenderPassRefCount.clear();
mRenderPassCache.clear(); mRenderPassCache.clear();
} }
@@ -380,8 +387,6 @@ void VulkanFboCache::gc() noexcept {
const FboVal fbo = iter->second; const FboVal fbo = iter->second;
if (fbo.timestamp < evictTime && fbo.handle) { if (fbo.timestamp < evictTime && fbo.handle) {
mRenderPassRefCount[iter->first.renderPass]--; mRenderPassRefCount[iter->first.renderPass]--;
vkDestroyFramebuffer(mDevice, fbo.handle, VKALLOC);
iter.value().handle = VK_NULL_HANDLE;
// erase(iterator) returns the iterator to the next element. // erase(iterator) returns the iterator to the next element.
iter = mFramebufferCache.erase(iter); iter = mFramebufferCache.erase(iter);
@@ -391,11 +396,8 @@ void VulkanFboCache::gc() noexcept {
} }
for (RenderPassMap::iterator iter = mRenderPassCache.begin(); iter != mRenderPassCache.end(); ) { for (RenderPassMap::iterator iter = mRenderPassCache.begin(); iter != mRenderPassCache.end(); ) {
const VkRenderPass handle = iter->second.handle; const VkRenderPass handle = iter->second.handle->getVkRenderPass();
if (iter->second.timestamp < evictTime && handle && mRenderPassRefCount[handle] == 0) { if (iter->second.timestamp < evictTime && handle && mRenderPassRefCount[handle] == 0) {
vkDestroyRenderPass(mDevice, handle, VKALLOC);
iter.value().handle = VK_NULL_HANDLE;
// erase(iterator) returns the iterator to the next element. // erase(iterator) returns the iterator to the next element.
iter = mRenderPassCache.erase(iter); iter = mRenderPassCache.erase(iter);
mRenderPassRefCount.erase(handle); mRenderPassRefCount.erase(handle);
@@ -404,6 +406,8 @@ void VulkanFboCache::gc() noexcept {
} }
} }
FVK_LOGD << "VulkanFboCache sizes: framebuffers=" << mFramebufferCache.size() << " renderpasses=" << mRenderPassCache.size();
FVK_SYSTRACE_END(); FVK_SYSTRACE_END();
} }

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