Compare commits

..
Author SHA1 Message Date
Doris Wu bd073119d2 Revert "buffer update opt: Add a flag to guard the feature (#9322)"
This reverts commit 49c4a5d62c.
2025-10-15 23:41:03 +08:00
Doris Wu 06c4ed4e6b fix: material sandbox scene crashes during shutdown (#9325) 2025-10-15 15:34:50 +00:00
Doris Wu 49c4a5d62c buffer update opt: Add a flag to guard the feature (#9322) 2025-10-15 23:14:53 +08:00
Doris Wu c757cc3629 buffer update opt: Introduce BufferAllocator and its tests (#9304)
* Introduce AllocationStrategy and its tests

* feedback

* feedback

* Add stress test

* Fix the build

* Minor fix

* Rename the class

* Rename

* Make const
2025-10-15 06:25:42 +00:00
rafadevai dbdf8f672b GL: Check if GL_EXT_texture_filter_anisotropic in supported in GLES (#9317)
In GLES the anisotropic filtering featuring is always
disabled even in the cases where is supported by the hardware
because the check for the extension is missing.
2025-10-14 23:03:25 -07:00
Filament Bot 54bd888374 [automated] Updating /docs due to commit b7dea28
Full commit hash is b7dea28cc5

DOCS_ALLOW_DIRECT_EDITS
2025-10-14 21:28:53 +00:00
Benjamin Doherty b7dea28cc5 Release Filament 1.66.0 2025-10-14 14:25:20 -07:00
Benjamin Doherty 9aa8e7780b Fix NEW_RELEASE_NOTES 2025-10-14 14:24:52 -07:00
Ben Doherty 271e639abe Revert "Make check-headers test use c++20" (#9316) 2025-10-14 14:12:34 -07:00
Ben Doherty 082a79eebc Fix, revert to C++17 string_view constructor (#9315) 2025-10-13 15:01:40 -07:00
Ben Doherty 7a10c09954 Fix, Abseil compilation error with StaticString (#9314) 2025-10-13 15:01:39 -07:00
Filament Bot 3139a37cc7 [automated] Updating /docs due to commit f8e8c27
Full commit hash is f8e8c27c04

DOCS_ALLOW_DIRECT_EDITS
2025-10-13 19:56:08 +00:00
Mathias AgopianandPowei Feng f8e8c27c04 Add a Renderer API to force skipping frames (#9313)
* Add a Renderer API to force skipping frames

Renderer::skipNextFrames(size_t) can be used to force filament to
pretend the next N frames must be skipped. This is mostly useful for
debugging.

* Add DebugOptions to Settings

We still need to move the "Debug" features of gltf_viewer to this,
but this give us a framework to do it. 
Currently there is one debug option that allows to set a number of
frames to skip.

ViewerGui propose a button to skip 10 frames using this framework

* Update libs/viewer/src/Settings.cpp

Co-authored-by: Powei Feng <powei@google.com>

* Update libs/viewer/src/Settings.cpp

Co-authored-by: Powei Feng <powei@google.com>

---------

Co-authored-by: Powei Feng <powei@google.com>
2025-10-13 12:52:17 -07:00
Mathias Agopian c79d695ffb fix wasm build. (#9312)
The new AsyncJobQueue can't work with wasm, which doesn't support 
threads.
2025-10-13 12:18:28 -07:00
David Neto ad6f6cf149 filamat: remove call to Glslang's SPIR-V remapper (#9310)
The Glslang SPIR-V remapper was removed, and replaced by the CanonicalizeIds
pass in spirv-opt.  See https://github.com/KhronosGroup/SPIRV-Tools/pull/6174

Filament's GLSL post-optimization algorithm already runs CanonicalizeIds.

Bug: b:450891564
2025-10-13 09:46:07 -07:00
Mathias Agopian 8506e94f10 Improvements to FrameInfo (#9293)
* fix a possible deadlock in AsyncJobQueue

drainAndExit() could get stuck because it waited for the job
queue to be empty, but that was not signaled.
in fact, drainAndExit() didn't need to do that.

* Improvements to FrameInfo 

- return the GPU Complete timestamp
- return the app VSYNC timestamp
- works TimerQueries are not supported

The VSYNC time is just a convenience as it is the same value
provided by the application during Renderer::beginFrame() or via
Renderer::setVsyncTime().
2025-10-10 21:40:28 +00:00
Mathias Agopian 5db8e0b9ab fix a regression in FMaterial::setConstant() (#9308)
it used to return false when setting the same value for the constant,
which other code used to invalidate the material or not. but it
was recently changed to always return true on success.

this reestablish the previous behavior.
2025-10-10 12:43:32 -07:00
Powei Feng da55574b32 webgpu: refactor blitter to use struct as keys (#9300)
Using struct as keys simplifies the parameter definitions of the
relevant pipeline, pipelinelayout, shader module methods. This
enables adding or removing fields without adding major changes
to method definitions.

Using utils::hash::MurmurHashFn also aligns with the style of
the rest of filament.
2025-10-10 17:31:47 +00:00
yein 736fed00b3 handle StatusCode::UNSUPPORTED in << operator (#9307) 2025-10-10 00:06:57 +00:00
Evan Mezeske 9b200fc33a Some trivial changes to compile under most recent MSVC as a CMake sub-project (#9275)
* Add a workaround for Filament's breaking change to add_subdirectory for Abseil, see: https://github.com/google/filament/issues/8611

* Fix #pragma warning syntax for MSVC

* Only search for an external Abseil dependency if a new option FILAMENT_USE_EXTERNAL_ABSL is set
2025-10-09 23:06:18 +00:00
yein fa436f1d12 Make JsonishParser use utils::Status (#9302)
* Make JsonishParser use utils::Status

- add unsupported error in utils::Status
- add invalid case in the test

* return a pair of Status and string in resolveEscapes;
use initializer list for JsonishString and move that to the header.
2025-10-09 15:44:03 -07:00
Powei Feng 837b2715a0 Update MATERIAL_VERSION to 66 2025-10-09 11:29:16 -07:00
Jordan Rupprecht 1a04312986 Add missing #include <ios> (#9305)
This is needed to instantiate `std::streampos`. It currently relies on transitive header inclusion to get that, which is going away for libc++ in https://github.com/llvm/llvm-project/commit/ebcf1bf2ecba6b25ece3c2bbddb4485e76189387.
2025-10-09 10:24:00 -07:00
Filament Bot a89711b006 [automated] Updating /docs due to commit 6d061b5
Full commit hash is 6d061b5d01

DOCS_ALLOW_DIRECT_EDITS
2025-10-08 20:04:04 +00:00
Powei Feng 6d061b5d01 renderdiff: add two params to local_test.sh (#9301)
- --no_rebuild will skip building the gltf_viewer again
 - --num_threads will limit the number of threads used in running
   the rendering test.

Both of these options are meant for debugging locally.
2025-10-08 13:01:09 -07:00
Powei Feng 53ddb3dd1c renderdiff: [viewer] enable frontend to be used with runID (#9277) 2025-10-08 18:36:30 +00:00
Powei Feng c7b0a7f441 webgpu: fix queue submission and fence (#9299)
- Add WebGPUQeuueManager to manage queue submissions
 - The manager will keep a "last status" field that will track
   the status of the last submission.
 - Any created HwFence will be associated with a status object.
 - The object updates in a callback that indicates that the
   submitted workfload has been completed.
2025-10-08 17:30:19 +00:00
Filament Bot d7b44a2585 [automated] Updating /docs due to commit caf886d
Full commit hash is caf886df6b

DOCS_ALLOW_DIRECT_EDITS
2025-10-07 22:58:22 +00:00
66 changed files with 1864 additions and 732 deletions
+1 -1
View File
@@ -391,7 +391,7 @@ Alternatively, if you have node installed you can use the
[live-server](https://www.npmjs.com/package/live-server) package, which automatically refreshes the
web page when it detects a change.
Each sample app has its own handwritten html file. Additionally the server folder contains assets
Each sample app has its own handwritten html file. Additionally, the server folder contains assets
such as meshes, textures, and materials.
## Running the native samples
-1
View File
@@ -1,7 +1,6 @@
# Filament Release Notes log
**If you are merging a PR into main**: please add the release note below, under the *Release notes
We are chaning the way Vulkan buffers are handled. We need to switch over to a managed (or view-based) model where the data stored inside the object is a proxy to a Vulkan object that can dynamically be swapped around.
**If you are cherry-picking a commit into an rc/ branch**: add the release note under the
appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md).
+6
View File
@@ -7,6 +7,12 @@ A new header is inserted each time a *tag* is created.
Instead, if you are authoring a PR for the main branch, add your release note to
[NEW_RELEASE_NOTES.md](./NEW_RELEASE_NOTES.md).
## v1.66.1
- filamat: Removed a dependency on Glslang's deprecated SPIR-V remapper.
The functionality is already implemented by calling the CanonicalizeIds pass
in the SPIRV-Tools, and should be a non-functional change.
## v1.66.0
- materials: include default values of spec constants in material metadata [⚠️ **Recompile Materials**]
@@ -208,3 +208,19 @@ Java_com_google_android_filament_Renderer_nSetVsyncTime(JNIEnv *, jclass,
Renderer *renderer = (Renderer *) nativeRenderer;
renderer->setVsyncTime(steadyClockTimeNano);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_Renderer_nSkipNextFrames(JNIEnv *, jclass ,
jlong nativeRenderer, jint frameCount) {
Renderer *renderer = (Renderer *) nativeRenderer;
renderer->skipNextFrames(frameCount);
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_google_android_filament_Renderer_nGetFrameToSkipCount(JNIEnv *, jclass ,
jlong nativeRenderer) {
Renderer *renderer = (Renderer *) nativeRenderer;
return renderer->getFrameToSkipCount();
}
@@ -732,6 +732,22 @@ public class Renderer {
nResetUserTime(getNativeObject());
}
/**
* Requests the next frameCount frames to be skipped. For Debugging.
* @param frameCount number of frames to skip.
*/
public void skipNextFrames(int frameCount) {
nSkipNextFrames(getNativeObject(), frameCount);
}
/**
* Remainder count of frame to be skipped
* @return remaining frames to be skipped
*/
public int getFrameToSkipCount() {
return nGetFrameToSkipCount(getNativeObject());
}
public long getNativeObject() {
if (mNativeObject == 0) {
throw new IllegalStateException("Calling method on destroyed Renderer");
@@ -773,4 +789,7 @@ public class Renderer {
float interval, float headRoomRatio, float scaleRate, int history);
private static native void nSetClearOptions(long nativeRenderer,
float r, float g, float b, float a, boolean clear, boolean discard);
private static native void nSkipNextFrames(long nativeObject, int frameCount);
private static native int nGetFrameToSkipCount(long nativeObject);
}
+1 -1
View File
@@ -434,7 +434,7 @@ to create a quick localhost server:</p>
<p>Alternatively, if you have node installed you can use the
<a href="https://www.npmjs.com/package/live-server">live-server</a> package, which automatically refreshes the
web page when it detects a change.</p>
<p>Each sample app has its own handwritten html file. Additionally the server folder contains assets
<p>Each sample app has its own handwritten html file. Additionally, the server folder contains assets
such as meshes, textures, and materials.</p>
<h2 id="running-the-native-samples"><a class="header" href="#running-the-native-samples">Running the native samples</a></h2>
<p>The <code>samples/</code> directory contains several examples of how to use Filament with SDL2.</p>
+2 -2
View File
@@ -181,7 +181,7 @@ important for <code>matc</code> (material compiler).</p>
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.65.3'
implementation 'com.google.android.filament:filament-android:1.66.0'
}
</code></pre>
<p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p>
@@ -196,7 +196,7 @@ dependencies {
</div>
<h3 id="ios"><a class="header" href="#ios">iOS</a></h3>
<p>iOS projects can use CocoaPods to install the latest release:</p>
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.65.3'
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.66.0'
</code></pre>
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
<ul>
+9 -5
View File
@@ -206,11 +206,15 @@ golden repo.</li>
</code></pre>
</li>
</ul>
<h2 id="filtering-tests"><a class="header" href="#filtering-tests">Filtering Tests</a></h2>
<p>You can run a subset of the tests by passing the <code>--test_filter</code> flag to <code>local_test.sh</code>. The
filter supports wildcards (<code>*</code>) to match test names. For example, to run all tests in the
<code>ClearCoat</code> suite, you can use the following command:</p>
<pre><code>bash test/renderdiff/local_test.sh --test_filter='ClearCoat.*.*'
<h2 id="command-line-options"><a class="header" href="#command-line-options">Command-line Options</a></h2>
<p>You can control the behavior of the test scripts with the following flags passed to <code>local_test.sh</code>:</p>
<ul>
<li><code>--test_filter=&lt;filter&gt;</code>: Run a subset of tests. The filter supports wildcards (<code>*</code>) to match test names.</li>
<li><code>--no_rebuild</code>: Skip rebuilding the <code>gltf_viewer</code> executable.</li>
<li><code>--num_threads=&lt;number&gt;</code>: Set the number of threads for rendering. If not set, the system's default is used.</li>
</ul>
<p>For example, to run all <code>MSAA</code> tests without rebuilding and using 8 threads:</p>
<pre><code>bash test/renderdiff/local_test.sh --test_filter='MSAA.*.*' --no_rebuild --num_threads=8
</code></pre>
<h2 id="update-the-golden-images"><a class="header" href="#update-the-golden-images">Update the golden images</a></h2>
<p>The golden images are stored in a github repository:
+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
@@ -117,6 +117,7 @@ set(SRCS
src/components/LightManager.cpp
src/components/RenderableManager.cpp
src/components/TransformManager.cpp
src/details/BufferAllocator.cpp
src/details/BufferObject.cpp
src/details/Camera.cpp
src/details/ColorGrading.cpp
@@ -196,6 +197,7 @@ set(PRIVATE_HDRS
src/components/LightManager.h
src/components/RenderableManager.h
src/components/TransformManager.h
src/details/BufferAllocator.h
src/details/BufferObject.h
src/details/Camera.h
src/details/ColorGrading.h
+2
View File
@@ -294,6 +294,8 @@ if (FILAMENT_SUPPORTS_WEBGPU)
src/webgpu/WebGPUPipelineLayoutCache.h
src/webgpu/WebGPUProgram.cpp
src/webgpu/WebGPUProgram.h
src/webgpu/WebGPUQueueManager.cpp
src/webgpu/WebGPUQueueManager.h
src/webgpu/WebGPURenderPassMipmapGenerator.cpp
src/webgpu/WebGPURenderPassMipmapGenerator.h
src/webgpu/WebGPURenderPrimitive.h
@@ -760,6 +760,7 @@ void OpenGLContext::initExtensionsGLES(Extensions* ext, GLint major, GLint minor
ext->EXT_texture_compression_rgtc = exts.has("GL_EXT_texture_compression_rgtc"sv);
ext->EXT_texture_compression_bptc = exts.has("GL_EXT_texture_compression_bptc"sv);
ext->EXT_texture_cube_map_array = exts.has("GL_EXT_texture_cube_map_array"sv) || exts.has("GL_OES_texture_cube_map_array"sv);
ext->EXT_texture_filter_anisotropic = exts.has("GL_EXT_texture_filter_anisotropic"sv);
ext->GOOGLE_cpp_style_line_directive = exts.has("GL_GOOGLE_cpp_style_line_directive"sv);
ext->KHR_debug = exts.has("GL_KHR_debug"sv);
ext->KHR_parallel_shader_compile = exts.has("GL_KHR_parallel_shader_compile"sv);
+73 -106
View File
@@ -17,19 +17,19 @@
#include "WebGPUBlitter.h"
#include "WebGPUConstants.h"
#include "WebGPUStrings.h"
#include "WebGPUTextureHelpers.h"
#include "webgpu/utils/StringPlaceholderTemplateProcessor.h"
#include <backend/DriverEnums.h>
#include <webgpu/webgpu_cpp.h>
#include <math/vec2.h>
#include <utils/Hash.h>
#include <utils/Panic.h>
#include <webgpu/webgpu_cpp.h>
#include <cstdint>
#include <string.h>
#include <string>
#include <string_view>
#include <unordered_map>
@@ -395,10 +395,15 @@ void WebGPUBlitter::blit(wgpu::Queue const& queue, wgpu::CommandEncoder const& c
const bool multisampledSource{ args.source.texture.GetSampleCount() > 1 };
const bool depthSource{ hasDepth(args.source.texture.GetFormat()) };
const bool depthDestination{ hasDepth(args.destination.texture.GetFormat()) };
const PipelineLayoutKey pipelineLayoutKey{
.sourceDimension = sourceDimension,
.filterType = args.filter,
.multisampledSource = multisampledSource,
.depthSource = depthSource,
};
const wgpu::BindGroupDescriptor textureBindGroupDescriptor{
.label = "blit_texture_bind_group",
.layout = getOrCreateTextureBindGroupLayout(args.filter, sourceDimension,
multisampledSource, depthSource),
.layout = getOrCreateTextureBindGroupLayout(pipelineLayoutKey),
.entryCount = textureBindGroupEntriesCount,
.entries = textureBindGroupEntries,
};
@@ -447,9 +452,14 @@ void WebGPUBlitter::blit(wgpu::Queue const& queue, wgpu::CommandEncoder const& c
&renderPassDescriptor) };
FILAMENT_CHECK_POSTCONDITION(renderPassEncoder)
<< "Failed to create render pass encoder for blit.";
renderPassEncoder.SetPipeline(getOrCreateRenderPipeline(args.filter, sourceDimension,
args.source.texture.GetSampleCount(), depthSource,
args.destination.texture.GetFormat()));
const RenderPipelineKey renderPipelineKey{
.sourceDimension = sourceDimension,
.destinationTextureFormat = args.destination.texture.GetFormat(),
.sourceSampleCount = static_cast<uint8_t>(args.source.texture.GetSampleCount()),
.filterType = args.filter,
.depthSource = depthSource,
};
renderPassEncoder.SetPipeline(getOrCreateRenderPipeline(renderPipelineKey));
renderPassEncoder.SetBindGroup(TEXTURE_BIND_GROUP_INDEX, textureBindGroup);
renderPassEncoder.Draw(3); // draw the full-screen triangle
// with hard-coded vertices in the shader
@@ -527,14 +537,9 @@ void WebGPUBlitter::createSampler(const SamplerMagFilter filter) {
// Caches and returns a render pipeline for a given blit configuration.
// If a pipeline for the given configuration does not exist, it creates and caches one.
wgpu::RenderPipeline const& WebGPUBlitter::getOrCreateRenderPipeline(
const SamplerMagFilter filterType, const wgpu::TextureViewDimension sourceDimension,
const uint32_t sourceSampleCount, const bool depthSource,
const wgpu::TextureFormat destinationTextureFormat) {
const size_t key{ hashRenderPipelineKey(filterType, sourceDimension, sourceSampleCount,
depthSource, destinationTextureFormat) };
RenderPipelineKey const& key) {
if (mRenderPipelines.find(key) == mRenderPipelines.end()) {
mRenderPipelines[key] = createRenderPipeline(filterType, sourceDimension, sourceSampleCount,
depthSource, destinationTextureFormat);
mRenderPipelines[key] = createRenderPipeline(key);
}
return mRenderPipelines[key];
}
@@ -542,18 +547,21 @@ wgpu::RenderPipeline const& WebGPUBlitter::getOrCreateRenderPipeline(
// Creates a render pipeline for a blit operation.
// The pipeline is configured with the appropriate vertex and fragment shaders,
// pipeline layout, and render target state based on the blit parameters.
wgpu::RenderPipeline WebGPUBlitter::createRenderPipeline(const SamplerMagFilter filterType,
const wgpu::TextureViewDimension sourceDimension, const uint32_t sourceSampleCount,
const bool depthSource, const wgpu::TextureFormat destinationTextureFormat) {
const bool depthDestination{ hasDepth(destinationTextureFormat) };
const wgpu::ColorTargetState colorTargetState{ .format = destinationTextureFormat };
wgpu::RenderPipeline WebGPUBlitter::createRenderPipeline(RenderPipelineKey const& key) {
const bool depthDestination{ hasDepth(key.destinationTextureFormat) };
const wgpu::ColorTargetState colorTargetState{ .format = key.destinationTextureFormat };
const wgpu::DepthStencilState depthStencilState{
.format = destinationTextureFormat,
.format = key.destinationTextureFormat,
.depthWriteEnabled = true,
.depthCompare = wgpu::CompareFunction::Always,
};
wgpu::ShaderModule const& shaderModule{ getOrCreateShaderModule(sourceDimension,
sourceSampleCount > 1, depthSource, hasDepth(destinationTextureFormat)) };
const ShaderModuleKey shaderModuleKey{
.sourceDimension = key.sourceDimension,
.multisampledSource = key.sourceSampleCount > 1,
.depthSource = key.depthSource,
.depthDestination = hasDepth(key.destinationTextureFormat),
};
wgpu::ShaderModule const& shaderModule{ getOrCreateShaderModule(shaderModuleKey) };
const wgpu::FragmentState fragmentState{
.module = shaderModule,
.entryPoint = FRAGMENT_SHADER_ENTRY_POINT,
@@ -562,9 +570,15 @@ wgpu::RenderPipeline WebGPUBlitter::createRenderPipeline(const SamplerMagFilter
.targetCount = depthDestination ? 0u : 1u,
.targets = depthDestination ? nullptr : &colorTargetState,
};
const PipelineLayoutKey pipelineLayoutKey{
.sourceDimension = key.sourceDimension,
.filterType = key.filterType,
.multisampledSource = key.sourceSampleCount > 1,
.depthSource = key.depthSource,
};
const wgpu::RenderPipelineDescriptor pipelineDescriptor{
.label = "render_pass_blit_pipeline",
.layout = getOrCreatePipelineLayout(filterType, sourceDimension, sourceSampleCount > 1, depthSource),
.layout = getOrCreatePipelineLayout(pipelineLayoutKey),
.vertex = {
.module = shaderModule,
.entryPoint = VERTEX_SHADER_ENTRY_POINT,
@@ -586,7 +600,7 @@ wgpu::RenderPipeline WebGPUBlitter::createRenderPipeline(const SamplerMagFilter
},
.depthStencil = depthDestination ? &depthStencilState : nullptr,
.multisample = {
.count = sourceSampleCount,
.count = key.sourceSampleCount,
.mask = 0xFFFFFFFF,
.alphaToCoverageEnabled = false,
},
@@ -597,41 +611,27 @@ wgpu::RenderPipeline WebGPUBlitter::createRenderPipeline(const SamplerMagFilter
return pipeline;
}
size_t WebGPUBlitter::hashRenderPipelineKey(const SamplerMagFilter filterType,
const wgpu::TextureViewDimension sourceDimension, const uint32_t sourceSampleCount,
const bool depthSource, const wgpu::TextureFormat destinationTextureFormat) {
size_t seed{ std::hash<uint8_t>{}(static_cast<uint8_t>(filterType)) };
utils::hash::combine(seed, static_cast<uint32_t>(sourceDimension));
utils::hash::combine(seed, sourceSampleCount);
utils::hash::combine(seed, depthSource);
utils::hash::combine(seed, static_cast<uint32_t>(destinationTextureFormat));
return seed;
bool WebGPUBlitter::RenderPipelineKey::operator==(RenderPipelineKey const& other) const {
return memcmp(this, &other, sizeof(RenderPipelineKey)) == 0;
}
// Caches and returns a pipeline layout for a given blit configuration.
// If a layout for the given configuration does not exist, it creates and caches one.
wgpu::PipelineLayout const& WebGPUBlitter::getOrCreatePipelineLayout(
const SamplerMagFilter filterType, const wgpu::TextureViewDimension sourceDimension,
const bool multisampledSource, const bool depthSource) {
const size_t key{ hashPipelineLayoutKey(filterType, sourceDimension, multisampledSource,
depthSource) };
PipelineLayoutKey const& key) {
if (mPipelineLayouts.find(key) == mPipelineLayouts.end()) {
mPipelineLayouts[key] =
createPipelineLayout(filterType, sourceDimension, multisampledSource, depthSource);
mPipelineLayouts[key] = createPipelineLayout(key);
}
return mPipelineLayouts[key];
}
// Creates a pipeline layout for a blit operation.
// The layout defines the bind group layouts used by the blit pipeline.
wgpu::PipelineLayout WebGPUBlitter::createPipelineLayout(const SamplerMagFilter filterType,
const wgpu::TextureViewDimension sourceDimension, const bool multisampledSource,
const bool depthSource) {
wgpu::PipelineLayout WebGPUBlitter::createPipelineLayout(PipelineLayoutKey const& key) {
const wgpu::PipelineLayoutDescriptor pipelineLayoutDescriptor{
.label = "render_pass_blit_pipeline_layout",
.bindGroupLayoutCount = 1,
.bindGroupLayouts = &getOrCreateTextureBindGroupLayout(filterType, sourceDimension,
multisampledSource, depthSource),
.bindGroupLayouts = &getOrCreateTextureBindGroupLayout(key),
};
const wgpu::PipelineLayout pipelineLayout{ mDevice.CreatePipelineLayout(
&pipelineLayoutDescriptor) };
@@ -640,26 +640,16 @@ wgpu::PipelineLayout WebGPUBlitter::createPipelineLayout(const SamplerMagFilter
return pipelineLayout;
}
size_t WebGPUBlitter::hashPipelineLayoutKey(const SamplerMagFilter filterType,
const wgpu::TextureViewDimension sourceDimension, const bool multisampledSource,
const bool depthSource) {
size_t seed{ std::hash<uint8_t>{}(static_cast<uint8_t>(filterType)) };
utils::hash::combine(seed, static_cast<uint32_t>(sourceDimension));
utils::hash::combine(seed, multisampledSource);
utils::hash::combine(seed, depthSource);
return seed;
bool WebGPUBlitter::PipelineLayoutKey::operator==(const PipelineLayoutKey& other) const {
return memcmp(this, &other, sizeof(PipelineLayoutKey)) == 0;
}
// Caches and returns a bind group layout for a given blit configuration.
// If a layout for the given configuration does not exist, it creates and caches one.
wgpu::BindGroupLayout const& WebGPUBlitter::getOrCreateTextureBindGroupLayout(
const SamplerMagFilter filterType, const wgpu::TextureViewDimension sourceDimension,
const bool multisampledSource, const bool depthSource) {
const size_t key{ hashTextureBindGroupLayoutKey(filterType, sourceDimension, multisampledSource,
depthSource) };
PipelineLayoutKey const& key) {
if (mTextureBindGroupLayouts.find(key) == mTextureBindGroupLayouts.end()) {
mTextureBindGroupLayouts[key] = createTextureBindGroupLayout(filterType, sourceDimension,
multisampledSource, depthSource);
mTextureBindGroupLayouts[key] = createTextureBindGroupLayout(key);
}
return mTextureBindGroupLayouts[key];
}
@@ -667,23 +657,21 @@ wgpu::BindGroupLayout const& WebGPUBlitter::getOrCreateTextureBindGroupLayout(
// Creates a bind group layout for the blit operation's texture resources.
// This layout specifies the bindings for the source texture, a uniform buffer with blit parameters,
// and optionally a sampler.
wgpu::BindGroupLayout WebGPUBlitter::createTextureBindGroupLayout(const SamplerMagFilter filterType,
const wgpu::TextureViewDimension sourceDimension, const bool multisampledSource,
const bool depthSource) {
const wgpu::BindGroupLayoutEntry bindGroupLayoutEntries[MAX_TEXTURE_BIND_GROUP_ENTRY_SIZE] {
wgpu::BindGroupLayout WebGPUBlitter::createTextureBindGroupLayout(PipelineLayoutKey const& key) {
const wgpu::BindGroupLayoutEntry bindGroupLayoutEntries[MAX_TEXTURE_BIND_GROUP_ENTRY_SIZE]{
{
.binding = TEXTURE_BINDING_INDEX,
.visibility = wgpu::ShaderStage::Fragment,
.texture = {
.sampleType = depthSource
.sampleType = key.depthSource
? wgpu::TextureSampleType::Depth
: (multisampledSource
: (key.multisampledSource
? wgpu::TextureSampleType::UnfilterableFloat
: wgpu::TextureSampleType::Float), // only F32 scalar sample
// type supported for now
// (aside from depth)
.viewDimension = sourceDimension,
.multisampled = multisampledSource,
.viewDimension = key.sourceDimension,
.multisampled = key.multisampledSource,
},
},
{
@@ -699,7 +687,7 @@ wgpu::BindGroupLayout WebGPUBlitter::createTextureBindGroupLayout(const SamplerM
.binding = SAMPLER_BINDING_INDEX,
.visibility = wgpu::ShaderStage::Fragment,
.sampler = {
.type =filterType == SamplerMagFilter::LINEAR
.type = key.filterType == SamplerMagFilter::LINEAR
? wgpu::SamplerBindingType::Filtering
: wgpu::SamplerBindingType::NonFiltering,
},
@@ -708,8 +696,8 @@ wgpu::BindGroupLayout WebGPUBlitter::createTextureBindGroupLayout(const SamplerM
const wgpu::BindGroupLayoutDescriptor textureBindGroupLayoutDescriptor{
.label = "render_pass_blit_texture_bind_group_layout",
// TODO, doesnt make any sense but gets rid of the error. Are the entries 0 based?
.entryCount = multisampledSource ? (MAX_TEXTURE_BIND_GROUP_ENTRY_SIZE - 1)
: (MAX_TEXTURE_BIND_GROUP_ENTRY_SIZE),
.entryCount = key.multisampledSource ? (MAX_TEXTURE_BIND_GROUP_ENTRY_SIZE - 1)
: (MAX_TEXTURE_BIND_GROUP_ENTRY_SIZE),
.entries = bindGroupLayoutEntries,
};
const wgpu::BindGroupLayout textureBindGroupLayout{ mDevice.CreateBindGroupLayout(
@@ -719,26 +707,11 @@ wgpu::BindGroupLayout WebGPUBlitter::createTextureBindGroupLayout(const SamplerM
return textureBindGroupLayout;
}
size_t WebGPUBlitter::hashTextureBindGroupLayoutKey(const SamplerMagFilter filterType,
const wgpu::TextureViewDimension sourceDimension, const bool multisampledSource,
const bool depthSource) {
size_t seed{ std::hash<uint8_t>{}(static_cast<uint8_t>(filterType)) };
utils::hash::combine(seed, static_cast<uint32_t>(sourceDimension));
utils::hash::combine(seed, multisampledSource);
utils::hash::combine(seed, depthSource);
return seed;
}
// Caches and returns a shader module for a given blit configuration.
// If a shader module for the given configuration does not exist, it creates and caches one.
wgpu::ShaderModule const& WebGPUBlitter::getOrCreateShaderModule(
const wgpu::TextureViewDimension sourceDimension, const bool multisampledSource,
const bool depthSource, const bool depthDestination) {
const size_t key{ hashShaderModuleKey(sourceDimension, multisampledSource, depthSource,
depthDestination) };
wgpu::ShaderModule const& WebGPUBlitter::getOrCreateShaderModule(ShaderModuleKey const& key) {
if (mShaderModules.find(key) == mShaderModules.end()) {
mShaderModules[key] = createShaderModule(sourceDimension, multisampledSource, depthSource,
depthDestination);
mShaderModules[key] = createShaderModule(key);
}
return mShaderModules[key];
}
@@ -746,46 +719,45 @@ wgpu::ShaderModule const& WebGPUBlitter::getOrCreateShaderModule(
// Creates a shader module containing the vertex and fragment shaders for the blit operation.
// The shader source is generated from a template, with placeholders filled in based on the
// blit configuration (e.g., texture type, sample count).
wgpu::ShaderModule WebGPUBlitter::createShaderModule(
const wgpu::TextureViewDimension sourceDimension, const bool multisampledSource,
const bool depthSource, const bool depthDestination) {
wgpu::ShaderModule WebGPUBlitter::createShaderModule(ShaderModuleKey const& key) {
std::string_view textureType;
if (depthSource) {
if (multisampledSource) {
if (key.depthSource) {
if (key.multisampledSource) {
textureType = "texture_depth_multisampled_2d";
} else {
textureType = "texture_depth_2d";
}
} else {
if (multisampledSource) {
if (key.multisampledSource) {
textureType = "texture_multisampled_2d<f32>";
} else {
if (sourceDimension == wgpu::TextureViewDimension::e3D) {
if (key.sourceDimension == wgpu::TextureViewDimension::e3D) {
textureType = "texture_3d<f32>";
} else {
textureType = "texture_2d<f32>";
}
}
}
const std::string_view texture2dType{ depthSource ? "texture_depth_2d" : "texture_2d<f32>" };
const std::string_view texture2dType{ key.depthSource ? "texture_depth_2d"
: "texture_2d<f32>" };
// we don't declare a sampler in the shader or pipeline for the multisampled case
const std::string_view samplerDeclaration{
multisampledSource ? "" : "@group(0) @binding(2) var sourceSampler: sampler;"
key.multisampledSource ? "" : "@group(0) @binding(2) var sourceSampler: sampler;"
};
const std::string_view fragmentReturnAttributeAndType{
depthDestination ? "@builtin(frag_depth) f32" : "@location(0) vec4<f32>"
key.depthDestination ? "@builtin(frag_depth) f32" : "@location(0) vec4<f32>"
};
const std::unordered_map<std::string_view, std::string_view>
valueByPlaceholderNameForFragmentSnippet{
{ FRAGMENT_RETURN_ATTRIBUTE_AND_TYPE_PLACEHOLDER, fragmentReturnAttributeAndType },
};
std::string fragmentShaderSnippet;
if (multisampledSource) {
if (key.multisampledSource) {
fragmentShaderSnippet = webgpuutils::processPlaceholderTemplate(
FRAGMENT_SHADER_SNIPPET_MSAA_INPUT_TEMPLATE, PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX,
valueByPlaceholderNameForFragmentSnippet);
} else {
if (sourceDimension == wgpu::TextureViewDimension::e3D) {
if (key.sourceDimension == wgpu::TextureViewDimension::e3D) {
fragmentShaderSnippet = webgpuutils::processPlaceholderTemplate(
FRAGMENT_SHADER_SNIPPET_3D_INPUT_TEMPLATE, PLACEHOLDER_PREFIX,
PLACEHOLDER_SUFFIX, valueByPlaceholderNameForFragmentSnippet);
@@ -815,13 +787,8 @@ wgpu::ShaderModule WebGPUBlitter::createShaderModule(
return shaderModule;
}
size_t WebGPUBlitter::hashShaderModuleKey(const wgpu::TextureViewDimension sourceDimension,
const bool multisampledSource, const bool depthSource, const bool depthDestination) {
size_t seed{ std::hash<uint32_t>{}(static_cast<uint32_t>(sourceDimension)) };
utils::hash::combine(seed, multisampledSource);
utils::hash::combine(seed, depthSource);
utils::hash::combine(seed, depthDestination);
return seed;
bool WebGPUBlitter::ShaderModuleKey::operator==(const ShaderModuleKey& other) const {
return memcmp(this, &other, sizeof(ShaderModuleKey)) == 0;
}
} // namespace filament::backend
+53 -33
View File
@@ -76,55 +76,75 @@ private:
// The following methods are used to create and cache WebGPU objects.
// The pattern is to have a `getOrCreate...` method that looks up the object in a cache,
// and if it's not found, it calls a `create...` method to create it and then stores it in the
// cache. A `hash...` method is used to generate a key for the cache.
// cache.
[[nodiscard]] wgpu::RenderPipeline const& getOrCreateRenderPipeline(SamplerMagFilter,
wgpu::TextureViewDimension sourceDimension, uint32_t sourceSampleCount,
bool depthSource, wgpu::TextureFormat destinationTextureFormat);
struct RenderPipelineKey;
struct PipelineLayoutKey;
struct ShaderModuleKey;
[[nodiscard]] wgpu::RenderPipeline createRenderPipeline(SamplerMagFilter,
wgpu::TextureViewDimension sourceDimension, uint32_t sourceSampleCount,
bool depthSource, wgpu::TextureFormat destinationTextureFormat);
[[nodiscard]] wgpu::RenderPipeline const& getOrCreateRenderPipeline(RenderPipelineKey const&);
[[nodiscard]] wgpu::RenderPipeline createRenderPipeline(RenderPipelineKey const&);
[[nodiscard]] static size_t hashRenderPipelineKey(SamplerMagFilter,
wgpu::TextureViewDimension sourceDimension, uint32_t sourceSampleCount,
bool depthSource, wgpu::TextureFormat destinationTextureFormat);
[[nodiscard]] wgpu::PipelineLayout const& getOrCreatePipelineLayout(PipelineLayoutKey const&);
[[nodiscard]] wgpu::PipelineLayout createPipelineLayout(PipelineLayoutKey const&);
[[nodiscard]] wgpu::PipelineLayout const& getOrCreatePipelineLayout(SamplerMagFilter,
wgpu::TextureViewDimension, bool multisampledSource, bool depthSource);
[[nodiscard]] wgpu::BindGroupLayout const& getOrCreateTextureBindGroupLayout(
PipelineLayoutKey const&);
[[nodiscard]] wgpu::BindGroupLayout createTextureBindGroupLayout(PipelineLayoutKey const&);
[[nodiscard]] wgpu::PipelineLayout createPipelineLayout(SamplerMagFilter,
wgpu::TextureViewDimension sourceDimension, bool multisampledSource, bool depthSource);
[[nodiscard]] wgpu::ShaderModule const& getOrCreateShaderModule(ShaderModuleKey const&);
[[nodiscard]] wgpu::ShaderModule createShaderModule(ShaderModuleKey const&);
[[nodiscard]] static size_t hashPipelineLayoutKey(SamplerMagFilter,
wgpu::TextureViewDimension sourceDimension, bool multisampledSource, bool depthSource);
struct RenderPipelineKey {
wgpu::TextureViewDimension sourceDimension; // 4 bytes
wgpu::TextureFormat destinationTextureFormat; // 4
uint8_t sourceSampleCount; // 1
SamplerMagFilter filterType; // 1
bool depthSource; // 1
uint8_t padding = 0; // 1
[[nodiscard]] wgpu::BindGroupLayout const& getOrCreateTextureBindGroupLayout(SamplerMagFilter,
wgpu::TextureViewDimension sourceDimension, bool multisampledSource, bool depthSource);
bool operator==(const RenderPipelineKey& other) const;
using Hasher = utils::hash::MurmurHashFn<RenderPipelineKey>;
};
[[nodiscard]] wgpu::BindGroupLayout createTextureBindGroupLayout(SamplerMagFilter,
wgpu::TextureViewDimension sourceDimension, bool multisampledSource, bool depthSource);
struct PipelineLayoutKey {
wgpu::TextureViewDimension sourceDimension; // 4 bytes
SamplerMagFilter filterType; // 1
bool multisampledSource; // 1
bool depthSource; // 1
uint8_t padding = 0; // 1
[[nodiscard]] static size_t hashTextureBindGroupLayoutKey(SamplerMagFilter,
wgpu::TextureViewDimension sourceDimension, bool multisampledSource, bool depthSource);
bool operator==(const PipelineLayoutKey& other) const;
using Hasher = utils::hash::MurmurHashFn<PipelineLayoutKey>;
};
[[nodiscard]] wgpu::ShaderModule const& getOrCreateShaderModule(
wgpu::TextureViewDimension sourceDimension, bool multisampledSource, bool depthSource,
bool depthDestination);
struct ShaderModuleKey {
wgpu::TextureViewDimension sourceDimension; // 4 bytes
bool multisampledSource; // 1
bool depthSource; // 1
bool depthDestination; // 1
uint8_t padding = 0; // 1
[[nodiscard]] wgpu::ShaderModule createShaderModule(wgpu::TextureViewDimension sourceDimension,
bool multisampledSource, bool depthSource, bool depthDestination);
bool operator==(const ShaderModuleKey& other) const;
using Hasher = utils::hash::MurmurHashFn<ShaderModuleKey>;
};
[[nodiscard]] static size_t hashShaderModuleKey(wgpu::TextureViewDimension sourceDimension,
bool multisampledSource, bool depthSource, bool depthDestination);
static_assert(sizeof(RenderPipelineKey) == 12,
"RenderPipelineKey must not have implicit padding.");
static_assert(sizeof(PipelineLayoutKey) == 8,
"PipelineLayoutKey must not have implicit padding.");
static_assert(sizeof(ShaderModuleKey) == 8, "ShaderModuleKey must not have implicit padding.");
wgpu::Device mDevice;
wgpu::Sampler mNearestSampler{ nullptr };
wgpu::Sampler mLinearSampler{ nullptr };
tsl::robin_map<size_t, wgpu::RenderPipeline> mRenderPipelines{};
tsl::robin_map<size_t, wgpu::PipelineLayout> mPipelineLayouts{};
tsl::robin_map<size_t, wgpu::BindGroupLayout> mTextureBindGroupLayouts{};
tsl::robin_map<size_t, wgpu::ShaderModule> mShaderModules{};
tsl::robin_map<RenderPipelineKey, wgpu::RenderPipeline, RenderPipelineKey::Hasher>
mRenderPipelines{};
tsl::robin_map<PipelineLayoutKey, wgpu::PipelineLayout, PipelineLayoutKey::Hasher>
mPipelineLayouts{};
tsl::robin_map<PipelineLayoutKey, wgpu::BindGroupLayout, PipelineLayoutKey::Hasher>
mTextureBindGroupLayouts{};
tsl::robin_map<ShaderModuleKey, wgpu::ShaderModule, ShaderModuleKey::Hasher> mShaderModules{};
};
} // namespace filament::backend
+46 -249
View File
@@ -57,11 +57,9 @@
#include <algorithm>
#include <array>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <sstream>
#include <utility>
using namespace std::chrono_literals;
@@ -108,10 +106,10 @@ WebGPUDriver::WebGPUDriver(WebGPUPlatform& platform,
: mPlatform{ platform },
mAdapter{ mPlatform.requestAdapter(nullptr) },
mDevice{ mPlatform.requestDevice(mAdapter) },
mQueue{ mDevice.GetQueue() },
mQueueManager{ mDevice },
mPipelineLayoutCache{ mDevice },
mPipelineCache{ mDevice },
mRenderPassMipmapGenerator{ mDevice },
mRenderPassMipmapGenerator{ mDevice, &mQueueManager },
mSpdComputePassMipmapGenerator{ mDevice },
mBlitter{ mDevice },
mHandleAllocator{ "Handles", driverConfig.handleArenaSize,
@@ -143,6 +141,7 @@ template class ConcreteDispatcher<WebGPUDriver>;
void WebGPUDriver::terminate() {
finish();
}
void WebGPUDriver::tick(int) {
@@ -183,62 +182,13 @@ void WebGPUDriver::endFrame(const uint32_t /* frameId */) {
// If a command encoder is in flight then the encoder is finished and submitted to the GPU queue.
// A new command encoder is created fresh after it.
// If a command encoder is not in flight (nullptr), then this is a noop.
void WebGPUDriver::flush(int /* dummy */) {
FWGPU_SYSTRACE_SCOPE();
if (mCommandEncoder == nullptr) {
return;
}
// Submit the commands recorded so far.
assert_invariant(mRenderPassEncoder == nullptr);
wgpu::CommandBufferDescriptor commandBufferDescriptor{
.label = "frame_command_after_flush",
};
mCommandBuffer = mCommandEncoder.Finish(&commandBufferDescriptor);
assert_invariant(mCommandBuffer);
mQueue.Submit(1, &mCommandBuffer);
mCommandBuffer = nullptr;
// create a new command buffer encoder to continue recording the next command for frame...
wgpu::CommandEncoderDescriptor commandEncoderDescriptor = {
.label = "frame_command_after_flush"
};
mCommandEncoder = mDevice.CreateCommandEncoder(&commandEncoderDescriptor);
assert_invariant(mCommandEncoder);
void WebGPUDriver::flush(int) {
mQueueManager.flush();
}
// Submits the currently recorded commands and waits for them to complete on the GPU.
// This is a synchronous operation and should be used sparingly.
void WebGPUDriver::finish(int /* dummy */) {
FWGPU_SYSTRACE_SCOPE();
mDevice.Tick();
mAdapter.GetInstance().ProcessEvents();
mReadPixelMapsCounter.waitForAllToFinish();
flush();
// Some applications/samples might call finish in the very beginning of an application before
// creating any resources/tasks to flush out the system on any pending work from previous runs.
// Currently, gltf_viewer sample on android exhibit this behavior.
// Bail out early to safeguard against those cases. Otherwise, OnSubmittedWorkDone will
// hang on work that was never submitted.
if (mCommandEncoder == nullptr) {
return;
}
// Wait for all previously submitted work to finish.
std::mutex syncPoint;
std::condition_variable syncCondition;
bool done = false;
mQueue.OnSubmittedWorkDone(wgpu::CallbackMode::AllowSpontaneous,
[&syncPoint, &syncCondition, &done](wgpu::QueueWorkDoneStatus status, wgpu::StringView message) {
if (status != wgpu::QueueWorkDoneStatus::Success)
{
FWGPU_LOGW << "WebGPUDriver::finish: QueueWorkDoneStatus was not successful. " << message;
}
assert_invariant(status == wgpu::QueueWorkDoneStatus::Success);
std::unique_lock<std::mutex> lock(syncPoint);
done = true;
syncCondition.notify_one();
});
std::unique_lock<std::mutex> lock(syncPoint);
syncCondition.wait(lock, [&done] { return done; });
mQueueManager.finish();
}
void WebGPUDriver::destroyRenderPrimitive(Handle<HwRenderPrimitive> rph) {
@@ -655,8 +605,7 @@ void WebGPUDriver::createRenderTargetR(Handle<HwRenderTarget> renderTargetHandle
void WebGPUDriver::createFenceR(Handle<HwFence> fenceHandle, utils::ImmutableCString&& tag) {
// The handle is constructed synchronously in createFenceS.
const auto fence = handleCast<WebGPUFence>(fenceHandle);
assert_invariant(mQueue);
fence->addMarkerToQueueState(mQueue);
fence->setSubmissionState(mQueueManager.getLatestSubmissionState());
setDebugTag(fenceHandle.getId(), std::move(tag));
}
@@ -899,7 +848,7 @@ void WebGPUDriver::updateIndexBuffer(Handle<HwIndexBuffer> indexBufferHandle,
// draw calls are made.
flush();
handleCast<WebGPUIndexBuffer>(indexBufferHandle)
->updateGPUBuffer(bufferDescriptor, byteOffset, mQueue);
->updateGPUBuffer(bufferDescriptor, byteOffset, mDevice.GetQueue());
scheduleDestroy(std::move(bufferDescriptor));
}
@@ -910,18 +859,14 @@ void WebGPUDriver::updateBufferObject(Handle<HwBufferObject> bufferObjectHandle,
// draw calls are made.
flush();
handleCast<WebGPUBufferObject>(bufferObjectHandle)
->updateGPUBuffer(bufferDescriptor, byteOffset, mQueue);
->updateGPUBuffer(bufferDescriptor, byteOffset, mDevice.GetQueue());
scheduleDestroy(std::move(bufferDescriptor));
}
void WebGPUDriver::updateBufferObjectUnsynchronized(Handle<HwBufferObject> bufferObjectHandle,
BufferDescriptor&& bufferDescriptor, const uint32_t byteOffset) {
// make sure command elements (draws, etc.) prior to the buffer update are processed before the
// update on the GPU, otherwise the expected data may not be available at the time certain
// draw calls are made.
flush();
handleCast<WebGPUBufferObject>(bufferObjectHandle)
->updateGPUBuffer(bufferDescriptor, byteOffset, mQueue);
->updateGPUBuffer(bufferDescriptor, byteOffset, mDevice.GetQueue());
scheduleDestroy(std::move(bufferDescriptor));
}
@@ -1043,23 +988,14 @@ void WebGPUDriver::update3DImage(Handle<HwTexture> textureHandle, const uint32_t
const wgpu::Texture stagingTexture{ mDevice.CreateTexture(&stagingTextureDescriptor) };
FILAMENT_CHECK_POSTCONDITION(stagingTexture)
<< "Failed to create staging input texture for blit?";
const auto copyInfo{ wgpu::TexelCopyTextureInfo{
const wgpu::TexelCopyTextureInfo copyInfo{
.texture = stagingTexture,
.mipLevel = 0,
.origin = { .x = 0, .y = 0, .z = 0 },
.aspect = texture->getAspect(),
} };
mQueue.WriteTexture(&copyInfo, dataBuff, dataSize, &layout, &extent);
bool reusedCommandEncoder{ true };
if (!mCommandEncoder) {
reusedCommandEncoder = false;
const wgpu::CommandEncoderDescriptor commandEncoderDescriptor{
.label = "blit_command",
};
mCommandEncoder = mDevice.CreateCommandEncoder(&commandEncoderDescriptor);
FILAMENT_CHECK_POSTCONDITION(mCommandEncoder)
<< "Failed to create command encoder for blit?";
}
};
mDevice.GetQueue().WriteTexture(&copyInfo, dataBuff, dataSize, &layout, &extent);
auto commandEncoder = mQueueManager.getCommandEncoder();
WebGPUBlitter::BlitArgs blitArgs{
.source = {
.texture = stagingTexture,
@@ -1078,27 +1014,7 @@ void WebGPUDriver::update3DImage(Handle<HwTexture> textureHandle, const uint32_t
for (uint32_t layerIndex{ 0 }; layerIndex < depth; ++layerIndex) {
blitArgs.source.layerOrDepth = layerIndex;
blitArgs.destination.layerOrDepth = layerIndex + zoffset;
mBlitter.blit(mQueue, mCommandEncoder, blitArgs);
}
if (!reusedCommandEncoder) {
// TODO consider removing encoder finish + submit once we have test coverage and can
// safely verify doing so will not result in strange errors. We don't think this
// submit is necessary, as the encoder should get finished and the command
// submitted in commit(), finish(), and/or flush() in this frame
// or the next anyway. And, the blit bits will be encoded before other things.
// Thus, not submitting here _seems_ ok. Nonetheless, doing the submit here likely
// represents unnecessary inefficiency and higher bandwidth
// between the CPU and GPU, but we are worried about functional correctness at this
// stage.
const wgpu::CommandBufferDescriptor commandBufferDescriptor{
.label = "blit_command_buffer",
};
const wgpu::CommandBuffer blitCommand{ mCommandEncoder.Finish(
&commandBufferDescriptor) };
FILAMENT_CHECK_POSTCONDITION(blitCommand)
<< "Failed to create command buffer for blit?";
mQueue.Submit(1, &blitCommand);
mCommandEncoder = nullptr;
mBlitter.blit(mDevice.GetQueue(), commandEncoder, blitArgs);
}
} else {
// Direct copy without a blit.
@@ -1108,7 +1024,7 @@ void WebGPUDriver::update3DImage(Handle<HwTexture> textureHandle, const uint32_t
.origin = { .x = xoffset, .y = yoffset, .z = zoffset, },
.aspect = texture->getAspect(),
}};
mQueue.WriteTexture(&copyInfo, dataBuff, dataSize, &layout, &extent);
mDevice.GetQueue().WriteTexture(&copyInfo, dataBuff, dataSize, &layout, &extent);
}
scheduleDestroy(std::move(pixelBufferDescriptor));
}
@@ -1155,7 +1071,7 @@ void WebGPUDriver::generateMipmaps(Handle<HwTexture> textureHandle) {
FILAMENT_CHECK_PRECONDITION(usage & wgpu::TextureUsage::RenderAttachment)
<< "Texture for mipmap generation via render pass blit must have "
"RenderAttachment usage.";
mRenderPassMipmapGenerator.generateMipmaps(mQueue, texture->getTexture());
mRenderPassMipmapGenerator.generateMipmaps(texture->getTexture());
break;
case WebGPUTexture::MipmapGenerationStrategy::NONE:
FWGPU_LOGW << "Trying to generate mipmaps for a texture we already know we cannot do "
@@ -1173,11 +1089,8 @@ void WebGPUDriver::generateMipmaps(Handle<HwTexture> textureHandle) {
<< webGPUTextureFormatToString(wgpuTexture.GetFormat()) << " is "
<< WebGPUTexture::supportsMultipleMipLevelsViaStorageBinding(
wgpuTexture.GetFormat());
// We will record all passes into a single command encoder.
wgpu::CommandEncoderDescriptor encoderDesc = {};
encoderDesc.label = "Mipmap Command Encoder";
wgpu::CommandEncoder encoder = mDevice.CreateCommandEncoder(&encoderDesc);
wgpu::CommandEncoder encoder = mQueueManager.getCommandEncoder();
spd::SPDPassConfig spdConfig = { .filter = spd::SPDFilter::Average,
.targetTexture = wgpuTexture,
.numMips = totalMipLevels,
@@ -1185,23 +1098,6 @@ void WebGPUDriver::generateMipmaps(Handle<HwTexture> textureHandle) {
.sourceMipLevel = 0 };
mSpdComputePassMipmapGenerator.Generate(encoder, wgpuTexture, spdConfig);
// Finish the encoder and submit all the passes at once.
// TODO consider reusing the existing command encoder or creating it in this function
// if it doesn't already exist and removing encoder finish + submit here once
// we have test coverage and can safely verify doing so will not result in
// strange errors. We don't think this submit is necessary, as the encoder
// should get finished and the command submitted in
// commit(), finish(), and/or flush() in this frame or the next anyway.
// And, the mipmap generation bits will be encoded before other things.
// Thus, not submitting here _seems_ ok. Nonetheless, doing the submit here likely
// represents unnecessary inefficiency and higher bandwidth
// between the CPU and GPU, but we are worried about functional correctness at this
// stage.
wgpu::CommandBufferDescriptor cmdBufferDesc = {};
cmdBufferDesc.label = "Mipmap Command Buffer";
wgpu::CommandBuffer commandBuffer = encoder.Finish(&cmdBufferDesc);
mQueue.Submit(1, &commandBuffer);
break;
}
}
@@ -1219,12 +1115,8 @@ void WebGPUDriver::compilePrograms(CompilerPriorityQueue priority,
// resolve targets if necessary.
void WebGPUDriver::beginRenderPass(Handle<HwRenderTarget> renderTargetHandle,
RenderPassParams const& params) {
FWGPU_SYSTRACE_SCOPE();
if (!mCommandEncoder) {
wgpu::CommandEncoderDescriptor commandEncoderDescriptor{ .label = "frame_command_encoder" };
mCommandEncoder = mDevice.CreateCommandEncoder(&commandEncoderDescriptor);
}
assert_invariant(mCommandEncoder);
auto commandEncoder = mQueueManager.getCommandEncoder();
assert_invariant(commandEncoder);
auto renderTarget = handleCast<WebGPURenderTarget>(renderTargetHandle);
wgpu::TextureView defaultColorView = nullptr;
@@ -1357,7 +1249,7 @@ void WebGPUDriver::beginRenderPass(Handle<HwRenderTarget> renderTargetHandle,
customDepthStencilView,
customDepthStencilMsaaSidecarTextureView);
mRenderPassEncoder = mCommandEncoder.BeginRenderPass(&renderPassDescriptor);
mRenderPassEncoder = commandEncoder.BeginRenderPass(&renderPassDescriptor);
// Ensure viewport dimensions are not 0
FILAMENT_CHECK_POSTCONDITION(params.viewport.width > 0) << "viewport width is 0?";
@@ -1396,55 +1288,11 @@ void WebGPUDriver::makeCurrent(Handle<HwSwapChain> drawSwapChain,
} else {
mTextureView = mSwapChain->getNextTextureView(mPlatform.getSurfaceExtent(mNativeWindow));
}
assert_invariant(mTextureView);
if (!mCommandEncoder) {
wgpu::CommandEncoderDescriptor commandEncoderDescriptor = {
.label = "frame_command_encoder"
};
mCommandEncoder = mDevice.CreateCommandEncoder(&commandEncoderDescriptor);
}
assert_invariant(mCommandEncoder);
}
void WebGPUDriver::commit(Handle<HwSwapChain> swapChainHandle) {
FWGPU_SYSTRACE_SCOPE();
if (UTILS_VERY_UNLIKELY(!mCommandEncoder)) {
// nothing to submit
if (UTILS_LIKELY(mSwapChain)) {
mSwapChain->present(*this);
}
return;
}
wgpu::CommandBufferDescriptor commandBufferDescriptor{
.label = "frame_command",
};
mCommandBuffer = mCommandEncoder.Finish(&commandBufferDescriptor);
assert_invariant(mCommandBuffer);
mCommandEncoder = nullptr;
mQueue.Submit(1, &mCommandBuffer);
mCommandBuffer = nullptr;
static bool firstRender = true;
// For the first frame rendered, we need to make sure the work is done before presenting or we
// get a purple flash
if (UTILS_UNLIKELY(firstRender)) {
auto f = mQueue.OnSubmittedWorkDone(wgpu::CallbackMode::WaitAnyOnly,
[](wgpu::QueueWorkDoneStatus status, wgpu::StringView message) {
// This lambda does nothing, but the signature must match.
if (status != wgpu::QueueWorkDoneStatus::Success){
FWGPU_LOGE << "Waiting for first frame work to finish resulted in an error";
}
});
const wgpu::Instance instance = mAdapter.GetInstance();
auto wStatus = instance.WaitAny(f,
std::chrono::duration_cast<std::chrono::nanoseconds>(1s).count());
if (wStatus != wgpu::WaitStatus::Success) {
FWGPU_LOGW << "Waiting for first frame work to finish resulted in an error"
<< static_cast<uint32_t>(wStatus);
}
firstRender = false;
}
void WebGPUDriver::commit(Handle<HwSwapChain> sch) {
mQueueManager.flush();
mTextureView = nullptr;
assert_invariant(mSwapChain);
mSwapChain->present(*this);
@@ -1485,6 +1333,7 @@ void WebGPUDriver::stopCapture(int /* dummy */) {
void WebGPUDriver::readPixels(Handle<HwRenderTarget> sourceRenderTargetHandle, const uint32_t x,
const uint32_t y, const uint32_t width, const uint32_t height,
PixelBufferDescriptor&& pixelBufferDescriptor) {
flush();
const auto srcTarget{ handleCast<WebGPURenderTarget>(sourceRenderTargetHandle) };
assert_invariant(srcTarget);
@@ -1524,14 +1373,12 @@ void WebGPUDriver::readPixels(Handle<HwRenderTarget> sourceRenderTargetHandle, c
return;
}
if (!mCommandEncoder) {
const wgpu::CommandEncoderDescriptor commandEncoderDescriptor{
.label = "read_pixels_command",
};
mCommandEncoder = mDevice.CreateCommandEncoder(&commandEncoderDescriptor);
FILAMENT_CHECK_POSTCONDITION(mCommandEncoder)
<< "Failed to create command encoder for readPixels?";
}
const wgpu::CommandEncoderDescriptor commandEncoderDescriptor{
.label = "read_pixels_command",
};
auto commandEncoder = mDevice.CreateCommandEncoder(&commandEncoderDescriptor);
FILAMENT_CHECK_POSTCONDITION(commandEncoder)
<< "Failed to create command encoder for readPixels?";
const wgpu::TextureFormat srcFormat{srcTexture.GetFormat()};
const wgpu::TextureFormat dstFormat{toWebGPUFormat(pixelBufferDescriptor.format,
@@ -1579,7 +1426,7 @@ void WebGPUDriver::readPixels(Handle<HwRenderTarget> sourceRenderTargetHandle, c
},
.filter = SamplerMagFilter::NEAREST, // Use NEAREST for a 1:1 copy
};
mBlitter.blit(mQueue, mCommandEncoder, blitArgs);
mBlitter.blit(mDevice.GetQueue(), commandEncoder, blitArgs);
// The subsequent read will be from the top-left of the new intermediate texture.
readX = 0;
@@ -1625,12 +1472,10 @@ void WebGPUDriver::readPixels(Handle<HwRenderTarget> sourceRenderTargetHandle, c
.height = actualHeight,
.depthOrArrayLayers = 1,
};
mCommandEncoder.CopyTextureToBuffer(&source, &destination, &copySize);
mCommandBuffer = mCommandEncoder.Finish();
assert_invariant(mCommandBuffer);
mCommandEncoder = nullptr;
mQueue.Submit(1, &mCommandBuffer);
mCommandBuffer = nullptr;
commandEncoder.CopyTextureToBuffer(&source, &destination, &copySize);
wgpu::CommandBuffer commandBuffer = commandEncoder.Finish();
assert_invariant(commandBuffer);
mDevice.GetQueue().Submit(1, &commandBuffer);
// Map the buffer to read the data
struct UserData final {
@@ -1652,7 +1497,7 @@ void WebGPUDriver::readPixels(Handle<HwRenderTarget> sourceRenderTargetHandle, c
mReadPixelMapsCounter.startTask();
userData->buffer.MapAsync(
wgpu::MapMode::Read, 0, bufferSize, wgpu::CallbackMode::AllowSpontaneous,
wgpu::MapMode::Read, 0, bufferSize, wgpu::CallbackMode::AllowProcessEvents,
[](wgpu::MapAsyncStatus status, const char* message, UserData* userdata) {
std::unique_ptr<UserData> data(static_cast<UserData*>(userdata));
if (UTILS_LIKELY(status == wgpu::MapAsyncStatus::Success)) {
@@ -1736,15 +1581,8 @@ void WebGPUDriver::blitDEPRECATED(TargetBufferFlags buffers,
const wgpu::Extent2D destinationSize{ static_cast<uint32_t>(destinationViewport.width),
static_cast<uint32_t>(destinationViewport.height) };
bool reusedCommandEncoder{ true };
if (mCommandEncoder) {
flush();
} else {
reusedCommandEncoder = false;
const wgpu::CommandEncoderDescriptor desc{ .label = "blit_deprecated_command" };
mCommandEncoder = mDevice.CreateCommandEncoder(&desc);
}
mQueueManager.flush();
wgpu::CommandEncoder commandEncoder = mQueueManager.getCommandEncoder();
const WebGPUBlitter::BlitArgs blitArgs{
.source = { .texture = sourceTexture->getTexture(),
.aspect = sourceTexture->getAspect(),
@@ -1762,26 +1600,16 @@ void WebGPUDriver::blitDEPRECATED(TargetBufferFlags buffers,
},
.filter = filter,
};
mBlitter.blit(mQueue, mCommandEncoder, blitArgs);
mBlitter.blit(mDevice.GetQueue(), commandEncoder, blitArgs);
if (!reusedCommandEncoder) {
const wgpu::CommandBufferDescriptor desc{ .label = "blit_deprecated_command_buffer" };
const wgpu::CommandBuffer blitCommand{ mCommandEncoder.Finish(&desc) };
mQueue.Submit(1, &blitCommand);
mCommandEncoder = nullptr;
}
mQueueManager.flush();
}
void WebGPUDriver::resolve(Handle<HwTexture> destinationTextureHandle, const uint8_t sourceLevel,
const uint8_t sourceLayer, Handle<HwTexture> sourceTextureHandle,
const uint8_t destinationLevel, const uint8_t destinationLayer) {
FWGPU_SYSTRACE_SCOPE();
if (!mCommandEncoder) {
const wgpu::CommandEncoderDescriptor encoderDescriptor{
.label = "command_created_with_resolve",
};
mCommandEncoder = mDevice.CreateCommandEncoder(&encoderDescriptor);
}
auto commandEncoder = mQueueManager.getCommandEncoder();
FILAMENT_CHECK_PRECONDITION(mRenderPassEncoder == nullptr)
<< "Resolve cannot be called during an existing render pass";
const auto sourceTexture{ handleCast<WebGPUTexture>(sourceTextureHandle) };
@@ -1789,7 +1617,7 @@ void WebGPUDriver::resolve(Handle<HwTexture> destinationTextureHandle, const uin
assert_invariant(sourceTexture);
assert_invariant(destinationTexture);
const WebGPUMsaaTextureResolver::ResolveRequest request{
.commandEncoder = mCommandEncoder,
.commandEncoder = commandEncoder,
.viewFormat = sourceTexture->getViewFormat(),
.source = {
.texture = sourceTexture->getTexture(),
@@ -1814,21 +1642,8 @@ void WebGPUDriver::blit(Handle<HwTexture> destinationTextureHandle, const uint8_
Handle<HwTexture> sourceTextureHandle, const uint8_t destinationLevel,
const uint8_t destinationLayer, const math::uint2 sourceOrigin, const math::uint2 size) {
FWGPU_SYSTRACE_SCOPE();
bool reusedCommandEncoder{ true };
if (mCommandEncoder) {
// make sure command elements (draws, etc.) prior to this blit are processed before the blit
// on the GPU
flush();
} else {
// a command encoder is not in flight, so create one for this blit itself
reusedCommandEncoder = false;
const wgpu::CommandEncoderDescriptor commandEncoderDescriptor{
.label = "blit_command",
};
mCommandEncoder = mDevice.CreateCommandEncoder(&commandEncoderDescriptor);
FILAMENT_CHECK_POSTCONDITION(mCommandEncoder)
<< "Failed to create command encoder for blit?";
}
mQueueManager.flush();
wgpu::CommandEncoder commandEncoder = mQueueManager.getCommandEncoder();
const auto sourceTexture{ handleCast<WebGPUTexture>(sourceTextureHandle) };
const auto destinationTexture{ handleCast<WebGPUTexture>(destinationTextureHandle) };
const WebGPUBlitter::BlitArgs blitArgs{
@@ -1850,25 +1665,8 @@ void WebGPUDriver::blit(Handle<HwTexture> destinationTextureHandle, const uint8_
},
.filter = SamplerMagFilter::NEAREST,
};
mBlitter.blit(mQueue, mCommandEncoder, blitArgs);
if (!reusedCommandEncoder) {
// TODO consider removing encoder finish + submit once we have test coverage and can
// safely verify doing so will not result in strange errors. We don't think this
// submit is necessary, as the encoder should get finished and the command submitted in
// commit(), finish(), and/or flush() in this frame or the next anyway.
// And, the blit bits will be encoded before other things.
// Thus, not submitting here _seems_ ok. Nonetheless, doing the submit here likely
// represents unnecessary inefficiency and higher bandwidth
// between the CPU and GPU, but we are worried about functional correctness at this
// stage.
const wgpu::CommandBufferDescriptor commandBufferDescriptor{
.label = "blit_command_buffer",
};
const wgpu::CommandBuffer blitCommand{ mCommandEncoder.Finish(&commandBufferDescriptor) };
FILAMENT_CHECK_POSTCONDITION(blitCommand) << "Failed to create command buffer for blit?";
mQueue.Submit(1, &blitCommand);
mCommandEncoder = nullptr;
}
mBlitter.blit(mDevice.GetQueue(), commandEncoder, blitArgs);
mQueueManager.flush();
}
void WebGPUDriver::bindPipeline(PipelineState const& pipelineState) {
@@ -2237,5 +2035,4 @@ void WebGPUDriver::copyToMemoryMappedBuffer(MemoryMappedBufferHandle mmbh, size_
// TODO: MetalDriver::copyToMemoryMappedBuffer
}
} // namespace filament::backend
+3 -3
View File
@@ -24,6 +24,7 @@
#include "webgpu/WebGPUPipelineCache.h"
#include "webgpu/WebGPUPipelineLayoutCache.h"
#include "webgpu/WebGPURenderPassMipmapGenerator.h"
#include "webgpu/WebGPUQueueManager.h"
#include "webgpu/utils/AsyncTaskCounter.h"
#include <backend/platforms/WebGPUPlatform.h>
@@ -49,6 +50,7 @@
namespace filament::backend {
class WebGPUSwapChain;
class WebGPUQueueManager;
/**
* Implements the private backend driver API for WebGPU specifically (that API is essentially
@@ -78,14 +80,12 @@ private:
wgpu::Adapter mAdapter = nullptr;
wgpu::Device mDevice = nullptr;
wgpu::Limits mDeviceLimits = {};
wgpu::Queue mQueue = nullptr;
WebGPUQueueManager mQueueManager;
void* mNativeWindow = nullptr;
WebGPUSwapChain* mSwapChain = nullptr;
uint64_t mNextFakeHandle = 1;
wgpu::CommandEncoder mCommandEncoder = nullptr;
wgpu::TextureView mTextureView = nullptr;
wgpu::RenderPassEncoder mRenderPassEncoder = nullptr;
wgpu::CommandBuffer mCommandBuffer = nullptr;
WebGPURenderTarget* mDefaultRenderTarget = nullptr;
WebGPURenderTarget* mCurrentRenderTarget = nullptr;
WebGPUPipelineLayoutCache mPipelineLayoutCache;
+29 -54
View File
@@ -15,76 +15,51 @@
*/
#include "WebGPUFence.h"
#include "WebGPUConstants.h"
#include <backend/DriverEnums.h>
#include <memory>
#include <webgpu/webgpu_cpp.h>
#include <condition_variable>
#include <chrono>
#include <cstdint>
#include <mutex>
namespace filament::backend {
WebGPUFence::~WebGPUFence() noexcept {
// if the fence was being waited on, make sure to return with an error
std::lock_guard const lock(state->lock);
state->status = FenceStatus::ERROR;
state->cond.notify_all();
WebGPUFence::WebGPUFence() = default;
WebGPUFence::~WebGPUFence() = default;
void WebGPUFence::setSubmissionState(std::shared_ptr<WebGPUSubmissionState> state) {
std::lock_guard<std::mutex> const lock(mLock);
mState = state;
mCond.notify_one();
}
FenceStatus WebGPUFence::getStatus() const {
std::lock_guard const lock(state->lock);
return state->status;
FenceStatus WebGPUFence::getStatus() {
return wait(0);
}
FenceStatus WebGPUFence::wait(uint64_t const timeout) {
FenceStatus WebGPUFence::wait(uint64_t timeout) {
// we have to take into account that the STL's wait_for() actually works with
// time_points relative to steady_clock::now() internally.
using namespace std::chrono;
auto const now = steady_clock::now();
auto now = steady_clock::now();
steady_clock::time_point until = steady_clock::time_point::max();
if (now <= steady_clock::time_point::max() - nanoseconds(timeout)) {
until = now + nanoseconds(timeout);
}
std::unique_lock lock(state->lock);
state->cond.wait_until(lock, until,
[state = state] { return state->status != FenceStatus::TIMEOUT_EXPIRED; });
return state->status;
}
void WebGPUFence::addMarkerToQueueState(wgpu::Queue const& queue) {
// The lambda function is called when the work is done. It updates the fence status based on the
// result of the work.
// Because the callback holds a weak_ptr to the state of the HwFence object, it can be
// destroyed before the fence signals.
std::weak_ptr const weak = state;
queue.OnSubmittedWorkDone(
wgpu::CallbackMode::AllowSpontaneous,
[weak](const wgpu::QueueWorkDoneStatus status, wgpu::StringView message) {
if (auto const state = weak.lock()) {
std::unique_lock const lock(state->lock);
if (state->status != FenceStatus::ERROR) {
switch (status) {
case wgpu::QueueWorkDoneStatus::Success:
state->status = FenceStatus::CONDITION_SATISFIED;
state->cond.notify_all();
break;
case wgpu::QueueWorkDoneStatus::CallbackCancelled:
case wgpu::QueueWorkDoneStatus::Error:
state->status = FenceStatus::ERROR;
state->cond.notify_all();
FWGPU_LOGW << "WebGPUFence: wgpu::QueueWorkDoneStatus::Error. " <<
message;
break;
}
}
}
});
{
std::unique_lock<std::mutex> lock(mLock);
bool const success = mCond.wait_until(lock, until, [this] {
return bool(mState);
});
if (!success) {
return FenceStatus::TIMEOUT_EXPIRED;
}
}
auto duration_ns = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(steady_clock::now() - now)
.count());
// In the unlikely event that duration_ns is too close to timeout, we just assume the leftover
// timeout is 0.
if (timeout == 0 || duration_ns > timeout) {
duration_ns = timeout;
}
return mState->waitForCompletion(timeout - duration_ns);
}
} // namespace filament::backend
+14 -25
View File
@@ -17,43 +17,32 @@
#ifndef TNT_FILAMENT_BACKEND_WEBGPUFENCE_H
#define TNT_FILAMENT_BACKEND_WEBGPUFENCE_H
#include "WebGPUQueueManager.h"
#include "DriverBase.h"
#include <backend/DriverEnums.h>
#include <utils/Mutex.h> // NOLINT(*-include-cleaner)
#include <utils/Condition.h> // NOLINT(*-include-cleaner)
#include <cstdint>
#include <memory>
namespace wgpu {
class Queue;
} // namespace wgpu
namespace filament::backend {
/**
* A WebGPU-specific implementation of the HwFence.
* This class is used to synchronize the CPU and GPU. It allows the CPU to know when the GPU has
* finished a set of commands.
*/
class WebGPUFence final : public HwFence {
class WebGPUFence : public HwFence {
public:
~WebGPUFence() noexcept;
[[nodiscard]] FenceStatus getStatus() const;
[[nodiscard]] FenceStatus wait(uint64_t timeout);
WebGPUFence();
~WebGPUFence();
void addMarkerToQueueState(wgpu::Queue const&);
// Initialize the fence with a submission state from the queue manager.
void setSubmissionState(std::shared_ptr<WebGPUSubmissionState> state);
// Note that getStatus cannot be const since it's implemented with wait();
FenceStatus getStatus();
FenceStatus wait(uint64_t timeout);
private:
struct State {
utils::Mutex lock; // NOLINT(*-include-cleaner)
utils::Condition cond; // NOLINT(*-include-cleaner)
FenceStatus status{ FenceStatus::TIMEOUT_EXPIRED };
};
// we need a shared_ptr because the Fence could be destroyed while we're waiting
std::shared_ptr<State> state{ std::make_shared<State>() };
std::mutex mLock;
std::condition_variable mCond;
std::shared_ptr<WebGPUSubmissionState> mState;
};
} // namespace filament::backend
@@ -0,0 +1,123 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "WebGPUQueueManager.h"
#include <utils/Panic.h>
#include <chrono>
#include <cstdint>
#include <thread>
namespace filament::backend {
FenceStatus WebGPUSubmissionState::waitForCompletion(uint64_t timeout) {
using namespace std::chrono;
auto now = steady_clock::now();
steady_clock::time_point until = steady_clock::time_point::max();
if (now <= steady_clock::time_point::max() - nanoseconds(timeout)) {
until = now + nanoseconds(timeout);
}
std::unique_lock<std::mutex> lock(mLock);
mCond.wait_until(lock, until, [this] {
return mStatus == FenceStatus::CONDITION_SATISFIED || mStatus == FenceStatus::ERROR;
});
// Note that mStatus is default to FenceStatus::TIMEOUT_EXPIRED, so if SetStatus() wasn't
// called, then FenceStatus::TIMEOUT_EXPIRED will be returned, which is aligned with the
// condition if the wait_until above times out.
return mStatus;
}
void WebGPUSubmissionState::setStatus(FenceStatus status) {
std::lock_guard<std::mutex> const lock(mLock);
mStatus = status;
mCond.notify_all();
}
WebGPUQueueManager::WebGPUQueueManager(wgpu::Device const& device)
: mDevice(device),
mQueue(device.GetQueue()),
mLatestSubmissionState(
std::make_shared<WebGPUSubmissionState>(FenceStatus::CONDITION_SATISFIED)) {}
WebGPUQueueManager::~WebGPUQueueManager() = default;
wgpu::CommandEncoder WebGPUQueueManager::getCommandEncoder() {
if (!mCommandEncoder) {
wgpu::CommandEncoderDescriptor commandEncoderDescriptor = {
.label = "Filament Command Encoder",
};
mCommandEncoder = mDevice.CreateCommandEncoder(&commandEncoderDescriptor);
ASSERT_POSTCONDITION(mCommandEncoder, "Failed to create command encoder.");
}
return mCommandEncoder;
}
void WebGPUQueueManager::flush() {
submit();
}
void WebGPUQueueManager::finish() {
submit();
// This is a strange side-effect of the dawn callback system, where we would need to keep
// calling ProcessEvents() to be able to advance an internal sequence ID and reach our callback.
// This is similar to draining a work queue. We currently have no other way to force the "last"
// callback to be called.
while (mLatestSubmissionState->getStatus() == FenceStatus::TIMEOUT_EXPIRED) {
mDevice.GetAdapter().GetInstance().ProcessEvents();
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
std::shared_ptr<WebGPUSubmissionState> WebGPUQueueManager::getLatestSubmissionState() {
return mLatestSubmissionState;
}
void WebGPUQueueManager::submit() {
if (!mCommandEncoder) {
return;
}
mLatestSubmissionState = std::make_shared<WebGPUSubmissionState>();
wgpu::CommandBufferDescriptor commandBufferDescriptor{
.label = "Filament Command Buffer",
};
auto cbuf = mCommandEncoder.Finish(&commandBufferDescriptor);
mQueue.Submit(1, &cbuf);
mQueue.OnSubmittedWorkDone(wgpu::CallbackMode::AllowSpontaneous,
[state = mLatestSubmissionState](wgpu::QueueWorkDoneStatus status,
wgpu::StringView sv) {
switch (status) {
case wgpu::QueueWorkDoneStatus::Success:
state->setStatus(FenceStatus::CONDITION_SATISFIED);
break;
case wgpu::QueueWorkDoneStatus::Error:
case wgpu::QueueWorkDoneStatus::CallbackCancelled:
state->setStatus(FenceStatus::ERROR);
break;
}
});
mCommandEncoder = nullptr;
}
} // namespace filament::backend
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef TNT_FILAMENT_BACKEND_WEBGPUQUEUEMANAGER_H
#define TNT_FILAMENT_BACKEND_WEBGPUQUEUEMANAGER_H
#include <backend/DriverEnums.h>
#include <webgpu/webgpu_cpp.h>
#include <memory>
#include <mutex>
namespace filament::backend {
// This struct represents the state of a single submission to the GPU queue.
// A shared pointer to this object is held by WebGPUFence objects.
struct WebGPUSubmissionState {
WebGPUSubmissionState()
: mStatus(FenceStatus::TIMEOUT_EXPIRED) {}
WebGPUSubmissionState(FenceStatus status)
: mStatus(status) {}
FenceStatus getStatus() {
std::lock_guard<std::mutex> const lock(mLock);
return mStatus;
}
FenceStatus waitForCompletion(uint64_t timeout = std::numeric_limits<uint64_t>::max());
void setStatus(FenceStatus status);
private:
std::mutex mLock;
std::condition_variable mCond;
FenceStatus mStatus;
};
class WebGPUQueueManager {
public:
WebGPUQueueManager(wgpu::Device const& device);
~WebGPUQueueManager();
// Returns the command encoder for the current workload. Creates one if it doesn't exist.
wgpu::CommandEncoder getCommandEncoder();
// Submits the current command buffer and creates a new submission state.
void flush();
// Submits the current command buffer and blocks until the work is done.
void finish();
// Returns a shared pointer to the latest submission state.
std::shared_ptr<WebGPUSubmissionState> getLatestSubmissionState();
private:
void submit();
wgpu::Device mDevice;
wgpu::Queue mQueue;
wgpu::CommandEncoder mCommandEncoder;
std::shared_ptr<WebGPUSubmissionState> mLatestSubmissionState;
};
} // namespace filament::backend
#endif // TNT_FILAMENT_BACKEND_WEBGPUQUEUEMANAGER_H
@@ -15,6 +15,7 @@
*/
#include "WebGPURenderPassMipmapGenerator.h"
#include "WebGPUQueueManager.h"
#include <utils/Panic.h>
@@ -199,12 +200,14 @@ constexpr std::string_view SHADER_SOURCE{ R"(
} // namespace
WebGPURenderPassMipmapGenerator::WebGPURenderPassMipmapGenerator(wgpu::Device const& device)
: mDevice{ device },
mPreviousMipLevelSampler{ createPreviousMipLevelSampler(mDevice) },
mShaderModule{ createShaderModule(mDevice) },
mTextureBindGroupLayout{ createTextureBindGroupLayout(mDevice) },
mPipelineLayout{ createPipelineLayout(mDevice, mTextureBindGroupLayout) } {}
WebGPURenderPassMipmapGenerator::WebGPURenderPassMipmapGenerator(wgpu::Device const& device,
WebGPUQueueManager* queueManager)
: mDevice{ device },
mQueueManager(queueManager),
mPreviousMipLevelSampler{ createPreviousMipLevelSampler(mDevice) },
mShaderModule{ createShaderModule(mDevice) },
mTextureBindGroupLayout{ createTextureBindGroupLayout(mDevice) },
mPipelineLayout{ createPipelineLayout(mDevice, mTextureBindGroupLayout) } {}
WebGPURenderPassMipmapGenerator::FormatCompatibility
WebGPURenderPassMipmapGenerator::getCompatibilityFor(const wgpu::TextureFormat format,
@@ -415,33 +418,20 @@ WebGPURenderPassMipmapGenerator::getScalarSampleTypeFrom(const wgpu::TextureForm
}
}
void WebGPURenderPassMipmapGenerator::generateMipmaps(wgpu::Queue const& queue,
wgpu::Texture const& texture) {
void WebGPURenderPassMipmapGenerator::generateMipmaps(wgpu::Texture const& texture) {
const uint32_t mipLevelCount{ texture.GetMipLevelCount() };
if (mipLevelCount < 2) {
return; // Nothing to do.
}
auto commandEncoder = mQueueManager->getCommandEncoder();
wgpu::RenderPipeline const& pipeline{ getOrCreatePipelineFor(texture.GetFormat()) };
const wgpu::CommandEncoderDescriptor commandEncoderDescriptor{
.label = "mipmap_generation_render_pass_cmd_encoder",
};
const wgpu::CommandEncoder commandEncoder{ mDevice.CreateCommandEncoder(
&commandEncoderDescriptor) };
FILAMENT_CHECK_POSTCONDITION(commandEncoder)
<< "Failed to create command encoder for render pass mipmap generation.";
const uint32_t layerCount{ texture.GetDepthOrArrayLayers() };
for (uint32_t layer = 0; layer < layerCount; layer++) {
for (uint32_t mipLevel = 1; mipLevel < mipLevelCount; mipLevel++) {
generateMipmap(commandEncoder, texture, pipeline, layer, mipLevel);
}
}
const wgpu::CommandBufferDescriptor commandBufferDescriptor{
.label = "mipmap_generation_render_pass_cmd_buffer",
};
const wgpu::CommandBuffer commandBuffer{ commandEncoder.Finish(&commandBufferDescriptor) };
FILAMENT_CHECK_POSTCONDITION(commandBuffer)
<< "Failed to create command buffer for render pass mipmap generation.";
queue.Submit(1, &commandBuffer);
mQueueManager->flush();
}
void WebGPURenderPassMipmapGenerator::generateMipmap(wgpu::CommandEncoder const& commandEncoder,
@@ -27,6 +27,8 @@
namespace filament::backend {
class WebGPUQueueManager;
/**
* A utility class for generating mipmaps for a texture using a series of render passes.
*/
@@ -37,6 +39,8 @@ public:
std::string_view reason;
};
WebGPURenderPassMipmapGenerator(wgpu::Device const&, WebGPUQueueManager* queueManager);
/**
* Checks if a given texture format is compatible with render pass-based mipmap generation.
* @return A FormatCompatibility struct indicating whether the format is compatible
@@ -45,16 +49,7 @@ public:
[[nodiscard]] static FormatCompatibility getCompatibilityFor(wgpu::TextureFormat,
wgpu::TextureDimension, uint32_t sampleCount);
explicit WebGPURenderPassMipmapGenerator(wgpu::Device const&);
/**
* IMPORTANT NOTE: when reusing a command encoder and/or textures make sure to flush/submit
* pending commands (draws, etc.) to the GPU prior to calling this call, because texture updates
* may otherwise (unintentionally) happen after draw commands encoded in the encoder.
* Submitting any commands up to this point ensures the calls happen in the expected
* sequence.
*/
void generateMipmaps(wgpu::Queue const&, wgpu::Texture const&);
void generateMipmaps(wgpu::Texture const&);
private:
enum class ScalarSampleType : uint8_t {
@@ -72,6 +67,7 @@ private:
wgpu::RenderPipeline const&, uint32_t layer, uint32_t mipLevel);
wgpu::Device mDevice;
WebGPUQueueManager* mQueueManager;
const wgpu::Sampler mPreviousMipLevelSampler{ nullptr };
const wgpu::ShaderModule mShaderModule{ nullptr };
const wgpu::BindGroupLayout mTextureBindGroupLayout{ nullptr };
+15
View File
@@ -97,6 +97,8 @@ public:
time_point_ns endFrame; //!< Renderer::endFrame() time since epoch [ns]
time_point_ns backendBeginFrame; //!< Backend thread time of frame start since epoch [ns]
time_point_ns backendEndFrame; //!< Backend thread time of frame end since epoch [ns]
time_point_ns gpuFrameComplete; //!< GPU thread time of frame end since epoch [ns] or 0
time_point_ns vsync; //!< VSYNC time of this frame since epoch [ns]
};
/**
@@ -646,6 +648,19 @@ public:
*/
void resetUserTime();
/**
* Requests the next frameCount frames to be skipped. For Debugging.
* @param frameCount number of frames to skip.
*/
void skipNextFrames(size_t frameCount) const noexcept;
/**
* Remainder count of frame to be skipped
* @return remaining frames to be skipped
*/
size_t getFrameToSkipCount() const noexcept;
protected:
// prevent heap allocation
~Renderer() = default;
+109 -44
View File
@@ -19,13 +19,15 @@
#include <filament/Renderer.h>
#include <backend/DriverEnums.h>
#include <backend/Handle.h>
#include <private/utils/Tracing.h>
#include <utils/FixedCapacityVector.h>
#include <utils/Logger.h>
#include <utils/compiler.h>
#include <utils/debug.h>
#include <utils/FixedCapacityVector.h>
#include <utils/JobSystem.h>
#include <utils/Logger.h>
#include <utils/ostream.h>
#include <algorithm>
@@ -33,6 +35,7 @@
#include <chrono>
#include <memory>
#include <ratio>
#include <utility>
#include <stdint.h>
#include <stddef.h>
@@ -42,24 +45,44 @@ namespace filament {
using namespace utils;
using namespace backend;
FrameInfoManager::FrameInfoManager(DriverApi& driver) noexcept {
for (auto& query : mQueries) {
query.handle = driver.createTimerQuery();
FrameInfoManager::FrameInfoManager(DriverApi& driver) noexcept
: mJobQueue("FrameInfoGpuComplete", JobSystem::Priority::URGENT_DISPLAY),
mHasTimerQueries(driver.isFrameTimeSupported()) {
if (mHasTimerQueries) {
for (auto& query : mQueries) {
query.handle = driver.createTimerQuery();
}
}
}
FrameInfoManager::~FrameInfoManager() noexcept = default;
void FrameInfoManager::terminate(DriverApi& driver) noexcept {
for (auto& query : mQueries) {
driver.destroyTimerQuery(query.handle);
if (mHasTimerQueries) {
for (auto const& query : mQueries) {
driver.destroyTimerQuery(query.handle);
}
}
// wait for all pending callbacks to be called & terminate the thread
mJobQueue.drainAndExit();
// destroy the fences that are still alive
for (size_t i = 0, c = mFrameTimeHistory.size(); i < c; i++) {
auto& info = mFrameTimeHistory[i];
if (info.fence) {
driver.destroyFence(std::move(info.fence));
}
}
}
void FrameInfoManager::beginFrame(DriverApi& driver, Config const& config, uint32_t frameId) noexcept {
void FrameInfoManager::beginFrame(DriverApi& driver, Config const& config,
uint32_t frameId, std::chrono::steady_clock::time_point vsync) noexcept {
auto& history = mFrameTimeHistory;
// don't exceed the capacity, drop the oldest entry
if (UTILS_LIKELY(history.size() == history.capacity())) {
assert_invariant(history.back().fence);
driver.destroyFence(std::move(history.back().fence));
history.pop_back();
}
@@ -67,44 +90,54 @@ void FrameInfoManager::beginFrame(DriverApi& driver, Config const& config, uint3
auto& front = history.emplace_front(frameId);
// store the current time
front.vsync = vsync;
front.beginFrame = std::chrono::steady_clock::now();
// references are not invalidated by CircularQueue<>, so we can associate a reference to
// the slot we created to the timer query used to find the frame time.
mQueries[mIndex].pInfo = std::addressof(front);
// issue the timer query
driver.beginTimerQuery(mQueries[mIndex].handle);
if (mHasTimerQueries) {
// references are not invalidated by CircularQueue<>, so we can associate a reference to
// the slot we created to the timer query used to find the frame time.
mQueries[mIndex].pInfo = std::addressof(front);
// issue the timer query
driver.beginTimerQuery(mQueries[mIndex].handle);
}
// issue the custom backend command to get the backend time
driver.queueCommand([&front](){
front.backendBeginFrame = std::chrono::steady_clock::now();
});
// now is a good time to check the oldest active query
while (mLast != mIndex) {
uint64_t elapsed = 0;
TimerQueryResult const result = driver.getTimerQueryValue(mQueries[mLast].handle, &elapsed);
switch (result) {
case TimerQueryResult::NOT_READY:
// nothing to do
break;
case TimerQueryResult::ERROR:
mLast = (mLast + 1) % POOL_COUNT;
break;
case TimerQueryResult::AVAILABLE: {
FILAMENT_TRACING_CONTEXT(FILAMENT_TRACING_CATEGORY_FILAMENT);
FILAMENT_TRACING_VALUE(FILAMENT_TRACING_CATEGORY_FILAMENT, "FrameInfo::elapsed", uint32_t(elapsed));
// conversion to our duration happens here
pFront = mQueries[mLast].pInfo;
pFront->frameTime = std::chrono::duration<uint64_t, std::nano>(elapsed);
mLast = (mLast + 1) % POOL_COUNT;
denoiseFrameTime(history, config);
if (mHasTimerQueries) {
// now is a good time to check the oldest active query
while (mLast != mIndex) {
uint64_t elapsed = 0;
TimerQueryResult const result = driver.getTimerQueryValue(mQueries[mLast].handle, &elapsed);
switch (result) {
case TimerQueryResult::NOT_READY:
// nothing to do
break;
case TimerQueryResult::ERROR:
mLast = (mLast + 1) % POOL_COUNT;
break;
case TimerQueryResult::AVAILABLE: {
FILAMENT_TRACING_CONTEXT(FILAMENT_TRACING_CATEGORY_FILAMENT);
FILAMENT_TRACING_VALUE(FILAMENT_TRACING_CATEGORY_FILAMENT, "FrameInfo::elapsed", uint32_t(elapsed));
// conversion to our duration happens here
pFront = mQueries[mLast].pInfo;
pFront->frameTime = std::chrono::duration<uint64_t, std::nano>(elapsed);
mLast = (mLast + 1) % POOL_COUNT;
denoiseFrameTime(history, config);
break;
}
}
if (result != TimerQueryResult::AVAILABLE) {
break;
}
// read the pending timer queries until we find one that's not ready
}
if (result != TimerQueryResult::AVAILABLE) {
break;
} else {
if (mLast != mIndex) {
mLast = (mLast + 1) % POOL_COUNT;
}
// read the pending timer queries until we find one that's not ready
}
#if 0
@@ -123,18 +156,48 @@ void FrameInfoManager::beginFrame(DriverApi& driver, Config const& config, uint3
}
void FrameInfoManager::endFrame(DriverApi& driver) noexcept {
// create a Fence to capture the GPU complete time
FenceHandle const fence = driver.createFence();
auto& front = mFrameTimeHistory.front();
// close the timer query
driver.endTimerQuery(mQueries[mIndex].handle);
front.fence = fence;
front.endFrame = std::chrono::steady_clock::now();
if (mHasTimerQueries) {
// close the timer query
driver.endTimerQuery(mQueries[mIndex].handle);
}
// queue custom backend command to query the current time
driver.queueCommand([&front](){
driver.queueCommand([&jobQueue = mJobQueue, &driver, &front] {
// backend frame end-time
front.backendEndFrame = std::chrono::steady_clock::now();
// signal that the data is available
front.ready.store(true, std::memory_order_release);
if (UTILS_UNLIKELY(!jobQueue.isValid())) {
front.gpuFrameComplete = {};
front.ready.store(true, std::memory_order_release);
return;
}
// now launch a job that'll wait for the gpu to complete
jobQueue.push([&driver, &front] {
FenceStatus const status = driver.fenceWait(front.fence, FENCE_WAIT_FOR_EVER);
if (status == FenceStatus::CONDITION_SATISFIED) {
front.gpuFrameComplete = std::chrono::steady_clock::now();
} else if (status == FenceStatus::TIMEOUT_EXPIRED) {
// that should never happen because:
// - we wait forever
// - made sure that the createFence() command was processed on the backed
// (because we're inside a custom command)
} else {
// We got an error, fenceWait might not be supported
front.gpuFrameComplete = {};
}
// finally, signal that the data is available
front.ready.store(true, std::memory_order_release);
});
});
// and finally acquire the time on the main thread
front.endFrame = std::chrono::steady_clock::now();
mIndex = (mIndex + 1) % POOL_COUNT;
}
@@ -180,7 +243,7 @@ FixedCapacityVector<Renderer::FrameInfo> FrameInfoManager::getFrameInfoHistory(
size_t const c = history.size();
for (; i < c; ++i) {
auto const& entry = history[i];
if (entry.ready.load(std::memory_order_acquire) && entry.valid) {
if (entry.ready.load(std::memory_order_acquire) && (entry.valid || !mHasTimerQueries)) {
// once we found an entry ready,
// we know by construction that all following ones are too
break;
@@ -197,7 +260,9 @@ FixedCapacityVector<Renderer::FrameInfo> FrameInfoManager::getFrameInfoHistory(
duration_cast<nanoseconds>(entry.beginFrame.time_since_epoch()).count(),
duration_cast<nanoseconds>(entry.endFrame.time_since_epoch()).count(),
duration_cast<nanoseconds>(entry.backendBeginFrame.time_since_epoch()).count(),
duration_cast<nanoseconds>(entry.backendEndFrame.time_since_epoch()).count()
duration_cast<nanoseconds>(entry.backendEndFrame.time_since_epoch()).count(),
duration_cast<nanoseconds>(entry.gpuFrameComplete.time_since_epoch()).count(),
duration_cast<nanoseconds>(entry.vsync.time_since_epoch()).count()
});
}
return result;
+28 -1
View File
@@ -25,6 +25,7 @@
#include <utils/compiler.h>
#include <utils/debug.h>
#include <utils/AsyncJobQueue.h>
#include <utils/FixedCapacityVector.h>
#include <array>
@@ -56,11 +57,18 @@ struct FrameInfoImpl : public details::FrameInfo {
time_point endFrame; // main thread endFrame time
time_point backendBeginFrame; // backend thread beginFrame time (makeCurrent time)
time_point backendEndFrame; // backend thread endFrame time (present time)
time_point gpuFrameComplete; // the frame is done rendering on the gpu
time_point vsync; // vsync time
backend::FenceHandle fence{}; // the fence used for gpuFrameComplete
std::atomic_bool ready{}; // true once backend thread has populated its data
explicit FrameInfoImpl(uint32_t const id) noexcept
: frameId(id) {
}
~FrameInfoImpl() noexcept {
assert_invariant(!fence);
}
FrameInfoImpl(FrameInfoImpl&& rhs) noexcept :
details::FrameInfo(rhs),
frameId(rhs.frameId),
@@ -68,6 +76,9 @@ struct FrameInfoImpl : public details::FrameInfo {
endFrame(rhs.endFrame),
backendBeginFrame(rhs.backendBeginFrame),
backendEndFrame(rhs.backendEndFrame),
gpuFrameComplete(rhs.gpuFrameComplete),
vsync(rhs.vsync),
fence(rhs.fence),
ready(rhs.ready.load())
{
}
@@ -79,6 +90,9 @@ struct FrameInfoImpl : public details::FrameInfo {
endFrame = rhs.endFrame;
backendBeginFrame = rhs.backendBeginFrame;
backendEndFrame = rhs.backendEndFrame;
gpuFrameComplete = rhs.gpuFrameComplete;
vsync = rhs.vsync;
fence = rhs.fence;
ready.store(rhs.ready.load());
return *this;
}
@@ -185,6 +199,16 @@ public:
return operator[](0);
}
T const& back() const noexcept {
assert_invariant(!empty());
return operator[](size() - 1);
}
T& back() noexcept {
assert_invariant(!empty());
return operator[](size() - 1);
}
private:
using Storage = std::aligned_storage_t<sizeof(T), alignof(T)>;
Storage mStorage[CAPACITY];
@@ -213,7 +237,8 @@ public:
void terminate(backend::DriverApi& driver) noexcept;
// call this immediately after "make current"
void beginFrame(backend::DriverApi& driver, Config const& config, uint32_t frameId) noexcept;
void beginFrame(backend::DriverApi& driver, Config const& config,
uint32_t frameId, std::chrono::steady_clock::time_point vsync) noexcept;
// call this immediately before "swap buffers"
void endFrame(backend::DriverApi& driver) noexcept;
@@ -237,6 +262,8 @@ private:
uint32_t mLast = 0; // index of oldest query still active
FrameInfoImpl* pFront = nullptr; // the most recent slot with a valid frame time
FrameHistoryQueue mFrameTimeHistory{};
utils::AsyncJobQueue mJobQueue;
bool const mHasTimerQueries = false;
};
+19 -1
View File
@@ -22,6 +22,8 @@
#include <utils/debug.h>
#include <algorithm>
#include <cstdint>
#include <limits>
#include <utility>
#include <stddef.h>
@@ -46,10 +48,16 @@ void FrameSkipper::terminate(DriverApi& driver) noexcept {
}
bool FrameSkipper::shouldRenderFrame(DriverApi& driver) const noexcept {
if (UTILS_UNLIKELY(mFrameToSkip)) {
mFrameToSkip--;
return false;
}
auto& fences = mDelayedFences;
if (fences.front()) {
// Do we have a latency old fence?
auto status = driver.getFenceStatus(fences.front());
FenceStatus const status = driver.getFenceStatus(fences.front());
if (UTILS_UNLIKELY(status == FenceStatus::TIMEOUT_EXPIRED)) {
// The fence hasn't signaled yet, skip this frame
return false;
@@ -75,4 +83,14 @@ void FrameSkipper::submitFrame(DriverApi& driver) noexcept {
fences[last] = driver.createFence();
}
void FrameSkipper::skipNextFrames(size_t frameCount) const noexcept {
frameCount = std::min(frameCount, size_t(std::numeric_limits<decltype(mFrameToSkip)>::max()));
mFrameToSkip = uint16_t(frameCount);
}
size_t FrameSkipper::getFrameToSkipCount() const noexcept {
return mFrameToSkip;
}
} // namespace filament
+6
View File
@@ -71,10 +71,16 @@ public:
void submitFrame(backend::DriverApi& driver) noexcept;
// set frameCount frame to report as "skip". For debugging.
void skipNextFrames(size_t frameCount) const noexcept;
// return remaining number of frame to be skipped
size_t getFrameToSkipCount() const noexcept;
private:
using Container = std::array<backend::Handle<backend::HwFence>, MAX_FRAME_LATENCY>;
Container mDelayedFences{};
uint8_t const mLatency;
mutable uint16_t mFrameToSkip{};
};
} // namespace filament
+8
View File
@@ -87,6 +87,14 @@ void Renderer::resetUserTime() {
downcast(this)->resetUserTime();
}
void Renderer::skipNextFrames(size_t frameCount) const noexcept {
downcast(this)->skipNextFrames(frameCount);
}
size_t Renderer::getFrameToSkipCount() const noexcept {
return downcast(this)->getFrameToSkipCount();
}
void Renderer::setDisplayInfo(const DisplayInfo& info) noexcept {
downcast(this)->setDisplayInfo(info);
}
+234
View File
@@ -0,0 +1,234 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "details/BufferAllocator.h"
#include <utils/Panic.h>
#include <utils/debug.h>
namespace filament {
namespace {
static bool isValidId(BufferAllocator::AllocationId id) {
return id != BufferAllocator::UNALLOCATED && id != BufferAllocator::REALLOCATION_REQUIRED;
}
#ifndef NDEBUG
constexpr static bool isPowerOfTwo(uint32_t n) {
return (n > 0) && ((n & (n - 1)) == 0);
}
#endif
} // anonymous namespace
BufferAllocator::BufferAllocator(allocation_size_t totalSize, allocation_size_t slotSize)
: mTotalSize(totalSize),
mSlotSize(slotSize) {
assert_invariant(mSlotSize > 0);
assert_invariant(isPowerOfTwo(mSlotSize));
reset(mTotalSize);
}
void BufferAllocator::reset(allocation_size_t newTotalSize) {
assert_invariant(newTotalSize % mSlotSize == 0);
mTotalSize = newTotalSize;
mSlotPool.clear();
mFreeList.clear();
mOffsetMap.clear();
// Initialize the pool with a single large free slot.
mSlotPool.emplace_back(InternalSlotNode{
.mSlot = {
.offset = 0,
.slotSize = mTotalSize,
.isAllocated = false,
.gpuUseCount = 0
}
});
InternalSlotNode* firstNode = &mSlotPool.front();
// Add this initial free slot to the free list and offset map.
auto freeListIter = mFreeList.emplace(newTotalSize, firstNode);
auto offsetMapIter = mOffsetMap.emplace(0, firstNode);
firstNode->mSlotPoolIterator = mSlotPool.begin();
firstNode->mFreeListIterator = freeListIter;
firstNode->mOffsetMapIterator = offsetMapIter.first;
}
std::pair<BufferAllocator::AllocationId, BufferAllocator::allocation_size_t>
BufferAllocator::allocate(allocation_size_t size) noexcept {
if (size == 0) {
return { UNALLOCATED, 0 };
}
const allocation_size_t alignedSize = alignUp(size);
auto bestFitIter = mFreeList.lower_bound(alignedSize);
if (bestFitIter == mFreeList.end()) {
return { REALLOCATION_REQUIRED, 0 };
}
InternalSlotNode* targetNode = bestFitIter->second;
const allocation_size_t originalSlotSize = targetNode->mSlot.slotSize;
mFreeList.erase(bestFitIter);
targetNode->mFreeListIterator = mFreeList.end();
targetNode->mSlot.isAllocated = true;
// Split the slot if it is larger than what we need.
if (originalSlotSize > alignedSize) {
targetNode->mSlot.slotSize = alignedSize;
allocation_size_t remainingSize = originalSlotSize - alignedSize;
allocation_size_t newSlotOffset = targetNode->mSlot.offset + alignedSize;
assert_invariant(remainingSize % mSlotSize == 0);
assert_invariant(newSlotOffset % mSlotSize == 0);
// Create a new node for the remaining free space.
auto insertPos = std::next(targetNode->mSlotPoolIterator);
auto newNodeIter = mSlotPool.emplace(insertPos, InternalSlotNode{
.mSlot = {
.offset = newSlotOffset,
.slotSize = remainingSize,
.isAllocated = false,
.gpuUseCount = 0
}
});
InternalSlotNode* newNode = &(*newNodeIter);
// Add the new free slot to our tracking maps.
auto freeListIter = mFreeList.emplace(remainingSize, newNode);
auto offsetMapIter = mOffsetMap.emplace(newSlotOffset, newNode);
newNode->mSlotPoolIterator = newNodeIter;
newNode->mFreeListIterator = freeListIter;
newNode->mOffsetMapIterator = offsetMapIter.first;
}
auto allocationId = calculateIdByOffset(targetNode->mSlot.offset);
return { allocationId, targetNode->mSlot.offset };
}
BufferAllocator::InternalSlotNode* BufferAllocator::getNodeById(
AllocationId id) const noexcept {
if (!isValidId(id)) {
return nullptr;
}
auto offset = getAllocationOffset(id);
auto iter = mOffsetMap.find(offset);
// We cannot find the corresponding node in the map.
if (iter == mOffsetMap.end()) {
return nullptr;
}
return iter->second;
}
void BufferAllocator::retire(AllocationId id) {
auto targetNode = getNodeById(id);
assert_invariant(targetNode != nullptr);
targetNode->mSlot.isAllocated = false;
}
void BufferAllocator::acquireGpu(AllocationId id) {
auto targetNode = getNodeById(id);
assert_invariant(targetNode != nullptr);
targetNode->mSlot.gpuUseCount++;
}
void BufferAllocator::releaseGpu(AllocationId id) {
auto targetNode = getNodeById(id);
assert_invariant(targetNode != nullptr);
assert_invariant(targetNode->mSlot.gpuUseCount > 0);
targetNode->mSlot.gpuUseCount--;
}
void BufferAllocator::releaseFreeSlots() {
auto curr = mSlotPool.begin();
while (curr != mSlotPool.end()) {
if (!curr->mSlot.isFree()) {
++curr;
continue;
}
auto next = std::next(curr);
bool merged = false;
while (next != mSlotPool.end() && next->mSlot.isFree()) {
merged = true;
// Combine the size of free slots
curr->mSlot.slotSize += next->mSlot.slotSize;
assert_invariant(curr->mSlot.slotSize % mSlotSize == 0);
// Erase the merged slot from all maps
if (next->mFreeListIterator != mFreeList.end()) {
mFreeList.erase(next->mFreeListIterator);
}
mOffsetMap.erase(next->mOffsetMapIterator);
next = mSlotPool.erase(next);
}
// If we performed any merge, the current block's size has changed.
// We need to update its position in the mFreeList.
if (curr->mFreeListIterator != mFreeList.end()) {
// If it's already in the free list and we merged, we need to update it.
if (merged) {
mFreeList.erase(curr->mFreeListIterator);
curr->mFreeListIterator = mFreeList.emplace(curr->mSlot.slotSize, &(*curr));
}
} else {
// If it's not in the free list, it must be a newly freed block. Add it.
curr->mFreeListIterator = mFreeList.emplace(curr->mSlot.slotSize, &(*curr));
}
curr = next;
}
}
BufferAllocator::allocation_size_t BufferAllocator::getTotalSize() const noexcept {
return mTotalSize;
}
BufferAllocator::allocation_size_t
BufferAllocator::getAllocationOffset(AllocationId id) const {
assert_invariant(isValidId(id));
return (id - 1) * mSlotSize;
}
BufferAllocator::AllocationId BufferAllocator::calculateIdByOffset(
allocation_size_t offset) const {
assert_invariant(offset % mSlotSize == 0);
// The ID is 1-based since we use 0 for UNALLOCATED.
return (offset / mSlotSize) + 1;
}
BufferAllocator::allocation_size_t BufferAllocator::alignUp(
allocation_size_t size) const noexcept {
if (size == 0) return 0;
return (size + mSlotSize - 1) & ~(mSlotSize - 1);
}
} // namespace filament
+118
View File
@@ -0,0 +1,118 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef TNT_FILAMENT_DETAILS_BUFFERALLOCATOR_H
#define TNT_FILAMENT_DETAILS_BUFFERALLOCATOR_H
#include <cstdint>
#include <list>
#include <map>
#include <unordered_map>
namespace filament {
// This class is NOT thread-safe.
//
// It internally manages shared state (e.g., mSlotPool, mFreeList, mOffsetMap) without any
// synchronization primitives. Concurrent access from multiple threads to the same
// BufferAllocator instance will result in data races and undefined behavior.
//
// If an instance of this class is to be shared between threads, all calls to its member
// functions MUST be protected by external synchronization (e.g., a std::mutex).
class BufferAllocator {
public:
using allocation_size_t = uint32_t;
using AllocationId = uint32_t;
static constexpr AllocationId UNALLOCATED = 0;
static constexpr AllocationId REALLOCATION_REQUIRED = ~0u;
struct Slot {
const allocation_size_t offset; // 4 bytes
allocation_size_t slotSize; // 4 bytes
bool isAllocated; // 1 byte
char padding[3]; // 3 bytes
uint32_t gpuUseCount; // 4 bytes
[[nodiscard]] bool isFree() const noexcept {
return !isAllocated && gpuUseCount == 0;
}
};
// `slotSize` is derived from the GPU's uniform buffer offset alignment requirement,
// which can be up to 256 bytes.
explicit BufferAllocator(allocation_size_t totalSize,
allocation_size_t slotSize);
BufferAllocator(BufferAllocator const&) = delete;
BufferAllocator(BufferAllocator&&) = delete;
// Allocate a new slot and return its id and slot offset in the UBO.
// If the returned id is not valid, that means there's no large enough slot for allocation.
[[nodiscard]] std::pair<AllocationId, allocation_size_t> allocate(
allocation_size_t size) noexcept;
// Call it when MaterialInstance gives up the ownership of the allocation.
// We don't release the slot immediately in this function even if it is not being used,
// the release is centralized in releaseFreeSlots().
void retire(AllocationId id);
// Increments the GPU read-lock.
void acquireGpu(AllocationId id);
// Decrements the GPU read-lock.
// We don't release the slot immediately in this function even if it is not being used,
// the release is centralized in releaseFreeSlots().
void releaseGpu(AllocationId id);
// Traverse all slots and free all slots that are not being used by both CPU and GPU.
// Perform the merge at the same time.
void releaseFreeSlots();
// Resets the allocator to its initial state with a new total size.
// All existing allocations are cleared.
void reset(allocation_size_t newTotalSize);
// Size of the UBO in bytes.
[[nodiscard]] allocation_size_t getTotalSize() const noexcept;
// Query the allocation offset by AllocationId.
allocation_size_t getAllocationOffset(AllocationId id) const;
private:
[[nodiscard]] AllocationId calculateIdByOffset(allocation_size_t offset) const;
[[nodiscard]] allocation_size_t alignUp(allocation_size_t size) const noexcept;
// Having an internal node type holding the base slot node and additional information.
struct InternalSlotNode {
Slot mSlot;
std::list<InternalSlotNode>::iterator mSlotPoolIterator;
std::multimap<allocation_size_t, InternalSlotNode*>::iterator mFreeListIterator;
std::unordered_map<allocation_size_t, InternalSlotNode*>::iterator mOffsetMapIterator;
};
[[nodiscard]] InternalSlotNode* getNodeById(AllocationId id) const noexcept;
allocation_size_t mTotalSize;
const allocation_size_t mSlotSize; // Size of a single slot in bytes.
std::list<InternalSlotNode> mSlotPool; // All slots, including both allocated and freed
std::multimap</*slot size*/allocation_size_t, InternalSlotNode*> mFreeList;
std::unordered_map</*slot offset*/allocation_size_t, InternalSlotNode*> mOffsetMap;
};
} // namespace filament
#endif // TNT_FILAMENT_DETAILS_BUFFERALLOCATOR_H
+5 -3
View File
@@ -725,9 +725,11 @@ bool FMaterial::setConstant(uint32_t id, T value) noexcept {
}
}
mSpecializationConstants[id] = value;
return true;
if (std::get<T>(mSpecializationConstants[id]) != value) {
mSpecializationConstants[id] = value;
return true;
}
return false;
}
FixedCapacityVector<Program::SpecializationConstant>
+1 -1
View File
@@ -391,7 +391,7 @@ bool FRenderer::beginFrame(FSwapChain* swapChain, uint64_t vsyncSteadyClockTimeN
mFrameInfoManager.beginFrame(driver, {
.historySize = mFrameRateOptions.history
}, mFrameId);
}, mFrameId, appVsync);
// ask the engine to do what it needs to (e.g. updates light buffer, materials...)
engine.prepare();
+8 -1
View File
@@ -83,10 +83,17 @@ public:
void resetUserTime();
void skipNextFrames(size_t frameCount) const noexcept {
mFrameSkipper.skipNextFrames(frameCount);
}
size_t getFrameToSkipCount() const noexcept {
return mFrameSkipper.getFrameToSkipCount();
}
// renders a single standalone view. The view must have a a custom rendertarget.
void renderStandaloneView(FView const* view);
void setPresentationTime(int64_t monotonic_clock_ns) const;
void setVsyncTime(uint64_t steadyClockTimeNano) noexcept;
+3 -3
View File
@@ -82,8 +82,8 @@ class JobSystem;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#elif defined(_MSC_VER)
#pragma warning push
#pragma warning disable : 4996
#pragma warning(push)
#pragma warning(disable : 4996)
#endif
namespace filament {
@@ -663,7 +663,7 @@ FILAMENT_DOWNCAST(View)
#if defined(__clang__)
#pragma clang diagnostic pop
#elif defined(_MSC_VER)
#pragma warning pop
#pragma warning(pop)
#endif
#endif // TNT_FILAMENT_DETAILS_VIEW_H
+2
View File
@@ -43,6 +43,8 @@ list(APPEND RESGEN_SOURCE ${DUMMY_SRC})
if (TNT_DEV)
add_executable(test_${TARGET}
filament_AtlasAllocator_test.cpp
test_BufferAllocator.cpp
test_BufferAllocatorStress.cpp
test_CircularQueue.cpp
filament_test_exposure.cpp
filament_rendering_test.cpp
+415
View File
@@ -0,0 +1,415 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <gtest/gtest.h>
#include "../src/details/BufferAllocator.h"
#include "utils/Panic.h"
#include <utility>
#include <vector>
using namespace filament;
namespace {
class BufferAllocatorTest : public ::testing::Test {
protected:
// We use a total size of 1024 and a slot size (alignment) of 64.
// This gives us 1024 / 64 = 16 total possible slots if aligned.
static constexpr BufferAllocator::allocation_size_t TOTAL_SIZE = 1024;
static constexpr BufferAllocator::allocation_size_t SLOT_SIZE = 64;
BufferAllocatorTest() : mAllocator(TOTAL_SIZE, SLOT_SIZE) {}
BufferAllocator mAllocator;
};
TEST_F(BufferAllocatorTest, ConstructorFailure) {
// The constructor requires slotSize to be a power of two.
constexpr BufferAllocator::allocation_size_t NON_POT_SLOT_SIZE = 60;
EXPECT_DEATH(BufferAllocator(TOTAL_SIZE, NON_POT_SLOT_SIZE), "failed assertion");
}
TEST_F(BufferAllocatorTest, InitialState) {
EXPECT_EQ(mAllocator.getTotalSize(), TOTAL_SIZE);
// Initially, there should be one large free block of the total size.
// Let's try to allocate the whole thing.
auto [id, offset] = mAllocator.allocate(TOTAL_SIZE);
EXPECT_EQ(id, 1); // The first ID should be 1.
EXPECT_EQ(offset, 0);
}
TEST_F(BufferAllocatorTest, SimpleAllocation) {
// Allocate 100 bytes, which should be aligned up to 128 (2 * 64).
auto [id, offset] = mAllocator.allocate(100);
EXPECT_EQ(id, 1);
EXPECT_EQ(offset, 0);
EXPECT_EQ(mAllocator.getAllocationOffset(id), offset);
// Try to allocate again. The next allocation should start after the first one.
auto [id2, offset2] = mAllocator.allocate(50); // Aligns to 64
EXPECT_EQ(id2, 3); // ID is (128 / 64) + 1 = 3
EXPECT_EQ(offset2, 128);
EXPECT_EQ(mAllocator.getAllocationOffset(id2), offset2);
}
TEST_F(BufferAllocatorTest, AllocateZeroSize) {
// Allocating zero bytes should return an unallocated ID and not affect the state.
auto [id, offset] = mAllocator.allocate(0);
EXPECT_EQ(id, BufferAllocator::UNALLOCATED);
EXPECT_EQ(offset, 0);
// The allocator should still be in its initial state, able to allocate the full size.
auto [fullId, fullOffset] = mAllocator.allocate(TOTAL_SIZE);
EXPECT_EQ(fullId, 1);
EXPECT_EQ(fullOffset, 0);
}
TEST_F(BufferAllocatorTest, AllocateAll) {
auto [id1, offset1] = mAllocator.allocate(512);
EXPECT_EQ(id1, 1);
EXPECT_EQ(offset1, 0);
auto [id2, offset2] = mAllocator.allocate(512);
EXPECT_EQ(id2, 9); // ID is (512 / 64) + 1 = 9
EXPECT_EQ(offset2, 512);
// The buffer is now full. The next allocation should fail.
auto [id3, offset3] = mAllocator.allocate(1);
EXPECT_EQ(id3, BufferAllocator::REALLOCATION_REQUIRED);
}
TEST_F(BufferAllocatorTest, AllocationFailure) {
auto [id1, offset1] = mAllocator.allocate(TOTAL_SIZE - 1); // Allocate most of the buffer.
EXPECT_EQ(id1, 1);
EXPECT_EQ(offset1, 0);
// Try to allocate more than the remaining space.
auto [id2, offset2] = mAllocator.allocate(100);
EXPECT_EQ(id2, BufferAllocator::REALLOCATION_REQUIRED);
EXPECT_EQ(offset2, 0);
}
TEST_F(BufferAllocatorTest, AllocationLifecycle) {
// 1. Allocate
auto [id, offset] = mAllocator.allocate(128);
EXPECT_EQ(id, 1);
// 2. Retire from CPU side
mAllocator.retire(id);
// 3. The slot is not free yet because the GPU might still be using it.
// Let's simulate the GPU acquiring and releasing it.
mAllocator.acquireGpu(id);
mAllocator.releaseGpu(id);
// Now the slot should be considered free, but it's not merged yet.
// Let's try to allocate something else to see where it goes.
auto [id2, offset2] = mAllocator.allocate(200); // Aligns to 256
EXPECT_EQ(id2, 3); // It should use the space after the first retired slot.
EXPECT_EQ(offset2, 128);
// Now, let's release the free slots.
mAllocator.releaseFreeSlots();
// The first slot (128 bytes) should now be available again.
// Let's try to allocate something that fits in it.
auto [id3, offset3] = mAllocator.allocate(100); // Aligns to 128
EXPECT_EQ(id3, 1); // It should reuse the first slot.
EXPECT_EQ(offset3, 0);
}
TEST_F(BufferAllocatorTest, RetireThenReleaseGpu) {
// 1. Allocate a block and a dummy block next to it.
auto [id1, offset1] = mAllocator.allocate(128);
auto [id2, offset2] = mAllocator.allocate(128);
EXPECT_EQ(id1, 1);
EXPECT_EQ(id2, 3);
EXPECT_EQ(offset1, 0);
EXPECT_EQ(offset2, 128);
// 2. Retire the first block (CPU is done), then acquire it for the GPU.
mAllocator.retire(id1);
mAllocator.acquireGpu(id1); // gpuUseCount = 1
// 3. The slot is now free from CPU but locked by GPU. releaseFreeSlots should do nothing to it.
mAllocator.releaseFreeSlots();
// 4. Try to allocate the same space. It should fail, and the allocation should go
// to the next available free space.
auto [id3, offset3] = mAllocator.allocate(64);
EXPECT_EQ(id3, 5);
EXPECT_EQ(offset3, 256); // It should be allocated after id2.
// 5. Now, release the GPU lock.
mAllocator.releaseGpu(id1); // gpuUseCount = 0
// 6. Call releaseFreeSlots again. This time it should be freed.
mAllocator.releaseFreeSlots();
// 7. The original slot should now be available for allocation.
auto [id4, offset4] = mAllocator.allocate(64);
EXPECT_EQ(id4, 1);
EXPECT_EQ(offset4, 0); // Success! It reuses the first slot.
}
TEST_F(BufferAllocatorTest, MultipleGpuAcquires) {
// 1. Allocate a block.
auto [id, offset] = mAllocator.allocate(128);
EXPECT_EQ(id, 1);
EXPECT_EQ(offset, 0);
// 2. Retire from CPU, then acquire multiple times for GPU (e.g., used in 3 command buffers).
mAllocator.retire(id);
mAllocator.acquireGpu(id); // gpuUseCount = 1
mAllocator.acquireGpu(id); // gpuUseCount = 2
mAllocator.acquireGpu(id); // gpuUseCount = 3
// 3. Release GPU lock once. The slot should still be locked.
mAllocator.releaseGpu(id); // gpuUseCount = 2
mAllocator.releaseFreeSlots();
auto [failId1, failOffset1] = mAllocator.allocate(64);
EXPECT_NE(failOffset1, 0); // Should not be able to allocate at offset 0.
EXPECT_NE(failId1, 1);
// 4. Release GPU lock again. The slot should still be locked.
mAllocator.releaseGpu(id); // gpuUseCount = 1
mAllocator.releaseFreeSlots();
auto [failId2, failOffset2] = mAllocator.allocate(64);
EXPECT_NE(failOffset2, 0); // Still cannot allocate at offset 0.
EXPECT_NE(failId2, 1);
// 5. Final release. The lock count is now 0.
mAllocator.releaseGpu(id); // gpuUseCount = 0
// 6. Now it should be freed and available.
mAllocator.releaseFreeSlots();
auto [successId, successOffset] = mAllocator.allocate(64);
EXPECT_EQ(successOffset, 0);
EXPECT_EQ(successId, 1);
}
TEST_F(BufferAllocatorTest, GpuPanicOnUnderflow) {
// 1. Allocate a block and acquire it.
auto [id, _] = mAllocator.allocate(128);
mAllocator.acquireGpu(id);
// 2. Release it once, which is fine.
mAllocator.releaseGpu(id);
// 3. Releasing it again when the count is 0 should trigger a failed assertion.
EXPECT_DEATH(mAllocator.releaseGpu(id), "failed assertion");
}
TEST_F(BufferAllocatorTest, MergeFreeSlots) {
// Allocate three blocks
auto [id1, offset1] = mAllocator.allocate(128); // Slot 0-127
auto [id2, offset2] = mAllocator.allocate(128); // Slot 128-255
auto [id3, offset3] = mAllocator.allocate(128); // Slot 256-383
EXPECT_EQ(id1, 1);
EXPECT_EQ(id2, 3);
EXPECT_EQ(id3, 5);
EXPECT_EQ(offset1, 0);
EXPECT_EQ(offset2, 128);
EXPECT_EQ(offset3, 256);
// Retire the first and third blocks
mAllocator.retire(id1);
mAllocator.retire(id3);
// At this point, we have: [Free, Allocated, Free, Free (remaining)]
// releaseFreeSlots should not merge slot 1 and 3 because they are not adjacent.
// It should merge slot 3 and slot 4 instead.
mAllocator.releaseFreeSlots();
// At this point, we have: [Free, Allocated, Free (remaining)]
// Let's verify by trying to allocate 200. It should go into the merged slot.
auto [id4, offset4] = mAllocator.allocate(200); // Aligns to 256
EXPECT_EQ(id4, 5);
EXPECT_EQ(offset4, 256);
// Now, retire the middle block
mAllocator.retire(id2);
// Now we have: [Free, Free, Allocated, Free (remaining)]
// The first two blocks are now adjacent and free.
mAllocator.releaseFreeSlots();
// After merging, the first 256 bytes should be one large free block.
// Let's try to allocate something that requires this merged space.
auto [id5, offset5] = mAllocator.allocate(200); // Aligns to 256
EXPECT_EQ(id5, 1);
EXPECT_EQ(offset5, 0);
}
TEST_F(BufferAllocatorTest, MergeAllSlots) {
// Allocate the entire buffer in small chunks.
constexpr BufferAllocator::allocation_size_t CHUNK_SIZE = 128;
constexpr uint32_t NUM_CHUNKS = TOTAL_SIZE / CHUNK_SIZE;
std::vector<BufferAllocator::AllocationId> ids;
for (uint32_t i = 0; i < NUM_CHUNKS; ++i) {
auto [id, offset] = mAllocator.allocate(CHUNK_SIZE);
ASSERT_NE(id, BufferAllocator::REALLOCATION_REQUIRED);
ids.push_back(id);
}
// The buffer should be full.
auto [failId, failOffset] = mAllocator.allocate(1);
EXPECT_EQ(failId, BufferAllocator::REALLOCATION_REQUIRED);
EXPECT_EQ(failOffset, 0);
// Retire all chunks.
for (auto id : ids) {
mAllocator.retire(id);
}
// Release and merge.
mAllocator.releaseFreeSlots();
// Now, the allocator should be back to its initial state with one large free block.
// We should be able to allocate the entire buffer again.
auto [fullId, fullOffset] = mAllocator.allocate(TOTAL_SIZE);
EXPECT_EQ(fullId, 1);
EXPECT_EQ(fullOffset, 0);
}
TEST_F(BufferAllocatorTest, NoMergePossible) {
// Allocate blocks in an alternating pattern.
auto [id1, offset1] = mAllocator.allocate(128);
auto [id2, offset2] = mAllocator.allocate(128);
auto [id3, offset3] = mAllocator.allocate(128);
auto [id4, offset4] = mAllocator.allocate(128);
EXPECT_EQ(id1, 1);
EXPECT_EQ(id2, 3);
EXPECT_EQ(id3, 5);
EXPECT_EQ(id4, 7);
EXPECT_EQ(offset1, 0);
EXPECT_EQ(offset2, 128);
EXPECT_EQ(offset3, 256);
EXPECT_EQ(offset4, 384);
// Retire the first and third blocks, leaving allocated blocks in between.
mAllocator.retire(id1);
mAllocator.retire(id3);
// At this point, we have: [Free, Allocated, Free, Allocated, Free (remaining)]
mAllocator.releaseFreeSlots();
// Now, let's try to re-allocate the two 128-byte slots.
auto [id5, offset5] = mAllocator.allocate(128);
auto [id6, offset6] = mAllocator.allocate(128);
EXPECT_EQ(offset5, 0);
EXPECT_EQ(offset6, 256);
}
TEST_F(BufferAllocatorTest, Reset) {
auto [id1, offset1] = mAllocator.allocate(100);
auto [id2, offset2] = mAllocator.allocate(200);
EXPECT_EQ(id1, 1);
EXPECT_EQ(id2, 3);
EXPECT_EQ(offset1, 0);
EXPECT_EQ(offset2, 128);
// Reset the allocator to a new size.
mAllocator.reset(2048);
EXPECT_EQ(mAllocator.getTotalSize(), 2048);
// After reset, all previous allocations should be gone,
// and we should be able to allocate the entire new size.
auto [id3, offset3] = mAllocator.allocate(2048);
EXPECT_EQ(id3, 1);
EXPECT_EQ(offset3, 0);
}
TEST_F(BufferAllocatorTest, ResetWithInvalidSize) {
// Reset to a size which is not a power of two.
EXPECT_DEATH(mAllocator.reset(123), "failed assertion");
}
TEST_F(BufferAllocatorTest, ResetWithGpuLock) {
// 1. Allocate a block and acquire a GPU lock on it.
auto [id1, offset1] = mAllocator.allocate(128);
EXPECT_EQ(id1, 1);
mAllocator.acquireGpu(id1); // gpuUseCount = 1
// 2. Call reset. This should disregard the GPU lock and clear everything.
constexpr BufferAllocator::allocation_size_t NEW_TOTAL_SIZE = 4096;
mAllocator.reset(NEW_TOTAL_SIZE);
// 3. Verify the allocator is in a pristine state with the new size.
EXPECT_EQ(mAllocator.getTotalSize(), NEW_TOTAL_SIZE);
// 4. The strongest verification is to allocate the entire new size, which should
// succeed, proving that the old GPU-locked block is gone.
auto [id2, offset2] = mAllocator.allocate(NEW_TOTAL_SIZE);
EXPECT_EQ(id2, 1);
EXPECT_EQ(offset2, 0);
}
TEST_F(BufferAllocatorTest, InvalidOperations) {
// These operations on invalid IDs should not crash and should be handled gracefully.
EXPECT_DEATH(mAllocator.retire(BufferAllocator::UNALLOCATED), "failed assertion");
EXPECT_DEATH(mAllocator.retire(999), "failed assertion"); // Non-existent ID
EXPECT_DEATH(mAllocator.acquireGpu(BufferAllocator::UNALLOCATED), "failed assertion");
EXPECT_DEATH(mAllocator.acquireGpu(999), "failed assertion");
EXPECT_DEATH(mAllocator.releaseGpu(BufferAllocator::UNALLOCATED), "failed assertion");
EXPECT_DEATH(mAllocator.releaseGpu(999), "failed assertion");
// Check that an invalid offset query panics in debug/testing builds.
EXPECT_DEATH(mAllocator.getAllocationOffset(BufferAllocator::UNALLOCATED), "failed assertion");
EXPECT_DEATH(mAllocator.getAllocationOffset(BufferAllocator::REALLOCATION_REQUIRED),
"failed assertion");
}
TEST_F(BufferAllocatorTest, ComplexScenario) {
std::vector<BufferAllocator::AllocationId> ids;
// 1. Allocate 4 blocks of 256 bytes
for (int i = 0; i < 4; ++i) {
auto [id, offset] = mAllocator.allocate(256);
EXPECT_EQ(offset, i * 256);
ids.push_back(id);
}
// Buffer should be full
auto [failId1, failOffset1] = mAllocator.allocate(1);
EXPECT_EQ(failId1, BufferAllocator::REALLOCATION_REQUIRED);
// 2. Retire the 2nd and 3rd blocks (ids[1] and ids[2])
mAllocator.retire(ids[1]);
mAllocator.retire(ids[2]);
// 3. Release free slots. This should merge the two retired blocks.
mAllocator.releaseFreeSlots();
// We now have a free block of 512 bytes at offset 256.
// Let's allocate 512 bytes. It should fit perfectly.
auto [id, offset] = mAllocator.allocate(512);
EXPECT_EQ(id, 5); // (256 / 64) + 1 = 5
EXPECT_EQ(offset, 256);
// 4. The buffer should be full again.
auto [failId2,failOffset2] = mAllocator.allocate(1);
EXPECT_EQ(failId2, BufferAllocator::REALLOCATION_REQUIRED);
EXPECT_EQ(failOffset2,0);
}
} // anonymous namespace
@@ -0,0 +1,122 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <gtest/gtest.h>
#include "../src/details/BufferAllocator.h"
#include "utils/Panic.h"
#include <algorithm>
#include <random>
#include <vector>
using namespace filament;
namespace {
class BufferAllocatorStressTest : public ::testing::Test {
protected:
// We use a total size of 1024 * 64 and a slot size (alignment) of 64.
// This gives us 1024 total possible slots if aligned.
static constexpr BufferAllocator::allocation_size_t SLOT_SIZE = 64;
static constexpr BufferAllocator::allocation_size_t SLOT_COUNT = 4096;
static constexpr BufferAllocator::allocation_size_t TOTAL_SIZE = SLOT_COUNT * SLOT_SIZE;
BufferAllocatorStressTest() : mAllocator(TOTAL_SIZE, SLOT_SIZE) {
}
BufferAllocator mAllocator;
};
TEST_F(BufferAllocatorStressTest, StressTest) {
// Many operations.
constexpr int operationCount = 5000;
// 1. Prepare the random number distributions.
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> slotCountDistrib(1, SLOT_COUNT);
// Random the slot offset we allocate within a slot.
std::uniform_int_distribution<> slotOffsetDistrib(0, SLOT_SIZE - 1);
// Random the operation we perform: 0,1,2 -> allocate, 3 -> retire, 4 -> release slots
// We bias the operations to make more allocations than releases.
std::uniform_int_distribution operationDistrib(0, 4);
// 2. Randomly do some actions and expect to have no crash
std::vector<BufferAllocator::AllocationId> ids;
for (int i = 0; i < operationCount; i++) {
switch (operationDistrib(gen)) {
case 0: // Allocate
case 1: // Allocate
case 2: // Allocate
{
// Random the slot count + its offset.
const BufferAllocator::allocation_size_t size =
slotCountDistrib(gen) * SLOT_SIZE - slotOffsetDistrib(gen);
if (auto [id, _] = mAllocator.allocate(size);
id != BufferAllocator::REALLOCATION_REQUIRED && id !=
BufferAllocator::UNALLOCATED) {
ids.push_back(id);
}
break;
}
case 3: // Allocate
{
if (ids.empty()) {
continue;
}
// Retire a random slot.
std::uniform_int_distribution<uint32_t> idDistrib(0, ids.size() - 1);
uint32_t indexToRetire = idDistrib(gen);
BufferAllocator::AllocationId idToRetire = ids[indexToRetire];
mAllocator.retire(idToRetire);
// Remove the retired id from the list.
std::swap(ids[indexToRetire], ids.back());
ids.pop_back();
break;
}
case 4: // Allocate
{
mAllocator.releaseFreeSlots();
break;
}
default:
break;
}
}
// 3. Retire all remaining allocations.
for (const auto& id: ids) {
mAllocator.retire(id);
}
ids.clear();
// 4. Release and merge everything.
mAllocator.releaseFreeSlots();
// 5. The allocator should now be in a pristine state.
// A final allocation of the total size should succeed.
auto [finalId, finalOffset] = mAllocator.allocate(TOTAL_SIZE);
EXPECT_NE(finalId, BufferAllocator::REALLOCATION_REQUIRED);
EXPECT_NE(finalId, BufferAllocator::UNALLOCATED);
EXPECT_EQ(finalOffset, 0);
}
} // anonymous namespace
+1 -3
View File
@@ -56,7 +56,6 @@ set(PRIVATE_HDRS
src/MetalArgumentBuffer.h
src/ShaderMinifier.h
src/SpirvFixup.h
src/SpirvRemapWrapper.h
src/sca/ASTHelpers.h
src/sca/GLSLTools.h
src/sca/builtinResource.h)
@@ -72,8 +71,7 @@ set(SRCS
src/sca/GLSLTools.cpp
src/GLSLPostProcessor.cpp
src/ShaderMinifier.cpp
src/SpirvFixup.cpp
src/SpirvRemapWrapper.cpp)
src/SpirvFixup.cpp)
# ==================================================================================================
# Include and target definitions
+2 -6
View File
@@ -290,8 +290,6 @@ GLSLPostProcessor::GLSLPostProcessor(
: mOptimization(optimization), mWorkarounds(workarounds),
mPrintShaders(flags & PRINT_SHADERS),
mGenerateDebugInfo(flags & GENERATE_DEBUG_INFO) {
// This should occur only once, to avoid races.
SpirvRemapWrapperSetUp();
}
GLSLPostProcessor::~GLSLPostProcessor() = default;
@@ -1002,7 +1000,8 @@ std::shared_ptr<Optimizer> GLSLPostProcessor::createOptimizer(
void GLSLPostProcessor::optimizeSpirv(OptimizerPtr optimizer, SpirvBlob& spirv) {
// always add the CanonicalizeIds Pass
// Always add the CanonicalizeIds Pass.
// The CanonicalIds pass replaces the old SPIR-V remapper in Glslang.
optimizer->RegisterPass(CreateCanonicalizeIdsPass());
// run optimizer
@@ -1010,9 +1009,6 @@ void GLSLPostProcessor::optimizeSpirv(OptimizerPtr optimizer, SpirvBlob& spirv)
slog.e << "SPIR-V optimizer pass failed" << io::endl;
return;
}
// Remove dead module-level objects: functions, types, vars
SpirvRemapWrapperRemap(spirv);
}
void GLSLPostProcessor::fixupClipDistance(
-1
View File
@@ -23,7 +23,6 @@
#include <private/filament/SamplerInterfaceBlock.h>
#include "ShaderMinifier.h"
#include "SpirvRemapWrapper.h"
#include <spirv-tools/optimizer.hpp>
-54
View File
@@ -1,54 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SpirvRemapWrapper.h"
#include <SPVRemapper.h>
#include <utils/Log.h>
#include <utils/ostream.h>
#include <string>
namespace filamat {
void SpirvRemapWrapperSetUp() {
// SPIRV error handler registration should occur only once.
// Construct this SpirvRemapWrapper object only once.
spv::spirvbin_t::registerErrorHandler([](const std::string& str) {
utils::slog.e << str << utils::io::endl;
});
// Similar to above, we need to do a no-op remap to init a static
// table in the remapper before the jobs start using remap().
spv::spirvbin_t remapper(0);
// We need to provide at least a valid header to not crash.
std::vector<uint32_t> spirv {
0x07230203,// MAGIC
0, // VERSION
0, // GENERATOR
0, // BOUND
0 // SCHEMA, must be 0
};
remapper.remap(spirv, 0);
}
void SpirvRemapWrapperRemap(std::vector<uint32_t>& spirv) {
// Remove dead module-level objects: functions, types, vars
spv::spirvbin_t remapper(0);
remapper.remap(spirv, spv::spirvbin_base_t::DCE_ALL);
}
} // namespace filamat
-30
View File
@@ -1,30 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef TNT_SPIRVREMAPWRAPPER_H
#define TNT_SPIRVREMAPWRAPPER_H
#include <cstdint>
#include <vector>
namespace filamat {
void SpirvRemapWrapperSetUp();
void SpirvRemapWrapperRemap(std::vector<uint32_t>& spirv);
} // namespace filamat
#endif //TNT_SPIRVREMAPWRAPPER_H
+43 -31
View File
@@ -16,15 +16,15 @@
#include "JsonishParser.h"
#include <iostream>
#include <utils/sstream.h>
#include <utils/string.h>
#include <utils/Status.h>
#include <string.h>
#include <utils/string.h>
namespace matp {
static std::string resolveEscapes(const std::string& s) {
static std::pair<utils::Status, std::string> resolveEscapes(const std::string& s) {
std::string out;
out.reserve(s.length());
@@ -58,11 +58,15 @@ static std::string resolveEscapes(const std::string& s) {
break;
case 'u':
// TODO: implement unicode escape sequences
std::cerr << "Unicode escape sequences currently not supported." << std::endl;
return { utils::Status::unsupported(
"Unicode escape sequences currently not supported."),
out };
break;
default:
std::cerr << "Invalid escape sequence \\" << c << "." << std::endl;
return out;
utils::io::sstream errorMessage;
errorMessage << "Invalid escape sequence \\" << c << ".";
return { utils::Status::invalidArgument(
errorMessage.c_str()), out };
}
inEscape = false;
} else {
@@ -76,14 +80,10 @@ static std::string resolveEscapes(const std::string& s) {
}
if (inEscape) {
std::cerr << "Incomplete escape sequence." << std::endl;
return { utils::Status::invalidArgument("Incomplete escape sequence."), out };
}
return out;
}
JsonishString::JsonishString(const std::string& string) : JsonishValue(STRING) {
mString = resolveEscapes(string);
return { utils::Status::ok(), out };
}
std::unique_ptr<JsonishObject> JsonishParser::parse() noexcept {
@@ -91,10 +91,14 @@ std::unique_ptr<JsonishObject> JsonishParser::parse() noexcept {
return p;
}
utils::Status JsonishParser::getParseStatus() const noexcept {
return mStatus;
}
JsonishObject* JsonishParser::parseObject() noexcept {
// Read the {
if (!consumeLexeme(JsonType::BLOCK_START)) {
reportError("expected token '{'");
setInvalidArgumentError("expected token '{'");
return nullptr;
}
@@ -108,7 +112,7 @@ JsonishObject* JsonishParser::parseObject() noexcept {
// Read the }
if (!consumeLexeme(JsonType::BLOCK_END)) {
reportError("expected token '}'");
setInvalidArgumentError("expected token '}'");
delete object;
return nullptr;
}
@@ -119,7 +123,7 @@ JsonishObject* JsonishParser::parseObject() noexcept {
JsonishArray* JsonishParser::parseArray() noexcept {
// Read the [
if (!consumeLexeme(JsonType::ARRAY_START)) {
reportError("expected token '['");
setInvalidArgumentError("expected token '['");
return nullptr;
}
@@ -133,7 +137,7 @@ JsonishArray* JsonishParser::parseArray() noexcept {
// Read the ]
if (!consumeLexeme(JsonType::ARRAY_END)) {
reportError("expected token ']'");
setInvalidArgumentError("expected token ']'");
delete array;
return nullptr;
}
@@ -203,7 +207,7 @@ JsonishObject* JsonishParser::parseMembers() noexcept {
break;
}
reportError("unable to parse pair");
setInvalidArgumentError("unable to parse pair");
delete object;
return nullptr;
@@ -225,7 +229,7 @@ JsonishArray* JsonishParser::parseElements() noexcept {
while (true) {
JsonishValue* value = parseValue();
if (!value) {
reportError("unable to read value");
setInvalidArgumentError("unable to read value");
delete array;
return nullptr;
}
@@ -260,7 +264,12 @@ JsonishValue* JsonishParser::parseString() noexcept {
} else {
tmp = std::string(strLexeme->getStart(), strLexeme->getSize());
}
return new JsonishString(tmp);
auto [status, resolvedStr] = resolveEscapes(tmp);
if (!status.isOk()) {
mStatus = status;
return nullptr;
}
return new JsonishString(resolvedStr);
}
JsonishValue* JsonishParser::parseValue() noexcept {
@@ -286,17 +295,20 @@ JsonishValue* JsonishParser::parseValue() noexcept {
consumeLexeme(NUll);
return new JsonishNull();
default:
reportError("unexpected token");
setInvalidArgumentError("unexpected token");
return nullptr;
}
}
void JsonishParser::reportError(const char* message) noexcept {
if (mErrorReported) {
void JsonishParser::setInvalidArgumentError(const char* message) noexcept {
// Already encountered an error.
if (!mStatus.isOk()) {
return;
}
std::cerr << "Syntax error, " << message;
utils::io::sstream errorMessage;
errorMessage << "JsonishParser error" << utils::io::endl;
errorMessage << "Syntax error, " << message;
const JsonLexeme* lexeme = peekNextLexemeType();
if (lexeme == nullptr) {
@@ -304,16 +316,16 @@ void JsonishParser::reportError(const char* message) noexcept {
}
if (lexeme != nullptr) {
std::cerr << " at line:" << lexeme->getLine()
<< " position:" << lexeme->getLinePosition() << std::endl
<< " got lexeme type: "
<< JsonLexeme::getTypeString(lexeme->getType()) << std::endl
<< " got lexeme value: " << lexeme->getStringValue() << std::endl;
errorMessage << " at line:" << lexeme->getLine()
<< " position:" << lexeme->getLinePosition() << utils::io::endl
<< " got lexeme type: "
<< JsonLexeme::getTypeString(lexeme->getType()) << utils::io::endl
<< " got lexeme value: " << lexeme->getStringValue() << utils::io::endl;
} else {
std::cerr << " but reached end of file instead." << std::endl;
errorMessage << " but reached end of file instead." << utils::io::endl;
}
mErrorReported = true;
mStatus = utils::Status::invalidArgument(errorMessage.c_str());
}
} // namespace matp
+8 -3
View File
@@ -19,6 +19,8 @@
#include "JsonishLexeme.h"
#include <utils/Status.h>
#include <memory>
#include <unordered_map>
#include <vector>
@@ -178,7 +180,8 @@ private:
class JsonishString final : public JsonishValue {
public:
explicit JsonishString(const std::string& string);
// Expects the input string to have its escape symbols resolved.
explicit JsonishString(const std::string& string) : JsonishValue(STRING), mString(string) {}
~JsonishString() override = default;
@@ -259,6 +262,8 @@ public:
std::unique_ptr<JsonishObject> parse() noexcept;
utils::Status getParseStatus() const noexcept;
private:
JsonishObject* parseObject() noexcept;
JsonishArray* parseArray() noexcept;
@@ -272,11 +277,11 @@ private:
const JsonLexeme* peekNextLexemeType() const noexcept;
const JsonLexeme* peekCurrentLexemeType() const noexcept;
void reportError(const char* message) noexcept;
void setInvalidArgumentError(const char* message) noexcept;
size_t mCursor = 0;
const std::vector<JsonLexeme>& mLexemes;
bool mErrorReported = false;
utils::Status mStatus = utils::Status::ok();
};
} // namespace matp
+4 -2
View File
@@ -68,8 +68,10 @@ utils::Status MaterialParser::processMaterial(const MaterialLexeme& jsonLexeme,
JsonishParser parser(jlexer.getLexemes());
std::unique_ptr<JsonishObject> const json = parser.parse();
if (json == nullptr) {
return utils::Status::internal("JsonishParser error (see above).");
// If it fails to parse, json value is always null.
if (utils::Status jsonishParseStatus = parser.getParseStatus(); !jsonishParseStatus.isOk() ||
json == nullptr) {
return jsonishParseStatus;
}
ParametersProcessor parametersProcessor;
+33
View File
@@ -259,6 +259,21 @@ material: {
})");
static std::string invalidJsonMaterialSourceNoQuotes(R"({
material: {
blending: opaque,
parameters: [
{
name: baseColorFactor\\a\\a, // invalid escape symbols
type: float4
},
],
shadingModel: lit
}
})");
TEST_F(MaterialLexer, JsonMaterialLexingAndParsing) {
matp::JsonishLexer jsonishLexer;
jsonishLexer.lex(jsonMaterialSource.c_str(), jsonMaterialSource.size(), 1);
@@ -299,6 +314,24 @@ TEST_F(MaterialLexer, JsonMaterialParser) {
EXPECT_EQ(result.getCode(), utils::StatusCode::OK);
}
TEST_F(MaterialLexer, JsonMaterialParserInvalidInputReturnsError) {
matp::JsonishLexer jsonishLexer;
jsonishLexer.lex(
invalidJsonMaterialSourceNoQuotes.c_str(),
invalidJsonMaterialSourceNoQuotes.size(), 1);
auto lexemes = jsonishLexer.getLexemes();
EXPECT_TRUE(!lexemes.empty());
matp::JsonishParser jsonishParser(lexemes);
EXPECT_TRUE(jsonishParser.getParseStatus().isOk());
std::unique_ptr<matp::JsonishObject> root = jsonishParser.parse();
EXPECT_FALSE(jsonishParser.getParseStatus().isOk());
EXPECT_EQ(root, nullptr);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
+6 -2
View File
@@ -28,8 +28,8 @@
namespace utils {
/**
* Simple asynchronous job queue. This manages a thread that executes jobs submitted to it
* in order.
* Simple asynchronous job queue. This manages a *single*thread that executes jobs submitted
* to it in order.
*/
class AsyncJobQueue {
public:
@@ -48,13 +48,17 @@ public:
// adds a job to the queue. no-op if drainAndExit() was called.
void push(Job&& job);
bool isValid() const noexcept;
private:
#if !defined(__EMSCRIPTEN__)
using Container = std::vector<Job>;
std::thread mThread;
Mutex mLock; // NOLINT(*-include-cleaner)
Condition mCondition; // NOLINT(*-include-cleaner)
Container mQueue;
bool mExitRequested = false;
#endif
};
} // namespace utils
+11
View File
@@ -34,6 +34,8 @@ enum class StatusCode {
INVALID_ARGUMENT,
/** Internal error was occurred while processing the request. */
INTERNAL,
/** The requested operation is not supported. */
UNSUPPORTED,
};
/**
@@ -138,6 +140,15 @@ public:
return {StatusCode::INVALID_ARGUMENT, message};
}
/**
* Creates an error Status with an UNSUPPORTED status code.
* @param message The error message to include.
* @return an error Status with an UNSUPPORTED status code.
*/
static Status unsupported(std::string_view message) {
return { StatusCode::UNSUPPORTED, message };
}
private:
StatusCode mStatusCode;
// Additional message for the Status. Usually contains the reason for the error.
+38 -8
View File
@@ -15,8 +15,10 @@
*/
#include <utils/AsyncJobQueue.h>
#include <utils/compiler.h>
#include <utils/JobSystem.h>
#include <utils/debug.h>
#include <utils/Logger.h>
#include <mutex>
#include <utility>
@@ -24,49 +26,77 @@
namespace utils {
AsyncJobQueue::AsyncJobQueue(const char* name, Priority priority) {
#if !defined(__EMSCRIPTEN__)
mQueue.reserve(2);
mThread = std::thread([this, name, priority]() {
JobSystem::setThreadName(name);
JobSystem::setThreadPriority(priority);
auto& queue = mQueue;
bool exitRequested;
do {
std::unique_lock lock(mLock);
mCondition.wait(lock, [this]() -> bool { return mExitRequested || !mQueue.empty(); });
// wait until we get a job, or we're asked to exit
mCondition.wait(lock, [this]() -> bool {
return mExitRequested || !mQueue.empty();
});
exitRequested = mExitRequested;
if (!queue.empty()) {
Job const job(std::move(queue.front()));
queue.erase(queue.begin());
lock.unlock();
auto const queue = std::move(mQueue);
// here we have drained the whole queue, and if exitRequested is set, we're guaranteed
// no more job will be added after we unlock.
lock.unlock();
// execute the jobs without holding a lock. These jobs must be executed in order,
// front to back, and are allowed to be long-running (like waiting on a fence).
for (auto& job : queue) {
job();
}
} while (!exitRequested);
});
#endif
}
AsyncJobQueue::~AsyncJobQueue() noexcept {
#if !defined(__EMSCRIPTEN__)
assert_invariant(mQueue.empty());
#endif
}
void AsyncJobQueue::push(Job&& job) {
#if !defined(__EMSCRIPTEN__)
std::unique_lock lock(mLock);
if (!mExitRequested) {
if (UTILS_UNLIKELY(mExitRequested)) {
LOG(WARNING) << "AsyncJobQueue::push() called after drainAndExit()";
}
assert_invariant(!mExitRequested);
if (UTILS_LIKELY(!mExitRequested)) {
mQueue.push_back(std::move(job));
lock.unlock();
mCondition.notify_one();
}
#endif
}
bool AsyncJobQueue::isValid() const noexcept {
#if !defined(__EMSCRIPTEN__)
return mThread.joinable();
#else
return false;
#endif
}
void AsyncJobQueue::drainAndExit() {
#if !defined(__EMSCRIPTEN__)
std::unique_lock lock(mLock);
mCondition.wait(lock, [this] { return mQueue.empty(); });
// we request the service thread to exit, but we're guaranteed that it'll only exit
// after all current callbacks are processed. In addition, once mExitRequested is set,
// no new jobs can be added, so we can join the thread.
mExitRequested = true;
lock.unlock();
mCondition.notify_one();
if (mThread.joinable()) {
mThread.join();
}
#endif
}
} // namespace utils
+2
View File
@@ -28,6 +28,8 @@ utils::io::ostream& operator<<(utils::io::ostream& os, const Status& status) {
break;
case StatusCode::INTERNAL: os << "Internal error";
break;
case StatusCode::UNSUPPORTED: os << "Unsupported error";
break;
}
os << ", with a message: " << status.getMessage();
return os;
+8 -2
View File
@@ -115,6 +115,12 @@ TEST(StatusTest, StaticInternalError) {
EXPECT_EQ(Status::internal(errorMessage), expected);
}
TEST(StatusTest, StaticUnsupportedError) {
std::string_view errorMessage = "unsupported error";
Status expected(StatusCode::UNSUPPORTED, errorMessage);
EXPECT_EQ(Status::unsupported(errorMessage), expected);
}
TEST(StatusTest, IsOk) {
Status status = Status::ok();
EXPECT_TRUE(status.isOk());
@@ -125,9 +131,9 @@ TEST(StatusTest, IsOk) {
TEST(StatusTest, SelfAssignment) {
Status status = Status::internal("error");
Status original_copy = status;
Status self = status;
status = status;
EXPECT_EQ(status, original_copy);
EXPECT_EQ(status, self);
}
+8
View File
@@ -51,6 +51,7 @@ struct Settings;
struct ViewSettings;
struct LightSettings;
struct ViewerOptions;
struct DebugOptions;
enum class ToneMapping : uint8_t {
LINEAR = 0,
@@ -88,6 +89,8 @@ void applySettings(Engine* engine, const LightSettings& settings, IndirectLight*
const utils::Entity* sceneLights, size_t sceneLightCount, LightManager* lm, Scene* scene, View* view);
void applySettings(Engine* engine, const ViewerOptions& settings, Camera* camera, Skybox* skybox,
Renderer* renderer);
void applySettings(Engine* engine, const DebugOptions& settings,
Renderer* renderer);
// Creates a new ColorGrading object based on the given settings.
UTILS_PUBLIC
@@ -246,11 +249,16 @@ struct ViewerOptions {
bool autoInstancingEnabled = false;
};
struct DebugOptions {
uint16_t skipFrames = 0;
};
struct Settings {
ViewSettings view;
MaterialSettings material;
LightSettings lighting;
ViewerOptions viewer;
DebugOptions debug;
};
} // namespace viewer
+1
View File
@@ -201,6 +201,7 @@ void AutomationEngine::applySettings(Engine* engine, const char* json, size_t js
Camera* camera = &content.view->getCamera();
Skybox* skybox = content.scene->getSkybox();
viewer::applySettings(engine, mSettings->viewer, camera, skybox, content.renderer);
viewer::applySettings(engine, mSettings->debug, content.renderer);
}
ColorGrading* AutomationEngine::getColorGrading(Engine* engine) {
+36 -2
View File
@@ -37,7 +37,6 @@
using namespace utils;
namespace filament::viewer {
std::string_view to_string(color::ColorSpace const& colorspace) noexcept {
using namespace color;
if (colorspace == Rec709-Linear-D65) {
@@ -544,6 +543,26 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ViewerOp
return i;
}
static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, DebugOptions* out) {
CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
int size = tokens[i++].size;
for (int j = 0; j < size; ++j) {
const jsmntok_t tok = tokens[i];
CHECK_KEY(tok);
if (compare(tok, jsonChunk, "skipFrames") == 0) {
i = parse(tokens, i + 1, jsonChunk, &out->skipFrames);
} else {
slog.w << "Invalid debug options key: '" << STR(tok, jsonChunk) << "'" << io::endl;
i = parse(tokens, i + 1);
}
if (i < 0) {
slog.e << "Invalid debug options value: '" << STR(tok, jsonChunk) << "'" << io::endl;
return i;
}
}
return i;
}
int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, Settings* out) {
CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
int size = tokens[i++].size;
@@ -558,6 +577,8 @@ int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, Settings* out)
i = parse(tokens, i + 1, jsonChunk, &out->lighting);
} else if (compare(tok, jsonChunk, "viewer") == 0) {
i = parse(tokens, i + 1, jsonChunk, &out->viewer);
} else if (compare(tok, jsonChunk, "debug") == 0) {
i = parse(tokens, i + 1, jsonChunk, &out->debug);
} else {
slog.w << "Invalid group key: '" << STR(tok, jsonChunk) << "'" << io::endl;
i = parse(tokens, i + 1);
@@ -681,6 +702,12 @@ void applySettings(Engine* engine, const ViewerOptions& settings, Camera* camera
}
}
void applySettings(Engine* engine, const DebugOptions& settings, Renderer* renderer) {
if (!renderer->getFrameToSkipCount()) {
renderer->skipNextFrames(settings.skipFrames);
}
}
constexpr ToneMapper* createToneMapper(const ColorGradingSettings& settings) noexcept {
switch (settings.toneMapping) {
case ToneMapping::LINEAR: return new LinearToneMapper;
@@ -914,6 +941,12 @@ static std::ostream& operator<<(std::ostream& out, const ViewerOptions& in) {
<< "}";
}
static std::ostream& operator<<(std::ostream& out, const DebugOptions& in) {
return out << "{\n"
<< "\"skipFrames\": " << (in.skipFrames) << "\n"
<< "}";
}
static std::ostream& operator<<(std::ostream& out, const DynamicLightingSettings& in) {
return out << "{\n"
<< "\"zLightNear\": " << (in.zLightNear) << ",\n"
@@ -950,7 +983,8 @@ static std::ostream& operator<<(std::ostream& out, const Settings& in) {
<< "\"view\": " << (in.view) << ",\n"
<< "\"material\": " << (in.material) << ",\n"
<< "\"lighting\": " << (in.lighting) << ",\n"
<< "\"viewer\": " << (in.viewer)
<< "\"viewer\": " << (in.viewer) << ",\n"
<< "\"debug\": " << (in.debug)
<< "}";
}
+7
View File
@@ -1149,6 +1149,13 @@ void ViewerGui::updateUserInterface() {
#endif
}
if (ImGui::CollapsingHeader("Debug Options")) {
mSettings.debug.skipFrames = 0;
if (ImGui::Button("Skip 10 frames")) {
mSettings.debug.skipFrames = 10;
}
}
colorGradingUI(mSettings, mRangePlot, mCurvePlot, mToneMapPlot);
// At this point, all View settings have been modified,
+4
View File
@@ -1143,6 +1143,10 @@ int main(int argc, char** argv) {
Skybox* skybox = scene->getSkybox();
applySettings(engine, app.viewer->getSettings().viewer, &camera, skybox, renderer);
// FIMXE: This applySettings() is done here instead of in AutomationEngine.cpp because
// we need access to the Renderer, which AutomationEngine does not provide.
applySettings(engine, app.viewer->getSettings().debug, renderer);
// technically we don't need to do this each frame
auto& tcm = engine->getTransformManager();
TransformManager::Instance const& root = tcm.getInstance(app.rootTransformEntity);
+2 -2
View File
@@ -191,6 +191,8 @@ static int handleCommandLineArgments(int argc, char* argv[], Config* config) {
}
static void cleanup(Engine* engine, View*, Scene*) {
g_meshSet.reset(nullptr);
for (const auto& material : g_meshMaterialInstances) {
engine->destroy(material.second);
}
@@ -203,8 +205,6 @@ static void cleanup(Engine* engine, View*, Scene*) {
engine->destroy(i);
}
g_meshSet.reset(nullptr);
engine->destroy(g_params.light);
engine->destroy(g_params.spotLight);
engine->destroy(g_colorGrading);
+3 -1
View File
@@ -66,6 +66,8 @@ for include in "${includes[@]}"; do
echo "#define SYSTRACE_TAG SYSTRACE_TAG_DISABLED" >> ${TMP_FILE}
fi
echo "#include <${include}>" >> ${TMP_FILE}
clang -std=c++20 -I "${FILAMENT_HEADERS}" ${TMP_FILE} -c -o /dev/null
# Filament is built internally with C++20, but we maintain C++17 compatibility (for the time
# being) in our public headers to support projects on older toolchains.
clang -std=c++17 -I "${FILAMENT_HEADERS}" ${TMP_FILE} -c -o /dev/null
done
echo "Done!"
+10 -6
View File
@@ -51,14 +51,18 @@ renderings, do the following step
bash test/renderdiff/generate.sh
```
## Filtering Tests
## Command-line Options
You can run a subset of the tests by passing the `--test_filter` flag to `local_test.sh`. The
filter supports wildcards (`*`) to match test names. For example, to run all tests in the
`ClearCoat` suite, you can use the following command:
You can control the behavior of the test scripts with the following flags passed to `local_test.sh`:
- `--test_filter=<filter>`: Run a subset of tests. The filter supports wildcards (`*`) to match test names.
- `--no_rebuild`: Skip rebuilding the `gltf_viewer` executable.
- `--num_threads=<number>`: Set the number of threads for rendering. If not set, the system's default is used.
For example, to run all `MSAA` tests without rebuilding and using 8 threads:
```
bash test/renderdiff/local_test.sh --test_filter='ClearCoat.*.*'
bash test/renderdiff/local_test.sh --test_filter='MSAA.*.*' --no_rebuild --num_threads=8
```
## Update the golden images
@@ -152,4 +156,4 @@ the run number is `18023632663`.
[github_token_ref]: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
[Mesa]: https://docs.mesa3d.org
[SwiftShader]: https://github.com/google/swiftshader
[presubmit-renderdiff]: https://github.com/google/filament/blob/e85dfe75c86106a05019e13ccdbef67e030af675/.github/workflows/presubmit.yml#L118
[presubmit-renderdiff]: https://github.com/google/filament/blob/e85dfe75c86106a05019e13ccdbef67e030af675/.github/workflows/presubmit.yml#L118
+16 -2
View File
@@ -41,7 +41,12 @@ function start_render_() {
# -W enables the webgpu build
# -f forces regeneration of cmake build files
# -X points to the mesa directory, which contains the compiled gl and vk drivers.
CXX=`which clang++` CC=`which clang` ./build.sh -f -W -X ${MESA_DIR} -p desktop debug gltf_viewer
GLTF_VIEWER_PATH="$(pwd)/out/cmake-debug/samples/gltf_viewer"
if [[ "$NOREBUILD" == "true" ]] && [[ -f ${GLTF_VIEWER_PATH} ]]; then
echo "Skipping build of gltf_viewer"
else
CXX=`which clang++` CC=`which clang` ./build.sh -f -W -X ${MESA_DIR} -p desktop debug gltf_viewer
fi
}
function end_render_() {
@@ -63,6 +68,14 @@ case $i in
TEST_FILTER="${i#*=}"
shift # past argument=value
;;
--no_rebuild)
NOREBUILD="true"
shift # past argument with no value
;;
--num_threads=*)
NUM_THREADS="${i#*=}"
shift # past argument=value
;;
*)
# unknown option
;;
@@ -77,5 +90,6 @@ start_render_ && \
--output_dir="${RENDER_OUTPUT_DIR}" \
--opengl_lib="${MESA_LIB_DIR}" \
--vk_icd="${MESA_VK_ICD_PATH}" \
${TEST_FILTER:+--test_filter="$TEST_FILTER"} && \
${TEST_FILTER:+--test_filter="$TEST_FILTER"} \
${NUM_THREADS:+--num_threads="$NUM_THREADS"} && \
end_render_
+2 -1
View File
@@ -35,5 +35,6 @@ bash `dirname $0`/generate.sh "$@" && \
--out=${DIFF_OUTPUT_DIR} \
--test="${RENDERDIFF_TEST_DIR}/tests/presubmit.json" "$@"
# $@ Pass arguments to compare.py, e.g. --test_filter
# $@ Pass arguments to generate.sh, e.g. --test_filter
end_
+6 -3
View File
@@ -83,7 +83,8 @@ def _render_test_config(gltf_viewer,
local_only=False,
opengl_lib=None,
vk_icd=None,
test_filter=None):
test_filter=None,
num_threads=None):
assert os.path.isdir(output_dir), f"output directory {output_dir} does not exist"
assert os.access(gltf_viewer, os.X_OK)
@@ -93,7 +94,7 @@ def _render_test_config(gltf_viewer,
gltf_viewer_abs = os.path.abspath(gltf_viewer)
results = []
with concurrent.futures.ThreadPoolExecutor() as executor:
with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = []
for test in test_config.tests:
test_json_path = os.path.abspath(
@@ -130,6 +131,7 @@ if __name__ == "__main__":
parser.add_argument('--opengl_lib', help='Path to the folder containing OpenGL driver lib (for LD_LIBRARY_PATH)')
parser.add_argument('--vk_icd', help='Path to VK ICD file')
parser.add_argument('--test_filter', help='Filter for the tests to run')
parser.add_argument('--num_threads', help='Number of threads to use for rendering', type=int)
args, _ = parser.parse_known_args(sys.argv[1:])
test = test_config.parse_from_path(args.test)
@@ -140,7 +142,8 @@ if __name__ == "__main__":
args.output_dir,
opengl_lib=args.opengl_lib,
vk_icd=args.vk_icd,
test_filter=args.test_filter)
test_filter=args.test_filter,
num_threads=args.num_threads)
with open(f'{output_dir}/render_results.json', 'w') as f:
f.write(json.dumps(results, indent=2))
+8 -4
View File
@@ -16,16 +16,20 @@ import { LitElement, html, css, repeat } from "https://cdn.jsdelivr.net/gh/lit/d
import './tools.js';
import './tiff-viewer.js';
const urlParams = new URLSearchParams(window.location.search);
const RUN_ID = urlParams.get('run_id');
const URL_PREFIX = RUN_ID ? `/artifacts/${RUN_ID}` : '';
function getGoldenUrl(testResult) {
return '/g/' + testResult.name;
return `${URL_PREFIX}/g/` + testResult.name;
}
function getCompUrl(testResult) {
return '/c/' + testResult.name;
return `${URL_PREFIX}/c/` + testResult.name;
}
function getDiffUrl(testResult) {
return '/d/' + testResult.diff;
return `${URL_PREFIX}/d/` + testResult.diff;
}
const DIFF_VIEW = 'diff'
@@ -570,7 +574,7 @@ class App extends LitElement {
};
async _init() {
const config = await ((await fetch("/r/")).json());
const config = await ((await fetch(`${URL_PREFIX}/r/`)).json());
config['results'] = config['results'].sort((a, b) => a.name < b.name ? -1 : (a.name > b.name ? 1 : 0));
this.tests = config['results']
}
+7 -1
View File
@@ -1,4 +1,10 @@
add_subdirectory(../ "${CMAKE_CURRENT_BINARY_DIR}/abseil")
option(FILAMENT_USE_EXTERNAL_ABSL "Use find_package to locate an external Abseil dependency" OFF)
if (FILAMENT_USE_EXTERNAL_ABSL)
find_package(absl CONFIG QUIET)
else()
add_subdirectory(../ "${CMAKE_CURRENT_BINARY_DIR}/abseil")
endif()
# To make Abseil easy for clients to link against, we create a dummy target just for installation
# purposes and combine all of Abseil's libraries into a single, easy to link against static library.
+4
View File
@@ -623,6 +623,10 @@ class_<Renderer>("Renderer")
}
engine->execute();
}), allow_raw_pointers())
.function("getUserTime", &Renderer::getUserTime)
.function("resetUserTime", &Renderer::resetUserTime)
.function("skipNextFrames", &Renderer::skipNextFrames)
.function("getFrameToSkipCount", &Renderer::getFrameToSkipCount)
.function("_setClearOptions", &Renderer::setClearOptions, allow_raw_pointers())
.function("getClearOptions", &Renderer::getClearOptions)
.function("setPresentationTime", &Renderer::setPresentationTime)