Compare commits

...

13 Commits

Author SHA1 Message Date
Run Yu
d105dc01b1 This is a test for pushing branch to github 2025-10-17 16:16:15 -04:00
Filament Bot
40d533ada1 [automated] Updating /docs due to commit b6df2b9
Full commit hash is b6df2b9b35

DOCS_ALLOW_DIRECT_EDITS
2025-10-17 06:20:51 +00:00
Powei Feng
b6df2b9b35 renderdiff: add transmission and ssao tests (#9232)
- Add transmission and ssao tests
 - Small mods to README.md
 - Add transmission models to gltf list
 - Change focal length to zoom in for more details

RDIFF_BRANCH=pf/renderdiff-add-transmission
2025-10-16 23:17:07 -07:00
Powei Feng
c379b31267 webgpu: fix picking (#9318)
Context:
 - Picking has switched to use integer textures and this texture
   is meant to have only RG components (#8917).
 - WebGPU only supports 4 component textures, and so we need to
   to a blit-based transform to turn 2 components into 4.
 - WebGPUBlitter supports only float textures

In this commit, we add support for integer textures to
WebGPUBlitter. In doing so, we refactor parts of the shader
construction code in the blitter class and introduce a couple
helper functions for texture formats.

This fixes picking (verifiable with gltf_viewer)

Co-authored-by: Ben Doherty <bendoherty@google.com>
2025-10-17 04:44:12 +00:00
Powei Feng
b03909c07a github: add hash id to commit message action (#9334)
We add the commit hash as output of get-commit-msg
2025-10-17 04:05:52 +00:00
Doris Wu
712c03b17b buffer update opt: Add uniform batching option for Material Instances (#9324) 2025-10-17 00:22:00 +00:00
Mathias Agopian
9dae0748d5 update perfetto sdk to 51.2 and add update script (#9335) 2025-10-16 16:00:25 -07:00
Powei Feng
fa02a7fa3b backend: add two command line arguments to backend test (#9329)
`--headless_only`
Headless-only mode indicates that we will only use headless
swapchains.  This is particularly useful if we want to test in
a CI (continuous integration) environment.

`--ci`
Having CONTINUOUS_INTEGRATION as an OS will allow us to have
CI-only temporary exceptions. Though it's expected that some
exceptions will be unfixable and will remain as workarounds.
2025-10-16 18:54:38 +00:00
Powei Feng
a068d3df79 github: fix get-commit-msg (#9330)
If we assign the message (which might contain quotes) to an
environment variable and then echo it, this should prevent the
problem of having a double quote (").

Also fix a problem in docs script for checking TAGs in commits.
2025-10-16 11:33:12 -07:00
Powei Feng
f64c087ffe backend: fix mapped buffer test for vk (#9328)
The descriptor set needs to be rebound when rendering two
separate render passes.
2025-10-16 06:26:51 +00:00
Doris Wu
9561137d53 buffer update opt: Change to use feature flag instead of engine flag (#9327)
* Revert "buffer update opt: Add a flag to guard the feature (#9322)"

This reverts commit 49c4a5d62c.

* Update feature flags

* feedback

* Update naming
2025-10-16 07:13:30 +08:00
Powei Feng
d0efda21ad webgpu: fix deadlock for readPixels (#9319)
To enable the readPixels callback to be called, we need to make
move to 'AllowSpontaneous' mode in order to not have to call
Instance::ProcessEvents() to force process the callback.
2025-10-15 11:11:05 -07:00
Mathias Agopian
a5b047f93d fix matc's -l option (#9321)
MaterialParser did the feature level check (-l) too early, before the
material was parsed, so -l0 would always fail.
2025-10-15 09:42:19 -07:00
40 changed files with 13748 additions and 2342 deletions

View File

@@ -1,43 +1,53 @@
# This action retrieves the latest commit message from a push or pull_request event
# and makes it available as an output variable named 'msg'.
name: 'Get commit message'
outputs:
msg:
value: ${{ steps.action_output.outputs.msg }}
hash:
value: ${{ steps.action_output.outputs.hash }}
runs:
using: "composite"
steps:
- name: Find commit message (on push)
if: github.event_name == 'push'
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
AUTHOR_NAME="${{ github.event.head_commit.author.name }}"
AUTHOR_EMAIL="${{ github.event.head_commit.author.email }}"
TSTAMP="${{ github.event.head_commit.timestamp }}"
echo "commit ${{ github.event.head_commit.id }}" >> /tmp/commit_msg.txt
HASH="${{ github.event.head_commit.id }}"
echo "commit $HASH" >> /tmp/commit_msg.txt
echo "Author: ${AUTHOR_NAME}<${AUTHOR_EMAIL}>" >> /tmp/commit_msg.txt
echo "Date: ${TSTAMP}" >> /tmp/commit_msg.txt
echo "" >> /tmp/commit_msg.txt
echo "${{ github.event.head_commit.message }}" >> /tmp/commit_msg.txt
echo "$COMMIT_MESSAGE" >> /tmp/commit_msg.txt
echo "$HASH" > /tmp/commit_hash.txt
- name: Find commit message (PR)
shell: bash
id: checkout_code
if: github.event_name == 'pull_request'
run: |
echo "+++++ head commit message +++++"
echo "$(git log -1 --no-merges)"
echo "+++++++++++++++++++++++++++++++"
echo "hash=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
git checkout ${{ github.event.pull_request.head.sha }}
echo "$(git log -1 --no-merges)" >> /tmp/commit_msg.txt
BEFORE_HASH=$(git rev-parse HEAD)
echo "hash=$BEFORE_HASH" >> "$GITHUB_OUTPUT"
# Next we will checkout the actual head (not the merge commits) of the PR
AFTER_HASH="${{ github.event.pull_request.head.sha }}"
git checkout $AFTER_HASH
COMMIT_MESSAGE=$(git log -1 --no-merges)
echo "$COMMIT_MESSAGE" > /tmp/commit_msg.txt
echo "$AFTER_HASH" > /tmp/commit_hash.txt
- shell: bash
id: action_output
run: |
# Get the commit message
DELIMITER="EOF_FILE_CONTENT_$(date +%s)" # Using timestamp to make it more unique
echo "msg<<$DELIMITER" >> "$GITHUB_OUTPUT"
cat /tmp/commit_msg.txt >> "$GITHUB_OUTPUT"
echo "$DELIMITER" >> "$GITHUB_OUTPUT"
echo "----- got commit message ---"
cat /tmp/commit_msg.txt
echo "----------------------------"
# Get the commit hash
echo "hash=$(cat /tmp/commit_hash.txt)" >> "$GITHUB_OUTPUT"
- name: Cleanup Find commit message (PR)
shell: bash
if: github.event_name == 'pull_request'

View File

@@ -23,7 +23,7 @@ jobs:
GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }}
run: |
GOLDEN_BRANCH=$(echo "${{ steps.get_commit_msg.outputs.msg }}" | python3 test/renderdiff/src/commit_msg.py)
COMMIT_HASH=$(echo "${{ steps.get_commit_msg.outputs.msg }}" | head -n 1 | sed "s/commit //g")
COMMIT_HASH="${{ steps.get_commit_msg.outputs.hash }}"
if [[ "${GOLDEN_BRANCH}" != "main" ]]; then
git config --global user.email "filament.bot@gmail.com"
git config --global user.name "Filament Bot"
@@ -51,7 +51,7 @@ jobs:
GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }}
run: |
bash docs_src/build/install_mdbook.sh && source ~/.bashrc
COMMIT_HASH=$(echo "${{ steps.get_commit_msg.outputs.msg }}" | head -n 1 | sed "s/commit //g")
COMMIT_HASH="${{ steps.get_commit_msg.outputs.hash }}"
git config --global user.email "filament.bot@gmail.com"
git config --global user.name "Filament Bot"
git config --global credential.helper cache

View File

@@ -108,8 +108,7 @@ jobs:
uses: ./.github/actions/get-commit-msg
- name: Check for manual edits to /docs
run: |
COMMIT_ID=$(echo "${{ steps.get_commit_msg.outputs.msg }}" | head -n 1 | sed "s/commit //g")
bash docs_src/build/presubmit_check.sh ${COMMIT_ID}
bash docs_src/build/presubmit_check.sh ${{ steps.get_commit_msg.outputs.hash }}
test-renderdiff:
name: test-renderdiff
@@ -121,7 +120,7 @@ jobs:
- uses: ./.github/actions/mac-prereq
- uses: ./.github/actions/get-gltf-assets
- uses: ./.github/actions/get-mesa
- uses: ./.github/actions/get-vulkan-sdk
- uses: ./.github/actions/get-vulkan-sdk
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- name: Prerequisites
@@ -132,12 +131,14 @@ jobs:
shell: bash
- name: Render and compare
id: render_compare
env:
COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }}
run: |
ls ./gltf/Models
TEST_DIR=test/renderdiff
source ${TEST_DIR}/src/preamble.sh
start_
GOLDEN_BRANCH=$(echo "${{ steps.get_commit_msg.outputs.msg }}" | python3 ${TEST_DIR}/src/commit_msg.py)
GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 ${TEST_DIR}/src/commit_msg.py)
bash ${TEST_DIR}/generate.sh && \
python3 ${TEST_DIR}/src/golden_manager.py \
--branch=${GOLDEN_BRANCH} \

View File

@@ -557,8 +557,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_Engine_nSetBu
jboolean disableHandleUseAfterFreeCheck,
jint preferredShaderLanguage,
jboolean forceGLES2Context, jboolean assertNativeWindowIsValid,
jint gpuContextPriority,
jboolean enableMaterialInstanceUniformBatching) {
jint gpuContextPriority) {
Engine::Builder* builder = (Engine::Builder*) nativeBuilder;
Engine::Config config = {
.commandBufferSizeMB = (uint32_t) commandBufferSizeMB,
@@ -577,7 +576,6 @@ extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_Engine_nSetBu
.forceGLES2Context = (bool) forceGLES2Context,
.assertNativeWindowIsValid = (bool) assertNativeWindowIsValid,
.gpuContextPriority = (Engine::GpuContextPriority) gpuContextPriority,
.enableMaterialInstanceUniformBatching = (bool) enableMaterialInstanceUniformBatching,
};
builder->config(&config);
}

View File

@@ -264,8 +264,7 @@ public class Engine {
config.disableHandleUseAfterFreeCheck,
config.preferredShaderLanguage.ordinal(),
config.forceGLES2Context, config.assertNativeWindowIsValid,
config.gpuContextPriority.ordinal(),
config.enableMaterialInstanceUniformBatching);
config.gpuContextPriority.ordinal());
return this;
}
@@ -526,15 +525,6 @@ public class Engine {
* GPU context priority level. Controls GPU work scheduling and preemption.
*/
public GpuContextPriority gpuContextPriority = GpuContextPriority.DEFAULT;
/**
* Enables uniform batching for all material instances.
*
* When enabled, material instances will share a common large uniform buffer
* and use sub-allocations within it. This is expected to reduce CPU overhead
* by minimizing the number of buffer updates sent to the driver.
*/
public boolean enableMaterialInstanceUniformBatching = false;
}
private Engine(long nativeEngine, Config config) {
@@ -1539,7 +1529,7 @@ public class Engine {
boolean disableHandleUseAfterFreeCheck,
int preferredShaderLanguage,
boolean forceGLES2Context, boolean assertNativeWindowIsValid,
int gpuContextPriority, boolean enableMaterialInstanceUniformBatching);
int gpuContextPriority);
private static native void nSetBuilderFeatureLevel(long nativeBuilder, int ordinal);
private static native void nSetBuilderSharedContext(long nativeBuilder, long sharedContext);
private static native void nSetBuilderPaused(long nativeBuilder, boolean paused);

View File

@@ -159,16 +159,16 @@
<div id="content" class="content">
<main>
<h1 id="rendering-difference-test"><a class="header" href="#rendering-difference-test">Rendering Difference Test</a></h1>
<p>We created a few scripts to run <code>gltf_viewer</code> and produce headless renderings.</p>
<p>This tool is a collections of scripts to run <code>gltf_viewer</code> and produce headless renderings.</p>
<p>This is mainly useful for continuous integration where GPUs are generally not available on cloud
machines. To perform software rasterization, these scripts are centered around <a href="https://docs.mesa3d.org">Mesa</a>'s
software rasterizers, but nothing bars us from using another rasterizer like <a href="https://github.com/google/swiftshader">SwiftShader</a>.
Additionally, we should be able to use GPUs where available (though this is more of a future
work).</p>
<p>The script <code>render.py</code> contains the core logic for taking input parameters (such as the test
description file) and then running gltf_viewer to produce the renderings.</p>
description file) and then running <code>gltf_viewer</code> to produce the renderings.</p>
<p>In the <code>test</code> directory is a list of test descriptions that are specified in json. Please see
<code>sample.json</code> to parse the structure.</p>
<code>sample.json</code> to glean the structure.</p>
<h2 id="setting-up-python"><a class="header" href="#setting-up-python">Setting up python</a></h2>
<p>The <code>renderdiff</code> project uses <code>python</code> extensively. To install the dependencies for producing
renderings, do the following step</p>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -77,11 +77,11 @@ def check_has_source_edits(commit_hash, printing=True):
# Returns true in a given TAG is found in the commit msg
def commit_msg_has_tag(commit_hash, tag, printing=True):
res, ret = execute(f'git log --pretty=%B {commit_hash}', cwd=ROOT_DIR)
res, ret = execute(f'git log -n1 --pretty=%B {commit_hash}', cwd=ROOT_DIR)
for l in ret.split('\n'):
if tag == l.strip():
if printing:
print(f'Found tag={tag} in commit message')
print(f'Found tag={tag} in commit={commit_hash}')
return True
return False

View File

@@ -84,128 +84,12 @@ constexpr uint32_t UNIFORM_BINDING_INDEX{ 1 };
constexpr uint32_t SAMPLER_BINDING_INDEX{ 2 };
constexpr std::string_view VERTEX_SHADER_ENTRY_POINT{ "vertexShaderMain" };
constexpr std::string_view FRAGMENT_SHADER_ENTRY_POINT{ "fragmentShaderMain" };
// note that the placeholders below must start and end with this prefix and suffix and should not
// otherwise be present in the template:
constexpr std::string_view PLACEHOLDER_PREFIX{ "{{" };
constexpr std::string_view PLACEHOLDER_SUFFIX{ "}}" };
// texture_2d<f32> or texture_multisampled_2d<f32> or texture_3d<f32> or
// texture_depth_2d or texture_depth_multisampled_2d
constexpr std::string_view TEXTURE_TYPE_PLACEHOLDER{ "TEXTURE_TYPE" };
// texture_2d<f32> or texture_depth_2d
[[maybe_unused]] constexpr std::string_view TEXTURE_2D_TYPE_PLACEHOLDER { "TEXTURE_2D_TYPE" };
// "" for no sampler, otherwise "@group(0) @binding(2) var sourceSampler: sampler;"
constexpr std::string_view SAMPLER_DECLARATION_PLACEHOLDER{ "SAMPLER_DECLARATION" };
constexpr std::string_view FRAGMENT_SHADER_SNIPPET_PLACEHOLDER{ "FRAGMENT_SHADER_SNIPPET" };
// @location(0) vec4<f32> or @builtin(frag_depth) f32
constexpr std::string_view FRAGMENT_RETURN_ATTRIBUTE_AND_TYPE_PLACEHOLDER{
"FRAGMENT_RETURN_ATTRIBUTE_AND_TYPE"
};
constexpr std::string_view SHADER_SOURCE_TEMPLATE{ R"(
struct BlitFragmentShaderArgs {
depthPlane: u32,
scale: vec2<f32>,
sourceOffset: vec2<u32>,
destinationOffset: vec2<u32>,
};
@group(0) @binding(0) var sourceTexture: {{TEXTURE_TYPE}};
@group(0) @binding(1) var<uniform> fragmentShaderArgs: BlitFragmentShaderArgs;
{{SAMPLER_DECLARATION}}
fn getUnnormalizedSourceTextureCoordinates(position: vec2<f32>) -> vec2<f32> {
// These coordinates match the Vulkan vkCmdBlitImage spec:
// https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkCmdBlitImage.html
let uvOffset: vec2<f32> = position - vec2<f32>(f32(fragmentShaderArgs.destinationOffset.x),f32(fragmentShaderArgs.destinationOffset.y));
let uvScaled: vec2<f32> = uvOffset * fragmentShaderArgs.scale;
return uvScaled + vec2<f32>(f32(fragmentShaderArgs.sourceOffset.x), f32(fragmentShaderArgs.sourceOffset.y));
}
fn normalize2dSourceTextureCoordinates(
unnormalizedSourceTextureCoordinates: vec2<f32>,
sourceDimensions: vec2<u32>) -> vec2<f32> {
return unnormalizedSourceTextureCoordinates /
vec2<f32>(f32(sourceDimensions.x), f32(sourceDimensions.y));
}
fn getNormalized2dSourceTextureCoordinates(
position: vec2<f32>,
sourceTexture: {{TEXTURE_2D_TYPE}}) -> vec2<f32> {
let sourceDimensions: vec2<u32> = textureDimensions(sourceTexture);
return normalize2dSourceTextureCoordinates(
getUnnormalizedSourceTextureCoordinates(position),
sourceDimensions
);
}
fn normalize3dSourceTextureCoordinates(
unnormalizedSourceTextureCoordinates: vec2<f32>,
sourceDimensions: vec3<u32>) -> vec3<f32> {
let uvNormalized: vec2<f32> = normalize2dSourceTextureCoordinates(
unnormalizedSourceTextureCoordinates,
sourceDimensions.xy
);
return vec3<f32>(
uvNormalized,
(f32(fragmentShaderArgs.depthPlane) + 0.5) / f32(sourceDimensions.z)
);
}
fn getNormalized3dSourceTextureCoordinates(
position: vec2<f32>,
sourceTexture: texture_3d<f32>) -> vec3<f32> {
let sourceDimensions: vec3<u32> = textureDimensions(sourceTexture);
return normalize3dSourceTextureCoordinates(
getUnnormalizedSourceTextureCoordinates(position),
sourceDimensions
);
}
@vertex
fn vertexShaderMain(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4<f32> {
let fullScreenTriangleVertices = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0)
);
return vec4<f32>(fullScreenTriangleVertices[vertexIndex].xy, 0.0, 1.0);
}
{{FRAGMENT_SHADER_SNIPPET}}
)" };
constexpr std::string_view FRAGMENT_SHADER_SNIPPET_MSAA_INPUT_TEMPLATE{ R"(
@fragment
fn fragmentShaderMain(@builtin(position) position: vec4<f32>) -> {{FRAGMENT_RETURN_ATTRIBUTE_AND_TYPE}} {
var color: vec4<f32> = vec4<f32>(0.0, 0.0, 0.0, 0.0);
let numberOfSamples: u32 = textureNumSamples(sourceTexture);
let coordinatesF: vec2<f32> = getUnnormalizedSourceTextureCoordinates(position.xy);
let coordinates: vec2<u32> = vec2<u32>(u32(coordinatesF.x), u32(coordinatesF.y));
for (var sampleIndex: u32 = 0; sampleIndex < numberOfSamples; sampleIndex++) {
color += textureLoad(sourceTexture, coordinates, sampleIndex);
}
color /= f32(numberOfSamples);
return color;
}
)" };
constexpr std::string_view FRAGMENT_SHADER_SNIPPET_3D_INPUT_TEMPLATE{ R"(
@fragment
fn fragmentShaderMain(@builtin(position) position: vec4<f32>) -> {{FRAGMENT_RETURN_ATTRIBUTE_AND_TYPE}} {
let coordinates: vec3<f32> = getNormalized3dSourceTextureCoordinates(position.xy, sourceTexture);
return textureSample(sourceTexture, sourceSampler, coordinates);
}
)" };
constexpr std::string_view FRAGMENT_SHADER_SNIPPET_2D_INPUT_TEMPLATE{ R"(
@fragment
fn fragmentShaderMain(@builtin(position) position: vec4<f32>) -> {{FRAGMENT_RETURN_ATTRIBUTE_AND_TYPE}} {
let coordinates: vec2<f32> = getNormalized2dSourceTextureCoordinates(position.xy, sourceTexture);
return textureSample(sourceTexture, sourceSampler, coordinates);
}
)" };
} // namespace
// Include the wgsl template sources
#include "WebGPUBlitter_wgsl.inc"
// It can perform a direct memory copy if the formats are compatible and no scaling or format
// conversion is needed. Otherwise, it uses a render pass with a custom shader to perform
// the blit. This allows for scaling, format conversion, and resolving multisampled textures.
@@ -395,11 +279,25 @@ 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()) };
wgpu::TextureSampleType srcSampleType = {};
if (depthSource) {
srcSampleType = wgpu::TextureSampleType::Depth;
} else if (multisampledSource) {
srcSampleType = wgpu::TextureSampleType::UnfilterableFloat;
} else if (isIntFormat(args.source.texture.GetFormat())) {
srcSampleType = wgpu::TextureSampleType::Sint;
} else if (isUIntFormat(args.source.texture.GetFormat())) {
srcSampleType = wgpu::TextureSampleType::Uint;
} else {
srcSampleType = wgpu::TextureSampleType::Float;
}
const PipelineLayoutKey pipelineLayoutKey{
.sourceDimension = sourceDimension,
.sourceSampleType = srcSampleType,
.filterType = args.filter,
.multisampledSource = multisampledSource,
.depthSource = depthSource,
};
const wgpu::BindGroupDescriptor textureBindGroupDescriptor{
.label = "blit_texture_bind_group",
@@ -454,10 +352,11 @@ void WebGPUBlitter::blit(wgpu::Queue const& queue, wgpu::CommandEncoder const& c
<< "Failed to create render pass encoder for blit.";
const RenderPipelineKey renderPipelineKey{
.sourceDimension = sourceDimension,
.sourceTextureFormat = args.source.texture.GetFormat(),
.sourceSampleType = srcSampleType,
.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);
@@ -557,9 +456,9 @@ wgpu::RenderPipeline WebGPUBlitter::createRenderPipeline(RenderPipelineKey const
};
const ShaderModuleKey shaderModuleKey{
.sourceDimension = key.sourceDimension,
.sourceTextureFormat = key.sourceTextureFormat,
.destinationTextureFormat = key.destinationTextureFormat,
.multisampledSource = key.sourceSampleCount > 1,
.depthSource = key.depthSource,
.depthDestination = hasDepth(key.destinationTextureFormat),
};
wgpu::ShaderModule const& shaderModule{ getOrCreateShaderModule(shaderModuleKey) };
const wgpu::FragmentState fragmentState{
@@ -572,9 +471,9 @@ wgpu::RenderPipeline WebGPUBlitter::createRenderPipeline(RenderPipelineKey const
};
const PipelineLayoutKey pipelineLayoutKey{
.sourceDimension = key.sourceDimension,
.sourceSampleType = key.sourceSampleType,
.filterType = key.filterType,
.multisampledSource = key.sourceSampleCount > 1,
.depthSource = key.depthSource,
};
const wgpu::RenderPipelineDescriptor pipelineDescriptor{
.label = "render_pass_blit_pipeline",
@@ -663,13 +562,7 @@ wgpu::BindGroupLayout WebGPUBlitter::createTextureBindGroupLayout(PipelineLayout
.binding = TEXTURE_BINDING_INDEX,
.visibility = wgpu::ShaderStage::Fragment,
.texture = {
.sampleType = key.depthSource
? wgpu::TextureSampleType::Depth
: (key.multisampledSource
? wgpu::TextureSampleType::UnfilterableFloat
: wgpu::TextureSampleType::Float), // only F32 scalar sample
// type supported for now
// (aside from depth)
.sampleType = key.sourceSampleType,
.viewDimension = key.sourceDimension,
.multisampled = key.multisampledSource,
},
@@ -695,7 +588,7 @@ wgpu::BindGroupLayout WebGPUBlitter::createTextureBindGroupLayout(PipelineLayout
};
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?
// No need for the last entry (sampler) if source is multisampled.
.entryCount = key.multisampledSource ? (MAX_TEXTURE_BIND_GROUP_ENTRY_SIZE - 1)
: (MAX_TEXTURE_BIND_GROUP_ENTRY_SIZE),
.entries = bindGroupLayoutEntries,
@@ -720,63 +613,109 @@ wgpu::ShaderModule const& WebGPUBlitter::getOrCreateShaderModule(ShaderModuleKey
// 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(ShaderModuleKey const& key) {
std::string_view textureType;
if (key.depthSource) {
if (key.multisampledSource) {
textureType = "texture_depth_multisampled_2d";
} else {
textureType = "texture_depth_2d";
}
bool const isDepthSrc = hasDepth(key.sourceTextureFormat);
bool const isDepthDst = hasDepth(key.destinationTextureFormat);
std::string_view vecDim;
if (key.sourceDimension == wgpu::TextureViewDimension::e3D) {
vecDim = "vec3";
} else {
if (key.multisampledSource) {
textureType = "texture_multisampled_2d<f32>";
} else {
if (key.sourceDimension == wgpu::TextureViewDimension::e3D) {
textureType = "texture_3d<f32>";
} else {
textureType = "texture_2d<f32>";
}
}
vecDim = "vec2";
}
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{
key.multisampledSource ? "" : "@group(0) @binding(2) var sourceSampler: sampler;"
};
const std::string_view fragmentReturnAttributeAndType{
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 (key.multisampledSource) {
fragmentShaderSnippet = webgpuutils::processPlaceholderTemplate(
FRAGMENT_SHADER_SNIPPET_MSAA_INPUT_TEMPLATE, PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX,
valueByPlaceholderNameForFragmentSnippet);
std::string_view srcPrimType;
std::string_view sampleImpl;
if (isIntFormat(key.sourceTextureFormat)) {
srcPrimType = "<i32>";
sampleImpl = INT_TEXTURE_SAMPLE_IMPL_TEMPLATE;
} else if (isUIntFormat(key.sourceTextureFormat)) {
srcPrimType = "<u32>";
sampleImpl = INT_TEXTURE_SAMPLE_IMPL_TEMPLATE;
} else {
if (key.sourceDimension == wgpu::TextureViewDimension::e3D) {
fragmentShaderSnippet = webgpuutils::processPlaceholderTemplate(
FRAGMENT_SHADER_SNIPPET_3D_INPUT_TEMPLATE, PLACEHOLDER_PREFIX,
PLACEHOLDER_SUFFIX, valueByPlaceholderNameForFragmentSnippet);
} else {
fragmentShaderSnippet = webgpuutils::processPlaceholderTemplate(
FRAGMENT_SHADER_SNIPPET_2D_INPUT_TEMPLATE, PLACEHOLDER_PREFIX,
PLACEHOLDER_SUFFIX, valueByPlaceholderNameForFragmentSnippet);
}
srcPrimType = "<f32>";
sampleImpl = FLOAT_TEXTURE_SAMPLE_IMPL_TEMPLATE;
}
const std::unordered_map<std::string_view, std::string_view> valueByPlaceholderNameForModule{
{ TEXTURE_TYPE_PLACEHOLDER, textureType },
{ TEXTURE_2D_TYPE_PLACEHOLDER, texture2dType },
{ SAMPLER_DECLARATION_PLACEHOLDER, samplerDeclaration },
{ FRAGMENT_SHADER_SNIPPET_PLACEHOLDER, fragmentShaderSnippet },
std::string_view fragmentTemplate;
std::string_view srcTextureType;
if (isDepthSrc && key.multisampledSource) {
srcTextureType = "texture_depth_multisampled_2d";
fragmentTemplate = FRAGMENT_SHADER_SNIPPET_2D_INPUT_TEMPLATE;
} else if (isDepthSrc && !key.multisampledSource) {
srcTextureType = "texture_depth_2d";
fragmentTemplate = FRAGMENT_SHADER_SNIPPET_2D_INPUT_TEMPLATE;
} else if (key.multisampledSource) {
srcTextureType = "texture_multisampled_2d{{SRC_PRIM_TYPE}}";
fragmentTemplate = FRAGMENT_SHADER_SNIPPET_MSAA_INPUT_TEMPLATE;
} else if (key.sourceDimension == wgpu::TextureViewDimension::e3D) {
srcTextureType = "texture_3d{{SRC_PRIM_TYPE}}";
fragmentTemplate = FRAGMENT_SHADER_SNIPPET_3D_INPUT_TEMPLATE;
} else {
srcTextureType = "texture_2d{{SRC_PRIM_TYPE}}";
fragmentTemplate = FRAGMENT_SHADER_SNIPPET_2D_INPUT_TEMPLATE;
}
std::string_view dstPrimType;
if (isIntFormat(key.destinationTextureFormat)) {
dstPrimType = "<i32>";
} else if (isUIntFormat(key.destinationTextureFormat)) {
dstPrimType = "<u32>";
} else {
dstPrimType = "<f32>";
}
std::string_view retAttribute;
std::string_view retType;
if (isDepthDst) {
retAttribute = "@builtin(frag_depth)";
retType = "f32";
} else {
retAttribute = "@location(0)";
retType = "vec4{{DST_PRIM_TYPE}}";
}
using ReplacementMap = std::unordered_map<std::string_view, std::string_view>;
auto const replace = [&](std::string_view src, ReplacementMap const& replacements) {
return webgpuutils::processPlaceholderTemplate(
src, PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, replacements);
};
const std::string shaderSource{ webgpuutils::processPlaceholderTemplate(SHADER_SOURCE_TEMPLATE,
PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, valueByPlaceholderNameForModule) };
// The dependency chain is shader module -> fragment source -> sampling function
// Hence we need to fill in the templates in reverse order.
// Fill in sampling implementation in the sampling func
std::string source = replace(TEXTURE_SAMPLE_IMPL_MAIN_TEMPLATE, {
{ TEXTURE_SAMPLE_IMPL, sampleImpl },
});
// Fill in sampling func in the fragment
source = replace(fragmentTemplate, {
{ TEXTURE_SAMPLE_IMPL_MAIN, source },
});
// Fill in fragment in the shader module, first pass
source = replace(SHADER_SOURCE_TEMPLATE, {
{ FRAGMENT_SHADER_SNIPPET, source },
});
// Fill in fragment in the shader module, second pass
source = replace(source, {
{ TEXTURE_TYPE, srcTextureType },
{ RET_ATTRIBUTE, retAttribute },
{ RET_TYPE, retType },
});
// Fill in fragment in the shader module, third pass
// TEXTURE_TYPE, RET_ATTRIBUTE, RET_TYPE have a dependency on SRC_PRIM_TYPE, DST_PRIM_TYPE,
// VECTOR_DIM so we need another pass to fill them in.
source = replace(source, {
{ VECTOR_DIM, vecDim },
{ SRC_PRIM_TYPE, srcPrimType },
{ DST_PRIM_TYPE, dstPrimType },
});
wgpu::ShaderModuleWGSLDescriptor wgslDescriptor{};
wgslDescriptor.code = shaderSource.data();
wgslDescriptor.code = source.data();
const wgpu::ShaderModuleDescriptor shaderModuleDescriptor{
.nextInChain = &wgslDescriptor,
.label = "render_pass_blit_shaders",

View File

@@ -97,11 +97,12 @@ private:
struct RenderPipelineKey {
wgpu::TextureViewDimension sourceDimension; // 4 bytes
wgpu::TextureFormat sourceTextureFormat; // 4
wgpu::TextureSampleType sourceSampleType; // 4
wgpu::TextureFormat destinationTextureFormat; // 4
uint8_t sourceSampleCount; // 1
SamplerMagFilter filterType; // 1
bool depthSource; // 1
uint8_t padding = 0; // 1
uint8_t padding[2] = {}; // 2
bool operator==(const RenderPipelineKey& other) const;
using Hasher = utils::hash::MurmurHashFn<RenderPipelineKey>;
@@ -109,10 +110,10 @@ private:
struct PipelineLayoutKey {
wgpu::TextureViewDimension sourceDimension; // 4 bytes
wgpu::TextureSampleType sourceSampleType; // 4
SamplerMagFilter filterType; // 1
bool multisampledSource; // 1
bool depthSource; // 1
uint8_t padding = 0; // 1
uint8_t padding[2] = {}; // 2
bool operator==(const PipelineLayoutKey& other) const;
using Hasher = utils::hash::MurmurHashFn<PipelineLayoutKey>;
@@ -120,20 +121,20 @@ private:
struct ShaderModuleKey {
wgpu::TextureViewDimension sourceDimension; // 4 bytes
wgpu::TextureFormat sourceTextureFormat; // 4
wgpu::TextureFormat destinationTextureFormat; // 4
bool multisampledSource; // 1
bool depthSource; // 1
bool depthDestination; // 1
uint8_t padding = 0; // 1
uint8_t padding0[3] = {}; // 3
bool operator==(const ShaderModuleKey& other) const;
using Hasher = utils::hash::MurmurHashFn<ShaderModuleKey>;
};
static_assert(sizeof(RenderPipelineKey) == 12,
static_assert(sizeof(RenderPipelineKey) == 20,
"RenderPipelineKey must not have implicit padding.");
static_assert(sizeof(PipelineLayoutKey) == 8,
static_assert(sizeof(PipelineLayoutKey) == 12,
"PipelineLayoutKey must not have implicit padding.");
static_assert(sizeof(ShaderModuleKey) == 8, "ShaderModuleKey must not have implicit padding.");
static_assert(sizeof(ShaderModuleKey) == 16, "ShaderModuleKey must not have implicit padding.");
wgpu::Device mDevice;
wgpu::Sampler mNearestSampler{ nullptr };

View File

@@ -0,0 +1,174 @@
/*
* 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.
*/
/*
* NOTE: This file is meant to be included with #include in WebGPUBlitter.cpp
* It is separated out for clarity.
*/
namespace {
// note that the placeholders below must start and end with this prefix and suffix and should not
// otherwise be present in the template:
constexpr std::string_view PLACEHOLDER_PREFIX{ "{{" };
constexpr std::string_view PLACEHOLDER_SUFFIX{ "}}" };
#define PLACEHOLDER_DEF(name) constexpr std::string_view name = #name
PLACEHOLDER_DEF(FRAGMENT_SHADER_SNIPPET);
PLACEHOLDER_DEF(TEXTURE_SAMPLE_IMPL);
PLACEHOLDER_DEF(TEXTURE_SAMPLE_IMPL_MAIN);
// texture_multisampled_2d<f32>
// texture_2d<f32/i32/u32>
// texture_3d<f32/i32/u32>
// texture_depth_2d
// texture_depth_multisampled_2d
PLACEHOLDER_DEF(TEXTURE_TYPE);
// vec2, vec3
PLACEHOLDER_DEF(VECTOR_DIM);
// vec_<f32>, vec_<u32>, vec_<i32>, f32
PLACEHOLDER_DEF(RET_TYPE);
// @builtin(frag_depth), @location(0)
PLACEHOLDER_DEF(RET_ATTRIBUTE);
// <f32>, <u32>, <i32>
PLACEHOLDER_DEF(DST_PRIM_TYPE);
PLACEHOLDER_DEF(SRC_PRIM_TYPE);
#undef PLACEHOLDER_DEF
constexpr std::string_view SHADER_SOURCE_TEMPLATE{ R"(
struct BlitFragmentShaderArgs {
depthPlane: u32,
scale: vec2<f32>,
sourceOffset: vec2<u32>,
destinationOffset: vec2<u32>,
};
@group(0) @binding(0) var sourceTexture: {{TEXTURE_TYPE}};
@group(0) @binding(1) var<uniform> fragmentShaderArgs: BlitFragmentShaderArgs;
fn getUnnormalizedSourceTextureCoordinates(position: vec2<f32>) -> vec2<f32> {
// These coordinates match the Vulkan vkCmdBlitImage spec:
// https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkCmdBlitImage.html
let uvOffset: vec2<f32> = position - vec2<f32>(f32(fragmentShaderArgs.destinationOffset.x),f32(fragmentShaderArgs.destinationOffset.y));
let uvScaled: vec2<f32> = uvOffset * fragmentShaderArgs.scale;
return uvScaled + vec2<f32>(f32(fragmentShaderArgs.sourceOffset.x), f32(fragmentShaderArgs.sourceOffset.y));
}
fn normalize2dSourceTextureCoordinates(
unnormalizedSourceTextureCoordinates: vec2<f32>,
sourceDimensions: vec2<u32>) -> vec2<f32> {
return unnormalizedSourceTextureCoordinates /
vec2<f32>(f32(sourceDimensions.x), f32(sourceDimensions.y));
}
@vertex
fn vertexShaderMain(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4<f32> {
let fullScreenTriangleVertices = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0)
);
return vec4<f32>(fullScreenTriangleVertices[vertexIndex].xy, 0.0, 1.0);
}
{{FRAGMENT_SHADER_SNIPPET}}
)"
};
constexpr std::string_view FLOAT_TEXTURE_SAMPLE_IMPL_TEMPLATE{ R"(
return textureSample(sourceTexture, sourceSampler, coordinates);
)" };
constexpr std::string_view INT_TEXTURE_SAMPLE_IMPL_TEMPLATE { R"(
let texelCoords = {{VECTOR_DIM}}<u32>(coordinates * {{VECTOR_DIM}}<f32>(sourceDimensions));
return textureLoad(sourceTexture, texelCoords, 0);
)"};
constexpr std::string_view TEXTURE_SAMPLE_IMPL_MAIN_TEMPLATE { R"(
// Assumes that "@group(0) @binding(2) var sourceSampler: sampler;" has been declared in the main program;
fn sampleTextureImpl(sourceTexture: {{TEXTURE_TYPE}}, sourceDimensions: {{VECTOR_DIM}}<u32>,
coordinates: {{VECTOR_DIM}}<f32>) -> vec4{{DST_PRIM_TYPE}} {
{{TEXTURE_SAMPLE_IMPL}}
}
)"};
constexpr std::string_view FRAGMENT_SHADER_SNIPPET_MSAA_INPUT_TEMPLATE{ R"(
@fragment
fn fragmentShaderMain(@builtin(position) position: vec4<f32>) -> {{RET_ATTRIBUTE}} {{RET_TYPE}} {
var color: vec4<f32> = vec4<f32>(0.0, 0.0, 0.0, 0.0);
let numberOfSamples: u32 = textureNumSamples(sourceTexture);
let coordinatesF: vec2<f32> = getUnnormalizedSourceTextureCoordinates(position.xy);
let coordinates: vec2<u32> = vec2<u32>(u32(coordinatesF.x), u32(coordinatesF.y));
for (var sampleIndex: u32 = 0; sampleIndex < numberOfSamples; sampleIndex++) {
color += textureLoad(sourceTexture, coordinates, sampleIndex);
}
color /= f32(numberOfSamples);
return color;
}
)" };
constexpr std::string_view FRAGMENT_SHADER_SNIPPET_3D_INPUT_TEMPLATE{ R"(
@group(0) @binding(2) var sourceSampler: sampler;
{{TEXTURE_SAMPLE_IMPL_MAIN}}
fn normalize3dSourceTextureCoordinates(
unnormalizedSourceTextureCoordinates: vec2<f32>,
sourceDimensions: vec3<u32>) -> vec3<f32> {
let uvNormalized: vec2<f32> = normalize2dSourceTextureCoordinates(
unnormalizedSourceTextureCoordinates,
sourceDimensions.xy
);
return vec3<f32>(
uvNormalized,
(f32(fragmentShaderArgs.depthPlane) + 0.5) / f32(sourceDimensions.z)
);
}
@fragment
fn fragmentShaderMain(@builtin(position) position: vec4<f32>) -> {{RET_ATTRIBUTE}} {{RET_TYPE}} {
let sourceDimensions: vec3<u32> = textureDimensions(sourceTexture);
let coordinates: vec3<f32> = normalize3dSourceTextureCoordinates(
getUnnormalizedSourceTextureCoordinates(position),
sourceDimensions
);
return sampleTextureImpl(sourceTexture, sourceDimensions, coordinates);
}
)" };
constexpr std::string_view FRAGMENT_SHADER_SNIPPET_2D_INPUT_TEMPLATE { R"(
@group(0) @binding(2) var sourceSampler: sampler;
{{TEXTURE_SAMPLE_IMPL_MAIN}}
@fragment
fn fragmentShaderMain(@builtin(position) position: vec4<f32>) -> {{RET_ATTRIBUTE}} {{RET_TYPE}} {
let sourceDimensions: vec2<u32> = textureDimensions(sourceTexture);
let coordinates: vec2<f32> = normalize2dSourceTextureCoordinates(
getUnnormalizedSourceTextureCoordinates(position.xy),
sourceDimensions
);
return sampleTextureImpl(sourceTexture, sourceDimensions, coordinates);
}
)" };
} // namespace

View File

@@ -446,6 +446,7 @@ void WebGPUDriver::createSwapChainHeadlessR(Handle<HwSwapChain> sch, uint32_t wi
void WebGPUDriver::createVertexBufferInfoR(Handle<HwVertexBufferInfo> vertexBufferInfoHandle,
const uint8_t bufferCount, const uint8_t attributeCount, const AttributeArray attributes,
utils::ImmutableCString&& tag) {
// Hello world! This is a test for pushing branch to Github
FWGPU_SYSTRACE_SCOPE();
constructHandle<WebGPUVertexBufferInfo>(vertexBufferInfoHandle, bufferCount, attributeCount,
attributes, mDeviceLimits);
@@ -1497,7 +1498,7 @@ void WebGPUDriver::readPixels(Handle<HwRenderTarget> sourceRenderTargetHandle, c
mReadPixelMapsCounter.startTask();
userData->buffer.MapAsync(
wgpu::MapMode::Read, 0, bufferSize, wgpu::CallbackMode::AllowProcessEvents,
wgpu::MapMode::Read, 0, bufferSize, wgpu::CallbackMode::AllowSpontaneous,
[](wgpu::MapAsyncStatus status, const char* message, UserData* userdata) {
std::unique_ptr<UserData> data(static_cast<UserData*>(userdata));
if (UTILS_LIKELY(status == wgpu::MapAsyncStatus::Success)) {

View File

@@ -41,6 +41,45 @@ namespace filament::backend {
textureFormat == wgpu::TextureFormat::Depth32FloatStencil8;
}
[[nodiscard]] constexpr bool isUIntFormat(const wgpu::TextureFormat format) {
// see https://www.w3.org/TR/webgpu/#texture-formats
// and https://www.w3.org/TR/webgpu/#texture-format-caps
switch (format) {
case wgpu::TextureFormat::R8Uint:
case wgpu::TextureFormat::R16Uint:
case wgpu::TextureFormat::RG8Uint:
case wgpu::TextureFormat::R32Uint:
case wgpu::TextureFormat::RG16Uint:
case wgpu::TextureFormat::RGBA8Uint:
case wgpu::TextureFormat::RGB10A2Uint:
case wgpu::TextureFormat::RG32Uint:
case wgpu::TextureFormat::RGBA16Uint:
case wgpu::TextureFormat::RGBA32Uint:
return true;
default:
return false;
}
}
[[nodiscard]] constexpr bool isIntFormat(const wgpu::TextureFormat format) {
// see https://www.w3.org/TR/webgpu/#texture-formats
// and https://www.w3.org/TR/webgpu/#texture-format-caps
switch (format) {
case wgpu::TextureFormat::R8Sint:
case wgpu::TextureFormat::R16Sint:
case wgpu::TextureFormat::RG8Sint:
case wgpu::TextureFormat::R32Sint:
case wgpu::TextureFormat::RG16Sint:
case wgpu::TextureFormat::RGBA8Sint:
case wgpu::TextureFormat::RG32Sint:
case wgpu::TextureFormat::RGBA16Sint:
case wgpu::TextureFormat::RGBA32Sint:
return true;
default:
return false;
}
}
[[nodiscard]] constexpr std::string_view toString(const PixelDataFormat format) {
switch (format) {
case PixelDataFormat::R: return "R";

View File

@@ -52,15 +52,14 @@ std::string processPlaceholderTemplate(std::string_view const& stringTemplate,
"Malformed source with missing suffix to placeholder");
const std::string_view placeholderName{ std::string_view(sourceData + positionOfPlaceholder,
positionAfterPlaceholder - positionOfPlaceholder) };
if (const auto iter{ valueByPlaceholderName.find(placeholderName) };
iter == valueByPlaceholderName.end()) {
// wrapping placeholderName in a std::string to null terminate the C string....
PANIC_POSTCONDITION("Found placeholder '%s' in template, but this is not present in "
"valueByPlaceholderName",
std::string{ placeholderName }.data());
} else {
iter != valueByPlaceholderName.end()) {
const std::string_view value{ iter->second };
out << value;
} else {
// It's ok to not replace a template item. We assume replacement can take multiple passes.
out << placeholderPrefix << placeholderName << placeholderSuffix;
}
// update the cursor for after the placeholder...
positionCursorInTemplateString = positionAfterPlaceholder + placeholderSuffix.size();

View File

@@ -23,13 +23,17 @@
namespace test {
Backend parseArgumentsForBackend(int argc, char* argv[]) {
Backend backend = Backend::OPENGL;
TestArguments parseArguments(int argc, char* argv[]) {
TestArguments arguments = {};
arguments.backend = Backend::OPENGL;
// The first colon in OPTSTR turns on silent error reporting. This is important, as the
// arguments may also contain gtest parameters we don't know about.
static constexpr const char* OPTSTR = ":a:";
static constexpr const char* OPTSTR = ":a:kc";
static const struct option OPTIONS[] = {
{ "api", required_argument, nullptr, 'a' },
{ "headless_only", no_argument, nullptr, 'k' },
{ "ci", no_argument, nullptr, 'c' },
{ nullptr, 0, nullptr, 0 } // termination of the option list
};
@@ -41,13 +45,13 @@ Backend parseArgumentsForBackend(int argc, char* argv[]) {
switch (opt) {
case 'a':
if (arg == "opengl") {
backend = Backend::OPENGL;
arguments.backend = Backend::OPENGL;
} else if (arg == "vulkan") {
backend = Backend::VULKAN;
arguments.backend = Backend::VULKAN;
} else if (arg == "metal") {
backend = Backend::METAL;
arguments.backend = Backend::METAL;
} else if (arg == "webgpu") {
backend = Backend::WEBGPU;
arguments.backend = Backend::WEBGPU;
} else {
std::cerr << "Unrecognized target API. Must be 'opengl'|'vulkan'|'metal'|'webgpu'."
<< std::endl
@@ -55,10 +59,16 @@ Backend parseArgumentsForBackend(int argc, char* argv[]) {
<< std::endl;
}
break;
case 'k':
arguments.headlessOnly = true;
break;
case 'c':
arguments.isContinuousIntegration = true;
break;
}
}
return backend;
return arguments;
}
} // namespace test

View File

@@ -41,6 +41,7 @@ enum class OperatingSystem: uint8_t {
LINUX = 2,
// Also represents iOS phones.
APPLE = 3,
CONTINUOUS_INTEGRATION = 4,
// TODO: When tests support windows add it here.
};
@@ -73,11 +74,17 @@ void initTests(Backend backend, OperatingSystem operatingSystem, bool isMobile,
*/
int runTests();
struct TestArguments {
Backend backend;
bool headlessOnly = false;
bool isContinuousIntegration = false;
};
/**
* A utility method that can be invoked by test runners to parse arguments.
* Looks through the provided command-line arguments and finds any -a <backend> arguments.
*/
Backend parseArgumentsForBackend(int argc, char* argv[]);
TestArguments parseArguments(int argc, char* argv[]);
} // namespace test

View File

@@ -43,7 +43,10 @@ std::array<test::Backend, 2> const VALID_BACKENDS{
}// namespace
int main(int argc, char* argv[]) {
auto backend = test::parseArgumentsForBackend(argc, argv);
const auto arguments = test::parseArguments(argc, argv);
const auto backend = arguments.backend;
// Note that Linux is headless-only.
if (!std::any_of(VALID_BACKENDS.begin(), VALID_BACKENDS.end(),
[backend](test::Backend validBackend) { return backend == validBackend; })) {
@@ -51,6 +54,8 @@ int main(int argc, char* argv[]) {
return 1;
}
test::initTests(backend, test::OperatingSystem::LINUX, false, argc, argv);
const auto operatingSystem = arguments.isContinuousIntegration ?
test::OperatingSystem::CONTINUOUS_INTEGRATION : test::OperatingSystem::LINUX;
test::initTests(backend, operatingSystem, false, argc, argv);
return test::runTests();
}

View File

@@ -32,29 +32,34 @@ test::NativeView getNativeView() {
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property test::Backend backend;
@property bool headlessOnly;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSView* view = [self createView];
if (self.backend == test::Backend::OPENGL) {
nativeView.ptr = (void*) view;
if (self.headlessOnly) {
nativeView.ptr = nullptr;
nativeView.width = test::WINDOW_WIDTH;
nativeView.height = test::WINDOW_HEIGHT;
} else {
NSView* view = [self createView];
switch (self.backend) {
case test::Backend::OPENGL:
nativeView.ptr = (void*) view;
break;
case test::Backend::METAL:
case test::Backend::VULKAN:
case test::Backend::WEBGPU:
case test::Backend::NOOP:
nativeView.ptr = (void*) view.layer;
break;
}
CGSize drawableSize = ((CAMetalLayer*) view.layer).drawableSize;
nativeView.width = static_cast<size_t>(drawableSize.width);
nativeView.height = static_cast<size_t>(drawableSize.height);
}
if (self.backend == test::Backend::METAL) {
nativeView.ptr = (void*) view.layer;
}
if (self.backend == test::Backend::VULKAN) {
nativeView.ptr = (void*) view.layer;
}
if (self.backend == test::Backend::WEBGPU) {
nativeView.ptr = (void*) view.layer;
}
CGSize drawableSize = ((CAMetalLayer*) view.layer).drawableSize;
nativeView.width = static_cast<size_t>(drawableSize.width);
nativeView.height = static_cast<size_t>(drawableSize.height);
exit(test::runTests());
}
@@ -100,11 +105,25 @@ test::NativeView getNativeView() {
@end
int main(int argc, char* argv[]) {
auto backend = test::parseArgumentsForBackend(argc, argv);
test::initTests(backend, test::OperatingSystem::APPLE, false, argc, argv);
AppDelegate* delegate = [AppDelegate new];
delegate.backend = backend;
const auto arguments = test::parseArguments(argc, argv);
const auto operatingSystem = arguments.isContinuousIntegration ?
test::OperatingSystem::CONTINUOUS_INTEGRATION : test::OperatingSystem::APPLE;
test::initTests(arguments.backend, operatingSystem, false, argc, argv);
NSApplication* app = [NSApplication sharedApplication];
AppDelegate* delegate = [AppDelegate new];
delegate.backend = arguments.backend;
delegate.headlessOnly = arguments.headlessOnly;
[app setDelegate:delegate];
if (arguments.headlessOnly) {
// In headless mode, we don't want to start the NSApplication event loop.
// Instead, we can manually "finish" launching the app, which will trigger the tests to run.
[app finishLaunching];
[delegate applicationDidFinishLaunching:nil];
// The line above calls exit(), so we should not reach here.
return 0;
}
[app run];
}

View File

@@ -158,7 +158,7 @@ TEST_F(BlitTest, ColorMagnify) {
constexpr int kNumLevels = 3;
// Create a SwapChain and make it current. We don't really use it so the res doesn't matter.
auto swapChain = mCleanup.add(api.createSwapChainHeadless(256, 256, 0));
auto swapChain = mCleanup.add(createSwapChain());
api.makeCurrent(swapChain, swapChain);
// Create a source texture.
@@ -222,7 +222,7 @@ TEST_F(BlitTest, ColorMinify) {
constexpr int kNumLevels = 3;
// Create a SwapChain and make it current. We don't really use it so the res doesn't matter.
auto swapChain = mCleanup.add(api.createSwapChainHeadless(256, 256, 0));
auto swapChain = mCleanup.add(createSwapChain());
api.makeCurrent(swapChain, swapChain);
// Create a source texture.
@@ -371,7 +371,7 @@ TEST_F(BlitTest, Blit2DTextureArray) {
constexpr int kDstTexLayer = 0;
// Create a SwapChain and make it current. We don't really use it so the res doesn't matter.
auto swapChain = mCleanup.add(api.createSwapChainHeadless(256, 256, 0));
auto swapChain = mCleanup.add(createSwapChain());
api.makeCurrent(swapChain, swapChain);
// Create a source texture.
@@ -437,7 +437,7 @@ TEST_F(BlitTest, BlitRegion) {
constexpr int kDstLevel = 0;
// Create a SwapChain and make it current. We don't really use it so the res doesn't matter.
auto swapChain = mCleanup.add(api.createSwapChainHeadless(256, 256, 0));
auto swapChain = mCleanup.add(createSwapChain());
api.makeCurrent(swapChain, swapChain);
// Create a source texture.

View File

@@ -103,7 +103,7 @@ TEST_F(BackendTest, FrameCompletedCallback) {
Cleanup cleanup(api);
// Create a SwapChain.
auto swapChain = cleanup.add(api.createSwapChainHeadless(256, 256, 0));
auto swapChain = cleanup.add(createSwapChain());
int callbackCountA = 0;
api.setFrameCompletedCallback(swapChain, nullptr,

View File

@@ -22,6 +22,7 @@
#include "SharedShaders.h"
#include "SharedShadersConstants.h"
#include "Skip.h"
#include "Workarounds.h"
#include <backend/BufferDescriptor.h>
#include <backend/DriverEnums.h>
@@ -55,7 +56,7 @@ protected:
auto colorTexture = cleanup.add(
api.createTexture(SamplerType::SAMPLER_2D, 1, TextureFormat::RGBA8, 1,
screenWidth(), screenHeight(), 1,
TextureUsage::COLOR_ATTACHMENT | TextureUsage::SAMPLEABLE));
TextureUsage::COLOR_ATTACHMENT | TextureUsage::SAMPLEABLE TEXTURE_USAGE_READ_PIXELS));
auto renderTarget = cleanup.add(api.createRenderTarget(TargetBufferFlags::COLOR0,
screenWidth(), screenHeight(), 1, 1, { { colorTexture } }, {}, {}));
return renderTarget;
@@ -90,19 +91,27 @@ protected:
return { vbih, vbh, ibh };
}
Shader getSimpleShader(Cleanup& cleanup, math::float4 const& color) {
std::pair<Shader, DescriptorSetHandle> getSimpleShader(Cleanup& cleanup,
math::float4 const& color) {
auto& api = getDriverApi();
Shader const shader = SharedShaders::makeShader(getDriverApi(), cleanup, ShaderRequest{
.mVertexType = VertexShaderType::Simple,
.mFragmentType = FragmentShaderType::SolidColored,
.mUniformType = ShaderUniformType::Simple
});
Shader const shader = SharedShaders::makeShader(getDriverApi(), cleanup,
ShaderRequest{
.mVertexType = VertexShaderType::Simple,
.mFragmentType = FragmentShaderType::SolidColored,
.mUniformType = ShaderUniformType::Simple,
});
auto descSet = shader.createDescriptorSet(api);
UniformBindingConfig uboBindingConfig = {
.descriptorSet = descSet,
};
auto const ubuffer = cleanup.add(api.createBufferObject(sizeof(SimpleMaterialParams),
BufferObjectBinding::UNIFORM, BufferUsage::DYNAMIC_BIT));
shader.bindUniform<SimpleMaterialParams>(api, ubuffer);
// This will bind the UBO to descSet and also bind descSet. But we also need to manually
// bind descset every begin/end frame.
shader.bindUniform<SimpleMaterialParams>(api, ubuffer, uboBindingConfig);
shader.uploadUniform(api, ubuffer, SimpleMaterialParams{ .color = color });
return shader;
return { shader, descSet };
}
void copyData(const void* data, size_t const size, size_t const offset,
@@ -122,7 +131,9 @@ protected:
int64_t const frame,
SwapChainHandle swapChain,
RenderTargetHandle renderTarget,
RenderPrimitiveHandle renderPrimitive, PipelineState const& state) {
RenderPrimitiveHandle renderPrimitive,
DescriptorSetHandle descSet,
PipelineState const& state) {
auto& api = getDriverApi();
RenderPassParams params = getClearColorRenderPass({0,0,0,1});
params.viewport = getFullViewport();
@@ -130,6 +141,8 @@ protected:
api.beginFrame(frame, 0, 0);
api.beginRenderPass(renderTarget, params);
api.bindPipeline(state);
// The binding index is always 0 for the simple shaders we're using.
api.bindDescriptorSet(descSet, 0, {});
api.bindRenderPrimitive(renderPrimitive);
api.draw2(0, count, 1);
api.endRenderPass();
@@ -185,7 +198,7 @@ protected:
flushAndWait();
EXPECT_EQ(callbackExecuted, 1);
Shader const shader = getSimpleShader(cleanup, color);
auto [shader, descset] = getSimpleShader(cleanup, color);
auto [vbih, vbh, ibh] = setupGeometryBuffer(cleanup, 3, stride, baseVertex, bufferObject);
@@ -197,7 +210,7 @@ protected:
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = vbih;
render(3, 0, swapChain, renderTarget, renderPrimitive, state);
render(3, 0, swapChain, renderTarget, renderPrimitive, descset, state);
EXPECT_IMAGE(renderTarget, ScreenshotParams(screenWidth(), screenHeight(), screenshotName, 0));
}
@@ -288,7 +301,7 @@ TEST_F(MemoryMappedTest, MultipleCopies) {
flushAndWait();
EXPECT_EQ(callbacksExecuted, 3);
Shader const shader = getSimpleShader(cleanup, { 1, 1, 0, 1 });
auto [shader, descset] = getSimpleShader(cleanup, { 1, 1, 0, 1 });
auto [vbih, vbh, ibh] = setupGeometryBuffer(cleanup, 9, sizeof(math::float2), 0, bufferObject);
@@ -300,7 +313,7 @@ TEST_F(MemoryMappedTest, MultipleCopies) {
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = vbih;
render(9, 0, swapChain, renderTarget, renderPrimitive, state);
render(9, 0, swapChain, renderTarget, renderPrimitive, descset, state);
EXPECT_IMAGE(renderTarget, ScreenshotParams(screenWidth(), screenHeight(), "MultipleCopies", 0));
}
@@ -345,7 +358,7 @@ TEST_F(MemoryMappedTest, UpdatePartial) {
}
Shader const shader = getSimpleShader(cleanup, { 1, 1, 0, 1 });
auto [shader, descset] = getSimpleShader(cleanup, { 1, 1, 0, 1 });
auto [vbih, vbh, ibh] = setupGeometryBuffer(cleanup, 9, sizeof(math::float2), 0, bufferObject);
@@ -357,7 +370,7 @@ TEST_F(MemoryMappedTest, UpdatePartial) {
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = vbih;
render(9, 0, swapChain, renderTarget, renderPrimitive, state);
render(9, 0, swapChain, renderTarget, renderPrimitive, descset, state);
EXPECT_IMAGE(renderTarget, ScreenshotParams(screenWidth(), screenHeight(), "UpdatePartial_before", 0));
@@ -380,7 +393,7 @@ TEST_F(MemoryMappedTest, UpdatePartial) {
api.makeCurrent(swapChain, swapChain);
// Second render, after update
render(9, 1, swapChain, renderTarget, renderPrimitive, state);
render(9, 1, swapChain, renderTarget, renderPrimitive, descset, state);
EXPECT_IMAGE(renderTarget, ScreenshotParams(screenWidth(), screenHeight(), "UpdatePartial_after", 0));
}

View File

@@ -385,8 +385,7 @@ TEST_F(ReadPixelsTest, ReadPixelsPerformance) {
Cleanup cleanup(api);
// Create a platform-specific SwapChain and make it current.
auto swapChain = cleanup.add(
api.createSwapChainHeadless(renderTargetSize, renderTargetSize, 0));
auto swapChain = cleanup.add(createSwapChain());
api.makeCurrent(swapChain, swapChain);
Shader shader = SharedShaders::makeShader(api, cleanup, ShaderRequest{

View File

@@ -70,7 +70,7 @@ TEST_F(BackendTest, ScissorViewportRegion) {
// executeCommands().
{
// Create a SwapChain and make it current. We don't really use it so the res doesn't matter.
auto swapChain = cleanup.add(api.createSwapChainHeadless(256, 256, 0));
auto swapChain = cleanup.add(createSwapChain());
api.makeCurrent(swapChain, swapChain);
Shader shader = SharedShaders::makeShader(api, cleanup,
@@ -149,7 +149,7 @@ TEST_F(BackendTest, ScissorViewportEdgeCases) {
// executeCommands().
{
// Create a SwapChain and make it current. We don't really use it so the res doesn't matter.
auto swapChain = cleanup.add(api.createSwapChainHeadless(256, 256, 0));
auto swapChain = cleanup.add(createSwapChain());
api.makeCurrent(swapChain, swapChain);
Shader shader = SharedShaders::makeShader(api, cleanup, ShaderRequest{

View File

@@ -33,8 +33,7 @@ TEST_F(BackendTest, TestTemplate) {
auto& api = getDriverApi();
Cleanup cleanup(api);
auto swapChain =
cleanup.add(api.createSwapChainHeadless(kRenderTargetSize, kRenderTargetSize, 0));
auto swapChain = cleanup.add(createSwapChain());
api.makeCurrent(swapChain, swapChain);
RenderTargetHandle renderTarget = cleanup.add(api.createDefaultRenderTarget());

View File

@@ -417,15 +417,6 @@ public:
* GPU context priority level. Controls GPU work scheduling and preemption.
*/
GpuContextPriority gpuContextPriority = GpuContextPriority::DEFAULT;
/**
* Enables uniform batching for all material instances.
*
* When enabled, material instances will share a common large uniform buffer
* and use sub-allocations within it. This is expected to reduce CPU overhead
* by minimizing the number of buffer updates sent to the driver.
*/
bool enableMaterialInstanceUniformBatching = false;
};

View File

@@ -776,6 +776,7 @@ public:
} backend;
struct {
bool check_crc32_after_loading = false;
bool enable_material_instance_uniform_batching = false;
} material;
} features;
@@ -825,6 +826,9 @@ public:
{ "material.check_crc32_after_loading",
"Verify the checksum of package data when a material is loaded.",
&features.material.check_crc32_after_loading, false },
{ "material.enable_material_instance_uniform_batching",
"Make all MaterialInstances share a common large uniform buffer and use sub-allocations within it.",
&features.material.enable_material_instance_uniform_batching, false },
}};
utils::Slice<const FeatureFlag> getFeatureFlags() const noexcept {

View File

@@ -60,8 +60,13 @@ namespace filament {
using namespace backend;
FMaterialInstance::FMaterialInstance(FEngine& engine, FMaterial const* material,
const char* name) noexcept
: mMaterial(material),
const char* name) noexcept
: FMaterialInstance(engine, material, name,
engine.features.material.enable_material_instance_uniform_batching) {
}
FMaterialInstance::FMaterialInstance(FEngine& engine, FMaterial const* material,
const char* name, bool useUboBatching) noexcept : mMaterial(material),
mDescriptorSet("MaterialInstance", material->getDescriptorSetLayout()),
mCulling(CullingMode::BACK),
mShadowCulling(CullingMode::BACK),
@@ -71,6 +76,7 @@ FMaterialInstance::FMaterialInstance(FEngine& engine, FMaterial const* material,
mHasScissor(false),
mIsDoubleSided(false),
mIsDefaultInstance(false),
mUseUboBatching(useUboBatching),
mTransparencyMode(TransparencyMode::DEFAULT),
mName(name ? CString(name) : material->getName()) {
@@ -80,12 +86,16 @@ FMaterialInstance::FMaterialInstance(FEngine& engine, FMaterial const* material,
// expected by the per-material descriptor-set layout
size_t const uboSize = std::max(size_t(16), material->getUniformInterfaceBlock().getSize());
mUniforms = UniformBuffer(uboSize);
mUbHandle = driver.createBufferObject(mUniforms.getSize(), BufferObjectBinding::UNIFORM,
BufferUsage::STATIC, utils::ImmutableCString{ material->getName().c_str_safe() });
// set the UBO, always descriptor 0
mDescriptorSet.setBuffer(material->getDescriptorSetLayout(),
0, mUbHandle, 0, mUniforms.getSize());
if (mUseUboBatching) {
mUboData = BufferAllocator::UNALLOCATED;
} else {
mUboData = driver.createBufferObject(mUniforms.getSize(), BufferObjectBinding::UNIFORM,
BufferUsage::STATIC, ImmutableCString{ material->getName().c_str_safe() });
// set the UBO, always descriptor 0
mDescriptorSet.setBuffer(material->getDescriptorSetLayout(),
0, std::get<Handle<HwBufferObject>>(mUboData), 0, mUniforms.getSize());
}
const RasterState& rasterState = material->getRasterState();
// At the moment, only MaterialInstances have a stencil state, but in the future it should be
@@ -140,6 +150,7 @@ FMaterialInstance::FMaterialInstance(FEngine& engine,
mHasScissor(false),
mIsDoubleSided(other->mIsDoubleSided),
mIsDefaultInstance(false),
mUseUboBatching(other->mUseUboBatching),
mScissorRect(other->mScissorRect),
mName(name ? CString(name) : other->mName) {
@@ -147,12 +158,16 @@ FMaterialInstance::FMaterialInstance(FEngine& engine,
FMaterial const* const material = other->getMaterial();
mUniforms.setUniforms(other->getUniformBuffer());
mUbHandle = driver.createBufferObject(mUniforms.getSize(), BufferObjectBinding::UNIFORM,
BufferUsage::DYNAMIC, ImmutableCString{ material->getName().c_str_safe() });
// set the UBO, always descriptor 0
mDescriptorSet.setBuffer(mMaterial->getDescriptorSetLayout(),
0, mUbHandle, 0, mUniforms.getSize());
if (mUseUboBatching) {
mUboData = BufferAllocator::UNALLOCATED;
} else {
mUboData = driver.createBufferObject(mUniforms.getSize(), BufferObjectBinding::UNIFORM,
BufferUsage::DYNAMIC, ImmutableCString{ material->getName().c_str_safe() });
// set the UBO, always descriptor 0
mDescriptorSet.setBuffer(material->getDescriptorSetLayout(),
0, std::get<Handle<HwBufferObject>>(mUboData), 0, mUniforms.getSize());
}
if (material->hasDoubleSidedCapability()) {
setDoubleSided(mIsDoubleSided);
@@ -173,7 +188,7 @@ FMaterialInstance::FMaterialInstance(FEngine& engine,
material->getId(), material->generateMaterialInstanceId());
// If the original descriptor set has been commited, the copy needs to commit as well.
if (other->mDescriptorSet.getHandle()) {
if (!mUseUboBatching && other->mDescriptorSet.getHandle()) {
mDescriptorSet.commitSlow(mMaterial->getDescriptorSetLayout(), driver);
}
}
@@ -190,13 +205,16 @@ FMaterialInstance::~FMaterialInstance() noexcept = default;
void FMaterialInstance::terminate(FEngine& engine) {
FEngine::DriverApi& driver = engine.getDriverApi();
mDescriptorSet.terminate(driver);
driver.destroyBufferObject(mUbHandle);
auto* ubHandle = std::get_if<Handle<HwBufferObject>>(&mUboData);
if (ubHandle){
driver.destroyBufferObject(*ubHandle);
}
}
void FMaterialInstance::commitStreamUniformAssociations(FEngine::DriverApi& driver) {
mHasStreamUniformAssociations = false;
if (!mTextureParameters.empty()) {
backend::BufferObjectStreamDescriptor descriptor;
BufferObjectStreamDescriptor descriptor;
for (auto const& [binding, p]: mTextureParameters) {
ssize_t offset = mMaterial->getUniformInterfaceBlock().getTransformFieldOffset(binding);
if (offset >= 0) {
@@ -206,7 +224,12 @@ void FMaterialInstance::commitStreamUniformAssociations(FEngine::DriverApi& driv
}
}
if (descriptor.mStreams.size() > 0) {
driver.registerBufferObjectStreams(mUbHandle, std::move(descriptor));
// UBO batching is incompatible with stream uniform associations because streams require a
// dedicated UBO handle, not a sub-allocation. Assert that any instance here uses its own UBO.
assert_invariant(!mUseUboBatching);
auto const* ubHandle = std::get_if<Handle<HwBufferObject>>(&mUboData);
assert_invariant(ubHandle);
driver.registerBufferObjectStreams(*ubHandle, std::move(descriptor));
}
}
}
@@ -217,11 +240,17 @@ void FMaterialInstance::commit(FEngine& engine) const {
}
}
void FMaterialInstance::commit(DriverApi& driver) const {
// update uniforms if needed
void FMaterialInstance::commit(FEngine::DriverApi& driver) const {
if (mUniforms.isDirty() || mHasStreamUniformAssociations) {
mUniforms.clean();
driver.updateBufferObject(mUbHandle, mUniforms.toBufferDescriptor(driver), 0);
if (mUseUboBatching) {
// TODO: update the content by `copyToMemoryMappedBuffer`
}
else {
auto* ubHandle = std::get_if<Handle<HwBufferObject>>(&mUboData);
assert_invariant(ubHandle != nullptr);
driver.updateBufferObject(*ubHandle, mUniforms.toBufferDescriptor(driver), 0);
}
}
if (!mTextureParameters.empty()) {
for (auto const& [binding, p]: mTextureParameters) {
@@ -411,6 +440,21 @@ void FMaterialInstance::use(FEngine::DriverApi& driver, Variant variant) const {
mDescriptorSet.bind(driver, DescriptorSetBindingPoints::PER_MATERIAL);
}
void FMaterialInstance::assignUboAllocation(
const Handle<HwBufferObject>& ubHandle,
BufferAllocator::AllocationId id,
BufferAllocator::allocation_size_t offset) {
assert_invariant(mUseUboBatching);
mUboData = id;
mDescriptorSet.setBuffer(mMaterial->getDescriptorSetLayout(), 0, ubHandle, offset,
mUniforms.getSize());
}
BufferAllocator::AllocationId FMaterialInstance::getAllocationId() const noexcept {
auto const* allocationId = std::get_if<BufferAllocator::AllocationId>(&mUboData);
return allocationId ? *allocationId : BufferAllocator::UNALLOCATED;
}
void FMaterialInstance::fixMissingSamplers() const {
// Here we check that all declared sampler parameters are set, this is required by
// Vulkan and Metal; GL is more permissive. If a sampler parameter is not set, we will

View File

@@ -23,6 +23,7 @@
#include "ds/DescriptorSet.h"
#include "details/BufferAllocator.h"
#include "details/Engine.h"
#include "private/backend/DriverApi.h"
@@ -57,6 +58,9 @@ class FMaterialInstance : public MaterialInstance {
public:
FMaterialInstance(FEngine& engine, FMaterial const* material,
const char* name) noexcept;
// Use this constructor when you need to override the ubo batching flag for an individual MI.
FMaterialInstance(FEngine& engine, FMaterial const* material,
const char* name, bool useUboBatching) noexcept;
FMaterialInstance(FEngine& engine, FMaterialInstance const* other, const char* name);
FMaterialInstance(const FMaterialInstance& rhs) = delete;
FMaterialInstance& operator=(const FMaterialInstance& rhs) = delete;
@@ -75,6 +79,12 @@ public:
void use(FEngine::DriverApi& driver, Variant variant = {}) const;
void assignUboAllocation(const backend::Handle<backend::HwBufferObject>& ubHandle,
BufferAllocator::AllocationId id,
BufferAllocator::allocation_size_t offset);
BufferAllocator::AllocationId getAllocationId() const noexcept;
FMaterial const* getMaterial() const noexcept { return mMaterial; }
uint64_t getSortingKey() const noexcept { return mMaterialSortingKey; }
@@ -234,6 +244,8 @@ public:
return mIsDefaultInstance;
}
bool isUsingUboBatching() const noexcept { return mUseUboBatching; }
// Called by the engine to ensure that unset samplers are initialized with placedholders.
void fixMissingSamplers() const;
@@ -274,7 +286,7 @@ private:
backend::SamplerParams params;
};
backend::Handle<backend::HwBufferObject> mUbHandle;
std::variant<BufferAllocator::AllocationId, backend::Handle<backend::HwBufferObject>> mUboData;
tsl::robin_map<backend::descriptor_binding_t, TextureParameter> mTextureParameters;
mutable DescriptorSet mDescriptorSet;
UniformBuffer mUniforms;
@@ -296,6 +308,7 @@ private:
bool mHasScissor : 1;
bool mIsDoubleSided : 1;
bool mIsDefaultInstance : 1;
bool mUseUboBatching : 1;
TransparencyMode mTransparencyMode : 2;
uint64_t mMaterialSortingKey = 0;

View File

@@ -459,13 +459,6 @@ utils::Status MaterialParser::processTemplateSubstitutions(
utils::Status MaterialParser::parse(filamat::MaterialBuilder& builder,
const Config& config, ssize_t& size, std::unique_ptr<const char[]>& buffer) {
if (builder.getFeatureLevel() > config.getFeatureLevel()) {
utils::io::sstream errorMessage;
errorMessage << "Material feature level (" << +builder.getFeatureLevel()
<< ") is higher than maximum allowed (" << +config.getFeatureLevel() << ")";
return utils::Status::invalidArgument(errorMessage.c_str());
}
// Before attempting an expensive lex, let's find out if we were sent pure JSON.
utils::Status parsedStatus;
if (utils::Status validJson = isValidJsonStart(buffer.get(), size_t(size));
@@ -479,6 +472,13 @@ utils::Status MaterialParser::parse(filamat::MaterialBuilder& builder,
return parsedStatus;
}
if (builder.getFeatureLevel() > config.getFeatureLevel()) {
utils::io::sstream errorMessage;
errorMessage << "Material feature level (" << +builder.getFeatureLevel()
<< ") is higher than maximum allowed (" << +config.getFeatureLevel() << ")";
return utils::Status::invalidArgument(errorMessage.c_str());
}
switch (config.getReflectionTarget()) {
case Config::Metadata::NONE:
break;

View File

@@ -1,6 +1,6 @@
# Rendering Difference Test
We created a few scripts to run `gltf_viewer` and produce headless renderings.
This tool is a collections of scripts to run `gltf_viewer` and produce headless renderings.
This is mainly useful for continuous integration where GPUs are generally not available on cloud
machines. To perform software rasterization, these scripts are centered around [Mesa]'s
@@ -9,10 +9,10 @@ Additionally, we should be able to use GPUs where available (though this is more
work).
The script `render.py` contains the core logic for taking input parameters (such as the test
description file) and then running gltf_viewer to produce the renderings.
description file) and then running `gltf_viewer` to produce the renderings.
In the `test` directory is a list of test descriptions that are specified in json. Please see
`sample.json` to parse the structure.
`sample.json` to glean the structure.
## Setting up python

View File

@@ -229,7 +229,7 @@ if __name__ == '__main__':
important_print(f'Successfully compared {success_count} / {len(results)} images')
if tolerance_used_details:
pstr = 'Tolerance-based passes:'
pstr = 'Passed:'
for detail in tolerance_used_details:
pstr += '\n' + detail
important_print(pstr)
@@ -237,7 +237,7 @@ if __name__ == '__main__':
if failed_details:
pstr = 'Failed:'
for detail in failed_details:
pstr = '\n' + detail
pstr += '\n' + detail
important_print(pstr)
if len(failed) > 0:
exit(1)

View File

@@ -74,7 +74,7 @@ def _get_deletes_updates(update_dir, golden_dir):
for fpath in base.intersection(new):
base_fpath = os.path.join(golden_dir, fpath)
new_fpath = os.path.join(update_dir, fpath)
if (ext == 'tif' and not same_image(new_fpath, base_fpath)) or \
if (ext == 'tif' and not same_image(new_fpath, base_fpath)[0]) or \
(ext == 'json' and _file_as_str(new_fpath) != _file_as_str(base_fpath)):
update.append(fpath)

View File

@@ -1,12 +1,13 @@
AlphaBlendModeTest
AttenuationTest
Box
BoomBoxWithAxes
Box
BoxInterleaved
BoxTextured
BoxTexturedNonPowerOfTwo
Duck
IridescenceSuzanne
IridescentDishWithOlives
Lantern
MetalRoughSpheres
NegativeScaleTest
@@ -17,4 +18,5 @@ Sponza
Suzanne
TextureCoordinateTest
TextureSettingsTest
TransmissionRoughnessTest
TwoSidedPlane

View File

@@ -5,9 +5,9 @@
"presets": [
{
"name": "base",
"models": ["Box", "BoxTextured", "Duck", "lucy", "FlightHelmet"],
"models": ["Box", "BoxTextured", "FlightHelmet", "lucy"],
"rendering": {
"viewer.cameraFocusDistance": 0,
"viewer.cameraFocalLength": 35.0,
"view.postProcessingEnabled": true,
"view.dithering": "NONE"
},
@@ -17,27 +17,54 @@
}
},
{
"name": "helmet_only",
"name": "bloom_models",
"models": ["DamagedHelmet"],
"rendering": {}
},
{
"name": "ssao_models",
"models": ["Duck", "lucy"],
"rendering": {}
},
{
"name": "transmission_models",
"models": ["TransmissionRoughnessTest", "IridescentDishWithOlives"],
"rendering": {}
}
],
"tests": [
{
"name": "Bloom",
"description": "Testing bloom",
"apply_presets": ["base", "helmet_only"],
"description": "Bloom",
"apply_presets": ["base", "bloom_models"],
"rendering": {
"view.bloom.enabled": true
}
},
{
"name": "MSAA",
"description": "Testing multisampling anti-aliasing",
"description": "Multisampling anti-aliasing",
"apply_presets": ["base"],
"rendering": {
"view.msaa.enabled": true
}
},
{
"name": "Transimssion",
"description": "transmission",
"apply_presets": ["base", "transmission_models"],
"rendering": {
"viewer.cameraFocalLength": 52.0,
"view.screenSpaceReflections.enabled": true
}
},
{
"name": "SSAO",
"description": "Screen space ambient occulsion",
"apply_presets": ["base", "ssao_models"],
"rendering": {
"view.ssao.enabled": true
}
}
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

12
third_party/perfetto/tnt/README.md vendored Normal file
View File

@@ -0,0 +1,12 @@
To update the Perfetto library, run the following command from the tnt directory:
```shell
./update_perfetto.sh <version_tag>
```
For example:
```shell
./update_perfetto.sh v51.2
```
**Useful links:**
https://perfetto.dev/
https://perfetto.dev/docs/instrumentation/tracing-sdk

View File

@@ -1,8 +0,0 @@
Useful links:
https://perfetto.dev/
https://perfetto.dev/docs/instrumentation/tracing-sdk
Perfetto was fetched as:
git clone https://github.com/google/perfetto.git -b v50.1
Only the sdk/ directory is needed.

86
third_party/perfetto/tnt/update_perfetto.sh vendored Executable file
View File

@@ -0,0 +1,86 @@
#!/bin/bash
set -euo pipefail
function usage() {
echo "Usage: $0 [-h] <version_tag>"
echo " -h: Display this help message"
echo " <version_tag>: The Perfetto version to download (e.g., v50.1)"
exit 1
}
if [[ $# -ne 1 || "$1" == "-h" ]]; then
usage
fi
DOWNLOAD_REF=$1
DOWNLOAD_URL="https://github.com/google/perfetto/archive/refs/tags/${DOWNLOAD_REF}.zip"
ZIP_FILE_NAME="${DOWNLOAD_REF}.zip"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
THIRD_PARTY_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
if ! command -v curl &> /dev/null; then
echo "Error: curl is not installed." >&2
exit 1
fi
if ! command -v unzip &> /dev/null; then
echo "Error: unzip is not installed." >&2
exit 1
fi
if ! command -v rsync &> /dev/null; then
echo "Error: rsync is not installed." >&2
exit 1
fi
cd "${THIRD_PARTY_DIR}"
echo "Downloading Perfetto ${DOWNLOAD_REF}..."
if ! curl -f -L -o "${ZIP_FILE_NAME}" "${DOWNLOAD_URL}"; then
echo "Error: Failed to download Perfetto ${DOWNLOAD_REF}." >&2
echo "Please check the version number and your network connection." >&2
rm -f "${ZIP_FILE_NAME}"
exit 1
fi
echo "Unzipping..."
TEMP_DIR="perfetto_temp_unzip"
# Clean up temp dir from previous runs, if any.
rm -rf "${TEMP_DIR}"
mkdir "${TEMP_DIR}"
if ! unzip -q "${ZIP_FILE_NAME}" -d "${TEMP_DIR}"; then
echo "Error: Failed to unzip the downloaded file." >&2
rm -f "${ZIP_FILE_NAME}"
rm -rf "${TEMP_DIR}"
exit 1
fi
# The archive contains a single directory. Find it.
EXTRACTED_DIR=$(find "${TEMP_DIR}" -mindepth 1 -maxdepth 1 -type d)
if [ -z "${EXTRACTED_DIR}" ] || [ ! -d "${EXTRACTED_DIR}" ]; then
echo "Error: Could not find the extracted directory inside the archive." >&2
rm -f "${ZIP_FILE_NAME}"
rm -rf "${TEMP_DIR}"
exit 1
fi
echo "Replacing existing perfetto contents with new version..."
# If perfetto_new exists, remove it.
rm -rf perfetto_new
mv "${EXTRACTED_DIR}" perfetto_new
# The perfetto directory should contain the content of the sdk directory from the archive, and our tnt directory.
rsync -a --delete perfetto_new/sdk/ perfetto/perfetto/
echo "Cleaning up..."
rm -rf "${ZIP_FILE_NAME}" perfetto_new "${TEMP_DIR}"
echo "Staging changes..."
git add perfetto
echo "Done. Please commit the changes."