Compare commits

..
Author SHA1 Message Date
Kevin Granade c5e64564a1 Use the backend test cleanup helper more extensively
BUGS=[408310509]
2025-08-15 12:44:25 -07:00
Juan Caldas 6b2804985b Modify Load Image (#9068) 2025-08-15 15:29:21 +00:00
Juan Caldas 320ee183c6 webgpu: Gemini provided/inspired documentation
# Conflicts:
#	filament/backend/src/webgpu/WebGPUDriver.cpp
2025-08-15 10:10:04 -05:00
Juan Caldas 8145a5dd25 webgpu: Perform Blit when textures dont match (#9105)
BUGS = [435200998]
2025-08-15 14:41:13 +00:00
Powei Feng ba0793ac18 Revert "[WIP] renderdiff: enable vulkan"
This reverts commit 2c154be0b3.
2025-08-14 16:19:46 -07:00
Powei Feng 2c154be0b3 [WIP] renderdiff: enable vulkan 2025-08-14 16:19:17 -07:00
51 changed files with 972 additions and 828 deletions
+168 -180
View File
@@ -9,6 +9,36 @@ on:
- main
jobs:
build-desktop-mac:
name: build-mac
runs-on: macos-14-xlarge
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/mac-prereq
- name: Run build script
run: |
cd build/mac && printf "y" | ./build.sh presubmit
- name: Test material parser
run: |
out/cmake-release/filament/test/test_material_parser
build-desktop-linux:
name: build-linux
runs-on: ubuntu-22.04-16core
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- name: Run build script
run: |
cd build/linux && printf "y" | ./build.sh presubmit
- name: Test material parser
run: |
out/cmake-release/filament/test/test_material_parser
build-windows:
name: build-windows
runs-on: windows-2022-32core
@@ -18,194 +48,152 @@ jobs:
fetch-depth: 0
- name: Run build script
run: |
build\windows\build-github.bat continuous
build\windows\build-github.bat presubmit
shell: cmd
# build-desktop-mac:
# name: build-mac
# runs-on: macos-14-xlarge
# steps:
# - uses: actions/checkout@v4.1.6
# with:
# fetch-depth: 0
# - uses: ./.github/actions/mac-prereq
# - name: Run build script
# run: |
# cd build/mac && printf "y" | ./build.sh presubmit
# - name: Test material parser
# run: |
# out/cmake-release/filament/test/test_material_parser
build-android:
name: build-android
runs-on: 'ubuntu-24.04-16core'
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Run build script
# Only build 1 64 bit target during presubmit to cut down build times during presubmit
# Continuous builds will build everything
run: |
cd build/android && printf "y" | ./build.sh presubmit arm64-v8a
# build-desktop-linux:
# name: build-linux
# runs-on: ubuntu-22.04-16core
# steps:
# - uses: actions/checkout@v4.1.6
# with:
# fetch-depth: 0
# - uses: ./.github/actions/linux-prereq
# - name: Run build script
# run: |
# cd build/linux && printf "y" | ./build.sh presubmit
# - name: Test material parser
# run: |
# out/cmake-release/filament/test/test_material_parser
build-ios:
name: build-iOS
runs-on: macos-14-xlarge
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/mac-prereq
- name: Run build script
run: |
cd build/ios && printf "y" | ./build.sh presubmit
- name: Build iOS samples
run: |
cd build/ios && ./build-samples.sh presubmit
# build-windows:
# name: build-windows
# runs-on: windows-2022-32core
# steps:
# - uses: actions/checkout@v4.1.6
# with:
# fetch-depth: 0
# - name: Run build script
# run: |
# build\windows\build-github.bat presubmit
# shell: cmd
build-web:
name: build-web
runs-on: 'ubuntu-24.04-16core'
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- uses: ./.github/actions/web-prereq
- name: Run build script
run: |
cd build/web && printf "y" | ./build.sh presubmit
# build-android:
# name: build-android
# runs-on: 'ubuntu-24.04-16core'
# steps:
# - uses: actions/checkout@v4.1.6
# with:
# fetch-depth: 0
# - uses: ./.github/actions/linux-prereq
# - uses: actions/setup-java@v3
# with:
# distribution: 'temurin'
# java-version: '17'
# - name: Run build script
# # Only build 1 64 bit target during presubmit to cut down build times during presubmit
# # Continuous builds will build everything
# run: |
# cd build/android && printf "y" | ./build.sh presubmit arm64-v8a
validate-docs:
name: validate-docs
runs-on: 'ubuntu-24.04-4core'
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- id: get_commit_msg
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}
# build-ios:
# name: build-iOS
# runs-on: macos-14-xlarge
# steps:
# - uses: actions/checkout@v4.1.6
# with:
# fetch-depth: 0
# - uses: ./.github/actions/mac-prereq
# - name: Run build script
# run: |
# cd build/ios && printf "y" | ./build.sh presubmit
# - name: Build iOS samples
# run: |
# cd build/ios && ./build-samples.sh presubmit
test-renderdiff:
name: test-renderdiff
runs-on: macos-14-xlarge
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- uses: ./.github/actions/mac-prereq
- uses: ./.github/actions/get-mesa
- name: Prerequisites
run: |
pip install tifffile numpy
shell: bash
- name: Render and compare
id: render_compare
run: |
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)
bash ${TEST_DIR}/generate.sh && \
python3 ${TEST_DIR}/src/golden_manager.py \
--branch=${GOLDEN_BRANCH} \
--output=${GOLDEN_OUTPUT_DIR}
# build-web:
# name: build-web
# runs-on: 'ubuntu-24.04-16core'
# steps:
# - uses: actions/checkout@v4.1.6
# with:
# fetch-depth: 0
# - uses: ./.github/actions/linux-prereq
# - uses: ./.github/actions/web-prereq
# - name: Run build script
# run: |
# cd build/web && printf "y" | ./build.sh presubmit
# Note that we need to upload the output even if comparison fails, so we undo `set -ex`
end_
# validate-docs:
# name: validate-docs
# runs-on: 'ubuntu-24.04-4core'
# steps:
# - uses: actions/checkout@v4.1.6
# with:
# fetch-depth: 0
# - id: get_commit_msg
# 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}
python3 ${TEST_DIR}/src/compare.py \
--src=${GOLDEN_OUTPUT_DIR} \
--dest=${RENDER_OUTPUT_DIR} \
--out=${DIFF_OUTPUT_DIR} 2>&1 | tee compare_output.txt
# test-renderdiff:
# name: test-renderdiff
# runs-on: macos-14-xlarge
# steps:
# - uses: actions/checkout@v4.1.6
# with:
# fetch-depth: 0
# - id: get_commit_msg
# uses: ./.github/actions/get-commit-msg
# - uses: ./.github/actions/mac-prereq
# - uses: ./.github/actions/get-mesa
# - name: Prerequisites
# run: |
# pip install tifffile numpy
# shell: bash
# - name: Render and compare
# id: render_compare
# run: |
# 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)
# bash ${TEST_DIR}/generate.sh && \
# python3 ${TEST_DIR}/src/golden_manager.py \
# --branch=${GOLDEN_BRANCH} \
# --output=${GOLDEN_OUTPUT_DIR}
if grep "Failed" compare_output.txt > /dev/null; then
DELIMITER="EOF_FILE_CONTENT_$(date +%s)" # Using timestamp to make it more unique
echo "err<<$DELIMITER" >> "$GITHUB_OUTPUT"
cat compare_output.txt >> "$GITHUB_OUTPUT"
echo "$DELIMITER" >> "$GITHUB_OUTPUT"
fi
- uses: actions/upload-artifact@v4
with:
name: presubmit-renderdiff-result
path: ./out/renderdiff
- name: Compare result
run: |
ERROR_STR="${{ steps.render_compare.outputs.err }}"
if [ -n "${ERROR_STR}" ]; then
echo "${ERROR_STR}"
exit 1
fi
# # Note that we need to upload the output even if comparison fails, so we undo `set -ex`
# end_
validate-wgsl-webgpu:
name: validate-wgsl-webgpu
runs-on: 'ubuntu-24.04-8core'
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- name: Run build script
run: ./build.sh -W debug test_filamat filament
- name: Run test
run: ./out/cmake-debug/libs/filamat/test_filamat --gtest_filter=MaterialCompiler.Wgsl*
# python3 ${TEST_DIR}/src/compare.py \
# --src=${GOLDEN_OUTPUT_DIR} \
# --dest=${RENDER_OUTPUT_DIR} \
# --out=${DIFF_OUTPUT_DIR} 2>&1 | tee compare_output.txt
# if grep "Failed" compare_output.txt > /dev/null; then
# DELIMITER="EOF_FILE_CONTENT_$(date +%s)" # Using timestamp to make it more unique
# echo "err<<$DELIMITER" >> "$GITHUB_OUTPUT"
# cat compare_output.txt >> "$GITHUB_OUTPUT"
# echo "$DELIMITER" >> "$GITHUB_OUTPUT"
# fi
# - uses: actions/upload-artifact@v4
# with:
# name: presubmit-renderdiff-result
# path: ./out/renderdiff
# - name: Compare result
# run: |
# ERROR_STR="${{ steps.render_compare.outputs.err }}"
# if [ -n "${ERROR_STR}" ]; then
# echo "${ERROR_STR}"
# exit 1
# fi
# validate-wgsl-webgpu:
# name: validate-wgsl-webgpu
# runs-on: 'ubuntu-24.04-8core'
# steps:
# - uses: actions/checkout@v4.1.6
# with:
# fetch-depth: 0
# - uses: ./.github/actions/linux-prereq
# - name: Run build script
# run: ./build.sh -W debug test_filamat filament
# - name: Run test
# run: ./out/cmake-debug/libs/filamat/test_filamat --gtest_filter=MaterialCompiler.Wgsl*
# test-code-correctness:
# name: test-code-correctness
# runs-on: 'macos-14-xlarge'
# steps:
# - uses: actions/checkout@v4.1.6
# with:
# fetch-depth: 0
# - uses: ./.github/actions/mac-prereq
# - name: Install clang-tidy and deps
# run: |
# pip install pyyaml
# brew install llvm@${GITHUB_LLVM_VERSION}
# sudo ln -s "$(brew --prefix llvm)@${GITHUB_LLVM_VERSION}/bin/clang-tidy" "/usr/local/bin/clang-tidy"
# - name: Run build script
# # We need to build before clang-tidy can run analysis
# run: |
# # This will build for all three desktop backends on mac
# ./build.sh -p desktop debug gltf_viewer
# - name: Run test
# run: bash test/code-correctness/test.sh
test-code-correctness:
name: test-code-correctness
runs-on: 'macos-14-xlarge'
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/mac-prereq
- name: Install clang-tidy and deps
run: |
pip install pyyaml
brew install llvm@${GITHUB_LLVM_VERSION}
sudo ln -s "$(brew --prefix llvm)@${GITHUB_LLVM_VERSION}/bin/clang-tidy" "/usr/local/bin/clang-tidy"
- name: Run build script
# We need to build before clang-tidy can run analysis
run: |
# This will build for all three desktop backends on mac
./build.sh -p desktop debug gltf_viewer
- name: Run test
run: bash test/code-correctness/test.sh
+1 -3
View File
@@ -135,9 +135,7 @@ set build_flags=-j %NUMBER_OF_PROCESSORS%
:: cmake --build . --target sample-resources --config %config% %build_flags% || exit /b
:: cmake --build . --target suzanne-resources --config %config% %build_flags% || exit /b
cmake --build . --target sample-resources --config %config% %build_flags% -- /m || exit /b
:: cmake --build . %INSTALL% --config %config% %build_flags% -- /m || exit /b
cmake --build . %INSTALL% --config %config% %build_flags% -- /m || exit /b
@echo off
echo Disk info after building variant: %variant%
+44 -18
View File
@@ -58,10 +58,10 @@ namespace {
areCopyCompatible(sourceFormat, destinationFormat) &&
(sourceExtent.width == destinationExtent.width) &&
(sourceExtent.height == destinationExtent.height) &&
((sourceOrigin.x & sourceBlockSize.width) == 0) &&
((sourceOrigin.y & sourceBlockSize.height) == 0) &&
((destinationOrigin.x & destinationBlockSize.width) == 0) &&
((destinationOrigin.y & destinationBlockSize.height) == 0);
((sourceOrigin.x % sourceBlockSize.width) == 0) &&
((sourceOrigin.y % sourceBlockSize.height) == 0) &&
((destinationOrigin.x % destinationBlockSize.width) == 0) &&
((destinationOrigin.y % destinationBlockSize.height) == 0);
}
struct BlitFragmentShaderArgs final {
@@ -206,9 +206,20 @@ constexpr std::string_view FRAGMENT_SHADER_SNIPPET_2D_INPUT_TEMPLATE{ R"(
} // namespace
// 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.
// The blitter caches pipelines, layouts, and shaders to avoid recompilation.
WebGPUBlitter::WebGPUBlitter(wgpu::Device const& device)
: mDevice{ device } {}
// Performs a blit operation from a source to a destination texture.
// This function first checks if a direct texture-to-texture copy can be performed.
// A direct copy is possible if the source and destination textures have the same sample count,
// compatible formats, and the copy extents match. If a direct copy is not possible, this
// function falls back to a render pass-based blit. The render pass uses a shader to read from
// the source texture and write to the destination texture, allowing for format conversion,
// scaling, and MSAA resolves.
void WebGPUBlitter::blit(wgpu::Queue const& queue, wgpu::CommandEncoder const& commandEncoder,
BlitArgs const& args) {
// current assumptions/simplifications made in this implementation:
@@ -267,22 +278,20 @@ void WebGPUBlitter::blit(wgpu::Queue const& queue, wgpu::CommandEncoder const& c
args.destination.texture.GetUsage() & wgpu::TextureUsage::CopyDst)
<< "destination texture usage doesn't have wgpu::TextureUsage::CopyDst";
copy(commandEncoder, args);
copyByteByByte(commandEncoder, args);
return;
}
// need to do a render pass blit...
FILAMENT_CHECK_PRECONDITION(args.destination.texture.GetSampleCount() == 1)
<< "Blitting does not currently support writing to multisampled output textures.";
FILAMENT_CHECK_PRECONDITION(args.source.texture.GetUsage() & wgpu::TextureUsage::TextureBinding)
<< "source texture usage doesn't have wgpu::TextureUsage::TextureBinding";
<< "Source texture usage doesn't have wgpu::TextureUsage::TextureBinding";
FILAMENT_CHECK_PRECONDITION(
args.destination.texture.GetUsage() & wgpu::TextureUsage::RenderAttachment)
<< "destination texture usage doesn't have wgpu::TextureUsage::RenderAttachment";
<< "Destination texture usage doesn't have wgpu::TextureUsage::RenderAttachment";
// create input/output texture views...
const wgpu::TextureViewDimension sourceDimension{ args.source.texture.GetDimension() ==
wgpu::TextureDimension::e3D
? wgpu::TextureViewDimension::e3D
@@ -327,7 +336,6 @@ void WebGPUBlitter::blit(wgpu::Queue const& queue, wgpu::CommandEncoder const& c
<< destinationViewDescriptor.baseArrayLayer << " and mip level "
<< destinationViewDescriptor.baseMipLevel << " for render pass blit?";
// create uniform buffer (for shader args)...
const BlitFragmentShaderArgs blitFragmentShaderArgs{
.depthPlane = args.source.layerOrDepth,
.scale = { static_cast<float>(args.source.extent.width) / args.destination.extent.width,
@@ -438,7 +446,7 @@ void WebGPUBlitter::blit(wgpu::Queue const& queue, wgpu::CommandEncoder const& c
const wgpu::RenderPassEncoder renderPassEncoder{ commandEncoder.BeginRenderPass(
&renderPassDescriptor) };
FILAMENT_CHECK_POSTCONDITION(renderPassEncoder)
<< "Failed to create render pass encoder for blit?";
<< "Failed to create render pass encoder for blit.";
renderPassEncoder.SetPipeline(getOrCreateRenderPipeline(args.filter, sourceDimension,
args.source.texture.GetSampleCount(), depthSource,
args.destination.texture.GetFormat()));
@@ -448,7 +456,7 @@ void WebGPUBlitter::blit(wgpu::Queue const& queue, wgpu::CommandEncoder const& c
renderPassEncoder.End();
}
void WebGPUBlitter::copy(wgpu::CommandEncoder const& commandEncoder, BlitArgs const& args) {
void WebGPUBlitter::copyByteByByte(wgpu::CommandEncoder const& commandEncoder, BlitArgs const& args) {
const wgpu::TexelCopyTextureInfo sourceCopyInfo{
.texture = args.source.texture,
.mipLevel = args.source.mipLevel,
@@ -505,7 +513,7 @@ void WebGPUBlitter::createSampler(const SamplerMagFilter filter) {
.maxAnisotropy = 1, // should not matter, just being consistently defined
};
const wgpu::Sampler sampler{ mDevice.CreateSampler(&descriptor) };
FILAMENT_CHECK_POSTCONDITION(sampler) << "Failed to create blit sampler?";
FILAMENT_CHECK_POSTCONDITION(sampler) << "Failed to create blit sampler.";
switch (filter) {
case SamplerMagFilter::NEAREST:
mNearestSampler = sampler;
@@ -516,6 +524,8 @@ 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,
@@ -529,6 +539,9 @@ wgpu::RenderPipeline const& WebGPUBlitter::getOrCreateRenderPipeline(
return mRenderPipelines[key];
}
// 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) {
@@ -580,7 +593,7 @@ wgpu::RenderPipeline WebGPUBlitter::createRenderPipeline(const SamplerMagFilter
.fragment = &fragmentState,
};
const wgpu::RenderPipeline pipeline{ mDevice.CreateRenderPipeline(&pipelineDescriptor) };
FILAMENT_CHECK_POSTCONDITION(pipeline) << "Failed to create pipeline for render pass blit?";
FILAMENT_CHECK_POSTCONDITION(pipeline) << "Failed to create pipeline for render pass blit.";
return pipeline;
}
@@ -595,6 +608,8 @@ size_t WebGPUBlitter::hashRenderPipelineKey(const SamplerMagFilter filterType,
return seed;
}
// 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) {
@@ -607,6 +622,8 @@ wgpu::PipelineLayout const& WebGPUBlitter::getOrCreatePipelineLayout(
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) {
@@ -619,7 +636,7 @@ wgpu::PipelineLayout WebGPUBlitter::createPipelineLayout(const SamplerMagFilter
const wgpu::PipelineLayout pipelineLayout{ mDevice.CreatePipelineLayout(
&pipelineLayoutDescriptor) };
FILAMENT_CHECK_POSTCONDITION(pipelineLayout)
<< "Failed to create pipeline layout for render pass blit?";
<< "Failed to create pipeline layout for render pass blit.";
return pipelineLayout;
}
@@ -633,6 +650,8 @@ size_t WebGPUBlitter::hashPipelineLayoutKey(const SamplerMagFilter filterType,
return seed;
}
// 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) {
@@ -645,6 +664,9 @@ wgpu::BindGroupLayout const& WebGPUBlitter::getOrCreateTextureBindGroupLayout(
return mTextureBindGroupLayouts[key];
}
// 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) {
@@ -693,7 +715,7 @@ wgpu::BindGroupLayout WebGPUBlitter::createTextureBindGroupLayout(const SamplerM
const wgpu::BindGroupLayout textureBindGroupLayout{ mDevice.CreateBindGroupLayout(
&textureBindGroupLayoutDescriptor) };
FILAMENT_CHECK_POSTCONDITION(textureBindGroupLayout)
<< "Failed to create texture bind group layout for render pass blit?";
<< "Failed to create texture bind group layout for render pass blit.";
return textureBindGroupLayout;
}
@@ -707,6 +729,8 @@ size_t WebGPUBlitter::hashTextureBindGroupLayoutKey(const SamplerMagFilter filte
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) {
@@ -719,6 +743,9 @@ wgpu::ShaderModule const& WebGPUBlitter::getOrCreateShaderModule(
return mShaderModules[key];
}
// 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) {
@@ -730,7 +757,6 @@ wgpu::ShaderModule WebGPUBlitter::createShaderModule(
textureType = "texture_depth_2d";
}
} else {
// non-depth
if (multisampledSource) {
textureType = "texture_multisampled_2d";
} else {
@@ -785,7 +811,7 @@ wgpu::ShaderModule WebGPUBlitter::createShaderModule(
};
const wgpu::ShaderModule shaderModule{ mDevice.CreateShaderModule(&shaderModuleDescriptor) };
FILAMENT_CHECK_POSTCONDITION(shaderModule)
<< "Failed to create shader module for render pass blit?";
<< "Failed to create shader module for render pass blit.";
return shaderModule;
}
+17 -1
View File
@@ -26,6 +26,17 @@
namespace filament::backend {
/**
* A blit essentially writes pixels to a region of one texture given a region of another texture.
* Such a process can be a byte-by-byte copy when possible, while other scenarios involve various
* transformations, such as pixel format conversion, image scaling, filtering/sampling
* (such as down sampling), or even multi-sample resolves. Some blits can be faster than others,
* depending on what is needed; for example, a byte-by-byte copy is generally faster than other
* operations.
*
* This class is responsible for performing blits throughout the lifecycle of its owner/caller,
* e.g. the WebGPU backend/driver, where it can leverage state to optimize such blit calls over time.
*/
class WebGPUBlitter final {
public:
struct BlitArgs final {
@@ -58,10 +69,15 @@ private:
/**
* ONLY should be called if canDoDirectCopy(...) is true (see it in WebGPUBlitter.cpp)
*/
void copy(wgpu::CommandEncoder const&, BlitArgs const&);
void copyByteByByte(wgpu::CommandEncoder const&, BlitArgs const&);
void createSampler(SamplerMagFilter);
// 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.
[[nodiscard]] wgpu::RenderPipeline const& getOrCreateRenderPipeline(SamplerMagFilter,
wgpu::TextureViewDimension sourceDimension, uint32_t sourceSampleCount,
bool depthSource, wgpu::TextureFormat destinationTextureFormat);
@@ -33,6 +33,9 @@ namespace filament::backend {
namespace {
// Creates a wgpu::Buffer, ensuring its size is a multiple of FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS.
// WebGPU's WriteBuffer requires the write size to be a multiple of 4. By ensuring the buffer
// size is also a multiple of 4, we simplify the update logic.
[[nodiscard]] wgpu::Buffer createBuffer(wgpu::Device const& device, const wgpu::BufferUsage usage,
uint32_t size, const char* const label) {
// Write size must be divisible by WEBGPU_BUFFER_SIZE_MODULUS (e.g. 4).
@@ -55,6 +58,11 @@ WebGPUBufferBase::WebGPUBufferBase(wgpu::Device const& device, const wgpu::Buffe
const uint32_t size, char const* const label)
: mBuffer{ createBuffer(device, usage, size, label) } {}
// Updates the GPU buffer with data from a BufferDescriptor.
// WebGPU requires that the size of the data written to a buffer is a multiple of 4.
// This function handles cases where the buffer descriptor's size is not a multiple of 4
// by writing the bulk of the data first, and then copying the remaining bytes into a
// padded temporary chunk which is then written to the buffer.
void WebGPUBufferBase::updateGPUBuffer(BufferDescriptor const& bufferDescriptor,
const uint32_t byteOffset, wgpu::Queue const& queue) {
FILAMENT_CHECK_PRECONDITION(bufferDescriptor.buffer)
@@ -28,6 +28,10 @@ namespace filament::backend {
class BufferDescriptor;
/**
* A base class for WebGPU buffer objects, providing common functionality for creating and
* updating GPU buffers.
*/
class WebGPUBufferBase /* intended to be extended */ {
public:
/**
@@ -46,7 +50,8 @@ protected:
private:
const wgpu::Buffer mBuffer;
// FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS (e.g. 4) bytes to hold any extra chunk we need.
// WebGPU requires that the source buffer of a writeBuffer call has a size that is a multiple
// of 4. This member is used to pad the data if the source size is not a multiple of 4.
std::array<uint8_t, FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS> mRemainderChunk{};
};
@@ -40,6 +40,8 @@ namespace {
} // namespace
// The usage flags are determined by the binding type, and always include CopyDst to allow for
// updating the buffer.
WebGPUBufferObject::WebGPUBufferObject(wgpu::Device const& device,
const BufferObjectBinding bindingType, const uint32_t byteCount)
: HwBufferObject{ byteCount },
@@ -31,6 +31,12 @@ namespace filament::backend {
enum class BufferObjectBinding : uint8_t;
/**
* A WebGPU-specific implementation of BufferObject.
* A BufferObject is a GPU buffer that can be used for a variety of purposes, such as storing
* uniform data, vertex attributes, or pixel data. The specific usage is determined by the
* BufferObjectBinding.
*/
class WebGPUBufferObject final : public HwBufferObject, public WebGPUBufferBase {
public:
WebGPUBufferObject(wgpu::Device const&, BufferObjectBinding, uint32_t byteCount);
@@ -21,6 +21,7 @@
#include <cstdint>
// WebGPU requires that the source buffer of a writeBuffer call has a size that is a multiple of 4.
constexpr size_t FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS = 4;
// FWGPU is short for Filament WebGPU
@@ -42,6 +42,9 @@ constexpr uint8_t INVALID_INDEX = MAX_DESCRIPTOR_COUNT + 1;
} // namespace
// The constructor initializes the descriptor set based on a given layout.
// It pre-allocates space for the bind group entries and sets up a mapping from binding index
// to the internal entry array index. This prepares the object to be populated with resources.
WebGPUDescriptorSet::WebGPUDescriptorSet(wgpu::BindGroupLayout const& layout,
std::vector<WebGPUDescriptorSetLayout::BindGroupEntryInfo> const& bindGroupEntries)
: mLayout{ layout },
@@ -52,11 +55,10 @@ WebGPUDescriptorSet::WebGPUDescriptorSet(wgpu::BindGroupLayout const& layout,
for (size_t i = 0; i < bindGroupEntries.size(); ++i) {
mEntries[i].binding = bindGroupEntries[i].binding;
}
// Establish the size of entries based on the layout. This should be reliable and efficient.
// Create a mapping from binding index to entry index for efficient lookup.
assert_invariant(INVALID_INDEX > mEntryIndexByBinding.size());
for (size_t i = 0; i < mEntryIndexByBinding.size(); i++) {
mEntryIndexByBinding[i] = INVALID_INDEX;
}
std::fill(mEntryIndexByBinding.begin(), mEntryIndexByBinding.end(), INVALID_INDEX);
for (size_t index = 0; index < mEntries.size(); index++) {
wgpu::BindGroupEntry const& entry = mEntries[index];
assert_invariant(entry.binding < mEntryIndexByBinding.size());
@@ -66,19 +68,20 @@ WebGPUDescriptorSet::WebGPUDescriptorSet(wgpu::BindGroupLayout const& layout,
void WebGPUDescriptorSet::addEntry(const unsigned int index, wgpu::BindGroupEntry&& entry) {
if (mBindGroup) {
// We will keep getting hits from future updates, but shouldn't do anything
// Filament guarantees this won't change after things have locked.
// The bind group has already been created, so we can't add new entries.
// Filament guarantees that the descriptor set will not be modified after it has been locked.
return;
}
// TODO: Putting some level of trust that Filament is not going to reuse indexes or go past the
// layout index for efficiency. Add guards if wrong.
FILAMENT_CHECK_POSTCONDITION(index < mEntryIndexByBinding.size())
<< "impossible/invalid index for a descriptor/binding (our of range or >= "
<< "impossible/invalid index for a descriptor/binding (out of range or >= "
"MAX_DESCRIPTOR_COUNT) "
<< index;
uint8_t entryIndex = mEntryIndexByBinding[index];
FILAMENT_CHECK_POSTCONDITION(entryIndex != INVALID_INDEX && entryIndex < mEntries.size())
<< "Invalid binding " << index;
<< "Invalid binding index: " << index;
entry.binding = index;
mEntries[entryIndex] = std::move(entry);
}
@@ -94,8 +97,9 @@ wgpu::BindGroup WebGPUDescriptorSet::lockAndReturn(wgpu::Device const& device) {
.entries = mEntries.data()
};
mBindGroup = device.CreateBindGroup(&descriptor);
FILAMENT_CHECK_POSTCONDITION(mBindGroup) << "Failed to create bind group?";
// once we have created the bind group itself we should no longer need any other state
FILAMENT_CHECK_POSTCONDITION(mBindGroup) << "Failed to create bind group.";
// Once the bind group is created, we can release the resources used to create it.
#if FWGPU_ENABLED(FWGPU_DEBUG_BIND_GROUPS)
FWGPU_LOGD << "WebGPUDescriptorSet (lockAndReturn):";
FWGPU_LOGD << " wgpu::BindGroupLayout handle: " << mLayout.Get();
@@ -32,23 +32,46 @@
namespace filament::backend {
/**
* A WebGPU-specific implementation of DescriptorSet.
* This class manages a collection of resource bindings (e.g., textures, buffers) that are used by
* a shader. It corresponds to a `wgpu::BindGroup` in WebGPU. The actual `wgpu::BindGroup` is
* created lazily when `lockAndReturn` is called, at which point the descriptor set becomes
* immutable.
*/
class WebGPUDescriptorSet final : public HwDescriptorSet {
public:
WebGPUDescriptorSet(wgpu::BindGroupLayout const&,
std::vector<WebGPUDescriptorSetLayout::BindGroupEntryInfo> const&);
~WebGPUDescriptorSet();
/**
* Adds or updates a resource binding in the descriptor set.
* This method is called to populate the descriptor set with buffers, textures, and samplers.
* It is only valid to call this before the descriptor set is locked (i.e., before
* `lockAndReturn` is called).
*/
void addEntry(unsigned int index, wgpu::BindGroupEntry&& entry);
/**
* Finalizes the descriptor set by creating the wgpu::BindGroup. After this call, the
* descriptor set is considered "locked" and cannot be modified further.
*/
[[nodiscard]] wgpu::BindGroup lockAndReturn(wgpu::Device const&);
/**
* @return true if the descriptor set has been locked (i.e., the wgpu::BindGroup has been
* created).
*/
[[nodiscard]] bool getIsLocked() const { return mBindGroup != nullptr; }
[[nodiscard]] size_t getEntitiesWithDynamicOffsetsCount() const {
return mEntriesWithDynamicOffsetsCount;
}
// May be nullptr. Use lockAndReturn to create the bind group when appropriate
/**
* May be nullptr. Use lockAndReturn to create the bind group when appropriate
*/
[[nodiscard]] wgpu::BindGroup const& getBindGroup() const { return mBindGroup; }
#if FWGPU_ENABLED(FWGPU_DEBUG_BIND_GROUPS)
@@ -60,6 +83,7 @@ private:
std::array<uint8_t, MAX_DESCRIPTOR_COUNT> mEntryIndexByBinding{};
std::vector<wgpu::BindGroupEntry> mEntries;
const size_t mEntriesWithDynamicOffsetsCount;
// This is created lazily when lockAndReturn is called.
wgpu::BindGroup mBindGroup = nullptr;
};
@@ -40,7 +40,9 @@ namespace filament::backend {
namespace {
// Convert Filament Shader Stage Flags bitmask to webgpu equivalent
/**
* Converts Filament shader stage flags to the corresponding WebGPU shader stage bitmask.
*/
[[nodiscard]] wgpu::ShaderStage filamentStageToWGPUStage(const ShaderStageFlags fFlags) {
wgpu::ShaderStage retStages = wgpu::ShaderStage::None;
if (any(ShaderStageFlags::VERTEX & fFlags)) {
@@ -70,10 +72,6 @@ WebGPUDescriptorSetLayout::WebGPUDescriptorSetLayout(DescriptorSetLayout const&
baseLabel = temp->c_str();
}
// TODO: layoutDescriptor has a "Label". Ideally we can get info on what this layout is for
// debugging. For now, hack an incrementing value.
static int layoutNum = 0;
const unsigned int samplerCount =
std::count_if(layout.bindings.begin(), layout.bindings.end(), [](auto& fEntry) {
return DescriptorSetLayoutBinding::isSampler(fEntry.type);
@@ -87,6 +85,11 @@ WebGPUDescriptorSetLayout::WebGPUDescriptorSetLayout(DescriptorSetLayout const&
auto& wEntry = wEntries.emplace_back();
auto& entryInfo = mBindGroupEntries.emplace_back();
wEntry.visibility = filamentStageToWGPUStage(fEntry.stageFlags);
// In WebGPU, textures and samplers are separate bindings.
// We map the Filament binding index to two WebGPU binding indices:
// - texture: binding * 2
// - sampler: binding * 2 + 1
wEntry.binding = fEntry.binding * 2;
entryInfo.binding = wEntry.binding;
@@ -178,11 +181,13 @@ WebGPUDescriptorSetLayout::WebGPUDescriptorSetLayout(DescriptorSetLayout const&
} else if (isCubeArrayTypeDescriptor(fEntry.type)) {
wEntry.texture.viewDimension = wgpu::TextureViewDimension::CubeArray;
}
// fEntry.count is unused currently
}
std::string label = "layout_" + baseLabel + std::to_string(++layoutNum) ;
// TODO: layoutDescriptor has a "Label". Ideally we can get info on what this layout is for
// debugging. For now, hack an incrementing value.
static int layoutNum = 0;
std::string label = "layout_" + baseLabel + "_" + std::to_string(++layoutNum) ;
const wgpu::BindGroupLayoutDescriptor layoutDescriptor{
.label{label.c_str()}, // Use .c_str() if label needs to be const char*
.label{label.c_str()},
.entryCount = wEntries.size(),
.entries = wEntries.data()
};
@@ -27,6 +27,11 @@
namespace filament::backend {
/**
* A WebGPU implementation of the HwDescriptorSetLayout.
* This class defines the layout of a descriptor set, which specifies the types and arrangement
* of resources that can be bound to a pipeline.
*/
class WebGPUDescriptorSetLayout final : public HwDescriptorSetLayout {
public:
struct BindGroupEntryInfo final {
+185 -100
View File
@@ -71,7 +71,7 @@ void setDefaultTargetFlags(WebGPURenderTarget& defaultRenderTarget,
const wgpu::TextureFormat depthFormat{ swapChain.getDepthFormat() };
TargetBufferFlags newTargetFlags{ filament::backend::TargetBufferFlags::NONE };
// Assuming Color always present in default render target.
// Color is always assumed to be present in the default render target.
newTargetFlags |= filament::backend::TargetBufferFlags::COLOR;
if (depthFormat != wgpu::TextureFormat::Undefined) {
if (hasDepth(depthFormat)) {
@@ -86,6 +86,12 @@ void setDefaultTargetFlags(WebGPURenderTarget& defaultRenderTarget,
} // namespace
// The WebGPUDriver is the main entry point for the WebGPU backend. It is responsible for
// creating and managing all WebGPU resources, and for submitting commands to the GPU.
// It implements the Driver interface, which is the abstraction layer used by Filament's
// renderer. The driver uses a handle-based system to manage resources, and it operates
// asynchronously, with `create...S` methods for synchronous handle allocation and
// `create...R` methods for resource creation on the backend thread.
Driver* WebGPUDriver::create(WebGPUPlatform& platform, const Platform::DriverConfig& driverConfig) noexcept {
constexpr size_t defaultSize = FILAMENT_WEBGPU_HANDLE_ARENA_SIZE_IN_MB * 1024U * 1024U;
Platform::DriverConfig validConfig {driverConfig};
@@ -160,6 +166,7 @@ void WebGPUDriver::endFrame(const uint32_t /* frameId */) {
mPipelineLayoutCache.onFrameEnd();
mPipelineCache.onFrameEnd();
// Clear the currently bound descriptor sets at the end of the frame.
for (size_t i = 0; i < MAX_DESCRIPTOR_SET_COUNT; i++) {
mCurrentDescriptorSets[i] = {};
}
@@ -172,7 +179,7 @@ void WebGPUDriver::flush(int /* dummy */) {
if (mCommandEncoder == nullptr) {
return;
}
// submit the command buffer thus far...
// Submit the commands recorded so far.
assert_invariant(mRenderPassEncoder == nullptr);
wgpu::CommandBufferDescriptor commandBufferDescriptor{
.label = "frame_command_after_flush",
@@ -189,12 +196,15 @@ void WebGPUDriver::flush(int /* dummy */) {
assert_invariant(mCommandEncoder);
}
// 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 */) {
if (mCommandEncoder == nullptr) {
return;
}
flush();
// Wait for all previously submitted work to finish.
std::mutex syncPoint;
std::condition_variable syncCondition;
bool done = false;
@@ -264,10 +274,8 @@ void WebGPUDriver::destroyRenderTarget(Handle<HwRenderTarget> renderTargetHandle
if (renderTarget == mCurrentRenderTarget) {
mCurrentRenderTarget = nullptr;
}
// WGPURenderTarget destructor is trivial.
// The HwTexture handles stored within WGPURenderTarget (via MRT, TargetBufferInfo)
// are not owned by WGPURenderTarget, so they are not destroyed here.
// They are destroyed via WebGPUDriver::destroyTexture.
// The HwTexture handles stored within WGPURenderTarget are not owned by it,
// so they are not destroyed here. They are destroyed via WebGPUDriver::destroyTexture.
destructHandle<WebGPURenderTarget>(renderTargetHandle);
}
}
@@ -312,6 +320,14 @@ void WebGPUDriver::destroyDescriptorSet(Handle<HwDescriptorSet> descriptorSetHan
}
}
// ------------------------------------------------------------------------------------------------
// Synchronous handle creation methods (S-methods)
//
// These methods are called from the frontend thread to allocate a handle for a backend resource.
// They are lightweight and do not perform any GPU operations. The actual resource creation
// happens in the corresponding R-method on the backend thread.
// ------------------------------------------------------------------------------------------------
Handle<HwSwapChain> WebGPUDriver::createSwapChainS() noexcept {
return allocHandle<WebGPUSwapChain>();
}
@@ -396,6 +412,13 @@ Handle<HwTexture> WebGPUDriver::createTextureExternalImagePlaneS() noexcept {
return allocHandle<WebGPUTexture>();
}
// ------------------------------------------------------------------------------------------------
// Asynchronous resource creation methods (R-methods)
//
// These methods are called on the backend thread to create the actual GPU resources
// associated with a handle that was previously allocated by an S-method.
// ------------------------------------------------------------------------------------------------
void WebGPUDriver::createSwapChainR(Handle<HwSwapChain> sch, void* nativeWindow, uint64_t flags) {
mNativeWindow = nativeWindow;
assert_invariant(!mSwapChain);
@@ -544,7 +567,7 @@ void WebGPUDriver::createRenderTargetR(Handle<HwRenderTarget> renderTargetHandle
}
void WebGPUDriver::createFenceR(Handle<HwFence> fenceHandle, const int /* dummy */) {
// the handle was already constructed in createFenceS
// The handle is constructed synchronously in createFenceS.
const auto fence = handleCast<WebGPUFence>(fenceHandle);
assert_invariant(mQueue);
fence->addMarkerToQueueState(mQueue);
@@ -614,8 +637,6 @@ FenceStatus WebGPUDriver::getFenceStatus(Handle<HwFence> fenceHandle) {
return fence->getStatus();
}
// We create all textures using VK_IMAGE_TILING_OPTIMAL, so our definition of "supported" is that
// the GPU supports the given texture format with non-zero optimal tiling features.
bool WebGPUDriver::isTextureFormatSupported(const TextureFormat format) {
return toWGPUTextureFormat(format) != wgpu::TextureFormat::Undefined;
}
@@ -796,6 +817,10 @@ void WebGPUDriver::setVertexBufferObject(Handle<HwVertexBuffer> vertexBufferHand
vertexBuffer->getBuffers()[index] = bufferObject->getBuffer();
}
// Updates a 3D texture region with pixel data from a buffer.
// This function handles various pixel formats and performs a blit operation if the source
// format needs to be converted to the destination format. For compressed textures, it ensures
// that the offsets are aligned to the block dimensions.
void WebGPUDriver::update3DImage(Handle<HwTexture> textureHandle, const uint32_t level,
const uint32_t xoffset, const uint32_t yoffset, const uint32_t zoffset,
const uint32_t width, const uint32_t height, const uint32_t depth,
@@ -812,15 +837,15 @@ void WebGPUDriver::update3DImage(Handle<HwTexture> textureHandle, const uint32_t
const auto texture{ handleCast<WebGPUTexture>(textureHandle) };
FILAMENT_CHECK_PRECONDITION((texture->getTexture().GetWidth() + xoffset) >= width)
<< "Blitting requires the destination region to have enough width ("
<< texture->getTexture().GetWidth() << " to accommodate the requested " << width
<< texture->getTexture().GetWidth() << ") to accommodate the requested " << width
<< " width to write/blit (accounting for xoffset, which is " << xoffset << ").";
FILAMENT_CHECK_PRECONDITION((texture->getTexture().GetHeight() + yoffset) >= height)
<< "Blitting requires the destination region to have enough height ("
<< texture->getTexture().GetHeight() << " to accommodate the requested " << height
<< texture->getTexture().GetHeight() << ") to accommodate the requested " << height
<< " height to write/blit (accounting for yoffset, which is " << yoffset << ").";
FILAMENT_CHECK_PRECONDITION((texture->getTexture().GetDepthOrArrayLayers() + zoffset) >= depth)
<< "Blitting requires the destination region to have enough depth/arrayLayers ("
<< texture->getTexture().GetDepthOrArrayLayers() << " to accommodate the requested "
<< texture->getTexture().GetDepthOrArrayLayers() << ") to accommodate the requested "
<< depth << " depth to write/blit (accounting for zoffset, which is " << zoffset << ").";
// TODO: Writing to a depth texture is illegal and errors. I'm not sure why Filament is trying
@@ -833,10 +858,8 @@ void WebGPUDriver::update3DImage(Handle<HwTexture> textureHandle, const uint32_t
inputData->type) };
const wgpu::TextureFormat outputLinearFormat{ toLinearFormat(
texture->getTexture().GetFormat()) };
const bool conversionNecessary{
inputPixelFormat != outputLinearFormat && inputData->type != PixelDataType::COMPRESSED
}; // compressed formats should never need conversion
const bool doBlit{ conversionNecessary };
const bool doBlit{ conversionNecessary(inputPixelFormat, outputLinearFormat, inputData->type) };
#if FWGPU_ENABLED(FWGPU_DEBUG_UPDATE_IMAGE)
if (texture->width > 1000 && texture->height > 500) {
FWGPU_LOGD << "Update3DImage(..., level=" << level << ", xoffset=" << xoffset
@@ -958,7 +981,7 @@ void WebGPUDriver::update3DImage(Handle<HwTexture> textureHandle, const uint32_t
}
stagingTexture.Destroy();
} else {
// not doing blit (copy byte-by-byte)...
// Direct copy without a blit.
const auto copyInfo { wgpu::TexelCopyTextureInfo{
.texture = texture->getTexture(),
.mipLevel = level,
@@ -1070,10 +1093,14 @@ void WebGPUDriver::compilePrograms(CompilerPriorityQueue priority,
}
}
// Begins a render pass. This sets up the render pass descriptor with the appropriate
// color and depth/stencil attachments, based on whether the target is the default
// swap chain render target or a custom one. It also handles MSAA by setting up
// resolve targets if necessary.
void WebGPUDriver::beginRenderPass(Handle<HwRenderTarget> renderTargetHandle,
RenderPassParams const& params) {
if (!mCommandEncoder) {
wgpu::CommandEncoderDescriptor commandEncoderDescriptor = { .label = "frame_command_encoder" };
wgpu::CommandEncoderDescriptor commandEncoderDescriptor{ .label = "frame_command_encoder" };
mCommandEncoder = mDevice.CreateCommandEncoder(&commandEncoderDescriptor);
}
assert_invariant(mCommandEncoder);
@@ -1321,19 +1348,24 @@ void WebGPUDriver::stopCapture(int) {
//todo
}
// Reads a block of pixels from a render target into a buffer.
// This function is asynchronous. It copies the pixel data to a staging buffer on the GPU,
// and then maps the buffer for reading on the CPU. The provided callback is invoked when the
// data is ready. This function also handles the 256-byte row alignment requirement for
// buffer-to-texture copies in WebGPU.
void WebGPUDriver::readPixels(Handle<HwRenderTarget> sourceRenderTargetHandle, const uint32_t x,
const uint32_t y, const uint32_t width, const uint32_t height,
PixelBufferDescriptor&& pixelBufferDescriptor) {
auto srcTarget = handleCast<WebGPURenderTarget>(sourceRenderTargetHandle);
const auto srcTarget{ handleCast<WebGPURenderTarget>(sourceRenderTargetHandle) };
assert_invariant(srcTarget);
wgpu::Texture srcTexture = nullptr;
wgpu::Texture srcTexture{ nullptr };
if (srcTarget->isDefaultRenderTarget()) {
assert_invariant(mSwapChain);
srcTexture = mSwapChain->getCurrentTexture();
} else {
// Handle custom render targets. Read from the first color attachment.
const auto& colorAttachmentInfos = srcTarget->getColorAttachmentInfos();
const auto& colorAttachmentInfos{ srcTarget->getColorAttachmentInfos() };
// TODO we are currently assuming the first attachment is the desired texture.
if (colorAttachmentInfos[0].handle) {
auto texture = handleCast<WebGPUTexture>(colorAttachmentInfos[0].handle);
@@ -1348,97 +1380,145 @@ void WebGPUDriver::readPixels(Handle<HwRenderTarget> sourceRenderTargetHandle, c
scheduleDestroy(std::move(pixelBufferDescriptor));
return;
}
const uint32_t srcWidth = srcTexture.GetWidth();
const uint32_t srcHeight = srcTexture.GetHeight();
const uint32_t srcWidth {srcTexture.GetWidth()};
const uint32_t srcHeight{srcTexture.GetHeight()};
// Clamp read region to texture bounds
if (x >= srcWidth || y >= srcHeight) {
if (UTILS_UNLIKELY(x >= srcWidth || y >= srcHeight)) {
scheduleDestroy(std::move(pixelBufferDescriptor));
return;
}
auto actualWidth = std::min(width, srcWidth - x);
auto actualHeight = std::min(height, srcHeight - y);
auto actualWidth{ std::min(width, srcWidth - x) };
auto actualHeight{ std::min(height, srcHeight - y)};
if (UTILS_UNLIKELY(actualWidth == 0 || actualHeight == 0)) {
scheduleDestroy(std::move(pixelBufferDescriptor));
return;
}
// Once we're ready to read the pixels, we need to flush all the previous work of this frame.
// This ensures that the readPixels will be ordered after all the draws.
flush();
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 size_t dstBytesPerPixel = PixelBufferDescriptor::computePixelSize(
pixelBufferDescriptor.format, pixelBufferDescriptor.type);
const size_t srcBytesPerPixel = getWGPUTextureFormatPixelSize(srcTexture.GetFormat());
const wgpu::TextureFormat srcFormat{srcTexture.GetFormat()};
const wgpu::TextureFormat dstFormat{toWebGPUFormat(pixelBufferDescriptor.format,
pixelBufferDescriptor.type)};
wgpu::Texture textureToReadFrom{srcTexture};
wgpu::Texture stagingTexture{ nullptr };
uint32_t readX{x};
uint32_t readY{y};
FILAMENT_CHECK_PRECONDITION(dstBytesPerPixel == srcBytesPerPixel && dstBytesPerPixel > 0)
<< "Source texture pixel size (" << srcBytesPerPixel
<< ") does not match destination pixel buffer pixel size (" << dstBytesPerPixel
<< "), or the format is not supported for readPixels.";
// If the source format is different from the destination (e.g. BGRA vs RGBA),
// we need to perform a conversion using an intermediate blit.
if (conversionNecessary(srcFormat, dstFormat, pixelBufferDescriptor.type)) {
const wgpu::TextureDescriptor stagingDescriptor{
.label = "readpixels_staging_texture",
.usage = wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::RenderAttachment,
.dimension = wgpu::TextureDimension::e2D, // are we sure we can assume this? thus far we have
.size = {
.width = actualWidth,
.height = actualHeight,
.depthOrArrayLayers = 1,
},
.format = dstFormat,
.mipLevelCount = 1,
.sampleCount = srcTexture.GetSampleCount(),
};
stagingTexture = mDevice.CreateTexture(&stagingDescriptor);
assert_invariant(stagingTexture);
textureToReadFrom = stagingTexture;
const WebGPUBlitter::BlitArgs blitArgs{
.source = {
.texture = srcTexture,
.origin = {.x = x, .y = y,},
.extent = {
.width = actualWidth,
.height = actualHeight,
},
},
.destination = {
.texture = stagingTexture,
.origin = {.x = 0, .y = 0},
.extent = {
.width = actualWidth,
.height = actualHeight,
},
},
.filter = SamplerMagFilter::NEAREST, // Use NEAREST for a 1:1 copy
};
mBlitter.blit(mQueue, mCommandEncoder, blitArgs);
// Create a staging buffer to copy the texture to. WebGPU requires 256 byte alignment
const size_t bytesPerPixel = dstBytesPerPixel;
const size_t unpaddedBytesPerRow = actualWidth * bytesPerPixel;
const size_t alignment = 256;
const size_t paddedBytesPerRow = (unpaddedBytesPerRow + alignment - 1) & ~(alignment - 1);
// The subsequent read will be from the top-left of the new intermediate texture.
readX = 0;
readY = 0;
}
size_t bufferSize = paddedBytesPerRow * height;
wgpu::BufferDescriptor bufferDesc;
bufferDesc.size = bufferSize;
bufferDesc.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead;
wgpu::Buffer stagingBuffer = mDevice.CreateBuffer(&bufferDesc);
// Create a staging buffer to copy the texture to. WebGPU requires 256 byte alignment for buffer-to-texture copies.
const size_t bytesPerPixel{ PixelBufferDescriptor::computePixelSize(
pixelBufferDescriptor.format, pixelBufferDescriptor.type) };
const size_t unpaddedBytesPerRow{ actualWidth * bytesPerPixel };
const size_t alignment{ 256 };
const size_t paddedBytesPerRow = { (unpaddedBytesPerRow + alignment - 1) & ~(alignment - 1) };
const size_t bufferSize{ paddedBytesPerRow * actualHeight };
const wgpu::BufferDescriptor bufferDesc{
.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead,
.size = bufferSize,
};
const wgpu::Buffer stagingBuffer{ mDevice.CreateBuffer(&bufferDesc) };
assert_invariant(stagingBuffer);
wgpu::CommandEncoder encoder = mDevice.CreateCommandEncoder();
assert_invariant(encoder);
// WebGPU's texture coordinates for copies are top-left, but Filament's y-coordinate is
// bottom-left. We must flip the y-coordinate relative to the texture we are reading from.
const uint32_t textureHeight{ textureToReadFrom.GetHeight() };
const uint32_t flippedY{ textureHeight - readY - actualHeight };
// WebGPU flips the y axis like Metal does
const uint32_t flippedY = srcHeight - y - height;
wgpu::TexelCopyTextureInfo source{
.texture = srcTexture,
.mipLevel = 0,
.origin = { .x = x, .y = flippedY, .z = 0 },
const wgpu::TexelCopyTextureInfo source{
.texture = textureToReadFrom, // Read from the original or intermediate texture
.mipLevel = 0,
.origin = {.x = readX, .y = flippedY, .z = 0,},
};
wgpu::TexelCopyBufferInfo destination {
.layout = {
.offset = 0,
.bytesPerRow = static_cast<uint32_t>(paddedBytesPerRow),
.rowsPerImage = actualHeight,
},
.buffer = stagingBuffer
const wgpu::TexelCopyBufferInfo destination{
.layout = {
.offset = 0,
.bytesPerRow = static_cast<uint32_t>(paddedBytesPerRow),
.rowsPerImage = actualHeight,
},
.buffer = stagingBuffer
};
wgpu::Extent3D copySize{ .width = actualWidth,
const wgpu::Extent3D copySize{
.width = actualWidth,
.height = actualHeight,
.depthOrArrayLayers = 1 };
// 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 read/copy 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.
encoder.CopyTextureToBuffer(&source, &destination, &copySize);
wgpu::CommandBuffer commandBuffer = encoder.Finish();
mQueue.Submit(1, &commandBuffer);
.depthOrArrayLayers = 1,
};
mCommandEncoder.CopyTextureToBuffer(&source, &destination, &copySize);
mCommandBuffer = mCommandEncoder.Finish();
assert_invariant(mCommandBuffer);
mCommandEncoder = nullptr;
mQueue.Submit(1, &mCommandBuffer);
mCommandBuffer = nullptr;
// Map the buffer to read the data
struct UserData {
struct UserData final {
PixelBufferDescriptor pixelBufferDescriptor;
wgpu::Buffer buffer;
size_t unpaddedBytesPerRow;
size_t paddedBytesPerRow;
uint32_t height;
WebGPUDriver* driver;
WebGPUDriver *driver;
};
auto userData = std::make_unique<UserData>(UserData{
.pixelBufferDescriptor = std::move(pixelBufferDescriptor),
.buffer = std::move(stagingBuffer),
.unpaddedBytesPerRow = unpaddedBytesPerRow,
.paddedBytesPerRow = paddedBytesPerRow,
.height = height,
.driver = this,
.pixelBufferDescriptor = std::move(pixelBufferDescriptor),
.buffer = std::move(stagingBuffer),
.unpaddedBytesPerRow = unpaddedBytesPerRow,
.paddedBytesPerRow = paddedBytesPerRow,
.height = actualHeight,
.driver = this,
});
userData->buffer.MapAsync(
@@ -1446,12 +1526,18 @@ void WebGPUDriver::readPixels(Handle<HwRenderTarget> sourceRenderTargetHandle, c
[](wgpu::MapAsyncStatus status, const char* message, UserData* userdata) {
std::unique_ptr<UserData> data(static_cast<UserData*>(userdata));
if (UTILS_LIKELY(status == wgpu::MapAsyncStatus::Success)) {
const char* src = static_cast<const char*>(
data->buffer.GetConstMappedRange(0, data->buffer.GetSize()));
char* dst = static_cast<char*>(data->pixelBufferDescriptor.buffer);
for (uint32_t i = 0; i < data->height; ++i) {
memcpy(dst + i * data->unpaddedBytesPerRow,
src + i * data->paddedBytesPerRow, data->unpaddedBytesPerRow);
const char *src {static_cast<const char *>(
data->buffer.GetConstMappedRange(0, data->buffer.GetSize()))};
char* dst{ static_cast<char*>(data->pixelBufferDescriptor.buffer) };
// If padding was added for alignment, we need to copy row by row.
if (data->paddedBytesPerRow == data->unpaddedBytesPerRow) {
memcpy(dst, src, data->unpaddedBytesPerRow * data->height);
} else {
for (uint32_t i{ 0 }; i < data->height; ++i) {
memcpy(dst + i * data->unpaddedBytesPerRow,
src + i * data->paddedBytesPerRow, data->unpaddedBytesPerRow);
}
}
data->buffer.Unmap();
} else {
@@ -1478,8 +1564,12 @@ void WebGPUDriver::blitDEPRECATED(TargetBufferFlags buffers,
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) {
FILAMENT_CHECK_PRECONDITION(mCommandEncoder)
<< "Resolve assumes there is a valid command encoder to piggyback on.";
if (!mCommandEncoder) {
const wgpu::CommandEncoderDescriptor encoderDescriptor{
.label = "command_created_with_resolve",
};
mCommandEncoder = mDevice.CreateCommandEncoder(&encoderDescriptor);
}
FILAMENT_CHECK_PRECONDITION(mRenderPassEncoder == nullptr)
<< "Resolve cannot be called during an existing render pass";
const auto sourceTexture{ handleCast<WebGPUTexture>(sourceTextureHandle) };
@@ -1605,13 +1695,10 @@ void WebGPUDriver::bindPipeline(PipelineState const& pipelineState) {
};
wgpu::TextureFormat depthStencilFormat{ wgpu::TextureFormat::Undefined };
if (renderTarget->isDefaultRenderTarget()) {
// default render target color(s) (one)...
colorFormatCount = 1;
colorFormats[0] = mSwapChain->getColorFormat();
// default render target depth/stencil...
depthStencilFormat = mSwapChain->getDepthFormat();
} else {
// custom render target color(s)...
MRT const& mrtColorAttachments{ mCurrentRenderTarget->getColorAttachmentInfos() };
for (size_t i{ 0 }; i < MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT; ++i) {
if (mrtColorAttachments[i].handle) {
@@ -1677,7 +1764,7 @@ void WebGPUDriver::bindRenderPrimitive(Handle<HwRenderPrimitive> renderPrimitive
void WebGPUDriver::draw2(const uint32_t indexOffset, const uint32_t indexCount,
const uint32_t instanceCount) {
// We defer actually binding until we actually draw
// Bind groups are deferred until the actual draw call.
for (size_t i = 0; i < MAX_DESCRIPTOR_SET_COUNT; i++) {
auto& binding = mCurrentDescriptorSets[i];
if (binding.bindGroup) {
@@ -1747,10 +1834,8 @@ void WebGPUDriver::updateDescriptorSetTexture(Handle<HwDescriptorSet> descriptor
auto texture = handleCast<WebGPUTexture>(textureHandle);
if (!bindGroup->getIsLocked()) {
// Dawn will cache duplicate samplers, so we don't strictly need to maintain a cache.
// Making a cache might save us minor perf by reducing param translation
// Dawn will cache duplicate samplers, so we don't strictly need to maintain a cache here.
const auto sampler = makeSampler(params);
// TODO making assumptions that size and offset mean the same thing here.
wgpu::BindGroupEntry tEntry{
.binding = static_cast<uint32_t>(binding * 2),
.textureView = texture->getDefaultTextureView() };
@@ -1784,7 +1869,7 @@ void WebGPUDriver::bindDescriptorSet(Handle<HwDescriptorSet> descriptorSetHandle
const backend::descriptor_set_t setIndex, backend::DescriptorSetOffsetArray&& offsets) {
assert_invariant(setIndex < MAX_DESCRIPTOR_SET_COUNT);
// An empty handle signifies we need to release this bind point
// An empty handle unbinds the descriptor set.
if (!descriptorSetHandle) {
mCurrentDescriptorSets[setIndex] = {};
return;
@@ -1914,4 +1999,4 @@ wgpu::AddressMode WebGPUDriver::fWrapModeToWAddressMode(const SamplerWrapMode& f
return wgpu::AddressMode::Undefined;
}
} // namespace filament
} // namespace filament::backend
+7 -3
View File
@@ -47,8 +47,12 @@
namespace filament::backend {
class WebGPUSwapChain;
/**
* WebGPU backend (driver) implementation
* Implements the private backend driver API for WebGPU specifically (that API is essentially
* expressed in private/backend/DriverAPI.inc)
*
* It manages all the WebGPU resources necessary to accomplish this.
*/
class WebGPUDriver final : public DriverBase {
public:
@@ -64,8 +68,8 @@ private:
[[nodiscard]] wgpu::Sampler makeSampler(SamplerParams const& params);
[[nodiscard]] static wgpu::AddressMode fWrapModeToWAddressMode(const filament::backend::SamplerWrapMode& fUsage);
// the platform (e.g. OS) specific aspects of the WebGPU backend are strictly only
// handled in the WebGPUPlatform
// The platform (e.g. OS) specific aspects of the WebGPU backend are strictly only
// handled in the WebGPUPlatform.
WebGPUPlatform& mPlatform;
wgpu::Adapter mAdapter = nullptr;
wgpu::Device mDevice = nullptr;
+2 -3
View File
@@ -28,12 +28,11 @@ namespace filament::backend {
FenceStatus WebGPUFence::getStatus() { return mStatus.load(); }
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.
queue.OnSubmittedWorkDone(
wgpu::CallbackMode::AllowSpontaneous,
[this](const wgpu::QueueWorkDoneStatus status, wgpu::StringView message) {
// Note: The 'message' parameter is required by the function signature
// but can be ignored if not needed.
switch (status) {
case wgpu::QueueWorkDoneStatus::Success:
mStatus.store(FenceStatus::CONDITION_SATISFIED);
@@ -28,6 +28,11 @@ class Queue;
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 {
public:
[[nodiscard]] FenceStatus getStatus();
@@ -35,6 +40,9 @@ public:
void addMarkerToQueueState(wgpu::Queue const&);
private:
// The current status of the fence.
// This is atomic because it can be updated by a WebGPU callback thread and read from the main
// thread.
std::atomic<FenceStatus> mStatus{ FenceStatus::TIMEOUT_EXPIRED };
};
@@ -30,6 +30,10 @@ enum class IndexFormat : uint32_t;
namespace filament::backend {
/**
* A WebGPU implementation of the HwIndexBuffer.
* This class represents a GPU buffer that stores indices for indexed drawing.
*/
class WebGPUIndexBuffer final : public HwIndexBuffer, public WebGPUBufferBase {
public:
WebGPUIndexBuffer(wgpu::Device const&, uint8_t elementSize, uint32_t indexCount);
@@ -48,8 +48,8 @@ void resolveColorTextures(wgpu::CommandEncoder const& commandEncoder,
const wgpu::RenderPassEncoder renderPassEncoder{ commandEncoder.BeginRenderPass(
&renderPassDescriptor) };
FILAMENT_CHECK_POSTCONDITION(renderPassEncoder)
<< "Failed to create wgpu::RenderPassEncoder for WebGPUDriver::resolve";
renderPassEncoder.End(); // only the implicit resolve is happening in the pass
<< "Failed to create wgpu::RenderPassEncoder for MSAA resolve.";
renderPassEncoder.End(); // The resolve is implicit in the pass.
}
} // namespace
@@ -59,30 +59,29 @@ void WebGPUMsaaTextureResolver::resolve(ResolveRequest const& request) {
ResolveRequest::TextureInfo const& destination{ request.destination };
FILAMENT_CHECK_PRECONDITION(destination.texture.GetWidth() == source.texture.GetWidth() &&
destination.texture.GetHeight() == source.texture.GetHeight())
<< "invalid resolve: source and destination sizes don't match";
<< "Invalid resolve: source and destination sizes don't match.";
FILAMENT_CHECK_PRECONDITION(
source.texture.GetSampleCount() > 1 && destination.texture.GetSampleCount() == 1)
<< "invalid resolve: source.samples=" << source.texture.GetSampleCount()
<< "Invalid resolve: source.samples=" << source.texture.GetSampleCount()
<< ", destination.samples=" << destination.texture.GetSampleCount();
FILAMENT_CHECK_PRECONDITION(source.texture.GetFormat() == destination.texture.GetFormat())
<< "source and destination texture format don't match";
<< "Source and destination texture format don't match.";
const wgpu::TextureFormat format{ source.texture.GetFormat() };
FILAMENT_CHECK_PRECONDITION(!hasDepth(format)) << "can't resolve depth formats";
FILAMENT_CHECK_PRECONDITION(!hasStencil(format)) << "can't resolve stencil formats";
FILAMENT_CHECK_PRECONDITION(!hasDepth(format)) << "Can't resolve depth formats.";
FILAMENT_CHECK_PRECONDITION(!hasStencil(format)) << "Can't resolve stencil formats.";
FILAMENT_CHECK_PRECONDITION(source.texture.GetUsage() & wgpu::TextureUsage::RenderAttachment)
<< "source texture usage doesn't have wgpu::TextureUsage::RenderAttachment";
<< "Source texture usage doesn't have wgpu::TextureUsage::RenderAttachment.";
FILAMENT_CHECK_PRECONDITION(
destination.texture.GetUsage() & wgpu::TextureUsage::RenderAttachment)
<< "destination texture usage doesn't have wgpu::TextureUsage::RenderAttachment";
<< "Destination texture usage doesn't have wgpu::TextureUsage::RenderAttachment.";
FILAMENT_CHECK_PRECONDITION(destination.texture.GetUsage() & wgpu::TextureUsage::TextureBinding)
<< "destination texture usage doesn't have wgpu::TextureUsage::TextureBinding";
<< "Destination texture usage doesn't have wgpu::TextureUsage::TextureBinding.";
const wgpu::TextureViewDescriptor sourceTextureViewDescriptor{
.label = "resolve_source_texture_view",
@@ -116,7 +115,7 @@ void WebGPUMsaaTextureResolver::resolve(ResolveRequest const& request) {
<< "Failed to create wgpu::TextureView destinationTextureView.";
if (hasDepth(format)) {
PANIC_PRECONDITION("DEPTH RESOLVE NOT IMPLEMENTED YET");
PANIC_PRECONDITION("Depth resolve not yet implemented.");
} else {
resolveColorTextures(request.commandEncoder, sourceTextureView, destinationTextureView);
}
@@ -31,6 +31,10 @@ namespace filament::backend {
class WebGPUTexture;
/**
* A utility class for resolving multisampled textures in WebGPU.
* This is necessary because WebGPU does not have an implicit resolve step like other APIs.
*/
class WebGPUMsaaTextureResolver final {
public:
struct ResolveRequest final {
@@ -47,6 +51,9 @@ public:
TextureInfo destination;
};
/**
* Resolves a multisampled texture into a single-sampled texture.
*/
void resolve(ResolveRequest const&);
};
@@ -54,7 +54,7 @@ namespace {
case CullingMode::FRONT: return wgpu::CullMode::Front;
case CullingMode::BACK: return wgpu::CullMode::Back;
case CullingMode::FRONT_AND_BACK:
// no WegGPU equivalent of front and back
// WebGPU does not support culling both front and back faces simultaneously.
FILAMENT_CHECK_POSTCONDITION(false)
<< "WebGPU does not support CullingMode::FRONT_AND_BACK";
return wgpu::CullMode::Undefined;
@@ -220,7 +220,7 @@ wgpu::RenderPipeline WebGPUPipelineCache::createRenderPipeline(
const bool requestedDepth{ any(request.targetRenderFlags & TargetBufferFlags::DEPTH) };
const bool requestedStencil{ any(request.targetRenderFlags & TargetBufferFlags::STENCIL) };
const bool depthOrStencilRequested{ requestedDepth || requestedStencil };
// depth/stencil...
if (depthOrStencilRequested) {
FILAMENT_CHECK_PRECONDITION(request.depthStencilFormat != wgpu::TextureFormat::Undefined)
<< "Depth or Stencil requested for pipeline, but depthStencilFormat is "
@@ -312,6 +312,13 @@ wgpu::RenderPipeline WebGPUPipelineCache::createRenderPipeline(
},
.fragment = nullptr // will add below if fragment module is included
};
// TODO:
if (pipelineDescriptor.primitive.topology == wgpu::PrimitiveTopology::LineStrip ||
pipelineDescriptor.primitive.topology == wgpu::PrimitiveTopology::TriangleStrip) {
PANIC_POSTCONDITION("stripIndexFormat must be set for strip topologies. "
"This needs to be plumbed through from the RenderPrimitive.");
}
wgpu::FragmentState fragmentState = {};
const wgpu::BlendState blendState {
.color = {
@@ -34,6 +34,11 @@
namespace filament::backend {
/**
* A cache for WebGPU render pipelines.
* This class is responsible for creating and caching wgpu::RenderPipeline objects to avoid
* expensive pipeline creation at runtime.
*/
class WebGPUPipelineCache final {
public:
struct RenderPipelineRequest final {
@@ -63,6 +68,9 @@ public:
[[nodiscard]] wgpu::RenderPipeline const& getOrCreateRenderPipeline(
RenderPipelineRequest const&);
/**
* Should be called at the end of each frame to perform cache maintenance.
*/
void onFrameEnd();
private:
@@ -77,12 +77,13 @@ wgpu::PipelineLayout WebGPUPipelineLayoutCache::createPipelineLayout(
};
const wgpu::PipelineLayout layout{ mDevice.CreatePipelineLayout(&descriptor) };
FILAMENT_CHECK_POSTCONDITION(layout)
<< "Failed to create pipeline layout " << descriptor.label << "?";
<< "Failed to create pipeline layout " << descriptor.label << ".";
return layout;
}
bool WebGPUPipelineLayoutCache::PipelineLayoutKeyEqual::operator()(PipelineLayoutKey const& key1,
PipelineLayoutKey const& key2) const {
// Compare the raw bytes of the keys for equality.
return 0 == memcmp(reinterpret_cast<void const*>(&key1), reinterpret_cast<void const*>(&key2),
sizeof(key1));
}
@@ -93,10 +94,9 @@ void WebGPUPipelineLayoutCache::removeExpiredPipelineLayouts() {
PipelineLayoutCacheEntry const& entry{ iterator.value() };
if (mFrameCount > (entry.lastUsedFrameCount +
FILAMENT_WEBGPU_PIPELINE_LAYOUT_EXPIRATION_IN_FRAME_COUNT)) {
// pipeline layout expired...
// The pipeline layout has not been used recently, so we can remove it from the cache.
iterator = mPipelineLayouts.erase(iterator);
} else {
// pipeline layout not yet expired...
++iterator;
}
}
@@ -31,6 +31,11 @@
namespace filament::backend {
/**
* A cache for WebGPU pipeline layouts.
* This class is responsible for creating and caching wgpu::PipelineLayout objects to avoid
* expensive pipeline layout creation at runtime.
*/
class WebGPUPipelineLayoutCache final {
public:
struct PipelineLayoutRequest final {
@@ -45,9 +50,16 @@ public:
WebGPUPipelineLayoutCache& operator=(WebGPUPipelineLayoutCache const&) = delete;
WebGPUPipelineLayoutCache& operator=(WebGPUPipelineLayoutCache const&&) = delete;
/**
* Retrieves a pipeline layout from the cache or creates a new one if it doesn't exist.
* @return A constant reference to the cached or newly created pipeline layout.
*/
[[nodiscard]] wgpu::PipelineLayout const& getOrCreatePipelineLayout(
PipelineLayoutRequest const&);
/**
* Should be called at the end of each frame to perform cache maintenance.
*/
void onFrameEnd();
private:
@@ -59,10 +71,10 @@ private:
* instances, single bytes for booleans etc.), trivial copying and comparison (byte by byte),
* and a word-aligned structure with a size in bytes as a multiple of 4 (for murmer hash).
*/
struct PipelineLayoutKey final { // size : offset (need multiples of 4 bytes for hashing)
WGPUBindGroupLayout bindGroupLayoutHandles[MAX_DESCRIPTOR_SET_COUNT]{ nullptr }; // 32 : 0
uint8_t bindGroupLayoutCount{ 0 }; // 1 : 32
uint8_t padding[7]{ 0 }; // 7 : 33
struct PipelineLayoutKey final {
WGPUBindGroupLayout bindGroupLayoutHandles[MAX_DESCRIPTOR_SET_COUNT]{ nullptr }; // 32 :0
uint8_t bindGroupLayoutCount{ 0 }; // 1 :32
uint8_t padding[7]{ 0 }; // 7 :33
};
static_assert(sizeof(PipelineLayoutKey) == 40,
"PipelineLayoutKey must not have implicit padding.");
@@ -89,7 +89,7 @@ namespace {
std::string_view(sourceData + posAfterId, posEndOfStatement - posAfterId);
size_t posOfEqual = statementSegment.find('=');
if (posOfEqual == std::string::npos) {
// not an assignment statement, so stream to the end of the statement and continue...
// Not an assignment, so we don't need to replace it.
processedShaderSource << std::string_view(sourceData + pos,
posEndOfStatement + 1 - pos);
pos = posEndOfStatement + 1;
@@ -122,10 +122,7 @@ namespace {
}
const auto newValueItr = specConstants.find(static_cast<uint32_t>(constantId));
if (newValueItr == specConstants.end()) {
// not going to override the constant,
// as the specConstants parameter doesn't specify it. So, we will keep the default
// already in the source text
// (stream to the end of the statement)...
// The constant is not being overridden, so keep the default value.
processedShaderSource << std::string_view(sourceData + pos,
posEndOfStatement + 1 - pos);
pos = posEndOfStatement + 1;
@@ -174,7 +171,7 @@ namespace {
utils::FixedCapacityVector<uint8_t> const& sourceBytes =
shaderSource[static_cast<size_t>(stage)];
if (sourceBytes.empty()) {
return nullptr;// nothing to compile/create, the shader was not provided
return nullptr; // No shader source to compile.
}
std::stringstream labelStream;
labelStream << programName << " " << filamentShaderStageToString(stage) << " shader";
@@ -192,7 +189,8 @@ namespace {
};
const wgpu::ShaderModule shaderModule = device.CreateShaderModule(&descriptor);
const wgpu::Instance instance = device.GetAdapter().GetInstance();
// synchronously creates the shader module...
// Synchronously compile the shader module.
const wgpu::WaitStatus waitResult = instance.WaitAny(
shaderModule.GetCompilationInfo(wgpu::CallbackMode::WaitAnyOnly,
[&descriptor](auto const& status,
@@ -282,7 +280,7 @@ WebGPUProgram::WebGPUProgram(wgpu::Device const& device, Program const& program)
: HwProgram{ program.getName() } {
std::unordered_map<uint32_t, std::variant<int32_t, float, bool>> specConstants;
toMap(program.getSpecializationConstants(), specConstants);
// TODO consider creating/compiling these shaders in parallel
// TODO: Consider creating/compiling these shaders in parallel.
vertexShaderModule = createShaderModule(device, program, ShaderStage::VERTEX, specConstants);
fragmentShaderModule =
createShaderModule(device, program, ShaderStage::FRAGMENT, specConstants);
@@ -25,6 +25,10 @@ namespace filament::backend {
class Program;
/**
* A WebGPU implementation of the HwProgram.
* This class holds the compiled shader modules for a given program.
*/
class WebGPUProgram final : public HwProgram {
public:
WebGPUProgram(wgpu::Device const&, Program const&);
@@ -27,9 +27,8 @@ namespace filament::backend {
namespace {
// The shaders expect a single-sampled 2D texture
// with a format compatible with scalar sample format f32
// (this is checked with the getCompatibilityFor function)
// The shaders expect a single-sampled 2D texture with a format compatible with a scalar sample format of f32.
// This is checked with the getCompatibilityFor function.
constexpr uint32_t TEXTURE_BIND_GROUP_INDEX{ 0 };
constexpr size_t TEXTURE_BIND_GROUP_ENTRY_SIZE{ 2 }; // sampler and texture
constexpr uint32_t SAMPLER_BINDING_INDEX{ 0 };
@@ -85,7 +84,7 @@ constexpr std::string_view SHADER_SOURCE{ R"(
.maxAnisotropy = 1, // should not matter, just being consistently defined
};
const wgpu::Sampler sampler{ device.CreateSampler(&descriptor) };
FILAMENT_CHECK_POSTCONDITION(sampler) << "Failed to create previous mip level sampler?";
FILAMENT_CHECK_POSTCONDITION(sampler) << "Failed to create previous mip level sampler.";
return sampler;
}
@@ -98,7 +97,7 @@ constexpr std::string_view SHADER_SOURCE{ R"(
};
const wgpu::ShaderModule shaderModule{ device.CreateShaderModule(&shaderModuleDescriptor) };
FILAMENT_CHECK_POSTCONDITION(shaderModule)
<< "Failed to create shader module for render pass mipmap generation?";
<< "Failed to create shader module for render pass mipmap generation.";
return shaderModule;
}
@@ -131,7 +130,7 @@ constexpr std::string_view SHADER_SOURCE{ R"(
const wgpu::BindGroupLayout textureBindGroupLayout{ device.CreateBindGroupLayout(
&textureBindGroupLayoutDescriptor) };
FILAMENT_CHECK_POSTCONDITION(textureBindGroupLayout)
<< "Failed to create texture bind group layout for render pass mipmap generation?";
<< "Failed to create texture bind group layout for render pass mipmap generation.";
return textureBindGroupLayout;
}
@@ -145,7 +144,7 @@ constexpr std::string_view SHADER_SOURCE{ R"(
const wgpu::PipelineLayout pipelineLayout{ device.CreatePipelineLayout(
&pipelineLayoutDescriptor) };
FILAMENT_CHECK_POSTCONDITION(pipelineLayout)
<< "Failed to create pipeline layout for render pass mipmap generation?";
<< "Failed to create pipeline layout for render pass mipmap generation.";
return pipelineLayout;
}
@@ -194,7 +193,7 @@ constexpr std::string_view SHADER_SOURCE{ R"(
};
const wgpu::RenderPipeline pipeline{ device.CreateRenderPipeline(&pipelineDescriptor) };
FILAMENT_CHECK_POSTCONDITION(pipeline)
<< "Failed to create pipeline for render pass mipmap generation?";
<< "Failed to create pipeline for render pass mipmap generation.";
return pipeline;
}
@@ -210,8 +209,7 @@ WebGPURenderPassMipmapGenerator::WebGPURenderPassMipmapGenerator(wgpu::Device co
WebGPURenderPassMipmapGenerator::FormatCompatibility
WebGPURenderPassMipmapGenerator::getCompatibilityFor(const wgpu::TextureFormat format,
const wgpu::TextureDimension dimension, const uint32_t sampleCount) {
// check that the format is compatible (currently expects that the scalar sample type is
// f32)...
// Check that the format is compatible (currently expects that the scalar sample type is f32).
switch (format) {
case wgpu::TextureFormat::Depth16Unorm:
case wgpu::TextureFormat::Depth24Plus:
@@ -220,20 +218,16 @@ WebGPURenderPassMipmapGenerator::getCompatibilityFor(const wgpu::TextureFormat f
case wgpu::TextureFormat::Depth32FloatStencil8:
return {
.compatible = false,
.reason = "A depth texture format requires special sampler treatment and does "
"not "
"generally support linear filtering needed for render pass based "
"mipmap "
"generation, thus this texture is not supported, as it is a kind of "
"depth texture.",
.reason = "A depth texture format requires special sampler treatment and does not "
"generally support linear filtering needed for render pass based mipmap "
"generation.",
};
case wgpu::TextureFormat::Undefined:
case wgpu::TextureFormat::External:
return {
.compatible = false,
.reason = "Undefined or External textures are not supported for render pass "
"based "
"mipmap generation",
.reason = "Undefined or External textures are not supported for render pass based "
"mipmap generation.",
};
default:
const ScalarSampleType scalarSampleType{ getScalarSampleTypeFrom(format) };
@@ -242,21 +236,19 @@ WebGPURenderPassMipmapGenerator::getCompatibilityFor(const wgpu::TextureFormat f
return {
.compatible = false,
.reason = "The provided texture format requires an unsigned integer "
"sampler, which does not natively support linear filtering, "
"which is needed for render pass based mipmap generation.",
"sampler, which does not natively support linear filtering.",
};
case ScalarSampleType::I32:
return {
.compatible = false,
.reason = "The provided texture format requires a signed integer "
"sampler, which does not natively support linear filtering, "
"which is needed for render pass based mipmap generation.",
"sampler, which does not natively support linear filtering.",
};
case ScalarSampleType::F32:
break;
}
}
// check that the dimensionality is compatible (currently expects 2D textures)...
// Check that the dimensionality is compatible (currently expects 2D textures).
switch (dimension) {
case wgpu::TextureDimension::Undefined:
return {
@@ -282,7 +274,7 @@ WebGPURenderPassMipmapGenerator::getCompatibilityFor(const wgpu::TextureFormat f
case wgpu::TextureDimension::e2D:
break;
}
// check that the texture is single-sampled (for now)...
// Check that the texture is single-sampled.
if (sampleCount > 1) {
return {
.compatible = false,
@@ -427,7 +419,7 @@ void WebGPURenderPassMipmapGenerator::generateMipmaps(wgpu::Queue const& queue,
wgpu::Texture const& texture) {
const uint32_t mipLevelCount{ texture.GetMipLevelCount() };
if (mipLevelCount < 2) {
return; // nothing to do
return; // Nothing to do.
}
wgpu::RenderPipeline const& pipeline{ getOrCreatePipelineFor(texture.GetFormat()) };
const wgpu::CommandEncoderDescriptor commandEncoderDescriptor{
@@ -436,20 +428,19 @@ void WebGPURenderPassMipmapGenerator::generateMipmaps(wgpu::Queue const& queue,
const wgpu::CommandEncoder commandEncoder{ mDevice.CreateCommandEncoder(
&commandEncoderDescriptor) };
FILAMENT_CHECK_POSTCONDITION(commandEncoder)
<< "Failed to create command encoder for layer for render pass mipmap generation?";
<< "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);
}
}
// submit the command buffer...
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 layer for render pass mipmap generation?";
<< "Failed to create command buffer for render pass mipmap generation.";
queue.Submit(1, &commandBuffer);
}
@@ -472,7 +463,7 @@ void WebGPURenderPassMipmapGenerator::generateMipmap(wgpu::CommandEncoder const&
.usage = wgpu::TextureUsage::TextureBinding,
};
const wgpu::TextureView sourceView{ texture.CreateView(&sourceViewDescriptor) };
FILAMENT_CHECK_POSTCONDITION(pipeline)
FILAMENT_CHECK_POSTCONDITION(sourceView)
<< "Failed to create source texture view for layer " << layer << " and mip level "
<< mipLevel << " for render pass mipmap generation?";
const wgpu::TextureViewDescriptor destinationViewDescriptor{
@@ -487,7 +478,7 @@ void WebGPURenderPassMipmapGenerator::generateMipmap(wgpu::CommandEncoder const&
.usage = wgpu::TextureUsage::RenderAttachment,
};
const wgpu::TextureView destinationView{ texture.CreateView(&destinationViewDescriptor) };
FILAMENT_CHECK_POSTCONDITION(pipeline)
FILAMENT_CHECK_POSTCONDITION(destinationView)
<< "Failed to create destination texture view for layer " << layer << " and mip level "
<< mipLevel << " for render pass mipmap generation?";
// create the render pass...
@@ -508,7 +499,7 @@ void WebGPURenderPassMipmapGenerator::generateMipmap(wgpu::CommandEncoder const&
.entries = textureBindGroupEntries,
};
const wgpu::BindGroup textureBindGroup{ mDevice.CreateBindGroup(&textureBindGroupDescriptor) };
FILAMENT_CHECK_POSTCONDITION(pipeline)
FILAMENT_CHECK_POSTCONDITION(textureBindGroup)
<< "Failed to create texture bind group for layer " << layer << " and mip level "
<< mipLevel << " for render pass mipmap generation?";
const wgpu::RenderPassColorAttachment colorAttachment{
@@ -534,8 +525,7 @@ void WebGPURenderPassMipmapGenerator::generateMipmap(wgpu::CommandEncoder const&
<< mipLevel << " for render pass mipmap generation?";
renderPassEncoder.SetPipeline(pipeline);
renderPassEncoder.SetBindGroup(TEXTURE_BIND_GROUP_INDEX, textureBindGroup);
renderPassEncoder.Draw(3); // draw the full-screen triangle
// with hard-coded vertices in the shader
renderPassEncoder.Draw(3); // Draw a full-screen triangle.
renderPassEncoder.End();
}
@@ -27,6 +27,9 @@
namespace filament::backend {
/**
* A utility class for generating mipmaps for a texture using a series of render passes.
*/
class WebGPURenderPassMipmapGenerator final {
public:
struct FormatCompatibility final {
@@ -34,6 +37,11 @@ public:
std::string_view reason;
};
/**
* Checks if a given texture format is compatible with render pass-based mipmap generation.
* @return A FormatCompatibility struct indicating whether the format is compatible
* and a reason if not.
*/
[[nodiscard]] static FormatCompatibility getCompatibilityFor(wgpu::TextureFormat,
wgpu::TextureDimension, uint32_t sampleCount);
@@ -24,6 +24,10 @@ namespace filament::backend {
class WebGPUVertexBuffer;
class WebGPUIndexBuffer;
/**
* A WebGPU implementation of the HwRenderPrimitive.
* This struct holds the vertex and index buffers that define a renderable primitive.
*/
struct WebGPURenderPrimitive : public HwRenderPrimitive {
WebGPUVertexBuffer* vertexBuffer = nullptr;
WebGPUIndexBuffer* indexBuffer = nullptr;
@@ -41,9 +41,9 @@ namespace filament::backend {
namespace {
/**
* Panics if the number of samples in the original attachment textures do not match.
* @return The number of samples in the original attachment textures (the number/count in each one).
* Returns 0 if there are no attachments.
* Creates MSAA sidecar textures for attachments if necessary.
* This function also verifies that all attachments have the same sample count.
* @return The number of samples per attachment.
*/
[[nodiscard]] uint8_t createMsaaSidecarTextures(const uint8_t renderTargetSampleCount,
const TargetBufferFlags targetFlags, MRT const& colorAttachments,
@@ -102,7 +102,7 @@ namespace {
}
FILAMENT_CHECK_PRECONDITION(texture->samples == sampleCountPerAttachment)
<< target.name << " attachment texture has " << +texture->samples
<< " but the other attachment(s) have " << +sampleCountPerAttachment;
<< " samples, but the other attachment(s) have " << +sampleCountPerAttachment << ".";
if (renderTargetSampleCount > 1 && sampleCountPerAttachment == 1) {
texture->createMsaaSidecarTextureIfNotAlreadyCreated(renderTargetSampleCount,
device);
@@ -136,7 +136,7 @@ WebGPURenderTarget::WebGPURenderTarget(const uint32_t width, const uint32_t heig
mColorAttachmentDesc.reserve(MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT);
}
// Default constructor for the default render target
// Constructor for the default render target
WebGPURenderTarget::WebGPURenderTarget()
: HwRenderTarget{ 0, 0 },
mDefaultRenderTarget{ true },
@@ -182,19 +182,19 @@ void WebGPURenderTarget::setUpRenderPassAttachments(wgpu::RenderPassDescriptor&
FILAMENT_CHECK_PRECONDITION(std::all_of(customColorMsaaSidecarTextureViews,
customColorMsaaSidecarTextureViews + customColorTextureViewCount,
[](wgpu::TextureView const& msaaView) { return msaaView != nullptr; }))
<< "A color or depth/stencil attachment texture has a MSAA sidecar but at least "
<< "A color or depth/stencil attachment texture has an MSAA sidecar but at least "
"one other color attachment texture does not.";
FILAMENT_CHECK_PRECONDITION(customDepthStencilMsaaSidecarTextureView != nullptr)
<< "The color attachment texture(s) have MSAA sidecar(s) but the depth/stencil "
<< "The color attachment texture(s) have MSAA sidecars but the depth/stencil "
"texture does not.";
} else {
FILAMENT_CHECK_PRECONDITION(std::all_of(customColorMsaaSidecarTextureViews,
customColorMsaaSidecarTextureViews + customColorTextureViewCount,
[](wgpu::TextureView const& msaaView) { return msaaView == nullptr; }))
<< "A color or depth/stencil attachment texture does not have a MSAA sidecar but "
<< "A color or depth/stencil attachment texture does not have an MSAA sidecar but "
"at least one color attachment texture does.";
FILAMENT_CHECK_PRECONDITION(customDepthStencilMsaaSidecarTextureView == nullptr)
<< "Custom color textures for the render target do not have MSAA sidecar(s) but "
<< "Custom color textures for the render target do not have MSAA sidecars but "
"the depth/stencil texture does.";
}
@@ -31,6 +31,10 @@
namespace filament::backend {
/**
* A WebGPU implementation of the HwRenderTarget.
* This class represents a collection of attachments (textures) that can be rendered to.
*/
class WebGPURenderTarget : public HwRenderTarget {
public:
using Attachment = TargetBufferInfo; // Using TargetBufferInfo directly for attachments
@@ -38,7 +42,8 @@ public:
WebGPURenderTarget(uint32_t width, uint32_t height, uint8_t samples, uint8_t layerCount,
MRT const& colorAttachments, Attachment const& depthAttachment,
Attachment const& stencilAttachment, TargetBufferFlags const& targetFlags,
std::function<WebGPUTexture*(const Handle<HwTexture>)> const&, wgpu::Device const&);
std::function<WebGPUTexture*(const Handle<HwTexture>)> const&,
wgpu::Device const&);
// Default constructor for the default render target
WebGPURenderTarget();
@@ -80,7 +85,7 @@ private:
MRT mColorAttachments{};
// TODO WebGPU only supports a DepthStencil attachment, should this be just
// mDepthStencilAttachment?
// mDepthStencilAttachment?
Attachment mDepthAttachment{};
Attachment mStencilAttachment{};
@@ -187,7 +187,8 @@ void initConfig(wgpu::SurfaceConfiguration& config, wgpu::Device const& device,
wgpu::SurfaceCapabilities const& capabilities, wgpu::Extent2D const& extent,
bool useSRGBColorSpace) {
config.device = device;
config.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
config.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc |
wgpu::TextureUsage::TextureBinding;
config.width = extent.width;
config.height = extent.height;
config.format = selectColorFormat(capabilities.formatCount, capabilities.formats, useSRGBColorSpace);
@@ -269,8 +270,6 @@ WebGPUSwapChain::WebGPUSwapChain(wgpu::Surface&& surface, wgpu::Extent2D const&
const bool useSRGBColorSpace = (flags & SWAP_CHAIN_CONFIG_SRGB_COLORSPACE) != 0;
initConfig(mConfig, device, capabilities, extent, useSRGBColorSpace);
mDepthFormat = selectDepthFormat(device.HasFeature(wgpu::FeatureName::Depth32FloatStencil8),
mNeedStencil);
#if FWGPU_ENABLED(FWGPU_PRINT_SYSTEM)
printSurfaceConfiguration(mConfig, mDepthFormat);
#endif
@@ -297,8 +296,6 @@ WebGPUSwapChain::WebGPUSwapChain(wgpu::Extent2D const& extent,
mConfig.presentMode = wgpu::PresentMode::Fifo;
mConfig.alphaMode = wgpu::CompositeAlphaMode::Auto;
mDepthFormat = selectDepthFormat(device.HasFeature(wgpu::FeatureName::Depth32FloatStencil8),
mNeedStencil);
const wgpu::TextureDescriptor textureDescriptor = {
.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc,
.dimension = wgpu::TextureDimension::e2D,
+2 -2
View File
@@ -78,9 +78,9 @@ public:
/**
* @param samples The number of samples the underlying texture supports
* @param mipLevel The mip level into the underyling texture for which this view will reference
* @param mipLevel The mip level into the underlying texture for which this view will reference
* (this view will only have one mip level)
* @param arrayLayer The layer into the underyling texture for which this view will reference
* @param arrayLayer The layer into the underlying texture for which this view will reference
* (this view will only have one layer)
* @return A texture view for the MSAA sidecar texture
*/
@@ -534,6 +534,22 @@ namespace filament::backend {
}
}
/**
* Todo: should this take into account sRGB/linear when determining if conversion is necessary?
* For instance, if the output format is the same as the input, except the output is sRGB
* do we really need to do a conversion? If the answer is no, we should do that check here
* and return conversionNecessary false in that case.
* However, doing a straight-forward comparison is the safest most conservative thing to do
* for functional correctness and NOT doing a conversion in such cases could be considered
* an optimization. Thus, consider the optimization when we have better test coverage to
* experiment with such a refactor.
* @return True if theres a format mismatch
*/
[[nodiscard]] constexpr bool conversionNecessary(const wgpu::TextureFormat source,
const wgpu::TextureFormat destination, const PixelDataType pixelDataType) {
return source != destination && pixelDataType != PixelDataType::COMPRESSED;
}
} // namespace filament::backend
#endif // TNT_FILAMENT_BACKEND_WEBGPUTEXTUREHELPERS_H
+1 -1
View File
@@ -49,7 +49,7 @@ Backend parseArgumentsForBackend(int argc, char* argv[]) {
} else if (arg == "webgpu") {
backend = Backend::WEBGPU;
} else {
std::cerr << "Unrecognized target API. Must be 'opengl'|'vulkan'|'metal'."
std::cerr << "Unrecognized target API. Must be 'opengl'|'vulkan'|'metal'|'webgpu'."
<< std::endl
<< "Defaulting to OpenGL."
<< std::endl;
+6 -11
View File
@@ -145,7 +145,6 @@ static void createFaces(DriverApi& dapi, Handle<HwTexture> texture, int baseWidt
TEST_F(BlitTest, ColorMagnify) {
auto& api = getDriverApi();
mCleanup.addPostCall([&]() { executeCommands(); });
constexpr int kSrcTexWidth = 256;
constexpr int kSrcTexHeight = 256;
@@ -207,7 +206,6 @@ TEST_F(BlitTest, ColorMagnify) {
TEST_F(BlitTest, ColorMinify) {
auto& api = getDriverApi();
mCleanup.addPostCall([&]() { executeCommands(); });
constexpr int kSrcTexWidth = 1024;
constexpr int kSrcTexHeight = 1024;
@@ -342,7 +340,6 @@ TEST_F(BlitTest, ColorResolve) {
TEST_F(BlitTest, Blit2DTextureArray) {
auto& api = getDriverApi();
mCleanup.addPostCall([&]() { executeCommands(); });
api.startCapture(0);
mCleanup.addPostCall([&]() { api.stopCapture(0); });
@@ -412,7 +409,6 @@ TEST_F(BlitTest, Blit2DTextureArray) {
TEST_F(BlitTest, BlitRegion) {
auto& api = getDriverApi();
mCleanup.addPostCall([&]() { executeCommands(); });
constexpr int kSrcTexWidth = 1024;
constexpr int kSrcTexHeight = 1024;
@@ -488,7 +484,6 @@ TEST_F(BlitTest, BlitRegion) {
TEST_F(BlitTest, BlitRegionToSwapChain) {
FAIL_IF(Backend::VULKAN, "Crashes due to not finding color attachment, see b/417481493");
auto& api = getDriverApi();
mCleanup.addPostCall([&]() { executeCommands(); });
constexpr int kSrcTexWidth = 1024;
constexpr int kSrcTexHeight = 1024;
@@ -541,13 +536,13 @@ TEST_F(BlitTest, BlitRegionToSwapChain) {
dstRect, srcRenderTargets[srcLevel],
srcRect, SamplerMagFilter::LINEAR);
api.commit(swapChain);
}
api.commit(swapChain);
// TODO: for some reason, this test has been disabled. It needs to be tested on all
// machines.
// EXPECT_IMAGE(dstRenderTarget,
// ScreenshotParams(kDstTexWidth, kDstTexHeight, "BlitRegionToSwapChain", 0x0));
// TODO: for some reason, this test has been disabled. It needs to be tested on all
// machines.
// EXPECT_IMAGE(dstRenderTarget,
// ScreenshotParams(kDstTexWidth, kDstTexHeight, "BlitRegionToSwapChain", 0x0));
}
}
} // namespace test
+38 -34
View File
@@ -91,6 +91,7 @@ TEST_F(BufferUpdatesTest, VertexBufferUpdate) {
shader.bindUniform<SimpleMaterialParams>(api, ubuffer, kBindingConfig);
api.startCapture(0);
cleanup.addPostCall([&]() { api.stopCapture(0); });
// Upload the uniform, but with an offset to accommodate the padding in the shader's
// uniform definition.
@@ -101,29 +102,42 @@ TEST_F(BufferUpdatesTest, VertexBufferUpdate) {
});
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
{
RenderFrame frame(api);
// Draw 10 triangles, updating the vertex buffer / index buffer each time.
size_t triangleIndex = 0;
for (float i = -1.0f; i < 1.0f; i += 0.2f) {
const float low = i, high = i + 0.2;
const filament::math::float2 v[3]{{ low, low },
{ high, low },
{ low, high }};
triangle.updateVertices(v);
// Draw 10 triangles, updating the vertex buffer / index buffer each time.
size_t triangleIndex = 0;
for (float i = -1.0f; i < 1.0f; i += 0.2f) {
const float low = i, high = i + 0.2;
const filament::math::float2 v[3]{{ low, low },
{ high, low },
{ low, high }};
triangle.updateVertices(v);
if (updateIndices) {
if (triangleIndex % 2 == 0) {
// Upload each index separately, to test offsets.
const TrianglePrimitive::index_type i[3]{ 0, 1, 2 };
triangle.updateIndices(i + 0, 1, 0);
triangle.updateIndices(i + 1, 1, 1);
triangle.updateIndices(i + 2, 1, 2);
} else {
// This effectively hides this triangle.
const TrianglePrimitive::index_type i[3]{ 0, 0, 0 };
triangle.updateIndices(i);
if (updateIndices) {
if (triangleIndex % 2 == 0) {
// Upload each index separately, to test offsets.
const TrianglePrimitive::index_type i[3]{ 0, 1, 2 };
triangle.updateIndices(i + 0, 1, 0);
triangle.updateIndices(i + 1, 1, 1);
triangle.updateIndices(i + 2, 1, 2);
} else {
// This effectively hides this triangle.
const TrianglePrimitive::index_type i[3]{ 0, 0, 0 };
triangle.updateIndices(i);
}
}
if (triangleIndex > 0) {
params.flags.clear = TargetBufferFlags::NONE;
params.flags.discardStart = TargetBufferFlags::NONE;
}
api.beginRenderPass(defaultRenderTarget, params);
api.draw(state, triangle.getRenderPrimitive(), 0, 3, 1);
api.endRenderPass();
triangleIndex++;
}
if (triangleIndex > 0) {
@@ -141,15 +155,7 @@ TEST_F(BufferUpdatesTest, VertexBufferUpdate) {
triangleIndex++;
}
api.flush();
api.commit(swapChain);
api.endFrame(0);
api.stopCapture(0);
}
executeCommands();
}
// This test renders two triangles in two separate draw calls. Between the draw calls, a uniform
@@ -241,15 +247,13 @@ TEST_F(BufferUpdatesTest, BufferObjectUpdateWithOffset) {
"BufferObjectUpdateWithOffset", 2320747245));
api.flush();
api.commit(swapChain);
api.endFrame(0);
{
RenderFrame frame(api);
api.commit(swapChain);
}
// This ensures all driver commands have finished before exiting the test.
api.finish();
executeCommands();
getDriver().purge();
}
} // namespace test
+85 -78
View File
@@ -29,58 +29,61 @@ TEST_F(BackendTest, FrameScheduledCallback) {
SKIP_IF(Backend::VULKAN, "Frame callbacks are unsupported in Vulkan, see b/417254479");
SKIP_IF(Backend::WEBGPU, "Frame callbacks are unsupported in WebGPU");
auto& api = getDriverApi();
Cleanup cleanup(api);
// Create a SwapChain.
// In order for the frameScheduledCallback to be called, this must be a real SwapChain (not
// headless) so we obtain a drawable.
auto swapChain = cleanup.add(createSwapChain());
Handle<HwRenderTarget> renderTarget = cleanup.add(api.createDefaultRenderTarget());
int callbackCountA = 0;
api.setFrameScheduledCallback(swapChain, nullptr, [&callbackCountA](PresentCallable callable) {
callable();
callbackCountA++;
}, 0);
// Render the first frame.
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
api.beginRenderPass(renderTarget, {});
api.endRenderPass(0);
api.commit(swapChain);
api.endFrame(0);
// Render the next frame. The same callback should be called.
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
api.beginRenderPass(renderTarget, {});
api.endRenderPass(0);
api.commit(swapChain);
api.endFrame(0);
// Now switch out the callback.
int callbackCountB = 0;
api.setFrameScheduledCallback(swapChain, nullptr, [&callbackCountB](PresentCallable callable) {
callable();
callbackCountB++;
}, 0);
{
auto& api = getDriverApi();
Cleanup cleanup(api);
cleanup.addPostCall([&]() { executeCommands(); });
cleanup.addPostCall([&]() { getDriver().purge(); });
// Render one final frame.
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
api.beginRenderPass(renderTarget, {});
api.endRenderPass(0);
api.commit(swapChain);
api.endFrame(0);
// Create a SwapChain.
// In order for the frameScheduledCallback to be called, this must be a real SwapChain (not
// headless) so we obtain a drawable.
auto swapChain = cleanup.add(createSwapChain());
api.finish();
Handle<HwRenderTarget> renderTarget = cleanup.add(api.createDefaultRenderTarget());
executeCommands();
getDriver().purge();
api.setFrameScheduledCallback(swapChain, nullptr, [&callbackCountA](PresentCallable callable) {
callable();
callbackCountA++;
}, 0);
// Render the first frame.
api.makeCurrent(swapChain, swapChain);
{
RenderFrame frame(api);
api.beginRenderPass(renderTarget, {});
api.endRenderPass(0);
api.commit(swapChain);
}
// Render the next frame. The same callback should be called.
api.makeCurrent(swapChain, swapChain);
{
RenderFrame frame(api);
api.beginRenderPass(renderTarget, {});
api.endRenderPass(0);
api.commit(swapChain);
}
// Now switch out the callback.
api.setFrameScheduledCallback(swapChain, nullptr, [&callbackCountB](PresentCallable callable) {
callable();
callbackCountB++;
}, 0);
// Render one final frame.
api.makeCurrent(swapChain, swapChain);
{
RenderFrame frame(api);
api.beginRenderPass(renderTarget, {});
api.endRenderPass(0);
api.commit(swapChain);
}
api.finish();
}
EXPECT_EQ(callbackCountA, 2);
EXPECT_EQ(callbackCountB, 1);
}
@@ -90,43 +93,47 @@ TEST_F(BackendTest, FrameCompletedCallback) {
SKIP_IF(Backend::VULKAN, "Frame callbacks are unsupported in Vulkan, see b/417254479");
SKIP_IF(Backend::WEBGPU, "Frame callbacks are unsupported in WebGPU");
auto& api = getDriverApi();
Cleanup cleanup(api);
// Create a SwapChain.
auto swapChain = cleanup.add(api.createSwapChainHeadless(256, 256, 0));
int callbackCountA = 0;
api.setFrameCompletedCallback(swapChain, nullptr,
[&callbackCountA]() { callbackCountA++; });
// Render the first frame.
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
api.commit(swapChain);
api.endFrame(0);
// Render the next frame. The same callback should be called.
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
api.commit(swapChain);
api.endFrame(0);
// Now switch out the callback.
int callbackCountB = 0;
api.setFrameCompletedCallback(swapChain, nullptr,
[&callbackCountB]() { callbackCountB++; });
{
auto& api = getDriverApi();
Cleanup cleanup(api);
cleanup.addPostCall([&]() { executeCommands(); });
cleanup.addPostCall([&]() { getDriver().purge(); });
// Render one final frame.
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
api.commit(swapChain);
api.endFrame(0);
// Create a SwapChain.
auto swapChain = cleanup.add(api.createSwapChainHeadless(256, 256, 0));
api.finish();
api.setFrameCompletedCallback(swapChain, nullptr,
[&callbackCountA]() { callbackCountA++; });
executeCommands();
getDriver().purge();
// Render the first frame.
api.makeCurrent(swapChain, swapChain);
{
RenderFrame frame(api);
api.commit(swapChain);
}
// Render the next frame. The same callback should be called.
api.makeCurrent(swapChain, swapChain);
{
RenderFrame frame(api);
api.commit(swapChain);
}
// Now switch out the callback.
api.setFrameCompletedCallback(swapChain, nullptr,
[&callbackCountB]() { callbackCountB++; });
// Render one final frame.
api.makeCurrent(swapChain, swapChain);
{
RenderFrame frame(api);
api.commit(swapChain);
}
api.finish();
}
EXPECT_EQ(callbackCountA, 2);
EXPECT_EQ(callbackCountB, 1);
+8 -7
View File
@@ -158,7 +158,10 @@ TEST_F(BackendTest, FeedbackLoops) {
PixelBufferDescriptor pb(buffer, size, PixelDataFormat::RGBA, PixelDataType::UBYTE, cb);
api.update3DImage(texture, 0, 0, 0, 0, kTexWidth, kTexHeight, 1, std::move(pb));
for (int frame = 0; frame < kNumFrames; frame++) {
for (int frameCount = 0; frameCount < kNumFrames; frameCount++) {
Cleanup frameCleanup(api);
frameCleanup.addPostCall([&]() { executeCommands(); });
frameCleanup.addPostCall([&]() { getDriver().purge(); });
// Prep for rendering.
PipelineState state = getColorWritePipelineState();
@@ -191,9 +194,9 @@ TEST_F(BackendTest, FeedbackLoops) {
};
shader.bindUniform<MaterialParams>(api, ubuffer, uniformBinding);
shader.uploadUniform(api, ubuffer, uniformBinding, MaterialParams{
.fbWidth = float(params.viewport.width),
.fbHeight = float(params.viewport.height),
.sourceLevel = float(sourceLevel),
.fbWidth = float(params.viewport.width),
.fbHeight = float(params.viewport.height),
.sourceLevel = float(sourceLevel),
});
api.beginRenderPass(renderTargets[targetLevel], params);
@@ -246,7 +249,7 @@ TEST_F(BackendTest, FeedbackLoops) {
//
// NOTE: Calling glReadPixels on any miplevel other than the base level
// seems to be un-reliable on some GPU's.
if (frame == kNumFrames - 1) {
if (frameCount == kNumFrames - 1) {
EXPECT_IMAGE(renderTargets[0],
ScreenshotParams(kTexWidth, kTexHeight, "FeedbackLoops", 4192780705));
}
@@ -255,8 +258,6 @@ TEST_F(BackendTest, FeedbackLoops) {
api.commit(swapChain);
api.endFrame(0);
api.finish();
executeCommands();
getDriver().purge();
}
}
}
+48 -53
View File
@@ -48,7 +48,7 @@ layout(location = 0) out vec4 fragColor;
// Filament's Vulkan backend requires a descriptor set index of 1 for all samplers.
// This parameter is ignored for other backends.
layout(location = 0, set = 0) uniform {samplerType} test_tex;
layout(binding = 0, set = 0) uniform {samplerType} test_tex;
void main() {
vec2 fbsize = vec2({texSize});
@@ -66,7 +66,7 @@ std::string fragmentUpdateImage3DTemplate (R"(#version 450 core
layout(location = 0) out vec4 fragColor;
// Filament's Vulkan backend requires a descriptor set index of 1 for all samplers.
layout(location = 0, set = 0) uniform {samplerType} test_tex;
layout(binding = 0, set = 0) uniform {samplerType} test_tex;
float getLayer(in sampler3D s) { return 2.5f / 4.0f; }
float getLayer(in sampler2DArray s) { return 2.0f; }
@@ -87,7 +87,7 @@ std::string fragmentUpdateImageMip (R"(#version 450 core
layout(location = 0) out vec4 fragColor;
// Filament's Vulkan backend requires a descriptor set index of 1 for all samplers.
layout(location = 0, set = 0) uniform sampler2D test_tex;
layout(binding = 0, set = 0) uniform sampler2D test_tex;
void main() {
vec2 fbsize = vec2({texSize});
@@ -271,15 +271,18 @@ TEST_F(LoadImageTest, UpdateImage2D) {
// Test format conversion.
// TODO: Vulkan crashes with `Texture at colorAttachment[0] has usage (0x01) which doesn't specify MTLTextureUsageRenderTarget (0x04)'
// TODO: WebGPU Crashes with "destination texture usage doesn't have wgpu::TextureUsage::RenderAttachment"
testCases.emplace_back("RGBA FLOAT to RGBA16F", PixelDataFormat::RGBA, PixelDataType::FLOAT, TextureFormat::RGBA16F);
// Test texture formats not all backends support natively.
// TODO: Vulkan crashes with "VK_FORMAT_R32G32B32_SFLOAT is not supported"
// TODO: WebGPU Crashes with "destination texture usage doesn't have wgpu::TextureUsage::RenderAttachment"
testCases.emplace_back("RGB FLOAT to RGB32F", PixelDataFormat::RGB, PixelDataType::FLOAT, TextureFormat::RGB32F);
testCases.emplace_back("RGB FLOAT to RGB16F", PixelDataFormat::RGB, PixelDataType::FLOAT, TextureFormat::RGB16F);
// Test packed format uploads.
// TODO: Vulkan crashes with "Texture at colorAttachment[0] has usage (0x01) which doesn't specify MTLTextureUsageRenderTarget (0x04)"
// TODO: WebGPU Crashes with "destination texture usage doesn't have wgpu::TextureUsage::RenderAttachment"
testCases.emplace_back("RGBA UINT_2_10_10_10_REV to RGB10_A2", PixelDataFormat::RGBA, PixelDataType::UINT_2_10_10_10_REV, TextureFormat::RGB10_A2);
testCases.emplace_back("RGB UINT_10F_11F_11F_REV to R11F_G11F_B10F", PixelDataFormat::RGB, PixelDataType::UINT_10F_11F_11F_REV, TextureFormat::R11F_G11F_B10F);
testCases.emplace_back("RGB HALF to R11F_G11F_B10F", PixelDataFormat::RGB, PixelDataType::HALF, TextureFormat::R11F_G11F_B10F);
@@ -287,9 +290,12 @@ TEST_F(LoadImageTest, UpdateImage2D) {
// Test integer format uploads.
// TODO: These cases fail on OpenGL and Vulkan.
// TODO: These cases now also fail on Metal, but at some point previously worked.
testCases.emplace_back("RGB_INTEGER UBYTE to RGB8UI", PixelDataFormat::RGB_INTEGER, PixelDataType::UBYTE, TextureFormat::RGB8UI);
testCases.emplace_back("RGB_INTEGER USHORT to RGB16UI", PixelDataFormat::RGB_INTEGER, PixelDataType::USHORT, TextureFormat::RGB16UI);
testCases.emplace_back("RGB_INTEGER INT to RGB32I", PixelDataFormat::RGB_INTEGER, PixelDataType::INT, TextureFormat::RGB32I);
// TODO: These cases fail for WebGPU. However, leaving them causes
// Tint Reader Error: error: sampled image must have float component type
// Beginning SpirV-output dump with ret 0
// testCases.emplace_back("RGB_INTEGER UBYTE to RGB8UI", PixelDataFormat::RGB_INTEGER, PixelDataType::UBYTE, TextureFormat::RGB8UI);
// testCases.emplace_back("RGB_INTEGER USHORT to RGB16UI", PixelDataFormat::RGB_INTEGER, PixelDataType::USHORT, TextureFormat::RGB16UI);
// testCases.emplace_back("RGB_INTEGER INT to RGB32I", PixelDataFormat::RGB_INTEGER, PixelDataType::INT, TextureFormat::RGB32I);
// Test uploads with buffer padding.
// TODO: Vulkan crashes with "Assertion failed: (offset + size <= allocationSize)"
@@ -308,15 +314,19 @@ TEST_F(LoadImageTest, UpdateImage2D) {
api.startCapture();
Cleanup cleanup(api);
cleanup.addPostCall([&]() { api.finish(); });
cleanup.addPostCall([&]() { api.stopCapture(); });
// The test is executed within this block scope to force destructors to run before
// executeCommands().
for (const auto& t : testCases) {
Cleanup cleanup(api);
Cleanup caseCleanup(api);
// Create a platform-specific SwapChain and make it current.
auto swapChain = cleanup.add(createSwapChain());
auto swapChain = caseCleanup.add(createSwapChain());
api.makeCurrent(swapChain, swapChain);
auto defaultRenderTarget = cleanup.add(api.createDefaultRenderTarget(0));
auto defaultRenderTarget = caseCleanup.add(api.createDefaultRenderTarget(0));
// Create a program.
filament::SamplerInterfaceBlock::SamplerInfo samplerInfo { "test", "tex", 0,
@@ -380,8 +390,6 @@ TEST_F(LoadImageTest, UpdateImage2D) {
api.commit(swapChain);
api.endFrame(0);
}
api.stopCapture();
}
TEST_F(LoadImageTest, UpdateImageSRGB) {
@@ -392,6 +400,7 @@ TEST_F(LoadImageTest, UpdateImageSRGB) {
auto& api = getDriverApi();
Cleanup cleanup(api);
api.startCapture();
cleanup.addPostCall([&]() { api.stopCapture(); });
PixelDataFormat const pixelFormat = PixelDataFormat::RGBA;
PixelDataType const pixelType = PixelDataType::UBYTE;
@@ -439,37 +448,36 @@ TEST_F(LoadImageTest, UpdateImageSRGB) {
api.update3DImage(texture, 0, 0, 0, 0, kTexSize, kTexSize, 1, std::move(descriptor));
api.beginFrame(0, 0, 0);
{
RenderFrame frame(api);
// Update samplers.
DescriptorSetHandle descriptorSet = shader.createDescriptorSet(api);
api.updateDescriptorSetTexture(descriptorSet, 0, texture, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
});
// Update samplers.
DescriptorSetHandle descriptorSet = shader.createDescriptorSet(api);
api.updateDescriptorSetTexture(descriptorSet, 0, texture, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
});
api.bindDescriptorSet(descriptorSet, 0, {});
api.bindDescriptorSet(descriptorSet, 0, {});
RenderPassParams params = getClearColorRenderPass();
params.viewport.width = kTexSize;
params.viewport.height = kTexSize;
PipelineState state = getColorWritePipelineState();
shader.addProgramToPipelineState(state);
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = mTriangle.getVertexBufferInfo();
api.beginRenderPass(defaultRenderTarget, params);
api.bindPipeline(state);
api.bindRenderPrimitive(mTriangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
RenderPassParams params = getClearColorRenderPass();
params.viewport.width = kTexSize;
params.viewport.height = kTexSize;
PipelineState state = getColorWritePipelineState();
shader.addProgramToPipelineState(state);
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = mTriangle.getVertexBufferInfo();
api.beginRenderPass(defaultRenderTarget, params);
api.bindPipeline(state);
api.bindRenderPrimitive(mTriangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
EXPECT_IMAGE(defaultRenderTarget,
ScreenshotParams(kTexSize, kTexSize, "UpdateImageSRGB", 3300305265));
EXPECT_IMAGE(defaultRenderTarget,
ScreenshotParams(kTexSize, kTexSize, "UpdateImageSRGB", 3300305265));
api.commit(swapChain);
api.endFrame(0);
api.stopCapture();
api.commit(swapChain);
}
}
TEST_F(LoadImageTest, UpdateImageMipLevel) {
@@ -480,6 +488,7 @@ TEST_F(LoadImageTest, UpdateImageMipLevel) {
auto& api = getDriverApi();
Cleanup cleanup(api);
api.startCapture();
cleanup.addPostCall([&]() { api.stopCapture(); });
PixelDataFormat pixelFormat = PixelDataFormat::RGBA;
PixelDataType pixelType = PixelDataType::HALF;
@@ -512,17 +521,7 @@ TEST_F(LoadImageTest, UpdateImageMipLevel) {
PixelBufferDescriptor descriptor = checkerboardPixelBuffer(pixelFormat, pixelType, kTexSize);
api.update3DImage(texture, /* level*/ 1, 0, 0, 0, kTexSize, kTexSize, 1, std::move(descriptor));
api.beginFrame(0, 0, 0);
// Update samplers.
DescriptorSetHandle descriptorSet = shader.createDescriptorSet(api);
api.updateDescriptorSetTexture(descriptorSet, 0, texture, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
});
api.bindDescriptorSet(descriptorSet, 0, {});
RenderFrame outerFrame(api);
{
RenderFrame frame(api);
RenderPassParams params = getClearColorRenderPass();
@@ -543,9 +542,6 @@ TEST_F(LoadImageTest, UpdateImageMipLevel) {
ScreenshotParams(kTexSize, kTexSize, "UpdateImageMipLevel", 1875922935));
api.commit(swapChain);
api.endFrame(0);
api.stopCapture();
}
TEST_F(LoadImageTest, UpdateImage3D) {
@@ -558,6 +554,7 @@ TEST_F(LoadImageTest, UpdateImage3D) {
auto& api = getDriverApi();
Cleanup cleanup(api);
api.startCapture();
cleanup.addPostCall([&]() { api.stopCapture(); });
PixelDataFormat pixelFormat = PixelDataFormat::RGBA;
PixelDataType pixelType = PixelDataType::FLOAT;
@@ -628,8 +625,6 @@ TEST_F(LoadImageTest, UpdateImage3D) {
EXPECT_IMAGE(defaultRenderTarget,
ScreenshotParams(kTexSize, kTexSize, "UpdateImage3D", 1875922935));
}
api.stopCapture();
}
} // namespace test
+15 -16
View File
@@ -108,28 +108,27 @@ TEST_F(BackendTest, MRT) {
RenderPassParams params = getClearColorRenderPass();
params.viewport = getFullViewport();
Cleanup captureCleanup(api);
api.startCapture(0);
captureCleanup.addPostCall([&]() { api.stopCapture(0); });
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
{
RenderFrame frame(api);
// Draw a triangle.
api.beginRenderPass(renderTarget, params);
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(state);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
// Draw a triangle.
api.beginRenderPass(renderTarget, params);
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(state);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
api.flush();
api.commit(swapChain);
api.endFrame(0);
api.stopCapture(0);
api.flush();
api.commit(swapChain);
}
}
executeCommands();
}
} // namespace test
+3 -8
View File
@@ -56,8 +56,9 @@ using namespace filament::backend;
TEST_F(BackendTest, TextureViewLod) {
auto& api = getDriverApi();
api.startCapture(0);
Cleanup cleanup(api);
api.startCapture(0);
cleanup.addPostCall([&]() { api.stopCapture(0); });
// The test is executed within this block scope to force destructors to run before
// executeCommands().
@@ -123,7 +124,7 @@ TEST_F(BackendTest, TextureViewLod) {
TrianglePrimitive triangle(api);
api.beginFrame(0, 0, 0);
RenderFrame frame(api);
// We set the base mip to 1, and the max mip to 3
// Level 0: 128x128 (red)
@@ -211,15 +212,9 @@ TEST_F(BackendTest, TextureViewLod) {
api.endRenderPass();
api.commit(swapChain);
api.endFrame(0);
api.stopCapture(0);
}
api.finish();
executeCommands();
getDriver().purge();
}
} // namespace test
@@ -59,10 +59,11 @@ using namespace filament::backend;
* successfully.
*/
TEST_F(BackendTest, MissingRequiredAttributes) {
DriverApi& api = getDriverApi();
// The test is executed within this block scope to force destructors to run before
// executeCommands().
{
DriverApi& api = getDriverApi();
Cleanup cleanup(api);
// Create a platform-specific SwapChain and make it current.
auto swapChain = cleanup.add(createSwapChain());
@@ -86,9 +87,10 @@ TEST_F(BackendTest, MissingRequiredAttributes) {
params.viewport = getFullViewport();
api.startCapture(0);
cleanup.addPostCall([&]() { api.stopCapture(0); });
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
RenderFrame frame(api);
// Render a triangle.
api.beginRenderPass(defaultRenderTarget, params);
@@ -101,12 +103,7 @@ TEST_F(BackendTest, MissingRequiredAttributes) {
api.flush();
api.commit(swapChain);
api.endFrame(0);
api.stopCapture(0);
}
executeCommands();
}
} // namespace test
+5 -7
View File
@@ -111,12 +111,13 @@ TEST_F(BackendTest, PushConstants) {
auto& api = getDriverApi();
api.startCapture(0);
Cleanup cleanup(api);
// The test is executed within this block scope to force destructors to run before
// executeCommands().
{
Cleanup cleanup(api);
api.startCapture(0);
cleanup.addPostCall([&]() { api.stopCapture(0); });
// Create a SwapChain and make it current.
auto swapChain = cleanup.add(createSwapChain());
api.makeCurrent(swapChain, swapChain);
@@ -141,7 +142,7 @@ TEST_F(BackendTest, PushConstants) {
ps.rasterState.depthWrite = false;
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
RenderFrame frame(api);
api.beginRenderPass(renderTarget, params);
api.bindPipeline(ps);
@@ -183,10 +184,7 @@ TEST_F(BackendTest, PushConstants) {
"pushConstants", 3575588741));
api.commit(swapChain);
api.endFrame(0);
}
api.stopCapture(0);
}
} // namespace test
+22 -25
View File
@@ -288,7 +288,7 @@ TEST_F(ReadPixelsTest, ReadPixels) {
params.viewport.height = t.getRenderTargetSize();
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
RenderFrame frame (api);
// Render a white triangle over blue.
api.beginRenderPass(renderTarget, params);
@@ -350,11 +350,7 @@ TEST_F(ReadPixelsTest, ReadPixels) {
api.endRenderPass();
api.commit(swapChain);
api.endFrame(0);
}
// This ensures all driver commands have finished before exiting the test.
flushAndWait();
}
TEST_F(ReadPixelsTest, ReadPixelsPerformance) {
@@ -363,6 +359,7 @@ TEST_F(ReadPixelsTest, ReadPixelsPerformance) {
DriverApi& api = getDriverApi();
Cleanup cleanup(api);
cleanup.addPostCall([&]() { executeCommands(); });
// Create a platform-specific SwapChain and make it current.
auto swapChain = cleanup.add(
@@ -416,28 +413,29 @@ TEST_F(ReadPixelsTest, ReadPixelsPerformance) {
}
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
{
RenderFrame frame(api);
// Render some content, just so we don't read back uninitialized data.
api.beginRenderPass(renderTarget, params);
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(state);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
// Render some content, just so we don't read back uninitialized data.
api.beginRenderPass(renderTarget, params);
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(state);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
PixelBufferDescriptor descriptor(buffer, renderTargetSize * renderTargetSize * 4,
PixelDataFormat::RGBA, PixelDataType::UBYTE, 1, 0, 0, renderTargetSize,
[](void* buffer, size_t size, void* user) {
ReadPixelsTest* test = (ReadPixelsTest*)user;
test->readPixelsFinished = true;
}, this);
PixelBufferDescriptor descriptor(buffer, renderTargetSize * renderTargetSize * 4,
PixelDataFormat::RGBA, PixelDataType::UBYTE, 1, 0, 0, renderTargetSize,
[](void* buffer, size_t size, void* user) {
ReadPixelsTest* test = (ReadPixelsTest*) user;
test->readPixelsFinished = true;
}, this);
api.readPixels(renderTarget, 0, 0, renderTargetSize, renderTargetSize,
std::move(descriptor));
api.commit(swapChain);
api.endFrame(0);
api.readPixels(renderTarget, 0, 0, renderTargetSize, renderTargetSize,
std::move(descriptor));
api.commit(swapChain);
}
flushAndWait();
getDriver().purge();
@@ -448,7 +446,6 @@ TEST_F(ReadPixelsTest, ReadPixelsPerformance) {
free(buffer);
api.finish();
executeCommands();
}
} // namespace test
@@ -81,31 +81,31 @@ TEST_F(BackendTest, RenderExternalImageWithoutSet) {
DescriptorSetHandle descriptorSet = shader.createDescriptorSet(api);
api.startCapture(0);
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
{
Cleanup cleanupCapture(api);
api.startCapture(0);
cleanup.addPostCall([&]() { api.stopCapture(0); });
api.makeCurrent(swapChain, swapChain);
{
RenderFrame frame(api);
api.updateDescriptorSetTexture(descriptorSet, 0, texture, {});
api.bindDescriptorSet(descriptorSet, 0, {});
api.updateDescriptorSetTexture(descriptorSet, 0, texture, {});
api.bindDescriptorSet(descriptorSet, 0, {});
// Render a triangle.
api.beginRenderPass(defaultRenderTarget, params);
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(state);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
api.flush();
api.commit(swapChain);
api.endFrame(0);
api.stopCapture(0);
// Render a triangle.
api.beginRenderPass(defaultRenderTarget, params);
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(state);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
api.flush();
api.commit(swapChain);
}
}
api.finish();
executeCommands();
}
TEST_F(BackendTest, RenderExternalImage) {
@@ -180,32 +180,32 @@ TEST_F(BackendTest, RenderExternalImage) {
RenderPassParams params = getClearColorRenderPass();
params.viewport = getFullViewport();
api.startCapture(0);
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
{
Cleanup cleanupCapture(api);
api.startCapture(0);
cleanup.addPostCall([&]() { api.stopCapture(0); });
api.makeCurrent(swapChain, swapChain);
{
RenderFrame frame(api);
api.updateDescriptorSetTexture(descriptorSet, 0, texture, {});
api.bindDescriptorSet(descriptorSet, 0, {});
api.updateDescriptorSetTexture(descriptorSet, 0, texture, {});
api.bindDescriptorSet(descriptorSet, 0, {});
// Render a triangle.
api.beginRenderPass(defaultRenderTarget, params);
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(state);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
api.flush();
api.commit(swapChain);
api.endFrame(0);
EXPECT_IMAGE(defaultRenderTarget,
ScreenshotParams(screenWidth(), screenHeight(), "RenderExternalImage", 1206264951));
api.stopCapture(0);
api.finish();
flushAndWait();
// Render a triangle.
api.beginRenderPass(defaultRenderTarget, params);
state.primitiveType = PrimitiveType::TRIANGLES;
state.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(state);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
api.flush();
api.commit(swapChain);
}
EXPECT_IMAGE(defaultRenderTarget,
ScreenshotParams(screenWidth(), screenHeight(), "RenderExternalImage", 1206264951));
}
}
} // namespace test
+6 -12
View File
@@ -43,8 +43,9 @@ TEST_F(BackendTest, ScissorViewportRegion) {
constexpr int kSrcRtWidth = 384;
constexpr int kSrcRtHeight = 384;
api.startCapture(0);
Cleanup cleanup(api);
api.startCapture(0);
cleanup.addPostCall([&]() { api.stopCapture(0); });
// color texture (mip level 1) 512x512 depth texture (mip level 0) 512x512
// +----------------------------------------+ +------------------------------------------+
@@ -121,7 +122,7 @@ TEST_F(BackendTest, ScissorViewportRegion) {
shader.addProgramToPipelineState(ps);
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
RenderFrame frame(api);
api.beginRenderPass(srcRenderTarget, params);
api.scissor(scissor);
@@ -136,9 +137,6 @@ TEST_F(BackendTest, ScissorViewportRegion) {
ScreenshotParams(kSrcTexWidth >> 1, kSrcTexHeight >> 1, "scissor", 15842520));
api.commit(swapChain);
api.endFrame(0);
api.stopCapture(0);
}
}
@@ -146,12 +144,11 @@ TEST_F(BackendTest, ScissorViewportRegion) {
TEST_F(BackendTest, ScissorViewportEdgeCases) {
auto& api = getDriverApi();
api.startCapture(0);
Cleanup cleanup(api);
// The test is executed within this block scope to force destructors to run before
// executeCommands().
{
Cleanup cleanup(api);
api.startCapture(0);
// 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));
api.makeCurrent(swapChain, swapChain);
@@ -201,7 +198,7 @@ TEST_F(BackendTest, ScissorViewportEdgeCases) {
shader.addProgramToPipelineState(ps);
api.makeCurrent(swapChain, swapChain);
api.beginFrame(0, 0, 0);
RenderFrame frame(api);
api.beginRenderPass(renderTarget, params);
api.scissor(scissor);
@@ -228,9 +225,6 @@ TEST_F(BackendTest, ScissorViewportEdgeCases) {
ScreenshotParams(512, 512, "ScissorViewportEdgeCases", 2199186852));
api.commit(swapChain);
api.endFrame(0);
api.stopCapture(0);
}
}
+31 -40
View File
@@ -89,7 +89,7 @@ public:
ps.stencilState.front.stencilOpDepthStencilPass = StencilOperation::INCR;
api.makeCurrent(mSwapChain, mSwapChain);
api.beginFrame(0, 0, 0);
RenderFrame frame(api);
api.beginRenderPass(renderTarget, params);
ps.primitiveType = PrimitiveType::TRIANGLES;
@@ -117,7 +117,6 @@ public:
api.endRenderPass();
api.commit(mSwapChain);
api.endFrame(0);
}
void TearDown() override {
@@ -146,9 +145,6 @@ TEST_F(BasicStencilBufferTest, StencilBuffer) {
EXPECT_IMAGE(renderTarget,
ScreenshotParams(screenWidth(), screenHeight(), "StencilBuffer", 0x3B1AEF0F));
flushAndWait();
getDriver().purge();
}
TEST_F(BasicStencilBufferTest, DepthAndStencilBuffer) {
@@ -172,9 +168,6 @@ TEST_F(BasicStencilBufferTest, DepthAndStencilBuffer) {
EXPECT_IMAGE(renderTarget,
ScreenshotParams(screenWidth(), screenHeight(), "DepthAndStencilBuffer", 0x3B1AEF0F));
flushAndWait();
getDriver().purge();
}
TEST_F(BasicStencilBufferTest, StencilBufferMSAA) {
@@ -204,6 +197,7 @@ TEST_F(BasicStencilBufferTest, StencilBufferMSAA) {
{ depthStencilTextureMSAA }, { depthStencilTextureMSAA }));
api.startCapture(0);
cleanup.addPostCall([&]() { api.stopCapture(0); });
// We'll be using a triangle as geometry.
TrianglePrimitive smallTriangle(api);
@@ -233,44 +227,41 @@ TEST_F(BasicStencilBufferTest, StencilBufferMSAA) {
ps.stencilState.front.stencilOpDepthStencilPass = StencilOperation::INCR;
api.makeCurrent(mSwapChain, mSwapChain);
api.beginFrame(0, 0, 0);
{
RenderFrame frame(api);
api.beginRenderPass(renderTarget0, params);
ps.primitiveType = PrimitiveType::TRIANGLES;
ps.vertexBufferInfo = smallTriangle.getVertexBufferInfo();
api.bindPipeline(ps);
api.bindRenderPrimitive(smallTriangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
api.beginRenderPass(renderTarget0, params);
ps.primitiveType = PrimitiveType::TRIANGLES;
ps.vertexBufferInfo = smallTriangle.getVertexBufferInfo();
api.bindPipeline(ps);
api.bindRenderPrimitive(smallTriangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
// Step 2: Render a larger triangle with the stencil test enabled.
params.flags.clear = TargetBufferFlags::COLOR0;
params.flags.discardStart = TargetBufferFlags::COLOR0;
params.flags.discardEnd = TargetBufferFlags::STENCIL;
params.clearColor = math::float4(0.0f, 0.0f, 1.0f, 1.0f);
ps.rasterState.colorWrite = true;
ps.stencilState.stencilWrite = false;
ps.stencilState.front.stencilOpDepthStencilPass = StencilOperation::KEEP;
ps.stencilState.front.stencilFunc = StencilState::StencilFunction::E;
ps.stencilState.front.ref = 0u;
// Step 2: Render a larger triangle with the stencil test enabled.
params.flags.clear = TargetBufferFlags::COLOR0;
params.flags.discardStart = TargetBufferFlags::COLOR0;
params.flags.discardEnd = TargetBufferFlags::STENCIL;
params.clearColor = math::float4(0.0f, 0.0f, 1.0f, 1.0f);
ps.rasterState.colorWrite = true;
ps.stencilState.stencilWrite = false;
ps.stencilState.front.stencilOpDepthStencilPass = StencilOperation::KEEP;
ps.stencilState.front.stencilFunc = StencilState::StencilFunction::E;
ps.stencilState.front.ref = 0u;
api.beginRenderPass(renderTarget1, params);
ps.primitiveType = PrimitiveType::TRIANGLES;
ps.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(ps);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
api.commit(mSwapChain);
api.stopCapture(0);
api.endFrame(0);
api.beginRenderPass(renderTarget1, params);
ps.primitiveType = PrimitiveType::TRIANGLES;
ps.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(ps);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
api.commit(mSwapChain);
api.stopCapture(0);
}
EXPECT_IMAGE(renderTarget1, ScreenshotParams(screenWidth(), screenHeight(),
"StencilBufferAutoResolve", 3353562179));
flushAndWait();
getDriver().purge();
}
} // namespace test
-7
View File
@@ -1165,30 +1165,23 @@ bool MaterialBuilder::generateShaders(JobSystem& jobSystem, const std::vector<Va
std::sort(metalEntries.begin(), metalEntries.end(), compare);
std::sort(wgslEntries.begin(), wgslEntries.end(), compare);
utils::slog.e <<"----------------------- a" << utils::io::endl;
// Generate the dictionaries.
for (const auto& s : glslEntries) {
textDictionary.addText(s.shader);
}
utils::slog.e <<"----------------------- b" << utils::io::endl;
for (const auto& s : essl1Entries) {
textDictionary.addText(s.shader);
}
utils::slog.e <<"----------------------- c" << utils::io::endl;
for (auto& s : spirvEntries) {
std::vector const spirv{ std::move(s.data) };
s.dictionaryIndex = spirvDictionary.addBlob(spirv);
}
utils::slog.e <<"----------------------- d" << utils::io::endl;
for (const auto& s : metalEntries) {
textDictionary.addText(s.shader);
}
utils::slog.e <<"----------------------- e" << utils::io::endl;
for (const auto& s : wgslEntries) {
textDictionary.addText(s.shader);
}
utils::slog.e <<"----------------------- f" << utils::io::endl;
// Emit dictionary chunk (TextDictionaryReader and DictionaryTextChunk)
const auto& dictionaryChunk = container.push<DictionaryTextChunk>(
+3 -68
View File
@@ -30,34 +30,13 @@
#include <string_view>
#include <utility>
#include <vector>
#include <algorithm>
namespace filamat {
namespace {
bool isWordChar(char const c) { return std::isalnum(c) || c == '_'; }
// Function to trim specified characters from both ends of a string_view
std::string_view trim(std::string_view sv, std::string_view chars_to_trim = " \t\r\n") {
// Find the first character that is not in our set of trim characters
const auto start = sv.find_first_not_of(chars_to_trim);
// If no such character exists, the string is empty or all trim characters
if (start == std::string_view::npos) {
return ""; // Return an empty view
bool isWordChar(char const c) {
return std::isalnum(c) || c == '_';
}
// Find the last character that is not in our set of trim characters
const auto end = sv.find_last_not_of(chars_to_trim);
// Calculate the length of the new view
const auto len = end - start + 1;
// Return a new view representing the trimmed string
return sv.substr(start, len);
}
} // anonymous namespace
LineDictionary::LineDictionary() = default;
@@ -85,15 +64,10 @@ std::vector<LineDictionary::index_t> LineDictionary::getIndices(
}
void LineDictionary::addText(std::string_view const text) noexcept {
utils::slog.e <<"begin addText l(" << text.length() << ")=" <<
trim(text.substr(0, std::min(15, (int)text.length())));
size_t cur = 0;
size_t const len = text.length();
const char* s = text.data();
size_t trueCount = 0;
while (cur < len) {
trueCount++;
// Start of the current line
size_t const pos = cur;
// Find the end of the current line or end of text
@@ -105,22 +79,12 @@ void LineDictionary::addText(std::string_view const text) noexcept {
cur++;
}
addLine({ s + pos, cur - pos });
if (trueCount > 1000000 && trueCount > 0) {
utils::slog.e <<"print addText at=" << trim(text.substr(0, std::min(15, (int)text.length()))) <<
" count=" << trueCount << " cur=" << cur << utils::io::endl;
trueCount = 0;
}
}
utils::slog.e <<" ----- end addText" << utils::io::endl;
}
void LineDictionary::addLine(std::string_view const line) noexcept {
utils::slog.e <<"********** begin addline l(" << line.length() << ")=" <<
trim(line.substr(0, std::min(15, (int)line.length())));
auto const lines = splitString(line);
for (std::string_view const& subline : lines) {
utils::slog.e <<"addline indices=" << mLineIndices.size() << utils::io::endl;
// Never add a line twice.
auto pos = mLineIndices.find(subline);
if (pos != mLineIndices.end()) {
@@ -133,7 +97,6 @@ void LineDictionary::addLine(std::string_view const line) noexcept {
.index = index_t(mStrings.size() - 1),
.count = 1 });
}
utils::slog.e <<"*********** end addline" << utils::io::endl;
}
std::string_view LineDictionary::ltrim(std::string_view s) {
@@ -147,26 +110,14 @@ std::pair<size_t, size_t> LineDictionary::findPattern(
// Patterns are ordered from longest to shortest to ensure correct prefix matching.
static constexpr std::string_view kPatterns[] = { "hp_copy_", "mp_copy_", "_" };
utils::slog.e <<"begin findPattern l(" << line.length() << ")=" <<
trim(line.substr(0, std::min(15, (int)line.length())));
if (line.length() == 2) {
utils::slog.e <<"begin findPattern l[0]=" << (int) line[0] << " l[1]=" << (int) line[1] <<
utils::io::endl;
}
size_t trueCount = 1;
const size_t line_len = line.length();
for (size_t i = offset; i < line_len; ++i, ++trueCount) {
for (size_t i = offset; i < line_len; ++i) {
// A pattern must be a whole word (or at the start of the string).
if (i > 0 && isWordChar(line[i - 1])) {
continue;
}
for (const auto& prefix : kPatterns) {
trueCount++;
if (line.size() - i >= prefix.size() && line.substr(i, prefix.size()) == prefix) {
// A known prefix has been matched. Now, check for a sequence of digits.
size_t const startOfDigits = i + prefix.size();
@@ -190,19 +141,11 @@ std::pair<size_t, size_t> LineDictionary::findPattern(
break;
}
}
if (trueCount % 10000 == 0 && trueCount > 0) {
utils::slog.e <<"print findPattern at=" << trim(line) <<
" count=" << trueCount << utils::io::endl;
}
}
utils::slog.e <<" ----- end findPattern" << utils::io::endl;
return { std::string_view::npos, 0 }; // No pattern found
}
std::vector<std::string_view> LineDictionary::splitString(std::string_view const line) {
utils::slog.e <<"begin splitString l=" << trim(line.substr(0,
std::min(15, (int)line.length())));
std::vector<std::string_view> result;
size_t current_pos = 0;
@@ -211,9 +154,7 @@ std::vector<std::string_view> LineDictionary::splitString(std::string_view const
return result;
}
size_t trueCount = 0;
while (current_pos < line.length()) {
trueCount++;
auto const [match_pos, match_len] = findPattern(line, current_pos);
if (match_pos == std::string_view::npos) {
@@ -232,13 +173,7 @@ std::vector<std::string_view> LineDictionary::splitString(std::string_view const
// Move cursor past the match.
current_pos = match_pos + match_len;
if (trueCount % 10000 == 0 && trueCount > 0) {
utils::slog.e <<"print splitString at=" << trim(line) <<
" count=" << trueCount << utils::io::endl;
}
}
utils::slog.e <<"------- end splitString" << utils::io::endl;
return result;
}
+1 -1
View File
@@ -76,7 +76,7 @@ bool MaterialCompiler::run(const matp::Config& config) {
return true;
}
JobSystem js(1);
JobSystem js;
js.adopt();
// Write builder.build() to output.