Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f039b917a | ||
|
|
b7dea28cc5 | ||
|
|
9aa8e7780b | ||
|
|
271e639abe | ||
|
|
082a79eebc | ||
|
|
7a10c09954 | ||
|
|
3139a37cc7 | ||
|
|
f8e8c27c04 | ||
|
|
c79d695ffb | ||
|
|
ad6f6cf149 | ||
|
|
8506e94f10 | ||
|
|
5db8e0b9ab | ||
|
|
da55574b32 | ||
|
|
736fed00b3 | ||
|
|
9b200fc33a | ||
|
|
fa436f1d12 | ||
|
|
837b2715a0 | ||
|
|
1a04312986 | ||
|
|
a89711b006 | ||
|
|
6d061b5d01 | ||
|
|
53ddb3dd1c | ||
|
|
c7b0a7f441 | ||
|
|
d7b44a2585 |
+1
-1
@@ -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,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).
|
||||
|
||||
@@ -31,7 +31,7 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.android.filament:filament-android:1.66.0'
|
||||
implementation 'com.google.android.filament:filament-android:1.66.1'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -51,7 +51,7 @@ Here are all the libraries available in the group `com.google.android.filament`:
|
||||
iOS projects can use CocoaPods to install the latest release:
|
||||
|
||||
```shell
|
||||
pod 'Filament', '~> 1.66.0'
|
||||
pod 'Filament', '~> 1.66.1'
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
GROUP=com.google.android.filament
|
||||
VERSION_NAME=1.66.0
|
||||
VERSION_NAME=1.66.1
|
||||
|
||||
POM_DESCRIPTION=Real-time physically based rendering engine for Android.
|
||||
|
||||
|
||||
@@ -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
@@ -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.65.4'
|
||||
}
|
||||
</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', '~> 1.65.3'
|
||||
<pre><code class="language-shell">pod 'Filament', '~> 1.65.4'
|
||||
</code></pre>
|
||||
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
|
||||
<ul>
|
||||
|
||||
@@ -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=<filter></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=<number></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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(©Info, 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(©Info, 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(©Info, dataBuff, dataSize, &layout, &extent);
|
||||
mDevice.GetQueue().WriteTexture(©Info, 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, ©Size);
|
||||
mCommandBuffer = mCommandEncoder.Finish();
|
||||
assert_invariant(mCommandBuffer);
|
||||
mCommandEncoder = nullptr;
|
||||
mQueue.Submit(1, &mCommandBuffer);
|
||||
mCommandBuffer = nullptr;
|
||||
commandEncoder.CopyTextureToBuffer(&source, &destination, ©Size);
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "Filament"
|
||||
spec.version = "1.66.0"
|
||||
spec.version = "1.66.1"
|
||||
spec.license = { :type => "Apache 2.0", :file => "LICENSE" }
|
||||
spec.homepage = "https://google.github.io/filament"
|
||||
spec.authors = "Google LLC."
|
||||
spec.summary = "Filament is a real-time physically based rendering engine for Android, iOS, Windows, Linux, macOS, and WASM/WebGL."
|
||||
spec.platform = :ios, "11.0"
|
||||
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.66.0/filament-v1.66.0-ios.tgz" }
|
||||
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.66.1/filament-v1.66.1-ios.tgz" }
|
||||
|
||||
# Fix linking error with Xcode 12; we do not yet support the simulator on Apple silicon.
|
||||
spec.pod_target_xcconfig = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include <private/filament/SamplerInterfaceBlock.h>
|
||||
|
||||
#include "ShaderMinifier.h"
|
||||
#include "SpirvRemapWrapper.h"
|
||||
|
||||
#include <spirv-tools/optimizer.hpp>
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
<< "}";
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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_
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "filament",
|
||||
"version": "1.66.0",
|
||||
"version": "1.66.1",
|
||||
"description": "Real-time physically based rendering engine",
|
||||
"main": "filament.js",
|
||||
"module": "filament.js",
|
||||
|
||||
Reference in New Issue
Block a user